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

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

  1. In [380]: ts In [381]: ts.resample('D')
  2. Out[380]: Out[381]:
  3. 2011-01-02 0.690002 2011-01-02 0.690002
  4. 2011-01-05 1.001543 2011-01-03 NaN
  5. 2011-01-07 -0.503087 2011-01-04 NaN
  6. 2011-01-08 -0.622274 2011-01-05 1.001543
  7. 2011-01-10 -0.921169 2011-01-06 NaN
  8. 2011-01-12 -0.726213 2011-01-07 -0.503087
  9. 2011-01-08 -0.622274
  10. 2011-01-09 NaN
  11. 2011-01-10 -0.921169
  12. 2011-01-11 NaN
  13. 2011-01-12 -0.726213
  14. Freq: D

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

生成日期范围

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

  1. In [382]: index = pd.date_range('4/1/2012', '6/1/2012')
  2.  
  3. In [383]: index
  4. Out[383]:
  5. <class 'pandas.tseries.index.DatetimeIndex'>
  6. [2012-04-01 00:00:00, ..., 2012-06-01 00:00:00]
  7. Length: 62, Freq: D, Timezone: None

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

  1. In [384]: pd.date_range(start='4/1/2012', periods=20)
  2. Out[384]:
  3. <class 'pandas.tseries.index.DatetimeIndex'>
  4. [2012-04-01 00:00:00, ..., 2012-04-20 00:00:00]
  5. Length: 20, Freq: D, Timezone: None
  6.  
  7. In [385]: pd.date_range(end='6/1/2012', periods=20)
  8. Out[385]:
  9. <class 'pandas.tseries.index.DatetimeIndex'>
  10. [2012-05-13 00:00:00, ..., 2012-06-01 00:00:00]
  11. Length: 20, Freq: D, Timezone: None

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

  1. In [386]: pd.date_range('1/1/2000', '12/1/2000', freq='BM')
  2. Out[386]:
  3. <class 'pandas.tseries.index.DatetimeIndex'>
  4. [2000-01-31 00:00:00, ..., 2000-11-30 00:00:00]
  5. Length: 11, Freq: BM, Timezone: None

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

  1. In [387]: pd.date_range('5/2/2012 12:56:31', periods=5)
  2. Out[387]:
  3. <class 'pandas.tseries.index.DatetimeIndex'>
  4. [2012-05-02 12:56:31, ..., 2012-05-06 12:56:31]
  5. Length: 5, Freq: D, Timezone: None

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

  1. In [388]: pd.date_range('5/2/2012 12:56:31', periods=5, normalize=True)
  2. Out[388]:
  3. <class 'pandas.tseries.index.DatetimeIndex'>
  4. [2012-05-02 00:00:00, ..., 2012-05-06 00:00:00]
  5. Length: 5, Freq: D, Timezone: None

频率和日期偏移量

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

  1. In [389]: from pandas.tseries.offsets import Hour, Minute
  2.  
  3. In [390]: hour = Hour()
  4.  
  5. In [391]: hour
  6. Out[391]: <1 Hour>

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

  1. In [392]: four_hours = Hour(4)
  2.  
  3. In [393]: four_hours
  4. Out[393]: <4 Hours>

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

  1. In [394]: pd.date_range('1/1/2000', '1/3/2000 23:59', freq='4h')
  2. Out[394]:
  3. <class 'pandas.tseries.index.DatetimeIndex'>
  4. [2000-01-01 00:00:00, ..., 2000-01-03 20:00:00]
  5. Length: 18, Freq: 4H, Timezone: None

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

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

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

  1. In [396]: pd.date_range('1/1/2000', periods=10, freq='1h30min')
  2. Out[396]:
  3. <class 'pandas.tseries.index.DatetimeIndex'>
  4. [2000-01-01 00:00:00, ..., 2000-01-01 13:30:00]
  5. Length: 10, Freq: 90T, Timezone: None

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

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

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

日期的范围、频率以及移动 - 图1

日期的范围、频率以及移动 - 图2

日期的范围、频率以及移动 - 图3

WOM日期

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

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

美国的股票期权交易人会意识到这些日子就是标准的月度到期日。

移动(超前和滞后)数据

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

  1. In [399]: ts = Series(np.random.randn(4),
  2. ...: index=pd.date_range('1/1/2000', periods=4, freq='M'))
  3. In [400]: ts In [401]: ts.shift(2) In [402]: ts.shift(-2)
  4. Out[400]: Out[401]: Out[402]:
  5. 2000-01-31 0.575283 2000-01-31 NaN 2000-01-31 1.814582
  6. 2000-02-29 0.304205 2000-02-29 NaN 2000-02-29 1.634858
  7. 2000-03-31 1.814582 2000-03-31 0.575283 2000-03-31 NaN
  8. 2000-04-30 1.634858 2000-04-30 0.304205 2000-04-30 NaN
  9. Freq: M Freq: M Freq: M

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

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

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

  1. In [403]: ts.shift(2, freq='M')
  2. Out[403]:
  3. 2000-03-31 0.575283
  4. 2000-04-30 0.304205
  5. 2000-05-31 1.814582
  6. 2000-06-30 1.634858
  7. Freq: M

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

  1. In [404]: ts.shift(3, freq='D') In [405]: ts.shift(1, freq='3D')
  2. Out[404]: Out[405]:
  3. 2000-02-03 0.575283 2000-02-03 0.575283
  4. 2000-03-03 0.304205 2000-03-03 0.304205
  5. 2000-04-03 1.814582 2000-04-03 1.814582
  6. 2000-05-03 1.634858 2000-05-03 1.634858
  7.  
  8. In [406]: ts.shift(1, freq='90T')
  9. Out[406]:
  10. 2000-01-31 01:30:00 0.575283
  11. 2000-02-29 01:30:00 0.304205
  12. 2000-03-31 01:30:00 1.814582
  13. 2000-04-30 01:30:00 1.634858

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

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

  1. In [407]: from pandas.tseries.offsets import Day, MonthEnd
  2.  
  3. In [408]: now = datetime(2011, 11, 17)
  4.  
  5. In [409]: now + 3 * Day()
  6. Out[409]: datetime.datetime(2011, 11, 20, 0, 0)

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

  1. In [410]: now + MonthEnd()
  2. Out[410]: datetime.datetime(2011, 11, 30, 0, 0)
  3.  
  4. In [411]: now + MonthEnd(2)
  5. Out[411]: datetime.datetime(2011, 12, 31, 0, 0)

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

  1. In [412]: offset = MonthEnd()
  2.  
  3. In [413]: offset.rollforward(now)
  4. Out[413]: datetime.datetime(2011, 11, 30, 0, 0)
  5.  
  6. In [414]: offset.rollback(now)
  7. Out[414]: datetime.datetime(2011, 10, 31, 0, 0)

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

  1. In [415]: ts = Series(np.random.randn(20),
  2. ...: index=pd.date_range('1/15/2000', periods=20, freq='4d'))
  3.  
  4. In [416]: ts.groupby(offset.rollforward).mean()
  5. Out[416]:
  6. 2000-01-31 -0.448874
  7. 2000-02-29 -0.683663
  8. 2000-03-31 0.251920

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

  1. In [417]: ts.resample('M', how='mean')
  2. Out[417]:
  3. 2000-01-31 -0.448874
  4. 2000-02-29 -0.683663
  5. 2000-03-31 0.251920
  6. Freq: M

译注5:拿本例来说,就是第一次位移的量可能没有一个月那么长,就在当月。