Pandas — это мощная библиотека для анализа данных, API которой обладает широкими функциональными возможностями. Этот API позволяет решить любую задачу, связанную с обработкой данных, несколькими способами. Некоторые из подходов к решению задач лучше других. Часто бывает так, что пользователи pandas узнают о подходах, не отличающихся особой эффективностью, привыкают к ним и постоянно их применяют. Этот материал посвящён разбору четырёх анти-паттернов pandas и рассказу о приёмах работы, которые стоит использовать вместо них. Автор черпал вдохновение из многих источников, ссылки на которые даны в статье. В частности — из замечательной книги Effective Pandas. Здесь, для показа примеров удачных и неудачных способов работы в pandas, я пользуюсь набором данных Netflix с Kaggle. Он содержит сведения о почти 6000 сериалов и фильмов Netflix. Эти сведения разбиты на 15 характеристик, представленных данными различных типов. Мы рассмотрим следующие анти-паттерны: Использование мутаций вместо цепочек методов. Применение циклов for при работе с объектами pandas DataFrame. Неоправданное использование .apply вместо np.select, np.where и .isin. Использование неподходящих типов данных. Большинство тех, кто использует pandas на практике, сначала осваивают не самый удачный подход к обработке данных. Он предусматривает пошаговое изменение (мутацию) объектов DataFrame, выполняемое с помощью набора отдельных операций. Избыточные мутации объектов DataFrame могут приводить к проблемам. У этого есть несколько причин: Это приводит к расточительному использованию памяти из-за создания глобальных переменных (это особенно справедливо в тех случаях, когда на каждом шаге обработки данных создают новый объект DataFrame). Код получается громоздким, его тяжело читать. Этот код подвержен ошибкам. В частности, в блокнотах Jupyter, где может случиться так, что не будет строго выдерживаться порядок выполнения шагов обработки данных. При таком подходе часто возникают раздражающие и известные своей запутанностью уведомления SettingWithCopyWarning. И, кстати, передача операции параметра inplace=True тут не помогает. Вместо последовательных мутаций объектов DataFrame лучше будет трансформировать их, объединяя в цепочки методы pandas. В следующем примере сравнивается подход, основанный на мутациях, и аналогичный подход, в котором используются цепочки методов. Тут мы выполняем различные операции по трансформации данных, занимаемся созданием колонок. Мы хотим исследовать гипотезу, в соответствии с которой сериалы с большим количеством сезонов обладают более высокими рейтингами популярности, чем те, в которых сезонов меньше. При объединении методов в цепочку выполняется однопроходная трансформация DataFrame в соответствии с многошаговой процедурой. Это гарантирует полное и правильное применение каждого использованного метода pandas, а значит — снижает риск возникновения ошибок. Улучшается читабельность кода — каждая строка чётко представляет отдельную операцию. (Обратите внимание на то, что многие средства для форматирования Python-кода разрушают эту структуру; чтобы этого избежать — оформляйте фрагменты pandas-кода в виде блоков, ограниченных '#fmt: off' и '#fmt: on'). Объединение методов в цепочки покажется естественным для пользователей R, знакомых с оператором magrittr %>%. Иногда может понадобиться выполнить сложное преобразование данных, которое нельзя красиво реализовать, пользуясь готовыми методами, объединяемыми в цепочки. В подобных ситуациях можно прибегнуть к методу pandas .pipe. Он позволяет абстрагировать сложные трансформации объектов DataFrame в виде самостоятельно определённых функций. В следующем примере мы преобразуем столбец, содержащий строковое представление списка кодов стран, в три отдельных столбца, содержащих первые три кода. Метод .pipe позволил встроить это сложное преобразование в цепочку методов, не усложняя код, описывающий эту цепочку. Начать можно с простого комментирования интересующих вас строк (то есть — методов) в коде, описывающем цепочку. О более продвинутых способах отладки подробно написано в 35 главе книги Effective Pandas. Для этого можно, применив .pipe, добавить в нужное место цепочки нижеприведённую функцию, использующую IPython.display.display. Это позволит вывести DataFrame на промежуточном этапе обработки и при этом не помешать работе цепочки методов. Вот эта функция: Для того чтобы сохранить объект DataFrame, находящийся в промежуточном состоянии, и при этом не прервать работу цепочки методов, можете, воспользовавшись .pipe, встроить в цепочку эту функцию. Если при работе с pandas используются циклы for — это признак плохого кода. От таких циклов надо избавляться. Это касается и встроенных методов-генераторов DataFrame.iterrows() и DataFrame.itertuples(). Вот две причины, в соответствии с которыми в pandas лучше не пользоваться циклами: При таком подходе код получается неоправданно длинным, неуклюжим и несовместимым с цепочками методов. Альтернативные подходы позволяют достичь того же результата путём применения более компактного кода и при этом поддерживают объединение методов в цепочки. Перебор отдельных строк — это медленно. Векторизованные операции обладают лучшей производительностью — особенно при обработке столбцов, содержащих данные числовых типов. Представим, что нам надо использовать недавно созданный столбец prod_country1 для создания ещё одного столбца. Его содержимое указывает на то, входит ли страна из prod_country1 в топ-3/10/20 стран по частоте их появления. В следующем фрагменте кода показан нерациональный подход, где используется цикл for, а так же пример более чистого решения задачи, основанного на .apply. Продемонстрированный здесь паттерн применения .apply представляет собой гибкий механизм, использование которого приводит к появлению понятного кода, среди преимуществ которого можно отметить возможность использования его в цепочках методов. Во всех случаях, когда обрабатывать объекты DataFrame можно с помощью цикла for, .apply позволяет изящнее добиться тех же результатов. Но .apply — это не векторизованная операция. В её недрах используется перебор строк DataFrame. В результате, если только вы не работаете со столбцами типа object (обычно — со строками), вместо .apply стоит использовать более производительные механизмы. Это ведёт нас к анти-паттерну №3… Как только pandas-программист узнаёт про .apply, часто случается так, что он начинает применять эту конструкцию буквально повсюду. Это, само по себе, не всегда говорит о проблемах, так как .apply позволяет создавать понятный код, обладающий, на наборах данных умеренных размеров, адекватной производительностью. Но при работе с большими наборами данных, при выполнении «тяжёлых» вычислений .apply может быть на порядки медленнее более эффективных, векторизованных механизмов. В этом материале хорошо показано преимущество подобных оптимизированных механизмов перед .apply. Есть один полезный паттерн — комбинация np.select и .isin, который можно использовать вместо .apply. Вот как выглядит его применение к нашей задаче об оценке стран. Тут нет такого же впечатляющего роста производительности, как в вышеупомянутой статье, так как мы работаем со столбцами типа object. Кроме того, большая часть вычислений связана с подсчётом значений, а это в обоих вариантах примеров выполняется одинаково. Но, несмотря на это, новый код примерно в 2 раза быстрее, чем тот, где используется .apply. Если же вычисление количества значений размещено внутри функции, используемой с .apply, то код оказывается в 500 раз медленнее, чем при совместном использовании np.select и .isin. В дополнение к улучшению производительности, размещение вычисления количества значений в функции, где совместно используются np.select и .isin, позволяет выполнить эти трансформации в виде «цепочечной» операции без глобальных зависимостей. Обратите внимание на то, что список условий np.select ведёт себя так же, как блок if elif else, осуществляя выбор элемента на основании первого встреченного выражения, возвращающего True. В случаях, когда выбирать нужно лишь из двух вариантов, можно прибегнуть к альтернативе — к методу np.where. Например, представьте, что мы знаем о том, что в значения оценок IMDB вкралась ошибка, и нам надо вычесть 1 из всех оценок сериалов и фильмов, вышедших после 2016 года. Сделать это можно так: Применение метода .apply, если только речь не идёт о работе со столбцами типа object, будет менее производительным, чем использование других подходов. Вместо .apply можно воспользоваться np.select, np.where и .isin, что позволит производить эффективные, векторизованные преобразования данных. По непонятной причине pd.where работает немного не так, как np.where. Но этот метод может пригодиться в том случае, если нужно оставить в столбце все значения, соответствующие некоему условию, заменив при этом всё остальное (например — оставить значения, входящие в топ-5, а всё остальное установить в значение 'other'). Подбор наиболее адекватных типов данных для столбцов объектов DataFrame позволяет улучшить производительность программ и снизить потребление памяти. Работая с большими наборами данных, можно привести значения типов float64 и int64, используемых по умолчанию, к их более компактным эквивалентам, таким, как float16 и int8. Применив такое преобразование к столбцам, в которых оно не вызовет потери данных, можно добиться значительных улучшений. Но самое страшное несовпадение типов, от которого стоит чистить pandas-код — это использование строк, а не pandas.Categorical, для описания категориальных признаков. Предположим, имеется столбец с категориальными данными, содержащий небольшое количество отличающихся значений. Если преобразовать тип этого столбца с используемого по умолчанию object, к типу category, это часто способно привести к примерно 100-кратному улучшению использования памяти, и к 10-кратному росту скорости вычислений. В следующем примере показано выполнение подобного преобразования с использованием методов, объединённых в цепочку. Если ограничения, связанные с производительностью и/или с памятью, причиняют вам неудобства при работе с большими наборами данных — подумайте об оптимизации типов данных в ваших DataFrame. Особенно это касается преобразования типов object к типу categorial в применении к тем столбцам, для которых это имеет смысл. Здесь можно найти сведения о преимуществах использования оптимизированных типов данных. В этом материале я продемонстрировал четыре анти-паттерна pandas и рассказал об альтернативных подходах, которые стоит применять вместо этих анти-паттернов. Нижеприведённый пример кода иллюстрирует совместное использование этих рекомендованных подходов, их объединение в гармоничную последовательность действий. Здесь показано вычисление средней уточнённой оценки сериалов, зависящей от того, в какую группу входит первая из стран-производителей сериала. Принятие этих практик позволяет выполнять сложные трансформации и преобразования данных в единственном выражении, представляющем собой последовательность методов, объединённых в цепочку. Код получается производительным, читабельным, его просто поддерживать и расширять. Если вы пока ещё не пишете pandas-код таким способом — рекомендую вам его попробовать! Мы в wunderfund.io занимаемся высокочастотной алготорговлей с 2014 года. Высокочастотная торговля — это непрерывное соревнование лучших программистов и математиков всего мира. Присоединившись к нам, вы станете частью этой увлекательной схватки. Мы предлагаем интересные и сложные задачи по анализу данных и low latency разработке для увлеченных исследователей и программистов. Гибкий график и никакой бюрократии, решения быстро принимаются и воплощаются в жизнь. Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров. Присоединяйтесь к нашей команде.