《利用Python进行数据分析》第10章 时间序列、日期和时间数
时间序列
日期和时间数据类型及工具
Python标准库包含用于日期(date)和时间(time)数据的数据类型,还有日历方面的功能。主要会用到datetime、time以及calendar模块。datetime.datetime(也可以简写为datetime)是用得最多的数据类型
from pandas import Series,DataFrame
import pandas as pd
import numpy as np
from datetime import datetime
now=datetime.now()
now
datetime.datetime(2017, 12, 27, 10, 25, 45, 574261)
now.year,now.month,now.day
(2017, 12, 26)
datetime以毫秒形式存储日期和时间。
datetime.timedelta表示两个datetime对象之间的时间差
delta=datetime(2017,12,1)-datetime(2012,9,8,8,15)
delta
datetime.timedelta(1909, 56700)
delta.days #日的时间差
1909
delta.seconds #秒的时间差
56700
可以给datetime对象加上(或减去)一个或多个timedelta,这样会产生一个新对象
from datetime import timedelta
start=datetime(2017,6,13)
start+timedelta(12)
datetime.datetime(2017, 6, 25, 0, 0)
start-2*timedelta(12)
datetime.datetime(2017, 5, 20, 0, 0)
datetime模块中的数据类型参见表10-1
字符串和datetime的相互转换
利用str或strftime方法(传入一个格式化字符串),datetime对象和pandas的Timestamp对象可以被格式化为字符串
stamp=datetime(2017,12,26)
str(stamp)
'2017-12-26 00:00:00'
stamp.strftime('%Y-%m-%d')
'2017-12-26'
datetime.strptime可以用表中的这些格式化编码将字符串转换为日期
value='2017-12-26'
datetime.strptime(value,'%Y-%m-%d')
datetime.datetime(2017, 12, 26, 0, 0)
datestrs=['11/12/2017','12/12/2017']
[datetime.strptime(x,'%m/%d/%Y') for x in datestrs]
[datetime.datetime(2017, 11, 12, 0, 0), datetime.datetime(2017, 12, 12, 0, 0)]
datetime.strptime是通过已知格式进行日期解析的最佳方式。d但是对于其他常见的日期格式,需要使用dateutil这个第三方包中的parser.parse方法
from dateutil.parser import parse
parse('2017-12-06')
datetime.datetime(2017, 12, 6, 0, 0)
dateutil可以解析几乎所有人类能够理解的日期表示形式
parse('Jan 31,2017 9:35 PM')
datetime.datetime(2017, 1, 31, 21, 35)
在国际通用的格式中,日通常出现在月的前面,传入dayfirst=True即可解决
parse('10/12/2017',dayfirst=True)
datetime.datetime(2017, 12, 10, 0, 0)
pandas通常是用于处理成组日期的,不管这些日期是DataFrame的轴索引还是列。to_datetime方法可以解析多种不同的日期表示形式。对标准日期格式(如ISO8601)的解析非常快。可以处理缺失值(None、空字符串等)
datestrs
['11/12/2017', '12/12/2017']
pd.to_datetime(datestrs)
DatetimeIndex(['2017-11-12', '2017-12-12'], dtype='datetime64[ns]', freq=None)
idx=pd.to_datetime(datestrs+[None])
idx
DatetimeIndex(['2017-11-12', '2017-12-12', 'NaT'], dtype='datetime64[ns]', freq=None)
idx[2]
NaT
idx[1]
Timestamp('2017-12-12 00:00:00')
pd.isnull(idx)
array([False, False, True], dtype=bool)
NaT(Not a Time)是pandas中时间戳数据的NA值
警告: dateutil.parser是一个实用但不完美的工具。比如说,它会把一些原本不是日期的字符串认作是日期(比如"42"会被解析为2042年的今天)
时间序列基础
pandas最基本的时间序列类型就是以时间戳(通常以Python字符串或datatime对象表示)为索引的Series
from datetime import datetime
dates=[datetime(2017,1,6),datetime(2017,1,10),datetime(2017,1,13),
datetime(2017,1,15),datetime(2017,1,18),datetime(2017,1,20)]
ts=Series(np.random.randn(6),index=dates)
ts
2017-01-06 0.005613
2017-01-10 1.222545
2017-01-13 0.000879
2017-01-15 0.172794
2017-01-18 -0.530976
2017-01-20 -0.808555
dtype: float64
可以看出datetime对象是被放在一个DatetimeIndex中,变量ts就成为一个TimeSeries
type(ts)
pandas.core.series.Series
ts.index
DatetimeIndex(['2017-01-06', '2017-01-10', '2017-01-13', '2017-01-15',
'2017-01-18', '2017-01-20'],
dtype='datetime64[ns]', freq=None)
注意: 没必要显式使用TimeSeries的构造函数。当创建一个带有DatetimeIndex的Series时,pandas就会知道该对象是一个时间序列。
ts[::2]
2017-01-06 0.170305
2017-01-13 -0.798955
2017-01-18 -0.367438
dtype: float64
跟其他Series一样,不同索引的时间序列之间的算术运算会自动按日期对齐
ts+ts[::2]
2017-01-06 0.340610
2017-01-10 NaN
2017-01-13 -1.597911
2017-01-15 NaN
2017-01-18 -0.734876
2017-01-20 NaN
dtype: float64
pandas用NumPy的datetime64数据类型以纳秒形式存储时间戳
ts.index.dtype
dtype('<M8[ns]')
DatetimeIndex中的各个标量值是pandas的Timestamp对象
stamp=ts.index[0]
stamp
Timestamp('2017-01-06 00:00:00')
ts.index[3]
Timestamp('2017-01-15 00:00:00')
如果有需要,TimeStamp可以随时自动转换为datetime对象
索引、选取、子集构造
由于TimeSeries是Series的一个子类,所以在索引以及数据选取方面它们的行为是一样的
stamp=ts.index[2]
stamp
Timestamp('2017-01-13 00:00:00')
ts[stamp]
-0.79895542662575525
可以传入一个可以被解释为日期的字符串
ts['1/10/2017']
1.3762403458229933
ts['20170118']
-0.36743784149263731
对于较长的时间序列,只需传入“年”或“年月”即可轻松选取数据的切片
longer_ts=Series(np.random.randn(1000),
index=pd.date_range('11/25/2017',periods=1000))
longer_ts
2017-11-25 -0.659067
2017-11-26 -1.539291
2017-11-27 -1.731498
2017-11-28 -0.560947
... ...
2020-08-17 -0.499714
2020-08-18 -1.978626
2020-08-19 1.757046
2020-08-20 -0.401384
Freq: D, Length: 1000, dtype: float64
longer_ts['2017']
2017-11-25 -0.659067
2017-11-26 -1.539291
2017-11-27 -1.731498
2017-11-28 -0.560947
2017-11-29 -2.086909
... ...
2017-12-28 0.123651
2017-12-29 -0.188641
2017-12-30 1.206445
2017-12-31 -1.734814
Freq: D, dtype: float64
longer_ts['2017-11']
2017-11-25 -0.659067
2017-11-26 -1.539291
2017-11-27 -1.731498
2017-11-28 -0.560947
2017-11-29 -2.086909
2017-11-30 -0.673677
Freq: D, dtype: float64
通过日期进行切片的方式只对规则Series有效。
ts
2017-01-06 0.170305
2017-01-10 1.376240
2017-01-13 -0.798955
2017-01-15 -0.496169
2017-01-18 -0.367438
2017-01-20 0.059816
dtype: float64
ts[datetime(2017,1,13):]
2017-01-13 -0.798955
2017-01-15 -0.496169
2017-01-18 -0.367438
2017-01-20 0.059816
dtype: float64
由于大部分时间序列数据都是按照时间先后排序的,因此你也可以用不存在于该时间序列中的时间戳对其进行切片(即范围查询
ts['1/12/2017':'1/17/2017']
2017-01-13 -0.798955
2017-01-15 -0.496169
dtype: float64
这里可以传入字符串日期、datetime或Timestamp。注意,这样切片所产生的是源时间序列的视图,跟NumPy数组的切片运算是一样的。此外,还有一个等价的实例方法也可以截取两个日期之间TimeSeries
ts.truncate(after='1/14/2017')
2017-01-06 0.170305
2017-01-10 1.376240
2017-01-13 -0.798955
dtype: float64
这些操作对DataFrame也有效
dates=pd.date_range('1/1/2017',periods=100,freq='W-WED')
long_df=DataFrame(np.random.randn(100,4),index=dates,
columns=['Colorado','Texas','New York','Ohio'])
long_df.ix['5-2017']
带有重复索引的时间序列
在某些应用场景中,可能会存在多个观测数据落在同一个时间点上的情况
dates=pd.DatetimeIndex(['1/1/2017','1/2/2017','1/2/2017','1/2/2017','1/3/2017'])
dup_ts=Series(np.arange(5),index=dates)
dup_ts
2017-01-01 0
2017-01-02 1
2017-01-02 2
2017-01-02 3
2017-01-03 4
dtype: int32
通过检查索引的is_unique属性,我们就可以知道它是不是唯一的
dup_ts.index.is_unique
False
对这个时间序列进行索引,要么产生标量值,要么产生切片,具体要看所选的时间点是否重复
dup_ts['1/3/2017'] #不重复
4
dup_ts['1/2/2017'] #重复
2017-01-02 1
2017-01-02 2
2017-01-02 3
dtype: int32
假设你想要对具有非唯一时间戳的数据进行聚合。一个办法是使用groupby,并传入level=0(索引的唯一一层!)
grouped=dup_ts.groupby(level=0)
grouped.mean()
2017-01-01 0
2017-01-02 2
2017-01-03 4
dtype: int32
grouped.count()
2017-01-01 1
2017-01-02 3
2017-01-03 1
dtype: int64
日期的范围、频率以及移动
pandas有一整套标准时间序列频率以及用于重采样、频率推断、生成固定频率日期范围的工具。例如,我们可以将之前那个时间序列转换为一个具有固定频率(每日)的时间序列,只需调用resample
ts
2017-01-06 0.005613
2017-01-10 1.222545
2017-01-13 0.000879
2017-01-15 0.172794
2017-01-18 -0.530976
2017-01-20 -0.808555
dtype: float64
ts.resample('D')
DatetimeIndexResampler [freq=<Day>, axis=0, closed=left, label=left, convention=start, base=0]
生成日期范围
pandas.date_range可用于生成指定长度的DatetimeIndex
index=pd.date_range('6/10/2017','7/1/2017')
index
DatetimeIndex(['2017-06-10', '2017-06-11', '2017-06-12', '2017-06-13',
'2017-06-14', '2017-06-15', '2017-06-16', '2017-06-17',
'2017-06-18', '2017-06-19', '2017-06-20', '2017-06-21',
'2017-06-22', '2017-06-23', '2017-06-24', '2017-06-25',
'2017-06-26', '2017-06-27', '2017-06-28', '2017-06-29',
'2017-06-30', '2017-07-01'],
dtype='datetime64[ns]', freq='D')
默认情况下,date_range会产生按天计算的时间点。如果只传入起始或结束日期,那就还得传入一个表示一段时间的数字
pd.date_range(start='12/8/2017',periods=20)
DatetimeIndex(['2017-12-08', '2017-12-09', '2017-12-10', '2017-12-11',
'2017-12-12', '2017-12-13', '2017-12-14', '2017-12-15',
'2017-12-16', '2017-12-17', '2017-12-18', '2017-12-19',
'2017-12-20', '2017-12-21', '2017-12-22', '2017-12-23',
'2017-12-24', '2017-12-25', '2017-12-26', '2017-12-27'],
dtype='datetime64[ns]', freq='D')
起始和结束日期定义了日期索引的严格边界。例如,如果你想要生成一个由每月最后一个工作日组成的日期索引,可以传入"BM"频率(表示business end of month),这样就只会包含时间间隔内(或刚好在边界上的)符合频率要求的日期
pd.date_range('1/1/2017','12/1/2017',freq='BM')
DatetimeIndex(['2017-01-31', '2017-02-28', '2017-03-31', '2017-04-28',
'2017-05-31', '2017-06-30', '2017-07-31', '2017-08-31',
'2017-09-29', '2017-10-31', '2017-11-30'],
dtype='datetime64[ns]', freq='BM')
date_range默认会保留起始和结束时间戳的时间信息(如果有的话)
pd.date_range('5/6/2017 12:30:20',periods=5)
DatetimeIndex(['2017-05-06 12:30:20', '2017-05-07 12:30:20',
'2017-05-08 12:30:20', '2017-05-09 12:30:20',
'2017-05-10 12:30:20'],
dtype='datetime64[ns]', freq='D')
有时,虽然起始和结束日期带有时间信息,但你希望产生一组被规范化(normalize)到午夜的时间戳。normalize选项即可实现该功能
pd.date_range('12/15/2017 12:50:33',periods=5,normalize=True)
DatetimeIndex(['2017-12-15', '2017-12-16', '2017-12-17', '2017-12-18',
'2017-12-19'],
dtype='datetime64[ns]', freq='D')
频率和日期偏移量
pandas中的频率是由一个基础频率(base frequency)和一个乘数组成的。基础频率通常以一个字符串别名表示,比如"M"表示每月,"H"表示每小时。对于每个基础频率,都有一个被称为日期偏移量(date offset)的对象与之对应。例如,按小时计算的频率可以用Hour类表示
from pandas.tseries.offsets import Hour,Minute
hour=Hour()
hour
<Hour>
传入一个整数即可定义偏移量的倍数
four_fours=Hour(4)
four_fours
<4 * Hours>
无需显式创建这样的对象,只需使用诸如"H"或"4H"这样的字符串别名即可。在基础频率前面放上一个整数即可创建倍数
pd.date_range('1/1/2017','1/3/2017 23:59',freq='4H')
DatetimeIndex(['2017-01-01 00:00:00', '2017-01-01 04:00:00',
'2017-01-01 08:00:00', '2017-01-01 12:00:00',
'2017-01-01 16:00:00', '2017-01-01 20:00:00',
'2017-01-02 00:00:00', '2017-01-02 04:00:00',
'2017-01-02 08:00:00', '2017-01-02 12:00:00',
'2017-01-02 16:00:00', '2017-01-02 20:00:00',
'2017-01-03 00:00:00', '2017-01-03 04:00:00',
'2017-01-03 08:00:00', '2017-01-03 12:00:00',
'2017-01-03 16:00:00', '2017-01-03 20:00:00'],
dtype='datetime64[ns]', freq='4H')
大部分偏移量对象都可通过加法进行连接;你也可以传入频率字符串(如"2h30min"),这种字符串可以被高效地解析为等效的表达式
Hour(3)+Minute(30)
<210 * Minutes>
pd.date_range('1/1/2017',periods=10,freq='1h30min')
DatetimeIndex(['2017-01-01 00:00:00', '2017-01-01 01:30:00',
'2017-01-01 03:00:00', '2017-01-01 04:30:00',
'2017-01-01 06:00:00', '2017-01-01 07:30:00',
'2017-01-01 09:00:00', '2017-01-01 10:30:00',
'2017-01-01 12:00:00', '2017-01-01 13:30:00'],
dtype='datetime64[ns]', freq='90T')
有些频率所描述的时间点并不是均匀分隔的。例如,"M"(日历月末)和"BM"(每月最后一个工作日)就取决于每月的天数,对于后者,还要考虑月末是不是周末。由于没有更好的术语,我将这些称为锚点偏移量(anchored offset)。
表10-4列出了pandas中的频率代码和日期偏移量类。
WOM日期
WOM(Week Of Month)是一种非常实用的频率类,它以WOM开头。它使你能获得诸如“每月第3个星期五”之类的日期
rng=pd.date_range('1/1/2017','9/1/2017',freq='WOM-3FRI')
list(rng)
[Timestamp('2017-01-20 00:00:00', freq='WOM-3FRI'),
Timestamp('2017-02-17 00:00:00', freq='WOM-3FRI'),
Timestamp('2017-03-17 00:00:00', freq='WOM-3FRI'),
Timestamp('2017-04-21 00:00:00', freq='WOM-3FRI'),
Timestamp('2017-05-19 00:00:00', freq='WOM-3FRI'),
Timestamp('2017-06-16 00:00:00', freq='WOM-3FRI'),
Timestamp('2017-07-21 00:00:00', freq='WOM-3FRI'),
Timestamp('2017-08-18 00:00:00', freq='WOM-3FRI')]
美国的股票期权交易人会意识到这些日子就是标准的月度到期日
移动(超前和滞后)数据
移动(shifting)指的是沿着时间轴将数据前移或后移。Series和DataFrame都有一个shift方法用于执行单纯的前移或后移操作,保持索引不变
ts=Series(np.random.randn(4),
index=pd.date_range('1/1/2017',periods=4,freq='M'))
ts
2017-01-31 -1.555270
2017-02-28 1.325946
2017-03-31 -1.458090
2017-04-30 1.064776
Freq: M, dtype: float64
ts.shift(2)
2017-01-31 NaN
2017-02-28 NaN
2017-03-31 -1.555270
2017-04-30 1.325946
Freq: M, dtype: float64
ts.shift(-2)
2017-01-31 -1.458090
2017-02-28 1.064776
2017-03-31 NaN
2017-04-30 NaN
Freq: M, dtype: float64
shift通常用于计算一个时间序列或多个时间序列(如DataFrame的列)中的百分比变化。可以这样表达:
ts / ts.shift(1) - 1
由于单纯的移位操作不会修改索引,所以部分数据会被丢弃。因此,如果频率已知,则可以将其传给shift以便实现对时间戳进行位移而不是对数据进行简单位移
ts.shift(2,freq='M')
2017-03-31 -1.555270
2017-04-30 1.325946
2017-05-31 -1.458090
2017-06-30 1.064776
Freq: M, dtype: float64
还可以使用其他频率,于是你就能非常灵活地对数据进行超前和滞后处理了
ts.shift(3,freq='D')
2017-02-03 -1.555270
2017-03-03 1.325946
2017-04-03 -1.458090
2017-05-03 1.064776
dtype: float64
ts.shift(1,freq='3D')
2017-02-03 -1.555270
2017-03-03 1.325946
2017-04-03 -1.458090
2017-05-03 1.064776
dtype: float64
ts.shift(-3,freq='D')
2017-01-28 -1.555270
2017-02-25 1.325946
2017-03-28 -1.458090
2017-04-27 1.064776
dtype: float64
通过偏移量对日期进行位移
pandas的日期偏移量还可以用在datetime或Timestamp对象上
from pandas.tseries.offsets import Day,MonthEnd
now=datetime(2017,12,27)
now+3*Day()
Timestamp('2017-12-30 00:00:00')
如果加的是锚点偏移量(比如MonthEnd),第一次增量会将原日期向前滚动到符合频率规则的下一个日期
now+MonthEnd()
Timestamp('2017-12-31 00:00:00')
now+MonthEnd(3)
Timestamp('2018-02-28 00:00:00')
通过锚点偏移量的rollforward和rollback方法,可显式地将日期向前或向后“滚动”
offset=MonthEnd()
offset.rollforward(now)
Timestamp('2017-12-31 00:00:00')
offset.rollback(now)
Timestamp('2017-11-30 00:00:00')
日期偏移量还有一个巧妙的用法,即结合groupby使用这两个“滚动”方法
ts=Series(np.random.randn(10),
index=pd.date_range('1/15/2017',periods=10,freq='4d'))
ts
2017-01-15 1.060753
2017-01-19 0.410493
2017-01-23 2.188356
2017-01-27 -0.335335
2017-01-31 -1.976843
2017-02-04 1.644975
2017-02-08 0.243292
2017-02-12 -0.444232
2017-02-16 -0.665907
2017-02-20 -0.771520
Freq: 4D, dtype: float64
ts.groupby(offset.rollforward).mean()
2017-01-31 0.269485
2017-02-28 0.001322
dtype: float64
更简单、更快速地实现该功能的办法是使用resample
ts.resample('M',how='mean')
2017-01-31 0.269485
2017-02-28 0.001322
Freq: M, dtype: float64
本章的时间序列知识点比较多,需要分阶段练习。