如何处理时间序列异常值?理解、检测和替换时间序列中的异常值
如何处理时间序列异常值?理解、检测和替换时间序列中的异常值
异常值的类型
异常值是与正常行为有显著偏差的观察结果。时间序列可能会因某些异常和非重复事件而出现异常值。这些异常值会影响时间序列分析,并误导从业者得出错误的结论或有缺陷的预测。因此,识别和处理异常值是确保时间序列建模可靠性的关键步骤。
在时间序列中,异常值通常分为两种类型:加性异常值和创新异常值。
附加异常值
附加异常值是相对于历史数据表现出异常高(或低)值的观察值。加性异常值的一个例子是由于促销或相关病毒式内容导致产品销量激增。有时这些异常值是由于错误的数据收集而发生的。加性与异常值对底层系统的非持久性影响有关。异常值仅限于相应的观察值,此后时间序列恢复其正常模式。
包含一些附加异常值的时间序列。图片由作者提供。
附加异常值可以跨越连续的观测值。这些异常值也称为子序列异常值或异常值块。
创新异类
创新异常值与附加异常值类似,但具有持久性效应。异常值会对后续观察产生影响。一个常见的例子是,由于某些病毒式内容,网站的访问量有所增加。该网站的访问量可能会继续高于平常,直到这种影响消失。
编辑搜图
请点击输入图片描述(最多18字)
具有创新异常值的时间序列。图片由作者提供。
处理创新异常值的一种方法是使用干预分析。例如,使用一个虚拟变量,其效果会随着时间的推移而逐渐减弱。
与变化点的关系
离群值与变化的概念相关。一些观测值被称为变化点,标志着时间序列中结构变化的开始。这些变化点与异常值相关但又不同。异常值是相对于特定分布的异常观测值。变化点是以分布变化为特征的结构性突变。
异常值的含义
如何处理异常值取决于其性质和分析的目标。
因噪声(例如数据收集错误)而产生的异常值是不需要的数据。在分析之前,应删除或替换此类异常值。
另一方面,一些异常值本身很有趣,而且预测起来很重要。因此,删除它们可能会导致误导性结论或过于乐观的预测。这种情况发生在各种领域,例如欺诈检测或能源。考虑一个能源需求的时间序列,其中能源负荷在某个时期激增。这种类型的异常值可能是由某些异常事件(例如极寒天气)引起的。公用事业公司需要预测此类异常值,因此删除它们并不是一个好主意。对这些观察结果进行建模是平衡能源供需和防止停电的关键。
检测并处理异常值
检测时间序列数据中的异常值的方法有很多种,其中很多方法可以分为两类:基于预测或基于估计。
基于预测的检测
根据预测检测异常值需要使用预测模型。目标是将预测值与实际值进行比较。两者之间的较大差异表明观察结果为异常值。
让我们使用以下时间序列来看一下这在实践中是如何运作的:
from datasetsforecast.m4 import M4
dataset, *_ = M4.load('./data', 'Hourly')
series = dataset.query(f'unique_id=="H1"').reset_index(drop=True)
上面代码中,我们从 M4 数据集中获取了 id 为 H1 的时间序列。接下来,我们基于 statsforecast 构建一个季节性朴素预测模型:
from statsforecast import StatsForecast
from statsforecast.models import SeasonalNaive
# 季节性朴素模型
model = [SeasonalNaive(season_length=24)]
# 创建 statsforecast 实例
sf = StatsForecast(df=series, models=model, freq='H')
# 拟合预测模型
sf.forecast(h=1, level=[99], fitted=True)
# 获取样本内预测
preds = sf.forecast_fitted_values()
建立模型后,我们使用forecast_fitted_values方法获取训练样本的预测区间。然后,我们将其与实际值进行比较:
# 基于预测区间的异常值
outliers = preds.loc[(preds['y'] >= preds['SeasonalNaive-hi-99']) | (preds['y'] <= preds['SeasonalNaive-lo-99'])]
任何超出 99% 预测区间的观测值均被视为异常值。
这是异常值的图:
季节性朴素模型检测到的异常值。图片来自作者
您也可以使用预测的实际误差来代替间隔。在这种情况下,当误差异常大时,就会出现异常值。
基于估计的检测
基于估计的方法使用汇总统计数据来检测异常值。一个例子是 z 分数。其思想是通过减去平均值并除以标准差来标准化数据。然后,异常值是具有较大 z 分数值的点。
以下是一个例子:
# 高于/低于 3 个标准差的值
thresh = 3
rolling_series = series['y'].rolling(window=24, min_periods=1, center=True)
avg = rolling_series.mean()
std = rolling_series.std(ddof=0)
zscore = series['y'].sub(avg).div(std)
m = zscore.between(-thresh, thresh)
请注意,使用滚动窗口计算平均值和标准差来解释时间序列中的时间依赖性。
另一种方法是使用时间序列分解方法并检测残差上的异常值。让我们首先使用 STL 获取残差:
from statsmodels.tsa.seasonal import STL
stl = STL(series['y'].values, period=24, robust=True).fit()
resid = pd.Series(stl.resid)
请注意,我们将参数robust=True传递给 STL,以便模型可以容忍更大的错误。
然后,您可以使用标准箱线图规则来检测异常值。例如,将低于第一四分位数或高于第三四分位数的 IQR 3 倍的观测值标记为异常。操作方法如下:
q1, q3 = resid.quantile([.25, .75])
iqr = q3 - q1
is_outlier_r = ~resid.apply(lambda x: q1 - (3 * iqr) < x < q3 + (3 * iqr))
is_outlier_r_idx = np.where(is_outlier_r)[0]
resid_df = resid.reset_index()
resid_df['index'] = pd.date_range(end='2021-12-01', periods=series.shape[0], freq='H')
resid_df.columns = ['index', '残差']
这些异常值在一系列残差中也很明显:
使用残差中的箱线图规则检测异常值。图片由作者提供。
替换异常值
检测后,您可以通过用更合理的值替换异常值来清除异常值。首先删除异常值,然后将问题转变为数据插补任务。
时间序列插补有很多方法。这些包括:
- 向前或向后填充
- 移动平均线
- 线性插值
关键要点
- 时间序列异常值是与历史数据有显著偏差的观测值
- 异常值在持久性和含义方面可以表现出不同的特征
- 异常值检测有多种方法,包括基于预测的方法和基于估计的方法
- 您可以使用数据插补技术替换不需要的异常值