《利用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

本章的时间序列知识点比较多,需要分阶段练习。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容