时间序列预测(arima_xgboost_lstm_fbprop)-以bitcoin比特币价格为例

时间序列预测(arima_xgboost_lstm_fbprop)-以bitcoin比特币价格为例

本实验案例主要采用EDA、特征工程,并使用随机、机器学习和深度学习模型来预测比特币价格,并进行arima/xgboost/lstm/fbprop等模型的预测性能对比。

1 数据预处理与EDA分析

在第一节中,对比特币数据进行了一系列处理和分析操作,包括数据的说明、插值法处理缺失值、可视化、密度图、滞后图分析自相关性和时间重采样。通过以上处理和分析步骤,我们可以对比特币数据进行初步的理解和探索。这些步骤有助于我们发现数据的特点、趋势和相关性,为后续的进一步分析和建模提供基础和指导。

bitstamp = pd.read_csv("bitcoin-historical-data/bitstampUSD_1-min_data_2012-01-01_to_2020-09-14.csv")
bitstamp['Timestamp'] = [datetime.fromtimestamp(x) for x in bitstamp['Timestamp']]
bitstamp.set_index("Timestamp").Weighted_Price.plot(figsize=(14,7), title="Bitcoin Weighted Price")

比特币数据的初始情况如下:
1689673512788.png

这里包含的是进行交易的选定比特币交易所每 1 分钟一次的历史比特币市场数据。 包括 2012 年 1 月至 2021 年 9 月的时间段,每分钟更新 OHLC(开盘价、最高价、最低价、收盘价)、BTC 和指定货币的交易量以及加权比特币价格。

数据已转换为所需的格式,对标签说明如下:

  • 开盘价和收盘价列表(Open and Close)示特定日期的开盘价和收盘价。

  • 最高价和最低价列(High and Low)分别提供特定日期的最高价格和最低价格。

  • 交易量列(Volume)告诉我们特定日期的交易总量。

  • 加权价格(Weighted price)是交易者使用的交易基准,它根据交易量和价格给出证券全天交易的加权价格。 它很重要,因为它为交易者提供了对证券趋势和价值的洞察。 如图所示是加权价格随时间变化的趋势,初步判断加权价格具有一定的时间序列性。
    1689673822365.png

(关注gz号“finance褪黑素”回复111可获取本文全部数据和代码)

1.1 处理时间序列数据中的缺失值

检测数据集每列中缺失值的计数/百分比:
1689673876976.png

插补是指用替换值替换缺失数据。根据问题和数据的性质,可以通过多种方式插补缺失值。 根据问题的性质,插补技术可以分成基本插补技术和高级插补技术(KNN、MICE)。

基本插补技术:

  • ‘ffill’ 或 ‘pad’ - 用最后观察到的值替换NaN

  • ‘bfill’ 或 ‘backfill’ - 用下一个观察到的值替换 NaN 值

  • 线性插值法

由于时间序列数据随时间有很多变化。 因此,使用回填和前向填充进行插补并不是解决缺失值问题的最佳解决方案。 更合适的替代方法是使用插值方法,其中值用递增或递减值填充。

线性插值是一种插补技术,它假设数据点之间存在线性关系,并利用相邻数据点的非缺失值来计算缺失数据点的值。 请参阅官方文档以获取插值策略的完整列表。在我们的数据集中,我们将对缺失值列执行线性插值

插值后,最终输出中已没有空值。

1689674147779.png
def fill_missing(df):
    ### function to impute missing values using interpolation ###
    df['Open'] = df['Open'].interpolate()
    df['Close'] = df['Close'].interpolate()
    df['Weighted_Price'] = df['Weighted_Price'].interpolate()

    df['Volume_(BTC)'] = df['Volume_(BTC)'].interpolate()
    df['Volume_(Currency)'] = df['Volume_(Currency)'].interpolate()
    df['High'] = df['High'].interpolate()
    df['Low'] = df['Low'].interpolate()

    print(df.head())
    print(df.isnull().sum())

(本文只展示部分代码,获取全套免费数据+可运行代码可关注公众号“金融褪黑素”)

1.2 探索性数据分析

在处理时间序列数据时,通过可视化可以揭示很多内容。 可以在图中添加标记来帮助强调时间序列中的特定观察或特定事件。

1689680783347.png

除了 2018 年底和 2019 年初的暴跌之外,比特币的加权价格一直在上涨。此外,还可以观察到 2017 年 12 月加权价格的飙升。

使用密度图汇总数据以查看数据质量所在的位置,曲线下的面积表示该区域内数据的密度。
通过密度图,可以辨别出数据中的异常值。异常值通常表现为分布曲线中的低密度区域或离群点。
受2012~2017年平稳序列的影响,密度图的高峰主要集中在0到2500之间。
可以按2017年前和2017年后的时间段进行划分,也可以使用统计测试方法(例如ADF测试、KPSS测试)来评估时间序列的平稳性。

sns.kdeplot(bitstamp['Weighted_Price'], shade=True)

1689680872690.png
ax = bitstamp['Weighted_Price'].plot(title='Bitcoin Prices', grid=True, figsize=(14,7))
ax.set_xlabel('Year')
ax.set_ylabel('Weighted Price')

ax.axvspan('2018-12-01','2019-01-31',color='red', alpha=0.3)
ax.axhspan(17500,20000, color='green',alpha=0.3)

(关注gz号“finance褪黑素”回复111可获取本文全部数据和代码)

1.3 滞后图判断自相关性

滞后图用于观察自相关。 当我们尝试纠正趋势和平稳性并且必须使用平滑函数时,这些至关重要。 滞后图可以帮助我们更好地理解数据。

1689681281341.png

我们可以看到分钟、小时和日滞后图存在正相关关系,月滞后图绝对没有相关性。

最多在每日级别重新采样我们的数据是有意义的,从而也保留了自相关性.

plt.figure(figsize=(15,12))
plt.suptitle('Lag Plots', fontsize=22)

plt.subplot(3,3,1)
pd.plotting.lag_plot(bitstamp['Weighted_Price'], lag=1) #minute lag
plt.title('1-Minute Lag')

plt.legend()
plt.show()
1.4 时间重采样提升

时间重采样是指将时间序列数据从一个时间频率转换为另一个时间频率的过程。它可以用于多种目的,包括数据压缩、数据平滑、数据对齐和分析的需要。

金融机构更关注市场趋势和概况,而不是每天的股价数据。通过使用时间重采样的过程将数据聚合到较长的时间周期(例如按月或按季度),金融机构可以更容易地观察和分析股票价格的总体趋势和概况。

时间重采样可以提供更高层次的数据摘要,使机构能够更好地理解和解释市场的长期变化。它有助于去除短期的价格波动和噪声,使我们能够更专注于观察长期的市场趋势。

通过对数据进行重采样,金融机构可以快速识别市场的增长、下降或稳定趋势,以及市场周期性的特征。这对于制定投资策略、评估资产组合或执行风险管理非常重要。例如,一家机构可能注意到在某个特定季度股价经常上涨,而在另一个季度则经常下跌,这可能会影响他们的投资决策。检查每天的股价数据对于金融机构来说没有多大用处,金融机构更感兴趣的是发现市场趋势。

pandas 库的.resample() 函数可以对此类时间序列数据进行重新采样。resample方法和groupby方法类似,本质上是按照一定的时间跨度进行分组。

1689681381778.png
hourly_data = bitstamp.resample('1H').mean()
hourly_data = hourly_data.reset_index()

data.resample() 用于对数据重新采样。 “1H”代表每小时频率,表示我们想要对数据进行重新采样的偏移值。 mean() 表示我们想要这段时间内的平均价格。

2 时间序列分解和统计检验

时间序列分解和统计测试

plt.figure(figsize=(15,12))
series = bitstamp_daily.Weighted_Price
result = seasonal_decompose(series, model='additive',period=1)
result.plot()

根据第一部分的数据特征分析,可以将时间序列分解为趋势、季节性和剩余成分。 该序列可以分解为基准水平、趋势、季节指数和残差的加法或乘法组合。 statsmodels中的seasonal_decompose用于实现分解。然后,我们将执行一些统计测试,例如 KPSS 和增强迪基-富勒测试来检查平稳性。

1689681496883.png 1689681515918.png

时间序列分解后,没有观察到任何季节性。此外,不存在恒定的均值、方差和协方差,因此该序列是非平稳的,进行 KPSS 和 ADF 等统计测试来确认我们的理解。

首先,绘制 ACF 和 PACF 图。

1689681602445.png

上图显示,效果几乎不会随着时间的推移而减弱,因此过去的值会影响当前的值。 我们包含的滞后越多,我们的模型就越适合数据集,现在的风险是系数可能会很好地预测数据集,导致过度拟合。 在我们的模型中,我们总是尝试仅包含那些对我们的现值有直接影响的滞后。 因此,让我们尝试一下 PACF。

1689681644611.png

上图显示滞后 > 5 的系数值在统计上并不显着,它们对模型的影响很小,除了 8、11、22 及以后的一些峰值之外

2.1 KPSS检验

KPSS 检验是 Kwiatkowski-Phillips-Schmidt-Shin (KPSS) 的缩写,是一种单位根检验,用于测试给定序列围绕确定性趋势的平稳性。

这里,原假设是:序列是平稳的。

也就是说,如果 p 值 < 显着水平(例如 0.05),则该序列是非平稳的,反之亦然。

KPSS 测试的输出包含 4 个内容:

  • KPSS统计
  • p 值
  • 测试使用的滞后数
  • 临界值
    检验报告的 p 值是概率分数,您可以根据该分数决定是否拒绝原假设。 如果 p 值小于预定义的 alpha 水平(通常为 0.05),我们将拒绝原假设。

KPSS 统计量是执行测试时计算的实际测试统计量。

报告的滞后数是 kpss 检验的模型方程实际使用的系列滞后数。

为了拒绝原假设,检验统计量应大于提供的临界值。 如果它实际上高于目标临界值,那么这应该自动反映为较低的 p 值。 也就是说,如果 p 值小于 0.05,kpss 统计量将大于 5% 临界值。

stats, p, lags, critical_values = kpss(series, 'ct')

Test Statistics : 0.9719743430417129
p-value : 0.01
Critical Values : {‘10%’: 0.119, ‘5%’: 0.146, ‘2.5%’: 0.176, ‘1%’: 0.216}
Series is not Stationary

2.2 ADF检验

ADF检验与 KPSS 正好相反。检验的原假设是存在单位根,即序列是非平稳的。

1689681988878.png

KPSS 表示序列不是平稳的,ADF 表示序列是平稳的。 这意味着序列是差分平稳的,我们得使用差分来使序列平稳。

3 特征提取

滚动窗户:由于市场的剧烈波动,时间序列数据可能会很嘈杂。 因此,很难衡量数据中的趋势或模式。当我们查看每日数据时,存在相当多的噪音。 如果我们能将其平均为一周,那就太好了,这就是滚动平均值的用武之地。

滚动平均值或移动平均值是一种转换方法,有助于平均数据中的噪声。 它的工作原理是根据函数(例如mean()、median()、count()等)将数据简单地拆分和聚合到窗口中。在本案例中,我们将使用3天、7天和30天的滚动平均值。

1689682139351.png
df.reset_index(drop=False, inplace=True)

lag_features = ["Open", "High", "Low", "Close","Volume_(BTC)"]
window1 = 3
window2 = 7
window3 = 30

df_rolled_3d = df[lag_features].rolling(window=window1, min_periods=0)
df_rolled_7d = df[lag_features].rolling(window=window2, min_periods=0)
df_rolled_30d = df[lag_features].rolling(window=window3, min_periods=0)

df_mean_3d = df_rolled_3d.mean().shift(1).reset_index()
df_mean_7d = df_rolled_7d.mean().shift(1).reset_index()
df_mean_30d = df_rolled_30d.mean().shift(1).reset_index()

df_std_3d = df_rolled_3d.std().shift(1).reset_index()
df_std_7d = df_rolled_7d.std().shift(1).reset_index()
df_std_30d = df_rolled_30d.std().shift(1).reset_index()

for feature in lag_features:
    df[f"{
      
      feature}_mean_lag{
      
      window1}"] = df_mean_3d[feature]
    df[f"{
      
      feature}_mean_lag{
      
      window2}"] = df_mean_7d[feature]
    df[f"{
      
      feature}_mean_lag{
      
      window3}"] = df_mean_30d[feature]
    
    df[f"{
      
      feature}_std_lag{
      
      window1}"] = df_std_3d[feature]
    df[f"{
      
      feature}_std_lag{
      
      window2}"] = df_std_7d[feature]
    df[f"{
      
      feature}_std_lag{
      
      window3}"] = df_std_30d[feature]

然后从日期列中提取时间和日期特征“month"/“week”/“day”/“day_of_week”:

1689682139351.png (关注gz号“finance褪黑素”回复111可获取本文全部数据和代码)

4 模型构建

交叉验证:为了衡量预测模型的性能,我们通常希望将时间序列分为训练期和验证期

在本案例中,我们将选择 2020 年的所有数据作为保留数据,并根据 2012 年至 2019 年的所有数据训练我们的模型。

4.1 ARIMA

ARIMA 是自回归综合移动平均线的缩写。 它是一类模型,捕获时间序列数据中的一组不同标准时间结构。这个首字母缩略词是描述性的,捕获模型本身的关键方面。 简而言之,它们是:

  • AR:自回归 使用观测值与一定数量的滞后观测值之间的依赖关系的模型。

  • MA:移动平均 一种模型,它使用观测值与应用于滞后观测值的移动平均模型的残差之间的相关性。

  • ARIMA(p,d,q) 使用标准表示法,其中参数用整数值替换,以快速指示所使用的特定 ARIMA 模型。

    p:模型中包含的滞后观测值的数量,也称为滞后阶数。

    d:原始观测值差异的次数,也称为差异度。

    q:移动平均窗口的大小,也称为移动平均阶数。

根据自动定阶结果:

Best model: ARIMA(1,0,2)(0,0,0)[0]

1689682525367.png

RMSE of Auto ARIMAX: 322.4230217048

MAE of Auto ARIMAX: 227.30262397360966

model = pm.auto_arima(df_train.Weighted_Price, exogenous=df_train[exogenous_features], trace=True, error_action="ignore", suppress_warnings=True)
model.fit(df_train.Weighted_Price, exogenous=df_train[exogenous_features])

forecast = model.predict(n_periods=len(df_valid), exogenous=df_valid[exogenous_features])
df_valid["Forecast_ARIMAX"] = forecast
df_valid[["Weighted_Price", "Forecast_ARIMAX"]].plot(figsize=(14, 7))
4.2 FB Prophet

Prophet 是一个基于加法模型预测时间序列数据的程序,其中非线性趋势与每年、每周和每日的季节性以及假期影响相匹配。它最适合具有强烈季节性影响的时间序列和多个季节的历史数据。 Prophet 对于缺失数据和趋势变化具有鲁棒性,并且通常能够很好地处理异常值。

该过程使用可分解的时间序列模型,该模型具有三个主要模型组件:趋势、季节性和假期。

y ( t ) = g ( t ) + s ( t ) + h ( t ) + e ( t ) y(t) = g(t) + s(t) + h(t) + e(t) y(t)=g(t)+s(t)+h(t)+e(t)

  • g(t) 趋势模型非周期性变化; 线性或逻辑
  • s(t) 季节性代表周期性变化; 即每周、每月、每年
  • h(t) 受假期影响的关系; 可能不规则的时间表 ≥ 1 天
    1689682697603.png
    1689682739656.png
    1689682764613.png

Prophet’s MAE : 229.59667095230242
Prophet’s RMSE : 323.0097924038709

model_fbp = Prophet()
for feature in exogenous_features:
    model_fbp.add_regressor(feature)

model_fbp.fit(df_train[["Timestamp", "Weighted_Price"] + exogenous_features].rename(columns={
    
    "Timestamp": "ds", "Weighted_Price": "y"}))

forecast = model_fbp.predict(df_valid[["Timestamp", "Weighted_Price"] + exogenous_features].rename(columns={
    
    "Timestamp": "ds"}))
forecast.head()
4.3 XGBoost

sklearn.metrics 模块实现了多个损失、得分和效用函数来衡量回归性能。 我们将应用其中的一些:

1.平均绝对误差:mean_absolute_error 函数计算平均绝对误差,这是与绝对误差损失或 L1 范数损失的预期值相对应的风险度量。

2.均方误差:mean_squared_error 函数计算均方误差,这是与均方误差的预期值相对应的风险度量。

3.R² 分数,决定系数:R 平方表示模型与数据的拟合程度。 R 平方接近 1.0 表示模型非常适合数据,而接近 0 则表示模型不太好。 当模型做出荒谬的预测时,R 平方也可能为负。
1689682887816.png

1689682917196.png

train MAE : 93.84335770668919
train RMSE : 180.86267546619558
train R2 : 0.9974666469851726

test MAE : 369.10469832408575
test RMSE : 470.2606637271415
test R2 : 0.908972182829824

4.4 LSTM
1689683097881.png
  • 长短期记忆网络(通常简称为“LSTM”)是一种特殊的 RNN,能够学习长期依赖性。
  • LSTM 的设计明确是为了避免长期依赖问题。 长时间记住信息实际上是它们的默认行为,而不是像 RNN 那样需要努力学习的东西!
  • 所有循环神经网络都具有神经网络重复模块链的形式。 在标准 RNN 中,这个重复模块将具有非常简单的结构,例如单个 tanh 层。
  • 此外,它们不会遇到梯度下降消失/爆炸等问题。
    1689683325105.png
    1689683349743.png
    1689683371761.png

Train RMSE: 0.04792889021418374
Train MAE: 0.20979423661732005
Test RMSE: 0.0544482676350978
Test MAE: 0.22614883920646772

# 建立LSTM Model

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense
from tensorflow.keras.layers import LSTM
from tensorflow.keras.layers import Dropout
regressor = Sequential()

# 第一层LSTM
regressor.add(LSTM(units = 50, return_sequences = True, input_shape = (X_train.shape[1], 1)))
regressor.add(Dropout(0.2))

# 第二层LSTM
regressor.add(LSTM(units = 50, return_sequences = True))
regressor.add(Dropout(0.2))

# 第三层LSTM
regressor.add(LSTM(units = 50, return_sequences = True))
regressor.add(Dropout(0.2))

# 第四层LSTM
regressor.add(LSTM(units = 50))
regressor.add(Dropout(0.2))

regressor.add(Dense(units = 1))

regressor.compile(optimizer = 'adam', loss = 'mean_squared_error')

history = regressor.fit(X_train, y_train, validation_split=0.1, epochs = 50, batch_size = 32, verbose=1, shuffle=False)

5 模型选择

1689683008175.png

ARIMAX RMSE : 322.4230217048
FB Prophet RMSE : 323.0097924038709
XGBoost RMSE : 470.2606637271415

ARIMAX MAE : 227.30262397360966
FB Prophet MAE : 229.59667095230242
XGBoost MAE : 369.10469832408575

LSTM RMSE: 0.04792889021418374
LSTM MAE: 0.20979423661732005

结合前面LSTM的结果,明显LSTM的效果更好,因此我们选择LSTM模型来预测未来 30 天的加权价格。

1689683615137.png 1689683570953.png

6 改进建议

  • 可以增加 epoch 的数量来优化模型性能,可以将 epoch 增加到 100 并且可以得到结果。 此外,滞后特征的数量可以增加到 100 以上,以帮助学习模型。
  • 可以使用 LSTM 自动编码器进行时间序列中的异常检测。
    (关注gz号“finance褪黑素”回复111可获取本文全部数据和代码)

猜你喜欢

转载自blog.csdn.net/celiaweiwei/article/details/131802336