Pandas & NumPy Academy · Lezione

ANOVA e test post-hoc

Confronti le medie di tre o più gruppi con un'ANOVA a una via ed esegua il test HSD di Tukey per identificare le coppie differenti.

Lezione 4 di 413 passaggi

ANOVA e test post-hoc è una lezione Pandas & NumPy Academy gratuita su CoddyKit. Questa è la lezione 4 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento Pandas & NumPy Academy, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso Pandas & NumPy Academy include 4 lezioni in totale.

Perché non usare più t-test?

Quando si confrontano le medie di tre o più gruppi, eseguire più test t aumenta il tasso di errore di tipo I (tasso di falsi positivi). Testare i gruppi A vs B, A vs C e B vs C con α=0,05 per ciascun confronto porta a una probabilità complessiva di falso positivo di circa il 14%, non del 5%. La Analysis of Variance (ANOVA) risolve il problema testando simultaneamente tutti i gruppi con un unico test e mantenendo il tasso di falsi positivi esattamente pari ad α. Una ANOVA sostituisce tutti i test t a coppie per la domanda omnibus: «I gruppi differiscono tra loro?»

ANOVA a una via: il quadro generale

La ANOVA a una via verifica se le medie di tre o più gruppi differiscono in modo significativo. Scompone la varianza totale in varianza tra i gruppi (spiegata dall'appartenenza al gruppo) e varianza entro i gruppi (rumore casuale). La statistica F è il loro rapporto: F = (varianza tra i gruppi) / (varianza entro i gruppi). Un valore elevato di F indica che le differenze tra i gruppi sono grandi rispetto al rumore e produce un p-value piccolo. L'ipotesi nulla è H₀: tutte le medie dei gruppi sono uguali.

import numpy as np
from scipy import stats

np.random.seed(0)
# Three product variants with different conversion rates
group_a = np.random.normal(10.0, 2.0, 50)  # baseline
group_b = np.random.normal(11.5, 2.0, 50)  # slightly better
group_c = np.random.normal(13.0, 2.0, 50)  # clearly better

f_stat, p = stats.f_oneway(group_a, group_b, group_c)
print(f'F-statistic: {f_stat:.3f}')
print(f'p-value: {p:.6f}')
print('At least one group differs?', 'Yes' if p < 0.05 else 'No')

ANOVA con DataFrame Pandas

Nella pratica, i dati si trovano in un DataFrame in formato long, in cui una colonna contiene l'etichetta del gruppo e un'altra contiene la misurazione. Estragga ciascun gruppo come Series e lo passi a stats.f_oneway(). In alternativa, se dispone di un DataFrame in formato wide (una colonna per ogni gruppo), passi direttamente ciascuna colonna. La funzione accetta un numero qualsiasi di argomenti posizionali, facilitando l'estensione a 4, 5 o più gruppi.

import pandas as pd
import numpy as np
from scipy import stats

np.random.seed(1)
df = pd.DataFrame({
    'group': ['A']*40 + ['B']*40 + ['C']*40 + ['D']*40,
    'score': np.concatenate([
        np.random.normal(70, 10, 40),
        np.random.normal(75, 10, 40),
        np.random.normal(80, 10, 40),
        np.random.normal(72, 10, 40)
    ])
})

groups = [group['score'].values
          for _, group in df.groupby('group')]

f, p = stats.f_oneway(*groups)
print(f'F={f:.3f}, p={p:.4f}')
print(df.groupby('group')['score'].mean().round(2))

Ipotesi dell'ANOVA

L'ANOVA a una via presuppone: (1) Indipendenza — le osservazioni sono indipendenti l'una dall'altra; (2) Normalità — i residui sono approssimativamente normali all'interno di ciascun gruppo (l'ANOVA è robusta per campioni grandi grazie al TLC); (3) Omoscedasticità — le varianze sono uguali tra i gruppi. Verifichi l'ipotesi sulle varianze con il test di Levene (stats.levene(*groups)). Se le varianze non sono uguali, utilizzi l'ANOVA di Welch, disponibile nella libreria pingouin o calcolabile con statsmodels.

import numpy as np
from scipy import stats

np.random.seed(0)
g1 = np.random.normal(10, 2, 40)
g2 = np.random.normal(12, 2, 40)
g3 = np.random.normal(11, 8, 40)   # Much higher variance

# Test equal variances
stat_l, p_l = stats.levene(g1, g2, g3)
print(f'Levene\'s test: stat={stat_l:.3f}, p={p_l:.4f}')
if p_l < 0.05:
    print('Warning: Unequal variances! Standard ANOVA may be unreliable.')
    print('Consider Welch\'s ANOVA or Kruskal-Wallis.')

Kruskal-Wallis: ANOVA non parametrica

Quando le ipotesi dell'ANOVA non sono rispettate (distribuzioni non normali, campioni piccoli, varianze diverse), il test di Kruskal-Wallis è l'alternativa non parametrica. Verifica se le distribuzioni dei gruppi differiscono convertendo i dati in ranghi e confrontando le somme dei ranghi. Utilizzi scipy.stats.kruskal(*groups). L'ipotesi nulla è che tutti i gruppi abbiano la stessa distribuzione (equivalente a verificare l'uguaglianza delle mediane quando le distribuzioni hanno la stessa forma). È sempre valido, ma meno potente dell'ANOVA quando le ipotesi sono rispettate.

import numpy as np
from scipy import stats

np.random.seed(0)
# Non-normal data: customer satisfaction scores (1-10 scale)
g1 = np.random.choice(range(1, 11), 50, p=[0.05]*10)
g2 = np.random.choice(range(1, 11), 50, p=[0.02, 0.03, 0.05, 0.08, 0.12, 0.15, 0.20, 0.15, 0.12, 0.08])
g3 = np.random.choice(range(1, 11), 50, p=[0.15, 0.15, 0.15, 0.15, 0.10, 0.10, 0.08, 0.05, 0.04, 0.03])

stat, p = stats.kruskal(g1, g2, g3)
print(f'Kruskal-Wallis: H={stat:.3f}, p={p:.4f}')
print('Groups differ?', 'Yes' if p < 0.05 else 'No')

Test post-hoc: perché sono necessari

Un'ANOVA significativa indica che almeno una media di gruppo è diversa, ma non quali coppie differiscono. I test post-hoc eseguono tutti i confronti a coppie correggendo al contempo per i confronti multipli. Il più usato è la differenza significativa onesta di Tukey (Tukey HSD), che controlla esattamente il tasso di errore familiare al livello α. È disponibile in statsmodels.stats.multicomp.pairwise_tukeyhsd(). Esegua i test post-hoc solo dopo un'ANOVA significativa, non come ricerca esplorativa su risultati non significativi.

import numpy as np
import pandas as pd
from scipy import stats
from statsmodels.stats.multicomp import pairwise_tukeyhsd

np.random.seed(0)
data = np.concatenate([
    np.random.normal(70, 10, 40),
    np.random.normal(80, 10, 40),
    np.random.normal(75, 10, 40)
])
groups = ['A']*40 + ['B']*40 + ['C']*40

# First confirm ANOVA is significant
f, p = stats.f_oneway(*[data[i*40:(i+1)*40] for i in range(3)])
print(f'ANOVA p={p:.4f}')
if p < 0.05:
    print('Post-hoc Tukey HSD:')
    print(pairwise_tukeyhsd(data, groups, alpha=0.05))

Interpretare l'output di Tukey HSD

La tabella di Tukey HSD contiene una riga per ogni coppia di gruppi. Le colonne principali sono: meandiff (differenza tra le medie della coppia), lower e upper (intervallo di confidenza al 95% per la differenza) e reject (True se la coppia differisce significativamente al livello α). Un intervallo di confidenza che non include lo zero indica che la coppia è significativamente diversa. I gruppi i cui intervalli di confidenza si sovrappongono non differiscono significativamente tra loro.

import numpy as np
from statsmodels.stats.multicomp import pairwise_tukeyhsd

np.random.seed(42)
data = np.concatenate([
    np.random.normal(10, 2, 60),  # Group A
    np.random.normal(13, 2, 60),  # Group B (clearly higher)
    np.random.normal(10.5, 2, 60) # Group C (barely different from A)
])
groups = ['A']*60 + ['B']*60 + ['C']*60

result = pairwise_tukeyhsd(data, groups, alpha=0.05)
print(result)
# A-B: reject=True (B is higher)
# A-C: reject=False (barely different)
# B-C: reject=True (B is higher)

Correzioni di Bonferroni e Benjamini-Hochberg

In alternativa al Tukey HSD, può applicare una correzione di Bonferroni ai test t a coppie: esegua tutti i test t, quindi imposti la soglia su α/k, dove k è il numero di confronti. Si tratta di un approccio conservativo, che può non rilevare differenze reali. La correzione Benjamini-Hochberg del False Discovery Rate (FDR) è meno conservativa: controlla la proporzione attesa di scoperte false, consentendo più veri positivi al costo di un numero leggermente maggiore di falsi positivi. Utilizzi statsmodels.stats.multitest.multipletests(p_values, method='fdr_bh').

import numpy as np
from scipy import stats
from statsmodels.stats.multitest import multipletests

np.random.seed(0)
groups = [np.random.normal(10 + i*1.5, 2, 40) for i in range(4)]
names = ['A', 'B', 'C', 'D']

# All pairwise t-tests
p_values = []
pairs = []
for i in range(len(groups)):
    for j in range(i+1, len(groups)):
        _, p = stats.ttest_ind(groups[i], groups[j])
        p_values.append(p)
        pairs.append(f'{names[i]}-{names[j]}')

# Benjamini-Hochberg correction
reject, adj_p, _, _ = multipletests(p_values, method='fdr_bh')
for pair, p, ap, r in zip(pairs, p_values, adj_p, reject):
    print(f'{pair}: raw_p={p:.4f}, adj_p={ap:.4f}, significant={r}')

Dimensione dell'effetto per l'ANOVA: eta quadrato

Per l'ANOVA, la misura analoga al d di Cohen è l'eta quadrato (η²): la proporzione di varianza totale spiegata dall'appartenenza al gruppo. η² = SS_between / SS_total. Valori inferiori a 0,01 indicano un effetto piccolo, 0,06 un effetto medio e superiori a 0,14 un effetto grande. L'eta quadrato parziale (η²_p) è più comune nella ricerca pubblicata: divide solo per la somma della varianza dei gruppi e della varianza dell'errore, non per la varianza totale. Calcoli sempre η² insieme al p-value dell'ANOVA per mostrare se l'effetto è rilevante dal punto di vista pratico, non solo statisticamente rilevabile.

import numpy as np
from scipy import stats

np.random.seed(0)
groups = [
    np.random.normal(10, 2, 50),
    np.random.normal(12, 2, 50),
    np.random.normal(14, 2, 50)
]

all_data = np.concatenate(groups)
grand_mean = all_data.mean()

ss_between = sum(len(g) * (g.mean() - grand_mean)**2 for g in groups)
ss_total = sum((x - grand_mean)**2 for g in groups for x in g)
eta_sq = ss_between / ss_total

f, p = stats.f_oneway(*groups)
print(f'F={f:.3f}, p={p:.4f}')
print(f'Eta-squared: {eta_sq:.4f} ({eta_sq:.1%} of variance explained)')

Panoramica dell'ANOVA a due vie

L'ANOVA a due vie estende l'ANOVA a una via a due fattori categorici simultaneamente. Per esempio, consente di verificare se i punteggi d'esame differiscono sia in base al metodo di insegnamento sia in base al livello di esperienza degli studenti. Verifica inoltre l'effetto di interazione: l'effetto del metodo di insegnamento dipende dal livello di esperienza? In statsmodels, l'ANOVA a due vie si implementa con ols('score ~ C(method) + C(level) + C(method):C(level)', data=df).fit() e anova_lm(model). Costituisce una base per progetti sperimentali più avanzati.

import pandas as pd
import numpy as np
from statsmodels.formula.api import ols
from statsmodels.stats.anova import anova_lm

np.random.seed(0)
df = pd.DataFrame({
    'method': ['trad']*60 + ['active']*60,
    'level': ['beginner']*30 + ['advanced']*30 + ['beginner']*30 + ['advanced']*30,
    'score': (np.random.normal(70, 8, 30).tolist() +
              np.random.normal(80, 8, 30).tolist() +
              np.random.normal(75, 8, 30).tolist() +
              np.random.normal(88, 8, 30).tolist())
})

model = ols('score ~ C(method) + C(level) + C(method):C(level)', data=df).fit()
print(anova_lm(model, typ=2)[['F', 'PR(>F)']].round(4))

ANOVA per misure ripetute

L'ANOVA per misure ripetute si usa quando gli stessi soggetti vengono misurati più volte, per esempio per analizzare il tempo di reazione a 0, 30 e 60 minuti da un trattamento. È la generalizzazione a più gruppi del test t per campioni appaiati. Tenendo conto delle differenze individuali, offre una potenza maggiore rispetto all'ANOVA a una via. In Python, utilizzi pingouin.rm_anova() oppure pg.pairwise_tests() per l'analisi post-hoc. Quando la sfericità non è rispettata (verificata con il test di Mauchly), utilizzi p-value corretti con Greenhouse-Geisser.

import pandas as pd
import numpy as np

# Example structure for repeated measures (pingouin library)
np.random.seed(0)
subjects = list(range(20)) * 3   # 20 subjects, 3 time points
timepoints = ['T0']*20 + ['T1']*20 + ['T2']*20
scores = np.concatenate([
    np.random.normal(50, 10, 20),   # baseline
    np.random.normal(55, 10, 20),   # improved
    np.random.normal(60, 10, 20)    # further improved
])

df_rm = pd.DataFrame({'subject': subjects, 'time': timepoints, 'score': scores})
print(df_rm.groupby('time')['score'].mean().round(2))
# Use pingouin.rm_anova(data=df_rm, dv='score', within='time', subject='subject')

Verifica rapida

Verifichi la Sua comprensione dei concetti di analisi dei dati trattati in questa lezione.

Riepilogo della lezione

In questa lezione ha imparato che scipy.stats.f_oneway() verifica se esistono differenze tra le medie dei gruppi controllando il tasso complessivo di errore di tipo I, che i test post-hoc (Tukey HSD) identificano le coppie specifiche che differiscono dopo un'ANOVA significativa e che il Kruskal-Wallis è l'alternativa non parametrica quando le ipotesi di normalità o di uguaglianza delle varianze non sono rispettate. Ora inizieremo il progetto conclusivo, integrando tutte le competenze in una pipeline completa di dati.

Gratis per iniziare

Impara Python con un tutor IA — gratis

Scrivi ed esegui vero codice nel tuo browser, ricevi aiuto istantaneo da un tutor IA disponibile 24/7, e riprendi da dove hai lasciato sul web o nell'app.

Corsi
30
Lezioni
120

Domande Frequenti

La lezione «ANOVA e test post-hoc» è gratuita?

Sì — il testo completo di «ANOVA e test post-hoc» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso Pandas & NumPy Academy, passa a CoddyKit PRO. Il corso Pandas & NumPy Academy include 4 lezioni in totale.

Cosa imparerò in «ANOVA e test post-hoc»?

Confronti le medie di tre o più gruppi con un'ANOVA a una via ed esegua il test HSD di Tukey per identificare le coppie differenti. Eserciti Pandas & NumPy Academy con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.

Ho bisogno di esperienza per iniziare Pandas & NumPy Academy?

Non è richiesta alcuna esperienza precedente. Pandas & NumPy Academy su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 4 di 4.

Quanto tempo richiede la lezione «ANOVA e test post-hoc»?

La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.

Posso scrivere ed eseguire codice in questa lezione Pandas & NumPy Academy?

Sì. Ogni lezione Pandas & NumPy Academy include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.

Tutte le lezioni di questo corso

  1. Statistiche descrittive e test di normalità
  2. T-test per confrontare le medie
  3. Test chi-quadrato di indipendenza
  4. ANOVA e test post-hoc
← Torna a Pandas & NumPy Academy