【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

时间戳Timestamp

单个Timestamp的构造与属性

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

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

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

ts.year
ts.month
ts.day
ts.hour
ts.minute
ts.second

Datetime序列的生成

一组时间戳可以组成时间序列,可以用to_datetimedate_range来生成。

pd.to_datetime

一列时间戳格式的列表或序列

pd.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)

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

temp = pd.to_datetime(['2020\\1\\1','2020\\1\\3'],format='%Y\\%m\\%d')
# 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]

多列时间属性的df

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]

pd.date_range

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)

选定periods时,startend一定会包含在里面,最后一个周期的长度可能比前面的周期短。

但选定freq时,会严格按照freq指定的长度(周期长度)生成时间序列,只限制最后一个序列在指定的end之前即可。end不一定包含在里面。

【练一练】

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

def rdate_range(start, end, size):
    start = pd.Timestamp(start)
    range_date = np.int((pd.Timestamp(end).value-start.value)/(10**9*60*60*24))
    randlist = 10**9*60*60*24*np.random.randint(0,range_date,size)
    out = pd.Series([pd.Timestamp(i+start.value) for i in randlist])
    return out
rdate_range('2020-05-18','2021-08-16',21)

改变序列采样频率的方法asfreq,它能够根据给定的freq序列进行类似于reindex的操作,此时是由时间作为index的Series。

s = pd.Series(np.random.rand(5), index=pd.to_datetime(['2020-1-%d'%i for i in range(1,10,2)]))
s.head()
#2020-01-01    0.393090
#2020-01-03    0.460112
#2020-01-05    0.823976
#2020-01-07    0.222040
#2020-01-09    0.169990
#dtype: float64

s.asfreq('D').head()
#2020-01-01    0.393090
#2020-01-02         NaN
#2020-01-03    0.460112
#2020-01-04         NaN
#2020-01-05    0.823976
#Freq: D, dtype: float64

【练一练】

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

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

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
s.dt.time
s.dt.day
s.dt.daysinmonth  #月中的第几天
s.dt.dayofweek    #周中的星期几

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

s.dt.month_name()  # 英文月名
s.dt.day_name()    # 英文星期名

测试是否为月/季/年的第一天或者最后一天

s.dt.is_year_start # 还可选 is_quarter/month_start
s.dt.is_year_end # 还可选 is_quarter/month_end

取整操作

包含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]

时间戳的切片与索引

一般而言,时间戳序列作为索引使用。如果想要选出某个子时间戳序列,第一类方法是利用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    0
#2020-01-02    1
#2020-01-03    1
#2020-01-04    0
#2020-01-05    0
#Freq: D, dtype: int32

## Example1:每月的第一天或者最后一天
s[(idx.is_month_start|idx.is_month_end).values].head()

## Example2:双休日
s[idx.dayofweek.isin([5,6]).values].head()

## Example3:取出单日值
s['2020-01-01']
s['20200101'] # 自动转换标准格式

## Example4:取出七月
s['2020-07'].head()

## Example5:取出5月初至7月15日
s['2020-05':'2020-7-15'].head()
s['2020-05':'2020-7-15'].tail()

时间差Timedelta

Timedelta的生成

Timestamp相减

pd.Timestamp('20200102 08:00:00')-pd.Timestamp('20200101 07:35:00')
#Timedelta('1 days 00:25:00')

pd.Timedelta

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]
type(df.Time_Record)
# pandas.core.series.Series
df.Time_Record
#0      0:04:34
#1      0:04:20
#2      0:05:22
#3      0:04:08
#4      0:05:22
#        ...   
#195    0:04:31
#196    0:04:03
#197    0:04:48
#198    0:04:58
#199    0:05:05
#Name: Time_Record, Length: 200, dtype: object

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]
  • timedelta_range, 与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()

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

pd.to_timedelta(df.Time_Record).dt.round('min').head()

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))) # 逐个相乘

日期偏置

Offset对象

有时候需要筛选一些特殊的日历时间,比如工作日、每个月的第n个周几这种周期性依赖于特殊月份的时间。可以借助offset对象来进行筛选和生成需要的时间。

  • pd.Timestamp +/- pd.offsets.BDay(n) n表示偏置的第n个工作日
  • pd.Timestamp +/- pd.offsets.WeekOfMonth(week, weekday) 第week+1周的第weekday+1天
  • pd.Timestamp +/- pd.offsets.CDay(n, weekmask, holidays) 自定义工作日

week的取值范围为0-3,weekday的取值范围为0-6

+表示获取往后取最近满足条件的一个日期,-表示往前取最近的满足条件的一个日期

pd.Timestamp('20210101') + pd.offsets.WeekOfMonth(week=0,weekday=5)
#Timestamp('2021-01-02 00:00:00')
pd.Timestamp('20210107') + pd.offsets.BDay(3)
#Timestamp('2021-01-12 00:00:00')

特别说明一下CDay

my_filter = pd.offsets.CDay(n=1,weekmask='Wed Fri',holidays=['20210109'])
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表示偏置n天,偏置的时间只有在符合条件下的才算
  • holidays传入了需要过滤的自定义日期列表,不符合条件的日期
  • weekmask能够对自定义的星期进行过滤,传入的是三个字母的星期缩写构成的星期字符串,其作用是只保留字符串中出现的星期,符合条件的星期几

偏置字符串

date_rangefreq取值可用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')

时序中的滑窗和分组

滑动窗口

Series.rolling()

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

import matplotlib.pyplot as plt
idx = 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)
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)

在这里插入图片描述

Series.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]的序列

重采样

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

s.resample('10D').mean().head()  #计算每10天的均值
s.resample('10D').apply(lambda x:x.max()-x.min()).head() # 极差

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

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
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
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

练习

Ex1:太阳辐射数据集

df = pd.read_csv('../data/solar.csv', usecols=['Data','Time','Radiation','Temperature'])
df.head(3)
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
  1. Datetime, Time合并为一个时间列Datetime,同时把它作为索引后排序。
  2. 每条记录时间的间隔显然并不一致,请解决如下问题:
  • 找出间隔时间的前三个最大值所对应的三组时间戳。
  • 是否存在一个大致的范围,使得绝大多数的间隔时间都落在这个区间中?如果存在,请对此范围内的样本间隔秒数画出柱状图,设置bins=50
  1. 求如下指标对应的Series
  • 温度与辐射量的6小时滑动相关系数
  • 以三点、九点、十五点、二十一点为分割,该观测所在时间区间的温度均值序列
  • 每个观测6小时前的辐射量(一般而言不会恰好取到,此时取最近时间戳对应的辐射量)
##1.
solar_date = df.Data.str.extract('([/|\w]+\s).+')[0]
df['Data'] = pd.to_datetime(solar_date + df.Time)
df = df.drop(columns='Time').rename(columns={
    
    'Data':'Datetime'}).set_index('Datetime').sort_index()
df.head(3)
##2.
s = df.index.to_series().reset_index(drop=True).diff().dt.total_seconds()
max_3 = s.nlargest(3).index
df.index[max_3.union(max_3-1)]

res = s.mask((s>s.quantile(0.99))|(s<s.quantile(0.01)))
_ = plt.hist(res, bins=50)
##3.
res = df.Radiation.rolling('6H').corr(df.Temperature)
res.tail(3)

res = df.Temperature.resample('6H', origin='03:00:00').mean()
res.head(3)

my_dt = df.index.shift(freq='-6H')
int_loc = [df.index.get_loc(i, method='nearest') for i in my_dt]
res = df.Radiation.iloc[int_loc]
res.tail(3)

(正则还是要补一补/(ㄒoㄒ)/~~)

感谢 joyful pandas

猜你喜欢

转载自blog.csdn.net/weixin_41545602/article/details/112441283