Mục lục
- Vì sao cần ROC/AUC — vượt qua giới hạn threshold
- TPR và FPR — hai trục của ROC
- ROC Curve — quét threshold từ 1 về 0
- AUC — diện tích dưới ROC
- Cách đọc AUC trực quan
- ROC/AUC trong sklearn
- predict_proba vs predict — sai lầm hay gặp
- PR Curve và Average Precision — alternative cho imbalanced
- Khi nào dùng ROC-AUC vs PR-AUC
- Multi-class ROC-AUC — OvR và OvO
- So sánh model bằng ROC
- Chọn operating point — Youden's J, cost-based, recall constraint
- Pitfall thường gặp
- Code Python — breast cancer và iris
- Bài tập thực hành
- Bài tiếp theo
Vì sao cần ROC/AUC — vượt qua giới hạn threshold
Precision, recall, F1 đều tính từ hard prediction sau khi áp threshold cố định (mặc định 0.5). Đổi threshold thì cả 3 metric đều thay đổi — báo cáo "F1 = 0.82" mà không nói threshold nào là thiếu thông tin.
ROC Curve và AUC giải bài toán khác: đánh giá khả năng phân biệt class của model một cách threshold-independent. Một model có AUC cao nghĩa là probability nó gán cho positive thường lớn hơn probability gán cho negative — bất kể sau đó ta chọn threshold nào.
Hai tình huống thực tế nên dùng AUC:
- So sánh nhiều model trước khi chốt threshold (Logistic Regression vs Random Forest vs Gradient Boosting).
- Báo cáo paper / benchmark khi threshold business chưa được định nghĩa.
TPR và FPR — hai trục của ROC
ROC dùng hai tỉ lệ tính từ confusion matrix:
\[ \text{TPR} = \frac{TP}{TP + FN} = \text{Recall}, \qquad \text{FPR} = \frac{FP}{FP + TN} \]- TPR (True Positive Rate) — tỉ lệ positive thực sự bị model bắt được. Đây chính là recall.
- FPR (False Positive Rate) — tỉ lệ negative bị model nhận nhầm thành positive. Còn gọi là fall-out; \( \text{FPR} = 1 - \text{Specificity} \).
Hai tỉ lệ này tính trên 2 nhóm sample tách rời: TPR chỉ dùng positive (mẫu số là tổng positive thực), FPR chỉ dùng negative (mẫu số là tổng negative thực). Đặc tính này quan trọng — sẽ giải thích vì sao ROC bị "kéo đẹp" với imbalanced data ở mục 9.
Mục tiêu của một classifier tốt: TPR cao (bắt được nhiều positive) đồng thời FPR thấp (ít báo nhầm). Hai mục tiêu này trade-off với nhau qua threshold.
ROC Curve — quét threshold từ 1 về 0
ROC (Receiver Operating Characteristic) là đồ thị tham số (FPR, TPR) khi quét threshold từ cao xuống thấp:
- Trục X: FPR (từ 0 đến 1).
- Trục Y: TPR (từ 0 đến 1).
- Mỗi điểm trên đường = (FPR, TPR) ở một threshold.
Hai điểm cố định ở hai đầu:
- Threshold = 1.0 → không sample nào được predict positive → \( TP = 0, FP = 0 \) → (FPR, TPR) = (0, 0).
- Threshold = 0.0 → tất cả sample đều predict positive → \( FN = 0, TN = 0 \) → (FPR, TPR) = (1, 1).
Khi threshold giảm dần từ 1 về 0, đường cong đi từ (0, 0) lên (1, 1). Model càng tốt thì đường càng "cong lên" về góc trái-trên (1.0 TPR ở FPR rất nhỏ). Model random tạo ra đường chéo (0, 0) — (1, 1).
Lưu ý kỹ thuật: ROC là step function — mỗi lần threshold vượt qua score của một sample mới, hoặc TP tăng 1 (sample đó là positive) hoặc FP tăng 1 (sample đó là negative). Không phải đường cong trơn.
AUC — diện tích dưới ROC
AUC (Area Under the ROC Curve), còn viết AUROC, là diện tích dưới ROC. Tính bằng trapezoidal rule trên các điểm step.
\[ \text{AUC} = \int_0^1 \text{TPR}(\text{FPR}) \, d(\text{FPR}) \in [0, 1] \]Các giá trị mốc:
- AUC = 1.0 — perfect classifier. Mọi positive đều có score cao hơn mọi negative, có ít nhất một threshold tách hoàn toàn hai class.
- AUC = 0.5 — random guess. ROC trùng với đường chéo (0,0) — (1,1). Model không có khả năng phân biệt.
- AUC < 0.5 — model tệ hơn random. Trường hợp này hiếm; nếu xảy ra có thể đảo prediction (predict 1 - p) để thu được AUC = 1 - AUC ban đầu > 0.5. Thường là dấu hiệu lỗi label hoặc lỗi pipeline.
Khoảng tham khảo:
- 0.5 — 0.6: gần như random.
- 0.6 — 0.7: yếu.
- 0.7 — 0.8: khá.
- 0.8 — 0.9: tốt.
- 0.9 — 1.0: rất tốt (kiểm tra xem có data leak không).
Các ngưỡng này chỉ là hướng dẫn — domain khác nhau có chuẩn khác nhau. Trong medical imaging AUC 0.8 đã là kết quả mạnh; trong fraud detection có khi cần 0.95+ mới triển khai được.
Cách đọc AUC trực quan
Có một interpretation xác suất rất gọn cho AUC: lấy ngẫu nhiên 1 sample positive và 1 sample negative, AUC chính là xác suất model gán score cho positive cao hơn negative.
\[ \text{AUC} = P\big( s(x^+) > s(x^-) \big) \]Trong đó \( s(x) \) là score (probability hoặc decision value) model trả ra, \( x^+ \) là sample positive, \( x^- \) là sample negative.
Theo interpretation này:
- AUC = 0.5 → 50/50, model rank ngẫu nhiên.
- AUC = 0.8 → 80% trường hợp model "ưu tiên" đúng positive hơn negative.
- AUC = 1.0 → 100% trường hợp positive được rank cao hơn negative.
Một hệ quả quan trọng: AUC chỉ phụ thuộc thứ tự của các score, không phụ thuộc giá trị tuyệt đối. Hai model cho score scale khác nhau nhưng rank giống hệt sẽ có AUC bằng nhau. Đây cũng là lý do AUC không thể đánh giá calibration (mức độ probability có đúng nghĩa xác suất hay không).
ROC/AUC trong sklearn
from sklearn.metrics import roc_curve, roc_auc_score, RocCurveDisplay
# y_score: probability của class positive
y_score = model.predict_proba(X_test)[:, 1]
# Hoặc với SVM / model có decision_function:
# y_score = model.decision_function(X_test)
fpr, tpr, thresholds = roc_curve(y_test, y_score)
auc = roc_auc_score(y_test, y_score)
print(f"AUC = {auc:.4f}")
# Vẽ trực tiếp từ estimator:
RocCurveDisplay.from_estimator(model, X_test, y_test)
# Hoặc từ predictions có sẵn:
# RocCurveDisplay.from_predictions(y_test, y_score)
Một số chi tiết về roc_curve:
- Trả về 3 mảng
fpr,tpr,thresholds. Sklearn tự thêm threshold đầu tiênmax(y_score) + 1(lớn hơn tất cả score thực) để bắt đầu từ (FPR, TPR) = (0, 0). Trong các phiên bản mới của sklearn, threshold đầu này được thay bằngnp.inf. - Threshold trả về theo thứ tự giảm dần (cao về thấp). Index 0 ứng với điểm (0, 0).
- Có tham số
drop_intermediate=Truemặc định bỏ một số điểm không ảnh hưởng tới đồ thị để giảm kích thước mảng.
predict_proba vs predict — sai lầm hay gặp
ROC quét theo score liên tục, không phải hard label. Sai lầm phổ biến: truyền model.predict(X) (0/1) vào roc_curve.
# SAI — predict trả về 0/1 sau khi đã áp threshold 0.5
y_pred = model.predict(X_test)
auc_wrong = roc_auc_score(y_test, y_pred) # AUC bị giảm vô lý
# ĐÚNG — predict_proba trả về probability liên tục
y_score = model.predict_proba(X_test)[:, 1]
auc_right = roc_auc_score(y_test, y_score)
Khi truyền hard label, ROC chỉ có 3 điểm (0, 0), một điểm trung gian = (FPR, TPR) tại threshold 0.5, và (1, 1) — diện tích trapezoidal sẽ nhỏ hơn AUC thực rất nhiều. Quy tắc: với roc_auc_score, roc_curve, precision_recall_curve, average_precision_score, luôn truyền score liên tục.
Model nào không có predict_proba (vd SVC mặc định) dùng decision_function. Nếu cần probability calibrated, bọc CalibratedClassifierCV.
PR Curve và Average Precision — alternative cho imbalanced
Bài 25 đã giới thiệu PR Curve. So sánh nhanh hai loại curve:
- ROC Curve: trục X = FPR, trục Y = TPR. AUC = diện tích.
- PR Curve: trục X = Recall, trục Y = Precision. Average Precision (AP) = diện tích (theo công thức step-wise của sklearn).
Điểm mấu chốt: precision có TP trên tử số và (TP + FP) trên mẫu — cả hai chỉ liên quan tới các positive prediction. FPR có TN ở mẫu — phụ thuộc tổng negative.
Hệ quả với data cực kỳ imbalanced (ví dụ 1% positive, 99% negative):
- FPR có mẫu rất lớn → kể cả model đoán nhầm vài trăm negative, FPR vẫn nhỏ → ROC cong đẹp lên, AUC cao.
- Precision có mẫu nhỏ (toàn positive prediction) → vài FP thôi đã kéo precision tụt mạnh → PR curve phản ánh thẳng độ "đắt" của false positive.
Tóm gọn: với positive rare, ROC quá lạc quan; PR-AUC sát thực tế hơn.
Khi nào dùng ROC-AUC vs PR-AUC
- Balanced data (positive rate ≈ 30%-70%): ROC-AUC hợp lý. Hai metric thường ra kết luận tương tự.
- Mildly imbalanced (10%-30% positive): cả ROC-AUC và PR-AUC đều dùng được; ưu tiên báo cáo cả hai.
- Highly imbalanced (vd 1% positive — fraud detection, hiếm bệnh, anomaly): luôn báo cáo PR-AUC. ROC-AUC riêng lẻ dễ gây hiểu nhầm.
Ví dụ minh hoạ — bài toán fraud detection có 1% giao dịch là gian lận. Model có thể đạt ROC-AUC = 0.95 (nghe rất ấn tượng) nhưng PR-AUC chỉ 0.30 — nghĩa là khi báo "đây là fraud", model chỉ đúng 30% trên đường cong trung bình. Báo ROC-AUC 0.95 mà bỏ qua PR-AUC = 0.30 là che giấu vấn đề thực sự.
Khuyến nghị: với positive rate < 10%, mặc định report PR-AUC + F1 (hoặc F-beta) thay cho ROC-AUC. Nếu vẫn muốn báo ROC-AUC, kèm theo baseline AUC của random classifier (0.5) và no-skill PR-AUC = positive rate (vd 0.01) để người đọc tự đặt context.
Multi-class ROC-AUC — OvR và OvO
ROC vốn định nghĩa cho binary. Với \( K \) class, sklearn hỗ trợ hai chiến lược chuyển thành nhiều bài toán binary rồi gộp:
- One-vs-Rest (OvR) — với mỗi class \( k \), coi đó là positive và mọi class còn lại là negative. Tính AUC cho từng class rồi average.
- One-vs-One (OvO) — với mỗi cặp class \( (i, j) \), tính AUC trên các sample chỉ thuộc 2 class đó. Có \( \binom{K}{2} \) cặp.
Cách average:
average="macro"— trung bình cộng AUC các class / cặp, không weight. Mọi class quan trọng như nhau.average="weighted"— trung bình có trọng số theo số sample của class. Class nhiều sample ảnh hưởng nhiều hơn.average=None— trả về vector AUC từng class (OvR).
from sklearn.metrics import roc_auc_score
# y_score: shape (n_samples, n_classes), output của predict_proba
y_score = model.predict_proba(X_test)
auc_ovr_macro = roc_auc_score(y_test, y_score, multi_class="ovr", average="macro")
auc_ovr_weighted = roc_auc_score(y_test, y_score, multi_class="ovr", average="weighted")
auc_ovo_macro = roc_auc_score(y_test, y_score, multi_class="ovo", average="macro")
auc_per_class = roc_auc_score(y_test, y_score, multi_class="ovr", average=None)
Khác biệt giữa OvR và OvO thường nhỏ với data balanced. Với data imbalanced, OvO ít bị skew bởi class lớn hơn vì mỗi cặp được đánh giá riêng. "ovo" chỉ kết hợp được với average="macro" hoặc "weighted".
So sánh model bằng ROC
So sánh hai model A và B bằng ROC có hai tình huống:
- ROC của A nằm hoàn toàn trên ROC của B (A dominate B). Mọi threshold A đều cho TPR cao hơn hoặc FPR thấp hơn. AUC của A cũng lớn hơn. Chọn A bất kể operating point.
- Hai ROC cắt nhau. AUC tổng có thể gần bằng nhau nhưng "vùng tốt" khác nhau: model A tốt hơn ở FPR thấp (precision-leaning), model B tốt hơn ở TPR cao (recall-leaning). Chọn model phụ thuộc operating point business cần — không thể kết luận chỉ qua AUC.
Quy tắc: luôn vẽ ROC chồng lên nhau khi so sánh model, không chỉ in AUC. Một con số AUC che giấu sự khác biệt ở các vùng threshold khác nhau.
Khi cần kiểm định có ý nghĩa thống kê, dùng DeLong test (so sánh hai AUC trên cùng dataset) hoặc bootstrap để có confidence interval. Sklearn không có sẵn — dùng scipy hoặc package phụ.
Chọn operating point — Youden's J, cost-based, recall constraint
AUC threshold-independent, nhưng khi deploy phải chốt 1 threshold cụ thể. Có 3 cách phổ biến chọn operating point dựa trên ROC:
1. Youden's J statistic — maximize TPR − FPR:
\[ J(t) = \text{TPR}(t) - \text{FPR}(t), \quad t^* = \arg\max_t J(t) \]Trực quan: tìm điểm trên ROC xa nhất theo phương dọc so với đường chéo random. Phù hợp khi cost FP và FN tương đương.
import numpy as np
fpr, tpr, thresholds = roc_curve(y_val, y_score_val)
j_scores = tpr - fpr
best_idx = np.argmax(j_scores)
best_threshold = thresholds[best_idx]
2. Cost-based — gán cost cụ thể cho FP và FN, chọn threshold minimize expected cost:
\[ \text{Cost}(t) = C_{FP} \cdot FP(t) + C_{FN} \cdot FN(t) \]Cách này phản ánh business trực tiếp. Ví dụ fraud: \( C_{FN} = 1000 \) USD (số tiền mất nếu bỏ sót), \( C_{FP} = 5 \) USD (chi phí review thủ công). Quét threshold, tính cost từng điểm, chọn min.
3. Constraint-based — fix một metric đạt mức tối thiểu, tối ưu phần còn lại:
- "Recall ≥ 95%" → chọn threshold nhỏ nhất sao cho recall ≥ 0.95, lấy precision/FPR tại đó.
- "FPR ≤ 1%" → chọn threshold lớn nhất sao cho FPR ≤ 0.01, lấy TPR tương ứng.
Tất cả ba phương pháp đều phải làm trên validation set, không phải test set, để tránh leak threshold.
Pitfall thường gặp
- ROC-AUC quá lạc quan với imbalanced data — đã phân tích ở mục 8-9. Khi positive rate < 10%, kèm PR-AUC.
- Truyền
predictthay vìpredict_proba— mục 7. AUC bị giảm vô lý vì chỉ còn 3 điểm. - Báo AUC mà không cross-validate — single split có variance lớn, đặc biệt với dataset nhỏ. Dùng
cross_val_score(model, X, y, scoring="roc_auc")để có mean ± std. - Tune threshold trên test set — leak. Threshold phải fix trên validation, test chỉ để báo cáo cuối.
- So sánh AUC giữa các dataset khác nhau không có ý nghĩa — AUC phụ thuộc class distribution của data đó.
- AUC cao không nghĩa probability calibrated — AUC chỉ đo rank. Nếu downstream cần dùng probability (Bayes optimal decision, threshold dynamic), kiểm tra calibration riêng (
CalibratedClassifierCV, reliability diagram). - AUC cao bất thường (≥ 0.99) trên domain khó — gần như luôn là dấu hiệu data leak (feature chứa label, duplicate giữa train/test, time leak trong time series).
Code Python — breast cancer và iris
Phần 1 — Breast cancer (binary): Logistic Regression, ROC + AUC + PR + AP.
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import (
roc_curve, roc_auc_score,
precision_recall_curve, average_precision_score,
)
X, y = load_breast_cancer(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)
model = LogisticRegression(max_iter=2000).fit(X_train, y_train)
y_score = model.predict_proba(X_test)[:, 1]
# ROC + AUC
fpr, tpr, roc_th = roc_curve(y_test, y_score)
auc = roc_auc_score(y_test, y_score)
print(f"ROC-AUC = {auc:.4f}") # ~ 0.997
# PR + AP
precision, recall, pr_th = precision_recall_curve(y_test, y_score)
ap = average_precision_score(y_test, y_score)
print(f"PR-AUC = {ap:.4f}") # ~ 0.998
Phần 2 — Vẽ ROC (mô tả): dùng matplotlib.pyplot.plot(fpr, tpr), set xlabel("False Positive Rate"), ylabel("True Positive Rate"), thêm đường chéo random plt.plot([0, 1], [0, 1], "--"). Hoặc gọi RocCurveDisplay.from_estimator(model, X_test, y_test) để sklearn tự vẽ kèm AUC trong legend.
Phần 3 — Imbalanced: so sánh ROC-AUC và PR-AUC.
from sklearn.datasets import make_classification
X_imb, y_imb = make_classification(
n_samples=5000, n_features=10, n_informative=5,
weights=[0.95, 0.05], random_state=0,
)
X_tr, X_te, y_tr, y_te = train_test_split(
X_imb, y_imb, test_size=0.3, random_state=0, stratify=y_imb
)
model = LogisticRegression(max_iter=2000).fit(X_tr, y_tr)
y_score = model.predict_proba(X_te)[:, 1]
print(f"Positive rate (test) = {y_te.mean():.3f}") # ~ 0.05
print(f"ROC-AUC = {roc_auc_score(y_te, y_score):.4f}") # ~ 0.95+
print(f"PR-AUC = {average_precision_score(y_te, y_score):.4f}") # thường thấp hơn nhiều
Nhận xét: ROC-AUC nhìn rất "đẹp" nhưng PR-AUC mới phản ánh đúng độ khó. No-skill baseline cho PR-AUC bằng positive rate (≈ 0.05) — nên luôn so sánh PR-AUC với baseline này.
Phần 4 — Multi-class ROC trên iris.
from sklearn.datasets import load_iris
X, y = load_iris(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=0, stratify=y
)
model = LogisticRegression(max_iter=2000).fit(X_train, y_train)
y_score = model.predict_proba(X_test)
print("OvR macro =", roc_auc_score(y_test, y_score, multi_class="ovr", average="macro"))
print("OvR weighted =", roc_auc_score(y_test, y_score, multi_class="ovr", average="weighted"))
print("OvO macro =", roc_auc_score(y_test, y_score, multi_class="ovo", average="macro"))
print("Per-class =", roc_auc_score(y_test, y_score, multi_class="ovr", average=None))
Iris balanced (50/50/50) nên macro ≈ weighted; per-class array cho thấy class nào model khó tách hơn (thường là versicolor vs virginica).
Bài tập thực hành
Bài 1 — Breast cancer baseline. Train Logistic Regression và Random Forest (n_estimators=100, random_state=0) trên breast cancer. Tính ROC-AUC trên test cho cả hai bằng predict_proba(...)[:, 1]. Báo cáo kèm cross-validation: cross_val_score(model, X, y, cv=5, scoring="roc_auc").mean(). So sánh single-split AUC với CV AUC — khác nhau bao nhiêu?
Bài 2 — Imbalanced compare. Dùng make_classification với weights=[0.95, 0.05] (5% positive). Train Logistic Regression và Random Forest. So sánh:
- ROC-AUC.
- PR-AUC (Average Precision).
- No-skill baselines: ROC-AUC = 0.5, PR-AUC = positive rate.
Cùng dataset, lặp lại với weights=[0.5, 0.5] (balanced). Khoảng chênh giữa ROC-AUC và PR-AUC trong hai tình huống khác nhau thế nào?
Bài 3 — Youden's J optimal threshold. Trên breast cancer, chia train thành train + val (80/20). Train LogReg trên train, lấy y_score_val. Compute fpr, tpr, thresholds = roc_curve(y_val, y_score_val) và best_idx = np.argmax(tpr - fpr). In ra best_threshold, TPR, FPR tại đó. Áp threshold mới lên test set, so sánh confusion matrix và F1 với threshold mặc định 0.5.
Bài 4 — Cost-based threshold. Tiếp Bài 3, giả sử FN cost = 100 (bỏ sót bệnh), FP cost = 5 (báo nhầm cần test lại). Quét tất cả threshold trong roc_curve, tính tổng cost = cost_FN * FN_count + cost_FP * FP_count tại từng threshold. Tìm threshold minimize cost. So sánh với threshold từ Youden's J — có khác không, và vì sao?
Bài 5 — Multi-class iris. Tái hiện Phần 4 mục 14 với RandomForestClassifier(n_estimators=50, random_state=0). So sánh AUC per-class với LogReg. Class nào khó tách nhất với cả hai model?
Bài tiếp theo
Bài 27: K-Nearest Neighbors (KNN) — thuật toán classification/regression dựa trên khoảng cách: cơ chế bỏ phiếu k láng giềng, chọn k và metric khoảng cách (Euclidean, Manhattan, Minkowski), tác động của feature scaling, ưu/nhược điểm và độ phức tạp inference.
Tài liệu tham khảo
- scikit-learn — ROC metrics
- scikit-learn — roc_curve API
- scikit-learn — roc_auc_score API
- scikit-learn — RocCurveDisplay
- scikit-learn — precision_recall_curve API
- scikit-learn — average_precision_score API
- Wikipedia — Receiver operating characteristic
- Wikipedia — Youden's J statistic
- Tom Fawcett (2006) — An introduction to ROC analysis (Pattern Recognition Letters)
- Davis & Goadrich (2006) — The Relationship Between Precision-Recall and ROC Curves (ICML)
- Saito & Rehmsmeier (2015) — The Precision-Recall Plot Is More Informative than the ROC Plot When Evaluating Binary Classifiers on Imbalanced Datasets (PLOS ONE)
