本章会用到的数据
- 审计意见数据(约 7.8 MB):用于练习非标准审计意见分类。
- 代码会在首次运行时把文件下载到
data/course/,以后直接读取已下载的副本。
- 我们按披露时间先后划分训练数据与最终测试数据,避免在练习中提前看到后来的信息。
【课堂核心】3 学时学习安排
| 动机与先修 |
20 |
| 概率、对数几率与损失 |
45 |
| 系数与标准化边界 |
30 |
| 形成性检查与反馈 |
20 |
| 独立决策任务 |
25 |
| 独立任务反馈 |
10 |
| 公开数据练习 |
20 |
| 小结 |
10 |
【可选拓展】:未列入本表的历史信用数据、完整边际效应推导与 Softmax 细节安排课后。
章节概览:逻辑回归
从分类标签走向可评估的概率
- 建模对象:预测事件发生的概率,而不只给出类别。
- 决策依据:把概率与误报、漏报成本连接起来。
- 解释边界:区分同期关联、因果效应与时间外预测。
本章学习路线图
完成本章后,学生能够:
- 写出概率、对数几率与系数的对应关系;
- 在给定成本矩阵下选择分类阈值;
- 区分效应量、置信区间与 p 值;
- 用最终测试集比较概率模型与多数类基准。
Part 1: 问题设定
为何线性回归不足以应对分类任务?
案例引入:银行的核心风控业务
- 银行的核心功能: 向个人和企业发放贷款是现代银行体系的基石。
- 风险与收益的权衡: 在审批贷款时,银行面临一个核心挑战——如何准确预测申请人的还款能力?
- 数据驱动决策: 在金融科技时代,我们不再依赖主观判断,而是利用机器学习模型,通过分析申请人的数据来创建一套精准的贷款违约预测系统。
商业问题可视化
银行的每日决策:对于每一位贷款申请人,我们应该批准(Approve)还是拒绝(Reject)?
我们的任务:构建一个贷款违约预测模型
我们将利用一个包含已审批贷款表现的数据集来构建模型。
| 年收入 (Income) |
申请人的年度总收入 |
越高,违约概率越低 |
| 负债收入比 (DTI) |
每月债务支出占总收入的比例 |
越高,违约概率越高 |
| 工作年限 (Emp. Year) |
申请人在当前工作的年限 |
越长,违约概率越低 |
目标变量 (y): 贷款是否违约。这是一个二元变量(Binary Variable)。
y = 1: 贷款已违约 (正类, Positive Class)
y = 0: 贷款未违约 (负类, Negative Class)
模型的核心任务是学习特征与结果之间的关系
我们的机器学习模型就像一个函数 \(f\),它的任务是学习输入(申请人的特征 \(\mathbf{x}\))与输出(是否违约 \(y\))之间的映射关系。
模型一旦训练完成,就可以输出一个介于0和1之间的概率值。
一个自然的问题:为什么不用线性回归?
我们已经很熟悉线性回归了,它预测的是一个连续的数值。我们能否用它来预测一个0或1的分类问题呢?
让我们用负债收入比来预测是否违约。
代码
# 为“一个自然的问题:为什么不用线性回归?”导入 `numpy` 并绑定 `np`,用于执行当前任务的数组、数值或随机机制计算。
import numpy as np
# 为“一个自然的问题:为什么不用线性回归?”导入 `pandas` 并绑定 `pd`,用于整理当前任务的表格、字段与时间索引。
import pandas as pd
# 为“一个自然的问题:为什么不用线性回归?”导入 `matplotlib.pyplot` 并绑定 `plt`,用于构建当前任务的坐标轴并呈现比较结果。
import matplotlib.pyplot as plt
# 为“一个自然的问题:为什么不用线性回归?”导入 `seaborn` 并绑定 `sns`,用于编码当前任务的统计分布或分组关系。
import seaborn as sns
# 为“一个自然的问题:为什么不用线性回归?”,从 `sklearn.linear_model` 导入`LinearRegression` 用于拟合普通最小二乘线性回归。
from sklearn.linear_model import LinearRegression
# 生成 DTI 与二元违约标签的机制样本,用于展示线性概率预测越界。
# 为“一个自然的问题:为什么不用线性回归?”,固定随机数序列,使课堂示例可重复。
np.random.seed(42)
# 在给定区间均匀抽取 `dti`,覆盖“一个自然的问题:为什么不用线性回归?”的输入范围。
dti = np.random.uniform(5, 50, 100)
# 将债务收入比映射为逻辑概率,构造随 DTI 单调上升且落在零到一之间的违约概率。
prob_default = 1 / (1 + np.exp(-( -4 + 0.15 * dti)))
# 按给定概率抽取 `is_default` 的 0/1 标签,固定“一个自然的问题:为什么不用线性回归?”的分类结果。
is_default = np.random.binomial(1, prob_default)
# 将债务收入比与二元违约标签整理为分类演示表。
df = pd.DataFrame({'dti': dti, 'is_default': is_default})
# 提取 `X` 作为“一个自然的问题:为什么不用线性回归?”的特征矩阵,明确模型可见的输入列。
X = df[['dti']]
# 提取违约标签,展示 OLS 对二元响应的局限。
y = df['is_default']
# 在二元标签上估计 OLS 线性概率基准,故意保留其范围缺陷供比较。
# 建立 OLS 基准模型,用于展示线性概率预测可能越过 `[0, 1]` 边界。
ols_model = LinearRegression()
# 用全部玩具样本估计 OLS 截距与 DTI 斜率,供线性概率边界反例绘图。
ols_model.fit(X, y)
# 建立 `x_fit` 的有序取值网格,用于展示“线性回归(OLS)直接应用于0/1分类问题的缺陷”随参数变化的比较结果。
x_fit = np.linspace(0, 60, 100).reshape(-1, 1)
# 在覆盖 DTI 范围的网格上生成 OLS 概率预测,检查两端是否越过零或一。
y_fit = ols_model.predict(x_fit)
# 创建 `fig, ax` 画布,承载“线性回归(OLS)直接应用于0/1分类问题的缺陷”的并排视觉比较。
fig, ax = plt.subplots(figsize=(10, 6))
# 以 `'dti'` 为横轴、`'is_default'` 为纵轴绘制散点,数据来自 `df`,展示“线性回归(OLS)直接应用于0/1分类问题的缺陷”。
sns.scatterplot(data=df, x='dti', y='is_default', ax=ax, s=80, alpha=0.7, label='实际数据 (0=未违约, 1=违约)')
# 以 `x_fit` 为横轴、`y_fit` 为纵轴绘制曲线,展示“线性回归(OLS)直接应用于0/1分类问题的缺陷”。
ax.plot(x_fit, y_fit, color='crimson', lw=3, label='OLS 拟合线')
# 添加概率边界、越界标注与统一坐标样式,突出 OLS 反例的失败区域。
# 在 `0` 处添加水平参考线,标出“线性回归(OLS)直接应用于0/1分类问题的缺陷”的基准或阈值。
ax.axhline(0, color='grey', linestyle='--')
# 在 `1` 处添加水平参考线,标出“线性回归(OLS)直接应用于0/1分类问题的缺陷”的基准或阈值。
ax.axhline(1, color='grey', linestyle='--')
# 在图中标注“概率 > 1”,解释“线性回归(OLS)直接应用于0/1分类问题的缺陷”的关键位置。
ax.text(55, 1.05, '概率 > 1', color='red', fontsize=12)
# 在图中标注“概率 < 0”,解释“线性回归(OLS)直接应用于0/1分类问题的缺陷”的关键位置。
ax.text(2, -0.05, '概率 < 0', color='red', fontsize=12)
# 为“一个自然的问题:为什么不用线性回归?”,限定纵轴范围,使比较对象使用一致尺度。
ax.set_ylim(-0.2, 1.2)
# 为“一个自然的问题:为什么不用线性回归?”,限定横轴范围,使比较对象使用一致尺度。
ax.set_xlim(0, 60)
# 将图题设为“线性回归无法将预测值约束在 区间内”,直接说明当前图形的比较目的。
ax.set_title('线性回归无法将预测值约束在 区间内', fontsize=16)
# 将横轴标为“负债收入比 (DTI)”,明确横向编码的变量。
ax.set_xlabel('负债收入比 (DTI)', fontsize=12)
# 把纵轴标签设为 `'预测违约概率 / 实际违约'`。
ax.set_ylabel('预测违约概率 / 实际违约', fontsize=12)
# 显示“一个自然的问题:为什么不用线性回归?”图例,使颜色或线型与比较对象一一对应。
ax.legend()
# 显示二元标签与 OLS 预测线,检查预测值越界及线性概率模型的结构局限。
plt.show()
缺陷 1:预测值越界
- 触发条件:
负债收入比非常高或非常低。
- 模型结果:线性预测值可能小于 0 或大于 1。
- 核心缺陷:超出 \([0,1]\) 的数不能解释为概率。
局限 2:二元误差异方差,推断需换口径
- 误差取值:若 \(p(\mathbf{x})=E[Y\mid\mathbf{X}=\mathbf{x}]\),则 \(\epsilon=Y-p(\mathbf{x})\) 只取 \(1-p\) 或 \(-p\),因而非正态。
- 条件方差:\(\operatorname{Var}(\epsilon\mid\mathbf{X}=\mathbf{x})=p(\mathbf{x})[1-p(\mathbf{x})]\),会随 \(\mathbf{x}\) 改变。
- 一致性边界:线性条件均值规格正确且满足外生性等常规条件时,LPM 系数仍可一致。
- 推断口径:不能直接使用同方差标准误和有限样本正态理论的精确 \(t/F\) 推断;应报告异方差稳健标准误,并说明采用大样本渐近推断。
LPM 与 Logit 的选择边界
- LPM 可用之处:在线性条件均值、外生性和常规条件成立时,系数可作平均概率差的线性近似;推断应使用异方差稳健标准误与渐近口径。
- Logit 的主要动机:将预测概率约束在 \((0,1)\),允许非线性条件概率,并在样本外检查校准。
- 禁止误读:二元误差非正态不意味着 LPM 估计或检验天然全部不可靠;两种模型都不会自动给出因果解释。
我们需要一个新工具
- 输入:线性得分 \(z=\mathbf{w}^T\mathbf{x}\in(-\infty,+\infty)\)。
- 所需映射:平滑、单调地把任意实数压缩到 \((0,1)\)。
- 解释边界:落在概率区间内并不自动保证概率已经校准。
Part 2: 模型形式
逻辑函数如何优雅地解决问题
解决方案:引入逻辑函数 (Logistic Function)
逻辑函数,也常被称为 Sigmoid 函数,提供平滑、单调的 \((0,1)\) 映射。它的数学形式如下:
\[ \large{g(z) = \frac{e^z}{1 + e^z} = \frac{1}{1 + e^{-z}}} \]
其中,\(z\) 就是我们熟悉的线性组合:\(z = w_0 + w_1x_1 + ... + w_kx_k = \mathbf{w}^T\mathbf{x}\)。
逻辑回归模型:线性与非线性的结合
我们的逻辑回归模型 \(f(\mathbf{x})\) 就是将线性模型的结果输入到逻辑函数中:
\[ \large{f(\mathbf{x}) = g(\mathbf{w}^T\mathbf{x}) = \frac{1}{1 + e^{-\mathbf{w}^T\mathbf{x}}}} \]
这是一个两步过程:
- 线性部分: 计算一个得分 \(z = \mathbf{w}^T\mathbf{x}\)。
- 非线性转换: 用逻辑函数 \(g(z)\) 得到模型概率分数;只有在指定 Bernoulli 条件模型时才解释为 \(P(Y=1\mid X)\),并须在留出数据上检查校准。
逻辑函数能将任意实数映射到 (0, 1) 区间
这个优美的S型曲线是逻辑回归的核心。
代码
# 为“逻辑函数能将任意实数映射到 (0, 1) 区间”导入 `numpy` 并绑定 `np`,用于执行当前任务的数组、数值或随机机制计算。
import numpy as np
# 为“逻辑函数能将任意实数映射到 (0, 1) 区间”导入 `matplotlib.pyplot` 并绑定 `plt`,用于构建当前任务的坐标轴并呈现比较结果。
import matplotlib.pyplot as plt
# 定义 `sigmoid`,把线性得分转换为 0—1 概率。
def sigmoid(z):
# 返回 `1 / (1 + np.exp(-z))`,把“逻辑函数能将任意实数映射到 (0, 1) 区间”的计算结果交给调用方。
return 1 / (1 + np.exp(-z))
# 建立 `z` 的有序取值网格,用于展示“逻辑 (Sigmoid) 函数的S型曲线”随参数变化的比较结果。
z = np.linspace(-10, 10, 200)
# 把线性得分映射为 `g_z` 概率曲线,展示 Sigmoid 的区间压缩作用。
g_z = sigmoid(z)
# 创建 `fig, ax` 画布,承载“逻辑 (Sigmoid) 函数的S型曲线”的并排视觉比较。
fig, ax = plt.subplots(figsize=(10, 6))
# 以 `z` 为横轴、`g_z` 为纵轴绘制曲线,展示“逻辑 (Sigmoid) 函数的S型曲线”。
ax.plot(z, g_z, color='dodgerblue', lw=3)
# 添加零、一与零点五参考线及公式标注,解释逻辑函数的概率边界。
# 在 `0.0` 处添加水平参考线,标出“逻辑 (Sigmoid) 函数的S型曲线”的基准或阈值。
ax.axhline(0.0, color='grey', linestyle='--')
# 在 `1.0` 处添加水平参考线,标出“逻辑 (Sigmoid) 函数的S型曲线”的基准或阈值。
ax.axhline(1.0, color='grey', linestyle='--')
# 在 `0.5` 处添加水平参考线,标出“逻辑 (Sigmoid) 函数的S型曲线”的基准或阈值。
ax.axhline(0.5, color='grey', linestyle=':', lw=1)
# 在 `0.0` 处添加垂直参考线,标出“逻辑 (Sigmoid) 函数的S型曲线”的基准或阈值。
ax.axvline(0.0, color='grey', linestyle=':', lw=1)
# 在图中标注“概率上限 = 1”,解释“逻辑 (Sigmoid) 函数的S型曲线”的关键位置。
ax.text(8, 0.95, '概率上限 = 1', color='grey', fontsize=14)
# 在图中标注“概率下限 = 0”,解释“逻辑 (Sigmoid) 函数的S型曲线”的关键位置。
ax.text(8, 0.05, '概率下限 = 0', color='grey', fontsize=14)
# 在图中标注“g(z) = 0.5”,解释“逻辑 (Sigmoid) 函数的S型曲线”的关键位置。
ax.text(0.2, 0.52, 'g(z) = 0.5', color='black', fontsize=12)
# 在图中标注“$g(z) = \frac{1}{1 + e^{-z}}$”,解释“逻辑 (Sigmoid) 函数的S型曲线”的关键位置。
ax.text(5, 0.6, r'$g(z) = \frac{1}{1 + e^{-z}}$', color='dodgerblue', fontsize=18)
# 隐藏上边框,使视线集中在 Sigmoid 曲线与两条渐近线。
ax.spines['top'].set_visible(False)
# 隐藏右边框,使概率曲线使用更简洁的坐标框。
ax.spines['right'].set_visible(False)
# 用图题概括“逻辑函数能将任意实数映射到 (0, 1) 区间”的比较对象与当前计算结果。
ax.set_title('逻辑函数将 z ∈ (-∞, +∞) 映射到 g(z) ∈ (0, 1)', fontsize=16)
# 将横轴标为“z (线性组合 wᵀx)”,明确横向编码的变量。
ax.set_xlabel('z (线性组合 wᵀx)', fontsize=12)
# 将纵轴标为“g(z) (预测概率)”,明确纵向编码的变量。
ax.set_ylabel('g(z)(模型概率分数)', fontsize=12)
# 显示逻辑函数的 S 形曲线,核对任意实数输入都被压缩到零与一之间。
plt.show()
逻辑函数的关键特性
案例图解:逻辑函数如何“弯曲”线性关系
我们来看一个具体的例子,假设经过模型训练,我们得到的线性关系是 \(z = -0.4 + 0.8x\)。
代码
# 为“案例图解:逻辑函数如何“弯曲”线性关系”导入 `numpy` 并绑定 `np`,用于执行当前任务的数组、数值或随机机制计算。
import numpy as np
# 为“案例图解:逻辑函数如何“弯曲”线性关系”导入 `matplotlib.pyplot` 并绑定 `plt`,用于构建当前任务的坐标轴并呈现比较结果。
import matplotlib.pyplot as plt
# 定义 `g`,把线性预测经 Sigmoid 映射为概率。
def g(z):
# 返回 `1 / (1 + np.exp(-z))`,把“案例图解:逻辑函数如何“弯曲”线性关系”的计算结果交给调用方。
return 1 / (1 + np.exp(-z))
# 建立 `x` 的有序取值网格,用于展示“线性函数通过逻辑函数转换为概率预测”随参数变化的比较结果。
x = np.linspace(-10, 10, 200)
# 在特征网格上计算类别表面 `z`,用于绘制决策区域。
z = -0.4 + 0.8 * x
# 计算线性得分 `f_x`,作为 Sigmoid 转换前的模型输出。
f_x = g(z)
# 创建 `fig, ax` 画布,承载“线性函数通过逻辑函数转换为概率预测”的并排视觉比较。
fig, ax = plt.subplots(figsize=(10, 6))
# 绘出未压缩的线性得分,作为 Sigmoid 概率映射的对照。
ax.plot(x, z, color='grey', lw=2, linestyle='--', label=r'线性部分: $z = -0.4 + 0.8x$')
# 以 `x` 为横轴、`f_x` 为纵轴绘制曲线,展示“线性函数通过逻辑函数转换为概率预测”。
ax.plot(x, f_x, color='crimson', lw=3, label=r'逻辑回归输出: $f(x) = g(z)$')
# 添加线性得分、概率阈值和对应点标注,连接两个坐标面板的映射。
# 在 `0.0` 处添加水平参考线,标出“线性函数通过逻辑函数转换为概率预测”的基准或阈值。
ax.axhline(0.0, color='darkgrey', linestyle='-', lw=1)
# 在 `1.0` 处添加水平参考线,标出“线性函数通过逻辑函数转换为概率预测”的基准或阈值。
ax.axhline(1.0, color='darkgrey', linestyle='-', lw=1)
# 在 `0.0` 处添加垂直参考线,标出“线性函数通过逻辑函数转换为概率预测”的基准或阈值。
ax.axvline(0.0, color='darkgrey', linestyle='-', lw=1)
# 求出概率曲线与 0.5 阈值的交点 `x_intersect`,定位分类决策边界。
x_intersect = 0.5
# 以 `x_intersect` 为横轴、`0.5` 为纵轴绘制曲线,展示“线性函数通过逻辑函数转换为概率预测”。
ax.plot(x_intersect, 0.5, 'bo', markersize=8, zorder=5)
# 在 `ax` 上画出指定横坐标和纵向范围的竖线。
ax.vlines(x_intersect, -4, 0.5, color='blue', linestyle=':', lw=2)
# 在 `ax` 上画出指定纵坐标和横向范围的横线。
ax.hlines(0.5, -10, x_intersect, color='blue', linestyle=':', lw=2)
# 固定 `ax.text(x_intersect + 0.2, 0.6, f'决策边界: x` 的业务取值与顺序,作为“案例图解:逻辑函数如何“弯曲”线性关系”的可复算输入。
ax.text(x_intersect + 0.2, 0.6, f'决策边界: x = {x_intersect:.1f}\n预测概率 = 0.5', color='blue')
# 将图题设为“逻辑函数保持了单调性,同时将输出约束在概率区间”,直接说明当前图形的比较目的。
ax.set_title('逻辑函数保持了单调性,同时将输出约束在概率区间', fontsize=16)
# 将横轴标为“特征 x”,明确横向编码的变量。
ax.set_xlabel('特征 x', fontsize=12)
# 将纵轴标为“输出值”,明确纵向编码的变量。
ax.set_ylabel('输出值', fontsize=12)
# 显示“案例图解:逻辑函数如何“弯曲”线性关系”图例,使颜色或线型与比较对象一一对应。
ax.legend(fontsize=12)
# 为“案例图解:逻辑函数如何“弯曲”线性关系”,限定纵轴范围,使比较对象使用一致尺度。
ax.set_ylim(-4, 4)
# 显示线性预测子与逻辑概率的对应关系,检查阈值附近最陡、两端渐近饱和的形状。
plt.show()
Part 3: 模型训练
如何找到最优的权重 w?
核心问题:如何衡量“好”与“坏”?
我们已经确定了模型的数学形式,但如何找到一组最优的权重向量 w,使得模型的预测最接近真实的标签 y 呢?
这就引出了机器学习的核心概念:代价函数 (Cost Function)。
- 代价函数 \(J(\mathbf{w})\): 衡量模型在整个训练集上预测的平均误差。
- 我们的目标: 找到能使代价函数 \(J(\mathbf{w})\) 最小化的 \(\mathbf{w}\)。
\[ \large{\min_{\mathbf{w}} J(\mathbf{w})} \]
统计基础:最大似然估计 (MLE)
理解逻辑回归代价函数的最佳方式是通过最大似然估计 (Maximum Likelihood Estimation, MLE)。
- 给定参数:模型为每个训练观测赋予一个条件概率。
- 联合似然:把所有观测的条件概率组合成训练数据的似然。
- 估计目标:选择使已观察训练数据似然最大的参数 \(\mathbf{w}\)。
步骤 1: 单个样本的似然
- 模型输出: \(f(\mathbf{x}^{(i)})\) 是模型预测第 \(i\) 个样本为正类(
y=1)的概率。
- 概率表示:
- 如果真实标签 \(y^{(i)} = 1\),该观测出现的概率是 \(f(\mathbf{x}^{(i)})\)。
- 如果真实标签 \(y^{(i)} = 0\),该观测出现的概率是 \(1 - f(\mathbf{x}^{(i)})\)。
一个巧妙的统一表达式
我们可以将上述两种情况合并成一个表达式。对于单个样本 \((\mathbf{x}^{(i)}, y^{(i)})\),其发生的概率(即似然)为:
\[ \large{P(y^{(i)}|\mathbf{x}^{(i)}; \mathbf{w}) = [f(\mathbf{x}^{(i)})]^{y^{(i)}} \cdot [1 - f(\mathbf{x}^{(i)})]^{1-y^{(i)}}} \]
- 验证:
- 当 \(y^{(i)}=1\) 时, 上式变为 \(f(\mathbf{x}^{(i)})\)。
- 当 \(y^{(i)}=0\) 时, 上式变为 \(1 - f(\mathbf{x}^{(i)})\)。
步骤 2: 整个数据集的似然函数
假设我们有 n 个独立的训练样本,那么整个数据集出现的联合概率(总似然函数 \(L(\mathbf{w})\))就是每个样本概率的乘积:
\[ \large{L(\mathbf{w}) = \prod_{i=1}^{n} P(y^{(i)}|\mathbf{x}^{(i)}; \mathbf{w})} \]
\[ \large{= \prod_{i=1}^{n} [f(\mathbf{x}^{(i)})]^{y^{(i)}} \cdot [1 - f(\mathbf{x}^{(i)})]^{1-y^{(i)}}} \]
我们的目标是最大化这个 \(L(\mathbf{w})\)。
步骤 3: 对数转换简化计算
- 计算难点:\(L(\mathbf{w})\) 是许多概率的连乘。
- 等价变换:
log 单调递增,因此最大化 \(L\) 等价于最大化 \(\log L\)。
- 计算收益:对数把连乘变为连加。
\[ \large{l(\mathbf{w}) = \log L(\mathbf{w}) = \sum_{i=1}^{n} \left[ y^{(i)}\log f(\mathbf{x}^{(i)}) + (1 - y^{(i)})\log(1 - f(\mathbf{x}^{(i)})) \right]} \]
步骤 4: 从最大化似然到最小化代价
在机器学习中,我们习惯于将问题表述为最小化一个代价函数。
- 对数似然 \(l(\mathbf{w})\) 越大,训练数据在模型下越相容。
- 改变符号,把最大化 \(l(\mathbf{w})\) 写成最小化 \(-l(\mathbf{w})\)。
- 再除以样本量 \(n\),得到平均对数损失 \(J(\mathbf{w})\)。
\[ \large{J(\mathbf{w}) = -\frac{1}{n} \sum_{i=1}^{n} \left[ y^{(i)}\log f(\mathbf{x}^{(i)}) + (1 - y^{(i)})\log(1 - f(\mathbf{x}^{(i)})) \right]} \]
这个函数也被称为对数损失 (Log Loss) 或交叉熵损失 (Cross-Entropy Loss)。
图解损失函数:当真实 y=1
- 损失变为 \(C = -\log(f(\mathbf{x}))\)。
- 如果模型预测概率 \(f(\mathbf{x}) \to 1\) (预测正确),那么损失 \(-\log(1) \to 0\)。
- 如果模型预测概率 \(f(\mathbf{x}) \to 0\) (预测错误),那么损失 \(-\log(0) \to +\infty\)。
图解损失函数:当真实 y=0
- 损失变为 \(C = -\log(1 - f(\mathbf{x}))\)。
- 如果模型预测概率 \(f(\mathbf{x}) \to 0\) (预测正确),那么损失 \(-\log(1) \to 0\)。
- 如果模型预测概率 \(f(\mathbf{x}) \to 1\) (预测错误),那么损失 \(-\log(0) \to +\infty\)。
结论: 这个损失函数的设计非常合理:预测越准确,损失越小;预测越离谱,损失就越大。
最小化代价函数:梯度下降法
- 目标地形:代价函数 \(J(\mathbf{w})\) 定义需要寻找的“谷底”。
- 局部方向:梯度指出当前参数处上升最快的方向。
- 更新策略:沿负梯度迭代移动,即梯度下降法(Gradient Descent)。
核心思想: 在当前位置,寻找最陡峭的下坡方向,然后迈出一步。重复此过程。
梯度下降的更新规则
- “最陡峭的下坡方向”是代价函数梯度的负方向 \((-\nabla J(\mathbf{w}))\)。
- “迈出一步”的大小由学习率 (Learning Rate) \(\alpha\) 控制。
对于每一个权重 \(w_j\),同步更新:
\[ \large{w_j := w_j - \alpha \frac{\partial}{\partial w_j} J(\mathbf{w})} \]
这里的核心是计算代价函数对每个权重的偏导数(梯度)。
学习率 \(\alpha\) 的重要性
学习率决定了我们“下山”的步子迈多大。选择合适的学习率至关重要。
逻辑回归代价函数的梯度
经过微积分推导,我们可以得到一个非常简洁和优美的结果:
\[ \large{\frac{\partial}{\partial w_j} J(\mathbf{w}) = \frac{1}{n} \sum_{i=1}^{n} (f(\mathbf{x}^{(i)}) - y^{(i)}) x_j^{(i)}} \]
- \(f(\mathbf{x}^{(i)}) - y^{(i)}\) 是第 \(i\) 个样本的预测误差。
- \(x_j^{(i)}\) 是第 \(i\) 个样本的第 \(j\) 个特征值。
这个梯度的形式和我们在线性回归中学到的几乎完全一样!
逻辑回归的梯度下降完整算法
初始化: 随机初始化权重向量 \(\mathbf{w}\) (例如,全零向量)。
迭代更新: 重复以下步骤直到收敛:
对于 \(j = 0, 1, ..., k\):
\[ \large{w_j := w_j - \alpha \frac{1}{n} \sum_{i=1}^{n} (\frac{1}{1 + e^{-\mathbf{w}^T\mathbf{x}^{(i)}}} - y^{(i)}) x_j^{(i)}} \]
(注意:\(x_0^{(i)}\) 通常设为1,对应截距项 \(w_0\))
收敛: 当 \(\mathbf{w}\) 的变化非常小,或代价函数 \(J(\mathbf{w})\) 不再显著下降时,算法停止。
Part 4: 模型应用与解读
模型训练好了,我们能用它做什么?
从概率到决策:决策边界
模型训练好之后,我们如何用它来进行分类决策呢?一个常见的方法是设定一个概率阈值,通常是 0.5。
- 如果模型预测概率 \(f(\mathbf{x}) > 0.5\),我们就分类为正类 (y=1)。
- 如果模型预测概率 \(f(\mathbf{x}) \le 0.5\),我们就分类为负类 (y=0)。
决策边界 (Decision Boundary) 就是使得预测概率恰好等于 0.5 的点的集合。
决策边界在数学上是一个超平面
我们知道,当 \(f(\mathbf{x}) = 0.5\) 时,逻辑函数的输入 \(z\) 必须为0。
\[ \large{f(\mathbf{x}) = \frac{1}{1 + e^{-\mathbf{w}^T\mathbf{x}}} = 0.5 \implies \mathbf{w}^T\mathbf{x} = 0} \]
因此,决策边界由以下线性方程定义:
\[ \large{\mathbf{w}^T\mathbf{x} = w_0 + w_1x_1 + ... + w_kx_k = 0} \]
- 几何意义:
- 当只有两个特征 (\(x_1, x_2\)) 时,决策边界是一条直线。
- 当有更多特征时,它是一个超平面 (Hyperplane)。
决策边界的可视化
决策边界(黑线)将特征空间一分为二。
代码
# 为“决策边界的可视化”导入 `numpy` 并绑定 `np`,用于执行当前任务的数组、数值或随机机制计算。
import numpy as np
# 为“决策边界的可视化”导入 `matplotlib.pyplot` 并绑定 `plt`,用于构建当前任务的坐标轴并呈现比较结果。
import matplotlib.pyplot as plt
# 为“决策边界的可视化”,从 `sklearn.datasets` 导入 `make_classification`,取得本节机制演示的数据接口。
from sklearn.datasets import make_classification
# 为“决策边界的可视化”,从 `sklearn.linear_model` 导入`LogisticRegression` 用于拟合逻辑回归分类器。
from sklearn.linear_model import LogisticRegression
# 生成二维分类机制样本,为逻辑概率表面和决策边界提供输入。
# 生成二维可分机制样本,专门用于展示逻辑回归决策边界的几何形状。
X, y = make_classification(n_samples=100, n_features=2, n_informative=2, n_redundant=0,
# 每个类别只生成一个簇,使二维决策边界保持清晰可辨。
n_clusters_per_class=1, flip_y=0.1, random_state=1)
# 用二维样本拟合逻辑回归,估计绘制概率区域所需的系数。
# 创建 `LogisticRegression` 实例 `model`,用于拟合逻辑回归分类器。
model = LogisticRegression(solver='liblinear')
# 用二维玩具样本估计逻辑回归系数,供网格概率与 `p=0.5` 边界计算。
model.fit(X, y)
# 在两项特征范围内构造细网格,用于计算连续正类概率表面。
# 用第一特征的样本极值外扩一单位,确定决策面横向绘图区间。
x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
# 用第二特征的样本极值外扩一单位,确定决策面纵向绘图区间。
y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
# 建立 `xx, yy` 的有序取值网格,用于展示“一个二维特征空间中的线性决策边界”随参数变化的比较结果。
xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.02), np.arange(y_min, y_max, 0.02))
# 计算网格点属于正类的概率并提取第二列,形成决策区域的扁平概率数组。
Z = model.predict_proba(np.c_[xx.ravel(), yy.ravel()])[:, 1]
# 在特征网格上计算类别表面 `Z`,用于绘制决策区域。
Z = Z.reshape(xx.shape)
# 创建 `fig, ax` 画布,承载“一个二维特征空间中的线性决策边界”的并排视觉比较。
fig, ax = plt.subplots(figsize=(12, 6))
# 保存决策区域图层 `contour`,随后为类别背景添加颜色标尺。
contour = ax.contourf(xx, yy, Z, cmap='RdBu_r', alpha=0.6)
# 为当前颜色编码添加数值色标。
fig.colorbar(contour, ax=ax, label='预测为正类 (y=1) 的概率')
# 用 `ax.contour` 绘制预测类别边界的等值线。
ax.contour(xx, yy, Z, levels=[0.5], colors='black', linewidths=2)
# 保留 `scatter` 的绘图句柄,用于构造与类别颜色一致的图例。
negative = y == 0
ax.scatter(X[negative, 0], X[negative, 1], c='#2C7BB6', marker='o', edgecolors='k', s=90, label='负类(y=0)')
ax.scatter(X[~negative, 0], X[~negative, 1], c='#D7191C', marker='^', edgecolors='k', s=100, label='正类(y=1)')
# 颜色与点形共同区分类别,方便投影和灰度阅读。
ax.legend()
# 将图题设为“决策边界 (wᵀx=0) 分割了预测区域”,直接说明当前图形的比较目的。
ax.set_title('决策边界 (wᵀx=0) 分割了预测区域', fontsize=16)
# 使用普通数字,避免不同电脑缺少 Unicode 下标字形。
ax.set_xlabel('特征 1', fontsize=14)
ax.set_ylabel('特征 2', fontsize=14)
# 在图中标注“预测为 负类 (p < 0.5)”,解释“一个二维特征空间中的线性决策边界”的关键位置。
ax.text(-2, -2.5, '预测为 负类 (p < 0.5)', ha='center', fontsize=12, color='white')
# 在图中标注“预测为 正类 (p > 0.5)”,解释“一个二维特征空间中的线性决策边界”的关键位置。
ax.text(1.5, 2, '预测为 正类 (p > 0.5)', ha='center', fontsize=12, color='white')
# 显示二维样本、概率背景与 `p=0.5` 决策边界,检查两类区域的划分及边界附近不确定性。
plt.show()
解读模型:系数的意义
逻辑回归不仅能做预测,它强大的解释性也是其在经济金融领域广受欢迎的重要原因。
- 系数的符号: 在其余变量固定、当前模型规格不变时,若 \(w_j>0\),\(x_j\) 增加与事件对数几率上升相关;这不是因果效应。
- 统计显著性: 我们可以使用 p-value 来判断某个特征与分类结果之间是否存在统计上的显著关系。
但是,这里有一个巨大的陷阱!
挑战:逻辑回归的系数不能被线性解释
这是初学者最容易犯的错误!
在线性回归中,系数 \(\beta_j\) 的意义是:当 \(x_j\) 增加一个单位时,\(y\) 平均增加 \(\beta_j\) 个单位。
但在逻辑回归中,\(w_j\) 的意义是:当 \(x_j\) 增加一个单位时,对数几率 (Log-Odds) 会增加 \(w_j\) 个单位。
\[ \large{\log\left(\frac{p}{1-p}\right) = w_0 + w_1x_1 + \dots + w_jx_j + \dots} \]
“对数几率”这个单位非常不直观,我们很难向非专业人士解释清楚。
解决方案:边际效应 (Marginal Effects)
为了用更直观的方式解释系数,我们引入边际效应 (Marginal Effect, ME)。
定义: 边际效应衡量的是,当一个特征 \(x_j\) 变化一个单位时,预测概率 \(p\) 本身会变化多少。
\[ \large{ME_j = \frac{\partial p}{\partial x_j}} \]
数学推导: 通过对逻辑回归模型 \(p = f(\mathbf{x})\) 求偏导,我们可以得到:
\[ \large{ME_j = \frac{\partial p}{\partial x_j} = f(\mathbf{x}) \cdot (1 - f(\mathbf{x})) \cdot w_j = p(1-p)w_j} \]
关键特点:边际效应不是一个常数!
从公式 \(ME_j = p(1-p)w_j\) 可以看出,边际效应的大小取决于当前点的预测概率 \(p\)(即取决于所有 \(x\) 的值)。
如何汇报一个统一的边际效应?
因为边际效应因人而异,我们通常汇报两种汇总统计量:
- 均值处的边际效应 (MEM): 先计算所有特征的均值 \(\bar{\mathbf{x}}\),然后计算在这一点上的边际效应。
- 平均边际效应 (AME): 为每一个样本点计算其边际效应,然后取所有边际效应的平均值。
AME (Average Marginal Effect) 通常被认为是更稳健和有代表性的度量。
Part 5: Python 实践
使用 statsmodels 和 scikit-learn
两种工具,两种哲学
| 主要目标 |
推断 (Inference) |
预测 (Prediction) |
| 核心优势 |
详细的统计摘要、p值、置信区间、边际效应 |
统一的API、丰富的模型库、完整的ML步骤 |
| 适用场景 |
学术研究、经济分析、需要解释变量影响的商业报告 |
实际预测、模型比较、较复杂的机器学习流程 |
| 一句话总结 |
“我想理解数据背后的关系” |
“我想对新数据做出最准确的预测” |
statsmodels 实践:解释中国上市公司非标准审计意见
statsmodels 是 Python 中进行统计建模和计量经济学分析的首选库。它的设计哲学是“解释”和“推断”。
我们的步骤:
- 导入库并加载数据。
- 构建并拟合
Logit 模型。
- 解读模型摘要报告。
- 计算并解释边际效应。
步骤 1: 导入库与数据准备
- 文件:公开的
audit_opinion.h5,key=audit_opinion。
- 样本:2014—2023 财年年度财报审计记录;每个公司—财年保留首次披露。
- 字段:公司代码、财年、披露日、审计机构与意见类型。
- 标签:
modified_opinion=1 表示非标准审计意见,即非纯标准无保留意见。
- 解释边界:分析同一记录中的模型关联,不是提前预测或因果效应。
代码
# 为“步骤 1: 导入库与数据准备”导入 `pandas` 并绑定 `pd`,用于整理当前任务的表格、字段与时间索引。
import pandas as pd
from pathlib import Path
from urllib.request import urlretrieve # 复用本章隐藏设置单元安装的浏览器标识下载器
# 导入 statsmodels 并绑定为 `sm`,用于给设计矩阵加截距并估计可报告系数检验的 Logit 模型。
import statsmodels.api as sm
# 所有读者都从同一公开地址取得课程数据;下载一次后可离线重复运行。
# 按 Linux 共享数据、Windows 共享数据、项目缓存的顺序选择审计意见文件。
audit_path = next((candidate_path for candidate_path in [Path('/home/ubuntu/r2_data_mount/data/stock/audit_opinion.h5'), Path('C:/qiufei/data/stock/audit_opinion.h5'), Path('data/course/audit_opinion.h5')] if candidate_path.exists()), Path('data/course/audit_opinion.h5'))
if not audit_path.exists():
audit_path.parent.mkdir(parents=True, exist_ok=True)
urlretrieve('https://assets.qiufei.site/data/stock/audit_opinion.h5', audit_path)
# 读取审计意见表,保留全部披露版本供按时间筛选。
audit_raw = pd.read_hdf(audit_path, key='audit_opinion')
# 把披露日期统一为时间戳,以便计算年末至审计报告日的滞后。
audit_raw['info_date'] = pd.to_datetime(audit_raw['info_date'])
# 只保留 2014—2023 年度财报审计记录,确定本节样本期与报告类型。
audit_raw = audit_raw.query("type == 'financial_statements' and quarter.str.endswith('q4') and '2014q4' <= quarter <= '2023q4'", engine='python').copy()
# 统计存在多个披露版本的公司—财年键,量化重发污染风险。
duplicate_company_years = int(audit_raw.groupby(['order_book_id', 'quarter']).size().gt(1).sum())
# 按披露日选择每个公司—财年的首次可得版本,禁止后续修订回填同期样本。
data = audit_raw.sort_values(['order_book_id', 'quarter', 'info_date']).drop_duplicates(['order_book_id', 'quarter'], keep='first').copy()
# 检查点时观察单位唯一,防止同一公司财年重复加权。
assert not data.duplicated(['order_book_id', 'quarter']).any(), '首次披露数据版本公司—财年键不唯一'
# 从季度字段提取财年,作为审计制度与披露环境的时间控制量。
data['fiscal_year'] = data['quarter'].str[:4].astype(int)
# 计算财年末至披露日的天数,描述当前审计报告的披露滞后。
data['report_delay_days'] = (data['info_date'] - pd.to_datetime(data['fiscal_year'].astype(str) + '-12-31')).dt.days
# 标记常见国际四大会计师事务所在中国的名称,用作可解释的机构类别变量。
data['is_big4_cn'] = data['audit_agency'].fillna('').str.contains('普华永道|德勤|毕马威|安永').astype(int)
# 将标准无保留意见编码为零、其他意见编码为一,固定正类业务含义。
data['modified_opinion'] = data['opinion_type'].ne('unqualified').astype(int)
# 固定披露滞后、财年与机构类别三项解释变量,限定本节教学规格。
features = ['report_delay_days', 'fiscal_year', 'is_big4_cn']
# 将非标准意见标记固定为二分类目标列。
target = 'modified_opinion'
# 报告版本筛选流量与异常长披露滞后,明确当前同期描述的样本边界。
print({'原始版本行': len(audit_raw), '重复公司财年键': duplicate_company_years, '首次披露行': len(data), '移除后续版本': len(audit_raw) - len(data), '披露滞后超过550天': int(data['report_delay_days'].gt(550).sum())})
# 提取 `X` 作为“步骤 1: 导入库与数据准备”的特征矩阵,明确模型可见的输入列。
X = data[features]
# 提取已重编码的审计意见目标,其中 1 表示非标准意见类别。
y = data[target]
# 给三项特征添加常数列,使 statsmodels 显式估计 Logit 截距。
X_sm = sm.add_constant(X)
{'原始版本行': 50892, '重复公司财年键': 438, '首次披露行': 50452, '移除后续版本': 440, '披露滞后超过550天': 6467}
步骤 1 (续): 数据预览
让我们看一下准备好的数据。
代码
# 为“步骤 1 (续): 数据预览”,显示前几条记录以核对字段与取值。
X_sm.head()
步骤 1 (续): 目标预览
单独核对二分类目标,确认 1 只表示非标准审计意见。
代码
# 为“步骤 1 (续): 数据预览”,显示前几条记录以核对字段与取值。
y.head()
步骤 2: 构建并拟合 Logit 模型
使用 sm.Logit 类来构建模型,然后调用 .fit() 方法来执行训练(即找到最优的 w)。
代码
# 构建 Logit 模型
logit_model = sm.Logit(y, X_sm)
# 按公司代码聚类标准误,处理同一公司跨财年的相关性而不把记录误作独立横截面。
result = logit_model.fit(cov_type='cluster', cov_kwds={'groups': data['order_book_id']})
Optimization terminated successfully.
Current function value: 0.176652
Iterations 9
步骤 3: 解读 statsmodels 的摘要报告
.summary() 方法提供了一份信息极其丰富的报告,是进行严肃的经济学分析的关键。
代码
# 将系数、公司聚类标准误、p 值与置信区间压成投影可读表。
logit_inference = pd.concat([result.params.rename('coef'), result.bse.rename('cluster_std_err'), result.pvalues.rename('p_value'), result.conf_int().rename(columns={0: 'ci_low', 1: 'ci_high'})], axis=1)
# 展示本次执行生成的唯一系数证据,避免默认摘要滚动和旧案例数字残留。
display(logit_inference.round(6))
| const |
-160.395592 |
21.119221 |
0.0 |
-201.788505 |
-119.002680 |
| report_delay_days |
-0.001541 |
0.000179 |
0.0 |
-0.001891 |
-0.001191 |
| fiscal_year |
0.078085 |
0.010453 |
0.0 |
0.057598 |
0.098573 |
| is_big4_cn |
-1.592093 |
0.244858 |
0.0 |
-2.072005 |
-1.112181 |
如何解读这份摘要报告?
Dep. Variable: 因变量是 modified_opinion,1 表示非标准审计意见。
Model: 模型是 Logit。
Covariance Type: 标准误按公司代码聚类,以允许同一公司跨财年相关。
Pseudo R-squ. 与 Log-Likelihood: 描述当前规格的样本内拟合,不能替代样本外验证。
这是最核心的部分。
coef: 系数的点估计值 (即 \(w_j\))。
std err: 标准误,衡量系数估计值的不确定性。
z: z-统计量 (coef / std err),用于假设检验。
P>|z|: 在原假设“该系数为 0”及模型假设成立时,得到当前或更极端 z 统计量的概率。阈值不能替代效应量、置信区间、模型诊断或研究设计。
[0.025 0.975]: 95% 置信区间。
系数表回答同一审计意见问题
report_delay_days 每增加 1 天,系数表给出非标准意见 log-odds 的条件关联;单位不能改写成百分比。
fiscal_year 每增加 1 年的系数混合制度、市场与样本构成变化,不是时间趋势的因果效应。
is_big4_cn 比较当前名称规则识别的两类机构;名称规则误分、遗漏变量与公司选择都会影响估计。
- 点估计必须与公司聚类标准误、95% 置信区间和样本定义一起读;
p 值不是原假设为真的概率。
问题: “对数几率”不够直观。我们需要边际效应!
步骤 4: 计算并解释平均边际效应 (AME)
我们可以使用 .get_margeff() 方法来计算边际效应,默认计算的是AME。
代码
# 计算连续变量平均导数,并把二元变量按 0→1 平均离散变化处理。
marginal_effects = result.get_margeff(at='overall', method='dydx', dummy=True)
# 提取 statsmodels 原始命名表,保留所有六项统计量供逐列核对。
ame_source = marginal_effects.summary_frame()
# 显式声明统计量映射,避免按位置错把 z 当作 p 值或丢失区间上界。
ame_column_map = {'dy/dx': 'AME', 'Std. Err.': '聚类稳健标准误', 'z': 'z值', 'Pr(>|z|)': 'p值', 'Conf. Int. Low': '95%CI下界', 'Cont. Int. Hi.': '95%CI上界'}
# 在展示前检查当前 statsmodels 版本确实提供事先确定的六列。
assert set(ame_column_map).issubset(ame_source.columns), f'AME列名变化: {ame_source.columns.tolist()}'
# 按明确列名选择并重命名变量级 AME 推断表。
ame_table = ame_source[list(ame_column_map)].rename(columns=ame_column_map)
# 展示与当前审计意见模型同源的动态 AME 证据。
display(ame_table.round(6))
| report_delay_days |
-0.000065 |
0.000008 |
-8.181934 |
0.0 |
-0.000080 |
-0.000049 |
| fiscal_year |
0.003282 |
0.000447 |
7.333797 |
0.0 |
0.002405 |
0.004159 |
| is_big4_cn |
-0.036998 |
0.003062 |
-12.083588 |
0.0 |
-0.042999 |
-0.030997 |
AME 仍是观察性关联,不是决策效应
report_delay_days 的 dy/dx 是披露滞后增加 1 天时,当前样本预测概率变化的平均局部斜率。
fiscal_year 的 dy/dx 是当前线性时间规格下的一年差异,不代表制度变化的因果效应。
is_big4_cn 是二元变量;dummy=True 使表中数值成为逐样本从 0 改为 1 后预测概率差的平均值,而不是把二元值当连续量求导。
- 每项都必须同时读置信区间;跨公司依赖虽由聚类标准误处理,模型错设、选择与遗漏变量仍可能改变结论。
从同期解释转向预测必须重建数据说明
- 当前证据:
report_delay_days 与 modified_opinion 来自同一披露记录,只支持同期关联。
- 预测重建:把决策日前可得的历史特征,对齐到未来财年标签。
- 验证结构:按财年划分训练、验证与一次性最终测试,不得随机打散公司—年度面板。
- 缩放规则:距离、梯度或惩罚模型的缩放统计量只能在每个训练折拟合。
- 解释限制:标准化系数不能单独代表预测重要性、统计显著性或因果效应。
形成性检查:一个系数有四种不同问题
独立作答:标准化 Logit 中 \(|w_1|>|w_2|\) 能否推出变量 1 的样本外预测贡献更大、p 值更小且具有更强因果效应?
形成性检查反馈:系数只回答当前规格的关联
- 预测贡献:用样本外消融或置换检验。
- 统计显著性:查看标准误、置信区间与相应检验。
- 因果效应:需要可信的识别设计。
- 额外边界:相关特征下系数还可能不稳定。
三项都不能由系数绝对值自动推出;答“都能”者返回 相关内容。
开始预测前要回答的三个问题
- 预测时点:先写决策日,再证明每个特征在该日可得;同一披露记录的滞后不能预测该记录的意见。
- 时间外切分:较早财年训练、后续财年验证、最后年份最终测试;同一公司跨年依赖不得被称为独立横截面。
- 评价与基线:类别不平衡时至少报告多数类基线、balanced accuracy、正类召回、固定标签顺序混淆矩阵与校准;最终测试期只打开一次。
超越二分类:多分类问题
我们的贷款违约预测是一个典型的二分类 (Binary Classification) 问题。但在现实中,我们经常遇到需要分到两个以上类别的问题。
- 例子: 一家风投机构预测初创公司的最终命运:1) IPO上市, 2) 被收购, 3) 破产。这三个结果是互相排斥的。
- 解决方案: 使用Softmax 回归。
Softmax 回归:逻辑回归的泛化形式
Softmax 回归是逻辑回归在处理K个互斥类别时的推广。
每个类别概率如何计算
\[p_k=\frac{e^{z_k}}{\sum_{j=1}^{K}e^{z_j}},\qquad \sum_{k=1}^{K}p_k=1\]
- Softmax 对 \(K\) 个有限 logits 做指数归一化,得到非负且总和为 1 的向量。
- 数值实现应先减去最大 logit 以提高稳定性;概率是否校准仍须在独立验证集检查。
当类别数 K=2 时,Softmax 回归就退化为标准的逻辑回归。
形成性检查:0.5 阈值一定最优吗?
- 漏报成本(FN):20 万元。
- 误报成本(FP):2 万元。
- 预测概率:
[0.15, 0.35, 0.55, 0.80]。
- 真实标签:
[0, 1, 0, 1]。
- 任务:比较阈值 0.5 与 0.3 的混淆结果和总成本,并说明为何 Accuracy 不能单独决定阈值。
参考答案:以决策成本选择阈值
- 阈值 0.5 的预测为
[0, 0, 1, 1]:1 个漏报、1 个误报,总成本为 \(20+2=22\) 万元。
- 阈值 0.3 的预测为
[0, 1, 1, 1]:0 个漏报、1 个误报,总成本为 2 万元。
- Accuracy:阈值 0.5 为 \(2/4=50\%\);阈值 0.3 为 \(3/4=75\%\)。
- 成本差:两者相差 20 万元;本例低成本阈值恰好也有更高 Accuracy。
- 选择依据:事先写明的业务损失函数,而不是事后最大化 Accuracy。
- 数据纪律:阈值在训练/验证阶段确定,最终测试集只评估一次。
独立决策任务:成本阈值迁移
- 新成本:漏报 5 万元,误报 3 万元。
- 候选阈值:0.3、0.5、0.7。
- 计算提交:三组混淆矩阵、总成本与 Accuracy。
- 方法提交:阈值确定时点,以及概率失准时不能直接解释风险的原因。
评分标准:混淆矩阵 30%、成本计算 30%、不使用测试集调阈值 20%、概率校准边界 20%。
提交暂停点:先交计算再看答案
在纸面或学习平台提交三行比较表和一句阈值建议;缺少任一混淆矩阵或把最终测试用于选阈值,均须返回重做。完成提交前不要进入下一页。
独立决策任务反馈
| 0.3 |
1 |
1 |
0 |
2 |
\(1\times3+0\times5=3\) |
0.75 |
| 0.5 |
1 |
1 |
1 |
1 |
\(1\times3+1\times5=8\) |
0.50 |
| 0.7 |
2 |
0 |
1 |
1 |
\(0\times3+1\times5=5\) |
0.75 |
- 本例选择:四条固定验证观测上,阈值 0.3 的成本最低。
- 选择时点:只能在训练/验证阶段完成;确定阈值后,最终测试集只打开一次。
- 校准条件:在独立验证数据上检查校准曲线或 Brier score。
- 解释边界:概率失准时,阈值对应的风险含义也会失真。
阶段小结:本章核心要点
核心概念
- 逻辑函数: 将线性输出映射为
(0, 1) 区间的概率。
- 代价函数: 基于最大似然估计推导出的对数损失。
- 优化方法: 使用梯度下降法找到最优参数。
模型解读
- 决策边界: \(w^T x = 0\) 是一个超平面,用于划分预测类别。
- 边际效应: 提供了比原始系数更直观的商业解释。
Python 实现
statsmodels: 专注于推断和解释。
scikit-learn: 专注于预测和性能。
公开数据练习:首次披露记录的时间迁移诊断
代码
from pathlib import Path
from urllib.request import urlretrieve # 复用本章已安装的浏览器标识下载器
import pandas as pd # 构造公司年度审计分类样本
from sklearn.compose import ColumnTransformer # 对审计机构做折内独热编码
from sklearn.linear_model import LogisticRegression # 建立逻辑回归分类器
from sklearn.metrics import balanced_accuracy_score # 评估不平衡标签分类
from sklearn.pipeline import make_pipeline # 绑定预处理与模型避免泄漏
from sklearn.preprocessing import OneHotEncoder # 编码训练期出现的审计机构
# 按 Linux 共享数据、Windows 共享数据、项目缓存的顺序选择审计意见文件。
opinion_path = next((candidate_path for candidate_path in [Path('/home/ubuntu/r2_data_mount/data/stock/audit_opinion.h5'), Path('C:/qiufei/data/stock/audit_opinion.h5'), Path('data/course/audit_opinion.h5')] if candidate_path.exists()), Path('data/course/audit_opinion.h5'))
if not opinion_path.exists():
opinion_path.parent.mkdir(parents=True, exist_ok=True)
urlretrieve('https://assets.qiufei.site/data/stock/audit_opinion.h5', opinion_path)
opinion_data = pd.read_hdf(opinion_path, key='audit_opinion') # 读取小型 Fixed HDF5 表
opinion_data = opinion_data.query("type == 'financial_statements' and quarter.str.endswith('q4') and '2014q4' <= quarter <= '2023q4'", engine='python').copy() # 限定教学期年度财报审计
opinion_data['info_date'] = pd.to_datetime(opinion_data['info_date']) # 统一实际披露时点以建立版本与时间迁移边界
opinion_data['year'] = opinion_data['quarter'].str[:4].astype(int) # 提取目标财年用于时间切分
opinion_duplicate_keys = int(opinion_data.groupby(['order_book_id', 'quarter']).size().gt(1).sum()) # 统计存在重发的公司财年键
opinion_version_rows = len(opinion_data) # 保存去重前版本行数以报告筛选流量
opinion_first_release = opinion_data.sort_values(['order_book_id', 'quarter', 'info_date']).drop_duplicates(['order_book_id', 'quarter'], keep='first').copy() # 每键只保留首次可得版本
assert not opinion_first_release.duplicated(['order_book_id', 'quarter']).any(), '首次披露记录键不唯一' # 防止重发版本重复加权
opinion_data = opinion_first_release.copy() # 使用教学期内首次披露数据版本而不回填后续版本
opinion_data['modified'] = opinion_data['opinion_type'].ne('unqualified').astype(int) # 构造非标准意见标签
training_opinions = opinion_data.query("info_date < '2022-01-01'").dropna(subset=['audit_agency']).copy() # 只用2022年前已首次披露记录拟合
testing_opinions = opinion_data.query("info_date >= '2022-01-01'").dropna(subset=['audit_agency']).copy() # 后续首次披露记录只作时间迁移评价
assert training_opinions['info_date'].max() < testing_opinions['info_date'].min(), '披露时点训练与评价窗口重叠' # 强制实际信息时点有序
agency_encoder = ColumnTransformer([('agency', OneHotEncoder(handle_unknown='ignore'), ['audit_agency'])]) # 只在训练期拟合机构词表
local_logit = make_pipeline(agency_encoder, LogisticRegression(max_iter=500, class_weight='balanced', random_state=42)).fit(training_opinions[['audit_agency']], training_opinions['modified']) # 拟合折内分类流程
testing_labels = local_logit.predict(testing_opinions[['audit_agency']]) # 生成时间外分类结果
print({'文件': opinion_path.name, '重复公司财年键': opinion_duplicate_keys, '移除后续版本': opinion_version_rows - len(opinion_data), '训练首次披露': len(training_opinions), '后续首次披露': len(testing_opinions), '平衡准确率': round(balanced_accuracy_score(testing_opinions['modified'], testing_labels), 4)}) # 输出点时流量与迁移指标
本章小结
- 能把 logit、概率、阈值与决策成本分开,并条件性解释标准化系数。
- 缩放需求由估计器决定;惩罚模型的缩放统计量只在训练窗拟合。
- 系数大小不单独代表重要性、显著性或因果效应。
- 下一章进入非线性方法,用同一开发—最终测试说明比较候选。