本章会用到的数据
公开下载:公司基本信息和线性回归练习数据。
这些数据能做什么:用上市年限、发行价和地区等变量理解线性回归。
分析时注意:先说明要解释还是预测,再选择变量、样本和评价方法。
判断模型:用较早数据拟合和选择模型,最后用较晚数据检查预测误差。
【课堂核心】3 学时学习安排
| 动机与先修 |
20 |
| 线性模型与梯度 |
45 |
| 共线性与验证 |
35 |
| 形成性检查与反馈 |
20 |
| 独立任务 |
35 |
| 答案、验证与小结 |
25 |
【可选拓展】几何、代数与额外函数形式
未列入学习安排的几何图、完整代数推导与额外函数形式均安排课后;课堂只沿显式核心锚点运行。
引言:金融预测与线性回归
金融大数据时代,预测能力是核心竞争力。线性回归作为最基础、最强大的预测模型之一,是理解更复杂算法的基石。
本章学习目标:理解金融预测的基石
- 理解核心应用: 线性回归是预测股价、盈利和回报率等连续变量的基石。
本章学习目标:掌握模型结构 \(y = w_0 + w_1x\)
- 掌握模型结构: 我们将深入剖析 \(y = w_0 + w_1x\) 的每一个组成部分。
本章学习目标:揭秘模型训练过程
- 揭秘训练过程: 理解机器如何通过“梯度下降”方法自动“学习”最佳参数。
本章学习目标:实现代码实战
- 实现代码实战: 我们将使用 Python 的专业库
statsmodels 和 scikit-learn 解决真实的金融预测问题。
核心问题:我们能否预测一家公司的盈利能力?
在金融市场,预期是影响价格的关键因素。公司季报或年报发布前,分析师会发布盈利预测。如果实际盈利不及预期,股价常会应声下跌。
传统预测方法及其局限
- 传统方法: 分析师通过大量行业调研、公司访谈来做出预测,成本高昂。
我们的目标:完成盈利预测练习
- 我们的目标: 利用机器学习,根据历史数据预测未来每股收益 (Earnings Per Share, EPS),并判断预测是否优于简单基准。
数据是模型的燃料:目标变量 y
我们将使用公开的财务数据来构建模型。我们的目标是预测 每股收益 (y)。
| 目标 (y) |
每股收益 (EPS) |
公司季报 |
季报公告后 |
数据是模型的燃料:特征变量 x
| 特征 (x) |
股价 (Price) |
交易所 |
季报公告前 |
| 特征 (x) |
账面市值比 (Book-to-Market, B/M) |
前期财报与预测时点市值 |
季报公告前 |
| 特征 (x) |
资产收益率 (ROA) |
前期财报 |
季报公告前 |
数据是模型的燃料:关键逻辑
关键逻辑: 我们必须使用在预测时刻点 已经可以获得 的信息 (特征 x) 来预测 未来未知 的信息 (目标 y)。
第一步:从最简单的模型开始——一元线性回归
现实世界是复杂的,但科学研究总是从最简单的模型开始。我们首先假设,只用一个特征来预测目标。
例如:我们假设公司的 股价 (x) 与其未来的 每股收益 (y) 之间存在线性关系。
模型表达式与参数解读
我们的模型可以表示为一个简单的直线方程:
\[ \large{ \text{预测的每股收益} = f(x) = w_0 + w_1 \times \text{股价} } \]
- \(x\): 特征变量 (股价)
- \(w_0\): 截距项 (Intercept),在机器学习中也称为 偏置项 (Bias)
- \(w_1\): 斜率 (Slope),代表股价每变动1单位,预测的EPS会变动多少。在机器学习中称为 权重 (Weight)
不同参数代表了对数据关系的不同“假设”
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()
核心任务:从候选直线中找到总误差最小者,即完成模型训练。
如何定义“最佳”拟合?——最小化预测误差
假设我们有了一些真实数据点(蓝色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()
我们需要一个数学工具来 量化 这个“整体误差”。这个工具就是 代价函数 (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}\):对误差求平方,使得正负误差不会相互抵消,并且对大误差的惩罚更重。
求和与平均化:完成代价函数
\[ \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)\) 的值 最小。
代价函数的可视化:一个二次型的碗
为了简化,我们暂时固定 \(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() # 输出两幅并排机制图
寻找最低点的方法:梯度下降 (Gradient Descent)
梯度下降是一种强大的优化算法,用于寻找函数的最小值。 它的核心目标是:
\[ \large{ \text{目标: } \min_{w_0, w_1} J(w_0, w_1) } \]
梯度下降:直观的山谷类比
核心思想: 想象你身处一个碗状的山谷中,想要走到谷底。最快的方法是什么?
- 环顾四周,找到 最陡峭的下坡方向。
- 朝着这个方向 迈出一步。
- 重复以上过程,直到你到达谷底(即无法再往下走)。
在数学上,“最陡峭的下坡方向”就是 负梯度 的方向。
梯度下降的数学表达:迭代更新规则
梯度下降是一个迭代过程。我们从一个随机的 \((w_0, w_1)\) 开始,然后不断地更新它们,使 \(J(w_0, w_1)\) 越来越小。
算法步骤:
- 初始化: 随机选择一个起始点 \((w_0, w_1)\)。
- 迭代更新: 重复以下步骤,直到收敛:
\[ \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\) 方向上的“坡度”。
更新规则的三个组成部分(图解)
学习率 \(\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()
线性回归的梯度下降更新规则
我们将代价函数 \(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 和 参数向量 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\) 进行同步更新。
多元回归的注意事项 (1): 特征缩放问题
- 尺度差异:例如股价在 10–100,而账面市值比 B/M 在 0.5–2.0。
- 几何后果:代价函数等高线变成狭长椭圆。
- 优化后果:梯度沿“Z 字形”来回震荡,收敛缓慢。
解决方案:特征标准化 (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 只是仿射平移与缩放,不会把偏态变成正态,也不会抹平原有形状。
多元回归的注意事项 (2): 多重共线性
多重共线性 (Multicollinearity) 指设计矩阵的列之间存在精确或近似线性依赖。高两两相关只是可能信号,并非必要条件。
- 例如: 同时使用“公司总资产”和“公司净资产”作为特征。这两个变量很可能是高度相关的。
多重共线性:诊断必须检查设计矩阵
问题:
- 使得模型参数的估计变得 不稳定。微小的数据变动可能导致系数估计值发生巨大变化。
- 难以解释单个特征的 独立贡献。我们分不清预测效果是来自特征A还是特征B。
诊断与处置:
- 先依据领域知识与估计目标检查变量构造,再检查矩阵秩与条件数,必要时用 VIF 辅助定位。
- 推断任务应报告系数不稳定性与置信区间;预测任务可在开发期验证正则化或降维。
- 删除变量必须有领域或事先确定依据,不能把固定相关阈值当成自动规则。
反例:令 \(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中实现线性回归最核心的库:
statsmodels:
- 源于统计学和计量经济学。
- 强项在于推断 (Inference): 提供详细的统计报告,如系数的t检验、p值、R²等,帮助我们理解变量之间的关系。
实践:用 Python 预测公司盈利 - scikit-learn
scikit-learn:
- 机器学习领域的标准库。
- 强项在于预测 (Prediction): 提供统一、简洁的API,专注于模型的训练、预测和评估,是构建机器学习步骤的首选。
实战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))
解读 statsmodels 的回归结果报告:coef 与 std err
这张表信息量巨大,是进行统计推断的核心。
coef |
系数 |
特征每增加一个单位,生成目标的条件均值变化量。 |
price_placeholder 的估计系数为 0.0220;它只复现预设机制,不代表股价或 EPS。 |
std err |
标准误 |
在模型假设下衡量系数估计的抽样不确定性。 |
price_placeholder 的标准误约为 0.0013,说明本次生成样本对该预设斜率估计较精确。 |
解读 statsmodels 的回归结果报告:t 与 P>|t|
t |
t统计量 |
在原假设和模型假设成立时衡量估计值相对标准误的极端程度。t = coef / std err。 |
绝对值较大只表示当前统计量与原假设较不相容;不能给“系数非零”赋后验概率。 |
P>|t| |
p 值 |
在原假设“系数为 0”及模型假设成立时,观察到当前或更极端 t 统计量的概率。 |
若报告值小于预先设定的显著性水平,可拒绝该原假设;它不是“系数为 0 的概率”,也不能单独证明因果。 |
限定性结论:
price_placeholder 与 profitability_placeholder 在 5% 水平拒绝零系数原假设。
bm_placeholder 的置信区间跨越 0。
- 结论只适用于这组教学生成数据和当前线性模型假设。
- 结果只帮助阅读回归表,不构成中国市场预测有效性或因果证据。
解读 statsmodels 的回归结果报告:R-squared 与置信区间
R-squared |
R² |
模型在当前生成样本中解释目标变异的比例。 |
本次 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 直接给出了截距和系数,但没有统计检验报告。
sklearn 结果解读:Beta系数的意义
- 保持不变:其他特征固定。
- 变化单位:
pps 增加一个训练样本标准差。
- 预测变化:模型预测的
eps_basic 增加约 0.2755。
这种标准化系数可在同一线性规格内比较特征与预测的相对关联。在本次样本内拟合中,pps 的系数绝对值最大;这不是因果重要性,也尚未构成样本外增量证据。
总结:statsmodels vs. scikit-learn - 统计推断
| 核心焦点 |
统计推断 |
机器学习预测 (次要) |
| 主要产出 |
详细的统计摘要 |
训练好的模型对象 (次要) |
| 显著性检验 |
提供 t-统计量, p-值 |
不直接提供 |
| 数据预处理 |
需要手动添加常数项 |
强调特征缩放 (可选,但推荐) |
| 适用场景 |
学术研究, 经济分析, 解释变量关系 |
(次要) |
结论: statsmodels 更适用于需要深入理解变量之间关系、进行假设检验和学术报告的场景。
总结:statsmodels vs. scikit-learn - 机器学习预测
| 核心焦点 |
统计推断 (次要) |
机器学习预测 |
| 主要产出 |
详细的统计摘要 (次要) |
训练好的模型对象 |
| 显著性检验 |
(次要) |
不直接提供 |
| 数据预处理 |
需要手动添加常数项 (次要) |
强调特征缩放 |
| 适用场景 |
(次要) |
构建预测系统, 机器学习流水线 |
- 偏重预测:
scikit-learn 更适合构建标准预测流程。
- 偏重解释与推断:
statsmodels 更便于查看统计检验与区间。
- 选择原则:先判断问题要回答“预测”还是“解释”,再选工具。
练习1:手动计算梯度下降 - 问题
给定以下数据点和初始模型参数,请手动执行一步梯度下降。
数据:
模型: \(f(x) = w_0 + w_1x\) 初始参数: \(w_0 = 0, w_1 = 0.5\) 学习率: \(\alpha = 0.01\)
问题:
- 计算当前参数下的代价函数 \(J(w_0, w_1)\) 的值。
- 计算当前参数下 \(J\) 对 \(w_0\) 和 \(w_1\) 的梯度。
- 执行一步梯度下降,计算更新后的新参数值 \(w_0'\) 和 \(w_1'\)。
练习1:解题步骤 1 - 计算预测值和误差
1. 计算每个点的预测值和误差
| 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:解题步骤 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 股样本。
- 处理非线性关系:
- 对目标变量
generated_target 和特征 price_placeholder 同时进行对数变换 (np.log)。
- 使用变换后的数据作为新的目标变量和特征,重新训练一个 OLS 模型 (
statsmodels)。
- 比较新模型的 R² 和均方误差(MSE),与原始模型相比是否有改善?
提示: MSE 是反向指标,越低越好。R² 是正向指标,越高越好。
练习2:编程实践与模型改进 - 任务 2
- 添加多项式特征:
- 在原始模型的基础上,额外加入二至四次项。
- 预先把三项定义为一个检验族,使用联合 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)})
| 0 |
样本数 |
547.0000 |
| 1 |
R² |
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)})
| 0 |
样本数 |
547.0000 |
| 1 |
R² |
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}
规格确定与一次性最终测试
- 预设并列规则:原始线性 → 对数 → 四阶多项式,优先较简规格。
- 基线角色:训练均值只作门槛,不作为需要重拟合的回归规格。
- 确定规格:只使用开发期验证结果完成选择。
- 一次测试:合并训练与验证窗重训,最后只读取一次最终测试末段。
代码
# 汇总训练均值与三种候选在完全相同验证窗、原始目标尺度上的 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)})
| 0 |
训练均值基线 |
0.2435 |
| 1 |
原始线性 |
0.1901 |
| 2 |
对数 |
0.1860 |
| 3 |
四阶多项式 |
0.1900 |
{'确定候选': '对数', '开发期决策': '采用候选', '最终测试执行路径': '对数', '最终测试MSE': 0.2152, '高次项联合检验p值': 0.9014106407956991}
开放式讨论:我们还能找到哪些变量来改进模型?
我们已经使用了股价、账面市值比和资产收益率。如果我们想进一步提高盈利预测的准确率,还可以考虑加入哪些新的特征变量?
请从以下几个角度思考:
- 宏观经济: 利率、GDP增长率、通货膨胀率…
- 行业层面: 行业景气指数、行业平均市盈率…
开放式讨论:公司自身与另类数据
- 公司自身:
- 历史财务数据: 过去几期的盈利增长率、研发投入…
- 另类数据: 管理层变动新闻、网络舆情、供应链数据…
什么样的变量可以帮助我们更好地进行预测?这是一个没有标准答案的问题,也是数据科学和金融学完美结合的地方。
公开中国数据练习:描述性 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('本结果仅描述数据版本关联,不识别上市年份对发行价的因果影响。') # 明示推断边界
本章小结
- 能写出模型、损失与一次梯度更新,并用矩阵秩/条件数诊断共线性。
- 统计推断与预测任务采用不同诊断和处置;删变量须有领域或事先确定依据。
- 当前公开 OLS 只描述关联,不建立因果或样本外预测能力。
- 下一必修章进入逻辑回归,因为二元目标需要概率链接与分类验证。