Как эффективно отменить операции заполнения или заполнения

Итак, вы сделали это, у вас есть хороший временной ряд с полезными функциями в pandasDataFrame. Возможно, вы использовали pd.ffill() или pd.bfill(), чтобы заполнить пустые временные шаги, используя предыдущее или следующее значение, и выполнить анализ или извлечение признаков для всей серии.

Чем вы занимаетесь?

Мы столкнулись с этой проблемой сегодня в Мансе, где мы сохраняли сотни (если не тысячи) ненужных строк после завершения нашего конвейера предварительной обработки.

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

TL; DR: переходите к самой сути, чтобы найти наиболее эффективный способ!

Чтобы проксировать наши реальные данные, я сделал этот простой генератор примеров, который будет выдавать заполненные временные ряды. Столбец date здесь - это столбец ежедневных меток времени, которые будут сгенерированы и заполнены для завершения (посредством повторной выборки).

В качестве простого примера предположим, что у нас есть пользователь с учетной записью, созданной в начале 2018 года. Этот пользователь использует ее в качестве дополнительной учетной записи, поэтому транзакция проводится только каждые два дня или около того. За три года у них есть только 500 дней, когда баланс действительно меняется.

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

Итак, если у меня был баланс 5 евро в четверг вечером и 500 евро в воскресенье вечером, то я знаю, что у меня также было 5 евро в пятницу вечером и в субботу вечером, поскольку наблюдения происходят только при изменении баланса.

Имея около 500 случайных наблюдений за три года, если мы переделаем выборку дат, чтобы получить полный дневной DataFrame, мы получим от 500 строк примерно до ~ 1k (365 * 3).

Почему бы просто не удалить дубликаты?

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

В зависимости от того, насколько малы ваши временные шаги и насколько разрежены ваши наблюдения, в вашем DataFrame может быть много повторяющихся или пустых строк.

Итак, как сжать его обратно?

  1. Первое наивное решение:

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

В этом случае я просто перебираю строки в DataFrame и нахожу все индексы, в которых происходит изменение между временным шагом i и i-1.

Это работает, но iterrows не быстро.

Посчитав блок кода с помощью %%timeit и моего небольшого сгенерированного DataFrame, я получаю:

2.39 s ± 794 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Мы можем лучше!

2. Попытка использовать reduce:

Итак, iterrows работает медленно, что мы можем сделать, чтобы ускорить его. Моя вторая мысль заключалась в том, чтобы работать напрямую с массивом значений numpy и использовать reduce из стандартной библиотеки, чтобы эффективно обрабатывать две строки за раз.

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

Затем мы можем снова создать DataFrame с новыми значениями.

Это было уже намного быстрее (улучшение в 100 раз)!

20.4 ms ± 6.26 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)

Но можем ли мы сделать это лучше?

3. Вернуться к pandas:

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

Это значительно упрощает код. Я все еще хочу сравнить только все столбцы, исключая «дату», поэтому я сдвигаю выбор столбцов на один и сравниваю с исходным. С ~(...).all(axis=1) я могу сравнить две строки, чтобы выбрать строки в исходном df, которые не совпадают с предыдущей строкой.

Это даже быстрее (неудивительно, поскольку мы больше не выполняем итерации)! Это в 4 раза быстрее, чем при использовании reduce подхода, и в 400 раз быстрее, чем наивный iterrows!

4.61 ms ± 319 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

Он также имеет то преимущество, что умещается в 2 строчки кода!

4. Чтобы работать еще быстрее, мы можем реализовать это в numpy:

По сути, это то же самое, что и сдвиг панд.

Мы создаем сдвиг, используя функцию np.roll и устанавливая первый элемент как nan. Опять же, мы сравниваем все значения со сдвинутой строкой массива.

Это снова быстрее!

973 µs ± 29 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

Почти в 5 раз быстрее, чем предыдущий подход, и в 2000 раз быстрее, чем наивный подход!

Примечание. Для всех решений предполагалось, что DataFrame будет отсортирован по столбцу временного шага (в данном случае «дата»). В противном случае эти решения не сработали бы.

Вывод

Избегайте iterrows, используйте shift.

В конце концов, я выбрал решение №3, не самое эффективное, поскольку оно требует меньше строк кода и, вероятно, более понятное. Иногда простота решения важнее, чем его производительность.

Я уверен, что мы могли бы еще больше повысить производительность, и я призываю вас, читатель, попробовать это!

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