查看原文
其他

《利用Python进行数据分析·第2版》第11章(下) 时间序列

SeanCheney Python爱好者社区 2019-04-07

作者:SeanCheney   Python爱好者社区专栏作者

简书专栏:https://www.jianshu.com/u/130f76596b02


前文传送门:

【翻译】《利用Python进行数据分析·第2版》第1章 准备工作

【翻译】《利用Python进行数据分析·第2版》第2章(上)Python语法基础,IPython和Jupyter

【翻译】《利用Python进行数据分析·第2版》第2章(中)Python语法基础,IPython和Jupyter

【翻译】《利用Python进行数据分析·第2版》第2章(下)Python语法基础,IPython和Jupyter

【翻译】《利用Python进行数据分析·第2版》第3章(上)Python的数据结构、函数和文件

【翻译】《利用Python进行数据分析·第2版》第3章(中)Python的数据结构、函数和文件

【翻译】《利用Python进行数据分析·第2版》第3章(下)Python的数据结构、函数和文件

【翻译】《利用Python进行数据分析·第2版》第4章(上)NumPy基础:数组和矢量计算

【翻译】《利用Python进行数据分析·第2版》第4章(中)NumPy基础:数组和矢量计算

【翻译】《利用Python进行数据分析·第2版》第4章(下)NumPy基础:数组和矢量计算

【翻译】《利用Python进行数据分析·第2版》第5章(上)pandas入门

【翻译】《利用Python进行数据分析·第2版》第5章(中)pandas入门

【翻译】《利用Python进行数据分析·第2版》第5章(下)pandas入门

【翻译】《利用Python进行数据分析·第2版》第6章(上) 数据加载、存储与文件格式

【翻译】《利用Python进行数据分析·第2版》第6章(中) 数据加载、存储与文件格式

【翻译】《利用Python进行数据分析·第2版》第6章(下) 数据加载、存储与文件格式

【翻译】《利用Python进行数据分析·第2版》第7章(上)数据清洗和准备

【翻译】《利用Python进行数据分析·第2版》第7章(中) 数据清洗和准备

【翻译】《利用Python进行数据分析·第2版》第7章(下) 数据清洗和准备

【翻译】《利用Python进行数据分析·第2版》第8章(上) 数据规整:聚合、合并和重塑

【翻译】《利用Python进行数据分析·第2版》第8章(中) 数据规整:聚合、合并和重塑

【翻译】《利用Python进行数据分析·第2版》第8章(下) 数据规整:聚合、合并和重塑

【翻译】《利用Python进行数据分析·第2版》第9章(上) 绘图和可视化

【翻译】《利用Python进行数据分析·第2版》第9章(中) 绘图和可视化

  【翻译】《利用Python进行数据分析·第2版》第9章(下) 绘图和可视化

  【翻译】《利用Python进行数据分析·第2版》第10章(上) 数据聚合与分组运算

  【翻译】《利用Python进行数据分析·第2版》第10章(中) 数据聚合与分组运算

  【翻译】《利用Python进行数据分析·第2版》第10章(下) 数据聚合与分组运算

  【翻译】《利用Python进行数据分析·第2版》第11章(上) 时间序列

  【翻译】《利用Python进行数据分析·第2版》第11章(中) 时间序列

  【翻译】《利用Python进行数据分析·第2版》第11章(中二) 时间序列


11.6 重采样及频率转换


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


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

In [208]: rng = pd.date_range('2000-01-01', periods=100, freq='D') In [209]: ts = pd.Series(np.random.randn(len(rng)), index=rng) In [210]: ts Out[210]: 2000-01-01    0.631634 2000-01-02   -1.594313 2000-01-03   -1.519937 2000-01-04    1.108752 2000-01-05    1.255853 2000-01-06   -0.024330 2000-01-07   -2.047939 2000-01-08   -0.272657 2000-01-09   -1.692615 2000-01-10    1.423830                ...   2000-03-31   -0.007852 2000-04-01   -1.638806 2000-04-02    1.401227 2000-04-03    1.758539 2000-04-04    0.628932 2000-04-05   -0.423776 2000-04-06    0.789740 2000-04-07    0.937568 2000-04-08   -2.253294 2000-04-09   -1.772919 Freq: D, Length: 100, dtype: float64 In [211]: ts.resample('M').mean() Out[211]: 2000-01-31   -0.165893 2000-02-29    0.078606 2000-03-31    0.223811 2000-04-30   -0.063643 Freq: M, dtype: float64 In [212]: ts.resample('M', kind='period').mean() Out[212]: 2000-01   -0.165893 2000-02    0.078606 2000-03    0.223811 2000-04   -0.063643 Freq: M, dtype: float64

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


表11-5 resample方法的参数


降采样


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

  • 各区间哪边是闭合的。

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


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

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

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

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

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

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

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

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

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


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


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

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

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


OHLC重采样


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

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

升采样和插值


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

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

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

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

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

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

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

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

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

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

通过时期进行重采样


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

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

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

# Q-DEC: Quarterly, year ending in December In [232]: annual_frame.resample('Q-DEC').ffill() Out[232]:        Colorado     Texas  New York      Ohio 2000Q1  0.556703  0.016631  0.111873 -0.027445 2000Q2  0.556703  0.016631  0.111873 -0.027445 2000Q3  0.556703  0.016631  0.111873 -0.027445 2000Q4  0.556703  0.016631  0.111873 -0.027445 2001Q1  0.046303  0.163344  0.251503 -0.157276 2001Q2  0.046303  0.163344  0.251503 -0.157276 2001Q3  0.046303  0.163344  0.251503 -0.157276 2001Q4  0.046303  0.163344  0.251503 -0.157276 In [233]: annual_frame.resample('Q-DEC', convention='end').ffill() Out[233]:        Colorado     Texas  New York      Ohio 2000Q4  0.556703  0.016631  0.111873 -0.027445 2001Q1  0.556703  0.016631  0.111873 -0.027445 2001Q2  0.556703  0.016631  0.111873 -0.027445 2001Q3  0.556703  0.016631  0.111873 -0.027445 2001Q4  0.046303  0.163344  0.251503 -0.157276

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

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

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

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

In [234]: annual_frame.resample('Q-MAR').ffill() Out[234]:        Colorado     Texas  New York      Ohio 2000Q4  0.556703  0.016631  0.111873 -0.027445 2001Q1  0.556703  0.016631  0.111873 -0.027445 2001Q2  0.556703  0.016631  0.111873 -0.027445 2001Q3  0.556703  0.016631  0.111873 -0.027445 2001Q4  0.046303  0.163344  0.251503 -0.157276 2002Q1  0.046303  0.163344  0.251503 -0.157276 2002Q2  0.046303  0.163344  0.251503 -0.157276 2002Q3  0.046303  0.163344  0.251503 -0.157276

11.7 移动窗口函数


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


开始之前,我们加载一些时间序列数据,将其重采样为工作日频率:

In [235]: close_px_all = pd.read_csv('examples/stock_px_2.csv',   .....:                            parse_dates=True, index_col=0) In [236]: close_px = close_px_all[['AAPL', 'MSFT', 'XOM']] In [237]: close_px = close_px.resample('B').ffill()

现在引入rolling运算符,它与resample和groupby很像。可以在TimeSeries或DataFrame以及一个window(表示期数,见图11-4)上调用它:

In [238]: close_px.AAPL.plot() Out[238]: <matplotlib.axes._subplots.AxesSubplot at 0x7f2f2570cf98> In [239]: close_px.AAPL.rolling(250).mean().plot()

图11-4 苹果公司股价的250日均线


表达式rolling(250)与groupby很像,但不是对其进行分组、创建一个按照250天分组的滑动窗口对象。然后,我们就得到了苹果公司股价的250天的移动窗口。


默认情况下,诸如rolling_mean这样的函数需要指定数量的非NA观测值。可以修改该行为以解决缺失数据的问题。其实,在时间序列开始处尚不足窗口期的那些数据就是个特例(见图11-5):

In [241]: appl_std250 = close_px.AAPL.rolling(250, min_periods=10).std() In [242]: appl_std250[5:12] Out[242]: 2003-01-09         NaN 2003-01-10         NaN 2003-01-13         NaN 2003-01-14         NaN 2003-01-15    0.077496 2003-01-16    0.074760 2003-01-17    0.112368 Freq: B, Name: AAPL, dtype: float64 In [243]: appl_std250.plot()

图11-5 苹果公司250日每日回报标准差


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

In [244]: expanding_mean = appl_std250.expanding().mean()

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

In [246]: close_px.rolling(60).mean().plot(logy=True)

图11-6 各股价60日均线(对数Y轴)


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

In [247]: close_px.rolling('20D').mean() Out[247]:                  AAPL       MSFT        XOM 2003-01-02    7.400000  21.110000  29.220000 2003-01-03    7.425000  21.125000  29.230000 2003-01-06    7.433333  21.256667  29.473333 2003-01-07    7.432500  21.425000  29.342500 2003-01-08    7.402000  21.402000  29.240000 2003-01-09    7.391667  21.490000  29.273333 2003-01-10    7.387143  21.558571  29.238571 2003-01-13    7.378750  21.633750  29.197500 2003-01-14    7.370000  21.717778  29.194444 2003-01-15    7.355000  21.757000  29.152000 ...                ...        ...        ... 2011-10-03  398.002143  25.890714  72.413571 2011-10-04  396.802143  25.807857  72.427143 2011-10-05  395.751429  25.729286  72.422857 2011-10-06  394.099286  25.673571  72.375714 2011-10-07  392.479333  25.712000  72.454667 2011-10-10  389.351429  25.602143  72.527857 2011-10-11  388.505000  25.674286  72.835000 2011-10-12  388.531429  25.810000  73.400714 2011-10-13  388.826429  25.961429  73.905000 2011-10-14  391.038000  26.048667  74.185333 [2292 rows x 3 columns]

指数加权函数


另一种使用固定大小窗口及相等权数观测值的办法是,定义一个衰减因子(decay factor)常量,以便使近期的观测值拥有更大的权数。衰减因子的定义方式有很多,比较流行的是使用时间间隔(span),它可以使结果兼容于窗口大小等于时间间隔的简单移动窗口(simple moving window)函数。


由于指数加权统计会赋予近期的观测值更大的权数,因此相对于等权统计,它能“适应”更快的变化。


除了rolling和expanding,pandas还有ewm运算符。下面这个例子对比了苹果公司股价的60日移动平均和span=60的指数加权移动平均(如图11-7所示):

In [249]: aapl_px = close_px.AAPL['2006':'2007'] In [250]: ma60 = aapl_px.rolling(30, min_periods=20).mean() In [251]: ewma60 = aapl_px.ewm(span=30).mean() In [252]: ma60.plot(style='k--', label='Simple MA') Out[252]: <matplotlib.axes._subplots.AxesSubplot at 0x7f2f252161d0> In [253]: ewma60.plot(style='', label='EW MA') Out[253]: <matplotlib.axes._subplots.AxesSubplot at 0x7f2f252161d0> In [254]: plt.legend()

图11-7 简单移动平均与指数加权移动平均


二元移动窗口函数


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

In [256]: spx_px = close_px_all['SPX'] In [257]: spx_rets = spx_px.pct_change() In [258]: returns = close_px.pct_change()

调用rolling之后,corr聚合函数开始计算与spx_rets滚动相关系数(结果见图11-8):

In [259]: corr = returns.AAPL.rolling(125, min_periods=100).corr(spx_rets) In [260]: corr.plot()

图11-8 AAPL 6个月的回报与标准普尔500指数的相关系数


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

In [262]: corr = returns.rolling(125, min_periods=100).corr(spx_rets) In [263]: corr.plot()

图11-9 3只股票6个月的回报与标准普尔500指数的相关系数


用户定义的移动窗口函数


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

In [265]: from scipy.stats import percentileofscore In [266]: score_at_2percent = lambda x: percentileofscore(x, 0.02) In [267]: result = returns.AAPL.rolling(250).apply(score_at_2percent) In [268]: result.plot()

图11-10 AAPL 2%回报率的百分等级(一年窗口期)


如果你没安装SciPy,可以使用conda或pip安装。


11.8 总结


与前面章节接触的数据相比,时间序列数据要求不同类型的分析和数据转换工具。

在接下来的章节中,我们将学习一些高级的pandas方法和如何开始使用建模库statsmodels和scikit-learn。


赞赏作者

Python爱好者社区历史文章大合集

Python爱好者社区历史文章列表(每周append更新一次)

福利:文末扫码立刻关注公众号,“Python爱好者社区”,开始学习Python课程:

关注后在公众号内回复“课程”即可获取:

小编的Python入门视频课程!!!

崔老师爬虫实战案例免费学习视频。

丘老师数据科学入门指导免费学习视频。

陈老师数据分析报告制作免费学习视频。

玩转大数据分析!Spark2.X+Python 精华实战课程免费学习视频。

丘老师Python网络爬虫实战免费学习视频。


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存