本文转自:采用pandas 处理时间序列(能源消耗实战) - 知乎
作者:BingBlackBean
前言
时间序列的处理是传统经济学里面的一个重要篇章,在数据科学和机器学习的背景下,时间序列分析所包含的内容更加复杂。
计量经济学里的时间序列特指一元时间序列,也就是数据包含两列,第一列是时间戳,第二列是观察对象。这属于比较经典的时间序列。有时候你会注意到一些时间序列的模型或者算法,比如ARIMA,prophet等,都是针对这类时间序列。
商业里面的交易历史信息也是一元时间序列。工业领域中,一些监测数据,比如天气温度,也是一元时间序列。但是时间序列不止有一元时间序列,当同一个时间戳对应的观测对象不只一个时,我们就有了多元时间序列。比如某个城市的空气PM2.5的预测,我们可以通过PM2.5的历史时间观测值来预测。我们也可以通过当天(或者近期)的其他观测对象来预测,比如风速,温度,湿度等。
多元时间序列在表现形式上就是数据包含多列(大于两列),第一列是时间戳,后面的列都是观察对象。当时间序列是多元时,很多经典的机器学习模型可以施展拳脚,比如回归模型,分类模型,这些模型都依赖于多元的特征。对于我们本文以及后续的分析中,我们不会将时间序列特指为一元时间序列。
无论是一元时间序列的分析还是多元时间序列的分析,对于时间相关的预处理格外重要。今天我们就讨论pandas在时间序列处理中应用。
导入数据
这里我们采用美国能源消耗数据集进行分析和讨论,数据集可以在kaggle上下载,如果有问题,可以留言讨论。该数据集包含了美国一家能源公司的长达数十年的能源消耗数据,数据分辨率为小时。这里我们下载了两个数据集进行对比分析,PJME_hourly 和PJMW_hourly (分别对应东区和西区)。数据集默认放在同目录的data文件夹下。
import seaborn as sns
import pandas as pd
import matplotlib.pyplot as plt
pjme_file = 'data/PJME_hourly.csv'
pjmw_file = 'data/PJMW_hourly.csv'
通过pandas 的read_csv 来读取数据。
df_1 = pd.read_csv(pjme_file)
df_2 = pd.read_csv(pjmw_file)
print(df_1.info())
print(df_2.info())
数据集并不大,只有2.2MB左右。df_1 包含了145366 行数据,df_2 包含了143206 行数据,这里可以看到两个数据集的样本个数不同,如果我们需要对比两个数据或者进行比较分析,需要对数据进行处理。
RangeIndex: 145366 entries, 0 to 145365
Data columns (total 2 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 Datetime 145366 non-null object
1 PJME_MW 145366 non-null float64
dtypes: float64(1), object(1)
memory usage: 2.2+ MB
None
RangeIndex: 143206 entries, 0 to 143205
Data columns (total 2 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 Datetime 143206 non-null object
1 PJMW_MW 143206 non-null float64
dtypes: float64(1), object(1)
memory usage: 2.2+ MB
None
时间列(时间戳)的处理
默认读取的时间列为字符形式,我们可以通过pandas的describe函数来进行统计,首先我们对原始时间列进行统计。
print(df_1['Datetime'].describe())
结果如下表,我们可以看到unique 数值不同于count数值,这说明有重复的时间戳。更重要是,由于当前的时间戳是字符串格式,无法进行时间相关的统计。
count 145366
unique 145362
top 2015-11-01 02:00:00
freq 2
我们通过to_datetime 将字符串转换为pandas 的Timestamp 格式。这里需要指定字符串的格式。需要注意的是指定的时间格式需要完全匹配样本的格式,而且要确保所有样本的时间戳格式是一致的。
df_1['Datetime'] = pd.to_datetime(df_1['Datetime'],format='%Y-%m-%d %H:%M:%S')
print(df_1['Datetime'].describe())
转换后的时间列重新进行统计,结果如下:
count 145366
unique 145362
top 2014-11-02 02:00:00
freq 2
first 2002-01-01 01:00:00
last 2018-08-03 00:00:00
Name: Datetime, dtype: object
这里可以看到和时间相关的统计信息,比如开始的时间是2002-01-01 01:00:00,结束的时间是2018-08-03 00:00:00。
如果我们显示数据集的前5行(如下图),就会发现第一行的时间并不等于上面的开始时间,这说明样本并不是按照时间顺序严格排序的,这对于时间序列分析来说是很大的坑:千万不要轻信时间系列是默认排序正确的!!!
Datetime PJME_MW
0 2002-12-31 01:00:00 26498.0
1 2002-12-31 02:00:00 25147.0
2 2002-12-31 03:00:00 24574.0
3 2002-12-31 04:00:00 24393.0
4 2002-12-31 05:00:00 24860.0
时间序列数据清理
对于这个数据集来说,目前有两处需要清理:
出现重复的时间戳及样本,需要我们移除
样本排序混乱
对于一般的时间序列去重,我们可以通过保留第一个或者最后一个来进行清理,这里我们采用求均值的方法,也就是对重复时间戳的样本进行求均值。理由如下:
其实该数据集是单纯的重复类型,保留第一个,最后一个,或者求均值,结果是一致的
重要的是想展示一个pivot_table的用法。
我们采用pivot_table,将时间列设为index,将观察对象列设为value,aggfuc采用mean。这样我们就消除了重复项,确保时间列的每个值是唯一的。
之后我们用sort_values进行重新排序,并且设置时间列为index(索引)。
df_1 = pd.pivot_table(data=df_1,values='PJME_MW',index='Datetime',aggfunc='mean').reset_index()
df_1.sort_values(by='Datetime',inplace=True)
df_1.set_index('Datetime',inplace=True)
这样我们就得到清理后的数据,并且索引为时间戳。我们对df_2 进行同样的操作,然后进行对比。
时间序列可视化
对于时间序列,最常用的plot就是趋势图。直接用pandas的plot函数即可,也可以用seaborn的lineplot。这里我们采用两种方式分别画出df_1和df_2的趋势,通过对比我们也可以看到两种plot方式的细微差别,尤其是对于y轴标签和图例默认值的处理上。
# plot data
fig,ax = plt.subplots(2,1)
df_1.plot(ax =ax[0])
sns.lineplot(data=df_1,x=df_1.index,y='PJME_MW',ax=ax[1])
两个时间序列的都呈现明显的周期性,这是可以理解的。因为能源消耗(耗电量)本来就是很人类活动息息相关,自然会和日历的周期性有一定的吻合。
当我们把两个df合并在一起时,就会得到一个多元的时间序列,对于多元的时间序列,相关分析也是最常用的分析方式。
df = pd.concat([df_1,df_2],axis=1)
sns.scatterplot(x='PJME_MW',y='PJMW_MW',data=df)
从上图来看,两者还是存在明显的正相关,也就是东区耗电量增加时,西区耗电量也增加。
时间序列重采样
对于原始数据来说,时间分辨率是小时。有时候我们需要对数据的分辨率进行调整,比如为了查看每月的耗电量的的情况。因此resample (重采样)必不可少。
# resample data
day_df = df_1.resample(rule='D').mean()
week_df = df_1.resample(rule='W').mean()
month_df = df_1.resample(rule='M').mean()
quarter_df = df_1.resample(rule='Q').mean()
year_df = df_1.resample(rule='Y').mean()
print(month_df.info())
fig,ax = plt.subplots(2,1)
sns.lineplot(data=df_1,x=df_1.index,y='PJME_MW',ax=ax[0])
sns.lineplot(data=month_df,x=month_df.index,y='PJME_MW',ax=ax[1])
常用的周期D,W,M,Q,Y分别代表每天,每周,每月,每季度和每年。我们对比每月重采样的数据和原始数据。可以看到按月重采样的曲线更加光滑,这是因为每周和每天的周期信息已经被过滤了。
其实重采样就是时间序列分解的”思想原型“,通过重采样我们可以看到每个时间周期的”信号分量“。
num_ax = 5
fig,ax = plt.subplots(num_ax,1)
#[ax[i].set_ylim(10000,22000) for i in range(num_ax)]
day_df.plot(ax=ax[0])
week_df.plot(ax=ax[1])
month_df.plot(ax=ax[2])
quarter_df.plot(ax=ax[3])
year_df.plot(ax=ax[4])
当然了,对于每一种重采样,后续采用的统计方式不一定是均值(mean),也可以选择其他,比如sum(求和)来获取每月耗电量之和。
month_sum_df = df_1.resample(rule='M').sum()
时间切片与索引
DataFrame数据用时间戳作为索引,最大的好处是可以快速对样本进行索引和切片。进行索引和切片时,不一定需要完全匹配时间戳的格式,比如,你可以快速索引某个年度的所有样本。
print(day_df.loc['2014-02-12']) #获得某一天的样本
print(day_df['2015']) #获得某一年的额样本
print(day_df['2014-02-12':'2014-02-19']) #获取某个时间段
#print(day_df['2014-02-12']) !!!这是错误示例
print(month_df.asof('2014-02')) #获取某一月
这里需要注意的是:当返回结果只有一个时,无法采用[]进行索引,比如
#print(day_df['2014-02-12']) !!!这是错误示例。因为day_df的每一天只有一个样本,此时只有iloc可以进行索引。详细的解释参考如下。
时间序列特征工程
一元时间序列如果需要进行回归分析或者分类预测,必然需要通过特征工程扩展特征数量,常用的特征工程有三类:
时间信息的提取
基于时间窗口的时域统计
基于时间窗口的频域统计
时间信息的提取指的是对时间列进行特征工程,提取时间戳中和人类活动日历相关的时间信息,比如是否是月末,是否是周末,这是几月等等。这里列出常用的时间信息的提取。
# get more datetime attributes
df_1['day']= df_1.index.day # means which day in this month
df_1['dayofweek']= df_1.index.dayofweek
df_1['dayofyear']= df_1.index.dayofyear
df_1['days_in_month']= df_1.index.days_in_month # how many days in this month
df_1['daysinmonth']= df_1.index.daysinmonth # same as days_in_month
df_1['is_month_end']= df_1.index.is_month_end
df_1['is_month_start']= df_1.index.is_month_start
df_1['is_quarter_start']= df_1.index.is_quarter_start
df_1['is_quarter_end']= df_1.index.is_quarter_end
df_1['month']= df_1.index.month
df_1['week']= df_1.index.week
df_1['weekofyear']= df_1.index.weekofyear # same as week
df_1['year']= df_1.index.year
df_1['date']= df_1.index.date
df_1['time']= df_1.index.time
基于时间窗口的统计分析,可以分析时域分析和频域分析。频域分析主要用于高频时间序列(信号)的分析,比如声音也算是时间序列。我们先不做讲解,这里主要说一下时域分析。
时域分析很简单,当一个时间窗口确定后,意味着我们有一段有限长度的时间序列(有限的数据样本),我们可以进行统计分析,比如求均值,方差,众数,中位数等等。
df_1['window_mean']= df_1['PJME_MW'].rolling(window=24,center=True).mean() # it will generate null
print(df_1.head(24))
这里采用rolling 函数进行”滚动窗口“,然后对每个滚动窗口内的所有样本进行求统计均值等操作。需要注意的是,采用滚动窗口的方式,会出现某些样本的窗口样本不足指定数量,从而结果为NaN,实践中需要进行缺失值处理。
PJME_MW window_mean
Datetime
2002-01-01 01:00:00 30393.0 NaN
2002-01-01 02:00:00 29265.0 NaN
2002-01-01 03:00:00 28357.0 NaN
2002-01-01 04:00:00 27899.0 NaN
2002-01-01 05:00:00 28057.0 NaN
2002-01-01 06:00:00 28654.0 NaN
2002-01-01 07:00:00 29308.0 NaN
2002-01-01 08:00:00 29595.0 NaN
2002-01-01 09:00:00 29943.0 NaN
2002-01-01 10:00:00 30692.0 NaN
2002-01-01 11:00:00 31395.0 NaN
2002-01-01 12:00:00 31496.0 NaN
2002-01-01 13:00:00 31031.0 31017.500000
2002-01-01 14:00:00 30360.0 30922.833333
2002-01-01 15:00:00 29798.0 30846.666667
2002-01-01 16:00:00 29720.0 30802.666667
2002-01-01 17:00:00 31271.0 30787.416667
2002-01-01 18:00:00 35103.0 30801.916667
2002-01-01 19:00:00 35732.0 30889.166667
2002-01-01 20:00:00 35639.0 31114.875000
2002-01-01 21:00:00 35285.0 31436.458333
2002-01-01 22:00:00 34007.0 31743.916667
2002-01-01 23:00:00 31857.0 32008.208333
2002-01-02 00:00:00 29563.0 32231.666667
总结
本文涵盖了时间序列分析的基本处理操作,包括时间戳的处理,排序,去重,索引与切片等。对于时间序列,可以进行重采样来满足特定的分辨率需求,也可以以此查看基本的周期趋势。一元时间序列可以通过滚动窗口时域分析,时间列信息提取等方法进行特征工程,为最终的机器学习模型做好准备。
●Pandas进阶文章!
●取数,取数,取个屁啊!
后台回复“入群”即可加入小z数据干货交流群