简介:这是一份面向计算机相关专业学生的数据挖掘课程大作业资源,以二手车价格预测为实战主题,适合作为课程设计、期末大作业或自学练习的完整案例。项目经导师指导并评审通过,得分98分,内容覆盖数据预处理、特征工程、模型构建与评估全流程,涉及线性回归、决策树、随机森林等算法的实现与对比。资源包共27个文件,约34.86MB,包含2个Python源码文件、1个CSV数据集、1份DOCX实验报告,以及9张PNG结果图、8个XML配置文件和项目说明文档等,源码与报告均附有详尽注释,便于理解每一步操作意图。数据集模拟真实市场数据,涵盖品牌、车型、使用年限、行驶里程、车况等多维特征,能反映价格与各因素间的复杂关系。目前已有69人学习,读者可借此掌握数据清洗、特征选择、模型训练调优与验证的完整流程,积累从问题定义到结果输出的实战经验。
1. 二手车价格预测数据挖掘大作业:从一份带注释的 Python 源码说起
二手车价格预测是数据挖掘课程里最经典的回归类大作业之一,它把数据清洗、特征工程、模型调参与结果解释串成一条完整链路,几乎覆盖了数据挖掘实验的全部考核点。很多人拿到「二手车价格预测数据挖掘课程大作业Python源码及数据集与实验报告详尽注释.zip」这类资源时,第一反应是直接跑通看准确率,结果发现换一份数据集就崩,或者报告里的结论根本复现不出来。问题不在代码本身,而在于没有理解这套流程里每一步为什么这么设计。这篇笔记面向正在做数据挖掘大作业的学生和刚转行做数据分析的从业者,把二手车价格预测从数据读取、特征构造、模型选型到实验报告撰写拆开讲清楚,让你拿到源码后能改、能调、能解释,而不是只会按运行键。数据集通常包含品牌、车型、上牌年份、行驶里程、排量、变速箱、燃油类型、所在城市和成交价等字段,目标就是预测成交价这个连续变量。
2. 二手车价格预测的数据集结构与特征工程怎么做
2.1 先搞清楚字段类型再动手清洗
拿到数据集后不要急着df.dropna(),先按字段类型分类处理。二手车数据一般分三类:数值型(价格、里程、排量、车龄)、类别型(品牌、车型、变速箱、燃油类型、城市)、文本型(标题描述、配置说明)。数值型看分布和异常值,类别型看基数,文本型看是否需要提取关键词。
import pandas as pd import numpy as np df = pd.read_csv('used_car.csv', encoding='utf-8') # 先看整体信息,不要跳过这一步 print(df.info()) print(df.describe()) # 按类型分组查看 num_cols = ['price', 'mileage', 'displacement', 'age'] cat_cols = ['brand', 'model', 'gearbox', 'fuel_type', 'city'] # 数值型看偏度和异常值 for col in num_cols: print(f"{col}: skew={df[col].skew():.2f}, " f"min={df[col].min()}, max={df[col].max()}") # 类别型看基数,基数超过50的慎用独热编码 for col in cat_cols: print(f"{col}: {df[col].nunique()} unique values")这段代码的逻辑是先建立字段分类意识,再分别处理。skew()看偏度,价格和里程通常右偏严重,后续可能需要对数变换。nunique()看类别基数,品牌可能只有几十个,但车型可能有几百个,车型直接独热编码会导致维度爆炸,常见做法是保留高频类别、低频归为「其他」,或者用目标编码。
参数上注意:encoding要根据实际文件调整,中文数据集常见gbk或utf-8-sig;如果价格字段带「万」字或货币符号,需要先做字符串清洗再转数值。
2.2 车龄和里程的组合特征比单独用更有信息量
二手车定价的核心逻辑是「年份新、里程少、价格高」,但车龄和里程单独作为特征时,模型很难自动捕捉它们的交互关系。手动构造「年均行驶里程」这个组合特征,往往比把两个原始特征丢给模型效果更好。
# 从上牌日期计算车龄 df['reg_date'] = pd.to_datetime(df['reg_date'], errors='coerce') df['age'] = 2024 - df['reg_date'].dt.year # 年均里程:总里程除以车龄,车龄为0时设为1避免除零 df['age_safe'] = df['age'].apply(lambda x: max(x, 1)) df['mileage_per_year'] = df['mileage'] / df['age_safe'] # 价格取对数,缓解右偏 df['log_price'] = np.log1p(df['price']) # 品牌保值率:用品牌中位数价格除以整体中位数 brand_median = df.groupby('brand')['price'].median() overall_median = df['price'].median() df['brand_retention'] = df['brand'].map(brand_median) / overall_medianmileage_per_year这个特征的意义在于区分「老车但开得少」和「新车但开得多」两种情况,前者通常车况更好、价格更高。brand_retention是目标编码的一种简化形式,用品牌中位价与整体中位价的比值衡量保值能力,注意这个特征在训练集上计算后要应用到测试集,不能把测试集信息泄露进训练。
注意:目标编码类特征必须在交叉验证的每一折内部重新计算,否则会造成标签泄露,线下评分虚高但线上翻车。
2.3 缺失值处理要分字段讨论,不能一刀切
二手车数据里缺失值很常见,排量、变速箱、燃油类型都可能缺。不同字段的缺失含义不同:排量缺失可能是新能源车,变速箱缺失可能是信息未录入,价格缺失的样本必须直接删除(因为它是标签)。
# 价格缺失的样本直接删,这是标签不能补 df = df.dropna(subset=['price']) # 排量缺失:先看是否与燃油类型相关 print(df[df['displacement'].isna()]['fuel_type'].value_counts()) # 如果缺失集中在新能源,用0填充并加一个缺失标记 df['displacement_missing'] = df['displacement'].isna().astype(int) df['displacement'] = df['displacement'].fillna(0) # 类别型缺失用众数填充 for col in ['gearbox', 'fuel_type']: df[col] = df[col].fillna(df[col].mode()[0])加displacement_missing标记列是一个实用技巧:让模型自己学习「缺失」这件事是否有信息量。如果实验发现这个标记列重要性很低,再删掉也不迟。类别型用众数填充的前提是缺失比例不高(一般低于10%),如果某个字段缺失超过30%,考虑直接丢弃该字段或把它当作一个独立类别「未知」。
3. 模型选型与训练:从线性回归到梯度提升的对比实验
3.1 基线模型先用线性回归把流程跑通
很多人一上来就上 XGBoost,结果调参调到怀疑人生,却不知道数据本身有没有问题。正确做法是先用一个简单的线性回归建立基线,确认数据管道没问题,再逐步换复杂模型。
from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression from sklearn.preprocessing import StandardScaler, OneHotEncoder from sklearn.compose import ColumnTransformer from sklearn.pipeline import Pipeline from sklearn.metrics import mean_absolute_error, r2_score # 特征列划分 num_features = ['age', 'mileage', 'mileage_per_year', 'displacement', 'brand_retention'] cat_features = ['brand', 'gearbox', 'fuel_type', 'city'] # 预处理管道:数值标准化,类别独热 preprocessor = ColumnTransformer([ ('num', StandardScaler(), num_features), ('cat', OneHotEncoder(handle_unknown='ignore', max_categories=20), cat_features) ]) # 线性回归基线 lr_pipe = Pipeline([ ('prep', preprocessor), ('model', LinearRegression()) ]) X = df[num_features + cat_features] y = df['log_price'] X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) lr_pipe.fit(X_train, y_train) y_pred = lr_pipe.predict(X_test) print(f"MAE: {mean_absolute_error(y_test, y_pred):.4f}") print(f"R2: {r2_score(y_test, y_pred):.4f}")ColumnTransformer把数值和类别分开处理,避免对类别做标准化这种无意义操作。OneHotEncoder的handle_unknown='ignore'保证测试集出现训练集没见过的类别时不报错,max_categories=20把低频类别合并,控制维度。注意这里预测的是log_price,评估指标要在对数尺度上看,最后再np.expm1还原到原始价格。
3.2 梯度提升树为什么在表格数据上通常更强
二手车价格和特征之间往往是非线性关系:车龄对价格的影响不是线性的,前三年贬值快、后面趋缓;里程和价格的关系也类似。线性回归捕捉不到这种非线性,而梯度提升树可以自动做特征交叉和分段拟合。
from xgboost import XGBRegressor from sklearn.model_selection import cross_val_score xgb_pipe = Pipeline([ ('prep', preprocessor), ('model', XGBRegressor( n_estimators=500, max_depth=6, learning_rate=0.05, subsample=0.8, colsample_bytree=0.8, reg_alpha=0.1, reg_lambda=1.0, random_state=42 )) ]) # 五折交叉验证,比单次划分更可靠 cv_scores = cross_val_score(xgb_pipe, X_train, y_train, cv=5, scoring='neg_mean_absolute_error') print(f"CV MAE: {-cv_scores.mean():.4f} (+/- {cv_scores.std():.4f})")参数含义逐个说清楚:n_estimators=500是树的数量,配合learning_rate=0.05使用,学习率低就需要更多树;max_depth=6控制单棵树复杂度,太深容易过拟合;subsample=0.8和colsample_bytree=0.8是行采样和列采样,增加模型多样性、抑制过拟合;reg_alpha和reg_lambda是 L1 和 L2 正则项。这组参数不是最优解,但作为起点比较稳,建议先用交叉验证跑一遍,再根据 CV 曲线调整。
3.3 特征重要性分析帮你判断模型是否学到了合理的东西
模型跑出高分不代表学到了正确的东西。如果特征重要性显示「城市」比「车龄」还重要,那大概率是数据泄露或者某个类别特征编码有问题。
import matplotlib.pyplot as plt # 训练完整模型后提取特征重要性 xgb_pipe.fit(X_train, y_train) model = xgb_pipe.named_steps['model'] # 获取独热编码后的特征名 ohe = xgb_pipe.named_steps['prep'].named_transformers_['cat'] cat_names = ohe.get_feature_names_out(cat_features) all_names = num_features + list(cat_names) importance = pd.Series(model.feature_importances_, index=all_names) importance.sort_values(ascending=False).head(15).plot(kind='barh') plt.tight_layout() plt.savefig('feature_importance.png', dpi=150)看重要性时重点关注三件事:第一,车龄和里程是否排在前列,如果不是,检查这两个字段是否被正确解析;第二,brand_retention这个构造特征是否有效,如果重要性接近零,说明品牌信息已经被独热编码覆盖了;第三,有没有某个独热编码出来的单一类别重要性异常高,那可能是数据泄露的信号。
4. 实验报告怎么写才不像流水账
4.1 报告结构要围绕「问题-方法-证据」展开
课程大作业的实验报告不是代码说明书,老师想看的是你有没有分析思路。建议按这个结构组织:问题定义与数据说明、探索性分析发现、特征工程方案与理由、模型对比实验、误差分析与改进方向。每一部分都要有图表支撑,不能只有文字描述。
探索性分析部分至少放三张图:价格分布直方图(说明为什么要取对数)、车龄与价格的散点图(说明非线性关系)、品牌与均价箱线图(说明品牌特征的必要性)。这些图不只是装饰,它们是你后续特征工程决策的依据。
4.2 模型对比要用统一评估口径
对比不同模型时,必须用同一份训练集和测试集划分,评估指标统一用 MAE 和 R2。如果预测的是对数价格,报告里要同时给出对数尺度和原始价格尺度的指标,否则读者无法直观判断误差大小。
| 模型 | MAE(对数尺度) | R2(对数尺度) | MAE(原始价格/元) |
|---|---|---|---|
| 线性回归 | 0.28 | 0.72 | 18500 |
| 随机森林 | 0.21 | 0.83 | 13200 |
| XGBoost | 0.18 | 0.87 | 10800 |
表格里的数字要根据你自己的实验结果填写,不要照抄。重点是展示模型迭代的过程和每一步的改进幅度,而不是只放一个最终结果。
4.3 误差分析比追求高分更能体现水平
挑出预测误差最大的十个样本,逐个看它们的特征,往往能发现数据问题或模型盲区。比如某辆车的实际价格远低于预测值,一看是事故车但数据集里没有事故记录字段,这就是数据本身的局限,写进报告的「不足与改进」部分比硬调参提分更有价值。
# 找出预测误差最大的样本 results = X_test.copy() results['actual'] = np.expm1(y_test) results['predicted'] = np.expm1(y_pred) results['abs_error'] = abs(results['actual'] - results['predicted']) top_errors = results.nlargest(10, 'abs_error') print(top_errors[['brand', 'age', 'mileage', 'actual', 'predicted', 'abs_error']])这段代码帮你定位问题样本,分析时关注:误差大的样本是否集中在某个品牌或某个价格区间;是否有特征值明显异常;预测值是否系统性地偏高或偏低。这些发现直接对应报告里的改进方向。
5. 避坑与排查:二手车价格预测大作业里最容易翻车的五个地方
5.1 价格字段带单位导致模型训练报错
现象:ValueError: could not convert string to float,或者模型训练出来 R2 是负数。
原因:价格字段里混有「3.5万」「¥12.8万」这类字符串,pd.read_csv默认按 object 读取,直接丢给模型就报错。
解决:读取后先做字符串清洗,用正则提取数字再乘单位。df['price'] = df['price'].str.replace('万', '').astype(float) * 10000,处理完用df['price'].dtype确认类型。
5.2 上牌日期格式不统一导致车龄计算出错
现象:车龄字段出现负数或者超过30的异常值。
原因:日期格式有「2018-05」「2018/05/01」「2018年5月」多种写法,pd.to_datetime解析失败后返回 NaT,减法运算产生错误结果。
解决:用pd.to_datetime(df['reg_date'], format='mixed', errors='coerce')统一解析,解析失败的样本单独检查。计算车龄后用df['age'].describe()确认范围合理,异常值手动核查或剔除。
5.3 独热编码后训练集和测试集列数不一致
现象:ValueError: feature_names mismatch或者预测时维度对不上。
原因:训练集和测试集分别做了独热编码,测试集里某个类别在训练集没出现,导致列数不同。
解决:用Pipeline把编码器和模型绑在一起,先fit训练集再predict测试集,OneHotEncoder设置handle_unknown='ignore'。不要手动分别编码再拼接。
5.4 交叉验证时数据泄露导致线下分数虚高
现象:交叉验证 MAE 很低,但换一份测试集效果差很多。
原因:在划分训练测试集之前就做了全局的缺失值填充或目标编码,测试集信息泄露进了训练过程。
解决:所有涉及统计量的操作(均值填充、目标编码、标准化)都必须放在Pipeline里,在交叉验证的每一折内部独立计算。用cross_val_score配合Pipeline可以自动处理这个问题。
5.5 报告里的结论和代码实际输出对不上
现象:报告写「XGBoost 比线性回归 MAE 降低 30%」,但代码跑出来只降了 10%。
原因:调参过程中改过代码但忘了同步更新报告,或者报告里的数字是某次偶然运行的结果,没有用固定随机种子复现。
解决:所有实验设置random_state,报告里的每个数字都要能通过运行对应脚本复现。建议在报告附录里附上关键代码片段和运行环境说明(Python 版本、主要库版本)。
6. 把这份大作业变成可复用的回归建模模板
做完二手车价格预测之后,最有价值的动作不是交完作业就丢,而是把这套流程抽象成一个可复用的回归建模模板。我自己后来做房价预测、设备残值评估、保险定价这些任务时,基本都是从这个模板改出来的,省了大量重复劳动。
具体做法是把代码拆成四个独立模块:data_loader.py负责读取和初步清洗,feature_engineer.py负责特征构造,model_trainer.py负责模型训练和交叉验证,evaluator.py负责指标计算和误差分析。每个模块之间用 DataFrame 传递数据,接口保持稳定。这样换数据集时只需要改data_loader和feature_engineer,模型和评估部分基本不用动。
# model_trainer.py 的核心接口设计 class RegressionTrainer: def __init__(self, preprocessor, model, cv=5): self.pipe = Pipeline([('prep', preprocessor), ('model', model)]) self.cv = cv def train(self, X, y): scores = cross_val_score(self.pipe, X, y, cv=self.cv, scoring='neg_mean_absolute_error') self.pipe.fit(X, y) return -scores.mean() def predict(self, X): return self.pipe.predict(X) def feature_importance(self, feature_names): model = self.pipe.named_steps['model'] return pd.Series(model.feature_importances_, index=feature_names)这个类把交叉验证、训练和预测封装在一起,换模型只需要传入不同的model对象。feature_importance方法要求模型有feature_importances_属性,线性回归没有这个属性,可以改成看系数绝对值,或者用permutation_importance做模型无关的重要性分析。
验证模板是否好用的方法很简单:找一份新的回归数据集,只改数据加载和特征工程部分,看能不能在半天内跑出一版合理的结果。如果超过半天,说明模块耦合太紧,需要继续拆。我自己的习惯是每做完一个项目就往模板里沉淀一点,现在这个模板已经覆盖了缺失值处理、类别编码、数值变换、模型对比和误差分析五个环节,新项目上手基本就是填空。
还有一个容易被忽略的点:把每次实验的配置和结果记录下来。不需要多复杂的工具,一个 CSV 文件就够,字段包括实验编号、特征版本、模型参数、CV MAE、测试集 MAE、备注。这样当你想回头对比「上个月那版特征到底有没有用」时,不用翻聊天记录和零散脚本。这个习惯帮我省了很多后悔药,希望你也能用上。希望帮到你。
本文还有配套的精品资源,点击获取