• 11.6 重采样及频率转换
    • 降采样
    • OHLC重采样
    • 升采样和插值
    • 通过时期进行重采样

    11.6 重采样及频率转换

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

    pandas对象都带有一个resample方法,它是各种频率转换工作的主力函数。resample有一个类似于groupby的API,调用resample可以分组数据,然后会调用一个聚合函数:

    1. In [208]: rng = pd.date_range('2000-01-01', periods=100, freq='D')
    2. In [209]: ts = pd.Series(np.random.randn(len(rng)), index=rng)
    3. In [210]: ts
    4. Out[210]:
    5. 2000-01-01 0.631634
    6. 2000-01-02 -1.594313
    7. 2000-01-03 -1.519937
    8. 2000-01-04 1.108752
    9. 2000-01-05 1.255853
    10. 2000-01-06 -0.024330
    11. 2000-01-07 -2.047939
    12. 2000-01-08 -0.272657
    13. 2000-01-09 -1.692615
    14. 2000-01-10 1.423830
    15. ...
    16. 2000-03-31 -0.007852
    17. 2000-04-01 -1.638806
    18. 2000-04-02 1.401227
    19. 2000-04-03 1.758539
    20. 2000-04-04 0.628932
    21. 2000-04-05 -0.423776
    22. 2000-04-06 0.789740
    23. 2000-04-07 0.937568
    24. 2000-04-08 -2.253294
    25. 2000-04-09 -1.772919
    26. Freq: D, Length: 100, dtype: float64
    27. In [211]: ts.resample('M').mean()
    28. Out[211]:
    29. 2000-01-31 -0.165893
    30. 2000-02-29 0.078606
    31. 2000-03-31 0.223811
    32. 2000-04-30 -0.063643
    33. Freq: M, dtype: float64
    34. In [212]: ts.resample('M', kind='period').mean()
    35. Out[212]:
    36. 2000-01 -0.165893
    37. 2000-02 0.078606
    38. 2000-03 0.223811
    39. 2000-04 -0.063643
    40. Freq: M, dtype: float64

    resample是一个灵活高效的方法,可用于处理非常大的时间序列。我将通过一系列的示例说明其用法。表11-5总结它的一些选项。

    表11-5 resample方法的参数
    11.6 重采样及频率转换 - 图1

    降采样

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

    • 各区间哪边是闭合的。
    • 如何标记各个聚合面元,用区间的开头还是末尾。

    为了说明,我们来看一些“1分钟”数据:

    1. In [213]: rng = pd.date_range('2000-01-01', periods=12, freq='T')
    2. In [214]: ts = pd.Series(np.arange(12), index=rng)
    3. In [215]: ts
    4. Out[215]:
    5. 2000-01-01 00:00:00 0
    6. 2000-01-01 00:01:00 1
    7. 2000-01-01 00:02:00 2
    8. 2000-01-01 00:03:00 3
    9. 2000-01-01 00:04:00 4
    10. 2000-01-01 00:05:00 5
    11. 2000-01-01 00:06:00 6
    12. 2000-01-01 00:07:00 7
    13. 2000-01-01 00:08:00 8
    14. 2000-01-01 00:09:00 9
    15. 2000-01-01 00:10:00 10
    16. 2000-01-01 00:11:00 11
    17. Freq: T, dtype: int64

    假设你想要通过求和的方式将这些数据聚合到“5分钟”块中:

    1. In [216]: ts.resample('5min', closed='right').sum()
    2. Out[216]:
    3. 1999-12-31 23:55:00 0
    4. 2000-01-01 00:00:00 15
    5. 2000-01-01 00:05:00 40
    6. 2000-01-01 00:10:00 11
    7. Freq: 5T, dtype: int64

    传入的频率将会以“5分钟”的增量定义面元边界。默认情况下,面元的右边界是包含的,因此00:00到00:05的区间中是包含00:05的。传入closed=’left’会让区间以左边界闭合:

    1. In [217]: ts.resample('5min', closed='right').sum()
    2. Out[217]:
    3. 1999-12-31 23:55:00 0
    4. 2000-01-01 00:00:00 15
    5. 2000-01-01 00:05:00 40
    6. 2000-01-01 00:10:00 11
    7. Freq: 5T, dtype: int64

    如你所见,最终的时间序列是以各面元右边界的时间戳进行标记的。传入label=’right’即可用面元的邮编界对其进行标记:

    1. In [218]: ts.resample('5min', closed='right', label='right').sum()
    2. Out[218]:
    3. 2000-01-01 00:00:00 0
    4. 2000-01-01 00:05:00 15
    5. 2000-01-01 00:10:00 40
    6. 2000-01-01 00:15:00 11
    7. Freq: 5T, dtype: int64

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

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

    最后,你可能希望对结果索引做一些位移,比如从右边界减去一秒以便更容易明白该时间戳到底表示的是哪个区间。只需通过loffset设置一个字符串或日期偏移量即可实现这个目的:

    1. In [219]: ts.resample('5min', closed='right',
    2. .....: label='right', loffset='-1s').sum()
    3. Out[219]:
    4. 1999-12-31 23:59:59 0
    5. 2000-01-01 00:04:59 15
    6. In [219]: ts.resample('5min', closed='right',
    7. .....: label='right', loffset='-1s').sum()
    8. Out[219]:
    9. 1999-12-31 23:59:59 0
    10. 2000-01-01 00:04:59 15

    此外,也可以通过调用结果对象的shift方法来实现该目的,这样就不需要设置loffset了。

    OHLC重采样

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

    1. In [220]: ts.resample('5min').ohlc()
    2. Out[220]:
    3. open high low close
    4. 2000-01-01 00:00:00 0 4 0 4
    5. 2000-01-01 00:05:00 5 9 5 9
    6. 2000-01-01 00:10:00 10 11 10 11

    升采样和插值

    在将数据从低频率转换到高频率时,就不需要聚合了。我们来看一个带有一些周型数据的DataFrame:

    1. In [221]: frame = pd.DataFrame(np.random.randn(2, 4),
    2. .....: index=pd.date_range('1/1/2000', periods=2,
    3. .....: freq='W-WED'),
    4. .....: columns=['Colorado', 'Texas', 'New York', 'Ohio'])
    5. In [222]: frame
    6. Out[222]:
    7. Colorado Texas New York Ohio
    8. 2000-01-05 -0.896431 0.677263 0.036503 0.087102
    9. 2000-01-12 -0.046662 0.927238 0.482284 -0.867130

    当你对这个数据进行聚合,每组只有一个值,这样就会引入缺失值。我们使用asfreq方法转换成高频,不经过聚合:

    1. In [223]: df_daily = frame.resample('D').asfreq()
    2. In [224]: df_daily
    3. Out[224]:
    4. Colorado Texas New York Ohio
    5. 2000-01-05 -0.896431 0.677263 0.036503 0.087102
    6. 2000-01-06 NaN NaN NaN NaN
    7. 2000-01-07 NaN NaN NaN NaN
    8. 2000-01-08 NaN NaN NaN NaN
    9. 2000-01-09 NaN NaN NaN NaN
    10. 2000-01-10 NaN NaN NaN NaN
    11. 2000-01-11 NaN NaN NaN NaN
    12. 2000-01-12 -0.046662 0.927238 0.482284 -0.867130

    假设你想要用前面的周型值填充“非星期三”。resampling的填充和插值方式跟fillna和reindex的一样:

    1. In [225]: frame.resample('D').ffill()
    2. Out[225]:
    3. Colorado Texas New York Ohio
    4. 2000-01-05 -0.896431 0.677263 0.036503 0.087102
    5. 2000-01-06 -0.896431 0.677263 0.036503 0.087102
    6. 2000-01-07 -0.896431 0.677263 0.036503 0.087102
    7. 2000-01-08 -0.896431 0.677263 0.036503 0.087102
    8. 2000-01-09 -0.896431 0.677263 0.036503 0.087102
    9. 2000-01-10 -0.896431 0.677263 0.036503 0.087102
    10. 2000-01-11 -0.896431 0.677263 0.036503 0.087102
    11. 2000-01-12 -0.046662 0.927238 0.482284 -0.867130

    同样,这里也可以只填充指定的时期数(目的是限制前面的观测值的持续使用距离):

    1. In [226]: frame.resample('D').ffill(limit=2)
    2. Out[226]:
    3. Colorado Texas New York Ohio
    4. 2000-01-05 -0.896431 0.677263 0.036503 0.087102
    5. 2000-01-06 -0.896431 0.677263 0.036503 0.087102
    6. 2000-01-07 -0.896431 0.677263 0.036503 0.087102
    7. 2000-01-08 NaN NaN NaN NaN
    8. 2000-01-09 NaN NaN NaN NaN
    9. 2000-01-10 NaN NaN NaN NaN
    10. 2000-01-11 NaN NaN NaN NaN
    11. 2000-01-12 -0.046662 0.927238 0.482284 -0.867130

    注意,新的日期索引完全没必要跟旧的重叠:

    1. In [227]: frame.resample('W-THU').ffill()
    2. Out[227]:
    3. Colorado Texas New York Ohio
    4. 2000-01-06 -0.896431 0.677263 0.036503 0.087102
    5. 2000-01-13 -0.046662 0.927238 0.482284 -0.867130

    通过时期进行重采样

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

    1. In [228]: frame = pd.DataFrame(np.random.randn(24, 4),
    2. .....: index=pd.period_range('1-2000', '12-2001',
    3. .....: freq='M'),
    4. .....: columns=['Colorado', 'Texas', 'New York', 'Ohio'])
    5. In [229]: frame[:5]
    6. Out[229]:
    7. Colorado Texas New York Ohio
    8. 2000-01 0.493841 -0.155434 1.397286 1.507055
    9. 2000-02 -1.179442 0.443171 1.395676 -0.529658
    10. 2000-03 0.787358 0.248845 0.743239 1.267746
    11. 2000-04 1.302395 -0.272154 -0.051532 -0.467740
    12. 2000-05 -1.040816 0.426419 0.312945 -1.115689
    13. In [230]: annual_frame = frame.resample('A-DEC').mean()
    14. In [231]: annual_frame
    15. Out[231]:
    16. Colorado Texas New York Ohio
    17. 2000 0.556703 0.016631 0.111873 -0.027445
    18. 2001 0.046303 0.163344 0.251503 -0.157276

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

    1. # Q-DEC: Quarterly, year ending in December
    2. In [232]: annual_frame.resample('Q-DEC').ffill()
    3. Out[232]:
    4. Colorado Texas New York Ohio
    5. 2000Q1 0.556703 0.016631 0.111873 -0.027445
    6. 2000Q2 0.556703 0.016631 0.111873 -0.027445
    7. 2000Q3 0.556703 0.016631 0.111873 -0.027445
    8. 2000Q4 0.556703 0.016631 0.111873 -0.027445
    9. 2001Q1 0.046303 0.163344 0.251503 -0.157276
    10. 2001Q2 0.046303 0.163344 0.251503 -0.157276
    11. 2001Q3 0.046303 0.163344 0.251503 -0.157276
    12. 2001Q4 0.046303 0.163344 0.251503 -0.157276
    13. In [233]: annual_frame.resample('Q-DEC', convention='end').ffill()
    14. Out[233]:
    15. Colorado Texas New York Ohio
    16. 2000Q4 0.556703 0.016631 0.111873 -0.027445
    17. 2001Q1 0.556703 0.016631 0.111873 -0.027445
    18. 2001Q2 0.556703 0.016631 0.111873 -0.027445
    19. 2001Q3 0.556703 0.016631 0.111873 -0.027445
    20. 2001Q4 0.046303 0.163344 0.251503 -0.157276

    由于时期指的是时间区间,所以升采样和降采样的规则就比较严格:

    • 在降采样中,目标频率必须是源频率的子时期(subperiod)。
    • 在升采样中,目标频率必须是源频率的超时期(superperiod)。

    如果不满足这些条件,就会引发异常。这主要影响的是按季、年、周计算的频率。例如,由Q-MAR定义的时间区间只能升采样为A-MAR、A-JUN、A-SEP、A-DEC等:

    1. In [234]: annual_frame.resample('Q-MAR').ffill()
    2. Out[234]:
    3. Colorado Texas New York Ohio
    4. 2000Q4 0.556703 0.016631 0.111873 -0.027445
    5. 2001Q1 0.556703 0.016631 0.111873 -0.027445
    6. 2001Q2 0.556703 0.016631 0.111873 -0.027445
    7. 2001Q3 0.556703 0.016631 0.111873 -0.027445
    8. 2001Q4 0.046303 0.163344 0.251503 -0.157276
    9. 2002Q1 0.046303 0.163344 0.251503 -0.157276
    10. 2002Q2 0.046303 0.163344 0.251503 -0.157276
    11. 2002Q3 0.046303 0.163344 0.251503 -0.157276