从零到Top 12%:我是如何用Python和基础模型搞定天池复购预测的(附完整代码)
第一次参加天池大赛时,面对5600多支队伍的激烈竞争,我作为一个仅有Python基础的数据科学爱好者,内心充满忐忑。但最终,仅用逻辑回归和决策树这类基础模型,我意外斩获前12%的排名。这篇文章将完整还原我的实战路径——从数据清洗的每个细节到特征工程的思考逻辑,再到模型调优的踩坑记录。不同于复杂算法的堆砌,这里只有可复现的代码和经过验证的有效策略。
1. 赛题理解与数据初探
复购预测本质上是一个二分类问题:给定用户在双十一期间的新客行为数据,预测其未来半年内是否会再次购买。评估指标采用AUC值,这意味着模型需要准确排序用户的复购概率而非简单判断是与否。
数据集包含四个关键文件:
train_format1.csv:训练集用户ID与商家ID对应关系及标签user_info_format1.csv:用户性别、年龄等静态信息user_log_format1.csv:用户6个月内的详细行为日志test_format1.csv:测试集数据
初次加载数据时,我发现了几个关键问题:
import pandas as pd user_log = pd.read_csv('data_format1/user_log_format1.csv') print(f"行为记录缺失比例:{user_log.isnull().sum().sum()/len(user_log):.2%}") print(f"时间跨度:{user_log['time_stamp'].nunique()}个离散时间点")输出显示约15%的行为记录存在缺失,且时间戳被离散化为1-31的整数。这直接影响了后续的特征设计策略。
提示:天猫数据中的time_stamp字段并非真实日期,而是经过脱敏的序列编号,这要求我们放弃常规的时间序列分析方法。
2. 特征工程实战:从原始数据到模型输入
2.1 用户基础特征构建
面对用户信息表中的年龄和性别字段,我采用了分层填充策略:
def process_user_info(df): # 性别处理:-1→未知,0→女,1→男 df['gender'] = df['gender'].replace(-1, np.nan) # 年龄分段:将7和8合并为≥50岁 df['age_range'] = df['age_range'].replace({7:8, -1:np.nan}) # 基于商家维度的众数填充 merchant_gender = df.groupby('merchant_id')['gender'].agg(lambda x: x.mode()[0]) df['gender'] = df.apply(lambda row: merchant_gender[row['merchant_id']] if pd.isna(row['gender']) else row['gender'], axis=1) return df2.2 行为特征聚合
用户日志中包含四种行为类型:
- 0:点击
- 1:加购
- 2:购买
- 3:收藏
我设计了三级聚合策略:
- 用户-商家维度统计基础行为次数
- 计算行为占比等衍生指标
- 加入时间维度上的行为变化趋势
def create_action_features(user_log): # 基础行为计数 action_counts = user_log.groupby(['user_id','seller_id','action_type'])['item_id'].count().unstack() action_counts.columns = ['clicks','add_to_cart','purchases','favorites'] # 行为占比特征 action_counts['total_actions'] = action_counts.sum(axis=1) for col in ['clicks','add_to_cart','purchases','favorites']: action_counts[f'{col}_ratio'] = action_counts[col]/action_counts['total_actions'] return action_counts.reset_index()2.3 关键特征清单
最终生成的核心特征包括:
| 特征类别 | 示例特征 | 生成方式 |
|---|---|---|
| 用户属性 | 年龄分段、性别 | 原始数据清洗 |
| 行为统计 | 点击次数、加购率 | 分组聚合计算 |
| 时间模式 | 最后行为间隔 | 时间戳差分 |
| 交叉特征 | 品类偏好指数 | 联合用户与商品类目 |
3. 模型构建与优化
3.1 基础模型对比
我首先在相同特征集上测试了三种基础模型的表现:
from sklearn.linear_model import LogisticRegression from sklearn.tree import DecisionTreeClassifier from sklearn.ensemble import RandomForestClassifier models = { 'Logistic Regression': LogisticRegression(max_iter=1000), 'Decision Tree': DecisionTreeClassifier(max_depth=5), 'Random Forest': RandomForestClassifier(n_estimators=100) } for name, model in models.items(): model.fit(X_train, y_train) pred = model.predict_proba(X_val)[:,1] score = roc_auc_score(y_val, pred) print(f"{name} AUC: {score:.4f}")初步结果显示:
- 逻辑回归:0.6213
- 决策树:0.5987
- 随机森林:0.6332
3.2 逻辑回归的逆袭
虽然随机森林表现最好,但考虑到比赛后期的模型融合需求,我决定优化逻辑回归作为基础学习器。关键改进点:
- 特征标准化:对连续型特征进行RobustScaler处理
- 类别特征编码:对年龄分段等有序变量采用Target Encoding
- 正则化调优:通过网格搜索确定最佳L2惩罚系数
from sklearn.preprocessing import RobustScaler from sklearn.compose import ColumnTransformer numeric_features = ['clicks','purchases_ratio','last_action_interval'] categorical_features = ['age_range','gender'] preprocessor = ColumnTransformer( transformers=[ ('num', RobustScaler(), numeric_features), ('cat', TargetEncoder(), categorical_features) ]) pipeline = Pipeline([ ('preprocessor', preprocessor), ('classifier', LogisticRegression(C=0.3, solver='saga')) ]) # 五折交叉验证 cv_scores = cross_val_score(pipeline, X_train, y_train, cv=5, scoring='roc_auc') print(f"平均AUC: {np.mean(cv_scores):.4f} (±{np.std(cv_scores):.4f})")优化后的逻辑回归AUC提升至0.6287,且训练速度比随机森林快20倍。
4. 比赛技巧与经验总结
4.1 有效特征筛选
通过特征重要性分析,我发现三个被低估但实际有效的特征:
行为集中度:用户在该商家行为占其总行为的比例
df['merchant_action_ratio'] = df['total_actions'] / df.groupby('user_id')['total_actions'].transform('sum')时间衰减权重:越接近双十一的行为权重越高
df['weighted_actions'] = df['clicks']*(1 + 0.1*df['time_stamp'])跨商家对比:用户在该商家的行为次数与平均值的比值
4.2 避免过拟合的策略
- 采用时间序列验证:按时间划分训练/验证集
- 限制决策树深度:设置max_depth≤5
- 早停机制:监控验证集AUC不再提升时终止训练
4.3 完整代码结构
项目最终的文件组织如下:
/repo │── data/ # 原始数据 │── features/ # 特征工程输出 │── notebooks/ │ ├── 01_eda.ipynb # 数据探索 │ ├── 02_features.ipynb # 特征工程 │ └── 03_model.ipynb # 模型训练 │── utils/ # 工具函数 │── requirements.txt # 依赖库 │── submit.py # 生成提交文件在最终提交的版本中,我融合了逻辑回归和随机森林的预测结果,采用简单的加权平均:
lr_pred = lr_model.predict_proba(test_features)[:,1] rf_pred = rf_model.predict_proba(test_features)[:,1] final_pred = 0.6*lr_pred + 0.4*rf_pred这个看似简单的组合策略,让我的成绩从Top 20%提升到了Top 12%。参赛过程中最大的体会是:在数据竞赛中,精心设计的特征往往比复杂的模型更能带来突破。现在回看,那些熬夜调试XGBoost参数的时间,或许更应该花在深入理解业务逻辑上。