paint-brush
Работа с недостающими данными в финансовых временных рядах: рецепты и подводные камник@vkirilin
12,436 чтения
12,436 чтения

Работа с недостающими данными в финансовых временных рядах: рецепты и подводные камни

к Vladimir Kirilin10m2024/04/03
Read on Terminal Reader

Слишком долго; Читать

Я концентрируюсь на методах обработки недостающих данных в финансовых временных рядах. Используя некоторые примеры данных, я показываю, что LOCF обычно является достойным методом перехода по сравнению с отбрасыванием и вменением, но имеет свои недостатки, т. е. может создавать искусственные нежелательные скачки в данных. Однако альтернативы, такие как интерполяция, имеют свои проблемы, особенно в контексте прогнозирования/прогнозирования в реальном времени.
featured image - Работа с недостающими данными в финансовых временных рядах: рецепты и подводные камни
Vladimir Kirilin HackerNoon profile picture
0-item

Если вы чем-то похожи на меня — вы хотя бы раз сталкивались с недостающими данными в своих наборах данных. Или дважды. Или слишком много раз…


Иногда все, что нужно для обработки этих надоедливых NA, — это удалить их, то есть удалить строки, содержащие недостающие данные. Однако это не всегда может быть оптимальным, особенно для данных временных рядов и тем более для финансовых. Конечно, эта проблема хорошо изучена, поэтому существует множество альтернатив дропсу.


Я рассмотрю некоторые из них (перечислены ниже) и обсужу плюсы и минусы:


  1. падение

  2. LOCF (последнее наблюдение перенесено)

  3. Среднее (или подобное) вменение

  4. Интерполяция


Спойлер: универсального подхода не существует! Я утверждаю, что LOCF обычно является хорошим выбором для финансирования, но не лишен и недостатков. Имея это в виду, позвольте мне описать методы и данные, которые я буду использовать для их демонстрации.


Примечание: если быть педантичным, то все методы 2–4 являются примерами некоторого вменения.


Данные модели

Давайте начнем с нескольких примеров того, почему вообще стоит беспокоиться о падении. Чтобы проиллюстрировать это, я сгенерировал некоторые чрезмерно упрощенные ежедневные данные о ценах на акции, предполагая, что они следуют случайному блужданию без дрейфа (т. е. средняя долгосрочная цена должна оставаться постоянной) – тем не менее, не самое точное, но благоприятное предположение.


 np.random.seed(10) # needed for reproducibility price_moves = 3*pd.Series(np.random.randn(100)) # generating random "steps" with 0 mean price_vec = 100 + price_moves.cumsum() # generating brownian motion price_vec.plot() 


неизменный ценовой ряд


Что ж, сюжет выглядит вполне благонамеренным.


Предположим, мы теперь хотим узнать эмпирическое среднее значение ежедневных различий в ценах:

 price_vec.diff().mean() #sample mean >0.20030544816842052

Очевидно, ненулевое, в отличие от порождающего ряда – но это всего лишь выборочный шум. Все идет нормально.


Удаление данных

Теперь давайте немного деформируем эти данные, удалив несколько точек данных:

 price_vec_na_simple = price_vec.copy() price_vec_na_simple.iloc[90:95] = np.array([np.NaN, np.NaN, np.NaN, np.NaN, np.NaN]) # price_vec_na_simple.diff().mean() >0.1433356258183252


Мы сразу замечаем пару вещей -

  1. Среднее значение каким-то образом не является NA, хотя вектор diff явно будет содержать NA.

  2. Среднее значение отличается от того, которое мы получили ранее


Теперь №1 довольно прост: pd.mean автоматически удаляет NA по умолчанию.

А как насчет №2? Давайте переосмыслим то, что мы вычисляем.


Легко показать, что, по крайней мере, без NA, средняя разница цен должна быть просто (price_vec[99]-price_vec[0])/99 - действительно, когда мы суммируем разницу цен, все «промежуточные» части сокращаются, вот так (price_vec[1] - price_vec[0]) + (price_vec[2] - price_vec[1]) + .. !


Теперь, когда вставлены недостающие данные, если мы сначала возьмем разности, а затем отбросим NA , это сокращение будет нарушено - некоторые простые математические вычисления показывают, что вы теперь вычисляете (price_vec[99] - price_vec[0] - price_vec[95] + price_vec[89])/93 .


Чтобы продемонстрировать это, обратите внимание, что следующие два термина теперь опущены — price_vec[95] - price_vec[94] и price_vec[90] - price_vec[89] , поскольку (NA - any number) оценивается как NA и затем отбрасывается.


Давайте проверим это:

 (price_vec[99] - price_vec[0])/99 >0.20030544816842052 (price_vec[99] - price_vec[0] - price_vec[95] + price_vec[89])/93 >0.1433356258183252


Теперь становится яснее, как мы можем исправить ситуацию — нам нужно сначала отбросить NA, а затем diff

 price_vec_na_simple.dropna().diff().mean() >0.21095999328376203


Среднее значение почти вернулось туда, где оно должно быть — небольшое расхождение происходит потому, что у нас теперь меньше членов в среднем — 94 вместо 99.


Хорошо, похоже, если нас интересует только среднее значение, мы можем просто использовать dropna (при условии, что мы делаем это правильно)? В конце концов, разница между 0.2 и 0.21 явно находится в пределах нашей толерантности к шуму. Ну не совсем - посмотрим почему.


LOCF

Что такое ЛОКФ?

LOCF означает «Последнее наблюдение перенесено». Идея, лежащая в основе этого, очень проста: если я записываю данные через некоторые интервалы времени, которые могут быть или не быть регулярными, если наблюдение за каким-то конкретным интервалом отсутствует, мы просто предполагаем, что с нашей переменной ничего не изменилось, и заменяем ее последней не- пропущенное значение (например — [3, 5, NA, 8] → [3, 5, 5, 8]). Можно спросить: а зачем вообще заботиться об интервале с отсутствующим наблюдением, т.е. не просто удалить его, как в методе «отбрасывания»? Что ж, ответ кроется в свойственном «падению» дефекте, о котором я не упомянул выше.


Предположим, вы записываете несколько величин одновременно, особенно те, которые обычно не меняются слишком сильно и быстро, например, ежечасные записи температуры и влажности. Предположим, у вас есть оба значения для 10:00, 11:00 и 12:00, но только влажность в 13:00. Вы просто удалите эту «строку», то есть притворитесь, что у вас нет показаний за 13:00? Что ж, это нормально, если у вас есть только две переменные, даже если вы только что удалили некоторую потенциально ценную информацию (влажность в 13:00). Но если у вас много таких случаев или много переменных одновременно, удаление может привести к тому, что у вас вообще не останется данных!


Очень привлекательная альтернатива — просто предположить, что с 12:00 до 13:00 ничего не изменилось в температуре. Ведь если бы кто-то пришел к нам в 12:30 и спросил - «какая сейчас температура», мы бы с полным правом ответили показаниями 12:00 (если, конечно, мы не сможем сразу получить новые показания). ). Почему бы не использовать ту же логику для значения 13:00?


Зачем использовать LOCF (в финансах)?

Во-первых, давайте проверим наш новый подход на предыдущих данных:

 price_vec_na_simple.ffill().diff().mean() # ffill performs LOCF by default >0.20030544816842052


Похоже, мы точно восстановили прежнее значение! Кроме того, если вы хотите провести дальнейшее исследование данных о разнице цен — теперь они выглядят более «упорядоченными», поскольку в них есть записи для каждого дня, хотя пять из этих записей теперь равны 0 (зачем? попробуйте запустить price_vec_na_simple.ffill().diff().iloc[90:95] чтобы убедиться в этом сами).


Кроме того, в финансах недостающие данные и данные, выходящие за рамки , часто объединяются. Позвольте мне проиллюстрировать это:

 #inflate two observations, delete three next ones price_moves_na[90] += 20 price_moves_na[91] += 30 price_moves_na[92] -= 50 # to "deflate" the price shock back price_vec_na = (100 + price_moves_na.cumsum()) price_vec_na[92:95] = [np.NaN, np.NaN, np.NaN] price_vec_na.tail(20).plot() price_vec_na.diff().dropna().mean() >0.7093365245831178 

всплеск + недостающие данные


Мы видим, что после резкого роста цен данные просто недоступны 3 дня подряд. Это не такой уж «искусственный» пример, как может показаться! Представьте себе, что после скачка торговля остановилась, по крайней мере, на этой конкретной бирже. Потом все немного успокоилось, и цена вернулась в нормальный режим. Возможно, произошло что-то постепенное. происходящее за кулисами, которое на самом деле «соединило» точки между спайком и постспайком, успокойтесь.Но вы этого не знаете и не имеете на это никаких данных!


Какое предположение будет наиболее естественным, если у нас нет новых данных? Напомним, что наша модель генерации данных была в основном основана на изменении цен. Так что, если новых данных нет, возможно, цена вообще не меняется? Именно это и предполагает LOCF (перенос последнего наблюдения).

Немного математики для контекста

Примечание для любознательного читателя: возможно, более фундаментальный взгляд на то, почему LOCF особенно подходит для данных о ценах на акции, заключается в том, что он обычно моделируется как мартингейл . Грубо говоря, мартингейл — это нечто, где лучшее предположение на завтра — это то, что мы видим сегодня, или E[x_{t+1} | x_t] = x_t


Хорошо, вернемся к фактическим данным! Давайте рассмотрим последствия LOCF как визуально, так и численно:

 price_vec_na.ffill().tail(20).plot() price_vec_na.ffill().diff().mean() >0.20030544816842052 

Вменение LOCF


Мы сразу видим плюсы и минусы LOCF (в буквальном смысле)! Во-первых, среднее значение возвращается туда, где мы «ожидаем», что оно будет, т.е. к неизмененному эмпирическому значению. Однако мы вводим довольно некрасивый период, когда цена выходит за пределы «типичного» и искусственное падение цены между 94 и 95 днями.

А как насчет вменения?

Давайте сравним результаты, которые мы получаем из LOCF, со (средним) вменением. Это очень распространенный выбор для обработки NA, особенно для данных, не относящихся к временным рядам. Однако, если это сделать наивно, у этого метода будет много недостатков при использовании для финансовых данных.


  • Если вы просто используете среднее значение для всей выборки, вы вносите очевидную предвзятую ошибку, т. е. используете будущие данные для приписания прошлых значений.

  • Использование какого-либо ретроспективного или скользящего среднего, безусловно, лучше, однако иногда это может вступать в противоречие с представлением «базового уровня» мартингейла, которое мы описали ранее.


Давайте рассмотрим это немного подробнее. Я воспользуюсь ретроспективным расчетом наших старых данных -

 price_vec_na_impute = price_vec_na.copy() price_vec_na_impute[price_vec_na_impute.isna()] = price_vec_na.iloc[:90].mean() price_vec_na_impute.diff().mean() >0.20030544816842052 

среднее вменение

Мы восстанавливаем «правильное» среднее изменение цены, так же, как и LOCF. НО мы вводим искусственное падение цен между 91 и 92 днями, которое в некотором смысле даже хуже того, которое было у нас раньше. В конце концов, это произошло, когда или после того, как все, вероятно, успокоилось, тогда как этот просто предполагает, что все сразу же возвращается в норму. Помимо этого, на практике может быть довольно сложно сбалансировать окно ретроспективного анализа, чтобы мы а) фиксировали недавние тенденции, но также б) фиксировали долгосрочные тенденции (обычный компромисс между смещением и отклонением).


Добавляем вторую переменную

Предположим, теперь мы хотим выполнить более сложную задачу — извлечь корреляцию между движениями цен двух активов из эмпирических данных, когда в одном или обоих ценовых рядах отсутствуют данные. Конечно, мы все еще можем использовать удаление, но:


  • даже если мы сможем его использовать, оптимально ли это?

  • что, если у нас много переменных — тогда удаление всех строк хотя бы с одним NA может оставить нас вообще без данных!


Существует множество причин, по которым может потребоваться вычисление корреляции: это первый шаг EDA почти в каждой модели с несколькими переменными, он довольно широко используется при построении любого портфеля и так далее. Поэтому измерение этого числа как можно точнее совершенно необходимо!


Для иллюстрации давайте сгенерируем вторую переменную со «встроенной» корреляцией 0,4 к первой. Для этого мы воспользуемся своего рода моделью гауссовой смеси . Можно представить себе картину двух коррелирующих акций, которые имеют общий важный фактор риска, но вторая акция также подвержена серьезному фактору риска, которого нет у первой. Возьмем, к примеру, Google и Facebook: первым фактором может быть общее мнение о технологическом секторе, а вторым — конкуренция с конкурирующими социальными сетями.


 np.random.seed(2) # needed to ensure a fixed second series price_moves_2 = pd.Series(np.random.randn(100)) price_vec_2 = 50+(0.4*price_moves/3 + np.sqrt(1-0.4**2)*price_moves_2).cumsum() # all this math to ensure we get a 0.4 "theoretical" correlation with the first one


Проверим «базовую» эмпирическую корреляцию – то есть без НС и скачков.

 pd.concat([price_vec, price_vec_2], axis = 1).diff().corr().iloc[0,1] >0.4866403018044526


Это достаточно близко к «теоретической» корреляции — хорошо известно, что эмпирическое измерение корреляции подвержено довольно большому шуму.


NA без выбросов

В качестве следующего шага мы рассмотрим случай с NA, но без выбросов. Мы также сравним, что произойдет, если мы dropna до и после diff

 pd.concat([price_vec_na_simple, price_vec_2], axis = 1).diff().corr().iloc[0,1] # implicit dropna after diff >0.5022675176281746 pd.concat([price_vec_na_simple, price_vec_2], axis = 1).dropna().diff().corr().iloc[0,1] >0.5287405341268966


Оба результата довольно близки и не слишком далеки от «эмпирического» значения, которое мы получили ранее. Давайте проверим, что LOCF и вменение также работают нормально:

 pd.concat([price_vec_na_simple, price_vec_2], axis = 1).ffill().diff().corr().iloc[0,1] >0.5049380499525835 price_vec_na_simple_impute = price_vec_na_simple.copy() price_vec_na_simple_impute[price_vec_na_simple_impute.isna()] = price_vec_na_simple_impute.iloc[:90].mean() pd.concat([price_vec_na_simple_impute, price_vec_2], axis = 1).ffill().diff().corr().iloc[0,1] >0.4866728183859715


Сравнивая приведенные выше 4 результата, мы видим, что все методы работают достаточно хорошо. Возможно, тогда нам следует ожидать того же и в случае выброса?


NA с выбросами

Помните, чтобы оставаться последовательными, нам нужно подвергнуть второй ценовой ряд тем же ценовым шокам, что и первый, но без следующих NA. Возвращаясь к приведенному выше примеру, представьте себе какое-то крупное событие, вызывающее резкий скачок первого фактора риска, что в конечном итоге останавливает торговлю первым активом. Второй актив, конечно, тоже испытает это, но, возможно, в меньшей степени, и поэтому не будет никаких остановок и, следовательно, никаких NA.


 price_vec_na_2 = 50+(0.4*price_moves_na/3 + np.sqrt(1-0.4**2)*price_moves_2).cumsum()


Давайте еще раз сравним производительность всех наших методов —

 pd.concat([price_vec_na, price_vec_na_2], axis = 1).diff().corr().iloc[0,1] >0.6527112906179914 pd.concat([price_vec_na, price_vec_na_2], axis = 1).dropna().diff().corr().iloc[0,1] >0.7122391279139506


Это большая разница как с теоретической, так и с эмпирической ценностью! А как насчет LOCF и вменения?

 pd.concat([price_vec_na, price_vec_na_2], axis = 1).ffill().diff().corr().iloc[0,1] >0.33178239830519984 pd.concat([price_vec_na_impute, price_vec_na_2], axis = 1).dropna().diff().corr().iloc[0,1] >0.7280990594963112


Теперь мы наконец-то видим, чего стоит LOCF! Он явно превосходит все остальные методы!


Потенциальные ловушки + интерполяция

Конечно, это не 100% надежность. Во-первых, применяя LOCF, мы обеспечиваем значительное падение цен, когда исчезают недостающие данные. Если он совпадает с некоторыми выбросами на втором ценовом векторе, результаты могут существенно измениться. (*Упражнение для читателя — переверните знак движения цены price_vec_na_2[95] и проверьте, как это влияет на результаты). Не совсем ясно, «чисто ли» просто ввести это снижение цены вместо, например, интерполяции между ценовым пиком price_vec_na[91] и «нормальным» значением после этого price_vec_na[95] . Однако, особенно при «живом» использовании, интерполяция на самом деле невозможна! В конце концов, если сегодня день №93, как мы можем интерполировать, используя будущее значение, записанное в конце дня №95? Для исторического исследования - конечно, такой вариант остается, но тогда остается неясным, как его интерпретировать и использовать для реального прогнозирования! В заключение, интерполяция во временном измерении возможна, но несколько более сомнительна.


Выводы

Я попытался провести небольшое тематическое исследование, чтобы представить и объяснить, почему LOCF часто является наиболее привлекательным и простым вариантом для обработки недостающих данных в финансовых временных рядах.


Подводя итог, плюсы:

  • Привлекателен с точки зрения мартингейла/информационного потока.
  • Очень легко реализовать
  • Нет необходимости оптимизировать (в отличие от вменения)
  • Справляется с выбросами как минимум прилично


Некоторые минусы:

  • Потенциально может вызвать большие скачки в конце пропущенного периода.
  • Могут быть упущены некоторые нюансы динамики суставов при использовании для нескольких переменных.


Будучи количественным специалистом в магазине по торговле реквизитом, я использую его почти во всех своих исследованиях в качестве эффективной основы. Некоторые ситуации, конечно, требуют более тонких мер, но их немного, и они обычно не на 100% «решаются» ни одним из трех других упомянутых методов.