特征工程:大力出奇迹,但别把力气用错地方
2019年秋天我接手过一个特别邪门的项目。一个电商平台的用户流失预测模型上线三个月AUC稳定在0.87不算惊艳但也够用了。突然有一天模型开始疯狂报警预测所有人都会流失。运维排查了一圈服务器没崩数据管道没断上游表也没改字段。最后定位到一个特征user_last_login_hour。这个特征从用户最近一次登录时间戳里提取出来原本取值范围是0到23。但那天所有用户的这个值都在12到14之间模型一看见这个分布当场就疯了。怎么回事呢数仓的ETL任务前一天晚上挂了早上才重新调度所有用户的最新登录时间都被刷成了补跑任务的时间——中午十二点多。一个看起来人畜无害的特征因为对上游数据生成机制毫不知情变成了定时炸弹。特征工程这件事网上的教程特别喜欢教你各种炫技多项式组合、目标编码、embedding、自动化特征构造工具。但干了这些年我最大的体会是一个模型的上限由特征决定但一个模型的下限也由特征决定。用力过猛或者用错地方比不用还糟糕。这篇不讲那些花里胡哨的就聊聊特征工程里那些让你半夜爬起来改代码的坑。一、信息泄露最隐蔽的作弊先说信息泄露这词听起来像网络安全问题其实是特征工程里最臭名昭著的错误。简单说就是你把不该知道的未来信息悄悄塞给了模型。我面试过一个人他在简历上写自己用XGBoost做过一个贷款违约预测AUC高达0.99。我当时就笑了问他“你用了哪些特征”他说把“逾期后催收次数”也放进去了。这就是典型的时间穿越。在真实放款场景里你不可能在放款前就知道一个人未来会被催收几次。这个特征本身就和目标变量是共生的放进去等于直接告诉模型答案。上线后这个模型的表现会断崖式下跌因为在真实预测时根本没有这个字段。更隐蔽的泄露长这样做用户购买预测你用了“用户最近一次购买金额”作为特征。看起来没问题对吧但你的训练集标注逻辑是“未来7天是否购买”。如果用户是在标注窗口内买的那这个“最近一次”很可能就发生在标注窗口里等于你把未来事件的特征拿过来预测未来。正确的做法是所有特征的时间戳必须严格早于标注时间起点。last_purchase_amount_before_label_start特征名里带个before是保命符。我的习惯是建一张特征时间点表每一个样本对应一个feature_cutoff_time所有特征的计算都用这个时间点之前的数据。哪怕多用一行WHERE event_time cutoff也比将来撕心裂肺强。有一次为了查一个泄露特征我逐列做了单特征AUC发现user_coupon_usage_future_7d的AUC高达0.96当场就抓出来毙了——那是把标注窗口内的领券行为当特征算了典型的搬起石头砸自己的脚。二、高基数类别特征别硬编码类别特征里的高基数问题教材上写得轻飘飘Label Encoding、One-Hot、Target Encoding。你拿到一个product_id字段里面有三十万个不同取值。One-Hot 直接炸内存Label Encoding 扔给树模型树模型会把0、1、2当成有序数值来切分虽然理论上树模型能处理但三十万个无序类别一棵树根本切不明白。Target Encoding 是个好方案用目标变量的均值来编码类别平滑一下更稳健。但很多人不知道这东西很容易过拟合尤其是小类别。比如某个产品只卖出去三次三次全退货编码值是1.0模型会死死记住这个产品一买就退。但实际上可能只是运气不好碰上了三个刁钻用户。我一般会给 target encoding 加个强力的平滑系数把全局均值拉进去兜底pythonencoded_value (n * mean_target m * global_mean) / (n m)m是平滑强度越大越向全局均值靠拢。小类别的编码值会缩回去不会极端。还有一种我特别爱用的方法就是把高基数类别降维成业务属性。product_id自己没啥信息但产品的一级类目、二级类目、品牌、价格带、上架时长这些衍生属性加进来信息量比原始ID还大。别迷信模型能自动学出来你给它一个ID它得需要海量样本才能把ID映射到隐含属性上不如你直接喂给它。把原始ID保留着让模型去抓长尾把业务属性展开给模型指方向两条腿走路。三、特征交叉不是越猛越好多项式特征、笛卡尔积交叉这些操作计算量是指数级的。一百个特征两两交叉能爆出几千列扔给逻辑回归跑半天最后L1正则化把99%的系数压成零你烧掉的GPU时间就换来一场空。真正有效的交叉往往来自业务直觉。比如一个打车平台你直觉上觉得“恶劣天气”和“早晚高峰”叠加会爆单那就单独构造一个is_rainy AND is_rush_hour的布尔特征一行代码的事。再比如电商里“用户浏览同类目次数”除以“用户总浏览次数”得到类目专注度这种比率特征往往比原始计数好用得多。一个做了五年运营转行数据分析的老哥教过我一个特征用户过去三个月优惠券使用率用券订单数除以总订单数加进流失模型里重要性排进了前五。他说“爱用券的人离不开你因为换个平台没这么高补贴”——这是坐在电脑前写代码的人想不出来的。但不是所有的业务直觉都对。我踩过一个坑直觉上“用户购买商品价格高于该商品历史均价”意味着用户可能冲动消费退货概率高。结果特征扔进去重要性几乎为零。查了一下才明白这个平台本身主打特卖用户早就习惯比价价格波动对决策影响极小。直觉需要被数据验证不要跟直觉谈恋爱。四、自动化特征构造的陷阱现在工具太多了Featuretools 一跑深度特征合成能自动生成几百个特征SUM(transactions.amount),MEAN(transactions.amount),STD(transactions.amount)……看起来很爽一把梭全扔进模型跑完看特征重要性取前五十。问题在哪可解释性灾难。你兴冲冲给业务方汇报“我们发现影响用户流失的关键特征是SUM(order_items after join with product_table).MEAN(price).STD。”业务方一脸茫然“所以这到底是啥意思我该做什么动作”你解释了半天他说“说人话”你翻译成“用户购买价格波动大的品类越多越容易流失”他反问“那是不是我们要稳定价格”你说不是他就不理你了。自动化构造出来的东西上线运维也是噩梦。这些特征依赖多表关联和复杂聚合数据管道稍微波动一下特征值全变模型直接沉默。排查起来你得顺着特征的名字反推计算逻辑有时候推半天还不如重写一个。我现在更倾向用自动化工具做探索阶段找灵感。比如 Featuretools 告诉我用户过去一个月的行为波动标准差这个方向有用我就自己手动写一个干净简洁的特征起个好懂的名字配上注释上下游都清楚模型跑起来稳稳当当。五、特征更新线上线下一一致性问题这是最容易被忽视的坑。训练时你的user_total_order_count是从数仓历史表里算出来的离线批处理数据完整。上线后这个特征要从实时特征平台取取的时候用户可能刚好下了一单数据库主从延迟还没同步过来特征值比实际小1。这个微小的差异会像雪球一样滚大尤其是依赖大量计数的模型。更隐蔽的是离线特征工程你用了pandas的mean默认跳过 NaN线上特征服务用 Java 写的求均值时遇到 null 可能直接返回 0 或者抛异常两边的处理逻辑没对齐特征分布直接偏移。我经历过一次严重事故原因是离线fillna(-1)的默认值线上代码漏了这一步所有缺失特征变成了 null模型在线打分全部乱套监控报警响了整整一夜。解决这个没有捷径只能靠规范。特征计算逻辑要写成统一的 DSL 或者配置文件离线在线都用同一套定义。实在做不到至少要有个特征校验的监控每天抽样对比离线在线特征值的分布KL 散度一飙升就报警。这个习惯帮我提前发现了至少三次线上特征管道的 bug。说到底特征工程是体力活也是艺术活。体力在于你得不断去看数据、查源头、写验证脚本艺术在于你得从业务故事里提炼出那一个关键的数字。那些花里胡哨的自动化工具和算法最后能不能落地取决于你对数据和业务的理解有多深。别急着用力先睁大眼睛看清楚这个特征配不配进我的模型