EduFlow

الوحدة 3 · التصنيف

أشجار القرار

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

الدرس في لمحة

ازرع أشجار قرار على بيانات الانقطاع، واقرأها كقواعد if/else بسيطة، وتحكّم في حجمها بـ max_depth وmin_samples_leaf، واعرف متى لا يمكن الوثوق باحتمالاتها.

  1. 1عشرون سؤالًا تختارها البيانات
  2. 2كيف تتنبأ الشجرة بالاحتمالات
  3. 3النمو أكثر من اللازم، وكيف توقفه

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

اسأل قائد فريق الدعم كيف يكتشف العملاء الذين أوشكوا على الرحيل، وستسمع شيئًا مثل: “إذا لم يطلبوا شيئًا منذ أشهر، فقد رحلوا. وإذا كانوا حديثين لكنهم لم يشتروا إلا مرة أو مرتين، فالأمر متأرجح. أما الزبائن الدائمون الذين طلبوا مؤخرًا فلا خوف عليهم”. هذه شجرة قرار (decision tree). وفي هذا الدرس تنمّي واحدة من البيانات وتتحقق إن كانت توافقه.

عشرون سؤالًا تختارها البيانات

تتنبأ شجرة القرار بطرح أسئلة نعم/لا عن feature واحد في كل مرة. ولبنائها، تنظر الخوارزمية في كل feature وكل عتبة ممكنة، وتختار السؤال الذي يقسم عملاء التدريب إلى أنقى مجموعتين: في الحالة المثالية، كل المنقطعين في جهة وكل الباقين في الأخرى. وتُقاس النقاوة افتراضيًا بـ Gini impurity (0 لمجموعة من فئة واحدة فقط، و0.5 لخليط 50/50). ثم تكرّر البحث داخل كل مجموعة، وهكذا نزولًا.

Python
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier, export_text

churn = pd.read_csv("churn.csv")
features = ["tenure_days", "orders", "total_spent", "avg_order_value",
            "days_since_last_order", "used_coupon", "support_tickets", "mobile_share"]
X_train, X_test, y_train, y_test = train_test_split(
    churn[features], churn["churned"], test_size=0.25, random_state=42,
    stratify=churn["churned"])

tree = DecisionTreeClassifier(max_depth=2, random_state=42).fit(X_train, y_train)
print(export_text(tree, feature_names=features, show_weights=True))

تطبع export_text القواعد، ويضيف show_weights=True عدد عملاء التدريب من كل فئة (بقي، انقطع) الذين وصلوا إلى كل ورقة.

سؤال الجذر: هل الأيام منذ آخر طلب 236.5 على الأكثر. إذا نعم، اسأل هل الطلبات 3.5 على الأكثر: نعم تعطي ورقة فيها 60 بقوا و67 انقطعوا، ولا تعطي 60 بقوا و13 انقطعوا. وإذا لا، اسأل هل مدة العضوية 637.5 على الأكثر: نعم تعطي 11 بقوا و84 انقطعوا، ولا تعطي 2 بقيا و1 انقطع. days_since_last ≤ 236.5? نعم لا orders ≤ 3.5? tenure_days ≤ 637.5? 60 بقوا / 67 انقطعوا متأرجح 60 بقوا / 13 انقطعوا يبقى 11 بقوا / 84 انقطعوا ينقطع 2 بقيا / 1 انقطع 3 عملاء فقط!
شجرة الانقطاع بعمق 2

ثلاث أوراق تطابق كلام قائد فريق الدعم حرفًا بحرف أو تكاد. العملاء الصامتون منذ زمن طويل ينقطعون (84 من 95). والزبائن الدائمون الحديثون بأربعة طلبات أو أكثر يبقون (60 من 73). والعملاء الحديثون قليلو الطلبات متأرجحون (67 من 127 ينقطعون). واكتشفت الشجرة أيضًا تفاعلًا (interaction) لم يضطر أحد إلى كتابته: عدد الطلبات مهم، لكن بين العملاء الحديثين فقط.

أما الورقة الرابعة فتحذير. لقد أنفقت الشجرة سؤالًا كاملًا لعزل ثلاثة عملاء ذوي عضوية طويلة جدًا. هذا هو البحث الجشع (greedy) أثناء عمله: يختار التقسيم الذي يساعد أكثر في اللحظة الحالية، حتى لو جاءت المساعدة من مجموعة أصغر من أن تعني شيئًا.

كيف تتنبأ الشجرة بالاحتمالات

العميل الذي يقع في ورقة يحصل على حصص الفئات في تلك الورقة كاحتمالات. قع في ورقة “الزبائن الدائمين” وستُعيد predict_proba نحو [0.82, 0.18]. لذا فإن شجرة بعمق 2 لا تستطيع أبدًا أن تُخرج إلا أربعة احتمالات مختلفة، واحدًا لكل ورقة. هذا خشن، لكنه صادق. وتبدأ المشكلة حين لا تكون للشجرة حدود.

النمو أكثر من اللازم، وكيف توقفه

تواصل الشجرة غير المقيّدة التقسيم حتى تصبح كل ورقة نقية. على بيانات الانقطاع، هذا يعني 64 ورقة، بعمق 11 مستوى، ودقة تدريب 100%. كل ورقة نقية، فكل احتمال يساوي 0 أو 1 بالضبط: الشجرة تدّعي يقينًا تامًا بشأن كل عميل اختبار بينما لا تصيب إلا في 72% من الحالات. ومع ترتيب العملاء بمثل هذه الاحتمالات، يتعادل معظمهم، وتتحوّل قائمة الاتصالات من الدرس السابق إلى يانصيب.

اثنان من الـ hyperparameters يتوليان معظم مهمة إبقاء الشجرة صادقة:

  • max_depth يحدّد أقصى عدد من الأسئلة المتتالية التي تستطيع الشجرة طرحها.
  • min_samples_leaf يرفض أي تقسيم يترك في ورقة عددًا من عملاء التدريب أقل من هذه القيمة. وهو يستهدف بالضبط مشكلة الورقة الرابعة أعلاه، ويتيح للشجرة أن تتعمّق حيث البيانات وفيرة وأن تبقى ضحلة حيث ليست كذلك.
Python
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier

churn = pd.read_csv("churn.csv")
features = ["tenure_days", "orders", "total_spent", "avg_order_value",
            "days_since_last_order", "used_coupon", "support_tickets", "mobile_share"]
X_train, X_test, y_train, y_test = train_test_split(
    churn[features], churn["churned"], test_size=0.25, random_state=42,
    stratify=churn["churned"])

for settings in [{}, {"max_depth": 3}, {"min_samples_leaf": 20}]:
    t = DecisionTreeClassifier(random_state=42, **settings).fit(X_train, y_train)
    distinct = len(set(t.predict_proba(X_test)[:, 1]))
    print(f"{str(settings):24} leaves {t.get_n_leaves():3}  train {t.score(X_train, y_train):.2f}  "
          f"test {t.score(X_test, y_test):.2f}  distinct probabilities {distinct}")

t = DecisionTreeClassifier(min_samples_leaf=20, random_state=42).fit(X_train, y_train)
print(pd.Series(t.feature_importances_, index=features).sort_values(ascending=False).round(2))

يقول feature_importances_ كم خفّض كل feature من عدم النقاوة عبر كل تقسيماته، بعد تطبيع القيم ليكون مجموعها 1. تستحوذ حداثة آخر طلب على أكثر من النصف، ويأتي عدد الطلبات ثانيًا؛ وتحصل القسائم وتذاكر الدعم على صفر لأن الشجرة لم تستخدمها قط. عامِل هذه الأرقام كوصف لهذه الشجرة، لا للعالم: فالـ features التي لم تحتجها الشجرة قد تظل مفيدة، والدرس القادم يبيّن طريقة أجدر بالثقة لقياس الأهمية.

وعدم الاستقرار هذا فرصة أيضًا. فإذا كانت الشجرة الواحدة مليئة بالضجيج، فخذ متوسط أشجار كثيرة نمت على بيانات مختلفة قليلًا، وسيلغي الضجيج بعضه بعضًا. هذه هي الفكرة وراء الغابات العشوائية والـ gradient boosting، موضوع الدرس القادم.

أهم النقاط

  • تقسم شجرة القرار البيانات بسؤال نعم/لا واحد في كل مرة، وتختار في كل خطوة السؤال الذي يجعل المجموعات أنقى ما يمكن.
  • الشجرة المدرَّبة مجموعة من القواعد المقروءة؛ وexport_text تطبعها.
  • الأشجار غير المقيّدة تنمو حتى تصبح كل ورقة نقية، فتحفظ بيانات التدريب وتنتج احتمالات مفرطة الثقة بقيمة 0 أو 1.
  • يحدّ max_depth وmin_samples_leaf من النمو؛ ويمنع min_samples_leaf الشجرة من إنفاق تقسيم على حفنة من العملاء.
  • لا تحتاج الأشجار إلى توحيد مقاييس الـ features وتلتقط التفاعلات تلقائيًا، لكن الشجرة الواحدة غير مستقرة: تغييرات صغيرة في البيانات قد تعيد تشكيلها.

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

تحقّق من فهمك

3 أسئلة

السؤال 1 من 3

ورقة في شجرة انقطاع تضم 60 عميلًا بقوا و13 انقطعوا. ماذا تُعيد predict_proba لعميل يقع فيها؟

السؤال 2 من 3

تنبؤات شجرة غير محدودة على بيانات الاختبار لا تحتوي إلا احتمالين مختلفين، 0.0 و1.0. لماذا تُعدّ هذه مشكلة؟

السؤال 3 من 3

أيّ هذه ميزة حقيقية للأشجار على الانحدار اللوجستي في بيانات الانقطاع؟

تدريب

تمرين Python

حوّل الشجرة إلى قواعد

الشجرة كومة من عبارات if/else. أثبت ذلك.

  1. شغّل الكود لطباعة الشجرة بعمق 2. أكمل rule_predict(row) بحيث تُعيد بالضبط ما تتنبأ به الشجرة (1 = انقطاع، 0 = بقاء) لأي صف عميل. تقارنها الفحوص بـ tree.predict على كل صفوف التدريب والاختبار.
  2. leafy: نموذج DecisionTreeClassifier(min_samples_leaf=20, random_state=42) مدرَّب على بيانات التدريب.
  3. top_feature: اسم الـ feature صاحب أكبر قيمة في feature_importances_ داخل leafy.

ملفات البيانات: churn.csv

import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier, export_text

churn = pd.read_csv("churn.csv")
features = ["tenure_days", "orders", "total_spent", "avg_order_value",
            "days_since_last_order", "used_coupon", "support_tickets", "mobile_share"]
X_train, X_test, y_train, y_test = train_test_split(
    churn[features], churn["churned"], test_size=0.25, random_state=42,
    stratify=churn["churned"])

tree = DecisionTreeClassifier(max_depth=2, random_state=42).fit(X_train, y_train)
print(export_text(tree, feature_names=features))

# 1. Rewrite the printed tree as plain Python rules. Return 1 for churn, 0 for stay.
def rule_predict(row):
    if row["days_since_last_order"] <= 236.5:
        return 1
    return 1

rule_preds = [rule_predict(row) for _, row in X_test.iterrows()]

# 2. A tree limited by min_samples_leaf=20 (random_state=42), fitted on the training data
leafy = None

# 3. The name of the feature with the highest importance in `leafy`
top_feature = None
جارٍ تحميل المحرّر…

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

(لا مخرجات)

الفحوص

  • rule_predict تطابق الشجرة على كل عملاء الاختبار
  • rule_predict تطابق الشجرة على كل عملاء التدريب
  • leafy مدرَّبة بـ min_samples_leaf=20
  • top_feature يسمّي الـ feature الأهم