二手车价格预测是数据科学领域一个经典且实用的回归问题。在现实生活中,二手车交易涉及众多因素,从车辆基本信息(品牌、年份、里程)到技术参数(发动机规格、变速箱类型),再到历史记录(事故情况、维修保养)等,都会影响最终成交价格。这个Kaggle竞赛项目提供了一个模拟真实场景的数据集,要求参赛者构建一个能够准确预测二手车价格的机器学习模型。
作为一名数据科学从业者,我经常遇到类似的价格预测需求。与新建模项目相比,二手车价格预测有几个独特挑战:数据质量参差不齐(缺失值、异常值多),特征类型复杂(数值型、类别型混合),且价格受主观因素影响大。本次实战将完整展示从数据清洗到模型部署的全流程,特别关注如何通过特征工程提升模型性能。
首先加载并观察数据集的基本情况。Kaggle提供了三个文件:训练集(train.csv)、测试集(test.csv)和提交样例(sample_submission.csv)。使用pandas读取后,我习惯先用head()和info()快速浏览数据结构和类型:
python复制import pandas as pd
df_train = pd.read_csv("train.csv")
df_test = pd.read_csv("test.csv")
print(df_train.head())
print(df_train.info())
初步观察发现数据集包含以下典型特征:
接下来系统检查数据质量问题:
python复制# 缺失值统计
print("训练集缺失值:")
print(df_train.isnull().sum())
# 数值特征分布
print("\n数值特征描述:")
print(df_train[['milage', 'model_year', 'price']].describe())
# 类别特征基数
categorical_cols = df_train.select_dtypes(include=['object']).columns
for col in categorical_cols:
print(f"{col}: {df_train[col].nunique()}个唯一值")
常见问题包括:
通过可视化更直观理解数据分布和关系:
python复制import matplotlib.pyplot as plt
import seaborn as sns
# 价格分布
plt.figure(figsize=(10,6))
sns.histplot(df_train['price'], bins=50, kde=True)
plt.title('二手车价格分布')
plt.show()
# 品牌与价格关系
plt.figure(figsize=(12,6))
brand_price = df_train.groupby('brand')['price'].median().sort_values(ascending=False)
sns.barplot(x=brand_price.index, y=brand_price.values)
plt.xticks(rotation=90)
plt.title('各品牌二手车中位价格')
plt.show()
关键发现:
针对发现的异常情况,制定处理策略:
python复制current_year = 2024
df_train['model_year'] = df_train['model_year'].apply(lambda x: x if x <= current_year else None)
python复制Q1 = df_train['milage'].quantile(0.25)
Q3 = df_train['milage'].quantile(0.75)
IQR = Q3 - Q1
upper_bound = Q3 + 1.5*IQR
df_train['milage'] = df_train['milage'].apply(lambda x: upper_bound if x > upper_bound else x)
python复制price_upper = df_train['price'].quantile(0.99)
df_train['is_outlier'] = df_train['price'] > price_upper
根据特征类型采用不同填充策略:
python复制from sklearn.impute import KNNImputer
num_cols = ['milage', 'model_year']
imputer = KNNImputer(n_neighbors=5)
df_train[num_cols] = imputer.fit_transform(df_train[num_cols])
python复制cat_cols = ['brand', 'fuel_type', 'transmission']
for col in cat_cols:
df_train[col] = df_train[col].fillna('Unknown')
python复制# 提取马力、排量等信息(见特征工程部分)
# 然后对提取的数值特征进行填充
按8:2划分训练集和验证集,保留时间序列特性:
python复制from sklearn.model_selection import train_test_split
train_df, val_df = train_test_split(df_train, test_size=0.2, random_state=42, shuffle=True)
python复制train_df['car_age'] = current_year - train_df['model_year']
python复制import re
def extract_engine_info(text):
# 提取马力
hp = re.search(r'(\d+)\s*HP', text)
hp = int(hp.group(1)) if hp else None
# 提取排量
liters = re.search(r'(\d+\.?\d*)\s*L', text)
liters = float(liters.group(1)) if liters else None
return hp, liters
train_df['engine_hp'], train_df['engine_l']] = zip(*train_df['engine'].apply(extract_engine_info))
python复制train_df['miles_per_year'] = train_df['milage'] / (train_df['car_age'] + 1)
python复制brand_avg = train_df.groupby('brand')['price'].mean().to_dict()
global_avg = train_df['price'].mean()
train_df['brand_premium'] = train_df['brand'].apply(lambda x: (brand_avg.get(x, global_avg) - global_avg)/global_avg)
python复制train_df['hp_per_liter'] = train_df['engine_hp'] / train_df['engine_l']
train_df['luxury_auto'] = (train_df['brand'].isin(['BMW','Mercedes'])) & (train_df['transmission'] == 'Automatic')
python复制train_df['tech_penalty'] = 0.95 ** train_df['car_age'] # 假设每年技术贬值5%
python复制from category_encoders import TargetEncoder
encoder = TargetEncoder(cols=['brand','fuel_type'])
train_df = encoder.fit_transform(train_df, train_df['price'])
python复制import numpy as np
train_df['month_sin'] = np.sin(2 * np.pi * train_df['sale_month']/12)
train_df['month_cos'] = np.cos(2 * np.pi * train_df['sale_month']/12)
考虑三类主流算法进行对比:
经过初步验证,梯度提升树表现最好,最终选择LightGBM作为基础模型。
python复制import lightgbm as lgb
from sklearn.metrics import mean_squared_error
# 准备数据
features = ['car_age', 'milage', 'engine_hp', 'brand_premium', ...] # 选择重要特征
X_train = train_df[features]
y_train = train_df['price']
X_val = val_df[features]
y_val = val_df['price']
# 定义模型
params = {
'objective': 'regression',
'metric': 'rmse',
'boosting_type': 'gbdt',
'learning_rate': 0.05,
'num_leaves': 31,
'min_data_in_leaf': 20,
'feature_fraction': 0.8,
'bagging_fraction': 0.8,
'verbosity': -1
}
# 训练
model = lgb.LGBMRegressor(**params)
model.fit(X_train, y_train,
eval_set=[(X_val, y_val)],
early_stopping_rounds=50,
verbose=10)
# 评估
val_pred = model.predict(X_val)
rmse = mean_squared_error(y_val, val_pred, squared=False)
print(f"Validation RMSE: {rmse:.2f}")
使用Optuna进行自动调参:
python复制import optuna
def objective(trial):
params = {
'learning_rate': trial.suggest_float('learning_rate', 0.01, 0.1),
'num_leaves': trial.suggest_int('num_leaves', 20, 100),
'min_data_in_leaf': trial.suggest_int('min_data_in_leaf', 10, 50),
'feature_fraction': trial.suggest_float('feature_fraction', 0.7, 1.0),
'lambda_l1': trial.suggest_float('lambda_l1', 0, 5),
'lambda_l2': trial.suggest_float('lambda_l2', 0, 5)
}
model = lgb.LGBMRegressor(**params)
model.fit(X_train, y_train)
preds = model.predict(X_val)
return mean_squared_error(y_val, preds, squared=False)
study = optuna.create_study(direction='minimize')
study.optimize(objective, n_trials=50)
best_params = study.best_params
使用SHAP值分析特征重要性:
python复制import shap
explainer = shap.TreeExplainer(model)
shap_values = explainer.shap_values(X_val)
# 特征重要性图
shap.summary_plot(shap_values, X_val, plot_type="bar")
# 单个预测解释
shap.force_plot(explainer.expected_value, shap_values[0,:], X_val.iloc[0,:])
关键发现:
除了RMSE,还监控以下指标:
python复制from sklearn.metrics import mean_absolute_error, r2_score
def calculate_metrics(y_true, y_pred):
metrics = {
'RMSE': mean_squared_error(y_true, y_pred, squared=False),
'MAE': mean_absolute_error(y_true, y_pred),
'R2': r2_score(y_true, y_pred),
'MAPE': np.mean(np.abs((y_true - y_pred)/y_true))*100
}
return metrics
print(calculate_metrics(y_val, val_pred))
深入分析预测误差分布:
python复制val_df['pred'] = val_pred
val_df['error'] = val_df['pred'] - val_df['price']
val_df['abs_error'] = np.abs(val_df['error'])
# 按价格区间分析误差
bins = [0, 10000, 20000, 30000, 50000, 100000, np.inf]
val_df['price_bin'] = pd.cut(val_df['price'], bins=bins)
error_by_bin = val_df.groupby('price_bin')['abs_error'].mean()
发现高价车(>5万)的预测误差显著增大,考虑以下改进:
结合XGBoost和CatBoost构建简单集成:
python复制from xgboost import XGBRegressor
from catboost import CatBoostRegressor
# 训练其他模型
xgb = XGBRegressor()
xgb.fit(X_train, y_train)
cb = CatBoostRegressor(verbose=0)
cb.fit(X_train, y_train)
# 加权平均
ensemble_pred = 0.5*model.predict(X_val) + 0.3*xgb.predict(X_val) + 0.2*cb.predict(X_val)
print(calculate_metrics(y_val, ensemble_pred))
集成后RMSE提升约3%,但推理时间增加,需权衡性能与效率。
保存训练好的模型和预处理管道:
python复制import joblib
from sklearn.pipeline import Pipeline
# 构建完整管道
pipeline = Pipeline([
('preprocessor', preprocessor), # 包含所有预处理步骤
('model', model)
])
# 保存
joblib.dump(pipeline, 'car_price_pipeline.pkl')
使用FastAPI创建预测服务:
python复制from fastapi import FastAPI
import pandas as pd
app = FastAPI()
model = joblib.load('car_price_pipeline.pkl')
@app.post("/predict")
async def predict(car_data: dict):
df = pd.DataFrame([car_data])
pred = model.predict(df)
return {"predicted_price": float(pred[0])}
建立模型监控机制:
特征工程决定上限:好的特征比模型选择更重要。在本次项目中,从engine字段提取的参数和创建的品牌溢价特征对模型提升最大。
业务理解至关重要:知道哪些因素真正影响二手车价格(如事故历史比颜色更重要)能指导特征选择。
误差分析指引方向:通过分析模型在哪里出错,可以针对性改进,如我们发现高价车预测不准后,增加了豪华车特定特征。
数据泄露:在目标编码或填充缺失值时,如果使用全量数据统计会导致验证分数虚高。务必只在训练集上计算统计量。
过度依赖自动化:自动特征工程工具和AutoML虽然方便,但无法替代对业务的理解。我曾尝试用autofeat生成数百个特征,结果反而降低了模型性能。
忽视推理成本:复杂的集成模型在线服务时可能延迟过高。我们最终选择了单个LightGBM模型而非集成,因为性能提升不足以justify额外的计算成本。
python复制for col in cat_cols:
df[col] = df[col].astype('category')
并行处理:LightGBM和CatBoost原生支持GPU加速。在大型数据集上,使用GPU训练可将时间从小时缩短到分钟。
增量学习:当数据太大无法一次性加载时,可以使用fit()的init_model参数进行增量训练:
python复制model = lgb.LGBMRegressor()
for chunk in pd.read_csv('large_data.csv', chunksize=10000):
model.fit(chunk[features], chunk['price'], init_model=model)
这个项目让我深刻体会到,一个好的预测系统不仅需要技术能力,更需要领域知识和工程思维的结合。每次当我陷入技术细节时,退一步思考"这个改动是否真的能帮助车商更好地定价",往往能找到更有效的改进方向。