本篇階段:Prj#2 架構專案
使用介面:Claude Code(via VS Code)
Day 13 做了三個小工具,做法是「先少講一點,讓問題自己浮出來」。起初沒給架構、沒給 UI 規範,程式能跑但很亂,然後逐漸補條件收斂。那套流程在小工具上算是 OK 的,因為那些工具錯了看得出來:圖示歪掉、視窗超出螢幕、轉檔失敗,用眼睛很簡單就可以驗收。
Prj#2 是一個統計分析程式,這與前面的 Prj#1 不一樣。統計程式跑完會給我有小數點的數字、配色漂亮的圖。但是不是對的,用看的無法得知。
所以這個專案我打算先決定資料從哪來、要看哪些圖、用什麼方法選變項、拿什麼指標驗收。等這四件事都有具體規劃以後,明天再一次餵給 Claude Code。今天的產出會是一份資料樣板(加上說明檔案)、一張視覺化的 HTML,還有一份 prompt。
這一篇的資料樣板、格式說明文件與視覺化 HTML 請參考我的 GitHub 對應的專案連結
昨天那三個工具,驗收條件都可以寫成測試:.ico 裡面必須有六種正方形尺寸、動畫 .webp 只取第一幀、壓完的 PDF 要小於指定大小。這些都是「輸入 A 就該得到 B」,機器自己就能判斷過不過。
迴歸分析不是這種東西。同一份資料,我可以跑出 R² = 0.64 的模型,也可以塞進一堆變項讓 R² 變成 0.70,兩個都不會報錯。差別在於後者是不是在騙自己。程式碼層面的測試擋不住這種問題,能擋住的只有方法本身選對。
所以今天要決定的是四件事,順序也是刻意的:
| 順序 | 要決定的 | 為什麼是這個順序 |
|---|---|---|
| 1 | 資料從哪來 | 沒有一份能重跑的資料,後面全部沒得驗 |
| 2 | 先把資料看過一遍 | 不先看分布跟相關,直接建模等於閉著眼睛算 |
| 3 | 用什麼方法挑變項 | 六個輸入不能全丟進去,也不能憑感覺刪 |
| 4 | 拿什麼指標驗收 | 指標選錯,模型好壞就是一句「感覺不錯」 |
第一個問題是資料。工作上手邊當然有資料,但那些不能用:有個資、不能公開、而且別人拿不到就沒辦法重現我產出的任何一個數字。
我去找了公開資料集,用的是 SEM 教材裡很常出現的:Worland 等人 1984 年發表在 Journal of Abnormal Child Psychology 的論文所附的相關矩陣。原研究追蹤了 158 名兒童,看的是高/中/低精神病理風險之下,智力、課堂行為與學業成就之間的關係,論文裡把 12 個觀察變項的相關矩陣完整印了出來。Kline 那本 SEM 教科書拿它當範例,UCLA OARC(原 IDRE)的 Mplus 課程講義也用它,後者還把資料檔 worland.dat 放在網站上。
有件事要先說明:worland.dat 裡那 500 筆不是原始受試者,而是照著論文發表的相關矩陣抽出來的合成資料,12 個變項全部標準化成平均 0、標準差 1。聽起來像缺點,但以示範來說滿合適的,因為抽樣誤差已經被消除,樣本相關係數會與論文上印的數字相同。
總共 12 欄,此處僅取 9 欄(教科書上那三組潛在因素):
| 潛在因素 | 原始欄位 | 樣板欄位 | 中文 |
|---|---|---|---|
| 適應 Adjustment | motiv / harm / stabi |
U_motiv / U_harm / U_stabi |
學習動機/人際和諧/情緒穩定度 |
| 風險 Risk | ppsych / ses / verbal |
U_ppsych / U_ses / U_verbal |
家長負向心理狀態/社經地位/語文智力 |
| 學業成就 Achievement | read / arith / spell |
U_read / U_arith / U_spell |
閱讀能力/算術能力/拼字能力 |
沒取的是 extra、vissp、mem。前六欄當 Input、後三欄當 Result,剛好湊出「多個輸入對多個結果」的資料型態。數值一律乘以 10(平均 0、標準差 10),除此之外沒改;ID 欄是我自己編的流水號,不屬於原始資料。
來源與對照分別寫進 (1) Definition 的 Note 列標中文名、原始變項名與所屬構念 (2) 格式說明文件裡另外開了一節放完整引用、下載連結與因素分組。
樣板裡有兩張工作表,Data 放資料,Definition 放每個欄位的角色。這張說明書是整個專案的設計核心,因為它決定了明天那支程式要怎麼寫。
Definition 是轉置的,欄代表特徵,列代表屬性。七列分別是 Feature、Feature_Type、Priority、Data_Type、Continuity、Category、Note。真正會影響分析流程的只有兩列:Feature_Type 用 Info_XX / Input_XX / Result_XX 決定這一欄要不要進分析、進去以後是 X 還是 Y;Data_Type 用 Continuous / Categorical 決定輸入端要標準化還是 One-Hot、結果端要走迴歸還是分類。
會這樣設計,是因為我不想讓使用者在 UI 上一欄一欄手動指定角色。十個欄位還好,六十個欄位點到手軟,而且每次重跑都要重點一次。把角色寫在 Excel 裡,程式讀進來就知道要做什麼,UI 上只留「哪幾欄這次不要分析」這一個選項。
順帶一提,Priority 那一列我留著但實際上不影響任何事(可以當作人去判斷的時候看哪個變項比較重要),Continuity 和 Category 也只是說明性的,這這版程式不會拿它們去過濾超出範圍的值。
/dataviz:先把資料看過一遍資料準備好之後,第二件事是看圖。這次我用 Claude 的 /dataviz 來做這件事。這是一個管視覺化設計規範的 skill,會在畫圖之前先載入,管的是配色公式、圖表形式的選擇、圖例與座標軸的規則這些東西。我給的要求很簡單:讀這份 Excel,做一份 HTML,有沒有分布、相關、迴歸係數之類的我都沒有指定。
而產出是一份單檔 HTML,Data_Template_Stats_Dashboard.html,唯一的外部相依是 Google Fonts,其餘的 CSS 和 JS 全部內嵌,資料則是以一段 JSON 直接寫在檔案裡,可以直接打開。
有幾個決定是它自己做的:
一個是顏色只負責方向和強度。相關矩陣用藍到紅的發散色階,正負各五階,可是每一格裡面都印著實際的 r 值,判讀不必靠顏色。這樣即使有人是色覺障礙、或者把這頁印成黑白,資訊也沒有掉。它還多做了一個「紋理輔助」的開關,打開之後正負相關會用不同的網點區分。順帶一提,深淺兩套色彩 token 它也各寫了一份,--plane、--surface-1 這些變數在深色模式下都有對應值,切換的時候不會有哪個顏色沒跟上。
另一個是九張直方圖共用同一組組距和同一條 y 軸。組距寬度 5,橫軸 −40 到 35,縱軸上限九張共用。因為共用,九張圖的高低才可以直接比;如果每張圖各自 autoscale,看起來會全部一樣高。
第一件是輸入特徵之間有相關。U_motiv 和 U_harm 的相關高達 0.77,U_stabi 跟這兩個也都在 0.58 上下。VIF 表把這件事講得更清楚:U_motiv 的 VIF 是 2.87、U_harm 是 2.65,其餘四個落在 1.3 到 1.8 之間。2.9 還沒到常見警戒值 5,但已經足夠讓係數估計有所不穩定。
第二件是結果變項之間也高度相關,U_read 和 U_spell 是 0.87。這代表三個模型大概會挑出很像的變項組合,如果跑出來差很多,反而要回頭檢查程式。
第三件是那張「零階相關 vs 標準化 β」的圖是整篇的轉折點。以閱讀能力為例:
| 輸入特徵 | 單獨看的 r | 放進模型的 β | p 值 |
|---|---|---|---|
U_verbal 語文智力 |
0.78 | 0.645 | < 0.001 |
U_motiv 學習動機 |
0.53 | 0.217 | < 0.001 |
U_ses 社經地位 |
0.43 | 0.042 | 0.20 |
U_harm 人際和諧 |
0.42 | −0.065 | 0.14 |
U_ppsych 家長負向心理狀態 |
−0.39 | −0.051 | 0.10 |
U_stabi 情緒穩定度 |
0.36 | 0.042 | 0.23 |
六個變項單獨看都跟閱讀能力有關,相關係數從 0.36 到 0.78 都有。可是一起放進模型之後,剩下兩個還有關係:U_verbal 的 β 是 0.645,U_motiv 是 0.217,其餘四個全部不顯著。U_ses 單看 0.43,控制其他五個之後只剩 0.042,p 值 0.20。
更有趣的是算術能力那一組,那裡有兩個變項直接不同。U_ppsych 單獨看的相關是 −0.24(家長精神病理越嚴重、算術越差),放進模型之後 β 變成 +0.097、p 值 0.004,符號翻過來而且顯著。U_harm 更有明顯變化,單看是 +0.44,模型裡是 −0.115、p 值 0.015。這種抑制效果在心理計量資料裡不算罕見,但如果直接看一張相關矩陣就決定要留哪些變項,百分之百會做錯決定。
三個模型的解釋力:閱讀 R² = 0.643、算術 0.580、拼字 0.504,調整後的 R² 各自掉 0.005 上下。
看到這裡,「該留哪些變項」這個問題就不能用眼睛回答了。
這裡要講清楚,免得後面混淆。今天這份 HTML 是我自己看的,屬一次性使用,目的是在寫 prompt 之前,先弄懂這份資料長怎樣。明天那支程式要輸出的是另一批圖,而且是每跑一次就重新輸出一次。
程式那邊我會指定五種圖:
coolwarm)會指定得這麼細,是因為今天這一輪讓我知道自己要看什麼。violin 包 boxplot 是因為只看 violin 讀不出四分位數、只看 boxplot 又看不出雙峰;相關矩陣範圍要寫死 −1 到 +1,是因為如果讓它跟著資料 autoscale,同一支程式跑兩份資料出來的顏色深淺就沒得比。
Lasso(Tibshirani, 1996)是在最小平方的目標函數後面加一個 L1 罰項:
minimize ‖y − Xβ‖² / (2n) + α · ‖β‖₁
L1 這個罰項的特性是,它會把不重要的係數壓成真正的 0,不是壓成 0.0001。所以 Lasso 跑完會直接給出一組「被選中的變項」,這也是它常被當成特徵選取工具用的原因。相較之下 Ridge 用的是 L2,只會把係數壓小,永遠不會歸零,拿來做選取就沒有意義。
問題出在穩定性。Lasso 要能保證選出正確的變項集合,理論上需要滿足一個叫 irrepresentable condition 的條件(Zhao & Yu, 2006),大意是「真正沒用的變項,不能被有用的那群變項解釋得太好」。這個條件在實務資料上經常不成立,尤其是預測變項彼此相關的時候。
不成立會發生什麼事?兩個高度相關的變項,Lasso 傾向只留一個、砍掉另一個,而留哪一個很大程度是隨機的。資料抽樣抖動一點點,選出來的組合就會更換。這份樣板剛好有現成的例子:U_motiv 和 U_harm 相關 0.77,Lasso 每次挑誰,基本上就是擲骰子。
如果只是要預測,這不是大問題,反正兩個都能撐起差不多的預測力。但如果要交出一份報告說「影響閱讀能力的因素是哪幾個」,那就不太相同了。
Bolasso 是 Francis Bach 在 2008 年 ICML 提出的(Bootstrap-enhanced Lasso),做法出乎意料地土法煉鋼:
第 3 步是整個方法的核心:想一下一個真正沒用的雜訊變項要怎麼活下來?它必須在 m 次獨立的抽樣裡每一次都被 Lasso 選中。假設它單次被誤選的機率是 p,那它撐過 m 次的機率大約是 p 的 m 次方。m 拉到 200,這個數字小到不必討論。Bach 在論文裡證明的就是這件事:選到正確變項集合的機率會隨著樣本數指數收斂到 1,而且不需要 irrepresentable condition。
反過來說,真正有用的變項在每一次抽樣裡都會被選中,交集不會去傷到它,這個方法的不對稱性就是對雜訊很嚴格,但對訊號很寬容。
第 4 步也不能省,Lasso 的 L1 罰項會把留下來的係數也一起壓小(shrinkage),直接拿 Lasso 的係數去解釋會低估效果量。選完變項之後重新用 OLS 配一次,係數才是沒有偏差的,也才能拿到標準誤和 p 值。這種「先選再配」的兩段式做法,實務上比「一段到底」常見得多。
這裡有個 Bolasso 沒有替我決定的東西:**每一輪的 α 要怎麼挑。**罰項的強度直接決定會留下幾個變項,α 給大一點就砍得凶、給小一點就留得多,而 200 輪要用同一個 α 還是各自挑,答案不一樣。原始論文是在一整段 α 的範圍上看結果的穩定性;實作上比較省事的是每一輪都用交叉驗證各自選一次(LassoCV 就在做這件事),代價是慢一點,好處是不必自己決定那個數字。我選後者,因為這支程式將來要吃的是別人的資料,我沒辦法預先知道一個對所有資料都合適的 α。這也是為什麼 200 這個次數不能再往上加太多:三個結果變項各 200 輪,每一輪裡面還包一次交叉驗證,實際跑的 Lasso 數量是這個數字再乘上幾倍。
嚴格取交集有個副作用,Bach 自己也講過,所以論文裡另外給了一個放寬的版本,通常叫 Bolasso-S(soft):不要求 100%,改成「被選中的比例超過某個門檻」就留,門檻常設在 90%。
為什麼需要這個?回到 U_motiv 和 U_harm 那對 0.77。因為 Lasso 在相關變項之間是隨機挑的,200 次裡可能 U_motiv 中了 130 次、U_harm 中了 70 次。取嚴格交集的結果是:兩個都被砍掉。明明其中至少有一個是真的有用,卻因為它們互相搶票而同歸於盡。
門檻放到 90% 也救不了這種對半分的情況,但至少可以擋掉「某一個變項因為一兩次抽樣意外落選就整組被砍」的意外。我這次打算用 90% 的門檻,同時把每個變項的實際入選次數印在報告裡。因為那個次數本身就是資訊:中 198 次和中 182 次的意義不一樣,而中 130 次的那個,就是在提醒我它跟別人共用了解釋力。
Bolasso 適合的情境,大致是這幾種:
不適合或者說沒必要的情境也很明確。樣本數只有三、四十筆的時候,bootstrap 抽出來的每一份都在重複同一批人,投票結果會很虛。變項之間幾乎不相關的時候,一次 Lasso 就夠了,跑 200 次只是把同一個答案算 200 遍。還有一種是純預測任務,只在乎誤差多小、不在乎裡面有哪些變項,那直接用 Elastic Net 或梯度提升樹通常更划算。
坦白講,我這份樣板只有六個輸入變項,用 Bolasso 是有點小題大作,六個變項的所有子集也才 64 種,全跑一遍都比 bootstrap 快。我還是要做,是因為這支程式的定位是給別人的資料用的樣板,Definition 那張表塞六十個欄位進去是很正常的事。用一份我知道正確答案的資料,先確認流程跑得對,比拿真的專案資料當白老鼠安全。
順帶一提,同一個年代還有一個思路很接近的方法叫 stability selection(Meinshausen & Bühlmann, 2010),它抽的是半份資料而不是 bootstrap,而且對「選錯的變項數量」給出了理論上的期望值上界。兩邊的假設是同一個:單跑一次得到的變項名單本身沒什麼參考價值,要重複很多次之後看誰一直在,那個名單才有意義。
Bolasso 主要就是搭配多元線性迴歸在用,這也是 Bach 論文裡的設定,這次的實作也是這樣。但值得講清楚的是:**Bolasso 本身是一個選變項的外殼,不是一個模型。**它包在外面做的事情就是重複抽樣、投票、取交集,裡面那個模型是可以抽換的。
抽換的時候只有一個硬性條件:裡面那個模型必須會產生稀疏解,也就是它得有辦法把某些變項的係數變成 0。這是投票機制成立的前提,沒有 0 就沒有「被選中」和「沒被選中」的分別,也就沒有票可以投。所以 Ridge 不能當底,Elastic Net 可以(它是 L1 加 L2 的混合,還保有 L1 的稀疏性),Adaptive Lasso(Zou, 2006)也可以,而且它本身就是為了修 Lasso 的一致性問題設計的。
結果變項如果是類別型的,換成 logistic regression 加 L1 罰項就好,投票邏輯不用改,只是最後 refit 的時候配的是 logistic 而不是 OLS。那份格式說明文件裡留了 Categorical 這個選項,就是為了這條路,雖然這次的樣板九個欄位全是連續型,暫時走不到。(以後有機會再說)
至於選完變項之後最終那個模型,自由度更大。這次用 OLS,但如果資料裡有明顯的離群值,換成 Huber 迴歸會比較穩;如果殘差的變異數不齊,可以改用加權最小平方或直接報 robust 標準誤;如果結果變項是次數,Poisson 迴歸才是對的。這些都不影響前面那 200 次投票。
Prompt 裡我要求 MAE、MSE、RMSE、Max Error、R²、Adjusted R² 六個都要算,而且要畫在同一張圖上。會列這麼多,是因為它們回答的問題真的不一樣:
| 指標 | 它在回答什麼 | 什麼時候看它 |
|---|---|---|
| MAE | 平均而言差多少 | 想知道「典型誤差」,不想被離群值影響 |
| MSE | 平方誤差的平均 | 內部最佳化用,單位是原單位的平方 |
| RMSE | 開根號回到原單位 | 想用原始尺度講誤差,但對大誤差敏感 |
| Max Error | 最糟的那一筆差多少 | 在乎最壞情況,例如不能有人被誤判太離譜 |
| R² | 解釋掉多少變異 | 跨資料集比較模型好壞 |
| Adjusted R² | 扣掉變項數的懲罰之後還剩多少 | 比較變項數不同的模型 |
MAE 和 RMSE 一起看最有用。兩個差不多,代表誤差分布得很平均;RMSE 明顯大於 MAE,代表有少數幾筆錯得特別離譜,那就該回去找那幾筆是誰。這也是我要 Max Error 的理由,它會直接把最糟的那一筆的規模講出來。
R² 和 Adjusted R² 的差別在這個專案裡特別重要,因為 Bolasso 選完變項之後,不同結果變項留下來的變項數可能不一樣。R² 有個惡名昭彰的性質:多加一個變項它只會上升,即使加的是亂數。Adjusted R² 用自由度做懲罰,這樣「六個變項的模型」和「三個變項的模型」才有得比。以這份樣板的三個模型來看,兩者差距都在 0.006 以內(0.643 對 0.639),因為 500 筆配 6 個變項算很寬裕;換成 50 筆配 20 個變項,差距會非常明顯。
上面四件事定案之後,prompt 就寫得出來了。這次的結構分成三塊:
基礎設計講技術選型,Python ttk、MVP 架構、用 /design 先做 UI、要 .ico、要 requirements.txt 和 .spec。這五條全部是 Day 13 中有提到的,這次直接當起手式寫進去。
UI 功能講畫面上要有什麼,以及執行中不能做什麼。這裡最重要的是兩條防呆:分析執行中要把所有按鈕和輸入鎖掉,以及執行中按關閉要先跳確認、確認之後必須真的把背景工作停掉、不能留殘留。第二條就是昨天 webp2image 那個「先取消 after 輪詢,再通知執行緒停止,最後才 destroy()」的教訓,換一支程式照樣適用。
分析流程把今天決定的東西按順序寫成五步:讀資料、建輸出資料夾、五種視覺化、每個結果變項各跑 200 次 Bolasso、建模型出報表。視覺化那段我寫得特別細,連 violin plot 裡要包 boxplot、histogram 要 50 個 bin 加擬合線、相關矩陣要套 coolwarm 都指定清楚。
刻意留白的地方也有。第 5 步我寫了一句「統計模型結果要有合適的表與圖(可能是我沒想到的部分)」,這是故意的。Day 13 的經驗是少講會讓問題浮出來,但那是在我不知道自己要什麼的時候;現在我知道八成,剩下兩成留給它補。
提示詞內容:
幫我建立一個迴歸統計分析程式。
[基礎設計]
1. UI 類別: Python TTK
2. 架構: MVP
3. UI 設計: 透過 /design 設計
4. 使用 .ico 檔案
5. 製作 requirements.txt 與 .spec 檔案
[UI功能]
1. 選取 EXCEL 並嘗試讀取,要能確認格式符合規定 (見 Data_Template_Stats_Format_Definition.md)
2. 辨識輸入變項與結果變項,讓使用者選擇哪些要排除不要分析
3. 畫面上要包含 (1)選取EXCEL表 (2)選擇輸出目錄(預設和輸入EXCEL表相同) (3)修改輸出資料夾名稱(預設 "Output [{INPUT FILENAME}] @{yyyy-MM-dd HHmmss}") (4)EXCEL欄位選擇(上面第2點) (5)開始分析按鈕 (6)進度條(只要左右動、顯示還在進行中就可) (7) 分析LOG(顯示目前進度)
4. 按下 "開始分析" 並執行中時,關閉原本 UI 可以使用的所有按鈕功能、修改名稱功能
5. 執行中按下關閉時,先跳出警告視窗確認使用者意圖,若真的要關閉,就停止分析並且不要在背景留下殘留
[分析流程]
1. 讀取資料
2. 建立輸出資料夾 "Output [{INPUT FILENAME}] @{yyyy-MM-dd HHmmss}"
3. 統計視覺化 (沒有特別寫就是輸出到 "視覺化輸出資料夾" - 新建 "Visualization" 資料夾):
- a. 所有連續型欄位畫 violin plot (內含 boxplot),同一張圖有許多子圖
- b. 所有連續型欄位畫 histogram (各50個bin、擬合線),一個變項一張圖,在 "Visualization" 建立子資料夾 "Variable Analysis" 並放入
- c. 所有連續型輸入欄位與所有連續型結果欄位畫 scatter plot,一個結果變項一張圖含不同輸入變項之子圖,在 "Visualization" 建立子資料夾 "Input×Result Analysis" 並放入
- d. 所有連續型欄位畫 Spearman / Pearson Correlation Matrix (範圍 -1 ~ +1、套用colormap "coolwarm"),同一張圖
- e. 所有連續型欄位畫一張 pair plot。
4. 針對不同結果變項,分別進行200次 Bolasso ,以篩選欄位
5. 針對不同結果變項,建立統計模型並撰寫分析報告
- a. 統計模型的結果需要整理成合適的EXCEL報表
- b. 回歸統計指標請包含 MAE, MSE, RMSE, Max Error, R², Adjusted R² 並畫在一張圖上
- c. 輸出EXCEL表,不同工作表代表不同結果變項,包含原始的欄位與模型預測的數字,以及誤差值(pred-true)與誤差率
- d. 統計模型結果要有合適的表與圖 (可能是我沒想到的部分) 並輸出到 "Output @{yyyy-MM-dd HHmmss}"
- e. 分析報告請撰寫成一份 Word 並輸出到 "Output @{yyyy-MM-dd HHmmss}"
今天一行程式都沒寫,但這是這個專案最該花時間的部分。
三個小工具出錯會自己跳出來,統計程式不會,它會安安靜靜地給出一個看起來很專業的答案。今天做的四件事,本質上都是在建立「我怎麼知道它算錯了」的能力:找一份我知道正確答案的資料、先把圖看過一遍好知道模型該長什麼樣子、用一個對雜訊嚴格的方法挑變項、用六個角度不同的指標交叉檢查。
明天就把這份 prompt 丟給 Claude Code,看它做出什麼。
Anthropic 官方文件
資料集
worland.dat 的說明) — https://stats.oarc.ucla.edu/mplus/seminars/mplus-class-notes/cfa/
worland.dat 資料檔 — https://stats.idre.ucla.edu/wp-content/uploads/2018/01/worland.dat
統計方法文獻
套件與工具文件
註一:
Data_Template_Stats.xlsx裡的 500 筆是合成資料,照 Worland et al. (1984) 已發表的相關矩陣抽出,不是原研究那 158 名兒童的原始資料。因為抽樣誤差被消去了,p 值衡量的是生成設定而不是真實抽樣,文中的顯著性只用來標示效果的相對強弱,不能拿來當任何心理學或教育學上的結論。
註二:第 4、5 節寫的是原理與規劃,明天實作出來的結果不一定跟這裡寫的一樣,跑出來再說。
註三:文中的統計數字取自今天那份視覺化 HTML,為了閱讀有四捨五入,完整數值請看前面附的 GitHub 連結。