数据分析·一 | 用pandas处理时序数据
缘起
笔者最近在一家金融公司的数据部门实习,主要运用 python
的 pandas
进行数据清洗,经过几天的面向百度/开发文档编程,积累了一些经验,特在此总结记录下。
时序数据处理
在金融、经济、物理学等领域,都需要在多个时间点观测或者测量数据,这样就产生了关于时间序列的数据。时间序列数据(Time Series Data)是在不同时间上收集到的数据,这类数据是按时间顺序收集到的,用于描述现象随时间变化的情况。例如我们的银行卡账单、股市的价格、历史降水量数据等都属于时序数据。学会如何对时间序列数据进行巧妙的处理非常重要,Pandas
拥有强大的时间序列数据处理的能力。
注意:本文约定已经将pandas
以如下方式导入:
import pandas as pd
相关知识介绍
首先我们来了解下pandas
中关于时序数据的相关知识,提升自己的姿势水平。
pandas
中包含四种主要的时间相关的数据类型。以下引用翻译自pandas官方文档
pandas拥有了4个与时间相关的概念:
日期时间:具有时区支持的特定日期和时间。与标准库中的
datetime.datetime
相似。时间增量:绝对持续时间。与标准库中的
datetime.timedelta
类似。时间跨度:由时间点及其相关频率定义的时间跨度。
日期偏移量:考虑了日历计算的相对持续时间。与
dateutil
包中的dateutil.relativedelta.relativedelta
相似。这四个概念分别对应的方法如下:
概念 标量类 数组类 pandas Data Type 主要的构造方法 时间日期 Timestamp
DatetimeIndex
datetime64[ns]
ordatetime64[ns, tz]
to_datetime
ordate_range
时间增量 Timedelta
TimedeltaIndex
timedelta64[ns]
to_timedelta
ortimedelta_range
时间范围 Period
PeriodIndex
period[freq]
Period
orperiod_range
日期偏移量 DateOffset
None
None
DateOffset
时间日期戳 Timestamp
-
构造
pandas
的时间戳pd.to_datetime('2020-07-06') # 将得到如下pandas的内置时间戳 # Timestamp('2020-07-06 00:00:00') # pandas的to_datetime函数可以解析多种格式的时间字符串 pd.to_datetime('06/07/2020') # 年份在前和日期在前的都可以解析 # 将得到如下pandas的内置时间戳 # Timestamp('2020-06-07 00:00:00') # 还可以解析unix时间戳 pd.to_datetime(1594044764, unit='s') # 根据unit精度不同可以调整unit参数的值,如ms,ns等 # 将得到如下pandas的内置时间戳 # Timestamp('2020-07-06 14:12:44')
-
构造时间序列
pd.date_range('2020-07-01',periods=30,freq='D') # 将得到如下时间索引 # DatetimeIndex(['2020-07-01', '2020-07-02', '2020-07-03', '2020-07-04', # '2020-07-05', '2020-07-06', '2020-07-07', '2020-07-08', # '2020-07-09', '2020-07-10', '2020-07-11', '2020-07-12', # '2020-07-13', '2020-07-14', '2020-07-15', '2020-07-16', # '2020-07-17', '2020-07-18', '2020-07-19', '2020-07-20', # '2020-07-21', '2020-07-22', '2020-07-23', '2020-07-24', # '2020-07-25', '2020-07-26', '2020-07-27', '2020-07-28', # '2020-07-29', '2020-07-30'], # dtype='datetime64[ns]', freq='D')
时间增量 Timedelta
主要是用于时间戳的加减运算,和Python原生库 datetime
中的 timedelta
类似,计算的是确定的时间增量。
t1 = pd.to_datetime('06/07/2020')
t2 = pd.to_datetime('30/07/2020')
t2 - t1
# 将得到如下两个日期的时间增量
# Timedelta('53 days 00:00:00')
(t2-t1).days
# 将得到如下两个日期日期相差天数
# 53
# Timedelata可以直接获得的属性有days,seconds和microseconds,但是seconds不会从days直接转换过来,例如
(t2-t1).seconds
# 0
# Timedelta 还可直接用于时间戳的相加,但是timedelta最大只能到天,不能到月
pd.to_datetime("2020-07-08") + pd.to_timedelta("2 day 2 hour")
# 将得到如下新的时间戳
# Timestamp('2020-07-10 02:00:00')
日期偏移量 DateOffset
日期偏移量DateOffset
和Timedelta
类似,可以处理时间戳的加减运算。但DateOffset
有着更多更强大的功能。
Timedelta
只能处理确定的时间增量,例如Timedelta('100 days 15:30:43.121000')
,而DataOffset
能处理不确定的时间增量,来看看下面的例子:
pd.to_datetime("2020-03-31") - pd.offsets.DateOffset(months=1)
# DateOffset可以处理抽象的日期,例如几个月或者几年,并且可以灵活处理闰年闰月等情况
# Timestamp('2020-02-29 00:00:00')
pd.Timestamp('2020-07-08') + pd.offsets.MonthBegin(n=1)
# MonthBegin可以直接取得后面N个月的月首/MonthEnd对应月尾
# Timestamp('2020-08-01 00:00:00')
# 这个方法可以用来获得本月的月初或者月末
pd.Timestamp('2020-07-08') - pd.offsets.MonthBegin()
# Timestamp('2020-07-01 00:00:00')
pd.to_datetime("2020-07-08") + 3 * pd.offsets.BDay()
# BDay能够区分周末和工作日,进行相应的增减运算(然而大家周六真的不用加班的嘛?🤣)
Timestamp('2020-07-13 00:00:00')
时间范围 Period
Period 表示时间跨度,即时间段,如年、季、月、日等。关键字 freq
与频率别名可以指定时间段。freq
表示的是 Period
的时间跨度,可以理解为是一个DateOffset
。但是freq不能为负,如-3D
。
pd.Period(2020,freq='A-DEC')
# Period的参数:
# 时间戳:该 period 在时间轴上的位置
# freq :该 period 的长度,在本例中,“A-DEC”表示以12月作为结束的一整年
# Period('2020', 'A-DEC')
通过加减整数可以实现对Period
的移动
# 加减整数
pd.Period(2020,freq='A-DEC') - 2
# Period('2018', 'A-DEC')
时间范围Period
的resample
方法可以按指定频率进行重采样,类似与groupby
方法。
这是一个比较实用的方法,可以看看下面的例子:
import numpy as np
# 构造测试数据
time_idx = pd.date_range('2020-07-01', periods=10, freq='D')
ts = pd.Series(np.random.randint(100,size=len(time_idx)) , index=time_idx)
ts
# 2020-07-01 24
# 2020-07-02 34
# 2020-07-03 71
# 2020-07-04 38
# 2020-07-05 1
# 2020-07-06 30
# 2020-07-07 71
# 2020-07-08 5
# 2020-07-09 29
# 2020-07-10 93
# Freq: D, dtype: int32
# 利用resample按照两天进行汇总
ts.resample("2D").sum()
# 2020-07-01 58
# 2020-07-03 109
# 2020-07-05 31
# 2020-07-07 76
# 2020-07-09 122
# Freq: 2D, dtype: int32
# label默认为left
ts.resample("2D",label="right").sum()
# 2020-07-03 58
# 2020-07-05 109
# 2020-07-07 31
# 2020-07-09 76
# 2020-07-11 122
# Freq: 2D, dtype: int32
# 本例中,closed设置为右闭合,即包括(07-09,07-10],默认为左闭合
ts.resample("2D",closed="right").sum()
# 2020-06-29 24
# 2020-07-01 105
# 2020-07-03 39
# 2020-07-05 101
# 2020-07-07 34
# 2020-07-09 93
# Freq: 2D, dtype: int32
# 按照周进行汇总 (周频率"W"是默认以周日作为一周的结束的),kind可以指定索引形式,本例设置为时间段
ts.resample("W",kind='period').sum()
# 2020-06-29/2020-07-05 168
# 2020-07-06/2020-07-12 228
# Freq: W-SUN, dtype: int32
如果目标数据是一个不是以时间作为索引的DataFrame
,则指定resample
方法的on
字段,参看下面的例子:
# 首先构造测试数据
time_range = pd.date_range(start='2020-01-05',periods =100,freq="D")
df = pd.DataFrame({"data":time_range,"value":np.random.randint(1000,size=len(time_range))})
df
得到如下的测试数据::
data | value | |
---|---|---|
0 | 2020-01-05 | 651 |
1 | 2020-01-06 | 188 |
2 | 2020-01-07 | 661 |
3 | 2020-01-08 | 913 |
4 | 2020-01-09 | 740 |
… | … | … |
95 | 2020-04-09 | 610 |
96 | 2020-04-10 | 475 |
97 | 2020-04-11 | 316 |
98 | 2020-04-12 | 700 |
99 | 2020-04-13 | 176 |
100 rows × 2 columns
进行如下resample
操作:
df.resample("Q",on="data",kind="period").sum()
# 通过on参数指定要进行resample的字段
得到结果如下:
value | |
---|---|
data | |
2020Q1 | 44672 |
2020Q2 | 5918 |
在上述例子中用到的参数详细介绍如下:
(我知道你肯定不想看的,建议先收藏本文,等你真正要写的时候再来细看)
-
label
、loffset
等参数用于生成标签。label
指定生成的结果如何为间隔标注起始时间。loffset
调整输出标签的时间。 -
closed
表示的是时间段中哪边是闭合的。closed
可以设置为left
或right
,用于指定关闭哪一端间隔。 -
除了
M
、A
、Q
、BM
、BA
、BQ
、W
的默认值是right
外,其它频率偏移量的label
与closed
默认值都是left
。 -
kind
参数可以是timestamp
或period
,转换为时间戳或时间段形式的索引。resample
默认保留输入的日期时间形式。
关于resample
的更多详细参数说明,可以参考pandas中文文档(非官方)。
大多数 DateOffset
都支持频率字符串或偏移别名,可用作 freq
关键字参数。
有效的日期偏移DateOffset
及频率字符串freq
如下:
日期偏移量 | 频率字符串 | 说明 |
---|---|---|
DateOffset | 无 | 通用偏移类,默认为一个日历日 |
BDay 或 BusinessDay | 'B' | 工作日 |
CDay 或 CustomBusinessDay | 'C' | 自定义工作日 |
Week | 'W' | 一周,可选周内固定某日 |
WeekOfMonth | 'WOM' | 每月第几周的第几天 |
LastWeekOfMonth | 'LWOM' | 每月最后一周的第几天 |
MonthEnd | 'M' | 日历日月末 |
MonthBegin | 'MS' | 日历日月初 |
BMonthEnd 或 BusinessMonthEnd | 'BM' | 工作日月末 |
BMonthBegin 或 BusinessMonthBegin | 'BMS' | 工作日月初 |
CBMonthEnd 或 CustomBusinessMonthEnd | 'CBM' | 自定义工作日月末 |
CBMonthBegin 或 CustomBusinessMonthBegin | 'CBMS' | 自定义工作日月初 |
SemiMonthEnd | 'SM' | 某月第 15 天(或其它半数日期)与日历日月末 |
SemiMonthBegin | 'SMS' | 日历日月初与第 15 天(或其它半数日期) |
QuarterEnd | 'Q' | 日历日季末 |
QuarterBegin | 'QS' | 日历日季初 |
BQuarterEnd | 'BQ | 工作日季末 |
BQuarterBegin | 'BQS' | 工作日季初 |
FY5253Quarter | 'REQ' | 零售季,又名 52-53 周 |
YearEnd | 'A' | 日历日年末 |
YearBegin | 'AS' 或 'BYS' | 日历日年初 |
BYearEnd | 'BA' | 工作日年末 |
BYearBegin | 'BAS' | 工作日年初 |
FY5253 | 'RE' | 零售年(又名 52-53 周) |
Easter | 无 | 复活节假日 |
BusinessHour | 'BH' | 工作小时 |
CustomBusinessHour | 'CBH' | 自定义工作小时 |
Day | 'D' | 一天 |
Hour | 'H' | 一小时 |
Minute | 'T' 或 'min' | 一分钟 |
Second | 'S' | 一秒 |
Milli | 'L' 或 'ms' | 一毫秒 |
Micro | 'U' 或 'us' | 一微秒 |
Nano | 'N' | 一纳秒 |
时序数据处理实践
了解了pandas
中关于时序数据处理的基本方法,接下来看几个实际的数据处理流程。
-
构造测试数据
import numpy as np import pandas as pd time_index = pd.date_range(start='2020-01-05',periods = 366,freq="D") df = pd.DataFrame({"value":np.random.randint(1000,size=len(time_index))},index=time_index) df
所得到的测试数据如下:
value 2020-01-05 109 2020-01-06 36 2020-01-07 955 2020-01-08 541 2020-01-09 577 … … 2020-12-31 747 2021-01-01 242 2021-01-02 19 2021-01-03 6 2021-01-04 825 366 rows × 1 columns
特意将测试数据的时间序列的跨度设置为涵盖了一个闰月,使得天数为366天,但是很明显,该时间序列的跨度没有大于一年
-
判断给定时间序列数据的时间跨度是否大于一年
先看一个错误示范:
(max(df.index) - min(df.index)).days > 365 # True
一个最容易想到的但是错误的方法是:直接计算最大最小值的天数差,判断是否大于365天,但出现闰月的时候会出错
接下来是正确示范:
max(df.index) - pd.offsets.DateOffset(years=1) >= min(df.index) # False
结果为False,因为
2020-01-05
到2021-01-04
的时间跨度不足一年 -
计算特定时间段的数据(如季度,半年等)的均值
获得按季度统计的均值:
df.resample('Q',kind="period").sum()
value | |
---|---|
2020Q1 | 48335 |
2020Q2 | 39703 |
2020Q3 | 48095 |
2020Q4 | 45851 |
2021Q1 | 1092 |
获得按半年统计的均值:
df.resample('6M',kind="period").sum()
value | |
---|---|
2020-01 | 88038 |
2020-07 | 93946 |
2021-01 | 1092 |
如果要按照指定的特殊时间进行统计,例如时间序列结束前的两周的总和,可以采用如下方法:
df[max(df.index)-pd.DateOffset(weeks = 2):].sum()
# value 6074
# dtype: int64
参考
你可以在以下网页中找到更多关于Pandas
的资料:
如有帮助,欢迎点赞/转载~
(听说给文章点赞的人代码bug特别少👀)
联系邮箱:mrjingcheng@foxmail.com
个人公众号:禅与电脑维修艺术
欢迎关注公众号,也欢迎通过邮箱交流。