食品營養系 · 儀器分析入門到進階

波峰疊在一起,怎麼辦?

從讀懂一張層析圖開始,一路走到用 R 語言的 RChromOptim 套件, 自動算出「最省時間又分得最開」的 HPLC 分析條件。 全程不需要你先懂物理化學——我們用滑桿、比喻和實際的食品檢驗例子把它講清楚。

教材依據:Zisi, Ch., Pappa-Louisi, A., & Nikitas, P. (2020). Separation optimization in HPLC analysis implemented in R programming language. Journal of Chromatography A, 1617, 460823.
使用檔案:RChromOptim.RData(R 函數)、Data.xlsx(範例數據)、Text files/(輸入檔範例)
建議時間:入門 40 分鐘 / 中階 50 分鐘 / 進階實作 90 分鐘 · 提示:本頁所有圖都可以拖動滑桿即時互動,請務必動手玩。

這份教材怎麼讀

左側目錄把內容分成三層,你可以照順序讀,也可以直接跳到需要的地方:

  • 入門 完全沒碰過 HPLC 也看得懂。學會讀懂一張層析圖。
  • 中階 理解為什麼調整條件能改變分離結果,以及背後那一條簡單的公式。
  • 進階 實際打開 R,用論文的套件跑完整套最佳化流程。

1.一個你真的會遇到的問題入門

先講一個情境。理解了這個情境,後面所有的數學才有意義。

你在食品分析實驗室工作,桌上有一瓶市售的機能性飲料。品管主管要你回答一件事: 這瓶飲料裡的咖啡因有沒有超過標示?防腐劑(苯甲酸、己二烯酸)有沒有超過法規上限? (己二烯酸即 sorbic acid,我國《食品添加物使用範圍及限量暨規格標準》採此名稱,部分教科書譯作「山梨酸」。)

你把樣品打進 HPLC(高效液相層析儀,High-Performance Liquid Chromatography), 機器跑了十分鐘,吐出一張圖。你期待看到三個漂亮、分得開的波峰, 每個波峰代表一種成分,峰的面積就是它的含量。

結果你看到的是這樣:

情境示範:三種成分擠在一起 按下按鈕看兩種結果的差別
選擇分析條件:

在「沒調好」的那張圖裡,咖啡因和苯甲酸的波峰疊在一起。 這不是小事——你沒辦法量出任何一個的正確面積,也就算不出正確的含量。 在檢驗報告上,這代表數據不能用,整批樣品要重跑。

這個出峰順序是真的嗎?

是。在最常見的分析條件下——逆相 C18 管柱、酸性移動相(磷酸鹽緩衝液 pH 約 3.5–4.5)搭配甲醇或乙腈—— 文獻報告的沖提順序就是咖啡因 → 苯甲酸 → 己二烯酸。 Kritsunankul 與 Jakmunee(2011)分析軟性飲料中五種添加物時,明確記錄順序為 醋磺內酯鉀 → 糖精 → 咖啡因 → 苯甲酸 → 己二烯酸,全程約 14 分鐘; Lino 等人(2010)分析葡萄牙市售飲料也採用同一類條件(C18、KH₂PO₄/乙腈 90:10、pH 4.2、220 nm)。

為什麼咖啡因最先出來?因為它的親脂性最低(log P ≈ −0.07), 最不愛黏 C18;苯甲酸(log P ≈ 1.87)和己二烯酸(log P ≈ 1.33)都比它疏水得多,所以留得比較久。 這也解釋了為什麼咖啡因和苯甲酸這一對特別容易擠在一起—— 它們剛好卡在「一個不太黏、一個開始黏」的交界,正是本教材要處理的典型問題。

順序不是固定的:pH 會改寫它

苯甲酸(pKa ≈ 4.2)和己二烯酸(pKa ≈ 4.8)都是弱酸。 移動相偏酸時它們維持中性、乖乖黏在管柱上;一旦 pH 升高、它們解離成帶負電的離子, 就會大幅提早沖出來。咖啡因則幾乎不受影響(它在 pH 2 以上都是中性)。 所以把 pH 調高,這兩根酸的峰會往左跑,順序真的可能翻轉—— 這正是文獻方法幾乎都刻意把 pH 壓在 4 以下的原因:不是習慣,是為了讓滯留穩定、順序可重現。 (想控制 pH 來分離可解離化合物?那就要用附錄的模型 7、8、10,而不是模型 1。)

生活類比:合唱團裡的獨唱

想像三個人同時唱歌。如果他們的聲音重疊在一起,你只聽得到「一團聲音」, 沒辦法判斷誰唱得大聲、誰唱得小聲。 但如果讓他們一個接一個、中間留空隙唱,你就能清楚評價每一個人。

HPLC 做的就是這件事:把混在一起的成分,在時間上排隊,一個一個送到偵測器面前。 這份教材要教你的,就是怎麼安排這個隊伍,讓大家不要擠在一起,又不要排太久。

為什麼不能「排很久就好」?

你當然可以讓每個成分間隔十分鐘慢慢出來,保證分得開。但一批樣品可能有 60 瓶, 每瓶多跑 30 分鐘就是多 30 小時,還要多燒掉好幾公升的溶劑(多半是甲醇或乙腈,有毒且昂貴)。 所以真正的目標是「在可接受的時間內,把最難分的那一對分開」—— 這正是「最佳化(optimization)」這個詞的意思,也是這整篇論文在做的事。

2.HPLC 到底在做什麼入門

如果你把 HPLC 想成一場賽跑,幾乎所有名詞都會瞬間變得直觀。

三個角色

HPLC 的東西賽跑的比喻它實際在做什麼
管柱
column
一條很長、佈滿黏膠的跑道 一根裝滿多孔顆粒的不鏽鋼管(通常 5–25 公分)。顆粒表面接了一層油性的碳鏈(C18),會「黏住」樣品分子。
移動相
mobile phase
從後面推選手的水流 幫浦持續打進去的液體,通常是水 + 有機溶劑(甲醇或乙腈)的混合液,把樣品往前推。
溶質
solute / analyte
選手 你要測的成分本身:咖啡因、苯甲酸、維生素 C……。論文裡一律叫「solute」。

比賽怎麼進行

每一個分子在管柱裡不斷重複兩件事:被黏在顆粒表面上(不動), 和溶在液體裡被水流帶著走(前進)。

關鍵直覺

一個分子「黏著的時間佔比越高,就跑得越慢,越晚出來」。

咖啡因偏油性,比較愛黏在 C18 上 → 跑得慢 → 晚出來。
維生素 C 很親水,幾乎不黏 → 幾乎被水流直接沖走 → 很早出來。
所以不同成分會在不同時間抵達終點,這就是分離。

管柱的另一端有一個偵測器(detector),通常是紫外光偵測器。 它像終點線的攝影機,每一瞬間都記錄「現在流過去的液體裡有多少東西」。 把這個訊號對時間畫成一條線,就是層析圖(chromatogram)—— 本教材從頭到尾都在看的那張圖。

3.讀懂一張層析圖入門

一張層析圖只有兩個軸,但藏了三種資訊。看懂這三種,你就看懂了八成。

層析圖解剖 把滑鼠移到圖上任一個波峰,看它代表什麼
標示:
你看到的它告訴你用途
波峰的位置(橫軸) 是什麼成分 拿標準品跑一次,記下它出現的時間。之後樣品在同一時間出現波峰 → 大概就是它(定性)。
波峰的面積 有多少 面積和濃度成正比。做一條標準曲線,就能算出樣品濃度(定量)。
波峰的寬度與間隔 這個方法好不好 峰又窄又分得開 → 面積量得準。峰又寬又重疊 → 數據不可信。這就是本教材的主角。
為什麼波峰是「山形」而不是一根線?

同一種成分的幾百萬個分子,不會剛好同時抵達終點。有的分子運氣好走中間水道,早一點到; 有的多黏了幾次,晚一點到。整體分布就形成一個鐘形(高斯,Gaussian)曲線。

而且走得越久的成分,峰越寬——因為它有更多時間讓這些差異累積開來。 這件事很重要:它是後面「梯度沖提」存在的理由,論文的 fitshape 函數就是專門在描述這個現象。

真實的層析圖長什麼樣實測資料

上面那張是畫出來的示意圖。真實儀器吐出來的,其實沒那麼乾淨。 下面三張是本校實驗室用 Shimadzu HPLC 實際跑出來的資料 (UV 230 nm、10 分鐘、每 0.5 秒取一點,共 1201 個點), 分析的正是苯甲酸與己二烯酸——第 1 節那個飲料情境的主角。

真實層析圖 Shimadzu LCsolution 原始資料 · 苯甲酸 + 己二烯酸 · 230 nm
切換樣品:

三件示意圖沒告訴你的事

一、前面那一大坨不是你的樣品

每一張圖在 1–2.5 分鐘都有一團又高又亂的訊號,甚至會衝到負值。 那是溶劑鋒(solvent front)——注射時跟著進去的溶劑、鹽類、還有食品基質裡 所有不被管柱滯留的東西,全部擠在死時間附近一起沖出來。

這正是第 4 節講 t0 時的實際後果:t0 附近的區域基本上不能用。 你的目標物必須被滯留到夠後面,才躲得開這團東西。 在「真實食品樣品」那張圖上特別明顯——基質訊號衝到 330 mV,比目標物還高。

二、峰不是對稱的

儀器報告的拖尾因子(tailing factor)是 1.17–1.31,代表峰的右半邊比左半邊胖一點。 完美對稱是 1.00。本教材的模擬器為了教學清楚,假設的是對稱的高斯峰—— 這是一個簡化。真實的峰會拖尾,論文的 fitshape 也只處理對稱峰, 作者自己在結論裡就說了,加入拖尾峰模型是後續可以擴充的方向。

三、同一支方法,濃度差 200 倍,出峰位置幾乎不動

0.25 ppm 和 50 ppm 差了 200 倍,但苯甲酸都在 5.37 分鐘出來、己二烯酸都在 5.94 分鐘。 滯留時間由化學性質決定,不受濃度影響——這就是為什麼可以拿滯留時間來「認人」(定性), 拿面積來「數量」(定量)。第 3 節那張表講的原則,在這裡看得到真憑實據。

4.三個一定要記住的數字入門

整篇論文的公式都建立在這三個量上面。它們其實都只是「時間」的不同講法。

t₀ — 死時間(column dead time)

一個完全不黏管柱的分子,從打進去到流出來需要的時間。 它等於管柱裡液體的體積除以流速,代表「不可能比這更快」的下限。 一般分析管柱大約是 1 分鐘左右。在論文所有函數裡它叫 t0,是你必須自己量、自己輸入的參數。

tR — 滯留時間(retention time)

某個成分的波峰最高點出現的時間。這是你在層析圖上直接讀到的數字。 論文中寫作 tR。

k — 滯留因子(retention factor)

這是最關鍵、也最常被搞混的一個。它的定義是:

(定義式) k = ( tR − t0 ) ⁄ t0 也可以反過來寫成後面會一直用到的形式: tR = t0 ( 1 + k )
k 是什麼意思?用白話說

tR − t0 是這個分子「比不黏的分子多花的時間」, 也就是它黏在管柱上的總時間。除以 t0 之後, k 就變成:

k = 黏著的時間 ÷ 流動的時間

k = 0 → 完全不黏,跟溶劑一起沖出來。
k = 1 → 黏著和流動各花一半時間,tR 是 t0 的 2 倍。
k = 10 → 黏著時間是流動時間的 10 倍,tR 是 t0 的 11 倍。

為什麼要多發明一個 k,直接用 tR 不好嗎?

因為 tR 會隨著儀器改變:換一根長一點的管柱、把流速調快, 所有 tR 都會跟著變,可是「這個分子有多愛黏管柱」其實沒變。

k 把管柱長度和流速的影響除掉了,剩下的是分子本身的化學性質。 所以所有的理論模型都用 k 來寫,不用 tR—— 包括論文 Table 2 的十個模型,全部都是在描述 k。 算出 k 之後,再用 tR = t0(1+k) 換回你看得到的時間。

隨堂測驗
某支管柱的 t₀ = 1.2 分鐘。你在 pH 3.0 的移動相下打進己二烯酸標準品, 波峰最高點出現在 4.8 分鐘。己二烯酸的滯留因子 k 是多少?
k = (tR − t₀) / t₀ = (4.8 − 1.2) / 1.2 = 3.6 / 1.2 = 3.0
意思是:己二烯酸黏在管柱上的時間,是它隨著液體流動時間的 3 倍。
題目特別註明「pH 3.0」是有意義的——己二烯酸是弱酸,換一個 pH 這個 k 就會變。 為什麼會變、變多少,第 10 節會用一個模擬器讓你親眼看到。
常見錯誤:選 4.0 是忘了先減 t₀(直接 4.8/1.2);選 3.6 是減了但忘了除。

5.解析度 Rs:這份教材的主角入門

前面說「波峰要分得開」,但「分得開」不能只憑感覺——需要一個數字。這個數字就是解析度。

解析度(resolution,記作 Rs)衡量相鄰兩個波峰分離得多好。定義是:

Rs = 2 ( tR2 − tR1 ) ⁄ ( w1 + w2 ) tR1、tR2:兩個波峰的滯留時間 · w1、w2:兩個波峰在基線上的寬度

這個式子在講一件很單純的事:「兩個峰離多遠」除以「兩個峰本身有多胖」。 分子越大(離得遠)或分母越小(峰越瘦),解析度就越好。

親手感受 Rs 拖動滑桿改變兩個峰的距離和寬度,看 Rs 怎麼變
相當於調整選擇性——讓兩個成分的化學行為差更多
相當於管柱效率——好的管柱、短的滯留時間會讓峰更瘦
解析度 Rs
1.50
重疊面積
0.3%
定量誤差(估)
<1%

要記住的三個門檻

Rs看起來像能不能用
< 0.8幾乎是一個峰,或只有一個小肩膀 不能用。連「有幾個成分」都判斷不出來。
≈ 1.0看得出是兩個峰,但谷底沒有落到基線 勉強。定性可以,定量誤差約 2–3%。
≥ 1.5兩個峰之間回到基線 合格。稱為「基線分離」,定量誤差 < 0.3%。這是法規和期刊的標準門檻。
整份教材的核心目標,一句話

在混合物所有相鄰的成分對當中,找出最難分的那一對(Rs 最小的那對), 然後調整分析條件,讓這個最小的 Rs 盡可能大, 同時總分析時間不要超過你能接受的上限。

論文裡把這個「最小 Rs」寫作 Rs (原文:the resolution of the least resolved pair of adjacent solutes), 把時間上限寫作 tmax。 iopt、gopt 這些最佳化函數做的事, 就是自動幫你掃過所有可能的條件,找出讓這個 Rs 最大的那一組。

隨堂測驗
你的樣品有 5 個成分。相鄰四對的 Rs 分別是 2.4、1.1、3.0、1.8。 你會怎麼描述這個分析方法?
分離品質由「最差的那一對」決定,不是平均值。 就像一條鎖鏈的強度取決於最弱的那一環。那對 Rs = 1.1 的成分沒有基線分離, 它們的峰面積會互相干擾,定量結果不可信——不管其他三對分得多漂亮。
這正是為什麼論文的最佳化函數一律回報「least resolved pair(最難分的一對)」的 Rs, 而不是平均值。(注意:4 對相鄰組合來自 5 個成分,n 個成分有 n−1 對。)

用真實資料驗算一次 Rs實測資料

公式講完了,來驗算。第 3 節那三張真實層析圖,儀器軟體自己就會報告 Rs。 我們不看它的答案,直接從原始訊號(1201 個數字)自己量峰寬、自己算一次,再對照:

樣品苯甲酸 tR己二烯酸 tR 儀器報告 Rs我們自己算的 Rs差異
50 ppm 標準品5.3665.935 2.2342.3110.077
真實食品樣品5.3575.945 2.2262.1720.054
0.25 ppm 標準品5.3735.938 2.1992.2360.037
對得上,而且差異的來源是可以解釋的

三組都落在 0.04–0.08 的差距內。你剛學的那條公式, 真的能重現商用軟體的計算結果。

那為什麼不是完全相同?因為「峰寬」怎麼量有好幾種做法。 我們用的是半高寬(訊號降到一半的地方量寬度,再乘 1.699 換算成基底寬); Shimadzu 用的是切線法(在峰的兩側反曲點畫切線,交於基線)。 兩種方法對完美對稱的高斯峰會給出相同答案,但真實的峰有拖尾(這裡是 1.29–1.31), 兩者就會出現這種小差距。方法不同不代表誰錯,但報告時應該說明你用的是哪一種。

同一張圖裡,還藏著一個「差一點就不合格」的例子

看「真實食品樣品」那張。積分軟體在 4.93 分鐘抓到一個很小的峰 (高度只有 1.2 mV,苯甲酸的 1/240),並報告它與苯甲酸之間的 Rs = 1.482。

1.482 —— 就差 0.018 沒到 1.5。 這不是課本編出來的例子,是真的跑出來的一針。 面對它你必須做判斷:那個小峰是雜質還是雜訊?它會不會吃掉苯甲酸的面積? 要不要重跑、要不要改條件?這種「就差一點」的情況,才是實驗室裡最常遇到的—— 完全合格和完全失敗都好處理,難的是中間地帶。

思考題
在「真實食品樣品」中,己二烯酸的峰高只有 4.46 mV,而苯甲酸是 287 mV,相差 64 倍。 但兩者的訊號雜訊比(S/N)分別是 112 和 7175,都遠大於一般定量要求的 10。 這代表什麼?
「峰小」和「測不準」是兩件事。 判斷能不能定量的標準是訊號雜訊比,不是峰的絕對高度。 一般規範是 S/N ≥ 3 可偵測(LOD)、S/N ≥ 10 可定量(LOQ)。 這裡己二烯酸的 S/N = 112,比門檻高一個數量級,定量沒有問題。
這個樣品的己二烯酸峰面積只有苯甲酸的 1.9%,反映的是這瓶產品本來就用苯甲酸為主、己二烯酸很少, 是真實的分析結果,不是方法的缺陷。
對照組:0.25 ppm 標準品的己二烯酸峰高只有 1.46 mV,S/N 仍有 259—— 因為那次跑的基線更安靜(雜訊 0.0056 mV,只有食品樣品的 1/7)。 基線乾不乾淨,往往比峰高不高更決定你測不測得到。

6.那顆能改變一切的旋鈕:有機修飾劑中階

你已經知道要讓 Rs 變大。問題是——你到底能轉哪個旋鈕?

在逆相層析(reversed-phase,就是用 C18 這種油性管柱的主流做法)裡, 最有力、最常用、也最容易調的一個變數,是移動相裡有機溶劑的比例。

移動相是水和有機溶劑(乙腈 acetonitrile 或甲醇 methanol)的混合液。 這個有機溶劑就叫有機修飾劑(organic modifier), 它佔的體積分率記作 φ(希臘字母 phi)。 在這份教材和論文的所有 R 程式碼裡,它一律寫成 f—— 請記住 f 就是 φ,論文特別註明了這件事。

為什麼加有機溶劑會讓東西跑更快?

C18 管柱表面是油性的。樣品分子之所以會黏上去,是因為它比較不喜歡待在水裡 (所謂「疏水作用」)。

現在你往水裡加乙腈。移動相變得「比較油」了, 對分子來說待在液體裡沒那麼難受了, 它就比較不需要躲到管柱表面上。黏得少 → k 變小 → 更早出來。

一句話:φ 越大,所有東西都跑得越快。 這就是那顆旋鈕。

7.模型 1:把直覺變成一條公式中階

「φ 越大跑越快」是定性的。要讓電腦幫你算,需要一條定量的式子。

真實的實驗數據告訴我們一件很漂亮的事:如果你把 ln k(滯留因子取自然對數) 對 φ 畫圖,資料點幾乎會落在一條直線上。

模型 1 ln k = c0 − c1 φ c0:直線在 φ=0(純水)時的截距——代表這個分子天生有多愛黏管柱
c1:直線的斜率(取正值)——代表這個分子對有機溶劑有多敏感

這就是論文 Table 2 的模型 1,在 R 程式裡寫作 model=1。 它是整個套件裡最簡單的一個,也是我們整份教材的主角。 (另外還有九個模型,處理 pH、雙溶劑等更複雜的情況——放在 附錄,你現在不需要它們。)

兩個參數,就描述完一個分子的全部行為

這件事的威力在於:只要你做三到四次實驗,量出同一個成分在不同 φ 下的 tR,就能算出它的 c0 和 c1。 然後你就能預測它在任何 φ 下的滯留時間,不用再做實驗。

這正是 ifitk 這個函數在做的事(i = isocratic 恆溶劑,fitk = 擬合 k)。 而算出參數之後,tR = t0(1 + k) 就把它換回你看得懂的時間。

用真實數據看看

下面是論文附帶的真實實驗數據(Data.xlsx 的 i-ret.fit 工作表)。 9 個成分,在 4 種不同的 φ 下各跑一次,管柱死時間 t0 = 1.4 分鐘。 這批樣品是酚類污染物與芳香烴——像五氯酚、溴酚這些, 正是飲用水和食品安全檢驗會遇到的目標物。

φA1A2A3A4A5A6A7A8A9
0.403.7354.6716.6467.6628.49810.7611.0915.9919.00
0.453.0003.6505.2005.6005.9006.8508.00010.3812.69
0.502.5373.0104.2084.3494.4304.8426.0517.2398.921
0.602.0132.2653.0002.9402.9103.0613.8664.1985.085
表格內容為滯留時間 tR(分鐘)。管柱:Kinetex 2.6 µm XB-C18,150 × 4.6 mm;移動相:乙腈/水 pH 5.7;t0 = 1.4 分鐘。

把每個數字換成 k = (tR − 1.4)/1.4,再取 ln,然後畫圖。 下面這張圖就是結果——九條漂亮的直線:

模型 1 的真面目 點選成分名稱可單獨查看它的直線
顯示:
選取成分
全部
截距 c₀
—
斜率 c₁
—
R²(貼合度)
—
你剛剛做的,就是 ifitk 做的事

這九組 (c0, c1) 是用最基本的線性迴歸算出來的 ——就是你在統計課學過、Excel 也能做的那個「加上趨勢線」。

我們把上面那張表用線性迴歸算出來的結果,和論文自己公布在 Data.xlsx 的 i-optim 工作表裡的模型 1 參數完全吻合 (c0 落在 3.1404–5.6030,c1 落在 5.9113–8.5366)。

換句話說:這一步你其實用 Excel 就做得出來。 R 套件真正幫你省力的,是後面那些手算不了的部分——峰形、模擬、和掃過幾千種條件的最佳化。

A3(苯)的參數是 c₀ = 3.669、c₁ = 5.911;A6(五氯酚)是 c₀ = 5.244、c₁ = 8.537。 當你把 φ 從 0.40 提高到 0.50,哪一個成分的滯留時間縮短得比較劇烈?
隨堂測驗
c₁ 是斜率,決定「對有機溶劑有多敏感」。 c₁ 越大,同樣提高 φ,ln k 掉得越多,tR 縮得越兇。
實際數字:A3 從 6.646 → 4.208 分鐘(縮 37%),A6 從 10.76 → 4.842 分鐘(縮 55%)。
這件事非常重要:因為不同成分縮短的幅度不一樣, 它們之間的相對位置會改變——甚至可能交換順序。下一節就會看到。

8.親手最佳化:把旋鈕轉一遍中階

現在你有了九個成分的 c0 和 c1,就能預測任何 φ 下的完整層析圖。 下面這個模擬器把整件事串起來了——請務必動手拖。

模擬器在算什麼

① 對每個成分算 k = exp(c0 − c1φ) → ② tR = t0(1 + k) → ③ 用峰形公式 h = h0 + h1tR、 s = s0 + s1tR 畫出每個高斯峰 → ④ 算出所有相鄰峰對的 Rs,回報最小的那個。 這正是 iopt 函數內部的計算流程。

恆溶劑分離模擬器 9 個真實成分 · t₀ = 1.4 分鐘 · 資料來自論文
往右 = 乙腈更多 = 全部跑更快。範圍 0.30–0.60,和論文 iopt 的預設搜尋範圍一致
你的時間預算。對應 iopt 的 tmax 參數
最小 Rs
—
最難分的一對
—
最後出峰時間
—
沖提順序
—
快速跳到:

電腦是怎麼「找到」最佳解的

你剛剛用手拖滑桿,其實就是在做最佳化——只是很慢。 iopt 做的事一模一樣,只是它每隔 0.005 把 φ 從 0.30 掃到 0.60 (這個步長就是參數 df),一共 61 個點,每個點都算一次最小 Rs, 然後挑出在時間預算內 Rs 最大的那一個。

下面這張圖就是掃描的完整結果。論文的 Fig. 7 畫的就是這種圖:

掃描曲線:Rs 與 tRmax 對 φ 把滑鼠移到圖上任一位置查看該 φ 的數值(與上方模擬器連動)
最小 Rs(越高越好) 最後出峰時間 tRmax(越低越快) 符合時間預算且 Rs ≥ 1.5 的區間
讀出來的答案

在 tmax = 20 分鐘的預算下,最佳解是 φ = 0.410,Rs = 2.455,最後一個峰在 17.13 分鐘出來。 這正是 iopt(model=1, t0=1.4, tmin=2, tmax=20, fmin=0.3, fmax=0.6, df=0.005) 會給你的答案。

把時間預算縮到 15 分鐘 → 最佳 φ = 0.430,Rs = 2.029,還是合格。
縮到 10 分鐘 → 最好也只有 Rs = 0.916,怎麼調都達不到 1.5。 這時候恆溶劑做不到了,你必須換方法——這就是下一節梯度沖提登場的理由。

那個藏在 φ = 0.35 的陷阱

如果你剛剛按過「陷阱 φ = 0.350」,應該會嚇一跳: 明明 φ 更小、時間更長、照理說應該分得更開, Rs 卻從 φ=0.325 的 1.50 暴跌到 0.235。

發生了什麼事:兩條線交叉了

回去看第 7 節那張 ln k 對 φ 的圖。 A6(五氯酚)的斜率比 A7(甲苯)陡—— 它們的兩條直線會在某個 φ 相交。

在交點左邊,A7 比 A6 晚出來;在交點右邊,順序反過來。 而在交點上,兩個成分的 tR 一模一樣——完全重疊, 這叫共沖提(coelution)。

你可以在模擬器上把 φ 從 0.30 慢慢拖到 0.40,親眼看到這兩個峰靠近、合體、再分開。

這就是為什麼需要電腦最佳化

這個陷阱沒有任何直覺能預測。 如果你用傳統的試誤法,先試 φ=0.30(不錯)再試 0.35(災難), 你很可能得出「往下調會變糟」的錯誤結論就放棄了—— 卻永遠不會發現真正的最佳解在 0.41。

iopt 把整個範圍掃過一遍,所以它看得到全貌。 這才是「電腦輔助最佳化」真正的價值:不是算得比較準,是不會漏看。

隨堂測驗
你把 φ 一路往上調到 0.60,發現最後一個峰 5 分鐘就出來了,超快。 可是 Rs 只有 0.002。這代表什麼?
φ 太高時,所有 k 值都趨近於 0,全部成分幾乎和溶劑一起沖出來。 速度是快了,但層析圖上只剩一團擠在一起的東西,完全沒有分析價值。
這說明了最佳化一定是一個取捨: 速度和解析度往相反方向走,你要找的是「在能忍受的時間內,解析度最好」的那個平衡點, 而不是任何單一指標的極端值。

9.當一個 φ 值救不了你:梯度沖提中階

剛才我們發現:10 分鐘的預算下,不管 φ 怎麼調都達不到基線分離。 這不是運氣不好,而是一個有名字的結構性問題。

一般沖提問題(the general elution problem)

當你的樣品裡同時有很親水的成分和很疏水的成分時,你會陷入兩難:

φ 調低(溶劑弱)

前面那幾個親水的成分分得很漂亮 ✓
但後面疏水的成分幾乎不出來, 而且因為在管柱裡待太久,峰又矮又寬, 甚至矮到看不見。✗

φ 調高(溶劑強)

後面疏水的成分很快出來、峰又高又瘦 ✓
但前面那幾個親水的成分全部擠在死時間附近, 黏成一團完全分不開。✗

兩邊都對,也都不夠。沒有任何單一的 φ 能同時滿足兩端。 這就是「一般沖提問題」。

解法:不要一直用同一個 φ

既然前段需要弱溶劑、後段需要強溶劑,那就讓 φ 隨時間慢慢變大。

開始時 φ 低,溫柔地把親水的成分一個一個分開送出來; 接著 φ 逐漸升高,越來越用力地把後面那些死黏著的疏水成分「洗」下來。 這就是梯度沖提(gradient elution)。

梯度的三個參數

參數意思程式裡的名字
φmin → φmax 從多少開始、升到多少。例如 0.30 → 0.60fmin、fmax
tG(梯度時間) 花多久時間爬完這一段。這是最主要要最佳化的旋鈕。 tG 短 = 爬得陡 = 快但擠;tG 長 = 爬得緩 = 慢但分得開 tG、搜尋範圍 tGmin/tGmax
tD(延遲時間) 幫浦「開始改變比例」到「新的比例真的抵達管柱入口」之間的延遲, 因為中間隔著管路和混合室。每台儀器都不一樣,必須自己量。 tD
梯度沖提模擬器 同樣的 9 個成分。上圖是梯度程式,下圖是結果
從 φmin 爬到 φmax 花的時間。對應 gopt 的 tG
太高的話,前面幾個成分會擠在一起
太低的話,最後幾個成分會拖很久才出來
最小 Rs
—
最難分的一對
—
最後出峰時間
—
峰寬(首/末)
—
比較看看:
回答第 8 節留下的問題:梯度真的救得了 10 分鐘嗎?

救得了。請按上面的「10 分鐘達標的梯度」按鈕 (φ 從 0.35 爬到 0.70,tG = 11 分鐘)。

時間預算恆溶劑最好能做到梯度最好能做到
10 分鐘 Rs = 0.916 ❌ 不合格 Rs = 1.63 ✅ 基線分離
15 分鐘 Rs = 2.03 ✅Rs = 2.41 ✅
20 分鐘 Rs = 2.46 ✅Rs = 2.45 ✅(幾乎相同)

看最後一列:時間夠寬裕時,梯度並沒有比較好。 梯度的價值不是「一定比較強」,而是在時間被壓縮的時候,它還撐得住,而恆溶劑已經垮了。 這也再次說明為什麼論文同時保留兩套函數。

注意看峰寬

切換「恆溶劑」和「好的梯度」,特別注意最後幾個峰的寬度。

恆溶劑時,第 9 個峰又矮又胖(因為它在管柱裡待了 17 分鐘,擴散得很厲害)。 梯度時,後面的成分被越來越強的溶劑「推」著走, 每個峰的寬度變得差不多——這是梯度沖提最漂亮的附加好處, 也讓後面的成分更容易被偵測到。

梯度的代價:數學變難了

恆溶劑時 k 是固定的,tR = t0(1+k) 一行就算完。

但梯度時 φ 一直在變,所以 k 也一直在變。 你必須解一條積分方程式(論文的 Eq. 1):

∫0tR dt ⁄ ( t0 k(t) ) = 1 白話:分子在管柱裡每一小段時間往前推進一點點,累積走完 100% 的管柱長度時就出來了。 這個模擬器就是這樣一小步一小步算出來的。

這個積分手算不了。論文的 gfitk、gpred、gopt 提供兩種解法:mode="A" 用解析解(比較快,但只適用模型 1–5 和 8), mode="NP" 用數值逼近(比較慢,但所有模型都能用)。 這就是你真的需要這套 R 軟體、而不是 Excel 的地方。

隨堂測驗
同事說:「梯度比較好用,以後全部都跑梯度就對了。」這個說法哪裡需要補充?
梯度解決的是「成分性質差很多」的問題。 如果你的樣品成分性質相近(例如只測三種防腐劑),恆溶劑就夠了,而且有幾個實際優勢: 不需要再平衡(每針之間省 5–10 分鐘)、基線更穩、儀器更便宜、方法更容易轉移到別的實驗室。
論文同時提供恆溶劑(iopt)和梯度(gopt)兩套函數,正是因為兩種都有它的位置。 選擇的依據是你的樣品,不是流行。

10.第二顆旋鈕:pH中階

φ 和梯度控制的是「所有成分一起變快或變慢」。 但有一顆旋鈕不一樣——它能改變「誰跑得比誰快」,甚至把出峰順序整個翻過來。

回到第 1 節那瓶飲料。三個目標物裡,咖啡因是中性分子, 但苯甲酸和己二烯酸都是弱酸。這個差別在調 φ 時看不出來, 一旦你動 pH,它就會主宰一切。

關鍵:帶電的分子不黏管柱

C18 管柱表面是油性的,它只喜歡「油性的、電中性的」分子。

弱酸在酸性環境裡維持完整的分子形式(—COOH),是中性的、偏油的 → 黏得住,出得晚。
但當 pH 升高,它會解離成帶負電的離子(—COO⁻)。 帶電的東西親水、不親油 → 幾乎不黏了,很快就被沖出來。

咖啡因呢?它在 pH 2 到 8 之間都是中性的,完全不受影響, 不管 pH 怎麼變,它都待在原地。

轉折點在哪裡:pKa

每個弱酸都有一個專屬的數字 pKa,代表它「剛好一半解離」時的 pH。 在 pKa 以下它主要是中性的,以上主要是離子態,轉變集中在前後約兩個 pH 單位之間。

成分酸鹼性質pKapH 升高時
咖啡因中性(極弱鹼)— 不變
苯甲酸單質子弱酸4.20 較早解離 → 先變快
己二烯酸單質子弱酸4.76 較晚解離 → 後變快

注意這兩個 pKa 差了 0.56。 就是這 0.56 的差距,讓兩根酸在不同 pH 下「各自以不同的速度提早出來」, 也讓整個出峰順序有機會被改寫。

描述它的公式:模型 7

模型 7 k = ( k0 + k1 · 10(pH−pK) ) ⁄ ( 1 + 10(pH−pK) ) k0:中性態的滯留因子(大) · k1:完全解離態的滯留因子(小)
低 pH 時 10(pH−pK) 趨近 0,k → k0; 高 pH 時該項變得很大,k → k1。 中間是一條平滑的 S 形轉折。

這就是論文 Table 2 的模型 7(model=7)。 它和模型 1 的角色完全對稱:模型 1 描述 φ 怎麼影響 k,模型 7 描述 pH 怎麼影響 k。 附錄裡的模型 8 和 10 則是把兩者合起來,同時處理 φ 和 pH。

pH 模擬器:看出峰順序被改寫 上圖是三個成分的滯留時間隨 pH 變化,下圖是該 pH 下的層析圖
慢慢往右拖,注意咖啡因(灰色虛線)完全不動,兩根酸卻一路往下掉
最小 Rs
—
最難分的一對
—
最後出峰時間
—
出峰順序
—
關鍵位置:
模擬用的數值哪裡來

pKa 用的是苯甲酸 4.20、己二烯酸 4.76 這兩個標準文獻值。 k0 與 k1 則是依照文獻報告的典型滯留時間反推的代表值 (t0 = 1.2 分鐘,酸性條件下咖啡因約 2.6 分、苯甲酸約 4.1 分、己二烯酸約 5.7 分), 目的是讓你看清楚趨勢。換一根管柱、換一種有機溶劑,絕對值會不一樣,但「兩根酸會提早、咖啡因不動」這個行為是通則。

三段不同的出峰順序

把 pH 從 2.5 拖到 7.0,你會經過三個截然不同的區間:

pH 區間出峰順序發生什麼事
2.5 – 4.4 咖啡因 → 苯甲酸 → 己二烯酸 兩根酸都是中性、都黏得牢。這就是文獻方法採用的區間,也是第 1 節那張圖的順序。
≈ 4.45 咖啡因 = 苯甲酸 苯甲酸已解離到追上咖啡因,兩個峰完全重疊。
4.5 – 5.4 苯甲酸 → 咖啡因 → 己二烯酸 苯甲酸已經跑到咖啡因前面,己二烯酸還沒。咖啡因被夾在中間。
≈ 5.42 咖啡因 = 己二烯酸 換己二烯酸追上咖啡因,再一次完全重疊。
5.5 – 7.0 苯甲酸 → 己二烯酸 → 咖啡因 兩根酸都完全解離、都很早出來,咖啡因反而變成最後一個——和一開始完全相反。
和第 8 節一模一樣的教訓

你在第 8 節看過 φ ≈ 0.35 的共沖提陷阱。現在 pH 這條軸上有兩個。 共通點是:解析度對條件的變化不是單調的,中間會塌陷, 而塌陷的位置完全無法憑直覺猜到——它取決於各成分 pKa 的細微差距。 這正是 iopt 提供 pHmin、pHmax、dpH 這組參數、 把整個 pH 範圍掃過一遍的理由。

思考題一
你接手一個舊方法,紀錄寫著「pH 6.5,第一個出來的峰是目標防腐劑」。 你手上有咖啡因、苯甲酸、己二烯酸三支標準品。第一個出峰的最可能是哪一個?
pH 6.5 遠高於兩根酸的 pKa(4.20 與 4.76),兩者都已幾乎完全解離。 帶負電的離子不親油、不黏 C18,所以都很早出來;而苯甲酸的離子態滯留又比己二烯酸更弱,所以它排第一。
咖啡因在這個 pH 仍是中性,反而變成最後一個出峰。
順帶一提:「分子量越小跑越快」在逆相層析裡是錯的直覺——決定順序的是親油性(以及帶不帶電),不是大小。
思考題二
同學把移動相調到 pH 4.5 跑標準品,結果層析圖上只看到兩個峰, 但他明明打了三支標準品的混合液。他懷疑是某一支標準品配壞了。 你會建議他先做什麼?
pH 4.5 剛好落在第一個共沖提陷阱上。 咖啡因與苯甲酸在此重疊成單一個峰,看起來就像「少了一支」。 只要把 pH 調回 3.0,兩者會拉開到 2.60 與 4.05 分鐘,三個峰立刻都回來了。
這題的重點是一個實驗室裡的習慣:看到峰「不見了」,先懷疑重疊,再懷疑樣品。 重配標準品要花半小時,改一個 pH 重跑只要幾分鐘,而且能直接證實或排除這個假設。
選項二不能算全錯——換波長確實是排查手段之一,但這三個成分在 220–230 nm 都有良好吸收,而且「兩個峰而非三個」的模式高度指向共沖提。
思考題三:綜合判斷
根據模擬器,pH 3.0 的最小 Rs ≈ 3.8(最後出峰 5.7 分鐘), pH 5.0 的最小 Rs ≈ 2.0(最後出峰 3.3 分鐘)。兩者都遠超過 1.5。 如果你要寫進標準作業程序(SOP)給整個實驗室每天用,你會選哪一個?
關鍵不是哪個數字漂亮,而是「配錯一點會怎樣」。
pH 5.0 的兩側各有一個陷阱(4.45 和 5.42),左右只有約 0.4 個 pH 單位的緩衝空間。 緩衝液配製誤差 ±0.2、不同批號的試劑、電極沒校正好——任何一個都可能把你推向懸崖, 而且失敗的方式是悄悄地少一個峰,不是報錯。
pH 3.0 距離最近的陷阱有 1.45 個 pH 單位,而且在這個區間 Rs 對 pH 的變化很平緩(你可以拖滑桿驗證:pH 2.5 到 3.5 之間 Rs 都在 3.4 以上)。 多花 2.4 分鐘換來的是方法的耐用性(robustness)。
這就是為什麼文獻方法幾乎清一色使用 pH 3–4——不是因為那裡最快, 而是因為那裡最不容易出錯。真實世界的方法開發,穩定性往往比最佳值更重要。
想親手驗證這一節嗎?

上面的 pH 模擬器是算出來的。 附錄第 23 節備有一份完整的實驗指引—— 三支標準品、五種移動相、一個下午,可以印出來直接帶進實驗室, 用真實的儀器檢驗上面那三個預測是否成立。

延伸案例:真實的論文怎麼做同一件事

你到目前為止做的(拖一個變數、看最小 Rs、避開陷阱、在時間與解析度之間取捨), 就是方法開發的核心工作。這裡看一篇真實的研究,它處理的正是同一批成分。

Aşçı 等人(2016):軟性飲料中五種添加物與咖啡因的同時定量

這個團隊要一次分開己二烯酸鉀、苯甲酸鈉、咖啡因, 再加上三種食用色素(carmoisine、allura red、ponceau 4R)——比你的三個成分更難。 管柱是 Inertsil ODS-3V(250 × 4.6 mm、5 µm),偵測波長 230 nm。

他們同時最佳化三個變數:

變數搜尋範圍找到的最佳值
移動相 pH6.0 – 7.06.0
流速(mL/min)1.0 – 1.41.0
醋酸緩衝液比例85% – 95%95%

最關鍵的是他們選的目標函數:原文寫的是 「Resolution values of all peak pairs were used as a response」—— 用所有相鄰峰對的解析度當作要最大化的目標。 這和你在第 8 節看到的 iopt 回報「最難分的一對」的 Rs, 是完全相同的思路。

兩種找最佳解的策略

這篇論文用的是實驗設計法(Box-Behnken design): 實際做十幾次實驗,用統計模型擬合出反應曲面,再推出最佳點。 而 iopt/gopt 走的是另一條路:先用少數實驗建立滯留模型, 再用電腦把整個範圍暴力掃過一遍。 前者不需要懂滯留機制,但每個變數都要花真實的實驗成本; 後者要先花力氣建模,但建好之後模擬幾千種條件是免費的—— 也因此才看得到第 8 節那種藏在中間的陷阱。

思考題四:這張表上有一個值得警覺的地方,你看出來了嗎?

再看一次上面那張表:三個最佳值全部都落在搜尋範圍的邊界上—— pH 停在下限 6.0、流速停在下限 1.0、緩衝液比例停在上限 95%。

當最佳解貼在你設定的邊界時,它通常在告訴你一件事: 真正的最佳點可能在你搜尋的範圍之外。 如果 pH 5.5 或 5.0 表現更好,這個搜尋設計是看不到的,因為它從來沒有去試。

這不代表這篇論文做錯了——實務上邊界常常是有理由的: 流速有管柱壓力上限、緩衝液比例有溶解度限制、pH 太低可能傷害管柱的鍵結相 (傳統矽膠 C18 一般建議 pH 2–8)。 重點是:作者應該說明邊界是「物理限制」還是「隨手設的」,讀者也應該去問這個問題。

這件事在 RChromOptim 裡也會發生

完全一樣的陷阱會出現在 ifitk/gfitk 的參數擬合上: 如果某個參數的估計值剛好等於你設的 upper(預設 200), 代表模型沒有真的收斂,只是被邊界擋住了。 論文自己的範例資料就有這個現象——模型 4 的 c2 對某些溶質卡在上限。 遇到這種情形的處理方式,請見附錄的常見問題排除。

養成一個習慣:任何最佳化跑完,第一件事是看最佳解是不是貼在邊界上。 是的話,把範圍放寬再跑一次——或者說明為什麼不能放寬。

11.架好你的 R 環境進階

從這一節開始,你會動手用真正的統計軟體 R 來跑 RChromOptim——這是 Zisi、Pappa-Louisi 與 Nikitas 三位學者在 2020 年發表於《Journal of Chromatography A》的一套工具,把前面幾節教你手算的滯留模型(例如 lnk = c0 − c1·f)、解析度 Rs、峰形,全部包成十個可以直接呼叫的函數。你不需要自己寫程式,只要照著步驟把資料餵給它。

Step 1:安裝 R

到 https://www.r-project.org/ 下載並安裝 R(Windows 版)。R 是免費、開放原始碼的統計軟體,你可以把它想成「一個看得懂數學公式、也能畫圖的超強計算機」。

Step 2:安裝四個必要套件(package)

套件(package)是別人寫好、可以直接拿來用的功能模組。RChromOptim 需要四個輔助套件才能運作。打開 R,在主控台(console)輸入:

R Console安裝套件
# 一次安裝四個套件,中間用逗號分隔 install.packages(c("optimx", "VGAM", "plot3D", "numDeriv"))

Step 3:載入 RChromOptim 的工作空間(workspace)

作者把十個函數都存在一個叫 RChromOptim.RData 的檔案裡。這種 .RData 檔就像一個「打包好的工具箱」,載入後裡面的函數就能直接叫得出來。兩種做法都可以:

用選單(新手推薦)

R 視窗上方選單:File > Load Workspace(載入工作空間),找到並選取 RChromOptim.RData。

用指令

R Console
load("RChromOptim.RData")

Step 4:確認十個函數都在

R Console檢查
ls()

執行後應該會列出剛好十個名字:ifitk、gfitk、fitshape、ipred、gpred、mgpred、iopt、gopt、mgopt、ifitopt。少一個都代表載入不完整,回頭檢查 Step 3。

重要警告:這套工具只能在 Windows 上跑,而且是「互動式」的

十個函數全部都會呼叫 choose.files()——這是 Windows 專屬的檔案選取視窗(file-picker dialog),跳出一個小視窗讓你「用滑鼠點檔案」。這代表:

  • 你永遠不會把檔案路徑打成參數塞進函數裡,而是執行後跳出視窗,你再用滑鼠去點選要用的 .txt 檔。
  • Mac 或 Linux 使用者:choose.files() 在你的系統上不存在,必須自己把原始碼裡的這行改成 file.choose() 才能執行。
  • 有幾個函數(例如畫圖、挑峰)還會開啟 windows(record=T) 繪圖視窗,並用 locator() 讓你在圖上用滑鼠點兩下(例如標出峰的位置)。
  • 正因如此,這套工具無法在沒有畫面的環境跑(headless)——不能放在 RStudio Server、不能寫進 knitr 報告自動產生,必須有人坐在電腦前面點滑鼠。
最容易漏看的地方:提示訊息不在主控台,而在繪圖視窗的「標題」上

很多程式習慣把「請輸入 xxx」這種提示印在主控台(console)文字裡,但 RChromOptim 不是——它把操作提示寫成繪圖視窗(graphics window)的圖表標題(plot title)。如果你執行完函數,畫面上的主控台一片安靜,先別慌,切換到跳出來的繪圖視窗看看標題寫什麼,通常是它在等你做下一步(例如「請點選這個峰的起點」)。

12.你的資料要長什麼樣子進階

RChromOptim 只吃一種格式的檔案:Tab 字元分隔的純文字檔(.txt),而且永遠只有一列標題(header)。這一節帶你認識四種會用到的檔案長相,之後每個範例都會對照到其中一種。

(a) 恆溶劑滯留資料(isocratic retention data)——給 ifitk、ifitopt 用

第一欄是實驗條件(通常是有機溶劑比例 f),後面每一欄是一個溶質(solute)在該條件下量到的滯留時間 tR(分鐘)。

fA1A2A3A4A5A6A7A8A9
0.43.7354.6716.6467.6628.49810.7611.0915.9919
0.4533.655.25.65.96.85810.3812.69
0.52.5373.014.2084.3494.434.8426.0517.2398.921
0.62.0132.26532.942.913.0613.8664.1985.085

如果你的滯留模型是 model 7(跟 pH 有關),第一欄就要換成 pH;model 8、9、10 則需要兩欄條件(例如兩種溶劑或 pH+f 併用)。

(b) 梯度滯留資料(gradient retention data)——給 gfitk 用

前面固定是四欄條件,之後每欄一個溶質。這四欄很容易搞混,務必記住:f1、f2 是梯度起點與終點的有機溶劑比例,t1、t2 是梯度斜坡開始與結束的時間——不是兩種溶劑的意思。

f1f2t1t2A1A2A3A4A5…
0.20.40101.631.751.681.552.94…
0.10.45101.782.012.134.479.97…

(c) 層析圖原始曲線(chromatogram trace)——給 fitshape、以及 ipred/gpred 的 ic=1 用

就是一條真實的層析圖曲線,永遠只有兩欄:時間、偵測器訊號值。

t (min)y(f = 0.45)
0.010030.001803685
0.020180.001784105

(d) 溶質參數檔(solute-parameter file)——ifitk/gfitk 的輸出,也是 pred/opt 的輸入

這種檔案是「轉置」的:第一欄是參數名稱,之後每欄是一個溶質。跑完 fitshape 之後,峰形參數(h0, h1, h2, s0, s1)會被附加在同一個檔案下方。

soluteA1A2A3…
c03.14043.46493.6690…
c16.64136.60435.9113…
c2000…

不同模型的參數列名稱不一樣:model 1–6 用 c0, c1, c2;model 7 用 k0, k1, pK;model 8 用 c0, c1, pK, r;model 9 用 c0…c5;model 10 用 c0, c1, c2, c3, pK。

實用技巧:怎麼從 Excel 存出這種 .txt 檔

在 Excel 整理好資料後,選檔案 > 另存新檔,存檔類型選「文字檔 (Tab 字元分隔) (*.txt)」(英文版是 Text (Tab delimited))。存好之後打開確認一下:欄與欄之間應該是看不見的 Tab 字元,而不是逗號或多個空格。

小心:範例檔本身的標題列裡藏了多餘的空格

論文附的範例檔裡,像 "  t (min)  " 這種欄名前後其實多打了空格。R 讀進來會把它自動改寫成一長串像 X..t..min.. 的欄位名稱(把空格、括號都換成點)。這個現象沒有壞掉、也不影響計算,只是看起來很嚇人——你只要知道這是怎麼回事,不用去修正它。

13.十個函數怎麼選進階

十個函數名字看起來很像,但只要抓住命名規則,五秒鐘就能認出每一個在做什麼:字首 i = 恆溶劑(isocratic)、g = 單線性梯度(gradient)、mg = 雙線性梯度(bilinear gradient);字尾 fitk = 擬合滯留模型、fitshape = 擬合峰形、pred = 預測/模擬層析圖、opt = 找最佳條件。

函數作用
ifitk用恆溶劑資料擬合滯留模型(求 c0, c1… 這些參數)
gfitk用梯度資料擬合滯留模型
fitshape從真實層析圖擬合峰形(峰高+峰寬)
ipred預測並畫出恆溶劑層析圖
gpred預測並畫出單線性梯度層析圖
mgpred預測並畫出雙線性梯度層析圖
iopt找出最佳的恆溶劑分離條件
gopt找出最佳的單線性梯度條件
mgopt找出最佳的雙線性梯度條件
ifitopt懶人包:一次做完「擬合+峰形+最佳化」(只支援恆溶劑 model 1–3)
文件裡的一個小烏龍

論文的補充說明檔(docx)寫的是「8 個 R 函數」,但實際附的工作空間裡是十個——這是原始文件的筆誤,你不用懷疑自己數錯。

決策流程:我該用哪一個?

先問自己兩個問題:① 你的實驗是恆溶劑還是梯度?(決定用 i 開頭還是 g/mg 開頭)② 你現在要做的是「求參數」「模擬」還是「找最佳條件」?(決定用 fitk、pred 還是 opt)。

不論恆溶劑或梯度,正規的操作永遠是同一套四步驟流程:

①
ifitk / gfitk
求滯留參數
用實測的 tR 資料,擬合出每個溶質的模型參數(如 c0, c1)
→
②
fitshape
求峰形參數
用真實層析圖曲線,擬合出峰高、峰寬相關參數
→
③
ipred / gpred
驗證模型準不準
把模型畫出來的層析圖疊在真實層析圖上比對
→
④
iopt / gopt
找最佳條件
在你設定的時間預算內搜尋 Rs 最大的條件
為什麼第③步不能跳過

iopt / gopt 這類最佳化函數,做的事情純粹是「相信你給的模型,去搜尋一個讓 Rs 最大的條件」。如果模型本身就跟真實層析圖對不上——例如峰形參數是亂猜的、或滯留模型擬合得不好——那麼最佳化找出來的「最佳條件」也只是一個錯誤模型底下的最佳解,拿去實際跑管柱不會得到一樣的結果。先驗證再最佳化,才不會白做工。

14.完整實戰:恆溶劑最佳化進階

這一節帶你走完整套流程,資料是真實案例:9 個溶質(酚類污染物與芳香烴,跟食品/飲用水安全檢測相關)在 Kinetex 2.6 µm XB-C18 150×4.6 mm 管柱上、乙腈/水(pH 5.7)系統分析,t0 = 1.4 分鐘。

Step 1:準備滯留資料 .txt

把第 12 節格式 (a) 的表格存成 tab 分隔的 .txt(例如 retention.txt),第一欄是 f,A1–A9 是 9 個溶質。

Step 2:擬合滯留模型(ifitk)

R Console擬合 model 1
ifitk(model = 1, t0 = 1.4) # 執行後跳出檔案選取視窗,用滑鼠點選 retention.txt

model 1 就是你在前面章節學過的 lnk = c0 − c1·f。輸出會是一張表,欄位大致包含:

本例用 9 個溶質實際擬合出來的結果(model 1):

溶質c0c1
A13.14046.6413
A23.46496.6043
A33.66905.9113
A44.25206.9596
A54.64637.6648
A65.24408.5366
A74.62756.7991
A85.57068.1844
A95.60307.7662

ifitk 會另外跳出存檔視窗,讓你把這張參數表存成「格式 (d)」的溶質參數檔(例如 param.txt)——之後每一步都要用到它。

Step 3:擬合峰形(fitshape)

R Console擬合峰形
fitshape(pd = 1) # 跳出視窗選一個或多個真實層析圖檔(格式 c) # 圖上會提示你用滑鼠點兩下,標出峰的範圍
操作眉角
  • pd 是峰高多項式的次方(degree),可設 1 或 2;本例用 1。
  • 畫面會要求你在層析圖上用滑鼠點兩下,標出峰的起訖位置(因為參數 w 預設是 NULL,靠這兩次點擊估出初始峰寬猜測值)。
  • 一次最多只能處理 4 個重疊/相鄰的峰——如果你的層析圖裡擠了超過 4 個峰,需要分批處理。
  • 峰的底寬是用 w = 4s/√2 算出來的,s 是峰形參數之一。

跑完後,h0、h1、h2(峰高相關)與 s0、s1(峰寬相關)這五列參數,會被附加到你在 Step 2 存的 param.txt 下方,讓這個檔案變成完整的六列以上參數檔。

Step 4:驗證模型(ipred)

R Console模擬並比對真實層析圖
ipred(model = 1, t0 = 1.4, fp = 0.45) # 先選 param.txt(含峰形列),再選要比對的真實層析圖檔

fp = 0.45 代表你要模擬「f = 0.45」這個條件下的層析圖。因為 ic=1 是預設值,畫面會把「模型模擬出來的曲線」跟「你手上真實量到的曲線」疊在一起。如果兩條線對得起來,代表模型跟峰形參數都可信,可以放心進入最佳化;如果對不起來,要回頭檢查 Step 2、3 是不是哪裡出了問題。

Step 5:找最佳分離條件(iopt)

R Console最佳化
iopt(model = 1, t0 = 1.4, tmin = 2, tmax = 20, fmin = 0.3, fmax = 0.6, df = 0.005) # 選 param.txt;tmax=20 代表你能接受的最長分析時間是 20 分鐘

tmax 是整個最佳化裡最關鍵的參數——它是你的「時間預算」:不管 Rs 理論上能拉多高,分析時間都不能超過這個上限。df 則是搜尋 f 值時的網格步長,越小搜得越細但越慢。

R Consoleiopt 輸出(數值為本教材以同一演算法重算)
Table for manual selection of the optimum f value f Rs tRmax 0.300 2.824 38.35 <- Rs 很好,但遠超過 tmax 0.305 2.555 36.94 ...................... 0.350 0.235 26.46 <- 共沖提陷阱(見第 8 節) ...................... 0.410 2.455 17.13 <- 最佳解 ...................... 0.595 0.167 5.14 0.600 0.002 5.00 <- 全部擠在一起 Optimum f : 0.410 Optimum Rs : 2.455 Optimum tR values: 3.53 4.39 6.26 7.07 7.70 9.41 10.21 14.23 17.13

白話翻譯這個結果:在 f = 0.410(也就是 41% 有機溶劑)這個條件下,9 個溶質裡分得最差的那一對相鄰峰的 Rs = 2.455(遠超過安全門檻 1.5,分離良好),而整批跑完最慢的溶質要 tRmax = 17.13 分鐘,還在你設定的 20 分鐘預算之內。

為什麼不直接挑 Rs 最高的那一列?

掃描表第一列 f = 0.300 的 Rs 是 2.824,比最佳解的 2.455 還高。但它的 tRmax = 38.35 分鐘,遠超過你設的 20 分鐘上限,所以被淘汰了。iopt 挑的是「在 tmax 之內 Rs 最大」的那一列,不是全表 Rs 最大的那一列。

另外注意 f = 0.350 那一列:Rs 只有 0.235,比它左右兩邊都差很多。這就是第 8 節講的共沖提陷阱——Rs 對 f 並不是單調變化的,中間會有塌陷。這正是為什麼要把整個範圍掃過一遍,而不是憑幾個點推測趨勢。

時間預算 vs. 解析度:同一組資料,換一個 tmax 會怎樣

只改 tmax,其他都不變,看看結果怎麼變化:

tmax(分鐘)最佳 f最佳 Rs結論
200.4102.455時間充裕,分離良好
150.4302.029時間變緊,只好用更高的 f 沖快一點,Rs 跟著下降,但仍達標
10—0.916再怎麼調 f,最好也只有 0.916——不合格(未達 1.5)
這告訴我們什麼

對這組 9 個溶質而言,10 分鐘在恆溶劑模式下根本不夠用——不是你調錯條件,而是這個時間預算跟這批溶質的分離難度天生衝突。這正是 iopt 這類工具的價值:與其在實驗室裡一次次試錯,不如先用模型算出「這個時間預算做不做得到」,做不到就得考慮拉長分析時間,或改用梯度沖提(gradient elution)。

15.一步到位:ifitopt進階

如果你只是想快速看看「這批資料大概能不能分開、大概要多久」,不想一步步跑 ifitk → fitshape → iopt 三個函數,ifitopt 把這三步合併成一次呼叫——但代價是它只支援恆溶劑、且只支援 model 1、2、3(比較簡單的滯留模型)。

它需要兩種輸入,缺一不可

ifitopt 除了跟 ifitk 一樣需要滯留資料表(格式 a),還需要真實層析圖檔(格式 c)來擬合峰形——這點跟單獨的 ifitk 不一樣。參數 blc(block)就是用來告訴函數:滯留資料表裡的哪幾列(哪幾個條件)有對應的層析圖檔可以拿來配對。例如 blc=c(1,3) 代表第 1 列與第 3 列的條件各自有一張層析圖,函數會依序跳出視窗讓你選這兩張圖。

R Consoleifitopt 一次做完
ifitopt(model = 1, t0 = 1.4, blc = c(1, 3), pd = 1, tmax = 20, fmin = 0.3, fmax = 0.6) # 依序跳出視窗:先選滯留資料 .txt,再選 blc 指定列數對應的層析圖檔

什麼時候用 ifitopt,什麼時候乖乖走三步驟

適合用 ifitopt

初步探索(快速探索)——想知道「這批溶質、這根管柱,大概能不能在預算時間內分開」,先抓個大概的答案再決定要不要深入。

適合走 ifitk → fitshape → ipred → iopt

需要檢查每一步的中間結果(例如懷疑某個溶質的 p 值很大、擬合不可靠)、資料有缺漏或條件超過 3 種模型範圍、或者本來就要跑梯度(ifitopt 完全不支援梯度)。

小測驗
如果你在 ipred 或 iopt 用的溶質參數檔,其實只擬合到 ifitk(Step 2)就存檔了,忘記再跑 fitshape 補上峰形參數(也就是整個檔案只有 c0、c1 這兩列,不到 6 列),程式會發生什麼事?
程式會報錯直接中止,提醒你缺少峰形參數
程式不會報錯,會自動代入預設值 h0=1、h1=0、h2=0、s0=0.002、s1=0,畫出來的是接近直線的「假層析圖」,而且最佳化的判斷標準會從真正的 Rs 悄悄退化成 dt(相鄰峰滯留時間差)
程式會自動幫你叫用 fitshape 補齊參數
RChromOptim 在偵測到參數檔不足 6 列(沒有峰形列)時,不會報錯,而是靜靜地代入一組預設峰形值,畫出來的峰會又高又窄、幾乎變成一條條垂直線,跟真實峰形完全不像。更容易被忽略的是:這時候最佳化用來判斷「分得開不開」的標準,會從真正考慮峰寬的解析度 Rs,悄悄換成只算滯留時間差的 dt。所以每次看最佳化輸出,都要養成習慣確認結果欄位寫的是 Rs 還是 dt——如果是 dt,代表你的峰形參數八成沒補齊。

16.現代 R 版:tidyverse + ggplot2進階

論文的套件寫於 2019 年,是 base R 風格的互動式程式。 R 語言在這幾年變化很大——本節用 R 4.6.1 + tidyverse 2.0 + ggplot2 4.0 把同一套方法重寫一次, 並附上可直接下載執行的完整腳本。

先說清楚:為什麼要重寫,而不是說原作者做得不好

原始 RChromOptim 的設計目標是「讓不寫程式的層析工作者也能用滑鼠操作」, 在 2019 年那是合理且體貼的選擇。但同樣的設計換到今天的教學與研究情境,會有幾個實際的痛點: 不能自動化、不能跨平台、不能重現。 重寫的目的不是取代它,而是讓你在理解演算法之後,能把它放進自己可重複執行的工作流。

改了哪些地方

面向原始 RChromOptim(2019, base R)現代版(R 4.6.1)
指定檔案 choose.files() 跳視窗用滑鼠點 路徑當參數傳入,可迴圈批次處理
作業系統 僅 Windows(choose.files()、windows() 是 Windows 專屬) Windows / macOS / Linux 皆可
資料結構 矩陣與 for 迴圈,欄位靠位置索引 tibble 長格式,欄位有名字
逐溶質擬合 手寫迴圈,每個函式內重複定義輔助函式 nest() + map() + broom,一次寫好
繪圖 base graphics,直接畫在螢幕上 ggplot2,回傳物件可再組合、可存檔
語法 R 3.x 風格 R 4.1+ 原生管線 |>、匿名函式 \(x)
可重現性 依賴人在正確時機點對位置 純函式、無副作用,可寫進報告或自動測試

核心:用 nest + map 一次擬合所有溶質

這是整份改寫最能展現差別的地方。原程式要用迴圈跑過每個溶質、把結果塞進矩陣; tidyverse 的作法是把每個溶質的資料收進一格裡(nest),再對每一格套用同一個函式(map)。

R逐溶質擬合模型 1:ln k = c0 − c1·f
fit_model1 <- function(data, t0 = T0) { data |> mutate(lnk = log((tR - t0) / t0)) |> # 先換成 ln k nest(.by = solute) |> # 每個溶質收成一列 mutate( fit = map(data, \(d) lm(lnk ~ f, data = d)), # 每列各跑一次迴歸 tidy = map(fit, broom::tidy), glance = map(fit, broom::glance) ) |> mutate( c0 = map_dbl(tidy, \(x) x$estimate[x$term == "(Intercept)"]), c1 = map_dbl(tidy, \(x) -x$estimate[x$term == "f"]), # 斜率取負才是 c1 r_squared = map_dbl(glance, "r.squared"), .keep = "unused" ) }
三個現代 R 語法,順便學起來
  • |> 是 R 4.1 內建的管線,把左邊的結果餵給右邊的函式當第一個參數。 不需要載入 magrittr 的 %>%。
  • \(d) ... 是 R 4.1 的匿名函式簡寫,等同 function(d) ...。
  • nest(.by = solute) 用的是 dplyr 1.1+ 的 .by 參數, 不必再寫 group_by() 然後記得 ungroup()。

解析度與最佳化:把迴圈換成資料表

lead() 讓「相鄰兩峰相減」變成一行;掃描最佳化則是先把每個 φ 的完整結果算出來收進表格, 再從表格裡挑出符合時間預算、Rs 最大的那一列。

R相鄰峰解析度 + 掃描最佳化(對應 iopt)
# Rs = 2(tR2 - tR1) / (w1 + w2),只算相鄰對 resolution_table <- function(peaks) { peaks |> arrange(tR) |> mutate(pair = paste(solute, lead(solute), sep = "/"), Rs = 2 * (lead(tR) - tR) / (w + lead(w))) |> filter(!is.na(Rs)) |> select(pair, Rs) } optimise_isocratic <- function(params, f_range = c(0.30, 0.60), df = 0.005, t_max = 20, t0 = T0) { scan <- tibble(f = seq(f_range[1], f_range[2], by = df)) |> mutate( peaks = map(f, \(x) predict_isocratic(params, x, t0)), worst = map(peaks, min_resolution), Rs = map_dbl(worst, "Rs"), tR_max = map_dbl(peaks, \(p) max(p$tR)), feasible = tR_max <= t_max # 時間預算 ) best <- scan |> filter(feasible) |> slice_max(Rs, n = 1) list(scan = scan, best = best) }

梯度:數值解基本梯度方程式

第 9 節那條積分式 ∫dt/(t0k(t)) = 1, 在這裡用累積和解決——把時間切成小格,每格前進一點點,累積到 1 就出管柱。 完全向量化,沒有迴圈。

R梯度沖提的滯留時間
predict_gradient_one <- function(c0, c1, f_start, f_end, tG, tD, t0 = T0, dt = 0.002) { tt <- seq(0, 400, by = dt) phi <- case_when( # 管柱入口看到的組成(含延遲 tD) tt <= tD ~ f_start, (tt - tD) / tG >= 1 ~ f_end, .default = f_start + (f_end - f_start) * (tt - tD) / tG ) k <- exp(c0 - c1 * phi) prog <- cumsum(dt / (t0 * k)) # 已走完的管柱比例 i <- which(prog >= 1)[1] before <- if (i > 1) prog[i - 1] else 0 te <- tt[i] + dt * (1 - before) / (prog[i] - before) # 格內線性內插 tibble(tR = te + t0, k_elute = k[i]) }
一個一定要寫的自我檢查

數值解很容易寫錯一格(off-by-one),而且錯了通常不會報錯,只會安靜地偏差。 有一個檢查可以立刻抓到:當起始組成等於結束組成時,梯度其實就是恆溶劑, 數值解必須退化成解析解 tR = t0(1+k)。

R退化檢查
iso <- predict_isocratic(params, 0.45) degen <- predict_gradient(params, 0.45, 0.45, tG = 15) stopifnot(all(abs(sort(iso$tR) - sort(degen$tR)) < 1e-4))

這不是形式主義——本教材的 R 版第一次寫出來就是錯的:內插時多減了一格 dt, 所有梯度滯留時間都偏移了 0.002 分鐘。畫出來的圖完全正常、程式也沒報錯, 就是上面這行 stopifnot() 把它抓出來的。

執行結果

終端機Rscript rchromoptim_modern.R
== 模型 1 擬合結果 == solute c0 c1 r_squared sigma p_c1 n 1 A1 3.14 6.64 0.998 0.0333 0.0011 4 2 A2 3.46 6.60 0.998 0.0298 0.0009 4 3 A3 3.67 5.91 0.999 0.0201 0.0005 4 ... 9 A9 5.60 7.77 0.997 0.0433 0.0014 4 c0 範圍 3.1404 - 5.6030 c1 範圍 5.9113 - 8.5366 論文 i-optim 工作表公布值:c0 3.1404-5.6030,c1 5.9113-8.5366 <- 完全一致 == 最佳化 (t_max = 20) == f = 0.410 Rs = 2.453 tR_max = 17.13 最難分 = A4/A5 t_max = 15 -> f = 0.430 Rs = 2.027 t_max = 10 -> f = 0.490 Rs = 0.914 == 梯度 (0.35->0.70, tG=11, tD=0.7) == Rs = 1.633 (A4/A5) tR_max = 9.99 檢查通過:f_start == f_end 時梯度解退化為恆溶劑解(誤差 < 1e-4 min)
為什麼這裡是 2.453,網頁模擬器卻是 2.455?

因為參數取到幾位小數不一樣。網頁模擬器用的是論文公布的四位小數值(c1 = 6.6413), R 腳本用的是 lm() 算出來的全精度值(6.64126514…)。同一組擬合、同一條公式, 只是四捨五入的位置不同:全精度 Rs = 2.4535,四位小數 Rs = 2.4554。 差異在小數第三位,不影響任何結論——但知道差異從哪裡來,比假裝它不存在重要。 腳本裡有一段就是專門印出這個對照。

ggplot2 的輸出

三張圖用 patchwork 的 / 與 | 運算子拼起來,一行程式碼:

R組圖並存檔
p <- (plot_model1(data, params) | plot_optimisation(opt)) / plot_chromatogram(best_peaks, "最佳條件 f = 0.410") ggsave("demo.png", p, width = 12, height = 8, dpi = 130)
現代 R 版輸出:模型 1 擬合圖、掃描最佳化圖、最佳條件層析圖
腳本實際跑出來的輸出。左上:九條 ln k–φ 直線與實測點。 右上:掃描最佳化,φ ≈ 0.355 的共沖提塌陷清楚可見,綠點為最佳解。 下:最佳條件下的模擬層析圖,九個峰全部基線分離。

還能再快嗎:從「跑得動」到「跑得動大的」

上面的腳本跑示範資料只要零點幾秒,看起來沒有優化的必要。 但真正的最佳化很少只掃一個變數——論文的 iopt 對模型 8、10 要掃 φ × pH 的二維網格, gopt 要掃 tG × pH。條件數從幾十跳到幾千, 原本沒感覺的成本就會變成好幾分鐘的等待。

第一步永遠是量測,不是猜

在動手改任何一行之前,先量出時間花在哪裡。實測結果是: fit_model1 只佔 0.006 秒,瓶頸完全在掃描與梯度預測。 如果憑直覺去優化擬合那段,會白費力氣。

優化一:把 61 個資料框變成 1 個矩陣

原本的寫法對每一個 φ 都建一個 tibble,再從中取最小 Rs。 61 個 φ 就配置 61 次記憶體、跑 61 次排序與資料框運算。 但這件事的本質只是一個矩陣:9 個溶質 × 61 個條件。

R優化後:整張表一次算完
scan_isocratic <- function(params, f, t0 = T0, shape = SHAPE) { tR <- t0 * (1 + exp(params$c0 - outer(params$c1, f))) # n_solute x n_f,一行算完 ord <- apply(tR, 2, order) # 每欄的沖提順序 S <- apply(tR, 2, sort) W <- 4 * (shape$s0 + shape$s1 * S) / sqrt(2) n <- nrow(S) Rs <- 2 * (S[-1, , drop = FALSE] - S[-n, , drop = FALSE]) / # 矩陣位移相減 (W[-1, , drop = FALSE] + W[-n, , drop = FALSE]) tibble(f = f, Rs = apply(Rs, 2, min), tR_max = S[n, ], ...) }

outer(c1, f) 一次產生所有「c1 × φ」的組合; S[-1,] - S[-n,] 就是「每一欄裡相鄰兩個峰相減」。 迴圈消失了,因為問題本來就是矩陣形狀的。

優化二:梯度的三個浪費

原本每個溶質各呼叫一次數值求解器,裡面藏了三筆重複成本:

實測結果

工作負載優化前優化後加速
恆溶劑掃描(61 個 φ,示範規模) 0.461 s0.011 s 42×
恆溶劑掃描(601 個 φ,df=0.0005) 5.07 s0.033 s 152×
單次梯度預測(9 溶質) 0.196 s0.016 s 12×
梯度二維掃描 tG × φstart(120 組) 25.2 s2.17 s 11.6×

注意第二列:掃描越細,優化的效益越大(42× → 152×)。 這正是重點——優化真正的價值不在於讓示範跑得快一點, 而在於讓原本做不到的規模變得做得到。 Python 版做了完全相同的兩項優化。

優化的鐵律:數字必須一模一樣

效能改寫最危險的地方,是順手改變了計算結果卻沒發現。 所以每一次優化後都要拿舊版跑一次,逐項比對:

R新舊對照
o <- opt_old(p)$best; nw <- optimise_isocratic(p)$best cat(abs(o$Rs - nw$Rs)) # -> 0.00e+00 cat(max(abs(sort(grad_old(...)$tR) - sort(predict_gradient(...)$tR)))) # -> 0.00e+00 min

本次兩項優化的差異都是精確的 0——不是「小到可以忽略」,是完全相同的浮點數。 加上第 17 節的退化檢查仍然通過,才能說優化是安全的。

順帶一提:連量測本身都可能出錯

製作這份教材時,第一版的計時程式寫成 system.time(for (i in 1:n) force(expr))——看起來很合理,其實是錯的。 R 的惰性求值(lazy evaluation)讓 expr 只在第一次 force() 時真的執行, 後面幾次都直接拿快取值,等於「跑一次的時間除以 n」,把耗時嚴重低估。 正確做法是傳入函式再呼叫:system.time(for (i in 1:n) fn())。 本頁表格中的數字都是用修正後的方法量的。 效能數據和實驗數據一樣,測量方法錯了,結論就沒有意義。

下載完整腳本

rchromoptim_modern.R ——完整可執行,含所有函式、註解與示範流程。 需要 tidyverse 與 patchwork:
install.packages(c("tidyverse", "patchwork")),然後 Rscript rchromoptim_modern.R。
範例資料已內嵌在腳本裡,不需要另外準備檔案就能跑; 要換成自己的資料時,把路徑傳給 read_retention("你的檔案.txt") 即可。


17.Python 版:pandas + numpy + scipy進階

同一套方法,用 Python 再寫一次。這不只是「換個語言」—— 兩個獨立實作跑出同一組數字,本身就是最強的正確性證據。

為什麼要有 Python 版
  • 很多食品科學的課程與實驗室是用 Python 教資料分析的,尤其是需要接機器學習或影像的場合。
  • 儀器廠商的 API、實驗室資訊系統(LIMS)串接,大多提供 Python SDK。
  • 最重要的:你可以拿它來檢查 R 版有沒有算錯。下面會看到這件事實際發揮作用。

同一件事,兩種語言怎麼寫

把兩份腳本並排看,是很有效的學習方式——你會發現思路完全一樣,只是詞彙不同。

要做的事R(tidyverse)Python(pandas / numpy)
讀 tab 分隔檔readr::read_tsv(path)pd.read_csv(path, sep="\t")
寬轉長pivot_longer()DataFrame.melt()
逐溶質分組nest(.by = solute)groupby("solute")
線性迴歸lm(lnk ~ f)scipy.stats.linregress(f, lnk)
取相鄰列lead(tR)Series.shift(-1)
累積和cumsum()np.cumsum()
找第一個超過門檻的位置which(prog >= 1)[1]np.searchsorted(prog, 1.0)
挑最大值那一列slice_max(Rs, n = 1)df.loc[df.Rs.idxmax()]
繪圖ggplot2matplotlib

擬合:groupby 取代 nest

Python逐溶質擬合模型 1
def fit_model1(data: pd.DataFrame, t0: float = T0) -> pd.DataFrame: """以模型 1 擬合滯留資料:ln k = c0 - c1 * f。""" if not (data["tR"] > t0).all(): raise ValueError("所有 tR 必須大於 t0,否則 ln k 無定義") rows = [] for solute, grp in data.groupby("solute", sort=False, observed=True): f = grp["f"].to_numpy(dtype=float) lnk = np.log((grp["tR"].to_numpy(dtype=float) - t0) / t0) res = stats.linregress(f, lnk) rows.append(dict( solute=solute, c0=res.intercept, c1=-res.slope, # 斜率取負才是 c1 r_squared=res.rvalue**2, p_c1=res.pvalue, )) return pd.DataFrame(rows)
兩種風格的差別在哪

R 的 nest + map 是函數式寫法:資料留在表格裡,你把函式套上去。
Python 這裡用的是迴圈累積:把每組結果做成 dict,最後一次組成 DataFrame。
後者在 Python 圈是很常見且清楚的寫法。兩種都對——可讀性比「哪種比較潮」重要。

梯度:用 searchsorted 找沖提點

Python 版用 np.searchsorted() 在累積陣列裡二分搜尋,比逐格比對更快也更乾淨:

Python數值解基本梯度方程式
tt = np.arange(0, t_limit + dt, dt) phi = np.where( # 管柱入口看到的組成(含延遲 tD) tt <= tD, f_start, np.where((tt - tD) / tG >= 1, f_end, f_start + (f_end - f_start) * (tt - tD) / tG), ) k = np.exp(c0 - c1 * phi) prog = np.cumsum(dt / (t0 * k)) # 已走完的管柱比例 idx = np.searchsorted(prog, 1.0) # 第一個 >= 1 的位置 before = prog[idx - 1] if idx > 0 else 0.0 te = tt[idx] + dt * (1 - before) / (prog[idx] - before) return te + t0, k[idx]

執行結果:和 R 版一模一樣

終端機python rchromoptim_modern.py
== 模型 1 擬合結果 == solute c0 c1 r_squared sigma p_c1 n A1 3.1404 6.6413 0.9977 0.0333 0.0011 4 A2 3.4649 6.6043 0.9981 0.0298 0.0009 4 ... A9 5.6030 7.7662 0.9972 0.0433 0.0014 4 c0 範圍 3.1404 - 5.6030 c1 範圍 5.9113 - 8.5366 論文 i-optim 工作表公布值:c0 3.1404-5.6030,c1 5.9113-8.5366 == 最佳化 (t_max = 20) == f = 0.410 Rs = 2.4535 tR_max = 17.13 最難分 = A4/A5 t_max = 15 -> f = 0.430 Rs = 2.027 t_max = 10 -> f = 0.490 Rs = 0.914 == f = 0.350 的共沖提檢查 == 最小 Rs = 0.2343(A7/A6) <- 第 8 節那個陷阱 == 梯度 (0.35->0.70, tG=11, tD=0.7) == Rs = 1.6330 (A4/A5) tR_max = 9.99 檢查通過:f_start == f_end 時梯度解退化為恆溶劑解 (最大誤差 8.67e-13 min < 1e-4)
Python 版輸出:模型 1 擬合圖、掃描最佳化圖、最佳條件層析圖
Python 版(matplotlib)的輸出。與第 16 節的 ggplot2 圖版面風格不同,但曲線與數值完全相同。
這才是重點:交叉驗證(cross-validation)

兩份腳本是不同語言、不同的線性迴歸函式(lm vs scipy.stats.linregress)、 不同的搜尋方式(which vs searchsorted)寫的,卻得到:

檢查項目R 版Python 版
c0 範圍3.1404 – 5.60303.1404 – 5.6030
c1 範圍5.9113 – 8.53665.9113 – 8.5366
最佳 f(tmax=20)0.4100.410
最佳 Rs2.45352.4535
tRmax17.1317.13
梯度 Rs/tRmax1.633 / 9.991.633 / 9.99

而且兩者都獨立重現了論文公布的 c0/c1 範圍。 三個來源(論文、R、Python)互相吻合,你可以相當有把握地說:這個計算是對的。

交叉驗證不是形式主義:它在這份教材裡真的抓到過錯

第 16 節提過,R 版的梯度數值解一開始寫錯了一格,所有梯度滯留時間偏移 0.002 分鐘。 當時發現的方式是「退化檢查」;而後來 Python 版寫好, 它的退化誤差是 8.67 × 10⁻¹³ 分鐘(等於機器精度), R 版修正後也降到同一個量級——兩邊對上了,才確定真的修好了。 如果只有一個實作,你永遠只能說「看起來沒問題」。

思考題
你寫了一個分析程式,跑出來的圖很漂亮、也沒有出現任何錯誤訊息。 以下哪一項最能提高你對結果正確性的信心?
正確答案是「用已知答案的特例去測」。
再跑一次只會重現同樣的錯誤(程式是決定性的);重讀程式碼會受你自己的思考盲點影響—— 你當初就是那樣想才寫錯的;註解和命名改善可讀性,但不會改變計算結果。
本教材用了兩個這樣的特例:① 起訖組成相同時,梯度必須退化成恆溶劑解析解; ② 擬合出的 c0/c1 必須重現論文已公布的數值範圍。 第一個抓出了真正的錯誤,第二個確認了整條資料流沒接錯。
這個觀念在分析化學裡其實你已經很熟悉了——它就是「跑標準品」: 用已知濃度的樣品確認儀器讀數正確,再去測未知樣品。寫程式是同一回事。
下載完整腳本

rchromoptim_modern.py ——完整可執行,函式名稱與第 16 節的 R 版一一對應,方便兩邊對照閱讀。
需要:pip install numpy pandas scipy matplotlib,然後 python rchromoptim_modern.py。
範例資料同樣已內嵌,不需要另外準備檔案;要用自己的資料就傳路徑給 read_retention("你的檔案.txt")。


18.互動筆記本:Quarto 與 Jupyter進階

第 16、17 節的腳本是「跑完看結果」。這一節提供兩份邊讀邊改的筆記本—— 文字說明和程式碼交錯排列,你可以隨時改一個數字、按下執行,馬上看到差別。

兩份筆記本,選一份就好
Quarto(R)Jupyter(Python)
檔案rchromoptim_tutorial.qmdrchromoptim_tutorial.ipynb
語言R 4.1+ / tidyversePython 3.9+ / pandas
怎麼開RStudio 直接開(已內建 Quarto),或裝 Quarto CLI 後 quarto render Jupyter Lab、VS Code,或上傳到 Google Colab
要不要裝東西要裝 R 與 tidyverseColab 完全不用裝,開瀏覽器就能跑
適合誰已經在用 R 做統計的人沒有安裝環境、想立刻開始的人

如果你完全沒有安裝過任何東西,建議走 Jupyter + Google Colab 這條路—— 上傳 .ipynb、按執行,五分鐘內就能看到第一張圖。

筆記本裡有什麼

兩份的結構刻意做成一樣,方便對照:

1
資料
9 個溶質 × 4 個 φ 的滯留時間表,已內嵌在檔案裡,不需要另外準備資料。
↓
2
算 k 值
k = (tR − t0)/t0。 附一個小練習:把 t0 改成 1.2 再跑一次,看 k 怎麼變。
↓
3
擬合模型 1 並自我檢查
算出 c0、c1,並當場比對論文公布的參數範圍 (3.1404–5.6030 與 5.9113–8.5366)——你會親眼看到自己算的數字和論文一致。
↓
4
畫圖與模擬層析圖
ln k–φ 診斷圖,以及模擬層析圖。 這裡有一行 f_choice <- 0.45(Python 是 f_choice = 0.45), 改這個數字再執行,就能看到波峰移動。
↓
5
掃描最佳化
畫出 Rs 與 tRmax 對 φ 的曲線、找出最佳解, 並標出 φ ≈ 0.35 的共沖提塌陷。
↓
6
三道練習(留空白給你寫)
① 把時間預算改成 12 分鐘,最佳解跑到哪裡去? ② 把 A6 從混合物裡拿掉,最佳 φ 會不會改變? ③ 調大峰寬參數 s1(相當於管柱變差),Rs 怎麼反應? 每題都有提示,程式格子留空白讓你自己動手。
為什麼「留空白」很重要

看懂別人的程式和自己寫得出來,中間差距很大。 這三題都只需要改動已經出現過的程式碼——不用發明新東西, 但你必須知道要改哪一行。這是從「讀者」變成「使用者」最短的一段路。

一個做中文教材一定會遇到的坑

圖上的中文變成一個個空白方框(俗稱「豆腐」)

這在 Windows 上用 R 畫圖幾乎必然發生:預設的繪圖裝置找不到中文字型, 就把每個中文字畫成 □。製作這份教材時實測,同一段程式碼 在預設裝置下產生 46 個字型轉換警告、中文全變方框;改用 ragg 裝置並指定中文字型後,警告 0 個、中文正常顯示。

Quarto 版的筆記本已經內建這段修正,你不必自己處理:

R自動挑一個系統上存在的中文字型
cjk_font <- local({ cands <- c("Microsoft JhengHei", "Noto Sans TC", "PingFang TC", "Heiti TC", "Noto Sans CJK TC", "SimSun") avail <- if (requireNamespace("systemfonts", quietly = TRUE)) systemfonts::system_fonts()$family else character(0) hit <- cands[cands %in% avail] if (length(hit)) hit[1] else "" # 找不到就用預設值 }) theme_set(theme_minimal(base_family = cjk_font))

搭配 YAML 裡的 knitr: opts_chunk: dev: ragg_png。 這段程式會自動偵測:Windows 用微軟正黑體、macOS 用蘋方、Linux 用 Noto, 都找不到就退回預設值而不會報錯。以後你自己做中文圖表,把這段複製過去就能用。

下載

rchromoptim_tutorial.qmd  (R / Quarto,15 個程式區塊)
rchromoptim_tutorial.ipynb  (Python / Jupyter,21 個 cell,輸出已清空讓你從頭跑一次)
兩份都經過實際執行驗證:R 版 15 個區塊全部無誤跑完、Python 版以 nbconvert --execute 從頭執行到尾 0 錯誤,最佳化結果都是 φ = 0.410、Rs = 2.4535、tRmax = 17.13。

19.十個滯留模型完整對照表

大部分食品分析的工作,其實只會用到模型 1 或模型 3。只有當你遇到「可解離的化合物」(例如有機酸、胺類——像乙酸、檸檬酸、生物胺)、或是需要控制 pH 值來調整分離效果時,才需要翻到這張完整清單。

展開:十個滯留模型的完整清單

模型 1–6 描述的是「有機修飾劑比例 f」(也就是流動相中甲醇或乙腈的比例 φ)如何影響滯留行為,適用於恆溶劑沖提,以及單線性/雙線性的 f 梯度。模型 7 開始加入 pH 的影響,模型 9 則是處理兩種有機修飾劑同時存在的情況。

模型編號方程式什麼時候用它需要的參數
1 lnk = c0 − c1·f 最簡單的形式,ln k 對 f 畫出來是一條直線。這就是線性溶劑強度理論(LSS)所用的模型。 c0, c1
2 lnk = c0 − c1·ln(f) f 的另一種對數形式,屬於曲線型態的替代選擇。 c0, c1
3 lnk = c0 − c1·f + c2·f² 在模型 1 的基礎上加入二次項,能描述曲率,可以擬合比模型 1 更寬的 f 範圍。 c0, c1, c2
4 lnk = c0 − c1·ln(1 + c2·f) 另一種帶曲率的 f 相依形式。 c0, c1, c2
5 lnk = c0 + 2·ln(1 + c1·f) − c2·f/(1 + c1·f) 較複雜的曲線型態,也是用來處理非線性的 f 相依關係。 c0, c1, c2
6 lnk = c0 − c2·f/(1 + c1·f) 模型 5 的簡化版本,同樣描述曲率型態。 c0, c1, c2
7 k = (k0 + k1·10^(pH−pK)) / (1 + 10^(pH−pK)) 描述 pH 如何影響「可解離化合物」(單質子的酸或鹼)。pK 是解離常數,k0、k1 分別是中性型態與完全解離型態的滯留因子。 k0, k1, pK
8 k = k0·(1 + r·10^(pH−pK)) / (1 + 10^(pH−pK)),其中 k0 = exp(c0 − c1·f) 同時處理 f 和 pH 兩個變數——k0 會隨 f 而變。 c0, c1, pK, r
9 lnk = c0 + c1·f1 + c2·f2 + c3·f1² + c4·f2² + c5·f1·f2 同時處理「兩種」有機修飾劑(f1、f2),例如三元流動相系統。 c0…c5(共 6 個)
10 k = (k0 + k1·10^(pH−pK)) / (1 + 10^(pH−pK)),其中 k0 = exp(c0−c1·f),k1 = exp(c2−c3·f) 最一般化的 f + pH 模型——k0 和 k1 都會隨 f 變化。 c0, c1, c2, c3, pK
參數越多,需要的實驗越多 你至少要做出「等於參數個數」的實驗次數,實務上建議抓 2–3 倍會比較保險。用 3 個點去擬合一個有 3 個參數的模型,等於自由度是零——你完全沒有辦法檢查這個擬合到底準不準。具體來說:模型 1、2(2 個參數)至少要 3 組實驗;模型 3–6(3 個參數)至少要 4–5 組實驗。
不同 R 函數支援的模型範圍不同 mgpred 和 mgopt(雙線性梯度函數)只支援模型 1–7;ifitopt 只支援模型 1–3;解析梯度解法中,mode="A"(解析法)涵蓋模型 1、2、3、4、5、8,mode="NP"(數值法)則十個模型全部都支援。

20.函數速查表

RChromOptim 套件一共有 10 個函數,命名有規律可循,看懂前綴和字根就能猜出功能。

函數用途輸入檔主要輸出
ifitk恆溶劑滯留資料擬合恆溶劑滯留資料 .txt參數表+標準差+p 值+SEE/ssr,可存成 .txt
gfitk梯度滯留資料擬合梯度滯留資料 .txt(4 個條件欄)同上
fitshape峰形建模2–3 張層析圖 .txt +對應滯留資料h0, h1, h2, s0, s1 參數表
ipred恆溶劑層析圖模擬參數表 .txt(+可選實驗層析圖)預測滯留時間+疊圖
gpred單線性梯度模擬同上同上+梯度曲線
mgpred雙線性梯度模擬同上同上
iopt恆溶劑最佳化參數表 .txt最佳 f、Rs、tR,Rs/tRmax 掃描圖,最佳層析圖
gopt單線性梯度最佳化參數表 .txt最佳 tG、Rs、梯度曲線、最佳層析圖
mgopt雙線性梯度最佳化參數表 .txt最佳兩段梯度
ifitopt恆溶劑一步到位滯留資料+層析圖 .txt參數表+最佳 f+最佳層析圖
命名口訣 前綴:i = isocratic 恆溶劑、g = gradient 梯度、mg = 雙線性梯度(bi-linear gradient)。字根:fitk = 擬合滯留(fit retention)、fitshape = 擬合峰形(fit peak shape)、pred = 預測(predict)、opt = 最佳化(optimize)。看到函數名稱,把前綴和字根拆開讀,就知道它在做什麼。

21.常見問題排除

這些是使用 RChromOptim 時真實會遇到的狀況,坦白列出來,照著檢查通常就能解決。

函數跑起來沒反應、找不到檔案對話框?

這些函數用的是 choose.files(),這是 Windows 專用的檔案選取視窗。它有可能開在 R 視窗「後面」,先檢查一下工作列或其他視窗,很可能對話框已經開好了,只是被擋住。

如果你是在 macOS 或 Linux 上執行,choose.files() 根本不存在,會直接報錯。這時必須自己修改函數原始碼,把 choose.files() 換成 file.choose() 才能用。

它要我點滑鼠,但我不知道要點哪裡?

提示文字是印在「繪圖視窗」的標題上,不是印在 R 主控台(console)裡。所以請看圖形視窗,不要盯著 console 找訊息。

fitshape 會請你用滑鼠點兩下來估計峰寬;如果需要做基線校正,則是每個峰(或每組重疊的峰)都要點兩下,分別標出起點和終點。

畫出來的層析圖是一根一根的直線,不是山形峰?

這代表你提供的參數檔列數不足 6 列,也就是缺少峰形參數。程式會偷偷用近似「delta 函數」(尖峰狀)的峰形取代,並且回報 dt(最小滯留時間差)而不是真正的 Rs 值。

解法:先執行 fitshape,把算出來的 h0, h1, h2, s0, s1 這幾列參數,補進參數檔裡。

錯誤訊息 could not find function?

代表 RChromOptim 的函數沒有被載入到目前的 R 工作環境(session),或是少裝了相依套件。可以先執行 ls() 確認這 10 個函數是否都存在於環境裡;如果缺套件,用以下指令補裝:

R安裝相依套件
install.packages(c("optimx","VGAM","plot3D","numDeriv"))
讀進來的欄位名稱變成 X..t..min.. 這種怪東西?

原因是 .txt 檔的表頭那一行含有多餘的空格,R 的 read.table 在解析時就會把它們轉成 X..t..min.. 這類亂碼欄名。

這其實不影響程式運作——因為程式是「用欄位位置」而不是「用欄位名稱」在讀資料。如果你想要乾淨的欄名,就手動把 .txt 檔表頭那行多餘的空格刪掉即可。

擬合結果的 p 值很大、參數卡在 200?

這代表該參數在最佳化過程中「頂到了 upper 上限」,意味著目前選用的模型不適合這筆資料。

可以試試:換一個 model 編號、放寬 lower/upper 邊界,或是提供更好的初始值(c0, c1…)。附帶一提:連原始論文自己的資料集裡,模型 4 的 c2 參數在某些溶質上也卡在邊界值——這種事連原作者都會遇到,不代表你哪裡做錯了。

最佳化結果的 Rs 很低,怎麼調都達不到 1.5?

可能是時間預算 tmax 設得太緊,或者這個混合物本身在恆溶劑條件下、合理時間內根本無法分離。

論文裡有個真實例子:9 種溶質的混合物,tmax=20(分鐘)可以得到 Rs=2.455,但如果把 tmax=10,就只剩 Rs=0.916。如果恆溶劑最佳化已經到了瓶頸,下一步可以考慮改用梯度沖提(gopt),或是換管柱、換 pH 條件。

可以把恆溶劑的參數檔拿去餵梯度函數嗎?
不行,這樣會出錯但不會報錯 以模型 8 為例,ifitk 寫出的參數列順序是 c0, c1, pK, r;但 gfitk 寫出的順序卻是 c0, c1, r, pK——兩條流程的欄位順序不一樣。如果把恆溶劑的參數檔誤餵給梯度函數(或反過來),數值會被讀進錯誤的位置,而且完全不會跳出任何錯誤訊息,只會默默算出錯的答案。使用前務必確認你手上的參數檔是哪條流程產生的。

22.中英名詞對照與延伸閱讀

整理課程中出現過的關鍵術語中英對照,以及原始論文的補充資源。

中文English一句話說明
高效液相層析HPLC用高壓把樣品推過管柱,依化合物與固定相的作用力差異來分離的分析技術。
管柱column裝填固定相的細管,是分離真正發生的地方。
移動相mobile phase推動樣品通過管柱的液體,通常是水與有機溶劑的混合物。
固定相stationary phase填在管柱裡不會移動的材料,化合物會與它產生不同強弱的作用力。
溶質/分析物solute/analyte你想分離、想測定的目標化合物。
層析圖chromatogram訊號強度對時間畫出的圖,峰的位置與大小代表化合物的滯留時間與含量。
死時間dead time (t0)完全不被固定相滯留的物質通過管柱所需的時間,是所有滯留時間的基準點。
滯留時間retention time (tR)某個化合物從注入到訊號出現峰頂所花的時間。
滯留因子retention factor (k)化合物在固定相停留時間相對於死時間的倍數,衡量它被「留住」的程度。
解析度resolution (Rs)衡量兩個相鄰峰分開程度的數值,越大代表分離越乾淨。
基線分離baseline separation兩個峰之間完全沒有重疊,訊號會回到基線再出現下一個峰。
有機修飾劑organic modifier加進移動相裡的有機溶劑(如甲醇、乙腈),用來調整沖提強度。
恆溶劑沖提isocratic elution整個分析過程中,移動相的組成比例都維持不變。
梯度沖提gradient elution分析過程中,有機修飾劑的比例會隨時間逐漸改變(通常是遞增)。
梯度時間gradient duration (tG)從梯度開始到結束所經過的時間長度。
延遲體積/滯留時間dwell time (tD)梯度訊號從幫浦設定改變,到真正到達管柱入口之間的延遲時間。
峰寬peak width (w)層析峰在基線上的寬度,反映峰的擴散程度。
高斯峰Gaussian peak形狀對稱、像鐘型曲線的理想層析峰。
基線校正baseline correction (BLC)把訊號中飄移或傾斜的基線扣除,讓峰的判讀更準確的處理步驟。
共沖提coelution兩個以上的化合物在幾乎相同的時間沖提出來,訊號重疊在一起。
沖提順序elution order各化合物依滯留時間先後,從管柱流出的順序。
選擇性selectivity系統區分兩種不同化合物的能力,通常以兩者滯留因子的比值表示。
最佳化optimization在時間與解析度之間找出最合適的分析條件(如 f、pH、梯度設定)。
殘差平方和sum of squared residuals (ssr)模型預測值與實際觀測值差距的平方總和,數值越小代表擬合越好。
估計標準誤standard error of estimate (SEE)衡量整體擬合誤差大小的指標,數值越小代表模型描述資料越準確。
一般沖提問題general elution problem恆溶劑條件下,早出峰擠在一起、晚出峰又拖得太久太寬的兩難困境,是梯度沖提被發展出來的主因。
線性溶劑強度理論Linear Solvent Strength (LSS) theory假設 ln k 與有機修飾劑比例 f 呈線性關係的經典理論,對應本課程的模型 1。

延伸閱讀

本課程內容主要根據以下論文:

Zisi, Ch., Pappa-Louisi, A., & Nikitas, P. (2020). Separation optimization in HPLC analysis implemented in R programming language. Journal of Chromatography A, 1617, 460823.

該論文的補充材料(Supplementary Material)包含:R_Instructions.pdf(操作說明)、R_Functions.docx(函數說明文件)、RChromOptim.RData(套件本體)、Data.xlsx(範例資料)、一個 Text files 資料夾(範例輸入檔),以及一段示範操作的教學影片。想更深入了解函數細節或取得範例檔案,可以從這些資源著手。

本教材食品分析情境所依據的文獻

第 1 節的咖啡因+防腐劑情境、第 10 節的 pH 與出峰順序、以及第 10 節末的延伸案例, 分別依據下列研究。想延伸閱讀或當作專題報告題材,建議從第一篇和第四篇入手。

  1. Kritsunankul, O., & Jakmunee, J. (2011). Simultaneous determination of some food additives in soft drinks and other liquid foods by flow injection on-line dialysis coupled to high performance liquid chromatography. Talanta, 84(5), 1342–1349.
    明確記載五種添加物的沖提順序為醋磺內酯鉀 → 糖精 → 咖啡因 → 苯甲酸 → 己二烯酸, 全程約 14 分鐘(逆相 C18、磷酸鹽緩衝液 pH 3.75、UV 230 nm)。本教材第 1 節出峰順序的主要依據。
  2. Lino, C. M., & Pena, A. (2010). Occurrence of caffeine, saccharin, benzoic acid and sorbic acid in soft drinks and nectars in Portugal and subsequent exposure assessment. Food Chemistry, 121(2), 503–508.
    同樣四種目標物,條件為 C18(250 × 4.6 mm)、KH₂PO₄ 0.02 M/乙腈(90:10)、 磷酸調至 pH 4.2、UV 220 nm。文中同時做了攝取量暴露評估,適合食品營養背景的讀者。
  3. Wong, T. (2008). Determination of aspartame, benzoic acid and caffeine in soft drinks using high performance liquid chromatography.
    在強酸性條件(乙腈 0.05% TFA/水 0.1% TFA,20:80)下給出具體滯留時間: 咖啡因 2.155 分、阿斯巴甜 2.810 分、苯甲酸 4.139 分,5 分鐘內完成。 再次印證酸性條件下咖啡因先於苯甲酸出峰。
  4. Aşçı, B., Zor, Ş. D., & Aksu Dönmez, Ö. (2016). Development and validation of HPLC method for the simultaneous determination of five food additives and caffeine in soft drinks. International Journal of Analytical Chemistry, 2016, 2879406.
    以 Box-Behnken 實驗設計同時最佳化 pH(6.0–7.0)、流速(1.0–1.4 mL/min) 與醋酸緩衝液比例(85–95%),並以「所有相鄰峰對的解析度」為目標函數—— 與 iopt 回報「最難分的一對」的邏輯相同。第 10 節延伸案例的來源。

這門課在整條學習路徑上的位置

本站另外兩門課處理的是同一件事的前後段。三門課用的是相似的樣品與同一間實驗室的儀器, 合起來就是一條完整的路:條件怎麼選 → 選好之後怎麼定量 → 訊號怎麼自動處理。

1
本課程 · HPLC 分離最佳化你在這裡
問題:把峰分開。 滯留模型、解析度、恆溶劑與梯度、pH 控制,最後用電腦掃出最佳條件。 得到的產出是「一組可以用的分析條件」。
↓
2
問題:峰分開之後,怎麼算出濃度。 外標法完整流程:峰面積積分 → 檢量線 → 樣品濃度。 進階版還談基線校正(airPLS)、滯留時間對齊、殘差診斷與 LOD/LOQ。 正好接續本課程第 3 節那三張真實層析圖之後的下一步。
↓
3
問題:訊號本身怎麼自動處理。 基線校正、自適應找峰、峰純度判斷、重疊峰去卷積、跨樣品化合物追蹤。 當你遇到本課程第 5 節那種「這個峰底下是不是藏了別的東西」的疑問時, 答案在那門課裡——它需要 PDA 的光譜維度才做得到。

另有 ChromatographyR(R 版層析資料分析)與 層析數據分析互動教學 可作為平行參考。

教材中的實測層析資料

第 3 節「真實的層析圖長什麼樣」與第 5 節「用真實資料驗算一次 Rs」所使用的層析圖, 是本實驗室自行量測的原始資料,非取自任何已出版文獻:

  • 分析物:苯甲酸(benzoic acid)與己二烯酸(sorbic acid)
  • 儀器與條件:Shimadzu HPLC/LCsolution,UV 230 nm, 總分析時間 10 分鐘,取樣間隔 0.5 秒(每張圖 1201 點),注射量 10 µL
  • 樣品:0.25–100 ppm 檢量線標準品系列,以及實際食品樣品
  • 資料處理:教材中的 Rs「自算值」由原始訊號以半高寬法計算 (w = 1.699 × w50),與 LCsolution 報告的切線法 Rs 並列對照
兩個可以直接接手做的專題方向
  • 把第 10 節的模擬變成真實數據。用三支標準品在 pH 3.0、4.0、4.5、5.0、6.0、7.0 各跑一次, 量出滯留時間,用 ifitk(model=7) 擬合出各自的 k0、k1、pK, 再用 iopt(model=7) 找最佳 pH。你會得到一張屬於你那台儀器的版本, 並可以和文獻的 pKa 值(4.20、4.76)比對——這是一個完整、可發表等級的小型研究。
  • 比較兩種最佳化策略。對同一組樣品,一邊用 Aşçı 等人的實驗設計法, 一邊用本教材的建模+掃描法,比較兩者找到的最佳條件是否一致、各花了幾次實驗。 這正好回答第 10 節那個「兩種策略」的問題。

本教材使用的軟體與版本

第 16、17 節的兩份腳本是在下列環境撰寫並實際執行驗證的。做研究或寫報告時, 軟體版本應該和文獻一樣被記錄下來——這是可重現性的一部分。

語言版本主要套件
R4.6.1 tidyverse 2.0.0(dplyr 1.2.1、tidyr 1.3.2、purrr 1.2.2、readr 2.2.0)、ggplot2 4.0.3、broom 1.0.13、patchwork 1.3.2
Python3.12.10 numpy 2.2.6、pandas 2.3.3、scipy 1.16.3、matplotlib 3.10.8

在 R 裡用 sessionInfo()、在 Python 裡用 pip freeze 就能列出你自己的版本清單, 建議附在報告的附錄裡。

授權、版權與引用方式

哪些東西不在這裡,以及為什麼

本教材沒有散布原論文的 PDF、補充材料(Data.xlsx、RChromOptim.RData、 R_Functions.docx、R_Instructions.pdf、Text files 資料夾)或示範影片—— 這些的著作權屬於原出版者(Elsevier)與原作者。 請透過你所屬機構的圖書館或期刊網站合法取得。

教材與腳本中出現的,只有少量事實性數據(9 個溶質在 4 個 φ 下的滯留時間, 以及論文正文已公布的模型參數範圍),且均已標明出處。 這樣的引用屬於學術寫作的正常範圍,也是讓你能親手驗證結果的必要條件。

如果你在報告或論文中使用本教材:

方法本身請引用原論文(這是必須的):
Zisi, Ch., Pappa-Louisi, A., & Nikitas, P. (2020). Separation optimization in HPLC analysis implemented in R programming language. Journal of Chromatography A, 1617, 460823. https://doi.org/10.1016/j.chroma.2019.460823

第 16、17 節的重新實作程式是本教材為教學目的獨立撰寫的, 與原作者無關,也未經原作者背書。若你直接使用或修改這兩份腳本,請一併註明來源網址, 並務必仍然引用上面那篇原論文——演算法與模型是他們的貢獻。

本教材的哪一部分授權條款你可以…
程式碼
.R、.py、兩份筆記本裡的程式區塊
MIT 自由使用、修改、散布,含商業用途;只需保留著作權聲明。
教材內容
本網頁全文、模擬器說明、測驗解說、輸出圖
CC BY-NC 4.0 自由用於教學、重製、翻譯、改作;需姓名標示並註明修改, 但不得作商業性使用。

程式碼刻意用 MIT 而不是 CC——Creative Commons 官方明確建議不要用 CC 授權軟體, 因為 CC 條款沒有處理原始碼/目的碼、專利授權等軟體特有的問題。
兩份授權條款的完整內容,見課程目錄下的 LICENSE 與 LICENSE-CONTENT.md。 這兩份授權都只涵蓋本教材自己的貢獻,不涵蓋、也無法授予上述第三方論文素材的任何權利。

你學會了什麼


23.附錄:pH 實驗指引(可印出帶進實驗室)

第 10 節的 pH 模擬器是算出來的。這份指引讓你用真實的儀器去驗證它—— 三支標準品、五種移動相、一個下午。

你要驗證的三個預測
  1. 咖啡因的滯留時間不會隨 pH 改變(它在 pH 2–8 都是中性)
  2. 苯甲酸與己二烯酸都會隨 pH 升高而提早出來(解離成陰離子,不再黏管柱)
  3. 苯甲酸會比己二烯酸早一步開始提早(pKa 4.20 < 4.76), 因此三者的出峰順序有機會翻轉
先說清楚:預測的是「趨勢」,不是具體數字

第 10 節模擬器裡的交叉 pH(4.45、5.42)是用代表性參數算的。 你那根管柱、你的乙腈比例、你的緩衝液濃度都不一樣, 實際的交叉點一定會落在別的位置。 定性的三個預測很穩健;定量的數字要靠你自己量。 如果你量到的交叉 pH 和模擬器不同,那不是實驗失敗,那正是實驗的價值。

1. 器材與試劑

項目規格備註
HPLC幫浦 + UV 或 PDA 偵測器 + C18 管柱矽膠基質 C18 的耐受範圍多為 pH 2–8,本實驗最高只到 7.0
苯甲酸分析級,MW 122.12水中溶解度低(約 3.4 g/L),需先溶於甲醇
己二烯酸分析級,MW 112.13水中溶解度更低(約 1.6 g/L)
咖啡因分析級,MW 194.19水溶性佳;粉末具毒性,勿吸入
磷酸二氫鉀 KH₂PO₄分析級配緩衝液用
磷酸 H₃PO₄ / 氫氧化鈉 NaOH調 pH 用腐蝕性,戴手套護目鏡
乙腈 ACNHPLC 級有毒、易燃,於通風櫃操作
甲醇HPLC 級配標準品母液用
pH 計使用前以 pH 4.01 / 7.00 標準液校正沒校正的 pH 計會毀掉整個實驗
0.45 µm 濾膜水系(緩衝液)/有機系(樣品)緩衝液務必過濾並除氣

2. 標準品配製

母液(各 1000 mg/L,分開配)

各稱取 25.0 mg,分別溶於甲醇後定容至 25 mL 量瓶。 三支分開配、分開保存(4 °C 避光,可放約一個月)。

混合工作標準品(各 50 mg/L,20 mL)

各取母液 1.0 mL(三支共 3.0 mL)於 20 mL 量瓶,以純水定容至 20 mL。
最終有機溶劑比例約 15%,低於移動相的乙腈比例,可避免溶劑效應造成的峰變形。

咖啡因的濃度可能需要調高

在 230 nm 下,苯甲酸吸收最強、己二烯酸中等,咖啡因相對弱(它的最大吸收在 273 nm 附近)。 如果第一針下去咖啡因的峰太小: 有 PDA 就同時監測 230 nm 與 273 nm; 只有單波長偵測器,就把咖啡因的濃度提高到 100–200 mg/L(另外配一支較濃的母液)。 濃度不影響滯留時間(第 3 節已用實測資料驗證過),所以三者濃度不同不會干擾本實驗的結論。

3. 移動相配方

五種移動相,差別只有緩衝液的 pH,其餘完全相同——這是本實驗唯一該變的變數。

緩衝液母液:25 mM KH₂PO₄

稱取 3.40 g KH₂PO₄ 溶於純水,定容至 1000 mL。 分成五份,各以 H₃PO₄ 或 NaOH 調到下列 pH:

編號目標 pH調整方向為什麼選這個點
A3.0加 H₃PO₄兩根酸完全質子化,對照組(多數文獻方法的條件)
B4.0加 H₃PO₄苯甲酸開始解離(接近其 pKa 4.20)
C4.5微調兩根酸解離程度差最大,最可能看到順序變化
D5.0加 NaOH己二烯酸也開始明顯解離
E6.0加 NaOH兩根酸皆大幅解離,預期都跑到咖啡因前面

移動相=乙腈 : 緩衝液 = 30 : 70(v/v),過濾除氣後使用。
若你原本的方法比例不同,請沿用原比例——重點是五次之間保持一致,不是用我建議的數字。

最容易做錯的一步:pH 要在「加乙腈之前」調

先把緩衝液調到目標 pH,再拿去和乙腈混合。 絕對不要混合之後才調 pH。

原因是加入有機溶劑會改變溶液的酸鹼行為,混合後量到的數值和純水相的意義不同 (文獻上分別記作 pHww 與 pHsw)。 國際慣例是報告水相在混合前的 pH。 兩種做法都能做出實驗,但混用就無法和任何文獻比較,也無法和你自己下一次的實驗比較。

磷酸鹽在 pH 4–5.5 的緩衝能力較弱

磷酸的兩個 pKa 是 2.15 與 7.20,所以本實驗最關鍵的 pH 4.5–5.0 區間, 磷酸鹽其實不是最佳緩衝劑。因應方式: ① 每一批現配現用,不要放隔夜; ② 調完之後把實際量到的 pH 記下來(例如你想調 4.50,實際是 4.47,就記 4.47); ③ 分析資料時用實際值,不要用目標值。 這不是瑕疵——真實的方法開發就是這樣處理的。

4. 儀器條件與操作順序

參數設定
管柱C18,5 µm,4.6 × 150 mm(或你原本使用的管柱)
流速1.0 mL/min
管柱溫度固定於 30 °C(溫度會影響滯留,必須控制)
注射量10 µL
偵測230 nm(有 PDA 則同時記錄 200–400 nm)
分析時間15 分鐘(pH 3.0 時滯留最長,預留餘裕)
1
量 t₀(每換一次移動相都要做)
注射 10 µL 純水或 0.01% 尿嘧啶(uracil), 記下第一個訊號擾動出現的時間。沒有 t₀ 就算不出 k,整個實驗就白做了(見第 4 節)。
↓
2
單標定位
三支標準品各單獨注射一次,確認誰是誰。 pH 改變後順序可能翻轉,不能靠「上次的順序」認峰——這正是本實驗的重點。
↓
3
混標分析
注射混合工作標準品,重複 2–3 針取平均。記錄每個峰的 滯留時間、峰高、峰面積與軟體報告的 Rs。
↓
4
換下一個 pH
先以純水沖 20 分鐘洗掉鹽類,再以新移動相平衡 至少 10 管柱體積(約 20–30 分鐘)直到基線穩定。回到步驟 1。
管柱保養:實驗結束一定要做

絕對不要讓緩衝液留在管柱裡過夜——鹽類析出會堵塞管柱與幫浦。 結束時依序沖洗:純水 20 分鐘 → 甲醇或乙腈/水 50:50 共 20 分鐘,再保存。

5. 資料記錄表

實測 pHt₀ (min)咖啡因 tR苯甲酸 tR己二烯酸 tR出峰順序最小 Rs
A ____________________________
B ____________________________
C ____________________________
D ____________________________
E ____________________________

6. 預期結果與判讀

你會看到代表什麼
咖啡因的 tR 五次幾乎相同(變化 < 2%) ✓ 預測 1 成立。它是中性分子,pH 動不了它。
也是很好的內部檢查:若咖啡因跑掉了,代表溫度、流速或管柱狀態有變,而不是 pH 的效應。
兩根酸的 tR 隨 pH 升高而變短,畫成圖是 S 形下降 ✓ 預測 2 成立。解離成陰離子後不再親油。
苯甲酸下降得比己二烯酸早 ✓ 預測 3 成立。pKa 較小者先解離。
某個 pH 下少了一個峰 很可能是共沖提,不是樣品出問題(見第 10 節思考題二)。 用單標各打一針確認,或改用 PDA 檢查峰純度。
高 pH 下所有峰都擠在死時間附近 兩根酸都完全解離了。此時 pH 已無法用來分離, 要改調乙腈比例(第 6–8 節)。

7. 資料分析:擬合模型 7

把每個 pH 下的 tR 換成 k = (tR − t0)/t0, 再擬合第 10 節那條式子,就能得到每個化合物的 k0、k1 與 pK。

R模型 7 擬合(已實測驗證)
library(tidyverse) # 模型 7:k = (k0 + k1*10^(pH-pK)) / (1 + 10^(pH-pK)) fit_model7 <- function(pH, tR, t0) { k <- (tR - t0) / t0 m <- nls(k ~ (k0 + k1 * 10^(pH - pK)) / (1 + 10^(pH - pK)), start = list(k0 = max(k), k1 = min(k), pK = median(pH)), control = nls.control(maxiter = 200, warnOnly = TRUE)) tibble(k0 = coef(m)[["k0"]], k1 = coef(m)[["k1"]], pK = coef(m)[["pK"]], rss = sum(residuals(m)^2)) } # 換成你自己的數字 pH <- c(3.02, 4.03, 4.47, 5.01, 5.98) # 實測 pH,不是目標值 tR <- c(5.37, 5.10, 4.42, 3.55, 2.71) # 苯甲酸的滯留時間 fit_model7(pH, tR, t0 = 1.20)
五個 pH 點就夠了嗎?夠。

我們用已知參數造假資料(加上 ±0.02 分鐘的量測誤差)測試過這段程式: 用 7 個 pH 點,擬合回來的 pK 誤差是 0.02;只用 5 個點,誤差 0.03。 對一堂實驗課來說,五個點是合理且足夠的設計。
擬合出來的 pK 可以和文獻值比對(苯甲酸 4.20、己二烯酸 4.76)。 但別期待完全相同——你量到的是在含 30% 乙腈的環境中的表觀值, 通常會比純水中的文獻值高 0.3–0.8。這個差距本身就是很好的討論題材。

8. 報告該回答的問題

安全提醒

乙腈與甲醇有毒且易燃,配製與廢液處理都在通風櫃內進行,全程戴手套與護目鏡。 磷酸與氫氧化鈉具腐蝕性,調 pH 時緩慢加入並持續攪拌。 咖啡因粉末具毒性,稱量時避免揚塵。 含乙腈的廢液屬有機溶劑廢液,不可倒入水槽。