這五行程式印出 0.99,看起來像個好模型。但它的內容是「全部都說沒問題」——recall 是 0、precision 是 0、F1 是 0。這一題考的不是算術,而是知不知道 accuracy 在 1% 正類的資料上完全沒有鑑別力,以及接下來該做什麼。
某詐欺資料的正類比例約為 1%,團隊先以「全預測為負類」的基準模型觀察 accuracy。若要訓練並選擇真正能辨識少數正類的分類器,下列後續方案何者最合理?
import numpy as np # 把 NumPy 拿進來,取個綽號叫 np from sklearn.metrics import accuracy_score # 只把「算 accuracy 的那支工具」拿出來 y_true = np.array([0] * 990 + [1] * 10) # 真實答案:990 個 0、10 個 1 y_pred = np.zeros_like(y_true) # 模型的預測:全部都填 0 print(accuracy_score(y_true, y_pred)) # 印出 accuracy
一份資料裡,兩個類別的數量差很多,就叫類別不平衡。詐欺偵測、罕見疾病篩檢、機台故障預測、廣告點擊——這些題目天生就長這樣:你真正想抓的那一類,佔比往往不到 1%。
| 模型預測 0(正常) | 模型預測 1(詐欺) | 這一列合計 | |
|---|---|---|---|
| 真實是 0(正常) | 990 TN 真陰性 | 0 FP 偽陽性 | 990 |
| 真實是 1(詐欺) | 10 FN 偽陰性 | 0 TP 真陽性 | 10 |
| 這一欄合計 | 1000 | 0 | 1000 |
整個「預測 1」那一欄是空的。模型從頭到尾沒說過任何一筆是詐欺,所以 TP = 0、FP = 0。這一格是 0,就決定了後面所有跟正類有關的指標全部歸零。
UndefinedMetricWarning)、F1 0.0、balanced_accuracy 0.5、MCC 0.0。點擊後出現漸進式說明:白話說明 → 說清楚一點 → 常見錯誤與考點。
點擊後出現漸進式說明。
五行,一行一行走完。右上角的「看位置」可以把這一行放回完整程式裡看。
.這是物件導向的寫法。np.array(...)、np.zeros_like(...)、sklearn.metrics 都用到它。
np.array(...) 點的左邊是模組(numpy 的綽號) 唸「裡面的」 sklearn.metrics 點的左邊是套件,右邊是子模組 唸「底下的」 np.array(...) 有括號 = 呼叫,叫它做一件事 y_true.dtype 沒括號 = 屬性,看一格資料
import numpy as np # 把 NumPy 拿進來,取個綽號叫 np
NumPy 是 Python 做數值運算的函式庫,專門處理成排成列的數字。as np 是取綽號——之後寫 np. 就等於 numpy.,少打五個字。
這個綽號不是規定,但全世界都這樣寫(就像 pandas 一律叫 pd)。看到別人的程式碼出現 np.,你就知道那是 NumPy。
本題用 NumPy 只是為了做出兩排標籤,運算本身很單純。真正的工作在第 5 行。
from sklearn.metrics import accuracy_score # 只把「算 accuracy 的那支工具」拿出來
from A import B 的意思是「去 A 那個抽屜裡,只把 B 這一支拿出來」。拿出來之後直接寫 accuracy_score(...) 就好,不用寫 sklearn.metrics.accuracy_score(...)。
跟第 1 行的 import 比較:第 1 行是把整個工具櫃推過來(所以每次用都要先講 np.),第 2 行是只抽一把螺絲起子放桌上(拿了就能直接用)。兩種寫法都對,看你要用多少東西。
sklearn.metrics 這個模組裡放著所有評估指標:accuracy_score、recall_score、precision_score、f1_score、confusion_matrix、roc_auc_score、average_precision_score⋯⋯y_true = np.array([0] * 990 + [1] * 10) # 真實答案:990 個 0、10 個 1
這一行從最裡面往外讀,拆成三步:
① [0] * 990 → [0, 0, 0, ... , 0] 990 個 0 (串列乘上數字=重複這麼多次)
② [1] * 10 → [1, 1, ... , 1] 10 個 1
③ ① + ② → [0, 0, ..., 0, 1, ..., 1] 1000 個
np.array(③) → 變成 NumPy 陣列,存進 y_true
這裡的 * 不是乘法、+ 也不是加法。對串列來說,* 是「重複幾次」、+ 是「接在後面」。所以 [0] * 990 得到的不是 0,而是一串 990 個 0。
y_true 裝的是「標準答案」——這 1000 筆交易,哪些真的是詐欺。0 代表正常、1 代表詐欺,其中詐欺只有 10 筆。
y_pred = np.zeros_like(y_true) # 做一個跟 y_true 一樣大的陣列,但裡面全是 0
np.zeros_like(y_true) 的意思是「照著 y_true 的形狀與型別,做一個全是 0 的新陣列」。結果是 1000 個 0。
為什麼不直接寫 np.zeros(1000)?因為 zeros_like 會自動對齊形狀和資料型別——y_true 是 1000 個整數,做出來的 y_pred 就也是 1000 個整數。少一個地方會出錯。
DummyClassifier(strategy="most_frequent")。print(accuracy_score(y_true, y_pred)) # 印出 0.99
accuracy_score(y_true, y_pred) 把兩排答案一筆一筆比對,數出「猜對幾筆」,再除以總筆數。
accuracy = 猜對的筆數 ÷ 全部筆數
= (990 個 0 猜對) + (10 個 1 猜錯 → 0 筆)
= 990 / 1000
= 0.99
0.99(不是 0.99000001 之類的浮點誤差,因為 990/1000 剛好可以精確表示)。注意參數順序:真實答案在前、預測在後。寫反了在 accuracy 上剛好沒差(因為只是數相等的筆數),但在 recall_score、confusion_matrix 上寫反會直接算錯。
print() 只是把括號裡的結果「印到螢幕上」。沒有它程式一樣會算,只是你看不到。
先把一件事講死:在「全預測負類」這個做法下,accuracy 一定等於「負類佔全部的比例」。這是恆等式,不是巧合,也跟資料內容無關。
全預測負類的 accuracy = 負類筆數 ÷ 總筆數 = 1 − 正類比例 正類 50% → accuracy = 0.50 正類 10% → accuracy = 0.90 正類 1% → accuracy = 0.99 ← 本題 正類 0.1% → accuracy = 0.999
| 指標 | 算式 | 數值 | 它在說什麼 |
|---|---|---|---|
| Accuracy | (TP + TN) / 1000 = 990 / 1000 | 0.9900 | 猜對了 99% |
| Recall | TP / (TP + FN) = 0 / 10 | 0.0000 | 10 筆詐欺一筆都沒抓到 |
| Precision | TP / (TP + FP) = 0 / 0 | 0.0000 | 分母是 0,sklearn 會警告後回傳 0 |
| F1 | 2PR / (P + R) = 0 / 0 | 0.0000 | P 和 R 都是 0 |
| balanced accuracy | (Recall + 特異度) / 2 = (0 + 1) / 2 | 0.5000 | 等於亂猜 |
| MCC | 相關係數版的分類分數 | 0.0000 | 預測與真實完全無關 |
precision_score 會先噴 UndefinedMetricWarning: Precision is ill-defined and being set to 0.0 due to no predicted samples——連 sklearn 自己都在提醒你「這個模型從沒說過任何一筆是正類」。六個指標裡有五個都說「這模型毫無用處」,只有 accuracy 給了 0.99。這就是為什麼題目要你選「後續方案」——重點不是把 0.99 算出來,而是知道它沒有意義之後該做什麼。
題目寫「團隊先以全預測為負類的基準模型觀察 accuracy」——這個動作本身完全正確,錯的是停在這裡。
| 用途 | 怎麼用 |
|---|---|
| ① 當及格線 | 任何真模型都必須明顯贏過基準。贏不過,就代表特徵沒訊號或流程有問題。 |
| ② 揭穿指標 | 基準模型的 accuracy 是 0.99,就等於宣告 accuracy 在這份資料上的天花板幾乎沒有空間——真模型頂多從 0.99 進步到 0.995,看不出差別。 |
| ③ 定出 PR 的基準線 | 隨機猜的 PR-AUC = 正類比例 = 0.01;隨機猜的 ROC-AUC = 0.5。有了基準線,才知道模型的分數算不算好。 |
DummyClassifier:strategy="most_frequent"(永遠猜最多的那一類,就是本題)、"stratified"(照比例亂猜)、"prior"、"uniform"。如果團隊看到 0.99 就說「模型不錯」,接下來會發生的事很好預測:
換指標的第一個直覺通常是「那我看 ROC-AUC 好了」。方向對,但在極度不平衡時,ROC-AUC 也會給你過度樂觀的答案。
| ROC 曲線 | PR 曲線 | |
|---|---|---|
| 橫軸 | FPR = FP / (FP + TN) | Recall = TP / (TP + FN) |
| 縱軸 | Recall = TP / (TP + FN) | Precision = TP / (TP + FP) |
| 用到 TN 嗎 | 用(藏在 FPR 的分母) | 完全不用 |
| 隨機猜的分數 | 永遠 0.5 | 等於正類比例(本題 0.01) |
| 不平衡時 | 會被龐大的 TN 稀釋,看起來很好 | 誠實反映「抓到的裡面有多少是真的」 |
關鍵在 TN。本題有 990 筆 TN,990 這個大數字塞在 FPR 的分母裡,就算誤報了 50 筆,FPR 也只有 50/990 ≈ 0.05,曲線看起來還是很漂亮。而 Precision 的分母裡沒有 TN,誤報 50 筆、抓對 5 筆,precision 就是 5/55 ≈ 0.09——藏不住。
| 正類比例 | ROC-AUC | PR-AUC | PR 隨機基準 | 贏過基準幾倍 |
|---|---|---|---|---|
| 50% | 0.9223 | 0.9227 | 0.5000 | 1.8× |
| 10% | 0.9214 | 0.6646 | 0.1000 | 6.6× |
| 5% | 0.9240 | 0.5448 | 0.0500 | 10.9× |
| 1%(本題) | 0.9212 | 0.2674 | 0.0100 | 26.7× |
| 0.1% | 0.9125 | 0.0690 | 0.0010 | 69.0× |
這張表也解釋了一個常見誤會:PR-AUC 比較低,不代表模型比較差。因為 PR 的基準線會跟著正類比例一起降,所以正確的讀法是「PR-AUC 除以正類比例」——贏過隨機猜幾倍。上表最右欄顯示,越不平衡的情境,同一個模型反而「贏得越多倍」。
average_precision_score(y_true, y_score) = PR-AUC(sklearn 官方推薦的算法,逐點加總,不做內插)precision_recall_curve(y_true, y_score) = 畫 PR 曲線用的三排數字roc_auc_score(y_true, y_score) = ROC-AUCPR-AUC 是把所有門檻掃過一遍的總分,但實務上你還是要挑一個門檻上線。所以單點的 precision / recall 一定要一起報,而且要說清楚是在哪個門檻下算的。
| scoring | 5 折平均 | 看到這個數字你會怎麼想 |
|---|---|---|
| accuracy | 0.9910 | 「太棒了」——但基準模型也有 0.9907 |
| roc_auc | 0.9429 | 「模型排序能力不錯」——這句是真的 |
| average_precision(PR-AUC) | 0.3011 | 「還有很大進步空間」——這才是實況 |
| recall | 0.0983 | 門檻 0.5 之下只抓到不到 1 成 |
| precision | 0.6464 | 抓到的裡面 6 成多是真的 |
換了指標之後,下一個問題是:模型本身要怎麼調,才會願意說「這筆是詐欺」?
訓練時模型在最小化損失(loss)。1000 筆裡有 990 筆是 0,只要把這 990 筆顧好,總損失就已經很低了。那 10 筆正類猜錯的代價,被 990 筆稀釋到幾乎看不見——所以「全部說 0」在數學上是個相當划算的解。
class_weight 做的事很單純:把少數類的錯誤放大幾倍再算損失。class_weight="balanced" 時,sklearn 自動把權重設成 n_samples / (n_classes × 該類筆數):1000 / (2 × 990) = 0.505 給負類、1000 / (2 × 10) = 50.0 給正類——猜錯一筆詐欺,等於猜錯 99 筆正常。兩條分佈是本站實跑模型分數的高斯近似(負類 ~ N(0,1)、正類 ~ N(2.1,1),測試集 15000 筆/正類 140 筆),數字與實跑會有小幅出入,趨勢一致。
| 做法 | TP | FP | FN | accuracy | recall | precision | PR-AUC |
|---|---|---|---|---|---|---|---|
| 全預測負類 | 0 | 0 | 140 | 0.9907 | 0.0000 | 0.0000 | 0.0093 |
| class_weight=None | 11 | 10 | 129 | 0.9907 | 0.0786 | 0.5238 | 0.2615 |
| class_weight='balanced' | 127 | 2028 | 13 | 0.8639 | 0.9071 | 0.0589 | 0.2653 |
balanced 之後 recall 從 0.079 衝到 0.907,代價是 precision 掉到 0.059、accuracy 掉到 0.864。| 做法 | 怎麼運作 | 優缺點 |
|---|---|---|
| class_weight | 不動資料,調整損失函數的權重 | 最省事、不會製造假資料;只支援有 class_weight 參數的模型 |
| 過抽樣 oversampling | 把少數類複製幾份 | 簡單,但重複樣本容易讓模型過擬合到那幾筆 |
| 欠抽樣 undersampling | 把多數類丟掉一部分 | 訓練快;丟掉的是真實資訊,資料少時很傷 |
| SMOTE | 在少數類之間內插造出新樣本 | 比單純複製好;高維或雜訊多時可能造出不存在的樣本 |
imblearn.pipeline.Pipeline),讓交叉驗證每一折都只在訓練部分重抽樣。分類器其實不直接吐出 0 或 1,它吐出的是一個介於 0 到 1 的分數(predict_proba)。predict 只是幫你套了一個 0.5 的門檻。
predict_proba(X)[:, 1] → 0.02, 0.71, 0.13, 0.49, 0.88, ... 模型給的風險分數
↓ 拿 0.5 當刀子切
predict(X) → 0, 1, 0, 0, 1, ... 變成類別
| 門檻 | TP | FP | FN | recall | precision | F1 | accuracy |
|---|---|---|---|---|---|---|---|
| 0.50 | 11 | 10 | 129 | 0.079 | 0.524 | 0.137 | 0.9907 |
| 0.30 | 32 | 46 | 108 | 0.229 | 0.410 | 0.294 | 0.9897 |
| 0.20 | 45 | 95 | 95 | 0.321 | 0.321 | 0.321 | 0.9873 |
| 0.10 | 63 | 215 | 77 | 0.450 | 0.227 | 0.301 | 0.9805 |
| 0.05 | 86 | 466 | 54 | 0.614 | 0.156 | 0.249 | 0.9653 |
| 0.02 | 113 | 1143 | 27 | 0.807 | 0.090 | 0.162 | 0.9220 |
| 0.01 | 122 | 1944 | 18 | 0.871 | 0.059 | 0.111 | 0.8692 |
把「漏抓一筆的損失」和「誤報一筆的成本」寫成數字,總成本就是:
\[ \text{總成本}(t) = C_{FN}\times FN(t) + C_{FP}\times FP(t) \]
把所有門檻掃過一遍,選成本最低的那一個。這件事 sklearn 有現成工具:precision_recall_curve 會一次回傳每個門檻的 precision 與 recall,剩下的就是四則運算。
prec, rec, thr = precision_recall_curve(y_val, prob) # 掃過所有門檻 n_pos = y_val.sum() # 驗證集有幾筆真正的正類 tp = rec[:-1] * n_pos # 每個門檻抓到幾筆 fn = (1 - rec[:-1]) * n_pos # 每個門檻漏掉幾筆 fp = tp / np.maximum(prec[:-1], 1e-9) - tp # 每個門檻誤報幾筆 cost = 500 * fn + 5 * fp # 漏抓 500 元、誤報 5 元 best = thr[cost.argmin()] # 成本最低的那個門檻
同樣使用本站實跑模型分數的高斯近似;橫軸是對數刻度,因為最佳門檻通常落在 0.001~0.1 這一段,線性刻度會全部擠在最左邊看不見。
把前面六節串起來,四個選項的對錯就一目了然。
整題的靶心就在這句話上。「accuracy 越接近 1 就越能找出正類」在不平衡資料上完全不成立——題目自己給的反例就寫在第 5 行:accuracy 0.99、找出正類 0 筆。
更致命的是:真模型的 accuracy 也是 0.9907,跟基準模型只差 0.0003。用一個天花板只剩 0.01 的指標去比較模型,等於沒有比較。
這一句把三件該做的事全部說對,而且順序也對:
① 換指標——PR-AUC 不用 TN,不會被 990 筆正常交易稀釋;precision/recall 補上單點的實況。
② 調訓練——class_weight 把少數類的錯誤放大,讓模型願意說「是詐欺」。
③ 調門檻——在驗證集上依漏抓/誤報的成本挑一條線,而不是照抄 0.5。
三個動作分別對應到「怎麼衡量」「怎麼訓練」「怎麼決策」,剛好是不平衡問題的三個層次。而且「在驗證集上」這五個字,正是它跟選項 C 的分水嶺。
這句話裡有兩個獨立的洩漏,任何一個都足以讓評估失效:
① 驗證集的樣本跑進訓練集。模型看過那些正類,等於考前看過考卷。
② 用「已經被汙染的驗證集」挑門檻。挑出來的門檻是為了那幾筆背下來的樣本量身訂做,換到真實資料上完全不適用。
如果真的想用「複製少數類」這招,正確做法是把它放進 Pipeline、只作用在訓練折,驗證折保持原樣。
這句話從頭到尾都在往錯的方向走:
① 「只降低 recall」等於更少抓到詐欺——題目要的是「真正能辨識少數正類」,這是反向操作。
② 「確保 accuracy 不變」把一個沒有鑑別力的指標當成要守住的目標。把 recall 降到 0,accuracy 就回到 0.99——那正是基準模型本人。
③ 「固定 0.5」放棄了成本這條最有效的槓桿。
照這個做法做到極致,你會得到的正是題目一開始那個什麼都不抓的模型。
看完之後再看一次同一段程式,應該有完全不同的感覺。
某詐欺資料的正類比例約為 1%,團隊先以「全預測為負類」的基準模型觀察 accuracy。若要訓練並選擇真正能辨識少數正類的分類器,下列後續方案何者最合理?
import numpy as np # 把 NumPy 拿進來,取個綽號叫 np from sklearn.metrics import accuracy_score # 只把「算 accuracy 的那支工具」拿出來 y_true = np.array([0] * 990 + [1] * 10) # 真實答案:990 個 0、10 個 1 y_pred = np.zeros_like(y_true) # 模型的預測:全部都填 0 print(accuracy_score(y_true, y_pred)) # 0.99
八題,答錯會直接告訴你錯在哪。
[0] * 990 的 * 是重複不是乘法、+ 是相接不是加法,做出來的是 1000 個標籤。np.zeros_like(y_true) = 照著 y_true 的形狀與型別做一個全 0 的陣列,也就是「全部預測負類」。from sklearn.metrics import ... 是從評估工具箱裡只抽一支工具出來;那個模組裡還有 recall、precision、PR-AUC 等一整排。class_weight="balanced" = n_samples / (n_classes × 該類筆數),把少數類的錯誤放大;它移動決策邊界,通常提升 recall、犧牲 precision,但不太會提升 PR-AUC。predict_proba 拿分數,在驗證集上依 \(C_{FN}\times FN + C_{FP}\times FP\) 挑成本最低的門檻。本站實跑省下 73.1% 成本。類別不平衡是中級科目三「機器學習技術與應用」的高頻考點,也會跨到科目二的資料準備與模型評估。最常見的五種問法:① 給一段「全猜多數類」的程式問 accuracy 是多少(送分,答案就是 1 − 正類比例);② 問 accuracy 很高但模型沒用是什麼情況;③ 問不平衡時該改看哪些指標(PR-AUC / recall / F1 / balanced accuracy / MCC);④ 給四個「後續方案」選最合理的(本題這種,錯的選項通常藏著資料洩漏或反向操作);⑤ 問 SMOTE/重抽樣該在切分前還是切分後做。把「衡量、訓練、決策三層都要動」和「任何驗證集資料流回訓練集都是洩漏」這兩句記熟,這一類幾乎都能秒殺。
accuracy 直覺,但它把兩類的錯誤看得一樣重,而不平衡資料的重點恰恰相反。本題 1000 筆裡只有 10 筆詐欺,模型就算全部漏掉,也只賠掉 1% 的 accuracy——這 1% 淹沒在 99% 裡看不出來。更實際的問題是天花板:基準模型已經 0.9907,真模型也是 0.9907,兩者差 0.0003,你不可能用這個數字選模型或做 early stopping。accuracy 適合的場合是「兩類數量差不多、兩類錯誤的代價也差不多」,例如貓狗分類;一旦其中一個條件不成立,就要換指標。
看你在意的是誰。ROC 曲線的橫軸 FPR = FP/(FP+TN),分母裡有 TN;不平衡時 TN 非常龐大,就算誤報了幾百筆,FPR 也還是很小,曲線看起來很漂亮。PR 曲線的兩個軸都不碰 TN,precision = TP/(TP+FP) 直接反映「示警的裡面有幾成是真的」,所以誤報藏不住。實務準則:正類比例低於 10%、或你關心的是「示警品質」時,以 PR-AUC 為主,ROC-AUC 當輔助。另外要注意基準線不同——ROC 的隨機基準永遠是 0.5,PR 的隨機基準等於正類比例,所以 PR-AUC 0.26 在 1% 正類的情境下其實是隨機的 28 倍,並不差。
公式是 n_samples / (n_classes × np.bincount(y))。以本題 1000 筆、990 個 0、10 個 1 為例:負類權重 = 1000/(2×990) = 0.505,正類權重 = 1000/(2×10) = 50.0。兩者相差約 99 倍,也就是「猜錯一筆詐欺,在損失函數裡等於猜錯 99 筆正常」。實際效果是把決策邊界往多數類那邊推,等價於偷偷調低了門檻。所以本站實跑會看到 recall 從 0.079 衝到 0.907、precision 從 0.524 掉到 0.059。要注意的是它幾乎不會提升 PR-AUC(0.2615 → 0.2653)——因為模型「排序」的能力沒變,只是切點換了位置。想真正提升 PR-AUC 要靠更好的特徵或更強的模型。
一定是切分之後,而且只對訓練折做。如果先對整份資料 SMOTE 再切分,合成樣本是由鄰近的真實樣本內插出來的,很可能「某筆合成樣本」進了訓練集、「生出它的那筆真實樣本」進了驗證集——模型等於看過答案,驗證分數必然虛高。正確做法是用 imblearn.pipeline.Pipeline 把 SMOTE 當成一個步驟串進去,交叉驗證時每一折都只在訓練部分重抽樣。這跟 StandardScaler 要放進 Pipeline 是同一個道理:任何「會從資料學東西」的步驟都不能先看到驗證資料。
有成本數字時,直接最小化 C_FN × FN + C_FP × FP 是最有說服力的做法,因為它把商業目標寫成了式子。沒有成本數字時,常見的替代方案有三種:① 最大化 F1(precision 與 recall 的調和平均,適合兩者同等重要);② 固定一個可接受的 precision 下限(例如「示警至少要有 30% 是真的,不然稽核人員會麻痺」),在滿足條件的門檻中挑 recall 最高的;③ 固定人力產能(例如「每天只能複查 200 筆」),就取分數最高的前 200 筆,這叫 precision@k。不管用哪一種,都要在驗證集上挑、在測試集上驗。
因為它同時毀掉了兩樣東西。第一,模型看過驗證集的樣本——複製過去的是一模一樣的資料點,容量夠大的模型(決策樹、隨機森林、KNN)可以直接把它們背下來。第二,挑門檻用的還是那個已經被汙染的驗證集,所以挑出來的門檻是為了那幾筆背下來的樣本量身訂做的。本站用 RandomForest 實跑的結果很極端:驗證集 F1 = 1.0000(完美),拿到乾淨的測試集上只剩 0.0141。最可怕的地方在於驗證分數是滿分,所以沒有任何警訊——你會很有信心地把一個沒用的模型送上線。
除了 PR-AUC、precision、recall、F1 之外,常見的還有三個。balanced accuracy = (recall + 特異度)/2,等於「兩類的 recall 直接平均」,全猜負類時剛好是 0.5,很適合當快速檢查。MCC(Matthews 相關係數)把四個格子全部用上,值域 −1 到 1,全猜一類時是 0,被認為是二元分類最穩健的單一數字。Fβ 分數則是 F1 的一般化:β>1 偏重 recall(例如 F2,適合詐欺、疾病篩檢),β<1 偏重 precision。實務上通常會同時報一組數字而不是只看一個,因為單一數字一定會藏東西。
兩件事。第一,一定要用 StratifiedKFold(分類問題的 cross_val_score 預設就是),讓每一折的正類比例都跟原始資料一樣。1% 正類的資料如果隨機切 5 折,很可能某一折一筆正類都沒有——那一折的 recall 會是 nan,PR-AUC 也算不出來。第二,scoring 要明確指定:cross_val_score 的預設是 accuracy,在不平衡資料上等於什麼都沒測。改成 scoring="average_precision"(PR-AUC)或傳一個 list 給 cross_validate 一次拿多個指標。本站實跑同一個模型:accuracy 0.9910、roc_auc 0.9429、average_precision 0.3011、recall 0.0983——換一個 scoring 就換了一個世界。