import numpy as np # 用数组运算拟合并评估样本曲线
import matplotlib.pyplot as plt # 绘制观测误差与不可约误差的概念示意
import pandas as pd # 组织受控示意中的观测点与条件均值
import os # 将在线教材的固定数据根同步给本章后续独立代码块
from pathlib import Path # 用路径对象解析数据根目录
# 明文构造在线教材的数据根,并让后续跨章样本代码复用同一位置
BOOK_DATA_DIR = Path('/home/ubuntu/r2_data_mount/data').resolve() # 将固定数据根解析为绝对路径对象
os.environ['BOOK_DATA_DIR'] = str(BOOK_DATA_DIR) # 为本章后续独立入口登记统一数据根
assert BOOK_DATA_DIR.is_dir(), f'BOOK_DATA_DIR 不存在: {BOOK_DATA_DIR}' # 在数据读取前验证目录
# 分别定位财务观测与公司地区行业口径
FINANCIAL_STMT_PATH = BOOK_DATA_DIR / 'stock/financial_statement.h5' # 提供2023Q4金额字段
STOCK_BASIC_PATH = BOOK_DATA_DIR / 'stock/stock_basic_data.h5' # 提供长三角与制造业筛选字段
assert FINANCIAL_STMT_PATH.is_file(), f'缺少财务报表文件: {FINANCIAL_STMT_PATH};请核对 BOOK_DATA_DIR' # 在HDF读取前给出精确缺失路径
assert STOCK_BASIC_PATH.is_file(), f'缺少公司基本信息文件: {STOCK_BASIC_PATH};请核对 BOOK_DATA_DIR' # 在HDF读取前给出精确缺失路径
# 固定教学抽样,使残差图可逐点复核
np.random.seed(42) # 锁定后续随机步骤的共同随机流2 统计学习 (Statistical Learning)
2.1 导读
统计学习研究如何从数据中估计变量之间的规律,并把这些规律用于预测或解释。本章以中国上市公司的财务与行情数据为背景,建立全书共同使用的语言:响应变量、预测变量、未知函数 \(f\)、随机误差、训练误差与样本外误差。读者还会看到,模型越灵活并不意味着预测一定越好;模型选择必须依靠与业务时点一致的验证数据。
2.2 学习目标
完成本章后,读者应能够:
- 区分预测、条件关联与因果问题,并为商业问题写出清楚的估计对象。
- 区分真实函数 \(f\)、估计函数 \(\hat f\)、随机误差 \(\epsilon\) 与样本残差 \(e\)。
- 解释模型灵活性、训练误差和样本外误差之间的关系。
- 在固定预测点下推导偏差—方差—不可约误差分解。
- 按信息可得时点划分训练集、验证集与测试集,并与朴素基线比较。
2.3 什么是统计学习? (What Is Statistical Learning?)
假设我们是一家位于中国长三角地区的金融科技公司的数据分析顾问,需要研究上市公司的财务投入与经营产出之间的关系。图 2.1 是一幅手绘概念示意图:点的位置和虚线方向由制图者设定,只用来说明输入、响应与拟合关系,不是 200 家真实公司的观测,也不能支持关于费用回报的经验结论。真实数据分析从后文明确读取本地数据的代码开始。
虽然管理层可以控制研发、销售和管理费用的预算分配,但本例的观测数据只能用来学习“在给定三类费用时,营业收入通常处于什么水平”。因此,本节的目标是估计条件均值 \(E(Y\mid X=x)\) 并评价它对新观测的预测误差,而不是回答“把某项预算从 \(x\) 调整到 \(x'\) 会使收入增加多少”。后一个问题涉及干预量,例如 \(E[Y\mid do(X=x')]-E[Y\mid do(X=x)]\);它不能仅由费用与收入之间的预测关联识别。资源配置是需要另行设计证据的决策问题,准确预测本身不保证预算调整能够提升业绩。
在这个案例中,费用投入是输入变量(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)\) 是当前目标总体、观测时期与特征支持域内可预测的条件模式。例如,在样本中广告投入较高的企业可能同时具有较高销售量;这说明 \(X\) 对预测 \(Y\) 有信息,却没有说明增加广告预算会使同一家企业的销售量提高。随机噪音 \(\epsilon\) 则包含给定 \(X\) 后仍无法预测的部分,例如未进入特征集的短期天气或竞争对手促销冲击。总体、时期或投入范围改变时,原有条件模式也可能漂移,不能无条件称为“稳定规律”。
反例:需求预期造成的混杂。 假设管理层在预计下一季度需求旺盛时同时提高广告预算和备货量。即使广告对销量的真实边际作用为零,观测数据仍会显示“广告投入越高,销售量越高”,因为未观测的需求预期同时推动二者。反向因果也可能出现:收入较高、现金更充裕的企业更有能力增加销售费用。此时,优秀的预测模型可以利用这些关联预测收入,却不能把系数或偏依赖曲线解释为预算效果。
要支持干预结论,需要额外证据。随机分配预算的实验可以在依从性、干扰和测量条件得到控制时识别相应处理效应;自然实验、断点、工具变量或双重差分等准实验设计则需要各自可信且可检验到合理程度的识别假设。仅在观察数据下主张因果效应,也至少要明确目标总体、处理版本、时间顺序、混杂控制、重叠性与无干扰等边界,并进行敏感性分析。本章其余部分只讨论预测量与样本外误差,不把观察关联升级为资源配置建议。
作为另一个例子,考虑 图 2.2 的左侧面板,这是一个来自长三角制造业上市公司的30个样本的净利润与总资产关系的散点图。该图表明,我们可能能够使用总资产规模来预测公司净利润。然而,连接输入变量和输出变量的函数\(f\)通常是未知的。在这种情况下,必须基于观测点来估计\(f\)。
下面的案例从本地数据中显式筛选长三角制造业上市公司,并抽取30个观测。二次多项式是对这一小样本的拟合曲线 \(\hat f\),不是不可观测的真实 \(f\)。观测值与拟合值之差是残差 \(e_i=y_i-\hat f(x_i)\),不是真实误差 \(\epsilon_i=y_i-f(x_i)\)。
下一个代码块只负责读取、筛选和构造拟合所需对象;它复用上一块中已验证的数据路径。
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 # 原始观测的总资产值
# 只在观测资产支持域内评估样本拟合曲线
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个样本,再拟合一条二次多项式曲线。下面左侧展示观测散点,右侧展示样本拟合曲线与残差;两者都不能揭示不可观测的真实函数或误差项。
# 并列比较原始观测与“拟合曲线—残差”分解
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') # 明示资产换算单位
ax_obs.set_ylabel('净利润 (Net Profit, 亿元)', fontsize=12, fontweight='bold') # 明示利润换算单位
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') # 与左图保持相同资产单位
ax_rel.set_ylabel('净利润 (Net Profit, 亿元)', fontsize=12, fontweight='bold') # 与左图保持相同利润单位
ax_rel.set_title('拟合曲线与样本残差', fontsize=14, fontweight='bold', color='#2C3E50') # 不把估计量写成真值
ax_rel.legend(frameon=True, facecolor='white') # 防止把拟合曲线误读为真实函数
plt.tight_layout() # 防止双面板标签与图例重叠
plt.show() # 输出观测层与估计层的概念对照/tmp/ipykernel_1400855/1511858950.py:31: UserWarning: Glyph 770 (\N{COMBINING CIRCUMFLEX ACCENT}) missing from font(s) Source Han Sans SC.
plt.tight_layout() # 防止双面板标签与图例重叠
/home/ubuntu/miniconda3/envs/peter/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)
图 2.2 的左侧面板呈现30家长三角制造业上市公司的总资产与净利润散点,右侧叠加同一样本估计的二次多项式曲线。灰色虚线是观测值与拟合值之差,即样本残差 \(e_i\);它既不等于不可观测误差 \(\epsilon_i\),也不能把曲线提升为真实 \(f\)。图形只支持样本内关联描述,不能说明扩大资产会因果地提高利润。
通常,函数\(f\)可能涉及不止一个输入变量。在 图 2.3 中,我们将收入绘制为受教育年限和工作年限的函数。在这种情况下,\(f\)是一个二维曲面,必须基于观测数据进行估计。
下面是一个机制实验,仅用于说明二维响应面 \(Y=f(X_1,X_2)+\epsilon\) 的表示方法。代码先规定一个含二次项的函数,再加入高斯噪声并绘制等高线。所有点都是模拟观测,不代表真实劳动者,也不能用于判断教育或工龄的现实收益。
import numpy as np # 构造已知二维函数与带噪观测
import matplotlib.pyplot as plt # 把二维条件均值投影为等高线
# 生成受教育年限的坐标轴(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') # 标出第一项模拟输入的年数单位
ax_contour.set_ylabel('工作年限 (Years of Seniority)', fontsize=12, fontweight='bold') # 标出第二项模拟输入的年数单位
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() # 输出二维条件均值与噪声观测的对照
图 2.3 中的颜色只编码预先设定的函数值,35 个点则在该函数上加入受控噪声。图形说明同一二维位置的观测可能偏离条件均值曲面;曲面的斜率和弯曲完全由代码设定,不能被解释为教育回报、职业周期或任何现实因果效应。
本质上,统计学习(statistical learning)指的就是一组用于估计\(f\)的方法。在本章中,我们将概述在估计\(f\)时出现的一些关键理论概念,以及评估所得结果的工具。
2.3.1 为什么要估计\(f\)? (Why Estimate \(f\)?)
我们希望估计\(f\)主要有两个原因:预测(prediction)和推断(inference)。我们将逐一讨论。
2.3.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\)之间的平方误差衡量。固定预测点 \(X=x\) 和训练数据集 \(\mathcal D\),令新观测满足 \(Y_0=f(x)+\epsilon_0\)、\(E(\epsilon_0\mid X=x)=0\),且新误差与训练数据独立,则只对新观测误差取条件期望可得:
\[ E_{\epsilon_0}\!\left[(Y_0-\hat f_{\mathcal D}(x))^2\mid X=x,\mathcal D\right] = [f(x)-\hat f_{\mathcal D}(x)]^2+\operatorname{Var}(\epsilon_0\mid X=x). \tag{2.3}\]
交叉项为零是因为条件误差均值为零。这个公式在给定训练集时揭示两个误差来源;若再对训练集的随机抽取取期望,第一项还会进一步分成偏差平方与方差。
- 可约误差(reducible error):\([f(X) - \hat{f}(X)]^2\)。这部分误差可以通过使用更合适的统计学习技术来估计\(f\)而减少。
- 不可约误差(irreducible error):\(\operatorname{Var}(\epsilon_0\mid X=x)\)。这部分误差即使 \(\hat f=f\) 也无法由当前预测变量消除;异方差情形下,它可以随 \(x\) 改变。
Tip: 理解不可约误差
不可约误差的存在是因为在现实世界中,没有任何模型能够完美地预测结果。考虑我们电商公司的案例:即使我们建立了完美的模型来预测销售量,仍然有一些不可控因素会影响实际销售,例如:
- 突发的公共卫生事件
- 竞争对手的意外促销
- 天气变化对消费者行为的影响
- 社交媒体上的意外舆论
因此,设置现实的期望很重要:我们的目标不是达到100%的预测准确率,而是将预测误差降到接近不可约误差的水平。
2.3.1.2 推断 (Inference)
我们关心估计\(f\)的第二个原因是理解\(Y\)如何随着\(X_1, X_2, \ldots, X_p\)的变化而变化。在这种情况下,我们希望将\(\hat{f}\)看作一个透明、可解释的模型,而不是黑盒。
问题示例:
- 哪些媒体与销售有关?:这有助于公司决定应该重点投资哪种媒体渠道。
- 哪些媒体与销售的条件关联最强?:观察性模型可比较关联与预测增量;预算调整的因果效果仍需实验或识别设计。
- 网络广告和销售之间的关系是线性的还是非线性的?:了解这种关系的性质可以帮助公司制定更有效的营销策略。
- 是否存在很强的交互效应?:例如,在电视广告和网络广告同时投放时,销售量的提升是否比单独投放时更大?
在这种情况下,可能更倾向于使用简单且高度可解释的模型。这类模型包括线性回归、逻辑回归等,它们的参数有明确的解释意义。
易混淆概念辨析:预测、关联与因果
- 预测问“对未见样本能否猜准”,证据是锁定模型的样本外损失。
- 关联问“在给定已纳入变量后 \(X\) 与 \(Y\) 如何共变”,回归系数默认只能作这一层解释。
- 因果问“若干预 \(X\),\(Y\) 会如何改变”,需要随机实验或可辩护的自然实验、工具变量、断点等识别设计,而不是仅靠一个可解释模型。
2.3.2 如何估计\(f\)? (How Do We Estimate \(f\)?)
在统计学习中,我们有许多用于估计\(f\)的方法。这些方法可以大致分为两类:参数化方法(parametric methods)和非参数化方法(non-parametric methods)。
2.3.2.1 参数化方法 (Parametric Methods)
参数化方法把候选函数限制在由有限维参数索引的函数族 \(\{f_\theta:\theta\in\Theta\subseteq\mathbb R^d\}\) 中。这里的“参数化”描述函数族的维数,并不要求完整指定响应变量的概率分布;只有进行似然推断时,才需要进一步给出概率模型。典型流程分为两个步骤:
第一步:对函数形式做出假设 首先,我们对\(f\)的形式或形状做出假设。例如,一个非常简单的假设是\(f\)是线性的: \[ f(X) = \beta_0 + \beta_1 X_1 + \beta_2 X_2 + \cdots + \beta_p X_p \tag{2.4}\]
第二步:拟合或训练模型 在这个步骤中,我们需要估计模型参数。对于线性模型,我们需要估计\(\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\)等。
因此,参数化方法的核心是用固定、有限维的参数刻画所研究的函数或分布。线性回归的条件均值函数是参数化的;若进一步假定正态误差,才得到一个完整的参数概率模型。某些通常称为“非参数”的算法也有调节参数,但其有效复杂度可随样本量增长,不能据此改称参数模型。
2.3.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.3.3 预测准确度与模型可解释性的权衡 (Trade-Off Between Prediction Accuracy and Model Interpretability)
在统计学习的商业应用中,预测性能与透明度常需同时考虑。灵活的(flexible)方法通常能更充分地拟合训练数据,但若额外自由度主要追踪样本噪声,就会产生过拟合(overfitting)。是否值得增加复杂度,应由样本外损失、解释需求和决策成本共同判断。
案例:量化选股中的模型选择 在构建多因子量化选股策略时,深度神经网络(Deep Neural Networks)能够表达复杂非线性,但是否改善未来期表现仍须用锁定验证证据判断;模型越难解释,投资委员会核对风险来源与失效条件的成本通常越高。透明基线可以采用线性多因子模型。例如,Fama–French 三因子模型使用市场、规模(SMB)和价值(HML)因子 (Fama 和 French 1993年);动量不是原始三因子之一,若加入动量必须另行命名和验证。透明结构便于解释因子暴露,却不保证预测准确度必然高于或低于神经网络。
图 2.4 展示了不同统计学习方法在灵活性和可解释性之间的权衡。子集选择(Subset Selection)、套索回归(Lasso)和岭回归(Ridge)等方法限制性较强,参数结构通常更容易核对。广义加法模型、树集成和深度学习能表达不同类型的非线性,但解释成本也取决于具体结构、正则化和应用语境。
为了呈现这一核心理论概念,下面的 Python 代码利用 matplotlib 手工构建了一张展示统计学习方法在“灵活性”与“可解释性”之间权衡的经典学术散点图。代码中明确定义了 8 种初中高级的机器学习模型在这个二维光谱上的相对位置(例如,Lasso 回归位于左上方代表高解释性低灵活性,而深度学习位于右下方代表低解释性高灵活性)。我们通过不同的颜色和节点文字对比,以及辅助斜虚线,强化了这张图表的理论表达力。理解这一图表也构成了我们在后续各章节中针对具体业务寻找、选择和评估各类算法的指南针。
import matplotlib.pyplot as plt # 绘制方法结构的概念定位图
# 统一中文标签与负号的出版显示
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 = [ # 登记仅供教学定位的方法坐标与类别颜色
(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) # 为全部人为灵活度坐标保留边距
ax_tradeoff.set_ylim(0, 11) # 为全部人为解释度坐标保留边距
ax_tradeoff.set_xlabel('模型灵活性 (Flexibility) →', fontsize=12, fontweight='bold', labelpad=15) # 说明横向位置表示定性灵活度
ax_tradeoff.set_ylabel('← 可解释性 (Interpretability)', fontsize=12, fontweight='bold', labelpad=15) # 说明纵向位置表示定性透明度
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 是灵活性与可解释性的教学示意,而不是对所有数据集都成立的固定排名。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.3.4 回归vs分类问题 (Regression Versus Classification Problems)
变量可以是定量的(quantitative)或定性的(qualitative/categorical)。
- 定量变量:取值为数值大小,例如收入、身高、温度、销售量
- 定性变量:取值为类别或标签,例如性别(男/女)、邮件类型(垃圾邮件/正常邮件)、客户类别(高价值/中价值/低价值)
那些涉及定量响应变量的问题称为回归问题(regression problems),而那些涉及定性响应变量的问题称为分类问题(classification problems)。
不过,这个区分有时有些模糊。例如,逻辑回归(logistic regression)是一种用于分类的方法,但其名称中包含”回归”二字。这是因为逻辑回归本质上是预测类别的概率,这是一个定量问题。
2.4 评估模型准确度 (Assessing Model Accuracy)
“没有免费午餐”结果在对所有可能目标函数按特定对称方式平均等条件下,说明不存在对所有问题都占优的学习算法 (Wolpert 1996年)。它不表示所有方法在某一具体任务上必然一样好;实务中仍要利用任务结构和开发期证据比较候选,再将测试集留给锁定模型的最终评价。
2.4.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: 为什么使用平方误差?
你可能好奇,为什么使用平方误差而不是绝对误差?这里有几个原因:
- 数学便利性:平方函数是可微的,这使得我们可以使用微积分来寻找最优解
- 对大错误的惩罚更重:平方误差对大误差的惩罚大于小误差,这符合许多实际应用的需求(例如,预测误差为10的错误是误差为5的4倍,而不是2倍)
- 与高斯噪声的联系:如果误差服从正态分布,最小化平方误差等价于最大化似然函数
然而,绝对误差(MAE)在某些情况下也有优势,特别是当数据中存在异常值时,因为它对异常值不那么敏感。
2.4.1.1 训练误差vs测试误差 (Training Error vs. Test Error)
在实践中,我们感兴趣的不仅仅是模型在训练数据上的表现,而是模型在未见过的新数据上的表现。因此,我们需要区分:
- 训练MSE (Training MSE):在训练数据上计算的MSE
- 验证MSE (Validation MSE):在训练集内部的验证部分上计算,用于选择复杂度
- 测试MSE (Test MSE):锁定模型后在独立测试集上只计算一次,用于评估泛化误差
我们用验证集选择模型,再用从未参与选择的测试集评估泛化能力。直接挑选测试 MSE 最小的次数,会把测试集变成调参数据。
机制实验:模型复杂度与样本外误差
下面用受控模拟说明训练误差和样本外误差的区别。真实函数由代码预先规定,因此结果只说明复杂度与泛化之间的统计机制,不是电商公司的经验规律。令营销预算 \(x\) 在 0 至 100 的支持域上均匀抽取,数据生成机制为:
\[ x\sim U(0,100),\qquad \text{销售收入}=20+0.5x+0.02x^2-0.0002x^3+\epsilon, \qquad \epsilon\sim N(0,8^2). \]
其中,\(\epsilon\) 是标准差为 8 的高斯随机误差。代码从该机制生成 120 个受控观测,把数据顺序划分为训练、验证和测试三段,再比较 1 至 11 次多项式的均方误差。验证集用于选择次数,测试集只在模型锁定后使用一次。该实验可能出现训练误差继续下降而验证误差回升的情形,但 U 型并非每个有限样本都必然呈现,结论应以当次输出为准。
import numpy as np # 生成已知三次信号与随机噪声
import matplotlib.pyplot as plt # 绘制训练与验证损失随次数的变化
from sklearn.preprocessing import PolynomialFeatures # 在训练数据上生成候选次数的设计矩阵
from sklearn.linear_model import LinearRegression # 估计各候选多项式的最小二乘系数
from sklearn.metrics import mean_squared_error # 用统一平方损失比较训练、验证与测试段
# 锁定受控 DGP 的一次样本实现以便复核
np.random.seed(99) # 让各次数比较共享同一批噪声观测
# 模拟长三角电商公司的营销预算与销售收入数据(共120个样本)
observation_count = 120 # 设定总样本量为120家模拟公司
marketing_budget = np.random.uniform(0, 100, observation_count) # 在预设预算支持域抽取输入
# 定义含三次项的真实销售收入趋势函数:revenue = 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 # 观测销售收入 = 真实趋势 + 噪声
# 把单一预算变量整理为模型所需的一列设计矩阵
marketing_budget_features = marketing_budget.reshape(-1, 1) # 保持每行对应一个模拟公司
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) # 预先登记由低到高的复杂度候选
training_mse_history = [] # 保存样本内损失以观察复杂度效应
validation_mse_history = [] # 保存开发段损失以选择次数
# 逐个测试从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() # 为当前多项式设计矩阵估计OLS系数
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 轨迹。下图展示当前模拟和切分下的形状;最低验证误差只是本次候选集合的选择依据,不是普遍的复杂度拐点。
# 对照各多项式次数的训练与验证 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') # 让高阶候选的数量级差异仍可比较
ax_error_curves.set_xlabel('模型复杂度 (多项式阶数 Degree)', fontsize=12, fontweight='bold') # 横轴对应预先登记的候选阶数
ax_error_curves.set_ylabel('均方误差 (MSE, 对数刻度)', fontsize=12, fontweight='bold') # 纵轴统一为平方损失的对数尺度
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() # 输出开发曲线;锁定测试损失不参与选模
选定次数后,把训练集和验证集合并重拟合,最后只访问一次测试标签:
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 展示了一个重要现象:
训练MSE:随着模型灵活性增加(多项式次数增加),训练MSE单调下降。这是因为更灵活的模型能够更好地拟合训练数据,甚至包括噪音。
验证MSE:可能呈现U型,因而用于选择次数;测试 MSE 只是锁定模型的一个最终样本外估计。
这种现象揭示了统计学习的核心挑战:模型灵活性必须在预先固定的验证设计中比较,不能由训练拟合或理论示意图单独决定。
2.4.2 偏差-方差权衡 (The Bias-Variance Trade-Off)
某些候选序列出现的测试 MSE 回升可用偏差—方差分解(bias–variance decomposition)分析,但分解本身不保证有限样本曲线必为 U 型或存在唯一最优点。对于给定的 \(x_0\),测试 MSE 可以分解为:
\[ E_{\mathcal D,\epsilon_0}\!\left[(Y_0-\hat f_{\mathcal D}(x_0))^2\mid X_0=x_0\right] =\operatorname{Var}_{\mathcal D}(\hat f_{\mathcal D}(x_0)) +\operatorname{Bias}_{\mathcal D}(\hat f_{\mathcal D}(x_0))^2 +\operatorname{Var}(\epsilon_0\mid X_0=x_0). \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):\(\operatorname{Var}(\epsilon_0\mid X_0=x_0)\) 是给定预测变量后的条件噪声。只有在同方差假设下,它才可简写为与 \(x_0\) 无关的常数 \(\sigma^2\)。
数学推导:严格的偏差-方差分解
为了深入理解这一权衡,我们提供 \(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 是确定性教学示意图:代码人为指定一条下降的偏差平方曲线、一条上升的方差曲线和常数噪声底座,再逐点相加。图形由所选公式决定,不是重复抽样、交叉验证或真实数据得到的证据,也不声称所有任务都有唯一的 U 型最小点。真实验证损失存在抽样波动;当多个候选的损失差异落在不确定性范围内时,应优先保留更简单、更稳定或决策成本更低的候选。
import numpy as np # 生成确定性示意曲线的连续坐标与人为函数值
import matplotlib.pyplot as plt # 绘制人工指定的偏差—方差确定性示意
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 # 设置仅供示意的常数条件噪声
# 计算总测试误差 = 偏差² + 方差 + 不可约误差
total_ms_error = squared_bias_curve + variance_curve + irreducible_noise_floor # 三者逐点相加得到总测试MSE序列上述三个数组与总和都是人为公式的确定输出。下图只用它们解释分解项如何相加,不把示意曲线的最低处当作可传递到其他数据的复杂度选择。
# 并列展示人为指定的偏差、方差、噪声与总误差分解项
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(ε))')
# 绘制三项人为相加得到的总误差示意,不声称通用形状
ax_bias_var.plot(flexibility_axis, total_ms_error, color='#1E293B', linewidth=4, label='总测试 MSE')
ax_bias_var.text(5.4, 20.5, '人为函数示意\n不是验证估计', color='#475569', fontsize=11) # 在图内明示证据身份
# 固定纵轴以完整呈现四条人为分解曲线
ax_bias_var.set_ylim(0, 25) # 保留所有人为指定分解项的完整范围
ax_bias_var.set_xlabel('模型灵活性 (Model Flexibility) →', fontsize=12, fontweight='bold') # 横轴只代表人为灵活度坐标
ax_bias_var.set_ylabel('误差期望 E(Test MSE) →', fontsize=12, fontweight='bold') # 纵轴展示分解式中的期望误差
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() # 输出确定性教学示意而非经验估计结果
关键要点:
本示意中的偏差项下降:人为公式让偏差平方随灵活性下降;真实候选是否如此应由任务结构核对。
本示意中的方差项上升:人为公式让方差随灵活性上升;真实候选的敏感度须用重采样或稳定性诊断估计。
候选选择:真实验证曲线可能不是 U 型,也可能有多个近似最小值;近似平局时优先较简单候选。
不可约误差:无论模型多么灵活,测试MSE都不会低于不可约误差的水平,这是因为数据中存在固有的随机噪音。
2.4.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.5 本章小结 (Chapter Summary)
本章介绍了统计学习的基础概念:
统计学习的定义:统计学习是一组用于估计函数\(f\)的方法,该函数将输入变量\(X\)映射到输出变量\(Y\)。
估计\(f\)的目的:
- 预测:使用\(\hat{f}(X)\)来预测\(Y\)
- 推断:理解\(Y\)如何随\(X\)的变化而变化
估计\(f\)的方法:
- 参数化方法:假设函数形式,然后估计参数
- 非参数化方法:不假设函数形式,让数据”说话”
预测准确度与模型可解释性的权衡:更灵活的模型通常更难解释,但可能产生更准确的预测。
评估模型准确度:
- 在回归问题中使用均方误差(MSE)
- 在分类问题中使用错误率或准确率
偏差-方差权衡:测试误差可以分解为偏差平方、方差和不可约误差三部分;有限候选应由开发期样本外证据比较。
在接下来的章节中,我们将详细讨论具体的统计学习方法,包括线性回归、分类方法、重采样技术、线性模型选择与正则化、非线性模型等。
2.6 理论来源与前沿
本章的统一框架——用损失函数刻画拟合质量、用复杂度刻画模型灵活度、用测试误差衡量泛化表现——一方面继承了统计学的风险最小化思想,另一方面与计算学习理论中的 PAC/VC 观点相呼应。偏差-方差分解把‘模型太简单’与‘模型太复杂’两类误差统一到同一个分解式中,从而为模型选择、正则化与误差评估提供了可操作的语言 (James 等 2023年; Hastie 等 2009年)。
近年来的研究前沿主要集中在三个方向:
- 可解释性与可复核性:在金融风控、信贷审批、保险定价等场景中,模型不仅要准确,还要能解释、可复核。
- 分布漂移与稳健学习:训练分布与部署分布不一致时,需要稳健估计、域自适应与在线更新机制。
- 因果视角的学习:将‘预测’与‘干预效果评估’区分开,在政策评估、营销增量与运营实验中尤为关键。
2.7 综合案例:构造共用的未来回撤样本
第2—9章回答同一个风险管理问题:在公司交易日 \(t\) 收盘后,风险经理能否仅凭当时已经形成的行情信息,识别未来 20 个公司交易日内最大回撤达到 10% 的公司—日期。误报会占用调查和对冲资源,漏报则会让风险暴露未被及时复核。本章只构造样本与朴素基线;第3—8章只使用训练段和验证段开发模型,第9章才打开封存测试段。
数据身份卡
| 项目 | 定义 |
|---|---|
| 来源 | stock/stock_price_post_adjusted.h5,本地 A 股日行情 |
| 研究对象 | 预先声明的 4 家长三角上市公司;它们用于地区化教学,不代表全部 A 股 |
| 观察期 | 2012-01-01 至 2024-12-31 |
| 唯一键 | order_book_id、prediction_date |
| 特征 | m02_features,全部在预测日收盘时形成 |
| 连续目标 | max_drawdown_20d,在 \(t\) 至 \(t+20\) 的 21 个后复权收盘价路径中,运行峰值到其后谷值的最大跌幅,取值不大于 0 |
| 二元标签 | drawdown_event_20d,当且仅当 max_drawdown_20d 不高于 \(-10\%\) 时为 1 |
| 标签末日 | label_end_date,同一公司未来第 20 个交易日;窗口不足则标签缺失 |
| 时间段 | 2012—2019 年训练,2020—2021 年验证,2022—2024 年测试;跨边界标签从前一段清除 |
先定义前向最大回撤函数。对路径 \(P_0,\ldots,P_{20}\),先计算运行峰值 \(H_j=\max_{0\le k\le j}P_k\),再取 \(\min_j(P_j/H_j-1)\)。因此价格路径 \(100\to120\to105\) 的最大回撤是 \(105/120-1=-12.5\%\),即使终点仍高于预测日价格,也必须记为事件。
import os # 读取跨平台数据根环境变量
from pathlib import Path # 用路径对象安全拼接本地文件
import numpy as np # 构造有限特征和时间分段
import pandas as pd # 处理公司—日期面板
def calculate_forward_max_drawdown(price_values, horizon_days=20): # 计算含预测日在内的前向峰谷最大回撤
price_array = np.asarray(price_values, dtype=float) # 转为连续数值数组以构造滑动路径
window_size = horizon_days + 1 # 路径包含t日与其后horizon_days个交易日
drawdowns = np.full(price_array.size, np.nan, dtype=float) # 末端窗口不完整时保持未知
if price_array.size < window_size: # 保护短序列而不虚构标签
return drawdowns # 返回全缺失结果表示没有完整路径
price_windows = np.lib.stride_tricks.sliding_window_view(price_array, window_size) # 构造每个预测日的完整价格路径
running_peaks = np.maximum.accumulate(price_windows, axis=1) # 逐路径记录每一时点之前的最高价
drawdowns[:price_windows.shape[0]] = np.min(price_windows / running_peaks - 1, axis=1) # 取峰值后最深谷值跌幅
return drawdowns # 返回与原价格序列等长的前向最大回撤
m02_fixture_drawdown = calculate_forward_max_drawdown([100, 120, 105], horizon_days=2)[0] # 用先涨后跌路径区分峰谷回撤与起点收益
assert np.isclose(m02_fixture_drawdown, -0.125) # 核对峰值120到谷值105的跌幅为百分之十二点五
assert m02_fixture_drawdown <= -0.10 # 核对该路径必须被判为百分之十回撤事件下面把数据读取、预测日特征和峰谷标签封装为一个准备函数。固定公司名单是教学研究边界,不是事后按收益挑出的“优胜者”;return_1d 等特征均以 \(t\) 日结束,只有标签计算向未来展开。
def prepare_m02_price_panel(book_data_dir): # 从统一数据根构造未切分的公司—预测日样本
data_root = Path(book_data_dir).expanduser().resolve() # 同时接受环境变量字符串或Path对象
assert data_root.is_dir(), f'BOOK_DATA_DIR 不存在: {data_root}' # 在读取前验证数据挂载
price_path = data_root / 'stock/stock_price_post_adjusted.h5' # 指向后复权日行情
assert price_path.is_file(), f'缺少后复权行情文件: {price_path}' # 在read_hdf前给出明确缺失文件
company_ids = ('600104.XSHG', '002415.XSHE', '600276.XSHG', '002230.XSHE') # 固定四家长三角教学公司
price_frames = [pd.read_hdf(price_path, where=f'order_book_id={company_id!r}', columns=['close', 'volume']).reset_index() for company_id in company_ids] # 在存储层选择所需公司和字段
price_panel = pd.concat(price_frames, ignore_index=True) # 合并为公司—交易日面板
price_panel['prediction_date'] = pd.to_datetime(price_panel['date']) # 把交易日定义为收盘后预测原点
price_panel = price_panel.loc[price_panel['prediction_date'].between('2012-01-01', '2024-12-31')].copy() # 固定研究观察期
price_panel = price_panel.sort_values(['order_book_id', 'prediction_date']).reset_index(drop=True) # 保证窗口只在公司内按时间推进
grouped_prices = price_panel.groupby('order_book_id', sort=False) # 建立公司内窗口边界
price_panel['return_1d'] = grouped_prices['close'].transform(lambda prices: prices.pct_change(fill_method=None)) # 计算截至预测日的一日收益
price_panel['momentum_5d'] = grouped_prices['close'].transform(lambda prices: prices.pct_change(5, fill_method=None)) # 计算截至预测日的五日动量
price_panel['volatility_20d'] = grouped_prices['return_1d'].transform(lambda returns: returns.rolling(20, min_periods=20).std()) # 计算过去二十日波动率
volume_mean_20d = grouped_prices['volume'].transform(lambda volume: volume.rolling(20, min_periods=20).mean()) # 估计预测日前成交量常态
price_panel['volume_ratio_20d'] = price_panel['volume'] / volume_mean_20d # 表示当日成交量相对过去均值的活跃度
feature_names = ['return_1d', 'momentum_5d', 'volatility_20d', 'volume_ratio_20d'] # 固定跨章特征名称与顺序
price_panel['max_drawdown_20d'] = grouped_prices['close'].transform(lambda prices: calculate_forward_max_drawdown(prices, horizon_days=20)) # 计算t至t+20真实峰谷最大回撤
price_panel['label_end_date'] = grouped_prices['prediction_date'].shift(-20) # 记录未来第二十个公司交易日
price_panel['drawdown_event_20d'] = price_panel['max_drawdown_20d'].le(-0.10).where(price_panel['max_drawdown_20d'].notna()).astype('Int64') # 完整路径才生成二元事件
price_panel = price_panel.replace([np.inf, -np.inf], np.nan).dropna(subset=feature_names + ['max_drawdown_20d', 'drawdown_event_20d', 'label_end_date']).copy() # 排除无效特征和未完成路径
return price_panel, feature_names # 把未切分样本交给公共构造函数公共函数负责固定切分、边界断言和四项跨章交付。任何章节在干净内核中都可以用 build_m02_shared_sample(BOOK_DATA_DIR) 重建同一对象,不需要依赖前一章的内存状态。
def build_m02_shared_sample(book_data_dir): # 返回第2至第9章唯一的共享分析对象
price_panel, feature_names = prepare_m02_price_panel(book_data_dir) # 从统一数据根重建真实峰谷标签
validation_start = pd.Timestamp('2020-01-01') # 固定验证段起点
test_start = pd.Timestamp('2022-01-01') # 固定封存测试段起点
study_end = pd.Timestamp('2024-12-31') # 固定研究终点
train_mask = (price_panel['prediction_date'] < validation_start) & (price_panel['label_end_date'] < validation_start) # 清除伸入验证期的训练标签
validation_mask = price_panel['prediction_date'].between(validation_start, test_start, inclusive='left') & (price_panel['label_end_date'] < test_start) # 清除伸入测试期的验证标签
test_mask = price_panel['prediction_date'].between(test_start, study_end, inclusive='both') # 标记只供第九章最终评价的日期
price_panel['split'] = np.select([train_mask, validation_mask, test_mask], ['train', 'validation', 'test'], default='unused') # 赋予互斥时间段
shared_columns = ['order_book_id', 'prediction_date', 'label_end_date', *feature_names, 'max_drawdown_20d', 'drawdown_event_20d', 'split'] # 固定第2至第9章权威字段顺序
shared_sample = price_panel.loc[price_panel['split'] != 'unused', shared_columns].copy() # 排除边界隔离行并形成共享对象
assert list(shared_sample.columns) == shared_columns # 阻止同名对象在不同章节出现字段或顺序漂移
assert not shared_sample.duplicated(['order_book_id', 'prediction_date']).any() # 验证公司—预测日键唯一
assert np.isfinite(shared_sample['max_drawdown_20d']).all() and shared_sample['max_drawdown_20d'].le(0).all() # 验证连续目标有限且符合回撤符号约定
assert shared_sample['drawdown_event_20d'].astype(bool).eq(shared_sample['max_drawdown_20d'].le(-0.10)).all() # 验证二元标签严格由连续目标派生
assert set(shared_sample['drawdown_event_20d'].unique()) == {0, 1} # 验证二元标签包含两类
assert shared_sample.loc[shared_sample['split'] == 'train', 'label_end_date'].max() < validation_start # 验证训练标签不触及验证期
assert shared_sample.loc[shared_sample['split'] == 'validation', 'label_end_date'].max() < test_start # 验证验证标签不触及测试期
training_event_rate = float(shared_sample.loc[shared_sample['split'] == 'train', 'drawdown_event_20d'].mean()) # 只用训练标签估计概率基线
test_keys = shared_sample.loc[shared_sample['split'] == 'test', ['order_book_id', 'prediction_date']].copy() # 固定第九章最终比较键
return shared_sample, feature_names, training_event_rate, test_keys # 返回约定的四项跨章交付
book_data_dir_value = os.environ.get('BOOK_DATA_DIR') # 安全读取统一数据根配置
assert book_data_dir_value, '请先设置 BOOK_DATA_DIR,使其指向包含 stock/ 子目录的数据根' # 缺失时说明修复方法
BOOK_DATA_DIR = Path(book_data_dir_value).expanduser().resolve() # 解析统一数据根
m02_shared_sample, m02_features, m02_training_event_rate, m02_test_keys = build_m02_shared_sample(BOOK_DATA_DIR) # 在当前干净内核构造共享对象下面的动态输出只核对样本身份和开发期基线,不报告测试目标分布。先检查每段的公司数、行数和日期范围,再检查训练期连续目标均值与事件率;若任一段为空、键重复、目标关系或日期断言失败,应停止建模并修正数据,而不是放宽边界。
m02_split_summary = m02_shared_sample.groupby('split', observed=True).agg(company_count=('order_book_id', 'nunique'), sample_count=('prediction_date', 'size'), first_prediction_date=('prediction_date', 'min'), last_prediction_date=('prediction_date', 'max')).reset_index() # 汇总不含测试结果的身份信息
print(m02_split_summary.to_string(index=False)) # 供学生核对公司数、样本数与日期边界
m02_training_drawdown_mean = float(m02_shared_sample.loc[m02_shared_sample['split'] == 'train', 'max_drawdown_20d'].mean()) # 只用训练段形成连续预测基线
print('训练期连续回撤均值基线: {:.4f}'.format(m02_training_drawdown_mean)) # 输出连续路线的朴素基线
print(f'训练期事件率概率基线: {m02_training_event_rate:.4f}') # 只公开开发所需的训练基线
assert 0 < m02_training_event_rate < 1 # 确认训练期同时包含事件与非事件 split company_count sample_count first_prediction_date last_prediction_date
test 4 2824 2022-01-04 2024-12-03
train 4 7572 2012-02-08 2019-12-03
validation 4 1864 2020-01-02 2021-12-03
训练期连续回撤均值基线: -0.0840
训练期事件率概率基线: 0.2896
这一步没有证明任何特征具有预测价值,只建立了可复核的比较对象。m02_shared_sample 的权威有序模式依次为公司键、预测日、标签末日、四项 m02_features、连续目标 max_drawdown_20d、由它派生的二元标签 drawdown_event_20d 和 split。后续所有模型必须使用相同键、两项目标定义、特征顺序和日期边界;测试段只保存键,不参与调参、变量选择或阈值选择。
2.8 练习
2.8.1 概念题
区分预测、条件关联与因果效应,各给出一个中国资本市场问题,并说明哪类额外证据才能把观察关联升级为干预结论。 [核心|难度:1|分值:6|任务:独立]
解释为什么训练误差通常不会随模型灵活性增加而上升,而验证误差可能上升。 [核心|难度:1|分值:6|任务:独立]
在 \(Y=f(X)+\epsilon\) 中,区分误差 \(\epsilon_i\) 与拟合后的残差 \(e_i\)。 [核心|难度:1|分值:6|任务:独立]
说明为什么在异方差情形下,不可约误差应写成 \(\operatorname{Var}(\epsilon\mid X=x)\)。 [核心|难度:2|分值:7|任务:独立]
2.8.2 应用题
使用 小节 2.7 的本地后复权行情构造
m02_shared_sample,逐项核对有序字段、唯一键、label_end_date、连续目标与二元标签的恒等关系、末端未知目标、跨段清除,以及训练期连续均值和事件率基线;不得读取测试目标分布。 [核心|难度:3|分值:20|任务:综合案例]某模型训练 MSE 为 0,验证 MSE 却高于均值基线。给出诊断,并提出两项必须通过验证数据检查的改进。 [核心|难度:2|分值:15|任务:独立]
2.8.3 理论题
固定 \(X_0=x_0\),推导 式 2.3,并明确期望针对哪些随机对象。 [拓展|难度:3|分值:20|任务:独立]
证明在没有预测变量、采用平方损失时,使 \(\sum_{i=1}^n(y_i-c)^2\) 最小的常数预测为样本均值。 [核心|难度:2|分值:20|任务:独立]
展开完整参考解答与评分键
2.9 练习参考解答
2.9.1 概念题解答
预测关注未见样本的误差,例如用截至 \(t\) 日的信息预测 \(t+1\) 日收益;条件关联描述控制已纳入变量后两个变量如何共变,例如给定公司规模后研发强度与利润率的样本关联;因果效应问干预会怎样改变结果,例如交易费下调是否改变流动性。预测准确和系数显著都不自动提供因果识别。干预结论需要随机实验,或具备可信识别假设的自然实验、断点、工具变量、双重差分等准实验设计;在观察研究中还须明确时间顺序、混杂控制、重叠性、无干扰和敏感性边界。
更灵活的候选空间包含或近似包含较简单模型,因此优化后的训练损失通常不升;但灵活模型也可能拟合训练样本的偶然噪声,使其对训练集抽取更敏感,方差上升。验证误差是否上升取决于信号、噪声、样本量和候选范围,不能预设每次都出现完整 U 型。
\(\epsilon_i=Y_i-f(X_i)\) 是相对于未知真实条件均值的随机误差,通常不可观测;\(e_i=Y_i-\hat f(X_i)\) 是使用已拟合模型计算的样本残差。残差同时包含随机误差与函数估计误差,不能当作真实误差本身。
异方差表示给定不同 \(x\) 时误差方差不同。即使知道真实 \(f(x)\),预测点 \(x\) 的最小平方误差仍包含 \(\operatorname{Var}(\epsilon\mid X=x)\);只有同方差时才可把它统一写成常数 \(\sigma^2\)。
2.9.2 应用题解答
完整实现与评分量规(20 分):调用 小节 2.7 的
build_m02_shared_sample(BOOK_DATA_DIR),最终得到m02_shared_sample、m02_features、m02_training_event_rate与m02_test_keys。有序模式必须为order_book_id、prediction_date、label_end_date、四项m02_features、max_drawdown_20d、drawdown_event_20d、split。连续目标必须从 \(t\) 至 \(t+20\) 路径的运行峰值计算,有限且不大于 0;二元标签必须逐行等于 \(\mathbb 1(\texttt{max\_drawdown\_20d}\leq-0.10)\)。[100,120,105]应得到 \(-12.5\%\) 并记为事件。正确结果还要求唯一键无重复、训练与验证标签结束日早于下一段起点且三段非空。若断言失败,检查公司内排序、运行峰值方向、窗口完整性和日期类型。评分为:有序字段与数据 4 分、连续目标/二元标签及单元样例 4 分、时间边界 4 分、断言与训练期双基线 4 分、结论边界 4 分。测试段只核对键和日期,不报告目标分布或模型成绩。训练 MSE 为 0 而验证表现更差,是高方差或数据泄漏的警报。首先检查切分、特征日期和预处理是否使用未来信息;其次在训练期验证中比较降低多项式次数、限制树深或加强正则化后的损失。只有验证误差稳定改善才接受改动,不能依据测试结果反复调参。
2.9.3 理论题解答
固定 \(x_0\) 与训练集 \(\mathcal D\),令 \(Y_0=f(x_0)+\epsilon_0\),其中 \(E(\epsilon_0\mid X_0=x_0)=0\) 且 \(\epsilon_0\) 与 \(\mathcal D\) 独立。展开得
\[ \begin{aligned} E_{\epsilon_0}[(Y_0-\hat f_{\mathcal D}(x_0))^2\mid x_0,\mathcal D] &=E_{\epsilon_0}[(f(x_0)-\hat f_{\mathcal D}(x_0)+\epsilon_0)^2]\ &=[f(x_0)-\hat f_{\mathcal D}(x_0)]^2 +2[f(x_0)-\hat f_{\mathcal D}(x_0)]E(\epsilon_0\mid x_0)\ &\quad+E(\epsilon_0^2\mid x_0)\ &=[f(x_0)-\hat f_{\mathcal D}(x_0)]^2 +\operatorname{Var}(\epsilon_0\mid x_0). \end{aligned} \]
若继续对训练集 \(\mathcal D\) 取期望,第一项再分成偏差平方与方差。
令 \(Q(c)=\sum_{i=1}^n(y_i-c)^2\)。一阶导数为 \(Q'(c)=-2\sum_i(y_i-c)=-2n(\bar y-c)\),令其为零得到 \(c=\bar y\);二阶导数 \(Q''(c)=2n>0\),所以这是唯一全局最小值。等价地, \[\sum_i(y_i-c)^2=\sum_i(y_i-\bar y)^2+n(c-\bar y)^2,\] 第二项仅在 \(c=\bar y\) 时为零。
2.10 章末回顾
本章建立了全书的共同框架:先定义预测时点与损失,再用训练期验证证据选择复杂度,最后只用测试集评价锁定模型。继续学习前,应能解释误差与残差、样本内与样本外、关联与因果,以及偏差—方差分解中每个期望的随机对象。
无提示检索
- 为什么训练误差不能单独决定模型复杂度?
- 为什么偏差—方差示意图不能证明每个任务都有唯一的 U 形最优点?
- 为什么每个样本的
label_end_date必须早于下一评分区间?
展开检索反馈与定向返回
若第 1 题不确定,返回“拟合质量与泛化能力”;若第 2 题不确定,返回“偏差—方差权衡”;若第 3 题不确定,返回 小节 2.7 的时间切分与标签审计。
下一章将把这一框架落到线性回归:先估计条件均值,再建立同一回撤事件任务的线性概率基线。