☰
微信大数据挑战赛复盘:CTR预估与特征工程实战全程解析
2026/10/20 7:19:31 网站建设 项目流程

微信大数据挑战赛,国内数据竞赛圈里关注度很高的一个场次。今年我和队友从初赛一路打到决赛,最终拿到的名次不算靠前,但整个过程下来,踩过的坑、试过的方法、调过的参,远比最后的奖项更有价值。这篇文章不打算写一段“获奖心得”,而是把我们从赛题拆解、数据探索、特征工程、模型训练到最后做融合的全流程,用复盘的方式记录下来。适合那些准备第一次参加数据竞赛、或者已经报了名但不知道从哪下手的同学,也适合那些打过几次比赛但总觉得分数卡在平台期的朋友。我会尽量说人话,把每一步“为什么这样做”讲清楚,而不是只丢一份跑分报告。

1. 赛前准备与赛题拆解

1.1 先别急着跑模型,把赛题读懂

数据竞赛最常见的一个错误,就是拿到数据立刻开始写代码,恨不得当天晚上就把 baseline 跑出来。我第一次参赛的时候也是这样,结果后来才发现对赛题的理解是错的,大量时间花在了无用的方向上。

微信大数据挑战赛这类的题目,通常会给一张用户行为日志表,里面包含用户ID、内容ID、行为类型、行为时间戳等字段,目标是根据历史行为去预测用户在特定场景下会不会发生某种行为。更具体地说,这是一个CTR/CVR类型的预估问题。第一步要做的事情不是建模,而是把几个关键问题想清楚:

  • 预测的目标到底是什么?是二分类概率,还是排序分数?
  • 评估指标是什么?AUC 和 GAUC 的差别很大,会直接决定特征和模型怎么设计。
  • 训练的样本是什么粒度?一条样本代表一次曝光,还是一对用户-内容组合?
  • 训练集和测试集的时间范围是怎样的?这决定了验证集的划分方式。

这些内容赛题说明里通常都有,但很多人看一遍就过了。我的习惯是,把赛题说明打印出来,用笔把关键词圈出来,尤其是“评估指标”和“训练/测试数据格式”这两段,逐字逐句读三遍。遇到不清楚的地方,再回到赛题页面上和主办方确认。宁可赛前多花半天,也好过后面用一个月为错误理解买单。

1.2 评估指标决定了解题方向

我记得这一届微信大数据挑战赛的评估指标用的是 AUC 家族。很多人觉得 AUC 不就是 ROC 曲线下面面积嘛,用 sklearn 算一下就出来了,能有多大差别?实际上差别大得很。

如果是普通 AUC,你把所有样本简单拼接在一起算就行,这时候你关心的是全局排序能力。但如果是 GAUC(Group AUC),那就要按照某个维度(通常是用户)分组,先计算每个组内的 AUC,再按组内样本数或曝光数加权平均。这意味着模型的重心不再是“全局把正样本排前面”,而是“在每个用户自己的候选集里把正样本排前面”。这两个目标并不完全一致。

举个例子:全局 AUC 高,可能是因为模型特别擅长区分活跃用户和非活跃用户,把活跃用户的正样本都排前面了。但 GAUC 更看重同一个用户内部,你对 TA 喜欢的内容和不喜欢的内容有没有区分度。所以做特征的时候,需要在用户维度的个性化特征上花更多功夫,而不是一味堆内容侧统计量。

我们当时花了大概一周时间,围绕指标设计了验证方式,后面基本没有走偏。我的建议是,参赛第一天就把评估指标吃透,并且自己写一个复现评估指标的脚本,用 baseline 的小结果跑通验证,后面每一次提交之前都可以本地先算一遍。否则你根本不知道自己的每次改动到底有没有用。

2. 数据分析与样本工程

2.1 数据探索要回答的三个问题

拿到数据以后,我先做了比较系统的 EDA(探索性数据分析)。很多人做 EDA 就是画几张分布图,看一眼就过去了。我的经验是,要有目的地去看,主要回答三个问题。

第一,数据规模有多大,能不能放进内存?微信大数据挑战赛这类比赛的数据量级通常在几千万到几亿行,单机 Pandas 很容易内存爆掉。我一开始直接 pd.read_csv,结果 16G 内存的机器直接卡死。后来改成用 pandas 的分块读取,或者换成分块读入后抽样统计,路径才顺畅起来。

第二,数据的时间分布是什么样的?用户行为数据往往有明显的星期效应和小时效应。我画了每天的行为量、每个小时的活跃度曲线,发现工作日的白天和晚上的行为模式完全不同。这些规律后面都做成了时间特征。

第三,缺失和脏数据多不多?ID 类字段一般比较干净,但数值字段经常有明显的异常值,比如某条记录的停留时长为负数,或者年龄字段出现几百这种离谱值。对待缺失值,我采取的策略是“能不做填充就不做填充”,因为很多模型对缺失值有自己的处理方式。LightGBM 可以直接把 NaN 当成一个分支方向,强行填充反而会引入噪声。

下面是我当时做数据概况统计时的核心代码,压力不算大:

import pandas as pd # 分块读取,先看整体行数和字段 chunk_size = 500000 total_rows = 0 dtypes_summary = {} for chunk in pd.read_csv("train_log.csv", chunksize=chunk_size): total_rows += len(chunk) for col in chunk.columns: unique_est = chunk[col].nunique(dropna=False) dtypes_summary[col] = dtypes_summary.get(col, 0) + unique_est print("总行数:", total_rows) print("每列唯一值累加:", dtypes_summary)

这里用的是一个近似估计思路,不需要精确统计每一列的唯一值,因为大数据量下 nunique 非常耗时。先快速了解字段的基数,再决定哪些字段适合做类别特征、哪些适合做数值特征。

2.2 样本构建与正负样本处理

赛题给的行为日志,并不直接就是训练样本。需要自己决定:一条样本怎么定义?正样本是什么?负样本从哪里来?

拿 CTR 预估场景来说,通常一条样本就是一次“曝光”。曝光后用户点了,就是正样本,没点就是负样本。但微信大数据挑战赛里给定的是用户过去一段时间的日志,没有显式的曝光记录,这就需要推断。我们当时的做法是,把用户发生过目标行为的时间点看作正样本,再在附近的时间窗口里采样未发生该行为的时刻构造负样本。

负采样的比例非常关键。如果正负样本比是 1:100 甚至更大,模型会倾向于把所有样本都预测为负样本。我们的经验值是控制在 1:5 到 1:20 之间,具体看模型。采样时还要注意“时间对齐”:负样本的时间分布要和正样本保持一致,不能只在半夜采样负样本,那样就等于强行塞入了时间信息。

我们第一版 baseline 用的就是 1:10 的负采样比例,线下 GAUC 大概 0.62,后面慢慢调到 0.70 以上。如果一开始就把负样本调到 1:50,baseline 会非常难看,甚至让你误判整个方案的方向。

2.3 验证集划分:按时间切,不要随机切

这条经验可以说是数据竞赛里最重要的一条,没有之一。用户行为数据本身有时间结构,线上测试集是未来一段时间的样本,如果你在训练集里随机抽 20% 做验证,那么验证集里既包含“过去”也包含“未来”,模型在训练时已经见过和验证集同时期的数据分布,离线分数会虚高,而且这种虚高在不同特征组合之间并不稳定。

正确做法是,按照时间先后把训练集切成两部分,比如前 7 天做训练,最后 1 天做验证。这样能够模拟真实线上场景。我们当时用了“滑窗 + 留一天”的方式,确保验证集的 GAUC 和最终线上结果的相关性高很多。

注意:如果赛题提供的训练数据本身就包含多个时间片,一定要先看测试集对应的时间范围。训练集最后一天的分布可能和测试集相差很大,那么验证集还要再往前放一点,尤其要留出“评价期”和“冷启动”缓冲。

另外,构造样本时要注意不能 lookahead。也就是说,某条样本在构造特征时只能使用该样本时间点之前的信息。这个点在特征工程部分还会反复踩到。

3. 特征工程实战

3.1 用户侧特征:一切围绕历史行为

特征工程是整个比赛里最花时间、也是提分最大的一块。在微信大数据挑战赛这种用户行为预测任务里,用户侧特征的重要性大于内容侧特征,因为 GAUC 本身就更看重用户内部的排序。

用户侧特征主要包括几类:

  • 历史行为统计:过去 1 天 / 3 天 / 7 天用户的行为次数、目标行为次数、点击率(CTR)、行为多样性(涉及多少个不同内容)等。
  • 活跃度刻画:用户每天活跃时段、最近一次活跃距今多久、活跃间隔的平均值和方差。
  • 兴趣向量:对内容 ID 的偏好,可以用用户最近行为的内容 ID 做聚合 embedding。

这里要特别强调时间衰减。同一个用户在 7 天前的一次行为和昨天的一次行为,信息量完全不同。所以做统计特征时,不能简单把整个历史平均一下,而是要做“时间衰减加权”。最常用的方法是指数衰减:给距当前时间越远的行为乘以越小的权重。

import numpy as np def decayed_count(df, time_col, span_days=7, half_life_days=2): # 构造指数衰减权重 delta_days = (df[time_col] - df[time_col].max()) / (24 * 3600) weights = np.power(0.5, -delta_days / half_life_days) df["decayed_cnt"] = df.groupby("user_id")["behavior_id"].transform("count") * weights return df

这段代码是我为了说明思路简化过的,真实场景里要先把时间戳换算成相对时间,然后对每个用户做聚合。重点不是代码本身,而是这个“衰减窗口 + 半衰期”的思路。半衰期可以多试几个值,比如 1 天、3 天、7 天,放到模型里让特征重要性告诉你哪个更有效。

3.2 内容侧与交叉特征

内容侧特征也很重要。每个内容 ID 会有自己的历史热度、近 1 天 / 近 7 天被行为次数、正样本率等。这类特征的作用是捕捉“这个内容本身火不火”。但它有一个问题:内容的热度是会衰减的,有的内容上线第一天很火,后面就淡了。所以内容侧特征同样需要加时间窗口,不能直接全量统计。

交叉特征是把用户和内容拼在一起。最简单的做法是统计“该用户过去对该内容的行为次数”,如果有的话,这个特征几乎会直接给出答案。但真实的推荐场景里,绝大多数用户和内容之间没有历史交互,大多数值都是 0,稀疏度极高。

这种情况下,我一般会构造一个“用户-内容类别”的组合特征。比如说内容有频道或分类字段,就可以统计“该用户在该频道上的历史行为数”、“该频道下内容的平均热度”这样的特征。它比单纯的用户侧特征更能反映个性化,又比用户-内容的精确匹配稀疏度低很多。

我在做交叉特征时的另一个心得是:不要迷信 pairwise 交叉,先做用户侧和内容侧的宽表,然后让模型自己去学特征之间的非线性交互。LightGBM 这类 GBDT 模型本身就能做特征分裂组合,很多时候你把交叉特征做好,提分效果反而不如多加一个高质量的单侧特征。

3.3 时间上下文特征与防穿越

行为数据比赛还有一个很容易被忽视的点:时间上下文特征。用户在周一早上看到的内容,和周五晚上看到的内容,行为概率可能差别很大。所以我在特征列表里加入了小时、星期、是否节假日这些字段。但它们不能直接作为原始特征喂进去,因为测试集的时间范围可能不同,直接把小时放进去会导致模型学到“测试集的某个小时概率低”这种错误的模式。

更好的做法是,把“用户在当前小时的活跃程度”或者“内容在最近一个小时内被行为的数量”作为动态特征。这种特征既携带了时间信息,又不是单纯的日历时间。

防穿越是特征工程里最需要小心的地方。最常见的坑是:构造内容侧特征时用了全量数据统计,导致训练集和验证集共享了同一份统计量,验证集的信息被泄露到了训练集里。解决办法是,特征统计必须按时间进行,比如每个内容的“7 天热度”只能在当前样本时间点之前的 7 天窗口内计算。这需要写一个有序的滚动聚合,不能简单地对整个表 groupby。

我们后来用过一个简单的 debug 方法:把训练集里“点击率特别高”的样本挑出来,检查它们的特征值,如果发现某些内容 ID 的热度特征高得离谱,通常就是泄露了。发现之后不要心疼数据,直接重算。

4. 模型训练与调优

4.1 第一个 Baseline:LightGBM 必须跑通

第一次做微信大数据挑战赛这种数据量级的比赛,我不建议一上来就上深度模型。哪怕你的深度学习经验再丰富,也先跑一个 LightGBM baseline。原因有三个:

第一,baseline 的价值在于打通流程,包括数据读取、特征拼接、训练验证、预测提交这一整套 pipeline。流程通了,后续所有优化才有依托。第二,GBDT 对特征工程的结果反馈非常直接,特征做得好不好,很快能从线下 GAUC 变化上看出来。第三,LightGBM 处理几十个特征、千万级样本完全够用,迭代速度快,适合快速验证想法。

我们当时的 baseline 只用了几十个特征,线下 GAUC 大概 0.62。流程跑通之后,后面所有的优化都在这个基础之上迭代。如果你连 baseline 都没有,就想着去做模型融合,那等于空中楼阁。

4.2 LightGBM 参数调优的实用顺序

LightGBM 参数很多,但实际调参不需要一上来就网格搜索。我的调参顺序是固定的:

  1. 先固定一个较大的学习率 0.1,把 num_leaves、min_data_in_leaf、feature_fraction 这些参数根据经验设一个合理初值。
  2. 用 early stopping 确定迭代轮数。千万不要手工固定一个 500 轮,那样容易过拟合或欠拟合。
  3. 再缩小学习率,比如 0.05、0.01,重新跑一遍,观察验证集 GAUC 的变化。
  4. 最后针对 num_leaves 和 min_data_in_leaf 做小范围搜索。

我给出一个我当时用的参数配置作为参考:

params = { "objective": "binary", "metric": "auc", "learning_rate": 0.05, "num_leaves": 63, "min_data_in_leaf": 100, "feature_fraction": 0.8, "bagging_fraction": 0.8, "bagging_freq": 1, "verbose": -1, "num_threads": 8, }

其中 min_data_in_leaf 设大一点,是为了在大数据集上抑制过拟合。数据量越大,叶子节点越小,模型越容易记住噪声。宁可让树的深度浅一点,把泛化性能留给后面的融合。

调参过程一定要做好实验记录。我们会在每轮实验后面记录特征版本、参数、线下 GAUC、线上 GAUC。没有记录,你后面一定会忘掉之前调过什么,然后浪费大量时间重复实验。

4.3 深度学习模型值不值得上

微信大数据挑战赛这类比赛,很多冠军方案会用深度学习模型,尤其是带 attention 机制的网络,比如 DIN、DIEN 这些,专门建模用户行为序列。但这些模型能不能帮你提升,完全取决于你的数据量和数据形态。

如果你的样本量在千万级以上,用户行为序列足够长,那么深度学习模型值得尝试。但如果只有几百万样本,行为序列又短,深度模型很可能打不过调参到位的 LightGBM,而且训练成本高到难以快速迭代。

我的建议是:前期把精力放在特征和 LightGBM 上,把框架跑通之后,如果时间充裕,再尝试一个深度学习版本。不要一上来就和深度学习较劲,数据竞赛里绝大部分分数来自特征工程和集成,深度学习是“锦上添花”,不是“雪中送炭”。

我们尝试过 DeepFM,在相同特征下因为 embedding 学习的限制,初期效果反而不如 LightGBM。后来改变策略,把 LightGBM 的概率输出作为深度模型的一个特征,才算有了正向收益。这个做法本质上是把两个模型的输出做了融合,非常实用。

5. 模型融合与线上提分

5.1 融合为什么总是能提升

单模型做到后期,线下 GAUC 基本在一个平台期上不动了。这时候最有效的提分手段通常就是模型融合。不同模型学到的数据模式不一样,LightGBM 擅长捕捉特征交叉,深度模型擅长捕捉 embedding 表示,融合之后可以取长补短。

最常用的融合方式是加权平均。假设你有两个模型,分别输出概率 p1 和 p2,那么最终概率就是 alpha * p1 + (1 - alpha) * p2。这个 alpha 需要在验证集上搜索,一般从 0.5 开始,向两边试。要注意的是,两个模型如果相关性太高,融合几乎没效果。所以我们一般会选择 LightGBM + 深度模型,或者 LightGBM + XGBoost,而不是两个结构完全一样的模型做融合。

还有一种叫排序融合。因为评估指标是 GAUC,本质上只关心排序,不关心具体概率值,所以可以把多个模型的预测结果按行分别排名,再把排名取平均。这个方法在指标是 GAUC 的时候往往比概率加权更稳,因为它不容易被某个模型的概率分布形态带偏。

我当时的方法是这样:先用 LightGBM 和 XGBoost 各出一个预测,再对每行取两个模型的概率平均,验证集 GAUC 从 0.685 提升到 0.692。然后又把深度模型的输出加了进来,三个模型做简单平均,线下又提升了 0.003。看起来幅度不大,但在排名榜上就是几个名次的差距。

5.2 离线高分线上掉分,问题出在哪

参赛过程中最让人崩溃的事就是:本地验证集 GAUC 很高,一提交线上分数就掉一截。这种情况在微信大数据挑战赛里非常常见,原因可能有几个。

第一,验证集和线上测试集的时间分布不一致。前面说过,如果验证集切得太靠近训练集,而线上是更远的未来,模型在验证集上看到的分布和历史更接近,自然会虚高。第二,特征统计量发生了穿越。某个内容的热度特征是在全量数据上统计的,训练集和验证集共享了信息,导致线下虚高,但线上做得到做不到。第三,线上测试集的样本构成不一样。比如线上正负样本比例和线下不同,AUC 会直接受影响,这是数据本身的问题,不一定是模型过拟合。

面对这种问题,我的做法是:先检查验证集时间切分是否合理,再看特征是否穿越,如果都没问题,就干脆以线上为准调整策略。有些特征线下特别有用但线上没用,说明是统计口径问题,要果断删掉。

注意:提交结果之前,一定要确认预测概率的列名、样本顺序和线上要求完全一致。看起来是小问题,实际上每年都能淘汰不少人。

6. 参赛心得与避坑清单

6.1 时间管理与迭代节奏

数据竞赛是一场马拉松,不是冲刺。两个多月的赛程,如果前期用力过猛,后期很容易疲掉。我当时把整个赛程分成了三个阶段:前两周做数据探索和 baseline,中间三周集中迭代特征,最后两周做模型融合和精调。每个阶段结束都留出缓冲时间,防止意外情况影响进度。

比赛的日常节奏也很关键。我固定每天早上跑一组实验,下午分析结果,晚上写代码构造下一天的特征。每天只做一到两个改动,配合实验记录,这样每一分提升都能定位到具体原因。千万不要一天同时改十个特征,那样就算分数涨了,你也不知道到底是哪个起作用。

6.2 我踩过的五个坑和解决方案

我把一次完整的比赛过程中遇到的典型问题整理成了一个表格,方便大家对照排查。

问题表现原因解决方案
内存爆炸读取数据时直接被 kill数据量太大,一次性 load分块读取,用抽样统计
验证集虚高线下 GAUC 0.75,线上 0.65验证集随机划分,特征穿越按时间划分验证集,特征滚动计算
负样本过多模型预测概率普遍极低正负比失衡负采样 1:10,观察 GAUC 变化
深度模型不涨分DeepFM 效果不如 GBDT数据量不足 / 行为序列短把深度模型输出作为融合特征
提交 ID 错误提交结果得分为 0预测顺序和测试集不一致提交前校验行数和顺序,写脚本自动检查

这五个坑,每一个我都不是从书上看来的,而是实打实踩过。尤其是验证集虚高那个问题,曾让我们整个团队一度怀疑特征方案错了,后来花了两天排查才发现是特征穿越。希望后来者能少花这种冤枉时间。

6.3 给新手的三个建议

如果这篇文章对你有用,最后我想给准备参加比赛的你说三句话。

第一,先跑通再优化。一个可以运行的 baseline,比一万个停留在脑子里的完美方案都值钱。第二,建立自己的实验记录体系。用简单的表格记录每次实验的改动和结果,这会让你的工作效率至少翻一倍。第三,多看看公开的思路分享。比赛最关键的是思路碰撞,不要闷头做。我在比赛过程中就借鉴了很多前人的特征构造经验,再用自己的方式落地。

说说我个人这几个月下来最深的体会。参赛之前,我以为数据竞赛拼的是模型复杂度,谁的网络结构高级谁就赢。真正走完微信大数据挑战赛这一趟,我才明白,决定排名的往往是那些看起来不起眼的基础工作,比如吃透指标、做对验证集、防住特征泄露、把实验记录做好。这些工作不性感,甚至有点枯燥,但每一步都在实打实地提高你的分数。如果你也想参加下一次比赛,别纠结于那些花哨的技巧,先把这几点做好,成绩一定不会差。

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

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

立即咨询