Polars: کتابخانه DataFrame فوق‌سریع پایتون — راهنمای جامع از صفر تا پیشرفته

Polars یک کتابخانه DataFrame فوق‌سریع پایتون با معماری Rust و Apache Arrow است. در این راهنما از نصب تا مفاهیم پیشرفته مثل Lazy Evaluation و Window Functions را با مثال‌های عملی یاد می‌گیرید.

مقدمه: Polars چیست و چرا باید بشناسیدش؟

خب، بیایید صادق باشیم. اگر تا حالا با Pandas کار کرده باشید، احتمالاً با مشکلات سرعت و مصرف حافظه‌اش آشنا هستید. مخصوصاً وقتی داده‌هایتان از چند میلیون سطر رد می‌شود، اوضاع جالب نیست! اینجاست که Polars وارد بازی می‌شود.

Polars یک کتابخانه DataFrame فوق‌سریع پایتون است که کاملاً به زبان Rust نوشته شده و از فرمت Apache Arrow برای مدیریت حافظه استفاده می‌کند. در سال ۲۰۲۶، با رسیدن به نسخه ۱.۳۸+، تبدیل به یکی از جدی‌ترین رقبای Pandas شده. اما چرا انقدر محبوب شده؟

  • سرعت باورنکردنی: تا ۱۰-۱۰۰ برابر سریع‌تر از Pandas، به‌خصوص روی داده‌های بزرگ
  • اجرای موازی: به صورت پیش‌فرض از چند هسته پردازنده استفاده می‌کند (بدون اینکه خودتان کاری بکنید!)
  • بهینه‌سازی خودکار: یک Query Optimizer داخلی داره که کوئری‌ها رو قبل از اجرا بهینه می‌کنه
  • مدیریت حافظه بهتر: Apache Arrow باعث می‌شه حافظه کمتری مصرف بشه
  • API مدرن و بیانگر: سیستم Expression-based که کدتان را هم خواناتر و هم قدرتمندتر می‌کند
  • Lazy و Eager Evaluation: می‌توانید بین اجرای فوری یا تنبل انتخاب کنید

نکته جالب اینه که Polars فقط برای داده‌های بزرگ نیست. حتی روی داده‌های کوچک هم عملکرد بهتری نسبت به Pandas داره. صادقانه بگویم، اگر در سال ۲۰۲۶ دارید تحلیل داده یاد می‌گیرید، Polars یکی از بهترین انتخاب‌هایی است که می‌توانید بکنید.

نصب و راه‌اندازی Polars

نصب Polars واقعاً ساده‌ست. فقط کافیه pip رو اجرا کنید:

pip install polars

اگر می‌خواهید قابلیت‌های اضافی مثل پشتیبانی از فرمت‌های مختلف فایل رو هم داشته باشید:

pip install 'polars[all]'

بعد از نصب، کافیه import کنید و شروع کنید:

import polars as pl

# بررسی نسخه نصب شده
print(pl.__version__)

طبق قرارداد رایج، Polars با نام مستعار pl وارد می‌شود — درست مثل pd برای Pandas.

مفاهیم پایه: Series و DataFrame

Series در Polars

Series در واقع یک آرایه یک‌بعدی از داده‌ها با یک نوع داده مشخص است. هر Series نمایانگر یک ستون از داده‌هاست:

import polars as pl

# ساخت یک Series
s = pl.Series("numbers", [1, 2, 3, 4, 5])
print(s)

# Series با نوع داده مشخص
s_float = pl.Series("decimals", [1.5, 2.5, 3.5], dtype=pl.Float32)
print(s_float)

# Series از رشته‌ها
s_str = pl.Series("names", ["Ali", "Zahra", "Mohammad"])
print(s_str)

خروجی چیزی شبیه این خواهد بود:

shape: (5,)
Series: 'numbers' [i64]
[
    1
    2
    3
    4
    5
]

DataFrame در Polars

DataFrame یک ساختار داده دو بعدی است — مجموعه‌ای از ستون‌ها (Series) که هر کدام می‌تواند نوع داده متفاوتی داشته باشد. اگر با Pandas کار کرده باشید، این مفهوم برایتان آشناست:

import polars as pl

# ساخت DataFrame از دیکشنری
df = pl.DataFrame({
    "name": ["Ali", "Zahra", "Mohammad", "Fatemeh"],
    "age": [25, 30, 35, 28],
    "city": ["Tehran", "Isfahan", "Shiraz", "Mashhad"],
    "salary": [50000, 65000, 70000, 58000]
})

print(df)
print(f"\nShape: {df.shape}")
print(f"\nColumns: {df.columns}")
print(f"\nDtypes: {df.dtypes}")

خروجی:

shape: (4, 4)
┌──────────┬─────┬─────────┬────────┐
│ name     ┆ age ┆ city    ┆ salary │
│ ---      ┆ --- ┆ ---     ┆ ---    │
│ str      ┆ i64 ┆ str     ┆ i64    │
╞══════════╪═════╪═════════╪════════╡
│ Ali      ┆ 25  ┆ Tehran  ┆ 50000  │
│ Zahra    ┆ 30  ┆ Isfahan ┆ 65000  │
│ Mohammad ┆ 35  ┆ Shiraz  ┆ 70000  │
│ Fatemeh  ┆ 28  ┆ Mashhad ┆ 58000  │
└──────────┴─────┴─────────┴────────┘

برای دیدن خلاصه آماری هم می‌توانید از این دستورات استفاده کنید:

# نمایش اطلاعات آماری
print(df.describe())

# نمایش چند سطر اول
print(df.head(2))

# نمایش چند سطر آخر
print(df.tail(2))

Eager و Lazy Evaluation: دو حالت اجرا در Polars

این بخش واقعاً مهمه. یکی از ویژگی‌های منحصر به فرد Polars پشتیبانی همزمان از دو حالت Eager (مشتاقانه) و Lazy (تنبل) است. درک این مفهوم کلید استفاده از قدرت واقعی Polars است.

Eager Evaluation (حالت پیش‌فرض)

در حالت Eager، عملیات بلافاصله اجرا می‌شوند و نتیجه برگردانده می‌شود. این شبیه نحوه کار Pandas است و برای کارهای ساده و اکتشاف اولیه داده‌ها خوبه:

import polars as pl

df = pl.DataFrame({
    "name": ["Ali", "Zahra", "Mohammad", "Fatemeh", "Reza"],
    "age": [25, 30, 35, 28, 42],
    "salary": [50000, 65000, 70000, 58000, 80000]
})

# عملیات Eager - بلافاصله اجرا می‌شود
result = df.filter(pl.col("age") > 28).select(["name", "salary"])
print(result)

Lazy Evaluation (حالت بهینه‌شده)

اینجا ماجرا جالب‌تر می‌شود. در حالت Lazy، عملیات اجرا نمی‌شوند بلکه یک نمودار محاسباتی (computation graph) ساخته می‌شود. وقتی .collect() رو فراخوانی می‌کنید، Polars اول تمام عملیات رو بهینه‌سازی می‌کنه و بعد اجرا می‌کنه:

import polars as pl

# ساخت LazyFrame
df_lazy = pl.DataFrame({
    "name": ["Ali", "Zahra", "Mohammad", "Fatemeh", "Reza"],
    "age": [25, 30, 35, 28, 42],
    "salary": [50000, 65000, 70000, 58000, 80000]
}).lazy()

# تعریف عملیات - هنوز اجرا نشده
query = (
    df_lazy
    .filter(pl.col("age") > 28)
    .select(["name", "salary"])
    .sort("salary", descending=True)
)

# مشاهده Query Plan بهینه‌شده
print(query.explain())

# اجرای واقعی
result = query.collect()
print(result)

مزایای Lazy Evaluation چیه؟

  • بهینه‌سازی خودکار: Polars عملیات‌ها رو مرتب‌سازی می‌کنه و غیرضروری‌ها رو حذف می‌کنه
  • Predicate Pushdown: فیلترها رو تا جایی که ممکنه به ابتدای زنجیره منتقل می‌کنه
  • Projection Pushdown: فقط ستون‌های موردنیاز رو می‌خونه
  • مصرف حافظه کمتر: فقط داده‌های ضروری در حافظه نگه داشته می‌شن

مقایسه عملی Eager و Lazy

بیایید با یک مثال واقعی تفاوتشان را ببینیم:

import polars as pl
import time

# ساخت DataFrame بزرگ‌تر
df = pl.DataFrame({
    "a": range(1000000),
    "b": range(1000000, 2000000),
    "c": range(2000000, 3000000)
})

# حالت Eager
start = time.time()
result_eager = (
    df
    .filter(pl.col("a") > 500000)
    .select(["a", "b"])
    .head(10)
)
eager_time = time.time() - start
print(f"Eager mode: {eager_time:.4f} seconds")

# حالت Lazy
start = time.time()
result_lazy = (
    df.lazy()
    .filter(pl.col("a") > 500000)
    .select(["a", "b"])
    .head(10)
    .collect()
)
lazy_time = time.time() - start
print(f"Lazy mode: {lazy_time:.4f} seconds")

در اکثر موارد، حالت Lazy سریع‌تره چون Polars می‌دونه که فقط ۱۰ سطر نیاز داریم. پس نیازی به پردازش کل DataFrame فیلترشده نیست — باهوشه، نه؟

سیستم Expressions: قلب تپنده Polars

اگر بخواهم یک چیز را به عنوان مهم‌ترین مفهوم Polars معرفی کنم، بدون شک Expression‌ها هستند. Expression‌ها عملیاتی هستند که روی ستون‌ها اعمال می‌شوند و می‌توانید آن‌ها را به صورت زنجیره‌ای ترکیب کنید. از نظر فنی، هر Expression یک تابع از نوع Fn(Series) -> Series است و نکته جالب اینه که تمام آن‌ها به صورت موازی اجرا می‌شوند.

مبانی Expressions

همه چیز با pl.col() شروع می‌شه که به یک ستون اشاره می‌کنه:

import polars as pl

df = pl.DataFrame({
    "name": ["Ali", "Zahra", "Mohammad", "Fatemeh"],
    "age": [25, 30, 35, 28],
    "salary": [50000, 65000, 70000, 58000]
})

# یک Expression ساده
result = df.select(pl.col("age"))

# Expression با عملیات
result = df.select(pl.col("salary") * 1.2)

# چند Expression همزمان
result = df.select([
    pl.col("name"),
    (pl.col("salary") * 1.2).alias("new_salary"),
    (pl.col("age") + 1).alias("next_year_age")
])
print(result)

Expressions پیشرفته

اینجاست که واقعاً قدرت Polars مشخص می‌شه. ببینید چطور می‌تونید محاسبات پیچیده رو به شکل تمیز و خوانا بنویسید:

import polars as pl

df = pl.DataFrame({
    "product": ["A", "B", "A", "C", "B", "A"],
    "revenue": [100, 200, 150, 300, 250, 180],
    "cost": [60, 120, 90, 180, 150, 100]
})

# محاسبات پیچیده با Expressions
result = df.select([
    pl.col("product"),
    pl.col("revenue"),
    pl.col("cost"),
    (pl.col("revenue") - pl.col("cost")).alias("profit"),
    ((pl.col("revenue") - pl.col("cost")) / pl.col("revenue") * 100)
        .alias("profit_margin")
        .round(2)
])
print(result)

Expression Functions

Polars تعداد زیادی تابع داخلی برای کار با Expression‌ها داره. اینجا چند تا از پرکاربردترین‌ها رو می‌بینید:

import polars as pl

df = pl.DataFrame({
    "name": ["Ali Mohammadi", "Zahra Ahmadi", "Mohammad Rezaei"],
    "email": ["[email protected]", "[email protected]", "[email protected]"],
    "score": [85.5, 92.3, 78.9]
})

result = df.select([
    pl.col("name").str.split(" ").list.first().alias("first_name"),
    pl.col("email").str.to_lowercase().alias("email_lower"),
    pl.col("score").round(0).alias("score_rounded"),
    pl.col("score").cast(pl.Int32).alias("score_int")
])
print(result)

Conditional Expressions

برای اعمال شرط‌ها در Expression‌ها از ساختار when/then/otherwise استفاده می‌کنیم. این یکی از زیباترین بخش‌های API در Polars به نظرم هست:

import polars as pl

df = pl.DataFrame({
    "student": ["Ali", "Zahra", "Mohammad", "Fatemeh"],
    "grade": [18, 12, 15, 9]
})

result = df.select([
    pl.col("student"),
    pl.col("grade"),
    pl.when(pl.col("grade") >= 17)
        .then(pl.lit("Excellent"))
        .when(pl.col("grade") >= 14)
        .then(pl.lit("Good"))
        .when(pl.col("grade") >= 10)
        .then(pl.lit("Pass"))
        .otherwise(pl.lit("Fail"))
        .alias("status")
])
print(result)

بارگذاری داده‌ها: CSV، Parquet، JSON و بیشتر

Polars از فرمت‌های مختلف فایل پشتیبانی می‌کنه و سرعت خواندنش واقعاً چشمگیره. یکی از مزایای بزرگ Polars اینه که می‌تونید فایل‌ها رو به صورت Lazy بخونید — این برای فایل‌های بزرگ یک نعمته.

خواندن فایل CSV

import polars as pl

# خواندن فایل CSV به صورت Eager
df = pl.read_csv("data.csv")

# خواندن با گزینه‌های مختلف
df = pl.read_csv(
    "data.csv",
    separator=",",
    has_header=True,
    skip_rows=0,
    n_rows=1000,  # فقط 1000 سطر اول
    columns=["name", "age", "salary"],  # فقط ستون‌های خاص
    dtypes={"age": pl.Int32, "salary": pl.Float64}
)

# خواندن به صورت Lazy (برای فایل‌های بزرگ بهتر است)
df_lazy = pl.scan_csv("large_file.csv")
result = df_lazy.filter(pl.col("age") > 30).collect()

خواندن فایل Parquet

Parquet یک فرمت ستونی بایناری است که برای داده‌های بزرگ فوق‌العاده کارآمده. Polars به طور طبیعی با Parquet سازگاره چون هر دو از Apache Arrow استفاده می‌کنند:

import polars as pl

# خواندن Parquet
df = pl.read_parquet("data.parquet")

# خواندن Lazy - فوق‌العاده سریع
df_lazy = pl.scan_parquet("large_data.parquet")

# نوشتن به Parquet
df.write_parquet("output.parquet", compression="snappy")

خواندن فایل JSON

import polars as pl

# خواندن JSON
df = pl.read_json("data.json")

# خواندن NDJSON (newline-delimited JSON)
df = pl.read_ndjson("data.ndjson")

# نوشتن به JSON
df.write_json("output.json")
df.write_ndjson("output.ndjson")

خواندن از دیتابیس

import polars as pl

# خواندن از SQL با استفاده از connectorx برای سرعت بالا
df = pl.read_database_uri(
    "SELECT * FROM users WHERE age > 25",
    "postgresql://user:password@localhost/dbname"
)

فیلتر کردن و انتخاب داده‌ها

انتخاب ستون‌ها با select

import polars as pl

df = pl.DataFrame({
    "name": ["Ali", "Zahra", "Mohammad", "Fatemeh"],
    "age": [25, 30, 35, 28],
    "city": ["Tehran", "Isfahan", "Shiraz", "Mashhad"],
    "salary": [50000, 65000, 70000, 58000]
})

# انتخاب یک ستون
result = df.select("name")

# انتخاب چند ستون
result = df.select(["name", "age"])

# انتخاب با Expression
result = df.select([
    pl.col("name"),
    pl.col("age"),
    (pl.col("salary") / 1000).alias("salary_k")
])

# انتخاب تمام ستون‌ها به جز برخی
result = df.select(pl.exclude("city"))

فیلتر کردن سطرها با filter

فیلتر کردن داده‌ها در Polars هم ساده‌ست و هم قدرتمند. ببینید:

import polars as pl

df = pl.DataFrame({
    "name": ["Ali", "Zahra", "Mohammad", "Fatemeh", "Reza"],
    "age": [25, 30, 35, 28, 42],
    "city": ["Tehran", "Isfahan", "Shiraz", "Mashhad", "Tehran"],
    "salary": [50000, 65000, 70000, 58000, 80000]
})

# فیلتر ساده
result = df.filter(pl.col("age") > 30)

# فیلترهای ترکیبی با AND
result = df.filter(
    (pl.col("age") > 25) & (pl.col("salary") > 60000)
)

# فیلترهای ترکیبی با OR
result = df.filter(
    (pl.col("city") == "Tehran") | (pl.col("age") > 35)
)

# فیلتر با is_between و is_in
result = df.filter(
    pl.col("age").is_between(28, 35),
    pl.col("city").is_in(["Tehran", "Isfahan"])
)

# فیلتر با تابع رشته‌ای
result = df.filter(
    pl.col("name").str.contains("a")
)

print(result)

ترکیب select و filter با زنجیره عملیات

یکی از چیزهایی که در Polars واقعاً لذت‌بخشه، زنجیره کردن عملیات‌هاست. کد زیر رو ببینید — تمیز و خواناست:

import polars as pl

df = pl.DataFrame({
    "product": ["Laptop", "Phone", "Tablet", "Headphone", "Watch"],
    "price": [25000000, 15000000, 8000000, 2000000, 5000000],
    "stock": [5, 12, 8, 25, 15],
    "category": ["electronics", "electronics", "electronics", "accessories", "accessories"]
})

# زنجیره عملیات
result = (
    df
    .filter(
        (pl.col("price") > 5000000) & (pl.col("stock") > 5)
    )
    .select([
        pl.col("product"),
        pl.col("price"),
        (pl.col("price") * pl.col("stock")).alias("total_stock_value")
    ])
    .sort("total_stock_value", descending=True)
)

print(result)

GroupBy و تجمیع داده‌ها

عملیات GroupBy برای تحلیل داده‌ها بر اساس دسته‌بندی فوق‌العاده مهمه. و Polars این کار رو با سرعتی انجام می‌ده که واقعاً شگفت‌زده می‌شید — به لطف اجرای چندنخی و بهینه‌سازی‌های داخلی.

GroupBy ساده

import polars as pl

df = pl.DataFrame({
    "category": ["A", "B", "A", "C", "B", "A", "C"],
    "revenue": [100, 200, 150, 300, 250, 180, 320],
    "quantity": [5, 8, 6, 12, 9, 7, 15]
})

# تجمیع ساده
result = df.group_by("category").agg(
    pl.col("revenue").sum().alias("total_revenue")
)
print(result)

# چند تابع تجمیع همزمان
result = df.group_by("category").agg([
    pl.col("revenue").sum().alias("total_revenue"),
    pl.col("revenue").mean().alias("avg_revenue"),
    pl.col("revenue").max().alias("max_revenue"),
    pl.col("revenue").min().alias("min_revenue"),
    pl.count().alias("row_count")
])
print(result)

GroupBy با چند ستون

import polars as pl

df = pl.DataFrame({
    "city": ["Tehran", "Tehran", "Isfahan", "Isfahan", "Tehran", "Shiraz"],
    "product": ["A", "B", "A", "B", "A", "A"],
    "revenue": [100, 150, 120, 180, 110, 90],
    "profit": [30, 50, 40, 60, 35, 25]
})

result = df.group_by(["city", "product"]).agg([
    pl.col("revenue").sum().alias("total_revenue"),
    pl.col("profit").sum().alias("total_profit"),
    (pl.col("profit").sum() / pl.col("revenue").sum() * 100)
        .alias("profit_pct")
        .round(2)
])
print(result)

توابع تجمیع پیشرفته

Polars یه مجموعه کامل از توابع تجمیعی داره. تقریباً هر چیزی که نیاز دارید:

import polars as pl

df = pl.DataFrame({
    "category": ["A", "A", "B", "B", "C", "C"],
    "value": [10, 20, 30, 40, 50, 60],
    "weight": [1, 2, 3, 4, 5, 6]
})

result = df.group_by("category").agg([
    pl.col("value").sum(),
    pl.col("value").mean(),
    pl.col("value").median(),
    pl.col("value").std().alias("std_dev"),
    pl.col("value").var().alias("variance"),
    pl.col("value").quantile(0.75).alias("q75"),
    pl.col("value").first().alias("first_value"),
    pl.col("value").last().alias("last_value")
])
print(result)

عملیات Join: ترکیب DataFrame‌ها

Join از اون عملیات‌هاییه که تقریباً در هر پروژه تحلیل داده‌ای بهش نیاز پیدا می‌کنید. Polars انواع مختلف Join از جمله inner، left، right، outer و cross رو با سرعت بالا پشتیبانی می‌کنه.

Inner Join

import polars as pl

# DataFrame اول
customers = pl.DataFrame({
    "customer_id": [1, 2, 3, 4],
    "name": ["Ali", "Zahra", "Mohammad", "Fatemeh"],
    "city": ["Tehran", "Isfahan", "Shiraz", "Mashhad"]
})

# DataFrame دوم
orders = pl.DataFrame({
    "order_id": [101, 102, 103, 104, 105],
    "customer_id": [1, 2, 1, 3, 5],
    "amount": [1000, 1500, 2000, 1200, 1800]
})

# Inner Join - فقط رکوردهای منطبق
result = customers.join(orders, on="customer_id", how="inner")
print(result)

Left Join و Outer Join

# Left Join - تمام رکوردهای چپ + رکوردهای منطبق راست
result = customers.join(orders, on="customer_id", how="left")
print(result)

# پر کردن مقادیر null
result = customers.join(orders, on="customer_id", how="left").fill_null(0)
print(result)

# Outer Join - تمام رکوردها از هر دو طرف
result = customers.join(orders, on="customer_id", how="full")
print(result)

Join با نام ستون‌های متفاوت

گاهی اوقات نام ستون کلید در دو DataFrame فرق داره. Polars این مورد رو هم پوشش می‌ده:

import polars as pl

products = pl.DataFrame({
    "product_id": [1, 2, 3],
    "product_name": ["Laptop", "Phone", "Tablet"],
    "price": [25000000, 15000000, 8000000]
})

sales = pl.DataFrame({
    "sale_id": [1, 2, 3],
    "prod_code": [1, 2, 1],
    "quantity": [2, 3, 1]
})

# Join با نام ستون‌های متفاوت
result = products.join(
    sales,
    left_on="product_id",
    right_on="prod_code",
    how="inner"
)
print(result)

Window Functions: توابع پنجره‌ای

توابع پنجره‌ای یکی از قابلیت‌های پیشرفته‌تر هستند که به شما اجازه می‌دهند محاسبات رو روی گروه‌هایی از سطرها انجام بدید، بدون اینکه داده‌ها تجمیع بشن. در Polars، از متد .over() استفاده می‌کنیم.

Window Function ساده

import polars as pl

df = pl.DataFrame({
    "group": ["A", "A", "A", "B", "B", "B"],
    "value": [10, 20, 30, 15, 25, 35]
})

# محاسبه میانگین در هر گروه بدون تجمیع
result = df.with_columns([
    pl.col("value").mean().over("group").alias("group_mean"),
    pl.col("value").sum().over("group").alias("group_sum"),
    (pl.col("value") / pl.col("value").sum().over("group") * 100)
        .alias("pct_of_total")
        .round(2)
])
print(result)

Ranking با Window Functions

import polars as pl

df = pl.DataFrame({
    "student": ["Ali", "Zahra", "Mohammad", "Fatemeh", "Reza", "Sara"],
    "class": ["A", "A", "A", "B", "B", "B"],
    "grade": [85, 92, 78, 88, 95, 82]
})

result = df.with_columns([
    pl.col("grade").rank(method="dense").over("class").alias("class_rank"),
    pl.col("grade").rank(method="dense").alias("overall_rank")
]).sort(["class", "class_rank"])

print(result)

توابع تجمعی و Lead/Lag

محاسبه مجموع تجمعی، میانگین متحرک و دسترسی به سطرهای قبلی و بعدی — همه اینها در Polars ساده‌ست:

import polars as pl

df = pl.DataFrame({
    "month": ["2024-01", "2024-02", "2024-03", "2024-04", "2024-05"],
    "revenue": [1000, 1200, 1100, 1400, 1300]
})

result = df.with_columns([
    pl.col("revenue").cum_sum().alias("cumulative_revenue"),
    pl.col("revenue").rolling_mean(window_size=3).alias("moving_avg_3m"),
    pl.col("revenue").shift(1).alias("prev_month"),
    pl.col("revenue").shift(-1).alias("next_month"),
    ((pl.col("revenue") - pl.col("revenue").shift(1)) / pl.col("revenue").shift(1) * 100)
        .alias("growth_pct")
        .round(2)
])
print(result)

مدیریت داده‌های گمشده (Missing Data)

کار با داده‌های گمشده (null) یکی از اون چالش‌هایی‌ست که هر تحلیلگر داده باهاش دست و پنجه نرم می‌کنه. خوشبختانه Polars ابزارهای خوبی برای این کار داره.

شناسایی و حذف مقادیر null

import polars as pl

df = pl.DataFrame({
    "name": ["Ali", "Zahra", None, "Fatemeh"],
    "age": [25, None, 35, 28],
    "city": ["Tehran", "Isfahan", "Shiraz", None]
})

# شمارش null ها در هر ستون
null_counts = df.null_count()
print(null_counts)

# حذف سطرهایی که هر ستون null دارد
result = df.drop_nulls()
print(result)

# حذف سطرهایی که ستون خاص null دارد
result = df.drop_nulls(subset=["age"])
print(result)

پر کردن مقادیر null

import polars as pl

df = pl.DataFrame({
    "name": ["Ali", "Zahra", None, "Fatemeh"],
    "age": [25, None, 35, 28],
    "score": [80, 90, None, 85]
})

# پر کردن با مقدار ثابت
result = df.fill_null(0)
print(result)

# پر کردن با استراتژی‌های مختلف
result = df.select([
    pl.col("name").fill_null("Unknown"),
    pl.col("age").fill_null(pl.col("age").mean()),
    pl.col("score").fill_null(pl.col("score").median())
])
print(result)

# Forward fill و Backward fill
result = df.select([
    pl.col("age").fill_null(strategy="forward"),
    pl.col("score").fill_null(strategy="backward")
])
print(result)

جایگزینی با coalesce

تابع coalesce اولین مقدار غیر null رو از بین چند ستون برمی‌گردونه. خیلی به درد می‌خوره وقتی چند منبع داده با اولویت‌های مختلف دارید:

import polars as pl

df = pl.DataFrame({
    "priority_1": [None, 20, None, 40],
    "priority_2": [15, None, 35, None],
    "default": [10, 10, 10, 10]
})

# استفاده از اولین مقدار غیر null
result = df.select([
    pl.coalesce(["priority_1", "priority_2", "default"]).alias("final_value")
])
print(result)

مقایسه عملکرد Polars و Pandas

خب، وقت اون رسیده که درباره عملکرد صحبت کنیم — که صادقانه بگم، اینجاست که Polars واقعاً می‌درخشه. بیایید با چند بنچمارک عملی این موضوع رو بررسی کنیم.

بنچمارک ۱: خواندن فایل CSV

import polars as pl
import pandas as pd
import numpy as np
import time

# ایجاد داده تصادفی
n_rows = 10_000_000
data = {
    "id": range(n_rows),
    "value_1": np.random.randn(n_rows),
    "value_2": np.random.randn(n_rows),
    "category": np.random.choice(["A", "B", "C", "D"], n_rows)
}

pd.DataFrame(data).to_csv("large_file.csv", index=False)

# خواندن با Pandas
start = time.time()
df_pandas = pd.read_csv("large_file.csv")
pandas_time = time.time() - start
print(f"Pandas read time: {pandas_time:.2f}s")

# خواندن با Polars
start = time.time()
df_polars = pl.read_csv("large_file.csv")
polars_time = time.time() - start
print(f"Polars read time: {polars_time:.2f}s")

print(f"Polars is {pandas_time/polars_time:.1f}x faster")

بنچمارک ۲: GroupBy و تجمیع

import polars as pl
import pandas as pd
import numpy as np
import time

n_rows = 5_000_000
df_pandas = pd.DataFrame({
    "group": np.random.choice(["A", "B", "C", "D", "E"], n_rows),
    "value": np.random.randn(n_rows)
})

df_polars = pl.from_pandas(df_pandas)

# GroupBy با Pandas
start = time.time()
result_pd = df_pandas.groupby("group")["value"].agg(["mean", "std", "min", "max"])
pandas_time = time.time() - start
print(f"Pandas GroupBy: {pandas_time:.2f}s")

# GroupBy با Polars
start = time.time()
result_pl = df_polars.group_by("group").agg([
    pl.col("value").mean(),
    pl.col("value").std(),
    pl.col("value").min(),
    pl.col("value").max()
])
polars_time = time.time() - start
print(f"Polars GroupBy: {polars_time:.2f}s")

print(f"Polars is {pandas_time/polars_time:.1f}x faster")

نتایج معمول بنچمارک‌ها

من خودم چندین بار این بنچمارک‌ها رو اجرا کردم و نتایج معمولاً این‌طوریه:

  • خواندن CSV: ۵ تا ۱۰ برابر سریع‌تر از Pandas
  • عملیات GroupBy: ۱۰ تا ۲۰ برابر سریع‌تر
  • Join عملیات: ۵ تا ۱۵ برابر سریع‌تر
  • فیلتر و انتخاب: ۳ تا ۸ برابر سریع‌تر
  • مصرف حافظه: ۳۰ تا ۵۰ درصد کمتر

دلیل این تفاوت چشمگیر؟ ترکیب معماری Rust، فرمت Apache Arrow، اجرای چندنخی و بهینه‌سازی خودکار کوئری‌ها. به عبارت دیگه، Polars از پایه برای سرعت ساخته شده.

راهنمای مهاجرت از Pandas به Polars

اگر تا حالا با Pandas کار کردید (که احتمالاً کردید!)، خبر خوب اینه که مهاجرت به Polars نسبتاً آسونه. بیایید الگوهای رایج رو کنار هم ببینیم.

مقایسه عملیات رایج

# ایجاد DataFrame
# Pandas:  pd.DataFrame({"a": [1, 2], "b": [3, 4]})
# Polars:  pl.DataFrame({"a": [1, 2], "b": [3, 4]})

# انتخاب ستون
# Pandas:  df[["col1", "col2"]]
# Polars:  df.select(["col1", "col2"])

# فیلتر
# Pandas:  df[df["age"] > 30]
# Polars:  df.filter(pl.col("age") > 30)

# افزودن ستون جدید
# Pandas:  df["new"] = df["a"] + df["b"]
# Polars:  df.with_columns((pl.col("a") + pl.col("b")).alias("new"))

# GroupBy
# Pandas:  df.groupby("cat")["val"].sum()
# Polars:  df.group_by("cat").agg(pl.col("val").sum())

# Sort
# Pandas:  df.sort_values("col", ascending=False)
# Polars:  df.sort("col", descending=True)

# Join
# Pandas:  df1.merge(df2, on="key", how="inner")
# Polars:  df1.join(df2, on="key", how="inner")

# Drop Nulls
# Pandas:  df.dropna()
# Polars:  df.drop_nulls()

# Fill Nulls
# Pandas:  df.fillna(0)
# Polars:  df.fill_null(0)

تبدیل بین Pandas و Polars

البته همیشه می‌تونید بین دو کتابخانه جابجا بشید. فقط یادتون باشه که تبدیل به Pandas مزایای سرعت Polars رو از بین می‌بره:

# از Pandas به Polars
df_polars = pl.from_pandas(df_pandas)

# از Polars به Pandas
df_pandas = df_polars.to_pandas()

# نکته: فقط زمانی تبدیل کنید که واقعاً نیاز دارید

بهترین شیوه‌ها و نکات کاربردی

بعد از مدتی کار با Polars، چند تا نکته مهم هست که تجربه‌تون رو خیلی بهتر می‌کنه. بذارید مهم‌ترین‌ها رو بگم.

۱. همیشه از Lazy برای داده‌های بزرگ استفاده کنید

import polars as pl

# بهتر - Lazy mode با بهینه‌سازی خودکار
result = (
    pl.scan_csv("large_file.csv")
    .filter(pl.col("value") > 100)
    .select(["id", "value"])
    .head(10)
    .collect()
)

۲. از Parquet به جای CSV استفاده کنید

این یکی واقعاً تفاوت بزرگی ایجاد می‌کنه. Parquet هم فشرده‌تره و هم خیلی سریع‌تر خونده می‌شه:

import polars as pl

# ذخیره به Parquet (فشرده و سریع)
df.write_parquet("data.parquet", compression="snappy")

# خواندن از Parquet (خیلی سریع‌تر از CSV)
df = pl.read_parquet("data.parquet")

۳. از Expression Chaining استفاده کنید

import polars as pl

# خوب - خوانا و کارآمد
result = (
    df
    .filter(pl.col("age") > 25)
    .select([
        pl.col("name"),
        (pl.col("salary") * 1.1).alias("new_salary")
    ])
    .sort("new_salary", descending=True)
    .head(10)
)

۴. از Streaming برای داده‌های خیلی بزرگ استفاده کنید

وقتی فایل‌هایی دارید که از RAM بزرگ‌ترند، حالت Streaming نجات‌دهنده‌ست:

import polars as pl

# پردازش streaming برای فایل‌هایی بزرگ‌تر از RAM
result = (
    pl.scan_csv("huge_file.csv")
    .filter(pl.col("value") > 100)
    .group_by("category")
    .agg(pl.col("value").sum())
    .collect(streaming=True)
)

۵. Debug کردن Query Plan

وقتی می‌خواهید ببینید Polars دقیقاً چه کاری انجام می‌ده (و چه بهینه‌سازی‌هایی اعمال کرده):

import polars as pl

query = (
    pl.scan_csv("data.csv")
    .filter(pl.col("age") > 30)
    .select(["name", "salary"])
    .sort("salary")
)

# مشاهده Query Plan بهینه‌شده
print(query.explain(optimized=True))

۶. استفاده از Selectors برای انتخاب ستون‌ها

یه قابلیت خیلی خوب که خیلی‌ها نمی‌دونن:

import polars as pl
import polars.selectors as cs

# فقط ستون‌های عددی
numeric_df = df.select(cs.numeric())

# فقط ستون‌های رشته‌ای
string_df = df.select(cs.string())

نتیجه‌گیری

Polars در سال ۲۰۲۶ جای خودش رو در اکوسیستم تحلیل داده پایتون محکم کرده. با سرعت باورنکردنی، مدیریت حافظه بهینه و API مدرن، تجربه‌ای کاملاً متفاوت نسبت به Pandas ارائه می‌ده.

نقاط قوت کلیدی Polars:

  • سرعت استثنایی: تا ۱۰۰ برابر سریع‌تر از Pandas در برخی عملیات
  • مدیریت حافظه بهینه: به لطف Apache Arrow و معماری Rust
  • پردازش موازی: استفاده خودکار از تمام هسته‌های پردازنده
  • Lazy Evaluation: بهینه‌سازی خودکار کوئری‌ها قبل از اجرا
  • API مدرن: سیستم Expression-based قدرتمند و خوانا

چه زمانی از Polars استفاده کنید؟

  • کار با داده‌های بزرگ (بیش از چند میلیون سطر)
  • نیاز به سرعت پردازش بالا در خطوط لوله داده
  • عملیات پیچیده GroupBy و Join
  • پروژه‌های جدید که از ابتدا شروع می‌شوند

چه زمانی هنوز Pandas بهتره؟

  • کدهای قدیمی که به Pandas وابسته‌اند
  • استفاده از کتابخانه‌هایی مثل scikit-learn که ورودی Pandas می‌خوان
  • داده‌های خیلی کوچک که اصلاً سرعت مهم نیست

در عمل، روند فعلی جامعه پایتون به سمت ترکیب ابزارهای مختلف پیش می‌ره. خیلی از تیم‌های داده در سال ۲۰۲۶ از ترکیب DuckDB + Polars + Pandas استفاده می‌کنن: DuckDB برای جستجو و فیلتر اولیه، Polars برای تبدیلات سنگین و پرسرعت، و Pandas برای ارتباط با کتابخانه‌های یادگیری ماشین.

پیشنهاد من اینه که همین الان یه پروژه کوچک رو با Polars شروع کنید. اول یه فایل CSV رو بخونید، چند تا فیلتر و GroupBy بزنید و ببینید چقدر سریع‌تر از Pandas انجام می‌شه. مطمئنم شگفت‌زده می‌شید!

درباره نویسنده Editorial Team

Our team of expert writers and editors.