拓冰建站拓冰建站
首页 / 资讯中心 / 正文

机器学习驱动的二手车价格预测系统实现全流程

简介面向二手车价格预测的完整机器学习项目针对二手车市场价格评估难的问题通过数据分析建立预测模型涵盖数据清洗、特征分析、线性回归建模、交叉验证调优与基于Flask的预测网站实现适合学习回归任务和模型落地的开发者。压缩包共71个文件包含Python脚本、Jupyter Notebook分析文档、训练好的.pkl模型、CSV数据集以及用于前端展示的图片和样式脚本HTML/CSS/JS等整体大小仅3.12MB结构清晰。资源已有1996人学习内容实用。项目中利用Pandas对二手车交易数据进行预处理包括缺失值删除与填补、异常值剔除并选用线性回归进行训练配合交叉验证评估模型。同时提供可直接运行的Flask Web应用实现价格预测功能。附带原始与清洗后的CSV数据、Python分析脚本以及.ipynb逐步分析文档方便对照复现。对于需要课程设计或快速了解机器学习项目完整流程的读者这份资源能提供很好的参考。1. 二手车价格预测系统实现前的关键判断基于机器学习的二手车价格预测系统核心不在于把模型堆得多深而在于让估价链路从原始车辆信息变成可交付的预测服务。输入品牌、车龄、里程、排量、变速箱和车况描述输出一档价格或区间再包装成 API 供估价页调用这套系统能不能落地往往取决于特征口径是不是稳定而不是模型结构是否够新。一个完整的二手车价格预测系统实现至少包括数据清洗、特征构造、模型训练、模型持久化和接口封装。这里只讲机器学习范围内的做法不引入深度学习因为表格型二手车特征用集成树模型就足够。适合正在做机器学习项目或准备做数据应用开发的人参考。2. 二手车价格预测系统的数据清洗与特征工程2.1 先检查价格分布再决定要不要缩尾处理多数二手车价格预测项目的数据来自平台车源记录或线下导入字段常包括上牌日期、表显里程、新车指导价、排量、变速箱、排放标准和车系。第一个要看的不是特征而是目标值 price因为价格分布直接决定后续该用 RMSE 还是带权误差来评估。初次看数据时我一般会先跑一段代码确认数据规模和价格分布import pandas as pd df pd.read_csv(car_source.csv) print(df.shape) print(df[price].describe()) # 过滤异常价格小于8000元的记录大概率是误写或过户工本费 df df[df[price] 8000] # 大于99分位的价格做截断避免个别超跑标价干扰模型 price_upper df[price].quantile(0.99) df df[df[price] price_upper]这段代码用describe()查看价格均值、四分位数和最大值再用业务经验把价格下限设为 8000 元上限取 99 分位数。8000 这个阈值不是固定的只做 10 万以上车型要调高下限只做廉价代步车则要压低上限。截断的目的是防止模型为了拟合极少数高价车而牺牲大部分普通车的预测精度。如果发现count明显小于原始行数说明 price 存在空值需要先看缺失记录print(df[df[price].isna()].head())价格缺失的记录占比低于 5% 时直接删除占比高就需要按车系补全否则样本会偏向未缺失的那部分价格区间。2.2 日期与里程加工成模型可用的特征原始数据里的上牌日期一般是字符串表显里程可能是带“万公里”的文本。车龄是二手车价格最重要的特征之一所以必须精确到月而不仅仅是年份。import numpy as np # 上牌日期转成月级车龄精度比年份更细 reg_date pd.to_datetime(df[register_date], errorscoerce) df[age_month] (pd.Timestamp.now() - reg_date) / np.timedelta64(1, M) # 表显里程统一为万公里数值 df[mileage] df[mileage].astype(str).str.replace(万公里, ).astype(float)车龄age_month的单位是月建模时不需要再除以 12树模型对数值尺度不敏感线性模型会自己学系数。里程统一成万公里后与价格的数量级更接近后续解释特征权重也更直观。里程缺失的常见做法是按车系分组填充中位数而不是全局填同一个值df[mileage] pd.to_numeric(df[mileage], errorscoerce) df[mileage] df.groupby(brand)[mileage].transform( lambda x: x.fillna(x.median()) )使用groupby之前要先确保brand列没有空值否则会丢失一部分样本。填充完成后可以再跑一次df[mileage].isna().sum()确认没有剩余缺失。2.3 类别特征转换成数值向量品牌、车系、变速箱、燃油类型都是类别特征。车系数量可能上百甚至上千直接 one-hot 会生成大量稀疏列对树模型是性能浪费对线性模型更容易造成共线性问题。我一般先把低频品牌合并或用频次编码再对少数类别做 one-hot# 品牌频次编码保留出现次数这个信息 brand_freq df[brand].value_counts().to_dict() df[brand_freq] df[brand].map(brand_freq) # 变速箱、燃油类型做 one-hot df pd.get_dummies(df, columns[transmission, fuel_type])频次编码把一个品牌压缩成一个数值等于告诉模型“这个品牌在样本里出现过多少次”对热门品牌有天然的先验信息。get_dummies会把transmission拆成transmission_自动、transmission_手动等。如果某个类别占比极低可以先映射成other再做 one-hot避免出现训练集有但预测请求没有的列。2.4 可用特征清单与划分验证经过清洗和构造后特征矩阵大致如下字段名类型处理方式原因age_month数值由上牌日期计算车龄是价格衰减第一驱动因素mileage数值统一为万公里并填充里程直接影响磨损和剩余价值brand_freq数值频次编码品牌类别多压缩维度并保留热度engine_power数值缺失用中位数填充动力参数对价格有正影响transmission_自动0/1one-hot自动挡溢价明显transmission_手动0/1one-hot手动挡对应低价车比例高划分数据前先检查特征矩阵是否还有空值因为后续reindex和predict阶段对空值非常敏感from sklearn.model_selection import train_test_split X df.drop(columns[price, car_id, register_date]) y df[price] assert X.isna().sum().sum() 0, 存在未处理的缺失值 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 )random_state42保证每次重跑结果一致test_size0.2在数据量低于 2 万条时也可以改成 0.3让测试集更稳定。3. 机器学习模型选型与评估从线性回归到集成模型3.1 先用线性回归建立基线选模型之前我会先让线性回归跑一版。它虽然假设价格与特征之间是线性可加的但能提供一个明确的下限如果后续模型比线性回归提升不到 5%说明改进重点应该在特征构造而不是盲目换复杂模型。from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_squared_error, mean_absolute_error lr LinearRegression() lr.fit(X_train, y_train) pred_lr lr.predict(X_test) print(Linear RMSE:, mean_squared_error(y_test, pred_lr, squaredFalse)) print(Linear MAE:, mean_absolute_error(y_test, pred_lr))mean_squared_error(..., squaredFalse)返回 RMSE单位是元MAE 更接近业务上“平均差多少钱”的描述。如果 RMSE 明显大于 MAE说明测试集中仍存在少量价格极端样本即使训练时做了截断高价车部分依然没有被完全覆盖。# 看残差分布确认是否有系统性高估或低估 residual y_test - pred_lr print(residual.quantile([0.1, 0.5, 0.9]))残差中位数如果小于 0表示预测价格普遍偏高90 分位和 10 分位跨度如果超过 5 万元说明模型对低价车或高价车的表现差异很大需要按价格区间进一步分桶评估。3.2 随机森林与 XGBoost 的取舍二手车特征是典型的表格型数据车龄、里程与价格之间不是简单线性关系因此线性模型只能做基线。常见做法是随机森林和 XGBoost 都试一遍看验证集表现再定。模型优点主要缺点优先调参项线性回归训练快解释性强非线性拟合弱易受共线性影响特征标准化删除高相关列随机森林不容易过拟合适合中小数据外推能力差高基数列干扰重要度n_estimators, max_depth, min_samples_leafXGBoost非线性强支持正则化和早停调参成本高小样本容易过拟合learning_rate, max_depth, subsample实现时我会先跑随机森林因为它对异常值和缺失值更宽容且不需要做特征标准化from sklearn.ensemble import RandomForestRegressor rf RandomForestRegressor( n_estimators200, max_depth12, min_samples_leaf4, random_state42, n_jobs-1 ) rf.fit(X_train, y_train) pred_rf rf.predict(X_test) print(RF RMSE:, mean_squared_error(y_test, pred_rf, squaredFalse)) print(RF MAE:, mean_absolute_error(y_test, pred_rf))n_estimators200在几万条样本内已经足够稳定再增大收益很小但训练时间线性增长。max_depth12防止树生长过深导致过拟合min_samples_leaf4让叶子节点至少有 4 个样本可以平滑价格噪声。如果随机森林的 MAE 还在可接受范围之外再上 XGBoostimport xgboost as xgb xgb_model xgb.XGBRegressor( learning_rate0.05, max_depth5, n_estimators500, subsample0.8, colsample_bytree0.8, random_state42 ) xgb_model.fit(X_train, y_train) pred_xgb xgb_model.predict(X_test) print(XGB RMSE:, mean_squared_error(y_test, pred_xgb, squaredFalse)) print(XGB MAE:, mean_absolute_error(y_test, pred_xgb))learning_rate0.05是常用起点为补偿小学习率把n_estimators提到 500。subsample0.8表示每轮迭代采样 80% 的行colsample_bytree0.8表示每棵树用 80% 的列这两个参数组合能有效减少过拟合。数据量少于 3 万行时max_depth不建议超过 6否则训练集上表现很好测试集容易崩。3.3 特征重要度要看排序也要看置换结果随机森林输出feature_importances_很方便但它会被高基数列带偏。比如car_id如果误留它会因为能完美划分单个样本而排到前面所以不能只依赖这个值。importance pd.Series( rf.feature_importances_, indexX_train.columns ).sort_values(ascendingFalse) print(importance.head(10))如果结果里brand_freq排在前面而mileage掉出前三需要怀疑里程字段填充比例过高。更稳妥的方式是计算置换重要性from sklearn.inspection import permutation_importance perm_res permutation_importance( rf, X_test, y_test, n_repeats5, random_state42, scoringneg_mean_absolute_error )置换重要性的逻辑是打乱某一列看模型误差上升多少。它对高基数列不友好但不会因为树的分裂次数而给出错误信号。n_repeats5表示每列打乱 5 次取平均运行时间大约是模型预测时间的 5 倍适合在模型定稿前做一次。3.4 用交叉验证代替单次划分单次train_test_split的结果随机波动较大模型最终要上线我一般再看一次交叉验证from sklearn.model_selection import cross_val_score cv_mae -cross_val_score( rf, X_train, y_train, cv5, scoringneg_mean_absolute_error ) print(cv_mae.mean(), cv_mae.std())cv5对二手车这种万级样本是常见选择。sklearn 的neg_mean_absolute_error是越高越好所以取负号后就是常规 MAE。如果标准差大于均值的一半说明模型在不同子集上表现波动大需要回到特征工程检查区域或品牌分布是否失衡。4. 预测系统实现把机器学习模型封装成可调用的服务4.1 固定特征列顺序再持久化模型训练时不固定列顺序上线后用pd.DataFrame构造输入就会列对齐错乱导致模型预测结果完全不可信。因此训练完要同时保存模型和特征列名。import joblib joblib.dump(rf, car_price_model.joblib) joblib.dump(list(X_train.columns), feature_columns.joblib)rf是上一章训练好的随机森林模型。feature_columns.joblib保存的是列名列表。预测服务启动时加载这两个文件按同一个顺序构造输入。4.2 Flask 提供单条预测接口最小可用版本是一个 Flask 服务接收 JSON 请求体返回预测价格。代码结构分为加载模型、构造输入、预测三部分from flask import Flask, request, jsonify import joblib import pandas as pd app Flask(__name__) model joblib.load(car_price_model.joblib) feature_columns joblib.load(feature_columns.joblib) app.route(/predict, methods[POST]) def predict(): data request.get_json() df pd.DataFrame([data]) df df.reindex(columnsfeature_columns, fill_value0) price model.predict(df)[0] return jsonify({ predicted_price: round(float(price), 2) }) if __name__ __main__: app.run(host0.0.0.0, port5000)reindex(columnsfeature_columns, fill_value0)是关键它保证请求里缺哪个字段就用 0 补齐顺序也严格按照训练时列名顺序来。但用 0 补缺失字段有风险例如手动挡字段填 0 会变成自动挡所以接口层需要先做必填字段校验。4.3 接口参数校验规则至少要有车龄、里程、品牌频次和变速箱类型否则模型预测的是缺字段样本。常用校验规则如下字段名是否必填取值范围失败处理brand_freq是大于 0返回 400提示品牌信息缺失age_month是1 到 400超过 400 按边界截断mileage是0 到 100超过 100 拒绝预测transmission_自动否0 或 1缺失时按 0 处理校验逻辑可以放在predict函数开头from flask import abort required [brand_freq, age_month, mileage] for col in required: if col not in data: abort(400, descriptionf缺少字段 {col})abort会返回带错误信息的 HTTP 400 响应比前端看到 500 更容易定位问题。这里建议把错误信息也写入日志方便后续统计哪些字段经常缺失。4.4 批量预测脚本与日志记录线上除了单条接口还经常需要 CSV 批量预测。脚本比接口更简单original_df pd.read_csv(to_predict.csv) batch_df original_df.reindex(columnsfeature_columns, fill_value0) batch_df[predicted_price] model.predict(batch_df) assert len(batch_df) len(original_df), 行数不一致 batch_df.to_csv(predicted_result.csv, indexFalse)reindex可能会因为原始表索引错位而丢行所以必须加assert校验行数。如果to_predict.csv有 1000 行结果也必须 1000 行。价格属于敏感业务数据每次预测都要有日志。简单做法是在predict里记录请求和响应import logging logging.basicConfig(levellogging.INFO) logger logging.getLogger(car_price) logger.info(request: %s, data) logger.info(response: %s, predicted_price)日志最终可以接到后端的监控告警里而不是永远没人看。请求参数和预测结果对调整模型阈值非常重要。5. 预测系统上线后的验证技巧与模型更新策略5.1 用真实成交记录对比预测误差上线后的系统不能只看训练时的测试集误差。常见做法是每天把已成交车辆的真实价格和系统预测价格放到一起算误差分位数。real_df pd.read_csv(actual_transactions.csv) pred model.predict(real_df.reindex(columnsfeature_columns, fill_value0)) real_df[pred_price] pred real_df[error_rate] (real_df[pred_price] - real_df[price]).abs() / real_df[price] print(real_df[error_rate].quantile([0.5, 0.9, 0.95]))中位数误差率超过 10%说明模型在主价格带上已经偏离90 分位误差率高说明有的车预测得离谱需要检查是否出现了训练集里没有的新车型或改装车。5.2 用 PSI 检测输入分布漂移二手车市场变化快半年前的模型特征分布可能已经不再适用。检测方法里 PSI 比较直观把训练集预测值作为基准当前批次预测值作为新样本看两者分布差异。import numpy as np def psi(expected, actual, bins10): hist_e, edges np.histogram(expected, binsbins) hist_a, _ np.histogram(actual, binsedges) pct_e hist_e 1e-6 pct_a hist_a 1e-6 pct_e pct_e / pct_e.sum() pct_a pct_a / pct_a.sum() return ((pct_a - pct_e) * np.log(pct_a / pct_e)).sum()调用时把训练集预测值和当天预测值分别传入。PSI 小于 0.1 表示稳定0.1 到 0.25 需要观察大于 0.25 就要触发重训。这里的edges复用基准分布的分箱保证两次对比使用同一套区间。5.3 重训与模型版本管理重训前要把旧模型保存到带时间戳的目录常见结构是models/20240101/和models/20240201/。预测服务加载时优先读最新目录发现误差变大时能快速回滚。import glob latest sorted(glob.glob(models/*))[-1] model joblib.load(latest /car_price_model.joblib)用glob按目录名排序时时间戳最好用YYYYMMDD格式这样字典序和日期序一致。重训脚本挂到每周任务里自动训练、替换、比对误差再配合 PSI 监控就能形成一套简单的模型自维护闭环。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门