С одной стороны мы продолжаем решать задачу, поставленную здесь, а потом было несколько шагов по решению этой задачи:
- получение и обработка данных,
- продолжение обработки,
- анализ временных рядов,
- разбор недостатков решения,
- долгосрочный анализ данных.
С другой стороны мы начинаем все с начала, в том числе с уточнения условий задачи. В прошлый раз мы пытались спрогнозировать будущее значение цены, анализируя предыдущие цены (авторегрессия). Сейчас мы будем прогнозировать цену по значениям нескольких показателей, с которыми эта цена коррелирует, то есть движения цен похожи друг на друга.
Попробуем логически определить от чего зависит валютная пара доллар/рубль, ну естественно от доллара, потому что он в числителе этой дроби. И такой показатель мы можем получить, потому что существует индекс доллара, в том числе по отношению к валютам развивающихся стран. Еще одна зависимость - стоимость барреля нефти марки брент. Можно предположить, что российские индексы фондового рынка также влияют на ценообразование валюты.
Как обычно создаем новый блокнот (кто забыл, посмотрите предыдущие статьи). Вы можете посмотреть на мой блокнот здесь. И традиционно мы начинаем с импорта библиотек:
import pandas as pd
import pandas_datareader as pdr
import numpy as np
Здесь нет ничего необычного. Получим данные с сайта Federal Reserve Economic Data, причем делать мы это будем с помощью уже известного нам инструмента pandas datareader:
dt_start = '2006-01-10'
utils_list = ['DTWEXEMEGS', 'DCOILBRENTEU']
df = pdr.get_data_fred(utils_list, start=dt_start)
df.info()
Первая строка должна быть вам понятна: дата начала для всех наших параметров. Вторая переменная содержит массив названий финансовых активов: DTWEXEMEGS - индекс доллара США по отношению к валютам развивающихся стран, DCOILBRENTEU - нефть марки брент на европейском рынке. Третья команда является функцией того самого датаридера, которая читает данные с FRED. В качестве первого аргумента мы передаем тот самый список активов, в качестве второго - дата с которой надо начать читать данные. Последняя строка также должна быть вам понятна: она выводит информацию о полученном наборе данных.
Вы уже знаете, что надо обратить внимание на количество индексов и количество ненулевых значений в каждой колонке. И если они не совпадают, то это сигнал для анализа таблицы на предмет пропущенных значений и дублей. Проверим количество уникальных записей в индексе:
df.index.nunique()
Оно у нас соответствует количеству индексов, следовательно, нет дублей в индексах, то есть на каждую дату приходится ровно одна строчка. А вот количество ненулевых значений в двух колонках меньше количества дней. В прошлый раз мы удаляли лишние записи так как были дубли дней, а теперь мы будем заполнять пустые значения предыдущим значением, но прежде мы заменим нулевые значения на пустые значения, чтобы включить их в перечень заполняемых значений:
df.replace(0.0, np.NaN, inplace=True)
df.fillna(method='ffill', axis=0, inplace=True)
df.info()
В функции заполнения пустых значений мы использовали метод ffill (forward fill), который заполняет пустые значения предыдущим значение, а вот если пустое значение в самом начале, то есть предыдущего нет, то следует воспользоваться методом bfill (back fill). Еще одна странность этой функции, что мы указываем axis=0, то есть по строчкам, но заполняет он по колонкам. Если вам потребуется заполнять по строчкам, то надо указывать axis=1. Вывод последней функции показывает совпадения количества индексов и ненулевых значений в наших колонках.
Вам может показаться странным, что мы начали получение данных не с валютной пары, но количество торговых дней в США больше, поэтому мы лучше заполним пустые значения, благо мы уже умеем это делать, чем будем терять дни, когда были праздники только в России. В прошлый раз мы использовали актив с поставкой сегодня, но другой актив с поставкой завтра имеет большую ликвидность, то есть количество контрактов больше. Получим данные именно по более ликвидному активу:
df1 = pdr.get_data_moex('USD000UTSTOM', start=dt_start)
df1.info()
Чтобы не затереть данные с FRED мы использовали переменную df1, все остальное должно быть вам уже понятно. Обратите внимание на количество индексов, их почти в два раза больше, чем должно быть, поэтому сразу же предполагаем наличие дублей. В этот раз мы будем хитрее и попробуем понять причину дубляжа, а она кроется в режиме торгов, которых в течение одного и того же дня может быть несколько. Каждый режим торгов имеет свой показатель в колонке BOARDID. Посмотрим на распределение значений по этой колонке:
df1.BOARDID.value_counts()
Логично предположить, что нам надо отобрать строчки по значению CETS, которых не только больше, но и количество значений соответствует временному промежутку (сравните с количеством строчек в наборе df):
df1 = df1[df1.BOARDID == 'CETS']
df1.info()
Вот теперь функция info показывает нужное количество и дней и строк. Но давайте проверим количество уникальный значений в индексе:
df1.index.nunique()
Опа! Есть еще и дубли, но с этим мы уже умеем работать:
df1.drop_duplicates(inplace=True)
df1.info()
Количество строк совпадает с количеством дней, но мы помним, что могут быть не только пустые значения, но и нулевые. По большому счету нам надо только значения колонки CLOSE. Если вам не понятно это утверждение, то вернитесь к статье, точнее к тому месту, где мы выбираем одну из четырех колонок после сравнения корреляции. Используем только с одну колонку:
df1.CLOSE.replace(0.0, np.NaN, inplace=True)
df1.CLOSE.fillna(method='ffill', axis=0, inplace=True)
Теперь данные подготовлены для объединения в один набор. Делается это так:
df = df.join(df1.CLOSE)
df.head()
В df мы записываем результат "левого" объединения df и df1 по индексу, который у нас является датой. Ни "левое" ни по индексу нигде не фигурирует в команде, но это значения по умолчанию. Если вы не работали с SQL-командами, то левым набором является первый набор (мы пишем слева направо). "Левое" объединение означает, что мы используем все записи с первого набора и только те со второго набора, которые совпадают индексами (на самом деле можно объединять не только по индексам, но нам сейчас это не интересно). В нашем случае даты с FRED войдут все, а с MOEX только те, которые совпадают. Если будет дата FRED для которой не будет значения с MOEX, то в этом поле будет пустое значение. Результат вы может увидеть в выводе функции head. Лично меня напрягают длинные ничего не значащие названия колонок, но это легко поправить:
cols_dic = {'DTWEXEMEGS': 'dollar', 'DCOILBRENTEU': 'brent', 'CLOSE': 'ruble'}
df.rename(columns=cols_dic, inplace=True)
df.tail()
Небольшое отступление от основной темы. Представьте, что все сделанные операции пришлось бы выполнять в Excel. Что-то пришлось бы выполнять во встроенном языке программирования, что-то - стандартными средствами. Это заняло бы значительно больше времени и было бы не так удобно как в jupyter. Вопрос: "Почему не в Excel?", возможен только для десятков и сотен строчек, а если их тысячи и больше, то это точно не Excel.
Теперь нам надо почти такие же действия повторить с каждым индексом с MOEX, который мы хотим включить. Для упрощения этой задачи создадим цикл, внутри которого будут выполняться одинаковые преобразования, меняться будет только название актива:
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.info()
Мы создали массив названий активов, во второй строке создали цикл по всем элементам массива. Тело цикла в python обозначается отступом после двоеточия (отступ в тексте не виден, поэтому воспользуйтесь оригинальным блокнотом). Внутри цикла используется переменная el_ind, в которой хранится текущее название актива. Все остальное мы уже разобрали. Первой строчкой после цикла, выводящей информацию об наборе данных, является строчка без отступа. И опять обращаем внимание на количество ненулевых значений в колонках. Принципиально мы могли бы один раз выполнить заполнение в этом месте, но этого требовала логика повествования. Опять же выполняется все быстро:
df.fillna(method='ffill', inplace=True)
df.info()
Сохраним полученный набор данных, чтобы в следующей статье получить прогноз:
df.to_csv('/content/drive/MyDrive/usdrub1.csv')
Надеюсь, что изложенный материал был понятен и полезен. На всякий случай предупреждаю, что это не является инвестиционной рекомендацией.