Въведение: Новата ера на обработка на данни в 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 за екосистемата и интеграцията с машинно учене.
Ето какво бих препоръчал на всеки дейта специалист:
- Научете и трите инструмента — всеки от тях има своето място в модерния пайплайн
- Преминете към Parquet формат за съхранение — CSV е минало (поне за данни, по-големи от няколко мегабайта)
- Активирайте PyArrow бекенда в Pandas — незабавно подобрение с минимално усилие
- Профилирайте преди да оптимизирате — не гадайте къде е проблемът
- Мигрирайте постепенно — не е нужно да пренаписвате всичко наведнъж
С правилната комбинация от инструменти можете да постигнете производителност, която преди изискваше специализирани Big Data решения — но с простотата и елегантността на Python.