מהי הנדסת פיצ׳רים ולמה היא קריטית להצלחת המודל?
אם כבר ביצעתם ניתוח נתונים חקרני (EDA) והכרתם את הדאטה שלכם — יופי, אתם בכיוון הנכון. אבל עכשיו מגיע השלב שבאמת עושה את ההבדל בין מודל שנותן תוצאות "בסדר" למודל שמרשים ברצינות: הנדסת פיצ׳רים (Feature Engineering).
בקצרה? זה התהליך של יצירה, שינוי ובחירה של משתנים (features) כדי לשפר את ביצועי המודל. בפועל, לוקחים נתונים גולמיים ומפשילים שרוולים — הופכים אותם למשהו שהמודל באמת יכול ללמוד ממנו.
למה זה כל כך חשוב? כי אלגוריתמים של למידת מכונה לא "מבינים" טקסט, תאריכים או קטגוריות. הם עובדים עם מספרים. ולא סתם מספרים — מספרים שמייצגים דפוסים אמיתיים בנתונים. מדעני נתונים מקדישים כ-60% מזמנם להכנת נתונים, וחלק גדול מזה הוא בדיוק מה שנלמד פה.
אז בואו נצלול פנימה. במדריך הזה נעבור צעד אחר צעד על הטכניקות המרכזיות, עם pandas 3.0 (שיצאה בינואר 2026) ו-scikit-learn 1.8. כל דוגמת קוד ניתנת להרצה ישירה ב-Jupyter Notebook, אז מומלץ לפתוח אחד לידכם ולהריץ תוך כדי קריאה.
הכנת סביבת העבודה
לפני הכל, נתקין את הספריות הנדרשות:
pip install pandas>=3.0 scikit-learn>=1.8 numpy pyarrow matplotlib seaborn
שימו לב: ב-pandas 3.0 כמעט חובה להתקין את pyarrow. טיפוס המחרוזות החדש (str) מבוסס עליו ומספק שיפורי ביצועים של פי 5-10 בפעולות טקסט, וחיסכון של עד 50% בזיכרון. בהתנסות שלי, ההבדל מורגש מאוד כבר מ-DataFrame של כמה אלפי שורות.
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler, MinMaxScaler, OneHotEncoder, OrdinalEncoder
from sklearn.impute import SimpleImputer
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_squared_error, r2_score
print(f"pandas: {pd.__version__}")
import sklearn
print(f"scikit-learn: {sklearn.__version__}")
טעינת נתונים לדוגמה
נעבוד עם מערך נתוני Ames Housing — מערך עשיר שכולל משתנים מספריים, קטגוריאליים ותאריכיים. הוא מושלם ללימוד הנדסת פיצ׳רים כי יש בו מגוון רחב של סוגי נתונים (וגם לא מעט ערכים חסרים, מה שמוסיף לכיף).
from sklearn.datasets import fetch_openml
# Ames Housing dataset
housing = fetch_openml(name="house_prices", as_frame=True, parser="auto")
df = housing.frame.copy()
print(f"Shape: {df.shape}")
print(f"\nColumn types:\n{df.dtypes.value_counts()}")
df.head()
שלב 1: טיפול בערכים חסרים עם SimpleImputer
ערכים חסרים הם אויב מספר אחד של מודלים. רוב האלגוריתמים פשוט לא יודעים להתמודד איתם ויזרקו שגיאה.
ב-scikit-learn, הכלי הכי ישיר לטיפול בהם הוא SimpleImputer:
# לעמודות מספריות - מילוי בחציון
numeric_imputer = SimpleImputer(strategy="median")
# לעמודות קטגוריאליות - מילוי בערך השכיח
categorical_imputer = SimpleImputer(strategy="most_frequent")
# דוגמה: מילוי עמודה מספרית
df["LotFrontage"] = numeric_imputer.fit_transform(df[["LotFrontage"]])
למה חציון ולא ממוצע? החציון עמיד יותר לערכים חריגים (outliers). חישבו על זה ככה — אם יש בית אחד ששווה 10 מיליון והשאר שווים 200-400 אלף, הממוצע יהיה מעוות לגמרי אבל החציון יישאר מייצג. תמיד כמעט עדיף חציון, אלא אם אתם באמת בטוחים שאין outliers.
אסטרטגיות מתקדמות למילוי ערכים
מעבר ל-SimpleImputer, יש גם KNNImputer שמשתמש בשכנים קרובים למילוי חכם יותר. הרעיון פשוט — במקום להגיד "הממוצע של כולם", הוא אומר "הממוצע של הדומים לך":
from sklearn.impute import KNNImputer
# מילוי לפי 5 שכנים קרובים
knn_imputer = KNNImputer(n_neighbors=5)
df_numeric_filled = pd.DataFrame(
knn_imputer.fit_transform(df.select_dtypes(include=np.number)),
columns=df.select_dtypes(include=np.number).columns
)
שלב 2: קידוד משתנים קטגוריאליים
מודלים עובדים עם מספרים בלבד. אז כל מה שהוא טקסט — שם שכונה, סוג גג, רמת איכות — צריך להפוך למספר. יש שתי גישות עיקריות, והבחירה ביניהן תלויה בשאלה אחת פשוטה: האם יש סדר טבעי בין הקטגוריות?
One-Hot Encoding — למשתנים ללא סדר
כשאין סדר (צפון, דרום, מזרח, מערב — אף כיוון לא "גדול יותר" מהאחר), משתמשים ב-One-Hot Encoding. הוא יוצר עמודה בינארית נפרדת לכל קטגוריה:
from sklearn.preprocessing import OneHotEncoder
ohe = OneHotEncoder(drop="first", sparse_output=False, handle_unknown="ignore")
# דוגמה
neighborhoods = df[["Neighborhood"]].head(5)
encoded = ohe.fit_transform(neighborhoods)
print(f"Original shape: {neighborhoods.shape}")
print(f"Encoded shape: {encoded.shape}")
הפרמטר drop="first" מונע מולטיקולינאריות — הוא מסיר עמודה אחת כי היא מיותרת (אם כל השאר 0, זו בהכרח הקטגוריה שהוסרה). handle_unknown="ignore" מבטיח שקטגוריות חדשות שלא הופיעו באימון לא יפילו את הכל בזמן חיזוי.
Ordinal Encoding — למשתנים עם סדר
כשיש סדר טבעי — גרוע, בינוני, טוב, מצוין — אנחנו רוצים לשמר אותו. כאן Ordinal Encoding הוא הבחירה הנכונה:
from sklearn.preprocessing import OrdinalEncoder
quality_order = [["Po", "Fa", "TA", "Gd", "Ex"]]
ordinal_enc = OrdinalEncoder(categories=quality_order, handle_unknown="use_encoded_value", unknown_value=-1)
df["ExterQual_encoded"] = ordinal_enc.fit_transform(df[["ExterQual"]])
print(df[["ExterQual", "ExterQual_encoded"]].drop_duplicates().sort_values("ExterQual_encoded"))
שלב 3: סקיילינג (Feature Scaling)
אלגוריתמים רבים — רגרסיה לינארית, SVM, KNN — רגישים מאוד לסקאלה של הפיצ׳רים. אם פיצ׳ר אחד נע בין 0 ל-1 ואחר בין 0 למיליון, המודל ייתן משקל לא פרופורציונלי לגדול.
זו טעות קלאסית שראיתי אנשים עושים שוב ושוב. אז בואו נתקן את זה.
StandardScaler — נורמליזציה לפי התפלגות
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler() # mean=0, std=1
df_scaled = pd.DataFrame(
scaler.fit_transform(df[["GrLivArea", "LotArea"]]),
columns=["GrLivArea_scaled", "LotArea_scaled"]
)
print(df_scaled.describe().round(2))
MinMaxScaler — נורמליזציה לטווח קבוע
from sklearn.preprocessing import MinMaxScaler
minmax = MinMaxScaler(feature_range=(0, 1))
df_minmax = pd.DataFrame(
minmax.fit_transform(df[["GrLivArea", "LotArea"]]),
columns=["GrLivArea_mm", "LotArea_mm"]
)
print(df_minmax.describe().round(2))
מתי להשתמש במה? StandardScaler מתאים כשהנתונים מתפלגים בצורה פעמונית (נורמלית). MinMaxScaler עדיף כשצריכים טווח מוגדר, למשל עבור רשתות נוירונים. ודבר חשוב — מודלים מבוססי עצים כמו Random Forest ו-XGBoost בדרך כלל לא צריכים סקיילינג בכלל, אז אל תבזבזו על זה זמן שם.
שלב 4: יצירת פיצ׳רים חדשים
וכאן מגיע החלק הכיפי באמת.
במקום להסתפק בפיצ׳רים הקיימים, אנחנו יוצרים חדשים שמייצגים דפוסים שהמודל לא יכול לגלות לבד. זה השלב שבו ידע תחומי (domain knowledge) שווה זהב — אם אתם מבינים את התחום, אתם יודעים אילו שילובים הגיוניים.
פיצ׳רים מבוססי אינטראקציה
# שטח כולל = שטח קומת קרקע + שטח קומה שנייה
df["TotalSF"] = df["1stFlrSF"] + df["2ndFlrSF"] + df["TotalBsmtSF"]
# יחס בין שטח מגורים לשטח מגרש
df["LivArea_Ratio"] = df["GrLivArea"] / df["LotArea"]
# האם יש מוסך?
df["HasGarage"] = (df["GarageArea"] > 0).astype(int)
# גיל הבית
df["HouseAge"] = df["YrSold"] - df["YearBuilt"]
# האם הבית שופץ?
df["WasRemodeled"] = (df["YearRemodAdd"] != df["YearBuilt"]).astype(int)
שימו לב ל-TotalSF — זה פיצ׳ר שלרוב משפר את הביצועים בצורה ניכרת, כי הוא מאחד מידע מפוזר על שטח הבית. גם HouseAge עדיף בהרבה על שנת בנייה גולמית.
Polynomial Features
כשיש קשרים לא-ליניאריים בנתונים (וכמעט תמיד יש), הוספת פיצ׳רים פולינומיאליים יכולה לשפר משמעותית:
from sklearn.preprocessing import PolynomialFeatures
poly = PolynomialFeatures(degree=2, interaction_only=True, include_bias=False)
# נבחר שני פיצ׳רים לדוגמה
features_for_poly = df[["GrLivArea", "OverallQual"]].dropna()
poly_features = poly.fit_transform(features_for_poly)
print(f"Original features: {features_for_poly.shape[1]}")
print(f"After polynomial: {poly_features.shape[1]}")
print(f"Feature names: {poly.get_feature_names_out()}")
רק זהירות — עם הרבה פיצ׳רים ודרגה גבוהה, מספר העמודות יכול לפוצץ. התחילו תמיד עם interaction_only=True ודרגה 2.
Binning — חלוקה לקטגוריות
# חלוקת גיל הבית לקבוצות
df["AgeGroup"] = pd.cut(
df["HouseAge"],
bins=[0, 10, 25, 50, 200],
labels=["new", "recent", "established", "old"]
)
print(df["AgeGroup"].value_counts())
שלב 5: בניית Pipeline מלא עם ColumnTransformer
טוב, עד עכשיו עשינו כל דבר בנפרד וזה עבד מצוין ללמידה. אבל בפרויקט אמיתי? ככה לא עובדים.
בפרויקט אמיתי אנחנו צריכים תהליך אוטומטי ומשוכפל שמונע דליפת מידע (data leakage) ומבטיח שאותו עיבוד בדיוק יחול גם על נתוני אימון וגם על נתוני מבחן. כאן נכנס ColumnTransformer — אחד הכלים השימושיים ביותר ב-scikit-learn, לדעתי.
הוא מאפשר להחיל טרנספורמציות שונות על עמודות שונות, והכל בתוך Pipeline אחד נקי:
# הגדרת עמודות לפי סוג
numeric_features = ["GrLivArea", "LotArea", "OverallQual", "OverallCond",
"YearBuilt", "TotalBsmtSF", "GarageArea"]
categorical_features = ["Neighborhood", "BldgType", "HouseStyle",
"Heating", "CentralAir"]
# Pipeline לעמודות מספריות
numeric_pipeline = Pipeline(steps=[
("imputer", SimpleImputer(strategy="median")),
("scaler", StandardScaler())
])
# Pipeline לעמודות קטגוריאליות
categorical_pipeline = Pipeline(steps=[
("imputer", SimpleImputer(strategy="most_frequent")),
("encoder", OneHotEncoder(drop="first", handle_unknown="ignore"))
])
# שילוב הכל עם ColumnTransformer
preprocessor = ColumnTransformer(transformers=[
("num", numeric_pipeline, numeric_features),
("cat", categorical_pipeline, categorical_features)
])
Pipeline מלא עם מודל
עכשיו נחבר את כל זה יחד — עיבוד מקדים + מודל, מקצה לקצה:
# Pipeline מקצה לקצה: עיבוד מקדים + מודל
full_pipeline = Pipeline(steps=[
("preprocessor", preprocessor),
("model", RandomForestRegressor(n_estimators=200, random_state=42))
])
# חלוקה לאימון ומבחן
X = df[numeric_features + categorical_features]
y = df["SalePrice"]
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# אימון
full_pipeline.fit(X_train, y_train)
# הערכה
y_pred = full_pipeline.predict(X_test)
rmse = np.sqrt(mean_squared_error(y_test, y_pred))
r2 = r2_score(y_test, y_pred)
print(f"RMSE: ${rmse:,.0f}")
print(f"R2 Score: {r2:.4f}")
שימו לב כמה זה נקי. שורה אחת של fit, שורה אחת של predict, ואין שום סיכוי לדליפת מידע.
שלב 6: בחירת פיצ׳רים (Feature Selection)
אחרי שיצרנו הרבה פיצ׳רים, הגיע הזמן לסנן. לא כל פיצ׳ר שיצרנו באמת עוזר — חלקם מיותרים, ויש כאלה שאפילו מזיקים למודל. יותר מדי פיצ׳רים? Overfitting.
חשיבות פיצ׳רים מ-Random Forest
import matplotlib.pyplot as plt
# חילוץ חשיבות הפיצ׳רים
feature_names = full_pipeline.named_steps["preprocessor"].get_feature_names_out()
importances = full_pipeline.named_steps["model"].feature_importances_
# מיון ותצוגה
feat_imp = pd.Series(importances, index=feature_names).sort_values(ascending=False)
fig, ax = plt.subplots(figsize=(10, 8))
feat_imp.head(15).plot(kind="barh", ax=ax, color="steelblue")
ax.set_title("Top 15 Feature Importances")
ax.set_xlabel("Importance")
plt.tight_layout()
plt.show()
SelectKBest — בחירה סטטיסטית
גישה נוספת היא בחירה סטטיסטית — בוחרים את k הפיצ׳רים עם הקשר הסטטיסטי החזק ביותר למשתנה המטרה:
from sklearn.feature_selection import SelectKBest, f_regression
# בחירת 10 הפיצ׳רים המשמעותיים ביותר
selector = SelectKBest(score_func=f_regression, k=10)
# שילוב בתוך Pipeline
pipeline_with_selection = Pipeline(steps=[
("preprocessor", preprocessor),
("selector", selector),
("model", RandomForestRegressor(n_estimators=200, random_state=42))
])
pipeline_with_selection.fit(X_train, y_train)
y_pred_selected = pipeline_with_selection.predict(X_test)
r2_selected = r2_score(y_test, y_pred_selected)
print(f"R2 with feature selection: {r2_selected:.4f}")
שלב 7: כוונון היפר-פרמטרים עם GridSearch
אחד הדברים הכי נוחים ב-Pipeline הוא שאפשר לכוונן את כל הפרמטרים בחיפוש אחד — גם של שלבי העיבוד וגם של המודל עצמו:
from sklearn.model_selection import GridSearchCV
param_grid = {
"preprocessor__num__imputer__strategy": ["mean", "median"],
"model__n_estimators": [100, 200],
"model__max_depth": [10, 20, None]
}
grid_search = GridSearchCV(
full_pipeline,
param_grid,
cv=5,
scoring="r2",
n_jobs=-1,
verbose=1
)
grid_search.fit(X_train, y_train)
print(f"Best params: {grid_search.best_params_}")
print(f"Best R2 (CV): {grid_search.best_score_:.4f}")
print(f"Test R2: {grid_search.score(X_test, y_test):.4f}")
כן, הסינטקס עם הקווים התחתונים הכפולים (__) נראה מוזר בהתחלה, אבל מתרגלים. זה פשוט מנווט בין שלבי ה-Pipeline: preprocessor → num → imputer → strategy.
שינויים חשובים ב-pandas 3.0 שמשפיעים על הנדסת פיצ׳רים
pandas 3.0 הביאה כמה שינויים שכדאי מאוד להכיר, במיוחד אם אתם משדרגים מגרסה 2.x:
- טיפוס str חדש: עמודות טקסט מזוהות אוטומטית כ-
str(במקוםobject). זה משפיע עלselect_dtypes— תשתמשו ב-include=["str", "object", "category"]כדי לתפוס את כל העמודות הקטגוריאליות. - Copy-on-Write: שרשור אינדקסים כמו
df[df["A"] > 0]["B"] = 1כבר לא עובד. תשתמשו ב-df.loc[df["A"] > 0, "B"] = 1במקום. אם לא תעשו את זה — תקבלו שגיאה במקום אזהרה, כמו שהיה בגרסאות הישנות. - רזולוציית datetime: ברירת המחדל השתנתה מננו-שניות למיקרו-שניות, מה שמרחיב את טווח התאריכים הנתמך. זה בעיקר רלוונטי אם אתם עובדים עם נתונים היסטוריים מאוד.
שאלות נפוצות
מה ההבדל בין הנדסת פיצ׳רים לבחירת פיצ׳רים?
הנדסת פיצ׳רים היא יצירה ושינוי — בונים פיצ׳רים חדשים מחישובים בין עמודות קיימות. בחירת פיצ׳רים (Feature Selection) היא סינון — בוחרים את הרלוונטיים מתוך כל מה שיש. בפועל, קודם יוצרים (הנדסה) ואז מסננים (בחירה).
האם מודלים מבוססי עצים צריכים סקיילינג?
לא. Random Forest, XGBoost, LightGBM — כולם עובדים עם פיצולים לפי ערכי סף ולא מחשבים מרחקים, אז הסקאלה לא משנה להם. לעומת זאת, רגרסיה לינארית, SVM ו-KNN בהחלט דורשים סקיילינג.
מהו data leakage ואיך Pipeline מונע אותו?
דליפת מידע קורית כשמידע מנתוני המבחן "מתגנב" לתהליך האימון. דוגמה קלאסית: מחשבים ממוצע של עמודה על כל הנתונים (כולל מבחן) ומשתמשים בו למילוי ערכים חסרים. Pipeline מונע את זה כי ה-fit רץ רק על נתוני אימון, וה-transform מחיל את אותם פרמטרים על נתוני המבחן — בלי לחשב כלום מחדש.
כמה פיצ׳רים חדשים כדאי ליצור?
אין מספר קסום. הגישה שעובדת לי הכי טוב: ליצור הרבה ואז לסנן בצורה שיטתית. ככלל אצבע, אם יש לכם n דגימות, נסו לא לעבור את n/10 פיצ׳רים — אחרת overfitting כמעט מובטח.
האם אפשר לבצע הנדסת פיצ׳רים אוטומטית?
בהחלט. ספריות כמו Featuretools יוצרות אינטראקציות וטרנספורמציות בצורה אוטומטית, וכלי AutoML כמו PyCaret כוללים שלב כזה מובנה. אבל — ולדעתי זה ה"אבל" הכי חשוב — ידע תחומי עדיין שווה יותר מכל אוטומציה. אם אתם מבינים את הביזנס, אתם יודעים ליצור פיצ׳רים שאף אלגוריתם לא יגלה לבד.