二手车价格预测竞赛优胜奖源码拆解:特征工程与模型融合实战
简介这份资料包是阿里天池与Datawhale联合举办的二手车交易价格预测竞赛的优胜奖方案源码与项目说明面向计算机科学、应用数学、电子信息工程等专业的学生和研究人员可作为课程设计、毕业设计或学术竞赛的参考素材帮助读者理解一套完整的预测建模流程与解题思路。压缩包共15个文件约610KB以Python脚本为主涵盖特征工程、神经网络生成、树模型生成、LightGBM建模以及模型融合等模块另附CSV预测结果、Markdown项目说明和依赖配置文件结构清晰便于复现。目前已有42人学习。读者可从中获取完整的赛题方案代码、模型构建与调参逻辑、多模型融合策略及项目文档适合具备一定编程与数据分析基础、希望深入钻研预测建模细节并自主调试优化的学习者参考。1. 二手车价格预测竞赛方案一份能直接跑通的优胜奖源码拆解二手车定价这件事放在业务里是个典型的信息不对称难题同一辆车车商、平台、个人卖家给出的价格能差出两三成。阿里天池和 Datawhale 联合办的二手车交易价格预测竞赛本质就是让参赛者用一份结构化字段品牌、里程、变速箱、功率、车身类型等去逼近真实成交价。这份优胜奖方案源码价值不在于它拿了奖而在于它把一整套特征工程 树模型 神经网络 模型融合的完整链路摊开给你看目录里feature、model、stackmix.py三个模块分工清晰NN_generation.py、Tree_generation.py、generation.py负责特征生成lgb_model.py、cab_model.py、nn_model.py各管一路模型。对正在找大学生竞赛源码、学习资料的同学来说这是一份能当课程设计、毕业设计骨架用的实战素材前提是你得先看懂它每一层在干什么而不是解压完直接python model.py就完事。2. 先看清目录结构feature、model、stackmix 三层到底怎么分工拿到压缩包第一件事不是急着跑代码而是把目录结构和数据流理清楚。这份方案的工程组织方式很典型属于特征生成与模型训练解耦的写法理解了这个分层后面调参和排错才不会迷路。2.1 三个核心目录的职责边界从 README 和文件命名能还原出这样一条流水线目录/文件职责关键产物feature/特征生成含NN_generation.py、Tree_generation.py、generation.py供树模型和 NN 使用的特征文件model/单模型训练含lgb_model.py、cab_model.py、nn_model.py、model.py各模型的预测结果stackmix.py模型融合把多路预测结果做 stacking 与加权混合最终predictions.csvprediction_result/存放中间与最终预测输出提交文件Tree_generation.py面向树模型LightGBM、CatBoost生成特征NN_generation.py面向神经网络生成特征两者对类别特征的处理方式不同——树模型可以直接吃类别编码NN 通常需要 embedding 或 one-hot。generation.py更像是公共的特征处理入口。这种拆分的好处是你想换模型只动model/下的文件想加特征只动feature/互不干扰。2.2 数据流与运行顺序正确的执行顺序是先生成特征再训单模型最后融合顺序错了会直接报文件找不到。常见做法是这样串起来# 1. 进入项目根目录确认依赖 pip install -r requirements.txt # 2. 生成特征树模型与NN分开 python feature/Tree_generation.py python feature/NN_generation.py # 3. 训练单模型各自产出预测 python model/lgb_model.py python model/cab_model.py python model/nn_model.py # 4. 融合得到最终提交 python stackmix.py这里每一步都会读写prediction_result/下的中间文件所以第 3 步必须等第 2 步完全跑完。requirements.txt里锁定了 LightGBM、CatBoost、PyTorch 等版本注意.zbak后缀的文件是备份不要当成正式脚本去跑。2.3 为什么这样分层值得学很多同学做竞赛是一个 notebook 从头写到尾能跑但没法复用。这份方案把特征、模型、融合拆成独立脚本好处是每一层都能单独验证特征生成完可以先看分布单模型跑完可以看各自的验证集分数融合前能对比哪路模型拖后腿。这种工程习惯比模型本身更值钱也是它适合当学习资料的原因——你学到的不只是一个高分方案而是一套可维护的实验组织方式。3. 特征工程实操Tree_generation 与 NN_generation 的差异与参数特征决定上限模型只是逼近上限。这份方案在特征层做了两套生成逻辑理解它们为什么分开是复现时最容易翻车也最值得抠的地方。3.1 树模型特征类别编码与统计量树模型对类别特征友好Tree_generation.py里通常会把原始类别字段做 label encoding 或 target encoding再叠加一些统计特征。二手车数据里几个高价值特征方向是车龄用注册日期和交易日期算、里程的年度均值、品牌与车型的组合频次、功率与排量的比值。这些不是拍脑袋而是业务上车况的直接代理变量。import pandas as pd import numpy as np def add_car_age(df, reg_colregDate, trade_colcreatDate): # 车龄交易时间减注册时间换算成天 df[regDate] pd.to_datetime(df[reg_col], format%Y%m%d, errorscoerce) df[creatDate] pd.to_datetime(df[trade_col], format%Y%m%d, errorscoerce) df[car_age] (df[creatDate] - df[regDate]).dt.days # 异常值处理车龄为负或过大的样本置为缺失 df.loc[(df[car_age] 0) | (df[car_age] 365*20), car_age] np.nan return df def add_power_ratio(df, power_colpower, disp_coldisplacement): # 功率排量比反映发动机效率缺失时用中位数兜底 df[power_disp_ratio] df[power_col] / (df[disp_col] 1) df[power_disp_ratio] df[power_disp_ratio].replace([np.inf, -np.inf], np.nan) return df逻辑说明car_age用交易日期减注册日期是二手车定价里最强的单变量之一但原始数据里regDate常有格式错误或缺失所以用errorscoerce转成 NaT 再算避免整列报错。参数上365*20这个上限是经验值超过 20 年车龄的样本极少且多为异常置缺失比硬保留更稳。power_disp_ratio分母加 1 是防止排量为 0 导致除零replace把无穷值清成 NaN 交给后续填充。3.2 NN 特征归一化与 embedding 准备神经网络对数值尺度敏感NN_generation.py的重点是标准化和类别字段的整数编码。和树模型不同NN 这边通常要把连续特征做 StandardScaler 或 MinMaxScaler类别特征转成从 0 开始的连续整数方便后面接 embedding 层。from sklearn.preprocessing import StandardScaler, LabelEncoder def build_nn_features(df, num_cols, cat_cols): out df.copy() # 连续特征标准化 scaler StandardScaler() out[num_cols] scaler.fit_transform(out[num_cols].fillna(out[num_cols].median())) # 类别特征整数编码缺失单独归为一类 for col in cat_cols: out[col] out[col].astype(str).fillna(missing) le LabelEncoder() out[col] le.fit_transform(out[col]) return out逻辑说明fillna(median)在标准化前做是因为 StandardScaler 遇到 NaN 会直接报错。类别字段先astype(str)再编码是为了让数值型和字符串型类别统一处理缺失值统一变成missing这一类而不是被丢弃。参数上num_cols和cat_cols需要你根据实际字段手动指定这一步没有银弹得对着数据字典一个个过。3.3 两套特征不能混用的原因新手最容易犯的错是把Tree_generation.py产出的特征直接喂给nn_model.py。树模型用的 target encoding 会引入基于标签的统计量NN 如果直接吃这种特征容易过拟合且尺度混乱。反过来NN 的标准化特征给树模型用也不是不行但会损失树模型对原始数值分裂点的敏感性。所以这份方案坚持两套生成逻辑复现时别图省事合并。4. 单模型训练与融合lgb、cab、nn 三路怎么调、stackmix 怎么接特征准备好之后进入模型层。这份方案训了三路模型LightGBM、CatBoost、神经网络最后用stackmix.py做融合。三路模型各有脾气调参重点完全不同。4.1 LightGBM 与 CatBoost 的参数侧重lgb_model.py和cab_model.py结构类似都是读特征、切训练验证集、训模型、存预测。LightGBM 在二手车这种中低维结构化数据上通常是最稳的一路重点调num_leaves、learning_rate、feature_fraction。CatBoost 的优势在于原生处理类别特征cab_model.py里如果用了cat_features参数就不需要提前做 target encoding这是它和 LGB 最大的用法差异。import lightgbm as lgb from sklearn.model_selection import KFold def train_lgb(X, y, params, n_splits5): oof np.zeros(len(X)) pred np.zeros(len(X)) kf KFold(n_splitsn_splits, shuffleTrue, random_state42) for tr_idx, va_idx in kf.split(X): tr_x, va_x X.iloc[tr_idx], X.iloc[va_idx] tr_y, va_y y.iloc[tr_idx], y.iloc[va_idx] model lgb.LGBMRegressor(**params) model.fit(tr_x, tr_y, eval_set[(va_x, va_y)], eval_metricmae, callbacks[lgb.early_stopping(100)]) oof[va_idx] model.predict(va_x) pred model.predict(X) / n_splits return oof, pred逻辑说明用 KFold 做 5 折oof存的是每折验证集的预测这是后面 stacking 的输入pred是每折模型对全量数据的预测取平均作为测试集预测。参数上early_stopping(100)表示验证集 MAE 连续 100 轮不下降就停防止过拟合eval_metricmae和竞赛评价指标对齐别用默认的 rmse否则早停判断标准就错了。random_state42固定随机种子保证结果可复现。4.2 神经网络模型的训练要点nn_model.py一般用 PyTorch 搭一个几层的 MLP输入是标准化后的数值特征加 embedding 后的类别特征。NN 在结构化数据上单打独斗往往不如 GBDT但它的预测和树模型相关性低融合时能贡献多样性这才是它存在的意义。训练时重点盯学习率、batch size 和早停轮数NN 对这三者比树模型敏感得多。4.3 stackmix 的融合逻辑stackmix.py是整份方案收口的地方通常做两件事一是用各模型的 OOF 预测作为新特征训一个元模型stacking二是直接对各模型预测做加权平均mixing。加权权重一般靠验证集分数搜出来。import numpy as np from scipy.optimize import minimize def blend_weights(oof_list, y_true): # oof_list: 每个模型的OOF预测形状 (n_models, n_samples) def loss(w): w np.abs(w) / np.abs(w).sum() # 归一化允许负权重 blend np.dot(w, oof_list) return np.mean(np.abs(blend - y_true)) n oof_list.shape[0] res minimize(loss, np.ones(n)/n, methodNelder-Mead) w np.abs(res.x) / np.abs(res.x).sum() return w逻辑说明loss里对权重取绝对值再归一化是为了允许优化过程中出现负权重某些模型反向贡献但最终权重和为 1。Nelder-Mead适合这种低维无梯度优化模型数量一般就三五个不用上更复杂的算法。拿到权重后对测试集预测做同样的加权写进predictions.csv。参数上y_true必须是训练集真实标签别拿验证集标签去拟合权重否则融合分数虚高。5. 避坑与排查复现这份方案时最容易翻车的五个点这份方案能拿优胜奖但直接解压复现十有八九会卡在下面几个地方。这些都是血泪经验提前知道能省半天。5.1 现象跑 model.py 报 FileNotFoundError原因model.py或各单模型脚本依赖feature/生成的中间特征文件但你没先跑特征生成或者跑特征生成时因为数据路径不对静默失败产物没落盘。解决严格按特征生成 → 单模型 → 融合顺序执行每步跑完去prediction_result/或特征输出目录确认文件真的生成了。数据路径建议在脚本顶部统一配置成绝对路径别用相对路径否则从不同目录执行会找不到文件。5.2 现象验证集分数正常提交分数差一大截原因特征生成时用了全量数据做 target encoding 或标准化造成标签泄露。比如StandardScaler在包含验证集的全量数据上 fit验证集信息就漏进了训练。解决所有基于统计量的特征变换必须在每折训练集上 fit、在验证集上 transform。Tree_generation.py里如果有 target encoding检查它是不是在 KFold 内部做的。这是竞赛里最隐蔽的坑本地分数虚高线上直接打回原形。5.3 现象NN 训练 loss 不下降或直接 NaN原因连续特征没标准化或者类别编码后取值范围过大导致梯度爆炸也可能是学习率设太高。解决确认NN_generation.py的标准化步骤真的执行了检查输入特征的均值和方差。学习率从 1e-3 往下调加梯度裁剪。NN 这路本来就是锦上添花调不动就先保证 LGB 和 CatBoost 跑通别在 NN 上死磕。5.4 现象stackmix 融合后分数反而比单模型低原因某一路模型 OOF 预测质量太差融合时被平均拖累或者权重优化过拟合了验证集。解决融合前先单独看每路模型的 OOF MAE把明显拖后腿的那路剔掉再融。权重优化用交叉验证的方式评估别直接在全部 OOF 上搜权重。常见做法是给每路模型设一个最低权重下限防止某路被优化成负贡献。5.5 现象requirements.txt 装完仍报版本冲突原因.zbak备份文件里可能有旧版依赖或者本地已有包版本和锁定版本冲突。解决用干净的虚拟环境装依赖python -m venv venv后激活再pip install -r requirements.txt。如果 CatBoost 或 PyTorch 装不上先单独装这两个大头再装其余。别在全局环境里硬装版本冲突会牵连一片。6. 从复现到改造把这份方案迁移到自己赛题的三个技巧跑通只是起点这份方案真正的价值是能迁移。下面三个技巧是我拆完这类竞赛源码后最常用的改造手法落到具体操作上。6.1 用 OOF 分数做特征筛选三路模型跑完后你手里有一堆特征和每路的 OOF 预测。一个实用技巧是对每个特征算它和 OOF 残差的相关性相关性低的特征直接砍掉再重训一轮。这比盲目加特征有效得多。具体做法是把oof - y_true作为残差用scipy.stats.pearsonr逐个算特征与残差的相关系数绝对值小于 0.01 的列可以进候选删除名单删完重训看验证分数是否回升。from scipy.stats import pearsonr import numpy as np def screen_features(X, oof, y_true, threshold0.01): residual oof - y_true drop_cols [] for col in X.columns: if X[col].dtype object: continue r, _ pearsonr(X[col].fillna(0), residual) if abs(r) threshold: drop_cols.append(col) return drop_cols逻辑说明残差代表模型还没解释掉的部分和残差相关性高的特征说明还有信息没被利用相关性极低的特征对当前模型基本无贡献。threshold0.01是保守值调大删得更狠但可能误伤。注意只对数值列算类别列跳过。6.2 把融合权重固化成配置文件每次调完权重都手改代码很痛苦。我一般会把stackmix.py里搜出来的权重写进一个weights.json融合脚本读配置而不是硬编码。这样换数据、换模型时只改配置不动逻辑。格式就三行模型名对应权重加一个版本号字段方便回溯。6.3 验证方法别只看单一分数判断一次改造是否有效不能只看融合后的 MAE。我的习惯是同时记录三路单模型的 OOF MAE、融合后 MAE、以及融合权重分布。如果融合后提升但某路权重接近 0说明那路模型已经没用了可以考虑砍掉换新模型。如果三路分数都在涨但融合不涨说明模型同质化严重该引入差异更大的模型而不是继续调参。这套验证习惯让我少走了很多调半天没提升的弯路。从那以后我每次拆这类竞赛源码都强制先跑通 baseline 再动任何一行代码确认数据流和评价指标对齐了才谈优化。希望这份拆解能帮到你把这份优胜奖方案真正变成自己能用的东西。本文还有配套的精品资源点击获取