年化20%量化笔记 · 第36篇
上一篇我们聊了时间序列分析中回归模型的建立,但建完模型只是第一步------更关键的是:你怎么知道这个模型到底靠不靠谱?今天我们就来聊聊回归诊断这件事。

WHY:为什么回归诊断这么重要?
一个血泪教训
去年我花了两周时间搭了一个多因子选股模型,回测结果漂亮得不像话------年化收益42%,夏普比率2.1。当时激动得差点直接上实盘。
但后来做了一轮完整的回归诊断,才发现这个模型"坏了":残差存在严重的自相关性,意味着模型没有真正提取到所有的信息,那些所谓的"超额收益"不过是模型对某些系统性风险的误判。
修正之后,年化收益降到了18%,但这才是一个真实可信的数字。
模型"坏了"是什么意思?
在统计学里,一个线性回归模型要满足四个基本假设(也叫Gauss-Markov条件):
-
线性关系 :因变量与自变量之间存在线性关系
-
残差独立性 :残差之间相互独立,不存在自相关
-
残差同方差性 :残差的方差是恒定的(同方差)
-
残差正态性 :残差服从正态分布
当这些假设被违反时,模型的估计结果就不再是最优的,甚至可能是完全错误的。这就像你用一把刻度不准的尺子去量东西------量出来的数字再精确,也没有意义。
在量化交易中尤其关键
对于量化交易而言,回归模型的"坏"可能带来更严重的后果:
• 虚假信号 :如果残差存在自相关,你可能会把噪声当成信号,产生大量虚假的交易信号
• 风险低估 :如果存在异方差(heteroscedasticity),你的风险估计会严重失真------在市场波动大的时候低估风险,在市场平静的时候过度保守
• 错误定价 :如果模型没有捕捉到非线性关系,你的因子暴露计算就是错的
所以,回归诊断不是"锦上添花",而是"生死攸关"。
HOW:回归诊断的具体方法
一、残差分析------最基础也最重要
残差(residual)是模型预测值与实际值之间的差距。如果模型是好的,残差应该像"白噪声"一样------随机的、无规律的、均值为零的。
1.1 残差图(Residual Plot)
最直观的 diagnose 方法就是画残差图:
// python
import matplotlib.pyplot as plt
import numpy as np
假设 y_pred 是模型预测值,residuals 是残差
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
残差 vs 预测值
axes0.scatter(y_pred, residuals, alpha=0.5)
axes0.axhline(y=0, color='r', linestyle='--')
axes0.set_title('Residuals vs Fitted')
axes0.set_xlabel('Fitted Values')
axes0.set_ylabel('Residuals')
残差的QQ图
from scipy import stats
stats.probplot(residuals, dist="norm", plot=axes1)
axes1.set_title('Normal Q-Q Plot')
残差的直方图
axes2.hist(residuals, bins=30, edgecolor='black', alpha=0.7)
axes2.set_title('Residual Distribution')
plt.tight_layout()
plt.show()
怎么看这些图?
• 残差 vs 预测值图 :如果残差均匀散布在零线两侧,说明模型假设基本满足。如果出现了"漏斗形"(一端散一端聚),说明存在异方差问题;如果出现了曲线趋势,说明可能遗漏了非线性关系。
• QQ图 :如果残差完美服从正态分布,所有点应该在一条直线上。如果尾部偏离,说明残差存在厚尾(fat tails)------这在金融数据中非常常见。
• 直方图 :直观看残差的分布形状,是否接近正态。
1.2 残差的自相关检测
对于时间序列数据,残差的自相关性是一个特别常见的问题。
Durbin-Watson 检验 :
// python
from statsmodels.stats.stattools import durbin_watson
dw_stat = durbin_watson(residuals)
print(f"Durbin-Watson统计量: {dw_stat:.4f}")
DW值接近2 → 无自相关
DW值显著小于2 → 正自相关
DW值显著大于2 → 负自相关
Ljung-Box 检验 :
// python
from statsmodels.stats.diagnostic import acorr_ljungbox
lb_test = acorr_ljungbox(residuals, lags=10, 15, 20, return_df=True)
print(lb_test)
p值小于0.05 → 拒绝"无自相关"的原假设
1.3 异方差检测
Breusch-Pagan 检验 :
// python
from statsmodels.stats.diagnostic import het_breuschpagan
bp_test = het_breuschpagan(residuals, X)
bp_test 返回: (LM统计量, LM的p值, F统计量, F的p值)
print(f"BP检验p值: {bp_test1:.4f}")
p值小于0.05 → 存在异方差
White 检验 :
// python
from statsmodels.stats.diagnostic import het_white
white_test = het_white(residuals, X)
print(f"White检验p值: {white_test1:.4f}")
二、异常值检测------别让极端值毁了你的模型
2.1 标准化残差法
最简单的方法:标准化残差超过±3的观测点,可以视为异常值。
// python
from statsmodels.stats.outliers_influence import OLSInfluence
influence = OLSInfluence(model_results)
standardized_residuals = influence.resid_studentized_internal
找出异常值
outliers = np.abs(standardized_residuals) > 3
print(f"发现 {outliers.sum()} 个异常值")
2.2 Cook's Distance------衡量影响力的综合指标
有些数据点不仅是"异常"的,它们还对模型参数产生了巨大的影响力。Cook's Distance 就是衡量这种影响力的指标。
// python
cooks_d = influence.cooks_distance0
常用阈值: 4/n 或 0.5
threshold = 4 / len(X)
influential_points = cooks_d > threshold
plt.figure(figsize=(10, 4))
plt.stem(range(len(cooks_d)), cooks_d, markerfmt=',', use_line_collection=True)
plt.axhline(y=threshold, color='r', linestyle='--', label=f'Threshold (4/n={threshold:.4f})')
plt.xlabel('Observation Index')
plt.ylabel("Cook's Distance")
plt.title("Cook's Distance Plot")
plt.legend()
plt.show()
在量化交易中,这些"高影响力"的观测点往往对应着极端市场行情------比如2020年3月的熔断、2015年的A股暴跌。你需要决定:这些是应该被模型学习的极端事件,还是应该被排除的异常?
2.3 杠杆值(Leverage)
杠杆值衡量的是一个数据点在自变量空间中的"偏离程度"。高杠杆点不一定是异常值,但它有潜力对模型产生很大影响。
// python
leverage = influence.hat_matrix_diag
high_leverage = leverage > 2 * X.shape1 / len(X)
print(f"高杠杆点数量: {high_leverage.sum()}")
三、多重共线性检测------因子之间的"内耗"
在量化多因子模型中,因子之间的高度相关性是一个常见问题。如果两个因子高度相关,模型就无法区分它们各自的贡献。
3.1 VIF(方差膨胀因子)
// python
from statsmodels.stats.outliers_influence import variance_inflation_factor
vif_data = pd.DataFrame()
vif_data"feature" = X.columns
vif_data"VIF" = variance_inflation_factor(X.values, i) for i in range(X.shape\[1)]
print(vif_data.sort_values("VIF", ascending=False))
VIF > 10 → 严重多重共线性
VIF > 5 → 需要关注
3.2 相关矩阵热力图
// python
import seaborn as sns
corr_matrix = X.corr()
plt.figure(figsize=(10, 8))
sns.heatmap(corr_matrix, annot=True, cmap='RdBu_r', center=0,
vmin=-1, vmax=1, square=True)
plt.title('Feature Correlation Matrix')
plt.show()
四、模型整体诊断
4.1 RESET检验------检查模型设定是否正确
Ramsey's RESET检验可以检测模型是否遗漏了重要的非线性项。
// python
from statsmodels.stats.diagnostic import linear_reset
reset_test = linear_reset(model_results, power=2, test_type='fitted')
print(f"RESET检验p值: {reset_test.pvalue:.4f}")
p值小于0.05 → 模型可能遗漏了重要的非线性关系
4.2 交叉验证------模型的"实战检验"
// python
from sklearn.model_selection import cross_val_score
from sklearn.linear_model import LinearRegression
model = LinearRegression()
cv_scores = cross_val_score(model, X, y, cv=5, scoring='r2')
print(f"交叉验证R²: {cv_scores.mean():.4f} ± {cv_scores.std():.4f}")
如果不同折的R²差异很大,说明模型不稳定
五、一套完整的诊断流程
在我的日常工作中,我会按照以下顺序对每个回归模型进行诊断:
Step 1: 看R²和调整R² → 模型整体解释力
↓
Step 2: 画残差图 → 线性假设是否成立
↓
Step 3: QQ图 + Shapiro-Wilk检验 → 残差正态性
↓
Step 4: Durbin-Watson / Ljung-Box → 残差自相关性
↓
Step 5: Breusch-Pagan / White → 异方差性
↓
Step 6: VIF → 多重共线性
↓
Step 7: Cook's Distance → 异常值和强影响点
↓
Step 8: RESET检验 → 模型设定
↓
Step 9: 交叉验证 → 模型稳定性
每一步如果发现问题,就需要相应地修正模型:
• 非线性 → 加入多项式项或使用非线性模型
• 自相关 → 使用广义最小二乘法(GLS)或加入滞后项
• 异方差 → 使用稳健标准误(Robust Standard Errors)
• 多重共线性 → 去除冗余因子或使用PCA降维
• 异常值 → 稳健回归(Robust Regression)或 Winsorize 处理
WHAT:诊断之后你能得到什么?
一张回归诊断"体检报告"
完成上述诊断后,你应该能输出一份清晰的模型健康报告:
|----------|--------------------|------------------|----------|--------|
| 诊断项目 | 检验方法 | 判定标准 | 本例结果 | 状态 |
| 整体拟合 | R² / Adj R² | R² > 0.3 | 0.45 | ✅ |
| 线性假设 | 残差图 | 无明显模式 | 通过 | ✅ |
| 残差正态性 | QQ图 / Shapiro-Wilk | p > 0.05 | p=0.03 | ⚠️ 厚尾 |
| 残差独立性 | Durbin-Watson | 1.5 < DW < 2.5 | 1.82 | ✅ |
| 同方差性 | Breusch-Pagan | p > 0.05 | p=0.01 | ❌ 异方差 |
| 多重共线性 | VIF | VIF < 5 | max=3.2 | ✅ |
| 异常值 | Cook's D | 无超高影响点 | 2个 | ⚠️ 需检查 |
| 模型设定 | RESET | p > 0.05 | p=0.12 | ✅ |
| 稳定性 | CV R²标准差 | std < 0.1 | 0.04 | ✅ |
根据诊断结果,形成明确的行动方案:
- 如果残差正态性不满足(厚尾)
◦ 使用 t 分布替代正态分布进行假设检验
◦ 或采用稳健标准误
◦ 量化影响:标准误可能低估 15-30%
- 如果存在异方差
◦ 使用 HC(Heteroscedasticity-Consistent)标准误
◦ 或使用 WLS(加权最小二乘法)
◦ 量化影响:t检验的显著性判断可能出错,导致保留无效因子或丢弃有效因子
- 如果发现高影响力异常点
◦ 对比剔除前后的模型参数变化
◦ 如果变化超过20%,需要谨慎决策
◦ 考虑使用 Huber 回归等稳健方法
- 如果多重共线性严重
◦ 逐步去除VIF最高的变量
◦ 或使用 Ridge 回归
◦ 量化影响:共线性存在时,单个因子的系数估计可能完全不可信
一个真实的量化案例
我曾经的一个多因子模型,诊断前后的对比:
|--------|--------------|-------------|
| 指标 | 诊断前(未修正) | 诊断后(修正) |
| 年化收益率 | 35.2% | 19.8% |
| 夏普比率 | 1.85 | 1.12 |
| 最大回撤 | -28.5% | -15.3% |
| 因子数量 | 12 | 8 |
| R² | 0.68 | 0.42 |
| 样本外表现 | 大幅衰减 | 稳定 |
看起来"退步"了?其实这才是真实的表现。诊断前那个漂亮的35%年化,不过是模型在"自欺欺人"------它把噪声当成了信号,把过拟合当成了alpha。
修正后虽然收益降了,但风险更可控,样本外表现更稳定。这才是能真正帮你赚钱的模型。
可量化的收益
坚持做回归诊断,根据我的经验:
• 减少虚假信号 :避免30-50%的虚假因子进入模型
• 提升样本外稳定性 :样本内外收益差距从50%+缩小到20%以内
• 降低极端损失 :通过识别和修正异常值,最大回撤通常能改善20-30%
• 提高决策信心 :你知道模型的每一个假设都经过了验证,每一笔交易都有统计依据
最后
回归诊断就像是给模型做一次"体检"。你不会开一辆从来没做过检测的车上高速,那为什么要把没有经过诊断的模型放到真金白银的市场里?
记住:一个好的量化模型,不是回测最漂亮的那个,而是经过严格诊断后依然稳健的那个。
下一篇我们将进入正则化的世界,聊聊如何通过Lasso和Ridge来防止模型过拟合------这其实也是回归诊断发现问题后的一个重要解决手段。敬请期待!
下一篇:【维克】正则化回归:从Lasso到Ridge------防止过拟合的数学魔法 。