美文网首页人工智能机器学习我爱编程
《利用Python进行数据分析》第10章 时间序列、日期和时间数

《利用Python进行数据分析》第10章 时间序列、日期和时间数

作者: 龍猫君 | 来源:发表于2017-12-30 15:34 被阅读0次

    时间序列

    日期和时间数据类型及工具

    Python标准库包含用于日期(date)和时间(time)数据的数据类型,还有日历方面的功能。主要会用到datetime、time以及calendar模块。datetime.datetime(也可以简写为datetime)是用得最多的数据类型

    from pandas import  Series,DataFrame
    import pandas as pd
    import numpy as np
    from datetime import datetime
    now=datetime.now()
    now
    
    datetime.datetime(2017, 12, 27, 10, 25, 45, 574261)
    
    now.year,now.month,now.day
    
    (2017, 12, 26)
    

    datetime以毫秒形式存储日期和时间。

    datetime.timedelta表示两个datetime对象之间的时间差

    delta=datetime(2017,12,1)-datetime(2012,9,8,8,15)
    delta
    
    datetime.timedelta(1909, 56700)
    
    delta.days    #日的时间差
    
    1909
    
    delta.seconds  #秒的时间差
    
    56700
    

    可以给datetime对象加上(或减去)一个或多个timedelta,这样会产生一个新对象

    from datetime import timedelta
    start=datetime(2017,6,13)
    start+timedelta(12)
    
    datetime.datetime(2017, 6, 25, 0, 0)
    
    start-2*timedelta(12)
    
    datetime.datetime(2017, 5, 20, 0, 0)
    

    datetime模块中的数据类型参见表10-1

    字符串和datetime的相互转换

    利用str或strftime方法(传入一个格式化字符串),datetime对象和pandas的Timestamp对象可以被格式化为字符串

    stamp=datetime(2017,12,26)
    str(stamp)
    
    '2017-12-26 00:00:00'
    
    stamp.strftime('%Y-%m-%d')
    
    '2017-12-26'
    

    datetime.strptime可以用表中的这些格式化编码将字符串转换为日期

    value='2017-12-26'
    datetime.strptime(value,'%Y-%m-%d')
    
    datetime.datetime(2017, 12, 26, 0, 0)
    
    datestrs=['11/12/2017','12/12/2017']
    [datetime.strptime(x,'%m/%d/%Y') for x in datestrs]
    
    [datetime.datetime(2017, 11, 12, 0, 0), datetime.datetime(2017, 12, 12, 0, 0)]
    

    datetime.strptime是通过已知格式进行日期解析的最佳方式。d但是对于其他常见的日期格式,需要使用dateutil这个第三方包中的parser.parse方法

    from dateutil.parser import parse
    parse('2017-12-06')
    
    datetime.datetime(2017, 12, 6, 0, 0)
    

    dateutil可以解析几乎所有人类能够理解的日期表示形式

    parse('Jan 31,2017 9:35 PM')
    
    datetime.datetime(2017, 1, 31, 21, 35)
    

    在国际通用的格式中,日通常出现在月的前面,传入dayfirst=True即可解决

    parse('10/12/2017',dayfirst=True)
    
    datetime.datetime(2017, 12, 10, 0, 0)
    

    pandas通常是用于处理成组日期的,不管这些日期是DataFrame的轴索引还是列。to_datetime方法可以解析多种不同的日期表示形式。对标准日期格式(如ISO8601)的解析非常快。可以处理缺失值(None、空字符串等)

    datestrs
    
    ['11/12/2017', '12/12/2017']
    
    pd.to_datetime(datestrs)
    
    DatetimeIndex(['2017-11-12', '2017-12-12'], dtype='datetime64[ns]', freq=None)
    
    idx=pd.to_datetime(datestrs+[None])
    idx
    
    DatetimeIndex(['2017-11-12', '2017-12-12', 'NaT'], dtype='datetime64[ns]', freq=None)
    
    idx[2]
    
    NaT
    
    idx[1]
    
    Timestamp('2017-12-12 00:00:00')
    
    pd.isnull(idx)
    
    array([False, False,  True], dtype=bool)
    

    NaT(Not a Time)是pandas中时间戳数据的NA值

    警告: dateutil.parser是一个实用但不完美的工具。比如说,它会把一些原本不是日期的字符串认作是日期(比如"42"会被解析为2042年的今天)

    时间序列基础

    pandas最基本的时间序列类型就是以时间戳(通常以Python字符串或datatime对象表示)为索引的Series

    from datetime import datetime
    dates=[datetime(2017,1,6),datetime(2017,1,10),datetime(2017,1,13),
          datetime(2017,1,15),datetime(2017,1,18),datetime(2017,1,20)]
    ts=Series(np.random.randn(6),index=dates)
    ts
    
    2017-01-06    0.005613
    2017-01-10    1.222545
    2017-01-13    0.000879
    2017-01-15    0.172794
    2017-01-18   -0.530976
    2017-01-20   -0.808555
    dtype: float64
    

    可以看出datetime对象是被放在一个DatetimeIndex中,变量ts就成为一个TimeSeries

    type(ts)
    
    pandas.core.series.Series
    
    ts.index
    
    DatetimeIndex(['2017-01-06', '2017-01-10', '2017-01-13', '2017-01-15',
                   '2017-01-18', '2017-01-20'],
                  dtype='datetime64[ns]', freq=None)
    

    注意: 没必要显式使用TimeSeries的构造函数。当创建一个带有DatetimeIndex的Series时,pandas就会知道该对象是一个时间序列。

    ts[::2]
    
    2017-01-06    0.170305
    2017-01-13   -0.798955
    2017-01-18   -0.367438
    dtype: float64
    

    跟其他Series一样,不同索引的时间序列之间的算术运算会自动按日期对齐

    ts+ts[::2]
    
    2017-01-06    0.340610
    2017-01-10         NaN
    2017-01-13   -1.597911
    2017-01-15         NaN
    2017-01-18   -0.734876
    2017-01-20         NaN
    dtype: float64
    

    pandas用NumPy的datetime64数据类型以纳秒形式存储时间戳

    ts.index.dtype
    
    dtype('<M8[ns]')
    

    DatetimeIndex中的各个标量值是pandas的Timestamp对象

    stamp=ts.index[0]
    stamp
    
    Timestamp('2017-01-06 00:00:00')
    
    ts.index[3]
    
    Timestamp('2017-01-15 00:00:00')
    

    如果有需要,TimeStamp可以随时自动转换为datetime对象

    索引、选取、子集构造

    由于TimeSeries是Series的一个子类,所以在索引以及数据选取方面它们的行为是一样的

    stamp=ts.index[2]
    stamp
    
    Timestamp('2017-01-13 00:00:00')
    
    ts[stamp]
    
    -0.79895542662575525
    

    可以传入一个可以被解释为日期的字符串

    ts['1/10/2017']
    
    1.3762403458229933
    
    ts['20170118']
    
    -0.36743784149263731
    

    对于较长的时间序列,只需传入“年”或“年月”即可轻松选取数据的切片

    longer_ts=Series(np.random.randn(1000),
                     index=pd.date_range('11/25/2017',periods=1000))
    longer_ts
    
    2017-11-25   -0.659067
    2017-11-26   -1.539291
    2017-11-27   -1.731498
    2017-11-28   -0.560947
          ...      ...   
    2020-08-17   -0.499714
    2020-08-18   -1.978626
    2020-08-19    1.757046
    2020-08-20   -0.401384
    Freq: D, Length: 1000, dtype: float64
    
    longer_ts['2017']
    
    2017-11-25   -0.659067
    2017-11-26   -1.539291
    2017-11-27   -1.731498
    2017-11-28   -0.560947
    2017-11-29   -2.086909
            ...      ...   
    2017-12-28    0.123651
    2017-12-29   -0.188641
    2017-12-30    1.206445
    2017-12-31   -1.734814
    Freq: D, dtype: float64
    
    longer_ts['2017-11']
    
    2017-11-25   -0.659067
    2017-11-26   -1.539291
    2017-11-27   -1.731498
    2017-11-28   -0.560947
    2017-11-29   -2.086909
    2017-11-30   -0.673677
    Freq: D, dtype: float64
    

    通过日期进行切片的方式只对规则Series有效。

    ts
    
    2017-01-06    0.170305
    2017-01-10    1.376240
    2017-01-13   -0.798955
    2017-01-15   -0.496169
    2017-01-18   -0.367438
    2017-01-20    0.059816
    dtype: float64
    
    ts[datetime(2017,1,13):]
    
    2017-01-13   -0.798955
    2017-01-15   -0.496169
    2017-01-18   -0.367438
    2017-01-20    0.059816
    dtype: float64
    

    由于大部分时间序列数据都是按照时间先后排序的,因此你也可以用不存在于该时间序列中的时间戳对其进行切片(即范围查询

    ts['1/12/2017':'1/17/2017']
    
    2017-01-13   -0.798955
    2017-01-15   -0.496169
    dtype: float64
    

    这里可以传入字符串日期、datetime或Timestamp。注意,这样切片所产生的是源时间序列的视图,跟NumPy数组的切片运算是一样的。此外,还有一个等价的实例方法也可以截取两个日期之间TimeSeries

    ts.truncate(after='1/14/2017')
    
    2017-01-06    0.170305
    2017-01-10    1.376240
    2017-01-13   -0.798955
    dtype: float64
    

    这些操作对DataFrame也有效

    dates=pd.date_range('1/1/2017',periods=100,freq='W-WED')
    long_df=DataFrame(np.random.randn(100,4),index=dates,
                     columns=['Colorado','Texas','New York','Ohio'])
    long_df.ix['5-2017']
    

    带有重复索引的时间序列

    在某些应用场景中,可能会存在多个观测数据落在同一个时间点上的情况

    dates=pd.DatetimeIndex(['1/1/2017','1/2/2017','1/2/2017','1/2/2017','1/3/2017'])
    dup_ts=Series(np.arange(5),index=dates)
    dup_ts
    
    2017-01-01    0
    2017-01-02    1
    2017-01-02    2
    2017-01-02    3
    2017-01-03    4
    dtype: int32
    

    通过检查索引的is_unique属性,我们就可以知道它是不是唯一的

    dup_ts.index.is_unique
    
    False
    

    对这个时间序列进行索引,要么产生标量值,要么产生切片,具体要看所选的时间点是否重复

    dup_ts['1/3/2017'] #不重复
    
    4
    
    dup_ts['1/2/2017']  #重复
    
    2017-01-02    1
    2017-01-02    2
    2017-01-02    3
    dtype: int32
    

    假设你想要对具有非唯一时间戳的数据进行聚合。一个办法是使用groupby,并传入level=0(索引的唯一一层!)

    grouped=dup_ts.groupby(level=0)
    grouped.mean()
    
    2017-01-01    0
    2017-01-02    2
    2017-01-03    4
    dtype: int32
    
    grouped.count()
    
    2017-01-01    1
    2017-01-02    3
    2017-01-03    1
    dtype: int64
    

    日期的范围、频率以及移动

    pandas有一整套标准时间序列频率以及用于重采样、频率推断、生成固定频率日期范围的工具。例如,我们可以将之前那个时间序列转换为一个具有固定频率(每日)的时间序列,只需调用resample

    ts
    
    2017-01-06    0.005613
    2017-01-10    1.222545
    2017-01-13    0.000879
    2017-01-15    0.172794
    2017-01-18   -0.530976
    2017-01-20   -0.808555
    dtype: float64
    
    ts.resample('D')
    
    DatetimeIndexResampler [freq=<Day>, axis=0, closed=left, label=left, convention=start, base=0]
    

    生成日期范围

    pandas.date_range可用于生成指定长度的DatetimeIndex

    index=pd.date_range('6/10/2017','7/1/2017')
    index
    
    DatetimeIndex(['2017-06-10', '2017-06-11', '2017-06-12', '2017-06-13',
                   '2017-06-14', '2017-06-15', '2017-06-16', '2017-06-17',
                   '2017-06-18', '2017-06-19', '2017-06-20', '2017-06-21',
                   '2017-06-22', '2017-06-23', '2017-06-24', '2017-06-25',
                   '2017-06-26', '2017-06-27', '2017-06-28', '2017-06-29',
                   '2017-06-30', '2017-07-01'],
                  dtype='datetime64[ns]', freq='D')
    

    默认情况下,date_range会产生按天计算的时间点。如果只传入起始或结束日期,那就还得传入一个表示一段时间的数字

    pd.date_range(start='12/8/2017',periods=20)
    
    DatetimeIndex(['2017-12-08', '2017-12-09', '2017-12-10', '2017-12-11',
                   '2017-12-12', '2017-12-13', '2017-12-14', '2017-12-15',
                   '2017-12-16', '2017-12-17', '2017-12-18', '2017-12-19',
                   '2017-12-20', '2017-12-21', '2017-12-22', '2017-12-23',
                   '2017-12-24', '2017-12-25', '2017-12-26', '2017-12-27'],
                  dtype='datetime64[ns]', freq='D')
    

    起始和结束日期定义了日期索引的严格边界。例如,如果你想要生成一个由每月最后一个工作日组成的日期索引,可以传入"BM"频率(表示business end of month),这样就只会包含时间间隔内(或刚好在边界上的)符合频率要求的日期

    pd.date_range('1/1/2017','12/1/2017',freq='BM')
    
    DatetimeIndex(['2017-01-31', '2017-02-28', '2017-03-31', '2017-04-28',
                   '2017-05-31', '2017-06-30', '2017-07-31', '2017-08-31',
                   '2017-09-29', '2017-10-31', '2017-11-30'],
                  dtype='datetime64[ns]', freq='BM')
    

    date_range默认会保留起始和结束时间戳的时间信息(如果有的话)

    pd.date_range('5/6/2017 12:30:20',periods=5)
    
    DatetimeIndex(['2017-05-06 12:30:20', '2017-05-07 12:30:20',
                   '2017-05-08 12:30:20', '2017-05-09 12:30:20',
                   '2017-05-10 12:30:20'],
                  dtype='datetime64[ns]', freq='D')
    

    有时,虽然起始和结束日期带有时间信息,但你希望产生一组被规范化(normalize)到午夜的时间戳。normalize选项即可实现该功能

    pd.date_range('12/15/2017 12:50:33',periods=5,normalize=True)
    
    DatetimeIndex(['2017-12-15', '2017-12-16', '2017-12-17', '2017-12-18',
                   '2017-12-19'],
                  dtype='datetime64[ns]', freq='D')
    

    频率和日期偏移量

    pandas中的频率是由一个基础频率(base frequency)和一个乘数组成的。基础频率通常以一个字符串别名表示,比如"M"表示每月,"H"表示每小时。对于每个基础频率,都有一个被称为日期偏移量(date offset)的对象与之对应。例如,按小时计算的频率可以用Hour类表示

    from pandas.tseries.offsets import Hour,Minute
    hour=Hour()
    hour
    
    <Hour>
    

    传入一个整数即可定义偏移量的倍数

    four_fours=Hour(4)
    four_fours
    
    <4 * Hours>
    

    无需显式创建这样的对象,只需使用诸如"H"或"4H"这样的字符串别名即可。在基础频率前面放上一个整数即可创建倍数

    pd.date_range('1/1/2017','1/3/2017 23:59',freq='4H')
    
    DatetimeIndex(['2017-01-01 00:00:00', '2017-01-01 04:00:00',
                   '2017-01-01 08:00:00', '2017-01-01 12:00:00',
                   '2017-01-01 16:00:00', '2017-01-01 20:00:00',
                   '2017-01-02 00:00:00', '2017-01-02 04:00:00',
                   '2017-01-02 08:00:00', '2017-01-02 12:00:00',
                   '2017-01-02 16:00:00', '2017-01-02 20:00:00',
                   '2017-01-03 00:00:00', '2017-01-03 04:00:00',
                   '2017-01-03 08:00:00', '2017-01-03 12:00:00',
                   '2017-01-03 16:00:00', '2017-01-03 20:00:00'],
                  dtype='datetime64[ns]', freq='4H')
    

    大部分偏移量对象都可通过加法进行连接;你也可以传入频率字符串(如"2h30min"),这种字符串可以被高效地解析为等效的表达式

    Hour(3)+Minute(30)
    
    <210 * Minutes>
    
    pd.date_range('1/1/2017',periods=10,freq='1h30min')
    
    DatetimeIndex(['2017-01-01 00:00:00', '2017-01-01 01:30:00',
                   '2017-01-01 03:00:00', '2017-01-01 04:30:00',
                   '2017-01-01 06:00:00', '2017-01-01 07:30:00',
                   '2017-01-01 09:00:00', '2017-01-01 10:30:00',
                   '2017-01-01 12:00:00', '2017-01-01 13:30:00'],
                  dtype='datetime64[ns]', freq='90T')
    

    有些频率所描述的时间点并不是均匀分隔的。例如,"M"(日历月末)和"BM"(每月最后一个工作日)就取决于每月的天数,对于后者,还要考虑月末是不是周末。由于没有更好的术语,我将这些称为锚点偏移量(anchored offset)。

    表10-4列出了pandas中的频率代码和日期偏移量类。

    WOM日期

    WOM(Week Of Month)是一种非常实用的频率类,它以WOM开头。它使你能获得诸如“每月第3个星期五”之类的日期

    rng=pd.date_range('1/1/2017','9/1/2017',freq='WOM-3FRI')
    list(rng)
    
    [Timestamp('2017-01-20 00:00:00', freq='WOM-3FRI'),
     Timestamp('2017-02-17 00:00:00', freq='WOM-3FRI'),
     Timestamp('2017-03-17 00:00:00', freq='WOM-3FRI'),
     Timestamp('2017-04-21 00:00:00', freq='WOM-3FRI'),
     Timestamp('2017-05-19 00:00:00', freq='WOM-3FRI'),
     Timestamp('2017-06-16 00:00:00', freq='WOM-3FRI'),
     Timestamp('2017-07-21 00:00:00', freq='WOM-3FRI'),
     Timestamp('2017-08-18 00:00:00', freq='WOM-3FRI')]
    

    美国的股票期权交易人会意识到这些日子就是标准的月度到期日

    移动(超前和滞后)数据

    移动(shifting)指的是沿着时间轴将数据前移或后移。Series和DataFrame都有一个shift方法用于执行单纯的前移或后移操作,保持索引不变

    ts=Series(np.random.randn(4),
             index=pd.date_range('1/1/2017',periods=4,freq='M'))
    ts
    
    2017-01-31   -1.555270
    2017-02-28    1.325946
    2017-03-31   -1.458090
    2017-04-30    1.064776
    Freq: M, dtype: float64
    
    ts.shift(2)
    
    2017-01-31         NaN
    2017-02-28         NaN
    2017-03-31   -1.555270
    2017-04-30    1.325946
    Freq: M, dtype: float64
    
    ts.shift(-2)
    
    2017-01-31   -1.458090
    2017-02-28    1.064776
    2017-03-31         NaN
    2017-04-30         NaN
    Freq: M, dtype: float64
    

    shift通常用于计算一个时间序列或多个时间序列(如DataFrame的列)中的百分比变化。可以这样表达:

    ts / ts.shift(1) - 1

    由于单纯的移位操作不会修改索引,所以部分数据会被丢弃。因此,如果频率已知,则可以将其传给shift以便实现对时间戳进行位移而不是对数据进行简单位移

    ts.shift(2,freq='M')
    
    2017-03-31   -1.555270
    2017-04-30    1.325946
    2017-05-31   -1.458090
    2017-06-30    1.064776
    Freq: M, dtype: float64
    

    还可以使用其他频率,于是你就能非常灵活地对数据进行超前和滞后处理了

    ts.shift(3,freq='D')
    
    2017-02-03   -1.555270
    2017-03-03    1.325946
    2017-04-03   -1.458090
    2017-05-03    1.064776
    dtype: float64
    
    ts.shift(1,freq='3D')
    
    2017-02-03   -1.555270
    2017-03-03    1.325946
    2017-04-03   -1.458090
    2017-05-03    1.064776
    dtype: float64
    
    ts.shift(-3,freq='D')
    
    2017-01-28   -1.555270
    2017-02-25    1.325946
    2017-03-28   -1.458090
    2017-04-27    1.064776
    dtype: float64
    

    通过偏移量对日期进行位移

    pandas的日期偏移量还可以用在datetime或Timestamp对象上

    from pandas.tseries.offsets import Day,MonthEnd
    now=datetime(2017,12,27)
    now+3*Day()
    
    Timestamp('2017-12-30 00:00:00')
    

    如果加的是锚点偏移量(比如MonthEnd),第一次增量会将原日期向前滚动到符合频率规则的下一个日期

    now+MonthEnd()
    
    Timestamp('2017-12-31 00:00:00')
    
    now+MonthEnd(3)
    
    Timestamp('2018-02-28 00:00:00')
    

    通过锚点偏移量的rollforward和rollback方法,可显式地将日期向前或向后“滚动”

    offset=MonthEnd()
    offset.rollforward(now)
    
    Timestamp('2017-12-31 00:00:00')
    
    offset.rollback(now)
    
    Timestamp('2017-11-30 00:00:00')
    

    日期偏移量还有一个巧妙的用法,即结合groupby使用这两个“滚动”方法

    ts=Series(np.random.randn(10),
             index=pd.date_range('1/15/2017',periods=10,freq='4d'))
    ts
    
    2017-01-15    1.060753
    2017-01-19    0.410493
    2017-01-23    2.188356
    2017-01-27   -0.335335
    2017-01-31   -1.976843
    2017-02-04    1.644975
    2017-02-08    0.243292
    2017-02-12   -0.444232
    2017-02-16   -0.665907
    2017-02-20   -0.771520
    Freq: 4D, dtype: float64
    
    ts.groupby(offset.rollforward).mean()
    
    2017-01-31    0.269485
    2017-02-28    0.001322
    dtype: float64
    

    更简单、更快速地实现该功能的办法是使用resample

    ts.resample('M',how='mean')
    
    2017-01-31    0.269485
    2017-02-28    0.001322
    Freq: M, dtype: float64
    

    本章的时间序列知识点比较多,需要分阶段练习。

    相关文章

      网友评论

        本文标题:《利用Python进行数据分析》第10章 时间序列、日期和时间数

        本文链接:https://www.haomeiwen.com/subject/mxdbgxtx.html