前言
最近做了一个时序数据的异常检测和预测项目,总结了几种常见的方法,针对的数据类型主要有两种,有周期特征和无周期特征的两种数据。对于没有周期特征的数据,可以直接使用基于统计学的3sigma方法,针对有周期特征的数据可以使用SARIMA、Prophet、XGBoost方法。下面针对每种方法进行逐个讲解,讲述一下如何运用以及效果展示。
一、基于统计学的3sigma方法
3 Sigma方法,又称为3倍标准差法,是一种基于统计学的质量控制方法。它是在正态分布假设下,通过统计数据的变异程度来判断数据是否合格的一种方法。在3 Sigma方法中,以平均值为中心,向两侧范围扩展3个标准差的距离,形成了一个带状区间。如果样本数据点落在这个带状区间内,就认为数据正常;如果落在带状区间外部,就认为数据异常。
下面是一个案例,用来检测时序数据的异常值:
import pandas as pd
import matplotlib.pyplot as plt
# 读取数据
df = pd.read_csv('data.csv', encoding='gb2312') # 这里我的字段名是汉字,所以用了encoding='gb2312'
# 将日期转换为Pandas日期格式
df['时间'] = pd.to_datetime(df['时间'])
# 设置日期为索引
df.set_index('时间', inplace=True)
plt.figure(figsize=(10, 4), dpi=150) # 设置画板的大小和分辨率
columns = ['字段1', '字段2', '字段3', '字段4', '字段5']
for column in columns: # 对每个字段逐个进行异常值检测
# 计算标准差和平均值
std = df[column].std()
mean = df[column].mean()
# 定义异常数据的条件
condition = (df[column] > mean + 3 * std) | (df[column] < mean - 3 * std)
# 筛选出异常数据的行
anomalies = df.loc[condition][column]
# 绘制时序图
plt.plot(df.index, df[column], label='Normal value')
# 标记异常点
plt.scatter(anomalies.index, anomalies, color='red', label='Outliers')
# 添加标题和标签
plt.title('Sequence Diagram of ' + column)
plt.xlabel('Date')
plt.ylabel('Value')
# 图像保存
plt.legend(loc='best')
plt.savefig('results/' + column + '.png', bbox_inches='tight', pad_inches=0.1)
plt.clf() # 清空画板
异常检测效果如下:
可以看出,3sigma对于异常值检测效果还是不错的,但是如果有周期,比如上面这张图,那个大大的黑色的点按理来说也是异常值,但是3sigma是对整体数据设置一个阈值上下限,这就没办法检测出这个异常值了,所以下面看一下能处理具有周期特征的方法。
二、基于SARIMA的时序数据异常检测和预测
SARIMA是一种时间序列分析模型,也称为季节性自回归滑动平均模型(Seasonal Autoregressive Integrated Moving Average model)。它是ARIMA模型的扩展,用于处理具有明显季节性周期的时间序列数据,比如月度、季度、年度数据。
SARIMA模型分为三部分,包括:季节性差分、非季节性差分和ARMA模型。其中季节性差分和非季节性差分用于使时间序列数据平稳化,ARMA模型则用于描述时间序列数据的自相关和移动平均性质。
SARIMA模型的参数比较复杂,包括p、d、q和P、D、Q、s。其中,p、d、q表示非季节性差分的阶数、自回归项的阶数和移动平均项的阶数;P、D、Q表示季节性差分的阶数、自回归项的阶数和移动平均项的阶数;s表示季节周期的长度。
SARIMA模型可以用于时间序列数据的预测和异常检测,对于需要考虑季节性周期因素的数据分析具有很好的效果。
下面讲述一下利用SARIMA进行建模的过程,先给出代码,然后进行分析:
import pandas as pd
from statsmodels.tsa.seasonal import seasonal_decompose
from statsmodels.tsa.stattools import adfuller
import matplotlib.pyplot as plt
from statsmodels.stats.diagnostic import acorr_ljungbox
from statsmodels.tsa.statespace.sarimax import SARIMAX
import tqdm
import itertools
import statsmodels.tsa.stattools as ts
import numpy as np
import warnings
warnings.filterwarnings('ignore')
# 加载数据
df = pd.read_csv('data.csv', encoding='gb2312', index_col=['时间'], parse_dates=['时间'])
# 参数设置
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei'] # 定义画图的时候使其正常显示中文字体黑体
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示表示负号
columns =['字段1', '字段2', '字段3', '字段4', '字段5']
for column in columns:
data = df[column] # 读取列数据并去掉空值
# 画出时序图
plt.figure(figsize=[15, 7], dpi=300)
data.plot()
plt.show()
p_pingwen = adfuller(data)[1] # 检验平稳性,p值小于0.05说明是平稳的
p_baizaosheng = acorr_ljungbox(data, lags=1)['lb_pvalue'].values[0] # # 检验是否为白噪声序列,p值小于0.05说明是非白噪声的
# 对数据进行差分后得到 自相关图和 偏相关图
diff = False # 如果数据平稳的话就不需要进行差分了
if diff:
D_data = data.diff().dropna()
D_data.columns = [column]
D_data.plot() # 画出差分后的时序图
plt.show()
p_pingwen = adfuller(D_data)[1]
p_baizaosheng = acorr_ljungbox(D_data, lags=1)['lb_pvalue'].values[0]
# 季节性分解
decomposition = seasonal_decompose(data, period=24) # 以一天为周期,以小时为粒度,所以周期是24
trend = decomposition.trend
seasonal = decomposition.seasonal
residual = decomposition.resid
plt.figure(figsize=[15, 7])
decomposition.plot()
print("test: p={}".format(ts.adfuller(seasonal)[1]))
plt.show()
# 对模型进行定阶
def optimize_ARIMA(order_list, exog):
results = []
for order in tqdm.tqdm(order_list):
try:
model = SARIMAX(exog, order=order).fit(disp=-1)
except:
continue
aic = model.aic
results.append([order, aic])
result_df = pd.DataFrame(results)
result_df.columns = ['(p, d, q)', 'AIC']
# Sort in ascending order, lower AIC is better
result_df = result_df.sort_values(by='AIC', ascending=True).reset_index(drop=True)
return result_df
ps = range(0, 8, 1) # 我们将尝试所有阶数 (p,q) 从 0 到 8 的组合,但保持差分阶数等于 1。
qs = range(0, 8, 1)
parameters = itertools.product(ps, qs) # Create a list with all possible combination of parameters
parameters_list = list(parameters)
order_list = []
for each in parameters_list:
each = list(each)
each.insert(1, 1) # 元组中第二个1是指差分的阶数,一般都是1,差分太多了就会导致过差分
each = tuple(each)
order_list.append(each)
result_df = optimize_ARIMA(order_list, exog=data) # 得到的AIC最小的参数就是模型的最佳参数
# 通过网格搜索对seasonal_order进行定阶
def get_ARIMA_params(data, pdq, m=24):
p = d = q = range(0, 2)
seasonal_pdq = [(x[0], x[1], x[2], m) for x in list(itertools.product(p, d, q))]
score_aic = 1000000.0
warnings.filterwarnings("ignore") # specify to ignore warning messages
for param_seasonal in tqdm.tqdm(seasonal_pdq):
mod = SARIMAX(data,
order=pdq,
seasonal_order=param_seasonal,
enforce_stationarity=False,
enforce_invertibility=False)
results = mod.fit()
print('x{}12 - AIC:{}'.format(param_seasonal, results.aic))
if results.aic < score_aic:
score_aic = results.aic
params = param_seasonal, score_aic
param_seasonal, aic = params
print('x{}12 - AIC:{}'.format(param_seasonal, aic))
return param_seasonal, aic
order = result_df.iloc[0, :].values[0] # 这个order是ARIMA模型的参数,也就是前面result_df确定的最佳参数
result_seasonal, aic = get_ARIMA_params(data, order, m=24)
# 模型拟合
pred_num = 12
best_model = SARIMAX(data[:-pred_num], order=result_df.iloc[0, :].values[0], seasonal_order=result_seasonal).fit() # 留出最后pred_num条数据不进行拟合,这是因为后面要对这pred_num条数据进行预测
# 模型拟合效果检查
print(best_model.summary().tables[1])
best_model.plot_diagnostics(figsize=(15, 12))
plt.show()
# 对拟合值的阈值上下限和真实值作图,并计算RMSE值作为评估参数
predict_ts = best_model.predict(tpy='levels', start=len(data) - 500, end=len(data)-pred_num)[1:] # 由于数据太长,这里只拟合最后500条数据的时序图;留出pred_num条数据,这是因为我们后面要进行预测,
myts = data[-500:-pred_num]
predict_ts.index = myts.index
predict_lower = best_model.get_prediction(tpy='levels', start=len(data)-500, end=len(data)-pred_num).conf_int()[1:]['lower ' + column] # 这里得到拟合值的阈值下限
predict_upper = best_model.get_prediction(tpy='levels', start=len(data)-500, end=len(data)-pred_num).conf_int()[1:]['upper ' + column]
predict_lower.index = myts.index
predict_upper.index = myts.index
outliers = myts[(myts < predict_lower) | (myts > predict_upper)] # 找出异常值
outliers.plot(color='red', label='异常值', figsize=(12, 8), marker='o', linestyle='', linewidth=2)
# predict_ts.plot(color='blue', label='拟合值', figsize=(12, 8), linestyle='-', linewidth=2)
myts.plot(color='blue', label='真实值', figsize=(12, 8), linestyle='-', linewidth=2)
predict_lower.plot(color='green', label='阈值下限', figsize=(12, 8), linestyle='--', linewidth=2)
predict_upper.plot(color='green', label='阈值上限', figsize=(12, 8), linestyle='--', linewidth=2)
plt.legend(loc='best')
plt.title(column + '拟合原始数据平均误差为: %.4f' % np.sqrt(sum((predict_ts - myts) ** 2) / myts.size))
plt.savefig('model_fitting_effect/' + column + '.png', bbox_inches='tight', pad_inches=0.1) # 放在model_fitting_effect文件下
plt.clf()
# 向后做pred_num个步长的预测
forecast_ts = best_model.forecast(pred_num)
myts_fore = data[-pred_num:]
forecast_lower = best_model.get_forecast(pred_num, alpha=0.05).conf_int()['lower '+column]
forecast_upper = best_model.get_forecast(pred_num, alpha=0.05).conf_int()['upper '+column]
forecast_ts.index, forecast_lower.index, forecast_upper.index = myts_fore.index, myts_fore.index, myts_fore.index # 索引对齐
outliers = myts_fore[(myts_fore < forecast_lower) | (myts_fore > forecast_upper)]
outliers.plot(color='red', label='异常值', figsize=(12, 8), marker='o', linestyle='', linewidth=2)
forecast_ts.plot(color='blue', label='预测值', figsize=(12, 8), linestyle='-', linewidth=2)
myts_fore.plot(color='green', label='真实值', figsize=(12, 8), linestyle='-', linewidth=2)
forecast_lower.plot(color='black', label='阈值下限', figsize=(12, 8), linestyle='--', linewidth=2)
forecast_upper.plot(color='black', label='阈值上限', figsize=(12, 8), linestyle='--', linewidth=2)
plt.legend(loc='best')
plt.title(column + '预测' + str(pred_num) + '期数据平均误差为: %.4f' % np.sqrt(sum((forecast_ts - myts_fore) ** 2) / myts_fore.size))
plt.savefig('model_forecast_effect/' + column + '.png', bbox_inches='tight', pad_inches=0.1)
plt.clf()
- 画出时序图看一下序列的特征,有没有周期和趋势的特征,对序列有一个大致的了解。然后就要检验序列的平稳性和纯随机性,这里我直接使用单位根检验序列平稳性,用LB检验来判断序列是否为白噪声。这里没有用自相关图和偏自相关图,是因为这两个图看起来是有门槛的,并不适合所有人。如果不是平稳的需要进行差分,一般差分后就平稳了。如果序列通过了平稳性和纯随机性的检验,就可以继续分析了,如果没通过那就无法进行建模了。其实也好理解,如果一个序列完全没有规律,就是完全随机产生的,这是没办法建模的。
- 然后进行季节性分解,这张图有四个子图,分别代表原始序列时序图、趋势图、周期特征图、残差图。通过周期特征图可以明显看出数据具有周期特征。
- 前面的预分析阶段结束了,下面就是建模阶段了。首先需要对SARIMA模型非季节性差分的阶数的进行定阶,这里直接用网格搜索的方法,利用计算机计算速度快的特点,一个一个的试,直接找到模型拟合效果最好的参数。这里也没有选择根据自相关图和偏自相关图进行定阶,除了看图具有门槛,而且还具有一定的主观性。然后对季节性差分的阶数的进行定阶,同样通过网格搜索的方法进行定阶,这里可能速度比较慢,尤其是周期大的时候,速度更是慢的不行。
- 参数定下来之后,接下来就是模型的拟合,这里没啥好说的,直接一键拟合。
- 下面分析模型的拟合效果,下面第一张图是模型的参数检验,coef 列显示每个特征的权重(即重要性)以及每个特征如何影响时间序列。而P>|z|列是对每个特征重要性的检验,每个权重的 p 值都低于0.05,因此在我们的模型中保留所有权重是合理的。第二张图有四个子图,分别表示:模型的残差是不相关的且符合正态分布,而且近似白噪声序列,所以模型对序列的信息已经提取完了;红色的KDE线紧跟N(0,1)线,其中N(0,1)是均值0和标准偏差为1的正态分布的标准表示法。这很好地表明了残差是正态分布的;残差(蓝色点)的有序分布遵循从具有N(0,1)的标准正态分布获取的样本的线性趋势。同样,这也表明残差是正态分布的;该图表明时间序列残差与自身的滞后值具有较低的相关性。
- 当我们训练好模型之后,并且模型拟合效果还不错,这时候我们就要进行时序预测了。这里我做了两种预测,一个是对历史数据的预测(图中用拟合值表示),一个是对未来数据的预测。在预测的同时得到一个置信区间,如果真实值不在这个区间内就为异常值,反之为正常值。下图分别是对历史数据的预测和对未来数据的预测效果图,为了展示效果,分别用了两个不同的字段,并预测未来48小时的数据。
三、基于Prophet的时序数据异常检测和预测
Prophet是Facebook开发的一种用于时间序列预测的开源库。它基于加法模型(additive model)和分解(decomposition)方法,可以对具有季节性、趋势性和节假日效应的时间序列数据进行准确的预测。Prophet库提供了简单而强大的API,使得时间序列的建模和预测变得非常方便。以下是安装Prophet库的一般步骤:
# 安装pystan
conda install pystan
# 安装prophet
sudo pip install fbprophet
老样子,先把代码贴出来,下面再详细讲解:
from fbprophet import Prophet
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# 参数设置
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei'] # 定义使其正常显示中文字体黑体
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示表示负号
datas = pd.read_csv('data.csv', encoding='gb2312')
columns = ['时间', '字段1', '字段2']
for idx, column in enumerate(columns):
# 读取数据
datadf = pd.DataFrame(columns=['ds', 'y']) # 初始化一个列名为ds和y的dataframe变量,这是因为模型默认数据的列名都是ds和y
data, datatime = datas[column], datas['时间']
datadf['ds'], datadf['y'] = pd.to_datetime(datatime), data # 读取数据
# 初始化一个模型
np.random.seed(1234) ## 设置随机数种子
pred_num = 48
confidence_interval = 0.95
model = Prophet(growth="linear", daily_seasonality=True,
weekly_seasonality=False,
seasonality_mode='multiplicative',
interval_width=confidence_interval, # 获取95%的置信区间
)
# 训练模型
model = model.fit(datadf) # 使用数据拟合模型 [:-pred_num]
# 预测未来数据和预测历史数据,prophet一步完成对历史数据和未来数据的预测
future = model.make_future_dataframe(periods=pred_num, freq='H') # periods 表示需要预测的步数,freq 表示时间序列的频率。
forecast = model.predict(future) # 使用模型对数据进行预测,interval_width默认为0.8,即80%的置信区间
forecast["y"] = datadf["y"].reset_index(drop=True)
# 根据模型预测值的置信区间"yhat_lower"和"yhat_upper"判断样本是否为异常值
def outlier_detection(forecast):
index = np.where((forecast["y"] <= forecast["yhat_lower"]) |
(forecast["y"] >= forecast["yhat_upper"]), True, False)
return index
outlier_index = outlier_detection(forecast)
outlier_df = datadf[outlier_index]
# 可视化异常值的结果
fig, ax = plt.subplots()
forecast[-pred_num:].plot(x="ds", y="yhat", style="b--", figsize=(18, 8), label="预测值", ax=ax, linewidth=2)
forecast[:-pred_num].plot(x="ds", y="yhat", style="b-", figsize=(18, 8), label="拟合值", ax=ax, linewidth=2)
ax.fill_between(forecast["ds"].values, forecast["yhat_lower"], forecast["yhat_upper"], color='green', alpha=.2, label=str(int(confidence_interval*100))+"%置信区间") ## 可视化出置信区间,alpha=.2表示透明度为0.2
forecast.plot(kind="scatter", x="ds", y="y", c="k", s=20, label="原始数据", ax=ax) # 原始数据散点图表示
outlier_df.plot(x="ds", y="y", style="ro", ax=ax, label="异常值") ## 可视化出异常值的点,"ro"表示红色圆点,‘rs’表示红色方块
plt.legend(loc='best')
plt.grid()
rmse = np.sqrt(sum(((forecast["y"][-pred_num:] - forecast["yhat"][-pred_num:]).dropna()) ** 2) / pred_num)
plt.title(column + '_模型拟合效果图_异常值检测效果图_预测未来' + str(int(pred_num)) + '小时数据效果图_预测均方根误差为: %.4f' % rmse)
plt.savefig('Prophet/Hour/model_effect/' + column + '.png', bbox_inches='tight', pad_inches=0.1, dpi=300)
plt.clf()
- 在建模之前对数据做一个预分析,比如画出时序图看看,检验一下平稳性和纯随机性,心里有一定的了解,下面继续建模
- prophet建模还是很方便的,直接调包,先初始化一个模型,这里解释一下常用的参数,
growth:指定趋势函数的类型。默认为’linear’(线性),也可以设置为’logistic’(逻辑斯蒂)
daily_seasonality:用于控制每日季节性的建模方式。它决定了是否考虑每天的季节性效应。当然,可以根据数据的季节性特征自行设定
changepoints:变化点列表,用于检测时间序列中的趋势突变。可以手动指定一组时间点,也可以使用自动检测算法
seasonality_mode:指定季节性模式的类型。默认为’additive’(加法模式),也可以设置为’multiplicative’(乘法模式)
seasonality_prior_scale:控制季节性项的平滑程度。较大的值表示较强的季节性效应,较小的值表示较弱的季节性效应
holidays:节假日效应列表。可以传递一个包含节假日日期和名称的DataFrame,以考虑节假日对模型的影响
changepoint_prior_scale:控制变化点的灵活性。较大的值表示更灵活的模型,较小的值表示更严格的模型
interval_width:预测区间的宽度,用于计算置信区间。默认为0.8,表示80%的置信区间 - 然后就是训模型并进行预测,如果不想预测未来数据,可以将future = model.make_future_dataframe(periods=pred_num, freq=‘H’) 中的periods设置为0
- 找出异常点,将结果进行可视化并保存,下面分别给出了分钟粒度和小时粒度的可视化结果,具体时间粒度根据自己的数据特征设置:
为了少贴一些图,我将多个信息都展示在一张图上了,具体可以根据自己的需求来设置。
四、基于XGBoost的时序数据异常检测和预测
XGBoost(eXtreme Gradient Boosting)是一种基于梯度提升树的机器学习算法,它通过迭代训练多个决策树模型来逐步提升整体模型的性能。具有梯度提升的优势,XGBoost在处理分类和回归问题时表现出色,并且具备高效性、灵活性和可解释性等特点,被广泛应用于数据挖掘、预测建模和特征选择等领域。
下面贴出代码:
import xgboost as xgb
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# 导入数据
df = pd.read_csv('data.csv', encoding='gb2312', index_col=['时间'], parse_dates=['时间'])
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei'] # 定义使其正常显示中文字体黑体
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示表示负号
# 按时间顺序排序
df.sort_index(inplace=True)
# 将时间序列按时间窗口切割成多个样本
def create_dataset(dataset, look_back=1):
dataX, dataY = [], []
for i in range(len(dataset)-look_back-1):
a = dataset[i:(i+look_back)]
dataX.append(a)
dataY.append(dataset[i + look_back])
return np.array(dataX), np.array(dataY)
columns = ['时间', '字段1', '字段2']
for column in columns:
# 数据预处理
dataset = df[column] # 如果有空值需要先进行填充或者删除
train_size = int(len(dataset) * 0.7) # 划分训练集和测试集
test_size = len(dataset) - train_size
train, test = dataset[0:train_size], dataset[train_size:len(dataset)]
# 创建数据集
look_back = 3
trainX, trainY = create_dataset(train, look_back)
testX, testY = create_dataset(test, look_back)
# 定义模型
model = xgb.XGBRegressor()
# 拟合模型并预测
model.fit(trainX, trainY)
trainPredict = model.predict(trainX) # 拟合值
testPredict = model.predict(testX) # 预测值
# 将得到的数据存入dataframe中
true_value = pd.DataFrame(columns=['x', 'y'])
true_value_x = np.concatenate((train[look_back:-1].index, test[look_back:-1].index), axis=-1)
true_value_y = np.concatenate((trainY, testY), axis=-1)
true_value['x'] = true_value_x
true_value['y'] = true_value_y
fitting_value = pd.DataFrame(columns=['x', 'y'])
fitting_value['x'] = train[look_back:-1].index
fitting_value['y'] = trainPredict
pred_value = pd.DataFrame(columns=['x', 'y'])
pred_value['x'] = test[look_back:-1].index
pred_value['y'] = testPredict
a = pred_value['y']
b = true_value['y'][-len(testX):]
b.index = a.index
c = np.sqrt(sum(((a - b).dropna()) ** 2) / len(testX))
print(column + ' 小时数据效果图_预测均方根误差为: %.4f' % c)
上面就是利用XGBoost的建模过程了,只是输出了模型的评估效果,具体的效果展示与前面介绍的方法一模一样,把前面随便哪一个方法的代码搞懂,XGBoost效果展示也很容易实现了。
从最后输出的评估结果来看,XGBoost取得了最好的预测效果,不得不说,XGBoost是真的牛。
总结
以上就是全部内容了,如有问题,欢迎评论区一起交流。