二手车价格预测实战:数据清洗、特征工程与随机森林建模全流程解析
2026/9/11 5:38:26 网站建设 项目流程

简介:针对Python、数据分析方向期末大作业与项目实战需求,这份资源以二手车价格预测为案例,围绕数据清洗、特征工程、模型训练到结果分析的完整数据挖掘流程展开。资源共26个文件,压缩包34.86MB,核心内容包括可运行的Python源码、用于训练的CSV数据集、详细实验报告、结果可视化图片以及工程配置文件,其中源码完成算法主体,数据集支撑训练验证,报告梳理实现步骤,便于按模块对照学习。项目经导师指导并获得98分评审,难度适中,源码经本地编译调试确保直接可用,特别适合正在准备期末大作业、毕业设计或想提升数据挖掘实战能力的计算机相关专业学生。目前已有168人学习/下载,无论完成类似选题还是系统练习完整项目流程,这套资料都能提供清晰的操作框架与报告写作参考,具有较强的借鉴价值。

1. 二手车价格预测:一份能跑通全流程的数据挖掘大作业

二手车价格预测是数据挖掘课程里出现频率最高的综合型题目之一。它不像图像分类那样需要 GPU,也不像 NLP 那样依赖预训练模型,核心就是表格数据的清洗、特征工程、回归建模和结果解释,正好覆盖 pandas、sklearn、matplotlib 这些 Python 数据科学生态里最常用的工具。这份项目资源包含完整源码、数据集和实验报告,代码经过本地编译调试,实验报告也有导师审核通过的痕迹,适合正在做期末大作业、毕业设计,或者想完整走一遍数据挖掘流程的读者。我自己拆完这份项目后最大的感受是:它的价值不在模型精度有多高,而在把「拿到一份脏数据后该怎么下手」这件事讲完整了——从缺失值处理到特征构造,再到模型对比和报告撰写,每一步都能对上实际工作的习惯。

2. 数据清洗与特征工程:先让数据集满足建模条件

2.1 原始数据的常见问题与预处理思路

二手车数据集里最典型的字段构成一般是:品牌、车系、车型、上牌年份、表显里程、排量、变速箱类型、排放标准、车辆所在地、价格等。这类数据有个共同特点——结构化程度高,但脏数据类型集中。拿到手先别急着训练模型,我一般会按三步走:先看缺失值分布,再处理文本和类别字段,最后检查数值列的量纲和异常点。

这份项目里用到的预处理逻辑很标准,核心代码大致如下:

import pandas as pd import numpy as np df = pd.read_csv('car_data.csv', encoding='gbk') # 1. 查看缺失值比例 missing_ratio = df.isnull().mean().sort_values(ascending=False) print(missing_ratio[missing_ratio > 0]) # 2. 删除缺失比例过高的列 drop_cols = missing_ratio[missing_ratio > 0.5].index.tolist() df = df.drop(columns=drop_cols) # 3. 数值列用中位数填充,类别列用众数填充 num_cols = df.select_dtypes(include=[np.number]).columns cat_cols = df.select_dtypes(include=['object']).columns df[num_cols] = df[num_cols].fillna(df[num_cols].median()) df[cat_cols] = df[cat_cols].fillna(df[cat_cols].mode().iloc[0])

这段代码的操作逻辑是:先用isnull().mean()计算每列缺失占比,超过 50% 的列直接丢弃,避免后续填充引入大量噪声;数值列用中位数填充是因为价格、里程这类字段的分布通常右偏,中位数比均值更稳健;类别列用众数填充则是保留最大概率出现的信息。实际处理时要注意encoding='gbk'这个参数,很多二手车数据集来自国内爬虫或导出工具,用 UTF-8 读取会直接报编码错误。

2.2 里程、年限与品牌特征的构造方法

原始字段不能直接全部丢进模型,原因有两个:一是上牌日期是字符串,需要转换成数值型特征;二是品牌和车系这类高基数类别字段,直接做 One-Hot 会让特征矩阵膨胀到几千维。常见做法是提取「车龄」——用当前年份减去上牌年份,这个特征对价格的影响比绝对年份更直观;品牌则按平均售价做目标编码,这样既保留品牌信息又不会让维度爆炸。

# 构造车龄特征 df['car_age'] = 2023 - df['register_year'] # 品牌目标编码:按品牌分组计算价格均值 brand_mean_price = df.groupby('brand')['price'].transform('mean') df['brand_encoded'] = brand_mean_price # 里程分箱:减少极端值影响 df['mileage_bin'] = pd.cut(df['mileage'], bins=[0, 3, 6, 10, 15, 30], labels=['0-3万', '3-6万', '6-10万', '10-15万', '15万+'])

这里有个容易被忽视的点:register_year如果存在明显的离群值(比如 1900 年),算出来的car_age会变成一百多,直接拉偏模型。处理方式是在构造之前先做一次范围过滤,超出合理区间的置为缺失再填充。另外,里程分箱这个操作在决策树模型里效果一般,因为树模型自己能找到切分点,但对于线性回归来说分箱能明显提升对非线性关系的拟合能力。

2.3 特征相关性检查与筛选

特征工程做完后,下一步是看特征之间的相关性,这一步的目的不是选特征,而是发现冗余。比如brand_encodedprice的相关性可能高达 0.6,而brandcar_age之间可能有交互效应。项目里用热力图来观察这一点:

import matplotlib.pyplot as plt import seaborn as sns # 选择数值型特征计算相关性矩阵 num_features = ['price', 'car_age', 'mileage', 'displacement', 'brand_encoded', 'mileage_bin'] # 将分箱列转为数值编码 df['mileage_bin_code'] = df['mileage_bin'].cat.codes corr_matrix = df[num_features + ['mileage_bin_code']].corr() plt.figure(figsize=(10, 8)) sns.heatmap(corr_matrix, annot=True, cmap='RdBu', fmt='.2f') plt.show()

相关性矩阵在这个项目里的价值主要是辅助判断:如果两个特征相关系数超过 0.8,比如排量和马力,那就只保留其中一个,避免多重共线性影响线性模型的系数解释。但对于随机森林这类树模型,多重共线性影响不大,所以这一步做不做取决于后续选的模型。项目里同时跑了线性回归和树模型,因此这里只做观察,不强制删除。

3. 模型选型与参数调优:线性回归到随机森林的对比逻辑

3.1 基线模型:线性回归为什么是必要的起点

很多初学者一上来就直接上 XGBoost,这其实是个误区。线性回归在这个项目里的作用不是拿高分,而是作为基线——它能告诉你特征和价格之间的大致线性关系是否成立,也能暴露数据里是否存在严重的非线性模式。项目代码里先用train_test_split划分数据集,然后训练线性回归并输出 R² 和 RMSE:

from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression from sklearn.metrics import r2_score, mean_squared_error # 特征与标签分离 X = df[['car_age', 'mileage', 'displacement', 'brand_encoded', 'mileage_bin_code']] y = df['price'] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) # 训练线性回归 lr = LinearRegression() lr.fit(X_train, y_train) y_pred_lr = lr.predict(X_test) print(f'R²: {r2_score(y_test, y_pred_lr):.4f}') print(f'RMSE: {mean_squared_error(y_test, y_pred_lr, squared=False):.2f}')

random_state=42固定随机种子是为了保证结果可复现,这在实验报告里非常重要——否则每次运行划分的样本不同,分数对不上,导师复查时很难验证。特征矩阵里同时包含了mileage原始值和mileage_bin_code分箱编码,这是有意的设计:线性回归可以同时建模线性趋势和分段效应。

3.2 随机森林与梯度提升树的效果对比

线性回归跑完后,项目进入核心对比环节:随机森林和梯度提升树。这里的选型逻辑很清晰——二手车价格和特征之间明显存在非线性关系,比如同样一年的车,里程从 2 万涨到 5 万可能只降价几千,但从 10 万涨到 15 万可能直接腰斩。决策树模型天然能捕捉这种分段变化。

from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor # 随机森林基线 rf = RandomForestRegressor( n_estimators=200, max_depth=10, min_samples_leaf=3, random_state=42 ) rf.fit(X_train, y_train) y_pred_rf = rf.predict(X_test) print(f'Random Forest R²: {r2_score(y_test, y_pred_rf):.4f}') print(f'Random Forest RMSE: {mean_squared_error(y_test, y_pred_rf, squared=False):.2f}')

n_estimators=200是随机森林的树数量,在这个数据量级下 200 棵已经能稳定收敛;max_depth=10限制单棵树的深度,防止过拟合;min_samples_leaf=3要求叶子节点至少 3 个样本,能让预测结果更平滑。如果数据量只有几千条,max_depth设到 15 以上就很容易在训练集上拿到接近 1.0 的 R² 但测试集表现很差。

3.3 网格搜索与交叉验证的坑

项目里用GridSearchCV做了参数调优,重点搜索的是n_estimatorsmax_depthmin_samples_split三个参数。但实际跑的时候有个常见问题:网格搜索的参数量是指数级的,三个参数每个 4 个候选值就是 64 组组合,再加上 5 折交叉验证,总共要训练 320 次。如果每一轮都用 200 棵树,时间会非常感人。

from sklearn.model_selection import GridSearchCV param_grid = { 'n_estimators': [100, 200], 'max_depth': [8, 10, 12], 'min_samples_leaf': [2, 3, 5] } grid_search = GridSearchCV( RandomForestRegressor(random_state=42), param_grid, cv=5, scoring='neg_mean_squared_error', n_jobs=-1, verbose=1 ) grid_search.fit(X_train, y_train) print(f'Best params: {grid_search.best_params_}') print(f'Best CV MSE: {-grid_search.best_score_:.2f}')

n_jobs=-1表示用所有 CPU 核心并行训练,能明显缩短时间。scoring='neg_mean_squared_error'这个设置很关键——sklearn 里的 GridSearchCV 默认以「越大越好」为标准,所以 MSE 要取负数才能保持一致的优化方向。很多人在这里直接用默认的 R² 做评分,结果搜出来的参数在测试集上并不理想,因为 R² 对离群值不敏感,MSE 才是真正反映预测误差的指标。

4. 数据增强与集成思路:单一模型之外的可优化方向

4.1 为什么需要扩充训练样本

二手车价格预测的数据量通常不大,几万条已经算不错,几千条也很常见。数据量不足时,模型很容易在价格的高值区间(比如豪车、性能车)表现很差,因为这部分样本在整体里占比太低。项目里没有额外引入外部数据,但提供了一条可操作的思路:对连续特征做小幅扰动,生成合成样本,这本质上是一种简单版的数据增强。

# 对里程做 ±2% 的扰动,生成第二条训练样本 np.random.seed(42) noise = np.random.normal(1.0, 0.02, size=len(df)) df_augmented = df.copy() df_augmented['mileage'] = df['mileage'] * noise # 合并原始数据与增强数据 df_combined = pd.concat([df, df_augmented], axis=0, ignore_index=True)

注意扰动的幅度不能太大,否则会改变特征和价格之间的真实关系。2% 的噪声会让同一辆车在里程上产生几百公里的波动,这在真实交易中是合理的测量误差,模型因此能学到更平滑的价格曲面。如果扰动超过 10%,模型会把噪声当成真实信号,测试集上的 RMSE 反而会变差。

4.2 基于聚类的价格分层建模

另一个实用技巧是按照品牌档次或价格区间分组建模,而不是用一个全局模型硬扛所有样本。这个项目里虽然没有显式做分层,但数据探索部分按品牌计算了平均价格,这其实已经为分层建模打了基础。实操时我会这样做:先用价格分位数把样本分成低、中、高三档,每档单独训练随机森林,最后预测时根据样本落在哪一档用对应的模型。

这样做的收益在于,低价车(3 万以内的代步车)的价格主要由里程和车龄决定,而高价车(30 万以上的豪华品牌)的价格更多受品牌保值率和车况影响。用一个全局模型拟合的时候,这两类样本的权重会互相拉扯,导致两头都不准确。分层之后每个模型只需关注自己那一档的具体规律,测试集 RMSE 通常能下降 5% 到 10%。

4.3 集成学习的边界与调参策略

项目报告里对比了单独随机森林和简单平均集成(随机森林 + 梯度提升树的预测值取平均)的结果。简单平均看似粗糙,但对于相关性不太高的两个模型来说,平均后的方差一定会下降。

# 简单平均集成 y_pred_ensemble = (y_pred_rf + y_pred_gbdt) / 2 print(f'Ensemble R²: {r2_score(y_test, y_pred_ensemble):.4f}') print(f'Ensemble RMSE: {mean_squared_error(y_test, y_pred_ensemble, squared=False):.2f}')

集成效果的判断标准不是 R² 一定提升,而是 RMSE 是否下降。R² 反映的是模型解释了百分之多少的方差,RMSE 才是实际预测误差。项目里集成的结果比单模型 RMSE 低大约 3%——不算多,但在期末答辩时属于能讲的亮点。更重要的是随机森林和梯度提升树在这个数据集上的残差相关性不高,这意味着它们的错误模式不同,简单平均就能有效对冲。

5. 跑通之后的加分项:残差诊断与结果解释

数据科学项目做到模型训练和评估还不够,期末大作业要拿高分,关键在最后两步:残差分析和特征重要度解释。这两步放在实验报告里,导师一眼就能看出你是真的理解了模型,而不是只调了调参。

import matplotlib.pyplot as plt # 残差计算 residuals = y_test - y_pred_rf # 残差 vs 预测值散点图 plt.figure(figsize=(8, 6)) plt.scatter(y_pred_rf, residuals, alpha=0.5) plt.axhline(y=0, color='red', linestyle='--') plt.xlabel('Predicted Price') plt.ylabel('Residuals') plt.show() # 特征重要度排序 feature_importance = pd.Series( rf.feature_importances_, index=X.columns ).sort_values(ascending=False) print(feature_importance)

残差图里有几个信息量很大的特征:如果残差在预测值比较大的区域出现喇叭形分布,说明模型在高价区间有系统性低估;如果残差整体偏移不为零,说明有未建模的因素。在这个项目里,car_agemileage的特征重要度通常会排在前两位,这符合直觉——车龄和里程是决定二手车价格最核心的两个因素。brand_encoded排第三也合理,因为品牌决定了保值率基线。

这个项目真正让我觉得值回票价的,是它把「从拿到数据到写出报告」的路径走完整了。代码结构清晰,数据量适中,模型复杂度可控,每个环节都能在实验报告里找到对应的描述。对于正在找期末大作业参考的读者,我建议不要直接照搬跑一遍就完事,而是从数据清洗开始,逐个环节改成自己的思路——比如换一种缺失值填充方式、调一下特征组合、跑一次交叉验证对比,然后把这些改动和原始结果做对比写进报告里。这样的作业在答辩时给人的感觉是真正做过研究的,而不是交了一份网上扒下来的代码。

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

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

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

立即咨询