《利用Python进行数据分析》时间序列
时间序列(time series)数据是一种重要的结构化数据形式,在多个时间点观察或测量到的任何事物都可以形成一段时间序列。很多时间序列是固定频率的,也就是说,数据点是根据某种规律定期出现的(比如每15秒、每5分钟、每月出现一次)。时间序列也可以是不定期的,没有固定的时间单位或单位之间的偏移量。时间序列数据的意义取决于具体的应用场景,主要有以下几种:
- 时间戳(timestamp),特定的时刻。
- 固定时期(period),如2007年1月或2010年全年。
- 时间间隔(interval),由起始和结束时间戳表示。时期(period)可以被看做间隔(interval)的特例。
- 实验或过程时间,每个时间点都是相对于特定起始时间的一个度量。例如,从放入烤箱时起,每秒钟饼干的直径。
本章主要讲解前3种时间序列。许多技术都可用于处理实验型时间序列,其索引可能是一个整数或浮点数(表示从实验开始算起已经过去的时间)。最简单也最常见的时间序列都是用时间戳进行索引的。
提示:pandas也支持基于timedeltas的指数,它可以有效代表实验或经过的时间。学习pandas的文档(http://pandas.pydata.org/)。
11.1 日期和时间数据类型及工具
Python标准库包含用于日期(date)和时间(time)数据的数据类型,而且还有日历方面的功能。主要会用到datetime、time以及calendar模块。datetime.datetime(也可以简写为datetime)是用得最多的数据类型:

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

datetime模块中的数据类型

字符串和datetime的相互转换
利用str方法将datetime对象格式化为字符串;
strftime方法(传入一个格式化字符串)将pandas的Timestamp对象格式化为字符串;
datetime.strptime可以用这些格式化编码将字符串转换为日期:

datetime格式定义(兼容ISO C89):


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

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

特定于当前环境的日期格式:
pandas最基本的时间序列类型就是以时间戳(通常以Python字符串或datatime对象表示)为索引的Series:

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

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

索引、选取、子集构造

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

由于大部分时间序列数据都是按照时间先后排序的,因此也可以用时间戳对其进行切片(即范围查询):
注意,这样切片所产生的是原时间序列的视图,跟NumPy数组的切片运算是一样的。对它们的更改会影响原数据。

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

这些操作也可以用于dataframe:

带有重复索引的时间序列
在某些应用场景中,可能会存在多个观测数据落在同一个时间点上的情况。

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

11.3 日期的范围、频率以及移动
pandas中的原生时间序列一般被认为是不规则的,也就是说,它们没有固定的频率。如果需要以某种相对固定的频率进行分析,比如每日、每月、每15分钟等(这样自然会在时间序列中引入缺失值)例如,将之前那个时间序列转换为一个具有固定频率(每日)的时间序列,只需调用resample即可:

生成日期范围
pandas.date_range可用于根据指定的频率生成指定长度的DatetimeIndex:

基本的时间序列频率(不完整)



date_range默认会保留起始和结束时间戳的时间信息(如果有的话):希望产生一组被规范化(normalize)的时间戳。normalize选项即可实现该功能:

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

有些频率所描述的时间点并不是均匀分隔的。例如,"M"(日历月末)和"BM"(每月最后一个工作日)就取决于每月的天数,对于后者,还要考虑月末是不是周末。
由于没有更好的术语,作者将这些称为锚点偏移量(anchored offset)。
【笔记:用户可以根据实际需求自定义一些频率类以便提供pandas所没有的日期逻辑,但具体的细节超出了本书的范围。】
时间序列的基础频率:



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

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

如果频率已知,则可以将其传给shift以便实现对时间戳进行位移而不是对数据进行简单位移:

通过偏移量对日期进行位移
pandas的日期偏移量还可以用在datetime或Timestamp对象上:

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

更简单、更快速地实现该功能的办法是使用resample:

11.4 时区处理
时间序列处理工作中最让人不爽的就是对时区的处理。许多人都选择以协调世界时(UTC,它是格林尼治标准时间(Greenwich Mean Time)的接替者,目前已经是国际标准了)来处理时间序列。
时区是以UTC偏移量的形式表示的。例如,夏令时期间,纽约比UTC慢4小时,而在全年其他时间则比UTC慢5小时。
在Python中,时区信息来自第三方库pytz,它使Python可以使用Olson数据库(汇编了世界时区信息)。这对历史数据非常重要,这是因为由于各地政府的各种突发奇想,夏令时转变日期(甚至UTC偏移量)已经发生过多次改变了。就拿美国来说,DST转变时间自1900年以来就改变过多次!
有关pytz库的更多信息,请查阅其文档。由于pandas包装了pytz的功能,因此暂时不用记忆其API,只要记得时区的名称即可。时区名可以在shell中看到,也可以通过文档查看:

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

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

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

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

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

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

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

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

时期的频率转换
Period和PeriodIndex对象都可以通过其asfreq方法被转换成别的频率。

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

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


获取该季度倒数第二个工作日下午4点的时间戳:

季度型范围的算术运算也跟上面是一样的

获取该季度倒数第二个工作日下午4点的时间戳作为index:

将Timestamp转换为Period(及其反向过程)
通过使用to_period方法,可以将由时间戳索引的Series和DataFrame对象转换为以时期索引:

新PeriodIndex的频率默认是从时间戳推断而来的,也可以指定任何别的频率。结果中允许存在重复时期:

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

通过数组创建PeriodIndex
固定频率的数据集通常会将时间信息分开存放在多个列中。例如,年度和季度分别存放在不同的列中;通过将这些数组以及一个频率传入PeriodIndex,就可以将它们合并成DataFrame的一个索引:

11.6 重采样及频率转换
重采样(resampling)指的是将时间序列从一个频率转换到另一个频率的处理过程。
将高频率数据聚合到低频率称为降采样(downsampling),而将低频率数据转换到高频率则称为升采样(upsampling)。
并不是所有的重采样都能被划分到这两个大类中。例如,将W-WED(每周三)转换为W-FRI既不是降采样也不是升采样。
pandas对象都带有一个resample方法,它是各种频率转换工作的主力函数。resample有一个类似于groupby的API,调用resample可以分组数据,然后会调用一个聚合函数:

resample方法的参数

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

默认情况下,面元的右边界是包含的,因此00:00到00:05的区间中是包含00:05的。传入closed='left'会让区间以左边界闭合:
最终的时间序列是以各面元右边界的时间戳进行标记的。传入label='right'即可用面元的邮编界对其进行标记:

各种closed、label约定的“5分钟”重采样演示

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

升采样和插值

resampling的填充和插值方式跟fillna和reindex的一样:

也可以只填充指定的时期数(目的是限制前面的观测值的持续使用距离):
⚠️⚠️⚠️新的日期索引完全没必要跟旧的重叠

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

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

由于时期指的是时间区间,所以升采样和降采样的规则就比较严格:
- 在降采样中,目标频率必须是源频率的子时期(subperiod)。
- 在升采样中,目标频率必须是源频率的超时期(superperiod)。
如果不满足这些条件,就会引发异常。这主要影响的是按季、年、周计算的频率。
例如,由Q-MAR定义的时间区间只能升采样为A-MAR、A-JUN、A-SEP、A-DEC等:
【⚠️⚠️⚠️当以3月为终点时,2018年后3个季度就属于2019年财务年】

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

表达式rolling(250)与groupby很像,但不是对其进行分组,而是创建一个按照250天分组的滑动窗口对象。

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


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


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


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

指数加权函数
另一种使用固定大小窗口及相等权数观测值的办法是,定义一个衰减因子(decay factor)常量,以便使近期的观测值拥有更大的权数。
衰减因子的定义方式有很多,比较流行的是使用时间间隔(span),它可以使结果兼容于窗口大小等于时间间隔的简单移动窗口(simple moving window)函数。
由于指数加权统计会赋予近期的观测值更大的权数,因此相对于等权统计,它能“适应”更快的变化。
除了rolling和expanding,pandas还有ewm运算符。
下面这个例子对比了苹果公司股价的30日移动平均和span=30的指数加权移动平均


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


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


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


最后一节11.7需要一点专业的统计学知识,比如:指数衰减、指数加权移动平均、滑动函数、扩展函数等等,先把自己的实践放在这里,方便以后自己查阅复习。