
简介压缩包内含天池O2O优惠券使用预测与达观杯文本智能处理挑战赛两套竞赛项目代码面向大学生数据竞赛、挑战杯备赛者及入门机器学习实战的开发者。包内共9个文件以4个Python脚本为主覆盖特征提取、XGBoost训练、稀疏特征分类器等关键环节另配套数据文件、Markdown说明文档、结果展示图片及开源许可证说明整体约121KB体量精简、结构清晰。通过阅读代码与说明可理解优惠券核销预测中的用户行为特征构造方式也能掌握文本智能处理中稀疏特征与分类器结合的应用思路两份任务分别侧重结构化特征工程与文本稀疏表示适合备赛时对照算法实现和参数调优。目前已有106人学习下载适合希望参考完整赛题方案、快速搭建竞赛代码框架并基于自身数据做二次改造的参赛者使用。1. 从 o2o 优惠券到长文本文本一次特征工程的两个极端把天池 o2o 优惠券使用预测和达观杯文本智能处理挑战赛的数据同时打开时很多人默认这是两个互不相关的项目。我反而觉得这两场比赛放一起分析更容易把建模框架用熟一个表格型数据的二分类预测一个长文本多分类最花时间的不是选模型而是样本标签的定义和特征在时间维度上的安全性。优惠券预测里最常见的翻车原因是把领取之后的消费信息混进特征文本分类里最常见的翻车是为了降维把长文章截断到只剩开头主题信息在预处理阶段就丢了。这篇按标签、特征、验证、模型调参的顺序把两条路径走一遍代码可以直接改来用。2. 天池o2o优惠券使用预测的问题拆解与标签工程2.1 领券记录到十五天核销标签的构造天池 o2o 优惠券使用预测的核心问题是 o2o 消费券的核销率预测用户在某个时间点领取一张优惠券之后 15 天内会不会到店使用。原始数据里一条记录就是一个领券事实字段主要包括 user_id、merchant_id、coupon_id、distance、date_received以及消费日期 date。date 为空代表这张券领取后没有产生任何核销行为。标签定义我一般直接写成 0/1 二分类import pandas as pd df pd.read_csv(o2o_train.csv, parse_dates[date_received, date]) df[gap_days] (df[date] - df[date_received]).dt.days df[label] ((df[gap_days] 0) (df[gap_days] 15)).astype(int) df.loc[df[date].isna(), label] 0这段代码里先看消费日和领取日之差gap_days两个日期都存在时差值在 0 到 15 天之间才给 1。gap_days为负说明消费发生在领取前属于异常记录按业务规则记为 0date为空的自然是 0。用parse_dates把日期列转成 Timestamp字符串比较在边界条件下容易出错Timestamp 直接做差更稳妥。这里最容易出问题的地方反而不是窗口函数写错而是在后续特征构造中把目标信息带进训练集。比如用 groupby 统计用户全部领券记录得到整体核销率再拼回原表模型会直接学到“这个用户最后都被核销了”线下 AUC 虚高线上分数掉下来才发现是特征泄漏。2.2 用户、商家和距离三类特征怎么落地特征设计不贪多先覆盖三个视角用户会不会用券、商家值不值得去、距离和时机对不对。常用构造方式是这样的特征组典型构造方式对模型的作用用户行为历史领券次数、历史核销次数、核销率区分活跃用户和领了不用的人群商家行为商家被领券总数、商家平均核销率区分热门商家和冷门商家时空特征距离分桶、星期几、是否周末、月中月末捕捉消费动机和出行成本代码上可以这样落df[weekday] df[date_received].dt.weekday df[is_weekend] (df[weekday] 5).astype(int) user_stat df.groupby(user_id)[label].agg([count, sum]) user_stat[user_use_rate] user_stat[sum] / user_stat[count] df df.merge( user_stat.rename(columns{count: user_recv_cnt, sum: user_use_cnt}), left_onuser_id, right_indexTrue, howleft, )user_use_rate是历史核销率对树模型来说它有很强的区分度但这里有一个容易被忽略的前提如果直接在完整训练集上 groupby等于用整段比赛时间窗的总结去预测开头发生的事仍然存在未来泄漏。保险做法是只用date_received之前的记录做滚动聚合或者先按时间段切分成窗口再逐窗口统计。竞赛里很多 baseline 图省事直接在全集上聚合线下指标会乐观提交后就被打回原形。2.3 按领券时间切验证集不要随机切表格类竞赛里换模型不是最难的难的是让线下验证接近线上评估。对 o2o 优惠券这种带强时间顺序的数据我很少用train_test_split(random_state42)随机切分而是直接按领券日期把数据集从中间劈开threshold pd.Timestamp(2016-06-01) train_part df[df[date_received] threshold] valid_part df[df[date_received] threshold]这样做是为了模拟线上真正面对的预测场景拿过去预测未来而不是拿未来帮助过去。随机切分时验证集里很可能含有训练集时间段内同一批用户的后续行为模型在训练时已经“见过”这些用户的消费习惯验证分数天然偏高。时间切分也不是没有代价。如果只切一个时间点节假日效应可能导致验证集和训练集分布差异很大比如训练集覆盖双十一而验证集落在低谷期。常见做法是留出连续一段比如前 6 个月训练、后 2 个月验证同时检查验证集和训练集在星期、节假日的分布是否接近如果差异太大就换成多个时间段交错验证每个训练段都严格截止在验证段之前。3. 达观杯文本智能处理挑战赛的特征与模型选型3.1 长文本分类任务先看长度分布再定截断策略达观杯文本智能处理挑战赛的常见形态是给出一段偏长的文本让模型判断文章属于哪个主题。这类任务和短文本分类不一样短文本信息集中截断影响有限长文本里主题往往分散在不同段落直接取前 N 个字符会把后半段的信号丢掉。拿到数据的第一步我建议打印文本长度分布并画分位数图看 90% 分位在哪再决定截断长度和是否分段建模。想象一下一篇 3000 字的文章主题句可能出现在第 2000 字的位置只保留前 512 个字等于主动放弃答题线索。反过来也不能把所有内容无脑塞进模型词频特征会因此变得稀疏。3.2 用字符级 TF-IDF 稳住基线再谈词向量和 CNN文本智能处理里我习惯按这条路线递进先用 TF-IDF 加线性模型跑出基线再尝试无监督词向量最后才上深度模型。对中文长文本字符级加上 bigram 的 TF-IDF 往往比直接分词更稳分词错误不会在特征层面无限放大。from sklearn.feature_extraction.text import TfidfVectorizer tfidf TfidfVectorizer( ngram_range(1, 2), min_df5, max_features200000, sublinear_tfTrue, ) X_train tfidf.fit_transform(train_text) X_valid tfidf.transform(valid_text)ngram_range(1, 2)同时保留单字和相邻双字组合中文里很多主题词是双字词单字会混入噪声纯多字又会特征爆炸。min_df5过滤掉只出现几次的生僻字max_features200000控制稀疏矩阵的列数防止内存被打爆。sublinear_tfTrue把词频换成1 log(tf)避免长文本里高频词反复出现导致权重失衡。如果想把路再走深一步可以用 Word2Vec 对分词后的文本训练词向量然后对文档内所有词的向量做均值池化得到一维文档向量。这个向量可以拼到 TF-IDF 后面也可以单独喂给逻辑回归或 GBDT。词向量能带来语义相似性但长文本均值池化会稀释关键信息对文章这种长篇幅文本来说往往不如直接做段落级别的表征。方法适合阶段优势局限字符级 TF-IDF快速基线稳定、内存可控、可解释稀疏不保留词序Word2Vec 均值特征补充带语义信息长文本信息被稀释TextCNN中期提升捕捉局部 n-gram 模式调参比线性模型复杂预训练模型冲刺上限上下文建模效果好推理成本高需要 GPU3.3 宏平均 F1 指标下单独寻找每类阈值文本多分类的评估指标如果用的是宏平均 F1那么直接用argmax定类别会吃亏。某个类别样本量少模型给它的概率天生偏保守统一用 0.5 阈值等于让少数类永远抬不起头。我通常对每个类别独立搜索最优阈值把概率输出变成类别判定import numpy as np from sklearn.metrics import f1_score probas model.predict_proba(X_valid) best_thr {} for cls in range(probas.shape[1]): y_bin (y_valid cls).astype(int) best_f1, best_t 0, 0.5 for t in np.arange(0.1, 0.9, 0.05): score f1_score(y_bin, (probas[:, cls] t).astype(int)) if score best_f1: best_f1, best_t score, t best_thr[cls] best_t这段代码逐个类别在 0.1 到 0.9 的区间里搜索 F1 最高的阈值。注意阈值搜索只允许在验证集上做不能在测试集上反复调整否则又会变成变相泄漏。得到best_thr后预测时对每个类别独立判断不再依赖argmax。这个方法在 o2o 优惠券这样的二分类任务里同样适用尤其当业务更关注核销召回时可以用阈值把正类召回拉高代价是精度下降需要结合业务成本权衡。4. 两份竞赛代码的落地实现与参数调优4.1 用 LightGBM 复现 o2o 优惠券预测的完整流程o2o 优惠券使用预测的表格数据适合用 GBDT 一族模型收尾。LightGBM 训练速度快对缺失值和异常值容忍度高特征工程里构造出的距离分桶、用户核销率都能直接吃进去。import lightgbm as lgb from sklearn.metrics import roc_auc_score features [ distance, weekday, is_weekend, user_recv_cnt, user_use_cnt, user_use_rate, merchant_recv_cnt, merchant_use_rate, ] params { objective: binary, metric: auc, learning_rate: 0.05, num_leaves: 31, min_child_samples: 50, feature_fraction: 0.8, bagging_fraction: 0.8, bagging_freq: 1, verbose: -1, } model lgb.train( params, lgb.Dataset(train_part[features], train_part[label]), num_boost_round1000, valid_sets[lgb.Dataset(valid_part[features], valid_part[label])], callbacks[lgb.early_stopping(100), lgb.log_evaluation(50)], ) pred model.predict(valid_part[features], num_iterationmodel.best_iteration) print(roc_auc_score(valid_part[label], pred))AUC 对阈值不敏感所以类别不均衡不会直接影响评估但会体现在最终概率分布的偏移上。num_leaves31控制单棵树复杂度min_child_samples50防止叶子节点样本太少导致过拟合feature_fraction和bagging_fraction分别是列采样和行采样适合特征冗余的场景。early_stopping(100)在验证分数连续 100 轮不提升时终止训练测试集上预测时也要用best_iteration避免把后面过拟合的树带进来。4.2 用 TF-IDF 加 Linear SVM 跑通达观杯基线达观杯代码里另一个常见做法是用 TF-IDF 配合 LinearSVC 做多分类。文本经过 TF-IDF 后是高维稀疏向量线性核 SVM 已经足够RBF 核在这种数据上又慢又不会带来明显提升。from sklearn.svm import LinearSVC from sklearn.pipeline import make_pipeline from sklearn.calibration import CalibratedClassifierCV pipeline make_pipeline( TfidfVectorizer(ngram_range(1, 2), min_df5, max_features200000, sublinear_tfTrue), CalibratedClassifierCV(LinearSVC(max_iter2000), cv3), ) pipeline.fit(X_text_train, y_text_train) probas pipeline.predict_proba(X_text_valid)LinearSVC本身不输出概率只能输出到超平面的距离。CalibratedClassifierCV用交叉验证把 decision function 校准成概率校准后的结果才能配合阈值搜索和之后的模型融合。cv3表示内部用三折做概率校准代价是训练时间变为原来的三倍这在文本基线阶段可以接受但大规模调参时建议把校准放在最后一步做。4.3 两个任务通用的参数表和早停策略把两个场景里最容易影响结果的参数单独列出来调参时我一般先固定一批只动其中一个避免参数之间互相干扰。场景参数推荐值作用o2o 表格模型num_leaves31 到 63树复杂度越大拟合能力越强o2o 表格模型min_child_samples50 到 100叶子最小样本数控制过拟合o2o 表格模型feature_fraction0.7 到 0.9列采样缓解冗余特征文本基线ngram_range(1, 2)单字加双字兼顾噪声和信号文本基线min_df3 到 5过滤低频字符稳定分类面文本基线sublinear_tfTrue平滑长文本词频偏斜通用learning_rate0.01 到 0.1学习率越低需要的迭代轮数越多通用early_stopping50 到 100防止在验证集上过拟合树模型里learning_rate和num_boost_round需要配套调学习率减半通常要把迭代轮数翻倍。min_child_samples设得越小模型越容易记住训练集的单点噪声文本模型这边的max_features需要按内存上限调整200000 是一个相对安全的起始点。5. 用 OOF 预测检查线上线下分数漂移5.1 为两个场景分别设置 OOF 检查点竞赛做到后期最怕的不是模型效果不好而是线下 AUC 很高、线上分数却明显偏低。我用一个简单手段来排查多折交叉验证生成 OOF 预测让每个样本都只被没有见过它的模型预测然后把 OOF 分数和线上分数对比。如果差值超过 0.02优先怀疑特征时间泄漏。from sklearn.model_selection import StratifiedKFold import numpy as np oof np.zeros(len(df)) skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) for tr_idx, va_idx in skf.split(df[features], df[label]): tr_data lgb.Dataset(df.iloc[tr_idx][features], df.iloc[tr_idx][label]) va_data lgb.Dataset(df.iloc[va_idx][features], df.iloc[va_idx][label]) m lgb.train( params, tr_data, num_boost_round2000, valid_sets[va_data], callbacks[lgb.early_stopping(100)], ) oof[va_idx] m.predict(df.iloc[va_idx][features], num_iterationm.best_iteration) print(roc_auc_score(df[label], oof))OOF 分数和线上分数接近说明验证流程可靠可以放心做下一轮特征迭代。OOF 明显高于线上时我会先检查有没有把消费日期、核销金额这类未来字段拼进特征再检查验证集切分是否把同一天的数据同时放进了训练集和验证集。文本分类场景里还有另一种漂移来源阈值搜索在验证集上过拟合换到线上新分布后失效。这种情况下把 OOF 阈值和验证集阈值对比若相差过大就减少搜索粒度或改用更保守的 0.5。多折 OOF 在 o2o 优惠券使用预测和达观杯文本智能处理里都能当最终提交前的一致性检查工具它比单折验证更能反映模型在不同数据子集上的稳定程度。本文还有配套的精品资源点击获取