MLRSTREAMLIT / apparevoir.py
MMOON's picture
Rename app.py to apparevoir.py
9bc58be verified
import logging
import json
import io
import zipfile
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Any, Tuple
import pandas as pd
import requests
import plotly.express as px
import streamlit as st
from tenacity import retry, stop_after_attempt, wait_exponential
import time
from collections import defaultdict
import hashlib
# Configuration Streamlit
st.set_page_config(page_title="Pesticide Data Explorer - Optimized", page_icon="🌿", layout="wide")
# Configuration logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
handlers=[
logging.FileHandler("pesticide_app_optimized.log", encoding="utf-8"),
logging.StreamHandler()
],
)
logger = logging.getLogger(__name__)
class PesticideDataFetcher:
"""Fetcher optimisé utilisant les endpoints de téléchargement bulk"""
BASE_URL = "https://api.datalake.sante.service.ec.europa.eu/sante/pesticides"
HEADERS = {
"Content-Type": "application/json",
"Cache-Control": "no-cache",
"User-Agent": "Mozilla/5.0"
}
def __init__(self):
self.session = requests.Session()
self.session.headers.update(self.HEADERS)
self.api_calls = 0
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10))
def download_data(self, endpoint: str, params: Dict) -> Optional[Any]:
"""Télécharge les données depuis un endpoint de download"""
url = f"{self.BASE_URL}{endpoint}"
try:
self.api_calls += 1
logger.info(f"Téléchargement depuis {endpoint} (appel API #{self.api_calls})")
response = self.session.get(url, params=params, timeout=120) # Timeout plus long pour les gros fichiers
response.raise_for_status()
content_type = response.headers.get('Content-Type', '')
# Si c'est du JSON
if 'json' in content_type or params.get('format') == 'json':
return response.json()
# Si c'est du CSV
elif 'csv' in content_type or params.get('format') == 'csv':
return response.text
# Si c'est un fichier ZIP (possible pour les gros datasets)
elif 'zip' in content_type:
with zipfile.ZipFile(io.BytesIO(response.content)) as zf:
# Prendre le premier fichier du ZIP
filename = zf.namelist()[0]
with zf.open(filename) as f:
content = f.read().decode('utf-8')
if filename.endswith('.json'):
return json.loads(content)
else:
return content
else:
# Par défaut, retourner le contenu brut
return response.text
except requests.RequestException as e:
logger.error(f"Erreur lors du téléchargement {endpoint}: {e}")
if hasattr(e, 'response') and e.response is not None:
logger.error(f"Status code: {e.response.status_code}")
logger.error(f"Response: {e.response.text[:500]}...")
return None
def get_products_paginated(self, language: str = 'FR') -> List[Dict]:
"""Récupère tous les produits avec pagination (pas d'endpoint download)"""
all_products = []
url = f"{self.BASE_URL}/pesticide_residues_products"
params = {
'format': 'json',
'language': language,
'api-version': 'v2.0'
}
# Première requête pour voir s'il y a pagination
self.api_calls += 1
response = self.session.get(url, params=params, timeout=30)
response.raise_for_status()
data = response.json()
if 'value' in data:
all_products.extend(data['value'])
# Gérer la pagination avec nextLink si présent
while 'nextLink' in data and self.api_calls < 10: # Limite de sécurité
self.api_calls += 1
response = self.session.get(data['nextLink'], timeout=30)
response.raise_for_status()
data = response.json()
if 'value' in data:
all_products.extend(data['value'])
else:
# Pas de structure 'value', c'est directement la liste
all_products = data if isinstance(data, list) else [data]
logger.info(f"Récupéré {len(all_products)} produits en {self.api_calls} appels")
return all_products
@st.cache_data(ttl=86400) # Cache de 24h pour les données bulk
def download_all_data() -> Dict[str, Any]:
"""Télécharge toutes les données en utilisant les endpoints optimisés"""
fetcher = PesticideDataFetcher()
results = {}
with st.spinner("Téléchargement des données complètes..."):
# 1. Télécharger toutes les substances actives
st.text("📥 Téléchargement des substances actives...")
substances_data = fetcher.download_data(
"/active_substances/download",
{"format": "json", "api-version": "v2.0"}
)
if substances_data:
# Convertir en dictionnaire pour accès rapide
if isinstance(substances_data, dict) and 'value' in substances_data:
substances_list = substances_data['value']
else:
substances_list = substances_data if isinstance(substances_data, list) else []
results['substances'] = {
item['substance_id']: item['substance_name']
for item in substances_list
if item.get('substance_id') and item.get('substance_name')
}
logger.info(f"✓ {len(results['substances'])} substances téléchargées")
# 2. Télécharger toutes les LMR
st.text("📥 Téléchargement de toutes les LMR (peut prendre quelques secondes)...")
mrls_data = fetcher.download_data(
"/pesticide_residues_mrls/download",
{"format": "json", "language": "FR", "api-version": "v2.0"}
)
if mrls_data:
if isinstance(mrls_data, dict) and 'value' in mrls_data:
results['mrls'] = mrls_data['value']
else:
results['mrls'] = mrls_data if isinstance(mrls_data, list) else []
logger.info(f"✓ {len(results['mrls'])} LMR téléchargées")
# 3. Récupérer tous les produits (avec pagination si nécessaire)
st.text("📥 Récupération des produits...")
products_list = fetcher.get_products_paginated(language='FR')
results['products'] = products_list
results['product_dict'] = {
p['product_id']: p['product_name']
for p in products_list
if p.get('product_id') and p.get('product_name')
}
logger.info(f"✓ {len(results['products'])} produits récupérés")
# 4. Statistiques
results['stats'] = {
'api_calls': fetcher.api_calls,
'substances_count': len(results.get('substances', {})),
'mrls_count': len(results.get('mrls', [])),
'products_count': len(results.get('products', [])),
'download_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
}
st.success(f"✅ Toutes les données téléchargées en seulement {fetcher.api_calls} appels API!")
return results
class PesticideInterface:
def __init__(self):
# Charger toutes les données une seule fois
self.data = download_all_data()
# Créer des index pour des recherches rapides
self._create_indexes()
def _create_indexes(self):
"""Crée des index pour optimiser les recherches"""
# Index des LMR par product_id
self.mrls_by_product = defaultdict(list)
for mrl in self.data.get('mrls', []):
if mrl.get('product_id'):
self.mrls_by_product[mrl['product_id']].append(mrl)
# Index des produits par nom
self.product_choices = {
p['product_name']: p['product_id']
for p in self.data.get('products', [])
if p.get('product_name') and p.get('product_id')
}
logger.info(f"Index créés: {len(self.mrls_by_product)} produits avec LMR")
def get_product_details(self, product_names: List[str], future_only: bool = False) -> pd.DataFrame:
"""Récupère les détails des produits depuis les données en cache"""
# Convertir les noms en IDs
product_ids = [self.product_choices[name] for name in product_names]
# Récupérer les LMR depuis l'index
all_mrls = []
for product_id in product_ids:
mrls = self.mrls_by_product.get(product_id, [])
all_mrls.extend(mrls)
if not all_mrls:
st.info("Aucune donnée de LMR trouvée pour les produits sélectionnés.")
return pd.DataFrame()
# Convertir en DataFrame
df = pd.DataFrame(all_mrls)
# Enrichir avec les données
df["Substance"] = df["pesticide_residue_id"].map(self.data.get('substances', {})).fillna("Inconnu")
df["Produit"] = df["product_id"].map(self.data.get('product_dict', {})).fillna("Inconnu")
# Ajouter le lien vers le règlement
if 'regulation_url' in df.columns and 'regulation_number' in df.columns:
# Pour LinkColumn de Streamlit, on a besoin juste de l'URL
df["Règlement"] = df["regulation_url"].fillna("")
df["Règlement_Num"] = df["regulation_number"].fillna("N/A")
elif 'regulation_number' in df.columns:
df["Règlement_Num"] = df["regulation_number"].fillna("N/A")
else:
df["Règlement_Num"] = "N/A"
# Conversion des dates
df["Date d'application"] = pd.to_datetime(df.get("entry_into_force_date"), errors="coerce")
# Filtrer pour les 6 prochains mois si demandé
if future_only:
now = datetime.now()
future_date = now + timedelta(days=180)
df = df[
(df["Date d'application"] > now) &
(df["Date d'application"] <= future_date)
]
if df.empty:
st.info(f"🔍 Aucun changement de LMR prévu dans les 6 prochains mois.")
return pd.DataFrame()
# Préparer le DataFrame final
if "mrl_value" in df.columns:
df["Valeur LMR"] = pd.to_numeric(df["mrl_value"], errors='coerce')
elif "Valeur LMR" not in df.columns:
df["Valeur LMR"] = pd.NA
# Sélection des colonnes finales
columns_to_keep = ["Produit", "Substance", "Valeur LMR"]
if "Date d'application" in df.columns:
columns_to_keep.append("Date d'application")
if "Règlement_Num" in df.columns:
columns_to_keep.append("Règlement_Num")
if "Règlement" in df.columns:
columns_to_keep.append("Règlement")
# Tri des données
sort_columns = ["Produit"]
if "Date d'application" in df.columns:
sort_columns.append("Date d'application")
sort_ascending = [True, False]
else:
sort_ascending = [True]
df = df.sort_values(sort_columns, ascending=sort_ascending)
return df
def create_interface(self):
st.title("🌿 EU Pesticides Database Explorer - Version Optimisée")
# Afficher les statistiques
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric("📦 Produits", f"{self.data['stats']['products_count']:,}")
with col2:
st.metric("🧪 Substances", f"{self.data['stats']['substances_count']:,}")
with col3:
st.metric("📊 LMR totales", f"{self.data['stats']['mrls_count']:,}")
with col4:
st.metric("🚀 Appels API", self.data['stats']['api_calls'])
st.success(f"✨ Toutes les données ont été téléchargées en {self.data['stats']['api_calls']} appels API seulement!")
st.markdown("---")
# Interface de sélection
col1, col2 = st.columns([3, 1])
with col1:
# Recherche avec autocomplétion
product_names = st.multiselect(
"🔍 Sélectionnez un ou plusieurs produits",
options=sorted(list(self.product_choices.keys())),
help="Commencez à taper pour filtrer les produits"
)
with col2:
future_only = st.checkbox(
"📅 6 prochains mois",
value=False,
help="Afficher uniquement les changements prévus"
)
# Affichage des résultats
if product_names:
df = self.get_product_details(product_names, future_only)
if not df.empty:
st.markdown("### 📊 Résultats")
# Statistiques rapides
col1, col2, col3 = st.columns(3)
with col1:
st.metric("Entrées trouvées", len(df))
with col2:
st.metric("Substances uniques", df["Substance"].nunique())
with col3:
if "Valeur LMR" in df.columns:
avg_mrl = df["Valeur LMR"].mean()
st.metric("LMR moyenne", f"{avg_mrl:.3f} mg/kg" if not pd.isna(avg_mrl) else "N/A")
# Options d'affichage
with st.expander("⚙️ Options d'affichage"):
show_zero = st.checkbox("Afficher les LMR à 0.01 mg/kg", value=True)
# Colonnes disponibles pour le tri
available_sort_columns = ["Produit", "Substance", "Valeur LMR"]
if "Date d'application" in df.columns:
available_sort_columns.append("Date d'application")
if "Règlement_Num" in df.columns:
available_sort_columns.append("N° Règlement")
sort_by = st.selectbox("Trier par", available_sort_columns)
sort_order = st.radio("Ordre", ["Croissant", "Décroissant"], horizontal=True)
# Appliquer les filtres
if not show_zero and "Valeur LMR" in df.columns:
df = df[df["Valeur LMR"] != 0.01]
# Mapper le nom d'affichage vers le nom réel de la colonne
sort_column_mapping = {
"N° Règlement": "Règlement_Num",
"Produit": "Produit",
"Substance": "Substance",
"Valeur LMR": "Valeur LMR",
"Date d'application": "Date d'application"
}
actual_sort_column = sort_column_mapping.get(sort_by, sort_by)
if actual_sort_column in df.columns:
df = df.sort_values(actual_sort_column, ascending=(sort_order == "Croissant"))
# Afficher le tableau avec configuration des colonnes
column_config = {
"Valeur LMR": st.column_config.NumberColumn(
"Valeur LMR (mg/kg)",
format="%.3f",
help="Limite Maximale de Résidus"
),
"Date d'application": st.column_config.DateColumn(
"Date d'application",
format="DD/MM/YYYY"
)
}
# Garder seulement les colonnes nécessaires pour l'affichage
display_columns = ["Produit", "Substance", "Valeur LMR"]
if "Date d'application" in df.columns:
display_columns.append("Date d'application")
# Gérer l'affichage du règlement
if "Règlement" in df.columns and df["Règlement"].notna().any():
display_columns.append("Règlement_Num")
display_columns.append("Règlement")
column_config["Règlement_Num"] = st.column_config.TextColumn(
"N° Règlement",
help="Numéro du règlement"
)
column_config["Règlement"] = st.column_config.LinkColumn(
"Lien",
help="Cliquez pour voir le règlement officiel",
display_text="📄 Voir"
)
elif "Règlement_Num" in df.columns:
display_columns.append("Règlement_Num")
column_config["Règlement_Num"] = st.column_config.TextColumn(
"Règlement",
help="Numéro du règlement"
)
df_display = df[display_columns]
st.dataframe(
df_display,
use_container_width=True,
hide_index=True,
column_config=column_config
)
# Visualisations
if len(df) > 1:
self.create_visualizations(df)
# Export - exclure la colonne des liens URL
export_columns = ["Produit", "Substance", "Valeur LMR"]
if "Date d'application" in df.columns:
export_columns.append("Date d'application")
if "Règlement_Num" in df.columns:
export_columns.append("Règlement_Num")
# Renommer Règlement_Num en Règlement pour l'export
df_export = df[export_columns].copy()
if "Règlement_Num" in df_export.columns:
df_export = df_export.rename(columns={"Règlement_Num": "Règlement"})
csv = df_export.to_csv(index=False)
st.download_button(
label="📥 Télécharger (CSV)",
data=csv,
file_name=f"pesticides_lmr_{datetime.now().strftime('%Y%m%d')}.csv",
mime="text/csv"
)
else:
# Afficher quelques statistiques globales
st.info("👆 Sélectionnez des produits pour voir leurs LMR")
with st.expander("📊 Statistiques globales"):
# Top 10 des produits avec le plus de LMR
product_mrl_count = {
pid: len(mrls)
for pid, mrls in self.mrls_by_product.items()
}
top_products = sorted(
product_mrl_count.items(),
key=lambda x: x[1],
reverse=True
)[:10]
if top_products:
st.markdown("**Top 10 des produits avec le plus de LMR:**")
for pid, count in top_products:
product_name = self.data['product_dict'].get(pid, f"ID: {pid}")
st.write(f"- {product_name}: {count} LMR")
def create_visualizations(self, df: pd.DataFrame):
"""Crée des visualisations interactives"""
tabs = st.tabs(["📈 Évolution temporelle", "📊 Distribution", "🏆 Top substances"])
with tabs[0]:
if "Date d'application" in df.columns and df["Date d'application"].notna().any():
# Filtrer les données pour le graphique (enlever les NaN)
df_plot = df[df["Date d'application"].notna() & df["Valeur LMR"].notna()].copy()
if not df_plot.empty:
fig = px.scatter(
df_plot,
x="Date d'application",
y="Valeur LMR",
color="Substance",
hover_data=["Produit"],
title="Évolution des LMR dans le temps",
log_y=True
)
st.plotly_chart(fig, use_container_width=True)
else:
st.info("Pas de données valides pour le graphique temporel")
else:
st.info("Pas de données temporelles disponibles")
with tabs[1]:
if "Valeur LMR" in df.columns and df["Valeur LMR"].notna().any():
# Filtrer les valeurs valides pour l'histogramme
df_hist = df[df["Valeur LMR"].notna()]
# Histogramme des valeurs LMR
fig = px.histogram(
df_hist,
x="Valeur LMR",
nbins=50,
title="Distribution des valeurs LMR",
log_x=True,
labels={"count": "Nombre d'occurrences"}
)
st.plotly_chart(fig, use_container_width=True)
# Box plot par produit si plusieurs produits
if df["Produit"].nunique() > 1 and len(df_hist) > 0:
fig2 = px.box(
df_hist,
x="Produit",
y="Valeur LMR",
title="Distribution des LMR par produit",
log_y=True
)
st.plotly_chart(fig2, use_container_width=True)
else:
st.info("Pas de données de LMR valides pour les graphiques de distribution")
with tabs[2]:
if "Valeur LMR" in df.columns and df["Valeur LMR"].notna().any():
# Filtrer les valeurs valides
df_valid = df[df["Valeur LMR"].notna()]
# Top substances par valeur maximale
top_substances = (
df_valid.groupby("Substance")["Valeur LMR"]
.agg(['max', 'count', 'mean'])
.sort_values('max', ascending=False)
.head(15)
)
if not top_substances.empty:
fig = px.bar(
x=top_substances['max'].values,
y=top_substances.index,
orientation='h',
title="Top 15 des substances par LMR maximale",
labels={'x': 'LMR maximale (mg/kg)', 'y': 'Substance'},
hover_data={
'Occurrences': top_substances['count'].values,
'Moyenne': top_substances['mean'].round(3).values
}
)
st.plotly_chart(fig, use_container_width=True)
else:
st.info("Pas assez de données pour afficher le top des substances")
else:
st.info("Pas de données de LMR valides pour le classement des substances")
def main():
# Configuration de la sidebar
with st.sidebar:
st.markdown("## 🌿 EU Pesticides Explorer")
st.markdown("### Version Ultra-Optimisée")
st.markdown("""
Cette version utilise les **endpoints de téléchargement bulk**
pour récupérer toutes les données en seulement **3-4 appels API** !
**Avantages :**
- ✅ Pas de limitation à 100 appels
- ✅ Accès à TOUTES les données
- ✅ Performance maximale
- ✅ Cache de 24h
**Données disponibles :**
- Tous les produits alimentaires
- Toutes les substances actives
- Toutes les LMR (>100,000 entrées)
""")
if st.button("🔄 Forcer le rechargement des données"):
st.cache_data.clear()
st.rerun()
st.markdown("---")
# Afficher l'heure du dernier téléchargement
data = download_all_data()
if 'download_time' in data.get('stats', {}):
st.caption(f"Dernière mise à jour : {data['stats']['download_time']}")
# Interface principale
interface = PesticideInterface()
interface.create_interface()
if __name__ == "__main__":
main()