ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

二手车价格预测:数据挖掘回归项目全流程实战

2026/9/17 2:32:17 拓冰建站 浏览量
二手车价格预测:数据挖掘回归项目全流程实战 简介这是一份面向计算机相关专业学生与数据挖掘初学者的Python期末大作业完整方案围绕二手车价格预测任务覆盖数据清洗、特征工程、模型训练与结果可视化等核心环节。项目源码经过本地编译调试可稳定运行并配有实验报告与数据集适合用于课程大作业参考、毕业设计预研或个人项目实战练习。资源包共26个文件约34.86MB主要包括2个Python脚本、1个CSV数据集、9张结果图片、1份Word实验报告以及若干PyCharm工程配置文件图片可直观呈现价格分布与预测效果报告则给出分析思路与结论便于对照源码理解。资源发布后已有168人学习下载说明其内容对同类任务有较好的参考价值。下载后可按目录结构快速定位源码、数据和报告整体难度适中能够帮助使用者少走弯路快速完成一份结构完整、可演示的期末大作业。1. 二手车价格预测数据挖掘期末作业里的典型回归题二手车价格预测是高校数据挖掘、机器学习课程里常年被选作期末大作业的题目。原因是它足够“经典”价格是连续型目标变量属于有监督回归问题工具链上只需要 Python 加 pandas、scikit-learn 就能跑通“业务理解—数据准备—建模—评估—报告”全流程不依赖 GPU 也不依赖深度学习框架。另一个让它适合做课程项目的原因是数据噪声大里程、年份、品牌、车况同时作用于价格给特征工程和结果分析留出了充足发挥空间。做得好的作业加分点往往不在调参上而在对数据的整理和对误差的合理解释上。所谓“源码数据集实验报告”本质就是把这道题的完整交付物整理出来一个能一键跑出结果的 Python 工程配一份能把每个决策讲明白的数据挖掘实验报告。2. 数据集预处理与探索性分析先看数据再定模型以“数据挖掘”冠名的作业第一件事不是选算法而是把数据集读进来核对字段结构检查缺失和分布。二手车价格数据多来自交易平台爬虫CSV 文件大小从几十 MB 到几百 MB 不等字段在 10 到 15 个之间大致分四类车辆属性品牌、车型、年份、排量、变速箱、使用情况里程、过户次数、事故记录、目标变量 price以及冗余字段车源编号、标题文本。见到数据集先写一个加载脚本把字段类型和缺失情况打印出来看几分钟再决定下一步。2.1 用 pandas 读入数据集并核对字段类型二手车数据集常因爬虫编码问题读不进来UTF-8 失败就试encodinggbk。下面这段代码完成读取和初步体检import pandas as pd df pd.read_csv(car_data.csv, encodingutf-8) # 查看数据量、字段类型、缺失数量 print(数据量:, df.shape) print(df.info()) print(df.isnull().sum()) # 数值字段的均值、标准差、分位数 print(df.describe().T) # 前5行数据直观确认字段内容 print(df.head())info()一次性展示字段类型和缺失统计。二手车数据集里常见的坑是 year 被读成int64但 mileage 被读成object因为源数据里写了“12.3万公里”这种带单位的字符串displacement 字段里混入“1.5T”也常见。这些类型不对齐如果不先识别出来后面建模会直接报错。describe()只看数值列但观察均值和中位数的差距已经能判断哪些字段存在明显的偏态分布。常见字段清单如表所示这张表也可以直接放进实验报告当作数据字典。字段名类型含义常见问题pricefloat售价万元分布右偏需做对数变换yearint注册年份需要转换为车龄mileagefloat/object已行驶里程万公里字符串混入单位brandobject品牌类别基数 30-100modelobject具体车型类别基数大易过拟合displacementfloat排量L混入涡轮标识字符transmissionobject手动/自动等低基数直接编码fuel_typeobject燃油类型类别少直接编码accidentobject事故记录缺失比例可能偏高2.2 缺失值处理中位数填充比均值更稳缺失集中在事故记录、里程和部分配置字段上。操作原则是数值字段用中位数填充类别字段用众数填充缺失比例超过 40% 的字段直接剔除因为引入的噪声大于信号。# 统计缺失比例并剔除超过40%的列 missing_ratio df.isnull().mean() drop_cols missing_ratio[missing_ratio 0.4].index.tolist() df df.drop(columnsdrop_cols) # 数值列用中位数填充类别列用众数填充 num_cols df.select_dtypes(include[float64, int64]).columns.drop(price) cat_cols df.select_dtypes(include[object]).columns for col in num_cols: df[col] df[col].fillna(df[col].median()) for col in cat_cols: df[col] df[col].fillna(df[col].mode()[0])价格相关的特征里程、车龄分布天然右偏少数高里程样本会把均值拉高中位数的鲁棒性更好。mode()[0]取众数列表第一个值避免某列出现双众数时返回一个 Series 导致赋值报错。处理完执行df.isnull().sum().sum()确认全表为 0 再进入下一步。2.3 价格分布与异常值筛查先取对数再看箱线图建模前至少画两幅图价格直方图和数值特征的箱线图。二手车价格长尾严重两三万的手动小面包车和五六十万的高端车挤在同一坐标轴上直方图看不出有效形态。import matplotlib.pyplot as plt import numpy as np fig, axes plt.subplots(1, 2, figsize(12, 4)) # 左图原始价格分布 axes[0].hist(df[price], bins50, edgecolorwhite) axes[0].set_title(Price Distribution (raw)) # 右图对数价格分布 log_price np.log1p(df[price]) axes[1].hist(log_price, bins50, edgecolorwhite) axes[1].set_title(Price Distribution (log)) plt.tight_layout() plt.savefig(fig_price_dist.png, dpi150)取log1p有两个作用压缩长尾让模型更容易拟合平滑的目标训练后预测结果可以再用expm1还原成“万元”不影响报告解释。目标做对数变换后模型优化的是对数空间中的误差通常能显著降低测试集上的 RMSE。异常值处理要克制建议不要按分位数一刀切删除。只处理两类一是价格离群但车况字段明显对应不上比如一万元价格配三年车龄和零里程属于录入错误二是新车指导价远低于二手车价属于脏数据。箱线图找出离群点后针对个案确认再删除。提示在实验报告中把缺失值填充策略和异常值处理数量单独列一小节这两步最容易被评分老师注意也是数据挖掘流程里最能体现工作量差异的地方。3. 特征工程与多模型构建从原始字段到可训练特征3.1 类别特征编码独热编码与数值字段标准化这一节处理数据挖掘流程中承上启下的特征工程。类别变量需要按基数分层处理低基数列变速箱、燃油类型、事故记录用独热编码没有副作用高基数列品牌、具体车型独热编码会让特征矩阵爆炸而且大量低频车型的学习意义微弱。品牌可以用频率编码或目标编码具体车型如果数量太多直接丢弃。用ColumnTransformer把数值标准化和类别编码组合成一个预处理对象后面建模全部复用from sklearn.compose import ColumnTransformer from sklearn.preprocessing import OneHotEncoder, StandardScaler from sklearn.pipeline import Pipeline categorical_low [transmission, fuel_type, accident] numeric_cols [mileage, displacement, age, engine_power_kw] preprocessor ColumnTransformer([ (num, StandardScaler(), numeric_cols), (cat, OneHotEncoder(handle_unknownignore), categorical_low) ])handle_unknownignore允许测试集出现训练集里没有见过的类别时不报错。数值列用StandardScaler做 Z-score 标准化线性模型对特征尺度敏感这一步必须有。类别特征数量少时不需要对训练集和测试集分开处理ColumnTransformer在 fit 的时候只学习训练集这一点已经在设计上规避了数据泄漏。3.2 特征构造车龄与品牌均价两个常用变量直接用原始 year 列训练效果一般我的做法是派生两个新特征车龄年份是绝对概念和价格之间不是线性关系。七年的车和五年的车价差远大于一年和三年之间的差异。把 year 转成车龄后模型更容易学习单调递减模式。品牌均价同品牌价格参考区间高度集中品牌均价携带很强的先验信息。df[age] 2024 - df[year] # 按品牌聚合价格均值transform 保持行数不变 df[brand_mean_price] df.groupby(brand)[price].transform(mean) # 删除原始低信息字段 df df.drop(columns[year, model])transform(mean)为每一行返回其所属品牌的平均价格不会改变 DataFrame 行数。但这属于“目标编码”训练集上表现会虚高跨类别泛化时存在数据泄漏风险。严谨做法是只在训练集上计算均值映射再应用到测试集期末作业里若想控制工程复杂度可以在报告中说明这一点并用交叉验证中的目标编码结果作为参考。3.3 基线与多模型对比线性回归、随机森林与 GBDT先把线性回归作为基线跑通再用集成模型做提升。基线模型的性能用于证明特征工程的必要性后续所有调参的对比都有参照物。from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score import numpy as np X df.drop(columns[price]) y np.log1p(df[price]) X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) models { linear: LinearRegression(), randomforest: RandomForestRegressor(n_estimators200, random_state42), gbdt: GradientBoostingRegressor(random_state42), } for name, model in models.items(): pipe Pipeline(steps[(prep, preprocessor), (model, model)]) pipe.fit(X_train, y_train) pred pipe.predict(X_test) # 还原到原始价格单位再计算指标 pred_price np.expm1(pred) y_test_price np.expm1(y_test) rmse mean_squared_error(y_test_price, pred_price, squaredFalse) mae mean_absolute_error(y_test_price, pred_price) r2 r2_score(y_test_price, pred_price) print(f{name}: RMSE {rmse:.2f}万元, MAE {mae:.2f}万元, R2 {r2:.4f})np.log1p和np.expm1是互逆运算。模型在训练时拟合对数价格评估时还原成万元这样报告里的指标才能直接解读为“平均差多少万元”。三组模型跑完大致规律是线性回归受限于特征与价格之间的非线性关系RMSE 通常明显偏高随机森林在大多数二手车数据集上能把误差压缩到线性模型的一半左右GBDT 在连续目标上继续优于随机森林因为它基于残差迭代拟合连续曲面更细腻。这里的关键结论不是“GBDT 最好”而是“特征工程带来的提升量级和换模型带来的提升量级谁更大”实验报告里要写清楚这一点。4. 模型评估与调参把 RMSE 降下来并确认泛化能力4.1 三个必看回归指标MAE、RMSE 与 R²实验报告中至少要出现三个指标每个指标的解释密度直接影响评阅质量MAE平均绝对误差预测值与真实值绝对值差的平均直接反映平均每辆车的预估偏差。RMSE均方根误差对方差求平方后取根放大长尾样本惩罚。当几十万的高端车型预测偏差大时RMSE 会显著上升而 MAE 波动较小。R²模型解释的方差比例0.8 相当于解释了 80% 的价格变动。二手车价格预测的 R² 在 0.85 到 0.93 之间属于正常水平。需要强调的是取对数训练会让每个样本的相对误差均匀化但还原到原始价格空间后高价车型的绝对误差仍然会被放大。写报告时可以把 MAE 和 RMSE 的差值作为一个观察点MAE 是 2.2 万而 RMSE 是 3.1 万说明误差集中在少数高价或特殊车型上这比单纯报指标更能体现分析深度。4.2 用 GridSearchCV 搜索随机森林关键参数随机森林需要关注三个参数n_estimators、max_depth、min_samples_split。树的棵数在 200 到 300 棵已足够继续增加对精度的边际贡献极低且拖慢训练max_depth设置过深容易过拟合min_samples_split控制节点继续划分所需的最少样本数值越大模型越保守。from sklearn.model_selection import GridSearchCV from sklearn.ensemble import RandomForestRegressor rf_params { model__max_depth: [10, 20, 30], model__min_samples_split: [2, 5, 10], } pipe_rf Pipeline(steps[ (prep, preprocessor), (model, RandomForestRegressor(n_estimators300, random_state42)), ]) grid GridSearchCV( pipe_rf, rf_params, cv5, scoringneg_mean_squared_error, n_jobs-1 ) grid.fit(X_train, y_train) print(Best params:, grid.best_params_) print(Best CV score:, np.sqrt(-grid.best_score_))注意管道传递参数的写法model__max_depth双下划线表示管道内 “model” 这一步的max_depth参数。neg_mean_squared_error取负值是因为 sklearn 网格搜索按分数越大越好排序MSE 越小越好所以取负。交叉验证选择 5 折作业场景下速度与稳定性比较均衡。调参完成后用最优参数重新在完整训练集上拟合一次再评估测试集同时记录交叉验证得分与测试集得分之间的差距两者接近说明没有明显过拟合。4.3 四种模型最终对比与结果解读以一个真实场景下的示例数据集为例最终报告里的对比表大致这样模型RMSE万元MAE万元R²线性回归原始特征5.213.440.81线性回归加特征工程4.753.110.84随机森林调参前3.382.020.89随机森林调参后3.241.910.90GBDT调参后3.051.800.92观察这张表能得出一个有价值的结论从线性回归到随机森林是误差的阶跃式下降而从随机森林到 GBDT 只有小幅提升。这说明模型复杂度带来的收益边际递减继续调参不如去看残差结构。画出预测值与真实值的散点图和残差图如果发现 30 万以上的高端区间残差普遍为正说明样本量不足导致系统性低估。这种分析写进实验报告比你写“我们用了 XGBoost 所以 R² 高”要有说服力得多。5. 源码结构与实验报告让数据和模型帮你拿分期末作业的评分通常贯穿源码、运行结果和实验报告三个部分缺一不可。我建议把 Python 工程拆成五个文件模块边界按数据挖掘流程划分car_price_prediction/ ├── data/ │ └── car_data.csv ├── preprocessing.py # 数据加载、清洗、特征构造 ├── model_training.py # 模型构建、调参、评估指标输出 ├── visualization.py # EDA 图和模型诊断图 ├── main.py # 串联全流程的执行入口 └── report/ ├── 实验报告.md └── figs/ # 所有输出图片main.py只保存执行顺序每一行调用对应模块的函数并注释目的报告里的所有图片以fig_price_dist.png、fig_residual.png这类文件名与visualization.py输出保持一致。一个小技巧是在model_training.py顶部统一固定随机种子比如random_state42并注释说明该参数保证实验可复现评阅老师再跑一次能得到相同数字会在这个细节上留下好印象。实验报告建议按“数据说明 → EDA → 特征工程 → 模型选择 → 评估 → 结论”展开每个章节配一到两张图数据说明部分放数据字典表EDA 部分放价格分布图和相关性热力图特征工程部分放新特征引入前后的模型对比评估部分放对比表和残差图。结论控制在两三段说明最优模型、误差集中的环节和可改进空间比如引入里程衰减的非线性变换或补充稀缺车型样本。把清洗前后的数据量、缺失值处理方式、异常值剔除数量都列出来这份“源码数据集实验报告”的完整度就能超过大多数只贴代码的作业。本文还有配套的精品资源点击获取