一 、时间序列中的基本对象
时间戳,即'2020-9-7 08:00:00'和'2020-9-7 10:00:00'这两个时间点分别代表了上课和下课的时刻,在pandas中称为Timestamp。同时,一系列的时间戳可以组成DatetimeIndex,而将它放到Series中后,Series的类型就变为了datetime64[ns],如果有涉及时区则为datetime64[ns, tz],其中tz是timezone的简写。
时间差,即上课需要的时间,两个Timestamp做差就得到了时间差,pandas中利用Timedelta来表示。类似的,一系列的时间差就组成了TimedeltaIndex, 而将它放到Series中后,Series的类型就变为了timedelta64[ns]。
时间段,即在8点到10点这个区间都会持续地在上课,在pandas利用Period来表示。类似的,一系列的时间段就组成了PeriodIndex, 而将它放到Series中后,Series的类型就变为了Period。
日期偏置,假设你只知道9月的第一个周一早上8点要去上课,但不知道具体的日期,那么就需要一个类型来处理此类需求。再例如,想要知道2020年9月7日后的第30个工作日是哪一天,那么时间差就解决不了你的问题,从而pandas中的DateOffset就出现了。同时,pandas中没有为一列时间偏置专门设计存储类型,理由也很简单,因为需求比较奇怪,一般来说我们只需要对一批时间特征做一个统一的特殊日期偏置。
二、时间戳
Timestamp的构造与属性
- 单个时间戳:使用pd.Timestamp
ts = pd.Timestamp('2020/1/1')
ts = pd.Timestamp('2020-1-1 08:10:30')
- 可以通过year, month, day, hour, min, second获取具体的数值
Datetime序列的生成
- to_datetime,能够把一列时间戳格式的对象转换成为datetime64[ns]类型的时间序列:
pd.to_datetime(['2020-1-1', '2020-1-3', '2020-1-6'])
# 可使用format进行格式匹配
temp = pd.to_datetime(['2020\\1\\1','2020\\1\\3'],format='%Y\\%m\\%d')
- date_range
- start:开始时间
- end:结束时间
- freq:时间间隔
- periods:时间戳个数
pd.date_range('2020-1-1','2020-2-28', freq='10D')
【练一练】
Timestamp
上定义了一个value
属性,其返回的整数值代表了从1970年1月1日零点到给定时间戳相差的纳秒数,请利用这个属性构造一个随机生成给定日期区间内日期序列的函数。
def rand_date(date_interval, size=10):
start = pd.Timestamp(date_interval[0]).value / 10**9 # 单位转换成s
end = pd.Timestamp(date_interval[1]).value / 10**9 # 单位转换成s
rand = np.random.randint(start, end+1, size=size)
return pd.to_datetime(rand, unit='s')
rand_date(['2020-1-1', '2020-2-1'])
asfreq, 对给定的序列进行类似于reindex操作
s = pd.Series(np.random.rand(5), index=pd.to_datetime(['2020-1-%d'%i for i in range(1,10,2)]))
s.asfreq('12H')
【练一练】
前面提到了datetime64[ns]
本质上可以理解为一个大整数,对于一个该类型的序列,可以使用max, min, mean
,来取得最大时间戳、最小时间戳和“平均”时间戳。
date =rand_date(['2020-1-1', '2020-2-1'], size=100) # 随机生成100个日期
print(date.max()) # 最大值
print(date.min()) # 最小值
print(date.mean()) # 平均值
dt对象,如同category, string的序列上定义了cat, str来完成分类数据和文本数据的操作,在时序类型的序列上定义了dt对象来完成许多时间序列的相关操作。
- 取出时间相关的属性:
date, time, year, month, day, hour, minute, second, microsecond, nanosecond, dayofweek, dayofyear, weekofyear, daysinmonth, quarter
- 判断时间戳是否满足条件
s.dt.is_year_start # 还可选 is_quarter/month_start
s.dt.is_year_end # 还可选 is_quarter/month_end
- 取整操作
s.dt.round('1H')
s.dt.ceil('1H')
s.dt.floor('1H')
时间戳的切片与索引
- 利用dt对象和布尔条件联合使用
s[(idx.is_month_start|idx.is_month_end).values].head()
- 利用切片,常用于连续时间戳
s['2020-05':'2020-7-15']
三、时间差
Timedelta的生成
- 单个, 通过pd.Timedelta
pd.Timestamp('20200102 08:00:00')-pd.Timestamp('20200101 07:35:00')
pd.Timedelta('1 days 25 minutes') # 字符串生成
- 序列,通过pd.to_timedelta, 或timedelta_range
s = pd.to_timedelta(df.Time_Record)
pd.timedelta_range('0s', '1000s', freq='6min')
pd.timedelta_range('0s', '1000s', periods=3)
- Timedelta序列也定义了dt对象,属性包括days, seconds, mircroseconds, nanoseconds,它们分别返回了对应的时间差特征。需要注意的是,这里的seconds不是指单纯的秒,而是对天数取余后剩余的秒数
Timedelta的运算
- 与标量的乘法运算
- 与时间戳的加减法运算
- 与时间差的加减法与除法运算
四、日期偏置
Offset对象,在pd.offsets中被定义。当使用+时获取离其最近的下一个日期,当使用-时获取离其最近的上一个日期
- CDay,其中的holidays, weekmask参数能够分别对自定义的日期和星期进行过滤,前者传入了需要过滤的日期列表,后者传入的是三个字母的星期缩写构成的星期字符串,其作用是只保留字符串中出现的星期
pd.Timestamp('20200831') - pd.offsets.WeekOfMonth(week=0,weekday=0)
pd.Timestamp('20200907') - pd.offsets.BDay(30)
pd.Timestamp('20200907') + pd.offsets.MonthEnd()
my_filter = pd.offsets.CDay(n=1,weekmask='Wed Fri',holidays=['20200109'])
dr = pd.date_range('20200108', '20200111')
dr.to_series().dt.dayofweek
偏置字符串
pd.date_range('20200101','20200331', freq='MS') # 月初
pd.date_range('20200101','20200331', freq='M') # 月末
pd.date_range('20200101','20200110', freq='B') # 工作日
pd.date_range('20200101','20200201', freq='W-MON') # 周一
pd.date_range('20200101','20200201', freq='WOM-1MON') # 每月第一个周一
五、时序中的滑窗与分组
滑动窗口,所谓时序的滑窗函数,即把滑动窗口用freq关键词代替
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)
s.head()
重采样,重采样对象resample和第四章中分组对象groupby的用法类似,前者是针对时间序列的分组计算而设计的分组对象
s.resample('7min', origin='start').mean().head()
六、练习
Ex1:太阳辐射数据集
现有一份关于太阳辐射的数据集:
df = pd.read_csv('../data/solar.csv', usecols=['Data','Time','Radiation','Temperature'])
df.head(3)
- 将
Datetime, Time
合并为一个时间列Datetime
,同时把它作为索引后排序。 - 每条记录时间的间隔显然并不一致,请解决如下问题:
- 找出间隔时间的前三个最大值所对应的三组时间戳。
- 是否存在一个大致的范围,使得绝大多数的间隔时间都落在这个区间中?如果存在,请对此范围内的样本间隔秒数画出柱状图,设置
bins=50
。
- 求如下指标对应的
Series
:
- 温度与辐射量的6小时滑动相关系数
- 以三点、九点、十五点、二十一点为分割,该观测所在时间区间的温度均值序列
- 每个观测6小时前的辐射量(一般而言不会恰好取到,此时取最近时间戳对应的辐射量)
# 合并为Datetime,作为索引后排序
Datetime = pd.to_datetime(df.Data) + pd.to_timedelta(df.Time)
df_op = df.drop(['Data', 'Time'], axis=1).set_index(Datetime)
df_op.rename_axis(index='Datetime', inplace=True)
df_op.sort_values('Datetime', inplace=True)
df_op.head()
# 找出间隔时间的前三个最大值对应的三组时间戳
Datetime = pd.Series(df_op.index)
index = Datetime.diff(1).sort_values(ascending=False).head(3).index
df_op.iloc[[index[0]-1, index[0], index[1]-1, index[1], index[2]-1, index[2]]]
delta = pd.to_timedelta(Datetime.diff(1))
delta.quantile(0.01)
cond1 = delta > delta.quantile(0.01)
cond2 = delta < delta.quantile(0.99)
delta.loc[(cond1 & cond2)].astype('timedelta64[s]').plot.hist(bins=50)
# 6小时滑动相关系数
r1 = df_op.Radiation.rolling('6H')
r1.corr(df_op.Temperature).head(10)
# 温度均值
df_op.Temperature.resample('6H', origin='2016-09-01 03:00:00').mean()
Ex2:水果销量数据集
现有一份2019年每日水果销量记录表:
df = pd.read_csv('../data/fruit.csv')
df.head(3)
- 统计如下指标:
- 每月上半月(15号及之前)与下半月葡萄销量的比值
- 每月最后一天的生梨销量总和
- 每月最后一天工作日的生梨销量总和
- 每月最后五天的苹果销量均值
- 按月计算周一至周日各品种水果的平均记录条数,行索引外层为水果名称,内层为月份,列索引为星期。
- 按天计算向前10个工作日窗口的苹果销量均值序列,非工作日的值用上一个工作日的结果填充。
df.Date = pd.to_datetime(df.Date)
df.set_index('Date', inplace=True)
df.head()
def process(group):
ret = pd.Series(data=np.zeros(4), index=['葡萄销量比值', '最后一天梨销量', '最后工作日梨销量', '最后五天苹果均值'])
# 上半月与下半月的葡萄销量的比值
date = pd.to_datetime(group.index).to_series()
Grape = group.Fruit == 'Grape'
ret['葡萄销量比值'] = group.loc[(date.dt.day<=15 & Grape), 'Sale'].sum() / group.loc[(date.dt.day>15 & Grape), 'Sale'].sum()
# 每月最后一天的省梨销量总和
Pear = group.Fruit == 'Pear'
ret['最后一天梨销量'] = group.loc[(date.dt.is_month_end & Pear), 'Sale'].sum()
# 每月最后一天工作日的省梨销量总和
workday = ~(date.dt.weekday.isin([5,6]))
lastworkday = date.dt.day.loc[workday].max()
ret['最后工作日梨销量'] = group.loc[((date.dt.day == lastworkday) & Pear), 'Sale'].sum()
# 每月最后五天的苹果销量均值
day = date.dt.day.unique()
day.sort()
Apple = group.Fruit == 'Apple'
ret['最后五天苹果均值'] = group.loc[((date.dt.day.isin(day[-5:])) & Apple), 'Sale'].mean()
return ret
df.resample('1M').apply(process).head()
month_order = ['January','February','March','April','May','June','July','August','September','October','November','December']
week_order = ['Mon','Tue','Wed','Thu','Fri','Sat','Sum']
df = df.reset_index()
group1 = df.Fruit
group2 = df.Date.dt.month_name().astype('category').cat.reorder_categories(month_order, ordered=True)
group3 = df.Date.dt.dayofweek.replace(dict(zip(range(7),week_order))).astype('category').cat.reorder_categories(week_order, ordered=True)
res = df.groupby([group1, group2,group3])['Sale'].count().to_frame().unstack(2).droplevel(0,axis=1)
res.head()
df_apple = df[(df.Fruit=='Apple')&(~df.Date.dt.dayofweek.isin([5,6]))]
s = pd.Series(df_apple.Sale.values,index=df_apple.Date).groupby('Date').sum()
res = s.rolling('10D').mean().reindex(pd.date_range('20190101','20191231')).fillna(method='ffill')
res.head()