时间序列(time series)数据是一种重要的结构化数据形式,应用于多个领域,包括金融学、经济学、生态学、神经科学、物理学等。在多个时间点观察或测量到的任何事物都可以形成一段时间序列。很多时间序列是固定频率的,也就是说,数据点是根据某种规律定期出现的(比如每15秒、每5分钟、每月出现一次)。时间序列也可以是不定期的,没有固定的时间单位或单位之间的偏移量。时间序列数据的意义取决于具体的应用场景,主要有以下几种:

  • 时间戳(timestamp),特定的时刻。
  • 固定时期(period),如2007年1月或2010年全年。
  • 时间间隔(interval),由起始和结束时间戳表示。时期(period)可以被看做间隔(interval)的特例。
  • 实验或过程时间,每个时间点都是相对于特定起始时间的一个度量。例如,从放入烤箱时起,每秒钟饼干的直径。

本章主要讲解前3种时间序列。许多技术都可用于处理实验型时间序列,其索引可能是一个整数或浮点数(表示从实验开始算起已经过去的时间)。最简单也最常见的时间序列都是用时间戳进行索引的。

提示:pandas也支持基于timedeltas的指数,它可以有效代表实验或经过的时间。这本书不涉及timedelta指数,但你可以学习pandas的文档(http://pandas.pydata.org/)。

pandas提供了许多内置的时间序列处理工具和数据算法。因此,你可以高效处理非常大的时间序列,轻松地进行切片/切块、聚合、对定期/不定期的时间序列进行重采样等。有些工具特别适合金融和经济应用,你当然也可以用它们来分析服务器日志数据。

11.1 日期和时间数据类型及工具

Python标准库包含用于日期(date)和时间(time)数据的数据类型,而且还有日历方面的功能。我们主要会用到datetime、time以及calendar模块。datetime.datetime(也可以简写为datetime)是用得最多的数据类型:

  1. In [10]: from datetime import datetime
  2. In [11]: now = datetime.now()
  3. In [12]: now
  4. Out[12]: datetime.datetime(2017, 9, 25, 14, 5, 52, 72973)
  5. In [13]: now.year, now.month, now.day
  6. Out[13]: (2017, 9, 25)

datetime以毫秒形式存储日期和时间。timedelta表示两个datetime对象之间的时间差:

  1. In [14]: delta = datetime(2011, 1, 7) - datetime(2008, 6, 24, 8, 15)
  2. In [15]: delta
  3. Out[15]: datetime.timedelta(926, 56700)
  4. In [16]: delta.days
  5. Out[16]: 926
  6. In [17]: delta.seconds
  7. Out[17]: 56700

可以给datetime对象加上(或减去)一个或多个timedelta,这样会产生一个新对象:

  1. In [18]: from datetime import timedelta
  2. In [19]: start = datetime(2011, 1, 7)
  3. In [20]: start + timedelta(12)
  4. Out[20]: datetime.datetime(2011, 1, 19, 0, 0)
  5. In [21]: start - 2 * timedelta(12)
  6. Out[21]: datetime.datetime(2010, 12, 14, 0, 0)

datetime模块中的数据类型参见表10-1。虽然本章主要讲的是pandas数据类型和高级时间序列处理,但你肯定会在Python的其他地方遇到有关datetime的数据类型。

表11-1 datetime模块中的数据类型

第11章 时间序列 - 图1

tzinfo 存储时区信息的基本类型

字符串和datetime的相互转换

利用str或strftime方法(传入一个格式化字符串),datetime对象和pandas的Timestamp对象(稍后就会介绍)可以被格式化为字符串:

  1. In [22]: stamp = datetime(2011, 1, 3)
  2. In [23]: str(stamp)
  3. Out[23]: '2011-01-03 00:00:00'
  4. In [24]: stamp.strftime('%Y-%m-%d')
  5. Out[24]: '2011-01-03'

表11-2列出了全部的格式化编码。

表11-2 datetime格式定义(兼容ISO C89)

第11章 时间序列 - 图2

第11章 时间序列 - 图3

datetime.strptime可以用这些格式化编码将字符串转换为日期:

  1. In [25]: value = '2011-01-03'
  2. In [26]: datetime.strptime(value, '%Y-%m-%d')
  3. Out[26]: datetime.datetime(2011, 1, 3, 0, 0)
  4. In [27]: datestrs = ['7/6/2011', '8/6/2011']
  5. In [28]: [datetime.strptime(x, '%m/%d/%Y') for x in datestrs]
  6. Out[28]:
  7. [datetime.datetime(2011, 7, 6, 0, 0),
  8. datetime.datetime(2011, 8, 6, 0, 0)]

datetime.strptime是通过已知格式进行日期解析的最佳方式。但是每次都要编写格式定义是很麻烦的事情,尤其是对于一些常见的日期格式。这种情况下,你可以用dateutil这个第三方包中的parser.parse方法(pandas中已经自动安装好了):

  1. In [29]: from dateutil.parser import parse
  2. In [30]: parse('2011-01-03')
  3. Out[30]: datetime.datetime(2011, 1, 3, 0, 0)

dateutil可以解析几乎所有人类能够理解的日期表示形式:

  1. In [31]: parse('Jan 31, 1997 10:45 PM')
  2. Out[31]: datetime.datetime(1997, 1, 31, 22, 45)

在国际通用的格式中,日出现在月的前面很普遍,传入dayfirst=True即可解决这个问题:

  1. In [32]: parse('6/12/2011', dayfirst=True)
  2. Out[32]: datetime.datetime(2011, 12, 6, 0, 0)

pandas通常是用于处理成组日期的,不管这些日期是DataFrame的轴索引还是列。to_datetime方法可以解析多种不同的日期表示形式。对标准日期格式(如ISO8601)的解析非常快:

  1. In [33]: datestrs = ['2011-07-06 12:00:00', '2011-08-06 00:00:00']
  2. In [34]: pd.to_datetime(datestrs)
  3. Out[34]: DatetimeIndex(['2011-07-06 12:00:00', '2011-08-06 00:00:00'], dtype='dat
  4. etime64[ns]', freq=None)

它还可以处理缺失值(None、空字符串等):

  1. In [35]: idx = pd.to_datetime(datestrs + [None])
  2. In [36]: idx
  3. Out[36]: DatetimeIndex(['2011-07-06 12:00:00', '2011-08-06 00:00:00', 'NaT'], dty
  4. pe='datetime64[ns]', freq=None)
  5. In [37]: idx[2]
  6. Out[37]: NaT
  7. In [38]: pd.isnull(idx)
  8. Out[38]: array([False, False, True], dtype=bool)

NaT(Not a Time)是pandas中时间戳数据的null值。

注意:dateutil.parser是一个实用但不完美的工具。比如说,它会把一些原本不是日期的字符串认作是日期(比如”42”会被解析为2042年的今天)。

datetime对象还有一些特定于当前环境(位于不同国家或使用不同语言的系统)的格式化选项。例如,德语或法语系统所用的月份简写就与英语系统所用的不同。表11-3进行了总结。

表11-3 特定于当前环境的日期格式

第11章 时间序列 - 图4

11.2 时间序列基础

pandas最基本的时间序列类型就是以时间戳(通常以Python字符串或datatime对象表示)为索引的Series:

  1. In [39]: from datetime import datetime
  2. In [40]: dates = [datetime(2011, 1, 2), datetime(2011, 1, 5),
  3. ....: datetime(2011, 1, 7), datetime(2011, 1, 8),
  4. ....: datetime(2011, 1, 10), datetime(2011, 1, 12)]
  5. In [41]: ts = pd.Series(np.random.randn(6), index=dates)
  6. In [42]: ts
  7. Out[42]:
  8. 2011-01-02 -0.204708
  9. 2011-01-05 0.478943
  10. 2011-01-07 -0.519439
  11. 2011-01-08 -0.555730
  12. 2011-01-10 1.965781
  13. 2011-01-12 1.393406
  14. dtype: float64

这些datetime对象实际上是被放在一个DatetimeIndex中的:

  1. In [43]: ts.index
  2. Out[43]:
  3. DatetimeIndex(['2011-01-02', '2011-01-05', '2011-01-07', '2011-01-08',
  4. '2011-01-10', '2011-01-12'],
  5. dtype='datetime64[ns]', freq=None)

跟其他Series一样,不同索引的时间序列之间的算术运算会自动按日期对齐:

  1. In [44]: ts + ts[::2]
  2. Out[44]:
  3. 2011-01-02 -0.409415
  4. 2011-01-05 NaN
  5. 2011-01-07 -1.038877
  6. 2011-01-08 NaN
  7. 2011-01-10 3.931561
  8. 2011-01-12 NaN
  9. dtype: float64

ts[::2] 是每隔两个取一个。

pandas用NumPy的datetime64数据类型以纳秒形式存储时间戳:

  1. In [45]: ts.index.dtype
  2. Out[45]: dtype('<M8[ns]')

DatetimeIndex中的各个标量值是pandas的Timestamp对象:

  1. In [46]: stamp = ts.index[0]
  2. In [47]: stamp
  3. Out[47]: Timestamp('2011-01-02 00:00:00')

只要有需要,TimeStamp可以随时自动转换为datetime对象。此外,它还可以存储频率信息(如果有的话),且知道如何执行时区转换以及其他操作。稍后将对此进行详细讲解。

索引、选取、子集构造

当你根据标签索引选取数据时,时间序列和其它的pandas.Series很像:

  1. In [48]: stamp = ts.index[2]
  2. In [49]: ts[stamp]
  3. Out[49]: -0.51943871505673811

还有一种更为方便的用法:传入一个可以被解释为日期的字符串:

  1. In [50]: ts['1/10/2011']
  2. Out[50]: 1.9657805725027142
  3. In [51]: ts['20110110']
  4. Out[51]: 1.9657805725027142

对于较长的时间序列,只需传入“年”或“年月”即可轻松选取数据的切片:

  1. In [52]: longer_ts = pd.Series(np.random.randn(1000),
  2. ....: index=pd.date_range('1/1/2000', periods=1000))
  3. In [53]: longer_ts
  4. Out[53]:
  5. 2000-01-01 0.092908
  6. 2000-01-02 0.281746
  7. 2000-01-03 0.769023
  8. 2000-01-04 1.246435
  9. 2000-01-05 1.007189
  10. 2000-01-06 -1.296221
  11. 2000-01-07 0.274992
  12. 2000-01-08 0.228913
  13. 2000-01-09 1.352917
  14. 2000-01-10 0.886429
  15. ...
  16. 2002-09-17 -0.139298
  17. 2002-09-18 -1.159926
  18. 2002-09-19 0.618965
  19. 2002-09-20 1.373890
  20. 2002-09-21 -0.983505
  21. 2002-09-22 0.930944
  22. 2002-09-23 -0.811676
  23. 2002-09-24 -1.830156
  24. 2002-09-25 -0.138730
  25. 2002-09-26 0.334088
  26. Freq: D, Length: 1000, dtype: float64
  27. In [54]: longer_ts['2001']
  28. Out[54]:
  29. 2001-01-01 1.599534
  30. 2001-01-02 0.474071
  31. 2001-01-03 0.151326
  32. 2001-01-04 -0.542173
  33. 2001-01-05 -0.475496
  34. 2001-01-06 0.106403
  35. 2001-01-07 -1.308228
  36. 2001-01-08 2.173185
  37. 2001-01-09 0.564561
  38. 2001-01-10 -0.190481
  39. ...
  40. 2001-12-22 0.000369
  41. 2001-12-23 0.900885
  42. 2001-12-24 -0.454869
  43. 2001-12-25 -0.864547
  44. 2001-12-26 1.129120
  45. 2001-12-27 0.057874
  46. 2001-12-28 -0.433739
  47. 2001-12-29 0.092698
  48. 2001-12-30 -1.397820
  49. 2001-12-31 1.457823
  50. Freq: D, Length: 365, dtype: float64

这里,字符串“2001”被解释成年,并根据它选取时间区间。指定月也同样奏效:

  1. In [55]: longer_ts['2001-05']
  2. Out[55]:
  3. 2001-05-01 -0.622547
  4. 2001-05-02 0.936289
  5. 2001-05-03 0.750018
  6. 2001-05-04 -0.056715
  7. 2001-05-05 2.300675
  8. 2001-05-06 0.569497
  9. 2001-05-07 1.489410
  10. 2001-05-08 1.264250
  11. 2001-05-09 -0.761837
  12. 2001-05-10 -0.331617
  13. ...
  14. 2001-05-22 0.503699
  15. 2001-05-23 -1.387874
  16. 2001-05-24 0.204851
  17. 2001-05-25 0.603705
  18. 2001-05-26 0.545680
  19. 2001-05-27 0.235477
  20. 2001-05-28 0.111835
  21. 2001-05-29 -1.251504
  22. 2001-05-30 -2.949343
  23. 2001-05-31 0.634634
  24. Freq: D, Length: 31, dtype: float64

datetime对象也可以进行切片:

  1. In [56]: ts[datetime(2011, 1, 7):]
  2. Out[56]:
  3. 2011-01-07 -0.519439
  4. 2011-01-08 -0.555730
  5. 2011-01-10 1.965781
  6. 2011-01-12 1.393406
  7. dtype: float64

由于大部分时间序列数据都是按照时间先后排序的,因此你也可以用不存在于该时间序列中的时间戳对其进行切片(即范围查询):

  1. In [57]: ts
  2. Out[57]:
  3. 2011-01-02 -0.204708
  4. 2011-01-05 0.478943
  5. 2011-01-07 -0.519439
  6. 2011-01-08 -0.555730
  7. 2011-01-10 1.965781
  8. 2011-01-12 1.393406
  9. dtype: float64
  10. In [58]: ts['1/6/2011':'1/11/2011']
  11. Out[58]:
  12. 2011-01-07 -0.519439
  13. 2011-01-08 -0.555730
  14. 2011-01-10 1.965781
  15. dtype: float64

跟之前一样,你可以传入字符串日期、datetime或Timestamp。注意,这样切片所产生的是原时间序列的视图,跟NumPy数组的切片运算是一样的。

这意味着,没有数据被复制,对切片进行修改会反映到原始数据上。

此外,还有一个等价的实例方法也可以截取两个日期之间TimeSeries:

  1. In [59]: ts.truncate(after='1/9/2011')
  2. Out[59]:
  3. 2011-01-02 -0.204708
  4. 2011-01-05 0.478943
  5. 2011-01-07 -0.519439
  6. 2011-01-08 -0.555730
  7. dtype: float64

面这些操作对DataFrame也有效。例如,对DataFrame的行进行索引:

  1. In [60]: dates = pd.date_range('1/1/2000', periods=100, freq='W-WED')
  2. In [61]: long_df = pd.DataFrame(np.random.randn(100, 4),
  3. ....: index=dates,
  4. ....: columns=['Colorado', 'Texas',
  5. ....: 'New York', 'Ohio'])
  6. In [62]: long_df.loc['5-2001']
  7. Out[62]:
  8. Colorado Texas New York Ohio
  9. 2001-05-02 -0.006045 0.490094 -0.277186 -0.707213
  10. 2001-05-09 -0.560107 2.735527 0.927335 1.513906
  11. 2001-05-16 0.538600 1.273768 0.667876 -0.969206
  12. 2001-05-23 1.676091 -0.817649 0.050188 1.951312
  13. 2001-05-30 3.260383 0.963301 1.201206 -1.852001

带有重复索引的时间序列

在某些应用场景中,可能会存在多个观测数据落在同一个时间点上的情况。下面就是一个例子:

  1. In [63]: dates = pd.DatetimeIndex(['1/1/2000', '1/2/2000', '1/2/2000',
  2. ....: '1/2/2000', '1/3/2000'])
  3. In [64]: dup_ts = pd.Series(np.arange(5), index=dates)
  4. In [65]: dup_ts
  5. Out[65]:
  6. 2000-01-01 0
  7. 2000-01-02 1
  8. 2000-01-02 2
  9. 2000-01-02 3
  10. 2000-01-03 4
  11. dtype: int64

通过检查索引的is_unique属性,我们就可以知道它是不是唯一的:

  1. In [66]: dup_ts.index.is_unique
  2. Out[66]: False

对这个时间序列进行索引,要么产生标量值,要么产生切片,具体要看所选的时间点是否重复:

  1. In [67]: dup_ts['1/3/2000'] # not duplicated
  2. Out[67]: 4
  3. In [68]: dup_ts['1/2/2000'] # duplicated
  4. Out[68]:
  5. 2000-01-02 1
  6. 2000-01-02 2
  7. 2000-01-02 3
  8. dtype: int64

假设你想要对具有非唯一时间戳的数据进行聚合。一个办法是使用groupby,并传入level=0:

  1. In [69]: grouped = dup_ts.groupby(level=0)
  2. In [70]: grouped.mean()
  3. Out[70]:
  4. 2000-01-01 0
  5. 2000-01-02 2
  6. 2000-01-03 4
  7. dtype: int64
  8. In [71]: grouped.count()
  9. Out[71]:
  10. 2000-01-01 1
  11. 2000-01-02 3
  12. 2000-01-03 1
  13. dtype: int64

11.3 日期的范围、频率以及移动

pandas中的原生时间序列一般被认为是不规则的,也就是说,它们没有固定的频率。对于大部分应用程序而言,这是无所谓的。但是,它常常需要以某种相对固定的频率进行分析,比如每日、每月、每15分钟等(这样自然会在时间序列中引入缺失值)。幸运的是,pandas有一整套标准时间序列频率以及用于重采样、频率推断、生成固定频率日期范围的工具。例如,我们可以将之前那个时间序列转换为一个具有固定频率(每日)的时间序列,只需调用resample即可:

  1. In [72]: ts
  2. Out[72]:
  3. 2011-01-02 -0.204708
  4. 2011-01-05 0.478943
  5. 2011-01-07 -0.519439
  6. 2011-01-08 -0.555730
  7. 2011-01-10 1.965781
  8. 2011-01-12 1.393406
  9. dtype: float64
  10. In [73]: resampler = ts.resample('D')

字符串“D”是每天的意思。

频率的转换(或重采样)是一个比较大的主题,稍后将专门用一节来进行讨论(11.6小节)。这里,我将告诉你如何使用基本的频率和它的倍数。

生成日期范围

虽然我之前用的时候没有明说,但你可能已经猜到pandas.date_range可用于根据指定的频率生成指定长度的DatetimeIndex:

  1. In [74]: index = pd.date_range('2012-04-01', '2012-06-01')
  2. In [75]: index
  3. Out[75]:
  4. DatetimeIndex(['2012-04-01', '2012-04-02', '2012-04-03', '2012-04-04',
  5. '2012-04-05', '2012-04-06', '2012-04-07', '2012-04-08',
  6. '2012-04-09', '2012-04-10', '2012-04-11', '2012-04-12',
  7. '2012-04-13', '2012-04-14', '2012-04-15', '2012-04-16',
  8. '2012-04-17', '2012-04-18', '2012-04-19', '2012-04-20',
  9. '2012-04-21', '2012-04-22', '2012-04-23', '2012-04-24',
  10. '2012-04-25', '2012-04-26', '2012-04-27', '2012-04-28',
  11. '2012-04-29', '2012-04-30', '2012-05-01', '2012-05-02',
  12. '2012-05-03', '2012-05-04', '2012-05-05', '2012-05-06',
  13. '2012-05-07', '2012-05-08', '2012-05-09', '2012-05-10',
  14. '2012-05-11', '2012-05-12', '2012-05-13', '2012-05-14',
  15. '2012-05-15', '2012-05-16', '2012-05-17', '2012-05-18',
  16. '2012-05-19', '2012-05-20', '2012-05-21', '2012-05-22',
  17. '2012-05-23', '2012-05-24', '2012-05-25', '2012-05-26',
  18. '2012-05-27', '2012-05-28', '2012-05-29', '2012-05-30',
  19. '2012-05-31', '2012-06-01'],
  20. dtype='datetime64[ns]', freq='D')

默认情况下,date_range会产生按天计算的时间点。如果只传入起始或结束日期,那就还得传入一个表示一段时间的数字:

  1. In [76]: pd.date_range(start='2012-04-01', periods=20)
  2. Out[76]:
  3. DatetimeIndex(['2012-04-01', '2012-04-02', '2012-04-03', '2012-04-04',
  4. '2012-04-05', '2012-04-06', '2012-04-07', '2012-04-08',
  5. '2012-04-09', '2012-04-10', '2012-04-11', '2012-04-12',
  6. '2012-04-13', '2012-04-14', '2012-04-15', '2012-04-16',
  7. '2012-04-17', '2012-04-18', '2012-04-19', '2012-04-20'],
  8. dtype='datetime64[ns]', freq='D')
  9. In [77]: pd.date_range(end='2012-06-01', periods=20)
  10. Out[77]:
  11. DatetimeIndex(['2012-05-13', '2012-05-14', '2012-05-15', '2012-05-16',
  12. '2012-05-17', '2012-05-18', '2012-05-19', '2012-05-20',
  13. '2012-05-21', '2012-05-22', '2012-05-23', '2012-05-24',
  14. '2012-05-25', '2012-05-26', '2012-05-27','2012-05-28',
  15. '2012-05-29', '2012-05-30', '2012-05-31', '2012-06-01'],
  16. dtype='datetime64[ns]', freq='D')

起始和结束日期定义了日期索引的严格边界。例如,如果你想要生成一个由每月最后一个工作日组成的日期索引,可以传入”BM”频率(表示business end of month,表11-4是频率列表),这样就只会包含时间间隔内(或刚好在边界上的)符合频率要求的日期:

  1. In [78]: pd.date_range('2000-01-01', '2000-12-01', freq='BM')
  2. Out[78]:
  3. DatetimeIndex(['2000-01-31', '2000-02-29', '2000-03-31', '2000-04-28',
  4. '2000-05-31', '2000-06-30', '2000-07-31', '2000-08-31',
  5. '2000-09-29', '2000-10-31', '2000-11-30'],
  6. dtype='datetime64[ns]', freq='BM')

表11-4 基本的时间序列频率(不完整)

第11章 时间序列 - 图5

第11章 时间序列 - 图6

第11章 时间序列 - 图7

date_range默认会保留起始和结束时间戳的时间信息(如果有的话):

  1. In [79]: pd.date_range('2012-05-02 12:56:31', periods=5)
  2. Out[79]:
  3. DatetimeIndex(['2012-05-02 12:56:31', '2012-05-03 12:56:31',
  4. '2012-05-04 12:56:31', '2012-05-05 12:56:31',
  5. '2012-05-06 12:56:31'],
  6. dtype='datetime64[ns]', freq='D')

有时,虽然起始和结束日期带有时间信息,但你希望产生一组被规范化(normalize)到午夜的时间戳。normalize选项即可实现该功能:

  1. In [80]: pd.date_range('2012-05-02 12:56:31', periods=5, normalize=True)
  2. Out[80]:
  3. DatetimeIndex(['2012-05-02', '2012-05-03', '2012-05-04', '2012-05-05',
  4. '2012-05-06'],
  5. dtype='datetime64[ns]', freq='D')

频率和日期偏移量

pandas中的频率是由一个基础频率(base frequency)和一个乘数组成的。基础频率通常以一个字符串别名表示,比如”M”表示每月,”H”表示每小时。对于每个基础频率,都有一个被称为日期偏移量(date offset)的对象与之对应。例如,按小时计算的频率可以用Hour类表示:

  1. In [81]: from pandas.tseries.offsets import Hour, Minute
  2. In [82]: hour = Hour()
  3. In [83]: hour
  4. Out[83]: <Hour>

传入一个整数即可定义偏移量的倍数:

  1. In [84]: four_hours = Hour(4)
  2. In [85]: four_hours
  3. Out[85]: <4 * Hours>

一般来说,无需明确创建这样的对象,只需使用诸如”H”或”4H”这样的字符串别名即可。在基础频率前面放上一个整数即可创建倍数:

  1. In [86]: pd.date_range('2000-01-01', '2000-01-03 23:59', freq='4h')
  2. Out[86]:
  3. DatetimeIndex(['2000-01-01 00:00:00', '2000-01-01 04:00:00',
  4. '2000-01-01 08:00:00', '2000-01-01 12:00:00',
  5. '2000-01-01 16:00:00', '2000-01-01 20:00:00',
  6. '2000-01-02 00:00:00', '2000-01-02 04:00:00',
  7. '2000-01-02 08:00:00', '2000-01-02 12:00:00',
  8. '2000-01-02 16:00:00', '2000-01-02 20:00:00',
  9. '2000-01-03 00:00:00', '2000-01-03 04:00:00',
  10. '2000-01-03 08:00:00', '2000-01-03 12:00:00',
  11. '2000-01-03 16:00:00', '2000-01-03 20:00:00'],
  12. dtype='datetime64[ns]', freq='4H')

大部分偏移量对象都可通过加法进行连接:

  1. In [87]: Hour(2) + Minute(30)
  2. Out[87]: <150 * Minutes>

同理,你也可以传入频率字符串(如”2h30min”),这种字符串可以被高效地解析为等效的表达式:

  1. In [88]: pd.date_range('2000-01-01', periods=10, freq='1h30min')
  2. Out[88]:
  3. DatetimeIndex(['2000-01-01 00:00:00', '2000-01-01 01:30:00',
  4. '2000-01-01 03:00:00', '2000-01-01 04:30:00',
  5. '2000-01-01 06:00:00', '2000-01-01 07:30:00',
  6. '2000-01-01 09:00:00', '2000-01-01 10:30:00',
  7. '2000-01-01 12:00:00', '2000-01-01 13:30:00'],
  8. dtype='datetime64[ns]', freq='90T')

有些频率所描述的时间点并不是均匀分隔的。例如,”M”(日历月末)和”BM”(每月最后一个工作日)就取决于每月的天数,对于后者,还要考虑月末是不是周末。由于没有更好的术语,我将这些称为锚点偏移量(anchored offset)。

表11-4列出了pandas中的频率代码和日期偏移量类。

笔记:用户可以根据实际需求自定义一些频率类以便提供pandas所没有的日期逻辑,但具体的细节超出了本书的范围。

表11-4 时间序列的基础频率

第11章 时间序列 - 图8

第11章 时间序列 - 图9

第11章 时间序列 - 图10

WOM日期

WOM(Week Of Month)是一种非常实用的频率类,它以WOM开头。它使你能获得诸如“每月第3个星期五”之类的日期:

  1. In [89]: rng = pd.date_range('2012-01-01', '2012-09-01', freq='WOM-3FRI')
  2. In [90]: list(rng)
  3. Out[90]:
  4. [Timestamp('2012-01-20 00:00:00', freq='WOM-3FRI'),
  5. Timestamp('2012-02-17 00:00:00', freq='WOM-3FRI'),
  6. Timestamp('2012-03-16 00:00:00', freq='WOM-3FRI'),
  7. Timestamp('2012-04-20 00:00:00', freq='WOM-3FRI'),
  8. Timestamp('2012-05-18 00:00:00', freq='WOM-3FRI'),
  9. Timestamp('2012-06-15 00:00:00', freq='WOM-3FRI'),
  10. Timestamp('2012-07-20 00:00:00', freq='WOM-3FRI'),
  11. Timestamp('2012-08-17 00:00:00', freq='WOM-3FRI')]

移动(超前和滞后)数据

移动(shifting)指的是沿着时间轴将数据前移或后移。Series和DataFrame都有一个shift方法用于执行单纯的前移或后移操作,保持索引不变:

  1. In [91]: ts = pd.Series(np.random.randn(4),
  2. ....: index=pd.date_range('1/1/2000', periods=4, freq='M'))
  3. In [92]: ts
  4. Out[92]:
  5. 2000-01-31 -0.066748
  6. 2000-02-29 0.838639
  7. 2000-03-31 -0.117388
  8. 2000-04-30 -0.517795
  9. Freq: M, dtype: float64
  10. In [93]: ts.shift(2)
  11. Out[93]:
  12. 2000-01-31 NaN
  13. 2000-02-29 NaN
  14. 2000-03-31 -0.066748
  15. 2000-04-30 0.838639
  16. Freq: M, dtype: float64
  17. In [94]: ts.shift(-2)
  18. Out[94]:
  19. 2000-01-31 -0.117388
  20. 2000-02-29 -0.517795
  21. 2000-03-31 NaN
  22. 2000-04-30 NaN
  23. Freq: M, dtype: float64

当我们这样进行移动时,就会在时间序列的前面或后面产生缺失数据。

shift通常用于计算一个时间序列或多个时间序列(如DataFrame的列)中的百分比变化。可以这样表达:

  1. ts / ts.shift(1) - 1

由于单纯的移位操作不会修改索引,所以部分数据会被丢弃。因此,如果频率已知,则可以将其传给shift以便实现对时间戳进行位移而不是对数据进行简单位移:

  1. In [95]: ts.shift(2, freq='M')
  2. Out[95]:
  3. 2000-03-31 -0.066748
  4. 2000-04-30 0.838639
  5. 2000-05-31 -0.117388
  6. 2000-06-30 -0.517795
  7. Freq: M, dtype: float64

这里还可以使用其他频率,于是你就能非常灵活地对数据进行超前和滞后处理了:

  1. In [96]: ts.shift(3, freq='D')
  2. Out[96]:
  3. 2000-02-03 -0.066748
  4. 2000-03-03 0.838639
  5. 2000-04-03 -0.117388
  6. 2000-05-03 -0.517795
  7. dtype: float64
  8. In [97]: ts.shift(1, freq='90T')
  9. Out[97]:
  10. 2000-01-31 01:30:00 -0.066748
  11. 2000-02-29 01:30:00 0.838639
  12. 2000-03-31 01:30:00 -0.117388
  13. 2000-04-30 01:30:00 -0.517795
  14. Freq: M, dtype: float64

通过偏移量对日期进行位移

pandas的日期偏移量还可以用在datetime或Timestamp对象上:

  1. In [98]: from pandas.tseries.offsets import Day, MonthEnd
  2. In [99]: now = datetime(2011, 11, 17)
  3. In [100]: now + 3 * Day()
  4. Out[100]: Timestamp('2011-11-20 00:00:00')

如果加的是锚点偏移量(比如MonthEnd),第一次增量会将原日期向前滚动到符合频率规则的下一个日期:

  1. In [101]: now + MonthEnd()
  2. Out[101]: Timestamp('2011-11-30 00:00:00')
  3. In [102]: now + MonthEnd(2)
  4. Out[102]: Timestamp('2011-12-31 00:00:00')

通过锚点偏移量的rollforward和rollback方法,可明确地将日期向前或向后“滚动”:

  1. In [103]: offset = MonthEnd()
  2. In [104]: offset.rollforward(now)
  3. Out[104]: Timestamp('2011-11-30 00:00:00')
  4. In [105]: offset.rollback(now)
  5. Out[105]: Timestamp('2011-10-31 00:00:00')

日期偏移量还有一个巧妙的用法,即结合groupby使用这两个“滚动”方法:

  1. In [106]: ts = pd.Series(np.random.randn(20),
  2. .....: index=pd.date_range('1/15/2000', periods=20, freq='4d'))
  3. In [107]: ts
  4. Out[107]:
  5. 2000-01-15 -0.116696
  6. 2000-01-19 2.389645
  7. 2000-01-23 -0.932454
  8. 2000-01-27 -0.229331
  9. 2000-01-31 -1.140330
  10. 2000-02-04 0.439920
  11. 2000-02-08 -0.823758
  12. 2000-02-12 -0.520930
  13. 2000-02-16 0.350282
  14. 2000-02-20 0.204395
  15. 2000-02-24 0.133445
  16. 2000-02-28 0.327905
  17. 2000-03-03 0.072153
  18. 2000-03-07 0.131678
  19. 2000-03-11 -1.297459
  20. 2000-03-15 0.997747
  21. 2000-03-19 0.870955
  22. 2000-03-23 -0.991253
  23. 2000-03-27 0.151699
  24. 2000-03-31 1.266151
  25. Freq: 4D, dtype: float64
  26. In [108]: ts.groupby(offset.rollforward).mean()
  27. Out[108]:
  28. 2000-01-31 -0.005833
  29. 2000-02-29 0.015894
  30. 2000-03-31 0.150209
  31. dtype: float64

当然,更简单、更快速地实现该功能的办法是使用resample(11.6小节将对此进行详细介绍):

  1. In [109]: ts.resample('M').mean()
  2. Out[109]:
  3. 2000-01-31 -0.005833
  4. 2000-02-29 0.015894
  5. 2000-03-31 0.150209
  6. Freq: M, dtype: float64

11.4 时区处理

时间序列处理工作中最让人不爽的就是对时区的处理。许多人都选择以协调世界时(UTC,它是格林尼治标准时间(Greenwich Mean Time)的接替者,目前已经是国际标准了)来处理时间序列。时区是以UTC偏移量的形式表示的。例如,夏令时期间,纽约比UTC慢4小时,而在全年其他时间则比UTC慢5小时。

在Python中,时区信息来自第三方库pytz,它使Python可以使用Olson数据库(汇编了世界时区信息)。这对历史数据非常重要,这是因为由于各地政府的各种突发奇想,夏令时转变日期(甚至UTC偏移量)已经发生过多次改变了。就拿美国来说,DST转变时间自1900年以来就改变过多次!

有关pytz库的更多信息,请查阅其文档。就本书而言,由于pandas包装了pytz的功能,因此你可以不用记忆其API,只要记得时区的名称即可。时区名可以在shell中看到,也可以通过文档查看:

  1. In [110]: import pytz
  2. In [111]: pytz.common_timezones[-5:]
  3. Out[111]: ['US/Eastern', 'US/Hawaii', 'US/Mountain', 'US/Pacific', 'UTC']

要从pytz中获取时区对象,使用pytz.timezone即可:

  1. In [112]: tz = pytz.timezone('America/New_York')
  2. In [113]: tz
  3. Out[113]: <DstTzInfo 'America/New_York' LMT-1 day, 19:04:00 STD>

pandas中的方法既可以接受时区名也可以接受这些对象。

时区本地化和转换

默认情况下,pandas中的时间序列是单纯(naive)的时区。看看下面这个时间序列:

  1. In [114]: rng = pd.date_range('3/9/2012 9:30', periods=6, freq='D')
  2. In [115]: ts = pd.Series(np.random.randn(len(rng)), index=rng)
  3. In [116]: ts
  4. Out[116]:
  5. 2012-03-09 09:30:00 -0.202469
  6. 2012-03-10 09:30:00 0.050718
  7. 2012-03-11 09:30:00 0.639869
  8. 2012-03-12 09:30:00 0.597594
  9. 2012-03-13 09:30:00 -0.797246
  10. 2012-03-14 09:30:00 0.472879
  11. Freq: D, dtype: float64

其索引的tz字段为None:

  1. In [117]: print(ts.index.tz)
  2. None

可以用时区集生成日期范围:

  1. In [118]: pd.date_range('3/9/2012 9:30', periods=10, freq='D', tz='UTC')
  2. Out[118]:
  3. DatetimeIndex(['2012-03-09 09:30:00+00:00', '2012-03-10 09:30:00+00:00',
  4. '2012-03-11 09:30:00+00:00', '2012-03-12 09:30:00+00:00',
  5. '2012-03-13 09:30:00+00:00', '2012-03-14 09:30:00+00:00',
  6. '2012-03-15 09:30:00+00:00', '2012-03-16 09:30:00+00:00',
  7. '2012-03-17 09:30:00+00:00', '2012-03-18 09:30:00+00:00'],
  8. dtype='datetime64[ns, UTC]', freq='D')

从单纯到本地化的转换是通过tz_localize方法处理的:

  1. In [119]: ts
  2. Out[119]:
  3. 2012-03-09 09:30:00 -0.202469
  4. 2012-03-10 09:30:00 0.050718
  5. 2012-03-11 09:30:00 0.639869
  6. 2012-03-12 09:30:00 0.597594
  7. 2012-03-13 09:30:00 -0.797246
  8. 2012-03-14 09:30:00 0.472879
  9. Freq: D, dtype: float64
  10. In [120]: ts_utc = ts.tz_localize('UTC')
  11. In [121]: ts_utc
  12. Out[121]:
  13. 2012-03-09 09:30:00+00:00 -0.202469
  14. 2012-03-10 09:30:00+00:00 0.050718
  15. 2012-03-11 09:30:00+00:00 0.639869
  16. 2012-03-12 09:30:00+00:00 0.597594
  17. 2012-03-13 09:30:00+00:00 -0.797246
  18. 2012-03-14 09:30:00+00:00 0.472879
  19. Freq: D, dtype: float64
  20. In [122]: ts_utc.index
  21. Out[122]:
  22. DatetimeIndex(['2012-03-09 09:30:00+00:00', '2012-03-10 09:30:00+00:00',
  23. '2012-03-11 09:30:00+00:00', '2012-03-12 09:30:00+00:00',
  24. '2012-03-13 09:30:00+00:00', '2012-03-14 09:30:00+00:00'],
  25. dtype='datetime64[ns, UTC]', freq='D')

一旦时间序列被本地化到某个特定时区,就可以用tz_convert将其转换到别的时区了:

  1. In [123]: ts_utc.tz_convert('America/New_York')
  2. Out[123]:
  3. 2012-03-09 04:30:00-05:00 -0.202469
  4. 2012-03-10 04:30:00-05:00 0.050718
  5. 2012-03-11 05:30:00-04:00 0.639869
  6. 2012-03-12 05:30:00-04:00 0.597594
  7. 2012-03-13 05:30:00-04:00 -0.797246
  8. 2012-03-14 05:30:00-04:00 0.472879
  9. Freq: D, dtype: float64

对于上面这种时间序列(它跨越了美国东部时区的夏令时转变期),我们可以将其本地化到EST,然后转换为UTC或柏林时间:

  1. In [124]: ts_eastern = ts.tz_localize('America/New_York')
  2. In [125]: ts_eastern.tz_convert('UTC')
  3. Out[125]:
  4. 2012-03-09 14:30:00+00:00 -0.202469
  5. 2012-03-10 14:30:00+00:00 0.050718
  6. 2012-03-11 13:30:00+00:00 0.639869
  7. 2012-03-12 13:30:00+00:00 0.597594
  8. 2012-03-13 13:30:00+00:00 -0.797246
  9. 2012-03-14 13:30:00+00:00 0.472879
  10. Freq: D, dtype: float64
  11. In [126]: ts_eastern.tz_convert('Europe/Berlin')
  12. Out[126]:
  13. 2012-03-09 15:30:00+01:00 -0.202469
  14. 2012-03-10 15:30:00+01:00 0.050718
  15. 2012-03-11 14:30:00+01:00 0.639869
  16. 2012-03-12 14:30:00+01:00 0.597594
  17. 2012-03-13 14:30:00+01:00 -0.797246
  18. 2012-03-14 14:30:00+01:00 0.472879
  19. Freq: D, dtype: float64

tz_localize和tz_convert也是DatetimeIndex的实例方法:

  1. In [127]: ts.index.tz_localize('Asia/Shanghai')
  2. Out[127]:
  3. DatetimeIndex(['2012-03-09 09:30:00+08:00', '2012-03-10 09:30:00+08:00',
  4. '2012-03-11 09:30:00+08:00', '2012-03-12 09:30:00+08:00',
  5. '2012-03-13 09:30:00+08:00', '2012-03-14 09:30:00+08:00'],
  6. dtype='datetime64[ns, Asia/Shanghai]', freq='D')

注意:对单纯时间戳的本地化操作还会检查夏令时转变期附近容易混淆或不存在的时间。

操作时区意识型Timestamp对象

跟时间序列和日期范围差不多,独立的Timestamp对象也能被从单纯型(naive)本地化为时区意识型(time zone-aware),并从一个时区转换到另一个时区:

  1. In [128]: stamp = pd.Timestamp('2011-03-12 04:00')
  2. In [129]: stamp_utc = stamp.tz_localize('utc')
  3. In [130]: stamp_utc.tz_convert('America/New_York')
  4. Out[130]: Timestamp('2011-03-11 23:00:00-0500', tz='America/New_York')

在创建Timestamp时,还可以传入一个时区信息:

  1. In [131]: stamp_moscow = pd.Timestamp('2011-03-12 04:00', tz='Europe/Moscow')
  2. In [132]: stamp_moscow
  3. Out[132]: Timestamp('2011-03-12 04:00:00+0300', tz='Europe/Moscow')

时区意识型Timestamp对象在内部保存了一个UTC时间戳值(自UNIX纪元(1970年1月1日)算起的纳秒数)。这个UTC值在时区转换过程中是不会发生变化的:

  1. In [133]: stamp_utc.value
  2. Out[133]: 1299902400000000000
  3. In [134]: stamp_utc.tz_convert('America/New_York').value
  4. Out[134]: 1299902400000000000

当使用pandas的DateOffset对象执行时间算术运算时,运算过程会自动关注是否存在夏令时转变期。这里,我们创建了在DST转变之前的时间戳。首先,来看夏令时转变前的30分钟:

  1. In [135]: from pandas.tseries.offsets import Hour
  2. In [136]: stamp = pd.Timestamp('2012-03-12 01:30', tz='US/Eastern')
  3. In [137]: stamp
  4. Out[137]: Timestamp('2012-03-12 01:30:00-0400', tz='US/Eastern')
  5. In [138]: stamp + Hour()
  6. Out[138]: Timestamp('2012-03-12 02:30:00-0400', tz='US/Eastern')

然后,夏令时转变前90分钟:

  1. In [139]: stamp = pd.Timestamp('2012-11-04 00:30', tz='US/Eastern')
  2. In [140]: stamp
  3. Out[140]: Timestamp('2012-11-04 00:30:00-0400', tz='US/Eastern')
  4. In [141]: stamp + 2 * Hour()
  5. Out[141]: Timestamp('2012-11-04 01:30:00-0500', tz='US/Eastern')

不同时区之间的运算

如果两个时间序列的时区不同,在将它们合并到一起时,最终结果就会是UTC。由于时间戳其实是以UTC存储的,所以这是一个很简单的运算,并不需要发生任何转换:

  1. In [142]: rng = pd.date_range('3/7/2012 9:30', periods=10, freq='B')
  2. In [143]: ts = pd.Series(np.random.randn(len(rng)), index=rng)
  3. In [144]: ts
  4. Out[144]:
  5. 2012-03-07 09:30:00 0.522356
  6. 2012-03-08 09:30:00 -0.546348
  7. 2012-03-09 09:30:00 -0.733537
  8. 2012-03-12 09:30:00 1.302736
  9. 2012-03-13 09:30:00 0.022199
  10. 2012-03-14 09:30:00 0.364287
  11. 2012-03-15 09:30:00 -0.922839
  12. 2012-03-16 09:30:00 0.312656
  13. 2012-03-19 09:30:00 -1.128497
  14. 2012-03-20 09:30:00 -0.333488
  15. Freq: B, dtype: float64
  16. In [145]: ts1 = ts[:7].tz_localize('Europe/London')
  17. In [146]: ts2 = ts1[2:].tz_convert('Europe/Moscow')
  18. In [147]: result = ts1 + ts2
  19. In [148]: result.index
  20. Out[148]:
  21. DatetimeIndex(['2012-03-07 09:30:00+00:00', '2012-03-08 09:30:00+00:00',
  22. '2012-03-09 09:30:00+00:00', '2012-03-12 09:30:00+00:00',
  23. '2012-03-13 09:30:00+00:00', '2012-03-14 09:30:00+00:00',
  24. '2012-03-15 09:30:00+00:00'],
  25. dtype='datetime64[ns, UTC]', freq='B')

11.5 时期及其算术运算

时期(period)表示的是时间区间,比如数日、数月、数季、数年等。Period类所表示的就是这种数据类型,其构造函数需要用到一个字符串或整数,以及表11-4中的频率:

  1. In [149]: p = pd.Period(2007, freq='A-DEC')
  2. In [150]: p
  3. Out[150]: Period('2007', 'A-DEC')

这里,这个Period对象表示的是从2007年1月1日到2007年12月31日之间的整段时间。只需对Period对象加上或减去一个整数即可达到根据其频率进行位移的效果:

  1. In [151]: p + 5
  2. Out[151]: Period('2012', 'A-DEC')
  3. In [152]: p - 2
  4. Out[152]: Period('2005', 'A-DEC')

如果两个Period对象拥有相同的频率,则它们的差就是它们之间的单位数量:

  1. In [153]: pd.Period('2014', freq='A-DEC') - p
  2. Out[153]: 7

period_range函数可用于创建规则的时期范围:

  1. In [154]: rng = pd.period_range('2000-01-01', '2000-06-30', freq='M')
  2. In [155]: rng
  3. Out[155]: PeriodIndex(['2000-01', '2000-02', '2000-03', '2000-04', '2000-05', '20
  4. 00-06'], dtype='period[M]', freq='M')

PeriodIndex类保存了一组Period,它可以在任何pandas数据结构中被用作轴索引:

  1. In [156]: pd.Series(np.random.randn(6), index=rng)
  2. Out[156]:
  3. 2000-01 -0.514551
  4. 2000-02 -0.559782
  5. 2000-03 -0.783408
  6. 2000-04 -1.797685
  7. 2000-05 -0.172670
  8. 2000-06 0.680215
  9. Freq: M, dtype: float64

如果你有一个字符串数组,你也可以使用PeriodIndex类:

  1. In [157]: values = ['2001Q3', '2002Q2', '2003Q1']
  2. In [158]: index = pd.PeriodIndex(values, freq='Q-DEC')
  3. In [159]: index
  4. Out[159]: PeriodIndex(['2001Q3', '2002Q2', '2003Q1'], dtype='period[Q-DEC]', freq
  5. ='Q-DEC')

时期的频率转换

Period和PeriodIndex对象都可以通过其asfreq方法被转换成别的频率。假设我们有一个年度时期,希望将其转换为当年年初或年末的一个月度时期。该任务非常简单:

  1. In [160]: p = pd.Period('2007', freq='A-DEC')
  2. In [161]: p
  3. Out[161]: Period('2007', 'A-DEC')
  4. In [162]: p.asfreq('M', how='start')
  5. Out[162]: Period('2007-01', 'M')
  6. In [163]: p.asfreq('M', how='end')
  7. Out[163]: Period('2007-12', 'M')

你可以将Period(‘2007’,’A-DEC’)看做一个被划分为多个月度时期的时间段中的游标。图11-1对此进行了说明。对于一个不以12月结束的财政年度,月度子时期的归属情况就不一样了:

  1. In [164]: p = pd.Period('2007', freq='A-JUN')
  2. In [165]: p
  3. Out[165]: Period('2007', 'A-JUN')
  4. In [166]: p.asfreq('M', 'start')
  5. Out[166]: Period('2006-07', 'M')
  6. In [167]: p.asfreq('M', 'end')
  7. Out[167]: Period('2007-06', 'M')

第11章 时间序列 - 图11

在将高频率转换为低频率时,超时期(superperiod)是由子时期(subperiod)所属的位置决定的。例如,在A-JUN频率中,月份“2007年8月”实际上是属于周期“2008年”的:

  1. In [168]: p = pd.Period('Aug-2007', 'M')
  2. In [169]: p.asfreq('A-JUN')
  3. Out[169]: Period('2008', 'A-JUN')

完整的PeriodIndex或TimeSeries的频率转换方式也是如此:

  1. In [170]: rng = pd.period_range('2006', '2009', freq='A-DEC')
  2. In [171]: ts = pd.Series(np.random.randn(len(rng)), index=rng)
  3. In [172]: ts
  4. Out[172]:
  5. 2006 1.607578
  6. 2007 0.200381
  7. 2008 -0.834068
  8. 2009 -0.302988
  9. Freq: A-DEC, dtype: float64
  10. In [173]: ts.asfreq('M', how='start')
  11. Out[173]:
  12. 2006-01 1.607578
  13. 2007-01 0.200381
  14. 2008-01 -0.834068
  15. 2009-01 -0.302988
  16. Freq: M, dtype: float64

这里,根据年度时期的第一个月,每年的时期被取代为每月的时期。如果我们想要每年的最后一个工作日,我们可以使用“B”频率,并指明想要该时期的末尾:

  1. In [174]: ts.asfreq('B', how='end')
  2. Out[174]:
  3. 2006-12-29 1.607578
  4. 2007-12-31 0.200381
  5. 2008-12-31 -0.834068
  6. 2009-12-31 -0.302988
  7. Freq: B, dtype: float64

按季度计算的时期频率

季度型数据在会计、金融等领域中很常见。许多季度型数据都会涉及“财年末”的概念,通常是一年12个月中某月的最后一个日历日或工作日。就这一点来说,时期”2012Q4”根据财年末的不同会有不同的含义。pandas支持12种可能的季度型频率,即Q-JAN到Q-DEC:

  1. In [175]: p = pd.Period('2012Q4', freq='Q-JAN')
  2. In [176]: p
  3. Out[176]: Period('2012Q4', 'Q-JAN')

在以1月结束的财年中,2012Q4是从11月到1月(将其转换为日型频率就明白了)。图11-2对此进行了说明:

  1. In [177]: p.asfreq('D', 'start')
  2. Out[177]: Period('2011-11-01', 'D')
  3. In [178]: p.asfreq('D', 'end')
  4. Out[178]: Period('2012-01-31', 'D')

第11章 时间序列 - 图12

因此,Period之间的算术运算会非常简单。例如,要获取该季度倒数第二个工作日下午4点的时间戳,你可以这样:

  1. In [179]: p4pm = (p.asfreq('B', 'e') - 1).asfreq('T', 's') + 16 * 60
  2. In [180]: p4pm
  3. Out[180]: Period('2012-01-30 16:00', 'T')
  4. In [181]: p4pm.to_timestamp()
  5. Out[181]: Timestamp('2012-01-30 16:00:00')

period_range可用于生成季度型范围。季度型范围的算术运算也跟上面是一样的:

  1. In [182]: rng = pd.period_range('2011Q3', '2012Q4', freq='Q-JAN')
  2. In [183]: ts = pd.Series(np.arange(len(rng)), index=rng)
  3. In [184]: ts
  4. Out[184]:
  5. 2011Q3 0
  6. 2011Q4 1
  7. 2012Q1 2
  8. 2012Q2 3
  9. 2012Q3 4
  10. 2012Q4 5
  11. Freq: Q-JAN, dtype: int64
  12. In [185]: new_rng = (rng.asfreq('B', 'e') - 1).asfreq('T', 's') + 16 * 60
  13. In [186]: ts.index = new_rng.to_timestamp()
  14. In [187]: ts
  15. Out[187]:
  16. 2010-10-28 16:00:00 0
  17. 2011-01-28 16:00:00 1
  18. 2011-04-28 16:00:00 2
  19. 2011-07-28 16:00:00 3
  20. 2011-10-28 16:00:00 4
  21. 2012-01-30 16:00:00 5
  22. dtype: int64

将Timestamp转换为Period(及其反向过程)

通过使用to_period方法,可以将由时间戳索引的Series和DataFrame对象转换为以时期索引:

  1. In [188]: rng = pd.date_range('2000-01-01', periods=3, freq='M')
  2. In [189]: ts = pd.Series(np.random.randn(3), index=rng)
  3. In [190]: ts
  4. Out[190]:
  5. 2000-01-31 1.663261
  6. 2000-02-29 -0.996206
  7. 2000-03-31 1.521760
  8. Freq: M, dtype: float64
  9. In [191]: pts = ts.to_period()
  10. In [192]: pts
  11. Out[192]:
  12. 2000-01 1.663261
  13. 2000-02 -0.996206
  14. 2000-03 1.521760
  15. Freq: M, dtype: float64

由于时期指的是非重叠时间区间,因此对于给定的频率,一个时间戳只能属于一个时期。新PeriodIndex的频率默认是从时间戳推断而来的,你也可以指定任何别的频率。结果中允许存在重复时期:

  1. In [193]: rng = pd.date_range('1/29/2000', periods=6, freq='D')
  2. In [194]: ts2 = pd.Series(np.random.randn(6), index=rng)
  3. In [195]: ts2
  4. Out[195]:
  5. 2000-01-29 0.244175
  6. 2000-01-30 0.423331
  7. 2000-01-31 -0.654040
  8. 2000-02-01 2.089154
  9. 2000-02-02 -0.060220
  10. 2000-02-03 -0.167933
  11. Freq: D, dtype: float64
  12. In [196]: ts2.to_period('M')
  13. Out[196]:
  14. 2000-01 0.244175
  15. 2000-01 0.423331
  16. 2000-01 -0.654040
  17. 2000-02 2.089154
  18. 2000-02 -0.060220
  19. 2000-02 -0.167933
  20. Freq: M, dtype: float64

要转换回时间戳,使用to_timestamp即可:

  1. In [197]: pts = ts2.to_period()
  2. In [198]: pts
  3. Out[198]:
  4. 2000-01-29 0.244175
  5. 2000-01-30 0.423331
  6. 2000-01-31 -0.654040
  7. 2000-02-01 2.089154
  8. 2000-02-02 -0.060220
  9. 2000-02-03 -0.167933
  10. Freq: D, dtype: float64
  11. In [199]: pts.to_timestamp(how='end')
  12. Out[199]:
  13. 2000-01-29 0.244175
  14. 2000-01-30 0.423331
  15. 2000-01-31 -0.654040
  16. 2000-02-01 2.089154
  17. 2000-02-02 -0.060220
  18. 2000-02-03 -0.167933
  19. Freq: D, dtype: float64

通过数组创建PeriodIndex

固定频率的数据集通常会将时间信息分开存放在多个列中。例如,在下面这个宏观经济数据集中,年度和季度就分别存放在不同的列中:

  1. In [200]: data = pd.read_csv('examples/macrodata.csv')
  2. In [201]: data.head(5)
  3. Out[201]:
  4. year quarter realgdp realcons realinv realgovt realdpi cpi \
  5. 0 1959.0 1.0 2710.349 1707.4 286.898 470.045 1886.9 28.98
  6. 1 1959.0 2.0 2778.801 1733.7 310.859 481.301 1919.7 29.15
  7. 2 1959.0 3.0 2775.488 1751.8 289.226 491.260 1916.4 29.35
  8. 3 1959.0 4.0 2785.204 1753.7 299.356 484.052 1931.3 29.37
  9. 4 1960.0 1.0 2847.699 1770.5 331.722 462.199 1955.5 29.54
  10. m1 tbilrate unemp pop infl realint
  11. 0 139.7 2.82 5.8 177.146 0.00 0.00
  12. 1 141.7 3.08 5.1 177.830 2.34 0.74
  13. 2 140.5 3.82 5.3 178.657 2.74 1.09
  14. 3 140.0 4.33 5.6 179.386 0.27 4.06
  15. 4 139.6 3.50 5.2 180.007 2.31 1.19
  16. In [202]: data.year
  17. Out[202]:
  18. 0 1959.0
  19. 1 1959.0
  20. 2 1959.0
  21. 3 1959.0
  22. 4 1960.0
  23. 5 1960.0
  24. 6 1960.0
  25. 7 1960.0
  26. 8 1961.0
  27. 9 1961.0
  28. ...
  29. 193 2007.0
  30. 194 2007.0
  31. 195 2007.0
  32. 196 2008.0
  33. 197 2008.0
  34. 198 2008.0
  35. 199 2008.0
  36. 200 2009.0
  37. 201 2009.0
  38. 202 2009.0
  39. Name: year, Length: 203, dtype: float64
  40. In [203]: data.quarter
  41. Out[203]:
  42. 0 1.0
  43. 1 2.0
  44. 2 3.0
  45. 3 4.0
  46. 4 1.0
  47. 5 2.0
  48. 6 3.0
  49. 7 4.0
  50. 8 1.0
  51. 9 2.0
  52. ...
  53. 193 2.0
  54. 194 3.0
  55. 195 4.0
  56. 196 1.0
  57. 197 2.0
  58. 198 3.0
  59. 199 4.0
  60. 200 1.0
  61. 201 2.0
  62. 202 3.0
  63. Name: quarter, Length: 203, dtype: float64

通过将这些数组以及一个频率传入PeriodIndex,就可以将它们合并成DataFrame的一个索引:

  1. In [204]: index = pd.PeriodIndex(year=data.year, quarter=data.quarter,
  2. .....: freq='Q-DEC')
  3. In [205]: index
  4. Out[205]:
  5. PeriodIndex(['1959Q1', '1959Q2', '1959Q3', '1959Q4', '1960Q1', '1960Q2',
  6. '1960Q3', '1960Q4', '1961Q1', '1961Q2',
  7. ...
  8. '2007Q2', '2007Q3', '2007Q4', '2008Q1', '2008Q2', '2008Q3',
  9. '2008Q4', '2009Q1', '2009Q2', '2009Q3'],
  10. dtype='period[Q-DEC]', length=203, freq='Q-DEC')
  11. In [206]: data.index = index
  12. In [207]: data.infl
  13. Out[207]:
  14. 1959Q1 0.00
  15. 1959Q2 2.34
  16. 1959Q3 2.74
  17. 1959Q4 0.27
  18. 1960Q1 2.31
  19. 1960Q2 0.14
  20. 1960Q3 2.70
  21. 1960Q4 1.21
  22. 1961Q1 -0.40
  23. 1961Q2 1.47
  24. ...
  25. 2007Q2 2.75
  26. 2007Q3 3.45
  27. 2007Q4 6.38
  28. 2008Q1 2.82
  29. 2008Q2 8.53
  30. 2008Q3 -3.16
  31. 2008Q4 -8.79
  32. 2009Q1 0.94
  33. 2009Q2 3.37
  34. 2009Q3 3.56
  35. Freq: Q-DEC, Name: infl, Length: 203, dtype: float64

11.6 重采样及频率转换

重采样(resampling)指的是将时间序列从一个频率转换到另一个频率的处理过程。将高频率数据聚合到低频率称为降采样(downsampling),而将低频率数据转换到高频率则称为升采样(upsampling)。并不是所有的重采样都能被划分到这两个大类中。例如,将W-WED(每周三)转换为W-FRI既不是降采样也不是升采样。

pandas对象都带有一个resample方法,它是各种频率转换工作的主力函数。resample有一个类似于groupby的API,调用resample可以分组数据,然后会调用一个聚合函数:

  1. In [208]: rng = pd.date_range('2000-01-01', periods=100, freq='D')
  2. In [209]: ts = pd.Series(np.random.randn(len(rng)), index=rng)
  3. In [210]: ts
  4. Out[210]:
  5. 2000-01-01 0.631634
  6. 2000-01-02 -1.594313
  7. 2000-01-03 -1.519937
  8. 2000-01-04 1.108752
  9. 2000-01-05 1.255853
  10. 2000-01-06 -0.024330
  11. 2000-01-07 -2.047939
  12. 2000-01-08 -0.272657
  13. 2000-01-09 -1.692615
  14. 2000-01-10 1.423830
  15. ...
  16. 2000-03-31 -0.007852
  17. 2000-04-01 -1.638806
  18. 2000-04-02 1.401227
  19. 2000-04-03 1.758539
  20. 2000-04-04 0.628932
  21. 2000-04-05 -0.423776
  22. 2000-04-06 0.789740
  23. 2000-04-07 0.937568
  24. 2000-04-08 -2.253294
  25. 2000-04-09 -1.772919
  26. Freq: D, Length: 100, dtype: float64
  27. In [211]: ts.resample('M').mean()
  28. Out[211]:
  29. 2000-01-31 -0.165893
  30. 2000-02-29 0.078606
  31. 2000-03-31 0.223811
  32. 2000-04-30 -0.063643
  33. Freq: M, dtype: float64
  34. In [212]: ts.resample('M', kind='period').mean()
  35. Out[212]:
  36. 2000-01 -0.165893
  37. 2000-02 0.078606
  38. 2000-03 0.223811
  39. 2000-04 -0.063643
  40. Freq: M, dtype: float64

resample是一个灵活高效的方法,可用于处理非常大的时间序列。我将通过一系列的示例说明其用法。表11-5总结它的一些选项。

表11-5 resample方法的参数
第11章 时间序列 - 图13

降采样

将数据聚合到规律的低频率是一件非常普通的时间序列处理任务。待聚合的数据不必拥有固定的频率,期望的频率会自动定义聚合的面元边界,这些面元用于将时间序列拆分为多个片段。例如,要转换到月度频率(’M’或’BM’),数据需要被划分到多个单月时间段中。各时间段都是半开放的。一个数据点只能属于一个时间段,所有时间段的并集必须能组成整个时间帧。在用resample对数据进行降采样时,需要考虑两样东西:

  • 各区间哪边是闭合的。
  • 如何标记各个聚合面元,用区间的开头还是末尾。

为了说明,我们来看一些“1分钟”数据:

  1. In [213]: rng = pd.date_range('2000-01-01', periods=12, freq='T')
  2. In [214]: ts = pd.Series(np.arange(12), index=rng)
  3. In [215]: ts
  4. Out[215]:
  5. 2000-01-01 00:00:00 0
  6. 2000-01-01 00:01:00 1
  7. 2000-01-01 00:02:00 2
  8. 2000-01-01 00:03:00 3
  9. 2000-01-01 00:04:00 4
  10. 2000-01-01 00:05:00 5
  11. 2000-01-01 00:06:00 6
  12. 2000-01-01 00:07:00 7
  13. 2000-01-01 00:08:00 8
  14. 2000-01-01 00:09:00 9
  15. 2000-01-01 00:10:00 10
  16. 2000-01-01 00:11:00 11
  17. Freq: T, dtype: int64

假设你想要通过求和的方式将这些数据聚合到“5分钟”块中:

  1. In [216]: ts.resample('5min', closed='right').sum()
  2. Out[216]:
  3. 1999-12-31 23:55:00 0
  4. 2000-01-01 00:00:00 15
  5. 2000-01-01 00:05:00 40
  6. 2000-01-01 00:10:00 11
  7. Freq: 5T, dtype: int64

传入的频率将会以“5分钟”的增量定义面元边界。默认情况下,面元的右边界是包含的,因此00:00到00:05的区间中是包含00:05的。传入closed=’left’会让区间以左边界闭合:

  1. In [217]: ts.resample('5min', closed='right').sum()
  2. Out[217]:
  3. 1999-12-31 23:55:00 0
  4. 2000-01-01 00:00:00 15
  5. 2000-01-01 00:05:00 40
  6. 2000-01-01 00:10:00 11
  7. Freq: 5T, dtype: int64

如你所见,最终的时间序列是以各面元右边界的时间戳进行标记的。传入label=’right’即可用面元的邮编界对其进行标记:

  1. In [218]: ts.resample('5min', closed='right', label='right').sum()
  2. Out[218]:
  3. 2000-01-01 00:00:00 0
  4. 2000-01-01 00:05:00 15
  5. 2000-01-01 00:10:00 40
  6. 2000-01-01 00:15:00 11
  7. Freq: 5T, dtype: int64

图11-3说明了“1分钟”数据被转换为“5分钟”数据的处理过程。

第11章 时间序列 - 图14

最后,你可能希望对结果索引做一些位移,比如从右边界减去一秒以便更容易明白该时间戳到底表示的是哪个区间。只需通过loffset设置一个字符串或日期偏移量即可实现这个目的:

  1. In [219]: ts.resample('5min', closed='right',
  2. .....: label='right', loffset='-1s').sum()
  3. Out[219]:
  4. 1999-12-31 23:59:59 0
  5. 2000-01-01 00:04:59 15
  6. In [219]: ts.resample('5min', closed='right',
  7. .....: label='right', loffset='-1s').sum()
  8. Out[219]:
  9. 1999-12-31 23:59:59 0
  10. 2000-01-01 00:04:59 15

此外,也可以通过调用结果对象的shift方法来实现该目的,这样就不需要设置loffset了。

OHLC重采样

金融领域中有一种无所不在的时间序列聚合方式,即计算各面元的四个值:第一个值(open,开盘)、最后一个值(close,收盘)、最大值(high,最高)以及最小值(low,最低)。传入how=’ohlc’即可得到一个含有这四种聚合值的DataFrame。整个过程很高效,只需一次扫描即可计算出结果:

  1. In [220]: ts.resample('5min').ohlc()
  2. Out[220]:
  3. open high low close
  4. 2000-01-01 00:00:00 0 4 0 4
  5. 2000-01-01 00:05:00 5 9 5 9
  6. 2000-01-01 00:10:00 10 11 10 11

升采样和插值

在将数据从低频率转换到高频率时,就不需要聚合了。我们来看一个带有一些周型数据的DataFrame:

  1. In [221]: frame = pd.DataFrame(np.random.randn(2, 4),
  2. .....: index=pd.date_range('1/1/2000', periods=2,
  3. .....: freq='W-WED'),
  4. .....: columns=['Colorado', 'Texas', 'New York', 'Ohio'])
  5. In [222]: frame
  6. Out[222]:
  7. Colorado Texas New York Ohio
  8. 2000-01-05 -0.896431 0.677263 0.036503 0.087102
  9. 2000-01-12 -0.046662 0.927238 0.482284 -0.867130

当你对这个数据进行聚合,每组只有一个值,这样就会引入缺失值。我们使用asfreq方法转换成高频,不经过聚合:

  1. In [223]: df_daily = frame.resample('D').asfreq()
  2. In [224]: df_daily
  3. Out[224]:
  4. Colorado Texas New York Ohio
  5. 2000-01-05 -0.896431 0.677263 0.036503 0.087102
  6. 2000-01-06 NaN NaN NaN NaN
  7. 2000-01-07 NaN NaN NaN NaN
  8. 2000-01-08 NaN NaN NaN NaN
  9. 2000-01-09 NaN NaN NaN NaN
  10. 2000-01-10 NaN NaN NaN NaN
  11. 2000-01-11 NaN NaN NaN NaN
  12. 2000-01-12 -0.046662 0.927238 0.482284 -0.867130

假设你想要用前面的周型值填充“非星期三”。resampling的填充和插值方式跟fillna和reindex的一样:

  1. In [225]: frame.resample('D').ffill()
  2. Out[225]:
  3. Colorado Texas New York Ohio
  4. 2000-01-05 -0.896431 0.677263 0.036503 0.087102
  5. 2000-01-06 -0.896431 0.677263 0.036503 0.087102
  6. 2000-01-07 -0.896431 0.677263 0.036503 0.087102
  7. 2000-01-08 -0.896431 0.677263 0.036503 0.087102
  8. 2000-01-09 -0.896431 0.677263 0.036503 0.087102
  9. 2000-01-10 -0.896431 0.677263 0.036503 0.087102
  10. 2000-01-11 -0.896431 0.677263 0.036503 0.087102
  11. 2000-01-12 -0.046662 0.927238 0.482284 -0.867130

同样,这里也可以只填充指定的时期数(目的是限制前面的观测值的持续使用距离):

  1. In [226]: frame.resample('D').ffill(limit=2)
  2. Out[226]:
  3. Colorado Texas New York Ohio
  4. 2000-01-05 -0.896431 0.677263 0.036503 0.087102
  5. 2000-01-06 -0.896431 0.677263 0.036503 0.087102
  6. 2000-01-07 -0.896431 0.677263 0.036503 0.087102
  7. 2000-01-08 NaN NaN NaN NaN
  8. 2000-01-09 NaN NaN NaN NaN
  9. 2000-01-10 NaN NaN NaN NaN
  10. 2000-01-11 NaN NaN NaN NaN
  11. 2000-01-12 -0.046662 0.927238 0.482284 -0.867130

注意,新的日期索引完全没必要跟旧的重叠:

  1. In [227]: frame.resample('W-THU').ffill()
  2. Out[227]:
  3. Colorado Texas New York Ohio
  4. 2000-01-06 -0.896431 0.677263 0.036503 0.087102
  5. 2000-01-13 -0.046662 0.927238 0.482284 -0.867130

通过时期进行重采样

对那些使用时期索引的数据进行重采样与时间戳很像:

  1. In [228]: frame = pd.DataFrame(np.random.randn(24, 4),
  2. .....: index=pd.period_range('1-2000', '12-2001',
  3. .....: freq='M'),
  4. .....: columns=['Colorado', 'Texas', 'New York', 'Ohio'])
  5. In [229]: frame[:5]
  6. Out[229]:
  7. Colorado Texas New York Ohio
  8. 2000-01 0.493841 -0.155434 1.397286 1.507055
  9. 2000-02 -1.179442 0.443171 1.395676 -0.529658
  10. 2000-03 0.787358 0.248845 0.743239 1.267746
  11. 2000-04 1.302395 -0.272154 -0.051532 -0.467740
  12. 2000-05 -1.040816 0.426419 0.312945 -1.115689
  13. In [230]: annual_frame = frame.resample('A-DEC').mean()
  14. In [231]: annual_frame
  15. Out[231]:
  16. Colorado Texas New York Ohio
  17. 2000 0.556703 0.016631 0.111873 -0.027445
  18. 2001 0.046303 0.163344 0.251503 -0.157276

升采样要稍微麻烦一些,因为你必须决定在新频率中各区间的哪端用于放置原来的值,就像asfreq方法那样。convention参数默认为’start’,也可设置为’end’:

  1. # Q-DEC: Quarterly, year ending in December
  2. In [232]: annual_frame.resample('Q-DEC').ffill()
  3. Out[232]:
  4. Colorado Texas New York Ohio
  5. 2000Q1 0.556703 0.016631 0.111873 -0.027445
  6. 2000Q2 0.556703 0.016631 0.111873 -0.027445
  7. 2000Q3 0.556703 0.016631 0.111873 -0.027445
  8. 2000Q4 0.556703 0.016631 0.111873 -0.027445
  9. 2001Q1 0.046303 0.163344 0.251503 -0.157276
  10. 2001Q2 0.046303 0.163344 0.251503 -0.157276
  11. 2001Q3 0.046303 0.163344 0.251503 -0.157276
  12. 2001Q4 0.046303 0.163344 0.251503 -0.157276
  13. In [233]: annual_frame.resample('Q-DEC', convention='end').ffill()
  14. Out[233]:
  15. Colorado Texas New York Ohio
  16. 2000Q4 0.556703 0.016631 0.111873 -0.027445
  17. 2001Q1 0.556703 0.016631 0.111873 -0.027445
  18. 2001Q2 0.556703 0.016631 0.111873 -0.027445
  19. 2001Q3 0.556703 0.016631 0.111873 -0.027445
  20. 2001Q4 0.046303 0.163344 0.251503 -0.157276

由于时期指的是时间区间,所以升采样和降采样的规则就比较严格:

  • 在降采样中,目标频率必须是源频率的子时期(subperiod)。
  • 在升采样中,目标频率必须是源频率的超时期(superperiod)。

如果不满足这些条件,就会引发异常。这主要影响的是按季、年、周计算的频率。例如,由Q-MAR定义的时间区间只能升采样为A-MAR、A-JUN、A-SEP、A-DEC等:

  1. In [234]: annual_frame.resample('Q-MAR').ffill()
  2. Out[234]:
  3. Colorado Texas New York Ohio
  4. 2000Q4 0.556703 0.016631 0.111873 -0.027445
  5. 2001Q1 0.556703 0.016631 0.111873 -0.027445
  6. 2001Q2 0.556703 0.016631 0.111873 -0.027445
  7. 2001Q3 0.556703 0.016631 0.111873 -0.027445
  8. 2001Q4 0.046303 0.163344 0.251503 -0.157276
  9. 2002Q1 0.046303 0.163344 0.251503 -0.157276
  10. 2002Q2 0.046303 0.163344 0.251503 -0.157276
  11. 2002Q3 0.046303 0.163344 0.251503 -0.157276

11.7 移动窗口函数

在移动窗口(可以带有指数衰减权数)上计算的各种统计函数也是一类常见于时间序列的数组变换。这样可以圆滑噪音数据或断裂数据。我将它们称为移动窗口函数(moving window function),其中还包括那些窗口不定长的函数(如指数加权移动平均)。跟其他统计函数一样,移动窗口函数也会自动排除缺失值。

开始之前,我们加载一些时间序列数据,将其重采样为工作日频率:

  1. In [235]: close_px_all = pd.read_csv('examples/stock_px_2.csv',
  2. .....: parse_dates=True, index_col=0)
  3. In [236]: close_px = close_px_all[['AAPL', 'MSFT', 'XOM']]
  4. In [237]: close_px = close_px.resample('B').ffill()

现在引入rolling运算符,它与resample和groupby很像。可以在TimeSeries或DataFrame以及一个window(表示期数,见图11-4)上调用它:

  1. In [238]: close_px.AAPL.plot()
  2. Out[238]: <matplotlib.axes._subplots.AxesSubplot at 0x7f2f2570cf98>
  3. In [239]: close_px.AAPL.rolling(250).mean().plot()

第11章 时间序列 - 图15

表达式rolling(250)与groupby很像,但不是对其进行分组,而是创建一个按照250天分组的滑动窗口对象。然后,我们就得到了苹果公司股价的250天的移动窗口。

默认情况下,rolling函数需要窗口中所有的值为非NA值。可以修改该行为以解决缺失数据的问题。其实,在时间序列开始处尚不足窗口期的那些数据就是个特例(见图11-5):

  1. In [241]: appl_std250 = close_px.AAPL.rolling(250, min_periods=10).std()
  2. In [242]: appl_std250[5:12]
  3. Out[242]:
  4. 2003-01-09 NaN
  5. 2003-01-10 NaN
  6. 2003-01-13 NaN
  7. 2003-01-14 NaN
  8. 2003-01-15 0.077496
  9. 2003-01-16 0.074760
  10. 2003-01-17 0.112368
  11. Freq: B, Name: AAPL, dtype: float64
  12. In [243]: appl_std250.plot()

第11章 时间序列 - 图16

要计算扩展窗口平均(expanding window mean),可以使用expanding而不是rolling。“扩展”意味着,从时间序列的起始处开始窗口,增加窗口直到它超过所有的序列。apple_std250时间序列的扩展窗口平均如下所示:

  1. In [244]: expanding_mean = appl_std250.expanding().mean()

对DataFrame调用rolling_mean(以及与之类似的函数)会将转换应用到所有的列上(见图11-6):

  1. In [246]: close_px.rolling(60).mean().plot(logy=True)

第11章 时间序列 - 图17

rolling函数也可以接受一个指定固定大小时间补偿字符串,而不是一组时期。这样可以方便处理不规律的时间序列。这些字符串也可以传递给resample。例如,我们可以计算20天的滚动均值,如下所示:

  1. In [247]: close_px.rolling('20D').mean()
  2. Out[247]:
  3. AAPL MSFT XOM
  4. 2003-01-02 7.400000 21.110000 29.220000
  5. 2003-01-03 7.425000 21.125000 29.230000
  6. 2003-01-06 7.433333 21.256667 29.473333
  7. 2003-01-07 7.432500 21.425000 29.342500
  8. 2003-01-08 7.402000 21.402000 29.240000
  9. 2003-01-09 7.391667 21.490000 29.273333
  10. 2003-01-10 7.387143 21.558571 29.238571
  11. 2003-01-13 7.378750 21.633750 29.197500
  12. 2003-01-14 7.370000 21.717778 29.194444
  13. 2003-01-15 7.355000 21.757000 29.152000
  14. ... ... ... ...
  15. 2011-10-03 398.002143 25.890714 72.413571
  16. 2011-10-04 396.802143 25.807857 72.427143
  17. 2011-10-05 395.751429 25.729286 72.422857
  18. 2011-10-06 394.099286 25.673571 72.375714
  19. 2011-10-07 392.479333 25.712000 72.454667
  20. 2011-10-10 389.351429 25.602143 72.527857
  21. 2011-10-11 388.505000 25.674286 72.835000
  22. 2011-10-12 388.531429 25.810000 73.400714
  23. 2011-10-13 388.826429 25.961429 73.905000
  24. 2011-10-14 391.038000 26.048667 74.185333
  25. [2292 rows x 3 columns]

指数加权函数

另一种使用固定大小窗口及相等权数观测值的办法是,定义一个衰减因子(decay factor)常量,以便使近期的观测值拥有更大的权数。衰减因子的定义方式有很多,比较流行的是使用时间间隔(span),它可以使结果兼容于窗口大小等于时间间隔的简单移动窗口(simple moving window)函数。

由于指数加权统计会赋予近期的观测值更大的权数,因此相对于等权统计,它能“适应”更快的变化。

除了rolling和expanding,pandas还有ewm运算符。下面这个例子对比了苹果公司股价的30日移动平均和span=30的指数加权移动平均(如图11-7所示):

  1. In [249]: aapl_px = close_px.AAPL['2006':'2007']
  2. In [250]: ma60 = aapl_px.rolling(30, min_periods=20).mean()
  3. In [251]: ewma60 = aapl_px.ewm(span=30).mean()
  4. In [252]: ma60.plot(style='k--', label='Simple MA')
  5. Out[252]: <matplotlib.axes._subplots.AxesSubplot at 0x7f2f252161d0>
  6. In [253]: ewma60.plot(style='k-', label='EW MA')
  7. Out[253]: <matplotlib.axes._subplots.AxesSubplot at 0x7f2f252161d0>
  8. In [254]: plt.legend()

第11章 时间序列 - 图18

二元移动窗口函数

有些统计运算(如相关系数和协方差)需要在两个时间序列上执行。例如,金融分析师常常对某只股票对某个参考指数(如标准普尔500指数)的相关系数感兴趣。要进行说明,我们先计算我们感兴趣的时间序列的百分数变化:

  1. In [256]: spx_px = close_px_all['SPX']
  2. In [257]: spx_rets = spx_px.pct_change()
  3. In [258]: returns = close_px.pct_change()

调用rolling之后,corr聚合函数开始计算与spx_rets滚动相关系数(结果见图11-8):

  1. In [259]: corr = returns.AAPL.rolling(125, min_periods=100).corr(spx_rets)
  2. In [260]: corr.plot()

第11章 时间序列 - 图19

假设你想要一次性计算多只股票与标准普尔500指数的相关系数。虽然编写一个循环并新建一个DataFrame不是什么难事,但比较啰嗦。其实,只需传入一个TimeSeries和一个DataFrame,rolling_corr就会自动计算TimeSeries(本例中就是spx_rets)与DataFrame各列的相关系数。结果如图11-9所示:

  1. In [262]: corr = returns.rolling(125, min_periods=100).corr(spx_rets)
  2. In [263]: corr.plot()

第11章 时间序列 - 图20

用户定义的移动窗口函数

rolling_apply函数使你能够在移动窗口上应用自己设计的数组函数。唯一要求的就是:该函数要能从数组的各个片段中产生单个值(即约简)。比如说,当我们用rolling(…).quantile(q)计算样本分位数时,可能对样本中特定值的百分等级感兴趣。scipy.stats.percentileofscore函数就能达到这个目的(结果见图11-10):

  1. In [265]: from scipy.stats import percentileofscore
  2. In [266]: score_at_2percent = lambda x: percentileofscore(x, 0.02)
  3. In [267]: result = returns.AAPL.rolling(250).apply(score_at_2percent)
  4. In [268]: result.plot()

第11章 时间序列 - 图21

如果你没安装SciPy,可以使用conda或pip安装。

11.8 总结

与前面章节接触的数据相比,时间序列数据要求不同类型的分析和数据转换工具。

在接下来的章节中,我们将学习一些高级的pandas方法和如何开始使用建模库statsmodels和scikit-learn。