Kaggle Top 5 | 198只股票、200条数据的金融预测——BattleFin高分方案从零复现 最终成绩目录Kaggle Top 5 | 198只股票、200条数据的金融预测——BattleFin高分方案从零复现1. 为什么要复现这场比赛2. 数据结构与预测目标3. 这场比赛的关键陷阱4. 方法一跨股票 Huber 回归5. 方法二I146 情绪特征决策树桩6. 后处理常数目标修复7. 最终融合方案8. 为什么仓库中保留预生成组件9. 项目工程化整理10. 如何运行项目11. 这次复现的收获12. 总结1. 为什么要复现这场比赛BattleFin Big Data Combine Forecasting Challenge 是 Kaggle 上一场金融预测比赛。比赛任务是根据盘中价格变化和匿名信息特征预测多只股票未来短时间内的价格变化。这场比赛虽然时间较早但很适合作为金融机器学习项目练习原因有三点第一数据规模很小。训练样本只有 200 个交易日级别文件但每个文件包含 198 只股票和多个时间步这会迫使我们认真思考过拟合问题。第二特征是匿名的。输入特征包括O1-O198和I1-I244其中 O 可以理解为股票价格变化I 是匿名信息特征。由于缺少明确业务字段名模型表现很依赖数据探索和特征理解。第三目标是多输出预测。每个测试样本需要同时预测 198 只股票的未来变化这比单目标回归更考验对数据结构的把握。这次复现给我的最大启发是在小样本金融预测中复杂模型未必占优。稳定、简单、可解释的模型反而可能比大规模堆模型更可靠。2. 数据结构与预测目标比赛数据包含 510 个 CSV 文件其中 200 个为训练样本310 个为测试样本。每个文件大致可以看作某一天的盘中截面数据。每个 CSV 文件包含 55 行时间步和 442 列特征198 个 O 列O1-O198表示 198 只股票的价格变化百分比244 个 I 列I1-I244表示匿名信息或情绪相关特征数据的时间结构可以用下面这张图来理解O1 O2 ... O198 | I1 I2 ... I244Row 1 [前一日收盘基准自身变化为 0]Row 2 [盘中价格变化 %] [匿名特征]...Row 54 [盘中价格变化 %] [匿名特征]Row 55 [1:55PM 最新价格] [最新匿名特征] ← 模型输入│↓ 预测未来约 2 小时目标: 4:00PM 收盘变化 %最后一行对应当天 1:55PM 的状态预测目标是收盘前的后续价格变化。一个容易忽略但非常重要的事实是O 列不是绝对价格而是相对于前一日收盘价的变化百分比。这使得不同股票之间具有一定可比性也让横截面建模成为可能。3. 这场比赛的关键陷阱刚开始做这类任务时你很自然会想到用 LightGBM、XGBoost、深度学习或者做大量特征工程。但在这份数据上复杂模型并不一定有效。主要原因是训练样本太少。只有 200 个训练文件却要预测 198 个目标。如果把问题直接当作普通监督学习模型很容易记住训练集噪声而不是学到稳定规律。因此这个任务的关键不是模型越复杂越好而是如何利用 198 只股票之间的横截面关系如何控制模型复杂度如何处理异常值和停牌/常数目标如何让最终提交文件可验证、可复现。最终在我的复现和验证中表现最稳定的方案非常朴素一个跨股票 Huber 回归组件加上一个基于 I146 特征的决策树桩组件再做简单加权融合。一个关键点是在这个任务上下一刻等于这一刻这种什么都不做的朴素预测就能拿到约 0.425 的 Private Score。这意味着模型需要从噪声中找到超越随机游走的信号而不是盲目追求拟合精度。4. 方法一跨股票 Huber 回归第一个核心组件是跨股票 Huber 回归。直觉是同一交易日里股票之间并不是完全独立的。某些股票可能受共同市场因子、行业联动或相似信息冲击影响。因此可以用其他股票的变化来预测目标股票的未来变化。具体做法如下1. 对 198 只股票计算训练集上的两两 Pearson 相关性矩阵198×1982. 对每个目标股票选出相关性绝对值最高的 top-20 候选股票3. 过滤 6σ 极端离群值对每个候选股票单独跑 HuberRegressor 做 t 检验4. 保留 p 0.04 的候选选 p 值最小的 2 个作为最终预测变量5. 用这 2 个变量做多变量 Huber 回归拟合预测增量乘以 0.5 收缩系数6. 得到组件 E_robust_pp.csv。为什么使用 Huber 回归金融数据中异常点很多普通线性回归容易被极端值带偏。你可以这样理解普通最小二乘回归像直接取所有评委打分的算术平均——只要有一个评委打了离谱的 0 分或 100 分整个均分就被带跑了。Huber 回归则像专业赛事评分——去掉一个最高分去掉一个最低分对极端打分自动降权不让少数离谱值绑架整体判断。因此更适合这种噪声较重的小样本金融数据。这个方法体现了一个重要思想在小样本下与其堆复杂模型不如先找到稳定的结构性信息。这里的结构性信息就是股票之间的横截面联动——不要只盯着一只股票的过去价格更要看看它周围 197 个邻居现在情况如何。5. 方法二I146 情绪特征决策树桩第二个核心组件只使用一个特征I146。这听起来很反直觉。数据里有 244 个 I 特征为什么最终选择一个单独特征原因是在小样本任务中多特征模型很容易过拟合而一个稳定的单特征规则有时反而泛化更好。I146 决策树桩的逻辑非常简单1. 取每个样本最后一个时间步的 I1462. 枚举可能的分裂阈值3. 将训练样本分成 high / low 两组4. 观察两组目标变化的均值差异5. 在测试集上按同一阈值分组预测增量乘以 0.3 收缩系数6. 得到 E_i146_stump_pp.csv。它本质上是一个非常浅的决策树只做一次二分。你可以把它想象成你在每个时刻只问一个问题——I146 高还是低高于某个阈值的一组和低于阈值的一组在目标均值上存在可利用的差异。就这么简单。虽然表达能力很弱但这种弱表达能力正好限制了过拟合。这个组件的价值不在于复杂而在于稳定。它为最终融合提供了与 Huber 回归不同来源的信息Huber 回归利用的是邻里关系其他股票怎么动I146 树桩利用的是情绪方向匿名特征在说什么。两者来源不同、互不重复组合在一起才有增益这就是集成的本质。6. 后处理常数目标修复金融数据里存在一些目标在最后几个时间步几乎不变化的情况。这可能对应停牌、无交易或数据本身保持常数。如果某只股票在最后几个时间步的标准差接近 0那么继续让模型预测一个变化值往往会引入额外噪声。更稳妥的方式是直接把预测值设为最后一个已知值。项目中的 post-processing 逻辑就是做这件事if last_6_steps_std 1e-9:prediction last_known_value这个步骤看似简单但对最终分数有帮助。它体现了比赛中非常常见的一点模型之外的数据规则和后处理往往和模型本身一样重要。7. 最终融合方案最终提交由两个后处理后的组件加权融合得到M 0.40 * E_robust_pp 0.60 * E_i146_stump_ppM np.clip(M, -30, 30)对应文件为E_robust_pp.csv # 跨股票 Huber 回归组件E_i146_stump_pp.csv # I146 决策树桩组件M_r40_i60_c30.csv # 最终提交文件其中clip(-30, 30)的作用可以继续用评委打分的类比来理解Huber 回归是去掉最高分和最低分——降低极端值的权重而 clip 是打分规则本身规定 0-10 分——不管模型算出什么离谱数字输出强制框死在 ±30% 以内。一支股票一天涨跌超过 30% 极其罕见所以把预测截断在这个范围里既能防住 ±500% 这种荒谬输出拖垮 RMSE又不会伤害正常预测。最终提交文件的 Private Score 为0.42426。这个结果与榜首0.42239非常接近说明轻量级模型在该任务上已经具有很强竞争力。8. 为什么仓库中保留预生成组件这是复现项目中特别需要说明的一点。仓库中提供了E_robust_pp.csv和E_i146_stump_pp.csv这两个预生成组件为了保证最终提交文件可以被bit-for-bit 精确验证。原因在于sklearn.linear_model.HuberRegressor的底层 solver 可能受到环境影响例如scikit-learn 版本BLAS / LAPACK 实现操作系统CPU 架构。这些差异在单个预测值上可能很小但在 310 × 198 的预测矩阵上会累积最终可能导致提交文件和公开结果不完全一致。因此本项目采用两条复现路径路径 A直接运行 verify_submission.py验证最终融合公式和提交文件一致性路径 B下载 Kaggle 原始数据后运行 reproduce_ens_v2.py复现完整训练流程和方法逻辑。这样既能保证最终结果可验证也能保留完整的建模过程。9. 项目工程化整理为了让项目适合公开发布我对仓库做了以下整理1. 不上传 Kaggle 原始数据只保留 data/raw/.gitkeep2. 使用 .gitignore 防止误提交 data/raw/*.csv、__pycache__、.pyc、venv 等文件3. 添加 MIT License4. 提供 verify_submission.py 验证最终融合文件5. 提供 REPRODUCE.md 说明不同复现路径6. 将方法分析和复现过程放入 docs/。10. 如何运行项目安装依赖pip install -r requirements.txt如果只想验证最终提交文件python verify_submission.py如果想完整复现训练过程需要先从 Kaggle 官方页面下载原始数据并放入data/raw/然后运行python reproduce_ens_v2.py需要注意的是完整训练过程可能因为本地环境差异产生轻微数值偏差。因此最终分数