4. 标签构建与数据划分:未来收益率计算、分类标签构建(涨/跌/平)、时间序列数据划分(训练/验证/测试)
好,咱们接着往下走。前面几章我们把数据拿回来了,也做了清洗和特征工程。现在手里有了一堆干干净净的因子数据,对吧?但光有这些还不够——模型得知道“学什么”。
这一章,我们就来解决这个核心问题:给数据打标签,然后划分数据集。
说白了,就是告诉模型:
“你看,历史上这些特征组合出现的时候,未来3天股价是涨了还是跌了?”
模型才能从中找到规律。
核心逻辑一句话:
特征 = 当前已知的信息(X)
标签 = 未来一段时间的收益表现(y)
模型的任务:从 X 预测 y。
4.1 未来收益率计算
我个人习惯,先把“未来收益率”算出来,再根据它来构建分类标签。
为什么要算未来收益率?
因为我们要知道:如果今天买入,持有N天后,能赚多少?
这里有个关键点:未来收益率 = 未来收盘价 / 当前收盘价 - 1
举个例子:
今天是1月1日,收盘价10元。
1月4日(3天后)收盘价11元。
那么未来3日收益率 = 11/10 - 1 = 10%。
嗯,这里要注意:时间窗口的选择。我见过不少新手直接拿未来1天的收益率做标签,结果模型过拟合得一塌糊涂。为什么?因为1天的噪声太大了,随机性太强。
我的经验:
- 短线模型:用未来3日或5日收益率
- 中线模型:用未来10日或20日收益率
- 长线模型:用未来60日或120日收益率
我个人做短线选股,最常用的是未来5日收益率。这个窗口既能过滤掉部分噪声,又不会太滞后。
代码实现其实很简单,用pandas的shift和rolling就能搞定:
import pandas as pd
import numpy as np
# 假设df已经按股票代码和日期排序
# 计算未来5日收益率
df['future_return_5d'] = df.groupby('stock_code')['close'].transform(
lambda x: x.shift(-5) / x - 1
)
这里有个坑:shift(-5)意味着最后5天的数据会变成NaN。因为未来5天的收盘价不存在了。所以回测的时候,最后几天的数据要记得剔除。
避坑指南:
我曾经在回测时忘了剔除最后几天的NaN标签,结果模型在测试集上表现“异常好”——因为它只预测了那些有标签的样本,而这些样本恰好都是历史数据。说白了就是未来信息泄露了。嗯,从那以后,我每次都会检查标签列是否有NaN。
4.2 分类标签构建:涨/跌/平
未来收益率算出来了,接下来就是把它变成分类标签。
为什么不用回归?
回归模型直接预测收益率数值,听起来更精确。但实际中,回归模型对异常值非常敏感。比如某只股票突然涨停,收益率20%,模型会拼命去拟合这个极端值,反而忽略了大部分正常样本。
所以,我更倾向于用分类模型。把问题简化成:
- 涨(1)
- 跌(-1)
- 平(0)
那阈值怎么定?
我个人习惯用百分位数,而不是固定阈值。
举个例子:
把所有股票的未来5日收益率排序,
- 前30% → 涨
- 后30% → 跌
- 中间40% → 平
这样做的好处是:每个时间截面上的标签分布是均衡的。不会因为某段时间大盘暴涨,导致“涨”的标签特别多。
def label_future_return(df, ret_col='future_return_5d',
upper_percentile=0.7, lower_percentile=0.3):
"""
根据未来收益率构建分类标签
"""
# 按日期分组,每个交易日独立分位数
def _label_group(group):
upper = group[ret_col].quantile(upper_percentile)
lower = group[ret_col].quantile(lower_percentile)
labels = np.zeros(len(group))
labels[group[ret_col] >= upper] = 1 # 涨
labels[group[ret_col] <= lower] = -1 # 跌
# 中间部分保持0(平)
return labels
df['label'] = df.groupby('date').apply(_label_group).values
return df
为什么按日期分组?
你想想看,如果我不按日期分组,而是对所有历史数据一起算分位数,会怎样?
假设2015年牛市,所有股票都在涨,那“涨”的阈值会非常高。到了2018年熊市,所有股票都在跌,那“跌”的阈值会非常低。
这样模型学到的其实是市场整体趋势,而不是选股能力。
所以,每个交易日独立计算分位数,才能让模型专注于“相对强弱”。
4.3 时间序列数据划分:训练/验证/测试
数据有了,标签有了,接下来就是划分数据集。
这里我要强调一点:金融时间序列数据,绝对不能随机划分!
为什么?
因为时间序列有时序依赖性。今天的股价和昨天的股价是相关的。如果你随机打乱数据,把未来的数据拿去训练,把过去的数据拿去测试,那就相当于用未来预测过去——结果当然好,但毫无意义。
正确的做法是:按时间顺序划分。
我个人常用的划分比例:
- 训练集:60%
- 验证集:20%
- 测试集:20%
但这里有个细节:要按时间点切分,而不是按样本量切分。
举个例子:
假设数据从2018年1月到2023年12月,共6年。
- 训练集:2018年1月 ~ 2021年12月(4年)
- 验证集:2022年1月 ~ 2022年12月(1年)
- 测试集:2023年1月 ~ 2023年12月(1年)
这样划分的好处是:验证集和测试集能模拟真实的未来场景。
def time_series_split(df, train_end='2021-12-31',
val_end='2022-12-31'):
"""
按时间顺序划分数据集
"""
train = df[df['date'] <= train_end]
val = df[(df['date'] > train_end) & (df['date'] <= val_end)]
test = df[df['date'] > val_end]
return train, val, test
一个小技巧:
我习惯在划分之前,先检查一下每个时间段的股票数量是否均衡。如果某段时间因为IPO或者退市,股票数量变化很大,那就要考虑调整划分比例。
另外,验证集和测试集的时间跨度最好一致。比如都是1年,或者都是6个月。这样对比模型在验证集和测试集上的表现时,才有可比性。
4.4 本章知识体系总览
下面这张图,把本章的核心逻辑串起来了:
4.5 本章小结
这一章的内容,说白了就三件事:
- 算未来收益率:选好时间窗口,用shift计算
- 打分类标签:按日期分组,用百分位数划分涨/跌/平
- 划分数据集:按时间顺序切分,不能随机打乱
这三步做完,你的数据就真正“可用”了。模型可以开始学习了。
嗯,我记得刚开始做量化的时候,总觉得打标签这一步很简单,随便写几行代码就完事了。后来发现,标签的质量直接决定了模型的上限。标签打歪了,再好的模型也白搭。
所以,这一章的内容虽然看起来基础,但值得你多花点心思。