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

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

Вступ: чому 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 вже настало.

Про Автора Editorial Team

Our team of expert writers and editors.