2  统计学习 (Statistical Learning)

2.1 本章学习契约

先修:掌握第 1 章的条件均值、损失、时点契约,并能用 pandas 排序与构造滞后变量。

学习目标与达成标准

  1. 区分预测与条件关联推断;标准是为新问题写出估计对象与不能声称的结论。
  2. 区分真实 \(f\)、估计 \(\hat f\)、误差 \(\epsilon\) 和残差 \(e\);标准是四者配对全对。
  3. 解释模型灵活性与泛化误差;标准是不预设 U 型或固定模型排序。
  4. 分解偏差、方差与不可约误差;标准是完成固定 \(x\) 下的交叉项消去推导。
  5. 构造训练—验证—测试边界和朴素基线;标准是测试集不参与选模且能输出基线对比。

2.1.1 入口检查与补修

入口题:“用过去 5 日收益预测下一日收益”中,shift(1)shift(-1) 分别用于什么?

作答后展开答案、门槛与补修

答案与门槛shift(1) 产生已知滞后特征,shift(-1) 把下一期结果对齐到当期预测原点。两者及标签末端缺失的含义均正确才通过;否则重做第 1 章时间线渐隐链。

2.1.2 低风险检索、渐隐链与迁移

检索:不看正文写出“残差”和“误差”的定义,再解释训练误差为何不是泛化误差。

渐隐链:先根据已给图辨认过拟合;再根据输出自行判断模型排序;最后在未见的保单赔付数据上设计损失、基线和时间测试。

2.1.3 目标—评价证据映射

目标 学习活动 评价证据
1—2 术语检索与案例辨析 练习 1、3
3—4 复杂度渐隐链与分解 练习 2、4、7—8
5 时间留出与陌生任务迁移 练习 5—6、小节 2.6

2.2 什么是统计学习? (What Is Statistical Learning?)

假设我们是一家位于中国长三角地区的金融科技公司的数据分析顾问,需要研究上市公司的财务投入与经营产出之间的关系。我们收集了200家长三角地区制造业上市公司的数据,包括它们的营业收入以及在三方面的投入:研发费用、销售费用和管理费用。数据如 图 2.1 所示。

企业投入与产出的系统性关联 长三角地区制造业上市公司:研发、销售及管理费用(横轴)与营业收入(纵轴) 投入强度 (Log) 营业收入 (Log) 研发费用 (R&D) 销售费用 (Sales) 管理费用 (Admin) 观测样本 样本拟合曲线
图 2.1: 统计学习初探:利用企业的三大费用投入(研发、销售、管理)来预测营业收入。本图展示了输入变量(特征)与响应变量(目标)之间的潜在关联。

虽然我们无法直接决定公司的营业收入,但管理层可以控制研发、销售和管理费用的预算分配。因此,如果我们能够确定这些投入与收入之间存在关联,就可以建议管理层优化资源配置,从而间接提升经营业绩。换句话说,我们的目标是建立一个准确的模型,基于三类费用投入来预测营业收入。

在这个案例中,费用投入是输入变量(input variable)或预测变量(predictor),而营业收入是输出变量(output variable)或响应变量(response)。输入变量通常用符号\(X\)表示,并用下标区分它们。例如,\(X_1\)可以是研发费用,\(X_2\)是销售费用,\(X_3\)是管理费用。输入变量有多个不同的名称,如预测变量(predictors)、自变量(independent variables)、特征(features)或简称为变量。输出变量——在本例中为营业收入——通常称为响应变量(response)或因变量(dependent variable),通常用符号\(Y\)表示。在本书中,我们将互换使用所有这些术语。

更一般地,假设我们有一个定量响应变量 \(Y\)\(p\) 个预测变量 \(X_1, X_2, \dots, X_p\)。我们假设 \(Y\)\(X = (X_1, X_2, \dots, X_p)\) 之间存在某种关系,这种关系可以用非常一般的形式表示为:

\[ Y = f(X) + \epsilon \tag{2.1}\]

其中,在平方损失下可定义 \(f(X)=E(Y\mid X)\),并令 \(\epsilon=Y-f(X)\),因而 \(E(\epsilon\mid X)=0\)。这一条件比“\(\epsilon\)\(X\) 独立”弱,且容许异方差。\(f\)代表\(X\)提供的关于\(Y\)系统信息,而\(\epsilon\)代表给定\(X\)后仍无法预测的部分。

Tip: 理解系统信息与随机噪音

系统信息\(f(X)\)是变量之间可预测的、稳定的模式。例如,广告投入越多,销售量通常越高——这是一种稳定的规律。而随机噪音\(\epsilon\)则包含了所有不可预测的因素,比如某一天突发的天气变化、竞争对手的意外促销活动等。

在实际数据分析中,我们的目标就是从充满噪音的数据中提取出有价值的系统信息。这就像在嘈杂的环境中聆听美妙的音乐——统计学习帮助我们”过滤”噪音,专注于真正的规律。

作为另一个例子,考虑 图 2.2 的左侧面板,这是一个来自长三角制造业上市公司的30个样本的净利润总资产关系的散点图。该图表明,我们可能能够使用总资产规模来预测公司净利润。然而,连接输入变量和输出变量的函数\(f\)通常是未知的。在这种情况下,必须基于观测点来估计\(f\)

下面的案例从本地数据中显式筛选长三角制造业上市公司,并抽取30个观测。二次多项式是对这一小样本的拟合曲线 \(\hat f\),不是不可观测的真实 \(f\)。观测值与拟合值之差是残差 \(e_i=y_i-\hat f(x_i)\),不是真实误差 \(\epsilon_i=y_i-f(x_i)\)

import numpy as np  # 导入numpy用于数值运算和多项式拟合
import matplotlib.pyplot as plt  # 导入matplotlib绘图库用于可视化
import pandas as pd  # 导入pandas用于数据框操作
import os  # 导入os模块用于跨平台路径处理
from pathlib import Path  # 用路径对象解析数据根目录

# 根据操作系统自动选择本地数据根目录路径
# 根据操作系统设置数据根目录路径
BOOK_DATA_DIR = Path(os.environ['BOOK_DATA_DIR']).expanduser().resolve()  # 从必需环境变量解析数据根目录
assert BOOK_DATA_DIR.is_dir(), f'BOOK_DATA_DIR 不存在: {BOOK_DATA_DIR}'  # 在数据读取前验证目录
# 拼接财务报表h5文件的完整路径
FINANCIAL_STMT_PATH = BOOK_DATA_DIR / 'stock/financial_statement.h5'  # 构建财务数据路径
STOCK_BASIC_PATH = BOOK_DATA_DIR / 'stock/stock_basic_data.h5'  # 构建公司基础信息路径

# 设置全局随机种子确保每次运行抽样结果一致
np.random.seed(42)  # 设置随机种子确保结果可复现

下一个代码块只负责读取、筛选和构造拟合所需对象;它复用上一块中已验证的数据路径。

latest_annual_data = pd.read_hdf(  # 从本地财务报表h5文件中选择性读取2023年年报所需字段,避免全量载入全部季度与全部列
    FINANCIAL_STMT_PATH,  # 指定财务报表数据文件路径
    where="quarter='2023q4'",  # 仅保留2023年第四季度年报记录
    columns=['order_book_id', 'quarter', 'net_profit', 'total_assets']  # 保留公司代码以便执行地区行业筛选
).dropna(subset=['net_profit', 'total_assets'])  # 删除净利润或总资产存在缺失值的记录,保证后续抽样与拟合可执行
company_scope = pd.read_hdf(STOCK_BASIC_PATH, columns=['order_book_id', 'province', 'industry_name'])  # 只读取筛选所需基本字段
yrd_provinces = ['上海市', '江苏省', '浙江省', '安徽省']  # 显式定义长三角四省市
yrd_manufacturers = company_scope[company_scope['province'].isin(yrd_provinces) & company_scope['industry_name'].str.contains('制造', na=False)]  # 同时执行地区与制造业筛选
latest_annual_data = latest_annual_data.merge(yrd_manufacturers[['order_book_id']], on='order_book_id', how='inner')  # 只保留符合案例口径的公司
representative_sample = latest_annual_data.sample(30, random_state=42)  # 从长三角制造业样本抽取30家公司
total_assets_scale = representative_sample['total_assets'].values / 1e10  # 将总资产从元转换为百亿元
net_profit_scale = representative_sample['net_profit'].values / 1e8  # 将净利润从元转换为亿元

sorted_indices = np.argsort(total_assets_scale)  # 按总资产大小排序获取索引
x_for_systematic_trend = total_assets_scale[sorted_indices]  # 排序后的总资产序列
y_for_systematic_trend = net_profit_scale[sorted_indices]  # 对应排序后的净利润序列

# 用二阶多项式估计样本拟合曲线,不把它写成真实 f(X)
# 多项式拟合
polynomial_model = np.poly1d(np.polyfit(x_for_systematic_trend, y_for_systematic_trend, 2))
observed_y_values = net_profit_scale  # 原始观测的净利润值(含噪声)
observed_x_values = total_assets_scale  # 原始观测的总资产值

# 在资产范围内生成100个均匀插值点,用于绘制样本拟合曲线
smooth_x_axis = np.linspace(x_for_systematic_trend.min(), x_for_systematic_trend.max(), 100)
systematic_trend_y = polynomial_model(smooth_x_axis)  # 计算样本拟合曲线上的预测利润

以上代码加载了长三角制造业上市公司的真实财务数据,并从中随机抽取30个样本,再拟合一条二次多项式曲线。下面左侧展示观测散点,右侧展示样本拟合曲线与残差;两者都不能揭示不可观测的真实函数或误差项。

# 创建1行2列的双面板画布
fig, (ax_obs, ax_rel) = plt.subplots(1, 2, figsize=(14, 6))  # 创建子图布局

# 为两个子图统一设置浅灰背景和虚线网格
for ax in [ax_obs, ax_rel]:  # 遍历循环
    ax.set_facecolor('#F8F9FA')  # 设置浅灰色背景
    ax.grid(True, linestyle='--', alpha=0.6)  # 添加半透明虚线网格

# ---- 左侧面板:观测数据散点图 ----
ax_obs.scatter(observed_x_values, observed_y_values, color='#E3120B', alpha=0.7, s=80, edgecolors='white', linewidth=1)  # 绘制红色散点
ax_obs.set_xlabel('总资产 (Total Assets, 百亿元)', fontsize=12, fontweight='bold')  # 设置X轴标签
ax_obs.set_ylabel('净利润 (Net Profit, 亿元)', fontsize=12, fontweight='bold')  # 设置Y轴标签
ax_obs.set_title('观测数据 (Observed Data)', fontsize=14, fontweight='bold', color='#2C3E50')  # 设置标题

# ---- 右侧面板:样本拟合曲线 + 残差线 ----
ax_rel.plot(smooth_x_axis, systematic_trend_y, color='#2C3E50', linewidth=3, label='样本拟合曲线 φ̂(X)')  # 明确标记为估计量
ax_rel.scatter(observed_x_values, observed_y_values, color='#E3120B', alpha=0.6, s=70, edgecolors='white', zorder=5)  # 叠加观测散点

# 绘制观测点到拟合曲线的垂直残差线
for i in range(len(observed_x_values)):  # 计算元素数量
    predicted_y = polynomial_model(observed_x_values[i])  # 从拟合模型获取预测值
    # 绘制从观测点到样本拟合曲线的垂直残差虚线
    ax_rel.plot([observed_x_values[i], observed_x_values[i]], [predicted_y, observed_y_values[i]],
                color='#64748B', linewidth=1, linestyle=':', alpha=0.8)  # 绘制灰色垂直虚线

ax_rel.set_xlabel('总资产 (Total Assets, 百亿元)', fontsize=12, fontweight='bold')  # 右图X轴标签
ax_rel.set_ylabel('净利润 (Net Profit, 亿元)', fontsize=12, fontweight='bold')  # 右图Y轴标签
ax_rel.set_title('拟合曲线与样本残差', fontsize=14, fontweight='bold', color='#2C3E50')  # 不把估计量写成真值
ax_rel.legend(frameon=True, facecolor='white')  # 添加白色背景图例

plt.tight_layout()  # 自动调整子图布局
plt.show()  # 显示双面板对比图
/tmp/ipykernel_1411164/297315750.py:31: UserWarning: Glyph 770 (\N{COMBINING CIRCUMFLEX ACCENT}) missing from font(s) Source Han Sans SC.
  plt.tight_layout()  # 自动调整子图布局
/tmp/tmp.CfULtrnrMQ/peter-validation/lib/python3.10/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 770 (\N{COMBINING CIRCUMFLEX ACCENT}) missing from font(s) Source Han Sans SC.
  fig.canvas.print_figure(bytes_io, **kw)
左图为 30 家长三角制造业公司总资产与净利润散点,右图叠加二次样本拟合曲线和垂直残差线。
图 2.2: 净利润与总资产的关系

图 2.2 的左侧面板呈现30家长三角制造业上市公司的总资产与净利润散点,右侧叠加同一样本估计的二次多项式曲线。灰色虚线是观测值与拟合值之差,即样本残差 \(e_i\);它既不等于不可观测误差 \(\epsilon_i\),也不能把曲线提升为真实 \(f\)。图形只支持样本内关联描述,不能说明扩大资产会因果地提高利润。

通常,函数\(f\)可能涉及不止一个输入变量。在 图 2.3 中,我们将收入绘制为受教育年限和工作年限的函数。在这种情况下,\(f\)是一个二维曲面,必须基于观测数据进行估计。

为了帮助大家直观理解多变量模型的作用机制,接下来的这段代码通过 Numpy 构建了一个二维的离散网格空间(代表受教育年限和工作年限组合的域)。我们定义了一个包含二次项的假设映射函数 \(f\),随后在其计算出的连续曲面上叠加了注入高斯随机噪音的少数模拟观测样本。最终,代码利用 matplotlib 生成了一张美感十足的等高线图(Contour Plot)。在这张图中,颜色的深浅生动地代表了预期收入的高低,而散布其中的红色圆点表示现实的观测样本。这种二维平面加第三维颜色的展示方式,是我们在特征分析中用来可视化多维 \(Y=f(X_1, X_2)\) 响应曲面形态的最主要工具之一。

import numpy as np  # 导入numpy用于数组和网格计算
import matplotlib.pyplot as plt  # 导入matplotlib用于等高线可视化

# 生成受教育年限的坐标轴(10到22年,100个均匀点)
education_years_axis = np.linspace(10, 22, 100)  # 生成等间隔序列
# 生成工龄的坐标轴(0到40年,100个均匀点)
working_years_axis = np.linspace(0, 40, 100)  # 生成等间隔序列
# 用meshgrid构建二维网格矩阵,为计算二维曲面做准备
edu_mesh, working_mesh = np.meshgrid(education_years_axis, working_years_axis)  # 生成网格坐标矩阵

# 定义含二次项的非线性收入映射函数:收入随教育年限加速增长,随工龄先升后降
income_predicted_mesh = 25 + 4 * (edu_mesh - 10) + 0.3 * (edu_mesh - 10)**2 + 0.8 * working_mesh - 0.01 * working_mesh**2

np.random.seed(42)  # 设置随机种子确保受控观测可复现
sample_count = 35  # 冻结模拟观测数
edu_obs = np.random.uniform(10, 22, sample_count)  # 生成观测教育年限
work_obs = np.random.uniform(0, 40, sample_count)  # 生成观测工龄
systematic_income = 25 + 4 * (edu_obs - 10) + 0.3 * (edu_obs - 10)**2 + 0.8 * work_obs - 0.01 * work_obs**2  # 计算系统部分
income_obs = systematic_income + np.random.normal(0, 8, sample_count)  # 加入受控噪声

以上代码构建了一个100×100的二维收入预测曲面。下面的代码将用等高线图(Contour Plot)将这个三维关系投影到二维平面上,颜色深浅代表预期收入水平。

# 创建画布并设置浅灰学术风格背景
fig, ax_contour = plt.subplots(figsize=(12, 8))  # 创建子图布局
ax_contour.set_facecolor('#F8FAFC')  # 设置画布背景色

# 绘制填充等高线图,用红-黄-蓝色谱映射收入高低
contour_set = ax_contour.contourf(  # 绘制填充等高线,用连续色谱映射收入高低
    edu_mesh, working_mesh, income_predicted_mesh,  # 传入网格坐标和收入预测值
    levels=20, cmap='RdYlBu_r', alpha=0.8  # 20层颜色分级,反转色谱使高收入为暖色
)  # 完成填充等高线渲染

# 叠加线条等高线以增强可读性
clabel_set = ax_contour.contour(  # 叠加线条等高线以增强曲面层次感
    edu_mesh, working_mesh, income_predicted_mesh,  # 传入网格坐标和收入预测值
    levels=10, colors='#1E293B', linewidths=0.5, alpha=0.3  # 浅色细线条
)  # 完成线条等高线叠加
ax_contour.clabel(clabel_set, inline=True, fontsize=8)  # 在等高线上标注数值

# 在等高线图上叠加观测样本散点
ax_contour.scatter(  # 在等高线图上叠加观测样本散点
    edu_obs, work_obs, color='#E3120B', s=60, alpha=0.7, edgecolors='white', linewidth=0.8, label='观测样本'  # 将教育年限和工龄作为散点的x/y坐标
)  # 完成观测样本散点叠加

ax_contour.set_xlabel('受教育年限 (Years of Education)', fontsize=12, fontweight='bold')  # X轴标签
ax_contour.set_ylabel('工作年限 (Years of Seniority)', fontsize=12, fontweight='bold')  # Y轴标签
ax_contour.set_title('双因素收入预测等高线图 (Contour f(X))', fontsize=15, fontweight='bold', pad=20)  # 标题

# 添加颜色条说明色谱含义
cbar = fig.colorbar(contour_set, ax=ax_contour)  # 在图右侧添加颜色条,标注色谱对应的预期收入数值
cbar.set_label('预测年收入 (千元)', fontsize=10)  # 颜色条标签

plt.legend(frameon=True, facecolor='white', loc='upper left')  # 添加白色背景图例
plt.tight_layout()  # 自动调整布局
plt.show()  # 显示收入等高线图
教育年限为横轴、工龄为纵轴的彩色收入等高面,叠加 35 个带噪声模拟观测点。
图 2.3: 受教育程度与工龄的收入等高线图

图 2.3 的运行结果是一张色彩丰富的等高线图(Contour Plot)。在这张图中,横轴代表受教育年限(10年至22年),纵轴代表工作年限(0年至40年),而填充的颜色深浅则编码了第三个维度——预期年收入的高低。暖色调(红色、橙色)区域对应较高的预期收入水平,冷色调(蓝色)区域对应较低的收入水平。从图中可以清晰观察到:教育年限对收入的边际影响非常显著——沿着横轴从左向右移动时,颜色从深蓝迅速过渡为暖红色,这意味着高学历人群的预期收入明显更高;而工龄(纵轴)的影响相对温和,在中等教育水平的条件下,收入随工龄的增长呈先升后略降的趋势(反映了模型中工龄二次项的负系数效应)。散布在图上的35个红色圆点是带有随机噪声的模拟观测样本,它们围绕在等高线”正确的”颜色区域附近但并不完全吻合,正好直观呈现了 \(Y = f(X_1, X_2) + \epsilon\) 这一核心等式中随机误差项 \(\epsilon\) 的存在。

本质上,统计学习(statistical learning)指的就是一组用于估计\(f\)的方法。在本章中,我们将概述在估计\(f\)时出现的一些关键理论概念,以及评估所得结果的工具。

2.2.1 为什么要估计\(f\)? (Why Estimate \(f\)?)

我们希望估计\(f\)主要有两个原因:预测(prediction)和推断(inference)。我们将逐一讨论。

2.2.1.1 预测 (Prediction)

在许多情况下,一组输入\(X\)很容易获得,但输出\(Y\)无法轻易获得。在这种情况下,由于误差项平均为零,我们可以使用以下公式预测\(Y\)

\[ \hat{Y} = \hat{f}(X) \tag{2.2}\]

其中,\(\hat{f}\)代表我们对\(f\)的估计,\(\hat{Y}\)代表对\(Y\)的预测结果。在这种情况下,\(\hat{f}\)通常被视为一个黑盒(black box),意思是只要它能够对\(Y\)产生准确的预测,人们通常不关心\(\hat{f}\)的确切形式。

案例:上市公司营收预测

假设我们的长三角制造业公司拥有 200 家上市公司的数据,包括:

  • \(X_1\):研发投入 (R&D Expenditure, 万元)
  • \(X_2\):营销费用 (Marketing Expenses, 万元)
  • \(X_3\):管理成本 (Administrative Costs, 万元)
  • \(Y\):营业收入 (Operating Revenue, 万元)

我们的目标是估计映射 \(f\),使得对于任何给定的投入组合 \(X = (X_1, X_2, X_3)\),我们都能计算出预测产出 \(\hat{Y} = \hat{f}(X)\)

Clarification on ‘Prediction’ in Machine Learning

在机器学习和统计学习中,‘预测’(prediction)这个词的含义与日常语言有所不同。在日常对话中,我们说’预测明天会下雨’指的是对未来的展望。而在统计学中,预测指的是对不可直接观测的变量进行估计,这个变量可能是未来的,也可能是当前的。

例如:

  • 未来预测:基于历史股价预测明天的股价
  • 当前推断:基于患者血液指标预测其当前是否患有某种疾病
  • 缺失数据填充:基于其他变量的值预测某个缺失的数据点

因此,本书中的’预测’是一个更广泛的概念,既包括时间维度上的未来预测,也包括横截面数据的估计。

预测的准确性通常使用\(\hat{Y}\)\(Y\)之间的差异来衡量。这个差异(在某些假设下)可以分解为:

\[ E(Y - \hat{Y})^2 = E[f(X) + \epsilon - \hat{f}(X)]^2 = [f(X) - \hat{f}(X)]^2 + \text{Var}(\epsilon) \tag{2.3}\]

其中,\(E\)表示期望值。这个公式揭示了预测误差的两个来源:

  1. 可约误差(reducible error):\([f(X) - \hat{f}(X)]^2\)。这部分误差可以通过使用更合适的统计学习技术来估计\(f\)而减少。
  2. 不可约误差(irreducible error):\(\text{Var}(\epsilon)\)。这部分误差即使我们使用完美的估计\(\hat{f} = f\)也无法消除,因为\(\epsilon\)包含了\(X\)未解释的\(Y\)的变异。

Tip: 理解不可约误差

不可约误差的存在是因为在现实世界中,没有任何模型能够完美地预测结果。考虑我们电商公司的案例:即使我们建立了完美的模型来预测销售量,仍然有一些不可控因素会影响实际销售,例如:

  • 突发的公共卫生事件
  • 竞争对手的意外促销
  • 天气变化对消费者行为的影响
  • 社交媒体上的意外舆论

因此,设置现实的期望很重要:我们的目标不是达到100%的预测准确率,而是将预测误差降到接近不可约误差的水平。

2.2.1.2 推断 (Inference)

我们关心估计\(f\)的第二个原因是理解\(Y\)如何随着\(X_1, X_2, \ldots, X_p\)的变化而变化。在这种情况下,我们希望将\(\hat{f}\)看作一个透明、可解释的模型,而不是黑盒。

问题示例

  1. 哪些媒体与销售有关?:这有助于公司决定应该重点投资哪种媒体渠道。
  2. 哪些媒体与销售的条件关联最强?:观察性模型可比较关联与预测增量;预算调整的因果效果仍需实验或识别设计。
  3. 网络广告和销售之间的关系是线性的还是非线性的?:了解这种关系的性质可以帮助公司制定更有效的营销策略。
  4. 是否存在很强的交互效应?:例如,在电视广告和网络广告同时投放时,销售量的提升是否比单独投放时更大?

在这种情况下,可能更倾向于使用简单且高度可解释的模型。这类模型包括线性回归、逻辑回归等,它们的参数有明确的解释意义。

易混淆概念辨析:预测、关联与因果

  • 预测问“对未见样本能否猜准”,证据是锁定模型的样本外损失。
  • 关联问“在给定已纳入变量后 \(X\)\(Y\) 如何共变”,回归系数默认只能作这一层解释。
  • 因果问“若干预 \(X\)\(Y\) 会如何改变”,需要随机实验或可辩护的自然实验、工具变量、断点等识别设计,而不是仅靠一个可解释模型。

2.2.2 如何估计\(f\)? (How Do We Estimate \(f\)?)

在统计学习中,我们有许多用于估计\(f\)的方法。这些方法可以大致分为两类:参数化方法(parametric methods)和非参数化方法(non-parametric methods)。

2.2.2.1 参数化方法 (Parametric Methods)

参数化方法分为两个步骤:

  1. 第一步:对函数形式做出假设 首先,我们对\(f\)的形式或形状做出假设。例如,一个非常简单的假设是\(f\)是线性的: \[ f(X) = \beta_0 + \beta_1 X_1 + \beta_2 X_2 + \cdots + \beta_p X_p \tag{2.4}\]

  2. 第二步:拟合或训练模型 在这个步骤中,我们需要估计模型参数。对于线性模型,我们需要估计\(\beta_0, \beta_1, \ldots, \beta_p\)。最常用的方法是最小二乘法(ordinary least squares, OLS),它选择使预测误差平方和最小的参数值。

参数化方法的优点是简化了估计\(f\)的问题——从估计任意\(p\)维函数的问题简化为估计\(p+1\)个参数的问题。然而,缺点是我们选择的模型形式可能与真实的\(f\)形式相差甚远。如果选择的模型与真实\(f\)相差太远,那么我们的估计将表现很差。例如,如果真实\(f\)是非线性的,而我们使用线性模型,那么无论数据量多大,估计都会不准确。

Clarification on ‘Parameter’ in Statistics

在统计学中,‘参数’(parameter)一词有着特定的含义,与日常语言中的用法不同。在统计学中:

  • 参数 (Parameter):描述总体特征的数值,通常是未知的、固定的常数。例如,总体的均值\(\mu\)、方差\(\sigma^2\)等。
  • 统计量 (Statistic):根据样本数据计算的数值,用于估计参数。例如,样本均值\(\bar{x}\)、样本方差\(s^2\)等。

因此,‘参数化方法’(parametric methods)指的是那些假设数据遵循具有有限个未知参数的特定概率分布的方法,而不是指’有参数的方法’(这会造成混淆,因为几乎所有方法都有参数)。

2.2.2.2 非参数化方法 (Non-parametric Methods)

非参数化方法对函数\(f\)的函数形式做出明确的假设。相反,它们尝试在不过度限制的情况下尽可能接近数据点进行估计。由于非参数化方法避免了选择特定函数形式的危险,它们可能比参数化方法更准确地拟合更广泛的可能形状。然而,非参数化方法的主要缺点是,由于它们不简化问题,通常需要大量的观测数据(大样本)才能获得准确的估计。

非参数化方法的例子包括:

  • 样条平滑 (smoothing splines)
  • 广义加法模型 (generalized additive models, GAM)
  • 决策树 (decision trees)
  • 随机森林 (random forests)
  • 支持向量机 (support vector machines, SVM)
  • 神经网络 (neural networks)

Tip: 参数化vs非参数化方法的选择

选择参数化还是非参数化方法,取决于你的目标和数据特征:

选择参数化方法的情况

  • 数据量较小(样本数 < 100)
  • 需要模型可解释性强,能够理解每个变量与预测结果的条件关联
  • 希望进行推断和假设检验
  • 对变量之间的关系有较强的先验知识

选择非参数化方法的情况

  • 数据量大(样本数 > 1000)
  • 主要目标是预测准确度,而非解释性
  • 变量之间的关系复杂、未知或高度非线性
  • 愿意接受计算成本较高的模型

在实际项目中,数据科学家通常会尝试多种方法,并通过交叉验证来比较它们的性能。

2.2.3 预测准确度与模型可解释性的权衡 (Trade-Off Between Prediction Accuracy and Model Interpretability)

在统计学习的商业应用中,我们经常面临一个核心抉择:是为了极致的预测性能牺牲透明度,还是为了获得可控的、能被人理解的逻辑而牺牲部分准确度?在整个本书中,我们将看到,灵活的(flexible)统计学习方法通常能够更准确地拟合训练数据。然而,如果这些方法过于灵活,它们可能会导致过拟合(overfitting)——即它们捕捉到了训练数据中的随机噪音,而不是真实的模式。

案例:量化选股中的模型选择 在构建多因子量化选股策略时,深度神经网络(Deep Neural Networks)由于高度灵活,可能在历史数据中发现复杂的非线性信号并实现高额回测收益。然而,当神经网络重仓某只基本面极差的股票时,基金经理几乎无法解释其背后的逻辑。由于这种“黑盒”模型缺乏可解释性,投资委员会通常会拒绝使用。相反,基金经理往往会退而求其次,使用线性因子多分类模型(如 Fama-French 模型)。尽管后者的预测准确度可能稍逊一筹,但其赋予每个因子(如估值、动量)明确且符合经济学直觉的权重。

图 2.4 展示了不同统计学习方法在灵活性和可解释性之间的权衡。子集选择(Subset Selection)、套索回归(Lasso)和岭回归(Ridge)等方法限制性较强(灵活性较低),因此更容易解释。广义加法模型、决策树、bagging、boosting等方法更加灵活,而深度神经网络等方法极其灵活,但较难解释。

为了呈现这一核心理论概念,下面的 Python 代码利用 matplotlib 手工构建了一张展示统计学习方法在“灵活性”与“可解释性”之间权衡的经典学术散点图。代码中明确定义了 8 种初中高级的机器学习模型在这个二维光谱上的相对位置(例如,Lasso 回归位于左上方代表高解释性低灵活性,而深度学习位于右下方代表低解释性高灵活性)。我们通过不同的颜色和节点文字对比,以及辅助斜虚线,强化了这张图表的理论表达力。理解这一图表也构成了我们在后续各章节中针对具体业务寻找、选择和评估各类算法的指南针。

import matplotlib.pyplot as plt  # 导入matplotlib用于可视化

# 设置中文字体为思源宋体,确保中文标签正确显示
plt.rcParams['font.family'] = ['Source Han Serif SC']
plt.rcParams['axes.unicode_minus'] = False  # 修复负号显示为方块的问题

# 定义学术配色方案中的三种强调色
ACCENT_BLUE = '#2563EB'  # 蓝色:表示简单/可解释性强的模型
ACCENT_ORANGE = '#EA580C'  # 橙色:表示中等复杂度模型
ACCENT_RED = '#DC2626'  # 红色:表示高灵活性/黑盒模型

# 定义8种统计学习方法在(灵活性, 可解释性)二维空间中的坐标位置
statistical_methods = [  # 定义8种模型在灵活性-可解释性二维空间中的坐标和颜色
    (1.2, 9.0, '子集选择', ACCENT_BLUE),  # 低灵活性、高可解释性
    (2.0, 8.2, 'Lasso 回归', ACCENT_BLUE),  # 正则化线性方法
    (3.0, 7.5, '最小二乘法', ACCENT_BLUE),  # 经典OLS线性回归
    (4.5, 6.0, '广义加法模型', ACCENT_ORANGE),  # 半参数方法
    (5.8, 5.0, '决策树', ACCENT_ORANGE),  # 基于规则的分类方法
    (7.0, 3.8, '随机森林', ACCENT_ORANGE),  # 集成学习方法
    (8.2, 2.5, '支持向量机', ACCENT_RED),  # 核方法
    (9.2, 1.5, '深度学习', ACCENT_RED),  # 极度灵活的黑盒模型
]  # 完成模型坐标列表定义

上面定义了8种模型的理论坐标。下面的代码将它们绘制成学术散点图,直观展示灵活性与可解释性之间不得兼得的核心权衡。

# 创建画布
fig, ax_tradeoff = plt.subplots(figsize=(11, 7))  # 创建子图布局

# 循环绘制每个模型在二维权衡空间中的位置
for flex, interp, label, color in statistical_methods:  # 遍历每个模型并绘制其在权衡空间中的位置
    ax_tradeoff.scatter(flex, interp, s=400, color=color, alpha=0.8, edgecolors='#1E293B', linewidth=1.2, zorder=3)  # 绘制带深色边框的大圆点
    ax_tradeoff.text(flex, interp - 0.5, label, fontsize=10, ha='center', fontweight='bold')  # 在圆点下方标注模型名称

# 绘制从左上到右下的趋势虚线,表示灵活性与可解释性的负相关关系
ax_tradeoff.plot([1, 9.5], [9, 1.2], linestyle='--', color='#94A3B8', alpha=0.5, zorder=1)

ax_tradeoff.set_xlim(0, 11)  # 设置X轴范围
ax_tradeoff.set_ylim(0, 11)  # 设置Y轴范围
ax_tradeoff.set_xlabel('模型灵活性 (Flexibility) →', fontsize=12, fontweight='bold', labelpad=15)  # X轴标签
ax_tradeoff.set_ylabel('← 可解释性 (Interpretability)', fontsize=12, fontweight='bold', labelpad=15)  # Y轴标签
ax_tradeoff.set_title('预测性能与模型透明度的核心平衡 (ISLP Framework)', fontsize=15, fontweight='bold', pad=20)  # 图表标题

ax_tradeoff.set_facecolor('#F8FAFC')  # 设置浅灰背景
ax_tradeoff.set_xticks([])  # 隐藏X轴刻度(灵活性为定性排序)
ax_tradeoff.set_yticks([])  # 隐藏Y轴刻度(可解释性为定性排序)

# 在左上角添加文字标注简单模型的特点
ax_tradeoff.text(1.5, 10, '简单模型: 适合推断', fontsize=10, style='italic', color='#475569', bbox=dict(facecolor='white', alpha=0.5))
# 在右下角添加文字标注复杂模型的特点
# 添加文本标签
ax_tradeoff.text(8.0, 0.5, '复杂模型: 适合黑盒预测', fontsize=10, style='italic', color='#475569', bbox=dict(facecolor='white', alpha=0.5))

plt.tight_layout()  # 自动调整布局
plt.show()  # 显示灵活性-可解释性权衡图
八种统计学习方法在模型灵活性和可解释性二维平面中的位置,虚线表示总体反向取舍。
图 2.4: 灵活性与可解释性的权衡

图 2.4 是灵活性与可解释性的教学示意,而不是对所有数据集都成立的固定排名。Lasso 与 OLS 的参数结构通常更容易审计,但观察性系数只描述模型中的条件关联,不能自动回答广告干预的因果效果;GAM、树集成、SVM 与深度学习则以不同方式提高灵活性,也带来不同的解释成本。

为什么存在这种权衡?

假设我们正在处理电商公司的销售数据,真实的关系是:

\[ \text{销售量} = 5 + 2 \times \text{电视广告} + 1.5 \times \text{网络广告} + 0.5 \times \text{报纸广告} + \epsilon \]

但是,由于随机噪音的存在,观测数据可能呈现出一些非线性的模式。如果我们使用过于灵活的模型(如高阶多项式或深度神经网络),模型可能会”记住”训练数据中的噪音,导致在新数据上表现不佳。这种现象称为过拟合(overfitting)。

相反,如果我们使用过于简单的模型(如仅包含电视广告的线性模型),模型可能无法捕捉到数据中的真实模式,导致预测不准确。这种现象称为欠拟合(underfitting)。

Note: 监督学习vs无监督学习

统计学习方法可以根据是否有响应变量分为两类:

监督学习 (Supervised Learning):

  • 有明确的响应变量\(Y\)和预测变量\(X\)
  • 目标是学习从\(X\)\(Y\)的映射
  • 包括回归问题(\(Y\)为定量变量)和分类问题(\(Y\)为定性变量)
  • 例如:根据房屋特征预测房价、根据邮件内容判断是否为垃圾邮件

无监督学习 (Unsupervised Learning):

  • 没有响应变量,只有预测变量\(X\)
  • 目标是发现数据中的结构、模式或分组
  • 包括聚类分析、主成分分析、关联规则挖掘等
  • 例如:客户细分、异常检测、数据压缩

本书主要关注监督学习方法,但也会在第12章介绍无监督学习方法。

2.2.4 回归vs分类问题 (Regression Versus Classification Problems)

变量可以是定量的(quantitative)或定性的(qualitative/categorical)。

  • 定量变量:取值为数值大小,例如收入、身高、温度、销售量
  • 定性变量:取值为类别或标签,例如性别(男/女)、邮件类型(垃圾邮件/正常邮件)、客户类别(高价值/中价值/低价值)

那些涉及定量响应变量的问题称为回归问题(regression problems),而那些涉及定性响应变量的问题称为分类问题(classification problems)。

不过,这个区分有时有些模糊。例如,逻辑回归(logistic regression)是一种用于分类的方法,但其名称中包含”回归”二字。这是因为逻辑回归本质上是预测类别的概率,这是一个定量问题。

2.3 评估模型准确度 (Assessing Model Accuracy)

在统计学习中,没有一个方法在所有数据集上都优于其他所有方法。这就是著名的没有免费午餐定理(No Free Lunch Theorem)。因此,我们需要评估不同方法在给定数据集上的表现,以便选择最佳方法。

2.3.1 衡量拟合质量 (Measuring the Quality of Fit)

为了评估统计学习方法的表现,我们需要一种方法来衡量预测\(\hat{Y}\)与实际观测值\(Y\)之间的接近程度。在回归设置中,最常用的衡量标准是均方误差(mean squared error, MSE):

\[ \text{MSE} = \frac{1}{n} \sum_{i=1}^{n} (y_i - \hat{f}(x_i))^2 \tag{2.5}\]

其中,\((x_i, y_i)\)是训练数据中的第\(i\)个观测,\(\hat{f}(x_i)\)是基于训练数据估计的函数在\(x_i\)处的预测值。

Tip: 为什么使用平方误差?

你可能好奇,为什么使用平方误差而不是绝对误差?这里有几个原因:

  1. 数学便利性:平方函数是可微的,这使得我们可以使用微积分来寻找最优解
  2. 对大错误的惩罚更重:平方误差对大误差的惩罚大于小误差,这符合许多实际应用的需求(例如,预测误差为10的错误是误差为5的4倍,而不是2倍)
  3. 与高斯噪声的联系:如果误差服从正态分布,最小化平方误差等价于最大化似然函数

然而,绝对误差(MAE)在某些情况下也有优势,特别是当数据中存在异常值时,因为它对异常值不那么敏感。

2.3.1.1 训练误差vs测试误差 (Training Error vs. Test Error)

在实践中,我们感兴趣的不仅仅是模型在训练数据上的表现,而是模型在未见过的新数据上的表现。因此,我们需要区分:

  1. 训练MSE (Training MSE):在训练数据上计算的MSE
  2. 验证MSE (Validation MSE):在训练集内部的验证部分上计算,用于选择复杂度
  3. 测试MSE (Test MSE):锁定模型后在独立测试集上只计算一次,用于评估泛化误差

我们用验证集选择模型,再用从未参与选择的测试集评估泛化能力。直接挑选测试 MSE 最小的次数,会把测试集变成调参数据。

案例:电商销售预测的模型选择

让我们通过一个模拟案例来说明训练误差和测试误差的区别。假设真实的关系是:

\[ \text{销售量} = 5 + 2 \times \text{电视广告} + 1.5 \times \text{网络广告} + \epsilon \]

其中,\(\epsilon \sim N(0, 2^2)\)是随机误差。

以下的代码通过一场详尽的蒙特卡洛随机模拟,为你栩栩如生地重现了这种“过拟合”陷阱与泛化表现分裂。首先,我们模拟了 120 个长三角电商公司的营销预算(特征 \(X\))和真实存在三次多项式关系的销售量(目标 \(Y\)),并在其中加入了高斯噪声干扰。接着,我们严格按照时间序列演进逻辑将数据切割为训练数据子集和全新的测试验证子集。最核心的拟合循环部分中,我们借助 scikit-learnPolynomialFeaturesLinearRegression 对象,不断测试并估计从 1 阶逐步增加到 11 阶的复杂多项式模型,并分别记录它们在训练集和测试集上的均方误差(MSE)。最终绘制出的这两条误差变化实线与虚线曲线,向你直观证明了那个铁律:无节制地盲目追求对历史训练数据的完美死记硬背,一定会招致在未见过的测试数据上出现灾难性的极端大误差。

import numpy as np  # 导入numpy用于数值模拟
import matplotlib.pyplot as plt  # 导入matplotlib用于可视化
from sklearn.preprocessing import PolynomialFeatures  # 导入多项式特征变换器
from sklearn.linear_model import LinearRegression  # 导入线性回归模型
from sklearn.metrics import mean_squared_error  # 导入均方误差评估函数

# 设置随机种子确保模拟结果一致可复现
np.random.seed(99)  # 设置随机种子确保结果可复现

# 模拟长三角电商公司的营销预算与销售量数据(共120个样本)
observation_count = 120  # 设定总样本量为120家模拟公司
marketing_budget = np.random.uniform(0, 100, observation_count)  # 生成0-100范围内的均匀分布营销预算
# 定义含三次项的真实销售趋势函数:sales = 20 + 0.5x + 0.02x² - 0.0002x³
systematic_sales_trend = 20 + 0.5 * marketing_budget + 0.02 * marketing_budget**2 - 0.0002 * marketing_budget**3
market_noise = np.random.normal(0, 8, observation_count)  # 添加标准差为8的高斯随机噪声
observed_sales_revenue = systematic_sales_trend + market_noise  # 观测销售量 = 真实趋势 + 噪声

# 将特征变量转换为二维数组(sklearn要求的输入格式)
marketing_budget_features = marketing_budget.reshape(-1, 1)  # 将一维数组重塑为二维列向量(sklearn输入要求每个特征为一列)
train_end = 60  # 前60个样本用于拟合候选模型
validation_end = 90  # 中间30个样本仅用于选择多项式次数
budget_features_train = marketing_budget_features[:train_end]  # 构造训练特征
budget_features_validation = marketing_budget_features[train_end:validation_end]  # 构造验证特征
budget_features_test = marketing_budget_features[validation_end:]  # 保留末30个样本作独立测试
sales_target_train = observed_sales_revenue[:train_end]  # 构造训练目标
sales_target_validation = observed_sales_revenue[train_end:validation_end]  # 构造验证目标
sales_target_test = observed_sales_revenue[validation_end:]  # 构造未参与选模的测试目标

上面的代码完成了数据生成和训练/测试集切分。接下来,我们遍历不同复杂度的多项式模型(从1阶到11阶),分别记录每个模型在训练集和测试集上的均方误差,最终揭示过拟合现象。

# 定义要评估的多项式阶数范围:从简单线性(1阶)到极度灵活(11阶)
polynomial_degree_range = range(1, 12)  # 定义1到11阶的多项式阶数范围,用于逐步提升模型复杂度
training_mse_history = []  # 用于存储各阶数模型在训练集上的MSE
validation_mse_history = []  # 用于存储各阶数模型在验证集上的MSE

# 逐个测试从1阶到11阶的多项式模型,记录各自的训练与测试误差
for degree in polynomial_degree_range:  # 逐一遍历从1阶到11阶的多项式模型
    # 创建多项式特征变换器,将原始特征扩展为指定阶数的多项式特征
    poly_transformer = PolynomialFeatures(degree=degree, include_bias=False)  # 实例化指定阶数的多项式特征变换器(不含截距项)
    # 在训练集上拟合多项式变换参数并同时完成特征扩展
    budget_features_train_transformed = poly_transformer.fit_transform(budget_features_train)
    # 使用训练集的变换参数对测试集执行相同的多项式特征扩展
    budget_features_validation_transformed = poly_transformer.transform(budget_features_validation)  # 复用训练期变换验证集

    estimating_model = LinearRegression()  # 实例化线性回归模型
    estimating_model.fit(budget_features_train_transformed, sales_target_train)  # 在训练集上拟合模型

    # 用拟合后的模型对训练集生成预测值
    sales_train_predictions = estimating_model.predict(budget_features_train_transformed)
    # 用拟合后的模型对测试集生成预测值
    sales_validation_predictions = estimating_model.predict(budget_features_validation_transformed)  # 只在验证集比较复杂度

    # 计算当前模型在训练集上的均方误差并追加到历史列表
    training_mse_history.append(mean_squared_error(sales_target_train, sales_train_predictions))
    # 计算当前模型在测试集上的均方误差并追加到历史列表
    validation_mse_history.append(mean_squared_error(sales_target_validation, sales_validation_predictions))  # 记录选模损失

模型训练循环完成后,我们已经获得了从1阶到11阶多项式模型在训练集和测试集上的MSE变化轨迹。下面的可视化代码将生成经典的训练-测试误差曲线图,直观展示过拟合拐点。

# 创建画布并设置浅灰学术背景
fig, ax_error_curves = plt.subplots(figsize=(11, 6.5))  # 创建子图布局
ax_error_curves.set_facecolor('#F8F9FA')  # 设置画布背景色

# 绘制训练MSE曲线(绿色菱形标记,随复杂度单调下降)
ax_error_curves.plot(polynomial_degree_range, training_mse_history, 'd-', color='#10B981', linewidth=2, label='训练 MSE (Training)')
# 绘制测试MSE曲线(红色方形标记,呈U型变化)
ax_error_curves.plot(polynomial_degree_range, validation_mse_history, 's-', color='#EF4444', linewidth=3, label='验证 MSE (Model Selection)')  # 用验证集而非测试集选模

# 寻找测试MSE最低点对应的多项式阶数
optimal_degree_idx = np.argmin(validation_mse_history)  # 只根据验证损失锁定次数
# 在最优阶数位置绘制垂直虚线标注最佳模型灵活性
ax_error_curves.axvline(x=polynomial_degree_range[optimal_degree_idx], color='#4B5563', linestyle='--', alpha=0.6)
# 添加文字标注说明最佳模型位置
ax_error_curves.text(polynomial_degree_range[optimal_degree_idx]+0.2, max(validation_mse_history)*0.8, '验证集选定次数', fontweight='bold', color='#4B5563')  # 标注选择依据

ax_error_curves.set_yscale('log')  # 设置Y轴为对数刻度以便观察数量级差异
ax_error_curves.set_xlabel('模型复杂度 (多项式阶数 Degree)', fontsize=12, fontweight='bold')  # X轴标签
ax_error_curves.set_ylabel('均方误差 (MSE, 对数刻度)', fontsize=12, fontweight='bold')  # Y轴标签
ax_error_curves.set_title('训练误差与验证误差的权衡', fontsize=15, fontweight='bold', pad=15)  # 测试集保持封存
ax_error_curves.grid(True, which='both', linestyle=':', alpha=0.5)  # 添加主次网格线
ax_error_curves.legend(frameon=True, facecolor='white')  # 添加白色背景图例

plt.tight_layout()  # 自动调整布局
plt.show()  # 显示训练-测试误差对比图
一至十一阶多项式的训练 MSE 与验证 MSE 折线,标记验证误差最低的候选阶数。
图 2.5: 训练集与验证集均方误差曲线(测试集不参与选模)

选定次数后,把训练集和验证集合并重拟合,最后只访问一次测试标签:

selected_degree = list(polynomial_degree_range)[optimal_degree_idx]  # 冻结验证集选定的多项式次数
development_features = marketing_budget_features[:validation_end]  # 合并训练与验证特征
development_target = observed_sales_revenue[:validation_end]  # 合并训练与验证目标
locked_transformer = PolynomialFeatures(degree=selected_degree, include_bias=False)  # 按锁定次数建立变换
development_design = locked_transformer.fit_transform(development_features)  # 只在开发集拟合变换
locked_model = LinearRegression().fit(development_design, development_target)  # 用全部开发数据重拟合
test_design = locked_transformer.transform(budget_features_test)  # 不在测试集重拟合变换
locked_test_predictions = locked_model.predict(test_design)  # 产生唯一一次测试预测
locked_test_mse = mean_squared_error(sales_target_test, locked_test_predictions)  # 计算锁定模型测试损失
print(f'选定次数: {selected_degree}; 独立测试 MSE: {locked_test_mse:.3f}')  # 报告不参与选模的最终证据
选定次数: 3; 独立测试 MSE: 44.097

图 2.5 展示了一个重要现象:

  1. 训练MSE:随着模型灵活性增加(多项式次数增加),训练MSE单调下降。这是因为更灵活的模型能够更好地拟合训练数据,甚至包括噪音。

  2. 验证MSE:可能呈现U型,因而用于选择次数;测试 MSE 只是锁定模型的一个最终样本外估计。

这种现象揭示了统计学习的核心挑战:我们需要在模型灵活性和泛化能力之间找到最佳平衡点。

2.3.2 偏差-方差权衡 (The Bias-Variance Trade-Off)

测试MSE的U型曲线可以用偏差-方差分解(bias-variance decomposition)来解释。对于给定的\(x_0\),测试MSE可以分解为:

\[ E(y_0 - \hat{f}(x_0))^2 = \text{Var}(\hat{f}(x_0)) + [\text{Bias}(\hat{f}(x_0))]^2 + \text{Var}(\epsilon) \tag{2.6}\]

其中:

  • 方差(Variance):\(\text{Var}(\hat{f}(x_0))\)指的是如果我们使用不同训练数据估计\(\hat{f}\)\(\hat{f}(x_0)\)的变化程度。高方差意味着模型对训练数据的特定样本很敏感,容易出现过拟合。

  • 偏差(Bias):\(\text{Bias}(\hat{f}(x_0)) = E[\hat{f}(x_0)] - f(x_0)\)指的是\(\hat{f}(x_0)\)与真实\(f(x_0)\)之间的平均差异。高偏差意味着模型过于简化,容易出现欠拟合。

  • 不可约误差(Irreducible Error):\(\text{Var}(\epsilon)\)是数据中固有的噪音,无法通过改进模型来减少。

数学推导:严格的偏差-方差分解

为了深入理解这一权衡,我们提供 \(E[(y_0 - \hat{f}(x_0))^2]\) 的严格分解。这里,期望 \(E_{\mathcal{D}}\) 是针对所有的训练数据集 \(\mathcal{D}\) 的抽取以及新观测中独立的误差项 \(\epsilon\) 取的。

假设真实的生成过程为 \(y_0 = f(x_0) + \epsilon\),且 \(E[\epsilon]=0\)\(Var(\epsilon)=\sigma_\epsilon^2\)。误差 \(\epsilon\) 与用于训练 \(\hat{f}\) 的数据集 \(\mathcal{D}\) 相互独立。

首先分解测试误差: \[ \begin{aligned} E_{\mathcal{D}, \epsilon}[(y_0 - \hat{f}(x_0))^2] &= E[(f(x_0) + \epsilon - \hat{f}(x_0))^2] \\ &= E[(f(x_0) - \hat{f}(x_0))^2] + E[\epsilon^2] + 2E_{\mathcal{D}, \epsilon}[(f(x_0) - \hat{f}(x_0))\epsilon] \end{aligned} \]

由于 \(\epsilon\)\(\hat{f}(x_0)\) 独立且均值为 \(0\),交叉项期望为 \(0\)。而 \(E[\epsilon^2] = \text{Var}(\epsilon) + E[\epsilon]^2 = \sigma_\epsilon^2\)。 因此: \[ E[(y_0 - \hat{f}(x_0))^2] = E_{\mathcal{D}}[(f(x_0) - \hat{f}(x_0))^2] + \sigma_\epsilon^2 \]

接下来分解第一项。令 \(\bar{f}(x_0) = E_{\mathcal{D}}[\hat{f}(x_0)]\) 为针对所有可能训练集估计出的模型的平均预测值。我们在平方项中加减 \(\bar{f}(x_0)\)\[ \begin{aligned} E_{\mathcal{D}}[(f(x_0) - \hat{f}(x_0))^2] &= E_{\mathcal{D}}[(f(x_0) - \bar{f}(x_0) + \bar{f}(x_0) - \hat{f}(x_0))^2] \\ &= E_{\mathcal{D}}[(f(x_0) - \bar{f}(x_0))^2] + E_{\mathcal{D}}[(\bar{f}(x_0) - \hat{f}(x_0))^2] \\ &\quad + 2E_{\mathcal{D}}[(f(x_0) - \bar{f}(x_0))(\bar{f}(x_0) - \hat{f}(x_0))] \end{aligned} \] 注意到 \(f(x_0)\)\(\bar{f}(x_0)\) 都是确定性常数(非随机变量),因此: 1. 第一项:\(E_{\mathcal{D}}[(f(x_0) - \bar{f}(x_0))^2] = (f(x_0) - \bar{f}(x_0))^2 = [\text{Bias}(\hat{f}(x_0))]^2\) 2. 第二项:这是估计值 \(\hat{f}(x_0)\) 偏离其均值 \(\bar{f}(x_0)\) 的平方期望,即 \(\text{Var}(\hat{f}(x_0))\) 3. 交叉项:\(2(f(x_0) - \bar{f}(x_0)) \cdot E_{\mathcal{D}}[\bar{f}(x_0) - \hat{f}(x_0)]\)。因为 \(E_{\mathcal{D}}[\hat{f}(x_0)] = \bar{f}(x_0)\),所以 \(E_{\mathcal{D}}[\bar{f}(x_0) - \hat{f}(x_0)] = 0\),交叉项为 \(0\)

最终,合并上述公式得到: \[ E[(y_0 - \hat{f}(x_0))^2] = [\text{Bias}(\hat{f}(x_0))]^2 + \text{Var}(\hat{f}(x_0)) + \sigma_\epsilon^2 \]

Tip: 直观理解偏差和方差

想象你在练习射击靶子:

  • 低偏差,低方差:所有子弹都打在靶心附近(理想情况)
  • 低偏差,高方差:子弹分布很分散,但平均位置在靶心(模型过拟合,对训练数据敏感)
  • 高偏差,低方差:子弹很集中,但都偏离靶心(模型欠拟合,过于简化)
  • 高偏差,高方差:子弹分散且偏离靶心(模型很差)

在统计学习中,我们的目标是找到偏差和方差的最佳平衡,使总误差最小。

图 2.6 展示了偏差、方差和测试MSE随模型灵活性变化的典型模式。

为了将这个抽象的误差“权衡”理论具象化,下面的代码构建了一个系统性数值模拟实验。我们分别在代码中定义了随灵活性的增加而发生衰减的偏差(Bias)平方衰减函数曲线,以及随之呈数学幂律级数恶化膨胀的方差(Variance)曲线。加上代表数据固有天花板的常数项(不可约误差基座),我们将这三部分特征矩阵数值简单点对点相加,便得到了用于评估预测泛化能力的预期的总测试误差序列。将其图表化后的结果在幕布上清晰地显示出该体系的总误差最终呈现“U型”峡谷——这表明,商业实战中既不要在左端的简陋模型中“欠拟合”,也不要在右端的极端复杂中“过拟合”,最佳的统计学习应用就是凭借交叉验证精确落在底部那个“最优平衡点”。

import numpy as np  # 导入numpy用于数值模拟计算
import matplotlib.pyplot as plt  # 导入matplotlib绘图库用于可视化

# 生成从1到10的100个等间距灵活性值,模拟模型复杂度的连续变化
flexibility_axis = np.linspace(1, 10, 100)  # 生成等间隔序列

# 定义偏差平方曲线:随模型灵活性增加而按反比例函数衰减
squared_bias_curve = 20 / flexibility_axis  # 偏差平方按反比例衰减:灵活性越高,模型对真实函数的逼近越准
# 定义方差曲线:随灵活性增加而按1.8次幂律增长
variance_curve = 0.8 * (flexibility_axis - 1)**1.8  # 方差随灵活性提升而加速膨胀,反映模型对训练样本的过度敏感
# 定义不可约误差(贝叶斯误差下界):数据固有噪声,与模型选择无关
irreducible_noise_floor = 2.0  # 设定不可约误差为常数2.0,代表数据固有噪声的下界

# 计算总测试误差 = 偏差² + 方差 + 不可约误差
total_ms_error = squared_bias_curve + variance_curve + irreducible_noise_floor  # 三者逐点相加得到总测试MSE序列

以上代码完成了偏差-方差权衡各分量的数值模拟。偏差平方随灵活性的增加而衰减,方差则随灵活性的增加而膨胀——两者此消彼长的关系构成了统计学习中最核心的权衡原理。下面我们将这些理论曲线可视化,直观展示总误差的”U型”谷底现象。

# 创建画布并设置浅灰学术风格背景
fig, ax_bias_var = plt.subplots(figsize=(12, 7.5))  # 创建子图布局
ax_bias_var.set_facecolor('#F8FAFC')  # 设置浅灰背景色

# 绘制偏差平方曲线(红色,随灵活性增加而下降)
ax_bias_var.plot(flexibility_axis, squared_bias_curve, color='#DC2626', linewidth=2.5, label='偏差平方 (Bias²)')
# 绘制方差曲线(蓝色,随灵活性增加而上升)
ax_bias_var.plot(flexibility_axis, variance_curve, color='#2563EB', linewidth=2.5, label='方差 (Variance)')
# 绘制不可约误差水平线(灰色虚线,恒定不变)
ax_bias_var.axhline(y=irreducible_noise_floor, color='#94A3B8', linestyle='--', label='不可约误差 (Var(ε))')
# 绘制总测试MSE曲线(黑色粗线,呈U型)
ax_bias_var.plot(flexibility_axis, total_ms_error, color='#1E293B', linewidth=4, label='总测试 MSE')

# 在总误差曲线上寻找最低点对应的灵活性值
optimal_idx = np.argmin(total_ms_error)  # 获取最小误差的索引
best_flexibility = flexibility_axis[optimal_idx]  # 对应的最优模型灵活性
min_error_level = total_ms_error[optimal_idx]  # 最小总误差值

# 用金色圆点标注U型曲线的最低点(最优拟合点)
ax_bias_var.plot(best_flexibility, min_error_level, 'o', color='gold', markersize=12, markeredgecolor='black')
# 添加带箭头的文字标注指向最优拟合点
ax_bias_var.annotate('最优拟合点', xy=(best_flexibility, min_error_level), xytext=(best_flexibility+0.5, min_error_level+5),
                    # 设置箭头样式:黑色实心箭头,收缩5%,线宽1
                    arrowprops=dict(facecolor='black', shrink=0.05, width=1), fontsize=11, fontweight='bold')

# 设置Y轴范围为0-25以确保完整显示所有曲线
ax_bias_var.set_ylim(0, 25)  # 设置子图Y轴范围
ax_bias_var.set_xlabel('模型灵活性 (Model Flexibility) →', fontsize=12, fontweight='bold')  # X轴标签
ax_bias_var.set_ylabel('误差期望 E(Test MSE) →', fontsize=12, fontweight='bold')  # Y轴标签
ax_bias_var.set_title('偏置-方差权衡的理论构架', fontsize=15, fontweight='bold', pad=15)  # 图表标题
ax_bias_var.grid(True, linestyle=':', alpha=0.4)  # 添加半透明点线网格
ax_bias_var.legend(frameon=True, shadow=True, facecolor='white')  # 添加带阴影的白色背景图例

plt.tight_layout()  # 自动调整子图间距
plt.show()  # 显示偏差-方差权衡图
随模型灵活性变化的偏差平方、方差、不可约误差和总测试误差四条教学示意曲线。
图 2.6: 偏差-方差权衡示意图

关键要点:

  1. 灵活性增加 → 偏差减少:更灵活的模型能够更好地拟合数据中的模式,减少系统性偏差。

  2. 灵活性增加 → 方差增加:更灵活的模型对训练数据的特定样本更敏感,导致不同训练数据产生的模型差异更大。

  3. 最优模型:在偏差和方差之间找到最佳平衡,使总测试MSE最小。

  4. 不可约误差:无论模型多么灵活,测试MSE都不会低于不可约误差的水平,这是因为数据中存在固有的随机噪音。

2.3.3 分类设置 (The Classification Setting)

在分类问题中,我们关注的是错误率(error rate)或准确率(accuracy):

\[ \text{错误率} = \frac{1}{n} \sum_{i=1}^{n} I(y_i \neq \hat{y}_i) \tag{2.7}\] \[ \text{准确率} = 1 - \text{错误率} \tag{2.8}\]

其中,\(I(\cdot)\)是指示函数,当条件为真时取1,否则取0。\(y_i\)是真实的类别标签,\(\hat{y}_i\)是预测的类别标签。

与回归类似,我们也关心训练错误率和测试错误率。一个好的分类模型应该在训练数据上有较低的错误率,同时在测试数据上也有良好的表现。

Clarification on ‘Accuracy’ vs ‘Precision’

在日常语言中,’accuracy’和’precision’常常互换使用,但在统计学和机器学习中,它们有明确的区别:

  • 准确率 (Accuracy):正确预测的样本占总样本的比例 \[ \text{Accuracy} = \frac{TP + TN}{TP + TN + FP + FN} \]

  • 精确率 (Precision):预测为正类的样本中,真正为正类的比例 \[ \text{Precision} = \frac{TP}{TP + FP} \]

  • 召回率 (Recall/Sensitivity):真正为正类的样本中,被正确预测为正类的比例 \[ \text{Recall} = \frac{TP}{TP + FN} \]

其中,TP(True Positive)是真正例,TN(True Negative)是真负例,FP(False Positive)是假正例,FN(False Negative)是假负例。

在类别不平衡的情况下(例如,罕见病检测,正类样本很少),准确率可能具有误导性。在这种情况下,精确率和召回率可能更有意义。

补充说明:为什么同一个模型会同时“准确”又“不好用”

很多初学者第一次看到这三个指标时会困惑:如果模型准确率已经很高,为什么老师还要继续追问精确率和召回率?关键原因在于,这三个指标回答的根本不是同一个问题。

设想一个典型的银行风控场景:1000个贷款申请人中,真正高风险客户只有50个,另外950个都是正常客户。如果一个模型非常保守,只把其中80个人标记为“高风险”,并且这80人里只有30个真的会违约,那么它的四格计数就是:TP=30,FP=50,FN=20,TN=900。

在这个例子里:

  • 准确率是 \(\frac{30+900}{1000}=93\%\),看起来很高,因为绝大多数正常客户都被判对了;
  • 精确率是 \(\frac{30}{30+50}=37.5\%\),表示被模型拦下来的客户里,真正有风险的比例并不高;
  • 召回率是 \(\frac{30}{30+20}=60\%\),表示真正危险的客户里,仍有40%漏了过去。

这说明:

  • 准确率关注“总体上判对了多少”;
  • 精确率关注“你发出的警报有多可信”;
  • 召回率关注“真正危险的人你抓住了多少”。

在商业决策里,这三者往往对应不同成本:

  • 如果错杀优质客户的代价很高,就要更重视精确率;
  • 如果漏掉高风险客户的代价很高,就要更重视召回率;
  • 如果两类错误成本大致相当,而且类别也相对平衡,准确率才更有参考价值。

进一步地,很多分类器并不是直接输出“0或1”,而是先输出一个概率,再由我们设定阈值决定是否判为正类。阈值调高时,模型通常会变得更谨慎,精确率上升、召回率下降;阈值调低时,则往往相反。这也是为什么在后续分类章节里,我们还要继续讨论 ROC 曲线、AUC 和 PR-AUC:它们帮助我们观察模型在所有可能阈值下的整体表现,而不是只盯住某一个阈值下的单点成绩。

2.4 本章小结 (Chapter Summary)

本章介绍了统计学习的基础概念:

  1. 统计学习的定义:统计学习是一组用于估计函数\(f\)的方法,该函数将输入变量\(X\)映射到输出变量\(Y\)

  2. 估计\(f\)的目的

    • 预测:使用\(\hat{f}(X)\)来预测\(Y\)
    • 推断:理解\(Y\)如何随\(X\)的变化而变化
  3. 估计\(f\)的方法

    • 参数化方法:假设函数形式,然后估计参数
    • 非参数化方法:不假设函数形式,让数据”说话”
  4. 预测准确度与模型可解释性的权衡:更灵活的模型通常更难解释,但可能产生更准确的预测。

  5. 评估模型准确度

    • 在回归问题中使用均方误差(MSE)
    • 在分类问题中使用错误率或准确率
  6. 偏差-方差权衡:测试误差可以分解为偏差平方、方差和不可约误差三部分。最优模型在偏差和方差之间找到最佳平衡。

在接下来的章节中,我们将详细讨论具体的统计学习方法,包括线性回归、分类方法、重采样技术、线性模型选择与正则化、非线性模型等。

2.5 理论来源与前沿

本章的统一框架——用损失函数刻画拟合质量、用复杂度刻画模型灵活度、用测试误差衡量泛化表现——一方面继承了统计学的风险最小化思想,另一方面与计算学习理论中的 PAC/VC 观点相呼应。偏差-方差分解把‘模型太简单’与‘模型太复杂’两类误差统一到同一个分解式中,从而为模型选择、正则化与误差评估提供了可操作的语言 (James 等 2023年; Hastie 等 2009年)

近年来的研究前沿主要集中在三个方向:

  1. 可解释性与可审计性:在金融风控、信贷审批、保险定价等场景中,模型不仅要准确,还要能解释、可复核。
  2. 分布漂移与稳健学习:训练分布与部署分布不一致时,需要稳健估计、域自适应与在线更新机制。
  3. 因果视角的学习:将‘预测’与‘干预效果评估’区分开,在政策评估、营销增量与运营实验中尤为关键。

2.6 贯穿项目里程碑 M02

本里程碑与根级 小节 6.3 的 M02 是同一交付点,预计 60 分钟,并冻结后续 M03—M09 唯一监督任务 a-share-drawdown-20d-v1:在公司日 \(t\) 收盘后,用当日及以前可得的 A 股价量/财务特征,预测该公司未来 20 个交易日内最大回撤是否不高于 \(-10\%\)

契约项 可审计要求
输入 逻辑快照名 a-share-project-snapshot-v1、带生效区间的证券状态历史、无结果公司交易日日历、仅开发标签,以及事前 validation_origin/test_origin/freeze_date
样本与标签 在每个 prediction_date 按半开生效区间判定普通 A 股、板块、ST、上市与退市状态,不得用冻结日状态回溯删行;每公司至少 20 个既往交易日 warm-up;label_date 必须等于公司序列未来第 20 个交易日;末端不足 20 日排除
输出 幂等的 manifest、逐公司日 eligibility roster、逐公司日排除表、train/validation 开发制品、不含 y 的测试键和机器合同;输出 roster/exclusions/member/development/test 及日历、标签、状态历史的真实文件 SHA-256
失败条件 状态区间缺口、重叠或一日多状态;总体规则、键、阈值、窗口或标签终点不唯一;单股伪总体;warm-up 不足;同名文件不同字节;特征可得日晚于预测日;用验证/测试标签定义基线
量规 D 数据与时间边界 60%,C 结论边界 40%;未验证快照状态须如实标注

2.7 练习

2.7.1 概念题

  1. [核心|难度:1|时间:10分钟|分值:10|项目:无] 预测与推断:用你自己的语言解释统计学习中“预测”与“推断”的区别。请各举一个中国金融市场的例子(如预测股价与研究拆股效应)。

  2. [核心|难度:2|时间:15分钟|分值:15|项目:无] 偏差-方差权衡:解释为什么增加模型灵活性通常会降低训练误差,但可能导致测试误差的显著上升。

  3. [核心|难度:1|时间:10分钟|分值:10|项目:无] 不可约误差:在 式 2.1 中,为什么我们无法消除 \(\epsilon\)?列举两个在分析 A 股上市公司财报时,可能导致不可约误差的现实因素。

  4. [补救|难度:1|时间:10分钟|分值:10|项目:无] 过度灵活的风险:如果一个模型在训练集上的 MSE 为 0,但在验证集上的表现极差,这处于什么状态?你会建议通过减少特征还是增加特征来缓解?

2.7.2 应用题

  1. [核心|难度:2|时间:35分钟|分值:25|项目:无] 本地行情探索 (回归任务)
    • 使用 Python 读取本地 stock/stock_price_post_adjusted.h5 数据。
    • 选择长三角地区任意一家上市公司。
    • 构造预测任务:使用过去 5 个交易日的对数收益率来预测下一日的对数收益率。
    • 比较简单的线性回归与一个你认为更灵活的模型(如 KNN,K=5)在测试集上的表现。
  2. [核心|难度:3|时间:60分钟|分值:40|项目:M02] 冻结贯穿任务与唯一切分:为 a-share-drawdown-20d-v1 提交问题陈述、数据合同和 project-manifest-v1。必须从 BOOK_PROJECT_SECURITY_STATUS_HISTORY 的生效区间逐 prediction_date 核验普通 A 股、板块、ST、上市与退市状态,严禁用冻结日 is_st/is_delisted 回筛历史;同时核验每公司 20 日 warm-up、公司序列第 20 个未来交易日、末端 NA、特征可得日和事前切分原点。幂等保存逐行 eligibility roster、逐行排除表、manifest、开发标签、测试键与合同,并输出每项及真实输入文件 SHA-256。交付物只向 M02—M08 暴露 train/validation 标签和不含 y 的 test 键;训练事件率/多数类只由 train 计算。

2.7.3 理论题

  1. [拓展|难度:3|时间:25分钟|分值:20|项目:无] 均方误差分解:在平方损失下,证明: \[ E[(Y - \hat{f}(X))^2] = \text{Bias}^2(\hat{f}(X)) + \text{Var}(\hat{f}(X)) + \text{Var}(\epsilon) \] (假设 \(\epsilon\)\(X\) 独立且 \(E(\epsilon) = 0\))。

  2. [拓展|难度:2|时间:15分钟|分值:15|项目:无] 样本均值的最优性:证明在没有预测变量的情况下,\(Y\) 的平方损失最小化估计量是样本均值 \(\bar{Y}\)

2.8 练习参考解答

展开第 2 章完整解答与评分键

统一评分与验收说明:按题面元数据分值计分;概念/证明题分配给定义、中间步骤与边界,应用题分配给数据入口、时间切分、基线、模型输出与条件解释。浮点结果相对容差 \(10^{-6}\);随机抽样使用题定种子,模型排序以当次输出为准。常见失败是将残差当作真误差、用测试集选模、未留出一期标签边界,或把单次 MSE 排序写成普遍模型规律。

2.8.1 概念题解答

  1. 预测与推断

    • 预测:关注 \(\hat{Y}\) 的准确度,将模型视为黑盒。例子:基于技术指标预测明日上证综指及其区间。
    • 关联推断:在明确模型与抽样假设下估计 \(X\)\(Y\) 的条件关联及不确定性。若问题是“实施股权激励是否提升 ROE”,还必须说明随机实验、自然实验或其他可辩护的因果识别设计。
  2. 偏差-方差权衡:更灵活的模型能够追踪训练数据中的每一处波动(低偏差),但这种“过度贴合”往往捕捉了仅属于该样本的随机噪声(高方差)。当应用于新样本时,这些噪声模式不复存在,导致预测失败。

  3. 不可约误差\(\epsilon\) 包含了未被模型观测到的所有变量。因素包括:(1) 企业报表可能存在的会计处理差异造成的随机测量误差;(2) 外部突发黑天鹅事件(如政策突变或自然灾害)。

  4. 状态与建议:这是典型的过拟合 (Overfitting) 状态。建议通过减少特征(降维)、实施惩罚项(正则化)或增加训练样本量来缓解。

2.8.2 应用题解答

  1. 股价预测代码实现

这个应用题比较线性回归、K-近邻与训练期均值基线对下一交易日收益率的预测。滞后收益率只使用预测时点以前的信息;由于标签是下一日收益率,训练集与测试集之间还留出一行隔离带。一次前向留出的 MSE 只能说明这些模型在该测试期间的相对表现,不能单独证明某种模型“过拟合”或某种市场机制成立。

import pandas as pd  # 导入pandas库用于数据框操作
import numpy as np  # 导入numpy库用于数值计算
from pathlib import Path  # 导入跨平台路径对象
from sklearn.linear_model import LinearRegression  # 导入线性回归模型
from sklearn.neighbors import KNeighborsRegressor  # 导入K-近邻回归模型
from sklearn.metrics import mean_squared_error  # 导入MSE评估指标函数
import os  # 导入os模块用于跨平台路径处理

# 根据操作系统自动选择本地数据存储根目录
# 根据操作系统设置数据根目录路径
BOOK_DATA_DIR = Path(os.environ['BOOK_DATA_DIR']).expanduser().resolve()  # 从必需环境变量解析数据根目录
# 拼接后复权股价数据文件的完整路径
PRICE_DATA_FILE = BOOK_DATA_DIR / 'stock/stock_price_post_adjusted.h5'  # 拼接后复权股价数据文件路径

shanghai_auto_data = pd.read_hdf(  # 从本地h5文件中按股票代码选择性读取上汽集团的后复权行情,避免全量载入全市场数据
    PRICE_DATA_FILE,  # 指定后复权股价数据文件路径
    where="order_book_id='600104.XSHG'",  # 仅保留上汽集团的历史交易记录
    columns=['date', 'order_book_id', 'close']  # 只读取时间序列建模真正需要的日期、股票代码与收盘价列
).reset_index()  # 将MultiIndex还原为普通列,便于后续排序与特征构造

# 对选择性读取后的上汽集团数据创建独立副本,确保后续新增收益率列时不会触发链式赋值问题
shanghai_auto_data = shanghai_auto_data.copy()
shanghai_auto_data = shanghai_auto_data.sort_values('date')  # 按交易日期升序排列

# 计算当日对数收益率:ln(P_t) - ln(P_{t-1})
shanghai_auto_data['current_return'] = np.log(shanghai_auto_data['close']).diff()  # 计算相邻交易日收盘价的对数差分作为日收益率
# 构造过去1到5天的滞后收益率特征(用历史信息预测未来)
for lag in range(1, 6):  # 逐一构造第1到5天的滞后收益率特征
    shanghai_auto_data[f'lag_ret_{lag}'] = shanghai_auto_data['current_return'].shift(lag)  # 第lag天的历史收益率

# 定义预测目标:明天的收益率(将当日收益率向前位移一期)
shanghai_auto_data['target_return'] = shanghai_auto_data['current_return'].shift(-1)  # 将当日收益率向前平移一期,构造“明日收益率”预测目标

# 删除因滞后和位移操作产生的缺失值行
modeling_final_df = shanghai_auto_data.dropna()  # 删除缺失值

接下来,我们将清洗后的数据按时间顺序切分为训练集(前80%)和测试集(后20%),并分别使用线性回归和 KNN (K=5) 两种模型进行预测。通过比较测试集上的均方误差(MSE),我们可以直观看到在金融时间序列这类高噪声数据上,简单线性模型与灵活非参数模型的泛化表现差异。

# 按80/20比例将数据切分为训练集和测试集,并隔离跨边界的一步标签
split_point = int(len(modeling_final_df) * 0.8)  # 计算80%分割点的整数索引,用于时间序列训练/测试切分
purge_rows = 1  # 标签使用下一日收益率,因此在切分边界留出一行
train_slice = modeling_final_df.iloc[:split_point - purge_rows]  # 边界前且不含隔离行的数据作为训练集
test_slice = modeling_final_df.iloc[split_point:]  # 后20%作为测试数据
assert train_slice['date'].max() < test_slice['date'].min()  # 验证预测时点严格按时间分离

# 定义用于建模的5个滞后收益率特征列名
lagged_return_columns = [f'lag_ret_{i}' for i in range(1, 6)]  # 构建5个滞后收益率的列名列表作为模型输入特征
# 提取训练集的特征矩阵与目标向量
return_features_train, target_return_train = train_slice[lagged_return_columns], train_slice['target_return']
# 提取测试集的特征矩阵与目标向量
return_features_test, target_return_test = test_slice[lagged_return_columns], test_slice['target_return']

# 模型A:经典线性回归——假设收益率与滞后特征之间为线性关系
linear_fit = LinearRegression().fit(return_features_train, target_return_train)  # 训练/拟合模型
ls_predictions = linear_fit.predict(return_features_test)  # 在测试集上生成线性模型预测

# 模型B:KNN回归(K=5)——通过邻域平均实现非参数化的灵活预测
knn_regressor = KNeighborsRegressor(n_neighbors=5).fit(return_features_train, target_return_train)  # 训练KNN回归模型
knn_predictions = knn_regressor.predict(return_features_test)  # 在测试集上生成KNN模型预测

# 输出两个模型在测试集上的均方误差进行对比
print(f'Linear MSE: {mean_squared_error(target_return_test, ls_predictions):.8f}')  # 计算均方误差
print(f'KNN MSE: {mean_squared_error(target_return_test, knn_predictions):.8f}')  # 计算均方误差
mean_predictions = np.repeat(target_return_train.mean(), len(target_return_test))  # 构造训练期均值基线
print(f'Mean baseline MSE: {mean_squared_error(target_return_test, mean_predictions):.8f}')  # 输出基线误差
Linear MSE: 0.00034387
KNN MSE: 0.00046225
Mean baseline MSE: 0.00034157

请以实际打印值作答:先报告三个测试 MSE,再说明哪一个最低以及相对训练期均值基线是否有改进。若 KNN 在这一留出期较差,可能原因包括局部邻域方差、尺度或市场状态变化;若它较好,也不能据此宣称存在稳定非线性。要判断差异是否可重复,应增加多个保持时间顺序的前向窗口,并报告各窗口误差及其离散程度。

  1. a-share-drawdown-20d-v1 合同与唯一 manifest

    • 决策与键:公司日 \(t\) 收盘后作预测;唯一键为 (order_book_id, prediction_date),键重复即失败;证券资格只能取覆盖 \(t\) 的状态历史区间。
    • 标签:按公司交易日排序,label_date\(t\) 后第 20 个交易日;未来 20 日最低收盘价相对 \(t\) 日收盘价的回撤不高于 \(-10\%\)y=1,否则 y=0。不足 20 日的末端行保持 NA,不能填成 0。
    • 特征与边界:特征及其 info_date 不得晚于 prediction_date;任何训练行的 label_date 必须早于下一集合的首个 prediction_date
    • 基线与输出:只从训练标签计算事件率,并由训练多数类锁定常数类别;M02 唯一创建 project-manifest-v1,后续章节只能核验原字节。roster 与 exclusions 都按公司日冻结,测试制品只含键,不含 y

本答案可在 fresh kernel 顺序运行。课程运行器必须显式提供 BOOK_PROJECT_SECURITY_STATUS_HISTORYBOOK_PROJECT_KEY_CALENDARBOOK_PROJECT_DEVELOPMENT_LABELSBOOK_PROJECT_ARTIFACT_DIRBOOK_VALIDATION_ORIGINBOOK_TEST_ORIGINBOOK_PROJECT_FREEZE_DATE。状态历史以 [effective_from, effective_to) 半开区间记录每只证券当时的证券类型、板块、ST、上市和退市状态;每个公司日必须恰好命中一个区间,否则 fail closed。运行器不得向 M02—M08 提供测试标签或测试期未来价格。下面逐 prediction_date 冻结 eligibility 与排除原因;后来发生的 ST 或退市只影响其生效日后的行,不得删除更早的合格行。任一输出已存在时,只有待写字节与原字节完全相同才允许继续。

import hashlib  # 计算输入与输出的真实字节指纹
import json  # 生成跨平台稳定合同字节
import os  # 读取课程运行器显式入口
from pathlib import Path  # 使用跨平台制品路径
import numpy as np  # 按排除规则生成可审计原因
import pandas as pd  # 读取并核验冻结表

def stable_csv_bytes(frame):  # 统一成员类制品的序列化口径
    return frame.to_csv(index=False, date_format='%Y-%m-%d', lineterminator='\n').encode('utf-8')  # 固定日期、换行与编码

def write_same_or_create(path, payload):  # 禁止同名制品被静默覆盖
    if path.exists() and path.read_bytes() != payload:  # 已有制品必须保持逐字节不变
        raise RuntimeError(f'status=artifact_conflict,path={path.name}')  # 输入变化时关闭流程
    if not path.exists():  # 首次运行才创建冻结制品
        path.write_bytes(payload)  # 保存已经规范化的唯一字节
calendar_path = Path(os.environ['BOOK_PROJECT_KEY_CALENDAR']).resolve()  # 解析无标签公司交易日日历
development_label_path = Path(os.environ['BOOK_PROJECT_DEVELOPMENT_LABELS']).resolve()  # 解析仅开发标签源
status_history_path = Path(os.environ['BOOK_PROJECT_SECURITY_STATUS_HISTORY']).resolve()  # 解析带生效区间的证券状态历史
artifact_dir = Path(os.environ['BOOK_PROJECT_ARTIFACT_DIR']).resolve()  # 解析唯一制品桶
validation_origin = pd.Timestamp(os.environ['BOOK_VALIDATION_ORIGIN'])  # 读取标签查看前登记的验证原点
test_origin = pd.Timestamp(os.environ['BOOK_TEST_ORIGIN'])  # 读取标签查看前登记的测试原点
freeze_date = pd.Timestamp(os.environ['BOOK_PROJECT_FREEZE_DATE'])  # 绑定输入文件的知识截止日
assert validation_origin < test_origin <= freeze_date  # 拒绝倒置或越过冻结日的设计
assert all(path.is_file() for path in [calendar_path, development_label_path, status_history_path])  # 缺任一权威输入立即失败
project_calendar = pd.read_csv(calendar_path, parse_dates=['prediction_date', 'label_date'])  # 读取不含结果的日历
development_labels = pd.read_csv(development_label_path, parse_dates=['prediction_date'])  # 读取仅开发标签
security_status_history = pd.read_csv(status_history_path, parse_dates=['effective_from', 'effective_to'])  # 读取逐区间历史状态
key_columns = ['order_book_id', 'prediction_date']  # 冻结唯一公司日键
status_columns = ['order_book_id', 'effective_from', 'effective_to', 'security_type', 'board', 'is_st', 'is_listed', 'is_delisted']  # 冻结状态历史模式
assert set(project_calendar.columns) == set(key_columns + ['label_date'])  # 禁止日历携带结果代理
assert set(development_labels.columns) == set(key_columns + ['y'])  # 禁止开发源夹带测试或特征
assert set(security_status_history.columns) == set(status_columns)  # 禁止状态定义隐式漂移
assert not project_calendar.duplicated(key_columns).any()  # 公司交易日必须唯一
assert project_calendar['prediction_date'].max() <= freeze_date  # 禁止日历越过知识截止日
security_status_history = security_status_history.sort_values(['order_book_id', 'effective_from']).reset_index(drop=True)  # 固定状态事件顺序
assert security_status_history['effective_from'].notna().all()  # 每个状态必须有明确生效日
assert security_status_history['effective_from'].le(freeze_date).all()  # 禁止消费冻结日后才生效的状态
assert not security_status_history.duplicated(['order_book_id', 'effective_from']).any()  # 同一时点不得出现竞争状态
assert security_status_history['effective_to'].isna().eq(security_status_history.groupby('order_book_id').cumcount(ascending=False).eq(0)).all()  # 每只证券仅最后区间可开放
next_status_start = security_status_history.groupby('order_book_id')['effective_from'].shift(-1)  # 找到同证券下一状态起点
closed_status_rows = next_status_start.notna()  # 识别所有非末状态区间
assert security_status_history.loc[closed_status_rows, 'effective_to'].equals(next_status_start[closed_status_rows])  # 要求区间首尾连续且无缺口重叠
status_flags = security_status_history[['is_st', 'is_listed', 'is_delisted']]  # 汇集必须为布尔值的状态列
assert status_flags.isin([True, False]).all().all()  # 拒绝字符串、空值或三态标志
assert not (security_status_history['is_listed'] & security_status_history['is_delisted']).any()  # 上市与已退市不得同时成立
assert security_status_history['order_book_id'].nunique() > 1  # 拒绝单股伪总体
project_calendar = project_calendar.sort_values(key_columns).reset_index(drop=True)  # 固定公司内交易日顺序
expected_label_date = project_calendar.groupby('order_book_id')['prediction_date'].shift(-20)  # 重算公司第20个未来交易日
assert project_calendar['label_date'].equals(expected_label_date)  # 拒绝自然日或跨公司标签终点
project_calendar['warmup_observations'] = project_calendar.groupby('order_book_id').cumcount()  # 计算预测日前可用公司交易日数
calendar_status_candidates = project_calendar.merge(security_status_history, on='order_book_id', how='left', validate='many_to_many')  # 枚举公司日可能命中的历史区间
is_effective_status = calendar_status_candidates['prediction_date'].ge(calendar_status_candidates['effective_from']) & (calendar_status_candidates['effective_to'].isna() | calendar_status_candidates['prediction_date'].lt(calendar_status_candidates['effective_to']))  # 应用半开生效区间
calendar_with_status = calendar_status_candidates.loc[is_effective_status].copy()  # 只保留预测日当时有效的状态
status_match_counts = calendar_with_status.groupby(key_columns).size()  # 统计每个公司日的有效状态数
assert len(status_match_counts) == len(project_calendar) and status_match_counts.eq(1).all()  # 状态缺口或重叠均立即失败
assert set(map(tuple, status_match_counts.index)) == set(map(tuple, project_calendar[key_columns].to_numpy()))  # 拒绝任何日历键未被状态历史覆盖
calendar_with_status = calendar_with_status.sort_values(key_columns).reset_index(drop=True)  # 固定逐行资格审计顺序
calendar_with_status['is_status_eligible'] = calendar_with_status['security_type'].eq('A股普通股') & calendar_with_status['board'].isin(['主板', '创业板', '科创板']) & ~calendar_with_status['is_st'] & calendar_with_status['is_listed'] & ~calendar_with_status['is_delisted']  # 逐预测日判断证券资格
calendar_with_status['is_sample_eligible'] = calendar_with_status['is_status_eligible'] & calendar_with_status['warmup_observations'].ge(20) & calendar_with_status['label_date'].notna()  # 合并状态、warm-up与标签终点规则
exclusion_conditions = [calendar_with_status['security_type'].ne('A股普通股'), ~calendar_with_status['board'].isin(['主板', '创业板', '科创板']), calendar_with_status['is_st'], calendar_with_status['is_delisted'], ~calendar_with_status['is_listed'], calendar_with_status['warmup_observations'].lt(20), calendar_with_status['label_date'].isna()]  # 冻结互斥原因的判断优先级
exclusion_labels = ['not_a_share_common', 'excluded_board', 'st_on_prediction_date', 'delisted_on_prediction_date', 'not_listed_on_prediction_date', 'insufficient_warmup', 'missing_label_horizon']  # 使用逐预测日而非冻结日原因
calendar_with_status['exclusion_reason'] = np.select(exclusion_conditions, exclusion_labels, default='included')  # 为每个公司日保存唯一状态
assert calendar_with_status['is_sample_eligible'].eq(calendar_with_status['exclusion_reason'].eq('included')).all()  # 资格布尔值必须与原因闭合
roster_columns = key_columns + ['label_date', 'effective_from', 'effective_to', 'security_type', 'board', 'is_st', 'is_listed', 'is_delisted', 'warmup_observations', 'is_status_eligible', 'is_sample_eligible', 'exclusion_reason']  # 冻结逐行 eligibility schema
frozen_roster = calendar_with_status[roster_columns].copy()  # 保存所有公司日的 point-in-time 资格账本
exclusions = frozen_roster.loc[~frozen_roster['is_sample_eligible']].copy()  # 保存逐公司日排除证据
eligible_calendar = calendar_with_status.loc[calendar_with_status['is_sample_eligible'], key_columns + ['label_date']].copy()  # 只让当时合格行进入下游
included_ids = set(eligible_calendar['order_book_id'])  # 核验实际进入项目的公司数
assert len(included_ids) > 1  # 拒绝单股伪总体
train_mask = eligible_calendar['prediction_date'].lt(validation_origin) & eligible_calendar['label_date'].lt(validation_origin)  # 防止训练结局跨入验证期
validation_mask = eligible_calendar['prediction_date'].ge(validation_origin) & eligible_calendar['prediction_date'].lt(test_origin) & eligible_calendar['label_date'].lt(test_origin)  # 防止验证结局跨入测试期
test_mask = eligible_calendar['prediction_date'].ge(test_origin)  # 测试只冻结键与终点日
eligible_calendar['split'] = pd.NA  # 初始化互斥成员字段
eligible_calendar.loc[train_mask, 'split'] = 'train'  # 分配训练成员
eligible_calendar.loc[validation_mask, 'split'] = 'validation'  # 分配验证成员
eligible_calendar.loc[test_mask, 'split'] = 'test'  # 分配测试成员
manifest = eligible_calendar.dropna(subset=['split'])[key_columns + ['label_date', 'split']].sort_values(key_columns).reset_index(drop=True)  # 形成唯一权威成员表
assert set(manifest['split']) == {'train', 'validation', 'test'}  # 拒绝空集合
development_manifest = manifest[manifest['split'].isin(['train', 'validation'])].copy()  # 以manifest为开发权威左表
development_panel = development_manifest.merge(development_labels, on=key_columns, how='left', validate='one_to_one', indicator=True)  # 审计标签精确覆盖
assert development_panel['_merge'].eq('both').all() and len(development_panel) == len(development_manifest)  # 禁止缺标签静默缩样
assert set(map(tuple, development_labels[key_columns].to_numpy())) == set(map(tuple, development_manifest[key_columns].to_numpy()))  # 禁止额外开发标签
development_panel = development_panel.drop(columns='_merge')  # 删除仅用于审计的合并标记
assert development_panel['y'].notna().all() and set(development_panel['y'].unique()) <= {0, 1}  # 标签必须完整且二元
sealed_test_keys = manifest.loc[manifest['split'].eq('test'), key_columns + ['label_date']].copy()  # 测试制品不含结果
train_labels = development_panel.loc[development_panel['split'].eq('train'), 'y']  # 只用训练标签定义基线
training_event_rate = train_labels.mean()  # 冻结训练事件率
training_majority = int(training_event_rate >= 0.5)  # 冻结训练多数类
manifest_bytes = stable_csv_bytes(manifest)  # 生成权威成员字节
roster_bytes = stable_csv_bytes(frozen_roster)  # 生成权威总体字节
exclusion_bytes = stable_csv_bytes(exclusions)  # 生成完整排除字节
development_bytes = stable_csv_bytes(development_panel)  # 生成仅开发标签字节
test_bytes = stable_csv_bytes(sealed_test_keys)  # 生成无标签测试键字节
member_bytes = stable_csv_bytes(manifest[key_columns + ['split']])  # 单独计算成员键指纹
artifact_hashes = {'manifest_sha256': hashlib.sha256(manifest_bytes).hexdigest(), 'roster_sha256': hashlib.sha256(roster_bytes).hexdigest(), 'exclusions_sha256': hashlib.sha256(exclusion_bytes).hexdigest(), 'member_sha256': hashlib.sha256(member_bytes).hexdigest(), 'development_sha256': hashlib.sha256(development_bytes).hexdigest(), 'test_sha256': hashlib.sha256(test_bytes).hexdigest()}  # 汇总下游必验哈希
source_hashes = {'calendar_sha256': hashlib.sha256(calendar_path.read_bytes()).hexdigest(), 'development_labels_sha256': hashlib.sha256(development_label_path.read_bytes()).hexdigest(), 'security_status_history_sha256': hashlib.sha256(status_history_path.read_bytes()).hexdigest()}  # 绑定日历、开发标签与状态历史真实字节
contract = {'schema_version': 'project-contract-v1', 'contract_id': 'a-share-drawdown-20d-v1', 'freeze_date': freeze_date.strftime('%Y-%m-%d'), 'universe_rule': {'status_time_rule': '[effective_from,effective_to) at prediction_date', 'security_type': 'A股普通股', 'boards': ['主板', '创业板', '科创板'], 'exclude_st': True, 'require_listed': True, 'exclude_delisted': True}, 'label_rule': {'horizon_company_trading_days': 20, 'event': 'future_min_close/current_close-1 <= -0.10'}, 'warmup_rule': {'minimum_prior_company_trading_days': 20}, 'source_hashes': source_hashes, 'artifact_hashes': artifact_hashes}  # 冻结 point-in-time 总体、标签与 warm-up 合同
contract_bytes = (json.dumps(contract, ensure_ascii=False, sort_keys=True, separators=(',', ':')) + '\n').encode('utf-8')  # 生成规范JSON字节
artifact_dir.mkdir(parents=True, exist_ok=True)  # 创建唯一制品桶
for artifact_name, artifact_bytes in [('project-manifest-v1.csv', manifest_bytes), ('project-roster-v1.csv', roster_bytes), ('project-exclusions-v1.csv', exclusion_bytes), ('project-development-v1.csv', development_bytes), ('project-test-keys-v1.csv', test_bytes), ('project-contract-v1.json', contract_bytes)]:  # 枚举全部冻结制品
    write_same_or_create(artifact_dir / artifact_name, artifact_bytes)  # 对每个制品执行同字节幂等写入
print({**artifact_hashes, **source_hashes, 'training_event_rate': training_event_rate, 'training_majority': training_majority})  # 输出评分所需全部指纹与基线

这六个文件是 M03—M09 的单向依赖起点。project-contract-v1.jsonsource_hashes 是实际读取文件的字节 SHA-256,而不是手填占位符;其中 security_status_history_sha256 绑定状态历史原字节。project-roster-v1.csv 冻结每个公司日命中的状态区间、两级资格布尔值和原因,project-exclusions-v1.csv 保存全部被排除的公司日,因此 2019 年正常、2022 年才 ST 或退市的证券仍保留其 2019 年合格成员。这里只给出真实输入接口与 fail-closed 程序,没有伪造真实市场执行结果;真实快照未运行时不得声称总体已验证。测试标签的操作系统权限、首次读取时间和第二次读取拒绝仍必须由课程运行器记录。

2.8.3 理论题解答

  1. 均方误差分解:固定预测点 \(x\),写 \(Y=f(x)+\epsilon\),且 \(E(\epsilon\mid X=x)=0\)。对训练样本 \(\mathcal D\) 和新观测的噪声取期望,有

    \[ \begin{aligned} E[(Y-\hat f_{\mathcal D}(x))^2] &=E[(f(x)-\hat f_{\mathcal D}(x)+\epsilon)^2]\\ &=E[(f(x)-\hat f_{\mathcal D}(x))^2]+E(\epsilon^2), \end{aligned} \]

    因为交叉项的条件期望为零。再加上并减去 \(E_{\mathcal D}[\hat f_{\mathcal D}(x)]\)

    \[ \begin{aligned} E[(f-\hat f)^2] &=E[(f-E\hat f+E\hat f-\hat f)^2]\\ &=(f-E\hat f)^2+E[(\hat f-E\hat f)^2], \end{aligned} \]

    其中交叉项因 \(E(\hat f-E\hat f)=0\) 消失。因而

    \[E[(Y-\hat f(x))^2]=\operatorname{Bias}^2(\hat f(x))+\operatorname{Var}(\hat f(x))+\operatorname{Var}(\epsilon).\]

  2. 均值最优性:目标函数为 \(L(c) = \sum_{i=1}^n (y_i - c)^2\)。对 \(c\) 求导得 \(\frac{dL}{dc} = -2\sum (y_i - c)\)。令其为 0,得 \(\sum y_i = n \cdot c\),即 \(c = \frac{1}{n} \sum y_i = \bar{Y}\)。二阶导数为 \(2n > 0\),故为最小值点。

2.9 章末学习闭环

逐项自检:能否用新案例区分预测与推断;能否解释训练/验证/测试职责;能否从现场误差判断复杂度;能否生成 M02 manifest 并说明标签时间线。禁止用测试误差调参,也禁止把末端未知标签填成负类。常见误区是把低训练误差当泛化证据、把残差当不可约误差。

不看正文回答:为何验证集不能兼任最终测试?偏差与方差如何随灵活性变化?label_date 为什么参与 purge?迁移任务:为“未来 30 日客户流失”写同结构合同。下一章直接复用条件均值、损失、开发切分与 M02 制品。

展开检索答案 验证集参与选择后会产生选择偏差;灵活性增加通常降低偏差并提高方差,最优点由开发证据决定;label_date 记录结果完全可知的时点,跨过下一集合原点会泄漏未来。

出口决策:训练—验证—测试职责、point-in-time 资格与 label_date 时间线均为 must-pass,三项不得由其他目标抵消;三项全过、其余目标至少一项有证据且检索至少答对两道才进入第 3 章。预测/推断错回到 小节 2.5 前的职责段,以“设备故障”区分解释与预测;集合职责错用“未来 30 日客户流失”重画三集合;时点错回到 小节 2.6,以新的状态生效日重画特征日与标签日。每项复测 2 分,定义与时间顺序均正确才重入 小节 2.9;答案默认折叠且不得复用原题数值。

Hastie, Trevor, Robert Tibshirani, 和 Jerome Friedman. 2009年. The Elements of Statistical Learning: Data Mining, Inference, and Prediction. 2 版. Springer. https://doi.org/10.1007/978-0-387-84858-7.
James, Gareth, Daniela Witten, Trevor Hastie, Robert Tibshirani, 和 Jonathan Taylor. 2023年. An Introduction to Statistical Learning: With Applications in Python. Springer. https://doi.org/10.1007/978-3-031-38747-0.