MMOON commited on
Commit
c0fbdb9
·
verified ·
1 Parent(s): 61fb759

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +512 -82
app.py CHANGED
@@ -1,113 +1,543 @@
1
- import streamlit as st
 
 
 
 
 
2
  import pandas as pd
3
  import requests
4
- from datetime import datetime
 
 
 
 
 
5
 
6
- st.set_page_config(page_title="Test Pesticides API", page_icon="🌿", layout="wide")
 
7
 
8
- def test_api():
9
- """Test simple de l'API"""
10
- BASE_URL = "https://api.datalake.sante.service.ec.europa.eu/sante/pesticides"
 
 
 
 
 
 
 
 
 
 
11
 
12
- # Test 1: Récupérer quelques produits
13
- st.write("### Test 1: Récupération des produits")
14
- try:
15
- url = f"{BASE_URL}/pesticide_residues_products"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  params = {
17
  'format': 'json',
18
- 'language': 'FR',
19
  'api-version': 'v2.0'
20
  }
21
- response = requests.get(url, params=params, timeout=30)
 
 
 
22
  response.raise_for_status()
23
  data = response.json()
24
 
25
  if 'value' in data:
26
- products = data['value'][:5] # Prendre seulement 5 produits
27
- st.success(f"✅ Récupéré {len(data['value'])} produits")
28
- st.write("Exemples de produits:")
29
- for p in products:
30
- st.write(f"- {p.get('product_name', 'N/A')} (ID: {p.get('product_id', 'N/A')})")
 
 
 
 
 
31
  else:
32
- st.error("Format de données inattendu")
33
- st.json(data)
34
- except Exception as e:
35
- st.error(f"Erreur: {e}")
 
 
 
 
 
36
 
37
- # Test 2: Télécharger les substances
38
- st.write("### Test 2: Téléchargement des substances")
39
- try:
40
- url = f"{BASE_URL}/active_substances/download"
41
- params = {
42
- 'format': 'json',
43
- 'api-version': 'v2.0'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  }
45
- response = requests.get(url, params=params, timeout=60)
46
- response.raise_for_status()
47
- substances = response.json()
48
 
49
- if isinstance(substances, list):
50
- st.success(f"✅ Téléchargé {len(substances)} substances")
51
- # Afficher quelques exemples
52
- for s in substances[:5]:
53
- st.write(f"- {s.get('substance_name', 'N/A')} (ID: {s.get('substance_id', 'N/A')})")
54
- else:
55
- st.warning("Format inattendu pour les substances")
56
- st.json(substances)
57
- except Exception as e:
58
- st.error(f"Erreur: {e}")
 
 
 
 
 
 
 
 
 
 
59
 
60
- # Test 3: Télécharger un échantillon de LMR
61
- st.write("### Test 3: Téléchargement des LMR")
62
- try:
63
- url = f"{BASE_URL}/pesticide_residues_mrls"
64
- params = {
65
- 'format': 'json',
66
- 'language': 'FR',
67
- 'api-version': 'v2.0',
68
- 'product_id': 110000 # Fruits
 
 
 
 
69
  }
70
- response = requests.get(url, params=params, timeout=30)
71
- response.raise_for_status()
72
- data = response.json()
73
 
74
- if 'value' in data:
75
- mrls = data['value'][:10]
76
- st.success(f"✅ Récupéré {len(data['value'])} LMR pour les fruits")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
 
78
- # Créer un DataFrame simple
79
- if mrls:
80
- df = pd.DataFrame(mrls)
81
- st.write("Colonnes disponibles:")
82
- st.write(list(df.columns))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
 
84
- # Afficher quelques données
85
- cols_to_show = ['product_id', 'pesticide_residue_id', 'mrl_value', 'regulation_number']
86
- cols_to_show = [col for col in cols_to_show if col in df.columns]
 
 
 
 
 
 
87
 
88
- if cols_to_show:
89
- st.dataframe(df[cols_to_show].head())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  else:
91
- st.dataframe(df.head())
 
 
 
 
 
 
 
 
 
92
  else:
93
- st.warning("Format inattendu pour les LMR")
94
- st.json(data)
95
- except Exception as e:
96
- st.error(f"Erreur: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
 
98
  def main():
99
- st.title("🌿 Test Simple de l'API Pesticides EU")
100
-
101
- if st.button("🔍 Lancer les tests"):
102
- test_api()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
 
104
- st.markdown("---")
105
- st.info("""
106
- Cette version de test vérifie:
107
- 1. La connexion à l'API
108
- 2. Le format des données reçues
109
- 3. Les colonnes disponibles dans les données
110
- """)
111
 
112
  if __name__ == "__main__":
113
  main()
 
1
+ import logging
2
+ import json
3
+ import io
4
+ import zipfile
5
+ from datetime import datetime, timedelta
6
+ from typing import Dict, List, Optional, Any, Tuple
7
  import pandas as pd
8
  import requests
9
+ import plotly.express as px
10
+ import streamlit as st
11
+ from tenacity import retry, stop_after_attempt, wait_exponential
12
+ import time
13
+ from collections import defaultdict
14
+ import hashlib
15
 
16
+ # Configuration Streamlit
17
+ st.set_page_config(page_title="Pesticide Data Explorer - Optimized", page_icon="🌿", layout="wide")
18
 
19
+ # Configuration logging
20
+ logging.basicConfig(
21
+ level=logging.INFO,
22
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
23
+ handlers=[
24
+ logging.FileHandler("pesticide_app_optimized.log", encoding="utf-8"),
25
+ logging.StreamHandler()
26
+ ],
27
+ )
28
+ logger = logging.getLogger(__name__)
29
+
30
+ class PesticideDataFetcher:
31
+ """Fetcher optimisé utilisant les endpoints de téléchargement bulk"""
32
 
33
+ BASE_URL = "https://api.datalake.sante.service.ec.europa.eu/sante/pesticides"
34
+ HEADERS = {
35
+ "Content-Type": "application/json",
36
+ "Cache-Control": "no-cache",
37
+ "User-Agent": "Mozilla/5.0"
38
+ }
39
+
40
+ def __init__(self):
41
+ self.session = requests.Session()
42
+ self.session.headers.update(self.HEADERS)
43
+ self.api_calls = 0
44
+
45
+ @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10))
46
+ def download_data(self, endpoint: str, params: Dict) -> Optional[Any]:
47
+ """Télécharge les données depuis un endpoint de download"""
48
+ url = f"{self.BASE_URL}{endpoint}"
49
+
50
+ try:
51
+ self.api_calls += 1
52
+ logger.info(f"Téléchargement depuis {endpoint} (appel API #{self.api_calls})")
53
+
54
+ response = self.session.get(url, params=params, timeout=120) # Timeout plus long pour les gros fichiers
55
+ response.raise_for_status()
56
+
57
+ content_type = response.headers.get('Content-Type', '')
58
+
59
+ # Si c'est du JSON
60
+ if 'json' in content_type or params.get('format') == 'json':
61
+ return response.json()
62
+
63
+ # Si c'est du CSV
64
+ elif 'csv' in content_type or params.get('format') == 'csv':
65
+ return response.text
66
+
67
+ # Si c'est un fichier ZIP (possible pour les gros datasets)
68
+ elif 'zip' in content_type:
69
+ with zipfile.ZipFile(io.BytesIO(response.content)) as zf:
70
+ # Prendre le premier fichier du ZIP
71
+ filename = zf.namelist()[0]
72
+ with zf.open(filename) as f:
73
+ content = f.read().decode('utf-8')
74
+ if filename.endswith('.json'):
75
+ return json.loads(content)
76
+ else:
77
+ return content
78
+
79
+ else:
80
+ # Par défaut, retourner le contenu brut
81
+ return response.text
82
+
83
+ except requests.RequestException as e:
84
+ logger.error(f"Erreur lors du téléchargement {endpoint}: {e}")
85
+ if hasattr(e, 'response') and e.response is not None:
86
+ logger.error(f"Status code: {e.response.status_code}")
87
+ logger.error(f"Response: {e.response.text[:500]}...")
88
+ return None
89
+
90
+ def get_products_paginated(self, language: str = 'FR') -> List[Dict]:
91
+ """Récupère tous les produits avec pagination (pas d'endpoint download)"""
92
+ all_products = []
93
+ url = f"{self.BASE_URL}/pesticide_residues_products"
94
  params = {
95
  'format': 'json',
96
+ 'language': language,
97
  'api-version': 'v2.0'
98
  }
99
+
100
+ # Première requête pour voir s'il y a pagination
101
+ self.api_calls += 1
102
+ response = self.session.get(url, params=params, timeout=30)
103
  response.raise_for_status()
104
  data = response.json()
105
 
106
  if 'value' in data:
107
+ all_products.extend(data['value'])
108
+
109
+ # Gérer la pagination avec nextLink si présent
110
+ while 'nextLink' in data and self.api_calls < 10: # Limite de sécurité
111
+ self.api_calls += 1
112
+ response = self.session.get(data['nextLink'], timeout=30)
113
+ response.raise_for_status()
114
+ data = response.json()
115
+ if 'value' in data:
116
+ all_products.extend(data['value'])
117
  else:
118
+ # Pas de structure 'value', c'est directement la liste
119
+ all_products = data if isinstance(data, list) else [data]
120
+
121
+ logger.info(f"Récupéré {len(all_products)} produits en {self.api_calls} appels")
122
+ return all_products
123
+
124
+ @st.cache_data(ttl=86400) # Cache de 24h pour les données bulk
125
+ def download_all_data() -> Dict[str, Any]:
126
+ """Télécharge toutes les données en utilisant les endpoints optimisés"""
127
 
128
+ fetcher = PesticideDataFetcher()
129
+ results = {}
130
+
131
+ with st.spinner("Téléchargement des données complètes..."):
132
+
133
+ # 1. Télécharger toutes les substances actives
134
+ st.text("📥 Téléchargement des substances actives...")
135
+ substances_data = fetcher.download_data(
136
+ "/active_substances/download",
137
+ {"format": "json", "api-version": "v2.0"}
138
+ )
139
+
140
+ if substances_data:
141
+ # Convertir en dictionnaire pour accès rapide
142
+ if isinstance(substances_data, dict) and 'value' in substances_data:
143
+ substances_list = substances_data['value']
144
+ else:
145
+ substances_list = substances_data if isinstance(substances_data, list) else []
146
+
147
+ results['substances'] = {
148
+ item['substance_id']: item['substance_name']
149
+ for item in substances_list
150
+ if item.get('substance_id') and item.get('substance_name')
151
+ }
152
+ logger.info(f"✓ {len(results['substances'])} substances téléchargées")
153
+
154
+ # 2. Télécharger toutes les LMR
155
+ st.text("📥 Téléchargement de toutes les LMR (peut prendre quelques secondes)...")
156
+ mrls_data = fetcher.download_data(
157
+ "/pesticide_residues_mrls/download",
158
+ {"format": "json", "language": "FR", "api-version": "v2.0"}
159
+ )
160
+
161
+ if mrls_data:
162
+ if isinstance(mrls_data, dict) and 'value' in mrls_data:
163
+ results['mrls'] = mrls_data['value']
164
+ else:
165
+ results['mrls'] = mrls_data if isinstance(mrls_data, list) else []
166
+ logger.info(f"✓ {len(results['mrls'])} LMR téléchargées")
167
+
168
+ # 3. Récupérer tous les produits (avec pagination si nécessaire)
169
+ st.text("📥 Récupération des produits...")
170
+ products_list = fetcher.get_products_paginated(language='FR')
171
+
172
+ results['products'] = products_list
173
+ results['product_dict'] = {
174
+ p['product_id']: p['product_name']
175
+ for p in products_list
176
+ if p.get('product_id') and p.get('product_name')
177
  }
178
+ logger.info(f"✓ {len(results['products'])} produits récupérés")
 
 
179
 
180
+ # 4. Statistiques
181
+ results['stats'] = {
182
+ 'api_calls': fetcher.api_calls,
183
+ 'substances_count': len(results.get('substances', {})),
184
+ 'mrls_count': len(results.get('mrls', [])),
185
+ 'products_count': len(results.get('products', [])),
186
+ 'download_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
187
+ }
188
+
189
+ st.success(f" Toutes les données téléchargées en seulement {fetcher.api_calls} appels API!")
190
+
191
+ return results
192
+
193
+ class PesticideInterface:
194
+ def __init__(self):
195
+ # Charger toutes les données une seule fois
196
+ self.data = download_all_data()
197
+
198
+ # Créer des index pour des recherches rapides
199
+ self._create_indexes()
200
 
201
+ def _create_indexes(self):
202
+ """Crée des index pour optimiser les recherches"""
203
+ # Index des LMR par product_id
204
+ self.mrls_by_product = defaultdict(list)
205
+ for mrl in self.data.get('mrls', []):
206
+ if mrl.get('product_id'):
207
+ self.mrls_by_product[mrl['product_id']].append(mrl)
208
+
209
+ # Index des produits par nom
210
+ self.product_choices = {
211
+ p['product_name']: p['product_id']
212
+ for p in self.data.get('products', [])
213
+ if p.get('product_name') and p.get('product_id')
214
  }
 
 
 
215
 
216
+ logger.info(f"Index créés: {len(self.mrls_by_product)} produits avec LMR")
217
+
218
+ def get_product_details(self, product_names: List[str], future_only: bool = False) -> pd.DataFrame:
219
+ """Récupère les détails des produits depuis les données en cache"""
220
+
221
+ # Convertir les noms en IDs
222
+ product_ids = [self.product_choices[name] for name in product_names]
223
+
224
+ # Récupérer les LMR depuis l'index
225
+ all_mrls = []
226
+ for product_id in product_ids:
227
+ mrls = self.mrls_by_product.get(product_id, [])
228
+ all_mrls.extend(mrls)
229
+
230
+ if not all_mrls:
231
+ st.info("Aucune donnée de LMR trouvée pour les produits sélectionnés.")
232
+ return pd.DataFrame()
233
+
234
+ # Convertir en DataFrame
235
+ df = pd.DataFrame(all_mrls)
236
+
237
+ # Enrichir avec les données
238
+ df["Substance"] = df["pesticide_residue_id"].map(self.data.get('substances', {})).fillna("Inconnu")
239
+ df["Produit"] = df["product_id"].map(self.data.get('product_dict', {})).fillna("Inconnu")
240
+
241
+ # Ajouter le lien vers le règlement
242
+ if 'regulation_url' in df.columns:
243
+ df["Règlement"] = df.apply(
244
+ lambda x: f'<a href="{x["regulation_url"]}" target="_blank">{x.get("regulation_number", "N/A")}</a>'
245
+ if pd.notna(x.get("regulation_url")) else x.get("regulation_number", "N/A"),
246
+ axis=1
247
+ )
248
+ else:
249
+ df["Règlement"] = df.get("regulation_number", "N/A")
250
+
251
+ # Conversion des dates
252
+ df["Date d'application"] = pd.to_datetime(df.get("entry_into_force_date"), errors="coerce")
253
+
254
+ # Filtrer pour les 6 prochains mois si demandé
255
+ if future_only:
256
+ now = datetime.now()
257
+ future_date = now + timedelta(days=180)
258
+ df = df[
259
+ (df["Date d'application"] > now) &
260
+ (df["Date d'application"] <= future_date)
261
+ ]
262
 
263
+ if df.empty:
264
+ st.info(f"🔍 Aucun changement de LMR prévu dans les 6 prochains mois.")
265
+ return pd.DataFrame()
266
+
267
+ # Préparer le DataFrame final
268
+ # C'est ici que les NaNs peuvent apparaître si mrl_value n'est pas numérique
269
+ df["Valeur LMR"] = pd.to_numeric(df.get("mrl_value"), errors='coerce')
270
+
271
+ # Sélection des colonnes finales
272
+ columns_to_keep = ["Produit", "Substance", "Valeur LMR"]
273
+ if "Date d'application" in df.columns:
274
+ columns_to_keep.append("Date d'application")
275
+ if "Règlement" in df.columns:
276
+ columns_to_keep.append("Règlement")
277
+
278
+ # S'assurer que toutes les colonnes à garder existent avant de les sélectionner
279
+ # Utilisez .copy() pour éviter SettingWithCopyWarning
280
+ df = df[[col for col in columns_to_keep if col in df.columns]].copy()
281
+
282
+ df = df.sort_values(
283
+ ["Produit", "Date d'application"] if "Date d'application" in columns_to_keep else ["Produit"],
284
+ ascending=[True, False] if "Date d'application" in columns_to_keep else [True]
285
+ )
286
+
287
+ return df
288
+
289
+ def create_interface(self):
290
+ st.title("🌿 EU Pesticides Database Explorer - Version Optimisée")
291
+
292
+ # Afficher les statistiques
293
+ col1, col2, col3, col4 = st.columns(4)
294
+ with col1:
295
+ st.metric("📦 Produits", f"{self.data['stats']['products_count']:,}")
296
+ with col2:
297
+ st.metric("🧪 Substances", f"{self.data['stats']['substances_count']:,}")
298
+ with col3:
299
+ st.metric("📊 LMR totales", f"{self.data['stats']['mrls_count']:,}")
300
+ with col4:
301
+ st.metric("🚀 Appels API", self.data['stats']['api_calls'])
302
+
303
+ st.success(f"✨ Toutes les données ont été téléchargées en {self.data['stats']['api_calls']} appels API seulement!")
304
+
305
+ st.markdown("---")
306
+
307
+ # Interface de sélection
308
+ col1, col2 = st.columns([3, 1])
309
+ with col1:
310
+ # Recherche avec autocomplétion
311
+ product_names = st.multiselect(
312
+ "🔍 Sélectionnez un ou plusieurs produits",
313
+ options=sorted(list(self.product_choices.keys())),
314
+ help="Commencez à taper pour filtrer les produits"
315
+ )
316
+
317
+ with col2:
318
+ future_only = st.checkbox(
319
+ "📅 6 prochains mois",
320
+ value=False,
321
+ help="Afficher uniquement les changements prévus"
322
+ )
323
+
324
+ # Affichage des résultats
325
+ if product_names:
326
+ df = self.get_product_details(product_names, future_only)
327
+
328
+ if not df.empty:
329
+ st.markdown("### 📊 Résultats")
330
+
331
+ # Statistiques rapides
332
+ # Filtrer les NaNs pour les calculs de statistiques
333
+ df_numeric_mrl = df[df["Valeur LMR"].notna()]
334
+
335
+ col1, col2, col3 = st.columns(3)
336
+ with col1:
337
+ st.metric("Entrées trouvées", len(df))
338
+ with col2:
339
+ st.metric("Substances uniques", df_numeric_mrl["Substance"].nunique() if not df_numeric_mrl.empty else 0)
340
+ with col3:
341
+ if "Valeur LMR" in df_numeric_mrl.columns:
342
+ avg_mrl = df_numeric_mrl["Valeur LMR"].mean()
343
+ st.metric("LMR moyenne", f"{avg_mrl:.3f} mg/kg" if not pd.isna(avg_mrl) else "N/A")
344
 
345
+ # Options d'affichage
346
+ with st.expander("⚙️ Options d'affichage"):
347
+ show_zero = st.checkbox("Afficher les LMR à 0.01 mg/kg", value=True)
348
+ sort_by = st.selectbox(
349
+ "Trier par",
350
+ ["Produit", "Substance", "Valeur LMR", "Date d'application"]
351
+ if "Date d'application" in df.columns else ["Produit", "Substance", "Valeur LMR"]
352
+ )
353
+ sort_order = st.radio("Ordre", ["Croissant", "Décroissant"], horizontal=True)
354
 
355
+ # Appliquer les filtres
356
+ if not show_zero and "Valeur LMR" in df.columns:
357
+ df = df[df["Valeur LMR"] != 0.01]
358
+
359
+ if sort_by in df.columns:
360
+ df = df.sort_values(sort_by, ascending=(sort_order == "Croissant"))
361
+
362
+ # Afficher le tableau
363
+ st.dataframe(
364
+ df,
365
+ use_container_width=True,
366
+ hide_index=True,
367
+ column_config={
368
+ "Valeur LMR": st.column_config.NumberColumn(
369
+ "Valeur LMR (mg/kg)",
370
+ format="%.3f",
371
+ help="Limite Maximale de Résidus"
372
+ ),
373
+ "Date d'application": st.column_config.DateColumn(
374
+ "Date d'application",
375
+ format="DD/MM/YYYY"
376
+ ),
377
+ "Règlement": st.column_config.LinkColumn(
378
+ "Règlement",
379
+ help="Cliquez pour voir le règlement officiel"
380
+ )
381
+ }
382
+ )
383
+
384
+ # Visualisations
385
+ # Condition pour s'assurer qu'il y a assez de données pour visualiser
386
+ if len(df) > 1:
387
+ self.create_visualizations(df)
388
  else:
389
+ st.info("Sélectionnez plus de données (au moins 2 entrées) pour générer des visualisations.")
390
+
391
+ # Export
392
+ csv = df.to_csv(index=False)
393
+ st.download_button(
394
+ label="📥 Télécharger (CSV)",
395
+ data=csv,
396
+ file_name=f"pesticides_lmr_{datetime.now().strftime('%Y%m%d')}.csv",
397
+ mime="text/csv"
398
+ )
399
  else:
400
+ # Afficher quelques statistiques globales
401
+ st.info("👆 Sélectionnez des produits pour voir leurs LMR")
402
+
403
+ with st.expander("📊 Statistiques globales"):
404
+ # Top 10 des produits avec le plus de LMR
405
+ product_mrl_count = {
406
+ pid: len(mrls)
407
+ for pid, mrls in self.mrls_by_product.items()
408
+ }
409
+ top_products = sorted(
410
+ product_mrl_count.items(),
411
+ key=lambda x: x[1],
412
+ reverse=True
413
+ )[:10]
414
+
415
+ if top_products:
416
+ st.markdown("**Top 10 des produits avec le plus de LMR:**")
417
+ for pid, count in top_products:
418
+ product_name = self.data['product_dict'].get(pid, f"ID: {pid}")
419
+ st.write(f"- {product_name}: {count} LMR")
420
+
421
+ def create_visualizations(self, df: pd.DataFrame):
422
+ """Crée des visualisations interactives"""
423
+
424
+ tabs = st.tabs(["📈 Évolution temporelle", "📊 Distribution", "🏆 Top substances"])
425
+
426
+ # Crée un DataFrame filtré pour les visualisations,
427
+ # en s'assurant que 'Valeur LMR' n'est pas NaN.
428
+ # Cela résout le problème de l'erreur 'size' et d'autres opérations numériques.
429
+ plot_df = df[df["Valeur LMR"].notna()].copy()
430
+
431
+ if plot_df.empty:
432
+ st.info("Pas de données valides pour la visualisation après filtrage (valeurs LMR manquantes ou non numériques).")
433
+ return
434
+
435
+ with tabs[0]:
436
+ # Filtrer davantage pour les données temporelles si nécessaire
437
+ if "Date d'application" in plot_df.columns and plot_df["Date d'application"].notna().any():
438
+ temp_plot_df = plot_df[plot_df["Date d'application"].notna()]
439
+ if not temp_plot_df.empty:
440
+ fig = px.scatter(
441
+ temp_plot_df,
442
+ x="Date d'application",
443
+ y="Valeur LMR",
444
+ color="Substance",
445
+ size="Valeur LMR", # 'Valeur LMR' est maintenant garantie sans NaNs
446
+ hover_data=["Produit", "Valeur LMR", "Substance", "Règlement"], # Plus de détails au survol
447
+ title="Évolution des LMR dans le temps",
448
+ log_y=True
449
+ )
450
+ st.plotly_chart(fig, use_container_width=True)
451
+ else:
452
+ st.info("Pas de données temporelles valides disponibles pour cette visualisation après filtrage.")
453
+ else:
454
+ st.info("Pas de données temporelles disponibles pour cette visualisation.")
455
+
456
+ with tabs[1]:
457
+ # Histogramme des valeurs LMR
458
+ fig = px.histogram(
459
+ plot_df, # Utiliser le plot_df déjà filtré
460
+ x="Valeur LMR",
461
+ nbins=50,
462
+ title="Distribution des valeurs LMR",
463
+ log_x=True,
464
+ labels={"count": "Nombre d'occurrences"}
465
+ )
466
+ st.plotly_chart(fig, use_container_width=True)
467
+
468
+ # Box plot par produit si plusieurs produits
469
+ if plot_df["Produit"].nunique() > 1:
470
+ fig2 = px.box(
471
+ plot_df, # Utiliser le plot_df déjà filtré
472
+ x="Produit",
473
+ y="Valeur LMR",
474
+ title="Distribution des LMR par produit",
475
+ log_y=True
476
+ )
477
+ st.plotly_chart(fig2, use_container_width=True)
478
+
479
+ with tabs[2]:
480
+ # Top substances par valeur maximale
481
+ # La colonne 'Valeur LMR' est déjà sans NaN dans plot_df
482
+ top_substances = (
483
+ plot_df.groupby("Substance")["Valeur LMR"]
484
+ .agg(['max', 'count', 'mean'])
485
+ .sort_values('max', ascending=False)
486
+ .head(15)
487
+ )
488
+
489
+ if not top_substances.empty:
490
+ fig = px.bar(
491
+ x=top_substances['max'].values,
492
+ y=top_substances.index,
493
+ orientation='h',
494
+ title="Top 15 des substances par LMR maximale",
495
+ labels={'x': 'LMR maximale (mg/kg)', 'y': 'Substance'},
496
+ hover_data={
497
+ 'Occurrences': top_substances['count'].values,
498
+ 'Moyenne': top_substances['mean'].round(3).values
499
+ }
500
+ )
501
+ st.plotly_chart(fig, use_container_width=True)
502
+ else:
503
+ st.info("Pas assez de données pour afficher le Top substances.")
504
 
505
  def main():
506
+ # Configuration de la sidebar
507
+ with st.sidebar:
508
+ st.markdown("## 🌿 EU Pesticides Explorer")
509
+ st.markdown("### Version Ultra-Optimisée")
510
+
511
+ st.markdown("""
512
+ Cette version utilise les **endpoints de téléchargement bulk**
513
+ pour récupérer toutes les données en seulement **3-4 appels API** !
514
+
515
+ **Avantages :**
516
+ - ✅ Pas de limitation à 100 appels
517
+ - ✅ Accès à TOUTES les données
518
+ - ✅ Performance maximale
519
+ - ✅ Cache de 24h
520
+
521
+ **Données disponibles :**
522
+ - Tous les produits alimentaires
523
+ - Toutes les substances actives
524
+ - Toutes les LMR (>100,000 entrées)
525
+ """)
526
+
527
+ if st.button("🔄 Forcer le rechargement des données"):
528
+ st.cache_data.clear()
529
+ st.rerun()
530
+
531
+ st.markdown("---")
532
+
533
+ # Afficher l'heure du dernier téléchargement
534
+ data = download_all_data()
535
+ if 'download_time' in data.get('stats', {}):
536
+ st.caption(f"Dernière mise à jour : {data['stats']['download_time']}")
537
 
538
+ # Interface principale
539
+ interface = PesticideInterface()
540
+ interface.create_interface()
 
 
 
 
541
 
542
  if __name__ == "__main__":
543
  main()