2020年,我干了件有点冲动的事——报名了fintech训练营的数据赛道。当时我的真实水平:SQL写不利索,Python只会拿来做爬虫,机器学习仅停留在某公开课看到一半,讲的还是线性回归。可就是这样一个零基础的小白,最后在这条赛道里拿到了一个还算体面的名次,也把“打boss”从玩笑变成了实打实的成长记录。这篇复盘,既是给那段日子留个存档,也是想让准备参加同类训练营或者数据竞赛、但还在犹豫“我行不行”的新手知道:这条路可以被复制,关键在于顺序、踩坑和复盘。
整个比赛过程对我来说很像闯关游戏:初赛是理解boss技能,复赛是跟排行榜这条大龙的数值硬碰硬,决赛答辩则是正面对抗评委的提问。每一关都在逼我从“能看懂教程”进化到“能独立做出一份可靠结果”。如果你也是数据领域的新手,希望借一场比赛快速成长,这篇文章里的赛制分析、特征工程思路、调参记录和答辩翻车经历,应该都能直接派上用场。
1. 为什么我一个零基础选手,敢报数据赛道
1.1 先把赛制摸清楚再决定要不要上场
报名前,我去翻了训练营往届的赛制说明,越看越觉得这个“数据赛道”是给新手留了一扇门。大多数这样的训练营会分好几个方向,有的偏产品方案,需要写商业分析报告、做路演,那种考验综合表达和资源整合;有的偏算法模型,对理论功底要求极高,上来就是论文复现级别的题目。但数据赛道一般是什么玩法?主办方提供一批脱敏后的结构化数据,参赛者要在限定时间内预测目标指标,然后提交预测结果,系统按统一指标打分排名。
这个形态意味着三件事。第一,评价标准透明,排行榜上的分数就能告诉你做得怎么样,不存在“评委主观喜好”的玄学;第二,数据是脱敏后的表格,不涉及真实业务系统对接,不需要你懂工程化部署;第三,绝大多数知识是公开的,pandas文档、各种开源库的官方示例、学长学姐的baseline分享,都能找到参考。说白了,这是一个“努力了就有反馈,方向对了就出分”的场子,很适合用来完成从零到一的跨越。
1.2 数据赛道三个吸引我的点
在犹豫期,我给自己列了一笔账,发现数据赛道有三个点对新手特别友好。
一是可解释。树模型的训练结果跑完能输出特征重要性,哪部分特征对预测最有帮助一清二楚,不像深度学习那样黑盒。作为小白,我能随时解释“模型为什么这么判断”,这在学习和答辩阶段都是救命属性。
二是有唯一答案。我不喜欢那种开放式命题,因为不知道往哪个方向使劲。数据赛道则明确告诉你,目标变量是二分类,指标是AUC,模型预测结果越准越好。目标足够具象,就可以拆成“数据处理、特征工程、模型调参、融合”几个子任务,逐个攻破。
三是复盘成本低。每次提交的结果都有线上分数,版本和版本之间可以直接对比。我做废了五个特征版本,哪一个拖后腿,看分数变化就能定位,这种清晰的正反馈对新手的成长速度太重要了。
1.3 我给自己的心理底线
当然,我也清楚自己有几斤几两。报名那天我给自己立了一条规矩:不指望拿名次,但必须完整走完整个比赛流程,从初赛提交到最终答辩,一个环节都不许退缩。
这个底线在后面救了我很多次。中间有段时间,我的线上分数卡在0.74左右,连续三天纹丝不动,差点就准备弃赛。但一想到“答应过自己要完整走完”,还是硬着头皮去看数据、翻文档、问队友,最后才在特征工程里找到突破口。所以如果你也是小白,不建议一上来就定“必须前三名”这种目标,而是定“完整跑完流程、搞懂每个环节”的过程性目标,boss战磨久了,数值自然会涨。
2. 赛前两周:把“能看懂”变成“能上手”
2.1 工具链:我用的是最朴素的组合
报名到正式开赛还有两周。我没有去啃大部头的机器学习教材,而是搭好环境,准备直接实战。这套组合到今天我也仍然会推荐给新手:
- Python 3.7 + conda 环境,避免依赖冲突;
- Jupyter Notebook,做快速探索和代码试错非常顺手;
- pandas、numpy 负责数据清洗和特征处理;
- missingno 用来快速查看缺失值分布;
- scikit-learn 提供各种评估工具和基础模型;
- LightGBM、XGBoost 作为主力建模工具。
当时有人问我为什么不用深度学习框架,我仔细想过这个问题。这个赛道的训练数据大概是几十万行规模,特征是几百列的结构化表格,这种数据场景下梯度提升树几乎总是从效率和效果上胜过深度学习。神经网络在图像、文本、语音这些非结构化数据上有绝对优势,但放在表格数据上,调参成本高、训练时间久、可解释性差,对新手来说是性价比最低的选择。这个判断后来被验证了——排行榜前排的多数方案,核心模型也还是树模型。
2.2 拿到数据后先做的十件事
开赛第一天,主办方在系统里放出了数据集。我给自己整理了一份“数据初探检查清单”,照着做就不会乱。这里直接分享我当时勾掉的十项:
- 读取数据,看 shape——训练集多少行多少列,测试集多少行多少列;
- 用 df.info() 看每一列的类型,区分数值列、类别列、时间列;
- 用 missingno 画缺失值热图,看缺失率高的列有哪些;
- 用 df.describe() 看数值列的分布、均值、标准差、极值;
- 检查目标变量的分布——正样本和负样本的比例;
- 检查是否有明显重复行;
- 检查ID列是否与预测目标存在直接关联(这一条后来帮我躲过一个大坑);
- 看时间字段的跨度,确定训练集和测试集在时间上的关系;
- 抽样几行数据,直观感受每一列的业务含义;
- 提前确认提交文件的格式要求,列名、顺序、是否要概率值。
这十件事看起来平平无奇,但每一步都在降低后续的不确定性。尤其第3和第5条,直接决定了我的数据处理策略——数据里缺失值不少,正样本比例也很不均衡,这些是后面所有特征工程的出发点。
2.3 第一次提交失败的血泪
现在回想第一次提交,简直想穿越回去按住自己的手。当时我跑通了一个最简单的逻辑回归baseline,本地算了一个AUC,感觉还行,就兴冲冲生成提交文件。结果线上直接报错“提交格式不正确”。
原因很蠢:我把训练集的索引直接当成ID列提交了,而测试集的ID列顺序和训练集并不完全一致。当时的系统要求是“ID列必须和测试集完全一致,概率保留小数点后6位”。我因为少看了格式说明,白白浪费了一次提交机会。有些比赛会限制每日提交次数,浪费一次就少一次试错空间。所以请大家务必把检查清单里第10项放在最重要的位置——先看提交要求,再生成结果,顺序不要搞反。
3. 数据清洗和特征工程:真正的分水岭
3.1 缺失值不是填空题,而是信号
最开始处理缺失值,我的思路很简单:哪个列有缺失,就用均值填上。直到一个深夜,我对比了两组实验,一组是均值填充,一组是保留缺失并用额外列记录“是否缺失”,后者线上AUC明显更高。我才意识到,缺失值本身可能携带信息。
举个直观的例子。在一个消费风控场景里,某列表示“用户最近一次贷款距离今天的天数”,如果这个值是空的,很可能说明该用户从未贷过款,这本身就是一种行为特征,而不是单纯的“未知”。均值填充会把这类用户硬生生往“有过贷款”的方向靠,等于抹掉了这个信号。我后来处理缺失值的三原则:
- 缺失率低的列,优先用基于时间或类别分组的统计量填充;
- 缺失率高的列,先判断是不是“含义为空”,如果是,就单独设一个“无此行为”的类别;
- 无论填不填,都额外生成一个二值特征“该字段是否缺失”,把缺失当成信息交给模型。
这一套做法,配合按时间分组统计填充,彻底改变了我的特征质量。
3.2 我如何从0造出接近200个新特征
赛题给的基础特征只有几十个,但建模时真正用到的是我自己构造出来的一大批新特征。整个造特征的过程,大概可以分成三类。
第一类是时间聚合特征。这是金融场景数据最值钱的部分。比如赛题里有一个“用户发生某操作的日期”字段,我不会只用它算一个“距今天数”,而是把行为按时间窗口拆开:近7天行为次数、近30天总金额、近90天最大单笔金额、行为间隔的平均值和标准差。为什么这么拆?因为业务上经常有“最近一个月突然活跃然后逾期”的规律,只有把时间窗口切出来,模型才能捕捉这种变化。
第二类是比例和频次特征。单看一个“总消费金额”意义有限,但“消费金额/收入”、“夜间操作次数/总操作次数”、“某类行为占比”就让数值有了业务含义。这类比例特征在实战中往往比原始数值特征更重要,因为它们天然剔除了用户体量的影响。
第三类是类别特征的频率编码。像“所在地区”、“设备类型”这种高基数类别列,直接用LabelEncoder会带进不存在的大小关系,用独热编码又会把特征维度撑爆。我的解法是统计每个类别的频次,把频次本身作为新特征。比如“这个地区在训练集中出现的次数”,其实很接近“该地区用户活跃度”,对模型很有价值。
到后面的版本,我手上已经有接近200个特征。但这里必须提醒一句:特征多≠分数高。我经历过一次教训,把所有造出来的特征一股脑丢进模型,AUC反而下降了0.01。后来用特征重要性排序和简单的相关性过滤,删掉低价值特征,分数才回升。特征工程是加法,但加法之后一定要做减法。
3.3 第一版模型的0.68:我犯的三个错
第一次跑出一个像样的模型,线上AUC是0.68。这个分数在排行榜上排到后三分之一,但比起直接放弃,我更庆幸它暴露了三个问题。
第一个错是数据泄漏。我差点把一个“目标变量”相关度极高的辅助字段当成普通特征用了。为什么会这样?因为我只看字段名字就开始造特征,没有细想这个字段的生成时间。做出的模型本地分数虚高到0.9,线上却只有0.68。排查后发现,这个字段必须在目标事件发生之后才能计算出来,放在特征里等于拿未来预测过去。后来我建立了一条规则:凡是“目标事件发生之后才有意义”的字段,一律不进特征。
第二个错是交叉验证切得太随意。我当时用普通的KFold随机切分数据,本地评估结果很乐观,但线下线上总差一截。后来发现,训练集和测试集在时间上是先后关系,随机切分会把未来数据混进训练折里,模型学到了不该学的时间窗口信息。解决办法很简单,改用按时间顺序切分的验证方式,让验证集始终晚于训练集。
第三个错是提交文件的对齐问题,前面已经写过。这个不涉及模型,纯粹是粗心。这三个错几乎每个新手都会遇到,写在前面,希望大家少交一点学费。
4. 调参与模型融合:把0.68提高到0.82的记录
4.1 为什么选择LightGBM而不是其他模型
线上分数到0.68之后,我正式进入调参阶段。第一件事是放弃逻辑回归,把重心放到LightGBM上。选择它的理由很直接:训练速度快、内存省、对特征缩放不敏感、自带处理缺失值的能力,还有非常灵活的早停机制。同一份数据,同样的交叉验证,LightGBM的训练时间是XGBoost的几分之一,迭代效率完全不在一个量级。
但我不是上来就瞎调参,而是先用默认参数跑两轮,确认它能到0.70以上,再开始看特征重要性。这里有一点心得:调参前先确认方向和数据的潜力,如果模型本身不行,调参只是浪费时间。我给自己算过一笔账:同一份特征,逻辑回归大概0.72,LightGBM默认参数能到0.75,说明树模型确实更适配这份表格数据,值得继续深入。
4.2 三次真正产生质变的调整
接下来从0.75到0.82,我做了大量尝试,但真正产生质变的只有三次。
第一次,换掉随机交叉验证。前面提到,我改用时间序列切分。具体做法是:按月份把训练集切成多段,每次用前几个月训练、后几个月验证,最后求平均。这一步直接让本地评估和线上分数的“走势”变得一致,之后所有实验的结论都是可靠的。二次调整有了可信的度量衡,后面才能谈提升。
第二次,去泄漏特征后重新做特征筛选。我保留了重要性排名前80个特征,砍掉了剩余一百多个低价值特征。砍完之后分数不但没掉,反而涨到0.78。这里有个反直觉的事:很多特征单独看跟目标变量有点相关性,但放到模型里只会增加噪声。特征数量不是越多越好,模型需要的是高信噪比信息。
第三次,针对树模型本身调整关键参数。当时我调的重点是这几个,表格可以直接抄:
| 参数 | 我的最终设置 | 调整逻辑 |
|---|---|---|
| learning_rate | 0.01 | 调低学习率,配合早停提升精度 |
| num_leaves | 63 | 逐步上调,但加了min_child_samples抑制过拟合 |
| min_child_samples | 50 | 限制叶子节点最少样本量,防止学噪声 |
| feature_fraction | 0.7 | 每棵树随机抽70%特征,增强泛化性 |
| bagging_fraction | 0.8 | 抽样训练行,同样为了降低方差 |
| max_depth | 8 | 限制树深度,减少过拟合风险 |
这三个组合拳打下来,线上分数从0.78涨到0.80左右。后面的0.02,主要靠模型融合拿到的。
4.3 Stacking融合的完整配置与两个防过拟合原则
单模型到0.80,再往上光靠调参已经很难。我开始尝试Stacking融合,这也是排行榜前排选手最常用的套路。我的方案是这样的:第一层放三个差异化的模型——参数不同的LightGBM、XGBoost、CatBoost,然后对每个模型做5折交叉验证,把每一折的验证集预测结果拼起来,形成一列“OOF预测概率”,作为第二层模型的特征。第二层用一个极简单的逻辑回归,把三个OOF特征拼起来做最终预测。
这里放一段我当时写的伪代码逻辑,方便理解:
# Stacking第二层伪代码 oof_lightgbm = run_5fold_cv(model_lightgbm, X_train, y_train) oof_xgboost = run_5fold_cv(model_xgboost, X_train, y_train) oof_catboost = run_5fold_cv(model_catboost, X_train, y_train) # 拼成第二层训练特征 stack_train = pandas.DataFrame({ "lgb": oof_lightgbm, "xgb": oof_xgboost, "cat": oof_catboost, "y": y_train }) # 第二层模型,用简单线性模型防止过拟合 meta_model = LogisticRegression() meta_model.fit(stack_train[["lgb", "xgb", "cat"]], y_train) # 测试集预测:每个模型单独预测,然后交给meta_model test_pred = meta_model.predict_proba(stack_test[["lgb", "xgb", "cat"]])[:, 1]这段代码能不能直接抄?可以,但有两个防过拟合的原则必须遵守。
第一,第二层模型一定不能太复杂。用逻辑回归或者简单岭回归就够了,如果用第二层再上LightGBM强模型,很容易把第一层OOF里的噪声也学进去,造成线上翻车。第二,三个第一层模型一定要“有分歧”。如果三个模型长得一模一样,融合等于白融合。我刻意让LightGBM强调数值特征、XGBoost用更多类别编码特征、CatBoost自带类别处理能力,让三方从不同视角看数据,融合效果才会好。
完成融合之后,线上分数稳定到了0.82上下,这也是我最终提交的成绩。回看整个升级过程,0.68到0.82看起来是数值变化,其实是三个维度的进步:数据理解深了、验证方法对了、模型形态丰富了。
5. 决赛答辩的20分钟:比分数更重要的东西
5.1 评委不关心你的分数,他们关心什么
初赛复赛拼分数,决赛却是另一套玩法。我原本以为,现场答辩就是把成绩单甩给评委看就行了。结果评委问的第一个问题就让我楞住了:“你这个特征体系里,如果只能保留五个特征,你会保留哪五个,为什么?”
那一刻我才意识到,评委要考察的不是“你调参调得多漂亮”,而是“你到底懂不懂你建的东西”。他们真正关心三件事:你的方法能不能解释,你的流程有没有严谨性,你的结论放到真实业务环境里是否站得住脚。分数在答辩环节只是一个起点,不是护身符。
后来我总结了评委的高频提问方向,基本逃不出这几个:
- 为什么选这个指标,而不是准确率?
- 训练集和测试集的时间窗口关系是什么?你怎么避免未来信息泄漏?
- 这个特征的业务含义是什么?它怎么帮助你解释用户行为?
- 如果不依赖竞赛提供的脱敏数据,你的方案在真实业务场景里要怎么调整?
这些问题的共同点是:全都在逼你从“背代码”走向“理解业务和数据”。这也是我后来觉得训练营最有价值的部分——分数是门槛,理解力才是天花板。
5.2 我被问住的问题和现在的解法
当时有一个问题,我到现在都记得。评委问:“你的特征工程里用了过去30天和90天的聚合特征,但测试集覆盖的时间段比训练集晚,如果用户行为规律在这两个时间段里发生漂移,你的模型会怎样?”
这个问题我答得磕磕巴巴,因为确实没想过“特征稳定性”。现在回看,应当这样拆解。第一,要提前统计每个特征在训练集和验证集上的均值、方差,如果发现某些特征在两个时间段分布差异巨大,说明它不稳定,应该谨慎使用。第二,可以通过时间分层验证来模拟漂移,把训练集拆成早期和晚期,用早期训练、晚期验证,观察分数下降幅度。第三,找出那些重要性高且稳定性差的特征,尝试做特征箱体的时间平滑。这些做法都是为了让模型在真实环境里少一些突发性翻车。
5.3 答辩PPT的四个层次
准备过程里,我慢慢把答辩PPT整理成了四个层次,这里也一并分享。第一层讲“我要解决什么问题”,把赛题背景和业务理解说清楚;第二层讲“我的整体方案链路”,从数据清洗、特征工程到模型选择和融合,画一条完整流水线;第三层讲“关键实验对比”,放一张表格,罗列baseline、加特征后、调参后、融合后的AUC变化,用数据证明每一步都有效;第四层讲“局限与改进方向”,主动说出方案的弱点和可能的延伸思路。
这四层讲下来,正好20分钟。我也因此明白,答辩的重点其实不是炫耀结果,而是展示思考过程。
6. 赛后三个月:从“竞技能手”到“工程新手”
6.1 比赛代码重构后的模样
比赛结束后,我一度觉得自己积累了不少经验。直到有一次和朋友一起回顾我的代码,打开那些全是长条cell的notebook,里面变量名叫df1、tmp、final_df_final,一瞬间有点脸红。比赛代码和工程代码之间的差距,大概是“能跑”和“可维护”之间的差距。
后来我花了大概三周时间,把整个比赛代码重构了一遍。按功能拆成模块:数据加载、特征工程、模型训练、预测提交,每个模块单独一个脚本,公共参数统一放在配置文件里,模型文件名带上版本号和AUC指标。重构之后最大的变化是,我可以随时回滚到“上一个效果不错的版本”,而不必在几十个变量名混乱的cell里挖地三尺。从比赛中形成的这个习惯,在后面的学习和工作中帮了我大忙。
6.2 给下一届新手的四周备赛路线
如果让我给下一届参加数据赛道的新手排一个备赛路线,四周足够。再说一次,这个节奏是我亲身试过之后觉得性价比最高的版本。
第一周,吃透规则和baseline。不要自己从头造轮子,先找一份写得清楚的baseline代码,跑通,再逐行理解。目标不是跑出高分,而是搞清楚一条完整流程的每一环。第二周,重点做特征工程。把时间聚合特征、比例特征、频率编码这三板斧用熟,每加一类特征就提交一次,用分数变化验证特征价值。第三周,调参与验证。换时间序列交叉验证,调整那几个核心树模型参数,学会用早停。第四周,融合与复盘。把三个差异化的模型做stacking,整理出一份方法和实验记录文档,为答辩做准备。
6.3 一个朴素但重要的建议
如果只留一条建议给下一届新手,我会说:先别急着调参,老老实实把数据的时间顺序和字段产生逻辑理清楚。这个比任何炫酷模型都值钱。训练营结束之后,我这个“小白”并没有一夜变成大神,但至少再拿到任何一张陌生表格时,我知道第一步不是跑模型,而是问清楚它从哪里来、每列代表什么、时间窗口怎么切。把这些搞明白的人,无论做什么数据集,都不会太差。