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

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

الترتيب والتقسيم إلى فئات

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

الدرس في لمحة

رتّب العملاء بحسب الإنفاق، وقِس مدى تركّز الإيراد، وحوّل رقمًا متصلًا إلى شرائح باستخدام cut (حدود ثابتة) و qcut (مجموعات متساوية الحجم).

  1. 1صف لكل عميل
  2. 2الترتيب
  3. 3cut: فئات بحدود تختارها أنت
  4. 4qcut: مجموعات متساوية الحجم

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

“مَن أفضل عملائنا؟” سؤال يطرحه كل فريق مبيعات أو تسويق، والإجابة الصادقة لها جزءان: قائمة مرتّبة، وكم يهم أعلى تلك القائمة. في Cartwheel الإجابة لافتة، وهي تشكّل طريقة قراءتك لكل رقم إيراد، ومنها قفزة الربع الرابع.

صف لكل عميل

ابدأ بجدول على مستوى العميل: صف لكل عميل، مع الطلبات والإنفاق، مبني بالتجميع (groupby) من الدرس 4.1.

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"])
orders = orders.merge(items.groupby("order_id", as_index=False)["revenue"].sum(), on="order_id", validate="one_to_one")
orders = orders[orders["status"] != "cancelled"]

cust = orders.groupby("customer_id").agg(orders=("order_id", "count"), spend=("revenue", "sum"))
print(len(cust), "customers with a non-cancelled order")
print(cust["spend"].describe().round(0))

503 عملاء. بلغ الوسيط 551 دولارًا، والمتوسط 888، والأعلى 5,370. حين يقع المتوسط فوق الوسيط بكثير، فهناك قيم كبيرة قليلة تسحبه إلى الأعلى؛ ويعود الدرس 5.1 إلى ما يعنيه ذلك عند الإبلاغ عن المتوسطات.

الترتيب

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"])
orders = orders.merge(items.groupby("order_id", as_index=False)["revenue"].sum(), on="order_id", validate="one_to_one")
orders = orders[orders["status"] != "cancelled"]
cust = orders.groupby("customer_id").agg(orders=("order_id", "count"), spend=("revenue", "sum"))

cust["rank"] = cust["spend"].rank(ascending=False, method="min")
cust["percentile"] = cust["spend"].rank(pct=True)
print(cust.sort_values("rank").head(5).round(2))

top20 = cust.nlargest(int(len(cust) * 0.2), "spend")
print(len(top20), "customers =", round(top20["spend"].sum() / cust["spend"].sum(), 3), "of revenue")

تعطي rank كل صف مرتبته، و1 لأعلى إنفاق بسبب ascending=False. والتعادل هو حيث تختلف الطرق: "min" تعطي العملاء المتعادلين المرتبة نفسها وتقفز فوق الأرقام التالية، مثل جدول الدوري، و"dense" لا تقفز أبدًا، و"first" تكسر التعادل بحسب ترتيب الصفوف، والقيمة الافتراضية "average" تعطيهم متوسط المراتب. وتحوّل pct=True المراتب إلى مئينات بين 0 و1، تصلح للمقارنة بين مجموعات مختلفة الأحجام.

السطر الأخير هو رقم التركّز: أعلى 100 عميل، أي خُمس القاعدة، يحقّقون 55.5% من الإيراد. هذا أقل تطرّفًا من قاعدة 80/20 المعروفة في الكتب، لكنه ما زال يعني أن خسارة حفنة من العملاء الكبار ستؤلم أكثر من خسارة مئة عميل صغير.

الترتيب داخل المجموعات

مديرو الدول يريدون أفضل عملائهم هم، لا قائمة عالمية تزاحم فيها الأسواق الكبرى الجميع. تعمل rank على عمود مجمَّع أيضًا، فتبدأ من 1 من جديد داخل كل مجموعة:

Python
import pandas as pd

items = pd.read_csv("order_items.csv")
orders = pd.read_csv("orders.csv", parse_dates=["order_date"])
customers = pd.read_csv("customers.csv")
items["revenue"] = items["quantity"] * items["unit_price"] * (1 - items["discount"])
orders = orders.merge(items.groupby("order_id", as_index=False)["revenue"].sum(), on="order_id", validate="one_to_one")
orders = orders[orders["status"] != "cancelled"]
cust = orders.groupby("customer_id", as_index=False).agg(spend=("revenue", "sum"))
cust = cust.merge(customers[["customer_id", "country"]], on="customer_id", validate="one_to_one")

cust["rank_in_country"] = cust.groupby("country")["spend"].rank(ascending=False, method="first")
print(cust[cust["rank_in_country"] == 1].sort_values("spend", ascending=False).round(0))

التصفية على rank_in_country == 1 تحتفظ بأعلى منفق في كل دولة، ثمانية صفوف إجمالًا؛ والاحتفاظ بالمراتب 3 أو أقل سيعطي قائمة أعلى ثلاثة لكل دولة. تضمن method="first" صفًا واحدًا بالضبط لكل مرتبة حتى حين يتعادل عميلان، وهذا ما تريده لقائمة ثابتة الطول. ونمط “رتّب داخل المجموعة ثم صفِّ” هذا يجيب عن عائلة كاملة من الأسئلة: المنتج الأكثر مبيعًا في كل فئة، وأول طلب لكل عميل، وآخر تذكرة لكل طلب.

cut: فئات بحدود تختارها أنت

يريد التسويق نطاقات إنفاق يستطيع تسميتها في حملة. أنت تختار الحدود:

Python
import numpy as np
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"])
orders = orders.merge(items.groupby("order_id", as_index=False)["revenue"].sum(), on="order_id", validate="one_to_one")
orders = orders[orders["status"] != "cancelled"]
cust = orders.groupby("customer_id").agg(orders=("order_id", "count"), spend=("revenue", "sum"))

cust["band"] = pd.cut(cust["spend"], bins=[0, 250, 500, 1000, 2000, np.inf],
                      labels=["<250", "250-500", "500-1k", "1k-2k", "2k+"])
print(cust.groupby("band", observed=True)["spend"].agg(["count", "sum"]).round(0))

كل زوج من الحدود يعرّف فئة، مغلقة من اليمين افتراضيًا: (250, 500] تشمل 500 ولا تشمل 250. والعملاء الـ 57 في النطاق 2k+، أي 11% من القاعدة، جلبوا 173,407 دولارات، أكثر من أي نطاق آخر. تعيد cut عمودًا فئويًا (categorical) تحافظ فئاته على ترتيبك، فيسرد الجدول النطاقات من الأصغر إلى الأكبر لا أبجديًا.

qcut: مجموعات متساوية الحجم

حين تريد مجموعات متساوية الحجم بدلًا من حدود ثابتة، تقطع qcut عند المئينات (quantiles):

الصف العلوي: تقسم cut محور الإنفاق عند القيم الثابتة 250 و 500 و 1000 و 2000؛ والفئات تحمل أعدادًا مختلفة من العملاء. الصف السفلي: تقسم qcut عند الرُّبيعات 210 و 551 و 1236، فتحمل كل فئة من الفئات الأربع نحو 126 عميلًا. cut 143 92 118 93 57 عميلًا qcut 126 126 125 126 210 551 1236 حدود الرُّبيعات
تستخدم cut حدودًا ثابتة؛ وتستخدم qcut أعدادًا متساوية
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"])
orders = orders.merge(items.groupby("order_id", as_index=False)["revenue"].sum(), on="order_id", validate="one_to_one")
orders = orders[orders["status"] != "cancelled"]
cust = orders.groupby("customer_id").agg(orders=("order_id", "count"), spend=("revenue", "sum"))

cust["quartile"] = pd.qcut(cust["spend"], q=4, labels=["Q1 low", "Q2", "Q3", "Q4 high"])
share = cust.groupby("quartile", observed=True)["spend"].sum() / cust["spend"].sum()
print(share.round(3))
print(pd.qcut(cust["orders"], q=4, duplicates="drop").value_counts().sort_index())

الرُّبيع الأعلى من العملاء يجلب 63.2% من الإيراد، والرُّبيع الأدنى 2.8%. ويبيّن السطر الأخير حدود qcut: ثلث العملاء قدّموا طلبًا واحدًا بالضبط، فيكون أدنى حدّين للرُّبيعات كلاهما 1، وتُطلق qcut العادية الخطأ ValueError: Bin edges must be unique. تدمج duplicates="drop" الحدود المكرّرة، فتبقى مجموعات أقل وغير متساوية. وللأعداد التي فيها تعادلات كثيرة، تكون cut مع حدود تختارها بيدك أوضح عادةً.

في التمرين تقسّم العملاء إلى شرائح بنفسك. وبهذا تكتمل عُدّة التحليل؛ ويوجّهها القسم 5 نحو أسئلة حقيقية، بدءًا بما يستطيع المتوسط أن يخبرك به وما لا يستطيع.

أهم النقاط

  • rank(ascending=False, method="min") ترقّم الصفوف بحسب القيمة؛ وmethod تحدّد طريقة ترقيم المتعادلين، وpct=True تعطي ترتيبًا مئينيًا.
  • تضع pd.cut القيم في فئات بحدود تختارها أنت؛ استخدم np.inf حدًّا أخيرًا كي لا تقع أي قيمة خارجها.
  • تصنع pd.qcut فئات فيها أعداد متساوية (تقريبًا) من الصفوف، مثل الرُّبيعات (quartiles)؛ وحين تتعادل قيم كثيرة، مرّر duplicates="drop".
  • كلتاهما تعيد عمودًا فئويًا (categorical)، فجمّع بحسبه مع observed=True ورتّب بحسب ترتيب الفئات لا أبجديًا.
  • رقم التركّز، مثل “أعلى 20% من العملاء يجلبون 55% من الإيراد”، كثيرًا ما يكون أنفع رقم مفرد تستطيع تقديمه للعمل.

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

تحقّق من فهمك

3 أسئلة

السؤال 1 من 3

تترك pd.cut(spend, bins=[0, 250, 500, 1000]) بعض العملاء بقيمة NaN. مَن هم؟

السؤال 2 من 3

تريد أربع مجموعات من العملاء في كل منها العدد نفسه من العملاء. أيّ أداة تناسب؟

السؤال 3 من 3

يتعادل عميلان على المركز الثاني بحسب الإنفاق. مع rank(ascending=False, method="min")، ما الترتيب الذي يحصلان عليه هما والعميل التالي؟

تدريب

تمرين Python

قسّم عملاء Cartwheel إلى شرائح

ابنِ cust، صفًا لكل customer_id، مع orders (عدد الطلبات) وspend (إجمالي الإيراد). أضِف rank (1 لأعلى منفق، والمتعادلون يتشاركون أدنى مرتبة)، وband، المصنوع بـ pd.cut بالحدود 0 و250 و500 و1000 و2000 واللانهاية والتسميات "<250" و"250-500" و"500-1k" و"1k-2k" و"2k+". وأخيرًا اجعل top10_share حصة الإيراد الكلي التي تأتي من أعلى 50 عميلًا إنفاقًا (نحو أعلى 10%)، مقرّبة إلى 3 منازل عشرية.

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

import numpy as np
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"])
orders = orders.merge(items.groupby("order_id", as_index=False)["revenue"].sum(), on="order_id", validate="one_to_one")
orders = orders[orders["status"] != "cancelled"]

# 1. cust: one row per customer_id with columns orders (count) and spend (sum of revenue)
cust = None

# 2. Column "rank": 1 for the highest spender, ties share the lowest rank ("min")

# 3. Column "band": spend bands with edges 0, 250, 500, 1000, 2000, infinity
#    and labels "<250", "250-500", "500-1k", "1k-2k", "2k+"

# 4. Share of all revenue from the top 10% of customers by spend (top 50), rounded to 3 decimals
top10_share = None
جارٍ تحميل المحرّر…

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

(لا مخرجات)

الفحوص

  • في cust 503 عملاء مع الطلبات والإنفاق
  • يبدأ rank من 1، والعميل 331 في المرتبة الثانية
  • band بلا فجوات وبالأعداد المتوقّعة لكل نطاق
  • top10_share هي حصة أعلى 50 عميلًا من الإيراد