天池移动推荐赛:从解压数据到LightGBM提交实战
2026/9/16 12:22:49 网站建设 项目流程

简介:阿里天池新人赛移动推荐试玩工程,定位为机器学习竞赛入门项目,面向刚接触天池或推荐场景的开发者。包内以用户行为预测为目标,提供完整的Python实现,包含数据预分析、特征构造、模型训练与预测等环节,并专门设有一份Spark版本代码,便于对比单机与分布式处理流程。压缩包共26个文件,以Python脚本为主,辅以说明文档、特征列表与配置文件,整体仅138KB,结构精简清晰。目前已有114人学习浏览。这份资源不仅能帮助新手理解竞赛项目的目录组织,还能通过可实际运行的代码掌握移动推荐场景从数据处理到模型比对的完整路径,对参加天池新人赛或入门推荐算法都很有参考价值;建议从数据预分析开始逐步读起,再对照特征构造与模型训练代码,形成完整认知闭环。

1. 天池新人赛试玩移动推荐,下载 zip 只是第一步

第一次看到“阿里天池竞赛新人赛试玩_移动推荐.zip”这个文件时,很多人以为解压出来就能直接跑模型,结果发现里面是一堆用户对商品的点击、收藏、加购、购买记录。这个赛题的目标很直白:根据过去若干天用户的行为日志,预测用户接下来一天会不会购买某个商品。对初学者来说,它比图像分类更有意思的地方在于数据是结构化的行为序列,但正样本稀疏,负采样和特征工程比堆模型重要得多。我整理了一套从解压 zip 到提交结果完整的试玩路子,顺着往下走,新手也能在本地调出一版有意义的提交。

2. 解压 zip 后先搞清楚 user、item、行为表怎么关联

拿到压缩包后,我习惯先把它放到一个干净的目录里,用 unzip 命令解压。常见的数据集包含用户、商品、行为日志和评价列表四个文件,命名和列名在不同赛题轮次会有点差异,但核心逻辑一致。解压之后先别忙于看数据,先核对列名和主键关系,否则后续 join 很容易产生笛卡尔积。如果你在解压时遇到invalid zip archivecould 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行为发生日期,格式为 20141201
evaluation.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, on="user_id", how="left") print(merged.shape, behavior.shape)

如果 merge 后的行数不等于 behavior 的行数,说明 user_feats 或 item_feats 存在重复键。重复键可能是数据本身有重复,也可能是特征聚合时没有按连接键分组。这个检查虽然简单,但能省下大量排错时间。移动推荐赛题的数据量大,一旦出现笛卡尔积,后续特征计算会直接卡死。

3. 从移动推荐行为序列里构造可落地的特征

新人赛的特征工程不需要花哨,核心是把用户行为强度转成统计量。关键在于怎么定义“正样本”。移动推荐这个赛题一般预测的是用户是否购买某个商品,因此正样本是 behavior_type==4 的记录,负样本是那些被曝光但没有购买的记录。由于数据里没有曝光日志,常见做法是采样一部分有点击却没有购买的行为作为负样本。

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: (x==1).sum()), user_fav_count=("behavior_type", lambda x: (x==2).sum()), user_cart_count=("behavior_type", lambda x: (x==3).sum()), user_buy_count=("behavior_type", lambda x: (x==4).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: (x==1).sum()会比value_counts()快一些,因为不需要构建完整频次表。如果内存不够,可以先把 behavior 表按 behavior_type 拆分再分别去重计数,但这里数据量在百万级,直接用 groupby 没问题。

用户特征的意义在于区分“浏览型用户”和“购买型用户”。有些用户每天点击大量商品但从不购买,有些用户收藏即购买,这些差异只能通过统计特征捕捉。

3.2 商品维度的流行度特征

商品特征描述一个商品在全局的热度。热门商品被购买的概率通常更高,但用户在移动端的行为序列具有长尾分布,过度依赖流行度会造成推荐结果趋同。我通常在训练集全量时间段内统计商品被点击、收藏、加购、购买的总次数,以及商品所属类目的购买热度。代码:

df_item = behavior.groupby("item_id").agg( item_click=("behavior_type", lambda x: (x==1).sum()), item_buy=("behavior_type", lambda x: (x==4).sum()), item_cart=("behavior_type", lambda x: (x==3).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: (x==1).sum() ).rename(f"user_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(n=len(positive) * 3, random_state=42) train = pd.concat([positive.assign(label=1), negative.assign(label=0)], ignore_index=True)

这里随机采样采用 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"], axis=1) y_train = train_feats["label"] X_valid = valid_feats.drop(["label", "day"], axis=1) 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, label=y_train) dvalid = lgb.Dataset(X_valid, label=y_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_round=1000, valid_sets=[dvalid], callbacks=[lgb.early_stopping(50)])

lgb.early_stopping(50)表示 50 轮验证集 AUC 没有提升就停止训练。早停不仅能防止过拟合,还能节省调参时间。如果验证集 AUC 在 0.7~0.75 之间,对新人赛来说已经是不错的基准。

关于num_leaves,我见过有人误以为越大越好。实际上num_leavesmax_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, on="user_id", how="left") test = test.merge(item_feats, on="item_id", how="left") test = test.fillna(0) y_pred = model.predict(test.drop(["user_id", "item_id"], axis=1), num_iteration=model.best_iteration) sub = pd.DataFrame({"user_id": test["user_id"], "item_id": test["item_id"], "probability": y_pred}) sub.to_csv("submission.csv", index=False)

提交格式一般要求两列 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"], axis=1), label=feature_df[train_mask]["label"]), 500) pred = model.predict(feature_df[valid_mask].drop(["label","day"], axis=1)) 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,基本可以断定是信息泄露。最常见的泄露源是使用全量行为日志构造滑窗特征,而没有按预测日进行时间截断。以后换到其他推荐赛题时,这个回测脚本也能用,只要把日期列名改成对应的字段即可。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询