本文为 Python 60 Days Challenge 课程学习整理,原作者与原 Notebook 链接见「课程资料」。代码按章节顺序运行;语法示意和故意报错的反例已单独标注。

文中保留的截图、性能表和数值属于原讲义的历史示例,不是本次修订后的重新训练结果。实际输出受数据、版本、随机种子和训练预算影响。

本次校订: 阈值曲线如果用于选择最佳 MCC/KS 阈值,必须在验证集计算;同一批样本上的最大值不是独立测试性能。下文阈值搜索示例已将该数据集明确命名为验证集。

打卡任务

  • 阅读本节讲义或 Notebook
  • 跑通示例代码
  • 整理 3 个关键点
  • 完成当日打卡输出

课程资料

学习内容

一、自带数据集介绍

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()

实际上,可以给每个类别单独寻找阈值,即使这样拼不成一个完整的混淆矩阵

但是这样可以起到最好的效果,在实际工业界有价值。