EduFlow

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

مقاييس التصنيف أبعد من الـ Accuracy

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

الدرس في لمحة

اقرأ مصفوفة الالتباس، واحسب الـ precision والـ recall والـ F1 والـ ROC AUC باستخدام scikit-learn، واختر المقياس الذي يطابق كلفة كل نوع من الأخطاء على الشركة.

  1. 1أربع نتائج
  2. 2الـ Precision والـ Recall والـ F1
  3. 3الـ ROC AUC: تقييم الترتيب
  4. 4اختيار المقياس

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

يحقق كلٌّ من الانحدار اللوجستي وقاعدة الـ 80 يومًا accuracy بقيمة 0.72 على بيانات اختبار الانقطاع. فهل هما جيدان بالقدر نفسه؟ الـ accuracy تقول نعم. وفريق التسويق سيخالف ذلك فور أن يرى من يُعلِّمه كلٌّ منهما. فالـ accuracy تمزج خطأين مختلفين جدًا في رقم واحد، وترمي الترتيب الذي جعل قائمة الاتصالات مفيدة. هذا الدرس يفكّكهما.

أربع نتائج

كل تنبؤ في مسألة نعم/لا يقع في واحدة من أربع خانات. سمِّ المنقطعين إيجابيات (positives) (فهم الفئة التي تحاول العثور عليها).

  • إيجابي صحيح (TP): تنبأنا بالانقطاع، وانقطع فعلًا. وصل التسويق إلى الشخص الصحيح.
  • إيجابي كاذب (FP): تنبأنا بالانقطاع، وبقي. عرض أُهدر على عميل وفيّ.
  • سلبي كاذب (FN): تنبأنا بالبقاء، وانقطع. عميل خسرناه دون محاولة.
  • سلبي صحيح (TN): تنبأنا بالبقاء، وبقي. تُرك وشأنه عن صواب.
Python
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import confusion_matrix
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

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"])
model = make_pipeline(StandardScaler(), LogisticRegression()).fit(X_train, y_train)
pred = model.predict(X_test)
rule = (X_test["days_since_last_order"] > 80).astype(int)

print("model:\n", confusion_matrix(y_test, pred))
print("rule:\n", confusion_matrix(y_test, rule))

يرتّب scikit-learn المصفوفة بحيث تكون الفئات الفعلية صفوفًا والفئات المتنبأ بها أعمدة، وكلاهما بترتيب تصاعدي (0 ثم 1). لذا تُقرأ مصفوفة النموذج [[35, 10], [18, 37]] هكذا:

شبكة اثنين في اثنين. بقي فعلًا وتُنبّئ ببقائه: 35 سلبيًا صحيحًا. بقي فعلًا وتُنبّئ بانقطاعه: 10 إيجابيات كاذبة. انقطع فعلًا وتُنبّئ ببقائه: 18 سلبيًا كاذبًا. انقطع فعلًا وتُنبّئ بانقطاعه: 37 إيجابيًا صحيحًا. المتنبأ به بقاء (0) انقطاع (1) الفعلي بقي (0) انقطع (1) 35 سلبيات صحيحة 10 إيجابيات كاذبة 18 سلبيات كاذبة 37 إيجابيات صحيحة
مصفوفة الالتباس للانحدار اللوجستي على 100 عميل اختبار

مصفوفة القاعدة هي [[31, 14], [14, 41]]. التنبؤات الصحيحة نفسها، 72، لكن الأخطاء مختلفة: النموذج يهدر عروضًا أقل (10 مقابل 14) ويفوّت منقطعين أكثر (18 مقابل 14). أيّهما أفضل يعتمد على كلفة كل خطأ، والـ accuracy لا تستطيع أن تخبرك بذلك.

الـ Precision والـ Recall والـ F1

نسبتان تحوّلان المصفوفة إلى أسئلة يطرحها المدير:

  • الـ Precision = TP / (TP + FP): من بين العملاء الذين علّمناهم، كم منهم كان راحلًا فعلًا؟ النموذج: 37 / 47 = 0.79. القاعدة: 0.75.
  • الـ Recall = TP / (TP + FN): من بين العملاء الذين رحلوا، كم منهم علّمنا؟ النموذج: 37 / 55 = 0.67. القاعدة: 0.75.

بينهما توتّر مدمج. علّم أشخاصًا أكثر فترتفع الـ recall بينما تنخفض الـ precision عادةً؛ وعلّم عددًا أقل فيحدث العكس. أما الـ F1 فهو المتوسط التوافقي للاثنين، 2 * P * R / (P + R). ولا يكون مرتفعًا إلا إذا كانا مرتفعين معًا، فلا يستطيع النموذج التلاعب به بتعظيم أحدهما. استخدمه حين تحتاج إلى رقم واحد ويكون للخطأين وزن متقارب.

Python
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report, roc_auc_score
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

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"])
model = make_pipeline(StandardScaler(), LogisticRegression()).fit(X_train, y_train)

print(classification_report(y_test, model.predict(X_test), target_names=["stayed", "churned"]))
print("model ROC AUC:", round(roc_auc_score(y_test, model.predict_proba(X_test)[:, 1]), 3))
print("rule  ROC AUC:", round(roc_auc_score(y_test, X_test["days_since_last_order"]), 3))

تطبع classification_report الـ precision والـ recall والـ F1 لكل فئة، مع الـ support (عدد الحالات الحقيقية). اقرأ صف “churned” للسؤال العملي؛ أما صف “stayed” فهو التحليل نفسه مع تبادل الأدوار.

الـ ROC AUC: تقييم الترتيب

تعتمد الـ precision والـ recall على الحدّ الفاصل 0.5. أما الـ ROC AUC فلا تستخدم حدًّا فاصلًا إطلاقًا. إنها تسأل: إذا اخترت منقطعًا واحدًا وباقيًا واحدًا عشوائيًا، فكم مرة يعطي النموذج المنقطعَ الدرجةَ الأعلى؟ القيمة 0.5 تعني أن الدرجات ليست أفضل من رمي عملة؛ و1.0 تعني أن كل منقطع يتقدّم على كل باقٍ.

تأخذ roc_auc_score درجات، لا labels: مرّر predict_proba(...)[:, 1]. وأي درجة ترتّب العملاء تصلح، فيمكنك تقييم القاعدة بالقيمة الخام لـ days_since_last_order. تحصل القاعدة على 0.77؛ ويحصل الانحدار اللوجستي على 0.81. هذه الفجوة هي الفرق الحقيقي بينهما، وقد أخفاه تعادل الـ accuracy: ترتيب النموذج أفضل بشكل ملموس، ولهذا كانت قائمة العشرة الأعلى خطرًا في درس الانحدار اللوجستي دقيقة إلى هذا الحد.

اختيار المقياس

ابدأ من كلفة كل خطأ، ثم اختر:

الموقف راقب السبب
تفويت حالة مكلف (فحص السرطان، الاحتيال) الـ Recall كل سلبي كاذب هو الخطأ المكلف
التصرّف بشأن حالة مكلف (عروض غالية، حظر حسابات) الـ Precision كل إيجابي كاذب يهدر مالًا أو ثقة
ستتصرّف بشأن أفضل N بحسب الدرجة الـ ROC AUC، أو نسبة الإصابة في أفضل N جودة الترتيب هي ما يهم
الخطآن مهمان، والمطلوب رقم واحد الـ F1 يعاقب إهمال أيٍّ منهما

تحرّكت الـ precision والـ recall في اتجاهين متعاكسين بين النموذج والقاعدة لأنهما يستخدمان فعليًا حدّين فاصلين مختلفين. ويمكنك أن تحرّك هذا الحدّ بنفسك، عن قصد، وهذا موضوع الدرس القادم.

أهم النقاط

  • تعدّ مصفوفة الالتباس (confusion matrix) السلبيات الصحيحة والإيجابيات الكاذبة والسلبيات الكاذبة والإيجابيات الصحيحة؛ وفي scikit-learn تمثّل الصفوفُ الفئاتِ الفعلية والأعمدةُ الفئاتِ المتنبأ بها.
  • الـ precision هي نسبة الحالات المُعلَّمة التي هي حقيقية؛ والـ recall هي نسبة الحالات الحقيقية التي عُلِّمت.
  • الـ F1 هو المتوسط التوافقي للـ precision والـ recall، ومفيد حين تحتاج إلى رقم واحد يعاقب إهمال أيٍّ منهما.
  • يقيس الـ ROC AUC جودة الترتيب عبر كل العتبات: أي احتمال أن ينال مثال إيجابي عشوائي درجة أعلى من مثال سلبي عشوائي.
  • تحقّق دائمًا من الفئة الإيجابية: في load_breast_cancer الورم الخبيث هو 0، فالـ recall للخبيث يحتاج إلى pos_label=0.

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

تحقّق من فهمك

4 أسئلة

السؤال 1 من 4

تُعيد confusion_matrix(y_test, pred) القيمة [[35, 10], [18, 37]] لبيانات الانقطاع (1 = منقطع). كم عميلًا منقطعًا فات النموذج؟

السؤال 2 من 4

كل عرض استرجاع يكلّف 40 دولارًا، لذا لا يريد فريق التسويق التواصل إلا مع العملاء المرجّح جدًا انقطاعهم. أيّ مقياس يجب أن تراقبه عن كثب؟

السؤال 3 من 4

للنموذج A قيمة ROC AUC تساوي 0.81 وللنموذج B قيمة 0.77، لكن كليهما يحقق accuracy بقيمة 0.72 عند العتبة الافتراضية. ماذا يخبرك ذلك؟

السؤال 4 من 4

نموذج على load_breast_cancer يُبلغ أن recall_score(y_test, pred) = 0.99. لماذا قد يضلّل هذا الرقمُ الطبيبَ؟

تدريب

تمرين Python

مقاييس للفئة التي تهمّ

شجرة بعمق 2 تفحص أورام الثدي. في هذه البيانات 0 يعني خبيثًا و1 يعني حميدًا. قِس النموذج من وجهة نظر الطبيب.

  1. cm: مصفوفة الالتباس لـ y_test مقابل pred.
  2. recall_malignant وprecision_malignant: الـ recall والـ precision مع اعتبار الخبيث الفئة الإيجابية.
  3. auc: قيمة الـ ROC AUC من الاحتمالات التي يتنبأ بها النموذج.
from sklearn.datasets import load_breast_cancer
from sklearn.metrics import confusion_matrix, precision_score, recall_score, roc_auc_score
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier

data = load_breast_cancer()
print(data.target_names)          # index 0 = malignant, 1 = benign
X, y = load_breast_cancer(return_X_y=True, as_frame=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42, stratify=y)

model = DecisionTreeClassifier(max_depth=2, random_state=42).fit(X_train, y_train)
pred = model.predict(X_test)

# 1. Confusion matrix (rows = actual, columns = predicted)
cm = None

# 2. Recall and precision for MALIGNANT tumours
recall_malignant = recall_score(y_test, pred)
precision_malignant = precision_score(y_test, pred)

# 3. ROC AUC of the model's scores
auc = None
جارٍ تحميل المحرّر…

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

(لا مخرجات)

الفحوص

  • cm هي مصفوفة الالتباس
  • recall_malignant تعتبر الخبيث (0) الفئة الإيجابية
  • precision_malignant تعتبر الخبيث (0) الفئة الإيجابية
  • auc محسوبة من الاحتمالات