11.md 64.3 KB
Newer Older
W
gitbook  
wizardforcel 已提交
1 2
# 第 11 章 时间序列

W
wizardforcel 已提交
3
时间序列(time series)数据是一种重要的结构化数据形式,应用于多个领域,包括金融学、经济学、生态学、神经科学、物理学等。在多个时间点观察或测量到的任何事物都可以形成一段时间序列。很多时间序列是固定频率的,也就是说,数据点是根据某种规律定期出现的(比如每 15 秒、每 5 分钟、每月出现一次)。时间序列也可以是不定期的,没有固定的时间单位或单位之间的偏移量。时间序列数据的意义取决于具体的应用场景,主要有以下几种:
W
gitbook  
wizardforcel 已提交
4 5

- 时间戳(timestamp),特定的时刻。
W
wizardforcel 已提交
6
- 固定时期(period),如 2007 年 1 月或 2010 年全年。
W
gitbook  
wizardforcel 已提交
7 8 9
- 时间间隔(interval),由起始和结束时间戳表示。时期(period)可以被看做间隔(interval)的特例。
- 实验或过程时间,每个时间点都是相对于特定起始时间的一个度量。例如,从放入烤箱时起,每秒钟饼干的直径。

W
wizardforcel 已提交
10
本章主要讲解前 3 种时间序列。许多技术都可用于处理实验型时间序列,其索引可能是一个整数或浮点数(表示从实验开始算起已经过去的时间)。最简单也最常见的时间序列都是用时间戳进行索引的。
W
gitbook  
wizardforcel 已提交
11

W
wizardforcel 已提交
12
> 提示:pandas 也支持基于 timedeltas 的指数,它可以有效代表实验或经过的时间。这本书不涉及 timedelta 指数,但你可以学习 pandas 的文档(http://pandas.pydata.org/)。
W
gitbook  
wizardforcel 已提交
13

W
wizardforcel 已提交
14
pandas 提供了许多内置的时间序列处理工具和数据算法。因此,你可以高效处理非常大的时间序列,轻松地进行切片/切块、聚合、对定期/不定期的时间序列进行重采样等。有些工具特别适合金融和经济应用,你当然也可以用它们来分析服务器日志数据。
W
gitbook  
wizardforcel 已提交
15 16 17

# 11.1 日期和时间数据类型及工具

W
wizardforcel 已提交
18
Python 标准库包含用于日期(date)和时间(time)数据的数据类型,而且还有日历方面的功能。我们主要会用到 datetime、time 以及 calendar 模块。datetime.datetime(也可以简写为 datetime)是用得最多的数据类型:
W
gitbook  
wizardforcel 已提交
19 20 21 22 23 24 25 26 27 28 29 30 31

```python
In [10]: from datetime import datetime

In [11]: now = datetime.now()

In [12]: now
Out[12]: datetime.datetime(2017, 9, 25, 14, 5, 52, 72973)

In [13]: now.year, now.month, now.day
Out[13]: (2017, 9, 25)
```

W
wizardforcel 已提交
32
datetime 以毫秒形式存储日期和时间。timedelta 表示两个 datetime 对象之间的时间差:
W
gitbook  
wizardforcel 已提交
33 34 35 36 37 38 39 40 41 42 43 44 45 46

```python
In [14]: delta = datetime(2011, 1, 7) - datetime(2008, 6, 24, 8, 15)

In [15]: delta
Out[15]: datetime.timedelta(926, 56700)

In [16]: delta.days
Out[16]: 926

In [17]: delta.seconds
Out[17]: 56700
```

W
wizardforcel 已提交
47
可以给 datetime 对象加上(或减去)一个或多个 timedelta,这样会产生一个新对象:
W
gitbook  
wizardforcel 已提交
48 49 50 51 52 53 54 55 56 57 58 59 60

```python
In [18]: from datetime import timedelta

In [19]: start = datetime(2011, 1, 7)

In [20]: start + timedelta(12)
Out[20]: datetime.datetime(2011, 1, 19, 0, 0)

In [21]: start - 2 * timedelta(12)
Out[21]: datetime.datetime(2010, 12, 14, 0, 0)
```

W
wizardforcel 已提交
61
datetime 模块中的数据类型参见表 10-1。虽然本章主要讲的是 pandas 数据类型和高级时间序列处理,但你肯定会在 Python 的其他地方遇到有关 datetime 的数据类型。
W
gitbook  
wizardforcel 已提交
62

W
wizardforcel 已提交
63
表 11-1 datetime 模块中的数据类型
W
gitbook  
wizardforcel 已提交
64 65 66 67 68

![](img/7178691-4af261a305a70aeb.png)

tzinfo  存储时区信息的基本类型

W
wizardforcel 已提交
69
## 字符串和 datetime 的相互转换
W
gitbook  
wizardforcel 已提交
70

W
wizardforcel 已提交
71
利用 str 或 strftime 方法(传入一个格式化字符串),datetime 对象和 pandas 的 Timestamp 对象(稍后就会介绍)可以被格式化为字符串:
W
gitbook  
wizardforcel 已提交
72 73 74 75 76 77 78 79 80 81
```python
In [22]: stamp = datetime(2011, 1, 3)

In [23]: str(stamp)
Out[23]: '2011-01-03 00:00:00'

In [24]: stamp.strftime('%Y-%m-%d')
Out[24]: '2011-01-03'
```

W
wizardforcel 已提交
82
表 11-2 列出了全部的格式化编码。
W
gitbook  
wizardforcel 已提交
83

W
wizardforcel 已提交
84
表 11-2 datetime 格式定义(兼容 ISO C89)
W
gitbook  
wizardforcel 已提交
85 86 87 88 89

![](img/7178691-50c751823754df58.png)

![](img/7178691-de0181e1f6b45eaf.png)

W
wizardforcel 已提交
90
datetime.strptime 可以用这些格式化编码将字符串转换为日期:
W
gitbook  
wizardforcel 已提交
91 92 93 94 95 96 97 98 99 100 101 102 103 104
```python
In [25]: value = '2011-01-03'

In [26]: datetime.strptime(value, '%Y-%m-%d')
Out[26]: datetime.datetime(2011, 1, 3, 0, 0)

In [27]: datestrs = ['7/6/2011', '8/6/2011']

In [28]: [datetime.strptime(x, '%m/%d/%Y') for x in datestrs]
Out[28]: 
[datetime.datetime(2011, 7, 6, 0, 0),
 datetime.datetime(2011, 8, 6, 0, 0)]
```

W
wizardforcel 已提交
105
datetime.strptime 是通过已知格式进行日期解析的最佳方式。但是每次都要编写格式定义是很麻烦的事情,尤其是对于一些常见的日期格式。这种情况下,你可以用 dateutil 这个第三方包中的 parser.parse 方法(pandas 中已经自动安装好了):
W
gitbook  
wizardforcel 已提交
106 107 108 109 110 111
```python
In [29]: from dateutil.parser import parse

In [30]: parse('2011-01-03')
Out[30]: datetime.datetime(2011, 1, 3, 0, 0)
```
W
wizardforcel 已提交
112
dateutil 可以解析几乎所有人类能够理解的日期表示形式:
W
gitbook  
wizardforcel 已提交
113 114 115 116 117 118

```python
In [31]: parse('Jan 31, 1997 10:45 PM')
Out[31]: datetime.datetime(1997, 1, 31, 22, 45)
```

W
wizardforcel 已提交
119
在国际通用的格式中,日出现在月的前面很普遍,传入 dayfirst=True 即可解决这个问题:
W
gitbook  
wizardforcel 已提交
120 121 122 123 124 125

```python
In [32]: parse('6/12/2011', dayfirst=True)
Out[32]: datetime.datetime(2011, 12, 6, 0, 0)
```

W
wizardforcel 已提交
126
pandas 通常是用于处理成组日期的,不管这些日期是`DataFrame`的轴索引还是列。to_datetime 方法可以解析多种不同的日期表示形式。对标准日期格式(如 ISO8601)的解析非常快:
W
gitbook  
wizardforcel 已提交
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150

```python
In [33]: datestrs = ['2011-07-06 12:00:00', '2011-08-06 00:00:00']

In [34]: pd.to_datetime(datestrs)
Out[34]: DatetimeIndex(['2011-07-06 12:00:00', '2011-08-06 00:00:00'], dtype='dat
etime64[ns]', freq=None)
```

它还可以处理缺失值(None、空字符串等):
```python
In [35]: idx = pd.to_datetime(datestrs + [None])

In [36]: idx
Out[36]: DatetimeIndex(['2011-07-06 12:00:00', '2011-08-06 00:00:00', 'NaT'], dty
pe='datetime64[ns]', freq=None)

In [37]: idx[2]
Out[37]: NaT

In [38]: pd.isnull(idx)
Out[38]: array([False, False,  True], dtype=bool)
```

W
wizardforcel 已提交
151
NaT(Not a Time)是 pandas 中时间戳数据的 null 值。
W
gitbook  
wizardforcel 已提交
152

W
wizardforcel 已提交
153
> 注意:dateutil.parser 是一个实用但不完美的工具。比如说,它会把一些原本不是日期的字符串认作是日期(比如"42"会被解析为 2042 年的今天)。
W
gitbook  
wizardforcel 已提交
154

W
wizardforcel 已提交
155
datetime 对象还有一些特定于当前环境(位于不同国家或使用不同语言的系统)的格式化选项。例如,德语或法语系统所用的月份简写就与英语系统所用的不同。表 11-3 进行了总结。
W
gitbook  
wizardforcel 已提交
156

W
wizardforcel 已提交
157
表 11-3 特定于当前环境的日期格式
W
gitbook  
wizardforcel 已提交
158 159 160 161 162

![](img/7178691-cf0119398273e2b0.png)

# 11.2 时间序列基础

W
wizardforcel 已提交
163
pandas 最基本的时间序列类型就是以时间戳(通常以 Python 字符串或 datatime 对象表示)为索引的`Series`
W
gitbook  
wizardforcel 已提交
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183
```python
In [39]: from datetime import datetime

In [40]: dates = [datetime(2011, 1, 2), datetime(2011, 1, 5),
   ....:          datetime(2011, 1, 7), datetime(2011, 1, 8),
   ....:          datetime(2011, 1, 10), datetime(2011, 1, 12)]

In [41]: ts = pd.Series(np.random.randn(6), index=dates)

In [42]: ts
Out[42]: 
2011-01-02   -0.204708
2011-01-05    0.478943
2011-01-07   -0.519439
2011-01-08   -0.555730
2011-01-10    1.965781
2011-01-12    1.393406
dtype: float64
```

W
wizardforcel 已提交
184
这些 datetime 对象实际上是被放在一个 DatetimeIndex 中的:
W
gitbook  
wizardforcel 已提交
185 186 187 188 189 190 191 192
```python
In [43]: ts.index
Out[43]: 
DatetimeIndex(['2011-01-02', '2011-01-05', '2011-01-07', '2011-01-08',
               '2011-01-10', '2011-01-12'],
              dtype='datetime64[ns]', freq=None)
```

W
wizardforcel 已提交
193
跟其他`Series`一样,不同索引的时间序列之间的算术运算会自动按日期对齐:
W
gitbook  
wizardforcel 已提交
194 195 196 197 198 199 200 201 202 203 204 205 206 207
```python
In [44]: ts + ts[::2]
Out[44]: 
2011-01-02   -0.409415
2011-01-05         NaN
2011-01-07   -1.038877
2011-01-08         NaN
2011-01-10    3.931561
2011-01-12         NaN
dtype: float64
```

ts[::2] 是每隔两个取一个。

W
wizardforcel 已提交
208
pandas 用 NumPy 的 datetime64 数据类型以纳秒形式存储时间戳:
W
gitbook  
wizardforcel 已提交
209 210 211 212 213
```python
In [45]: ts.index.dtype
Out[45]: dtype('<M8[ns]')
```

W
wizardforcel 已提交
214
DatetimeIndex 中的各个标量值是 pandas 的 Timestamp 对象:
W
gitbook  
wizardforcel 已提交
215 216 217 218 219 220 221
```python
In [46]: stamp = ts.index[0]

In [47]: stamp
Out[47]: Timestamp('2011-01-02 00:00:00')
```

W
wizardforcel 已提交
222
只要有需要,TimeStamp 可以随时自动转换为 datetime 对象。此外,它还可以存储频率信息(如果有的话),且知道如何执行时区转换以及其他操作。稍后将对此进行详细讲解。
W
gitbook  
wizardforcel 已提交
223 224 225

## 索引、选取、子集构造

W
wizardforcel 已提交
226
当你根据标签索引选取数据时,时间序列和其它的 pandas.`Series`很像:
W
gitbook  
wizardforcel 已提交
227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326
```python
In [48]: stamp = ts.index[2]

In [49]: ts[stamp]
Out[49]: -0.51943871505673811
```

还有一种更为方便的用法:传入一个可以被解释为日期的字符串:
```python
In [50]: ts['1/10/2011']
Out[50]: 1.9657805725027142

In [51]: ts['20110110']
Out[51]: 1.9657805725027142
```

对于较长的时间序列,只需传入“年”或“年月”即可轻松选取数据的切片:
```python
In [52]: longer_ts = pd.Series(np.random.randn(1000),
   ....:                       index=pd.date_range('1/1/2000', periods=1000))

In [53]: longer_ts
Out[53]: 
2000-01-01    0.092908
2000-01-02    0.281746
2000-01-03    0.769023
2000-01-04    1.246435
2000-01-05    1.007189
2000-01-06   -1.296221
2000-01-07    0.274992
2000-01-08    0.228913
2000-01-09    1.352917
2000-01-10    0.886429
                ...   
2002-09-17   -0.139298
2002-09-18   -1.159926
2002-09-19    0.618965
2002-09-20    1.373890
2002-09-21   -0.983505
2002-09-22    0.930944
2002-09-23   -0.811676
2002-09-24   -1.830156
2002-09-25   -0.138730
2002-09-26    0.334088
Freq: D, Length: 1000, dtype: float64

In [54]: longer_ts['2001']
Out[54]: 
2001-01-01    1.599534
2001-01-02    0.474071
2001-01-03    0.151326
2001-01-04   -0.542173
2001-01-05   -0.475496
2001-01-06    0.106403
2001-01-07   -1.308228
2001-01-08    2.173185
2001-01-09    0.564561
2001-01-10   -0.190481
                ...   
2001-12-22    0.000369
2001-12-23    0.900885
2001-12-24   -0.454869
2001-12-25   -0.864547
2001-12-26    1.129120
2001-12-27    0.057874
2001-12-28   -0.433739
2001-12-29    0.092698
2001-12-30   -1.397820
2001-12-31    1.457823
Freq: D, Length: 365, dtype: float64
```

这里,字符串“2001”被解释成年,并根据它选取时间区间。指定月也同样奏效:
```python
In [55]: longer_ts['2001-05']
Out[55]: 
2001-05-01   -0.622547
2001-05-02    0.936289
2001-05-03    0.750018
2001-05-04   -0.056715
2001-05-05    2.300675
2001-05-06    0.569497
2001-05-07    1.489410
2001-05-08    1.264250
2001-05-09   -0.761837
2001-05-10   -0.331617
                ...   
2001-05-22    0.503699
2001-05-23   -1.387874
2001-05-24    0.204851
2001-05-25    0.603705
2001-05-26    0.545680
2001-05-27    0.235477
2001-05-28    0.111835
2001-05-29   -1.251504
2001-05-30   -2.949343
2001-05-31    0.634634
Freq: D, Length: 31, dtype: float64
```

W
wizardforcel 已提交
327
datetime 对象也可以进行切片:
W
gitbook  
wizardforcel 已提交
328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357
```python
In [56]: ts[datetime(2011, 1, 7):]
Out[56]: 
2011-01-07   -0.519439
2011-01-08   -0.555730
2011-01-10    1.965781
2011-01-12    1.393406
dtype: float64
```

由于大部分时间序列数据都是按照时间先后排序的,因此你也可以用不存在于该时间序列中的时间戳对其进行切片(即范围查询):
```python
In [57]: ts
Out[57]: 
2011-01-02   -0.204708
2011-01-05    0.478943
2011-01-07   -0.519439
2011-01-08   -0.555730
2011-01-10    1.965781
2011-01-12    1.393406
dtype: float64

In [58]: ts['1/6/2011':'1/11/2011']
Out[58]: 
2011-01-07   -0.519439
2011-01-08   -0.555730
2011-01-10    1.965781
dtype: float64
```

W
wizardforcel 已提交
358
跟之前一样,你可以传入字符串日期、datetime 或 Timestamp。注意,这样切片所产生的是原时间序列的视图,跟 NumPy 数组的切片运算是一样的。
W
gitbook  
wizardforcel 已提交
359 360 361

这意味着,没有数据被复制,对切片进行修改会反映到原始数据上。

W
wizardforcel 已提交
362
此外,还有一个等价的实例方法也可以截取两个日期之间 TimeSeries:
W
gitbook  
wizardforcel 已提交
363 364 365 366 367 368 369 370 371 372
```python
In [59]: ts.truncate(after='1/9/2011')
Out[59]: 
2011-01-02   -0.204708
2011-01-05    0.478943
2011-01-07   -0.519439
2011-01-08   -0.555730
dtype: float64
```

W
wizardforcel 已提交
373
面这些操作对`DataFrame`也有效。例如,对`DataFrame`的行进行索引:
W
gitbook  
wizardforcel 已提交
374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409
```python
In [60]: dates = pd.date_range('1/1/2000', periods=100, freq='W-WED')

In [61]: long_df = pd.DataFrame(np.random.randn(100, 4),
   ....:                        index=dates,
   ....:                        columns=['Colorado', 'Texas',
   ....:                                 'New York', 'Ohio'])

In [62]: long_df.loc['5-2001']
Out[62]: 
            Colorado     Texas  New York      Ohio
2001-05-02 -0.006045  0.490094 -0.277186 -0.707213
2001-05-09 -0.560107  2.735527  0.927335  1.513906
2001-05-16  0.538600  1.273768  0.667876 -0.969206
2001-05-23  1.676091 -0.817649  0.050188  1.951312
2001-05-30  3.260383  0.963301  1.201206 -1.852001
```

## 带有重复索引的时间序列

在某些应用场景中,可能会存在多个观测数据落在同一个时间点上的情况。下面就是一个例子:
```python
In [63]: dates = pd.DatetimeIndex(['1/1/2000', '1/2/2000', '1/2/2000',
   ....:                           '1/2/2000', '1/3/2000'])
In [64]: dup_ts = pd.Series(np.arange(5), index=dates)

In [65]: dup_ts
Out[65]: 
2000-01-01    0
2000-01-02    1
2000-01-02    2
2000-01-02    3
2000-01-03    4
dtype: int64
```

W
wizardforcel 已提交
410
通过检查索引的 is_unique 属性,我们就可以知道它是不是唯一的:
W
gitbook  
wizardforcel 已提交
411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428
```python
In [66]: dup_ts.index.is_unique
Out[66]: False
```

对这个时间序列进行索引,要么产生标量值,要么产生切片,具体要看所选的时间点是否重复:
```python
In [67]: dup_ts['1/3/2000']  # not duplicated
Out[67]: 4

In [68]: dup_ts['1/2/2000']  # duplicated
Out[68]: 
2000-01-02    1
2000-01-02    2
2000-01-02    3
dtype: int64
```

W
wizardforcel 已提交
429
假设你想要对具有非唯一时间戳的数据进行聚合。一个办法是使用 groupby,并传入 level=0:
W
gitbook  
wizardforcel 已提交
430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449
```python
In [69]: grouped = dup_ts.groupby(level=0)

In [70]: grouped.mean()
Out[70]: 
2000-01-01    0
2000-01-02    2
2000-01-03    4
dtype: int64

In [71]: grouped.count()
Out[71]: 
2000-01-01    1
2000-01-02    3
2000-01-03    1
dtype: int64
```

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

W
wizardforcel 已提交
450
pandas 中的原生时间序列一般被认为是不规则的,也就是说,它们没有固定的频率。对于大部分应用程序而言,这是无所谓的。但是,它常常需要以某种相对固定的频率进行分析,比如每日、每月、每 15 分钟等(这样自然会在时间序列中引入缺失值)。幸运的是,pandas 有一整套标准时间序列频率以及用于重采样、频率推断、生成固定频率日期范围的工具。例如,我们可以将之前那个时间序列转换为一个具有固定频率(每日)的时间序列,只需调用 resample 即可:
W
gitbook  
wizardforcel 已提交
451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466
```python
In [72]: ts
Out[72]: 
2011-01-02   -0.204708
2011-01-05    0.478943
2011-01-07   -0.519439
2011-01-08   -0.555730
2011-01-10    1.965781
2011-01-12    1.393406
dtype: float64

In [73]: resampler = ts.resample('D')
```

字符串“D”是每天的意思。

W
wizardforcel 已提交
467
频率的转换(或重采样)是一个比较大的主题,稍后将专门用一节来进行讨论(11.6 小节)。这里,我将告诉你如何使用基本的频率和它的倍数。
W
gitbook  
wizardforcel 已提交
468 469 470

## 生成日期范围

W
wizardforcel 已提交
471
虽然我之前用的时候没有明说,但你可能已经猜到`pandas.date_range`可用于根据指定的频率生成指定长度的 DatetimeIndex:
W
gitbook  
wizardforcel 已提交
472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495
```python
In [74]: index = pd.date_range('2012-04-01', '2012-06-01')

In [75]: index
Out[75]: 
DatetimeIndex(['2012-04-01', '2012-04-02', '2012-04-03', '2012-04-04',
               '2012-04-05', '2012-04-06', '2012-04-07', '2012-04-08',
               '2012-04-09', '2012-04-10', '2012-04-11', '2012-04-12',
               '2012-04-13', '2012-04-14', '2012-04-15', '2012-04-16',
               '2012-04-17', '2012-04-18', '2012-04-19', '2012-04-20',
               '2012-04-21', '2012-04-22', '2012-04-23', '2012-04-24',
               '2012-04-25', '2012-04-26', '2012-04-27', '2012-04-28',
               '2012-04-29', '2012-04-30', '2012-05-01', '2012-05-02',
               '2012-05-03', '2012-05-04', '2012-05-05', '2012-05-06',
               '2012-05-07', '2012-05-08', '2012-05-09', '2012-05-10',
               '2012-05-11', '2012-05-12', '2012-05-13', '2012-05-14',
               '2012-05-15', '2012-05-16', '2012-05-17', '2012-05-18',
               '2012-05-19', '2012-05-20', '2012-05-21', '2012-05-22',
               '2012-05-23', '2012-05-24', '2012-05-25', '2012-05-26',
               '2012-05-27', '2012-05-28', '2012-05-29', '2012-05-30',
               '2012-05-31', '2012-06-01'],
              dtype='datetime64[ns]', freq='D')
```

W
wizardforcel 已提交
496
默认情况下,date_range 会产生按天计算的时间点。如果只传入起始或结束日期,那就还得传入一个表示一段时间的数字:
W
gitbook  
wizardforcel 已提交
497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516
```python
In [76]: pd.date_range(start='2012-04-01', periods=20)
Out[76]: 
DatetimeIndex(['2012-04-01', '2012-04-02', '2012-04-03', '2012-04-04',
               '2012-04-05', '2012-04-06', '2012-04-07', '2012-04-08',
               '2012-04-09', '2012-04-10', '2012-04-11', '2012-04-12',
               '2012-04-13', '2012-04-14', '2012-04-15', '2012-04-16',
               '2012-04-17', '2012-04-18', '2012-04-19', '2012-04-20'],
              dtype='datetime64[ns]', freq='D')

In [77]: pd.date_range(end='2012-06-01', periods=20)
Out[77]: 
DatetimeIndex(['2012-05-13', '2012-05-14', '2012-05-15', '2012-05-16',
               '2012-05-17', '2012-05-18', '2012-05-19', '2012-05-20',
               '2012-05-21', '2012-05-22', '2012-05-23', '2012-05-24',
               '2012-05-25', '2012-05-26', '2012-05-27','2012-05-28',
               '2012-05-29', '2012-05-30', '2012-05-31', '2012-06-01'],
              dtype='datetime64[ns]', freq='D')
```

W
wizardforcel 已提交
517
起始和结束日期定义了日期索引的严格边界。例如,如果你想要生成一个由每月最后一个工作日组成的日期索引,可以传入"BM"频率(表示 business end of month,表 11-4 是频率列表),这样就只会包含时间间隔内(或刚好在边界上的)符合频率要求的日期:
W
gitbook  
wizardforcel 已提交
518 519 520 521 522 523 524 525 526
```python
In [78]: pd.date_range('2000-01-01', '2000-12-01', freq='BM')
Out[78]: 
DatetimeIndex(['2000-01-31', '2000-02-29', '2000-03-31', '2000-04-28',
               '2000-05-31', '2000-06-30', '2000-07-31', '2000-08-31',
               '2000-09-29', '2000-10-31', '2000-11-30'],
              dtype='datetime64[ns]', freq='BM')
```

W
wizardforcel 已提交
527
表 11-4 基本的时间序列频率(不完整)
W
gitbook  
wizardforcel 已提交
528 529 530 531 532 533 534

![](img/7178691-c8614ddbd10793ca.png)

![](img/7178691-8da46ba96544b071.png)

![](img/7178691-3ca410609195edc4.png)

W
wizardforcel 已提交
535
date_range 默认会保留起始和结束时间戳的时间信息(如果有的话):
W
gitbook  
wizardforcel 已提交
536 537 538 539 540 541 542 543 544
```python
In [79]: pd.date_range('2012-05-02 12:56:31', periods=5)
Out[79]: 
DatetimeIndex(['2012-05-02 12:56:31', '2012-05-03 12:56:31',
               '2012-05-04 12:56:31', '2012-05-05 12:56:31',
               '2012-05-06 12:56:31'],
              dtype='datetime64[ns]', freq='D')
```

W
wizardforcel 已提交
545
有时,虽然起始和结束日期带有时间信息,但你希望产生一组被规范化(normalize)到午夜的时间戳。normalize 选项即可实现该功能:
W
gitbook  
wizardforcel 已提交
546 547 548 549 550 551 552 553 554 555
```python
In [80]: pd.date_range('2012-05-02 12:56:31', periods=5, normalize=True)
Out[80]: 
DatetimeIndex(['2012-05-02', '2012-05-03', '2012-05-04', '2012-05-05',
               '2012-05-06'],
              dtype='datetime64[ns]', freq='D')
```

## 频率和日期偏移量

W
wizardforcel 已提交
556
pandas 中的频率是由一个基础频率(base frequency)和一个乘数组成的。基础频率通常以一个字符串别名表示,比如"M"表示每月,"H"表示每小时。对于每个基础频率,都有一个被称为日期偏移量(date offset)的对象与之对应。例如,按小时计算的频率可以用 Hour 类表示:
W
gitbook  
wizardforcel 已提交
557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609
```python
In [81]: from pandas.tseries.offsets import Hour, Minute

In [82]: hour = Hour()

In [83]: hour
Out[83]: <Hour>
```

传入一个整数即可定义偏移量的倍数:
```python
In [84]: four_hours = Hour(4)

In [85]: four_hours
Out[85]: <4 * Hours>
```

一般来说,无需明确创建这样的对象,只需使用诸如"H"或"4H"这样的字符串别名即可。在基础频率前面放上一个整数即可创建倍数:
```python
In [86]: pd.date_range('2000-01-01', '2000-01-03 23:59', freq='4h')
Out[86]: 
DatetimeIndex(['2000-01-01 00:00:00', '2000-01-01 04:00:00',
               '2000-01-01 08:00:00', '2000-01-01 12:00:00',
               '2000-01-01 16:00:00', '2000-01-01 20:00:00',
               '2000-01-02 00:00:00', '2000-01-02 04:00:00',
               '2000-01-02 08:00:00', '2000-01-02 12:00:00',
               '2000-01-02 16:00:00', '2000-01-02 20:00:00',
               '2000-01-03 00:00:00', '2000-01-03 04:00:00',
               '2000-01-03 08:00:00', '2000-01-03 12:00:00',
               '2000-01-03 16:00:00', '2000-01-03 20:00:00'],
              dtype='datetime64[ns]', freq='4H')
```

大部分偏移量对象都可通过加法进行连接:
```python
In [87]: Hour(2) + Minute(30)
Out[87]: <150 * Minutes>
```

同理,你也可以传入频率字符串(如"2h30min"),这种字符串可以被高效地解析为等效的表达式:
```python
In [88]: pd.date_range('2000-01-01', periods=10, freq='1h30min')
Out[88]: 
DatetimeIndex(['2000-01-01 00:00:00', '2000-01-01 01:30:00',
               '2000-01-01 03:00:00', '2000-01-01 04:30:00',
               '2000-01-01 06:00:00', '2000-01-01 07:30:00',
               '2000-01-01 09:00:00', '2000-01-01 10:30:00',
               '2000-01-01 12:00:00', '2000-01-01 13:30:00'],
              dtype='datetime64[ns]', freq='90T')
```

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

W
wizardforcel 已提交
610
表 11-4 列出了 pandas 中的频率代码和日期偏移量类。
W
gitbook  
wizardforcel 已提交
611

W
wizardforcel 已提交
612
> 笔记:用户可以根据实际需求自定义一些频率类以便提供 pandas 所没有的日期逻辑,但具体的细节超出了本书的范围。             
W
gitbook  
wizardforcel 已提交
613

W
wizardforcel 已提交
614
表 11-4 时间序列的基础频率
W
gitbook  
wizardforcel 已提交
615 616 617 618 619 620 621

![](img/7178691-ff139312cd972204.png)

![](img/7178691-adfa57a998c0296e.png)

![](img/7178691-d09e577a10d0e6eb.png)

W
wizardforcel 已提交
622
## WOM 日期
W
gitbook  
wizardforcel 已提交
623

W
wizardforcel 已提交
624
WOM(Week Of Month)是一种非常实用的频率类,它以 WOM 开头。它使你能获得诸如“每月第 3 个星期五”之类的日期:
W
gitbook  
wizardforcel 已提交
625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641
```python
In [89]: rng = pd.date_range('2012-01-01', '2012-09-01', freq='WOM-3FRI')

In [90]: list(rng)
Out[90]: 
[Timestamp('2012-01-20 00:00:00', freq='WOM-3FRI'),
 Timestamp('2012-02-17 00:00:00', freq='WOM-3FRI'),
 Timestamp('2012-03-16 00:00:00', freq='WOM-3FRI'),
 Timestamp('2012-04-20 00:00:00', freq='WOM-3FRI'),
 Timestamp('2012-05-18 00:00:00', freq='WOM-3FRI'),
 Timestamp('2012-06-15 00:00:00', freq='WOM-3FRI'),
 Timestamp('2012-07-20 00:00:00', freq='WOM-3FRI'),
 Timestamp('2012-08-17 00:00:00', freq='WOM-3FRI')]
```

## 移动(超前和滞后)数据

W
wizardforcel 已提交
642
移动(shifting)指的是沿着时间轴将数据前移或后移。`Series``DataFrame`都有一个 shift 方法用于执行单纯的前移或后移操作,保持索引不变:
W
gitbook  
wizardforcel 已提交
643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673
```python
In [91]: ts = pd.Series(np.random.randn(4),
   ....:                index=pd.date_range('1/1/2000', periods=4, freq='M'))

In [92]: ts
Out[92]: 
2000-01-31   -0.066748
2000-02-29    0.838639
2000-03-31   -0.117388
2000-04-30   -0.517795
Freq: M, dtype: float64

In [93]: ts.shift(2)
Out[93]: 
2000-01-31         NaN
2000-02-29         NaN
2000-03-31   -0.066748
2000-04-30    0.838639
Freq: M, dtype: float64

In [94]: ts.shift(-2)
Out[94]: 
2000-01-31   -0.117388
2000-02-29   -0.517795
2000-03-31         NaN
2000-04-30         NaN
Freq: M, dtype: float64
```

当我们这样进行移动时,就会在时间序列的前面或后面产生缺失数据。

W
wizardforcel 已提交
674
shift 通常用于计算一个时间序列或多个时间序列(如`DataFrame`的列)中的百分比变化。可以这样表达:
W
gitbook  
wizardforcel 已提交
675 676 677 678
```python
ts / ts.shift(1) - 1
```

W
wizardforcel 已提交
679
由于单纯的移位操作不会修改索引,所以部分数据会被丢弃。因此,如果频率已知,则可以将其传给 shift 以便实现对时间戳进行位移而不是对数据进行简单位移:
W
gitbook  
wizardforcel 已提交
680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710
```python
In [95]: ts.shift(2, freq='M')
Out[95]: 
2000-03-31   -0.066748
2000-04-30    0.838639
2000-05-31   -0.117388
2000-06-30   -0.517795
Freq: M, dtype: float64
```

这里还可以使用其他频率,于是你就能非常灵活地对数据进行超前和滞后处理了:
```python
In [96]: ts.shift(3, freq='D')
Out[96]: 
2000-02-03   -0.066748
2000-03-03    0.838639
2000-04-03   -0.117388
2000-05-03   -0.517795
dtype: float64

In [97]: ts.shift(1, freq='90T')
Out[97]: 
2000-01-31 01:30:00   -0.066748
2000-02-29 01:30:00    0.838639
2000-03-31 01:30:00   -0.117388
2000-04-30 01:30:00   -0.517795
Freq: M, dtype: float64
```

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

W
wizardforcel 已提交
711
pandas 的日期偏移量还可以用在 datetime 或 Timestamp 对象上:
W
gitbook  
wizardforcel 已提交
712 713 714 715 716 717 718 719 720
```python
In [98]: from pandas.tseries.offsets import Day, MonthEnd

In [99]: now = datetime(2011, 11, 17)

In [100]: now + 3 * Day()
Out[100]: Timestamp('2011-11-20 00:00:00')
```

W
wizardforcel 已提交
721
如果加的是锚点偏移量(比如 MonthEnd),第一次增量会将原日期向前滚动到符合频率规则的下一个日期:
W
gitbook  
wizardforcel 已提交
722 723 724 725 726 727 728 729
```python
In [101]: now + MonthEnd()
Out[101]: Timestamp('2011-11-30 00:00:00')

In [102]: now + MonthEnd(2)
Out[102]: Timestamp('2011-12-31 00:00:00')
```

W
wizardforcel 已提交
730
通过锚点偏移量的 rollforward 和 rollback 方法,可明确地将日期向前或向后“滚动”:
W
gitbook  
wizardforcel 已提交
731 732 733 734 735 736 737 738 739 740
```python
In [103]: offset = MonthEnd()

In [104]: offset.rollforward(now)
Out[104]: Timestamp('2011-11-30 00:00:00')

In [105]: offset.rollback(now)
Out[105]: Timestamp('2011-10-31 00:00:00')
```

W
wizardforcel 已提交
741
日期偏移量还有一个巧妙的用法,即结合 groupby 使用这两个“滚动”方法:
W
gitbook  
wizardforcel 已提交
742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777
```python
In [106]: ts = pd.Series(np.random.randn(20),
   .....:                index=pd.date_range('1/15/2000', periods=20, freq='4d'))

In [107]: ts
Out[107]: 
2000-01-15   -0.116696
2000-01-19    2.389645
2000-01-23   -0.932454
2000-01-27   -0.229331
2000-01-31   -1.140330
2000-02-04    0.439920
2000-02-08   -0.823758
2000-02-12   -0.520930
2000-02-16    0.350282
2000-02-20    0.204395
2000-02-24    0.133445
2000-02-28    0.327905
2000-03-03    0.072153
2000-03-07    0.131678
2000-03-11   -1.297459
2000-03-15    0.997747
2000-03-19    0.870955
2000-03-23   -0.991253
2000-03-27    0.151699
2000-03-31    1.266151
Freq: 4D, dtype: float64

In [108]: ts.groupby(offset.rollforward).mean()
Out[108]: 
2000-01-31   -0.005833
2000-02-29    0.015894
2000-03-31    0.150209
dtype: float64
```

W
wizardforcel 已提交
778
当然,更简单、更快速地实现该功能的办法是使用 resample(11.6 小节将对此进行详细介绍):
W
gitbook  
wizardforcel 已提交
779 780 781 782 783 784 785 786 787 788 789
```python
In [109]: ts.resample('M').mean()
Out[109]: 
2000-01-31   -0.005833
2000-02-29    0.015894
2000-03-31    0.150209
Freq: M, dtype: float64
```

# 11.4 时区处理

W
wizardforcel 已提交
790
时间序列处理工作中最让人不爽的就是对时区的处理。许多人都选择以协调世界时(UTC,它是格林尼治标准时间(Greenwich Mean Time)的接替者,目前已经是国际标准了)来处理时间序列。时区是以 UTC 偏移量的形式表示的。例如,夏令时期间,纽约比 UTC 慢 4 小时,而在全年其他时间则比 UTC 慢 5 小时。
W
gitbook  
wizardforcel 已提交
791

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

W
wizardforcel 已提交
794
有关 pytz 库的更多信息,请查阅其文档。就本书而言,由于 pandas 包装了 pytz 的功能,因此你可以不用记忆其 API,只要记得时区的名称即可。时区名可以在 shell 中看到,也可以通过文档查看:
W
gitbook  
wizardforcel 已提交
795 796 797 798 799 800 801
```python
In [110]: import pytz

In [111]: pytz.common_timezones[-5:]
Out[111]: ['US/Eastern', 'US/Hawaii', 'US/Mountain', 'US/Pacific', 'UTC']
```

W
wizardforcel 已提交
802
要从 pytz 中获取时区对象,使用 pytz.timezone 即可:
W
gitbook  
wizardforcel 已提交
803 804 805 806 807 808 809
```python
In [112]: tz = pytz.timezone('America/New_York')

In [113]: tz
Out[113]: <DstTzInfo 'America/New_York' LMT-1 day, 19:04:00 STD>
```

W
wizardforcel 已提交
810
pandas 中的方法既可以接受时区名也可以接受这些对象。
W
gitbook  
wizardforcel 已提交
811 812 813

# 时区本地化和转换

W
wizardforcel 已提交
814
默认情况下,pandas 中的时间序列是单纯(naive)的时区。看看下面这个时间序列:
W
gitbook  
wizardforcel 已提交
815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830
```python
In [114]: rng = pd.date_range('3/9/2012 9:30', periods=6, freq='D')

In [115]: ts = pd.Series(np.random.randn(len(rng)), index=rng)

In [116]: ts
Out[116]: 
2012-03-09 09:30:00   -0.202469
2012-03-10 09:30:00    0.050718
2012-03-11 09:30:00    0.639869
2012-03-12 09:30:00    0.597594
2012-03-13 09:30:00   -0.797246
2012-03-14 09:30:00    0.472879
Freq: D, dtype: float64
```

W
wizardforcel 已提交
831
其索引的 tz 字段为 None:
W
gitbook  
wizardforcel 已提交
832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848
```python
In [117]: print(ts.index.tz)
None
```

可以用时区集生成日期范围:
```python
In [118]: pd.date_range('3/9/2012 9:30', periods=10, freq='D', tz='UTC')
Out[118]: 
DatetimeIndex(['2012-03-09 09:30:00+00:00', '2012-03-10 09:30:00+00:00',
               '2012-03-11 09:30:00+00:00', '2012-03-12 09:30:00+00:00',
               '2012-03-13 09:30:00+00:00', '2012-03-14 09:30:00+00:00',
               '2012-03-15 09:30:00+00:00', '2012-03-16 09:30:00+00:00',
               '2012-03-17 09:30:00+00:00', '2012-03-18 09:30:00+00:00'],
              dtype='datetime64[ns, UTC]', freq='D')
```

W
wizardforcel 已提交
849
从单纯到本地化的转换是通过 tz_localize 方法处理的:
W
gitbook  
wizardforcel 已提交
850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880
```python
In [119]: ts
Out[119]: 
2012-03-09 09:30:00   -0.202469
2012-03-10 09:30:00    0.050718
2012-03-11 09:30:00    0.639869
2012-03-12 09:30:00    0.597594
2012-03-13 09:30:00   -0.797246
2012-03-14 09:30:00    0.472879
Freq: D, dtype: float64

In [120]: ts_utc = ts.tz_localize('UTC')

In [121]: ts_utc
Out[121]: 
2012-03-09 09:30:00+00:00   -0.202469
2012-03-10 09:30:00+00:00    0.050718
2012-03-11 09:30:00+00:00    0.639869
2012-03-12 09:30:00+00:00    0.597594
2012-03-13 09:30:00+00:00   -0.797246
2012-03-14 09:30:00+00:00    0.472879
Freq: D, dtype: float64

In [122]: ts_utc.index
Out[122]: 
DatetimeIndex(['2012-03-09 09:30:00+00:00', '2012-03-10 09:30:00+00:00',
               '2012-03-11 09:30:00+00:00', '2012-03-12 09:30:00+00:00',
               '2012-03-13 09:30:00+00:00', '2012-03-14 09:30:00+00:00'],
              dtype='datetime64[ns, UTC]', freq='D')
```

W
wizardforcel 已提交
881
一旦时间序列被本地化到某个特定时区,就可以用 tz_convert 将其转换到别的时区了:
W
gitbook  
wizardforcel 已提交
882 883 884 885 886 887 888 889 890 891 892 893
```python
In [123]: ts_utc.tz_convert('America/New_York')
Out[123]: 
2012-03-09 04:30:00-05:00   -0.202469
2012-03-10 04:30:00-05:00    0.050718
2012-03-11 05:30:00-04:00    0.639869
2012-03-12 05:30:00-04:00    0.597594
2012-03-13 05:30:00-04:00   -0.797246
2012-03-14 05:30:00-04:00    0.472879
Freq: D, dtype: float64
```

W
wizardforcel 已提交
894
对于上面这种时间序列(它跨越了美国东部时区的夏令时转变期),我们可以将其本地化到 EST,然后转换为 UTC 或柏林时间:
W
gitbook  
wizardforcel 已提交
895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918
```python
In [124]: ts_eastern = ts.tz_localize('America/New_York')

In [125]: ts_eastern.tz_convert('UTC')
Out[125]: 
2012-03-09 14:30:00+00:00   -0.202469
2012-03-10 14:30:00+00:00    0.050718
2012-03-11 13:30:00+00:00    0.639869
2012-03-12 13:30:00+00:00    0.597594
2012-03-13 13:30:00+00:00   -0.797246
2012-03-14 13:30:00+00:00    0.472879
Freq: D, dtype: float64

In [126]: ts_eastern.tz_convert('Europe/Berlin')
Out[126]: 
2012-03-09 15:30:00+01:00   -0.202469
2012-03-10 15:30:00+01:00    0.050718
2012-03-11 14:30:00+01:00    0.639869
2012-03-12 14:30:00+01:00    0.597594
2012-03-13 14:30:00+01:00   -0.797246
2012-03-14 14:30:00+01:00    0.472879
Freq: D, dtype: float64
```

W
wizardforcel 已提交
919
tz_localize 和 tz_convert 也是 DatetimeIndex 的实例方法:
W
gitbook  
wizardforcel 已提交
920 921 922 923 924 925 926 927 928
```python
In [127]: ts.index.tz_localize('Asia/Shanghai')
Out[127]: 
DatetimeIndex(['2012-03-09 09:30:00+08:00', '2012-03-10 09:30:00+08:00',
               '2012-03-11 09:30:00+08:00', '2012-03-12 09:30:00+08:00',
               '2012-03-13 09:30:00+08:00', '2012-03-14 09:30:00+08:00'],
              dtype='datetime64[ns, Asia/Shanghai]', freq='D')
```

W
wizardforcel 已提交
929
> 注意:对单纯时间戳的本地化操作还会检查夏令时转变期附近容易混淆或不存在的时间。
W
gitbook  
wizardforcel 已提交
930

W
wizardforcel 已提交
931
## 操作时区意识型 Timestamp 对象
W
gitbook  
wizardforcel 已提交
932

W
wizardforcel 已提交
933
跟时间序列和日期范围差不多,独立的 Timestamp 对象也能被从单纯型(naive)本地化为时区意识型(time zone-aware),并从一个时区转换到另一个时区:
W
gitbook  
wizardforcel 已提交
934 935 936 937 938 939 940 941 942
```python
In [128]: stamp = pd.Timestamp('2011-03-12 04:00')

In [129]: stamp_utc = stamp.tz_localize('utc')

In [130]: stamp_utc.tz_convert('America/New_York')
Out[130]: Timestamp('2011-03-11 23:00:00-0500', tz='America/New_York')
```

W
wizardforcel 已提交
943
在创建 Timestamp 时,还可以传入一个时区信息:
W
gitbook  
wizardforcel 已提交
944 945 946 947 948 949 950
```python
In [131]: stamp_moscow = pd.Timestamp('2011-03-12 04:00', tz='Europe/Moscow')

In [132]: stamp_moscow
Out[132]: Timestamp('2011-03-12 04:00:00+0300', tz='Europe/Moscow')
```

W
wizardforcel 已提交
951
时区意识型 Timestamp 对象在内部保存了一个 UTC 时间戳值(自 UNIX 纪元(1970 年 1 月 1 日)算起的纳秒数)。这个 UTC 值在时区转换过程中是不会发生变化的:
W
gitbook  
wizardforcel 已提交
952 953 954 955 956 957 958 959
```python
In [133]: stamp_utc.value
Out[133]: 1299902400000000000

In [134]: stamp_utc.tz_convert('America/New_York').value
Out[134]: 1299902400000000000
```

W
wizardforcel 已提交
960
当使用 pandas 的 DateOffset 对象执行时间算术运算时,运算过程会自动关注是否存在夏令时转变期。这里,我们创建了在 DST 转变之前的时间戳。首先,来看夏令时转变前的 30 分钟:
W
gitbook  
wizardforcel 已提交
961 962 963 964 965 966 967 968 969 970 971 972
```python
In [135]: from pandas.tseries.offsets import Hour

In [136]: stamp = pd.Timestamp('2012-03-12 01:30', tz='US/Eastern')

In [137]: stamp
Out[137]: Timestamp('2012-03-12 01:30:00-0400', tz='US/Eastern')

In [138]: stamp + Hour()
Out[138]: Timestamp('2012-03-12 02:30:00-0400', tz='US/Eastern')
```

W
wizardforcel 已提交
973
然后,夏令时转变前 90 分钟:
W
gitbook  
wizardforcel 已提交
974 975 976 977 978 979 980 981 982 983 984 985
```python
In [139]: stamp = pd.Timestamp('2012-11-04 00:30', tz='US/Eastern')

In [140]: stamp
Out[140]: Timestamp('2012-11-04 00:30:00-0400', tz='US/Eastern')

In [141]: stamp + 2 * Hour()
Out[141]: Timestamp('2012-11-04 01:30:00-0500', tz='US/Eastern')
```

## 不同时区之间的运算

W
wizardforcel 已提交
986
如果两个时间序列的时区不同,在将它们合并到一起时,最终结果就会是 UTC。由于时间戳其实是以 UTC 存储的,所以这是一个很简单的运算,并不需要发生任何转换:
W
gitbook  
wizardforcel 已提交
987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022
```python
In [142]: rng = pd.date_range('3/7/2012 9:30', periods=10, freq='B')

In [143]: ts = pd.Series(np.random.randn(len(rng)), index=rng)

In [144]: ts
Out[144]: 
2012-03-07 09:30:00    0.522356
2012-03-08 09:30:00   -0.546348
2012-03-09 09:30:00   -0.733537
2012-03-12 09:30:00    1.302736
2012-03-13 09:30:00    0.022199
2012-03-14 09:30:00    0.364287
2012-03-15 09:30:00   -0.922839
2012-03-16 09:30:00    0.312656
2012-03-19 09:30:00   -1.128497
2012-03-20 09:30:00   -0.333488
Freq: B, dtype: float64

In [145]: ts1 = ts[:7].tz_localize('Europe/London')

In [146]: ts2 = ts1[2:].tz_convert('Europe/Moscow')

In [147]: result = ts1 + ts2

In [148]: result.index
Out[148]: 
DatetimeIndex(['2012-03-07 09:30:00+00:00', '2012-03-08 09:30:00+00:00',
               '2012-03-09 09:30:00+00:00', '2012-03-12 09:30:00+00:00',
               '2012-03-13 09:30:00+00:00', '2012-03-14 09:30:00+00:00',
               '2012-03-15 09:30:00+00:00'],
              dtype='datetime64[ns, UTC]', freq='B')
```

# 11.5 时期及其算术运算

W
wizardforcel 已提交
1023
时期(period)表示的是时间区间,比如数日、数月、数季、数年等。Period 类所表示的就是这种数据类型,其构造函数需要用到一个字符串或整数,以及表 11-4 中的频率:
W
gitbook  
wizardforcel 已提交
1024 1025 1026 1027 1028 1029 1030
```python
In [149]: p = pd.Period(2007, freq='A-DEC')

In [150]: p
Out[150]: Period('2007', 'A-DEC')
```

W
wizardforcel 已提交
1031
这里,这个 Period 对象表示的是从 2007 年 1 月 1 日到 2007 年 12 月 31 日之间的整段时间。只需对 Period 对象加上或减去一个整数即可达到根据其频率进行位移的效果:
W
gitbook  
wizardforcel 已提交
1032 1033 1034 1035 1036 1037 1038 1039
```python
In [151]: p + 5
Out[151]: Period('2012', 'A-DEC')

In [152]: p - 2
Out[152]: Period('2005', 'A-DEC')
```

W
wizardforcel 已提交
1040
如果两个 Period 对象拥有相同的频率,则它们的差就是它们之间的单位数量:
W
gitbook  
wizardforcel 已提交
1041 1042 1043 1044 1045
```python
In [153]: pd.Period('2014', freq='A-DEC') - p
Out[153]: 7
```

W
wizardforcel 已提交
1046
period_range 函数可用于创建规则的时期范围:
W
gitbook  
wizardforcel 已提交
1047 1048 1049 1050 1051 1052 1053 1054
```python
In [154]: rng = pd.period_range('2000-01-01', '2000-06-30', freq='M')

In [155]: rng
Out[155]: PeriodIndex(['2000-01', '2000-02', '2000-03', '2000-04', '2000-05', '20
00-06'], dtype='period[M]', freq='M')
```

W
wizardforcel 已提交
1055
PeriodIndex 类保存了一组 Period,它可以在任何 pandas 数据结构中被用作轴索引:
W
gitbook  
wizardforcel 已提交
1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067
```python
In [156]: pd.Series(np.random.randn(6), index=rng)
Out[156]: 
2000-01   -0.514551
2000-02   -0.559782
2000-03   -0.783408
2000-04   -1.797685
2000-05   -0.172670
2000-06    0.680215
Freq: M, dtype: float64
```

W
wizardforcel 已提交
1068
如果你有一个字符串数组,你也可以使用 PeriodIndex 类:
W
gitbook  
wizardforcel 已提交
1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080
```python
In [157]: values = ['2001Q3', '2002Q2', '2003Q1']

In [158]: index = pd.PeriodIndex(values, freq='Q-DEC')

In [159]: index
Out[159]: PeriodIndex(['2001Q3', '2002Q2', '2003Q1'], dtype='period[Q-DEC]', freq
='Q-DEC')
```

## 时期的频率转换

W
wizardforcel 已提交
1081
Period 和 PeriodIndex 对象都可以通过其 asfreq 方法被转换成别的频率。假设我们有一个年度时期,希望将其转换为当年年初或年末的一个月度时期。该任务非常简单:
W
gitbook  
wizardforcel 已提交
1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094
```python
In [160]: p = pd.Period('2007', freq='A-DEC')

In [161]: p
Out[161]: Period('2007', 'A-DEC')

In [162]: p.asfreq('M', how='start')
Out[162]: Period('2007-01', 'M')

In [163]: p.asfreq('M', how='end')
Out[163]: Period('2007-12', 'M')
```

W
wizardforcel 已提交
1095
你可以将 Period('2007','A-DEC')看做一个被划分为多个月度时期的时间段中的游标。图 11-1 对此进行了说明。对于一个不以 12 月结束的财政年度,月度子时期的归属情况就不一样了:
W
gitbook  
wizardforcel 已提交
1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108
```python
In [164]: p = pd.Period('2007', freq='A-JUN')

In [165]: p
Out[165]: Period('2007', 'A-JUN')

In [166]: p.asfreq('M', 'start')
Out[166]: Period('2006-07', 'M')

In [167]: p.asfreq('M', 'end')
Out[167]: Period('2007-06', 'M')
```

W
wizardforcel 已提交
1109
![图 11-1 Period 频率转换示例](img/7178691-d201200d0e65676f.png)
W
gitbook  
wizardforcel 已提交
1110

W
wizardforcel 已提交
1111
在将高频率转换为低频率时,超时期(superperiod)是由子时期(subperiod)所属的位置决定的。例如,在 A-JUN 频率中,月份“2007 年 8 月”实际上是属于周期“2008 年”的:
W
gitbook  
wizardforcel 已提交
1112 1113 1114 1115 1116 1117 1118
```python
In [168]: p = pd.Period('Aug-2007', 'M')

In [169]: p.asfreq('A-JUN')
Out[169]: Period('2008', 'A-JUN')
```

W
wizardforcel 已提交
1119
完整的 PeriodIndex 或 Time`Series`的频率转换方式也是如此:
W
gitbook  
wizardforcel 已提交
1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155
```python
In [170]: rng = pd.period_range('2006', '2009', freq='A-DEC')

In [171]: ts = pd.Series(np.random.randn(len(rng)), index=rng)

In [172]: ts
Out[172]: 
2006    1.607578
2007    0.200381
2008   -0.834068
2009   -0.302988
Freq: A-DEC, dtype: float64

In [173]: ts.asfreq('M', how='start')
Out[173]: 
2006-01    1.607578
2007-01    0.200381
2008-01   -0.834068
2009-01   -0.302988
Freq: M, dtype: float64
```

这里,根据年度时期的第一个月,每年的时期被取代为每月的时期。如果我们想要每年的最后一个工作日,我们可以使用“B”频率,并指明想要该时期的末尾:
```python
In [174]: ts.asfreq('B', how='end')

Out[174]: 
2006-12-29    1.607578
2007-12-31    0.200381
2008-12-31   -0.834068
2009-12-31   -0.302988
Freq: B, dtype: float64
```

## 按季度计算的时期频率

W
wizardforcel 已提交
1156
季度型数据在会计、金融等领域中很常见。许多季度型数据都会涉及“财年末”的概念,通常是一年 12 个月中某月的最后一个日历日或工作日。就这一点来说,时期"2012Q4"根据财年末的不同会有不同的含义。pandas 支持 12 种可能的季度型频率,即 Q-JAN 到 Q-DEC:
W
gitbook  
wizardforcel 已提交
1157 1158 1159 1160 1161 1162 1163 1164

```python
In [175]: p = pd.Period('2012Q4', freq='Q-JAN')

In [176]: p
Out[176]: Period('2012Q4', 'Q-JAN')
```

W
wizardforcel 已提交
1165
在以 1 月结束的财年中,2012Q4 是从 11 月到 1 月(将其转换为日型频率就明白了)。图 11-2 对此进行了说明:
W
gitbook  
wizardforcel 已提交
1166 1167 1168 1169 1170 1171 1172 1173 1174

```python
In [177]: p.asfreq('D', 'start')
Out[177]: Period('2011-11-01', 'D')

In [178]: p.asfreq('D', 'end')
Out[178]: Period('2012-01-31', 'D')
```

W
wizardforcel 已提交
1175
![图 11.2 不同季度型频率之间的转换](img/7178691-e2e1d52c9766f6ff.png)
W
gitbook  
wizardforcel 已提交
1176

W
wizardforcel 已提交
1177
因此,Period 之间的算术运算会非常简单。例如,要获取该季度倒数第二个工作日下午 4 点的时间戳,你可以这样:
W
gitbook  
wizardforcel 已提交
1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188

```python
In [179]: p4pm = (p.asfreq('B', 'e') - 1).asfreq('T', 's') + 16 * 60

In [180]: p4pm
Out[180]: Period('2012-01-30 16:00', 'T')

In [181]: p4pm.to_timestamp()
Out[181]: Timestamp('2012-01-30 16:00:00')
```

W
wizardforcel 已提交
1189
period_range 可用于生成季度型范围。季度型范围的算术运算也跟上面是一样的:
W
gitbook  
wizardforcel 已提交
1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219
```python
In [182]: rng = pd.period_range('2011Q3', '2012Q4', freq='Q-JAN')

In [183]: ts = pd.Series(np.arange(len(rng)), index=rng)

In [184]: ts
Out[184]: 
2011Q3    0
2011Q4    1
2012Q1    2
2012Q2    3
2012Q3    4
2012Q4    5
Freq: Q-JAN, dtype: int64

In [185]: new_rng = (rng.asfreq('B', 'e') - 1).asfreq('T', 's') + 16 * 60

In [186]: ts.index = new_rng.to_timestamp()

In [187]: ts
Out[187]:
2010-10-28 16:00:00    0
2011-01-28 16:00:00    1
2011-04-28 16:00:00    2
2011-07-28 16:00:00    3
2011-10-28 16:00:00    4
2012-01-30 16:00:00    5
dtype: int64
```

W
wizardforcel 已提交
1220
## 将 Timestamp 转换为 Period(及其反向过程)
W
gitbook  
wizardforcel 已提交
1221

W
wizardforcel 已提交
1222
通过使用 to_period 方法,可以将由时间戳索引的`Series``DataFrame`对象转换为以时期索引:
W
gitbook  
wizardforcel 已提交
1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245

```python
In [188]: rng = pd.date_range('2000-01-01', periods=3, freq='M')

In [189]: ts = pd.Series(np.random.randn(3), index=rng)

In [190]: ts
Out[190]: 
2000-01-31    1.663261
2000-02-29   -0.996206
2000-03-31    1.521760
Freq: M, dtype: float64

In [191]: pts = ts.to_period()

In [192]: pts
Out[192]: 
2000-01    1.663261
2000-02   -0.996206
2000-03    1.521760
Freq: M, dtype: float64
```

W
wizardforcel 已提交
1246
由于时期指的是非重叠时间区间,因此对于给定的频率,一个时间戳只能属于一个时期。新 PeriodIndex 的频率默认是从时间戳推断而来的,你也可以指定任何别的频率。结果中允许存在重复时期:
W
gitbook  
wizardforcel 已提交
1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272
```python
In [193]: rng = pd.date_range('1/29/2000', periods=6, freq='D')

In [194]: ts2 = pd.Series(np.random.randn(6), index=rng)

In [195]: ts2
Out[195]: 
2000-01-29    0.244175
2000-01-30    0.423331
2000-01-31   -0.654040
2000-02-01    2.089154
2000-02-02   -0.060220
2000-02-03   -0.167933
Freq: D, dtype: float64

In [196]: ts2.to_period('M')
Out[196]: 
2000-01    0.244175
2000-01    0.423331
2000-01   -0.654040
2000-02    2.089154
2000-02   -0.060220
2000-02   -0.167933
Freq: M, dtype: float64
```

W
wizardforcel 已提交
1273
要转换回时间戳,使用 to_timestamp 即可:
W
gitbook  
wizardforcel 已提交
1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298

```python
In [197]: pts = ts2.to_period()

In [198]: pts
Out[198]: 
2000-01-29    0.244175
2000-01-30    0.423331
2000-01-31   -0.654040
2000-02-01    2.089154
2000-02-02   -0.060220
2000-02-03   -0.167933
Freq: D, dtype: float64

In [199]: pts.to_timestamp(how='end')
Out[199]: 
2000-01-29    0.244175
2000-01-30    0.423331
2000-01-31   -0.654040
2000-02-01    2.089154
2000-02-02   -0.060220
2000-02-03   -0.167933
Freq: D, dtype: float64
```

W
wizardforcel 已提交
1299
## 通过数组创建 PeriodIndex
W
gitbook  
wizardforcel 已提交
1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370

固定频率的数据集通常会将时间信息分开存放在多个列中。例如,在下面这个宏观经济数据集中,年度和季度就分别存放在不同的列中:
```python
In [200]: data = pd.read_csv('examples/macrodata.csv')

In [201]: data.head(5)
Out[201]: 
     year  quarter   realgdp  realcons  realinv  realgovt  realdpi    cpi  \
0  1959.0      1.0  2710.349    1707.4  286.898   470.045   1886.9  28.98   
1  1959.0      2.0  2778.801    1733.7  310.859   481.301   1919.7  29.15   
2  1959.0      3.0  2775.488    1751.8  289.226   491.260   1916.4  29.35   
3  1959.0      4.0  2785.204    1753.7  299.356   484.052   1931.3  29.37   
4  1960.0      1.0  2847.699    1770.5  331.722   462.199   1955.5  29.54   
      m1  tbilrate  unemp      pop  infl  realint  
0  139.7      2.82    5.8  177.146  0.00     0.00  
1  141.7      3.08    5.1  177.830  2.34     0.74  
2  140.5      3.82    5.3  178.657  2.74     1.09  
3  140.0      4.33    5.6  179.386  0.27     4.06  
4  139.6      3.50    5.2  180.007  2.31     1.19  

In [202]: data.year
Out[202]: 
0      1959.0
1      1959.0
2      1959.0
3      1959.0
4      1960.0
5      1960.0
6      1960.0
7      1960.0
8      1961.0
9      1961.0
        ...  
193    2007.0
194    2007.0
195    2007.0
196    2008.0
197    2008.0
198    2008.0
199    2008.0
200    2009.0
201    2009.0
202    2009.0
Name: year, Length: 203, dtype: float64

In [203]: data.quarter
Out[203]: 
0      1.0
1      2.0
2      3.0
3      4.0
4      1.0
5      2.0
6      3.0
7      4.0
8      1.0
9      2.0
      ... 
193    2.0
194    3.0
195    4.0
196    1.0
197    2.0
198    3.0
199    4.0
200    1.0
201    2.0
202    3.0
Name: quarter, Length: 203, dtype: float64
```

W
wizardforcel 已提交
1371
通过将这些数组以及一个频率传入 PeriodIndex,就可以将它们合并成`DataFrame`的一个索引:
W
gitbook  
wizardforcel 已提交
1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414
```python
In [204]: index = pd.PeriodIndex(year=data.year, quarter=data.quarter,
   .....:                        freq='Q-DEC')

In [205]: index
Out[205]: 
PeriodIndex(['1959Q1', '1959Q2', '1959Q3', '1959Q4', '1960Q1', '1960Q2',
             '1960Q3', '1960Q4', '1961Q1', '1961Q2',
             ...
             '2007Q2', '2007Q3', '2007Q4', '2008Q1', '2008Q2', '2008Q3',
             '2008Q4', '2009Q1', '2009Q2', '2009Q3'],
            dtype='period[Q-DEC]', length=203, freq='Q-DEC')

In [206]: data.index = index

In [207]: data.infl
Out[207]: 
1959Q1    0.00
1959Q2    2.34
1959Q3    2.74
1959Q4    0.27
1960Q1    2.31
1960Q2    0.14
1960Q3    2.70
1960Q4    1.21
1961Q1   -0.40
1961Q2    1.47
          ... 
2007Q2    2.75
2007Q3    3.45
2007Q4    6.38
2008Q1    2.82
2008Q2    8.53
2008Q3   -3.16
2008Q4   -8.79
2009Q1    0.94
2009Q2    3.37
2009Q3    3.56
Freq: Q-DEC, Name: infl, Length: 203, dtype: float64
```

# 11.6 重采样及频率转换

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

W
wizardforcel 已提交
1417
pandas 对象都带有一个 resample 方法,它是各种频率转换工作的主力函数。resample 有一个类似于 groupby 的 API,调用 resample 可以分组数据,然后会调用一个聚合函数:
W
gitbook  
wizardforcel 已提交
1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464
```python
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
```

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

W
wizardforcel 已提交
1467
表 11-5 resample 方法的参数
W
gitbook  
wizardforcel 已提交
1468 1469 1470 1471 1472 1473
![](img/7178691-b40a57086c904e83.png)



## 降采样

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

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

W
wizardforcel 已提交
1479
为了说明,我们来看一些“1 分钟”数据:
W
gitbook  
wizardforcel 已提交
1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501
```python
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
```

W
wizardforcel 已提交
1502
假设你想要通过求和的方式将这些数据聚合到“5 分钟”块中:
W
gitbook  
wizardforcel 已提交
1503 1504 1505 1506 1507 1508 1509 1510 1511 1512
```python
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
```

W
wizardforcel 已提交
1513
传入的频率将会以“5 分钟”的增量定义面元边界。默认情况下,面元的右边界是包含的,因此 00:00 到 00:05 的区间中是包含 00:05 的。传入 closed='left'会让区间以左边界闭合:
W
gitbook  
wizardforcel 已提交
1514 1515 1516 1517 1518 1519 1520 1521 1522 1523
```python
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
```

W
wizardforcel 已提交
1524
如你所见,最终的时间序列是以各面元右边界的时间戳进行标记的。传入 label='right'即可用面元的邮编界对其进行标记:
W
gitbook  
wizardforcel 已提交
1525 1526 1527 1528 1529 1530 1531 1532 1533 1534
```python
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
```

W
wizardforcel 已提交
1535
图 11-3 说明了“1 分钟”数据被转换为“5 分钟”数据的处理过程。
W
gitbook  
wizardforcel 已提交
1536

W
wizardforcel 已提交
1537
![图 11-3 各种 closed、label 约定的“5 分钟”重采样演示](img/7178691-7a77f47844f2ee8c.png)
W
gitbook  
wizardforcel 已提交
1538

W
wizardforcel 已提交
1539
最后,你可能希望对结果索引做一些位移,比如从右边界减去一秒以便更容易明白该时间戳到底表示的是哪个区间。只需通过 loffset 设置一个字符串或日期偏移量即可实现这个目的:
W
gitbook  
wizardforcel 已提交
1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552
```python
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
```

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

W
wizardforcel 已提交
1555
## OHLC 重采样
W
gitbook  
wizardforcel 已提交
1556

W
wizardforcel 已提交
1557
金融领域中有一种无所不在的时间序列聚合方式,即计算各面元的四个值:第一个值(open,开盘)、最后一个值(close,收盘)、最大值(high,最高)以及最小值(low,最低)。传入 how='ohlc'即可得到一个含有这四种聚合值的`DataFrame`。整个过程很高效,只需一次扫描即可计算出结果:
W
gitbook  
wizardforcel 已提交
1558 1559 1560 1561 1562 1563 1564 1565 1566
```python
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
```

W
wizardforcel 已提交
1567
## 升采样和插值
W
gitbook  
wizardforcel 已提交
1568

W
wizardforcel 已提交
1569
在将数据从低频率转换到高频率时,就不需要聚合了。我们来看一个带有一些周型数据的`DataFrame`
W
gitbook  
wizardforcel 已提交
1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582
```python
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
```

W
wizardforcel 已提交
1583
当你对这个数据进行聚合,每组只有一个值,这样就会引入缺失值。我们使用 asfreq 方法转换成高频,不经过聚合:
W
gitbook  
wizardforcel 已提交
1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599
```python
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
```

W
wizardforcel 已提交
1600
假设你想要用前面的周型值填充“非星期三”。resampling 的填充和插值方式跟 fillna 和 reindex 的一样:
W
gitbook  
wizardforcel 已提交
1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666
```python
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
```

同样,这里也可以只填充指定的时期数(目的是限制前面的观测值的持续使用距离):
```python
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
```

注意,新的日期索引完全没必要跟旧的重叠:
```python
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
```

## 通过时期进行重采样

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

```python
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
```

W
wizardforcel 已提交
1667
升采样要稍微麻烦一些,因为你必须决定在新频率中各区间的哪端用于放置原来的值,就像 asfreq 方法那样。convention 参数默认为'start',也可设置为'end':
W
gitbook  
wizardforcel 已提交
1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697

```python
# 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)。

W
wizardforcel 已提交
1698
如果不满足这些条件,就会引发异常。这主要影响的是按季、年、周计算的频率。例如,由 Q-MAR 定义的时间区间只能升采样为 A-MAR、A-JUN、A-SEP、A-DEC 等:
W
gitbook  
wizardforcel 已提交
1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726
```python
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),其中还包括那些窗口不定长的函数(如指数加权移动平均)。跟其他统计函数一样,移动窗口函数也会自动排除缺失值。

开始之前,我们加载一些时间序列数据,将其重采样为工作日频率:
```python
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()
```

W
wizardforcel 已提交
1727
现在引入 rolling 运算符,它与 resample 和 groupby 很像。可以在 Time`Series``DataFrame`以及一个 window(表示期数,见图 11-4)上调用它:
W
gitbook  
wizardforcel 已提交
1728 1729 1730 1731 1732 1733 1734
```python
In [238]: close_px.AAPL.plot()
Out[238]: <matplotlib.axes._subplots.AxesSubplot at 0x7f2f2570cf98>

In [239]: close_px.AAPL.rolling(250).mean().plot()
```

W
wizardforcel 已提交
1735
![图 11-4 苹果公司股价的 250 日均线](img/7178691-3327483eab730b09.png)
W
gitbook  
wizardforcel 已提交
1736

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

W
wizardforcel 已提交
1739
默认情况下,rolling 函数需要窗口中所有的值为非 NA 值。可以修改该行为以解决缺失数据的问题。其实,在时间序列开始处尚不足窗口期的那些数据就是个特例(见图 11-5):
W
gitbook  
wizardforcel 已提交
1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756
```python
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()
```

W
wizardforcel 已提交
1757
![图 11-5 苹果公司 250 日每日回报标准差](img/7178691-15f565bed1ccad09.png)
W
gitbook  
wizardforcel 已提交
1758

W
wizardforcel 已提交
1759
要计算扩展窗口平均(expanding window mean),可以使用 expanding 而不是 rolling。“扩展”意味着,从时间序列的起始处开始窗口,增加窗口直到它超过所有的序列。apple_std250 时间序列的扩展窗口平均如下所示:
W
gitbook  
wizardforcel 已提交
1760 1761 1762 1763
```python
In [244]: expanding_mean = appl_std250.expanding().mean()
```

W
wizardforcel 已提交
1764
`DataFrame`调用 rolling_mean(以及与之类似的函数)会将转换应用到所有的列上(见图 11-6):
W
gitbook  
wizardforcel 已提交
1765 1766 1767 1768
```python
In [246]: close_px.rolling(60).mean().plot(logy=True)
```

W
wizardforcel 已提交
1769
![图 11-6 各股价 60 日均线(对数 Y 轴)](img/7178691-979f748052b2279f.png)
W
gitbook  
wizardforcel 已提交
1770

W
wizardforcel 已提交
1771
rolling 函数也可以接受一个指定固定大小时间补偿字符串,而不是一组时期。这样可以方便处理不规律的时间序列。这些字符串也可以传递给 resample。例如,我们可以计算 20 天的滚动均值,如下所示:
W
gitbook  
wizardforcel 已提交
1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805
```python
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)函数。

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

W
wizardforcel 已提交
1806
除了 rolling 和 expanding,pandas 还有 ewm 运算符。下面这个例子对比了苹果公司股价的 30 日移动平均和 span=30 的指数加权移动平均(如图 11-7 所示):
W
gitbook  
wizardforcel 已提交
1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822
```python
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='k-', label='EW MA')
Out[253]: <matplotlib.axes._subplots.AxesSubplot at 0x7f2f252161d0>

In [254]: plt.legend()
```

W
wizardforcel 已提交
1823
![图 11-7 简单移动平均与指数加权移动平均](img/7178691-dae48defe3749fad.png)
W
gitbook  
wizardforcel 已提交
1824 1825 1826

## 二元移动窗口函数

W
wizardforcel 已提交
1827
有些统计运算(如相关系数和协方差)需要在两个时间序列上执行。例如,金融分析师常常对某只股票对某个参考指数(如标准普尔 500 指数)的相关系数感兴趣。要进行说明,我们先计算我们感兴趣的时间序列的百分数变化:
W
gitbook  
wizardforcel 已提交
1828 1829 1830 1831 1832 1833 1834 1835
```python
In [256]: spx_px = close_px_all['SPX']

In [257]: spx_rets = spx_px.pct_change()

In [258]: returns = close_px.pct_change()
```

W
wizardforcel 已提交
1836
调用 rolling 之后,corr 聚合函数开始计算与 spx_rets 滚动相关系数(结果见图 11-8):
W
gitbook  
wizardforcel 已提交
1837 1838 1839 1840 1841 1842
```python
In [259]: corr = returns.AAPL.rolling(125, min_periods=100).corr(spx_rets)

In [260]: corr.plot()
```

W
wizardforcel 已提交
1843
![图 11-8 AAPL 6 个月的回报与标准普尔 500 指数的相关系数](img/7178691-e81e0f602b4db0ed.png)
W
gitbook  
wizardforcel 已提交
1844

W
wizardforcel 已提交
1845
假设你想要一次性计算多只股票与标准普尔 500 指数的相关系数。虽然编写一个循环并新建一个`DataFrame`不是什么难事,但比较啰嗦。其实,只需传入一个 Time`Series`和一个`DataFrame`,rolling_corr 就会自动计算 TimeSeries(本例中就是 spx_rets)与`DataFrame`各列的相关系数。结果如图 11-9 所示:
W
gitbook  
wizardforcel 已提交
1846 1847 1848 1849 1850 1851
```python
In [262]: corr = returns.rolling(125, min_periods=100).corr(spx_rets)

In [263]: corr.plot()
```

W
wizardforcel 已提交
1852
![图 11-9 3 只股票 6 个月的回报与标准普尔 500 指数的相关系数](img/7178691-0a54a028a62b9b50.png)
W
gitbook  
wizardforcel 已提交
1853 1854 1855 1856


## 用户定义的移动窗口函数

W
wizardforcel 已提交
1857
rolling_apply 函数使你能够在移动窗口上应用自己设计的数组函数。唯一要求的就是:该函数要能从数组的各个片段中产生单个值(即约简)。比如说,当我们用 rolling(...).quantile(q)计算样本分位数时,可能对样本中特定值的百分等级感兴趣。`scipy.stats.percentileofscore`函数就能达到这个目的(结果见图 11-10):
W
gitbook  
wizardforcel 已提交
1858 1859 1860 1861 1862 1863 1864 1865 1866 1867
```python
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()
```

W
wizardforcel 已提交
1868
![图 11-10 AAPL 2%回报率的百分等级(一年窗口期)](img/7178691-af49e84a90c23c1e.png)
W
gitbook  
wizardforcel 已提交
1869

W
wizardforcel 已提交
1870
如果你没安装 SciPy,可以使用 conda 或 pip 安装。
W
gitbook  
wizardforcel 已提交
1871 1872 1873 1874 1875

# 11.8 总结

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

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