import streamlit as st import pandas as pd import sqlite3 import plotly.express as px import plotly.graph_objects as go from io import BytesIO import datetime from st_aggrid import AgGrid, GridOptionsBuilder import altair as alt import os import requests # === CONFIGURATION STREAMLIT === st.set_page_config( page_title="RASFF Alerts Dashboard", page_icon="đš", layout="wide", initial_sidebar_state="expanded" ) # Custom CSS pour amĂ©liorer l'interface st.markdown(""" """, unsafe_allow_html=True) # === CONFIGURATION BASE DE DONNĂES === DB_PATH = "rasff_data.db" DB_URL = "https://raw.githubusercontent.com/M00N69/RASFFDB/main/rasff_data.db" CURRENT_YEAR = datetime.datetime.now().year CURRENT_WEEK = datetime.datetime.now().isocalendar()[1] WEEKS = list(range(1, 53)) # Configuration pour le cache cache_ttl = 3600 # 1 heure # === FONCTIONS === def download_db(): """TĂ©lĂ©charge et remplace la base de donnĂ©es depuis GitHub""" with st.spinner("đ„ TĂ©lĂ©chargement de la base de donnĂ©es..."): response = requests.get(DB_URL) response.raise_for_status() with open(DB_PATH, "wb") as file: file.write(response.content) st.success("âïž Base de donnĂ©es tĂ©lĂ©chargĂ©e avec succĂšs!") st.cache_data.clear() # Efface le cache obsolĂšte # RecrĂ©er la table si nĂ©cessaire create_database() # Forcer le rechargement avec st.stop() if 'reloaded' not in st.session_state: st.session_state['reloaded'] = True st.stop() def create_database(): """CrĂ©e la base de donnĂ©es si elle n'existe pas""" if not os.path.exists(DB_PATH): st.error("Base de donnĂ©es non trouvĂ©e. Veuillez la tĂ©lĂ©charger manuellement.") return conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute(""" CREATE TABLE IF NOT EXISTS rasff_notifications ( reference TEXT PRIMARY KEY, category TEXT, type TEXT, subject TEXT, date DATETIME, notifying_country TEXT, classification TEXT, risk_decision TEXT, distribution TEXT, forAttention TEXT, forFollowUp TEXT, operator TEXT, origin TEXT, hazards TEXT ) """) conn.commit() conn.close() def check_database_structure(): """VĂ©rifie la structure de la base de donnĂ©es et affiche des informations""" if not os.path.exists(DB_PATH): st.error("â ïž Base de donnĂ©es non trouvĂ©e.") return conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() # VĂ©rifier si la table existe cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='rasff_notifications';") table_exists = cursor.fetchone() if not table_exists: st.error("â ïž La table 'rasff_notifications' n'existe pas dans la base de donnĂ©es.") conn.close() return # Afficher les colonnes cursor.execute("PRAGMA table_info(rasff_notifications);") columns = cursor.fetchall() columns_df = pd.DataFrame(columns, columns=["cid", "name", "type", "notnull", "dflt_value", "pk"]) st.subheader("Structure de la table") st.dataframe(columns_df) # VĂ©rifier le nombre d'enregistrements cursor.execute("SELECT COUNT(*) FROM rasff_notifications") count = cursor.fetchone()[0] st.metric("Nombre total d'enregistrements", count) if count == 0: st.warning("â ïž La table est vide.") conn.close() @st.cache_data(ttl=cache_ttl) def get_clean_dataframe(): """RĂ©cupĂšre et nettoie les donnĂ©es de la base SQLite affichĂ©e dans DĂ©bogage""" st.cache_data.clear() # Efface le cache pour forcer le rechargement # VĂ©rifier si le fichier existe if not os.path.exists(DB_PATH): st.error("â ïž Base de donnĂ©es non trouvĂ©e.") return pd.DataFrame() # Connexion Ă la base dĂ©tectĂ©e dans DĂ©bogage conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() # VĂ©rifier si la table existe cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='rasff_notifications';") table_exists = cursor.fetchone() if not table_exists: st.error("â ïž La table 'rasff_notifications' n'existe pas dans la base de donnĂ©es.") return pd.DataFrame() # Charger les donnĂ©es de 2024 et 2025 df = pd.read_sql("SELECT * FROM rasff_notifications WHERE strftime('%Y', date) IN ('2024', '2025')", conn) conn.close() if df.empty: st.warning("â ïž Aucune donnĂ©e disponible pour 2024 et 2025.") return pd.DataFrame() # Corriger le format des dates if 'date' in df.columns: df["date"] = pd.to_datetime(df["date"], errors="coerce") df = df.dropna(subset=["date"]) else: st.error("â ïž La colonne 'date' est absente.") return pd.DataFrame() st.write("đ AnnĂ©es prĂ©sentes dans le DataFrame aprĂšs chargement :", df["date"].dt.year.unique()) return df def create_interactive_table(df): """CrĂ©e une table interactive avec AgGrid""" if df.empty: st.warning("â ïž Aucune donnĂ©e Ă afficher.") return # Formater la date pour l'affichage display_df = df.copy() display_df["date"] = display_df["date"].dt.strftime("%Y-%m-%d") gb = GridOptionsBuilder.from_dataframe(display_df) gb.configure_pagination(paginationAutoPageSize=False, paginationPageSize=15) gb.configure_column("date", headerName="Date", type=["dateColumnFilter"], sortable=True) gb.configure_column("reference", headerName="RĂ©fĂ©rence", sortable=True, filter=True) gb.configure_column("notifying_country", headerName="Pays notifiant", sortable=True, filter=True) gb.configure_column("origin", headerName="Pays d'origine", sortable=True, filter=True) gb.configure_column("category", headerName="CatĂ©gorie", sortable=True, filter=True) gb.configure_column("subject", headerName="Produit", sortable=True, filter=True) gb.configure_column("hazards", headerName="Substance", sortable=True, filter=True) gb.configure_column("classification", headerName="Type de danger", sortable=True, filter=True) gb.configure_column("risk_decision", headerName="DĂ©cision de risque", sortable=True, filter=True) gb.configure_selection('single') gridOptions = gb.build() grid_response = AgGrid( display_df, gridOptions=gridOptions, data_return_mode='AS_INPUT', update_mode='MODEL_CHANGED', fit_columns_on_grid_load=False, theme='streamlit', enable_enterprise_modules=False, height=400, width='100%', reload_data=False ) return grid_response def create_monthly_chart(df, selected_year): """CrĂ©e un graphique interactif des alertes par mois""" if df.empty: return None # Filter by year year_df = df[df["date"].dt.year == selected_year] if year_df.empty: return None # Count alerts by month monthly_counts = year_df["date"].dt.month.value_counts().sort_index() month_names = { 1: "Janvier", 2: "FĂ©vrier", 3: "Mars", 4: "Avril", 5: "Mai", 6: "Juin", 7: "Juillet", 8: "AoĂ»t", 9: "Septembre", 10: "Octobre", 11: "Novembre", 12: "DĂ©cembre" } monthly_data = pd.DataFrame({ 'Mois': [month_names[m] for m in monthly_counts.index], 'Nombre': monthly_counts.values, 'Mois_num': monthly_counts.index # Pour trier correctement }) # Trier par numĂ©ro de mois monthly_data = monthly_data.sort_values('Mois_num') # Create bar chart with Plotly fig = px.bar( monthly_data, x='Mois', y='Nombre', title=f"Alertes par mois en {selected_year}", labels={'Nombre': "Nombre d'alertes"}, color='Nombre', color_continuous_scale='Blues' ) fig.update_layout( xaxis_title="Mois", yaxis_title="Nombre d'alertes", height=400, template="plotly_white" ) return fig def create_country_chart(df, selected_year): """CrĂ©e un graphique des alertes par pays d'origine""" if df.empty: return None # Filter by year year_df = df[df["date"].dt.year == selected_year] if year_df.empty: return None # Get top 10 countries - filtrer les valeurs nulles top_countries = year_df["origin"].dropna().value_counts().nlargest(10) country_data = pd.DataFrame({ 'Pays': top_countries.index, 'Nombre': top_countries.values }) # Create horizontal bar chart with Plotly fig = px.bar( country_data, y='Pays', x='Nombre', title=f"Top 10 pays d'origine des alertes en {selected_year}", orientation='h', color='Nombre', color_continuous_scale='Reds' ) fig.update_layout( yaxis=dict(autorange="reversed"), height=400, template="plotly_white" ) return fig def create_category_chart(df, selected_year): """CrĂ©e un graphique des alertes par catĂ©gorie de produit""" if df.empty: return None # Filter by year year_df = df[df["date"].dt.year == selected_year] if year_df.empty: return None # Get top 10 categories - filtrer les valeurs nulles top_categories = year_df["category"].dropna().value_counts().nlargest(10) category_data = pd.DataFrame({ 'CatĂ©gorie': top_categories.index, 'Nombre': top_categories.values }) # Create pie chart with Plotly fig = px.pie( category_data, names='CatĂ©gorie', values='Nombre', title=f"RĂ©partition par catĂ©gorie de produit en {selected_year}", hole=0.4 ) fig.update_layout( height=400, template="plotly_white" ) return fig def create_hazard_chart(df, selected_year): """CrĂ©e un graphique des alertes par type de danger""" if df.empty: return None # Filter by year year_df = df[df["date"].dt.year == selected_year] if year_df.empty: return None # Get hazard categories - filtrer les valeurs nulles hazard_counts = year_df["classification"].dropna().value_counts() hazard_data = pd.DataFrame({ 'Type': hazard_counts.index, 'Nombre': hazard_counts.values }) # Create treemap with Plotly fig = px.treemap( hazard_data, path=['Type'], values='Nombre', title=f"Types de dangers signalĂ©s en {selected_year}", color='Nombre', color_continuous_scale='Greens' ) fig.update_layout( height=400, template="plotly_white" ) return fig def create_risk_decision_chart(df, selected_year): """CrĂ©e un graphique des alertes par dĂ©cision de risque""" if df.empty: return None # Filter by year year_df = df[df["date"].dt.year == selected_year] if year_df.empty: return None # Get risk decisions - filtrer les valeurs nulles risk_counts = year_df["risk_decision"].dropna().value_counts() risk_data = pd.DataFrame({ 'DĂ©cision': risk_counts.index, 'Nombre': risk_counts.values }) # Create bar chart with Plotly fig = px.bar( risk_data, x='DĂ©cision', y='Nombre', title=f"DĂ©cisions de risque en {selected_year}", color='Nombre', color_continuous_scale='Oranges' ) fig.update_layout( xaxis_title="DĂ©cision de risque", yaxis_title="Nombre d'alertes", height=400, template="plotly_white" ) return fig # === INTERFACE STREAMLIT === # Sidebar with st.sidebar: st.image("https://riskplaza.com/wp-content/uploads/2017/10/Riskplaza-RASFF-portal.png", width=200) st.markdown("## đ Ă propos") st.markdown(""" Cette application permet de suivre les alertes RASFF (Rapid Alert System for Food and Feed) de l'Union EuropĂ©enne concernant la sĂ©curitĂ© alimentaire. Les donnĂ©es sont tĂ©lĂ©chargĂ©es depuis le site de diffusion officiel. """) # Bouton pour tĂ©lĂ©charger la base de donnĂ©es if st.button("TĂ©lĂ©charger la base de donnĂ©es"): download_db() create_database() # CrĂ©er la table aprĂšs le tĂ©lĂ©chargement # Bouton pour forcer la mise Ă jour de la base if st.button("đ Forcer la mise Ă jour de la base"): download_db() create_database() st.cache_data.clear() st.session_state['reloaded'] = True st.stop() # ArrĂȘte l'exĂ©cution pour dĂ©clencher un reload automatique # Afficher le chemin et la taille de la base de donnĂ©es st.write(f"Chemin de la base de donnĂ©es : {DB_PATH}") if os.path.exists(DB_PATH): st.write(f"Taille de la base de donnĂ©es : {os.path.getsize(DB_PATH) / 1024:.2f} KB") # VĂ©rifier les annĂ©es disponibles dans la base conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute("SELECT DISTINCT strftime('%Y', date) FROM rasff_notifications") years_in_db = cursor.fetchall() conn.close() st.write(f"AnnĂ©es disponibles dans la base : {[year[0] for year in years_in_db]}") # VĂ©rifier les tables existantes dans la base conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute("SELECT name FROM sqlite_master WHERE type='table';") tables = cursor.fetchall() conn.close() st.write(f"Tables disponibles : {[table[0] for table in tables]}") # VĂ©rifier les enregistrements de 2025 conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute("SELECT COUNT(*), MIN(date), MAX(date) FROM rasff_notifications WHERE strftime('%Y', date) = '2025'") count_2025, min_date, max_date = cursor.fetchone() conn.close() st.write(f"Nombre d'enregistrements pour 2025 : {count_2025}") st.write(f"Dates disponibles pour 2025 : De {min_date} Ă {max_date}") # Main content st.markdown('