jaimalklyde commited on
Commit
ce01761
·
verified ·
1 Parent(s): 886e570

Update src/module/Validation_Contract.py

Browse files
Files changed (1) hide show
  1. src/module/Validation_Contract.py +281 -0
src/module/Validation_Contract.py CHANGED
@@ -0,0 +1,281 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MODULE: VALIDATION CONTRACT (ETL ENGINE)
3
+ ========================================
4
+ Responsabilité : Centraliser le schéma, extraire, nettoyer et VISUALISER l'impact du nettoyage.
5
+ Pipeline: Extract -> Transform -> Validate -> Load -> Report
6
+ """
7
+ import pandas as pd
8
+ import numpy as np
9
+ import streamlit as st
10
+ import plotly.express as px
11
+ import plotly.graph_objects as go
12
+ from datetime import datetime
13
+
14
+ class DataExplorer:
15
+ """Conteneur intelligent pour stocker les données et les métriques ETL"""
16
+
17
+ def __init__(self):
18
+ self.dataframes = {}
19
+ self.metadata = {}
20
+ self.load_timestamp = None
21
+
22
+ def add_dataframe(self, name, df, metrics):
23
+ """
24
+ Ajoute un dataframe et ses métriques de performance ETL
25
+ metrics = {
26
+ 'rows_before': int, 'rows_after': int,
27
+ 'mem_before': float, 'mem_after': float
28
+ }
29
+ """
30
+ self.dataframes[name] = df
31
+
32
+ # Calcul des métadonnées détaillées
33
+ numeric_cols = df.select_dtypes(include=[np.number]).columns.tolist()
34
+ categorical_cols = df.select_dtypes(include=['object']).columns.tolist()
35
+ date_cols = df.select_dtypes(include=['datetime64']).columns.tolist()
36
+
37
+ # Détection des anomalies si la colonne existe (spécifique à ton projet)
38
+ anomalies_count = 0
39
+ if 'dq_is_anomaly' in df.columns:
40
+ anomalies_count = int(df['dq_is_anomaly'].sum())
41
+
42
+ self.metadata[name] = {
43
+ # Métriques ETL (Avant/Après)
44
+ 'rows_before': metrics.get('rows_before', len(df)),
45
+ 'rows_after': len(df),
46
+ 'mem_before_mb': metrics.get('mem_before', 0),
47
+ 'mem_after_mb': df.memory_usage(deep=True).sum() / 1024**2,
48
+
49
+ # Structure
50
+ 'columns': len(df.columns),
51
+ 'dtypes': df.dtypes.astype(str).to_dict(),
52
+ 'null_cells': int(df.isnull().sum().sum()),
53
+ 'anomalies_detected': anomalies_count,
54
+
55
+ # Typologie
56
+ 'numeric_columns': numeric_cols,
57
+ 'categorical_columns': categorical_cols,
58
+ 'datetime_columns': date_cols
59
+ }
60
+
61
+ def get_summary(self):
62
+ """Retourne un résumé global"""
63
+ total_rows = sum(m['rows_after'] for m in self.metadata.values())
64
+ total_mem = sum(m['mem_after_mb'] for m in self.metadata.values())
65
+ return {
66
+ 'tables_loaded': len(self.dataframes),
67
+ 'total_rows': total_rows,
68
+ 'total_memory_mb': round(total_mem, 2),
69
+ 'timestamp': self.load_timestamp
70
+ }
71
+
72
+ def show_etl_report(self):
73
+ """Génère un tableau de bord visuel de la performance ETL"""
74
+ st.markdown("---")
75
+ st.markdown("### 🏗️ Rapport de Performance ETL")
76
+
77
+ if not self.metadata:
78
+ st.warning("Aucune métadonnée disponible.")
79
+ return
80
+
81
+ # Préparation des données pour les graphiques
82
+ names = []
83
+ rows_delta = []
84
+ mem_delta = []
85
+ anomalies = []
86
+
87
+ for name, meta in self.metadata.items():
88
+ names.append(name)
89
+ # Calcul du % de lignes conservées
90
+ retention = (meta['rows_after'] / meta['rows_before'] * 100) if meta['rows_before'] > 0 else 0
91
+ rows_delta.append(retention)
92
+ # Gain mémoire
93
+ gain = meta['mem_before_mb'] - meta['mem_after_mb']
94
+ mem_delta.append(gain)
95
+ anomalies.append(meta['anomalies_detected'])
96
+
97
+ col1, col2 = st.columns(2)
98
+
99
+ with col1:
100
+ # Graphique 1 : Optimisation Mémoire
101
+ fig_mem = go.Figure()
102
+ fig_mem.add_trace(go.Bar(
103
+ x=names,
104
+ y=[m['mem_before_mb'] for m in self.metadata.values()],
105
+ name='Avant Clean', marker_color='gray'
106
+ ))
107
+ fig_mem.add_trace(go.Bar(
108
+ x=names,
109
+ y=[m['mem_after_mb'] for m in self.metadata.values()],
110
+ name='Après Clean', marker_color='#00CC96'
111
+ ))
112
+ fig_mem.update_layout(title="Optimisation Mémoire (MB)", barmode='group')
113
+ st.plotly_chart(fig_mem, use_container_width=True)
114
+
115
+ with col2:
116
+ # Graphique 2 : Qualité Intrinsèque (Anomalies flagguées)
117
+ fig_qual = px.bar(
118
+ x=names, y=anomalies,
119
+ title="Lignes marquées 'Anomaly' par l'ETL",
120
+ labels={'x': 'Table', 'y': 'Nb Anomalies'},
121
+ color=anomalies, color_continuous_scale='Reds'
122
+ )
123
+ st.plotly_chart(fig_qual, use_container_width=True)
124
+
125
+ # Graphique 3 : Rétention des données
126
+ with st.expander("📊 Détails volumétrie et types"):
127
+ df_meta = pd.DataFrame.from_dict(self.metadata, orient='index')
128
+ df_display = df_meta[['rows_before', 'rows_after', 'mem_after_mb', 'null_cells', 'anomalies_detected']]
129
+ df_display.columns = ['Lignes (Brut)', 'Lignes (Clean)', 'Mémoire (MB)', 'Cellules Vides', 'Anomalies DQ']
130
+ st.dataframe(df_display, use_container_width=True)
131
+
132
+
133
+ class ETL_Engine:
134
+ """Moteur ETL garantissant le Contrat de Données"""
135
+
136
+ def __init__(self, gspread_client, sheet_name="bdd_mvp_faker"):
137
+ self.client = gspread_client
138
+ self.sheet_name = sheet_name
139
+
140
+ # --- SCHEMA CONTRACTUEL ---
141
+ self.SCHEMA = {
142
+ 'patients': {
143
+ 'numeric': ['age', 'code_postal'],
144
+ 'date': ['date_naissance'],
145
+ 'categorical': ['patient_id', 'nom', 'prenom', 'sexe', 'ville',
146
+ 'num_carte_vitale_fictif', 'num_carte_vitale_masque',
147
+ 'pathologie_frequente_code'],
148
+ 'preserve_as_is': ['dq_is_anomaly', 'dq_anomaly_type', 'dq_anomaly_detail']
149
+ },
150
+ 'sejours': {
151
+ 'numeric': ['gravite_1_5', 'duree_sejour_jours', 'consommables_estimes_score'],
152
+ 'date': ['date_entree', 'date_sortie'],
153
+ 'categorical': ['stay_id', 'patient_id', 'service_code', 'mode_entree',
154
+ 'type_lit', 'pathologie_code', 'scenario_jour',
155
+ 'equipement_principal_code'],
156
+ 'preserve_as_is': ['dq_is_anomaly', 'dq_anomaly_type', 'dq_anomaly_detail']
157
+ },
158
+ 'ressources_jour': {
159
+ 'numeric': ['intensity', 'lits_total', 'lits_occupes', 'lits_disponibles',
160
+ 'taux_occupation', 'personnel_total', 'taux_absence_conges',
161
+ 'taux_absence_scenario', 'est_periode_conges', 'personnel_disponible',
162
+ 'personnel_requis', 'tension_personnel'],
163
+ 'date': ['date'],
164
+ 'categorical': ['service_code', 'scenario', 'periode_conges_label'],
165
+ 'preserve_as_is': ['dq_is_anomaly', 'dq_anomaly_type', 'dq_anomaly_detail']
166
+ },
167
+ 'admissions_jour': {
168
+ 'numeric': ['nb_admissions', 'intensity'],
169
+ 'date': ['date'],
170
+ 'categorical': ['service_code', 'scenario']
171
+ },
172
+ 'stock_materiel_jour': {
173
+ 'numeric': ['stock_total', 'stock_disponible', 'consommation_estimee_jour',
174
+ 'tension_stock'],
175
+ 'date': ['date'],
176
+ 'categorical': ['service_code', 'materiel_code', 'alerte_stock'],
177
+ 'preserve_as_is': ['dq_is_anomaly', 'dq_anomaly_type', 'dq_anomaly_detail']
178
+ },
179
+ 'scenarios_calendrier': {
180
+ 'numeric': ['intensity'],
181
+ 'date': ['date'],
182
+ 'categorical': ['scenario']
183
+ }
184
+ }
185
+
186
+ self.REFERENCE_TABLES = [
187
+ 'service_ref', 'pathologie_ref', 'materiel_ref',
188
+ 'mode_entree_ref', 'type_lit_ref', 'calendrier_conges', 'hypotheses'
189
+ ]
190
+
191
+ def _clean_dataframe(self, name, df_raw):
192
+ """Transform: Applique le schéma et nettoie les types"""
193
+ df = df_raw.copy()
194
+ name_normalized = name.lower().strip().replace(' ', '_').replace('-', '_')
195
+
196
+ if name_normalized in self.REFERENCE_TABLES:
197
+ return df
198
+
199
+ config = self.SCHEMA.get(name_normalized)
200
+ if not config:
201
+ return self._generic_cleaning(df)
202
+
203
+ # 1. DATES
204
+ for col in config.get('date', []):
205
+ if col in df.columns:
206
+ df[col] = pd.to_datetime(df[col], errors='coerce')
207
+
208
+ # 2. NUMÉRIQUES
209
+ for col in config.get('numeric', []):
210
+ if col in df.columns:
211
+ if df[col].dtype == 'object':
212
+ df[col] = df[col].astype(str).str.replace(',', '.').str.replace(' ', '')
213
+ df[col] = df[col].replace(['Full', 'Erreur', 'full', 'erreur', ''], np.nan)
214
+ df[col] = pd.to_numeric(df[col], errors='coerce')
215
+
216
+ # 3. CATÉGORIELLES
217
+ for col in config.get('categorical', []):
218
+ if col in df.columns and df[col].dtype == 'object':
219
+ df[col] = df[col].astype(str).str.strip()
220
+ df[col] = df[col].replace(['nan', 'NaN', 'None', ''], np.nan)
221
+
222
+ return df
223
+
224
+ def _generic_cleaning(self, df):
225
+ """Nettoyage fallback"""
226
+ df_clean = df.copy()
227
+ for col in df_clean.columns:
228
+ if 'date' in col.lower():
229
+ df_clean[col] = pd.to_datetime(df_clean[col], errors='coerce')
230
+ return df_clean
231
+
232
+ def run_etl(self):
233
+ """Pipeline complet avec visualisation"""
234
+ data_explorer = DataExplorer()
235
+
236
+ try:
237
+ spreadsheet = self.client.open(self.sheet_name)
238
+ worksheets = spreadsheet.worksheets()
239
+ relevant_sheets = [ws for ws in worksheets if ws.title.lower() not in ['', 'sheet1']]
240
+
241
+ progress_bar = st.progress(0, text="🔄 ETL Engine: Extraction...")
242
+
243
+ for i, ws in enumerate(relevant_sheets):
244
+ # EXTRACT
245
+ raw_data = ws.get_all_values()
246
+ if len(raw_data) > 1:
247
+ headers = raw_data[0]
248
+ rows = raw_data[1:]
249
+ df_raw = pd.DataFrame(rows, columns=headers)
250
+
251
+ # Capture métriques AVANT
252
+ metrics = {
253
+ 'rows_before': len(df_raw),
254
+ 'mem_before': df_raw.memory_usage(deep=True).sum() / 1024**2
255
+ }
256
+
257
+ # TRANSFORM
258
+ std_name = ws.title.lower().strip().replace(' ', '_').replace('-', '_').split('.')[0]
259
+ df_clean = self._clean_dataframe(std_name, df_raw)
260
+
261
+ # LOAD
262
+ data_explorer.add_dataframe(std_name, df_clean, metrics)
263
+
264
+ progress_bar.progress((i + 1) / len(relevant_sheets))
265
+
266
+ data_explorer.load_timestamp = datetime.now()
267
+ progress_bar.empty()
268
+ st.session_state.data_explorer = data_explorer
269
+
270
+ # --- AFFICHAGE DU RAPPORT VISUEL ---
271
+ summary = data_explorer.get_summary()
272
+ st.success(f"✅ ETL Terminé : {summary['total_rows']:,} lignes traitées.")
273
+
274
+ # APPEL DU DASHBOARD VISUEL
275
+ data_explorer.show_etl_report()
276
+
277
+ return True, data_explorer
278
+
279
+ except Exception as e:
280
+ st.error(f"❌ Erreur ETL : {str(e)}")
281
+ return False, str(e)