التوصيف باستخدام timeit وmemory_profiler
قِس زمن التنفيذ باستخدام %timeit والذاكرة القصوى باستخدام memory_profiler لتحديد أبطأ أجزاء مسار المعالجة.
التوصيف باستخدام timeit وmemory_profiler درس مجاني في Pandas & NumPy Academy على CoddyKit. هذا هو الدرس 1 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في Pandas & NumPy Academy، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة Pandas & NumPy Academy 4 دروس في المجموع.
لماذا ننشئ ملف تعريف قبل التحسين؟
من الأخطاء الشائعة تحسين الشيفرة بصورة حدسية، أي إعادة كتابة دالة تبدو بطيئة من دون قياس ما إذا كانت فعلًا عنق الزجاجة. ففي مسارات معالجة البيانات، يأتي 90% من وقت التنفيذ غالبًا من 10% من الشيفرة. يحدد إنشاء ملف التعريف بدقة مواضع استهلاك الوقت والذاكرة، حتى تتمكن من تركيز جهود التحسين حيث تكون أكثر فائدة. والقاعدة الذهبية هي: قِس أولًا، ثم حسّن. تقيس أدوات مثل timeit الوقت، بينما تقيس memory_profiler ذاكرة RAM.
import pandas as pd
import numpy as np
import timeit
# Naive approach vs vectorised approach — which is faster?
np.random.seed(0)
df = pd.DataFrame({'a': np.random.randn(100000), 'b': np.random.randn(100000)})
t1 = timeit.timeit(lambda: df['a'] + df['b'], number=100)
t2 = timeit.timeit(lambda: [a + b for a, b in zip(df['a'], df['b'])], number=10)
print(f'Vectorised (100 runs): {t1:.3f}s')
print(f'List comprehension (10 runs): {t2:.3f}s')
print(f'Per-run speedup: ~{(t2/10)/(t1/100):.0f}x')timeit: مؤقت Python المدمج
تقيس الوحدة timeit وقت التنفيذ من خلال تشغيل عبارة عدة مرات وإرجاع إجمالي الوقت المنقضي. يشغّل timeit.timeit(stmt, number=n) العبارة stmt عدد n من المرات ويعيد إجمالي الثواني. اقسم الناتج على number للحصول على زمن التشغيل الواحد. وللحصول على قياسات دقيقة، اختر قيمة number تجعل التشغيل الإجمالي يستغرق 0.1 ثانية على الأقل؛ فالتشغيل الواحد لعملية سريعة لا يمثل الأداء بسبب ضوضاء جدولة النظام.
import timeit
import pandas as pd
import numpy as np
np.random.seed(0)
df = pd.DataFrame({'x': np.random.randn(10000)})
# Compare three ways to square a column
time_a = timeit.timeit(lambda: df['x'] ** 2, number=1000)
time_b = timeit.timeit(lambda: df['x'].apply(lambda v: v**2), number=100)
time_c = timeit.timeit(lambda: np.square(df['x']), number=1000)
print(f'Operator **2 per run: {time_a/1000*1000:.3f} ms')
print(f'.apply(v**2) per run: {time_b/100*1000:.3f} ms')
print(f'np.square() per run: {time_c/1000*1000:.3f} ms')تعليمة %timeit السحرية في Jupyter
في دفاتر Jupyter، تختار التعليمة السحرية %timeit تلقائيًا عدد التكرارات وعمليات التشغيل لتقديم تقدير مستقر إحصائيًا، ثم تعرض أفضل الأزمنة ومتوسطها مع الانحراف المعياري. وهي الطريقة الأسهل لإجراء اختبار أداء سريع لسطر واحد. استخدم %%timeit (علامة النسبة المئوية المزدوجة) لقياس خلية كاملة. ويكون تنسيق المخرجات: X ns/μs/ms ± Y ns per loop (mean ± std. dev. of 7 runs, N loops each).
# In Jupyter notebook:
# %timeit df['x'] ** 2
# Output: 47.3 us +- 1.2 us per loop (mean +- std. dev. of 7 runs, 10000 loops each)
# Multi-line cell timing:
# %%timeit
# result = df['x'] ** 2
# total = result.sum()
# In a regular Python script, use timeit.timeit() instead:
import timeit
import pandas as pd, numpy as np
df = pd.DataFrame({'x': np.random.randn(10000)})
result = timeit.repeat(lambda: df['x']**2, number=1000, repeat=7)
print(f'Best run: {min(result)/1000*1000:.3f} ms per call')cProfile لإنشاء ملف تعريف على مستوى الدوال
بينما يقيس timeit تعبيرًا واحدًا، ينشئ cProfile ملف تعريف لمكدس الاستدعاءات بأكمله، إذ يوضح مقدار الوقت المستغرق في كل دالة جرى استدعاؤها أثناء التنفيذ. وهذا ضروري لتحديد عنق الزجاجة في مسار معالجة معقد. شغّل python -m cProfile -s cumulative script.py من سطر الأوامر، أو استخدم cProfile.run('function()') داخل شيفرة. وفي Jupyter، استخدم %prun function().
import cProfile
import pandas as pd
import numpy as np
np.random.seed(0)
df = pd.DataFrame({'a': np.random.randn(50000), 'b': np.random.randn(50000)})
def slow_pipeline(df):
result = []
for _, row in df.iterrows():
result.append(row['a'] * row['b'])
return pd.Series(result)
# Profile the function
print('Profile output (top 5 functions by cumulative time):')
cProfile.run('slow_pipeline(df.head(1000))', sort='cumulative')قياس استهلاك الذاكرة
استخدم df.memory_usage(deep=True) لمعرفة مقدار ذاكرة RAM التي يستهلكها كل عمود. وتقيس الوسيطة deep=True الذاكرة الفعلية التي تستهلكها أعمدة object dtype (السلاسل النصية)، والتي تعرض بخلاف ذلك حجم المؤشر فقط. استخدم .sum() للحصول على إجمالي ذاكرة DataFrame بالبايت. اقسم على 1e6 للحصول على MB أو على 1e9 للحصول على GB. وهذه هي نقطة البداية لتحسين الذاكرة؛ فلا يمكنك تقليل ما لم تقسه.
import pandas as pd
import numpy as np
np.random.seed(0)
df = pd.DataFrame({
'id': range(100000),
'name': ['user_' + str(i) for i in range(100000)],
'score': np.random.randn(100000),
'category': np.random.choice(['A', 'B', 'C', 'D'], 100000)
})
mem = df.memory_usage(deep=True)
print('Memory per column (bytes):')
print(mem)
print(f'\nTotal: {mem.sum() / 1e6:.2f} MB')memory_profiler لقياس الذاكرة سطرًا بسطر
تقيس حزمة memory_profiler (pip install memory-profiler) استخدام RAM سطرًا بسطر داخل دالة. زيّن الدالة بالوسم @profile وشغّلها باستخدام python -m memory_profiler script.py. وفي Jupyter، استخدم التعليمة السحرية %memit (من إضافة ipython) لقياس ذروة الذاكرة لتعبير واحد. ويساعد ذلك على تحديد هياكل البيانات التي تسبب ارتفاعات مفاجئة في استهلاك الذاكرة، وغالبًا ما يكون السبب هو الدمج داخل حلقة بدلًا من جمع البيانات ودمجها مرة واحدة.
# Install: pip install memory-profiler
# Usage as a decorator:
# from memory_profiler import profile
# @profile
# def process_data():
# import pandas as pd, numpy as np
# df = pd.DataFrame({'x': np.random.randn(1000000)})
# result = df['x'].rolling(100).mean() # <-- sees if this spikes RAM
# return result
# Run: python -m memory_profiler script.py
# In Jupyter (after %load_ext memory_profiler):
# %memit pd.DataFrame({'x': np.random.randn(1000000)}).rolling(100).mean()
print('memory_profiler decorates functions for line-by-line RAM measurement.')
print('Typical output: Line # Mem usage Increment Line Contents')مقارنة sys.getsizeof وmemory_usage للكائنات
تعيد sys.getsizeof(obj) حجم ذاكرة كائن Python، لكنها بالنسبة إلى DataFrames في pandas تعرض فقط الحمل الزائد للحاوية (بضع مئات من البايتات)، لا البيانات الفعلية. استخدم دائمًا df.memory_usage(deep=True).sum() مع DataFrames. أما بالنسبة إلى كائنات Python الفردية مثل القوائم والقواميس، فـ sys.getsizeof دقيق، لكنه لا يتتبع الكائنات المتداخلة بصورة递归ية؛ استخدم مكتبة pympler (asizeof) للحصول على أحجام الكائنات بعمق.
import sys
import pandas as pd
import numpy as np
df = pd.DataFrame({'x': np.random.randn(100000)})
# sys.getsizeof reports only the container metadata — misleading!
print('sys.getsizeof(df):', sys.getsizeof(df), 'bytes (misleading!)')
# memory_usage gives the true memory
true_mem = df.memory_usage(deep=True).sum()
print('df.memory_usage(deep=True).sum():', true_mem, 'bytes')
print(f'True memory: {true_mem/1e6:.2f} MB')نمط مدير السياق لقياس الزمن
بالنسبة إلى خطوات مسار المعالجة الطويلة التي لا يناسبها timeit (مثل استعلامات قواعد البيانات أو عمليات الإدخال والإخراج للملفات)، استخدم مدير سياق بسيطًا مع time.perf_counter(). ضع كل خطوة من خطوات مسار المعالجة داخل سياق مؤقت لتسجيل مدتها. وهذا أكثر عملية من timeit في شيفرة الإنتاج، لأنه يسجل الزمن الفعلي المنقضي على الساعة، بما في ذلك انتظار الإدخال والإخراج؛ وهي أمور يقيسها timeit أيضًا، لكن يصعب دمجها مع أطر عمل التسجيل.
import time
import contextlib
import pandas as pd
import numpy as np
@contextlib.contextmanager
def timer(name):
start = time.perf_counter()
yield
elapsed = time.perf_counter() - start
print(f'[{name}] {elapsed*1000:.1f} ms')
np.random.seed(0)
df = pd.DataFrame({'x': np.random.randn(500000), 'y': np.random.randn(500000)})
with timer('rolling mean'):
result1 = df['x'].rolling(30).mean()
with timer('groupby mean'):
df['group'] = df['x'].round(0)
result2 = df.groupby('group')['y'].mean()إنشاء ملف تعريف لعمليات Pandas بطريقة منهجية
سير عمل منهجي لتحليل الأداء: 1) ابدأ بعينة تمثيلية من بياناتك (يكفي استخدام 10,000 صف لإجراء مقارنات التوقيت). 2) حدّد أبطأ الخطوات باستخدام سياق مؤقّت أو cProfile. 3) أعد كتابة أبطأ خطوة باستخدام نهج أسرع (التنفيذ المتجهي، أو نوع البيانات Categorical، أو إضافة C). 4) تحقّق من صحة النتائج باستخدام عبارات assert. 5) أعد قياس الوقت للتأكد من تحسّن السرعة. 6) كرّر ذلك مع عنق الزجاجة التالي حتى يفي مسار المعالجة باتفاقية مستوى الخدمة (SLA) الخاصة به.
import pandas as pd
import numpy as np
import timeit
np.random.seed(0)
df = pd.DataFrame({
'value': np.random.randn(100000),
'category': np.random.choice(['A', 'B', 'C', 'D', 'E'], 100000)
})
# Benchmark two groupby implementations
t1 = timeit.timeit(lambda: df.groupby('category')['value'].mean(), number=100)
df2 = df.copy()
df2['category'] = df2['category'].astype('category')
t2 = timeit.timeit(lambda: df2.groupby('category')['value'].mean(), number=100)
print(f'Object dtype groupby: {t1/100*1000:.2f} ms per call')
print(f'Categorical dtype: {t2/100*1000:.2f} ms per call')
print(f'Speedup: {t1/t2:.1f}x')متى ينبغي تحليل الأداء: حدود عملية
لا تحتاج كل البرامج النصية إلى تحليل الأداء. خصّص وقتًا لتحليل الأداء عندما: يستغرق مسار المعالجة أكثر من 30 ثانية للتنفيذ ويُشغَّل بشكل متكرر (يوميًا أو أكثر)، أو تعالج دالة أكثر من 100,000 صف، أو يتجاوز استخدام الذاكرة 50% من ذاكرة RAM المتاحة. أما البرامج النصية التي تُشغَّل مرة واحدة أو تنفّذ في غضون ثوانٍ، فإمكانية القراءة أهم من التحسين على مستوى الميكروثانية. وغالبًا ما تتجاوز تكلفة صيانة الشيفرة المُحسّنة بشكل مفرط وغير الواضحة الوقتَ الذي يوفره التحسين.
لمحة سريعة: ملخص أدوات تحليل الأداء
مرجع سريع:
- timeit.timeit — قياس زمن تنفيذ عبارة واحدة (في برنامج نصي)
- %timeit / %%timeit — قياس الزمن في Jupyter (مع التكرار تلقائيًا)
- %prun / cProfile — تحليل مكدس الاستدعاءات على مستوى الدوال
- df.memory_usage(deep=True) — قياس استهلاك RAM لكل عمود
- memory_profiler @profile — قياس استهلاك RAM سطرًا بسطر
- time.perf_counter() — مؤقّت يدوي لخطوات مسار المعالجة
اختبار سريع
اختبر مدى فهمك لأدوات تحليل الأداء التي تناولها هذا الدرس.
مراجعة الدرس
تعلّمت في هذا الدرس أن timeit.timeit و%timeit يقيسان زمن تنفيذ عمليات محددة، وأن df.memory_usage(deep=True) يقيس استهلاك RAM لكل عمود بدقة، وأن cProfile / memory_profiler يقدّمان تحليلًا أعمق لمسارات المعالجة المعقدة. احرص دائمًا على تحليل الأداء قبل التحسين، وتحقّق من صحة النتائج بعد كل تغيير. بعد ذلك، سنستكشف كيفية تجنّب حلقات iterrows البطيئة واستبدالها بعمليات متجهية.
الأسئلة الشائعة
هل درس «التوصيف باستخدام timeit وmemory_profiler» مجاني؟
نعم — نص درس «التوصيف باستخدام timeit وmemory_profiler» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة Pandas & NumPy Academy، انتقل إلى CoddyKit PRO. تتضمن دورة Pandas & NumPy Academy 4 دروس في المجموع.
ماذا ستتعلم في «التوصيف باستخدام timeit وmemory_profiler»؟
قِس زمن التنفيذ باستخدام %timeit والذاكرة القصوى باستخدام memory_profiler لتحديد أبطأ أجزاء مسار المعالجة. تتمرن على Pandas & NumPy Academy مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.
هل أحتاج إلى خبرة سابقة لأبدأ Pandas & NumPy Academy؟
لا تُشترط خبرة سابقة. Pandas & NumPy Academy على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 1 من أصل 4.
كم من الوقت يستغرق درس «التوصيف باستخدام timeit وmemory_profiler»؟
معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.
هل يمكنني كتابة وتشغيل أكواد في درس Pandas & NumPy Academy هذا؟
نعم. كل درس في Pandas & NumPy Academy يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.
جميع الدروس في هذه الدورة
- التوصيف باستخدام timeit وmemory_profiler
- تجنب iterrows وحلقات Python
- أنواع بيانات فعّالة لتقليل الذاكرة
- القراءة على دفعات للملفات الكبيرة