pandas 时序统计的高级用法!
本次介绍pandas时间统计分析的一个高级用法--重采样。
重采样指的是时间重采样,就是将时间序列从一个频率转换到另一个频率上,对应数据也跟着频率进行变化。比如时间序列数据是以天为周期的,通过重采样我们可以将其转换为按分钟、小时、周、月、季度等等的其他周期上。根据转换的频率精度可分为向上采样和向下采样。
向上采样:转换到更细颗粒度的频率,比如将天转为小时、分钟、秒等 向下采样:转换到更粗颗粒度的频率,比如将天转为周、月、季度、年等
resample用法
pandas中时间重采样的方法是resample()
,可以对series和dataframe对象操作。由于重采样默认对索引执行变换,因此索引必须是时间类型,或者通过on指定要重采样的时间类型的column列。
用法:
pandas.DataFrame.resample()
pandas.Series.resample()
------
返回:Resampler对象
参数:
rule:定义重采样的规则,DateOffset,Timedelta或str类型,当为str类型时,其参数及含义如下表所示 axis:指定轴方向,str类型,默认为0 0
:代表索引1
:代表列closed:指定时间频率分组的左右闭合状态,默认 M
,A
,Q
,BM
,BA
,BQ
,W
右闭合,其余均是左闭合left
:指定左闭合right
:指定右闭合label:指定左或右边界作为分组标签,默认 M
,A
,Q
,BM
,BA
,BQ
,W
以右边界为分组标签,其余均是以左边界为分组标签left
:以左边界为分组标签right
:以右边界为分组标签kind:将结果索引转化为指定的时间类型 timestamp
:将结果索引转换为DateTimeIndex
period
:将结果索引转换为PeriodIndex
on:对于dataframe,指定被重采样的列,且列必须是时间类型 level:对于多级索引,指定要被重采样的索引层级,int或str类型。 int
:索引层级str
:索引层级名称origin:调整时间分组的起点。Timestamp或str类型,当为str时: epoch
:1970-01-01start
:时间序列的第一个值start_day
:时间序列第一天的午夜end
:时间序列的最后一个值end_day
:最后一天的午夜offset:对origin添加的偏移量,Timedelta或str类型 group_keys:指定是否在结果索引包含分组keys,当采样对象使用了 .apply()
方法,默认False不包含
举例:
1)指定列名
resample
默认只对索引对象操作,换句话说,默认情况下索引必须是时间类型的数据,否则执行会报错。
对于dataframe而言,如不想对索引重采样,可以通过on
参数选择一个column列代替索引进行重采样操作。
# 将时间类型索引重置,变为column列
df.reset_index(drop=False,inplace=True)
# 通过参数on指定时间类型的列名,也可以实现重采样
df.resample('W', on='index')['C_0'].sum().head()
由于W
是默认为右闭且取右边界作为分组标签的,重采样后结果如下。从1/3至1/9(绿色)是完整一周,因此之前非完整部分(黄色)自动归为一周,后面依次按周统计。
2)开闭区间指定
通过closed
参数可以控制左右闭合的状态。
默认情况下,M
,A
,Q
,BM
,BA
,BQ
,W
是右闭合,其余频率均是左闭合。
下面将天频率转为W
周频率(默认是右闭)。我们手动设置左、右闭合进行对比,可以看出二者区别,对于求和结果的影响。
df=generate_sample_data_datetime()
pd.concat([df.resample('W', closed='left')['C_0'].sum().to_frame(name='left_clsd'),
df.resample('W', closed='right')['C_0'].sum().to_frame(name='right_clsd')],
axis=1).head(5)
3)输出结果控制
通过label
参数可以控制输出结果的标签。
默认情况下,M
,A
,Q
,BM
,BA
,BQ
,W
以分组内右侧边界为输出的标签,其余均是以分组内左边界为标签。
下面将天频率转为W
周频率(label默认右边界)。我们手动设置label为左、右进行对比,可以看出第二个采样分组下输出标签的区别。
df=generate_sample_data_datetime()
df.resample('W', label='left')['C_0'].sum().to_frame(name='left_bnd').head(5)
df.resample('W', label='right')['C_0'].sum().to_frame(name='right_bnd').head(5)
4)聚合统计
类似于groupby
和窗口的聚合方法, 重采样也适用相关方法,参考pandas分组8个常用技巧!
以下是resample
采样后可以支持的描述性统计和计算的内置函数。
内置方法下面例子中会举例说明。
上采样
分为上采样和下采样。通过以下数据举例说明。
# 生成时间索引的数据
def generate_sample_data_datetime():
np.random.seed(123)
number_or_rows = 365*2
num_cols = 5
start_date = '2022-01-01'
cols = ["C_0", "C_1", "C_2", "C_3", "C_4"]
df = pd.DataFrame(np.random.randint(1, 100, size = (number_or_rows, num_cols)), columns=cols)
df.index = pd.date_range(start=start_date, periods=number_or_rows)
return df
df=generate_sample_data_datetime()
以上生成数据时间索引是以天为频率的。
根据rule参数含义码表,H
代表小时的意思,12H
也就是12小时。这是resample非常强大的地方,可以把采样定位的非常精确。
下面将天的时间频率转换为12小时的频率,并对新的频率分组后求和。
df.resample('12H')['C_0'].sum().head(10)
比天颗粒度更小的还可以有分钟、秒、毫秒、微秒、纳秒,可根据实际情况自行设定频率大小。
以上可以看到,上采样的过程中由于频率更高导致采样后数据部分缺失。这时候可以使用上采样的填充方法,方法如下:
1)ffill
只有一个参数limit
控制向前填充的数量。
下面将天为频率的数据上采样到8H
频率,向前填充1行和2行的结果。
df.resample('8H')['C_0'].ffill(limit=1)
2)bfill
与向前填充用法一样,下面向后填充1行和2行的结果。
df.resample('8H')['C_0'].bfill(limit=1)
3)nearest
该方法为就近填充,无确定方向,可能向前或者向后。参数也是limit对填充数量进行控制。以下对缺失部分按最近数据填充1行,结果如下。
df.resample('8H')['C_0'].nearest(limit=1)
4)fillna
该方法是前三种方法的集合,参数method
可设置{'pad'/'ffill','bfill','nearest'}
三种,分别代表向前,向后、取最近,同时也可以设置limit
进行数量控制,因此该方法可以取代前面三种。
df.resample('8H')['C_0'].fillna(method='pad', limit=1)
5)asfreq
该方法可以指定固定值对所有缺失部分一次性填充,比如对缺失部分统一填充-999。
df.resample('8H')['C_0'].asfreq(-999)
6)interpolate
该方法可以使用更高级的算法进行填充。具体方法可通过参数method
设置,不详细介绍,这里以linear
线性插值方法举例。
df.resample('8H').interpolate(method='linear').applymap(lambda x:round(x,2))
应用函数
1)agg
如果想同时对多列的聚合,或者对单列赋予多个聚合函数,可以使用agg()
聚合方法。
下面进行下采样,将天频率降为周,并对多个变量进行多种聚合操作。
df.resample('W').agg(
{
'C_0': ['sum', 'mean'],
'C_1': lambda x: np.std(x, ddof=1)
}
).head()
以上结果列名显示了两个层级,如果想去掉层级并自定义结果中的变量名,可通过以下代码实现。
df=generate_sample_data_datetime()
df.resample('W').agg(
C_0_sum=('C_0','sum'),
C_0_avg=('C_0','mean'),
C_1_delta=('C_1', lambda x:x.max()-x.min())
).head()
2)apply
使用apply
函数也可以达到agg
的聚合效果,以下对多个变量进行不同的聚合函数,其中也可以自定义函数。
def agg_func(x):
names = {
'C_0_mean': round(x['C_0'].mean(),2),
'C_1_sum': x['C_1'].sum(),
'C_2_max': x['C_2'].max(),
'C_3_mean_plus1': round(x['C_3'].mean()+1,2),
}
return pd.Series(names, index=[ key for key in names.keys()])
df.resample('W').apply(agg_func).head()
3)transform
transform
在分组系列中介绍过,会对原数据进行分组内转换但不改变原索引结构,在重采样中用法一样。transform()
函数的使用方法可参考pandas transform 数据转换的 4 个常用技巧!
以下对C_0变量进行采样分组内的累加和排序操作。
df['C_0_cumsum'] = df.resample('W')['C_0'].transform('cumsum')
df['C_0_rank'] = df.resample('W')['C_0'].transform('rank')
df.head(10)
4)pipe
pipe()
被称为管道函数,可以对重采样后的resampler对象应用带参数的自定义函数。pipe()
函数的使用方法可参考pandas一个优雅的高级应用函数!
它最大的优势在于可以链式使用,每次函数执行后的输出结果可以作为下一个函数的参数,形式如:pipe(func1).pipe(func2)
,参数可以是series、dataFrames、groupBy对象、或者resampler对象。
通过pipe
的链式可以像管道一样按顺序依次执行操作,并且只需要一行代码即可,极大地提高了可读性。
以下对下采样后的C_0和C_1变量进行累加求和操作,然后再对两个求和作差。
df['cumsum_delta'] = df.resample('W')['C_0','C_1'] \
.pipe(lambda x:x.cumsum()) \
.pipe(lambda x:x['C_1']-x['C_0'])
df.head(10)
这里当pipe
应用了cumsum()
函数后,与transform
一样可以返回不改变原索引的结果。
-end-