| |
| """ |
| Interface web Streamlit pour le réseau bayésien d'autonomie |
| Application déployée sur Hugging Face Spaces |
| """ |
|
|
| import streamlit as st |
| import pandas as pd |
| import plotly.graph_objects as go |
| import plotly.express as px |
| import json |
| from bayesian_network_interface import AutonomyBayesianNetwork |
|
|
| |
| st.set_page_config( |
| page_title="Réseau Bayésien - Autonomie", |
| page_icon="🧠", |
| layout="wide", |
| initial_sidebar_state="expanded" |
| ) |
|
|
| def main(): |
| """Interface principale Streamlit""" |
|
|
| st.title("🧠 Réseau Bayésien pour l'Évaluation d'Autonomie") |
| st.markdown("---") |
|
|
| |
| if 'network' not in st.session_state: |
| with st.spinner("Chargement du réseau bayésien..."): |
| try: |
| st.session_state.network = AutonomyBayesianNetwork() |
| |
| if hasattr(st.session_state.network, 'pgmpy_model') and st.session_state.network.pgmpy_model: |
| st.session_state.loading_debug = f"✅ Réseau chargé: {len(list(st.session_state.network.pgmpy_model.nodes()))} nœuds" |
| else: |
| st.session_state.loading_debug = "❌ Erreur: pgmpy_model non chargé" |
| except Exception as e: |
| st.session_state.loading_debug = f"❌ Erreur de chargement: {str(e)}" |
| st.session_state.network = None |
|
|
| network = st.session_state.network |
|
|
| |
| st.sidebar.title("Navigation") |
| page = st.sidebar.selectbox( |
| "Choisir une page", |
| ["🏠 Accueil", "📊 Structure du Réseau", "🌐 Visualisation D3.js", "🔍 Inférence", |
| "💊 Analyse d'Intervention", "📈 Facteurs Influents", "📋 Recommandations"] |
| ) |
|
|
| if page == "🏠 Accueil": |
| show_home_page(network) |
| elif page == "📊 Structure du Réseau": |
| show_structure_page(network) |
| elif page == "🌐 Visualisation D3.js": |
| show_d3_visualization_page(network) |
| elif page == "🔍 Inférence": |
| show_inference_page(network) |
| elif page == "💊 Analyse d'Intervention": |
| show_intervention_page(network) |
| elif page == "📈 Facteurs Influents": |
| show_factors_page(network) |
| elif page == "📋 Recommandations": |
| show_recommendations_page(network) |
|
|
| def show_home_page(network): |
| """Page d'accueil avec démonstration""" |
| st.header("🏠 Bienvenue dans l'Interface d'Évaluation d'Autonomie") |
|
|
| st.markdown(""" |
| Cette application utilise un **réseau bayésien complet** avec **12 variables** pour évaluer |
| l'autonomie des personnes âgées et générer des recommandations personnalisées. |
| |
| ### 🎯 **Fonctionnalités principales** |
| - **Inférence probabiliste** : Calcul des probabilités d'autonomie |
| - **Analyse d'interventions** : Impact des changements de comportement |
| - **Recommandations personnalisées** : Conseils basés sur le profil individuel |
| - **Facteurs influents** : Identification des variables les plus importantes |
| - **Visualisation interactive** : Graphique D3.js du réseau |
| """) |
|
|
| st.subheader("🔍 Démonstration avec deux scénarios") |
|
|
| col1, col2 = st.columns(2) |
|
|
| with col1: |
| st.markdown("**👤 Scénario 1: Personne Active**") |
| evidence1 = { |
| 'Age': 'age_60_69', |
| 'Physical_Activity': 'high', |
| 'BMI_Category': 'underweight_normal' |
| } |
|
|
| for key, value in evidence1.items(): |
| st.write(f"• {key}: {value}") |
|
|
| result1 = network.perform_inference_pgmpy(evidence1, ['Global_Autonomy']) |
| if not result1.empty: |
| autonomous_prob = result1[result1['State'] == 'autonomous']['Probability'].iloc[0] |
| st.success(f"🎉 **Probabilité d'autonomie: {autonomous_prob:.1%}**") |
|
|
| with col2: |
| st.markdown("**👴 Scénario 2: Personne Sédentaire**") |
| evidence2 = { |
| 'Age': 'age_80_89', |
| 'Physical_Activity': 'sedentary', |
| 'BMI_Category': 'obese_severe' |
| } |
|
|
| for key, value in evidence2.items(): |
| st.write(f"• {key}: {value}") |
|
|
| result2 = network.perform_inference_pgmpy(evidence2, ['Global_Autonomy']) |
| if not result2.empty: |
| autonomous_prob = result2[result2['State'] == 'autonomous']['Probability'].iloc[0] |
| st.warning(f"⚠️ **Probabilité d'autonomie: {autonomous_prob:.1%}**") |
|
|
| |
| if not result1.empty and not result2.empty: |
| st.markdown("---") |
| st.subheader("📊 Comparaison des Scénarios") |
|
|
| fig = go.Figure() |
|
|
| |
| states1 = result1['State'].tolist() |
| probs1 = result1['Probability'].tolist() |
|
|
| |
| states2 = result2['State'].tolist() |
| probs2 = result2['Probability'].tolist() |
|
|
| fig.add_trace(go.Bar( |
| name='Personne Active (60-69 ans)', |
| x=states1, |
| y=probs1, |
| marker_color='lightgreen' |
| )) |
|
|
| fig.add_trace(go.Bar( |
| name='Personne Sédentaire (80-89 ans)', |
| x=states2, |
| y=probs2, |
| marker_color='lightcoral' |
| )) |
|
|
| fig.update_layout( |
| title="Comparaison des Probabilités d'Autonomie", |
| xaxis_title="État d'Autonomie", |
| yaxis_title="Probabilité", |
| barmode='group', |
| height=500 |
| ) |
|
|
| st.plotly_chart(fig, use_container_width=True) |
|
|
| st.markdown("---") |
| st.info("💡 **Conseil**: Explorez les autres pages pour des analyses plus détaillées!") |
|
|
| def show_structure_page(network): |
| """Page structure du réseau""" |
| st.header("📊 Structure du Réseau Bayésien") |
|
|
| |
| st.write("🔍 **Debug Info:**") |
|
|
| |
| if hasattr(st.session_state, 'loading_debug'): |
| st.write(f"- Chargement initial: {st.session_state.loading_debug}") |
|
|
| if network is None: |
| st.error("❌ Réseau non initialisé!") |
| return |
|
|
| |
| if hasattr(network, 'debug_messages'): |
| st.write("**Messages de chargement détaillés:**") |
| for msg in network.debug_messages: |
| st.code(msg) |
|
|
| st.write(f"- pgmpy_model exists: {network.pgmpy_model is not None}") |
| if network.pgmpy_model: |
| st.write(f"- pgmpy nodes: {len(list(network.pgmpy_model.nodes()))}") |
| st.write(f"- pgmpy edges: {len(list(network.pgmpy_model.edges()))}") |
|
|
| st.write(f"- pyagrum_model exists: {hasattr(network, 'pyagrum_model') and network.pyagrum_model is not None}") |
| st.write(f"- actionable_vars: {len(getattr(network, 'actionable_vars', []))}") |
|
|
| try: |
| structure = network.get_network_structure() |
| st.write(f"- Structure nodes: {len(structure['nodes'])}") |
| st.write(f"- Structure edges: {len(structure['edges'])}") |
| except Exception as e: |
| st.error(f"Erreur lors de l'obtention de la structure: {e}") |
| structure = {'nodes': [], 'edges': []} |
|
|
| col1, col2, col3 = st.columns(3) |
|
|
| with col1: |
| st.metric("Nombre de nœuds", len(structure.get('nodes', []))) |
| with col2: |
| st.metric("Nombre d'arcs", len(structure.get('edges', []))) |
| with col3: |
| st.metric("Variables actionnables", len(network.actionable_vars)) |
|
|
| |
| st.subheader("Variables par catégorie") |
|
|
| categories = {} |
| for node in structure['nodes']: |
| cat = node['category'] |
| if cat not in categories: |
| categories[cat] = [] |
| categories[cat].append(node['name']) |
|
|
| for cat in ['non_actionable', 'actionable', 'intermediate', 'outcome']: |
| if cat in categories: |
| with st.expander(f"{cat.replace('_', ' ').title()} ({len(categories[cat])} variables)"): |
| for var in categories[cat]: |
| st.write(f"• {var}") |
|
|
| def show_inference_page(network): |
| """Page d'inférence""" |
| st.header("🔍 Inférence Bayésienne") |
|
|
| col1, col2 = st.columns(2) |
|
|
| with col1: |
| st.subheader("Variables Non-Actionnables") |
| evidence = {} |
|
|
| age = st.selectbox("Âge", |
| ["", "age_60_69", "age_70_79", "age_80_89", "age_90_plus"]) |
| if age: |
| evidence['Age'] = age |
|
|
| sex = st.selectbox("Sexe", ["", "male", "female"]) |
| if sex: |
| evidence['Sex'] = sex |
|
|
| education = st.selectbox("Niveau d'Éducation", |
| ["", "primary_or_below", "secondary", "higher_education"]) |
| if education: |
| evidence['Education_Level'] = education |
|
|
| st.subheader("Variables Actionnables") |
|
|
| activity = st.selectbox("Activité Physique", |
| ["", "sedentary", "low", "moderate", "high"]) |
| if activity: |
| evidence['Physical_Activity'] = activity |
|
|
| bmi = st.selectbox("Catégorie IMC", |
| ["", "underweight_normal", "overweight", "obese_mild", "obese_severe"]) |
| if bmi: |
| evidence['BMI_Category'] = bmi |
|
|
| smoking = st.selectbox("Statut Tabagique", |
| ["", "never", "former", "current"]) |
| if smoking: |
| evidence['Smoking_Status'] = smoking |
|
|
| social_eng = st.selectbox("Engagement Social", |
| ["", "low", "moderate", "high"]) |
| if social_eng: |
| evidence['Social_Engagement'] = social_eng |
|
|
| social_sup = st.selectbox("Support Social", |
| ["", "poor", "moderate", "good"]) |
| if social_sup: |
| evidence['Social_Support'] = social_sup |
|
|
| with col2: |
| st.subheader("Variables à inférer") |
| query_vars = st.multiselect( |
| "Sélectionner les variables", |
| network.outcome_vars + network.intermediate_vars, |
| default=['Global_Autonomy'] |
| ) |
|
|
| if st.button("🔍 Effectuer l'inférence"): |
| if query_vars: |
| with st.spinner("Calcul en cours..."): |
| result = network.perform_inference_pgmpy(evidence, query_vars) |
|
|
| if not result.empty: |
| st.subheader("📊 Résultats") |
|
|
| for var in query_vars: |
| var_data = result[result['Variable'] == var] |
| if not var_data.empty: |
| fig = px.bar(var_data, x='State', y='Probability', |
| title=f"Distribution de probabilité pour {var}", |
| color='Probability', |
| color_continuous_scale='viridis') |
| st.plotly_chart(fig, use_container_width=True) |
|
|
| |
| st.dataframe(var_data[['State', 'Probability']].round(4)) |
| else: |
| st.error("❌ Aucun résultat disponible") |
| else: |
| st.warning("⚠️ Veuillez sélectionner au moins une variable à inférer") |
|
|
| def show_intervention_page(network): |
| """Page analyse d'intervention""" |
| st.header("💊 Analyse d'Intervention") |
|
|
| col1, col2 = st.columns(2) |
|
|
| with col1: |
| st.markdown("**Intervention à tester**") |
| intervention = {} |
|
|
| new_activity = st.selectbox("Nouvelle Activité Physique", |
| ["Non modifié", "sedentary", "low", "moderate", "high"]) |
| if new_activity != "Non modifié": |
| intervention['Physical_Activity'] = new_activity |
|
|
| new_bmi = st.selectbox("Nouvelle Catégorie IMC", |
| ["Non modifié", "underweight_normal", "overweight", "obese_mild", "obese_severe"]) |
| if new_bmi != "Non modifié": |
| intervention['BMI_Category'] = new_bmi |
|
|
| with col2: |
| st.markdown("**Profil de base (optionnel)**") |
| baseline_evidence = {} |
|
|
| base_age = st.selectbox("Âge de référence", |
| ["", "age_60_69", "age_70_79", "age_80_89", "age_90_plus"]) |
| if base_age: |
| baseline_evidence['Age'] = base_age |
|
|
| if st.button("🧪 Analyser l'intervention"): |
| if intervention: |
| with st.spinner("Analyse en cours..."): |
| baseline_result = network.perform_inference_pgmpy(baseline_evidence, ['Global_Autonomy']) |
|
|
| |
| intervention_evidence = baseline_evidence.copy() |
| intervention_evidence.update(intervention) |
| intervention_result = network.perform_inference_pgmpy(intervention_evidence, ['Global_Autonomy']) |
|
|
| if not baseline_result.empty and not intervention_result.empty: |
| st.subheader("📊 Comparaison Avant/Après") |
|
|
| |
| fig = go.Figure() |
|
|
| states = baseline_result['State'].tolist() |
| baseline_probs = baseline_result['Probability'].tolist() |
| intervention_probs = intervention_result['Probability'].tolist() |
|
|
| fig.add_trace(go.Bar( |
| name='Avant intervention', |
| x=states, |
| y=baseline_probs, |
| marker_color='lightblue' |
| )) |
|
|
| fig.add_trace(go.Bar( |
| name='Après intervention', |
| x=states, |
| y=intervention_probs, |
| marker_color='darkgreen' |
| )) |
|
|
| fig.update_layout( |
| title="Impact de l'intervention sur l'autonomie", |
| xaxis_title="État d'Autonomie", |
| yaxis_title="Probabilité", |
| barmode='group', |
| height=500 |
| ) |
|
|
| st.plotly_chart(fig, use_container_width=True) |
|
|
| |
| baseline_autonomous = baseline_result[baseline_result['State'] == 'autonomous']['Probability'].iloc[0] |
| intervention_autonomous = intervention_result[intervention_result['State'] == 'autonomous']['Probability'].iloc[0] |
| improvement = intervention_autonomous - baseline_autonomous |
|
|
| if improvement > 0: |
| st.success(f"✅ **Amélioration**: +{improvement:.1%} de probabilité d'autonomie") |
| elif improvement < 0: |
| st.warning(f"⚠️ **Détérioration**: {improvement:.1%} de probabilité d'autonomie") |
| else: |
| st.info("➡️ **Aucun changement** significatif") |
|
|
| else: |
| st.error("❌ Impossible d'effectuer l'analyse") |
| else: |
| st.warning("⚠️ Veuillez définir au moins une intervention") |
|
|
| def show_factors_page(network): |
| """Page facteurs influents""" |
| st.header("📈 Facteurs les Plus Influents") |
|
|
| if st.button("🔍 Analyser les facteurs influents"): |
| with st.spinner("Calcul en cours..."): |
| factors = network.get_most_influential_factors() |
|
|
| if factors: |
| st.subheader("🎯 Classement des Variables") |
|
|
| |
| variables = [factor[0] for factor in factors] |
| influences = [factor[1] for factor in factors] |
|
|
| fig = px.bar( |
| x=variables, |
| y=influences, |
| title="Impact des Variables sur l'Autonomie", |
| labels={'x': 'Variables', 'y': 'Influence (différence de probabilité)'} |
| ) |
| st.plotly_chart(fig, use_container_width=True) |
|
|
| |
| df_factors = pd.DataFrame(factors, columns=['Variable', 'Influence']) |
| df_factors['Influence %'] = (df_factors['Influence'] * 100).round(1) |
| st.dataframe(df_factors) |
|
|
| else: |
| st.info("ℹ️ Aucun facteur influent identifié avec les données actuelles") |
|
|
| def show_recommendations_page(network): |
| """Page recommandations""" |
| st.header("📋 Recommandations Personnalisées") |
|
|
| st.subheader("Profil du Patient") |
|
|
| profile = {} |
|
|
| col1, col2 = st.columns(2) |
|
|
| with col1: |
| age = st.selectbox("Âge", |
| ["age_60_69", "age_70_79", "age_80_89", "age_90_plus"]) |
| profile['Age'] = age |
|
|
| activity = st.selectbox("Activité Physique actuelle", |
| ["sedentary", "low", "moderate", "high"]) |
| profile['Physical_Activity'] = activity |
|
|
| with col2: |
| sex = st.selectbox("Sexe", ["male", "female"]) |
| profile['Sex'] = sex |
|
|
| education = st.selectbox("Niveau d'éducation", |
| ["primary_or_below", "secondary", "higher_education"]) |
| profile['Education_Level'] = education |
|
|
| bmi = st.selectbox("Catégorie IMC actuelle", |
| ["underweight_normal", "overweight", "obese_mild", "obese_severe"]) |
| profile['BMI_Category'] = bmi |
|
|
| if st.button("🎯 Générer les recommandations"): |
| with st.spinner("Analyse du profil..."): |
| |
| current_state = network.perform_inference_pgmpy(profile, ['Global_Autonomy']) |
|
|
| if not current_state.empty: |
| current_autonomous = current_state[current_state['State'] == 'autonomous']['Probability'].iloc[0] |
|
|
| st.subheader("📊 État Actuel") |
| st.metric("Probabilité d'autonomie", f"{current_autonomous:.1%}") |
|
|
| |
| recommendations = network.generate_recommendations(profile) |
|
|
| if recommendations: |
| st.subheader("🎯 Recommandations Prioritaires") |
|
|
| for i, rec in enumerate(recommendations): |
| with st.expander(f"💡 Recommandation #{i+1} - Priorité {rec['priority']}"): |
| st.write(f"**{rec['recommendation']}**") |
| st.info(f"📈 **Amélioration attendue**: +{rec['expected_improvement']:.1f}%") |
|
|
| |
| col_metric, col_bar = st.columns([1, 3]) |
| with col_metric: |
| st.metric("Impact", f"+{rec['expected_improvement']:.1f}%") |
| with col_bar: |
| progress = min(rec['expected_improvement'] / 20.0, 1.0) |
| st.progress(progress) |
|
|
| st.markdown("---") |
|
|
| else: |
| st.info("ℹ️ Aucune recommandation spécifique disponible pour ce profil") |
|
|
| else: |
| st.error("❌ Impossible d'analyser le profil") |
|
|
| def show_d3_visualization_page(network): |
| """Page avec visualisation D3.js du réseau bayésien""" |
| st.header("🌐 Visualisation Interactive D3.js") |
|
|
| st.markdown(""" |
| Cette page présente une visualisation interactive du réseau bayésien utilisant D3.js. |
| Les nœuds sont colorés selon leur catégorie et les arcs montrent les dépendances causales. |
| """) |
|
|
| |
| structure = network.get_network_structure() |
|
|
| |
| nodes = [] |
| links = [] |
|
|
| |
| color_map = { |
| 'non_actionable': '#FF6B6B', |
| 'actionable': '#4ECDC4', |
| 'intermediate': '#45B7D1', |
| 'outcome': '#96CEB4' |
| } |
|
|
| |
| for i, node in enumerate(structure['nodes']): |
| nodes.append({ |
| "id": node['name'], |
| "group": node['category'], |
| "color": color_map.get(node['category'], '#DDDDDD'), |
| "title": f"{node['name']} ({node['category']})" |
| }) |
|
|
| |
| for source, target in structure['edges']: |
| links.append({ |
| "source": source, |
| "target": target |
| }) |
|
|
| |
| d3_code = f""" |
| <!DOCTYPE html> |
| <html> |
| <head> |
| <script src="https://d3js.org/d3.v7.min.js"></script> |
| <style> |
| .node {{ |
| stroke: #fff; |
| stroke-width: 2px; |
| cursor: pointer; |
| }} |
| .link {{ |
| stroke: #999; |
| stroke-opacity: 0.6; |
| stroke-width: 2px; |
| }} |
| .node-label {{ |
| font: 12px sans-serif; |
| pointer-events: none; |
| text-anchor: middle; |
| fill: #333; |
| }} |
| .tooltip {{ |
| position: absolute; |
| padding: 8px; |
| background: rgba(0, 0, 0, 0.8); |
| color: white; |
| border-radius: 4px; |
| pointer-events: none; |
| font-size: 12px; |
| }} |
| </style> |
| </head> |
| <body> |
| <div id="network"></div> |
| <div class="tooltip" style="opacity: 0;"></div> |
| |
| <script> |
| // Dimensions |
| const width = 800; |
| const height = 600; |
| |
| // Données du réseau |
| const nodes = {nodes}; |
| const links = {links}; |
| |
| // Créer le SVG |
| const svg = d3.select("#network") |
| .append("svg") |
| .attr("width", width) |
| .attr("height", height); |
| |
| // Tooltip |
| const tooltip = d3.select(".tooltip"); |
| |
| // Simulation de force |
| const simulation = d3.forceSimulation(nodes) |
| .force("link", d3.forceLink(links).id(d => d.id).distance(100)) |
| .force("charge", d3.forceManyBody().strength(-300)) |
| .force("center", d3.forceCenter(width / 2, height / 2)); |
| |
| // Créer les liens |
| const link = svg.append("g") |
| .selectAll("line") |
| .data(links) |
| .enter().append("line") |
| .attr("class", "link"); |
| |
| // Créer les nœuds |
| const node = svg.append("g") |
| .selectAll("circle") |
| .data(nodes) |
| .enter().append("circle") |
| .attr("class", "node") |
| .attr("r", 15) |
| .attr("fill", d => d.color) |
| .on("mouseover", function(event, d) {{ |
| tooltip.transition().duration(200).style("opacity", .9); |
| tooltip.html(d.title) |
| .style("left", (event.pageX + 10) + "px") |
| .style("top", (event.pageY - 28) + "px"); |
| }}) |
| .on("mouseout", function() {{ |
| tooltip.transition().duration(500).style("opacity", 0); |
| }}) |
| .call(d3.drag() |
| .on("start", dragstarted) |
| .on("drag", dragged) |
| .on("end", dragended)); |
| |
| // Étiquettes des nœuds |
| const labels = svg.append("g") |
| .selectAll("text") |
| .data(nodes) |
| .enter().append("text") |
| .attr("class", "node-label") |
| .text(d => d.id.replace('_', ' ')); |
| |
| // Mise à jour de la simulation |
| simulation.on("tick", () => {{ |
| link |
| .attr("x1", d => d.source.x) |
| .attr("y1", d => d.source.y) |
| .attr("x2", d => d.target.x) |
| .attr("y2", d => d.target.y); |
| |
| node |
| .attr("cx", d => d.x) |
| .attr("cy", d => d.y); |
| |
| labels |
| .attr("x", d => d.x) |
| .attr("y", d => d.y + 25); |
| }}); |
| |
| // Fonctions de drag |
| function dragstarted(event, d) {{ |
| if (!event.active) simulation.alphaTarget(0.3).restart(); |
| d.fx = d.x; |
| d.fy = d.y; |
| }} |
| |
| function dragged(event, d) {{ |
| d.fx = event.x; |
| d.fy = event.y; |
| }} |
| |
| function dragended(event, d) {{ |
| if (!event.active) simulation.alphaTarget(0); |
| d.fx = null; |
| d.fy = null; |
| }} |
| </script> |
| </body> |
| </html> |
| """ |
|
|
| |
| st.components.v1.html(d3_code, height=650) |
|
|
| |
| st.subheader("📋 Légende") |
| col1, col2, col3, col4 = st.columns(4) |
|
|
| with col1: |
| st.markdown("🔴 **Non-actionnables**") |
| st.write("Variables démographiques") |
|
|
| with col2: |
| st.markdown("🔵 **Actionnables**") |
| st.write("Variables modifiables") |
|
|
| with col3: |
| st.markdown("🟦 **Intermédiaires**") |
| st.write("Variables de transition") |
|
|
| with col4: |
| st.markdown("🟢 **Résultat**") |
| st.write("Variable d'autonomie") |
|
|
| st.info("💡 **Interaction**: Cliquez et faites glisser les nœuds pour explorer la structure du réseau!") |
|
|
| if __name__ == "__main__": |
| main() |