本文为 Python 60 Days Challenge 课程学习整理,原作者与原 Notebook 链接见「课程资料」。代码按章节顺序运行;语法示意和故意报错的反例已单独标注。
文中保留的截图、性能表和数值属于原讲义的历史示例,不是本次修订后的重新训练结果。实际输出受数据、版本、随机种子和训练预算影响。
本次校订: 阈值曲线如果用于选择最佳 MCC/KS 阈值,必须在验证集计算;同一批样本上的最大值不是独立测试性能。下文阈值搜索示例已将该数据集明确命名为验证集。
打卡任务
- 阅读本节讲义或 Notebook
- 跑通示例代码
- 整理 3 个关键点
- 完成当日打卡输出
课程资料
- 训练营讲义:https://docs.qq.com/doc/DTFhndExSRGNHS3B6?nlc=1
- GitHub 仓库:https://github.com/cheer932041235/Python60DaysChallenge
- 本日 Notebook:Day17 多分类问题.ipynb
学习内容
一、自带数据集介绍
import sklearn.datasets
# 获取 sklearn.datasets 模块下所有的属性名
all_names = dir(sklearn.datasets)
print("【1】自带的小数据集 (load_*) - 适合练手/测试:")
print([name for name in all_names if name.startswith("load_")]) # startswith 方法用于判断字符串是否以指定子字符串开头
print("\n【2】在线下载的大数据集 (fetch_*) - 适合真实项目:")
print([name for name in all_names if name.startswith("fetch_")])
print("\n【3】构造的假数据 (make_*) - 适合做实验/画图:")
print([name for name in all_names if name.startswith("make_")])
二、鸢尾花三分类数据集实战
import numpy as np
import pandas as pd # 引入pandas是为了打印表格更漂亮
from sklearn import datasets
# --- 1. 加载数据 ---
iris = datasets.load_iris()
X = iris.data
y = iris.target
n_classes = len(np.unique(y)) # 类别数量:3
class_names = iris.target_names # ['setosa', 'versicolor', 'virginica']
# 转换成 DataFrame 显示
df = pd.DataFrame(X, columns=iris.feature_names)
df['label (标签)'] = y # 把标签也拼上去方便看
# 打印前 5 行
df.head()
标签的数组顺序和列表的索引顺序是一致的。
df.info()
对随机森林而言,训练时只需要 1 个模型,但在画 ROC 曲线时,我们需要把它的结果“拆”成 3 份来看。
1个阈值 对应 一个3 * 3混淆矩阵 对应 3个2 * 2的混淆矩阵 对应 3个roc曲线
如果用了非原生模型,ovr策略的方法接口是要求y是独热编码后的形状。
import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import label_binarize
from sklearn.multiclass import OneVsRestClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_curve, auc
# --- 1. 设置画图风格 ---
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
# --- 2. 加载数据 & 预处理 ---
iris = datasets.load_iris()
X = iris.data
y = iris.target
n_classes = len(np.unique(y)) # 类别数量:3
class_names = iris.target_names # ['setosa', 'versicolor', 'virginica']
# 【核心步骤】标签二值化 (Binarize)
# 将 y = [0, 1, 2] 转换为 One-Hot 形式:
# 0 -> [1, 0, 0]
# 1 -> [0, 1, 0]
# 2 -> [0, 0, 1]
# 只有这样,才能画出 "类0 vs 其他", "类1 vs 其他" 的 3 条 ROC 曲线
y = label_binarize(y, classes=[0, 1, 2])
# 划分训练集和测试集 (50% 训练, 50% 测试)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.5, random_state=0)
# --- 3. 训练模型 (One-vs-Rest 策略) ---
# 我们使用逻辑回归,并用 OneVsRestClassifier 包装它
# 它的作用是自动训练 3 个二分类器
classifier = OneVsRestClassifier(LogisticRegression(random_state=0, solver='liblinear'))
# 获取预测概率 (注意是 score/proba,而不是直接 predict 类别)
y_score = classifier.fit(X_train, y_train).decision_function(X_test)
# --- 4. 计算每一类的 ROC 和 AUC ---
fpr = {}
tpr = {}
roc_auc = {}
for i in range(n_classes):
# 计算第 i 类 vs 其余类的 FPR 和 TPR
# y_test[:, i] 取出第 i 列标签(0或1)
# y_score[:, i] 取出第 i 列预测分数
fpr[i], tpr[i], _ = roc_curve(y_test[:, i], y_score[:, i])
roc_auc[i] = auc(fpr[i], tpr[i])
# --- 5. 绘制 ROC 曲线 ---
plt.figure(figsize=(10, 8))
# 定义三种颜色
colors = ['aqua', 'darkorange', 'cornflowerblue']
# 循环画出每一类的曲线
for i, color in zip(range(n_classes), colors):
plt.plot(fpr[i], tpr[i], color=color, lw=2,
label='类别 {0} (AUC = {1:0.2f})'.format(class_names[i], roc_auc[i]))
# 画对角线(随机猜测基准线)
plt.plot([0, 1], [0, 1], 'k--', lw=2, label='随机猜测')
# 装饰图表
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
plt.xlabel('FPR (假正率 / 误伤率)', fontsize=12)
plt.ylabel('TPR (真正率 / 召回率)', fontsize=12)
plt.title('鸢尾花多分类 ROC 曲线 (One-vs-Rest 策略)', fontsize=16)
plt.legend(loc="lower right", fontsize=12)
plt.grid(True, alpha=0.3)
plt.show()
import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import label_binarize
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import roc_curve, auc
# --- 1. 设置画图风格 ---
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
# --- 2. 加载纯净数据 (无噪声) ---
iris = datasets.load_iris()
X = iris.data
y = iris.target
class_names = iris.target_names
# --- 3. 标签处理 ---
# 准备二值化标签用于画图 (One-Hot)
y_bin = label_binarize(y, classes=[0, 1, 2])
n_classes = y_bin.shape[1]
# 切分数据
# 技巧:我们切分 y_bin,这样测试集 y_test_bin 就是 One-Hot 的,方便画图
X_train, X_test, y_train_bin, y_test_bin = train_test_split(X, y_bin, test_size=0.5, random_state=0)
# 【核心步骤】训练集转回整数标签 [0, 1, 2]
# 随机森林最喜欢吃整数标签,原生支持!
y_train_integer = np.argmax(y_train_bin, axis=1)
# --- 4. 训练模型 (随机森林) ---
# 不需要指定 multi_class参数,它自动识别 y 是多分类
clf = RandomForestClassifier(n_estimators=100, random_state=0)
clf.fit(X_train, y_train_integer)
# --- 5. 获取预测概率 ---
# 原生输出 (N_samples, 3) 的矩阵
# 每一行代表:这棵树觉得是类0、类1、类2的概率分别是多少
y_score = clf.predict_proba(X_test)
# --- 6. 绘制 ROC 曲线 ---
fpr = {}
tpr = {}
roc_auc = {}
plt.figure(figsize=(10, 8))
colors = ['aqua', 'darkorange', 'cornflowerblue']
for i in range(n_classes):
# 计算第 i 类的 ROC
# 依然是把第 i 列拎出来做二分类评估
fpr[i], tpr[i], _ = roc_curve(y_test_bin[:, i], y_score[:, i])
roc_auc[i] = auc(fpr[i], tpr[i])
plt.plot(fpr[i], tpr[i], color=colors[i], lw=2,
label='类别 {0} (AUC = {1:0.2f})'.format(class_names[i], roc_auc[i]))
plt.plot([0, 1], [0, 1], 'k--', lw=2, label='随机猜测')
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
plt.xlabel('FPR (假正率)')
plt.ylabel('TPR (真正率)')
plt.title('随机森林原生多分类 ROC 曲线 (无噪声)', fontsize=16)
plt.legend(loc="lower right", fontsize=12)
plt.grid(True, alpha=0.3)
plt.show()
from sklearn.metrics import classification_report, roc_auc_score
# ==========================================
# 6. (新增) 计算 Precision/Recall/F1 的三种平均
# ==========================================
# 为了生成分类报告,我们需要“硬预测”(类别 0, 1, 2),而不是概率
# 使用 argmax 从概率矩阵中选出概率最大的那个类别
y_pred_class = np.argmax(y_score, axis=1)
# y_test_bin 是 One-Hot 的,我们也把它转回整数方便对比
y_test_integer = np.argmax(y_test_bin, axis=1)
print("="*20 + " 多分类详细指标报告 " + "="*20)
# classification_report 自动帮你算好了 Macro, Weighted 和 Accuracy (Micro)
print(classification_report(y_test_integer, y_pred_class, target_names=class_names))
# ==========================================
# 7. (新增) 计算 AUC 的三种平均 并画在图上
# ==========================================
# --- A. 计算 Micro-average ROC (微平均) ---
# 逻辑:把所有类别的预测结果“展平”成一条长长的线,看作一个巨大的二分类
# y_test_bin.ravel() -> 把 [[1,0,0], [0,1,0]] 变成 [1,0,0,0,1,0...]
# y_score.ravel() -> 把概率矩阵也展平
fpr["micro"], tpr["micro"], _ = roc_curve(y_test_bin.ravel(), y_score.ravel())
roc_auc["micro"] = auc(fpr["micro"], tpr["micro"])
# --- B. 计算 Macro-average ROC (宏平均) ---
# 逻辑:把刚才算好的 3 条线的 TPR 加起来除以 3
# 这一步稍微复杂点,需要对齐横坐标(FPR),通过插值来实现
all_fpr = np.unique(np.concatenate([fpr[i] for i in range(n_classes)]))
mean_tpr = np.zeros_like(all_fpr)
for i in range(n_classes):
mean_tpr += np.interp(all_fpr, fpr[i], tpr[i])
mean_tpr /= n_classes
fpr["macro"] = all_fpr
tpr["macro"] = mean_tpr
roc_auc["macro"] = auc(fpr["macro"], tpr["macro"])
# --- C. 计算 Weighted-average AUC (加权平均) ---
# sklearn 直接提供了函数,根据样本数量加权
weighted_auc = roc_auc_score(y_test_bin, y_score, average='weighted', multi_class='ovr')
print(f"【AUC 汇总】")
print(f" Micro-average AUC (微平均): {roc_auc['micro']:.4f}")
print(f" Macro-average AUC (宏平均): {roc_auc['macro']:.4f}")
print(f" Weighted-average AUC (加权): {weighted_auc:.4f}")
# ==========================================
# 8. (进阶) 重新画图:加上平均线
# ==========================================
plt.figure(figsize=(10, 8))
# 1. 画 Micro-average ROC 曲线 (粉色虚线)
plt.plot(fpr["micro"], tpr["micro"],
label='微平均 ROC (AUC = {0:0.2f})'.format(roc_auc["micro"]),
color='deeppink', linestyle=':', linewidth=4)
# 2. 画 Macro-average ROC 曲线 (深蓝色虚线)
plt.plot(fpr["macro"], tpr["macro"],
label='宏平均 ROC (AUC = {0:0.2f})'.format(roc_auc["macro"]),
color='navy', linestyle=':', linewidth=4)
# 3. 画每个类别的 ROC 曲线 (细线)
colors = ['aqua', 'darkorange', 'cornflowerblue']
for i, color in zip(range(n_classes), colors):
plt.plot(fpr[i], tpr[i], color=color, lw=2,
label='类别 {0} (AUC = {1:0.2f})'.format(class_names[i], roc_auc[i]))
plt.plot([0, 1], [0, 1], 'k--', lw=2)
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
plt.xlabel('FPR')
plt.ylabel('TPR')
plt.title('原生多分类 ROC 曲线 (含 Micro & Macro)', fontsize=16)
plt.legend(loc="lower right")
plt.grid(True, alpha=0.3)
plt.show()
三、MCC和KS指标
import numpy as np
import matplotlib.pyplot as plt
from sklearn.metrics import roc_curve, matthews_corrcoef
from scipy.stats import ks_2samp
# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
# 1. 模拟数据 (还是之前的逻辑)
np.random.seed(42)
n_samples = 10000
y_true = np.array([0]*n_samples + [1]*n_samples)
# 模拟一个效果还不错的模型
y_scores_neg = np.random.normal(0.3, 0.15, n_samples)
y_scores_pos = np.random.normal(0.6, 0.15, n_samples)
y_scores = np.concatenate([y_scores_neg, y_scores_pos])
y_scores = np.clip(y_scores, 0, 1) # 限制在0-1
# --- 核心 1: 计算 MCC ---
# MCC 需要具体的预测类别(0或1),所以我们需要先定一个阈值(比如0.5)
y_pred = (y_scores > 0.5).astype(int)
mcc = matthews_corrcoef(y_true, y_pred)
print(f"MCC (马修斯相关系数): {mcc:.4f} (越接近1越好)")
# --- 核心 2: 计算 KS 统计量 ---
# 方法A: 使用 scipy 直接通过原始分布计算 (最准确)
ks_stat, p_value = ks_2samp(y_scores[y_true==1], y_scores[y_true==0])
print(f"KS 统计量 (scipy计算): {ks_stat:.4f}")
# 方法B: 通过 ROC 曲线数据手动计算 (方便画图)
fpr, tpr, thresholds = roc_curve(y_true, y_scores)
# KS 就是 TPR 和 FPR 差值的最大值
ks_diff = tpr - fpr
max_ks_idx = np.argmax(ks_diff) # 找到最大差值对应的索引
max_ks = ks_diff[max_ks_idx]
best_threshold = thresholds[max_ks_idx]
print(f"KS 统计量 (ROC推导): {max_ks:.4f}, 最佳切分阈值: {best_threshold:.4f}")
# --- 绘图: KS 曲线 ---
plt.figure(figsize=(10, 6))
# 绘制 TPR (覆盖率) 和 FPR (误伤率) 随阈值变化的曲线
plt.plot(thresholds, tpr, label='TPR (正样本累积/捕获率)', color='red')
plt.plot(thresholds, fpr, label='FPR (负样本累积/误伤率)', color='blue')
# 绘制 KS 最大距离线
plt.plot([best_threshold, best_threshold], [fpr[max_ks_idx], tpr[max_ks_idx]],
color='green', linestyle='--', lw=3, label=f'KS={max_ks:.3f}')
# 装饰
plt.title('KS 曲线 (K-S Curve) - 金融风控标准', fontsize=14)
plt.xlabel('阈值 (Threshold)', fontsize=12)
plt.ylabel('累积比例', fontsize=12)
plt.xlim([0, 1])
plt.ylim([0, 1.05])
plt.legend(loc='best')
plt.grid(True)
plt.text(best_threshold+0.02, (fpr[max_ks_idx] + tpr[max_ks_idx])/2,
'← 最大差距 (KS)', color='green', fontweight='bold')
# 注意:x轴通常是阈值从1到0逆序,或者按分位数排序。
# 这里为了直观,直接展示随阈值变化的走势。
# 可以看到在 KS 线那里,红线和蓝线分得最开。
plt.show()
import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import label_binarize
from sklearn.multiclass import OneVsRestClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_curve, matthews_corrcoef
# --- 1. 设置画图风格 ---
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
# --- 2. 加载数据 & 预处理 ---
iris = datasets.load_iris()
X = iris.data
y = iris.target
class_names = iris.target_names # ['setosa', 'versicolor', 'virginica']
# 【为了演示效果添加噪声】
# 鸢尾花太简单,不加噪声 MCC 很容易全是 1.0,加噪声为了模拟真实困难场景
random_state = np.random.RandomState(0)
n_samples, n_features = X.shape
X = np.c_[X, random_state.randn(n_samples, 200 * n_features)]
# 【标签二值化】
y = label_binarize(y, classes=[0, 1, 2])
n_classes = y.shape[1]
# 划分训练集和验证集
X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.5, random_state=0)
# --- 3. 训练模型 (改为输出概率) ---
# 使用 OneVsRest 策略训练逻辑回归
classifier = OneVsRestClassifier(LogisticRegression(random_state=0, solver='liblinear'))
# 【核心修改点】使用 predict_proba 获取 [0, 1] 之间的概率
# y_score 的形状是 (n_samples, n_classes),每一列对应一个类别的正例概率
y_score = classifier.fit(X_train, y_train).predict_proba(X_val)
# --- 4. 遍历阈值计算 MCC 并寻找最佳点 ---
plt.figure(figsize=(10, 8))
colors = ['aqua', 'darkorange', 'cornflowerblue']
print("="*20 + " 各类别最佳 MCC 分析 (基于概率) " + "="*20)
for i in range(n_classes):
# 1. 获取当前类别的真实标签 (0或1) 和 预测概率 (0.0~1.0)
y_true_cls = y_val[:, i]
y_prob_cls = y_score[:, i]
# 2. 利用 ROC 得到的阈值 (这些是数据中实际存在的关键切分点)
# roc_curve 返回的 thresholds 是从大到小排序的
current_fpr, current_tpr, current_thresholds = roc_curve(y_true_cls, y_prob_cls, drop_intermediate=False)
# 3. 阈值筛选
# roc_curve 通常会在第一个位置加一个 > 1 的阈值 (比如 max_prob + 1),
# 对于概率来说这是无效的,我们需要过滤掉 > 1.0 的阈值
valid_thresholds = current_thresholds[current_thresholds <= 1.0]
mcc_list = []
# 4. 遍历这些概率阈值,计算 MCC
for t in valid_thresholds:
# 大于等于阈值 t 的预测为 1,否则为 0
y_pred_cls = (y_prob_cls >= t).astype(int)
# 计算 MCC
mcc = matthews_corrcoef(y_true_cls, y_pred_cls)
mcc_list.append(mcc)
# 5. 找到 MCC 最大的位置
best_idx = np.argmax(mcc_list)
best_mcc = mcc_list[best_idx]
best_thresh = valid_thresholds[best_idx]
print(f"【类别 {class_names[i]}】")
print(f" 最佳 MCC: {best_mcc:.4f}")
print(f" 最佳阈值: {best_thresh:.4f} (概率值)")
print("-" * 30)
# 6. 绘制 MCC 随阈值变化的曲线
plt.plot(valid_thresholds, mcc_list, color=colors[i], lw=2,
label=f'{class_names[i]} (Max MCC={best_mcc:.2f})')
# 标记最高点
plt.scatter(best_thresh, best_mcc, s=100, color=colors[i], edgecolors='k', zorder=10)
# --- 5. 完善图表 ---
plt.xlabel('概率阈值 (Probability Threshold)', fontsize=12)
plt.ylabel('MCC 值', fontsize=12)
plt.title('各类别 MCC 随概率阈值变化曲线', fontsize=16)
plt.legend(loc="lower center") # 图例放下面,避免挡住曲线
plt.grid(True, alpha=0.3)
# 【核心修改点】限制 X 轴为 0 到 1,因为现在是概率
plt.xlim([0.0, 1.0])
plt.ylim([-0.2, 1.05])
plt.show()
实际上,可以给每个类别单独寻找阈值,即使这样拼不成一个完整的混淆矩阵
但是这样可以起到最好的效果,在实际工业界有价值。
原创
Day17 多分类问题
本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
评论交流
欢迎留下你的想法