01
明确任务
一般的预测任务分为回归任务和分类任务,二手车的价格是一个明确的固定值,因此二手车价格预测是一个回归任务。
回归任务的模型有很多,如:线性回归、K近邻(KNN)、岭回归、多层感知机、决策树回归、极限树回归、随机森林、梯度提升树……
诸多模型中有一部分模型是通过多个弱学习器集成起来的模型,像随机森林、Voting等,它们具有更强的学习能力,集成多个单一学习器得到最终结果,一般集成模型的预测效果表现都很良好。
为了给大家提供一个二手车价格预测的baseline,我将选取几个经典模型进行训练。
02
模型训练
【导包】
代码语言:javascript复制import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.pyplot import MultipleLocator
import sys
import seaborn as sns
import warnings
warnings.filterwarnings("ignore")
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
pd.set_option('display.max_rows', 100,'display.max_columns', 1000,"display.max_colwidth",1000,'display.width',1000)
from sklearn.metrics import *
from sklearn.linear_model import *
from sklearn.neighbors import *
from sklearn.svm import *
from sklearn.neural_network import *
from sklearn.tree import *
from sklearn.ensemble import *
from xgboost import *
import lightgbm as lgb
import tensorflow as tf
from tensorflow.keras import layers
from sklearn.preprocessing import *
from sklearn.ensemble import RandomForestRegressor as RFR
from sklearn.model_selection import *
【读取数据】
代码语言:javascript复制# final_data.xlsx 是上一次分享最后数据处理后的
data = pd.read_excel("final_data.xlsx", na_values=np.nan
# 将数据划分输入和结果集
X = data[ data.columns[1:] ]
y_reg = data[ data.columns[0] ]
# 切分训练集和测试集, random_state是切分数据集的随机种子,要想复现本文的结果,随机种子应该一致
x_train, x_test, y_train, y_test = train_test_split(X, y_reg, test_size=0.3, random_state=42)
【评价指标】
- 平均绝对误差(MAE)
平均绝对误差的英文全称为 Mean Absolute Error,也称之为 L1 范数损失。是通过计算预测值和真实值之间的距离的绝对值的均值,来衡量预测值与真实值之间的距离。
- 计算公式:
2. 均方误差(MSE)
均方误差英文全称为 Mean Squared Error,也称之为 L2 范数损失。通过计算真实值与预测值的差值的平方和的均值来衡量距离。
- 计算公式:
3. 均方根误差(RMSE)
均方根误差的英文全称为 Root Mean Squared Error,代表的是预测值与真实值差值的样本标准差。
- 计算公式:
4. 决定系数(R2)
决定系数评估的是预测模型相对于基准模型(真实值的平均值作为预测值)的好坏程度。
- 计算公式:
- 最好的模型预测的 R2 的值为 1,表示预测值与真实值之间是没有偏差的;
- 但是最差的模型,得到的 R2 的值并不是 0,而是会得到负值;
- 当模型的 R2 值为负值,表示模型预测结果比基准模型(均值模型)表现要差;
- 当模型的 R2 值大于 0,表示模型的预测结果比使用均值预测得到的结果要好。
# 评价指标函数定义,其中R2的指标可以由模型自身得出,后面的score即为R2
def evaluation(model):
ypred = model.predict(x_test)
mae = mean_absolute_error(y_test, ypred)
mse = mean_squared_error(y_test, ypred)
rmse = sqrt(mse)
print("MAE: %.2f" % mae)
print("MSE: %.2f" % mse)
print("RMSE: %.2f" % rmse)
return ypred
【线性回归】
代码语言:javascript复制model_LR = LinearRegression()
model_LR.fit(x_train, y_train)
print("params: ", model_LR.get_params())
print("train score: ", model_LR.score(x_train, y_train))
print("test score: ", model_LR.score(x_test, y_test))
predict_y = evaluation(model_LR)
输出结果:
代码语言:javascript复制params: {'copy_X': True, 'fit_intercept': True, 'n_jobs': None, 'normalize': False}
train score: 0.823587700962806
test score: 0.7654427047191524
MAE: 1.97
MSE: 25.90
RMSE: 5.09
我们可以将 y_test 转换为 numpy 的 array 形式,方便后面的绘图。
代码语言:javascript复制test_y = np.array(y_test)
由于数据量较多,我们取预测结果和真实结果的前50个数据进行绘图。
代码语言:javascript复制plt.figure(figsize=(10,10))
# 预测值
plt.title('线性回归-真实值预测值对比')
plt.plot(predict_y[:50], 'ro-', label='预测值')
plt.plot(test_y[:50], 'go-', label='真实值')
plt.legend()
plt.show()
上图中,绿色点为真实值,红色点为预测值,当两点几乎重合时,说明模型的预测结果十分接近。
【K近邻】
代码语言:javascript复制model_knn = KNeighborsRegressor()
model_knn.fit(x_train, y_train)
print("params: ", model_knn.get_params())
print("train score: ", model_knn.score(x_train, y_train))
print("test score: ", model_knn.score(x_test, y_test))
predict_y = evaluation(model_knn)
输出结果:
代码语言:javascript复制params: {'algorithm': 'auto', 'leaf_size': 30, 'metric': 'minkowski', 'metric_params': None, 'n_jobs': None, 'n_neighbors': 5, 'p': 2, 'weights': 'uniform'}
train score: 0.8701966571367806
test score: 0.7886892618747352
MAE: 1.35
MSE: 23.33
RMSE: 4.83
绘图:
代码语言:javascript复制plt.figure(figsize=(10,10))
# 预测值
plt.title('KNN-真实值预测值对比')
plt.plot(predict_y[:50], 'ro-', label='预测值')
plt.plot(test_y[:50], 'go-', label='真实值')
plt.legend()
plt.show()
【决策树回归】
代码语言:javascript复制model_dtr = DecisionTreeRegressor(max_depth = 5, random_state=30)
model_dtr.fit(x_train, y_train)
print("params: ", model_dtr.get_params())
print("train score: ", model_dtr.score(x_train, y_train))
print("test score: ", model_dtr.score(x_test, y_test))
predict_y = evaluation(model_dtr)
在这里,决策树学习器加入了一个max_depth的参数,这个参数限定了树的最大深度,设置这个参数的主要原因是为了防止模型过拟合
输出结果:
代码语言:javascript复制params: {'criterion': 'mse', 'max_depth': 5, 'max_features': None, 'max_leaf_nodes': None, 'min_impurity_decrease': 0.0, 'min_impurity_split': None, 'min_samples_leaf': 1, 'min_samples_split': 2, 'min_weight_fraction_leaf': 0.0, 'presort': False, 'random_state': 30, 'splitter': 'best'}
train score: 0.8339532311129941
test score: 0.7857733129009127
MAE: 2.16
MSE: 18.37
RMSE: 4.29
如果不限定树的最大深度会发生什么呢?
代码语言:javascript复制model_dtr = DecisionTreeRegressor(random_state=30)
model_dtr.fit(x_train, y_train)
print("params: ", model_dtr.get_params())
print("train score: ", model_dtr.score(x_train, y_train))
print("test score: ", model_dtr.score(x_test, y_test))
predict_y = evaluation(model_dtr)
输出结果:
代码语言:javascript复制params: {'criterion': 'mse', 'max_depth': None, 'max_features': None, 'max_leaf_nodes': None, 'min_impurity_decrease': 0.0, 'min_impurity_split': None, 'min_samples_leaf': 1, 'min_samples_split': 2, 'min_weight_fraction_leaf': 0.0, 'presort': False, 'random_state': 30, 'splitter': 'best'}
train score: 0.999999225529954
test score: 0.8673942873037808
MAE: 1.16
MSE: 14.64
RMSE: 3.83
获取树的最大深度:
代码语言:javascript复制model_dtr.get_depth()
输出结果:
代码语言:javascript复制38
我们发现,在不限定树的最大深度时,决策树模型的训练得分(R2)为:0.999999225529954,但测试得分仅为:0.8673942873037808。
这就是模型过拟合,在训练数据上的表现非常良好,当用未训练过的测试数据进行预测时,模型的泛化能力不足,导致测试结果不理想。
感兴趣的同学可以自行查阅关于决策树剪枝的过程。
绘图:
代码语言:javascript复制plt.figure(figsize=(10,10))
# 预测值
plt.title('决策树回归-真实值预测值对比')
plt.plot(predict_y[:50], 'ro-', label='预测值')
plt.plot(test_y[:50], 'go-', label='真实值')
plt.legend()
plt.show()
【随机森林】
代码语言:javascript复制model_rfr = RandomForestRegressor(random_state=30)
model_rfr.fit(x_train, y_train)
print("params: ", model_rfr.get_params())
print("train score: ", model_rfr.score(x_train, y_train))
print("test score: ", model_rfr.score(x_test, y_test))
predict_y = evaluation(model_rfr)
输出结果:
代码语言:javascript复制params: {'bootstrap': True, 'criterion': 'mse', 'max_depth': None, 'max_features': 'auto', 'max_leaf_nodes': None, 'min_impurity_decrease': 0.0, 'min_impurity_split': None, 'min_samples_leaf': 1, 'min_samples_split': 2, 'min_weight_fraction_leaf': 0.0, 'n_estimators': 10, 'n_jobs': None, 'oob_score': False, 'random_state': 30, 'verbose': 0, 'warm_start': False}
train score: 0.9873589573962257
test score: 0.9004628017201377
MAE: 0.88
MSE: 10.99
RMSE: 3.32
绘图:
代码语言:javascript复制plt.figure(figsize=(10,10))
# 预测值
plt.title('随机森林-真实值预测值对比')
plt.plot(predict_y[:50], 'ro-', label='预测值')
plt.plot(test_y[:50], 'go-', label='真实值')
plt.legend()
plt.show()
【XGBoost】
XGboost是华盛顿大学博士陈天奇创造的一个梯度提升(Gradient Boosting)的开源框架。至今可以算是各种数据比赛中的大杀器,被大家广泛地运用。
代码语言:javascript复制model_xgbr = XGBRegressor(n_estimators = 200, max_depth=5, random_state=1024)
model_xgbr.fit(x_train, y_train)
print("params: ", model_xgbr.get_params())
print("train score: ", model_xgbr.score(x_train, y_train))
print("test score: ", model_xgbr.score(x_test, y_test))
predict_y = evaluation(model_xgbr)
输出结果:
代码语言:javascript复制params: {'objective': 'reg:squarederror', 'base_score': 0.5, 'booster': 'gbtree', 'colsample_bylevel': 1, 'colsample_bynode': 1, 'colsample_bytree': 1, 'gamma': 0, 'gpu_id': -1, 'importance_type': 'gain', 'interaction_constraints': '', 'learning_rate': 0.300000012, 'max_delta_step': 0, 'max_depth': 5, 'min_child_weight': 1, 'missing': nan, 'monotone_constraints': '()', 'n_estimators': 200, 'n_jobs': 0, 'num_parallel_tree': 1, 'random_state': 1024, 'reg_alpha': 0, 'reg_lambda': 1, 'scale_pos_weight': 1, 'subsample': 1, 'tree_method': 'exact', 'validate_parameters': 1, 'verbosity': None}
train score: 0.9897272945187993
test score: 0.8923618501911923
MAE: 0.88
MSE: 11.88
RMSE: 3.45
绘图:
代码语言:javascript复制plt.figure(figsize=(10,10))
# 预测值
plt.title('XGBR-真实值预测值对比')
plt.plot(predict_y[:50], 'ro-', label='预测值')
plt.plot(test_y[:50], 'go-', label='真实值')
plt.legend()
plt.show()
【集成模型Voting】
Voting可以简单理解为将各个模型的结果加权平均,也是使用较多的一种集成模型。
代码语言:javascript复制model_voting = VotingRegressor(estimators=[('model_LR', model_LR),
('model_knn', model_knn),
('model_dtr', model_dtr),
('model_rfr', model_rfr),
('model_xgbr', model_xgbr)])
model_voting.fit(x_train, y_train)
# print("params: ", model_voting.get_params())
print("train score: ", model_voting.score(x_train, y_train))
print("test score: ", model_voting.score(x_test, y_test))
predict_y = evaluation(model_voting)
输出结果:
代码语言:javascript复制train score: 0.9572901767964346
test score: 0.8964163648799375
MAE: 1.08
MSE: 11.44
RMSE: 3.38
绘图:
代码语言:javascript复制plt.figure(figsize=(10,10))
# 预测值
plt.title('集成模型voting-真实值预测值对比')
plt.plot(predict_y[:50], 'ro-', label='预测值')
plt.plot(test_y[:50], 'go-', label='真实值')
plt.legend()
plt.show()
【Tensorflow神经网络】
使用Tensorflow前需要安装,本文使用的是tensorflow2.0.0版本。
为了使模型快速收敛,首先需要将数据标准化。
代码语言:javascript复制scaler = StandardScaler()
x_train_scaled = scaler.fit_transform(x_train)
x_test_scaled = scaler.transform(x_test)
train_x = np.array(x_train_scaled)
test_x = np.array(x_test_scaled)
train_y = np.array(y_train)
test_y = np.array(y_test)
代码语言:javascript复制# 1、模型初始化
model_tf = tf.keras.Sequential()
# 2、添加隐藏层,并设置激活函数,这里我们用relu
model_tf.add(layers.Dense(128, activation='relu'))
model_tf.add(layers.Dense(64, activation='relu'))
model_tf.add(layers.Dense(8, activation='relu'))
model_tf.add(layers.Dense(4, activation='relu'))
model_tf.add(layers.Dense(1, activation='relu'))
# 3、初始化输入的shape,189为输入的189维特征
model_tf.build(input_shape =(None,189))
# 4、编译模型
model_tf.compile(optimizer=tf.keras.optimizers.RMSprop(0.001),
loss=tf.keras.losses.mean_squared_error,
metrics=['mse', 'mae'])
# 5、模型训练
history = model_tf.fit(train_x, train_y, epochs=200, batch_size=128,
validation_split = 0.2, #从测试集中划分80%给训练集
validation_freq = 1) #测试的间隔次数为1
# 获取模型训练过程
model_tf.summary()
输出结果:
模型训练损失情况:
代码语言:javascript复制history_dict = history.history
loss_values = history_dict['loss']
mae=history_dict["mae"]
mse=history_dict["mse"]
val_loss = history_dict['val_loss']
val_mae = history_dict['val_mae']
val_mse = history_dict['val_mse']
epochs = range(1, len(loss_values) 1)
plt.figure(figsize=(8,5))
plt.plot(range(1, len(loss_values) 1), loss_values, label = 'Training loss')
plt.plot(range(1, len(val_loss) 1), val_loss, label = 'Validation loss')
plt.title('Training and validation loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()
plt.show()
模型MAE:
代码语言:javascript复制plt.figure(figsize=(8,5))
plt.plot(range(1, len(mae) 1), mae, label = 'mae')
plt.plot(range(1, len(val_mae) 1), val_mae, label = 'val_mae')
plt.title('mean_absolute_error')
plt.xlabel('Epochs')
plt.ylabel('error')
plt.legend()
#plt.plot(loss_values)
#plt.plot(val_loss)
#plt.plot(pre)
plt.show()
模型MSE:
代码语言:javascript复制plt.figure(figsize=(8,5))
plt.plot(range(1, len(mse) 1), mse, label = 'mse')
plt.plot(range(1, len(val_mse) 1), val_mse, label = 'val_mse')
plt.title('mean_squre_error')
plt.xlabel('Epochs')
plt.ylabel('error')
plt.legend()
#plt.plot(loss_values)
#plt.plot(val_loss)
#plt.plot(pre)
plt.show()
模型评估:
代码语言:javascript复制ypred = model_tf.predict(test_x)
mae = mean_absolute_error(test_y, ypred)
mse = mean_squared_error(test_y, ypred)
rmse = sqrt(mse)
print("MAE: %.2f" % mae)
print("MSE: %.2f" % mse)
print("RMSE: %.2f" % rmse)
输出结果:
代码语言:javascript复制MAE: 0.94
MSE: 24.03
RMSE: 4.90
【各模型结果】
模型名称 | MAE | MSE | RMSE | TrainR2 | TestR2 |
---|---|---|---|---|---|
线性回归 | 1.97 | 25.9 | 5.09 | 0.82 | 0.76 |
KNN | 1.35 | 23.3 | 4.83 | 0.87 | 0.78 |
决策树 | 2.09 | 23.9 | 4.89 | 0.84 | 0.78 |
随机森林 | 0.88 | 10.99 | 3.32 | 0.98 | 0.90 |
XGBoost | 0.88 | 11.88 | 3.45 | 0.98 | 0.89 |
Voting | 1.08 | 11.44 | 3.38 | 0.95 | 0.89 |
神经网络 | 0.94 | 24.0 | 4.90 | 0.98 | 0.78 |
从上表可以看出,随机森林模型的整体表现最好。当然,这不代表最优结果,因为上述7种模型都没有进行调参优化,经过调参后的各个模型效果还有可能提升。
【重要特征筛选】
为了提升模型效果也可以对数据做特征筛选,这里可以通过相关系数分析法,将影响二手车售价的特征与售价做相关系数计算,pandas包很方便的集成了算法,并且可以通过seaborn、matplotlib等绘图包将相关系数用热力图的方式可视化。
代码语言:javascript复制# 这里的列取final_data中,除0-1和独热编码形式的数据
corr_cols = list(data.columns[:28]) list(data.columns[43:49])
test_data = data[corr_cols]
test_data_corr = test_data.corr()
price_corr = dict(test_data_corr.iloc[0])
price_corr = sorted(price_corr.items(), key=lambda x: abs(x[1]), reverse=True)
# 输出按绝对值排序后的相关系数
print(price_corr)
输出结果,除"售价"外,共有33个特征:
代码语言:javascript复制[('售价', 1.0),
('新车售价', 0.7839935301900343),
('最大功率(kW)', 0.7117612690884588),
('最大马力(Ps)', 0.7110554212212731),
('最大扭矩(N·m)', 0.696952631401139),
('最高车速(km/h)', 0.5419965777866631),
('排量(mL)', 0.5330405118740592),
('排量(L)', 0.5308107362792799),
('整备质量(kg)', 0.49194473580917225),
('官方0-100km/h加速(s)', -0.48176542342318535),
('宽度(mm)', 0.4714397275896106),
('轴距(mm)', 0.46166756230018036),
('气缸数(个)', 0.4454272911508132),
('油箱容积(L)', 0.437777686409757),
('后轮距(mm)', 0.38050056092464896),
('长度(mm)', 0.3713764530321577),
('前轮距(mm)', 0.3620381932871346),
('最大扭矩转速(rpm)', -0.3350318499883131),
('注册日期差(天)', -0.2737947095245184),
('出厂日期差(天)', -0.2598892184068559),
('工信部综合油耗(L/100km)', 0.21117598576704852),
('行驶里程', -0.15837809707633024),
('最大功率转速(rpm)', -0.14929627599693607),
('行李厢容积(L)', 0.1303796302281349),
('每缸气门数(个)', 0.08610716158708019),
('压缩比', 0.0820485577972737),
('车门数', -0.05405277879497536),
('高度(mm)', 0.04866102899154555),
('商业险过期日期差(天)', -0.047366786391213236),
('交强险过期日期差(天)', -0.04465347803879652),
('车船税过期日期差(天)', -0.03694367077131783),
('载客/人', -0.03234487106605593),
('最小离地间隙(mm)', 0.027822359248097703),
('座位数', 0.016036623933084658)]
绘制热力图:
代码语言:javascript复制price_corr_cols = [ r[0] for r in price_corr ]
price_data = test_data_corr[price_corr_cols].loc[price_corr_cols]
plt.figure(figsize=(15, 10))
plt.title("相关系数热力图")
ax = sns.heatmap(price_data, linewidths=0.5, cmap='OrRd', cbar=True)
plt.show()
为了验证相关系数分析出来的重要特征是否对模型有效,我们将模型效果最好的随机森林模型的前33个重要特征输出。
代码语言:javascript复制feature_important = sorted(
zip(x_train.columns, map(lambda x:round(x,4), model_rfr.feature_importances_)),
key=lambda x: x[1],reverse=True)
for i in range(33):
print(feature_important[i])
输出结果:
代码语言:javascript复制('新车售价', 0.5435)
('最大马力(Ps)', 0.0928)
('注册日期差(天)', 0.077)
('最大扭矩(N·m)', 0.0547)
('最大功率(kW)', 0.05)
('行驶里程', 0.0406)
('出厂日期差(天)', 0.0187)
('高度(mm)', 0.0128)
('驻车制动类型_手刹', 0.0107)
('最大功率转速(rpm)', 0.0098)
('宽度(mm)', 0.007)
('轴距(mm)', 0.0059)
('工信部综合油耗(L/100km)', 0.0052)
('最大扭矩转速(rpm)', 0.0043)
('长度(mm)', 0.0042)
('助力类型_电动助力', 0.0041)
('后轮距(mm)', 0.0038)
('整备质量(kg)', 0.0038)
('挡位个数_5', 0.0035)
('行李厢容积(L)', 0.0027)
('最高车速(km/h)', 0.0026)
('官方0-100km/h加速(s)', 0.0026)
('油箱容积(L)', 0.0026)
('前轮距(mm)', 0.0024)
('压缩比', 0.0023)
('排量(mL)', 0.0021)
('排量(L)', 0.0019)
('上坡辅助', 0.0015)
('日间行车灯', 0.0013)
('商业险过期日期差(天)', 0.0013)
('最小离地间隙(mm)', 0.0012)
('挡位个数_6', 0.0012)
('交强险过期日期差(天)', 0.0011)
将相关系数分析出来的重要特征集合与随机森林的前33个重要特征取交集,并输出有多少个重复的特征。
代码语言:javascript复制f1_list = []
f2_list = []
for i in range(33):
f1_list.append(feature_important[i][0])
for i in range(1, 34):
f2_list.append(price_corr[i][0])
cnt = 0
for i in range(33):
if f1_list[i] in f2_list:
print(f1_list[i])
cnt = 1
print("共有" str(cnt) "个重复特征!")
输出结果:
代码语言:javascript复制新车售价
最大马力(Ps)
注册日期差(天)
最大扭矩(N·m)
最大功率(kW)
行驶里程
出厂日期差(天)
高度(mm)
最大功率转速(rpm)
宽度(mm)
轴距(mm)
工信部综合油耗(L/100km)
最大扭矩转速(rpm)
长度(mm)
后轮距(mm)
整备质量(kg)
行李厢容积(L)
最高车速(km/h)
官方0-100km/h加速(s)
油箱容积(L)
前轮距(mm)
压缩比
排量(mL)
排量(L)
商业险过期日期差(天)
最小离地间隙(mm)
交强险过期日期差(天)
共有27个重复特征!
结果表明,相关系数分析出来的重要特征对模型的效果是有效的。数据分析的魅力就在于通过统计学习的方法,能在模型训练前得到有价值的信息,甚至能挖掘出意想不到的结论。
在我看来,模型预测结果的好坏,最重要的是数据预处理,其次是特征工程和特征筛选,而模型及调参只是找到一个无限接近正确答案的工具。
03
结语
数据分析、数据挖掘以及模型训练还有很多没有学到的,我也只是将我个人学习理解到的东西展现出来,为朋友们抛砖引玉,希望朋友们在我的baseline方法中,创造更完美的答案。
· END ·
最近是懒吉吉