4. 简单随机切分:使用 sklearn 的 train_test_split 进行随机切分,适用场景与局限性
简单随机切分是绩效归因训练集切分中最基础、最常用的方法。它通过完全随机的机制将数据集划分为训练集和测试集,不依赖任何时间顺序或分组结构。在 sklearn 中,train_test_split 函数提供了这一功能的标准化实现。
4.1 核心原理与实现
简单随机切分的核心思想是:对每个样本独立地以固定概率分配到训练集或测试集。对于绩效归因数据,这意味着每个时间点、每个投资组合的观测值都有相同的概率被选入训练集或测试集。
关键参数:
- test_size:测试集比例(如 0.2 表示 20% 数据用于测试)
- random_state:随机种子,保证结果可复现
- shuffle:是否在切分前打乱数据(默认 True)
- stratify:分层抽样依据,保持类别比例
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
# 模拟绩效归因数据:包含时间、组合ID、因子暴露和超额收益
np.random.seed(42)
n_samples = 1000
data = pd.DataFrame({
'date': pd.date_range('2020-01-01', periods=n_samples, freq='D'),
'portfolio_id': np.random.choice(['A', 'B', 'C'], n_samples),
'factor_exposure': np.random.randn(n_samples),
'excess_return': np.random.randn(n_samples) * 0.02
})
# 特征和目标变量
X = data[['factor_exposure']]
y = data['excess_return']
# 简单随机切分:70% 训练,30% 测试
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42, shuffle=True
)
print(f"训练集样本数: {len(X_train)}")
print(f"测试集样本数: {len(X_test)}")
print(f"训练集日期范围: {data.loc[X_train.index, 'date'].min()} 至 {data.loc[X_train.index, 'date'].max()}")
print(f"测试集日期范围: {data.loc[X_test.index, 'date'].min()} 至 {data.loc[X_test.index, 'date'].max()}")
实践建议:在绩效归因中,建议设置
random_state 为固定值(如 42),确保每次运行结果一致。同时,shuffle=True 是默认行为,但对于时间序列数据,这可能导致未来信息泄露。
4.2 适用场景
简单随机切分在以下绩效归因场景中表现良好:
| 场景 | 说明 | 示例 |
|---|---|---|
| 截面数据归因 | 同一时间点多个组合的横截面分析 | 月度所有基金的因子暴露与收益 |
| 独立观测值 | 样本间无时间依赖或序列相关性 | 不同策略的独立回测结果 |
| 大规模数据集 | 样本量足够大,随机性不会导致严重偏差 | 超过 10,000 条日频归因记录 |
| 初步模型验证 | 快速评估模型性能,无需严格时间结构 | 因子有效性初步筛选 |
| 分层抽样需求 | 保持不同组合或策略的分布比例 | 按组合类型分层切分 |
4.3 局限性分析
在绩效归因领域,简单随机切分存在以下关键局限:
- 时间依赖性破坏:随机打乱会破坏时间序列结构,导致模型使用未来信息预测过去,产生过高的绩效评估。
- 组合信息泄露:同一投资组合的观测值可能同时出现在训练集和测试集,造成组合层面的数据泄露。
- 市场状态不均衡:训练集和测试集可能包含不同市场环境(如牛市 vs 熊市),导致归因结果不稳定。
- 小样本偏差:当样本量较小时,随机切分可能导致训练集或测试集分布严重偏离总体。
- 无法评估时序泛化:无法验证模型在未见过的未来时间段的表现。
绩效归因中的致命陷阱:使用简单随机切分处理时间序列归因数据时,模型可能“偷看”未来信息。例如,训练集中包含 2023 年 6 月的数据,测试集中包含 2023 年 1 月的数据,模型在训练时已经见过未来的市场环境,导致归因结果虚高。
4.4 随机切分 vs 时间序列切分
| 对比维度 | 简单随机切分 | 时间序列切分 |
|---|---|---|
| 数据顺序 | 完全打乱 | 保持时间顺序 |
| 未来信息泄露 | 存在风险 | 避免 |
| 适用数据 | 截面数据、独立样本 | 时间序列、面板数据 |
| 绩效评估可靠性 | 可能高估 | 更接近实际 |
| 实现复杂度 | 低(一行代码) | 中(需自定义) |
| sklearn 支持 | train_test_split | TimeSeriesSplit |
4.5 随机切分流程示意图
4.6 绩效归因中的典型错误示例
以下代码展示了在时间序列归因数据上错误使用简单随机切分的情况:
# 错误示例:对时间序列归因数据使用简单随机切分
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import r2_score
# 模拟时间序列归因数据
dates = pd.date_range('2020-01-01', periods=500, freq='D')
np.random.seed(42)
time_series_data = pd.DataFrame({
'date': dates,
'momentum_factor': np.random.randn(500) + np.linspace(0, 1, 500), # 趋势性因子
'excess_return': np.random.randn(500) * 0.02 + np.linspace(0, 0.05, 500)
})
X = time_series_data[['momentum_factor']]
y = time_series_data['excess_return']
# 错误:随机切分导致未来信息泄露
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42
)
# 训练模型
model = LinearRegression()
model.fit(X_train, y_train)
# 评估(结果虚高)
y_pred = model.predict(X_test)
print(f"错误切分的 R²: {r2_score(y_test, y_pred):.4f}")
# 正确做法:按时间顺序切分
split_idx = int(len(time_series_data) * 0.7)
X_train_time = X.iloc[:split_idx]
X_test_time = X.iloc[split_idx:]
y_train_time = y.iloc[:split_idx]
y_test_time = y.iloc[split_idx:]
model_time = LinearRegression()
model_time.fit(X_train_time, y_train_time)
y_pred_time = model_time.predict(X_test_time)
print(f"时间序列切分的 R²: {r2_score(y_test_time, y_pred_time):.4f}")
输出对比:
- 错误随机切分 R²:通常较高(如 0.85),反映的是模型“预测过去”的能力
- 正确时间切分 R²:通常较低(如 0.45),反映的是模型“预测未来”的真实能力
- 差异原因:随机切分让模型学到了时间趋势,而非真正的因子-收益关系
4.7 何时坚持使用简单随机切分
尽管存在局限性,以下情况仍应优先考虑简单随机切分:
- 截面归因分析:分析同一时间点不同组合的因子暴露差异,样本间独立
- 策略分类任务:判断组合属于价值型还是成长型,不涉及时间序列
- 因子筛选阶段:快速评估大量候选因子的区分能力
- 数据量极大:超过 100 万条记录时,随机切分的偏差可忽略
- 分层抽样需求:使用
stratify参数保持组合类型、行业分布等比例
最佳实践:在绩效归因项目中,建议将简单随机切分作为“基线方法”,然后使用时间序列切分或组合分组切分进行对比。如果两种方法的结果差异较大(如 R² 相差超过 0.1),说明数据中存在严重的时间依赖或分组结构,应放弃简单随机切分。