1. 时序中的基本对象

时间序列的概念在日常生活中十分常见,但对于一个具体的时序事件而言,可以从多个时间对象的角度来描述。例如 2020 年 9 月 7 日周一早上 8 点整需要到教室上课,这个课会在当天早上 10 点结束,其中包含了哪些时间概念?

  • 第一,会出现**时间戳(Date times)**的概念,即’2020-9-7 08:00:00’和’2020-9-7 10:00:00’这两个时间点分别代表了上课和下课的时刻,在 pandas 中称为 Timestamp。同时,一系列的时间戳可以组成 DatetimeIndex,而将它放到 Series 中后,Series 的类型就变为了 datetime64[ns],如果有涉及时区则为 datetime64[ns, tz] ,其中 tz 是 timezone 的简写。
  • 第二,会出现**时间差(Time deltas)**的概念,即上课需要的时间,两个 Timestamp 做差就得到了时间差,pandas 中利用 Timedelta 来表示。类似的,一系列的时间差就组成了 TimedeltaIndex,而将它放到 Series 中后,Series 的类型就变为了 timedelta64[ns]
  • 第三,会出现**时间段(Time spans)**的概念,即在 8 点到 10 点这个区间都会持续地在上课,在 pandas 利用 Period 来表示。类似的,一系列的时间段就组成了 PeriodIndex ,而将它放到 Series 中后,Series 的类型就变为了 Period
  • 第四,会出现**日期偏置(Date offsets)**的概念,假设你只知道 9 月的第一个周一早上 8 点要去上课,但不知道具体的日期,那么就需要一个类型来处理此类需求。再例如,想要知道 2020 年 9 月 7 日后的第30 个工作日是哪一天,那么时间差就解决不了你的问题,从而 pandas 中的 DateOffset 就出现了。同时,pandas 中没有为一列时间偏置专门设计存储类型,理由也很简单,因为需求比较奇怪,一般来说我们只需要对一批时间特征做一个统一的特殊日期偏置。
概念 单元素类型 数组类型 pandas 数据类型
Date times Timestamp DatetimeIndex datetime64[ns]
Time deltas Timedelta TimedeltaIndex timedelta64[ns]
Time spans Period PeriodIndex period[freq]
Date offsets DateOffset None None

2. 时间戳

2.1 Timestamp 的构造与属性

单个时间戳的生成利用 pd.Timestamp 实现,一般而言的常见日期格式都能被成功地转换:

ts = pd.Timestamp('2020/1/1')
ts
Timestamp('2020-01-01 00:00:00')
ts = pd.Timestamp('2020-1-1 08:10:30')
ts
Timestamp('2020-01-01 08:10:30')

通过 year, month, day, hour, min, second 可以获取具体的数值:

print('ts.yaer:',ts.year)
print('ts.month:',ts.month)
print('ts.day:',ts.day)
print('ts.hour:',ts.hour)
print('ts.minute:', ts.minute)
print('ts.second:', ts.second)
ts.yaer: 2020
ts.month: 1
ts.day: 1
ts.hour: 8
ts.minute: 10
ts.second: 30

在 pandas 中,时间戳的最小精度为纳秒 ns ,由于使用了 64 位存储,可以表示的时间范围大约可以如下计算:

TimeRange=264109×60×60×24×365≈585YearsTime \ Range = \frac{2^{64}}{10^9 \times 60 \times 60 \times 24 \times 365} \approx 585 \ YearsTime Range=109×60×60×24×365264​≈585 Years

通过 pd.Timestamp.maxpd.Timestamp.min 可以获取时间戳表示的范围,可以看到确实表示的区间年数大小正如上述计算结果:

print('pd.Timestamp.max: ',pd.Timestamp.max)
print('pd.Timestamp.min: ',pd.Timestamp.min)
print('pd.Timestamp.max.year - pd.Timestamp.min.year: ',pd.Timestamp.max.year - pd.Timestamp.min.year)
pd.Timestamp.max:  2262-04-11 23:47:16.854775807
pd.Timestamp.min:  1677-09-21 00:12:43.145225
pd.Timestamp.max.year - pd.Timestamp.min.year:  585

2.2 Datetime 序列的生成

一组时间戳可以组成时间序列,可以用 to_datetimedate_range 来生成。其中,to_datetime 能够把一列时间戳格式的对象转换成为 datetime64[ns] 类型的时间序列

pd.to_datetime(['2020-1-1', '2020-1-3', '2020-1-6'])
DatetimeIndex(['2020-01-01', '2020-01-03', '2020-01-06'], dtype='datetime64[ns]', freq=None)
df = pd.read_csv('G:/代码/joyful-pandas-master/data/learn_pandas.csv')
s = pd.to_datetime(df.Test_Date)
s.head()
0   2019-10-05
1   2019-09-04
2   2019-09-12
3   2020-01-03
4   2019-11-06
Name: Test_Date, dtype: datetime64[ns]

在极少数情况,时间戳的格式不满足转换时,可以强制使用 format 进行匹配:

temp = pd.to_datetime(['2020\\1\\1','2020\\1\\3'],format='%Y\\%m\\%d')
temp
DatetimeIndex(['2020-01-01', '2020-01-03'], dtype='datetime64[ns]', freq=None)

注意上面由于传入的是列表,而非 pandas 内部的 Series ,因此返回的是 DatetimeIndex ,如果想要转为 datetime64[ns] 的序列,需要显式用 Series 转化:

 pd.Series(temp).head()
0   2020-01-01
1   2020-01-03
dtype: datetime64[ns]

另外,还存在一种把表的多列时间属性拼接转为时间序列的 to_datetime 操作,此时的列名必须和以下给定的时间关键词列名一致:

df_date_cols = pd.DataFrame({'year': [2020, 2020],'month': [1, 1],'day': [1, 2],'hour': [10, 20],'minute': [30, 50],'second': [20, 40]})
pd.to_datetime(df_date_cols)
0   2020-01-01 10:30:20
1   2020-01-02 20:50:40
dtype: datetime64[ns]

date_range 是一种生成连续间隔时间的一种方法,其重要的参数为 start, end, freq, periods ,它们分别表示开始时间,结束时间,时间间隔,时间戳个数。其中,四个中的三个参数决定了,那么剩下的一个就随之确定了。这里要注意,开始或结束日期如果作为端点则它会被包含:

 pd.date_range('2020-1-1','2020-1-21', freq='10D') # 包含开始、结束日期
DatetimeIndex(['2020-01-01', '2020-01-11', '2020-01-21'], dtype='datetime64[ns]', freq='10D')
pd.date_range('2020-1-1','2020-2-28', freq='10D')
DatetimeIndex(['2020-01-01', '2020-01-11', '2020-01-21', '2020-01-31','2020-02-10', '2020-02-20'],dtype='datetime64[ns]', freq='10D')
pd.date_range('2020-1-1','2020-2-28', periods=6) # 由于结束日期无法取到,freq 不为 10 天
DatetimeIndex(['2020-01-01 00:00:00', '2020-01-12 14:24:00','2020-01-24 04:48:00', '2020-02-04 19:12:00','2020-02-16 09:36:00', '2020-02-28 00:00:00'],dtype='datetime64[ns]', freq=None)

这里的 freq 参数与 DateOffset 对象紧密相关,将在第四节介绍其具体的用法。


Question: Timestamp 上定义了一个 value 属性,其返回的整数值代表了从 1970 年 1 月 1 日零点到给定时间戳相差的纳秒数,请利用这个属性构造一个随机生成给定日期区间内日期序列的函数。

def rand_date(date_interval, size=10):start = pd.Timestamp(date_interval[0]).value / 10**9 # 从毫秒转化为秒end = pd.Timestamp(date_interval[1]).value / 10**9 rand = np.random.randint(start, end+1, size=size)return pd.to_datetime(rand, unit='s')rand_date(['2020-1-1', '2020-2-1'])
DatetimeIndex(['2020-01-14 10:03:26', '2020-01-12 18:05:08','2020-01-25 15:26:43', '2020-01-20 14:31:24','2020-01-05 02:27:19', '2020-01-07 01:52:27','2020-01-16 03:28:15', '2020-01-13 01:02:44','2020-01-08 11:20:16', '2020-01-31 20:46:26'],dtype='datetime64[ns]', freq=None)

最后,要介绍一种改变序列采样频率的方法 asfreq,它能够根据给定的 freq 对序列进行类似于 reindex 的操作:

s = pd.Series(np.random.rand(5),index=pd.to_datetime(['2020-1-%d'%i for i in range(1,10,2)]))
s
2020-01-01    0.212937
2020-01-03    0.257733
2020-01-05    0.575242
2020-01-07    0.968967
2020-01-09    0.515945
dtype: float64
 s.asfreq('D')
2020-01-01    0.212937
2020-01-02         NaN
2020-01-03    0.257733
2020-01-04         NaN
2020-01-05    0.575242
2020-01-06         NaN
2020-01-07    0.968967
2020-01-08         NaN
2020-01-09    0.515945
Freq: D, dtype: float64
 s.asfreq('12H').head()
2020-01-01 00:00:00    0.212937
2020-01-01 12:00:00         NaN
2020-01-02 00:00:00         NaN
2020-01-02 12:00:00         NaN
2020-01-03 00:00:00    0.257733
Freq: 12H, dtype: float64

datetime64[ns] 序列的最值与均值:
前面提到了 datetime64[ns] 本质上可以理解为一个大整数,对于一个该类型的序列,可以使用 max, min, mean,来取得最大时间戳、最小时间戳和“平均”时间戳。

print('s.index.max(): ',s.index.max())
print('s.index.min(): ',s.index.min())
print('s.index.mean(): ',s.index.mean())
s.index.max():  2020-01-09 00:00:00
s.index.min():  2020-01-01 00:00:00
s.index.mean():  2020-01-05 00:00:00

2.3 dt对象

如同 category, string 的序列上定义了 cat, str 来完成分类数据和文本数据的操作,在时序类型的序列上定义了 dt 对象来完成许多时间序列的相关操作。这里对于 datetime64[ns] 类型而言,可以大致分为三类操作:取出时间相关的属性、判断时间戳是否满足条件、取整操作

  • 第一类操作的常用属性包括: date, time, year, month, day, hour, minute, second, microsecond, nanosecond, dayofweek, dayofyear, weekofyear, daysinmonth, quarter,其中 daysinmonth, quarter 分别表示该月一共有几天和季度。
s = pd.Series(pd.date_range('2020-1-1','2020-1-3', freq='D'))
s.dt.date
0    2020-01-01
1    2020-01-02
2    2020-01-03
dtype: object
s.dt.time
0    00:00:00
1    00:00:00
2    00:00:00
dtype: object
s.dt.day
0    1
1    2
2    3
dtype: int64
s.dt.daysinmonth
0    31
1    31
2    31
dtype: int64

在这些属性中,经常使用的是 dayofweek,它返回了周中的星期情况,周一为0、周二为1,以此类推:

s.dt.dayofweek
0    2
1    3
2    4
dtype: int64

此外,可以通过 month_name, day_name 返回英文的月名和星期名,注意它们是方法而不是属性:

s.dt.month_name()
0    January
1    January
2    January
dtype: object
s.dt.day_name()
0    Wednesday
1     Thursday
2       Friday
dtype: object

  • 第二类判断操作主要用于测试是否为月/季/年的第一天或者最后一天:
s.dt.is_year_start # 还可选 is_quarter/month_start
0     True
1    False
2    False
dtype: bool
s.dt.is_year_end # 还可选 is_quarter/month_end
0    False
1    False
2    False
dtype: bool

  • 第三类的取整操作包含 round, ceil, floor ,它们的公共参数为 freq ,常用的包括 H, min, S (小时、分钟、秒),所有可选的 freq 可参考 此处。
s = pd.Series(pd.date_range('2020-1-1 20:35:00', '2020-1-1 22:35:00',freq='45min'))
s
0   2020-01-01 20:35:00
1   2020-01-01 21:20:00
2   2020-01-01 22:05:00
dtype: datetime64[ns]
s.dt.round('1H')
0   2020-01-01 21:00:00
1   2020-01-01 21:00:00
2   2020-01-01 22:00:00
dtype: datetime64[ns]
s.dt.ceil('1H')
0   2020-01-01 21:00:00
1   2020-01-01 22:00:00
2   2020-01-01 23:00:00
dtype: datetime64[ns]
s.dt.floor('1H')
0   2020-01-01 20:00:00
1   2020-01-01 21:00:00
2   2020-01-01 22:00:00
dtype: datetime64[ns]

2.4 时间戳的切片与索引

一般而言,时间戳序列作为索引使用。如果想要选出某个子时间戳序列,第一类方法是利用 dt 对象和布尔条件联合使用,另一种方式是利用切片,后者常用于连续时间戳。下面,举一些例子说明:

s = pd.Series(np.random.randint(2,size=366),index=pd.date_range('2020-01-01','2020-12-31'))
idx = pd.Series(s.index).dt
s.head()
2020-01-01    1
2020-01-02    0
2020-01-03    0
2020-01-04    0
2020-01-05    1
Freq: D, dtype: int32

Example1:每月的第一天或者最后一天

s[(idx.is_month_start|idx.is_month_end).values].head()
2020-01-01    1
2020-01-31    1
2020-02-01    0
2020-02-29    1
2020-03-01    0
dtype: int32

Example2:双休日

s[idx.dayofweek.isin([5,6]).values].head()
2020-01-04    0
2020-01-05    1
2020-01-11    1
2020-01-12    1
2020-01-18    0
dtype: int32

Example3:取出某天的值

s['2020-01-01']
1
s['20200101'] # 自动转换标准格式
1

Example4:取出七月的值

s['2020-07'].head()
2020-07-01    1
2020-07-02    0
2020-07-03    0
2020-07-04    1
2020-07-05    0
Freq: D, dtype: int32

Example5:取出5月初至7月15日

s['2020-05':'2020-7-15'].head()
2020-05-01    1
2020-05-02    0
2020-05-03    1
2020-05-04    1
2020-05-05    0
Freq: D, dtype: int32
s['2020-05':'2020-7-15'].tail()
2020-07-11    1
2020-07-12    1
2020-07-13    0
2020-07-14    0
2020-07-15    1
Freq: D, dtype: int32

3. 时间差

3.1 Timedelta的生成

正如在第一节中所说,时间差可以理解为两个时间戳的差,这里也可以通过 pd.Timedelta 来构造:

pd.Timestamp('20200102 08:00:00')-pd.Timestamp('20200101 07:35:00')
Timedelta('1 days 00:25:00')
pd.Timedelta(days=1, minutes=25) # 需要注意加s
Timedelta('1 days 00:25:00')
pd.Timedelta('1 days 25 minutes') # 字符串生成
Timedelta('1 days 00:25:00')

生成时间差序列的主要方式是 pd.to_timedelta,其类型为 timedelta64[ns] :

s = pd.to_timedelta(df.Time_Record)
s.head()
0   0 days 00:04:34
1   0 days 00:04:20
2   0 days 00:05:22
3   0 days 00:04:08
4   0 days 00:05:22
Name: Time_Record, dtype: timedelta64[ns]

与 date_range 一样,时间差序列也可以用 timedelta_range 来生成,它们两者具有一致的参数:

pd.timedelta_range('0s', '1000s', freq='6min')
TimedeltaIndex(['0 days 00:00:00', '0 days 00:06:00', '0 days 00:12:00'], dtype='timedelta64[ns]', freq='6T')
pd.timedelta_range('0s', '1000s', periods=3)
TimedeltaIndex(['0 days 00:00:00', '0 days 00:08:20', '0 days 00:16:40'], dtype='timedelta64[ns]', freq=None)

对于 Timedelta 序列,同样也定义了 dt 对象,上面主要定义了的属性包括 days, seconds, mircroseconds, nanoseconds ,它们分别返回了对应的时间差特征。需要注意的是,这里的 seconds 不是指单纯的秒,而是对天数取余后剩余的秒数:

s.dt.seconds.head()
0    274
1    260
2    322
3    248
4    322
Name: Time_Record, dtype: int64

如果不想对天数取余而直接对应秒数,可以使用 total_seconds

s.dt.total_seconds().head()
0    274.0
1    260.0
2    322.0
3    248.0
4    322.0
Name: Time_Record, dtype: float64

与时间戳序列类似,取整函数也是可以在 dt 对象上使用的:

pd.to_timedelta(df.Time_Record).dt.round('min').head()
0   0 days 00:05:00
1   0 days 00:04:00
2   0 days 00:05:00
3   0 days 00:04:00
4   0 days 00:05:00
Name: Time_Record, dtype: timedelta64[ns]

3.2 Timedelta的运算

时间差支持的常用运算有三类:与标量的乘法运算、与时间戳的加减法运算、与时间差的加减法与除法运算

td1 = pd.Timedelta(days=1)
td2 = pd.Timedelta(days=3)
ts = pd.Timestamp('20200101')
td1 * 2
Timedelta('2 days 00:00:00')
td2 - td1
Timedelta('2 days 00:00:00')
ts + td1
Timestamp('2020-01-02 00:00:00')
ts - td1
Timestamp('2019-12-31 00:00:00')

这些运算都可以移植到时间差的序列上:

td1 = pd.timedelta_range(start='1 days', periods=5)
td2 = pd.timedelta_range(start='12 hours',freq='2H',periods=5)
ts = pd.date_range('20200101', '20200105')
td1 * 5
TimedeltaIndex(['5 days', '10 days', '15 days', '20 days', '25 days'], dtype='timedelta64[ns]', freq='5D')
td1 * pd.Series(list(range(5))) # 逐个相乘
0    0 days
1    2 days
2    6 days
3   12 days
4   20 days
dtype: timedelta64[ns]
td1 - td2
TimedeltaIndex(['0 days 12:00:00', '1 days 10:00:00', '2 days 08:00:00','3 days 06:00:00', '4 days 04:00:00'],dtype='timedelta64[ns]', freq=None)
td1 + pd.Timestamp('20200101')
TimedeltaIndex(['0 days 12:00:00', '0 days 14:00:00', '0 days 16:00:00','0 days 18:00:00', '0 days 20:00:00'],dtype='timedelta64[ns]', freq='2H')
td1 + ts # 逐个相加
DatetimeIndex(['2020-01-02', '2020-01-04', '2020-01-06', '2020-01-08','2020-01-10'],dtype='datetime64[ns]', freq=None)

4. 日期偏置

4.1 Offset对象

日期偏置是一种和日历相关的特殊时间差,例如回到第一节中的两个问题:如何求2020年9月第一个周一的日期,以及如何求2020年9月7日后的第30个工作日是哪一天。

pd.Timestamp('20200831') + pd.offsets.WeekOfMonth(week=0,weekday=0)
Timestamp('2020-09-07 00:00:00')
pd.Timestamp('20200907') + pd.offsets.BDay(30)
Timestamp('2020-10-19 00:00:00')

从上面的例子中可以看到, Offset 对象在 pd.offsets 中被定义。当使用 + 时获取离其最近的下一个日期,当使用 - 时获取离其最近的上一个日期

pd.Timestamp('20200831') - pd.offsets.WeekOfMonth(week=0,weekday=0)
Timestamp('2020-08-03 00:00:00')
pd.Timestamp('20200907') - pd.offsets.BDay(30)
Timestamp('2020-07-27 00:00:00')
pd.Timestamp('20200907') + pd.offsets.MonthEnd()
Timestamp('2020-09-30 00:00:00')

常用的日期偏置如下:

日期偏移量 频率字符串 说明
DateOffset 通用偏移类,默认为一个日历日
BDay 或 BusinessDay ‘B’ 工作日
CDay 或 CustomBusinessDay ‘C’ 自定义工作日
Week ‘W’ 一周,可选周内固定某日
WeekOfMonth ‘WOM’ 每月第几周的第几天
LastWeekOfMonth ‘LWOM’ 每月最后一周的第几天
MonthEnd ‘M’ 日历日月末
MonthBegin ‘MS’ 日历日月初
BMonthEnd 或 BusinessMonthEnd ‘BM’ 工作日月末
BMonthBegin 或 BusinessMonthBegin ‘BMS’ 工作日月初
CBMonthEnd 或 CustomBusinessMonthEnd ‘CBM’ 自定义工作日月末
CBMonthBegin 或 CustomBusinessMonthBegin ‘CBMS’ 自定义工作日月初
SemiMonthEnd ‘SM’ 某月第 15 天(或其它半数日期)与日历日月末
SemiMonthBegin ‘SMS’ 日历日月初与第 15 天(或其它半数日期)
QuarterEnd ‘Q’ 日历日季末
QuarterBegin ‘QS’ 日历日季初
BQuarterEnd ‘BQ’ 工作日季末
BQuarterBegin ‘BQS’ 工作日季初
FY5253Quarter ‘REQ’ 零售季,又名 52-53 周
YearEnd ‘A’ 日历日年末
YearBegin ‘AS’ 或 ‘BYS’ 日历日年初
BYearEnd ‘BA’ 工作日年末
BYearBegin ‘BAS’ 工作日年初
FY5253 ‘RE’ 零售年(又名 52-53 周)
Easter 复活节假日
BusinessHour ‘BH’ 工作小时
CustomBusinessHour ‘CBH’ 自定义工作小时
Day ‘D’ 一天
Hour ‘H’ 一小时
Minute ‘T’ 或 ‘min’ 一分钟
Second ‘S’ 一秒
Milli ‘L’ 或 ‘ms’ 一毫秒
Micro ‘U’ 或 ‘us’ 一微秒
Nano ‘N’ 一纳秒

需要介绍一个特殊的 Offset 对象 CDay ,其中的 holidays, weekmask 参数能够分别对自定义的日期和星期进行过滤,前者传入了需要过滤的日期列表,后者传入的是三个字母的星期缩写构成的星期字符串,其作用是只保留字符串中出现的星期:

my_filter = pd.offsets.CDay(n=1,weekmask='Wed Fri',holidays=['20200109'])
dr = pd.date_range('20200108', '20200111')
dr.to_series().dt.dayofweek
2020-01-08    2
2020-01-09    3
2020-01-10    4
2020-01-11    5
Freq: D, dtype: int64
[i + my_filter for i in dr]
[Timestamp('2020-01-10 00:00:00'),Timestamp('2020-01-10 00:00:00'),Timestamp('2020-01-15 00:00:00'),Timestamp('2020-01-15 00:00:00')]

上面的例子中, n 表示增加一天 CDay , dr 中的第一天为 20200108 ,但由于下一天 20200109 被排除了,并且 20200110 是合法的周五,因此转为 20200110 ,其他后面的日期处理类似。

4.2 偏置字符串

前面提到了关于 date_range 的 freq 取值可用 Offset 对象,同时在 pandas 中几乎每一个 Offset 对象绑定了日期偏置字符串( frequencies strings/offset aliases ),可以指定 Offset 对应的字符串来替代使用。下面举一些常见的例子。

pd.date_range('20200101','20200331', freq='MS') # 月初
DatetimeIndex(['2020-01-01', '2020-02-01', '2020-03-01'], dtype='datetime64[ns]', freq='MS')
pd.date_range('20200101','20200331', freq='M') # 月末
DatetimeIndex(['2020-01-31', '2020-02-29', '2020-03-31'], dtype='datetime64[ns]', freq='M')
pd.date_range('20200101','20200110', freq='B') # 工作日
DatetimeIndex(['2020-01-01', '2020-01-02', '2020-01-03', '2020-01-06','2020-01-07', '2020-01-08', '2020-01-09', '2020-01-10'],dtype='datetime64[ns]', freq='B')
pd.date_range('20200101','20200201', freq='W-MON') # 周一
DatetimeIndex(['2020-01-06', '2020-01-13', '2020-01-20', '2020-01-27'], dtype='datetime64[ns]', freq='W-MON')
pd.date_range('20200101','20200201',freq='WOM-1MON') # 每月第一个周一
DatetimeIndex(['2020-01-06'], dtype='datetime64[ns]', freq='WOM-1MON')

上面的这些字符串,等价于使用如下的 Offset 对象:

pd.date_range('20200101','20200331',freq=pd.offsets.MonthBegin())
DatetimeIndex(['2020-01-01', '2020-02-01', '2020-03-01'], dtype='datetime64[ns]', freq='MS')
pd.date_range('20200101','20200331',freq=pd.offsets.MonthEnd())
DatetimeIndex(['2020-01-31', '2020-02-29', '2020-03-31'], dtype='datetime64[ns]', freq='M')
pd.date_range('20200101','20200110', freq=pd.offsets.BDay())
DatetimeIndex(['2020-01-01', '2020-01-02', '2020-01-03', '2020-01-06','2020-01-07', '2020-01-08', '2020-01-09', '2020-01-10'],dtype='datetime64[ns]', freq='B')
pd.date_range('20200101','20200201',freq=pd.offsets.CDay(weekmask='Mon'))
DatetimeIndex(['2020-01-06', '2020-01-13', '2020-01-20', '2020-01-27'], dtype='datetime64[ns]', freq='C')
pd.date_range('20200101','20200201',freq=pd.offsets.WeekOfMonth(week=0,weekday=0))
DatetimeIndex(['2020-01-06'], dtype='datetime64[ns]', freq='WOM-1MON')

5. 时序中的滑窗与分组

5.1 滑动窗口

所谓时序的滑窗函数,即把滑动窗口用 freq 关键词代替,下面给出一个具体的应用案例:在股票市场中有一个指标为 BOLL 指标,它由中轨线、上轨线、下轨线这三根线构成,具体的计算方法分别是 N 日均值线、 N 日均值加两倍 N 日标准差线、 N 日均值减两倍 N 日标准差线。利用 rolling 对象计算 N=30 的 BOLL 指标可以如下写出:

import matplotlib.pyplot as pltidx = pd.date_range('20200101', '20201231', freq='B')
np.random.seed(2020)
data = np.random.randint(-1,2,len(idx)).cumsum() # 随机游动构造模拟序列
s = pd.Series(data,index=idx)
s.head()
2020-01-01   -1
2020-01-02   -2
2020-01-03   -1
2020-01-06   -1
2020-01-07   -2
Freq: B, dtype: int32
r = s.rolling('30D')
plt.plot(s)
plt.title('BOLL LINES')
plt.plot(r.mean())
plt.plot(r.mean()+r.std()*2)
plt.plot(r.mean()-r.std()*2)

对于 shift 函数而言,作用在 datetime64 为索引的序列上时,可以指定 freq 单位进行滑动:

s.shift(freq='50D').head()
2020-02-20   -1
2020-02-21   -2
2020-02-22   -1
2020-02-25   -1
2020-02-26   -2
dtype: int32

另外, datetime64[ns] 的序列进行 diff 后就能够得到 timedelta64[ns] 的序列,这能够使用户方便地观察有序时间序列的间隔:

my_series = pd.Series(s.index)
my_series.head()
0   2020-01-01
1   2020-01-02
2   2020-01-03
3   2020-01-06
4   2020-01-07
dtype: datetime64[ns]
my_series.diff(1).head()
0      NaT
1   1 days
2   1 days
3   3 days
4   1 days
dtype: timedelta64[ns]

5.2 重采样

重采样对象 resample 和分组对象 groupby 的用法类似,前者是针对时间序列的分组计算而设计的分组对象。

例如,对上面的序列计算每10天的均值:

s.resample('10D').mean().head()
2020-01-01   -2.000000
2020-01-11   -3.166667
2020-01-21   -3.625000
2020-01-31   -4.000000
2020-02-10   -0.375000
Freq: 10D, dtype: float64

同时,如果没有内置定义的处理函数,可以通过 apply 方法自定义:

s.resample('10D').apply(lambda x:x.max()-x.min()).head() # 极差
2020-01-01    3
2020-01-11    4
2020-01-21    4
2020-01-31    2
2020-02-10    4
Freq: 10D, dtype: int32

在 resample 中要特别注意组边界值的处理情况,默认情况下起始值的计算方法是从最小值时间戳对应日期的午夜 00:00:00 开始增加 freq ,直到不超过该最小时间戳的最大时间戳,由此对应的时间戳为起始值,然后每次累加 freq 参数作为分割结点进行分组,区间情况为左闭右开。下面构造一个不均匀的例子:

idx = pd.date_range('20200101 8:26:35', '20200101 9:31:58', freq='77s')
data = np.random.randint(-1,2,len(idx)).cumsum()
s = pd.Series(data,index=idx)
s.head()
2020-01-01 08:26:35   -1
2020-01-01 08:27:52   -1
2020-01-01 08:29:09   -2
2020-01-01 08:30:26   -3
2020-01-01 08:31:43   -4
Freq: 77S, dtype: int32

下面对应的第一个组起始值为 08:24:00 ,其是从当天0点增加72个 freq=7 min 得到的,如果再增加一个 freq 则超出了序列的最小时间戳 08:26:35 :

s.resample('7min').mean().head()
2020-01-01 08:24:00   -1.750000
2020-01-01 08:31:00   -2.600000
2020-01-01 08:38:00   -2.166667
2020-01-01 08:45:00    0.200000
2020-01-01 08:52:00    2.833333
Freq: 7T, dtype: float64

有时候,用户希望从序列的最小时间戳开始依次增加 freq 进行分组,此时可以指定 origin 参数为 start :

s.resample('7min', origin='start').mean().head()
2020-01-01 08:26:35   -2.333333
2020-01-01 08:33:35   -2.400000
2020-01-01 08:40:35   -1.333333
2020-01-01 08:47:35    1.200000
2020-01-01 08:54:35    3.166667
Freq: 7T, dtype: float64

在返回值中,要注意索引一般是取组的第一个时间戳,但 M, A, Q, BM, BA, BQ, W 这七个是取对应区间的最后一个时间戳:

s = pd.Series(np.random.randint(2,size=366),index=pd.date_range('2020-01-01','2020-12-31'))
s.resample('M').mean().head()
2020-01-31    0.451613
2020-02-29    0.448276
2020-03-31    0.516129
2020-04-30    0.566667
2020-05-31    0.451613
Freq: M, dtype: float64
s.resample('MS').mean().head() # 结果一样,但索引不同
2020-01-01    0.451613
2020-02-01    0.448276
2020-03-01    0.516129
2020-04-01    0.566667
2020-05-01    0.451613
Freq: MS, dtype: float64

6. 练习

Ex1:太阳辐射数据集

现有一份关于太阳辐射的数据集:

df = pd.read_csv('G:/代码/joyful-pandas-master/data/solar.csv', usecols=['Data','Time','Radiation','Temperature'])
df.head()
Data Time Radiation Temperature
0 9/29/2016 12:00:00 AM 23:55:26 1.21 48
1 9/29/2016 12:00:00 AM 23:50:23 1.21 48
2 9/29/2016 12:00:00 AM 23:45:26 1.23 48
3 9/29/2016 12:00:00 AM 23:40:21 1.21 48
4 9/29/2016 12:00:00 AM 23:35:24 1.17 48
  1. 将 Datetime, Time 合并为一个时间列 Datetime ,同时把它作为索引后排序。

  2. 每条记录时间的间隔显然并不一致,请解决如下问题:

    a. 找出间隔时间的前三个最大值所对应的三组时间戳。

    b. 是否存在一个大致的范围,使得绝大多数的间隔时间都落在这个区间中?如果存在,请对此范围内的样本间隔秒数画出柱状图,设置 bins=50 。

  3. 求如下指标对应的 Series :

    a. 温度与辐射量的6小时滑动相关系数

    b. 以三点、九点、十五点、二十一点为分割,该观测所在时间区间的温度均值序列

    c. 每个观测6小时前的辐射量(一般而言不会恰好取到,此时取最近时间戳对应的辐射量)

1.将 Datetime, Time 合并为一个时间列 Datetime ,同时把它作为索引后排序

df['Data'] = pd.to_datetime(df.Data.str.extract('([/|\w]+\s).+')[0] + df.Time)
df = df.drop(columns='Time').rename(columns={'Data':'Datetime'}).set_index('Datetime').sort_index()
df.head()
Radiation Temperature
Datetime
2016-09-01 00:00:08 2.58 51
2016-09-01 00:05:10 2.83 51
2016-09-01 00:20:06 2.16 51
2016-09-01 00:25:05 2.21 51
2016-09-01 00:30:09 2.25 51

2.a 找出间隔时间的前三个最大值所对应的三组时间戳

tep = df.index.to_series().reset_index(drop=True).diff().dt.total_seconds().nlargest(3).index
df.index[tep.union(tep-1)]
DatetimeIndex(['2016-09-29 23:55:26', '2016-10-01 00:00:19','2016-11-29 19:05:02', '2016-12-01 00:00:02','2016-12-05 20:45:53', '2016-12-08 11:10:42'],dtype='datetime64[ns]', name='Datetime', freq=None)

2.b 是否存在一个大致的范围,使得绝大多数的间隔时间都落在这个区间中

s = df.index.to_series().reset_index(drop=True).diff().dt.total_seconds()
tep = s.mask((s>s.quantile(0.99))|(s<s.quantile(0.01)))
plt.hist(tep, bins=50)
plt.show()

3.a 温度与辐射量的6小时滑动相关系数

tep = df.Radiation.rolling('6H').corr(df.Temperature)
tep.head(10)
Datetime
2016-09-01 00:00:08         NaN
2016-09-01 00:05:10         NaN
2016-09-01 00:20:06         inf
2016-09-01 00:25:05        -inf
2016-09-01 00:30:09        -inf
2016-09-01 00:45:04        -inf
2016-09-01 00:50:06        -inf
2016-09-01 00:55:04   -0.338638
2016-09-01 01:00:07   -0.404863
2016-09-01 01:05:05   -0.462415
dtype: float64

3.b 以三点、九点、十五点、二十一点为分割,该观测所在时间区间的温度均值序列

tep = df.Temperature.resample('6H', origin='03:00:00').mean()
tep.head()
Datetime
2016-08-31 21:00:00    51.218750
2016-09-01 03:00:00    50.033333
2016-09-01 09:00:00    59.379310
2016-09-01 15:00:00    57.984375
2016-09-01 21:00:00    51.393939
Freq: 6H, Name: Temperature, dtype: float64

3.c 每个观测6小时前的辐射量

tep = df.index.shift(freq='-6H')
int_loc = [df.index.get_loc(i, method='nearest') for i in tep]
res = df.Radiation.iloc[int_loc]
res.tail()
Datetime
2016-12-31 17:35:01    15.96
2016-12-31 17:40:06    11.98
2016-12-31 17:45:02     9.33
2016-12-31 17:50:01     8.49
2016-12-31 17:55:02     5.84
Name: Radiation, dtype: float64

Ex2:水果销量数据集

现有一份2019年每日水果销量记录表:

df = pd.read_csv('G:/代码/joyful-pandas-master/data/fruit.csv')
df.head()
Date Fruit Sale
0 2019-04-18 Peach 15
1 2019-12-29 Peach 15
2 2019-06-05 Peach 19
3 2019-10-28 Pear 19
4 2019-06-11 Peach 5
  1. 统计如下指标:

    a. 每月上半月(15号及之前)与下半月葡萄销量的比值

    b. 每月最后一天的生梨销量总和

    c. 每月最后一天工作日的生梨销量总和

    d. 每月最后五天的苹果销量均值

  2. 按月计算周一至周日各品种水果的平均记录条数,行索引外层为水果名称,内层为月份,列索引为星期。

  3. 按天计算向前10个工作日窗口的苹果销量均值序列,非工作日的值用上一个工作日的结果填充。

1.a 每月上半月(15号及之前)与下半月葡萄销量的比值

df.Date = pd.to_datetime(df.Date)
# 筛选葡萄的销售量
df_grape = df.query("Fruit == 'Grape'")
df_grape.head()
Date Fruit Sale
5 2019-05-19 Grape 17
12 2019-06-16 Grape 28
17 2019-08-11 Grape 25
18 2019-03-29 Grape 20
25 2019-07-09 Grape 13
df_grape['type'] = np.where(df_grape.Date.dt.day<=15,'First','Second')
df_grape.head()
Date Fruit Sale type
5 2019-05-19 Grape 17 Second
12 2019-06-16 Grape 28 Second
17 2019-08-11 Grape 25 First
18 2019-03-29 Grape 20 Second
25 2019-07-09 Grape 13 First
tep = df_grape.groupby(['type',df_grape.Date.dt.month])['Sale'].mean()
tep['First']/tep['Second']
Date
1     1.174998
2     0.968890
3     0.951351
4     1.020797
5     0.931061
6     1.163553
7     1.184920
8     1.041966
9     1.070703
10    1.005624
11    1.026276
12    0.972197
Name: Sale, dtype: float64

1.b 每月最后一天的生梨销量总和

df[df.Date.dt.is_month_end].query("Fruit == 'Pear'").groupby('Date').Sale.sum().head()
Date
2019-01-31    847
2019-02-28    774
2019-03-31    761
2019-04-30    648
2019-05-31    616
Name: Sale, dtype: int64

1.c 每月最后一天工作日的生梨销量总和

df[df.Date.isin(pd.date_range('20190101','20191231',freq='BM'))].query("Fruit == 'Pear'").groupby('Date').Sale.sum().head()
Date
2019-01-31    847
2019-02-28    774
2019-03-29    510
2019-04-30    648
2019-05-31    616
Name: Sale, dtype: int64

1.d 每月最后五天的苹果销量均值

# 每个月的最后5天
tep = df.drop_duplicates().groupby(df.Date.drop_duplicates().dt.month)['Date'].nlargest(5).reset_index(drop=True)
tep.head()
0   2019-01-31
1   2019-01-30
2   2019-01-29
3   2019-01-28
4   2019-01-27
Name: Date, dtype: datetime64[ns]
# 每个月最后5天苹果的销量
res = df.set_index('Date').loc[tep].reset_index().query("Fruit == 'Apple'")
res.head()
Date Fruit Sale
4 2019-01-31 Apple 18
21 2019-01-31 Apple 36
24 2019-01-31 Apple 65
30 2019-01-31 Apple 74
38 2019-01-31 Apple 77
# 根据月份进行分组求均值
res = res.groupby(res.Date.dt.month)['Sale'].mean().rename_axis('Month')
res.head()
Month
1    65.313725
2    54.061538
3    59.325581
4    65.795455
5    57.465116
Name: Sale, dtype: float64

Pandas学习——时序数据相关推荐

  1. Data Whale第20期组队学习 Pandas学习—时序数据

    Data Whale第20期组队学习 Pandas学习-时序数据 一.时序中的基本对象 二.时间戳 2.1 Timestamp的构造与属性 2.2 Datetime序列的生成 2.3 dt对象 2.4 ...

  2. Pandas处理时序数据(初学者必会)!

    ↑↑↑关注后"星标"Datawhale每日干货 & 每月组队学习,不错过Datawhale干货 作者:耿远昊,Datawhale成员,华东师范大学 时序数据是指时间序列数据 ...

  3. pandas基于时序数据计算模型预测推理需要的统计数据(累计时间、长度变化、变化率、方差、均值、最大、最小等):范围内的统计量、变化率、获得数据集最后的几条数据的统计量、变化率、获得范围内的统计量

    pandas基于时序数据计算模型预测推理需要的统计数据(累计时间.长度变化.变化率.方差.均值.最大.最小等):范围内的统计量.变化率.获得数据集最后的几条数据的统计量.变化率.获得范围内的统计量 目 ...

  4. pandas基于时序数据计算模型预测推理需要的统计数据(累计时间、长度变化、变化率、方差、均值、最大、最小等):数据持续的时间(分钟)、获得某一节点之后的数据总变化量、获得范围内的统计量

    pandas基于时序数据计算模型预测推理需要的统计数据(累计时间.长度变化.变化率.方差.均值.最大.最小等):数据持续的时间(分钟).获得某一节点之后的数据总变化量.获得范围内的统计量 目录

  5. pandas之时序数据

    pandas之时序数据 一. 时序的创建 时间点的创建 (1)to_datetime方法 pandas在时间点进阿里的输入格式在规定上给了很大的自由度,下面都可以: pd.to_datetime('2 ...

  6. Data Whale第20期组队学习 Pandas学习—缺失数据

    Data Whale第20期组队学习 Pandas学习-缺失数据 一.缺失值的统计和删除 1.1 统计缺失信息 1.2 删除缺失信息 二.缺失值的填充和插值 2.1 利用fillna进行填充 2.2 ...

  7. Datawhale Pandas task10 时序数据

    一.时序中的基本对象 时间序列的概念在日常生活中十分常见,但对于一个具体的时序事件而言,可以从多个时间对象的角度来描述.例如2020年9月7日周一早上8点整需要到教室上课,这个课会在当天早上10点结束 ...

  8. datawhale pandas 打卡10 时序数据

    内容简介 这次文章的内容是pandas的文本数据 文章目录 内容简介 时序中的基本对象 时间戳 时间戳的创建 时间戳的上下限 Datetime序列的生成 dt对象 时间戳的切片与索引 时间差 Time ...

  9. python处理时序数据总结

    pandas处理时序数据容易出错,经过多次摸索之后总结如下: #先读取数据 data=pd.read_csv(fs, header=None, sep=';',index_col=False,enco ...

最新文章

  1. 双击U盘时既能运行程序又能打开U盘
  2. idea如何全局查找和替换
  3. Linux知识积累(2)dirname的使用方法
  4. 计算机网络学习笔记(17. 计算机网络作业一)
  5. 《OpenGL编程指南(原书第9版)》——2.3 OpenGL着色语言概述
  6. MikroTik RouterOS使用U盘安装提示FATAL ERROR: no CD-ROM found Press ENTER to reboot的问题解决...
  7. 【渝粤教育】电大中专液压与气动技术作业 题库
  8. 电能质量分析仪安装(access数据库版本 /WIN10系统) 出现
  9. SecureCRT下载、安装、激活
  10. go 导出 html 报告(使用 hero 预编译 html 模板引擎)
  11. 利用ffmpeg将H264解码为RGB
  12. angular 万年历_jQuery实现的简单日历组件定义与用法示例
  13. qq邮箱如何在win10邮箱连接到服务器,win10自带邮箱如何使用?win10自带邮箱如何同步qq邮箱邮件?...
  14. 实现短信验证码有效时间
  15. 职业规划,如何月入1万、3万、5万、10万?
  16. 【大道至简】机器学习算法之EM算法(Expectation Maximization Algorithm)详解(附代码)---通俗理解EM算法。
  17. 关于立创EDA专业版图纸尺寸的设置问题
  18. 后台用户角色权限管理设计
  19. 社区运营秘笈:病毒式营销!
  20. 【JavaScript设计模式张容铭】抽象工厂模式深度剖析——关于继承与constructor属性的深度理解

热门文章

  1. 数值分析:矩阵求逆-奇异性、条件数
  2. DNS(Domain Name System,域名系统)、免费公共DNS服务列表
  3. html刷新网页基本方法
  4. 激光投影行业正在发生四大变化
  5. 如何让网页中所有链接都在新窗口打开
  6. 错误: 在类 shiyan.Triangle 中找不到 main 方法, 请将 main 方法定义为: public static void main(String[] args)
  7. cad面积累计lisp怎么用_cad中面积及重量、长度及等分、长度及总长、导出文字、数字编号等lisp应用...
  8. 新玺配资:能耗双控双刃剑 造纸板块一飞冲天
  9. 联发科处理器被抓跑分“作弊”:P95性能比天玑1000L还高?
  10. Python自然语言处理 | 获得文本语料与词汇资源