Мы продолжаем цикл статей:
- введение,
- получение и обработка данных,
- продолжение обработки,
- анализ временных рядов,
- разбор недостатков решения,
- долгосрочный анализ данных,
- получение и обработка данных для машинного обучения,
- линейная регрессия.
И в этот раз мы с вами займемся классификацией. Помните, мы как-то обсуждали, что точно спрогнозировать определенное значение почти невозможно, зато можно с определенной вероятностью попасть в диапазон значений. Теперь мы делаем следующий шаг - вместо диапазона значений используем классификацию. Например, прогноз на падение в пределах одного стандартного отклонения можно обозначить цифрой буквой, словом и т.д., таким образом, мы будем прогнозировать какого класса будет следующее значение.
В статистике традиционно используется 6-классовое разделение:
0 - вниз более 2 стандартных отклонений,
1 - от -2 до -1 стандартных отклонений,
2 - от -1 стандартного отклонения до 0,
3 - от 0 до 1 стандартного отклонения,
4 - от 1 до 2 стандартных отклонений,
5 - более 2 стандартных отклонений.
Что такое стандартное отклонение, как оно считается, как обозначается в статистике и другие вопросы вы можете нати самостоятельно в интернете, потому что ресурсов, в том числе на русском языке предостаточно. А мы по традиции создаем новый блокнот, подсмотреть мой вариант можно здесь. Первым делом объявляем библиотеки и функции:
import pandas as pd
import pandas_datareader as pdr
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import MinMaxScaler
К сожалению, данные, полученные прошлый раз, нам нужны в другом виде. Кроме того, с момента последнего получения в конец добавилось еще несколько дней. Поэтому получаем данные заново:
dt_start = '2006-01-10'
df = pdr.get_data_fred(['DTWEXEMEGS', 'DCOILBRENTEU'], start=dt_start)
df.replace(0.0, np.NaN, inplace=True)
df.fillna(method='ffill', axis=0, inplace=True)
df.rename(columns={'DTWEXEMEGS': 'dollar', 'DCOILBRENTEU': 'brent'}, inplace=True)
df.info()
Получаем данные по USD000UTSTOM:
df1 = pdr.get_data_moex('USD000UTSTOM', start=dt_start)
df1 = df1[df1.BOARDID == 'CETS']
df1.drop_duplicates(inplace=True)
df1.replace(0.0, np.NaN, inplace=True)
df1.fillna(method='ffill', axis=0, inplace=True)
Вот теперь начинаются изменения, а именно сформируем новый столбец, который будет отражать относительную разницу между ценой закрытия и открытия:
df1['result'] = (df1.CLOSE - df1.OPEN) / df1.HIGH
df1 = df1[['result']]
v_std = df1.result.std()
df1['cl'] = 0
Почему относительную? Да потому что абсолютная разница сильно меняется в течение времени, по крайней мере, в этом случае. Это будет сильно сказываться на результате. Кроме этого столбца из остальных значений нам больше ничего не потребуется, поэтому оставим в наборе данных только его. Вычислим то самое стандартное отклонение, и добавим новую колонку, заполнив её нулями. Как мы говорили выше, нулю соответствует значения отклоняющиеся вниз более 2 стандартных отклонений. Далее мы будем использовать методику последовательного переписывания значений попадающих в необходимый диапазон:
df1.loc[df1.result > v_std * -2, 'cl'] = 1
df1.loc[df1.result > v_std * -1, 'cl'] = 2
df1.loc[df1.result > 0, 'cl'] = 3
df1.loc[df1.result > v_std, 'cl'] = 4
df1.loc[df1.result > v_std * 2, 'cl'] = 5
df1.cl.value_counts()
Последняя строчка выводит количество значений в каждой категории:
Мы получили нужную колонку заполненную значениями классификации относительного результата дня. Теперь нужно её добавить в наш набор данных:
df = df.join(df1.cl)
df.cl.fillna(method='ffill', axis=0, inplace=True)
df.cl = df.cl.astype('int8')
df.info()
Третьей строчкой мы заменяем тип данных для уменьшения расходования памяти, конкретно в этом случае такой необходимости нет. Далее выполняем уже знакомый нам цикл:
ls_ind = ['IMOEX', 'RGBITR', 'RUCBITR']
for el_ind in ls_ind:
df1 = pdr.get_data_moex(el_ind, start=dt_start)
df1 = df1[['CLOSE']]
df1.drop_duplicates(inplace=True)
df1.rename({'CLOSE': el_ind}, axis=1, inplace=True)
df = df.join(df1)
df.fillna(method='ffill', axis=0, inplace=True)
df.info()
Также знакомая нам процедура выделения целевого значения и выборки данных:
y = df.cl
x = df.drop('cl', axis=1)
Дальше опять знакомая процедура нормализации и выделения тренировочной и тестовой выборки:
scaler = MinMaxScaler()
x_scaled = scaler.fit_transform(x)
x_train, x_test, y_train, y_test = train_test_split(x_scaled, y, test_size=0.1, shuffle=False)
Как видите многие действия очень скучны и однообразны, разобравшись в их назначении, вам придется их в буквальном смысле копипастить. Вот следующий участок кода хоть и отличается от участка с предыдущей статьи, но многие могут даже не увидеть эти отличия:
model = LogisticRegression()
model.fit(x_train, y_train)
model.score(x_train, y_train)
Первая строчка создает модель логистической регрессии, которую можно также назвать моделью классификации. Вторая строчка обучает модель, а третья выдает долю попаданий. С одной стороны мы видим, что доля попаданий ниже 0,5, но надо учитывать, что у нас не бинарная классификация, где попаданий долно быть выше 50%. Проверим на тестовой выборке:
model.score(x_test, y_test)
По сравнению с тренировочной выборкой результат отличный. Проблему с прогнозом решим таким же способом, как и прошлый раз:
y = df.cl[5:]
x = df.drop('cl', axis=1)[:-5]
Заново выполните ячейки с нормализацией, разделением, созданием и тренировкой модели. Сравните долю попаданий, она хоть и уменьшилась но не значительно. Теперь получаем прогноз:
x = df.drop('cl', axis=1)[-5:]
x_scaled = scaler.transform(x)
pred = model.predict(x_scaled)
pred
Попробуйте самостоятельно интерпретировать этот прогноз. На всякий случай, предупреждаю, что данный материал не является инвестиционной рекомендацией, а следовательно использовать его можно только на свой страх и риск.