简介:基于天池二手车交易价格预测赛题整理的完整项目资源,面向机器学习与数据挖掘学习者、竞赛参与者,适合希望掌握价格回归建模流程的读者。内容围绕历史交易数据清洗、特征工程、多模型融合预测展开,能帮助读者走通从探索性数据分析到多种主流集成算法落地的完整建模链路。资源包共22个文件,大小65.01MB。核心文件为7个CSV与3个TSV数据文件,涵盖训练集、测试集及预测结果;另外包含Python脚本、Jupyter Notebook笔记、训练好的模型文件(Joblib/CBM)以及两篇Markdown说明文档,便于按数据、模型、说明三部分查阅。已有168人学习浏览。资源提供了完整赛题方案,包含多模型预测结果、最优模型参数与Stacking集成策略记录,可直接复现或迁移到二手车估价项目。配套的探索性数据分析与建模思路笔记,能减少在数据预处理和调参环节的重复踩坑,适合参考对照。
1. 赛题价值与整体思路
天池的“二手车交易价格预测”是个特别“耐打”的赛题。数据来自国内某二手车交易平台的真实成交记录,目标是根据车辆属性预测成交价。你可能会觉得,二手车价格不都是平台估价师说了算吗?但平台每天上架几万辆车,靠人肉估价根本不现实,所以才会把价格预测做成一个标准的回归问题,交给算法来处理。这也正是这个赛题的含金量所在——它模拟的是工业界里一个非常真实、非常高频的需求。
先说结论:这个赛题适合谁?不管你是刚学完机器学习理论、想找项目练手的新手,还是已经在做风控、营销、推荐等方向、想往价格预估类业务转的算法工程师,都可以把这份题完整跑一遍。数据量15万条左右、31个字段,单卡CPU就能跑,不挑机器,跑一轮端到端的完整流程也就一两个小时。对比动辄几百万样本的推荐系统赛题,这个体量对新手特别友好,你能很清楚地看到每一步操作对结果的影响,不用陷入“调参两小时、训练一整天”的泥潭。
再说这个赛题最核心的一个认知:它的评估指标是MAE(平均绝对误差),不是MSE,也不是R2。这意味什么?意味着你预测出一个离谱的价格,损失会被线性放大,而不是平方放大。这样的指标设置,让模型的容错空间更大,但也更考验你对价格分布长尾部分的处理能力。我在实际跑数据的时候,第一步就让价格分布说话,这一看就看出了门道。
整体技术路线我建议直接照搬一线比赛的成熟套路:先做EDA看清数据结构,再做特征工程把时间字段、类别字段、匿名特征榨干,然后上LightGBM、XGBoost做五折交叉验证,最后融合出结果。这套组合拳在结构化数据比赛里能覆盖百分之八九十的常规问题,二手车价格预测正好落在里面,不需要花哨的深度学习,树模型就是王道。
2. 数据探索:先看清价格和特征再说建模
2.1 数据概况与字段的“潜台词”
训练集大概15万行,31列,其中包含一个价格字段price,这是我们的目标变量。剩下30个特征可以大体分成三类,这个分类很关键,直接决定后续特征工程的思路。
第一类是强业务特征,比如name(车辆名称)、regDate(注册日期)、creatDate(上架日期)、kilometer(行驶里程)、power(功率)、brand(品牌)、model(车型)、gearbox(变速箱)、fuelType(燃油类型)、bodyType(车身类型)。这些字段一看就懂,但越“懂”越容易踩坑。比如regDate,它并不是让你直接做成数值特征塞进模型的,而是要拿它和creatDate做差,算出车龄和上架等待时间,这个才是真正有价值的信息。
第二类是匿名特征v_0到v_14,一共15个。比赛方在这类赛题里通常会做脱敏处理,把一些比较私密或敏感的字段匿名化。你不用猜它们背后的业务含义,只需要知道一点:这些匿名特征大多和价格有很强的相关性。我在相关性矩阵里看,v_0这种特征的相关系数能到0.6以上,比很多业务特征都猛。所以对这些字段,我不做任何业务假设,就当成纯数值特征处理,但保留它们的原始值,最多做缺失值填充。
第三类是名字看起来“很干净”实际全是坑的字段,比如notRepairedDamage(是否有未修复损伤),这个字段在原始数据里根本不是二值0/1,而是有大量“-”字符。再比如model和name,model是车型编码,name是车辆名称,这俩字段在匿名处理后依然存在高基数问题——model有两三百个取值,name更是有几千个唯一值。怎么处理这类字段,是特征工程部分的重点,后面我会详细说。
2.2 价格分布必须看,决定目标变换怎么做
拿到数据第一件事,不是急着跑模型,而是画出price的分布直方图。我印象很深,这个赛题的价格分布是严重右偏的,大部分车集中在几万到十几万的区间,但少数豪车能到几十万甚至上百万,长尾拖得很长。如果你直接拿原始价格当目标训练回归模型,树模型会被那些极端大值带偏,整体预测都会往上飘。
处理办法很成熟:对价格做log1p变换,也就是取 log(price + 1)。这样做的本质是把乘法关系变成加法关系,让价格在数值空间里更加对称。实际跑下来,log变换之后再做回归,MAE能降低不少。但是这里面有个新手必踩的坑:预测结束之后,千万别忘了把结果exp回去。如果你输出的是log价格,而评估的时候直接拿exp(log预测)和真实价格算MAE,那没问题;但如果忘了反变换,或者反变换方式不对,分数直接废掉。我当时为了防止这问题,在交叉验证里同时计算了“log空间的MAE”和“原始空间的MAE”,两边都盯住,就能及时发现到底是模型问题还是后处理问题。
另外还有一个小细节,就是bodyType、fuelType、gearbox这几个字段都有少量缺失值。缺失值在树模型里其实不需要过度恐慌,LightGBM原生支持缺失值方向学习,也就是它会在训练时自动学习缺失值该往左还是往右走。所以对这几个字段,我最开始直接填空值就行,等模型验证完再加更精细的填充策略对比效果。别一开始就在缺失值上花大把时间,先跑通流程再回来优化,效率更高。
2.3 几个值得玩味的字段规律
先说notRepairedDamage。这个字段是“是否有未修复损伤”,按理说应该是0/1二值。但实际数据里有大量“-”值,我统计了下能达到三分之一左右。这些“-”并不是模型跑出来的噪声,而是交易平台上很多车主压根没填这个信息。处理方式通常是把它当成第三类,也就是“未知”状态,不要简单粗暴地删掉或填成0。你可以把“-”替换成-1,或者单独映射成一个新类别。这里头有个细节:如果你把“-”直接填成0,模型会以为这些车“没有未修复损伤”,但实际上它们是“未知有没有”,这是个完全不同的语义,会在预测时引入系统性偏差。
再说power字段。这个字段的分布让我第一眼看了有点崩溃——最小值是0,最大值能到好几千,而且0值比例不低。正常家用车功率在几十到两三百千瓦之间,power=0明显是数据录入异常或缺失。对于这些异常值,我处理的办法是先用分位数看一下分布,比如低于1%分位和高于99%分位的数据都标记出来,然后结合“是否影响目标价格”来判断是删还是填充。直接删行当然会影响样本量,填充中位数又可能失真,最后我的方案是:power=0的样本,如果价格也明显异常,直接删除;如果价格正常,用同model组内中位数填充。这是处理异常值的一个好思路——不要只看单特征,而要结合目标变量一起判断。
3. 特征工程实操:从原始字段里“挤”出有效信息
3.1 时间字段的转换要落地到什么程度
regDate是注册日期,也就是车的“生日”。creatDate是上架日期,也就是这辆车在平台上挂出来的时间。这两个日期相减,就能得到“上牌到上架之间隔了多久”,这个才是真正的车龄,比单纯用regDate截取年份要准得多。为什么?因为一辆2015年生产的车,如果到2020年才上架销售,它的实际使用年限是5年,而不是按2015年算出来的“年轻”状态。
我实际构造的时间特征有这么几个,都经过了验证有效:
- car_age_days:上架日期减注册日期的天数。这个特征对价格的影响非常直观,车龄越长、价格越低。
- reg_year、reg_month:注册年份、注册月份。注册月份能捕捉到“年底上牌还是年头上牌”的季节性差异,这在二手车市场里确实存在。
- creat_year、creat_month:上架年份和上架月份。这个能反映车源上架的季节性,比如春节前上架的车往往价格偏高。
- 上架日期与当前日期的间隔:这个特征能体现车辆在平台上的“滞留时间”,滞留越久可能说明定价偏高、车况不佳,价格也会相应往下走。
我在这里做了一个操作:以天为单位计算车龄,而不是用年。因为天级别的差分会保留更多信息,你要是四舍五入到年,等于把一辆350天车龄的车和一辆10天车龄的车混为一谈,这个精度损失在特征层面是不划算的。
时间特征构造完,还需要留意一下日期字段的格式。原始数据里regDate是整数,比如20150102这种,直接拿来算差分会出问题。要先把它转换成datetime格式,再计算差值。这个坑看着小,但是真有人在这上面愣了半天没想明白为什么日期差算出来是错的。
3.2 类别特征的处理:高基数不是无底洞
这个赛题里,brand有大概40个取值,model有200多,name有几千个。我看到很多初学者一上来就想对name做LabelEncoder,然后直接喂给模型。这个做法不是不行,但效果很有限。因为LabelEncoder只是给类别编了个数字编号,而这些编号本身没有排序含义,树模型在切分的时候又只能做大于小于的比较,所以编码后的数字其实没有被有效利用。
那高基数类别特征怎么处理?我实测下来,有三个方法是有效的,按性价比排序如下:
第一个是目标编码(Target Encoding),也叫均值编码。就是统计每个类别下的价格均值,用这个均值替换原始类别值。这个做法非常能打,因为它直接把类别的“价格倾向”编码进去了,信息量远大于一个随机编号。但目标编码有个致命陷阱——容易过拟合,所以必须配合交叉验证做平滑处理,否则模型在训练集上表现很好、线上分数直接崩。我当时用K折交叉验证,在每一折内部分别计算均值,再把均值填回去,这样能有效防止标签信息泄漏。
第二个是频次编码(Count Encoding)。统计每个类别出现的次数,用次数替换类别值。这个特征反映的是市场供需,某种车在平台上出现得越多,可能说明这种车流通量大、相对保值,也可能说明不好卖所以一直挂着。效果不如目标编码,但胜在稳定、不过拟合。
第三个是类别字段的交叉组合,比如brand和model组合成一个新字段。这是因为model编码可能在不同brand下有重复,组合之后作为更细粒度的类别,往往能带来增量信息。但要注意,组合字段的基数会变得更高,目标编码的平滑参数要跟着调整。
对name这个几千取值的字段,我的处理策略是:不直接对它做目标编码,因为基数太高、每个类别的样本量太少,统计出来的均值噪声很大。我会先统计每个name的出现次数,把出现次数少于10次的合并成“其他”类,把高基数压缩到一个可控范围,然后再做目标编码。这个“先压缩、再编码”的思路,处理所有高基数类别特征都适用。
3.3 匿名特征与数值特征的细节处理
v_0到v_14这15个匿名特征,我的原则是“能不加工就不加工,加工也以减噪为主”。为什么这么说?因为匿名特征很可能已经被平台脱敏处理过,可能已经包含了某种标准化或变换,你再去试图做复杂的特征组合,很容易引入噪声反而掉点。
我用到的数值特征处理手段有三个:
- 缺失值填充:用中位数填充,或者干脆用LightGBM的原生缺失值处理机制。
- 异常值截断:对每个特征做分位数截断,比如把超过99.9%分位的值直接截断到99.9%分位的值,防止极端值干扰树模型的切分点。
- 特征间做差或做比:v_0到v_14之间的差值和比值本质是描述车辆某些匿名属性的“差异度”,我试过加了几组差值特征,稳定提升了验证集表现。
有人可能会问,要不要对匿名特征做主成分分析降维?我的结论是:谨慎。PCA会破坏特征的稀疏性,而树模型对特征的稀疏性其实是有依赖的。你在线性模型里习惯的操作,到了树模型里未必适用。所以我在这个赛题里没有做PCA,反而是把原始特征保留了下来,让树模型自己去挖掘。
4. 建模与验证:LightGBM是主力,融合才是上限
4.1 验证方案为什么必须是五折交叉验证
这个赛题没有独立的测试集标签,你只能靠训练集划分出验证集来评估模型的真实水平。我看到有些人直接随机切了80%/20%就去跑,这样不够严谨。因为随机划分出来的验证集和训练集分布基本一致,模型在这个验证集上的表现会偏乐观,不能代表线上测试集的表现。
我采用的是五折交叉验证(5-fold CV)。具体做法是:把训练集随机切分为5份,每次取4份训练、1份验证,轮转5次,最终验证分数取5次的平均值。这样每个样本都恰好被预测过一次,得到的MAE评估更加稳健,不容易受到某一次划分的运气影响。
这里有一个我在实际比赛中总结出来的经验:不仅仅用5折CV的均值做最终分数评估,还要关注5折之间的方差。如果方差过大,说明某个折的分布存在偏差,可能和你特征处理时的某些随机因素有关,需要回去检查特征构造过程是否有泄漏,或者是否需要调整随机种子。我在这个赛题里就遇到过验证集第3折的MAE明显高于其他折的场景,排查下来发现是某次目标编码时用了全量数据的统计值,造成了标签泄漏,改为分折计算后问题才消失。
4.2 LightGBM实战:参数怎么设、为什么这么设
LightGBM是我在这个赛题里的主力模型,没有之一。对比XGBoost,它在同等精度下训练速度快了好几倍,而且对内存的占用也更友好。在5折交叉验证的场景下,这一点优势会被放大到“能不能快速迭代实验”的层面。跑实验快,意味着你一天能试十几个特征组合,而不是等一晚上只跑完一轮。
我用到的核心参数可以给个参考基线,这不是最优配置,但能保证你在一开始就有一个不错的起点:
- objective: regression_l1。对应MAE评估指标,用MAE作为损失函数,优化目标和评估指标一致,这在比赛里是个很实用的策略。
- metric: mae。训练过程直接监控MAE,方便观察模型是否收敛。
- learning_rate: 0.05。这个学习率偏保守,需要更多迭代轮数,但能有效提升最终精度。如果你时间紧张,可以调到0.1。
- num_leaves: 31。LightGBM的核心参数,控制树的复杂度,一般31是基线,后续可以调大到63或127观察效果。
- feature_fraction: 0.8。每次迭代随机使用80%的特征,起到正则化作用,防止过拟合,同时也能提升训练速度。
- bagging_fraction: 0.8。每次迭代随机使用80%的数据,同样是防止过拟合,效果比单纯调大num_leaves更稳。
- early_stopping_rounds: 100。在验证集上连续100轮没有提升就停止训练,这个能省下大量无效迭代时间。
我实际跑下来,使用上面的参数,默认特征集上5折CV的MAE大概在600到700之间,加上精心构造的特征和调参之后,能进一步压到500多。具体数值因为不同人的特征组合不一样会有浮动,但至少可以给你一个参考锚点。
在训练之前,我还会设置一个随机种子固定住,保证实验的可复现性。这个细节看似不起眼,但如果你不固定种子,每次跑出来的结果都不同,就很难判断某个特征到底带来了提升还是随机波动。
4.3 模型融合的性价比:怎么融合、融什么
单模型做到一定程度之后,想要再提升就不太容易了。这时候模型融合是性价比最高的一步。
我用的融合方案是加权平均。具体做法:分别训练LightGBM、XGBoost和CatBoost三个模型,各自做5折CV,然后把三份预测结果按权重加权求和。权重的确定方法很简单——先用grid search在验证集上找最优权重组合,比如LGB权重0.5、XGB权重0.3、CatBoost权重0.2,然后把这个权重套到最终预测上。
用这个融合方式,MAE能比最优单模型再降低10到20个点。这说明三个模型的差异性带来了有效信息互补。每个模型的误差来源不完全相同,加权平均正好能对冲掉一部分误差。
这里有个容易忽略的点:融合前每个模型的预测都是在真实空间里的价格值,不是在log空间里。所以一定要先把每个模型的预测结果做exp反变换回原始价格空间,再做加权平均。如果你直接对log价格做融合,权重就失去了意义,因为log空间的误差不是线性的。
5. 踩坑记录与经验沉淀:这套题最值得记住的几件事
5.1 五大翻车现场和对应解法
我在整个过程中踩过的坑不少,整理成一张表格,新手可以直接对照自查。
| 场景 | 表现 | 原因 | 解法 |
|---|---|---|---|
| 目标编码后验证集MAE骤降,线上预估翻车 | 验证分数很好,但提交分数很差 | 目标编码使用全量统计值,造成标签泄漏 | 改为5折交叉验证内部分别计算均值编码 |
| price预测值整体偏低 | 最终预测价格总比真实值低一截 | 训练时用了log变换,但预测结果未做exp反变换 | 预测结果加1后取指数还原;用原始空间算MAE验证 |
| power=0的样本太多导致模型偏移 | 价格预测对低功率车有明显低估 | power=0是缺失/异常值,直接进入模型 | 结合price字段判断,异常样本删除,其余用同model中位数填充 |
| regDate日期差算错 | 车龄特征全部异常 | 整数型日期没有转datetime格式就直接相减 | 先pd.to_datetime转换,再计算差值 |
| 不固定随机种子导致实验结果不可复现 | 同一代码跑两次验证分数不同 | 模型训练和CV划分有随机性 | 固定所有随机种子,保证实验可对比 |
除开这些常规问题,还有一个值得你特别留意的坑:邮寄分割线。有一些特征字段在不同折之间的分布差异明显,比如某些brand只在特定地区出现。如果你在CV划分时不做分层抽样,某些折可能完全没有某个brand的样本,那模型在这个折上对brand的预测能力就会很差。我的做法是,在CV划分时按照principal字段做分层抽样,保证每个折内各类别的占比和全量基本一致。这个操作不大,但能显著降低验证分数的方差。
5.2 从赛题到工业落地:这套流程还能怎么用
二手车价格预测跑完之后,我最大的一个感受是:这个赛题的流程完全可以平移到其他价格预估场景,比如租房价格预测、二手手机回收估价、甚至保险定价。
核心逻辑是一样的:目标变量先做分布检查、决定是否做变换;时间和类别特征做深度加工;验证方案用K折CV保证稳健性;模型用树模型起步,融合兜底。这套“标准动作”跑通一次,你就不再害怕任何结构化回归类型的业务问题。
如果后续想在这个方向深入,有几个可以扩展的点:把匿名特征重新做一次嵌入表示、尝试用神经网络对高基数类别做embedding、或者引入外部数据例如新车指导价、地区GDP等。但这些都是锦上添花,先把本赛题的核心流程吃透,比什么都强。
我个人的体会是,诸如此类结构化数据挖掘赛题,真正拉开差距的往往不是模型,而是特征工程里那些别人懒得做的细节。行数少、字段简单的公开数据集,反而因为数据量可控,让你能看见每个细节对结果的影响,这是最珍贵的学习机会。
本文还有配套的精品资源,点击获取