НЛП, НН, временные ряды возможно ли предсказать цены на нефть с использованием данных от Google Trends?
Прогнозирование цен на нефть с помощью данныx от Google Trends НЛП, НН, временные ряды
Сначала используя Word2Vec, затем с помощью извлечения информации с Google Trends о частоте поиска в Google, а затем через временные ряды (с использованием разложения Фурье) и нейронные сети с помощью Keras, я пытаюсь предсказать будущие цены на нефть.
Во многих публикациях мы видим алгоритмы, разработанные для выполнения конкретной задачи, но на самом деле, полноценная работа с аналитикой или научными данными является сложным предприятием, требующим комбинации различных этапов, каждый из которых имеет свою ключевую аналитическую задачу и модель, чтобы получить полезные результаты.
Этот проект – очень амбициозная попытка предсказать цены на газ на основе трех алгоритмов: NLP (word2vec) для поиска слов, связанных с ‘Нефть’, декомпозиция временного ряда после анализа Фурье (для прогнозирования каждого временного эффекта отдельно) и нейронные сети с использованием Keras для прогнозирования случайной вариации цены на нефть, используя частоту поиска слов в Google.
В общих чертах, такова структура подхода, используемого в этой работе:
- NVIDIA и Scaleway ускоряют развитие для европейских стартапов и предприятий
- Байесовский AB-тестирование с помощью Pyro
- Ускорение обучения PyTorch с помощью FP8
Первый шаг – загрузить необходимые библиотеки и загрузить предобученные модели NLP из Gensim. Я использовал две модели Word2Vec, одну обученную на данных Wikipedia, а другую – на данных 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")
Word2Vec – правильный выбор для части анализа NLP, потому что:
- Он обучен для использования схожестей и расстояний, в отличие, например, от BERT, который обычно обучается для различных подзадач, таких как маскировка.
- Он генерирует word embeddings, что и является нашей целью и то, что мы пытаемся получить и сравнить.
В этом случае я обосновываю использование предобученной модели, поскольку мы собираемся использовать ее на данных Google, которые, в теории, должны быть чистыми. В случаях, когда текстовые данные не являются чистыми, или используемый язык слишком специфический, я бы не рекомендовал использовать предобученные модели.
Затем я определяю функцию для получения наиболее схожих слов из обеих моделей и их усреднения.
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 = []
Здесь я делаю поиск нескольких ключевых слов, которые, по моему мнению, связаны с нефтью или ценами на нефть. Сначала я начинаю с слова “нефть”, затем ищу другие слова, полученные из списка, следуя “цепочке”. Шаг после этого “сохраняет” результаты поиска в списке фреймов данных.
В то же время, чтобы избежать использования схожих терминов, я применяю процесс лемматизации. Обратите внимание, что я предпочитаю лемматизацию перед стеммингом, поскольку леммы – это реальные слова, которые будут появляться в поисках Google, а стемы – нет.
search = 'баррели'wordsim = close_words(search)lemmatizer = WordNetLemmatizer()wordsim['Lemma'] = wordsim.indexwordsim['Lemma'] = wordsim['Lemma'].apply(lambda x: lemmatizer.lemmatize(x))wordsim
Если мне нравятся результаты (они имеют смысл для меня), я добавляю их в список. Затем я повторяю процесс, пока не получу достаточно большой список. Обратите внимание, что в этом упражнении у меня есть ограничение: API, которое я использую для обращения к Google Trends, имеет ограничение на бесплатную лицензию.
Наконец, я сохраняю результаты в файл, который я могу использовать в скрапере.
dfs.append(wordsim)finaldf = pd.concat(dfs, ignore_index=True).groupby('Lemma').mean()finaldf.to_csv("finaldf.csv")
Теперь я сохраняю все в список, который я собираюсь использовать для поиска в Google Trends с использованием стороннего API, называемого Google Trends Scraper (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, fftfreq
import numpy as np
ydf = pd.DataFrame({'GASREGW': [1, 2, 3, 4, 5]})
yf = fft(ydf['GASREGW'].values)
N = len(ydf)
xf = 1 / (fftfreq(N, d=1.0))
nyf = np.abs(yf)
four = pd.DataFrame({'Period': xf, 'Amp': nyf})
four = four[(four['Period']>0) & (four['Period']<=200)]
four = four.sort_values(['Period'], ascending=True)
four['Period'] = four['Period'].apply(lambda x: math.floor(x))
four = four.groupby('Period').max().reset_index(drop=False)
four = four.sort_values('Amp', ascending=False).head(5)
four
Затем я делаю декомпозицию временного ряда. Обратите внимание, что я заменил 131 неделю на 104 недели, так как у меня нет данных для более длительного периода.
Результаты очень хорошие, я считаю, по нескольким причинам:
- Тренд линейный.
- Сезонные колебания имеют регулярное синусоидальное поведение.
- Остаточная составляющая выглядит “случайной”, что означает, что из нее исключены все трендовые и циклические изменения.
То, что я собираюсь предсказать с помощью нейронных сетей – это остаточная составляющая.
seas = MSTL(ydf['GASREGW'], periods=(32, 37, 52, 65, 104)).fit()
seas.plot()
plt.tight_layout()
plt.show()
Теперь у меня красиво оформленный набор данных, с которого я собираюсь начать работу.
ydf['seasonal_32'] = seas.seasonal['seasonal_32']
ydf['seasonal_37'] = seas.seasonal['seasonal_37']
ydf['seasonal_52'] = seas.seasonal['seasonal_52']
ydf['seasonal_65'] = seas.seasonal['seasonal_65']
ydf['seasonal_104'] = seas.seasonal['seasonal_104']
ydf['Trend'] = seas.trend
ydf['Resid'] = seas.resid
ydf['Diff'] = ydf['Resid'].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 resultsWordstrans
xresidualsS = properscaler(xres)
xresidualsS
Затем, чтобы “сгладить” остаточную составляющую, я буду работать с производной. Используя определение производной и поскольку период времени постоянен, это просто означает работу с разницей между остаточными значениями.
На этом же шаге я применяю преобразование Tanh к целевой переменной. Я делаю это потому, что это будет моя последняя функция активации, и я хочу, чтобы реальные значения и предсказанные моделью нейронной сети были в одном масштабе и диапазоне.
DF = pd.merge(xresidualsS, ydf, left_index=True, right_index=True)
DF['Diff'] = DF['Diff'].apply(lambda x: math.tanh(x))
DF.index = workdf.index
DF = 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. Как упоминалось ранее, функция активации, которую я использую, – это Tanh, которую я нашел наилучшей для данного упражнения после проб и ошибок.
#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
Обратите внимание, что то, что только что было предсказано, – это Tanh разницы остатков. Следующий шаг – отменить эти преобразования.
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 с моделью, которую я хочу подогнать, и давая ей “хорошие” начальные значения для параметров (амплитуда, частота, угол фазы и смещение). Пришлось запустить ее несколько раз, так как результаты были очень чувствительны к начальным значениям. Это предсказание для seasonal_32.
from scipy import optimize
from sklearn.preprocessing import MinMaxScaler
setreg = 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) + d
params, 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.
#assign to outcome
outcome['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) + d
params, 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()
#assign to outcome
outcome['Predseasonal_37'] = outcome['rown'].apply(lambda x: fit_func(x, *params))
- Сезонная 52 недели
from scipy import optimizefrom sklearn.preprocessing import MinMaxScalersetreg = 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) + dparams, 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['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) + dparams, 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['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) + dparams, 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['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)
Что является очень консервативным вариантом вариации цен, но модель в целом показывает общую тенденцию.
Полагаю, что если бы я мог предсказывать цены на нефть с помощью бесплатных записных книжек Google Collab, я бы не писал весь этот проект для вас. Но реальность в том, что в Google Trends поисковые запросы есть некоторая предиктивная сила, которая может быть исследована с более мощными инструментами.