第十章 时序数据
一、时序中的基本对象
时间序列的概念在日常生活中很常见,对于时序事件而言,可以从多个时间对象的角度来描述
- Date times,pandas中利用
Timestamp
时间戳,一系列时间戳可以组成DatetimeIndex
,放到Series,变成了datetime64[ns],如果有涉及时区则为datetime64[ns,tz]
,tz:timezone - time delta 时间差,两个timestamp做差可以得到时间差,pandas中利用
Timedelta
来表示,一系列的时间差组成了TimedeltaIndex
,放到Series
变成了timedelta64[ns]
- Time spans时间段,pands中利用
Period
来,一系列组成PeriodIndex,放到Series,Series类型变成Peroid - DateOffsets日期偏置。
二、时间戳
1. Timestamp的构造与属性
#单个时间戳的生成利用pd.Timestamp实现
ts=pd.Timestamp('2020/1/1')
ts=pd.Timestamp('2020-1-1 08:10:30')
ts.year
ts.month
ts.day
ts.hour
ts.minute
ts.second
pandas
,时间戳最小精度为纳秒ns,使用了64位存储
可以表示的时间范围大约可以如下计算:
T
i
m
e
R
a
n
g
e
=
2
64
1
0
9
×
60
×
60
×
24
×
365
≈
585
(
Y
e
a
r
s
)
\rm Time\,Range = \frac{2^{64}}{10^9\times 60\times 60\times 24\times 365} \approx 585 (Years)
TimeRange=109×60×60×24×365264≈585(Years)
通过`pd.Timestamp.max`和`pd.Timestamp.min`可以获取时间戳表示的范围
#pd.Timestamp.max
#pd.Timestamp.min
#pd.Timestamp.max.year-pd.Timestamp.min.year
2. Datetime序列的生成
- 一组时间戳可以组成时间序列
可以用to_datetime
和date_range
生成
to_datetime可以把一列时间戳格式的对象转换成datetime64[ns]类型的时间序列
pd.to_datetime(['2020-1-1','2020-1-3','2020-1-6'])
df=pd.read_csv('../data/learn_pandas.csv')
s=pd.to_datetime(df.Test_Date)
s.head()
- 少数情况,时间戳的格式不满足转换时,可以强制使用format进行匹配:
temp=pd.to_datetime(['2020\\1\\1','2020\\1\\3'],format='%Y\\%m\\%d')
#DatetimeIndex
传入的是列表,而非pandas
内部的Series
返回的是DatetimeIndex
,如果想要转为datetime64[ns]
的序列,需要显式用Series
转化
pd.Series(temp).head()
#datetime64[ns]
- 把表的多列时间属性拼接转为时间序列的
to_datetime
操作,此时的列名必须和以下给定的时间关键词列名一致
df_data_cols=pd.DataFrame({'year':[2020,2020],
'month':[1,1],'day':[1,2],'hour':[10,20],'minute':[30,50],'second':[20,40]})
pd.to_datetime(df_date_cols)
date_range
是一种生成连续间隔时间的一种方法
参数为start, end, freq, periods
,它们分别表示开始时间,结束时间,时间间隔,时间戳个数
pd.date_range('2020-1-1','2020-1-21',freq='10D')
pd.date_range('2020-1-1','2020-2-28',freq='10D')
pd.date_range('2020-1-1','2020-2-28',periods=6)
# 由于结束日期无法取到,freq不为10天
freq参数与Data
【练一练】
Timestamp
上定义了一个value
属性,其返回的整数值代表了从1970年1月1日零点到给定时间戳相差的纳秒数,请利用这个属性构造一个随机生成给定日期区间内日期序列的函数。
def intervaldate(start, end):
begin=pd.Timestamp(start).value
over=pd.Timestamp(end).value
randdatev=random.randint(begin, end)
return pd.Timestamp(randdatev)
【END】
最后,要介绍一种改变序列采样频率的方法asfreq
,它能够根据给定的freq
对序列进行类似于reindex
的操作
s=pd.Series(np.random.rand(5),index=pd.to_datetime(['2020-1-%d'%i for i in range(1,10,2)]))
s.head()
s.asfreq('D').head()
s.asfreq('12H').head()
s,min,h,d
【NOTE】
datetime64[ns]
本质上可以理解为一个大整数,max,min,mean
取得最大时间戳,最小时间戳和平均时间戳
3. dt对象
category,string
序列上定义了cat,str
完成分类数据 和文本数据的操作,时序类型的序列上定义了dt对象完成许多时间序列的相关操作
- 取出时间相关的属性
- 判断时间戳是否满足条件
- 取整操作
第一类操作的常用属性包括:date, time, year, month, day, hour, minute, second, microsecond, nanosecond, dayofweek, dayofyear, weekofyear, daysinmonth, quarter
,其中daysinmonth, quarter
分别表示该月一共有几天和季度。
第二类判断操作主要用于测试是否为月/季/年的第一天或者最后一天:
第三类的取整操作包含round, ceil, floor
,它们的公共参数为freq
,常用的包括H, min, S
(小时、分钟、秒),所有可选的freq
可参考此处。
s = pd.Series(pd.date_range('2020-1-1','2020-1-3', freq='D'))
s.dt.date
s.dt.time
s.dt.day
s.dt.daysinmonth
s.dt.dayofweek
s.dt.month_name()#返回英文名
s.dt.day_name()
#第二类操作
s.dt.is_year_start
# 还可选 is_quarter/month_start
s.dt.is_year_end
# 还可选 is_quarter/month_end
#取整
s = pd.Series(pd.date_range('2020-1-1 20:35:00', '2020-1-1 22:35:00', freq='45min'))
s.dt.round('1H')
s.dt.ceil('1H')
s.dt.floor('1H')
4. 时间戳的切片与索引
第一类方法是利用dt
对象和布尔条件联合使用
另一种方式是利用切片,后者常用于连续时间戳
s=pd.Series(np.random.randint(2,size=366),index=pd.date_range('2020-01-01','2020-12-31'))
#2020有366天
idx=pd.Series(s.index).dt
s.head()
#Example1:每月的第一天或者最后一天
s[(idx.is_month_start|idx.is_month_end).values]
#Example2:双休日
s[idx.dayofweek.isin([5,6]).values].head()
dayofweek,数组下标从0(星期一)开始
#Example3:取出单日值
s['2020-01-01']
s['20200101']
#自动转换标准格式
#Example4:取出七月
s['2020-07'].head()
#Example5:取出5月初至7月15日
s['2020-05':'2020-7-15'].head()
s['2020-05':'2020-7-15'].tail()
三、时间差
1. Timedelta的生成
时间差可以理解为两个时间戳的差,这里也可以通过pd.Timedelta
来构造:
生成时间差序列的主要方式是pd.to_timedelta
,其类型为timedelta64[ns]
:
直接生成
#两个timestamp相减
pd.Timestamp('20200102 08:00:00')-pd.Timestamp('20200101 07:35:00')
#构造参数设置
pd.Timedelta(days=1, minutes=25) # 需要注意加s
#字符串构造生成
pd.Timedelta('1 days 25 minutes')
# dataframe转换
s=pd.to_timedelta(df.Time_Record)
# 时间差序列
pd.timedelta_range('0s', '1000s', freq='6min')
pd.timedelta_range('0s', '1000s', periods=3)
Timedelta序列定义了dt对象
属性包括days, seconds, mircroseconds, nanoseconds
seconds不是指单纯的秒,而是对天数取余后剩余的秒数:
s.dt.seconds.head()
#天数取余得到秒数
s.dt.total_seconds().head()
#直接对应秒数
pd.to_timedelta(df.Time_Record).dt.round('min').head()
#对分进行取整
2. Timedelta的运算
时间差支持的常用运算有三类:与标量的乘法运算、与时间戳的加减法运算、与时间差的加减法与除法运算:
td1 = pd.Timedelta(days=1)
td2 = pd.Timedelta(days=3)
ts = pd.Timestamp('20200101')
td1 * 2
td1 = pd.timedelta_range(start='1 days', periods=5)
td2 = pd.timedelta_range(start='12 hours', freq='2H', periods=5)
ts = pd.date_range('20200101', '20200105')
四、日期偏置
1. Offset对象
当使用`+`时获取离其最近的下一个日期,当使用`-`时获取离其最近的上一个日期:
pd.Timestamp('20200831') + pd.offset.WeekOfMonth(week=0,weekday=0)
#查找求2020年9月第一个周一的日期
pd.Timestamp('20200907') + pd.offsets.BDay(30)
#求2020年9月7日后的第30个工作日是哪一天
pd.Timestamp('20200831') - pd.offsets.WeekOfMonth(week=0,weekday=0)
pd.Timestamp('20200907') - pd.offsets.BDay(30)
pd.Timestamp('20200907') + pd.offsets.MonthEnd()
特殊的Offset对象CDay
,其中的holidays, weekmask
参数能够分别对自定义的日期和星期进行过滤
前者传入了需要过滤的日期列表,后者传入的是三个字母的星期缩写构成的星期字符串
只保留字符串中出现的星期
my_filter = pd.offsets.CDay(n=1,weekmask='Wed Fri',holidays=['20200109'])
dr = pd.date_range('20200108', '20200111')
dr.to_series().dt.dayofweek
[i + my_filter for i in dr]
上面的例子中,n
表示增加一天CDay
,dr
中的第一天为20200108
,但由于下一天20200109
被排除了,并且20200110
是合法的周五,因此转为20200110
,其他后面的日期处理类似。
【CAUTION】不要使用部分Offset
在当前版本下由于一些 bug
,不要使用 Day
级别以下的 Offset
对象,比如 Hour, Second
等,请使用对应的 Timedelta
对象来代替。
2. 偏置字符串
前面提到了关于date_range
的freq
取值可用Offset
对象,同时在pandas
中几乎每一个Offset
对象绑定了日期偏置字符串(frequencies strings/offset aliases
),可以指定Offset
对应的字符串来替代使用。下面举一些常见的例子。
pd.date_range('20200101','20200331',freq='MS')#月初
pd.date_range('20200101','20200331', freq=pd.offsets.MonthBegin())
pd.date_range('20200101','20200331', freq='M') # 月末
pd.date_range('20200101','20200331', freq=pd.offsets.MonthEnd())
pd.date_range('20200101','20200110', freq='B') # 工作日
pd.date_range('20200101','20200110', freq=pd.offsets.BDay())
pd.date_range('20200101','20200201', freq='W-MON') # 周一
pd.date_range('20200101','20200201', freq=pd.offsets.CDay(weekmask='Mon'))
pd.date_range('20200101','20200201', freq='WOM-1MON') # 每月第一个周一
pd.date_range('20200101','20200201', freq=pd.offsets.WeekOfMonth(week=0,weekday=0))
各类时间对象的开发,除了使用python
内置的datetime
模块,pandas
还利用了dateutil
模块我国是没有夏令时调整时间一说的,但有些国家会有这种做法,导致了相对而言一天里可能会有23/24/25个小时,也就是relativedelta
,这使得Offset
对象和Timedelta
对象有了对同一问题处理产生不同结果的现象,其中的规则也较为复杂,官方文档的写法存在部分描述错误,并且难以对描述做出统一修正,因为牵涉到了Offset
相关的很多组件。因此,本教程完全不考虑时区处理
五、时序中的滑窗与分组
1. 滑动窗口
所谓时序的滑窗函数,即把滑动窗口用freq
关键词代替,下面给出一个具体的应用案例:在股票市场中有一个指标为BOLL
指标,它由中轨线、上轨线、下轨线这三根线构成,具体的计算方法分别是N
日均值线、N
日均值加两倍N
日标准差线、N
日均值减两倍N
日标准差线。利用rolling
对象计算N=30
的BOLL
指标可以如下写出:
import matplotlib.pyplot as plt
idx = pd.date_range('20200101', '20201231', freq='B')
np.random.seed(2020)
data = np.random.randint(-1,2,len(idx)).cumsum() # 随机游动构造模拟序列
s = pd.Series(data,index=idx)
s.head()
import pandas as pd
amount = pd.Series([100, 90, 110, 150, 110, 130, 80, 90, 100, 150])
print(amount. Rolling(3).sum())
import matplotlib.pyplot as plt
idx = pd.date_range('20200101', '20201231', freq='B')
np.random.seed(2020)
data = np.random.randint(-1,2,len(idx)).cumsum() # 随机游动构造模拟序列
s = pd.Series(data, index=idx)
r = s.rolling('30D')#每月的计算
plt.plot(s)
plt.title('BOLL LINES')
plt.plot(r.mean())
plt.plot(r.mean()+r.std()*2)
plt.plot(r.mean()-r.std()*2)
s.shift(freq='50D').head()
#shift50天移动
my_series = pd.Series(s.index)
my_series.head()
my_series.diff(1).head()
#进行微分之后得到序列
2. 重采样
重采样对象resample
和第四章中分组对象groupby
的用法类似,前者是针对时间序列的分组计算而设计的分组对象。
对上面的序列计算每10天的均值:
s.resample('10D').mean().head()
s.resample('10D').apply(lambda x:x.max()-x.min()).head() # 极差
在resample
中要特别注意组边界值的处理情况,默认情况下起始值的计算方法是从最小值时间戳对应日期的午夜00:00:00
开始增加freq
,直到不超过该最小时间戳的最大时间戳,由此对应的时间戳为起始值,然后每次累加freq
参数作为分割结点进行分组,区间情况为左闭右开。下面构造一个不均匀的例子:
idx = pd.date_range('20200101 8:26:35', '20200101 9:31:58', freq='77s')
data = np.random.randint(-1,2,len(idx)).cumsum()
s = pd.Series(data,index=idx)
s.head()
下面对应的第一个组起始值为08:24:00
,其是从当天0点增加72个freq=7 min
得到的,如果再增加一个freq
则超出了序列的最小时间戳08:26:35
:
s.resample('7min').mean().head()
从序列的最小时间戳开始依次增加freq进行分组,此时可以指定origin参数为start
s.resample('7min', origin='start').mean().head()
在返回值中,要注意索引一般是取组的第一个时间戳,但M, A, Q, BM, BA, BQ, W
这七个是取对应区间的最后一个时间戳:
s = pd.Series(np.random.randint(2,size=366), index=pd.date_range('2020-01-01', '2020-12-31'))
s.resample('M').mean().head()
s.resample('MS').mean().head() # 结果一样,但索引不同
指定label向后偏移
series.resample('W',label='left').sum()
series.resample('W',closed='left').sum()