Skip to content

时间序列

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

时间序列数据在很多领域都是重要的结构化数据形式。在多个时间点观测或测量的数据形成了时间序列。

许多时间序列是固定频率的,也就是说数据是根据相同的规则定期出现的,例如每15秒、每5分钟或每月1次。

时间序列也可以是不规则的,没有固定的时间单位或单位间的偏移量。

如何标记和引用时间序列数据取决于应用程序,时间序列包括:

  • 时间戳,具体的时刻。
  • 固定的时间区间,例如2007的1月或整个2010年。
  • 时间间隔,由开始和结束时间戳表示。时间区间可以被认为是间隔的特殊情况。
  • 实验时间或消耗时间。每个时间戳是相对于特定开始时间的时间的量度(例如,自从被放置在烤箱中每秒烘烤的饼干的直径)。
  • 目前主要关注前三类中的时间序列。
from datetime import datetime, timedelta
import datetime as dt
from dateutil.parser import parse
import pandas as pd

datetime

datetime格式符:

  • %a 星期的英文单词的缩写:如星期一, 则返回 Mon
  • %A 星期的英文单词的全拼:如星期一,返回 Monday
  • %b 月份的英文单词的缩写:如一月, 则返回 Jan
  • %B 月份的引文单词的缩写:如一月, 则返回 January
  • %c 返回datetime的字符串表示,如03/08/15 23:01:26
  • %d 返回的是当前时间是当前月的第几天
  • %f 微秒的表示: 范围: [0,999999]
  • %H 以24小时制表示当前小时
  • %I 以12小时制表示当前小时
  • %m 返回月份 范围[0,12]
  • %M 返回分钟数 范围 [0,59]
  • %P 返回是上午还是下午–AM or PM
  • %S 返回秒数 范围 [0,61]。。。手册说明的
  • %U 返回当周是当年的第几周 以周日为第一天
  • %W 返回当周是当年的第几周 以周一为第一天
  • %w 当天在当周的天数,范围为[0, 6],6表示星期天
  • %x 日期的字符串表示 :03/08/15
  • %X 时间的字符串表示 :23:22:08
  • %y 两个数字表示的年份 15
  • %Y 四个数字表示的年份 2015
  • %z 与utc时间的间隔 (如果是本地时间,返回空字符串)
  • %Z 时区名称(如果是本地时间,返回空字符串)
datestrs = ['2020/5/6', '2021/10/1']
# 注意区分datetime模块和datetime类,名字相同,容易引起错误。
# 比如datetime.datetime就报错type object 'datetime.datetime' has no attribute 'datetime'
print(datetime)
# <class 'datetime.datetime'>
print(dt)
# <module 'datetime' from '/opt/Python-3.9.6/Lib/datetime.py'>

Python标准库包含了日期和时间数据的类型。datetimetimecalendar模块是开始处理时间数据的主要内容。 datetime.datetime类型,或简写为datetime,是广泛使用的。

now = datetime.now()
print(now)
# 2021-10-07 20:24:43.834293

result = dt.datetime(2021, 10, 7, 20, 26, 00, 72973)
print(result)
# 2021-10-07 20:26:00.072973

datetime既存储了日期,也存储了细化到微秒的时间。 timedelta表示两个datetime对象的时间差。

delta = datetime(2021, 10, 7) - datetime(2021, 9, 7)
print(delta)
# 30 days, 0:00:00
print(delta.days)
# 30
print(delta.seconds)
# 0

result = dt.timedelta(926, 56700)
print(result)
# 926 days, 15:45:00

可以为一个datetime对象加上(或减去)一个timedelta或其整数倍来产生一个新的datetime对象。

start = datetime(2021, 10, 7)

result = start + timedelta(12)
print(result)
# 2021-10-19 00:00:00

result = start - 2 * timedelta(5)
print(result)
# 2021-09-27 00:00:00

字符串与datetime互相转换

使用str方法或传递一个指定的格式给strftime方法来对datetime对象和pandas的Timestamp对象进行格式化。

stamp = datetime(2021, 10, 7)
result = str(stamp)
print(result)
# 2021-10-07 00:00:00

使用datetime.srtptimedatetime格式符,把字符串转换日期。 datetime.strptime是在已知格式的情况下转换日期的好方式。

value = '2021-10-7'
result = datetime.strptime(value, '%Y-%m-%d')
print(result)
# 2021-10-07 00:00:00

datestrs = ['2020/5/6', '2021/10/1']
result = [datetime.strptime(x, '%Y/%m/%d') for x in datestrs]
print(result)
# [datetime.datetime(2020, 5, 6, 0, 0), datetime.datetime(2021, 10, 1, 0, 0)]

dateutil解析通用日期格式:

print(parse('2020/5/6'))
# 2020-05-06 00:00:00

print(parse('Jan 31, 2021 10:25 AM'))
# 2021-01-31 10:25:00

print(parse('5/6/2021', dayfirst=True))  # 日期出现在月份之前
# 2021-06-05 00:00:00

pandas主要是面向处理日期数组的,无论是用作轴索引还是用作DataFrame中的列。 to_datetime方法可以转换很多不同的日期表示格式。 to_datetime方法还可以处理那些被认为是缺失值的值(None、空字符串等)。 NaT(Not a time)是pandas中时间戳数据的是null值。

datestrs = ['2020/5/6 12:00:00', '2021/10/1 09:00:00']
result = pd.to_datetime(datestrs)
print(result)
# DatetimeIndex(['2020-05-06 12:00:00', '2021-10-01 09:00:00'], dtype='datetime64[ns]', freq=None)

idx = pd.to_datetime(datestrs + [None])
print(idx)
# DatetimeIndex(['2020-05-06 12:00:00', '2021-10-01 09:00:00', 'NaT'], dtype='datetime64[ns]', freq=None)
print(idx[2])
# NaT
print(pd.isnull(idx))
# [False False  True]

时间序列基础

from datetime import datetime
import pandas as pd
import numpy as np

DatetimeIndex

pandas中的基础时间序列种类是由时间戳索引的Series,在pandas外部则通常表示为Python字符串或datetime对象。

所有使用datetime对象的地方都可以用Timestamp

dates = [
    datetime(2021, 10, 1),
    datetime(2021, 10, 3),
    datetime(2021, 10, 5),
    datetime(2021, 10, 7),
    datetime(2021, 10, 9),
    datetime(2021, 10, 11)
]
data = np.random.rand(6)
ts = pd.Series(data, index=dates)
print(ts)
# 2021-10-01    0.678297
# 2021-10-03    0.538631
# 2021-10-05    0.934413
# 2021-10-07    0.018534
# 2021-10-09    0.938441
# 2021-10-11    0.173329
# dtype: float64

这些datetime对象被放入DatetimeIndex中。

print(ts.index)
# DatetimeIndex(['2021-10-01', '2021-10-03', '2021-10-05', '2021-10-07',
#                '2021-10-09', '2021-10-11'],
#               dtype='datetime64[ns]', freq=None)

DatetimeIndex中的标量值是pandasTimestamp对象:

stamp = ts.index[0]
print(stamp)
# 2021-10-01 00:00:00

和其他Series类似,不同索引的时间序列之间的算术运算在日期上自动对齐:

print(ts + ts[::2])  # ts[::2]会将ts中每隔一个的元素选择出
# 2021-10-01    1.356595
# 2021-10-03         NaN
# 2021-10-05    1.868825
# 2021-10-07         NaN
# 2021-10-09    1.876883
# 2021-10-11         NaN
# dtype: float64

pandas使用NumPy的datetime64数据类型在纳秒级的分辨率下存储时间戳

print(ts.index.dtype)
# datetime64[ns]

索引、选择、子集

当基于标签进行索引和选择时,时间序列的行为和其他的pandas.Series类似:

stamp = ts.index[2]
print(ts[stamp])
# 0.9344125159374457  对应2021-10-05

也可以传递一个能解释为日期的字符串:

print(ts['10/9/2021'])
print(ts['20211003'])

对一个长的时间序列,可以传递一个年份或一个年份和月份来选择数据切片:

data = np.random.randn(1000)
longer_ts = pd.Series(
    data,
    index=pd.date_range('1/1/2021', periods=1000)
)

print(longer_ts)
# 2021-01-01   -0.009192
# 2021-01-02   -1.079068
# 2021-01-03   -1.851176
# 2021-01-04    1.347109
# 2021-01-05   -0.236394
#                 ...
# 2023-09-23   -1.317943
# 2023-09-24    0.201741
# 2023-09-25    0.442282
# 2023-09-26    0.176137
# 2023-09-27    1.146437
# Freq: D, Length: 1000, dtype: float64

字符串’2001’被解释为一个年份,并选择了相应的时间区间。

print(longer_ts['2021'])
# 2021-01-01    2.170411
# 2021-01-02    1.186933
# 2021-01-03    0.399262
# 2021-01-04   -1.042606
# 2021-01-05    2.082112
#                 ...
# 2021-12-27   -0.988282
# 2021-12-28    0.598683
# 2021-12-29    2.770580
# 2021-12-30   -1.463262
# 2021-12-31   -1.642846
# Freq: D, Length: 365, dtype: float64

指定了年份和月份也是有效的。

print(longer_ts['2021-10'])
# 2021-10-01    0.712265
# 2021-10-02    1.195221
# 2021-10-03   -1.930220
# 2021-10-04   -0.720816
# 2021-10-05    0.081777
# 2021-10-06   -0.037466
# 2021-10-07    3.737303
# 2021-10-08    1.620383
# 2021-10-09    0.990797
# 2021-10-10    0.507850
# 2021-10-11    0.846935
# 2021-10-12    0.996947
# 2021-10-13   -1.078558
# 2021-10-14    0.871832
# 2021-10-15   -0.591698
# 2021-10-16   -0.805463
# 2021-10-17    0.160528
# 2021-10-18   -0.028474
# 2021-10-19    2.305579
# 2021-10-20   -1.132288
# 2021-10-21    0.649980
# 2021-10-22    0.615327
# 2021-10-23    0.185108
# 2021-10-24    0.857199
# 2021-10-25   -1.473752
# 2021-10-26   -0.895161
# 2021-10-27   -0.432717
# 2021-10-28    0.734504
# 2021-10-29    1.892493
# 2021-10-30    0.456619
# 2021-10-31   -0.255288
# Freq: D, dtype: float64

使用datetime对象进行切片也是可以的:

print(longer_ts[datetime(2023, 1, 6):])
# 2023-01-06    0.952591
# 2023-01-07   -0.900259
# 2023-01-08    0.925332
# 2023-01-09    0.173215
# 2023-01-10   -0.507791
#                 ...
# 2023-09-23   -0.319989
# 2023-09-24   -1.105417
# 2023-09-25   -2.118769
# 2023-09-26    0.009420
# 2023-09-27   -0.310281
# Freq: D, Length: 265, dtype: float64

因为大部分的时间序列数据是按时间顺序排序的,可以使用不包含在时间序列中的时间戳进行切片,以执行范围查询:

print(longer_ts['2021/10/1':'2021/10/5'])
# 2021-10-01   -0.591853
# 2021-10-02   -1.554564
# 2021-10-03   -0.712585
# 2021-10-04   -0.326657
# 2021-10-05    1.044887
# Freq: D, dtype: float64

使用truncate在两个日期间对Series进行切片:

print(longer_ts.truncate(after='2021/10/1'))
# 2021-01-01   -0.906685
# 2021-01-02   -0.470732
# 2021-01-03   -0.041316
# 2021-01-04   -0.287356
# 2021-01-05    0.104268
#                 ...
# 2021-09-27   -0.669198
# 2021-09-28   -2.222169
# 2021-09-29   -0.653814
# 2021-09-30   -0.625868
# 2021-10-01    0.872684
# Freq: D, Length: 274, dtype: float64

上面这些操作也都适用于DataFrame,并在其行上进行索引:

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

print(long_df)
#             Colorado     Texas  New York      Ohio
# 2020-10-07 -1.186789  2.020634  0.300076 -0.955234
# 2020-10-14  1.502838  0.965368 -0.797539 -0.292833
# ...              ...       ...       ...       ...
# 2022-08-24 -0.253116 -0.263307  0.602425  0.370599
# 2022-08-31  0.907918  0.091939  0.789694  2.781535
# [100 rows x 4 columns]


print(long_df.loc['10-2020'])
#             Colorado     Texas  New York      Ohio
# 2020-10-07  1.031616 -1.812038 -0.446577  0.395656
# 2020-10-14 -0.673167  0.198804 -0.439141  0.086004
# 2020-10-21 -1.139786  0.716820  0.006516 -0.284335
# 2020-10-28 -0.637939  1.647810 -0.750786  0.140637

含有重复索引的时间序列

在某些应用中,可能会有多个数据观察值落在特定的时间戳上。下面是个例子:

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

通过检查索引的is_unique属性,可以看出索引并不是唯一的:

print(dup_ts.index.is_unique)
# False

对上面的Series进行索引,结果是标量值还是Series切片取决于是否有时间戳是重复的:

result = dup_ts['2021/1/3']
print(result)
# 4
result = dup_ts['2021/1/2']
print(result)
# 2021-01-02    1
# 2021-01-02    2
# 2021-01-02    3
# dtype: int64

假设想要聚合含有非唯一时间戳的数据。一种方式就是使用groupby并传递level=0

grouped = dup_ts.groupby(level=0)
result = grouped.mean()
print(result)
# 2021-01-01    0.0
# 2021-01-02    2.0
# 2021-01-03    4.0
# dtype: float64

result = grouped.count()
print(result)
# 2021-01-01    1
# 2021-01-02    3
# 2021-01-03    1
# dtype: int64

日期范围、频率和移位

from datetime import datetime, timedelta
import pandas as pd
import numpy as np
from pandas.tseries.offsets import Hour, Minute, Day, MonthEnd

pandas的通用时间序列是不规则的,即时间序列的频率不是固定的。 但有时需要处理固定频率的场景,例如每日的、每月的或每15分钟的时间序列数据。 可以通过调用resample方法将样本时间序列转换为固定的每日频率数据。

在频率间转换,又称为重新采样。

dates = [
    datetime(2021, 10, 1),
    datetime(2021, 10, 3),
    datetime(2021, 10, 5),
    datetime(2021, 10, 7),
    datetime(2021, 10, 9),
    datetime(2021, 10, 11)
]
data = np.random.rand(6)
ts = pd.Series(data, index=dates)
print(ts)
# 2021-10-01    0.956685
# 2021-10-03    0.817168
# 2021-10-05    0.275543
# 2021-10-07    0.614226
# 2021-10-09    0.061377
# 2021-10-11    0.357080
# dtype: float64


resampler = ts.resample('D')  # 字符串’D’被解释为每日频率
print(resampler)
# DatetimeIndexResampler [freq=<Day>, axis=0, closed=left, label=left, convention=start, origin=start_day]

生成日期范围

pandas.date_range是用于根据特定频率生成指定长度的DatetimeIndex。 默认情况下,date_range生成的是每日的时间戳。如果只传递一个起始或结尾日期,你必须传递一个用于生成范围的数字。 开始日期和结束日期严格定义了生成日期索引的边界。

index = pd.date_range('2021/1/1', '2021/1/30')
print(index)
index = pd.date_range(start='2021/1/1', periods=30)
print(index)
index = pd.date_range(end='2021/1/30', periods=30)
print(index)
# DatetimeIndex(['2021-01-01', '2021-01-02', '2021-01-03', '2021-01-04',
#                '2021-01-05', '2021-01-06', '2021-01-07', '2021-01-08',
#                '2021-01-09', '2021-01-10', '2021-01-11', '2021-01-12',
#                '2021-01-13', '2021-01-14', '2021-01-15', '2021-01-16',
#                '2021-01-17', '2021-01-18', '2021-01-19', '2021-01-20',
#                '2021-01-21', '2021-01-22', '2021-01-23', '2021-01-24',
#                '2021-01-25', '2021-01-26', '2021-01-27', '2021-01-28',
#                '2021-01-29', '2021-01-30'],
#               dtype='datetime64[ns]', freq='D')

默认情况下,date_range保留开始或结束时间戳的时间(如果有的话)。 normalize选项可以实现生成的是标准化为零点的时间戳。

index = pd.date_range('2021/1/1 12:56:30', periods=5)
print(index)
# DatetimeIndex(['2021-01-01 12:56:30', '2021-01-02 12:56:30',
#                '2021-01-03 12:56:30', '2021-01-04 12:56:30',
#                '2021-01-05 12:56:30'],
#               dtype='datetime64[ns]', freq='D')
index = pd.date_range('2021/1/1 12:56:30', periods=5, normalize=True)
print(index)
# DatetimeIndex(['2021-01-01', '2021-01-02', '2021-01-03', '2021-01-04',
#                '2021-01-05'],
#               dtype='datetime64[ns]', freq='D')

Pandas时间序列:频率和日期偏移量。

pandas中的频率是由一个基础频率(例如“日”、“月”)和一个乘数组成。 基础频率通常以一个字符串别名表示,比如“D”表示日,“M”表示月。 对于每个基础频率,都有一个被称为日期偏移量(dateoffset)的对象与之对应,比如日期偏移量Hour对应的频率是H

常用频率与日期偏移量。

频率 日期偏移量 说明
D Day 日历日
B BusinessDay 工作日
H Hour 小时
T/min Minute
S Second
L/ms Milli 毫秒
U Micro 微秒
M MonthEnd 每月最后一个日历日
BM BusinessMonthEnd 每月最后一个工作日
MS MonthBegin 每月第一个日历日
BMS BussinessMonthBegin 每月第一个工作日
W-MON, W-TUE, ... Week 指定星期几(MON,TUE,WED,THU,FRI,SAT,SUN)
WOM-1MON,WOM-2MON, ... WeekOfMonth 产生每月第一,第二,第三或第四周的星期几。例如WOM-3FRI表示每月第3个星期五
Q-JAN,Q-FEB, ... QuarterEnd 以指定月份结束的年度,每季度最后一个月的最后一个日历日
BQ-JAN,BQ-FEB, ... BusinessQuarterEnd 以指定月份结束的年度,每季度最后一个月的最后一个工作日
QS-JAN,QS-FEB, ... QuarterBegin 以指定月份结束的年度,每季度最后一个月的第一个日历日
BQS-JAN,BQS-FEB, ... BusinessQuarterBegin 以指定月份结束的年度,每季度最后一个月的第一个工作日
A-JAN,A-FEB, ... YearEnd 每年指定月份的最后一个日历日
BA-JAN,BA-FEB, ... BusinessYearEnd 每年指定月份的最后一个工作日
AS-JAN,AS-FEB, ... YearBegin 每年指定月份的第一个日历日
BAS-JAN,BAS-FEB, ... BusinessYearBegin 每年指定月份的第一个工作日

频率和日期偏置

pandas中的频率是由基础频率和倍数组成的。 基础频率通常会有字符串别名,例如M代表每月,H代表每小时。 对于每个基础频率,都有一个对象可以被用于定义日期偏置。

例如,每小时的频率可以使用Hour类来表示:

hour = Hour()
print(hour)
# <Hour>

可以传递一个整数来定义偏置量的倍数:

four_hours = Hour(4)
print(four_hours)
# <4 * Hours>

在大多数应用中,不需要显式地创建这些对象,而是使用字符串别名,如H4H。在基础频率前放一个整数就可以生成倍数:

ts = pd.date_range('2021/1/1', '2021/1/2 23:59', freq='4h')
print(ts)
# DatetimeIndex(['2021-01-01 00:00:00', '2021-01-01 04:00:00',
#                '2021-01-01 08:00:00', '2021-01-01 12:00:00',
#                '2021-01-01 16:00:00', '2021-01-01 20:00:00',
#                '2021-01-02 00:00:00', '2021-01-02 04:00:00',
#                '2021-01-02 08:00:00', '2021-01-02 12:00:00',
#                '2021-01-02 16:00:00', '2021-01-02 20:00:00'],
#               dtype='datetime64[ns]', freq='4H')

多个偏置可以通过加法进行联合:

print(Hour(2) + Minute(30))
# <150 * Minutes>

类似地,可以传递频率字符串:

ts = pd.date_range('2021/1/1', '2021/1/1 23:59', freq='4h30min')
print(ts)
# DatetimeIndex(['2021-01-01 00:00:00', '2021-01-01 04:30:00',
#                '2021-01-01 09:00:00', '2021-01-01 13:30:00',
#                '2021-01-01 18:00:00', '2021-01-01 22:30:00'],
#               dtype='datetime64[ns]', freq='270T')

有些频率描述点的时间并不是均匀分隔的。例如,M(日历月末)和BM(月内最后工作日)取决于当月天数,月末是否是周末。我们将这些日期称为锚定偏置量。

月中某星期的日期

"月中某星期"(week of month )的日期是一个有用的频率类,以WOM开始。

rng = pd.date_range('2021-1-1', '2021-9-1', freq='WOM-3FRI')  # 每月第三个星期五
print(rng)
# DatetimeIndex(['2021-01-15', '2021-02-19', '2021-03-19', '2021-04-16',
#                '2021-05-21', '2021-06-18', '2021-07-16', '2021-08-20'],
#               dtype='datetime64[ns]', freq='WOM-3FRI')

移位(前向和后向)日期

"移位"是指将日期按时间向前移动或向后移动。

Series和DataFrame都有一个shift方法用于进行简单的前向或后向移位,而不改变索引。 进行移位时,会在时间序列的起始位或结束位引入缺失值。

data = [0.882972, 1.363282, -0.687750, -0.048117]
ts = pd.Series(data, index=pd.date_range('2021-1-1', periods=4, freq='M'))
print(ts)
# 2021-01-31    0.882972
# 2021-02-28    1.363282
# 2021-03-31   -0.687750
# 2021-04-30   -0.048117
# Freq: M, dtype: float64
print(ts.shift(2))
# 2021-01-31         NaN
# 2021-02-28         NaN
# 2021-03-31    0.882972
# 2021-04-30    1.363282
# Freq: M, dtype: float64
print(ts.shift(-2))
# 2021-01-31   -0.687750
# 2021-02-28   -0.048117
# 2021-03-31         NaN
# 2021-04-30         NaN
# Freq: M, dtype: float64

shift常用于计算时间序列或DataFrame多列时间序列的百分比变化:

print(ts/ts.shift(1))
# 2021-01-31         NaN
# 2021-02-28    1.543970
# 2021-03-31   -0.504481
# 2021-04-30    0.069963
# Freq: M, dtype: float64

print(ts/ts.shift(1) - 1)
# 2021-01-31         NaN
# 2021-02-28    0.543970
# 2021-03-31   -1.504481
# 2021-04-30   -0.930037
# Freq: M, dtype: float64

如果频率是已知的,则可以将频率传递给shift来推移时间戳:

print(ts.shift(2, freq='M'))  # 原始数据的“月“增加了偏移值
# 2021-03-31    0.882972
# 2022021-10-31 00:00:001-04-30    1.363282
# 2021-05-31   -0.687750
# 2021-06-30   -0.048117
# Freq: M, dtype: float64
print(ts.shift(2, freq='D'))  # 原始数据的“日“增加了偏移值
# 2021-02-02    0.882972
# 2021-03-02    1.363282
# 2021-04-02   -0.687750
# 2021-05-02   -0.048117
# dtype: float64
print(ts.shift(2, freq='90T'))  # 原始数据的“小时“增加了偏移值
# 2021-01-31 03:00:00    0.882972
# 2021-02-28 03:00:00    1.363282
# 2021-03-31 03:00:00   -0.687750
# 2021-04-30 03:00:00   -0.048117
# dtype: float64

使用偏置进行移位日期

pandas日期偏置也可以使用datetimeTimestamp对象完成:

now = datetime(2021, 10, 9)
print(now)
# 2021-10-09 00:00:00
print(now + 3 * Day())
# 2021-10-12 00:00:00

锚定偏置可以使用rollforwardrollback分别显式地将日期向前或向后"滚动"。 如果添加了一个锚定偏置量,比如MonthEnd,根据频率规则,第一个增量会将日期“前滚”到下一个日期:

print(now + MonthEnd())  # “前滚”到当前月的月底
# 2021-10-31 00:00:00
print(now + MonthEnd(2))  # 注意这里的序列号,当前月是1,下个月是2
# 2021-11-30 00:00:00

offset = MonthEnd()
print(offset.rollback(now))
# 2021-09-30 00:00:00
print(offset.rollforward(now))
# 2021-10-31 00:00:00

将移位方法与groupby一起使用是日期偏置的一种创造性用法:

ts = pd.Series(
    np.random.randn(20),
    index=pd.date_range('2021/1/1', periods=20, freq='4d')
)
print(ts)
# 2021-01-01    0.674348
# 2021-01-05   -1.437803
# 2021-01-09   -0.079218
# 2021-01-13   -1.444890
# 2021-01-17    0.643279
# 2021-01-21    1.089965
# 2021-01-25    0.021876
# 2021-01-29    0.692138
# 2021-02-02    0.833496
# 2021-02-06    1.082616
# 2021-02-10   -0.729415
# 2021-02-14    0.271186
# 2021-02-18   -1.416218
# 2021-02-22   -0.780402
# 2021-02-26   -0.113773
# 2021-03-02    2.095338
# 2021-03-06   -0.302612
# 2021-03-10    1.113632
# 2021-03-14   -1.314581
# 2021-03-18    0.947746
# Freq: 4D, dtype: float64

print(ts.groupby(offset.rollforward).mean())  # 前滚至当月月底,计算当月平均值
# 2021-01-31    0.019962
# 2021-02-28   -0.121787
# 2021-03-31    0.507905
# dtype: float64

# 使用resample是更简单更快捷的方法
print(ts.resample('M').mean())
# 2021-01-31    0.019962
# 2021-02-28   -0.121787
# 2021-03-31    0.507905
# Freq: M, dtype: float64

时区处理

时区通常被表示为UTC的偏置。 在Python语言中,时区信息来源于第三方库pytz(可以使用pip或conda安装),其中公开了Olson数据库,这是世界时区信息的汇编。 pandas封装了pytz的功能。

from datetime import datetime, timedelta
import pandas as pd
import numpy as np
from pandas.tseries.offsets import Hour, Minute, Day, MonthEnd
import pytz

common_timezones

tz = pytz.common_timezones[-5:]  # 读取common_timezones这个列表的最后5个元素
print(tz)
# ['US/Eastern', 'US/Hawaii', 'US/Mountain', 'US/Pacific', 'UTC']

要获得pytz的时区对象,可使用pytz.timezone:

tz = pytz.timezone('Asia/Shanghai')
print(tz)

时区的本地化和转换

默认情况下,pandas中的时间序列是时区简单型的。

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

print(rng)
# DatetimeIndex(['2021-01-01 09:30:00', '2021-01-02 09:30:00',
#                '2021-01-03 09:30:00', '2021-01-04 09:30:00',
#                '2021-01-05 09:30:00', '2021-01-06 09:30:00'],
#               dtype='datetime64[ns]', freq='D')
print(ts)
# 2021-01-01 09:30:00    0.339822
# 2021-01-02 09:30:00    1.356382
# 2021-01-03 09:30:00    0.475429
# 2021-01-04 09:30:00    1.826654
# 2021-01-05 09:30:00   -0.245510
# 2021-01-06 09:30:00    0.705274
# Freq: D, dtype: float64

print(ts.index.tz)  # 索引的tz属性是None
# None

日期范围可以通过时区集合来生成:

rng = pd.date_range('2021/3/1', periods=10, freq='D', tz='UTC')
print(rng)
# DatetimeIndex(['2021-03-01 00:00:00+00:00', '2021-03-02 00:00:00+00:00',
#                '2021-03-03 00:00:00+00:00', '2021-03-04 00:00:00+00:00',
#                '2021-03-05 00:00:00+00:00', '2021-03-06 00:00:00+00:00',
#                '2021-03-07 00:00:00+00:00', '2021-03-08 00:00:00+00:00',
#                '2021-03-09 00:00:00+00:00', '2021-03-10 00:00:00+00:00'],
#               dtype='datetime64[ns, UTC]', freq='D')

使用tz_localize方法可以从简单时区转换到本地化时区:

print(ts)
# 2021-01-01 09:30:00    0.294647
# 2021-01-02 09:30:00    0.958414
# 2021-01-03 09:30:00    0.424235
# 2021-01-04 09:30:00   -1.714333
# 2021-01-05 09:30:00   -0.030319
# 2021-01-06 09:30:00   -0.744940
# Freq: D, dtype: float64

print(ts.tz_localize('UTC'))
# 2021-01-01 09:30:00+00:00    0.294647
# 2021-01-02 09:30:00+00:00    0.958414
# 2021-01-03 09:30:00+00:00    0.424235
# 2021-01-04 09:30:00+00:00   -1.714333
# 2021-01-05 09:30:00+00:00   -0.030319
# 2021-01-06 09:30:00+00:00   -0.744940
# Freq: D, dtype: float64

print(ts.tz_localize('Asia/Shanghai'))
# 2021-01-01 09:30:00+08:00    0.052521
# 2021-01-02 09:30:00+08:00   -0.305417
# 2021-01-03 09:30:00+08:00    0.150215
# 2021-01-04 09:30:00+08:00   -0.953715
# 2021-01-05 09:30:00+08:00    0.543622
# 2021-01-06 09:30:00+08:00    0.222422
# dtype: float64

print(ts.tz_localize('Asia/Shanghai').index)
# DatetimeIndex(['2021-01-01 09:30:00+08:00', '2021-01-02 09:30:00+08:00',
#                '2021-01-03 09:30:00+08:00', '2021-01-04 09:30:00+08:00',
#                '2021-01-05 09:30:00+08:00', '2021-01-06 09:30:00+08:00'],
#               dtype='datetime64[ns, Asia/Shanghai]', freq=None)

一旦时间序列被本地化为某个特定的时区,则可以通过tz_convert将其转换为另一个时区:

tz_sha = ts.tz_localize('Asia/Shanghai')
tz_utc = tz_sha.tz_convert('UTC')
print(tz_sha)
# 2021-01-01 09:30:00+08:00    0.095689
# 2021-01-02 09:30:00+08:00   -0.392730
# 2021-01-03 09:30:00+08:00    0.151468
# 2021-01-04 09:30:00+08:00    0.027467
# 2021-01-05 09:30:00+08:00    0.393709
# 2021-01-06 09:30:00+08:00    0.872914
# dtype: float64
print(tz_utc)
# 2021-01-01 01:30:00+00:00    0.095689
# 2021-01-02 01:30:00+00:00   -0.392730
# 2021-01-03 01:30:00+00:00    0.151468
# 2021-01-04 01:30:00+00:00    0.027467
# 2021-01-05 01:30:00+00:00    0.393709
# 2021-01-06 01:30:00+00:00    0.872914
# dtype: float64

# tz_localize和tz_convert也是DatetimeIndex的实例方法:
print(ts.index.tz_localize('Asia/Shanghai'))
# DatetimeIndex(['2021-01-01 09:30:00+08:00', '2021-01-02 09:30:00+08:00',
#                '2021-01-03 09:30:00+08:00', '2021-01-04 09:30:00+08:00',
#                '2021-01-05 09:30:00+08:00', '2021-01-06 09:30:00+08:00'],
#               dtype='datetime64[ns, Asia/Shanghai]', freq=None)

时区感知时间戳对象的操作

与时间序列和日期范围类似,单独的Timestamp对象也可以从简单时间戳本地化为时区感知时间戳,并从一个时区转换为另一个时区:

stamp = pd.Timestamp('2021-5-1 05:30')
print(stamp)
# 2021-05-01 05:30:00

stamp_utc = stamp.tz_localize('utc')
print(stamp_utc)
# 2021-05-01 05:30:00+00:00

stamp_sha = stamp_utc.tz_convert('Asia/Shanghai')
print(stamp_sha)
# 2021-05-01 13:30:00+08:00

也可以在创建Timestamp的时候传递一个时区:

stamp_sha = pd.Timestamp('2021-5-1 05:30', tz='Asia/Shanghai')
print(stamp_sha)
# 2021-05-01 05:30:00+08:00

Timestamp对象内部存储了一个Unix纪元(1970年1月1日)至今的纳秒数量UTC时间戳数值,该数值在时区转换中是不变的:

print(stamp_utc.value)
# 1619847000000000000
print(stamp_utc.tz_convert('Asia/Shanghai').value)
# 1619847000000000000

在使用pandas的DateOffset进行时间算术时,pandas尽可能遵从夏时制。

首先,构造转换到DST之前的30分钟的时间:

stamp = pd.Timestamp('2012-3-12 1:30', tz='US/Eastern')
print(stamp)
# 2012-03-12 01:30:00-04:00
print(stamp + Hour())
# 2012-03-12 02:30:00-04:00

之后,构建从DST进行转换前的90分钟:

stamp = pd.Timestamp('2012-11-04 0:30-04:00', tz='US/Eastern')
print(stamp)
# 2012-11-04 00:30:00-04:00
print(stamp + 2 * Hour())  # 只增加了一小时
# 2012-11-04 01:30:00-05:00

不同时区间的操作

如果两个时区不同的时间序列需要联合,那么结果将是UTC时间的,因为时间戳以UTC格式存储。

rng = pd.date_range('2021/1/1 9:30', periods=9, freq='B')
ts = pd.Series(np.random.randn(len(rng)), index=rng)
print(ts)
# 2021-01-01 09:30:00    0.715681
# 2021-01-04 09:30:00    0.524563
# 2021-01-05 09:30:00   -0.482199
# 2021-01-06 09:30:00   -0.661303
# 2021-01-07 09:30:00    1.750010
# 2021-01-08 09:30:00    0.251478
# 2021-01-11 09:30:00   -1.487268
# 2021-01-12 09:30:00   -0.224024
# 2021-01-13 09:30:00   -1.621853
# Freq: B, dtype: float64

ts1 = ts[:7].tz_localize('Europe/London')
ts2 = ts1[2:].tz_convert('Europe/Moscow')
result = ts1 + ts2
print(ts1)
# 2021-01-01 09:30:00+00:00   -1.393445
# 2021-01-04 09:30:00+00:00   -1.179614
# 2021-01-05 09:30:00+00:00    0.716669
# 2021-01-06 09:30:00+00:00   -0.485656
# 2021-01-07 09:30:00+00:00    0.433000
# 2021-01-08 09:30:00+00:00    1.540745
# 2021-01-11 09:30:00+00:00    0.343751
# dtype: float64
print(ts2)
# 2021-01-05 12:30:00+03:00    0.716669
# 2021-01-06 12:30:00+03:00   -0.485656
# 2021-01-07 12:30:00+03:00    0.433000
# 2021-01-08 12:30:00+03:00    1.540745
# 2021-01-11 12:30:00+03:00    0.343751
# dtype: float64
print(result)
# 2021-01-01 09:30:00+00:00         NaN
# 2021-01-04 09:30:00+00:00         NaN
# 2021-01-05 09:30:00+00:00    1.433337
# 2021-01-06 09:30:00+00:00   -0.971312
# 2021-01-07 09:30:00+00:00    0.866000
# 2021-01-08 09:30:00+00:00    3.081489
# 2021-01-11 09:30:00+00:00    0.687502
# dtype: float64

时间区间和区间算术

from datetime import datetime, timedelta
import pandas as pd
import numpy as np
from pandas.tseries.offsets import Hour, Minute, Day, MonthEnd
import pytz

时间区间表示的是时间范围通过原索引1~202,把yearquarter联合起来,生成新索引,并替换原索引,比如一些天、一些月、一些季度或者是一些年。

Period类表示的正是这种数据类型,需要一个字符串或数字以及频率。 在这个例子中,Period对象表示的是从2007年1月1日到2007年12月31日(包含在内)的时间段。 在时间段上增加或减去整数可以方便地根据它们的频率进行移位。

p = pd.Period(2020, freq='A-DEC')
print(p)
# 2020
print(p + 5)
# 2025
print(p - 5)
# 2015

如果两个区间拥有相同的频率,则它们的差是它们之间的单位数。

p1 = pd.Period(2020, freq='A-DEC')
p2 = pd.Period(2010, freq='A-DEC')
print(p1 - p2)
# <10 * YearEnds: month=12>

p1 = pd.Period(2020, freq='Q-DEC')
p2 = pd.Period(2010, freq='Q-DEC')
print(p1 - p2)
# <40 * QuarterEnds: startingMonth=12>

使用period_range函数可以构造规则区间序列。PeriodIndex类存储的是区间的序列,可以作为任意pandas数据结构的轴索引。

data = np.random.randn(6)
strings = ['2021Q1', '2021Q2', '2021Q3', '2021Q4', '2022Q1', '2022Q2']

rng = pd.period_range('2001-1-1', '2001-6-30', freq='M')
ts = pd.Series(data, index=rng)
print(ts)
# 2001-01   -0.481408
# 2001-02   -0.297590
# 2001-03   -0.860354
# 2001-04    1.281540
# 2001-05    1.036551
# 2001-06   -0.522592
# Freq: M, dtype: float64

rng = pd.PeriodIndex(strings, freq='Q-DEC')  # 字符串数组也可以使用PeriodIndex类
ts = pd.Series(data, index=rng)
print(ts)
# 2021Q1   -2.077200
# 2021Q2   -0.948796
# 2021Q3   -1.104737
# 2021Q4    0.090281
# 2022Q1    0.431517
# 2022Q2    1.537045
# Freq: Q-DEC, dtype: float64

区间频率转换

使用asfreq可以将区间和PeriodIndex对象转换为其他的频率。

例如,假设我们有一个年度区间,并且想要在一年的开始或结束时将其转换为月度区间。 可以将Period('2020', 'A-DEC')看作一段时间中的一种游标,将时间按月份划分。

p = pd.Period(2020, freq='A-DEC')
print(p.asfreq('M', how='start'))
# 2020-01
print(p.asfreq('M', how='end'))
# 2020-12

如果财年结束不在12月,则每月分期会自动调整。 按当年财年结束计算,起始年份就是上一年了。

p = pd.Period(2020, freq='A-JUN')
print(p.asfreq('M', how='start'))
# 2019-07
print(p.asfreq('M', how='end'))
# 2020-06

当从高频率向低频率转换时,pandas根据子区间的"所属"来决定父区间。 例如,在A-JUN频率中,Aug-2020是2020区间的一部分:

print(p.asfreq('A-JUN'))

2020通过原索引1~202,把yearquarter联合起来,生成新索引,并替换原索引。

完整的PeriodIndex对象或时间序列可以按照相同的语义进行转换:

rng = pd.period_range('2018', '2021', freq='A-DEC')
data = np.random.randn(len(rng))
ts = pd.Series(data, index=rng)

print(ts)
# 2018    0.221634
# 2019   -0.392724
# 2020   -0.355022
# 2021    0.114000
# Freq: A-DEC, dtype: float64

下面年度区间将通过asfreq被替换为对应于每个年度区间内的第一个月的月度区间。

print(ts.asfreq('M', how='start'))
# 2018-01    0.681874
# 2019-01   -1.006585
# 2020-01   -0.619142
# 2021-01    1.445820
# Freq: M, dtype: float64

如果我们想要每年最后一个工作日,我们可以使用B频率来表示我们想要的是区间的末端。

print(ts.asfreq('B', how='end'))
# 2018-12-31   -1.520316
# 2019-12-31   -0.425544
# 2020-12-31   -0.658073
# 2021-12-31    1.206881
# Freq: B, dtype: float64

季度区间频率

季度数据是会计、金融和其他领域的标准。 很多季度数据是在财年结尾报告的,通常是一年12个月中的最后一个日历日或工作日。 pandas支持所有的可能的12个季度频率从Q-JAN到Q-DEC:

下例中,财年结束于1月,2020Q4行时间为上一年11月至当年1月。可以通过转换为每日频率(asfreq)进行检查。

p = pd.Period('2020Q4', freq='Q-JAN')
print(p)
# 2020Q4
print(p.asfreq('D', 'start'))
# 2019-11-01
print(p.asfreq('D', 'end'))
# 2020-01-31

假如财年结束于2月,2020Q4行时间为上一年12月至当年2月。

p = pd.Period('2020Q4', freq='Q-FEB')
print(p)
# 2020Q4
print(p.asfreq('D', 'start'))
# 2019-11-01
print(p.asfreq('D', 'end'))
# 2020-01-31

假如财年结束于4月,2020Q4行时间为上一年12月至当年2月。

p = pd.Period('2020Q4', freq='Q-APR')
print(p)
# 2020Q4
print(p.asfreq('D', 'start'))
# 2020-02-01
print(p.asfreq('D', 'end'))
# 2020-04-30

可以对区间数据做算术操作。例如,要获取在季度倒数第二个工作日下午4点的时间戳,可以这么做:(疑问:这里的参数e代表什么 ???)

p4pm = (p.asfreq('B', 'e') - 1).asfreq('T', 's') + 16 * 60
print(p4pm)
# 2020-04-29 16:00
print(p4pm.to_timestamp())
# 2020-04-29 16:00:00

可以使用peroid_range生成季度序列。它的算术也是一样的:

rng = pd.period_range('2000Q3', '2001Q4', freq='Q-JAN')
ts = pd.Series(np.arange(len(rng)), index=rng)
print(ts)
# 2000Q3    0
# 2000Q4    1
# 2001Q1    2
# 2001Q2    3
# 2001Q3    4
# 2001Q4    5
# Freq: Q-JAN, dtype: int64
new_rng = (rng.asfreq('B', 'e') - 1).asfreq('T', 's') + 16 * 60
ts.index = new_rng.to_timestamp()
print(ts)
# 1999-10-28 16:00:00    0
# 2000-01-28 16:00:00    1
# 2000-04-27 16:00:00    2
# 2000-07-28 16:00:00    3
# 2000-10-30 16:00:00    4
# 2001-01-30 16:00:00    5
# dtype: int64

将时间戳转换为区间(以及逆转换)

通过时间戳索引的Series和DataFrame可以被to_period方法转换为区间:

rng = pd.date_range('2020-01-01', periods=3, freq='M')
ts = pd.Series(np.random.randn(3), index=rng)
print(ts)
# 2020-01-31   -0.567097
# 2020-02-29    0.63452通过原索引1~202,把year和quarter联合起来,生成新索引,并替换原索引2
# 2020-03-31    0.297777
# Freq: M, dtype: float64
pts = ts.to_period()
print(pts)
# 2020-01   -0.567097
# 2020-02    0.634522
# 2020-03    0.297777
# Freq: M, dtype: float64

由于区间是非重叠时间范围,一个时间戳只能属于给定频率的单个区间。 尽管默认情况下根据时间戳推断出新PeriodIndex的频率,但可以指定任何想要的频率。 在结果中包含重复的区间也是没有问题的。

rng = pd.date_range('2020-01-01', periods=6, freq='D')
ts = pd.Series(np.random.randn(6), index=rng)
print(ts)
# 2020-01-01   -0.111287
# 2020-01-02    1.442234
# 2020-01-03   -0.767553
# 2020-01-04   -0.265064
# 2020-01-05    1.200312
# 2020-01-06   -1.782557
# Freq: D, dtype: float64
ts_m = ts.to_period('M')  # 指定period的频率(M),输出结果包含重复period
print(ts_m)
# 2020-01   -0.111287
# 2020-01    1.442234
# 2020-01   -0.767553
# 2020-01   -0.265064
# 2020-01    1.200312
# 2020-01   -1.782557
# Freq: M, dtype: float64

使用to_timestamp可以将区间再转换为时间戳:

print(ts_m.to_timestamp(how='end'))
# 2020-01-31 23:59:59.999999999   -0.111287
# 2020-01-31 23:59:59.999999999    1.442234
# 2020-01-31 23:59:59.999999999   -0.767553
# 2020-01-31 23:59:59.999999999   -0.265064
# 2020-01-31 23:59:59.999999999    1.200312
# 2020-01-31 23:59:59.999999999   -1.782557
# dtype: float64
print(ts_m.to_timestamp(how='start'))
# 2020-01-01   -0.111287
# 2020-01-01    1.442234
# 2020-01-01   -0.767553
# 2020-01-01   -0.265064
# 2020-01-01    1.200312
# 2020-01-01   -1.782557
# dtype: float64

从数组生成PeriodIndex

固定频率数据集有时存储在跨越多列的时间范围信息中。例如,在这个宏观经济数据集中,年份和季度在不同列中:

data = pd.read_csv('../examples/macrodata.csv')
print(data.head(5))
#      year  quarter   realgdp  realcons  ...  unemp      pop  infl  realint
# 0  1959.0      1.0  2710.349    1707.4  ...    5.8  177.146  0.00     0.00
# 1  1959.0      2.0  2778.801    1733.7  ...    5.1  177.830  2.34     0.74
# 2  1959.0      3.0  2775.488    1751.8  ...    5.3  178.657  2.74     1.09
# 3  1959.0      4.0  2785.204    1753.7  ...    5.6  179.386  0.27     4.06
# 4  1960.0      1.0  2847.699    1770.5  ...    5.2  180.007  2.31     1.19
print(data.year)
# 0      1959.0
# 1      1959.0
# 2      1959.0
# 3      1959.0
# 4      1960.0
#         ...
# 198    2008.0
# 199    2008.0
# 200    2009.0
# 201    2009.0
# 202    2009.0
# Name: year, Length: 203, dtype: float64
print(data.quarter)
# 0      1.0
# 1      2.0
# 2      3.0
# 3      4.0
# 4      1.0
#       ...
# 198    3.0
# 199    4.0
# 200    1.0
# 201    2.0
# 202    3.0
# Name: quarter, Length: 203, dtype: float64

通过将这些数组和频率传递给PeriodIndex,可以联合形成DataFrame的索引

index = pd.PeriodIndex(year=data.year, quarter=data.quarter, freq='Q-DEC')
print(index)
# PeriodIndex(['1959Q1', '1959Q2', '1959Q3', '1959Q4', '1960Q1', '1960Q2',
#              '1960Q3', '1960Q4', '1961Q1', '1961Q2',
#              ...
#              '2007Q2', '2007Q3', '2007Q4', '2008Q1', '2008Q2', '2008Q3',
#              '2008Q4', '2009Q1', '2009Q2', '2009Q3'],
#             dtype='period[Q-DEC]', length=203)
data.index = index  # 通过原索引1~202,把year和quarter联合起来,生成新索引,并替换原索引
print(data.infl)
# 1959Q1    0.00
# 1959Q2    2.34
# 1959Q3    2.74
# 1959Q4    0.27
# 1960Q1    2.31
#           ...
# 2008Q3   -3.16
# 2008Q4   -8.79
# 2009Q1    0.94
# 2009Q2    3.37
# 2009Q3    3.56
# Freq: Q-DEC, Name: infl, Length: 203, dtype: float64

重新采样频率转换

import pandas as pd
import numpy as np
from pandas.tseries.frequencies import to_offset

重新采样是指将时间序列从一个频率转换为另一个频率的过程。 将更高频率的数据聚合到低频率被称为向下采样,而从低频率转换到高频率称为向上采样。 并不是所有的重新采样都属于上面说的两类。例如,将W-WED(weekly on Wednesday,每周三)转换到W-FRI(每周五)既不是向上采样也不是向下采样。

pandas对象都配有resample方法,该方法是所有频率转换的工具函数。resample拥有类似于groupby的API;调用resample对数据分组,之后再调用聚合函数:

resample方法参数

参数

  • freq: 表示重采样频率,例如‘M'、‘5min',Second(15)
  • how='mean': 用于产生聚合值的函数名或数组函数,例如‘mean'、‘ohlc'、np.max等,默认是‘mean',其他常用的值由:‘first'、‘last'、‘median'、‘max'、‘min'
  • axis=0: 默认是纵轴,横轴设置axis=1
  • fill_method = None: 升采样时如何插值,比如‘ffill'、‘bfill'等
  • closed = ‘right': 在降采样时,各时间段的哪一段是闭合的,‘right'或‘left',默认‘right'
  • label= ‘right': 在降采样时,如何设置聚合值的标签,例如,9:30-9:35会被标记成9:30还是9:35,默认9:35
  • loffset = None: 面元标签的时间校正值,比如‘-1s'或Second(-1)用于将聚合标签调早1秒
  • limit=None: 在向前或向后填充时,允许填充的最大时期数
  • kind = None: 聚合到时期(‘period')或时间戳(‘timestamp'),默认聚合到时间序列的索引类型
  • convention = None: 当重采样时期时,将低频率转换到高频率所采用的约定(start或end)。默认‘end'
rng = pd.date_range('2020-1-1', periods=100, freq='D')
ts = pd.Series(np.random.randn(len(rng)), index=rng)
print(ts)
# 2020-01-01    0.802409
# 2020-01-02   -1.147130
# 2020-01-03   -1.076115
# 2020-01-04   -2.097443
# 2020-01-05    0.577671
#                 ...
# 2020-04-05   -0.110747
# 2020-04-06    0.132867
# 2020-04-07   -0.294061
# 2020-04-08   -0.246155
# 2020-04-09    0.927194
# Freq: D, Length: 100, dtype: float64
print(ts.resample('M'))
# DatetimeIndexResampler [freq=<MonthEnd>, axis=0, closed=right, label=right, convention=start, origin=start_day]
print(ts.resample('M').mean())  # 把100天的数据按月groupby,并输出月末最后一天,计算平均值
# 2020-01-31   -0.311714
# 2020-02-29    0.121526
# 2020-03-31   -0.051131
# 2020-04-30   -0.273113
# Freq: M, dtype: float64
print(ts.resample('M', kind='period').mean())  # # 把100天的数据按月groupby,并输出月份(参数period),计算平均值
# 2020-01   -0.311714
# 2020-02    0.121526
# 2020-03   -0.051131
# 2020-04   -0.273113
# Freq: M, dtype: float64

向下采样

将数据聚合到一个规则的低频率上是一个常见的时间序列任务。 要聚合的数据不必是固定频率的。 期望的频率定义了用于对时间序列切片以聚合的箱体边界。例如,要将时间转换为每月,MBM,则需要将数据分成一个月的时间间隔。 每个间隔是半闭合的,一个数据点只能属于一个时间间隔,时间间隔的并集必须是整个时间帧。

在使用resample进行向下采样数据时有些事情需要考虑:

  • 每段间隔的哪一边是闭合的。
  • 如何在间隔的起始或结束位置标记每个已聚合的箱体。
rng = pd.date_range('2020-1-1', periods=12, freq='T')
ts = pd.Series(np.arange(12), index=rng)
print(ts)
# 2020-01-01 00:00:00     0
# 2020-01-01 00:01:00     1
# 2020-01-01 00:02:00     2
# 2020-01-01 00:03:00     3
# 2020-01-01 00:04:00     4
# 2020-01-01 00:05:00     5
# 2020-01-01 00:06:00     6
# 2020-01-01 00:07:00     7
# 2020-01-01 00:08:00     8
# 2020-01-01 00:09:00     9
# 2020-01-01 00:10:00    10
# 2020-01-01 00:11:00    11
# Freq: T, dtype: int64

按五分钟频率聚合分组,计算每一组的加和。频率按五分钟的增量定义了箱体边界。 默认情况下,左箱体边界是包含的,因此00:00的值是包含在00:00到00:05间隔内的。 传递closed='right'将间隔的闭合端改为了右边。

分组:

  • left: [00:00,00:01,00:02,00:03,00:04],[00:05,00:06,00:07,00:08,00:09],[00:10,00:11]
  • right:[00:00],[00:01,00:02,00:03,00:04,00:05],[00:06,00:07,00:08,00:09,00:10],[00:11]
result = ts.resample('5min', closed='right').sum()
print(result)
# 2019-12-31 23:55:00     0
# 2020-01-01 00:00:00    15
# 2020-01-01 00:05:00    40
# 2020-01-01 00:10:00    11
# Freq: 5T, dtype: int64
result = ts.resample('5min', closed='left').sum()
print(result)
# 2020-01-01 00:00:00    10
# 2020-01-01 00:05:00    35
# 2020-01-01 00:10:00    21
# Freq: 5T, dtype: int64

最后,将结果索引移动一定的数量,例如从右边缘减去一秒,以使其更清楚地表明时间戳所指的间隔。 要实现这个功能,向loffset传递字符串或日期偏置:

result = ts.resample('5min', closed='right', label='right', loffset='-1s').sum()
print(result)
# 2019-12-31 23:59:59     0
# 2020-01-01 00:04:59    15
# 2020-01-01 00:09:59    40
# 2020-01-01 00:14:59    11
# Freq: 5T, dtype: int64
# FutureWarning: 'loffset' in .resample() and in Grouper() is deprecated.
# >>> df.resample(freq="3s", loffset="8H")
# becomes:
# >>> from pandas.tseries.frequencies import to_offset
# >>> df = df.resample(freq="3s").mean()
# >>> df.index = df.index.to_timestamp() + to_offset("8H")

开端-峰值-谷值-结束(OHLC)重新采样

在金融中,为每个数据桶计算四个值是一种流行的时间序列聚合方法:第一个值(开端)、最后一个值(结束)、最大值(峰值)和最小值(谷值)。 通过使用ohlc聚合函数取得包含四种聚合值列的DataFrame,这些值在数据的单次扫描中被高效计算:

result = ts.resample('5min').ohlc()
print(result)
#                      open  high  low  close
# 2020-01-01 00:00:00     0     4    0      4
# 2020-01-01 00:05:00     5     9    5      9
# 2020-01-01 00:10:00    10    11   10     11

向上采样与插值

当从低频率转换为高频率时,并不需要任何聚合。

df = pd.DataFrame(
    np.random.randn(2, 4),
    index=pd.date_range('2020/1/1', periods=2, freq='W-WED'),
    columns=['Colorado', 'Texas', 'New York', 'Ohio']
)
print(df)
#             Colorado     Texas  New York      Ohio
# 2020-01-01 -0.228758 -0.758718 -0.025410 -1.001819
# 2020-01-08 -0.704541 -0.261414 -0.863335  0.267101

df_daily = df.resample('W-WED').sum()
print(df_daily)
#             Colorado     Texas  New York      Ohio
# 2020-01-01 -0.228758 -0.758718 -0.025410 -1.001819
# 2020-01-08 -0.704541 -0.261414 -0.863335  0.267101

df_daily = df.resample('D').sum()
print(df_daily)
#             Colorado     Texas  New York      Ohio
# 2020-01-01 -0.228758 -0.758718 -0.025410 -1.001819
# 2020-01-02  0.000000  0.000000  0.000000  0.000000
# 2020-01-03  0.000000  0.000000  0.000000  0.000000
# 2020-01-04  0.000000  0.000000  0.000000  0.000000
# 2020-01-05  0.000000  0.000000  0.000000  0.000000
# 2020-01-06  0.000000  0.000000  0.000000  0.000000
# 2020-01-07  0.000000  0.000000  0.000000  0.000000
# 2020-01-08 -0.704541 -0.261414 -0.863335  0.267101

当对这些数据使用聚合函数时,每一组只有一个值,并且会在间隙中产生缺失值。 使用asfreq方法在不聚合的情况下转换到高频率:

df_daily = df.resample('D').asfreq()
print(df_daily)
#             Colorado     Texas  New York      Ohio
# 2020-01-01 -0.228758 -0.758718 -0.025410 -1.001819
# 2020-01-02       NaN       NaN       NaN       NaN
# 2020-01-03       NaN       NaN       NaN       NaN
# 2020-01-04       NaN       NaN       NaN       NaN
# 2020-01-05       NaN       NaN       NaN       NaN
# 2020-01-06       NaN       NaN       NaN       NaN
# 2020-01-07       NaN       NaN       NaN       NaN
# 2020-01-08 -0.704541 -0.261414 -0.863335  0.267101

在非星期三的日期上向前填充每周数值。fillnareindex方法中可用的填充或插值方法可用于重采样:

df_daily = df.resample('D').ffill()
print(df_daily)
#             Colorado     Texas  New York      Ohio
# 2020-01-01 -0.228758 -0.758718 -0.025410 -1.001819
# 2020-01-02 -0.228758 -0.758718 -0.025410 -1.001819
# 2020-01-03 -0.228758 -0.758718 -0.025410 -1.001819
# 2020-01-04 -0.228758 -0.758718 -0.025410 -1.001819
# 2020-01-05 -0.228758 -0.758718 -0.025410 -1.001819
# 2020-01-06 -0.228758 -0.758718 -0.025410 -1.001819
# 2020-01-07 -0.228758 -0.758718 -0.025410 -1.001819
# 2020-01-08 -0.704541 -0.261414 -0.863335  0.267101

可以同样选择仅向前填充一定数量的区间,以限制继续使用观测值的时距:

df_daily = df.resample('D').ffill(limit=2)
print(df_daily)
#             Colorado     Texas  New York      Ohio
# 2020-01-01 -0.228758 -0.758718 -0.025410 -1.001819
# 2020-01-02 -0.228758 -0.758718 -0.025410 -1.001819
# 2020-01-03 -0.228758 -0.758718 -0.025410 -1.001819
# 2020-01-04       NaN       NaN       NaN       NaN
# 2020-01-05       NaN       NaN       NaN       NaN
# 2020-01-06       NaN       NaN       NaN       NaN
# 2020-01-07       NaN       NaN       NaN       NaN
# 2020-01-08 -0.704541 -0.261414 -0.863335  0.267101

注意,新的日期索引不需要与旧的索引重叠,和原来df的值一样,只是日期索引变了。

df_new = df.resample('W-THU').ffill()
print(df_new)
#             Colorado     Texas  New York      Ohio
# 2020-01-02 -0.228758 -0.758718 -0.025410 -1.001819
# 2020-01-09 -0.704541 -0.261414 -0.863335  0.267101

使用区间进行重新采样

对以区间为索引的数据进行采样与时间戳的情况类似:

df = pd.DataFrame(
    np.random.randn(24, 4),
    index=pd.period_range('2020-1', periods=24, freq='M'),
    columns=['Colorado', 'Texas', 'New York', 'Ohio']
)
print(df)
# 2020-01  0.721395 -1.492674  0.707410  1.641890
# 2020-02 -0.894880  0.032823 -0.676158  0.029203
# 2020-03  2.147365 -0.176796  0.562695 -0.747656
# 2020-04  1.496037 -0.797119 -0.495601  0.774147
# 2020-05 -0.309839  0.502563  0.237244  0.910624
# 2020-06  1.231869 -0.105227  1.315759  0.217701
# 2020-07  1.447419  0.263876 -0.342045 -0.768907
# 2020-08 -2.567162 -1.008827  0.391085  1.259560
# 2020-09 -0.772501  1.183532  0.450374  0.450714
# 2020-10  0.228974  0.461224  1.393178  0.175243
# 2020-11 -0.725193 -1.544131  1.372029 -0.659224
# 2020-12  0.718195  0.862024 -0.166460 -0.940191
# 2021-01 -0.617054 -0.887312  0.338451 -1.392838
# 2021-02 -0.081140  0.634730 -0.868051 -1.277167
# 2021-03 -0.999642 -1.959715 -0.930662  0.748687
# 2021-04  1.851453  1.561669 -0.688822 -0.371255
# 2021-05 -0.540777 -0.890403 -1.204188  0.243480
# 2021-06  1.318905  1.247457  0.518969  0.799793
# 2021-07  0.223238  0.747177 -0.410889  0.904593
# 2021-08 -0.652551 -0.254351 -0.464604 -0.676923
# 2021-09  0.562312  0.182099  0.018617  0.573331
# 2021-10  0.429490 -0.045959 -0.356292 -0.295776
# 2021-11  2.552155  0.801299  1.378421  1.232792
# 2021-12  1.102288  0.850280 -0.767015 -0.519840

df_annual = df.resample('A-DEC').mean()
print(df_annual)
#       Colorado     Texas  New York      Ohio
# 2020  0.226807 -0.151561  0.395793  0.195259
# 2021  0.429056  0.165581 -0.286339 -0.002594

向上采样更为细致,因为必须在重新采样前决定新频率中在时间段的哪一端放置数值,就像asfreq方法一样。 convention参数默认值是start,但也可以是end

result = df_annual.resample('Q-DEC').ffill()
print(result)
#         Colorado     Texas  New York      Ohio
# 2020Q1  0.226807 -0.151561  0.395793  0.195259
# 2020Q2  0.226807 -0.151561  0.395793  0.195259
# 2020Q3  0.226807 -0.151561  0.395793  0.195259
# 2020Q4  0.226807 -0.151561  0.395793  0.195259
# 2021Q1  0.429056  0.165581 -0.286339 -0.002594
# 2021Q2  0.429056  0.165581 -0.286339 -0.002594
# 2021Q3  0.429056  0.165581 -0.286339 -0.002594
# 2021Q4  0.429056  0.165581 -0.286339 -0.002594

result = df_annual.resample('Q-DEC', convention='end').ffill()
print(result)
#         Colorado     Texas  New York      Ohio
# 2020Q4  0.226807 -0.151561  0.395793  0.195259
# 2021Q1  0.226807 -0.151561  0.395793  0.195259
# 2021Q2  0.226807 -0.151561  0.395793  0.195259
# 2021Q3  0.226807 -0.151561  0.395793  0.195259
# 2021Q4  0.429056  0.165581 -0.286339 -0.002594

由于区间涉及时间范围,向上采样和向下采样就更为严格:

  • 在向下采样中,目标频率必须是原频率的子区间。
  • 在向上采样中,目标频率必须是原频率的父区间。

如果不满足这些规则,将会引起异常。这主要会影响每季度、每年和每周的频率。

例如,根据Q-MAR定义的时间范围将只和A-MAR、A-JUN、A-SEP和A-DEC保持一致:

result = df_annual.resample('Q-MAR').ffill()
print(result)
#         Colorado     Texas  New York      Ohio
# 2020Q4  0.226807 -0.151561  0.395793  0.195259
# 2021Q1  0.226807 -0.151561  0.395793  0.195259
# 2021Q2  0.226807 -0.151561  0.395793  0.195259
# 2021Q3  0.226807 -0.151561  0.395793  0.195259
# 2021Q4  0.429056  0.165581 -0.286339 -0.002594
# 2022Q1  0.429056  0.165581 -0.286339 -0.002594
# 2022Q2  0.429056  0.165581 -0.286339 -0.002594
# 2022Q3  0.429056  0.165581 -0.286339 -0.002594

移动窗口函数

统计那些通过移动窗口或指数衰减而运行的函数,是用于时间序列操作的数组变换的一个重要类别。 这对平滑噪声或粗糙的数据非常有用。称这些函数为移动窗口函数,尽管它也包含了一些没有固定长度窗口的函数,比如指数加权移动平均。 与其他的统计函数类似,这些函数会自动排除缺失数据。

import matplotlib.pyplot as plt
import pandas as pd
from scipy.stats import percentileofscore
import numpy as np
from pandas.tseries.offsets import Hour, Minute, Day, MonthEnd
import pytz

在深入了解之前,我们可以先载入一些时间序列数据并按照工作日频率进行重新采样:

close_px_all = pd.read_csv(
    '../examples/stock_px_2.csv',
    parse_dates = True,
    index_col=0
)
print(close_px_all.head(5))
#             AAPL   MSFT    XOM     SPX
# 2003-01-02  7.40  21.11  29.22  909.03
# 2003-01-03  7.45  21.14  29.24  908.59
# 2003-01-06  7.45  21.52  29.96  929.01
# 2003-01-07  7.43  21.93  28.95  922.93
# 2003-01-08  7.28  21.31  28.83  909.93

close_px = close_px_all[
    ['AAPL', 'MSFT', 'XOM']
]
close_px = close_px.resample('B').ffill()
print(close_px)
#               AAPL   MSFT    XOM
# 2003-01-02    7.40  21.11  29.22
# 2003-01-03    7.45  21.14  29.24
# ...            ...    ...    ...
# 2011-10-13  408.43  27.18  76.37
# 2011-10-14  422.00  27.27  78.11
# [2292 rows x 3 columns]

rolling算子,它的行为与resamplegroupby类似。 rolling可以在Series或DataFrame上通过一个window(以一个区间的数字来表示)进行调用。

close_px.AAPL.plot()

表达式rolling(250)groupby的行为类似,但是它创建的对象是根据250日滑动窗口分组的而不是直接分组。 因此这里我们获得了苹果公司股票价格的250日移动窗口平均值。

close_px.AAPL.rolling(250).mean().plot()
plt.show()

默认情况下,滚动函数需要窗口中所有的值必须是非NA值。 由于存在缺失值这种行为会发生改变,尤其是在时间序列的起始位置你拥有的数据是少于窗口区间的

apple_std250 = close_px.AAPL.rolling(250, min_periods=10).std()  # 苹果公司250日每日返回标准差
print(apple_std250[5:12])
# 2003-01-09         NaN
# 2003-01-10         NaN
# 2003-01-13         NaN
# 2003-01-14         NaN
# 2003-01-15    0.077496
# 2003-01-16    0.074760
# 2003-01-17    0.112368
# Freq: B, Name: AAPL, dtype: float64
apple_std250.plot()
plt.show()

expanding_mean = apple_std250.expanding().mean()
print(expanding_mean[5:12])
# 2003-01-09         NaN
# 2003-01-10         NaN
# 2003-01-13         NaN
# 2003-01-14         NaN
# 2003-01-15    0.077496
# 2003-01-16    0.076128
# 2003-01-17    0.088208
# Freq: B, Name: AAPL, dtype: float64
expanding_mean.plot()
plt.show()

在DataFrame上调用一个移动窗口函数会将变换应用到每一列上:

close_px.rolling(60).mean().plot(logy=True)  # 股票价格60日MA(Y轴取对数)
plt.show()

rolling函数也接收表示固定大小的时间偏置字符串,而不只是一个区间的集合数字。 对不规则时间序列使用注释非常有用。这些字符串可以传递给resample

例如,我们可以像这样计算20天的滚动平均值:

result = close_px.rolling('20D').mean()
print(result)
#                   AAPL       MSFT        XOM
# 2003-01-02    7.400000  21.110000  29.220000
# ...                ...        ...        ...
# 2011-10-14  391.038000  26.048667  74.185333
# [2292 rows x 3 columns]
result.plot()
plt.show()

指数加权函数

指定一个常数衰减因子以向更多近期观测值提供更多权重,可以替代使用具有相等加权观察值的静态窗口尺寸的方法。 有多种方式可以指定衰减因子。其中一种流行的方式是使用一个span(跨度),这使得结果与窗口大小等于跨度的简单移动窗口函数。 由于指数加权统计值给更近期的观测值以更多的权重,与等权重的版本相比,它对变化“适应”得更快。

pandas拥有ewm算子,同rollingexpanding算子一起使用。

以下是将苹果公司股票价格的60日均线与span=60的EW移动平均线进行比较的例子:

aapl_ex = close_px.AAPL['2006':'2007']
ma60 = aapl_ex.rolling(30, min_periods=20).mean()
ewma60 = aapl_ex.ewm(span=30).mean()
ma60.plot(style='k--', label='Simple MA')
ewma60.plot(style='k-', label='EWMA')
plt.legend()
plt.show()

二元移动窗口函数

一些统计算子,例如相关度和协方差,需要操作两个时间序列。 例如,金融分析师经常对股票与基准指数(如标普500)的关联性感兴趣。 我们首先计算所有我们感兴趣的时间序列的百分比变化:

spx_px = close_px_all['SPX']
spx_rets = spx_px.pct_change()
returns = close_px.pct_change()
# 在调用rolling后,corr聚合函数可以根据spx_rets计算滚动相关性:
corr = returns.AAPL.rolling(125, min_periods=100).corr(spx_rets)  # 苹果公司与标普500的六个月的收益相关性
corr.plot()
plt.show()
corr = returns.rolling(125, min_periods=100).corr(spx_rets)  # 多只股票与标普500的六个月收益相关性
corr.plot()
plt.show()

用户自定义的移动窗口函数

rolling及其相关方法上使用apply方法提供了一种在移动窗口中应用你自己设计的数组函数的方法。 唯一的要求是该函数从每个数组中产生一个单值(缩聚)。

例如,尽管我们可以使用rolling(...).quantile(q)计算样本的分位数,但我们可能会对样本中特定值的百分位数感兴趣。 scipy.stats.percentileofscore函数就是实现这个功能的:

score_at_2percent = lambda x: percentileofscore(x, 0.02)
result = returns.AAPL.rolling(250).apply(score_at_2percent)  # 一年窗口下苹果公司股价2%收益的百分位等级
result.plot()
plt.show()
result = returns.rolling(250).apply(score_at_2percent)  # 一年窗口下所有公司股价2%收益的百分位等级
result.plot()
plt.show()