Markidakis竞赛(又称M竞赛或M-Competitions),是由预测研究员Spyros Makridakis领导的团队组织的一系列公开对于时间序列预测的竞赛,旨在评估和比较不同预测方法的准确性。
每一届均是真实数据,其实验方案可用于测试真实的业务。
M5是最近的一次比赛,目标:预测零售巨头沃尔玛在未来28天的销量,kaggle官网下载数据地址。
我们正在处理42,840个分层时间序列。数据是从美国3个加利福尼亚州(CA),德克萨斯州(TX)和威斯康星州(WI),7个部门的3049种单独产品中获得的。
这里的“分层”表示可以在不同级别上汇总数据:商店级别,部门级别,产品类别级别和州级别。销售信息可以追溯到2011年1月至2016年6月。除了销售数量,我们还提供了有关价格,促销和节假日的相应数据。
注意:大多数时间序列数据都包含零值。
层级关系为 State->Store->Category->Department->item,如上图所示。
The historical data range from 2011-01-29 to 2016-06-19.数据时间范围: 2011-01-29 to 2016-06-19。
RMSSE
探索性数据分析(Exploratory Data Analysis,简称EDA)
- 数据EDA->特征工程->建模训练->预测结果产出->误差分析及模型解释
- 特征工程:选定预测输出策略;常见特征的生成;特征选择;类别型变量编码
- 建模训练:模型选择(如何兼顾效果和速度),AutoML
- 误差分析:Feature importance,shaply value
大致看一下数据的大小,列名、内容、数据类型
# 读入数据
import pandas as pd
train = pd.read_csv('filename.csv')
# 检查数据基本信息
train.shape
train.sample()
train.dtypes
# 确定主键后按照主键去重,判断数据的 Uniqueness
train[train.duplicated(subset=['Store', 'Date'])]
# 检查数据空值,并按照要求填充空值
train.isna().sum()
train.fillna(0, inplace=True)
# 查看数据的数值分布
train.describe()
是一个大的课题,后续为专门梳理一篇博客来介绍
Python有专门的异常值检测库:Python Outlieer Detection(PyOD)
通过调用API的方式,来进行异常值检测。
# 1号店的销量随时间的图像
seclect_store = train[train['Store']==1]
select_store[['Date', 'Sales']].plot(x='Date', y='Sales', title='Store1', figsize=(16, 4))
# 目标值 Sales 分布曲线
for i in ['Sales']:
sns.histplot(data=train[i], kde=True)
5000万行以下的数据,用pandas是完全没有问题的。再大的话,比如8000万*40的数据,则需要spark分布式操作,或者用Downcast方法。
当数据量太大,又只能用pandas处理的时候,需要用一些Downcast手段,去把项目的内存拉小,
对于比较小的数字,比如32,达不到2^8,故没有必要用64位来存储,这就是可以降低内存的手段。根据数字的大小,适度转为int8或者int16,避免浪费。
def downcast(df):
cols = df.dtypes.index.tolist()
types = df.dtypes.values.tolist()
for i,t in enumerate(types):
if 'int' in str(t):
if df[cols[i]].min() > np.iinfo(np.int8).min and df[cols[i]].max() < np.iinfo(np.int8).max:
df[cols[i]] = df[cols[i]].astype(np.int8)
elif df[cols[i]].min() > np.iinfo(np.int16).min and df[cols[i]].max() < np.iinfo(np.int16).max:
df[cols[i]] = df[cols[i]].astype(np.int16)
elif df[cols[i]].min() > np.iinfo(np.int32).min and df[cols[i]].max() < np.iinfo(np.int32).max:
df[cols[i]] = df[cols[i]].astype(np.int32)
else:
df[cols[i]] = df[cols[i]].astype(np.int64)
elif 'float' in str(t):
# float 16不能被pyarrow to parquet 所以全部转成float32
if df[cols[i]].min() > np.finfo(np.float16).min and df[cols[i]].max() < np.finfo(np.float32).max:
df[cols[i]] = df[cols[i]].astype(np.float32)
else:
df[cols[i]] = df[cols[i]].astype(np.float64)
elif t == np.object:
if cols[i] == 'date':
df[cols[i]] = pd.to_datetime(df[cols[i]], format='%Y-%m-%d')
else:
df[cols[i]] = df[cols[i]].astype('category')
return df
df['Year'] = df['Date'].dt.year
df['Month'] = df['Date'].dt.month
df['DayOfWeek'] = df['Date'].dt.dayofweek
df['WeekOfYear'] = df['Date'].dt.weekofyear
prediction(t+1) = model1(obs(t-1), obs(t-2), ..., obs(t-n))
prediction(t+2) = model2(obs(t-2), obs(t-3), ..., obs(t-n))
prediction(t+1) = model(obs(t-1), obs(t-2), ..., obs(t-n))
prediction(t+2) = model(prediction(t+1), obs(t-1), ..., obs(t-n))
如果 model 是一个线性 model,看起来不太会有 bias;但是如果不是,bias 会变的越来越大
prediction(t+1) = model1(obs(t-1), obs(t-2), ..., obs(t-n))
prediction(t+2) = model2(prediction(t+1), obs(t-1), ..., obs(t-n))
为什么会有上述三种策略呢?
prediction(t+1), prediction(t+2) = model(obs(t-1), obs(t-2), ..., obs(t-n))
一个完整的特征工程可以包括但不限于:
- Lag特征(历史销量特征,如’lag1’表示过去一天的销量。)
- MA特征
- 一阶差分特征
- Window 统计值特征
- 预防 leakage
# 历史销量lag特征,在已经按照日期排序填充的数据集上利用pandas shift函数完成
df[‘lag1’] = df.groupby([‘store‘, ‘sku’])['Sales'].shift(1)
# 对lag可以取moving average,
# w_avg = w1*(t-1) + w2*(t-2) + w3*(t-3)+ w4*(t-4)
df[‘ma_1_4’] = df[[‘lag1’,’lag2’,’lag3’,’lag4’]].mul([0.4,0.3,0.2,0.1]).sum(1)
# 一阶差分,可以提取增长率的特征
df[‘diff_1_2’] = df[‘lag1’] - df[‘lag2’]
# 窗口统计值
df['rolling_max'] = df[‘Sales'].rolling(window=4).max()
下图中,对于2月19来说,其lag1就是2月18的销量1.0 。
下图中,Lag1是Sales经过一次shift的结果,Lag2是Sales经过两次shift后的结果,MA2是取Lag1以2为窗口数量的滑动平均(MA2是Lag1和Lag2的平均)
Time-Related Features: 时间相关特征
业务相关特征:比如价格、促销,还会衍生出其它相关特征,比如有个特征代表是否是促销,还可以做对于每一天(假如是促销),其上一个促销日的销量,这也是一种Lag特征,只不过执行的Lag是纯统计的无脑的,而现在的聚合逐渐增加了是否是促销,或者是节假日等。
pandas 对时间有非常多的预制写好的功能非常强大的操作,如果有一列是pandas.Datetime的格式,如下面的Date列,就可以提取到时间相关的年月日,甚至它在哪一周,在哪一年。
下面有pandas的官方文档,在下面你可以看到,甚至可以看到它是不是一个月的开始,是不是一个月的结束,是不是一个季度的开始,是不是一个季度的结束,
# 提取时间相关特征
df['Year'] = df['Date'].dt.year
df['Month'] = df['Date'].dt.month
df['Day'] = df['Date'].dt.day
df['DayOfWeek'] = df['Date'].dt.dayofweek
df['WeekOfYear'] = df['Date'].dt.weekofyear
这些时间特征,在我们实际模型训练的时候,该如何使用?
其实它能够反映出销量的季节性,或者时间相关的一些规律性,比如说某一个商品——冰淇淋,可能夏天它的销量比较高,这就是时间相关特征的重要性。
业务相关特征,比如价格、促销等各种各样的业务相关特征,
之后,比如说有一个特征,我能做比如某一天它是促销的情况下,其上一个促销日的销量情况,也可以构架一个lag特征,这个聚合增加了一个是否是促销这个特征,同理,是节假日的某天,也可以与上一个是节假日的某天做lag,而不是节假日的某天,也可以找上一个不是节假日的某天计算lag特征,
也就是有了业务特征之后,你还可以衍生出来更多的东西。
特征选择的好处:
- 简化模型,使模型更易于理解:去除不相关的特征会降低学习任务的难度。并且可解释性能对模型效果的稳定性有更多的把握
- 改善性能:节省存储和计算开销
- 改善通用性、降低过拟合风险:减轻维数灾难,特征的增多会大大增加模型的搜索空间,大多数模型所需要的训练样本随着特征数量的增加而显著增加。特征的增加虽然能更好地拟合训练数据,但也可能增加方差。(特征增多的时候,模型变得越复杂,需要增加更多的样本,如果不增加样本,就容易过拟合。)
# 去除leak的特征,包括Sales总销售额,Sales自己肯定要去除,因为它也是dataframe的一个列;
# 有些人做了log_Sales也要去除,
# Customers:那天来了多少顾客。它也会反应那天卖了多少东西,当前节点去预测未来的时候,不知道未来会有多少顾客,要去除
# Date:需要处理后,才能放入模型,处理方法:把时间格式按照顺序排列,比如2017.2.18为1,依次增加一天加1,也可以去除
# 目标值,datetime值等不需要的特征
excluded_cols = ['Date', 'Sales','log_Sales', 'Customers','PromoInterval','monthStr']
init_cols = whole_df.columns
features = columns_minus(init_cols, excluded_cols) # list1 - list2
# 去除常数项特征,如open全都是1(代表店是开着的状态)
constant_cols = [col for col in features if whole_df[col].nunique() == 1]
features = columns_minus(features, constant_cols)
# 剩下特征区分numeric & categorical
num_features = whole_df[features].select_dtypes(include=[np.number]).columns.tolist()
cate_features = columns_minus(features, num_features)
# num_features可以去计算correlation,默认是pearson correlation
# 而pandas.DataFrame.corr()只能处理数值类的特征,
corr_matrix = whole_df[num_features].corr().abs()
# 选择左上矩阵,目的为了观察哪些是high correlation的
upper = corr_matrix.where(np.triu(np.ones(corr_matrix.shape), k=1).astype(np.bool)) # np.triu选择矩阵的左上
# 去除相关性高于某个threshold的特征,而不能处理类别或者中文类特征,比如中文的省份和另外一列一一对应了,这时候只有把中文省份转为数值,再进行处理,
high_corr_cols = [col for col in num_features if any(upper[col] >= 0.9)]
说明:
- 如果用pandas自带的求特征相关度的corr(),其只能计算数值类的特征,不能计算类别类的特征,需要先将其转为数值;
# Label-Encoder
from sklearn import preprocessing
label_encoder = preprocessing.LabelEncoder()
# Encode labels in column
for col in cate_features:
train_df[f'{col}_le']= label_encoder.fit_transform(train_df[col])
经过上述的处理,N 条时间序列数据已经转化为标准的表格类结构化数据(Tabular Data),将其分为合适的训练预测数据进行建模。
# 模型考察指标
def rmspe(y_true, y_pred):
return np.sqrt(np.mean((y_pred/y_true-1) ** 2))
def log_rmspe_lgb(y_true, y_pred):
y_true = np.expm1(y_true)
y_pred = np.expm1(y_pred)
return "rmspe", rmspe(y_true, y_pred), False
# 参数
model_params = {
'boosting_type':'gbdt',
'objective': 'rmse',
'num_leaves': 127,
'learning_rate': 0.15,
'n_estimators': 200,
'feature_fraction': 0.8,
'bagging_fraction': 0.8,
'max_bin': 100,
'max_depth':9,
}
# 模型训练
target = 'log_Sales'
m = lgb.LGBMRegressor(**model_params) # 定义你的 LGB
features = num_features + cate_features
m.fit(X=train_df[features], y=train_df[target],
eval_set = [(val_df[features], val_df[target])],
eval_metric=log_rmspe_lgb,
categorical_feature=cate_features,
early_stopping_rounds=15,
verbose=10,
)
# 在测试集上进行预测输出
test_df['log_pred'] = m.predict(test_df[features])
test_df['pred'] = np.expm1(test_df['log_pred'])
cmd中输入 jupyter notebook ,把弹出到word中的链接复制到地址栏,打开即可,代码放在机器学习算法课件
[1] Kaggle知识点:数据分析EDA;