ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

基金相关性预测全流程实战:特征工程与LightGBM建模

基金相关性预测全流程实战:特征工程与LightGBM建模

简介:完整收录CCF-BDCI基金相关性预测训练赛课程大作业成果,面向机器学习期末大作业、课程设计及赛题入门学习者,覆盖从数据分析、特征处理、模型训练到答辩汇报的完整链路。压缩包共5个文件、约681KB,含Python源代码(附Top3预测结果CSV)、技术报告docx、汇报PPT与README说明文档,结构清晰,各文件用途明确。当前已有267人学习/下载。源码包含详细注释,从数据加载、特征构造、模型训练到预测输出均有清晰指引,便于新手理解基金相关性预测的完整流程与实现细节;技术报告系统梳理了赛题背景、实验方案与关键结果,答辩PPT直接可用于课堂展示,README则补充了运行说明。整体是一份兼具参考性与可复用性的高分大作业模板,适合作为课程设计或竞赛项目的基础再开发。

1. 基金相关性预测:课程大作业里最值得做的数据竞赛题

基金相关性预测这类赛题,在课程大作业里属于“数据量不大、业务含义清楚、模型手段丰富”的类型。任务不是预测基金涨跌,而是预测未来一段时间内基金与基金之间的相关系数;听起来像做一次相关性计算,但真正落地时要解决时间窗切分、标签泄漏、pair特征设计、矩阵后处理和结果可复现这一串问题。选它当课程大作业的好处很直接:普通笔记本就能跑,不用抢服务器;技术报告有明确的图表可写;答辩时能从业务讲到模型、再从模型讲到工程交付,讲两个小时都不怕没材料。这篇笔记按一条可执行的主线展开——看懂赛题、跑通baseline、设计特征、训练调参、整理源代码和文档、组织答辩PPT,最后给一个答辩现场最实用的验证技巧。

2. 赛题拆解与数据准备:先用朴素baseline把流程跑通

2.1 赛题在预测什么:从净值序列到未来相关系数

先明确输入输出。这类预测赛题给的数据往往是多只基金在一段连续交易日内的净值序列,字段通常只有日期、基金代码、净值三列,跑数之前先把它整理成“日期为行、基金为列”的宽表,后面所有计算都基于这个结构。原始数据长这样:

字段含义示例
date交易日2023-01-05
fund_id基金唯一标识fund_001
nav复权单位净值1.5230

任务要预测的是未来某个时间窗口内任意两只基金之间的相关系数。常见设定是:已知截至 T 日的全部历史数据,预测 T+1 到 T+20 这 20 个交易日里的两两相关系数。注意这里预测的不是某一只基金的涨跌,而是一对基金在同一个未来窗口内同步波动的程度。之所以可预测,是因为市场风格、板块暴露、基金策略类型都有持续性,过去半年高度同步的两只主题基金,未来一个月大概率不会突然分道扬镳。

提示:做数据清洗时务必用复权净值。普通净值会因为分红除权产生人为跳变,导致收益率序列出现假波动,相关系数直接失真。

课程大作业拿这类训练赛做载体,最常见的问题不是模型跑不起来,而是很多人拿到数据就开始堆特征,根本说不清自己在预测什么。动手之前先写一段文字:“输入是什么、输出是什么、用什么数据计算标签、用什么数据计算特征”。这句话写清楚了,后面所有代码和报告才不至于跑偏。

2.2 数据预处理与时间窗划分:先解决标签泄漏

相关性预测最隐蔽的坑是标签泄漏。特征只能用 T 日之前的信息计算,标签只能用 T 日之后的信息计算,两边不能有任何一天重叠。很多初版代码为了省事,用同一段收益率既算特征又算标签,训练时分数很好看,一到测试集就翻车,因为模型学的是“拿答案抄答案”。

我一般先把净值转成对数收益率,然后按“特征窗口在前、标签窗口在后”的方式构造样本。每一条样本是两只基金的一个组合(pair),特征取截止 T 日前 60 天的收益表现,标签取 T 日后 20 天的真实相关系数:

import pandas as pd import numpy as np from itertools import combinations def make_pair_dataset(nav_df, feature_win=60, label_win=20, min_obs=40): """ 构造两两基金的时间序列样本。 nav_df: 宽表,行=日期,列=基金代码,值=复权单位净值 返回: 每行为一个 (date, fund_a, fund_b) 的样本 """ ret = np.log(nav_df).diff() # 对数收益率 dates = list(ret.index) rows = [] for t_idx in range(feature_win + label_win, len(dates)): t = dates[t_idx] # 特征窗口: [t - feature_win - label_win, t - label_win) feat_slice = ret.iloc[t_idx - feature_win - label_win: t_idx - label_win] # 标签窗口: [t - label_win, t] label_slice = ret.iloc[t_idx - label_win: t_idx] for a, b in combinations(ret.columns, 2): feat_pair = feat_slice[[a, b]].dropna() label_pair = label_slice[[a, b]].dropna() if len(feat_pair) < min_obs or len(label_pair) < min_obs: continue rows.append({ "date": t, "fund_a": a, "fund_b": b, "hist_corr": feat_pair[a].corr(feat_pair[b]), "hist_vol_a": feat_pair[a].std(), "hist_vol_b": feat_pair[b].std(), "ret_diff": feat_pair[a].mean() - feat_pair[b].mean(), "label": label_pair[a].corr(label_pair[b]), }) return pd.DataFrame(rows)

这段代码里最要命的设计是t_idx - feature_win - label_win到t_idx - label_win这个区间。特征窗口结束于t - label_win,和标签窗口开始的位置刚好错开,中间不重叠。min_obs=40控制的是两只基金在窗口内同时有收益数据的最少天数,避免用三个点算出来的相关系数拿来训练,那完全是噪声。hist_corr是最朴素也最有效的特征,后面所有模型都该拿它当基线参照物。

2.3 历史相关性Baseline:最简单的分数锚点

任何预测任务先做一个朴素 baseline,再谈模型。基金相关性预测的 baseline 非常直接:直接用过去 20 天的历史相关系数,当作未来 20 天相关系数的预测值。别笑,这个 baseline 经常能打败一半以上的花哨模型,因为相关性本身有很强的自相关性。

def baseline_rolling_corr(nav_df, window=20): """用最近 window 个交易日的相关系数作为下一阶段预测""" ret = np.log(nav_df).diff() # rolling.corr 返回多层索引,取最后一期作为全体 pair 的预测矩阵 corr = ret.rolling(window=window).corr() latest = corr.iloc[-1] # 转成方阵: latest 是带 MultiIndex 的 Series matrix = latest.unstack() return matrix pred = baseline_rolling_corr(nav_df, window=20)

rolling(window=20).corr()是等权滚动窗口,参数含义是“最近 20 个交易日”。窗口太短,相关系数受个别极端日影响大;窗口太长,又会把已经变化的市场风格算进去。对于基金相关性预测,20 到 60 日是比较常用的区间。建议把 20、40、60 三个窗口都跑一遍,生成的三个结果分别和真实标签算 MSE,选最低的那个作为 baseline 分。后面无论上什么模型,第一个要回答的问题就是:比这个滚动窗口低了多少?如果模型只低了 1%,那说明提升主要来自调参运气,而不是方法本身。

3. 相关性预测怎么做:特征工程与模型选型的核心取舍

3.1 特征设计:单基金状态与配对联动

有了 pair 样本之后,特征设计就是决定分数上限的环节。我的经验是把特征分成三层:单基金状态、配对关系、外部风格。单基金状态描述每一方自身的风险收益状态,配对关系捕捉两两之间的联动强度,外部风格数据有就加、没有也别硬凑。

特征类别具体特征说明
单基金状态区间收益、波动率、最大回撤、偏度、极端收益次数描述基金个体风险状态
配对关系多窗口历史相关、同涨同跌频率、收益差、波动率比值描述两两联动强度
风格暴露大小盘暴露差、行业暴露差、风格因子相关性数据可得时锦上添花

单基金状态里最容易被忽略的是偏度和极端收益次数。两只基金平均收益相同、波动率也差不多,但如果一个经常出现极端负收益、另一个收益分布很平,它们在未来极端行情下的同步性会很不一样。配对关系里,同涨同跌频率比相关系数更稳健:相关系数会被个别极端日主导,同涨同跌频率是对所有交易日等权看待,能反映更稳定的同步倾向。

多窗口历史相关一定要同时上。我只用一个窗口时,模型输出基本就是历史相关的线性变换;把 10、20、60、120 日四个窗口的相关系数都放进去之后,LightGBM 能学出“短窗口相关高但长窗口相关低说明只是短期情绪共振,未来相关性大概率回落”这类非线性判断。这是特征工程里性价比最高的一步。

3.2 模型选型:为什么 LightGBM 是稳妥起点

相关性预测可以走四条路线:线性回归、树模型、深度模型、矩阵补全。课程大作业的时间有限,我建议把 LightGBM 当主力,其他路线作为技术报告里的对比实验。

方案优点风险建议用途
线性回归可解释、训练快学不出非线性关系写进 baseline 对比
LightGBM稳定、容忍缺失、训练快需要调参主力模型
LSTM/GNN能建模时序依赖易过拟合、debug 慢有余力再试
矩阵补全/协同过滤思路新颖对时序特征利用弱做对比实验

选 LightGBM 不是因为它一定分数最高,而是因为它在大作业场景下最不容易翻车。它对特征尺度不敏感,不用做标准化;对缺失值有原生处理;自带正则和早停,不容易过拟合。最重要的是训练速度快,可以让你在一个晚上迭代几十组特征组合,把精力花在特征分析而不是训练优化上。

说句实在话,矩阵补全和 GNN 在这个任务里属于“看起来高级但很难稳定超过树模型”的方向。如果你要做技术报告里的亮点,可以只做一个小实验证明“深度模型在数据量小时不如树模型”,这也是一个合格的对比结论,比硬着头皮把 GNN 调三天要划算得多。

3.3 训练样本构造与 LightGBM 最小实现

有了特征和标签,直接开训。注意 pair 样本之间不是独立同分布的:同一只基金出现在很多样本里,同一天的样本共享市场环境,所以训练集和验证集必须按日期切,不能随机打乱。

import lightgbm as lgb from sklearn.metrics import mean_squared_error # pair_df 来自 2.2 节的 make_pair_dataset pair_df = pair_df.sort_values("date").reset_index(drop=True) split_date = pair_df["date"].quantile(0.8) train_df = pair_df[pair_df["date"] < split_date] val_df = pair_df[pair_df["date"] >= split_date] feature_cols = [ "hist_corr", "hist_vol_a", "hist_vol_b", "ret_diff", # 实际项目里再加: corr_10d, corr_20d, corr_60d, up_down_rate ... ] model = lgb.LGBMRegressor( n_estimators=600, learning_rate=0.05, num_leaves=31, min_child_samples=20, subsample=0.8, colsample_bytree=0.8, random_state=42, ) model.fit( train_df[feature_cols], train_df["label"], eval_set=[(val_df[feature_cols], val_df["label"])], callbacks=[lgb.early_stopping(50), lgb.log_evaluation(50)], ) val_pred = model.predict(val_df[feature_cols]) print("val mse:", mean_squared_error(val_df["label"], val_pred))

split_date用quantile(0.8)按日期位置切分,保证验证集全部晚于训练集。early_stopping(50)表示连续 50 轮验证集分数不提升就停,log_evaluation(50)让每 50 轮打印一次日志,方便观察是否过拟合。如果验证集 MSE 比历史相关 baseline 还高,先别调参,回去看特征窗口是不是有重叠,这是最常见的“高分翻车”源头。

4. 把精度抬上去:评估指标、交叉验证与调参顺序

4.1 评估指标选哪个:MSE、MAE 与 Spearman

基金相关性预测的官方评估一般用 MSE 或 MAE,但答辩时要能同时解释另外两个视角:相关系数本身的误差、预测排序和真实排序是否一致。我在报告里一般三个指标都列出来:

指标含义什么时候最有用
MSE预测与真实相关性的平方误差官方比分,最直接
MAE平均绝对误差对异常误差不那么敏感
Spearman 相关预测排序与真实排序的一致性答辩展示“选对强相关组合”

有个容易被追问的点:MSE 对“预测 0.8、真实 0.6”和“预测 0.3、真实 0.1”一视同仁,但从投资角度看,前者是幅度高估、后者只是噪声范围内偏差,业务含义完全不同。所以技术报告里可以把 MSE 拆成正误差和负误差两个方向,看模型是系统性高估还是低估相关性。这一步成本很低,但会让答辩老师觉得你想得比一般人深。

4.2 时间序列交叉验证:别让随机 K 折骗了你

pair 样本的日期是同一个市场环境,如果直接train_test_split(random_state=42),同一只基金、同一天的数据会同时出现在训练集和验证集,模型看到的“验证集”其实和训练集高度同源,分数虚高得离谱。正确做法是严格按时间顺序做交叉验证:

from sklearn.model_selection import TimeSeriesSplit # 先按日期排序,保证每一行先后顺序就是时间顺序 pair_df = pair_df.sort_values("date").reset_index(drop=True) tscv = TimeSeriesSplit(n_splits=5) scores = [] for fold, (train_idx, val_idx) in enumerate(tscv.split(pair_df)): train_fold = pair_df.iloc[train_idx] val_fold = pair_df.iloc[val_idx] model = lgb.LGBMRegressor( n_estimators=600, learning_rate=0.05, num_leaves=31, min_child_samples=20, subsample=0.8, colsample_bytree=0.8, random_state=42, ) model.fit( train_fold[feature_cols], train_fold["label"], eval_set=[(val_fold[feature_cols], val_fold["label"])], callbacks=[lgb.early_stopping(50)], ) pred = model.predict(val_fold[feature_cols]) scores.append(mean_squared_error(val_fold["label"], pred)) print(f"fold {fold + 1}: {scores[-1]:.4f}") print("cv mse mean:", np.mean(scores))

TimeSeriesSplit(n_splits=5)会按行顺序生成 5 组训练/验证切分,每组的验证集一定在训练集之后。这里有个隐含前提:pair_df必须已按日期排序,而且同一天的样本要连续排列,否则同一日期会被人为切开。最后取 5 折 MSE 的均值作为模型真实水平,这个数字才能写进技术报告。

4.3 调参顺序与三个最值得调的参数

LightGBM 调参有一些玄学成分,但我总结出一个稳定的顺序,能避免在参数海洋里溺亡。前提是把learning_rate固定在 0.05,n_estimators交给早停决定,不去手动碰迭代次数。

调参顺序参数经验范围作用
第 1 步num_leaves15~127控制模型复杂度,越大越容易过拟合
第 2 步min_child_samples5~50控制叶子节点最少样本数
第 3 步colsample_bytree0.5~0.9每次迭代随机抽特征比例

第一步只调num_leaves,其他参数保持默认,用 4.2 节的时间序列交叉验证跑 5 个值,选 MSE 最低的。第二步固定num_leaves,调min_child_samples。第三步再固定前两个,调colsample_bytree。为什么是这个顺序?因为num_leaves决定模型容量上限,不先定容量就去调采样率,效果会被相互干扰。colsample_bytree放在最后,是因为它对结果的影响最小,主要作用是降低方差。

一个常见的血泪经验:不要拿同一个验证集反复调参超过五次。每调一次参数就在验证集上看一次分数,本质上是在把验证集的信息泄漏进模型选择过程,最后提交到测试集一定会缩水。我一般限制自己:同一个验证折上最多调三轮,后面只做特征增删,不再动参数。

4.4 后处理:对称化与半正定修正

模型预测得到的是所有 pair 的相关系数,要还原成完整的相关性矩阵才能写进报告。直接拼矩阵会出现一个尴尬的问题:预测的corr(A,B)和corr(B,A)大概率不一样,矩阵不对称;更严重的还会出现负特征值,这在数学上不是合法的相关系数矩阵。后处理是白捡分的环节:

import numpy as np # pred_pairs: DataFrame,每行是 pair 预测结果 matrix = np.eye(n_funds) for _, row in pred_pairs.iterrows(): i, j = fund_index[row["fund_a"]], fund_index[row["fund_b"]] val = float(np.clip(row["pred"], -1, 1)) matrix[i, j] = val matrix[j, i] = val # 强制对称 # 半正定修正:拉平负特征值 eigvals, eigvecs = np.linalg.eigh(matrix) eigvals = np.clip(eigvals, 0.0, None) fixed = eigvecs @ np.diag(eigvals) @ eigvecs.T # 归一化对角线为 1 d = np.sqrt(np.diag(fixed)) fixed = fixed / np.outer(d, d)

强制对称是最基本的要求。半正定修正的原理是:把特征值中小于 0 的截断为 0,再用逆变换拼回矩阵,这样矩阵在数学上就“合法”了。最后一步除以对角线的平方根,是让对角线重新变成 1。这个后处理不会显著改变整体误差,但会让你的技术报告里放出来的相关矩阵热力图干净很多,答辩时少一个被追问的点。

5. 交付物避坑:源代码、文档、技术报告与答辩PPT最容易丢分的地方

5.1 源代码工程化:跑得起来比什么都重要

课程大作业的源代码,评判标准不是代码多优雅,而是“另一台机器能不能按 README 跑出你报告里的数字”。我见过太多报告写得漂亮、代码却跑不起来的项目:有的缺requirements.txt,有的数据路径写死成C:\Users\me\...,有的把中间结果存在本地缓存里根本没提交。源代码这块建议按下面这个结构组织:

project/ ├── README.md ├── requirements.txt ├── data/ │ ├── raw/ # 原始净值数据(放进来,方便直接复现) │ └── processed/ # 预处理后的宽表与 pair 样本 ├── src/ │ ├── preprocess.py │ ├── features.py │ ├── train.py │ └── evaluate.py ├── results/ │ ├── pred_matrix.csv │ └── metrics.json └── docs/ ├── 技术报告.md └── 答辩PPT.pptx

data/raw/里直接放一份原始数据,别指望评分的人去平台上下载。src/里四个文件各干一件事,不要把所有逻辑堆在一个main.py里,否则 500 行代码没人愿意读。results/里必须有最终预测结果和指标,这是复现报告数字的关键。

5.2 文档说明:环境、命令、数据路径、复现结果

文档说明(README)通常是最被低估的交付物。一个合格的 README 最少要有四段内容:环境依赖、数据放哪、怎么跑、跑完看什么。用第一人称写清楚每一步,假设读者是一台全新电脑。

# 环境安装 pip install -r requirements.txt # 数据预处理:从原始净值生成 pair 样本 python src/preprocess.py --input data/raw/nav.csv --output data/processed/ # 训练模型并评估 python src/train.py --data data/processed/pair.csv --output results/ # 生成并检查最终指标 python src/evaluate.py --pred results/pred_matrix.csv --label data/processed/label.csv

每个命令后面要写一行注释:这一步干什么、运行多久、输出什么文件。脚本里所有路径都用相对路径,并且以项目根目录为起点,不要出现绝对路径。一个很实用的习惯:在项目根目录加一个run_all.sh,把上面三条命令串起来,评分的人一条命令就能复现全流程。

5.3 技术报告:让老师二十分钟看懂你的工作

技术报告和 README 是两回事:README 教人怎么跑代码,技术报告讲清楚“为什么这样做”。常见误区是把报告写成模型介绍大全,从线性回归到 Transformer 每个都贴公式,看了半天不知道作者自己干了什么。我的报告结构稳定是五段:

章节内容篇幅建议
问题定义任务输入输出、评估方式、数据规模1~2 页
数据与方法时间窗切分、特征设计、模型选择理由3~4 页
实验设置交叉验证方式、baseline、超参数1~2 页
实验结果baseline 对比、消融实验、误差分析3~4 页
结论与失败记录哪些实验失败、为什么、如果重做什么1 页

实验对比表必须有:历史相关 baseline、线性回归、LightGBM、后处理前后指标。哪怕线性回归比 baseline 还差,也要写上去,这叫“失败实验记录”,比只报喜不报忧更能体现工程素养。图表要和数字对齐:热力图上标的数值、散点图里的点、表格里的 MSE,三个地方必须是同一次运行的结果,你不能训练跑了一遍、画图又跑了一遍,最后数字对不上。

5.4 答辩PPT:一页只回答一个问题

答辩 PPT 最常见的问题是信息密度失衡:要么每页都是公式截图,要么每页只有一句话。我的做法是每页只回答一个问题,一个页面就是一个完整的论点。

页码页面标题回答的问题
1赛题与任务定义我预测的是什么,输入输出什么
2数据与时间窗设计如何切窗避免标签泄漏
3特征与模型选择为什么选这些特征,为什么用 LightGBM
4实验结果比 baseline 低多少,提升来自哪里
5结论与不足什么地方失败了,重做会怎么改

每一页的标题要是一句结论,而不是名词。比如“标签泄漏是分数虚高的最大源头”就比“数据预处理”好得多。页面上只放和结论相关的图表,公式最多两行。答辩老师翻 PPT 的速度远比你讲的快,一页能看懂你在回答什么问题,他才会有兴趣听你展开。

5.5 五个高频踩坑记录:现象、原因、解决

第一条:训练集 MSE 很低,提交测试集分数大幅缩水。现象:本地 0.012,测试集 0.019。原因:随机划分验证集导致未来信息泄漏,模型在跨时间泛化时失效。解决:改用 4.2 节的时间序列交叉验证重新评估,按日期切分后重训。

第二条:预测的相关矩阵不对称,被老师当场指出数据有 bug。现象:corr(A,B)=0.35而corr(B,A)=0.42。原因:预测脚本只填充了矩阵下三角,忘了对称。解决:所有 pair 预测都写入两个位置,并用np.allclose(matrix, matrix.T)做断言检查。

第三条:报告里的 MSE 和 PPT 里的 MSE 对不上。现象:文档里写 0.014,PPT 里写 0.011。原因:两次运行用了不同的随机种子,或者最后一次调参后忘了更新报告。解决:固定random_state=42,所有交付物引用同一份metrics.json的数字,不在文档里手动敲任何指标。

第四条:别人拿到代码跑不出结果。现象:README 里没写数据路径,直接运行报FileNotFoundError。原因:代码里用了写死的绝对路径。解决:统一改用相对路径,并把data/raw/nav.csv放进项目,保证一条命令从头跑到尾。

第五条:答辩时被问“你的提升到底是特征带来的还是调参带来的”答不上来。现象:只给了最终分数,没有中间过程。原因:没做消融实验。解决:提前跑三组实验——只用hist_corr、用全部特征、用全部特征加调参,三行分数列在 PPT 里,提升来源一眼就清楚。

6. 答辩现场的一图胜千言:相关性散点图与结果对齐技巧

答辩时与其背参数,不如直接放一张“预测 vs 真实”散点图。把验证集所有 pair 的真实相关系数放横轴、模型预测值放纵轴,每个点是一只基金对。再叠加一个历史相关 baseline 的散点作对比,两张图放在同一页,评委能一眼看到模型在哪些区间比 baseline 准、哪些区间还在漂移。

import matplotlib.pyplot as plt # val_df 带上模型预测 val_df["pred"] = val_pred plt.figure(figsize=(6, 6)) plt.scatter(val_df["label"], val_df["hist_corr"], alpha=0.3, label="baseline") plt.scatter(val_df["label"], val_df["pred"], alpha=0.3, label="model") plt.plot([-1, 1], [-1, 1], "--", color="gray", label="perfect") plt.xlabel("true correlation") plt.ylabel("predicted correlation") plt.legend() plt.grid(alpha=0.3) plt.savefig("results/scatter_compare.png", dpi=150)

这张图的用途不是展示指标,而是展示“模型在哪里进步”:如果模型的点比 baseline 更聚拢在对角线附近,说明提升是真实的;如果只是整体点云更密,分数却差不多,说明提升可能只是压缩了预测方差。答辩现场把这个图和 MSE、Spearman 三个东西一起讲,从数值、分布、排序三个角度闭环,基本不太会被追问倒。

还有一个值钱的小习惯:把最终预测矩阵的热力图也放进 PPT,但要在图下方写一行小字说明“矩阵已经过对称化和半正定修正”。这样一来,懂行的人知道你不只是会调包,还处理了数学上的合法性。我第一次做的时候没固定随机种子,训了两版模型,报告和 PPT 各写一个版本的数字,被老师一眼看穿。后来一律以results/metrics.json里导出的数字为准,所有图表从同一个脚本重新生成,再也没出过这种低级错误。希望这几个踩出来的教训能帮到你,让你的课程大作业从“跑通了”变成“站得住”。

本文还有配套的精品资源,点击获取

返回列表