Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| import pandas as pd | |
| import sqlite3 | |
| import requests | |
| import os | |
| import plotly.express as px | |
| import plotly.graph_objects as go | |
| from datetime import datetime | |
| import tempfile | |
| import time | |
| # === CONFIGURATION DE LA PAGE === | |
| st.set_page_config( | |
| page_title="RASFF Tableau de Bord", | |
| page_icon="🚨", | |
| layout="wide", | |
| initial_sidebar_state="expanded" | |
| ) | |
| # === CSS PERSONNALISÉ === | |
| st.markdown(""" | |
| <style> | |
| .main-header { | |
| font-size: 42px; | |
| font-weight: bold; | |
| color: #FF5A5F; | |
| margin-bottom: 20px; | |
| text-align: center; | |
| padding: 20px; | |
| background-color: #f0f2f6; | |
| border-radius: 10px; | |
| } | |
| .subheader { | |
| font-size: 24px; | |
| font-weight: bold; | |
| color: #1E88E5; | |
| margin: 20px 0; | |
| padding-left: 10px; | |
| border-left: 5px solid #1E88E5; | |
| } | |
| .stat-box { | |
| background-color: #f0f2f6; | |
| border-radius: 10px; | |
| padding: 20px; | |
| text-align: center; | |
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
| } | |
| .stat-value { | |
| font-size: 36px; | |
| font-weight: bold; | |
| color: #1E88E5; | |
| } | |
| .stat-label { | |
| font-size: 16px; | |
| color: #616161; | |
| } | |
| .filter-section { | |
| background-color: #f8f9fa; | |
| padding: 20px; | |
| border-radius: 10px; | |
| margin-bottom: 20px; | |
| } | |
| .download-btn { | |
| background-color: #4CAF50; | |
| color: white; | |
| padding: 10px 24px; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| font-size: 16px; | |
| margin: 10px 0; | |
| } | |
| .stDataFrame { | |
| border-radius: 10px; | |
| overflow: hidden; | |
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
| } | |
| .banner { | |
| background-image: url('https://github.com/M00N69/BUSCAR/blob/main/logo%2002%20copie.jpg?raw=true'); | |
| background-size: cover; | |
| height: 120px; | |
| border-radius: 5px; | |
| margin-top: -60px; | |
| margin-bottom: 20px; | |
| box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # === CSS PERSONNALISÉ POUR LE EXPANDER === | |
| st.markdown(""" | |
| <style> | |
| .custom-expander .streamlit-expanderHeader { | |
| background-color: #FFEBCC; /* Couleur de fond personnalisée (orange clair) */ | |
| color: #333333; /* Couleur du texte */ | |
| font-weight: bold; | |
| padding: 5px 10px; | |
| border-radius: 5px; | |
| } | |
| .custom-expander .streamlit-expanderContent { | |
| background-color: #FFF5E6; /* Fond légèrement plus clair pour l'intérieur */ | |
| padding: 10px; | |
| border: 1px solid #FFCC99; /* Bordure fine pour l'intérieur */ | |
| border-radius: 0 0 5px 5px; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # === BANNER VISIPILOT === | |
| st.sidebar.markdown(""" | |
| <div style="text-align: center; margin-bottom: 20px;"> | |
| <a href="https://visipilot.fr" target="_blank"> | |
| <img src="https://visipilot.fr/wp-content/uploads/2023/07/cropped-logo-1.png" width="200"> | |
| </a> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # === EXPANDER PERSONNALISÉ === | |
| with st.expander("ℹ️ Guide d'utilisation du Dashboard", expanded=True): | |
| st.markdown(""" | |
| <div class="custom-expander"> | |
| <p>Bienvenue sur le <b>RASFF Alerts Dashboard</b>! 👋</p> | |
| <ul> | |
| <li>Utilisez les <b>filtres</b> dans la barre latérale pour affiner les alertes affichées.</li> | |
| <li>Les statistiques clés et graphiques se mettent à jour automatiquement selon vos choix.</li> | |
| <li><b>Descendez plus bas</b> pour explorer les graphiques détaillés et les tableaux d'alertes.</li> | |
| <li>Utilisez les boutons de téléchargement pour exporter les données filtrées en CSV.</li> | |
| </ul> | |
| <p>Bonne exploration des données! 🚀</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| DB_PATH = "rasff_data.db" | |
| REPO_OWNER = "M00N69" | |
| REPO_NAME = "RASFFDB" | |
| FILE_PATH = "rasff_data.db" | |
| # === TÉLÉCHARGEMENT ET MISE À JOUR DE LA BASE DE DONNÉES === | |
| def get_github_file_info(): | |
| """Récupère les informations sur le fichier stocké sur GitHub (comme le SHA)""" | |
| api_url = f"https://api.github.com/repos/{REPO_OWNER}/{REPO_NAME}/contents/{FILE_PATH}" | |
| response = requests.get(api_url) | |
| if response.status_code == 200: | |
| return response.json() | |
| return None | |
| def download_database(): | |
| with st.spinner("Téléchargement de la base de données en cours..."): | |
| url = f"https://raw.githubusercontent.com/{REPO_OWNER}/{REPO_NAME}/main/{FILE_PATH}" | |
| response = requests.get(url) | |
| if response.status_code == 200: | |
| with open(DB_PATH, "wb") as file: | |
| file.write(response.content) | |
| # Enregistrer la version actuelle | |
| if 'github_sha' in st.session_state: | |
| with open(f"{DB_PATH}.sha", "w") as f: | |
| f.write(st.session_state['github_sha']) | |
| st.success("✅ Base de données téléchargée avec succès!") | |
| return True | |
| else: | |
| st.error("❌ Erreur lors du téléchargement de la base de données.") | |
| return False | |
| def check_database_update(): | |
| # Récupérer les informations du fichier sur GitHub | |
| github_info = get_github_file_info() | |
| if not github_info: | |
| st.warning("⚠️ Impossible de vérifier les mises à jour. Utilisation de la version locale.") | |
| return False | |
| # Stocker le SHA actuel pour l'enregistrer après téléchargement | |
| st.session_state['github_sha'] = github_info.get('sha', '') | |
| # Vérifier si nous avons déjà cette version | |
| if os.path.exists(f"{DB_PATH}.sha"): | |
| with open(f"{DB_PATH}.sha", "r") as f: | |
| local_sha = f.read().strip() | |
| # Si les SHA correspondent, la base de données est à jour | |
| if local_sha == st.session_state['github_sha']: | |
| st.info("✅ La base de données est à jour.") | |
| return False | |
| # SHA différent ou fichier SHA inexistant, mise à jour nécessaire | |
| st.info("🔄 Une mise à jour de la base de données est disponible.") | |
| return True | |
| # === INITIALISATION DE LA BASE DE DONNÉES === | |
| if not os.path.exists(DB_PATH): | |
| st.write("📥 Premier téléchargement de la base de données...") | |
| download_database() | |
| else: | |
| # Vérifier s'il y a des mises à jour | |
| st.write("📥 Vérification des mises à jour de la base de données...") | |
| if check_database_update(): | |
| download_database() | |
| # === CHARGEMENT DES DONNÉES === | |
| def load_data(): | |
| conn = sqlite3.connect(DB_PATH) | |
| df = pd.read_sql_query("SELECT * FROM rasff_notifications", conn) | |
| conn.close() | |
| # Assurer la conversion correcte des colonnes 'year' et 'week' en entiers | |
| df['year'] = df['year'].astype(int) | |
| df['week'] = df['week'].astype(int) | |
| # Créer une colonne de date approximative pour meilleure visualisation | |
| df['date_approx'] = pd.to_datetime(df['year'].astype(str) + '-' + df['week'].astype(str) + '-1', format='%Y-%W-%w') | |
| return df | |
| # Afficher un spinner pendant le chargement des données | |
| with st.spinner("Chargement des données..."): | |
| df = load_data() | |
| # === SIDEBAR: FILTRES === | |
| # Ajout du logo VisiPilot en haut de la sidebar | |
| st.sidebar.markdown(""" | |
| <div style="text-align: center;"> | |
| <a href="https://www.visipilot.com" target="_blank"> | |
| <img src="https://raw.githubusercontent.com/M00N69/RAPPELCONSO/main/logo%2004%20copie.jpg" alt="Visipilot Logo" style="width: 250px; margin-top: 20px;"> | |
| </a> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.sidebar.markdown("<h2 style='text-align: center; color: #FF5A5F;'>🔍 Filtres</h2>", unsafe_allow_html=True) | |
| # Ajout d'un accordéon pour organiser les filtres | |
| with st.sidebar.expander("⏱️ Filtres temporels", expanded=True): | |
| # Filtre: Année | |
| annees = sorted(df['year'].unique(), reverse=True) | |
| annee_selectionnee = st.selectbox("Année", ["Tous"] + [str(annee) for annee in annees]) | |
| # Filtre: Semaine (conditionnel à l'année) | |
| if annee_selectionnee != "Tous": | |
| semaines_disponibles = sorted(df[df['year'] == int(annee_selectionnee)]['week'].unique()) | |
| semaine_selectionnee = st.selectbox("Semaine", ["Toutes"] + [str(semaine) for semaine in semaines_disponibles]) | |
| else: | |
| semaines = sorted(df['week'].unique()) | |
| semaine_selectionnee = st.selectbox("Semaine", ["Toutes"] + [str(semaine) for semaine in semaines]) | |
| with st.sidebar.expander("🌍 Filtres géographiques", expanded=True): | |
| # Filtre: Pays | |
| pays = sorted(df['notifying_country'].dropna().unique()) | |
| pays_selectionne = st.selectbox("Pays notifiant", ["Tous"] + pays) | |
| # Nouvel ajout: filtre sur pays d'origine | |
| pays_origine = sorted(df['origin'].dropna().unique()) | |
| pays_origine_selectionne = st.selectbox("Pays d'origine", ["Tous"] + pays_origine) | |
| with st.sidebar.expander("🍎 Filtres produits et risques", expanded=True): | |
| # Filtre: Produits | |
| produits = sorted(df['category'].dropna().unique()) | |
| produit_selectionne = st.selectbox("Catégorie de produit", ["Tous"] + produits) | |
| # Filtre: Type de risque | |
| types_risque = sorted(df['hazards'].dropna().unique()) | |
| type_risque_selectionne = st.selectbox("Type de risque", ["Tous"] + types_risque) | |
| # Ajout filtre: Type de notification | |
| types_notification = sorted(df['type'].dropna().unique()) | |
| type_notification_selectionne = st.selectbox("Type de notification", ["Tous"] + types_notification) | |
| # Filtre par mots-clés (recherche dans subjects et hazards) | |
| st.markdown("---") # Séparateur visuel | |
| mot_cle = st.text_input("🔍 Recherche par mot-clé", | |
| placeholder="Ex: Salmonella, fruits, etc.", | |
| help="Recherche dans les sujets et types de dangers (non sensible à la casse)") | |
| # Bouton pour réinitialiser tous les filtres | |
| if st.sidebar.button("🔄 Réinitialiser tous les filtres", use_container_width=True): | |
| for key in st.session_state.keys(): | |
| del st.session_state[key] | |
| # Ajout d'une section d'information | |
| with st.sidebar.expander("ℹ️ À propos de RASFF", expanded=False): | |
| st.markdown(""" | |
| **RASFF** (Rapid Alert System for Food and Feed) est le système d'alerte rapide pour les denrées alimentaires et les aliments pour animaux de l'Union européenne. | |
| Ce dashboard vous permet d'explorer les alertes RASFF pour identifier les tendances et les risques émergents. | |
| Dernière mise à jour: {} | |
| """.format(datetime.now().strftime("%d/%m/%Y"))) | |
| # === APPLICATION DES FILTRES === | |
| df_filtre = df.copy() | |
| if annee_selectionnee != "Tous": | |
| df_filtre = df_filtre[df_filtre['year'] == int(annee_selectionnee)] | |
| if semaine_selectionnee != "Toutes": | |
| df_filtre = df_filtre[df_filtre['week'] == int(semaine_selectionnee)] | |
| if pays_selectionne != "Tous": | |
| df_filtre = df_filtre[df_filtre['notifying_country'] == pays_selectionne] | |
| if pays_origine_selectionne != "Tous": | |
| df_filtre = df_filtre[df_filtre['origin'] == pays_origine_selectionne] | |
| if produit_selectionne != "Tous": | |
| df_filtre = df_filtre[df_filtre['category'] == produit_selectionne] | |
| if type_risque_selectionne != "Tous": | |
| df_filtre = df_filtre[df_filtre['hazards'] == type_risque_selectionne] | |
| if type_notification_selectionne != "Tous": | |
| df_filtre = df_filtre[df_filtre['type'] == type_notification_selectionne] | |
| # Application du filtre par mot-clé | |
| if mot_cle: | |
| # Convertir le mot-clé en minuscules pour une recherche insensible à la casse | |
| mot_cle_lower = mot_cle.lower() | |
| # Recherche dans subjects et hazards (avec gestion des valeurs NaN) | |
| mask_subjects = df_filtre['subject'].fillna('').str.lower().str.contains(mot_cle_lower) | |
| mask_hazards = df_filtre['hazards'].fillna('').str.lower().str.contains(mot_cle_lower) | |
| # Appliquer les deux masques avec un OU logique | |
| df_filtre = df_filtre[mask_subjects | mask_hazards] | |
| # === HEADER ET STATISTIQUES GÉNÉRALES === | |
| # Affichage de la bannière au tout début de la page principale | |
| st.markdown("<div class='banner'></div>", unsafe_allow_html=True) | |
| st.markdown("<div class='main-header'>🚨 RASFF Alerts Dashboard</div>", unsafe_allow_html=True) | |
| # KPIs en ligne | |
| col1, col2, col3, col4 = st.columns(4) | |
| with col1: | |
| st.markdown("<div class='stat-box'><div class='stat-value'>{}</div><div class='stat-label'>Alertes totales</div></div>".format(len(df_filtre)), unsafe_allow_html=True) | |
| with col2: | |
| nb_pays = df_filtre['notifying_country'].nunique() | |
| st.markdown("<div class='stat-box'><div class='stat-value'>{}</div><div class='stat-label'>Pays notifiants</div></div>".format(nb_pays), unsafe_allow_html=True) | |
| with col3: | |
| nb_produits = df_filtre['category'].nunique() | |
| st.markdown("<div class='stat-box'><div class='stat-value'>{}</div><div class='stat-label'>Catégories de produits</div></div>".format(nb_produits), unsafe_allow_html=True) | |
| with col4: | |
| nb_dangers = df_filtre['hazards'].nunique() | |
| st.markdown("<div class='stat-box'><div class='stat-value'>{}</div><div class='stat-label'>Types de dangers</div></div>".format(nb_dangers), unsafe_allow_html=True) | |
| # === VISUALISATION TEMPORELLE === | |
| st.markdown("<div class='subheader'>📅 Évolution temporelle des alertes</div>", unsafe_allow_html=True) | |
| # Créer une agrégation par date | |
| if not df_filtre.empty: | |
| df_time = df_filtre.groupby(['year', 'week']).size().reset_index(name='count') | |
| df_time['date_approx'] = pd.to_datetime(df_time['year'].astype(str) + '-' + df_time['week'].astype(str) + '-1', format='%Y-%W-%w') | |
| fig = px.line(df_time, x='date_approx', y='count', | |
| title="Évolution du nombre d'alertes au fil du temps", | |
| labels={'count': "Nombre d'alertes", 'date_approx': 'Date (Année-Semaine)'}, | |
| markers=True) | |
| fig.update_layout( | |
| hovermode='x unified', | |
| xaxis_title='Date', | |
| yaxis_title="Nombre d'alertes", | |
| template='plotly_white', | |
| height=400 | |
| ) | |
| st.plotly_chart(fig, use_container_width=True) | |
| else: | |
| st.info("Aucune donnée disponible pour l'évolution temporelle avec les filtres actuels.") | |
| # === TOP DANGERS ET PRODUITS === | |
| st.markdown("<div class='subheader'>🌟 Top 10 Dangers et Produits</div>", unsafe_allow_html=True) | |
| col1, col2 = st.columns(2) | |
| # Top 10 des dangers avec graphique amélioré | |
| with col1: | |
| if not df_filtre.empty: | |
| top_dangers = df_filtre['hazards'].value_counts().nlargest(10) | |
| if not top_dangers.empty: | |
| fig = px.bar(top_dangers, | |
| x=top_dangers.values, | |
| y=top_dangers.index, | |
| orientation='h', | |
| title="Top 10 des dangers signalés", | |
| color=top_dangers.values, | |
| color_continuous_scale='Reds') | |
| fig.update_layout( | |
| xaxis_title="Nombre d'alertes", | |
| yaxis_title="Type de danger", | |
| yaxis={'categoryorder':'total ascending'}, | |
| template='plotly_white', | |
| height=500 | |
| ) | |
| st.plotly_chart(fig, use_container_width=True) | |
| else: | |
| st.info("Aucun danger trouvé avec les filtres actuels.") | |
| else: | |
| st.info("Aucune donnée disponible pour les dangers.") | |
| # Top 10 des produits avec graphique amélioré | |
| with col2: | |
| if not df_filtre.empty: | |
| top_produits = df_filtre['category'].value_counts().nlargest(10) | |
| if not top_produits.empty: | |
| fig = px.bar(top_produits, | |
| x=top_produits.values, | |
| y=top_produits.index, | |
| orientation='h', | |
| title="Top 10 des produits concernés", | |
| color=top_produits.values, | |
| color_continuous_scale='Blues') | |
| fig.update_layout( | |
| xaxis_title="Nombre d'alertes", | |
| yaxis_title="Catégorie de produit", | |
| yaxis={'categoryorder':'total ascending'}, | |
| template='plotly_white', | |
| height=500 | |
| ) | |
| st.plotly_chart(fig, use_container_width=True) | |
| else: | |
| st.info("Aucun produit trouvé avec les filtres actuels.") | |
| else: | |
| st.info("Aucune donnée disponible pour les produits.") | |
| # === RÉPARTITION GÉOGRAPHIQUE === | |
| st.markdown("<div class='subheader'>🌍 Répartition géographique des alertes</div>", unsafe_allow_html=True) | |
| col1, col2 = st.columns(2) | |
| # Pays notifiants | |
| with col1: | |
| if not df_filtre.empty: | |
| top_pays_notifiants = df_filtre['notifying_country'].value_counts().nlargest(10) | |
| if not top_pays_notifiants.empty: | |
| fig = px.bar(top_pays_notifiants, | |
| x=top_pays_notifiants.values, | |
| y=top_pays_notifiants.index, | |
| orientation='h', | |
| title="Top 10 des pays notifiants", | |
| color=top_pays_notifiants.values, | |
| color_continuous_scale='Greens') | |
| fig.update_layout( | |
| xaxis_title="Nombre d'alertes", | |
| yaxis_title="Pays notifiant", | |
| yaxis={'categoryorder':'total ascending'}, | |
| template='plotly_white', | |
| height=400 | |
| ) | |
| st.plotly_chart(fig, use_container_width=True) | |
| else: | |
| st.info("Aucun pays notifiant trouvé avec les filtres actuels.") | |
| else: | |
| st.info("Aucune donnée disponible pour les pays notifiants.") | |
| # Pays d'origine | |
| with col2: | |
| if not df_filtre.empty: | |
| top_pays_origine = df_filtre['origin'].value_counts().nlargest(10) | |
| if not top_pays_origine.empty: | |
| fig = px.bar(top_pays_origine, | |
| x=top_pays_origine.values, | |
| y=top_pays_origine.index, | |
| orientation='h', | |
| title="Top 10 des pays d'origine des produits signalés", | |
| color=top_pays_origine.values, | |
| color_continuous_scale='Oranges') | |
| fig.update_layout( | |
| xaxis_title="Nombre d'alertes", | |
| yaxis_title="Pays d'origine", | |
| yaxis={'categoryorder':'total ascending'}, | |
| template='plotly_white', | |
| height=400 | |
| ) | |
| st.plotly_chart(fig, use_container_width=True) | |
| else: | |
| st.info("Aucun pays d'origine trouvé avec les filtres actuels.") | |
| else: | |
| st.info("Aucune donnée disponible pour les pays d'origine.") | |
| # === MATRICE DE CORRÉLATION DANGERS / PRODUITS === | |
| st.markdown("<div class='subheader'>🔄 Relation entre produits et dangers</div>", unsafe_allow_html=True) | |
| if not df_filtre.empty and len(df_filtre) > 5: | |
| # Création de la matrice produit/danger | |
| danger_product_matrix = pd.crosstab(df_filtre['hazards'], df_filtre['category']) | |
| # Sélection des top 10 dangers et produits pour une meilleure lisibilité | |
| top_dangers = df_filtre['hazards'].value_counts().nlargest(10).index.tolist() | |
| top_products = df_filtre['category'].value_counts().nlargest(10).index.tolist() | |
| # Filtrer la matrice pour les top dangers/produits | |
| filtered_matrix = danger_product_matrix.loc[danger_product_matrix.index.isin(top_dangers), | |
| danger_product_matrix.columns.isin(top_products)] | |
| # Création de la heatmap | |
| fig = px.imshow(filtered_matrix, | |
| labels=dict(x="Catégorie de produit", y="Type de danger", color="Nombre d'alertes"), | |
| x=filtered_matrix.columns, | |
| y=filtered_matrix.index, | |
| color_continuous_scale='YlOrRd', | |
| title="Heatmap des relations entre dangers et produits") | |
| fig.update_layout( | |
| height=500, | |
| xaxis={'tickangle': 45}, | |
| template='plotly_white' | |
| ) | |
| st.plotly_chart(fig, use_container_width=True) | |
| else: | |
| st.info("Pas assez de données pour générer une matrice de corrélation. Veuillez élargir vos critères de filtrage.") | |
| # === TABLEAU DES DONNÉES === | |
| with st.expander("📋 Données brutes", expanded=False): | |
| st.markdown("### Alertes RASFF ({} enregistrements)".format(len(df_filtre))) | |
| # Options de tri | |
| sort_col, sort_order = st.columns(2) | |
| with sort_col: | |
| sort_column = st.selectbox("Trier par", df_filtre.columns) | |
| with sort_order: | |
| sort_ascending = st.radio("Ordre", ["Ascendant", "Descendant"], horizontal=True) == "Ascendant" | |
| # Affichage du tableau trié | |
| df_display = df_filtre.sort_values(by=sort_column, ascending=sort_ascending) | |
| st.dataframe(df_display, use_container_width=True) | |
| # Options d'export | |
| csv = df_filtre.to_csv(index=False).encode('utf-8') | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.download_button( | |
| "📥 Télécharger en CSV", | |
| data=csv, | |
| file_name="rasff_data_filtered.csv", | |
| mime="text/csv", | |
| use_container_width=True | |
| ) | |
| with col2: | |
| pass | |
| # Pied de page | |
| st.markdown(""" | |
| <div style="text-align: center; margin-top: 40px; padding: 20px; background-color: #f0f2f6; border-radius: 10px;"> | |
| <p style="color: #616161;">RASFF Alerts Dashboard - Développé avec Streamlit</p> | |
| <p style="color: #616161; font-size: 12px;">Données issues du système d'alerte rapide pour les denrées alimentaires et les aliments pour animaux de l'UE</p> | |
| <p style="color: #616161; font-size: 12px;">Propulsé par <a href="https://visipilot.fr" target="_blank">VisiPilot</a></p> | |
| </div> | |
| """, unsafe_allow_html=True) |