来源:http://inter.joyfulpandas.datawhale.club/build/html/%E7%9B%AE%E5%BD%95/ch10.html
一、时序中的基本对象
概念 | 单元素类型 | 数组类型 | pandas数据类型 |
---|---|---|---|
Date times | Timestamp |
DatetimeIndex |
datetime64[ns] |
Time deltas | Timedelta |
TimedeltaIndex |
timedelta64[ns] |
Time spans | Period |
PeriodIndex |
period[freq] |
Date offsets | DateOffset |
None |
None |
二、时间戳
1. Timestamp的构造与属性pd.Timestamp
单个时间戳的生成: 一般的时间格式都支持
ts = pd.Timestamp('2020-1-1 08:10:30')
ts = pd.Timestamp('2020/1/1')
#通过 year, month, day, hour, min, second 可以获取具体的数值
In [7]: ts.year
Out[7]: 2020
In [8]: ts.month
Out[8]: 1
In [9]: ts.day
Out[9]: 1
In [10]: ts.hour
Out[10]: 8
In [11]: ts.minute
Out[11]: 10
In [12]: ts.second
Out[12]: 30
#通过 pd.Timestamp.max 和 pd.Timestamp.min 可以获取时间戳表示的范围
In [13]: pd.Timestamp.max
Out[13]: Timestamp('2262-04-11 23:47:16.854775807')
In [14]: pd.Timestamp.min
Out[14]: Timestamp('1677-09-21 00:12:43.145225')
In [15]: pd.Timestamp.max.year - pd.Timestamp.min.year
Out[15]: 585
2. Datetime序列的生成to_datetime和date_range
to_datetime :
能够把一列时间戳格式的对象转换成为 datetime64[ns]
类型的时间序列
1)传入列表:生成 DatetimeIndex 序列
temp = pd.to_datetime(['2020-1-1', '2020-1-3', '2020-1-6'])
如果想要转为 datetime64[ns]
的序列,需要显式用 Series
转化:
pd.Series(temp).head()
2)传入series:生成datetime64类型的序列
s = pd.to_datetime(df.Test_Date)
当时间戳格式不满足转换时,可以强制使用format进行匹配:
temp = pd.to_datetime(['2020\\1\\1','2020\\1\\3'],format='%Y\\%m\\%d')
'''
DatetimeIndex(['2020-01-01', '2020-01-03'], dtype='datetime64[ns]', freq=None)
'''
3)把一个表的多列时间属性拼接转为时间序列,此时列名必须和以下给定的时间关键词列名一致:
df_date_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)
'''
0 2020-01-01 10:30:20
1 2020-01-02 20:50:40
dtype: datetime64[ns]
'''
date_range
是一种生成连续间隔时间的一种方法,4个参数:start,end,freq,periods,分别表示开始时间,结束时间,时间间隔,时间戳个数。同样只要决定三个参数即可确定。这里要注意,开始或结束日期如果作为端点则它会被包含:
In [25]: pd.date_range('2020-1-1','2020-1-21', freq='10D') # 包含
Out[25]: DatetimeIndex(['2020-01-01', '2020-01-11', '2020-01-21'], dtype='datetime64[ns]', freq='10D')
In [26]: pd.date_range('2020-1-1','2020-2-28', freq='10D')
Out[26]:
DatetimeIndex(['2020-01-01', '2020-01-11', '2020-01-21', '2020-01-31',
'2020-02-10', '2020-02-20'],
dtype='datetime64[ns]', freq='10D')
In [27]: pd.date_range('2020-1-1',
'2020-2-28', periods=6) # 由于结束日期无法取到,freq不为10天
Out[27]:
DatetimeIndex(['2020-01-01 00:00:00', '2020-01-12 14:24:00',
'2020-01-24 04:48:00', '2020-02-04 19:12:00',
'2020-02-16 09:36:00', '2020-02-28 00:00:00'],
dtype='datetime64[ns]', freq=None)
3.改变时间序列的index(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()
'''
2020-01-01 0.393911
2020-01-03 0.703650
2020-01-05 0.650046
2020-01-07 0.726203
2020-01-09 0.126783
dtype: float64
'''
#按天数index
s.asfreq('D').head()
'''
2020-01-01 0.836578
2020-01-02 NaN
2020-01-03 0.678419
2020-01-04 NaN
2020-01-05 0.711897
Freq: D, dtype: float64
'''
#每12小时index一次
s.asfreq('12H').head()
'''
2020-01-01 00:00:00 0.836578
2020-01-01 12:00:00 NaN
2020-01-02 00:00:00 NaN
2020-01-02 12:00:00 NaN
2020-01-03 00:00:00 0.678419
Freq: 12H, dtype: float64
'''
练一练
Timestamp
上定义了一个value
属性,其返回的整数值代表了从1970年1月1日零点到给定时间戳相差的纳秒数,请利用这个属性构造一个随机生成给定日期区间内日期序列的函数。
import random
def randomDateTimeSeries(time1str, time2str, n):
time1 = pd.Timestamp(time1str)
time2 = pd.Timestamp(time2str)
return pd.Series(pd.to_datetime([pd.Timestamp(random.randrange(time1.value,time2.value)) for i in range(n)]))
randomDateTimeSeries('2019-1-2', '2019-2-3',3)
'''
0 2019-02-01 11:57:10.618481694
1 2019-01-20 01:09:36.476473128
2 2019-01-27 07:59:37.138227967
dtype: datetime64[ns]
'''
4. dt对象
三类操作:
取出时间相关的属性,
date, time, year, month, day, hour, minute, second,microsecond, nanosecond, dayofweek, dayofyear, weekofyear, daysinmonth, quarter
,其中daysinmonth, quarter
分别表示月中的第几天和季度,month_name(), day_name()
返回英文的月名和星期名判断时间戳是否满足条件,主要用于测试是否为月/季/年的第一天或者最后一天: s.dt.is_year_start, s.dt.is_year_end, is_quarter/month_start, is_quarter/month_end
取整操作包含
round, ceil, floor
,它们的公共参数为freq
,常用的包括H, min, S
(小时、分钟、秒)
#原列表
In [42]: s = pd.Series(pd.date_range('2020-1-1 20:35:00',
....: '2020-1-1 22:35:00',
....: freq='45min'))
....:
In [43]: s
Out[43]:
0 2020-01-01 20:35:00
1 2020-01-01 21:20:00
2 2020-01-01 22:05:00
dtype: datetime64[ns]
#四舍五入
In [44]: s.dt.round('1H')
Out[44]:
0 2020-01-01 21:00:00
1 2020-01-01 21:00:00
2 2020-01-01 22:00:00
dtype: datetime64[ns]
#向上取整
In [45]: s.dt.ceil('1H')
Out[45]:
0 2020-01-01 21:00:00
1 2020-01-01 22:00:00
2 2020-01-01 23:00:00
dtype: datetime64[ns]
#向下取整
In [46]: s.dt.floor('1H')
Out[46]:
0 2020-01-01 20:00:00
1 2020-01-01 21:00:00
2 2020-01-01 22:00:00
dtype: datetime64[ns]
5. 时间戳的切片与索引
一般而言,时间戳序列作为索引使用。如果想要选出某个子时间戳序列,第一类方法是利用dt
对象和布尔条件联合使用,另一种方式是利用切片,后者常用于连续时间戳。下面,举一些例子说明:
#原时间序列
s = pd.Series(np.random.randint(2,size=366), index=pd.date_range('2020-01-01','2020-12-31'))
idx = pd.Series(s.index).dt
s.head()
'''
2020-01-01 1
2020-01-02 1
2020-01-03 0
2020-01-04 1
2020-01-05 0
Freq: D, dtype: int32
'''
第一类方法:利用dt对象和布尔条件
#Example1:每月的第一天或者最后一天
s[(idx.is_month_start|idx.is_month_end).values].head()
'''
2020-01-01 1
2020-01-31 0
2020-02-01 1
2020-02-29 1
2020-03-01 0
dtype: int32
'''
#Example2:双休日
s[idx.dayofweek.isin([5,6]).values].head()
'''
2020-01-04 1
2020-01-05 0
2020-01-11 0
2020-01-12 1
2020-01-18 1
dtype: int32
'''
第二类:切片,常用于连续时间戳
#Example3:取出单日值
s['2020-01-01']
'''
1
'''
#Example4:取出七月
s['2020-07'].head()
'''
2020-07-01 0
2020-07-02 1
2020-07-03 0
2020-07-04 0
2020-07-05 0
Freq: D, dtype: int32
'''
#Example5:取出5月初至7月15日
In [55]: s['2020-05':'2020-7-15'].head()
Out[55]:
2020-05-01 0
2020-05-02 1
2020-05-03 0
2020-05-04 1
2020-05-05 1
Freq: D, dtype: int32
In [56]: s['2020-05':'2020-7-15'].tail()
Out[56]:
2020-07-11 0
2020-07-12 0
2020-07-13 1
2020-07-14 0
2020-07-15 1
Freq: D, dtype: int32
三、时间差
1. Timedelta的生成
3种方法生成时间差:
- pd.Timestamp('')-pd.Timestamp('')
- pd.Timedelta(days=1, minutes=25) # 需要注意加s
- pd.Timedelta('') # 字符串生成
生成时间差序列,类型是timedelta64[ns]:
- pd.(df.Time_Record)
- pd.('0s', '1000s', freq='6min'),类似date_range
时间差序列,也有dt对象,属性有:
- s.dt.days
- s.dt.seconds,对天数取余后的秒数
- s.dt.microseconds
- s.dt.total_seconds
- s.dt.nanoseconds
也可以在dt对象上继续使用取整函数:
pd.to_timedelta(df.Time_Recond).dt.round('min')
2. Timedelta的运算
1) 与标量的乘法运算
td1 = pd.Timedelta(days=1)
td2 = pd.Timedelta(days=3)
ts = pd.Timestamp('20200101')
td1 * 2
2) 与时间戳的加减法运算
ts + td1
ts - td1
3) 与时间差的加减法与除法运算
td2 - td1
这些运算都可以移植到时间差的序列上:
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')
td1 * 5
td1 * pd.Series(list(range(5))) # 逐个相乘
'''
0 0 days
1 2 days
2 6 days
3 12 days
4 20 days
dtype: timedelta64[ns]
'''
td1 - td2
'''
TimedeltaIndex(['0 days 12:00:00', '1 days 10:00:00', '2 days 08:00:00',
'3 days 06:00:00', '4 days 04:00:00'],
dtype='timedelta64[ns]', freq=None)
'''
td1 + pd.Timestamp('20200101')
'''
DatetimeIndex(['2020-01-02', '2020-01-03', '2020-01-04', '2020-01-05',
'2020-01-06'],
dtype='datetime64[ns]', freq='D')
'''
td1 + ts # 逐个相加
'''
DatetimeIndex(['2020-01-02', '2020-01-04', '2020-01-06', '2020-01-08',
'2020-01-10'],
dtype='datetime64[ns]', freq=None)
'''
四、日期偏置
1. Offset对象
是一种和日历相关的特殊时间差
#求2020年9月第一个周一的日期:
pd.Timestamp('20200831') + pd.offsets.WeekOfMonth(week=0,weekday=0)
'''
Timestamp('2020-09-07 00:00:00')
'''
#求2020年9月7日后的第30个工作日是哪一天。
pd.Timestamp('20200907') + pd.offsets.BDay(30)
'''
Timestamp('2020-10-19 00:00:00')
'''
#使用-时,获取向前的日期
pd.Timestamp('20200831') - pd.offsets.WeekOfMonth(week=0,weekday=0)
'''
Timestamp('2020-08-03 00:00:00')
'''
pd.Timestamp('20200907') - pd.offsets.BDay(30)
'''
Timestamp('2020-07-27 00:00:00')
'''
pd.Timestamp('20200907') + pd.offsets.MonthEnd()
'''
Timestamp('2020-09-30 00:00:00')
'''
特殊的Offset对象CDay,参数:
- holiday: 跳过这个日期往后找符合的星期
- weekmask: 保留字符串中出现的星期
- n: 往后加几天
my_filter= pd.offsets.CDay(n=1, weekmask='Wed Fri', holidays=['20200109'])
dr = pd.date_range('20200108', '20200111')
dr.to_series().dt.dayofweek
'''
my_filter = pd.offsets.CDay(n=1,weekmask='Wed Fri',holidays=['20200109'])
dr = pd.date_range('20200108', '20200111')
dr.to_series().dt.dayofweek
2020-01-08 2
2020-01-09 3
2020-01-10 4
2020-01-11 5
Freq: D, dtype: int64
'''
[i + my_filter for i in dr]
'''
[Timestamp('2020-01-10 00:00:00'), 1-8后一天是1-9,跳过,后一天是1-10,是周五,所以改为1-10
Timestamp('2020-01-10 00:00:00'),1-9后一天是1-10周五,保留
Timestamp('2020-01-15 00:00:00'),1-10后一天是1-11向后找,1-15是周三
Timestamp('2020-01-15 00:00:00')]同上
'''
2. 偏置字符串
date_range的freq取值可用Offset对象,同时也可以用Offset对应的字符串替代:
pd.date_range('20200101', '20200331', freq='MS') #月初
pd.date_range('20200101','20200331', freq=pd.offsets.MonthBegin()) #效果同上
'''
DatetimeIndex(['2020-01-01', '2020-02-01', '2020-03-01'], dtype='datetime64[ns]', freq='MS')
'''
类似的还有:
’M' | pd.offsets.MonthEnd() | 月末 |
---|---|---|
'B' | pd.offsets.BDay() | 工作日 |
'W-MON' | pd.offsets.CDay() | 周一 |
'WOM-1MON' | pd.offsets.WeekOfMonth(week, weekday=0) | 每月第一个周一 |
五、时序中的滑窗与分组
1. 滑动窗口
时序的滑窗函数,即把滑动窗口用freq关键词代替。
r = s.rolling('30D')
s.shift(freq = '50D')
my_series.diff(1).head() 获得时间间隔序列
2. 重采样
类似groupby的用法,针对时间序列进行分组计算。
#计算每10天的均值:
s.resample('10D').mean()
'''
2020-01-01 -2.000000
2020-01-11 -3.166667
2020-01-21 -3.625000
2020-01-31 -4.000000
2020-02-10 -0.375000
Freq: 10D, dtype: float64
'''
#apply
s.resample('10D').apply(lambda x:x.max()-x.min()).head() # 极差
'''
2020-01-01 3
2020-01-11 4
2020-01-21 4
2020-01-31 2
2020-02-10 4
Freq: 10D, dtype: int32
'''
resample的边界情况:
- 默认
是从最小时间戳对应日期的午夜00:00:00开始增加freq,直到不超过该最小时间戳的最大时间戳。
例如某时间序列的第一个时间戳是2020-01-01 8:26:35,resample('7min'),那么会从该天的00:00:00开始增加7min*n,且7min*n小于等于8:26:35,即得到8:24
- 指定origin
从序列的最小时间戳开始依次增加freq进行分组,可以指定origin=start
s.resample('7min', origin='start').mean()
- 在返回值中,要注意索引一般是取组的第一个时间戳,但
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()
'''
2020-01-31 0.451613 月末为索引
2020-02-29 0.448276
2020-03-31 0.516129
2020-04-30 0.566667
2020-05-31 0.451613
Freq: M, dtype: float64
'''
s.resample('MS').mean().head() # 结果一样,但索引不同
'''
2020-01-01 0.451613 月初为索引
2020-02-01 0.448276
2020-03-01 0.516129
2020-04-01 0.566667
2020-05-01 0.451613
Freq: MS, dtype: float64
'''
六、练习
Ex1:太阳辐射数据集
现有一份关于太阳辐射的数据集:
1.将Datetime, Time
合并为一个时间列Datetime
,同时把它作为索引后排序。
df['Datetime'] = pd.to_datetime(df.Data.str.split(' ').str[0] + " " + df.Time)
df = df.drop(columns=['Data', 'Time'])
df.set_index('Datetime').sort_index()
'''
Radiation Temperature
Datetime
2016-09-01 00:00:08 2.58 51
2016-09-01 00:05:10 2.83 51
2016-09-01 00:20:06 2.16 51
2016-09-01 00:25:05 2.21 51
2016-09-01 00:30:09 2.25 51
'''
- 每条记录时间的间隔显然并不一致,请解决如下问题:
- 找出间隔时间的前三个最大值所对应的三组时间戳。
- 是否存在一个大致的范围,使得绝大多数的间隔时间都落在这个区间中?如果存在,请对此范围内的样本间隔秒数画出柱状图,设置
bins=50
。
#找出间隔时间的前三个最大值所对应的三组时间戳。
#1.求diff并转成total_seconds模式
s = df.index.to_series().reset_index(drop=True).diff().dt.total_seconds()
#2.选出前3个最大值,并获取它们的index列表(Int64Index类型)
max_3 = s.nlargest(3).index
#3.联合这个index的前一个index,并获取值
df.index[max_3.union(max_3-1)]
#是否存在一个大致的范围,使得绝大多数的间隔时间都落在这个区间中?如果存在,请对此范围内的样本间隔秒数画出柱状图,设置`bins=50`。
#把分位数大于0.99的和分位数小于0.01的都设置为NaN,然后画图
res = s.mask((s>s.quantile(0.99))|(s
- 求如下指标对应的
Series
:
- 温度与辐射量的6小时滑动相关系数
- 以三点、九点、十五点、二十一点为分割,该观测所在时间区间的温度均值序列
- 每个观测6小时前的辐射量(一般而言不会恰好取到,此时取最近时间戳对应的辐射量)
#1.温度与辐射量的6小时滑动相关系数
#注意:使用rolling时,设置一个dateTime列为index,然后指明另一个列为rolling对象
df = df.set_index('Datetime').sort_index()
df.Radiation.rolling('6H').corr(df.Temperature)
#2. 分割时区,选择resample
res = df.Temperature.resample('6H', origin='03:00:00').mean()
#3. 滑动窗口或日期偏置,get_loc,method='nearest'
my_dt = df.index.shift(freq='-6H') #或者: df.index-pd.offsets.Hour(6)
int_loc = [df.index.get_loc(i, method='nearest') for i in my_dt]
res = df.Radiation.iloc[int_loc]
Ex2:水果销量数据集
现有一份2019年每日水果销量记录表:
Date Fruit Sale
0 2019-04-18 Peach 15
1 2019-12-29 Peach 15
2 2019-06-05 Peach 19
- 统计如下指标:
-
每月上半月(15号及之前)与下半月葡萄销量的比值
可以通过自定义的方式进行分组,并命名np.where(df_grape.Date.dt.day<=15,'First', 'Second')
df.Date = pd.to_datetime(df.Date) df_grape = df.query("Fruit == 'Grape'") #可以通过自定义的方式进行分组,并命名np.where(df_grape.Date.dt.day<=15,'First', 'Second') res = df_grape.groupby([np.where(df_grape.Date.dt.day<=15,'First', 'Second'),df_grape.Date.dt.month])['Sale'].mean().to_frame().unstack(0).droplevel(0,axis=1) res = (res.First/res.Second).rename_axis('Month')
每月最后一天的生梨销量总和
获取月最后一天可以直接使用[df.Date.dt.is_month_end]索引,不需要定义索引
df.query('Fruit == "Pear"')[df.Date.dt.is_month_end].groupby('Date').sum()
-
每月最后一天工作日的生梨销量总和
BM: 工作日最后一天
构造一个date序列,查看它是否在这个序列里
df[df.Date.isin(pd.date_range('20190101', '20191231',freq='BM'))].query("Fruit == 'Pear'").groupby('Date').Sale.mean().head()
- 每月最后五天的苹果销量均值
groupby后可以跟nlargest(5) 选出每个组前5大的索引
#筛选出所有月最后五天的日期
target_dt = df.drop_duplicates().groupby(df.Date.drop_duplicates().dt.month)['Date'].nlargest(5).reset_index(drop=True)
#根据日期找到相关的记录
res = df.set_index('Date').loc[target_dt].reset_index().query("Fruit == 'Apple'")
#按月份计算均值
res = res.groupby(res.Date.dt.month)['Sale'].mean().rename_axis('Month')
res.head()
- 按月计算周一至周日各品种水果的平均记录条数,行索引外层为水果名称,内层为月份,列索引为星期。
复习:groupby里的分类标准是字符串列表或者序列,可以直接自己定义;category可以快速自定义顺序并排序
month_order = ['January','February','March','April','May','June','July','August','September','October','November','December']
week_order = ['Mon','Tue','Wed','Thu','Fri','Sat','Sum']
group1 = df.Date.dt.month_name().astype('category').cat.reorder_categories(month_order, ordered=True)
group2 = df.Fruit
group3 = df.Date.dt.dayofweek.replace(dict(zip(range(7),week_order))).astype('category').cat.reorder_categories(week_order, ordered=True)
res = df.groupby([group1, group2,group3])['Sale'].count().to_frame().unstack(0).droplevel(0,axis=1)
res.head()
- 按天计算向前10个工作日窗口的苹果销量均值序列,非工作日的值用上一个工作日的结果填充。
#获取苹果工作日的销量记录
df_apple = df[(df.Fruit=='Apple')&(~df.Date.dt.dayofweek.isin([5,6]))]
#构造一个序列,填充苹果工作日的销量记录,设置index为date,按Date分组,并计算组和
s = pd.Series(df_apple.Sale.values,index=df_apple.Date).groupby('Date').sum()
#重新设置index
res = s.rolling('10D').mean().reindex(pd.date_range('20190101','20191231')).fillna(method='ffill')