| | 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 |
| |
|
| | |
| | st.set_page_config( |
| | page_title="RASFF Tableau de Bord", |
| | page_icon="🚨", |
| | layout="wide", |
| | initial_sidebar_state="expanded" |
| | ) |
| |
|
| | |
| | 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) |
| |
|
| | |
| | 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) |
| |
|
| | |
| | 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) |
| |
|
| | |
| | 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" |
| |
|
| | |
| | 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) |
| | |
| | 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(): |
| | |
| | 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 |
| | |
| | |
| | st.session_state['github_sha'] = github_info.get('sha', '') |
| | |
| | |
| | if os.path.exists(f"{DB_PATH}.sha"): |
| | with open(f"{DB_PATH}.sha", "r") as f: |
| | local_sha = f.read().strip() |
| | |
| | |
| | if local_sha == st.session_state['github_sha']: |
| | st.info("✅ La base de données est à jour.") |
| | return False |
| | |
| | |
| | st.info("🔄 Une mise à jour de la base de données est disponible.") |
| | return True |
| |
|
| |
|
| | |
| | if not os.path.exists(DB_PATH): |
| | st.write("📥 Premier téléchargement de la base de données...") |
| | download_database() |
| | else: |
| | |
| | st.write("📥 Vérification des mises à jour de la base de données...") |
| | if check_database_update(): |
| | download_database() |
| |
|
| |
|
| | |
| | @st.cache_data |
| | def load_data(): |
| | conn = sqlite3.connect(DB_PATH) |
| | df = pd.read_sql_query("SELECT * FROM rasff_notifications", conn) |
| | conn.close() |
| | |
| | |
| | df['year'] = df['year'].astype(int) |
| | df['week'] = df['week'].astype(int) |
| | |
| | |
| | df['date_approx'] = pd.to_datetime(df['year'].astype(str) + '-' + df['week'].astype(str) + '-1', format='%Y-%W-%w') |
| | |
| | return df |
| |
|
| | |
| | with st.spinner("Chargement des données..."): |
| | df = load_data() |
| |
|
| | |
| | |
| | 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) |
| |
|
| | |
| | with st.sidebar.expander("⏱️ Filtres temporels", expanded=True): |
| | |
| | annees = sorted(df['year'].unique(), reverse=True) |
| | annee_selectionnee = st.selectbox("Année", ["Tous"] + [str(annee) for annee in annees]) |
| |
|
| | |
| | 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): |
| | |
| | pays = sorted(df['notifying_country'].dropna().unique()) |
| | pays_selectionne = st.selectbox("Pays notifiant", ["Tous"] + pays) |
| | |
| | |
| | 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): |
| | |
| | produits = sorted(df['category'].dropna().unique()) |
| | produit_selectionne = st.selectbox("Catégorie de produit", ["Tous"] + produits) |
| |
|
| | |
| | types_risque = sorted(df['hazards'].dropna().unique()) |
| | type_risque_selectionne = st.selectbox("Type de risque", ["Tous"] + types_risque) |
| | |
| | |
| | types_notification = sorted(df['type'].dropna().unique()) |
| | type_notification_selectionne = st.selectbox("Type de notification", ["Tous"] + types_notification) |
| | |
| | |
| | st.markdown("---") |
| | 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)") |
| |
|
| | |
| | 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] |
| |
|
| |
|
| | |
| | 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"))) |
| |
|
| | |
| | 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] |
| |
|
| | |
| | if mot_cle: |
| | |
| | mot_cle_lower = mot_cle.lower() |
| | |
| | |
| | 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) |
| | |
| | |
| | df_filtre = df_filtre[mask_subjects | mask_hazards] |
| |
|
| | |
| | |
| | st.markdown("<div class='banner'></div>", unsafe_allow_html=True) |
| | st.markdown("<div class='main-header'>🚨 RASFF Alerts Dashboard</div>", unsafe_allow_html=True) |
| |
|
| | |
| | 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) |
| |
|
| | |
| | st.markdown("<div class='subheader'>📅 Évolution temporelle des alertes</div>", unsafe_allow_html=True) |
| |
|
| | |
| | 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.") |
| |
|
| | |
| | st.markdown("<div class='subheader'>🌟 Top 10 Dangers et Produits</div>", unsafe_allow_html=True) |
| |
|
| | col1, col2 = st.columns(2) |
| |
|
| | |
| | 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.") |
| |
|
| | |
| | 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.") |
| |
|
| | |
| | st.markdown("<div class='subheader'>🌍 Répartition géographique des alertes</div>", unsafe_allow_html=True) |
| |
|
| | col1, col2 = st.columns(2) |
| |
|
| | |
| | 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.") |
| |
|
| | |
| | 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.") |
| |
|
| | |
| | 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: |
| | |
| | danger_product_matrix = pd.crosstab(df_filtre['hazards'], df_filtre['category']) |
| | |
| | |
| | top_dangers = df_filtre['hazards'].value_counts().nlargest(10).index.tolist() |
| | top_products = df_filtre['category'].value_counts().nlargest(10).index.tolist() |
| | |
| | |
| | filtered_matrix = danger_product_matrix.loc[danger_product_matrix.index.isin(top_dangers), |
| | danger_product_matrix.columns.isin(top_products)] |
| | |
| | |
| | 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.") |
| |
|
| | |
| | with st.expander("📋 Données brutes", expanded=False): |
| | st.markdown("### Alertes RASFF ({} enregistrements)".format(len(df_filtre))) |
| | |
| | |
| | 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" |
| | |
| | |
| | df_display = df_filtre.sort_values(by=sort_column, ascending=sort_ascending) |
| | st.dataframe(df_display, use_container_width=True) |
| | |
| | |
| | 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 |
| |
|
| | |
| | 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 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) |
| |
|
| |
|
| |
|