Copilot 写的预处理漏了训练集,我补完数据预处理课才揪出那行 fillna

📅 发布时间:2026/8/27 19:09:51
Copilot 写的预处理漏了训练集,我补完数据预处理课才揪出那行 fillna
Copilot 写的预处理漏了训练集,我补完数据预处理课才揪出那行 fillna上周三下午四点,运营总监在群里催客户流失预测模型 V2 的排期,我回了一句「这周五能上灰度」。底气来自两个新装的 AI 编程助手--Copilot 和CodeWhisperer,帮我把特征清洗、缺失值填充、标准化一口气生成了上百行代码。Amazon CodeWhisperer的上下文感知比预想中强,能自动识别我 pandas 里的列名,补全速度也很快。当时我觉得这波稳了,连午饭都没耽误。结果灰度跑完,市场部反馈预测的流失名单里有一大批根本没打过客服电话的客户,模型 AUC 只有 0.61。我以为是超参没调好,连着改了三天学习率、树深度,最后发现测试集上的 SHAP 值与训练集高度雷同--这是教科书级别的数据泄漏。源头在哪里?就藏在那行我根本没审的fillna里。后来我回头去学了机器学习基础课程里专门讲数据预处理的那一章,才知道这种全局均值填充如果作用在整个 DataFrame 上,等于把测试集的分布提前告诉了模型。这门课不会只讲概念,它会用生产案例演示一条机器学习管道要怎么设计才能杜绝信息穿越,学完之后我重建了公司三条数据流水线,每一条都带上了训练/测试隔离。Copilot 省了 2 小时,CodeWhisperer 补了边界开始写 V2 的时候,老板的要求就一句:把近 90 天的行为特征、客服工单、支付记录揉进一个模型,预测 30 天内流失概率。我手上有大约 18 万条用户数据,原始表有 240 多个字段。以往这种活我最起码要花一天半做数据预处理,但这次我直接把需求写成注释,让 Copilot 生成函数体,再让CodeWhisperer补完全部分支。Amazon CodeWhisperer在补完代码时会给出安全扫描建议,这个功能在写 SQL 拼接时防了一次注入,让我对它多了几分信任。不过涉及到特征工程的时候,两个工具的策略完全不同:Copilot 倾向于用均值/中位数填充,CodeWhisperer会多给一个众数填充的分支。我当时选了前者,因为代码更短。# 错误的预处理脚本(Copilot 生成的片段) df[last_payment_gap] df[last_payment_gap].fillna(df[last_payment_gap].mean()) df[ticket_count] df[ticket_count].fillna(df[ticket_count].median()) # 这行把整张表的均值直接填进去了,训练和测试还没分开写完这版代码我只用了不到两小时,顺手跑了一下本地验证,accuracy 0.87,以为可以交差了。但后来翻机器学习管道最佳实践的时候才明白,这种写法在 train_test_split 之前执行,等于把测试集的统计量混进了训练集,模型当然会过拟合。灰度那天,AUC 只有 0.61灰度环境把模型搭好后,我们用最近一周没参与训练的真实数据做回溯验证。预测结果一出来,业务方直接截图到群里:近半数高流失评分客户是刚续费年费的优质用户。AUC 0.61,比随机猜好不了几个点。我第一时间怀疑的是过拟合。调参三天,把 XGBoost 的 max_depth 从 6 砍到 3,加 L2 正则,甚至换了 LightGBM,测试集上的提升微乎其微。这时候同事提醒我:“你数据预处理是不是在切分之前做的?”我开始翻代码,发现全局fillna确实在train_test_split前面执行了。当时脑袋嗡了一下--这不就是入门课上反复警告的泄漏陷阱吗?在机器学习基础那门课里,有一个用信用卡欺诈数据做的实验,演示了全局填充会让模型在测试集上的 AUC 虚高,一到生产环境就失效。我当时觉得那是给新手看的,没想到自己栽在同一块石头上。那行 fillna 怎么把测试集污染了我回头对照AWS机器学习课程里关于数据预处理的内容画了一张数据流图。简单说,df[col].mean()计算了整张表的均值,如果表里已经包含了未来要用作测试的那部分数据,测试集的特征就掺进了训练集的统计量里。模型在训练时看到的是“加工过的”测试信息,预测新数据时却没有这个优势,效果自然崩塌。真正安全的做法是在切分后,只用训练集算出填充值,然后分别应用到训练和测试集。Scikit-learn 的Pipeline可以强行把 fit 和 transform 隔离,避免这种愚蠢错误。from sklearn.pipeline import Pipeline from sklearn.impute import SimpleImputer from sklearn.preprocessing import StandardScaler preprocessor Pipeline([ (imputer, SimpleImputer(strategymedian)), # 只在训练集上fit (scaler, StandardScaler()) ]) # 先切分 train, test train_test_split(df, test_size0.2) X_train preprocessor.fit_transform(train[features]) X_test preprocessor.transform(test[features]) # 直接用训练集学到的参数这套写法我在机器学习入门课程里见过,但之前觉得太啰嗦,总想用几行 pandas 搞定。吃了亏才明白,机器学习管道强制分离 fit/transform 的设计就是为了防呆。补数据预处理课,才找到全部泄漏点泄漏源头不止一个fillna。我用AWS机器学习提供的检查清单复查了一遍,发现还有一个StandardScaler也是在切分前对全量数据做的。更隐蔽的是,我在构造滑动窗口特征时,把未来时间窗口的统计量也打了进去,等于让模型“看”到了答案。我在机器学习基础那门课里跟着实验一步一步做,才学会用时间序列交叉验证来代替随机切分,并且把数据预处理的逻辑封装成可复用的 Transformer。课程里还提到,对于高基数类别变量,如果用目标编码,一定要在交叉验证框架下做,否则同样会泄漏。这些知识点如果不是系统学一遍,光靠CodeWhisperer或 Copilot 生成的代码根本看不出来。Amazon CodeWhisperer在我后来重构时又帮了大忙。我让它生成一个基于训练集折叠统计的函数,它给出的代码自动带了pd.merge时加validatem:1的参数,防止多对多爆炸,这种细节在CodeWhisperer的安全建议里会被标黄提醒。同样一段需求,Copilot 没有做这个校验。# 用 CodeWhisperer 命令行检查预处理脚本的潜在泄漏 grep -n fillna.*mean\|StandardScaler.*fit preprocess_pipeline.py # 结合 AWS 课程里的泄漏清单逐行复核用 Pipeline 重写后,模型翻盘花了一个周末把整个数据预处理逻辑迁移到 Scikit-learnPipeline里,并严格按照课程里的步骤做了两次验证: 1. 用cross_val_score在训练集上跑 5 折交叉验证,确保 AUC 标准差小于 0.02。 2. 在留出的最近一周时间窗口数据上做回溯测试,模拟真实上线环境。重跑后的模型 AUC 从 0.61 提到了 0.84,并且业务方抽查 Top 50 高流失评分客户,有 42 个确实是近三个月投诉量大、支付中断的用户。老板在周会上提了一嘴“这次特征下得对”,我没好意思说其实是机器学习基础那门课里的一页泄漏清单救了我。之后我把这段 Pipeline 打包成自定义容器,推到了 SageMaker 上跑批量推理,深度学习入门课程里的多 GPU 数据加载部分也给了我启发--虽然这次用的是 XGBoost,但未来要把文本类特征加进来,怎么在数据预处理阶段把 tokenizer 集成进 Pipeline,我心里已经有底了。给同样踩坑的人几条可执行建议不管多赶,数据预处理的fit和transform必须隔离,用 Scikit-learnPipeline或等效机制强制约束。AI 编程助手生成的特征填充代码默认不做训练/测试分离,务必把fillna挪到切分之后。如果你在用CodeWhisperer,可以打开安全扫描,它会标记潜在的统计泄漏模式。系统学一遍机器学习基础或机器学习入门课程里关于机器学习管道和特征工程的章节,里面不止讲算法,更有一整套防止线上翻车的设计模式。时间序列问题一定要用时间切分而非随机切分,AWS机器学习课程里有一个完整的财务预警案例,直接告诉你错用随机切分会导致什么样的假高准确率。如果特征计算涉及滑动窗口、群体聚合,把聚合逻辑也写成 Transformer,并在数据预处理阶段就完成,后期模型迭代时可以复用,省去重复排查泄漏的成本。别忽略深度学习入门里的 Dataset 设计,即使你现在只用传统模型,将来要把 TensorFlow 或 PyTorch 的 DataLoader 和业务逻辑对齐时,你一定会感谢当初认真做了数据预处理的工程化封装。最后,不管 Copilot 还是Amazon CodeWhisperer,它们都是加速器,但数据预处理的安全边界必须自己兜底。每加一个 AI 工具,就同步加一份审查清单,这份清单你在机器学习基础课程里能找到现成的。