公司动态
IJCAI-17客流预测赛题复盘:特征工程与验证策略详解
简介从机器学习回归任务的基本概念出发时间序列预测是其中一类兼具挑战与实用价值的场景。客流量、销量等连续值预测往往面临小样本、强周期、噪声多等难题。合理的特征工程能将日期、历史统计量转化为模型可理解的信息而严格的时序验证策略则是避免数据泄漏、保证泛化能力的关键。在IJCAI-17口碑商家客流量预测竞赛中通过构建滞后特征、滚动窗口统计、星期对齐等操作并结合LightGBM等梯度提升树模型与时间滑窗验证可有效提升RMSLE等指标。该赛题完整覆盖了从特征构造、模型选型到后处理的预测链路其经验可迁移至门店客流预估、商品销量预测等实际业务帮助从业者避开常见的特征泄漏陷阱建立稳健的预测流程。 在机器学习竞赛圈子里IJCAI-17 口碑商家客流量预测一直是我很推荐新手去复盘的一道题。赛题本身一句话就能说完给你一批匿名商户的画像信息和过去一段时间的客流数据让你预测它们未来每一天的客流量。听起来就是个普通的时间序列回归但实际上手才会发现里面塞满了小样本、强周期、节假日效应、零客流记录这些让人头疼的东西。我当年完整跑完一轮之后最大的感受是——这道题把机器学习预测模型从特征到验证的每一个环节都考了一遍。这篇文章就把我当时踩过的坑和验证下来有效的做法重新梳理一遍给正在做客流预测、销量预测或者任何带时间维度的回归任务的人一个参考。1. 赛题还原客流预测的边界比想象中窄1.1 官方给出的数据长什么样赛题提供的数据分为两类。第一类是商户静态信息表每条记录是一个商户字段包括商户ID、城市ID、商圈ID、类别ID。这里所有ID都做了匿名化处理你不知道城市ID 23 对应哪个城市也不知道类别ID 7 代表餐饮还是零售。第二类是客流记录表这是整个赛题的数据核心每一行是某个商户在某一天的客流量时间跨度大概是三个月左右商户规模在千级别。数据量不算大至少和动辄上亿样本的点击率预测相比算是非常克制。之所以先把这个数据规模说清楚是因为它决定了后续所有方案的天花板。如果是上亿样本你可以无脑堆深度学习模型但在这个赛题的体量下商户少、时间短模型能学到的有效规律非常有限特征工程和验证策略才是真正拉开差距的地方。另外官方没有提供天气、节假日、平台活动这些额外的外部变量所有外部影响都要靠日期本身去推测。1.2 评价指标里藏着的几个要求比赛用的评价指标是均方根对数误差全称 RMSLE公式长这样RMSLE sqrt( (1/n) * sum( (log(pred_i 1) - log(actual_i 1))^2 ) )这个指标有两个特点直接决定了建模策略。第一它对误差取了对数客流同样偏差10%客流100和客流10000在指标上等权。这个设定其实很符合商家经营的直觉你不会因为一个大商场的客流预测差了几百人就觉得模型不可用但一个小店预测值翻了一倍会显得很离谱。第二公式里对预测值和真实值都加了1本质上是照顾到客流量很低甚至为0的样本。这也提醒我0客流不能直接当成异常值扔掉它本身就是一种有效状态后面会专门讲这个点。1.3 为什么这个赛题值得反复复盘这个赛题的问题规模、数据噪声、外部影响因素复杂度都处在机器学习入门到进阶的最佳区间。它不是一个纯粹的表格回归因为时间先后顺序和信息泄漏问题就藏在数据里它也不是一个纯粹的时序预测因为商户的静态属性在决定客流基线上起着很大作用。把这道题跑通一遍相当于把机器学习预测模型的完整链路重新走了一遍数据探索、特征构造、模型选型、验证方案、结果融合、后处理每个环节都会踩到真实的坑。这也是我后来遇到时序类项目时第一反应就是回到这个赛题找方法论的原因。2. 特征工程匿名商户画像和历史客流能榨出什么2.1 商户静态信息的几种打开方式商户静态信息只有几个匿名ID最简单的做法是直接把ID当类别特征喂给模型。LightGBM这类模型支持直接传入类别特征不需要手动one-hot效果也不会太差。但这样做有一个隐患匿名ID没有语义而且类别数量可能很不均衡某些城市下可能只有一两个商户模型很容易把ID当成噪声去记而不是真正学到规律。我当时的做法分两层。第一层把城市、商圈、类别分别作为类别特征让树模型自己去切分。第二层基于这些ID构造统计特征比如每个城市下所有商户的平均客流、每个类别的客流标准差、同一个商圈内商户之间的客流稳定程度。统计特征的意义在于它把ID背后的“繁华程度”“业态差异”这些隐含信息提取出来让模型直接读结论而不是在ID值上硬找规律。还可以把城市和类别合并成一个新ID城市和商圈合并成另一个组合ID这能捕捉到“某城市里某类商户客流偏高”这一类的联合效应。2.2 历史客流能滚动出哪些特征历史客流是整个赛题的重头戏。每个商户在过去几十天里每天都在产生数据但直接把原始序列喂给GBDT是行不通的树模型不懂时间顺序它只会把连续数值当成可以任意切分的标量。所以必须把时间序列压缩成一组能反映客流水平的统计量。我从几个维度做了特征设计这里整理成一张表特征类型具体特征作用水平特征最近1天、3天、7天、14天、30天的平均客流刻画当前客流水平波动特征最近7天、14天客流标准差最大最小值刻画经营稳定性趋势特征近3天均值/近7天均值近7天均值/近14天均值捕捉客流上升或下降方向活跃特征历史零客流天数、最近非零客流距今天数处理间歇营业商户周期特征星期几、月份、距离节假日天数刻画周期性规律还有一个非常关键的特征是星期对齐。如果预测目标是未来某一天那么过去几个星期对应星期几的客流均值比最近7天的整体均值更有参考价值。比如要预测周三就把过去4个周三的客流取均值这个特征直接抓住了“每个商户在一周内不同天的固有差异”这个规律对周末型商户尤其有效。这里要特别强调一句所有滚动窗口都只允许使用当前预测日之前的数据。如果直接对全历史均值用 transform等于把未来信息也放进了当前特征验证分数会很漂亮但一上测试就会原形毕露。代码上做的第一件事就是先 shift(1)再去算滚动统计。2.3 星期效应和节假日怎么近似客流数据里最稳定的规律就是星期效应周末比工作日高对偏向休闲消费的商户尤其明显。这个规律不需要额外数据从日期里取 weekday 就能拿到。麻烦的是节假日训练数据覆盖的时间窗口里节假日数量很少模型很难从有限样本里学会“节假日客流会涨”这个规律。我当时的做法是构造一组距离特征今天的日期离最近节假日前第几天、后第几天、是否处于节假日区间。虽然节假日标签需要自己根据日期标注但在这种小样本赛题里给模型一点外部先验比让它自己从日期编号里硬学要靠谱得多。如果连标注都不想标也可以用另一个办法先看所有商户的客流总量曲线找到明显的异常高峰日再把这些日期单独作为一个特征标记出来。这个方案本质上是用数据本身反推节假日效应效果也够用。3. 建模路线对比GBDT、序列模型和融合的取舍3.1 先跑通一个绝不丢分的基线我上手第一版的原则是能多简单就多简单目的是确认数据链路没接错。当时写了一个极其朴素的模型用每个商户最近7天的平均客流作为未来每天的预测值。就这么一个模型RMSLE已经能压到很低的水平。这个数字的重要意义在于后面所有花哨操作都必须能稳定跑赢这个基线否则就是白费劲。很多人在做时序预测时会犯一个毛病一上来就上LSTM、GRU觉得深度学习才配做时间序列。但在这个赛题里因为星期效应太强了一个简单的周期均值模型已经吃掉了一大块确定性信息剩下的都是随机波动。先把基线做出来再去看模型到底在哪个部分有提升空间这是更高效的做法。3.2 LightGBM为主力的回归方案接下来把特征矩阵拼好用LightGBM做主力模型。目标变量不直接用客流而是先做 log1p 变换也就是对原始客流量取 log(flow 1)。这样模型在优化MSE时本质上就是在优化对数空间的误差和RMSLE的目标完全一致。推理时再用 expm1 还原预测出来的值天然大于等于0不会出现客流为负数这种荒唐结果。LightGBM的超参不需要过分调我用的是一套非常固定的参数就已经接近最终成绩叶子数31学习率0.05特征采样0.8样本采样0.8最大深度不限目标用回归训练600轮配early stopping。最后发现真正拉开分数差距的是特征够不够扎实而不是最后一组超参的微小差异。这也是这个赛题最值得学习的地方模型能力到一定程度之后就饱和了特征和验证才是真正决定上限的部分。3.3 LSTM和MLP在这个赛题里的表现我还专门试过用LSTM做序列预测思路是把每个商户的历史客流整理成固定长度的序列再用LSTM编码最后接全连接层输出未来某天的客流。实验做下来的结果很实在在验证集上LSTM的效果接近LightGBM但并没有明显优势。原因在于商户数量只有千级别序列长度有限模型能吃到的样本量撑不起复杂网络的学习能力。倒是在用了商户ID embedding之后模型对商户个体差异的建模能力有所增强但仍然没有达到能替代GBDT的程度。MLP也有类似的处境。把商户ID做成embedding再接几个隐藏层效果中规中矩。深度学习模型真正的价值出现在融合阶段它学到的表征和GBDT学到的特征切分方式差异很大融合之后往往能再榨出一点分数。所以在方案设计上可以以GBDT为主深度学习作为辅助和融合项而不是想着用深度学习来一锤定音。3.4 融合与后处理的实际收益融合我这里做得很简单把LightGBM、XGBoost、LSTM三个模型的预测结果取对数空间平均最后再expm1还原。平均方式用加权平均权重根据验证集上的RMSLE来确定整体比单一LightGBM能再降零点零几的误差。别看数字小在比赛排名里这一小步可能就跨越好几个名次。后处理有三件事必须做。第一预测值截断到0以上虽然log1p空间里模型很少输出负值但偶尔会出现一定不能放任。第二如果一个商户在历史数据里长期没有客流预测值应该往0压不能让它因为别的原因被带偏。第三如果对客流的量级有业务直觉比如某类商户日均客流不可能超过某个值可以在最后做一步软截断把极端预测拉回来。这些后处理操作看着不起眼但每一次都能让分数往正确的方向走一点。4. 验证与翻车排查时间序列预测最容易被分数骗4.1 随机K折为何不可信很多做表格回归的人会习惯性用随机K折但这一步在客流预测里是致命的。原因很简单同一个商户的时间序列既可能出现在训练集也可能出现在验证集模型在训练时已经接触过验证集时间范围内的数据分布验证分数就会虚高。我当时发现一个典型现象随机K折的RMSLE能到很低但到了真正要预测的未来时间段实际分数比验证结果差不少这就是典型的被数据泄漏骗了。更隐蔽的泄漏点在特征上。如果特征里包含全历史均值之类的统计量随机划分时验证集的真实值已经被模型在特征里看到过了这个分数完全失真。所以时序预测的验证方案必须从第一天就按时间序列的思路来做。4.2 时间滑窗验证的正确姿势正确做法是按照时间顺序做滑窗验证。假如训练数据覆盖100天先用前80天训练、后20天验证再把窗口整体前移多切几组不同的训练/验证组合。sklearn自带的TimeSeriesSplit就是干这个的它不会打乱样本顺序对时序回归任务非常合适。切分的时候有一个细节不能只把数据按行切片还要保证所有滚动窗口特征在切分点重新计算。简单说验证集里的特征只能用切分点之前的数据来算不能用整个训练集的信息。这一步不做好时间滑窗验证的效果也会被打折扣。我在4.3里讲一个真实踩过的坑就是这个问题的具体体现。4.3 一次真实的翻车全局均值特征泄漏我踩过的一个典型坑是构造“商户历史平均客流”这个静态特征时直接用了整个训练集所有日期的客流均值。在时间滑窗验证里这个特征看起来一切正常分数也不错。但我后来仔细一想发现这个均值里其实包含了验证集时间段的真实客流信息等于模型在看验证集样本时已经提前知道了结果的平均水平。这种情况在比赛里不算少见很多队伍线上分数和本地验证对不上排查到最后都出在特征泄漏上。解决的办法只有一条所有统计特征的求解窗口必须严格限制在训练部分的数据内验证时特征计算方式和测试时保持一致。如果某个特征放进去不放心就在验证脚本里单独打印出来检查一遍看它是否依赖了切分点之后的数据。4.4 按商户维度拆解误差只看整体RMSLE很容易错过真正的问题。我会把验证集上的误差按商户、按城市、按类别拆开来看看误差到底集中在哪些样本上。拆完之后问题往往很清晰误差大的通常是小客流商户因为它们的客流波动受单日随机性影响太大模型很难准确预测另外一类误差大的是那些客流忽高忽低、极不稳定的商户它们的模式本身就不规律换任何模型都很难救。针对小客流商户一个可行的策略是单独调低预测值或者用更保守的估计针对不稳定商户可以尝试在特征里加入更多波动特征让模型至少能感知到这个商户不可预测。误差诊断不是锦上添花它决定了下一步优化往哪个方向走。5. 实操复盘从特征矩阵到提交文件的完整链路5.1 特征矩阵构建的代码骨架这里给出我当时用的特征工程核心代码逻辑经过简化但关键点都在import pandas as pd import numpy as np # flow 字段: merchant_id, date, flow flow[date] pd.to_datetime(flow[date]) flow flow.sort_values([merchant_id, date]).reset_index(dropTrue) flow[weekday] flow[date].dt.weekday flow[doy] flow[date].dt.dayofyear # 每个商户的滞后特征shift(1) 保证只用历史信息 flow[lag1] flow.groupby(merchant_id)[flow].shift(1) # 最近7天平均客流窗口从预测日前一天开始 flow[rolling_mean_7] ( flow.groupby(merchant_id)[flow] .transform(lambda x: x.rolling(7, min_periods1).mean().shift(1)) ) # 最近7天客流标准差 flow[rolling_std_7] ( flow.groupby(merchant_id)[flow] .transform(lambda x: x.rolling(7, min_periods1).std().shift(1)) ) # 星期对齐特征过去所有同星期客流均值 flow[same_weekday_mean] flow.groupby( [merchant_id, weekday] )[flow].transform(lambda x: x.expanding().mean().shift(1))这里有个细节值得单独说明same_weekday_mean 用的是 expanding().mean().shift(1)而不是直接对这个星期列取均值。这样能保证预测周三时特征里只有过去所有周三的历史信息未来那个周三的真实值不会混进特征里。5.2 训练与预测的代码骨架训练部分我用 LightGBM 加 TimeSeriesSplit核心代码如下from sklearn.model_selection import TimeSeriesSplit import lightgbm as lgb feature_cols [weekday, doy, lag1, rolling_mean_7, rolling_std_7, same_weekday_mean, city_id, cate_id] train[label] np.log1p(train[flow]) tscv TimeSeriesSplit(n_splits4) models [] for train_idx, val_idx in tscv.split(train): tr train.iloc[train_idx] va train.iloc[val_idx] model lgb.LGBMRegressor( objectiveregression, n_estimators600, learning_rate0.05, num_leaves31, colsample_bytree0.8, subsample0.8, min_child_samples10, ) model.fit( tr[feature_cols], tr[label], eval_set[(va[feature_cols], va[label])], callbacks[lgb.early_stopping(50)] ) models.append(model) # 测试集预测先对数空间平均再还原 pred_log np.mean([m.predict(test[feature_cols]) for m in models], axis0) pred np.expm1(pred_log) test[pred_flow] np.where(pred 0, 0, pred)几个小点early stopping的验证集必须来自时间滑窗切分不能从训练集里随机抽多折模型在预测时取对数空间平均相当于给最终预测加了一层正则能稍微压住方差最后的负数截断千万不能省。5.3 提交前再检查一遍的清单提交前我会按下面这个清单快速过一遍能省掉很多低级失误特征列顺序和训练时完全一致类别特征没有丢。标签做过 log1p预测还原用 expm1方向不能搞反。提交文件是否包含所有测试商户和所有测试日期。预测值是否有负数有就截断成0。测试集特征是否有NaN如果有想好回填策略比如用同城市同类别的均值补。这个清单看着基础但越到比赛后期越容易在最后一刻因为这种小问题丢掉分数。养成提交前检查的习惯对以后做工程项目同样有好处。6. 从赛题到真实客流预测哪些经验还管用6.1 冷启动商户怎么办赛题里所有商户都有历史数据但真实场景里新店开张、新商户接入平台的情况非常常见这时候历史客流特征全部缺失。我的做法是用同城市、同类别的商户均值作为默认特征再随着商户产生新的数据逐步切换成商户自己的统计值。这个思路和赛题里对小样本商户的处理是相通的在数据量不足时用群体统计先顶上等个体数据积累到一定程度再慢慢让位给个体特征。6.2 模型重训与监控真实环境里客流分布会随着季节、周边建设、平台活动不断变化一个季度前训练好的模型很可能已经过时。比较靠谱的方案是按周做增量训练同时每天对比预测客流和真实客流计算滑动窗口内的RMSLE一旦连续几天超过警戒线就触发重训。模型上线不是终点持续监控和迭代才是保证预测效果稳定的关键。6.3 一点个人体会IJCAI-17 口碑商家客流量预测这个赛题从技术角度看现在可能不算前沿但它的价值在于足够完整。它逼着我把特征、验证、模型、后处理整条链路都过了一遍也让我真正理解了时间序列预测里“信息泄漏”这几个字的分量。如果让我重新做一次我会把更多时间花在画图上——每画出一张商户客流曲线你对特征的设计就会更准确。这个习惯我现在还在用也是每次做预测类项目时最底层的那个抓手。本文还有配套的精品资源点击获取