Carbon-Lens-Senegal / feature_selection.py
Zkone's picture
Upload 3 files
2f7eb6a verified
Raw
History Blame Contribute Delete
13.2 kB
"""
Module de sélection de variables pour les données AGB.
Utilise une approche combinée : filtrage (variance, corrélation),
importance par 4 modèles, agrégation Borda, et analyse de stabilité Bootstrap.
"""
import random
import warnings
from collections import defaultdict
import numpy as np
import pandas as pd
from catboost import CatBoostRegressor
from lightgbm import LGBMRegressor, early_stopping as lgb_early_stopping, log_evaluation
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.feature_selection import VarianceThreshold
from sklearn.linear_model import Lasso, LassoCV
from sklearn.model_selection import KFold
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import RobustScaler
warnings.filterwarnings("ignore")
# Paramètres par défaut (peuvent être modifiés lors de l'appel)
DEFAULT_PARAMS = {
"target": "agb",
"n_features": 12,
"n_splits": 5,
"corr_threshold": 0.90,
"var_threshold": 0.01,
"borda_coverage": 0.80,
"max_iter": 50000,
"n_bootstrap": 50,
"stability_threshold": 0.70,
"stability_strategy": "strict", # 'strict' ou 'hybrid'
}
# Colonnes à exclure de la sélection (ne pas les utiliser comme features)
DROP_COLS = [
"inventory_date", "start_date", "end_date", "geometry",
"agb", "cagb", "cagb_category", "HCS",
"latitude", "longitude", "latitude_proj", "longitude_proj"
]
def get_importances_kfold(model_name, build_model_fn, X, y,
feature_names, n_splits=5, needs_scale_arg=False,
random_state=42):
"""
Calcule les importances par validation croisée KFold,
avec un score de rang réciproque.
"""
kf = KFold(n_splits=n_splits, shuffle=True, random_state=random_state)
importances = np.zeros(len(feature_names))
for train_idx, val_idx in kf.split(X):
X_tr, y_tr = X[train_idx], y[train_idx]
X_val, y_val = X[val_idx], y[val_idx]
model = build_model_fn()
if needs_scale_arg:
pipe = Pipeline([
("scaler", RobustScaler()),
("model", model)
])
pipe.fit(X_tr, y_tr)
fold_imp = np.abs(pipe.named_steps["model"].coef_)
elif model_name == "LightGBM":
model.fit(
X_tr, y_tr,
eval_set=[(X_val, y_val)],
callbacks=[
lgb_early_stopping(stopping_rounds=30, verbose=False),
log_evaluation(-1)
]
)
fold_imp = model.feature_importances_
elif model_name == "CatBoost":
model.fit(
X_tr, y_tr,
eval_set=(X_val, y_val),
use_best_model=True,
verbose=False
)
fold_imp = model.get_feature_importance()
else: # GradientBoosting ou autres
model.fit(X_tr, y_tr)
fold_imp = model.feature_importances_
fold_imp = fold_imp / (fold_imp.sum() + 1e-10)
ranked_idx = np.argsort(fold_imp)[::-1]
for rank_based, feat_idx in enumerate(ranked_idx):
importances[feat_idx] += 1.0 / (rank_based + 1)
return pd.Series(importances / n_splits, index=feature_names).sort_values(ascending=False)
def select_features(
input_csv_path,
output_csv_path=None,
target=DEFAULT_PARAMS["target"],
n_features=DEFAULT_PARAMS["n_features"],
n_splits=DEFAULT_PARAMS["n_splits"],
corr_threshold=DEFAULT_PARAMS["corr_threshold"],
var_threshold=DEFAULT_PARAMS["var_threshold"],
borda_coverage=DEFAULT_PARAMS["borda_coverage"],
max_iter=DEFAULT_PARAMS["max_iter"],
n_bootstrap=DEFAULT_PARAMS["n_bootstrap"],
stability_threshold=DEFAULT_PARAMS["stability_threshold"],
stability_strategy=DEFAULT_PARAMS["stability_strategy"],
drop_cols=None,
random_state=42
):
"""
Exécute la sélection de variables sur le fichier CSV d'entrée et sauvegarde
le jeu de données réduit aux features sélectionnées.
Paramètres
----------
input_csv_path : str
Chemin du fichier CSV contenant le jeu de données enrichi (avec toutes les features).
output_csv_path : str, optionnel
Chemin de sauvegarde du fichier sélectionné. Par défaut, le nom du fichier
d'entrée avec le suffixe '_selected.csv'.
target : str
Nom de la variable cible.
n_features : int
Nombre maximum de features à sélectionner.
... (autres paramètres)
drop_cols : list, optionnel
Liste des colonnes à exclure de la sélection (en plus de celles par défaut).
random_state : int
Graine aléatoire pour la reproductibilité.
Retourne
--------
pandas.DataFrame
Le DataFrame avec les colonnes sélectionnées + target.
"""
if drop_cols is None:
drop_cols = DROP_COLS.copy()
else:
# Fusionner avec la liste par défaut
drop_cols = list(set(DROP_COLS) | set(drop_cols))
if output_csv_path is None:
output_csv_path = input_csv_path.replace('.csv', '_selected.csv')
print("\n" + "="*60)
print(" FEATURE SELECTION EXECUTION")
print("="*60)
print(f"Fichier d'entrée : {input_csv_path}")
print(f"Fichier de sortie : {output_csv_path}")
# 1. Lecture
train_data = pd.read_csv(input_csv_path)
print(f"Dataset : {train_data.shape[0]} observations x {train_data.shape[1]} features")
# 2. Colonnes candidates (exclure drop_cols)
feature_cols = [c for c in train_data.columns if c not in drop_cols and c != target]
print(f"Feature candidates : {len(feature_cols)}")
X_dev = train_data[feature_cols].copy()
y_dev = train_data[target].copy()
# 3. Variance threshold
vt = VarianceThreshold(threshold=var_threshold)
X_dev_vt = vt.fit_transform(X_dev)
feature_cols_vt = np.array(feature_cols)[vt.get_support()].tolist()
print(f"After variance threshold : {len(feature_cols_vt)} features")
# 4. Correlation filter
X_dev_vt_df = pd.DataFrame(X_dev_vt, columns=feature_cols_vt)
target_corr = X_dev_vt_df.corrwith(pd.Series(np.array(y_dev), name='y')).abs()
corr_matrix = X_dev_vt_df.corr().abs()
upper = corr_matrix.where(np.triu(np.ones(corr_matrix.shape), k=1).astype(bool))
to_drop = set()
for col in upper.columns:
correlated_with = upper.index[upper[col] > corr_threshold].tolist()
for row in correlated_with:
if target_corr.get(col, 0) >= target_corr.get(row, 0):
to_drop.add(row)
else:
to_drop.add(col)
cols_to_keep = [c for c in feature_cols_vt if c not in to_drop]
col_to_idx = {col: i for i, col in enumerate(feature_cols_vt)}
keep_idx = [col_to_idx[c] for c in cols_to_keep]
X_dev_clean = X_dev_vt[:, keep_idx]
y_dev_arr = np.array(y_dev)
print(f"After correlation filter : {len(cols_to_keep)} features")
if to_drop:
print(f" Dropped ({len(to_drop)}) : {sorted(to_drop)}")
# 5. LassoCV pour trouver alpha optimal
scaler_lasso = RobustScaler()
X_dev_scaled = scaler_lasso.fit_transform(X_dev_clean)
lasso_cv = LassoCV(cv=n_splits, random_state=random_state, n_jobs=-1, max_iter=max_iter)
lasso_cv.fit(X_dev_scaled, y_dev_arr)
best_alpha = lasso_cv.alpha_
print(f"LassoCV best alpha : {best_alpha:.6f}")
# 6. Configurer les modèles pour l'importance
models_config = {
"Lasso": (
lambda: Lasso(alpha=best_alpha, max_iter=max_iter, random_state=random_state),
True
),
"LightGBM": (
lambda: LGBMRegressor(random_state=random_state, n_jobs=-1, verbose=-1),
False
),
"CatBoost": (
lambda: CatBoostRegressor(random_seed=random_state, verbose=0),
False
),
"GradientBoosting": (
lambda: GradientBoostingRegressor(random_state=random_state),
False
),
}
all_importances = {}
print(f"\nCalcul des importances ({len(models_config)} modèles) ...")
for name, (build_fn, needs_scale) in models_config.items():
try:
imp = get_importances_kfold(
model_name=name,
build_model_fn=build_fn,
X=X_dev_clean,
y=y_dev_arr,
feature_names=cols_to_keep,
n_splits=n_splits,
needs_scale_arg=needs_scale,
random_state=random_state
)
all_importances[name] = imp
except Exception as e:
print(f" Ignoré {name} : {e}")
if not all_importances:
raise RuntimeError("Aucun modèle n'a fourni d'importances. Vérifiez vos données.")
# 7. Agrégation Borda
borda_scores = defaultdict(float)
for name, imp in all_importances.items():
for rank_0based, feat in enumerate(imp.index):
borda_scores[feat] += (1.0 / (rank_0based + 1)) / len(cols_to_keep)
borda_series = pd.Series(borda_scores).sort_values(ascending=False)
# 8. Sélection automatique du nombre de features (couverture Borda)
cumulative_ratio = borda_series.cumsum() / borda_series.sum()
N_auto = int((cumulative_ratio < borda_coverage).sum()) + 1
N_final = min(N_auto, n_features)
print(f"Auto selection : {N_auto} features cover {borda_coverage:.0%} → N_final = {N_final}")
top_features = borda_series.head(N_final).index.tolist()
# 9. Bootstrap stability
rng = np.random.default_rng(random_state)
n_samples = X_dev_clean.shape[0]
selection_counts = defaultdict(int)
for b in range(n_bootstrap):
idx = rng.integers(0, n_samples, size=n_samples)
X_boot = X_dev_clean[idx]
y_boot = y_dev_arr[idx]
boot_importances = {}
for name, (build_fn, needs_scale) in models_config.items():
try:
imp = get_importances_kfold(
model_name=name,
build_model_fn=build_fn,
X=X_boot,
y=y_boot,
feature_names=cols_to_keep,
n_splits=n_splits,
needs_scale_arg=needs_scale,
random_state=random_state
)
boot_importances[name] = imp
except Exception:
pass
if not boot_importances:
continue
boot_borda = defaultdict(float)
for name, imp in boot_importances.items():
for rank_0, feat in enumerate(imp.index):
boot_borda[feat] += (1.0 / (rank_0 + 1)) / len(cols_to_keep)
boot_borda_series = pd.Series(boot_borda).sort_values(ascending=False)
selected_boot = boot_borda_series.head(N_final).index.tolist()
for feat in selected_boot:
selection_counts[feat] += 1
if (b + 1) % 10 == 0:
print(f"Bootstrap {b + 1}/{n_bootstrap} done")
stability_series = (
pd.Series(selection_counts)
.reindex(borda_series.index, fill_value=0)
.sort_values(ascending=False)
)
stability_freq = stability_series / n_bootstrap
# 10. Application du filtre de stabilité
if stability_strategy == 'strict':
top_features_final = [
f for f in top_features
if stability_freq.get(f, 0) >= stability_threshold
]
elif stability_strategy == 'hybrid':
hybrid_scores = borda_series * stability_freq.reindex(borda_series.index, fill_value=0)
hybrid_scores = hybrid_scores.sort_values(ascending=False)
top_features_final = hybrid_scores.head(N_final).index.tolist()
else:
raise ValueError(f"Unknown stability_strategy: {stability_strategy}")
# 11. Rapport final
print("\n" + "="*60)
print("RÉSULTATS DE LA SÉLECTION")
print("="*60)
print(f"Nombre de features retenues : {len(top_features_final)}")
print("Features sélectionnées :")
for i, f in enumerate(top_features_final, 1):
print(f" {i:>2}. {f} (stability = {stability_freq.get(f, 0):.0%})")
# 12. Sauvegarde du dataset filtré
train_selected = train_data[top_features_final + [target]].copy()
train_selected.to_csv(output_csv_path, index=False)
print(f"\nJeu de données sélectionné sauvegardé : {output_csv_path}")
print(f"Shape : {train_selected.shape}")
return train_selected
if __name__ == "__main__":
# Exemple d'utilisation en ligne de commande (debug)
import sys
if len(sys.argv) < 2:
print("Usage: python feature_selection.py <input_csv> [output_csv]")
sys.exit(1)
input_file = sys.argv[1]
output_file = sys.argv[2] if len(sys.argv) > 2 else None
select_features(input_file, output_file)