إنتقل إلى المحتوى الرئيسي

الأسبوع 9: التجميع (Groupby)، التلخيص، والدمج

🗂️ "قسّم مجموعة البيانات إلى مجموعات، ثم لخّص كل مجموعة" هي جملة واحدة في pandas وحوالي عشرة أسطر في بايثون الخالصة.

🎯 أهداف التعلّم

بنهاية هذا الأسبوع ستكون قادرًا على:

  • تقسيم DataFrame إلى مجموعات بـ .groupby()، وشرح الصلة بتقسيم مجموعة رياضية.
  • حساب تلخيصات لكل مجموعة (.mean()، .sum()، .count()، وعدة تلخيصات دفعة واحدة بـ .agg()).
  • التجميع حسب أكثر من عمود دفعة واحدة، وتحويل نتيجة مُجمَّعة مرة أخرى إلى DataFrame عادي.
  • دمج DataFrame بآخر بـ .merge()، بمطابقة الصفوف بمفتاح مشترك.

الدرس

.groupby(): تقسيم مجموعة

تذكّر أن تقسيم (partition) مجموعة SS يُجزّئها إلى مجموعات فرعية منفصلة اتحادها هو SS. تفعل .groupby("column") هذا بالضبط: تُقسّم صفوف DataFrame إلى مجموعات، واحدة لكل قيمة مختلفة في ذلك العمود:

df.groupby("lunch") # كائن GroupBy — المجموعات تكوّنت، لا شيء حُسب بعد
df.groupby("lunch")["math_score"].mean() # متوسط math_score داخل كل مجموعة lunch

لا يُحسَب شيء حتى تُلحق تلخيصًا — .groupby() وحدها تصف فقط كيفية تقسيم الصفوف.

التلخيص

تُخطّط التلخيصات القياسية مباشرة على إحصاءات تعرفها بالفعل:

df.groupby("lunch")["math_score"].mean() # متوسط math_score لكل نوع lunch
df.groupby("lunch")["math_score"].count() # كم صفًا (طالبًا) لكل مجموعة
df.groupby("lunch").agg({ # عدة أعمدة/إحصاءات دفعة واحدة
"math_score": "mean",
"reading_score": "mean",
})

تُقرأ .groupby(...)[...] من اليسار لليمين كـ"قسّم بهذا العمود، ثم انظر لهذا العمود الآخر داخل كل جزء" — صياغة pandas لـ"لكل مجموعة، احسب إحصاءً على متغيّر مختلف".

التجميع حسب أكثر من عمود

تمرير قائمة من أسماء الأعمدة يُجمّع حسب كل مجموعة فريدة من قيمها دفعة واحدة — نفس فكرة التقسيم بزوج (a,b)(a, b) بدلًا من قيمة واحدة:

df.groupby(["lunch", "gender"])["math_score"].mean()

يُجيب هذا عن سؤال أكثر تحديدًا من أي عمود بمفرده: هل يبقى أثر lunch على math_score بنفس الطريقة داخل كل gender، أم يبدو مختلفًا بمجرد فصلهما؟

استرجاع DataFrame عادي

تستخدم نتيجة groupby-aggregate العمود (الأعمدة) المُجمَّعة كفهرسها بدلًا من فهرس صفوف عادي 0, 1, 2, ... — مريح لعمليات بحث إضافية، لكن أحيانًا تريد DataFrame عاديًا مرة أخرى (مثلًا، لترتيبه، أو دمجه بشيء آخر). تفعل .reset_index() ذلك:

summary = df.groupby("lunch")["math_score"].mean().reset_index()
# lunch math_score
# free/reduced 58.2
# standard 66.9

summary.sort_values("math_score", ascending=False) # الآن عمود عادي يمكنك الترتيب به

الدمج: جمع اثنين من DataFrame

تدمج .merge() اثنين من DataFrame على عمود مفتاح مشترك، نفس فكرة الربط في قاعدة بيانات أو مطابقة مُدخلات بين جدولي بحث بمعرِّف مشترك:

students = pd.DataFrame({"student_id": [1, 2, 3], "name": ["Amina", "Youssef", "Sara"]})
grades = pd.DataFrame({"student_id": [1, 2, 3], "grade": ["A", "B", "A"]})

merged = students.merge(grades, on="student_id")
# student_id | name | grade
# 1 | Amina | A
# 2 | Youssef | B
# 3 | Sara | A

how="inner" (الافتراضي) تُبقي فقط الصفوف حيث يوجد المفتاح في كلا DataFrame؛ تتحكم how="left"/"right"/"outer" بما يحدث للصفوف التي يفتقد مفتاحها من الجانب الآخر — يستحق الفحص صراحة كلما احتمل ألا يتطابق عدد الصفوف بعد دمج. إن كانت أعمدة المفتاح بأسماء مختلفة في كل DataFrame، استخدم left_on/right_on بدلًا من on:

students.merge(scores, left_on="student_id", right_on="id")

⚠️ أخطاء شائعة

  • نسيان أن .groupby() وحدها لا تحسب شيئًا. df.groupby("lunch") بمفردها مجرد وصف للتقسيم — تحتاج دائمًا تلخيصًا (.mean()، .agg(...)، إلخ) مُلحَقًا لرؤية أرقام فعليًا.
  • افتراض أن الدمج يحافظ على عدد الصفوف الأصلي. merge بمفتاح مكرر في أي جانب يمكن أن يُنتج صفوفًا أكثر مما كان لدى أي مدخل — تحقق دائمًا من .shape بعد دمج لست متأكدًا منه 100%.
  • عدم فحص how= صراحة. الافتراضي "inner" يُسقط بصمت أي صف لا يوجد مفتاحه بالجانب الآخر — جيد عندما يكون هذا فعليًا ما تريده، لكن يستحق أن يكون خيارًا مقصودًا، لا حادثًا.
  • نسيان .reset_index() عندما تحتاج عمودًا عاديًا مرة أخرى. محاولة .sort_values() أو .merge() باستخدام فهرس نتيجة groupby (بدلًا من عمود عادي) وكأنه عمود عادي ستفشل أو تتصرف بشكل غير متوقع حتى تستدعي .reset_index().

🧩 تحديات

باستخدام DataFrame على نمط بيانات أداء الطلاب (تشمل الأعمدة gender وmath_score)، احسب متوسط math_score لكل gender.

جمّع حسب test_preparation_course واحسب كلًا من متوسط وعدّ math_score لكل مجموعة، في استدعاء .agg(...) واحد.

أي فئة lunch لديها متوسط reading_score أعلى؟ أجب باستخدام .groupby()، وليس تصفية يدوية.

أنشئ اثنين من DataFrame يدويًا (مثلًا واحد بأسماء طلاب، وآخر بدرجة منفصلة لكل منهم)، مشتركَين بعمود مفتاح، وادمجهما. ثم جرّب عدم مطابقة مفتاح واحد عمدًا ولاحظ ما تفعله how="inner" به مقابل how="outer".

جمّع حسب lunch وtest_preparation_course معًا، واحسب متوسط math_score لكل مجموعة. هل يبقى نمط التحدي 3 داخل كلا مجموعتي التحضير للاختبار، أم يبدو مختلفًا؟

خذ نتيجة التحدي 1 لديك (متوسط math_score حسب gender)، حوّلها إلى DataFrame عادي بـ.reset_index()، ورتّبها من الأعلى للأدنى متوسطًا.

🤔 أسئلة سقراطية

  • .groupby("lunch") وحدها (بلا تلخيص مُلحَق) لا تطبع جدول أرقام — بماذا تعتقد أنها تُعيد فعليًا، ولماذا تنتظر pandas لحساب أي شيء حتى تحدد تلخيصًا؟
  • إن كان اثنان من DataFrame يُدمَجان يشتركان بعمود مفتاح لكن أحدهما به مفتاح مكرر (صفان بنفس student_id)، ماذا تتوقع أن يحدث لعدد الصفوف بعد الدمج؟ اختبره.
  • تتيح لك .groupby(...).agg({...}) تطبيق تلخيص مختلف لكل عمود (مثلًا متوسط لواحد، أعلى قيمة لآخر) في استدعاء واحد. لماذا قد لا يكون حساب متوسط كل عمود بنفس الإحصاء منطقيًا دائمًا لمجموعة بيانات حقيقية؟
  • التجميع حسب عمودين دفعة واحدة (["lunch", "test_preparation_course"]) يمكن أن يكشف نمطًا يُخفيه التجميع بأي عمود بمفرده، أو يجعل نمطًا بدا قويًا يبدو أضعف فعليًا بمجرد مراعاة العامل الثاني. لماذا يُغيّر التقسيم لمجموعات أدق القصة أحيانًا بهذا القدر؟
  • تستخدم نتيجة groupby-aggregate العمود المُجمَّع كفهرسها بدلًا من فهرس أعداد صحيحة عادي. ما الفرق العملي الذي يُحدثه ذلك أول مرة تحاول فيها استخدام .iloc[0] عليها، مقابل على DataFrame عادي؟

✅ اختبار الأسبوع

✅ اختبار الأسبوع

1. ماذا تفعل df.groupby("lunch") بمفردها (بلا تلخيص مُلحَق)؟
2. أي نوع دمج يُبقي فقط الصفوف التي يوجد مفتاحها في كلا DataFrame؟
3. ماذا تحسب df.groupby("lunch").agg({"math_score": "mean", "reading_score": "mean"})؟
4. دالة .merge() لـDataFrame أشبه مفاهيميًا بـ:
5. ماذا تفعل .reset_index() بعد groupby-aggregate؟