简介:CCF-BDCI基金相关性预测课程实训资源包,面向机器学习、数据挖掘方向的高校学生、科研人员及竞赛入门者,完整覆盖从赛题理解、环境配置、数据探索、特征工程到模型训练与结果评估的工程流程。压缩包内含可运行源码、技术报告、答辩PPT和配套文档全集,源码经过严格测试,稳定复现基金净值相关性预测任务;技术报告详细记录方案设计、实验对比与结论,答辩PPT提炼核心亮点,文档齐全便于课程实训、毕业设计或项目立项参考。资源以zip格式打包,整体约715KB,文件类型以Python脚本、Word文档及PowerPoint演示文稿为主,模块划分清晰,便于按需查阅与二次开发,既可直接用于课程实训与毕业设计,也可作为拓展预测项目的起点。项目已通过功能验证,适合不同基础的学习者借鉴改进,目前已有70人学习/下载,如遇配置或运行问题,可进行远程指导交流。
1. 基金相关性预测赛题:一门课里能把数据、特征、模型全练到的实训方向
“基金相关性预测”这个赛题,表面上是预测一组基金之间的相关系数矩阵,实际上考的是时序数据理解、特征构造、标签对齐和金融噪声的过滤能力。我先后在某课程实训里带学员做过三轮这个方向,最直观的结论是:多数人能在一周内跑通模型,但要做到数据切片干净、标签不泄露、答辩能讲清楚,至少还需要一周。这题适合想系统练一遍 pandas、特征工程和树模型的人,也适合课程实训拿来做“能演示、有指标、有可视化”的完整项目。它比纯图像分类更有信息密度,比纯销量预测更好讲业务价值。
2. 赛题拆解与数据认识:先搞清楚要预测什么再动手
2.1 任务定义与评估指标:相关性矩阵预测到底在算什么
赛题输入通常是若干只基金在某个时间段内的净值序列,输出是未来一段时间的基金两两相关矩阵。很多新手一上来就把“预测相关矩阵”理解成“预测未来净值曲线”,然后抱着 LSTM 去拟合价格,这是方向性错误。相关性是一个截面统计量,它描述的是两只基金在同一段时间内收益变动的同步程度,而不是价格本身。
任务定义上,常见做法是切成两种:一种是预测未来固定时间窗口内的滚动相关矩阵,另一种是预测下一期横截面相关矩阵。前者偏时序回归,后者偏截面预测。实训里绝大多数队伍用的是前者:把历史收益矩阵切成样本,用过去 T 天的特征预测未来 H 天的相关矩阵。
评估指标上,竞赛常用平均绝对误差 MAE 或矩阵 Frobenius 范数误差。MAE 的优点是直观,单位就是相关系数的绝对值误差,答辩时好解释;Frobenius 范数对整体偏差更敏感,但听众不容易直观感受。我一般建议实训报告里两个都算,主指标用 MAE,附指标用 Frobenius,避免答辩被追问“你这个误差大不大”时只能笼统回答。
选型理由要说清楚:为什么不做成逐对基金独立预测?因为逐对预测会忽略横截面的共同冲击。比如市场系统性下跌会同时压低所有基金收益,逐对模型各自拟合,系数震荡大;矩阵级的数据组织方式或批量样本训练,至少能让同期样本共享同一种市场状态,误差更稳。
2.2 净值数据的结构与预处理基线
原始数据一般长这样:date、fund_code、nav,nav 可能是单位净值也可能是复权净值。预处理基线分四步:按基金代码与日期排序、分红除权修正、收益序列计算、异常值截断。排序是基础,很多翻车都发生在数据没排序、pct_change 当成全局函数而不是组内函数用,导致收益全算错。
分红修正这块,基金相关性分析必须用复权口径。单位净值遇到大额分红会突然下跌,直接用单位净值算日收益,会在分红日制造一个很大的负收益,进而污染整段相关性。常见做法是直接使用累计净值,或者在拿到原始数据后做复权因子还原。如果实训给的数据已经是复权净值,这一步可以跳过,但要在技术报告里注明口径,这也是答辩加分项。
异常值截断建议用分位数裁剪而不是直接删除。删除样本会导致不同基金的时间轴错位,后面构造相关矩阵时配对不上;裁剪则保留时间点,只是把极值拉回合理范围。
2.3 净值加载与数据形态检查:动手前的最后一步
写一个最小脚本确认数据形态,再考虑后续特征工程。代码:
import pandas as pd # 读取净值数据,字段:date, fund_code, nav df = pd.read_csv('fund_nav.csv', parse_dates=['date']) df = df.sort_values(['fund_code', 'date']).reset_index(drop=True) # 组内计算日收益,dropna 去掉每只基金的首个交易日 df['ret'] = df.groupby('fund_code')['nav'].pct_change() # 用 1% 和 99% 分位数做极值截断,避免单日异常净值污染相关性 df['ret'] = df.groupby('fund_code')['ret'].transform( lambda x: x.clip(lower=x.quantile(0.01), upper=x.quantile(0.99)) ) # 检查:每只基金的样本量是否对齐,日期是否有缺口 pivot = df.pivot(index='date', columns='fund_code', values='ret') print(pivot.shape, pivot.isna().sum().sum())逻辑说明:groupby 配合 pct_change 确保收益是在每只基金内部计算,不会出现跨基金串行;transform 里的 clip 在组内做分位数截断,同时保留原时间索引,不影响后续 pivot 成面板。参数说明:1% 分位数是经验值,若数据源没有明显异常跳变,可以放宽到 0.5%,过紧的截断会把真实市场波动削掉,反而不利于后续相关性计算。
pivot 之后你还会发现两个常见问题:一是部分基金在某些交易日停牌或未更新净值,形成空值;二是不同基金上架日期不同,早期只有少数基金有数据。这两个问题直接决定后面特征窗口怎么切,下一章展开。
3. 特征工程与数据预处理:把净值序列变成模型能吃的特征面板
3.1 收益面板与时间窗口切分
把长表转成宽表后,得到的是一个 T×N 的日收益矩阵,T 是交易日,N 是基金数量。模型要从这个矩阵中预测 N×N 的相关矩阵,样本构造的核心循环是:以时间 t 为锚点,取过去 L 天的收益矩阵计算特征,然后把 t 到 t+H 天的真实相关矩阵当作标签。
这里有个隐蔽细节:训练集和验证集不能随机打乱。相关性预测本质是时序问题,随机切分会把未来信息泄进训练集。我通常按时间顺序切:前 60% 时间训练、中间 20% 验证、最后 20% 测试。窗口参数 L 取 60 个交易日比较稳妥,H 取 5,对应一周的预测跨度。
窗口重叠策略也影响样本量。如果实训数据只有两三年净值,按 step=1 滑动会产生大量相邻样本,训练指标会很乐观;按 step=5 滑动样本相对独立,验证结果更可信。答辩时建议用不重叠的 step 再做一次评估,评委问“你这指标是不是过拟合”时能拿出第二组数字。
3.2 标签构造:严格不重叠的窗口
标签的计算要独立于特征。正确姿势是拿到收益矩阵后,直接对 t+1 到 t+H 这段收益用 corr 求相关矩阵,只保留上三角作为预测目标。上三角展开后是 N×(N-1)/2 个值,这就是每个样本的输出维度。
常见翻车是标签窗口和特征窗口重叠。比如用过去 60 天算特征,却用过去 30 天到未来 30 天算标签,中间重叠部分会让模型在测试时表现虚高,答辩被追问数据泄露时很难解释。代码实现:
import numpy as np def make_samples(return_panel, feat_len=60, horizon=5, step=5): """ return_panel: DataFrame, 行为日期, 列为基金, 值为日收益 yield: (X, y) X: 过去 feat_len 天的收益矩阵展开特征 y: 未来 horizon 天的相关矩阵上三角 """ dates = return_panel.index for i in range(feat_len, len(dates) - horizon, step): # 特征窗口 feat_window = return_panel.iloc[i - feat_len: i] # 标签窗口:与特征窗口严格不重叠 label_window = return_panel.iloc[i: i + horizon] # 历史相关矩阵作为特征的一部分 hist_corr = feat_window.corr().values # 未来真实相关矩阵作为标签 true_corr = label_window.corr().values upper = true_corr[np.triu_indices_from(true_corr, k=1)] yield hist_corr[np.triu_indices_from(hist_corr, k=1)], upper逻辑说明:窗口起点 i 从 feat_len 开始,保证特征窗口完整;标签窗口从 i 起算,与特征窗口零重叠。step=5 让相邻样本间隔一周,样本量适中。参数说明:如果数据集很小,step 可以降到 1,但相邻样本高度自相关会导致训练指标偏乐观,技术报告里要注明这一点,最好在验证集上用 step 不重叠再测一次。
3.3 特征面板标准化与缺失值处理
特征标准化要注意量纲。相关矩阵的取值范围是 -1 到 1,但直接把它上三角展开喂给模型,模型并不知道这些值之间来自同一个矩阵。所以很多队伍会额外补一组特征:滚动波动率、滚动动量、横截面收益排序百分位、市场因子(所有基金收益的等权平均)。这些特征的量纲差异很大,需要做 z-score 标准化。
缺失值处理优先用前向填充。基金停牌导致的缺失,用前一天的收益值填充比填 0 更合理,因为停牌当天实际成交价不变,收益约等于 0,但前值法能保留净值水平信息。如果某只基金在特征窗口内缺失超过 20%,直接丢弃该基金,否则相关矩阵会被无效值污染。
from sklearn.preprocessing import StandardScaler # 假设 X_raw 是 (n_samples, n_features) 的原始特征矩阵 # 先看缺失率,再决定删样本还是删特征 missing_rate = np.isnan(X_raw).mean(axis=1) X_raw = X_raw[missing_rate < 0.2] y_raw = y_raw[missing_rate < 0.2] # 在训练集上 fit,验证集和测试集只能 transform scaler = StandardScaler().fit(X_raw) X_scaled = scaler.transform(X_raw)逻辑说明:StandardScaler 在训练集上 fit,验证集和测试集只能 transform,不能重新 fit,这是防止数据泄露的底线。参数说明:缺失率阈值 20% 是经验值,数据充足时收紧到 10%,数据紧张时放宽到 30%,但改动后都要在报告里写明,答辩时这就是“数据处理细节”。
4. 相关性预测模型选型与训练:从线性基线到梯度提升
4.1 先跑线性基线:一分钟验证特征是否有效
模型选型上,我强烈建议先跑一个岭回归基线。原因很简单:如果线性基线在验证集上的误差已经明显低于“预测零矩阵”或“预测历史平均相关矩阵”的误差,说明特征里包含真信息;如果线性基线和零模型差不多,后面再怎么堆模型都是徒劳。
岭回归的好处是自带正则化。相关矩阵上三角展开后维度很高,普通线性回归容易过拟合。alpha 参数从 1.0 开始,用验证集快速扫 [0.1, 1.0, 10.0] 三档就够了。
from sklearn.linear_model import Ridge from sklearn.metrics import mean_absolute_error model = Ridge(alpha=1.0) model.fit(X_train, y_train) y_pred = model.predict(X_val) # 只评估上三角,对角线不参与 mae = mean_absolute_error(y_val, y_pred) print(f'Ridge baseline MAE: {mae:.4f}')逻辑说明:Ridge 把上三角的每个位置当成独立的回归目标,训练时所有位置共享一套特征,拟合的是“特征到相关系数的全局映射”。参数说明:alpha 控制正则强度,过大模型趋于预测均值,过小则噪声拟合。实训数据量在几千样本时,alpha 在 1.0 到 10.0 之间通常都有稳定表现。
4.2 用 LightGBM 拟合相关矩阵上三角:多输出训练与早停
线性基线跑通后,换上 LightGBM。核心是把上三角位置当成多输出回归,用 sklearn 的 MultiOutputRegressor 包一层,或者直接循环每个位置单独训练。循环训练的缺点是慢,但好处是可以观察哪些基金对的误差大,答辩时能讲出故事:比如同类主题基金相关性误差小,跨资产类别误差大。
样本构建沿用 3.2 节的 make_samples。特征除了历史相关矩阵上三角,再拼上每只基金的波动率、动量、市场因子等统计特征。LightGBM 对缺失值和量纲不敏感,但标准化后的特征对线性模型友好,所以训练集还是统一走 StandardScaler。
from lightgbm import LGBMRegressor from sklearn.multioutput import MultiOutputRegressor # 每个上三角位置一个 LGBM 模型 base_model = LGBMRegressor( n_estimators=300, learning_rate=0.05, num_leaves=31, subsample=0.8, colsample_bytree=0.8, random_state=42, verbose=-1 ) model = MultiOutputRegressor(base_model, n_jobs=4) model.fit(X_train, y_train) y_pred = model.predict(X_val) mae = mean_absolute_error(y_val, y_pred) print(f'LGBM MAE: {mae:.4f}')逻辑说明:MultiOutputRegressor 对每个输出维度独立训练一个 LGBM,等于同时训练 N×(N-1)/2 个模型,n_jobs=4 并行能省不少时间。参数说明:n_estimators=300 配 learning_rate=0.05 是树模型最常见的组合;样本量小时加早停,样本量大时可加到 500 棵树;num_leaves=31 是保守设置,实训数据通常不足以支撑更大的叶子数。
训练完一定要看 feature_importance。如果历史相关矩阵上三角占了绝对主导,说明模型只是在“延续历史相关性”,对横截面新信息的利用不足;如果市场因子重要性很高,说明系统性风险主导了基金相关性,这个结论可以直接写进技术报告,答辩时比单纯贴损失曲线更有说服力。
4.3 模型输出的后处理:对称化、对角线回填与区间裁剪
模型预测的是上三角展开的向量,交给答辩演示前必须重组回完整矩阵,并做三个后处理:对称化、对角线回填、区间裁剪。
对称化是把上三角和下三角取平均,避免出现 A 对 B 相关 0.6、B 对 A 相关 0.4 的矛盾;对角线回填为 1,因为基金与自身相关性恒为 1;区间裁剪把预测值限制到 [-1, 1]。有些队伍还会做矩阵投影,将预测矩阵投影到半正定空间,但实训里做到前两步就足够。
def postprocess(upper_pred, n_funds): corr = np.eye(n_funds) idx = np.triu_indices_from(corr, k=1) corr[idx] = upper_pred corr[(idx[1], idx[0])] = upper_pred # 对称化 corr = np.clip(corr, -1, 1) # 区间裁剪 return corr逻辑说明:先初始化单位阵,再写上三角值,然后对称复制到下三角,最后 clip 保证输出在合法范围。注意:严格半正定投影需要 SVD 或特征值裁剪,如果答辩有人问,技术报告里可以注明“实训中保留原始预测,只用对称化和裁剪,避免过度平滑真实信号”。
4.4 结果评估与可视化:热力图对比是答辩的核心素材
指标算完,还要画图。最有效的可视化是“三张图并列”:真实相关矩阵热力图、预测相关矩阵热力图、逐元素绝对误差热力图。三张图放在一页 PPT 上,评委一眼就能看出模型在哪些基金对上学得准、在哪些对上学偏了。
误差可视化还可以加上时间维度:把测试集按时间切片,画出每个时间段的 MAE 曲线。如果 MAE 在市场大幅波动时显著上升,说明模型的横截面适应性还有提升空间,这也自然引出技术报告里的“未来改进方向”。这一步不是凑字数,是让整个项目从“调参跑数”升级成“有结论的实验报告”的关键素材。
import matplotlib.pyplot as plt import seaborn as sns # true_corr 与 pred_corr 都是 (N, N) 矩阵 fig, axes = plt.subplots(1, 3, figsize=(18, 5)) for ax, mat, title in zip( axes, [true_corr, pred_corr, np.abs(true_corr - pred_corr)], ['True Correlation', 'Predicted Correlation', 'Absolute Error'] ): sns.heatmap(mat, ax=ax, cmap='RdBu_r' if 'Error' not in title else 'YlOrRd', vmin=-1 if 'Error' not in title else 0, vmax=1 if 'Error' not in title else 0.3, cbar=True) ax.set_title(title) plt.tight_layout() plt.savefig('corr_compare.png', dpi=150)逻辑说明:误差热力图的色标固定在 0 到 0.3,而不是跟随数据自动缩放,否则视觉上全是深色,看不出差异。参数说明:vmin/vmax 的 0.3 上限是经验值,如果整体误差低于 0.1,可以缩到 0.15,让误差分布更明显。
5. 实训答辩避坑指南:源码、报告、PPT 最容易翻车的五个细节
5.1 数据泄露:验证集指标虚高,答辩被当场拆穿
现象:验证集 MAE 很低,一上测试集就崩,现场演示时指标对不上。原因:最常见的是对全体数据做了标准化或填充,再去切训练验证集,验证集的信息提前进了特征。解决:标准化和缺失填充都只允许在训练集上 fit,验证集和测试集只能 transform;代码里把 fit 和 transform 分开写,并在报告里注明。
5.2 标签窗口与特征窗口重叠:预测“未来”时偷偷看了答案
现象:模型指标好到不合理,比如 MAE 接近 0.01,相关系数几乎完全预测准确。原因:构造样本时,特征窗口和标签窗口有重叠,模型相当于用未来一段收益同时算特征和标签。解决:严格按 3.2 节的切片方式,特征窗口结束于 t,标签窗口开始于 t,并写一个单元测试断言两个窗口的日期集合交集为空。
5.3 随机种子与依赖版本不固定:换台机器结果对不上
现象:答辩前一天在教室电脑上复现,结果和报告里的表差 0.02。原因:LightGBM 和 sklearn 的版本不同,随机种子没固定,数值稳定性差异被放大。解决:在代码开头固定 random_state,并在 requirements.txt 里锁版本;技术报告里记录运行环境的 Python 版本和关键库版本,答辩现场如果复现失败,这份记录能帮你快速定位是库的问题还是数据的问题。
5.4 路径写死与绝对路径:评委一跑就报错
现象:评审拿到的源码压缩包在本地解压后直接运行,报 FileNotFoundError。原因:代码里写死了数据路径,比如 /home/xxx/fund_nav.csv,或者用相对路径但脚本所在目录不对。解决:用 Path 读取,路径统一基于当前文件目录定位:
from pathlib import Path BASE_DIR = Path(__file__).resolve().parent DATA_PATH = BASE_DIR / 'data' / 'fund_nav.csv'逻辑说明:这样压缩包解压到任何目录都能运行。参数说明:如果实训要求数据文件放在特定子目录,就在 README 里写清楚目录结构,而不是依赖某台机器的绝对路径。
5.5 热力图色标不统一:视觉上预测得很好,但数值一塌糊涂
现象:真实矩阵和预测矩阵的热力图配色完全不同,对比时视觉误差被掩盖。原因:画图时每个子图单独用 vmin/vmax 自动缩放,真实矩阵的色标是 -1 到 1,预测矩阵的色标也自动到 -1 到 1,但误差图自动缩放后颜色过于饱和。解决:统一三张图的色标范围,误差图的色标单独固定到 0 到 0.3 或其他合理区间,让视觉对比和数值误差保持一致的尺度。
6. 把技术报告写成加分项:一份能复用的实验记录模板
技术报告是实训答辩的重头戏,但很多人把它写成了“代码流程说明书”。我后来养成一个习惯:每次实验都按一张表记录,最后直接把表贴进报告,比用文字描述实验结果可靠得多。这张表长这样:
| 实验版本 | 特征组合 | 模型 | 验证集 MAE | 测试集 MAE | 备注 |
|---|---|---|---|---|---|
| v1.0 | 历史相关矩阵 | Ridge | 0.058 | 0.062 | 线性基线 |
| v2.0 | 历史相关+波动率+动量 | LGBM | 0.046 | 0.051 | 默认参数 |
| v2.1 | v2.0 + 市场因子 | LGBM | 0.043 | 0.048 | 消融添加 |
| v2.2 | v2.1 去掉历史相关特征 | LGBM | 0.051 | 0.056 | 验证历史相关的作用 |
这张表回答三个问题:每个模块到底有没有用、最终模型选哪个、超参数在哪一步定的。答辩时被问“你为什么用 LGBM 而不用 XGBoost”,你不需要讲原理,只需要说“我在同一份数据上对比过,LGBM 比 XGBoost 训练快一倍,验证集 MAE 低 0.002,报告的表里有记录”。这个回答比任何理论推导都稳。
另一个技巧是给每个特征组合一个版本号。v2.0、v2.1、v2.2 这种命名,配合实验记录里的时间戳和数据版本,基本上杜绝了“这个结果是怎么跑出来的”这类追问。我最初带学员做这个方向时,为了让测试集指标好看,反复调参,结果答辩被问“超参数在哪一步选的”答不上来。后来养成习惯:每次改参数就记录一条实验表,参数、数据窗口、指标一起记,答辩时把表往 PPT 上一放,可信度立刻不一样。
最后关于答辩 PPT,建议只放四页内容:任务定义与评估指标、数据预处理与标签构造、模型对比与消融实验表、热力图对比与误差分析。其他细节全部放进附录文档,评委感兴趣时再翻。源码包里这份文档全集,真正起到作用的是“能快速验证你的结论”的实验记录表,而不是大段文字描述。希望帮到你。
本文还有配套的精品资源,点击获取