تحسين استهلاك الذاكرة في pandas: دليل عملي لتقليل حجم DataFrame بنسبة 90%

تعلّم كيف تقلّل استهلاك الذاكرة في pandas بنسبة تصل إلى 90% باستخدام تقنيات dtype downcasting والنوع category وصيغة Parquet ومزايا pandas 3.0 مثل PyArrow وCopy-on-Write — مع أمثلة تطبيقية بالكود.

لماذا يُعَدّ تحسين الذاكرة ضروريًا عند استخدام pandas؟

إذا سبق لك التعامل مع بيانات حقيقية في pandas، فأنت على الأرجح تعرف ذلك الشعور — تفتح ملف CSV كبير، تنتظر قليلاً، ثم يفاجئك خطأ MemoryError أو يبدأ الجهاز بالبطء الشديد. السبب؟ pandas تُحمّل كل شيء في ذاكرة الوصول العشوائي (RAM) دفعة واحدة. وملف CSV بحجم 2 غيغابايت قد يلتهم 6-10 غيغابايت من الذاكرة بسبب الطريقة التي يتعامل بها Python مع الكائنات داخليًا.

صراحةً، هذا أمر محبط.

لكن الخبر الجيد أن هناك تقنيات مُثبتة يمكنها تقليل استهلاك الذاكرة بنسبة تصل إلى 90% أو أكثر — وبدون تعقيدات كبيرة. في هذا الدليل، سنمر على كل تقنية خطوة بخطوة مع أمثلة بالكود تقدر تنفذها فورًا. وسنتطرق أيضًا لمزايا pandas 3.0 الجديدة مثل محرك PyArrow ونظام Copy-on-Write اللي تجعل الأمور أسهل بكثير.

الخطوة الأولى: قياس استهلاك الذاكرة الحالي

قبل ما تبدأ بأي تحسين، لازم تعرف وضعك الحالي. كم ذاكرة يستهلك الـ DataFrame فعلاً؟ لحسن الحظ، pandas توفر أدوات مدمجة لهذا الغرض.

استخدام info() و memory_usage()

import pandas as pd
import numpy as np

# إنشاء DataFrame تجريبي
df = pd.DataFrame({
    "id": np.arange(1_000_000),
    "name": np.random.choice(["أحمد", "سارة", "محمد", "فاطمة", "علي"], 1_000_000),
    "city": np.random.choice(["الرياض", "جدة", "القاهرة", "دبي", "الدار البيضاء"], 1_000_000),
    "salary": np.random.uniform(3000, 50000, 1_000_000),
    "age": np.random.randint(18, 65, 1_000_000),
    "is_active": np.random.choice([True, False], 1_000_000)
})

# عرض معلومات الذاكرة
df.info(memory_usage="deep")

# تفصيل الذاكرة لكل عمود (بالميغابايت)
memory_per_col = df.memory_usage(deep=True) / 1024**2
print(memory_per_col)
print(f"
الإجمالي: {memory_per_col.sum():.2f} MB")

نقطة مهمة هنا: المعامل deep=True ضروري جدًا. بدونه، لن يحسب pandas الحجم الحقيقي للأعمدة النصية (object) لأنها تُخزَّن كمؤشرات لكائنات Python منفصلة في الذاكرة. بدون هذا المعامل، القراءة اللي تحصل عليها ستكون أقل بكثير من الواقع — وهذا فخ وقعت فيه شخصيًا أكثر من مرة.

التقنية الأولى: تحسين أنواع البيانات الرقمية (Dtype Downcasting)

هذه التقنية بسيطة لكنها فعّالة بشكل مذهل. الفكرة أن pandas عند تحميل ملف CSV يستخدم int64 (8 بايت لكل قيمة) وfloat64 (8 بايت) كأنواع افتراضية — حتى لو كان العمود يحتوي على أرقام صغيرة مثل الأعمار (18-65) التي تكفيها بايت واحد فقط!

جدول أنواع البيانات الرقمية

النوعالحجمالنطاق
int81 بايت-128 إلى 127
int162 بايت-32,768 إلى 32,767
int324 بايت-2.1 مليار إلى 2.1 مليار
int648 بايتنطاق واسع جدًا
float324 بايتدقة 6-7 أرقام عشرية
float648 بايتدقة 15-16 رقمًا عشريًا

تطبيق عملي: تقليص الأنواع الرقمية

# قبل التحسين
print(f"قبل: {df.memory_usage(deep=True).sum() / 1024**2:.2f} MB")

# تقليص الأعداد الصحيحة
df["id"] = pd.to_numeric(df["id"], downcast="integer")
df["age"] = pd.to_numeric(df["age"], downcast="integer")

# تقليص الأعداد العشرية
df["salary"] = pd.to_numeric(df["salary"], downcast="float")

# بعد التحسين
print(f"بعد: {df.memory_usage(deep=True).sum() / 1024**2:.2f} MB")

تحويل عمود age من int64 إلى int8 يوفر 87.5% من ذاكرة ذلك العمود. أما salary، فالتحويل من float64 إلى float32 يوفر 50%. أرقام مبهرة من تغيير بسيط.

التقنية الثانية: استخدام النوع Category للأعمدة النصية

هنا يأتي التوفير الأكبر — وأعني ذلك حرفيًا.

الأعمدة النصية ذات القيم المتكررة (مثل أسماء المدن أو حالات الطلبات) هي أكبر مستهلك للذاكرة في pandas. النوع object يخزّن كل قيمة ككائن Python منفصل، حتى لو كانت نفس الكلمة مكررة مليون مرة. تخيل تخزين كلمة "الرياض" مليون مرة كمليون كائن مستقل!

النوع category يحل هذه المشكلة بذكاء: يخزّن جدول مرجعي صغير للقيم الفريدة، ويستخدم أكواد رقمية صحيحة للإشارة إليها.

# تحويل الأعمدة النصية المتكررة إلى category
df["name"] = df["name"].astype("category")
df["city"] = df["city"].astype("category")

# مقارنة الذاكرة
print(df.memory_usage(deep=True) / 1024**2)

عمود city الذي يحتوي على 5 قيم فريدة فقط يمكن أن ينخفض من 50 ميغابايت إلى أقل من 1 ميغابايت. نعم، توفير بنسبة 98% أو أكثر! هذا ليس خطأ مطبعي.

متى تستخدم النوع category؟

  • عندما تكون نسبة القيم الفريدة أقل من 50% من إجمالي الصفوف
  • الأعمدة التي تحتوي على حالات محددة مثل: الجنس، المدينة، الفئة، الحالة
  • الأعمدة المُستخدمة كثيرًا في عمليات groupby وmerge (وهنا تحصل على مكافأة إضافية في سرعة التنفيذ أيضًا)

التقنية الثالثة: تحميل الأعمدة المطلوبة فقط

هذه النصيحة قد تبدو واضحة، لكنك ستتفاجأ كم مرة يتم تجاهلها. في كثير من الحالات، لا تحتاج فعلاً إلى كل أعمدة الملف. استخدام المعامل usecols عند القراءة يمكن أن يقلل الاستهلاك بشكل كبير:

# بدلاً من تحميل الملف كاملاً
# df = pd.read_csv("sales_data.csv")

# حمّل الأعمدة المطلوبة فقط
df = pd.read_csv(
    "sales_data.csv",
    usecols=["date", "product", "revenue", "quantity"],
    dtype={"product": "category", "quantity": "int16"}
)

لاحظ كيف دمجنا usecols مع dtype لتحديد الأنواع المناسبة مباشرة عند القراءة. هذا يمنع pandas من استخدام الأنواع الافتراضية الكبيرة من البداية — أفضل بكثير من تحويلها لاحقًا.

التقنية الرابعة: القراءة المجزأة (Chunked Reading)

ماذا لو كان حجم الملف أكبر من الذاكرة المتاحة أصلاً؟ هنا يأتي دور المعامل chunksize الذي يتيح لك معالجة الملف على دفعات:

# معالجة ملف كبير على دفعات من 100,000 صف
results = []

for chunk in pd.read_csv("huge_dataset.csv", chunksize=100_000):
    # تطبيق التصفية والتجميع على كل دفعة
    filtered = chunk[chunk["status"] == "completed"]
    summary = filtered.groupby("category")["amount"].sum()
    results.append(summary)

# دمج النتائج
final_result = pd.concat(results).groupby(level=0).sum()
print(final_result)

هذا الأسلوب يعمل بشكل ممتاز عندما تكون العمليات المطلوبة مستقلة على كل جزء — مثل التصفية، حساب إحصائيات لكل مجموعة، أو تحويل الصيغة. لكن انتبه: إذا كانت عمليتك تحتاج رؤية البيانات كاملة (مثل حساب الوسيط على كل البيانات)، فهذا الأسلوب لن يكون مناسبًا بشكل مباشر.

التقنية الخامسة: استخدام صيغة Parquet بدلاً من CSV

هذه التقنية — بصراحة — من أفضل القرارات التي يمكنك اتخاذها إذا كنت تتعامل مع بيانات كبيرة بشكل متكرر. صيغة Parquet هي صيغة عمودية ثنائية مصممة خصيصًا للبيانات الكبيرة، وتتفوق على CSV في كل جانب تقريبًا:

المعيارCSVParquet
سرعة القراءةبطيئةأسرع بـ 5-10 مرات
حجم الملفكبيرأصغر بـ 2-5 مرات (مضغوط)
حفظ أنواع البياناتلا (يحتاج تحديد يدوي)نعم (يحفظ الأنواع تلقائيًا)
قراءة أعمدة محددةيقرأ الملف كاملاًيقرأ الأعمدة المطلوبة فقط
الضغطغير مدعوممدعوم (snappy, gzip, zstd)
# تحويل CSV إلى Parquet
df = pd.read_csv("large_data.csv")
df.to_parquet("large_data.parquet", engine="pyarrow", compression="snappy")

# القراءة من Parquet — أسرع بكثير
df = pd.read_parquet(
    "large_data.parquet",
    columns=["date", "revenue", "category"],  # أعمدة محددة فقط
    engine="pyarrow"
)

# قراءة مع تصفية على مستوى الملف (Row Group Filtering)
df = pd.read_parquet(
    "large_data.parquet",
    filters=[("year", ">=", 2025)],
    engine="pyarrow"
)

عند استخدام Parquet مع PyArrow، يقرأ المحرك البيانات الوصفية (metadata) أولاً لتحديد أي أجزاء من الملف تحتوي على البيانات المطلوبة، ويتخطى الباقي تمامًا. هذا يوفر وقتًا وذاكرة — خصوصًا مع الملفات التي حجمها عدة غيغابايت.

التقنية السادسة: الاستفادة من مزايا pandas 3.0 الجديدة

إذا كنت لا تزال على إصدار قديم من pandas، فهذا القسم قد يكون السبب الذي يدفعك أخيرًا للترقية. أحدث إصدار (pandas 3.0، فبراير 2026) يقدم تحسينات جوهرية في إدارة الذاكرة — والجميل أنها تعمل تلقائيًا.

النصوص المدعومة بـ PyArrow (الافتراضي الجديد)

في pandas 3.0، أصبح النوع النصي المدعوم بمكتبة PyArrow هو الافتراضي. هذا يعني أن الأعمدة النصية تُخزَّن بتنسيق Apache Arrow العمودي بدلاً من كائنات Python العادية. النتيجة؟

  • تقليل استهلاك الذاكرة للأعمدة النصية بنسبة تصل إلى 70%
  • تسريع عمليات النصوص مثل str.contains() وstr.lower() بمقدار 5-10 مرات
  • مشاركة البيانات بدون نسخ (zero-copy) مع أدوات مثل Polars وDuckDB
# pandas 3.0: النصوص تُخزَّن بـ PyArrow تلقائيًا
import pandas as pd

df = pd.DataFrame({"name": ["أحمد", "سارة", "محمد"]})
print(df.dtypes)  # name    str (مدعوم بـ PyArrow)

# للاستفادة القصوى، استخدم dtype_backend عند القراءة
df = pd.read_csv(
    "data.csv",
    engine="pyarrow",                # محرك قراءة متعدد الأنوية
    dtype_backend="pyarrow"          # جميع الأنواع مدعومة بـ Arrow
)

نظام Copy-on-Write (CoW)

هذا التغيير ذكي جدًا. أصبح نظام Copy-on-Write هو الوضع الافتراضي والوحيد في pandas 3.0. ببساطة، عمليات التقطيع والفهرسة لم تعد تنشئ نسخًا فورية من البيانات — بل تشترك في نفس الذاكرة حتى يتم التعديل فعلاً:

# pandas 3.0: Copy-on-Write مُفعّل تلقائيًا
df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})

# هذا يشارك الذاكرة مع df الأصلي (بدون نسخ)
subset = df[df["a"] > 1]

# النسخ يحدث فقط عند التعديل
subset["b"] = 99  # هنا فقط يتم إنشاء نسخة منفصلة

في الإصدارات السابقة، كان pandas ينسخ البيانات "احتياطيًا" في كثير من العمليات حتى لو لم تكن بحاجة لذلك. الآن، هذا الهدر اختفى تمامًا.

دالة شاملة لتحسين الذاكرة تلقائيًا

بعد كل هذه التقنيات، إليك دالة جاهزة يمكنك نسخها واستخدامها في أي مشروع. أنا شخصيًا أضعها في ملف أدوات مساعدة وأستدعيها فورًا بعد كل عملية قراءة:

def optimize_dataframe_memory(df: pd.DataFrame, verbose: bool = True) -> pd.DataFrame:
    """تحسين استهلاك الذاكرة لـ 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" or str(col_type) == "string":
            # تحويل الأعمدة النصية منخفضة التنوع إلى category
            num_unique = df[col].nunique()
            num_total = len(df[col])
            if num_unique / num_total < 0.5:
                df[col] = df[col].astype("category")

        elif col_type in ["int64", "int32", "int16"]:
            # تقليص الأعداد الصحيحة
            df[col] = pd.to_numeric(df[col], downcast="integer")

        elif col_type in ["float64"]:
            # تقليص الأعداد العشرية
            df[col] = pd.to_numeric(df[col], downcast="float")

    end_mem = df.memory_usage(deep=True).sum() / 1024**2

    if verbose:
        reduction = (1 - end_mem / start_mem) * 100
        print(f"الذاكرة قبل: {start_mem:.2f} MB")
        print(f"الذاكرة بعد: {end_mem:.2f} MB")
        print(f"نسبة التوفير: {reduction:.1f}%")

    return df

# الاستخدام
df = pd.read_csv("large_data.csv")
df = optimize_dataframe_memory(df)

مثال تطبيقي كامل: من 800 ميغابايت إلى 80 ميغابايت

حسنًا، لنجمع كل ما تعلمناه في مثال واقعي يوضح الفرق. تخيل أن لديك ملف بيانات تجارة إلكترونية ضخم:

import pandas as pd

# الخطوة 1: تحميل الأعمدة المطلوبة فقط مع تحديد الأنواع
df = pd.read_csv(
    "ecommerce_transactions.csv",
    usecols=["order_id", "customer_id", "product_category",
             "quantity", "price", "order_date", "country"],
    dtype={
        "order_id": "int32",
        "customer_id": "int32",
        "product_category": "category",
        "quantity": "int16",
        "price": "float32",
        "country": "category"
    },
    parse_dates=["order_date"]
)

print(f"بعد التحميل المحسّن: {df.memory_usage(deep=True).sum() / 1024**2:.2f} MB")

# الخطوة 2: تطبيق التحسين التلقائي على أي أعمدة متبقية
df = optimize_dataframe_memory(df)

# الخطوة 3: حفظ بصيغة Parquet للاستخدام المستقبلي
df.to_parquet("ecommerce_optimized.parquet", engine="pyarrow", compression="zstd")

# الاستخدامات المستقبلية: قراءة فورية مع الأنواع المحفوظة
df = pd.read_parquet("ecommerce_optimized.parquet")

الفرق شاسع. من 800 ميغابايت إلى 80 ميغابايت — أي توفير 90%. وأفضل جزء؟ كل خطوة من هذه الخطوات بسيطة ولا تحتاج أكثر من سطرين أو ثلاثة من الكود.

ملخص التقنيات ونسب التوفير

التقنيةنسبة التوفير التقريبيةمستوى الجهد
تقليص الأنواع الرقمية (int64int8)حتى 87.5%منخفض
تحويل النصوص إلى categoryحتى 98%+منخفض
تحميل أعمدة محددة فقط (usecols)متغيرة (حسب عدد الأعمدة)منخفض
استخدام صيغة Parquet50-80% (حجم الملف)منخفض
القراءة المجزأة (chunksize)تتجنب نفاد الذاكرةمتوسط
محرك PyArrow في pandas 3.0حتى 70% (نصوص)صفر (تلقائي)
Copy-on-Write في pandas 3.0يلغي النسخ غير الضروريةصفر (تلقائي)

متى تحتاج إلى أدوات أخرى غير pandas؟

رغم كل هذه التقنيات، هناك نقطة يجب أن نكون صريحين بشأنها: pandas لها حدود. إذا كنت تتعامل مع بيانات أكبر من ذاكرة جهازك حتى بعد التحسين، فقد حان وقت النظر في بدائل:

  • Polars: مكتبة مكتوبة بلغة Rust، أسرع بـ 5-30 مرة من pandas للبيانات الكبيرة وتستهلك ذاكرة أقل بكثير. مثالية لأنابيب ETL ومعالجة البيانات الثقيلة.
  • Dask: تتيح معالجة بيانات أكبر من الذاكرة عبر التوزيع على عدة أنوية أو أجهزة، مع واجهة مشابهة جدًا لـ pandas (وهذا يسهّل الانتقال).
  • DuckDB: قاعدة بيانات تحليلية مدمجة تدعم SQL مباشرة على ملفات Parquet وCSV. إذا كنت مرتاحًا مع SQL، فهذا خيار ممتاز.

القاعدة العملية بسيطة: ابدأ بتحسين pandas باستخدام التقنيات المذكورة أعلاه. إذا ظل حجم بياناتك يتجاوز الذاكرة المتاحة بعد التحسين، عندها انتقل إلى Polars أو Dask حسب احتياجاتك.

الأسئلة الشائعة

كيف أعرف حجم الذاكرة الحقيقي لـ DataFrame في pandas؟

استخدم df.memory_usage(deep=True).sum() مع المعامل deep=True للحصول على القيمة الحقيقية. بدون هذا المعامل، لا يتم حساب الحجم الفعلي للأعمدة النصية (object)، مما يعطي قراءة أقل بكثير من الواقع.

هل تحويل الأنواع الرقمية (downcasting) يؤثر على دقة البيانات؟

بالنسبة للأعداد الصحيحة، لا يوجد فقدان في الدقة طالما أن القيم ضمن نطاق النوع الجديد. أما الأعداد العشرية، فالتحويل من float64 إلى float32 يقلل الدقة من 15-16 رقمًا عشريًا إلى 6-7 أرقام. هذا كافٍ لمعظم التطبيقات العملية، لكن تجنبه في الحسابات المالية الدقيقة أو العمليات العلمية التي تتطلب دقة عالية.

ما الفرق بين engine="pyarrow" وdtype_backend="pyarrow" في read_csv؟

المعامل engine="pyarrow" يحدد محرك القراءة (parsing engine) ويستفيد من المعالجة متعددة الأنوية لتسريع قراءة الملف. أما dtype_backend="pyarrow" فيحدد كيفية تخزين البيانات في الذاكرة بعد القراءة باستخدام تنسيق Arrow بدلاً من NumPy. الأفضل؟ استخدهما معًا للحصول على أقصى أداء.

هل أحتاج إلى تثبيت PyArrow بشكل منفصل مع pandas 3.0؟

نعم، PyArrow ليست تبعية إلزامية تُثبَّت تلقائيًا مع pandas. لكن يُوصى بشدة بتثبيتها عبر pip install pyarrow للاستفادة من النوع النصي الجديد وتحسينات الأداء. بدونها، يعود pandas لاستخدام النوع object القديم — وتخسر كثيرًا من المزايا.

ما أفضل صيغة لحفظ البيانات الكبيرة بدلاً من CSV؟

Parquet هي الخيار الأفضل في معظم الحالات بلا منازع. فهي عمودية (تقرأ أعمدة محددة بسرعة)، مضغوطة (أصغر بـ 2-5 مرات من CSV)، وتحفظ أنواع البيانات تلقائيًا. استخدم ضغط snappy لأفضل سرعة قراءة، أو zstd لأصغر حجم ملف.

عن الكاتب Editorial Team

Our team of expert writers and editors.