Самая быстрая библиотека для работы с данными. Как Pandas, но гораздо быстрее (Polars)

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

Для решения этой проблемы существует множество библиотек. PySpark, Vaex, Modin и Dask – вот некоторые из них.

Сегодня я предлагаю ознакомиться с фреймом Polars.

@bigdatai – здесь собраны лучшие инструменты для работы с данными.

Самая быстрая библиотека для работы с данными. Как Pandas, но гораздо быстрее (Polars)

Polars – очень быстрая библиотека

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

При сравнении Polars доказал, что его скорость вне конкуренции.

Самая быстрая библиотека для работы с данными. Как Pandas, но гораздо быстрее (Polars)

Ниже показана таблица ввода, которая имеет размер 50 ГБ, содержит 1 миллиард строк и 9 столбцов. И снова Polars защитил свой титул.

Самая быстрая библиотека для работы с данными. Как Pandas, но гораздо быстрее (Polars)

Почему Polars такой быстрый?

Распараллеливание программ

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

  • Уменьшение количества избыточных копий;
  • Эффективное кэширование памяти;
  • Сведение конфликтов при параллелизме к минимуму.
Реализован на Rust, а не на Python

Polars намного быстрее, чем библиотеки, которые пытаются реализовать параллелизм с помощью Python. Например, Pandas очень сильно ему уступает. Это потому, что Polars написан с помощью языка программирования Rust, а Rust намного лучше реализует параллелизм, нежелиPython, .

Причина, по которой Python плохо реализует параллелизм, заключается в том, что он использует global interpreter lock (GIL), функцию, отсутствующую в Rust.

Самая быстрая библиотека для работы с данными. Как Pandas, но гораздо быстрее (Polars)
Он поддерживает отложенное выполнение

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

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

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

Вы сможете увидеть пример разницы между отложенным и стремительным выполнением ниже.

Установка Polars

Установка Polars проста. Напишите следующую команду в своём терминале. (Обратите внимание, что часть [all] здесь необязательна.

pip install polars[all]

Набор данных: Парковочные талоны в Нью-Йорке (42 миллиона строк х 51 столбец)

Чтобы проиллюстрировать использование библиотеки Polars, мы будем использовать большой набор данных: 42,3 миллиона строк штрафов за парковку в Нью-Йорке от Kaggle (у него есть лицензия общественного достояния, так что не стесняйтесь использовать его!)

Департамент финансов Нью-Йорка собирает данные о каждом парковочном талоне, выданном в Нью-Йорке (~10 млн в год!).

Самая быстрая библиотека для работы с данными. Как Pandas, но гораздо быстрее (Polars)

Полный набор данных содержит 42 миллиона строк и распределен по 4 файлам — по одному файлу на каждый год. Для остальной части мы будем использовать только один файл (с 2013 по 2014 года).

Почему мы не можем объединить все файлы в один? К сожалению, Polars потерпел крах, когда я попытался объединить все четыре файла вместе.

Весь приведенный ниже код выполняется в  Kaggle notebook, которая имеет:

  • четырёх-ядерный процессор
  • 30 ГБ оперативной памяти

Считывание данных с помощью Polars

Polars содержит в себе функцию scan_csv. Сканирование задерживает парсинг файла и вместо этого возвращает обработчик отложенных выполнений, называемый LazyFrame.

Фактическое вычисление происходит при вызове функции collect().

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

# Scanning in 9 million rows and 51 columns. 
# We ignore any potential errors in the dataset due to encoding / dirty null values.
temp_df = pl.scan_csv("/kaggle/input/nyc-parking-tickets/Parking_Violations_Issued_-_Fiscal_Year_2014__August_2013___June_2014_.csv", 
                      ignore_errors = True)

# Read the data
result_df = temp_df.collect()

# Reading dataset
result_df

# Time taken: 14.1 s ± 3.29 s per loop

Вы также можете использовать другие функции для чтения данных, в том числе:

Фильтрация строк на основе условия

Фильтрация для получения точных значений

Вы также можете выполнить фильтрацию определённых строк, используя ключевое слово filter. Для этого вам нужно будет использовать функцию pl.col(['column_name_here']), чтобы указать имя столбца.

# Lazily read (scan) in 9 million rows and 51 columns.
temp_df = pl.scan_csv("/kaggle/input/nyc-parking-tickets/Parking_Violations_Issued_-_Fiscal_Year_2014__August_2013___June_2014_.csv", ignore_errors = True)

# Filtering for rows with the "Registration State" in NY
result_df = temp_df.filter(pl.col(['Registration State'])=="NY")

# Run the filtering using collect.
result_df.collect()

# Time taken: 12.6 s ± 205 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Для более сложных условий, мы можем использовать такие операторы, как > (больше чем), < (меньше чем), >= (больше или равно), & (и) , | (или).

Фильтрация для более сложных условий

Также вы можете фильтровать по более сложным критериям. Здесь я использую регулярное выражение для фильтрации строк. Условие состоит в том, что Plate ID должен содержать либо “a”, либо “1”.

# Lazily read (scan) in 9 million rows and 51 columns.
temp_df = pl.scan_csv("/kaggle/input/nyc-parking-tickets/Parking_Violations_Issued_-_Fiscal_Year_2014__August_2013___June_2014_.csv", ignore_errors = True)

# Find all carplates that contain the letter 'a' or '1'. 
result_df = temp_df.filter(pl.col("Plate ID").str.contains(r"[a1]"))

# Run the filter using collect.
result_df.collect()

# Time taken: 12 s ± 176 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
Фильтрация по точным значениям (очень медленный способ)

Ещё вы можете выбирать строки с помощью индексов, что является знакомым способом выбора данных для пользователей Pandas.

Для этого вы не можете использовать функцию scan_csv, которая отложено считывает CSV. Вместо этого вы должны выбрать функцию open_csv, которая стремительно считывает CSV.

Обратите внимание, что это противоречит шаблону, поскольку это не позволяет Polars выполнять распараллеливание.

# Eagerly read in 9 million rows and 51 columns.
# Note that we use read_csv, not scan_csv here.
temp_df = pl.read_csv("/kaggle/input/nyc-parking-tickets/Parking_Violations_Issued_-_Fiscal_Year_2014__August_2013___June_2014_.csv", ignore_errors = True)

# Filtering for rows with the "Registration State" in NY
result_df = temp_df[['Registration State']=="NY"]

# Time taken: 15 s ± 3.72 s per loop (mean ± std. dev. of 7 runs, 1 loop each)

Выбор столбца

Отложенное считывание столбцов

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

# Lazily read (scan) in 9 million rows and 51 columns.
temp_df = pl.scan_csv("/kaggle/input/nyc-parking-tickets/Parking_Violations_Issued_-_Fiscal_Year_2014__August_2013___June_2014_.csv", ignore_errors = True)

# Selecting a particular column called Plate ID. 
# In pandas, this will look like result_df = temp_df['Plate ID']
result_df = temp_df.select(['Plate ID']).collect()

# Run it using the collect()
result_df.collect()

# Time taken: 1.59 s ± 24.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
Стремительное считывание столбцов

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

# Eagerly read in all 9 million rows and 51 columns.
temp_df = pl.read_csv("/kaggle/input/nyc-parking-tickets/Parking_Violations_Issued_-_Fiscal_Year_2014__August_2013___June_2014_.csv", ignore_errors = True)

# Selecting the Plate ID column
result_df = temp_df['Plate ID']

# Time taken: 12.8 s ± 304 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Давайте сравним скорости отложенного и стремительного сравнения. Отложенное считывание занимает 1,59 секунд, в то время как стремительное занимает 12,8 секунд. Да, только представьте, отложенное сравнение быстрее в 7 раз.

Создание нового столбца

Чтобы создать новый столбец, Polars использует синтаксис with_columns.

Создание столбцов с использованием строковых функций

Ниже представлен код, с помощью которого можно создать столбец с использованием строковой функции:

# Lazily read (scan) in 9 million rows and 51 columns.
temp_df = pl.scan_csv("/kaggle/input/nyc-parking-tickets/Parking_Violations_Issued_-_Fiscal_Year_2014__August_2013___June_2014_.csv", ignore_errors = True)

# String functions to find all Plate ID that contain the letter 'a' or '1'
result_df = temp_df\
            .with_column(pl.col("Plate ID").str.lengths().alias("plate_id_letter_count"))\
            
# Evaluate the string function.
result_df.collect()

# Time taken: 14.8 s ± 5.79 s per loop
Создание столбцов с использованием лямбда-функции

Можно использовать лямбда-функцию, чтобы создать столбец в том месте, в котором вам нужно.

# Lazily read (scan) in 9 million rows and 51 columns.
temp_df = pl.scan_csv("/kaggle/input/nyc-parking-tickets/Parking_Violations_Issued_-_Fiscal_Year_2014__August_2013___June_2014_.csv", ignore_errors = True)

# Create a new column called "Clean Violation Code"
# using the formula 10000 + df["Violation Code"]
result_df = temp_df.with_columns([
    pl.col("Violation Code").\
    map(lambda x: x+10000).\
    alias("Clean Violation Code")
])

# Evaluate the function.
result_df.collect()

# Time taken: 13.8 s ± 796 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Выполнение агрегирования

У нас также есть пример groupby и агрегации:

# Lazily read (scan) in 9 million rows and 51 columns.
temp_df = pl.scan_csv("/kaggle/input/nyc-parking-tickets/Parking_Violations_Issued_-_Fiscal_Year_2014__August_2013___June_2014_.csv", ignore_errors = True)

# For each vehicle registration state, calculate the number of tikcets 
# and create a list of all violation codes.
result_df = temp_df\
            .groupby("Registration State").agg(
    [
        pl.count(),
        pl.col("Violation Code").list(),

    ]
).sort('Registration State')\
.collect()

result_df

# time taken: 2.3 s ± 29.1 ms per loop

Объединение нескольких функций

Специалистам по Data Science часто приходится выполнять несколько шагов одновременно. Мы можем сделать это в Polars, используя обозначение . .

В следующем примере, мы сначала используем with_column для замены столбца Issue Date из столбца stringcolumn на столбец datetime.

Затем мы выполняем groupby в Registration State. Для каждого штата мы находим самую раннюю Issue Date (дату выдачи) билета.

Наконец, мы сортируем данные по состоянию регистрации в алфавитном порядке.

# Lazily read (scan) in 9 million rows and 51 columns.
temp_df = pl.scan_csv("/kaggle/input/nyc-parking-tickets/Parking_Violations_Issued_-_Fiscal_Year_2014__August_2013___June_2014_.csv", ignore_errors = True)

# Combine multiple steps into one
# Convert "Issue Date" intoa date column, 
# Then group by Registration State and perform some aggregation.
result_df = temp_df\
            .with_column(pl.col("Issue Date").str.strptime(pl.Date, fmt="%m/%d/%Y"))\
            .groupby("Registration State").agg(
                [pl.first("Issue Date")]
              ).sort('Registration State')\

# Run the steps
result_df.collect()

# Took 1.69 s ± 18.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Объединение двух таблиц в одну

Что делать, если у вас есть две таблицы, хранящиеся в двух отдельных файлах, и вы хотели бы объединить их в один фрейм данных? Используйте метод concat.

# Lazily scan two dataframes 
temp_df1 = pl.scan_csv("/kaggle/input/nyc-parking-tickets/Parking_Violations_Issued_-_Fiscal_Year_2014__August_2013___June_2014_.csv", ignore_errors = True)
temp_df2 = pl.scan_csv("/kaggle/input/nyc-parking-tickets/Parking_Violations_Issued_-_Fiscal_Year_2015.csv", ignore_errors = True)

# Concatenating datasets
result_df = pl.concat(
    [
        temp_df1,
        temp_df2,
    ],
    how="vertical",
)

# Reading dataset
result_df.collect()

# Time taken:

Вердикт: Используйте Polars при этох условиях

Должны ли вы использовать Polars? Vaex? PySpark? Dask? Вот как я бы порассуждал об этом:

  • Если ваши данные огромны, переходя в область “больших данных” объемом более 10 ГБ, вы хотите рассмотреть возможность использования PySpark. В противном случае возможны варианты Polars, Vaex и Dask.
  • Если у вас несколько компьютеров в кластере и вы хотите распределить свою рабочую нагрузку между ними, используйте Dask.
  • Если вам нужна визуализация, машинное обучение и глубокое обучение, используйте Vaex. Если нет, используйте Polars.

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

Самая быстрая библиотека для работы с данными. Как Pandas, но гораздо быстрее (Polars)
+1
0
+1
3
+1
0
+1
0
+1
1

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *