基于 LightGBM 的二手车价格预测
基于 LightGBM 的二手车价格预测
代码详见:https://github.com/xiaozhou-alt/Used-Car-Price-Prediction
文章目录
- 基于 LightGBM 的二手车价格预测
- 一、项目介绍
- 二、文件夹结构
- 三、数据集介绍
- 四、LightGBM 算法介绍
- 1. 基于直方图的决策树算法
- 2. Leaf-wise 树生长策略
- 3. 梯度单边采样(GOSS)
- 4. 互斥特征捆绑(EFB)
- 五、项目实现
- 1. 数据加载和预处理
- 2. 特征工程
- 3. 开始训练!
- 4. 评估与可视化
- 5. 主函数
- 六、结果展示
- Reference
一、项目介绍
这个项目使用 机器学习 技术预测二手车市场价格,基于车辆属性如品牌、型号、使用年限、行驶里程等特征构建预测模型。系统实现了完整的数据处理流程,包括:
- 数据清洗与预处理
- 特征工程
- 高级编码策略处理分类变量
- LightGBMLightGBMLightGBM 模型训练与调优
- 模型评估与可视化
本项目源于:【AI入门系列】车市先知:二手车价格预测学习赛_学习赛_赛题与数据_天池大赛-阿里云天池的赛题与数据 (aliyun.com)
数据集下载
:Used_Car_Price_Prediction | Kaggle
训练完成的模型下载
: Used_Car_Price_Prediction | Kaggle
二、文件夹结构
Used-Car-Price-Prediction/
├── README.md
├── data.ipynb # 数据分析文件
├── data/ # 数据文件夹├── README-data.md # 数据说明文档├── images/ # 数据集说明图片文件夹├── used_car_sample_submit.csv # 示例提交文件├── used_car_testA_20200313.csv # 测试集A├── used_car_testB_20200421.csv # 测试集B└── used_car_train_20200313.csv # 训练集
├── output/ # 输出文件夹├── feature_importance.csv # 特征重要性分析结果├── log/ # 日志文件夹├── model/ # 模型文件夹├── pic/ # 图片输出文件夹└── sample_predictions.csv # 样本预测结果
├── requirements.txt
├── train.py
└── 【AI入门系列】车市先知:二手车价格预测学习赛_学习赛_天池大赛-阿里云天池的赛制.pdf # 比赛说明文档
三、数据集介绍
该数据来自某交易平台的二手车交易记录,总数据量超过40w,包含31列变量信息,其中15列为匿名变量,包含两个CSV文件:
- 训练集:
used_car_train_20200313.csv
- 150,000条记录
- 包含31个特征字段和价格标签
- 测试集:
used_car_testB_20200421.csv
- 50,000条记录
- 包含31个特征字段,无价格标签
字段表
Field | Description |
SaleID | 交易ID,唯一编码 |
name | 汽车交易名称,已脱敏 |
regDate | 汽车注册日期,例如20160101,2016年01月01日 |
model | 车型编码,已脱敏 |
brand | 汽车品牌,已脱敏 |
bodyType | 车身类型:豪华轿车:0,微型车:1,厢型车:2,大巴车:3,敞篷车:4,双门汽车:5,商务车:6,搅拌车:7 |
fuelType | 燃油类型:汽油:0,柴油:1,液化石油气:2,天然气:3,混合动力:4,其他:5,电动:6 |
gearbox | 变速箱:手动:0,自动:1 |
power | 发动机功率:范围[0,600] |
kilometer | 汽车已行驶公里,单位万km |
notRepairedData | 汽车有尚未修复的损坏:是:0,否:1 |
regionCode | 地区编码,已脱敏 |
seller | 销售方:个体:0,非个体:1 |
offerType | 报价类型:提供:0,请求:1 |
creatDate | 汽车上线时间,即开始售卖时间 |
price | 二手车交易价格(预测目标) |
v系列特征 | 匿名特征,包含v0-14在内15个匿名特征 |
数据集二手车数量-价格区间:
四、LightGBM 算法介绍
背景与发展
LightGBMLightGBMLightGBM(Light Gradient Boosting Machine)是微软于2017年开源的分布式梯度提升框架,旨在解决传统 GBDT 算法在处理大规模数据时面临的主要瓶颈:
- 效率问题:传统 GBDT 算法在处理海量数据时训练速度慢
- 内存限制:需要将整个数据集加载到内存,难以处理大规模数据
- 维度灾难:对高维特征处理效率低
GBDT(梯度提升决策树)是一种广泛应用于机器学习的集成算法,主要用于处理结构化数据。其核心思想是通过将复杂问题分解为一系列简单的近似问题,从而逐步提高模型的预测精度。GBDT结合了决策树的优点,具有良好的可解释性和高效的特征工程能力,常用于分类和回归任务。它的实现通常涉及损失函数的优化和梯度下降法的应用,广泛应用于数据竞赛和实际业务中
LightGBM 通过创新性的算法优化,实现了训练速度快 101010 倍以上、内存消耗降低 50%50\%50% 的突破,成为竞赛和工业界最受欢迎的梯度提升框架之一
1. 基于直方图的决策树算法
直方图算法 的基本思想是:先把连续的浮点特征值离散化成 kkk 个整数,同时构造一个宽度为 kkk 的直方图。在遍历数据的时候,根据离散化后的值作为索引在直方图中累积统计量,当遍历一次数据后,直方图累积了需要的统计量,然后根据直方图的离散值,遍历寻找最优的分割点
直方图算法简单理解为:首先确定对于每一个特征需要多少个 箱子(bin)并为每一个箱子分配一个整数;然后将浮点数的范围均分成若干区间,区间个数与箱子个数相等,将属于该箱子的样本数据更新为箱子的值;最后用直方图(bins)表示。看起来很高大上,其实就是直方图统计,将大规模的数据放在了直方图中。
本项目中对于直方图算法:
# LightGBM参数配置
params = {'max_bin': 255, # 默认直方图分箱数'bin_construct_sample_cnt': 200000, # 构建直方图的样本数...
}
- 特征离散化:
- 对于power特征(发动机功率):原始值范围 0-600+
- LightGBM 自动将其离散化为 255255255 个 binsbinsbins(默认设置)
- 例如:0-2, 3-5, …, 598-600
- 分裂点计算优化:
- 传统方法需计算 15w15w15w 条记录的功率值
- LightGBM 只需计算 255255255 个 binsbinsbins 的统计量
- 计算量减少:150,000 → 255(减少 99.8%)
- 实际效果:
- power_bin特征在特征工程中已预分桶
- LightGBM 内部对连续特征(如vehicle_age)自动分桶
- 训练速度提升:CPU 模式从 小时 级降至 分钟 级
2. Leaf-wise 树生长策略
LightGBM 采用 Leaf-wise的增长策略,该策略每次从当前所有叶子中,找到分裂增益最大的一个叶子,然后分裂,如此循环。因此同 Level-wise 相比,Leaf-wise 的 优点 是:在分裂次数相同的情况下,Leaf-wise 可以降低更多的误差,得到更好的精度;Leaf-wise 的 缺点 是:可能会长出比较深的决策树,产生 过拟合。因此 LightGBM 会在Leaf-wise 之上增加了一个 最大深度的限制,在保证高效率的同时防止过拟合。
本项目中对于 Leaf-wise 树增长策略:
params = {'num_leaves': 63, # 控制叶子节点数量'max_depth': 7, # 限制树深'min_data_in_leaf': 50, # 叶子节点最小样本...
}
- 生长策略对比:
- 实际分裂过程:
- 首轮分裂:选择 vehicle_age(车龄)作为根节点,因其信息增益最高
- 第二轮:在年轻车辆(<3年)中选择 kilometer_log(对数里程)分裂
- 第三轮:在高里程车辆中选择notRepairedDamage(事故历史)分裂
- 优势体现:
- 更深的树结构捕捉 非线性 关系:
- 车龄与价格的指数衰减关系
- 事故车在特定车龄段的贬值规律
- 验证集 MAE 降低约 5%5\%5% 相比 Level-wise 策略
3. 梯度单边采样(GOSS)
GOSS 是一个样本的采样算法,目的是 丢弃 一些对计算信息增益没有帮助的样本留下有帮助的。根据计算信息增益的定义,梯度大的样本对信息增益有更大的影响。因此,GOSS 在进行数据采样的时候只保留了 梯度较大 的数据,但是如果直接将所有梯度较小的数据都丢弃掉势必会影响数据的总体分布。所以,GOSS 首先将要进行分裂的特征的所有取值按照绝对值大小 降序排序(XGBoostXGBoostXGBoost 一样也进行了排序,但是 LightGBM 不用保存排序后的结果),选取绝对值最大的 a∗100%a*100\%a∗100% 个数据。然后在剩下的较小梯度数据中随机选择 b∗100%b*100\%b∗100% 个数据。接着将这 b∗100%b*100\%b∗100% 个数据乘以一个常数 1−ab\frac{1-a}{b}b1−a,这样算法就会更关注训练不足的样本,而不会过多改变原数据集的分布。最后使用这 (a+b)∗100%(a+b)*100\%(a+b)∗100% 个数据来计算信息增益。下图是 GOSS 的具体算法:
本项目中对于 GOSS 算法:
params = {'boosting_type': 'gbdt', # 可改为'goss''top_rate': 0.2, # 大梯度样本保留比例'other_rate': 0.1, # 小梯度样本采样比例...
}
- 梯度计算:
- 高梯度样本:价格异常车辆(古董车/严重事故车)
- 如:101010 年车龄但价格极高的收藏车
- 低梯度样本:普通家用车
- 采样过程:
def GOSS_sampling(gradients):# 获取梯度最大的前20%样本top_samples = np.argsort(gradients)[-int(0.2*len(gradients)):]# 随机抽取剩余样本的10%remaining = [i for i in range(len(gradients)) if i not in top_samples]rand_samples = np.random.choice(remaining, int(0.1*len(gradients)))return np.concatenate([top_samples, rand_samples])
- 实际效益:
- 训练样本减少:150,000 → (30,000 + 12,000) = 42,000
- 训练速度提升:约 3.53.53.5 倍加速
- 精度保持:MAE 变化 < 0.5%0.5\%0.5%
4. 互斥特征捆绑(EFB)
高维度的数据往往是 稀疏 的,这种稀疏性启发我们设计一种无损的方法来 减少特征的维度。通常被捆绑的特征都是 互斥 的(即特征不会同时为非零值,像one-hot),这样两个特征捆绑起来才不会丢失信息。如果两个特征并不是完全互斥(部分情况下两个特征都是非零值),可以用一个指标对特征不互斥程度进行衡量,称之为 冲突比率,当这个值较小时,我们可以选择把不完全互斥的两个特征捆绑,而不影响最后的精度。
将相互独立的特征进行绑定是一个 NP-Hard 问题,LightGBM 的 EFB 算法将这个问题转化为图着色的问题来求解,将所有的特征视为图的各个顶点,将不是相互独立的特征用一条边连接起来,边的 权重 就是两个相连接的特征的 总冲突值,这样需要绑定的特征就是在图着色问题中要涂上同一种颜色的那些点(特征)。此外,我们注意到通常有很多特征,尽管不是 100%100\%100% 相互排斥,但也很少同时取非零值。 如果我们的算法可以允许一小部分的冲突,我们可以得到更少的特征包,进一步提高计算效率。经过简单的计算,随机污染小部分特征值将 影响精度 最多 O([(1−γ)n−23])O([(1-\gamma)n^{-\frac{2}{3}}])O([(1−γ)n−32]) , γ\gammaγ 是每个绑定中的最大冲突比率,当其相对较小时,能够完成精度和效率之间的平衡。
下图的伪代码中,左边为 特征捆绑的贪心算法,右边为特征合并算法:
特征合并算法,其关键在于原始特征能从合并的特征中分离出来。绑定几个特征在同一个 bundlebundlebundle 里需要保证绑定前的原始特征的值可以在 bundlebundlebundle 中识别,考虑到histogram-based算法 将连续的值保存为离散的 binsbinsbins,我们可以使得不同特征的值分到 bundlebundlebundle 中的不同 binbinbin(箱子)中,这可以通过在特征值中加一个 偏置常量 来解决。比如,我们在 bundlebundlebundle 中绑定了两个特征 AAA 和 BBB,AAA 特征的原始取值为区间 [0,10)[0,10)[0,10),BBB 特征的原始取值为区间 [0,20)[0,20)[0,20),我们可以在 BBB 特征的取值上加一个偏置常量 101010,将其取值范围变为[10,30)[10,30)[10,30),绑定后的特征取值范围为 [0,30)[0, 30)[0,30),这样就可以放心的融合特征 AAA 和 BBB 了。
本项目中对于 EFB 算法:
params = {'feature_fraction': 0.8, # 特征采样比例'bundle_size': 10, # 捆绑特征的最大数量...
}
特征互斥性分析(仅为例证,并非是数据集的特征):
特征组 | 互斥特征 | 共存概率 |
---|---|---|
车身类型 | bodyType_suv , bodyType_sedan | <1%<1\%<1% |
燃油类型 | fuelType_diesel , fuelType_electric | 0.01%0.01\%0.01% |
地域特征 | regionCode_101 , regionCode_205 | 0%0\%0% |
… | … | … |
五、项目实现
1. 数据加载和预处理
- 日期特征处理:
- 检测并修复无效日期格式(非 888 位数字或含非数字字符)
- 使用 中位数 日期替换无效值(训练集计算,测试集复用)
- 提取 年、月、日 特征增强时间维度信息
- 车龄计算:
- 计算 车辆年龄:(创建日期−注册日期)/365.25车辆年龄:(创建日期 - 注册日期) / 365.25车辆年龄:(创建日期−注册日期)/365.25
此处加入闰年取平均(闰年4年一次):(365*3+366)/4=365.25
- 修正负车龄(设为 000),处理数据异常
- 分类特征编码:
- 训练集:为每个分类特征创建 LabelEncoderLabelEncoderLabelEncoder 并存储
- 测试集:复用训练集编码器,处理未知类别:
- 新类别标记为“unknown”
- 使用训练集最常见的类别编码处理未知值
- 支持 999 个分类特征:车型、品牌、车身类型等
- 数据清理:
- 删除 原始日期 列和 名称 列
- 使用 中位数 填充所有缺失值
def load_and_preprocess(file_path, is_train=True):df = pd.read_csv(file_path, sep=' ')# 日期特征处理 - 处理无效日期for col in ['regDate', 'creatDate']:...# 分类特征编码cat_features = ['model', 'brand', 'bodyType', 'fuelType', 'gearbox', 'notRepairedDamage', 'regionCode', 'seller', 'offerType']if is_train:# 训练集:拟合编码器for col in cat_features:...# 测试集:使用训练集的编码器for col in cat_features:...# 删除原始日期列...# 处理缺失值...return df
2. 特征工程
- 功率分桶:
- 将连续功率值离散化为 666 个区间:负值/ 000、低功率(0−100)(0-100)(0−100)、中功率(100−200)(100-200)(100−200)等
- 处理 NaNNaNNaN 值(设为 000 )
- 非线性变换:
对行驶里程进行对数变换( log1plog1plog1p ),缓解偏态分布影响 - 特征组合:
对匿名特征 v0v_0v0 到 v4v_4v4 创建平方特征,捕捉非线性关系
创建 年月 组合特征(如 202001202001202001 表示 202020202020 年 111 月)
def feature_engineering(df):# 功率分桶 - 修复边界并安全转换df['power_bin'] = pd.cut(df['power'],bins=[-np.inf, 0, 100, 200, 300, 600, np.inf],labels=[0, 1, 2, 3, 4, 5],include_lowest=True).astype('int64')# 处理可能的NaN值df['power_bin'].fillna(0, inplace=True)# 里程对数变换df['kilometer_log'] = np.log1p(df['kilometer'])# 匿名特征组合for i in range(5):df[f'v_{i}_squared'] = df[f'v_{i}'] ** 2# 创建日期相关特征df['creatDate_year_month'] = df['creatDate_year'] * 100 + df['creatDate_month']return df
3. 开始训练!
- 参数配置:
- 使用 MAE 作为损失函数(regression_l1)
- 强正则化设置:特征采样(80%80\%80%)、数据采样(80%80\%80%)、L1/L2L1/L2L1/L2 正则
- 结构控制:叶子数(636363)、最大深度(777)、叶子最小样本(505050)
- 大迭代次数(150,000150,000150,000)配合 早停 机制
- 训练过程:
- 使用验证集监控模型性能
- 每 100100100 轮输出一次评估指标
- 早停机制防止过拟合( 100010001000 轮无提升)
def train_model(X_train, y_train, X_val, y_val):# 基本参数params = {'objective': 'regression_l1', # MAE损失'boosting_type': 'gbdt','metric': 'mae','num_leaves': 63, # 减少叶子数量防止过拟合'learning_rate': 0.01, # 降低学习率'feature_fraction': 0.8, # 增加正则化'bagging_fraction': 0.8,'bagging_freq': 10,'min_data_in_leaf': 50, # 增加叶子最小样本'min_child_samples': 30,'reg_alpha': 0.8, # 增强L1正则'reg_lambda': 0.8, # 增强L2正则'max_depth': 7, # 限制树深'n_estimators': 150000, # 增加迭代次数'early_stopping_round': 1000,'verbosity': -1,'seed': 42}# 尝试使用GPU,如果失败则回退到CPUtry:...# 使用CPU参数cpu_params = params.copy()train_data = lgb.Dataset(X_train, label=y_train)val_data = lgb.Dataset(X_val, label=y_val)model = lgb.train(...print("CPU训练成功完成!")return model
训练输出示例:
正在加载和预处理训练数据…
训练数据形状: (150000, 35)
正在进行特征工程…
特征矩阵形状: (150000, 41)
目标变量形状: (150000,)
划分训练集和验证集…
训练集: (120000, 41), 验证集: (30000, 41)
开始训练模型…
尝试使用GPU进行训练…
GPU训练失败: No OpenCL device found
回退到CPU训练…
Training until validation scores don’t improve for 200 rounds
[100] train’s l1: 2227.66 valid’s l1: 2219.08
[200] train’s l1: 1329.99 valid’s l1: 1326.9
[300] train’s l1: 980.502 valid’s l1: 985.605
[400] train’s l1: 826.208 valid’s l1: 840.589
…
[101800] train’s l1: 317.29 valid’s l1: 491.269
[101900] train’s l1: 317.257 valid’s l1: 491.266
[102000] train’s l1: 317.23 valid’s l1: 491.264
Early stopping, best iteration is:
[101854] train’s l1: 317.269 valid’s l1: 491.264
CPU训练成功完成!
模型已保存到 /kaggle/working/output/car_price_model.pkl
4. 评估与可视化
本项目使用为 MAE (Mean Absolute Error) 作为评估标准:
若 真实值为 =(y1,y2,…,yn)= (y_{1},y_{2},\dots ,y_{n})=(y1,y2,…,yn) ,模型的 预测值为 y^=(y^1,y^2,…,y^n)\hat{y} = (\hat{y}_1,\hat{y}_2,\dots ,\hat{y}_n)y^=(y^1,y^2,…,y^n) ,那么该模型的 MAE 计算公式为
MAE=∑i=1n∣yi−y^i∣nMAE = \frac{\sum_{i = 1}^{n}\left|y_{i} - \hat{y}_{i}\right|}{n} MAE=n∑i=1n∣yi−y^i∣
例如,真实值 y=(15,20,12)y = (15,20,12)y=(15,20,12) ,预测值 y^=(17,24,9)\hat{y} = (17,24,9)y^=(17,24,9) ,那么这个预测结果的 MAE 为
MAE=∣15−17∣+∣20−24∣+∣12−9∣3=3MAE = \frac{|15 - 17| + |20 - 24| + |12 - 9|}{3} = 3 MAE=3∣15−17∣+∣20−24∣+∣12−9∣=3
MAE 越小,结果越准确。
- 特征重要性分析:
- 基于信息增益计算特征重要性
- 可视化 Top30Top30Top30 重要特征
- 保存重要性数据和图表
- 训练过程监控:
- 绘制 训练/验证 MAE 曲线
- 标记最佳验证点(最小 MAE 位置)
- 保存训练历史图表
# 4. 特征重要性分析
def plot_feature_importance(model, features):importance = pd.DataFrame({'Feature': features,'Importance': model.feature_importance(importance_type='gain')}).sort_values('Importance', ascending=False)...return importance# 5. 绘制训练历史
def plot_training_history(evals_result):plt.figure(figsize=(10, 6))# 提取训练和验证的MAEtrain_mae = evals_result['train']['l1']valid_mae = evals_result['valid']['l1']# 绘制曲线plt.plot(train_mae, label='Training MAE')plt.plot(valid_mae, label='Validation MAE')# 找到最小验证误差点if valid_mae:...
5. 主函数
- 数据准备阶段:
加载并预处理训练数据;执行特征工程;确保所有特征为数值类型;处理缺失值 - 模型训练阶段:
划分训练集/验证集(80%/20%80\%/20\%80%/20%);训练LightGBM模型;保存模型到文件 - 模型评估阶段:
计算验证集 MAE;分析并可视化特征重要性;随机抽样 101010 个样本进行详细验证;保存样本预测结果 - 测试预测阶段:
加载并预处理测试数据;复用训练集的处理流程和编码器;执行特征工程;生成预测结果并保存为提交文件 - 可视化与报告:
绘制训练历史曲线;保存所有输出结果到 ./output./output./output 目录
def main():# 加载训练数据print("正在加载和预处理训练数据...")train_df = load_and_preprocess('/kaggle/input/used-car-price-prediction/data/used_car_train_20200313.csv', is_train=True)print("训练数据形状:", train_df.shape)print("正在进行特征工程...")train_df = feature_engineering(train_df)# 确保所有特征都是数值类型...# 准备训练数据X = train_df.drop(columns=['price', 'SaleID'])y = train_df['price']print("特征矩阵形状:", X.shape)print("目标变量形状:", y.shape)# 划分数据集print("划分训练集和验证集...")...# 训练模型print("开始训练模型...")model = train_model(X_train, y_train, X_val, y_val)...# 评估模型val_pred = model.predict(X_val)...# 特征重要性分析print("分析特征重要性...")...# 随机样本验证print("随机选取10个样本进行验证...")...# 保存样本数据sample_path = '/kaggle/working/sample_predictions.csv'...# 处理测试数据print("\n处理测试数据...")...# 确保所有特征都是数值类型...# 预测测试集print("预测测试集...")test_pred = model.predict(X_test)...# 绘制训练历史...print("\n训练完成!所有结果已保存到 ./output 目录")
六、结果展示
模型在验证集上得到的 样本 预测基本信息结果如下所示:
特征重要性 Top30Top\ 30Top 30 排行:
战绩可查 ∠( ᐛ 」∠)_:
Reference
[1] Ke G, Meng Q, Finley T, et al. Lightgbm: A highly efficient gradient boosting decision tree[C]//Advances in Neural Information Processing Systems. 2017: 3146-3154.
[2] 知乎-深入理解LightGBM
如果你喜欢我的文章,不妨给小周一个免费的点赞和关注吧!