模型评估:别被 99% 的准确率骗了
这一篇比任何算法都重要。 评估错了,模型再好也是白做;评估对了,你能一眼看出谁在自欺。
一个真实场景
信用卡欺诈检测,欺诈占比 0.5%。你的模型把所有交易都判为「正常」,准确率 99.5%。 这个模型毫无价值,但它「准确率」很高。
结论:指标选错,努力白费。
一、分类指标:先看混淆矩阵
| 预测为正 | 预测为负 | |
|---|---|---|
| 实际为正 | TP(真正例) | FN(漏报) |
| 实际为负 | FP(误报) | TN(真负例) |
由它派生出所有分类指标:
| 指标 | 公式 | 回答的问题 | 何时重点看 |
|---|---|---|---|
| 准确率 Accuracy | (TP+TN)/全部 | 整体对了多少 | 仅类别均衡时 |
| 精确率 Precision | TP/(TP+FP) | 报出来的有多少是真的 | 误报代价高(推荐、审核) |
| 召回率 Recall | TP/(TP+FN) | 真的有多少被抓到 | 漏报代价高(风控、医疗) |
| F1 | 2PR/(P+R) | P 与 R 的调和平均 | 需要单指标汇总 |
| AUC-ROC | 曲线下面积 | 排序能力,与阈值无关 | 类别均衡偏好、需要整体排序 |
| PR-AUC | PR 曲线下面积 | 正类稀有时的排序能力 | 极不平衡时首选 |
记住这条
类别比例 < 10% 时,别用准确率,别只看 ROC-AUC,要看 PR-AUC 与 F1。 ROC 曲线在极不平衡时会被大量 TN 拉得虚高,看起来很美,业务上没用。
python
from sklearn.metrics import (classification_report, confusion_matrix,
roc_auc_score, average_precision_score)
y_pred = clf.predict(X_test)
y_proba = clf.predict_proba(X_test)[:, 1]
print(confusion_matrix(y_test, y_pred))
print(classification_report(y_test, y_pred, digits=3))
print("ROC-AUC:", round(roc_auc_score(y_test, y_proba), 4))
print("PR-AUC :", round(average_precision_score(y_test, y_proba), 4))二、阈值不是 0.5 —— 是业务决策
模型输出概率,阈值决定「多确信才报警」。
| 阈值 | 召回 | 精确 | 业务含义 |
|---|---|---|---|
| 0.2 | 高 | 低 | 宁可错杀,人工复核量大 |
| 0.5 | 中 | 中 | 默认,通常不是最优 |
| 0.8 | 低 | 高 | 只抓最确定的,漏很多 |
正确做法:算出代价函数,选期望代价最小的阈值。
python
import numpy as np
# 漏报损失 1000 元,误报损失 20 元(人工核查成本)
cost_fn, cost_fp = 1000, 20
rows = []
for t in np.arange(0.05, 0.96, 0.05):
pred = y_proba >= t
fn = ((y_test == 1) & ~pred).sum()
fp = ((y_test == 0) & pred).sum()
rows.append((t, fn, fp, fn * cost_fn + fp * cost_fp))
best = min(rows, key=lambda r: r[3])
print(f"最优阈值={best[0]:.2f} 期望总代价={best[3]:.0f} 元")这段代码的价值远超任何调参 —— 它把「模型」变成「决策」。
三、回归指标
| 指标 | 含义 | 注意 |
|---|---|---|
| MAE | 平均绝对误差 | 与原目标同量纲,最易解释 |
| RMSE | 均方根误差 | 对大误差惩罚重,怕异常值 |
| MAPE | 平均百分比误差 | 目标接近 0 时会爆炸 |
| R² | 解释了多少方差 | 可为负(比预测均值还差) |
python
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
import numpy as np
print("MAE :", round(mean_absolute_error(y_test, pred), 3))
print("RMSE:", round(np.sqrt(mean_squared_error(y_test, pred)), 3))
print("R² :", round(r2_score(y_test, pred), 3))永远先看 MAE 的量级:一个 MAE=50 的房价模型(单位万元)意味着平均差 50 万,业务上可能完全不可用,哪怕 R²=0.9。
四、交叉验证:小数据集的必修课
单次切分的结果随机性太大。K 折交叉验证把数据切 K 份,轮流做验证。
python
from sklearn.model_selection import cross_val_score, StratifiedKFold
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(clf, X, y, cv=cv, scoring="roc_auc")
print(f"AUC = {scores.mean():.4f} ± {scores.std():.4f}")| 场景 | 用什么 |
|---|---|
| 普通分类 | StratifiedKFold(保持类别比例) |
| 普通回归 | KFold |
| 时间序列 | TimeSeriesSplit(绝不能用随机切分) |
| 分组数据(同一用户多条) | GroupKFold(防止同组跨越 train/test) |
时间序列用随机切分 = 穿越未来
随机切分会让模型「用明天的数据预测今天」。必须按时间顺序切:训练用过去,验证用未来。
五、数据泄漏:分数虚高的头号元凶
数据泄漏 = 训练时用到了预测时拿不到的信息。
| 泄漏类型 | 例子 | 怎么发现 |
|---|---|---|
| 预处理泄漏 | 在全量数据上做标准化/填充 | 分数异常高,去掉泄漏后大跌 |
| 目标泄漏 | 特征里含「结果」信息(如「结案金额」用于预测「是否结案」) | 单特征重要性异常高(>0.9) |
| 时间穿越 | 随机切分时间数据 | 换 TimeSeriesSplit 后分数降很多 |
| 重复样本 | 同一实体出现在 train 和 test | 用 groupby 查重复 |
| 目标编码泄漏 | 用全量 y 均值编码类别 | 必须 CV 内做 |
检查清单(上线前必过):
六、报告怎么写
一份可信的模型报告包含:
| 部分 | 内容 |
|---|---|
| 问题定义 | 业务问题、标签定义、成功标准(指标 + 阈值) |
| 数据 | 规模、时间范围、缺失情况、切分方式 |
| 基线 | 常数基线 + 简单模型的分数 |
| 最终模型 | 算法、超参、CV 分数(均值 ± 标准差) |
| 测试集结果 | 混淆矩阵、PR-AUC、选定阈值及理由 |
| 重要特征 | Top10 及业务解释 |
| 局限 | 数据偏差、覆盖范围、已知失效场景 |
| 上线建议 | 阈值、监控指标、重训频率 |
最后两行最能体现水平。 只报好数字、不说局限的报告,是新手报告。
通过标准
下一步
→ 集成学习与树模型