الوحدة 4 · التحليل: التجميع والدمج وإعادة التشكيل
إعادة التشكيل باستخدام pivot_table و melt
18 دقراءة 4 دتمرين Python
الدرس في لمحة
أعِد تشكيل البيانات بين الصيغتين الطويلة والعريضة - pivot_table لملخّصات الجداول المتقاطعة، و crosstab للأعداد والمعدّلات، و melt لإعادة الجداول العريضة على طريقة جداول البيانات إلى جداول طويلة قابلة للتحليل.
- 1الجدول المحوري pivot_table: لخّص وأعِد التشكيل معًا
- 2crosstab: الأعداد والمعدّلات
- 3melt: من العريض إلى الطويل من جديد
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
المدير لا يريد 3,991 صفًا. يريد شبكة: القنوات على الجانب، والأرباع في الأعلى، والإيراد في الخلايا. وفريق المالية يرسل الأهداف بالطريقة نفسها، عمود لكل ربع. أما تحليلك فيعمل أفضل على الجداول الطويلة التي فيها صف لكل ملاحظة، لأن groupby وmerge والتصفية كلها تتوقّع هذا الشكل. إعادة التشكيل تنقل البيانات بين الصيغتين، وهي آخر مهارة أساسية قبل أن تبدأ الإجابة عن الأسئلة بجدّية.
الجدول المحوري pivot_table: لخّص وأعِد التشكيل معًا
ابدأ من جدول المبيعات الذي بنيته في الدرس 4.2 (أُعيد بناؤه هنا في بضعة أسطر)، واطلب الإيراد بحسب القناة والسنة:
import pandas as pd
items = pd.read_csv("order_items.csv")
orders = pd.read_csv("orders.csv", parse_dates=["order_date"])
items["revenue"] = items["quantity"] * items["unit_price"] * (1 - items["discount"])
sales = items.merge(orders, on="order_id", how="left", validate="many_to_one")
sales = sales[sales["status"] != "cancelled"]
sales = sales.assign(year=sales["order_date"].dt.year, quarter=sales["order_date"].dt.quarter)
table = sales.pivot_table(index="channel", columns="year", values="revenue",
aggfunc="sum", margins=True, margins_name="Total")
print(table.round(0))كل وسيط يقابل جزءًا من الجدول المحوري (pivot table) في جداول البيانات: index هو الصفوف، وcolumns الأعمدة، وvalues الحقل، وaggfunc دالة التلخيص. تضيف margins=True صف المجاميع وعمودها، وهما يؤكّدان النتيجة: 446,666 دولارًا إجمالًا، وهو تعريف الإيراد من الدرس السابق. نما الإيراد في كل قناة من 2024 إلى 2025، وتضاعف الويب نحو ثلاث مرات.
pivot_table هي الحساب نفسه الذي تجريه groupby(["channel", "year"])["revenue"].sum().unstack(). استخدم groupby حين ستواصل الحساب، وpivot_table حين تكون الشبكة نفسها هي المُخرج. ولعدّ الطلبات المختلفة، غيّر دالة التجميع: values="order_id", aggfunc="nunique". وتستبدل fill_value=0 قيم NaN في الخلايا الفارغة حين يكون الصفر هو الإجابة الصادقة، كما في “لا مبيعات في هذه الخلية”.
من المبالغ إلى الحصص
شبكة المبالغ تستدعي السؤال التالي: كيف تغيّر المزيج؟ اقسم كل عمود على مجموعه، فتعرض الشبكة الحصص بدلًا من المبالغ. تعيد by_year.sum() مجموعًا لكل عمود، وقسمة DataFrame على تلك الـ Series تحاذي كل مجموع مع عموده بحسب التسمية، وهي قاعدة المحاذاة نفسها التي قابلتها مع Series وأخرى في الدرس 2.1. ولقسمة كل صف على مجموع صفّه بدلًا من ذلك، استخدم by_year.div(by_year.sum(axis=1), axis=0).
import pandas as pd
items = pd.read_csv("order_items.csv")
orders = pd.read_csv("orders.csv", parse_dates=["order_date"])
items["revenue"] = items["quantity"] * items["unit_price"] * (1 - items["discount"])
sales = items.merge(orders, on="order_id", how="left", validate="many_to_one")
sales = sales[sales["status"] != "cancelled"]
by_year = sales.pivot_table(index="channel", columns=sales["order_date"].dt.year,
values="revenue", aggfunc="sum")
print((by_year / by_year.sum()).round(3))ارتفعت حصة الويب من الإيراد من 53% في 2024 إلى 59% في 2025، بينما انخفضت حصة التطبيق من 35% إلى 31%. المبالغ تخبرك أن كل شيء نما؛ والحصص تخبرك أين تركّز النمو. أبلغ عن الاثنين، لأن كلًّا منهما يخفي ما يُظهره الآخر.
crosstab: الأعداد والمعدّلات
لعمودين فئويين، تعدّ pd.crosstab التركيبات، وتحوّل normalize الأعداد إلى حصص. هذه زيارات موقع Cartwheel:
import pandas as pd
sessions = pd.read_csv("web_sessions.csv")
print(pd.crosstab(sessions["source"], sessions["device"]))
print(pd.crosstab(sessions["device"], sessions["purchased"], normalize="index").round(3))الجدول الثاني يجيب عن سؤال حقيقي. تجعل normalize="index" مجموع كل صف 1، فيكون العمود 1 معدّل الشراء لكل جهاز: 12.4% من جلسات الحاسوب المكتبي تنتهي بشراء، لكن 6.7% فقط من جلسات الهاتف، مع أن الهاتف يجلب أكبر قدر من الزيارات. هذه الفجوة تستحق حديثًا مع المسؤول عن صفحة الدفع على الهاتف. اختر التطبيع بحسب السؤال: "index" للمعدّلات داخل كل مجموعة صفوف، و"columns" لتركيبة كل عمود، و"all" للحصص من المجموع الكلي.
melt: من العريض إلى الطويل من جديد
تصل أهداف فريق المالية لعام 2025 كجدول بيانات، عمود لكل ربع. لمقارنتها بالإيراد الفعلي تحتاجها طويلة، صف لكل قناة وربع، كي يمكن دمجها:
import pandas as pd
targets = pd.DataFrame({
"channel": ["marketplace", "mobile_app", "web"],
"Q1": [5000, 15000, 28000],
"Q2": [5000, 16000, 30000],
"Q3": [6000, 20000, 40000],
"Q4": [9000, 30000, 60000],
})
long = targets.melt(id_vars="channel", var_name="quarter", value_name="target")
long["quarter"] = long["quarter"].str[1].astype(int)
print(long.head(6))
print(long.shape)id_vars هي الأعمدة التي تحدّد الصف وتبقى كما هي؛ وكل عمود آخر يصبح زوجًا من القيم في عمودين جديدين، اسم العمود القديم في var_name وقيمته في value_name. ثلاثة صفوف في أربعة أرباع تعطي اثني عشر. تأخذ str[1] الحرف الثاني من "Q1"، فيصبح الربع رقمًا يطابق sales["quarter"]، جاهزًا لعملية دمج مثل التي في الدرس 4.2.
في التمرين تبني شبكة الأرباع لعام 2025 وتفحص الأداء مقابل تلك الأهداف. وبعدها يأتي الزمن نفسه: إعادة التجميع الزمني، والفترات، ومعدّلات النمو.
أهم النقاط
- البيانات الطويلة فيها صف لكل ملاحظة؛ والبيانات العريضة تنشر متغيّرًا واحدًا عبر الأعمدة. تحلّل pandas البيانات الطويلة أفضل، ويقرأ الناس الجداول العريضة أفضل.
pivot_table(index=..., columns=..., values=..., aggfunc="sum")تجمّع وتعيد التشكيل في خطوة واحدة، مثل الجدول المحوري في جداول البيانات.pivotتعيد التشكيل فقط؛ وتُطلقValueError: Index contains duplicate entriesحين تتشارك عدة صفوف خلية واحدة، فاستخدمpivot_tableللبيانات الخام.pd.crosstab(a, b, normalize="index")تحوّل عمودين إلى نِسَب مئوية لكل صف، وهي أسرع طريقة لمقارنة المعدّلات بين المجموعات.melt(id_vars=..., var_name=..., value_name=...)تعيد الأعمدة العريضة مثلQ1…Q4إلى صفوف.