صراحةً، إذا كنت لا تزال تستخدم pandas في عام 2026 لكل عمليات البيانات، فأنت تترك أداءً ضخمًا على الطاولة. خلال السنتين الماضيتين، نضجت Polars — مكتبة DataFrame المكتوبة بلغة Rust — إلى بديل جاهز للإنتاج يتفوق على pandas بمعدل 10 إلى 100 ضعف في العمليات الشائعة. وبحلول مطلع 2026، يُقدَّر أن نصف فرق البيانات إما أنهت الترحيل، أو تختبر Polars فعليًا في خطوط معالجتها (pipelines).
أنا شخصيًا، كنت من أكبر المتحمسين لـ pandas منذ 2017، ورحّلت خط ETL ضخمًا قبل بضعة أشهر. والنتيجة؟ ما كان يستغرق 12 دقيقة، أصبح ينتهي في أقل من 30 ثانية. هذا الدليل يلخّص ما تعلمته على الطريق.
سنبدأ من فهم لماذا Polars أسرع، ثم ننتقل إلى معايير أداء حقيقية لعام 2026، وننتهي باستراتيجية ترحيل تدريجية لا تكسر مشاريعك القائمة. ستجد أمثلة كود قابلة للنسخ، ومقارنة جنبًا إلى جنب لأشهر العمليات، ومناقشة صريحة لمتى يجب أن تبقى مع pandas.
لماذا Polars أسرع: الفرق المعماري
تخيّل pandas كطريق ريفي بحارة واحدة. مهما كان حاسوبك قويًا — حتى لو كان لديك معالج بـ 16 نواة — فإن pandas يتجاهل هذه القوة في معظم العمليات، ويرسل كل صف من البيانات عبر تلك الحارة الواحدة، الواحد تلو الآخر. أما Polars، فقد بُني من الصفر مع وضع العتاد الحديث في الاعتبار:
- متعدد المسارات (multi-threaded) افتراضيًا — يستخدم جميع نوى المعالج تلقائيًا دون أي تكوين إضافي.
- التقييم الكسول (Lazy Evaluation) — يحلل خطة الاستعلام بأكملها قبل التنفيذ، ويعيد ترتيب الخطوات لأقصى كفاءة.
- تنسيق الذاكرة Apache Arrow — يتيح التشغيل البيني بدون نسخ (zero-copy) وتخطيطًا صديقًا لذاكرة التخزين المؤقت للمعالج.
- بدون فهرس (Index) — يلغي مصدرًا رئيسيًا للارتباك في pandas، ويبسّط واجهة الاستعلامات بشكل ملحوظ.
- محرك Streaming — يعالج بيانات أكبر من الذاكرة المتوفرة عبر تقسيمها إلى دفعات.
هذه القرارات المعمارية ليست مجرد تحسينات صغيرة. إنها إعادة تصور كاملة لفكرة DataFrame. والنتيجة المباشرة: تنفيذ موازٍ تلقائي، استهلاك ذاكرة أقل، وقدرة على تحسين الاستعلام (query optimization) لا توفرها pandas حتى مع PyArrow backend.
معايير أداء 2026: أرقام حقيقية
الأرقام التالية مأخوذة من اختبارات حديثة على آلة بثمانية نوى ومجموعات بيانات تتراوح بين 5 ملايين و100 مليون صف. لا أرقام تسويقية — فقط نتائج قابلة للتكرار.
التصفية (Filtering) على 100 مليون صف
- pandas: 9.38 ثانية.
- Polars: 1.89 ثانية — أي أسرع بـ 5 أضعاف.
التجميع (GroupBy) على 100 مليون صف
- pandas: 3.37 ثانية.
- DuckDB: 2.66 ثانية.
- Polars: 1.58 ثانية — أسرع بـ 2.1 ضعف من pandas.
التجميع على 5 ملايين صف
- pandas: ~0.45 ثانية.
- Polars: ~0.03 ثانية — تسريع بمعدل 15 ضعفًا.
سيناريو خط معالجة ETL حقيقي
خط معالجة pandas يعالج 50 مليون صف في 8 دقائق. بعد الترحيل إلى Polars بالتقييم الكسول، انخفض الوقت إلى 19 ثانية فقط — تسريع يفوق 25 ضعفًا. وأول مرة شاهدت فيها هذا الرقم بعيني، اعتقدت أن الاختبار فشل.
زمن الاستجابة لواجهة API
نقطة نهاية لتجميع الإيرادات كانت تستغرق متوسط 59.38 ثانية مع pandas. بعد إعادة بناء المنطق بـ Polars، أصبح المتوسط 25.21 ثانية — انخفاض يقارب 50% في زمن الاستجابة، مع تقليل ملحوظ في استهلاك المعالج.
تظهر الأبحاث المستقلة أيضًا أن Polars يستهلك طاقة أقل بثمانية أضعاف لنفس العمليات — وهذا مقياس بات يهم العملاء ذوي التزامات الاستدامة (وهو شيء لم يكن أحد يسأل عنه قبل سنتين).
المقارنة الجنبية: نفس المهام بكلتا المكتبتين
قراءة CSV
# pandas
import pandas as pd
df = pd.read_csv("sales.csv")
# Polars (eager)
import polars as pl
df = pl.read_csv("sales.csv")
# Polars (lazy — موصى به)
lf = pl.scan_csv("sales.csv")
لاحظ scan_csv هنا. لا يقرأ الملف فعليًا — بل ينشئ خطة استعلام فقط. ستُقرأ البيانات لاحقًا عند استدعاء .collect()، مما يتيح لـ Polars تطبيق دفع المسند (predicate pushdown) ودفع الإسقاط (projection pushdown)؛ أي قراءة الأعمدة والصفوف اللازمة فقط.
التصفية والاختيار
# pandas
result = df[(df["country"] == "EG") & (df["amount"] > 1000)][["id", "amount", "date"]]
# Polars
result = (
df
.filter((pl.col("country") == "EG") & (pl.col("amount") > 1000))
.select(["id", "amount", "date"])
)
التجميع (GroupBy + Aggregation)
# pandas
agg = (
df.groupby("country")
.agg(total=("amount", "sum"),
orders=("id", "count"),
avg=("amount", "mean"))
.reset_index()
)
# Polars
agg = (
df
.group_by("country")
.agg(
pl.col("amount").sum().alias("total"),
pl.col("id").count().alias("orders"),
pl.col("amount").mean().alias("avg"),
)
)
الانضمام (Join)
# pandas
merged = orders.merge(customers, on="customer_id", how="left")
# Polars
merged = orders.join(customers, on="customer_id", how="left")
في معايير 2026، يتفوق Polars على pandas بـ 9 أضعاف في عمليات الانضمام، خاصة الانضمامات الكبيرة على مفاتيح رقمية. وهذا، ببساطة، أكبر سبب رحّلت بسببه أنا أول pipeline.
التقييم الكسول: السر الحقيقي وراء الأداء
التقييم الفوري (eager) يعني تنفيذ كل سطر فور كتابته. أما التقييم الكسول (lazy)، فيعني تسجيل كل عملية في خطة استعلام دون تنفيذها، ثم السماح لمحرّك Polars بفحص الخطة بأكملها وإعادة ترتيب الخطوات قبل التنفيذ. الفرق بين الاثنين يصبح واضحًا فور أن تجرّب على بيانات حقيقية.
import polars as pl
result = (
pl.scan_parquet("transactions/*.parquet") # لا قراءة بعد
.filter(pl.col("year") == 2026)
.filter(pl.col("amount") > 0)
.group_by("region")
.agg(pl.col("amount").sum().alias("revenue"))
.sort("revenue", descending=True)
.collect() # هنا فقط يبدأ التنفيذ
)
عندما تستدعي .collect()، يقوم محرّك Polars بـ:
- دفع المسندات (Predicate Pushdown): يدفع شرط
year == 2026وصولًا إلى مرحلة قراءة الملف، فيقرأ من القرص الأسطر التي تطابق الشرط فقط. - دفع الإسقاط (Projection Pushdown): يقرأ الأعمدة المطلوبة فقط (
year, amount, region)، متجاهلًا البقية. - إعادة استخدام الحسابات: إذا تكرر تعبير، يحسبه مرة واحدة ويخزّن نتيجته.
ولمعاينة الخطة قبل تنفيذها — وهذا مفيد جدًا للتنقيح:
print(result.explain())
# أو رسم بياني:
result.show_graph()
معالجة بيانات أكبر من الذاكرة باستخدام Streaming
أحد أعظم نقاط ضعف pandas: إذا كان حجم ملفك أكبر من ذاكرة الجهاز، فأنت في مأزق. كنت أتعامل مع هذا سابقًا عبر chunksize ومنطق يدوي مزعج. Polars يحل المشكلة عبر محرك Streaming الذي ينفذ الاستعلام على دفعات تلقائيًا:
result = (
pl.scan_csv("very_large_file.csv") # 50 جيجابايت
.filter(pl.col("status") == "active")
.group_by("category")
.agg(pl.col("revenue").sum())
.collect(streaming=True) # المحرك المتدفق
)
هذا يسمح لك بمعالجة ملفات بحجم 100 جيجابايت أو أكثر على حاسوب محمول بـ 16 جيجابايت من الذاكرة — شيء كان مستحيلًا تقريبًا في pandas دون تقسيم يدوي عبر chunksize ودموع.
استراتيجية ترحيل عملية: لا تعد كتابة كل شيء دفعة واحدة
الخطوة 1: قِس قبل أن تُحسّن
قبل ترحيل أي شيء، حدد عنق الزجاجة الفعلي. أكثر مراحل الـ pipeline استهلاكًا للوقت عادةً واحدة من: I/O، أو join، أو groupby، أو sort، أو تحليل النصوص (string parsing)، أو دالة Python معرّفة من المستخدم (UDF). قياس بسيط مثل هذا قد يوفر عليك أيامًا من العمل في الاتجاه الخاطئ:
import time
start = time.perf_counter()
result = df.groupby("user_id").agg({"amount": "sum"})
print(f"groupby استغرق: {time.perf_counter() - start:.2f}s")
الخطوة 2: استخدم نمط الترحيل التدريجي (Strangler)
لا ترحّل كامل خط المعالجة دفعة واحدة. ابدأ بالمرحلة الأبطأ — عادةً قراءة الملفات الكبيرة أو التجميعات الثقيلة — ورحّلها أولًا إلى Polars، ثم حوّل النتيجة إلى pandas للمراحل التالية. هذا النمط مأخوذ من Martin Fowler، وهو ينطبق هنا تمامًا.
الخطوة 3: خطوط معالجة هجينة (Hybrid Pipelines)
الخطوط الهجينة عملية ممتازة، وأنا أنصح بها بشدة. استخدم Polars لمعالجة ETL الثقيلة، ثم حوّل إلى pandas عند الحدود مع scikit-learn أو مكتبات التصور:
import polars as pl
from sklearn.ensemble import RandomForestClassifier
# تحميل ومعالجة ثقيلة بـ Polars
features = (
pl.scan_parquet("events/*.parquet")
.filter(pl.col("event_type") == "purchase")
.group_by("user_id")
.agg([
pl.col("amount").sum().alias("total_spent"),
pl.col("event_id").count().alias("purchase_count"),
pl.col("session_duration").mean().alias("avg_session"),
])
.collect()
)
# تحويل إلى pandas للتعلم الآلي
features_pd = features.to_pandas()
X = features_pd[["total_spent", "purchase_count", "avg_session"]]
y = features_pd["churned"]
model = RandomForestClassifier()
model.fit(X, y)
الخطوة 4: انتبه للفروقات الخفية في السلوك
Polars ليست بديلًا مباشرًا (drop-in) لـ pandas. وهذه نقطة يخفيها كثيرون. الفروقات التي قد تظهر صامتة في الإنتاج:
- التعامل مع القيم الفارغة (Null): pandas يفرّق بين
NaNوNone، بينما Polars يستخدم تمثيل null الصارم لـ Arrow. - الترتيب الافتراضي: pandas مستقر افتراضيًا، Polars ليس كذلك إلا إذا حددت
maintain_order=True. - أنواع البيانات: نظام أنواع Polars أكثر صرامة. عمليات Mixed-type التي تنفّذها pandas بصمت قد ترفع خطأ في Polars — وهذا غالبًا ما يكشف أخطاء كانت مخفية في كودك منذ سنوات (تكلمت من تجربة).
- الفهرس (Index): Polars لا يملك فهرسًا. أي كود pandas يعتمد على
set_indexأوlocيحتاج إعادة صياغة.
متى تبقى مع pandas؟
لنكن صرحاء: Polars ليس الحل الصحيح لكل سيناريو.
- مجموعات بيانات أصغر من 100 ألف صف: الفرق في الأداء مهمل، وتكلفة إعادة الكتابة لا تستحق.
- التحليل التفاعلي في Jupyter: نظام pandas البيئي للعرض والاستكشاف لا يزال أنضج بمسافة كبيرة.
- تكامل مكتبات تعلم الآلة: scikit-learn و statsmodels لا تزالان تتوقعان pandas DataFrames في معظم الواجهات.
- قاعدة كود pandas ضخمة وقائمة: إعادة كتابة 50 ألف سطر لا توفر دائمًا قيمة تجارية، خصوصًا إذا كان الكود يعمل بسرعة "جيدة بما فيه الكفاية".
- pandas 2.x مع PyArrow backend: يستهلك ذاكرة أقل بـ 60-80% للأعمدة النصية مقارنة بـ pandas 1.x — تحسّن كبير، لكنه لا يحل مشكلة نموذج التنفيذ الجوهرية.
التكامل مع DuckDB: الخيار الثالث
ولا تنسَ DuckDB. هي قاعدة بيانات OLAP داخلية تكمّل Polars وpandas بشكل ممتاز. DuckDB يمكنه الاستعلام مباشرة عن DataFrames بدون نسخ (zero-copy) عبر آلية replacement scan:
import duckdb
import polars as pl
df = pl.read_parquet("sales.parquet")
result = duckdb.sql("""
SELECT region, SUM(amount) AS revenue
FROM df
WHERE year = 2026
GROUP BY region
ORDER BY revenue DESC
""").pl() # العودة إلى Polars
سير العمل المثالي لعام 2026، باختصار: DuckDB للاستيعاب الأولي والاستعلامات SQL المعقدة، Polars للسرعة والكفاءة في تحويلات البيانات، وpandas للتفاعل مع نظامها البيئي الناضج (scikit-learn، matplotlib، statsmodels).
قائمة فحص الترحيل (Migration Checklist)
- قِس الأداء الحالي وحدد عنق الزجاجة الحقيقي.
- ابدأ من المرحلة الأبطأ — عادةً قراءة CSV/Parquet الكبيرة أو التجميعات.
- استخدم
scan_csv/scan_parquetبدلًا منread_*كلما أمكن. - اعتنق التقييم الكسول: ابنِ الاستعلام كاملًا قبل
.collect(). - اكتب اختبارات قبل الترحيل تتحقق من تطابق النتائج (الصفوف، القيم، الأنواع). هذه خطوة لا تتساهل فيها.
- راقب فروقات السلوك — خاصة null، الترتيب، وأنواع البيانات المختلطة.
- للبيانات الأكبر من الذاكرة، فعّل
collect(streaming=True). - عند الحدود مع scikit-learn، استخدم
.to_pandas()— التكلفة منخفضة بفضل Arrow.
الأسئلة الشائعة (FAQ)
هل Polars يستبدل pandas تمامًا في 2026؟
لا. Polars يتفوق بشكل ساحق على البيانات الكبيرة وخطوط ETL، لكن pandas لا يزال الخيار الأفضل للتحليل التفاعلي السريع، ولمجموعات البيانات الصغيرة، وللتكامل المباشر مع scikit-learn و statsmodels. الواقعية: معظم المشاريع الناضجة في 2026 تستخدم الاثنين معًا، وهذا أمر جيد لا سيئ.
ما الفرق بين read_csv وscan_csv في Polars؟
read_csv فوري (eager) — يقرأ الملف بأكمله إلى الذاكرة فورًا. scan_csv كسول (lazy) — يُنشئ خطة استعلام دون قراءة الملف، ويسمح لـ Polars بتطبيق دفع المسند والإسقاط، فيقرأ الأعمدة والصفوف اللازمة فقط عند .collect(). القاعدة العامة (وأنا ألتزم بها دائمًا): استخدم scan_* دائمًا إلا إذا كنت تستكشف البيانات تفاعليًا.
هل يمكن لـ Polars التعامل مع بيانات أكبر من ذاكرة الجهاز؟
نعم، عبر محرك Streaming. استدعِ .collect(streaming=True) على LazyFrame، وسينفذ Polars الاستعلام على دفعات، مما يسمح بمعالجة ملفات تتجاوز 100 جيجابايت على جهاز بـ 16 جيجابايت ذاكرة عشوائية. لاحظ أن بعض العمليات (مثل ترتيب البيانات بالكامل) لا تدعم Streaming حتى الآن.
كيف أحوّل DataFrame من pandas إلى Polars والعكس؟
التحويل سريع جدًا بفضل Apache Arrow: pl.from_pandas(df) لتحويل pandas إلى Polars، وdf.to_pandas() لتحويل Polars إلى pandas. التكلفة في معظم الحالات قريبة من zero-copy، خاصة إذا كان pandas يستخدم backend Arrow.
هل يدعم Polars عمليات السلاسل الزمنية (Time Series) مثل pandas؟
نعم، ويتحسن سريعًا. Polars يدعم group_by_dynamic للنوافذ المتدحرجة، وعمليات إعادة العينة (resampling)، والانضمامات الزمنية (asof joins). لكن في 2026 لا يزال نظام pandas البيئي للسلاسل الزمنية أنضج (مثل تكامل statsmodels و pandas-ta). للسلاسل الزمنية المعقدة، الترحيل قد يحتاج عملًا إضافيًا.
الخلاصة
في 2026، السؤال لم يعد "هل يجب أن أنتقل إلى Polars؟"، بل "أي أجزاء من خط معالجتي تستحق الترحيل أولًا؟". إذا كانت بياناتك تتجاوز 1 جيجابايت، أو خط الـ ETL يستهلك دقائق ثمينة، فالأرقام واضحة: تسريع 10x-100x، وتوفير ذاكرة وطاقة فعلي.
ابدأ صغيرًا، رحّل مرحلة واحدة، قِس النتيجة، ثم وسّع. الخطوط الهجينة Polars + pandas + DuckDB ليست حلًا انتقاليًا، إنها البنية المثلى لتحليل البيانات في 2026 وما بعده. وأنا، شخصيًا، لن أعود.