弹性网在中国 A 股月度截面上的
全流程实验
基于 65 个因子、119 个月的面板数据,通过滚动窗口验证弹性网(Elastic Net) 在股票收益预测中的表现。实验覆盖数据预处理、超参数搜索、系数稀疏化分析、 样本外评估与多空组合回测的完整流程。
1.引言:从因子到月度收益
在高维截面回归中,弹性网通过同时施加 L1 与 L2 惩罚,在变量选择与系数稳定性之间取得平衡。
实验基于中国 A 股 2010-01 至 2019-11 的月度截面面板数据。每一行代表「某月某只股票」的 65 个因子取值与其下一个月的已实现收益率。目标是用当期因子预测下期收益——一个典型的 高维回归问题(p = 65 个预测变量,每月 n ≈ 2400 个观测)。
核心工具是 sklearn 的 ElasticNet 实现。弹性网(Zou & Hastie, 2005)的提出动机在于: 纯 Lasso 在预测变量高度相关时倾向于随机选取组内一个变量而忽略其余; 纯 Ridge 虽然抗共线性但不做变量选择。弹性网在两者之间取得折中, 既保留稀疏性又对相关因子组施加均匀收缩。
实验采用滚动窗口方案:24 个月训练、12 个月验证(用于选择最优正则化强度 α)、 1 个月样本外预测,共产生 83 个 OOS 月。这种设计模拟了真实投资场景中 「只用历史信息做预测」的约束。
import osimport timeimport jsonimport datetimeimport warningswarnings.filterwarnings('ignore') import numpy as npimport pandas as pdimport matplotlib.pyplot as pltfrom sklearn.linear_model import ElasticNet plt.rcParams['figure.dpi'] = 100plt.rcParams['savefig.dpi'] = 130plt.rcParams['font.sans-serif'] = ['DejaVu Sans']plt.rcParams['axes.unicode_minus'] = False BASE_DIR = os.path.dirname(os.path.abspath('__file__')) if '__file__' in dir() else os.getcwd()DATA_DIR = os.path.join(BASE_DIR, 'data')RESULTS_DIR = os.path.join(BASE_DIR, 'results')FIGURES_DIR = os.path.join(BASE_DIR, 'figures')for d in (DATA_DIR, RESULTS_DIR, FIGURES_DIR): os.makedirs(d, exist_ok=True)2.Elastic Net 的数学动机
L1 选变量、L2 抗共线,弹性网在两者之间取得最优折中。
在高维回归中(p 与 n 同阶甚至 p > n),普通最小二乘(OLS)的方差爆炸, 需要通过正则化来约束系数空间。两种经典惩罚各有优劣:
- Ridge(L2):将所有系数均匀收缩向零,但不会精确置零—— 无法做变量选择。优点是在预测变量高度相关时表现稳定。
- Lasso(L1):可以将部分系数精确压为零,实现自动变量选择。 但当多个预测变量高度相关时,Lasso 倾向于随机选取组内一个变量而丢弃其余(Zou & Hastie 2005), 且在 p > n 时最多只能选出 n 个变量。
弹性网通过凸组合 L1 与 L2 惩罚,同时获得两者的优势。 目标函数中的 负责产生稀疏性(变量选择), 而 使目标函数严格凸, 保证解的唯一性,并通过分组效应让相关变量获得相近的系数。
从优化几何的角度看,弹性网的约束域是 L1 球与 L2 球的交集—— 一个介于菱形(Lasso)和圆形(Ridge)之间的「圆角菱形」。 这种形状既保留了 L1 在坐标轴上的尖角(产生稀疏解), 又避免了纯 L1 约束域的非光滑边界导致的解不稳定。
:响应向量(下月收益)
:设计矩阵(因子值)
:待估系数向量
:总正则化强度
:L1/L2 混合比例
→ Lasso(纯 L1)
→ Ridge(纯 L2)
→ 本实验设定
3.数据导入与探查
面板数据结构:每行 = 某月某股票的因子快照 + 下月收益标签。
原始数据集 CHN_sample_data.csv 包含 285,283 行 × 68 列。每行是「某月某只股票」的一条记录,列包括: 股票代码 stkcd、月份标识 Dates(如 201001 表示 2010 年 1 月)、 下月已实现收益 y,以及 65 个因子特征。
将非特征列(股票代码、日期、标签)排除后,剩余 65 列即为预测变量。 这些因子涵盖估值(BM、EP)、盈利(ROE、ROA)、动量(mom1m、mom6m、mom12m)、 流动性(dolvol)、波动率(retvol、idiovol)、规模(size、mv)等多个维度。
数据跨越 119 个月(2010-01 至 2019-11),每月截面股票数从 1,434 到 3,354 不等, 反映了 A 股市场在此期间的扩容趋势。月度截面的不等长意味着每个滚动窗口的 训练样本量不同,但弹性网对样本量变化具有较好的鲁棒性。
DATA_FILE = os.path.join(DATA_DIR, 'CHN_sample_data.csv')whole_data = pd.read_csv(DATA_FILE)print(f'Loaded shape={whole_data.shape}') Xtodrop = ['stkcd', 'Dates', 'y']feature_cols = [c for c in whole_data.columns if c not in Xtodrop]print(f'特征数: {len(feature_cols)}') all_month_list = sorted(whole_data['Dates'].drop_duplicates().tolist())month_counts = whole_data.groupby('Dates').size()print(f'月份数: {len(all_month_list)} 起止: {all_month_list[0]} ~ {all_month_list[-1]}')print(f'每月股票数 平均={month_counts.mean():.0f} 最小={month_counts.min()} 最大={month_counts.max()}')preview = whole_data.head(1000).copy()preview.to_csv(os.path.join(RESULTS_DIR, 'data_preview_1000rows.csv'), index=False) y_stats = whole_data['y'].describe().to_dict()print('y 统计:', {k: round(v,4) for k,v in y_stats.items()}) fig, ax = plt.subplots(figsize=(8,4))ax.hist(whole_data['y'], bins=80, color='#4C78A8', edgecolor='white')ax.set_title('Distribution of next-month return (y)')ax.set_xlabel('y'); ax.set_ylabel('count')fig.tight_layout()fig.savefig(os.path.join(FIGURES_DIR, '01_y_distribution.png'))plt.show()| Dates | stkcd | y | ACC | BM | ROE | mom1m | dolvol | size |
|---|---|---|---|---|---|---|---|---|
| 201001 | 2 | 0.0096 | 0.5527 | 0.0834 | 0.5748 | 0.1274 | 0.8778 | 0.1126 |
| 201001 | 5 | 0.0371 | 0.4789 | 0.0657 | 0.5303 | 0.1550 | 0.5023 | 0.0058 |
| 201001 | 6 | 0.0422 | 0.6861 | 0.0809 | 0.5814 | 0.1407 | 0.4336 | 0.0058 |
| 201001 | 9 | 0.0844 | 0.5379 | 0.0742 | 0.5744 | 0.2031 | 0.7949 | 0.0125 |
| 201001 | 11 | 0.0267 | 0.4958 | 0.0838 | 0.5756 | 0.1681 | 0.3630 | 0.0013 |
| 201001 | 12 | 0.0690 | 0.4847 | 0.0874 | 0.5795 | 0.1866 | 0.6131 | 0.0126 |
| 201001 | 14 | 0.0292 | 0.4823 | 0.0674 | 0.5804 | 0.1068 | 0.3329 | 0.0029 |
| 201001 | 16 | 0.0702 | 0.4934 | 0.1094 | 0.5499 | 0.1991 | 0.4685 | 0.0051 |
4.窗口内 Min-Max 归一化
在每个滚动窗口内部统一缩放,兼顾一致性与信息隔离。
归一化函数接收一个 DataFrame 和一组「不归一」的列名。对于弹性网而言, 特征缩放至关重要:L1 惩罚对系数的绝对值施加约束,如果特征量纲不同, 量纲大的变量会被过度惩罚。Min-Max 将所有特征映射到 [0, 1] 区间, 使得惩罚项对每个特征的作用力度可比。
逐列计算极值范围。当某列在当前窗口内取值完全相同(range = 0)时, 直接置零——这种情况偶尔出现在某些因子在特定时段内缺乏变异的场景中, 置零等价于告诉模型「该特征在此窗口内无信息量」。
标准的 min-max 变换:x′ = (x − min) / (max − min)。 变换后每个特征的最小值为 0、最大值为 1,中间值按比例线性映射。
def Norm(in_df, no_Norm): op_df = in_df.copy() for col in op_df.columns: if col in no_Norm: continue col_max = op_df[col].max() col_min = op_df[col].min() rng = col_max - col_min if rng == 0: op_df[col] = 0.0 else: op_df[col] = (op_df[col] - col_min) / rng return op_df5.滚动训练 / 验证 / OOS 窗口
每次取 24 个月训练、12 个月验证、1 个月样本外,向前滚动 83 次。
定义窗口参数:训练 24 个月、验证 12 个月、OOS 1 个月。START = 36 表示从第 37 个月起 才有完整的训练 + 验证 + OOS 窗口可用。
按当前索引 i 切出三段时间窗口:训练集取 i 之前的 24 个月,验证集紧接着 12 个月, OOS 就是第 i 个月本身。三段在时间上严格不重叠、单调递增。
从面板数据中按月份筛选出对应子集,再分离特征矩阵 X 和标签 y。Xtodrop 排除了 股票代码、日期和标签列,确保只有 65 个因子进入模型。
调用 EN_window(...) 完成本窗口的 5-α 网格搜索 + OOS 预测,结果追加到列表中,循环继续向前滚动。
TRAIN_M = 24TEST_M = 12OOS_M = 1START = TRAIN_M + TEST_M + OOS_M - 1 # = 36 for i in range(START, len(all_month_list)): train_months = all_month_list[i-TEST_M-TRAIN_M : i-TEST_M] test_months = all_month_list[i-TEST_M : i] oos_month = all_month_list[i] train_data = whole_data[whole_data['Dates'].isin(train_months)] test_data = whole_data[whole_data['Dates'].isin(test_months)] oos_data = whole_data[whole_data['Dates'] == oos_month] X_train = train_data.drop(columns=Xtodrop); y_train = train_data['y'].values X_test = test_data.drop(columns=Xtodrop); y_test = test_data['y'].values X_oos = oos_data.drop(columns=Xtodrop) en_result = EN_window(X_train, y_train, X_test, y_test, X_oos, test_data, oos_data) en_op.append(en_result)# 汇总所有窗口的调参预测tuning_pred = pd.concat([r[0] for r in en_op], ignore_index=True)tuning_pred['Dates'] = [datetime.datetime(year=int(x//100), month=int(x%100), day=28) for x in tuning_pred['Dates']]tuning_pred = tuning_pred.sort_values(['AlphaValue','Dates','stkcd']).reset_index(drop=True)print('tuning_pred shape:', tuning_pred.shape) # (1085090, 6) # 汇总最优 alpha 的预测best_pred = pd.concat([r[1] for r in en_op], ignore_index=True)best_pred['Dates_int'] = best_pred['Dates'].astype(int)best_pred['Dates'] = [datetime.datetime(year=int(x//100), month=int(x%100), day=28) for x in best_pred['Dates_int']]best_pred = best_pred.sort_values(['Dates','stkcd']).reset_index(drop=True)print('best_pred shape:', best_pred.shape) # (217018, 7) # 每个窗口选出的最优 alpha 记录best_alpha_log = pd.DataFrame({ 'Dates_int': all_month_list[START:], 'BestAlpha': [r[1]['BestAlpha'].iloc[0] for r in en_op], 'TestR2' : [r[1]['TestR2'].iloc[0] for r in en_op],})6.弹性网训练函数与 α 网格搜索
在 5 个对数等距的正则化强度上逐一拟合,用验证集 R² 选出最优 α。
正则化强度 α(sklearn 中的 alpha 参数) 在 5 个对数等距点上搜索:10⁻⁶, 10⁻⁵, 10⁻⁴, 10⁻³, 10⁻²。l1_ratio = 0.5 表示 L1 与 L2 惩罚各占一半权重。 在 sklearn 的参数化中,弹性网的目标函数为:
其中 α 控制总惩罚强度,ρ(即 l1_ratio)控制 L1 与 L2 的相对比例。 α 越大,系数被压缩得越厉害;ρ = 0.5 意味着 L1 和 L2 惩罚等权混合。
对每个候选 α,用训练集拟合模型,然后在验证集上计算预测 R²。 这里的 R² 定义为:
注意分母是 Σy²(而非 Σ(y − ȳ)²),这是金融文献中常用的「无截距 R²」, 衡量模型相对于「预测为零」这一 null model 的改进程度。 当 R² < 0 时,说明模型预测还不如直接预测零。
同时记录每个 α 下的 OOS 预测值和系数向量。系数向量coef_records[a]是一个以特征名为索引的 Series,后续用于分析因子重要性和稀疏度。
选出验证 R² 最高的 α 作为本窗口的最优正则化强度,用该 α 重新拟合并生成 「最优预测」记录。函数返回四个对象:全部 α 的调参预测、最优预测、系数字典、 以及各 α 的验证 R²。
ALPHAS = [1e-6, 1e-5, 1e-4, 1e-3, 1e-2]L1_RATIO = 0.5 TRAIN_M = 24TEST_M = 12OOS_M = 1START = TRAIN_M + TEST_M + OOS_M - 1 def EN_window(X_train, y_train, X_test, y_test, X_oos, test_data, oos_data): test_data = test_data.copy() tune_records = [] coef_records = {} test_r2 = [] for a in ALPHAS: m = ElasticNet(l1_ratio=L1_RATIO, alpha=a, fit_intercept=True, max_iter=2000, tol=1e-4, random_state=123, selection='cyclic') m.fit(X_train, y_train) yhat_test = m.predict(X_test) test_data['rethat'] = yhat_test r2 = 1 - np.sum((test_data['y'] - test_data['rethat'])**2) / np.sum(test_data['y']**2) test_r2.append(r2) oos_pred = oos_data.copy() oos_pred['rethat'] = m.predict(X_oos) rec = oos_pred[['Dates','stkcd','y','rethat']].rename(columns={'rethat':'yhat'})7.系数稀疏化与因子重要性
弹性网的 L1 惩罚将大量系数压缩为零,留下的非零系数即为模型选出的活跃因子。
将 83 个滚动窗口的系数结果展开为一张大表:每行是「某 α × 某 OOS 月」的 65 维系数向量。 总计 5 × 83 = 415 行。这张表让我们可以分析系数随时间和正则化强度的变化模式。
弹性网的稀疏性来自 L1 惩罚项 。 当 α 较小(如 10⁻⁶)时,惩罚几乎不起作用,65 个系数中有 64.7 个平均非零—— 模型接近 OLS。随着 α 增大到 10⁻²,平均仅剩 2.6 个非零系数,模型变得极度稀疏。
最优 α = 10⁻³ 时,平均 21/65 个因子活跃。这意味着模型自动从 65 个候选因子中 筛选出约三分之一作为有效预测变量。被选中的因子在不同窗口间有一定稳定性, 但也存在时变性——反映了市场因子溢价的非平稳特征。
coef_rows = []for r, oos_int in zip(en_op, all_month_list[START:]): coefs = r[2] for a, s in coefs.items(): row = s.to_dict() row['AlphaValue'] = a row['Dates'] = oos_int coef_rows.append(row)all_coef = pd.DataFrame(coef_rows)all_coef = all_coef.sort_values(['AlphaValue','Dates']).reset_index(drop=True)print('all_coef shape:', all_coef.shape) # (415, 67)# 稀疏度统计sparsity_records = []for alpha_v in ALPHAS: sub = all_coef[all_coef['AlphaValue'] == alpha_v].drop(columns=['AlphaValue','Dates']) nonzero_per_window = (sub != 0).sum(axis=1) sparsity_records.append({ 'AlphaValue': alpha_v, 'mean_nonzero': float(nonzero_per_window.mean()), 'min_nonzero': int(nonzero_per_window.min()), 'max_nonzero': int(nonzero_per_window.max()), 'total_features': int(sub.shape[1]), }) # Top-20 因子重要性top20_records = []for alpha_v in ALPHAS: sub = all_coef[all_coef['AlphaValue'] == alpha_v].drop(columns=['AlphaValue','Dates']) abs_mean = sub.abs().mean().sort_values(ascending=False).head(20) for rank, (feat, val) in enumerate(abs_mean.items(), 1): top20_records.append({'AlphaValue': alpha_v, 'rank': rank, 'feature': feat, 'abs_mean_coef': val})8.样本外评估与多空组合
从预测精度和经济价值两个维度检验弹性网的样本外表现。
样本外评估的核心指标是 MSFE(均方预测误差)和 OOS R²。MSFE 定义为:
其中 N = 217,018 是所有 OOS 月的预测总数。最优 α = 10⁻³ 将 MSFE 压到 0.0286, 相比 α = 10⁻⁶(接近 OLS)的 0.0585 降低了 51%。这说明适度的正则化 显著减少了过拟合导致的预测误差。
从经济价值角度,将每个 OOS 月的股票按预测值 ŷ 分成十组(decile), 做多预测收益最高的 Top 10%、做空最低的 Bottom 10%,等权持仓一个月。 这个多空组合的月均收益为 6.6%,年化 Sharpe 比率约 2.5, 82 个月累计收益达 130 倍——表明弹性网的预测具有显著的截面排序能力。
# 整体 MSFE 与 R²metric_rows = []for a, sub in tuning_pred.groupby('AlphaValue'): msfe = float(np.sum((sub['yhat'] - sub['y'])**2) / len(sub)) r2 = float(1 - np.sum((sub['yhat'] - sub['y'])**2) / np.sum(sub['y']**2)) metric_rows.append({'AlphaValue': a, 'MSFE': msfe, 'R2_OOS': r2, 'n_pred': len(sub)}) # 月度 MSFE 分解monthly_msfe_records = []for a, sub in tuning_pred.groupby('AlphaValue'): m = sub.groupby('Dates').apply(lambda g: float(np.sum((g['yhat']-g['y'])**2) / len(g))) df_m = m.reset_index()9.SNR 视角:Ridge 为何优于 Lasso
信噪比与信号结构共同决定正则化方法的优劣——金融弱信号环境下 Ridge 胜出。
Hastie et al. (2020):SNR 决定方法优劣
Hastie, Tibshirani & Tibshirani (2020) 在系统仿真中发现:正则化方法的相对优劣 取决于信噪比(SNR)。SNR 衡量真实信号方差与噪声方差的比值。
- 低 SNR(< 1):Lasso 的收缩特性有利于控制方差,优于 Best Subset
- 高 SNR(> 2):Best Subset 的无偏估计更优,Lasso 的收缩成为负担
- Relaxed Lasso:通过参数 γ 自适应调节收缩程度,在所有 SNR 水平上表现最优
金融月度收益的 SNR 通常远低于 0.05(PVE < 5%),处于论文测试范围的下界之外。 我们的 A 股数据估计 SNR ≈ 0.001–0.05,OOS R² 均为负值,印证了这一判断。
Shen & Xiu (2024):弱信号下 Ridge 优于 Lasso
修大成(Dacheng Xiu)与 Shen (2024) 的论文 "Can Machines Learn Weak Signals?" 提出了 一个关键的补充视角:方法选择不仅取决于 SNR,还取决于信号结构。
- Lasso 的稀疏假设失效:Lasso 假设真实模型是稀疏的(少数强预测变量)。 但金融收益的预测能力分散在大量弱信号中——没有单个因子足够强到能被可靠"选中"。
- 变量选择在弱信号下不可靠:当所有信号都很弱时,Lasso 无法区分该保留哪些变量。 错误的选择导致 OOS 表现甚至不如零基准。
- Ridge 的均匀收缩保留集体预测力:Ridge 保留所有变量但均匀缩小系数。 当预测能力来自众多微弱贡献的累加时,"不丢弃"是最优策略。
实验验证:Ridge Sharpe = 3.89
我们在相同的 A 股滚动窗口框架下(24 月训练 / 12 月验证 / 1 月 OOS), 对比了 Ridge、Lasso、Elastic Net 和 Relaxed Lasso 四种方法。 Ridge 的年化多空 Sharpe 达到 3.89,几乎是 Lasso (2.03) 的两倍。
- 优势来源:Ridge 的月度多空波动率仅 6.55%(vs Lasso 8.42%、EN 10.05%)。 保留所有 65 个因子的集体信息使截面排序更加稳定。
- 低 SNR 窗口中优势最大:在估计 SNR 最低的 25% 窗口中, Ridge 多空收益 10.9% vs Lasso 2.5%——信号越弱,变量选择越不可靠,Ridge 越有价值。
- 点预测 vs 经济价值:Relaxed Lasso 的 MSFE (0.0294) 最低, 但 Ridge 的 Sharpe 最高。排序稳定性比点预测精度更重要。
实践建议
- 多空策略 / 排序任务 → Ridge(最稳定的截面排序,最高 Sharpe)
- 点预测 / 风险模型 → Relaxed Lasso(最低 MSFE,自适应收缩)
- 因子筛选 / 可解释性 → Lasso(稀疏解,但牺牲经济价值)
- Elastic Net → 不推荐(在两个维度上都不是最优)
:真实系数向量
:特征协方差矩阵
:噪声方差
:正则化强度
| 方法 | Sharpe | MSFE | 非零 |
|---|---|---|---|
| Ridge | 3.89 | 0.0298 | 65 |
| Lasso | 2.03 | 0.0302 | 12.8 |
| Relaxed | 1.57 | 0.0294 | 11.8 |
| EN | 1.26 | 0.0302 | 15.7 |
10.下载与致谢
本实验基于 Chapter 7 的中国 A 股月度面板数据集,核心方法参考:
- 姜富伟, 唐国豪, 马甜. 金融与财务机器学习[M]. 北京: 机械工业出版社, 2024.
- Zou, H. & Hastie, T. (2005). Regularization and variable selection via the elastic net. JRSS-B, 67(2), 301–320.
- Gu, S., Kelly, B. & Xiu, D. (2020). Empirical asset pricing via machine learning. RFS, 33(5), 2223–2273.
- Hastie, T., Tibshirani, R. & Tibshirani, R. (2020). Best subset, forward stepwise or lasso? Statistical Science, 35(4), 579–592.