数据分析·一 | 用Pandas高效处理时序数据

数据分析·一 | 用pandas处理时序数据

缘起

笔者最近在一家金融公司的数据部门实习,主要运用 pythonpandas 进行数据清洗,经过几天的面向百度/开发文档编程,积累了一些经验,特在此总结记录下。

时序数据处理

在金融、经济、物理学等领域,都需要在多个时间点观测或者测量数据,这样就产生了关于时间序列的数据。时间序列数据(Time Series Data)是在不同时间上收集到的数据,这类数据是按时间顺序收集到的,用于描述现象随时间变化的情况。例如我们的银行卡账单股市的价格历史降水量数据等都属于时序数据。学会如何对时间序列数据进行巧妙的处理非常重要,Pandas拥有强大的时间序列数据处理的能力。

注意:本文约定已经将pandas以如下方式导入:

import pandas as pd

相关知识介绍

首先我们来了解下pandas中关于时序数据的相关知识,提升自己的姿势水平

在这里插入图片描述

pandas中包含四种主要的时间相关的数据类型。以下引用翻译自pandas官方文档

pandas拥有了4个与时间相关的概念:

  1. 日期时间:具有时区支持的特定日期和时间。与标准库中的datetime.datetime相似。

  2. 时间增量:绝对持续时间。与标准库中的datetime.timedelta类似。

  3. 时间跨度:由时间点及其相关频率定义的时间跨度。

  4. 日期偏移量:考虑了日历计算的相对持续时间。与dateutil包中的dateutil.relativedelta.relativedelta相似。

这四个概念分别对应的方法如下:

概念标量类数组类pandas Data Type主要的构造方法
时间日期TimestampDatetimeIndexdatetime64[ns] or datetime64[ns, tz]to_datetime or date_range
时间增量TimedeltaTimedeltaIndextimedelta64[ns]to_timedelta or timedelta_range
时间范围PeriodPeriodIndexperiod[freq]Period or period_range
日期偏移量DateOffsetNoneNoneDateOffset
时间日期戳 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

日期偏移量DateOffsetTimedelta类似,可以处理时间戳的加减运算。但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')

时间范围Periodresample方法可以按指定频率进行重采样,类似与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

得到如下的测试数据::

datavalue
02020-01-05651
12020-01-06188
22020-01-07661
32020-01-08913
42020-01-09740
952020-04-09610
962020-04-10475
972020-04-11316
982020-04-12700
992020-04-13176

100 rows × 2 columns

进行如下resample操作:

df.resample("Q",on="data",kind="period").sum()
# 通过on参数指定要进行resample的字段

得到结果如下:

value
data
2020Q144672
2020Q25918

在上述例子中用到的参数详细介绍如下:

(我知道你肯定不想看的,建议先收藏本文,等你真正要写的时候再来细看)

  • labelloffset 等参数用于生成标签。

    label 指定生成的结果如何为间隔标注起始时间。loffset 调整输出标签的时间。

  • closed表示的是时间段中哪边是闭合的。closed 可以设置为leftright,用于指定关闭哪一端间隔。

  • 除了 MAQBMBABQW 的默认值是 right 外,其它频率偏移量的 labelclosed 默认值都是 left

  • kind 参数可以是 timestampperiod,转换为时间戳或时间段形式的索引。resample 默认保留输入的日期时间形式。

关于resample的更多详细参数说明,可以参考pandas中文文档(非官方)

大多数 DateOffset 都支持频率字符串或偏移别名,可用作 freq 关键字参数。
有效的日期偏移DateOffset及频率字符串freq如下:

日期偏移量频率字符串说明
DateOffset通用偏移类,默认为一个日历日
BDayBusinessDay'B'工作日
CDayCustomBusinessDay'C'自定义工作日
Week'W'一周,可选周内固定某日
WeekOfMonth'WOM'每月第几周的第几天
LastWeekOfMonth'LWOM'每月最后一周的第几天
MonthEnd'M'日历日月末
MonthBegin'MS'日历日月初
BMonthEndBusinessMonthEnd'BM'工作日月末
BMonthBeginBusinessMonthBegin'BMS'工作日月初
CBMonthEndCustomBusinessMonthEnd'CBM'自定义工作日月末
CBMonthBeginCustomBusinessMonthBegin'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中关于时序数据处理的基本方法,接下来看几个实际的数据处理流程。

  1. 构造测试数据

    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-05109
    2020-01-0636
    2020-01-07955
    2020-01-08541
    2020-01-09577
    2020-12-31747
    2021-01-01242
    2021-01-0219
    2021-01-036
    2021-01-04825

    366 rows × 1 columns

    特意将测试数据的时间序列的跨度设置为涵盖了一个闰月,使得天数为366天,但是很明显,该时间序列的跨度没有大于一年

  2. 判断给定时间序列数据的时间跨度是否大于一年

    先看一个错误示范

    (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-052021-01-04的时间跨度不足一年

  3. 计算特定时间段的数据(如季度,半年等)的均值

    获得按季度统计的均值:

    df.resample('Q',kind="period").sum()
    
value
2020Q148335
2020Q239703
2020Q348095
2020Q445851
2021Q11092

获得按半年统计的均值:

df.resample('6M',kind="period").sum()
value
2020-0188038
2020-0793946
2021-011092

如果要按照指定的特殊时间进行统计,例如时间序列结束前的两周的总和,可以采用如下方法:

df[max(df.index)-pd.DateOffset(weeks = 2):].sum()
# value    6074
# dtype: int64

参考

你可以在以下网页中找到更多关于Pandas的资料:

  1. pandas官方文档
  2. pandas中文文档(非官方)

如有帮助,欢迎点赞/转载~
(听说给文章点赞的人代码bug特别少👀)
联系邮箱:mrjingcheng@foxmail.com
个人公众号:禅与电脑维修艺术
欢迎关注公众号,也欢迎通过邮箱交流。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值