| | 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 |
| |
|
| | |
| | app = dash.Dash( |
| | __name__, |
| | external_stylesheets=[ |
| | dbc.themes.BOOTSTRAP, |
| | "https://use.fontawesome.com/releases/v5.15.4/css/all.css" |
| | ], |
| | suppress_callback_exceptions=True, |
| | meta_tags=[ |
| | {"name": "viewport", "content": "width=device-width, initial-scale=1"} |
| | ] |
| | ) |
| |
|
| | |
| | app.title = "AFC Pro - Analyse Factorielle des Correspondances" |
| |
|
| | |
| | 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 = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNDAgNjAiPjxwYXRoIGZpbGw9IiMwZDZlZmQiIGQ9Ik0zMCwxMEMxOC45NTQsMTAgMTAsMTguOTU0IDEwLDMwczguOTU0LDIwIDIwLDIwczIwLTguOTU0IDIwLTIwUzQxLjA0NiwxMCAzMCwxMHogTTMwLDQ1Yy04LjI4NCwwLTE1LTYuNzE2LTE1LTE1czYuNzE2LTE1IDE1LTE1czE1LDYuNzE2IDE1LDE1UzM4LjI4NCw0NSAzMCw0NXoiLz48cGF0aCBmaWxsPSIjMGQ2ZWZkIiBkPSJNNzAsMTBDNTguOTU0LDEwIDUwLDE4Ljk1NCA1MCwzMHM4Ljk1NCwyMCAyMCwyMHMyMC04Ljk1NCAyMC0yMFM4MS4wNDYsMTAgNzAsMTB6IE03MCw0NWMtOC4yODQsMC0xNS02LjcxNi0xNS0xNXM2LjcxNi0xNSAxNS0xNXMxNSw2LjcxNiAxNSwxNVM3OC4yODQsNDUgNzAsNDV6Ii8+PHBhdGggZmlsbD0iIzIxMjUyOSIgZD0iTTExMCwyMGg1djIwaC01VjIweiIvPjxwYXRoIGZpbGw9IiMyMTI1MjkiIGQ9Ik0xMjAsMjBoMjB2NWgtMjBWMjB6Ii8+PHBhdGggZmlsbD0iIzIxMjUyOSIgZD0iTTEyMCwzNWgyMHY1aC0yMFYzNXoiLz48cGF0aCBmaWxsPSIjMjEyNTI5IiBkPSJNMTIwLDI3LjVoMjB2NWgtMjBWMjcuNXoiLz48cGF0aCBmaWxsPSIjMjEyNTI5IiBkPSJNMTUwLDIwaDIwdjVoLTIwVjIweiIvPjxwYXRoIGZpbGw9IiMyMTI1MjkiIGQ9Ik0xNTAsMzVoMjB2NWgtMjBWMzV6Ii8+PHBhdGggZmlsbD0iIzIxMjUyOSIgZD0iTTE1MCwyMGg1djIwaC01VjIweiIvPjxwYXRoIGZpbGw9IiMyMTI1MjkiIGQ9Ik0xODAsMjBoMjB2NWgtMjBWMjB6Ii8+PHBhdGggZmlsbD0iIzIxMjUyOSIgZD0iTTE4MCwzNWgyMHY1aC0yMFYzNXoiLz48cGF0aCBmaWxsPSIjMjEyNTI5IiBkPSJNMTgwLDI3LjVoMjB2NWgtMjBWMjcuNXoiLz48cGF0aCBmaWxsPSIjMjEyNTI5IiBkPSJNMTgwLDIwaDV2MjBoLTVWMjB6Ii8+PHBhdGggZmlsbD0iIzIxMjUyOSIgZD0iTTE5NSwyMGg1djIwaC01VjIweiIvPjwvc3ZnPg==" |
| |
|
| | |
| | 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" |
| | } |
| | } |
| |
|
| | |
| | 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"}) |
| |
|
| | |
| | def afc_detailed(matrice_contingence, row_labels=None, col_labels=None): |
| | results = {} |
| | |
| | |
| | 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." |
| | } |
| | |
| | |
| | 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." |
| | } |
| | |
| | |
| | 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." |
| | } |
| | |
| | |
| | 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)." |
| | } |
| | |
| | |
| | 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." |
| | } |
| | |
| | |
| | S = X.T @ X |
| | valeurs_propres, vecteurs_propres = np.linalg.eig(S) |
| | |
| | valeurs_propres = np.real_if_close(valeurs_propres) |
| | vecteurs_propres = np.real_if_close(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." |
| | } |
| | |
| | |
| | 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." |
| | } |
| | |
| | |
| | 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 |
| | |
| | |
| | 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." |
| | } |
| | |
| | |
| | fig = go.Figure() |
| | |
| | |
| | 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='<b>%{text}</b><br>Axe 1: %{x:.4f}<br>Axe 2: %{y:.4f}<extra></extra>' |
| | )) |
| | |
| | |
| | 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='<b>%{text}</b><br>Axe 1: %{x:.4f}<br>Axe 2: %{y:.4f}<extra></extra>' |
| | )) |
| | |
| | |
| | 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") |
| | ) |
| | |
| | |
| | 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") |
| | ) |
| | |
| | |
| | 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)' |
| | ) |
| | |
| | |
| | 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 |
| | } |
| | |
| | |
| | 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 |
| |
|
| | |
| | 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]) |
| | |
| | |
| | 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)}") |
| |
|
| | |
| | def parse_uploaded_file(contents, filename): |
| | content_type, content_string = contents.split(',') |
| | decoded = base64.b64decode(content_string) |
| | |
| | try: |
| | if 'csv' in filename: |
| | |
| | df = pd.read_csv(io.StringIO(decoded.decode('utf-8'))) |
| | |
| | |
| | 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)}") |
| |
|
| | |
| | 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"}) |
| |
|
| | |
| | app.layout = html.Div([ |
| | |
| | dbc.Navbar( |
| | [ |
| | |
| | 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"}, |
| | ), |
| | |
| | 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", |
| | ), |
| | |
| | |
| | dbc.Container([ |
| | |
| | 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"]) |
| | ]) |
| | ]), |
| | |
| | |
| | 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([ |
| | |
| | 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), |
| | |
| | |
| | dbc.Col([ |
| | |
| | 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"]), |
| | |
| | |
| | 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) |
| | ]), |
| | |
| | |
| | dbc.Spinner( |
| | html.Div(id="loading-output"), |
| | color="primary", |
| | type="border", |
| | fullscreen=False, |
| | spinner_style={"width": "3rem", "height": "3rem"} |
| | ) |
| | ]), |
| | |
| | |
| | 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"]), |
| | |
| | |
| | dbc.Tabs([ |
| | dbc.Tab([ |
| | dbc.Row([ |
| | dbc.Col([ |
| | |
| | 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), |
| | |
| | |
| | dbc.Col([ |
| | |
| | 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"]), |
| | |
| | |
| | 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"}), |
| | |
| | |
| | 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), |
| | |
| | |
| | |
| | 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"), |
| | |
| | |
| | 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"), |
| | |
| | |
| | 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"), |
| | |
| | |
| | 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) |
| | ]) |
| |
|
| | |
| | @app.callback( |
| | Output('use-example-button', 'disabled'), |
| | Input('example-selector', 'value') |
| | ) |
| | def toggle_example_button(selected_example): |
| | return selected_example is None |
| |
|
| | |
| | @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 |
| |
|
| | |
| | @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) |
| | |
| | |
| | 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) |
| |
|
| | |
| | @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: |
| | |
| | matrix, row_labels_list, col_labels_list = parse_input_matrix(input_matrix, row_labels, col_labels) |
| | |
| | |
| | results = afc_detailed(matrix, row_labels_list, col_labels_list) |
| | |
| | |
| | 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.") |
| | ] |
| | |
| | |
| | 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" |
| | ) |
| | ]) |
| | ]) |
| | |
| | |
| | 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") |
| | ]) |
| | |
| | |
| | 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"}, "", "", "", "" |
| |
|
| | |
| | @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, "" |
| | |
| | |
| | chat_history.append(create_chat_message("Vous", user_input)) |
| | |
| | |
| | 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"]): |
| | |
| | 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." |
| | |
| | |
| | chat_history.append(create_chat_message("Assistant AFC", bot_response)) |
| | |
| | return chat_history, "" |
| |
|
| | |
| | @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 |
| | |
| | |
| | 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'] |
| | |
| | |
| | fig = go.Figure() |
| | |
| | |
| | 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='<b>%{text}</b><br>Axe 1: %{x:.4f}<br>Axe 2: %{y:.4f}<extra></extra>' |
| | )) |
| | |
| | |
| | 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='<b>%{text}</b><br>Axe 1: %{x:.4f}<br>Axe 2: %{y:.4f}<extra></extra>' |
| | )) |
| | |
| | |
| | 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") |
| | ) |
| | |
| | |
| | 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") |
| | ) |
| | |
| | |
| | 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)' |
| | ) |
| | |
| | |
| | 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 |
| |
|
| | |
| | @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 |
| |
|
| | |
| | if __name__ == '__main__': |
| | app.run(debug=False, host='0.0.0.0', port=7860) |
| |
|
| |
|
| |
|