Вступ: чому Polars — це майбутнє роботи з даними в Python
Якщо ви Python-розробник і хоч трішки працюєте з даними, то, мабуть, вже натрапляли на згадки про Polars. Чесно кажучи, коли я вперше побачив ще одну "pandas-кілерну" бібліотеку, подумав — ну ось, знову хайп. Але ні. Polars — це реально інший рівень.
Бібліотека написана на Rust, побудована на Apache Arrow, і пропонує такий рівень швидкості та ефективності пам'яті, що після неї pandas починає відчуватися... ну, трохи застарілим.
Станом на 2026 рік Polars вже на версії ~1.38, а компанія-розробник залучила €18 млн у раунді Series A. Це не pet-проєкт ентузіаста — за цим стоять серйозні гроші та серйозні наміри. Тут і ледача обробка, і GPU-прискорення, і хмарна платформа, і розподілена обробка — повноцінна екосистема.
Отже, давайте розбиратися. У цьому посібнику пройдемо весь шлях від встановлення до просунутих прийомів, порівняємо з pandas на живих прикладах і розберемось, коли саме Polars — правильний вибір.
Встановлення та перший крок
Встановлення
Тут все максимально просто — одна команда, і готово:
pip install polars
А якщо потрібні додаткові можливості (часові зони, Excel, GPU) — є розширення:
# Повна установка з усіма розширеннями
pip install polars[all]
# Тільки GPU-прискорення
pip install polars[gpu]
# Тільки робота з часовими зонами та Excel
pip install polars[timezone,excel]
Створення першого DataFrame
Створити DataFrame можна кількома способами. Найпростіший — зі звичайного словника Python:
import polars as pl
# Створення DataFrame зі словника
df = pl.DataFrame({
"ім'я": ["Олена", "Андрій", "Марія", "Дмитро"],
"вік": [28, 35, 42, 31],
"місто": ["Київ", "Львів", "Одеса", "Харків"],
"зарплата": [45000, 52000, 61000, 48000]
})
print(df)
# shape: (4, 4)
# ┌────────┬─────┬────────┬──────────┐
# │ ім'я ┆ вік ┆ місто ┆ зарплата │
# │ --- ┆ --- ┆ --- ┆ --- │
# │ str ┆ i64 ┆ str ┆ i64 │
# ╞════════╪═════╪════════╪══════════╡
# │ Олена ┆ 28 ┆ Київ ┆ 45000 │
# │ Андрій ┆ 35 ┆ Львів ┆ 52000 │
# │ Марія ┆ 42 ┆ Одеса ┆ 61000 │
# │ Дмитро ┆ 31 ┆ Харків ┆ 48000 │
# └────────┴─────┴────────┴──────────┘
Якщо раніше працювали з pandas — синтаксис дуже схожий, переключитися нескладно.
Читання даних із файлів
Polars підтримує всі основні формати прямо з коробки:
# Читання CSV
df_csv = pl.read_csv("дані/продажі.csv")
# Читання Parquet (оптимальний формат для Polars)
df_parquet = pl.read_parquet("дані/продажі.parquet")
# Читання JSON
df_json = pl.read_json("дані/продажі.json")
# Читання з декількох файлів Parquet одночасно
df_multi = pl.read_parquet("дані/продажі_*.parquet")
Eager vs Lazy: два підходи
Ось тут стає цікаво. Polars підтримує два режими роботи. Eager — виконує операції одразу, як ви звикли в pandas. Lazy — будує план виконання, оптимізує його, і лише потім запускає. Різниця на великих даних — колосальна.
# Eager-режим: результат обчислюється одразу
result_eager = df.filter(pl.col("вік") > 30).select("ім'я", "зарплата")
# Lazy-режим: будується план, виконання відкладається
result_lazy = (
df.lazy()
.filter(pl.col("вік") > 30)
.select("ім'я", "зарплата")
.collect() # тільки тут відбувається обчислення
)
# Читання файлу одразу в lazy-режимі
lf = pl.scan_csv("дані/продажі.csv") # повертає LazyFrame
result = lf.filter(pl.col("сума") > 1000).collect()
Для невеликих даних різниця мінімальна. Але коли у вас мільйони рядків — ледачий режим може бути в рази швидшим завдяки оптимізації плану.
Ключові концепції: вирази та контексти
Polars побудований навколо ідеї виразів (expressions). Це декларативний підхід: замість того щоб казати "зроби це, потім те", ви описуєте що хочете отримати, а Polars сам розбирається, як це зробити найефективніше.
Звучить абстрактно? Зараз все стане зрозуміло на прикладах. Вирази працюють у чотирьох основних контекстах:
1. select() — вибір та трансформація стовпців
Контекст select() створює новий DataFrame з обраних або трансформованих стовпців:
import polars as pl
df = pl.DataFrame({
"продукт": ["Ноутбук", "Телефон", "Планшет", "Монітор"],
"ціна": [25000, 15000, 12000, 8000],
"кількість": [10, 50, 30, 25],
"категорія": ["Комп'ютери", "Мобільні", "Мобільні", "Комп'ютери"]
})
# Вибір конкретних стовпців
result = df.select("продукт", "ціна")
# Трансформація стовпців у виразах
result = df.select(
pl.col("продукт"),
pl.col("ціна").alias("ціна_грн"),
(pl.col("ціна") * pl.col("кількість")).alias("загальна_вартість"),
pl.col("ціна").mean().alias("середня_ціна")
)
print(result)
# shape: (4, 4)
# ┌──────────┬──────────┬──────────────────┬─────────────┐
# │ продукт ┆ ціна_грн ┆ загальна_вартість ┆ середня_ціна │
# │ --- ┆ --- ┆ --- ┆ --- │
# │ str ┆ i64 ┆ i64 ┆ f64 │
# ╞══════════╪══════════╪══════════════════╪═════════════╡
# │ Ноутбук ┆ 25000 ┆ 250000 ┆ 15000.0 │
# │ Телефон ┆ 15000 ┆ 750000 ┆ 15000.0 │
# │ Планшет ┆ 12000 ┆ 360000 ┆ 15000.0 │
# │ Монітор ┆ 8000 ┆ 200000 ┆ 15000.0 │
# └──────────┴──────────┴──────────────────┴─────────────┘
2. with_columns() — додавання нових стовпців
На відміну від select(), тут зберігаються всі існуючі стовпці, а ви додаєте нові (або перезаписуєте старі):
# Додавання нових стовпців до існуючого DataFrame
result = df.with_columns(
# Нова колонка — загальна вартість
(pl.col("ціна") * pl.col("кількість")).alias("загальна_вартість"),
# Категорія ціни на основі умови
pl.when(pl.col("ціна") > 15000)
.then(pl.lit("Дорого"))
.when(pl.col("ціна") > 10000)
.then(pl.lit("Середньо"))
.otherwise(pl.lit("Бюджетно"))
.alias("цінова_категорія"),
# Відсоток від максимальної ціни
(pl.col("ціна") / pl.col("ціна").max() * 100)
.round(1)
.alias("відсоток_від_макс")
)
print(result)
3. filter() — фільтрація рядків
Фільтрація — одна з найчастіших операцій, і Polars робить її інтуїтивно зрозумілою:
# Простий фільтр
дорогі = df.filter(pl.col("ціна") > 10000)
# Складний фільтр з кількома умовами
result = df.filter(
(pl.col("категорія") == "Мобільні") &
(pl.col("кількість") > 20)
)
# Фільтр з використанням рядкових методів
result = df.filter(
pl.col("продукт").str.contains("(?i)телефон|планшет")
)
# Фільтр за списком значень
result = df.filter(
pl.col("категорія").is_in(["Мобільні", "Аксесуари"])
)
4. group_by() — агрегація даних
Групування та агрегації — це, мабуть, те місце, де Polars виглядає найелегантніше в порівнянні з pandas:
# Групування з агрегацією
stats = df.group_by("категорія").agg(
pl.col("ціна").mean().alias("середня_ціна"),
pl.col("ціна").max().alias("макс_ціна"),
pl.col("кількість").sum().alias("загальна_кількість"),
pl.col("продукт").count().alias("кількість_товарів"),
# Список усіх продуктів у категорії
pl.col("продукт").alias("список_продуктів")
)
print(stats)
# shape: (2, 5)
# ┌────────────┬──────────────┬───────────┬───────────────────┬──────────────────┐
# │ категорія ┆ середня_ціна ┆ макс_ціна ┆ загальна_кількість ┆ кількість_товарів │
# │ --- ┆ --- ┆ --- ┆ --- ┆ --- │
# │ str ┆ f64 ┆ i64 ┆ i64 ┆ u32 │
# ╞════════════╪══════════════╪═══════════╪═══════════════════╪══════════════════╡
# │ Комп'ютери ┆ 16500.0 ┆ 25000 ┆ 35 ┆ 2 │
# │ Мобільні ┆ 13500.0 ┆ 15000 ┆ 80 ┆ 2 │
# └────────────┴──────────────┴───────────┴───────────────────┴──────────────────┘
Ледача обробка (Lazy Evaluation)
Гаразд, це та частина, де Polars по-справжньому показує свої м'язи. Ледача обробка — це, мабуть, головна перевага над pandas, і ось чому.
Замість того щоб виконувати кожну операцію негайно, Polars будує план запиту (query plan). Потім оптимізує його. І тільки тоді виконує. Як SQL-оптимізатор, тільки для DataFrame-операцій.
Концепція LazyFrame
LazyFrame — це, по суті, "рецепт" обчислення. Ви описуєте ланцюг трансформацій, а Polars знаходить оптимальний спосіб їх виконати:
import polars as pl
# Створення LazyFrame з файлу (дані ще не завантажені!)
lf = pl.scan_parquet("продажі_2025.parquet")
# Опис ланцюга трансформацій (нічого ще не обчислено)
query = (
lf
.filter(pl.col("дата") > "2025-06-01")
.filter(pl.col("сума") > 500)
.group_by("регіон")
.agg(
pl.col("сума").sum().alias("загальна_сума"),
pl.col("замовлення_id").count().alias("кількість_замовлень")
)
.sort("загальна_сума", descending=True)
.head(10)
)
# Подивимося на план запиту
print(query.explain())
# Polars покаже оптимізований план виконання
# Тепер виконуємо запит
result = query.collect()
print(result)
Оптимізації плану запиту
Polars автоматично застосовує кілька потужних оптимізацій (і це одна з причин, чому lazy-режим такий швидкий):
- Predicate Pushdown — фільтри "проштовхуються" якомога ближче до джерела даних. Читаєте Parquet і фільтруєте по стовпцю? Polars прочитає лише потрібні row groups.
- Projection Pushdown — потрібні тільки 3 стовпці з 50? Polars прочитає з файлу саме ці 3.
- Common Subexpression Elimination — однакові підвирази обчислюються лише один раз.
- Slice Pushdown — потрібні тільки перші 10 рядків? Polars не буде обробляти весь датасет.
Давайте подивимось на практичний приклад:
import polars as pl
import time
# Уявімо великий CSV-файл з мільйонами рядків
# Eager-підхід: все завантажується в пам'ять
start = time.perf_counter()
df_eager = pl.read_csv("великий_файл.csv")
result_eager = (
df_eager
.filter(pl.col("статус") == "активний")
.select("ім'я", "email", "дата_реєстрації")
)
eager_time = time.perf_counter() - start
# Lazy-підхід: Polars оптимізує план
start = time.perf_counter()
result_lazy = (
pl.scan_csv("великий_файл.csv")
.filter(pl.col("статус") == "активний")
.select("ім'я", "email", "дата_реєстрації")
.collect() # тут відбувається магія оптимізації
)
lazy_time = time.perf_counter() - start
print(f"Eager: {eager_time:.2f}с")
print(f"Lazy: {lazy_time:.2f}с")
# Lazy-підхід зазвичай у 2-5 разів швидший завдяки
# projection pushdown (читає тільки 3 стовпці) та
# predicate pushdown (фільтрує на рівні читання)
Перегляд плану виконання
До речі, можна подивитись і на неоптимізований, і на оптимізований план — це дуже корисно для дебагу:
query = (
pl.scan_parquet("дані.parquet")
.filter(pl.col("категорія") == "електроніка")
.select("назва", "ціна", "рейтинг")
.sort("рейтинг", descending=True)
.head(100)
)
# Неоптимізований план
print(query.explain(optimized=False))
# Оптимізований план (за замовчуванням)
print(query.explain())
# Різниця буде видна: у оптимізованому плані
# фільтрація та вибір стовпців буде "проштовхнута"
# до самого джерела даних
Порівняння з pandas: практичні приклади
Для тих, хто переходить з pandas (а це, думаю, більшість читачів), найкращий спосіб навчитися — порівняти код бік о бік. Тож давайте пройдемось по типових операціях.
Фільтрація рядків
# === pandas ===
import pandas as pd
df_pd = pd.read_csv("продажі.csv")
result_pd = df_pd[df_pd["сума"] > 1000]
# або
result_pd = df_pd.query("сума > 1000")
# === Polars ===
import polars as pl
df_pl = pl.read_csv("продажі.csv")
result_pl = df_pl.filter(pl.col("сума") > 1000)
Групування та агрегація
# === pandas ===
result_pd = (
df_pd
.groupby("регіон")
.agg({"сума": ["sum", "mean"], "замовлення_id": "count"})
)
result_pd.columns = ["сума_загальна", "сума_середня", "кількість"]
result_pd = result_pd.reset_index()
# === Polars — чистіший та зрозуміліший синтаксис ===
result_pl = df_pl.group_by("регіон").agg(
pl.col("сума").sum().alias("сума_загальна"),
pl.col("сума").mean().alias("сума_середня"),
pl.col("замовлення_id").count().alias("кількість")
)
Зверніть увагу, як у Polars не треба возитись із reset_index() та перейменуванням стовпців — все одразу зрозуміло і чисто.
З'єднання (Join)
# === pandas ===
merged = pd.merge(
замовлення_pd, клієнти_pd,
left_on="клієнт_id", right_on="id",
how="left"
)
# === Polars ===
joined = замовлення_pl.join(
клієнти_pl,
left_on="клієнт_id",
right_on="id",
how="left"
)
Створення нових стовпців
# === pandas — повільний apply ===
df_pd["цінова_категорія"] = df_pd["ціна"].apply(
lambda x: "Дорого" if x > 1000 else "Дешево"
)
# === Polars — швидкі вирази ===
df_pl = df_pl.with_columns(
pl.when(pl.col("ціна") > 1000)
.then(pl.lit("Дорого"))
.otherwise(pl.lit("Дешево"))
.alias("цінова_категорія")
)
Обробка пропущених значень
# === pandas ===
df_pd["стовпець"].fillna(0)
df_pd.dropna(subset=["стовпець"])
df_pd["стовпець"].isna().sum()
# === Polars ===
df_pl.with_columns(pl.col("стовпець").fill_null(0))
df_pl.drop_nulls(subset=["стовпець"])
df_pl.select(pl.col("стовпець").is_null().sum())
Сортування
# === pandas ===
df_pd.sort_values(by=["регіон", "сума"], ascending=[True, False])
# === Polars ===
df_pl.sort("регіон", "сума", descending=[False, True])
Ланцюжок операцій (Method Chaining)
Ось тут Polars виграє красиво. Порівняйте самі:
# === pandas — часто потребує проміжних змінних ===
result_pd = df_pd[df_pd["статус"] == "завершено"]
result_pd = result_pd.groupby("категорія").agg({"сума": "sum"})
result_pd = result_pd.sort_values("сума", ascending=False)
result_pd = result_pd.head(5)
# === Polars — елегантний ланцюжок ===
result_pl = (
df_pl
.filter(pl.col("статус") == "завершено")
.group_by("категорія")
.agg(pl.col("сума").sum())
.sort("сума", descending=True)
.head(5)
)
Продуктивність: бенчмарки та оптимізація
Polars не просто зручний — він реально швидший за pandas у більшості сценаріїв. Ось конкретні цифри з незалежних бенчмарків:
- Фільтрація: Polars швидший у 4.6 рази порівняно з pandas на датасеті в 10 млн рядків
- Агрегація (group_by): прискорення у 2.6 рази
- З'єднання (join): прискорення у 3-5 разів
- Загальні ETL-пайплайни: прискорення у 5-10 разів
- Використання пам'яті: на 30-50% менше завдяки Apache Arrow
Непогано, правда?
Чому Polars такий швидкий?
За цією продуктивністю стоять кілька принципових архітектурних рішень:
- Написаний на Rust — мова системного рівня без збирача сміття, з гарантіями безпеки пам'яті. Це не Python-обгортка над C — це нативний Rust-код.
- Apache Arrow як формат у пам'яті — колоночне зберігання, zero-copy обмін між бібліотеками, оптимальне використання кешу процесора.
- Автоматична багатопотоковість — Polars сам розпаралелює операції між усіма ядрами. Нічого налаштовувати не треба.
- Оптимізація плану запитів — ледачий режим аналізує весь ланцюг і знаходить оптимальний план виконання.
- SIMD-інструкції — векторні операції процесора для паралельної обробки на рівні заліза.
Практичний бенчмарк
Хочете переконатися самі? Ось код, який можна запустити прямо зараз:
import polars as pl
import pandas as pd
import numpy as np
import time
# Генеруємо великий датасет
n = 10_000_000
np.random.seed(42)
data = {
"id": np.arange(n),
"категорія": np.random.choice(["A", "B", "C", "D", "E"], n),
"значення": np.random.randn(n) * 100,
"кількість": np.random.randint(1, 100, n),
}
df_pd = pd.DataFrame(data)
df_pl = pl.DataFrame(data)
# Бенчмарк: складна агрегація
# pandas
start = time.perf_counter()
result_pd = (
df_pd
.groupby("категорія")
.agg({"значення": ["sum", "mean", "std"], "кількість": "sum"})
)
pd_time = time.perf_counter() - start
# Polars
start = time.perf_counter()
result_pl = (
df_pl
.group_by("категорія")
.agg(
pl.col("значення").sum().alias("сума"),
pl.col("значення").mean().alias("середнє"),
pl.col("значення").std().alias("стд"),
pl.col("кількість").sum().alias("загальна_кількість"),
)
)
pl_time = time.perf_counter() - start
print(f"pandas: {pd_time:.3f}с")
print(f"Polars: {pl_time:.3f}с")
print(f"Прискорення: {pd_time / pl_time:.1f}x")
Стримінг для великих даних (Out-of-Core)
А що робити, коли датасет взагалі не влазить у RAM? Polars має відповідь — стримінговий режим:
# Стримінгова обробка — дані обробляються частинами
result = (
pl.scan_parquet("величезний_файл.parquet")
.filter(pl.col("дата") > "2025-01-01")
.group_by("регіон")
.agg(pl.col("дохід").sum())
.collect(streaming=True) # обробка по частинах
)
# Стримінг працює з будь-яким форматом
result = (
pl.scan_csv("100gb_файл.csv")
.filter(pl.col("статус") == "активний")
.select("ім'я", "email")
.collect(streaming=True)
)
Стримінг дозволяє обробляти датасети, що значно перевищують об'єм вашої оперативної пам'яті, розбиваючи обробку на невеликі фрагменти. Дуже зручно.
Прискорення на GPU
А тепер — вишенька на торті. Polars підтримує GPU-прискорення через інтеграцію з NVIDIA RAPIDS cuDF. І найкрутіше тут те, що ваш існуючий код працюватиме на GPU без жодних змін. Серйозно.
Встановлення
# Встановлення Polars з підтримкою GPU
pip install polars[gpu]
# Вимоги:
# - NVIDIA GPU з підтримкою CUDA
# - Драйвер NVIDIA >= 525
# - Достатньо VRAM для ваших даних
Використання GPU
Щоб увімкнути GPU — один додатковий параметр, і все:
import polars as pl
# Звичайний запит на CPU
result_cpu = (
pl.scan_parquet("великий_файл.parquet")
.filter(pl.col("значення") > 100)
.group_by("категорія")
.agg(
pl.col("значення").sum(),
pl.col("значення").mean(),
pl.col("id").count()
)
.sort("значення")
.collect() # виконання на CPU
)
# Той самий запит на GPU — ЄДИНА зміна: engine="gpu"
result_gpu = (
pl.scan_parquet("великий_файл.parquet")
.filter(pl.col("значення") > 100)
.group_by("категорія")
.agg(
pl.col("значення").sum(),
pl.col("значення").mean(),
pl.col("id").count()
)
.sort("значення")
.collect(engine="gpu") # виконання на GPU!
)
Результати прискорення
Цифри говорять самі за себе:
- До 13x прискорення для операцій агрегації на великих датасетах
- 5-8x прискорення для фільтрації та з'єднань
- Автоматичний fallback — якщо операція не підтримується на GPU, Polars автоматично виконає її на CPU (без помилок, без зайвого клопоту)
# Складний аналітичний запит, який чудово прискорюється на GPU
result = (
pl.scan_parquet("транзакції_2025.parquet")
.filter(
(pl.col("дата") >= "2025-01-01") &
(pl.col("сума") > 0)
)
.with_columns(
pl.col("дата").dt.month().alias("місяць"),
pl.col("дата").dt.weekday().alias("день_тижня")
)
.group_by("місяць", "день_тижня", "категорія")
.agg(
pl.col("сума").sum().alias("загальна_сума"),
pl.col("сума").mean().alias("середній_чек"),
pl.col("клієнт_id").n_unique().alias("унікальних_клієнтів")
)
.sort("загальна_сума", descending=True)
.collect(engine="gpu") # GPU-прискорення
)
print(result)
Варто зазначити: GPU-прискорення найефективніше для великих датасетів (від сотень тисяч рядків). Для маленьких даних накладні витрати на передачу між CPU і GPU можуть нівелювати виграш.
Polars Cloud та розподілена обробка
У 2025-2026 роках Polars зробив серйозний крок до хмари. Polars Cloud — це повністю керована платформа, і, чесно кажучи, ідея за нею просто чудова: ваш локальний Polars-код масштабується до хмари з мінімальними змінами.
Ключові характеристики
- General Availability на AWS — вже доступна для production
- Розподілений рушій — горизонтальне масштабування на кілька машин (відкрита бета)
- Єдиний API — один і той самий код на ноутбуці та у хмарі
- Три типи масштабування: вертикальне (більша машина), горизонтальне (більше машин) та діагональне (комбінація обох)
import polars as pl
import polars.cloud as pc
# Той самий код, що працює локально
query = (
pl.scan_parquet("s3://мій-бакет/дані/продажі_*.parquet")
.filter(pl.col("рік") == 2025)
.group_by("регіон", "категорія")
.agg(
pl.col("дохід").sum().alias("загальний_дохід"),
pl.col("замовлення_id").count().alias("кількість_замовлень")
)
.sort("загальний_дохід", descending=True)
)
# Локальне виконання
result_local = query.collect()
# Виконання у хмарі — обробка терабайтів даних
result_cloud = query.collect(engine=pc.CloudEngine())
# Розподілене виконання на кластері
result_distributed = query.collect(
engine=pc.CloudEngine(distributed=True)
)
Переваги Polars Cloud
Головна фішка — нульовий бар'єр входу. Не треба вивчати новий API, налаштовувати Spark-кластери чи розбиратися з розподіленими системами. Просто додаєте один параметр — і ваш код працює на потужних хмарних машинах.
- Розробка локально — тестуєте на невеликій вибірці на своєму ноутбуці
- Production у хмарі — той самий код обробляє терабайти
- Автоматичне масштабування — Polars Cloud сам визначає оптимальну конфігурацію
Міграція з pandas на Polars
Окей, ви переконались, що Polars — це круто. Але ж у вас вже є проєкт на pandas, і переписувати все з нуля — не варіант. Добра новина: міграція може бути поступовою.
Таблиця відповідностей
Тримайте шпаргалку — основні відповідності між pandas та Polars:
# ╔═══════════════════════════════╦══════════════════════════════════════╗
# ║ pandas ║ Polars ║
# ╠═══════════════════════════════╬══════════════════════════════════════╣
# ║ pd.merge(df1, df2) ║ df1.join(df2) ║
# ║ df.fillna(0) ║ df.fill_null(0) ║
# ║ df.dropna() ║ df.drop_nulls() ║
# ║ df.rename(columns={...}) ║ df.rename({...}) ║
# ║ df.apply(func) ║ використовуйте вирази ║
# ║ df.groupby() ║ df.group_by() ║
# ║ df.sort_values() ║ df.sort() ║
# ║ df["col"] ║ df.select("col") / pl.col("col") ║
# ║ df.iloc[0:5] ║ df.head(5) / df.slice(0, 5) ║
# ║ df.shape[0] ║ df.height ║
# ║ df.dtypes ║ df.schema ║
# ╚═══════════════════════════════╩══════════════════════════════════════╝
Заміна apply() на вирази
Це, мабуть, найважливіша зміна при міграції. apply() у pandas — це повільна поелементна операція. У Polars замість неї — швидкі векторизовані вирази:
# === pandas (повільно — поелементна обробка) ===
df_pd["знижка"] = df_pd.apply(
lambda row: row["ціна"] * 0.1 if row["категорія"] == "VIP"
else row["ціна"] * 0.05,
axis=1
)
# === Polars (швидко — векторизована обробка) ===
df_pl = df_pl.with_columns(
pl.when(pl.col("категорія") == "VIP")
.then(pl.col("ціна") * 0.1)
.otherwise(pl.col("ціна") * 0.05)
.alias("знижка")
)
Робота з індексом
Важлива відмінність: Polars не має індексу. І, чесно кажучи, це спрощує життя. Скільки разів ви забували зробити reset_index() у pandas?
# pandas — часто потрібно скидати індекс
result_pd = df_pd.groupby("категорія").sum().reset_index()
# Polars — індексу немає, все просто
result_pl = df_pl.group_by("категорія").agg(pl.all().sum())
when/then/otherwise замість np.where
# === pandas з numpy ===
import numpy as np
df_pd["рівень"] = np.where(
df_pd["бал"] >= 90, "Відмінно",
np.where(df_pd["бал"] >= 70, "Добре",
np.where(df_pd["бал"] >= 50, "Задовільно", "Незадовільно")))
)
# === Polars — чистий та зрозумілий код ===
df_pl = df_pl.with_columns(
pl.when(pl.col("бал") >= 90).then(pl.lit("Відмінно"))
.when(pl.col("бал") >= 70).then(pl.lit("Добре"))
.when(pl.col("бал") >= 50).then(pl.lit("Задовільно"))
.otherwise(pl.lit("Незадовільно"))
.alias("рівень")
)
Стратегія поступової міграції
Ось план, який працює на практиці:
- Визначте вузькі місця — профілюйте pandas-код і знайдіть найповільніші операції. Їх мігруйте першими.
- Новий код — на Polars — для нових функцій одразу використовуйте Polars.
- Міст між бібліотеками — конвертуйте дані між pandas та Polars на межах модулів.
- Поступово переписуйте старе — починаючи з найбільш критичних за швидкістю частин.
- Переходьте на lazy API — замініть eager-операції на lazy для максимальної оптимізації.
# Конвертація між pandas та Polars
import pandas as pd
import polars as pl
# pandas -> Polars
df_pandas = pd.read_csv("дані.csv")
df_polars = pl.from_pandas(df_pandas)
# Polars -> pandas (коли потрібно для ML-бібліотек)
df_pandas_back = df_polars.to_pandas()
# Приклад гібридного пайплайну
# 1. Обробка даних у Polars (швидко)
df = (
pl.scan_parquet("великий_файл.parquet")
.filter(pl.col("дата") > "2025-01-01")
.group_by("категорія")
.agg(pl.col("значення").mean())
.collect()
)
# 2. Передача в scikit-learn (потрібен pandas/numpy)
from sklearn.linear_model import LinearRegression
X = df.select("категорія_код").to_pandas()
y = df.select("значення").to_pandas()
model = LinearRegression().fit(X, y)
Коли обирати Polars, а коли pandas
Polars — потужний інструмент, але це не означає, що pandas треба викинути. Кожен має свої сильні сторони.
Обирайте Polars, коли:
- Великі датасети — якщо дані перевищують кілька сотень мегабайтів, Polars покаже значну перевагу
- Продуктивність критична — ETL-пайплайни, обробка логів, data engineering
- Складні агрегації — вирази Polars дозволяють описати складну логіку чисто та ефективно
- Новий проєкт — починаєте з нуля? Polars — кращий фундамент для масштабування
- Parquet/Arrow — нативна робота без накладних витрат
- Дані не влазять у RAM — стримінг та lazy-режим рятують
Обирайте pandas, коли:
- Невеликі датасети — для кількох тисяч рядків різниця непомітна
- Існуючий великий проєкт — переписувати все заради переписування не варто
- ML-екосистема — scikit-learn, statsmodels, plotly очікують pandas або numpy
- Інтерактивний аналіз — Jupyter + pandas все ще дуже зручна комбінація
- Команда знає тільки pandas — навчання потребує часу
Вони чудово співіснують!
І це, мабуть, головний посил. Polars та pandas — не вороги. Вони прекрасно працюють разом в одному проєкті:
import polars as pl
import pandas as pd
# Важка обробка — Polars
df_processed = (
pl.scan_parquet("s3://bucket/raw_data/*.parquet")
.filter(pl.col("дата").is_between("2025-01-01", "2025-12-31"))
.group_by("клієнт_id", "місяць")
.agg(
pl.col("сума").sum().alias("загальна_сума"),
pl.col("транзакція_id").count().alias("кількість_транзакцій"),
pl.col("категорія").n_unique().alias("унікальних_категорій")
)
.collect(streaming=True)
)
# Передача в pandas для візуалізації
df_viz = df_processed.to_pandas()
import matplotlib.pyplot as plt
df_viz.plot(kind="bar", x="місяць", y="загальна_сума")
plt.title("Загальна сума продажів по місяцях")
plt.show()
# Або для ML
from sklearn.cluster import KMeans
features = df_processed.select(
"загальна_сума", "кількість_транзакцій", "унікальних_категорій"
).to_numpy()
kmeans = KMeans(n_clusters=5).fit(features)
Просунуті прийоми
Для тих, хто вже освоївся з основами — кілька потужних інструментів, які варто мати в арсеналі.
Віконні функції (Window Functions)
Якщо ви працювали з SQL, віконні функції в Polars здадуться вам рідними:
df = pl.DataFrame({
"відділ": ["IT", "IT", "HR", "HR", "IT", "HR"],
"працівник": ["Олена", "Андрій", "Марія", "Петро", "Ірина", "Василь"],
"зарплата": [50000, 55000, 45000, 48000, 52000, 47000]
})
# Віконна функція — ранг зарплати всередині відділу
result = df.with_columns(
pl.col("зарплата")
.rank(descending=True)
.over("відділ")
.alias("ранг_у_відділі"),
pl.col("зарплата")
.mean()
.over("відділ")
.alias("середня_по_відділу"),
(pl.col("зарплата") - pl.col("зарплата").mean().over("відділ"))
.alias("відхилення_від_середньої")
)
print(result)
Робота з часовими рядами
# Часові ряди з rolling-агрегаціями
df_ts = pl.DataFrame({
"дата": pl.date_range(
pl.date(2025, 1, 1), pl.date(2025, 12, 31), eager=True
),
}).with_columns(
pl.lit(None).cast(pl.Float64).alias("продажі")
).with_columns(
(pl.arange(0, pl.count()) * 100 +
pl.Series(name="шум", values=np.random.randn(365) * 50))
.alias("продажі")
)
# Ковзне середнє за 7 днів
result = df_ts.with_columns(
pl.col("продажі")
.rolling_mean(window_size=7)
.alias("ковзне_середнє_7д"),
pl.col("продажі")
.rolling_sum(window_size=30)
.alias("сума_за_30д")
)
Робота з вкладеними структурами
Polars нативно підтримує вкладені типи — List, Struct, Array. Це неймовірно зручно для роботи з напівструктурованими даними:
# Polars підтримує вкладені типи даних: List, Struct, Array
df = pl.DataFrame({
"клієнт": ["Олена", "Андрій"],
"замовлення": [[100, 200, 150], [300, 50]],
"теги": [["VIP", "постійний"], ["новий"]]
})
# Операції з вкладеними списками
result = df.with_columns(
pl.col("замовлення").list.sum().alias("сума_замовлень"),
pl.col("замовлення").list.len().alias("кількість_замовлень"),
pl.col("замовлення").list.max().alias("макс_замовлення"),
pl.col("теги").list.contains("VIP").alias("є_VIP")
)
print(result)
Власні функції через map_elements та map_batches
Коли стандартних виразів не вистачає (буває рідко, але буває), можна написати власну функцію:
# map_batches працює з цілим стовпцем — набагато швидше за map_elements
def нормалізація(серія: pl.Series) -> pl.Series:
мін = серія.min()
макс = серія.max()
return (серія - мін) / (макс - мін)
result = df.with_columns(
pl.col("зарплата").map_batches(нормалізація).alias("норм_зарплата")
)
# map_elements — поелементна обробка (використовуйте рідко!)
# Тільки коли дійсно немає альтернативи серед виразів
result = df.with_columns(
pl.col("ім'я")
.map_elements(lambda x: x.upper()[:3], return_dtype=pl.String)
.alias("код_працівника")
)
Висновок
Polars — це не просто "швидший pandas". Це продуманий, сучасний інструмент нового покоління для роботи з даними у Python:
- Від 2.6x до 13x прискорення залежно від операції (CPU або GPU)
- Ефективне використання пам'яті — Apache Arrow + стримінг для великих даних
- Елегантний API на основі виразів — декларативний підхід, який і зрозуміліший, і швидший
- Ледача обробка з оптимізацією — predicate pushdown, projection pushdown "з коробки"
- GPU-прискорення — один параметр
engine="gpu", і готово - Polars Cloud — масштабування від ноутбука до кластера одним API
Станом на 2026 рік Polars впевнено рухається до статусу стандарту де-факто для обробки даних у Python. €18 млн інвестицій, активна розробка хмарної платформи, зростаюча спільнота — все вказує на те, що це надовго.
Але pandas нікуди не дінеться. Для невеликих даних, швидкого прототипування та інтеграції з ML-бібліотеками він все ще чудовий. Найрозумніший підхід — використовувати обидві бібліотеки: Polars для важких обчислень і ETL, pandas для візуалізації та ML.
Якщо ви ще не спробували Polars — спробуйте. Перепишіть один pandas-скрипт і оцініть різницю. Майбутнє роботи з даними в Python вже настало.