# 第 11 章 时间序列 时间序列(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`)是用得最多的数据类型: ```python In [10]: from datetime import datetime In [11]: now = datetime.now() In [12]: now Out[12]: datetime.datetime(2017, 9, 25, 14, 5, 52, 72973) In [13]: now.year, now.month, now.day Out[13]: (2017, 9, 25) ``` `datetime`以毫秒形式存储日期和时间。`timedelta`表示两个`datetime`对象之间的时间差: ```python In [14]: delta = datetime(2011, 1, 7) - datetime(2008, 6, 24, 8, 15) In [15]: delta Out[15]: datetime.timedelta(926, 56700) In [16]: delta.days Out[16]: 926 In [17]: delta.seconds Out[17]: 56700 ``` 可以给`datetime`对象加上(或减去)一个或多个`timedelta`,这样会产生一个新对象: ```python In [18]: from datetime import timedelta In [19]: start = datetime(2011, 1, 7) In [20]: start + timedelta(12) Out[20]: datetime.datetime(2011, 1, 19, 0, 0) In [21]: start - 2 * timedelta(12) Out[21]: datetime.datetime(2010, 12, 14, 0, 0) ``` `datetime`模块中的数据类型参见表 10-1。虽然本章主要讲的是 pandas 数据类型和高级时间序列处理,但你肯定会在 Python 的其他地方遇到有关`datetime`的数据类型。 表 11-1 `datetime`模块中的数据类型 ![](img/7178691-4af261a305a70aeb.png) `tzinfo`存储时区信息的基本类型 ## 字符串和`datetime`的相互转换 利用`str`或`strftime`方法(传入一个格式化字符串),`datetime`对象和 pandas 的 `Timestamp`对象(稍后就会介绍)可以被格式化为字符串: ```python In [22]: stamp = datetime(2011, 1, 3) In [23]: str(stamp) Out[23]: '2011-01-03 00:00:00' In [24]: stamp.strftime('%Y-%m-%d') Out[24]: '2011-01-03' ``` 表 11-2 列出了全部的格式化编码。 表 11-2 `datetime`格式定义(兼容 ISO C89) ![](img/7178691-50c751823754df58.png) ![](img/7178691-de0181e1f6b45eaf.png) `datetime.strptime`可以用这些格式化编码将字符串转换为日期: ```python In [25]: value = '2011-01-03' In [26]: datetime.strptime(value, '%Y-%m-%d') Out[26]: datetime.datetime(2011, 1, 3, 0, 0) In [27]: datestrs = ['7/6/2011', '8/6/2011'] In [28]: [datetime.strptime(x, '%m/%d/%Y') for x in datestrs] Out[28]: [datetime.datetime(2011, 7, 6, 0, 0), datetime.datetime(2011, 8, 6, 0, 0)] ``` `datetime.strptime`是通过已知格式进行日期解析的最佳方式。但是每次都要编写格式定义是很麻烦的事情,尤其是对于一些常见的日期格式。这种情况下,你可以用`dateutil`这个第三方包中的 `parser.parse`方法(pandas 中已经自动安装好了): ```python In [29]: from dateutil.parser import parse In [30]: parse('2011-01-03') Out[30]: datetime.datetime(2011, 1, 3, 0, 0) ``` `dateutil`可以解析几乎所有人类能够理解的日期表示形式: ```python In [31]: parse('Jan 31, 1997 10:45 PM') Out[31]: datetime.datetime(1997, 1, 31, 22, 45) ``` 在国际通用的格式中,日出现在月的前面很普遍,传入`dayfirst=True`即可解决这个问题: ```python In [32]: parse('6/12/2011', dayfirst=True) Out[32]: datetime.datetime(2011, 12, 6, 0, 0) ``` pandas 通常是用于处理成组日期的,不管这些日期是`DataFrame`的轴索引还是列。`to_datetime`方法可以解析多种不同的日期表示形式。对标准日期格式(如 ISO8601)的解析非常快: ```python In [33]: datestrs = ['2011-07-06 12:00:00', '2011-08-06 00:00:00'] In [34]: pd.to_datetime(datestrs) Out[34]: DatetimeIndex(['2011-07-06 12:00:00', '2011-08-06 00:00:00'], dtype='dat etime64[ns]', freq=None) ``` 它还可以处理缺失值(`None`、空字符串等): ```python In [35]: idx = pd.to_datetime(datestrs + [None]) In [36]: idx Out[36]: DatetimeIndex(['2011-07-06 12:00:00', '2011-08-06 00:00:00', 'NaT'], dty pe='datetime64[ns]', freq=None) In [37]: idx[2] Out[37]: NaT In [38]: pd.isnull(idx) Out[38]: array([False, False, True], dtype=bool) ``` `NaT`(Not a Time)是 pandas 中时间戳数据的`null`值。 > 注意:`dateutil.parser`是一个实用但不完美的工具。比如说,它会把一些原本不是日期的字符串认作是日期(比如`"42"`会被解析为 2042 年的今天)。 `datetime`对象还有一些特定于当前环境(位于不同国家或使用不同语言的系统)的格式化选项。例如,德语或法语系统所用的月份简写就与英语系统所用的不同。表 11-3 进行了总结。 表 11-3 特定于当前环境的日期格式 ![](img/7178691-cf0119398273e2b0.png) # 11.2 时间序列基础 pandas 最基本的时间序列类型就是以时间戳(通常以 Python 字符串或`datatime`对象表示)为索引的`Series`: ```python In [39]: from datetime import datetime In [40]: dates = [datetime(2011, 1, 2), datetime(2011, 1, 5), ....: datetime(2011, 1, 7), datetime(2011, 1, 8), ....: datetime(2011, 1, 10), datetime(2011, 1, 12)] In [41]: ts = pd.Series(np.random.randn(6), index=dates) In [42]: ts Out[42]: 2011-01-02 -0.204708 2011-01-05 0.478943 2011-01-07 -0.519439 2011-01-08 -0.555730 2011-01-10 1.965781 2011-01-12 1.393406 dtype: float64 ``` 这些`datetime`对象实际上是被放在一个`DatetimeIndex`中的: ```python In [43]: ts.index Out[43]: DatetimeIndex(['2011-01-02', '2011-01-05', '2011-01-07', '2011-01-08', '2011-01-10', '2011-01-12'], dtype='datetime64[ns]', freq=None) ``` 跟其他`Series`一样,不同索引的时间序列之间的算术运算会自动按日期对齐: ```python In [44]: ts + ts[::2] Out[44]: 2011-01-02 -0.409415 2011-01-05 NaN 2011-01-07 -1.038877 2011-01-08 NaN 2011-01-10 3.931561 2011-01-12 NaN dtype: float64 ``` `ts[::2]`是每隔两个取一个。 pandas 用 NumPy 的`datetime64`数据类型以纳秒形式存储时间戳: ```python In [45]: ts.index.dtype Out[45]: dtype(' ``` 传入一个整数即可定义偏移量的倍数: ```python In [84]: four_hours = Hour(4) In [85]: four_hours Out[85]: <4 * Hours> ``` 一般来说,无需明确创建这样的对象,只需使用诸如"H"或"4H"这样的字符串别名即可。在基础频率前面放上一个整数即可创建倍数: ```python In [86]: pd.date_range('2000-01-01', '2000-01-03 23:59', freq='4h') Out[86]: DatetimeIndex(['2000-01-01 00:00:00', '2000-01-01 04:00:00', '2000-01-01 08:00:00', '2000-01-01 12:00:00', '2000-01-01 16:00:00', '2000-01-01 20:00:00', '2000-01-02 00:00:00', '2000-01-02 04:00:00', '2000-01-02 08:00:00', '2000-01-02 12:00:00', '2000-01-02 16:00:00', '2000-01-02 20:00:00', '2000-01-03 00:00:00', '2000-01-03 04:00:00', '2000-01-03 08:00:00', '2000-01-03 12:00:00', '2000-01-03 16:00:00', '2000-01-03 20:00:00'], dtype='datetime64[ns]', freq='4H') ``` 大部分偏移量对象都可通过加法进行连接: ```python In [87]: Hour(2) + Minute(30) Out[87]: <150 * Minutes> ``` 同理,你也可以传入频率字符串(如"2h30min"),这种字符串可以被高效地解析为等效的表达式: ```python In [88]: pd.date_range('2000-01-01', periods=10, freq='1h30min') Out[88]: DatetimeIndex(['2000-01-01 00:00:00', '2000-01-01 01:30:00', '2000-01-01 03:00:00', '2000-01-01 04:30:00', '2000-01-01 06:00:00', '2000-01-01 07:30:00', '2000-01-01 09:00:00', '2000-01-01 10:30:00', '2000-01-01 12:00:00', '2000-01-01 13:30:00'], dtype='datetime64[ns]', freq='90T') ``` 有些频率所描述的时间点并不是均匀分隔的。例如,`"M"`(日历月末)和`"BM"`(每月最后一个工作日)就取决于每月的天数,对于后者,还要考虑月末是不是周末。由于没有更好的术语,我将这些称为锚点偏移量(anchored offset)。 表 11-4 列出了 pandas 中的频率代码和日期偏移量类。 > 笔记:用户可以根据实际需求自定义一些频率类以便提供 pandas 所没有的日期逻辑,但具体的细节超出了本书的范围。 表 11-4 时间序列的基础频率 ![](img/7178691-ff139312cd972204.png) ![](img/7178691-adfa57a998c0296e.png) ![](img/7178691-d09e577a10d0e6eb.png) ## WOM 日期 WOM(Week Of Month)是一种非常实用的频率类,它以`WOM`开头。它使你能获得诸如“每月第 3 个星期五”之类的日期: ```python In [89]: rng = pd.date_range('2012-01-01', '2012-09-01', freq='WOM-3FRI') In [90]: list(rng) Out[90]: [Timestamp('2012-01-20 00:00:00', freq='WOM-3FRI'), Timestamp('2012-02-17 00:00:00', freq='WOM-3FRI'), Timestamp('2012-03-16 00:00:00', freq='WOM-3FRI'), Timestamp('2012-04-20 00:00:00', freq='WOM-3FRI'), Timestamp('2012-05-18 00:00:00', freq='WOM-3FRI'), Timestamp('2012-06-15 00:00:00', freq='WOM-3FRI'), Timestamp('2012-07-20 00:00:00', freq='WOM-3FRI'), Timestamp('2012-08-17 00:00:00', freq='WOM-3FRI')] ``` ## 移动(超前和滞后)数据 移动(shifting)指的是沿着时间轴将数据前移或后移。`Series`和`DataFrame`都有一个`shift`方法用于执行单纯的前移或后移操作,保持索引不变: ```python In [91]: ts = pd.Series(np.random.randn(4), ....: index=pd.date_range('1/1/2000', periods=4, freq='M')) In [92]: ts Out[92]: 2000-01-31 -0.066748 2000-02-29 0.838639 2000-03-31 -0.117388 2000-04-30 -0.517795 Freq: M, dtype: float64 In [93]: ts.shift(2) Out[93]: 2000-01-31 NaN 2000-02-29 NaN 2000-03-31 -0.066748 2000-04-30 0.838639 Freq: M, dtype: float64 In [94]: ts.shift(-2) Out[94]: 2000-01-31 -0.117388 2000-02-29 -0.517795 2000-03-31 NaN 2000-04-30 NaN Freq: M, dtype: float64 ``` 当我们这样进行移动时,就会在时间序列的前面或后面产生缺失数据。 `shift`通常用于计算一个时间序列或多个时间序列(如`DataFrame`的列)中的百分比变化。可以这样表达: ```python ts / ts.shift(1) - 1 ``` 由于单纯的移位操作不会修改索引,所以部分数据会被丢弃。因此,如果频率已知,则可以将其传给`shift`以便实现对时间戳进行位移而不是对数据进行简单位移: ```python In [95]: ts.shift(2, freq='M') Out[95]: 2000-03-31 -0.066748 2000-04-30 0.838639 2000-05-31 -0.117388 2000-06-30 -0.517795 Freq: M, dtype: float64 ``` 这里还可以使用其他频率,于是你就能非常灵活地对数据进行超前和滞后处理了: ```python In [96]: ts.shift(3, freq='D') Out[96]: 2000-02-03 -0.066748 2000-03-03 0.838639 2000-04-03 -0.117388 2000-05-03 -0.517795 dtype: float64 In [97]: ts.shift(1, freq='90T') Out[97]: 2000-01-31 01:30:00 -0.066748 2000-02-29 01:30:00 0.838639 2000-03-31 01:30:00 -0.117388 2000-04-30 01:30:00 -0.517795 Freq: M, dtype: float64 ``` ## 通过偏移量对日期进行位移 pandas 的日期偏移量还可以用在`datetime`或`Timestamp`对象上: ```python In [98]: from pandas.tseries.offsets import Day, MonthEnd In [99]: now = datetime(2011, 11, 17) In [100]: now + 3 * Day() Out[100]: Timestamp('2011-11-20 00:00:00') ``` 如果加的是锚点偏移量(比如`MonthEnd`),第一次增量会将原日期向前滚动到符合频率规则的下一个日期: ```python In [101]: now + MonthEnd() Out[101]: Timestamp('2011-11-30 00:00:00') In [102]: now + MonthEnd(2) Out[102]: Timestamp('2011-12-31 00:00:00') ``` 通过锚点偏移量的`rollforward`和`rollback`方法,可明确地将日期向前或向后“滚动”: ```python In [103]: offset = MonthEnd() In [104]: offset.rollforward(now) Out[104]: Timestamp('2011-11-30 00:00:00') In [105]: offset.rollback(now) Out[105]: Timestamp('2011-10-31 00:00:00') ``` 日期偏移量还有一个巧妙的用法,即结合`groupby`使用这两个“滚动”方法: ```python In [106]: ts = pd.Series(np.random.randn(20), .....: index=pd.date_range('1/15/2000', periods=20, freq='4d')) In [107]: ts Out[107]: 2000-01-15 -0.116696 2000-01-19 2.389645 2000-01-23 -0.932454 2000-01-27 -0.229331 2000-01-31 -1.140330 2000-02-04 0.439920 2000-02-08 -0.823758 2000-02-12 -0.520930 2000-02-16 0.350282 2000-02-20 0.204395 2000-02-24 0.133445 2000-02-28 0.327905 2000-03-03 0.072153 2000-03-07 0.131678 2000-03-11 -1.297459 2000-03-15 0.997747 2000-03-19 0.870955 2000-03-23 -0.991253 2000-03-27 0.151699 2000-03-31 1.266151 Freq: 4D, dtype: float64 In [108]: ts.groupby(offset.rollforward).mean() Out[108]: 2000-01-31 -0.005833 2000-02-29 0.015894 2000-03-31 0.150209 dtype: float64 ``` 当然,更简单、更快速地实现该功能的办法是使用`resample`(11.6 小节将对此进行详细介绍): ```python In [109]: ts.resample('M').mean() Out[109]: 2000-01-31 -0.005833 2000-02-29 0.015894 2000-03-31 0.150209 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 中看到,也可以通过文档查看: ```python In [110]: import pytz In [111]: pytz.common_timezones[-5:] Out[111]: ['US/Eastern', 'US/Hawaii', 'US/Mountain', 'US/Pacific', 'UTC'] ``` 要从`pytz`中获取时区对象,使用`pytz.timezone`即可: ```python In [112]: tz = pytz.timezone('America/New_York') In [113]: tz Out[113]: ``` pandas 中的方法既可以接受时区名也可以接受这些对象。 # 时区本地化和转换 默认情况下,pandas 中的时间序列是单纯(naive)的时区。看看下面这个时间序列: ```python In [114]: rng = pd.date_range('3/9/2012 9:30', periods=6, freq='D') In [115]: ts = pd.Series(np.random.randn(len(rng)), index=rng) In [116]: ts Out[116]: 2012-03-09 09:30:00 -0.202469 2012-03-10 09:30:00 0.050718 2012-03-11 09:30:00 0.639869 2012-03-12 09:30:00 0.597594 2012-03-13 09:30:00 -0.797246 2012-03-14 09:30:00 0.472879 Freq: D, dtype: float64 ``` 其索引的`tz`字段为`None`: ```python In [117]: print(ts.index.tz) None ``` 可以用时区集生成日期范围: ```python In [118]: pd.date_range('3/9/2012 9:30', periods=10, freq='D', tz='UTC') Out[118]: DatetimeIndex(['2012-03-09 09:30:00+00:00', '2012-03-10 09:30:00+00:00', '2012-03-11 09:30:00+00:00', '2012-03-12 09:30:00+00:00', '2012-03-13 09:30:00+00:00', '2012-03-14 09:30:00+00:00', '2012-03-15 09:30:00+00:00', '2012-03-16 09:30:00+00:00', '2012-03-17 09:30:00+00:00', '2012-03-18 09:30:00+00:00'], dtype='datetime64[ns, UTC]', freq='D') ``` 从单纯到本地化的转换是通过`tz_localize`方法处理的: ```python In [119]: ts Out[119]: 2012-03-09 09:30:00 -0.202469 2012-03-10 09:30:00 0.050718 2012-03-11 09:30:00 0.639869 2012-03-12 09:30:00 0.597594 2012-03-13 09:30:00 -0.797246 2012-03-14 09:30:00 0.472879 Freq: D, dtype: float64 In [120]: ts_utc = ts.tz_localize('UTC') In [121]: ts_utc Out[121]: 2012-03-09 09:30:00+00:00 -0.202469 2012-03-10 09:30:00+00:00 0.050718 2012-03-11 09:30:00+00:00 0.639869 2012-03-12 09:30:00+00:00 0.597594 2012-03-13 09:30:00+00:00 -0.797246 2012-03-14 09:30:00+00:00 0.472879 Freq: D, dtype: float64 In [122]: ts_utc.index Out[122]: DatetimeIndex(['2012-03-09 09:30:00+00:00', '2012-03-10 09:30:00+00:00', '2012-03-11 09:30:00+00:00', '2012-03-12 09:30:00+00:00', '2012-03-13 09:30:00+00:00', '2012-03-14 09:30:00+00:00'], dtype='datetime64[ns, UTC]', freq='D') ``` 一旦时间序列被本地化到某个特定时区,就可以用`tz_convert`将其转换到别的时区了: ```python In [123]: ts_utc.tz_convert('America/New_York') Out[123]: 2012-03-09 04:30:00-05:00 -0.202469 2012-03-10 04:30:00-05:00 0.050718 2012-03-11 05:30:00-04:00 0.639869 2012-03-12 05:30:00-04:00 0.597594 2012-03-13 05:30:00-04:00 -0.797246 2012-03-14 05:30:00-04:00 0.472879 Freq: D, dtype: float64 ``` 对于上面这种时间序列(它跨越了美国东部时区的夏令时转变期),我们可以将其本地化到 EST,然后转换为 UTC 或柏林时间: ```python In [124]: ts_eastern = ts.tz_localize('America/New_York') In [125]: ts_eastern.tz_convert('UTC') Out[125]: 2012-03-09 14:30:00+00:00 -0.202469 2012-03-10 14:30:00+00:00 0.050718 2012-03-11 13:30:00+00:00 0.639869 2012-03-12 13:30:00+00:00 0.597594 2012-03-13 13:30:00+00:00 -0.797246 2012-03-14 13:30:00+00:00 0.472879 Freq: D, dtype: float64 In [126]: ts_eastern.tz_convert('Europe/Berlin') Out[126]: 2012-03-09 15:30:00+01:00 -0.202469 2012-03-10 15:30:00+01:00 0.050718 2012-03-11 14:30:00+01:00 0.639869 2012-03-12 14:30:00+01:00 0.597594 2012-03-13 14:30:00+01:00 -0.797246 2012-03-14 14:30:00+01:00 0.472879 Freq: D, dtype: float64 ``` `tz_localize`和`tz_convert`也是`DatetimeIndex`的实例方法: ```python In [127]: ts.index.tz_localize('Asia/Shanghai') Out[127]: DatetimeIndex(['2012-03-09 09:30:00+08:00', '2012-03-10 09:30:00+08:00', '2012-03-11 09:30:00+08:00', '2012-03-12 09:30:00+08:00', '2012-03-13 09:30:00+08:00', '2012-03-14 09:30:00+08:00'], dtype='datetime64[ns, Asia/Shanghai]', freq='D') ``` > 注意:对单纯时间戳的本地化操作还会检查夏令时转变期附近容易混淆或不存在的时间。 ## 操作时区意识型`Timestamp`对象 跟时间序列和日期范围差不多,独立的`Timestamp`对象也能被从单纯型(naive)本地化为时区意识型(time zone-aware),并从一个时区转换到另一个时区: ```python In [128]: stamp = pd.Timestamp('2011-03-12 04:00') In [129]: stamp_utc = stamp.tz_localize('utc') In [130]: stamp_utc.tz_convert('America/New_York') Out[130]: Timestamp('2011-03-11 23:00:00-0500', tz='America/New_York') ``` 在创建`Timestamp`时,还可以传入一个时区信息: ```python In [131]: stamp_moscow = pd.Timestamp('2011-03-12 04:00', tz='Europe/Moscow') In [132]: stamp_moscow Out[132]: Timestamp('2011-03-12 04:00:00+0300', tz='Europe/Moscow') ``` 时区意识型`Timestamp`对象在内部保存了一个 UTC 时间戳值(自 UNIX 纪元(1970 年 1 月 1 日)算起的纳秒数)。这个 UTC 值在时区转换过程中是不会发生变化的: ```python In [133]: stamp_utc.value Out[133]: 1299902400000000000 In [134]: stamp_utc.tz_convert('America/New_York').value Out[134]: 1299902400000000000 ``` 当使用 pandas 的`DateOffset`对象执行时间算术运算时,运算过程会自动关注是否存在夏令时转变期。这里,我们创建了在 DST 转变之前的时间戳。首先,来看夏令时转变前的 30 分钟: ```python In [135]: from pandas.tseries.offsets import Hour In [136]: stamp = pd.Timestamp('2012-03-12 01:30', tz='US/Eastern') In [137]: stamp Out[137]: Timestamp('2012-03-12 01:30:00-0400', tz='US/Eastern') In [138]: stamp + Hour() Out[138]: Timestamp('2012-03-12 02:30:00-0400', tz='US/Eastern') ``` 然后,夏令时转变前 90 分钟: ```python In [139]: stamp = pd.Timestamp('2012-11-04 00:30', tz='US/Eastern') In [140]: stamp Out[140]: Timestamp('2012-11-04 00:30:00-0400', tz='US/Eastern') In [141]: stamp + 2 * Hour() Out[141]: Timestamp('2012-11-04 01:30:00-0500', tz='US/Eastern') ``` ## 不同时区之间的运算 如果两个时间序列的时区不同,在将它们合并到一起时,最终结果就会是 UTC。由于时间戳其实是以 UTC 存储的,所以这是一个很简单的运算,并不需要发生任何转换: ```python In [142]: rng = pd.date_range('3/7/2012 9:30', periods=10, freq='B') In [143]: ts = pd.Series(np.random.randn(len(rng)), index=rng) In [144]: ts Out[144]: 2012-03-07 09:30:00 0.522356 2012-03-08 09:30:00 -0.546348 2012-03-09 09:30:00 -0.733537 2012-03-12 09:30:00 1.302736 2012-03-13 09:30:00 0.022199 2012-03-14 09:30:00 0.364287 2012-03-15 09:30:00 -0.922839 2012-03-16 09:30:00 0.312656 2012-03-19 09:30:00 -1.128497 2012-03-20 09:30:00 -0.333488 Freq: B, dtype: float64 In [145]: ts1 = ts[:7].tz_localize('Europe/London') In [146]: ts2 = ts1[2:].tz_convert('Europe/Moscow') In [147]: result = ts1 + ts2 In [148]: result.index Out[148]: DatetimeIndex(['2012-03-07 09:30:00+00:00', '2012-03-08 09:30:00+00:00', '2012-03-09 09:30:00+00:00', '2012-03-12 09:30:00+00:00', '2012-03-13 09:30:00+00:00', '2012-03-14 09:30:00+00:00', '2012-03-15 09:30:00+00:00'], dtype='datetime64[ns, UTC]', freq='B') ``` # 11.5 时期及其算术运算 时期(period)表示的是时间区间,比如数日、数月、数季、数年等。`Period`类所表示的就是这种数据类型,其构造函数需要用到一个字符串或整数,以及表 11-4 中的频率: ```python In [149]: p = pd.Period(2007, freq='A-DEC') In [150]: p Out[150]: Period('2007', 'A-DEC') ``` 这里,这个`Period`对象表示的是从 2007 年 1 月 1 日到 2007 年 12 月 31 日之间的整段时间。只需对`Period`对象加上或减去一个整数即可达到根据其频率进行位移的效果: ```python In [151]: p + 5 Out[151]: Period('2012', 'A-DEC') In [152]: p - 2 Out[152]: Period('2005', 'A-DEC') ``` 如果两个`Period`对象拥有相同的频率,则它们的差就是它们之间的单位数量: ```python In [153]: pd.Period('2014', freq='A-DEC') - p Out[153]: 7 ``` `period_range`函数可用于创建规则的时期范围: ```python In [154]: rng = pd.period_range('2000-01-01', '2000-06-30', freq='M') In [155]: rng Out[155]: PeriodIndex(['2000-01', '2000-02', '2000-03', '2000-04', '2000-05', '20 00-06'], dtype='period[M]', freq='M') ``` `PeriodIndex`类保存了一组`Period`,它可以在任何 pandas 数据结构中被用作轴索引: ```python In [156]: pd.Series(np.random.randn(6), index=rng) Out[156]: 2000-01 -0.514551 2000-02 -0.559782 2000-03 -0.783408 2000-04 -1.797685 2000-05 -0.172670 2000-06 0.680215 Freq: M, dtype: float64 ``` 如果你有一个字符串数组,你也可以使用`PeriodIndex`类: ```python In [157]: values = ['2001Q3', '2002Q2', '2003Q1'] In [158]: index = pd.PeriodIndex(values, freq='Q-DEC') In [159]: index Out[159]: PeriodIndex(['2001Q3', '2002Q2', '2003Q1'], dtype='period[Q-DEC]', freq ='Q-DEC') ``` ## 时期的频率转换 `Period`和`PeriodIndex`对象都可以通过其`asfreq`方法被转换成别的频率。假设我们有一个年度时期,希望将其转换为当年年初或年末的一个月度时期。该任务非常简单: ```python In [160]: p = pd.Period('2007', freq='A-DEC') In [161]: p Out[161]: Period('2007', 'A-DEC') In [162]: p.asfreq('M', how='start') Out[162]: Period('2007-01', 'M') In [163]: p.asfreq('M', how='end') Out[163]: Period('2007-12', 'M') ``` 你可以将`Period('2007','A-DEC')`看做一个被划分为多个月度时期的时间段中的游标。图 11-1 对此进行了说明。对于一个不以 12 月结束的财政年度,月度子时期的归属情况就不一样了: ```python In [164]: p = pd.Period('2007', freq='A-JUN') In [165]: p Out[165]: Period('2007', 'A-JUN') In [166]: p.asfreq('M', 'start') Out[166]: Period('2006-07', 'M') In [167]: p.asfreq('M', 'end') Out[167]: Period('2007-06', 'M') ``` ![图 11-1 Period 频率转换示例](img/7178691-d201200d0e65676f.png) 在将高频率转换为低频率时,超时期(superperiod)是由子时期(subperiod)所属的位置决定的。例如,在`A-JUN`频率中,月份“2007 年 8 月”实际上是属于周期“2008 年”的: ```python In [168]: p = pd.Period('Aug-2007', 'M') In [169]: p.asfreq('A-JUN') Out[169]: Period('2008', 'A-JUN') ``` 完整的`PeriodIndex`或`TimeSeries`的频率转换方式也是如此: ```python In [170]: rng = pd.period_range('2006', '2009', freq='A-DEC') In [171]: ts = pd.Series(np.random.randn(len(rng)), index=rng) In [172]: ts Out[172]: 2006 1.607578 2007 0.200381 2008 -0.834068 2009 -0.302988 Freq: A-DEC, dtype: float64 In [173]: ts.asfreq('M', how='start') Out[173]: 2006-01 1.607578 2007-01 0.200381 2008-01 -0.834068 2009-01 -0.302988 Freq: M, dtype: float64 ``` 这里,根据年度时期的第一个月,每年的时期被取代为每月的时期。如果我们想要每年的最后一个工作日,我们可以使用`B`频率,并指明想要该时期的末尾: ```python In [174]: ts.asfreq('B', how='end') Out[174]: 2006-12-29 1.607578 2007-12-31 0.200381 2008-12-31 -0.834068 2009-12-31 -0.302988 Freq: B, dtype: float64 ``` ## 按季度计算的时期频率 季度型数据在会计、金融等领域中很常见。许多季度型数据都会涉及“财年末”的概念,通常是一年 12 个月中某月的最后一个日历日或工作日。就这一点来说,时期`"2012Q4"`根据财年末的不同会有不同的含义。pandas 支持 12 种可能的季度型频率,即`Q-JAN`到`Q-DEC`: ```python In [175]: p = pd.Period('2012Q4', freq='Q-JAN') In [176]: p Out[176]: Period('2012Q4', 'Q-JAN') ``` 在以 1 月结束的财年中,`2012Q4`是从 11 月到 1 月(将其转换为日型频率就明白了)。图 11-2 对此进行了说明: ```python In [177]: p.asfreq('D', 'start') Out[177]: Period('2011-11-01', 'D') In [178]: p.asfreq('D', 'end') Out[178]: Period('2012-01-31', 'D') ``` ![图 11.2 不同季度型频率之间的转换](img/7178691-e2e1d52c9766f6ff.png) 因此,`Period`之间的算术运算会非常简单。例如,要获取该季度倒数第二个工作日下午 4 点的时间戳,你可以这样: ```python In [179]: p4pm = (p.asfreq('B', 'e') - 1).asfreq('T', 's') + 16 * 60 In [180]: p4pm Out[180]: Period('2012-01-30 16:00', 'T') In [181]: p4pm.to_timestamp() Out[181]: Timestamp('2012-01-30 16:00:00') ``` `period_range`可用于生成季度型范围。季度型范围的算术运算也跟上面是一样的: ```python In [182]: rng = pd.period_range('2011Q3', '2012Q4', freq='Q-JAN') In [183]: ts = pd.Series(np.arange(len(rng)), index=rng) In [184]: ts Out[184]: 2011Q3 0 2011Q4 1 2012Q1 2 2012Q2 3 2012Q3 4 2012Q4 5 Freq: Q-JAN, dtype: int64 In [185]: new_rng = (rng.asfreq('B', 'e') - 1).asfreq('T', 's') + 16 * 60 In [186]: ts.index = new_rng.to_timestamp() In [187]: ts Out[187]: 2010-10-28 16:00:00 0 2011-01-28 16:00:00 1 2011-04-28 16:00:00 2 2011-07-28 16:00:00 3 2011-10-28 16:00:00 4 2012-01-30 16:00:00 5 dtype: int64 ``` ## 将`Timestamp`转换为`Period`(及其反向过程) 通过使用`to_period`方法,可以将由时间戳索引的`Series`和`DataFrame`对象转换为以时期索引: ```python In [188]: rng = pd.date_range('2000-01-01', periods=3, freq='M') In [189]: ts = pd.Series(np.random.randn(3), index=rng) In [190]: ts Out[190]: 2000-01-31 1.663261 2000-02-29 -0.996206 2000-03-31 1.521760 Freq: M, dtype: float64 In [191]: pts = ts.to_period() In [192]: pts Out[192]: 2000-01 1.663261 2000-02 -0.996206 2000-03 1.521760 Freq: M, dtype: float64 ``` 由于时期指的是非重叠时间区间,因此对于给定的频率,一个时间戳只能属于一个时期。新`PeriodIndex`的频率默认是从时间戳推断而来的,你也可以指定任何别的频率。结果中允许存在重复时期: ```python In [193]: rng = pd.date_range('1/29/2000', periods=6, freq='D') In [194]: ts2 = pd.Series(np.random.randn(6), index=rng) In [195]: ts2 Out[195]: 2000-01-29 0.244175 2000-01-30 0.423331 2000-01-31 -0.654040 2000-02-01 2.089154 2000-02-02 -0.060220 2000-02-03 -0.167933 Freq: D, dtype: float64 In [196]: ts2.to_period('M') Out[196]: 2000-01 0.244175 2000-01 0.423331 2000-01 -0.654040 2000-02 2.089154 2000-02 -0.060220 2000-02 -0.167933 Freq: M, dtype: float64 ``` 要转换回时间戳,使用`to_timestamp`即可: ```python In [197]: pts = ts2.to_period() In [198]: pts Out[198]: 2000-01-29 0.244175 2000-01-30 0.423331 2000-01-31 -0.654040 2000-02-01 2.089154 2000-02-02 -0.060220 2000-02-03 -0.167933 Freq: D, dtype: float64 In [199]: pts.to_timestamp(how='end') Out[199]: 2000-01-29 0.244175 2000-01-30 0.423331 2000-01-31 -0.654040 2000-02-01 2.089154 2000-02-02 -0.060220 2000-02-03 -0.167933 Freq: D, dtype: float64 ``` ## 通过数组创建`PeriodIndex` 固定频率的数据集通常会将时间信息分开存放在多个列中。例如,在下面这个宏观经济数据集中,年度和季度就分别存放在不同的列中: ```python In [200]: data = pd.read_csv('examples/macrodata.csv') In [201]: data.head(5) Out[201]: year quarter realgdp realcons realinv realgovt realdpi cpi \ 0 1959.0 1.0 2710.349 1707.4 286.898 470.045 1886.9 28.98 1 1959.0 2.0 2778.801 1733.7 310.859 481.301 1919.7 29.15 2 1959.0 3.0 2775.488 1751.8 289.226 491.260 1916.4 29.35 3 1959.0 4.0 2785.204 1753.7 299.356 484.052 1931.3 29.37 4 1960.0 1.0 2847.699 1770.5 331.722 462.199 1955.5 29.54 m1 tbilrate unemp pop infl realint 0 139.7 2.82 5.8 177.146 0.00 0.00 1 141.7 3.08 5.1 177.830 2.34 0.74 2 140.5 3.82 5.3 178.657 2.74 1.09 3 140.0 4.33 5.6 179.386 0.27 4.06 4 139.6 3.50 5.2 180.007 2.31 1.19 In [202]: data.year Out[202]: 0 1959.0 1 1959.0 2 1959.0 3 1959.0 4 1960.0 5 1960.0 6 1960.0 7 1960.0 8 1961.0 9 1961.0 ... 193 2007.0 194 2007.0 195 2007.0 196 2008.0 197 2008.0 198 2008.0 199 2008.0 200 2009.0 201 2009.0 202 2009.0 Name: year, Length: 203, dtype: float64 In [203]: data.quarter Out[203]: 0 1.0 1 2.0 2 3.0 3 4.0 4 1.0 5 2.0 6 3.0 7 4.0 8 1.0 9 2.0 ... 193 2.0 194 3.0 195 4.0 196 1.0 197 2.0 198 3.0 199 4.0 200 1.0 201 2.0 202 3.0 Name: quarter, Length: 203, dtype: float64 ``` 通过将这些数组以及一个频率传入`PeriodIndex`,就可以将它们合并成`DataFrame`的一个索引: ```python In [204]: index = pd.PeriodIndex(year=data.year, quarter=data.quarter, .....: freq='Q-DEC') In [205]: index Out[205]: PeriodIndex(['1959Q1', '1959Q2', '1959Q3', '1959Q4', '1960Q1', '1960Q2', '1960Q3', '1960Q4', '1961Q1', '1961Q2', ... '2007Q2', '2007Q3', '2007Q4', '2008Q1', '2008Q2', '2008Q3', '2008Q4', '2009Q1', '2009Q2', '2009Q3'], dtype='period[Q-DEC]', length=203, freq='Q-DEC') In [206]: data.index = index In [207]: data.infl Out[207]: 1959Q1 0.00 1959Q2 2.34 1959Q3 2.74 1959Q4 0.27 1960Q1 2.31 1960Q2 0.14 1960Q3 2.70 1960Q4 1.21 1961Q1 -0.40 1961Q2 1.47 ... 2007Q2 2.75 2007Q3 3.45 2007Q4 6.38 2008Q1 2.82 2008Q2 8.53 2008Q3 -3.16 2008Q4 -8.79 2009Q1 0.94 2009Q2 3.37 2009Q3 3.56 Freq: Q-DEC, Name: infl, Length: 203, dtype: float64 ``` # 11.6 重采样及频率转换 重采样(resampling)指的是将时间序列从一个频率转换到另一个频率的处理过程。将高频率数据聚合到低频率称为降采样(downsampling),而将低频率数据转换到高频率则称为升采样(upsampling)。并不是所有的重采样都能被划分到这两个大类中。例如,将`W-WED`(每周三)转换为`W-FRI`既不是降采样也不是升采样。 pandas 对象都带有一个`resample`方法,它是各种频率转换工作的主力函数。`resample`有一个类似于`groupby`的 API,调用`resample`可以分组数据,然后会调用一个聚合函数: ```python In [208]: rng = pd.date_range('2000-01-01', periods=100, freq='D') In [209]: ts = pd.Series(np.random.randn(len(rng)), index=rng) In [210]: ts Out[210]: 2000-01-01 0.631634 2000-01-02 -1.594313 2000-01-03 -1.519937 2000-01-04 1.108752 2000-01-05 1.255853 2000-01-06 -0.024330 2000-01-07 -2.047939 2000-01-08 -0.272657 2000-01-09 -1.692615 2000-01-10 1.423830 ... 2000-03-31 -0.007852 2000-04-01 -1.638806 2000-04-02 1.401227 2000-04-03 1.758539 2000-04-04 0.628932 2000-04-05 -0.423776 2000-04-06 0.789740 2000-04-07 0.937568 2000-04-08 -2.253294 2000-04-09 -1.772919 Freq: D, Length: 100, dtype: float64 In [211]: ts.resample('M').mean() Out[211]: 2000-01-31 -0.165893 2000-02-29 0.078606 2000-03-31 0.223811 2000-04-30 -0.063643 Freq: M, dtype: float64 In [212]: ts.resample('M', kind='period').mean() Out[212]: 2000-01 -0.165893 2000-02 0.078606 2000-03 0.223811 2000-04 -0.063643 Freq: M, dtype: float64 ``` `resample`是一个灵活高效的方法,可用于处理非常大的时间序列。我将通过一系列的示例说明其用法。表 11-5 总结它的一些选项。 表 11-5 `resample`方法的参数 ![](img/7178691-b40a57086c904e83.png) ## 降采样 将数据聚合到规律的低频率是一件非常普通的时间序列处理任务。待聚合的数据不必拥有固定的频率,期望的频率会自动定义聚合的面元边界,这些面元用于将时间序列拆分为多个片段。例如,要转换到月度频率(`'M'`或`'BM'`),数据需要被划分到多个单月时间段中。各时间段都是半开放的。一个数据点只能属于一个时间段,所有时间段的并集必须能组成整个时间帧。在用`resample`对数据进行降采样时,需要考虑两样东西: - 各区间哪边是闭合的。 - 如何标记各个聚合面元,用区间的开头还是末尾。 为了说明,我们来看一些“1 分钟”数据: ```python In [213]: rng = pd.date_range('2000-01-01', periods=12, freq='T') In [214]: ts = pd.Series(np.arange(12), index=rng) In [215]: ts Out[215]: 2000-01-01 00:00:00 0 2000-01-01 00:01:00 1 2000-01-01 00:02:00 2 2000-01-01 00:03:00 3 2000-01-01 00:04:00 4 2000-01-01 00:05:00 5 2000-01-01 00:06:00 6 2000-01-01 00:07:00 7 2000-01-01 00:08:00 8 2000-01-01 00:09:00 9 2000-01-01 00:10:00 10 2000-01-01 00:11:00 11 Freq: T, dtype: int64 ``` 假设你想要通过求和的方式将这些数据聚合到“5 分钟”块中: ```python In [216]: ts.resample('5min', closed='right').sum() Out[216]: 1999-12-31 23:55:00 0 2000-01-01 00:00:00 15 2000-01-01 00:05:00 40 2000-01-01 00:10:00 11 Freq: 5T, dtype: int64 ``` 传入的频率将会以“5 分钟”的增量定义面元边界。默认情况下,面元的右边界是包含的,因此`00:00`到`00:05`的区间中是包含`00:05`的。传入`closed='left'`会让区间以左边界闭合: ```python In [217]: ts.resample('5min', closed='right').sum() Out[217]: 1999-12-31 23:55:00 0 2000-01-01 00:00:00 15 2000-01-01 00:05:00 40 2000-01-01 00:10:00 11 Freq: 5T, dtype: int64 ``` 如你所见,最终的时间序列是以各面元右边界的时间戳进行标记的。传入`label='right'`即可用面元的邮编界对其进行标记: ```python In [218]: ts.resample('5min', closed='right', label='right').sum() Out[218]: 2000-01-01 00:00:00 0 2000-01-01 00:05:00 15 2000-01-01 00:10:00 40 2000-01-01 00:15:00 11 Freq: 5T, dtype: int64 ``` 图 11-3 说明了“1 分钟”数据被转换为“5 分钟”数据的处理过程。 ![图 11-3 各种`closed`、`label`约定的“5 分钟”重采样演示](img/7178691-7a77f47844f2ee8c.png) 最后,你可能希望对结果索引做一些位移,比如从右边界减去一秒以便更容易明白该时间戳到底表示的是哪个区间。只需通过`loffset`设置一个字符串或日期偏移量即可实现这个目的: ```python In [219]: ts.resample('5min', closed='right', .....: label='right', loffset='-1s').sum() Out[219]: 1999-12-31 23:59:59 0 2000-01-01 00:04:59 15 In [219]: ts.resample('5min', closed='right', .....: label='right', loffset='-1s').sum() Out[219]: 1999-12-31 23:59:59 0 2000-01-01 00:04:59 15 ``` 此外,也可以通过调用结果对象的`shift`方法来实现该目的,这样就不需要设置`loffset`了。 ## OHLC 重采样 金融领域中有一种无所不在的时间序列聚合方式,即计算各面元的四个值:第一个值(`open`,开盘)、最后一个值(`close`,收盘)、最大值(`high`,最高)以及最小值(`low`,最低)。传入`how='ohlc'`即可得到一个含有这四种聚合值的`DataFrame`。整个过程很高效,只需一次扫描即可计算出结果: ```python In [220]: ts.resample('5min').ohlc() Out[220]: open high low close 2000-01-01 00:00:00 0 4 0 4 2000-01-01 00:05:00 5 9 5 9 2000-01-01 00:10:00 10 11 10 11 ``` ## 升采样和插值 在将数据从低频率转换到高频率时,就不需要聚合了。我们来看一个带有一些周型数据的`DataFrame`: ```python In [221]: frame = pd.DataFrame(np.random.randn(2, 4), .....: index=pd.date_range('1/1/2000', periods=2, .....: freq='W-WED'), .....: columns=['Colorado', 'Texas', 'New York', 'Ohio']) In [222]: frame Out[222]: Colorado Texas New York Ohio 2000-01-05 -0.896431 0.677263 0.036503 0.087102 2000-01-12 -0.046662 0.927238 0.482284 -0.867130 ``` 当你对这个数据进行聚合,每组只有一个值,这样就会引入缺失值。我们使用`asfreq`方法转换成高频,不经过聚合: ```python In [223]: df_daily = frame.resample('D').asfreq() In [224]: df_daily Out[224]: Colorado Texas New York Ohio 2000-01-05 -0.896431 0.677263 0.036503 0.087102 2000-01-06 NaN NaN NaN NaN 2000-01-07 NaN NaN NaN NaN 2000-01-08 NaN NaN NaN NaN 2000-01-09 NaN NaN NaN NaN 2000-01-10 NaN NaN NaN NaN 2000-01-11 NaN NaN NaN NaN 2000-01-12 -0.046662 0.927238 0.482284 -0.867130 ``` 假设你想要用前面的周型值填充“非星期三”。`resample`的填充和插值方式跟`fillna`和`reindex`的一样: ```python In [225]: frame.resample('D').ffill() Out[225]: Colorado Texas New York Ohio 2000-01-05 -0.896431 0.677263 0.036503 0.087102 2000-01-06 -0.896431 0.677263 0.036503 0.087102 2000-01-07 -0.896431 0.677263 0.036503 0.087102 2000-01-08 -0.896431 0.677263 0.036503 0.087102 2000-01-09 -0.896431 0.677263 0.036503 0.087102 2000-01-10 -0.896431 0.677263 0.036503 0.087102 2000-01-11 -0.896431 0.677263 0.036503 0.087102 2000-01-12 -0.046662 0.927238 0.482284 -0.867130 ``` 同样,这里也可以只填充指定的时期数(目的是限制前面的观测值的持续使用距离): ```python In [226]: frame.resample('D').ffill(limit=2) Out[226]: Colorado Texas New York Ohio 2000-01-05 -0.896431 0.677263 0.036503 0.087102 2000-01-06 -0.896431 0.677263 0.036503 0.087102 2000-01-07 -0.896431 0.677263 0.036503 0.087102 2000-01-08 NaN NaN NaN NaN 2000-01-09 NaN NaN NaN NaN 2000-01-10 NaN NaN NaN NaN 2000-01-11 NaN NaN NaN NaN 2000-01-12 -0.046662 0.927238 0.482284 -0.867130 ``` 注意,新的日期索引完全没必要跟旧的重叠: ```python In [227]: frame.resample('W-THU').ffill() Out[227]: Colorado Texas New York Ohio 2000-01-06 -0.896431 0.677263 0.036503 0.087102 2000-01-13 -0.046662 0.927238 0.482284 -0.867130 ``` ## 通过时期进行重采样 对那些使用时期索引的数据进行重采样与时间戳很像: ```python In [228]: frame = pd.DataFrame(np.random.randn(24, 4), .....: index=pd.period_range('1-2000', '12-2001', .....: freq='M'), .....: columns=['Colorado', 'Texas', 'New York', 'Ohio']) In [229]: frame[:5] Out[229]: Colorado Texas New York Ohio 2000-01 0.493841 -0.155434 1.397286 1.507055 2000-02 -1.179442 0.443171 1.395676 -0.529658 2000-03 0.787358 0.248845 0.743239 1.267746 2000-04 1.302395 -0.272154 -0.051532 -0.467740 2000-05 -1.040816 0.426419 0.312945 -1.115689 In [230]: annual_frame = frame.resample('A-DEC').mean() In [231]: annual_frame Out[231]: Colorado Texas New York Ohio 2000 0.556703 0.016631 0.111873 -0.027445 2001 0.046303 0.163344 0.251503 -0.157276 ``` 升采样要稍微麻烦一些,因为你必须决定在新频率中各区间的哪端用于放置原来的值,就像`asfreq`方法那样。`convention`参数默认为`'start'`,也可设置为`'end'`: ```python # Q-DEC: Quarterly, year ending in December In [232]: annual_frame.resample('Q-DEC').ffill() Out[232]: Colorado Texas New York Ohio 2000Q1 0.556703 0.016631 0.111873 -0.027445 2000Q2 0.556703 0.016631 0.111873 -0.027445 2000Q3 0.556703 0.016631 0.111873 -0.027445 2000Q4 0.556703 0.016631 0.111873 -0.027445 2001Q1 0.046303 0.163344 0.251503 -0.157276 2001Q2 0.046303 0.163344 0.251503 -0.157276 2001Q3 0.046303 0.163344 0.251503 -0.157276 2001Q4 0.046303 0.163344 0.251503 -0.157276 In [233]: annual_frame.resample('Q-DEC', convention='end').ffill() Out[233]: Colorado Texas New York Ohio 2000Q4 0.556703 0.016631 0.111873 -0.027445 2001Q1 0.556703 0.016631 0.111873 -0.027445 2001Q2 0.556703 0.016631 0.111873 -0.027445 2001Q3 0.556703 0.016631 0.111873 -0.027445 2001Q4 0.046303 0.163344 0.251503 -0.157276 ``` 由于时期指的是时间区间,所以升采样和降采样的规则就比较严格: - 在降采样中,目标频率必须是源频率的子时期(subperiod)。 - 在升采样中,目标频率必须是源频率的超时期(superperiod)。 如果不满足这些条件,就会引发异常。这主要影响的是按季、年、周计算的频率。例如,由`Q-MAR`定义的时间区间只能升采样为`A-MAR`、`A-JUN`、`A-SEP`、`A-DEC`等: ```python In [234]: annual_frame.resample('Q-MAR').ffill() Out[234]: Colorado Texas New York Ohio 2000Q4 0.556703 0.016631 0.111873 -0.027445 2001Q1 0.556703 0.016631 0.111873 -0.027445 2001Q2 0.556703 0.016631 0.111873 -0.027445 2001Q3 0.556703 0.016631 0.111873 -0.027445 2001Q4 0.046303 0.163344 0.251503 -0.157276 2002Q1 0.046303 0.163344 0.251503 -0.157276 2002Q2 0.046303 0.163344 0.251503 -0.157276 2002Q3 0.046303 0.163344 0.251503 -0.157276 ``` # 11.7 移动窗口函数 在移动窗口(可以带有指数衰减权数)上计算的各种统计函数也是一类常见于时间序列的数组变换。这样可以圆滑噪音数据或断裂数据。我将它们称为移动窗口函数(moving window function),其中还包括那些窗口不定长的函数(如指数加权移动平均)。跟其他统计函数一样,移动窗口函数也会自动排除缺失值。 开始之前,我们加载一些时间序列数据,将其重采样为工作日频率: ```python In [235]: close_px_all = pd.read_csv('examples/stock_px_2.csv', .....: parse_dates=True, index_col=0) In [236]: close_px = close_px_all[['AAPL', 'MSFT', 'XOM']] In [237]: close_px = close_px.resample('B').ffill() ``` 现在引入`rolling`运算符,它与`resample`和`groupby`很像。可以在`TimeSeries`或`DataFrame`以及一个窗口(表示期数,见图 11-4)上调用它: ```python In [238]: close_px.AAPL.plot() Out[238]: In [239]: close_px.AAPL.rolling(250).mean().plot() ``` ![图 11-4 苹果公司股价的 250 日均线](img/7178691-3327483eab730b09.png) 表达式`rolling(250)`与`groupby`很像,但不是对其进行分组,而是创建一个按照 250 天分组的滑动窗口对象。然后,我们就得到了苹果公司股价的 250 天的移动窗口。 默认情况下,`rolling`函数需要窗口中所有的值为非 NA 值。可以修改该行为以解决缺失数据的问题。其实,在时间序列开始处尚不足窗口期的那些数据就是个特例(见图 11-5): ```python In [241]: appl_std250 = close_px.AAPL.rolling(250, min_periods=10).std() In [242]: appl_std250[5:12] Out[242]: 2003-01-09 NaN 2003-01-10 NaN 2003-01-13 NaN 2003-01-14 NaN 2003-01-15 0.077496 2003-01-16 0.074760 2003-01-17 0.112368 Freq: B, Name: AAPL, dtype: float64 In [243]: appl_std250.plot() ``` ![图 11-5 苹果公司 250 日每日回报标准差](img/7178691-15f565bed1ccad09.png) 要计算扩展窗口平均(expanding window mean),可以使用`expanding`而不是`rolling`。“扩展”意味着,从时间序列的起始处开始窗口,增加窗口直到它超过所有的序列。`apple_std250`时间序列的扩展窗口平均如下所示: ```python In [244]: expanding_mean = appl_std250.expanding().mean() ``` 对`DataFrame`调用`rolling_mean`(以及与之类似的函数)会将转换应用到所有的列上(见图 11-6): ```python In [246]: close_px.rolling(60).mean().plot(logy=True) ``` ![图 11-6 各股价 60 日均线(对数 Y 轴)](img/7178691-979f748052b2279f.png) `rolling`函数也可以接受一个指定固定大小时间补偿字符串,而不是一组时期。这样可以方便处理不规律的时间序列。这些字符串也可以传递给`resample`。例如,我们可以计算 20 天的滚动均值,如下所示: ```python In [247]: close_px.rolling('20D').mean() Out[247]: AAPL MSFT XOM 2003-01-02 7.400000 21.110000 29.220000 2003-01-03 7.425000 21.125000 29.230000 2003-01-06 7.433333 21.256667 29.473333 2003-01-07 7.432500 21.425000 29.342500 2003-01-08 7.402000 21.402000 29.240000 2003-01-09 7.391667 21.490000 29.273333 2003-01-10 7.387143 21.558571 29.238571 2003-01-13 7.378750 21.633750 29.197500 2003-01-14 7.370000 21.717778 29.194444 2003-01-15 7.355000 21.757000 29.152000 ... ... ... ... 2011-10-03 398.002143 25.890714 72.413571 2011-10-04 396.802143 25.807857 72.427143 2011-10-05 395.751429 25.729286 72.422857 2011-10-06 394.099286 25.673571 72.375714 2011-10-07 392.479333 25.712000 72.454667 2011-10-10 389.351429 25.602143 72.527857 2011-10-11 388.505000 25.674286 72.835000 2011-10-12 388.531429 25.810000 73.400714 2011-10-13 388.826429 25.961429 73.905000 2011-10-14 391.038000 26.048667 74.185333 [2292 rows x 3 columns] ``` ## 指数加权函数 另一种使用固定大小窗口及相等权数观测值的办法是,定义一个衰减因子(decay factor)常量,以便使近期的观测值拥有更大的权数。衰减因子的定义方式有很多,比较流行的是使用时间间隔(span),它可以使结果兼容于窗口大小等于时间间隔的简单移动窗口(simple moving window)函数。 由于指数加权统计会赋予近期的观测值更大的权数,因此相对于等权统计,它能“适应”更快的变化。 除了`rolling`和`expanding`,pandas 还有`ewm`运算符。下面这个例子对比了苹果公司股价的 30 日移动平均和`span=30`的指数加权移动平均(如图 11-7 所示): ```python In [249]: aapl_px = close_px.AAPL['2006':'2007'] In [250]: ma60 = aapl_px.rolling(30, min_periods=20).mean() In [251]: ewma60 = aapl_px.ewm(span=30).mean() In [252]: ma60.plot(style='k--', label='Simple MA') Out[252]: In [253]: ewma60.plot(style='k-', label='EW MA') Out[253]: In [254]: plt.legend() ``` ![图 11-7 简单移动平均与指数加权移动平均](img/7178691-dae48defe3749fad.png) ## 二元移动窗口函数 有些统计运算(如相关系数和协方差)需要在两个时间序列上执行。例如,金融分析师常常对某只股票对某个参考指数(如标准普尔 500 指数)的相关系数感兴趣。要进行说明,我们先计算我们感兴趣的时间序列的百分数变化: ```python In [256]: spx_px = close_px_all['SPX'] In [257]: spx_rets = spx_px.pct_change() In [258]: returns = close_px.pct_change() ``` 调用`rolling`之后,`corr`聚合函数开始计算与`spx_rets`滚动相关系数(结果见图 11-8): ```python In [259]: corr = returns.AAPL.rolling(125, min_periods=100).corr(spx_rets) In [260]: corr.plot() ``` ![图 11-8 `AAPL` 6 个月的回报与标准普尔 500 指数的相关系数](img/7178691-e81e0f602b4db0ed.png) 假设你想要一次性计算多只股票与标准普尔 500 指数的相关系数。虽然编写一个循环并新建一个`DataFrame`不是什么难事,但比较啰嗦。其实,只需传入一个`TimeSeries`和一个`DataFrame`,`rolling_corr`就会自动计算`TimeSeries`(本例中就是`spx_rets`)与`DataFrame`各列的相关系数。结果如图 11-9 所示: ```python In [262]: corr = returns.rolling(125, min_periods=100).corr(spx_rets) In [263]: corr.plot() ``` ![图 11-9 3 只股票 6 个月的回报与标准普尔 500 指数的相关系数](img/7178691-0a54a028a62b9b50.png) ## 用户定义的移动窗口函数 `rolling_apply`函数使你能够在移动窗口上应用自己设计的数组函数。唯一要求的就是:该函数要能从数组的各个片段中产生单个值(即约简)。比如说,当我们用`rolling(...).quantile(q)`计算样本分位数时,可能对样本中特定值的百分等级感兴趣。`scipy.stats.percentileofscore`函数就能达到这个目的(结果见图 11-10): ```python In [265]: from scipy.stats import percentileofscore In [266]: score_at_2percent = lambda x: percentileofscore(x, 0.02) In [267]: result = returns.AAPL.rolling(250).apply(score_at_2percent) In [268]: result.plot() ``` ![图 11-10 `AAPL` 2% 回报率的百分等级(一年窗口期)](img/7178691-af49e84a90c23c1e.png) 如果你没安装 SciPy,可以使用`conda`或`pip`安装。 # 11.8 总结 与前面章节接触的数据相比,时间序列数据要求不同类型的分析和数据转换工具。 在接下来的章节中,我们将学习一些高级的 pandas 方法和如何开始使用建模库 statsmodels 和 scikit-learn。