时间序列(一)
本文中可能使用的数据集来自: 《利用python进行数据分析》数据集
日期和时间数据的类型和工具
python标准库中包含了时间和日期数据的类型比如datetime.datetime类:
from datetime import datetime
now = datetime.now()
print(now)
# 2020-04-02 19:39:38.819068
print(type(now))
# <class 'datetime.datetime'>
print(now.year,now.month,now.day)
# 2020 4 2
我们可以一用两个datetime对象相减来计算时间差:
res = datetime(2011,1,7)-datetime(2008,6,24,8,15)
print(type(res))
# <class 'datetime.timedelta'>
print(res)
# 926 days, 15:45:00
print(res.seconds)
# 56700
注意res和now两个对象的类型。
我们可以为一个datetime对象加上或者减去一个datedelta的整数倍来得到一个人新的datetime对象。在此不再演示。
以下是datetime模块中的一些数据类型:
类型 | 描述 |
---|---|
date | 使用公历日历存储日历日期(年,月,日) |
time | 将时间存储为小时,分钟,秒和微秒 |
datetime | 存储日期和时间 |
timedelta | 表示两个datetime值之间的差(如日,秒或者微秒) |
tzinfo | 用于存储时区信息的基本类型 |
字符串与datetime互相转换
可以使用str方法或传递一个格式给datetime的strftime或者pandas中的Timestanp对象来将其转化为字符串的格式:
stamp = datetime(2011,1,3)
print(str(stamp))
# 2011-01-03 00:00:00
print(stamp.strftime('%Y-%m-%d'))
# 2011-01-03
日期格式代码的完整列表:
类型 | 描述 |
---|---|
%Y | 四位的年份 |
%y | 两位的年份 |
%m | 两位的月份【01,12】 |
%d | 两位的日期号【01,31】 |
%H | 24小时制【0,23】 |
%I | 12小时制【01,12】 |
%M | 两位的分钟【00,59】 |
%S | 秒【00,61】(60,61是闰秒) |
%w | 星期日期【0(星期天),6】 |
%U | 一年中的星期数【00,53】,以星期天为每周的第一天,一年中第一个星期天前的日期作为第0周 |
%W | 一年中的星期数【00,53】,以星期一为每周的第一天 |
%z | 格式为+HHMM或者-HHMM的UTC时区偏移;如果没有时区则为空 |
%F | %Y-%m-%d的简写 |
%D | %m/%d/%y的简写 |
可以使用datetime.strptime方法将字符串转化为日期:
value = "2011-01-03"
time = datetime.strptime(value,"%Y-%m-%d")
print(time)
# 2011-01-03 00:00:00
datestrs = ["7/6/2011",'8/6/2011']
times = [datetime.strptime(x,"%m/%d/%Y") for x in datestrs]
print(times)
# [datetime.datetime(2011, 7, 6, 0, 0), datetime.datetime(2011, 8, 6, 0, 0)]
使用strptime可以将字符串类型的日期转化为datetime类型,但是每次都需要输入日期的格式,非常的麻烦,为此我们可以使用dateuil包中 的parser.parse方法(这个包在安装pandas时也会自动安装),该方法可以将各种类型的字符串类型的时间转换为datetime类型:
from dateutil.parser import parse
print(parse("'2011-01-03"))
# 2011-01-03 00:00:00
print(parse("Jan 31,1997 10:45 PM"))
# 2020-01-31 22:45:00
在一些情况下,日期会出现在月份前面,可以设置参数dayfrist=True来处理这种情况:
print(parse("6/12/2012",dayfirst = True))
# 2012-12-06 00:00:00
在pandas中也有用于将字符串转化为datetime类型的函数(主要用于处理日期数组),即to_datetime函数:
datestrs = ['2011-07-06 12:00:00','2011-08-06 00:00:00']
res = pd.to_datetime(datestrs)
print(res)
# DatetimeIndex(['2011-07-06 12:00:00', '2011-08-06 00:00:00'], dtype='datetime64[ns]', freq=None)
假设日期数组中含有None或者空的字符串等,则to_datetime方法将会把他转化为NaT(Not a Time):
datestrs = ['2011-07-06 12:00:00','2011-08-06 00:00:00']+[None]
res = pd.to_datetime(datestrs)
print(res)
# DatetimeIndex(['2011-07-06 12:00:00', '2011-08-06 00:00:00', 'NaT'], dtype='datetime64[ns]', freq=None)
时间序列基础
我们可以使用datetime组成的数字来作为Series或者DataFrame的时间戳索引:
import pandas as pd
from datetime import datetime
import numpy as np
dates = [datetime(2011,1,2),datetime(2011,1,3),
datetime(2011,1,4),datetime(2011,1,5),
datetime(2011,1,6),datetime(2011,1,7)]
np.random.seed(1234)
ts = pd.Series(np.random.randn(6),index=dates)
print(ts)
# 2011-01-02 0.471435
# 2011-01-03 -1.190976
# 2011-01-04 1.432707
# 2011-01-05 -0.312652
# 2011-01-06 -0.720589
# 2011-01-07 0.887163
# dtype: float64
此时我们可以使用ts.index来查看索引,其数据类型被转换为了DatetimeIndex,而其中的标量值是pandas中的Timestamp对象:
print(ts.index)
# DatetimeIndex(['2011-01-02', '2011-01-03', '2011-01-04', '2011-01-05',
# '2011-01-06', '2011-01-07'],
# dtype='datetime64[ns]', freq=None)
print(ts.index[0])
# 2011-01-02 00:00:00
索引、选择、子集
当我们选择想要通过索引选择Series对象中的某一条数据时,可以有以下几种方法:
print(ts[2])
print(ts[ts.index[2]])
#使用Timestamp对象访问
print(ts['1/4/2011'])
print(ts['20110104'])
# 1.4327069684260973
对于切片操作,可以通过传入一个年份或者一个年份和一个月份来完成选择当年或者当月的数据:
print(longer_ts.head())
# 2000-01-01 0.471435
# 2000-01-02 -1.190976
# 2000-01-03 1.432707
# 2000-01-04 -0.312652
# 2000-01-05 -0.720589
print(longer_ts['2001'].head())
# 2001-01-01 -1.154601
# 2001-01-02 -1.268069
# 2001-01-03 0.607862
# 2001-01-04 -1.080096
# 2001-01-05 -0.611282
print(longer_ts['2001-05'].head())
# 2001-05-01 -0.766040
# 2001-05-02 0.099882
# 2001-05-03 -0.713024
# 2001-05-04 -2.226118
# 2001-05-05 0.725714
# Freq: D, dtype: float64
也可以使用datetime的类型来进行切片。另外由于时间戳往往是连续的,也允许使用不包含在索引内的时间戳来进行切片:
print(longer_ts[datetime(2001,7,28):].head())
# 2001-07-28 -1.530892
# 2001-07-29 0.801888
# 2001-07-30 -0.424467
# 2001-07-31 1.118855
# 2001-08-01 1.569548
# Freq: D, dtype: float64
print(longer_ts[datetime(2002,1,1):'2004-1-1'][-5:])
# 2002-09-22 0.272164
# 2002-09-23 0.931546
# 2002-09-24 0.327532
# 2002-09-25 0.740814
# 2002-09-26 -0.801905
# Freq: D, dtype: float64
注意,通过切片得到的是原数据的视图,这就意味着当切片得到的数据被更改的时候原数据内容也会被更改。
对于Series的切片方法对于DataFrame同样适用,在此不再赘述。
含有重复索引的时间序列
在有些情况下,数据中可能会含有具有数个相同的时间索引,考虑一下例子:
np.random.seed(1234)
dates = pd.DatetimeIndex(['2011-1-1','2011-1-2','2011-1-2','2011-1-2','2011-1-3'])
dup_ts = pd.Series(np.arange(5),index=dates)
print(dup_ts)
# 2011-01-01 0
# 2011-01-02 1
# 2011-01-02 2
# 2011-01-02 3
# 2011-01-03 4
# dtype: int32
我们可以查看dup_ts.index的is_unique属性来查看是否出现了索引重复的问题,选取如果通过重复的索引来选择数据,则会返回一个Series:
print(dup_ts.index.is_unique)
# False
print(dup_ts['2011-1-1'])
# 0
print(dup_ts['2011-1-2'])
# 2011-01-02 1
# 2011-01-02 2
# 2011-01-02 3
# dtype: int32
对于含有重复的索引的数据,一种处理方法是通过groupby方法来聚合,传递参数level = 0,groupby的聚合具体用法见:《利用python进行数据分析》读书笔记之数据聚合,这里我们通过取平均来完成聚合的操作:
print(dup_ts.groupby(level=0).mean())
# 2011-01-01 0
# 2011-01-02 2
# 2011-01-03 4
# dtype: int32
日期范围、频率和位移
我们可以使用含有时间序列的对象的resample方法将数据从时域转换为时域,以之前的ts对象为例:
print(ts)
# 2011-01-02 0.471435
# 2011-01-03 -1.190976
# 2011-01-04 1.432707
# 2011-01-05 -0.312652
# 2011-01-06 -0.720589
# 2011-01-07 0.887163
# dtype: float64
ts.resample("D")
# "D"代表转换为每日的频率
生成日期范围
首先重新介绍一下pd.date_range函数,该函数可以用来生成指定长度的DatatimeIndex:
index = pd.date_range('2012-04-01','2014-06-01')
print(index)
# 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',
# ...
# '2014-05-23', '2014-05-24', '2014-05-25', '2014-05-26',
# '2014-05-27', '2014-05-28', '2014-05-29', '2014-05-30',
# '2014-05-31', '2014-06-01'],
# dtype='datetime64[ns]', length=792, freq='D')
默认情况下,生成的是以日为单位的连续时间戳,我们也可以只指定一个起始或者结尾日期,但是必须传递一个数字,这样pandas才能确定出时间戳的范围:
index1 = pd.date_range(start = '2012-04-01',periods=20)
print(index1)
# 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')
index2 = pd.date_range(end = '2012-04-01',periods=20)
print(index2)
# DatetimeIndex(['2012-03-13', '2012-03-14', '2012-03-15', '2012-03-16',
# '2012-03-17', '2012-03-18', '2012-03-19', '2012-03-20',
# '2012-03-21', '2012-03-22', '2012-03-23', '2012-03-24',
# '2012-03-25', '2012-03-26', '2012-03-27', '2012-03-28',
# '2012-03-29', '2012-03-30', '2012-03-31', '2012-04-01'],
# dtype='datetime64[ns]', freq='D')
当然我们还可以指定生成时间戳的单位(即频率),比如将“BM”作为频率参数传入时,得到的时间戳是每一个月的最后一天(只有落在范围内的日期被包含在内):
index1 = pd.date_range(start = '2012-04-01',end="2012-12-01",freq="BM")
print(index1)
# DatetimeIndex(['2012-04-30', '2012-05-31', '2012-06-29', '2012-07-31',
# '2012-08-31', '2012-09-28', '2012-10-31', '2012-11-30'],
# dtype='datetime64[ns]', freq='BM')
下表是部分可选时间序列的频率:
频率 | 描述 |
---|---|
D | 日历日的每天 |
B | 工作日的每天 |
H | 每小时 |
T或者min | 每分钟 |
S | 每秒 |
L或ms | 每毫秒 |
U | 每微秒 |
M | 日历日的月底日期 |
BM | 工作日的月底日期 |
MS | 日历日的月初日期 |
BMS | 工作日的月初日期 |
W-MON,W-TUE… | 按照给定的星期日期按照每周取日期(MON,TUE,WED,THU,FRI,SAT,SUN) |
现在假设我们使用date_range生成一个系列日为频率的时间戳,如果给定的起始时间不为0点,则之后生成的时间也不为0点:
index = pd.date_range(start = '2012-04-01 12:56:31',periods=5)
print(index)
# DatetimeIndex(['2012-04-01 12:56:31', '2012-04-02 12:56:31',
# '2012-04-03 12:56:31', '2012-04-04 12:56:31',
# '2012-04-05 12:56:31'],
# dtype='datetime64[ns]', freq='D')
如果我们想要将生成的时间戳全部设为0点,可以使用normalize选项进行实现:
index = pd.date_range(start = '2012-04-01 12:56:31',periods=5,normalize=True)
print(index)
# DatetimeIndex(['2012-04-01', '2012-04-02', '2012-04-03', '2012-04-04',
# '2012-04-05'],
# dtype='datetime64[ns]', freq='D')
频率和日期偏置
学会了date_range函数的基本用法后,又出现了一个问题,假设我们想设置一系列以若干小时为频率的时间戳序列,应该怎么办呢?有两种方法(以1小时30位置为例子):
- 将“90min”或者“1.5H”传给freq
- 将“1H30min”传给freq
index1 = pd.date_range(start = '2012-04-01 12:0000',periods=5,freq="90min")
print(index1)
index2 = pd.date_range(start = '2012-04-01 12:0000',periods=5,freq="1.5H")
print(index2)
index3 = pd.date_range(start = '2012-04-01 12:0000',periods=5,freq="1H30min")
print(index3)
# DatetimeIndex(['2012-04-01 12:00:00', '2012-04-01 13:30:00',
# '2012-04-01 15:00:00', '2012-04-01 16:30:00',
# '2012-04-01 18:00:00'],
# dtype='datetime64[ns]', freq='90T')
移位(向前或者向后)日期
移位是指将日期按照时间向前或者向后移动,类似于几何学上的平移。Series和DataFrame对象都有一个shift方法来完成日期的移位,当传入参数为正表示数据整体向前(日期增加)移动,负整则相反(索引不会变):
np.random.seed(1234)
ts = pd.Series(np.random.randn(5),index=pd.date_range("2011-11-30",periods=5))
print(ts)
# 2011-11-30 0.471435
# 2011-12-01 -1.190976
# 2011-12-02 1.432707
# 2011-12-03 -0.312652
# 2011-12-04 -0.720589
# Freq: D, dtype: float64
print(ts.shift(2))
# 2011-11-30 NaN
# 2011-12-01 NaN
# 2011-12-02 0.471435
# 2011-12-03 -1.190976
# 2011-12-04 1.432707
# Freq: D, dtype: float64
print(ts.shift(-2))
# 2011-11-30 1.432707
# 2011-12-01 -0.312652
# 2011-12-02 -0.720589
# 2011-12-03 NaN
# 2011-12-04 NaN
# Freq: D, dtype: float64
我们可以看到,当进行时间的移位时,会产生缺失值,这是因为原始的数据移走后,后续的位置没有来对其进行顶替导致的。如果我们给freq选项传入相应的频率,则时间序列也会进行移位,此时则不会发生数据的丢失:
print(ts.shift(2,freq="D"))
# 2011-12-02 0.471435
# 2011-12-03 -1.190976
# 2011-12-04 1.432707
# 2011-12-05 -0.312652
# 2011-12-06 -0.720589
# Freq: D, dtype: float64
时区处理
时间的本地化和转换
一般情况下,世界上各个地区的时间都可以表示为UTC(世界协调时)的一个偏执,比如中国的背景就是UTC+8,默认情况下作为索引的时间序列是没有时区的,我们可以使用ts.index的tz属性来查看:
np.random.seed(1234)
rng = pd.date_range("2012-3-27 9:30",periods=6,freq="D")
ts = pd.Series(np.random.randn(6),index=rng)
print(ts)
# 2012-03-27 09:30:00 0.471435
# 2012-03-28 09:30:00 -1.190976
# 2012-03-29 09:30:00 1.432707
# 2012-03-30 09:30:00 -0.312652
# 2012-03-31 09:30:00 -0.720589
# 2012-04-01 09:30:00 0.887163
# Freq: D, dtype: float64
print(ts.index.tz)
# None
此时我们可以使用tz_localize方法设定该时间序列所在的时区:
ts_utc = ts.tz_localize("UTC")
# 将时区设置为UTC
print(ts_utc)
# 2012-03-27 09:30:00+00:00 0.471435
# 2012-03-28 09:30:00+00:00 -1.190976
# 2012-03-29 09:30:00+00:00 1.432707
# 2012-03-30 09:30:00+00:00 -0.312652
# 2012-03-31 09:30:00+00:00 -0.720589
# 2012-04-01 09:30:00+00:00 0.887163
# Freq: D, dtype: float64
print(ts_utc.index.tz)
# UTC
设定了本地时区后,我们就可以使用tz_convert方法轻松的切换时区,比如切换到上海(北京)时间(UTC+8):
print(ts_utc.tz_convert("Asia/Shanghai"))
# 2012-03-27 17:30:00+08:00 0.471435
# 2012-03-28 17:30:00+08:00 -1.190976
# 2012-03-29 17:30:00+08:00 1.432707
# 2012-03-30 17:30:00+08:00 -0.312652
# 2012-03-31 17:30:00+08:00 -0.720589
# 2012-04-01 17:30:00+08:00 0.887163
# Freq: D, dtype: float64
不同时区之前的操作
当两个拥有不同时区的时间戳序列的Series或者DataFrame对象进行联合时,结果时间序列的本地市区会自动转换为UTC:
rng = pd.date_range("2012-3-27 9:30",periods=6,freq="D")
ts = pd.Series(np.random.randn(6),index=rng)
print(ts)
# 2012-03-27 09:30:00 0.471435
# 2012-03-28 09:30:00 -1.190976
# 2012-03-29 09:30:00 1.432707
# 2012-03-30 09:30:00 -0.312652
# 2012-03-31 09:30:00 -0.720589
# 2012-04-01 09:30:00 0.887163
# Freq: D, dtype: float64
ts1 = ts[:7].tz_localize("Europe/London")
ts2 = ts1[2:].tz_convert("Asia/Shanghai")
res = ts1 + ts2
print(res.index)
# DatetimeIndex(['2012-03-27 08:30:00+00:00', '2012-03-28 08:30:00+00:00',
# '2012-03-29 08:30:00+00:00', '2012-03-30 08:30:00+00:00',
# '2012-03-31 08:30:00+00:00', '2012-04-01 08:30:00+00:00'],
# dtype='datetime64[ns, UTC]', freq='D')