时间序列简介
时间序列分析是数据分析过程中,尤其是在金融数据分析过程中会经常遇到的。时间序列,就是以时间排序的一组随机变量,例如国家统计局每年或每月定期发布的 GDP 或 CPI 指数;24 小时内某一股票、基金、指数的数值变化等,都是时间序列。
下图截取自雅虎财经网站,它就是纳斯达克指数某一天内数值变化(时间序列)的可视化结果。
时间序列处理
我们拿到一些时间序列原始数据时,可能会遇到下面的一些情况:
某一段时间缺失,需要填充。
时间序列错位,需要对齐。
数据表 a 和数据表 b 所采用的时间间隔不一致,需要重新采样。
……
面对这些问题,我们就要通过一些处理手段来获得最终想要的数据。本节课程中,我们会继续用到 Pandas 提供的时间序列处理模块,下面先看一些基本的方法和操作。
目前,Pandas 针对时间序列处理的类和方法如下:
我们按照顺序来看一看这些方法可以做什么。
Timestamp 时间戳
时间戳,即代表一个时间时刻。我们可以直接用 pd.Timestamp()来创建时间戳。我们使用 ipython 演示,在重点中通过 anaconda/bin/ipython 打开。(小提示:使用 ipython 时,可以通过 Tab 键完成代码自动补全。)
In [1]: import pandas as pd
In [2]: pd.Timestamp("2017-1-1")
Out[2]: Timestamp('2017-01-01 00:00:00')
In [3]: pd.Timestamp(2017,10,1)
Out[3]: Timestamp('2017-10-01 00:00:00')
In [4]: pd.Timestamp("2017-1-1 12:59:59")
Out[4]: Timestamp('2017-01-01 12:59:59')
时间戳索引
我们可以看到,单个时间戳为 Timestamp 数据,而时间戳以列表形式存在时,Pandas 将强制转换为 DatetimeIndex。此时,我们就不能再使用 pd.Timestamp()来创建时间戳了,而是 pd.to_datetime()来创建:
In [6]: pd.to_datetime(["2017-1-1","2017-1-2","2017-1-3"])
Out[6]: DatetimeIndex(['2017-01-01', '2017-01-02', '2017-01-03'], dtype='datetime64[ns]', freq=None)
注意输出部分和上面的区别。
pd.to_datetime() 不仅仅可用来创建 DatetimeIndex,它还可以将对时间戳序列格式进行转换等操作。例如下面,常见的时间戳书写样式,都可以通过pd.to_datetime() 规范化。
In [7]: pd.to_datetime(['Jul 1, 2017', '2017-10-10', None])
Out[7]: DatetimeIndex(['2017-07-01', '2017-10-10', 'NaT'], dtype='datetime64[ns]', freq=None)
In [8]: pd.to_datetime(['2017/10/1', '2017.1.31'])
Out[8]: DatetimeIndex(['2017-10-01', '2017-01-31'], dtype='datetime64[ns]', freq=None)
对于欧洲时区普遍采用的书写样式,我们还可以通过 dayfirst=True 参数进行修正:
In [11]: pd.to_datetime('1-10-2017')
Out[11]: Timestamp('2017-01-10 00:00:00')
In [12]: pd.to_datetime('1-10-2017', dayfirst=True)
Out[12]: Timestamp('2017-10-01 00:00:00')
当然,Pandas 所熟悉的 Seris 和 DataFrame 格式的字符串,也可以直接通过 to_datetime 转换:
In [15]: pd.to_datetime(pd.Series(['2017-1-1', '2017-1-2', '2017-1-3']))
Out[15]:
0 2017-01-01
1 2017-01-02
2 2017-01-03
dtype: datetime64[ns]
In [16]: pd.to_datetime(['2017-1-1', '2017-1-2', '2017-1-3'])
In [16]: DatetimeIndex(['2017-01-01', '2017-01-02', '2017-01-03'], dtype='datetime64[ns]', freq=None)
In [17]: pd.to_datetime(pd.DataFrame({'year': [2017, 2017], 'month': [1, 2], 'day': [3, 4], 'hour': [5, 6]}))
Out[17]:
0 2017-01-03 05:00:00
1 2017-02-04 06:00:00
dtype: datetime64[ns]
其中:
pd.to_datetime(Series/DataFrame)返回的是Series。
pd.to_datetime(List)返回的是DatetimeIndex。
如果要转换如上所示的DataFrame,必须存在的列名有year,month,day。另外 hour, minute, second, millisecond, microsecond, nanosecond可选。
当我们在使用pd.to_datetime() 转换数据时,很容易遇到无效数据。有一些任务对无效数据非常苛刻,所以报错让我们找到这些无效数据是不错的方法。当然,也有一些任务不在乎零星的无效数据,这时候就可以选择忽略。
遇到无效数据报错
In [17]: pd.to_datetime(['2017-1-1', 'invalid'], errors='raise')
ValueError: Unknown string format
忽略无效数据
In [18]: pd.to_datetime(['2017-1-1', 'invalid'], errors='ignore')
Out[18]: array(['2017-1-1', 'invalid'], dtype=object)
将无效数据显示为 NaT
In [19]: pd.to_datetime(['2017-1-1', 'invalid'], errors='coerce')
Out[19]: DatetimeIndex(['2017-01-01', 'NaT'], dtype='datetime64[ns]', freq=None)
接下来,我们看一看生成 DatetimeIndex 的另一个重要方法 pandas.data_range。你应该可以从名字看出该方法的作用,我们可以通过指定一个规则,让 pandas.data_range 生成有序的 DatetimeIndex。
pandas.data_range 方法带有的默认参数如下:
pandas.date_range(start=None, end=None, periods=None, freq=’D’, tz=None, normalize=False,
name=None, closed=None, **kwargs)
常用参数的含义如下:
start= :设置起始时间
end=:设置截至时间
periods= :设置时间区间,若 None 则需要设置单独设置起止和截至时间。
freq= :设置间隔周期。
tz=:设置时区。
其中,freq= 参数是非常关键的参数,我们可以设置的周期有:
freq='s': 秒
freq='min' : 分钟
freq='H': 小时
freq='D': 天
freq='w': 周
freq='m': 月
freq='BM': 每个月最后一天
freq='W':每周的星期日
# 从 2017-1-1 到 2017-1-2,以小时间隔
In [21]: pd.date_range('2017-1-1','2017-1-2',freq='H')
Out[21]:
DatetimeIndex(['2017-01-01 00:00:00', '2017-01-01 01:00:00',
'2017-01-01 02:00:00', '2017-01-01 03:00:00',
'2017-01-01 04:00:00', '2017-01-01 05:00:00',
'2017-01-01 06:00:00', '2017-01-01 07:00:00',
'2017-01-01 08:00:00', '2017-01-01 09:00:00',
'2017-01-01 10:00:00', '2017-01-01 11:00:00',
'2017-01-01 12:00:00', '2017-01-01 13:00:00',
'2017-01-01 14:00:00', '2017-01-01 15:00:00',
'2017-01-01 16:00:00', '2017-01-01 17:00:00',
'2017-01-01 18:00:00', '2017-01-01 19:00:00',
'2017-01-01 20:00:00', '2017-01-01 21:00:00',
'2017-01-01 22:00:00', '2017-01-01 23:00:00',
'2017-01-02 00:00:00'],
dtype='datetime64[ns]', freq='H')
# 从 2017-1-1 开始,以 1s 为间隔,向后推 10 次
In [23]: pd.date_range('2017-1-1',periods=10,freq='s')
Out[23]:
DatetimeIndex(['2017-01-01 00:00:00', '2017-01-01 00:00:01',
'2017-01-01 00:00:02', '2017-01-01 00:00:03',
'2017-01-01 00:00:04', '2017-01-01 00:00:05',
'2017-01-01 00:00:06', '2017-01-01 00:00:07',
'2017-01-01 00:00:08', '2017-01-01 00:00:09'],
dtype='datetime64[ns]', freq='S')
# 从 2017-1-1 开始,以 1H20min 为间隔,向后推 10 次
In [24]: pd.date_range('1/1/2017', periods=10, freq='1H20min')
Out[24]:
DatetimeIndex(['2017-01-01 00:00:00', '2017-01-01 01:20:00',
'2017-01-01 02:40:00', '2017-01-01 04:00:00',
'2017-01-01 05:20:00', '2017-01-01 06:40:00',
'2017-01-01 08:00:00', '2017-01-01 09:20:00',
'2017-01-01 10:40:00', '2017-01-01 12:00:00'],
dtype='datetime64[ns]', freq='80T')
除了生成 DatetimeIndex,我们还可以对已有的 DatetimeIndex 进行操作。这些操作包括选择、切片等。类似于对 Series 的操作。
In [31]: a = pd.date_range('2017-1-1',periods=10,freq='1D1H')
In [32]: a
Out[32]:
DatetimeIndex(['2017-01-01 00:00:00', '2017-01-02 01:00:00',
'2017-01-03 02:00:00', '2017-01-04 03:00:00',
'2017-01-05 04:00:00', '2017-01-06 05:00:00',
'2017-01-07 06:00:00', '2017-01-08 07:00:00',
'2017-01-09 08:00:00', '2017-01-10 09:00:00'],
dtype='datetime64[ns]', freq='25H')
# 选取索引为 1 的时间戳
In [33]: a[1]
Out[33]: Timestamp('2017-01-02 01:00:00', freq='25H')
# 对索引从 0 到 4 的时间进行切片
In [34]: a[:5]
Out[34]:
DatetimeIndex(['2017-01-01 00:00:00', '2017-01-02 01:00:00',
'2017-01-03 02:00:00', '2017-01-04 03:00:00',
'2017-01-05 04:00:00'],
dtype='datetime64[ns]', freq='25H')
时序数据检索
DatetimeIndex 之所以称之为时间戳索引,当然是它的主要用途是作为 Series 或者 DataFrame 的索引。下面,我们就随机生成一些数据,然后看一看如果对时间序列数据进行操作。
In [1]: import numpy as np
In [2]: import pandas as pd
# 生成时间索引
In [3]: i = pd.date_range('2017-1-1', periods=20, freq='M')
# 生成随机数据并添加时间作为索引
In [4]: data = pd.Series(np.random.randn(len(i)), index = i)
# 查看数据
In [5]: data
Out[5]:
2017-01-31 -1.233579
2017-02-28 0.494723
2017-03-31 -2.160592
2017-04-30 0.517173
2017-05-31 -1.984762
2017-06-30 0.655989
2017-07-31 0.919411
2017-08-31 0.114805
2017-09-30 -0.080374
2017-10-31 1.360448
2017-11-30 -0.417094
2017-12-31 0.555434
2018-01-31 1.659271
2018-02-28 -0.514907
2018-03-31 0.330979
2018-04-30 -0.707362
2018-05-31 -0.724524
2018-06-30 0.362518
2018-07-31 0.157280
2018-08-31 -0.724665
Freq: M, dtype: float64
上面就生成了一个以时间为所以的 Series 序列。其实,这就回到了对 Pandas 中 Series 和 DataFrame 类型数据操作的问题。下面演示一些操作:
# 检索 2017 年的所有数据
In [12]: data['2017']
Out[12]:
2017-01-31 -1.233579
2017-02-28 0.494723
2017-03-31 -2.160592
2017-04-30 0.517173
2017-05-31 -1.984762
2017-06-30 0.655989
2017-07-31 0.919411
2017-08-31 0.114805
2017-09-30 -0.080374
2017-10-31 1.360448
2017-11-30 -0.417094
2017-12-31 0.555434
Freq: M, dtype: float64
# 检索 2017 年 7 月到 2018 年 3 月之间的所有数据
In [13]: data['2017-07':'2018-03']
Out[13]:
2017-07-31 0.919411
2017-08-31 0.114805
2017-09-30 -0.080374
2017-10-31 1.360448
2017-11-30 -0.417094
2017-12-31 0.555434
2018-01-31 1.659271
2018-02-28 -0.514907
2018-03-31 0.330979
Freq: M, dtype: float64
# 使用 loc 方法检索 2017 年 1 月的所有数据
In [14]: data.loc['2017-01']
Out[14]:
2017-01-31 -1.233579
Freq: M, dtype: float64
# 使用 truncate 方法检索 2017-3-1 到 2018-4-2 期间的数据
In [17]: data.truncate(before='2017-3-1',after='2018-4-2')
Out[17]:
2017-03-31 -2.160592
2017-04-30 0.517173
2017-05-31 -1.984762
2017-06-30 0.655989
2017-07-31 0.919411
2017-08-31 0.114805
2017-09-30 -0.080374
2017-10-31 1.360448
2017-11-30 -0.417094
2017-12-31 0.555434
2018-01-31 1.659271
2018-02-28 -0.514907
2018-03-31 0.330979
Freq: M, dtype: float64
时序数据偏移
对于时序数据的处理,肯定不只是查询和切片这么简单。我们这里可能会用到 Shifting 方法,将时间索引进行整体偏移。
In [1]: import numpy as np
In [2]: import pandas as pd
# 生成时间索引
In [3]: i = pd.date_range('2017-1-1', periods=5, freq='M')
# 生成随机数据并添加时间作为索引
In [4]: data = pd.Series(np.random.randn(len(i)), index = i)
# 查看数据
In [5]: data
Out[5]:
2017-01-31 0.830480
2017-02-28 0.348324
2017-03-31 -0.622078
2017-04-30 -1.192675
2017-05-31 0.441947
Freq: M, dtype: float64
# 将索引向前位移 3 个单位,也就是数据向后位移 3 个单位,缺失数据 Pandas 会用 NaN 自动填充
In [8]: data.shift(3)
Out[8]:
2017-01-31 NaN
2017-02-28 NaN
2017-03-31 NaN
2017-04-30 0.830480
2017-05-31 0.348324
Freq: M, dtype: float64
# 将索引向后位移 3 个单位,也就是数据向前位移 3 个单位
In [9]: data.shift(-3)
Out[9]:
2017-01-31 -1.192675
2017-02-28 0.441947
2017-03-31 NaN
2017-04-30 NaN
2017-05-31 NaN
Freq: M, dtype: float64
# 将索引的时间向后移动 3 天
In [10]: data.shift(3,freq='D')
Out[10]:
2017-02-03 0.830480
2017-03-03 0.348324
2017-04-03 -0.622078
2017-05-03 -1.192675
2017-06-03 0.441947
dtype: float64
时序数据重采样
除了 Shifting 方法,重采样 Resample 也会经常用到。Resample 可以提升或降低一个时间索引序列的频率,大有用处。例如:当时间序列数据量非常大时,我们可以通过低频率采样的方法得到规模较小到时间覆盖依然较为全面的新数据集。另外,对于多个不同频率的数据集需要数据对齐时,重采样可以十分重要的手段。
In [1]: import pandas as pd
In [2]: import numpy as np
In [3]: i = pd.date_range('2017-1-1', periods=20, freq='D')
In [4]: data = pd.Series(np.random.randn(len(i)), index = i)
In [5]: data
Out[5]:
2017-01-01 0.384984
2017-01-02 0.341555
2017-01-03 -0.100246
2017-01-04 -0.660066
2017-01-05 0.007575
2017-01-06 2.402068
2017-01-07 -0.365657
2017-01-08 -0.853025
2017-01-09 0.588139
2017-01-10 0.047322
2017-01-11 0.213384
2017-01-12 1.056038
2017-01-13 -1.588518
2017-01-14 0.076655
2017-01-15 1.467056
2017-01-16 -1.877541
2017-01-17 0.003218
2017-01-18 -0.811914
2017-01-19 0.143571
2017-01-20 0.837088
Freq: D, dtype: float64
# 按照 2 天进行降采样,并对 2 天对应的数据求和作为新数据
In [6]: data.resample('2D').sum()
Out[6]:
2017-01-01 0.726539
2017-01-03 -0.760312
2017-01-05 2.409643
2017-01-07 -1.218682
2017-01-09 0.635461
2017-01-11 1.269422
2017-01-13 -1.511864
2017-01-15 -0.410485
2017-01-17 -0.808696
2017-01-19 0.980658
Freq: 2D, dtype: float64
# 按照 2 天进行降采样,并对 2 天对应的数据求平均值作为新数据
In [7]: data.resample('2D').mean()
Out[7]:
2017-01-01 0.363269
2017-01-03 -0.380156
2017-01-05 1.204821
2017-01-07 -0.609341
2017-01-09 0.317730
2017-01-11 0.634711
2017-01-13 -0.755932
2017-01-15 -0.205243
2017-01-17 -0.404348
2017-01-19 0.490329
Freq: 2D, dtype: float64
# 按照 2 天进行降采样,并选取对应 2 天的最大值作为新数据
In [9]: data.resample('2D').max()
Out[9]:
2017-01-01 0.384984
2017-01-03 -0.100246
2017-01-05 2.402068
2017-01-07 -0.365657
2017-01-09 0.588139
2017-01-11 1.056038
2017-01-13 0.076655
2017-01-15 1.467056
2017-01-17 0.003218
2017-01-19 0.837088
Freq: 2D, dtype: float64
# 按照 2 天进行降采样,并将对应 2 天数据的原值、最大值、最小值、以及临近值列出
In [10]: data.resample('2D').ohlc()
Out[10]:
open high low close
2017-01-01 0.384984 0.384984 0.341555 0.341555
2017-01-03 -0.100246 -0.100246 -0.660066 -0.660066
2017-01-05 0.007575 2.402068 0.007575 2.402068
2017-01-07 -0.365657 -0.365657 -0.853025 -0.853025
2017-01-09 0.588139 0.588139 0.047322 0.047322
2017-01-11 0.213384 1.056038 0.213384 1.056038
2017-01-13 -1.588518 0.076655 -1.588518 0.076655
2017-01-15 1.467056 1.467056 -1.877541 -1.877541
2017-01-17 0.003218 0.003218 -0.811914 -0.811914
2017-01-19 0.143571 0.837088 0.143571 0.837088
采样操作起来非常简单,只是需要注意采样后对新数据不同的处理方法。上面介绍的是降频采样。我们也可以升频采样。
继续沿用上面 data 的示例数据
# 时间频率从天提升到小时,并使用相同的数据对新增加行填充
In [11]: data.resample('H').ffill()
Out[11]:
2017-01-01 00:00:00 0.384984
2017-01-01 01:00:00 0.384984
2017-01-01 02:00:00 0.384984
2017-01-01 03:00:00 0.384984
2017-01-01 04:00:00 0.384984
...
2017-01-19 21:00:00 0.143571
2017-01-19 22:00:00 0.143571
2017-01-19 23:00:00 0.143571
2017-01-20 00:00:00 0.837088
Freq: H, Length: 457, dtype: float64
# 时间频率从天提升到小时,不对新增加行填充
In [12]: data.resample('H').asfreq()
Out[12]:
2017-01-01 00:00:00 0.384984
2017-01-01 01:00:00 NaN
2017-01-01 02:00:00 NaN
...
2017-01-19 23:00:00 NaN
2017-01-20 00:00:00 0.837088
Freq: H, Length: 457, dtype: float64
# 时间频率从天提升到小时,只对新增加前 3 行填充
In [13]: data.resample('H').ffill(limit=3)
Out[13]:
2017-01-01 00:00:00 0.384984
2017-01-01 01:00:00 0.384984
2017-01-01 02:00:00 0.384984
2017-01-01 03:00:00 0.384984
2017-01-01 04:00:00 NaN
2017-01-01 05:00:00 NaN
...
2017-01-19 21:00:00 NaN
2017-01-19 22:00:00 NaN
2017-01-19 23:00:00 NaN
2017-01-20 00:00:00 0.837088
Freq: H, Length: 457, dtype: float64