Instant coffee
56 個 MIR-DRIFT spectra;Arabica vs Robusta。最適合第一個示範,資料小、類別清楚。
classificationcoffee authentication這一章會帶你把一維食品 FTIR 光譜整理成 sample × wavenumber 矩陣,先做 PCA / PLS-DA 的基礎判別,再進一步把光譜轉成 two-trace 2D correlation maps,作為機器學習或影像模型的輸入。
資料來源是 Data for: Spectra Data Classification with Kernel Extreme Learning。它不是專門為 2T2D-COS 建立的資料集,但因為每個樣本都有完整 FTIR spectrum 與類別標籤,非常適合用來教「從一維光譜生成 2D correlation image」。
56 個 MIR-DRIFT spectra;Arabica vs Robusta。最適合第一個示範,資料小、類別清楚。
classificationcoffee authentication120 spectra,來自 60 個 extra virgin olive oils,每個樣本有 duplicate acquisition。適合討論 replicate 與產地判別。
replicatesorigin983 個 ATR-FTIR spectra;Strawberry vs non-strawberry / adulterated puree。適合進階分類與 spectral-image model。
adulterationlarge n以下圖形直接由 Mendeley coffee FTIR CSV 重新計算產生,不是示意圖。可作為參考答案,回到 Python / Orange 重算即可確認是否得到相同趨勢。




已完成的一維 FTIR × PCA / PLS-DA 教學頁,可作為本頁 2T2D-COS 前的 baseline:先看原始 FTIR 光譜如何做 Arabica vs Robusta 鑑別,再比較 2T2D 影像化後多了哪些 band-to-band pattern。
GC 層析指紋同樣用 Arabica vs Robusta 作為食品真偽案例。可拿來對照:FTIR 是官能基 / 整體組成 fingerprint;GC 是揮發性或層析峰型 fingerprint,兩者的化學選擇性與特徵解釋方式不同。
R 套件 pgmm 的經典 coffee 資料(Streuli:43 支咖啡 × 12 項化學/物理成分,Arabica vs Robusta),以 model-based clustering(PGMM)分群。與本頁互補:FTIR 是光譜指紋,PGMM 是化學成分變數+機率式分群,可比較「光譜 vs 成分」「判別 vs 非監督分群」的差異。
同樣是 coffee authentication,FTIR、GC、PGMM 成分、2T2D-COS 分別回答不同問題:FTIR 看官能基區域,GC 看分離後峰型,PGMM 看化學成分的機率式分群,2T2D-COS 看光譜區域之間的相關圖樣。可比較哪一種最容易解釋、哪一種最適合快速篩檢。

同一批 Arabica / Robusta 咖啡,FTIR 鑑別課用 R 套件 pgmm 的 coffee 真實化學成分(43 支、12 項)做 PCA,Arabica / Robusta 一樣自己分兩群(PC1 37.7%)。把「量到的成分差異」對到「它的 FTIR 吸收帶」,再對到本頁的 FTIR 差異光譜(圖 4)與 2D 圖(圖 5):
原始 FTIR 是一條曲線:每個 wavenumber 對應一個吸收強度。2D 相關把光譜轉成 wavenumber × wavenumber 的 correlation map,呈現哪些譜帶一起變動。
只有「樣本 vs 類別平均」兩條光譜時,同步圖其實就是差異光譜的外積(rank-1),資訊和 1D 差異相同。要看到「1D 看不到的 band covariation」,需要一系列隨擾動(烘焙時間、摻配比例…)變化的光譜做廣義 2D-COS。
下面程式下載 Mendeley zip、讀取 coffee FTIR CSV、整理成 X: samples × wavenumbers,做 SNV 前處理,再用與 SpectraView 一致的 Noda 公式算真 2T2D 與廣義 2D-COS(不是會讓異步退化為 0 的減平均版)。
import io, zipfile, requests
import numpy as np
import pandas as pd
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.svm import SVC
url = "https://data.mendeley.com/public-api/zip/frrv2yd9rg/download/1"
r = requests.get(url, headers={"User-Agent": "Mozilla/5.0"})
r.raise_for_status()
z = zipfile.ZipFile(io.BytesIO(r.content))
coffee_file = [f for f in z.namelist() if "instant_coffee" in f and f.endswith(".csv")][0]
raw = pd.read_csv(z.open(coffee_file), header=None)
sample_id = raw.iloc[0, 1:].astype(str).to_numpy()
group_code = raw.iloc[1, 1:].astype(str).to_numpy()
y = raw.iloc[2, 1:].astype(str).to_numpy() # Arabica / Robusta
wavenumber = pd.to_numeric(raw.iloc[3:, 0]).to_numpy()
X = raw.iloc[3:, 1:].T.astype(float).to_numpy() # samples × wavenumbers
print(X.shape, y[:5], wavenumber.min(), wavenumber.max())
Xz = StandardScaler().fit_transform(X)
pca = PCA(n_components=2).fit_transform(Xz)
clf = SVC(kernel="linear")
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=7)
scores = cross_val_score(clf, Xz, y, cv=cv)
print("CV accuracy:", scores.mean(), scores.std())# Noda 2018 two-trace 2D correlation.
# 不要對兩條光譜減平均:只有兩條時減平均會得到 ya-yb 與 yb-ya,
# 使異步圖恆為 0、同步圖塌成 1D 差異光譜的外積(rank-1)。
# 另外避免變數名用 async(Python 3.7+ 是保留字)。
def two_trace_2dcos(ya, yb):
sync = 0.5 * (np.outer(ya, ya) + np.outer(yb, yb))
asyn = 0.5 * (np.outer(ya, yb) - np.outer(yb, ya))
return sync, asyn
# SNV 前處理,再算兩類平均的真 2T2D
Xs = (X - X.mean(1, keepdims=True)) / X.std(1, keepdims=True)
arab, robu = Xs[y == "Arabica"].mean(0), Xs[y == "Robusta"].mean(0)
sync, asyn = two_trace_2dcos(arab, robu)
ratio = np.abs(asyn).max() / np.abs(sync).max()
print(sync.shape, "max|async|/max|sync| =", round(ratio, 3)) # ~0.15 非退化# 廣義 2D-COS:給一條「隨擾動變化」的光譜序列 M (m_spectra × n_wavenumbers)。
def noda_matrix(m):
N = np.zeros((m, m))
for j in range(m):
for k in range(m):
if j != k:
N[j, k] = 1.0 / (np.pi * (k - j))
return N
def generalized_2dcos(M, reference="mean"):
D = M - M.mean(axis=0) if reference == "mean" else M
m = D.shape[0]
sync = D.T @ D / (m - 1)
asyn = D.T @ (noda_matrix(m) @ D) / (m - 1)
return sync, asyn
# 把不同烘焙時間 / 摻配比例的光譜疊成 M,再 sync, asyn = generalized_2dcos(snv(M))。
# Arabica/Robusta 分類沒有這種擾動軸,因此只能做兩跡 2T2D;
# 要展現 band-to-band covariation 與先後順序,請改用真正的擾動序列。除了 Python 版 2T2D-COS,Orange Data Mining 的 Spectroscopy add-on 也有 2D Correlation Plot widget。建議先用 Orange 做互動式探索,看懂「一維光譜如何變成二維相關圖」;再用 Python 重現流程、輸出圖檔與建立機器學習特徵。

Orange 適合入門與互動探索:拖拉 widget、調整前處理、立即觀察波數 × 波數的相關圖樣,看出哪些 fingerprint 區域會一起變化。
Python 適合可重現分析:可固定 reference trace、批次輸出 synchronous / asynchronous maps、產生 flattened features,進一步接 SVM、Random Forest 或 CNN。
Orange 的 2D Correlation Plot 是視覺探索工具;若要做嚴格模型評估,reference spectrum 與 class mean 必須在訓練 fold 內計算,避免把測試資料洩漏進模型。
為什麼不能把 duplicate acquisition 當成完全獨立樣本來隨機切分 train/test?
class mean spectrum 當 reference trace 有什麼優點?有什麼可能造成資料洩漏的風險?
2T2D synchronous map 與原始一維 spectrum 相比,多提供了哪一類資訊?
如果換成 fruit puree dataset,Strawberry vs non-strawberry 的模型評估應該看 accuracy 之外的哪些指標?