绝大部分行业场景,尤其是互联网、量化行业,每天都会产生大量的数据。金融领域股票价格随时间的走势;电商行业每日的销售额;旅游行业随着节假日周期变化的机票酒店价格等;我们称这种不同时间收到的,描述一个或多种特征随着时间发生变化的数据,为时间序列数据(Time Series Data)。而时间序列预测做的就是通过多种维度的数据本身内在与时间的关联特性,利用历史的数据预测未来这么一件事情。
时序预测从不同角度看有不同分类:
从实现原理的角度,可以分为传统统计学,机器学习(又分非深度学习和深度学习)。
按预测步长区分,可以分为单步预测和多步预测,简单来说就是一次预测未来一个时间单元还是一次预测未来多个时间单元的区别。
按输入变量区分,可以分为自回归预测和使用协变量进行预测,区别在于维度中是否含有协变量,例如预测未来销售量时,如果只接受时间和历史销售量数据,则是自回归预测,如果可以接受天气、经济指数、政策事件分类等其他相关变量(称为协变量),则称为使用协变量进行预测。
按输出结果区分,可以分为点预测和概率预测,很多模型只提供了点预测而不提供概率预测,点预测模型后再加蒙特卡洛模拟(或其他转化为概率预测的方式)往往不能准确反映模型输出的预测概念,而在大多数场景下,概率预测更贴近事实情况,对于未来的预测本身就应该是一种概率分布。
按目标个数区分,可以分为一元、多元、多重时间序列预测。举例理解,使用历史的销售量预测未来1天的销售量为一元时间序列预测,使用历史的进店人数、销售量、退货量预测未来1天的进店人数、销售量、退货量(预测目标有三个)为多元时间序列预测,使用历史的红烧牛肉面、酸菜牛肉面、海鲜面的销售量预测未来1天的红烧牛肉面、酸菜牛肉面、海鲜面的销售量(预测目标有三种)为多重时间序列预测。 [1]
本文接下来会从实现原理的角度切入,大致介绍一下场景的时间序列预测方法的原理以及具体python实现,大部分深度学习的实现依赖于AWS于2019开源的时间序列建模工具包Gluon Time Series(GluonTS)
ARIMA是一种非常流行的时间序列预测统计方法,它是自回归综合移动平均(Auto-Regressive Integrated Moving Averages)的首字母缩写。ARIMA模型建立在以下假设的基础上: 数据序列是平稳的,这意味着均值和方差不应随时间而变化。 通过对数变换或差分可以使序列平稳。
from gluonts.model.r_forecast import RForecastPredictor
# build model
arima_estimator = RForecastPredictor(freq='1D', prediction_length=args.horizon, method_name="arima")
# Predicting
forecast_df = pd.DataFrame(columns=['id', 'target_start_date', 'point_fcst_value']) # df_pred
for entry_, forecast_ in tqdm(zip(training_data, estimator.predict(training_data))):
id = entry_["id"]
forecast_df = forecast_df.append(
pd.DataFrame({"id": id,
"target_start_date": forecast_.index.map(lambda s: s.strftime('%Y%m%d')),
"point_fcst_value": forecast_.median}))
其实就是一种(三次)指数平滑方法,Holt (1957) 和 Winters (1960) 将Holt方法进行拓展用来捕获季节因素。Holt-Winters季节性方法包括预测方程和三个平滑方程:一个用于水平 ,一个用于趋势 ,另一个用于季节性分量。然后以累加或累乘的方式叠加分量组成预测。
from statsmodels.tsa.holtwinters import ExponentialSmoothing
data = pd.Series(data)
yhat = ExponentialSmoothing(data, seasonal_periods=4, trend='add', seasonal='add').fit(use_boxcox=True)
prophet 算法是基于时间序列分解(同上:seasonal、trend、residual)和机器学习的拟合来做的,它最适用于具有强烈季节性影响和多个季节历史数据的时间序列。Prophet 对缺失数据和趋势变化具有稳健性,并且通常可以很好地处理异常值。
from gluonts.model.prophet import ProphetPredictor
# build model
prophet_estimator = ProphetPredictor(freq='1D',
prediction_length=args.horizon,
prophet_params={'daily_seasonality': True,
'weekly_seasonality': True,
'changepoint_prior_scale': 0.03,
'changepoint_range': 0.5,
'seasonality_mode': 'multiplicative'
}
# Predicting
# same as above
Boosting 是集成学习中非常重要的一类算法,其基本原理是串行生成一系列弱学习器(weak learner),这些弱学习器直接通过组合到一起构成最终的模型。Boosting 算法可以用于解决分类和回归问题,主要的算法包括早期的AdaBoost 和 后续的Gradient Boosting。
而GBDT(Gradient Boosting Decision Tree)是弱学习器使用 CART 回归树的一种 Gradient Boosting,使用决策树作为弱学习器的一个好处是:决策树本身是一种不稳定的学习器(训练数据的一点波动可能给结果带来较大的影响),从统计学的角度单棵决策树的方差比较大。而在集成学习中,弱学习器间方差越大,弱学习器本身泛化性能越好,则集成学习模型的泛化性能就越好。因此使用决策树作为弱学习器通常比使用较稳定的弱学习器(如线性回归等)泛化性能更好。
包括后续演变而出的XGBoost和LightGBM算法等基于GBM的衍生算法,在早年由于泛化性好,训练速度快等优点在Kaggle等比赛中得到广泛使用。
from xgboost import XGBRegressor
from sklearn.model_selection import train_test_split
# 重新分割训练和测试数据
dm_trainDF = dm_allDF[:len(trainDF)]
dm_testDF = dm_allDF[len(trainDF):]
# 去掉id号
train_data = dm_trainDF.drop(['Id'],axis=1).values
train_label = trainDF_label.values
X_test_ids = dm_testDF['Id'].values
X_test = dm_testDF.drop(['Id'],axis=1).values
# 分割训练集和验证集
X_train, X_valid, Y_train, Y_valid = train_test_split(train_data, train_label,test_size=0.2)
xgb = XGBRegressor(n_estimators=500, learning_rate=0.05, min_child_weight=5, max_depth=4)
xgb.fit(X_train,Y_train)
print "Validation:",xgb.score(X_valid,Y_valid)
predict = xgb.predict(X_test)
如RNN、LSTM、DeepAR等
DeepAR 是一个自回归循环神经网络,使用递归神经网络 (RNN) 结合自回归 AR 来预测标量(一维)时间序列。可以冷启动预测,直接学习概率分布的参数,但受限于RNN框架,对于较长时间的周期季节等信息则难以补获。
from gluonts.model.deepar import DeepAREstimator
# build model
deepar_estimator = DeepAREstimator(
freq="1D",
num_layers=num_layers,
num_cells=num_cells,
cell_type=cell_type,
dropout_rate=dropout_rate,
prediction_length=args.horizon,
trainer=Trainer(epochs=80))
# Predicting
# same as above
确定输入的协变量的参数:
use_feat_dynamic_real:是否启用时变连续变量作为协变量,如果我们调用天气信息作为协变量,需要设置为True
use_feat_static_cat:是否采用时不变类别变量作为协变量,这里我们设置为True,因为我们需要提供观测点的信息。对于上文提到的管网和测点分布,如果合理分析,可以提取更多有用的时不变类别信息
use_feat_static_real:是否启用时不变连续变量作为协变量,这里我们设置为False
lags_seq:显式设置lag参数,否则系统自动计算lag
time_features:显式设置时间特征,否则系统自动计算
输入数据的格式相对简单,因为GluonTS对输入数据格式ListDataset要求比较宽松。多重时间序列用list表示,每个时间序列需要指定一个字典:
详见: 数据如琥珀:多重时序高阶算法-DeepAR(供水管网压力预测Baseline)
对deepAR引入的协变量是“动态cat feature”还是 “静态cat feature”区分的讨论:
https://github.com/awslabs/gluon-ts/issues/392
马东什么:temporal fusion transformer
import tqdm
from gluonts.model.deepar import DeepAREstimator
# Building training dataset
train_data_list = []
def _split_times_series(df, data_list, dynamic_real_1, dynamic_cat_1, dynamic_cat_2):
new_df = df.sort_values(by=date_col, ascending=True)
time_series_dict = {
"item_id": new_df[id_col].values[0],
"start": pd.to_datetime(new_df[date_col].min()),
"target": new_df[target_col].values,
"dynamic_real_1": [dynamic_real_1.T], # 主要是这行的差别
"dynamic_cat_1": dynamic_cat_1,
"dynamic_cat_2": dynamic_cat_2,
}
data_list.append(time_series_dict)
tqdm.pandas(desc='progress: ')
train_df.groupby(train_df[id_col]).progress_apply(_split_times_series,
data_list=train_data_list,
dynamic_real_1=camp_df_real_features_for_train,
dynamic_cat_1=camp_df_cat1_features_for_train,
dynamic_cat_2=camp_df_cat2_features_for_train)
training_data = ListDataset(train_data_list, freq="1D")
# build model
rmv_imputation = RollingMeanValueImputation(10)
estimator = DeepAREstimator(
freq="1D",
num_layers=num_layers,
num_cells=num_cells,
prediction_length=args.horizon,
imputation_method=rmv_imputation,
impute_missing_values=True,
use_feat_dynamic_real=True,
trainer=Trainer(epochs=epochs))
# Predicting
# same as above
https://github.com/aws/amazon-sagemaker-examples/issues/1312
如TCN、WaveNet等
WaveNet是DeepMind于2016年9月提出的一种基于空洞因果卷积式的语音生产模型,后面专家们发现它也可被用于时间序列的预测,他的感受野较大,网络结构可以自动的在不同时间尺度上提取周期信息。
rom gluonts.model.wavenet import WaveNetEstimator
# build model
wavenet_estimator = WaveNetEstimator(
freq="1D",
n_residue=n_residue,
embedding_dimension=embedding_dimension,
act_type=act_type,
n_skip=n_skip,
prediction_length=args.horizon,
trainer=Trainer(epochs=50, learning_rate=learning_rate)
)
# Predicting
# same as above
N-BEATS是Bengio团队于2019年5月提出的用于时间序列预测的深度网络,主要结构包括前向后向残差链接以及不同的模块分解。他的可解释性相较其他深度学习模型会强一些(通过模块分解),但由于其存在复原原序列的过程,因此预测结果的稳定性要差一些。
from gluonts.model.n_beats import NBEATSEstimator
# build model
nbeats_estimator = NBEATSEstimator(
freq="1D",
widths=[64, 64],
num_stacks=2,
num_blocks=[3],
num_block_layers=[10],
stack_types=['T', 'S'],
prediction_length=args.horizon,
trainer=Trainer(epochs=30))
# Predicting
# same as above
如Transformer、Informer、TFT等
Transformer是Google 在 2017 年提出的一种 用于NLP任务的模型,可以用来做时间序列预测。它可以关注到序列的长期依赖信息,且支持并行化计算,但其本身没有序列的概念,需要增加position embedding是网络学习到序列位置信息。
from gluonts.model.transformer import TransformerEstimator
# build model
transformer_estimator = TransformerEstimator(
freq="1D",
embedding_dimension=15,
model_dim=64,
num_heads=4,
prediction_length=args.horizon,
trainer=Trainer(epochs=50)
)
# Predicting
# same as above
来源鱼arxiv上的一篇文章《Temporal Fusion Transformers for Interpretable Multi-horizon Time Series Forecasting》,用于可解释多元时间序列预测的时间融合变换器。
import tqdm
from gluonts.model.tft import TemporalFusionTransformerEstimator
# Building training dataset
train_data_list = []
def _split_times_series(df, data_list, dynamic_real_1, dynamic_cat_1, dynamic_cat_2):
new_df = df.sort_values(by=date_col, ascending=True)
time_series_dict = {
"item_id": new_df[id_col].values[0],
"start": pd.to_datetime(new_df[date_col].min()),
"target": new_df[target_col].values,
"dynamic_real_1": np.array(dynamic_real_1, ndmin=2),
"dynamic_cat_1": dynamic_cat_1,
"dynamic_cat_2": dynamic_cat_2,
}
data_list.append(time_series_dict)
tqdm.pandas(desc='progress: ')
train_df.groupby(train_df[id_col]).progress_apply(_split_times_series,
data_list=train_data_list,
dynamic_real_1=camp_df_real_features_for_train,
dynamic_cat_1=camp_df_cat1_features_for_train,
dynamic_cat_2=camp_df_cat2_features_for_train)
training_data = ListDataset(train_data_list, freq="1D")
# build model
estimator = TemporalFusionTransformerEstimator(
freq="1D",
prediction_length=args.horizon,
batch_size=batch_size,
context_length=context_length,
dropout_rate=dropout_rate,
trainer=Trainer(batch_size=batch_size, epochs=epochs),
# 数值型协变量dimension引入
dynamic_feature_dims={"dynamic_real_1": 1},
# 非数值型协变量引入
dynamic_cardinalities={"dynamic_cat_1": cat1_cardinality, "dynamic_cat_2": cat2_cardinality},
num_heads=num_heads,
hidden_dim=hidden_dim)
# Predicting
# same as above