בניית מודל סיווג בפייתון: מדריך מעשי עם scikit-learn 1.8 ו-pandas 3.0

מדריך מעשי צעד-אחר-צעד לבניית מודל סיווג בפייתון עם scikit-learn 1.8 ו-pandas 3.0. כולל רגרסיה לוגיסטית, יער אקראי, SVM, הערכת ביצועים, קרוס-ולידציה וכוונון היפר-פרמטרים עם דוגמאות קוד מלאות.

מה זה סיווג ולמה זה חשוב?

אם אתם עובדים עם נתונים — ויש סיכוי לא רע שזו הסיבה שאתם כאן — אז כנראה שנתקלתם כבר במצב שבו צריך "לשייך" דברים לקטגוריות. זה בדיוק מה שסיווג (Classification) עושה. בין אם מדובר בזיהוי אימיילים כספאם או לא-ספאם, אבחון רפואי (שפיר או ממאיר), או חיזוי האם לקוח יעזוב שירות — כל אלה הן בעיות סיווג קלאסיות.

בניגוד לרגרסיה (Regression) שבה אנחנו מנבאים ערך מספרי רציף (כמו מחיר דירה), בסיווג אנחנו מנבאים קטגוריה. זה יכול להיות סיווג בינארי (שתי קטגוריות) או סיווג רב-מחלקתי (Multi-class) עם מספר קטגוריות.

במדריך הזה נבנה יחד, צעד אחר צעד, מודלים של סיווג באמצעות scikit-learn 1.8 ו-pandas 3.0 — שתי הספריות המרכזיות ב-Python לעבודה עם נתונים ולמידת מכונה. נעבור על הכנת הנתונים, נבנה שלושה מודלים שונים, נשווה ביניהם, ונלמד איך לכוונן היפר-פרמטרים. אז בואו נתחיל.

הכנת סביבת העבודה

לפני שנצלול לקוד, צריך לוודא שהכלים שלנו מותקנים ומעודכנים. נכון ל-2026, הגרסאות העדכניות הן scikit-learn 1.8 ו-pandas 3.0 — ושתיהן מביאות שינויים משמעותיים שכדאי להכיר.

pip install scikit-learn==1.8 "pandas>=3.0" matplotlib seaborn pyarrow

אחרי ההתקנה, נוודא שהכל עובד כמו שצריך:

import sklearn
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

print(f"scikit-learn: {sklearn.__version__}")
print(f"pandas: {pd.__version__}")
print(f"numpy: {np.__version__}")

כמה דברים שכדאי לדעת על הגרסאות האלה:

  • pandas 3.0 — מפעיל את Copy-on-Write כברירת מחדל, משתמש ב-str מבוסס PyArrow במקום object למחרוזות, וברירת המחדל ל-datetime היא datetime64[us].
  • scikit-learn 1.8 — תומך ב-set_output(transform="pandas") לאינטגרציה חלקה עם DataFrame-ים, ומביא שיפורים ב-metadata routing.

טעינת נתונים וחקירה ראשונית

שימוש בדאטאסטים מובנים של scikit-learn

הדרך הכי מהירה להתחיל היא עם הדאטאסטים המובנים של scikit-learn. נשתמש בדאטאסט סרטן השד (Breast Cancer) — דאטאסט קלאסי לסיווג בינארי עם 569 דגימות ו-30 מאפיינים:

from sklearn.datasets import load_breast_cancer

# טעינת הדאטאסט
data = load_breast_cancer()

# המרה ל-DataFrame של pandas
df = pd.DataFrame(data.data, columns=data.feature_names)
df["target"] = data.target

# הצצה ראשונית
print(f"גודל הדאטאסט: {df.shape}")
print(f"\nמחלקות: {dict(zip(data.target_names, pd.Series(data.target).value_counts().sort_index().values))}")
print(f"\nחמש שורות ראשונות:")
df.head()

בואו נבדוק עוד כמה דברים על הנתונים שלנו:

# מידע כללי על הנתונים
print(df.info())
print("\n--- סטטיסטיקה תיאורית ---")
print(df.describe())

# בדיקת ערכים חסרים
print(f"\nערכים חסרים בכל עמודה:\n{df.isnull().sum().sum()} ערכים חסרים בסך הכל")

הדאטאסט הזה "נקי" יחסית — אין בו ערכים חסרים וכל המאפיינים מספריים. בעולם האמיתי, כמובן, זה כמעט אף פעם לא ככה (מניסיון אישי, הדאטאסטים ה"נקיים" הם האיחוד הנדיר).

טעינת נתונים מקובץ CSV עם pandas 3.0

בפרויקטים אמיתיים, הנתונים מגיעים בדרך כלל מקבצי CSV, מסדי נתונים, או APIs. הנה איך טוענים נתונים מ-CSV עם pandas 3.0 — שימו לב לשינויים:

# טעינת נתונים מ-CSV
# ב-pandas 3.0, מחרוזות נשמרות כ-str (מבוסס PyArrow) ולא כ-object
df_csv = pd.read_csv("my_dataset.csv")

# שימו לב לסוגי הנתונים החדשים
print(df_csv.dtypes)
# עמודות טקסט יופיעו כ-str במקום object

# ב-pandas 3.0, Copy-on-Write פעיל כברירת מחדל
# זה אומר שהפעולה הבאה לא תשנה את df_csv המקורי
df_copy = df_csv[["column1", "column2"]]
# שינויים ב-df_copy לא ישפיעו על df_csv — בלי הפתעות!

השינוי ל-Copy-on-Write ב-pandas 3.0 הוא עניין גדול. אם אתם רגילים לגרסאות ישנות יותר, כנראה זוכרים את ה-SettingWithCopyWarning המעצבן הזה — אז הוא כבר לא רלוונטי. עכשיו כל פעולת slice יוצרת "צפייה עצלנית" שמעתיקה נתונים רק כשבאמת משנים אותם. זה גם בטוח יותר וגם יעיל יותר.

עיבוד מקדים של נתונים

עיבוד מקדים (Preprocessing) הוא — בלי להגזים — חצי מהעבודה בלמידת מכונה. מודל מעולה עם נתונים גרועים יתן תוצאות גרועות. אז בואו נעשה את זה כמו שצריך.

טיפול בערכים חסרים

import pandas as pd
import numpy as np

# יצירת דאטאסט לדוגמה עם ערכים חסרים
df_example = pd.DataFrame({
    "age": [25, 30, np.nan, 45, 50, np.nan, 35],
    "income": [50000, np.nan, 70000, 80000, np.nan, 60000, 55000],
    "category": ["A", "B", None, "A", "B", "C", None],
    "target": [0, 1, 0, 1, 1, 0, 1]
})

print("לפני טיפול בערכים חסרים:")
print(df_example.isnull().sum())

# מילוי ערכים מספריים בחציון (יותר עמיד לערכים קיצוניים מאשר ממוצע)
df_example["age"] = df_example["age"].fillna(df_example["age"].median())
df_example["income"] = df_example["income"].fillna(df_example["income"].median())

# מילוי ערכים קטגוריאליים בערך הנפוץ ביותר
# ב-pandas 3.0, הטיפוס הוא str ולא object
df_example["category"] = df_example["category"].fillna(df_example["category"].mode()[0])

print("\nאחרי טיפול:")
print(df_example.isnull().sum())

קידוד מאפיינים קטגוריאליים

מודלים של למידת מכונה עובדים עם מספרים — הם לא יודעים מה לעשות עם טקסט. לכן צריך להמיר מאפיינים קטגוריאליים למספרים. יש כמה גישות:

from sklearn.preprocessing import LabelEncoder, OneHotEncoder

# Label Encoding — מתאים למשתנה מטרה או לסדר יחסי
le = LabelEncoder()
df_example["category_encoded"] = le.fit_transform(df_example["category"])

# One-Hot Encoding — מתאים למאפיינים נומינליים (בלי סדר)
# עם pandas 3.0 וסוג str החדש
df_one_hot = pd.get_dummies(df_example, columns=["category"], dtype=int)
print(df_one_hot.head())

נרמול ושינוי קנה מידה (Feature Scaling)

חלק מהאלגוריתמים (כמו SVM ורגרסיה לוגיסטית) רגישים מאוד לקנה המידה של המאפיינים. אם מאפיין אחד נע בין 0 ל-1 ואחר בין 0 ל-100,000 — זה עלול לתעתע במודל.

from sklearn.preprocessing import StandardScaler, MinMaxScaler

# StandardScaler — ממרכז סביב 0 עם סטיית תקן 1
scaler = StandardScaler()

# MinMaxScaler — מכווץ לטווח [0, 1]
min_max_scaler = MinMaxScaler()

# דוגמה עם StandardScaler
# ב-scikit-learn 1.8 אפשר להוציא פלט כ-DataFrame
scaler.set_output(transform="pandas")
scaled_data = scaler.fit_transform(df[data.feature_names])
print(scaled_data.head())

שימו לב ל-set_output(transform="pandas") — זו תכונה ממש שימושית ב-scikit-learn 1.8 שמחזירה DataFrame במקום מערך NumPy. ככה שומרים על שמות העמודות ואפשר להמשיך לעבוד בצורה נוחה.

חלוקה לאימון ומבחן

from sklearn.model_selection import train_test_split

# הפרדת מאפיינים ומשתנה מטרה
X = df[data.feature_names]
y = df["target"]

# חלוקה: 80% אימון, 20% מבחן
X_train, X_test, y_train, y_test = train_test_split(
    X, y,
    test_size=0.2,
    random_state=42,
    stratify=y  # שומר על יחס המחלקות בשני הסטים
)

print(f"סט אימון: {X_train.shape[0]} דגימות")
print(f"סט מבחן: {X_test.shape[0]} דגימות")
print(f"\nהתפלגות מחלקות באימון:\n{y_train.value_counts(normalize=True).round(3)}")
print(f"\nהתפלגות מחלקות במבחן:\n{y_test.value_counts(normalize=True).round(3)}")

הפרמטר stratify=y הוא קריטי כשהמחלקות לא מאוזנות. בלי אותו — אתם עלולים לקבל סט מבחן שבו מחלקה אחת כמעט לא מיוצגת, ואז ההערכה שלכם תהיה חסרת משמעות. גיליתי את זה בדרך הקשה כשקיבלתי דיוק של 98% על מודל שבפועל לא למד כלום מועיל.

בניית Pipeline מקצועי

לפני שנתחיל לבנות מודלים, בואו נדבר על Pipeline. במקום להריץ כל שלב בנפרד (סקיילינג, קידוד, מודל), אפשר לשרשר הכל יחד. זה מונע דליפת מידע (data leakage), מפשט את הקוד, וחוסך כאבי ראש בייצור.

from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer

# הגדרת עמודות מספריות
numeric_features = data.feature_names.tolist()

# Pipeline לעיבוד מאפיינים מספריים
numeric_transformer = Pipeline(steps=[
    ("imputer", SimpleImputer(strategy="median")),
    ("scaler", StandardScaler())
])

# אם יש גם מאפיינים קטגוריאליים:
# categorical_transformer = Pipeline(steps=[
#     ("imputer", SimpleImputer(strategy="most_frequent")),
#     ("encoder", OneHotEncoder(handle_unknown="ignore"))
# ])

# שילוב הכל עם ColumnTransformer
preprocessor = ColumnTransformer(
    transformers=[
        ("num", numeric_transformer, numeric_features),
    ]
)

# הגדרת פלט כ-pandas DataFrame (scikit-learn 1.8)
preprocessor.set_output(transform="pandas")

print("ה-Pipeline מוכן!")

רגרסיה לוגיסטית (Logistic Regression)

למרות השם המבלבל, רגרסיה לוגיסטית (Logistic Regression) היא בעצם אלגוריתם סיווג. היא אחד האלגוריתמים הפשוטים והיעילים ביותר — ולעתים קרובות היא הבסיס (baseline) שמולו משווים מודלים מורכבים יותר. אם המודל המתוחכם שלכם לא מצליח להכות רגרסיה לוגיסטית פשוטה, יש מקום לחשיבה מחדש.

from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, classification_report

# בניית Pipeline מלא: עיבוד מקדים + מודל
log_reg_pipeline = Pipeline(steps=[
    ("preprocessor", preprocessor),
    ("classifier", LogisticRegression(
        max_iter=1000,
        random_state=42,
        solver="lbfgs"
    ))
])

# אימון המודל
log_reg_pipeline.fit(X_train, y_train)

# חיזוי על סט המבחן
y_pred_lr = log_reg_pipeline.predict(X_test)

# הערכת ביצועים
accuracy_lr = accuracy_score(y_test, y_pred_lr)
print(f"דיוק רגרסיה לוגיסטית: {accuracy_lr:.4f}")
print(f"\nדוח סיווג מפורט:")
print(classification_report(y_test, y_pred_lr, target_names=data.target_names))

מה שיפה ברגרסיה לוגיסטית זה שהיא גם נותנת הסתברויות — לא רק חיזוי "יבש". זה שימושי מאוד כשצריך לקבוע סף (threshold) מותאם אישית:

# הסתברויות חיזוי
y_proba_lr = log_reg_pipeline.predict_proba(X_test)

# הסתברות השתייכות לכל מחלקה עבור 5 דגימות ראשונות
proba_df = pd.DataFrame(
    y_proba_lr[:5],
    columns=[f"P({name})" for name in data.target_names]
)
print(proba_df.round(4))

יער אקראי (Random Forest)

אם רגרסיה לוגיסטית היא הסכין השוויצרי של הסיווג, אז יער אקראי (Random Forest) הוא הטנק. זה אלגוריתם הרכבה (Ensemble) שבונה מאות (או אלפי) עצי החלטה ומשלב את התחזיות שלהם. היתרון הגדול — הוא עמיד להתאמת יתר (overfitting) ולא דורש נרמול של המאפיינים.

from sklearn.ensemble import RandomForestClassifier

# בניית Pipeline ליער אקראי
rf_pipeline = Pipeline(steps=[
    ("preprocessor", preprocessor),
    ("classifier", RandomForestClassifier(
        n_estimators=200,
        max_depth=10,
        min_samples_split=5,
        random_state=42,
        n_jobs=-1  # שימוש בכל הליבות
    ))
])

# אימון
rf_pipeline.fit(X_train, y_train)

# חיזוי והערכה
y_pred_rf = rf_pipeline.predict(X_test)
accuracy_rf = accuracy_score(y_test, y_pred_rf)
print(f"דיוק יער אקראי: {accuracy_rf:.4f}")
print(f"\nדוח סיווג:")
print(classification_report(y_test, y_pred_rf, target_names=data.target_names))

חשיבות מאפיינים (Feature Importance)

אחד הדברים הכי שימושיים ביער אקראי הוא היכולת לזהות אילו מאפיינים הכי חשובים לחיזוי. זה עוזר גם להבנה של הבעיה וגם להנדסת מאפיינים:

# חילוץ חשיבות המאפיינים
feature_importance = rf_pipeline.named_steps["classifier"].feature_importances_

# יצירת DataFrame מסודר
importance_df = pd.DataFrame({
    "feature": data.feature_names,
    "importance": feature_importance
}).sort_values("importance", ascending=False)

# הצגת 10 המאפיינים החשובים ביותר
print("10 המאפיינים החשובים ביותר:")
print(importance_df.head(10).to_string(index=False))

# ויזואליזציה
plt.figure(figsize=(10, 8))
top_15 = importance_df.head(15)
plt.barh(range(len(top_15)), top_15["importance"].values)
plt.yticks(range(len(top_15)), top_15["feature"].values)
plt.xlabel("Importance")
plt.title("Top 15 Feature Importances - Random Forest")
plt.gca().invert_yaxis()
plt.tight_layout()
plt.show()

מניסיון אישי, חשיבות המאפיינים היא אחד הכלים הכי יעילים לתקשורת עם אנשים שאינם טכניים. כשאתם יכולים להראות למנהל שהמאפיין "worst radius" הוא הכי משמעותי — זה הופך את המודל מ"קופסה שחורה" למשהו שאפשר להסביר.

מכונת וקטורים תומכים (SVM)

SVM — Support Vector Machine הוא אלגוריתם אלגנטי שמחפש את ה"גבול" הטוב ביותר בין המחלקות. הוא עובד מצוין עם נתונים ממימד גבוה ועם דגימות בגודל בינוני. החיסרון העיקרי — הוא איטי יחסית על דאטאסטים גדולים מאוד וחייב נרמול של המאפיינים.

from sklearn.svm import SVC

# בניית Pipeline ל-SVM
svm_pipeline = Pipeline(steps=[
    ("preprocessor", preprocessor),
    ("classifier", SVC(
        kernel="rbf",
        C=1.0,
        gamma="scale",
        probability=True,  # לאפשר חישוב הסתברויות
        random_state=42
    ))
])

# אימון
svm_pipeline.fit(X_train, y_train)

# חיזוי והערכה
y_pred_svm = svm_pipeline.predict(X_test)
accuracy_svm = accuracy_score(y_test, y_pred_svm)
print(f"דיוק SVM: {accuracy_svm:.4f}")
print(f"\nדוח סיווג:")
print(classification_report(y_test, y_pred_svm, target_names=data.target_names))

הפרמטר kernel="rbf" הוא ה-kernel הנפוץ ביותר ומתאים לרוב המקרים. אם הנתונים שלכם ניתנים להפרדה ליניארית, kernel="linear" יהיה מהיר יותר. הפרמטר C קובע את האיזון בין התאמה לנתוני האימון לבין הכללה — ערך גבוה יותר אומר "להתאים יותר לנתונים" (בסיכון של overfitting).

השוואת מודלים ומדדי הערכה

עכשיו שיש לנו שלושה מודלים, בואו נשווה ביניהם בצורה מסודרת. דיוק (Accuracy) לבד הוא לא מספיק — צריך להסתכל על התמונה המלאה.

מטריצת בלבול (Confusion Matrix)

from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay

fig, axes = plt.subplots(1, 3, figsize=(18, 5))

models = {
    "Logistic Regression": y_pred_lr,
    "Random Forest": y_pred_rf,
    "SVM": y_pred_svm
}

for ax, (name, y_pred) in zip(axes, models.items()):
    cm = confusion_matrix(y_test, y_pred)
    disp = ConfusionMatrixDisplay(cm, display_labels=data.target_names)
    disp.plot(ax=ax, cmap="Blues", colorbar=False)
    ax.set_title(name)

plt.tight_layout()
plt.show()

מטריצת הבלבול מראה בדיוק איפה המודל טועה. True Positive ו-True Negative — המודל צדק. False Positive — המודל אמר "כן" כשהתשובה היא "לא" (אזעקת שווא). False Negative — המודל אמר "לא" כשהתשובה היא "כן" (פספוס). במקרה של אבחון סרטן, למשל, FN (פספוס של גידול ממאיר) הוא הרבה יותר מסוכן מ-FP.

דוח סיווג מפורט

from sklearn.metrics import classification_report

print("=" * 60)
for name, y_pred in models.items():
    print(f"\n{name}:")
    print("-" * 40)
    report = classification_report(
        y_test, y_pred,
        target_names=data.target_names,
        output_dict=True
    )
    print(f"  Precision (דיוק):   {report['weighted avg']['precision']:.4f}")
    print(f"  Recall (רגישות):    {report['weighted avg']['recall']:.4f}")
    print(f"  F1-Score:           {report['weighted avg']['f1-score']:.4f}")
    print(f"  Accuracy:           {report['accuracy']:.4f}")
print("=" * 60)

הסבר קצר על המדדים:

  • Precision (דיוק) — מתוך כל מה שהמודל סיווג כחיובי, כמה באמת חיובי? מדד חשוב כשהעלות של FP גבוהה.
  • Recall (רגישות) — מתוך כל החיוביים האמיתיים, כמה המודל הצליח לזהות? מדד חשוב כשהעלות של FN גבוהה.
  • F1-Score — ממוצע הרמוני של Precision ו-Recall. שימושי כשרוצים מדד יחיד שמאזן בין השניים.

עקומת ROC ו-AUC

עקומת ROC (Receiver Operating Characteristic) היא כלי ויזואלי מצוין להשוואת מודלים. היא מראה את היחס בין שיעור החיוביים האמיתיים (True Positive Rate) לשיעור החיוביים השגויים (False Positive Rate) בסיפים שונים. ה-AUC (Area Under Curve) מסכם את הביצועים למספר יחיד בין 0 ל-1.

from sklearn.metrics import roc_curve, auc

fig, ax = plt.subplots(figsize=(8, 6))

pipelines = {
    "Logistic Regression": log_reg_pipeline,
    "Random Forest": rf_pipeline,
    "SVM": svm_pipeline
}

for name, pipeline in pipelines.items():
    y_proba = pipeline.predict_proba(X_test)[:, 1]
    fpr, tpr, thresholds = roc_curve(y_test, y_proba)
    roc_auc = auc(fpr, tpr)

    ax.plot(fpr, tpr, label=f"{name} (AUC = {roc_auc:.4f})")

ax.plot([0, 1], [0, 1], "k--", label="Random (AUC = 0.5)")

ax.set_xlabel("False Positive Rate")
ax.set_ylabel("True Positive Rate")
ax.set_title("ROC Curves - Model Comparison")
ax.legend(loc="lower right")
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

ככל שעקומת ROC קרובה יותר לפינה השמאלית העליונה — המודל טוב יותר. AUC של 1.0 זה מושלם (וכנראה סימן שיש דליפת מידע), AUC של 0.5 זה כמו לנחש באקראי.

וולידציה צולבת (Cross-Validation)

חלוקה אחת לאימון ומבחן יכולה לתת תוצאות שמשתנות בהתאם ל"מזל" של החלוקה. וולידציה צולבת (Cross-Validation) פותרת את הבעיה הזו — היא מחלקת את הנתונים ל-K חלקים, מאמנת K פעמים (כל פעם עם חלק אחר כסט מבחן), ומחשבת ממוצע. זה נותן הערכה הרבה יותר אמינה.

from sklearn.model_selection import cross_val_score, StratifiedKFold

# הגדרת אסטרטגיית חלוקה
cv_strategy = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

print("תוצאות וולידציה צולבת (5-Fold):")
print("=" * 55)

for name, pipeline in pipelines.items():
    cv_scores = cross_val_score(
        pipeline, X, y,
        cv=cv_strategy,
        scoring="accuracy",
        n_jobs=-1
    )

    f1_scores = cross_val_score(
        pipeline, X, y,
        cv=cv_strategy,
        scoring="f1_weighted",
        n_jobs=-1
    )

    print(f"\n{name}:")
    print(f"  Accuracy: {cv_scores.mean():.4f} (+/- {cv_scores.std():.4f})")
    print(f"  F1 Score: {f1_scores.mean():.4f} (+/- {f1_scores.std():.4f})")
    print(f"  Scores per fold: {[f'{s:.4f}' for s in cv_scores]}")

print("\n" + "=" * 55)

שימו לב ל-StratifiedKFold — זה מבטיח שכל fold שומר על אותו יחס מחלקות כמו בדאטאסט המלא. זה חשוב במיוחד כשיש חוסר איזון בין המחלקות. הסטייה התקנית (std) מראה כמה יציב המודל — סטייה גבוהה מרמזת שהמודל רגיש לנתונים הספציפיים שהוא רואה.

כוונון היפר-פרמטרים

כל מודל מגיע עם "כפתורים" שאפשר לכוונן — אלה ההיפר-פרמטרים. הבחירה הנכונה שלהם יכולה לעשות הבדל משמעותי בביצועים. יש שתי גישות עיקריות: GridSearchCV (חיפוש ממצה) ו-RandomizedSearchCV (חיפוש אקראי).

GridSearchCV — חיפוש ממצה

from sklearn.model_selection import GridSearchCV

# הגדרת מרחב החיפוש ליער אקראי
param_grid = {
    "classifier__n_estimators": [100, 200, 300],
    "classifier__max_depth": [5, 10, 15, None],
    "classifier__min_samples_split": [2, 5, 10],
    "classifier__min_samples_leaf": [1, 2, 4]
}

# חיפוש עם Grid Search
grid_search = GridSearchCV(
    rf_pipeline,
    param_grid,
    cv=5,
    scoring="f1_weighted",
    n_jobs=-1,
    verbose=1
)

grid_search.fit(X_train, y_train)

print(f"הפרמטרים הטובים ביותר:")
for param, value in grid_search.best_params_.items():
    print(f"  {param}: {value}")
print(f"\nציון F1 הטוב ביותר: {grid_search.best_score_:.4f}")

# הערכה על סט המבחן
y_pred_best = grid_search.predict(X_test)
print(f"\nדיוק על סט המבחן: {accuracy_score(y_test, y_pred_best):.4f}")

הבעיה עם GridSearchCV היא שהוא בודק כל שילוב אפשרי. במקרה שלנו למעלה זה 3 × 4 × 3 × 3 = 108 שילובים, כפול 5 folds = 540 אימונים. עם מרחבי חיפוש גדולים יותר, זה יכול לקחת שעות.

RandomizedSearchCV — חיפוש אקראי

from sklearn.model_selection import RandomizedSearchCV
from scipy.stats import randint, uniform

# מרחב חיפוש עם התפלגויות
param_distributions = {
    "classifier__n_estimators": randint(50, 500),
    "classifier__max_depth": [5, 10, 15, 20, 25, None],
    "classifier__min_samples_split": randint(2, 20),
    "classifier__min_samples_leaf": randint(1, 10),
    "classifier__max_features": ["sqrt", "log2", None]
}

# חיפוש אקראי — בודק רק n_iter שילובים
random_search = RandomizedSearchCV(
    rf_pipeline,
    param_distributions,
    n_iter=50,
    cv=5,
    scoring="f1_weighted",
    n_jobs=-1,
    random_state=42,
    verbose=1
)

random_search.fit(X_train, y_train)

print(f"הפרמטרים הטובים ביותר:")
for param, value in random_search.best_params_.items():
    print(f"  {param}: {value}")
print(f"\nציון F1 הטוב ביותר: {random_search.best_score_:.4f}")

# הערכה סופית
y_pred_random_best = random_search.predict(X_test)
print(f"\nדיוק על סט המבחן: {accuracy_score(y_test, y_pred_random_best):.4f}")
print(f"\nדוח סיווג מפורט:")
print(classification_report(y_test, y_pred_random_best, target_names=data.target_names))

מחקרים הראו ש-RandomizedSearchCV מגיע לתוצאות דומות ל-GridSearchCV עם הרבה פחות זמן חישוב — במיוחד כשמרחב החיפוש גדול. מניסיון אישי, 50-100 איטרציות מספיקות ברוב המקרים כדי למצוא שילוב טוב מאוד.

סיכום השוואה סופית

# טבלת סיכום
results = []
for name, pipeline in pipelines.items():
    cv_acc = cross_val_score(pipeline, X, y, cv=5, scoring="accuracy").mean()
    cv_f1 = cross_val_score(pipeline, X, y, cv=5, scoring="f1_weighted").mean()

    y_pred = pipeline.predict(X_test)
    test_acc = accuracy_score(y_test, y_pred)

    y_proba = pipeline.predict_proba(X_test)[:, 1]
    fpr, tpr, _ = roc_curve(y_test, y_proba)
    test_auc = auc(fpr, tpr)

    results.append({
        "מודל": name,
        "CV Accuracy": f"{cv_acc:.4f}",
        "CV F1": f"{cv_f1:.4f}",
        "Test Accuracy": f"{test_acc:.4f}",
        "Test AUC": f"{test_auc:.4f}"
    })

results_df = pd.DataFrame(results)
print("\n--- טבלת השוואה סופית ---")
print(results_df.to_string(index=False))

שמירת המודל לשימוש עתידי

אחרי שמצאנו את המודל הטוב ביותר, נרצה לשמור אותו — בלי צורך לאמן מחדש בכל פעם:

import joblib

# שמירת המודל הטוב ביותר
best_model = random_search.best_estimator_
joblib.dump(best_model, "best_classifier_model.joblib")
print("המודל נשמר בהצלחה!")

# טעינה בעתיד
loaded_model = joblib.load("best_classifier_model.joblib")

# שימוש במודל הטעון
new_prediction = loaded_model.predict(X_test[:5])
print(f"חיזויים חדשים: {new_prediction}")
print(f"תוויות אמיתיות: {y_test[:5].values}")

טיפים מעשיים

אחרי שעברנו את כל התהליך, הנה כמה דברים שלמדתי מעבודה יומיומית עם מודלי סיווג:

  • תמיד תתחילו פשוט — רגרסיה לוגיסטית היא baseline מצוין. אם היא נותנת 95% דיוק, אולי לא צריך מודל מורכב יותר. "המודל הפשוט ביותר שעובד" הוא כלל זהב.
  • בדקו דליפת מידע (Data Leakage) — אם המודל שלכם מראה תוצאות "טובות מדי" (למשל AUC של 0.99), סביר שיש דליפת מידע. בדקו שאתם לא משתמשים בסט המבחן בזמן העיבוד המקדים.
  • Pipeline הוא הכרחי — הוא מונע דליפת מידע, מפשט את הקוד, ומקל על הפריסה לייצור. אין שום סיבה טובה לא להשתמש בו.
  • נצלו את pandas 3.0 Copy-on-Write — תנו ל-Copy-on-Write לעשות את העבודה ותפסיקו לכתוב .copy() בכל מקום. הקוד נקי יותר והביצועים טובים יותר.
  • השתמשו ב-set_output(transform="pandas") — זה שומר על שמות העמודות אחרי טרנספורמציות ומקל על דיבוג. ב-scikit-learn 1.8 זה עובד חלק.
  • שימו לב לאיזון המחלקות — אם 95% מהנתונים שייכים למחלקה אחת, מודל ש"תמיד מנחש" את המחלקה הזו יקבל 95% דיוק בלי ללמוד כלום. השתמשו ב-F1-Score או AUC במקרים כאלה, ושקלו שימוש ב-class_weight="balanced".
  • RandomizedSearchCV לפני GridSearchCV — תתחילו עם חיפוש אקראי כדי למצוא את האזור הטוב, ואז צמצמו עם חיפוש ממצה. זה חוסך המון זמן חישוב.
  • תיעוד ושחזוריות — תמיד השתמשו ב-random_state קבוע, תעדו את הגרסאות של הספריות, ותשמרו את הנתונים והמודל יחד.

שאלות נפוצות

מתי להשתמש ברגרסיה לוגיסטית ומתי ביער אקראי?

רגרסיה לוגיסטית מתאימה כשהקשר בין המאפיינים למטרה הוא קרוב לליניארי, כשיש צורך בפרשנות ברורה של המקדמים, או כשאתם רוצים baseline מהיר. יער אקראי מתאים כשיש קשרים לא-ליניאריים בנתונים, כשיש הרבה מאפיינים (כולל כאלה לא רלוונטיים — הוא עמיד לזה), או כשרוצים ביצועים גבוהים בלי הרבה כוונון. באופן כללי, תתחילו עם רגרסיה לוגיסטית כ-baseline ותעברו ליער אקראי אם צריך.

מה ההבדל בין StandardScaler ל-MinMaxScaler?

StandardScaler ממרכז את הנתונים סביב 0 עם סטיית תקן 1 — מתאים כשהנתונים מתפלגים נורמלית ויש ערכים קיצוניים. MinMaxScaler מכווץ לטווח [0,1] — מתאים כשרוצים שמירה על הצורה המקורית של ההתפלגות ואין ערכים קיצוניים משמעותיים. לרוב, StandardScaler הוא הבחירה הבטוחה יותר. ואם אתם משתמשים באלגוריתמים מבוססי עצים (כמו יער אקראי או XGBoost), לא צריך סקיילינג בכלל.

איך מתמודדים עם דאטאסט לא מאוזן?

יש כמה אסטרטגיות. ראשית, שנו את מדד ההערכה — במקום דיוק, השתמשו ב-F1-Score או AUC. שנית, השתמשו ב-class_weight="balanced" בפרמטרים של המודל. שלישית, שקלו דגימת יתר (oversampling) של המחלקה הקטנה עם SMOTE (מהספרייה imbalanced-learn) או דגימת חסר (undersampling) של המחלקה הגדולה. רביעית, בבעיות מסוימות אפשר לכוונן את ה-סף (threshold) של ההסתברות — במקום 0.5 ברירת המחדל, אולי 0.3 מתאים יותר.

מה חדש ב-pandas 3.0 שמשפיע על למידת מכונה?

השינוי הכי גדול הוא Copy-on-Write שהפך לברירת מחדל — כל פעולת slicing יוצרת עותק בטוח ולא "תצוגה" שעלולה לשנות את ה-DataFrame המקורי. בנוסף, סוג ה-str החדש (מבוסס PyArrow) מהיר ויעיל יותר מ-object, ו-datetime64[us] הוא ברירת המחדל ל-datetime. בפועל, רוב הקוד ימשיך לעבוד — אבל כדאי להיות מודעים לשינויים האלה.

האם חייבים להשתמש ב-Pipeline?

טכנית, אפשר בהחלט להריץ כל שלב בנפרד — ובפרויקטים קטנים של חקירה זה לפעמים אפילו יותר נוח. אבל בכל פרויקט רציני, Pipeline הוא הכרח. הסיבה העיקרית: הוא מונע דליפת מידע. כשעושים סקיילינג לפני train/test split, מידע מסט המבחן "דולף" לסט האימון דרך הממוצע וסטיית התקן. Pipeline מבטיח שה-fit קורה רק על סט האימון. בנוסף, Pipeline מפשט את הפריסה לייצור — שומרים קובץ אחד עם כל שרשרת העיבוד והמודל, וזהו.

אודות הכותב Editorial Team

Our team of expert writers and editors.