时序数据
时序中的基本对象
对于时序数据我们需要时间点、时间差、时间段和时间位移?
时间差和时间段有什么不一样?
概念 | 单元素类型 | 数组类型 | 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_datetime
和date_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
时,start
和end
一定会包含在里面,最后一个周期的长度可能比前面的周期短。
但选定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_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')
时序中的滑窗和分组
滑动窗口
Series.rolling()
所谓时序的滑窗函数,即把滑动窗口用freq
关键词代替,下面给出一个具体的应用案例:在股票市场中有一个指标为BOLL
指标,它由中轨线、上轨线、下轨线这三根线构成,具体的计算方法分别是N
日均值线、N
日均值加两倍N
日标准差线、N
日均值减两倍N
日标准差线。利用rolling
对象计算N=30
的BOLL
指标可以如下写出:
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 |
- 将
Datetime, Time
合并为一个时间列Datetime
,同时把它作为索引后排序。 - 每条记录时间的间隔显然并不一致,请解决如下问题:
- 找出间隔时间的前三个最大值所对应的三组时间戳。
- 是否存在一个大致的范围,使得绝大多数的间隔时间都落在这个区间中?如果存在,请对此范围内的样本间隔秒数画出柱状图,设置
bins=50
。
- 求如下指标对应的
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ㄒ)/~~)