Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,624 +1,95 @@
|
|
| 1 |
import streamlit as st
|
| 2 |
-
import pandas as pd
|
| 3 |
-
import plotly.express as px
|
| 4 |
import requests
|
| 5 |
-
import
|
| 6 |
-
import
|
| 7 |
-
import
|
| 8 |
-
import datetime
|
| 9 |
-
from functools import lru_cache
|
| 10 |
-
import base64
|
| 11 |
-
from io import StringIO
|
| 12 |
|
| 13 |
-
# Configuration
|
| 14 |
-
|
| 15 |
-
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
| 16 |
-
logger = logging.getLogger('pesticide_explorer')
|
| 17 |
|
| 18 |
-
#
|
| 19 |
-
st.
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
)
|
|
|
|
|
|
|
| 24 |
|
| 25 |
-
#
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
|
|
|
| 32 |
|
| 33 |
-
#
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
if "api_key" not in st.session_state:
|
| 42 |
-
st.session_state.api_key = ""
|
| 43 |
-
|
| 44 |
-
return st.session_state.api_key
|
| 45 |
|
| 46 |
-
#
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
# Produits de démonstration
|
| 51 |
-
products_data = [
|
| 52 |
-
{"product_id": 1, "product_name": "Pommes", "product_code": "0130010"},
|
| 53 |
-
{"product_id": 2, "product_name": "Poires", "product_code": "0130020"},
|
| 54 |
-
{"product_id": 3, "product_name": "Tomates", "product_code": "0231010"},
|
| 55 |
-
{"product_id": 4, "product_name": "Raisins", "product_code": "0151010"},
|
| 56 |
-
{"product_id": 5, "product_name": "Oranges", "product_code": "0110010"},
|
| 57 |
-
{"product_id": 6, "product_name": "Blé", "product_code": "0500010"},
|
| 58 |
-
{"product_id": 7, "product_name": "Riz", "product_code": "0500060"},
|
| 59 |
-
{"product_id": 8, "product_name": "Carottes", "product_code": "0213020"},
|
| 60 |
-
]
|
| 61 |
-
|
| 62 |
-
# Convertir en DataFrame
|
| 63 |
-
return pd.DataFrame(products_data)
|
| 64 |
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
url = f"{BASE_URL}/pesticide_residues_products"
|
| 73 |
-
params = {
|
| 74 |
-
"format": DEFAULT_FORMAT,
|
| 75 |
-
"language": DEFAULT_LANGUAGE,
|
| 76 |
-
"api-version": API_VERSION
|
| 77 |
-
}
|
| 78 |
-
|
| 79 |
-
all_products = []
|
| 80 |
-
next_link = None
|
| 81 |
-
|
| 82 |
-
try:
|
| 83 |
-
with st.spinner("Chargement de la liste des produits..."):
|
| 84 |
-
while True:
|
| 85 |
-
if next_link:
|
| 86 |
-
response = make_api_request(next_link)
|
| 87 |
-
else:
|
| 88 |
-
response = make_api_request(url, params)
|
| 89 |
-
|
| 90 |
-
if not response:
|
| 91 |
-
st.error("Impossible de récupérer la liste des produits. Veuillez réessayer plus tard.")
|
| 92 |
-
return pd.DataFrame()
|
| 93 |
-
|
| 94 |
-
data = response.json()
|
| 95 |
-
|
| 96 |
-
# Récupérer les produits de la page actuelle
|
| 97 |
-
products = data.get('value', [])
|
| 98 |
-
all_products.extend(products)
|
| 99 |
-
|
| 100 |
-
# Vérifier s'il y a une page suivante
|
| 101 |
-
next_link = data.get('nextLink') or data.get('@odata.nextLink')
|
| 102 |
-
if not next_link:
|
| 103 |
-
break
|
| 104 |
-
|
| 105 |
-
# Convertir en DataFrame pour faciliter la manipulation
|
| 106 |
-
df_products = pd.DataFrame(all_products)
|
| 107 |
-
|
| 108 |
-
# Renommer les colonnes pour une utilisation cohérente (gestion des inconsistances de l'API)
|
| 109 |
-
column_mapping = {
|
| 110 |
-
'productId': 'product_id',
|
| 111 |
-
'product_Id': 'product_id',
|
| 112 |
-
'productName': 'product_name',
|
| 113 |
-
'product_Name': 'product_name'
|
| 114 |
-
}
|
| 115 |
-
|
| 116 |
-
df_products = df_products.rename(columns={col: column_mapping[col]
|
| 117 |
-
for col in column_mapping
|
| 118 |
-
if col in df_products.columns})
|
| 119 |
-
|
| 120 |
-
# Tri par nom de produit
|
| 121 |
-
if 'product_name' in df_products.columns:
|
| 122 |
-
df_products = df_products.sort_values('product_name')
|
| 123 |
-
|
| 124 |
-
logger.info(f"Récupération réussie de {len(df_products)} produits")
|
| 125 |
-
return df_products
|
| 126 |
-
|
| 127 |
-
except Exception as e:
|
| 128 |
-
logger.error(f"Erreur lors de la récupération des produits: {str(e)}")
|
| 129 |
-
st.error(f"Erreur lors de la récupération des produits: {str(e)}")
|
| 130 |
-
return pd.DataFrame()
|
| 131 |
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
try:
|
| 143 |
-
response = make_api_request(url, params)
|
| 144 |
-
if not response:
|
| 145 |
-
logger.warning(f"Impossible de récupérer le nom de la substance {residue_id}")
|
| 146 |
-
return f"Substance {residue_id}"
|
| 147 |
-
|
| 148 |
-
data = response.json()
|
| 149 |
-
substances = data.get('value', [])
|
| 150 |
-
|
| 151 |
-
if substances:
|
| 152 |
-
# Chercher le nom dans différentes variables possibles
|
| 153 |
-
name_fields = ['pesticide_residue_name', 'pesticideResidueName', 'name']
|
| 154 |
-
for field in name_fields:
|
| 155 |
-
if field in substances[0]:
|
| 156 |
-
return substances[0][field]
|
| 157 |
-
|
| 158 |
-
# Si aucun champ de nom n'est trouvé
|
| 159 |
-
return f"Substance {residue_id}"
|
| 160 |
-
else:
|
| 161 |
-
return f"Substance {residue_id}"
|
| 162 |
-
|
| 163 |
-
except Exception as e:
|
| 164 |
-
logger.error(f"Erreur lors de la récupération du nom de la substance {residue_id}: {str(e)}")
|
| 165 |
-
return f"Substance {residue_id}"
|
| 166 |
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
7: "Acetamipride"
|
| 179 |
-
}
|
| 180 |
-
|
| 181 |
-
# Générer des données aléatoires
|
| 182 |
-
import random
|
| 183 |
-
|
| 184 |
-
# Date de base
|
| 185 |
-
base_date = datetime.datetime.now().date()
|
| 186 |
-
|
| 187 |
-
# Générer entre 20 et 40 entrées aléatoires
|
| 188 |
-
num_entries = random.randint(20, 40)
|
| 189 |
-
|
| 190 |
-
mrls_data = []
|
| 191 |
-
|
| 192 |
-
for _ in range(num_entries):
|
| 193 |
-
# Choisir une substance aléatoire
|
| 194 |
-
substance_id = random.choice(list(substances.keys()))
|
| 195 |
-
|
| 196 |
-
# Générer une date aléatoire dans les 3 dernières années
|
| 197 |
-
days_offset = random.randint(-1095, 180) # -3 ans à +6 mois
|
| 198 |
-
random_date = base_date + datetime.timedelta(days=days_offset)
|
| 199 |
-
|
| 200 |
-
# Générer une valeur LMR aléatoire (entre 0.01 et 5.0)
|
| 201 |
-
mrl_value = round(random.uniform(0.01, 5.0), 2)
|
| 202 |
-
|
| 203 |
-
# Règlement fictif
|
| 204 |
-
regulation = f"{random.randint(2020, 2024)}/{random.randint(100, 999)}"
|
| 205 |
-
|
| 206 |
-
mrls_data.append({
|
| 207 |
-
"pesticide_residue_id": substance_id,
|
| 208 |
-
"substance_name": substances[substance_id],
|
| 209 |
-
"mrl_value": mrl_value,
|
| 210 |
-
"application_date": pd.to_datetime(random_date),
|
| 211 |
-
"regulation_number": regulation
|
| 212 |
})
|
| 213 |
-
|
| 214 |
-
# Convertir en DataFrame
|
| 215 |
-
df_mrls = pd.DataFrame(mrls_data)
|
| 216 |
-
|
| 217 |
-
# Appliquer le filtre temporel si spécifié
|
| 218 |
-
if time_filter:
|
| 219 |
-
today = datetime.datetime.now().date()
|
| 220 |
-
|
| 221 |
-
if time_filter == 'last_week':
|
| 222 |
-
one_week_ago = today - datetime.timedelta(days=7)
|
| 223 |
-
df_mrls = df_mrls[df_mrls['application_date'].dt.date >= one_week_ago]
|
| 224 |
-
|
| 225 |
-
elif time_filter == 'last_month':
|
| 226 |
-
one_month_ago = today - datetime.timedelta(days=30)
|
| 227 |
-
df_mrls = df_mrls[df_mrls['application_date'].dt.date >= one_month_ago]
|
| 228 |
-
|
| 229 |
-
elif time_filter == 'next_six_months':
|
| 230 |
-
six_months_later = today + datetime.timedelta(days=180)
|
| 231 |
-
df_mrls = df_mrls[(df_mrls['application_date'].dt.date >= today) &
|
| 232 |
-
(df_mrls['application_date'].dt.date <= six_months_later)]
|
| 233 |
-
|
| 234 |
-
return df_mrls
|
| 235 |
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
"""Récupère les LMR pour un produit spécifique avec gestion de la pagination."""
|
| 239 |
-
# Vérifier si le mode démo est activé
|
| 240 |
-
if hasattr(st.session_state, 'demo_mode') and st.session_state.demo_mode:
|
| 241 |
-
return load_demo_mrls(product_id, time_filter)
|
| 242 |
-
|
| 243 |
-
url = f"{BASE_URL}/pesticide_residues_mrls"
|
| 244 |
-
params = {
|
| 245 |
-
"format": DEFAULT_FORMAT,
|
| 246 |
-
"product_id": product_id,
|
| 247 |
-
"api-version": API_VERSION
|
| 248 |
-
}
|
| 249 |
-
|
| 250 |
-
all_mrls = []
|
| 251 |
-
next_link = None
|
| 252 |
-
|
| 253 |
-
try:
|
| 254 |
-
with st.spinner("Chargement des LMR..."):
|
| 255 |
-
progress_bar = st.progress(0)
|
| 256 |
-
pages_processed = 0
|
| 257 |
-
|
| 258 |
-
while True:
|
| 259 |
-
if next_link:
|
| 260 |
-
response = make_api_request(next_link)
|
| 261 |
-
else:
|
| 262 |
-
response = make_api_request(url, params)
|
| 263 |
-
|
| 264 |
-
if not response:
|
| 265 |
-
if not all_mrls: # Si aucune donnée n'a été récupérée
|
| 266 |
-
st.error("Impossible de récupérer les LMR pour ce produit. Veuillez réessayer plus tard.")
|
| 267 |
-
return pd.DataFrame()
|
| 268 |
-
else: # Si des données partielles ont été récupérées
|
| 269 |
-
st.warning("Récupération partielle des LMR. Certaines données peuvent manquer.")
|
| 270 |
-
break
|
| 271 |
-
|
| 272 |
-
data = response.json()
|
| 273 |
-
|
| 274 |
-
# Récupérer les LMR de la page actuelle
|
| 275 |
-
mrls = data.get('value', [])
|
| 276 |
-
all_mrls.extend(mrls)
|
| 277 |
-
|
| 278 |
-
# Vérifier s'il y a une page suivante
|
| 279 |
-
next_link = data.get('nextLink') or data.get('@odata.nextLink')
|
| 280 |
-
|
| 281 |
-
# Mettre à jour la barre de progression
|
| 282 |
-
pages_processed += 1
|
| 283 |
-
progress_bar.progress(min(1.0, pages_processed / 10)) # Estimation de progression
|
| 284 |
-
|
| 285 |
-
if not next_link:
|
| 286 |
-
progress_bar.progress(1.0)
|
| 287 |
-
break
|
| 288 |
-
|
| 289 |
-
if not all_mrls:
|
| 290 |
-
st.info("Aucune LMR trouvée pour ce produit.")
|
| 291 |
-
return pd.DataFrame()
|
| 292 |
-
|
| 293 |
-
# Convertir en DataFrame
|
| 294 |
-
df_mrls = pd.DataFrame(all_mrls)
|
| 295 |
-
|
| 296 |
-
# Renommer les colonnes pour une utilisation cohérente
|
| 297 |
-
column_mapping = {
|
| 298 |
-
'pesticideResidueId': 'pesticide_residue_id',
|
| 299 |
-
'pesticide_Residue_Id': 'pesticide_residue_id',
|
| 300 |
-
'mrlValue': 'mrl_value',
|
| 301 |
-
'mrl_Value': 'mrl_value',
|
| 302 |
-
'applicationDate': 'application_date',
|
| 303 |
-
'application_Date': 'application_date',
|
| 304 |
-
'regulationNumber': 'regulation_number',
|
| 305 |
-
'regulation_Number': 'regulation_number'
|
| 306 |
-
}
|
| 307 |
-
|
| 308 |
-
df_mrls = df_mrls.rename(columns={col: column_mapping[col]
|
| 309 |
-
for col in column_mapping
|
| 310 |
-
if col in df_mrls.columns})
|
| 311 |
-
|
| 312 |
-
# Convertir les dates (gestion de différents formats possibles)
|
| 313 |
-
if 'application_date' in df_mrls.columns:
|
| 314 |
-
df_mrls['application_date'] = pd.to_datetime(df_mrls['application_date'], errors='coerce')
|
| 315 |
-
|
| 316 |
-
# Ajouter le nom de la substance
|
| 317 |
-
if 'pesticide_residue_id' in df_mrls.columns:
|
| 318 |
-
df_mrls['substance_name'] = df_mrls['pesticide_residue_id'].apply(get_residue_name)
|
| 319 |
-
|
| 320 |
-
# Appliquer le filtre temporel si spécifié
|
| 321 |
-
if time_filter and 'application_date' in df_mrls.columns:
|
| 322 |
-
today = datetime.datetime.now().date()
|
| 323 |
-
|
| 324 |
-
if time_filter == 'last_week':
|
| 325 |
-
one_week_ago = today - datetime.timedelta(days=7)
|
| 326 |
-
df_mrls = df_mrls[df_mrls['application_date'].dt.date >= one_week_ago]
|
| 327 |
-
|
| 328 |
-
elif time_filter == 'last_month':
|
| 329 |
-
one_month_ago = today - datetime.timedelta(days=30)
|
| 330 |
-
df_mrls = df_mrls[df_mrls['application_date'].dt.date >= one_month_ago]
|
| 331 |
-
|
| 332 |
-
elif time_filter == 'next_six_months':
|
| 333 |
-
six_months_later = today + datetime.timedelta(days=180)
|
| 334 |
-
df_mrls = df_mrls[(df_mrls['application_date'].dt.date >= today) &
|
| 335 |
-
(df_mrls['application_date'].dt.date <= six_months_later)]
|
| 336 |
-
|
| 337 |
-
logger.info(f"Récupération réussie de {len(df_mrls)} LMR pour le produit {product_id}")
|
| 338 |
-
return df_mrls
|
| 339 |
-
|
| 340 |
-
except Exception as e:
|
| 341 |
-
logger.error(f"Erreur lors de la récupération des LMR pour le produit {product_id}: {str(e)}")
|
| 342 |
-
st.error(f"Erreur lors de la récupération des LMR: {str(e)}")
|
| 343 |
-
return pd.DataFrame()
|
| 344 |
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
#
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
# headers["X-API-Key"] = api_key
|
| 356 |
-
# ou
|
| 357 |
-
# headers["Ocp-Apim-Subscription-Key"] = api_key
|
| 358 |
-
|
| 359 |
-
for attempt in range(RETRY_ATTEMPTS):
|
| 360 |
-
try:
|
| 361 |
-
response = requests.get(url, params=params, headers=headers)
|
| 362 |
-
|
| 363 |
-
# Si nous obtenons une erreur 403 et que nous n'avons pas de clé API,
|
| 364 |
-
# lever une exception spécifique
|
| 365 |
-
if response.status_code == 403 and not api_key:
|
| 366 |
-
raise Exception("Authentification requise pour accéder à l'API")
|
| 367 |
-
|
| 368 |
-
response.raise_for_status()
|
| 369 |
-
return response
|
| 370 |
-
|
| 371 |
-
except requests.exceptions.RequestException as e:
|
| 372 |
-
logger.warning(f"Tentative {attempt+1}/{RETRY_ATTEMPTS} échouée: {str(e)}")
|
| 373 |
-
|
| 374 |
-
if attempt < RETRY_ATTEMPTS - 1:
|
| 375 |
-
time.sleep(RETRY_DELAY * (attempt + 1)) # Délai progressif
|
| 376 |
-
else:
|
| 377 |
-
logger.error(f"Échec de la requête après {RETRY_ATTEMPTS} tentatives: {url}")
|
| 378 |
-
return None
|
| 379 |
-
|
| 380 |
-
except Exception as e:
|
| 381 |
-
logger.error(f"Erreur: {str(e)}")
|
| 382 |
-
return None
|
| 383 |
|
| 384 |
-
|
| 385 |
-
"""Génère un lien de téléchargement pour un DataFrame."""
|
| 386 |
csv = df.to_csv(index=False)
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
"""Crée un graphique montrant l'évolution des LMR dans le temps."""
|
| 393 |
-
if df_mrls.empty or 'application_date' not in df_mrls.columns or 'mrl_value' not in df_mrls.columns:
|
| 394 |
-
return None
|
| 395 |
-
|
| 396 |
-
# Sélectionner les 10 substances les plus fréquentes
|
| 397 |
-
top_substances = df_mrls['substance_name'].value_counts().nlargest(10).index.tolist()
|
| 398 |
-
df_plot = df_mrls[df_mrls['substance_name'].isin(top_substances)]
|
| 399 |
-
|
| 400 |
-
# Filtrer les valeurs NaN
|
| 401 |
-
df_plot = df_plot.dropna(subset=['application_date', 'mrl_value'])
|
| 402 |
-
|
| 403 |
-
if df_plot.empty:
|
| 404 |
-
return None
|
| 405 |
-
|
| 406 |
-
# Créer le graphique
|
| 407 |
-
fig = px.scatter(
|
| 408 |
-
df_plot,
|
| 409 |
-
x='application_date',
|
| 410 |
-
y='mrl_value',
|
| 411 |
-
color='substance_name',
|
| 412 |
-
hover_name='substance_name',
|
| 413 |
-
size_max=10,
|
| 414 |
-
title='Évolution des LMR par substance active',
|
| 415 |
-
labels={
|
| 416 |
-
'application_date': 'Date d\'application',
|
| 417 |
-
'mrl_value': 'Valeur LMR (mg/kg)',
|
| 418 |
-
'substance_name': 'Substance active'
|
| 419 |
-
}
|
| 420 |
-
)
|
| 421 |
-
|
| 422 |
-
fig.update_layout(
|
| 423 |
-
xaxis_title='Date d\'application',
|
| 424 |
-
yaxis_title='Valeur LMR (mg/kg)',
|
| 425 |
-
legend_title='Substance active',
|
| 426 |
-
height=600
|
| 427 |
)
|
| 428 |
-
|
| 429 |
-
return fig
|
| 430 |
-
|
| 431 |
-
# Fonction principale de l'application
|
| 432 |
-
def main():
|
| 433 |
-
# Barre latérale
|
| 434 |
-
with st.sidebar:
|
| 435 |
-
st.image("https://european-union.europa.eu/sites/default/files/styles/oe_theme_medium_no_crop/public/2021-12/Flag_of_Europe.png", width=150)
|
| 436 |
-
st.title("Pesticide Data Explorer")
|
| 437 |
-
st.markdown("Application pour explorer les données de pesticides de l'Union Européenne.")
|
| 438 |
-
|
| 439 |
-
# Section d'authentification
|
| 440 |
-
st.subheader("Authentification API")
|
| 441 |
-
api_key = st.text_input("Clé API (obligatoire)",
|
| 442 |
-
type="password",
|
| 443 |
-
help="Entrez votre clé API pour accéder aux données")
|
| 444 |
-
st.session_state.api_key = api_key
|
| 445 |
-
|
| 446 |
-
st.info("""
|
| 447 |
-
Pour obtenir une clé API, inscrivez-vous sur le portail européen des données
|
| 448 |
-
et demandez l'accès à l'API des pesticides.
|
| 449 |
-
""")
|
| 450 |
-
|
| 451 |
-
# Mode démonstration
|
| 452 |
-
st.subheader("Mode démonstration")
|
| 453 |
-
use_demo = st.checkbox("Utiliser des données de démonstration",
|
| 454 |
-
help="Activez cette option pour utiliser des données fictives et tester l'application sans clé API")
|
| 455 |
-
if use_demo:
|
| 456 |
-
st.session_state.demo_mode = True
|
| 457 |
-
st.success("Mode démonstration activé!")
|
| 458 |
-
else:
|
| 459 |
-
st.session_state.demo_mode = False
|
| 460 |
-
|
| 461 |
-
# Bouton de réinitialisation du cache
|
| 462 |
-
if st.button("Réinitialiser le cache"):
|
| 463 |
-
st.cache_data.clear()
|
| 464 |
-
st.success("Cache réinitialisé avec succès!")
|
| 465 |
-
|
| 466 |
-
# Titre principal
|
| 467 |
-
st.title("Base de données des pesticides de l'UE")
|
| 468 |
-
st.markdown("Explorez les Limites Maximales de Résidus (LMR) par produit agricole")
|
| 469 |
-
|
| 470 |
-
# Vérifier si le mode démo est activé ou si une clé API est fournie
|
| 471 |
-
if not ((hasattr(st.session_state, 'demo_mode') and st.session_state.demo_mode) or get_api_key()):
|
| 472 |
-
st.error("""
|
| 473 |
-
⚠️ Authentification requise!
|
| 474 |
-
|
| 475 |
-
Cette API nécessite une clé d'accès. Veuillez soit:
|
| 476 |
-
1. Entrer votre clé API dans la barre latérale pour accéder aux données réelles, ou
|
| 477 |
-
2. Activer le "Mode démonstration" pour utiliser des données fictives.
|
| 478 |
-
|
| 479 |
-
Erreur actuelle: 403 Forbidden - L'accès à l'API est refusé sans authentification valide.
|
| 480 |
-
""")
|
| 481 |
-
|
| 482 |
-
# Afficher une section d'aide pour obtenir une clé API
|
| 483 |
-
col1, col2 = st.columns(2)
|
| 484 |
-
|
| 485 |
-
with col1:
|
| 486 |
-
st.subheader("Option 1: Obtenir une clé API")
|
| 487 |
-
st.markdown("""
|
| 488 |
-
1. Rendez-vous sur le [Portail des données de l'UE](https://data.europa.eu/)
|
| 489 |
-
2. Créez un compte ou connectez-vous
|
| 490 |
-
3. Naviguez vers la section API des pesticides
|
| 491 |
-
4. Demandez une clé d'accès pour l'API
|
| 492 |
-
5. Copiez la clé dans le champ "Clé API" dans la barre latérale
|
| 493 |
-
""")
|
| 494 |
-
|
| 495 |
-
with col2:
|
| 496 |
-
st.subheader("Option 2: Mode démonstration")
|
| 497 |
-
st.markdown("""
|
| 498 |
-
1. Activez l'option "Utiliser des données de démonstration" dans la barre latérale
|
| 499 |
-
2. Utilisez l'application avec des données fictives pour tester les fonctionnalités
|
| 500 |
-
3. Notez que les données affichées ne seront pas réelles
|
| 501 |
-
""")
|
| 502 |
-
|
| 503 |
-
if st.button("Activer le mode démonstration"):
|
| 504 |
-
st.session_state.demo_mode = True
|
| 505 |
-
st.experimental_rerun()
|
| 506 |
-
|
| 507 |
-
return
|
| 508 |
-
|
| 509 |
-
# Afficher un message si en mode démonstration
|
| 510 |
-
if hasattr(st.session_state, 'demo_mode') and st.session_state.demo_mode:
|
| 511 |
-
st.warning("""
|
| 512 |
-
🔍 **Mode démonstration activé**
|
| 513 |
-
|
| 514 |
-
Vous utilisez des données fictives pour tester l'application.
|
| 515 |
-
Les valeurs affichées ne correspondent pas aux données réelles de l'API de l'UE.
|
| 516 |
-
|
| 517 |
-
Pour accéder aux données réelles, désactivez le mode démonstration et fournissez une clé API valide.
|
| 518 |
-
""")
|
| 519 |
-
|
| 520 |
-
# Récupération de la liste des produits
|
| 521 |
-
df_products = get_products()
|
| 522 |
-
|
| 523 |
-
if df_products.empty and not (hasattr(st.session_state, 'demo_mode') and st.session_state.demo_mode):
|
| 524 |
-
st.warning("Aucun produit disponible. Veuillez vérifier votre clé API ou la connexion à l'API.")
|
| 525 |
-
|
| 526 |
-
# Ajouter des options alternatives
|
| 527 |
-
st.subheader("Options alternatives")
|
| 528 |
-
st.markdown("""
|
| 529 |
-
Si vous rencontrez des difficultés pour accéder à l'API en direct, vous pouvez:
|
| 530 |
-
|
| 531 |
-
1. **Activer le mode démonstration** dans la barre latérale pour tester les fonctionnalités
|
| 532 |
-
2. Accéder directement aux données via le site web de l'UE:
|
| 533 |
-
- [Base de données des pesticides de l'UE](https://ec.europa.eu/food/plant/pesticides/eu-pesticides-database/products/)
|
| 534 |
-
- [Téléchargement direct des fichiers CSV](https://ec.europa.eu/food/plant/pesticides/eu-pesticides-database/products/download/)
|
| 535 |
-
""")
|
| 536 |
-
|
| 537 |
-
if st.button("Activer le mode démonstration", key="demo_btn_alt"):
|
| 538 |
-
st.session_state.demo_mode = True
|
| 539 |
-
st.experimental_rerun()
|
| 540 |
-
|
| 541 |
-
return
|
| 542 |
-
|
| 543 |
-
# Interface de sélection
|
| 544 |
-
col1, col2 = st.columns(2)
|
| 545 |
-
|
| 546 |
-
with col1:
|
| 547 |
-
# Sélection du produit
|
| 548 |
-
product_options = [(row['product_id'], row['product_name'])
|
| 549 |
-
for _, row in df_products.iterrows()]
|
| 550 |
-
selected_product_id = st.selectbox(
|
| 551 |
-
"Sélectionnez un produit agricole",
|
| 552 |
-
options=[id for id, _ in product_options],
|
| 553 |
-
format_func=lambda x: next((name for id, name in product_options if id == x), str(x))
|
| 554 |
-
)
|
| 555 |
-
|
| 556 |
-
with col2:
|
| 557 |
-
# Filtre temporel
|
| 558 |
-
time_filter = st.radio(
|
| 559 |
-
"Période",
|
| 560 |
-
options=[
|
| 561 |
-
("all", "Toutes dates"),
|
| 562 |
-
("last_week", "Dernière semaine"),
|
| 563 |
-
("last_month", "Dernier mois"),
|
| 564 |
-
("next_six_months", "6 prochains mois")
|
| 565 |
-
],
|
| 566 |
-
format_func=lambda x: x[1],
|
| 567 |
-
index=0
|
| 568 |
-
)
|
| 569 |
-
|
| 570 |
-
# Bouton pour lancer l'analyse
|
| 571 |
-
if st.button("Analyser les données", type="primary"):
|
| 572 |
-
# Récupérer les LMR pour le produit sélectionné
|
| 573 |
-
df_mrls = get_mrls_for_product(selected_product_id, time_filter[0] if time_filter[0] != "all" else None)
|
| 574 |
-
|
| 575 |
-
if df_mrls.empty:
|
| 576 |
-
st.info(f"Aucune donnée LMR trouvée pour ce produit avec les filtres sélectionnés.")
|
| 577 |
-
return
|
| 578 |
-
|
| 579 |
-
# Afficher les résultats
|
| 580 |
-
st.subheader("Limites Maximales de Résidus (LMR)")
|
| 581 |
-
|
| 582 |
-
# Formater les données pour l'affichage
|
| 583 |
-
display_df = df_mrls.copy()
|
| 584 |
-
|
| 585 |
-
# Formater les liens vers les règlements
|
| 586 |
-
if 'regulation_number' in display_df.columns:
|
| 587 |
-
display_df['regulation_number'] = display_df['regulation_number'].apply(
|
| 588 |
-
lambda x: f'<a href="https://eur-lex.europa.eu/search.html?qid=1673437220860&text={x}&scope=EURLEX&type=quick&lang=fr" target="_blank">{x}</a>' if x else "")
|
| 589 |
-
|
| 590 |
-
# Sélectionner et renommer les colonnes pour l'affichage
|
| 591 |
-
columns_to_display = {
|
| 592 |
-
'substance_name': 'Substance active',
|
| 593 |
-
'mrl_value': 'LMR (mg/kg)',
|
| 594 |
-
'application_date': 'Date d\'application',
|
| 595 |
-
'regulation_number': 'Règlement'
|
| 596 |
-
}
|
| 597 |
-
|
| 598 |
-
display_df = display_df[[col for col in columns_to_display.keys() if col in display_df.columns]]
|
| 599 |
-
display_df = display_df.rename(columns=columns_to_display)
|
| 600 |
-
|
| 601 |
-
# Formater la date
|
| 602 |
-
if 'Date d\'application' in display_df.columns:
|
| 603 |
-
display_df['Date d\'application'] = display_df['Date d\'application'].dt.strftime('%d/%m/%Y')
|
| 604 |
-
|
| 605 |
-
# Afficher le tableau avec liens cliquables
|
| 606 |
-
st.markdown(display_df.to_html(escape=False, index=False), unsafe_allow_html=True)
|
| 607 |
-
|
| 608 |
-
# Afficher le nombre total de LMR
|
| 609 |
-
st.info(f"Nombre total de LMR: {len(df_mrls)}")
|
| 610 |
-
|
| 611 |
-
# Lien de téléchargement
|
| 612 |
-
st.markdown(generate_download_link(df_mrls), unsafe_allow_html=True)
|
| 613 |
-
|
| 614 |
-
# Graphique d'évolution des LMR
|
| 615 |
-
st.subheader("Évolution des LMR dans le temps")
|
| 616 |
-
fig = create_mrls_chart(df_mrls)
|
| 617 |
-
|
| 618 |
-
if fig:
|
| 619 |
-
st.plotly_chart(fig, use_container_width=True)
|
| 620 |
-
else:
|
| 621 |
-
st.info("Pas assez de données pour générer un graphique.")
|
| 622 |
-
|
| 623 |
-
if __name__ == "__main__":
|
| 624 |
-
main()
|
|
|
|
| 1 |
import streamlit as st
|
|
|
|
|
|
|
| 2 |
import requests
|
| 3 |
+
import pandas as pd
|
| 4 |
+
import matplotlib.pyplot as plt
|
| 5 |
+
from datetime import datetime, timedelta
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
+
# Configuration de l'application
|
| 8 |
+
st.title("Base de données des pesticides de l'UE")
|
|
|
|
|
|
|
| 9 |
|
| 10 |
+
# Fonction pour récupérer la liste des produits
|
| 11 |
+
@st.cache_data
|
| 12 |
+
def get_products():
|
| 13 |
+
url = "https://api.datalake.sante.service.ec.europa.eu/sante/pesticides/pesticide_residues_products"
|
| 14 |
+
params = {"format": "json", "language": "FR", "api-version": "v2.0"}
|
| 15 |
+
response = requests.get(url, params=params)
|
| 16 |
+
response.raise_for_status()
|
| 17 |
+
return response.json()
|
| 18 |
|
| 19 |
+
# Fonction pour récupérer les LMR par produit
|
| 20 |
+
@st.cache_data
|
| 21 |
+
def get_mrls(product_id):
|
| 22 |
+
url = f"https://api.datalake.sante.service.ec.europa.eu/sante/pesticides/pesticide_residues_mrls"
|
| 23 |
+
params = {"format": "json", "product_id": product_id, "api-version": "v2.0"}
|
| 24 |
+
response = requests.get(url, params=params)
|
| 25 |
+
response.raise_for_status()
|
| 26 |
+
return response.json()
|
| 27 |
|
| 28 |
+
# Fonction pour récupérer les informations sur les substances
|
| 29 |
+
@st.cache_data
|
| 30 |
+
def get_substance_info(substance_id):
|
| 31 |
+
url = f"https://api.datalake.sante.service.ec.europa.eu/sante/pesticides/pesticide_residues"
|
| 32 |
+
params = {"format": "json", "pesticide_residue_id": substance_id, "api-version": "v2.0"}
|
| 33 |
+
response = requests.get(url, params=params)
|
| 34 |
+
response.raise_for_status()
|
| 35 |
+
return response.json()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
+
# Interface utilisateur
|
| 38 |
+
products = get_products()
|
| 39 |
+
product_names = [product['name'] for product in products]
|
| 40 |
+
selected_product = st.selectbox("Sélectionnez un produit agricole", product_names)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
+
# Filtrage par période
|
| 43 |
+
period = st.radio("Filtrer par période", ["Toutes dates", "Dernière semaine", "Dernier mois", "6 prochains mois"])
|
| 44 |
+
|
| 45 |
+
# Bouton pour lancer l'analyse
|
| 46 |
+
if st.button("Analyser les données"):
|
| 47 |
+
product_id = next(product['id'] for product in products if product['name'] == selected_product)
|
| 48 |
+
mrls = get_mrls(product_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
|
| 50 |
+
# Filtrer les données par période
|
| 51 |
+
today = datetime.today()
|
| 52 |
+
if period == "Dernière semaine":
|
| 53 |
+
start_date = today - timedelta(days=7)
|
| 54 |
+
elif period == "Dernier mois":
|
| 55 |
+
start_date = today - timedelta(days=30)
|
| 56 |
+
elif period == "6 prochains mois":
|
| 57 |
+
start_date = today + timedelta(days=180)
|
| 58 |
+
else:
|
| 59 |
+
start_date = datetime.min
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
+
filtered_mrls = [mrl for mrl in mrls if datetime.strptime(mrl['date'], "%Y-%m-%d") >= start_date]
|
| 62 |
+
|
| 63 |
+
# Afficher les résultats dans un tableau
|
| 64 |
+
mrl_data = []
|
| 65 |
+
for mrl in filtered_mrls:
|
| 66 |
+
substance_info = get_substance_info(mrl['substance_id'])
|
| 67 |
+
mrl_data.append({
|
| 68 |
+
"Substance": substance_info['name'],
|
| 69 |
+
"Valeur LMR": mrl['value'],
|
| 70 |
+
"Date d'application": mrl['date'],
|
| 71 |
+
"Règlement": f"[Lien]({mrl['regulation_url']})"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
|
| 74 |
+
df = pd.DataFrame(mrl_data)
|
| 75 |
+
st.write(df)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
+
# Visualisation graphique
|
| 78 |
+
plt.figure(figsize=(10, 6))
|
| 79 |
+
for substance in df['Substance'].unique()[:10]: # Limiter aux 10 substances les plus fréquentes
|
| 80 |
+
subset = df[df['Substance'] == substance]
|
| 81 |
+
plt.plot(subset['Date d'application'], subset['Valeur LMR'], label=substance)
|
| 82 |
+
plt.xlabel("Date d'application")
|
| 83 |
+
plt.ylabel("Valeur LMR")
|
| 84 |
+
plt.title("Évolution des LMR dans le temps")
|
| 85 |
+
plt.legend()
|
| 86 |
+
st.pyplot(plt)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
|
| 88 |
+
# Téléchargement CSV
|
|
|
|
| 89 |
csv = df.to_csv(index=False)
|
| 90 |
+
st.download_button(
|
| 91 |
+
label="Télécharger les données en CSV",
|
| 92 |
+
data=csv,
|
| 93 |
+
file_name="lmr_data.csv",
|
| 94 |
+
mime="text/csv",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|