Закажи экспресс-аудит своего дела онлайн всего за 199 ₽
и получи рекомендации по улучшению - Жми сюда !

Я переписал реальный рабочий процесс обработки данных на языке Polar. Pandas не имел ни единого шанса.

С 61 секунды до 0,20 секунды — и неожиданный сдвиг в мышлении.

Делиться

301b9cd1237876bef7c900e5ca4ea083
Создано с помощью Gemini AI

Честно говоря, я целенаправленно не искал очки Polar.

В последнее время я немного увлекся оптимизацией Pandas. Сначала я написал о том, почему следует перестать писать циклы в Pandas и вместо этого мыслить в столбцовом режиме.

Затем я углубился в анализ реальных рабочих процессов, исправил ошибки векторизации и в итоге сократил время выполнения конвейера с 61 секунды до 0,33 секунды, используя только улучшенные библиотеки Pandas и NumPy. Это удивило даже меня.

Так что с Pandas у меня всё было хорошо. Мне казалось, что я наконец-то понял, как правильно им пользоваться.

Затем кто-то оставил комментарий к одному из моих постов. Что-то вроде: «Вы пробовали Polars? Они созданы именно для таких целей».

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

Этого комментария было достаточно, чтобы вывести меня из себя.

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

Результаты меня удивили. Удивили не только показатели скорости, но и то, чему Polars незаметно учит о том, как на самом деле работают конвейеры обработки данных.

Разве панд недостаточно?

Вполне резонный вопрос.

В своей последней статье я взял медленный конвейер обработки данных Pandas и оптимизировал его до 0,33 секунды. Векторизованные операции, корректные типы данных, отсутствие ненужных копирований. Результаты, честно говоря, превзошли мои ожидания.

Так зачем же мы вообще говорим о полюсах?

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

Кроме того, Polars построен на совершенно иной основе, чем Pandas. Он по умолчанию использует все ядра вашего процессора. Он управляет памятью по-другому. И он предлагает способ написания конвейеров обработки данных, который, как только вы его поймете, изменит ваше представление обо всем процессе.

Оптимизированная версия Pandas впечатляет. Но у неё всё ещё есть предел. Эта статья о том, что находится по ту сторону этого предела.

Рабочий процесс

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

Если вы хотите сгенерировать его самостоятельно, вот код настройки:

 import pandas as pd import numpy as np import time np.random.seed(42) n = 1_000_000 regions = ['north', 'south', 'east', 'west'] categories = ['electronics', 'clothing', 'furniture', 'food', 'sports'] statuses = ['completed', 'returned', 'pending', 'cancelled'] df = pd.DataFrame({ 'order_id': np.arange(1000, 1000 + n), 'order_date': pd.date_range(start='2022-01-01', periods=n, freq='1min'), 'region': np.random.choice(regions, size=n), 'category': np.random.choice(categories, size=n), 'sales': np.random.randint(100, 10000, size=n), 'quantity': np.random.randint(1, 20, size=n), 'discount': np.round(np.random.uniform(0.0, 0.5, size=n), 2), 'status': np.random.choice(statuses, size=n), }) df.to_csv('large_sales_data.csv', index=False)

Конвейер обработки данных, который мы используем, довольно прост. Это типичная ситуация для реальных рабочих процессов:

  • Заранее определите типы данных.
  • Рассчитайте чистую выручку с каждого заказа.
  • Отмечать заказы высокой стоимости
  • Совокупная чистая выручка по регионам

Просто. Знакомо. И уже оптимизировано в Pandas из предыдущей статьи, что делает его идеальным эталоном для Polars.

Версия для панд

Я не буду показывать здесь простой код Pandas. Я уже сделал это в своей предыдущей статье — в версии с тремя вызовами .apply() `, которая заняла 61 секунду на том же наборе данных. Если вы её ещё не читали, стоит взглянуть.

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

 import pandas as pd import numpy as np import time df = pd.read_csv('large_sales_data.csv') start = time.time() # Fix data types upfront df['region'] = df['region'].astype('category') df['category'] = df['category'].astype('category') df['status'] = df['status'].astype('category') # Vectorized revenue calculation df['net_revenue'] = df['sales'] * df['quantity'] * (1 - 0.075) # Vectorized flagging df['order_flag'] = np.where(df['net_revenue'] > 50000, 'high', 'low') # Aggregation result = df.groupby('region')['net_revenue'].sum() end = time.time() print(f"Pandas runtime: {end - start:.2f} seconds") print(result)

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

И каков результат?

 Pandas runtime: 0.31 seconds

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

Вот что нам сейчас предстоит выяснить.

Установка Polars и First Impressions

Начать было очень просто. Если вы, как и я, используете Google Colab, вам понадобится всего одна строка кода:

 !pip install polars import polars as pl print(pl.__version__)
 1.35.2

Готово. Никаких проблем с окружением, никаких конфликтов зависимостей. Уже одно это было хорошим началом.

Но затем я открыл документацию Polar и сразу же кое-что заметил. Синтаксис показался достаточно знакомым — DataFrames, столбцы, фильтрация — но подход к операциям показался мне другим.

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

Я ещё не до конца это понимал. Но скоро пойму.

Ещё один момент, который сразу привлёк моё внимание, — это концепция ленивого и немедленного выполнения. В Pandas каждая строка кода выполняется в момент её написания. Polars же предоставляет выбор: вы можете выполнять запрос немедленно, как в Pandas, или сначала составить полный план выполнения запроса и позволить Polars оптимизировать его, прежде чем что-либо выполнять.

Я ещё не знал, что это значит на практике. Но я постоянно видел это в документации. Поэтому я решил, что лучший способ разобраться — просто переписать свой конвейер и посмотреть, что получится.

Версия для нетерпения

 import polars as pl import time start = time.time() result = ( pl.read_csv('large_sales_data.csv') .with_columns([ (pl.col('sales') * pl.col('quantity') * (1 - 0.075)).alias('net_revenue') ]) .with_columns([ pl.when(pl.col('net_revenue') > 50000) .then(pl.lit('high')) .otherwise(pl.lit('low')) .alias('order_flag') ]) .group_by('region') .agg(pl.col('net_revenue').sum()) ) end = time.time() print(f"Polars Eager runtime: {end - start:.2f} seconds") print(result)
 Polars Eager runtime: 0.83 seconds

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

Здесь всё выстроено в единое целое от начала до конца. Вы описываете весь конвейер целиком.

Позвольте мне быстро разобрать синтаксис, поскольку некоторые его части могут показаться незнакомыми:

  • pl.read_csv() — считывает CSV-файл, принцип тот же, что и у pd.read_csv() . Ничего удивительного.
  • .with_columns([...]) — так Polars добавляет или преобразует столбцы. Эквивалент df['new_col'] = ... в Pandas. Вы можете вычислить несколько столбцов за один вызов.
  • pl.col('sales') — так в Polar-таблицах осуществляется ссылка на столбец. Вместо df['sales'] вы пишете pl.col('sales') . Вы не получаете данные напрямую — вы описываете операцию над этим столбцом. Это различие важнее, чем кажется.
  • .alias('net_revenue') — просто присваивает имя результату. Как будто говорите: «Назовите этот новый столбец net_revenue».
  • pl.when(...).then(...).otherwise(...) — версия np.where() от Polars. Читается почти как обычный английский: если это условие истинно, вернуть это значение, в противном случае — то.
  • .group_by('region').agg(...) — та же концепция, что и в Pandas .groupby() . Группируем данные, затем определяем агрегацию. Другой синтаксис, та же идея.

А вот в чем дело. Эта ускоренная версия выполнилась за 0,83 секунды. Это даже медленнее, чем наша оптимизированная версия Pandas, которая выполнилась за 0,31 секунды. Если бы я остановился на этом, я бы вообще отказался от Polars.

Но я продолжал читать документацию. И обнаружил нечто, называемое ленивой оценкой.

Ленивая версия

 start = time.time() result = ( pl.scan_csv('large_sales_data.csv') .with_columns([ (pl.col('sales') * pl.col('quantity') * (1 - 0.075)).alias('net_revenue') ]) .with_columns([ pl.when(pl.col('net_revenue') > 50000) .then(pl.lit('high')) .otherwise(pl.lit('low')) .alias('order_flag') ]) .group_by('region') .agg(pl.col('net_revenue').sum()) .collect() ) end = time.time() print(f"Polars Lazy runtime: {end - start:.2f} seconds") print(result)
 Polars Lazy runtime: 0.20 seconds

Найдите два отличия от версии с активным использованием. pl.read_csv() стало pl.scan_csv() — это указывает Polars пока ничего не загружать, а только начать строить план запроса. И в конце был добавлен .collect() — именно там вы сообщаете Polars: «Хорошо, теперь выполните все».

Два изменения. Вот и всё.

И вот так, 0,83 секунды превратились в 0,20 секунды.

Функция Polar lazy работает на 35% быстрее, чем наш уже оптимизированный конвейер Pandas. При этом мне не пришлось проводить ручную оптимизацию, профилировать процессы и заранее знать, какие операции являются узкими местами.

Полярные планеты сами это поняли.

Вот тогда я и начал обращать внимание.

Первое изменение в ментальной модели — Ленивое против энергичного выполнения

Именно эта работа изменила мое представление о конвейерах обработки данных.
В Pandas каждая строка кода выполняется в момент ее написания. Вы присваиваете значение столбцу — выполняется. Вы фильтруете DataFrame — выполняется. Вы группируете и агрегируете — выполняется.

Каждая операция независима, выполняется мгновенно и не зависит от того, что происходит до или после неё.

Это неспешное выполнение. Оно интуитивно понятно. Оно кажется естественным, потому что соответствует тому, как мы представляем себе пошаговое написание кода.

Polars предоставляет вам выбор.

Когда вы используете pl.scan_csv() вместо pl.read_csv() , вы говорите Polars: пока ничего не выполняйте. Просто начните строить план. Каждый .with_columns() , каждый .filter() , каждый .group_by() , который вы добавляете после этого, не выполняется — он записывается. Вы описываете то, что хотите, а не запускаете это.

Затем, когда вы вызываете метод .collect() в конце, Polars берет весь этот план, анализирует его целиком, оптимизирует и выполняет за один эффективный проход.

Подумайте об этом так:

  • В Pandas все делается как пошаговое следование рецепту. Нарежьте лук. Положите его на сковороду. Добавьте чеснок. Каждая инструкция выполняется немедленно, по порядку, одна за другой.
  • Ленивый подход Polars подобен повару, который сначала внимательно изучает весь рецепт, находит наиболее эффективный способ приготовления всех ингредиентов, а затем выполняет все действия в оптимальной последовательности. Результат тот же. Меньше потраченных впустую усилий.

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

Вы можете увидеть план запроса, который Polars формирует перед выполнением.

Выполните это:

 lazy_query = ( pl.scan_csv('large_sales_data.csv') .with_columns([ (pl.col('sales') * pl.col('quantity') * (1 - 0.075)).alias('net_revenue') ]) .with_columns([ pl.when(pl.col('net_revenue') > 50000) .then(pl.lit('high')) .otherwise(pl.lit('low')) .alias('order_flag') ]) .group_by('region') .agg(pl.col('net_revenue').sum()) ) print(lazy_query.explain())

Вызов метода .explain() ` наглядно показывает, что именно планирует сделать Polars, прежде чем это произойдёт. Это ход мыслей оптимизатора, ставший видимым.
Этого просто нет у панд.

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

Это не просто небольшая разница. Это принципиально иные взаимоотношения между вами и вашим конвейером обработки данных.

Вторая смена ментальной модели — Оптимизация запросов

Как только я понял принцип ленивой оценки, я начал задумываться — хорошо, но что именно оптимизирует Polars? Что именно он делает по-другому, «под капотом»?

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

Проталкивание предикатов

Допустим, вы добавили фильтр в наш конвейер обработки заказов — вам нужны только завершенные заказы:

 result = ( pl.scan_csv('large_sales_data.csv') .with_columns([ (pl.col('sales') * pl.col('quantity') * (1 - 0.075)).alias('net_revenue') ]) .filter(pl.col('status') == 'completed') .group_by('region') .agg(pl.col('net_revenue').sum()) .collect() )

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

Функция отложенного выполнения запроса в Polar работает более эффективно. Она анализирует весь план запроса и говорит: здесь есть фильтр. Давайте применим этот фильтр как можно раньше — в идеале до загрузки данных или сразу после. Таким образом, ресурсоемкие вычисления будут выполняться только над теми строками, которые действительно важны.

Это метод переноса предикатов вниз. Фильтр перемещается в самую раннюю возможную точку конвейера. Обрабатывается меньше данных. Используется меньше памяти. Результат быстрее.

Проекционная обрезка

Та же идея, но для столбцов, а не для строк.

Наш CSV-файл содержит восемь столбцов. Но для получения конечного результата нам нужны только sales , quantity , region и status . Polars анализирует план выполнения запроса, определяет, какие столбцы действительно необходимы для получения конечного результата, и загружает только их. Остальные полностью игнорируются.

В Pandas сначала загружается всё необходимое, а потом уже определяется, что нужно. Polars же определяет свои потребности ещё до загрузки чего-либо.
Эти две оптимизации — перенос предикатов вниз и обрезка проекций — это то, что делает оптимизатор запросов к базе данных. Если вы когда-либо писали SQL-запросы и задавались вопросом, почему база данных работает быстро даже с огромными таблицами, то это одна из главных причин.

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

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

Сдвиг в ментальной модели №3 — Столбчатая память

Есть ещё один аспект, объясняющий скорость работы Polars. И он находится на уровне ниже вашего кода.

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

Но когда вам нужно вычислить что-то по всему столбцу — например, суммировать все значения выручки — Pandas приходится просматривать память, извлекая соответствующее значение из каждой строки по очереди.

Polars использует столбцовый формат памяти, называемый Apache Arrow. Вместо того чтобы хранить строки вместе, он хранит столбцы вместе. Все значения выручки находятся рядом друг с другом в памяти. Все значения регионов находятся рядом друг с другом. Когда вам нужно что-то вычислить над столбцом, все необходимое уже находится в одном непрерывном блоке памяти.

Почему это важно? По двум причинам.

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

Во-вторых, столбцовое хранение данных означает, что Polars обращается только к необходимым столбцам. Если ваш DataFrame содержит 20 столбцов, а для операции требуется только 3, Polars будет работать с этими 3 столбцами в памяти. Pandas загрузит весь DataFrame независимо от этого.

Для получения этой выгоды вам ничего не нужно делать. Так устроена система Polars.

Представьте себе это так: Pandas — это как картотека, в каждом ящике которой хранится вся информация об одном клиенте — его имя, заказы, история платежей, всё вместе.

Polars похож на картотеку, где в одном ящике хранятся все имена, в другом — все заказы, а в третьем — вся история платежей. Если вам нужно проанализировать историю платежей всех клиентов, Polars открывает один ящик. Pandas открывает каждый ящик.

Это столбцовая модель памяти. И в сочетании с ленивой оценкой и оптимизацией запросов именно поэтому Polar может сделать за 0,20 секунды то, что Pandas занимает 0,31 секунды — даже после тщательной ручной оптимизации Pandas.

Где панды по-прежнему побеждают

Хочу сразу сказать: эта статья — не некролог пандам.

Даже после всего этого упражнения, всё ещё встречаются ситуации, когда я без колебаний выбираю Pandas.

Для быстрого исследования и анализа по запросу Pandas трудно превзойти. Синтаксис знаком, экосистема огромна, и когда вы просто изучаете набор данных, пытаясь его понять, разница в производительности не имеет значения. Вы не запускаете конвейер — вы просто размышляете вслух.

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

Для визуализации и интеграции Pandas глубоко интегрирован в экосистему обработки данных Python. Matplotlib, Seaborn, Scikit-learn, Statsmodels — все они используют Pandas в качестве основного языка. Polars догоняет, но Pandas по-прежнему остается распространенным языком.

И, честно говоря, для команд и сотрудников знакомство с библиотекой имеет значение. Если все в вашей команде знают Pandas, а Polars никто не знает, внедрение этой библиотеки будет сопряжено с реальными затратами.

Я не заменяю Pandas. Я просто стал более осознанно подходить к его использованию.

Небольшой набор данных, быстрое исследование, визуализация, знакомство с библиотекой командой — Pandas. Большой набор данных, повторяющийся конвейер обработки, производственный рабочий процесс, производительность имеет значение — Polars.

Оба инструмента. Подходящие ситуации.

Заключение

Вот чего я ожидал: сравнения скорости. Запустить один и тот же код в двух библиотеках, показать результаты, и всё.

Вот что я в итоге получил: другой способ мышления о данных.

Показатели скорости реальны — мы перешли от 61 секунды, затрачиваемой на некорректную обработку Pandas в моей предыдущей статье, к 0,31 секунды с оптимизированным Pandas, а затем к 0,20 секунды с ленивой оценкой Polar. Это путь, который стоит задокументировать.

Но самое важное, чему Polars незаметно учит вас по ходу работы, — это то, что фильтры следует применять как можно раньше. Что загружать нужно только то, что необходимо. Что описание желаемого результата и предоставление оптимизатору возможности самому определить способ его получения — это законный и эффективный способ построения конвейеров обработки данных.

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

От всего этого ситуация с пандами нисколько не ухудшилась. Просто мои ожидания стали выше.

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

Какие рабочие процессы вы используете, которые, возможно, стоит переписать?

Ибрагим Салами. Все материалы от Ибрагима Салами.

Источник: towardsdatascience.com

✅ Найденные теги: «Я, Pandas, Polar, новости, Обработка Данных, Рабочий Процесс

Добавить комментарий

Нет других записей в этой рубрике.

Новости других рубрик

Архив рубрики ~Обо всем~: 5 аксессуаров для iPad, о покупке которых я никогда не пожалею (включая альтернативу Apple Pencil за 35 долларов) Архив рубрики ~Обо всем~: Sony выплатит 7,85 млн долларов в виде подарочных сертификатов для PlayStation Store в рамках урегулирования спора по поводу игровых ваучеров. Архив рубрики ~Обо всем~: Гибридный ИИ: сочетание детерминированного анализа с логическим мышлением на основе логики LLM. Архив рубрики ~Обо всем~: Компания Ayaneo анонсировала очередной ремейк для Game Boy, но на этот раз с искусственным интеллектом. Архив рубрики ~Обо всем~: AT&T предоставит вам новый раскладной телефон Razr+ менее чем за 5 долларов в месяц — без необходимости обмена старого телефона на новый. Архив рубрики ~Обо всем~: Корпоративная система управления документами: серия статей о поэтапном создании RAG-системы, от минимального масштаба до масштаба корпуса документов. Архив рубрики ~Обо всем~: НАСА опубликовало фотографии Марса, сделанные космическим аппаратом Psyche. Архив рубрики ~Обо всем~: Сотни читателей оформили предварительный заказ на Fitbit Air по этой акции — вот почему.