Pandas 处理时间序列(能源消耗实战)

2022-09-16 13:36:41

本文转自:采用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 行数据,这里可以看到两个数据集的样本个数不同,如果我们需要对比两个数据或者进行比较分析,需要对数据进行处理。

<class 'pandas.core.frame.DataFrame'>
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
<class 'pandas.core.frame.DataFrame'>
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

时间序列数据清理

对于这个数据集来说,目前有两处需要清理:

  1. 出现重复的时间戳及样本,需要我们移除

  2. 样本排序混乱

对于一般的时间序列去重,我们可以通过保留第一个或者最后一个来进行清理,这里我们采用求均值的方法,也就是对重复时间戳的样本进行求均值。理由如下:

  1. 其实该数据集是单纯的重复类型,保留第一个,最后一个,或者求均值,结果是一致的

  2. 重要的是想展示一个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可以进行索引。详细的解释参考如下。

时间序列特征工程

一元时间序列如果需要进行回归分析或者分类预测,必然需要通过特征工程扩展特征数量,常用的特征工程有三类:

  1. 时间信息的提取

  2. 基于时间窗口的时域统计

  3. 基于时间窗口的频域统计

时间信息的提取指的是对时间列进行特征工程,提取时间戳中和人类活动日历相关的时间信息,比如是否是月末,是否是周末,这是几月等等。这里列出常用的时间信息的提取。

# 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数据干货交流群
  • 作者:数据不吹牛
  • 原文链接:https://blog.csdn.net/SeizeeveryDay/article/details/108989109
    更新时间:2022-09-16 13:36:41