Eric2mangel commited on
Commit
72eee83
·
verified ·
1 Parent(s): 2e35e54

Upload 4 files

Browse files

Import des fichiers

Files changed (4) hide show
  1. Dockerfile +34 -20
  2. README.md +41 -19
  3. app.py +262 -0
  4. requirements.txt +10 -3
Dockerfile CHANGED
@@ -1,20 +1,34 @@
1
- FROM python:3.13.5-slim
2
-
3
- WORKDIR /app
4
-
5
- RUN apt-get update && apt-get install -y \
6
- build-essential \
7
- curl \
8
- git \
9
- && rm -rf /var/lib/apt/lists/*
10
-
11
- COPY requirements.txt ./
12
- COPY src/ ./src/
13
-
14
- RUN pip3 install -r requirements.txt
15
-
16
- EXPOSE 8501
17
-
18
- HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health
19
-
20
- ENTRYPOINT ["streamlit", "run", "src/streamlit_app.py", "--server.port=8501", "--server.address=0.0.0.0"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # syntax = docker/dockerfile:1.4
2
+ FROM python:3.12-slim
3
+
4
+ # Installe les dépendances système nécessaires
5
+ RUN apt-get update && apt-get install -y --no-install-recommends \
6
+ gcc \
7
+ g++ \
8
+ libjpeg62-turbo-dev \
9
+ zlib1g-dev \
10
+ libpng-dev \
11
+ libfreetype6-dev \
12
+ libopenjp2-7-dev \
13
+ libtiff5-dev \
14
+ curl \
15
+ git \
16
+ && rm -rf /var/lib/apt/lists/*
17
+
18
+ WORKDIR /app
19
+
20
+ # Copie requirements en premier (meilleur cache)
21
+ COPY requirements.txt .
22
+
23
+ # Installe tout (sans cache pour réduire la taille finale)
24
+ RUN pip install --no-cache-dir -r requirements.txt
25
+
26
+ # Copie le code
27
+ COPY app.py .
28
+
29
+ # Port + commande obligatoire pour HF Spaces
30
+ EXPOSE 8501
31
+
32
+ HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health || exit 1
33
+
34
+ CMD ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0", "--server.enableCORS=false", "--server.enableXsrfProtection=false"]
README.md CHANGED
@@ -1,19 +1,41 @@
1
- ---
2
- title: Mapping Variables
3
- emoji: 🚀
4
- colorFrom: red
5
- colorTo: red
6
- sdk: docker
7
- app_port: 8501
8
- tags:
9
- - streamlit
10
- pinned: false
11
- short_description: Mapping variables with mutual information index
12
- ---
13
-
14
- # Welcome to Streamlit!
15
-
16
- Edit `/src/streamlit_app.py` to customize this app to your heart's desire. :heart:
17
-
18
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
19
- forums](https://discuss.streamlit.io).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Cartographie des variables
3
+ emoji: 🔗
4
+ colorFrom: blue
5
+ colorTo: blue
6
+ sdk: docker
7
+ app_port: 8501
8
+ pinned: false
9
+ ---
10
+
11
+ # 🔗 Cartographie des variables
12
+
13
+ Application Streamlit pour analyser les relations entre variables via l'Information Mutuelle Normalisée (IMN).
14
+
15
+ ## Fonctionnalités
16
+
17
+ - Import de jeux de données Seaborn
18
+ - Import de fichiers CSV et Excel (détection automatique du séparateur)
19
+ - Calcul de l'Information Mutuelle Normalisée entre toutes les variables
20
+ - Visualisation interactive des relations sous forme de graphe réseau
21
+ - Identification automatique des variables redondantes (doublons)
22
+ - Matrice de proximité triangulaire avec heatmap
23
+ - Seuils d'interprétation configurables
24
+
25
+ ## Interprétation de l'IMN
26
+
27
+ - **0.90 – 1.00** : Quasi-doublons (variables redondantes)
28
+ - **0.60 – 0.90** : Relation forte
29
+ - **0.30 – 0.60** : Relation modérée
30
+ - **0.10 – 0.30** : Relation faible
31
+ - **< 0.10** : Indépendance
32
+
33
+ ## Utilisation
34
+
35
+ 1. Sélectionnez une source de données (dataset Seaborn ou fichier importé)
36
+ 2. Ajustez le seuil de visibilité des liens selon vos besoins
37
+ 3. Explorez les 4 onglets :
38
+ - **Graphe interactif** : visualisation réseau des relations
39
+ - **Doublons filtrés** : variables redondantes détectées
40
+ - **Matrice triangulaire** : heatmap de toutes les relations
41
+ - **Aperçu des données** : visualisation du dataset
app.py ADDED
@@ -0,0 +1,262 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import seaborn as sns
3
+ import pandas as pd
4
+ import numpy as np
5
+ import networkx as nx
6
+ import plotly.graph_objects as go
7
+ import matplotlib.pyplot as plt
8
+ from sklearn.feature_selection import mutual_info_regression
9
+ from scipy.stats import entropy
10
+ from concurrent.futures import ThreadPoolExecutor
11
+
12
+ # ==========================================
13
+ # CONFIGURATION FACILE DU PROGRAMME
14
+ # ==========================================
15
+ GRAPH_HEIGHT = 480 # Modifiez cette valeur (en pixels) pour adapter la hauteur du graphe
16
+ # ==========================================
17
+
18
+ st.set_page_config(page_title="Analyse de proximité", page_icon="🔗", layout="wide")
19
+ st.title("🔗 Cartographie des variables")
20
+ st.subheader("Méthode : Information Mutuelle Normalisée (IMN)")
21
+
22
+ # --- SIDEBAR ---
23
+ with st.sidebar:
24
+ st.header("⚙️ Configuration")
25
+ data_source = st.radio("Source des données", ["Dataset Seaborn", "Importer un fichier"])
26
+
27
+ df = None
28
+ if data_source == "Importer un fichier":
29
+ uploaded_file = st.file_uploader("Fichier CSV ou Excel", type=["csv", "xlsx", "xls"])
30
+ if uploaded_file:
31
+ file_extension = uploaded_file.name.split('.')[-1].lower()
32
+
33
+ with st.spinner("Chargement du fichier..."):
34
+ try:
35
+ if file_extension == 'csv':
36
+ # Tentative de lecture CSV avec détection automatique du séparateur
37
+ df = pd.read_csv(uploaded_file, sep=None, engine='python', encoding_errors='ignore')
38
+ df = df.dropna()
39
+ elif file_extension in ['xlsx', 'xls']:
40
+ df = pd.read_excel(uploaded_file)
41
+ df = df.dropna()
42
+ except Exception as e:
43
+ st.error(f"Erreur lors du chargement : {str(e)}")
44
+ df = None
45
+ else:
46
+ # Sélection du dataset Seaborn
47
+ dataset_name = st.selectbox(
48
+ "Choisir un dataset",
49
+ ["titanic", "tips", "iris", "penguins", "mpg", "planets", "flights", "diamonds"]
50
+ )
51
+ try:
52
+ with st.spinner(f"Chargement du dataset {dataset_name}..."):
53
+ df = sns.load_dataset(dataset_name).dropna()
54
+ except:
55
+ st.error(f"Impossible de charger le dataset '{dataset_name}'")
56
+ df = None
57
+
58
+ if df is not None:
59
+ # Affichage de la taille du dataset
60
+ st.metric("Lignes × Colonnes", f"{len(df)} × {len(df.columns)}")
61
+
62
+ # Remplacement du slider classique par un select_slider avec vos paliers spécifiques
63
+ threshold = st.select_slider(
64
+ "Seuil de visibilité des liens (IMN)",
65
+ options=[0, 0.1, 0.3, 0.6, 0.9],
66
+ value=0.3,
67
+ help="Filtre les liens selon les paliers d'interprétation définis ci-dessous."
68
+ )
69
+
70
+ st.info("💡 **Légende de l'IMN**")
71
+ st.markdown("""
72
+ * **0.90 – 1.00** : Quasi-doublons
73
+ * **0.60 – 0.90** : Relation forte
74
+ * **0.30 – 0.60** : Relation modérée
75
+ * **0.10 – 0.30** : Relation faible
76
+ * **< 0.10** : Indépendance
77
+ """)
78
+
79
+ # --- LOGIQUE DE CALCUL ---
80
+ if df is not None:
81
+ with st.spinner("Calcul de l'information mutuelle en cours..."):
82
+ # Préparation des colonnes avec typage optimisé
83
+ df_calc = df.copy()
84
+ discrete_map = []
85
+
86
+ for col in df.columns:
87
+ if df[col].dtype == 'object' or df[col].dtype.name == 'category':
88
+ df_calc[col] = df[col].astype('category').cat.codes.astype(np.int32)
89
+ discrete_map.append(True)
90
+ else:
91
+ df_calc[col] = df_calc[col].astype(np.float32)
92
+ discrete_map.append(False)
93
+
94
+ n_vars = len(df.columns)
95
+ mi_matrix = np.zeros((n_vars, n_vars), dtype=np.float32)
96
+
97
+ # Calcul des entropies avec bins adaptés
98
+ entropies = []
99
+ for i in range(n_vars):
100
+ bins = min(10, len(df_calc.iloc[:, i].unique()))
101
+ hist = np.histogram(df_calc.iloc[:, i], bins=bins, density=True)[0]
102
+ entropies.append(entropy(hist + 1e-9))
103
+
104
+ # Fonction pour calculer une ligne de la matrice
105
+ def compute_mi_row(i):
106
+ scores = mutual_info_regression(
107
+ df_calc,
108
+ df_calc.iloc[:, i],
109
+ discrete_features=discrete_map,
110
+ random_state=42,
111
+ n_neighbors=min(3, max(1, len(df_calc) // 100))
112
+ )
113
+ return i, scores
114
+
115
+ # Calcul parallélisé de la matrice d'information mutuelle
116
+ with ThreadPoolExecutor(max_workers=4) as executor:
117
+ results = list(executor.map(compute_mi_row, range(n_vars)))
118
+
119
+ # Remplissage de la matrice et symétrisation
120
+ for i, scores in results:
121
+ for j, s in enumerate(scores):
122
+ if i == j:
123
+ mi_matrix[i, j] = 1.0
124
+ else:
125
+ h_min = min(entropies[i], entropies[j])
126
+ nmi = s / h_min if h_min > 0 else 0
127
+ mi_matrix[i, j] = min(max(nmi, 0), 1.0)
128
+
129
+ # Symétrisation de la matrice (moyenne des deux directions)
130
+ mi_matrix = (mi_matrix + mi_matrix.T) / 2
131
+ np.fill_diagonal(mi_matrix, 1.0)
132
+
133
+ to_keep = []
134
+ redundant_pairs = []
135
+ seen = set()
136
+
137
+ for i in range(n_vars):
138
+ if i in seen: continue
139
+ for j in range(i + 1, n_vars):
140
+ val_im = mi_matrix[i, j]
141
+ if val_im >= 0.99:
142
+ seen.add(j)
143
+ redundant_pairs.append({
144
+ "Variable conservée": df.columns[i],
145
+ "Doublon supprimé": df.columns[j],
146
+ "Score IMN": f"{val_im:.4f}"
147
+ })
148
+ to_keep.append(i)
149
+
150
+ final_vars = [df.columns[i] for i in to_keep]
151
+ final_mi = mi_matrix[np.ix_(to_keep, to_keep)]
152
+
153
+ G = nx.Graph()
154
+ for i in range(len(final_vars)):
155
+ for j in range(i + 1, len(final_vars)):
156
+ im_val = final_mi[i, j]
157
+ if im_val > threshold:
158
+ G.add_edge(final_vars[i], final_vars[j], weight=float(im_val))
159
+ if final_vars[i] not in G:
160
+ G.add_node(final_vars[i])
161
+
162
+ pos = nx.spring_layout(G, k=1.2, seed=42)
163
+
164
+ node_hover_texts = []
165
+ for node in G.nodes():
166
+ hover_text = f"<b>Variable : {node}</b><br><br>Liens (IMN > {threshold}):<br>"
167
+ neighbors = G.edges(node, data=True)
168
+ sorted_neighbors = sorted(neighbors, key=lambda x: x[2]['weight'], reverse=True)
169
+
170
+ if not sorted_neighbors:
171
+ hover_text += "<i>Aucun lien significatif</i>"
172
+ else:
173
+ for _, neighbor, data in sorted_neighbors:
174
+ hover_text += f"• {neighbor} : <b>{data['weight']:.4f}</b><br>"
175
+ node_hover_texts.append(hover_text)
176
+
177
+ # --- AFFICHAGE ---
178
+ tab1, tab2, tab3, tab4 = st.tabs(["📊 Graphe interactif", "👯 Doublons filtrés", "📋 Matrice triangulaire", "📄 Aperçu des données"])
179
+
180
+ with tab1:
181
+ edge_traces = []
182
+ for edge in G.edges(data=True):
183
+ x0, y0 = pos[edge[0]]
184
+ x1, y1 = pos[edge[1]]
185
+ w = edge[2]['weight']
186
+ color = f'rgba({int(255*w)}, {int(150*(1-w))}, {int(200*(1-w))}, {0.3 + 0.4*w})'
187
+
188
+ edge_traces.append(go.Scatter(
189
+ x=[x0, x1, None], y=[y0, y1, None],
190
+ line=dict(width=w*12, color=color),
191
+ hoverinfo='none',
192
+ mode='lines'
193
+ ))
194
+
195
+ node_trace = go.Scatter(
196
+ x=[pos[n][0] for n in G.nodes()],
197
+ y=[pos[n][1] for n in G.nodes()],
198
+ mode='markers+text',
199
+ text=list(G.nodes()),
200
+ textposition="bottom center",
201
+ textfont=dict(color='white', size=11),
202
+ marker=dict(
203
+ size=[10 + G.degree(n) * 5 for n in G.nodes()],
204
+ color='#1f77b4',
205
+ line=dict(width=2, color='white'),
206
+ opacity=1
207
+ ),
208
+ hoverinfo='text',
209
+ hovertext=node_hover_texts
210
+ )
211
+
212
+ fig = go.Figure(data=edge_traces + [node_trace])
213
+ fig.update_layout(
214
+ paper_bgcolor='rgba(15,15,25,1)',
215
+ plot_bgcolor='rgba(0,0,0,0)',
216
+ height=GRAPH_HEIGHT,
217
+ showlegend=False,
218
+ xaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
219
+ yaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
220
+ margin=dict(b=20, l=5, r=5, t=40),
221
+ hoverlabel=dict(bgcolor="rgba(30, 30, 50, 0.9)", font_size=13)
222
+ )
223
+ st.plotly_chart(fig, use_container_width=True)
224
+
225
+ with tab2:
226
+ st.subheader("Variables supprimées (Redondance forte)")
227
+ if redundant_pairs:
228
+ st.dataframe(pd.DataFrame(redundant_pairs), use_container_width=True)
229
+ else:
230
+ st.info("Aucun doublon détecté.")
231
+
232
+ with tab3:
233
+ st.subheader("Matrice de proximité")
234
+ df_imn = pd.DataFrame(final_mi, index=final_vars, columns=final_vars)
235
+ mask = np.triu(np.ones_like(df_imn, dtype=bool))
236
+
237
+ fig_map, ax = plt.subplots(figsize=(10, 8), layout="constrained")
238
+ fig_map.patch.set_facecolor('white')
239
+
240
+ sns.heatmap(df_imn, mask=mask, cmap="coolwarm", vmax=1.0, vmin=0,
241
+ annot=True, fmt=".2f", square=True, linewidths=.5,
242
+ cbar_kws={"shrink": .8}, ax=ax, annot_kws={"size": 9})
243
+
244
+ plt.xticks(rotation=45, ha='right', color='black')
245
+ plt.yticks(rotation=0, color='black')
246
+ ax.set_facecolor('white')
247
+
248
+ st.pyplot(fig_map)
249
+
250
+ with tab4:
251
+ st.subheader("Aperçu des données (20 premières lignes)")
252
+ st.dataframe(df.head(20), use_container_width=True)
253
+ else:
254
+ st.info("👈 Veuillez sélectionner ou importer un jeu de données.")
255
+
256
+ # Footer
257
+ st.markdown("---")
258
+ st.markdown("""
259
+ <div style='text-align: center; color: gray;'>
260
+ <small>L'Information Mutuelle Normalisée (IMN) mesure la dépendance entre variables (0 = indépendance, 1 = dépendance totale)</small>
261
+ </div>
262
+ """, unsafe_allow_html=True)
requirements.txt CHANGED
@@ -1,3 +1,10 @@
1
- altair
2
- pandas
3
- streamlit
 
 
 
 
 
 
 
 
1
+ streamlit==1.40.2
2
+ pandas==2.2.3
3
+ numpy==2.2.1
4
+ seaborn==0.13.2
5
+ matplotlib==3.10.0
6
+ scipy==1.15.1
7
+ scikit-learn==1.6.1
8
+ networkx==3.4.2
9
+ plotly==5.24.1
10
+ openpyxl==3.1.5