Python二手车价格预测实战:数据清洗、特征工程与模型评估全流程解析

📅 发布时间:2026/10/10 10:08:52
Python二手车价格预测实战:数据清洗、特征工程与模型评估全流程解析
简介这是一份面向Python课程设计、期末大作业及毕业设计的二手车价格预测案例数据挖掘项目资源适合已掌握Python基础语法、希望完整跑通数据探索与建模流程的学习者。资源以真实二手车交易数据为输入覆盖数据清洗、特征工程、模型训练与评估等核心环节源码包含详细中文注释新手也能逐步读懂并可直接复现或改造。压缩包共27个文件包含py源码、csv数据集、docx实验报告、png结果图表、xml工程配置与md说明文档等类型整体约34.86MB目录划分清晰按“代码、数据、图片、文档”分别存放便于按模块学习与整理。目前已有148人学习下载。项目经过严格调试运行稳定操作界面简洁作为课程设计/毕业设计的高分参考模板具备很强的实用价值与扩展空间。1. 一份能跑通的 Python 二手车价格预测项目先搞清楚它值不值得下选期末大作业题目时二手车价格预测几乎是数据挖掘课的「大众款」但真正能在答辩现场把每个环节讲清楚、能把代码从报错里捞出来的人不多。这套压缩包我拆过一遍里面是完整源码、详细注释、实验报告和数据集四件套走的是数据挖掘的标准流程加载数据、清洗、特征工程、建模、评估不是那种只给个model.fit就完事的半成品。它的技术栈是纯 Python pandas scikit-learn适合课程设计、期末大作业和本科毕设新手照着注释能看懂每一步在干嘛熟手可以拿它当基线工程改出自己的版本。文档和代码对应的环境不复杂装好依赖就能复现。2. 动手前资源盘点这份压缩包里到底装了啥2.1 目录结构与文件职责压缩包解压后第一件事不是急着双击运行而是先把目录结构看明白。常见做法是先对着 README 梳理文件用途再决定先看哪份文档。这份资源里的目录形态一般是这样的pythonProject ├── code-main │ ├── .idea │ ├── data # 原始数据集存放目录 │ ├── src # 核心源码目录 │ ├── report # 实验报告 │ ├── README.md # 项目说明 │ └── requirements.txt.idea是 PyCharm 的工程配置目录它存在只代表作者用 PyCharm 打开过项目对你运行代码没有任何影响删掉也不影响别被它吓到。重点看src或者与code-main平级的代码文件数据挖掘大作业的代码通常按「数据读取、数据清洗、建模、评估」拆成几个.py文件或直接一个.ipynb文件打通全部流程。2.2 环境准备与依赖安装这份项目在 PyCharm 或者 VS Code 里都能跑我更推荐用 PyCharm因为大作业项目里带着.idea目录导入时它能保留作者的运行配置和虚拟环境引用。先把依赖装齐python -m venv venv source venv/bin/activate # Windows 上用 venv\Scripts\activate pip install -r requirements.txt pip install pandas numpy scikit-learn matplotlib seabornvenv是 Python 3.3 以后内置的虚拟环境模块比直接装在全局环境里干净得多能避免不同项目之间包版本冲突。scikit-learn建议装在 1.x 的稳定版本上不要用 alpha 版否则 API 可能对不上。装完以后可以先用python -c import pandas; print(pandas.__version__)做一个快速验证。2.3 第一次跑通全流程代码能不能跑通是判断这个资源是否可用的第一步。如果项目入口是一个.py文件用命令行执行如果是 Jupyter Notebook自己按顺序执行每个 cellcd code-main python src/train.py --data data/used_cars.csv --model output/model.pkl--data指定的是二手车数据集的 CSV 路径--model是训练完成后模型的输出路径。很多大作业源码会把路径写死在代码里如果你执行时发现文件路径报错优先看当前终端路径和源码里pd.read_csv()引用的相对路径是否一致。这一步跑通之后你已经确认了项目的运行链路是通的接下来再逐行看数据流的细节。跑通不是终点后面还会踩到各种隐患。3. 数据读入与探索性分析先看懂车价数据的脾气3.1 字段字典与数据量确认读完数据第一件事是什么不是马上describe()而是确认行数和列名防止数据集被人动过手脚。这是一份二手车数据集里常见的字段字典字段名含义类型缺失情况说明brand品牌类别通常完整model车型类别可能有少量脏值year上牌年份数值可能含 0 或极值km_driven已行驶里程数值可能有单位混用engine_cc排量数值部分车缺失fuel_type燃油类型类别少量缺失transmission变速箱类别一般完整price二手售价数值目标变量必须有import pandas as pd df pd.read_csv(data/used_cars.csv) print(df.shape) print(df.dtypes) print(df.isnull().sum())df.shape返回的元组里第一个是行数第二个是列数行数太少说明样本量不足后面模型容易欠拟合行数过多则要注意内存占用。df.isnull().sum()是处理缺失值前的摸底动作它告诉你每一列的缺失情况后续处理策略都由这张表决定。3.2 目标变量与特征分布价格分析要一上来先看分布的形状这不仅决定用什么模型还决定要不要对目标变量做变换。真实场景里二手车的价格分布往往右偏严重低价的便宜车数量巨大高价豪车只有零星几辆这完全是正常现象。import matplotlib.pyplot as plt import seaborn as sns sns.histplot(df[price], bins50, kdeTrue) plt.title(Price Distribution - Before Log Transform) plt.show() import numpy as np log_price np.log1p(df[price]) sns.histplot(log_price, bins50, kdeTrue) plt.title(Price Distribution - After Log Transform) plt.show()log1p是log(1x)的简写它的好处是当价格为 0 时结果仍然有意义而直接取对数会因为 0 值报错。看 log 之后的分布如果接近正态后续建模时可以基于log(price)做回归最后预测值再用expm1还原成实际价格。右偏分布不处理模型对高价车的预测误差会被均匀地「掩盖」在低价区里这是数据挖掘课上一个很加分的处理细节。3.3 相关性分析与数据切分特征之间的相关性和特征与目标的相关性要分开看。前者帮助你去冗余后者帮你做筛选但不能单靠相关系数决定一切特征去留某些相关性弱的特征是合法信号相关性强的特征则可能存在泄漏风险。numeric_cols df.select_dtypes(include[np.number]).columns corr_matrix df[numeric_cols].corr() plt.figure(figsize(10, 8)) sns.heatmap(corr_matrix, annotTrue, cmapcoolwarm, fmt.2f) plt.show() from sklearn.model_selection import train_test_split X df.drop(price, axis1) y df[price] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 )annotTrue把相关系数值直接标在热力图格子里方便的排查两个特征相关性超过 0.9 的情况比如engine_cc和engine_type可能有强关联重复建模等价于一个特征用了两遍。random_state42的作用是固定随机切分的种子保证每次运行得到同一份训练测试集大作业答辩时同一份代码输出稳定结果是基本要求。4. 特征工程让模型能听懂的数据长什么样4.1 缺失值与异常值处理缺失值不能统一的填 0 或填均值得看业务含义来决定。km_driven缺失可以考虑用品牌和年份组合的众数填充其背后的理由是同一年份、同一品牌的二手车行驶里程中位数具备参照性总比全表填充一个有意义的数字更合理。df[km_driven] df.groupby([brand, year])[km_driven].transform( lambda s: s.fillna(s.median()) ) df[engine_cc].fillna(df[engine_cc].median(), inplaceTrue)transform和普通的apply区别在于transform返回的是和原 DataFrame 等长的对象可以直接填入原列不会改变索引结构。groupby里的两个键是填充的参照维度组合维度越细填充越准但维度太细可能导致某些组样本太少执行时会看到警告不用太紧张。数值型缺失用中位数填充比均值更抗异常值这是数据挖掘课里容易在答辩时被老师追问的一个点。异常值处理要用箱线图定位而不是人眼看图。先用IQR方法定义异常值边界q1 df[km_driven].quantile(0.25) q3 df[km_driven].quantile(0.75) iqr q3 - q1 lower_bound q1 - 1.5 * iqr upper_bound q3 1.5 * iqr outliers df[(df[km_driven] lower_bound) | (df[km_driven] upper_bound)] print(f异常值数量: {len(outliers)})IQR的计算逻辑是把落在(Q1 - 1.5IQR, Q3 1.5IQR)这个区间之外的点视为异常值这个 1.5 是经验系数没有绝对正确性。在二手车场景中高里程车可能是真实存在的运营车辆不一定直接删可以先看看这些异常值有多少占比小就删占比大切不可一刀切。4.2 类别编码与数值归一化类别特征不能直接喂给 scikit-learn常见做法是先做 Label Encoding 再转机器学习模型能用的格式但无序类别用 Label Encoding 等于强行给类别排了大小顺序这个逻辑在很多模型里是不成立的。品牌属于无序类别推荐 One-Hot 编码。年份和里程属于有序数值直接保留from sklearn.preprocessing import OneHotEncoder, StandardScaler import pandas as pd categorical_cols [brand, fuel_type, transmission] encoder OneHotEncoder(handle_unknownignore) encoded_arr encoder.fit_transform(df[categorical_cols]) numeric_cols [year, km_driven, engine_cc] scaler StandardScaler() scaled_arr scaler.fit_transform(df[numeric_cols])handle_unknownignore的意思是训练时没见过的类别在预测时会整行转成全 0 向量而不是直接报错。这在实际部署时很有用新车品牌上市后模型不至于崩溃。StandardScaler把连续特征转换成均值为 0、标准差为 1 的标准正态分布对线性模型和神经网络友好对决策树系模型则意义不大因为树模型不关心量纲。这里有一个需要留意的细节scaled_arr和encoded_arr需要拼接时不能直接用np.hstack把原来的 DataFrame 也拼进去因为原 DataFrame 里还残留着未编码的原始字符串列拼进去会导致 dtype 不一致而报错。4.3 衍生特征与业务规则特征工程加分项常常来自对业务的拆解而不是更多的调参。二手车的车龄比年份更直接年份是绝对时间车龄是相对当前时间模型更容易理解后者from datetime import datetime current_year datetime.now().year df[car_age] current_year - df[year] df[price_per_km] df[price] / (df[km_driven] 1) df[year_bucket] pd.cut( df[year], bins[1990, 2000, 2008, 2015, 2024], labels[90s, 00s, 10s_early, 10s_late] )car_age把「越新越贵」这个业务直觉直接变成了数值特征。price_per_km是个单位价格指标注意千万不能被用作建模输入因为它由price除以里程构造和预测目标存在直接函数关系放进特征就是典型泄漏。pd.cut把年份分箱让模型可以从不同年代区间学习不同的价格衰减模式。衍生特征不是越多越好新增每个特征之前先问一句这个特征在预测时能拿到吗比如「该车已售天数」如果预测时必须先知道结果那就是数据泄漏大作业答辩时这种问题一旦被提问会被扣掉不少分。5. 建模避坑与调参这些坑我替你先踩了5.1 坑 1把目标变量标准化后忘了逆变换现象训练用标准化后的log(price)直接调model.predict(X_test)得到的预测值是一个标准化过的无量纲数字和人眼能理解的价格差着十万八千里。原因数据挖掘流程里目标变量进模型前做了StandardScaler或LogTransform预测结果也要跟着做对应的逆变换很多人只记住了特征要归一化忘了目标是同一套逻辑。解决预测后必须做逆变换。如果用的是np.log1p(y)还原就是np.expm1(pred)如果用了StandardScaler变换目标就用scaler.inverse_transform(pred)还原。可以在代码里写一个固定函数def inverse_transform_price(pred_log): return np.expm1(pred_log) pred_price inverse_transform_price(model.predict(X_test_scaled))5.2 坑 2训练集验证集混在一起做编码现象OneHotEncoder在整份 DataFrame 上fit_transform后再切训练集测试集测试集信息泄漏进训练集然后测试集 AUC 虚高。答辩时老师问一句「你测试集怎么切的」如果回答不上来基本扣分。原因编码器和标准化器必须在训练集上fit再分别transform训练集和测试集。整表fit_transform等于用全量数据的分布信息去变换训练集测试集的类别分布提前暴露给了模型。解决先切分再做编码。正确顺序是train_test_split在前特征编码在后from sklearn.preprocessing import OneHotEncoder X_train, X_test, y_train, y_test train_test_split(...) encoder OneHotEncoder(handle_unknownignore) encoder.fit(X_train[[brand, fuel_type]]) X_train_encoded encoder.transform(X_train[[brand, fuel_type]]) X_test_encoded encoder.transform(X_test[[brand, fuel_type]])5.3 坑 3回归问题用准确率当评判指标现象在代码里看到from sklearn.metrics import accuracy_score并且直接对price的预测结果调用输出值是 0.0 或者一个毫无意义的小数点。原因回归问题的预测值是连续实数准确率只适合分类问题中类别完全相等的判断。用准确率评回归任务逻辑上就不成立。解决回归任务看mean_squared_error、mean_absolute_error或R2 score。价格跨度大的数据里MAE 比 MSE 更接近人理解的「平均差多少钱」from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score mae mean_absolute_error(y_test, pred_price) mse mean_squared_error(y_test, pred_price) r2 r2_score(y_test, pred_price) print(fMAE: {mae:.2f} | MSE: {mse:.2f} | R2: {r2:.4f})MAE 单位是「元」直接对应人脑理解的平均差距。R2 接近 1 说明模型解释了大部分方差但 R2 对异常值敏感实测时单看 R2 高有可能是运气好要结合 MAE 一起看。碰到 R2 很高但 MAE 也高的怪象基本是少量极贵车的预测残差拉高了误差的平方项。5.4 坑 4乱调参之前不看交叉验证的稳定度现象手动改了一堆参数比如n_estimators500, max_depth20, learning_rate0.01换来换去测试集分数忽高忽低不知道到底哪些参数真正有效。原因没有做交叉验证单次切分的测试集结果受随机性影响很大同一组参数在不同随机种子下表现可能差 3 到 5 个点。解决先跑一个固定KFold做基线确认模型稳定性再进网格搜索from sklearn.model_selection import cross_val_score from sklearn.ensemble import RandomForestRegressor rf RandomForestRegressor(n_estimators200, random_state42) scores cross_val_score(rf, X_train, y_train, cv5, scoringr2) print(fCV R2 - mean: {scores.mean():.4f}, std: {scores.std():.4f})cv5把训练集切成 5 份每轮用其中 4 份训练、1 份验证最终分数取 5 轮平均。看std比看mean更重要std超过 0.05 说明模型对数据分布的局部变化太敏感优先处理数据问题而不是继续调参。6. 验证与部署如何把模型从屏幕搬到大作业报告里要让答辩现场的老师相信你的模型真的有用光有训练时那几张图不够最好准备一个「预测结果对照表」随机选几辆车把真实价格和预测价格并列再做一张残差分布图。这份资源里没有专门的可视化界面但用 Flask 包一个本地预测接口并不费事手打代码时牢记模型内部保存的文件格式。模型训练完毕后用joblib把管线整体保存这是最省心的做法因为归一化器和编码器会连同训练好的模型一起打包import joblib joblib.dump(rf, output/model_pipeline.pkl)恢复使用时直接加载并predict不需要重新编码特征顺序省掉不少时间。验证时除了 R2 和 MAE我建议加一个残差图检查residuals y_test - pred_price plt.scatter(pred_price, residuals, alpha0.5) plt.axhline(y0, colorred, linestyle--) plt.xlabel(Predicted Price) plt.ylabel(Residuals) plt.show()残差图里如果散点围绕红色零线上下基本均匀分布说明模型预测误差没有明显趋势如果出现喇叭形状——预测值越大残差越散通常对应高价区样本量不足这时可以考虑对目标值加 log 变换或者按品牌单独建模。一般来说评价一个模型值不值得写进报告残差图比 R2 更有说服力。部署接口不用做得很复杂本地能跑通即可from flask import Flask, request, jsonify app Flask(__name__) app.route(/predict, methods[POST]) def predict(): payload request.get_json() features [[ payload[brand], payload[fuel_type], payload[transmission], payload[year], payload[km_driven], payload[engine_cc] ]] pred model.predict(features)[0] return jsonify({predicted_price: round(float(pred), 2)})接口数据格式是 JSONget_json()拿到的是一个字典前台传过来的字段名必须和训练时的列名完全一致否则模型构造特征时顺序错位预测结果直接废掉。部署这种模块化接口课程设计足够用跟老师讲清楚「模型持久化、接口分离、可嵌入其他系统」比单独交一个.py文件有明显优势。如果这份资源里的预测结果和实际价格差距始终偏大优先检查数据里是否有品牌混入拼写错误比如Toyota和Toyta被编码成两个类别测试集上一个从没见过的拼法会直接变全 0 向量预测值被拉向均值。从那以后我每次拿到新数据会先跑一轮df[brand].value_counts()把词频过低且拼写可疑的类别做归一化处理再拆分数据集已经成了固定习惯。希望帮到你。本文还有配套的精品资源点击获取