الوحدة 4 · التحليل: التجميع والدمج وإعادة التشكيل
دمج الجداول باستخدام merge
20 دقراءة 3 دتمرين Python
الدرس في لمحة
ادمج الجداول باستخدام merge، واختر نوع الربط المناسب، واحمِ نفسك من الصفوف المكرّرة باستخدام validate= وعدّ الصفوف، واستخدم indicator=True لتجد السجلات التي لا مطابق لها.
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
سؤال رئيس النمو يحتاج أعمدة من أربعة جداول. الإيراد موجود في order_items. أما هل أُلغي الطلب ومتى قُدّم فمعلومتان في orders. وفئة المنتج في products، ودولة العميل في customers. في جدول البيانات كنت ستكتب صيغ بحث (lookup)؛ وفي pandas تستخدم الدمج (merge). عمليات الدمج هي المكان الذي تحدث فيه معظم الأخطاء الصامتة في التحليل، لذا يقضي هذا الدرس في فحص عملية الدمج وقتًا يساوي ما يقضيه في كتابتها.
أول عملية دمج
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: الطلبات الملغاة ليست إيرادًا.
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 ما يحدث للمفاتيح الموجودة في طرف واحد فقط:
يحتفظ inner (القيمة الافتراضية) فقط بالمفاتيح الموجودة في الطرفين. ويحتفظ left بكل صف أيسر، ويملأ الأعمدة اليمنى بـ NaN حيث لا يوجد نظير. وright صورته المعكوسة، وouter يحتفظ بكل شيء. ولإضافة معلومات إلى جدول وقائع، مثل الطلبات أو البنود أو الجلسات، استخدم how="left" مع جدول الوقائع على اليسار: لن تفقد أبدًا واقعة لأن جدول البحث ناقص، وقيم NaN تبيّن لك بالضبط أين النقص.
العثور على ما لم يتطابق
تضيف indicator=True عمودًا اسمه _merge يسجّل من أين جاء كل صف:
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) للتمييز بينهما، وتستطيع اختيارهما:
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) يبيّن أيّ الصفوف وجدت نظيرًا.