Оконные функции в пандах

Промежуточные итоги, возврат за период до даты и другие забавные вещи

В SQL есть удобная функция, называемая оконными функциями. Кстати, вам определенно следует знать, как работать с ними в SQL, если вы ищете работу аналитика данных. Pandas (немного поработав) позволяет нам делать то же самое. Посмотрим как.

Но во-первых, что такое оконные функции?

Оконные функции позволяют нам выполнять операции с данными данной строки и данными из другой строки, которая находится на указанном количестве строк от вас - это «значение количества строк на выходе» называется окном.

Например, предположим, что у вас есть курс акций за 10 дней:

Оконные функции позволяют нам выполнять вычисления среди значений указанного столбца. Например, я мог бы сравнить сегодняшнюю цену акций со вчерашней - тогда мне нужно, чтобы окно «1» смотрело назад. Это позволяет нам оконная функция. С другой стороны, если я хочу сравнить сегодняшнюю цену с ценой год назад, мне нужно окно «356» (при условии, что в вашем наборе данных есть выходные).

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

Выполнение оконных вычислений с помощью Pandas

Допустим, мы хотим рассчитать ежедневное изменение цены наших акций. Для этого нам нужно взять цену каждого дня, разделить ее на цену предыдущего дня и вычесть 1. Мы получаем наши данные в виде списка:

stock_list = [100, 98, 95, 96, 99, 102, 103, 105, 105, 108]

Списки не очень дружелюбны к математике, поэтому мы можем поместить наши данные в массив numpy (я изменил его так, чтобы он был массивом 9 на 1, чтобы его было легче просматривать и отображать):

In:
stock_array = np.array(stock_list)
(stock_array[1:]/stock_array[:-1] - 1).reshape(-1,1)
Out:
array([[-0.02      ],
       [-0.03061224],
       [ 0.01052632],
       [ 0.03125   ],
       [ 0.03030303],
       [ 0.00980392],
       [ 0.01941748],
       [ 0.        ],
       [ 0.02857143]])

Круто, мы получили отдачу. Нет необходимости в фреймах данных, верно? Не совсем так. Фреймы данных более универсальны, чем массивы numpy (которые оптимизированы для работы с числовыми данными). А разработчики pandas создали все эти изящные оконные методы, чтобы облегчить нам жизнь - им было бы грустно, если бы мы не воспользовались этим преимуществом.

Итак, давайте поместим наши цены на акции во фрейм данных:

stock_df = pd.DataFrame(stock_list, columns=['price'])

Мы можем использовать метод shift, чтобы получить цену за предыдущий день. Метод сдвига очень похож на функцию задержки SQL. Цифра 1 означает отставание на один день, что дает нам цену за предыдущий день:

stock_df['prev_price'] = stock_df.shift(1)

Теперь stock_df выглядит так:

   price  prev_price
0    100         NaN
1     98       100.0
2     95        98.0
3     96        95.0
4     99        96.0
5    102        99.0
6    103       102.0
7    105       103.0
8    105       105.0
9    108       105.0

Круто, теперь нам просто нужно разделить цену на prev_price и вычесть 1, чтобы получить дневную доходность:

In:
stock_df['daily_return'] = stock_df['price']/stock_df['prev_price']-1
print(stock_df)
Out:
   price  prev_price  daily_return
0    100         NaN           NaN
1     98       100.0     -0.020000
2     95        98.0     -0.030612
3     96        95.0      0.010526
4     99        96.0      0.031250
5    102        99.0      0.030303
6    103       102.0      0.009804
7    105       103.0      0.019417
8    105       105.0      0.000000
9    108       105.0      0.028571

Теперь самое интересное

Мы можем сделать гораздо больше, чем просто рассчитать доходность. Например, предположим, что мы хотим сравнить нашу дневную доходность со средней доходностью в расширяющемся окне, чтобы увидеть, как каждая доходность сравнивается со средним историческим значением. Вы можете подумать, почему бы просто не вычислить среднее значение всех значений в ежедневном return_column и не использовать его? Ответ - утечка данных.

При анализе временных рядов, когда мы пытаемся спрогнозировать будущее, нам нужно быть очень осторожными в отношении того, что можно было наблюдать, а что нельзя было наблюдать в конкретную дату. Например, в день 5 нашего набора данных, мы можем наблюдать только первые 5 цен: 100, 98, 95, 96, 99. Таким образом, если мы тестируем функции, чтобы сделать прогноз на 6 день, мы не можем сравнить доходность 5 дня, равную 3,03. % со средним дневным изменением за весь период, потому что в день 5 мы еще не наблюдали дни с 6 по 9.

Расширение и сворачивание окон

Вот тут-то и появляется расширяющееся окно. Если вы не знакомы с расширением и сворачиванием окон, на следующем рисунке показано, что они из себя представляют. При расширении окна мы вычисляем показатели в расширяющемся режиме - это означает, что мы включаем в расчет все строки до текущей. Скользящее окно позволяет нам вычислять показатели на непрерывной основе - например, скользящее (3) означает, что мы используем текущее наблюдение, а также два предыдущих, чтобы вычислить желаемую метрику.

Обоснование использования расширяющегося окна заключается в том, что с каждым днем ​​мы получаем новую цену и еще одно ежедневное изменение, которое мы можем добавить к нашему среднему расчету. Это новая информация, которую мы должны зафиксировать в наших расчетных показателях. Мы можем сделать это с помощью следующего кода (я также добавил трехдневное скользящее окно для развлечения).

stock_df['expand_mean']=stock_df['daily_return'].expanding().mean()
stock_df['roll_mean_3']=stock_df['daily_return'].rolling(3).mean()

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

   price  prev_price  daily_return  expand_mean  roll_mean_3
0    100         NaN           NaN          NaN          NaN
1     98       100.0     -0.020000    -0.020000          NaN
2     95        98.0     -0.030612    -0.025306          NaN
3     96        95.0      0.010526    -0.013362    -0.013362
4     99        96.0      0.031250    -0.002209     0.003721
5    102        99.0      0.030303     0.004293     0.024026
6    103       102.0      0.009804     0.005212     0.023786
7    105       103.0      0.019417     0.007241     0.019841
8    105       105.0      0.000000     0.006336     0.009740
9    108       105.0      0.028571     0.008807     0.015996

Обратите внимание, что в день 1 expand_mean и daily_return равны - это обязательно так, потому что мы вычисляем расширяющееся среднее только с одной дневной доходностью в день 1. Кроме того, в день 3, когда у нас, наконец, есть достаточно данных для расчета скользящего среднего значения за 3 дня. , первое значение roll_mean_3 равно expand_mean. Это тоже имеет смысл - на 3-й день наше расширяющееся среднее также рассчитывается с использованием доходности за последние 3 дня.

Вот график (и код) дневной доходности акций, а цифра 2 означает, что мы рассчитали:

plt.subplots(figsize=(8,6))
plt.plot(stock_df['daily_return'], label='Daily Return')
plt.plot(stock_df['expand_mean'], label='Expanding Mean')
plt.plot(stock_df['roll_mean_3'], label = 'Rolling Mean')
plt.xlabel('Day')
plt.ylabel('Return')
plt.legend()
plt.show()

Общее количество положительных дней

Помимо среднего, мы можем применять и другие функции. Допустим, к нам подходит наш босс и говорит: «Я хочу, чтобы вы отслеживали, сколько дней эти акции были в цене».

Это можно сделать с помощью расширяющегося объекта и метода sum (для хранения промежуточной суммы). Сначала нам нужно добавить столбец в наш фрейм данных, чтобы обозначить, выросли ли акции в тот день или нет. Мы можем воспользоваться преимуществом метода apply (который применяет функцию к каждой строке фрейма данных или ряда). Мы можем либо определить функцию, чтобы дать применить, либо использовать лямбда-функцию - я выбрал лямбда-функцию (меньше строк кода), которая принимает каждый возврат и возвращает 1, если он положительный, и 0, если он отрицательный.

stock_df['positive'] = stock_df['daily_return'].apply(lambda x: 1 if x>0 else 0)

Когда у нас есть «положительный» столбец, мы можем применить к нему расширяющееся окно и метод sum (поскольку каждый положительный день обозначается 1, нам просто нужно сохранить текущую сумму из числа единиц):

stock_df['num_positive'] = stock_df['positive'].expanding().sum()

И фрейм данных, который мы отправим нашему боссу, выглядит так:

   price  daily_return  num_positive
0    100           NaN           0.0
1     98     -0.020000           0.0
2     95     -0.030612           0.0
3     96      0.010526           1.0
4     99      0.031250           2.0
5    102      0.030303           3.0
6    103      0.009804           4.0
7    105      0.019417           5.0
8    105      0.000000           5.0
9    108      0.028571           6.0

Так что на данный момент для акции 6 положительных дней. Но в будущем по мере того, как мы будем собирать больше цен, наша промежуточная сумма будет обновляться вместе с ней. Отлично, наш босс должен быть доволен этим.

Спасибо за прочтение! Приветствую всех и оставайтесь в безопасности и здоровье.

Если вам понравилась эта статья и мои статьи в целом, пожалуйста, поддержите мое письмо, подписавшись на Medium по моей реферальной ссылке здесь. Спасибо!