import numpy as np # 导入numpy用于数值运算和多项式拟合
import matplotlib.pyplot as plt # 导入matplotlib绘图库用于可视化
import pandas as pd # 导入pandas用于数据框操作
import os # 导入os模块用于跨平台路径处理
# 根据操作系统自动选择本地数据根目录路径
# 根据操作系统设置数据根目录路径
DATA_ROOT = 'C:/qiufei/data' if os.name == 'nt' else '/home/ubuntu/r2_data_mount/qiufei/data'
# 拼接财务报表h5文件的完整路径
FINANCIAL_STMT_PATH = os.path.join(DATA_ROOT, 'stock/financial_statement.h5') # 构建数据文件的完整路径
# 设置全局随机种子确保每次运行抽样结果一致
np.random.seed(42) # 设置随机种子确保结果可复现
financial_statements_data = pd.read_hdf(FINANCIAL_STMT_PATH) # 加载全量A股上市公司财务报表数据
# 筛选2023年年报数据,同时剔除净利润或总资产为缺失值的记录
# 删除缺失值
latest_annual_data = financial_statements_data[financial_statements_data['quarter'] == '2023q4'].dropna(subset=['net_profit', 'total_assets'])
representative_sample = latest_annual_data.sample(30) # 从筛选后的数据中随机抽取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) # 计算理论曲线上的预测利润3 统计学习 (Statistical Learning)
3.1 什么是统计学习? (What Is Statistical Learning?)
假设我们是一家位于中国长三角地区的金融科技公司的数据分析顾问,需要研究上市公司的财务投入与经营产出之间的关系。我们收集了200家长三角地区制造业上市公司的数据,包括它们的营业收入以及在三方面的投入:研发费用、销售费用和管理费用。数据如 图 3.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{3.1}\]
其中,\(f\)是某个固定但未知的\(X_1, \ldots, X_p\)的函数,\(\epsilon\)是一个随机误差项(random error term),独立于\(X\)且均值为零。在这个公式中,\(f\)代表了\(X\)提供的关于\(Y\)的系统信息(systematic information),而\(\epsilon\)代表了未被\(X\)解释的随机噪音。
Tip: 理解系统信息与随机噪音
系统信息\(f(X)\)是变量之间可预测的、稳定的模式。例如,广告投入越多,销售量通常越高——这是一种稳定的规律。而随机噪音\(\epsilon\)则包含了所有不可预测的因素,比如某一天突发的天气变化、竞争对手的意外促销活动等。
在实际数据分析中,我们的目标就是从充满噪音的数据中提取出有价值的系统信息。这就像在嘈杂的环境中聆听美妙的音乐——统计学习帮助我们”过滤”噪音,专注于真正的规律。
作为另一个例子,考虑 图 3.2 的左侧面板,这是一个来自长三角制造业上市公司的30个样本的净利润与总资产关系的散点图。该图表明,我们可能能够使用总资产规模来预测公司净利润。然而,连接输入变量和输出变量的函数\(f\)通常是未知的。在这种情况下,必须基于观测点来估计\(f\)。
下面的Python代码演示了如何从本地获取长三角地区制造业上市公司的实际财务数据(总资产与净利润),并从中随机抽取30个样本。在此基础上,代码模拟并拟合出了一条平滑的非线性曲线作为“真实”的结构关系 \(f(X)\)。图表分为左右两个面板展示:左侧(观测数据)再现了我们在现实世界中能看到的杂乱无章的散点;右侧(真实关系与测量误差)则同时画出了这条潜在的、完美的平滑折线,并通过垂直虚线将每一个实际观测点与平滑曲线连接。这些虚线直观地代表了等式 式 3.1 中的随机噪音 \(\epsilon\)——也就是在总资产规模之外,影响净利润波动的所有其他不可观测因素的总和。
以上代码加载了长三角制造业上市公司的真实财务数据,并从中随机抽取了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='统计映射 f(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('真实关系与测量误差 (Systematic vs Error)', fontsize=14, fontweight='bold', color='#2C3E50') # 右图标题
ax_rel.legend(frameon=True, facecolor='white') # 添加白色背景图例
plt.tight_layout() # 自动调整子图布局
plt.show() # 显示双面板对比图
图 3.2 的运行结果展示了两幅并列的散点图。左侧面板直接呈现了30家长三角制造业上市公司的财务数据散点——每一个红色圆点代表一家公司,其横坐标为该公司的总资产规模(单位:百亿元),纵坐标为对应的年净利润(单位:亿元)。我们可以从中观察到一个直觉上合理的正相关趋势:总资产规模更大的公司,其净利润往往也更高,但这种关系并非完全确定性的——散点围绕趋势呈现出明显的随机波动。右侧面板在相同的散点之上叠加了一条平滑的深色二次多项式曲线,这条曲线就是我们对潜在真实关系 \(f(X)\) 的近似估计。连接每个散点到曲线的灰色虚线清晰地展示了误差项 \(\epsilon\) 的概念——它代表了现实观测值与理论预测值之间不可避免的偏差。有的公司实际利润高于曲线预测(正误差),有的低于预测(负误差),这正是因为净利润不仅取决于总资产,还受到管理效率、行业周期、竞争格局等诸多无法被单一变量完全捕捉的随机因素影响。
通常,函数\(f\)可能涉及不止一个输入变量。在 图 3.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以上代码构建了一个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) # 在等高线上标注数值
# 设置随机种子以确保模拟观测样本可复现
np.random.seed(42) # 设置随机种子确保结果可复现
sample_count = 35 # 模拟35个观测样本
edu_obs = np.random.uniform(10, 22, sample_count) # 随机生成观测样本的教育年限
work_obs = np.random.uniform(0, 40, sample_count) # 随机生成观测样本的工龄
# 根据真实映射函数计算35个观测样本的理论收入基准值
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) # 添加高斯噪声得到"观测"收入
# 在等高线图上叠加观测样本散点
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() # 显示收入等高线图
图 3.3 的运行结果是一张色彩丰富的等高线图(Contour Plot)。在这张图中,横轴代表受教育年限(10年至22年),纵轴代表工作年限(0年至40年),而填充的颜色深浅则编码了第三个维度——预期年收入的高低。暖色调(红色、橙色)区域对应较高的预期收入水平,冷色调(蓝色)区域对应较低的收入水平。从图中可以清晰观察到:教育年限对收入的边际影响非常显著——沿着横轴从左向右移动时,颜色从深蓝迅速过渡为暖红色,这意味着高学历人群的预期收入明显更高;而工龄(纵轴)的影响相对温和,在中等教育水平的条件下,收入随工龄的增长呈先升后略降的趋势(反映了模型中工龄二次项的负系数效应)。散布在图上的35个红色圆点是带有随机噪声的模拟观测样本,它们围绕在等高线”正确的”颜色区域附近但并不完全吻合,正好直观呈现了 \(Y = f(X_1, X_2) + \epsilon\) 这一核心等式中随机误差项 \(\epsilon\) 的存在。
本质上,统计学习(statistical learning)指的就是一组用于估计\(f\)的方法。在本章中,我们将概述在估计\(f\)时出现的一些关键理论概念,以及评估所得结果的工具。
3.1.1 为什么要估计\(f\)? (Why Estimate \(f\)?)
我们希望估计\(f\)主要有两个原因:预测(prediction)和推断(inference)。我们将逐一讨论。
3.1.1.1 预测 (Prediction)
在许多情况下,一组输入\(X\)很容易获得,但输出\(Y\)无法轻易获得。在这种情况下,由于误差项平均为零,我们可以使用以下公式预测\(Y\):
\[ \hat{Y} = \hat{f}(X) \tag{3.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{3.3}\]
其中,\(E\)表示期望值。这个公式揭示了预测误差的两个来源:
- 可约误差(reducible error):\([f(X) - \hat{f}(X)]^2\)。这部分误差可以通过使用更合适的统计学习技术来估计\(f\)而减少。
- 不可约误差(irreducible error):\(\text{Var}(\epsilon)\)。这部分误差即使我们使用完美的估计\(\hat{f} = f\)也无法消除,因为\(\epsilon\)包含了\(X\)未解释的\(Y\)的变异。
Tip: 理解不可约误差
不可约误差的存在是因为在现实世界中,没有任何模型能够完美地预测结果。考虑我们电商公司的案例:即使我们建立了完美的模型来预测销售量,仍然有一些不可控因素会影响实际销售,例如:
- 突发的公共卫生事件
- 竞争对手的意外促销
- 天气变化对消费者行为的影响
- 社交媒体上的意外舆论
因此,设置现实的期望很重要:我们的目标不是达到100%的预测准确率,而是将预测误差降到接近不可约误差的水平。
3.1.1.2 推断 (Inference)
我们关心估计\(f\)的第二个原因是理解\(Y\)如何随着\(X_1, X_2, \ldots, X_p\)的变化而变化。在这种情况下,我们希望将\(\hat{f}\)看作一个透明、可解释的模型,而不是黑盒。
问题示例:
- 哪些媒体与销售有关?:这有助于公司决定应该重点投资哪种媒体渠道。
- 哪些媒体对销售的影响最大?:了解不同媒体的相对影响力可以帮助公司优化营销预算分配。
- 网络广告和销售之间的关系是线性的还是非线性的?:了解这种关系的性质可以帮助公司制定更有效的营销策略。
- 是否存在很强的交互效应?:例如,在电视广告和网络广告同时投放时,销售量的提升是否比单独投放时更大?
在这种情况下,可能更倾向于使用简单且高度可解释的模型。这类模型包括线性回归、逻辑回归等,它们的参数有明确的解释意义。
3.1.2 如何估计\(f\)? (How Do We Estimate \(f\)?)
在统计学习中,我们有许多用于估计\(f\)的方法。这些方法可以大致分为两类:参数化方法(parametric methods)和非参数化方法(non-parametric methods)。
3.1.2.1 参数化方法 (Parametric Methods)
参数化方法分为两个步骤:
第一步:对函数形式做出假设 首先,我们对\(f\)的形式或形状做出假设。例如,一个非常简单的假设是\(f\)是线性的: \[ f(X) = \beta_0 + \beta_1 X_1 + \beta_2 X_2 + \cdots + \beta_p X_p \tag{3.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\)等。
因此,‘参数化方法’(parametric methods)指的是那些假设数据遵循具有有限个未知参数的特定概率分布的方法,而不是指’有参数的方法’(这会造成混淆,因为几乎所有方法都有参数)。
3.1.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)
- 主要目标是预测准确度,而非解释性
- 变量之间的关系复杂、未知或高度非线性
- 愿意接受计算成本较高的模型
在实际项目中,数据科学家通常会尝试多种方法,并通过交叉验证来比较它们的性能。
3.1.3 预测准确度与模型可解释性的权衡 (Trade-Off Between Prediction Accuracy and Model Interpretability)
在统计学习的商业应用中,我们经常面临一个核心抉择:是为了极致的预测性能牺牲透明度,还是为了获得可控的、能被人理解的逻辑而牺牲部分准确度?在整个本书中,我们将看到,灵活的(flexible)统计学习方法通常能够更准确地拟合训练数据。然而,如果这些方法过于灵活,它们可能会导致过拟合(overfitting)——即它们捕捉到了训练数据中的随机噪音,而不是真实的模式。
案例:量化选股中的模型选择 在构建多因子量化选股策略时,深度神经网络(Deep Neural Networks)由于高度灵活,可能在历史数据中发现复杂的非线性信号并实现高额回测收益。然而,当神经网络重仓某只基本面极差的股票时,基金经理几乎无法解释其背后的逻辑。由于这种“黑盒”模型缺乏可解释性,投资委员会通常会拒绝使用。相反,基金经理往往会退而求其次,使用线性因子多分类模型(如 Fama-French 模型)。尽管后者的预测准确度可能稍逊一筹,但其赋予每个因子(如估值、动量)明确且符合经济学直觉的权重。
图 3.4 展示了不同统计学习方法在灵活性和可解释性之间的权衡。子集选择(Subset Selection)、套索回归(Lasso)和岭回归(Ridge)等方法限制性较强(灵活性较低),因此更容易解释。广义加法模型、决策树、bagging、boosting等方法更加灵活,而深度神经网络等方法极其灵活,但较难解释。
为了呈现这一核心理论概念,下面的 Python 代码利用 matplotlib 手工构建了一张展示统计学习方法在“灵活性”与“可解释性”之间权衡的经典学术散点图。代码中明确定义了 8 种初中高级的机器学习模型在这个二维光谱上的相对位置(例如,Lasso 回归位于左上方代表高解释性低灵活性,而深度学习位于右下方代表低解释性高灵活性)。我们通过不同的颜色和节点文字对比,以及辅助斜虚线,强化了这张图表的理论表达力。理解这一图表也构成了我们在后续各章节中针对具体业务寻找、选择和评估各类算法的指南针。
import matplotlib.pyplot as plt # 导入matplotlib用于可视化
# 设置中文字体为思源宋体,确保中文标签正确显示
plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS']
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() # 显示灵活性-可解释性权衡图
图 3.4 的运行结果呈现了一张将8种统计学习方法按”灵活性”和”可解释性”两个维度定位的学术散点图。图中从左上角到右下角的灰色虚线清晰地刻画了一条负相关趋势线:越靠近左上角的方法,可解释性越强但灵活性越低;越靠近右下角的方法,灵活性越高但模型的内部机制越难以用人类直觉理解。具体来看:Lasso回归和最小二乘法(OLS)位于左上方的”高可解释性”区域——它们输出的系数可以直接告诉我们”每增加一个单位的广告投入,销售额预期增加多少”,因此非常适合需要向管理层汇报因果推断结论的商业场景;广义加性模型(GAM)和Bagging/Boosting位于中间区域,在灵活性和可解释性之间取得了较好的折中;而支持向量机(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章介绍无监督学习方法。
3.1.4 回归vs分类问题 (Regression Versus Classification Problems)
变量可以是定量的(quantitative)或定性的(qualitative/categorical)。
- 定量变量:取值为数值大小,例如收入、身高、温度、销售量
- 定性变量:取值为类别或标签,例如性别(男/女)、邮件类型(垃圾邮件/正常邮件)、客户类别(高价值/中价值/低价值)
那些涉及定量响应变量的问题称为回归问题(regression problems),而那些涉及定性响应变量的问题称为分类问题(classification problems)。
不过,这个区分有时有些模糊。例如,逻辑回归(logistic regression)是一种用于分类的方法,但其名称中包含”回归”二字。这是因为逻辑回归本质上是预测类别的概率,这是一个定量问题。
3.2 评估模型准确度 (Assessing Model Accuracy)
在统计学习中,没有一个方法在所有数据集上都优于其他所有方法。这就是著名的没有免费午餐定理(No Free Lunch Theorem)。因此,我们需要评估不同方法在给定数据集上的表现,以便选择最佳方法。
3.2.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{3.5}\]
其中,\((x_i, y_i)\)是训练数据中的第\(i\)个观测,\(\hat{f}(x_i)\)是基于训练数据估计的函数在\(x_i\)处的预测值。
Tip: 为什么使用平方误差?
你可能好奇,为什么使用平方误差而不是绝对误差?这里有几个原因:
- 数学便利性:平方函数是可微的,这使得我们可以使用微积分来寻找最优解
- 对大错误的惩罚更重:平方误差对大误差的惩罚大于小误差,这符合许多实际应用的需求(例如,预测误差为10的错误是误差为5的4倍,而不是2倍)
- 与高斯噪声的联系:如果误差服从正态分布,最小化平方误差等价于最大化似然函数
然而,绝对误差(MAE)在某些情况下也有优势,特别是当数据中存在异常值时,因为它对异常值不那么敏感。
3.2.1.1 训练误差vs测试误差 (Training Error vs. Test Error)
在实践中,我们感兴趣的不仅仅是模型在训练数据上的表现,而是模型在未见过的新数据上的表现。因此,我们需要区分:
- 训练MSE (Training MSE):在训练数据上计算的MSE
- 测试MSE (Test MSE):在独立的测试数据上计算的MSE
我们希望选择在测试数据上MSE最小的模型,因为这意味着模型具有良好的泛化能力(generalization ability)。
案例:电商销售预测的模型选择
让我们通过一个模拟案例来说明训练误差和测试误差的区别。假设真实的关系是:
\[ \text{销售量} = 5 + 2 \times \text{电视广告} + 1.5 \times \text{网络广告} + \epsilon \]
其中,\(\epsilon \sim N(0, 2^2)\)是随机误差。
以下的代码通过一场详尽的蒙特卡洛随机模拟,为你栩栩如生地重现了这种“过拟合”陷阱与泛化表现分裂。首先,我们模拟了 120 个长三角电商公司的营销预算(特征 \(X\))和真实存在三次多项式关系的销售量(目标 \(Y\)),并在其中加入了高斯噪声干扰。接着,我们严格按照时间序列演进逻辑将数据切割为训练数据子集和全新的测试验证子集。最核心的拟合循环部分中,我们借助 scikit-learn 的 PolynomialFeatures 和 LinearRegression 对象,不断测试并估计从 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输入要求每个特征为一列)
split_idx = 70 # 前70个样本作为训练集,后50个作为测试集
budget_features_train, budget_features_test = marketing_budget_features[:split_idx], marketing_budget_features[split_idx:] # 切分特征
sales_target_train, sales_target_test = observed_sales_revenue[:split_idx], observed_sales_revenue[split_idx:] # 切分目标上面的代码完成了数据生成和训练/测试集切分。接下来,我们遍历不同复杂度的多项式模型(从1阶到11阶),分别记录每个模型在训练集和测试集上的均方误差,最终揭示过拟合现象。
# 定义要评估的多项式阶数范围:从简单线性(1阶)到极度灵活(11阶)
polynomial_degree_range = range(1, 12) # 定义1到11阶的多项式阶数范围,用于逐步提升模型复杂度
training_mse_history = [] # 用于存储各阶数模型在训练集上的MSE
testing_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_test_transformed = poly_transformer.transform(budget_features_test)
estimating_model = LinearRegression() # 实例化线性回归模型
estimating_model.fit(budget_features_train_transformed, sales_target_train) # 在训练集上拟合模型
# 用拟合后的模型对训练集生成预测值
sales_train_predictions = estimating_model.predict(budget_features_train_transformed)
# 用拟合后的模型对测试集生成预测值
sales_test_predictions = estimating_model.predict(budget_features_test_transformed)
# 计算当前模型在训练集上的均方误差并追加到历史列表
training_mse_history.append(mean_squared_error(sales_target_train, sales_train_predictions))
# 计算当前模型在测试集上的均方误差并追加到历史列表
testing_mse_history.append(mean_squared_error(sales_target_test, sales_test_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, testing_mse_history, 's-', color='#EF4444', linewidth=3, label='测试 MSE (Testing/Generalization)')
# 寻找测试MSE最低点对应的多项式阶数
optimal_degree_idx = np.argmin(testing_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(testing_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() # 显示训练-测试误差对比图
图 3.5 展示了一个重要现象:
训练MSE:随着模型灵活性增加(多项式次数增加),训练MSE单调下降。这是因为更灵活的模型能够更好地拟合训练数据,甚至包括噪音。
测试MSE:呈现U型曲线。在模型灵活性较低时(欠拟合),测试MSE较高;随着灵活性增加到最优水平,测试MSE下降到最低点;但继续增加灵活性(过拟合),测试MSE反而上升。
这种现象揭示了统计学习的核心挑战:我们需要在模型灵活性和泛化能力之间找到最佳平衡点。
3.2.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{3.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: 直观理解偏差和方差
想象你在练习射击靶子:
- 低偏差,低方差:所有子弹都打在靶心附近(理想情况)
- 低偏差,高方差:子弹分布很分散,但平均位置在靶心(模型过拟合,对训练数据敏感)
- 高偏差,低方差:子弹很集中,但都偏离靶心(模型欠拟合,过于简化)
- 高偏差,高方差:子弹分散且偏离靶心(模型很差)
在统计学习中,我们的目标是找到偏差和方差的最佳平衡,使总误差最小。
图 3.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() # 显示偏差-方差权衡图
关键要点:
灵活性增加 → 偏差减少:更灵活的模型能够更好地拟合数据中的模式,减少系统性偏差。
灵活性增加 → 方差增加:更灵活的模型对训练数据的特定样本更敏感,导致不同训练数据产生的模型差异更大。
最优模型:在偏差和方差之间找到最佳平衡,使总测试MSE最小。
不可约误差:无论模型多么灵活,测试MSE都不会低于不可约误差的水平,这是因为数据中存在固有的随机噪音。
3.2.3 分类设置 (The Classification Setting)
在分类问题中,我们关注的是错误率(error rate)或准确率(accuracy):
\[ \text{错误率} = \frac{1}{n} \sum_{i=1}^{n} I(y_i \neq \hat{y}_i) \tag{3.7}\] \[ \text{准确率} = 1 - \text{错误率} \tag{3.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)是假负例。
在类别不平衡的情况下(例如,罕见病检测,正类样本很少),准确率可能具有误导性。在这种情况下,精确率和召回率可能更有意义。
3.3 本章小结 (Chapter Summary)
本章介绍了统计学习的基础概念:
统计学习的定义:统计学习是一组用于估计函数\(f\)的方法,该函数将输入变量\(X\)映射到输出变量\(Y\)。
估计\(f\)的目的:
- 预测:使用\(\hat{f}(X)\)来预测\(Y\)
- 推断:理解\(Y\)如何随\(X\)的变化而变化
估计\(f\)的方法:
- 参数化方法:假设函数形式,然后估计参数
- 非参数化方法:不假设函数形式,让数据”说话”
预测准确度与模型可解释性的权衡:更灵活的模型通常更难解释,但可能产生更准确的预测。
评估模型准确度:
- 在回归问题中使用均方误差(MSE)
- 在分类问题中使用错误率或准确率
偏差-方差权衡:测试误差可以分解为偏差平方、方差和不可约误差三部分。最优模型在偏差和方差之间找到最佳平衡。
在接下来的章节中,我们将详细讨论具体的统计学习方法,包括线性回归、分类方法、重采样技术、线性模型选择与正则化、非线性模型等。
3.4 理论来源与前沿
本章的统一框架——用损失函数刻画拟合质量、用复杂度刻画模型灵活度、用测试误差衡量泛化表现——一方面继承了统计学的风险最小化思想,另一方面与计算学习理论中的 PAC/VC 观点相呼应。偏差-方差分解把‘模型太简单’与‘模型太复杂’两类误差统一到同一个分解式中,从而为模型选择、正则化与误差评估提供了可操作的语言。
近年来的研究前沿主要集中在三个方向:
- 可解释性与可审计性:在金融风控、信贷审批、保险定价等场景中,模型不仅要准确,还要能解释、可复核。
- 分布漂移与稳健学习:训练分布与部署分布不一致时,需要稳健估计、域自适应与在线更新机制。
- 因果视角的学习:将‘预测’与‘干预效果评估’区分开,在政策评估、营销增量与运营实验中尤为关键。
3.5 练习
3.5.1 概念题
预测与推断:用你自己的语言解释统计学习中“预测”与“推断”的区别。请各举一个中国金融市场的例子(如预测股价与研究拆股效应)。
偏差-方差权衡:解释为什么增加模型灵活性通常会降低训练误差,但可能导致测试误差的显著上升。
不可约误差:在 式 3.1 中,为什么我们无法消除 \(\epsilon\)?列举两个在分析 A 股上市公司财报时,可能导致不可约误差的现实因素。
过度灵活的风险:如果一个模型在训练集上的 MSE 为 0,但在验证集上的表现极差,这处于什么状态?你会建议通过减少特征还是增加特征来缓解?
3.5.2 应用题
- 本地行情探索 (回归任务):
- 使用 Python 读取本地
stock/stock_price_post_adjusted.h5数据。 - 选择长三角地区任意一家上市公司。
- 构造预测任务:使用过去 5 个交易日的对数收益率来预测下一日的对数收益率。
- 比较简单的线性回归与一个你认为更灵活的模型(如 KNN,K=5)在测试集上的表现。
- 使用 Python 读取本地
- 业务流程建模:假设你正在为一家中国的商业银行设计“信用卡违约预测系统”。请描述从业务问题转化为统计学习问题的完整流程,包括样本选取(如何处理 T+1 效应)、特征构造及评估指标的选择。
3.5.3 理论题
均方误差分解:在平方损失下,证明: \[ 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\))。
样本均值的最优性:证明在没有预测变量的情况下,\(Y\) 的平方损失最小化估计量是样本均值 \(\bar{Y}\)。
3.6 练习参考解答
3.6.1 概念题解答
预测与推断:
- 预测:关注 \(\hat{Y}\) 的准确度,将模型视为黑盒。例子:基于技术指标预测明日上证综指及其区间。
- 推断:关注 \(X\) 对 \(Y\) 的影响路径和显著性。例子:研究上市公司实施股权激励(ESOP)是否显著提升了其 ROE。
偏差-方差权衡:更灵活的模型能够追踪训练数据中的每一处波动(低偏差),但这种“过度贴合”往往捕捉了仅属于该样本的随机噪声(高方差)。当应用于新样本时,这些噪声模式不复存在,导致预测失败。
不可约误差:\(\epsilon\) 包含了未被模型观测到的所有变量。因素包括:(1) 企业报表可能存在的会计处理差异造成的随机测量误差;(2) 外部突发黑天鹅事件(如政策突变或自然灾害)。
状态与建议:这是典型的过拟合 (Overfitting) 状态。建议通过减少特征(降维)、实施惩罚项(正则化)或增加训练样本量来缓解。
3.6.2 应用题解答
- 股价预测代码实现:
在这个应用题的参考代码中,我们尝试对比经典线性模型与灵活模型(K-近邻)在处理真实中国股票长序列时间数据时的表现。代码首先加载了上海汽车在长三角的代表股价面板数据,并通过 pandas 平移函数 shift 分别构造了过去 1 到 5 天的历史收益率回报作为输入学习特征簇(Features),并与明天(shift(-1))的真实回报构建监督预测任务对。在此严密准备基础上,系统将严格不发生交叉的切分出来的训练时序数据分别投入经典的 OLS 线性回归回归器和 KNN (K=5) 距离计算回归器上进行监督拟合估测。如同代码末尾 print 语句所计算并输出比对的 MSE 损失那样,读者会发现,对于这类富含极强随机噪音的有效金融时间序列,如果盲目追求和使用过于灵活或者过拟合的诸如 KNN 的模型,实际上有时反而可能在绝对未见过的纯洁测试集上全线面临预测滑铁卢(这是典型的高模型变异方差引致的过拟合反噬),而看似枯燥的普通单调线性模型反而可能依然维持着不可替代的基础稳健分析价值点。
import pandas as pd # 导入pandas库用于数据框操作
import numpy as np # 导入numpy库用于数值计算
from sklearn.linear_model import LinearRegression # 导入线性回归模型
from sklearn.neighbors import KNeighborsRegressor # 导入K-近邻回归模型
from sklearn.metrics import mean_squared_error # 导入MSE评估指标函数
import os # 导入os模块用于跨平台路径处理
# 根据操作系统自动选择本地数据存储根目录
# 根据操作系统设置数据根目录路径
DATA_DIR = 'C:/qiufei/data' if os.name == 'nt' else '/home/ubuntu/r2_data_mount/qiufei/data'
# 拼接后复权股价数据文件的完整路径
PRICE_DATA_FILE = os.path.join(DATA_DIR, 'stock/stock_price_post_adjusted.h5') # 拼接后复权股价数据文件的完整路径
equity_price_dataframe = pd.read_hdf(PRICE_DATA_FILE).reset_index() # 从本地读取全市场后复权日度行情数据并重置MultiIndex
# 筛选上汽集团(600104.XSHG)的历史交易记录,创建数据副本避免修改原始数据
shanghai_auto_data = equity_price_dataframe[equity_price_dataframe['order_book_id'] == '600104.XSHG'].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%分割点的整数索引,用于时间序列训练/测试切分
train_slice = modeling_final_df.iloc[:split_point] # 前80%作为训练数据
test_slice = modeling_final_df.iloc[split_point:] # 后20%作为测试数据
# 定义用于建模的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}') # 计算均方误差Linear MSE: 0.00034387
KNN MSE: 0.00046130
上述代码的运行结果输出了两行关键数值:Linear MSE: 0.00034387 和 KNN MSE: 0.00046130。这一结果非常值得深入分析。线性回归模型(Linear MSE ≈ 0.000344)的测试集均方误差明显低于KNN模型(KNN MSE ≈ 0.000461),这意味着在上汽集团股票日收益率这一金融时间序列数据上,更”简单”的线性模型反而比更”灵活”的KNN模型拥有更好的泛化预测能力。为什么会出现这种看似反直觉的结果?核心原因在于金融收益率数据的信噪比极低(即噪声 \(\epsilon\) 远大于系统性信号 \(f(X)\))。在这种高噪声环境下,KNN模型由于过度灵活,容易将训练数据中的随机噪声当作真实模式来”记忆”(即过拟合),导致在未见过的测试数据上表现不佳。相比之下,线性模型因其内在的结构约束,天然地抵御了对噪声的过度拟合。这个结果完美印证了本章”偏差-方差权衡”的核心理论:更灵活的模型并不总是更好,选择合适的模型复杂度比盲目追求灵活性更为重要。
- 业务建模流程:
- 目标:预测未来 6 个月内是否会发生分期业务逾期。
- 样本:选取截至观测日的存量客户,注意剔除已违约样本;需保留 6 个月观察期。
- 特征:近半年月均消费额、信用额度利用率、查询征信次数、社保缴纳稳定性。
- 指标:AUC (区分度)、Recall (捕获风险能力)、Expected Profit (业务价值)。
3.6.3 理论题解答
推断过程见 小节 3.5 的推导小节。其核心在于展开 \((y - \hat{f} + E\hat{f} - E\hat{f})^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\),故为最小值点。