Kaggle Top 5 | 198只股票、200条数据的金融预测------BattleFin高分方案从零复现
前言 :一场比赛,训练集只有 200 条、测试集 310 条,却要同时预测 198 只股票的价格变化------这种"数据不够、目标太多"的反直觉困境,反而逼出了简单模型的威力。本文记录参考第 1 名(BreakfastPirate)和第 2 名(Sergey Yurgenson)的公开方案思路后,从零独立完成的高分复现过程。最终提交 Private Score 为 0.42426 ,与榜首 0.42239 非常接近。本文重点不是展示复杂模型,而是复盘小样本金融预测中数据理解、稳健建模和复现工程的重要性。
比赛链接:BattleFin Big Data Combine Forecasting Challenge
最终成绩
目录
- [Kaggle Top 5 | 198只股票、200条数据的金融预测------BattleFin高分方案从零复现](#Kaggle Top 5 | 198只股票、200条数据的金融预测——BattleFin高分方案从零复现)
- [1. 为什么要复现这场比赛?](#1. 为什么要复现这场比赛?)
- [2. 数据结构与预测目标](#2. 数据结构与预测目标)
- [3. 这场比赛的关键陷阱](#3. 这场比赛的关键陷阱)
- [4. 方法一:跨股票 Huber 回归](#4. 方法一:跨股票 Huber 回归)
- [5. 方法二:I146 情绪特征决策树桩](#5. 方法二:I146 情绪特征决策树桩)
- [6. 后处理:常数目标修复](#6. 后处理:常数目标修复)
- [7. 最终融合方案](#7. 最终融合方案)
- [8. 为什么仓库中保留预生成组件?](#8. 为什么仓库中保留预生成组件?)
- [9. 项目工程化整理](#9. 项目工程化整理)
- [10. 如何运行项目?](#10. 如何运行项目?)
- [11. 这次复现的收获](#11. 这次复现的收获)
- [12. 总结](#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 列特征:
text
198 个 O 列:O1-O198,表示 198 只股票的价格变化百分比
244 个 I 列:I1-I244,表示匿名信息或情绪相关特征
数据的时间结构可以用下面这张图来理解:
text
O1 O2 ... O198 | I1 I2 ... I244
Row 1 [前一日收盘基准,自身变化为 0]
Row 2 [盘中价格变化 %] [匿名特征]
...
Row 54 [盘中价格变化 %] [匿名特征]
Row 55 [1:55PM 最新价格] [最新匿名特征] ← 模型输入
│
↓ 预测未来约 2 小时
目标: 4:00PM 收盘变化 %
最后一行对应当天 1:55PM 的状态,预测目标是收盘前的后续价格变化。
一个容易忽略但非常重要的事实是:O 列不是绝对价格,而是相对于前一日收盘价的变化百分比。这使得不同股票之间具有一定可比性,也让横截面建模成为可能。
3. 这场比赛的关键陷阱
刚开始做这类任务时,你很自然会想到用 LightGBM、XGBoost、深度学习,或者做大量特征工程。但在这份数据上,复杂模型并不一定有效。
主要原因是训练样本太少。只有 200 个训练文件,却要预测 198 个目标。如果把问题直接当作普通监督学习,模型很容易记住训练集噪声,而不是学到稳定规律。
因此,这个任务的关键不是"模型越复杂越好",而是:
text
如何利用 198 只股票之间的横截面关系;
如何控制模型复杂度;
如何处理异常值和停牌/常数目标;
如何让最终提交文件可验证、可复现。
最终在我的复现和验证中,表现最稳定的方案非常朴素:一个跨股票 Huber 回归组件,加上一个基于 I146 特征的决策树桩组件,再做简单加权融合。一个关键点是:在这个任务上,"下一刻等于这一刻"这种"什么都不做"的朴素预测就能拿到约 0.425 的 Private Score。这意味着模型需要从噪声中找到超越随机游走的信号,而不是盲目追求拟合精度。
4. 方法一:跨股票 Huber 回归
第一个核心组件是跨股票 Huber 回归。
直觉是:同一交易日里,股票之间并不是完全独立的。某些股票可能受共同市场因子、行业联动或相似信息冲击影响。因此,可以用其他股票的变化来预测目标股票的未来变化。
具体做法如下:
text
1. 对 198 只股票计算训练集上的两两 Pearson 相关性矩阵(198×198);
2. 对每个目标股票,选出相关性绝对值最高的 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 决策树桩的逻辑非常简单:
text
1. 取每个样本最后一个时间步的 I146;
2. 枚举可能的分裂阈值;
3. 将训练样本分成 high / low 两组;
4. 观察两组目标变化的均值差异;
5. 在测试集上按同一阈值分组,预测增量乘以 0.3 收缩系数;
6. 得到 E_i146_stump_pp.csv。
它本质上是一个非常浅的决策树,只做一次二分。你可以把它想象成:你在每个时刻只问一个问题------"I146 高还是低?"高于某个阈值的一组和低于阈值的一组,在目标均值上存在可利用的差异。就这么简单。 虽然表达能力很弱,但这种弱表达能力正好限制了过拟合。
这个组件的价值不在于复杂,而在于稳定。它为最终融合提供了与 Huber 回归不同来源的信息:Huber 回归利用的是"邻里关系"(其他股票怎么动),I146 树桩利用的是"情绪方向"(匿名特征在说什么)。两者来源不同、互不重复,组合在一起才有增益,这就是集成的本质。
6. 后处理:常数目标修复
金融数据里存在一些目标在最后几个时间步几乎不变化的情况。这可能对应停牌、无交易或数据本身保持常数。
如果某只股票在最后几个时间步的标准差接近 0,那么继续让模型预测一个变化值,往往会引入额外噪声。更稳妥的方式是:直接把预测值设为最后一个已知值。
项目中的 post-processing 逻辑就是做这件事:
python
if last_6_steps_std < 1e-9:
prediction = last_known_value
这个步骤看似简单,但对最终分数有帮助。它体现了比赛中非常常见的一点:模型之外的数据规则和后处理,往往和模型本身一样重要。
7. 最终融合方案
最终提交由两个后处理后的组件加权融合得到:
python
M = 0.40 * E_robust_pp + 0.60 * E_i146_stump_pp
M = np.clip(M, -30, 30)
对应文件为:
text
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 可能受到环境影响,例如:
text
scikit-learn 版本;
BLAS / LAPACK 实现;
操作系统;
CPU 架构。
这些差异在单个预测值上可能很小,但在 310 × 198 的预测矩阵上会累积,最终可能导致提交文件和公开结果不完全一致。
因此,本项目采用两条复现路径:
text
路径 A:直接运行 verify_submission.py,验证最终融合公式和提交文件一致性;
路径 B:下载 Kaggle 原始数据后,运行 reproduce_ens_v2.py,复现完整训练流程和方法逻辑。
这样既能保证最终结果可验证,也能保留完整的建模过程。
9. 项目工程化整理
为了让项目适合公开发布,我对仓库做了以下整理:
text
1. 不上传 Kaggle 原始数据,只保留 data/raw/.gitkeep;
2. 使用 .gitignore 防止误提交 data/raw/*.csv、__pycache__、.pyc、venv 等文件;
3. 添加 MIT License;
4. 提供 verify_submission.py 验证最终融合文件;
5. 提供 REPRODUCE.md 说明不同复现路径;
6. 将方法分析和复现过程放入 docs/。
10. 如何运行项目?
安装依赖:
bash
pip install -r requirements.txt
如果只想验证最终提交文件:
bash
python verify_submission.py
如果想完整复现训练过程,需要先从 Kaggle 官方页面下载原始数据,并放入:
text
data/raw/
然后运行:
bash
python reproduce_ens_v2.py
需要注意的是,完整训练过程可能因为本地环境差异产生轻微数值偏差。因此,最终分数精确复现以仓库中的预生成组件和 verify_submission.py 为准。
11. 这次复现的收获
回顾整个过程,主要有四点收获。
第一,金融预测不能只依赖模型复杂度。小样本、高噪声、非平稳数据中,稳健性比表达能力更重要。
第二,横截面信息非常有价值。单只股票的时间序列很短,但同一时刻 198 只股票之间的关系提供了额外信息。
第三,单特征规则并不低级。I146 决策树桩虽然简单,但因为足够稳定,最终能和 Huber 回归形成互补。
第四,复现不是只跑出一个分数。真正完整的复现还包括数据说明、文件清理、许可证、结果验证、复现路径和公开表述。一个能公开展示的项目,必须让别人知道结果从哪里来、如何验证、哪些文件可以分发、哪些文件不能分发。
12. 总结
BattleFin 这场比赛说明了一个很有价值的事实:在小样本金融预测中,简单、稳健、结构清晰的模型往往比复杂模型更可靠。
最终方案并没有依赖深度学习或复杂集成,而是由两个轻量组件构成:
text
跨股票 Huber 回归:利用股票之间的横截面联动;
I146 决策树桩:利用匿名情绪特征的方向性信号;
后处理与裁剪:控制停牌/常数目标和极端预测值风险。
这类项目很适合作为金融机器学习、Kaggle 复现和面试展示案例。它不仅展示了建模能力,也展示了数据理解、实验验证和工程整理能力。
📢 声明:本文借助AI辅助工具进行资料整理与初稿生成,所有内容均经过作者本人的详细核对、修改与编排,文责自负。