EduFlow
Python لتحليل البيانات
⁦0%⁩

الوحدة 4 · التحليل: التجميع والدمج وإعادة التشكيل

إعادة التشكيل باستخدام pivot_table و melt

18 دقراءة 4 دتمرين Python

الدرس في لمحة

أعِد تشكيل البيانات بين الصيغتين الطويلة والعريضة - pivot_table لملخّصات الجداول المتقاطعة، و crosstab للأعداد والمعدّلات، و melt لإعادة الجداول العريضة على طريقة جداول البيانات إلى جداول طويلة قابلة للتحليل.

  1. 1الجدول المحوري pivot_table: لخّص وأعِد التشكيل معًا
  2. 2crosstab: الأعداد والمعدّلات
  3. 3melt: من العريض إلى الطويل من جديد

لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.

المدير لا يريد 3,991 صفًا. يريد شبكة: القنوات على الجانب، والأرباع في الأعلى، والإيراد في الخلايا. وفريق المالية يرسل الأهداف بالطريقة نفسها، عمود لكل ربع. أما تحليلك فيعمل أفضل على الجداول الطويلة التي فيها صف لكل ملاحظة، لأن groupby وmerge والتصفية كلها تتوقّع هذا الشكل. إعادة التشكيل تنقل البيانات بين الصيغتين، وهي آخر مهارة أساسية قبل أن تبدأ الإجابة عن الأسئلة بجدّية.

على اليسار، جدول طويل بالأعمدة quarter و channel و revenue فيه ستة صفوف. سهم عنوانه pivot_table يشير إلى جدول عريض فيه الأرباع صفوفًا والقناتان web و app أعمدة. وسهم عائد عنوانه melt يشير إلى الخلف. طويل quarter channel revenue Q1 web 27,868 Q1 app 16,814 Q2 web 30,110 Q2 app 15,840 صف لكل ملاحظة pivot_table melt عريض quarter web app Q1 27,868 16,814 Q2 30,110 15,840 عمود لكل قناة
الصيغتان الطويلة والعريضة تحملان البيانات نفسها بشكلين مختلفين

الجدول المحوري pivot_table: لخّص وأعِد التشكيل معًا

ابدأ من جدول المبيعات الذي بنيته في الدرس 4.2 (أُعيد بناؤه هنا في بضعة أسطر)، واطلب الإيراد بحسب القناة والسنة:

Python
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).

Python
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:

Python
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 كجدول بيانات، عمود لكل ربع. لمقارنتها بالإيراد الفعلي تحتاجها طويلة، صف لكل قناة وربع، كي يمكن دمجها:

Python
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 إلى صفوف.

قراءات إضافية

تحقّق من فهمك

3 أسئلة

السؤال 1 من 3

تُطلق sales.pivot(index="quarter", columns="channel", values="revenue") الخطأ ValueError: Index contains duplicate entries, cannot reshape. ما الإصلاح؟

السؤال 2 من 3

في جدول المالية الأعمدة channel, Q1, Q2, Q3, Q4. أيّ استدعاء يجعل صفًا لكل قناة وربع؟

السؤال 3 من 3

تعرض pd.crosstab(sessions["device"], sessions["purchased"], normalize="index") القيمة 0.067 في صف mobile والعمود 1. ماذا تعني؟

تدريب

تمرين Python

عام 2025 مقابل الهدف

يحتوي sales_2025 على بنود 2025 غير الملغاة، مع العمودين quarter وchannel، وtargets هو جدول فريق المالية العريض. ابنِ grid، جدولًا محوريًا للإيراد فيه الأرباع من 1 إلى 4 صفوف والقنوات أعمدة. حوّل targets بـ melt إلى targets_long بالأعمدة channel وquarter (عدد صحيح من 1 إلى 4) وtarget. ثم أنشئ vs_target: هو targets_long مع عمود revenue فيه الإيراد الفعلي لكل قناة وربع. وأخيرًا اجعل beat عدد الصفوف التي بلغ فيها الإيراد الهدف.

ملفات البيانات: order_items.csv, orders.csv

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)
sales_2025 = sales[sales["year"] == 2025]

targets = pd.DataFrame({
    "channel": ["marketplace", "mobile_app", "web"],
    "Q1": [5000, 15000, 28000],
    "Q2": [5000, 16000, 30000],
    "Q3": [6000, 20000, 40000],
    "Q4": [9000, 30000, 60000],
})

# 1. grid: 2025 revenue with quarters (1-4) as rows and channels as columns
grid = None

# 2. targets_long: columns channel, quarter (int 1-4), target - 12 rows
targets_long = None

# 3. vs_target: targets_long with an added "revenue" column (actual 2025 revenue
#    for that channel and quarter)
vs_target = None

# 4. Number of channel-quarters where revenue reached the target
beat = None
جارٍ تحميل المحرّر…

Ctrl/⌘ + Enter للتشغيل · يعمل في بيئة معزولة داخل متصفّحك. لا شيء يغادر جهازك.

(لا مخرجات)

الفحوص

  • grid بحجم 4 أرباع × 3 قنوات، والويب في الربع الرابع نحو 87,530
  • في targets_long 12 صفًا والأرباع أعداد صحيحة
  • في vs_target 12 صفًا وإيراد فعلي مجموعه يساوي إجمالي 2025
  • beat يساوي 8