Perché l'Analisi Statistica in Python è Così Importante nel 2026
Se lavori con i dati, prima o poi ti ritrovi a dover andare oltre i grafici e le medie. Devi capire se una differenza è reale, quanto è grande un effetto, e quanto puoi fidarti dei tuoi risultati. È qui che entra in gioco l'inferenza statistica.
Mentre scikit-learn domina il mondo del machine learning predittivo, SciPy e statsmodels restano gli strumenti di riferimento quando si tratta di validare ipotesi, analizzare relazioni tra variabili e quantificare l'incertezza. Sono due librerie che, onestamente, ogni data analyst dovrebbe conoscere bene.
Con SciPy 1.17 (uscito a gennaio 2026) e statsmodels 0.14.6 (dicembre 2025), entrambi compatibili con Python 3.13, l'ecosistema statistico di Python non è mai stato così solido. In questa guida vediamo come condurre un'analisi statistica completa — dalla descrittiva ai test d'ipotesi, passando per correlazione e regressione — con codice che puoi copiare e usare subito.
Configurazione dell'Ambiente di Lavoro
Prima di tutto, installiamo quello che serve.
Installazione delle Librerie
pip install numpy pandas scipy statsmodels matplotlib seaborn
Import Iniziali
import numpy as np
import pandas as pd
from scipy import stats
import statsmodels.api as sm
import statsmodels.formula.api as smf
from statsmodels.stats import diagnostic, weightstats
import matplotlib.pyplot as plt
import seaborn as sns
# Configurazione grafica
sns.set_theme(style="whitegrid")
plt.rcParams["figure.figsize"] = (10, 6)
print(f"SciPy: {__import__('scipy').__version__}")
print(f"statsmodels: {sm.__version__}")
print(f"pandas: {pd.__version__}")
Se tutto funziona senza errori, sei pronto a partire.
Statistica Descrittiva con pandas e SciPy
Il primo passo di ogni analisi seria è esplorare i dati. Sembra banale, ma ti evita un sacco di problemi dopo. pandas offre metodi integrati piuttosto completi, mentre SciPy aggiunge statistiche più avanzate.
Statistiche di Base con pandas
# Carichiamo un dataset di esempio
df = pd.DataFrame({
"altezza_cm": [168, 175, 182, 170, 165, 178, 190, 172, 169, 180,
160, 185, 177, 173, 166, 188, 171, 179, 174, 183],
"peso_kg": [65, 78, 85, 70, 60, 80, 95, 72, 66, 82,
55, 90, 76, 74, 62, 92, 68, 81, 73, 87],
"gruppo": ["A"]*10 + ["B"]*10
})
# Statistiche descrittive per gruppo
print(df.groupby("gruppo").describe().round(2))
Misure di Forma: Asimmetria e Curtosi
L'asimmetria (skewness) e la curtosi (kurtosis) descrivono la forma della distribuzione. In pratica, la skewness ti dice se i dati sono "sbilanciati" verso un lato, mentre la curtosi misura quanto sono pesanti le code. Valori di asimmetria vicini a zero indicano una distribuzione più o meno simmetrica.
from scipy.stats import skew, kurtosis, shapiro
altezze = df["altezza_cm"]
print(f"Asimmetria (skewness): {skew(altezze):.4f}")
print(f"Curtosi (excess kurtosis): {kurtosis(altezze):.4f}")
# Test di Shapiro-Wilk per la normalita
stat_sw, p_sw = shapiro(altezze)
print(f"\nTest di Shapiro-Wilk:")
print(f"Statistica W = {stat_sw:.4f}, p-value = {p_sw:.4f}")
if p_sw > 0.05:
print("Non possiamo rifiutare l'ipotesi di normalita (alpha=0.05)")
else:
print("La distribuzione non e normale (alpha=0.05)")
Un consiglio: non affidarti solo al test di Shapiro-Wilk. Guardare il Q-Q plot è spesso più informativo, soprattutto con campioni molto grandi (dove il test tende a rifiutare anche per deviazioni minime dalla normalità).
Visualizzazione della Distribuzione
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
# Istogramma con curva KDE
sns.histplot(altezze, kde=True, ax=axes[0], color="steelblue")
axes[0].set_title("Distribuzione delle Altezze")
axes[0].set_xlabel("Altezza (cm)")
# Q-Q plot per la normalita
stats.probplot(altezze, dist="norm", plot=axes[1])
axes[1].set_title("Q-Q Plot (Normalita)")
plt.tight_layout()
plt.savefig("distribuzione_altezze.png", dpi=150)
plt.show()
Test d'Ipotesi: la Base dell'Inferenza Statistica
Allora, i test d'ipotesi. Sono probabilmente lo strumento più usato (e abusato) in statistica. Il flusso è sempre lo stesso: formuli un'ipotesi nulla (H₀) e un'alternativa (H₁), scegli il test giusto, calcoli la statistica test e il p-value, e decidi se rifiutare H₀.
Semplice a dirsi. In pratica, la parte difficile è scegliere il test corretto e interpretare i risultati senza cadere in trappole comuni.
T-Test per Campioni Indipendenti
Il t-test verifica se le medie di due gruppi sono statisticamente diverse. È il pane quotidiano dell'analisi statistica.
gruppo_a = df[df["gruppo"] == "A"]["peso_kg"]
gruppo_b = df[df["gruppo"] == "B"]["peso_kg"]
# Prima verifichiamo l'omogeneita delle varianze con il test di Levene
stat_lev, p_lev = stats.levene(gruppo_a, gruppo_b)
print(f"Test di Levene: F = {stat_lev:.4f}, p-value = {p_lev:.4f}")
varianze_uguali = p_lev > 0.05
# T-test a due campioni indipendenti
t_stat, p_value = stats.ttest_ind(gruppo_a, gruppo_b, equal_var=varianze_uguali)
print(f"\nT-test indipendente (equal_var={varianze_uguali}):")
print(f"t = {t_stat:.4f}, p-value = {p_value:.4f}")
if p_value < 0.05:
print("Le medie dei due gruppi sono significativamente diverse (alpha=0.05)")
else:
print("Non c'e differenza significativa tra le medie (alpha=0.05)")
Nota importante: controlla sempre le varianze con Levene prima di lanciare il t-test. Se le varianze non sono omogenee, equal_var=False usa la correzione di Welch, che è più robusta.
Calcolo della Dimensione dell'Effetto (Cohen's d)
Il p-value da solo non basta — e questo è un punto su cui insisto sempre. Un p-value basso ti dice che c'è qualcosa, ma non ti dice quanto è grande quell'effetto. Per quello serve il Cohen's d.
def cohens_d(group1, group2):
n1, n2 = len(group1), len(group2)
var1, var2 = group1.var(), group2.var()
pooled_std = np.sqrt(((n1 - 1) * var1 + (n2 - 1) * var2) / (n1 + n2 - 2))
return (group1.mean() - group2.mean()) / pooled_std
d = cohens_d(gruppo_a, gruppo_b)
print(f"Cohen's d = {d:.4f}")
# Interpretazione: |d| < 0.2 piccolo, 0.2-0.8 medio, > 0.8 grande
T-Test per Campioni Appaiati
Questo è utile quando confronti misurazioni "prima e dopo" sullo stesso soggetto — tipo peso prima e dopo una dieta, o punteggi prima e dopo un corso.
# Simuliamo dati di peso prima e dopo un programma di allenamento
np.random.seed(42)
peso_prima = np.random.normal(80, 10, 30)
peso_dopo = peso_prima - np.random.normal(2, 3, 30) # Calo medio di ~2 kg
t_stat, p_value = stats.ttest_rel(peso_prima, peso_dopo)
print(f"T-test appaiato: t = {t_stat:.4f}, p-value = {p_value:.4f}")
differenze = peso_prima - peso_dopo
print(f"Differenza media: {differenze.mean():.2f} kg")
print(f"IC 95%: ({np.percentile(differenze, 2.5):.2f}, {np.percentile(differenze, 97.5):.2f}) kg")
Test del Chi-Quadrato per l'Indipendenza
Quando hai a che fare con variabili categoriche (e non continue), il chi-quadrato è il test che fa per te. Verifica se esiste un'associazione tra due variabili nominali.
# Tabella di contingenza: metodo di studio vs esito esame
contingenza = pd.DataFrame({
"Promosso": [45, 30, 50],
"Bocciato": [15, 20, 10]
}, index=["Studio individuale", "Gruppo di studio", "Tutor privato"])
print("Tabella di contingenza:")
print(contingenza)
print()
chi2, p_value, dof, expected = stats.chi2_contingency(contingenza)
print(f"Chi-quadrato = {chi2:.4f}")
print(f"Gradi di liberta = {dof}")
print(f"p-value = {p_value:.4f}")
# V di Cramer per la dimensione dell'effetto
n = contingenza.values.sum()
min_dim = min(contingenza.shape) - 1
cramers_v = np.sqrt(chi2 / (n * min_dim))
print(f"V di Cramer = {cramers_v:.4f}")
ANOVA: Confronto tra Più Gruppi
Se il t-test confronta due gruppi, l'ANOVA (Analysis of Variance) fa lo stesso con tre o più gruppi. Verifica se almeno una delle medie è diversa dalle altre.
Attenzione però: l'ANOVA ti dice solo se c'è una differenza, non dove. Per quello servono i test post-hoc.
ANOVA a Una Via con SciPy
# Simuliamo i punteggi di tre metodi di insegnamento
np.random.seed(2026)
metodo_1 = np.random.normal(75, 8, 25)
metodo_2 = np.random.normal(80, 7, 25)
metodo_3 = np.random.normal(78, 9, 25)
# Test ANOVA a una via
F_stat, p_value = stats.f_oneway(metodo_1, metodo_2, metodo_3)
print(f"ANOVA a una via: F = {F_stat:.4f}, p-value = {p_value:.4f}")
if p_value < 0.05:
print("Almeno un gruppo ha una media significativamente diversa")
# Test post-hoc di Tukey per capire QUALI gruppi differiscono
from statsmodels.stats.multicomp import pairwise_tukeyhsd
dati = np.concatenate([metodo_1, metodo_2, metodo_3])
gruppi = ["Metodo 1"]*25 + ["Metodo 2"]*25 + ["Metodo 3"]*25
tukey = pairwise_tukeyhsd(dati, gruppi, alpha=0.05)
print(f"\nTest post-hoc di Tukey HSD:")
print(tukey)
ANOVA a Due Vie con statsmodels
Quando entrano in gioco due fattori (e magari la loro interazione), serve l'ANOVA a due vie. Qui statsmodels brilla, con una sintassi simile a R.
# Dataset con due fattori: metodo di studio e livello di esperienza
np.random.seed(2026)
n = 20
data_anova2 = pd.DataFrame({
"punteggio": np.random.normal(75, 10, n*4),
"metodo": (["Online", "Aula"] * (n*2)),
"esperienza": (["Junior"] * (n*2) + ["Senior"] * (n*2))
})
# ANOVA a due vie con interazione
model = smf.ols("punteggio ~ C(metodo) * C(esperienza)", data=data_anova2).fit()
anova_table = sm.stats.anova_lm(model, typ=2)
print("ANOVA a due vie:")
print(anova_table.round(4))
Analisi della Correlazione
La correlazione misura quanto due variabili "si muovono insieme". Un concetto semplice, ma con diverse sfumature a seconda del tipo di dati che hai.
Coefficiente di Pearson
# Correlazione tra altezza e peso
r, p_value = stats.pearsonr(df["altezza_cm"], df["peso_kg"])
print(f"Correlazione di Pearson: r = {r:.4f}, p-value = {p_value:.6f}")
# Matrice di correlazione con pandas
corr_matrix = df[["altezza_cm", "peso_kg"]].corr()
print(f"\nMatrice di correlazione:\n{corr_matrix.round(4)}")
# Heatmap della correlazione
sns.heatmap(corr_matrix, annot=True, cmap="coolwarm", vmin=-1, vmax=1,
square=True, fmt=".3f")
plt.title("Matrice di Correlazione")
plt.tight_layout()
plt.show()
Correlazioni Non Parametriche: Spearman e Kendall
Quando i dati non sono normali o la relazione non è lineare, le correlazioni di Pearson possono essere fuorvianti. In questi casi, meglio affidarsi a Spearman (basata sui ranghi) o Kendall (particolarmente robusta con campioni piccoli).
# Correlazione di Spearman (basata sui ranghi)
rho, p_spearman = stats.spearmanr(df["altezza_cm"], df["peso_kg"])
print(f"Spearman rho = {rho:.4f}, p-value = {p_spearman:.6f}")
# Correlazione di Kendall (robusta con campioni piccoli)
tau, p_kendall = stats.kendalltau(df["altezza_cm"], df["peso_kg"])
print(f"Kendall tau = {tau:.4f}, p-value = {p_kendall:.6f}")
# Confronto rapido
print(f"\nConfronto coefficienti:")
print(f" Pearson r = {r:.4f}")
print(f" Spearman rho = {rho:.4f}")
print(f" Kendall tau = {tau:.4f}")
Nella mia esperienza, Spearman è la scelta giusta nella maggior parte dei casi pratici. È più robusta di Pearson e funziona bene anche quando la relazione non è perfettamente lineare.
Regressione Lineare con statsmodels
La regressione lineare è probabilmente il modello statistico più importante da conoscere. A differenza di scikit-learn (che è ottimizzato per la predizione), statsmodels ti dà un'analisi inferenziale completa: p-value per ogni coefficiente, intervalli di confidenza, diagnostica del modello. Tutto quello che serve per capire davvero cosa sta succedendo nei dati.
Regressione Lineare Semplice
# Regressione: peso in funzione dell'altezza
X = df["altezza_cm"]
y = df["peso_kg"]
# Con la formula API (stile R)
model = smf.ols("peso_kg ~ altezza_cm", data=df).fit()
print(model.summary())
Interpretazione del Summary di statsmodels
L'output di model.summary() può sembrare intimidatorio la prima volta, ma le metriche chiave sono poche:
- R-squared: percentuale di varianza spiegata dal modello (va da 0 a 1). Più è alto, meglio è.
- Adj. R-squared: versione corretta per il numero di variabili — indispensabile quando confronti modelli con un numero diverso di predittori.
- F-statistic e Prob(F): test sulla significatività globale. Se il p-value è basso, il modello nel suo insieme è significativo.
- coef: i coefficienti stimati per ogni variabile.
- P>|t|: p-value per ogni singolo coefficiente. Sotto 0.05 = significativo.
- [0.025 0.975]: intervallo di confidenza al 95% per ogni coefficiente.
Regressione Lineare Multipla
# Aggiungiamo variabili al dataset
np.random.seed(42)
df["eta"] = np.random.randint(20, 55, len(df))
df["attivita_fisica_ore"] = np.random.uniform(0, 10, len(df)).round(1)
# Modello multiplo
model_multi = smf.ols(
"peso_kg ~ altezza_cm + eta + attivita_fisica_ore",
data=df
).fit()
print(model_multi.summary())
# Confronto modelli con AIC e BIC
print(f"\nModello semplice - AIC: {model.aic:.2f}, BIC: {model.bic:.2f}")
print(f"Modello multiplo - AIC: {model_multi.aic:.2f}, BIC: {model_multi.bic:.2f}")
Un AIC o BIC più basso indica un modello migliore (a parità di altre condizioni). Sono particolarmente utili quando devi decidere se aggiungere una variabile migliora davvero il modello o lo complica inutilmente.
Diagnostica del Modello di Regressione
Stimare il modello è solo metà del lavoro. L'altra metà — quella che molti saltano — è verificare che le assunzioni reggano: normalità dei residui, omoschedasticità, assenza di multicollinearità.
# 1. Test di normalita dei residui (Jarque-Bera)
jb_stat, jb_pvalue, jb_skew, jb_kurt = diagnostic.jarque_bera(model_multi.resid)
print(f"Test di Jarque-Bera: statistica = {jb_stat:.4f}, p-value = {jb_pvalue:.4f}")
# 2. Test di omoschedasticita (Breusch-Pagan)
bp_stat, bp_pvalue, bp_fstat, bp_fpvalue = diagnostic.het_breuschpagan(
model_multi.resid, model_multi.model.exog
)
print(f"Test di Breusch-Pagan: statistica = {bp_stat:.4f}, p-value = {bp_pvalue:.4f}")
# 3. Variance Inflation Factor (VIF) per la multicollinearita
from statsmodels.stats.outliers_influence import variance_inflation_factor
X_vif = df[["altezza_cm", "eta", "attivita_fisica_ore"]]
X_vif = sm.add_constant(X_vif)
print("\nVariance Inflation Factor (VIF):")
for i, col in enumerate(X_vif.columns):
vif = variance_inflation_factor(X_vif.values, i)
print(f" {col}: {vif:.2f}")
# VIF > 5 indica possibile multicollinearita, VIF > 10 e critico
Grafici Diagnostici dei Residui
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
# 1. Residui vs Valori Stimati
axes[0,0].scatter(model_multi.fittedvalues, model_multi.resid, alpha=0.7)
axes[0,0].axhline(y=0, color="red", linestyle="--")
axes[0,0].set_xlabel("Valori Stimati")
axes[0,0].set_ylabel("Residui")
axes[0,0].set_title("Residui vs Valori Stimati")
# 2. Q-Q plot dei residui
stats.probplot(model_multi.resid, dist="norm", plot=axes[0,1])
axes[0,1].set_title("Q-Q Plot dei Residui")
# 3. Distribuzione dei residui
sns.histplot(model_multi.resid, kde=True, ax=axes[1,0], color="steelblue")
axes[1,0].set_title("Distribuzione dei Residui")
# 4. Scale-Location plot
axes[1,1].scatter(model_multi.fittedvalues, np.sqrt(np.abs(model_multi.resid)), alpha=0.7)
axes[1,1].set_xlabel("Valori Stimati")
axes[1,1].set_ylabel("sqrt(|Residui|)")
axes[1,1].set_title("Scale-Location Plot")
plt.tight_layout()
plt.savefig("diagnostica_regressione.png", dpi=150)
plt.show()
Se noti pattern chiari nel primo grafico (tipo una forma a imbuto o una curva), è un segnale che qualcosa non va. In quel caso, potresti aver bisogno di trasformare le variabili o usare un modello diverso.
Test Non Parametrici
A volte i dati non rispettano le assunzioni dei test parametrici. E va bene così — i test non parametrici sono lì apposta. Non richiedono ipotesi sulla distribuzione e funzionano anche con campioni piccoli o dati ordinali.
Test di Mann-Whitney U (Alternativa al T-Test)
u_stat, p_value = stats.mannwhitneyu(gruppo_a, gruppo_b, alternative="two-sided")
print(f"Mann-Whitney U: U = {u_stat:.4f}, p-value = {p_value:.4f}")
# Dimensione dell'effetto: r = Z / sqrt(N)
from scipy.stats import norm
z_score = norm.ppf(p_value / 2)
r_effect = abs(z_score) / np.sqrt(len(gruppo_a) + len(gruppo_b))
print(f"Dimensione dell'effetto r = {r_effect:.4f}")
Test di Kruskal-Wallis (Alternativa all'ANOVA)
h_stat, p_value = stats.kruskal(metodo_1, metodo_2, metodo_3)
print(f"Kruskal-Wallis: H = {h_stat:.4f}, p-value = {p_value:.4f}")
# Eta-quadrato come dimensione dell'effetto
n_total = len(metodo_1) + len(metodo_2) + len(metodo_3)
eta_squared = (h_stat - 2) / (n_total - 3)
print(f"Eta-quadrato = {eta_squared:.4f}")
Test di Wilcoxon (Alternativa al T-Test Appaiato)
w_stat, p_value = stats.wilcoxon(peso_prima, peso_dopo)
print(f"Wilcoxon signed-rank: W = {w_stat:.4f}, p-value = {p_value:.4f}")
Quando usarli? Regola pratica: se il campione è piccolo (sotto i 30 elementi) e il test di Shapiro-Wilk rifiuta la normalità, vai con il non parametrico. Non perdi molto in termini di potenza e dormi sonni più tranquilli.
Correzione per Test Multipli
Questo è un aspetto che viene sottovalutato troppo spesso. Se esegui 20 test con alpha=0.05, ti aspetti circa un falso positivo per puro caso. La correzione per test multipli è essenziale per non prendere fischi per fiaschi.
from statsmodels.stats.multitest import multipletests
# Simuliamo 20 p-value da test multipli
p_values = [0.001, 0.013, 0.029, 0.032, 0.048,
0.065, 0.070, 0.089, 0.102, 0.130,
0.180, 0.230, 0.340, 0.450, 0.520,
0.610, 0.720, 0.800, 0.890, 0.950]
# Metodo di Bonferroni (conservativo)
reject_bonf, pvals_bonf, _, _ = multipletests(p_values, alpha=0.05, method="bonferroni")
# Metodo di Benjamini-Hochberg (controlla il FDR)
reject_bh, pvals_bh, _, _ = multipletests(p_values, alpha=0.05, method="fdr_bh")
risultati = pd.DataFrame({
"p_originale": p_values,
"p_bonferroni": pvals_bonf.round(4),
"significativo_bonf": reject_bonf,
"p_BH": pvals_bh.round(4),
"significativo_BH": reject_bh
})
print("Confronto metodi di correzione:")
print(risultati.to_string(index=False))
Come puoi vedere dall'output, Bonferroni è molto più severo — in certi casi troppo severo. Benjamini-Hochberg è generalmente la scelta migliore per studi esplorativi.
Workflow Completo: un Caso Pratico
Bene, mettiamo insieme tutto quello che abbiamo visto. Analizzeremo se il tipo di dieta e le ore di attività fisica influenzano la perdita di peso. Un esempio realistico che segue il flusso che useresti in un progetto vero.
# Generazione del dataset di esempio
np.random.seed(2026)
n = 120
diete = np.random.choice(["Mediterranea", "Low-carb", "Vegana"], n)
ore_sport = np.random.uniform(0, 8, n).round(1)
# La perdita di peso dipende dalla dieta e dall'attivita
perdita_base = {"Mediterranea": 3.5, "Low-carb": 4.0, "Vegana": 3.0}
perdita = np.array([perdita_base[d] for d in diete])
perdita += 0.5 * ore_sport + np.random.normal(0, 2, n)
dataset = pd.DataFrame({
"dieta": diete,
"ore_sport_settimana": ore_sport,
"perdita_peso_kg": perdita.round(2)
})
print("Prime righe del dataset:")
print(dataset.head(10))
print(f"\nDimensioni: {dataset.shape}")
print(f"\nStatistiche per dieta:")
print(dataset.groupby("dieta")["perdita_peso_kg"].describe().round(2))
Fase 1: Esplorazione e Verifica delle Assunzioni
# Test di normalita per ogni gruppo
print("Test di Shapiro-Wilk per gruppo:")
for dieta in dataset["dieta"].unique():
dati = dataset[dataset["dieta"] == dieta]["perdita_peso_kg"]
stat, p = shapiro(dati)
normale = "Normale" if p > 0.05 else "Non normale"
print(f" {dieta}: W = {stat:.4f}, p-value = {p:.4f} - {normale}")
# Test di Levene per l'omogeneita delle varianze
gruppi = [g["perdita_peso_kg"].values for _, g in dataset.groupby("dieta")]
stat_lev, p_lev = stats.levene(*gruppi)
print(f"\nTest di Levene: F = {stat_lev:.4f}, p-value = {p_lev:.4f}")
Fase 2: ANOVA e Test Post-Hoc
# ANOVA a una via
F_stat, p_anova = stats.f_oneway(*gruppi)
print(f"ANOVA: F = {F_stat:.4f}, p-value = {p_anova:.4f}")
# Se significativo, test post-hoc
if p_anova < 0.05:
tukey = pairwise_tukeyhsd(
dataset["perdita_peso_kg"],
dataset["dieta"],
alpha=0.05
)
print(f"\nTest di Tukey HSD:")
print(tukey)
# Visualizzazione
tukey.plot_simultaneous()
plt.title("Confronto Diete - Intervalli di Confidenza Tukey HSD")
plt.xlabel("Perdita di peso (kg)")
plt.tight_layout()
plt.show()
Fase 3: Regressione Multipla
# Modello di regressione con variabile categorica e continua
model_dieta = smf.ols(
"perdita_peso_kg ~ C(dieta, Treatment(reference='Vegana')) + ore_sport_settimana",
data=dataset
).fit()
print(model_dieta.summary())
# Intervalli di confidenza per i coefficienti
print("\nIntervalli di confidenza al 95%:")
print(model_dieta.conf_int().round(4))
Tabella Riepilogativa: Quale Test Usare?
Questa è una di quelle tabelle da tenere a portata di mano. Scegliere il test sbagliato è uno degli errori più comuni.
| Situazione | Test Parametrico | Test Non Parametrico | Funzione Python |
|---|---|---|---|
| Confronto 2 gruppi indipendenti | t-test indipendente | Mann-Whitney U | ttest_ind / mannwhitneyu |
| Confronto 2 misure sugli stessi soggetti | t-test appaiato | Wilcoxon signed-rank | ttest_rel / wilcoxon |
| Confronto 3+ gruppi | ANOVA a una via | Kruskal-Wallis | f_oneway / kruskal |
| Associazione variabili categoriche | Chi-quadrato | Test esatto di Fisher | chi2_contingency / fisher_exact |
| Correlazione lineare | Pearson | Spearman / Kendall | pearsonr / spearmanr |
| Relazione variabile dipendente continua | Regressione lineare | --- | smf.ols |
| Verifica normalità | Shapiro-Wilk | --- | shapiro |
Domande Frequenti (FAQ)
Qual è la differenza tra SciPy e statsmodels per l'analisi statistica?
SciPy (scipy.stats) è perfetto per test statistici rapidi — t-test, ANOVA, correlazioni, test non parametrici — con un'interfaccia semplice che restituisce statistica test e p-value. statsmodels, invece, è orientato alla modellazione statistica completa: regressione, ANOVA a più vie, diagnostica dei modelli, intervalli di confidenza e tabelle dettagliate (molto simili a quelle di R). In pratica, li uso sempre insieme: SciPy per i test veloci, statsmodels quando serve la regressione o un'analisi più approfondita.
Quando devo usare un test parametrico rispetto a uno non parametrico?
I test parametrici (t-test, ANOVA, Pearson) assumono che i dati provengano da una distribuzione normale e hanno maggiore potenza statistica quando le assunzioni sono rispettate. I test non parametrici (Mann-Whitney, Kruskal-Wallis, Spearman) non richiedono queste assunzioni e sono la scelta giusta con campioni piccoli (n < 30), dati ordinali, distribuzioni molto asimmetriche, o quando Shapiro-Wilk rifiuta la normalità.
Come scelgo tra Bonferroni e Benjamini-Hochberg per la correzione?
Bonferroni controlla il FWER (Family-Wise Error Rate) ed è molto conservativo: riduce i falsi positivi ma rischia di farti perdere risultati veri. È adatto quando anche un singolo falso positivo è inaccettabile (come nei trial clinici). Benjamini-Hochberg controlla il FDR (False Discovery Rate) ed è meno severo: ideale per studi esplorativi con molti test simultanei (genomica, A/B testing), dove un certo tasso di falsi positivi è tollerabile.
Come verifico se il mio modello di regressione è valido?
Ci sono quattro assunzioni da controllare: (1) linearità — il grafico residui vs valori stimati non deve mostrare pattern; (2) normalità dei residui — verificabile con Q-Q plot e test di Jarque-Bera; (3) omoschedasticità — la varianza dei residui deve essere costante (test di Breusch-Pagan); (4) assenza di multicollinearità — il VIF dovrebbe restare sotto 5 per ogni variabile.
Cosa significa davvero il p-value?
Il p-value è la probabilità di osservare un risultato altrettanto estremo (o più estremo) assumendo che l'ipotesi nulla sia vera. Un p-value sotto 0.05 suggerisce che il risultato è improbabile sotto H₀, portando al suo rifiuto. Ma attenzione: il p-value non ti dice la probabilità che H₀ sia vera, né quanto è grande l'effetto. Riporta sempre anche la dimensione dell'effetto (Cohen's d, r, eta-quadrato) e gli intervalli di confidenza — è l'unico modo per avere un quadro completo.