רגרסיה לוגיסטית, Random Forest, XGBoost, LightGBM ו-CatBoost חוזים אילו לקוחות יעזבו את הבנק (Exited = 1). כל המדדים מחושבים על 20% נתוני המבחן, שהמודלים לא ראו באימון.
נמדד על 2,000 לקוחות המבחן · סף החלטה 0.5
ערך גבוה יותר = מודל טוב יותר
ככל שהעקומה קרובה יותר לפינה השמאלית-עליונה, המודל טוב יותר
המשתנה החזוי הוא ההסתברות לעזיבה (0 עד 1), והערך בפועל הוא 0 או 1. כאן הוא מוצג כמו מודל רגרסיה: R² ו-MAE מחושבים על ההסתברות, והשארית היא בפועל − חזוי. אחרי הכוונון כל המודלים נותנים משקל גבוה יותר ללקוחות שעוזבים (class_weight / scale_pos_weight), ולכן ההסתברויות שהם חוזים גבוהות מהשיעור האמיתי: הנקודות נמצאות מתחת לקו המקווקו, וה-R² של הרגרסיה הלוגיסטית אפילו שלילי. זה המחיר של Recall גבוה יותר, והוא לא משפיע על AUC.
לקוחות המבחן חולקו ל-10 קבוצות שוות לפי ההסתברות החזויה
נקודה על הקו המקווקו פירושה שהמודל צדק: למשל, בקבוצה שחזה לה 40% עזיבה, באמת עזבו 40%.
כמה לקוחות יש בכל טווח של שגיאה
ככל שיותר לקוחות קרובים ל-0, המודל מדויק יותר. שארית שלילית: חזה עזיבה ללקוח שנשאר. שארית חיובית: פספס לקוח שעזב.
אילו נתונים על הלקוח השפיעו הכי הרבה על החיזוי. ב-Random Forest זו חשיבות המשתנה בעצים (Gini importance). ב-XGBoost וב-LightGBM זה השיפור שהמשתנה תורם בפיצולים (gain). ב-CatBoost זה כמה התחזית משתנה כשהמשתנה משתנה (PredictionValuesChange). ברגרסיה הלוגיסטית זה הערך המוחלט של המקדם אחרי נרמול. בכל המודלים החשיבות מוצגת כאחוז מסך החשיבות, כדי שאפשר יהיה להשוות ביניהם.
ממוין לפי החשיבות הממוצעת בכל המודלים
לכל מודל הורץ GridSearchCV עם 5-Fold Cross-Validation מרובד, על נתוני האימון בלבד (8,000 לקוחות). כל צירוף של פרמטרים אומן 5 פעמים, ובכל פעם נבדק על קפל אחר. נבחר הצירוף עם ה-F1 הממוצע הגבוה ביותר. אחר כך המודל אומן מחדש על כל נתוני האימון ונבדק פעם אחת על נתוני המבחן. כל שאר הלשוניות מציגות את המודלים אחרי הכוונון.
נתוני המבחן (2,000 לקוחות) · לפני = פרמטרים מקוריים, אחרי = הפרמטרים מ-GridSearchCV
ממוצע של 5 קפלים · הקו האנכי מראה ± סטיית תקן
עיגול ריק = פרמטרים מקוריים · עיגול מלא = אחרי כוונון
הערך שנבחר מודגש
כל השלבים שבוצעו, מהקובץ הגולמי ועד למדדים הסופיים, לפי הסדר. לחצו על "הצג קוד" כדי לראות את הקוד של כל שלב.
קריאת הקובץ churn_modelling.csv ובדיקה שאין ערכים חסרים ושורות כפולות. התוצאה: הנתונים נקיים, ואין צורך להשלים או למחוק שורות.
df = pd.read_csv("churn_modelling.csv")
df.shape # (10000, 13)
df.isna().sum().sum() # 0 missing values
df.duplicated().sum() # 0 duplicate rows
df["Exited"].mean() # 0.204 churn rateמשתנה המטרה הוא Exited (1 = הלקוח עזב). הוסרו CustomerId ו-Surname: הם מזהים לקוח ואין בהם מידע על התנהגות, ומודל שלומד מהם עלול "לשנן" לקוחות במקום ללמוד דפוסים. נשארו 10 משתנים מסבירים.
data = df.drop(columns=["CustomerId", "Surname"])מודלים עובדים רק עם מספרים, ולכן Geography ו-Gender הומרו לעמודות 0/1. drop_first=True מוריד קטגוריה אחת בכל משתנה, כי אפשר להסיק אותה מהשאר (למשל: לא גרמניה ולא ספרד = צרפת). הקידוד לא "לומד" שום דבר מהנתונים, כי כל שורה מקודדת לפי הערך שלה בלבד. לכן בטוח לבצע אותו לפני החלוקה.
data = pd.get_dummies(data, columns=["Geography", "Gender"],
drop_first=True, dtype=int)
X = data.drop(columns=["Exited"])
y = data["Exited"]חלוקה אקראית של הנתונים המעובדים: 80% לאימון ו-20% למבחן. החלוקה מרובדת (stratify=y), כך ששיעור העוזבים זהה בשתי הקבוצות. נתוני המבחן נשמרים בצד, ומשמשים רק בשלב האחרון.
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y)8 המשתנים המספריים עוברים נרמול לממוצע 0 וסטיית תקן 1, כדי שמשתנה עם ערכים גדולים, כמו יתרה, לא ישלוט ברגרסיה הלוגיסטית. הנרמול בא אחרי החלוקה בכוונה: הוא מחשב ממוצע וסטיית תקן, ואם היה מחושב על כל הנתונים, מידע מנתוני המבחן היה "דולף" לאימון (data leakage). לכן הוא נמצא בתוך ה-Pipeline ונלמד מנתוני האימון בלבד. בתוך ה-Cross-Validation הוא נלמד בכל פעם רק מקפלי האימון.
prep = ColumnTransformer([("num", StandardScaler(), num_cols)],
remainder="passthrough") # 0/1 columns stay as is
model = Pipeline([("prep", prep), ("clf", classifier)])
model.fit(X_train, y_train) # the scaler learns mean/std from X_train onlyלכל מודל נבדקו כל צירופי הפרמטרים ברשת. נתוני האימון חולקו ל-5 קפלים. כל צירוף אומן 5 פעמים על 4 קפלים ונבדק על הקפל החמישי, ונבחר הצירוף עם ה-F1 הממוצע הגבוה ביותר. נתוני המבחן לא השתתפו בבחירה. הפירוט המלא נמצא בלשונית 4.
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
gs = GridSearchCV(Pipeline([("prep", prep), ("clf", RandomForestClassifier(...))]),
param_grid={"clf__max_depth": [None, 6, 10, 14],
"clf__min_samples_leaf": [1, 3, 5],
"clf__max_features": ["sqrt", 0.5],
"clf__class_weight": [None, "balanced_subsample"]},
scoring="f1", cv=cv, n_jobs=-1)
gs.fit(X_train, y_train)
gs.best_params_, gs.best_score_כל מודל אומן מחדש על כל 8,000 לקוחות האימון, עם הפרמטרים שנבחרו בשלב 6.
כל מודל חזה את 2,000 לקוחות המבחן, שהוא לא ראה אף פעם. לקוח מסווג כ"עוזב" כשההסתברות שהמודל חוזה לו היא 0.5 ומעלה. מתוך מטריצת הבלבול מחושבים המדדים: Recall = TP / (TP + FN), Precision = TP / (TP + FP), ו-F1 הוא הממוצע ההרמוני של שניהם.
pred = model.predict(X_test) # threshold 0.5
proba = model.predict_proba(X_test)[:, 1]
confusion_matrix(y_test, pred)
recall_score(y_test, pred), precision_score(y_test, pred)
f1_score(y_test, pred), roc_auc_score(y_test, proba)