时间序列: 按时间发生的先后顺序进行排序的数据。
通常,时间序列存在时间间隔(每隔固定的时间,记录和收集一次信息),时间间隔也称为时间颗粒度,比如 小时,天,周等。
时序预测(time series forecasting):我们想要根据过去和现在已有的数据对未来某时间点或者时间段的某物理量(观测值)进行预测。 而我们知道,某个物理量会受到一系列物理量的影响(外部变量:external regressor)。
比如,在零售中,我们想要预测未来四周内某产品的销售情况,我们就需要过去一段时间的销售历史以及影响销量的相关变量(天气,节日,温度等等),制定 生产,订货和定价。
工厂:根据订单量的估计,制定招聘和排版。
基金:根据股价进行预估,决定股票的买卖。
IT运维:根据并发量的估计,决定服务器的数量和值班人数。
因此,时间序列并不满足 马尔可夫性质(根据当前的状态就能得到预测的结果:因为 我们不可能收集到所有影响观测值的状态;同时当前的观测值会受到历史数据的影响)。我们在对未来的观测量进行预测时,输入变量不仅要包括未来外部变量的状态,也要包括过去一段时间间隔的历史数据(包括外部变量和观测量)。
另外需要结合业务需求来进行问题分析: 对时间序列规律进行分析,对模型进行选型,误差分析,模型优化。
特点 | 传统统计学算法 | 机器学习算法 | 深度学习算法 |
---|---|---|---|
对问题的理解(不同外部变量的自相关性,稳定性,周期) | 需要 | 不需要 | 不需要 |
能够刻画外部变量对于观测值的非线性关系 | 不可以 | 根据算法(线性,非线性算法) | 可以 |
数据量的要求 | 低 | 中等 | 很多 |
特征工程 难度 | 低 | 取决于算法 | 低 |
是否有可解释性 | 是 | 根据算法 | 无可解释性 |
股价预测 | 销量预测 | |
---|---|---|
场景 | 根据某公司明天的股价走势,决定目前是否买入/卖出某公司的股票 | 根据某店铺每周根据未来1-4周的销量,决定每个产品的备货情况 |
预测对象(观测值) | 股价 | 产品销量 |
结果产生的频率(每过多久,进行一次预测) | 每天 | 每周 |
预测的周期 (预测的观测对象的未来时间长度) | 未来一天 | 未来4周 |
时间颗粒度(收集数据的频率) | 每天收集数据 | 每周收集数据 |
截面颗粒度(预测的观测值对应的对象) | 某公司 | 某店铺的某产品 |
准确性标准 | MAPE(mean average percentage error) | MAPE |
2.2 预测值的周期性规律(历史规律是否会在未来重复出现,以及出现呢的周期性规律唱长度)
2.3 我们需要的数据量多少。一方面取决于 预测值的周期长度,另一方面也取决于算法的类型。(数据越多越好)
2.4 使用最简单的移动平均法来预测效果
移动平均法 主要是根据 预测值在过去一段时间内的数据 来对未来的数据进行预测和估计。 也就是说 移动平均法 假设 外部变量是长期不变的,适用于 不快速增长/减少的变量的预测,主要用于消除 随机变量对于 预测值的影响,得到预测值的趋势变化。如果预测值有周期性/季节性的因素影响,移动平均法在对未来数据预测时,需要考虑超过一个周期的数据,用于消除 周期/季节的影响。
移动平均法分为 简单平均法和加权平均法:
简单平均法: A t + 1 = A t + A t − 1 + ⋯ + A t − N + 1 N A_{t+1}=\frac{A_{t}+A_{t-1}+\dots+A_{t-N+1}}{N} At+1=NAt+At−1+⋯+At−N+1
加权平均法: A t + 1 = w 1 A t + w 2 A t − 1 + ⋯ + w N A t − N + 1 A_{t+1}=w_1A_{t}+w_2 A_{t-1}+\dots+w_{N} A_{t-N+1} At+1=w1At+w2At−1+⋯+wNAt−N+1,满足 w 1 + w 2 + ⋯ + w N = 1 w_1+w_2+\dots+w_{N}=1 w1+w2+⋯+wN=1 一般来说,时间上距离 t + 1 t+1 t+1较近的预测值权重较大;另外权重值也取决于周期的影响: 对于周期性的预测值,那么权重值也是周期性的。
收集数据
历史的 观测值 与 外部变量数据
未来的 外部变量数据
对数据进行探索性分析(数据处理和特征工程)
4.1 数据的范围
4.2 数据中是否存在 异常值和缺失值,clip, 是否满足 高斯分布等等
4.3 预测对象的趋势和周期性,自相关性系数等等
4.4 外部变量对于 观测值的 如何影响(正相关,负相关的程度),是否是多个外部变量耦合 对 观测值进行影响。
算法选择
5.1 是否具有 可解释性(是否要求可解释性)(同时,如果没有可解释性,我们如何在建模过程中对于算法好坏与算法建模正确与否 进行评估)
5.2 算法能够达到精度要求,运算性能要求
5.3 考虑鲁棒性,是否能够后续算法的改进和参数优化
建模和测试
6.1 常用符号
t t t: 时间点
y t y_t yt:t时刻的观测值
x t x_t xt:t时刻的外部变量, x t x_t xt是一个向量,长度表示了外部变量的个数
T T T:基于多久的历史数据来对未来进行预测
h h h: 预测未来多少个时间点的观测值
6.2 模型输入
输入: 过去T个时刻的 观测值与外部变量,未来h个时间点的外部变量(对于问题理解越透彻,外部变量选择越好,最后的预测效果更好)
[ y 1 , x 1 y 2 , x 2 , … , y T , x T , x T + 1 , … , x T + h ] [y_1,x_1y_2,x_2,\dots,y_T,x_T,x_{T+1},\dots,x_{T+h}] [y1,x1y2,x2,…,yT,xT,xT+1,…,xT+h]
输出: 未来h个时间点的观测值
[ y T , y T + 1 , … , y T + h ] [y_T,y_{T+1},\dots,y_{T+h}] [yT,yT+1,…,yT+h]
使用滑动窗口 讲过去的数据 喂给 模型,让模型进行学习。
6.3 划分 训练集,验证集,测试集
时间预测和传统监督学习数据集划分不同。
传统监督学习 的不同数据采集 都是独立同分布的,也就是 不同时间段采集的数据都是 同分布的,不存在时间关系。 此时数据集划分时 随机划分就可以。
但是对于时间预测模型来说,数据本身是存在先后顺序的,而我们期望的泛化能力是 对未来(而不是过去的数据)进行预测。 因此,我们在测试数据时,我们需要测试的 模型对于未来数据的预测能力,所以划分数据时,不能随即划分数据。 而应该讲 最开始的部分划分为训练集,将最新数据部分划分为测试集。 从而考察 对于过去数据集学习,然后对于未来数据进行预测的泛化能力考察。
PS: 划分 训练集与测试集的细节: 我们使用滑动窗口来对数据进行获取。 而输出h如果长度不是1,那么直接形成的数据中 训练集和测试集的输出 会存在 重叠部分,但这是不可以的。 因此我们在 划分训练集和测试集时,会剔除掉 h-1组数据。
我们进行多个算法/多组超参数选择时,就需要划分成 训练集,验证集和测试集。 这三组数据的输出是不能有交集的。而且 数据的时间先后顺序也是按照: 训练集,验证机,测试集。
数据: stock_price.csv, store_sales.csv
import pandas as pd
df_price = pd.read_csv("stock_price.csv")
df_sales = pd.read_csv("store_sales.csv")
# 数据量大小, 数据的特征数量,特征的类型
print( df_price.info() )
print( df_sales.info() )
>>>
# stock prices
RangeIndex: 8398 entries, 0 to 8397
Data columns (total 3 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 date 8398 non-null object
1 company 8398 non-null object
2 price 8398 non-null float64
dtypes: float64(1), object(2)
memory usage: 197.0+ KB
###############
# store sales
RangeIndex: 421570 entries, 0 to 421569
Data columns (total 4 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 store 421570 non-null int64
1 dept 421570 non-null int64
2 week 421570 non-null object
3 sales 421570 non-null float64
dtypes: float64(1), int64(2), object(1)
memory usage: 12.9+ MB
股票有 8398个数据,有特征 data,company两个特征,输出price,并且没有缺失值。 data和company 是 object类型(具体类型是什么,后续查看),输出price是float64类型。
商店销售量 有 421570个数据, 有 store, dept, week三个特征,输出sales,没有缺失值。 store,dept是Int64类型,week是object格式,sales是 float64类型。
# 查看数据
print( df_price.head(2) )
print( df_sales.head(2) )
>>>
date company price
0 2012-05-18 FB 38.230000
1 2012-05-21 FB 34.029999
################
store dept week sales
0 1 1 2010-02-01 24924.50
1 1 1 2010-02-08 46039.49
股票数据中 data是time string类型, company是 公司名称。
商店销售中 store和dept是 Int类型,表示不同的商店和部门。 week是time string类型。
print(df_price.groupby("company").count() )
print(df_sales.groupby("store").count())
print(df_sales.groupby("dept").count())
>>>
date price
company
ETSY 1758 1758
FB 2488 2488
PINS 749 749
SNAP 1285 1285
TWTR 2118 2118
dept week sales
store
1 10244 10244 10244
2 10238 10238 10238
3 9036 9036 9036
4 10272 10272 10272
5 8999 8999 8999
6 10211 10211 10211
7 9762 9762 9762
8 9895 9895 9895
9 8867 8867 8867
10 10315 10315 10315
11 10062 10062 10062
12 9705 9705 9705
13 10474 10474 10474
14 10040 10040 10040
15 9901 9901 9901
16 9443 9443 9443
17 9864 9864 9864
18 9859 9859 9859
19 10148 10148 10148
20 10214 10214 10214
21 9582 9582 9582
22 9688 9688 9688
23 10050 10050 10050
24 10228 10228 10228
25 9804 9804 9804
26 9854 9854 9854
27 10225 10225 10225
28 10113 10113 10113
29 9455 9455 9455
30 7156 7156 7156
31 10142 10142 10142
32 10202 10202 10202
33 6487 6487 6487
34 10224 10224 10224
35 9528 9528 9528
36 6222 6222 6222
37 7206 7206 7206
38 7362 7362 7362
39 9878 9878 9878
40 10017 10017 10017
41 10088 10088 10088
42 6953 6953 6953
43 6751 6751 6751
44 7169 7169 7169
45 9637 9637 9637
store week sales
dept
1 6435 6435 6435
2 6435 6435 6435
3 6435 6435 6435
4 6435 6435 6435
5 6347 6347 6347
... ... ... ...
95 6435 6435 6435
96 4854 4854 4854
97 6278 6278 6278
98 5836 5836 5836
99 862 862 862
所以我们知道,在股价数据中主键是date和company; 在商店销售量中 store, dept和week是主键。
print(df_price.groupby("date").count())
print(df_sales.groupby("week").count())
>>>
company price
date
2012-05-18 1 1
2012-05-21 1 1
2012-05-22 1 1
2012-05-23 1 1
2012-05-24 1 1
... ... ...
2022-03-31 5 5
2022-04-01 5 5
2022-04-04 5 5
2022-04-05 5 5
2022-04-06 5 5
[2488 rows x 2 columns]
store dept sales
week
2010-02-01 2955 2955 2955
2010-02-08 2956 2956 2956
2010-02-15 2977 2977 2977
2010-02-22 2951 2951 2951
2010-03-01 2944 2944 2944
... ... ... ...
2012-09-24 2962 2962 2962
2012-10-01 2976 2976 2976
2012-10-08 2990 2990 2990
2012-10-15 2950 2950 2950
2012-10-22 2959 2959 2959
[143 rows x 3 columns]
股价时间颗粒度是 天, 商店销售时间颗粒度是周。
股价 时间范围是: 2012-05-18 ~ 2022-04-06
商店销售量 时间范围是: 2010-02-01 ~ 2012-10-22
# 主键重复判断
print( df_price.duplicated(subset=["date","company"]).sum() )
print( df_sales.duplicated(subset=["store","dept","week"]).sum() )
>>>
0
0
主键无重复数据
对于股价数据来说, 除了"date"之外,只有"company"一个主键,一共有5个公司,所以有5条时间序列
# store and dept 时间序列条数
print( df_sales.groupby(["store","dept"]).count() )
>>>
[3331 rows x 2 columns]
所以对于商店销量数据中,一共有 3331条时间序列
# 判断时间序列是否连续:
# 时间上的最大 - 时间上的最小 +1 == 数量: 连续 否则就不连续
## stocks
df_price["date"] = pd.to_datetime( df_price["date"] )
df_sales["week"] = pd.to_datetime( df_sales["week"] )
price_summary = df_price.groupby("company")["date"].agg(["min","max","nunique"]).reset_index()
sales_summary = df_sales.groupby(["store","dept"])["week"].agg(["min","max","nunique"]).reset_index()
price_summary["time_nt_continue"] =(price_summary["max"]-price_summary["min"]).dt.days+1 - price_summary["nunique"]
print( "not continue num in stock: ",price_summary[ price_summary["time_nt_continue"]>0 ].count() )
sales_summary["time_nt_continue"] = (sales_summary["max"]-sales_summary["min"]).dt.days/7+1 - sales_summary["nunique"]
print( "not continue num in sales: ",sales_summary[ sales_summary["time_nt_continue"]>0 ].count() )
>>>
not continue num in stock: company 5
min 5
max 5
nunique 5
time_nt_continue 5
dtype: int64
not continue num in sales: store 605
dept 605
min 605
max 605
nunique 605
time_nt_continue 605
dtype: int64
所以股价中 五条都不连续。 而商品中有605条不连续。