import numpy as np import dash from dash import dcc, html, Input, Output, State, callback_context import dash_bootstrap_components as dbc import plotly.graph_objs as go import pandas as pd import base64 import io import json from dash.exceptions import PreventUpdate # Initialiser l'application Dash avec un thème moderne et professionnel app = dash.Dash( __name__, external_stylesheets=[ dbc.themes.BOOTSTRAP, # Thème plus professionnel "https://use.fontawesome.com/releases/v5.15.4/css/all.css" # Icônes FontAwesome ], suppress_callback_exceptions=True, meta_tags=[ {"name": "viewport", "content": "width=device-width, initial-scale=1"} # Responsive design ] ) # Définir le titre de l'application app.title = "AFC Pro - Analyse Factorielle des Correspondances" # Styles personnalisés pour une apparence plus professionnelle CUSTOM_STYLES = { "card": { "box-shadow": "0 4px 6px rgba(0, 0, 0, 0.1)", "margin-bottom": "20px", "border-radius": "8px", "border": "none" }, "card-header": { "background-color": "#f8f9fa", "border-bottom": "1px solid #eaecef", "padding": "15px 20px", "border-radius": "8px 8px 0 0" }, "button-primary": { "background-color": "#0d6efd", "border-color": "#0d6efd", "font-weight": "500" }, "section-title": { "color": "#212529", "font-weight": "600", "margin-bottom": "15px", "padding-bottom": "10px", "border-bottom": "1px solid #eaecef" } } # Logo de l'application (encodé en base64 pour simplicité) LOGO = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNDAgNjAiPjxwYXRoIGZpbGw9IiMwZDZlZmQiIGQ9Ik0zMCwxMEMxOC45NTQsMTAgMTAsMTguOTU0IDEwLDMwczguOTU0LDIwIDIwLDIwczIwLTguOTU0IDIwLTIwUzQxLjA0NiwxMCAzMCwxMHogTTMwLDQ1Yy04LjI4NCwwLTE1LTYuNzE2LTE1LTE1czYuNzE2LTE1IDE1LTE1czE1LDYuNzE2IDE1LDE1UzM4LjI4NCw0NSAzMCw0NXoiLz48cGF0aCBmaWxsPSIjMGQ2ZWZkIiBkPSJNNzAsMTBDNTguOTU0LDEwIDUwLDE4Ljk1NCA1MCwzMHM4Ljk1NCwyMCAyMCwyMHMyMC04Ljk1NCAyMC0yMFM4MS4wNDYsMTAgNzAsMTB6IE03MCw0NWMtOC4yODQsMC0xNS02LjcxNi0xNS0xNXM2LjcxNi0xNSAxNS0xNXMxNSw2LjcxNiAxNSwxNVM3OC4yODQsNDUgNzAsNDV6Ii8+PHBhdGggZmlsbD0iIzIxMjUyOSIgZD0iTTExMCwyMGg1djIwaC01VjIweiIvPjxwYXRoIGZpbGw9IiMyMTI1MjkiIGQ9Ik0xMjAsMjBoMjB2NWgtMjBWMjB6Ii8+PHBhdGggZmlsbD0iIzIxMjUyOSIgZD0iTTEyMCwzNWgyMHY1aC0yMFYzNXoiLz48cGF0aCBmaWxsPSIjMjEyNTI5IiBkPSJNMTIwLDI3LjVoMjB2NWgtMjBWMjcuNXoiLz48cGF0aCBmaWxsPSIjMjEyNTI5IiBkPSJNMTUwLDIwaDIwdjVoLTIwVjIweiIvPjxwYXRoIGZpbGw9IiMyMTI1MjkiIGQ9Ik0xNTAsMzVoMjB2NWgtMjBWMzV6Ii8+PHBhdGggZmlsbD0iIzIxMjUyOSIgZD0iTTE1MCwyMGg1djIwaC01VjIweiIvPjxwYXRoIGZpbGw9IiMyMTI1MjkiIGQ9Ik0xODAsMjBoMjB2NWgtMjBWMjB6Ii8+PHBhdGggZmlsbD0iIzIxMjUyOSIgZD0iTTE4MCwzNWgyMHY1aC0yMFYzNXoiLz48cGF0aCBmaWxsPSIjMjEyNTI5IiBkPSJNMTgwLDI3LjVoMjB2NWgtMjBWMjcuNXoiLz48cGF0aCBmaWxsPSIjMjEyNTI5IiBkPSJNMTgwLDIwaDV2MjBoLTVWMjB6Ii8+PHBhdGggZmlsbD0iIzIxMjUyOSIgZD0iTTE5NSwyMGg1djIwaC01VjIweiIvPjwvc3ZnPg==" # Exemples prédéfinis pour l'AFC PREDEFINED_EXAMPLES = { "exemple_1": { "name": "Exemple 1: Préférences alimentaires par région", "matrix": "30,10,3,15;45,8,0,12;8,66,33,5;2,44,50,8", "row_labels": "Nord,Sud,Est,Ouest", "col_labels": "Viande,Poisson,Légumes,Fruits" }, "exemple_2": { "name": "Exemple 2: Niveaux d'éducation par catégorie d'emploi", "matrix": "120,45,30,5;60,80,70,20;25,40,90,60;5,15,40,80", "row_labels": "Sans diplôme,Baccalauréat,Licence,Master/Doctorat", "col_labels": "Ouvrier,Employé,Cadre moyen,Cadre supérieur" }, "exemple_3": { "name": "Exemple 3: Modes de transport par tranche d'âge", "matrix": "50,30,10,5;30,60,40,15;10,40,60,30;5,15,30,50", "row_labels": "18-25 ans,26-40 ans,41-60 ans,61+ ans", "col_labels": "Voiture,Transport en commun,Vélo,Marche" } } # Fonction pour créer un message de chat avec un style amélioré def create_chat_message(sender, content, is_code=False): if is_code: message_content = dbc.Card( dbc.CardBody([ html.Pre(content, className="mb-0", style={"white-space": "pre-wrap"}) ]), className="mb-2", style={"background-color": "#f8f9fa", "border-left": "4px solid #0d6efd"} ) else: message_content = dbc.Card( dbc.CardBody([ html.P(content, className="mb-0", style={"white-space": "pre-wrap"}) ]), className="mb-2" ) if sender == "Assistant AFC": return html.Div([ html.Div([ html.Span(sender, className="font-weight-bold", style={"color": "#0d6efd"}), html.Small(f" • {dash.callback_context.triggered_id or 'maintenant'}", className="text-muted ml-2") ], className="mb-1"), message_content ], className="mb-3", style={"margin-left": "0px"}) else: return html.Div([ html.Div([ html.Span(sender, className="font-weight-bold", style={"color": "#212529"}), html.Small(f" • {dash.callback_context.triggered_id or 'maintenant'}", className="text-muted ml-2") ], className="mb-1"), message_content ], className="mb-3", style={"margin-left": "auto", "text-align": "right"}) # Fonction pour effectuer l'AFC avec des étapes détaillées def afc_detailed(matrice_contingence, row_labels=None, col_labels=None): results = {} # Étape 1 : Valider et préparer la matrice d'entrée results["step1"] = { "title": "Étape 1: Préparation de la matrice", "original_matrix": matrice_contingence.tolist(), "explanation": "Nous commençons avec la matrice de contingence originale qui représente les fréquences observées." } # Étape 2 : Calculer la matrice des fréquences N = np.sum(matrice_contingence) f = matrice_contingence / N results["step2"] = { "title": "Étape 2: Calcul de la matrice des fréquences", "frequency_matrix": f.tolist(), "explanation": f"Nous divisons chaque élément de la matrice par le total général N = {N} pour obtenir la matrice des fréquences relatives." } # Étape 3 : Calculer les fréquences marginales f_i = np.sum(f, axis=1) f_j = np.sum(f, axis=0) results["step3"] = { "title": "Étape 3: Calcul des fréquences marginales", "row_margins": f_i.tolist(), "column_margins": f_j.tolist(), "explanation": "Nous calculons les fréquences marginales en ligne (f_i) et en colonne (f_j), qui représentent les distributions marginales." } # Étape 4 : Calculer la matrice des écarts à l'indépendance independence_matrix = np.outer(f_i, f_j) ecarts_independance = f - independence_matrix results["step4"] = { "title": "Étape 4: Calcul des écarts à l'indépendance", "independence_matrix": independence_matrix.tolist(), "deviation_matrix": ecarts_independance.tolist(), "explanation": "Nous calculons la matrice d'indépendance théorique (produit des marges) et les écarts à l'indépendance (différence entre fréquences observées et théoriques)." } # Étape 5 : Calculer la matrice X D_i_inv_sqrt = np.diag(1 / np.sqrt(f_i)) D_j_inv_sqrt = np.diag(1 / np.sqrt(f_j)) X = D_i_inv_sqrt @ ecarts_independance @ D_j_inv_sqrt results["step5"] = { "title": "Étape 5: Calcul de la matrice X", "X_matrix": X.tolist(), "explanation": "Nous calculons la matrice X qui normalise les écarts à l'indépendance par les racines carrées des marges." } # Étape 6 : Calculer la matrice S et ses valeurs propres/vecteurs propres S = X.T @ X valeurs_propres, vecteurs_propres = np.linalg.eig(S) # Convertir en réel si les valeurs complexes sont très petites valeurs_propres = np.real_if_close(valeurs_propres) vecteurs_propres = np.real_if_close(vecteurs_propres) # Trier les valeurs propres et vecteurs propres indices_tries = np.argsort(valeurs_propres)[::-1] valeurs_propres_triees = valeurs_propres[indices_tries] vecteurs_propres_tries = vecteurs_propres[:, indices_tries] results["step6"] = { "title": "Étape 6: Décomposition en valeurs propres", "eigenvalues": valeurs_propres_triees.tolist(), "eigenvectors": vecteurs_propres_tries.tolist(), "explanation": "Nous calculons les valeurs propres et vecteurs propres de la matrice S = X'X, qui représentent les axes factoriels et leur importance." } # Étape 7 : Calculer l'inertie inertia_total = np.sum(valeurs_propres_triees) inertia_percentages = (valeurs_propres_triees / inertia_total) * 100 results["step7"] = { "title": "Étape 7: Calcul de l'inertie", "total_inertia": float(inertia_total), "inertia_by_axis": inertia_percentages.tolist(), "cumulative_inertia": np.cumsum(inertia_percentages).tolist(), "explanation": "L'inertie représente la variance expliquée par chaque axe factoriel. Le pourcentage d'inertie indique l'importance relative de chaque axe." } # Étape 8 : Calculer les coordonnées des profils lignes et colonnes vecteur_propre_1 = vecteurs_propres_tries[:, 0] vecteur_propre_2 = vecteurs_propres_tries[:, 1] C1_lignes = X @ vecteur_propre_1 C2_lignes = X @ vecteur_propre_2 C1_colonnes = np.sqrt(valeurs_propres_triees[0]) * vecteur_propre_1 C2_colonnes = np.sqrt(valeurs_propres_triees[1]) * vecteur_propre_2 # Utiliser les étiquettes si fournies if row_labels is None: row_labels = [f'L{i+1}' for i in range(len(C1_lignes))] if col_labels is None: col_labels = [f'C{i+1}' for i in range(len(C1_colonnes))] results["step8"] = { "title": "Étape 8: Calcul des coordonnées des profils", "row_coordinates": { "axis1": C1_lignes.tolist(), "axis2": C2_lignes.tolist(), "labels": row_labels }, "column_coordinates": { "axis1": C1_colonnes.tolist(), "axis2": C2_colonnes.tolist(), "labels": col_labels }, "explanation": "Nous calculons les coordonnées des profils lignes et colonnes sur les deux premiers axes factoriels." } # Créer la visualisation du plan factoriel fig = go.Figure() # Ajouter les profils lignes fig.add_trace(go.Scatter( x=C1_lignes, y=C2_lignes, mode='markers+text', text=row_labels, textposition='top center', name='Profils Lignes', marker=dict(color='#0d6efd', size=10, symbol='circle'), hovertemplate='%{text}
Axe 1: %{x:.4f}
Axe 2: %{y:.4f}' )) # Ajouter les profils colonnes fig.add_trace(go.Scatter( x=C1_colonnes, y=C2_colonnes, mode='markers+text', text=col_labels, textposition='top center', name='Profils Colonnes', marker=dict(color='#dc3545', size=10, symbol='diamond'), hovertemplate='%{text}
Axe 1: %{x:.4f}
Axe 2: %{y:.4f}' )) # Ajouter des lignes pour l'origine fig.add_shape( type="line", x0=-max(abs(min(C1_lignes.min(), C1_colonnes.min())), abs(max(C1_lignes.max(), C1_colonnes.max()))) * 1.1, x1=max(abs(min(C1_lignes.min(), C1_colonnes.min())), abs(max(C1_lignes.max(), C1_colonnes.max()))) * 1.1, y0=0, y1=0, line=dict(color="rgba(0,0,0,0.3)", width=1, dash="dash") ) fig.add_shape( type="line", x0=0, x1=0, y0=-max(abs(min(C2_lignes.min(), C2_colonnes.min())), abs(max(C2_lignes.max(), C2_colonnes.max()))) * 1.1, y1=max(abs(min(C2_lignes.min(), C2_colonnes.min())), abs(max(C2_lignes.max(), C2_colonnes.max()))) * 1.1, line=dict(color="rgba(0,0,0,0.3)", width=1, dash="dash") ) # Améliorer le style du graphique fig.update_layout( title={ 'text': 'Projection des profils dans le plan factoriel', 'y':0.95, 'x':0.5, 'xanchor': 'center', 'yanchor': 'top', 'font': {'size': 18, 'color': '#212529', 'family': 'Arial, sans-serif'} }, xaxis_title={ 'text': f'Axe 1 ({inertia_percentages[0]:.2f}% d\'inertie)', 'font': {'size': 14, 'color': '#495057'} }, yaxis_title={ 'text': f'Axe 2 ({inertia_percentages[1]:.2f}% d\'inertie)', 'font': {'size': 14, 'color': '#495057'} }, showlegend=True, legend={ 'x': 0.01, 'y': 0.99, 'bgcolor': 'rgba(255,255,255,0.8)', 'bordercolor': 'rgba(0,0,0,0.1)', 'borderwidth': 1 }, hovermode='closest', plot_bgcolor='white', paper_bgcolor='white', autosize=True, margin=dict(l=60, r=60, t=80, b=60), font=dict(family="Arial, sans-serif", size=12, color="#212529") ) # Ajouter une grille légère fig.update_xaxes( showgrid=True, gridwidth=1, gridcolor='rgba(0,0,0,0.05)', zeroline=False, showline=True, linewidth=1, linecolor='rgba(0,0,0,0.2)' ) fig.update_yaxes( showgrid=True, gridwidth=1, gridcolor='rgba(0,0,0,0.05)', zeroline=False, showline=True, linewidth=1, linecolor='rgba(0,0,0,0.2)' ) # Ajuster les limites du graphique max_abs_x = max(abs(min(C1_lignes.min(), C1_colonnes.min())), abs(max(C1_lignes.max(), C1_colonnes.max()))) max_abs_y = max(abs(min(C2_lignes.min(), C2_colonnes.min())), abs(max(C2_lignes.max(), C2_colonnes.max()))) padding = 0.2 fig.update_xaxes(range=[-max_abs_x * (1 + padding), max_abs_x * (1 + padding)]) fig.update_yaxes(range=[-max_abs_y * (1 + padding), max_abs_y * (1 + padding)]) results["visualization"] = { "title": "Visualisation du plan factoriel", "plot": fig } # Étape 9 : Interprétation results["step9"] = { "title": "Étape 9: Interprétation", "interpretation": """ **Interprétation du graphique factoriel:** 1. **Proximité entre points:** - Les points proches les uns des autres sur le graphique ont des profils similaires. - Les points éloignés ont des profils très différents. 2. **Position par rapport aux axes:** - Les points qui contribuent fortement à un axe sont éloignés de l'origine sur cet axe. - Les points proches de l'origine sont peu discriminants. 3. **Relation entre lignes et colonnes:** - Une ligne et une colonne proches l'une de l'autre indiquent une association positive. - Une ligne et une colonne situées dans des directions opposées indiquent une association négative. 4. **Interprétation des axes:** - L'axe 1 explique la plus grande part de l'inertie totale ({inertia_percentages[0]:.2f}%). - L'axe 2 explique la deuxième plus grande part de l'inertie totale ({inertia_percentages[1]:.2f}%). - L'interprétation précise dépend de la nature des données analysées. 5. **Qualité de représentation:** - Les deux premiers axes expliquent {inertia_percentages[0] + inertia_percentages[1]:.2f}% de l'inertie totale. - Plus ce pourcentage est élevé, meilleure est la qualité de la représentation. """ } return results # Fonction pour parser la matrice d'entrée def parse_input_matrix(input_matrix, row_labels=None, col_labels=None): try: rows = input_matrix.strip().split(';') matrix = np.array([list(map(float, row.split(','))) for row in rows]) # Vérifier si les étiquettes sont fournies if row_labels: row_labels = row_labels.strip().split(',') if len(row_labels) != matrix.shape[0]: row_labels = [f'L{i+1}' for i in range(matrix.shape[0])] else: row_labels = [f'L{i+1}' for i in range(matrix.shape[0])] if col_labels: col_labels = col_labels.strip().split(',') if len(col_labels) != matrix.shape[1]: col_labels = [f'C{i+1}' for i in range(matrix.shape[1])] else: col_labels = [f'C{i+1}' for i in range(matrix.shape[1])] return matrix, row_labels, col_labels except Exception as e: raise ValueError(f"Erreur lors du parsing de la matrice: {str(e)}") # Fonction pour analyser un fichier CSV def parse_uploaded_file(contents, filename): content_type, content_string = contents.split(',') decoded = base64.b64decode(content_string) try: if 'csv' in filename: # Lire le fichier CSV df = pd.read_csv(io.StringIO(decoded.decode('utf-8'))) # Extraire les étiquettes et la matrice row_labels = df.index.tolist() if not df.index.is_numeric() else None if row_labels is None: row_labels = df.iloc[:, 0].tolist() df = df.iloc[:, 1:] col_labels = df.columns.tolist() matrix = df.values return matrix, row_labels, col_labels else: raise ValueError("Format de fichier non pris en charge. Veuillez télécharger un fichier CSV.") except Exception as e: raise ValueError(f"Erreur lors de l'analyse du fichier: {str(e)}") # Fonction pour créer un tooltip def create_tooltip(id, text): return html.Div([ html.Span( html.I(className="fas fa-info-circle ml-1"), id=id, style={"cursor": "pointer", "color": "#6c757d", "margin-left": "5px"} ), dbc.Tooltip( text, target=id, placement="right" ) ], style={"display": "inline"}) # Interface de l'application Dash app.layout = html.Div([ # Barre de navigation améliorée dbc.Navbar( [ # Logo et titre dbc.Container( [ html.A( dbc.Row( [ dbc.Col(html.Img(src=LOGO, height="40px")), dbc.Col(dbc.NavbarBrand("AFC Pro", className="ml-2", style={"font-weight": "600"})), ], align="center", className="g-0", ), href="#", style={"textDecoration": "none"}, ), # Boutons de navigation dbc.NavbarToggler(id="navbar-toggler"), dbc.Collapse( dbc.Nav( [ dbc.NavItem(dbc.NavLink( [html.I(className="fas fa-table mr-1"), " Données"], href="#data-section", external_link=True )), dbc.NavItem(dbc.NavLink( [html.I(className="fas fa-chart-scatter mr-1"), " Résultats"], href="#results-section", external_link=True )), dbc.NavItem(dbc.NavLink( [html.I(className="fas fa-question-circle mr-1"), " Aide"], href="#", id="help-button" )), dbc.NavItem(dbc.NavLink( [html.I(className="fas fa-download mr-1"), " Exporter"], href="#", id="export-button" )), ], className="ms-auto", navbar=True, ), id="navbar-collapse", navbar=True, ), ], fluid=True, ) ], color="primary", dark=True, className="mb-4", sticky="top", ), # Contenu principal dbc.Container([ # Message de bienvenue et introduction dbc.Row([ dbc.Col([ dbc.Card([ dbc.CardBody([ html.H3("Bienvenue dans AFC Pro", className="card-title"), html.P([ "AFC Pro est un outil professionnel pour réaliser des Analyses Factorielles des Correspondances. ", "Cette méthode statistique permet d'explorer les relations entre deux variables qualitatives ", "et de visualiser ces relations dans un espace factoriel." ], className="card-text"), dbc.Button( [html.I(className="fas fa-play-circle mr-1"), " Démarrer"], color="primary", href="#data-section", className="mt-2" ) ]) ], style=CUSTOM_STYLES["card"]) ]) ]), # Section d'entrée des données html.Div([ html.H3([ html.I(className="fas fa-table mr-2"), "Entrée des données", create_tooltip( "data-tooltip", "Entrez votre matrice de contingence sous forme de valeurs séparées par des virgules. " "Utilisez des points-virgules pour séparer les lignes." ) ], id="data-section", className="mt-4 mb-3", style=CUSTOM_STYLES["section-title"]), dbc.Row([ # Colonne de gauche - Entrée manuelle dbc.Col([ dbc.Card([ dbc.CardHeader([ html.H5("Entrée manuelle", className="mb-0"), ], style=CUSTOM_STYLES["card-header"]), dbc.CardBody([ html.Label("Matrice de contingence :", className="mb-1 font-weight-bold"), dcc.Textarea( id='input-matrix', value="30,10,3,15;45,8,0,12;8,66,33,5;2,44,50,8", style={'width': '100%', 'height': '100px', 'border': '1px solid #ced4da', 'border-radius': '0.25rem', 'padding': '0.5rem'}, className="mb-3" ), html.Label("Étiquettes des lignes (optionnel) :", className="mb-1 font-weight-bold"), dbc.Input( id='row-labels', type="text", placeholder="ex: Nord,Sud,Est,Ouest", className="mb-3" ), html.Label("Étiquettes des colonnes (optionnel) :", className="mb-1 font-weight-bold"), dbc.Input( id='col-labels', type="text", placeholder="ex: Viande,Poisson,Légumes,Fruits", className="mb-3" ), dbc.Button( [html.I(className="fas fa-calculator mr-1"), ' Lancer l\'AFC'], id='submit-button', color="primary", className="w-100 mb-3", style=CUSTOM_STYLES["button-primary"] ), ]) ], style=CUSTOM_STYLES["card"]) ], md=6), # Colonne de droite - Import et exemples dbc.Col([ # Carte pour l'importation de fichiers dbc.Card([ dbc.CardHeader([ html.H5("Importer des données", className="mb-0"), ], style=CUSTOM_STYLES["card-header"]), dbc.CardBody([ dcc.Upload( id='upload-data', children=html.Div([ html.I(className="fas fa-file-upload fa-2x mb-2"), html.P('Glissez-déposez ou ', style={'margin-bottom': '0'}), dbc.Button('Sélectionnez un fichier', color="outline-primary", size="sm") ], style={'textAlign': 'center'}), style={ 'width': '100%', 'height': '150px', 'lineHeight': '60px', 'borderWidth': '1px', 'borderStyle': 'dashed', 'borderRadius': '5px', 'textAlign': 'center', 'padding': '20px 10px', 'margin-bottom': '10px', 'display': 'flex', 'flexDirection': 'column', 'justifyContent': 'center', 'alignItems': 'center' }, multiple=False ), html.P("Formats acceptés: CSV", className="text-muted small") ]) ], className="mb-3", style=CUSTOM_STYLES["card"]), # Carte pour les exemples prédéfinis dbc.Card([ dbc.CardHeader([ html.H5("Exemples prédéfinis", className="mb-0"), ], style=CUSTOM_STYLES["card-header"]), dbc.CardBody([ dbc.RadioItems( id='example-selector', options=[ {'label': PREDEFINED_EXAMPLES['exemple_1']['name'], 'value': 'exemple_1'}, {'label': PREDEFINED_EXAMPLES['exemple_2']['name'], 'value': 'exemple_2'}, {'label': PREDEFINED_EXAMPLES['exemple_3']['name'], 'value': 'exemple_3'}, ], value=None, className="mb-3" ), dbc.Button( [html.I(className="fas fa-check-circle mr-1"), ' Utiliser cet exemple'], id='use-example-button', color="success", className="w-100", disabled=True ), ]) ], style=CUSTOM_STYLES["card"]) ], md=6) ]), # Indicateur de chargement dbc.Spinner( html.Div(id="loading-output"), color="primary", type="border", fullscreen=False, spinner_style={"width": "3rem", "height": "3rem"} ) ]), # Section des résultats html.Div([ html.H3([ html.I(className="fas fa-chart-scatter mr-2"), "Résultats de l'AFC", create_tooltip( "results-tooltip", "Cette section affiche les résultats de l'Analyse Factorielle des Correspondances, " "y compris le graphique factoriel et les statistiques associées." ) ], id="results-section", className="mt-5 mb-3", style=CUSTOM_STYLES["section-title"]), # Onglets pour les différentes visualisations dbc.Tabs([ dbc.Tab([ dbc.Row([ dbc.Col([ # Graphique principal dbc.Card([ dbc.CardHeader([ html.H5("Plan factoriel", className="mb-0 d-inline"), html.Div([ dbc.ButtonGroup([ dbc.Button( html.I(className="fas fa-download"), id="download-graph-button", color="link", size="sm", className="p-0 border-0", style={"color": "#6c757d"} ), dbc.Button( html.I(className="fas fa-expand"), id="expand-graph-button", color="link", size="sm", className="p-0 border-0 ml-2", style={"color": "#6c757d"} ), ], className="float-right") ], style={"float": "right"}) ], style=CUSTOM_STYLES["card-header"]), dbc.CardBody([ dcc.Graph( id='afc-plot', style={'height': '500px'}, config={ 'displayModeBar': True, 'modeBarButtonsToRemove': ['select2d', 'lasso2d'], 'displaylogo': False, 'toImageButtonOptions': { 'format': 'png', 'filename': 'afc_plot', 'height': 800, 'width': 1200, 'scale': 2 } } ) ]) ], style=CUSTOM_STYLES["card"]) ], md=8), # Colonne de droite - Statistiques et contrôles dbc.Col([ # Carte des statistiques dbc.Card([ dbc.CardHeader([ html.H5("Statistiques", className="mb-0"), ], style=CUSTOM_STYLES["card-header"]), dbc.CardBody([ html.Div(id="afc-stats") ]) ], className="mb-3", style=CUSTOM_STYLES["card"]), # Carte des options de visualisation dbc.Card([ dbc.CardHeader([ html.H5("Options de visualisation", className="mb-0"), ], style=CUSTOM_STYLES["card-header"]), dbc.CardBody([ html.Label("Taille des points:", className="mb-1 font-weight-bold"), dcc.Slider( id='point-size-slider', min=5, max=20, step=1, value=10, marks={i: str(i) for i in range(5, 21, 5)}, className="mb-3" ), html.Label("Couleurs:", className="mb-1 font-weight-bold"), dbc.Row([ dbc.Col([ html.Label("Lignes:", className="mb-1"), dbc.Input( id='row-color-input', type="color", value="#0d6efd", style={"width": "100%", "height": "38px"} ) ], width=6), dbc.Col([ html.Label("Colonnes:", className="mb-1"), dbc.Input( id='col-color-input', type="color", value="#dc3545", style={"width": "100%", "height": "38px"} ) ], width=6) ], className="mb-3"), html.Label("Affichage:", className="mb-1 font-weight-bold"), dbc.Checklist( options=[ {"label": "Étiquettes", "value": "labels"}, {"label": "Axes", "value": "axes"}, {"label": "Grille", "value": "grid"} ], value=["labels", "axes", "grid"], id="display-options", inline=True, className="mb-3" ), dbc.Button( [html.I(className="fas fa-sync-alt mr-1"), ' Appliquer'], id='apply-viz-options', color="primary", className="w-100", style=CUSTOM_STYLES["button-primary"] ), ]) ], style=CUSTOM_STYLES["card"]) ], md=4) ]) ], label="Graphique", tab_id="tab-graph"), dbc.Tab([ dbc.Card([ dbc.CardBody([ html.Div(id="afc-details") ]) ], style=CUSTOM_STYLES["card"]) ], label="Détails", tab_id="tab-details"), dbc.Tab([ dbc.Card([ dbc.CardBody([ html.Div(id="afc-interpretation") ]) ], style=CUSTOM_STYLES["card"]) ], label="Interprétation", tab_id="tab-interpretation"), dbc.Tab([ dbc.Card([ dbc.CardHeader([ html.H5("Assistant AFC", className="mb-0"), ], style=CUSTOM_STYLES["card-header"]), dbc.CardBody([ html.Div( id="chat-container", style={ "height": "500px", "overflowY": "auto", "padding": "15px", "border": "1px solid #e9ecef", "border-radius": "0.25rem", "background-color": "#f8f9fa" }, className="mb-3" ), dbc.InputGroup([ dbc.Input( id="user-input", placeholder="Posez une question sur l'AFC...", type="text" ), dbc.Button( [html.I(className="fas fa-paper-plane")], id="send-button", color="primary" ) ]) ]) ], style=CUSTOM_STYLES["card"]) ], label="Assistant", tab_id="tab-assistant"), ], id="result-tabs", active_tab="tab-graph"), ], id="results-container", style={"display": "none"}), # Pied de page html.Footer([ html.Hr(className="mt-5"), dbc.Row([ dbc.Col([ html.P([ "AFC Pro - Analyse Factorielle des Correspondances ", html.Span("v1.0", className="badge bg-secondary ml-1") ], className="text-muted") ], md=6), dbc.Col([ html.P([ html.A([html.I(className="fas fa-book mr-1"), " Documentation"], href="#", className="text-muted mr-3"), html.A([html.I(className="fas fa-question-circle mr-1"), " Aide"], href="#", className="text-muted") ], className="text-right") ], md=6) ]) ], className="mt-4") ], fluid=True), # Modales # Modale d'aide dbc.Modal([ dbc.ModalHeader("Aide - AFC Pro"), dbc.ModalBody([ html.H5("Qu'est-ce que l'AFC ?"), html.P([ "L'Analyse Factorielle des Correspondances (AFC) est une méthode statistique qui permet d'étudier ", "l'association entre deux variables qualitatives. Elle est particulièrement utile pour analyser ", "des tableaux de contingence et visualiser les relations entre les modalités des variables." ]), html.H5("Comment utiliser cette application ?"), dbc.ListGroup([ dbc.ListGroupItem([ html.Strong("1. Entrée des données"), html.P([ "Entrez votre matrice de contingence dans le champ prévu à cet effet. ", "Les valeurs doivent être séparées par des virgules et les lignes par des points-virgules. ", "Exemple : 10,5,3;7,8,2;4,3,5" ], className="mb-0") ]), dbc.ListGroupItem([ html.Strong("2. Étiquettes (optionnel)"), html.P([ "Vous pouvez spécifier des étiquettes pour les lignes et les colonnes. ", "Séparez-les par des virgules." ], className="mb-0") ]), dbc.ListGroupItem([ html.Strong("3. Lancer l'analyse"), html.P([ "Cliquez sur le bouton 'Lancer l'AFC' pour effectuer l'analyse." ], className="mb-0") ]), dbc.ListGroupItem([ html.Strong("4. Explorer les résultats"), html.P([ "Utilisez les onglets pour explorer le graphique factoriel, les détails de l'analyse ", "et l'interprétation des résultats." ], className="mb-0") ]), dbc.ListGroupItem([ html.Strong("5. Poser des questions"), html.P([ "Utilisez l'onglet 'Assistant' pour poser des questions sur l'analyse et les résultats." ], className="mb-0") ]) ], flush=True) ]), dbc.ModalFooter( dbc.Button("Fermer", id="close-help-modal", className="ml-auto") ) ], id="help-modal", size="lg"), # Modale d'exportation dbc.Modal([ dbc.ModalHeader("Exporter les résultats"), dbc.ModalBody([ html.P("Choisissez le format d'exportation :"), dbc.RadioItems( id='export-format', options=[ {'label': 'CSV - Données brutes', 'value': 'csv'}, {'label': 'PNG - Graphique factoriel', 'value': 'png'}, {'label': 'PDF - Rapport complet', 'value': 'pdf'}, {'label': 'JSON - Résultats détaillés', 'value': 'json'} ], value='csv', className="mb-3" ), dbc.Button( [html.I(className="fas fa-download mr-1"), ' Télécharger'], id='download-button', color="primary", className="w-100" ) ]), dbc.ModalFooter( dbc.Button("Fermer", id="close-export-modal", className="ml-auto") ) ], id="export-modal"), # Modale d'erreur dbc.Modal([ dbc.ModalHeader("Erreur"), dbc.ModalBody(id="error-message"), dbc.ModalFooter( dbc.Button("Fermer", id="close-error-modal", className="ml-auto") ) ], id="error-modal"), # Stockage des données dcc.Store(id='afc-results'), dcc.Store(id='chat-history', data=[]), dcc.Store(id='current-example', data=None), dcc.Store(id='uploaded-data', data=None) ]) # Callback pour activer/désactiver le bouton d'exemple @app.callback( Output('use-example-button', 'disabled'), Input('example-selector', 'value') ) def toggle_example_button(selected_example): return selected_example is None # Callback pour utiliser un exemple prédéfini @app.callback( [Output('input-matrix', 'value'), Output('row-labels', 'value'), Output('col-labels', 'value'), Output('current-example', 'data')], Input('use-example-button', 'n_clicks'), State('example-selector', 'value'), prevent_initial_call=True ) def use_example(n_clicks, selected_example): if selected_example: example = PREDEFINED_EXAMPLES[selected_example] return example['matrix'], example['row_labels'], example['col_labels'], selected_example return dash.no_update, dash.no_update, dash.no_update, dash.no_update # Callback pour traiter les fichiers téléchargés @app.callback( [Output('input-matrix', 'value', allow_duplicate=True), Output('row-labels', 'value', allow_duplicate=True), Output('col-labels', 'value', allow_duplicate=True), Output('uploaded-data', 'data'), Output('error-modal', 'is_open'), Output('error-message', 'children')], Input('upload-data', 'contents'), State('upload-data', 'filename'), prevent_initial_call=True ) def update_output(contents, filename): if contents is None: return dash.no_update, dash.no_update, dash.no_update, None, False, "" try: matrix, row_labels, col_labels = parse_uploaded_file(contents, filename) # Convertir la matrice en format texte matrix_text = ";".join([",".join(map(str, row)) for row in matrix]) row_labels_text = ",".join(row_labels) if row_labels else "" col_labels_text = ",".join(col_labels) if col_labels else "" return matrix_text, row_labels_text, col_labels_text, { "matrix": matrix.tolist(), "row_labels": row_labels, "col_labels": col_labels }, False, "" except Exception as e: return dash.no_update, dash.no_update, dash.no_update, None, True, str(e) # Callback pour effectuer l'AFC et mettre à jour les résultats @app.callback( [Output('afc-plot', 'figure'), Output('afc-results', 'data'), Output('chat-container', 'children'), Output('results-container', 'style'), Output('afc-stats', 'children'), Output('afc-details', 'children'), Output('afc-interpretation', 'children'), Output('loading-output', 'children')], Input('submit-button', 'n_clicks'), [State('input-matrix', 'value'), State('row-labels', 'value'), State('col-labels', 'value')], prevent_initial_call=True ) def perform_afc(n_clicks, input_matrix, row_labels, col_labels): if n_clicks is None: return dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update try: # Parser la matrice d'entrée matrix, row_labels_list, col_labels_list = parse_input_matrix(input_matrix, row_labels, col_labels) # Effectuer l'AFC results = afc_detailed(matrix, row_labels_list, col_labels_list) # Mettre à jour le chat avec les résultats de l'AFC chat_messages = [ create_chat_message("Assistant AFC", "L'analyse AFC a été effectuée avec succès. Voici les résultats :"), create_chat_message("Assistant AFC", f"**Inertie totale :** {results['step7']['total_inertia']:.4f}"), create_chat_message("Assistant AFC", f"**Inertie expliquée par l'axe 1 :** {results['step7']['inertia_by_axis'][0]:.2f}%"), create_chat_message("Assistant AFC", f"**Inertie expliquée par l'axe 2 :** {results['step7']['inertia_by_axis'][1]:.2f}%"), create_chat_message("Assistant AFC", "Vous pouvez me poser des questions sur les résultats ou l'interprétation.") ] # Créer les statistiques stats = html.Div([ dbc.ListGroup([ dbc.ListGroupItem([ html.Div([ html.Strong("Inertie totale:"), html.Span(f"{results['step7']['total_inertia']:.4f}", className="float-right") ]) ]), dbc.ListGroupItem([ html.Div([ html.Strong("Dimensions:"), html.Span(f"{matrix.shape[0]} × {matrix.shape[1]}", className="float-right") ]) ]) ], className="mb-3"), html.Strong("Inertie par axe:"), html.Div([ dbc.Progress([ dbc.Progress( value=results['step7']['inertia_by_axis'][0], color="primary", bar=True, label=f"{results['step7']['inertia_by_axis'][0]:.2f}%", style={"color": "white"} ), ], multi=True, className="mb-1"), dbc.Progress([ dbc.Progress( value=results['step7']['inertia_by_axis'][1], color="success", bar=True, label=f"{results['step7']['inertia_by_axis'][1]:.2f}%", style={"color": "white"} ), ], multi=True, className="mb-1"), dbc.Progress([ dbc.Progress( value=results['step7']['inertia_by_axis'][2] if len(results['step7']['inertia_by_axis']) > 2 else 0, color="info", bar=True, label=f"{results['step7']['inertia_by_axis'][2]:.2f}%" if len(results['step7']['inertia_by_axis']) > 2 else "", style={"color": "white"} ), ], multi=True, className="mb-3"), ]), html.Strong("Inertie cumulée:"), html.Div([ dbc.Progress([ dbc.Progress( value=results['step7']['cumulative_inertia'][1], color="primary", bar=True, label=f"{results['step7']['cumulative_inertia'][1]:.2f}%", style={"color": "white"} ), ], multi=True, className="mb-3"), ]), html.Div([ dbc.Button( [html.I(className="fas fa-table mr-1"), " Voir les coordonnées"], id="show-coordinates-button", color="outline-primary", className="w-100" ) ]) ]) # Créer les détails details = html.Div([ dbc.Tabs([ dbc.Tab([ html.Div([ html.H5("Matrice de contingence originale", className="mt-3 mb-2"), html.Div( dbc.Table.from_dataframe( pd.DataFrame( results['step1']['original_matrix'], index=row_labels_list, columns=col_labels_list ), striped=True, bordered=True, hover=True, responsive=True, className="mb-3" ) ), html.H5("Matrice des fréquences", className="mt-4 mb-2"), html.Div( dbc.Table.from_dataframe( pd.DataFrame( results['step2']['frequency_matrix'], index=row_labels_list, columns=col_labels_list ).round(4), striped=True, bordered=True, hover=True, responsive=True, className="mb-3" ) ), html.H5("Fréquences marginales", className="mt-4 mb-2"), dbc.Row([ dbc.Col([ html.H6("Lignes", className="mb-2"), html.Div( dbc.Table.from_dataframe( pd.DataFrame( results['step3']['row_margins'], index=row_labels_list, columns=["Fréquence"] ).round(4), striped=True, bordered=True, hover=True, responsive=True ) ) ], md=6), dbc.Col([ html.H6("Colonnes", className="mb-2"), html.Div( dbc.Table.from_dataframe( pd.DataFrame( results['step3']['column_margins'], index=col_labels_list, columns=["Fréquence"] ).round(4), striped=True, bordered=True, hover=True, responsive=True ) ) ], md=6) ]) ]) ], label="Matrices", tab_id="tab-matrices"), dbc.Tab([ html.Div([ html.H5("Valeurs propres", className="mt-3 mb-2"), html.Div( dbc.Table.from_dataframe( pd.DataFrame({ "Valeur propre": results['step6']['eigenvalues'], "Inertie (%)": results['step7']['inertia_by_axis'], "Inertie cumulée (%)": results['step7']['cumulative_inertia'] }).round(4), striped=True, bordered=True, hover=True, responsive=True, className="mb-4" ) ), html.H5("Coordonnées des profils", className="mt-4 mb-2"), dbc.Row([ dbc.Col([ html.H6("Lignes", className="mb-2"), html.Div( dbc.Table.from_dataframe( pd.DataFrame({ "Axe 1": results['step8']['row_coordinates']['axis1'], "Axe 2": results['step8']['row_coordinates']['axis2'] }, index=results['step8']['row_coordinates']['labels']).round(4), striped=True, bordered=True, hover=True, responsive=True ) ) ], md=6), dbc.Col([ html.H6("Colonnes", className="mb-2"), html.Div( dbc.Table.from_dataframe( pd.DataFrame({ "Axe 1": results['step8']['column_coordinates']['axis1'], "Axe 2": results['step8']['column_coordinates']['axis2'] }, index=results['step8']['column_coordinates']['labels']).round(4), striped=True, bordered=True, hover=True, responsive=True ) ) ], md=6) ]) ]) ], label="Coordonnées", tab_id="tab-coordinates") ], className="mt-3") ]) # Créer l'interprétation interpretation = html.Div([ dbc.Card([ dbc.CardBody([ html.H5("Interprétation du graphique factoriel", className="mb-3"), dcc.Markdown(results['step9']['interpretation'], className="mb-0") ]) ], className="mb-4", style=CUSTOM_STYLES["card"]), dbc.Card([ dbc.CardBody([ html.H5("Comment lire le graphique ?", className="mb-3"), html.P([ "Le graphique factoriel représente les relations entre les lignes et les colonnes de la matrice de contingence. ", "Voici quelques clés d'interprétation :" ]), dbc.ListGroup([ dbc.ListGroupItem([ html.Strong("Points proches dans le graphique:"), html.P("Indiquent des profils similaires.", className="mb-0") ]), dbc.ListGroupItem([ html.Strong("Points éloignés de l'origine:"), html.P("Contribuent fortement à la définition des axes.", className="mb-0") ]), dbc.ListGroupItem([ html.Strong("Points proches entre lignes et colonnes:"), html.P("Indiquent une association positive entre ces modalités.", className="mb-0") ]), dbc.ListGroupItem([ html.Strong("Points dans des directions opposées:"), html.P("Indiquent une association négative.", className="mb-0") ]), dbc.ListGroupItem([ html.Strong("Axes factoriels:"), html.P([ f"L'axe 1 (horizontal) explique {results['step7']['inertia_by_axis'][0]:.2f}% de l'inertie totale. ", f"L'axe 2 (vertical) explique {results['step7']['inertia_by_axis'][1]:.2f}% de l'inertie totale." ], className="mb-0") ]) ], flush=True, className="mb-3"), html.P([ "Pour une interprétation plus précise, examinez les coordonnées des points sur chaque axe ", "et identifiez les modalités qui contribuent le plus à la définition de ces axes." ]) ]) ], style=CUSTOM_STYLES["card"]) ]) return results['visualization']['plot'], results, chat_messages, {"display": "block"}, stats, details, interpretation, "" except Exception as e: error_message = f"Erreur: {str(e)}" return go.Figure(), None, [create_chat_message("Assistant AFC", error_message, is_code=True)], {"display": "none"}, "", "", "", "" # Callback pour gérer l'entrée de l'utilisateur dans le chat @app.callback( [Output('chat-container', 'children', allow_duplicate=True), Output('user-input', 'value')], Input('send-button', 'n_clicks'), [State('user-input', 'value'), State('chat-container', 'children'), State('afc-results', 'data')], prevent_initial_call=True ) def handle_chat_input(n_clicks, user_input, chat_history, afc_results): if not user_input: return chat_history, "" # Ajouter le message de l'utilisateur au chat chat_history.append(create_chat_message("Vous", user_input)) # Générer une réponse du bot en fonction de l'entrée de l'utilisateur if afc_results: user_input_lower = user_input.lower() if any(keyword in user_input_lower for keyword in ["valeur propre", "valeurs propres"]): eigenvalues = afc_results['step6']['eigenvalues'] inertia = afc_results['step7']['inertia_by_axis'] eigenvalues_table = pd.DataFrame({ "Valeur propre": eigenvalues[:3], "Inertie (%)": inertia[:3], }).round(4) bot_response = f"Les principales valeurs propres sont :\n\n" bot_response += eigenvalues_table.to_string() bot_response += f"\n\nCes valeurs représentent l'importance de chaque axe factoriel." elif any(keyword in user_input_lower for keyword in ["inertie", "variance"]): inertia_total = afc_results['step7']['total_inertia'] inertia_by_axis = afc_results['step7']['inertia_by_axis'] cumulative_inertia = afc_results['step7']['cumulative_inertia'] bot_response = f"L'inertie totale est : {inertia_total:.4f}\n\n" bot_response += f"Répartition de l'inertie :\n" bot_response += f"- Axe 1 : {inertia_by_axis[0]:.2f}% de l'inertie totale\n" bot_response += f"- Axe 2 : {inertia_by_axis[1]:.2f}% de l'inertie totale\n" if len(inertia_by_axis) > 2: bot_response += f"- Axe 3 : {inertia_by_axis[2]:.2f}% de l'inertie totale\n" bot_response += f"\nInertie cumulée des deux premiers axes : {cumulative_inertia[1]:.2f}%" elif any(keyword in user_input_lower for keyword in ["coordonnée", "coordonnées", "position"]): row_coords = pd.DataFrame({ "Axe 1": afc_results['step8']['row_coordinates']['axis1'], "Axe 2": afc_results['step8']['row_coordinates']['axis2'] }, index=afc_results['step8']['row_coordinates']['labels']).round(4) col_coords = pd.DataFrame({ "Axe 1": afc_results['step8']['column_coordinates']['axis1'], "Axe 2": afc_results['step8']['column_coordinates']['axis2'] }, index=afc_results['step8']['column_coordinates']['labels']).round(4) bot_response = f"Coordonnées des profils lignes :\n\n" bot_response += row_coords.to_string() bot_response += f"\n\nCoordonnées des profils colonnes :\n\n" bot_response += col_coords.to_string() elif any(keyword in user_input_lower for keyword in ["matrice", "fréquence", "fréquences", "contingence"]): # Extraire et formater la matrice de fréquence frequency_matrix = pd.DataFrame( afc_results['step2']['frequency_matrix'], index=afc_results['step8']['row_coordinates']['labels'], columns=afc_results['step8']['column_coordinates']['labels'] ).round(4) bot_response = f"La matrice de fréquence est :\n\n" bot_response += frequency_matrix.to_string() elif any(keyword in user_input_lower for keyword in ["interpréter", "interprétation", "signification", "expliquer"]): bot_response = afc_results['step9']['interpretation'] elif any(keyword in user_input_lower for keyword in ["axe", "axes", "facteur", "facteurs"]): inertia_by_axis = afc_results['step7']['inertia_by_axis'] bot_response = f"Interprétation des axes factoriels :\n\n" bot_response += f"- L'axe 1 (horizontal) explique {inertia_by_axis[0]:.2f}% de l'inertie totale.\n" bot_response += f"- L'axe 2 (vertical) explique {inertia_by_axis[1]:.2f}% de l'inertie totale.\n\n" bot_response += "Pour interpréter ces axes, examinez les modalités qui ont les coordonnées les plus élevées (en valeur absolue) sur chaque axe." elif any(keyword in user_input_lower for keyword in ["aide", "help", "comment", "utiliser"]): bot_response = """ Je suis l'Assistant AFC, je peux vous aider à comprendre les résultats de l'Analyse Factorielle des Correspondances. Voici quelques questions que vous pouvez me poser : - Quelles sont les valeurs propres ? - Quelle est l'inertie expliquée par les axes ? - Quelles sont les coordonnées des profils ? - Comment interpréter le graphique factoriel ? - Quelle est la signification des axes ? - Montrer la matrice de fréquence N'hésitez pas à me poser d'autres questions sur l'AFC ! """ else: bot_response = """ Je suis l'Assistant AFC. Je peux vous aider à comprendre les résultats de l'analyse. Vous pouvez me poser des questions sur : - Les valeurs propres et l'inertie - Les coordonnées des profils - L'interprétation du graphique - La signification des axes - Les matrices de données Comment puis-je vous aider avec votre analyse ? """ else: bot_response = "Veuillez d'abord lancer l'AFC pour obtenir des résultats." # Ajouter la réponse du bot au chat chat_history.append(create_chat_message("Assistant AFC", bot_response)) return chat_history, "" # Callback pour mettre à jour les options de visualisation @app.callback( Output('afc-plot', 'figure', allow_duplicate=True), Input('apply-viz-options', 'n_clicks'), [State('afc-results', 'data'), State('point-size-slider', 'value'), State('row-color-input', 'value'), State('col-color-input', 'value'), State('display-options', 'value')], prevent_initial_call=True ) def update_visualization(n_clicks, afc_results, point_size, row_color, col_color, display_options): if not afc_results: return dash.no_update # Récupérer les données C1_lignes = np.array(afc_results['step8']['row_coordinates']['axis1']) C2_lignes = np.array(afc_results['step8']['row_coordinates']['axis2']) C1_colonnes = np.array(afc_results['step8']['column_coordinates']['axis1']) C2_colonnes = np.array(afc_results['step8']['column_coordinates']['axis2']) row_labels = afc_results['step8']['row_coordinates']['labels'] col_labels = afc_results['step8']['column_coordinates']['labels'] inertia_percentages = afc_results['step7']['inertia_by_axis'] # Créer la visualisation du plan factoriel fig = go.Figure() # Ajouter les profils lignes fig.add_trace(go.Scatter( x=C1_lignes, y=C2_lignes, mode='markers+text' if 'labels' in display_options else 'markers', text=row_labels, textposition='top center', name='Profils Lignes', marker=dict(color=row_color, size=point_size, symbol='circle'), hovertemplate='%{text}
Axe 1: %{x:.4f}
Axe 2: %{y:.4f}' )) # Ajouter les profils colonnes fig.add_trace(go.Scatter( x=C1_colonnes, y=C2_colonnes, mode='markers+text' if 'labels' in display_options else 'markers', text=col_labels, textposition='top center', name='Profils Colonnes', marker=dict(color=col_color, size=point_size, symbol='diamond'), hovertemplate='%{text}
Axe 1: %{x:.4f}
Axe 2: %{y:.4f}' )) # Ajouter des lignes pour l'origine si l'option est activée if 'axes' in display_options: max_abs_x = max(abs(min(C1_lignes.min(), C1_colonnes.min())), abs(max(C1_lignes.max(), C1_colonnes.max()))) max_abs_y = max(abs(min(C2_lignes.min(), C2_colonnes.min())), abs(max(C2_lignes.max(), C2_colonnes.max()))) fig.add_shape( type="line", x0=-max_abs_x * 1.1, x1=max_abs_x * 1.1, y0=0, y1=0, line=dict(color="rgba(0,0,0,0.3)", width=1, dash="dash") ) fig.add_shape( type="line", x0=0, x1=0, y0=-max_abs_y * 1.1, y1=max_abs_y * 1.1, line=dict(color="rgba(0,0,0,0.3)", width=1, dash="dash") ) # Améliorer le style du graphique fig.update_layout( title={ 'text': 'Projection des profils dans le plan factoriel', 'y':0.95, 'x':0.5, 'xanchor': 'center', 'yanchor': 'top', 'font': {'size': 18, 'color': '#212529', 'family': 'Arial, sans-serif'} }, xaxis_title={ 'text': f'Axe 1 ({inertia_percentages[0]:.2f}% d\'inertie)', 'font': {'size': 14, 'color': '#495057'} }, yaxis_title={ 'text': f'Axe 2 ({inertia_percentages[1]:.2f}% d\'inertie)', 'font': {'size': 14, 'color': '#495057'} }, showlegend=True, legend={ 'x': 0.01, 'y': 0.99, 'bgcolor': 'rgba(255,255,255,0.8)', 'bordercolor': 'rgba(0,0,0,0.1)', 'borderwidth': 1 }, hovermode='closest', plot_bgcolor='white', paper_bgcolor='white', autosize=True, margin=dict(l=60, r=60, t=80, b=60), font=dict(family="Arial, sans-serif", size=12, color="#212529") ) # Ajouter une grille légère si l'option est activée fig.update_xaxes( showgrid='grid' in display_options, gridwidth=1, gridcolor='rgba(0,0,0,0.05)', zeroline=False, showline=True, linewidth=1, linecolor='rgba(0,0,0,0.2)' ) fig.update_yaxes( showgrid='grid' in display_options, gridwidth=1, gridcolor='rgba(0,0,0,0.05)', zeroline=False, showline=True, linewidth=1, linecolor='rgba(0,0,0,0.2)' ) # Ajuster les limites du graphique max_abs_x = max(abs(min(C1_lignes.min(), C1_colonnes.min())), abs(max(C1_lignes.max(), C1_colonnes.max()))) max_abs_y = max(abs(min(C2_lignes.min(), C2_colonnes.min())), abs(max(C2_lignes.max(), C2_colonnes.max()))) padding = 0.2 fig.update_xaxes(range=[-max_abs_x * (1 + padding), max_abs_x * (1 + padding)]) fig.update_yaxes(range=[-max_abs_y * (1 + padding), max_abs_y * (1 + padding)]) return fig # Callbacks pour les modales @app.callback( Output("help-modal", "is_open"), [Input("help-button", "n_clicks"), Input("close-help-modal", "n_clicks")], [State("help-modal", "is_open")], ) def toggle_help_modal(n1, n2, is_open): if n1 or n2: return not is_open return is_open @app.callback( Output("export-modal", "is_open"), [Input("export-button", "n_clicks"), Input("close-export-modal", "n_clicks")], [State("export-modal", "is_open")], ) def toggle_export_modal(n1, n2, is_open): if n1 or n2: return not is_open return is_open @app.callback( Output("error-modal", "is_open", allow_duplicate=True), [Input("close-error-modal", "n_clicks")], [State("error-modal", "is_open")], prevent_initial_call=True ) def close_error_modal(n, is_open): if n: return False return is_open # Lancer l'application if __name__ == '__main__': app.run(debug=False, host='0.0.0.0', port=7860)