Guide méthodologique complet

L’Analyse de Variance (ANOVA) expliquĂ©e par le code

De l’import du fichier Excel Ă  la conclusion automatisĂ©e — chaque ligne du script Python dĂ©cryptĂ©e, chaque test statistique illustrĂ©.

L’objectif de cette mĂ©thode : Dans notre Ă©tude, nous avons testĂ© 3 scĂ©narios diffĂ©rents impliquant un agent d’Intelligence Artificielle (IA) chargĂ© de commander des repas Ă  la cantine. L’ANOVA est l’outil statistique qui permet de savoir si la perception des utilisateurs change rĂ©ellement d’un scĂ©nario Ă  l’autre, ou si les Ă©carts de notes (sur l’Ă©chelle de 1 Ă  5) sont simplement dus au hasard des profils interrogĂ©s

Sommaire
  1. Chargement & recodage des données
  2. Analyse en Composantes Principales (ACP)
  3. Statistiques descriptives
  4. Test de Levene (homogénéité)
  5. Table ANOVA & test de Welch
  6. Tailles d’effet (ηÂČ et ωÂČ)
  7. Comparaisons post-hoc (Tukey)
  8. Double boucle & synthĂšse finale
Les trois scénarios de commande de repas via un agent d'IA
Figure 1 — Les trois stimuli (ScĂ©narios 1, 2 et 3) prĂ©sentĂ©s aux participants.

🔍 Les 4 dimensions mesurĂ©es (variables dĂ©pendantes)

Chaque participant Ă©value l’agent sur ces 4 construits, mesurĂ©s via plusieurs items sur une Ă©chelle de Likert (1 Ă  5) :

  • Alignement (5 items) — L’agent agit-il dans mon intĂ©rĂȘt ou celui du restaurant ?
  • CompĂ©tence (4 items) — L’agent fait-il de bonnes suggestions de repas ?
  • ContrĂŽle (5 items, dont 1 inversĂ©) — Est-ce que je garde la main sur la commande ?
  • Intention d’usage (4 items) — Suis-je prĂȘt(e) Ă  utiliser ce mode Ă  l’avenir ?

La variable indépendante (facteur) est le numéro de scénario, stocké dans la colonne GROUPE (valeurs 1, 2 ou 3).

Vue d’ensemble du pipeline

Chaque nƓud correspond Ă  une section prĂ©cise du code Python.

ImportExcel → Pandas
â€ș
RecodageItems inversés
â€ș
ACPScore global
â€ș
DescriptifsMoy. / É.-T.
â€ș
LeveneHomogénéité
â€ș
ANOVA/ Welch
â€ș
EffetηÂČ & ωÂČ
â€ș
TukeyPost-hoc

Chargement et préparation des données

Le socle de toute analyse : importer, filtrer et recoder.

Étape 0A · Importation
Charger le fichier Excel et sélectionner les groupes

Le script lit le fichier RC_AGENT.xlsx (feuille BASE), puis ne conserve que les lignes oĂč GROUPE vaut 1, 2 ou 3 — ce qui exclut d’Ă©ventuels prĂ©-tests ou donnĂ©es incomplĂštes.

chargement.py
import pandas as pd
import numpy as np

file_path = '/content/RC_AGENT.xlsx'
df = pd.read_excel(file_path, sheet_name='BASE')

# Filtrage : on ne garde que les 3 groupes expérimentaux
df_analysis = df[df['GROUPE'].isin([1, 2, 3])].copy()
Étape 0B · Recodage d’item inversĂ©
Retourner les Ă©chelles qui mesurent « Ă  l’envers »

L’item CTRL3R_S1 est formulĂ© de façon nĂ©gative (ex : « J’ai le sentiment de ne PAS avoir le contrĂŽle »). Un score Ă©levĂ© (5/5) y signifie en rĂ©alitĂ© un faible contrĂŽle perçu. Le code inverse donc l’Ă©chelle : 1↔5, 2↔4, 3 reste 3. AprĂšs recodage, un score Ă©levĂ© signifie toujours « plus de contrĂŽle », comme pour les autres items du construit.

recodage.py
# Inversion de l'Ă©chelle : 1↔5, 2↔4, 3=3
mapping_inverse = {1: 5, 2: 4, 3: 3, 4: 2, 5: 1}
df_analysis['CTRL3R_S1'] = df_analysis['CTRL3R_S1'].map(mapping_inverse)
💡 C’est comme un thermomĂštre qui afficherait le froid au lieu de la chaleur : on retourne la graduation pour que toutes les mesures « montent » dans le mĂȘme sens.
Étape 0C · DĂ©finition des construits
Associer chaque item à son concept théorique

Le dictionnaire Python concepts regroupe les colonnes du questionnaire par dimension. Ce mapping sera utilisĂ© Ă  l’Ă©tape suivante (ACP) pour fabriquer un score unique par construit.

ConstruitItems du questionnaireNb
AlignementALIGN1_S1 
 ALIGN5_S15
CompétenceCOMP1_S1 
 COMP4_S14
ContrĂŽleCTRL1_S1, CTRL2_S1, CTRL3R_S1, CTRL4_S1, CTRL5_S15
IntentionINT1_S1 
 INT4_S14

Le dictionnaire item_labels fournit en parallĂšle un libellĂ© lisible pour chaque code (ex : COMP2_S1 → « Fait de bonnes suggestions de repas »). Ces libellĂ©s apparaĂźtront dans les sorties console et les graphiques.

Étape prĂ©liminaire — l’ACP

Résumer plusieurs questions en un score unique et robuste.

ACP · Score global par construit
Pourquoi ne pas simplement faire la moyenne des items ?

On pourrait calculer la moyenne brute des 5 items d’Alignement. Mais l’Analyse en Composantes Principales (ACP) fait mieux : elle attribue un poids diffĂ©rent Ă  chaque item selon sa contribution rĂ©elle Ă  la dimension commune. Un item qui « colle » parfaitement au concept pĂšsera plus lourd qu’un item un peu pĂ©riphĂ©rique. Le rĂ©sultat est un score standardisĂ© (centrĂ©-rĂ©duit) par participant, plus fiable statistiquement qu’une simple moyenne.

Cliquez ici pour en savoir plus

Le code enchaßne trois opérations pour chaque construit :

acp.py
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler

pca_df = pd.DataFrame({'GROUPE': df_analysis['GROUPE']})

for name, items in concepts.items():
    # 1. Standardisation : moyenne → 0, Ă©cart-type → 1
    scaler = StandardScaler()
    # 2. Extraction de la 1Ăšre composante principale
    pca = PCA(n_components=1)
    # 3. Projection : score unique par participant
    pca_df[name] = pca.fit_transform(
        scaler.fit_transform(
            df_analysis[items].fillna(df_analysis[items].mean())
        )
    ).flatten()

Détail des 3 opérations internes :

1. StandardScaler() — Chaque item est centrĂ© (moyenne ramenĂ©e Ă  0) et rĂ©duit (Ă©cart-type ramenĂ© Ă  1). Cela empĂȘche un item dont les notes sont naturellement plus Ă©levĂ©es ou plus dispersĂ©es de dominer le score final.

2. PCA(n_components=1) — On ne retient que la premiĂšre composante, c’est-Ă -dire la direction de variance commune maximale entre les items. L’ACP trouve la combinaison linĂ©aire qui explique le plus de variabilitĂ© : plus un item « va dans le mĂȘme sens » que les autres, plus son poids (loading) sera Ă©levĂ©.

3. fillna(mean()) — Si un participant n’a pas rĂ©pondu Ă  une question, sa valeur manquante est remplacĂ©e par la moyenne de cet item pour ne pas perdre l’ensemble de ses donnĂ©es.

💡 Imaginez mesurer la « sportivitĂ© » d’un Ă©lĂšve : plutĂŽt que la moyenne simple de course, saut et natation, l’ACP donnera plus de poids Ă  l’Ă©preuve qui diffĂ©rencie le mieux les sportifs des non-sportifs.
RĂ©sultat : Pour chaque participant, on dispose d’un score unique par construit (Alignement, CompĂ©tence, ContrĂŽle, Intention). C’est ce score, issu de l’ACP, qui entre dans l’ANOVA — pas les items individuels. Le code exĂ©cute ensuite l’ANOVA item par item en complĂ©ment (« Étape B ») pour identifier les questions spĂ©cifiques qui diffĂšrent.

Les 5 Ă©tapes de l’ANOVA

Chaque étape est exécutée automatiquement par la fonction run_full_analysis() pour chaque variable testée.

Étape 1 · Statistiques descriptives
Observer les moyennes et la dispersion avant tout test

Premier rĂ©flexe du chercheur : regarder les chiffres bruts. Le code calcule la moyenne, l’Ă©cart-type, la variance et l’effectif de chaque groupe, puis identifie automatiquement quel scĂ©nario obtient la meilleure et la plus faible note.

descriptifs.py
# Tableau récapitulatif par groupe
desc = data.groupby('GROUPE')[target].agg(
    ['mean', 'std', 'count', 'var']
).round(3)

# Identification automatique du meilleur / pire scénario
max_g = desc['mean'].idxmax()
min_g = desc['mean'].idxmin()

La mĂ©thode .agg() applique quatre fonctions d’agrĂ©gation Ă  chaque groupe en un seul appel. Le code repĂšre ensuite le meilleur et le pire scĂ©nario pour gĂ©nĂ©rer une phrase d’interprĂ©tation automatique.

Mais attention : un Ă©cart de moyennes ne prouve rien Ă  lui seul. Si le ScĂ©nario 1 obtient 3,9 et le ScĂ©nario 3 obtient 3,6, cette diffĂ©rence de 0,3 point pourrait n’ĂȘtre que du bruit statistique. Les Ă©tapes suivantes vont le vĂ©rifier.

💡 C’est comme comparer la tempĂ©rature de deux villes un jour donnĂ© : 22 °C vs 24 °C. Avant de conclure qu’une ville est « plus chaude », il faudrait vĂ©rifier que cet Ă©cart se reproduit jour aprĂšs jour.
Étape 2 · Test de Levene
Les avis sont-ils aussi dispersĂ©s d’un groupe Ă  l’autre ?

L’ANOVA classique repose sur une hypothĂšse technique : les trois groupes doivent avoir des variances Ă  peu prĂšs Ă©gales. Le test de Levene vĂ©rifie cette condition. S’il la rejette, le rĂ©sultat de l’ANOVA classique pourrait ĂȘtre faussĂ©.

levene.py
from scipy import stats

# Séparation des scores par groupe
g1 = data[data['GROUPE']==1][target]
g2 = data[data['GROUPE']==2][target]
g3 = data[data['GROUPE']==3][target]

# Test d'homogénéité des variances
levene_stat, levene_p = stats.levene(g1, g2, g3)

La fonction stats.levene() de SciPy compare la déviation médiane des observations dans chaque groupe et renvoie une statistique F accompagnée de sa p-value :

p > 0,05 → Feu vert
Les variances sont homogĂšnes. L’ANOVA classique est fiable.
p < 0,05 → Alerte
Un groupe est plus dispersé. Le code bascule vers le test de Welch (étape 3b).
💡 On compare les notes d’un examen entre 3 classes. Dans la classe A, tout le monde a entre 12 et 14 ; dans la classe C, les notes vont de 4 Ă  20. Comparer leurs moyennes serait trompeur : Levene dĂ©tecte ce dĂ©sĂ©quilibre.
Étape 3A · Table ANOVA classique
Le moment de vĂ©ritĂ© — la p-value

L’ANOVA dĂ©compose la variation totale des scores en deux sources :

Variation inter-groupes (SSbetween) — la part de variation expliquĂ©e par le changement de scĂ©nario. Plus les moyennes des 3 groupes divergent, plus cette valeur est grande.

Variation intra-groupe (SSwithin) — la part « rĂ©siduelle » due aux diffĂ©rences individuelles. C’est le bruit naturel.

F  =  MSinter ÷ MSintra  =  (SSinter / dfinter) ÷ (SSintra / dfintra)
Plus F est grand, plus le signal (effet du scénario) domine le bruit (variabilité individuelle).
anova_classique.py
from statsmodels.formula.api import ols
from statsmodels.stats.anova import anova_lm

# ModÚle linéaire : score expliqué par le facteur GROUPE
model = ols(f"{target} ~ C(GROUPE)", data=data).fit()
anova_tab = anova_lm(model, typ=2)

# Extraction des composantes de la table
ss_bet = anova_tab.loc['C(GROUPE)', 'sum_sq']   # SS inter-groupes
ss_err = anova_tab.loc['Residual',  'sum_sq']   # SS intra-groupe
df_bet = anova_tab.loc['C(GROUPE)', 'df']       # ddl inter (k-1 = 2)
df_err = anova_tab.loc['Residual',  'df']       # ddl intra  (N-k)
ms_bet = ss_bet / df_bet                          # Moyenne des carrés inter
ms_err = ss_err / df_err                          # Moyenne des carrés intra
f_stat = anova_tab.loc['C(GROUPE)', 'F']
p_val  = anova_tab.loc['C(GROUPE)', 'PR(>F)']

La syntaxe C(GROUPE) indique Ă  Python de traiter GROUPE comme une variable catĂ©gorielle : les valeurs 1, 2, 3 sont des Ă©tiquettes, pas des grandeurs numĂ©riques. Le paramĂštre typ=2 demande une ANOVA de type II — avec un seul facteur, les types I, II et III donnent les mĂȘmes rĂ©sultats.

La table produite ressemble Ă  ce qu’afficherait SPSS :

SourceSSdfMSFSig.
Inter-groupesSSbet2MSbetFp
Intra-groupe (RĂ©siduel)SSerrN − 3MSerr——
p < 0,05 → Effet significatif
Moins de 5 % de chance que les écarts soient dus au hasard.
p > 0,05 → Pas d’effet
Les 3 scĂ©narios gĂ©nĂšrent la mĂȘme perception.
Étape 3B · Test de Welch (alternative robuste)
Quand les variances ne sont pas homogĂšnes

Si Levene a dĂ©clenchĂ© une alerte (p < 0,05), le code bascule sur le test de Welch. Ce test pondĂšre chaque groupe par l’inverse de sa variance : les groupes consensuels pĂšsent davantage, corrigeant le biais.

welch.py
def welch_anova_test(data, target, group_col='GROUPE'):
    k     = data[group_col].nunique()       # Nombre de groupes (3)
    n     = data.groupby(group_col)[target].count()
    means = data.groupby(group_col)[target].mean()
    vars_ = data.groupby(group_col)[target].var(ddof=1)

    # Poids = effectif / variance → groupes consensuels pùsent plus
    weights = n / vars_
    sum_weights = weights.sum()
    grand_mean_welch = (weights * means).sum() / sum_weights

    # Statistique F de Welch
    num = (weights * (means - grand_mean_welch)**2).sum() / (k - 1)
    lam = ((1 - weights / sum_weights)**2 / (n - 1)).sum()
    den = 1 + (2 * (k - 2) / (k**2 - 1)) * lam

    f_welch = num / den
    df1 = k - 1
    # Degrés de liberté ajustés (souvent non entiers)
    df2 = (k**2 - 1) / (3 * lam)

    p_val = stats.f.sf(f_welch, df1, df2)
    return f_welch, p_val

Points clés :

— weights = n / vars_ : poids Ă©levĂ© pour les groupes homogĂšnes, faible pour les groupes trĂšs dispersĂ©s.

— grand_mean_welch : grande moyenne recalculĂ©e avec ces pondĂ©rations.

— df2 : degrĂ©s de libertĂ© ajustĂ©s par Welch-Satterthwaite, gĂ©nĂ©ralement non entiers (ex : 45,7), rendant le test plus conservateur.

⚠ Post-hoc : le code prĂ©vient que lorsque Welch est utilisĂ©, le test de Tukey (variances Ă©gales) n’est qu’indicatif. Le test de Games-Howell serait thĂ©oriquement prĂ©fĂ©rable.
Étape 4 · Tailles d’effet
Significatif ne veut pas dire « important »

Une p-value < 0,05 certifie l’existence d’un effet mais n’en mesure pas la force. Avec un trĂšs grand Ă©chantillon, mĂȘme un Ă©cart infime devient significatif. Les tailles d’effet quantifient la puissance pratique.

effet.py
def calculate_eta_omega(ss_bet, ss_err, df_bet, ms_err):
    ss_tot = ss_bet + ss_err

    # ηÂČ â€” proportion brute de variance expliquĂ©e
    eta2 = ss_bet / ss_tot

    # ωÂČ â€” estimation corrigĂ©e (moins biaisĂ©e)
    omega2 = (ss_bet - (df_bet * ms_err)) / (ss_tot + ms_err)

    return eta2, omega2
ηÂČ  =  SSinter ÷ SStotal
Part de la variance totale expliquée par le facteur scénario.

ηÂČ (Eta-carrĂ©) est intuitif mais surestime l’effet. ωÂČ (Omega-carrĂ©) corrige ce biais en retirant la part de hasard attendue (df_bet × ms_err). Si ωÂČ tombe sous zĂ©ro, max(0, omega2) le force Ă  0.

ηÂČ < 0,06
Effet faible
0,06 ≀ ηÂČ < 0,14
Effet modéré
ηÂČ â‰„ 0,14
Effet fort
Lecture concrĂšte : Si ηÂČ = 0,08, cela signifie que 8 % des diffĂ©rences de perception entre participants s’expliquent par le scĂ©nario. Les 92 % restants proviennent d’autres facteurs (habitudes, Ăąge, humeur
).
Étape 5 · Comparaisons post-hoc (Tukey HSD)
Quel scénario bat quel scénario ?

L’ANOVA dit « au moins un groupe diffĂšre ». Le test de Tukey HSD compare chaque paire en contrĂŽlant le risque d’erreurs multiples. Avec 3 groupes : 1 vs 2, 1 vs 3, 2 vs 3.

posthoc.py
from statsmodels.stats.multicomp import pairwise_tukeyhsd

# Uniquement si l'ANOVA (ou Welch) est significative
if reported_p < 0.05:
    tukey = pairwise_tukeyhsd(data[target], data['GROUPE'])
    print(tukey)
else:
    print("Non nécessaire.")

Lecture de la sortie :

ColonneSignification
group1 / group2Les deux scénarios comparés
meandiffDiffĂ©rence des moyennes (group2 − group1)
p-adjp-value ajustée pour comparaisons multiples
lower / upperIntervalle de confiance Ă  95 %
rejectTrue = significatif ; False = non
Lecture : Si « 1 vs 3 » affiche reject = True, la perception diffÚre entre ces scénarios. meandiff positif = Scénario 3 mieux noté ; négatif = Scénario 1.
Logique conditionnelle : Tukey n’est lancĂ© que si p < 0,05 globalement. Sinon, le code affiche « Non nĂ©cessaire » pour Ă©viter les faux positifs.

La double boucle d’exĂ©cution

Le pipeline complet est appliqué deux fois, à deux niveaux de granularité.

Architecture du script
Étape A (globale) puis Étape B (dĂ©taillĂ©e)

La fonction run_full_analysis() encapsule les 5 Ă©tapes. Le script l’appelle dans deux boucles :

Étape A — Scores ACP (4 analyses) : appliquĂ©e aux 4 scores globaux. « Le scĂ©nario influence-t-il la perception globale de chaque dimension ? »

Étape B — Items individuels (18 analyses) : appliquĂ©e aux 18 items bruts. « Sur quelle question prĂ©cise le scĂ©nario fait-il bouger les rĂ©ponses ? » L’Alignement global peut sembler stable, mais « L’agent serait honnĂȘte sur les limites des plats » pourrait diverger.

execution.py
all_results = []

# ── ÉTAPE A : 4 construits globaux (scores ACP) ──
for name, items in concepts.items():
    res = run_full_analysis(pca_df, name, f"Concept Global : {name}")
    all_results.append(res)

# ── ÉTAPE B : 18 items individuels ──
for code, label in item_labels.items():
    res = run_full_analysis(df_analysis, code, label)
    all_results.append(res)

Chaque appel renvoie un dictionnaire (F, p, ηÂČ, ωÂČ, test utilisĂ©, Levene p), collectĂ© dans all_results pour le tableau de synthĂšse.

SynthÚse finale automatisée

Un tableau récapitulatif et un verdict en langage clair.

Tableau de synthĂšse
Tous les rĂ©sultats en un coup d’Ɠil
synthese.py
summary_final = pd.DataFrame(all_results)

print(summary_final[[
    'Variable',       # Construit ou item
    'Test_Appliqué',  # "ANOVA" ou "Welch"
    'F',               # Statistique F
    'p',               # p-value
    'Eta2',            # Taille d'effet ηÂČ
    'Omega2',          # Taille d'effet ωÂČ
    'Levene_p'         # p du test de Levene
]].to_string(index=False))

Ce tableau permet d’identifier d’un seul regard quelles variables ont un p < 0,05, quel test a Ă©tĂ© retenu, et la magnitude de chaque effet.

Verdict automatique
Conclusion en langage naturel
conclusion.py
sig_found = summary_final[summary_final['p'] < 0.05]

if sig_found.empty:
    print("StabilitĂ© confirmĂ©e : mĂȘme perception.")
else:
    print(f"Divergence : {len(sig_found)} variable(s) significative(s).")
Aucune variable significative
« StabilitĂ© confirmĂ©e » — l’agent IA est perçu de la mĂȘme maniĂšre quel que soit le scĂ©nario.
Au moins une variable significative
« Divergence dĂ©tectĂ©e » — le scĂ©nario a modifiĂ© l’opinion. Les tests post-hoc identifient lesquels.
Bonus · Visualisations
Boxplots superposĂ©s d’un nuage de points

Pour chaque variable, un boxplot (mĂ©diane, quartiles, extrĂȘmes) combinĂ© Ă  un swarmplot (chaque point = un participant) permet de voir la tendance centrale, la dispersion et la distribution rĂ©elle des rĂ©ponses.

graphiques.py
import seaborn as sns
import matplotlib.pyplot as plt

plt.figure(figsize=(7, 4))
sns.boxplot(x='GROUPE', y=target, data=data,
           hue='GROUPE', palette='Set2', legend=False)
sns.swarmplot(x='GROUPE', y=target, data=data,
              color=".25", alpha=0.5, size=4)
plt.title(f"Répartition : {label}")
plt.show()
💡 Le boxplot montre la « forĂȘt » (tendance gĂ©nĂ©rale), le swarmplot les « arbres » (chaque individu). Si les boĂźtes se chevauchent largement, l’ANOVA ne trouvera probablement rien de significatif.

Code Python complet

Le script intĂ©gral, prĂȘt Ă  exĂ©cuter dans Google Colab ou Jupyter Notebook.

analyse_anova_complete.py
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
import warnings
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
import statsmodels.api as sm
from statsmodels.formula.api import ols
from statsmodels.stats.anova import anova_lm
from statsmodels.stats.multicomp import pairwise_tukeyhsd
from scipy import stats

# 1. CONFIGURATION ET NETTOYAGE
warnings.simplefilter(action='ignore', category=FutureWarning)
warnings.simplefilter(action='ignore', category=UserWarning)
pd.options.mode.chained_assignment = None

# 2. CHARGEMENT ET RECODAGE
file_path = '/content/RC_AGENT.xlsx'
df = pd.read_excel(file_path, sheet_name='BASE')
df_analysis = df[df['GROUPE'].isin([1, 2, 3])].copy()

mapping_inverse = {1: 5, 2: 4, 3: 3, 4: 2, 5: 1}
df_analysis['CTRL3R_S1'] = df_analysis['CTRL3R_S1'].map(mapping_inverse)

item_labels = {
    'ALIGN1_S1': "Agit dans mon meilleur intĂ©rĂȘt",
    'ALIGN2_S1': "PrivilĂ©giera mes intĂ©rĂȘts vs restaurant",
    'ALIGN3_S1': "Ne poussera pas Ă  un choix avantageux resto",
    'ALIGN4_S1': "Serait honnĂȘte sur les limites des plats",
    'ALIGN5_S1': "S'assurera que la commande me convient",
    'COMP1_S1': "Comprend mes préférences alimentaires",
    'COMP2_S1': "Fait de bonnes suggestions de repas",
    'COMP3_S1': "Est fiable pour gérer la commande",
    'COMP4_S1': "Est compétent pour choisir un repas",
    'CTRL1_S1': "Sentiment de garder le contrÎle (préférences)",
    'CTRL2_S1': "ContrÎle via ajustement des préférences",
    'CTRL3R_S1': "Perception de contrÎle personnel (Inversé)",
    'CTRL4_S1': "Facilité à refuser/annuler",
    'CTRL5_S1': "Décision d'activer dépend de moi",
    'INT1_S1': "Prévoit de laisser l'agent agir en mon nom",
    'INT2_S1': "Compte laisser l'agent gérer la commande",
    'INT3_S1': "PrĂȘt Ă  activer mode sans validation",
    'INT4_S1': "Continuerait Ă  utiliser ce mode Ă  l'avenir"
}

concepts = {
    'Alignement': ['ALIGN1_S1','ALIGN2_S1','ALIGN3_S1','ALIGN4_S1','ALIGN5_S1'],
    'Compétence': ['COMP1_S1','COMP2_S1','COMP3_S1','COMP4_S1'],
    'ContrĂŽle':   ['CTRL1_S1','CTRL2_S1','CTRL3R_S1','CTRL4_S1','CTRL5_S1'],
    'Intention':  ['INT1_S1','INT2_S1','INT3_S1','INT4_S1']
}

def calculate_eta_omega(ss_bet, ss_err, df_bet, ms_err):
    ss_tot = ss_bet + ss_err
    eta2 = ss_bet / ss_tot
    omega2 = (ss_bet - (df_bet * ms_err)) / (ss_tot + ms_err)
    return eta2, omega2

def welch_anova_test(data, target, group_col='GROUPE'):
    k = data[group_col].nunique()
    n = data.groupby(group_col)[target].count()
    means = data.groupby(group_col)[target].mean()
    vars_ = data.groupby(group_col)[target].var(ddof=1)
    weights = n / vars_
    sum_weights = weights.sum()
    grand_mean_welch = (weights * means).sum() / sum_weights
    num = (weights * (means - grand_mean_welch)**2).sum() / (k - 1)
    lam = ((1 - weights / sum_weights)**2 / (n - 1)).sum()
    den = 1 + (2 * (k - 2) / (k**2 - 1)) * lam
    f_welch = num / den
    df1 = k - 1
    df2 = (k**2 - 1) / (3 * lam)
    p_val = stats.f.sf(f_welch, df1, df2)
    return f_welch, p_val

def run_full_analysis(data, target, label):
    print(f"\n{'#'*70}\nANALYSE : {label}\n{'#'*70}")
    desc = data.groupby('GROUPE')[target].agg(['mean','std','count','var']).round(3)
    print("\n[1] DESCRIPTIFS :")
    print(desc)
    max_g = desc['mean'].idxmax()
    min_g = desc['mean'].idxmin()
    print(f"\n💡 ScĂ©nario {max_g} = meilleure moyenne ({desc.loc[max_g,'mean']}), "
          f"Scénario {min_g} = plus faible ({desc.loc[min_g,'mean']}).")

    g1 = data[data['GROUPE']==1][target]
    g2 = data[data['GROUPE']==2][target]
    g3 = data[data['GROUPE']==3][target]
    levene_stat, levene_p = stats.levene(g1, g2, g3)
    print(f"\n[2] LEVENE : F={levene_stat:.3f}, p={levene_p:.4f}")
    if levene_p > 0.05:
        print("→ Variances homogùnes. ANOVA classique OK.")
    else:
        print("→ Variances hĂ©tĂ©rogĂšnes. Bascule vers Welch.")

    model = ols(f"{target} ~ C(GROUPE)", data=data).fit()
    anova_tab = anova_lm(model, typ=2)
    ss_bet = anova_tab.loc['C(GROUPE)','sum_sq']
    ss_err = anova_tab.loc['Residual','sum_sq']
    df_bet = anova_tab.loc['C(GROUPE)','df']
    df_err = anova_tab.loc['Residual','df']
    ms_bet = ss_bet / df_bet
    ms_err = ss_err / df_err
    f_stat = anova_tab.loc['C(GROUPE)','F']
    p_val = anova_tab.loc['C(GROUPE)','PR(>F)']
    eta2, omega2 = calculate_eta_omega(ss_bet, ss_err, df_bet, ms_err)

    print(f"\n[3a] TABLE ANOVA :")
    print(f"{'Source':<12} | {'SS':<10} | {'df':<4} | {'MS':<10} | {'F':<8} | {'Sig.':<8}")
    print(f"{'Inter-Group':<12} | {ss_bet:<10.3f} | {df_bet:<4.0f} | {ms_bet:<10.3f} | {f_stat:<8.3f} | {p_val:<8.4f}")
    print(f"{'Intra-Group':<12} | {ss_err:<10.3f} | {df_err:<4.0f} | {ms_err:<10.3f} |")

    if levene_p < 0.05:
        print("\n[3b] ⚠ TEST DE WELCH :")
        f_welch, p_welch = welch_anova_test(data, target, 'GROUPE')
        print(f"F(Welch)={f_welch:.3f}, p={p_welch:.4f}")
        reported_f, reported_p, test_used = f_welch, p_welch, "Welch"
    else:
        reported_f, reported_p, test_used = f_stat, p_val, "ANOVA"

    if reported_p < 0.05:
        print(f"\n💡 [{test_used}] p < 0,05 → Effet significatif.")
    else:
        print(f"\n💡 [{test_used}] p > 0,05 → Pas d'effet.")

    print(f"\n[4] ηÂČ={eta2:.4f}, ωÂČ={max(0,omega2):.4f}")
    mag = "faible" if eta2<0.06 else "modérée" if eta2<0.14 else "forte"
    print(f"→ Effet {mag} ({eta2*100:.1f}% de variance expliquĂ©e).")

    if reported_p < 0.05:
        print("\n[5] TUKEY HSD :")
        if test_used == "Welch":
            print("(Games-Howell serait préférable)")
        tukey = pairwise_tukeyhsd(data[target], data['GROUPE'])
        print(tukey)
    else:
        print("\n[5] Post-hoc non nécessaire.")

    plt.figure(figsize=(7, 4))
    sns.boxplot(x='GROUPE', y=target, data=data,
                hue='GROUPE', palette='Set2', legend=False)
    sns.swarmplot(x='GROUPE', y=target, data=data,
                  color=".25", alpha=0.5, size=4)
    plt.title(f"Répartition : {label}")
    plt.xlabel("Scénario"); plt.ylabel("Score")
    plt.show()

    return {'Variable': label, 'Test_Appliqué': test_used,
            'F': round(reported_f,3), 'p': round(reported_p,4),
            'Eta2': round(eta2,4), 'Omega2': round(max(0,omega2),4),
            'Levene_p': round(levene_p,4)}

all_results = []

print("=" * 70)
print("ÉTAPE A : CONCEPTS GLOBAUX (ACP)")
print("=" * 70)
pca_df = pd.DataFrame({'GROUPE': df_analysis['GROUPE']})
for name, items in concepts.items():
    scaler = StandardScaler()
    pca = PCA(n_components=1)
    pca_df[name] = pca.fit_transform(
        scaler.fit_transform(
            df_analysis[items].fillna(df_analysis[items].mean())
        )).flatten()
    all_results.append(run_full_analysis(pca_df, name, f"Global : {name}"))

print("\n\n" + "=" * 70)
print("ÉTAPE B : ITEMS INDIVIDUELS")
print("=" * 70)
for code, label in item_labels.items():
    all_results.append(run_full_analysis(df_analysis, code, label))

summary_final = pd.DataFrame(all_results)
print("\n" + "=" * 100)
print("TABLEAU SYNTHÉTIQUE FINAL")
print("=" * 100)
print(summary_final[['Variable','Test_Appliqué','F','p',
                      'Eta2','Omega2','Levene_p']].to_string(index=False))

sig = summary_final[summary_final['p'] < 0.05]
if sig.empty:
    print("\n→ StabilitĂ© : aucune diffĂ©rence significative.")
else:
    print(f"\n→ {len(sig)} variable(s) significative(s). Voir tests post-hoc.")
Guide mĂ©thodologique — Étude Agent IA & Commande de repas — ANOVA & Python