Python 时间序列分析

时间序列简介
时间序列分析是数据分析过程中,尤其是在金融数据分析过程中会经常遇到的。时间序列,就是以时间排序的一组随机变量,例如国家统计局每年或每月定期发布的 GDP 或 CPI 指数;24 小时内某一股票、基金、指数的数值变化等,都是时间序列。

下图截取自雅虎财经网站,它就是纳斯达克指数某一天内数值变化(时间序列)的可视化结果。

image.png

时间序列处理

我们拿到一些时间序列原始数据时,可能会遇到下面的一些情况:

某一段时间缺失,需要填充。
时间序列错位,需要对齐。
数据表 a 和数据表 b 所采用的时间间隔不一致,需要重新采样。
……

面对这些问题,我们就要通过一些处理手段来获得最终想要的数据。本节课程中,我们会继续用到 Pandas 提供的时间序列处理模块,下面先看一些基本的方法和操作。

目前,Pandas 针对时间序列处理的类和方法如下:


image.png

我们按照顺序来看一看这些方法可以做什么。

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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容