文章目录
《Python数据科学手册》读书笔记
处理时间序列
由于 Pandas 最初是为金融模型而创建的, 因此它拥有一些功能非常强
大的日期、 时间、 带时间索引数据的处理工具。 本节将介绍的日期与时
间数据主要包含三类。
-
时间戳表示某个具体的时间点(例如 2015 年 7 月 4 日上午 7
点) 。 -
时间间隔与周期表示开始时间点与结束时间点之间的时间长度, 例
如 2015 年(指的是 2015 年 1 月 1 日至 2015 年 12 月 31 日这段时
间间隔) 。 周期通常是指一种特殊形式的时间间隔, 每个间隔长度
相同, 彼此之间不会重叠(例如, 以 24 小时为周期构成每一
天) 。 -
时间增量(time delta) 或持续时间(duration) 表示精确的时间长
度(例如, 某程序运行持续时间 22.56 秒) 。
Python的日期与时间工具
在 Python 标准库与第三方库中有许多可以表示日期、 时间、 时间增量
和时间跨度(timespan) 的工具。
- 原生Python的日期与时间工具: datetime与dateutil
Python 基本的日期与时间功能都在标准库的 datetime 模块中。 如
果和第三方库 dateutil 模块搭配使用, 可以快速实现许多处理日
期与时间的功能。 例如, 你可以用 datetime 类型创建一个日期:
from datetime import datetime
datetime(year=2015, month=7, day=4)
datetime.datetime(2015, 7, 4, 0, 0)
或者使用 dateutil 模块对各种字符串格式的日期进行正确解析:
from dateutil import parser
date = parser.parse("4th of July, 2015")
date
datetime.datetime(2015, 7, 4, 0, 0)
一旦有了 datetime 对象, 就可以进行许多操作了, 例如打印出这
一天是星期几:
date.strftime('%A')
'Saturday'
datetime 和 dateutil 模块在灵活性与易用性方面都表现出色,
你可以用这些对象及其相应的方法轻松完成你感兴趣的任意操作。
但如果你处理的时间数据量比较大, 那么速度就会比较慢。 就像之
前介绍过的 Python 的原生列表对象没有 NumPy 中已经被编码的数
值类型数组的性能好一样, Python 的原生日期对象同样也没有
NumPy 中已经被编码的日期(encoded dates) 类型数组的性能好。
- 时间类型数组: NumPy的datetime64类型
datetime64 类型将日期编码为 64 位整
数, 这样可以让日期数组非常紧凑(节省内存) 。 datetime64 需
要在设置日期时确定具体的输入类型:
import numpy as np
date = np.array('2015-07-04', dtype=np.datetime64)
date
array('2015-07-04', dtype='datetime64[D]')
import numpy as np
date = np.array('2015-07-04', dtype=np.datetime64)
date
array('2015-07-04', dtype='datetime64[D]')
但只要有了这个日期格式, 就可以进行快速的向量化运算:
date + np.arange(12)
array(['2015-07-04', '2015-07-05', '2015-07-06', '2015-07-07',
'2015-07-08', '2015-07-09', '2015-07-10', '2015-07-11',
'2015-07-12', '2015-07-13', '2015-07-14', '2015-07-15'],
dtype='datetime64[D]')
因为 NumPy 的 datetime64 数组内元素的类型是统一的, 所以这
种数组的运算速度会比 Python 的 datetime 对象的运算速度快很
多, 尤其是在处理较大数组时。
datetime64 与 timedelta64 对象的一个共同特点是, 它们都是
在 基本时间单位(fundamental time unit) 的基础上建立的。 由于
datetime64 对象是 64 位精度, 所以可编码的时间范围可以是基
本单元的 264 倍。 也就是说, datetime64 在时间精度(time
resolution) 与最大时间跨度(maximum time span) 之间达成了一
种平衡。
比如你想要一个时间纳秒(nanosecond, ns) 级的时间精度, 那么
你就可以将时间编码到 0~264 纳秒或 600 年之内, NumPy 会自动判
断输入时间需要使用的时间单位。 例如, 下面是一个以天为单位的日期:
np.datetime64('2015-07-04')
numpy.datetime64('2015-07-04')
而这是一个以分钟为单位的日期:
np.datetime64('2015-07-04 12:00')
numpy.datetime64('2015-07-04T12:00')
需要注意的是, 时区将自动设置为执行代码的操作系统的当地时
区。 你可以通过各种格式的代码设置基本时间单位。 例如, 将时间
单位设置为纳秒:
np.datetime64('2015-07-04 12:59:59.50', 'ns')
numpy.datetime64('2015-07-04T12:59:59.500000000')
- 日期与时间单位格式代码
代码 | 含义 | 时间跨度 (相对) | 时间跨度 (绝对) |
---|---|---|---|
Y | 年(year) | ± 9.2e18 年 | [9.2e18 BC, 9.2e18 AD] |
M | 月(month) | ± 7.6e17 年 | [7.6e17 BC, 7.6e17 AD] |
W | 周(week) | ± 1.7e17 年 | [1.7e17 BC, 1.7e17 AD] |
D | 日(day) | ± 2.5e16 年 | [2.5e16 BC, 2.5e16 AD] |
h | 时(hour) | ± 1.0e15 年 | [1.0e15 BC, 1.0e15 AD] |
m | 分(minute) | ± 1.7e13 年 | [1.7e13 BC, 1.7e13 AD] |
s | 秒(second) | ± 2.9e12 年 | [ 2.9e9 BC, 2.9e9 AD]ms 毫秒(millisecond) ± 2.9e9 年 [ 2.9e6 BC, 2.9e6 AD] |
us | 微秒(microsecond) | ± 2.9e6 年 | [290301 BC, 294241 AD] |
ns | 纳秒(nanosecond) | ± 292 年 | [ 1678 AD, 2262 AD] |
ps | 皮秒(picosecond) | ± 106 天 | [ 1969 AD, 1970 AD] |
fs | 飞秒(femtosecond) | ± 2.6 小时 | [ 1969 AD, 1970 AD] |
as | 原秒(attosecond) | ± 9.2 秒 | [ 1969 AD, 1970 AD] |
对于日常工作中的时间数据类型, 默认单位都用纳秒
datetime64[ns], 因为用它来表示时间范围精度可以满足绝大部
分需求。
最后还需要说明一点, 虽然 datetime64 弥补了 Python 原生的
datetime 类型的不足, 但它缺少了许多 datetime(尤其是
dateutil) 原本具备的便捷方法与函数, 具体内容请参考 NumPy
的 datetime64 文档
(http://docs.scipy.org/doc/numpy/reference/arrays.datetime.html) 。
- Pandas的日期与时间工具: 理想与现实的最佳解决方案
Pandas 所有关于日期与时间的处理方法全部都是通过 Timestamp
对象实现的, 它利用 numpy.datetime64 的有效存储和向量化接
口将 datetime 和 dateutil 的易用性有机结合起来。 Pandas 通过
一组 Timestamp 对象就可以创建一个可以作为 Series 或
DataFrame 索引的 DatetimeIndex,。
import pandas as pd
date = pd.to_datetime("4th of July, 2015")
date
Timestamp('2015-07-04 00:00:00')
date.strftime('%A')
'Saturday'
另外, 也可以直接进行 NumPy 类型的向量化运算
date + pd.to_timedelta(np.arange(12), 'D')
DatetimeIndex(['2015-07-04', '2015-07-05', '2015-07-06', '2015-07-07',
'2015-07-08', '2015-07-09', '2015-07-10', '2015-07-11',
'2015-07-12', '2015-07-13', '2015-07-14', '2015-07-15'],
dtype='datetime64[ns]', freq=None)
## 下面将详细介绍 Pandas 用来处理时间序列数据的工具。
Pandas 时间序列工具非常适合用来处理带时间戳的索引数据。 例如,
可以通过一个时间索引数据创建一个 Series 对象:
index = pd.DatetimeIndex(['2014-07-04', '2014-08-04',
'2015-07-04', '2015-08-04'])
data = pd.Series([0, 1, 2, 3], index=index)
data
2014-07-04 0
2014-08-04 1
2015-07-04 2
2015-08-04 3
dtype: int64
有了一个带时间索引的 Series 之后, 就能用它来演示之前介绍过的
Series 取值方法, 可以直接用日期进行切片取值:
data['2014-07-04':'2015-07-04']
2014-07-04 0
2014-08-04 1
2015-07-04 2
dtype: int64
另外, 还有一些仅在此类 Series 上可用的取值操作, 例如直接通过年
份切片获取该年的数据:
Pandas时间序列数据结构
-
针对时间戳数据, Pandas 提供了 Timestamp 类型。 与前面介绍的
一样, 它本质上是 Python 的原生 datetime 类型的替代品, 但是
在性能更好的 numpy.datetime64 类型的基础上创建。 对应的索
引数据结构是 DatetimeIndex。 -
针对时间周期数据, Pandas 提供了 Period 类型。 这是利用
numpy.datetime64 类型将固定频率的时间间隔进行编码。 对应的
索引数据结构是 PeriodIndex。 -
针对时间增量或持续时间, Pandas 提供了 Timedelta 类
型。 Timedelta 是一种代替 Python 原生 datetime.timedelta 类
型的高性能数据结构, 同样是基于 numpy.timedelta64 类型。 对
应的索引数据结构是 TimedeltaIndex。
最基础的日期 / 时间对象是 Timestamp 和 DatetimeIndex。
这两种对
象可以直接使用, 最常用的方法是 pd.to_datetime() 函数, 它可以
解析许多日期与时间格式。 对 pd.to_datetime() 传递一个日期会返
回一个 Timestamp 类型, 传递一个时间序列会返回一个
DatetimeIndex 类型:
dates = pd.to_datetime([datetime(2015, 7, 3), '4th of July, 2015',
'2015-Jul-6', '07-07-2015', '20150708'])
dates
DatetimeIndex(['2015-07-03', '2015-07-04', '2015-07-06', '2015-07-07',
'2015-07-08'],
dtype='datetime64[ns]', freq=None)
任何 DatetimeIndex 类型都可以通过 to_period() 方法和一个频率代
码转换成 PeriodIndex 类型。 下面用 ‘D’ 将数据转换成单日的时间序
列:
dates.to_period('D')
PeriodIndex(['2015-07-03', '2015-07-04', '2015-07-06', '2015-07-07',
'2015-07-08'],
dtype='period[D]', freq='D')
当用一个日期减去另一个日期时, 返回的结果是 TimedeltaIndex 类
型:
dates - dates[0]
TimedeltaIndex(['0 days', '1 days', '3 days', '4 days', '5 days'], dtype='timedelta64[ns]', freq=None)
有规律的时间序列: pd.date_range()
为了能更简便地创建有规律的时间序列, Pandas 提供了一些方
法: pd.date_range() 可以处理时间戳、 pd.period_range() 可以处
理周期、 pd.timedelta_range() 可以处理时间间隔。 我们已经介绍
过, Python 的 range() 和 NumPy 的 np.arange() 可以用起点、 终点
和步长(可选的) 创建一个序列。 pd.date_range() 与之类似, 通过
开始日期、 结束日期和频率代码(同样是可选的) 创建一个有规律的日
期序列, 默认的频率是天:
pd.date_range('2015-07-03', '2015-07-10')
DatetimeIndex(['2015-07-03', '2015-07-04', '2015-07-05', '2015-07-06',
'2015-07-07', '2015-07-08', '2015-07-09', '2015-07-10'],
dtype='datetime64[ns]', freq='D')
此外, 日期范围不一定非是开始时间与结束时间, 也可以是开始时间与
周期数 periods:
pd.date_range('2015-07-03', periods=8)
DatetimeIndex(['2015-07-03', '2015-07-04', '2015-07-05', '2015-07-06',
'2015-07-07', '2015-07-08', '2015-07-09', '2015-07-10'],
dtype='datetime64[ns]', freq='D')
可以通过 freq 参数改变时间间隔, 默认值是 D。 例如, 可以创建一
个按小时变化的时间戳:
pd.date_range('2015-07-03', periods=8, freq='H')
DatetimeIndex(['2015-07-03 00:00:00', '2015-07-03 01:00:00',
'2015-07-03 02:00:00', '2015-07-03 03:00:00',
'2015-07-03 04:00:00', '2015-07-03 05:00:00',
'2015-07-03 06:00:00', '2015-07-03 07:00:00'],
dtype='datetime64[ns]', freq='H')
如果要创建一个有规律的周期或时间间隔序列, 有类似的函数
pd.period_range() 和 pd.timedelta_range()。 下面是一个以月为
周期的示例:
pd.period_range('2015-07', periods=8, freq='M')
PeriodIndex(['2015-07', '2015-08', '2015-09', '2015-10', '2015-11', '2015-12',
'2016-01', '2016-02'],
dtype='period[M]', freq='M')
以及一个以小时递增的序列:
pd.timedelta_range(0, periods=10, freq='H')
TimedeltaIndex(['00:00:00', '01:00:00', '02:00:00', '03:00:00', '04:00:00',
'05:00:00', '06:00:00', '07:00:00', '08:00:00', '09:00:00'],
dtype='timedelta64[ns]', freq='H')
时间频率与偏移量
Pandas 时间序列工具的基础是时间频率或偏移量(offset) 代码。 就像
之前见过的 D(day) 和 H(hour) 代码, 可以用这些代码设置任意
需要的时间间隔。
- Pandas频率代码
代码 | 描述 | 代码 | 描述 |
---|---|---|---|
D | 天(calendar day, 按日历算, 含双休日) | B | 天(business day, 仅含工作日) |
W | 周(weekly) | ||
M | 月末(month end) | BM | 月末(business month end, 仅含工作日) |
Q | 季末(quarter end) | BQ | 季末(business quarter end, 仅含工作日) |
A | 年末(year end) | BA | 年末(business year end, 仅含工作日) |
H | 小时(hours) | BH | 小时 |
T | 分钟(minutes) | ||
S | 秒(seconds) | ||
L | 毫秒(milliseonds) | ||
U | 微秒(microseconds) | ||
N | 纳秒(nanoseconds) |
月、 季、 年频率都是具体周期的结束时间(月末、 季末、 年末) , 而有
一些以 S(start, 开始) 为后缀的代码表示日期开始 。
- 带开始索引的频率代码
代码 | 频率 |
---|---|
MS | 月初(month start) |
BMS | 月初(business month start, 仅含工作日) |
QS | 季初(quarter start) |
BQS | 季初(business quarter start, 仅含工作日) |
AS | 年初(year start) |
BAS | 年初(business year start, 仅含工作日) |
另外, 你可以在频率代码后面加三位月份缩写字母来改变季、 年频率的
开始时间。
-
Q-JAN、 BQ-FEB、 QS-MAR、 BQS-APR 等。
-
A-JAN、 BA-FEB、 AS-MAR、 BAS-APR 等。
同理, 也可以在后面加三位星期缩写字母来改变一周的开始时间。
- W-SUN、 W-MON、 W-TUE、 W-WED 等。
在这些代码的基础上, 还可以将频率组合起来创建的新的周期。 例如,
可以用小时(H) 和分钟(T) 的组合来实现 2 小时 30 分钟:
pd.timedelta_range(0, periods=9, freq="2H30T")
TimedeltaIndex(['00:00:00', '02:30:00', '05:00:00', '07:30:00', '10:00:00',
'12:30:00', '15:00:00', '17:30:00', '20:00:00'],
dtype='timedelta64[ns]', freq='150T')
所有这些频率代码都对应 Pandas 时间序列的偏移量, 具体内容可以在
pd.tseries.offsets 模块中找到。 例如, 可以用下面的方法直接创建
一个工作日偏移序列:
from pandas.tseries.offsets import BDay
pd.date_range('2015-07-01', periods=5, freq=BDay())
DatetimeIndex(['2015-07-01', '2015-07-02', '2015-07-03', '2015-07-06',
'2015-07-07'],
dtype='datetime64[ns]', freq='B')