时间序列 ¶
日期和时间数据的类型及工具 ¶
时间序列数据在很多领域都是重要的结构化数据形式。在多个时间点观测或测量的数据形成了时间序列。
许多时间序列是固定频率的,也就是说数据是根据相同的规则定期出现的,例如每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标准库包含了日期和时间数据的类型。datetime
、time
和calendar
模块是开始处理时间数据的主要内容。 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.srtptime
和datetime
格式符,把字符串转换日期。 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
中的标量值是pandas
的Timestamp
对象:
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>
在大多数应用中,不需要显式地创建这些对象,而是使用字符串别名,如H
或4H
。在基础频率前放一个整数就可以生成倍数:
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日期偏置也可以使用datetime
或Timestamp
对象完成:
now = datetime(2021, 10, 9)
print(now)
# 2021-10-09 00:00:00
print(now + 3 * Day())
# 2021-10-12 00:00:00
锚定偏置可以使用rollforward
和rollback
分别显式地将日期向前或向后"滚动"。 如果添加了一个锚定偏置量,比如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,把year
和quarter
联合起来,生成新索引,并替换原索引,比如一些天、一些月、一些季度或者是一些年。
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,把year
和quarter
联合起来,生成新索引,并替换原索引。
完整的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
向下采样 ¶
将数据聚合到一个规则的低频率上是一个常见的时间序列任务。 要聚合的数据不必是固定频率的。 期望的频率定义了用于对时间序列切片以聚合的箱体边界。例如,要将时间转换为每月,M
或BM
,则需要将数据分成一个月的时间间隔。 每个间隔是半闭合的,一个数据点只能属于一个时间间隔,时间间隔的并集必须是整个时间帧。
在使用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
在非星期三的日期上向前填充每周数值。fillna
和reindex
方法中可用的填充或插值方法可用于重采样:
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
算子,它的行为与resample
和groupby
类似。 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
算子,同rolling
、expanding
算子一起使用。
以下是将苹果公司股票价格的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()