简介:这份资料包是阿里天池与Datawhale联合举办的二手车交易价格预测竞赛的优胜奖方案源码与项目说明,面向计算机科学、应用数学、电子信息工程等专业的学生和研究人员,可作为课程设计、毕业设计或学术竞赛的参考素材,帮助读者理解一套完整的预测建模流程与解题思路。压缩包共15个文件,约610KB,以Python脚本为主,涵盖特征工程、神经网络生成、树模型生成、LightGBM建模以及模型融合等模块,另附CSV预测结果、Markdown项目说明和依赖配置文件,结构清晰便于复现。目前已有42人学习。读者可从中获取完整的赛题方案代码、模型构建与调参逻辑、多模型融合策略及项目文档,适合具备一定编程与数据分析基础、希望深入钻研预测建模细节并自主调试优化的学习者参考。
1. 二手车价格预测竞赛方案:一份能直接跑通的优胜奖源码拆解
二手车定价这件事,放在业务里是个典型的"信息不对称"难题:同一辆车,车商、平台、个人卖家给出的价格能差出两三成。阿里天池和 Datawhale 联合办的二手车交易价格预测竞赛,本质就是让参赛者用一份结构化字段(品牌、里程、变速箱、功率、车身类型等)去逼近真实成交价。这份优胜奖方案源码,价值不在于它拿了奖,而在于它把一整套"特征工程 + 树模型 + 神经网络 + 模型融合"的完整链路摊开给你看,目录里feature、model、stack+mix.py三个模块分工清晰,NN_generation.py、Tree_generation.py、generation.py负责特征生成,lgb_model.py、cab_model.py、nn_model.py各管一路模型。对正在找大学生竞赛源码、学习资料的同学来说,这是一份能当课程设计、毕业设计骨架用的实战素材,前提是你得先看懂它每一层在干什么,而不是解压完直接python model.py就完事。
2. 先看清目录结构:feature、model、stack+mix 三层到底怎么分工
拿到压缩包,第一件事不是急着跑代码,而是把目录结构和数据流理清楚。这份方案的工程组织方式很典型,属于"特征生成与模型训练解耦"的写法,理解了这个分层,后面调参和排错才不会迷路。
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 | 各模型的预测结果 |
stack+mix.py | 模型融合,把多路预测结果做 stacking 与加权混合 | 最终predictions.csv |
prediction_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 "stack+mix.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_col='regDate', trade_col='creatDate'): # 车龄:交易时间减注册时间,换算成天 df['regDate'] = pd.to_datetime(df[reg_col], format='%Y%m%d', errors='coerce') df['creatDate'] = pd.to_datetime(df[trade_col], format='%Y%m%d', errors='coerce') 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_col='power', disp_col='displacement'): # 功率排量比,反映发动机效率,缺失时用中位数兜底 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常有格式错误或缺失,所以用errors='coerce'转成 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 三路怎么调、stack+mix 怎么接
特征准备好之后,进入模型层。这份方案训了三路模型:LightGBM、CatBoost、神经网络,最后用stack+mix.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_splits=5): oof = np.zeros(len(X)) pred = np.zeros(len(X)) kf = KFold(n_splits=n_splits, shuffle=True, random_state=42) 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_metric='mae', 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_metric='mae'和竞赛评价指标对齐,别用默认的 rmse,否则早停判断标准就错了。random_state=42固定随机种子,保证结果可复现。
4.2 神经网络模型的训练要点
nn_model.py一般用 PyTorch 搭一个几层的 MLP,输入是标准化后的数值特征加 embedding 后的类别特征。NN 在结构化数据上单打独斗往往不如 GBDT,但它的预测和树模型相关性低,融合时能贡献多样性,这才是它存在的意义。训练时重点盯学习率、batch size 和早停轮数,NN 对这三者比树模型敏感得多。
4.3 stack+mix 的融合逻辑
stack+mix.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, method='Nelder-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 现象:stack+mix 融合后分数反而比单模型低
原因:某一路模型 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, threshold=0.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逻辑说明:残差代表模型还没解释掉的部分,和残差相关性高的特征说明还有信息没被利用,相关性极低的特征对当前模型基本无贡献。threshold=0.01是保守值,调大删得更狠但可能误伤。注意只对数值列算,类别列跳过。
6.2 把融合权重固化成配置文件
每次调完权重都手改代码很痛苦。我一般会把stack+mix.py里搜出来的权重写进一个weights.json,融合脚本读配置而不是硬编码。这样换数据、换模型时只改配置,不动逻辑。格式就三行:模型名对应权重,加一个版本号字段方便回溯。
6.3 验证方法:别只看单一分数
判断一次改造是否有效,不能只看融合后的 MAE。我的习惯是同时记录三路单模型的 OOF MAE、融合后 MAE、以及融合权重分布。如果融合后提升但某路权重接近 0,说明那路模型已经没用了,可以考虑砍掉换新模型。如果三路分数都在涨但融合不涨,说明模型同质化严重,该引入差异更大的模型而不是继续调参。这套验证习惯让我少走了很多"调半天没提升"的弯路。
从那以后我每次拆这类竞赛源码,都强制先跑通 baseline 再动任何一行代码,确认数据流和评价指标对齐了才谈优化。希望这份拆解能帮到你,把这份优胜奖方案真正变成自己能用的东西。
本文还有配套的精品资源,点击获取