Pandas, Polars и DuckDB: Пълно ръководство за обработка на данни в Python (2026)

Практическо ръководство за Pandas, Polars и DuckDB в Python през 2026. Бенчмарки, код примери, хибридни ETL пайплайни и съвети за миграция — всичко на едно място.

Въведение: Новата ера на обработка на данни в Python

Ако работите с данни в Python през 2026 година, вероятно вече усещате — нещата се промениха доста. Pandas си остава номер едно по популярност (около 77% от дейта специалистите го ползват ежедневно), но появата на Polars и DuckDB промени играта. Вече не се питаме „кой инструмент да избера", а по-скоро „как да ги комбинирам най-добре".

В тази статия ще разгледаме тези три инструмента в детайли — с реални бенчмарки, примери от практиката и конкретен хибриден ETL пайплайн. Ако се колебаете дали да пробвате Polars или DuckDB, или просто искате да оптимизирате съществуващия си Pandas код — тук ще намерите отговори.

Pandas през 2026: Зрялост и обновление

Новостите в Pandas 2.x

Pandas мина през сериозна еволюция с версиите 2.0, 2.1 и 2.2. Най-съществените промени? Интеграцията с Apache Arrow чрез PyArrow бекенд, механизмът Copy-on-Write и подобрената поддръжка на типове данни. А с наближаването на Pandas 3.0 PyArrow ще стане задължителна зависимост — което казва доста за посоката на развитие.

Ето как активирате PyArrow бекенда:

import pandas as pd

# Четене на CSV файл с PyArrow бекенд
df = pd.read_csv(
    "sales_data.csv",
    engine="pyarrow",
    dtype_backend="pyarrow"
)

# Проверка на типовете данни
print(df.dtypes)
# Забележете, че колоните вече са с Arrow типове:
# product_name    string[pyarrow]
# price           double[pyarrow]
# quantity        int64[pyarrow]
# date            timestamp[ns][pyarrow]

print(f"Памет: {df.memory_usage(deep=True).sum() / 1024**2:.2f} MB")

Copy-on-Write оптимизация

Механизмът Copy-on-Write (CoW) е една от онези неща, които наистина правят разлика в ежедневната работа. Идеята е проста — данните не се копират, докато реално не ги промените. На практика, при работа с по-големи набори от данни, това спестява огромно количество памет.

import pandas as pd

# Активиране на Copy-on-Write
pd.set_option("mode.copy_on_write", True)

# Създаване на голям DataFrame
df = pd.DataFrame({
    "A": range(1_000_000),
    "B": range(1_000_000),
    "C": range(1_000_000)
})

# Тази операция НЕ копира данните веднага
df_subset = df[["A", "B"]]

# Копирането се извършва едва при модификация
df_subset["A"] = df_subset["A"] * 2

# Оригиналният DataFrame остава непроменен
print(df["A"].head())  # 0, 1, 2, 3, 4
print(df_subset["A"].head())  # 0, 2, 4, 6, 8

Модерни практики с Pandas

Метод чейнинг (method chaining) е подход, който лично аз препоръчвам на всеки, който пише Pandas код. Вместо купчина междинни променливи, свързвате операциите в един четим поток. Кодът изглежда по-чист и е много по-лесен за поддръжка:

import pandas as pd

# Пример за чист пайплайн с method chaining
result = (
    pd.read_csv("transactions.csv", engine="pyarrow")
    .query("amount > 0")
    .assign(
        date=lambda x: pd.to_datetime(x["date"]),
        month=lambda x: x["date"].dt.month,
        category_upper=lambda x: x["category"].str.upper()
    )
    .groupby(["month", "category_upper"])
    .agg(
        total_amount=("amount", "sum"),
        avg_amount=("amount", "mean"),
        count=("amount", "count")
    )
    .reset_index()
    .sort_values("total_amount", ascending=False)
)

print(result.head(10))

Polars: Скоростта на Rust в Python

Защо Polars е толкова бърз?

Polars е написан на Rust и — честно казано — когато за пръв път видях бенчмарките, не повярвах. Говорим за 10 до 100 пъти по-висока производителност от традиционния Pandas за редица операции. Как го постига?

  • Мързелива оценка (Lazy Evaluation): Polars може да отложи изпълнението и да оптимизира целия план, преди реално да обработи данните
  • Многонишково изпълнение: За разлика от Pandas, Polars автоматично използва всички налични ядра — без да пишете допълнителен код
  • Оптимизирана памет: Apache Arrow формат за данни, който минимизира копирането и подобрява кеширането
  • Оптимизатор на заявки: Подобно на SQL двигателите, Polars пренарежда и опростява операциите автоматично

Основни операции в Polars

import polars as pl

# Четене на CSV файл
df = pl.read_csv("sales_data.csv")

# Основни трансформации
result = df.filter(
    pl.col("price") > 10
).select([
    pl.col("product_name"),
    pl.col("price"),
    pl.col("quantity"),
    (pl.col("price") * pl.col("quantity")).alias("total_revenue")
]).sort("total_revenue", descending=True)

print(result.head(10))

Мързелива оценка — ключът към производителността

Мързеливата оценка е може би най-мощната функция на Polars. Вместо да изпълнява всяка операция веднага, Polars изгражда план за изпълнение и го оптимизира. На практика, това означава, че ненужни стъпки просто отпадат, а останалите се пренареждат за максимална ефективност.

import polars as pl

# Мързеливо четене — данните НЕ се зареждат веднага
lazy_df = pl.scan_csv("large_dataset.csv")

# Изграждане на план за обработка
result = (
    lazy_df
    .filter(pl.col("status") == "active")
    .with_columns([
        (pl.col("revenue") - pl.col("cost")).alias("profit"),
        pl.col("date").str.to_datetime("%Y-%m-%d").alias("parsed_date")
    ])
    .group_by("region")
    .agg([
        pl.col("profit").sum().alias("total_profit"),
        pl.col("profit").mean().alias("avg_profit"),
        pl.col("customer_id").n_unique().alias("unique_customers")
    ])
    .sort("total_profit", descending=True)
    .limit(20)
)

# Визуализация на плана за изпълнение
print(result.explain())

# Реално изпълнение — СЕГА данните се обработват
final_result = result.collect()
print(final_result)

Бенчмарки: Polars срещу Pandas

Нека видим конкретни числа. Реалните бенчмарки от 2026 година са доста показателни:

  • Четене на CSV: Polars е 7.7 пъти по-бърз от Pandas
  • GroupBy операции: Polars е 8.7 пъти по-бърз
  • Join операции: Polars е 5 пъти по-бърз
  • Четене на Excel: Polars е 10-12 пъти по-бърз (да, правилно прочетохте)
  • Потребление на памет: 30-60% по-ниско пиково потребление при големи join операции

Ето как можете сами да направите бърз бенчмарк:

import time
import pandas as pd
import polars as pl

# Бенчмарк за четене на CSV
file_path = "large_file.csv"  # 1GB CSV файл

# Pandas
start = time.perf_counter()
df_pandas = pd.read_csv(file_path)
pandas_time = time.perf_counter() - start
print(f"Pandas: {pandas_time:.2f} секунди")

# Polars
start = time.perf_counter()
df_polars = pl.read_csv(file_path)
polars_time = time.perf_counter() - start
print(f"Polars: {polars_time:.2f} секунди")

print(f"Polars е {pandas_time / polars_time:.1f}x по-бърз")

DuckDB: SQL мощ директно в Python

Какво представлява DuckDB?

DuckDB е вградена аналитична база данни, която работи директно в процеса на Python — без сървър, без конфигурация. Просто pip install duckdb и сте готови. Тя е оптимизирана за аналитични заявки (OLAP) и може да обработва данни директно от Parquet, CSV и JSON файлове, дори без да ги зарежда изцяло в паметта.

Ако обичате SQL (а кой дейта специалист не го обича поне малко?), DuckDB ще ви се стори като подарък.

Основни предимства на DuckDB

  • SQL интерфейс: Познат SQL синтаксис за сложни аналитични заявки — без компромиси
  • Нулева конфигурация: Работи директно, без инсталация на сървър
  • Поточна обработка: Може да обработва файлове, по-големи от наличната RAM, чрез автоматично изнасяне на данни на диск
  • Директна интеграция с Pandas и Polars: Работи директно с DataFrame обекти без копиране на данни
import duckdb

# Заявка директно към CSV файл — без зареждане в паметта
result = duckdb.sql("""
    SELECT
        region,
        product_category,
        SUM(revenue) as total_revenue,
        AVG(revenue) as avg_revenue,
        COUNT(*) as transaction_count,
        SUM(revenue) - SUM(cost) as total_profit
    FROM 'sales_2024_*.csv'
    WHERE status = 'completed'
    GROUP BY region, product_category
    HAVING total_revenue > 100000
    ORDER BY total_revenue DESC
    LIMIT 20
""")

print(result.df())  # Конвертиране към Pandas DataFrame

DuckDB с Parquet файлове

Parquet е колонен формат за съхранение, който е особено ефективен за аналитични заявки. DuckDB работи с Parquet файлове наистина добре — и комбинацията от двете е нещо, което трябва да пробвате:

import duckdb

# Заявка към Parquet файлове с glob pattern
result = duckdb.sql("""
    SELECT
        DATE_TRUNC('month', order_date) as month,
        COUNT(DISTINCT customer_id) as unique_customers,
        SUM(order_total) as monthly_revenue,
        AVG(order_total) as avg_order_value,
        PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY order_total) as median_order
    FROM 'data/orders/**/*.parquet'
    WHERE order_date >= '2025-01-01'
    GROUP BY month
    ORDER BY month
""")

# Директно конвертиране към Polars DataFrame
polars_df = result.pl()
print(polars_df)

Интеграция с Pandas DataFrames

Едно от наистина готините неща при DuckDB е, че може да работи директно с Pandas DataFrames чрез SQL. Без копиране, без конвертиране — просто пишете SQL и използвате имената на вашите DataFrames като таблици:

import pandas as pd
import duckdb

# Създаване на Pandas DataFrame
customers = pd.DataFrame({
    "id": [1, 2, 3, 4, 5],
    "name": ["Иван", "Мария", "Петър", "Елена", "Георги"],
    "city": ["София", "Пловдив", "София", "Варна", "Пловдив"]
})

orders = pd.DataFrame({
    "order_id": range(1, 11),
    "customer_id": [1, 2, 1, 3, 4, 2, 5, 1, 3, 4],
    "amount": [150, 200, 75, 300, 180, 95, 220, 160, 140, 250]
})

# SQL заявка директно върху Pandas DataFrames
result = duckdb.sql("""
    SELECT
        c.name,
        c.city,
        COUNT(o.order_id) as order_count,
        SUM(o.amount) as total_spent,
        AVG(o.amount) as avg_order
    FROM customers c
    JOIN orders o ON c.id = o.customer_id
    GROUP BY c.name, c.city
    ORDER BY total_spent DESC
""")

print(result.df())

Сравнителен анализ: Кога кой инструмент да използвате

Добре, стигнахме до най-важния въпрос. Кой инструмент за какво е най-подходящ?

Pandas — най-добрият избор когато:

  • Работите с данни, които се побират в паметта (до няколко GB)
  • Имате нужда от богатата екосистема — scikit-learn, matplotlib, seaborn и т.н.
  • Правите интерактивен анализ в Jupyter notebook
  • Екипът ви вече е свикнал с Pandas API и имате съществуващ код
  • Нуждаете се от специфична функционалност, която все още липсва в Polars

Polars — най-добрият избор когато:

  • Производителността е критична и работите с големи файлове
  • Искате да използвате всички CPU ядра автоматично, без допълнителна конфигурация
  • Изграждате пайплайни, които се възползват от мързеливата оценка
  • Паметта е ограничена и трябва да обработвате данни максимално ефективно
  • Пишете нов код без наследствена зависимост от Pandas

DuckDB — най-добрият избор когато:

  • Предпочитате SQL за аналитични заявки (и знаете го добре)
  • Работите с файлове, по-големи от наличната RAM
  • Нуждаете се от сложни SQL операции — window функции, CTE, подзаявки
  • Искате да заявявате Parquet, CSV или JSON файлове директно, без предварително зареждане
  • Търсите мост между SQL и Python DataFrames

Window функции и сложни аналитики с DuckDB

Едно от най-силните предимства на DuckDB е поддръжката на усъвършенствани SQL конструкции. Window функции, CTE-та и — специално внимание тук — QUALIFY клаузата. Последната е уникална за DuckDB и значително опростява филтрирането по резултати от window функции.

Пример с window функции

import duckdb

# Сложна аналитика с window функции
result = duckdb.sql("""
    WITH ranked_products AS (
        SELECT
            category,
            product_name,
            SUM(quantity * price) as total_sales,
            ROW_NUMBER() OVER (
                PARTITION BY category
                ORDER BY SUM(quantity * price) DESC
            ) as rank_in_category,
            SUM(SUM(quantity * price)) OVER (
                PARTITION BY category
            ) as category_total,
            SUM(quantity * price) / SUM(SUM(quantity * price)) OVER (
                PARTITION BY category
            ) * 100 as pct_of_category,
            LAG(SUM(quantity * price)) OVER (
                PARTITION BY category
                ORDER BY SUM(quantity * price) DESC
            ) as prev_product_sales
        FROM 'data/sales_*.parquet'
        GROUP BY category, product_name
    )
    SELECT
        category,
        product_name,
        total_sales,
        rank_in_category,
        ROUND(pct_of_category, 2) as market_share_pct,
        ROUND(category_total, 2) as category_total,
        CASE
            WHEN rank_in_category = 1 THEN 'Лидер'
            WHEN rank_in_category <= 3 THEN 'Топ 3'
            WHEN pct_of_category > 10 THEN 'Значителен'
            ELSE 'Останали'
        END as product_tier
    FROM ranked_products
    WHERE rank_in_category <= 10
    ORDER BY category, rank_in_category
""")

print(result.df())

QUALIFY клаузата — уникална за DuckDB

QUALIFY е мощна клауза, която позволява филтриране директно по резултати от window функции — без нужда от подзаявка. Разликата в четимостта е значителна. Сравнете двата подхода:

import duckdb

# Без QUALIFY — традиционен подход с подзаявка
traditional = duckdb.sql("""
    SELECT * FROM (
        SELECT
            customer_id,
            order_id,
            amount,
            order_date,
            ROW_NUMBER() OVER (
                PARTITION BY customer_id
                ORDER BY amount DESC
            ) as rn
        FROM orders
    ) sub
    WHERE rn = 1
""")

# С QUALIFY — по-кратък и по-четим синтаксис
modern = duckdb.sql("""
    SELECT
        customer_id,
        order_id,
        amount,
        order_date
    FROM orders
    QUALIFY ROW_NUMBER() OVER (
        PARTITION BY customer_id
        ORDER BY amount DESC
    ) = 1
""")

Работа с времеви серии

Анализът на времеви серии е нещо, с което почти всеки дейта специалист се сблъсква рано или късно. И трите инструмента предлагат различен подход — нека видим какъв.

Времеви серии с Polars

Polars има наистина удобни специализирани методи за дати и часове. Плъзгащи средни стойности, процентни промени, месечна статистика — всичко работи бързо и елегантно:

import polars as pl

# Генериране на времеви данни
df = pl.DataFrame({
    "date": pl.date_range(
        pl.date(2024, 1, 1),
        pl.date(2025, 12, 31),
        eager=True
    ),
}).with_columns(
    pl.Series("value", [
        100 + i * 0.5 + (i % 30) * 2
        for i in range(731)
    ])
)

# Анализ на времеви серии
analysis = (
    df
    .with_columns([
        pl.col("date").dt.month().alias("month"),
        pl.col("date").dt.weekday().alias("weekday"),
        pl.col("date").dt.quarter().alias("quarter"),
    ])
    # Плъзгащи средни стойности
    .with_columns([
        pl.col("value").rolling_mean(window_size=7).alias("ma_7d"),
        pl.col("value").rolling_mean(window_size=30).alias("ma_30d"),
        pl.col("value").rolling_std(window_size=7).alias("volatility_7d"),
    ])
    # Процентна промяна
    .with_columns([
        pl.col("value").pct_change().alias("daily_return"),
        (pl.col("value") / pl.col("value").shift(7) - 1)
            .alias("weekly_return"),
    ])
)

# Месечна статистика
monthly_stats = (
    analysis
    .group_by(["month"])
    .agg([
        pl.col("value").mean().alias("avg_value"),
        pl.col("value").std().alias("std_value"),
        pl.col("value").min().alias("min_value"),
        pl.col("value").max().alias("max_value"),
        pl.col("daily_return").mean().alias("avg_daily_return"),
    ])
    .sort("month")
)

print(monthly_stats)

Ресемплиране с DuckDB

DuckDB предлага елегантен SQL подход за ресемплиране на времеви серии. Особено полезен е при работа с неравномерно разпределени данни — нещо, което в Pandas често се решава по-тромаво:

import duckdb

# Ресемплиране от минутни към часови данни
hourly = duckdb.sql("""
    SELECT
        DATE_TRUNC('hour', timestamp) as hour,
        AVG(temperature) as avg_temp,
        MIN(temperature) as min_temp,
        MAX(temperature) as max_temp,
        COUNT(*) as readings_count,
        STDDEV(temperature) as temp_stddev
    FROM sensor_readings
    WHERE timestamp >= '2025-01-01'
    GROUP BY hour
    ORDER BY hour
""")

# Попълване на липсващи часове с generate_series
filled = duckdb.sql("""
    WITH hours AS (
        SELECT UNNEST(
            generate_series(
                TIMESTAMP '2025-01-01 00:00:00',
                TIMESTAMP '2025-12-31 23:00:00',
                INTERVAL '1 hour'
            )
        ) as hour
    ),
    data AS (
        SELECT
            DATE_TRUNC('hour', timestamp) as hour,
            AVG(temperature) as avg_temp
        FROM sensor_readings
        GROUP BY hour
    )
    SELECT
        h.hour,
        COALESCE(d.avg_temp, LAG(d.avg_temp) OVER (ORDER BY h.hour)) as avg_temp
    FROM hours h
    LEFT JOIN data d ON h.hour = d.hour
    ORDER BY h.hour
""")

print(filled.df())

Обработка на текстови данни

Работата с текстови данни е друга област, в която трите инструмента имат различни силни страни. Ето практическо сравнение — обработка на клиентски отзиви с Polars и DuckDB:

import polars as pl
import pandas as pd
import duckdb

# Пример с текстови данни — обработка на клиентски отзиви

# === Polars подход ===
reviews_pl = pl.DataFrame({
    "review": [
        "Продуктът е СТРАХОТЕН! Препоръчвам на всички.",
        "Лошо качество, връщам обратно...",
        "Добра цена за качеството. 5 звезди!",
        "Не отговаря на описанието. Разочарован съм.",
        "Перфектно! Ще поръчам отново 100%"
    ]
})

polars_result = reviews_pl.with_columns([
    pl.col("review").str.to_lowercase().alias("lower_review"),
    pl.col("review").str.len_chars().alias("review_length"),
    pl.col("review").str.contains("(?i)препоръчвам|страхотен|перфектно|добр")
        .alias("is_positive"),
    pl.col("review").str.split(" ").list.len().alias("word_count"),
    pl.col("review").str.extract(r"(\d+)", 1).alias("mentioned_number"),
])

print("Polars резултат:")
print(polars_result)

# === DuckDB подход ===
reviews_text = [
    "Продуктът е СТРАХОТЕН! Препоръчвам на всички.",
    "Лошо качество, връщам обратно...",
    "Добра цена за качеството. 5 звезди!",
    "Не отговаря на описанието. Разочарован съм.",
    "Перфектно! Ще поръчам отново 100%"
]

duckdb_result = duckdb.sql("""
    SELECT
        review,
        LOWER(review) as lower_review,
        LENGTH(review) as review_length,
        regexp_matches(LOWER(review),
            'препоръчвам|страхотен|перфектно|добр') as is_positive,
        array_length(string_split(review, ' ')) as word_count
    FROM (
        SELECT UNNEST($1::VARCHAR[]) as review
    )
""", params=[reviews_text])

print("\nDuckDB резултат:")
print(duckdb_result.df())

Изграждане на хибриден пайплайн: Практически пример

Добре, стигнахме до най-интересната част. Модерният подход през 2026 е да комбинирате и трите инструмента в един пайплайн — всеки прави това, което е най-добър. DuckDB извлича и агрегира данните, Polars ги трансформира бързо, а Pandas се грижи за интеграцията с машинно учене. Ето пълен пример:

import duckdb
import polars as pl
import pandas as pd
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
import matplotlib.pyplot as plt

# ===== ЕТАП 1: Извличане на данни с DuckDB =====
# DuckDB за ефективно четене и SQL агрегации
print("Етап 1: Извличане и агрегиране на данни с DuckDB...")

raw_data = duckdb.sql("""
    WITH monthly_sales AS (
        SELECT
            customer_id,
            DATE_TRUNC('month', sale_date) as sale_month,
            SUM(amount) as monthly_total,
            COUNT(*) as transaction_count,
            AVG(amount) as avg_transaction
        FROM 'data/sales_*.parquet'
        WHERE sale_date >= '2025-01-01'
          AND status != 'cancelled'
        GROUP BY customer_id, sale_month
    )
    SELECT
        customer_id,
        COUNT(DISTINCT sale_month) as active_months,
        SUM(monthly_total) as total_revenue,
        AVG(monthly_total) as avg_monthly_revenue,
        MAX(monthly_total) as peak_month_revenue,
        SUM(transaction_count) as total_transactions,
        AVG(avg_transaction) as avg_transaction_value
    FROM monthly_sales
    GROUP BY customer_id
    HAVING active_months >= 3
""")

# ===== ЕТАП 2: Трансформация с Polars =====
# Polars за бърза обработка и feature engineering
print("Етап 2: Трансформация на данни с Polars...")

df_polars = raw_data.pl()

features_df = (
    df_polars
    .with_columns([
        (pl.col("total_revenue") / pl.col("total_transactions"))
            .alias("revenue_per_transaction"),
        (pl.col("peak_month_revenue") / pl.col("avg_monthly_revenue"))
            .alias("peak_to_avg_ratio"),
        pl.when(pl.col("active_months") >= 10)
            .then(pl.lit("loyal"))
            .when(pl.col("active_months") >= 6)
            .then(pl.lit("regular"))
            .otherwise(pl.lit("occasional"))
            .alias("customer_segment")
    ])
    .filter(pl.col("total_revenue") > 0)
)

print(f"Обработени клиенти: {features_df.shape[0]}")
print(features_df.describe())

# ===== ЕТАП 3: Машинно учене с Pandas + scikit-learn =====
# Pandas за интеграция със scikit-learn
print("Етап 3: Клъстериране с scikit-learn...")

df_pandas = features_df.to_pandas()

# Избор на числови характеристики за клъстериране
feature_columns = [
    "total_revenue",
    "avg_monthly_revenue",
    "total_transactions",
    "avg_transaction_value",
    "revenue_per_transaction",
    "peak_to_avg_ratio"
]

# Стандартизация
scaler = StandardScaler()
scaled_features = scaler.fit_transform(df_pandas[feature_columns])

# K-Means клъстериране
kmeans = KMeans(n_clusters=4, random_state=42, n_init=10)
df_pandas["cluster"] = kmeans.fit_predict(scaled_features)

# ===== ЕТАП 4: Визуализация и резултати =====
print("Етап 4: Генериране на отчети...")

cluster_summary = df_pandas.groupby("cluster").agg({
    "total_revenue": ["mean", "sum", "count"],
    "total_transactions": "mean",
    "avg_transaction_value": "mean"
}).round(2)

print("\nРезюме по клъстери:")
print(cluster_summary)

Оптимизация на производителността: Практически съвети

1. Избор на правилния формат за съхранение

Ако все още съхранявате данните си като CSV, спрете и прочетете тази секция. Parquet е колонен формат, който предлага компресия и бърз достъп до отделни колони. Разликата в производителността е драматична:

import polars as pl
import time

# Сравнение на формати: CSV vs Parquet
# Запис
df = pl.read_csv("large_data.csv")

start = time.perf_counter()
df.write_parquet("large_data.parquet", compression="zstd")
parquet_write = time.perf_counter() - start

# Четене на Parquet — значително по-бързо
start = time.perf_counter()
df_parquet = pl.read_parquet("large_data.parquet")
parquet_read = time.perf_counter() - start

start = time.perf_counter()
df_csv = pl.read_csv("large_data.csv")
csv_read = time.perf_counter() - start

print(f"CSV четене: {csv_read:.2f}s")
print(f"Parquet четене: {parquet_read:.2f}s")
print(f"Parquet е {csv_read / parquet_read:.1f}x по-бърз")

2. Оптимизация на типовете данни

Правилният избор на типове данни може да намали потреблението на памет с 50-80%. Да, толкова много. Ето една удобна функция, която прави това автоматично:

import pandas as pd
import numpy as np

def optimize_dtypes(df):
    """Автоматична оптимизация на типовете данни в Pandas DataFrame."""
    start_mem = df.memory_usage(deep=True).sum() / 1024**2

    for col in df.columns:
        col_type = df[col].dtype

        if col_type == "object":
            num_unique = df[col].nunique()
            num_total = len(df[col])
            # Ако уникалните стойности са < 50% от общите,
            # конвертираме към category
            if num_unique / num_total < 0.5:
                df[col] = df[col].astype("category")

        elif col_type in ["int64", "int32"]:
            col_min = df[col].min()
            col_max = df[col].max()
            if col_min >= 0:
                if col_max < 255:
                    df[col] = df[col].astype(np.uint8)
                elif col_max < 65535:
                    df[col] = df[col].astype(np.uint16)
                elif col_max < 4294967295:
                    df[col] = df[col].astype(np.uint32)
            else:
                if col_min > -128 and col_max < 127:
                    df[col] = df[col].astype(np.int8)
                elif col_min > -32768 and col_max < 32767:
                    df[col] = df[col].astype(np.int16)
                elif col_min > -2147483648 and col_max < 2147483647:
                    df[col] = df[col].astype(np.int32)

        elif col_type == "float64":
            df[col] = df[col].astype(np.float32)

    end_mem = df.memory_usage(deep=True).sum() / 1024**2
    reduction = (1 - end_mem / start_mem) * 100
    print(f"Памет: {start_mem:.1f} MB → {end_mem:.1f} MB "
          f"(намаление с {reduction:.1f}%)")
    return df

# Пример за употреба
df = pd.read_csv("large_dataset.csv")
df = optimize_dtypes(df)

3. Паралелна обработка с Polars

Polars предлага вградена поддръжка за паралелна обработка и го прави без да ви кара да мислите за нишки и процеси. Ето пример за обработка на множество файлове:

import polars as pl
from pathlib import Path

# Мързеливо четене на множество файлове
data_dir = Path("data/monthly_reports")
parquet_files = list(data_dir.glob("*.parquet"))

# Polars автоматично паралелизира обработката
lazy_frames = [
    pl.scan_parquet(f)
    .filter(pl.col("amount").is_not_null())
    .with_columns([
        pl.col("date").str.to_datetime("%Y-%m-%d"),
        pl.col("amount").cast(pl.Float64)
    ])
    for f in parquet_files
]

# Обединяване и агрегиране
combined = (
    pl.concat(lazy_frames)
    .group_by([
        pl.col("date").dt.year().alias("year"),
        pl.col("date").dt.month().alias("month"),
        "category"
    ])
    .agg([
        pl.col("amount").sum().alias("total"),
        pl.col("amount").mean().alias("average"),
        pl.len().alias("count")
    ])
    .sort(["year", "month"])
    .collect()  # Изпълнение на целия план
)

print(combined)

Интеграция с машинно учене

Един от въпросите, който често чувам, е: „Добре, Polars е бърз, но как работи със scikit-learn?" Отговорът е — доста добре, просто трябва да знаете как да конвертирате. Ето практически пример за предсказване на клиентски отлив:

import polars as pl
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report

# Подготовка на данни с Polars (бързо и ефективно)
df = (
    pl.scan_parquet("customer_churn.parquet")
    .with_columns([
        (pl.col("last_purchase_days") > 90).cast(pl.Int8).alias("churned"),
        pl.col("total_purchases").log1p().alias("log_purchases"),
        (pl.col("total_spent") / pl.col("total_purchases"))
            .alias("avg_order_value"),
        pl.col("support_tickets").fill_null(0)
    ])
    .select([
        "log_purchases", "avg_order_value", "support_tickets",
        "account_age_days", "email_opens_rate", "churned"
    ])
    .drop_nulls()
    .collect()
)

# Конвертиране към NumPy за scikit-learn
feature_cols = [c for c in df.columns if c != "churned"]
X = df.select(feature_cols).to_numpy()
y = df.select("churned").to_numpy().ravel()

# Разделяне и обучение
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

model = RandomForestClassifier(n_estimators=100, random_state=42, n_jobs=-1)
model.fit(X_train, y_train)

# Оценка
y_pred = model.predict(X_test)
print(classification_report(
    y_test, y_pred,
    target_names=["Активен", "Напуснал"]
))

# Важност на характеристиките
importance = pl.DataFrame({
    "feature": feature_cols,
    "importance": model.feature_importances_
}).sort("importance", descending=True)

print("\nВажност на характеристиките:")
print(importance)

Мониторинг и профилиране на пайплайни

Когато пайплайнът ви стане достатъчно сложен (а това се случва по-бързо, отколкото очаквате), ще ви трябва начин да следите какво колко време отнема. Ето един практически клас за мониторинг, който можете да вградите в кода си:

import time
import psutil
import functools
from dataclasses import dataclass, field

@dataclass
class PipelineMetrics:
    """Клас за събиране на метрики от пайплайна."""
    steps: list = field(default_factory=list)

    def log_step(self, name, duration, memory_mb, rows_processed=0):
        self.steps.append({
            "name": name,
            "duration_sec": round(duration, 2),
            "memory_mb": round(memory_mb, 1),
            "rows": rows_processed
        })

    def summary(self):
        total_time = sum(s["duration_sec"] for s in self.steps)
        peak_memory = max(s["memory_mb"] for s in self.steps)
        total_rows = sum(s["rows"] for s in self.steps)
        print(f"\n{'='*50}")
        print(f"Обобщение на пайплайна")
        print(f"{'='*50}")
        print(f"Общо време: {total_time:.2f} секунди")
        print(f"Пикова памет: {peak_memory:.1f} MB")
        print(f"Обработени редове: {total_rows:,}")
        print(f"\nДетайли по стъпки:")
        for step in self.steps:
            print(f"  {step['name']}: {step['duration_sec']}s, "
                  f"{step['memory_mb']}MB, {step['rows']:,} реда")


def monitor_step(metrics, step_name):
    """Декоратор за мониторинг на стъпки от пайплайна."""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            process = psutil.Process()
            start_time = time.perf_counter()
            start_mem = process.memory_info().rss / 1024**2

            result = func(*args, **kwargs)

            duration = time.perf_counter() - start_time
            current_mem = process.memory_info().rss / 1024**2

            rows = len(result) if hasattr(result, '__len__') else 0
            metrics.log_step(step_name, duration, current_mem, rows)

            return result
        return wrapper
    return decorator


# Пример за употреба
metrics = PipelineMetrics()

@monitor_step(metrics, "Зареждане на данни")
def load_data(path):
    import polars as pl
    return pl.read_parquet(path)

@monitor_step(metrics, "Трансформация")
def transform_data(df):
    return df.filter(pl.col("amount") > 0).with_columns([
        (pl.col("amount") * 1.2).alias("amount_with_tax")
    ])

@monitor_step(metrics, "Агрегация")
def aggregate_data(df):
    return df.group_by("category").agg([
        pl.col("amount_with_tax").sum().alias("total")
    ])

# Изпълнение на пайплайна
data = load_data("sales.parquet")
transformed = transform_data(data)
result = aggregate_data(transformed)

metrics.summary()

Често срещани грешки и как да ги избегнете

Нека бъдем честни — всеки от нас е допускал тези грешки поне веднъж. Ето най-честите капани при работа с Pandas, Polars и DuckDB:

1. Итериране ред по ред в Pandas

Това е класика. Ако ползвате iterrows() за обработка на данни, спрете веднага. Разликата в производителността може да е стотици пъти:

import pandas as pd
import numpy as np

df = pd.DataFrame({
    "value": np.random.randn(1_000_000),
    "category": np.random.choice(["A", "B", "C"], 1_000_000)
})

# ГРЕШНО — изключително бавно
# result = []
# for idx, row in df.iterrows():
#     if row["value"] > 0:
#         result.append(row["value"] * 2)
#     else:
#         result.append(row["value"])
# df["result"] = result

# ПРАВИЛНО — векторизирана операция (100x по-бързо)
df["result"] = np.where(df["value"] > 0, df["value"] * 2, df["value"])

2. Игнориране на типовете при конвертиране между инструменти

При прехвърляне на данни между Polars, Pandas и DuckDB типовете могат да се променят неочаквано. Винаги проверявайте типовете след конвертиране — това ще ви спести главоболия по-нататък:

import polars as pl
import pandas as pd

# Polars DataFrame с конкретни типове
df_pl = pl.DataFrame({
    "id": pl.Series([1, 2, 3], dtype=pl.UInt32),
    "value": pl.Series([1.5, 2.7, 3.14], dtype=pl.Float32),
    "date": pl.Series(["2025-01-01", "2025-02-01", "2025-03-01"])
        .str.to_date("%Y-%m-%d")
})

# Конвертиране към Pandas — проверете типовете!
df_pd = df_pl.to_pandas()
print("Pandas типове след конвертиране:")
print(df_pd.dtypes)
# id       uint32
# value    float32
# date     object  <-- Внимание! Датата може да стане object

# Обратно конвертиране — отново проверка
df_pl_back = pl.from_pandas(df_pd)
print("\nPolars типове след обратно конвертиране:")
print(df_pl_back.dtypes)

3. Зареждане на цели файлове когато трябват само части от тях

Това е друга честа грешка, особено при работа с големи файлове. Зареждайте само необходимите колони и редове — разликата в потреблението на памет може да е десетократна:

import polars as pl

# ГРЕШНО — зареждане на всичко
# df = pl.read_parquet("huge_file.parquet")
# result = df.select(["col_a", "col_b"]).filter(pl.col("col_a") > 100)

# ПРАВИЛНО — зареждане само на нужните колони и филтриране
result = (
    pl.scan_parquet("huge_file.parquet")
    .select(["col_a", "col_b"])
    .filter(pl.col("col_a") > 100)
    .collect()
)

# Или с DuckDB — четене на конкретни колони от Parquet
import duckdb
result_sql = duckdb.sql("""
    SELECT col_a, col_b
    FROM 'huge_file.parquet'
    WHERE col_a > 100
""")

Практически съвети за миграция от Pandas към Polars

Ако решите да мигрирате част от Pandas кода си към Polars, ето бърза шпаргалка с най-честите съответствия. Запазете я някъде — ще ви потрябва:

import polars as pl
import pandas as pd

# =============================================
# Съответствия между Pandas и Polars
# =============================================

# 1. Филтриране на редове
# Pandas:  df[df["age"] > 30]
# Polars:  df.filter(pl.col("age") > 30)

# 2. Избор на колони
# Pandas:  df[["name", "age"]]
# Polars:  df.select(["name", "age"])

# 3. Създаване на нова колона
# Pandas:  df["total"] = df["price"] * df["qty"]
# Polars:  df.with_columns(
#              (pl.col("price") * pl.col("qty")).alias("total")
#          )

# 4. GroupBy агрегация
# Pandas:  df.groupby("city").agg({"sales": "sum"})
# Polars:  df.group_by("city").agg(
#              pl.col("sales").sum()
#          )

# 5. Сортиране
# Pandas:  df.sort_values("price", ascending=False)
# Polars:  df.sort("price", descending=True)

# 6. Преименуване на колони
# Pandas:  df.rename(columns={"old": "new"})
# Polars:  df.rename({"old": "new"})

# 7. Попълване на липсващи стойности
# Pandas:  df["col"].fillna(0)
# Polars:  df.with_columns(pl.col("col").fill_null(0))

# 8. Уникални стойности
# Pandas:  df["city"].unique()
# Polars:  df.select(pl.col("city").unique())

# 9. Прилагане на функция
# Pandas:  df["col"].apply(lambda x: x**2)
# Polars:  df.with_columns(pl.col("col").pow(2))
#          или: df.with_columns(
#              pl.col("col").map_elements(lambda x: x**2)
#          )

# 10. Merge / Join
# Pandas:  pd.merge(df1, df2, on="id", how="left")
# Polars:  df1.join(df2, on="id", how="left")

Важно: не е нужно да мигрирате всичко наведнъж. Анализирайте къде производителността наистина е проблем и фокусирайте усилията си там. За интерактивен анализ в Jupyter notebooks Pandas може да е напълно достатъчен — и в това няма нищо лошо.

Бъдещето: Какво да очакваме

Екосистемата на Python за данни продължава да се развива с бързи темпове. Ето тенденциите, които очаквам да се засилят:

  • Pandas 3.0 с задължителен PyArrow: Това ще донесе значителни подобрения в производителността и по-добра съвместимост с останалите инструменти
  • Polars в продуктивна среда: Все повече компании мигрират критични пайплайни от Pandas към Polars
  • DuckDB като стандарт за ad-hoc анализи: Бързи SQL анализи без нужда от инфраструктура — DuckDB се утвърждава все по-силно
  • Хибридни архитектури: Комбинацията DuckDB + Polars + Pandas в един пайплайн се превръща в стандартна практика
  • ADBC (Arrow Database Connectivity): Новият стандарт за комуникация с бази данни, който ще замени ODBC/JDBC за аналитични натоварвания

Заключение

Обработката на данни в Python през 2026 не е въпрос на „Pandas срещу Polars срещу DuckDB". Истината е, че всеки от тях си има своето място и модерният подход е да ги комбинирате. DuckDB за мощни SQL заявки и данни, по-големи от паметта. Polars за високопроизводителни трансформации. Pandas за екосистемата и интеграцията с машинно учене.

Ето какво бих препоръчал на всеки дейта специалист:

  1. Научете и трите инструмента — всеки от тях има своето място в модерния пайплайн
  2. Преминете към Parquet формат за съхранение — CSV е минало (поне за данни, по-големи от няколко мегабайта)
  3. Активирайте PyArrow бекенда в Pandas — незабавно подобрение с минимално усилие
  4. Профилирайте преди да оптимизирате — не гадайте къде е проблемът
  5. Мигрирайте постепенно — не е нужно да пренаписвате всичко наведнъж

С правилната комбинация от инструменти можете да постигнете производителност, която преди изискваше специализирани Big Data решения — но с простотата и елегантността на Python.