第十一篇 时间序列

时间序列(time series)数据是⼀种重要的结构化数据形式,应⽤于多个领域,包括⾦融学、经济学、⽣态学、神经科学、物理学等。在多个时间点观察或测量到的任何事物都可以形成⼀段时间序列。很多时间序列是固定频率的,也就是说,数据点是根据某种规律定期出现的(⽐如每15秒、每5分钟、每⽉出现⼀次)。时间序列也可以是不定期的,没有固定的时间单位或单位之间的偏移量。时间序列数据的意义取决于具体的应⽤场景,主要有以下⼏种:
         时间戳(timestamp),特定的时刻。
         固定时期(period),如2007年1⽉或2010年全年。
         时间间隔(interval),由起始和结束时间戳表示。时期(period)可以被看做间隔(interval)的特例。
         实验或过程时间,每个时间点都是相对于特定起始时间的⼀个度量。例如,从放⼊烤箱时起,每秒钟饼⼲的直径。

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

注意:pandas也⽀持基于timedeltas的指数,它可以有效代表实验或经过的时间。可以在pandas的官方⽂档学习timedelta指数(http://pandas.pydata.org/)。

pandas提供了许多内置的时间序列处理⼯具和数据算法。因此,你可以⾼效处理⾮常⼤的时间序列,轻松地进⾏切⽚/切块、聚合、对定期/不定期的时间序列进⾏重采样等。有些⼯具特别适合⾦融和经济应⽤,你当然也可以⽤它们来分析服务器⽇志数据。

一、⽇期和时间数据类型及⼯具
Python标准库包含⽤于⽇期(date)和时间(time)数据的数据类型,⽽且还有⽇历⽅⾯的功能。我们主要会⽤到datetime、time以及calendar模块。datetime.datetime(也可以简写为datetime)是⽤得最多的数据类型
from datetime import datetime
now = datetime.now()
now         # 输出:datetime.datetime(2018, 12, 20, 14, 43, 59, 744323)
now.year, now.month, now.day        # 输出:(2018, 12, 20)

datetime以毫秒形式存储⽇期和时间。timedelta表示两个datetime对象之间的时间差
delta = datetime(2018, 8, 1) - datetime(2018, 3, 1, 9, 14)
delta                      # 输出:datetime.timedelta(152, 53160)
delta.days             # 输出:152
delta.seconds      # 输出:53160

可以给datetime对象加上(或减去)⼀个或多个timedelta,这样会产⽣⼀个新对象:
from datetime import timedelta
start = datetime(2016, 3, 2)
start + timedelta(12)            # 输出:datetime.datetime(2016, 3, 14, 0, 0),加12天
start - 2 * timedelta(12)       # 输出:datetime.datetime(2016, 2, 7, 0, 0),减24天
datetime模块中的数据类型参⻅表11-1。虽然本篇主要讲的是pandas数据类型和⾼级时间序列处理,但你肯定会在Python的其他地⽅遇到有关datetime的数据类型。

表11-1 datetime模块中的数据类型
类型              说明
date              以公历形式存储日历日期(年、月、日)
time              将时间存储为时、分、秒、毫秒
datetime       存储日期和时间
timedelta      表示两个datetime值之间的差(日、秒、毫秒)
tzinfo            存储时区信息的基本类型

1、字符串和datetime的相互转换
利⽤str或strftime⽅法(传⼊⼀个格式化字符串),datetime对象和pandas的Timestamp对象(稍后就会介绍)可以被格式化为字符串:
stamp = datetime(2016, 3, 2)
str(stamp)      # 输出 :'2016-03-02 00:00:00'
stamp.strftime('%Y-%m-%d')      # 输出: '2016-03-02'
表11-2列出了全部的格式化编码。

表11-2 datetime格式定义(兼容ISO C89)
代码          说明
%Y            4位数的年
%y            2位数的年
%m           2位数的月[01, 12]
%d            2位数的日[01, 31]
%H            时(24小时制)[00, 23]
%I              时(12小时制)[01, 12]
%M           2位数的分[00, 59]
%S             秒[00, 61](秒60和61用于闰秒)
%w            用整数表示的星期几 [ 0(星期天), 6]
%U            每年的第几周[00, 53]。星期天被认为是每周的第一天,每年的第一个
                  星期天之前的那几天被认为是“第0周”
%W           每年的第几周[00, 53]。星期一被认为是每周的第一天,每年的第一个
                  星期一之前的那几天被认为是“第0周”
%z             以+HHMM或-HHMM表示的UTC时区偏移量,如果时区为naive,则返回空字符串
%F             %Y-%m-%d简写形式,例如2012-4-18
%D            %m/%d/%y简写形式,例如04/18/12

datetime.strptime可以⽤这些格式化编码将字符串转换为⽇期
value = '2016-03-02'
datetime.strptime(value, '%Y-%m-%d')        # 输出:datetime.datetime(2016, 3, 2, 0, 0)

datestrs = ['5/4/2018', '8/10/2018']
[datetime.strptime(x, '%m/%d/%Y') for x in datestrs]        # 输出如下:使用推导式逐个转换
[datetime.datetime(2018, 5, 4, 0, 0), datetime.datetime(2018, 8, 10, 0, 0)]

datetime.strptime是通过已知格式进⾏⽇期解析的最佳⽅式。但是每次都要编写格式定义是很麻烦的事情,尤其
是对于⼀些常⻅的⽇期格式。这种情况下,你可以⽤dateutil这个第三⽅包中的parser.parse⽅法(pandas中已经
⾃动安装好):
from dateutil.parser import parse
parse('2016-3-2')       # 输出:datetime.datetime(2016, 3, 2, 0, 0)
dateutil可以解析⼏乎所有⼈类能够理解的⽇期表示形式
parse('Jan 31, 1997 10:45 PM')      # 输出:datetime.datetime(1997, 1, 31, 22, 45)
在国际通⽤的格式中,⽇出现在⽉的前⾯很普遍,传⼊dayfirst=True即可解决这个问题:
parse('7/8/2018', dayfirst=True)    # 输出:datetime.datetime(2018, 8, 7, 0, 0)

pandas通常是⽤于处理成组⽇期的,不管这些⽇期是DataFrame的轴索引还是列。to_datetime⽅法可以解析多种不同的⽇期表示形式。对标准⽇期格式(如ISO8601)的解析⾮常快:
datestrs = ['2018-07-10 12:00:00', '2018-10-16 00:00:00']
pd.to_datetime(datestrs)        # 输出如下:
DatetimeIndex(['2018-07-10 12:00:00', '2018-10-16 00:00:00'], dtype='datetime64[ns]', freq=None)
它还可以处理缺失值(None、空字符串等):
idx = pd.to_datetime(datestrs + [None])
idx         # 输出如下:
DatetimeIndex(['2018-07-10 12:00:00', '2018-10-16 00:00:00', 'NaT'], dtype='datetime64[ns]', freq=None)
idx[2]          # 输出:NaT
pd.isnull(idx)          # 输出:array([False, False,  True])
NaT(Not a Time)是pandas中时间戳数据的null值

注意:dateutil.parser是⼀个实⽤但不完美的⼯具。⽐如说,它会把⼀些原本不是⽇期的字符串认作是⽇期
(⽐如"42"会被解析为2042年的今天)。

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

表11-3 特定于当前环境的⽇期格式
代码          说明
%a            星期几的简写
%A           星期几的全称
%b           月份的简写
%B           月份的全称
%c           完整的日期和时间,例如"Tue 01 May 2012 04:20:57 PM"
%p           不同环境中的AM或PM
%x           适合于当前环境的日期格式,例如,在USA,“May 1, 2012”会生产"05/01/2012"
%X           适合于当前环境的时间格式,例如 "04:24:12 PM"

二、时间序列基础
pandas最基本的时间序列类型就是以时间戳(通常以Python字符串或datatime对象表示)为索引的Series:
from datetime import datetime
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)]
ts = pd.Series(np.random.randn(6), index=dates)
ts               # 输出如下:
2011-01-02    0.758055
2011-01-05    0.197013
2011-01-07   -1.014007
2011-01-08   -0.083171
2011-01-10    1.299712
2011-01-12   -0.799759
dtype: float64
这些datetime对象实际上是被放在⼀个DatetimeIndex中的:
ts.index            # 输出如下:
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⼀样,不同索引的时间序列之间的算术运算会⾃动按⽇期对⻬
ts + ts[::2]            # 输出如下:
2011-01-02    1.516111
2011-01-05           NaN
2011-01-07   -2.028014
2011-01-08           NaN
2011-01-10    2.599424
2011-01-12           NaN
dtype: float64
ts[::2] 是每隔两个取⼀个。
pandas⽤NumPy的datetime64数据类型以纳秒形式存储时间戳
ts.index.dtype              # 输出:dtype('<M8[ns]')

DatetimeIndex中的各个标量值是pandas的Timestamp对象
stamp = ts.index[0]
stamp           # 输出:Timestamp('2011-01-02 00:00:00')
只要有需要,TimeStamp可以随时⾃动转换为datetime对象。此外,它还可以存储频率信息(如果有的话),且知道如何执⾏时区转换以及其他操作。稍后将对此进⾏详细讲解。

1、索引、选取、⼦集构造
当你根据标签索引选取数据时,时间序列和其它的pandas.Series很像:
stamp = ts.index[2]
ts[stamp]          # 输出: -1.014007193261713
还有⼀种更为⽅便的⽤法:传⼊⼀个可以被解释为⽇期的字符串
ts['1/10/2011']         # 输出:1.2997122200536004
ts['20110110']           # 输出:1.2997122200536004

对于较⻓的时间序列,只需传⼊“年”或“年⽉”即可轻松选取数据的切⽚
longer_ts = pd.Series(np.random.randn(1000),
                                     index=pd.date_range('1/1/2000', periods=1000))
longer_ts           # 输出如下:
2000-01-01    0.297975
2000-01-02   -1.153909
2000-01-03    0.090682
2000-01-04    0.552662
2000-01-05    1.635998
                 ...
2002-09-22    0.039126
2002-09-23    0.379367
2002-09-24   -0.037303
2002-09-25    1.009411
2002-09-26   -0.360946
Freq: D, Length: 1000, dtype: float64
longer_ts['2001']           # 输出如下:选取的切片是一年
2001-01-01   -0.069738
2001-01-02   -1.858975
2001-01-03   -0.021057
2001-01-04   -0.928934
2001-01-05   -1.600358
                 ...
2001-12-27   -0.133934
2001-12-28    0.216217
2001-12-29   -0.888471
2001-12-30    0.103915
2001-12-31   -0.128421
Freq: D, Length: 365, dtype: float64
这⾥,字符串“2001”被解释成年,并根据它选取时间区间。指定⽉也同样奏效
longer_ts['2001-05']            # 输出如下:选取的切片是一月
2001-05-01   -1.132128
2001-05-02    0.730976
2001-05-03   -1.047071
2001-05-04   -0.687509
2001-05-05   -0.618300
                 ...
2001-05-27    0.389774
2001-05-28    1.751153
2001-05-29    0.940651
2001-05-30    0.631526
2001-05-31    0.000392
Freq: D, Length: 31, dtype: float64
datetime对象也可以进⾏切⽚
ts[datetime(2011, 1, 7):]       # 输出如下:
2011-01-07   -1.014007
2011-01-08   -0.083171
2011-01-10    1.299712
2011-01-12   -0.799759
dtype: float64
由于⼤部分时间序列数据都是按照时间先后排序的,因此你也可以⽤不存在于该时间序列中的时间戳对其进⾏切⽚(即范围查询):
ts                  # 输出如下:
2011-01-02    0.758055
2011-01-05    0.197013
2011-01-07   -1.014007
2011-01-08   -0.083171
2011-01-10    1.299712
2011-01-12   -0.799759
dtype: float64
ts['1/6/2011':'1/11/2011']          # 输出如下:切片的起始时间不在时间序列中
2011-01-07   -1.014007
2011-01-08   -0.083171
2011-01-10    1.299712
dtype: float64
跟之前⼀样,你可以传⼊字符串⽇期、datetime或Timestamp。注意,这样切⽚所产⽣的是源时间序列的视图,跟NumPy数组的切⽚运算是⼀样的。这意味着,没有数据被复制,对切⽚进⾏修改会反映到原始数据上。

此外,还有⼀个等价的实例⽅法也可以截取两个⽇期之间TimeSeries:
ts.truncate(after='1/9/2011')           # 输出如下:实例方法truncate()
2011-01-02    0.758055
2011-01-05    0.197013
2011-01-07   -1.014007
2011-01-08   -0.083171
dtype: float64

这些操作对DataFrame也有效。例如,对DataFrame的⾏进⾏索引:
dates = pd.date_range('1/1/2000', periods=100, freq='W-WED')
long_df = pd.DataFrame(np.random.randn(100, 4),
                                         index=dates,
                                         columns=['Colorado', 'Texas', 'New York', 'Ohio'])
long_df.loc['5-2001']            # 输出如下:
                      Colorado       Texas   New York        Ohio
2001-05-02 -0.439088  0.405889 -0.758566 -1.257653
2001-05-09  1.048846 -0.822053  0.250146  1.263414
2001-05-16 -1.811179  1.282718 -0.274745 -2.329642
2001-05-23 -1.590255  1.057494 -1.306906  0.308010
2001-05-30 -2.094225  0.816898 -0.576982 -0.257714

2、带有重复索引的时间序列
在某些应⽤场景中,可能会存在多个观测数据落在同⼀个时间点上的情况。下⾯就是⼀个例⼦:
dates = pd.DatetimeIndex(['1/1/2000', '1/2/2000', '1/2/2000',
                                              '1/2/2000', '1/3/2000'])
dup_ts = pd.Series(np.arange(5), index=dates)
dup_ts          # 输出如下:
2000-01-01    0
2000-01-02    1
2000-01-02    2
2000-01-02    3
2000-01-03    4
dtype: int32
通过检查索引的is_unique属性,我们就可以知道它是不是唯⼀的:
dup_ts.index.is_unique          # 输出:False
对这个时间序列进⾏索引,要么产⽣标量值,要么产⽣切⽚,具体要看所选的时间点是否重复:
dup_ts['1/3/2000']          # 输出:4,(不重复,产生标量值)
dup_ts['1/2/2000']           # 输出如下:(重复,duplicated,产生的是切片)
2000-01-02    1
2000-01-02    2
2000-01-02    3
dtype: int32
假设你想要对具有⾮唯⼀时间戳的数据进⾏聚合。⼀个办法是使⽤groupby,并传⼊level=0
grouped = dup_ts.groupby(level=0)       # level=0,对第1层标签分组
grouped.mean()             # 输出如下:分组平均数
2000-01-01    0
2000-01-02    2
2000-01-03    4
dtype: int32
grouped.count()             # 输出如下:分组统计
2000-01-01    1
2000-01-02    3
2000-01-03    1
dtype: int64

三、⽇期的范围、频率以及移动
pandas中的原⽣时间序列⼀般被认为是不规则的,也就是说,它们没有固定的频率。对于⼤部分应⽤程序⽽⾔,这是⽆所谓的。但是,它常常需要以某种相对固定的频率进⾏分析,⽐如每⽇、每⽉、每15分钟等(这样⾃然会在时间序列中引⼊缺失值)。幸运的是,pandas有⼀整套标准时间序列频率以及⽤于重采样、频率推断、⽣成固定频率⽇期范围的⼯具。例如,我们可以将之前那个时间序列转换为⼀个具有固定频率(每⽇)的时间序列,只需调⽤resample即可:
ts                      # 现在有ts变量的内容如下:
2011-01-02   -1.418847
2011-01-05   -1.585231
2011-01-07    1.136025
2011-01-08    0.586130
2011-01-10   -0.789736
2011-01-12    0.128133
dtype: float64
resampler = ts.resample('D')    # 字符串“D”是每天的意思。调用resample()方法重采样
频率的转换(或重采样)是⼀个⽐较⼤的主题,在本篇第六小节讨论。这⾥,先了解如何使⽤基本的频率和它的倍数。

1、⽣成⽇期范围
pandas.date_range可⽤于根据指定的频率⽣成指定⻓度的DatetimeIndex
index = pd.date_range('2012-4-1', '2012-6-1')       # 日期范围。默认按天计算时间点
index               # 输出如下:
DatetimeIndex(['2012-04-01', '2012-04-02', '2012-04-03', '2012-04-04',
                           '2012-04-05', '2012-04-06', '2012-04-07', '2012-04-08',
                           '2012-04-09', '2012-04-10', '2012-04-11', '2012-04-12',
                           '2012-04-13', '2012-04-14', '2012-04-15', '2012-04-16',
                           '2012-04-17', '2012-04-18', '2012-04-19', '2012-04-20',
                           '2012-04-21', '2012-04-22', '2012-04-23', '2012-04-24',
                           '2012-04-25', '2012-04-26', '2012-04-27', '2012-04-28',
                           '2012-04-29', '2012-04-30', '2012-05-01', '2012-05-02',
                           '2012-05-03', '2012-05-04', '2012-05-05', '2012-05-06',
                           '2012-05-07', '2012-05-08', '2012-05-09', '2012-05-10',
                           '2012-05-11', '2012-05-12', '2012-05-13', '2012-05-14',
                           '2012-05-15', '2012-05-16', '2012-05-17', '2012-05-18',
                           '2012-05-19', '2012-05-20', '2012-05-21', '2012-05-22',
                           '2012-05-23', '2012-05-24', '2012-05-25', '2012-05-26',
                           '2012-05-27', '2012-05-28', '2012-05-29', '2012-05-30',
                           '2012-05-31', '2012-06-01'],
                           dtype='datetime64[ns]', freq='D')
默认情况下,date_range会产⽣按天计算的时间点。如果只传⼊起始或结束⽇期,那就还得传⼊⼀个表示⼀段时间的数字
pd.date_range(start='2012-4-1', periods=20)         # 输出如下:按天计算,向前计算20天
DatetimeIndex(['2012-04-01', '2012-04-02', '2012-04-03', '2012-04-04',
                           '2012-04-05', '2012-04-06', '2012-04-07', '2012-04-08',
                           '2012-04-09', '2012-04-10', '2012-04-11', '2012-04-12',
                           '2012-04-13', '2012-04-14', '2012-04-15', '2012-04-16',
                           '2012-04-17', '2012-04-18', '2012-04-19', '2012-04-20'],
                         dtype='datetime64[ns]', freq='D')
pd.date_range(end='2012-6-1', periods=20)           # 输出如下:按天计算,向后计算20天
DatetimeIndex(['2012-05-13', '2012-05-14', '2012-05-15', '2012-05-16',
                          '2012-05-17', '2012-05-18', '2012-05-19', '2012-05-20',
                          '2012-05-21', '2012-05-22', '2012-05-23', '2012-05-24',
                          '2012-05-25', '2012-05-26', '2012-05-27', '2012-05-28',
                          '2012-05-29', '2012-05-30', '2012-05-31', '2012-06-01'],
                      dtype='datetime64[ns]', freq='D')
起始和结束⽇期定义了⽇期索引的严格边界。例如,如果你想要⽣成⼀个由每⽉最后⼀个⼯作⽇组成的⽇期索引,可以传⼊"BM"频率(表示business end of month,表11-4是频率列表),这样就只会包含时间间隔内(或刚好在边界上的)符合频率要求的⽇期:
pd.date_range('2000-1-1', '2001-1-1', freq='BM')        # 输出如下:频率是每月最后一个工作日
DatetimeIndex(['2000-01-31', '2000-02-29', '2000-03-31', '2000-04-28',
                          '2000-05-31', '2000-06-30', '2000-07-31', '2000-08-31',
                          '2000-09-29', '2000-10-31', '2000-11-30', '2000-12-29'],
                         dtype='datetime64[ns]', freq='BM')

表11-4 基本的时间序列频率(不完整)(下面的频率用于date_range()函数的freq参数)

image

date_range默认会保留起始和结束时间戳的时间信息(如果有的话):
pd.date_range('2016-3-2 8:30:15', periods=5)                # 输出如下:注意默认的频率是天('D')
DatetimeIndex(['2016-03-02 08:30:15', '2016-03-03 08:30:15',
                          '2016-03-04 08:30:15', '2016-03-05 08:30:15',
                          '2016-03-06 08:30:15'],
                         dtype='datetime64[ns]', freq='D')
有时,虽然起始和结束⽇期带有时间信息,但你希望产⽣⼀组被规范化(normalize)到午夜的时间戳。normalize选项即可实现该功能
pd.date_range('2016-3-2 8:30:15', periods=5, normalize=True)            # 规范化的时间戳显示:
DatetimeIndex(['2016-03-02', '2016-03-03', '2016-03-04', '2016-03-05',
                          '2016-03-06'],
                         dtype='datetime64[ns]', freq='D')

2、频率和⽇期偏移量
pandas中的频率是由⼀个基础频率(base frequency)和⼀个乘数组成的基础频率通常以⼀个字符串别名表示,⽐如"M"表示每⽉,"H"表示每⼩时。对于每个基础频率,都有⼀个被称为⽇期偏移量(date offset)的对象与之对应。例如,按⼩时计算的频率可以⽤Hour类表示:
from pandas.tseries.offsets import Hour, Minute
hour = Hour()
hour        # 输出hour对象: <Hour>
传⼊⼀个整数即可定义偏移量的倍数:
four_hours = Hour(4)
four_hours              # 输出: <4 * Hours>
⼀般来说,⽆需明确创建这样的对象,只需使⽤诸如"H"或"4H"这样的字符串别名即可。在基础频率前⾯放上⼀个整数即可创建倍数
pd.date_range('2018-1-1', '2018-1-3 23:59', freq='4h')      # 输出如下:
DatetimeIndex(['2018-01-01 00:00:00', '2018-01-01 04:00:00',
                          '2018-01-01 08:00:00', '2018-01-01 12:00:00',
                          '2018-01-01 16:00:00', '2018-01-01 20:00:00',
                          '2018-01-02 00:00:00', '2018-01-02 04:00:00',
                          '2018-01-02 08:00:00', '2018-01-02 12:00:00',
                          '2018-01-02 16:00:00', '2018-01-02 20:00:00',
                          '2018-01-03 00:00:00', '2018-01-03 04:00:00',
                          '2018-01-03 08:00:00', '2018-01-03 12:00:00',
                          '2018-01-03 16:00:00', '2018-01-03 20:00:00'],
                         dtype='datetime64[ns]', freq='4H')

⼤部分偏移量对象都可通过加法进⾏连接
Hour(2) + Minute(30)                # 输出:<150 * Minutes>
同理,你也可以传⼊频率字符串(如"2h30min"),这种字符串可以被⾼效地解析为等效的表达式:
pd.date_range('2018-1-1', periods=10, freq='1h30min')       # 输出如下:
DatetimeIndex(['2018-01-01 00:00:00', '2018-01-01 01:30:00',
                          '2018-01-01 03:00:00', '2018-01-01 04:30:00',
                          '2018-01-01 06:00:00', '2018-01-01 07:30:00',
                          '2018-01-01 09:00:00', '2018-01-01 10:30:00',
                          '2018-01-01 12:00:00', '2018-01-01 13:30:00'],
                        dtype='datetime64[ns]', freq='90T')

有些频率所描述的时间点并不是均匀分隔的。例如,"M"(⽇历⽉末)和"BM"(每⽉最后⼀个⼯作⽇)就取决于每⽉的天数,对于后者,还要考虑⽉末是不是周末。由于没有更好的术语,我将这些称为锚点偏移量(anchored offset)。
表11-4列出了pandas中的频率代码和⽇期偏移量类。
可根据实际需求⾃定义⼀些频率类以便提供pandas所没有的⽇期逻辑。

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

3、WOM⽇期
WOM(Week Of Month)是⼀种⾮常实⽤的频率类,它以WOM开头。它使你能获得诸如“每⽉第3个星期五”之类的⽇期:
rng = pd.date_range('2018-1-1', '2018-9-1', freq='WOM-3FRI')      # 每月第三个星期五
list(rng)                           # 输出如下:
[Timestamp('2018-01-19 00:00:00', freq='WOM-3FRI'),
  Timestamp('2018-02-16 00:00:00', freq='WOM-3FRI'),
  Timestamp('2018-03-16 00:00:00', freq='WOM-3FRI'),
  Timestamp('2018-04-20 00:00:00', freq='WOM-3FRI'),
  Timestamp('2018-05-18 00:00:00', freq='WOM-3FRI'),
  Timestamp('2018-06-15 00:00:00', freq='WOM-3FRI'),
  Timestamp('2018-07-20 00:00:00', freq='WOM-3FRI'),
  Timestamp('2018-08-17 00:00:00', freq='WOM-3FRI')]

4、移动(超前和滞后)数据
移动(shifting)指的是沿着时间轴将数据前移或后移。Series和DataFrame都有⼀个shift⽅法⽤于执⾏单纯的前移或后移操作,保持索引不变
ts = pd.Series(np.random.randn(4),
                       index=pd.date_range('1/1/2000', periods=4, freq='M'))
ts                       # 输出如下:
2000-01-31   -0.832222
2000-02-29    0.341062
2000-03-31   -1.939174
2000-04-30    0.861032
Freq: M, dtype: float64
ts.shift(2)             # 输出如下:沿时间轴将数据向前移动
2000-01-31           NaN
2000-02-29           NaN
2000-03-31   -0.832222
2000-04-30    0.341062
Freq: M, dtype: float64
ts.shift(-2)           # 输出如下:沿时间轴将数据向后移动
2000-01-31   -1.939174
2000-02-29     0.861032
2000-03-31            NaN
2000-04-30            NaN
Freq: M, dtype: float64
当我们这样进⾏移动时,就会在时间序列的前⾯或后⾯产⽣缺失数据

shift通常⽤于计算⼀个时间序列或多个时间序列(如DataFrame的列)中的百分⽐变化。可以这样表达:
ts / ts.shift(1) - 1

由于单纯的移位操作不会修改索引,所以部分数据会被丢弃。因此,如果频率已知,则可以将其传给shift以便实现对时间戳进⾏位移⽽不是对数据进⾏简单位移:
ts.shift(2, freq='M')               # 输出如下:已知频率的情况,对时间戳进行位移
2000-03-31   -0.832222
2000-04-30    0.341062
2000-05-31   -1.939174
2000-06-30    0.861032
Freq: M, dtype: float64
这⾥还可以使⽤其他频率,于是就能⾮常灵活地对数据进⾏超前和滞后处理
ts.shift(3, freq='D')               # 输出如下:向前移3天
2000-02-03   -0.832222
2000-03-03    0.341062
2000-04-03   -1.939174
2000-05-03    0.861032
dtype: float64
ts.shift(1, freq='90T')             # 输出如下:向前移动90分钟
2000-01-31 01:30:00   -0.832222
2000-02-29 01:30:00    0.341062
2000-03-31 01:30:00   -1.939174
2000-04-30 01:30:00    0.861032
Freq: M, dtype: float64

5、通过偏移量对⽇期进⾏位移
pandas的⽇期偏移量还可以⽤在datetime或Timestamp对象上:
from pandas.tseries.offsets import Day, MonthEnd
now = datetime(2011, 11, 17)
now + 3 * Day()         # 输出如下:时间戳,使用now + Day(3)是一样的结果
Timestamp('2011-11-20 00:00:00')

如果加的是锚点偏移量(⽐如MonthEnd),第⼀次增量会将原⽇期向前滚动到符合频率规则的下⼀个⽇期:
now + MonthEnd()         # 输出如下:MonthEnd是偏移到每月的最后一天
Timestamp('2011-11-30 00:00:00')
now + MonthEnd(2)        # 输出如下:
Timestamp('2011-12-31 00:00:00')

通过锚点偏移量的rollforward和rollback⽅法,可明确地将⽇期向前或向后“滚动”:
offset = MonthEnd()
offset.rollforward(now)                # 输出如下:将now对应的日期向前滚动到月末
Timestamp('2011-11-30 00:00:00')
offset.rollback(now)                # 输出如下:将now对应的日期向后滚动到上一个月的月末
Timestamp('2011-10-31 00:00:00')

⽇期偏移量还有⼀个巧妙的⽤法,即结合groupby使⽤这两个“滚动”⽅法
ts = pd.Series(np.random.randn(20),
                        index=pd.date_range('1/15/2000', periods=20, freq='4D'))
ts                       # 输出如下:
2000-01-15   -0.476511
2000-01-19    0.646956
2000-01-23    0.157716
2000-01-27   -1.207581
2000-01-31   -0.629533
                 ...
2000-03-15    0.082505
2000-03-19   -0.154366
2000-03-23    1.368601
2000-03-27   -2.117017
2000-03-31   -1.594271
Freq: 4D, Length: 20, dtype: float64
ts.groupby(offset.rollforward).mean()      # 输出如下:由于offset.rollforward指向月末,计算每月的平均数
2000-01-31   -0.301791
2000-02-29   -0.001096
2000-03-31   -0.129890
dtype: float64
当然,更简单、更快速地实现该功能的办法是使⽤resample(后面六⼩节详细介绍):
ts.resample('M').mean()              # 输出如下:
2000-01-31   -0.301791
2000-02-29   -0.001096
2000-03-31   -0.129890
Freq: M, dtype: float64

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

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

有关pytz库的更多信息,可查阅其⽂档。由于pandas包装了pytz的功能,因此可不⽤记忆其API,只要记得时区的名称即可。时区名可以在shell中看到,也可以通过⽂档查看:
import pytz
pytz.common_timezones[-5:]          # 输出如下:
['US/Eastern', 'US/Hawaii', 'US/Mountain', 'US/Pacific', 'UTC']
要从pytz中获取时区对象,使⽤pytz.timezone即可
tzz = pytz.timezone('Asia/Shanghai')
tzz         # 输出: <DstTzInfo 'Asia/Shanghai' LMT+8:06:00 STD>
pandas中的⽅法既可以接受时区名也可以接受这些对象

1、时区本地化和转换
默认情况下,pandas中的时间序列是单纯的(naive)时区。看看下⾯这个时间序列:
rng = pd.date_range('3/9/2012 9:30', periods=6, freq='D')
ts = pd.Series(np.random.randn(len(rng)), index=rng)
ts                      # 输出如下:
2012-03-09 09:30:00   -0.604688
2012-03-10 09:30:00   -0.824233
2012-03-11 09:30:00   -1.376939
2012-03-12 09:30:00    0.660257
2012-03-13 09:30:00   -0.362623
2012-03-14 09:30:00   -1.885881
Freq: D, dtype: float64
其索引的tz字段为None
print(ts.index.tz)                  # 输出:None
可以⽤时区集⽣成⽇期范围
pd.date_range('3/9/2012 9:30', periods=10, freq='D', tz='UTC')          # 输出如下:
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⽅法处理的:
ts                      # 有ts如下:
2012-03-09 09:30:00   -0.604688
2012-03-10 09:30:00   -0.824233
2012-03-11 09:30:00   -1.376939
2012-03-12 09:30:00    0.660257
2012-03-13 09:30:00   -0.362623
2012-03-14 09:30:00   -1.885881
Freq: D, dtype: float64
ts_utc = ts.tz_localize('UTC')
ts_utc                  # 输出如下:
2012-03-09 09:30:00+00:00   -0.604688
2012-03-10 09:30:00+00:00   -0.824233
2012-03-11 09:30:00+00:00   -1.376939
2012-03-12 09:30:00+00:00    0.660257
2012-03-13 09:30:00+00:00   -0.362623
2012-03-14 09:30:00+00:00   -1.885881
Freq: D, dtype: float64
ts_utc.index            # 输出如下:
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将其转换到别的时区了:
ts_utc.tz_convert('Asia/Shanghai')              # 输出省略
ts_utc.tz_convert('America/New_York')           # 输出如下:
2012-03-09 04:30:00-05:00   -0.604688
2012-03-10 04:30:00-05:00   -0.824233
2012-03-11 05:30:00-04:00   -1.376939
2012-03-12 05:30:00-04:00    0.660257
2012-03-13 05:30:00-04:00   -0.362623
2012-03-14 05:30:00-04:00   -1.885881
Freq: D, dtype: float64
对于上⾯这种时间序列(它跨越了美国东部时区的夏令时转变期),我们可以将其本地化到EST,然后转换为UTC或柏林时间:
ts_eastern = ts.tz_localize('America/New_York')      # tz_localize()从本地转换到指定时区
ts_eastern.tz_convert('UTC')                     # 输出如下:转换为UTC时间
2012-03-09 14:30:00+00:00   -0.604688
2012-03-10 14:30:00+00:00   -0.824233
2012-03-11 13:30:00+00:00   -1.376939
2012-03-12 13:30:00+00:00    0.660257
2012-03-13 13:30:00+00:00   -0.362623
2012-03-14 13:30:00+00:00   -1.885881
Freq: D, dtype: float64
ts_eastern.tz_convert('Europe/Berlin')          # 输出如下:转换为柏林时间
2012-03-09 15:30:00+01:00   -0.604688
2012-03-10 15:30:00+01:00   -0.824233
2012-03-11 14:30:00+01:00   -1.376939
2012-03-12 14:30:00+01:00    0.660257
2012-03-13 14:30:00+01:00   -0.362623
2012-03-14 14:30:00+01:00   -1.885881
Freq: D, dtype: float64

tz_localize和tz_convert也是DatetimeIndex的实例⽅法
ts.index.tz_localize('Asia/Shanghai')           # 将ts.index的时间序列本地化为上海时间
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')
注意:对单纯时间戳的本地化操作还会检查夏令时转变期附近容易混淆或不存在的时间。

2、操作时区意识型Timestamp对象(时区操作--已知时间戳对象)
跟时间序列和⽇期范围差不多,独⽴的Timestamp对象也能被从单纯型(naive)本地化为已知时区(time zone-aware),并从⼀个时区转换到另⼀个时区:
stamp = pd.Timestamp('2011-03-12 04:00')
stamp_utc = stamp.tz_localize('utc')            # 本地化为已知时区
stamp_utc.tz_convert('America/New_York')         # 输出如下:转换为纽约时间
Timestamp('2011-03-11 23:00:00-0500', tz='America/New_York')

已知时区Timestamp对象在内部保存了⼀个UTC时间戳值(⾃UNIX纪元(1970年1⽉1⽇)算起的纳秒数)。这个UTC值在时区转换过程中是不会发⽣变化的:
stamp_utc.value         # 输出:1299902400000000000
stamp_utc.tz_convert('America/New_York').value              # 输出:1299902400000000000

当使⽤pandas的DateOffset对象执⾏时间算术运算时,运算过程会⾃动关注是否存在夏令时转变期。这⾥,我们创建了在DST转变之前的时间戳。⾸先,来看夏令时转变前的30分钟:
from pandas.tseries.offsets import Hour
stamp = pd.Timestamp('2012-3-12 01:30', tz='US/Eastern')
stamp                   # 输出:Timestamp('2012-03-12 01:30:00-0400', tz='US/Eastern')
stamp + Hour()          # 输出如下:
Timestamp('2012-03-12 02:30:00-0400', tz='US/Eastern')
然后,夏令时转变前90分钟:
stamp = pd.Timestamp('2012-11-04 00:30', tz='US/Eastern')
stamp                   # 输出如下:
Timestamp('2012-11-04 00:30:00-0400', tz='US/Eastern')
stamp + 2 * Hour()      # 输出如下:
Timestamp('2012-11-04 01:30:00-0500', tz='US/Eastern')

3、不同时区之间的运算
如果两个时间序列的时区不同,在将它们合并到⼀起时,最终结果就会是UTC。由于时间戳其实是以UTC存储的,所以这是⼀个很简单的运算,并不需要发⽣任何转换:
rng = pd.date_range('3/7/2012 9:30', periods=10, freq='B')
ts = pd.Series(np.random.randn(len(rng)), index=rng)
ts                      # 输出如下:
2012-03-07 09:30:00   -2.376522
2012-03-08 09:30:00   -0.666842
2012-03-09 09:30:00   -0.261704
2012-03-12 09:30:00   -0.552518
2012-03-13 09:30:00   -0.595424
2012-03-14 09:30:00    0.055178
2012-03-15 09:30:00   -0.147100
2012-03-16 09:30:00    0.150247
2012-03-19 09:30:00    1.349859
2012-03-20 09:30:00    0.235983
Freq: B, dtype: float64
ts1 = ts[:7].tz_localize('Europe/London')      # 本地化时区为欧洲伦敦时区
ts2 = ts1[2:].tz_convert('Europe/Moscow')    # 反转为莫斯科时区
result = ts1 + ts2
result.index            # 输出如下:结果时区为UTC
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')

五、时期及其算术运算
时期(period)表示的是时间区间,⽐如数⽇、数⽉、数季、数年等。Period类所表示的就是这种数据类型,其构造函数需要⽤到⼀个字符串或整数,以及表11-4中的频率:
p = pd.Period(2007, freq='A-DEC')               # 参数A-DEC指向每年第12月的最后一天
p           # 输出:Period('2007', 'A-DEC')
这⾥,这个Period对象表示的是从2007年1⽉1⽇到2007年12⽉31⽇之间的整段时间。只需对Period对象加上或减去⼀个整数即可达到根据其频率进⾏位移的效果:
p           # 输出:Period('2007', 'A-DEC')
p + 5       # 输出:Period('2012', 'A-DEC'),加5年
p - 2        # 输出:Period('2005', 'A-DEC'),减2年
如果两个Period对象拥有相同的频率,则它们的差就是它们之间的单位数量:
pd.Period('2014', freq='A-DEC') - p             # 输出:7,表示相差7年

period_range函数可⽤于创建规则的时期范围
rng = pd.period_range('2000-1-1', '2000-6-30', freq='M')
rng         # 输出如下:
PeriodIndex(['2000-01', '2000-02', '2000-03', '2000-04', '2000-05', '2000-06'], dtype='period[M]', freq='M')
PeriodIndex类保存了⼀组Period,它可以在任何pandas数据结构中被⽤作轴索引:
pd.Series(np.random.randn(len(rng)), index=rng)             # 输出如下:
2000-01   -0.301482
2000-02    0.410068
2000-03    1.628897
2000-04    0.555670
2000-05   -1.277706
2000-06    0.424157
Freq: M, dtype: float64
如果你有⼀个字符串数组,你也可以使⽤PeriodIndex类
values = ['2001Q3', '2002Q2', '2003Q1']
index = pd.PeriodIndex(values, freq='Q-DEC')       # 对字符串数组使用PeriodIndex类
index                   # 输出如下:
PeriodIndex(['2001Q3', '2002Q2', '2003Q1'], dtype='period[Q-DEC]', freq='Q-DEC')

1、时期的频率转换
Period和PeriodIndex对象都可以通过其asfreq⽅法被转换成别的频率。假设我们有⼀个年度时期,希望将其转换
为当年年初或年末的⼀个⽉度时期。该任务⾮常简单:
p = pd.Period('2007', freq='A-DEC')
p           # 输出如下:Period('2007', 'A-DEC')
p.asfreq('M', how='start')          # 输出:Period('2007-01', 'M')
p.asfreq('M', how='end')            # 输出:Period('2007-12', 'M')
你可以将Period('2007','A-DEC')看做⼀个被划分为多个⽉度时期的时间段中的游标。图11-1对此进⾏了说明。

image
                                            图11-1  Period频率转换示例
对于⼀个不以12⽉结束的财政年度,⽉度⼦时期的归属情况就不⼀样了:
p = pd.Period('2007', freq='A-JUN')
p                       # 输出:Period('2007', 'A-JUN')
p.asfreq('M', how='start')          # 输出:Period('2006-07', 'M'),跨年
p.asfreq('M', how='end')            # 输出:Period('2007-06', 'M')

在将⾼频率转换为低频率时,父时期(superperiod)是由⼦时期(subperiod)所属的位置决定的。例如,在A-JUN频率中,⽉份“2007年8⽉”实际上是属于周期“2008年”的:
p = pd.Period('Aug-2007', 'M')
p.asfreq('A-JUN')                   # 输出:Period('2008', 'A-JUN'),周期是2008年

完整的PeriodIndex或TimeSeries的频率转换⽅式也是如此:
rng = pd.period_range('2006', '2009', freq='A-DEC')
ts = pd.Series(np.random.randn(len(rng)), index=rng)
ts                      # 输出如下:(频率是每年最后一个月)
2006   -0.634701
2007   -1.254044
2008   -1.077110
2009    0.887097
Freq: A-DEC, dtype: float64
ts.asfreq('M', how='start')         # 输出如下:(频率是每年最第一个月)
2006-01   -0.634701
2007-01   -1.254044
2008-01   -1.077110
2009-01    0.887097
Freq: M, dtype: float64
这⾥,根据年度时期的第⼀个⽉,每年的时期被取代为每⽉的时期。如果我们想要每年的最后⼀个⼯作⽇,我们可以使⽤“B”频率,并指明想要该时期的末尾:
ts.asfreq('B', how='end')                       # 输出如下:
2006-12-29   -0.634701
2007-12-31   -1.254044
2008-12-31   -1.077110
2009-12-31    0.887097
Freq: B, dtype: float64

2、按季度计算的时期频率
季度型数据在会计、⾦融等领域中很常⻅。许多季度型数据都会涉及“财年末”的概念,通常是⼀年12个⽉中某⽉的最后⼀个⽇历⽇或⼯作⽇。就这⼀点来说,时期"2012Q4"根据财年末的不同会有不同的含义。pandas⽀持12种可能的季度型频率,即Q-JAN到Q-DEC:
p = pd.Period('2012Q4', freq='Q-JAN')           # 财年末是1月的最后一天
p           # 输出:Period('2012Q4', 'Q-JAN')
在以1⽉结束的财年中,2012Q4是从11⽉到1⽉(将其转换为⽇型频率就明⽩了)。图11-2对此进⾏了说明:

image
                                        图11-2  不同季度型频率之间的转换
p.asfreq('D', 'start')              # 输出如下:财年的第一天(参数传递方式:位置参数)
Period('2011-11-01', 'D')
p.asfreq('D', 'end')                # 输出如下:指向财年末的最后一天
Period('2012-01-31', 'D')

因此,Period之间的算术运算会⾮常简单。例如,要获取该季度倒数第⼆个⼯作⽇下午4点的时间戳,你可以这样:
# 参数传递方式:简写的位置参数。asfreq('T', 's')的频率是分,所以16*60指向下午的16点整
p4pm = (p.asfreq('B', 'e') -1).asfreq('T', 's') + 16*60
p4pm                    # 输出:Period('2012-01-30 16:00', 'T')
p4pm.to_timestamp()                 # 输出:Timestamp('2012-01-30 16:00:00')

period_range可⽤于⽣成季度型范围。季度型范围的算术运算也跟上⾯是⼀样的:
rng = pd.period_range('2011Q3', '2012Q4', freq='Q-JAN')
ts = pd.Series(np.arange(len(rng)), index=rng)
ts                      # 输出如下:
2011Q3    0
2011Q4    1
2012Q1    2
2012Q2    3
2012Q3    4
2012Q4    5
Freq: Q-JAN, dtype: int32
new_rng = (rng.asfreq('B', 'e') - 1).asfreq('T', 's') + 16*60           # 频率转换
ts.index = new_rng.to_timestamp()   # 改变ts的索引
ts                      # 输出如下:
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: int32

3、将Timestamp转换为Period(及其反向过程)
通过使⽤to_period⽅法,可以将由时间戳索引的Series和DataFrame对象转换为以时期索引:
rng = pd.date_range('2000-1-1', periods=3, freq='M')
ts = pd.Series(np.random.randn(3), index=rng)
ts                      # 输出如下:
2000-01-31    0.620808
2000-02-29    0.242898
2000-03-31    0.480687
Freq: M, dtype: float64
pts = ts.to_period()    # Timestamp转换为Period
pts
2000-01    0.620808
2000-02    0.242898
2000-03    0.480687
Freq: M, dtype: float64

由于时期指的是⾮重叠时间区间,因此对于给定的频率,⼀个时间戳只能属于⼀个时期新PeriodIndex的频率默认是从时间戳推断⽽来的,你也可以指定任何别的频率。结果中允许存在重复时期
rng = pd.date_range('1/29/2000', periods=6, freq='D')

ts2 = pd.Series(np.random.randn(6), index=rng)

ts2                     # 输出如下:
2000-01-29   -0.012140
2000-01-30   -0.950665
2000-01-31   -0.197126
2000-02-01    0.551863
2000-02-02    0.813741
2000-02-03    0.646920
Freq: D, dtype: float64

ts2.to_period('M')                  # 输出如下:通过to_period()方法将时间戳转换为月
2000-01   -0.012140
2000-01   -0.950665
2000-01   -0.197126
2000-02    0.551863
2000-02    0.813741
2000-02    0.646920
Freq: M, dtype: float64

要转换回时间戳,使⽤to_timestamp即可:
pts = ts2.to_period()
pts                     # 输出如下:
2000-01-29   -0.012140
2000-01-30   -0.950665
2000-01-31   -0.197126
2000-02-01    0.551863
2000-02-02    0.813741
2000-02-03    0.646920
Freq: D, dtype: float64

pts.to_timestamp(how='end')         # 输出如下:
2000-01-29   -0.012140
2000-01-30   -0.950665
2000-01-31   -0.197126
2000-02-01    0.551863
2000-02-02    0.813741
2000-02-03    0.646920
Freq: D, dtype: float64

4、通过数组创建PeriodIndex
固定频率的数据集通常会将时间信息分开存放在多个列中。例如,在下⾯这个宏观经济数据集中,年度和季度
分别存放在不同的列中:
data = pd.read_csv('examples/macrodata.csv')
data.head()             # 输出如下:
         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
data.year               # 输出如下:
0      1959.0
1      1959.0
2      1959.0
3      1959.0
4      1960.0
         ...
198    2008.0
199    2008.0
200    2009.0
201    2009.0
202    2009.0
Name: year, Length: 203, dtype: float64

data.quarter            # 输出如下:
0      1.0
1      2.0
2      3.0
3      4.0
4      1.0
       ...
198    3.0
199    4.0
200    1.0
201    2.0
202    3.0
Name: quarter, Length: 203, dtype: float64

通过将这些数组以及⼀个频率传⼊PeriodIndex,就可以将它们合并成DataFrame的⼀个索引:
index = pd.PeriodIndex(year=data.year, quarter=data.quarter, freq='Q-DEC')
index                   # 输出如下:
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')

data.index = index
data.infl               # 输出如下:(输出data的infl列)
1959Q1    0.00
1959Q2    2.34
1959Q3    2.74
1959Q4    0.27
1960Q1    2.31
           ...
2008Q3   -3.16
2008Q4   -8.79
2009Q1    0.94
2009Q2    3.37
2009Q3    3.56
Freq: Q-DEC, Name: infl, Length: 203, dtype: float64

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

pandas对象都带有⼀个resample⽅法,它是各种频率转换⼯作的主⼒函数。resample有⼀个类似于groupby的API,调⽤resample可以分组数据,然后会调⽤⼀个聚合函数:
rng = pd.date_range('2000-1-1', periods=100, freq='D')
ts = pd.Series(np.random.randn(len(rng)), index=rng)

ts                                  # 输出如下:
2000-01-01   -0.222942
2000-01-02    0.026890
2000-01-03   -0.233215
2000-01-04   -0.090225
2000-01-05   -1.650894
                 ...
2000-04-05    0.792029
2000-04-06    0.901992
2000-04-07   -1.138330
2000-04-08    0.264210
2000-04-09   -1.102930
Freq: D, Length: 100, dtype: float64

ts.resample('M').mean()             # 按月重采样,按月计算平均值,结果按月聚合
2000-01-31   -0.170285
2000-02-29    0.122144
2000-03-31    0.025351
2000-04-30   -0.129685
Freq: M, dtype: float64

ts.resample('M', kind='period').mean()          # 聚合到周期,输出如下:
2000-01   -0.170285
2000-02    0.122144
2000-03    0.025351
2000-04   -0.129685
Freq: M, dtype: float64
resample是⼀个灵活⾼效的⽅法,可⽤于处理⾮常⼤的时间序列。后面将通过⼀系列的示例说明其⽤法。
表11-5总结它的⼀些选项。

表11-5  resample⽅法的参数
参数                    说明
freq                    表示重采样频率的字符串或Dateoffset,例如'M'、'5min'或Second(15)
axis                    重采样的轴,默认为 axis=0
fill_method        升采样如何插值,比如'ffill'或'bfill'。默认不插值
closed                在降采样中,各时间段的哪一端是闭合(即包含)的,right或left。默认是right
label                   在降采样中,如何设置聚合值得标签,right或left(面元的右边界或左边界)。
                           例如,9:30到9:35之间的这5分钟会被标记为9:30或9:35。默认为right
loffset                 面元标签的时间校正值,比如'-1s'/Second(-1)用于将聚合标签调早1秒
limit                    在前向或后向填充时,允许填充的最大时间数
kind                    聚合到周期('period')或时间戳('timestamp'),默认聚合到时间序列的索引类型
convention         当对周期进行重采样,将低频周期转换为高频的惯用法('start'或'end');默认是'end'

1、降采样
将数据聚合到规律的低频率是⼀件⾮常普通的时间序列处理任务。待聚合的数据不必拥有固定的频率,期望的频率会⾃动定义聚合的⾯元边界,这些⾯元⽤于将时间序列拆分为多个⽚段。例如,要转换到⽉度频率('M'或'BM'),数据需要被划分到多个单⽉时间段中。各时间段都是半开放的。⼀个数据点只能属于⼀个时间段,所有时间段的并集必须能组成整个时间帧。在⽤resample对数据进⾏降采样时,需要考虑两点:
             各区间哪边是闭合的。
             如何标记各个聚合⾯元,⽤区间的开头还是末尾。
为了说明,我们来看⼀些“1分钟”数据:
rng = pd.date_range('2000-1-1', periods=12, freq='T')
ts = pd.Series(np.arange(12), index=rng)
ts                      # 输出如下:
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: int32
假设你想要通过求和的⽅式将这些数据聚合到“5分钟”块中:
ts.resample('5min', closed='right').sum()       # 输出如下:
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: int32

传⼊的频率将会以“5分钟”的增量定义⾯元边界。默认情况下,⾯元的右边界是包含的,因此00:00到00:05的区间中是包含00:05的。传⼊closed='left'会让区间以左边界闭合:
ts.resample('5min', closed='left').sum()        # 输出如下:
2000-01-01 00:00:00    10
2000-01-01 00:05:00    35
2000-01-01 00:10:00    21
Freq: 5T, dtype: int32
如你所⻅,最终的时间序列是以各⾯元左边界的时间戳进⾏标记的。传⼊label='right'即可⽤⾯元的右边界对其进⾏标记:
ts.resample('5min', closed='left', label='right').sum()     # 输出如下:右边界进行标记,但数据不包含右边界
2000-01-01 00:05:00    10
2000-01-01 00:10:00    35
2000-01-01 00:15:00    21
Freq: 5T, dtype: int32

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

image
                             图11-3  各种closed、label约定的“5分钟”重采样演示

最后,你可能希望对结果索引做⼀些位移,⽐如从右边界减去⼀秒以便更容易明⽩该时间戳到底表示的是哪个区间。只需通过loffset设置⼀个字符串或⽇期偏移量即可实现这个⽬的:
ts.resample('5min', closed='left', label='right', loffset='-1s').sum()
2000-01-01 00:04:59    10
2000-01-01 00:09:59    35
2000-01-01 00:14:59    21
Freq: 5T, dtype: int32
此外,也可以通过调⽤结果对象的shift⽅法来实现该⽬的,这样就不需要设置loffset了。

2、OHLC重采样
⾦融领域中有⼀种⽆所不在的时间序列聚合⽅式,即计算各⾯元的四个值:第⼀个值(open,开盘)、最后⼀个
值(close,收盘)、最⼤值(high,最⾼)以及最⼩值(low,最低)。传⼊how='ohlc'即可得到⼀个含有这四
种聚合值的DataFrame。整个过程很⾼效,只需⼀次扫描即可计算出结果:
ts.resample('5min').ohlc()                      # 输出如下:
                                  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

3、升采样和插值
在将数据从低频率转换到⾼频率时,就不需要聚合了。来看⼀个带有⼀些周型数据(weekly data)的DataFrame:
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'])
frame                   # 输出如下:
                      Colorado       Texas  New York         Ohio
2000-01-05  1.454027 -0.827189 -1.434377 -0.714617
2000-01-12 -2.558629 -1.383027  0.218594 -1.543188
当你对这个数据进⾏聚合,每组只有⼀个值,这样就会引⼊缺失值。我们使⽤asfreq⽅法转换成⾼频,不经过聚合:
df_daily = frame.resample('D').asfreq()         # 会引入缺失值
df_daily                # 输出如下:
                      Colorado       Texas  New York         Ohio
2000-01-05  1.454027 -0.827189 -1.434377 -0.714617
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 -2.558629 -1.383027  0.218594 -1.543188

假设你想要⽤前⾯的周型值填充“⾮星期三”。resampling的填充和插值⽅式跟fillna和reindex的⼀样:
frame.resample('D').ffill()                     # 输出如下:按天重采样填充
                      Colorado       Texas  New York         Ohio
2000-01-05  1.454027 -0.827189 -1.434377 -0.714617
2000-01-06  1.454027 -0.827189 -1.434377 -0.714617
2000-01-07  1.454027 -0.827189 -1.434377 -0.714617
2000-01-08  1.454027 -0.827189 -1.434377 -0.714617
2000-01-09  1.454027 -0.827189 -1.434377 -0.714617
2000-01-10  1.454027 -0.827189 -1.434377 -0.714617
2000-01-11  1.454027 -0.827189 -1.434377 -0.714617
2000-01-12 -2.558629 -1.383027  0.218594 -1.543188

同样,这⾥也可以只填充指定的时期数(⽬的是限制前⾯的观测值的持续使⽤距离):
frame.resample('D').ffill(limit=2)              # 输出如下:
                      Colorado       Texas  New York         Ohio
2000-01-05  1.454027 -0.827189 -1.434377 -0.714617
2000-01-06  1.454027 -0.827189 -1.434377 -0.714617
2000-01-07  1.454027 -0.827189 -1.434377 -0.714617
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 -2.558629 -1.383027  0.218594 -1.543188

注意,新的⽇期索引完全没必要跟旧的重叠:
frame.resample('W-THU').ffill()
                      Colorado       Texas  New York         Ohio
2000-01-06  1.454027 -0.827189 -1.434377 -0.714617
2000-01-13 -2.558629 -1.383027  0.218594 -1.543188

4、通过时期进⾏重采样
对那些使⽤时期索引的数据进⾏重采样与时间戳很像:
frame = pd.DataFrame(np.random.randn(24, 4),
                                       index=pd.period_range('1-2000', '12-2001', freq='M'),
                                       columns=['Colorado', 'Texas', 'New York', 'Ohio'])
frame[:5]               # 前5行输出如下:([-5:]取最后5行),也可用head()和tail()方法
                 Colorado        Texas  New York        Ohio
2000-01 -0.122366 -1.482307 -1.511748 -1.001796
2000-02 -0.309046 -0.433579  0.641963 -0.845334
2000-03  1.467931  1.524688 -1.107858  1.721680
2000-04 -0.007002  1.601335  0.366802  1.904509
2000-05 -1.687389 -1.237108 -0.567321 -0.918862
每年的最后一个月重采样求平均值
annual_frame = frame.resample('A-DEC').mean()
annual_frame                        # 输出如下:
           Colorado       Texas  New York        Ohio
2000  0.293250 -0.001142  -0.25785  0.007990
2001 -0.266541 -0.091771   0.56443 -0.783206

升采样要稍微麻烦⼀些,因为你必须决定在新频率中各区间的哪端⽤于放置原来的值,就像asfreq⽅法那样。
convention参数默认为'start',可设置为'end':
# Q-DEC: Quarterly, year ending in December,按季度采样,每年的最后一个月为边界
annual_frame.resample('Q-DEC').ffill()          # convention参数默认为'start',升采样
              Colorado       Texas  New York        Ohio
2000Q1  0.293250 -0.001142  -0.25785  0.007990
2000Q2  0.293250 -0.001142  -0.25785  0.007990
2000Q3  0.293250 -0.001142  -0.25785  0.007990
2000Q4  0.293250 -0.001142  -0.25785  0.007990
2001Q1 -0.266541 -0.091771   0.56443 -0.783206
2001Q2 -0.266541 -0.091771   0.56443 -0.783206
2001Q3 -0.266541 -0.091771   0.56443 -0.783206
2001Q4 -0.266541 -0.091771   0.56443 -0.783206
convention参数设置为'end'
annual_frame.resample('Q-DEC', convention='end').ffill()
                Colorado       Texas  New York        Ohio
2000Q4  0.293250 -0.001142  -0.25785  0.007990
2001Q1  0.293250 -0.001142  -0.25785  0.007990
2001Q2  0.293250 -0.001142  -0.25785  0.007990
2001Q3  0.293250 -0.001142  -0.25785  0.007990
2001Q4 -0.266541 -0.091771   0.56443 -0.783206
由于时期指的是时间区间,所以升采样和降采样的规则就⽐较严格:
             在降采样中,⽬标频率必须是源频率的⼦时期(subperiod)。
             在升采样中,⽬标频率必须是源频率的父时期(superperiod)。

如果不满⾜这些条件,就会引发异常。这主要影响的是按季、年、周计算的频率。例如,由Q-MAR定义的时间区间只能升采样为A-MAR、A-JUN、A-SEP、A-DEC等:
annual_frame.resample('Q-MAR').ffill()          # 输出如下:
                Colorado       Texas  New York        Ohio
2000Q4  0.293250 -0.001142  -0.25785  0.007990
2001Q1  0.293250 -0.001142  -0.25785  0.007990
2001Q2  0.293250 -0.001142  -0.25785  0.007990
2001Q3  0.293250 -0.001142  -0.25785  0.007990
2001Q4 -0.266541 -0.091771   0.56443 -0.783206
2002Q1 -0.266541 -0.091771   0.56443 -0.783206
2002Q2 -0.266541 -0.091771   0.56443 -0.783206
2002Q3 -0.266541 -0.091771   0.56443 -0.783206

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

首先加载⼀些时间序列数据,将其重采样为⼯作⽇频率:
close_px_all = pd.read_csv('examples/stock_px_2.csv',
                                            parse_dates=True, index_col=0)
close_px = close_px_all[['AAPL', 'MSFT', 'XOM']]            # 取指定列的数据
close_px = close_px.resample('B').ffill()       # 根据每工作日重采样

现在引⼊rolling运算符,它与resample和groupby很像。可以在TimeSeries或DataFrame以及⼀个window(表示期数,⻅图11-4)上调⽤它:
close_px.AAPL.plot()
<matplotlib.axes._subplots.AxesSubplot at 0x1a6ac2ab668>
close_px.AAPL.rolling(250).mean().plot()        # 输出图形11-4
Out[150]: <matplotlib.axes._subplots.AxesSubplot at 0x1a6ac2ab668>

图11-4 苹果公司股价的250⽇均线
                                             图11-4  苹果公司股价的250⽇均线

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

默认情况下,rolling函数需要窗口中所有的值为非NA值。可以修改该行为以解决缺失数据的问题。其实,在时间序列开始处尚不足窗口期的那些数据就是个特例(见图11-5)
appl_std250 = close_px.AAPL.rolling(250, min_periods=10).std()
appl_std250[5:12]                   # 输出如下:
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
appl_std250.plot()                  # 输出图形11-5

图11-5  苹果公司250⽇每⽇回报标准差
                                          图11-5  苹果公司250⽇每⽇回报标准差

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

对DataFrame调⽤rolling_mean(以及与之类似的函数)会将转换应⽤到所有的列上(⻅图11-6):
close_px.rolling(60).mean().plot(logy=True)     # 输出图形11-6

图11-6 各股价60⽇均线(对数Y轴)
                                              图11-6 各股价60⽇均线(对数Y轴)

rolling函数也可以接受一个指定固定大小时间补偿字符串,而不是一组时期。这样可以方便处理不规律的时间序
列。这些字符串也可以传递给resample。例如,我们可以计算20天的滚动均值,如下所示:
close_px.rolling('20D').mean()                  # 输出如下:
                             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
...                                ...               ...                 ...
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]

1、指数加权函数
另⼀种使⽤固定⼤⼩窗⼝及相等权数观测值的办法是,定义⼀个衰减因⼦(decay factor)常量,以便使近期的观测值拥有更⼤的权数。衰减因⼦的定义⽅式有很多,⽐较流⾏的是使⽤时间间隔(span),它可以使结果兼容于窗⼝⼤⼩等于时间间隔的简单移动窗⼝(simple moving window)函数

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

除了rolling和expanding,pandas还有ewm运算符。下⾯这个例⼦对⽐了苹果公司股价的30⽇移动平均和span=30的指数加权移动平均(如图11-7所示):
appl_px = close_px.AAPL['2006':'2007']
ma30 = appl_px.rolling(30, min_periods=30).mean()           # 30日移动平均
ewma30 = appl_px.ewm(span=30).mean()            # 30日移动加权平均
ma30.plot(style='r--', label='Simple MA')
ewma30.plot(style='b-', label='EW MA')
plt.legend()            # 输出图形11-7

图11-7 简单移动平均与指数加权移动平均
                                          图11-7  简单移动平均与指数加权移动平均

2、⼆元移动窗⼝函数
有些统计运算(如相关系数和协方差)需要在两个时间序列上执行。例如,金融分析师常常对某只股票对某个参考指数(如标准普尔500指数)的相关系数感兴趣。要进行说明,我们先计算我们感兴趣的时间序列的百分数变化
spx_px = close_px_all['SPX']
spx_rets = spx_px.pct_change()
returns = close_px.pct_change()

调⽤rolling之后,corr聚合函数开始计算与spx_rets滚动相关系数(结果⻅图11-8):
corr = returns.AAPL.rolling(125, min_periods=100).corr(spx_rets)
corr.plot()             # 输出图形11-8

图11-8 AAPL 6个⽉的回报与标准普尔500指数的相关系数
                                 图11-8  AAPL 6个⽉的回报与标准普尔500指数的相关系数

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

图11-9 3只股票6个⽉的回报与标准普尔500指数的相关系数
                                    图11-9  3只股票6个⽉的回报与标准普尔500指数的相关系数

3、⽤户定义的移动窗⼝函数
rolling_apply函数使你能够在移动窗⼝上应⽤⾃⼰设计的数组函数。唯⼀要求的就是:该函数要能从数组的各个⽚段中产⽣单个值(即约简)。⽐如说,当我们⽤rolling(...).quantile(q)计算样本分位数时,可能对样本中特定值的百分等级感兴趣。scipy.stats.percentileofscore函数就能达到这个⽬的(结果⻅图11-10):
from scipy.stats import percentileofscore
score_at_2percent = lambda x: percentileofscore(x, 0.02)
result = returns.AAPL.rolling(250).apply(score_at_2percent)
result.plot()           # 输出图形11-10

图11-10 AAPL 2%回报率的百分等级(⼀年窗⼝期)
                                 图11-10  AAPL 2%回报率的百分等级(⼀年窗⼝期)
如果没安装SciPy,可以使⽤conda或pip安装。

八、总结
与之前接触到的数据相⽐,时间序列数据要求不同类型的分析和数据转换⼯具。

posted @ 2018-12-28 14:55  远方那一抹云  阅读(861)  评论(0编辑  收藏  举报