时间序列分析—缺失值处理

本文依据知乎大佬的文章

清洗数据

数据清洗是数据分析的一个重要环节,对于时间序列数据也不例外,本节将详细介绍针对时间序列数据的数据清洗方法。

  • 缺失值处理
  • 改变时间频率
  • 平滑数据
  • 处理季节性问题
  • 防止无意识的向前看

缺失值处理

缺失值的出现很常见,例如在医疗场景中,一个时间序列数据出现缺失可能有以下原因:

  • 病人没有遵从医嘱
  • 病人的健康状态很好,因此没必要在每个时刻都记录
  • 病人被忘记了
  • 医疗设备出现随机性的技术故障
  • 数据录入问题

最常用的处理缺失值的方法包括填补(imputation)和删除(deletion)两种。

Imputation:基于完整数据集的其他值填补缺失值

Deletion:直接删除有缺失值的时间段

一般来说,我们更倾向于保留数据而不是删掉,避免造成信息损失。在实际案例中,采取何种方式要考虑是否可以承受删除特定数据的损失。

本节将重点讨论三种数据填补方法,并用python演示如何使用:

  • Forward fill
  • Moving average
  • Interpolation

用到的数据集是美国年度失业率数据,数据集来自OECD官网

Forward fill

前向填充法是用来填补数据最简单的方法之一,核心思想是用缺失值之前出现的最近一个时间点的数值来填补当前缺失值。使用这种方法不需要任何数学或复杂逻辑。

与前向填充相对应的,还有一种backward fill的方法,顾名思义,是指用缺失值之后出现的最近一个时间点的数值来填充。但是使用这种方法需要特别谨慎,因为这种方法是一种lookahead行为,只有当你不需要预测未来数据的时候才能考虑使用。

总结前向填充法的优点,计算简单,很容易用于实时流媒体数据。

Moving average

移动平均法是填补数据的另一种方法,核心思想是取出缺失值发生之前的一段滚动时间内的值,计算其平均值或中位数来填补缺失。在有些场景下,这种方法会比前向填充效果更好,例如数据的噪声很大,对于单个数据点有很大的波动,但用移动平均的方法就可以弱化这些噪声。

同样的,你也可以使用缺失值发生之后的时间点计算均值,但需要注意lookahead问题。

另外一个小trick是,计算均值时可以根据实际情况采取多种方法,如指数加权,给最近的数据点赋予更高的权重。

Interpolation

插值是另一种确定缺失数据点值的方法,主要基于我们希望整体数据如何表现的各种图像上的约束。 例如,线性插值要求缺失数据和邻近点之间满足一定的线性拟合关系。因此插值法是一种先验方法,使用插值法时需要代入一些业务经验。

在许多情况下,线性(或样条)插值都是非常合适的。例如考虑平均每周温度,其中存在已知的上升或上升趋势,气温下降取决于一年中的时间。或者考虑一个已知年度销售数据 不断增长的业务。在这些场景下,使用插值法都能取得不错的效果。

当然也有很多情况不适合线性(或样条)插值的场景。例如在天气数据集中缺少降水数据,就不应在已知天数之间进行线性推断,因为降水的规律不是这样的。同样,如果我们查看某人每天的睡眠时间,我们也不应该利用已知天数的睡眠时间线性外推。

Python代码实现
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
pd.set_option('max_row',1000)
# 导入美国年度失业率数据
unemploy = pd.read_csv('data\\unemployment.csv')
unemploy.head()
yearrate
019554.383333
119564.141667
219574.258333
319586.800000
419595.475000
# 构建一列随机缺失值列
unemploy['missing'] = unemploy['rate']
# 随机选择10%行手动填充缺失值
mis_index = unemploy.sample(frac=0.1,random_state=999).index
unemploy.loc[mis_index,'missing']=None

1.使用forward fill填补缺失值

unemploy['f_fill'] = unemploy['missing']
unemploy['f_fill'].ffill(inplace=True)
# 观察填充效果
plt.scatter(unemploy.year,unemploy.rate,s=10)
plt.plot(unemploy.year,unemploy.rate,label='real')
plt.scatter(unemploy[~unemploy.index.isin(mis_index)].year,unemploy[~unemploy.index.isin(mis_index)].f_fill,s=10,c='r')
plt.scatter(unemploy.loc[mis_index].year,unemploy.loc[mis_index].f_fill,s=50,c='r',marker='v')
plt.plot(unemploy.year,unemploy.f_fill,label='forward fill')
plt.legend()

2_1

2.使用moving average填补缺失值

unemploy['moveavg']=np.where(unemploy['missing'].isnull(),
                             unemploy['missing'].shift(1).rolling(3,min_periods=1).mean(),
                             unemploy['missing'])
# 观察填充效果
plt.scatter(unemploy.year,unemploy.rate,s=10)
plt.plot(unemploy.year,unemploy.rate,label='real')
plt.scatter(unemploy[~unemploy.index.isin(mis_index)].year,unemploy[~unemploy.index.isin(mis_index)].f_fill,s=10,c='r')
plt.scatter(unemploy.loc[mis_index].year,unemploy.loc[mis_index].f_fill,s=50,c='r',marker='v')
plt.plot(unemploy.year,unemploy.f_fill,label='forward fill',c='r',linestyle = '--')
plt.scatter(unemploy[~unemploy.index.isin(mis_index)].year,unemploy[~unemploy.index.isin(mis_index)].moveavg,s=10,c='r')
plt.scatter(unemploy.loc[mis_index].year,unemploy.loc[mis_index].moveavg,s=50,c='g',marker='^')
plt.plot(unemploy.year,unemploy.moveavg,label='moving average',c='g',linestyle = '--')
plt.legend()

2_2

3.使用interpolation填补缺失值

# 尝试线性插值和多项式插值
unemploy['inter_lin']=unemploy['missing'].interpolate(method='linear')
unemploy['inter_poly']=unemploy['missing'].interpolate(method='polynomial', order=3)
# 观察填充效果
plt.plot(unemploy.year,unemploy.rate,label='real')
plt.plot(unemploy.year,unemploy.inter_lin,label='linear interpolation',c='r',linestyle = '--')
plt.plot(unemploy.year,unemploy.inter_poly,label='polynomial interpolation',c='g',linestyle = '--')
plt.legend()

2_3

改变数据集时间频率

通常我们会发现来自不同数据源的时间轴常常无法一一对应,此时就要用到改变时间频率的方法进行数据清洗。由于无法改变实际测量数据的频率,我们能做的是改变数据收集的频率,也就是本节提到的上采样(upsamping)和下采样(downsampling)。

下采样

下采样指的是减少数据收集的频率,也就是从原始数据中抽取子集的方式。

以下是一些下采样会用到的场景:

  • 数据原始的分辨率不合理:例如有一个记录室外温度的数据,时间频率是每秒钟一次。我们都知道,气温不会在秒钟这个级别有明显的变化,而且秒级的气温数据的测量误差甚至会比数据本身的波动还要大,因此这个数据集有着大量的冗余。在这个案例中,每隔n个元素取一次数据可能更合理一些。
  • 关注某个特定季节的信息:如果担心某些数据存在季节性的波动,我们可以只选择某一个季节(或月份)进行分析,例如只选择每年一月份的数据进行分析。
  • 进行数据匹配:例如你有两个时间序列数据集,一个更低频(年度数据),一个更高频(月度数据),为了将两个数据集匹配进行下一步的分析,可以对高频数据进行合并操作,如计算年度均值或中位数,从而获得相同时间轴的数据集。

上采样

上采样在某种程度上是凭空获得更高频率数据的方式,我们要记住的是使用上采样,只是让我们获得了更多的数据标签,而没有增加额外的信息。

以下是一些上采样会用到的场景:

  • 不规律的时间序列:用于处理多表关联中存在不规则时间轴的问题。

    例如现在有两个数据,一个记录了捐赠的时间和数量

amtdt
992019-2-27
1002019-3-2
52019-6-13
152019-8-1
112019-8-31
12002019-9-15

另一个数据记录了公共活动的时间和代号

identifierdt
q4q422019-1-1
4299hj2019-4-1
bbg22019-7-1

这时我们需要合并这两个表的数据,为每次捐赠打上标签,记录每次捐赠之前最近发生的一次公共活动,这种操作叫做rolling join,关联后的数据结果如下。

identifierdtamt
q4q42992019-2-27
q4q421002019-3-2
4299hj52019-6-13
bbg2152019-8-1
bbg2112019-8-31
bbg212002019-9-15
  • 进行数据匹配:类似下采样的场景,例如我们有一个月度的失业率数据,为了和其他数据匹配需要转换成日度的数据,如果我们假定新工作一般都是从每个月第一天开始的,那么可以推演认为这个月每天的失业率都等于该月的失业率。

通过以上的案例我们发现,即使是在十分干净的数据集中,由于需要比较来自不同维度的具有不同尺度的数据,也经常需要使用到上采样和下采样的方法。

平滑数据

数据平滑也是一个常用的数据清洗的技巧,为了能讲述一个更能被理解的故事,在数据分析前常常会进行平滑处理。数据平滑通常是为了消除一些极端值或测量误差。即使有些极端值本身是真实的,但是并没有反应出潜在的数据模式,我们也会把它平滑掉。

在讲述数据平滑的概念时,需要引入下图层层递进。

2_4

weighted averaging,也就是上文曾经讲过的moving average,也是一种最简单的平滑技术,即可以给予数据点相同的权重,也可以给越邻近的数据点更高的权重。

exponential smoothing,本质上和weighted averaging类似,都是给越邻近的数据点更高的权重,区别在于衰减的方式不同,指数平滑法顾名思义,从最邻近到最早的数据点的权重呈现指数型下降的规律,weighted averaging需要为每一个权重指定一个确定值。指数平滑法在很多场景下效果都很好,但它也有一个明显的缺点,无法适用于呈现趋势变化或季节性变化的数据。

image-20220121115026312

其中image-20220121115241864表示当前时刻和上一个时刻的平滑值,表image-20220121115606952示当前时刻的实际值,α表示平滑系数,该系数越大则越近邻的数据影响越大。

Holt Exponential Smoothing,这种技术通过引入一个额外的系数,解决了指数平滑无法应用于具有趋势特点数据的不足,但但是依然无法解决具有季节性变化数据的平滑问题。

Holt-Winters Exponential Smoothing,这种技术通过再次引入一个新系数的方式同时解决了Holt Exponential Smoothing无法解决具有季节性变化数据的不足。简单来说,它是在指数平滑只有一个平滑系数的基础上,额外引入了趋势系数和季节系数来实现的。这种技术在时间序列的预测上(例如未来销售数据预测)有着很广泛的应用。

Python实现指数平滑

# 导入航空乘客数据
air = pd.read_csv('data\\air.csv')
# 设置两种平滑系数
air['smooth_0.5']= air.Passengers.ewm(alpha =0.5).mean()
air['smooth_0.9']= air.Passengers.ewm(alpha =0.9).mean()
# 可视化展现
plt.plot(air.Date,air.Passengers,label='actual')
plt.plot(air.Date,air['smooth_0.5'],label='alpha=0.5')
plt.plot(air.Date,air['smooth_0.9'],label='alpha=0.9')
plt.xticks(rotation=45)
plt.legend()

image-20220121120153616

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐