Eric2mangel's picture
Update app.py
3c31915 verified
import streamlit as st
import seaborn as sns
import pandas as pd
import numpy as np
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.compose import ColumnTransformer
from sklearn.linear_model import LinearRegression, LogisticRegression
from sklearn.feature_selection import mutual_info_regression, mutual_info_classif
from sklearn.metrics import r2_score, accuracy_score
from sklearn.model_selection import train_test_split
#from scipy.stats import pearsonr
from scipy.stats import spearmanr
# ------------------------------------------------------------
# Configuration Globale
# ------------------------------------------------------------
TEST_SIZE = 0.3
RANDOM_STATE = 42
st.set_page_config(page_title="Analyse d'importance", layout="wide")
st.title("🔍 Analyse de l'importance des caractéristiques")
st.markdown(
"""
Cette application illustre la différence entre la pertinence marginale et la pertinence conditionnelle d'une caractéristique.
- Pertinence marginale : corrélation ou information mutuelle avec la cible.
- Pertinence conditionnelle : valeur ajoutée d'une variable excluant les redondances après contrôle.
"""
)
# ------------------------------------------------------------
# Sidebar: Dataset et Importation
# ------------------------------------------------------------
with st.sidebar:
st.header("⚙️ Configuration")
# Choix de la source de données
data_source = st.radio(
"Source des données",
["Jeu de données Seaborn", "Importer un fichier"],
label_visibility="visible"
)
df = None
if data_source == "Importer un fichier":
uploaded_file = st.file_uploader("Importer un fichier CSV", type=["csv"])
if uploaded_file is not None:
try:
df = pd.read_csv(uploaded_file, sep=None, engine='python')
# Seuil de valeurs manquantes (configurable)
missing_threshold = st.slider(
"Seuil max de valeurs manquantes (%)",
min_value=0,
max_value=100,
value=50,
help="Les colonnes avec plus de X% de valeurs manquantes seront supprimées"
)
# Calcul du pourcentage de valeurs manquantes par colonne
missing_pct = (df.isnull().sum() / len(df)) * 100
cols_to_drop = missing_pct[missing_pct > missing_threshold].index.tolist()
if cols_to_drop:
st.info(f"ℹ️ {len(cols_to_drop)} colonne(s) supprimée(s) (>{missing_threshold}% manquantes) : {', '.join(cols_to_drop)}")
df = df.drop(columns=cols_to_drop)
# Suppression des lignes avec valeurs manquantes restantes
df = df.dropna()
if len(df) == 0:
st.error("❌ Aucune donnée après nettoyage. Essayez d'augmenter le seuil de valeurs manquantes.")
df = None
else:
st.success(f"✅ Fichier CSV chargé ! ({len(df)} lignes, {len(df.columns)} colonnes)")
except Exception as e:
st.error(f"Erreur : {e}")
df = None
else:
excluded_datasets = ['anagrams', 'anscombe', 'attention', 'brain_networks',
'car_crashes', 'dowjones','diamonds','flights','geyser',
'planets','seaice']
available_datasets = [d for d in sorted(sns.get_dataset_names()) if d not in excluded_datasets]
default_dataset = "iris"
default_index = available_datasets.index(default_dataset) if default_dataset in available_datasets else 0
dataset_name = st.selectbox(
"Dataset d'exemple",
available_datasets,
index=default_index
)
#dataset_name = st.selectbox("Dataset d'exemple", available_datasets)
try:
df = sns.load_dataset(dataset_name)
df = df.dropna()
st.success(f"✅ Jeu '{dataset_name}' chargé")
except Exception as e:
st.error(f"Erreur : {e}")
df = None
if df is not None:
target = st.selectbox("Sélection cible (Y)", df.columns)
y = df[target]
X = df.drop(columns=[target])
# Vérification que X n'est pas vide après suppression de la cible
if len(X.columns) == 0:
st.warning("⚠️ Aucune variable disponible après sélection de la cible.")
X = None
y = None
task = None
else:
task = "Regression" if (y.dtype.kind in "ifu" and y.nunique() > 10) else "Classification"
excluded_features = st.multiselect("Variables à exclure :", X.columns.tolist(), default=[])
if excluded_features:
X = X.drop(columns=excluded_features)
# Vérification après exclusion
if len(X.columns) == 0:
st.error("❌ Vous avez exclu toutes les variables ! Veuillez en garder au moins une.")
X = None
y = None
task = None
else:
st.info("👈 Veuillez sélectionner ou importer un jeu de données.")
X = None
y = None
task = None
# ------------------------------------------------------------
# Onglets
# ------------------------------------------------------------
if df is not None and X is not None and len(X.columns) > 0:
tab1, tab2, tab3 = st.tabs(["📊 Analyse d'Importance", "📋 Données Brutes", "🔧 Types"])
with tab2:
st.dataframe(df.head(20), use_container_width=True)
with tab3:
st.header("Types des variables")
num_cols = X.select_dtypes(include=[np.number]).columns.tolist()
cat_cols = X.select_dtypes(exclude=[np.number]).columns.tolist()
col1, col2 = st.columns(2)
with col1:
st.subheader("Numériques")
for col in num_cols or ["None"]:
st.write(f"- {col}")
with col2:
st.subheader("Catégorielles")
for col in cat_cols or ["None"]:
st.write(f"- {col}")
# ------------------------------------------------------------
# Analyse Principale (Tab 1)
# ------------------------------------------------------------
with tab1:
if len(X.columns) > 0:
try:
num_cols = X.select_dtypes(include=[np.number]).columns.tolist()
cat_cols = X.select_dtypes(exclude=[np.number]).columns.tolist()
# Vérification qu'il y a au moins une variable
if len(num_cols) == 0 and len(cat_cols) == 0:
st.warning("⚠️ Aucune variable disponible pour l'analyse. Veuillez ne pas tout exclure.")
st.stop()
# Construction du préprocesseur seulement avec les colonnes qui existent
transformers = []
if num_cols:
transformers.append(("num", StandardScaler(), num_cols))
if cat_cols:
transformers.append(("cat", OneHotEncoder(drop="first", handle_unknown="ignore", sparse_output=False), cat_cols))
if not transformers:
st.warning("⚠️ Aucune colonne à traiter.")
st.stop()
preprocess = ColumnTransformer(transformers=transformers)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=TEST_SIZE, random_state=RANDOM_STATE)
# Vérification qu'il y a assez de données pour le split
if len(X_train) == 0 or len(X_test) == 0:
st.error("❌ Pas assez de données pour créer les ensembles d'entraînement et de test.")
st.info(f"Données disponibles : {len(X)} lignes. Minimum requis : 2 lignes.")
st.stop()
X_train_proc = preprocess.fit_transform(X_train)
# Vérification que les données transformées ne sont pas vides
if X_train_proc.shape[0] == 0 or X_train_proc.shape[1] == 0:
st.error("❌ Erreur : Les données transformées sont vides.")
st.info(f"Shape après transformation : {X_train_proc.shape}")
st.info(f"Variables numériques : {num_cols}")
st.info(f"Variables catégorielles : {cat_cols}")
st.stop()
feature_names = preprocess.get_feature_names_out()
model = LinearRegression() if task == "Regression" else LogisticRegression(max_iter=1000)
model.fit(X_train_proc, y_train)
y_pred = model.predict(preprocess.transform(X_test))
perf = r2_score(y_test, y_pred) if task == "Regression" else accuracy_score(y_test, y_pred)
st.subheader("📊 Pertinence marginale vs conditionnelle")
st.markdown(f"**🎯 Performance globale : {perf:.2f} ({'R²' if task == 'Regression' else 'Précision'})**")
# Métriques
mi = mutual_info_regression(X_train_proc, y_train, random_state=0) if task == "Regression" else mutual_info_classif(X_train_proc, y_train, random_state=0)
coefs = model.coef_.ravel() if task == "Regression" else model.coef_[0]
res = pd.DataFrame({
"Variable": feature_names,
"Importance seule (MI)": mi,
"Poids dans le modèle": np.abs(coefs),
"Sens": np.where(coefs > 0, "+", "-")
})
#if task == "Regression":
# res["Lien direct (Corr)"] = [pearsonr(X_train_proc[:, i], y_train)[0] for i in range(len(feature_names))]
if task == "Regression":
res["Lien direct (Corr)"] = [spearmanr(X_train_proc[:, i], y_train)[0] for i in range(len(feature_names))]
# Normalisation pour Score Synthétique
def normalize(s): return (s - s.min()) / (s.max() - s.min() + 1e-10)
mi_n = normalize(res["Importance seule (MI)"])
poids_n = normalize(res["Poids dans le modèle"])
if task == "Regression":
corr_n = normalize(res["Lien direct (Corr)"].abs())
res["Score synthétique"] = ((mi_n + corr_n) / 2 + poids_n) / 2
else:
res["Score synthétique"] = (mi_n + poids_n) / 2
res = res.sort_values("Score synthétique", ascending=False)
# Réorganisation des colonnes
cols = ["Variable", "Score synthétique", "Importance seule (MI)", "Poids dans le modèle", "Sens"]
if task == "Regression":
cols = ["Variable", "Score synthétique", "Importance seule (MI)", "Lien direct (Corr)", "Poids dans le modèle", "Sens"]
final_df = res[cols].copy()
# --- STYLISATION ET AFFICHAGE ---
# 1. Préparation du style pour la colonne Sens (couleurs)
def style_sign(val):
color = 'color: #2ecc71;' if val == '+' else 'color: #e74c3c;'
return f'{color} font-weight: bold; font-size: 20px;'
# 2. Application du formatage (2 décimales) et des gradients
num_cols_to_style = [c for c in cols if c not in ["Variable", "Sens", "Score synthétique"]]
styled_res = (final_df.style
.format({c: "{:.2f}" for c in cols if c not in ["Variable", "Sens"]})
.background_gradient(subset=num_cols_to_style, cmap="RdYlGn")
.map(style_sign, subset=['Sens'])
)
# 3. Affichage avec st.data_editor pour fixer la hauteur (6 lignes env = 250px)
st.data_editor(
styled_res,
use_container_width=True,
height=250, # Limite la hauteur avec scrollbar
hide_index=True,
disabled=True, # Empêche l'édition, agit comme un dataframe
column_config={
"Sens": st.column_config.Column(
"Sens",
help="Direction de l'influence",
width="small"
)
}
)
st.subheader("📖 Guide de lecture")
st.markdown(
"""
- **Score synthétique** : Note globale d'importance.
- **Importance seule (MI)** : Mesure la dépendance globale entre la variable et la cible. Contrairement à la corrélation qui ne voit que les lignes droites, l'Information Mutuelle détecte toutes les formes de relations (courbes, motifs complexes, etc.). Elle indique quelle quantité d'information "pure" cette variable partage avec la cible, sans tenir compte des autres variables.
- **Poids dans le modèle** : Contribution finale au modèle.
- **Sens (+) / (-)** : Direction de l'impact sur la cible.
"""
)
except ValueError as e:
if "Found array with 0 sample(s)" in str(e) or "shape=(0," in str(e):
st.error("❌ Erreur d'analyse : données insuffisantes ou incompatibles")
st.warning("⚠️ Vérifiez que :")
st.markdown("""
- Vous n'avez pas exclu toutes les variables
- La variable cible choisie est appropriée (elle ne doit pas être identique à une variable prédictive)
- Il reste suffisamment de données après nettoyage
- Les variables ont suffisamment de variance
""")
else:
st.error(f"❌ Erreur : {str(e)}")
except Exception as e:
st.error(f"❌ Une erreur s'est produite lors de l'analyse")
st.warning(f"Détails : {str(e)}")
st.info("💡 Essayez de changer de variable cible ou de variables prédictives.")
else:
st.info("ℹ️ Veuillez sélectionner au moins une variable.")
else:
st.info("👈 Veuillez sélectionner ou importer un jeu de données pour commencer l'analyse.")