经典回归理论

预计学习时间:3-4 小时

难度:⭐⭐⭐

核心问题:如何正确地用线性模型从数据中提取可信的关系?你的回归结果真的可信吗?


概述

经典回归理论是计量经济学的地基。量化中几乎所有的统计推断——因子收益估计、策略归因、风险模型——最终都建立在回归分析之上。

但大多数人只知道”跑回归”,不知道:

  • 回归结果在什么条件下才是可信的
  • 结果不可信时怎么诊断和修正
  • 更糟糕的情况——因果方向搞反了——怎么处理

本文件覆盖回归分析从基础到进阶的完整链条。


一、OLS 最小二乘法

1.1 白话直觉

假设你有一组数据:每个股票的”账面市值比”和它”下个月的收益率”。你怀疑两者有线性关系:

OLS 要做的事,就是找一条最”贴合”所有数据点的线。

什么叫”最贴合”?直觉上,就是让所有数据点到这条线的距离总和最小。具体来说,是让残差(实际值减去预测值)的平方和最小。

为什么用平方?因为直接加距离会正负抵消(线上方为正、下方为负),而平方保证所有偏差都被”算上”。

1.2 数学推导

设我们有 个观测值, 个解释变量。模型可以写成矩阵形式:

其中:

  • 的被解释变量(如收益率)
  • 的解释变量矩阵(包含常数项列)
  • 的系数向量(我们要估计的)
  • 的误差项

目标函数——最小化残差平方和:

展开:

求导并令其为零:

得到正规方程(Normal Equation):

假设 可逆,解为:

这就是 OLS 估计量的闭式解。

1.3 Python 手写 OLS + 与 statsmodels 对比

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
 
np.random.seed(42)
 
# ============================================================
# 模拟数据:账面市值比 vs 收益率
# ============================================================
n = 200  # 200 只股票
# 账面市值比(BM)
bm = np.random.normal(1.0, 0.5, n)
# 真实关系:收益率 = 0.005 + 0.02 * BM + 噪声
alpha_true = 0.005
beta_true = 0.02
y = alpha_true + beta_true * bm + np.random.normal(0, 0.05, n)
 
# ============================================================
# 方法 1:手写 OLS(矩阵求解)
# ============================================================
# 构造设计矩阵 X = [1, BM](第一列是常数项)
X = np.column_stack([np.ones(n), bm])
 
# beta_hat = (X'X)^(-1) X'y
XtX = X.T @ X
Xty = X.T @ y
beta_hat = np.linalg.inv(XtX) @ Xty
 
# 计算残差和拟合值
y_hat = X @ beta_hat
residuals = y - y_hat
 
# 计算统计量
n_obs = n
k_params = 2
df = n_obs - k_params  # 自由度
MSE = np.sum(residuals**2) / df  # 均方误差
# 系数方差-协方差矩阵
var_beta = MSE * np.linalg.inv(XtX)
se_beta = np.sqrt(np.diag(var_beta))  # 标准误
t_stats = beta_hat / se_beta  # t 统计量
 
# R² 和 调整 R²
SS_total = np.sum((y - np.mean(y))**2)
SS_residual = np.sum(residuals**2)
R2 = 1 - SS_residual / SS_total
R2_adj = 1 - (1 - R2) * (n_obs - 1) / df
 
print("=" * 50)
print("手写 OLS 结果")
print("=" * 50)
print(f"截距 alpha_hat = {beta_hat[0]:.6f}  (真实值: {alpha_true})")
print(f"斜率 beta_hat  = {beta_hat[1]:.6f}  (真实值: {beta_true})")
print(f"标准误: {se_beta}")
print(f"t 统计量: {t_stats}")
print(f"R² = {R2:.4f}, 调整 R² = {R2_adj:.4f}")
 
# ============================================================
# 方法 2:使用 statsmodels(标准做法)
# ============================================================
import statsmodels.api as sm
 
X_sm = sm.add_constant(bm)  # 自动添加常数项
model = sm.OLS(y, X_sm).fit()
 
print("\n" + "=" * 50)
print("statsmodels OLS 结果")
print("=" * 50)
print(model.summary())

输出中,你会看到手写结果和 statsmodels 完全一致。理解手写过程后,实际工作中用 statsmodels 就够了,但它帮你做了很多隐含的事情(标准误、t 值、p 值等),你需要知道这些数字是怎么来的。


二、Gauss-Markov 定理:为什么 OLS 是”最好的”

2.1 白话解释

Gauss-Markov 定理说的是:在所有线性无偏的估计量中,OLS 的方差最小。

“最小方差”意味着什么?意味着如果你重复抽样很多次,OLS 估计值波动最小、最稳定。

2.2 定理的前提假设(BLUE 条件)

要让 Gauss-Markov 定理成立,OLS 需要满足以下假设:

假设数学表达白话解释违反后果
线性因变量是参数的线性函数模型设定错误
严格外生性$E(\varepsilonX) = 0$误差项与解释变量不相关
无完全共线性解释变量之间不能完全”重合”无法估计
球形扰动$Var(\varepsilonX) = \sigma^2 I$误差方差恒定、无自相关

前三个假设比较直观,第四个”球形扰动”是实践中最容易违反的,也是后面诊断部分要解决的核心问题。

2.3 BLUE 的含义

Best(最优)——方差最小 Linear(线性)——估计量是 y 的线性函数 Unbiased(无偏)——多次估计的期望等于真实值 Estimator(估计量)

注意:BLUE 只在”线性无偏”这个类别里是最好的。如果你允许有偏估计,可能存在方差更小的估计量(如岭回归)。但在线性无偏的框架下,OLS 是王者。


三、假设检验:模型结果到底能不能信?

跑完回归后,你会得到一堆数字。怎么判断这些数字有意义还是巧合?

3.1 t 检验:单个系数是否显著不为零

问题:你估计出 ,但这个数值是真实的效应,还是数据噪音?

逻辑

  1. 假设 (零假设 H0)
  2. 在零假设下, 的分布应该以 0 为中心
  3. 计算 t 统计量:
  4. 如果 足够大,就拒绝 H0

判定标准

  • :在 5% 显著性水平下显著
  • :在 1% 显著性水平下显著
  • p 值 < 0.05:拒绝零假设,认为系数显著不为零

3.2 F 检验:整个模型是否有解释力

问题:即使某个系数显著,整个模型是不是真的有用?

逻辑:比较”有模型”和”只有截距”的拟合差异。

如果 F 统计量远大于 1(通常 p 值 < 0.05),说明模型整体有解释力。

3.3 R²:模型解释了多少方差

  • R² = 0.8:模型解释了 80% 的方差
  • R² = 0.05:模型只解释了 5% 的方差

量化中的注意事项

  1. R² 太高反而可疑——可能有过拟合
  2. 截面因子的 R² 通常很低(2%-5%),但只要有统计显著性,组合后也能获利
  3. 用调整 R² 比较不同变量数量的模型更公平

3.4 Python 代码:完整假设检验

import numpy as np
import statsmodels.api as sm
 
np.random.seed(42)
n = 500
 
# 模拟三因子模型
market = np.random.normal(0.001, 0.02, n)
size = np.random.normal(0.0, 0.01, n)
value = np.random.normal(0.0, 0.01, n)
 
# 真实关系:市场因子有解释力,size 没有
y = 0.001 + 1.2 * market + 0.3 * size + 0.5 * value + np.random.normal(0, 0.015, n)
 
X = np.column_stack([market, size, value])
X = sm.add_constant(X)
 
model = sm.OLS(y, X).fit()
 
print("=" * 60)
print("假设检验详解")
print("=" * 60)
 
# t 检验结果
print("\n--- t 检验(单个系数显著性)---")
for name, coef, tval, pval in zip(
    ['截距', '市场', '规模', '价值'],
    model.params,
    model.tvalues,
    model.pvalues
):
    sig = "***" if pval < 0.001 else "**" if pval < 0.01 else "*" if pval < 0.05 else ""
    print(f"  {name}: coef={coef:.4f}, t={tval:.2f}, p={pval:.4f} {sig}")
 
# F 检验结果
print(f"\n--- F 检验(模型整体显著性)---")
print(f"  F 统计量 = {model.fvalue:.2f}")
print(f"  F 检验 p 值 = {model.f_pvalue:.2e}")
print(f"  结论: {'模型整体显著' if model.f_pvalue < 0.05 else '模型整体不显著'}")
 
# R²
print(f"\n--- R² ---")
print(f"  R² = {model.rsquared:.4f}")
print(f"  调整 R² = {model.rsquared_adj:.4f}")
print(f"  模型解释了 {model.rsquared*100:.1f}% 的收益方差")

四、OLS 假设与诊断

Gauss-Markov 定理要求”球形扰动”假设。现实金融数据几乎一定会违反它。这里逐一讲解如何诊断和修正。

4.1 异方差:误差的大小不是均匀的

白话解释

正常情况下,OLS 假设误差的方差对所有观测值都一样。但金融数据中:

  • 大盘股的收益波动小,小盘股的收益波动大
  • 市场平静时误差小,危机时误差大
  • 这就是异方差

为什么是问题?

异方差不改变系数估计的一致性(估计值方向没问题),但会让标准误有偏。标准误有偏 → t 检验不可靠 → 你可能把不显著的系数当显著的。

White 检验

检验是否存在异方差的标准方法。

import numpy as np
import statsmodels.api as sm
from statsmodels.stats.diagnostic import het_white
 
np.random.seed(42)
n = 300
x = np.random.normal(0, 1, n)
# 误差方差随 x 增大而增大 → 异方差
heteroskedastic_error = np.random.normal(0, 1, n) * (1 + 0.5 * np.abs(x))
y = 1 + 2 * x + heteroskedastic_error
 
X = sm.add_constant(x)
model = sm.OLS(y, X).fit()
 
# White 检验
white_test = het_white(model.resid, model.model.exog)
labels = ['LM 统计量', 'LM p 值', 'F 统计量', 'F p 值']
print("White 异方差检验:")
for name, val in zip(labels, white_test):
    print(f"  {name}: {val:.4f}")
 
is_heteroskedastic = white_test[1] < 0.05
print(f"\n结论: {'存在异方差' if is_heteroskedastic else '不存在异方差'}")

WLS 加权最小二乘修正

给误差大的观测值较小的权重,误差小的观测值较大的权重。

# WLS:用 1/|x| 作为权重
weights = 1.0 / (1 + 0.5 * np.abs(x))
model_wls = sm.WLS(y, X, weights=weights).fit()
 
print("\nOLS vs WLS 对比:")
print(f"  OLS 标准误: {model.bse}")
print(f"  WLS 标准误: {model_wls.bse}")

4.2 自相关:误差项之间存在相关性

白话解释

在时间序列数据中,如果今天的误差和昨天的误差有关系,就是自相关

比如:市场情绪的持续性会导致正自相关——好消息之后往往还有好消息。

为什么是问题?

和异方差一样,自相关不会影响系数方向,但会让标准误被低估。标准误被低估 → t 值虚高 → 你会过度相信结果。

Durbin-Watson 检验

from statsmodels.stats.stattools import durbin_watson
 
np.random.seed(42)
n = 200
x = np.random.normal(0, 1, n)
# 构造自相关误差:误差 = 0.6 * 上期误差 + 新噪音
errors = np.zeros(n)
for t in range(1, n):
    errors[t] = 0.6 * errors[t-1] + np.random.normal(0, 1)
y = 1 + 2 * x + errors
 
X = sm.add_constant(x)
model = sm.OLS(y, X).fit()
 
# Durbin-Watson 检验
dw = durbin_watson(model.resid)
print(f"Durbin-Watson 统计量: {dw:.4f}")
print(f"  DW ≈ 2: 无自相关")
print(f"  DW < 2: 正自相关(常见于金融时间序列)")
print(f"  DW > 2: 负自相关")
print(f"  当前结果: {'正自相关' if dw < 2 else '负自相关' if dw > 2 else '无自相关'}")

DW 值的判断规则:取 作为临界值(查 DW 表):

  • :正自相关
  • :无自相关
  • :负自相关

4.3 多重共线性:两个变量太像了

白话解释

当解释变量之间高度相关时,模型无法区分哪个变量在起作用。

比如你同时放入”市值”和”流通市值”,这两个变量高度相关,回归结果会很不稳定——加一个数据点,系数可能完全翻转。

VIF 诊断

VIF(Variance Inflation Factor)衡量每个变量被其他变量”解释”的程度。

其中 是用其他解释变量回归第 个变量的 R²。

from statsmodels.stats.outliers_influence import variance_inflation_factor
 
np.random.seed(42)
n = 200
# 制造共线性:x1 和 x2 高度相关
x1 = np.random.normal(0, 1, n)
x2 = x1 + np.random.normal(0, 0.1, n)  # 和 x1 几乎一样
x3 = np.random.normal(0, 1, n)
y = 1 + 2*x1 + 0.5*x3 + np.random.normal(0, 1, n)
 
X = np.column_stack([x1, x2, x3])
X_with_const = sm.add_constant(X)
 
# 计算 VIF
print("VIF 诊断(VIF > 10 表示严重共线性):")
for i, name in enumerate(['x1', 'x2', 'x3']):
    vif = variance_inflation_factor(X_with_const, i+1)  # +1 跳过常数项
    warning = " ⚠️ 严重共线性!" if vif > 10 else ""
    print(f"  {name}: VIF = {vif:.1f}{warning}")

处理方法

  1. 删除共线性变量中不太重要的那个
  2. 使用 PCA 降维
  3. 使用岭回归(有偏但稳定)

4.4 HAC 稳健标准误(Newey-West)

为什么重要?

前面说的异方差和自相关,分别有各自的检验和修正方法。但在实际研究中,我们常常两种问题同时存在,逐个修正很麻烦。

Newey-West HAC 标准误是一次性解决异方差和自相关的方法。它是金融计量中最常用的标准误修正方法

HAC = Heteroskedasticity-Autocorrelation Consistent(异方差-自相关一致)

import numpy as np
import statsmodels.api as sm
 
np.random.seed(42)
n = 300
x = np.random.normal(0, 1, n)
# 同时有异方差和自相关的误差
errors = np.zeros(n)
for t in range(1, n):
    errors[t] = 0.5 * errors[t-1] + np.random.normal(0, 1 + 0.3 * abs(x[t]))
y = 1 + 2 * x + errors
 
X = sm.add_constant(x)
model_ols = sm.OLS(y, X).fit()
 
# 使用 Newey-West HAC 标准误重新拟合
model_hac = sm.OLS(y, X).fit(cov_type='HAC', cov_kwds={'maxlags': int(n**(1/4))})
 
print("OLS 标准误 vs HAC 稳健标准误:")
print(f"{'系数':>8} {'OLS SE':>10} {'HAC SE':>10} {'OLS t':>8} {'HAC t':>8}")
for i, name in enumerate(['截距', '斜率']):
    print(f"{name:>8} {model_ols.bse[i]:10.4f} {model_hac.bse[i]:10.4f} "
          f"{model_ols.tvalues[i]:8.2f} {model_hac.tvalues[i]:8.2f}")
 
print(f"\n对比结论:")
print(f"  HAC 标准误通常 >= OLS 标准误")
print(f"  如果差距很大,说明 OLS 的 t 值虚高了")

量化实战建议

  • 分析时间序列数据时,默认使用 HAC 标准误,而不是普通 OLS 标准误
  • 这是最”安全”的做法,即使数据满足 OLS 假设,HAC 也仍然有效

五、工具变量(IV):因果方向搞反了怎么办

5.1 内生性问题

OLS 的核心假设之一是”严格外生性”:。如果违反了,就是内生性

白话解释:你用 OLS 估计”广告投入 → 销售额”的关系,但可能是”销售额好 → 公司有钱投广告”。因果关系搞反了,OLS 估计就有偏。

量化中的内生性例子

  • 用公司市值预测收益:市值大的公司可能有其他系统性差异
  • 用利率预测股市:利率和股市可能同时被宏观因素驱动

5.2 工具变量是什么

白话类比:你想知道”上补习班 → 成绩”的因果效果,但上补习班的学生本身可能更努力(内生性)。你需要找一个”只影响是否上补习班,但不直接影响成绩”的变量——比如”家附近有没有补习班的分校”。这个变量就是工具变量

数学定义:工具变量 需要满足:

  1. 相关性 —— 工具变量和内生变量相关
  2. 外生性 —— 工具变量和误差项不相关

5.3 两阶段最小二乘(2SLS)

第一阶段:用工具变量 解释内生变量

得到 的拟合值

第二阶段:用 替代原来的 做 OLS

import numpy as np
import statsmodels.api as sm
from linearmodels.iv import IV2SLS
 
np.random.seed(42)
n = 500
 
# 工具变量 Z(外生)
Z = np.random.normal(0, 1, n)
# 误差项 v(影响 X 但不影响 y 的部分)
v = np.random.normal(0, 1, n)
# 内生变量 X(被 Z 和 v 共同决定)
X = 2 * Z + 1.5 * v + np.random.normal(0, 0.5, n)
# 误差项 u(和 v 相关 → 内生性!)
u = 0.8 * v + np.random.normal(0, 1, n)
# 被解释变量 y
beta_true = 1.0
y = 1 + beta_true * X + u
 
# ============================================================
# 方法 1:直接 OLS(有偏)
# ============================================================
X_ols = sm.add_constant(X)
ols_result = sm.OLS(y, X_ols).fit()
 
# ============================================================
# 方法 2:手写 2SLS
# ============================================================
# 第一阶段:X = gamma0 + gamma1 * Z
Z_stage1 = sm.add_constant(Z)
stage1 = sm.OLS(X, Z_stage1).fit()
X_hat = stage1.fittedvalues  # X 的拟合值
 
# 第二阶段:y = alpha + beta * X_hat
X_hat_stage2 = sm.add_constant(X_hat)
stage2 = sm.OLS(y, X_hat_stage2).fit()
 
# ============================================================
# 方法 3:使用 linearmodels 的 IV2SLS
# ============================================================
# pip install linearmodels
import pandas as pd
df = pd.DataFrame({'y': y, 'X': X, 'Z': Z})
iv_result = IV2SLS(df['y'], sm.add_constant(df[['X']]), df[['Z']]).fit(cov_type='robust')
 
print("=" * 60)
print(f"真实 beta = {beta_true}")
print("=" * 60)
print(f"  OLS 估计:    beta = {ols_result.params[1]:.4f} (有偏!)")
print(f"  手写 2SLS:   beta = {stage2.params[1]:.4f}")
print(f"  IV2SLS 估计: beta = {iv_result.params['X']:.4f}")
print(f"\n结论: OLS 严重有偏,IV/2SLS 接近真实值")

5.4 弱工具变量问题

如果工具变量 和内生变量 的相关性太弱,2SLS 的表现可能比 OLS 还差。

判断方法:第一阶段的 F 统计量。

  • F > 10:工具变量足够强(经验法则)
  • F < 10:弱工具变量,2SLS 可能有严重偏误
# 第一阶段 F 统计量
print(f"\n第一阶段诊断:")
print(f"  第一阶段 F = {stage1.fvalue:.2f}")
print(f"  {'工具变量足够强' if stage1.fvalue > 10 else '⚠️ 弱工具变量!'}")

六、虚假回归:两个独立随机游走也可能”相关”

6.1 什么是虚假回归

这是量化中最容易犯的错误之一。

假设你取两个完全独立的随机游走序列(比如抛硬币生成的两个序列),对它们做回归,你很可能得到”显著”的 R² 和 t 值。

但这两个序列根本没有任何关系——你的回归结果完全是假的。

为什么会这样? 因为随机游走是非平稳的。在非平稳序列之间做回归,即使它们完全独立,也会因为”共同趋势”而表现出虚假的相关性。

6.2 模拟演示

import numpy as np
import statsmodels.api as sm
import matplotlib.pyplot as plt
 
np.random.seed(42)
n = 500
 
# 生成两个完全独立的随机游走
rw1 = np.cumsum(np.random.normal(0, 1, n))
rw2 = np.cumsum(np.random.normal(0, 1, n))
 
# 对它们做回归
X = sm.add_constant(rw1)
model = sm.OLS(rw2, X).fit()
 
print("虚假回归演示(两个完全独立的随机游走):")
print(f"  R² = {model.rsquared:.4f}")
print(f"  斜率 t 值 = {model.tvalues[1]:.2f}")
print(f"  斜率 p 值 = {model.pvalues[1]:.4f}")
print(f"\n  即使两个序列完全独立,")
print(f"  你很可能看到 '显著' 的回归结果!")
print(f"  这就是虚假回归(Spurious Regression)")
 
# 对比:对差分后的平稳序列做回归
drw1 = np.diff(rw1)
drw2 = np.diff(rw2)
X_diff = sm.add_constant(drw1)
model_diff = sm.OLS(drw2, X_diff).fit()
 
print(f"\n差分后(平稳序列)的回归:")
print(f"  R² = {model_diff.rsquared:.4f}")
print(f"  斜率 t 值 = {model_diff.tvalues[1]:.2f}")
print(f"  斜率 p 值 = {model_diff.pvalues[1]:.4f}")
print(f"  差分后结果变得不显著了——这才是真实的")

6.3 如何检测和避免

  1. 先做单位根检验(详见时间序列计量文件)——确认序列是平稳的
  2. 对非平稳序列先做差分,使其平稳后再回归
  3. 如果序列不平稳但有协整关系——可以用误差修正模型(ECM)
  4. 永远不要对两个非平稳序列直接做回归——除非你已经验证了它们有协整关系

6.4 量化中的教训

在量化研究中,你经常会对两个资产的收益率、两个因子、或者价格序列做回归。如果回归结果”太好”(R² 很高、系数非常显著),你反而应该警惕:

  • 是不是虚假回归?
  • 序列是不是非平稳的?
  • 我是否验证了平稳性或协整性?

“太好的结果往往需要更多的怀疑。“


七、完整实战案例:因子收益估计与诊断

把前面的所有知识串联起来,做一个完整的因子收益估计流程。

import numpy as np
import pandas as pd
import statsmodels.api as sm
from statsmodels.stats.diagnostic import het_white
from statsmodels.stats.stattools import durbin_watson
from statsmodels.stats.outliers_influence import variance_inflation_factor
 
np.random.seed(42)
 
# ============================================================
# 模拟截面数据:500 只股票,5 个月
# ============================================================
n_stocks = 500
n_months = 60
 
results_list = []
beta_monthly = []
 
for m in range(n_months):
    # 每月随机生成因子暴露
    size = np.random.normal(0, 1, n_stocks)
    value = np.random.normal(0, 1, n_stocks)
    momentum = np.random.normal(0, 1, n_stocks)
    quality = np.random.normal(0, 1, n_stocks)
 
    # 因子收益(每月变化,但有持续性)
    if m == 0:
        factor_returns = np.array([0.02, 0.015, 0.03, 0.01])
    else:
        factor_returns = 0.7 * prev_factor_returns + np.array([
            np.random.normal(0.02, 0.05),
            np.random.normal(0.015, 0.04),
            np.random.normal(0.03, 0.06),
            np.random.normal(0.01, 0.03)
        ])
    prev_factor_returns = factor_returns.copy()
 
    # 股票收益 = 因子收益 × 因子暴露 + 特质收益
    expected_return = (factor_returns[0] * size +
                       factor_returns[1] * value +
                       factor_returns[2] * momentum +
                       factor_returns[3] * quality)
    stock_returns = expected_return + np.random.normal(0, 0.1, n_stocks)
 
    # 截面回归
    X = np.column_stack([size, value, momentum, quality])
    X = sm.add_constant(X)
    model = sm.OLS(stock_returns, X).fit()
 
    beta_monthly.append(model.params[1:])
    results_list.append({
        'month': m,
        'R2': model.rsquared,
        'size_t': model.tvalues[1],
        'value_t': model.tvalues[2],
        'mom_t': model.tvalues[3],
        'qual_t': model.tvalues[4],
        'size_p': model.pvalues[1],
        'value_p': model.pvalues[2],
        'mom_p': model.pvalues[3],
        'qual_p': model.pvalues[4],
    })
 
results_df = pd.DataFrame(results_list)
betas = np.array(beta_monthly)
 
# ============================================================
# Fama-MacBeth 第二步:对因子收益做时间序列平均
# ============================================================
print("=" * 60)
print("Fama-MacBeth 因子收益估计")
print("=" * 60)
 
factor_names = ['size', 'value', 'momentum', 'quality']
for i, name in enumerate(factor_names):
    monthly_betas = betas[:, i]
    mean_beta = np.mean(monthly_betas)
    std_beta = np.std(monthly_betas, ddof=1)
    t_stat = mean_beta / (std_beta / np.sqrt(n_months))
 
    sig_months = np.sum(np.abs(monthly_betas / std_beta) > 1.96)
    print(f"  {name:>10}: 平均收益={mean_beta:.4f}, t={t_stat:.2f}, "
          f"显著月份={sig_months}/{n_months}")
 
# ============================================================
# 诊断汇总
# ============================================================
print(f"\n{'='*60}")
print("回归诊断汇总(以最后一个月为例)")
print(f"{'='*60}")
 
# 使用最后一个月的数据做完整诊断
X_diag = np.column_stack([size, value, momentum, quality])
X_diag = sm.add_constant(X_diag)
model_diag = sm.OLS(stock_returns, X_diag).fit()
 
# 1. 异方差检验(White)
white = het_white(model_diag.resid, model_diag.model.exog)
print(f"\n1. White 异方差检验: p = {white[1]:.4f} "
      f"({'存在异方差' if white[1] < 0.05 else '无异方差'})")
 
# 2. 自相关检验(DW)
dw = durbin_watson(model_diag.resid)
print(f"2. Durbin-Watson: {dw:.4f} "
      f"({'正自相关' if dw < 1.5 else '无自相关' if dw > 1.5 and dw < 2.5 else '负自相关'})")
 
# 3. VIF
print(f"3. VIF 多重共线性诊断:")
for i, name in enumerate(factor_names):
    vif = variance_inflation_factor(X_diag, i+1)
    warning = " ⚠️" if vif > 10 else ""
    print(f"     {name}: VIF = {vif:.1f}{warning}")
 
# 4. HAC 标准误 vs OLS 标准误
model_hac = sm.OLS(stock_returns, X_diag).fit(
    cov_type='HAC', cov_kwds={'maxlags': int(n_stocks**0.25)}
)
print(f"\n4. OLS vs HAC 标准误:")
for i, name in enumerate(['截距'] + factor_names):
    diff_pct = (model_hac.bse[i] - model_diag.bse[i]) / model_diag.bse[i] * 100
    print(f"     {name}: OLS SE={model_diag.bse[i]:.4f}, "
          f"HAC SE={model_hac.bse[i]:.4f} (差异 {diff_pct:+.1f}%)")

本文件要点回顾

概念一句话总结
OLS最小化残差平方和,找”最贴合”的线
Gauss-Markov在线性无偏估计量中,OLS 方差最小
t 检验单个系数是否显著不为零
F 检验整个模型是否有解释力
异方差误差方差不均匀 → 标准误有偏 → t 值不可信
自相关误差项之间相关 → 标准误被低估
多重共线性解释变量太像 → 系数不稳定
HAC 稳健标准误一次性解决异方差 + 自相关
工具变量解决内生性(因果方向搞反了)
虚假回归两个独立非平稳序列也可能”显著相关”

下一站

理解了经典回归后,我们进入时间序列计量——金融数据天然是时间序列,普通回归用在时间序列上会出大问题。