资讯详情 基于机器学习二手车交易预测评估系统:从数据清洗到模型部署的完整实战
📅 2026/10/10 19:59:57
简介这份资源面向人工智能、机器学习方向的课程期末作业与毕业设计场景提供一套完整的二手车交易价格预测评估系统实现方案帮助学习者理解从数据预处理到多模型对比的完整流程。压缩包共19个文件约55.92MB以9个csv数据与结果文件、5个py源码脚本为主另含1个ipynb交互笔记、1个h5模型权重、1个npy参数文件及md说明文档覆盖数据、代码与产出结果三类内容。资源围绕支持向量机回归、多元线性回归、LightGBM与CNN四种模型展开包含训练集与测试集预测结果、标准化数据、特征交叉后经Spearman筛选的冗余特征记录以及模型参数文件代码注释较充分新手也能对照理解。目前已有4124人学习下载适合需要完成机器学习大作业、复现多模型评估流程或积累特征工程与调参经验的学生与开发者参考。1. 二手车定价预测系统从一份带源码和数据集的压缩包说起二手车商最怕什么收车时拍脑袋给价卖车时被同行截胡。一辆 2018 款的车表显 6 万公里车况精品到底该标 12 万还是 14 万传统做法是老师傅凭经验估但经验这东西没法复制换个人就翻车。基于机器学习二手车交易预测评估系统要解决的就是把这个「拍脑袋」变成「看数据」——用历史成交记录训练模型输入车龄、里程、品牌、变速箱类型等字段输出一个可解释的估价区间。这个方向适合谁一是想入门机器学习但苦于没有完整项目练手的开发者二手车数据集字段干净、业务逻辑直白比鸢尾花数据集有意思得多二是二手车平台或车商的技术团队想搭一套内部估价工具三是做课程设计的学生源码加数据集的组合能直接跑通「数据清洗→特征工程→模型训练→评估→可视化」全流程。标题里提到的源码和数据集核心价值在于省去你找数据、搭框架的时间让你把精力放在特征处理和模型调参上。下面我按实际落地顺序把这条路走一遍。2. 数据到手先别急着建模二手车数据集的字段清洗与特征取舍2.1 先搞清楚数据集里有什么、缺什么拿到一份二手车交易数据集第一件事不是pd.read_csv然后model.fit而是把字段含义和缺失情况摸清楚。常见字段包括车辆 ID、品牌、车型、注册日期、里程、变速箱、燃油类型、排量、功率、车身类型、颜色、是否事故、成交价。不同来源的数据集字段名和格式差异很大有的用德语列名有的把价格写成带货币符号的字符串。我一般会先跑一段探查代码把每列的缺失率、唯一值数量、数据类型打出来import pandas as pd import numpy as np df pd.read_csv(used_car_transactions.csv) # 基础信息行列数、内存占用 print(df.shape) print(df.dtypes) # 缺失率排序优先处理缺失超过 30% 的列 missing df.isnull().mean().sort_values(ascendingFalse) print(missing[missing 0]) # 类别型字段看唯一值数量判断是品牌还是车型 for col in df.select_dtypes(includeobject).columns: print(col, df[col].nunique(), df[col].unique()[:10]) # 数值型字段看分布价格和里程最容易有异常值 print(df[[price, mileage, power]].describe())这段代码的逻辑是先看整体规模再定位缺失严重的列然后区分哪些是类别字段、哪些是数值字段。参数上缺失率 30% 是个经验阈值超过这个比例的列要么直接丢弃要么用业务逻辑补全比如「是否事故」缺失可以默认填「否」但「排量」缺失就不能随便填。describe()重点看 max 和 min价格出现 999999 这种值基本可以判定是占位符或录入错误。2.2 缺失值、异常值和重复记录的处理顺序处理顺序有讲究先去重再处理异常值最后补缺失。反过来做会导致填充的值被后续去重逻辑误删。去重不能只看车辆 ID因为同一辆车可能被多次挂牌。我一般用「品牌车型注册年份里程」组合判断里程完全一致的记录大概率是重复录入# 组合去重保留第一条 df df.drop_duplicates(subset[brand, model, registration_year, mileage], keepfirst) # 价格异常值用分位数截断而不是直接删 low, high df[price].quantile([0.01, 0.99]) df df[(df[price] low) (df[price] high)] # 里程为 0 但车龄大于 3 年的标记为异常并置空 df.loc[(df[mileage] 0) (df[registration_year] 2021), mileage] np.nan # 数值型缺失用中位数填充类别型用众数 for col in [mileage, power]: df[col] df[col].fillna(df[col].median()) for col in [gearbox, fuel_type]: df[col] df[col].fillna(df[col].mode()[0])分位数截断比直接删除温和保留 98% 的数据同时去掉极端值。里程为 0 但车龄大的记录很可能是卖家没填或填错置空后用中位数补比直接删更合理。类别字段用众数填充的前提是该类别占比不超过 70%否则填充会引入偏差。2.3 哪些字段该进模型、哪些该丢不是所有字段都对价格有正向贡献。车辆 ID 是唯一标识直接丢注册日期要转成车龄才有意义品牌和车型如果唯一值太多比如超过 50 个要么做目标编码要么只保留高频品牌其余归为「其他」。我通常保留这几类特征数值型车龄、里程、功率、排量、低基数类别型变速箱、燃油类型、车身类型、布尔型是否事故、是否一手车。品牌和车型用目标编码处理但要注意在训练集上计算编码值再映射到验证集避免数据泄露。# 注册日期转车龄 df[age] 2024 - df[registration_year] # 高频品牌保留低频归为 Other brand_counts df[brand].value_counts() top_brands brand_counts[brand_counts 100].index df[brand_grouped] df[brand].where(df[brand].isin(top_brands), Other) # 目标编码用训练集均值映射这里先划分再编码 from sklearn.model_selection import train_test_split train_df, test_df train_test_split(df, test_size0.2, random_state42) brand_mean train_df.groupby(brand_grouped)[price].mean() train_df[brand_encoded] train_df[brand_grouped].map(brand_mean) test_df[brand_encoded] test_df[brand_grouped].map(brand_mean)目标编码的关键是「在训练集上算在验证集上用」否则验证集的编码值包含了自身价格信息评估结果会虚高。品牌阈值设 100 是经验值数据量小可以降到 50数据量大可以提到 200。3. 从特征矩阵到估价输出模型选型、训练与评估的完整链路3.1 为什么树模型是二手车定价的默认选择二手车价格和特征之间很少是线性关系。车龄从 1 年变 2 年贬值可能只有 8%但从 8 年变 9 年贬值可能跳到 15%。里程也是前 5 万公里和后 5 万公里的边际影响完全不同。线性回归拟合这种非线性关系会欠拟合而树模型天然能捕捉分段效应。在树模型里随机森林和梯度提升树GBDT是两条主流路线。随机森林抗过拟合能力强调参少适合快速出基线GBDT 精度更高但学习率和树数量需要仔细调。我一般先用随机森林跑一版基线再用 XGBoost 或 LightGBM 做提升对比验证集上的 MAE 和 R²。模型优点缺点适用场景线性回归可解释性强、训练快无法拟合非线性特征与价格近似线性时随机森林抗过拟合、调参少模型体积大、推理慢快速基线、特征重要性分析XGBoost精度高、支持缺失值调参复杂、易过拟合追求精度的生产环境LightGBM训练快、内存占用低小数据集易过拟合数据量大、迭代频繁3.2 训练脚本从划分数据集到保存模型下面是一段可以直接跑的完整训练脚本用随机森林做基线输出 MAE、RMSE 和 R²import pandas as pd import numpy as np from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score import joblib # 读取清洗后的数据 df pd.read_csv(cleaned_car_data.csv) # 特征列和标签列 feature_cols [age, mileage, power, displacement, brand_encoded, gearbox, fuel_type, body_type, is_accident] X pd.get_dummies(df[feature_cols], columns[gearbox, fuel_type, body_type], drop_firstTrue) y df[price] # 划分训练集和验证集 X_train, X_val, y_train, y_val train_test_split(X, y, test_size0.2, random_state42) # 随机森林树数量 200最大深度 15防止过拟合 model RandomForestRegressor( n_estimators200, max_depth15, min_samples_leaf5, random_state42, n_jobs-1 ) model.fit(X_train, y_train) # 预测与评估 y_pred model.predict(X_val) mae mean_absolute_error(y_val, y_pred) rmse np.sqrt(mean_squared_error(y_val, y_pred)) r2 r2_score(y_val, y_pred) print(fMAE: {mae:.2f}) print(fRMSE: {rmse:.2f}) print(fR2: {r2:.4f}) # 保存模型和特征列推理时保持一致 joblib.dump({model: model, columns: X.columns.tolist()}, car_price_model.pkl)参数说明n_estimators200是树的数量太少欠拟合太多训练慢且收益递减200 到 500 之间比较常见。max_depth15控制单棵树深度二手车数据特征不多15 层足够捕捉交互再深容易记住噪声。min_samples_leaf5保证叶子节点至少有 5 个样本防止对个别车辆过拟合。n_jobs-1用满所有 CPU 核心加速训练。评估指标里MAE 最直观——平均预测偏差多少元。RMSE 对大误差更敏感如果 RMSE 远大于 MAE说明存在个别预测离谱的样本要回去检查特征。R² 衡量模型解释了价格波动的比例二手车场景下 0.85 以上算不错0.9 以上要警惕数据泄露。3.3 特征重要性分析与模型可解释性训练完不能只看指标还要看模型到底学到了什么。随机森林自带feature_importances_但更可靠的是用 SHAP 值分析每个特征对单条预测的贡献import shap # 用树解释器计算 SHAP 值 explainer shap.TreeExplainer(model) shap_values explainer.shap_values(X_val) # 全局重要性平均绝对 SHAP 值 shap.summary_plot(shap_values, X_val, plot_typebar) # 单条预测解释为什么这辆车估了 13 万 shap.force_plot(explainer.expected_value, shap_values[0], X_val.iloc[0])SHAP 的价值在于把「黑匣子」打开。如果发现age的 SHAP 值方向反了——车龄越大价格越高那说明数据有问题可能是注册年份字段录入错误。特征重要性还能指导特征工程如果brand_encoded贡献很低说明品牌对价格影响不大可以考虑去掉以减少维度。4. 避坑指南二手车预测项目里最容易翻车的五个地方4.1 数据泄露验证集指标虚高上线就崩现象验证集 R² 达到 0.95MAE 只有几千块但拿新数据一测误差翻三倍。原因最常见的是目标编码泄露——在全集上计算品牌均价再划分数据集验证集的编码值包含了自身价格。其次是时间泄露——用未来的成交记录预测过去的价格。解决所有涉及标签的编码、填充、归一化必须在训练集上fit再transform验证集。时间相关的数据按时间切分不要随机划分。检查方法很简单把验证集指标和训练集指标对比差距超过 10% 就要怀疑泄露。4.2 类别字段基数爆炸品牌有 200 个唯一值现象pd.get_dummies之后特征维度从 20 涨到 300训练变慢模型在低频品牌上预测离谱。原因品牌、车型这类字段唯一值太多独热编码导致维度灾难且低频类别样本太少模型学不到规律。解决高频保留、低频归并。阈值按数据量定一般出现次数少于 50 次的品牌归为「Other」。或者用目标编码替代独热但注意 4.1 的泄露问题。车型字段如果基数更大可以考虑只保留车型级别如 SUV、轿车、跑车而不是具体型号。4.3 价格分布偏态模型对豪车预测普遍偏低现象普通车预测误差几千块但 50 万以上的豪车误差十几万模型系统性低估。原因价格通常右偏大部分样本集中在 5 到 20 万区间豪车样本少损失函数被普通车主导。解决对价格取对数再训练预测后再指数还原。这样损失函数对相对误差更敏感豪车和普通车的权重更均衡。或者用分位数回归直接预测价格区间而不是点估计。# 对数变换 y_log np.log1p(y) model.fit(X_train, y_log) y_pred np.expm1(model.predict(X_val))4.4 推理时特征列不一致模型加载后报错现象训练时一切正常保存模型后重新加载做推理报feature_names mismatch或预测结果全为 NaN。原因训练时用了pd.get_dummies推理时新数据的类别字段缺少某些类别导致生成的列数和训练时不一致。解决保存模型时一并保存特征列名推理时用reindex对齐saved joblib.load(car_price_model.pkl) model saved[model] train_columns saved[columns] # 推理数据对齐列 X_new pd.get_dummies(X_new) X_new X_new.reindex(columnstrain_columns, fill_value0) pred model.predict(X_new)4.5 里程单位不统一公里和英里混用现象模型对某些样本预测偏差极大检查发现里程数值分布有两个峰。原因数据集里部分记录用英里部分用公里没有统一单位。1 英里约等于 1.6 公里混用会导致特征分布失真。解决清洗阶段先判断单位。如果里程最大值超过 50 万大概率是公里如果集中在 10 万以内可能是英里。统一转成公里后再训练。更稳妥的做法是看数据来源文档但很多公开数据集没有文档只能靠分布推断。5. 把估价系统跑得更稳交叉验证、超参搜索与线上推理技巧5.1 用交叉验证替代单次划分指标更可信单次train_test_split的评估结果受随机种子影响大换一个种子 MAE 可能差几千块。我习惯用 5 折交叉验证把每折的 MAE 和 R² 都打出来看均值和方差from sklearn.model_selection import cross_val_score # 5 折交叉验证评分用负 MAEsklearn 约定越大越好 mae_scores -cross_val_score(model, X, y, cv5, scoringneg_mean_absolute_error) r2_scores cross_val_score(model, X, y, cv5, scoringr2) print(fMAE 均值: {mae_scores.mean():.2f}, 标准差: {mae_scores.std():.2f}) print(fR2 均值: {r2_scores.mean():.4f}, 标准差: {r2_scores.std():.4f})标准差比均值更值得关注。如果某折 MAE 明显高于其他折说明数据分布不均匀可能是某个价格区间的样本集中在某一折。这时候要考虑分层抽样按价格分箱后再划分。5.2 超参搜索别用网格搜索用随机搜索网格搜索在参数多的时候组合爆炸随机搜索在同样的计算预算下通常能找到更优解。我一般用RandomizedSearchCV先粗搜定位范围再在小范围里细搜from sklearn.model_selection import RandomizedSearchCV param_dist { n_estimators: [100, 200, 300, 500], max_depth: [8, 12, 15, 20, None], min_samples_leaf: [1, 3, 5, 10], min_samples_split: [2, 5, 10] } search RandomizedSearchCV( RandomForestRegressor(random_state42, n_jobs-1), param_distributionsparam_dist, n_iter30, cv3, scoringneg_mean_absolute_error, random_state42, n_jobs-1 ) search.fit(X_train, y_train) print(search.best_params_) print(-search.best_score_)n_iter30表示随机采样 30 组参数配合 3 折交叉验证总共训练 90 次。如果数据量大可以降到 20 组。scoring用负 MAE 是因为 sklearn 的评分函数约定「越大越好」MAE 本身越小越好所以取负。5.3 线上推理批量预测和单条预测的写法差异训练完的模型最终要落到推理。批量预测直接传矩阵单条预测要注意构造 DataFrame 而不是 Series否则列名丢失# 批量预测 batch_pred model.predict(X_batch) # 单条预测用 DataFrame 包一层保证列名和训练时一致 single pd.DataFrame([{ age: 5, mileage: 60000, power: 150, displacement: 2.0, brand_encoded: 120000, gearbox_Manual: 0, fuel_type_Petrol: 1, body_type_Sedan: 1, is_accident: 0 }]) single single.reindex(columnstrain_columns, fill_value0) single_pred model.predict(single)单条预测最容易踩的坑是列顺序和训练时不一致。reindex能解决列缺失和顺序问题但前提是保存了train_columns。另外推理时的类别编码值要和训练时一致比如brand_encoded用的是训练集品牌均价新品牌要映射到「Other」的编码值。5.4 一个我常用的验证习惯每次训练完新模型我会挑 10 条验证集样本手动对比预测价和实际价再看 SHAP 解释。如果某条样本预测偏差大SHAP 能告诉我哪个特征把价格拉偏了。这个习惯帮我抓到过好几次数据问题——有一次发现power字段单位是马力还是千瓦没统一导致高功率车预测普遍偏低。模型指标只是数字落到具体样本上才能看出业务逻辑对不对。希望帮到你。本文还有配套的精品资源点击获取