自然语言处理(NLP)、神经网络(NN)、时间序列:能否使用谷歌趋势数据预测油价?

自然语言处理(NLP)、神经网络(NN)以及时间序列分析:利用谷歌趋势数据预测油价的可行性

首先使用Word2Vec,然后从谷歌趋势中抓取谷歌搜索的频率,接着通过Fourier分解进行时间序列分析,并使用Keras进行神经网络预测,尝试预测未来的油价。

在许多出版物中,我们看到建立算法以执行特定任务,但实际上,完整的数据分析/科学工作是一项复杂的任务,将需要一系列不同的步骤组合,每个步骤都有核心分析任务和模型,以获得有用的结果。

这个项目是一个非常雄心勃勃的企图,根据三个算法来预测油价:NLP(word2vec)用于查找与“石油”相关的词汇,傅里叶分析后的时间序列分解(分别预测每个时间效应),以及使用Keras的神经网络来根据谷歌中的词汇搜索频率预测油价的随机波动。

以下是该工作所遵循的方法的高级结构:

第一步是加载必要的库并从Gensim下载预训练的NLP模型。我使用了两个Word2Vec模型,一个是使用维基百科数据训练的,另一个是使用Twitter数据训练的。

import numpy as npimport pandas as pdimport matplotlib.pyplot as pltimport seaborn as snsfrom statsmodels.tsa.seasonal import MSTLimport klibfrom scipy import spatialimport gensim.downloader as apifrom statsmodels.tsa.seasonal import MSTLfrom nltk.stem import WordNetLemmatizerfrom sklearn.preprocessing import StandardScalerfrom sklearn.model_selection import train_test_splitfrom sklearn.linear_model import LinearRegressionfrom datetime import timedeltaimport tensorflow as tfimport mathimport nltkfrom scipy import optimizefrom sklearn.preprocessing import MinMaxScalernltk.download('wordnet')from pmdarima import auto_arimamodel1 = api.load("glove-wiki-gigaword-200")model2 = api.load("glove-twitter-100")

对于分析中的NLP部分,Word2Vec是正确的选择,因为:

  1. 它是为相似性-距离而训练的,与例如BERT相比,后者通常是为不同的下游任务(如屏蔽)而训练的。
  2. 它生成词嵌入,这正是我们试图获取和比较的。

在这种情况下,我在预训练模型上的使用是合理的,因为我们将在谷歌数据上使用它,这应该是干净的数据。在文本数据不干净或使用的语言过于特定的情况下,我不建议使用预训练模型。

然后,我定义一个函数来获取两个模型中最相似的单词并对其求平均。

def close_words(wrd): tbl1 = pd.DataFrame(model1.most_similar(wrd, topn=10), columns=['word','siml']).set_index('word') tbl2 = pd.DataFrame(model2.most_similar(wrd, topn=10), columns=['word','siml']).set_index('word') tbl = pd.concat((tbl1, tbl2), axis=1).mean(axis=1) tbl = pd.DataFrame(tbl).sort_values(0, ascending=False) return tbldfs = []

在这里,我对我认为与石油或油价相关的几个关键词进行搜索。首先,我从“oil”开始,然后寻找来自列表的其他单词,遵循“链”的方式。此步骤之后,“保存”搜索结果到一个数据帧列表。

同时,为了避免使用类似的术语,我应用了词形还原(Lemmatization)。请注意,我更喜欢词形还原而不是词干提取(stemming),因为词形还原得到的是出现在谷歌搜索中的真实词语,而词干提取得到的不是。

search = 'barrels'wordsim = close_words(search)lemmatizer = WordNetLemmatizer()wordsim['Lemma'] = wordsim.indexwordsim['Lemma'] = wordsim['Lemma'].apply(lambda x: lemmatizer.lemmatize(x))wordsim

如果我喜欢这些结果(它们对我有意义),我将它们添加到列表中。然后,我重复这个过程直到有一个相当大的列表。请注意,在这个练习中,我有一个限制,即我用来运行与Google趋势相匹配的API具有其免费许可证的限制。

最后,我将结果保存到我可以在网络爬虫中使用的文件中。

dfs.append(wordsim)finaldf = pd.concat(dfs, ignore_index=True).groupby('Lemma').mean()finaldf.to_csv("finaldf.csv")

现在我将它们都保存到一个列表中,我将使用一个名为Google趋势爬虫的第三方API来在Google趋势中检索(https://apify.com/emastra/google-trends-scraper

免责声明:本文仅用于教育目的。我们不鼓励任何人爬取网站,尤其是那些可能有相应行动条款和条件的网站产权。

爬虫可能需要一些时间来运行。一旦爬虫完成,我将保存结果并导入它们。它们以转置格式显示,所以我接下来会对其进行处理,并为时间序列和数字指定格式。

workdf = pd.read_csv("dataset_google-trends-scraper_2023-10-17_07-39-06-381.csv")workdf = workdf.Tdf2 = workdf.iloc[[-1]]workdf = workdf[:-1]workdf.columns = list(df2.values[0])workdf['Timestamp'] = workdf.indexworkdf['Timestamp'] = pd.to_datetime(workdf['Timestamp'], format='%b %d, %Y')workdf = workdf.sort_values('Timestamp')workdf = workdf.set_index('Timestamp')workdf = workdf.apply(pd.to_numeric, errors='coerce', axis=1)workdf

我发现预测变量中也有一些季节性,所以我使用52和104周(一年和两年)的时间表对它们进行分解。我使用分解后的残差。

对于整个工作的一个确凿改进可能是在预测变量上使用与响应变量相同的方法,在时间的限制下,我没有在这种情况下这样做。

a = []for col in workdf.columns: sea = MSTL(workdf[col], periods=(52, 104)).fit() res = pd.DataFrame(sea.resid) res.columns = [col] a.append(res)xres = pd.concat(a, axis=1)xres

这个工作的下一部分涉及响应变量的准备。

首先,我导入来自政府数据的燃料价格并对其应用格式。

ydf = pd.read_csv("GASREGW.csv")from datetime import timedeltaydf['DATE'] = pd.to_datetime(ydf['DATE'], format='%Y-%m-%d')  - timedelta(days=1)ydf = ydf.sort_values('DATE')ydf = ydf.set_index('DATE')ydf = ydf.apply(pd.to_numeric, errors='coerce', axis=1)ydf

现在,我想更多地了解油价的季节性。为了做到这一点,我对该系列进行了傅里叶变换,以找到频率,其逆变换将是可能成分的时间周期(以周为单位)。

然后,我提取振幅较大的成分并将其用作季节性分解的输入。

from scipy.fft import fft, fftfreqimport numpy as npyf = fft(ydf['GASREGW'].values)N = len(ydf)xf = 1/(fftfreq(N, d=1.0))nyf = np.abs(yf)four = pd.DataFrame({'周期':xf, '振幅':nyf})four = four[(four['周期']>0) & (four['周期']<=200)]four = four.sort_values(['周期'], ascending=True)four['周期'] = four['周期'].apply(lambda x: math.floor(x))four = four.groupby('周期').max().reset_index(drop=False)four = four.sort_values('振幅', ascending=False).head(5)four

接下来,我要进行时间序列分解。请注意,我将131周替换为104周,因为我无法继续使用我所拥有的数据。

出于以下几个原因,结果非常好:

  1. 趋势是线性的。
  2. 季节性呈现出规律性的正弦行为。
  3. 剩余部分看起来“随机”,意味着其中排除了所有的趋势和周期性行为。

我要用神经网络预测的是剩余部分。

seas = MSTL(ydf['GASREGW'], periods=(32, 37, 52, 65, 104)).fit()seas.plot()plt.tight_layout()plt.show()

现在,我已经对数据集进行了良好的格式化。

ydf['季节性_32'] = seas.seasonal['seasonal_32']ydf['季节性_37'] = seas.seasonal['seasonal_37']ydf['季节性_52'] = seas.seasonal['seasonal_52']ydf['季节性_65'] = seas.seasonal['seasonal_65']ydf['季节性_104'] = seas.seasonal['seasonal_104']ydf['趋势'] = seas.trendydf['剩余'] = seas.residydf['差分'] = ydf['剩余'].diff(-1)ydf

数据仍然不适合建模,首先我需要进行重新缩放,以确保所有预测因子处于相同的尺度上。

def properscaler(simio): scaler = StandardScaler() resultsWordstrans = scaler.fit_transform(simio) resultsWordstrans = pd.DataFrame(resultsWordstrans) resultsWordstrans.index = simio.index resultsWordstrans.columns = simio.columns return resultsWordstransxresidualsS = properscaler(xres)xresidualsS

然后,为了“平滑”残差,我将对导数进行处理。利用导数的定义,并且由于时间周期是恒定的,这意味着只需处理残差之间的差异。

在这一步中,我对响应变量应用了Tanh转换。我这样做的原因是因为那将是我的最后一个激活函数,我希望实际值和由神经网络模型预测的值在相同的尺度和范围内。

DF = pd.merge(xresidualsS, ydf, left_index=True, right_index=True)DF['差分'] = DF['差分'].apply(lambda x: math.tanh(x))DF.index = workdf.indexDF = DF.dropna()DFf = DF.drop(columns=['GASREGW','seasonal_32','seasonal_37','seasonal_52','seasonal_65','seasonal_104','Trend','Resid'])DFf

由于描述性分析不是本工作的目标,而且这篇文章已经很长了,我只打算通过一个相关矩阵进行快速探索。

corr = DFf.corr(method = 'pearson')sns.heatmap(corr, cmap=sns.color_palette("vlag", as_cmap=True))

最后,是时候进行建模阶段了。通常,第一步是将数据分为训练集和评估集。

finaleval=DFf[-12:]subset=DFf[:-12]x_subset = subset.drop(columns=["Diff"]).to_numpy()y_subset = subset['Diff'].to_numpy()x_finaleval = finaleval.drop(columns=["Diff"]).to_numpy()y_finaleval = finaleval[['Diff']].to_numpy()

然后我使用了Keras库里的神经网络回归策略。如前所述,我使用的激活函数是双曲正切函数,经过试错后发现这是最适合这个练习的函数。

#initializeneur = tf.keras.models.Sequential()#layersneur.add(tf.keras.layers.Dense(units=1000, activation='tanh'))neur.add(tf.keras.layers.Dense(units=5000, activation='tanh'))neur.add(tf.keras.layers.Dense(units=7000, activation='tanh'))#output layerneur.add(tf.keras.layers.Dense(units=1))from keras import backend as Kdef custom_metric(y_true, y_pred):    SS_res =  K.sum(K.square( y_true-y_pred ))    SS_tot = K.sum(K.square( y_true - K.mean(y_true) ) )    return ( 1 - SS_res/(SS_tot + K.epsilon()) )#using mse for regression. Simple and clearneur.compile(optimizer='Adam', loss='mean_squared_error', metrics=[custom_metric])#trainneur.fit(x_subset, y_subset, batch_size=220, epochs=2000)

好了,我们得到了一个模型。现在我要评估未知数据了。R2结果还不错,不是最好,但也不错。

test_out = neur.predict(x_finaleval)output = finaleval[['Diff']]output['predicted'] = test_outoutput['actual'] = y_finalevalfrom sklearn.metrics import mean_absolute_error, mean_squared_error, r2_scoreprint("R2: ", r2_score(output['actual'], output['predicted']))print("MeanSqError: ",np.sqrt(mean_squared_error(output['actual'], output['predicted'])))print("MeanAbsError: ", mean_absolute_error(output['actual'],output['predicted']))output = output[['predicted','actual']]output

请注意,刚才预测的是残差的双曲正切函数。下一步是撤销这些转换。

output = output[['predicted']]output['predictedArcTanh'] = output['predicted'].apply(lambda x: math.atanh(x))output['PredResid'] = np.nanstart = 0prevval = 0for index, row in output.iterrows(): if start == 0:  prevval = -0.037122 - row['predictedArcTanh'] else:  prevval = prevval - row['predictedArcTanh'] output.at[index, 'PredResid'] = prevvaloutput = output[['PredResid']]output

这是我们对这些周的残差的预测。

我们还没有完成,对于分解的趋势组件,我将进行线性回归和外推。

ydf['rown'] = range(len(ydf))setreg = ydf[ydf.index <'2023-07-23']setreg = setreg[['Trend','rown']]mod = LinearRegression().fit(setreg[['rown']], setreg['Trend'])setreg['PredTrend'] = mod.predict(setreg[['rown']])plt.scatter(setreg['rown'], setreg['Trend'], color="black")plt.plot(setreg['rown'], setreg['PredTrend'], color="blue", linewidth=3)plt.xticks(())plt.yticks(())plt.show()

这是我对趋势的预测。

outcome = ydf[ydf.index >='2023-07-23']outcome = outcome[['GASREGW','rown']]outcome['PredTrend'] = mod.predict(outcome[['rown']])outcome

对于季节性值,我将它们拟合到一个余弦函数中,创建一个自定义的numpy函数,并给定“良好”的参数种子(振幅,频率,相位角和偏移量)。由于种子对结果非常敏感,我不得不多次运行它。这是对季节性_32的预测。

from scipy import optimizefrom sklearn.preprocessing import MinMaxScalersetreg = ydf[ydf.index <'2023-07-23']setreg = setreg[['seasonal_32','rown']]def fit_func(x, a, b, c, d):    return a*np.cos(b*x+c) + dparams, params_covariance = optimize.curve_fit(fit_func, setreg['rown'], setreg['seasonal_32'], p0=(10,.19,0,0))setreg['Predseasonal_32'] = setreg['rown'].apply(lambda x: fit_func(x, *params))plt.scatter(setreg['rown'], setreg['seasonal_32'], color="black")plt.plot(setreg['rown'], setreg['Predseasonal_32'], color="blue", linewidth=3)plt.xticks(())plt.yticks(())plt.show()

拟合效果足够准确。我将结果保存在outcome数据集中。

#分配给outcomeoutcome['Predseasonal_32'] = outcome['rown'].apply(lambda x: fit_func(x, *params))

我对其他季节性成分也采用同样的方法进行处理。

  • 37周的季节性
setreg = ydf[ydf.index <'2023-07-23']setreg = setreg[['seasonal_37','rown']]def fit_func(x, a, b, c, d):    return a*np.cos(b*x+c) + dparams, params_covariance = optimize.curve_fit(fit_func, setreg['rown'], setreg['seasonal_37'], p0=(10,.17,0,0))setreg['Predseasonal_37'] = setreg['rown'].apply(lambda x: fit_func(x, *params))plt.scatter(setreg['rown'], setreg['seasonal_37'], color="black")plt.plot(setreg['rown'], setreg['Predseasonal_37'], color="blue", linewidth=3)plt.xticks(())plt.yticks(())plt.show()

#分配给outcomeoutcome['Predseasonal_37'] = outcome['rown'].apply(lambda x: fit_func(x, *params))
  • 季节性52周
import numpy as np
from scipy import optimize
from sklearn.preprocessing import MinMaxScaler

setreg = ydf[ydf.index <'2023-07-23']
setreg = setreg[['seasonal_52','rown']]

def fit_func(x, a, b, c, d):
    return a*np.cos(b*x+c) + d

params, params_covariance = optimize.curve_fit(fit_func, setreg['rown'], setreg['seasonal_52'], p0=(10,.13,0,0))
setreg['Predseasonal_52'] = setreg['rown'].apply(lambda x: fit_func(x, *params))
plt.scatter(setreg['rown'], setreg['seasonal_52'], color="black")
plt.plot(setreg['rown'], setreg['Predseasonal_52'], color="blue", linewidth=3)
plt.xticks(())
plt.yticks(())
plt.show()

#赋值到outcome
outcome['Predseasonal_52'] = outcome['rown'].apply(lambda x: fit_func(x, *params))
  • 季节性65周
setreg = ydf[ydf.index <'2023-07-23']
setreg = setreg[['seasonal_65','rown']]

def fit_func(x, a, b, c, d):
    return a*np.cos(b*x+c) + d

params, params_covariance = optimize.curve_fit(fit_func, setreg['rown'], setreg['seasonal_65'], p0=(10,.1,0,0))
setreg['Predseasonal_65'] = setreg['rown'].apply(lambda x: fit_func(x, *params))
plt.scatter(setreg['rown'], setreg['seasonal_65'], color="black")
plt.plot(setreg['rown'], setreg['Predseasonal_65'], color="blue", linewidth=3)
plt.xticks(())
plt.yticks(())
plt.show()

#赋值到outcome
outcome['Predseasonal_65'] = outcome['rown'].apply(lambda x: fit_func(x, *params))
  • 季节性104周
setreg = ydf[ydf.index <'2023-07-23']
setreg = setreg[['seasonal_104','rown']]

def fit_func(x, a, b, c, d):
    return a*np.cos(b*x+c) + d

params, params_covariance = optimize.curve_fit(fit_func, setreg['rown'], setreg['seasonal_104'], p0=(10,.05,0,0))
setreg['Predseasonal_104'] = setreg['rown'].apply(lambda x: fit_func(x, *params))
plt.scatter(setreg['rown'], setreg['seasonal_104'], color="black")
plt.plot(setreg['rown'], setreg['Predseasonal_104'], color="blue", linewidth=3)
plt.xticks(())
plt.yticks(())
plt.show()

最后,这是整个结果数据集的样子:

#赋值到outcome
outcome['Predseasonal_104'] = outcome['rown'].apply(lambda x: fit_func(x, *params))
outcome

所有组成部分的总和将给出一个预测价格。

final = pd.merge(outcome,output,left_index=True,right_index=True)
final['GASREGW_prep'] = final['PredTrend'] + final['Predseasonal_32'] + final['Predseasonal_37'] + final['Predseasonal_52'] + final['Predseasonal_65'] + final['Predseasonal_104'] + final['PredResid']
final

然后,让我们看看我们的结果如何!

sns.lineplot(x='index', y='value', hue='variable', data=pd.melt(final[['GASREGW','GASREGW_prep']].reset_index(drop=False), ['index']))plt.ylim(3,4.5)

<p这个变化十分保守的价格,但这个模型确实显示了一个普遍的趋势。

<p我猜,如果我可以使用免费的google collab笔记本来预测石油价格,我就不会在这里写这个整个项目给你了。但实际情况是,google趋势搜索主题中确实有一定的预测能力,可以用更强大的工具来探索。

</p这个变化十分保守的价格,但这个模型确实显示了一个普遍的趋势。