Почему ваш pandas работает в 3000 раз медленнее?

Наверное, все пользователи pandas знают, что многие действия можно сделать разными способами. Но далеко не каждый понимает, в чем разница, и почему их код может работать более чем в 3000 раз медленнее, чем у соседа. Давайте разберемся, в чём проблема.

Работа с библиотекой pandas для каждого аналитика данных, а тем более ML-инженера – дело совершенно тривиальное. Казалось бы, каждый читал и обрабатывал уже тысячи различных таблиц, находил метрики, строил по ним аналитику и все, используя стандартные методы, имеющиеся в библиотеке. Что можно узнать нового?

Мы на простом примере разберем несколько методов pandas, которые, к слову, известны почти всем уверенным аналитикам, и сравним их скорость работы. Кроме того, постараемся разобраться в причинах большой разницы в скорости. Зная, как работают методы pandas, работа с ними может открыться с другой стороны.

Очевидно, что если вы обрабатываете небольшое количество строк, то совершенно не обязательно, что бы ваш код отличался особой элегантностью или необычной техникой кодинга. Но если обрабатывать большие данные, то появляется проблема производительности и скорости обработки.

Для начала давайте создадим тестовый DataFrame с 1000000 строк:

df = pd.DataFrame(np.random.rand(1000000,2), columns=('a','b')) df.describe()
Видим, что все создалось верно и как ожидалось.
Видим, что все создалось верно и как ожидалось.

Давайте создадим новую метрику, представляющую собой сумму a и b. Сделаем мы это, конечно, разными способами:

1) .iterrows()

Данный метод проходится по всем строкам DataFrame в виде пар (index, Series), т.е. конвертирует каждую строку в Series object, из-за чего сильно страдает время работы. Кроме того, есть вероятность автоматической смены типа данных.

По скорости работы — это худший возможный метод

%%timeit -n 3 fin=[] for i,row in df.iterrows(): fin.append(row['a']+row['b'])
Почему ваш pandas работает в 3000 раз медленнее?
Почему ваш pandas работает в 3000 раз медленнее?

2) .loc[ ] или .iloc[ ]

Давайте теперь протестируем всем знакомые .loc и .iloc. Хоть и сами методы работают отлично, но они совершенно не предназначены для использования в цикле for. Суть работы приблизительно такая же, как и в прошлом методе. Однако, .iloc работает чуть быстрее чем .loc, т.к. метод обращается напрямую к месту, где строка хранится в памяти.

%%timeit -n 3 fin=[] for i in range(len(df)): fin.append(df['a'].loc[i]+df['b'].loc[i])
В 2 раза быстрее относительно .iterrows()
В 2 раза быстрее относительно .iterrows()
%%timeit -n 3 fin=[] for i in range(len(df)): fin.append(df['a'].iloc[i]+df['b'].iloc[i])
<p>Почти в 3 раза быстрее относительно .iterrows() </p>

Почти в 3 раза быстрее относительно .iterrows()

3) .apply()

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

%%timeit -n 3 df.apply(lambda row: row['a']+row['b'], axis=1).to_list()
<p>Еще в 2 раза быстрее относительно предыдущего метода </p>

Еще в 2 раза быстрее относительно предыдущего метода

4) .itertuples()

Метод по сути крайне похож на .iterrows() (даже названием), но с отличием, позволяющим работать быстрее – вместо конвертирования строк в Series object, он переводит их в кортежи, которые намного легче. Однако в коде теперь нужно обращаться к колонкам по их номеру, а не с помощью имени.

%%timeit fin=[] for row in df.itertuples(): fin.append(row[1]+row[2])
<p>Сократили время работы еще примерно 12 раз! </p>

Сократили время работы еще примерно 12 раз!

5) list comprehension

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

%timeit [a+b for a,b in zip(df['a'],df['b'])]
<p>Почти x4, в сравнении с .itertuples()</p>

Почти x4, в сравнении с .itertuples()

Когда используется цикл for, python выполняет итерацию по каждому объекту, в данном случае по строке df. Он идентифицирует класс объекта (массив numpy), оценивает, является ли применяемый метод (итерация) приемлемым. Затем он перебирает каждый объект в строке. Как только он находит объект, он идентифицирует тип данных и проверяет связанные методы. Затем он анализирует метод, применяемый к ним, он применяется (или выдает ошибку, если не может), а затем переходит к следующему объекту и начинает все сначала. Для каждого объекта выполняется множество операций, что фактически убивает скорость. Методы, описанные в этой статье, сокращают количество необходимых шагов.

Как избежать всего этого?

6) pandas vectorization

До этого момента мы просто суммировали 2 значения для каждой строки. Но pandas позволяет нам сгруппировать значения в вектор и просуммировать уже их. Векторизация работает с фреймом данных, который эффективно хранит типы объектов и пропускает или ускоряет весь поиск метаданных объекта и проверку ошибок. Решение от pandas интуитивно и выполняет векторизацию за нас. И, конечно же, такой метод работает быстрее.

%timeit (df['a'] + df['b']).to_list()
Ускорили работу еще в 10 раз!
Ускорили работу еще в 10 раз!

Можно ли придумать что-то быстрее? На самую малость:

7) numpy vectorization

Если вы работаете с массивами одного типа, использование массива numpy позволяет еще более эффективно извлекать метаданные, поскольку массивы numpy должны быть одного типа данных. Вот мы и получаем дополнительное увеличение скорости.

%timeit (df['a'].values + df['b'].values).tolist()
Почему ваш pandas работает в 3000 раз медленнее?
Почему ваш pandas работает в 3000 раз медленнее?

Давайте подытожим и составим топ худших решений, по сравнению с самым быстрым - numpy vectorization.

  • pandas vectorization = 1.09 * numpy vectorization
  • list comprehension= 10.5 * numpy vectorization
  • .itertuples() = 40 * numpy vectorization
  • .apply() = 480 * numpy vectorization
  • .iloc[ ] = 990 * numpy vectorization
  • .loc[ ] = 1544 * numpy vectorization
  • .iterrows() = 3120 * numpy vectorization

Т.е. представьте себе, один специалист, работающий правильно, может заменить 3120 специалистов, которые не знают различных нюансов работы с pandas или, хотя бы, не ознакомились с данной статьей.

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

1717
7 комментариев

Комментарий недоступен

5
Ответить

Ну кажется большинство пандасистов знают что векторные операции быстрей чем любая итерация по данным :)

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

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

2
Ответить

Комментарий недоступен

2
Ответить

Спасибо за материал!

1
Ответить

Спасибо за материал!
Для себя сделал вывод, что Pandas больше про понятное представление данных в табличной виде, что актуально для всяких ЮпитерНоутбуков и аналогов. "Под капотом" проекта от него толку нет. Являясь самоучкой, изначально использовал Панду в одном из своих проектов. Код писать не буду, но результаты были примерно следующие: 1:30 при использовании Pandas, 7 секунд на чистых списках, и 5 секунд на массиве Numpy. Алгоритмы везде были идентичные, с поправкой на тип данных. Теперь весь проект переписываю в Numpy. Да, не так удобно (приходится по индексам ориентироваться, а не по именам), но гораздо быстрее работает. А с Пандой пусть играются любители Юпитера и Матплотлиба

Ответить

Комментарий недоступен

3
Ответить