☰
基于LightGBM与XGBoost的二手车价格预测实战:从数据清洗到模型融合
2026/10/22 8:15:01 网站建设 项目流程

简介:这份资源是面向计算机、人工智能、自动化等专业学生与从业者的机器学习实战项目包,以天池二手车价格预测赛题为背景,提供从数据读取、特征工程到模型训练与结果提交的完整流程,适合作为期末大作业、课程设计或毕业设计参考,也便于初学者进阶练手。压缩包共16个文件,约32.05MB,包含5个csv数据集与提交样例、2个ipynb实验笔记(分别基于LightGBM与XGBoost建模)、4个xml工程配置、1个md说明文档及requirements.txt依赖清单等,结构清晰,便于按模块复现与调试。项目代码经过调试测试,可直接运行,答辩评审分达98分,已有66人学习下载。读者可据此掌握回归预测的完整建模思路、两种集成模型的对比调参方法,并在此基础上修改调整,实现自己的预测功能。

1. 从一份 98 分大作业说起:这套二手车价格预测源码到底能跑出什么

如果你正在为机器学习期末大作业或者毕业设计找一份能直接跑通、结构清晰的参考项目,这套基于 LightGBM 和 XGBoost 的二手车价格预测源码值得认真拆一遍。它来自天池竞赛的经典赛题,包含完整的训练集、测试集、提交样例和两份 Notebook,外加 requirements.txt 和 readme 说明。我拿到手第一件事不是看代码,而是先确认数据规模——训练集约 15 万条,测试集 5 万条,特征涵盖车型、注册日期、里程、功率、变速箱等 30 余列。这个体量刚好卡在「单机可跑、又不至于太玩具」的区间,用 LightGBM 在普通笔记本上十几分钟能出第一版结果。适合谁?计算机、通信、人工智能方向的学生拿来做课程设计或毕设底稿,也适合刚转机器学习、想找一个完整 pipeline 练手的从业者。下面我按实际拆包顺序,把数据探查、特征工程、双模型训练和提交生成一步步走一遍,中间踩过的坑也一并标出来。

2. 数据探查与清洗:先搞清楚哪些列是「脏」的

2.1 读入数据后第一轮体检

拿到used_car_train_20200313.csv和used_car_testB_20200421.csv之后,别急着往模型里灌。这个数据集有个特点:列之间用空格分隔,不是标准逗号 CSV,直接pd.read_csv会读成一整列。我一般会先跑一段探查代码,把形状、缺失率、类型分布一次性看清楚。

import pandas as pd import numpy as np # 注意:天池这份数据是空格分隔,必须显式指定 sep=' ' train = pd.read_csv('used_car_train_20200313.csv', sep=' ') test = pd.read_csv('used_car_testB_20200421.csv', sep=' ') print('train shape:', train.shape) print('test shape:', test.shape) # 缺失率排序,只看有缺失的列 missing = train.isnull().sum() missing = missing[missing > 0].sort_values(ascending=False) print(missing) # 看一下 price 的分布,这是标签列 print(train['price'].describe())

这段代码的逻辑很直白:先确认分隔符,再统计缺失。实际跑下来你会发现bodyType、fuelType、gearbox三列缺失比较集中,price本身没有缺失。参数上唯一要改的是文件路径,如果你把数据放在子目录里,记得同步改。sep=' '这个参数是这份数据的命门,漏了它后面全错。

2.2 缺失值处理与异常值裁剪

缺失值怎么填,直接决定模型能不能学到有效信号。bodyType和fuelType是类别型,我一般用众数填充;gearbox缺失极少,同样众数处理即可。但这里有个容易翻车的地方:price列存在极端值,直接训练会让 LightGBM 的叶子节点被拉偏。

# 类别型缺失用众数填充 for col in ['bodyType', 'fuelType', 'gearbox']: mode_val = train[col].mode()[0] train[col] = train[col].fillna(mode_val) test[col] = test[col].fillna(mode_val) # price 裁剪掉极端值,保留 1% 到 99% 分位 low, high = train['price'].quantile([0.01, 0.99]) train = train[(train['price'] >= low) & (train['price'] <= high)] print('after clip:', train.shape)

逻辑说明:众数填充对类别列是稳妥选择,不会引入虚假的序关系。裁剪用分位数而不是固定阈值,是因为不同批次数据的价格尺度可能不同,分位数自适应更强。参数0.01和0.99可以调,如果你发现裁剪后样本掉得太多,放宽到0.005和0.995也行。注意测试集不做价格裁剪,因为测试集根本没有 price 列。

2.3 时间特征的拆解

regDate和creatDate是字符串格式的日期,直接丢给模型等于浪费。我习惯把它们拆成「注册年份」「注册月份」「使用月数」三个数值特征,其中使用月数 = creatDate 减 regDate,这个特征对价格影响很直接。

for df in [train, test]: df['regDate'] = pd.to_datetime(df['regDate'], format='%Y%m%d', errors='coerce') df['creatDate'] = pd.to_datetime(df['creatDate'], format='%Y%m%d', errors='coerce') df['regYear'] = df['regDate'].dt.year df['regMonth'] = df['regDate'].dt.month # 使用月数,单位月 df['usedMonths'] = (df['creatDate'] - df['regDate']).dt.days / 30

这里errors='coerce'是后悔药,遇到无法解析的日期会转成 NaT 而不是直接报错。usedMonths可能出现负值,说明 creatDate 早于 regDate,属于脏数据,后续可以统一置零或删掉。这一步做完,你的特征表就比原始列多出四个可用维度。

3. 特征工程:把 30 列原始字段榨出更多信号

3.1 类别特征的编码策略

这份数据里model、brand、bodyType、fuelType、gearbox都是类别型。LightGBM 和 XGBoost 对类别特征的处理方式不同:LightGBM 可以直接声明 categorical_feature,XGBoost 则更适合先做编码。我一般对品牌和车型用频次编码,对车身类型和燃油类型用 one-hot,因为后两者类别数少。

# 频次编码:用出现次数替代原始类别值 for col in ['brand', 'model']: freq = train[col].value_counts() train[col + '_freq'] = train[col].map(freq) test[col + '_freq'] = test[col].map(freq) test[col + '_freq'] = test[col + '_freq'].fillna(0) # one-hot 处理低基数类别 train = pd.get_dummies(train, columns=['bodyType', 'fuelType', 'gearbox']) test = pd.get_dummies(test, columns=['bodyType', 'fuelType', 'gearbox'])

频次编码的好处是不增加维度,同时保留了「这个品牌/车型有多常见」的信息。注意测试集映射时可能出现训练集没见过的类别,fillna(0)兜底。one-hot 之后训练集和测试集的列数必须对齐,如果测试集少了某个 dummy 列,后面预测会报错,可以用reindex对齐。

3.2 构造交互特征与统计特征

原始特征之外,我一般会加几个统计量:同一品牌下的平均价格、同一车型下的价格标准差、功率与里程的比值。这些特征在树模型里往往能带来一截提升。

# 品牌均价(只用训练集算,避免标签泄露) brand_price_mean = train.groupby('brand')['price'].mean() train['brand_price_mean'] = train['brand'].map(brand_price_mean) test['brand_price_mean'] = test['brand'].map(brand_price_mean) # 功率里程比,加 1 防止除零 for df in [train, test]: df['power_km_ratio'] = df['power'] / (df['kilometer'] + 1)

逻辑说明:brand_price_mean是目标编码的简化版,用训练集统计量映射到测试集,绝不能把测试集的 price 掺进来算。power_km_ratio是领域直觉——同样里程下功率越高,车况通常越好。参数上+1是防除零,如果你的数据里 kilometer 最小值就是 0,这个保护必须有。

3.3 特征筛选:别把噪声当信号

特征不是越多越好。构造完一堆特征后,我会用 LightGBM 自带的 feature_importance 做一轮筛选,把重要性为 0 或者极低的列去掉。常见做法是训练一个 100 轮的小模型,看 gain 排序,砍掉末尾 10% 到 20% 的特征。这一步能缩短训练时间,也能降低过拟合风险。注意不要用 accuracy 或 MSE 单独判断,树模型的特征重要性看 gain 比 split 次数更可靠。

4. 双模型训练:LightGBM 与 XGBoost 的参数怎么设

4.1 LightGBM 训练与五折交叉验证

LightGBM 在这份数据上的优势是训练快、对类别特征友好。我一般用五折交叉验证,每折记录验证集的 MAE,最后取平均。核心参数:learning_rate=0.05、num_leaves=31、feature_fraction=0.8、bagging_fraction=0.8、bagging_freq=5。

import lightgbm as lgb from sklearn.model_selection import KFold from sklearn.metrics import mean_absolute_error features = [c for c in train.columns if c not in ['price', 'SaleID', 'regDate', 'creatDate']] X = train[features] y = train['price'] X_test = test[features] kf = KFold(n_splits=5, shuffle=True, random_state=42) oof_preds = np.zeros(len(X)) test_preds = np.zeros(len(X_test)) for fold, (tr_idx, val_idx) in enumerate(kf.split(X)): tr_x, val_x = X.iloc[tr_idx], X.iloc[val_idx] tr_y, val_y = y.iloc[tr_idx], y.iloc[val_idx] model = lgb.LGBMRegressor( learning_rate=0.05, num_leaves=31, feature_fraction=0.8, bagging_fraction=0.8, bagging_freq=5, n_estimators=2000, verbose=-1 ) model.fit(tr_x, tr_y, eval_set=[(val_x, val_y)], eval_metric='mae', callbacks=[lgb.early_stopping(100)]) oof_preds[val_idx] = model.predict(val_x) test_preds += model.predict(X_test) / kf.n_splits print('LightGBM OOF MAE:', mean_absolute_error(y, oof_preds))

逻辑说明:early_stopping(100)表示验证集 MAE 连续 100 轮不下降就停,防止过拟合。n_estimators=2000是上限,实际会被 early stopping 截断。test_preds用五折结果平均,比单模型预测更稳。参数调整上,如果 MAE 偏高,先把learning_rate降到 0.03 再跑;如果训练太慢,把num_leaves降到 15。

4.2 XGBoost 训练与参数差异

XGBoost 的接口和 LightGBM 类似,但参数名不同。max_depth替代num_leaves,subsample替代bagging_fraction,colsample_bytree替代feature_fraction。我一般设max_depth=6、eta=0.05、subsample=0.8、colsample_bytree=0.8。

import xgboost as xgb xgb_test_preds = np.zeros(len(X_test)) xgb_oof = np.zeros(len(X)) for fold, (tr_idx, val_idx) in enumerate(kf.split(X)): tr_x, val_x = X.iloc[tr_idx], X.iloc[val_idx] tr_y, val_y = y.iloc[tr_idx], y.iloc[val_idx] model = xgb.XGBRegressor( max_depth=6, learning_rate=0.05, subsample=0.8, colsample_bytree=0.8, n_estimators=2000, early_stopping_rounds=100, eval_metric='mae', verbosity=0 ) model.fit(tr_x, tr_y, eval_set=[(val_x, val_y)], verbose=False) xgb_oof[val_idx] = model.predict(val_x) xgb_test_preds += model.predict(X_test) / kf.n_splits print('XGBoost OOF MAE:', mean_absolute_error(y, xgb_oof))

注意 XGBoost 的early_stopping_rounds是写在构造函数里的,和 LightGBM 的 callback 写法不同。另外 XGBoost 对缺失值有自己的处理逻辑,但我们已经提前填过,所以不冲突。两个模型跑完后,可以按 0.6 和 0.4 的权重做加权融合,通常比单模型低几十到一百的 MAE。

4.3 生成提交文件

提交格式参考used_car_sample_submit.csv,只有SaleID和price两列。融合后的预测值直接写入即可。

final_pred = 0.6 * test_preds + 0.4 * xgb_test_preds submission = pd.DataFrame({'SaleID': test['SaleID'], 'price': final_pred}) submission.to_csv('submission.csv', index=False) print(submission.head())

逻辑说明:权重 0.6/0.4 是经验值,你可以用 OOF 预测做网格搜索找最优权重。index=False避免多出一列索引,提交时格式不对会被判错。生成后建议用pd.read_csv再读一遍,确认列名和行数无误。

5. 避坑与排查:那些让 MAE 突然翻倍的细节

5.1 现象:MAE 高得离谱,超过 2000

原因:最常见的是price没有做对数变换或者极端值没裁。这份数据的 price 分布右偏严重,直接回归会被大值主导。解决:先裁剪分位数,再考虑对 price 取 log1p 训练,预测后再 expm1 还原。

5.2 现象:测试集预测报错,列数不匹配

原因:one-hot 之后训练集和测试集的 dummy 列不一致,测试集可能少某个类别。解决:用test = test.reindex(columns=train.columns, fill_value=0)对齐,但注意要把 price 列排除在外。

5.3 现象:交叉验证分数很好,提交后排名很差

原因:特征工程里用了测试集信息,比如用全量数据算品牌均价。解决:所有统计量必须只在训练集上计算,再映射到测试集。这是血泪经验,标签泄露在竞赛里是致命的。

5.4 现象:LightGBM 训练到一半报内存不足

原因:num_leaves设太大或者特征维度爆炸。解决:先把num_leaves降到 31 以下,同时用feature_fraction做列采样。如果还不行,把数据转成 float32 再训练。

5.5 现象:提交文件行数对不上

原因:测试集读取时用了sep=','导致行数错乱,或者裁剪时误伤了测试集。解决:测试集只做填充和对齐,绝不做价格裁剪。提交前用assert len(submission) == 50000卡一道。

6. 进阶技巧:用 OOF 权重搜索把融合效果再压一截

最后一章说一个我每次做回归融合都会走的流程:用 OOF 预测做权重搜索,而不是拍脑袋定 0.6/0.4。具体做法是把 LightGBM 和 XGBoost 的 OOF 预测存下来,在 0 到 1 之间以 0.05 为步长遍历权重,找 MAE 最低的组合。

best_w, best_mae = 0, float('inf') for w in np.arange(0, 1.05, 0.05): blend = w * oof_preds + (1 - w) * xgb_oof mae = mean_absolute_error(y, blend) if mae < best_mae: best_mae, best_w = mae, w print(f'best weight: {best_w:.2f}, best MAE: {best_mae:.2f}')

这段代码跑完你会得到一个具体的最优权重,通常和 0.5 有偏差。参数上步长 0.05 够用,想更细可以调到 0.01。注意这个权重是在 OOF 上搜的,直接套到测试集预测上即可,不需要重新训练。

另外两个可以试的方向:一是把usedMonths做分箱,变成类别特征喂给 LightGBM;二是对 brand 做目标编码时加平滑,公式是(count * mean + prior * smoothing) / (count + smoothing),smoothing 取 10 左右。这两个改动我在类似数据集上分别拿到过 1% 到 3% 的 MAE 下降。

验证方法很简单:每次改动后重新跑五折,对比 OOF MAE。如果 OOF 降了但提交分数没动,大概率是过拟合了验证集,这时候要回头检查特征是否泄露。从那以后我每次做完特征工程,都会强制走一遍「只用训练集统计、测试集映射」的检查,再跑交叉验证。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询