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)