Самая быстрая библиотека для работы с данными. Как Pandas, но гораздо быстрее (Polars)
Давайте посмотрим правде в глаза. Фреймворк Pandas медленный. Когда у вас есть миллионы строк в вашей структуре данных, становится очень неприятно ждать в течение минуты выполнения одной строки кода. В конечном итоге, вы потратите больше времени на ожидание, чем на реальную аналитику.
Для решения этой проблемы существует множество библиотек. PySpark, Vaex, Modin и Dask – вот некоторые из них.
Сегодня я предлагаю ознакомиться с фреймом Polars.
@bigdatai – здесь собраны лучшие инструменты для работы с данными.
Polars – очень быстрая библиотека
С уверенностью можно сказать, что Pandas – одна из самых быстрых библиотек в мире. Такой вывод можно сделать из исследования, сутью которого являлось сравнение данной библиотеки с похожими.
При сравнении Polars доказал, что его скорость вне конкуренции.
Ниже показана таблица ввода, которая имеет размер 50 ГБ, содержит 1 миллиард строк и 9 столбцов. И снова Polars защитил свой титул.
Почему Polars такой быстрый?
Распараллеливание программ
Polars быстр, потому что использует эффективные алгоритмы распараллеливания и кэширования для ускорения выполнения аналитических задач. Вот его стратегии:
- Уменьшение количества избыточных копий;
- Эффективное кэширование памяти;
- Сведение конфликтов при параллелизме к минимуму.
Реализован на Rust, а не на Python
Polars намного быстрее, чем библиотеки, которые пытаются реализовать параллелизм с помощью Python. Например, Pandas очень сильно ему уступает. Это потому, что Polars написан с помощью языка программирования Rust, а Rust намного лучше реализует параллелизм, нежелиPython, .
Причина, по которой Python плохо реализует параллелизм, заключается в том, что он использует global interpreter lock (GIL), функцию, отсутствующую в Rust.
Он поддерживает отложенное выполнение
Отложенное выполнение необходимо для того, чтобы выражение вычислялось не сразу, а только при необходимости. Напротив, стремительное выполнение вычисляется немедленно.
Таким образом, Polars может выполнять оптимизацию — запускать только то, что необходимо на данный момент, и игнорировать то, что пока не требуется.
С другой стороны, Pandas выполняет весь этот процесс стремительно, способствуя пустой трате ресурсов.
Вы сможете увидеть пример разницы между отложенным и стремительным выполнением ниже.
Установка Polars
Установка Polars проста. Напишите следующую команду в своём терминале. (Обратите внимание, что часть [all] здесь необязательна.
pip install polars[all]
Набор данных: Парковочные талоны в Нью-Йорке (42 миллиона строк х 51 столбец)
Чтобы проиллюстрировать использование библиотеки Polars, мы будем использовать большой набор данных: 42,3 миллиона строк штрафов за парковку в Нью-Йорке от Kaggle (у него есть лицензия общественного достояния, так что не стесняйтесь использовать его!)
Департамент финансов Нью-Йорка собирает данные о каждом парковочном талоне, выданном в Нью-Йорке (~10 млн в год!).
Полный набор данных содержит 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.
Это субъективный вердикт, ведь всё зависит именно от вашего выбора. Поэтому я рекомендую вам поэкспериментировать с разными вариантами и найти лучший из них!