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

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

دمج الجداول باستخدام merge

20 دقراءة 3 دتمرين Python

الدرس في لمحة

ادمج الجداول باستخدام merge، واختر نوع الربط المناسب، واحمِ نفسك من الصفوف المكرّرة باستخدام validate= وعدّ الصفوف، واستخدم indicator=True لتجد السجلات التي لا مطابق لها.

  1. 1أول عملية دمج
  2. 2أنواع الربط
  3. 3العثور على ما لم يتطابق
  4. 4اسم العمود نفسه في الطرفين

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

سؤال رئيس النمو يحتاج أعمدة من أربعة جداول. الإيراد موجود في order_items. أما هل أُلغي الطلب ومتى قُدّم فمعلومتان في orders. وفئة المنتج في products، ودولة العميل في customers. في جدول البيانات كنت ستكتب صيغ بحث (lookup)؛ وفي pandas تستخدم الدمج (merge). عمليات الدمج هي المكان الذي تحدث فيه معظم الأخطاء الصامتة في التحليل، لذا يقضي هذا الدرس في فحص عملية الدمج وقتًا يساوي ما يقضيه في كتابتها.

أول عملية دمج

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"])

lines = items.merge(orders, on="order_id", how="left", validate="many_to_one")
print(len(items), "->", len(lines))
print(lines[["order_item_id", "order_id", "revenue", "order_date", "status"]].head(3))

لكل صف في الجدول الأيسر، تبحث pandas عن صف orders الذي له order_id نفسه، وتنسخ أعمدته إليه. بنود كثيرة تشير إلى طلب واحد، فالعلاقة كثير إلى واحد (many to one)، وvalidate="many_to_one" تطلب من pandas أن تتحقّق من ذلك: لو كان في orders صفان لمعرّف واحد، لأطلق الدمج الخطأ MergeError بدلًا من مضاعفة تلك البنود بهدوء. وعدد الصفوف قبل وبعد هو الفحص الثاني. الدمج الأيسر على مفتاح فريد لا يغيّره أبدًا.

الآن يستطيع الإيراد أخيرًا أن يحترم قاعدة العمل التي أجّلتها في الدرس 2.5: الطلبات الملغاة ليست إيرادًا.

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"])
lines = items.merge(orders, on="order_id", how="left", validate="many_to_one")

kept = lines[lines["status"] != "cancelled"]
print(round(kept["revenue"].sum(), 2), "revenue excluding cancelled orders")

446,666.32 دولارًا. هذا هو تعريف الإيراد في بقية الدورة: إيراد البنود بعد الخصومات، دون رسوم الشحن ودون الطلبات الملغاة. الطلبات المُرجَعة تبقى، لأن مبالغها المستردّة تُتتبَّع منفصلة في returns. اكتب تعريفات كهذه حيث يراها القرّاء؛ فالرقم الذي بلا تعريف يدعو إلى الجدال.

حين يكون للمفتاح اسم مختلف في كل طرف، سمِّ الاثنين: left_on="customer_id", right_on="referred_by" في دمج customers مع نفسه يطابق كل عميل بالأشخاص الذين أحالهم. واختر المفتاح الذي يحدّد ما تطابقه. المرتجعات تخصّ بنود الطلبات، فيُربط returns بـ order_items على order_item_id؛ أما ربطه على order_id فسيلحق كل مبلغ مسترد بكل بند في الطلب.

أنواع الربط

تقرّر how ما يحدث للمفاتيح الموجودة في طرف واحد فقط:

مفاتيح الجدول الأيسر A و B و C، ومفاتيح الجدول الأيمن B و C و D. يحتفظ inner بـ B و C. ويحتفظ left بـ A و B و C مع أعمدة يمنى فارغة لـ A. ويحتفظ right بـ B و C و D. ويحتفظ outer بـ A و B و C و D. الأيسر A B C الأيمن B C D inner BC left A + NaNBC right BCNaN + D outer A + NaNBCNaN + D تُملأ النظائر المفقودة بـ NaN
أربعة أنواع من الربط، بحسب المفاتيح التي تبقى

يحتفظ inner (القيمة الافتراضية) فقط بالمفاتيح الموجودة في الطرفين. ويحتفظ left بكل صف أيسر، ويملأ الأعمدة اليمنى بـ NaN حيث لا يوجد نظير. وright صورته المعكوسة، وouter يحتفظ بكل شيء. ولإضافة معلومات إلى جدول وقائع، مثل الطلبات أو البنود أو الجلسات، استخدم how="left" مع جدول الوقائع على اليسار: لن تفقد أبدًا واقعة لأن جدول البحث ناقص، وقيم NaN تبيّن لك بالضبط أين النقص.

العثور على ما لم يتطابق

تضيف indicator=True عمودًا اسمه _merge يسجّل من أين جاء كل صف:

Python
import pandas as pd

customers = pd.read_csv("customers.csv")
orders = pd.read_csv("orders.csv")
buyers = orders[["customer_id"]].drop_duplicates()

check = customers.merge(buyers, on="customer_id", how="left", indicator=True)
print(check["_merge"].value_counts())
never_ordered = check[check["_merge"] == "left_only"]
print(len(never_ordered), "customers never ordered")

85 عميلًا سجّلوا ولم يشتروا شيئًا، وهو العدد نفسه الذي وجدته بـ nunique في الدرس 2.2، لكنه الآن قائمة أسماء تستطيع تسليمها لفريق التسويق. وindicator=True هي أيضًا أسرع تدقيق لأيّ عملية دمج: صفوف right_only مستحيلة في الربط الأيسر، والعدد المفاجئ من صفوف left_only يعني أن مفاتيح يُفترض أن تتطابق لا تتطابق، وكثيرًا ما يكون السبب الأنواع (int في طرف ونص في الآخر) أو مسافات لم تُحذف.

اسم العمود نفسه في الطرفين

في order_items وproducts كليهما عمود unit_price: السعر المدفوع وسعر القائمة. تضيف pandas لاحقتين (suffixes) للتمييز بينهما، وتستطيع اختيارهما:

Python
import pandas as pd

items = pd.read_csv("order_items.csv")
orders = pd.read_csv("orders.csv", parse_dates=["order_date"])
products = pd.read_csv("products.csv")

lines = (
    items.merge(orders[["order_id", "order_date"]], on="order_id", validate="many_to_one")
    .merge(products[["product_id", "name", "unit_price"]], on="product_id",
           suffixes=("_charged", "_list"), validate="many_to_one")
)
lines["price_ratio"] = (lines["unit_price_charged"] / lines["unit_price_list"]).round(2)
print(lines.groupby(lines["order_date"] >= "2025-03-01")["price_ratio"].agg(["min", "max"]))

قبل مارس 2025، كان كل بند يُباع بسعر القائمة بالضبط؛ ومن 1 مارس، صار كل بند يُباع بزيادة 5%. لقد أكّد الدمج للتو زيادة الأسعار من البيانات وحدها، وهي أحد المشتبه بهم في قفزة إيراد الربع الرابع.

في التمرين تبني الجدول الكامل على مستوى البنود الذي تحلّله بقية الدورة. وفي الدرس التالي تعيد تشكيل الجداول بين الصيغتين العريضة والطويلة باستخدام pivot_table وmelt.

أهم النقاط

  • left.merge(right, on="key", how=...) تدمج الجداول بمطابقة قيم المفتاح؛ وhow تقرّر ما يحدث للصفوف التي لا مطابق لها.
  • how="left" تحتفظ بكل صف في الجدول الأيسر، وهي الخيار الافتراضي الآمن لإضافة أعمدة إلى جدول وقائع (fact table).
  • مرّر validate="many_to_one" (أو "one_to_one") كي تُطلق pandas الخطأ MergeError إذا لم تكن مفاتيح الجدول الأيمن فريدة، بدلًا من مضاعفة الصفوف بصمت.
  • قارن len() قبل كل عملية دمج وبعدها؛ فالدمج الأيسر على مفتاح فريد لا يغيّر عدد الصفوف أبدًا.
  • تضيف indicator=True عمودًا اسمه _merge (both أو left_only أو right_only) يبيّن أيّ الصفوف وجدت نظيرًا.

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

تحقّق من فهمك

3 أسئلة

السؤال 1 من 3

دمجت order_items (3,991 صفًا) دمجًا أيسر مع orders على order_id. كم صفًا يجب أن يكون في النتيجة؟

السؤال 2 من 3

أيّ عملية دمج تجد العملاء الذين لم يقدّموا أي طلب؟

السؤال 3 من 3

تعيد orders.merge(tickets, on="customer_id") 6,624 صفًا من 2,066 طلبًا و873 تذكرة. ما الذي حدث؟

تدريب

تمرين Python

ابنِ جدول المبيعات

ابنِ sales: كل صف من items، مع أعمدة طلبه من orders، وname الخاص بالمنتج بعد إعادة تسميته إلى product، وcountry الخاص بالعميل. استخدم عمليات دمج يسرى مع validate="many_to_one"، ثم احذف بنود الطلبات الملغاة. بعد ذلك اجعل revenue_by_country إجمالي الإيراد لكل دولة على هيئة Series، مرتّبة من الأعلى إلى الأدنى.

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

import pandas as pd

items = pd.read_csv("order_items.csv")
orders = pd.read_csv("orders.csv", parse_dates=["order_date"])
products = pd.read_csv("products.csv")
customers = pd.read_csv("customers.csv")
items["revenue"] = items["quantity"] * items["unit_price"] * (1 - items["discount"])

# 1. sales: every item line with its order's columns, the product name (as "product"),
#    and the customer's country. Use left merges with validate="many_to_one".
#    Then drop the lines of cancelled orders.
sales = None

# 2. Revenue by country, a Series sorted from highest to lowest
revenue_by_country = None
جارٍ تحميل المحرّر…

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

(لا مخرجات)

الفحوص

  • في sales أعمدة الإيراد والطلب والمنتج والدولة
  • لم تبقَ أي طلبات ملغاة
  • كل بند من الطلبات غير الملغاة موجود مرة واحدة بالضبط
  • إجمالي الإيراد 446,666.32
  • revenue_by_country مرتّبة، والإمارات أولًا بـ 74,531.80