商品销售预测几乎时每个运营部门的必备数据支持项目,无论是大型促销活动还是单品营销都是如此。这个项目就是针对某单品做的订单量预测。
本项目用到的主要技术包括:
主要用到的库包括:pandas、numpy、matplotlib、sklearn、pickle,其中sklearn是数据建模的核心库。
本项目技术重点是设置集成回归算法的不同参数值,通过交叉验证寻找每个参数值的效果,并寻找最优值,得到最优值下的集成回归算法模型。这种参数优化方法需要手动调整选择参数,这种方法的好处在于,可以观察到每个参数的变化趋势。
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import pickle
from sklearn.model_selection import train_test_split
from sklearn.model_selection import cross_val_score
from sklearn.model_selection import GridSearchCV
from sklearn.metrics import accuracy_score
from sklearn.metrics import mean_squared_error as mse
from sklearn.ensemble import GradientBoostingRegressor
df = pd.read_table("products_sales.txt", sep=',')
df.shape
(731, 11)
df.head()
limit_infor | campaign_type | campaign_level | product_level | resource_amount | email_rate | price | discount_rate | hour_resouces | campaign_fee | orders | |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 6 | 0 | 1 | 1 | 0.08 | 140.0 | 0.83 | 93 | 888 | 1981 |
1 | 0 | 0 | 0 | 1 | 1 | 0.10 | 144.0 | 0.75 | 150 | 836 | 986 |
2 | 0 | 1 | 1 | 1 | 1 | 0.12 | 149.0 | 0.84 | 86 | 1330 | 1416 |
3 | 0 | 3 | 1 | 2 | 1 | 0.12 | 141.0 | 0.82 | 95 | 2273 | 2368 |
4 | 0 | 0 | 0 | 1 | 1 | 0.10 | 146.0 | 0.59 | 73 | 1456 | 1529 |
df.info()
RangeIndex: 731 entries, 0 to 730
Data columns (total 11 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 limit_infor 731 non-null int64
1 campaign_type 731 non-null int64
2 campaign_level 731 non-null int64
3 product_level 731 non-null int64
4 resource_amount 731 non-null int64
5 email_rate 731 non-null float64
6 price 729 non-null float64
7 discount_rate 731 non-null float64
8 hour_resouces 731 non-null int64
9 campaign_fee 731 non-null int64
10 orders 731 non-null int64
dtypes: float64(3), int64(8)
memory usage: 62.9 KB
从结果可以看到,price列包含缺失值,缺失了2条。
到这里数据审查就结束了。对于回归来说,涉及的数据审查还有:数据异常值查看、数据量纲差异、特征相关性等。在这个项目中我们要用的梯度提升树是一种基于CART(分类回归树)的集成算法,对上面这些问题能有效应对,因此没有做更多的审查。
根据步骤3得到的结论,需要对price中的缺失值进行处理。考虑到整个样本量比较少,因此做填充处理;而price列是数值型,选择填充为均值。
# 缺失值填充
fill_df = df.fillna(df['price'].mean())
# 分割数据集X和y
X = fill_df.iloc[:, :-1]
y = fill_df.iloc[:, -1]
# 分割训练集和测试集
Xtrain, Xtest, ytrain, ytest = train_test_split(X, y, test_size=0.3, random_state=0)
# GradientBoostingRegressor(
# loss='ls',
# learning_rate=0.1,
# n_estimators=100,
# subsample=1.0,
# criterion='friedman_mse',
# min_samples_split=2,
# min_samples_leaf=1,
# min_weight_fraction_leaf=0.0,
# max_depth=3,
# min_impurity_decrease=0.0,
# min_impurity_split=None,
# init=None,
# random_state=None,
# max_features=None,
# alpha=0.9,
# verbose=0,
# max_leaf_nodes=None,
# warm_start=False,
# presort='deprecated',
# validation_fraction=0.1,
# n_iter_no_change=None,
# tol=0.0001,
# ccp_alpha=0.0,
# )
我们把GradientBoostingRegressor的重要参数分为两类,第一类是Boosting框架的重要参数,第二类是弱学习器即CART回归树的重要参数。
GBDT类库boosting框架参数:
GBDT类库弱学习器参数:
不管任何参数,都用默认的,我们拟合下数据看看:
gbr = GradientBoostingRegressor(random_state=10)
# 回归模型使用交叉验证cross_val_score默认的score是R2。
cross_val_score(gbr, Xtrain, ytrain, cv=5, scoring='r2').mean()
0.9351560634463129
从结果可以看到,拟合还可以,但还有提升的空间,我们下面看看怎么通过调参提高模型的准确率和泛化能力。
R2衡量的是1 - 我们的模型没有捕获到的信息量占真实标签中所带的信息量的比例,所以, R2越接近1越好。
R2可以使用三种方式来调用,一种是直接从metrics中导入r2_score,输入预测值和真实值后打分。第二种是直接从线性回归LinearRegression的接口score来进行调用。第三种是在交叉验证中,输入"r2"来调用.
均方误差MSE衡量预测值和实际值的差异,得到的是每个样本量上的平均误差。
MSE用sklearn专用的模型评估模块metrics里的类mean_squared_error,另一种是调用交叉验证的类cross_val_score并使用里面的scoring参数来设置使用均方误差。
为什么不直接用网格搜索寻找最佳参数呢?
泛化误差 = 偏差^2 + 方差 + 噪声^2
bias:模型中预测值和真实值之间的差异,模型越准确,偏差越低;
var:衡量模型的稳定性,模型越稳定,方差越低。
range_ = range(10, 1010, 50)
rs = []
var = []
ge = []
for i in range_:
reg = GradientBoostingRegressor(n_estimators=i, random_state=0)
cvresult = cross_val_score(reg, Xtrain, ytrain, cv=5)
# 记录1-偏差
rs.append(cvresult.mean())
# 记录方差
var.append(cvresult.var())
# 记录泛化误差的可控部分
ge.append((1-cvresult.mean())**2 + cvresult.var())
# R2最大(偏差最小)时,参数n_estimators的值,对应的方差
print(range_[rs.index(max(rs))], var[rs.index(max(rs))], max(rs))
# 方差最小时,参数n_estimators的值,对应的偏差
print(range_[var.index(min(var))], rs[var.index(min(var))], min(var))
# 偏差最小时,参数n_estimators的值, 对应的偏差、方差
print(range_[ge.index(min(ge))], rs[ge.index(min(ge))], var[ge.index(min(ge))], min(ge))
60 0.002024876765215483 0.936313277476458
10 0.7942980630202685 0.0007085726842073462
60 0.936313277476458 0.002024876765215483 0.006080875391006117
可视化R2(1-偏差)随着树数量的变化趋势。
plt.figure(figsize=(20, 5))
plt.plot(range_, rs, c='red', label='GBR_R2')
plt.legend()
plt.show()
从结果可以看出,参数n_estimators等于60的时候,R2最大,偏差最小,泛化误差最小。但这个范围太大,还需进一步细化学习。
细化学习曲线,找出最佳n_estimators
range_ = range(20, 200, 10)
rs = []
var = []
ge = []
for i in range_:
reg = GradientBoostingRegressor(n_estimators=i, random_state=0)
cvresult = cross_val_score(reg, Xtrain, ytrain, cv=5)
rs.append(cvresult.mean())
var.append(cvresult.var())
ge.append((1-cvresult.mean())**2 + cvresult.var())
print(range_[rs.index(max(rs))], var[rs.index(max(rs))], max(rs))
print(range_[var.index(min(var))], rs[var.index(min(var))], min(var))
print(range_[ge.index(min(ge))], rs[ge.index(min(ge))], var[ge.index(min(ge))], min(ge))
60 0.002024876765215483 0.936313277476458
20 0.9136080277188249 0.0012046961880539126
40 0.9359234165834291 0.0018879348131213929 0.005993743355462156
plt.figure(figsize=(20, 5))
plt.plot(range_, rs, c='red', label='GBR_R2')
plt.legend()
plt.show()
为什么不把泛化误差和R2画在一起呢?因为他们两个的值范围相差太大,如果画到一起,他们两个的变化趋势图就不明显了。
plt.figure(figsize=(20, 5))
plt.plot(range_, ge, c='gray', linestyle='-.', label='GBR_E')
plt.legend()
plt.show()
从以上结果可以看出,
树类是天生过拟合的模型,GBR也是很容易过拟合,看看模型在测试集上的表现怎么样,会不会过拟合,过拟合严重不严重。
cross_val_score(GradientBoostingRegressor(n_estimators=60, random_state=0), Xtest, ytest, cv=5).mean()
0.9744364153165174
测试集上的表现比训练集上的表现更好,没有出现预期的过拟合,说明模型的泛化能力比较好,模型基本是又准又稳。模型的准确率还有提升的空间,再调整下其他参数试试。
range_ = np.linspace(0.001, 1, 50)
cv = []
for i in range_:
cv.append(
cross_val_score(
GradientBoostingRegressor(n_estimators=60, learning_rate=i, random_state=0), Xtrain, ytrain, cv=5).mean())
print(max(cv),range_[cv.index(max(cv))])
0.9373087477995744 0.10293877551020408
plt.figure(figsize=(20, 5))
plt.plot(range_, cv, c='red', linestyle='-.', label='learning_rate')
plt.legend()
plt.show()
从结果可以看出,
range_ = np.linspace(0.001, 1, 20)
cv = []
for i in range_:
cv.append(
cross_val_score(
GradientBoostingRegressor(n_estimators=60, subsample=i, random_state=0), Xtrain, ytrain, cv=5).mean())
print(max(cv),range_[cv.index(max(cv))])
0.9377894347708191 0.7371052631578947
plt.figure(figsize=(20, 5))
plt.plot(range_, cv, c='red', linestyle='-.', label='subsample')
plt.legend()
plt.show()
从结果可以看出,
range_ = ['ls', 'lad', 'huber', 'quantile']
rs = []
var = []
ge = []
for i in range_:
reg = GradientBoostingRegressor(n_estimators=60, loss=i, random_state=0)
cvresult = cross_val_score(reg, Xtrain, ytrain, cv=5)
rs.append(cvresult.mean())
var.append(cvresult.var())
ge.append((1-cvresult.mean())**2 + cvresult.var())
pd.DataFrame({'loss':range_, 'r2':rs, 'var':var, 'ge':ge})
loss | r2 | var | ge | |
---|---|---|---|---|
0 | ls | 0.936313 | 0.002025 | 0.006081 |
1 | lad | 0.940946 | 0.001695 | 0.005183 |
2 | huber | 0.940849 | 0.002030 | 0.005529 |
3 | quantile | 0.594232 | 0.006639 | 0.171287 |
从结果可以看出,
range_ = range(1, 20, 1)
rs = []
var = []
ge = []
for i in range_:
reg = GradientBoostingRegressor(n_estimators=60, loss='lad', max_depth=i, random_state=0)
cvresult = cross_val_score(reg, Xtrain, ytrain, cv=5)
rs.append(cvresult.mean())
var.append(cvresult.var())
ge.append((1-cvresult.mean())**2 + cvresult.var())
print(range_[rs.index(max(rs))], var[rs.index(max(rs))], max(rs))
print(range_[var.index(min(var))], rs[var.index(min(var))], min(var))
print(range_[ge.index(min(ge))], rs[ge.index(min(ge))], var[ge.index(min(ge))], min(ge))
3 0.0016954362343966239 0.9409462581444764
1 0.9235824099884041 0.0010230022803099283
3 0.9409462581444764 0.0016954362343966239 0.005182780661535442
plt.figure(figsize=(20, 5))
plt.plot(range_, rs, c='red', label='max_depth')
plt.legend()
plt.show()
plt.figure(figsize=(20, 5))
plt.plot(range_, ge, c='gray', linestyle='-.', label='max_depth')
plt.legend()
plt.show()
从结果可以看出,
range_ = range(2, 20, 1)
rs = []
var = []
ge = []
for i in range_:
reg = GradientBoostingRegressor(n_estimators=60, loss='lad', min_samples_split=i, random_state=0)
cvresult = cross_val_score(reg, Xtrain, ytrain, cv=5)
rs.append(cvresult.mean())
var.append(cvresult.var())
ge.append((1-cvresult.mean())**2 + cvresult.var())
print(range_[rs.index(max(rs))], var[rs.index(max(rs))], max(rs))
print(range_[var.index(min(var))], rs[var.index(min(var))], min(var))
print(range_[ge.index(min(ge))], rs[ge.index(min(ge))], var[ge.index(min(ge))], min(ge))
15 0.0015897736983169672 0.9429893567834393
17 0.9424458978306802 0.0015685688257210165
15 0.9429893567834393 0.0015897736983169672 0.004839987138282945
plt.figure(figsize=(20, 5))
plt.plot(range_, rs, c='red', label='min_samples_split')
plt.legend()
plt.show()
plt.figure(figsize=(20, 5))
plt.plot(range_, ge, c='gray', linestyle='-.', label='min_samples_split')
plt.legend()
plt.show()
从结果可以看出,
range_ = range(1, 20, 1)
rs = []
var = []
ge = []
for i in range_:
reg = GradientBoostingRegressor(n_estimators=60, loss='lad', min_samples_leaf=i, random_state=0)
cvresult = cross_val_score(reg, Xtrain, ytrain, cv=5)
rs.append(cvresult.mean())
var.append(cvresult.var())
ge.append((1-cvresult.mean())**2 + cvresult.var())
print(range_[rs.index(max(rs))], var[rs.index(max(rs))], max(rs))
print(range_[var.index(min(var))], rs[var.index(min(var))], min(var))
print(range_[ge.index(min(ge))], rs[ge.index(min(ge))], var[ge.index(min(ge))], min(ge))
18 0.0016066538059972297 0.943714359964585
16 0.9427300239366675 0.0015682135145183393
18 0.943714359964585 0.0016066538059972297 0.004774727080193545
plt.figure(figsize=(20, 5))
plt.plot(range_, rs, c='red', label='min_samples_leaf')
plt.legend()
plt.show()
plt.figure(figsize=(20, 5))
plt.plot(range_, ge, c='gray', linestyle='-.', label='min_samples_leaf')
plt.legend()
plt.show()
从结果可以看出,
min_samples_leaf对模型的影响比较复杂,当min_samples_leaf等于18时,模型的R2最大,泛化误差最低,模型提升不明显,只提升了0.003,选择不调整min_samples_leaf。
我们选择了7个参数进行调整,最终只选择了2个让模型提升明显的参数:n_estimators 和 loss,最终模型在训练集上的表现和测试集上的表现如下:
reg = GradientBoostingRegressor(n_estimators=60, loss='lad', random_state=0)
cross_val_score(reg, Xtrain, ytrain, cv=5).mean()
0.9409462581444764
cross_val_score(reg, Xtest, ytest, cv=5).mean()
0.9718038505339532
从以上分析可以得出,这个模型的表现还是不错的,具体表现为准确率高、泛化能力强,鲁棒性较好。接下来我们从其他指标来评估模型。
为了更好的评估模型效果,我们使用MSE作为回归模型的评估指标。
reg_ = reg.fit(Xtrain, ytrain)
ypred = reg_.predict(Xtest)
reg_.score(Xtest, ytest)
0.9791569249618568
mse_score = mse(ypred, ytest)
mse_score
70668.93448194841
plt.figure(figsize=(20,5))
plt.plot(np.arange(Xtest.shape[0]), ytest, linestyle='--', color='red', label='true y')
plt.plot(np.arange(Xtest.shape[0]), ypred, linestyle='-', color='black', label='predicted y')
plt.title('best model with mse of {0}'.format(int(mse_score)))
plt.legend()
从图形输出结果来看,测试集的预测值(黑色实线)与真实值(红色虚线)的拟合程度比较高。
业务方给了一个新样本数据,针对新数据做预测如下:
New_X = np.array([[1, 1, 0, 1, 15, 0.5, 177, 0.66, 101, 798]])
print(int(reg_.predict(New_X)))
1567
由此得到预测的订单量为1567.
# 保存模型
pickle.dump(reg_, open("gbdtonsales.dat","wb"))
# #注意,open中我们往往使用w或者r作为读取的模式,但其实w与r只能用于文本文件,当我们希望导入的不是文本文件,而
# 是模型本身的时候,我们使用"wb"和"rb"作为读取的模式。其中wb表示以二进制写入,rb表示以二进制读入
# 导入模型
load_model = pickle.load(open("gbdtonsales.dat","rb"))
print('load model from : gbdtonsales.dat')
load model from : gbdtonsales.dat
# 做预测
print(int(load_model.predict(New_X)))
1567