Pandas时间序列分析

Posted by Samson Yuen on 2023-10-10
Estimated Reading Time 42 Minutes
Words 8.1k In Total

Pandas时间序列分析

原生Python时间类型

datetime以及timedelta

原生Python中,datetime模块是经常用到的时间库,其中有datetime类型表示一个特定的时间点,timedelta类型,表示两个datetime类型的时间间隔

1
2
from datetime import datetime
from datetime import timedelta

datetime类型可以表示年月日时分秒微秒

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> In[]:
# 构造一个datetime类型
dt = datetime(2022,12,31,12,12,39,1234)
print(dt.year)
print(dt.month)
print(dt.day)
print(dt.hour)
print(dt.minute)
print(dt.second)
print(dt.microsecond)

>>> Out[]:
2022
12
31
12
12
39
1234
1
2
3
4
5
6
7
>>> In[]:
# 获取当前时间对象
now = datetime.now()
now

>>> Out[]:
datetime.datetime(2023, 9, 18, 21, 16, 33, 888535)

两个时间对象相减则可以获取二者之间的时间差对象。下列代码说明,这两个时间相差261天32643秒887301微秒。但是delta对象没有时分属性。

1
2
3
4
5
6
7
8
9
10
11
12
>>> In[]:
delta = now - dt
print(delta)
print(delta.days)
print(delta.seconds)
print(delta.microseconds)

>>> Out[]:
261 days, 9:03:54.887301
261
32634
887301
1
2
3
4
5
6
>>> In[]:
# 也可以手动构造一个时间差对象
timedelta(days = 200,seconds = 15,microseconds = 12355)


datetime.timedelta(days=200, seconds=15, microseconds=12355)

timedelta和datetime类型之间是可以进行加减算术运算的,得到另一个datetime对象

1
2
3
4
5
6
7
>>> In[]:
print(dt + delta)
print(dt + 2*delta)

>>> Out[]:
2023-09-18 21:16:33.888535
2024-06-06 06:20:28.775836

字符串与时间对象的转换

dateimte库中的strftime意为strformattime,其作用为将datetime对象转换为字符串形式,此时需要指明转换为字符串后的格式;strptime意为strparsertime,其作用为将字符串形式的日期转换为datetime对象,也需指明传入的字符串的日期格式。

常见的日期格式如下所示:

  • %Y:4位数年份
  • %y:2位数年份
  • %m:月份
  • %d:日期
  • %H:24小时制时
  • %I:12小时制时
  • %M:分
  • %S:秒
1
2
3
4
5
>>> In[]:
dt.strftime('%Y-%m-%d %H:%M:%S')

>>> Out[]:
'2022-12-31 12:12:39'
1
2
3
4
5
>>> In[]:
datetime.strptime('6/30/2022 12:30','%m/%d/%Y %H:%M')

>>> Out[]:
datetime.datetime(2022, 6, 30, 12, 30)

pandas时间序列

pandas中最基础的时间类型是时间戳Series,可以使用pd.to_datetime将字符串列表(array-like)转换为时间戳Series。可以发现,得到的结果是一个DatetimeIndex,这是一个广义上的Series对象,其中的每个元素都是一个时间戳对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> In[]:
import numpy as np
import pandas as pd
datestrs = ["2011-07-06 12:00:00", "2011-08-06 00:00:00"]
DTI = pd.to_datetime(datestrs)
DTI

>>> Out[]:
DatetimeIndex(['2011-07-06 12:00:00', '2011-08-06 00:00:00'], dtype='datetime64[ns]', freq=None)

>>> In[]:
DTI[0]

>>> Out[]:
Timestamp('2011-07-06 12:00:00')

pandas中,None也可以称为一个时间戳对象,使用NaT(Not a Time)对其进行表示,其类似于NaN,可以使用isna方法进行检测。

1
2
3
4
5
6
7
8
9
10
11
>>> In[]:
pd.to_datetime(["2011-07-06 12:00:00", "2011-08-06 00:00:00",None])

>>> Out[]:
DatetimeIndex(['2011-07-06 12:00:00', '2011-08-06 00:00:00', 'NaT'], dtype='datetime64[ns]', freq=None)

>>> In[]:
pd.isna(pd.to_datetime(["2011-07-06 12:00:00", "2011-08-06 00:00:00",None]))

>>> Out[]:
array([False, False, True])

除了字符串,python的datetime对象也适用于pd.to_datetime方法

1
2
3
4
5
6
7
8
9
>>> In[]:
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)]

pd.to_datetime(dates)

>>> Out[]:
DatetimeIndex(['2011-01-02', '2011-01-05', '2011-01-07', '2011-01-08','2011-01-10', '2011-01-12'],dtype='datetime64[ns]', freq=None)

DatetimeIndex对象一般用来作为一个数字Series或者DataFrame的索引

1
2
3
4
5
6
7
8
9
10
11
12
>>> In[]:
ts = pd.Series(np.random.randint(1,10,6),index=pd.to_datetime(dates))
ts

>>> Out[]:
2011-01-02 8
2011-01-05 2
2011-01-07 1
2011-01-08 2
2011-01-10 2
2011-01-12 5
dtype: int64

当然也可以不必这么麻烦,直接将dates作为索引也可以得到相同的效果

1
2
3
4
5
6
7
8
9
10
11
12
>>> In[]:
ts = pd.Series(np.random.randint(1,10,6),index=dates)
ts

>>> Out[]:
2011-01-02 1
2011-01-05 5
2011-01-07 7
2011-01-08 9
2011-01-10 8
2011-01-12 7
dtype: int64

但是不可以直接将字符串作为索引,此时得到的索引类型并不是DatetimeIndex类型

1
2
3
4
5
6
7
8
9
>>> In[]:
ts_str = pd.Series(np.random.randint(1,10,2),
index=["2011-07-06 12:00:00", "2011-08-06 00:00:00"])
ts_str

>>> Out[]:
2011-07-06 12:00:00 9
2011-08-06 00:00:00 6
dtype: int64

对比结果如下

1
2
3
4
5
6
7
8
9
>>> In[]:
ts_str.index
ts.index

>>> Out[]:
Index(['2011-07-06 12:00:00', '2011-08-06 00:00:00'], dtype='object')

DatetimeIndex(['2011-01-02', '2011-01-05', '2011-01-07', '2011-01-08','2011-01-10', '2011-01-12'],
dtype='datetime64[ns]', freq=None)

DatetimeInde对象的索引

DatetimeIndex对象与一般的数字Index在索引取值方面并无不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
>>> In[]:
# 使用下标取值
ts[0]

>>> Out[]:
8


# 使用下标进行切片
>>> In[]:
ts[0:2]

>>> Out[]:
2011-01-02 8
2011-01-05 2
dtype: int64



>>> In[]:
ts[::2]

>>> Out[]:
2011-01-02 8
2011-01-07 1
2011-01-10 2
dtype: int64

可以使用索引本身的值进行取值或者切片

1
2
3
4
5
>>> In[]:
ts[ts.index[2]]

>>> Out[]:
1

为了方便起见,也可以直接使用字符串形式进行索引

1
2
3
4
5
>>> In[]:
ts['2011-01-02']

>>> Out[]:
8

由于按照value进行切片,因此此时的切片是包含两端的

1
2
3
4
5
6
7
8
>>> In[]:
ts['2011-01-02':'2011-01-07']

>>> Out[]:
2011-01-02 8
2011-01-05 2
2011-01-07 1
dtype: int64

甚至可以只包含某个年份或者月份,这样pandas将会把满足这些条件的日期索引出来相当于是进行布尔掩码索引

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
>>> In[]:
ts_long = pd.Series(np.random.randn(500),
index=pd.date_range('2000-01-01',periods=500,freq='d'))
ts_long

>>> Out[]:
2000-01-01 0.979330
2000-01-02 -0.868795
2000-01-03 -0.439165
2000-01-04 0.803966
2000-01-05 0.012652
...
2001-05-10 -0.513777
2001-05-11 0.813053
2001-05-12 0.508245
2001-05-13 -1.782211
2001-05-14 1.427013
Freq: D, Length: 500, dtype: float64

>>> In[]:
ts_long['2000']

>>> Out[]:
2000-01-01 0.979330
2000-01-02 -0.868795
2000-01-03 -0.439165
2000-01-04 0.803966
2000-01-05 0.012652
...
2000-12-27 0.071322
2000-12-28 0.422151
2000-12-29 -0.016263
2000-12-30 -0.934628
2000-12-31 1.014883
Freq: D, Length: 366, dtype: float64


>>> In[]:
ts_long['2000-01']

>>> Out[]:
2000-01-01 0.979330
2000-01-02 -0.868795
2000-01-03 -0.439165
2000-01-04 0.803966
2000-01-05 0.012652
2000-01-06 0.822313
2000-01-07 0.205753
2000-01-08 0.265532
2000-01-09 -0.039236
2000-01-10 0.215647
2000-01-11 -0.646638
2000-01-12 0.390420
2000-01-13 2.390138
2000-01-14 0.107671
2000-01-15 0.925517
2000-01-16 1.692496
2000-01-17 -2.251996
2000-01-18 1.684247
2000-01-19 0.450169
2000-01-20 0.408878
2000-01-21 0.798527
2000-01-22 0.027421
2000-01-23 0.046561
2000-01-24 -0.302578
2000-01-25 1.213720
2000-01-26 0.524915
2000-01-27 0.432314
2000-01-28 1.183822
2000-01-29 -2.538193
2000-01-30 -0.861307
2000-01-31 0.186349
Freq: D, dtype: float64

>>> In[]:
ts_long['2000-01-21':'2000-03']

>>> Out[]:
2000-01-21 0.798527
2000-01-22 0.027421
2000-01-23 0.046561
2000-01-24 -0.302578
2000-01-25 1.213720
...
2000-03-27 -2.134014
2000-03-28 -0.800815
2000-03-29 1.472516
2000-03-30 -1.901447
2000-03-31 -0.238811
Freq: D, Length: 71, dtype: float64

也可以由datetime对象进行索引

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> In[]:
ts_long[datetime(2000,1,21):]

>>> Out[]:
2000-01-21 0.798527
2000-01-22 0.027421
2000-01-23 0.046561
2000-01-24 -0.302578
2000-01-25 1.213720
...
2001-05-10 -0.513777
2001-05-11 0.813053
2001-05-12 0.508245
2001-05-13 -1.782211
2001-05-14 1.427013
Freq: D, Length: 480, dtype: float64

由于时间序列一般是排序的,因此我们也可以使用两个不存在的时间对象进行索引

1
2
3
4
5
6
7
8
9
>>> In[]:
ts['2011-01-03':'2011-01-11']

>>> Out[]:
2011-01-05 2
2011-01-07 1
2011-01-08 2
2011-01-10 2
dtype: int64

即便时间序列索引是无序的,此时pandas也会按照时间序列的顺序对其进行切片

1
2
3
4
5
6
7
>>> In[]:
ts.sort_values()['2011-01-09':'2011-01-12']

>>> Out[]:
2011-01-10 2
2011-01-12 5
dtype: int64

更好的办法是,首先使用sort_values方法对其进行排序,然后再进行切片

1
2
3
4
5
6
7
>>> In[]:
ts[ts.index.sort_values()]['2011-01-09':'2011-01-12']

>>> Out[]:
2011-01-10 2
2011-01-12 5
dtype: int64

使用truncate方法完全可以达到相同效果,before参数相当于开始,after参数相当于结尾

1
2
3
4
5
6
7
8
9
>>> In[]:
ts.truncate(before='2011-01-06',after='2011-01-12')

>>> Out[]:
2011-01-07 1
2011-01-08 2
2011-01-10 2
2011-01-12 5
dtype: int64

重复的时间索引

pandas是允许索引的值存在重复的,如果遇到了这种情况,那么甚至可以对时间索引进行分组聚合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
>>> In[]:
dates = pd.DatetimeIndex(["2000-01-01", "2000-01-02", "2000-01-02","2000-01-02", "2000-01-03"])
dup_ts = pd.Series(np.arange(5), index=dates)
dup_ts

>>> Out[]:
2000-01-01 0
2000-01-02 1
2000-01-02 2
2000-01-02 3
2000-01-03 4
dtype: int64

>>> In[]:
dup_ts['2000-01-01']

>>> Out[]:
0

>>> In[]:
dup_ts['2000-01-02']

>>> Out[]:
2000-01-02 1
2000-01-02 2
2000-01-02 3
dtype: int64

>>> In[]:
# level=0表示对一级索引进行分组,但此处也只有一个索引,因此level=0就表示对数据按照索引进行分组
dup_ts.groupby(level=0).count()

>>> Out[]:
2000-01-01 1
2000-01-02 3
2000-01-03 1
dtype: int64

时间范围

padnas中的时间范围函数pd.date_range的作用是按照固定的频率产生一定范围内的DatetimeIndex对象,也即是时间戳对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
>>> In[]:
# 默认情况下,date_range的频率是天
# 可以发现,date_range对象是包含两端的
index = pd.date_range("2012-04-01", "2012-06-01")
index

>>> Out[]:
DatetimeIndex(['2012-04-01', '2012-04-02', '2012-04-03', '2012-04-04',
'2012-04-05', '2012-04-06', '2012-04-07', '2012-04-08',
'2012-04-09', '2012-04-10', '2012-04-11', '2012-04-12',
'2012-04-13', '2012-04-14', '2012-04-15', '2012-04-16',
'2012-04-17', '2012-04-18', '2012-04-19', '2012-04-20',
'2012-04-21', '2012-04-22', '2012-04-23', '2012-04-24',
'2012-04-25', '2012-04-26', '2012-04-27', '2012-04-28',
'2012-04-29', '2012-04-30', '2012-05-01', '2012-05-02',
'2012-05-03', '2012-05-04', '2012-05-05', '2012-05-06',
'2012-05-07', '2012-05-08', '2012-05-09', '2012-05-10',
'2012-05-11', '2012-05-12', '2012-05-13', '2012-05-14',
'2012-05-15', '2012-05-16', '2012-05-17', '2012-05-18',
'2012-05-19', '2012-05-20', '2012-05-21', '2012-05-22',
'2012-05-23', '2012-05-24', '2012-05-25', '2012-05-26',
'2012-05-27', '2012-05-28', '2012-05-29', '2012-05-30',
'2012-05-31', '2012-06-01'],
dtype='datetime64[ns]', freq='D')

如果仅指定开始或者结尾,那么就还需要传入生成的时间戳数量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
>>> In[]:
pd.date_range(start="2012-04-01", periods=20)

>>> Out[]:
DatetimeIndex(['2012-04-01', '2012-04-02', '2012-04-03', '2012-04-04',
'2012-04-05', '2012-04-06', '2012-04-07', '2012-04-08',
'2012-04-09', '2012-04-10', '2012-04-11', '2012-04-12',
'2012-04-13', '2012-04-14', '2012-04-15', '2012-04-16',
'2012-04-17', '2012-04-18', '2012-04-19', '2012-04-20'],
dtype='datetime64[ns]', freq='D')

>>> In[]:
pd.date_range(end="2012-6-30", periods=20)

>>> Out[]:
DatetimeIndex(['2012-06-11', '2012-06-12', '2012-06-13', '2012-06-14',
'2012-06-15', '2012-06-16', '2012-06-17', '2012-06-18',
'2012-06-19', '2012-06-20', '2012-06-21', '2012-06-22',
'2012-06-23', '2012-06-24', '2012-06-25', '2012-06-26',
'2012-06-27', '2012-06-28', '2012-06-29', '2012-06-30'],
dtype='datetime64[ns]', freq='D')

也可以指定生成时间的频率,pandas提供了非常丰富的时间频率参数,经常使用的如下:

  • D: 天
  • H: 时
  • T or min: 分
  • S: 秒
  • B: 工作日
  • M: 每月的最后一天
  • BM: 每月的最后一个工作日(每个月最晚的周五)
  • MS: 每月的第一天
  • BMS: 每月的第一个工作日(每个月最早的周一)
  • W-MON: 指定具星期几(MON,TUE,WED,THU,FRI,SAT,SUN)
  • WOM-1MON: 指定每个月的第几个星期几

完整的表格如下所示:

Alias Offset type Description
D Day Calendar daily
B BusinessDay Business daily
H Hour Hourly
T or min Minute Once a minute
S Second Once a second
L or ms Milli Millisecond (1/1,000 of 1 second)
U Micro Microsecond (1/1,000,000 of 1 second)
M MonthEnd Last calendar day of month
BM BusinessMonthEnd Last business day (weekday) of month
MS MonthBegin First calendar day of month
BMS BusinessMonthBegin First weekday of month
W-MON, W-TUE, … Week Weekly on given day of week (MON, TUE, WED, THU, FRI, SAT, or SUN)
WOM-1MON, WOM-2MON, … WeekOfMonth Generate weekly dates in the first, second, third, or fourth week of the month (e.g., WOM-3FRI for the third Friday of each month)
Q-JAN, Q-FEB, … QuarterEnd Quarterly dates anchored on last calendar day of each month, for year ending in indicated month (JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, or DEC)
BQ-JAN, BQ-FEB, … BusinessQuarterEnd Quarterly dates anchored on last weekday day of each month, for year ending in indicated month
QS-JAN, QS-FEB, … QuarterBegin Quarterly dates anchored on first calendar day of each month, for year ending in indicated month
BQS-JAN, BQS-FEB, … BusinessQuarterBegin Quarterly dates anchored on first weekday day of each month, for year ending in indicated month
A-JAN, A-FEB, … YearEnd Annual dates anchored on last calendar day of given month (JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, or DEC)
BA-JAN, BA-FEB, … BusinessYearEnd Annual dates anchored on last weekday of given month
AS-JAN, AS-FEB, … YearBegin Annual dates anchored on first day of given month
BAS-JAN, BAS-FEB, … BusinessYearBegin Annual dates anchored on first weekday of given month
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
>>> In[]:
pd.date_range(datetime.now(),periods=10,freq='H')

>>> Out[]:
DatetimeIndex(['2023-09-19 15:38:21.557455', '2023-09-19 16:38:21.557455',
'2023-09-19 17:38:21.557455', '2023-09-19 18:38:21.557455',
'2023-09-19 19:38:21.557455', '2023-09-19 20:38:21.557455',
'2023-09-19 21:38:21.557455', '2023-09-19 22:38:21.557455',
'2023-09-19 23:38:21.557455', '2023-09-20 00:38:21.557455'],
dtype='datetime64[ns]', freq='H')


>>> In[]:
pd.date_range('2023-03-01','2023-12-31',freq='BM')

>>> Out[]:
DatetimeIndex(['2023-03-31', '2023-04-28', '2023-05-31', '2023-06-30',
'2023-07-31', '2023-08-31', '2023-09-29', '2023-10-31',
'2023-11-30', '2023-12-29'],
dtype='datetime64[ns]', freq='BM')

>>> In[]:
pd.date_range('2023-03-01','2023-12-31',freq='M')

>>> Out[]:
DatetimeIndex(['2023-03-31', '2023-04-30', '2023-05-31', '2023-06-30',
'2023-07-31', '2023-08-31', '2023-09-30', '2023-10-31',
'2023-11-30', '2023-12-31'],
dtype='datetime64[ns]', freq='M')

找出 2023-03-01~2023-12-31的所有周五

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
>>> In[]:
pd.date_range('2023-03-01 12:30','2023-12-31 12:30',freq='W-FRI')

>>> Out[]:
DatetimeIndex(['2023-03-03 12:30:00', '2023-03-10 12:30:00',
'2023-03-17 12:30:00', '2023-03-24 12:30:00',
'2023-03-31 12:30:00', '2023-04-07 12:30:00',
'2023-04-14 12:30:00', '2023-04-21 12:30:00',
'2023-04-28 12:30:00', '2023-05-05 12:30:00',
'2023-05-12 12:30:00', '2023-05-19 12:30:00',
'2023-05-26 12:30:00', '2023-06-02 12:30:00',
'2023-06-09 12:30:00', '2023-06-16 12:30:00',
'2023-06-23 12:30:00', '2023-06-30 12:30:00',
'2023-07-07 12:30:00', '2023-07-14 12:30:00',
'2023-07-21 12:30:00', '2023-07-28 12:30:00',
'2023-08-04 12:30:00', '2023-08-11 12:30:00',
'2023-08-18 12:30:00', '2023-08-25 12:30:00',
'2023-09-01 12:30:00', '2023-09-08 12:30:00',
'2023-09-15 12:30:00', '2023-09-22 12:30:00',
'2023-09-29 12:30:00', '2023-10-06 12:30:00',
'2023-10-13 12:30:00', '2023-10-20 12:30:00',
'2023-10-27 12:30:00', '2023-11-03 12:30:00',
'2023-11-10 12:30:00', '2023-11-17 12:30:00',
'2023-11-24 12:30:00', '2023-12-01 12:30:00',
'2023-12-08 12:30:00', '2023-12-15 12:30:00',
'2023-12-22 12:30:00', '2023-12-29 12:30:00'],
dtype='datetime64[ns]', freq='W-FRI')

找出 2023-03-01~2023-12-31中每个月的第三个周日

1
2
3
4
5
6
7
8
9
>>> In[]:
pd.date_range('2023-03-01','2023-12-31',freq='WOM-3SUN')


>>> Out[]:
DatetimeIndex(['2023-03-19', '2023-04-16', '2023-05-21', '2023-06-18',
'2023-07-16', '2023-08-20', '2023-09-17', '2023-10-15',
'2023-11-19', '2023-12-17'],
dtype='datetime64[ns]', freq='WOM-3SUN')

找出每个季度末尾的那天.默认情况下,按照1月为第一季度开始

1
2
3
4
5
>>> In[]:
pd.date_range('2023-03-01','2023-12-31',freq='Q')

>>> Out[]:
DatetimeIndex(['2023-03-31', '2023-06-30', '2023-09-30', '2023-12-31'], dtype='datetime64[ns]', freq='Q-DEC')

指定2月为季度的结尾,并在此基础上,找出某段时间内的每个季度的末尾一天.此时要注意,date_range方法产生时间戳是包含两端的

1
2
3
4
5
>>> In[]:
pd.date_range('2023-02-28','2023-12-31',freq='Q-FEB')

>>> Out[]:
DatetimeIndex(['2023-02-28', '2023-05-31', '2023-08-31', '2023-11-30'], dtype='datetime64[ns]', freq='Q-FEB')

指定3月份为每年的开始,并且在此基础上求出时间范围内每年的第一个工作日. 注意,此处由于2025年的3月1日以及2日是周末,因此不计入工作日的范畴内

1
2
3
4
5
>>> In[]:
pd.date_range('2023-02-28','2025-12-31',freq='BAS-MAR')

>>> Out[]:
DatetimeIndex(['2023-03-01', '2024-03-01', '2025-03-03'], dtype='datetime64[ns]', freq='BAS-MAR')

日期偏移

date_range中的freq参数本质上传递了一个时间偏移对象,在pandas中可以以如下方式创建时间偏移对象:

1
from pandas.tseries.offsets import Hour,Minute

创建一个1小时的时间偏移对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

>>> In[]:
hour = Hour()
hour

>>> Out[]:
<Hour>

>>> In[]:

minutes = Minute(30)
minutes

>>> Out[]:
<30 * Minutes>

时间偏移对象是可以进行算术加减的

1
2
3
4
5
6

>>> In[]:
hour+3*minutes

>>> Out[]:
<150 * Minutes>

可以使用时间偏移对象进行date_range

1
2
3
4
5
6
7
8
9
>>> In[]:
pd.date_range('2023-09-15',periods=5,freq=hour+3*minutes)


>>> Out[]:
DatetimeIndex(['2023-09-15 00:00:00', '2023-09-15 02:30:00',
'2023-09-15 05:00:00', '2023-09-15 07:30:00',
'2023-09-15 10:00:00'],
dtype='datetime64[ns]', freq='150T')

大部分情况下,我们并不会这样做,而是直接传递字符串即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
>>> In[]:
pd.date_range('2023-09-15',periods=5,freq='3H')

>>> Out[]:
DatetimeIndex(['2023-09-15 00:00:00', '2023-09-15 03:00:00',
'2023-09-15 06:00:00', '2023-09-15 09:00:00',
'2023-09-15 12:00:00'],
dtype='datetime64[ns]', freq='3H')

>>> In[]:
pd.date_range('2023-09-15',periods=5,freq='90min')

>>> Out[]:
DatetimeIndex(['2023-09-15 00:00:00', '2023-09-15 01:30:00',
'2023-09-15 03:00:00', '2023-09-15 04:30:00',
'2023-09-15 06:00:00'],
dtype='datetime64[ns]', freq='90T')
>>> In[]:
pd.date_range('2023-09-15',periods=5,freq='2M')

>>> Out[]:
DatetimeIndex(['2023-09-30', '2023-11-30', '2024-01-31', '2024-03-31',
'2024-05-31'],
dtype='datetime64[ns]', freq='2M')

时间段对象

时间段对象(Period)是除了时间戳对象之外的另一种 基本时间数据类型,它表示的是某段时间。时段对象的创建可以由pd.Period方法创建,输入的参数就是一个时间戳(表示时段开始时间)以及一个时间频率参数(表示时段持续时间)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> In[]:
# 创建一个从2022年1月1日开始,持续3天的时段对象(2022-1-1~2022-1-3)
pd.Period('2022-1-1','3D')


>>> Out[]:
Period('2022-01-01', '3D')

>>> In[]:
# 创建一个长度为一年的,以5月为年末月份的时段
# 此处要注意的是,由于5月已经被指定为年末,因此2022-3-5就不能作为时段的开始了
# 此时,时段的开始是2022年6月1日
# 所以,当用Period方法创建一个年份时段的时候,该时段的时长一定是一整年,因此这种情况下是没有必要再指定具体的月份和日期了
pd.Period('2022-3-5','A-MAY').asfreq("D",how='start')

>>> Out[]:
Period('2021-06-01', 'D')

可以对时段对象进行加减算数操作,表示将时段进行偏置(offset)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> In[]:
# 2021-6-1~2022-5-31
p = pd.Period('2022','A-MAY')
# 2022-6-1~2023-5-31
p+1

>>> Out[]:
Period('2023', 'A-MAY')

>>> In[]:
# 2019-6-1~2020-5-31

p-2
>>> Out[]:
Period('2020', 'A-MAY')

同样的,也可以使用pd.period_range也可以生成多个时段对象,包含多个时段对象的对象称为PeriodIndex(类似于包含多个时间戳对象的DateTimeIndex)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
>>> In[]:
pd.period_range('2022-2','2023-3',freq = 'M')

>>> Out[]:
PeriodIndex(['2022-02', '2022-03', '2022-04',
'2022-05', '2022-06', '2022-07',
'2022-08', '2022-09', '2022-10',
'2022-11', '2022-12','2023-01',
'2023-02', '2023-03'],
dtype='period[M]')

>>> In[]:
dt = pd.Series(np.random.randn(14),index = pd.period_range('2022-2','2023-3',freq = 'M'))
dt

>>> Out[]:

2022-02 -1.068388
2022-03 1.438250
2022-04 1.372934
2022-05 0.592564
2022-06 -1.945797
2022-07 -0.354653
2022-08 -0.576030
2022-09 -0.939876
2022-10 -0.952067
2022-11 0.432889
2022-12 1.168628
2023-01 0.997162
2023-02 -0.104164
2023-03 -0.384355
Freq: M, dtype: float64

时段频率转换

时段对象的频率使用asfreq是可以转换的

1
2
3
4
5
6
7
8
>>> In[]: 
# 将2022-1-1~2022-12-31这个时段的频率转换为月
# 需要注意的是,转换过后,并不会生成12个时间段,而是只能生成一个持续时间为1M的时间段
p = pd.Period('2022',freq = 'A-DEC')
p.asfreq('M',how='start')

>>> Out[]:
Period('2022-01', 'M')

时间戳与时断对象的转换

时间戳转换为时段对象可以使用pd.to_period方法,时段对象转换为时间吹则可以使用pd.to_timestamp 对象

1
2
3
4
5
6
7
>>> In[]: 
ts = pd.to_datetime(['2022-1','2022-2','2022-3','2022-4'])
p = ts.to_period()
p

>>> Out[]:
PeriodIndex(['2022-01', '2022-02', '2022-03', '2022-04'], dtype='period[M]')

默认情况下,转换为时段对象的频率是自动确认,从时间戳对象中推算出来的,但也是可以手动指定频率的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> In[]: 
ts = pd.date_range('2022-1-28','2022-2-5',freq='D')
dt = pd.Series(np.random.randn(len(ts)),index=ts)
dt

>>> Out[]:
2022-01-28 0.613319
2022-01-29 0.396804
2022-01-30 -0.544125
2022-01-31 -0.357917
2022-02-01 1.536737
2022-02-02 2.057696
2022-02-03 -0.730858
2022-02-04 0.391824
2022-02-05 -0.888997
Freq: D, dtype: float64

由于指定了频率为月,因此时间戳中的日期数据都被抛弃了.当索引是时间戳的时候,也可以直接对Series或者DataFrame对象使用to_period方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
>>> In[]: 
dt.to_period(freq="M")

>>> Out[]:
2022-01 0.613319
2022-01 0.396804
2022-01 -0.544125
2022-01 -0.357917
2022-02 1.536737
2022-02 2.057696
2022-02 -0.730858
2022-02 0.391824
2022-02 -0.888997
Freq: M, dtype: float64

>>> In[]:
p = pd.period_range('2022-3','2023-2',freq='M')
dt = pd.Series(np.random.randn(len(p)),index = p)
dt

>>> Out[]:

2022-03 -2.274375
2022-04 0.626871
2022-05 -1.011548
2022-06 2.227270
2022-07 -0.477682
2022-08 -0.244246
2022-09 0.468265
2022-10 0.195185
2022-11 0.081317
2022-12 -1.097176
2023-01 -0.100736
2023-02 -1.200283
Freq: M, dtype: float64

由于时段对象表示一段时间,而时间戳对象表示一个时间点,因此将时段转化为时间戳时.需要使用how参数指定转化到该时段对象的开始或者结束点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> In[]:
dt.to_timestamp(how='end')

>>> Out[]:
2022-03-31 23:59:59.999999999 -2.274375
2022-04-30 23:59:59.999999999 0.626871
2022-05-31 23:59:59.999999999 -1.011548
2022-06-30 23:59:59.999999999 2.227270
2022-07-31 23:59:59.999999999 -0.477682
2022-08-31 23:59:59.999999999 -0.244246
2022-09-30 23:59:59.999999999 0.468265
2022-10-31 23:59:59.999999999 0.195185
2022-11-30 23:59:59.999999999 0.081317
2022-12-31 23:59:59.999999999 -1.097176
2023-01-31 23:59:59.999999999 -0.100736
2023-02-28 23:59:59.999999999 -1.200283
dtype: float64

重采样

重采样意味着对一个时序数据的频率进行重新规定,由高频向低频的重采样称为向下采样,由低频向高频的采样,称为向上采样。 向下采样的过程是数据分组聚合的过程,比如由月到年的重采,那么就会将所有同属一年的月份分为一组,然后可以进行求平均等聚合方法。向上采样则不会引起数据分组的过程。

时间戳的向下采样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
>>> In[]:
# 单位,创建一个时间序列
ts = pd.date_range('2000-1-1',periods=100)
tsdt = pd.Series(np.random.randn(len(ts)),index=ts)
tsdt

>>> Out[]:

2000-01-01 -0.383793
2000-01-02 2.075331
2000-01-03 0.203583
2000-01-04 -0.729329
2000-01-05 -0.678031
...
2000-04-05 -0.278502
2000-04-06 1.329450
2000-04-07 0.563900
2000-04-08 0.996625
2000-04-09 0.548595
Freq: D, Length: 100, dtype: float64

将时间序列进行重采样,频率为.默认情况下,采样后的时间数据类型和采样前保持一致.采样前是时间戳,采样后也是时间戳,因此采样后的时间只能保持在该月份的某个时间点

1
2
3
4
5
6
7
8
9
>>> In[]:
tsdt.resample('M').mean()

>>> Out[]:
2000-01-31 0.122733
2000-02-29 -0.076518
2000-03-31 -0.238215
2000-04-30 0.384449
Freq: M, dtype: float64

也可以使用kind参数,指定采样后的时间数据类型为时段.这样,就不会再表示该月份中的某个时间点,而是表示该月本身,其实当时间戳数据向下采样时.这种方式是更为合理的

1
2
3
4
5
6
7
8
9
>>> In[]:
tsdt.resample('M',kind = 'period').mean()

>>> Out[]:
2000-01 0.122733
2000-02 -0.076518
2000-03 -0.238215
2000-04 0.384449
Freq: M, dtype: float64
1
2
# 向下采样的本质是聚合分组,因此groupby中的agg方法是可用的
tsdt.resample('M').agg(['mean','median'])
mean median
2000-01-31 0.122733 0.076847
2000-02-29 -0.076518 -0.269821
2000-03-31 -0.238215 -0.236879
2000-04-30 0.384449 0.548595

向下采样相当于按照低频率的时间将时间序列数据分成了一个个区间.每个时序数据存在且仅存在于某个区间中,因此向下采样的区间是half-open的.默认情况下,区间是左闭右开的,在这种情况下,此时的区间是 [2000-01-01,2000-02-01),[2000-02-01,2000-03-01)…

1
2
3
4
5
6
7
8
9
>>> In[]:
tsdt.resample("MS").mean()

>>> Out[]:
2000-01-01 0.122733
2000-02-01 -0.076518
2000-03-01 -0.238215
2000-04-01 0.384449
Freq: MS, dtype: float64

如果设置区间为左开右闭,则此时的区间表示为(1999-12-01,2000-01-01],(2000-01-01,2000-02-01],(2000-02-01,2000-03-01]…值得注意的是,由于此时设置区间左端为开,因此2000-01-01是不被包含在第二个区间的.因此,还必须设置区间 (1999-12-01,2000-01-01] 来包含这个日期.还有一点需要注意的是,由于MS表示每个月日历开始的那一天,因此完整的区间是一个月的第一天到下个月的第一天

1
2
3
4
5
6
7
8
9
10
>>> In[]:
tsdt.resample("MS",closed='right').mean()

>>> Out[]:
1999-12-01 -0.383793
2000-01-01 0.114894
2000-02-01 -0.019938
2000-03-01 -0.299048
2000-04-01 0.541478
Freq: MS, dtype: float64

closed参数用来决定区间的左右开闭,如果想要采样以后调整索引的显示,可以如下操作
所谓label=right,本质上 是在将每区间中右边的值作为采样结果的新索引(不管这个值是开还是闭)

1
2
3
4
5
6
7
8
9
10
11
>>> In[]:
tsdt.resample('MS',closed='right' , label='right').mean()

>>> Out[]:

2000-01-01 -0.383793
2000-02-01 0.114894
2000-03-01 -0.019938
2000-04-01 -0.299048
2000-05-01 0.541478
Freq: MS, dtype: float64

ohlc

在金融数据分析中,ohlc表示某股票在市场一天中价格的 open high low close的值,及开盘价,最高价,最低价和收盘价,pandas内置了用来求某时段的ohlc方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> In[]:
ts = pd.date_range('2000-01-01',periods=100,freq='D')
tsdt = pd.Series(np.random.randn(100),index=ts)
tsdt

>>> Out[]:
2000-01-01 1.312267
2000-01-02 -0.723157
2000-01-03 0.300057
2000-01-04 0.565059
2000-01-05 0.641482
...
2000-04-05 0.448233
2000-04-06 1.654772
2000-04-07 0.134817
2000-04-08 2.054210
2000-04-09 -1.363439
Freq: D, Length: 100, dtype: float64

统计出[2000-01-01,2000-02-01)间数据的 ohlc值

1
2

tsdt.resample('MS').ohlc()
open high low close
2000-01-01 1.312267 1.482948 -1.607714 0.343055
2000-02-01 1.271338 1.459851 -1.642738 -0.924122
2000-03-01 0.475739 2.475645 -1.888443 0.621041
2000-04-01 2.655665 2.655665 -1.577870 -1.363439
1
tsdt.resample('M',kind='period').ohlc()
open high low close
2000-01 1.312267 1.482948 -1.607714 0.343055
2000-02 1.271338 1.459851 -1.642738 -0.924122
2000-03 0.475739 2.475645 -1.888443 0.621041
2000-04 2.655665 2.655665 -1.577870 -1.363439

多级索引下的分组

resample当向下采样时,本质是对时间索引做了一个分组,但是如果还有另外的列需要分组时,这是就需要使用pd.Grouper方法对时间数据进行预设分组,而后再使用groupby方法

1
2
3
4
5
times = pd.date_range("2017-05-20 00:00", freq="1min", periods=15)
df = pd.DataFrame({"time": times.repeat(3),
"key": np.tile(["a", "b", "c"], 15),
"value": np.arange(15 * 3.)})
df.head(9)
time key value
0 2017-05-20 00:00:00 a 0.0
1 2017-05-20 00:00:00 b 1.0
2 2017-05-20 00:00:00 c 2.0
3 2017-05-20 00:01:00 a 3.0
4 2017-05-20 00:01:00 b 4.0
5 2017-05-20 00:01:00 c 5.0
6 2017-05-20 00:02:00 a 6.0
7 2017-05-20 00:02:00 b 7.0
8 2017-05-20 00:02:00 c 8.0

需要对time进行分组重采样,也需要对key进行分组,在这种情况下, 我们首先使用Grouper方法,预设一个频率为5分钟的分组条件.Grouper相当于是在脱离具体时间序列的条件下,规定了重采样的规则.

1
2
3
4
5
6
>>> In[]:
tk = pd.Grouper(freq='5T',closed='left',label = 'left')
tk

>>> Out[]:
TimeGrouper(freq=<5 * Minutes>, axis=0, sort=True, dropna=True, closed='left', label='left', how='mean', convention='e', origin='start_day')

进行真正的分组

1
2
3
4
5
6
7
8
>>> In[]:

# 要注意,tk对象只是一个规则,并不包含数据,因此其必须依附在某组具体的时间序列数据之上。
# tk只能依附在作为索引的时间序列数据之上,因此在真正分组之前,还需要将time列转化为索引,否则将报错。如下所示
df.groupby(['key',tk])

>>> Out[]:
TypeError: Only valid with DatetimeIndex, TimedeltaIndex or PeriodIndex, but got an instance of 'RangeIndex'

下载链接

Pandas时间序列分析