agritech-api / src /modeling /interpretation.py
github-actions
Deploy API from GitHub Actions
42dfbed
Raw
History Blame Contribute Delete
13.8 kB
""" Module d'interprétation des modèles de machine learning.
Ce module contient des fonctions pour :
- récupérer les importances natives des features d'un modèle tree-based
- regrouper les importances par variable métier
- calculer la permutation importance sur le jeu de test
- analyser les erreurs de prédiction (résidus) en log et en valeurs réelles
- sauvegarder tous les artefacts du modèle (CSV, métriques, graphiques) de manière organisée
"""
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import json
from sklearn.inspection import permutation_importance
from sklearn.metrics import mean_absolute_error, root_mean_squared_error, r2_score
def get_feature_importance(pipeline, top_n=20, plot=True):
"""
Récupère les importances natives des features d'un modèle tree-based.
args:
pipeline : sklearn.pipeline.Pipeline, modèle entraîné avec un préprocesseur intégré
top_n : int, nombre de features à afficher
plot : bool, afficher le graphique ou pas
returns:
feature_importance_df : pd.DataFrame, importance de chaque feature
"""
preprocessor = pipeline.named_steps["preprocessor"]
model = pipeline.named_steps["model"]
feature_names = preprocessor.get_feature_names_out()
importances = model.feature_importances_
feature_importance_df = pd.DataFrame({
"feature": feature_names,
"importance": importances
}).sort_values(by="importance", ascending=False)
top_features = feature_importance_df.head(top_n)
if plot:
plt.figure(figsize=(10, 6))
sns.barplot(
data=top_features,
x="importance",
y="feature"
)
plt.title(f"Top {top_n} - Feature importance native")
plt.xlabel("Importance")
plt.ylabel("Variable")
plt.tight_layout()
plt.show()
return feature_importance_df
def simplify_feature_name(feature_name):
"""
Regroupe les variables transformées vers leur variable métier d'origine.
Par exemple :
- item_1, item_2, item_3 -> item
- area_1, area_2, area_3 -> area
args:
feature_name : str, nom de la feature transformée
returns:
str, nom de la variable métier d'origine
"""
feature_name = feature_name.replace("num__", "").replace("cat__", "")
if feature_name.startswith("item_"):
return "item"
if feature_name.startswith("area_"):
return "area"
return feature_name
def get_grouped_feature_importance(feature_importance_df, top_n=20, plot=True):
"""
Regroupe les importances par variable métier.
Par exemple :
- item_1, item_2, item_3 -> item
- area_1, area_2, area_3 -> area
Calcule l'importance cumulée pour chaque variable métier.
Trie par importance décroissante.
Affiche un graphique des variables métier les plus importantes.
args:
feature_importance_df : pd.DataFrame, importance de chaque feature transformée
top_n : int, nombre de variables métier à afficher
plot : bool, afficher le graphique ou pas
returns:
grouped_importance_df : pd.DataFrame, importance cumulée par variable métier
"""
grouped_df = feature_importance_df.copy()
grouped_df["feature_group"] = grouped_df["feature"].apply(simplify_feature_name)
grouped_df = (
grouped_df
.groupby("feature_group", as_index=False)["importance"]
.sum()
.sort_values(by="importance", ascending=False)
)
top_grouped = grouped_df.head(top_n)
if plot:
plt.figure(figsize=(10, 6))
sns.barplot(
data=top_grouped,
x="importance",
y="feature_group"
)
plt.title(f"Top {top_n} - Variables métier les plus importantes")
plt.xlabel("Importance cumulée")
plt.ylabel("Variable métier")
plt.tight_layout()
plt.show()
return grouped_df
def get_permutation_importance(
pipeline,
X_test,
y_test,
n_repeats=10,
random_state=42,
top_n=20,
scoring="neg_root_mean_squared_error",
plot=True
):
"""
Calcule la permutation importance sur le jeu de test.
Mesure la perte de performance quand une variable est mélangée.
Affiche un graphique des variables les plus importantes par permutation.
args:
pipeline : sklearn.Pipeline, modèle entraîné
X_test : pd.DataFrame, données de test
y_test : pd.Series, valeurs cibles de test
n_repeats : int, nombre de répétitions pour la permutation
random_state : int, graine pour la reproductibilité
top_n : int, nombre de variables à afficher
scoring : str, métrique de performance
plot : bool, afficher le graphique ou pas
returns:
permutation_df : pd.DataFrame, importance par permutation
"""
result = permutation_importance(
pipeline,
X_test,
y_test,
n_repeats=n_repeats,
random_state=random_state,
scoring=scoring,
n_jobs=-1
)
permutation_df = pd.DataFrame({
"feature": X_test.columns,
"importance_mean": result.importances_mean,
"importance_std": result.importances_std
}).sort_values(by="importance_mean", ascending=False)
top_features = permutation_df.head(top_n).copy()
if plot:
top_features_plot = top_features.sort_values(
by="importance_mean",
ascending=True
)
plt.figure(figsize=(10, 6))
plt.barh(
y=top_features_plot["feature"],
width=top_features_plot["importance_mean"],
xerr=top_features_plot["importance_std"]
)
plt.title(f"Top {top_n} - Permutation importance")
plt.xlabel("Impact moyen sur la performance")
plt.ylabel("Variable")
plt.tight_layout()
plt.show()
return permutation_df
def analyze_residuals(pipeline, X_test, y_test, plot=True):
"""
Analyse les erreurs de prédiction :
- en log, cohérent avec l'entraînement
- en valeurs réelles, utile pour l'interprétation métier
Affiche des graphiques d'analyse des résidus.
args:
pipeline : sklearn.Pipeline, modèle entraîné
X_test : pd.DataFrame, données de test
y_test : pd.Series, valeurs cibles de test
plot : bool, afficher les graphiques ou pas
returns:
residuals_df : pd.DataFrame, résidus et valeurs associées
metrics : dict, métriques d'évaluation des résidus
"""
# Prédictions en log
y_pred_log = pipeline.predict(X_test)
y_test_log = pd.Series(y_test).reset_index(drop=True)
y_pred_log = pd.Series(y_pred_log).reset_index(drop=True)
residuals_log = y_test_log - y_pred_log
# Conversion en valeurs réelles
y_test_real = np.expm1(y_test_log)
y_pred_real = np.expm1(y_pred_log)
residuals_real = y_test_real - y_pred_real
residuals_df = pd.DataFrame({
"y_true_log": y_test_log,
"y_pred_log": y_pred_log,
"residual_log": residuals_log,
"abs_residual_log": np.abs(residuals_log),
"y_true_real": y_test_real,
"y_pred_real": y_pred_real,
"residual_real": residuals_real,
"abs_residual_real": np.abs(residuals_real),
})
metrics = {
"mae_log": mean_absolute_error(y_test_log, y_pred_log),
"rmse_log": root_mean_squared_error(y_test_log, y_pred_log),
"r2_log": r2_score(y_test_log, y_pred_log),
"mae_real": mean_absolute_error(y_test_real, y_pred_real),
"rmse_real": root_mean_squared_error(y_test_real, y_pred_real),
"r2_real": r2_score(y_test_real, y_pred_real),
}
if plot:
# Résidus en log
plt.figure(figsize=(8, 5))
sns.scatterplot(
data=residuals_df,
x="y_pred_log",
y="residual_log"
)
plt.axhline(0, color="black", linestyle="--")
plt.title("Analyse des résidus - échelle log")
plt.xlabel("Valeurs prédites (log)")
plt.ylabel("Erreur de prédiction (log)")
plt.tight_layout()
plt.show()
plt.figure(figsize=(8, 5))
sns.histplot(residuals_df["residual_log"], kde=True)
plt.title("Distribution des erreurs - échelle log")
plt.xlabel("Erreur (log)")
plt.tight_layout()
plt.show()
# Résidus en réel
plt.figure(figsize=(8, 5))
sns.scatterplot(
data=residuals_df,
x="y_pred_real",
y="residual_real"
)
plt.axhline(0, color="black", linestyle="--")
plt.title("Analyse des résidus - valeurs réelles")
plt.xlabel("Valeurs prédites")
plt.ylabel("Erreur de prédiction")
plt.tight_layout()
plt.show()
plt.figure(figsize=(8, 5))
sns.histplot(residuals_df["residual_real"], kde=True)
plt.title("Distribution des erreurs - valeurs réelles")
plt.xlabel("Erreur")
plt.tight_layout()
plt.show()
return residuals_df, metrics
def save_model_artifacts(
base_path,
feature_importance_df,
grouped_importance_df,
permutation_df,
residuals_df,
residual_metrics,
final_model_df=None,
top_n=20
):
"""
Sauvegarde tous les artefacts du modèle :
- CSV
- métriques
- graphiques
Les artefacts sont organisés dans des dossiers "tables" et "figures" sous le dossier racine.
Les graphiques sont enregistrés au format PNG avec une résolution de 300 dpi.
Les CSV contiennent les données d'importance des features, les résidus et la comparaison des modèles finaux.
Les métriques sont sauvegardées dans un fichier JSON pour une utilisation ultérieure.
Args:
base_path (Path): dossier racine des artefacts
feature_importance_df (pd.DataFrame): importance native des features
grouped_importance_df (pd.DataFrame): importance regroupée par variable métier
permutation_df (pd.DataFrame): importance par permutation
residuals_df (pd.DataFrame): résidus et valeurs associées
residual_metrics (dict): métriques d'évaluation des résidus
final_model_df (pd.DataFrame, optional): comparaison des modèles finaux
top_n (int, optional): nombre de features à afficher dans les graphiques (par défaut 20)
"""
tables_dir = base_path / "tables"
figures_dir = base_path / "figures"
tables_dir.mkdir(parents=True, exist_ok=True)
figures_dir.mkdir(parents=True, exist_ok=True)
# =====================
# CSV
# =====================
feature_importance_df.to_csv(
tables_dir / "feature_importance_native.csv",
index=False
)
grouped_importance_df.to_csv(
tables_dir / "feature_importance_grouped.csv",
index=False
)
permutation_df.to_csv(
tables_dir / "permutation_importance.csv",
index=False
)
residuals_df.to_csv(
tables_dir / "residuals.csv",
index=False
)
if final_model_df is not None:
final_model_df.drop(columns=["trained_pipeline"], errors="ignore").to_csv(
tables_dir / "final_model_comparison.csv",
index=False
)
# Metrics
with open(tables_dir / "metrics.json", "w", encoding="utf-8") as f:
json.dump(
{k: float(v) for k, v in residual_metrics.items()},
f,
indent=4
)
# =====================
# GRAPHIQUES
# =====================
# Feature importance
top_features = feature_importance_df.head(top_n)
plt.figure(figsize=(10, 6))
sns.barplot(
data=top_features,
x="importance",
y="feature"
)
plt.title(f"Top {top_n} - Feature importance native")
plt.tight_layout()
plt.savefig(figures_dir / "feature_importance_native.png", dpi=300)
plt.close()
# Feature importance groupée
top_grouped = grouped_importance_df.head(10)
plt.figure(figsize=(10, 6))
sns.barplot(
data=top_grouped,
x="importance",
y="feature_group"
)
plt.title("Feature importance métier")
plt.tight_layout()
plt.savefig(figures_dir / "feature_importance_grouped.png", dpi=300)
plt.close()
# Permutation importance
top_perm = permutation_df.head(10).sort_values(
by="importance_mean",
ascending=True
)
plt.figure(figsize=(10, 6))
plt.barh(
y=top_perm["feature"],
width=top_perm["importance_mean"],
xerr=top_perm["importance_std"]
)
plt.title("Permutation importance")
plt.tight_layout()
plt.savefig(figures_dir / "permutation_importance.png", dpi=300)
plt.close()
# Résidus log
if "y_pred_log" in residuals_df.columns:
plt.figure(figsize=(8, 5))
sns.scatterplot(
data=residuals_df,
x="y_pred_log",
y="residual_log"
)
plt.axhline(0, linestyle="--")
plt.title("Résidus - log")
plt.tight_layout()
plt.savefig(figures_dir / "residuals_log.png", dpi=300)
plt.close()
# Résidus réel
if "y_pred_real" in residuals_df.columns:
plt.figure(figsize=(8, 5))
sns.scatterplot(
data=residuals_df,
x="y_pred_real",
y="residual_real"
)
plt.axhline(0, linestyle="--")
plt.title("Résidus - réel")
plt.tight_layout()
plt.savefig(figures_dir / "residuals_real.png", dpi=300)
plt.close()
print(f"Artefacts sauvegardés dans : {base_path}")