尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

二手车价格预测实战:从特征工程到模型融合压降MAE

二手车价格预测实战:从特征工程到模型融合压降MAE 简介这是一份天池『二手车交易价格预测』竞赛的完整项目包面向机器学习与数据挖掘初学者、竞赛参赛者聚焦基于历史交易数据的二手车价格回归预测任务。资源共22个文件压缩包约65MB主要包含CSV/TSV数据文件、Python训练脚本、Jupyter交互式分析Notebook、多种模型文件以及中英文说明文档数据清洗、特征工程、交叉验证、模型融合等关键环节均有覆盖。除原始训练集、测试集与提交样例CSV外还提供XGBoost、LightGBM、CatBoost、Stacking等模型的预测结果并保存了最优模型参数、策略信息、训练日志及CatBoost训练过程记录便于读者复现实验、对比不同算法效果并进一步调参优化。项目说明与EDA建模思路文档详细梳理了探索性数据分析、缺失值处理、特征构造、模型评估与调优的完整流程。目前已有168人学习下载适合希望系统掌握结构化数据竞赛通用方法、提升价格预测精度的读者。 第一次跑通天池《二手车交易价格预测》的完整流程时我的MAE停在0.52左右排名掉到一半开外。后来把注意力从“调参”挪到“理解数据”上才一路压到0.43上下。这个赛题表面是结构化回归的入门题实际上考的是你对误差分布、特征语义和模型融合这三件事的把握。如果你也在准备这类预测赛题或者工作中正好遇到二手车估价、二手数码产品估价这类需求这篇复盘应该能帮你少走不少弯路。1. 赛题到底在预测什么任务定义、数据字段和评估指标的底层逻辑1.1 先看清任务边界别急着写代码这个赛题给的是15万条训练样本、5万条测试样本大约31个字段。目标是根据车辆属性预测成交价格评估指标是MAE也就是平均绝对误差。MAE和RMSE最大的区别在于它不惩罚平方误差意味着一个偏差3万的样本和被10个偏差3000的样本拉平的效果是一样的。所以在调参之前你脑子里得清楚一件事优化目标不是“猜得准”而是“尽量不猜离谱”。我第一次上手时犯了个典型错误一上来就LightGBM默认参数跑一遍然后盯着特征重要性列表开始删特征。后来发现结构化比赛里最不值钱的就是过早调参最值钱的是把字段的“业务含义”翻译成“模型能直接吃进去的形态”。比如regDate和creatDate这两个字段一个是注册日期一个是线上创建时间两者相减就是车龄这是整个价格模型里最硬的信息之一但如果你只把它们当成两个独立时间戳丢进去模型基本学不到什么东西。1.2 初始数据体检不能省先搞清楚字段分布动手做特征之前我用pandas做了一次完整的体检主要看三件事缺失值、常数特征、价格分布。缺失值集中在bodyType车身类型、fuelType燃油类型、gearbox变速箱、notRepairedDamage未修复损伤这几个字段上其中notRepairedDamage的缺失率很高而且NaN本身有业务含义——它通常表示“没有损伤记录”不是数据采集失败。这个字段我用-1填充和0、1区分开。seller和offerType两个字段几乎是常数seller只有一个取值offerType也只有一个取值这种特征对树模型没有任何分裂价值直接删。price分布则严重右偏尾部有几十万上百万的豪华车也有极少数几百块的问题车。画个直方图就能看到大部分车集中在几万到十几万这个区间。这种分布对MAE极其不友好所以后面的对数变换几乎是必选项。还有一个容易忽略的点重复样本。我检查下来发现有少量完全重复的行不同id对应相同属性这种数据对模型训练没帮助保留一份就行避免对重复样本过拟合。2. 特征工程的取舍时间、品牌和缺失值哪些真正影响价格2.1 车龄和品牌车型的组合特征是价格模型的压舱石这个赛题的特征工程里收益最大的两刀一刀切在时间上一刀切在品牌和车型的交叉上。时间方面最基础的特征是车龄也就是creatDate减去regDate得到的天数除以365。车龄和价格并不是线性关系新车头两年折旧率最高后面趋于平缓。所以我除了直接用车龄还构造了车龄的平方项、车龄与公里数的比值用来刻画“一年跑两万公里”和“一年跑五千公里”的差异。实测下来车龄和公里数交互项的加入让单模型MAE大概降了0.005左右听着不多但在这个量级的赛题里已经是不小的提升。品牌和车型方面brand是品牌编码name是车型编码name的粒度比brand细得多同一个品牌下可能有几十个车型编码。直接对name做LabelEncoder塞给树模型是可用的因为LightGBM和XGBoost能处理高基数分类特征。但更好的做法是把brand和name拼成一个组合特征比如brand * 1000 name这样模型可以同时学到“品牌级别”的溢价和“车型级别”的细分差异。再加上model字段三者组合后基本就把“这辆车是什么车”这件事刻画完整了。2.2 缺失值、0值和极端值的处理不能一把梭很多新手习惯用均值或中位数填充所有缺失值这在这个赛题里是浪费。树模型对缺失值的天然处理能力比线性模型强得多LightGBM默认就能让缺失值走最优分裂方向。我实际试下来bodyType、fuelType、gearbox直接填-1让模型把它们当成一个独立的类别效果比填众数或中位数都要好。比较隐蔽的是power功率字段里面有相当一部分样本是0还有一些异常大值比如600、1000以上。功率为0显然不合理可能是采集错误也可能是电动车数据没填好这个根本没有物理意义。我的处理是先看分布然后对0值用该品牌车型组合下的中位数去替换对大于600的极值同样截断到合理区间。极端大功率样本数量很少留着只会让模型为了拟合它们扭曲分裂点得不偿失。另一个值得注意的字段是regionCode地区编码。它的基数很高直接丢给模型不是不行但效果一般。我试过做目标编码mean encoding也就是用历史数据计算不同地区的平均价格结果线上分数提升很有限反而在后期融合时增加过拟合风险。最后我选择只保留原始编码让树模型自己去挖掘地区相关的价格差异。2.3 哪些特征是“白做”甚至“帮倒忙”我前前后后试过几十个特征最后线上稳定有效的其实不超过一半。有几个典型的无效尝试可以帮你避坑第一个是“燃油类型×车身类型”这种纯组合交叉看起来合理实际上两个高基数类别相乘会生成大量稀疏二值列树模型很难从中找到稳定的分裂点。第二个是对kilometer做分箱直接把连续值切成几段这会损失距离信息比如“5万公里”和“5.1万公里”本来应该价格几乎一样但分箱后可能被切到两个区间里。再提一个有点反直觉的点不要把测试集和训练集合并做标准化。树模型对特征的尺度不敏感这个操作本身不会带来收益但如果后期你想试线性模型或神经网络一旦合并计算均值和方差就会把测试集的信息泄漏到训练过程里线上分数会虚高提交时被打回原形。3. 模型怎么选LGB、XGB和CatBoost在15万条数据上的实测对比3.1 为什么默认就是GBDT三兄弟而不是线性回归或深度学习二手车价格预测是典型的表格数据回归问题特征大多是类别型和高基数的离散值价格和特征之间还有大量非线性关系。线性回归在这种场景下基本没有竞争力因为你不可能手动构造出所有品牌、车型、地区、时间之间的交互项。深度学习在这个赛题里也不是首选。15万条样本、几十维特征这个数据量对深度模型来说偏小想靠Embedding加MLP做出优势需要非常细致的调参和正则化。我试过一版简单的EmbeddingMLP线下分数比LightGBM差了一截训练时间倒是翻了好几倍后来就放弃了。不是深度模型不能做而是性价比太低GBDT在这个数据规模上的表现几乎不可战胜。3.2 三个模型的实际表现和参数偏好我的基准策略是LightGBM和XGBoost双模型并行后期视情况加入CatBoost。三个模型的侧重点略有不同LightGBM训练速度快对高基数类别特征友好XGBoost在相同特征下往往能略优一点点但训练时间明显更长CatBoost对类别特征的原生处理最强但在这个赛题里优势不明显。核心参数上我用的配置大概是这样的LightGBMobjective设为regression_l1因为评估指标是MAE直接用L1损失比L2更贴近线上目标learning_rate固定0.03num_leaves在128到256之间min_data_in_leaf设在50左右feature_fraction和bagging_fraction都取0.8配合early_stopping。XGBoostobjective用reg:absoluteerroreval_metric用maemax_depth控制在7到9learning_rate同样0.03左右。CatBoostloss_function设为MAEdepth设为8learning_rate 0.05迭代轮数靠early_stopping确定。这里有个特别重要的教训learning_rate不要一上来就设0.1或者更高。虽然高学习率配early_stopping也能收敛但往往会欠拟合后几轮的最优模型导致分数波动变大。我把learning_rate降到0.03之后每一轮的验证集MAE更平滑最终分数也更稳。3.3 融合是必须的但不是“三个模型都跑一遍就完事”我最后提交用的方案是三模型融合但融合之前做了严格的折外预测OOF。简单说就是每个模型都在5折交叉验证下对训练集和测试集做预测训练集的预测结果拼成一个完整的OOF向量测试集的预测结果取5折平均。然后在这个基础上搜索融合权重。这里有一个关键细节融合权重不能用验证集分数拍脑袋定更不能用测试集分数去反推。我用的是网格搜索在OOF向量上尝试不同的权重组合看哪个组合的MAE最低然后把这个权重套到测试集预测结果上。最终搜索出来的权重大概是LGB占0.4、XGB占0.35、CatBoost占0.25三个模型差异越大融合收益越明显。4. MAE评估下的两个隐藏坑对数变换和K折划分4.1 价格长尾分布下直接做回归会吃亏前面提到price是严重右偏的这个特性在MAE指标下会放大两个问题。第一高价位车虽然样本少但价格动辄几十万预测偏差很容易达到五六万一个样本的误差就能顶得上几十个普通车的误差总和。第二模型的优化过程会被这些极端样本牵引导致中低价位车的预测偏差被忽略。解决办法是训练前对price做log1p变换也就是把价格取自然对数后再作为回归目标预测完再通过expm1还原。这样做的本质是把“绝对误差优化”转换成“相对误差优化”在log空间里5万和6万的差距与50万和60万的差距是等价的模型不会被尾部高价车带偏。但这个变换不是万能的。线下验证时我对比过两个方案一个在原始价格上直接回归一个在log价格上回归后者在MAE评估下稳定胜出但提升幅度没有想象中大。原因在于log变换压缩了高价位样本的误差却会让低价位样本的相对误差更容易被放大所以最终提交时还是要回到MAE本身去评估不要只看log空间的指标。4.2 K折划分要是随便用你的验证集分数就是骗人的很多入门选手直接调sklearn的KFold做5折这在价格分布极不均匀的赛题里就是个雷。因为不同折的训练集和验证集价格分布可能差异巨大某折的验证集里恰好全是低价车另一折全是高价车每折的MAE天差地别你根本没法判断模型到底是好是坏。正确做法是用StratifiedKFold但这个分层不能基于原始价格因为价格是连续值直接分层会报错。我先把price按分位数切成10个桶然后对桶标签做分层采样这样每一折里高中低价车的比例都和总体保持一致。做了这个改动之后各折的MAE方差明显下降模型调参才有参考意义。选多少折也值得斟酌。3折验证速度快但分数波动大10折线下评估更准但训练时间成倍增加。我最终选的是5折对15万条数据来说训练时间可接受线下分数和线上分数的相关性也比较稳定。调参阶段用2折加速尝试确定最终方案后再跑完整5折效率和精度两头兼顾。5. 折外融合的正确姿势把三个模型的结果拼起来MAE还能再降5.1 OOF预测里最容易犯的操作错误折外预测听起来简单实际操作里有个高频错误把每个模型在训练集上的预测直接拼起来而不是用“模型没见过”的验证集预测去拼。如果你在每一折里对训练集本身也做了预测再把这些预测拼起来和真实价格做拟合那就是严重的过拟合融合权重会被垃圾信息带偏。正确流程我展开说一下对训练集做5折划分伪标签就按4.2的分层方式生成。每一折里模型在4折数据上训练对留出的1折验证集做预测同时对该折的测试集做预测。5折完成后验证集预测拼接成完整的OOF向量测试集预测取5折的平均。对每个模型重复以上过程得到LGB_OOF、XGB_OOF、CatBoost_OOF。在OOF向量之间做加权融合网格搜索权重找到让MAE最低的组合。这个流程有个细节测试集预测的5折平均和只跑一折拿到的预测结果相比要稳定得多。因为每一折模型看到的训练数据略有差异预测结果会围绕真实值波动取平均能把噪声压下去。我在提交中对比过用完整的5折平均比单折预测线上分数更稳。5.2 融合权重搜索是在OOF上做的我搜索权重时用了简单的三层循环LGB权重从0.1到0.8、XGB权重从0.1到0.8、CatBoost权重等于剩余部分每次计算三者的加权MAE。听起来是个笨办法但在这个量级下计算很快而且不会陷入复杂的优化工具反而搞不清结果来源的窘境。搜索出来的最优权重一般会和单模型分数的排名相关但不完全一致。比如单跑XGB分数略优于LGB但融合时LGB的权重反而可能更高因为两个模型的相关性不同如果LGB和XGB的预测高度相关给它们各设一半权重就浪费了融合空间。所以最终权重应当以搜索为准不要依据直觉。融合之后我通常还能在单模型最好成绩上再压掉0.002到0.003的MAE。这个数字在不同特征版本下有波动但从没有出现过融合后分数反而变差的情况。3点总结就是折外预测不能省权重搜索不能省以及多模型差异越大融合收益越高。5.3 融合的进阶直接合并特征训练一个Stacking模型除了加权融合我还试过Stacking——把三个模型的OOF预测作为训练特征再训练一个LightGBM作为第二层模型。实测下来Stacking在OOF上的表现往往比加权融合好一点点但更容易过拟合尤其在测试集分布与训练集有细微差异时稳定性反而不如加权融合。最后我没有在提交中使用Stacking原因有三个一是加权融合的代码和原理都更简单可解释性强二是Stacking需要额外划分一部分数据来训练第二层模型在15万条数据里这部分的浪费不可忽略三是这个赛题的线上评测分布和训练集比较接近简单的加权融合已经能吃到大部分融合红利再复杂的结构产生的收益不成比例。6. 踩坑记录数据泄漏、随机种子和版本一致性问题6.1 数据泄漏不是只有“用了未来信息”这一种形式数据泄漏在这个赛题里最常见的形态是对测试集做了信息外溢。比如用全体数据计算某个地区的平均价格再把平均值作为特征喂给模型这个平均值里已经包含了测试集样本的标签信息训练时会严重高估线下分数线上却崩盘。更隐蔽的泄漏是时间泄漏。这个赛题的creatDate字段不是均匀分布的训练集的创建时间集中在某些时间段测试集则整体偏后。如果你直接用creatDate当作时间特征而不做任何处理模型会学到“创建时间靠后的车价格更高”这种伪规律线上自然翻车。我的做法是不直接使用creatDate的原始值只用它和regDate的差值即车龄然后单独把车龄和年份抽象出来避免模型过度依赖时间轴本身。6.2 随机种子随便设分数能差出一个身位LightGBM和XGBoost都带有随机性K折划分的shuffle过程也一样。我这个赛题里测试过同一个特征集、同一个参数只改random_state从0变成425折OOF分数就能波动0.002甚至更多。听起来不多但在排行榜中段0.002足够让排名变化几百名。解决办法有两个一是固定所有随机种子包括模型的seed、K折划分的random_state、以及特征工程里的任何随机采样环节二是多组种子各跑一遍对OOF预测取平均后再去搜索融合权重。前者保证可复现后者能进一步压低噪声。我最后采用了五个不同种子的LGB模型OOF平均加上两个不同种子的XGB模型OOF平均融合时分数更稳。6.3 版本不一致带来的“隐形掉分”另一个容易被忽视的问题是库版本。我在本地用LightGBM 3.3.5训练和调参中途升级到4.x后没有改动任何参数OOF分数直接变了而且部分特征的重要性排名也发生了改变。这类变动不影响知识迁移但会影响你对“当前版本”的判断。所以我后面固定了一套环境Python 3.9、LightGBM 3.3.5、XGBoost 1.7.3、CatBoost 1.2。所有实验都在这个环境里完成确定最终提交之前不升级任何依赖。如果你用的是在线Notebook环境也最好在环境说明里锁死版本避免平台侧的库更新影响你的复现结果。6.4 异常样本的删除要谨慎也要果断训练集里有一些极端样本价格低到1元或者高到几百万。前者可能是数据录入错误后者虽然真实存在但数量极少。在MAE评估下这些样本对模型的影响非常大我试过保留和剔除两种方案总体来看剔除价格最低和最高的极少数样本大约占训练集0.1%之后模型在验证集上的MAE更稳定。但不建议大规模清洗异常值。剔得太多模型会失去对真实边界情况的拟合能力比如低价事故车和高价收藏级跑车你要是把这些都删了测试集里遇到类似样本时就会毫无招架之力。我的原则是只删除“明显不合理”的记录比如价格低于100元且公里数极高的组合以及功率为0但价格在30万以上的组合这些都是业务上几乎不可能出现的数据。最后再分享一个小技巧不要只盯着一版特征跑到黑。我最终提交的特征集其实是在B榜开放前三天才定下来的前面尝试过的很多版本都进了垃圾桶。赛题复盘真正带给我的不是某个具体模型调参公式而是一套“先理解赛题目标、再理解数据、最后才是模型炫技”的做事顺序。这个顺序换到任何结构化预测任务里都值得先按这个流程走一遍。本文还有配套的精品资源点击获取
返回列表