03 线性回归

本章会用到的数据

  • 公开下载公司基本信息线性回归练习数据

  • 这些数据能做什么:用上市年限、发行价和地区等变量理解线性回归。

  • 分析时注意:先说明要解释还是预测,再选择变量、样本和评价方法。

  • 判断模型:用较早数据拟合和选择模型,最后用较晚数据检查预测误差。

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

学习内容 分钟
动机与先修 20
线性模型与梯度 45
共线性与验证 35
形成性检查与反馈 20
独立任务 35
答案、验证与小结 25

【可选拓展】几何、代数与额外函数形式

未列入学习安排的几何图、完整代数推导与额外函数形式均安排课后;课堂只沿显式核心锚点运行。

引言:金融预测与线性回归

金融大数据时代,预测能力是核心竞争力。线性回归作为最基础、最强大的预测模型之一,是理解更复杂算法的基石。

从数据到预测的流程 一个流程图,展示了金融数据通过线性回归模型转化为预测趋势的过程。 金融大数据 历史股价、财报等 y=w₀+w₁x 线性回归模型 发现数据关系 预测 未来趋势

本章学习目标:理解金融预测的基石

  • 理解核心应用: 线性回归是预测股价、盈利和回报率等连续变量的基石。
核心应用:预测连续金融变量 一个图表展示了使用历史数据预测未来金融指标(如股价)的理念。 时间 金融指标 (股价, 盈利等) 历史数据 (已知) 未来预测 (未知) 当前时刻

本章学习目标:掌握模型结构 \(y = w_0 + w_1x\)

  • 掌握模型结构: 我们将深入剖析 \(y = w_0 + w_1x\) 的每一个组成部分。
线性回归模型结构解析 图示线性回归公式 y = w₀ + w₁x 的四个核心组成部分:目标变量、截距、斜率和特征变量。 y = w₀ + w₁ x 目标变量 (y) 要预测的值 (EPS) 截距 (w₀) 基础值 / 偏置项 斜率 (w₁) 特征的影响力 / 权重 特征变量 (x) 用于预测的指标 (股价)

本章学习目标:揭秘模型训练过程

  • 揭秘训练过程: 理解机器如何通过“梯度下降”方法自动“学习”最佳参数。
模型训练的迭代循环 一个循环流程图,展示了模型训练的三个核心步骤:预测、计算误差和更新参数。 1. 预测 f(x) = w₀+w₁x 2. 计算误差 J(w₀, w₁) 3. 更新参数 梯度下降 重复迭代直至收敛

本章学习目标:实现代码实战

  • 实现代码实战: 我们将使用 Python 的专业库 statsmodelsscikit-learn 解决真实的金融预测问题。
Python实战库:statsmodels与scikit-learn 一个图示,并列展示了statsmodels和scikit-learn两个库,并说明了它们在金融预测中的不同侧重点。 statsmodels 核心:统计推断 分析变量关系 检验经济学假设 + scikit-learn 核心:机器学习预测 完成预测练习 评估模型性能

核心问题:我们能否预测一家公司的盈利能力?

在金融市场,预期是影响价格的关键因素。公司季报或年报发布前,分析师会发布盈利预测。如果实际盈利不及预期,股价常会应声下跌。

盈利预期对股价的影响 一个时间线图,展示了财报发布后,实际盈利低于预期导致股价下跌的典型场景。 事件时间轴 分析师发布盈利预测 市场形成“预期” 公司发布财报 “现实”揭晓 股价走势 实际盈利 < 预期 ➡️ 股价下跌

传统预测方法及其局限

  • 传统方法: 分析师通过大量行业调研、公司访谈来做出预测,成本高昂。
传统预测流程及其高昂成本 一个流程图,展示了传统分析师通过调研和访谈进行预测,最终导致高昂的时间和人力成本。 行业调研 分析报告、数据 公司访谈 沟通、会议 成本高昂 耗时、主观、低效

我们的目标:完成盈利预测练习

  • 我们的目标: 利用机器学习,根据历史数据预测未来每股收益 (Earnings Per Share, EPS),并判断预测是否优于简单基准。
盈利预测练习流程 一个三步流程图,展示从历史数据输入,经过机器学习模型处理,到得到未来 EPS 预测的过程。 数据驱动的盈利预测 历史财务数据 (股价, 账面市值比, ROA等) 机器学习模型 (线性回归) 未来每股收益 (EPS) 预测值 ✓ 高效    ✓ 客观    ✓ 可扩展

数据是模型的燃料:目标变量 y

我们将使用公开的财务数据来构建模型。我们的目标是预测 每股收益 (y)

变量类型 具体指标 (示例) 数据来源 何时可知
目标 (y) 每股收益 (EPS) 公司季报 季报公告
目标变量Y:未来未知的每股收益 一个时间轴图,强调目标变量EPS是在财报公告后才可知的,因此是模型需要预测的未来信息。 每股收益 (EPS) 我们的预测目标 (y) 财务季末 (数据收集截止) 财报公告 目标 y 在此变为已知

数据是模型的燃料:特征变量 x

变量类型 具体指标 (示例) 数据来源 何时可知
特征 (x) 股价 (Price) 交易所 季报公告
特征 (x) 账面市值比 (Book-to-Market, B/M) 前期财报与预测时点市值 季报公告
特征 (x) 资产收益率 (ROA) 前期财报 季报公告
特征变量X:模型输入 一个图示,展示了股价、净市率和资产收益率等多个特征变量作为模型的输入。 股价 (Price) 来源:交易所实时数据 账面市值比 (B/M) 来源:前期财报 资产收益率 (ROA) 来源:前期财报 模型输入 (特征 x)

数据是模型的燃料:关键逻辑

关键逻辑: 我们必须使用在预测时刻点 已经可以获得 的信息 (特征 x) 来预测 未来未知 的信息 (目标 y)。

预测的关键时序逻辑 一个时间轴,清晰地展示了特征(x)必须在预测时刻之前可知,而目标(y)是在预测时刻之后才可知。 过去/现在 (信息已知) 特征 x (输入) 预测时刻点 未来 (信息未知) 目标 y (输出)

第一步:从最简单的模型开始——一元线性回归

现实世界是复杂的,但科学研究总是从最简单的模型开始。我们首先假设,只用一个特征来预测目标。

例如:我们假设公司的 股价 (x) 与其未来的 每股收益 (y) 之间存在线性关系。

一元线性回归:股价与每股收益关系图 一个带有回归线的散点图,展示了股价(x)和每股收益(y)之间的正相关线性关系。 股价 (x) 每股收益 (y) y = w₀ + w₁x

模型表达式与参数解读

我们的模型可以表示为一个简单的直线方程:

\[ \large{ \text{预测的每股收益} = f(x) = w_0 + w_1 \times \text{股价} } \]

  • \(x\): 特征变量 (股价)
  • \(w_0\): 截距项 (Intercept),在机器学习中也称为 偏置项 (Bias)
  • \(w_1\): 斜率 (Slope),代表股价每变动1单位,预测的EPS会变动多少。在机器学习中称为 权重 (Weight)
线性模型参数的几何意义 一个坐标系中的直线,清晰地标示出截距(w₀)和斜率(w₁)的含义。 股价 (x) 预测EPS (y) f(x) = w₀ + w₁x w₀ (截距) 当 x=0 时的y值 Δx = 1 Δy = w₁ w₁ (斜率) x每增加1,y的变化量

不同参数代表了对数据关系的不同“假设”

w₀w₁ 的不同取值,就构成了不同的直线,代表了我们对股价和EPS关系的不同理解。

左图 (\(w_1 > 0\)) 假设股价越高,未来盈利越强。右图 (\(w_1 < 0\)) 则相反。

代码
# 为“不同参数代表了对数据关系的不同“假设””导入 `numpy` 并绑定 `np`,用于执行当前任务的数组、数值或随机机制计算。
import numpy as np
# 为“不同参数代表了对数据关系的不同“假设””导入 `matplotlib.pyplot` 并绑定 `plt`,用于构建当前任务的坐标轴并呈现比较结果。
import matplotlib.pyplot as plt

# 设置清晰可区分的投影配色
# 固定 `primary_blue` 的图形配色,使“不同参数 w0 和 w1 定义了不同的线性模型”中的曲线或标记易于区分。
primary_blue = '#007bff'
# 固定 `accent_red` 的图形配色,使“不同参数 w0 和 w1 定义了不同的线性模型”中的曲线或标记易于区分。
accent_red = '#dc3545'
# 固定 `text_color` 的颜色编码,使“不同参数 w0 和 w1 定义了不同的线性模型”中的对象易于区分。
text_color = '#212529'
# 固定 `grid_color` 的颜色编码,使“不同参数 w0 和 w1 定义了不同的线性模型”中的对象易于区分。
grid_color = '#e0e0e0'
# 固定 `background_color` 的颜色编码,使“不同参数 w0 和 w1 定义了不同的线性模型”中的对象易于区分。
background_color = '#f8f9fa'


# 为“不同参数代表了对数据关系的不同“假设””,创建本图所需画布与坐标轴,并保存到 `(fig, (ax1, ax2))`。
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4), sharey=True)
# 用浅色底区分总览面板与右侧两个候选模型。
fig.set_facecolor(background_color)

# 模型一使用正斜率 w1 > 0
# 建立 `x1` 的有序取值网格,用于展示“不同参数 w0 和 w1 定义了不同的线性模型”随参数变化的比较结果。
x1 = np.linspace(0, 10, 100)
# 用第一组截距与斜率计算候选直线 A 的预测值。
y1 = 1.5 + 0.6 * x1
# 在左面板绘出正斜率候选线,展示参数符号如何改变预测方向。
ax1.plot(x1, y1, color=accent_red, linewidth=3, label='$f(x) = 1.5 + 0.6x$')
# 将图题设为“$w_1 > 0$ (正相关)”,直接说明当前图形的比较目的。
ax1.set_title('$w_1 > 0$ (正相关)', fontsize=16, color=text_color, pad=10)
# 将横轴标为“特征 (x)”,明确横向编码的变量。
ax1.set_xlabel('特征 (x)', color=text_color, fontsize=12)
# 将纵轴标为“预测值 (y)”,明确纵向编码的变量。
ax1.set_ylabel('预测值 (y)', color=text_color, fontsize=12)
# 为“不同参数代表了对数据关系的不同“假设””,限定横轴范围,使比较对象使用一致尺度。
ax1.set_xlim(0, 10)
# 为“不同参数代表了对数据关系的不同“假设””,限定纵轴范围,使比较对象使用一致尺度。
ax1.set_ylim(0, 10)
# 为“不同参数代表了对数据关系的不同“假设””添加辅助网格,便于比较位置、斜率或组间差异。
ax1.grid(True, linestyle='--', alpha=0.7, color=grid_color)
# 统一刻度文字样式,提升投影可读性。
ax1.tick_params(colors=text_color)
# 用白底突出候选模型 A 的散点与拟合线。
ax1.set_facecolor('white')
# 在图中标注“$f(x) = 1.5 + 0.6x$”,解释“不同参数 w0 和 w1 定义了不同的线性模型”的关键位置。
ax1.text(5, 9, '$f(x) = 1.5 + 0.6x$', ha='center', va='center', fontsize=14, color=accent_red)


# 模型二使用负斜率 w1 < 0
# 建立 `x2` 的有序取值网格,用于展示“不同参数 w0 和 w1 定义了不同的线性模型”随参数变化的比较结果。
x2 = np.linspace(0, 10, 100)
# 用第二组截距与斜率计算候选直线 B 的预测值。
y2 = 8 - 0.5 * x2
# 在右面板绘出负斜率候选线,与左侧模型使用同一坐标尺度比较。
ax2.plot(x2, y2, color=primary_blue, linewidth=3, label='$f(x) = 8.0 - 0.5x$')
# 将图题设为“$w_1 < 0$ (负相关)”,直接说明当前图形的比较目的。
ax2.set_title('$w_1 < 0$ (负相关)', fontsize=16, color=text_color, pad=10)
# 将横轴标为“特征 (x)”,明确横向编码的变量。
ax2.set_xlabel('特征 (x)', color=text_color, fontsize=12)
# 为“不同参数代表了对数据关系的不同“假设””,限定横轴范围,使比较对象使用一致尺度。
ax2.set_xlim(0, 10)
# 为“不同参数代表了对数据关系的不同“假设””,限定纵轴范围,使比较对象使用一致尺度。
ax2.set_ylim(0, 10)
# 为“不同参数代表了对数据关系的不同“假设””添加辅助网格,便于比较位置、斜率或组间差异。
ax2.grid(True, linestyle='--', alpha=0.7, color=grid_color)
# 统一刻度文字样式,提升投影可读性。
ax2.tick_params(colors=text_color)
# 用白底突出候选模型 B 的散点与拟合线。
ax2.set_facecolor('white')
# 在图中标注“$f(x) = 8.0 - 0.5x$”,解释“不同参数 w0 和 w1 定义了不同的线性模型”的关键位置。
ax2.text(5, 2, '$f(x) = 8.0 - 0.5x$', ha='center', va='center', fontsize=14, color=primary_blue)


# 为“不同参数代表了对数据关系的不同“假设””,压缩子图留白,避免标题和坐标标签相互遮挡。
plt.tight_layout(pad=2.0)
# 显示多组截距与斜率对应的回归直线,比较参数变化如何改变水平位置和倾斜方向。
plt.show()
同一组观测点上叠加多条截距和斜率不同的直线,展示 w0、w1 改变会同步改变预测位置。
图 1: 不同参数 w0 和 w1 定义了不同的线性模型

核心任务:从候选直线中找到总误差最小者,即完成模型训练。

如何定义“最佳”拟合?——最小化预测误差

假设我们有了一些真实数据点(蓝色x),我们尝试用两条不同的直线去拟合。哪一条更好?

直观上,左边的模型更好,因为它产生的 预测误差 (灰色虚线) 整体更小。

代码
# 为“如何定义“最佳”拟合?——最小化预测误差”导入 `numpy` 并绑定 `np`,用于执行当前任务的数组、数值或随机机制计算。
import numpy as np
# 为“如何定义“最佳”拟合?——最小化预测误差”导入 `matplotlib.pyplot` 并绑定 `plt`,用于构建当前任务的坐标轴并呈现比较结果。
import matplotlib.pyplot as plt

# 生成固定机制样本
# 为“如何定义“最佳”拟合?——最小化预测误差”,固定随机数序列,使课堂示例可重复。
np.random.seed(42)
# 在 [0,2] 区间抽取 20 个单特征观测,构成线性拟合的横坐标。
X = 2 * np.random.rand(20, 1) # 生成单特征输入
# 由截距 4、斜率 3 和单位高斯噪声生成回归目标 `y`。
y = 4 + 3 * X + np.random.randn(20, 1) # 在线性响应中加入随机扰动

# 固定 `x_range` 的数值分量,作为“如何定义“最佳”拟合?——最小化预测误差”的可手算输入。
x_range = np.array([[np.min(X)-0.1], [np.max(X)+0.1]])

# 设置一组较好拟合与一组较差拟合参数
# 取得参数 `w1_good, w0_good`,用于比较不同斜率或学习率对应的损失位置。
w1_good, w0_good = 3.1, 4.2 # 较好拟合的斜率与截距
# 取得参数 `w1_bad, w0_bad`,用于比较不同斜率或学习率对应的损失位置。
w1_bad, w0_bad = -0.5, 8.0  # 较差拟合的斜率与截距

# 在共同横轴网格上计算较好参数组的预测直线,作为残差较小的比较基准。
y_good_pred_line = w0_good + w1_good * x_range
# 在同一横轴网格上计算较差参数组的预测直线,用于展示较大残差。
y_bad_pred_line = w0_bad + w1_bad * x_range

# 设置统一的投影配色
# 固定 `data_point_color` 的颜色编码,使““最佳”模型是使预测值与真实值之间总差距最小的模型”中的对象易于区分。
data_point_color = '#007bff'
# 固定 `good_fit_line_color` 的颜色编码,使““最佳”模型是使预测值与真实值之间总差距最小的模型”中的对象易于区分。
good_fit_line_color = '#166534'
# 固定 `bad_fit_line_color` 的颜色编码,使““最佳”模型是使预测值与真实值之间总差距最小的模型”中的对象易于区分。
bad_fit_line_color = '#dc3545'
# 固定 `error_line_color` 的颜色编码,使““最佳”模型是使预测值与真实值之间总差距最小的模型”中的对象易于区分。
error_line_color = '#6c757d'
# 固定 `text_color` 的颜色编码,使““最佳”模型是使预测值与真实值之间总差距最小的模型”中的对象易于区分。
text_color = '#212529'
# 固定 `grid_color` 的颜色编码,使““最佳”模型是使预测值与真实值之间总差距最小的模型”中的对象易于区分。
grid_color = '#e0e0e0'
# 固定 `background_color` 的颜色编码,使““最佳”模型是使预测值与真实值之间总差距最小的模型”中的对象易于区分。
background_color = '#f8f9fa'


# 为“如何定义“最佳”拟合?——最小化预测误差”,创建本图所需画布与坐标轴,并保存到 `(fig, (ax1, ax2))`。
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 3.6), sharey=True)
# 用浅色底区分成本最小化总览与右侧误差分解面板。
fig.set_facecolor(background_color)

# 绘制较好拟合模型
# 以 `X` 为横轴、`y` 为纵轴绘制散点,展示““最佳”模型是使预测值与真实值之间总差距最小的模型”。
ax1.scatter(X, y, marker='x', color=data_point_color, s=80, label='真实数据点', zorder=5)
# 以 `x_range` 为横轴、`y_good_pred_line` 为纵轴绘制曲线,展示““最佳”模型是使预测值与真实值之间总差距最小的模型”。
ax1.plot(x_range, y_good_pred_line, "-", color=good_fit_line_color, linewidth=3, label='模型预测')
# 遍历 `range(len(X))` 的候选或观测,为“如何定义“最佳”拟合?——最小化预测误差”逐项更新结果。
for i in range(len(X)):
    # 为“如何定义“最佳”拟合?——最小化预测误差”,把投影连接线设为灰色虚线,并用 0.8 线宽弱化其视觉层级。
    ax1.plot([X[i], X[i]], [y[i], w0_good + w1_good * X[i]], '--', color=error_line_color, alpha=0.7)
# 将图题设为“模型 A (较好拟合)”,直接说明当前图形的比较目的。
ax1.set_title(f'模型 A (较好拟合)', fontsize=20, color=text_color, pad=10)
# 为“如何定义“最佳”拟合?——最小化预测误差”,将横轴标为“x”,明确横向编码的变量。
ax1.set_xlabel('x', color=text_color, fontsize=18)
# 为“如何定义“最佳”拟合?——最小化预测误差”,将纵轴标为“y”,明确纵向编码的变量。
ax1.set_ylabel('y', color=text_color, fontsize=18)
# 显示“如何定义“最佳”拟合?——最小化预测误差”图例,使颜色或线型与比较对象一一对应。
ax1.legend(fontsize=18, loc='upper left')
# 为“如何定义“最佳”拟合?——最小化预测误差”添加辅助网格,便于比较位置、斜率或组间差异。
ax1.grid(True, linestyle='--', alpha=0.7, color=grid_color)
# 统一刻度文字样式,提升投影可读性。
ax1.tick_params(colors=text_color, labelsize=18)
# 用白底突出候选斜率与观测点之间的残差。
ax1.set_facecolor('white')

# 绘制较差拟合模型
# 以 `X` 为横轴、`y` 为纵轴绘制散点,展示““最佳”模型是使预测值与真实值之间总差距最小的模型”。
ax2.scatter(X, y, marker='x', color=data_point_color, s=80, label='真实数据点', zorder=5)
# 以 `x_range` 为横轴、`y_bad_pred_line` 为纵轴绘制曲线,展示““最佳”模型是使预测值与真实值之间总差距最小的模型”。
ax2.plot(x_range, y_bad_pred_line, "-", color=bad_fit_line_color, linewidth=3, label='模型预测')
# 遍历 `range(len(X))` 的候选或观测,为“如何定义“最佳”拟合?——最小化预测误差”逐项更新结果。
for i in range(len(X)):
    # 为“如何定义“最佳”拟合?——最小化预测误差”,把投影连接线设为灰色虚线,并用 0.8 线宽弱化其视觉层级。
    ax2.plot([X[i], X[i]], [y[i], w0_bad + w1_bad * X[i]], '--', color=error_line_color, alpha=0.7)
# 将图题设为“模型 B (较差拟合)”,直接说明当前图形的比较目的。
ax2.set_title(f'模型 B (较差拟合)', fontsize=20, color=text_color, pad=10)
# 为“如何定义“最佳”拟合?——最小化预测误差”,将横轴标为“x”,明确横向编码的变量。
ax2.set_xlabel('x', color=text_color, fontsize=18)
# 显示“如何定义“最佳”拟合?——最小化预测误差”图例,使颜色或线型与比较对象一一对应。
ax2.legend(fontsize=18, loc='upper left')
# 为“如何定义“最佳”拟合?——最小化预测误差”添加辅助网格,便于比较位置、斜率或组间差异。
ax2.grid(True, linestyle='--', alpha=0.7, color=grid_color)
# 统一刻度文字样式,提升投影可读性。
ax2.tick_params(colors=text_color, labelsize=18)
# 用白底突出误差平方和随斜率变化的曲线。
ax2.set_facecolor('white')

# 为“如何定义“最佳”拟合?——最小化预测误差”,压缩子图留白,避免标题和坐标标签相互遮挡。
plt.tight_layout(pad=2.0)
# 显示好、差两条拟合线及残差线段,比较平方误差随参数选择的变化。
plt.show()
两个特征—目标散点分面叠加候选回归线;每个观测到拟合线的灰色竖向残差显示误差,左侧残差整体短于右侧,表示左侧拟合较好。
图 2: “最佳”模型是使预测值与真实值之间总差距最小的模型

我们需要一个数学工具来 量化 这个“整体误差”。这个工具就是 代价函数 (Cost Function)

用代价函数 \(J(w_0, w_1)\) 量化模型的总误差

代价函数 (Cost Function),也叫 损失函数 (Loss Function),衡量的是模型在所有数据点上总的预测误差。

对于线性回归,最常用的代价函数是 均方误差 (Mean Squared Error, MSE)

\[ \large{ J(w_0, w_1) = \frac{1}{2n} \sum_{i=1}^{n} \left( f(x^{(i)}) - y^{(i)} \right)^2 } \]

  • \(n\): 数据点的总数。
  • \(x^{(i)}, y^{(i)}\): 第 \(i\) 个数据点的特征和真实值。
  • \(f(x^{(i)})\):模型对第 \(i\) 个数据点的预测值,即 \(w_0 + w_1x^{(i)}\)

区分预测误差与传统残差

\[ \large{ J(w_0, w_1) = \frac{1}{2n} \sum_{i=1}^{n} \left( \color{#dc3545}{f(x^{(i)}) - y^{(i)}} \right)^2 } \]

  • \(\color{#dc3545}{(f(x^{(i)}) - y^{(i)})}\):这是本章梯度约定下的预测误差
  • 传统回归残差记为 \(e_i=y^{(i)}-f(x^{(i)})\);因此梯度表达式中的预测误差等于 \(-e_i\)
  • \(\color{#0057B8}{(\dots)^2}\):对误差求平方,使得正负误差不会相互抵消,并且对大误差的惩罚更重。
传统残差 e_i 与平方误差的可视化 散点图把竖直线段明确定义为传统残差 e_i=y_i-f(x_i);代价函数使用的 [f(x_i)-y_i]^2 与 e_i^2 数值相同。 真实值 y⁽ⁱ⁾ 预测值 f(x⁽ⁱ⁾) 传统残差 eᵢ = y⁽ⁱ⁾ − f(x⁽ⁱ⁾) 平方误差 [f(x⁽ⁱ⁾) − y⁽ⁱ⁾]²

求和与平均化:完成代价函数

\[ \large{ J(w_0, w_1) = \color{#0057B8}{\frac{1}{2n} \sum_{i=1}^{n}} \left( f(x^{(i)}) - y^{(i)} \right)^2 } \]

  • \(\color{#0057B8}{\sum_{i=1}^{n}}\):将所有数据点的平方误差加起来。
  • \(\color{#0057B8}{\frac{1}{2n}}\):求平均值,使代价函数的大小与数据量无关。分母中的2是为了后续求导计算方便。

我们的目标: 找到一组参数 \((w_0, w_1)\),使得代价函数 \(J(w_0, w_1)\) 的值 最小

代价函数的构建过程 一个流程图,展示了从各个数据点的平方误差,通过求和与平均化,最终得到总代价函数J的过程。 e₁² e₂² e₃² eₙ² 求和 Σ 平均 1/(2n) 总代价 J(w₀, w₁)

代价函数的可视化:一个二次型的碗

为了简化,我们暂时固定 \(w_0=0\),只看代价函数 \(J(w_1)\) 如何随 \(w_1\) 变化。它是一个开口向上的抛物线。

  • 左图: 不同的 \(w_1\) 值对应不同的拟合直线。
  • 右图: 每一条直线都对应代价函数 \(J(w_1)\) 上的一个点。
  • 最佳拟合: 左图中最贴合数据的直线,对应右图抛物线的 最低点
代码
import numpy as np  # 计算候选斜率对应的均方代价
import matplotlib.pyplot as plt  # 并排绘制拟合线与代价曲线
feature_values = np.array([-1.0, -0.75, -0.45, -0.15, 0.15, 0.45, 0.75, 1.0])  # 固定机制图的横轴取值
observed_targets = np.array([-3.1, -2.0, -1.5, -0.2, 0.7, 1.2, 2.4, 3.2])  # 固定含轻微偏差的目标值
slope_grid = np.linspace(-1, 7, 160)  # 枚举候选斜率以形成平滑代价曲线
cost_curve = np.array([np.mean((slope * feature_values - observed_targets) ** 2) / 2 for slope in slope_grid])  # 逐斜率计算半均方误差
best_slope = slope_grid[np.argmin(cost_curve)]  # 找出固定网格上的最低代价斜率
scenario_records = [(0.5, '斜率过小', '#963900'), (best_slope, '最低代价', '#166534'), (5.5, '斜率过大', '#174F87')]  # 固定三种对照情景
figure, plot_axes = plt.subplots(1, 2, figsize=(11, 3.0))  # 压缩图高并保留两幅大图的投影字号
plot_axes[0].scatter(feature_values, observed_targets, marker='x', s=90, color='#212529', label='固定观测')  # 展示同一组机制观测
plot_axes[1].plot(slope_grid, cost_curve, color='#166534', linewidth=3)  # 绘制斜率—代价曲线
for scenario_slope, scenario_label, scenario_color in scenario_records:  # 让三条拟合线与三个代价点使用一致颜色
    plot_axes[0].plot([-1, 1], [-scenario_slope, scenario_slope], color=scenario_color, linewidth=3, label=rf'{scenario_label}: $w_1$={scenario_slope:.2f}')  # 叠加候选拟合线
    scenario_cost = np.mean((scenario_slope * feature_values - observed_targets) ** 2) / 2  # 计算该候选线的代价
    plot_axes[1].scatter(scenario_slope, scenario_cost, s=100, color=scenario_color, edgecolor='black', zorder=3, label=scenario_label)  # 在代价曲线上标出对应位置
plot_axes[0].set(title='同一观测的三条候选拟合线', xlabel='特征 x', ylabel='目标 y', xlim=(-1.1, 1.1), ylim=(-6, 6))  # 固定左图标题、轴名与尺度
plot_axes[1].set(title=r'代价函数 $J(w_1)$', xlabel=r'候选斜率 $w_1$', ylabel=r'半均方误差 $J(w_1)$')  # 明确右图的参数与评价量
for plot_axis in plot_axes:  # 统一两幅图的投影可读样式
    plot_axis.grid(True, linestyle='--', alpha=0.45)  # 用浅网格辅助读取坐标
    plot_axis.tick_params(labelsize=15)  # 使缩放后的刻度文字仍不低于 18px
    plot_axis.title.set_fontsize(18)  # 使缩放后的子图标题保持投影可读
    plot_axis.xaxis.label.set_size(16)  # 使缩放后的横轴标题保持投影可读
    plot_axis.yaxis.label.set_size(16)  # 使缩放后的纵轴标题保持投影可读
    plot_axis.legend(fontsize=15, loc='best')  # 解释三种情景及其颜色对应关系
plt.tight_layout()  # 为标题、坐标轴和图例保留互不遮挡的边距
plt.show()  # 输出两幅并排机制图
左面板在固定特征—目标散点上比较斜率过小、最低代价与斜率过大的三条拟合线;右面板以候选斜率为横轴、半均方误差为纵轴,用同色点对应三条线,最低代价点位于碗形曲线谷底。
图 3: 模型拟合程度与代价函数值的关系

寻找最低点的方法:梯度下降 (Gradient Descent)

梯度下降是一种强大的优化算法,用于寻找函数的最小值。 它的核心目标是:

\[ \large{ \text{目标: } \min_{w_0, w_1} J(w_0, w_1) } \]

梯度下降:直观的山谷类比

核心思想: 想象你身处一个碗状的山谷中,想要走到谷底。最快的方法是什么?

  1. 环顾四周,找到 最陡峭的下坡方向
  2. 朝着这个方向 迈出一步
  3. 重复以上过程,直到你到达谷底(即无法再往下走)。

在数学上,“最陡峭的下坡方向”就是 负梯度 的方向。

梯度下降的山谷类比 一个代价函数的等高线图,将梯度下降的过程比作从山坡走向谷底,每一步都沿着最陡峭的下坡方向。 最小值 (谷底) 起点 最陡下坡方向 (- 梯度)

梯度下降的数学表达:迭代更新规则

梯度下降是一个迭代过程。我们从一个随机的 \((w_0, w_1)\) 开始,然后不断地更新它们,使 \(J(w_0, w_1)\) 越来越小。

算法步骤:

  1. 初始化: 随机选择一个起始点 \((w_0, w_1)\)
  2. 迭代更新: 重复以下步骤,直到收敛:

\[ \large{ w_0 := w_0 - \alpha \frac{\partial}{\partial w_0} J(w_0, w_1) } \]

\[ \large{ w_1 := w_1 - \alpha \frac{\partial}{\partial w_1} J(w_0, w_1) } \]

更新规则的组成部分

\[ \large{ w_j := w_j - \alpha \frac{\partial}{\partial w_j} J(w_0, w_1) } \]

  • \(:=\) 是赋值操作,表示用右边的值更新左边的变量。
  • \(\alpha\)学习率 (Learning Rate),它控制着我们每一步“走”多远。
  • \(\frac{\partial}{\partial w_j} J(w_0, w_1)\) 是代价函数对参数 \(w_j\)偏导数 (即梯度)。它告诉我们在当前点,代价函数在 \(w_j\) 方向上的“坡度”。

更新规则的三个组成部分(图解)

梯度下降更新规则的组成部分 一个图示,分解了梯度下降公式中的参数、学习率和梯度。 wⱼ := wⱼ - α ∂J/∂wⱼ 参数 wⱼ 要优化的模型系数 学习率 α 更新步长 (超参数) 梯度 ∂J/∂wⱼ 代价函数的“坡度”

学习率 \(\alpha\) 是决定成败的关键超参数

学习率 \(\alpha\) (Alpha) 是一个 超参数 (Hyperparameter),需要我们手动设定。它的选择至关重要:

  • 如果 \(\alpha\) 太小: 每次更新的步子太小,收敛速度会非常慢。
  • 如果 \(\alpha\) 太大: 每次更新的步子太大,可能会“跨过”最低点,导致在谷底两侧来回震荡,甚至无法收敛。
代码
# 为“学习率 $\alpha$ 是决定成败的关键超参数”导入 `numpy` 并绑定 `np`,用于执行当前任务的数组、数值或随机机制计算。
import numpy as np
# 为“学习率 $\alpha$ 是决定成败的关键超参数”导入 `matplotlib.pyplot` 并绑定 `plt`,用于构建当前任务的坐标轴并呈现比较结果。
import matplotlib.pyplot as plt

# 定义 `J`,计算一维参数的平方误差代价。
def J(w): return (w - 5)**2 + 3
# 定义 `dJ`,计算代价函数关于参数的导数。
def dJ(w): return 2 * (w - 5)

# 定义一维梯度下降比较结果计算器,用初值、学习率与迭代次数比较收敛路径。
def gradient_descent(w_start, alpha, n_iterations):
    # 用初始权重建立迭代比较结果列表,后续每轮追加更新值以比较学习率的收敛路径。
    w_path = [w_start]
    # 取得参数 `w`,用于比较不同斜率或学习率对应的损失位置。
    w = w_start
    # 遍历 `range(n_iterations)` 的候选或观测,为“学习率 $\alpha$ 是决定成败的关键超参数”逐项更新结果。
    for _ in range(n_iterations):
        # 取得参数 `w`,用于比较不同斜率或学习率对应的损失位置。
        w = w - alpha * dJ(w)
        # 把 `w` 加入 `w_path`,保留“学习率 $\alpha$ 是决定成败的关键超参数”本次循环的实际结果。
        w_path.append(w)
    # 返回 `np.array(w_path)`,把“学习率 $\alpha$ 是决定成败的关键超参数”的计算结果交给调用方。
    return np.array(w_path)

# 固定 `cost_plot_color` 的颜色编码,使“学习率 α 对梯度下降过程的影响”中的对象易于区分。
cost_plot_color = '#007bff'
# 固定 `path_color_good` 的颜色编码,使“学习率 α 对梯度下降过程的影响”中的对象易于区分。
path_color_good = '#166534'
# 固定 `path_color_bad` 的颜色编码,使“学习率 α 对梯度下降过程的影响”中的对象易于区分。
path_color_bad = '#dc3545'
# 固定 `text_color` 的颜色编码,使“学习率 α 对梯度下降过程的影响”中的对象易于区分。
text_color = '#212529'
# 固定 `grid_color` 的颜色编码,使“学习率 α 对梯度下降过程的影响”中的对象易于区分。
grid_color = '#e0e0e0'
# 固定 `background_color` 的颜色编码,使“学习率 α 对梯度下降过程的影响”中的对象易于区分。
background_color = '#f8f9fa'

# 建立 `w_range` 的有序取值网格,用于展示“学习率 α 对梯度下降过程的影响”随参数变化的比较结果。
w_range = np.linspace(0, 10, 100)
# 在代价函数值域内建立 `j_range` 网格,用作梯度下降比较结果图的纵轴背景。
j_range = J(w_range)

# 保存 `path_small_alpha` 的参数迭代比较结果,用于比较不同学习率的收敛速度与稳定性。
path_small_alpha = gradient_descent(w_start=0.5, alpha=0.1, n_iterations=15)
# 保存 `path_large_alpha` 的参数迭代比较结果,用于比较不同学习率的收敛速度与稳定性。
path_large_alpha = gradient_descent(w_start=0.5, alpha=0.98, n_iterations=15)


# 为“学习率 $\alpha$ 是决定成败的关键超参数”,创建本图所需画布与坐标轴,并保存到 `(fig, (ax1, ax2))`。
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 3.5))
# 用浅色底区分学习率总览与三个下降比较结果面板。
fig.set_facecolor(background_color)

# 绘制适中学习率的更新比较结果
# 以 `w_range` 为横轴、`j_range` 为纵轴绘制曲线,展示“学习率 α 对梯度下降过程的影响”。
ax1.plot(w_range, j_range, color=cost_plot_color, linewidth=2.5, zorder=1)
# 以 `path_small_alpha` 为横轴、`J(path_small_alpha)` 为纵轴绘制曲线,展示“学习率 α 对梯度下降过程的影响”。
ax1.plot(path_small_alpha, J(path_small_alpha), '-o', color=path_color_good, markersize=8, linewidth=2, label=r'学习路径', zorder=2)
# 将图题设为“合适的 α: 平稳收敛”,直接说明当前图形的比较目的。
ax1.set_title(r'合适的 α: 平稳收敛', fontsize=16, color=text_color, pad=10)
# 将横轴标为“$w_1$”,明确横向编码的变量。
ax1.set_xlabel('$w_1$', color=text_color, fontsize=16)
# 将纵轴标为“$J(w_1)$”,明确纵向编码的变量。
ax1.set_ylabel('$J(w_1)$', color=text_color, fontsize=16)
# 显示“学习率 $\alpha$ 是决定成败的关键超参数”图例,使颜色或线型与比较对象一一对应。
ax1.legend(fontsize=16)
# 为“学习率 $\alpha$ 是决定成败的关键超参数”添加辅助网格,便于比较位置、斜率或组间差异。
ax1.grid(True, linestyle='--', alpha=0.7, color=grid_color)
# 统一刻度文字样式,提升投影可读性。
ax1.tick_params(colors=text_color, labelsize=16)
# 用白底突出较小学习率的缓慢下降比较结果。
ax1.set_facecolor('white')

# 绘制过大学习率的更新比较结果
# 以 `w_range` 为横轴、`j_range` 为纵轴绘制曲线,展示“学习率 α 对梯度下降过程的影响”。
ax2.plot(w_range, j_range, color=cost_plot_color, linewidth=2.5, zorder=1)
# 以 `path_large_alpha` 为横轴、`J(path_large_alpha)` 为纵轴绘制曲线,展示“学习率 α 对梯度下降过程的影响”。
ax2.plot(path_large_alpha, J(path_large_alpha), '-o', color=path_color_bad, markersize=8, linewidth=2, label=r'学习路径', zorder=2)
# 将图题设为“过大的 α: 震荡或发散”,直接说明当前图形的比较目的。
ax2.set_title(r'过大的 α: 震荡或发散', fontsize=16, color=text_color, pad=10)
# 将横轴标为“$w_1$”,明确横向编码的变量。
ax2.set_xlabel('$w_1$', color=text_color, fontsize=16)
# 显示“学习率 $\alpha$ 是决定成败的关键超参数”图例,使颜色或线型与比较对象一一对应。
ax2.legend(fontsize=16)
# 为“学习率 $\alpha$ 是决定成败的关键超参数”添加辅助网格,便于比较位置、斜率或组间差异。
ax2.grid(True, linestyle='--', alpha=0.7, color=grid_color)
# 统一刻度文字样式,提升投影可读性。
ax2.tick_params(colors=text_color, labelsize=16)
# 用白底突出适中学习率的稳定收敛比较结果。
ax2.set_facecolor('white')

# 为“学习率 $\alpha$ 是决定成败的关键超参数”,压缩子图留白,避免标题和坐标标签相互遮挡。
plt.tight_layout(pad=2.0)
# 显示不同学习率的权重迭代比较结果,检查缓慢收敛、稳定收敛与发散三种诊断模式。
plt.show()
分面比较小、中、大三个学习率下参数沿损失曲线的更新比较结果;过小收敛慢,适中趋近谷底,过大可能跨越并发散。
图 4: 学习率 α 对梯度下降过程的影响

线性回归的梯度下降更新规则

我们将代价函数 \(J(w_0, w_1)\) 的公式代入梯度下降的更新规则中,通过求偏导数可以得到线性回归的具体更新步骤:

重复执行直到收敛:

\[ \large{ w_0 := w_0 - \alpha \frac{1}{n} \sum_{i=1}^{n} (f(x^{(i)}) - y^{(i)}) } \]

\[ \large{ w_1 := w_1 - \alpha \frac{1}{n} \sum_{i=1}^{n} (f(x^{(i)}) - y^{(i)}) \cdot x^{(i)} } \]

理解线性回归的梯度更新

\[ \large{ w_0 := w_0 - \alpha \frac{1}{n} \sum_{i=1}^{n} \color{#dc3545}{(f(x^{(i)}) - y^{(i)})} } \]

\[ \large{ w_1 := w_1 - \alpha \frac{1}{n} \sum_{i=1}^{n} \color{#dc3545}{(f(x^{(i)}) - y^{(i)})} \cdot x^{(i)} } \]

  • 更新 \(w_0\): 与所有数据点的 平均误差 成正比。
  • 更新 \(w_1\): 与所有数据点的 平均误差特征值 \(x^{(i)}\) 的乘积成正比。

重要说明: 这是一个 批量梯度下降 (Batch Gradient Descent) 算法,因为每次更新都使用了 全部 的训练数据。

扩展到多维:多元线性回归

现实中,只用一个特征(如股价)来预测盈利通常是不够的。我们需要使用多个特征来构建一个更强大的模型。

例如,我们可以同时使用 股价 (\(x_1\))净市率 (\(x_2\))资产收益率 (\(x_3\)) 来预测 每股收益 (y)

模型就从一条直线扩展为一个高维平面:

\[ \large{ f(x_1, x_2, x_3) = w_0 + w_1 x_1 + w_2 x_2 + w_3 x_3 } \]

这个模型被称为 多元线性回归 (Multiple Linear Regression)

多元回归:模型几何意义

当有两个特征时,线性回归模型 \(y = w_0 + w_1 x_1 + w_2 x_2\) 不再是一条直线,而是一个三维空间中的 平面。 当特征数量更多时,它是一个抽象的 超平面

多元线性回归的几何意义:拟合平面 一个三维坐标系,显示了数据点和一个拟合这些点的平面,代表了具有两个特征的多元线性回归模型。 x₁ (股价) x₂ (账面市值比 B/M) y (EPS) 拟合平面 f(x₁, x₂)

使用线性代数简化多元回归的表达

当特征数量增多时,写出一长串的公式会很繁琐。我们可以用线性代数中的向量和矩阵来优雅地表示它。

我们定义 特征向量 x参数向量 w:

\[ \large{ \mathbf{x} = \begin{bmatrix} x_0 \\ x_1 \\ x_2 \\ \vdots \\ x_m \end{bmatrix} \quad , \quad \mathbf{w} = \begin{bmatrix} w_0 \\ w_1 \\ w_2 \\ \vdots \\ w_m \end{bmatrix} } \]

其中 \(x_0\) 是一个常数项,我们通常设为 1,这样 \(w_0\) 就可以被包含进向量运算中。

向量点积表示模型函数

模型函数就可以简洁地表示为向量的 点积:

\[ \large{ f(\mathbf{x}) = w_0x_0 + w_1x_1 + \dots + w_mx_m = \mathbf{w}^T \mathbf{x} } \]

梯度下降的更新规则也同样适用于向量 w,我们对 w 中的每一个元素 \(w_j\) 进行同步更新。

用线性代数表示多元回归 图示权重向量w的转置与特征向量x的点积,得到预测值f(x)。 权重向量 wᵀ [ w₀ w₁ ... ] 特征向量 x [ x₀ x₁ ] = f(x)

多元回归的注意事项 (1): 特征缩放问题

  • 尺度差异:例如股价在 10–100,而账面市值比 B/M 在 0.5–2.0。
  • 几何后果:代价函数等高线变成狭长椭圆。
  • 优化后果:梯度沿“Z 字形”来回震荡,收敛缓慢。
未进行特征缩放导致的梯度下降问题 一个拉长的代价函数等高线图,展示了在特征尺度差异大时,梯度下降路径会呈Z字形,收敛缓慢。 代价函数 J(w₁, w₂) w₁ (股价) w₂ (B/M) 起点

解决方案:特征标准化 (Standardization)

训练窗口口径(与 StandardScaler 一致):对每个非恒定特征,用训练样本拟合

\[ \large{ z_{ij}=\frac{x_{ij}-\hat\mu_j}{\hat\sigma_{j,0}},\quad \hat\sigma_{j,0}=\sqrt{\frac{1}{n}\sum_{i=1}^{n}(x_{ij}-\hat\mu_j)^2} } \]

  • 训练样本:转换后均值为 0,按 ddof=0 计算的标准差为 1。
  • 验证/测试样本:只沿用训练期统计量,不保证自身恰为 0 和 1。
  • 形状边界:z-score 只是仿射平移与缩放,不会把偏态变成正态,也不会抹平原有形状。
特征标准化的效果 样本点前后对比图。训练窗口内,股价的五个对称样本点和账面市值比的四个重合低值加一个高值,分别经各自训练均值和按 ddof 等于 0 计算的标准差进行 z-score。转换后两组训练样本的均值都为 0、标准差都为 1,但对称与偏态的经验形状仍然不同;标准化不蕴含正态分布。 原始量纲各不相同 股价(元) B/M 各自拟合训练期位置与尺度 z-score 后同尺度 −2 0 2 均值 0、标准差 1;形状仍不同

多元回归的注意事项 (2): 多重共线性

多重共线性 (Multicollinearity) 指设计矩阵的列之间存在精确或近似线性依赖。高两两相关只是可能信号,并非必要条件。

  • 例如: 同时使用“公司总资产”和“公司净资产”作为特征。这两个变量很可能是高度相关的。
多重共线性示意图 一个散点图,展示了两个高度正相关的特征变量,数据点紧密地分布在一条直线上,说明了多重共线性的概念。 特征A (总资产) 特征B (净资产) 高度相关 (ρ ≈ 1)

多重共线性:诊断必须检查设计矩阵

问题:

  1. 使得模型参数的估计变得 不稳定。微小的数据变动可能导致系数估计值发生巨大变化。
  2. 难以解释单个特征的 独立贡献。我们分不清预测效果是来自特征A还是特征B。

诊断与处置

  1. 先依据领域知识与估计目标检查变量构造,再检查矩阵秩与条件数,必要时用 VIF 辅助定位。
  2. 推断任务应报告系数不稳定性与置信区间;预测任务可在开发期验证正则化或降维。
  3. 删除变量必须有领域或事先确定依据,不能把固定相关阈值当成自动规则。

反例:令 \(x_3=x_1+x_2\)\(x_1,x_2\) 相互独立。任一两两相关未必超过 0.8,但三列仍精确共线。

形成性检查:相关矩阵能否排除共线性?

独立作答:若所有两两相关都低于 0.8,能否断言没有共线性?先写出一个三变量反例,再决定要检查秩、条件数还是仅删一列。

形成性检查反馈:固定阈值不是删除规则

  • 结论:不能;\(x_3=x_1+x_2\) 就是反例。
  • 诊断:检查设计矩阵的秩与条件数,而不只看两两相关。
  • 处置:按任务和领域约束报告不稳定性、验证正则化,或有依据地删变量。
  • 订正:答成“低于 0.8 即安全”者返回上一页。

实践:用 Python 预测公司盈利 - statsmodels

理论讲完了,现在我们进入实战环节。我们将使用真实的A股财务数据来预测每股收益 (EPS)。

我们将介绍两个在Python中实现线性回归最核心的库:

  1. statsmodels:
    • 源于统计学和计量经济学。
    • 强项在于推断 (Inference): 提供详细的统计报告,如系数的t检验、p值、R²等,帮助我们理解变量之间的关系。
statsmodels库的核心功能 一个流程图,展示数据输入到statsmodels库,产出详细的统计报告,强调其在统计推断和解释方面的作用。 输入数据 (X, y) statsmodels 统计报告 (p-值, R², t-检验) 专注统计推断与解释

实践:用 Python 预测公司盈利 - scikit-learn

  1. scikit-learn:
    • 机器学习领域的标准库。
    • 强项在于预测 (Prediction): 提供统一、简洁的API,专注于模型的训练、预测和评估,是构建机器学习步骤的首选。
scikit-learn库的核心功能 一个流程图,展示数据经过预处理后输入scikit-learn库,产出预测值,强调其在机器学习预测方面的作用。 输入数据 (预处理, X_scaled) scikit-learn 预测值 (y_pred, 性能评估) 专注机器学习预测与评估

实战1:使用 statsmodels 进行回归分析

statsmodels 的流程更接近传统的统计软件,重在分析和解释。

  • 本页任务:用三个无经济身份的生成特征演示 OLS 输出结构。
  • 解释边界:这不是 A 股实证结果;bm_placeholder 也不是账面市值比。
  • 禁止外推:不得把预设关系解释为经验发现或因果效应。

真实点时 B/M 的构造

  • 分子info_date <= decision_date 的最后一版财报账面权益(总资产减总负债)。
  • 分母:同一决策日的公司市值。
  • 异常值:账面权益非正时记为缺失并单独报告。
  • 口径:B/M 与 P/B 互为倒数,不能混用。

OLS 摘要结构只演示推断字段

代码
import pandas as pd  # 组织机制演示数据
import statsmodels.api as sm  # 生成OLS估计与推断摘要
import numpy as np  # 使用固定随机生成机制

# 固定机制样本量与随机种子,构造已知线性关系供 statsmodels 推断演示。
np.random.seed(2024)  # 固定机制演示的随机状态
n_samples = 1000  # 设置足以观察回归输出结构的样本量
# 为 `generated_features` 建立包含 3 项的键值记录。
generated_features = {
    'price_placeholder': np.random.uniform(5, 50, n_samples),  # 生成不对应真实价格的占位特征
    'bm_placeholder': np.random.uniform(0.2, 2.5, n_samples),  # 生成不对应真实B/M的占位特征
    'profitability_placeholder': np.random.uniform(0.01, 0.15, n_samples)  # 生成不对应真实ROA的占位特征
}
df = pd.DataFrame(generated_features)  # 形成仅供机制演示的数据框
df['generated_target'] = 0.1 + 0.02 * df['price_placeholder'] + 0.03 * df['bm_placeholder'] + 0.8 * df['profitability_placeholder'] + np.random.normal(0, 0.5, n_samples)  # 按已知规则生成目标

# 从机制表中提取三项解释变量和响应变量,固定 OLS 设计矩阵口径。
features = ['price_placeholder', 'bm_placeholder', 'profitability_placeholder']  # 固定三个无经济身份的解释变量
y = df['generated_target']  # 指定机制演示目标
X = df[features]  # 构建回归设计矩阵

# 给解释变量矩阵添加常数列,使 statsmodels 显式估计截距。
X = sm.add_constant(X)  # 加入截距列以匹配生成方程

# 用机制样本估计 OLS 系数及模型假设下的标准误。
model = sm.OLS(y, X)  # 定义样本内OLS模型
result = model.fit()  # 估计系数与模型假设下的标准误

# 将系数、标准误、p 值与置信区间压成投影可读表,完整摘要留在折叠代码复现链中。
ols_inference = pd.concat([result.params.rename('coef'), result.bse.rename('std_err'), result.pvalues.rename('p_value'), result.conf_int().rename(columns={0: 'ci_low', 1: 'ci_high'})], axis=1)
# 展示足以支持后续三页解读的推断字段,避免默认输出产生内部滚动。
display(ols_inference.round(4))
coef std_err p_value ci_low ci_high
const 0.0404 0.0578 0.4843 -0.0730 0.1538
price_placeholder 0.0220 0.0013 0.0000 0.0196 0.0245
bm_placeholder 0.0243 0.0234 0.3000 -0.0217 0.0703
profitability_placeholder 1.1125 0.3978 0.0053 0.3320 1.8931
图 5: 使用 statsmodels 进行 OLS 回归

解读 statsmodels 的回归结果报告:coefstd err

这张表信息量巨大,是进行统计推断的核心。

关键指标 名称 意义 如何解读 (以 pps 为例)
coef 系数 特征每增加一个单位,生成目标的条件均值变化量。 price_placeholder 的估计系数为 0.0220;它只复现预设机制,不代表股价或 EPS。
std err 标准误 在模型假设下衡量系数估计的抽样不确定性。 price_placeholder 的标准误约为 0.0013,说明本次生成样本对该预设斜率估计较精确。

解读 statsmodels 的回归结果报告:tP>|t|

关键指标 名称 意义 如何解读 (以 pps 为例)
t t统计量 在原假设和模型假设成立时衡量估计值相对标准误的极端程度。t = coef / std err 绝对值较大只表示当前统计量与原假设较不相容;不能给“系数非零”赋后验概率。
P>|t| p 值 在原假设“系数为 0”及模型假设成立时,观察到当前或更极端 t 统计量的概率。 若报告值小于预先设定的显著性水平,可拒绝该原假设;它不是“系数为 0 的概率”,也不能单独证明因果。

限定性结论

  • price_placeholderprofitability_placeholder 在 5% 水平拒绝零系数原假设。
  • bm_placeholder 的置信区间跨越 0。
  • 结论只适用于这组教学生成数据和当前线性模型假设。
  • 结果只帮助阅读回归表,不构成中国市场预测有效性或因果证据。

解读 statsmodels 的回归结果报告:R-squared 与置信区间

关键指标 名称 意义 如何解读 (以 pps 为例)
R-squared 模型在当前生成样本中解释目标变异的比例。 本次 R² 约为 0.243,只描述预设机制与噪声共同形成的样本内拟合。
[0.025 0.975] 置信区间 若在相同条件下重复抽样并按同一程序构造区间,长期约95%的区间覆盖真实参数。 price_placeholder 的本次区间约为 [0.0196, 0.0245];频率学解释不把固定参数描述为有95%概率落入该区间。

实战2:使用 scikit-learn 进行回归分析

scikit-learn 的流程是为构建机器学习步骤设计的:预处理 -> 训练 -> 预测

查看 scikit-learn 线性回归拟合与文本输出代码
# 为“实战2:使用 `scikit-learn` 进行回归分析”导入 `pandas` 并绑定 `pd`,用于整理当前任务的表格、字段与时间索引。
import pandas as pd
from pathlib import Path
from urllib.request import urlretrieve  # 复用本章隐藏设置单元安装的浏览器标识下载器
# 为“实战2:使用 `scikit-learn` 进行回归分析”,导入 `StandardScaler`,用全部机制演示样本按特征列估计均值与尺度;本页不声称样本外验证。
from sklearn.preprocessing import StandardScaler
# 为“实战2:使用 `scikit-learn` 进行回归分析”,从 `sklearn.linear_model` 导入`LinearRegression` 用于拟合普通最小二乘线性回归。
from sklearn.linear_model import LinearRegression
# 为“实战2:使用 `scikit-learn` 进行回归分析”,从 `sklearn.metrics` 导入`mean_squared_error` 用于计算真实值与预测值的均方误差。
from sklearn.metrics import mean_squared_error

# 读取公开练习 CSV,并提取三项解释变量与响应变量供 sklearn 对照。
# 按 Linux 共享数据、Windows 共享数据、项目缓存的顺序选择回归练习文件。
ols_training_path = next((candidate_path for candidate_path in [Path('/home/ubuntu/r2_data_mount/data/course/ols_training.csv'), Path('C:/qiufei/data/course/ols_training.csv'), Path('data/course/ols_training.csv')] if candidate_path.exists()), Path('data/course/ols_training.csv'))
if not ols_training_path.exists():
    ols_training_path.parent.mkdir(parents=True, exist_ok=True)
    urlretrieve('https://assets.qiufei.site/data/course/ols_training.csv', ols_training_path)
df_sklearn = pd.read_csv(ols_training_path)
# 为“实战2:使用 `scikit-learn` 进行回归分析”,固定 `features` 的特征名称,使图表标签与模型输入列一一对应。
features = ['pps', 'bm', 'roa']
y = df_sklearn['eps_basic']  # 指定该示例的真实列名目标
X = df_sklearn[features]  # 仅从该示例数据框提取特征

# 创建缩放器,随后由全部机制演示样本逐特征估计均值与标准差。
scaler = StandardScaler()
# 用机制演示样本逐特征估计均值与标准差并转换;本段不作样本外评价。
X_scaled = scaler.fit_transform(X)

# 建立 sklearn 线性回归器,随后用标准化机制样本估计截距与系数。
model_sk = LinearRegression()
# 用全部机制样本的标准化特征估计 sklearn 线性回归系数,供与手工 OLS 结果对照。
model_sk.fit(X_scaled, y)

# 打印 sklearn 回归截距,开始与手工 OLS 参数逐项对照。
print(f'截距 (w0): {model_sk.intercept_:.4f}')
# 将各标准化特征与对应回归系数配对成可读表格。
coef_df = pd.DataFrame(model_sk.coef_, index=features, columns=['标准化系数'])
# 标出标准化特征的线性回归系数表。
print('特征系数 (w1, w2, ...):')
# 输出回归系数表,比较各财务变量与目标的样本内线性关联。
print(coef_df)

# 用已拟合模型生成机制样本预测数组,供计算同样本 MSE。
y_pred = model_sk.predict(X_scaled)
# 计算 `mse`(均方误差),量化“实战2:使用 scikit-learn 进行回归分析”的模型表现。
mse = mean_squared_error(y, y_pred)
# 报告“模型在训练集上的均方误差 (MSE): {mse:.4f}”中的 `mse`,评价“实战2:使用 scikit-learn 进行回归分析”的模型表现。
print(f'\n模型在训练集上的均方误差 (MSE): {mse:.4f}')
截距 (w0): 0.7756
特征系数 (w1, w2, ...):
        标准化系数
pps  0.275488
bm   0.016283
roa  0.043906

模型在训练集上的均方误差 (MSE): 0.2451

输出说明:本单元依次打印截距、三项标准化系数与训练样本 MSE;它是文本诊断输出,不生成散点图、拟合线或待预测位置。

sklearn 结果解读:截距与标准化系数

sklearn 直接给出了截距和系数,但没有统计检验报告。

  • 截距 (w₀): 0.7756。这是当所有特征都取其 均值(标准化后为 0)时,对 eps_basic 的样本内预测值。

  • pps 的系数: 0.2755注意:这里的解释不再是“股价每增加1元……”。

sklearn 结果解读:Beta系数的意义

  • 保持不变:其他特征固定。
  • 变化单位pps 增加一个训练样本标准差。
  • 预测变化:模型预测的 eps_basic 增加约 0.2755。

这种标准化系数可在同一线性规格内比较特征与预测的相对关联。在本次样本内拟合中,pps 的系数绝对值最大;这不是因果重要性,也尚未构成样本外增量证据。

总结:statsmodels vs. scikit-learn - 统计推断

特性 statsmodels scikit-learn
核心焦点 统计推断 机器学习预测 (次要)
主要产出 详细的统计摘要 训练好的模型对象 (次要)
显著性检验 提供 t-统计量, p-值 不直接提供
数据预处理 需要手动添加常数项 强调特征缩放 (可选,但推荐)
适用场景 学术研究, 经济分析, 解释变量关系 (次要)

结论: statsmodels 更适用于需要深入理解变量之间关系、进行假设检验和学术报告的场景。

总结:statsmodels vs. scikit-learn - 机器学习预测

特性 statsmodels scikit-learn
核心焦点 统计推断 (次要) 机器学习预测
主要产出 详细的统计摘要 (次要) 训练好的模型对象
显著性检验 (次要) 不直接提供
数据预处理 需要手动添加常数项 (次要) 强调特征缩放
适用场景 (次要) 构建预测系统, 机器学习流水线
  • 偏重预测scikit-learn 更适合构建标准预测流程。
  • 偏重解释与推断statsmodels 更便于查看统计检验与区间。
  • 选择原则:先判断问题要回答“预测”还是“解释”,再选工具。

练习1:手动计算梯度下降 - 问题

给定以下数据点和初始模型参数,请手动执行一步梯度下降。

数据:

x y
2.3 -1.2
4.5 0.0
6.7 3.4

模型: \(f(x) = w_0 + w_1x\) 初始参数: \(w_0 = 0, w_1 = 0.5\) 学习率: \(\alpha = 0.01\)

问题:

  1. 计算当前参数下的代价函数 \(J(w_0, w_1)\) 的值。
  2. 计算当前参数下 \(J\)\(w_0\)\(w_1\) 的梯度。
  3. 执行一步梯度下降,计算更新后的新参数值 \(w_0'\)\(w_1'\)

练习1:解题步骤 1 - 计算预测值和误差

1. 计算每个点的预测值和误差

x y \(f(x) = w_0 + w_1x\) (当前 \(w_0=0, w_1=0.5\)) 误差 \((f(x) - y)\)
2.3 -1.2 \(0 + 0.5 \times 2.3 = 1.15\) \(1.15 - (-1.2) = 2.35\)
4.5 0.0 \(0 + 0.5 \times 4.5 = 2.25\) \(2.25 - 0.0 = 2.25\)
6.7 3.4 \(0 + 0.5 \times 6.7 = 3.35\) \(3.35 - 3.4 = -0.05\)
练习1:数据点、模型与误差 一个坐标图,展示了练习题中的三个数据点,当前的回归线 f(x)=0.5x,以及每个数据点到回归线的误差。 x y f(x)=0.5x (2.3, -1.2) (4.5, 0.0) (6.7, 3.4)

练习1:解题步骤 2 - 计算代价函数 J

2. 计算代价函数 J(采用 \(1/(2n)\),使下一页的梯度表达式与题面一致)

\[ \large{ J = \frac{1}{2\times3} \sum (f(x) - y)^2 = \frac{1}{6} (2.35^2 + 2.25^2 + (-0.05)^2) } \]

\[ \large{ = \frac{10.5875}{6} \approx 1.765 } \]

练习1:解题步骤 3 - 计算梯度

3. 计算梯度

\[ \large{ \frac{\partial J}{\partial w_0} = \frac{1}{n} \sum (f(x) - y) = \frac{1}{3}(2.35 + 2.25 - 0.05) = \frac{4.55}{3} \approx 1.517 } \]

\[ \large{ \frac{\partial J}{\partial w_1} = \frac{1}{n} \sum (f(x) - y) \cdot x = \frac{1}{3}(2.35 \cdot 2.3 + 2.25 \cdot 4.5 - 0.05 \cdot 6.7) } \]

\[ \large{ = \frac{1}{3}(5.405 + 10.125 - 0.335) = \frac{15.195}{3} \approx 5.065 } \]

练习1:解题步骤 4 - 更新参数

4. 更新参数 (学习率 \(\alpha = 0.01\))

\[ \large{ w_0' = w_0 - \alpha \frac{\partial J}{\partial w_0} = 0 - 0.01 \cdot 1.517 = -0.01517 } \]

\[ \large{ w_1' = w_1 - \alpha \frac{\partial J}{\partial w_1} = 0.5 - 0.01 \cdot 5.065 = 0.44935 } \]

更新后的参数为: \(w_0 \approx -0.015\), \(w_1 \approx 0.449\)

练习2:编程实践与模型改进 - 任务 1

请使用本章内存中的 df 机制演示数据完成以下任务;它不写入文件,也不代表真实 A 股样本。

  1. 处理非线性关系:
    • 对目标变量 generated_target 和特征 price_placeholder 同时进行对数变换 (np.log)。
    • 使用变换后的数据作为新的目标变量和特征,重新训练一个 OLS 模型 (statsmodels)。
    • 比较新模型的 R² 和均方误差(MSE),与原始模型相比是否有改善?

提示: MSE 是反向指标,越低越好。R² 是正向指标,越高越好。

练习2:编程实践与模型改进 - 任务 2

  1. 添加多项式特征:
    • 在原始模型的基础上,额外加入二至四次项。
    • 预先把三项定义为一个检验族,使用联合 F/Wald 检验;不得逐项筛 p 值后宣称发现非线性。
    • 在同一目标尺度、同一预先划定的验证窗口比较基准与多项式模型。
    • 最终末段保持最终测试,只在规格确定后打开一次。
    • 本机制数据没有经济时间含义;样本内 R² 上升不是选择依据。

练习:先完成再查看答案:两种规格先最终测试

  • 模型规格:提交对数与多项式两种规格。
  • 时间证据:标明训练、验证与最终测试边界。
  • 比较结果:提交训练均值基线、验证 MSE 表与联合显著性检验。
  • 提交纪律:未交开发窗证据不得查看答案;测试期参与选模则重做。

练习2:参考代码与解答

代码
# 为“练习2:参考代码与解答”导入 `pandas` 并绑定 `pd`,用于整理当前任务的表格、字段与时间索引。
import pandas as pd
# 为“练习2:参考代码与解答”导入 `numpy` 并绑定 `np`,用于执行当前任务的数组、数值或随机机制计算。
import numpy as np
# 导入 statsmodels 并绑定为 `sm`,用于添加截距并拟合带推断统计量的 OLS 规格。
import statsmodels.api as sm
# 为“练习2:参考代码与解答”,从 `sklearn.metrics` 导入`mean_squared_error` 用于计算真实值与预测值的均方误差。
from sklearn.metrics import mean_squared_error

df_cleaned = df[(df['generated_target'] > 0) & (df['price_placeholder'] > 0)].copy()  # 只保留可进行对数变换的生成观测
# 记录完整样本数,用原生成顺序划定互斥的三段机制样本。
exercise_sample_count = len(df_cleaned)
# 确定前 60% 为训练窗,所有规格只在此段估计参数。
training_end = int(exercise_sample_count * 0.6)
# 确定中间 20% 为验证窗,所有候选与训练均值基线只在此段比较。
validation_end = int(exercise_sample_count * 0.8)
# 保存训练、验证与末段最终测试索引,后续禁止重新切分。
training_rows, validation_rows, sealed_test_rows = df_cleaned.index[:training_end], df_cleaned.index[training_end:validation_end], df_cleaned.index[validation_end:]
# 汇总三段边界与样本量,作为提交要求中的第一项证据。
split_boundaries = pd.DataFrame({'窗口': ['训练', '验证', '最终测试'], '起始序号': [0, training_end, validation_end], '结束序号': [training_end - 1, validation_end - 1, exercise_sample_count - 1], '样本数': [len(training_rows), len(validation_rows), len(sealed_test_rows)]})
# 展示互斥窗口,证明最终测试没有参与候选比较。
display(split_boundaries)

# 打印原始线性规格标题,将其推断结果与后续变换规格分区展示。
print("--- 原始模型 ---")
# 固定 `features_orig` 的业务取值与顺序,作为“练习2:参考代码与解答”的可复算输入。
features_orig = ['price_placeholder', 'bm_placeholder', 'profitability_placeholder']
# 提取 `X_orig` 作为“练习2:参考代码与解答”的特征矩阵,明确模型可见的输入列。
X_orig = sm.add_constant(df_cleaned[features_orig])
# 提取 EPS 作为三种函数形式共同评价的回归目标。
y_orig = df_cleaned['generated_target']
# 仅用前段训练行拟合原始线性候选,保留验证与测试观测。
model_orig = sm.OLS(y_orig.loc[training_rows], X_orig.loc[training_rows]).fit()
# 提取训练期 R² 只作拟合诊断,不用它替代最终测试比较。
r2_orig = model_orig.rsquared
# 只由训练窗确定常数均值,建立不接触验证目标的朴素基线。
training_mean_prediction = float(y_orig.loc[training_rows].mean())
# 在共同验证窗计算原始线性候选 MSE,作为函数形式比较证据。
mse_orig = mean_squared_error(y_orig.loc[validation_rows], model_orig.predict(X_orig.loc[validation_rows]))
# 在同一验证窗计算训练均值基线 MSE,防止复杂规格只与彼此比较。
mse_training_mean = mean_squared_error(y_orig.loc[validation_rows], np.repeat(training_mean_prediction, len(validation_rows)))
# 同时报告训练拟合、线性候选与训练均值的验证误差。
print({'训练R²': round(r2_orig, 4), '线性验证MSE': round(mse_orig, 4), '训练均值验证MSE': round(mse_training_mean, 4)})
窗口 起始序号 结束序号 样本数
0 训练 0 546 547
1 验证 547 729 183
2 最终测试 730 912 183
--- 原始模型 ---
{'训练R²': 0.1994, '线性验证MSE': 0.1901, '训练均值验证MSE': 0.2435}

练习2答案:任务1(对数模型)

代码
# 打印对数变换规格标题,明确随后结果来自变化后的响应与特征口径。
print("--- 任务1:对数变换模型 ---")
# 对严格为正的目标变量取自然对数,建立对数尺度回归目标。
df_cleaned['log_target'] = np.log(df_cleaned['generated_target'])
# 对严格为正的价格占位变量取自然对数,匹配对数模型的解释尺度。
df_cleaned['log_price_placeholder'] = np.log(df_cleaned['price_placeholder'])
# 固定 `features_log` 的业务取值与顺序,作为“练习2答案:任务1(对数模型)”的可复算输入。
features_log = ['log_price_placeholder', 'bm_placeholder', 'profitability_placeholder']
# 为对数规格加入截距列,使训练矩阵与 OLS 方程一致。
X_log = sm.add_constant(df_cleaned[features_log])
# 提取对数规格的响应变量,与已加截距的共同样本设计矩阵逐行对齐。
y_log = df_cleaned['log_target']
# 仅用共同训练行拟合对数模型,避免读取最终测试目标。
model_log = sm.OLS(y_log.loc[training_rows], X_log.loc[training_rows]).fit()
log_diagnostics = pd.DataFrame({'指标': ['样本数', 'R²', '调整R²', '残差MSE'], '数值': [int(model_log.nobs), model_log.rsquared, model_log.rsquared_adj, model_log.mse_resid]})  # 提取投影所需诊断而不输出整页摘要
display(log_diagnostics.round(4))  # 用紧凑表替代会产生内部滚动的完整 statsmodels 摘要
# 提取 `r2_log` 的 R²,限定在同一目标尺度内评价拟合程度。
r2_log = model_log.rsquared
# 用训练残差估计 Duan smearing 因子,校正对数预测反变换的均值偏差。
smearing_factor = np.exp(model_log.resid).mean()
# 把验证窗对数预测还原到原始目标尺度,与线性候选和基线同窗比较。
log_validation_predictions = np.exp(model_log.predict(X_log.loc[validation_rows])) * smearing_factor
# 在共同验证窗计算原始尺度 MSE,避免跨尺度误判。
mse_log = mean_squared_error(y_orig.loc[validation_rows], log_validation_predictions)
# 报告训练 R² 与同尺度验证 MSE,不提前读取最终测试目标。
print({'对数模型训练R²': round(r2_log, 4), '对数模型验证MSE': round(mse_log, 4), '相对线性验证MSE变化': round(mse_log - mse_orig, 4)})
--- 任务1:对数变换模型 ---
指标 数值
0 样本数 547.0000
1 0.1365
2 调整R² 0.1318
3 残差MSE 0.5748
{'对数模型训练R²': 0.1365, '对数模型验证MSE': 0.186, '相对线性验证MSE变化': -0.0041}

练习2答案:任务2(多项式模型)

代码
# 打印多项式规格标题,明确随后结果来自四阶曲率设计。
print("--- 任务2:多项式特征模型 ---")
# 计算股价平方项,允许回归捕捉二阶曲率。
df_cleaned['pps2'] = df_cleaned['price_placeholder']**2
# 计算股价三次项,扩展多项式候选模型。
df_cleaned['pps3'] = df_cleaned['price_placeholder']**3
# 构造价格代理变量的四次项,补齐四阶多项式设计矩阵以检验非线性曲率。
df_cleaned['pps4'] = df_cleaned['price_placeholder']**4
# 固定 `features_poly` 的业务取值与顺序,作为“练习2答案:任务2(多项式模型)”的可复算输入。
features_poly = ['price_placeholder', 'pps2', 'pps3', 'pps4', 'bm_placeholder', 'profitability_placeholder']
# 为四阶多项式规格加入截距列,保留与线性基准相同的常数项。
X_poly = sm.add_constant(df_cleaned[features_poly])
# 提取四阶多项式规格的响应变量,确保与共同最终测试设计的训练行一致。
y_poly = df_cleaned['generated_target']
# 仅用共同训练行拟合四阶多项式规格,最终测试行不参与参数估计。
model_poly = sm.OLS(y_poly.loc[training_rows], X_poly.loc[training_rows]).fit()
polynomial_diagnostics = pd.DataFrame({'指标': ['样本数', 'R²', '调整R²', '残差MSE'], '数值': [int(model_poly.nobs), model_poly.rsquared, model_poly.rsquared_adj, model_poly.mse_resid]})  # 提取投影所需诊断而不输出整页摘要
display(polynomial_diagnostics.round(4))  # 用紧凑表替代会产生内部滚动的完整 statsmodels 摘要
joint_polynomial_test = model_poly.f_test('pps2 = 0, pps3 = 0, pps4 = 0')  # 对预先声明的高次项检验族做联合F检验
print('高次项联合F检验:', joint_polynomial_test)  # 报告整体非线性证据而非逐项筛选p值
# 提取 `r2_poly` 的 R²,限定在同一目标尺度内评价拟合程度。
r2_poly = model_poly.rsquared
# 在共同验证窗计算原始尺度 MSE,把复杂度比较与样本内拟合分开。
mse_poly = mean_squared_error(y_poly.loc[validation_rows], model_poly.predict(X_poly.loc[validation_rows]))
# 报告训练 R² 与同尺度验证 MSE,评估曲率是否带来开发期增量。
print({'多项式训练R²': round(r2_poly, 4), '多项式验证MSE': round(mse_poly, 4), '相对线性验证MSE变化': round(mse_poly - mse_orig, 4)})
--- 任务2:多项式特征模型 ---
指标 数值
0 样本数 547.0000
1 0.2003
2 调整R² 0.1914
3 残差MSE 0.1923
高次项联合F检验: <F test: F=0.19264898013617124, p=0.9014106407956991, df_denom=540, df_num=3>
{'多项式训练R²': 0.2003, '多项式验证MSE': 0.19, '相对线性验证MSE变化': -0.0001}

规格确定与一次性最终测试

  1. 预设并列规则:原始线性 → 对数 → 四阶多项式,优先较简规格。
  2. 基线角色:训练均值只作门槛,不作为需要重拟合的回归规格。
  3. 确定规格:只使用开发期验证结果完成选择。
  4. 一次测试:合并训练与验证窗重训,最后只读取一次最终测试末段。
代码
# 汇总训练均值与三种候选在完全相同验证窗、原始目标尺度上的 MSE。
validation_mse_table = pd.DataFrame({'规格': ['训练均值基线', '原始线性', '对数', '四阶多项式'], '验证MSE': [mse_training_mean, mse_orig, mse_log, mse_poly]})
# 展示完整验证比较结果,禁止只报告最后赢家。
display(validation_mse_table.round(4))
# 确定候选并列时的简约优先次序,使 tie-break 不受最终测试结果影响。
specification_tie_rank = {'原始线性': 0, '对数': 1, '四阶多项式': 2}
# 仅在三个模型候选中按验证 MSE 与事先确定 tie-break 确定规格。
selected_specification = min(specification_tie_rank, key=lambda name: (dict(zip(validation_mse_table['规格'], validation_mse_table['验证MSE']))[name], specification_tie_rank[name]))
# 在查看测试目标前确定“采用/不采用”,未胜训练均值基线时走基线路径。
adoption_decision = '采用候选' if validation_mse_table.set_index('规格').loc[selected_specification, '验证MSE'] < mse_training_mean else '不采用:未胜训练均值基线'
# 合并开发期索引,只在候选与采用决策确定后用于最终重训。
development_rows = training_rows.append(validation_rows)
if adoption_decision != '采用候选':  # 验证未过门槛时确定开发期均值基线路径
    final_model_name, sealed_predictions = '开发期均值基线', np.repeat(float(y_orig.loc[development_rows].mean()), len(sealed_test_rows))  # 用开发期均值生成最终测试预测
elif selected_specification == '原始线性':  # 线性候选胜出时仅用开发期重拟合
    final_model_name, final_model = '原始线性', sm.OLS(y_orig.loc[development_rows], X_orig.loc[development_rows]).fit()  # 重估确定线性规格
    sealed_predictions = final_model.predict(X_orig.loc[sealed_test_rows])  # 对最终测试末段执行唯一一次预测
elif selected_specification == '对数':  # 对数候选胜出时在开发期重估模型与涂抹因子
    final_model_name, final_model = '对数', sm.OLS(y_log.loc[development_rows], X_log.loc[development_rows]).fit()  # 重估确定对数规格
    sealed_predictions = np.exp(final_model.predict(X_log.loc[sealed_test_rows])) * np.exp(final_model.resid).mean()  # 还原原始目标尺度
else:  # 多项式候选胜出时仅用开发期重拟合事先确定四阶规格
    final_model_name, final_model = '四阶多项式', sm.OLS(y_poly.loc[development_rows], X_poly.loc[development_rows]).fit()  # 重估确定四阶规格
    sealed_predictions = final_model.predict(X_poly.loc[sealed_test_rows])  # 对最终测试末段执行唯一一次预测
# 只在确定规格与不采用路径之后计算一次最终测试 MSE。
sealed_test_mse = mean_squared_error(y_orig.loc[sealed_test_rows], sealed_predictions)
# 报告确定决策、最终路径与唯一测试证据,不据测试结果翻案。
print({'确定候选': selected_specification, '开发期决策': adoption_decision, '最终测试执行路径': final_model_name, '最终测试MSE': round(sealed_test_mse, 4), '高次项联合检验p值': float(joint_polynomial_test.pvalue)})
规格 验证MSE
0 训练均值基线 0.2435
1 原始线性 0.1901
2 对数 0.1860
3 四阶多项式 0.1900
{'确定候选': '对数', '开发期决策': '采用候选', '最终测试执行路径': '对数', '最终测试MSE': 0.2152, '高次项联合检验p值': 0.9014106407956991}

开放式讨论:我们还能找到哪些变量来改进模型?

我们已经使用了股价、账面市值比和资产收益率。如果我们想进一步提高盈利预测的准确率,还可以考虑加入哪些新的特征变量?

请从以下几个角度思考:

  • 宏观经济: 利率、GDP增长率、通货膨胀率…
  • 行业层面: 行业景气指数、行业平均市盈率…
改进模型的潜在变量来源 一个概念图,展示了可以从宏观经济和行业层面引入新的变量来改进预测模型。 宏观经济变量 ✓ 利率 (Interest Rate) ✓ GDP 增长率 ✓ 通货膨胀率 (CPI) 行业层面变量 ✓ 行业景气指数 ✓ 行业平均市盈率 ✓ 产业链供需数据 改进模型

开放式讨论:公司自身与另类数据

  • 公司自身:
    • 历史财务数据: 过去几期的盈利增长率、研发投入…
    • 另类数据: 管理层变动新闻、网络舆情、供应链数据…

什么样的变量可以帮助我们更好地进行预测?这是一个没有标准答案的问题,也是数据科学和金融学完美结合的地方。

改进模型的潜在变量来源:公司与另类数据 一个概念图,展示了可以从公司自身数据和另类数据中引入新的变量来改进预测模型。 公司自身变量 ✓ 历史盈利增长率 ✓ 研发投入强度 ✓ 管理层变动新闻 另类数据 ✓ 网络舆情/情绪分析 ✓ 卫星图像 (如工厂活动) ✓ 招聘数据 更准确的预测

公开中国数据练习:描述性 OLS

代码
from pathlib import Path  # 管理公司基础信息数据版本路径
from urllib.request import urlretrieve  # 复用本章已安装的浏览器标识下载器
import numpy as np  # 对发行价做对数变换
import pandas as pd  # 清洗上市日期和发行价格
import statsmodels.api as sm  # 估计带标准误的描述性线性模型
# 按 Linux 共享数据、Windows 共享数据、项目缓存的顺序选择公司基础信息文件。
basic_path = next((candidate_path for candidate_path in [Path('/home/ubuntu/r2_data_mount/data/stock/stock_basic_data.h5'), Path('C:/qiufei/data/stock/stock_basic_data.h5'), Path('data/course/stock_basic_data.h5')] if candidate_path.exists()), Path('data/course/stock_basic_data.h5'))
if not basic_path.exists():
    basic_path.parent.mkdir(parents=True, exist_ok=True)
    urlretrieve('https://assets.qiufei.site/data/stock/stock_basic_data.h5', basic_path)
listing_data = pd.read_hdf(basic_path, key='stock_basic_info', columns=['order_book_id', 'listed_date', 'issue_price', 'province'])  # 选择性读取回归字段
listing_data['listed_date'] = pd.to_datetime(listing_data['listed_date'], errors='coerce')  # 将非法日期转为缺失
listing_data['listing_year'] = listing_data['listed_date'].dt.year  # 构造上市年份解释变量
listing_sample = listing_data.query('issue_price > 0 and 1991 <= listing_year <= 2024').dropna().copy()  # 固定有效横截面样本
listing_sample['log_issue_price'] = np.log(listing_sample['issue_price'])  # 缩减发行价右偏
ols_design = sm.add_constant(listing_sample[['listing_year']])  # 加入截距并确定设计矩阵
local_ols = sm.OLS(listing_sample['log_issue_price'], ols_design).fit(cov_type='HC1')  # 使用异方差稳健标准误估计关联
print({'文件': basic_path.name, '样本数': len(listing_sample), '样本期': (listing_sample['listing_year'].min(), listing_sample['listing_year'].max())})  # 输出数据口径
print(local_ols.summary2().tables[1].round(4))  # 输出系数、标准误、统计量与区间
print('本结果仅描述数据版本关联,不识别上市年份对发行价的因果影响。')  # 明示推断边界
表 1: 中国上市公司发行价与上市年份的描述性 OLS 检查
{'文件': 'stock_basic_data.h5', '样本数': 5399, '样本期': (1991.0, 2024.0)}
                Coef.  Std.Err.        z  P>|z|    [0.025   0.975]
const        -95.7822    2.5602 -37.4115    0.0 -100.8001 -90.7642
listing_year   0.0489    0.0013  38.4369    0.0    0.0464   0.0514
本结果仅描述数据版本关联,不识别上市年份对发行价的因果影响。

本章小结

  • 能写出模型、损失与一次梯度更新,并用矩阵秩/条件数诊断共线性。
  • 统计推断与预测任务采用不同诊断和处置;删变量须有领域或事先确定依据。
  • 当前公开 OLS 只描述关联,不建立因果或样本外预测能力。
  • 下一必修章进入逻辑回归,因为二元目标需要概率链接与分类验证。