时区处理

时间序列处理工作中最让人不爽的就是对时区的处理。尤其是夏令时(DST)转变,这是一种最常见的麻烦事。就这一点来说,许多人都选择以协调世界时(UTC,它是格林尼治标准时间(Greenwich Mean Time)的接替者,目前已经是国际标准了)来处理时间序列。时区是以UTC偏移量的形式表示的。例如,夏令时期间,纽约比UTC慢4小时,而在全年其他时间则比UTC慢5小时。

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

有关pytz库的更多信息,请查阅其文档。就本书而言,由于pandas包装了pytz的功能,因此你可以不用记忆其API,只要记得时区的名称即可。时区名可以在文档中找到,也可以通过交互的方式查看:

  1. In [418]: import pytz
  2.  
  3. In [419]: pytz.common_timezones[-5:]
  4. Out[419]: ['US/Eastern', 'US/Hawaii', 'US/Mountain', 'US/Pacific', 'UTC']

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

  1. In [420]: tz = pytz.timezone('US/Eastern')
  2.  
  3. In [421]: tz
  4. Out[421]: <DstTzInfo 'US/Eastern' EST-1 day, 19:00:00 STD>

pandas中的方法既可以接受时区名也可以接受这种对象。我建议只用时区名。

本地化和转换

默认情况下,pandas中的时间序列是单纯的(naive)时区。看看下面这个时间序列:

  1. rng = pd.date_range('3/9/2012 9:30', periods=6, freq='D')
  2. ts = Series(np.random.randn(len(rng)), index=rng)

其索引的tz字段为None:

  1. In [423]: print(ts.index.tz)
  2. None

在生成日期范围的时候还可以加上一个时区集:

  1. In [424]: pd.date_range('3/9/2012 9:30', periods=10, freq='D', tz='UTC')
  2. Out[424]:
  3. <class 'pandas.tseries.index.DatetimeIndex'>
  4. [2012-03-09 09:30:00, ..., 2012-03-18 09:30:00]
  5. Length: 10, Freq: D, Timezone: UTC

从单纯到本地化的转换是通过tz_localize方法处理的:

  1. In [425]: ts_utc = ts.tz_localize('UTC')
  2.  
  3. In [426]: ts_utc
  4. Out[426]:
  5. 2012-03-09 09:30:00+00:00 0.414615
  6. 2012-03-10 09:30:00+00:00 0.427185
  7. 2012-03-11 09:30:00+00:00 1.172557
  8. 2012-03-12 09:30:00+00:00 -0.351572
  9. 2012-03-13 09:30:00+00:00 1.454593
  10. 2012-03-14 09:30:00+00:00 2.043319
  11. Freq: D
  12.  
  13. In [427]: ts_utc.index
  14. Out[427]:
  15. <class 'pandas.tseries.index.DatetimeIndex'>
  16. [2012-03-09 09:30:00, ..., 2012-03-14 09:30:00]
  17. Length: 6, Freq: D, Timezone: UTC

一旦时间序列被本地化到某个特定时区,就可以用tz_convert将其转换到别的时区了:

  1. In [428]: ts_utc.tz_convert('US/Eastern')
  2. Out[428]:
  3. 2012-03-09 04:30:00-05:00 0.414615
  4. 2012-03-10 04:30:00-05:00 0.427185
  5. 2012-03-11 05:30:00-04:00 1.172557
  6. 2012-03-12 05:30:00-04:00 -0.351572
  7. 2012-03-13 05:30:00-04:00 1.454593
  8. 2012-03-14 05:30:00-04:00 2.043319
  9. Freq: D

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

  1. In [429]: ts_eastern = ts.tz_localize('US/Eastern')
  2.  
  3. In [430]: ts_eastern.tz_convert('UTC')
  4. Out[430]:
  5. 2012-03-09 14:30:00+00:00 0.414615
  6. 2012-03-10 14:30:00+00:00 0.427185
  7. 2012-03-11 13:30:00+00:00 1.172557
  8. 2012-03-12 13:30:00+00:00 -0.351572
  9. 2012-03-13 13:30:00+00:00 1.454593
  10. 2012-03-14 13:30:00+00:00 2.043319
  11. Freq: D
  12.  
  13. In [431]: ts_eastern.tz_convert('Europe/Berlin')
  14. Out[431]:
  15. 2012-03-09 15:30:00+01:00 0.414615
  16. 2012-03-10 15:30:00+01:00 0.427185
  17. 2012-03-11 14:30:00+01:00 1.172557
  18. 2012-03-12 14:30:00+01:00 -0.351572
  19. 2012-03-13 14:30:00+01:00 1.454593
  20. 2012-03-14 14:30:00+01:00 2.043319
  21. Freq: D

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

  1. In [432]: ts.index.tz_localize('Asia/Shanghai')
  2. Out[432]:
  3. <class 'pandas.tseries.index.DatetimeIndex'>
  4. [2012-03-09 09:30:00, ..., 2012-03-14 09:30:00]
  5. Length: 6, Freq: D, Timezone: Asia/Shanghai

警告: 对单纯时间戳的本地化操作还会检查夏令时转变期附近容易混淆或不存在的时间。

操作时区意识型Timestamp对象

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

  1. In [433]: stamp = pd.Timestamp('2011-03-12 04:00')
  2.  
  3. In [434]: stamp_utc = stamp.tz_localize('utc')
  4.  
  5. In [435]: stamp_utc.tz_convert('US/Eastern')
  6. Out[435]: <Timestamp: 2011-03-11 23:00:00-0500 EST, tz=US/Eastern>

在创建Timestamp时,还可以传入一个时区信息:

  1. In [436]: stamp_moscow = pd.Timestamp('2011-03-12 04:00', tz='Europe/Moscow')
  2.  
  3. In [437]: stamp_moscow
  4. Out[437]: <Timestamp: 2011-03-12 04:00:00+0300 MSK, tz=Europe/Moscow>

时区意识型Timestamp对象在内部保存了一个UTC时间戳值(自UNIX纪元(1970年1月1日)算起的纳秒数)。这个UTC值在时区转换过程中是不会发生变化的:

  1. In [438]: stamp_utc.value
  2. Out[438]: 1299902400000000000
  3.  
  4. In [439]: stamp_utc.tz_convert('US/Eastern').value
  5. Out[439]: 1299902400000000000

当使用pandas的DateOffset对象执行时间算术运算时,运算过程会自动关注是否存在夏令时转变期:

  1. # 夏令时转变前30分钟
  2. In [440]: from pandas.tseries.offsets import Hour
  3.  
  4. In [441]: stamp = pd.Timestamp('2012-03-12 01:30', tz='US/Eastern')
  5.  
  6. In [442]: stamp
  7. Out[442]: <Timestamp: 2012-03-12 01:30:00-0400 EDT, tz=US/Eastern>
  8.  
  9. In [443]: stamp + Hour()
  10. Out[443]: <Timestamp: 2012-03-12 02:30:00-0400 EDT, tz=US/Eastern>
  11.  
  12. # 夏令时转变前90分钟
  13. In [444]: stamp = pd.Timestamp('2012-11-04 00:30', tz='US/Eastern')
  14.  
  15. In [445]: stamp
  16. Out[445]: <Timestamp: 2012-11-04 00:30:00-0400 EDT, tz=US/Eastern>
  17.  
  18. In [446]: stamp + 2 * Hour()
  19. Out[446]: <Timestamp: 2012-11-04 01:30:00-0500 EST, tz=US/Eastern>

不同时区之间的运算

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

  1. In [447]: rng = pd.date_range('3/7/2012 9:30', periods=10, freq='B')
  2.  
  3. In [448]: ts = Series(np.random.randn(len(rng)), index=rng)
  4.  
  5. In [449]: ts
  6. Out[449]:
  7. 2012-03-07 09:30:00 -1.749309
  8. 2012-03-08 09:30:00 -0.387235
  9. 2012-03-09 09:30:00 -0.208074
  10. 2012-03-12 09:30:00 -1.221957
  11. 2012-03-13 09:30:00 -0.067460
  12. 2012-03-14 09:30:00 0.229005
  13. 2012-03-15 09:30:00 -0.576234
  14. 2012-03-16 09:30:00 0.816895
  15. 2012-03-19 09:30:00 -0.772192
  16. 2012-03-20 09:30:00 -1.333576
  17. Freq: B
  18.  
  19. In [450]: ts1 = ts[:7].tz_localize('Europe/London')
  20.  
  21. In [451]: ts2 = ts1[2:].tz_convert('Europe/Moscow')
  22.  
  23. In [452]: result = ts1 + ts2
  24.  
  25. In [453]: result.index
  26. Out[453]:
  27. <class 'pandas.tseries.index.DatetimeIndex'>
  28. [2012-03-07 09:30:00, ..., 2012-03-15 09:30:00]
  29. Length: 7, Freq: B, Timezone: UTC

译注6:也叫时区信息数据库,以创始人David Olson命名。