【学习笔记】《深入浅出Pandas》第14章:Pandas时序数据

14.1 固定时间

14.1.1 时间的表示

固定时间是指一个时间点,如2020年11月11日00:00:00。在计算机中,时间多用时间戳(Timestamp)表示,它指的是格林威治时间1970年1月1日00时00分00秒起至当下的总秒数。
Python的官网库datetime支持创建和处理时间:

import datetime
# 当前时间
datetime.datetime.now()
# datetime.datetime(2022, 9, 29, 9, 46, 20, 679667)

# 指定时间
datetime.datetime(2020, 11, 1, 19)
# datetime.datetime(2020, 11, 1, 19, 0)

# 指定时间
datetime.datetime(year=2020, month=11, day=11)
# datetime.datetime(2020, 11, 11, 0, 0)

14.1.2 创建时间点

pd.Timestamp()是Pandas定义时间的主要函数,代替Python中的datetime.datetime()对象。

(1)使用datetime.datetime函数:

import datetime
# 至少需要年、月、日
pd.Timestamp(datetime.datetime(2020, 6, 8))
# Timestamp('2020-06-08 00:00:00')

# 指定时、分、秒
pd.Timestamp(datetime.datetime(2020, 6, 8, 16, 17, 18))
# Timestamp('2020-06-08 16:17:18')

(2)指定时间字符串:

pd.Timestamp('2012-05-01')
# Timestamp('2012-05-01 00:00:00')
pd.Timestamp('2017-01-01T12')
# Timestamp('2017-01-01 12:00:00')

(3)指定时间位置数字,可依次定义year、month、day、hour、minute、second、microsecond:

pd.Timestamp(2012, 5, 1)
# Timestamp('2012-05-01 00:00:00')
pd.Timestamp(year=2017, month=1, day=1, hour=12)
# Timestamp('2017-01-01 12:00:00')

(4)解析时间戳:

pd.Timestamp(1513393355.5, unit='s') # 单位为秒
# Timestamp('2017-12-16 03:02:35.500000')

(5)用tz指定时区,需要记住北京时间值为Asia/Shanghai:

pd.Timestamp(1513393355, unit='s', tz='US/Pacific')
# Timestamp('2017-12-15 19:02:35-0800', tz='US/Pacific')
pd.Timestamp(1513393355, unit='s', tz='Asia/Shanghai')
# Timestamp('2017-12-16 11:02:35+0800', tz='Asia/Shanghai')

(6)获取到当前时间,从而可通过属性取到今天的日期、年份等信息:

pd.Timestamp('today')
pd.Timestamp('now') # 效果同上
# Timestamp('2022-09-29 09:59:37.361192')

pd.Timestamp('today').date() # 只取日期
# datetime.date(2022, 9, 29)

(7)通过当前时间计算出昨天、明天等信息:

# 昨天
pd.Timestamp('now') - pd.Timedelta(days=1)
# Timestamp('2022-09-28 10:01:06.419889')

# 明天
pd.Timestamp('now') + pd.Timedelta(days=1)
# Timestamp('2022-09-30 10:01:40.628220')

# 当月初,一日
pd.Timestamp('now').replace(day=1)
# Timestamp('2022-09-01 10:03:20.150130')

(8)pd.to_datetime() 也可以实现上述功能,不过常用在时间转换上。

pd.to_datetime('now')
# Timestamp('2022-09-29 02:04:34.651490')

(9)由于Pandas以纳秒粒度表示时间戳,因此可以使用64位整数表示时间跨度限制为大约584年,意味着能表示的时间范围有最早和最晚的限制:

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

不过,Pandas给出了一个解决方案:使用PeriodIndex解决。

14.1.3 属性

(1)定义一个当前时间:

time = pd.Timestamp('now')
# Timestamp('2022-09-29 10:07:48.470740')

(2)以下是丰富的时间属性:

time.asm8 # 返回Numpy datetime64格式(以纳秒为单位)
# numpy.datetime64('2022-09-29T10:07:48.470740000')
time.dayofweek # 3 (周几,周一为0)
time.dayofyear # 272 (一年的第几天)
time.days_in_month # 30 (当月有多少天)
time.daysinmonth # 30 同上
time.freqstr # None (周期字符)
time.is_leap_year # False (是否闰年)
time.is_month_end # False (是否当月最后一天)
time.is_month_start # False (是否当月第一天)
time.is_quarter_end # False (是否当季最后一天)
time.is_quarter_start # False (是否当季第一天)
time.is_year_end # False (是否当年最后一天)
time.is_year_start # False (是否当年第一天)
time.quarter # 3 (当前季度数)
time.tz # None(当前时区别名,如果指定,会返回类似<DstTzInfo 'Asia/Shanghai' CST+8:00:00 STD>)
time.week # 39 (当前周数)
time.weekofyear # 39 同上
time.day # 29 (日)
time.fold # 0
time.freq # None (频度周期)
time.hour # 10()
time.microsecond #470740(微秒)
time.minute # 7
time.month # 9
time.nanosecond # 0
time.second # 48
time.tzinfo # None
time.value # 1664446068470740000
time.year # 2022

14.1.4 时间的方法

取当前时间,并指定时区为北京时间:

time = pd.Timestamp('now', tz='Asia/Shanghai')
# Timestamp('2022-09-29 10:19:34.762386+0800', tz='Asia/Shanghai')

(1)转换为指定时区:

time.astimezone('UTC')
# Timestamp('2022-09-29 02:19:34.762386+0000', tz='UTC')

(2)转换单位,向上舍入:

time.ceil('s') # 转为以秒为单位
# Timestamp('2022-09-29 10:19:35+0800', tz='Asia/Shanghai')
time.ceil('ns') # 转为以纳秒为单位
time.ceil('d') # 保留日
time.ceil('h') # 保留时
# Timestamp('2022-09-29 11:00:00+0800', tz='Asia/Shanghai')

(3)转换单位,向下舍入:

time.floor('h') # 保留时
# Timestamp('2022-09-29 10:00:00+0800', tz='Asia/Shanghai')

(4)转换单位,四舍五入:

time.round('h') # 保留时
# Timestamp('2022-09-29 10:00:00+0800', tz='Asia/Shanghai')

(5)返回星期和月份名:

time.day_name() # 'Thursday'
time.month_name() # 'September'

(6)将时间戳规范为午夜,保留tz信息:

time.normalize()
# Timestamp('2022-09-29 00:00:00+0800', tz='Asia/Shanghai')

(7)将时间元素替换datetime.replace,可处理微秒:

time.replace(year=2019) # 替换年份
# Timestamp('2019-09-29 10:19:34.762386+0800', tz='Asia/Shanghai')
time.replace(month=8) # 替换月份
# Timestamp('2022-08-29 10:19:34.762386+0800', tz='Asia/Shanghai')

(8)转换为周期类型,丢弃时区:

time.to_period(freq='h') # 周期为小时
# Period('2022-09-29 10:00', 'H')

(9)转换为指定时区:

time.tz_convert('UTC')
# Timestamp('2022-09-29 02:19:34.762386+0000', tz='UTC')

(10)本地化时区转换:

time = pd.Timestamp('now')
time.tz_localize('Asia/Shanghai')
# Timestamp('2022-09-29 10:30:31.041733+0800', tz='Asia/Shanghai')
time.tz_localize(None) # 删除时区
# Timestamp('2022-09-29 10:30:54.821096')

14.1.5 时间缺失值

(1)对于时间的缺失值,有专门的NaT来表示:

pd.Timestamp(pd.NaT)
# NaT
pd.Timedelta(pd.NaT)
# NaT
pd.Period(pd.NaT)
# NaT

# 类似np.nna
pd.NaT == pd.NaT # False

(2)NaT可以代表固定时间、时长、时间周期为空的情况,类似于np.nan可以参与到时间的各种计算中:

pd.NaT + pd.Timestamp('20200101')
# NaT

pd.NaT + pd.Timedelta('2 days')
# NaT

14.2 时长数据

两个固定时间相减会得到时间差或者时长

14.2.1 创建时间差

pd.Timedelta() 对象表示时间差,也就是时长,以差异单位表示,例如天、小时等。既可以是正数,又可以是负数。
(1)两个固定时间相减会产生时间差:

pd.Timestamp('2020-11-01 15') - pd.Timestamp('2020-11-01 14')
# Timedelta('0 days 01:00:00')

(2)传入字符串:

pd.Timedelta('1 days')
# Timedelta('1 days 00:00:00')

pd.Timedelta('1 days 00:00:00')
# Timedelta('1 days 00:00:00')

pd.Timedelta('1 days 2 min 3 us')
# Timedelta('1 days 00:02:00.000003')

(3)用关键字参数指定时间:

pd.Timedelta(days=5, seconds=10)
# Timedelta('5 days 00:00:10')

# 可以将指定分钟转化为天和小时
pd.Timedelta(minutes=3242)
# Timedelta('2 days 06:02:00')

(4)使用带周期量的偏移量别名:

pd.Timedelta('1D')
# Timedelta('1 days 00:00:00')
pd.Timedelta('2W')
# Timedelta('14 days 00:00:00')
pd.Timedelta('1D2H3M4S')
# Timedelta('1 days 02:03:04')

(5)带单位的整型数字:

# 一天
pd.Timedelta(1, unit='d')
# Timedelta('1 days 00:00:00')
pd.Timedelta(100, unit='s')
# Timedelta('0 days 00:01:40')

(6)使用Python内置的datetime.timedelta或者Numpy的np.timedelta64:

pd.Timedelta(datetime.timedelta(days=1, minutes=10))
# Timedelta('1 days 00:10:00')
pd.Timedelta(np.timedelta64(100, 'ns'))
# Timedelta('0 days 00:00:00.000000100')

(7)负值:

pd.Timedelta('-1min')
# Timedelta('-1 days +23:59:00')

(8)缺失值:

pd.Timedelta('nan')
# NaT
pd.Timedelta('nat')
# NaT

(9)标准字符串(ISO 8601 Duration strings):

pd.Timedelta('P0DT0H1M0S')
# Timedelta('0 days 00:01:00')
pd.Timedelta('P0DT0H0M0.000000123S')
# Timedelta('0 days 00:00:00.000000123')

(10)使用时间偏移对象DateOffsets(Day, Hour, Minute, Second, Milli, Micro, Nano)直接创建:

# 两分钟
pd.Timedelta(pd.offsets.Minute(2))
# Timedelta('0 days 00:02:00')

(11)pd.to_timedelta()也可以完成上述操作,不过大多用于时长类型的数据转换上:

pd.to_timedelta(pd.offsets.Day(3))
# Timedelta('3 days 00:00:00')
pd.to_timedelta('15.5min')
# Timedelta('0 days 00:15:30')
pd.to_timedelta(124524564574835)
# Timedelta('1 days 10:35:24.564574835')

和时间戳数据一样,时长数据的存储也有上下限:

pd.Timedelta.min
# Timedelta('-106752 days +00:12:43.145224193')
pd.Timedelta.max
# Timedelta('106751 days 23:47:16.854775807')

如果想处理更大的时长数据,可以将其转换为一定单位的数字类型。

14.2.2 时长的加减

(1)相加,多个时长累积为一个更长的时长:

pd.Timedelta(pd.offsets.Day(1)) + pd.Timedelta(pd.offsets.Hour(5))
# Timedelta('1 days 05:00:00')

(2)相减:

pd.Timedelta(pd.offsets.Day(1)) - pd.Timedelta(pd.offsets.Hour(5))
# Timedelta('0 days 19:00:00')

(3)固定时间与时长相加减会得到一个新的固定时间:

pd.Timestamp('2020-11-11') - pd.Timedelta(pd.offsets.Day(1))
# Timestamp('2020-11-10 00:00:00')

14.2.3 时长的属性

时长数据中可以解析出指定时间计数单位的值,如小时、秒。

tdt = pd.Timedelta('10 days 9 min 3 sec')
tdt.days # 10
tdt.seconds # 543
(-tdt).days # -11
tdt.value # 864543000000000 时间戳

14.2.4 时长索引

时长数据可以作为索引(TimedeltaIndex),使用的场景比较少。比如在一项体育运动中,分别有2分钟完成、4分钟完成等。

14.3 时间序列

将众多固定时间组织起来就形成了时间序列,即时序数据

14.3.1 时序索引

DatetimeIndex是时间索引对象,一般由to_datetime()date_range() 来创建:
(1)使用to_datetime()

pd.to_datetime(['11/1/2020', # 类时间字符串
                np.datetime64('2020-11-02'), # Numpy的事件类型
                datetime.datetime(2020, 11, 3)]) # Python自带时间类型
# DatetimeIndex(['2020-11-01', '2020-11-02', '2020-11-03'], dtype='datetime64[ns]', freq=None)

(2)date_range() 可以给定开始或者结束时间,并给定周期数据、周期频率,会自动生成在此范围内的时间索引数据:

# 默认频率为天
pd.date_range('2020-01-01', periods=10)
pd.date_range('2020-01-01', '2020-01-10') # 同上
pd.date_range(end='2020-01-10', periods=10) # 同上
"""
DatetimeIndex(['2020-01-01', '2020-01-02', '2020-01-03', '2020-01-04',
               '2020-01-05', '2020-01-06', '2020-01-07', '2020-01-08',
               '2020-01-09', '2020-01-10'],
              dtype='datetime64[ns]', freq='D')
"""

(3)pd.bdate_range()生成数据可以跳过周六日,实现工作日的时间索引序列:

# 频率为工作日
pd.bdate_range('2020-11-1', periods=10)
"""
DatetimeIndex(['2020-11-02', '2020-11-03', '2020-11-04', '2020-11-05',
               '2020-11-06', '2020-11-09', '2020-11-10', '2020-11-11',
               '2020-11-12', '2020-11-13'],
              dtype='datetime64[ns]', freq='B')
"""

14.3.2 创建时序数据

创建包含时序的Series和DataFrame与创建普通的Series和DataFrame一样,将时序索引序列作为索引或者将时间列转换为时间类型。
(1)创建Series:

# 生成时序索引
tidx = pd.date_range('2020-11-1', periods=10)
# 应用时序索引
s = pd.Series(range(len(tidx)), index=tidx)
"""
2020-11-01    0
2020-11-02    1
2020-11-03    2
2020-11-04    3
2020-11-05    4
2020-11-06    5
2020-11-07    6
2020-11-08    7
2020-11-09    8
2020-11-10    9
Freq: D, dtype: int64
"""

将时间作为Series内容,则序列的数据类型为datetime64[ns]:

pd.Series(tidx)
"""
0   2020-11-01
1   2020-11-02
2   2020-11-03
3   2020-11-04
4   2020-11-05
5   2020-11-06
6   2020-11-07
7   2020-11-08
8   2020-11-09
9   2020-11-10
dtype: datetime64[ns]
"""

(2)创建DataFrame:

# 索引
tidx = pd.date_range('2020-11-1', periods=10)
# 应用索引生成DataFrame
df = pd.DataFrame({'A': range(len(tidx)), 'B': range(len(tidx))[::-1]},
 index=tidx)
"""
            A   B
2020-11-01  0   9
2020-11-02  1   8
2020-11-03  2   7
2020-11-04  3   6
2020-11-05  4   5
2020-11-06  5   4
2020-11-07  6   3
2020-11-08  7   2
2020-11-09  8   1
2020-11-10  9   0
"""

14.3.3 数据访问

# 时序索引数据创建
idx = pd.date_range('1/1/2020', '12/1/2021', freq='H')
ts = pd.Series(np.random.randn(len(idx)), index=idx)
"""
2020-01-01 00:00:00   -0.204984
2020-01-01 01:00:00   -0.693256
                         ...   
2021-11-30 23:00:00   -0.086156
2021-12-01 00:00:00    1.237712
Freq: H, Length: 16801, dtype: float64
"""

(1)使用[ ]、loc,按切片的操作对数据进行访问:

# 指定区间
ts[5:10]
"""
2020-01-01 05:00:00   -1.098832
2020-01-01 06:00:00    1.156500
2020-01-01 07:00:00    0.127538
2020-01-01 08:00:00   -0.077853
2020-01-01 09:00:00    2.063492
Freq: H, dtype: float64
"""

# 只筛选2020年
ts['2020']

(2)支持传入时间字符和各种时间对象:

# 指定天,以下结果相同
ts['11/30/2020']
ts['2020-11-30']
ts['20201130']
# 指定时间点
ts[datetime.datetime(2020, 11, 30)]
ts[pd.Timestamp(2020, 11, 30)]
ts[pd.Timestamp('2020-11-30')]
ts[np.datetime64('2020-11-30')]

(3)使用部分字符查询一定范围内的数据:

ts['2021'] # 查询整个2021年
ts['2021-6'] # 查询20216月
ts['2021-6':'2021-10'] # 查询20216月到10月
dft['2021-1':'2021-2-28 00:00:00'] # 精确时间
df2.loc['2020-01-05']

# 索引选择器
idx = pd.IndexSlice
dft2.loc[idx[:, '2020-01-05'], :]

# 带时区,原数据时区可能不是这个
df['2020-01-01 12:00:00+04:00:':'2020-01-01 13:00:00+04:00']

(4)使用ts.resolution查看序列的粒度(频率):

# 时间粒度(频率)
ts.index.resulution
# 'hour'

(5)使用df.truncate()对时间序列进行截取:

# 给定开始时间和结束时间
ts.truncate(before='2020-11-10 11:20', after='2020-12')

14.3.4 类型转换

由于时间格式样式比较多,很多情况下,Pandas并不能自动将时序数据识别为时间类型,因此需要专门对数据进行时间类型转换:
(1)astype只能针对相对标准的时间格式:

s  = pd.Series(['2020-11-01 01:10', '2020-11-11 11:10'])
"""
0    2020-11-01 01:10
1    2020-11-11 11:10
dtype: object
"""

①从数据内容上看,s符合时序格式,但要想让它成为时间类型,需要用astype进行转换:

s.astype('datetime64[ns]')
"""
0   2020-11-01 01:10:00
1   2020-11-11 11:10:00
dtype: datetime64[ns]
"""

②修改频率:

# 转为时间类型,指定频率为天
s.astype('datetime64[D]')
"""
0   2020-11-01
1   2020-11-11
dtype: datetime64[ns]
"""

③指定时区:

# 转为时间类型, 指定时区为北京时间
s.astype('datetime64[ns, Asia/Shanghai]')
"""
0   2020-11-01 01:10:00+08:00
1   2020-11-11 11:10:00+08:00
dtype: datetime64[ns, Asia/Shanghai]
"""

(2)使用pd.to_datetime()
①转换时间类型:

pd.to_datetime(s)

②将多列组合成一个时间进行转换:

df = pd.DataFrame({'year': [2020, 2020, 2020],
                   'month': [10, 11, 12],
                   'day': [10, 11, 12]})
"""
 	year 	month 	day
0 	2020 	10 		10
1 	2020 	11 		11
2 	2020 	12 		12
"""

# 转为时间类型
pd.to_datetime(df)
pd.to_datetime(df[['year', 'month', 'day']]) # 同上
"""
0   2020-10-10
1   2020-11-11
2   2020-12-12
dtype: datetime64[ns]
"""

③对于Series,pd.to_datetime()会智能识别其时间格式并进行转换:

s = pd.Series(['2020-11-01 01:10', '2020-11-11 11:10', None])
pd.to_datetime(s)
"""
0   2020-11-01 01:10:00
1   2020-11-11 11:10:00
2                   NaT
dtype: datetime64[ns]
"""

④对于列表,pd.to_datetime()也会智能识别其时间格式并转为时间序列索引:

pd.to_datetime(['2020/11/11', '2020/12/12'])
# DatetimeIndex(['2020-11-11', '2020-12-12'], dtype='datetime64[ns]', freq=None)

pd.to_datetime(['1-10-2020 10:00'], dayfirst=True) # 按日期在前解析
# DatetimeIndex(['2020-10-01 10:00:00'], dtype='datetime64[ns]', freq=None)

(3)使用pd.DatetimeIndex直接转为时间序列索引:

# 转为时间序列索引,自动推断频率
pd.DatetimeIndex(['20201101', '20201102'], freq='infer')
# DatetimeIndex(['2020-11-01', '2020-11-02'], dtype='datetime64[ns]', freq=None)

(4)针对单个时间,使用pd.Timestamp()转换为时间格式:

pd.to_datetime('2020/11/12')
# Timestamp('2020-11-12 00:00:00')

pd.Timestamp('2020/11/12')
# Timestamp('2020-11-12 00:00:00')

14.3.5 按格式转换

(1)如果原数据的格式是不规范的时间规范数据,可以通过格式映射来将其转为时间数据:

# 不规则格式转换时间
pd.to_datetime('2020_11_11', format='%Y_%m_%d', errors='ignore')
# Timestamp('2020-11-11 00:00:00')

以上时间数据用下划线连接各个部分,形式不规范,需要通过format参数来匹配此格式,将对应部分分配给年月日。

(2)更多实例如下:

# 让系统自己推断时间格式
pd.to_datetime('20200101', infer_datetime_format=True, errors='ignore')
# Timestamp('2020-01-01 00:00:00')

# 将errors参数设置为coerce,将不会忽略错误,返回空值
pd.to_datetime('20200101', format='%Y%m%d', errors='coerce')
# Timestamp('2020-01-01 00:00:00') 
# 与书本不一致

# 列转为字符串,再改为时间类型
#pd.to_datetime(df.d.astype(str), format='%m/%d/%Y')

# 其他
pd.to_datetime('2020/11/12', format='%Y/%m/%d')
# Timestamp('2020-11-12 00:00:00')

pd.to_datetime('01-01-2020 00:00', format='%d-%m-%Y %H:%M')
# Timestamp('2020-01-01 00:00:00')

# 对时间戳进行转换,需要给出时间单位,一般为秒
pd.to_datetime(1490195805, unit='s')
# Timestamp('2017-03-22 15:16:45')

可以将数字列表转换为时间:

pd.to_datetime([10, 11, 12, 15], unit='D', origin=pd.Timestamp('2020-11-01'))
# DatetimeIndex(['2020-11-11', '2020-11-12', '2020-11-13', '2020-11-16'], dtype='datetime64[ns]', freq=None)

14.3.6 时间访问器.dt

.dt.< method >可以以time.dt.xxx的形式来访问时间序列数据的属性和调用他们的方法,返回对应值的序列。

# 创建时间
s = pd.Series(pd.date_range('2020-11-01', periods=5, freq='d'))

# 对应的星期几
s.dt.day_name()
"""
0       Sunday
1       Monday
2      Tuesday
3    Wednesday
4     Thursday
dtype: object
"""

以下列出时间访问器的一些属性和方法:

# 时间访问操作
s.dt.date
s.dt.time
s.dt.timetz

# 以下为时间各成分的值
s.dt.year
s.dt.month
s.dt.day
s.dt.hour
s.dt.minute
s.dt.second
s.dt.microsecond
s.dt.nanosecond

# 以下为与周、月、年相关的属性
s.dt.week
s.dt.weekofyear
s.dt.dayofweek
s.dt.weekday
s.dt.dayofyear
s.dt.quarter # 季度数
s.dt.is_month_start
s.dt.is_month_end
s.dt.is_quarter_start
s.dt.is_quarter_end
s.dt.is_year_start
s.dt.is_year_end
s.dt.is_leap_year # 是否闰年
s.dt.daysinmonth # 当月有多少天
s.dt.days_in_month

s.dt.tz
s.dt.freq # 频率

# 以下为转换方法
s.dt.to_period
s.dt.to_pydatetime
s.dt.tz_localize
s.dt.tz_convert
s.dt.normalize
s.dt.strftime

s.dt.round(freq='D') # 类似四舍五入
s.dt.floor(freq='D') # 向下舍入为天
s.dt.ceil(freq='D') # 向上舍入为天

s.dt.month_name
s.dt.day_name
s.dt.start_time
s.dt.end_time
s.dt.days
s.dt.seconds
s.dt.components # 各时间成分的值
s.dt.to_pytimedelta # 转为Python时间格式
s.dt.total_seconds # 总秒数

# 个别用法举例
# 将时间转为UTC时间,再转为美国东部时间
s.dt.tz_localize('UTC').dt.tz_convert('US/Eastern')
# 输出时间显示格式
s.dt.strftime('%Y/%m/%d')

14.3.7 时长数据访问器

时长数据访问器可以解析出时长的相关属性,最终产出一个结果序列:

# 创建数据
ts = pd.Series(pd.to_timedelta(np.arange(5), unit='hour'))
"""
0   0 days 00:00:00
1   0 days 01:00:00
2   0 days 02:00:00
3   0 days 03:00:00
4   0 days 04:00:00
dtype: timedelta64[ns]
"""
# 计算秒数
ts.dt.seconds
"""
0        0
1     3600
2     7200
3    10800
4    14400
dtype: int64
"""
# 转为Python时间格式
ts.dt.to_pytimedelta()
"""
array([datetime.timedelta(0), datetime.timedelta(0, 3600),
       datetime.timedelta(0, 7200), datetime.timedelta(0, 10800),
       datetime.timedelta(0, 14400)], dtype=object)
"""

14.3.8 时序数据移动

(1)shift()方法可以在时序对象上实现向上或向下移动:

rng = pd.date_range('2020-11-01', '2020-11-04')
ts = pd.Series(range(len(rng)), index=rng)
"""
2020-11-01    0
2020-11-02    1
2020-11-03    2
2020-11-04    3
Freq: D, dtype: int64
"""

# 向上移动一位
ts.shift(-1)
"""
2020-11-01    1.0
2020-11-02    2.0
2020-11-03    3.0
2020-11-04    NaN
Freq: D, dtype: float64
"""

(2)shift方法接受freq频率参数,该参数可以接受DateOffset类或者其他类似timedelta的对象,也可以接受偏移别名:

# 向上移动一个工作日,11-01是周日
ts.shift(-1, freq='B')
"""
2020-10-30    0
2020-10-30    1
2020-11-02    2
2020-11-03    3
dtype: int64
"""

14.3.9 频率转换

更换时间频率是将时间序列由一个频率单位更换为另一个频率单位,实现时间粒度的变化。
主要通过asfreq()方法来实现。

# 创建时间:频率为自然日的时间序列
rng  = pd.date_range('2020-11-01', '2020-12-01')
ts = pd.Series(range(len(rng)), index=rng)  
"""
2020-11-01     0
...
2020-12-01    30
Freq: D, dtype: int64
"""

(1)将频率变为更细的粒度,会产生缺失值:

# 频率转为12小时
ts.asfreq(pd.offsets.Hour(12))

"""
2020-11-01 00:00:00     0.0
2020-11-01 12:00:00     NaN
2020-11-02 00:00:00     1.0
2020-11-02 12:00:00     NaN
2020-11-03 00:00:00     2.0
                       ... 
2020-11-29 00:00:00    28.0
2020-11-29 12:00:00     NaN
2020-11-30 00:00:00    29.0
2020-11-30 12:00:00     NaN
2020-12-01 00:00:00    30.0
Freq: 12H, Length: 61, dtype: float64
"""

(2)对于缺失值可以用指定值或者指定方法来填充:

# 使用指定值填充
ts.asfreq(freq='12H', fill_value=0)

# 使用指定方法填充
ts.asfreq(pd.offsets.Hour(12), method='pad') # 重复上值

14.4 时间偏移

DateOffset类似于时间TimeDelta,但它使用日历中时间日期的规则,而不是直接进行时间性质的算术计算。
比如工作日就是常见的应用,周四办事,承诺三个工作日内完结,不是最迟周日,而是跳过周六周日,最迟周二办完。

14.4.1 DateOffset对象

(1)通过夏令时理解DateOffset对象

有些地区使用夏令时,每日偏移时间有可能是23或24小时,甚至25个小时。

# 生成一个指定的时间,芬兰赫尔辛基时间执行夏令时
t = pd.Timestamp('2016-10-30 00:00:00', tz='Europe/Helsinki')
# Timestamp('2016-10-30 00:00:00+0300', tz='Europe/Helsinki')

t + pd.Timedelta(days=1) # 增加一个自然天
# Timestamp('2016-10-30 23:00:00+0200', tz='Europe/Helsinki')
t + pd.DateOffset(days=1) # 增加一个时间偏移天
# Timestamp('2016-10-31 00:00:00+0200', tz='Europe/Helsinki')

可以发现,与时长Timedelta不同,时间偏移DateOffset不是数学意义上的增加或减少,而是根据实际生活的日历对现有时间进行偏移。时长可以独立存在,作为业务的一个数据指标,而时间偏移DateOffset的意义是找到一个时间起点,对它进行时间移动

(2)增加两个工作日

# 定义一个日期
d = pd.Timestamp('2020-10-30')
d # Timestamp('2020-10-30 00:00:00')
d.day_name() # 'Friday'

# 定义2个工作日时间偏移变量
two_business_days = 2 * pd.offsets.BDay()

# 增加两个工作日
two_business_days.apply(d)
d + two_business_days # 同上
# Timestamp('2020-11-03 00:00:00')

# 取增加两个工作日后的星期
(d + two_business_days).day_name() # 'Tuesday'

(3)所有的日期偏移对象都在pandas.tseries.offsets下,其中pandas.tseries.offsets.DateOffset是标准的日期范围时间偏移类型,默认是一个日历日。

from pandas.tseries.offsets import DateOffset

ts = pd.Timestamp('2020-01-01 09:10:11')
ts + DateOffset(months=3)
# Timestamp('2020-04-01 09:10:11')
ts + DateOffset(hours=2)
# Timestamp('2020-01-01 11:10:11')
ts + DateOffset() # 默认为1天
# Timestamp('2020-01-02 09:10:11')

14.4.2 偏移别名

DateOffset基本都支持频率字符串或偏移别名,传入freq参数,时间偏移的子类、子对象都支持时间偏移的相关操作。有效的日期偏移及频率字符串见表(略)。

可以将日期偏移别名组合,如3W(三周)、1h30min。

14.4.3 移动偏移

(1)Offset通过计算支持向前或向后偏移:

ts = pd.Timestamp('2020-06-06 00:00:00')
ts.day_name() # 'Saturday'

# 定义一个工作小时偏移,默认是周一到周五9~17点,我们从10点开始
offset = pd.offsets.BusinessHour(start='10:00')

# 向前偏移一个工作小时,是一个周一,跳过了周日
offset.rollforward(ts) # Timestamp('2020-06-08 10:00:00')

# 向前偏移至最近的工作日,小时也会增加
ts + offset # Timestamp('2020-06-08 11:00:00')

# 向后偏移,会在周五下班前的一个小时
offset.rollback(ts) # Timestamp('2020-06-05 17:00:00')

ts - pd.offsets.Day(1) # 昨日 Timestamp('2020-06-05 00:00:00')
ts - pd.offsets.Week(weekday=0) # 上个周一 Timestamp('2020-06-01 00:00:00')
ts - pd.offsets.MonthEnd() - pd.offsets.MonthBegin() # Timestamp('2020-05-01 00:00:00')
# 先回到上个月末,再从上个月末回到上个月初

(2)时间偏移操作会保留小时和分钟,有时候我们不在意具体时间,可以使用normalize进行标准化到午夜0点:

offset.rollback(ts).normalize()
# Timestamp('2020-06-05 00:00:00')

14.4.4 应用偏移

apply可以使偏移对象应用到一个时间上:

ts = pd.Timestamp('2020-06-01 09:00')
day = pd.offsets.Day() # 定义偏移对象
day.apply(ts) # Timestamp('2020-06-02 09:00:00')

14.4.5 偏移参数

(1)之前我们只偏移了偏移对象的一个单位,可以传入参数来偏移多个单位对象中的其他单位

import datetime
d = datetime.datetime(2020,6, 1,9, 0)

d + pd.offsets.Week() # 偏移一周  
#Timestamp('2020-06-08 09:00:00')

d + pd.offsets.Week(weekday=4) # 偏移4周中的日期 
# Timestamp('2020-06-05 09:00:00')

(2)参数也支持标准化

d + pd.offsets.Week(normalize=True)
# Timestamp('2020-06-08 00:00:00')

(3)YearEnd支持用参数month指定月份:

d + pd.offsets.YearEnd()
# Timestamp('2020-12-31 09:00:00')

d + pd.offsets.YearEnd(month=6)
# Timestamp('2020-06-30 09:00:00')

14.4.6 相关查询

(1)当使用日期作为索引的DataFrame时,此函数可以基于日期偏移量使用last选择最后几行,使用first选择前几行:

i = pd.date_range('2018-04-09', periods=4, freq='2D')
ts = pd.DataFrame({'A': [1, 2, 3, 4]}, index=i)
"""
			A
2018-04-09 	1
2018-04-11 	2
2018-04-13 	3
2018-04-15 	4
"""

# 取最后三天,返回最近3天的数据
# 而不是数据集中最近3天的数据,因此未返回2018-04-11的数据
ts.last('3D')
"""
			A
2018-04-13 	3
2018-04-15 	4
"""

# 前3天
ts.first('3D')

(2)可以用at_time() 来指定时间:

# 指定时间
ts.at_time('12:00')

(3)用between_time() 来指定时间区间:

ts.between_time('0:15', '0:45')

14.4.7 与时序的计算

可以对Series或DatetimeIndex时间索引序列应用时间偏移,与其他时间序列数据一样,时间偏移后的数据一般作为索引
(1)序列与时间偏移操作:

rng = pd.date_range('2020-01-01', '2020-01-03')
s = pd.Series(rng)
rng
# DatetimeIndex(['2020-01-01', '2020-01-02', '2020-01-03'], dtype='datetime64[ns]', freq='D')
s
"""
0   2020-01-01
1   2020-01-02
2   2020-01-03
dtype: datetime64[ns]
"""

rng + pd.DateOffset(months=2)
# DatetimeIndex(['2020-03-01', '2020-03-02', '2020-03-03'], dtype='datetime64[ns]', freq=None)

s + pd.DateOffset(months=2)
"""
0   2020-03-01
1   2020-03-02
2   2020-03-03
dtype: datetime64[ns]
"""

(2)序列与时长的操作:

s - pd.offsets.Day(2)
"""
0   2019-12-30
1   2019-12-31
2   2020-01-01
dtype: datetime64[ns]
"""
td = s - pd.Series(pd.date_range('2019-12-29', '2019-12-31'))
td
"""
0   3 days
1   3 days
2   3 days
dtype: timedelta64[ns]
"""

td + pd.offsets.Minute(15)
"""
0   3 days 00:15:00
1   3 days 00:15:00
2   3 days 00:15:00
dtype: timedelta64[ns]
"""

总结:
时长只能和pd.offsets.xxx运算; eg:td
时间序列数据只能和pd.DateOffset(xxx)运算。 eg:s

14.4.8 锚定偏移

锚点是网页制作中超级链接的一种,又叫命名锚记,像一个迅速定位器。

对于某些频率,可以指定锚定后缀,让它支持在一定的时间开始或结束,比如可以将周频率从默认的周日调到周一’W-MON’,具体见表14-2(略)。

(1)对于固定在特定频率开始或结束(MonthEnd、MonthBegin、WeekEnd等)的偏移,向前和向后移动的规则是:当n不为0时,如果给定日期不在锚点上,则它会捕捉到下一个(上一个)锚点,并向前或向后移动|n|-1步。

pd.Timestamp('2020-01-02') + pd.offsets.MonthBegin(n=1)
# Timestamp('2020-02-01 00:00:00')

pd.Timestamp('2020-01-02') + pd.offsets.MonthEnd(n=1)
# Timestamp('2020-01-31 00:00:00')

pd.Timestamp('2020-01-02') - pd.offsets.MonthBegin(n=1)
# Timestamp('2020-01-01 00:00:00')

pd.Timestamp('2020-01-02') - pd.offsets.MonthEnd(n=1)
# Timestamp('2019-12-31 00:00:00')

eg:pd.Timestamp(‘2020-01-02’) + pd.offsets.MonthBegin(n=1)
n=1,则偏移|n|-1=0步,直接变成下一个月的月初。

(2)如果给定的日期在锚点上,则向前或向后移动|n|步:

给定日期在锚点上,指的应该是:如果日期恰好在月初,偏移至月初(即MonthBegin)。

pd.Timestamp('2020-01-01') + pd.offsets.MonthBegin(n=1)
# Timestamp('2020-02-01 00:00:00')

pd.Timestamp('2020-01-31') + pd.offsets.MonthEnd(n=1)
# Timestamp('2020-02-29 00:00:00')

(3)对于n=0的情况,如果在锚点上,则日期不会移动,否则会移动到下一个锚点:

pd.Timestamp('2020-01-01') + pd.offsets.MonthBegin(n=0)
# Timestamp('2020-01-01 00:00:00')

pd.Timestamp('2020-01-02') + pd.offsets.MonthBegin(n=0)
# Timestamp('2020-02-01 00:00:00')

14.4.9 自定义工作时间

(1)可以想CdayCustomBusinessDay类传入节假日参数自定义一个工作日偏移对象:

weekmask_egypt = 'Sun Mon Tue Wed Thu'

# 定义出五一劳动节的日期(05-01)
holidays = ['2018-05-01', datetime.datetime(2019, 5, 1),
            np.datetime64('2020-05-01')]

# 自定义工作日中传入休假日期,一个正常星期工作日的顺序
# holidays传入假期,weekmask传入工作日
bday_egypt = pd.offsets.CustomBusinessDay(holidays=holidays,
 weekmask=weekmask_egypt) 

# 指定一个日期
dt = datetime.datetime(2020, 4, 30)
# 偏移两个工作日,跳过了休假日
dt + 2 * bday_egypt
# Timestamp('2020-05-04 00:00:00')

我们输出星期对照观察,发现跳过了2020年5月1日(定义的休假日)和2020年5月2日(定义的工作周中为休息日):

# 输出时序及星期几
idx = pd.date_range(dt, periods=5, freq=bday_egypt)
pd.Series(idx.weekday+1, index=idx)
"""
2020-04-30    4
2020-05-03    7
2020-05-04    1
2020-05-05    2
2020-05-06    3
Freq: C, dtype: int64
"""

(2)BusinessHour表是开始和结束工作的小时时间,默认的工作时间是09:00-17:00,与时间相加超过一个小时会移到下一个小时,超过一天会移动到下一个工作日。

# 定义一个工作时间
bh = pd.offsets.BusinessHour()
bh # <BusinessHour: BH=09:00-17:00>

# 2020-08-01是周六
pd.Timestamp('2020-08-01 10:00').weekday() # 5

①在周末增加一个工作小时:

# 增加一个工作小时 (周末ver.)
pd.Timestamp('2020-08-01 10:00') + bh
# Timestamp('2020-08-03 10:00:00')
# 因为1号和2号都是周末,被跳过
# 相当于作了一个增加操作,因此在时间上没有变动

②在正常工作日(已经到了上班点 增加一个工作小时:

# 增加一个工作小时 (工作日ver.)
pd.Timestamp('2020-08-03 10:00') + bh
# Timestamp('2020-08-03 11:00:00')

③在正常工作日(未到上班点 增加一个工作小时:

# 一旦计算就开始上班
# 等同于pd.Timestamp('2020-08-03 09:00') + bh
pd.Timestamp('2020-08-03 08:00') + bh
# Timestamp('2020-08-03 10:00:00')

④在周五(计算后已经下班) 增加一个工作小时:

# 计算后已经下班,就移到下一个工作小时(跳过周末)
# 2020-07-31 周五 
pd.Timestamp('2020-07-31 16:00') + bh
# Timestamp('2020-08-03 09:00:00')

(3)对于BusinessHour表自定义开始和结束工作的时间,格式必须是hour:minute字符串,不支持秒、微秒、纳秒。

# 11点开始上班
bh = pd.offsets.BusinessHour(start='11:00', end=datetime.time(20))
bh # <BusinessHour: BH=11:00-20:00>

(4)对于BusinessHour表,如果start时间晚于end时间表示夜班工作时间,此时,工作时间将从午夜延至第二天。

bh = pd.offsets.BusinessHour(start='17:00', end='09:00')
bh # <BusinessHour: BH=17:00-09:00>

pd.Timestamp('2014-08-01 17:00') + bh
# Timestamp('2014-08-01 18:00:00')

# 2014-08-02 周六
# 但是由于工作时间从周五17:00开始,因此也有效
pd.Timestamp('2014-08-02 04:00') + bh
# Timestamp('2014-08-02 05:00:00')

14.4.10 小结

时间偏移与时长的不同是它是真实日历上的时间移动,在数据分析中时间偏移的意义是大于时长的。另外,通过继承pandas.tseries.holiday.AbstractHolidayCalendar创建子类,可以自定义假期日历,完成更为复杂的时间偏移操作。

注意区分:pd.DateOffsetpd.offsets.xxx

14.5 时间段

pandas中的Period()对象表示一个时间段,比如一年、一个月。与时间长度不同,它表示一个具体的时间区间,有时间起点和周期频率。

14.5.1 Period对象

(1)利用pd.Period() 创建时间段对象:

# 创建一个时间段(年)
pd.Period('2020')
# Period('2020', 'A-DEC')

# 创建一个时间段(季度)
pd.Period('2020Q4')
# Period('2020Q4', 'Q-DEC')

以第一个为例,返回对象有两个值:第一个是这个时间段的起始时间,第二个字符串"A-DEC"中的A指年度(Annual),DEC指12月(December)。这个时间段对象代表一个在2020年结束语12月的全年时间段。

(2)传入更多参数

# 2020-01-01全天的时间段
pd.Period(year=2020, freq='D')
# Period('2020-01-01', 'D')

# 一周
pd.Period('20201101', freq='W')
# Period('2020-10-26/2020-11-01', 'W-SUN')

# 默认周期,对应到最细粒度--分钟
pd.Period('2020-11-11 23:00')
# Period('2020-11-11 23:00', 'T')

# 指定周期
pd.Period('2020-11-11 23:00', 'D')
# Period('2020-11-11', 'D')

14.5.2 属性方法

# 定义时间段
p = pd.Period('2020Q4')

(1)获取开始和结束时间:

# 开始与结束时间
p.start_time
# Timestamp('2020-10-01 00:00:00')

p.end_time
# Timestamp('2020-12-31 23:59:59.999999999')

(2)如果当前时间段不符合业务实际,可以转换频率:

p.asfreq('D') # 转换频率为天
# Period('2020-12-31', 'D')

p.asfreq('D', how='start')
# Period('2020-10-01', 'D')

(3)其他属性如下:

p.freq # <QuarterEnd: startingMonth=12>(时间偏移对象)
p.freqstr # 'Q-DEC' (时间偏移别名)
p.is_leap_year # True 是否闰年
p.to_timestamp() # Timestamp('2020-10-01 00:00:00')

# 以下日期取时间段内最后一天 (即针对2020.12.31)
p.day # 31
p.dayofweek # 3 周四
p.dayofyear # 366 一年中的第几天
p.hour # 0
p.week
p.minute
p.qyear # 2020 财年(财经年度)
p.year
p.days_in_month # 31 当月第几天
p.daysinmonth # 31 当月共多少天
p.strftime('%Y年%m月') # 格式化时间

14.5.3 时间段的计算

(1)时间段可以做加减法,表示将此时间段前移或后移相应单位:

# 在2020Q4上增加一个周期
pd.Period('2020Q4') + 1
# Period('2021Q1', 'Q-DEC')

(2)时间段对象也可以和时间偏移对象做加减:

# 增加一小时
pd.Period('20200101 15') + pd.offsets.Hour(1)
# Period('2020-01-01 16:00', 'H')

tips:如果偏移量频率时间段不同,则其单位要大于时间段频率,否则会报错:

pd.Period('20200101 14') + pd.offsets.Minute(10)
# 偏移量频率分钟,小于时间段频率,因此报错
# IncompatibleFrequency: Input cannot be converted to Period(freq=H)

(3)时间段时间差相加减:

pd.Period('20200101 14') + pd.Timedelta('1 days')
# Period('2020-01-02 14:00', 'H')

(4)相同频率的时间段实例之差将返回它们之间的频率单位数

pd.Period('20200101 14') - pd.Period('20200101 10')
# <4 * Hours>

pd.Period('2020Q4') - pd.Period('2020Q1')
# <3 * QuarterEnds: startingMonth=12>

14.5.4 时间段索引

(1)类似于时间范围pd.date_range()生成时序索引数据,pd.period_range() 可以生成时间段索引数据:

# 生成时间段索引对象
pd.period_range('2020-11-01 10:00', periods=10, freq='H')

"""
PeriodIndex(['2020-11-01 10:00', '2020-11-01 11:00', '2020-11-01 12:00',
             '2020-11-01 13:00', '2020-11-01 14:00', '2020-11-01 15:00',
             '2020-11-01 16:00', '2020-11-01 17:00', '2020-11-01 18:00',
             '2020-11-01 19:00'],
            dtype='period[H]', freq='H')
"""

上例生成了时间段索引对象,它从2020年11月1日10点开始,频率为小时,共有10个周期,数据类型period[H]可以看到频率。时间段索引对象可以用于时序索引,也可以用于Series和DataFrame中的数据。

(2)指定开始和结束时间

pd.period_range('2020Q1', '2021Q4', freq='Q-NOV')
"""
PeriodIndex(['2020Q1', '2020Q2', '2020Q3', '2020Q4', '2021Q1', '2021Q2',
             '2021Q3', '2021Q4'],
            dtype='period[Q-NOV]', freq='Q-NOV')
"""

上例定义了一个从2020年第一季度到2021第四季度共8个季度的时间段,一年以11月为最后时间。

(3)通过时间段对象来定义:

pd.period_range(start=pd.Period('2020Q1', freq='Q'),
                end=pd.Period('2021Q2', freq='Q'), freq='M')
"""
PeriodIndex(['2020-03', '2020-04', '2020-05', '2020-06', '2020-07', '2020-08',
             '2020-09', '2020-10', '2020-11', '2020-12', '2021-01', '2021-02',
             '2021-03', '2021-04', '2021-05', '2021-06'],
            dtype='period[M]', freq='M')
"""

(4)时间段索引可以应用于数据中:

pd.Series(pd.period_range('2020Q1', '2021Q4', freq='Q-NOV'))
"""
0    2020Q1
1    2020Q2
2    2020Q3
3    2020Q4
4    2021Q1
5    2021Q2
6    2021Q3
7    2021Q4
dtype: period[Q-NOV]
"""
pd.Series(range(8), index=pd.period_range('2020Q1', '2021Q4', freq='Q-NOV'))
"""
2020Q1    0
2020Q2    1
2020Q3    2
2020Q4    3
2021Q1    4
2021Q2    5
2021Q3    6
2021Q4    7
Freq: Q-NOV, dtype: int64
"""

14.5.5 数据查询

数据查询方法和时序查询一致,支持切片操作

# 建立索引数据
s = pd.Series(1, index=pd.period_range('2020-10-01 10:00', '2021-10-01 10:00', freq='H'))
"""
2020-10-01 10:00    1
2020-10-01 11:00    1
2020-10-01 12:00    1
2020-10-01 13:00    1
2020-10-01 14:00    1
                   ..
2021-10-01 06:00    1
2021-10-01 07:00    1
2021-10-01 08:00    1
2021-10-01 09:00    1
2021-10-01 10:00    1
Freq: H, Length: 8761, dtype: int64
"""
s['2020']
"""
2020-10-01 10:00    1
2020-10-01 11:00    1
2020-10-01 12:00    1
2020-10-01 13:00    1
2020-10-01 14:00    1
                   ..
2020-12-31 19:00    1
2020-12-31 20:00    1
2020-12-31 21:00    1
2020-12-31 22:00    1
2020-12-31 23:00    1
Freq: H, Length: 2198, dtype: int64
"""
s['2020-10':'2020-11']
"""
2020-10-01 10:00    1
2020-10-01 11:00    1
2020-10-01 12:00    1
2020-10-01 13:00    1
2020-10-01 14:00    1
                   ..
2020-11-30 19:00    1
2020-11-30 20:00    1
2020-11-30 21:00    1
2020-11-30 22:00    1
2020-11-30 23:00    1
Freq: H, Length: 1454, dtype: int64
"""

14.5.6 相关类型转换

astype() 可以在几种数据之间自由转换。
(1)DatetimeIndexPeriodIndex

ts = pd.date_range('20201101', periods=100)
"""
DatetimeIndex(['2020-11-01', '2020-11-02', '2020-11-03', '2020-11-04',
               ...
               '2021-02-05', '2021-02-06', '2021-02-07', '2021-02-08'],
              dtype='datetime64[ns]', freq='D')
"""

# 转为PeriodIndex,频率为月
ts.astype('period[M]')
"""
PeriodIndex(['2020-11', '2020-11', '2020-11', '2020-11', '2020-11', '2020-11',
             ...
             '2021-02', '2021-02', '2021-02', '2021-02'],
            dtype='period[M]', freq='M')
"""

(2)PeriodIndexDatetimeIndex

ts = pd.period_range('2020-11', periods=100, freq='M')
"""
PeriodIndex(['2020-11', '2020-12', '2021-01', '2021-02', '2021-03', '2021-04',
             ...
             '2028-11', '2028-12', '2029-01', '2029-02'],
            dtype='period[M]', freq='M')
"""

# 转为DatetimeIndex
ts.astype('datetime64[ns]')
"""
DatetimeIndex(['2020-11-01', '2020-12-01', '2021-01-01', '2021-02-01',
               ...
               '2028-11-01', '2028-12-01', '2029-01-01', '2029-02-01'],
              dtype='datetime64[ns]', freq='MS')
"""

(3)PeriodIndex转换频率:

# 频率从月转为季度
ts.astype('period[Q]')
"""
PeriodIndex(['2020Q4', '2020Q4', '2021Q1', '2021Q1', '2021Q1', '2021Q2',
             ...
             '2028Q4', '2028Q4', '2029Q1', '2029Q1'],
            dtype='period[Q-DEC]', freq='Q-DEC')
"""

14.6 时间操作

本节介绍一些通用时间操作和高级功能。

14.6.1 时区转换

Pandas使用pytzdateutil库或标准库中的datetime.timezone对象为使用不同时区的时间戳提供了丰富的支持。

(1)查看所有时区及时区的字符名称:

import pytz

print(pytz.common_timezones) # 显示所有时区
print(pytz.timezone) # <function timezone at 0x000001F618F99E18>

(2)如果没有指定,时间一般不带时区

ts = pd.date_range('11/11/2020 00:00', periods=10, freq='D')
ts.tz is None # True

(3)简单的时区指定,中国通用的北京时区使用’Asia/Shanghai’定义:

pd.date_range('2020-01-01', periods=10, freq='D', tz='Asia/Shanghai')
"""
DatetimeIndex(['2020-01-01 00:00:00+08:00', '2020-01-02 00:00:00+08:00',
               '2020-01-03 00:00:00+08:00', '2020-01-04 00:00:00+08:00',
               '2020-01-05 00:00:00+08:00', '2020-01-06 00:00:00+08:00',
               '2020-01-07 00:00:00+08:00', '2020-01-08 00:00:00+08:00',
               '2020-01-09 00:00:00+08:00', '2020-01-10 00:00:00+08:00'],
              dtype='datetime64[ns, Asia/Shanghai]', freq='D')
"""
pd.Timestamp('2020-01-01', tz='Asia/Shanghai')
# Timestamp('2020-01-01 00:00:00+0800', tz='Asia/Shanghai')

(4)指定时区的更多方法:

# 使用pytz
rng_pytz = pd.date_range('11/11/2020 00:00', periods=3,
                         freq='D', tz='Europe/London')
rng_pytz.tz
# <DstTzInfo 'Europe/London' LMT-1 day, 23:59:00 STD>

还可以使用dateutil支持,使用dateutil指定为UTC时间,在这里不再赘述。

(5)从一个时区转为另外一个时区,使用tz_convert

rng_pytz.tz_convert('US/Eastern')
"""
DatetimeIndex(['2020-11-10 19:00:00-05:00', '2020-11-11 19:00:00-05:00',
               '2020-11-12 19:00:00-05:00'],
              dtype='datetime64[ns, US/Eastern]', freq='D')
"""

14.6.2 时间格式化

在数据格式解析、输出格式和格式转换过程中,需要用标识符来匹配日期元素的位置,Pandas使用了Python的格式化符号系统。

import locale # 保证格式化能够正常执行
locale.setlocale(locale.LC_CTYPE, 'chinese') 

# 解析时间格式
pd.to_datetime('2020*11*12', format='%Y*%m*%d')
# Timestamp('2020-11-12 00:00:00')

# 输出的时间格式
pd.Timestamp('now').strftime('%Y年%m月%d日')
# Timestamp('2022-10-26 17:35:41.008394')
# '2022年10月26日'

Python中日期和时间的格式化符号见表14-3(略)。

14.6.3 时间重采样

Pandas可以对时序数据按不同的频率进行重采样操作。

(1)原时序数据频率为分钟,使用resample() 可以按5分钟、15分钟、半小时等频率分组,然后完成聚合计算。

idx = pd.date_range('2020-01-01', periods=500, freq='Min')
ts = pd.Series(range(len(idx)), index=idx)
"""
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 08:15:00    495
2020-01-01 08:16:00    496
2020-01-01 08:17:00    497
2020-01-01 08:18:00    498
2020-01-01 08:19:00    499
Freq: T, Length: 500, dtype: int64
"""
# 每5分钟进行一次聚合
ts.resample('5Min').sum()
"""
2020-01-01 00:00:00      10
2020-01-01 00:05:00      35
2020-01-01 00:10:00      60
2020-01-01 00:15:00      85
2020-01-01 00:20:00     110
                       ... 
2020-01-01 07:55:00    2385
2020-01-01 08:00:00    2410
2020-01-01 08:05:00    2435
2020-01-01 08:10:00    2460
2020-01-01 08:15:00    2485
Freq: 5T, Length: 100, dtype: int64
"""

(2)可以指定许多不同参数来控制频率转换重采样操作。通过类似于groupby聚合后的各种统计函数来实现数据的分组聚合,包括sum、mean、std、sem、max、min、median、first、last和ohlc。

ts.resample('5Min').mean() # 平均
"""
2020-01-01 00:00:00      2
2020-01-01 00:05:00      7
2020-01-01 00:10:00     12
2020-01-01 00:15:00     17
2020-01-01 00:20:00     22
                      ... 
2020-01-01 07:55:00    477
2020-01-01 08:00:00    482
2020-01-01 08:05:00    487
2020-01-01 08:10:00    492
2020-01-01 08:15:00    497
Freq: 5T, Length: 100, dtype: int64
"""

(3)ohlc,又叫美国线(Open-High-Low-Close chart, OHLC chart),可以呈现类似股票的开盘价、最高价、最低价和收盘价。

# 两小时频率的美国线
ts.resample('2h').ohlc()
"""
 						open 	high 	low 	close
2020-01-01 00:00:00 	0 		119 	0 		119
2020-01-01 02:00:00 	120 	239 	120 	239
2020-01-01 04:00:00 	240 	359 	240 	359
2020-01-01 06:00:00 	360 	479 	360 	479
2020-01-01 08:00:00 	480 	499 	480 	499
"""

(4)closed参数可以设为“left”或“right”,以指定开闭区间的哪一端:

ts.resample('2h', closed='left').mean()
"""
2020-01-01 00:00:00     59.5
2020-01-01 02:00:00    179.5
2020-01-01 04:00:00    299.5
2020-01-01 06:00:00    419.5
2020-01-01 08:00:00    489.5
Freq: 2H, dtype: float64
"""

(5)label参数可以控制输出结果显示左还是右,但不像closed那样影响计算结果。

ts.resample('5Min').mean() # 默认label='left'
"""
2020-01-01 00:00:00      2
2020-01-01 00:05:00      7
2020-01-01 00:10:00     12
2020-01-01 00:15:00     17
2020-01-01 00:20:00     22
                      ... 
2020-01-01 07:55:00    477
2020-01-01 08:00:00    482
2020-01-01 08:05:00    487
2020-01-01 08:10:00    492
2020-01-01 08:15:00    497
Freq: 5T, Length: 100, dtype: int64
"""

ts.resample('5Min', label='right').mean()
"""
2020-01-01 00:05:00      2
2020-01-01 00:10:00      7
2020-01-01 00:15:00     12
2020-01-01 00:20:00     17
2020-01-01 00:25:00     22
                      ... 
2020-01-01 08:00:00    477
2020-01-01 08:05:00    482
2020-01-01 08:10:00    487
2020-01-01 08:15:00    492
2020-01-01 08:20:00    497
Freq: 5T, Length: 100, dtype: int64
"""

14.6.4 上采样

上采样一般应用在图形图像学中,目的是放大图像。由于原数据有限,放大图像后需要对缺失值进行内插值填充。在时序数据同样存在着类似的问题。

上例中的数据频率是分钟,对其按30s重采样:

ts.head(3).resample('30S').asfreq()
"""
2020-01-01 00:00:00    0.0
2020-01-01 00:00:30    NaN
2020-01-01 00:01:00    1.0
2020-01-01 00:01:30    NaN
2020-01-01 00:02:00    2.0
Freq: 30S, dtype: float64
"""

此时,发现由于原数据粒度不够,出现缺失值,需要用**.ffill().bfill()**来计算填充值:

# 补充的值和前值一样
ts.head(3).resample('30S').ffill()
"""
2020-01-01 00:00:00    0
2020-01-01 00:00:30    0
2020-01-01 00:01:00    1
2020-01-01 00:01:30    1
2020-01-01 00:02:00    2
Freq: 30S, dtype: int64
"""
# 补充的值和后值一样
ts.head(3).resample('30S').bfill()

14.6.5 重采样聚合

(1)重采样适用于相关的统计聚合方法

df = pd.DataFrame(np.random.randn(1000, 3),
                  index=pd.date_range('1/1/2020', freq='S', periods=1000),
                  columns=['A', 'B', 'C'])
# 生成Resampler重采样对象
r = df.resample('3T')
r.mean()
"""
							A 			B 			C
2020-01-01 00:00:00 	-0.039264 	0.081512 	0.063693
2020-01-01 00:03:00 	-0.023642 	0.027389 	-0.127066
2020-01-01 00:06:00 	0.022440 	-0.100795 	0.181529
2020-01-01 00:09:00 	0.115326 	0.046553 	0.043965
2020-01-01 00:12:00 	-0.067841 	0.089492 	0.065904
2020-01-01 00:15:00 	-0.088980 	-0.061358 	-0.039684
"""

(2)多个聚合方式

r['A'].agg([np.sum, np.mean, np.std])
r.agg([np.sum, np.mean]) # 每个列

# 不同的聚合方式
r.agg({'A': np.sum,
       'B': lambda x: np.std(x, ddof=1)})

# 用字符指定
r.agg({'A': 'sum', 'B': 'std'})

r.agg({'A': ['sum', 'std'], 'B': ['mean', 'std']})

(3)如果索引不是时间,可以指定采样的时间列

# date是一个普通列
df.resample('M', on='date').sum()
df.resample('M', level='d').sum() # 多层索引

(4)迭代采样对象

# r是重采样对象
for name, group in r:
    print("Group: ", name)
    print("-" * 20)
    print(group, end='\n\n')

14.6.6 时间类型间转换

(1)to_period()DatetimeIndex转化为PeriodIndex

pd.date_range('1/1/2020', periods=5)
"""
DatetimeIndex(['2020-01-01', '2020-01-02', '2020-01-03', '2020-01-04',
               '2020-01-05'],
              dtype='datetime64[ns]', freq='D')
"""

# 转换为时间周期
pd.date_range('1/1/2020', periods=5).to_period()
"""
PeriodIndex(['2020-01-01', '2020-01-02', '2020-01-03', '2020-01-04',
             '2020-01-05'],
            dtype='period[D]', freq='D')
"""

(2)to_timestamp()将默认周期的开始时间转换为DatetimeIndex

pd.period_range('1/1/2020', periods=5)
"""
PeriodIndex(['2020-01-01', '2020-01-02', '2020-01-03', '2020-01-04',
             '2020-01-05'],
            dtype='period[D]', freq='D')
"""

# 转换为时序索引
pd.period_range('1/1/2020', periods=5).to_timestamp()
"""
DatetimeIndex(['2020-01-01', '2020-01-02', '2020-01-03', '2020-01-04',
               '2020-01-05'],
              dtype='datetime64[ns]', freq='D')
"""

14.6.7 超出时间戳范围时间

Pandas原生支持的时间范围大约在1677-2262年,如果分析时间不在这个区间,应该怎么办呢?

# 定义一个超限时间周期
pd.period_range('1111-01-01', '8888-01-01', freq='D')
"""
PeriodIndex(['1111-01-01', '1111-01-02', '1111-01-03', '1111-01-04',
             '1111-01-05', '1111-01-06', '1111-01-07', '1111-01-08',
             '1111-01-09', '1111-01-10',
             ...
             '8887-12-23', '8887-12-24', '8887-12-25', '8887-12-26',
             '8887-12-27', '8887-12-28', '8887-12-29', '8887-12-30',
             '8887-12-31', '8888-01-01'],
            dtype='period[D]', length=2840493, freq='D')
"""

可以正常计算和使用。还可以将时间以数字形式保存,在计算的时候再转换为周期数据:

(pd.Series([123_1111, 2008_10_01, 8888_12_12])
# 将整型转为时间周期类型
.apply(lambda x: pd.Period(year=x // 10000,
                           month=x // 100 % 100,
                           day=x % 100,
                           freq='D')
      )
)
"""
0    0123-11-11
1    2008-10-01
2    8888-12-12
dtype: period[D]
"""

14.6.8 区间间隔

Pandas.Interval可以解决数字区间和时间区间的相关问题,它实现一个名为Interval的不可变对象,该对象是一个有界的切片状间隔

(1)Interval对象构建:

pd.Interval(left=0, right=5, closed='right')
# Interval(0, 5, closed='right')

# 4是否在1~10之间
4 in pd.Interval(1, 10) # True

(2)参数定义如下:

  • left:定值,间隔的左边界;
  • right:定值,间隔的右边界;
  • closed:字符,可选right、left、both、neither,分别代表区间在右侧,左侧,同时闭合,都不闭合。默认为right。

(3)Interval可以对数字、固定时间、时长起作用
①构建数字类型间隔的方法:

iv = pd.Interval(left=0, right=5) # Interval(0, 5, closed='right')

# 可以检查元素是否属于
3.5 in iv # True

②创建时间区间间隔

# 定义2020年的区间
year_2020 = pd.Interval(pd.Timestamp('2020-01-01 00:00:00'),
                        pd.Timestamp('2021-01-01 00:00:00'),
                        closed='left')

# 检查指定时间是否在此区间内
pd.Timestamp('2020-01-01 00:00') in year_2020 # True

# 2020年时间区间的长度
year_2020.length
# Timedelta('366 days 00:00:00')

③创建时长区间间隔

# 定义一个时长区间,3秒到1天
time_deltas = pd.Interval(pd.Timedelta('3 seconds'),
                          pd.Timedelta('1 days'),
                          closed='both')

# 时长区间长度
time_deltas.length 
# Timedelta('0 days 23:59:57') 

(4)pd.Interval支持以下属性

# 区间闭合之处
iv.closed # 'right'

# 检查间隔是否在左侧关闭
iv.closed_left # False

# 间隔是否为空,表示该间隔不包含任何点
iv.is_empty # False

# 间隔的左边界
iv.left # 0
# 间隔的中点
iv.mid 
# 间隔的长度
iv.length

# 间隔是否在左侧为开区间
iv.open_left # True 

(5)其中,Interval.is_empty指示间隔是否为空,表示该间隔不包含任何点。

pd.Interval(0, 1, closed='right').is_empty # False

# 不包含任何点的间隔为空
pd.Interval(0, 0, closed='right').is_empty # True
pd.Interval(0, 0, closed='left').is_empty # True
pd.Interval(0, 0, closed='neither').is_empty # True

# 包含单个点的间隔不为空
pd.Interval(0, 0, closed='both').is_empty # False

# 一个IntervalArray或IntervalIndex返回一个布尔ndarray
# 它在位置上指示Interval是否为空
ivs = [pd.Interval(0, 0, closed='neither'),
       pd.Interval(1, 2, closed='neither')]
pd.arrays.IntervalArray(ivs).is_empty 
# array([ True, False])

# 缺失值不为空
ivs = [pd.Interval(0, 0, closed='neither'), np.nan]
pd.IntervalIndex(ivs).is_empty
# array([ True, False])

(6)pd.Interval.overlaps检查两个Interval对象是否重叠。如果两个间隔至少共享一个公共点(包括封闭的端点),则它们重叠。
①重叠:

i1 = pd.Interval(0, 2)
i2 = pd.Interval(1, 3)
i1.overlaps(i2) # True

②共享封闭端点的间隔重叠

i4 = pd.Interval(0, 1, closed='both')
i5 = pd.Interval(1, 2, closed='both')
i4.overlaps(i5) # True

③只有共同的开放端点的间隔不重叠

i6 = pd.Interval(1, 2, closed='neither')
i4.overlaps(i6) # False

(7)间隔对象能使用+和*与一个固定值进行计算,此操作将同时应用于对象的两个边界,结果取决于绑定边界值数据的类型。

iv
# Interval(0, 5, closed='right')
shifted_iv = iv + 3
# Interval(3, 8, closed='right')
extended_iv = iv * 10.0
# Interval(0.0, 50.0, closed='right')

(8)Pandas不支持两个区间的合并、取交集等操作,可以使用Python的第三方库portion实现。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值