Datawhale-二分类任务

9.3k 词

比赛简介

用户新增预测挑战赛」是由科大讯飞主办的一项数据科学竞赛,旨在通过机器学习方法预测用户是否为新增用户

比赛属于二分类任务,评价指标采用F1分数,分数越高表示模型性能越好。

如果你有一份带标签的表格型数据,只要目标是分类、回归或排序,那么 LightGBM 都是最强机器学习模型之一,默认首选

赛题建模的价值

用户新增预测是分析 用户使用场景 以及 预测用户增长情况 的关键步骤,有助于进行其后续产品和应用的迭代升级,主要有对行业和技术有如下价值:

行业价值:

  • 精准预测用户增长趋势,优化产品迭代方向

  • 降低用户获取成本,提高营销转化率

  • 为AI能力落地提供量化评估依据

技术价值:

  • 解决实际业务场景中的用户增长预测问题

  • 验证AI在用户行为分析领域的有效性

  • 建立可复用的用户增长预测方法论

赛题要求:

参与算法赛事,一定要仔细理解赛事的 输入-输出 究竟是什么,尤其是提交的格式

输入数据:

  • 用户行为事件记录

  • 15个原始特征字段

  • 关键字段: udmap (JSON)、 common_ts (时间戳)

输出要求:

  • 预测用户是否为新增 ( is_new_did )

  • 提交格式:CSV文件包含一列,字段名为is_new_did,值为0/1

    • 0 表示不是新增用户

    • 1 表示是新增用户

原始数据初步预览

image.png

其中:

  • mid为用户行为模块id,

  • eid为用户行为事件id,

解析一下两者的区别:

  • mid(模块ID):是一类行为的编号,表示用户在做什么“类型”的事情,比如:

    • mid = 1:表示“浏览商品模块”

    • mid = 2:表示“搜索模块”

    • mid = 3:表示“结算模块”

    所以它更像是一个大分类,告诉我们用户当前在哪个模块里操作。

  • eid(事件ID):是具体某个行为的编号,它属于某个模块(mid),但粒度更细,比如:

    • 在“浏览商品模块”中(mid=1),可能有:

      • eid = 101:点击商品详情

      • eid = 102:滑动商品列表

    • 在“搜索模块”中(mid=2),可能有:

      • eid = 201:输入关键词

      • eid = 202:点击搜索按钮

  • did为用户id,

  • device_brand为设备品牌/厂商,

  • ntt为网络类型,

  • operator为运营商,

  • common_country为国家,

  • common_province为省份,

  • common_city为城市,

  • appver为应用版本,

  • channel为应用渠道,

  • common_ts为事件发生时间(毫秒时间戳),

  • os_type用于判断Android还是iOS,

  • udmap为事件自定义属性(标准json文本,内含botId助手ID和pluginId插件ID)

  • is_new_did为预测目标,即是否为新增用户

分析训练集与测试集用户重叠度

然后我们可以通过 经验/资料查阅肉眼观测/代码 等手段,对 赛事提供的数据 有大致的理解和把握:

提取了训练集和测试集中的所有唯一用户ID,分别组成集合

1
2
train_dids = set(train_df['did'].unique())
test_dids = set(test_df['did'].unique())

计算两者交集

1
overlap_dids = train_dids & test_dids

统计数量

1
2
3
num_overlap = len(overlap_dids)
num_train = len(train_dids)
num_test = len(test_dids)

计算比例:

1
2
ratio_in_train = num_overlap / num_train
ratio_in_test = num_overlap / num_test

我们通过数据探索的代码,有一些关键发现:

  • 测试集93%的用户出现在训练集

  • 训练集中88%的用户is_new_did为 0

建模初步思路

  • 数据处理与特征工程: 如处理缺失值、异常值,解析JSON字段,从时间戳中提取特征(年、月、日、小时等),以及构造新的特征以提升模型表现。

  • 分类模型与集成学习: 了解常用的分类算法(如逻辑回归、决策树、随机森林等),特别是梯度提升树(如LightGBM)的原理和使用。LightGBM是微软开发的高效梯度提升框架,具有训练速度快、内存占用低和精度高等优点。

  • 模型评估与指标: 掌握二分类问题的评估指标,如精确率(Precision)、召回率(Recall)和F1分数的定义及计算方法。F1分数是精确率和召回率的调和均值,在正负样本不均衡时比准确率更有参考意义。

  • 交叉验证: 理解交叉验证的作用,能够使用分层K折交叉验证来评估模型性能,避免因随机划分导致的偏差。

  • 超参数调优: 熟悉如何调整模型的超参数(如学习率、树的深度等)以提高模型效果,常用方法包括网格搜索、随机搜索和贝叶斯优化等。

  • 结果提交与分析: 了解竞赛提交的格式要求,能够将模型预测结果生成符合要求的CSV文件提交,并通过排行榜成绩分析模型改进方向。

解题 要点和难点

  • 用户行为事件数据 → 用户级别预测

  • 高维稀疏特征(设备/地域/行为ID)
    device_brand, common_city, mid, eid 这类字段,有成百上千种取值。用 One-hot 编码会变成高维稀疏矩阵,容易导致模型训练慢、过拟合,必须小心处理,比如可以做频率编码、embedding、或仅保留Top-N

  • 正负样本不均衡(新增用户占比较少)
    is_new_did 的 0 和 1 不平衡,大多数是老用户(0)。这会让模型“只学会预测0”,所以需要:

  • 采样策略(欠采样、过采样)

    • 比如统计:
    • 用户一共触发了几个行为(行为数量)
    • 用户用过哪些网络、在哪些城市
    • 用户行为的时间分布(首次时间、活跃时段等)
  • 这是建模效果优劣的关键步骤。

  • 合理评价指标(如AUC或F1,而不是Accuracy)

  • 使用内置样本权重的模型(如 LightGBM)

  • 用户行为聚合:如何将事件级数据转化为用户特征
    数据是以“行为事件”为单位的,比如用户点击了某个按钮,这样的行为记录很多,但我们的目标是预测“某个用户是否是新用户”,所以我们需要将多条事件数据合并成一个用户级别的数据,这是一个“从行到列”的特征聚合问题。

  • 时间敏感特征:用户行为模式随时间变化
    比如一个用户一开始很频繁,后面沉寂,也许是老用户;新用户行为更集中在某几个小时;时间戳可以提取:

    • 小时、星期几、行为密度
    • 首次行为和最后一次行为的间隔
    • 是否在特定时间段活跃(如晚上活跃)

解题思考过程

关键决策点:

  1. 选择树模型而非神经网络(训练速度/特征处理)

  2. 优先构造简单的时间特征而非复杂特征工程

参考资料:

  • LightGBM官方文档(分类任务参数配置)

  • 时序特征工程最佳实践(FeatureTools库)

Baseline方案的设计思路

image.png

核心函数1:交叉验证建模

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
n_folds = 5
kf = StratifiedKFold(n_splits=n_folds, shuffle=True, random_state=42)

for fold, (train_idx, val_idx) in enumerate(kf.split(X_train, y_train)):
print(f"\n======= Fold {fold+1}/{n_folds} =======")
X_tr, X_val = X_train.iloc[train_idx], X_train.iloc[val_idx]
y_tr, y_val = y_train.iloc[train_idx], y_train.iloc[val_idx]

# 创建数据集(指定类别特征)
train_set = lgb.Dataset(X_tr, label=y_tr)
val_set = lgb.Dataset(X_val, label=y_val)

# 模型训练
model = lgb.train(
params,train_set,
num_boost_round=5000,
valid_sets=[train_set, val_set],
callbacks=[
lgb.early_stopping(stopping_rounds=200, verbose=False),
lgb.log_evaluation(period=200)
]
)

细节补充(什么是k-fold cross-validation):

image.png

根据题型还要有的改进点:

使用StratifiedKFold
如果你在处理的是分类问题,并且正负样本比例不平衡(比如“新增用户”只占很少),那你用的 StratifiedKFold 就更进一步了,它在划分每一份的时候,还会确保正负样本的比例一致,这能避免某一折验证集全是负样本,导致模型评估不准。

核心函数2:目标优化函数

1
2
3
4
5
6
7
8
9
10
11
12
13
def find_optimal_threshold(y_true, y_pred_proba):
"""寻找最大化F1分数的阈值"""
best_threshold = 0.5
best_f1 = 0

for threshold in [0.1,0.15,0.2,0.25,0.3,0.35,0.4]:
y_pred = (y_pred_proba >= threshold).astype(int)
f1 = f1_score(y_true, y_pred)
if f1 > best_f1:
best_f1 = f1
best_threshold = threshold

return best_threshold, best_f1

背景:

在二分类模型中,很多模型(比如 LightGBM、XGBoost)输出的不是“0”或“1”的标签,而是一个概率(比如这个样本属于正类的概率是 0.73)
但我们最终要做出决策:到底是正类(1)还是负类(0)?这就需要设置一个“阈值”来做转换
例如:默认是用 0.5,当预测概率大于 0.5 就认为是正类。但这个阈值不是固定的,在样本不均衡或业务目标特殊时,调整阈值可以显著提升 F1 等指标。

设计特点:

  1. 候选范围选择
    • 聚焦 0.1-0.4:适用于正例稀少的场景(如欺诈检测)

    • 若正例比例高,可扩展范围至 np.arange(0.05, 0.95, 0.05)

  2. 数值稳定性
    • 避免使用 np.ptp 等可能受异常值影响的指标

    • 离散化搜索简单高效,复杂度 O(n)

合并数据做特征工程(补充)

1. 类别特征编码一致性

1
2
3
4
5
for feature in cat_features:
le = LabelEncoder()
all_values = pd.concat([train_df[feature], test_df[feature]]).astype(str)
le.fit(all_values)
# 确保训练集和测试集使用相同的编码映射
  • le.fit(values) 这一步是“学习”阶段:它会扫描 values 中所有不同的取值,按字典序排序,然后给每个唯一类别分配一个整数标签(从 0 开始)。这些映射规则保存在 le.classes_ 属性里。

  • 然后用 le.transform(...) 把原始列中的每个值,根据这个映射规则变成整数(比如 ['banana', 'apple'][1, 0])。

  • 最后,这些编码后的数字会“回填到数据集的原列里”,替换掉原来的字符串

关键问题:如果分别编码,可能出现:

  • 训练集中 device_brand='Apple' 编码为 0
  • 测试集中 device_brand='Apple' 编码为 1
  • 导致模型无法正确识别相同的设备品牌

2. 统计特征的完整性

1
2
3
4
5
6
# 基于合并数据计算统计特征
brand_stats = full_df.groupby('device_brand').agg({
'did': 'nunique',
'hour': ['mean', 'std'],
'dayofweek': 'mean'
}).reset_index()

优势

  • 统计更准确(基于更大的样本)
  • 避免训练集和测试集统计分布不一致
  • 提高特征的泛化能力

3. 数据泄露检查

1
2
# 注意:标签信息只在训练集中存在
print(f"正样本比例: {train_df['is_new_did'].mean():.4f}")

安全性

  • 测试集中的 is_new_did 列为 NaN
  • 只使用特征信息,不使用标签信息
  • 不会造成数据泄露

聚合的意义(补充)

原始数据的真实结构:

一行 ≠ 一个用户的所有行为
一行 = 一次用户行为事件,不是用户全部行为

从代码中可以看出:

  • 训练集大小: (3,429,925, 15) - 343万条记录
  • 唯一用户数: 270,837 个用户
  • 这意味着:平均每个用户有 12.7 条记录
1
2
3
4
# 证明:每个用户有多条记录
print(f"总记录数: {len(train_df)}") # 3,429,925
print(f"唯一用户数: {len(train_df['did'].unique())}") # 270,837
print(f"平均每用户记录数: {len(train_df) / len(train_df['did'].unique()):.1f}") # 12.7

🔍 具体例子说明
原始数据长这样:

1
2
3
4
5
6
7
8
    did      eid    common_ts      device_brand  hour  is_new_did
0 user001 100 1640000000000 Apple 14 1
1 user001 200 1640000001000 Apple 14 1
2 user001 300 1640000002000 Apple 15 1
3 user001 100 1640000003000 Apple 16 1
4 user002 100 1640000000500 Samsung 10 0
5 user002 150 1640000001500 Samsung 11 0
6 user003 200 1640000002000 Xiaomi 20 1

问题是什么?

每一行只代表用户的一次行为事件,而不是用户的全部行为!

  • 第0行:用户001在14:00触发了事件100
  • 第1行:用户001在14:00触发了事件200
  • 第2行:用户001在15:00触发了事件300
  • 第3行:用户001在16:00又触发了事件100

我们需要用户级别的特征!

🎯 为什么要聚合?

1. 单行信息不足

  • 单行只能告诉我们:这个用户在某个时间点做了什么
  • 无法告诉我们:这个用户的整体行为模式

2. 用户级别特征更重要

1
2
3
4
5
6
7
8
9
# 没有聚合的情况下,我们只知道:
# - 用户001在14:00触发了事件100
# - 用户001使用Apple设备

# 有了聚合后,我们还知道:
# - 用户001总共触发了4次事件 (frequency)
# - 用户001使用了3种不同事件类型 (monetary)
# - 用户001最后一次活动在16:00 (recency)
# - 用户001的活跃时间跨度是2小时

3. 模型需要用户画像

  • 活跃用户: 高频次、多事件类型、最近活跃
  • 不活跃用户: 低频次、单一事件、很久没活跃

💡 实际价值

聚合特征能回答的问题:

  1. 这个用户活跃吗?frequency (行为频次)
  2. 这个用户最近活跃吗?recency (最近活跃时间)
  3. 这个用户行为多样吗?monetary (不同事件类型数)
  4. 这个用户是重度用户吗? → 综合RFM得分

这些特征对预测”新用户”很重要:

  • 新用户: 通常频次低、事件类型少、最近注册
  • 老用户: 通常频次高、事件类型多、历史悠久

🎯 总结

聚合的核心价值:

  • 将用户的多次行为汇总成用户画像
  • 事件级别提升到用户级别的特征
  • 为模型提供更丰富的用户行为信息

没有聚合:只知道用户做了什么
有了聚合:知道用户是什么样的人

Baseline方案的优缺点

方案优点:

  1. 数据预处理完整

    • 时间特征提取:将毫秒时间戳转换为 daydayofweekhour 等可解释的时序特征,捕捉用户行为的周期性规律。

    • 类别特征编码:对 device_brandoperator 等高基数类别特征使用 LabelEncoder 进行编码,避免了独热编码导致的维度爆炸问题。

    • 交叉验证策略:采用 StratifiedKFold 分层交叉验证,确保训练集和验证集的类别分布一致性,减少模型偏差。

  2. 模型选择合理

    • 使用LightGBM作为基模型,适合高维稀疏数据,且对类别特征处理友好(如 udmap 中的JSON字段)。

    • 阈值优化:通过动态调整分类阈值( find_optimal_threshold )最大化F1分数,适应类别不平衡问题(新增用户比例较低)。

  3. 特征重要性分析

    • 输出特征重要性( gain ),帮助识别关键特征(如 udmap 中的插件ID或助手ID),为后续特征优化提供方向。

方案不足:

  1. 特征工程深度不足

    • 未充分挖掘 udmap 字段中的JSON信息(如 botIdpluginId ),仅将其作为类别特征处理,未提取组合特征(如 botId+pluginId 的交互)。

    • 忽略用户行为序列模式(如用户访问频次、事件路径),未能构建基于时间窗口的统计特征(如24小时内访问次数、连续登录天数)。

  2. 模型调参空间有限

    • 参数固定(如 max_depth=12num_leaves=63 ),未通过网格搜索或贝叶斯优化探索更优参数组合。

    • 未尝试集成模型(如CatBoost、XGBoost)或模型融合策略(如Stacking)提升泛化能力

进阶要点1:如何挖掘用户行为信息

时间序列特征:

  • 滑动窗口统计:计算用户在最近7天、30天内的行为频次(如 eid 事件发生次数)、活跃时长(连续登录天数)。
  • 示例代码:
1
2
3
4
5
# 按用户分组,按时间排序
train_df.sort_values(['did', 'common_ts'], inplace=True)

# 计算用户历史行为频次(滑动窗口)
train_df['user_event_count'] = train_df.groupby('did')['eid'].transform('cumcount') + 1

RFM特征:

  • Recency(最近一次行为时间):计算用户最近一次行为距离当前时间的天数。
  • Frequency(行为频率):用户总行为次数。
  • Monetary(行为价值):假设某些事件(如 eid )有经济价值,可定义虚拟价值字段。
  • 示例代码:
1
2
3
4
5
6
7
# 计算RFM特征
rfm = train_df.groupby('did').agg(
recency=('common_ts', 'max'),
frequency=('eid', 'count'),
monetary=('eid', 'nunique') # 假设不同事件类型代表不同价值
).reset_index()
train_df = train_df.merge(rfm, on='did', how='left')

组合特征与高阶交互
目标:通过特征交叉和非线性组合,捕捉复杂模式。
方法:

  • 类别特征交叉:
    • device_brandos_type 组合(如 Android_Samsung ),反映设备-系统的用户偏好。
    • 示例代码:
1
train_df['device_os'] = train_df['device_brand'].astype(str) + '_' + train_df['os_type'].astype(str)
  • 数值特征分桶与交叉:
    • 将将 common_ts 分桶为时间段(如早高峰、晚高峰),与 hour 特征交叉,分析用户在特定时段的行为差异。
    • 示例代码:
1
2
# 分桶:将时间戳转换为时间段(如0-6: 0, 6-12:1...)
train_df['time_bucket'] = pd.cut(train_df['hour'], bins=[0,6,12,18,24], labels=[0,1,2,3])

进阶要点2:选择什么样的建模思路

1、按 did 聚合后的分层建模

核心思想:
将问题拆解为两层:用户层和事件层,通过分层建模捕捉用户级特征与事件级特征的交互。
具体步骤:

  1. 用户层建模:
    • 对每个 did 进行特征聚合(如行为频次、时间分布、RFM特征等),训练一个用户级模型(如LightGBM/XGBoost),预测该 did 的标签(或概率)。
    • 例如:使用每个 did 的累计行为次数、最近一次行为时间间隔等作为输入特征。
  2. 事件层建模:
    • 在事件级别(即原始样本)引入用户级预测结果作为特征,构建混合模型。
    • 例如:将用户级模型的输出(如预测概率)与事件级特征(如 eid 、时间戳、设备信息)拼接,输入到最终模型(如神经网络或集成树)中进行预测。
      优势:
  • 用户层模型可捕捉全局用户行为模式,事件层模型可结合局部事件特征,提升模型鲁棒性。
  • 对于测试集中重复的 did ,用户层预测结果可直接复用,减少冗余计算。

2、半监督学习策略

核心思想:

利用测试数据中重复的 did (部分标签已知但不唯一)作为伪标签,结合训练数据进行半监督训练。

具体步骤:

  1. 筛选伪标签:
    • 对测试集中重复的 did ,若其在训练集中有多个样本且标签一致,可直接使用该标签作为伪标签。
    • 若标签不一致,可通过以下方式处理:
      • 投票机制:选择训练集中该 did 的多数类标签。
      • 置信度阈值:对预测概率较高的样本(如阈值>0.95)生成伪标签。

优势:

  • 利用测试数据中的隐式信息(重复 did 的已知标签),提升模型泛化能力。

  • 对标签不唯一的场景,通过置信度筛选降低噪声风险。

3、动态标签映射与修正

核心思想:
针对测试集中重复的 did ,动态修正其预测标签,结合训练数据中的历史行为模式。
具体步骤:

  1. 标签冲突检测:
    • 对于测试集中某个 did ,若其在训练集中有多个标签,统计标签分布(如出现次数、时间趋势)。

    • 例如:若某 did 在训练集中同时存在0和1标签,但1标签集中在近期事件中,则优先选择1作为预测值。

  2. 动态修正策略:
    • 时间加权平均:根据训练集中 did 的标签时间分布,加权计算预测值。

    • 行为相似度匹配:将测试样本与训练集中该 did 的相似行为样本匹配,提取标签分布。

优势:

  • 精细化处理标签不唯一的场景,避免简单投票导致的偏差。

  • 结合时间动态性和行为相似性,提升预测的合理性。