数据分析学习总结笔记15:时间序列分析及Python实现

1 引言

本篇主要帮助大家理解移动平均,指数平滑,平稳性,自相关,SARIMA,通过案例和Python编程实现时间序列的预测技术。

无论我们是预测金融市场或股票趋势,或是电能耗费的趋势,时间都是我们模型中必须考虑的一个重要因素。例如,预测一天中什么时间会出现的电能消耗高峰,并以此来调整价格或发电量。

时间序列简单来说就是按时间顺序排列的数据点。在时间序列中,时间通常是独立变量,目标则是对未来进行预测。

当然,在处理时间序列时,其他许多因素同样会有影响。

  • 时间序列是平稳的吗?
  • 时间序列具有季节性吗?
  • 目标变量是自相关的吗?

在本文中,我们将介绍时间序列的不同特征,以及如何进行建模以获得尽可能准确的预测。当然,预测未来是十分困难的。

2 时间序列的特性

2.1 自相关

简单来说,自相关是观察值之间的相似性,并作为它们之间是时间延迟的函数。
在这里插入图片描述
上图就是一个自相关的图例。仔细观察,我们发现第1个值和第24个值具有很高的自相关性。同样,第12 个观测和第36个观测也是高度相关的。这意味着,每24个单位时间,我们就可以找到一个相似的值。

注意,这个图与正弦函数很相似,这就是对序列季节性的暗示,在上图中,我们可以按照24个单位时间一个周期,找到相似的值。

2.2 季节性

季节性指的是周期性的波动。例如,白天用电量高,晚上低,或是网购量在圣诞节期间增加,然后再次放缓。
在这里插入图片描述
正如上图所示,每一天都有明显的季节性趋势。每一天,傍晚时都会出现一个高峰,而最低点则是每天的开始和结束。
如果自相关图像是正弦的,那么也可以认为其具有季节性。只要简单的看一下它的周期,就可以得到季节的长度。

2.3 平稳性

平稳性是时间序列的一个重要特征。如果一个时间序列的统计特征不随时间而改变,那么他就是平稳的。换句话说,它的均值和方差是常数,并且协方差与时间无关
在这里插入图片描述
同一张图,可以看到上面的过程是平稳的。均值和方差不随时间变化。

通常情况下,股票价格不是一个平稳过程,因为我们可能会看到增长的趋势,或者它的波动性会随着时间而增加(方差的变化)。

理想情况下,我们希望对一个平稳的时间序列建模。当然,并不是所有的时间序列都是平稳的,但我们可以通过不同的变换使它们变为平稳序列。

如何检验一个过程是否平稳

你可能已经注意到,上图标题中的迪基-福勒Dickey-Fuller。迪基-福勒检验就是用来在统计学领域检验时间序列是否平稳的。

不讨论迪基-福勒检验的技术细节,简单来说,它检验了原假设:单位根存在。

  • 如果为真,则p>0,这个过程是不平稳的。
  • 否则,p=0, 拒绝原假设,过程被认为是平稳的。

以下图为例,该过程是不平稳的,平均值随时间变化,不为常数。
在这里插入图片描述

3 时间序列建模

有许多方法可以对时间序列进行建模,并作出预测。本文,我们将介绍:

  • 移动平均法
  • 指数平滑法
  • ARIMA

3.1 移动平均法

移动平均模型可能是所有时间序列模型中最初级的方法:下一个观测值是之前所有观测值的平均值。尽管十分简单,但这个模型却出人意料的有效,也是一个很好的起点。

移动平均模型可以用于识别数据中有趣的趋势。我们可以定义一个窗口,应用移动平均模型对时间序列进行平滑处理,以突出不同的趋势。
在这里插入图片描述
在上图中,我们将移动平均应用在一个窗口为24小时的模型。图中绿线是时间序列的平滑,我们可以看到在24小时内存在两个峰值。

当然,时间窗口越长,趋势就越平稳。下图是一个小窗口的移动平均模型的案例。

在这里插入图片描述

3.2 指数平滑法

指数平滑法使用与移动平均法相似的逻辑,但在此方法中,将赋予每个观测值不同的递减权重。换句话说,随着观察值离现在的时间点越来越远,观察值的重要性就越来越小。

在数学上,指数平滑表示为:
在这里插入图片描述
α是一个平滑因子,值在0到1之间。它决定了观测值权重下降的速度。
在这里插入图片描述
从上图可以看出:

  • 深蓝线表示使用0.3平滑因子对时间序列进行指数平滑的结果;
  • 橙线则是使用0.05的指数平滑结果。
  • 正如我们看到的,平滑因子越小,结果就越平滑。这很容易理解,因为当平滑因子接近0时,模型就越接近移动平均模型。

3.3 双指数平滑法

当时间序列中存在趋势时,我们可以采用双指数平滑法。这种方法简单来说,是指数平滑法的递归使用

数学上:
在这里插入图片描述
上式中,β是趋势平滑因子,值在0-1之间。

预测公式为:
在这里插入图片描述
下图中,我们可以看到不同值如何影响处理后时间序列的形状。
在这里插入图片描述

3.4 三重指数平滑法

此方法通过增加季节平滑因子,扩展了双指数平滑法。因此,当你发现时间序列具有季节性时,此方法就十分有效。

数学上:
在这里插入图片描述
其中 γ 是季节平滑因子,L是季节的长度。

3.5 周期性差分自动平滑回归模型(SARIMA)

SARIMA模型实际上是一些简单模型的组合,并组成复杂的模型,此模型可以对非平稳的具有季节性的时间序列进行建模。

我们先来看自回归模型AR§,它可以说是对时间序列自身的回归。我们假设当前值依赖于有些许滞后的先前值,并使用参数p表示最大滞后值。为了找到它,我们可以观察偏自相关图像,找到滞后值开始不显著的点,即最大滞后值。

下图的例子中,该p值为4。
在这里插入图片描述
接下来,我们加入移动平均模型MA(q)。参数q代表最大滞后值,在此后的其他滞后在相关性图中不显著。下图中,q值为4。
在这里插入图片描述
之后,我们再加入单整阶数I(d)。参数d表示是序列平稳所需进行的差分次数(阶数)。

现在,让我们添加最后一个组成部分:季节性s(P, D, Q, s),其中s为季节的长度,参数P和Q是与p和q相似的季节性部分。最后,D是季节性单整阶数,代表从序列中去除季节性所需的差分次数。

综合这些,我们得到了SARIMA(p, d, q)(P,D, Q, s)模型。

主要结论:在使用SARIMA建模前,我们必须对时间序列进行变换,以消除季节性,并使序列平稳。

这一大堆理论可能让我们难以理解,下面我们将在项目中应用上面所讨论的技术,
我们将预测某一公司的股票价格。当然,预测股票价格在现在是几乎不可能的,但是这仍然是个有趣的联系,也是一个很好实践所学的方式。

4 实例——股票价格的预测

我们利用新德国基金(GF)的历史股价来预测未来五个交易日的收盘价。
我们可以在https://github.com/marcopeix/stock-prediction找到数据集和笔记本。

启动您的编辑器,让我们开始吧。

(1)导入数据

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
sns.set()

from sklearn.metrics import r2_score, median_absolute_error, mean_absolute_error
from sklearn.metrics import median_absolute_error, mean_squared_error, mean_squared_log_error

from scipy.optimize import minimize
import statsmodels.tsa.api as smt
import statsmodels.api as sm

from tqdm import tqdm_notebook

from itertools import product

def mean_absolute_percentage_error(y_true, y_pred):
    return np.mean(np.abs((y_true - y_pred) / y_true)) * 100

import warnings
warnings.filterwarnings('ignore')

%matplotlib inline

DATAPATH = 'data/stock_prices_sample.csv'

data = pd.read_csv(DATAPATH, index_col=['DATE'], parse_dates=['DATE'])
data.head(10)

首先,我们导入一些在分析过程中可能会用到的Python包。此外,定义了平均百分比误差(MAPE),这将是误差的评判标准。之后,我们导入数据集并预览了前十条数据:
在这里插入图片描述
我们可以发现,有一些条目并不属于新德国基金,并且有些条目为交易日内信息,并非我们所需的收盘信息。因此我们需要对数据进行一定的清理。

(2)数据清理
我们首先删除不符合要求的条目,然后删除不需要的列,只留下我们关注的股票收盘价。

data = data[data.TICKER != 'GEF']
data = data[data.TYPE != 'Intraday']

drop_cols = ['SPLIT_RATIO', 'EX_DIVIDEND', 'ADJ_FACTOR', 'ADJ_VOLUME', 'ADJ_CLOSE', 'ADJ_LOW', 'ADJ_HIGH', 'ADJ_OPEN', 'VOLUME', 'FREQUENCY', 'TYPE', 'FIGI']

data.drop(drop_cols, axis=1, inplace=True)

data.head()

预览处理后的数据集,之后我们就可以进行下一步的探索性数据分析了。
在这里插入图片描述
(3)探索性数据分析(EDA)

# Plot closing price

plt.figure(figsize=(17, 8))
plt.plot(data.CLOSE)
plt.title('Closing price of New Germany Fund Inc (GF)')
plt.ylabel('Closing price ($)')
plt.xlabel('Trading day')
plt.grid(False)
plt.show()

我们绘制了数据集中,所有时间段内的收盘价,可以得到:
在这里插入图片描述
很明显,这不是一个平稳的时间序列,也很难判断它是否具有季节性。

(4)移动平均

我们使用移动平均模型来使我们的时间序列平滑化。为此,我们会使用一个辅助函数,他会在指定的时间窗运行移动平均模型,并绘制结果得到的平滑曲线:

def plot_moving_average(series, window, plot_intervals=False, scale=1.96):

    rolling_mean = series.rolling(window=window).mean()

    plt.figure(figsize=(17,8))
    plt.title('Moving average\n window size = {}'.format(window))
    plt.plot(rolling_mean, 'g', label='Rolling mean trend')

    #Plot confidence intervals for smoothed values
    if plot_intervals:
        mae = mean_absolute_error(series[window:], rolling_mean[window:])
        deviation = np.std(series[window:] - rolling_mean[window:])
        lower_bound = rolling_mean - (mae + scale * deviation)
        upper_bound = rolling_mean + (mae + scale * deviation)
        plt.plot(upper_bound, 'r--', label='Upper bound / Lower bound')
        plt.plot(lower_bound, 'r--')

    plt.plot(series[window:], label='Actual values')
    plt.legend(loc='best')
    plt.grid(True)

#Smooth by the previous 5 days (by week)
plot_moving_average(data.CLOSE, 5)

#Smooth by the previous month (30 days)
plot_moving_average(data.CLOSE, 30)

#Smooth by previous quarter (90 days)
plot_moving_average(data.CLOSE, 90, plot_intervals=True)

使用时间窗为5天时,我们可以得到:
在这里插入图片描述
可以看到,时间窗为5时,它太接近实际曲线了,我们几乎看不到任何趋势。那么,使用前一个月或前一个季度时间窗的平滑结果是怎样的呢?
在这里插入图片描述
在这里插入图片描述
这时候,我们可以看出,这两种时间窗所得到的平滑结果,在结束时是呈下降趋势的。这可能意味着股价在未来几天有可能下跌。

(5)指数平滑法

现在,我们使用指数平滑法来看看它是否会得到更好的趋势。

def exponential_smoothing(series, alpha):

    result = [series[0]]    #first value is same as series
    for n in range(1, len(series)):
        result.append(alpha * series[n] + (1 - alpha) * result[n-1])
    return result


def plot_exponential_smoothing(series, alphas):

    plt.figure(figsize=(17, 8))
    for alpha in alphas:
        plt.plot(exponential_smoothing(series, alpha), label = 'Alpha {}'.format(alpha))
    plt.plot(series.values, "c", label="Actual")
    plt.legend(loc="best")
    plt.axis('tight')
    plt.title("Exponential Smoothing")
    plt.grid(True)


plot_exponential_smoothing(data['CLOSE'], [0.05, 0.3])
plt.show()

我们使用0.05和0.3作为平滑因子的值。当然,也可以尝试其他值,看看结果如何。
在这里插入图片描述
(6)Holt指数平滑法

def double_exponential_smoothing(series, alpha, beta):

    result = [series[0]]
    for n in range(1, len(series)+1):
        if n == 1:
            level, trend = series[0], series[1] - series[0]
        if n >= len(series): # forecasting
            value = result[-1]
        else:
            value = series[n]
        last_level, level = level, alpha * value + (1 - alpha) * (level + trend)
        trend = beta * (level - last_level) + (1 - beta) * trend
        result.append(level + trend)
    return result

def plot_double_exponential_smoothing(series, alphas, betas):

    plt.figure(figsize=(17, 8))
    for alpha in alphas:
        for beta in betas:
            plt.plot(double_exponential_smoothing(series, alpha, beta), label="Alpha {}, beta {}".format(alpha, beta))
    plt.plot(series.values, label = "Actual")
    plt.legend(loc="best")
    plt.axis('tight')
    plt.title("Double Exponential Smoothing")
    plt.grid(True)

plot_double_exponential_smoothing(data.CLOSE, alphas=[0.9, 0.02], betas=[0.9, 0.02])

我们可以得到:
在这里插入图片描述
同样的,我们也尝试了不同的α和β组合,以获取最好的平滑曲线。

(7)建模

正如本文之前所说,为了对时间序列进行建模,必须将它处理为一个平稳过程。因此,我们使用Dickey-Fuller测试来检验它是否为平稳过程。

def tsplot(y, lags = None, figsize=(12,7), style='bmh'):
    if not isinstance(y, pd.Series):
        y = pd.Series(y)

    with plt.style.context(style='bmh'):
        fig = plt.figure(figsize=figsize)
        layout = (2,2)
        ts_ax = plt.subplot2grid(layout, (0,0), colspan=2)
        acf_ax = plt.subplot2grid(layout, (1,0), colspan=1)
        pacf_ax = plt.subplot2grid(layout, (1,1), colspan=1)

        y.plot(ax=ts_ax)
        p_value = sm.tsa.stattools.adfuller(y)[1]
        ts_ax.set_title('Time Series Analysis Plots\n Dickey-Fuller: p={0:.5f}'.format(p_value))
        smt.graphics.plot_acf(y, lags=lags, ax = acf_ax)
        smt.graphics.plot_pacf(y, lags=lags, ax = pacf_ax)
        plt.tight_layout()


tsplot(data['CLOSE'], lags=30)
plt.show()

得到以下图像:
在这里插入图片描述
通过Dicky-Fuller检验,p值大于0.05,时间序列明显为非平稳的。此外,相关图中并未显示出明显的季节性。

因此,为了消除高自相关性并使过程变为平稳,我们做一次差分,即简单的从时间序列本身减去一天的滞后:

# Take the first difference to remove to make the process stationary
data_diff = data['CLOSE'] - data['CLOSE'].shift(1)

tsplot(data_diff[1:], lags=30)
plt.show()

在这里插入图片描述
很明显,现在的时间序列是平稳的,我们就可以对它进行建模。

(8)SARIMA

# Set initial values and some bounds
ps = range(0, 5)
d = 1
qs = range(0, 5)
Ps = range(0, 5)
D = 1
Qs = range(0, 5)
s = 5

# Create a list with all possible combinations of parameters
parameters = product(ps, qs, Ps, Qs)
parameters_list = list(parameters)
print(len(parameters_list))


# Train many SARIMA models to find the best set of parameters
def optimize_SARIMA(parameters_list, d, D, s):
    """
        Return dataframe with parameters and corresponding AIC

        parameters_list - list with (p, q, P, Q) tuples
        d - integration order
        D - seasonal integration order
        s - length of season
    """

    results = []
    best_aic = float('inf')

    for param in parameters_list:
        try:
            model = sm.tsa.statespace.SARIMAX(data.CLOSE, order=(param[0], d, param[1]),
                                              seasonal_order=(param[2], D, param[3], s)).fit(disp=-1)
        except:
            continue

        aic = model.aic

        results.append([param, aic])

    result_table = pd.DataFrame(results)
    result_table.columns = ['parameters', 'aic']
    # Sort in ascending order, lower AIC is better
    result_table = result_table.sort_values(by='aic', ascending=True).reset_index(drop=True)

    return result_table


result_table = optimize_SARIMA(parameters_list, d, D, s)

# Set parameters that give the lowest AIC (Akaike Information Criteria)
p, q, P, Q = result_table.parameters[0]

best_model = sm.tsa.statespace.SARIMAX(data.CLOSE, order=(p, d, q),
                                       seasonal_order=(P, D, Q, s)).fit(disp=-1)

print(best_model.summary())

对于SARIMA来说,需要首先定义一些参数及值的范围,以生成一个包好p, q, d, P, Q, D, s所有可能的组合列表。

在上面的代码中,我们得到了625种不同组合。通过尝试用每个组合训练SARIMA,涨到性能最佳的模型。因为组合较多,训练时间将取决于您计算机的处理能力,或许需要一段时间。

训练完成后,打印出最佳模型:
在这里插入图片描述
运用这个最佳模型对未来五个交易日的收盘价进行预测,得到MAPE为0.79%,可以认为预测十分贴近真实。

(9)将预测的价格与实际数据进行比较

为了将预测与实际数据进行比较,我们从雅虎财经获取了实际数据并创建了一个数据模型。

# Make a dataframe containing actual and predicted prices
comparison = pd.DataFrame({'actual': [18.93, 19.23, 19.08, 19.17, 19.11, 19.12],
                          'predicted': [18.96, 18.97, 18.96, 18.92, 18.94, 18.92]}, 
                          index = pd.date_range(start='2018-06-05', periods=6,))


#Plot predicted vs actual price

plt.figure(figsize=(17, 8))
plt.plot(comparison.actual)
plt.plot(comparison.predicted)
plt.title('Predicted closing price of New Germany Fund Inc (GF)')
plt.ylabel('Closing price ($)')
plt.xlabel('Trading day')
plt.legend(loc='best')
plt.grid(False)
plt.show()

制作一张图表,展示预测与实际收盘价的差距:
在这里插入图片描述
这样看来,我们的预测有不少偏差。实际上,预测得到的价格基本为水平的,也意味着模型可能并不有效。

当然,这不一定源于模型的问题,同样,股票价格的预测基本是不可能的。

5 结论

这篇文章很长,但是内容丰富,我们学习了运用SARIMA进行时间序列分析,以及使时间序列平稳的整个过程。

这是一个冗长乏味的过程,需要大量的手工的调整。希望这篇文章对你有用,在以后遇到类似问题时,可以再次想到并进行参考。

讨论
考虑到我们的预测结果并不尽如人意,其可能原因有许多,但是我们可以想到,或许简单模型或传统统计模型,已经不能对如今复杂的数据进行有效预测。
可以进一步思考是否可以通过其他模型,使我们对股票的预测更加准确?

本文主要参考于:
知识分享 | 时间序列分析和预测不完全指南(沈浩老师)

相关笔记:

  1. Python相关实用技巧01:安装Python库超实用方法,轻松告别失败!
  2. Python相关实用技巧02:Python2和Python3的区别
  3. Python相关实用技巧03:14个对数据科学最有用的Python库
  4. Python相关实用技巧04:网络爬虫之Scrapy框架及案例分析
  5. Python相关实用技巧05:yield关键字的使用
  6. Scrapy爬虫小技巧01:轻松获取cookies
  7. Scrapy爬虫小技巧02:HTTP status code is not handled or not allowed的解决方法
  8. 数据分析学习总结笔记01:情感分析
  9. 数据分析学习总结笔记02:聚类分析及其R语言实现
  10. 数据分析学习总结笔记03:数据降维经典方法
  11. 数据分析学习总结笔记04:异常值处理
  12. 数据分析学习总结笔记05:缺失值分析及处理
  13. 数据分析学习总结笔记06:T检验的原理和步骤
  14. 数据分析学习总结笔记07:方差分析
  15. 数据分析学习总结笔记07:回归分析概述
  16. 数据分析学习总结笔记08:数据分类典型方法及其R语言实现
  17. 数据分析学习总结笔记09:文本分析
  18. 数据分析学习总结笔记10:网络分析
  19. 数据分析学习总结笔记11:空间复杂度和时间复杂度
  20. 数据分析学习总结笔记12:空间自相关——空间位置与相近位置的指标测度
  21. 数据分析学习总结笔记13:生存分析及Python实现
  22. 数据分析学习总结笔记14:A/B Test及Python实现
发布了30 篇原创文章 · 获赞 0 · 访问量 521

猜你喜欢

转载自blog.csdn.net/weixin_41961559/article/details/105610172