本章节以及后续章节的源码,当然也可以从我的github下载,在源码中我自己加了一些中文注释。
在多个时间点观察或测量到的任何事物都可以形成一段时间序列。
时间戳(timestamp):特定的时刻。
固定时期(period):如2007年1月或2010年全年。
时间间隔(interval):由起始和结束时间戳表示。时期(period)可以被看做间隔(interval)的特例。
实验或过程时间:每个时间点都是相对于特定起始时间的一个度量。例如,从放入烤箱时起,每秒钟饼干的直径。
pandas提供了许多内置的时间序列处理工具和数据算法。因此,你可以高效处理非常大的时间序列,轻松地进行切片/切块、聚合、对定期/不定期的时间序列进行重采样等。有些工具特别适合金融和经济应用,你当然也可以用它们来分析服务器日志数据。
一、日期和时间数据类型及工具
Python标准库包含用于日期(date)和时间(time)数据的数据类型,而且还有日历方面的功能。我们主要会用到datetime、time以及calendar模块。datetime.datetime(也可以简写为datetime)是用得最多的数据类型:
datetime以毫秒形式存储日期和时间。timedelta表示两个datetime对象之间的时间差:
可以给datetime对象加上(或减去)一个或多个timedelta,这样会产生一个新对象:
字符串和datetime的相互转换
利用str或strftime方法(传入一个格式化字符串),datetime对象和pandas的Timestamp对象可以被格式化为字符串:
全部的格式化编码:
datetime.strptime可以用这些格式化编码将字符串转换为日期:
datetime.strptime是通过已知格式进行日期解析的最佳方式。但是每次都要编写格式定义是很麻烦的事情,尤其是对于一些常见的日期格式。这种情况下,你可以用dateutil这个第三方包中的parser.parse方法(pandas中已经自动安装好了):
在国际通用的格式中,日出现在月的前面很普遍,传入dayfirst=True即可解决这个问题:
pandas通常是用于处理成组日期的,不管这些日期是DataFrame的轴索引还是列。to_datetime方法可以解析多种不同的日期表示形式。
还可以处理缺失值(None、空字符串等):
二、时间序列基础
pandas最基本的时间序列类型就是以时间戳(通常以Python字符串或datatime对象表示)为索引的Series:
跟其他Series一样,不同索引的时间序列之间的算术运算会自动按日期对齐:
pandas用NumPy的datetime64数据类型以纳秒形式存储时间戳:
DatetimeIndex中的各个标量值是pandas的Timestamp对象:
只要有需要,TimeStamp可以随时自动转换为datetime对象。
1、索引、选取、子集构造
当你根据标签索引选取数据时,时间序列和其它的pandas.Series很像:
传入一个可以被解释为日期的字符串:
对于较长的时间序列,只需传入“年”或“年月”即可轻松选取数据的切片:
datetime对象也可以进行切片:
由于大部分时间序列数据都是按照时间先后排序的,因此你也可以用不存在于该时间序列中的时间戳对其进行切片(即范围查询):
可以传入字符串日期、datetime或Timestamp。注意,这样切片所产生的是原时间序列的视图,跟NumPy数组的切片运算是一样的。这意味着,没有数据被复制,对切片进行修改会反映到原始数据上。
还有一个等价的实例方法也可以截取两个日期之间TimeSeries:
这些操作对DataFrame也有效:
2、带有重复索引的时间序列
在某些应用场景中,可能会存在多个观测数据落在同一个时间点上的情况。
对具有非唯一时间戳的数据进行聚合。一个办法是使用groupby,并传入level=0:
三、日期的范围、频率以及移动
pandas中的原生时间序列一般被认为是不规则的,也就是说,它们没有固定的频率。对于大部分应用程序而言,这是无所谓的。但是,它常常需要以某种相对固定的频率进行分析,比如每日、每月、每15分钟等(这样自然会在时间序列中引入缺失值)。幸运的是,pandas有一整套标准时间序列频率以及用于重采样、频率推断、生成固定频率日期范围的工具。例如,我们可以将之前那个时间序列转换为一个具有固定频率(每日)的时间序列,只需调用resample即可:
1、生成日期范围
date_range会产生按天计算的时间点。如果只传入起始或结束日期,那就还得传入一个表示一段时间的数字:
起始和结束日期定义了日期索引的严格边界。例如,如果你想要生成一个由每月最后一个工作日组成的日期索引,可以传入"BM"频率:
基本的时间序列频率:
date_range默认会保留起始和结束时间戳的时间信息:
虽然起始和结束日期带有时间信息,但你希望产生一组被规范化(normalize)到午夜的时间戳。normalize选项即可实现该功能:
2、频率和日期偏移量
pandas中的频率是由一个基础频率(base frequency)和一个乘数组成的。基础频率通常以一个字符串别名表示,比如"M"表示每月,"H"表示每小时。对于每个基础频率,都有一个被称为日期偏移量(date offset)的对象与之对应。例如,按小时计算的频率可以用Hour类表示:
一般来说,无需明确创建这样的对象,只需使用诸如"H"或"4H"这样的字符串别名即可。在基础频率前面放上一个整数即可创建倍数:
大部分偏移量对象都可通过加法进行连接:
也可以传入频率字符串(如"2h30min"),这种字符串可以被高效地解析为等效的表达式:
3、WOM日期
WOM(Week Of Month)是一种非常实用的频率类,它以WOM开头。它使你能获得诸如“每月第3个星期五”之类的日期:
4、移动(超前和滞后)数据
移动(shifting)指的是沿着时间轴将数据前移或后移。Series和DataFrame都有一个shift方法用于执行单纯的前移或后移操作,保持索引不变:
shift通常用于计算一个时间序列或多个时间序列(如DataFrame的列)中的百分比变化。可以这样表达:
由于单纯的移位操作不会修改索引,所以部分数据会被丢弃。因此,如果频率已知,则可以将其传给shift以便实现对时间戳进行位移而不是对数据进行简单位移:
5、通过偏移量对日期进行位移
pandas的日期偏移量还可以用在datetime或Timestamp对象上:
如果加的是锚点偏移量(比如MonthEnd),第一次增量会将原日期向前滚动到符合频率规则的下一个日期:
通过锚点偏移量的rollforward和rollback方法,可明确地将日期向前或向后“滚动”:
结合groupby使用这两个“滚动”方法:
更简单、更快速地实现该功能的办法是使用resample:
四、时区处理
在Python中,时区信息来自第三方库pytz,它使Python可以使用Olson数据库(汇编了世界时区信息)。pandas包装了pytz的功能。
时区名可以在shell中看到,也可以通过文档查看:
要从pytz中获取时区对象,使用pytz.timezone即可:
pandas中的方法既可以接受时区名也可以接受这些对象。
1、时区本地化和转换
默认情况下,pandas中的时间序列是单纯(naive)的时区。看看下面这个时间序列:
可以用时区集生成日期范围:
从单纯到本地化的转换是通过tz_localize方法处理的:
一旦时间序列被本地化到某个特定时区,就可以用tz_convert将其转换到别的时区了:
对于上面这种时间序列(它跨越了美国东部时区的夏令时转变期),我们可以将其本地化到EST,然后转换为UTC或柏林时间:
tz_localize和tz_convert也是DatetimeIndex的实例方法:
2、操作时区意识型Timestamp对象
跟时间序列和日期范围差不多,独立的Timestamp对象也能被从单纯型(naive)本地化为时区意识型(time zone-aware),并从一个时区转换到另一个时区:
在创建Timestamp时,还可以传入一个时区信息:
时区意识型Timestamp对象在内部保存了一个UTC时间戳值(自UNIX纪元(1970年1月1日)算起的纳秒数)。这个UTC值在时区转换过程中是不会发生变化的:
当使用pandas的DateOffset对象执行时间算术运算时,运算过程会自动关注是否存在夏令时转变期。这里,我们创建了在DST转变之前的时间戳。首先,来看夏令时转变前的30分钟:
夏令时转变前90分钟:
3、不同时区之间的运算
如果两个时间序列的时区不同,在将它们合并到一起时,最终结果就会是UTC。由于时间戳其实是以UTC存储的,所以这是一个很简单的运算,并不需要发生任何转换:
五、时期及其算术运算
时期(period)表示的是时间区间,比如数日、数月、数季、数年等。Period类所表示的就是这种数据类型,其构造函数需要用到一个字符串或整数,以及频率:
这个Period对象表示的是从2007年1月1日到2007年12月31日之间的整段时间。只需对Period对象加上或减去一个整数即可达到根据其频率进行位移的效果:
如果两个Period对象拥有相同的频率,则它们的差就是它们之间的单位数量:
period_range函数可用于创建规则的时期范围:
PeriodIndex类保存了一组Period,它可以在任何pandas数据结构中被用作轴索引:
如果你有一个字符串数组,你也可以使用PeriodIndex类:
1、时期的频率转换
Period和PeriodIndex对象都可以通过其asfreq方法被转换成别的频率。假设我们有一个年度时期,希望将其转换为当年年初或年末的一个月度时期。
对于一个不以12月结束的财政年度,月度子时期的归属情况就不一样了:
在将高频率转换为低频率时,超时期(superperiod)是由子时期(subperiod)所属的位置决定的。例如,在A-JUN频率中,,月份“2007年8月”实际上是属于周期“2008年”的:
完整的PeriodIndex或TimeSeries的频率转换方式也是如此:
2、按季度计算的时期频率
季度型数据在会计、金融等领域中很常见。许多季度型数据都会涉及“财年末”的概念,通常是一年12个月中某月的最后一个日历日或工作日。就这一点来说,时期"2012Q4"根据财年末的不同会有不同的含义。pandas支持12种可能的季度型频率,即Q-JAN到Q-DEC:
在以1月结束的财年中,2012Q4是从11月到1月(将其转换为日型频率就明白了)
因此,Period之间的算术运算会非常简单。例如,要获取该季度倒数第二个工作日下午4点的时间戳:
period_range可用于生成季度型范围。季度型范围的算术运算也跟上面是一样的:
3、将Timestamp转换为Period(及其反向过程)
通过使用to_period方法,可以将由时间戳索引的Series和DataFrame对象转换为以时期索引:
由于时期指的是非重叠时间区间,因此对于给定的频率,一个时间戳只能属于一个时期。新PeriodIndex的频率默认是从时间戳推断而来的,你也可以指定任何别的频率。结果中允许存在重复时期:
要转换回时间戳,使用to_timestamp即可:
4、通过数组创建PeriodIndex
固定频率的数据集通常会将时间信息分开存放在多个列中。例如,在下面这个宏观经济数据集中,年度和季度就分别存放在不同的列中:
通过将这些数组以及一个频率传入PeriodIndex,就可以将它们合并成DataFrame的一个索引:
六、重采样及频率转换
重采样(resampling)指的是将时间序列从一个频率转换到另一个频率的处理过程。将高频率数据聚合到低频率称为降采样(downsampling),而将低频率数据转换到高频率则称为升采样(upsampling)。并不是所有的重采样都能被划分到这两个大类中。例如,将W-WED(每周三)转换为W-FRI既不是降采样也不是升采样。
pandas对象都带有一个resample方法,它是各种频率转换工作的主力函数。resample有一个类似于groupby的API,调用resample可以分组数据,然后会调用一个聚合函数:
resample是一个灵活高效的方法,可用于处理非常大的时间序列。
1、降采样
将数据聚合到规律的低频率是一件非常普通的时间序列处理任务。待聚合的数据不必拥有固定的频率,期望的频率会自动定义聚合的面元边界,这些面元用于将时间序列拆分为多个片段。例如,要转换到月度频率('M'或'BM'),数据需要被划分到多个单月时间段中。各时间段都是半开放的。一个数据点只能属于一个时间段,所有时间段的并集必须能组成整个时间帧。在用resample对数据进行降采样时,需要考虑两样东西:各区间哪边是闭合的、如何标记各个聚合面元,用区间的开头还是末尾。
看一些“1分钟”数据:
通过求和的方式将这些数据聚合到“5分钟”块中。传入的频率将会以“5分钟”的增量定义面元边界。默认情况下,面元的右边界是包含的,因此00:00到00:05的区间中是包含00:05的。传入closed='left'会让区间以左边界闭合:
最终的时间序列是以各面元右边界的时间戳进行标记的。传入label='right'即可用面元的邮编界对其进行标记:
你可能希望对结果索引做一些位移,比如从右边界减去一秒以便更容易明白该时间戳到底表示的是哪个区间。只需通过loffset设置一个字符串或日期偏移量即可实现这个目的:
2、OHLC重采样
金融领域中有一种无所不在的时间序列聚合方式,即计算各面元的四个值:第一个值(open,开盘)、最后一个值(close,收盘)、最大值(high,最高)以及最小值(low,最低)。传入how='ohlc'即可得到一个含有这四种聚合值的DataFrame。整个过程很高效,只需一次扫描即可计算出结果:
3、升采样和插值
在将数据从低频率转换到高频率时,就不需要聚合了。看一个带有一些周型数据的DataFrame:
对这个数据进行聚合,每组只有一个值,这样就会引入缺失值。我们使用asfreq方法转换成高频,不经过聚合:
假设你想要用前面的周型值填充“非星期三”。resampling的填充和插值方式跟fillna和reindex的一样:
也可以只填充指定的时期数(目的是限制前面的观测值的持续使用距离):
新的日期索引完全没必要跟旧的重叠:
4、通过时期进行重采样
对那些使用时期索引的数据进行重采样与时间戳很像:
升采样要稍微麻烦一些,因为你必须决定在新频率中各区间的哪端用于放置原来的值,就像asfreq方法那样。convention参数默认为'start',也可设置为'end':
由于时期指的是时间区间,所以升采样和降采样的规则就比较严格:在降采样中,目标频率必须是源频率的子时期(subperiod)。在升采样中,目标频率必须是源频率的超时期(superperiod)。
如果不满足这些条件,就会引发异常。这主要影响的是按季、年、周计算的频率。例如,由Q-MAR定义的时间区间只能升采样为A-MAR、A-JUN、A-SEP、A-DEC等:
七、移动窗口函数
在移动窗口(可以带有指数衰减权数)上计算的各种统计函数也是一类常见于时间序列的数组变换。这样可以圆滑噪音数据或断裂数据。将它们称为移动窗口函数(moving window function),其中还包括那些窗口不定长的函数(如指数加权移动平均)。跟其他统计函数一样,移动窗口函数也会自动排除缺失值。
为了提升数据的准确性,将某个点的取值扩大到包含这个点的一段区间,用区间来进行判断,这个区间就是窗口。移动窗口就是窗口向一端滑行,默认是从右往左,每次滑行并不是区间整块的滑行,而是一个单位一个单位的滑行。给个例子好理解一点:
首先我们设置的窗口window=3,也就是3个数取一个均值。index 0,1 为NaN,是因为它们前面都不够3个数,等到index2 的时候,它的值是怎么算的呢,就是(index0+index1+index2 )/3
index3 的值就是( index1+index2+index3)/ 3
rolling函数参数详解:
加载一些时间序列数据,将其重采样为工作日频率:
引入rolling运算符,它与resample和groupby很像。可以在TimeSeries或DataFrame以及一个window(表示期数)上调用它:
表达式rolling(250)与groupby很像,但不是对其进行分组,而是创建一个按照250天分组的滑动窗口对象。然后,我们就得到了苹果公司股价的250天的移动窗口。
默认情况下,rolling函数需要窗口中所有的值为非NA值。可以修改该行为以解决缺失数据的问题。其实,在时间序列开始处尚不足窗口期的那些数据就是个特例:
要计算扩展窗口平均(expanding window mean),可以使用expanding。“扩展”意味着,从时间序列的起始处开始窗口,增加窗口直到它超过所有的序列。apple_std250时间序列的扩展窗口平均如下所示:
对DataFrame调用rolling_mean(以及与之类似的函数)会将转换应用到所有的列上:
rolling函数也可以接受一个指定固定大小时间补偿字符串,而不是一组时期。这样可以方便处理不规律的时间序列。这些字符串也可以传递给resample。例如,我们可以计算20天的滚动均值,如下所示:
1、指数加权函数
另一种使用固定大小窗口及相等权数观测值的办法是,定义一个衰减因子(decay factor)常量,以便使近期的观测值拥有更大的权数。衰减因子的定义方式有很多,比较流行的是使用时间间隔(span),它可以使结果兼容于窗口大小等于时间间隔的简单移动窗口(simple moving window)函数。
由于指数加权统计会赋予近期的观测值更大的权数,因此相对于等权统计,它能“适应”更快的变化。
除了rolling和expanding,pandas还有ewm运算符。下面这个例子对比了苹果公司股价的30日移动平均和span=30的指数加权移动平均:
2、二元移动窗口函数
有些统计运算(如相关系数和协方差)需要在两个时间序列上执行。例如,金融分析师常常对某只股票对某个参考指数(如标准普尔500指数)的相关系数感兴趣。要进行说明,我们先计算我们感兴趣的时间序列的百分数变化:
调用rolling之后,corr聚合函数开始计算与spx_rets滚动相关系数:
假设你想要一次性计算多只股票与标准普尔500指数的相关系数。虽然编写一个循环并新建一个DataFrame不是什么难事,但比较啰嗦。其实,只需传入一个TimeSeries和一个DataFrame,rolling_corr就会自动计算TimeSeries(本例中就是spx_rets)与DataFrame各列的相关系数:
3、用户定义的移动窗口函数
rolling_apply函数使你能够在移动窗口上应用自己设计的数组函数。唯一要求的就是:该函数要能从数组的各个片段中产生单个值(即约简)。比如说,当我们用rolling(...).quantile(q)计算样本分位数时,可能对样本中特定值的百分等级感兴趣。scipy.stats.percentileofscore函数就能达到这个目的:
快速学习: