04 逻辑回归:从分类问题到概率预测

本章会用到的数据

  • 审计意见数据(约 7.8 MB):用于练习非标准审计意见分类。
  • 代码会在首次运行时把文件下载到 data/course/,以后直接读取已下载的副本。
  • 我们按披露时间先后划分训练数据与最终测试数据,避免在练习中提前看到后来的信息。

【课堂核心】3 学时学习安排

学习内容 分钟
动机与先修 20
概率、对数几率与损失 45
系数与标准化边界 30
形成性检查与反馈 20
独立决策任务 25
独立任务反馈 10
公开数据练习 20
小结 10

【可选拓展】:未列入本表的历史信用数据、完整边际效应推导与 Softmax 细节安排课后。

章节概览:逻辑回归

从分类标签走向可评估的概率

  • 建模对象:预测事件发生的概率,而不只给出类别。
  • 决策依据:把概率与误报、漏报成本连接起来。
  • 解释边界:区分同期关联、因果效应与时间外预测。

本章学习路线图

完成本章后,学生能够:

  • 写出概率、对数几率与系数的对应关系;
  • 在给定成本矩阵下选择分类阈值;
  • 区分效应量、置信区间与 p 值;
  • 用最终测试集比较概率模型与多数类基准。
本章学习路线图 一个包含六个节点的水平流程图,展示了本章的学习路径,每个节点下有简短的描述。 1 问题设定 为何需要它? 2 模型形式 数学表达 3 模型训练 如何训练? 4 模型解读 如何解释? 5 代码实践 如何实现? 6 模型拓展 如何扩展?

Part 1: 问题设定

为何线性回归不足以应对分类任务?

案例引入:银行的核心风控业务

  • 银行的核心功能: 向个人和企业发放贷款是现代银行体系的基石。
  • 风险与收益的权衡: 在审批贷款时,银行面临一个核心挑战——如何准确预测申请人的还款能力?
  • 数据驱动决策: 在金融科技时代,我们不再依赖主观判断,而是利用机器学习模型,通过分析申请人的数据来创建一套精准的贷款违约预测系统。

商业问题可视化

银行的每日决策:对于每一位贷款申请人,我们应该批准(Approve)还是拒绝(Reject)?

银行贷款决策流程示意图 一个申请人数据进入银行决策系统,系统最终输出批准或拒绝的决策。 申请人数据 (收入, 负债, 年龄...) 银行决策系统 批准贷款 拒绝贷款

我们的任务:构建一个贷款违约预测模型

我们将利用一个包含已审批贷款表现的数据集来构建模型。

特征 (X) 描述 对违约的影响(直觉)
年收入 (Income) 申请人的年度总收入 越高,违约概率越低
负债收入比 (DTI) 每月债务支出占总收入的比例 越高,违约概率越高
工作年限 (Emp. Year) 申请人在当前工作的年限 越长,违约概率越低

目标变量 (y): 贷款是否违约。这是一个二元变量(Binary Variable)。

  • y = 1: 贷款已违约 (正类, Positive Class)
  • y = 0: 贷款未违约 (负类, Negative Class)

模型的核心任务是学习特征与结果之间的关系

我们的机器学习模型就像一个函数 \(f\),它的任务是学习输入(申请人的特征 \(\mathbf{x}\))与输出(是否违约 \(y\))之间的映射关系。

机器学习模型作为函数映射的示意图 展示了特征向量X通过机器学习模型f,映射到预测概率P(y=1)的过程。 输入特征 (申请人数据) x 机器学习模型 f(x) 输出 (预测概率) P(y=1|x)

模型一旦训练完成,就可以输出一个介于0和1之间的概率值。

一个自然的问题:为什么不用线性回归?

我们已经很熟悉线性回归了,它预测的是一个连续的数值。我们能否用它来预测一个01的分类问题呢?

让我们用负债收入比来预测是否违约

代码
# 为“一个自然的问题:为什么不用线性回归?”导入 `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()
横轴为负债收入比、纵轴为 0/1 违约状态及 OLS 预测;直线越过 0 到 1 的概率边界,显示 OLS 不适合直接输出分类概率。
图 1: 线性回归(OLS)直接应用于0/1分类问题的缺陷

缺陷 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\) 推断;应报告异方差稳健标准误,并说明采用大样本渐近推断。
二元结果的非正态误差散点沿y=1与y=0对应的两条误差比较结果分布,显示二元结果误差不会形成连续正态云团。 二元结果的误差分布 Error Distribution for Binary Outcomes 误差 (Error, ε) 预测概率 (Predicted Probability, p) 1.0 0.0 -1.0 0.0 1.0 当真实值 y = 1 ε = 1 - p 当真实值 y = 0 ε = 0 - p

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}}}} \]

这是一个两步过程:

  1. 线性部分: 计算一个得分 \(z = \mathbf{w}^T\mathbf{x}\)
  2. 非线性转换: 用逻辑函数 \(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、纵轴为 g(z);S 形曲线穿过 (0,0.5),并在两端逼近 0 与 1。
图 2: 逻辑 (Sigmoid) 函数的S型曲线

逻辑函数的关键特性

逻辑函数的三个关键特性 三个并排的卡片,分别通过图标和文字描述了逻辑函数的值域、单调性和中心对称性。 0,1 值域 (0, 1) Bernoulli 模型下的概率分数 单调递增 保留线性关系的方向 z=0 p=.5 中心对称 当 z=0 时,概率=0.5

案例图解:逻辑函数如何“弯曲”线性关系

我们来看一个具体的例子,假设经过模型训练,我们得到的线性关系是 \(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()
横轴为特征 x、纵轴同时画线性得分与逻辑概率;Sigmoid 将无界直线压缩到 0 至 1,0.5 处标出分类阈值。
图 3: 线性函数通过逻辑函数转换为概率预测

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: 从最大化似然到最小化代价

在机器学习中,我们习惯于将问题表述为最小化一个代价函数。

  1. 对数似然 \(l(\mathbf{w})\) 越大,训练数据在模型下越相容。
  2. 改变符号,把最大化 \(l(\mathbf{w})\) 写成最小化 \(-l(\mathbf{w})\)
  3. 再除以样本量 \(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=1时的对数损失函数曲线 一条从左上角急剧下降到右下角的曲线,表示预测概率接近1时损失趋近于0,接近0时损失趋近于无穷大。 p = f(x) 损失 1 0 Cost = -log(p) 预测错误,巨大惩罚 预测正确,损失小

图解损失函数:当真实 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\)
当y=0时的对数损失函数曲线 一条从左下角急剧上升到右上角的曲线,表示预测概率接近0时损失趋近于0,接近1时损失趋近于无穷大。 p = f(x) 损失 1 0 Cost = -log(1 - p) 预测错误,巨大惩罚 预测正确,损失小

结论: 这个损失函数的设计非常合理:预测越准确,损失越小;预测越离谱,损失就越大

最小化代价函数:梯度下降法

  • 目标地形:代价函数 \(J(\mathbf{w})\) 定义需要寻找的“谷底”。
  • 局部方向:梯度指出当前参数处上升最快的方向。
  • 更新策略:沿负梯度迭代移动,即梯度下降法(Gradient Descent)。
梯度下降法示意图 一个表示代价函数的二维曲线,一个点从高处沿着曲线的切线方向逐步移动到最低点。 参数 w 代价 J(w) 初始点 最优点

核心思想: 在当前位置,寻找最陡峭的下坡方向,然后迈出一步。重复此过程。

梯度下降的更新规则

  • “最陡峭的下坡方向”是代价函数梯度的负方向 \((-\nabla J(\mathbf{w}))\)
  • “迈出一步”的大小由学习率 (Learning Rate) \(\alpha\) 控制。

对于每一个权重 \(w_j\),同步更新:

\[ \large{w_j := w_j - \alpha \frac{\partial}{\partial w_j} J(\mathbf{w})} \]

这里的核心是计算代价函数对每个权重的偏导数(梯度)。

学习率 \(\alpha\) 的重要性

学习率决定了我们“下山”的步子迈多大。选择合适的学习率至关重要。

学习率对梯度更新路径的影响三条带箭头路径比较过小、合适和过大的学习率:步长过小收敛慢,合适步长接近最优点,过大步长越过目标并振荡。 不同学习率对梯度下降的影响 最优解 (Optimum) 起点 (Start) α 小:收敛慢 α 大:震荡或发散 α 适中:高效收敛

逻辑回归代价函数的梯度

经过微积分推导,我们可以得到一个非常简洁和优美的结果:

\[ \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\) 个特征值。

这个梯度的形式和我们在线性回归中学到的几乎完全一样!

逻辑回归的梯度下降完整算法

  1. 初始化: 随机初始化权重向量 \(\mathbf{w}\) (例如,全零向量)。

  2. 迭代更新: 重复以下步骤直到收敛:

    对于 \(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\))

  3. 收敛: 当 \(\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()
横纵轴为两个特征,背景色表示预测概率区域;0.5 等高线形成线性边界,散点颜色为真实类别。
图 4: 一个二维特征空间中的线性决策边界

解读模型:系数的意义

逻辑回归不仅能做预测,它强大的解释性也是其在经济金融领域广受欢迎的重要原因。

  • 系数的符号: 在其余变量固定、当前模型规格不变时,若 \(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\) 的值)。

Logit边际效应随概率变化S形概率曲线在中部最陡、两端平缓,三条切线显示同一系数的边际效应取决于当前预测概率。 p 线性组合 z 0 0.5 1.0 同一 wⱼ 下 Ap≈.05 → ME≈.045 Bp=.50 → .25(峰值) Cp≈.95 → ME≈.045 中部最陡,两端平缓

如何汇报一个统一的边际效应?

因为边际效应因人而异,我们通常汇报两种汇总统计量:

  1. 均值处的边际效应 (MEM): 先计算所有特征的均值 \(\bar{\mathbf{x}}\),然后计算在这一点上的边际效应。
  2. 平均边际效应 (AME): 为每一个样本点计算其边际效应,然后取所有边际效应的平均值。

AME (Average Marginal Effect) 通常被认为是更稳健和有代表性的度量。

Part 5: Python 实践

使用 statsmodelsscikit-learn

两种工具,两种哲学

statsmodels scikit-learn
主要目标 推断 (Inference) 预测 (Prediction)
核心优势 详细的统计摘要、p值、置信区间、边际效应 统一的API、丰富的模型库、完整的ML步骤
适用场景 学术研究、经济分析、需要解释变量影响的商业报告 实际预测、模型比较、较复杂的机器学习流程
一句话总结 “我想理解数据背后的关系” “我想对新数据做出最准确的预测”

statsmodels 实践:解释中国上市公司非标准审计意见

statsmodels 是 Python 中进行统计建模和计量经济学分析的首选库。它的设计哲学是“解释”和“推断”。

我们的步骤:

  1. 导入库并加载数据。
  2. 构建并拟合 Logit 模型。
  3. 解读模型摘要报告。
  4. 计算并解释边际效应。

步骤 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: 特征数据 X (前5行)
const report_delay_days fiscal_year is_big4_cn
0 1.0 72 2014 1
4280 1.0 70 2015 1
8630 1.0 76 2016 1
13331 1.0 74 2017 1
18426 1.0 66 2018 1

步骤 1 (续): 目标预览

单独核对二分类目标,确认 1 只表示非标准审计意见。

代码
# 为“步骤 1 (续): 数据预览”,显示前几条记录以核对字段与取值。
y.head()
表 2: 目标数据 y (前5行)
0        0
4280     0
8630     0
13331    0
18426    0
Name: modified_opinion, dtype: int64

步骤 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))
coef cluster_std_err p_value ci_low ci_high
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_opinion1 表示非标准审计意见。
  • 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))
AME 聚类稳健标准误 z值 p值 95%CI下界 95%CI上界
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_daysdy/dx 是披露滞后增加 1 天时,当前样本预测概率变化的平均局部斜率。
  • fiscal_yeardy/dx 是当前线性时间规格下的一年差异,不代表制度变化的因果效应。
  • is_big4_cn 是二元变量;dummy=True 使表中数值成为逐样本从 0 改为 1 后预测概率差的平均值,而不是把二元值当连续量求导。
  • 每项都必须同时读置信区间;跨公司依赖虽由聚类标准误处理,模型错设、选择与遗漏变量仍可能改变结论。

从同期解释转向预测必须重建数据说明

  • 当前证据report_delay_daysmodified_opinion 来自同一披露记录,只支持同期关联。
  • 预测重建:把决策日前可得的历史特征,对齐到未来财年标签。
  • 验证结构:按财年划分训练、验证与一次性最终测试,不得随机打散公司—年度面板。
  • 缩放规则:距离、梯度或惩罚模型的缩放统计量只能在每个训练折拟合。
  • 解释限制:标准化系数不能单独代表预测重要性、统计显著性或因果效应。

形成性检查:一个系数有四种不同问题

独立作答:标准化 Logit 中 \(|w_1|>|w_2|\) 能否推出变量 1 的样本外预测贡献更大、p 值更小且具有更强因果效应?

形成性检查反馈:系数只回答当前规格的关联

  • 预测贡献:用样本外消融或置换检验。
  • 统计显著性:查看标准误、置信区间与相应检验。
  • 因果效应:需要可信的识别设计。
  • 额外边界:相关特征下系数还可能不稳定。

三项都不能由系数绝对值自动推出;答“都能”者返回 相关内容

开始预测前要回答的三个问题

  1. 预测时点:先写决策日,再证明每个特征在该日可得;同一披露记录的滞后不能预测该记录的意见。
  2. 时间外切分:较早财年训练、后续财年验证、最后年份最终测试;同一公司跨年依赖不得被称为独立横截面。
  3. 评价与基线:类别不平衡时至少报告多数类基线、balanced accuracy、正类召回、固定标签顺序混淆矩阵与校准;最终测试期只打开一次。

Part 6: 拓展与总结

超越二分类问题

超越二分类:多分类问题

我们的贷款违约预测是一个典型的二分类 (Binary Classification) 问题。但在现实中,我们经常遇到需要分到两个以上类别的问题。

  • 例子: 一家风投机构预测初创公司的最终命运:1) IPO上市, 2) 被收购, 3) 破产。这三个结果是互相排斥的。
  • 解决方案: 使用Softmax 回归

Softmax 回归:逻辑回归的泛化形式

Softmax 回归是逻辑回归在处理K个互斥类别时的推广。

Softmax多分类概率流程输入特征先产生各类别线性得分,再经指数归一化为总和等于1的类别概率,最大概率对应预测类别。 Softmax回归流程 (Softmax Regression Pipeline) 输入特征 x (d维向量) 得分1 (Score 1) z1 = w1ᵀx 得分2 (Score 2) z2 = w2ᵀx 得分3 (Score 3) z3 = w3ᵀx Softmax 函数 指数归一化 概率1 p1 概率2 p2 概率3 p3

每个类别概率如何计算

\[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%。

提交暂停点:先交计算再看答案

在纸面或学习平台提交三行比较表和一句阈值建议;缺少任一混淆矩阵或把最终测试用于选阈值,均须返回重做。完成提交前不要进入下一页。

独立决策任务反馈

阈值 TN FP FN TP 总成本(万元) Accuracy
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。
  • 解释边界:概率失准时,阈值对应的风险含义也会失真。

阶段小结:本章核心要点

核心概念

  1. 逻辑函数: 将线性输出映射为 (0, 1) 区间的概率。
  2. 代价函数: 基于最大似然估计推导出的对数损失
  3. 优化方法: 使用梯度下降法找到最优参数。

模型解读

  1. 决策边界: \(w^T x = 0\) 是一个超平面,用于划分预测类别。
  2. 边际效应: 提供了比原始系数更直观的商业解释。

Python 实现

  1. statsmodels: 专注于推断和解释
  2. scikit-learn: 专注于预测和性能

感谢聆听

Q & A

公开数据练习:首次披露记录的时间迁移诊断

代码
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)})  # 输出点时流量与迁移指标
表 3: 公开审计意见首次披露记录的时间迁移结果
{'文件': 'audit_opinion.h5', '重复公司财年键': 438, '移除后续版本': 440, '训练首次披露': 32562, '后续首次披露': 17890, '平衡准确率': 0.7182}

本章小结

  • 能把 logit、概率、阈值与决策成本分开,并条件性解释标准化系数。
  • 缩放需求由估计器决定;惩罚模型的缩放统计量只在训练窗拟合。
  • 系数大小不单独代表重要性、显著性或因果效应。
  • 下一章进入非线性方法,用同一开发—最终测试说明比较候选。