automl / core /feature_engineer.py
Aju360's picture
Enhance AutoML functionality with class label handling, improved feature engineering, and refined prediction outputs. Update FastAPI integration for better state management and progress reporting during training and tuning processes. Improve Streamlit UI for displaying class probabilities in predictions.
eb9be7f
Raw
History Blame Contribute Delete
5.39 kB
"""Optional feature engineering: polynomial features and target encoding."""
import logging
import numpy as np
import pandas as pd
from sklearn.preprocessing import PolynomialFeatures
logger = logging.getLogger(__name__)
# How many top-correlated numerical columns to use for polynomial features
_TOP_N_POLY = 5
def _top_correlated_numericals(
df: pd.DataFrame, numerical_cols: list[str], target: pd.Series, top_n: int
) -> list[str]:
"""Return the top-N numerical columns most correlated with the target.
Args:
df: Feature DataFrame.
numerical_cols: Candidate column names.
target: Target series.
top_n: Number of columns to select.
Returns:
List of column names sorted by absolute correlation, descending.
"""
correlations: dict[str, float] = {}
for col in numerical_cols:
try:
correlations[col] = abs(df[col].corr(target))
except Exception:
correlations[col] = 0.0
sorted_cols = sorted(correlations, key=lambda c: correlations[c], reverse=True)
return sorted_cols[:top_n]
def add_polynomial_features(
df: pd.DataFrame,
numerical_cols: list[str],
target: pd.Series,
degree: int = 2,
poly_cols: list[str] | None = None,
) -> tuple[pd.DataFrame, list[str]]:
"""Add degree-2 polynomial interaction terms for the top correlated numerical columns.
Args:
df: Feature DataFrame (no target column).
numerical_cols: All numerical column names in df.
target: Target series (used only to rank correlations).
degree: Polynomial degree (default 2).
poly_cols: If given, expand exactly these columns instead of ranking by
correlation. Used at inference so the engineered features match
training exactly (the target is not available then).
Returns:
Tuple of (DataFrame with new polynomial columns appended, list of
columns that were expanded — pass back in as poly_cols at inference).
"""
if poly_cols is not None:
top_cols = [c for c in poly_cols if c in df.columns]
else:
if not numerical_cols:
return df, []
top_cols = _top_correlated_numericals(df, numerical_cols, target, _TOP_N_POLY)
if not top_cols:
return df, []
try:
poly = PolynomialFeatures(degree=degree, include_bias=False, interaction_only=False)
poly_array = poly.fit_transform(df[top_cols].fillna(0))
poly_names = [
f"poly_{name}" for name in poly.get_feature_names_out(top_cols)
]
poly_df = pd.DataFrame(poly_array, columns=poly_names, index=df.index)
# Drop columns that already exist in the original df
new_cols = [c for c in poly_df.columns if c not in df.columns]
df = pd.concat([df, poly_df[new_cols]], axis=1)
logger.info("Added %d polynomial features from top-%d numerical cols.", len(new_cols), _TOP_N_POLY)
except Exception as exc:
logger.warning("Polynomial feature generation failed: %s", exc)
return df, []
return df, top_cols
def add_target_encoding(
df: pd.DataFrame,
categorical_cols: list[str],
target: pd.Series,
n_splits: int = 5,
) -> pd.DataFrame:
"""Add target-encoded versions of high-cardinality categorical columns.
Uses K-fold cross-encoding to avoid target leakage: each fold's encoding
is computed from the other folds.
Args:
df: Feature DataFrame (no target column).
categorical_cols: Categorical column names to encode.
target: Target series aligned with df.
n_splits: Number of folds for cross-encoding (default 5).
Returns:
DataFrame with new `te_<col>` columns appended.
"""
if not categorical_cols:
return df
from sklearn.model_selection import KFold
df = df.copy()
global_mean = target.mean()
kf = KFold(n_splits=n_splits, shuffle=True, random_state=42)
for col in categorical_cols:
encoded = pd.Series(index=df.index, dtype=float)
for train_idx, val_idx in kf.split(df):
mapping = target.iloc[train_idx].groupby(df[col].iloc[train_idx]).mean()
encoded.iloc[val_idx] = df[col].iloc[val_idx].map(mapping).fillna(global_mean)
df[f"te_{col}"] = encoded
logger.info("Target-encoded column '%s'.", col)
return df
def run_feature_engineering(
df: pd.DataFrame,
numerical_cols: list[str],
categorical_cols: list[str],
target: pd.Series,
enable: bool = True,
poly_cols: list[str] | None = None,
) -> tuple[pd.DataFrame, list[str]]:
"""Run all optional feature engineering steps.
Args:
df: Feature DataFrame (no target column).
numerical_cols: Numerical column names.
categorical_cols: Categorical column names.
target: Target series.
enable: If False, returns df unchanged (no-op).
poly_cols: Columns to expand polynomially (inference). None means
rank by target correlation (training).
Returns:
Tuple of (augmented DataFrame, columns used for polynomial features).
"""
if not enable:
return df, []
df, used_poly_cols = add_polynomial_features(df, numerical_cols, target, poly_cols=poly_cols)
df = add_target_encoding(df, categorical_cols, target)
return df, used_poly_cols