资讯动态

二手车价格预测实战:从数据清洗到XGBoost建模全流程详解

发布时间:2026/9/13 3:56:47 来源:尧图企业网站定制
简介一份面向计算机、通信、人工智能、自动化等专业学生与从业者的二手车价格预测数据挖掘课程大作业资源覆盖从数据预处理、特征工程到模型构建与评估的完整流程适合期末课程设计、大作业或毕业设计参考。压缩包共26个文件整体大小34.86MB核心为2个Python源码文件配合CSV数据集完成训练与预测9张PNG图片保存可视化分析结果DOCX文档为实验报告另有XML、IML等PyCharm工程配置文件便于直接打开运行和二次修改。已有63人学习浏览项目代码经过调试测试可稳定运行实验报告结构完整能帮助快速理解二手车价格预测的建模思路和关键步骤。基础较好的读者可在现有框架上调整特征或模型进一步扩展功能具备较高的学习借鉴与复用价值。1. 二手车价格预测大作业的建模主线与常见翻车点先说一个反直觉的结论在二手车价格预测这类数据挖掘课设里决定最后分数上限的往往不是哪个模型调得好而是你有没有把「数据清洗 → 特征工程 → 建模评估」这条主线讲圆。很多人把精力全花在调参上结果答辩时连价格分布为什么偏态、类别变量为什么用 target encoding 都说不清直接被追问卡住。这份项目包包含完整 Python 源码、可直接运行的数据集和一份 97 分以上的实验报告覆盖了数据探索EDA、缺失值处理、特征构造、多模型对比和结果可视化的完整链路。对计算机、人工智能、自动化方向的在校生来说它既是一份可以直接跑通的课程大作业模板也是一个用来理解回归任务全流程的现成样本。哪怕你基础偏弱只要照着源码把数据和模型替换成自己的场景就能快速改成一份能过盲审的期末项目。2. 数据体检字段分布、缺失值与价格长尾2.1 先看清数据长什么样加载与结构概览拿到数据集的第一步不是建模而是搞清楚每一列的业务含义、取值范围和缺失程度。常见二手车数据集通常包含名称、年份、已行驶里程、售价、燃料类型、变速箱类型、车主数量等字段有的还会带马力、排量、发动机排量这类配置信息。用 pandas 加载后我一般会先看df.info()和df.describe()把字段类型和数值分布一次性摆在眼前。import pandas as pd import numpy as np df pd.read_csv(car_price.csv) print(df.info()) # 查看字段类型与非空计数 print(df.describe()) # 数值字段的均值、分位数、极值 print(df.head(5)) # 抽查前几行确认数据格式这段代码的核心作用是快速建立对数据集的整体认知info()告诉你哪些列有缺失、哪些是 object 类型需要编码describe()展示售价、里程这类数值字段的量纲差异——比如售价可能是 30 万量级而里程是 10 万量级后续建模前必须归一化或树模型直接吃原始值。这里有个常见误用很多人拿到数据直接describe()然后不细看结果忽略了某些字段出现 0 值或极端值后面模型偏差全从这里来。2.2 价格分布与对数变换为什么预测目标要处理二手车价格几乎一定是右偏分布即大量车集中在低价区间少数豪车把均值拉得很高。如果直接拿原始价格做回归模型会被少数高价样本主导RMSE 很难降下来。处理方式是先对价格取对数再训练模型预测完后用exp还原。import matplotlib.pyplot as plt import seaborn as sns fig, axes plt.subplots(1, 2, figsize(12, 4)) sns.histplot(df[selling_price], bins50, axaxes[0]) axes[0].set_title(Original Price Distribution) df[log_price] np.log1p(df[selling_price]) sns.histplot(df[log_price], bins50, axaxes[1]) axes[1].set_title(Log-transformed Price Distribution) plt.tight_layout() plt.savefig(price_distribution.png, dpi150)np.log1p是log(1x)好处是当价格有 0 值时不会产生负无穷。做这个变换的核心原因是让目标变量更接近正态分布梯度下降类模型收敛更快树模型虽然不受分布影响但回归指标 RMSE 对高价样本的惩罚依然存在所以对数变换在二手车价格预测里几乎是标配操作。到这里你会发现报告里如果能放一张「变换前后分布对比图」比写三行文字解释偏态更直观。2.3 缺失值、异常值与相关性初筛数据清洗阶段要回答三个问题哪些字段有缺失、缺失占比多少、缺失是否与售价相关哪些样本是异常值比如里程为 0 的新车、价格翻倍的同款车型特征之间是否存在强相关性导致冗余。missing df.isnull().sum() missing missing[missing 0].sort_values(ascendingFalse) print(missing) # 相关性矩阵只取数值列 num_cols df.select_dtypes(include[np.number]).columns corr df[num_cols].corr() plt.figure(figsize(10, 8)) sns.heatmap(corr, annotTrue, fmt.2f, cmapcoolwarm, center0) plt.savefig(correlation_heatmap.png, dpi150)相关性热力图的直接价值是帮你发现冗余特征。比如年份和车龄是负相关的两列同时保留会导致线性模型的多重共线性树模型里虽然不致命但会稀释特征重要性排序。这里我的习惯是先用热力图初步判断再结合业务场景决定删哪列——保留车龄而不是年份因为模型对「车龄 5 年」的泛化能力比「年份 2019」更强。注意缺失值处理不要一上来就 fillna先看缺失列的类型。数值型的缺失可以用中位数填类别型的缺失要用「缺失本身作为一个类别」去编码这两种策略在下一章展开。3. 特征工程与编码从类别变量到价格规律3.1 数值特征缺失值填充中位数与插值的取舍经典型缺失场景有里程缺失、发动机排量缺失和座位数缺失。里程和排量是右偏分布用均值填充会被极大值带偏中位数更稳座位数这类离散字段可以直接用众数。更讲究一点的做法是按品牌分组填充——同一品牌下车型的配置差异不太大组内中位数比全局中位数更合理。# 按品牌分组的里程中位数填充 df[km_driven] df.groupby(name)[km_driven].transform( lambda x: x.fillna(x.median()) ) # 离散字段用众数填充 df[seats] df[seats].fillna(df[seats].mode()[0])注意transform与apply的区别这里要用transform保持索引对齐否则填充完的 Series 无法直接赋回原数据。组内填充在前全局填充兜底是一条不容易出错的处理顺序。3.2 类别变量编码Label Encoding、One-Hot 与 Target Encoding 的边界类别变量的处理是这个项目的核心拉开分差的地方。品牌、车型、燃料类型、变速箱都算类别字段。三种编码各自的适用场景编码方式适用场景优点风险Label Encoding有序类别如车主数量 1/2/3简单不增加维度无序类别会强行赋值导致错误序关系One-Hot Encoding类别数量少 10 类无偏可解释性强类别多时维度爆炸稀疏严重Target Encoding高基数类别如车型直接编码价格均值信息容易过拟合必须配合交叉验证对这个数据集我一般这样处理车主数量、变速箱手动/自动用 Label Encoding燃料类型类别少但无天然顺序用 One-Hot车型或品牌这类高基数特征必须用 Target Encoding否则 One-Hot 会生成上百列模型训练慢且泛化差。from sklearn.preprocessing import LabelEncoder # 有序或二分类字段用 LabelEncoder le LabelEncoder() df[transmission] le.fit_transform(df[transmission]) df[owner_type] le.fit_transform(df[owner_type]) # 高基数类别字段用 target encoding带平滑 def target_encode(series, target, alpha10.0): global_mean target.mean() group_mean target.groupby(series).agg([mean, count]) smooth (group_mean[mean] * group_mean[count] global_mean * alpha) / (group_mean[count] alpha) return series.map(smooth) df[brand_encoded] target_encode(df[brand], df[log_price])alpha是平滑系数它的作用是防止类别样本数太少时编码值直接等于该类的价格均值从而过拟合。比如某个车型只有 1 条样本它的 target encoding 值会被拉向全局均值而不是完全信任这 1 条数据。实际使用中建议把 target encoding 放在交叉验证内部计算避免泄漏这是进阶做法的关键一步。3.3 构造有业务含义的新特征原始数据里的字段通常不够直接。常见做法是构造车龄、每公里折旧率、品牌平均价格偏离度这类特征。车龄直接由当前年份减去车辆出厂年份得到这个特征在二手车里对价格的影响往往排前二。from datetime import datetime current_year datetime.now().year df[car_age] current_year - df[year] df[age_km_ratio] df[car_age] / (df[km_driven] 1) # 品牌高端度品牌内平均价格的排名 brand_price_mean df.groupby(brand)[selling_price].transform(mean) df[brand_price_premium] df[selling_price] / brand_price_mean - 1age_km_ratio的含义是每行驶一公里对应的车龄它能捕捉「车龄大但里程低」的潜在价值。brand_price_premium则是单辆车相对其品牌均价的偏离大于 0 说明该车在品牌内属于高配版本。这类特征的作用是让线性模型也能学到非线性关系——你不需要显式告诉模型丰田和保时捷的残值率差异特征里已经带上了品牌的价格层级。4. 模型对比与网格调参线性回归、随机森林与 XGBoost4.1 数据集划分与评估指标训练集与测试集的划分要注意两个点随机种子固定才能复现回归任务的评估不要只看 R²RMSE 和 MAE 必须一起看。R² 反映解释力RMSE 对大误差敏感MAE 反映平均绝对偏差。在二手车价格预测场景里RMSE 的绝对值更有业务含义——比如测试集上 RMSE 是 4 万意味着平均预测误差在 4 万元上下。from sklearn.model_selection import train_test_split features [c for c in df.columns if c not in [selling_price, log_price, name, year]] X df[features] y df[log_price] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 )4.2 三个模型的定位与对比思路基线模型用线性回归Ridge 更稳随机森林负责验证非线性上限XGBoost 是树模型梯度提升的代表。三个模型的组合恰恰覆盖了数据挖掘课程的知识点线性模型的解释性、Bagging 的方差降低、Boosting 的偏差降低。评估时统一用相同的训练测试划分否则对比无意义。from sklearn.linear_model import Ridge from sklearn.ensemble import RandomForestRegressor from xgboost import XGBRegressor from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score import numpy as np models { Ridge: Ridge(alpha1.0), RandomForest: RandomForestRegressor(n_estimators300, random_state42, n_jobs-1), XGBoost: XGBRegressor(n_estimators300, learning_rate0.05, random_state42) } for name, model in models.items(): model.fit(X_train, y_train) pred model.predict(X_test) pred_price np.expm1(pred) y_test_price np.expm1(y_test) print(f{name}: R2{r2_score(y_test_price, pred_price):.4f}, fRMSE{mean_squared_error(y_test_price, pred_price, squaredFalse):.0f}, fMAE{mean_absolute_error(y_test_price, pred_price):.0f})注意这里评估时把expm1还原成真实价格再算指标而不是在对数空间算。因为评委关心的是预测价格差多少钱对数值 RMSE 是 0.3 没有业务概念。三个模型的直接对比能体现随机森林相对线性回归的提升幅度、XGBoost 相对随机森林的进一步优化这一段是实验报告里最有说服力的部分。4.3 网格搜索的参数范围与防过拟合策略网格搜索的经典坑是把参数范围设得太大导致搜索耗时成倍增长。我一般先粗搜定位、再细搜微调。下面是 XGBoost 的常用搜索范围和最终推荐值。from sklearn.model_selection import GridSearchCV param_grid { max_depth: [3, 5, 7], # 树深度越大越易过拟合 learning_rate: [0.01, 0.05, 0.1], # 学习率越小越稳但需更多树 subsample: [0.7, 0.8, 1.0], # 行采样比例防过拟合 colsample_bytree: [0.7, 0.9] # 列采样比例 } xgb XGBRegressor(n_estimators300, random_state42, early_stopping_rounds20) grid GridSearchCV( xgb, param_grid, cv5, scoringneg_root_mean_squared_error, n_jobs-1, verbose1 ) grid.fit(X_train, y_train, eval_set[(X_test, y_test)], verboseFalse) print(grid.best_params_)三个关键参数的业务直觉max_depth控制树的复杂度车龄、里程、品牌之间虽然有些交叉效应但一般不需要很深就能捕捉subsample和colsample_bytree是防过拟合的标配能显著提升测试集上的表现early_stopping_rounds让模型在验证集指标不再提升时提前停止省时间且能防止树数量过多带来的过拟合。下表是一个典型对比结果具体数值会因数据集不同而浮动但提升幅度和排序应该一致模型R²RMSE元MAE元Ridge0.825200036000RandomForest0.913800024000XGBoost0.933300020000注意网格搜索用neg_root_mean_squared_error而不是neg_mean_squared_error这样评分量纲是元而不是元的平方打印出来的分数更直观看得懂。如果数据量超过 5 万条优先用RandomizedSearchCV同样的时间预算下找到的好参数更多。5. 实验报告组织与代码可复现性细节5.1 报告结构的顺序和篇幅分配这份课设的实验报告能拿高分顺序很关键。我建议按「问题定义 → 数据探索 → 特征工程 → 建模对比 → 结论与改进方向」组织电子版 20 页左右正文 6000 字上下即可重点不是堆文字而是每章关联一张图。问题定义部分写清楚因变量是二手价格、任务是回归、评估指标是 RMSE/MAE数据探索放前面做过的价格分布图、相关性热力图和缺失值统计表特征工程部分要写出每一步的理由——比如为什么用 target encoding 而不是 one-hot为什么构造车龄特征建模对比部分直接放模型评估表格和特征重要性图。5.2 特征重要性如何解读随机森林和 XGBoost 都能直接输出特征重要性分数做法是pd.Series(model.feature_importances_, indexfeatures).sort_values().plot.barh()。常见结果是车龄、里程、品牌编码位列前三这正好呼应答辩时「哪个特征对价格影响最大」的经典问题。报告里配上图后可以写一段有信息量的观察品牌编码的 target encoding 本质上携带了品牌均价信息这让特征重要性的解读需要谨慎——高重要性不代表因果关系而是相关关系。5.3 可复现性清单与答辩展示技巧代码侧需要固定三样东西随机种子模型和划分均用random_state42、依赖版本pip freeze requirements.txt、数据读写路径的相对化用os.path.join而不是写死绝对路径。答辩展示时有一个好用的技巧在训练循环里把每次实验的参数字典、RMSE、R² 追加到一个列表并转成 DataFrame 保存为 CSV现场演示时直接展示这个调参记录表。import csv results [] for lr in [0.01, 0.05, 0.1]: model XGBRegressor(n_estimators200, learning_ratelr, random_state42) model.fit(X_train, y_train) pred np.expm1(model.predict(X_test)) y_true np.expm1(y_test) results.append({ learning_rate: lr, rmse: mean_squared_error(y_true, pred, squaredFalse), r2: r2_score(y_true, pred) }) pd.DataFrame(results).to_csv(tuning_log.csv, indexFalse)这个 CSV 的价值在于它展示了你「调参是有方法论地试而不是瞎试」参数变化带来的指标波动一目了然答辩时直接把表投出来就完成了实验过程的证明。对比那些只在报告里放一个最优参数的作业这一个小细节就是区分 90 分和 97 分的分水岭。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价