ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

二手车价格预测实战:特征工程与模型融合完整方案

二手车价格预测实战:特征工程与模型融合完整方案 简介本资源是阿里天池与Datawhale联合举办的二手车价格预测比赛优胜奖团队的完整代码方案总结面向机器学习初学者、数据竞赛参赛者及工业界建模实践者聚焦真实场景下的回归建模全流程——从原始车辆特征出发精准预测二手市场价格。压缩包共11个文件8个Python脚本承担数据预处理、特征工程、多模型训练与融合等核心任务1个CSV提供预测结果示例1个Markdown格式README说明整体流程与复现要点1个TXT列出依赖环境总大小仅600KB轻量易部署。已有251人下载学习可直接运行复现获奖方案涵盖缺失值智能填充、IQR异常值清洗、One-hot与时间特征构造、XGBoost/LightGBM模型调优、K折交叉验证及Stacking融合策略等关键实现代码模块清晰、注释充分是理解工业级回归建模落地逻辑的优质参考样本。 兄弟抽空把阿里天池和Datawhale联合办的二手车价格预测比赛那套优胜奖方案代码重新翻出来整理了一遍。说实话这种比赛每年都有不少但价格预测这个方向特别适合练手尤其是对刚接触数据挖掘比赛的朋友——数据量不大、字段含义相对清楚、评价指标固定能完整体验一遍从数据分析到特征工程再到模型融合的完整流程。这篇就把当时那套方案的核心思路、代码组织方式和复盘时觉得最关键的一些点写出来给后面想打这类比赛或者想系统学一下回归建模的朋友做个参考。1. 这个比赛的真正难点不是调参而是理解价格背后的业务逻辑1.1 赛题回顾与评估指标先快速过一下赛题。官方给了一批二手车交易数据每条样本对应一辆车包含车辆的基础属性品牌、车型、车身类型、燃油类型、变速箱、功率、里程、注册日期、上架日期、售卖地区编码以及一串匿名处理过的特征 v_0 到 v_15。任务非常直接根据这些信息预测车辆的成交价格。评估指标用的是 MAE也就是平均绝对误差。这个选择其实对建模策略有非常大的影响。MAE 计算的是预测值和真实值之间绝对差的平均值它不像 RMSE 那样会对大误差样本施以平方级别的惩罚。换句话说模型不需要为了迁就几个极端高价车或者低价车而拼命拉高或者压低预测它对异常值相对宽容但对“绝大多数样本的预测精度”要求更高。这个差异直接决定了三件事目标值要不要做变换我当时尝试了对数变换从验证集结果看log1p 之后训练出来的模型在 MAE 指标上普遍能提升 3% 左右原因后面详细说。异常样本要不要清洗有一些明显不合理的价格比如价格接近 0 或者功率异常大在 MAE 下影响有限但清洗后模型会更稳定。模型融合的权重怎么分配因为 MAE 对野值宽容融合时不太需要用复杂的加权策略去“救”那些极端样本简单的均值融合往往就够了。1.2 数据集的几个反直觉现象再来看数据集本身。训练集大约 15 万条测试集 5 万条字段看上去不多但实际跑起来会发现几个比较隐蔽的问题。第一个是类别特征的分布极度不均衡。比如 seller 字段绝大多数样本都是同一个取值另一个取值只有个位数offerType 字段也一样基本可以认为没有信息量。这类字段如果直接丢进模型不仅没有帮助还会增加过拟合风险。第二个是匿名特征 v_0 到 v_15 并不是传统意义上的“数值连续变量”。从分布看它们更像是某种经过编码的统计量有些列大量重复值有些列之间存在强相关性。后来对这几列做了相关性矩阵发现 v_1 和 v_4、v_7 和 v_10 之间的相关系数能到 0.95 以上。这种字段不能一股脑全塞进去需要做筛选或者降维。第三个是关于时间的细节。regDate 表示注册日期creatDate 表示上架日期两者相减可以得到车辆从注册到上架的时间差也就是“库存时长”。这个特征对价格的影响其实不小新车上架和挂了很久才卖出去的车定价逻辑完全不同。这个字段在原始数据里没有直接给出很多新手会忽略。1.3 为什么说业务理解比模型更重要打比赛和做学术研究最大的区别在于比赛里你得在固定数据集上把分数榨干而数据集本身是别人从真实业务场景里采样出来的所以业务逻辑的理解会直接决定你特征工程的上限。拿这个比赛举例。二手车的定价逻辑本质上跟三个因素强相关车龄、品牌保值率、行驶里程。这三个因素在数据里分别对应 regDate、brand、kilometer。如果你只把这三个字段当成独立的普通特征丢进模型模型也能学到一部分规律但如果你能主动构造“车龄”这个字段用上架日期减注册日期并且把品牌和车龄做交叉模型的效果会有明显提升。我在比赛过程中最深的一点体会调参只能在你特征工程做到位之后才有意义特征没做够调参都是在原地打转。2. 数据勘探从价格分布到隐藏的坏样本2.1 价格分布与目标变换正式建模之前第一步永远是看目标变量的分布。我画了 price 的直方图典型的右偏长尾分布绝大多数车集中在 0 到 10 万这个区间但也有少量车价格到了 30 万甚至更高。这种分布直接扔给模型会有两个问题一是模型在训练时会把大量“注意力”放在拟合长尾的大价格样本上导致普通样本的预测精度下降二是 MAE 作为损失函数时长尾样本会把整体误差拉高。解决办法是把目标值做 log 变换也就是训练时预测 log(price)在提交时对预测结果做 expm1 还原。实际对比效果不做变换五折交叉验证 MAE 约 7240对 price 做 log1p 变换五折 MAE 约 6980这个差距非常可观而且几乎是白拿的。2.2 异常值清洗的判断标准价格分布里能看到一些价格非常低的样本比如几百块的“车”。这些样本大概率是数据采集错误、过户价填错或者特殊交易场景比如亲属过户把它们留在训练集里会让模型学到错误的规律。我当时的清洗策略比较保守只处理三类明确异常的样本price 等于 0 或者小于 100 的样本直接删掉。power 大于 600 的样本二手车市场里民用车功率超过 600 马力的基本不存在这些属于录入错误。车龄为负数上架日期早于注册日期的样本也直接删掉。清洗的样本量大概有 300 多条在 15 万的数据量里占比不大但对模型稳定性的提升却很显著。这里特别想提醒的是清洗要慎重不要凭感觉一刀切。比如价格低的样本先确认下它是不是集中在某些特定品牌或者特定车型里如果是那可能不是异常而是正常业务特征。2.3 时间字段的信息提取regDate 和 creatDate 都是日期格式我提取了三组特征车龄creatDate 的年份减去 regDate 的年份按年计算注册月份regDate 的月份这个字段对价格影响很弱但可以作为补充信息上架年份creatDate 的年份能反映当时的市场环境比较值得关注的是车龄和价格的负相关关系。直觉上车龄越大价格越低但实际数据里这个关系不是线性的前 3 年的折旧速度非常快3 到 6 年逐渐放缓6 年以上价格趋于平稳。树模型能自动捕捉这种非线性关系所以不需要手动分箱但如果你用线性模型或者神经网络就得考虑做分箱或者样条变换。3. 特征工程的三个关键决策3.1 类别特征不是直接编码就行类别特征的处理是特征工程里最容易踩坑的地方。比赛中涉及的类别字段有 brand品牌、model车型、bodyType车身类型、fuelType燃油类型、gearbox变速箱、regionCode地区编码。最直接的处理方式是用 LabelEncoder 或者 OneHotEncoder。但在实际验证中直接 LabelEncoder 的性能其实一般因为树模型会把它当成有序变量来处理如果类别本身没有天然的顺序关系模型就需要额外分裂来拟合消耗树的深度。OneHot 的问题是类别太多brand 有 40 多个model 有 300 多个会让特征矩阵变得稀疏训练速度变慢。我们最后采用的是频次编码 均值编码的组合方式对 brand、model、regionCode 做频次编码对 bodyType、fuelType、gearbox 做 LabelEncoder对 model 做目标均值编码也就是用历史样本里该车型的平均价格作为编码值均值编码在树模型里效果通常不错但要注意防止过拟合。我用了五折交叉的方式做均值编码每一折只用另外四折的数据来计算均值这样能有效避免泄露。3.2 匿名特征 v_0 到 v_15 的正确打开方式这 16 个匿名特征是很多人头疼的地方因为它们没有业务含义你不知道它们背后代表什么。但如果直接全部丢掉又觉得浪费。我的处理方式是分三个层次先做相关性分析把相关性超过 0.9 的字段做去重只保留其中一个降低冗余。对剩下的字段做 PCA保留前 6 个主成分作为新的合成特征加入模型。把 v_0 到 v_15 的整体均值、标准差作为额外的统计特征。从验证集效果看加入这些匿名特征后 MAE 大概能降低 1.5% 左右不算特别多但在这个分数竞争激烈的比赛里每一个千分点的提升都可能决定最终排名。3.3 业务特征从“车况”到“性价比”除了上面这些常规特征还额外构造了几个业务向的特征每万公里折旧率用当前价格除以车龄和里程的综合值用来刻画“这辆车值不值”品牌市场占有率统计每个品牌在训练集中的样本占比占比越高代表市场流通性越好通常更保值车型价格标准差计算同车型历史成交价格的标准差反映该车型的价格波动性这些特征本质上是对原始字段的交叉组合虽然树模型理论上能自己找到这些组合但手动显式构造出来的特征往往能引导模型更快地学到有价值的模式。实际验证中业务特征整体贡献了大约 2% 的 MAE 提升。4. 模型选型与融合单模型上限与融合收益4.1 三个基学习器的表现对比这次比赛我主要用了三个模型LightGBM、XGBoost 和 CatBoost。很多人会问这三个选哪个好我的经验是不要纠结全部试一遍用数据说话。在同一个五折交叉验证框架下三个模型的单模型表现如下模型五折 MAE训练耗时LightGBM6992约 8 分钟XGBoost7105约 15 分钟CatBoost7038约 20 分钟LightGBM 在速度上优势明显精度也最好所以后续的调参和特征迭代都以它为主。但三个模型的错误模式不完全相同融合之后的效果通常比单模型更好。4.2 调参思路先粗后细先结构后正则调参这个事情很多人一上来就用网格搜索非常浪费时间。我的思路是分阶段进行第一阶段固定学习率为 0.1用比较保守的参数比如 num_leaves31max_depth-1跑一轮确定大致的树数量和 early stopping 轮数。第二阶段调整结构类参数主要是 num_leaves 和 max_depth。num_leaves 控制模型的复杂度太大容易过拟合太小欠拟合。我会在 16、31、63、127 几个值之间试观察验证集的 MAE 变化曲线。第三阶段调整正则化参数包括 min_data_in_leaf、feature_fraction、bagging_fraction 和 lambda_l2。这个阶段的目标是在不降低验证集精度的前提下尽量提高模型的泛化能力。最终 LightGBM 的核心参数如下{ objective: regression, metric: mae, learning_rate: 0.02, num_leaves: 63, max_depth: 8, min_data_in_leaf: 50, feature_fraction: 0.8, bagging_fraction: 0.8, bagging_freq: 1, lambda_l2: 5.0, num_boost_round: 5000, early_stopping_rounds: 200 }学习率降到 0.02 之后需要更多的树才能收敛但精度会明显改善。训练时间虽然变长了但完全值得。4.3 融合策略简单加权平均就很稳模型融合方面我尝试过不少方案包括 Stacking、加权平均、甚至简单的 Ridge 回归作为二级模型。最终发现在这个比赛中三个模型预测结果的简单加权平均就已经很稳定。权重分配的依据是单模型的验证集 MAE 表现LightGBM 的权重最大XGBoost 和 CatBoost 次之。最终权重大概是 LightGBM 0.4、XGBoost 0.3、CatBoost 0.3。融合之后五折 MAE 降到了 6810 左右比最好的单模型又降了接近 3%。这说明三个模型之间的差异性确实存在融合的收益是实打实的。5. 代码方案的模块设计与复现要点5.1 整体目录结构这套代码打包出来之后我按照模块化思路整理了目录确保其他人拿到压缩包之后能按顺序跑通├── data/ │ ├── train.csv │ ├── test.csv │ └── sample_submit.csv ├── features/ │ ├── build_features.py # 特征工程 │ ├── target_encode.py # 均值编码 │ └── pca_features.py # 匿名特征降维 ├── models/ │ ├── train_lgb.py # LightGBM 训练 │ ├── train_xgb.py # XGBoost 训练 │ ├── train_cat.py # CatBoost 训练 │ └── ensemble.py # 模型融合 ├── utils/ │ └── cv.py # 五折交叉验证工具 └── main.py # 一键运行入口具体运行方式很简单先把数据放到 data 目录然后依次运行 main.py 即可。main.py 内部会自动按照“特征构建 → 模型训练 → 交叉验证 → 融合预测”的顺序执行并在 output 目录下生成最终的提交文件。5.2 复现时最容易出问题的几个点第一均值编码的时序问题。均值编码的时候必须先用交叉验证把训练集分开在每一折上独立计算编码值然后在测试集上用全量训练集的均值做编码。如果不这样做直接用全量训练集计算均值编码再加到训练集上会引入目标泄露验证集指标虚高但线上效果暴跌。第二catboost 的类别特征声明。CatBoost 本身支持类别特征但需要在训练时用 cat_features 参数显式声明。如果不声明它会把所有特征当数值处理类别特征就会被拆成一堆无意义的阈值分裂效果大打折扣。第三预测结果的还原。如果训练时对 price 做了 log1p 变换预测之后必须做 expm1 还原。这个步骤看起来简单但很容易被忽略。还有个细节是还原后可能出现负值理论上不会但某些极端情况会出现我习惯对最终预测值做一个 np.maximum(pred, 0) 的截断保证提交结果不为负数。5.3 交叉验证框架保证线下线上一致交叉验证的划分方式一定要谨慎。这个比赛的数据集里同一个车型的样本分布并不均匀如果随机划分可能会出现训练集和验证集里同一车型的样本分布差异较大导致验证集指标波动很大。我用的是 StratifiedKFold但这里有个坑price 是连续值不能直接作为分层标签。我先把 price 按十分位分箱用分箱后的类别做分层这样能保证每一折里不同价位段的样本比例大致一致。线下验证的结果和线上结果的相关性会更好。6. 复盘里最想说的话踩坑记录与可复用的经验6.1 三个印象深刻的坑第一个坑是过度依赖匿名特征。最开始我花了很多时间在 v_0 到 v_15 上试图从分布里找出它们的业务含义。后来复盘发现真正让分数提升的反而是那些朴素的业务特征车龄、品牌、里程。匿名特征只能算是锦上添花千万不要本末倒置。第二个坑是线上和线下不一致。第一次提交的时候我把所有数据包括测试集都拿去统计均值编码了导致线下验证集 MAE 低得离谱线上分数却崩了。后来花了很长时间排查才发现是目标编码泄露。这个教训极其深刻。第三个坑是过早开始调参。刚把基础特征做好之后我就迫不及待地去调 LightGBM 的参数花了两天时间MAE 只下降了 200 多。后来静下心来做特征工程几组特征组合下来MAE 直接降了 1500 多。这个对比让我彻底相信了一句话特征决定上限模型只是逼近上限的方式。6.2 这套方案还能怎么改如果后面想用这套代码继续迭代我会从三个方向入手一是尝试深度学习模型。比赛的时候时间紧张没来得及做 Embedding MLP 的方案。对于类别特征特别多的表格数据Embedding 可以捕捉到类别之间的隐含相似性很多比赛里 NLP 和视觉的模型都比纯树模型效果更好。二是做特征筛选。现在代码里保留了 80 多个特征但并不是越多越好。可以用特征重要性排序 前向搜索的方式找一个最优特征子集。这不仅能提升精度还能加快训练速度。三是尝试时序信息。原始的 creatDate 是按时间排序的如果测试集和训练集在时间上有 shift可以考虑用时间序列交叉验证也就是用前 80% 的时间段数据训练后 20% 验证。这样做出来的模型对未来的预测能力可能更强。最后再分享一个比赛的小技巧提交之前一定把最终预测分布和训练集的真实价格分布对比一下如果两者形态差距很大说明某个环节出了问题。这个习惯帮我避免过至少三次无效提交。本文还有配套的精品资源点击获取
返回列表