File size: 15,058 Bytes
1cc6897
 
 
 
 
 
 
 
 
 
 
8387489
 
1cc6897
 
 
 
 
 
 
2c80a75
1cc6897
2c80a75
1cc6897
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25f5b6f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1cc6897
25f5b6f
 
 
 
 
 
1cc6897
 
 
 
 
 
 
 
3c31915
 
 
 
 
 
 
 
1cc6897
 
 
 
 
 
 
 
 
 
 
 
 
25f5b6f
 
 
ae39dfb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1cc6897
 
 
 
 
 
 
 
 
ae39dfb
1cc6897
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ae39dfb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1cc6897
ae39dfb
 
 
 
 
 
 
2c80a75
ae39dfb
 
 
8387489
 
 
ae39dfb
8387489
ae39dfb
 
 
 
2c80a75
1cc6897
8387489
 
ae39dfb
2c80a75
ae39dfb
 
 
1cc6897
ae39dfb
1cc6897
ae39dfb
2c80a75
ae39dfb
2c80a75
ae39dfb
 
1cc6897
ae39dfb
 
 
 
 
1cc6897
ae39dfb
 
 
 
 
 
 
 
1cc6897
ae39dfb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1cc6897
ae39dfb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1cc6897
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
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.")