
简介阿里天池新人赛移动推荐试玩工程定位为机器学习竞赛入门项目面向刚接触天池或推荐场景的开发者。包内以用户行为预测为目标提供完整的Python实现包含数据预分析、特征构造、模型训练与预测等环节并专门设有一份Spark版本代码便于对比单机与分布式处理流程。压缩包共26个文件以Python脚本为主辅以说明文档、特征列表与配置文件整体仅138KB结构精简清晰。目前已有114人学习浏览。这份资源不仅能帮助新手理解竞赛项目的目录组织还能通过可实际运行的代码掌握移动推荐场景从数据处理到模型比对的完整路径对参加天池新人赛或入门推荐算法都很有参考价值建议从数据预分析开始逐步读起再对照特征构造与模型训练代码形成完整认知闭环。1. 天池新人赛试玩移动推荐下载 zip 只是第一步第一次看到“阿里天池竞赛新人赛试玩_移动推荐.zip”这个文件时很多人以为解压出来就能直接跑模型结果发现里面是一堆用户对商品的点击、收藏、加购、购买记录。这个赛题的目标很直白根据过去若干天用户的行为日志预测用户接下来一天会不会购买某个商品。对初学者来说它比图像分类更有意思的地方在于数据是结构化的行为序列但正样本稀疏负采样和特征工程比堆模型重要得多。我整理了一套从解压 zip 到提交结果完整的试玩路子顺着往下走新手也能在本地调出一版有意义的提交。2. 解压 zip 后先搞清楚 user、item、行为表怎么关联拿到压缩包后我习惯先把它放到一个干净的目录里用 unzip 命令解压。常见的数据集包含用户、商品、行为日志和评价列表四个文件命名和列名在不同赛题轮次会有点差异但核心逻辑一致。解压之后先别忙于看数据先核对列名和主键关系否则后续 join 很容易产生笛卡尔积。如果你在解压时遇到invalid zip archive或could not find eocd之类的报错优先检查文件是否下载完整用 7-Zip 重新解压就能解决这个问题在 zip 数据集上出现得比想象中频繁。2.1 数据文件字段含义和连接键以我这次试玩拿到的文件为例里面通常有 user.csv、item.csv、user_behavior.csv、evaluation.csv 这四个文件。user 和 item 是静态维度表behavior 是核心行为日志表。具体字段如下文件字段含义user.csvuser_id用户唯一标识user.csvage_range年龄段离散值user.csvgender性别离散值item.csvitem_id商品唯一标识item.csvitem_category商品类目user_behavior.csvuser_id行为用户user_behavior.csvitem_id行为商品user_behavior.csvbehavior_type1点击 2收藏 3加购 4购买user_behavior.csvvisit_date行为发生日期格式为 20141201evaluation.csvuser_id待预测用户evaluation.csvitem_id待预测商品evaluation.csvprobability提交时填写的购买概率behavior 表通过 user_id 关联 user 表通过 item_id 关联 item 表。evaluation 表是要预测的user, item对不是让你自由生成候选集这一点与推荐系统的召回不同。我见过不少新人把 evaluation 表当成测试集直接读入发现它只有 user_id 和 item_id没有标签。真正的标签在线上本地只能用后几天的行为构造验证集。提示evaluation 表里的候选对是有业务含义的它们通常来自用户有过点击或加购行为的商品。因此不要对全部商品做预测这会引入大量没有行为支持的噪声导致提交分数异常。2.2 用 pandas 快速检查数据分布解压后第一件事是用 pandas 读入数据确认行数和缺失值。这里要注意日期字段天池移动推荐的数据日期通常是从 20141201 到 20141218其中 12.18 是待预测日12.01-12.17 是训练行为。我一般会写个几十行的检查脚本import pandas as pd behavior pd.read_csv(user_behavior.csv) print(behavior.shape) print(behavior.head(3)) print(behavior.dtypes) print(behavior[visit_date].min(), behavior[visit_date].max()) print(behavior[behavior_type].value_counts())输出里能直接看到四类行为数量级点击量通常比购买量大两个数量级。如果发现 visit_date 读出来是 int 类型建议统一转成字符串格式方便后续按日期切片behavior[visit_date] behavior[visit_date].astype(str)这里把日期转字符串是为了能用字符串比较直接筛选日期范围比如behavior[behavior[visit_date] 20141210]。如果你使用 pandas 的 datetime 类型当然也行但天池这个赛题的数据量不大字符串切割更快而且不容易踩时区相关的坑。接下来检查 user 和 item 表是否有重复主键print(user[user_id].nunique(), len(user)) print(item[item_id].nunique(), len(item))如果 user_id 数量小于 len(user)说明有重复行需要去重。实测中 user.csv 和 item.csv 通常没有重复但 behavior 表由于一个用户对同一商品会产生多次点击所以不去重。这个去重逻辑会在后面构造训练样本时再次用到建议这里先想清楚。2.3 行为序列的活跃度与稀疏度统计移动推荐的行为数据天然具有长尾分布。少数活跃用户贡献了大量点击而大部分用户只有零星几条行为。为了让后续特征工程更有针对性我通常会在检查阶段统计两个维度的分布每个用户的行为天数以及每个商品被多少用户行为过。这两个指标能直观反映数据的稀疏程度也能帮助判断负采样时是否会出现“无行为用户”。daily_users behavior.groupby(user_id)[visit_date].nunique() print(daily_users.describe()) item_user_count behavior.groupby(item_id)[user_id].nunique() print(item_user_count.describe())从 describe 结果里可以看到大部分用户活跃天数集中在 1~2 天商品的平均生效用户数也很低。这种情况下如果直接使用全局统计特征长尾部分的估计会很不可靠。一个常见的处理方式是对高频统计量做平滑比如把点击率改为(点击次数 5) / (浏览次数 10)但这会引入额外的超参数。对于新人赛先用归一化的原始统计量跑通基线后面再考虑平滑。2.4 用 merge 前的基数检查防止笛卡尔积在特征工程阶段我们需要把用户特征、商品特征合并到行为表上。这里最经典的错误是没有先验证连接键的单一性导致 merge 后行数爆炸。我的策略是在每次 merge 之前都检查右表连接键是否唯一user_feats user_feats.drop_duplicates(user_id) item_feats item_feats.drop_duplicates(item_id) merged behavior.merge(user_feats, onuser_id, howleft) print(merged.shape, behavior.shape)如果 merge 后的行数不等于 behavior 的行数说明 user_feats 或 item_feats 存在重复键。重复键可能是数据本身有重复也可能是特征聚合时没有按连接键分组。这个检查虽然简单但能省下大量排错时间。移动推荐赛题的数据量大一旦出现笛卡尔积后续特征计算会直接卡死。3. 从移动推荐行为序列里构造可落地的特征新人赛的特征工程不需要花哨核心是把用户行为强度转成统计量。关键在于怎么定义“正样本”。移动推荐这个赛题一般预测的是用户是否购买某个商品因此正样本是 behavior_type4 的记录负样本是那些被曝光但没有购买的记录。由于数据里没有曝光日志常见做法是采样一部分有点击却没有购买的行为作为负样本。3.1 用户维度的聚合特征用户特征描述一个用户的活跃度。我一般按 user_id 分组统计每种行为的总次数、独特商品数、独特类目数以及转化率购买次数/点击次数。特征列表如下功能生成方式user_click_count点击行为计数user_fav_count收藏计数user_cart_count加购计数user_buy_count购买计数user_item_uniq点击不同商品数user_cat_uniq点击不同类目数user_buy_ratio购买次数 / 点击次数代码可以这样写df_group behavior.groupby(user_id).agg( user_click_count(behavior_type, lambda x: (x1).sum()), user_fav_count(behavior_type, lambda x: (x2).sum()), user_cart_count(behavior_type, lambda x: (x3).sum()), user_buy_count(behavior_type, lambda x: (x4).sum()), user_item_uniq(item_id, nunique), user_cat_uniq(item_category, nunique) ) df_group[user_buy_ratio] df_group[user_buy_count] / df_group[user_click_count]聚合操作在 pandas 中用 groupby 加 agg 完成。需要说明的是lambda x: (x1).sum()会比value_counts()快一些因为不需要构建完整频次表。如果内存不够可以先把 behavior 表按 behavior_type 拆分再分别去重计数但这里数据量在百万级直接用 groupby 没问题。用户特征的意义在于区分“浏览型用户”和“购买型用户”。有些用户每天点击大量商品但从不购买有些用户收藏即购买这些差异只能通过统计特征捕捉。3.2 商品维度的流行度特征商品特征描述一个商品在全局的热度。热门商品被购买的概率通常更高但用户在移动端的行为序列具有长尾分布过度依赖流行度会造成推荐结果趋同。我通常在训练集全量时间段内统计商品被点击、收藏、加购、购买的总次数以及商品所属类目的购买热度。代码df_item behavior.groupby(item_id).agg( item_click(behavior_type, lambda x: (x1).sum()), item_buy(behavior_type, lambda x: (x4).sum()), item_cart(behavior_type, lambda x: (x3).sum()), ) df_item[item_buy_ratio] df_item[item_buy] / df_item[item_click]商品特征要避免使用待预测当天的行为因为当天行为在真实线上是不存在的。我一般把 12.17 之前所有数据用于构造全局统计量12.17 当天单独拿出来做样本内验证。如果直接使用全量数据构造商品特征会在验证集中引入未来信息导致本地 AUC 虚高。这里有一个容易被忽略的点item_category 是类目维度的信息但它同样可以作为特征。比如统计每个类目的平均购买率再把这个值映射回商品。类目特征的维度比商品低很多能够缓解商品维度稀疏的问题。我通常会把类目维度的购买率作为一个单独的特征加入模型。3.3 基于时间窗口的行为滑窗特征移动推荐和普通推荐最大的区别是时效性用户昨天浏览的商品和上周浏览的商品对应购买意愿差别很大。因此我构造了 1 天、3 天、7 天三个时间窗口内的用户行为统计。比如统计用户最近 1 天点击某商品的次数、最近 3 天加购某商品的次数等。窗口特征可以通过遍历日期区间实现但更高效的做法是先对 behavior 表按照 user_id、item_id 排序然后对每个user, item对统计不同窗口内各类行为的计数。由于数据量不大我直接用 DataFrame 按日期过滤再分组合并for window in [1, 3, 7]: start_day 201412%02d % (17 - window 1) mask behavior[visit_date] start_day window_df behavior[mask].groupby([user_id, item_id])[behavior_type].agg( lambda x: (x1).sum() ).rename(fuser_item_click_{window}d).reset_index() # 后续将此结果 merge 到训练样本上start_day的计算方式是以 12 月 17 日为参考点往前推 window 天。实际赛题评测用的是 12.18所以训练集里也统一用 12.17 及其之前的数据生成特征再预测 12.18。如果测试集的预测日期变了这里需要同步调整。注意滑窗特征很怕信息泄露。比如用包含 12.18 当天行为的数据生成窗口特征会让本地验证分数虚高线上分数却低很多。我本地验证时会严格把特征生成日期限制在训练行为截止日之前。另外窗口长度不宜太长超过 7 天的特征在移动推荐场景中作用会明显下降因为用户偏好漂移太快。3.4 负采样构造训练样本我们要预测的 evaluation 表是 (user, item) 对所以训练样本应当也组织成 (user, item) 的形式标签为是否购买。直接使用全部行为日志会出现大量负样本因为一个用户在一次会话中会浏览很多商品但只买很少商品。常见做法是取购买行为的 (user, item) 作为正样本再从未购买列表中随机抽样相同数量或一定倍数的负样本。我一般按下述逻辑操作positive behavior[behavior[behavior_type] 4][[user_id, item_id]].copy() negative_candidates behavior[(behavior[behavior_type].isin([1,2,3])) (behavior[behavior_type] ! 4)] negative_candidates negative_candidates.drop_duplicates([user_id, item_id]) negative negative_candidates.sample(nlen(positive) * 3, random_state42) train pd.concat([positive.assign(label1), negative.assign(label0)], ignore_indexTrue)这里随机采样采用 3 倍负样本因为移动推荐场景购买率通常在 1%~3%1:3 的比例会让模型有足够多的负样本学习边界又不至于因为正样本被稀释而模型输出概率偏低。负采样比例是一个重要超参数比例越大模型预测分数整体越保守提交前需要进行阈值调整。需要注意负采样只从有点击、收藏、加购行为的 (user, item) 对中抽取。完全没有行为交互的商品对不应出现在训练集中因为它们在 evaluation 表中基本不会出现。这是利用 evaluation 表候选对先验知识快速收敛的技巧。训练样本准备好之后把用户特征、商品特征、滑窗特征通过 merge 拼接到 train 上注意避免 merge 后的行数膨胀先对特征表去重再 merge。拼接完成后用train.isnull().sum()检查缺失值对所有特征填充 0因为缺失通常意味着该统计量为零。4. 用 LightGBM 跑移动推荐预测模型天池移动推荐新人赛中GBDT 类模型是性价比最高的选择。逻辑回归容易欠拟合深度模型需要调参和 GPU而 LightGBM 对稀疏特征和类别特征都有很好支持训练速度快并且能够输出概率。这里的任务是预测用户对商品是否购买属于二分类问题但本质上是排序问题最终提交的是购买概率。4.1 样本切分与验证集构造我把 12.01~12.16 的数据作为训练集12.17 的数据作为验证集用 12.18 作为线上预测目标。为什么不用最后两天做验证因为模型需要看到足够近的数据来捕捉时效性用离预测日最近的一天作为验证最接近线上分布。代码切分时要注意不是随机切分而是按日期切分模拟线上时间顺序train_feats feature_df[feature_df[day] 20141216] valid_feats feature_df[feature_df[day] 20141217] X_train train_feats.drop([label, day], axis1) y_train train_feats[label] X_valid valid_feats.drop([label, day], axis1) y_valid valid_feats[label]如果训练数据中每天的行为量不均匀按日期切分可能让验证集过小。这时可以采用时间序列交叉验证比如用滑动窗口训练多个模型但对新人赛来说单个切分已经足够。切分前最好把day列单独保留不要作为特征否则模型会直接学到“日期越接近购买日概率越高”的伪规律。4.2 LightGBM 参数设置与早停我常用的 LightGBM 参数如下表具体值需要根据数据量微调参数值说明num_leaves31每个树的叶子数量控制模型复杂度learning_rate0.05学习率低一些更稳feature_fraction0.8每次迭代随机选用特征比例bagging_fraction0.8随机抽样比例min_data_in_leaf50叶子最小样本数防止过拟合lambda_l21.0L2 正则训练时使用早停防止在验证集上过拟合。代码如下import lightgbm as lgb dtrain lgb.Dataset(X_train, labely_train) dvalid lgb.Dataset(X_valid, labely_valid) params { objective: binary, metric: auc, num_leaves: 31, learning_rate: 0.05, feature_fraction: 0.8, bagging_fraction: 0.8, min_data_in_leaf: 50, lambda_l2: 1.0, verbose: -1 } model lgb.train(params, dtrain, num_boost_round1000, valid_sets[dvalid], callbacks[lgb.early_stopping(50)])lgb.early_stopping(50)表示 50 轮验证集 AUC 没有提升就停止训练。早停不仅能防止过拟合还能节省调参时间。如果验证集 AUC 在 0.7~0.75 之间对新人赛来说已经是不错的基准。关于num_leaves我见过有人误以为越大越好。实际上num_leaves和max_depth是两套控制树复杂度的机制LightGBM 使用叶子生长策略num_leaves直接限制叶子数量。数据量小时num_leaves从 31 降到 15 反而更稳定数据量充足时可以尝试 64。这里 31 是一个在多数表格数据集上表现稳定的默认值。4.3 特征重要性与迭代方向训练完成后用model.feature_importance()打印特征重要性可以快速判断哪类特征起作用。常见结果中用户最近 7 天点击某商品的次数user_item_click_7d通常靠前因为它同时包含用户活跃度和商品热度两层信息。如果发现全局商品特征重要性过高而用户特征基本为 0说明模型可能没有学到用户差异此时可以检查用户特征是否在 merge 时因重复键被覆盖了。我一般会在第一版模型后做一次特征筛选删掉重要性为 0 的列然后再跑一次验证。这个过程不需要重新写代码只需要把特征列表和训练脚本抽成函数def run_lgb(feature_cols, params, X_train, y_train, X_valid, y_valid): ...把特征列作为参数传入方便不同迭代版本快速切换。新人赛不需要做复杂的特征筛选删掉零重要性特征通常能带来 0.001 左右的 AUC 提升更重要的是能减少过拟合风险。4.4 预测与提交格式生成预测未来一天时需要先把 evaluation 表配上与训练样本相同的特征。这里容易踩坑evaluation 表中有些 (user, item) 对在训练行为中没有出现过merge 后会产生 NaN。处理方式是用 0 填充行为计数再用全量均值填充统计类特征。我倾向用 0 填充因为缺失意味着没有行为而不是均值。test evaluation.copy() test test.merge(user_feats, onuser_id, howleft) test test.merge(item_feats, onitem_id, howleft) test test.fillna(0) y_pred model.predict(test.drop([user_id, item_id], axis1), num_iterationmodel.best_iteration) sub pd.DataFrame({user_id: test[user_id], item_id: test[item_id], probability: y_pred}) sub.to_csv(submission.csv, indexFalse)提交格式一般要求两列 user_id,item_id 和 probability有的版本要求用空格分隔。阅读赛题说明的时候注意分隔符。如果提交后分数与本地验证有较大出入大概率是特征中有泄露或负采样比例不一致。另外预测时一定要用model.best_iteration而不是训练轮的最后一轮。早停后的最佳迭代数可能在一百多轮而模型继续训练到一千轮会在验证集上过拟合。这一点在本地验证时看不出太大差别但线上数据分布稍有偏移时影响会放大。5. 最后一公里用本地回测验证线上波动很多选手在移动推荐赛题上提交后发现线上分数和本地验证差一大截原因经常出在特征时间切分和负采样比例上。这里分享一个具体技巧构造一个“样本内回测”评分函数用每天数据独立做用户购买预测然后计算整体 AUC和你最终的验证 AUC 做对标。5.1 用回测识别特征泄漏回测脚本思路如下from sklearn.metrics import roc_auc_score for day in [20141214, 20141215, 20141216, 20141217]: train_mask feature_df[day] day valid_mask feature_df[day] day model lgb.train(params, lgb.Dataset(feature_df[train_mask].drop([label,day], axis1), labelfeature_df[train_mask][label]), 500) pred model.predict(feature_df[valid_mask].drop([label,day], axis1)) print(day, roc_auc_score(feature_df[valid_mask][label], pred))如果 4 天的 AUC 波动超过 0.02说明模型方差比较大此时不要急着提交优先检查特征是否有高基数维度比如把 item_id 直接作为数值特征送入模型。item_id 是类别型类别数超过 10 万直接数值化会被模型当成连续值导致分裂错误。建议对 item_id 做 target encoding或者干脆只用 item 聚合特征。回测还能帮助识别负采样比例的影响。如果某一天的 AUC 忽高忽低可以观察当天的正样本数量是否远小于其他天。对于正样本不足的日子可以尝试提升负采样倍率或者对这一天的样本赋予更高权重。5.2 用阈值调整控制提交效果线上提交通常看 AUC但最终评分可能采用 F1 或其他指标这时需要把概率转成 0/1。常见做法是在验证集上搜索最佳阈值thresholds [0.3, 0.4, 0.5, 0.6] for t in thresholds: pred_label (pred t).astype(int) # 计算验证集上的 F1 分数由于正样本比例极低直接使用 0.5 往往导致所有样本都判负。移动推荐场景中更可靠的做法是提交概率值而非标签因为天池评分一般支持概率排序。如果一定要提交二分类标签就用验证集 F1 最大化的阈值。注意阈值搜索必须在验证集上进行不能在整个训练集上搜索否则会带来自适应过拟合使本地指标虚高。5.3 检查本地与线上不一致的常见原因在提交前我还会检查三件事。第一确认 evaluation 表里的 user_id 和 item_id 与训练行为中的数据分布是否一致是否存在训练中完全没出现过的 user_id 或 item_id如果存在需要检查字典表是否包含这些 ID。第二确认模型预测概率的范围如果所有预测概率都集中在 0.01 以下说明负采样比例过高或者正样本定义有问题。第三确认提交文件的列名和顺序与赛题要求一致很多新手因为列名多了空格或大小写不一致导致提交失败。如果本地 AUC 和线上 AUC 相差在 0.02 以内属于正常现象。如果相差超过 0.05基本可以断定是信息泄露。最常见的泄露源是使用全量行为日志构造滑窗特征而没有按预测日进行时间截断。以后换到其他推荐赛题时这个回测脚本也能用只要把日期列名改成对应的字段即可。本文还有配套的精品资源点击获取