Polars для Python-розробників: практичний посібник від основ до GPU-прискорення

Повний практичний посібник з Polars для Python-розробників. Вирази, ледача обробка, GPU-прискорення, Polars Cloud, порівняння з pandas та покрокова стратегія міграції з реальними прикладами коду.

Polars для Python: Швидкий Гайд 2026

Вступ: чому 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 такий швидкий?

За цією продуктивністю стоять кілька принципових архітектурних рішень:

  1. Написаний на Rust — мова системного рівня без збирача сміття, з гарантіями безпеки пам'яті. Це не Python-обгортка над C — це нативний Rust-код.
  2. Apache Arrow як формат у пам'яті — колоночне зберігання, zero-copy обмін між бібліотеками, оптимальне використання кешу процесора.
  3. Автоматична багатопотоковість — Polars сам розпаралелює операції між усіма ядрами. Нічого налаштовувати не треба.
  4. Оптимізація плану запитів — ледачий режим аналізує весь ланцюг і знаходить оптимальний план виконання.
  5. 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-кластери чи розбиратися з розподіленими системами. Просто додаєте один параметр — і ваш код працює на потужних хмарних машинах.

  1. Розробка локально — тестуєте на невеликій вибірці на своєму ноутбуці
  2. Production у хмарі — той самий код обробляє терабайти
  3. Автоматичне масштабування — 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("рівень")
)

Стратегія поступової міграції

Ось план, який працює на практиці:

  1. Визначте вузькі місця — профілюйте pandas-код і знайдіть найповільніші операції. Їх мігруйте першими.
  2. Новий код — на Polars — для нових функцій одразу використовуйте Polars.
  3. Міст між бібліотеками — конвертуйте дані між pandas та Polars на межах модулів.
  4. Поступово переписуйте старе — починаючи з найбільш критичних за швидкістю частин.
  5. Переходьте на 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 вже настало.

Article changelog (1)
  • — SEO meta refreshed (title and description updated)
Про Автора Priya Raghavan

Priya is a senior data engineer with 11 years building analytics platforms, most recently at Stripe where she led the migration of the merchant analytics pipeline from pandas to polars (cut p95 batch latency from 42 minutes to under 6). Before Stripe she spent four years at Mode Analytics writing the query engine that powered customer dashboards, and two years at Etsy on the seller-insights team. She writes mainly about polars internals, lazy evaluation patterns, and the practical edges of moving production pandas code to polars without breaking analyst muscle memory. Her side project is a 12k-row benchmark suite comparing pandas 2.x, polars, and DuckDB across realistic e-commerce joins. Priya lives in Oakland, mentors through Women in Data, and is slowly learning to play go.