简介这份资料包是阿里天池与Datawhale联合举办的二手车交易价格预测竞赛的优胜奖方案源码与项目说明面向计算机科学、应用数学、电子信息工程等相关专业的学生与研究人员可作为课程设计、毕业设计或学术竞赛的参考素材帮助理解数据挖掘类赛题的完整建模流程与解题思路。压缩包共15个文件约610KB以Python脚本为主涵盖特征工程、神经网络生成、树模型生成、LightGBM建模及模型融合等模块另含CSV预测结果、Markdown项目说明与依赖配置文件结构清晰便于复现。目前已有42人学习。读者可从中获得一套完整的获奖方案代码与项目文档对照理解特征构造、多模型训练与stacking融合的实现细节并在此基础上自主调试、修改与优化拓展同类预测问题的解决能力。资源来源于网络分享仅供学习交流请勿用于商业用途。1. 二手车价格预测竞赛从数据到优胜方案的落地路径二手车价格预测是阿里天池与 Datawhale 联合推出的经典回归竞赛核心任务是根据车辆的品牌、里程、排量、上牌时间、变速箱类型等字段预测二手车的成交价格。这个赛题看起来像普通的表格回归但真正做过的人都知道它的难点不在模型本身而在数据清洗和特征构造——同一款车在不同城市、不同时间、不同车况下价格差异巨大原始字段里还藏着大量缺失值、异常值和格式不统一的问题。这套优胜奖方案源码的价值在于它把从原始数据到最终提交的完整链路拆解得足够细包括缺失值处理策略、类别特征编码方式、时间特征的拆解、目标编码的构造以及 LightGBM 与 XGBoost 的融合逻辑。适合已经跑过一两个 Kaggle 入门赛、想系统学习表格回归完整流程的从业者也适合需要一套可复现 baseline 来快速迭代的工程师。下面按实际落地顺序把每个环节的参数和踩坑点讲清楚。2. 数据清洗与特征工程把原始字段变成模型能吃的格式2.1 先搞清楚哪些字段是「脏」的拿到数据第一步不是急着跑模型而是把每个字段的分布和缺失情况过一遍。二手车数据里最典型的脏字段有三类power功率经常出现 0 值kilometer里程偶尔有负数notRepairedDamage未修复损伤用「-」表示缺失而不是 NaN。这些如果不处理树模型会把 0 当成真实功率去分裂节点直接拉低预测精度。我一般会先写一段统计脚本把每个字段的缺失率、唯一值数量、最小最大值打出来再决定用哪种填充策略。数值型字段用中位数填充比均值稳因为二手车价格分布本身右偏均值容易被高价车拉偏。类别型字段如果缺失率低于 5%直接填众数高于 5% 就单独设一个「未知」类别让模型自己学。import pandas as pd import numpy as np def clean_basic(df): # power 为 0 的视为缺失用品牌分组中位数填充 df[power] df[power].replace(0, np.nan) df[power] df.groupby(brand)[power].transform( lambda x: x.fillna(x.median()) ) # 里程负数直接取绝对值再对极端值做截断 df[kilometer] df[kilometer].abs() df[kilometer] df[kilometer].clip(upper30) # notRepairedDamage 的 - 转成 NaN再填众数 df[notRepairedDamage] df[notRepairedDamage].replace(-, np.nan) df[notRepairedDamage] df[notRepairedDamage].fillna( df[notRepairedDamage].mode()[0] ) return df这段代码的逻辑是先按品牌分组填充功率因为不同品牌的功率分布差异很大用全局中位数会引入偏差里程截断到 30 万公里超过这个值的样本极少且可能是录入错误未修复损伤字段填众数是因为它只有两个取值缺失比例不高时众数最稳。参数上clip(upper30)的上限可以根据实际数据的 99 分位数调整不必死守 30。2.2 时间特征拆解与目标编码regDate上牌时间和creatDate数据创建时间是这套数据里信息量最大的两个字段。直接把它们当数值扔给模型效果一般因为「2015 年上牌」和「2018 年上牌」的差异不是线性的。常见做法是拆成「车龄」和「上牌月份」车龄用creatDate - regDate算出来月份单独作为一个类别特征。目标编码Target Encoding是这套方案里提升最明显的特征构造手段。具体做法是按brand、model、bodyType等组合分组计算每组的价格均值再用平滑系数防止过拟合。平滑公式是(group_mean * group_count global_mean * k) / (group_count k)k 一般取 10 到 20 之间。def target_encode(train, test, cols, target, k15): global_mean train[target].mean() for col in cols: agg train.groupby(col)[target].agg([mean, count]) smooth (agg[mean] * agg[count] global_mean * k) / (agg[count] k) train[f{col}_te] train[col].map(smooth) test[f{col}_te] test[col].map(smooth) # 测试集里没出现过的类别用全局均值兜底 test[f{col}_te] test[f{col}_te].fillna(global_mean) return train, test这里的关键参数是 kk 越大平滑越强适合类别样本量少的情况k 越小越信任组内均值适合样本量大的类别。我一般会先用 15 跑一版看验证集 RMSE 再微调。注意目标编码必须在训练集上算统计量再映射到测试集如果先合并再算会泄露标签这是新手最容易翻车的地方。2.3 类别特征编码的选型理由LightGBM 原生支持类别特征但前提是把字段转成category类型而不是做 One-Hot。One-Hot 在model这种高基数特征上会炸出几千列训练慢且稀疏。我一般对基数低于 10 的字段用 One-Hot高于 10 的用目标编码或直接交给 LightGBM 的categorical_feature参数处理。cat_cols [brand, bodyType, fuelType, gearbox, notRepairedDamage] for col in cat_cols: df[col] df[col].astype(category) # LightGBM 训练时指定类别列 lgb_params { objective: regression, metric: rmse, categorical_feature: cat_cols, learning_rate: 0.05, num_leaves: 64, feature_fraction: 0.8, bagging_fraction: 0.8, bagging_freq: 5, verbose: -1 }num_leaves设 64 是经验值二手车数据量在 15 万条左右叶子太多容易过拟合太少欠拟合。feature_fraction和bagging_fraction都设 0.8 是为了增加随机性配合 5 轮 bagging 一次验证集 RMSE 通常能降 0.01 到 0.02。学习率 0.05 配合 2000 轮早停基本能收敛。3. 模型训练与融合LightGBM 和 XGBoost 怎么搭才不白干3.1 单模型调参的优先级表格回归任务里调参的收益排序是学习率 叶子数 特征采样 正则项。先把学习率降到 0.05 以下再调num_leaves和max_depth最后加lambda_l1和lambda_l2。我见过很多人一上来就网格搜min_child_samples其实这个参数在数据量超过 10 万时影响很小不如把时间花在特征上。XGBoost 这边参数逻辑类似但max_depth通常设 6 到 8比 LightGBM 的叶子数更保守。两个模型的验证集 RMSE 一般能跑到 0.28 到 0.30 之间价格取 log 后具体取决于特征工程做到什么程度。import xgboost as xgb xgb_params { objective: reg:squarederror, eval_metric: rmse, max_depth: 7, learning_rate: 0.05, subsample: 0.8, colsample_bytree: 0.8, lambda: 1.0, alpha: 0.1, tree_method: hist, seed: 42 } dtrain xgb.DMatrix(X_train, y_train) dvalid xgb.DMatrix(X_valid, y_valid) model xgb.train(xgb_params, dtrain, num_boost_round3000, evals[(dvalid, valid)], early_stopping_rounds100, verbose_eval200)tree_method设hist比默认的auto快很多尤其在特征数超过 100 时。early_stopping_rounds100是防止过拟合的后悔药验证集 RMSE 连续 100 轮不降就停。注意 XGBoost 的lambda和alpha分别对应 L2 和 L1 正则二手车数据里 L2 设 1.0 左右比较稳L1 设 0.1 能筛掉一些噪声特征。3.2 融合策略加权平均还是 Stacking两个模型跑完后最简单的融合是加权平均。权重不用手动试用验证集 RMSE 反推RMSE 越小的模型权重越大。我一般用w1 rmse2 / (rmse1 rmse2)这种反比权重比 0.5 对 0.5 稳定。Stacking 的做法是把两个模型的验证集预测值作为新特征再训一个线性回归或小 LightGBM 做元学习器。但二手车数据量不算大Stacking 容易过拟合我试过几次收益不如加权平均明显。如果非要上 Stacking元学习器用 Ridge 回归alpha 设 1.0 以上别用复杂模型。from sklearn.linear_model import Ridge # 假设 lgb_pred 和 xgb_pred 是验证集预测 stack_X np.column_stack([lgb_pred, xgb_pred]) meta Ridge(alpha1.0) meta.fit(stack_X, y_valid) # 测试集同样拼接后预测 test_stack np.column_stack([lgb_test_pred, xgb_test_pred]) final_pred meta.predict(test_stack)融合前记得把两个模型的预测值都做同样的后处理如果训练时对价格取了 log预测完要np.exp还原。这一步漏掉的话提交的 RMSE 会直接爆到几万血泪经验。3.3 交叉验证的折数和种子选择5 折交叉验证是标配但二手车数据里如果按时间划分效果会比随机划分更接近真实场景。我一般用KFold(n_splits5, shuffleTrue, random_state42)然后跑 3 个不同种子取平均。种子不同带来的 RMSE 波动大概在 0.005 左右取平均能再降 0.002 到 0.003。注意验证集不能参与目标编码的统计量计算每一折都要在训练折上重新算。如果偷懒用全量数据算目标编码再分折验证集 RMSE 会虚低线上提交直接翻车。这个坑我踩过两次后来写了个TargetEncoder类强制按折计算才根治。4. 避坑与排查二手车价格预测里最容易翻车的 5 个点4.1 目标编码泄露导致验证集虚高现象本地验证集 RMSE 跑到 0.25提交到天池线上只有 0.32差距巨大。原因目标编码在划分验证集之前就用全量数据算了统计量验证集的标签信息泄露到了特征里。解决把目标编码封装成 sklearn 的TransformerMixin在fit里只算训练折的统计量transform时映射到验证折和测试集。或者用category_encoders库的TargetEncoder它自带按折计算逻辑。4.2 价格取 log 后忘记还原现象提交文件里的预测价格全是 0 到 10 之间的小数线上 RMSE 几万。原因训练时对price做了np.log1p预测完直接输出没有做np.expm1还原。解决在生成提交文件前加一行pred np.expm1(pred)并且检查预测值的范围是否在合理区间比如 0 到 200 万。我一般会打印预测值的describe()确认 min 和 max 不离谱再提交。4.3 类别特征没转 category 直接喂 LightGBM现象LightGBM 训练报错pandas dtypes must be int, float or bool或者训练能跑但 RMSE 比预期高很多。原因LightGBM 的categorical_feature参数要求字段是int或category类型如果是object字符串会报错如果转成了整数但没声明类别模型会当成数值特征处理分裂逻辑完全错误。解决统一用astype(category)转成 pandas 类别类型并在lgb_params里显式指定categorical_feature列表。XGBoost 不支持原生类别特征需要用目标编码或 One-Hot 替代。4.4 缺失值填充用了全局均值现象模型在验证集上表现还行但线上测试集 RMSE 波动很大。原因power字段用全局均值填充忽略了品牌和车型的差异导致特征分布和真实分布不一致。解决数值型缺失值优先用分组中位数填充分组字段选brand或model。如果分组后仍有缺失再用全局中位数兜底。填充完打印一下填充前后的分布对比确认没有引入明显偏差。4.5 融合时权重手动拍脑袋现象LightGBM 单模型 RMSE 0.29XGBoost 0.30融合后反而变成 0.31。原因两个模型用了相同的特征和相似的参数预测结果高度相关简单平均没有带来多样性收益。解决融合前先算两个模型预测值的相关系数如果超过 0.95说明模型太像融合意义不大。要么换不同特征子集训练要么换不同算法比如加一个 CatBoost要么直接选单模型提交。融合的前提是模型之间有差异不是数量多就好。5. 从优胜方案里能带走的三个进阶技巧5.1 用伪标签把测试集里的高置信样本加回训练天池竞赛的测试集通常有几千到几万条这些数据虽然没有标签但可以用训练好的模型预测出伪标签筛选预测置信度高的样本比如预测值在训练集价格分布的中位数附近加回训练集重新训练。我一般会跑两轮伪标签第一轮用 0.9 分位以上的置信样本第二轮放宽到 0.8。这个技巧在二手车数据上能带来 0.003 到 0.005 的 RMSE 下降但要注意别把噪声样本也加进去否则适得其反。# 第一轮伪标签 pseudo_pred model.predict(X_test) confidence np.abs(pseudo_pred - np.median(y_train)) / np.std(y_train) high_conf_idx confidence 0.5 # 选靠近中位数的样本 X_aug pd.concat([X_train, X_test[high_conf_idx]]) y_aug np.concatenate([y_train, pseudo_pred[high_conf_idx]]) # 用增广数据重新训练 model_aug lgb.train(lgb_params, lgb.Dataset(X_aug, y_aug), num_boost_round2000)置信度的定义可以灵活调整核心思路是选模型「有把握」的样本。如果测试集分布和训练集差异大伪标签反而会引入偏差这时候先做分布对齐再考虑伪标签。5.2 特征重要性筛选的阈值怎么定LightGBM 训练完可以输出feature_importance但直接按重要性排序删特征容易删过头。我一般看两个指标一是重要性低于 0.001 的特征直接删二是如果两个特征相关系数超过 0.9保留重要性高的那个。二手车数据里power和displacement经常高度相关留一个就够。筛选策略阈值预期效果重要性过滤 0.001减少 10% 到 15% 特征RMSE 基本不变相关性过滤 0.9减少 5% 到 8% 特征训练速度提升组合过滤两者叠加特征数减少 20% 左右RMSE 可能降 0.001筛选完记得重新跑一遍交叉验证确认 RMSE 没有明显上升再提交。特征筛选的收益主要在训练速度和推理速度上对精度的提升有限别指望靠删特征拿优胜奖。5.3 提交前的最后检查清单生成提交文件前我习惯过一遍这个清单预测值范围是否在训练集价格的 min 和 max 之间提交文件的列名和样例文件是否一致行数是否和测试集一致有没有 NaN 或 Inf。这四步花不了两分钟但能避免 90% 的低级翻车。有一次我忘了检查行数提交文件少了一行线上直接报格式错误白白浪费一次提交机会。这套方案源码我前后复现过三遍每次都能发现新的细节可以优化。二手车价格预测这个赛题的好处是数据量适中、特征工程空间大适合反复打磨。如果你正在做这个方向建议先把第 2 章的特征工程跑通再按第 3 章的融合逻辑搭一版 baseline最后用第 5 章的技巧做增量优化。别一上来就追求复杂模型把数据和特征吃透优胜奖方案里的每一个决策都有它的道理。希望帮到你。本文还有配套的精品资源点击获取