利用python进行数据分析--时间序列(上)

与公众号同步更新,详细内容及相关ipynb文件在公众号中,公众号:AI入门小白


时间序列(time series)数据是⼀种重要的结构化数据形式。

在多个时间点观察或测量到的任何事物都可以形成⼀段时间序列。很多时间序列是固定频率的,也就是说,数据点是根据某种规律定期出现的(⽐如每15秒、每5分钟、每⽉出现⼀次)。时间序列也可以是不定期的,没有固定的时间单位或单位之间的偏移量。时间序列数据的意义取决于具体的应⽤场景,主要有以下⼏种:

  • 时间戳(timestamp),特定的时刻。
  • 固定时期(period),如2007年1⽉或2010年全年。
  • 时间间隔(interval),由起始和结束时间戳表示。时期(period)可以被看做间隔(interval)的特例。
  • 实验或过程时间,每个时间点都是相对于特定起始时间的⼀个度量。例如,从放⼊烤箱时起,每秒钟饼⼲的直径。

本章主要讲解前3种时间序列。许多技术都可⽤于处理实验型时间序列,其索引可能是⼀个整数或浮点数(表示从实验开始算起已经过去的时间)。最简单也最常⻅的时间序列都是⽤时间戳进⾏索引的。

提示:pandas也⽀持基于timedeltas的指数,它可以有效代表实验或经过的时间。这本书不涉及timedelta指数,但你可以学习pandas的⽂档(http://pandas.pydata.org/ )。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

⽇期和时间数据类型及⼯具

Python标准库包含⽤于⽇期(date)和时间(time)数据的数据类型,⽽且还有⽇历⽅⾯的功能。我们主要会⽤到datetime、time以及calendar模块。datetime.datetime(也可以简写为datetime)是⽤得最多的数据类型:

from datetime import datetime
now = datetime.now()
now

在这里插入图片描述

now.year, now.month, now.day

在这里插入图片描述
datetime以毫秒形式存储⽇期和时间。timedelta表示两个datetime对象之间的时间差:

delta = datetime(2011, 1, 7) - datetime(2008, 6, 24, 8, 15)
delta

在这里插入图片描述

delta.days

在这里插入图片描述

delta.seconds

在这里插入图片描述
可以给datetime对象加上(或减去)⼀个或多个timedelta,这样会产⽣⼀个新对象:

from datetime import timedelta
start = datetime(2011, 1, 7)
start + timedelta(12)

在这里插入图片描述

start - 2 * timedelta(12)

在这里插入图片描述
表11-1 datetime模块中的数据类型
在这里插入图片描述

字符串和datetime的相互转换

利⽤str或strftime⽅法(传⼊⼀个格式化字符串),datetime对象和pandas的Timestamp对象(稍后就会介绍)可以被格式化为字符串:

stamp = datetime(2011, 1, 3)
str(stamp)

在这里插入图片描述

stamp.strftime('%Y-%m-%d')

在这里插入图片描述
表11-2 datetime格式定义(兼容ISO C89)
在这里插入图片描述
datetime.strptime可以⽤这些格式化编码将字符串转换为⽇期:

value = '2011-01-03'
datetime.strptime(value, '%Y-%m-%d')

在这里插入图片描述

datestrs = ['7/6/2011', '8/6/2011']
[datetime.strptime(x, '%m/%d/%Y') for x in datestrs]

在这里插入图片描述
datetime.strptime是通过已知格式进⾏⽇期解析的最佳⽅式。但是每次都要编写格式定义是很麻烦的事情,尤其是对于⼀些常⻅的⽇期格式。这种情况下,你可以⽤dateutil这个第三⽅包中的parser.parse⽅法(pandas中已经⾃动安装好了):

from dateutil.parser import parse
parse('2011-01-03')

在这里插入图片描述
dateutil可以解析⼏乎所有⼈类能够理解的⽇期表示形式:

parse('Jan 31, 1997 10:45 PM')

在这里插入图片描述
在国际通⽤的格式中,⽇出现在⽉的前⾯很普遍,传⼊dayfirst=True即可解决这个问题:

parse('6/12/2011', dayfirst=True)

在这里插入图片描述
pandas通常是⽤于处理成组⽇期的,不管这些⽇期是DataFrame的轴索引还是列。to_datetime⽅法可以解析多种不同的⽇期表示形式。对标准⽇期格式(如ISO8601)的解析⾮常快:

datestrs = ['2011-07-06 12:00:00', '2011-08-06 00:00:00']
pd.to_datetime(datestrs)

在这里插入图片描述
它还可以处理缺失值(None、空字符串等):

idx = pd.to_datetime(datestrs + [None])
idx

在这里插入图片描述

pd.isnull(idx)

在这里插入图片描述

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

注意:dateutil.parser是⼀个实⽤但不完美的⼯具。⽐如说,它会把⼀些原本不是⽇期的字符串认作是⽇期(⽐如"42"会被解析为2042年的今天)。

datetime对象还有⼀些特定于当前环境(位于不同国家或使⽤不同语⾔的系统)的格式化选项。例如,德语或法语系统所⽤的⽉份简写就与英语系统所⽤的不同。表11-3进⾏了总结。

表11-3 特定于当前环境的⽇期格式
在这里插入图片描述

时间序列基础

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

from datetime import datetime
dates = [datetime(2011, 1, 2), datetime(2011, 1, 5),
         datetime(2011, 1, 7), datetime(2011, 1, 8),
         datetime(2011, 1, 10), datetime(2011, 1, 12)]
ts = pd.Series(np.random.randn(6), index=dates)
ts

在这里插入图片描述
这些datetime对象实际上是被放在⼀个DatetimeIndex中的:

ts.index

在这里插入图片描述
跟其他Series⼀样,不同索引的时间序列之间的算术运算会⾃动按⽇期对⻬:

ts + ts[::2]

在这里插入图片描述
pandas⽤NumPy的datetime64数据类型以纳秒形式存储时间戳:

ts.index.dtype

在这里插入图片描述
DatetimeIndex中的各个标量值是pandas的Timestamp对象:

stamp = ts.index[0]
stamp

在这里插入图片描述
只要有需要,TimeStamp可以随时⾃动转换为datetime对象。此外,它还可以存储频率信息(如果有的话),且知道如何执⾏时区转换以及其他操作。稍后将对此进⾏详细讲解。

索引、选取、⼦集构造

当你根据标签索引选取数据时,时间序列和其它的pandas.Series很像:

stamp = ts.index[2]
ts[stamp]

在这里插入图片描述
还有⼀种更为⽅便的⽤法:传⼊⼀个可以被解释为⽇期的字符串:

ts['1/10/2011']

在这里插入图片描述

ts['20110110']

在这里插入图片描述
对于较⻓的时间序列,只需传⼊“年”或“年⽉”即可轻松选取数据的切⽚:

longer_ts = pd.Series(np.random.randn(1000),
                      index=pd.date_range('1/1/2000', periods=1000))  # periods:要生成的周期数。
longer_ts

在这里插入图片描述

longer_ts['2001']

在这里插入图片描述
这⾥,字符串“2001”被解释成年,并根据它选取时间区间。指定⽉也同样奏效:

longer_ts['2001-05']

在这里插入图片描述
datetime对象也可以进⾏切⽚:

ts[datetime(2011, 1, 7):]

在这里插入图片描述
由于⼤部分时间序列数据都是按照时间先后排序的,因此你也可以⽤不存在于该时间序列中的时间戳对其进⾏切⽚(即范围查询):

ts

在这里插入图片描述

ts['1/6/2011':'1/11/2011']

在这里插入图片描述
跟之前⼀样,你可以传⼊字符串⽇期、datetime或Timestamp。

注意,这样切⽚所产⽣的是源时间序列的视图,跟NumPy数组的切⽚运算是⼀样的。

这意味着,没有数据被复制,对切⽚进⾏修改会反映到原始数据上。

此外,还有⼀个等价的实例⽅法也可以截取两个⽇期之间TimeSeries:

ts.truncate(after='1/9/2011')

在这里插入图片描述
⾯这些操作对DataFrame也有效。例如,对DataFrame的⾏进⾏索引:

# freq:频率,参数详情(https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#timeseries-offset-aliases)
dates = pd.date_range('1/1/2000', periods=100, freq='W-WED')
long_df = pd.DataFrame(np.random.randn(100, 4),
                       index=dates,
                       columns=['Colorado', 'Texas',
                                'New York', 'Ohio'])
long_df.loc['5-2001']

在这里插入图片描述

带有重复索引的时间序列

在某些应⽤场景中,可能会存在多个观测数据落在同⼀个时间点上的情况。下⾯就是⼀个例⼦:

dates = pd.DatetimeIndex(['1/1/2000', '1/2/2000', '1/2/2000',
                          '1/2/2000', '1/3/2000'])
dup_ts = pd.Series(np.arange(5), index=dates)
dup_ts

在这里插入图片描述
通过检查索引的is_unique属性,我们就可以知道它是不是唯⼀的:

dup_ts.index.is_unique

在这里插入图片描述
对这个时间序列进⾏索引,要么产⽣标量值,要么产⽣切⽚,具体要看所选的时间点是否重复:

dup_ts['1/3/2000']  # 未重复

在这里插入图片描述

dup_ts['1/2/2000']  # 重复

在这里插入图片描述
假设你想要对具有⾮唯⼀时间戳的数据进⾏聚合。⼀个办法是使⽤groupby,并传⼊level=0

grouped = dup_ts.groupby(level=0)
grouped.mean()

在这里插入图片描述

grouped.count()

在这里插入图片描述

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

pandas中的原⽣时间序列⼀般被认为是不规则的,也就是说,它们没有固定的频率。对于⼤部分应⽤程序⽽⾔,这是⽆所谓的。但是,它常常需要以某种相对固定的频率进⾏分析,⽐如每⽇、每⽉、每15分钟等(这样⾃然会在时间序列中引⼊缺失值)。幸运的是,pandas有⼀整套标准时间序列频率以及⽤于重采样、频率推断、⽣成固定频率⽇期范围的⼯具。例如,我们可以将之前那个时间序列转换为⼀个具有固定频率(每⽇)的时间序列,只需调⽤resample即可:

ts

在这里插入图片描述

# 字符串“D”是每天的意思,参数详情(https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#timeseries-offset-aliases)
resampler = ts.resample('D')

频率的转换(或重采样)是⼀个⽐较⼤的主题,稍后将专⻔⽤⼀节来进⾏讨论。这⾥,我将告诉你如何使⽤基本的频率和它的倍数。

⽣成⽇期范围

pandas.date_range可⽤于根据指定的频率⽣成指定⻓度的DatetimeIndex:

index = pd.date_range('2012-04-01', '2012-06-01')
index

在这里插入图片描述
默认情况下,date_range会产⽣按天计算的时间点。如果只传⼊起始或结束⽇期,那就还得传⼊⼀个表示⼀段时间的数字:

pd.date_range(start='2012-04-01', periods=20)

在这里插入图片描述

pd.date_range(end='2012-06-01', periods=20)

在这里插入图片描述
起始和结束⽇期定义了⽇期索引的严格边界。例如,如果你想要⽣成⼀个由每⽉最后⼀个⼯作⽇组成的⽇期索引,可以传⼊"BM"频率(表示business end of month,表11-4是频率列表),这样就只会包含时间间隔内(或刚好在边界上的)符合频率要求的⽇期:
表11-4 基本的时间序列频率(不完整)
在这里插入图片描述

时间序列频率,同上面:freq:频率,参数详情一致

pd.date_range('2000-01-01', '2000-12-01', freq='BM')

在这里插入图片描述
date_range默认会保留起始和结束时间戳的时间信息(如果有的话):

pd.date_range('2012-05-02 12:56:31', periods=5)

在这里插入图片描述
有时,虽然起始和结束⽇期带有时间信息,但你希望产⽣⼀组被规范化(normalize)到午夜的时间戳。normalize选项即可实现该功能:

pd.date_range('2012-05-02 12:56:31', periods=5, normalize=True)

在这里插入图片描述

频率和⽇期偏移量

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

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

在这里插入图片描述
传⼊⼀个整数即可定义偏移量的倍数:

four_hours = Hour(4)
four_hours

在这里插入图片描述
⼀般来说,⽆需明确创建这样的对象,只需使⽤诸如"H"或"4H"这样的字符串别名即可。在基础频率前⾯放上⼀个整数即可创建倍数:

pd.date_range('2000-01-01', '2000-01-03 23:59', freq='4h')

在这里插入图片描述
⼤部分偏移量对象都可通过加法进⾏连接:

Hour(2) + Minute(30)

在这里插入图片描述
同理,你也可以传⼊频率字符串(如"2h30min"),这种字符串可以被⾼效地解析为等效的表达式:

pd.date_range('2000-01-01', periods=10, freq='1h30min')

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

表11-4列出了pandas中的频率代码和⽇期偏移量类。
笔记:⽤户可以根据实际需求⾃定义⼀些频率类以便提供pandas所没有的⽇期逻辑,但具体的细节超出了本书的范围。

WOM⽇期

WOM(Week Of Month)是⼀种⾮常实⽤的频率类,它以WOM开头。它使你能获得诸如“每⽉第3个星期五”之类的⽇期:

rng = pd.date_range('2012-01-01', '2012-09-01', freq='WOM-3FRI')
list(rng)

在这里插入图片描述

移动(超前和滞后)数据

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

ts = pd.Series(np.random.randn(4),
               index=pd.date_range('1/1/2000', periods=4, freq='M'))
ts

在这里插入图片描述

ts.shift(2)

在这里插入图片描述

ts.shift(-2)

在这里插入图片描述
当我们这样进⾏移动时,就会在时间序列的前⾯或后⾯产⽣缺失数据。

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

ts / ts.shift(1) - 1

在这里插入图片描述
由于单纯的移位操作不会修改索引,所以部分数据会被丢弃。因此,如果频率已知,则可以将其传给shift以便实现对时间戳进⾏位移⽽不是对数据进⾏简单位移:

ts.shift(2, freq='M')  # 将月份后移两月

在这里插入图片描述
这⾥还可以使⽤其他频率,于是你就能⾮常灵活地对数据进⾏超前和滞后处理了:

ts.shift(3, freq='D')

在这里插入图片描述

ts.shift(1, freq='90T')

在这里插入图片描述

通过偏移量对⽇期进⾏位移

pandas的⽇期偏移量还可以⽤在datetime或Timestamp对象上:

from pandas.tseries.offsets import Day, MonthEnd
now = datetime(2011, 11, 17)
now + 3 * Day()

在这里插入图片描述
如果加的是锚点偏移量(⽐如MonthEnd),第⼀次增量会将原⽇期向前滚动到符合频率规则的下⼀个⽇期:

now + MonthEnd()

在这里插入图片描述

now + MonthEnd(2)

在这里插入图片描述
通过锚点偏移量的rollforward和rollback⽅法,可明确地将⽇期向前或向后“滚动”:

offset = MonthEnd()
offset.rollforward(now)

在这里插入图片描述

offset.rollback(now)

在这里插入图片描述
⽇期偏移量还有⼀个巧妙的⽤法,即结合groupby使⽤这两个“滚动”⽅法:

ts = pd.Series(np.random.randn(20),
               index=pd.date_range('1/15/2000', periods=20, freq='4d'))
ts

在这里插入图片描述

ts.groupby(offset.rollforward).mean()

在这里插入图片描述
当然,更简单、更快速地实现该功能的办法是使⽤resample(后面将进行详细介绍):

ts.resample('M').mean()

在这里插入图片描述

时区处理

时间序列处理⼯作中最让⼈不爽的就是对时区的处理。许多⼈都选择以协调世界时(UTC,它是格林尼治标准时间(Greenwich Mean Time)的接替者,⽬前已经是国际标准了)来处理时间序列。时区是以UTC偏移量的形式表示的。例如,夏令时期间,纽约⽐UTC慢4⼩时,⽽在全年其他时间则⽐UTC慢5⼩时。

在Python中,时区信息来⾃第三⽅库pytz,它使Python可以使⽤Olson数据库(汇编了世界时区信息)。这对历史数据⾮常重要,这是因为由于各地政府的各种突发奇想,夏令时转变⽇期(甚⾄UTC偏移量)已经发⽣过多次改变了。就拿美国来说,DST转变时间⾃1900年以来就改变过多次!

有关pytz库的更多信息,请查阅其⽂档。就本书⽽⾔,由于pandas包装了pytz的功能,因此你可以不⽤记忆其API,只要记得时区的名称即可。时区名可以在shell中看到,也可以通过⽂档查看:

import pytz
pytz.common_timezones[-5:]

在这里插入图片描述
要从pytz中获取时区对象,使⽤pytz.timezone即可:

tz = pytz.timezone('America/New_York')
tz

在这里插入图片描述
pandas中的⽅法既可以接受时区名也可以接受这些对象。

时区本地化和转换

默认情况下,pandas中的时间序列是单纯的(naive)时区。看看下⾯这个时间序列:

rng = pd.date_range('3/9/2012 9:30', periods=6, freq='D')
ts = pd.Series(np.random.randn(len(rng)), index=rng)
ts

在这里插入图片描述
其索引的tz字段为None:

print(ts.index.tz)

在这里插入图片描述
可以⽤时区集⽣成⽇期范围:

pd.date_range('3/9/2012 9:30', periods=10, freq='D', tz='UTC')

在这里插入图片描述
从单纯到本地化的转换是通过tz_localize⽅法处理的:

ts_utc = ts.tz_localize('UTC')
ts_utc

在这里插入图片描述

ts_utc.index

在这里插入图片描述
⼀旦时间序列被本地化到某个特定时区,就可以⽤tz_convert将其转换到别的时区了:

ts_utc.tz_convert('America/New_York')

在这里插入图片描述
对于上⾯这种时间序列(它跨越了美国东部时区的夏令时转变期),我们可以将其本地化到EST,然后转换为UTC或柏林时间:

ts_eastern = ts.tz_localize('America/New_York')
ts_eastern.tz_convert('UTC')

在这里插入图片描述

ts_eastern.tz_convert('Europe/Berlin')

在这里插入图片描述
tz_localize和tz_convert也是DatetimeIndex的实例⽅法:

ts.index.tz_localize('Asia/Shanghai')

在这里插入图片描述
注意:对单纯时间戳的本地化操作还会检查夏令时转变期附近容易混淆或不存在的时间。

操作时区意识型Timestamp对象

跟时间序列和⽇期范围差不多,独⽴的Timestamp对象也能被从单纯型(naive)本地化为时区意识型(time zone-aware),并从⼀个时区转换到另⼀个时区:

stamp = pd.Timestamp('2011-03-12 04:00')
stamp

在这里插入图片描述

stamp_utc = stamp.tz_localize('utc')
stamp_utc

在这里插入图片描述

stamp_utc.tz_convert('America/New_York')

在这里插入图片描述
在创建Timestamp时,还可以传⼊⼀个时区信息:

stamp_moscow = pd.Timestamp('2011-03-12 04:00', tz='Europe/Moscow')
stamp_moscow

在这里插入图片描述
时区意识型Timestamp对象在内部保存了⼀个UTC时间戳值(⾃UNIX纪元(1970年1⽉1⽇)算起的纳秒数)。这个UTC值在时区转换过程中是不会发⽣变化的:

stamp_utc.value

在这里插入图片描述

stamp_utc.tz_convert('America/New_York').value

在这里插入图片描述
当使⽤pandas的DateOffset对象执⾏时间算术运算时,运算过程会⾃动关注是否存在夏令时转变期。这⾥,我们创建了在DST转变之前的时间戳。⾸先,来看夏令时转变前的30分钟:

from pandas.tseries.offsets import Hour
stamp = pd.Timestamp('2012-03-12 01:30', tz='US/Eastern')
stamp

在这里插入图片描述

stamp + Hour()

在这里插入图片描述
然后,夏令时转变前90分钟:

stamp = pd.Timestamp('2012-11-04 00:30', tz='US/Eastern')
stamp

在这里插入图片描述

stamp + 2 * Hour()

在这里插入图片描述

不同时区之间的运算

如果两个时间序列的时区不同,在将它们合并到⼀起时,最终结果就会是UTC。由于时间戳其实是以UTC存储的,所以这是⼀个很简单的运算,并不需要发⽣任何转换:

rng = pd.date_range('3/7/2012 9:30', periods=10, freq='B')
ts = pd.Series(np.random.randn(len(rng)), index=rng)
ts

在这里插入图片描述

ts1 = ts[:7].tz_localize('Europe/London')
ts1

在这里插入图片描述

ts2 = ts1[2:].tz_convert('Europe/Moscow')
ts2

在这里插入图片描述

result = ts1 + ts2
result.index

在这里插入图片描述

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值