Vai al contenuto principale

Test esatto di Fisher: prendere decisioni con campioni piccoli

Impara ad analizzare le relazioni categoriali quando le dimensioni del campione sono ridotte.
· 9 min leggi

Scopri con l'IA

Le organizzazioni prendono decisioni ogni giorno in base al comportamento dei clienti, alle prestazioni dei prodotti e alle fluttuazioni operative. Per assicurarsi che queste decisioni si basino su relazioni e schemi reali nei dati, gli analisti ricorrono spesso a test statistici. 

Sebbene i test statistici più comuni funzionino bene con dataset di grandi dimensioni, le aziende non hanno sempre il lusso di disporre di molti dati. Anzi, molte decisioni devono essere prese usando campioni piccoli, segmenti di clienti di nicchia o eventi rari. In queste situazioni, i test tradizionali, come il chi-quadrato, possono non essere all’altezza, producendo risultati fuorvianti o privi della precisione necessaria per agire con sicurezza sugli insight. 

È qui che il test esatto di Fisher diventa prezioso. Continua a leggere e ti mostrerò come.

Che cos’è il test esatto di Fisher?

Il test esatto di Fisher viene utilizzato per valutare l’associazione tra variabili categoriali quando i dati sono limitati. Si applica più spesso a tabelle di contingenza 2×2 e calcola la probabilità esatta di osservare dati estremi quanto quelli raccolti, assumendo che l’ipotesi nulla di indipendenza sia vera.

Un analista potrebbe usare il test esatto di Fisher, ad esempio, per valutare l’efficacia di una campagna di marketing mirata. Se, per esempio, l’analista stesse testando l’efficacia di una campagna email rispetto a una sui social media, e questo per un gruppo di clienti di nicchia con campioni ridotti, potrebbe creare una tabella di contingenza ed eseguire il test di Fisher. Oppure, in un contesto di controllo qualità, un analista potrebbe usare il test per affrontare problemi di segnalazione degli incidenti e tracciamento dei difetti. Quando determinati tipi di guasto si verificano solo poche volte, il test esatto di Fisher può valutare se fattori come il tipo di macchina siano realmente collegati al verificarsi dei difetti.

Teoria statistica e fondamenti matematici

Dopo aver sviluppato un’intuizione sul perché il test sia importante, possiamo ora formalizzarlo. Alla base, il test esatto di Fisher è un metodo statistico usato per determinare se esiste un’associazione non casuale tra due variabili categoriali. È più comunemente applicato a tabelle di contingenza 2×2 per confrontare due gruppi rispetto a due esiti, per esempio “trattamento contro controllo” rispetto a “migliorato contro non migliorato”.

Curiosamente, le sue origini risalgono a uno degli episodi più famosi della statistica, noto come l’esperimento della “signora che assaggia il tè”. Sir Ronald A. Fisher progettò un esperimento per valutare l’affermazione della Dott.ssa Muriel Bristol secondo cui avrebbe potuto distinguere, solo dal sapore, se nel tè fosse stato versato prima il latte o il tè. Come parte dell’esperimento, Fisher preparò otto tazze e le chiese di identificare quali fossero “latte-prima” rispetto a “tè-prima”. A causa della piccola dimensione del campione, Fisher sviluppò un metodo esatto per determinare se lo schema delle risposte corrette potesse essersi verificato per caso, gettando le basi di quello che oggi conosciamo come test esatto di Fisher.

In situazioni in cui i dati sono limitati, sbilanciati o difficili da ottenere, come nella ricerca clinica, il test di Fisher aiuta a determinare se trattamenti ed esiti siano associati in modo significativo. Ecologia, epidemiologia, genetica e controllo qualità sono alcuni degli ambiti che trattano dati sparsi e si affidano a questo test.

Il test esatto di Fisher valuta l’indipendenza computando probabilità ipergeometriche esatte. Per una tabella 2×2:

 

Gruppo B1

Gruppo B2

Totale

Gruppo A1

a

b

a+b

Gruppo A2

c

d

c+d

Totale

a+c

b+d

n

La probabilità di osservare proprio quella configurazione sotto l’ipotesi nulla (nessuna associazione) si calcola con la distribuzione ipergeometrica:

I test possono essere monodirezionali, ad esempio per rispondere a “A1 è più probabile in B1 che in B2?” oppure bidirezionali: “C’è qualche associazione?”. A differenza delle approssimazioni per grandi campioni (ad es., chi-quadrato), il metodo di Fisher garantisce accuratezza anche quando le frequenze attese sono basse.

Assunzioni e prerequisiti

Per applicare il test esatto di Fisher, dobbiamo avere le seguenti condizioni.

  • I dati devono essere categoriali
  • Presentati in una tabella di contingenza 2×2
  • Le osservazioni sono indipendenti
  • I margini (totali di riga e colonna) sono fissati sotto l’ipotesi nulla

Il test di Fisher è preferibile quando una qualsiasi delle frequenze attese nella griglia è molto bassa (inferiore a 5), oppure i risultati sono sparsi o fortemente sbilanciati.

Test esatto di Fisher in Python

Supponiamo di testare se un farmaco migliora la guarigione:

 

Guarito

Non guarito

Totale

Trattamento

8

2

10

Controllo

4

11

15

Totale

12

13

25

Ecco le ipotesi:

  • H₀: Trattamento e guarigione sono indipendenti
  • H₁: Il trattamento migliora la guarigione (monodirezionale)

Ed ecco come lo considererei in Python:

from scipy.stats import fisher_exact

# Construct contingency table
table = [[8, 2],
         [4, 11]]

oddsratio, p_value = fisher_exact(table, alternative='two-sided')
print("Odds Ratio:", oddsratio)
print("Two-sided p-value:", round(p_value, 2))

# One-sided test (treatment better)
_, p_value_one = fisher_exact(table, alternative='greater')
print("One-sided p-value:", round(p_value_one, 2))
Odds Ratio: 11.0
Two-sided p-value: 0.02
One-sided p-value: 0.01

Un p-value piccolo di 0,02 suggerisce un’associazione reale tra trattamento e guarigione.

Ora, scegliamo un esempio reale usando i dati pubblici di NHANES 2017–2018 per esaminare l’associazione tra “stato di fumatore attuale” e “esserti mai stato detto che hai l’asma” negli adulti.

File utilizzati (tutti dal sito ufficiale NHANES):

  • DEMO_J.XPT — Dati demografici (età, sesso, pesi, ecc.)

  • SMQ_J.XPT — Fumo - Uso di sigarette (storia del fumo e uso attuale)

  • MCQ_J.XPT — Condizioni mediche (incluso "ti è mai stato detto che avevi l’asma")

Di seguito i passaggi che seguiremo:

  1. Scaricare e caricare i dataset

  2. Creare variabili binarie:

    1. current_smoker (sì/no)

    2. asthma_ever (sì/no)

  3. Costruire una tabella di contingenza 2×2

  4. Eseguire il test esatto di Fisher (bidirezionale e monodirezionale)

  5. Calcolare un intervallo di confidenza al 95% per l’odds ratio

  6. Interpretare i risultati

Nota: Un’analisi corretta di NHANES richiede pesi di campionamento e un disegno complesso.

Qui ignoriamo i pesi per finalità didattiche. Importiamo le librerie richieste.

# %% Imports
import pandas as pd
import numpy as np
from scipy.stats import fisher_exact
from statsmodels.stats.contingency_tables import Table2x2

Scarichiamo manualmente i file XPT dal CDC e mettiamoli in una cartella locale data/:

  • Vai a:

    • Dati demografici: Dati demografici 2017–2018 (DEMO_J)

    • Condizioni mediche: MCQ_J

    • Fumo - Uso di sigarette: SMQ_J

  • Scarica:

    • DEMO_J Data [XPT]

    • MCQ_J Data [XPT]

    • SMQ_J Data [XPT]

  • Salvali come:

    • data/DEMO_J.XPT

    • data/MCQ_J.XPT

    • data/SMQ_J.XPT

# Set paths or URLs here.
# If you've downloaded locally, point to the local paths:
DEMO_PATH = "data/DEMO_J.XPT"
MCQ_PATH  = "data/MCQ_J.XPT"
SMQ_PATH  = "data/SMQ_J.XPT"

# If you prefer to read directly from URLs, uncomment these instead:
# DEMO_PATH = "https://wwwn.cdc.gov/Nchs/NHanes/2017-2018/DEMO_J.XPT"
# MCQ_PATH  = "https://wwwn.cdc.gov/Nchs/NHanes/2017-2018/MCQ_J.XPT"
# SMQ_PATH  = "https://wwwn.cdc.gov/Nchs/NHanes/2017-2018/SMQ_J.XPT"

# Read SAS transport (.XPT) files
demo = pd.read_sas(DEMO_PATH, format="xport")
mcq  = pd.read_sas(MCQ_PATH,  format="xport")
smq  = pd.read_sas(SMQ_PATH,  format="xport")
demo.head()

mcq.head()

smq.head()

Diamo un’occhiata ad alcune variabili chiave in questi DataFrame. Qui, SEQN è l’ID del rispondente e può essere utilizzato come chiave di join.

# %% Inspect key variables

# SEQN is the respondent ID present in all three files.
# Check columns and pick what we need:
print("DEMO columns (subset):", demo.columns[:10])
print("MCQ columns (subset):", mcq.columns[:10])
print("SMQ columns (subset):", smq.columns[:10])
DEMO columns (subset): Index(['SEQN', 'SDDSRVYR', 'RIDSTATR', 'RIAGENDR', 'RIDAGEYR', 'RIDAGEMN',
       'RIDRETH1', 'RIDRETH3', 'RIDEXMON', 'RIDEXAGM'],
      dtype='object')
MCQ columns (subset): Index(['SEQN', 'MCQ010', 'MCQ025', 'MCQ035', 'MCQ040', 'MCQ050', 'AGQ030',
       'MCQ053', 'MCQ080', 'MCQ092'],
      dtype='object')
SMQ columns (subset): Index(['SEQN', 'SMQ020', 'SMD030', 'SMQ040', 'SMQ050Q', 'SMQ050U', 'SMD057',
       'SMQ078', 'SMD641', 'SMD650'],
      dtype='object')

Filtriamo le colonne richieste e applichiamo anche un filtro per età (maggiore di 20). 

# %% Keep only necessary variables and filter adults

demo_sub = demo[["SEQN", "RIDAGEYR", "RIAGENDR"]].copy()
mcq_sub  = mcq[["SEQN", "MCQ010"]].copy()
smq_sub  = smq[["SEQN", "SMQ040"]].copy()

# Merge on SEQN (inner join: keep only participants present in all three files)
df = (
    demo_sub
    .merge(mcq_sub, on="SEQN", how="inner")
    .merge(smq_sub, on="SEQN", how="inner")
)
# Restrict to adults aged 20+
df = df[df["RIDAGEYR"] >= 20].copy()
df.head()

MCQ010 rappresenta i pazienti con condizioni di asma, dove il valore 1 indica Sì e 2 indica No. Allo stesso modo, SMQ040 indica se il rispondente è un fumatore o meno. 

# %% Recode variables into binary indicators

# 1. Asthma: MCQ010
#    1 = Yes, 2 = No, 7 = Refused, 9 = Don't know
#    We'll create asthma_ever: 1 = yes, 0 = no, NaN otherwise

df["asthma_ever"] = np.nan
df.loc[df["MCQ010"] == 1, "asthma_ever"] = 1
df.loc[df["MCQ010"] == 2, "asthma_ever"] = 0

# 2. Current smoker: SMQ040
#    1 = Every day, 2 = Some days, 3 = Not at all, 7/9 = missing
#    We'll create current_smoker: 1 = yes (1 or 2), 0 = no (3), NaN otherwise

df["current_smoker"] = np.nan
df.loc[df["SMQ040"].isin([1, 2]), "current_smoker"] = 1
df.loc[df["SMQ040"] == 3, "current_smoker"] = 0

# Drop rows with missing recodes
df_clean = df.dropna(subset=["asthma_ever", "current_smoker"]).copy()
df_clean[["RIDAGEYR", "MCQ010", "asthma_ever", "SMQ040", "current_smoker"]].head()

Creiamo nuove colonne, asthma_ever e current_smoker

Creiamo la tabella di contingenza per poter usare il test esatto di Fisher.

# %% Build a 2×2 contingency table

# Ensure ints
df_clean["asthma_ever"] = df_clean["asthma_ever"].astype(int)
df_clean["current_smoker"] = df_clean["current_smoker"].astype(int)

contingency = pd.crosstab(
    df_clean["current_smoker"],
    df_clean["asthma_ever"],
    rownames=["Current smoker (1=yes,0=no)"],
    colnames=["Ever told asthma (1=yes,0=no)"]
)
contingency

Convertiamo la tabella di contingenza in un array NumPy.

# %% Fisher's Exact Test

# Convert to 2x2 numpy array with explicit ordering:
# rows: [non-smoker, smoker] ; cols: [no asthma, asthma]
table = np.array([
    [contingency.loc[0, 0], contingency.loc[0, 1]],
    [contingency.loc[1, 0], contingency.loc[1, 1]]
])

table
array([[1102,  226],
       [ 832,  172]])

Infine, eseguiamo i test esatti di Fisher bidirezionale e monodirezionale.

# Run Fisher's exact test (two-sided and one-sided)
oddsratio, p_two_sided = fisher_exact(table, alternative="two-sided")
_, p_smoker_higher_asthma = fisher_exact(table, alternative="greater")
_, p_smoker_lower_asthma  = fisher_exact(table, alternative="less")

print("2x2 table (rows: non-smoker, smoker; cols: no asthma, asthma)")
print(table)
print()
print(f"Odds ratio (Fisher): {oddsratio:.3f}")
print(f"Two-sided p-value: {p_two_sided:.5f}")
print(f"One-sided p-value (H1: smokers have higher asthma odds): {p_smoker_higher_asthma:.5f}")
print(f"One-sided p-value (H1: smokers have lower asthma odds): {p_smoker_lower_asthma:.5f}")
2x2 table (rows: non-smoker, smoker; cols: no asthma, asthma)
[[1102  226]
 [ 832  172]]

Odds ratio (Fisher): 1.008
Two-sided p-value: 0.95571
One-sided p-value (H1: smokers have higher asthma odds): 0.49274
One-sided p-value (H1: smokers have lower asthma odds): 0.55144

P-value elevati mostrano che non ci sono evidenze di un’associazione statisticamente significativa tra fumo attuale e aver ricevuto una diagnosi di asma in questo campione.

Un odds ratio ~1 indica che i fumatori attuali hanno praticamente le stesse probabilità di riportare una diagnosi di asma dei non fumatori. Qualsiasi differenza osservata rientra ampiamente in ciò che potrebbe verificarsi per caso.

Confronto con metodi statistici alternativi

Diversi test statistici vengono usati per analizzare le relazioni nelle tabelle di contingenza a seconda della dimensione del campione, della struttura dei dati e delle assunzioni:

Test esatto di Fisher

Ideale per campioni piccoli e quando alcune frequenze attese sono basse. Fornisce p-value esatti, risultando molto affidabile, ma può diventare computazionalmente lento con tabelle grandi.

Test chi-quadrato

La scelta standard per campioni grandi. È veloce, diffuso e facile da applicare, ma diventa impreciso quando le frequenze attese scendono sotto 5 a causa dell’uso di approssimazioni.

Test esatto di Barnard

Un’alternativa più potente al test di Fisher nelle tabelle 2×2. Non assume margini fissi, il che può rendere i risultati più sensibili, ma il metodo è meno diffuso nei software.

Test di McNemar

Progettato per dati categoriali appaiati o appaiati per soggetto (ad es., prima vs dopo il trattamento sugli stessi soggetti). Tiene conto della dipendenza tra osservazioni, ma non può essere usato quando i dati sono indipendenti.

Metodo

Quando usarlo

Punti di forza

Limitazioni

Test esatto di Fisher

Campioni piccoli, frequenze attese basse

P-value esatti

Conservativo, lento per tabelle grandi

Test chi-quadrato

Campioni di grandi dimensioni

Veloce, ampiamente utilizzato

Distorto quando le frequenze attese < 5

Test esatto di Barnard

Alternativa a Fisher, maggiore potenza

Non vincolato da margini fissi

Meno comunemente disponibile

Test di McNemar

Dati categoriali appaiati

Gestisce le dipendenze

Non per campioni indipendenti

Considerazioni computazionali

Le prestazioni possono rappresentare una criticità con il test esatto di Fisher, poiché i calcoli scalano male oltre le semplici tabelle di contingenza 2×2. Sebbene l’estensione di Freeman–Halton consenta di applicare il test a tabelle 2×3 e 3×3, il costo computazionale cresce rapidamente all’aumentare della dimensione della tabella. Di conseguenza, algoritmi ed ottimizzazioni efficienti sono particolarmente importanti in campi come la genetica e la ricerca clinica, dove i test esatti potrebbero dover essere eseguiti ripetutamente su molti dataset piccoli ma complessi.

Interpretazione e reporting statistico

Quando interpreti e riporti i risultati del test esatto di Fisher, è importante includere il p-value esatto, fornire l’odds ratio insieme a un intervallo di confidenza al 95% e indicare chiaramente se è stato eseguito un test mono- o bidirezionale. I risultati dovrebbero inoltre essere contestualizzati per aiutare i lettori a comprenderne la rilevanza pratica. Per esempio, si potrebbe scrivere: “Il test esatto di Fisher ha mostrato un’associazione significativa tra trattamento e guarigione (OR=11,0, p=0,02, a due code).”

Estensioni e applicazioni avanzate

Le estensioni e le applicazioni avanzate del test esatto di Fisher aiutano ad affrontare scenari di dati più complessi e a migliorare l’accuratezza quando si lavora con dataset piccoli o sparsi. L’estensione di Freeman–Halton consente di applicare il test a tabelle di contingenza più grandi oltre il formato standard 2×2, come 2×3 o 3×3. Il test exact mid-P offre un’alternativa meno conservativa al test di Fisher, migliorando la potenza statistica pur mantenendo l’affidabilità. I test esatti bayesiani incorporano conoscenze pregresse nell’analisi, risultando particolarmente utili quando i dati sono limitati o quando il giudizio degli esperti può guidare l’inferenza. Inoltre, i metodi di correzione del bias per gli odds ratio forniscono stime dell’effetto più accurate in studi con campioni piccoli o conteggi pari a zero, garantendo una migliore interpretazione dell’intensità e della direzione delle associazioni.

Limitazioni e considerazioni metodologiche

Il test esatto di Fisher può essere conservativo, il che può ridurre la potenza statistica e rendere più difficile rilevare effetti reali, soprattutto in studi piccoli. Quando i dati sono molto sparsi, l’interpretazione dei risultati può essere complessa perché gli intervalli di confidenza si allargano e le stime diventano instabili. Inoltre, applicare ripetutamente il test su molte variabili aumenta il rischio di false scoperte, quindi è importante un attento controllo della molteplicità.

Visualizzare le tabelle di contingenza

Buone opzioni di visualizzazione per presentare i risultati delle tabelle di contingenza includono barre impilate, barre affiancate e mosaic plot. Queste visualizzazioni aiutano a comunicare chiaramente la relazione tra variabili categoriali e rendono più semplici da interpretare schemi o differenze tra gruppi.

Ecco un esempio in Python:

import seaborn as sns
import matplotlib.pyplot as plt
import pandas as pd

df = pd.DataFrame({
    "Outcome": ["Recovered", "Not Recovered"] * 2,
    "Count": [8, 2, 4, 11],
    "Group": ["Treatment"] * 2 + ["Control"] * 2
})

sns.barplot(data=df, x="Outcome", y="Count", hue="Group")
plt.title("Treatment Outcomes")
plt.show()

Conclusione

Il test esatto di Fisher resta uno standard di riferimento per l’analisi di piccoli dataset categoriali. Nato da una tazzina di tè in un giardino, è diventato uno strumento importante e ampiamente usato in medicina, genetica, ricerca su soggetti umani e sperimentazione in ambito business.

Iscriviti al percorso di competenze Fondamenti di statistica in Python per continuare a imparare: oltre ai test di ipotesi, ti aiuterà a fare pratica con la costruzione di modelli statistici con regressione lineare e logistica.

Vidhi Chugh

Autore

Vidhi Chugh

Sono una stratega ed eticista dell’AI che lavora all’intersezione tra data science, prodotto e ingegneria per creare sistemi di machine learning scalabili. Inserita tra i "Top 200 Business and Technology Innovators" al mondo, ho la missione di democratizzare il machine learning e di eliminare il gergo tecnico affinché tutti possano partecipare a questa trasformazione.

FAQs

In cosa differisce il test esatto di Fisher dal test chi-quadrato?

Il test esatto di Fisher calcola una probabilità esatta ed è ideale per campioni piccoli o frequenze attese basse nelle celle, mentre il test chi-quadrato usa un’approssimazione per grandi campioni che può diventare imprecisa quando le frequenze attese scendono sotto 5.

Quali sono le applicazioni pratiche del test esatto di Fisher nella ricerca medica?

È comunemente usato per valutare l’efficacia dei trattamenti, analizzare gli esiti di trial clinici, indagare le associazioni con le malattie e studiare i fattori di rischio genetici — soprattutto quando le dimensioni dei campioni di pazienti sono ridotte.

Il test esatto di Fisher può essere usato per tabelle di contingenza più grandi di 2×2?

Sì, il test esatto di Fisher può essere esteso a tabelle più grandi usando metodi come l’estensione di Freeman–Halton, anche se i calcoli diventano più onerosi all’aumentare della dimensione della tabella.

Quali sono le assunzioni necessarie per usare il test esatto di Fisher?

I dati devono essere categoriali, organizzati in una tabella di contingenza, con osservazioni indipendenti e totali di riga e colonna fissati sotto l’ipotesi nulla.

In che modo la distribuzione ipergeometrica è collegata al test esatto di Fisher?

Il test esatto di Fisher basa i suoi calcoli di probabilità sulla distribuzione ipergeometrica, che modella la probabilità di osservare una specifica disposizione dei conteggi dati i totali marginali fissati.

Argomenti

Correlato