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
- Chargement & recodage des données
- Analyse en Composantes Principales (ACP)
- Statistiques descriptives
- Test de Levene (homogénéité)
- Table ANOVA & test de Welch
- Tailles d’effet (ηÂČ et ÏÂČ)
- Comparaisons post-hoc (Tukey)
- Double boucle & synthĂšse finale
đ 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.
Chargement et préparation des données
Le socle de toute analyse : importer, filtrer et recoder.
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.
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()
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.
# 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)
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.
| Construit | Items du questionnaire | Nb |
|---|---|---|
| Alignement | ALIGN1_S1 ⊠ALIGN5_S1 | 5 |
| Compétence | COMP1_S1 ⊠COMP4_S1 | 4 |
| ContrĂŽle | CTRL1_S1, CTRL2_S1, CTRL3R_S1, CTRL4_S1, CTRL5_S1 | 5 |
| Intention | INT1_S1 ⊠INT4_S1 | 4 |
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.
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 plusLe code enchaßne trois opérations pour chaque construit :
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.
Les 5 Ă©tapes de l’ANOVA
Chaque étape est exécutée automatiquement par la fonction run_full_analysis() pour chaque variable testée.
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.
# 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.
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Ă©.
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 :
Les variances sont homogĂšnes. L’ANOVA classique est fiable.
Un groupe est plus dispersé. Le code bascule vers le test de Welch (étape 3b).
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.
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 :
| Source | SS | df | MS | F | Sig. |
|---|---|---|---|---|---|
| Inter-groupes | SSbet | 2 | MSbet | F | p |
| Intra-groupe (RĂ©siduel) | SSerr | N â 3 | MSerr | â | â |
Moins de 5 % de chance que les écarts soient dus au hasard.
Les 3 scĂ©narios gĂ©nĂšrent la mĂȘme perception.
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.
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.
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.
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
ηÂČ (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.
Effet faible
Effet modéré
Effet fort
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.
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 :
| Colonne | Signification |
|---|---|
group1 / group2 | Les deux scénarios comparés |
meandiff | DiffĂ©rence des moyennes (group2 â group1) |
p-adj | p-value ajustée pour comparaisons multiples |
lower / upper | Intervalle de confiance Ă 95 % |
reject | True = significatif ; False = non |
La double boucle d’exĂ©cution
Le pipeline complet est appliqué deux fois, à deux niveaux de granularité.
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.
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.
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.
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).")
« StabilitĂ© confirmĂ©e » â l’agent IA est perçu de la mĂȘme maniĂšre quel que soit le scĂ©nario.
« Divergence dĂ©tectĂ©e » â le scĂ©nario a modifiĂ© l’opinion. Les tests post-hoc identifient lesquels.
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.
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()
Code Python complet
Le script intĂ©gral, prĂȘt Ă exĂ©cuter dans Google Colab ou Jupyter Notebook.
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.")