Introduction : Pourquoi Maitriser les Tests Statistiques en Python en 2026
Bon, soyons honnetes. Si vous bossez avec des donnees en Python — que ce soit en data science, en recherche, en marketing analytique ou en ingenierie qualite — il arrive toujours un moment ou les moyennes et les jolis graphiques ne suffisent plus. Vous devez prouver quelque chose. Est-ce que ce nouveau modele est vraiment meilleur que l'ancien, ou c'est juste une impression ? La difference de conversion entre vos deux variantes A/B, elle est reelle ou c'est du bruit ? Les temps de reponse du serveur ont-ils vraiment change apres la mise a jour, ou vous voyez des patterns la ou il n'y en a pas ?
C'est la que les tests statistiques entrent en jeu.
En 2026, l'ecosysteme Python n'a jamais ete aussi mature pour l'analyse statistique. SciPy 1.17 (version stable 1.17.1, sortie le 22 fevrier 2026) apporte un support natif du traitement par lots N-dimensionnel et une compatibilite etendue avec l'Array API — concretement, ca veut dire que vos tests statistiques peuvent desormais s'executer sur des tableaux multidimensionnels sans boucles manuelles. La version 1.15 avait deja introduit des ajouts majeurs : le coefficient Xi de Chatterjee (chatterjeexi), les L-moments (lmoment), les statistiques d'ordre (order_statistic), un test de Wilcoxon ameliore, de nouveaux parametres method pour ttest_ind (notamment PermutationMethod et MonteCarloMethod), et le parametre rng pour la reproductibilite. Bref, y'a de quoi faire.
Ce guide est concu pour etre votre reference pratique. On va couvrir chaque famille de tests statistiques — normalite, comparaisons de moyennes, tests non parametriques, independance et correlation, methodes avancees (bootstrap, permutation, Monte Carlo) — avec du code fonctionnel, des interpretations claires et des conseils concrets issus de la pratique quotidienne. J'ai essaye de garder le ton d'un collegue qui vous explique les choses autour d'un cafe, pas celui d'un manuel de stats de 800 pages (on a tous essaye d'en lire un, n'est-ce pas ?).
Cet article fait partie d'une serie sur l'analyse de donnees en Python. Si vous n'avez pas encore lu nos guides sur le nettoyage de donnees avec pandas 3.0, la visualisation avancee avec matplotlib et seaborn, les pipelines de machine learning avec scikit-learn, ou l'analyse de series temporelles avec statsmodels, ils constituent d'excellents complements a ce qui suit.
Pre-requis : Installation et Configuration de l'Environnement
Avant de plonger dans le code, configurons un environnement propre. SciPy 1.17 necessite Python 3.11 ou superieur (compatible jusqu'a Python 3.14) et NumPy 1.26.4+. Si vous utilisez encore Python 3.10 ou une version anterieure, c'est le moment de mettre a jour — les fonctionnalites dont on va parler ne sont tout simplement pas disponibles sur les anciennes versions. J'ai personnellement passe une bonne heure a debugger un import error avant de realiser que j'etais encore sur 3.10... ne faites pas la meme erreur.
Installez les bibliotheques necessaires via pip :
pip install scipy==1.17.1 numpy==2.4.3 pandas==3.0.0 matplotlib>=3.9
Une fois l'installation terminee, verifiez que tout est en ordre :
import scipy
import numpy as np
import pandas as pd
import matplotlib
import sys
print(f"Python : {sys.version}")
print(f"SciPy : {scipy.__version__}") # 1.17.1
print(f"NumPy : {np.__version__}") # 2.4.3
print(f"pandas : {pd.__version__}") # 3.0.0
print(f"matplotlib : {matplotlib.__version__}")
Si vos versions correspondent a celles indiquees dans les commentaires (ou sont superieures), c'est bon, vous etes pret. Tout le code de cet article utilise np.random.default_rng(42) comme generateur aleatoire pour garantir la reproductibilite — une bonne pratique que SciPy 1.15+ encourage via son parametre rng.
Importons les modules qu'on va utiliser tout au long de ce guide :
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy import stats
# Generateur aleatoire reproductible
rng = np.random.default_rng(42)
Comprendre les Fondamentaux des Tests d'Hypotheses
Avant d'ecrire la moindre ligne de code, clarifions les bases. Un test statistique, c'est une procedure formelle pour determiner si une observation dans vos donnees est le fruit du hasard ou si elle reflete un effet reel. Toute la logique repose sur deux hypotheses concurrentes.
L'hypothese nulle (H0), c'est l'hypothese du statu quo. Elle affirme qu'il n'y a pas d'effet, pas de difference, pas de relation. Par exemple : "la moyenne du groupe A est egale a la moyenne du groupe B" ou "les deux variables sont independantes". L'hypothese alternative (H1), c'est ce que vous essayez de demontrer — qu'il y a un effet, une difference ou une relation significative.
Le test produit une p-value — la probabilite d'observer un resultat au moins aussi extreme que celui obtenu, en supposant que H0 est vraie. Si cette probabilite est tres faible (typiquement inferieure a un seuil alpha fixe a 0.05), on rejette H0 en faveur de H1. Sinon, on ne rejette pas H0. Attention, et c'est un point sur lequel je ne peux pas assez insister : ne pas rejeter H0 ne signifie pas que H0 est vraie. Ca veut juste dire que les donnees ne fournissent pas assez de preuves pour la rejeter. La nuance est importante.
Deux types d'erreurs sont possibles :
- Erreur de type I (faux positif) : rejeter H0 alors qu'elle est vraie. La probabilite de cette erreur est controlee par alpha (generalement 5 %).
- Erreur de type II (faux negatif) : ne pas rejeter H0 alors qu'elle est fausse. La probabilite de cette erreur depend de la taille de l'echantillon et de l'ampleur de l'effet.
Parametrique ou non parametrique, comment choisir ? Les tests parametriques (t-test, ANOVA) supposent que vos donnees suivent une distribution specifique — generalement la loi normale. Ils sont plus puissants quand leurs hypotheses sont respectees. Les tests non parametriques (Mann-Whitney, Wilcoxon, Kruskal-Wallis) ne font pas cette supposition et fonctionnent sur les rangs. Ils sont plus robustes mais generalement un chouia moins puissants. Dans mon experience, quand j'ai un doute, je lance les deux et je compare — si les conclusions convergent, ca rassure.
Voici un arbre de decision simplifie pour choisir le bon test :
Objectif ?
|
|-- Comparer un echantillon a une valeur de reference
| |-- Donnees normales ? --> Test t a 1 echantillon (ttest_1samp)
| |-- Sinon --> Test de Wilcoxon signe (wilcoxon)
|
|-- Comparer 2 groupes independants
| |-- Donnees normales ? --> Test t pour 2 echantillons (ttest_ind)
| |-- Sinon --> Mann-Whitney U (mannwhitneyu)
|
|-- Comparer 2 mesures appariees
| |-- Donnees normales ? --> Test t apparie (ttest_rel)
| |-- Sinon --> Wilcoxon signe (wilcoxon)
|
|-- Comparer 3+ groupes independants
| |-- Donnees normales ? --> ANOVA a un facteur (f_oneway)
| |-- Sinon --> Kruskal-Wallis (kruskal)
|
|-- Tester l'independance de 2 variables categoriques
| --> Chi-deux (chi2_contingency)
|
|-- Mesurer la correlation entre 2 variables continues
| |-- Relation lineaire ? --> Pearson (pearsonr)
| |-- Relation monotone ? --> Spearman (spearmanr)
| |-- Relation quelconque ? --> Chatterjee Xi (chatterjeexi)
Tests de Normalite : Verifier les Hypotheses de Base
La normalite des donnees, c'est la condition prealable a la plupart des tests parametriques. Si vos donnees ne sont pas normalement distribuees, les resultats d'un t-test ou d'une ANOVA peuvent etre carrement trompeurs. La bonne nouvelle, c'est que SciPy propose plusieurs tests pour verifier ca. Et franchement, la bonne pratique c'est d'en utiliser au moins deux pour confirmer le diagnostic — un seul test, ca peut toujours vous jouer des tours.
Test de Shapiro-Wilk
Le test de Shapiro-Wilk est l'un des tests de normalite les plus puissants pour les echantillons de taille petite a moderee (jusqu'a environ 5 000 observations). Son hypothese nulle est simple : "les donnees suivent une distribution normale".
rng = np.random.default_rng(42)
# Donnees normales (on s'attend a NE PAS rejeter H0)
donnees_normales = rng.normal(loc=50, scale=10, size=200)
# Donnees non normales (distribution exponentielle)
donnees_expo = rng.exponential(scale=10, size=200)
# Test de Shapiro-Wilk
stat_n, p_n = stats.shapiro(donnees_normales)
stat_e, p_e = stats.shapiro(donnees_expo)
print("=== Test de Shapiro-Wilk ===")
print(f"Donnees normales : W = {stat_n:.4f}, p-value = {p_n:.4f}")
# Donnees normales : W = 0.9976, p-value = 0.9581
print(f"Donnees exponent. : W = {stat_e:.4f}, p-value = {p_e:.4f}")
# Donnees exponent. : W = 0.8807, p-value = 0.0000
alpha = 0.05
print(f"\nDonnees normales : {'Normalite rejetee' if p_n < alpha else 'Normalite non rejetee'}")
print(f"Donnees exponent.: {'Normalite rejetee' if p_e < alpha else 'Normalite non rejetee'}")
Test de Kolmogorov-Smirnov
Le test de Kolmogorov-Smirnov (K-S) compare la distribution empirique de vos donnees a une distribution theorique. Contrairement a Shapiro-Wilk, il peut tester n'importe quelle distribution de reference, pas seulement la normale. Petit piege a connaitre cependant : pour tester la normalite, vous devez specifier les parametres de la distribution (moyenne, ecart-type). Si vous utilisez les parametres estimes a partir des donnees elles-memes, le test devient conservateur — il aura tendance a ne pas rejeter H0 meme quand il devrait.
rng = np.random.default_rng(42)
donnees = rng.normal(loc=50, scale=10, size=300)
# Test K-S contre une loi normale de meme moyenne et ecart-type
stat_ks, p_ks = stats.kstest(donnees, 'norm', args=(donnees.mean(), donnees.std()))
print("=== Test de Kolmogorov-Smirnov ===")
print(f"Statistique D = {stat_ks:.4f}, p-value = {p_ks:.4f}")
# Statistique D = 0.0305, p-value = 0.9318
print(f"Conclusion : {'Normalite rejetee' if p_ks < 0.05 else 'Normalite non rejetee'}")
Test de D'Agostino-Pearson
Le test de D'Agostino-Pearson (normaltest) combine les mesures d'asymetrie (skewness) et d'aplatissement (kurtosis) pour evaluer la normalite. Il est souvent recommande pour les echantillons de taille superieure a 20. Perso, j'aime bien l'utiliser en complement de Shapiro-Wilk — quand les deux sont d'accord, je dors mieux la nuit.
rng = np.random.default_rng(42)
donnees_normales = rng.normal(loc=0, scale=1, size=500)
donnees_skew = rng.exponential(scale=2, size=500)
stat_n, p_n = stats.normaltest(donnees_normales)
stat_s, p_s = stats.normaltest(donnees_skew)
print("=== Test de D'Agostino-Pearson ===")
print(f"Donnees normales : stat = {stat_n:.4f}, p-value = {p_n:.4f}")
# Donnees normales : stat = 1.2543, p-value = 0.5340
print(f"Donnees asymetr. : stat = {stat_s:.4f}, p-value = {p_s:.4f}")
# Donnees asymetr. : stat = 192.8431, p-value = 0.0000
Verification visuelle avec un QQ-plot
Les tests formels c'est bien, mais rien ne remplace la verification visuelle. Serieusement. Un QQ-plot (quantile-quantile plot) compare les quantiles de vos donnees aux quantiles theoriques d'une loi normale. Si les points s'alignent gentiment sur la diagonale, vos donnees sont approximativement normales. Les deviations aux extremites indiquent des queues de distribution plus lourdes ou plus legeres que la normale — et ca, un chiffre seul ne vous le montre pas aussi bien.
rng = np.random.default_rng(42)
donnees_normales = rng.normal(loc=50, scale=10, size=300)
donnees_skew = rng.exponential(scale=10, size=300)
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
# QQ-plot pour les donnees normales
stats.probplot(donnees_normales, dist="norm", plot=axes[0])
axes[0].set_title("QQ-plot : Donnees normales")
axes[0].grid(True, alpha=0.3)
# QQ-plot pour les donnees asymetriques
stats.probplot(donnees_skew, dist="norm", plot=axes[1])
axes[1].set_title("QQ-plot : Donnees exponentielles")
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig("qqplots_normalite.png", dpi=150)
plt.show()
Interpretation pratique : dans la vraie vie, la normalite parfaite n'existe pas. La question n'est pas "mes donnees sont-elles exactement normales ?" (spoiler : elles ne le sont jamais) mais plutot "mes donnees sont-elles suffisamment normales pour que les tests parametriques restent fiables ?". Pour les grands echantillons (n > 30), le theoreme central limite garantit que les moyennes d'echantillons tendent vers la normalite, ce qui rend les tests parametriques robustes meme si les donnees brutes ne sont pas parfaitement normales.
Et voila le paradoxe : les tests de normalite deviennent trop sensibles sur les grands echantillons. Ils rejettent souvent la normalite pour des ecarts qui n'ont aucune consequence pratique. J'ai vu des collegues paniquer parce que Shapiro-Wilk rejetait la normalite sur un echantillon de 50 000 observations... alors que le QQ-plot etait quasi parfait.
Tests de Comparaison de Moyennes
Les tests de comparaison de moyennes sont probablement les outils statistiques qu'on utilise le plus au quotidien. La question est toujours la meme : est-ce que la moyenne de ce groupe est differente de celle de cet autre groupe ? Est-ce que la valeur observee differe de la valeur theorique attendue ? SciPy offre une gamme complete de ces tests, avec des ameliorations vraiment appreciables dans les versions recentes.
Test t a un echantillon (ttest_1samp)
Le test t a un echantillon compare la moyenne d'un echantillon a une valeur de reference connue. Cas d'usage typique (et vecu) : verifier si le temps de reponse moyen d'une API est conforme au SLA fixe a 200 ms. Le genre de truc qu'on vous demande de prouver un vendredi soir avant une mise en prod...
rng = np.random.default_rng(42)
# Temps de reponse d'une API (en ms) — simule
temps_reponse = rng.normal(loc=210, scale=30, size=100)
# H0 : la moyenne est egale a 200 ms (le SLA)
# H1 : la moyenne est differente de 200 ms
stat, p_value = stats.ttest_1samp(temps_reponse, popmean=200)
print("=== Test t a un echantillon ===")
print(f"Moyenne observee : {temps_reponse.mean():.2f} ms")
print(f"Statistique t : {stat:.4f}")
print(f"p-value : {p_value:.4f}")
# Moyenne observee : 212.34 ms
# Statistique t : 4.0312
# p-value : 0.0001
if p_value < 0.05:
print("Conclusion : Le temps de reponse moyen differe significativement du SLA de 200 ms.")
else:
print("Conclusion : Pas de difference significative par rapport au SLA.")
La p-value est largement inferieure a 0.05 — on a des preuves solides que le temps de reponse moyen depasse le SLA. Notez que le test est bilateral par defaut, il detecte des differences dans les deux sens. Si vous voulez tester uniquement "le temps de reponse est-il superieur a 200 ms ?", utilisez le parametre alternative='greater'.
Test t pour deux echantillons independants (ttest_ind)
Le test t pour echantillons independants, c'est le cheval de bataille des tests A/B. Il compare les moyennes de deux groupes qui n'ont aucun lien entre eux. SciPy 1.15 a introduit une fonctionnalite que j'attendais depuis un moment : le parametre method, qui permet de specifier PermutationMethod ou MonteCarloMethod pour calculer la p-value autrement que par la methode parametrique classique. C'est particulierement utile quand les hypotheses du test classique sont un peu... bancales.
rng = np.random.default_rng(42)
# Test A/B : taux de conversion (temps passe sur le site en secondes)
groupe_controle = rng.normal(loc=45, scale=12, size=150)
groupe_test = rng.normal(loc=50, scale=12, size=150)
# Test t classique (hypothese de variances egales : equal_var=True par defaut)
stat, p_value = stats.ttest_ind(groupe_controle, groupe_test)
print("=== Test t classique (deux echantillons independants) ===")
print(f"Moyenne controle : {groupe_controle.mean():.2f}")
print(f"Moyenne test : {groupe_test.mean():.2f}")
print(f"Statistique t : {stat:.4f}")
print(f"p-value : {p_value:.4f}")
# Test de Welch (variances inegales) — recommande par defaut
stat_w, p_w = stats.ttest_ind(groupe_controle, groupe_test, equal_var=False)
print(f"\nTest de Welch : t = {stat_w:.4f}, p-value = {p_w:.4f}")
# Nouveau dans SciPy 1.15+ : methode par permutation
result_perm = stats.ttest_ind(
groupe_controle, groupe_test,
method=stats.PermutationMethod(n_resamples=9999, rng=rng)
)
print(f"\nTest par permutation : t = {result_perm.statistic:.4f}, p-value = {result_perm.pvalue:.4f}")
# La p-value par permutation ne depend d'aucune hypothese distributionnelle
L'approche par permutation est une excellente alternative quand vous n'etes pas sur que vos donnees respectent les hypotheses du test parametrique. Le principe est assez elegant : elle calcule la distribution de la statistique de test sous H0 en permutant les etiquettes des groupes des milliers de fois, puis compare votre statistique observee a cette distribution empirique. Le parametre rng garantit la reproductibilite — une addition vraiment bienvenue dans SciPy 1.15.
Test t apparie (ttest_rel)
Le test t apparie s'utilise quand les deux mesures proviennent des memes sujets — par exemple, les performances avant et apres une formation, ou les mesures du meme capteur dans deux conditions differentes. Ce test est plus puissant que le test a deux echantillons independants car il elimine la variabilite inter-sujets (et croyez-moi, cette variabilite peut etre enorme).
rng = np.random.default_rng(42)
# Score de productivite avant et apres une formation (memes employes)
n_employes = 50
avant_formation = rng.normal(loc=70, scale=15, size=n_employes)
# L'effet de la formation : +5 points en moyenne, avec du bruit
apres_formation = avant_formation + rng.normal(loc=5, scale=8, size=n_employes)
stat, p_value = stats.ttest_rel(avant_formation, apres_formation)
print("=== Test t apparie ===")
print(f"Moyenne avant : {avant_formation.mean():.2f}")
print(f"Moyenne apres : {apres_formation.mean():.2f}")
print(f"Difference moy.: {(apres_formation - avant_formation).mean():.2f}")
print(f"Statistique t : {stat:.4f}")
print(f"p-value : {p_value:.4f}")
if p_value < 0.05:
print("Conclusion : La formation a un effet significatif sur la productivite.")
else:
print("Conclusion : Pas d'effet significatif detecte.")
ANOVA a un facteur (f_oneway)
L'ANOVA (analyse de la variance) etend la comparaison a trois groupes ou plus. Au lieu de faire plusieurs t-tests (ce qui augmenterait dangereusement le risque d'erreur de type I — on en reparle plus bas), l'ANOVA teste en une seule fois si au moins un groupe differe des autres.
Attention cependant : si l'ANOVA est significative, elle ne vous dit pas quel groupe differe. C'est un peu frustrant, je sais. Vous devrez ensuite faire des comparaisons post-hoc (comme Tukey HSD) pour identifier les paires qui different.
rng = np.random.default_rng(42)
# Trois algorithmes de tri, temps d'execution en ms
algo_a = rng.normal(loc=100, scale=15, size=60)
algo_b = rng.normal(loc=110, scale=15, size=60)
algo_c = rng.normal(loc=105, scale=15, size=60)
stat, p_value = stats.f_oneway(algo_a, algo_b, algo_c)
print("=== ANOVA a un facteur ===")
print(f"Moyenne algo A : {algo_a.mean():.2f} ms")
print(f"Moyenne algo B : {algo_b.mean():.2f} ms")
print(f"Moyenne algo C : {algo_c.mean():.2f} ms")
print(f"Statistique F : {stat:.4f}")
print(f"p-value : {p_value:.4f}")
if p_value < 0.05:
print("Conclusion : Au moins un algorithme a un temps d'execution significativement different.")
# Comparaisons post-hoc avec Tukey HSD
donnees_groupes = np.concatenate([algo_a, algo_b, algo_c])
etiquettes = ['Algo A'] * 60 + ['Algo B'] * 60 + ['Algo C'] * 60
result_tukey = stats.tukey_hsd(algo_a, algo_b, algo_c)
print(f"\nComparaisons post-hoc (Tukey HSD) :")
print(result_tukey)
else:
print("Conclusion : Pas de difference significative entre les algorithmes.")
Tests Non Parametriques : Quand la Normalite Fait Defaut
Dans le monde reel, vos donnees ne sont pas toujours normalement distribuees. Loin de la, meme. Les durees de session sur un site web, les montants de transaction, les temps d'attente — ces distributions sont souvent fortement asymetriques avec des queues lourdes. J'ai personnellement passe des heures a essayer de transformer des donnees de temps de reponse pour les rendre "normales" avant de realiser que le plus simple, c'etait d'utiliser un test non parametrique.
Ces tests travaillent sur les rangs des donnees plutot que sur les valeurs brutes, ce qui les rend robustes face aux distributions exotiques et aux valeurs aberrantes.
Test de Mann-Whitney U (mannwhitneyu)
Le test de Mann-Whitney U est l'equivalent non parametrique du test t pour deux echantillons independants. Il teste si les distributions de deux groupes sont identiques, en se basant sur les rangs. C'est le test qu'on degaine quand la normalite n'est pas au rendez-vous.
rng = np.random.default_rng(42)
# Temps de resolution de tickets (distribution asymetrique, typique des donnees reelles)
equipe_a = rng.exponential(scale=4, size=80) + 2 # heures
equipe_b = rng.exponential(scale=5.5, size=80) + 2 # heures
stat, p_value = stats.mannwhitneyu(equipe_a, equipe_b, alternative='two-sided')
print("=== Test de Mann-Whitney U ===")
print(f"Mediane equipe A : {np.median(equipe_a):.2f} heures")
print(f"Mediane equipe B : {np.median(equipe_b):.2f} heures")
print(f"Statistique U : {stat:.1f}")
print(f"p-value : {p_value:.4f}")
if p_value < 0.05:
print("Conclusion : Les temps de resolution different significativement entre les equipes.")
else:
print("Conclusion : Pas de difference significative detectee.")
Petite precision qui a son importance : Mann-Whitney compare les distributions, pas strictement les medianes (c'est un malentendu tres repandu, meme dans certains manuels). Si les deux distributions ont la meme forme mais sont decalees, alors oui, ca revient a tester la difference de medianes. Sinon, ca teste si un echantillon a tendance a avoir des valeurs plus grandes que l'autre. Subtil, mais ca peut changer l'interpretation.
Test de Wilcoxon signe (wilcoxon)
Le test de Wilcoxon signe, c'est l'alternative non parametrique au test t apparie. On l'utilise pour des mesures appariees quand les differences entre les paires ne suivent pas une distribution normale. Depuis SciPy 1.15, le choix entre methode exacte, approximation et methode asymptotique est plus intelligent — notamment pour les petits echantillons ou ca pouvait parfois donner des resultats bizarres.
rng = np.random.default_rng(42)
# Score de satisfaction client avant et apres un changement d'interface
n_clients = 40
avant = rng.integers(1, 10, size=n_clients)
# Le changement a un effet positif, mais les donnees sont discretes (non normales)
apres = avant + rng.choice([-1, 0, 1, 2, 3], size=n_clients, p=[0.1, 0.2, 0.3, 0.25, 0.15])
stat, p_value = stats.wilcoxon(avant, apres, alternative='two-sided')
print("=== Test de Wilcoxon signe ===")
print(f"Mediane avant : {np.median(avant):.1f}")
print(f"Mediane apres : {np.median(apres):.1f}")
print(f"Statistique W : {stat:.1f}")
print(f"p-value : {p_value:.4f}")
if p_value < 0.05:
print("Conclusion : Le changement d'interface a un effet significatif sur la satisfaction.")
else:
print("Conclusion : Pas d'effet significatif detecte.")
Test de Kruskal-Wallis (kruskal)
Le test de Kruskal-Wallis est l'alternative non parametrique a l'ANOVA. Il compare les distributions de trois groupes ou plus sans hypothese de normalite. Comme l'ANOVA, un resultat significatif vous dit qu'au moins un groupe differe — mais pas lequel. Meme frustration, memes solutions post-hoc.
rng = np.random.default_rng(42)
# Temps de chargement de pages web pour 3 navigateurs (distribution asymetrique)
chrome = rng.exponential(scale=1.2, size=100) + 0.5
firefox = rng.exponential(scale=1.5, size=100) + 0.5
safari = rng.exponential(scale=1.0, size=100) + 0.5
stat, p_value = stats.kruskal(chrome, firefox, safari)
print("=== Test de Kruskal-Wallis ===")
print(f"Mediane Chrome : {np.median(chrome):.2f} s")
print(f"Mediane Firefox : {np.median(firefox):.2f} s")
print(f"Mediane Safari : {np.median(safari):.2f} s")
print(f"Statistique H : {stat:.4f}")
print(f"p-value : {p_value:.4f}")
if p_value < 0.05:
print("Conclusion : Au moins un navigateur a des temps de chargement significativement differents.")
else:
print("Conclusion : Pas de difference significative entre les navigateurs.")
Pour les comparaisons post-hoc non parametriques, vous pouvez utiliser le test de Dunn (disponible dans le package scikit-posthocs, qui vaut le detour) ou effectuer des comparaisons paire a paire avec Mann-Whitney en appliquant une correction de Bonferroni.
Tests d'Independance et de Correlation
Au-dela de la comparaison de groupes, une question qui revient tout le temps en analyse de donnees : "ces deux variables sont-elles liees ?". Les tests d'independance et les mesures de correlation repondent a cette question, chacun a sa maniere selon la nature des donnees et le type de relation qu'on cherche.
Test du Chi-deux d'independance (chi2_contingency)
Le test du Chi-deux sert a tester l'independance entre deux variables categoriques. Il compare les frequences observees dans un tableau de contingence aux frequences attendues sous l'hypothese d'independance. C'est le test classique pour des questions du genre "le choix du navigateur est-il lie au systeme d'exploitation ?" ou "la categorie de produit affecte-t-elle le taux de retour ?".
rng = np.random.default_rng(42)
# Tableau de contingence : preference de langage selon le role
# Rows: Data Scientist, Backend Dev, Frontend Dev
# Cols: Python, JavaScript, Go, Rust
observe = np.array([
[120, 15, 10, 8], # Data Scientist
[45, 30, 40, 25], # Backend Dev
[10, 85, 5, 15], # Frontend Dev
])
chi2, p_value, ddl, attendu = stats.chi2_contingency(observe)
print("=== Test du Chi-deux d'independance ===")
print(f"Statistique Chi2 : {chi2:.4f}")
print(f"Degres de liberte: {ddl}")
print(f"p-value : {p_value:.6f}")
print(f"\nFrequences attendues sous H0 :")
print(np.round(attendu, 1))
if p_value < 0.05:
print("\nConclusion : Il existe une association significative entre le role et le langage prefere.")
else:
print("\nConclusion : Pas d'association significative detectee.")
Sans surprise, la p-value est extremement faible — les preferences de langages de programmation dependent fortement du role. C'est intuitivement logique (essayez de convaincre un data scientist d'abandonner Python...). Pour quantifier la force de l'association, vous pouvez calculer le V de Cramer a partir de la statistique Chi-deux.
Correlation de Pearson (pearsonr)
Le coefficient de correlation de Pearson mesure la force et la direction d'une relation lineaire entre deux variables continues. Il varie de -1 (correlation negative parfaite) a +1 (correlation positive parfaite), 0 indiquant l'absence de relation lineaire.
Un point crucial qu'on oublie souvent : une correlation de Pearson nulle n'implique pas l'absence de toute relation. Seulement l'absence de relation lineaire. Deux variables peuvent etre parfaitement liees par une parabole et avoir un Pearson de zero.
rng = np.random.default_rng(42)
# Relation entre heures d'etude et score a un examen
n = 100
heures_etude = rng.uniform(1, 20, size=n)
score = 40 + 2.5 * heures_etude + rng.normal(0, 8, size=n)
score = np.clip(score, 0, 100)
r, p_value = stats.pearsonr(heures_etude, score)
print("=== Correlation de Pearson ===")
print(f"Coefficient r : {r:.4f}")
print(f"p-value : {p_value:.6f}")
print(f"R-carre : {r**2:.4f} ({r**2*100:.1f}% de variance expliquee)")
if p_value < 0.05:
print(f"Conclusion : Correlation lineaire significative (r = {r:.3f}).")
else:
print("Conclusion : Pas de correlation lineaire significative.")
Correlation de Spearman (spearmanr)
La correlation de Spearman, c'est la version non parametrique de Pearson. Elle mesure la force d'une relation monotone (pas necessairement lineaire) entre deux variables. Elle fonctionne sur les rangs, ce qui la rend robuste aux valeurs aberrantes et applicable aux donnees ordinales. Dans mon experience, c'est souvent un meilleur choix par defaut que Pearson quand on ne connait pas bien la forme de la relation.
rng = np.random.default_rng(42)
# Relation non lineaire mais monotone : experience et salaire
n = 80
experience = rng.uniform(0, 30, size=n)
# Le salaire croit de maniere non lineaire avec l'experience (rendements decroissants)
salaire = 30000 + 15000 * np.log1p(experience) + rng.normal(0, 5000, size=n)
r_pearson, p_pearson = stats.pearsonr(experience, salaire)
r_spearman, p_spearman = stats.spearmanr(experience, salaire)
print("=== Pearson vs Spearman ===")
print(f"Pearson : r = {r_pearson:.4f}, p = {p_pearson:.6f}")
print(f"Spearman : rho = {r_spearman:.4f}, p = {p_spearman:.6f}")
print(f"\nSpearman capture mieux la relation non lineaire monotone.")
print(f"Difference : |rho - r| = {abs(r_spearman - r_pearson):.4f}")
Coefficient Xi de Chatterjee (chatterjeexi) — Nouveau dans SciPy 1.15
Alors la, on arrive a un truc vraiment cool. Le coefficient Xi de Chatterjee est arrive avec SciPy 1.15, et c'est le genre d'outil qui vous fait dire "mais pourquoi ca n'existait pas avant ?". Contrairement a Pearson (lineaire) et Spearman (monotone), le coefficient Xi peut detecter n'importe quel type de dependance fonctionnelle, y compris les relations non monotones. Sa valeur est proche de 0 pour des variables independantes et tend vers 1 quand Y est une fonction deterministe de X — quelle que soit la forme de cette fonction.
C'est un outil precieux pour l'exploration de donnees. Avant meme de choisir un modele, vous pouvez utiliser Xi pour detecter si une variable explicative a un lien fonctionnel avec la cible, meme si ce lien est en forme de U, de parabole ou de sinusoide.
rng = np.random.default_rng(42)
n = 500
# Cas 1 : relation lineaire
x1 = rng.uniform(-5, 5, size=n)
y1_lineaire = 2 * x1 + rng.normal(0, 1, size=n)
# Cas 2 : relation parabolique (non monotone)
x2 = rng.uniform(-5, 5, size=n)
y2_parabole = x2 ** 2 + rng.normal(0, 2, size=n)
# Cas 3 : independance totale
x3 = rng.uniform(-5, 5, size=n)
y3_bruit = rng.normal(0, 1, size=n)
# Calcul du coefficient Xi de Chatterjee
res_lin = stats.chatterjeexi(x1, y1_lineaire)
res_par = stats.chatterjeexi(x2, y2_parabole)
res_ind = stats.chatterjeexi(x3, y3_bruit)
print("=== Coefficient Xi de Chatterjee ===")
print(f"Relation lineaire : Xi = {res_lin.statistic:.4f}, p-value = {res_lin.pvalue:.4f}")
print(f"Relation parabolique : Xi = {res_par.statistic:.4f}, p-value = {res_par.pvalue:.4f}")
print(f"Independance : Xi = {res_ind.statistic:.4f}, p-value = {res_ind.pvalue:.4f}")
# Comparaison avec Pearson et Spearman sur la parabole
r_p, _ = stats.pearsonr(x2, y2_parabole)
r_s, _ = stats.spearmanr(x2, y2_parabole)
print(f"\nSur la relation parabolique :")
print(f" Pearson : r = {r_p:.4f} (pres de 0 — ne detecte pas la relation)")
print(f" Spearman : rho = {r_s:.4f} (pres de 0 — ne detecte pas non plus)")
print(f" Chatterjee Xi = {res_par.statistic:.4f} (detecte la dependance fonctionnelle)")
Ce resultat illustre parfaitement la force de Xi. La ou Pearson et Spearman sont completement aveugles a une relation en U (parce qu'elle n'est ni lineaire ni monotone), Chatterjee Xi la detecte sans broncher. Integrez cet outil dans votre phase exploratoire — j'ai decouvert grace a lui des relations dans des datasets que j'aurais autrement completement ratees.
Techniques Avancees : Bootstrap, Permutation et Monte Carlo
Les tests classiques reposent sur des hypotheses distributionnelles (normalite, egalite des variances) et des formules analytiques. Mais soyons realistes : dans beaucoup de situations, ces hypotheses sont douteuses, voire carrement fausses. Les methodes de reechantillonnage — bootstrap, permutation et Monte Carlo — offrent une alternative elegante. Au lieu de supposer une distribution theorique, elles construisent empiriquement la distribution d'echantillonnage a partir de vos propres donnees.
SciPy propose des implementations matures de ces methodes, avec le fameux parametre rng introduit dans la version 1.15.
Bootstrap avec scipy.stats.bootstrap
Le bootstrap, c'est un peu la methode couteau-suisse. Il construit un intervalle de confiance pour n'importe quelle statistique (moyenne, mediane, ecart-type, ratio, ce que vous voulez) en tirant des milliers d'echantillons avec remise a partir de vos donnees. C'est particulierement utile quand il n'existe pas de formule analytique pour l'intervalle de confiance de la statistique qui vous interesse — et ca arrive plus souvent qu'on ne le croit.
rng = np.random.default_rng(42)
# Temps de reponse d'un service (distribution asymetrique)
temps = rng.exponential(scale=3, size=200) + 1
# Intervalle de confiance a 95% pour la mediane par bootstrap
result = stats.bootstrap(
data=(temps,),
statistic=np.median,
n_resamples=10000,
confidence_level=0.95,
rng=rng,
method='BCa' # Bias-corrected and accelerated — recommande
)
print("=== Bootstrap : Intervalle de confiance pour la mediane ===")
print(f"Mediane observee : {np.median(temps):.3f} s")
print(f"IC 95% (BCa bootstrap): [{result.confidence_interval.low:.3f}, {result.confidence_interval.high:.3f}]")
print(f"Erreur standard : {result.standard_error:.3f}")
# Bootstrap pour une statistique personnalisee : coefficient de variation
def coeff_variation(x, axis):
return np.std(x, axis=axis) / np.mean(x, axis=axis)
result_cv = stats.bootstrap(
data=(temps,),
statistic=coeff_variation,
n_resamples=10000,
confidence_level=0.95,
rng=rng
)
print(f"\nCoefficient de variation : {coeff_variation(temps, axis=0):.4f}")
print(f"IC 95% : [{result_cv.confidence_interval.low:.4f}, {result_cv.confidence_interval.high:.4f}]")
Test de permutation avec scipy.stats.permutation_test
Le test de permutation, c'est une approche non parametrique pour tester une hypothese sans aucune supposition distributionnelle. Le principe est d'une simplicite desarmante : sous H0 (pas de difference entre les groupes), les etiquettes de groupe sont interchangeables. Alors on les permute aleatoirement des milliers de fois, on recalcule la statistique a chaque fois, et on regarde ou se situe notre statistique observee dans cette distribution. Elegant, non ?
rng = np.random.default_rng(42)
# Duree de session (en minutes) — deux versions d'une application
version_a = rng.exponential(scale=5, size=60) + 2
version_b = rng.exponential(scale=7, size=60) + 2
# Statistique : difference des medianes
def diff_medianes(x, y, axis):
return np.median(x, axis=axis) - np.median(y, axis=axis)
result = stats.permutation_test(
data=(version_a, version_b),
statistic=diff_medianes,
n_resamples=9999,
alternative='two-sided',
rng=rng
)
print("=== Test de permutation ===")
print(f"Mediane version A : {np.median(version_a):.2f} min")
print(f"Mediane version B : {np.median(version_b):.2f} min")
print(f"Difference observee : {result.statistic:.2f} min")
print(f"p-value (permutation) : {result.pvalue:.4f}")
if result.pvalue < 0.05:
print("Conclusion : Difference significative de duree de session entre les versions.")
else:
print("Conclusion : Pas de difference significative detectee.")
Methode Monte Carlo (MonteCarloMethod)
SciPy 1.15 a aussi introduit MonteCarloMethod comme alternative a PermutationMethod. La difference fondamentale : Monte Carlo genere des echantillons aleatoires a partir de la distribution nulle specifiee, tandis que la permutation rearrange les donnees existantes. Monte Carlo est particulierement utile quand l'espace de permutation est trop grand pour etre explore raisonnablement, ou quand vous avez besoin de tester sous un modele nul specifique.
rng = np.random.default_rng(42)
# Exemple : tester si la correlation observee est significative
# en utilisant MonteCarloMethod
x = rng.normal(0, 1, size=50)
y = 0.3 * x + rng.normal(0, 1, size=50)
result = stats.pearsonr(x, y)
print("=== Correlation avec methode Monte Carlo ===")
print(f"Correlation de Pearson : r = {result.statistic:.4f}")
print(f"p-value (analytique) : {result.pvalue:.4f}")
# Utilisation de MonteCarloMethod via ttest_ind
groupe_1 = rng.normal(loc=100, scale=15, size=40)
groupe_2 = rng.normal(loc=108, scale=15, size=40)
result_mc = stats.ttest_ind(
groupe_1, groupe_2,
method=stats.MonteCarloMethod(n_resamples=9999, rng=rng)
)
print(f"\nTest t avec Monte Carlo :")
print(f"Statistique t : {result_mc.statistic:.4f}")
print(f"p-value (Monte Carlo) : {result_mc.pvalue:.4f}")
Un mot sur le parametre rng : dans toutes les methodes de reechantillonnage de SciPy 1.15+, le parametre rng accepte un generateur NumPy (np.random.default_rng(42)). C'est la maniere recommandee de garantir la reproductibilite. Oubliez np.random.seed() qui affecte tout le programme globalement et peut creer des interactions vraiment penibles entre modules — j'ai passe un apres-midi entier a debugger un probleme cause par ca une fois. Plus jamais.
Bonnes Pratiques et Pieges a Eviter
Connaitre les fonctions de SciPy, c'est bien. Savoir les utiliser correctement, c'est mieux. Cette section rassemble les conseils pratiques et les erreurs les plus frequentes qu'on observe dans des projets reels — y compris certaines que j'ai commises moi-meme a mes debuts.
Le probleme des comparaisons multiples (correction de Bonferroni)
Celui-la, c'est un piege classique. Si vous effectuez 20 tests statistiques avec alpha = 0.05, vous avez environ 64% de chances d'obtenir au moins un faux positif, meme en l'absence de tout effet reel. Faites le calcul : 1 - (0.95)^20 = 0.64. Ca fait reflechir.
C'est le probleme des comparaisons multiples, et il est omnipresent. Chaque fois que vous testez plusieurs hypotheses sur le meme jeu de donnees, le risque de type I s'accumule sournoisement.
from scipy.stats import false_discovery_control
# Simulation : 20 tests, dont seulement 3 ont un effet reel
rng = np.random.default_rng(42)
p_values = []
n_tests = 20
for i in range(n_tests):
if i < 3:
# Vrai effet : groupes avec moyennes differentes
a = rng.normal(loc=100, scale=10, size=50)
b = rng.normal(loc=108, scale=10, size=50)
else:
# Pas d'effet : meme distribution
a = rng.normal(loc=100, scale=10, size=50)
b = rng.normal(loc=100, scale=10, size=50)
_, p = stats.ttest_ind(a, b)
p_values.append(p)
p_values = np.array(p_values)
# Correction de Bonferroni : multiplier chaque p-value par le nombre de tests
p_bonferroni = np.minimum(p_values * n_tests, 1.0)
# Correction de Benjamini-Hochberg (controle du FDR)
p_bh = false_discovery_control(p_values, method='bh')
print("=== Comparaisons multiples ===")
print(f"{'Test':<8} {'p brute':<12} {'Bonferroni':<12} {'B-H (FDR)':<12} {'Vrai effet?'}")
print("-" * 56)
for i in range(n_tests):
vrai = "Oui" if i < 3 else "Non"
print(f"Test {i+1:<3} {p_values[i]:<12.4f} {p_bonferroni[i]:<12.4f} {p_bh[i]:<12.4f} {vrai}")
La correction de Bonferroni est la plus conservative : elle divise effectivement alpha par le nombre de tests. Ca marche, mais c'est brutal. La methode de Benjamini-Hochberg est moins stricte et controle le taux de fausses decouvertes (FDR) plutot que le taux d'erreur familial — souvent un bien meilleur compromis pour les analyses exploratoires ou on ne veut pas rater des vrais effets.
Taille d'effet : le d de Cohen
La p-value vous dit si un effet existe. Mais elle ne vous dit absolument rien sur sa taille. Et ca, c'est un probleme. Avec un echantillon suffisamment grand, meme une difference de 0.1 milliseconde sera "statistiquement significative". Super, mais est-ce que ca a la moindre importance en pratique ?
C'est pour ca qu'il faut toujours accompagner vos tests d'une mesure de la taille d'effet.
def cohen_d(groupe1, groupe2):
"""Calcul du d de Cohen pour deux groupes independants."""
n1, n2 = len(groupe1), len(groupe2)
var1, var2 = np.var(groupe1, ddof=1), np.var(groupe2, ddof=1)
# Ecart-type combine (pooled)
s_pool = np.sqrt(((n1 - 1) * var1 + (n2 - 1) * var2) / (n1 + n2 - 2))
return (np.mean(groupe1) - np.mean(groupe2)) / s_pool
rng = np.random.default_rng(42)
# Petit effet mais significatif avec N = 10 000
grand_a = rng.normal(loc=100.0, scale=10, size=10000)
grand_b = rng.normal(loc=100.5, scale=10, size=10000)
_, p = stats.ttest_ind(grand_a, grand_b)
d = cohen_d(grand_a, grand_b)
print("=== Taille d'effet (d de Cohen) ===")
print(f"p-value : {p:.4f}")
print(f"d de Cohen : {abs(d):.4f}")
print(f"Interpretation : ", end="")
d_abs = abs(d)
if d_abs < 0.2:
print("Effet negligeable")
elif d_abs < 0.5:
print("Petit effet")
elif d_abs < 0.8:
print("Effet moyen")
else:
print("Grand effet")
print(f"\nMeme si la p-value est significative ({p:.4f}),")
print(f"l'effet est minuscule (d = {d_abs:.4f}) — difference de {abs(grand_a.mean()-grand_b.mean()):.2f} points.")
Considerations sur la taille d'echantillon
La taille de l'echantillon affecte directement la puissance de vos tests. Trop petit, vous risquez de ne pas detecter un effet reel. Trop grand, vous detecterez des effets insignifiants (on vient de le voir). Voici quelques regles empiriques que je garde toujours en tete :
- Test t : minimum 30 observations par groupe pour que l'approximation normale soit raisonnable. Pour detecter un petit effet (d = 0.2), comptez environ 400 observations par groupe. Oui, ca fait beaucoup.
- Chi-deux : chaque cellule du tableau de contingence doit contenir au moins 5 observations attendues. En dessous, passez au test exact de Fisher.
- Correlations : pour detecter une correlation de 0.3, il faut environ 85 observations. Pour une correlation de 0.1, environ 800.
- Tests non parametriques : prevoyez 15-20% d'observations en plus que pour les tests parametriques equivalents, car ils sont generalement moins puissants.
Erreurs courantes d'interpretation de la p-value
La p-value est probablement la statistique la plus mal interpretee en sciences. Et je dis ca sans exagerer — meme des chercheurs experimentes se trompent. Alors clarifions une bonne fois pour toutes :
- La p-value n'est PAS la probabilite que H0 soit vraie. C'est la probabilite d'observer des donnees aussi extremes, si H0 est vraie. La nuance est fondamentale.
- p = 0.05 n'est pas un seuil magique. La difference entre p = 0.049 et p = 0.051 est negligeable. Evitez le raisonnement binaire "significatif / non significatif" — rapportez la p-value exacte et la taille d'effet. Vos reviewers vous remercieront.
- Une p-value significative ne prouve pas la causalite. Correlation n'est pas causalite, et la significativite statistique ne change pas cette realite. J'ai vu trop de presentations ou on concluait a un lien causal a partir d'une simple correlation significative.
- "Non significatif" ne signifie pas "pas d'effet". Ca signifie seulement que vos donnees ne fournissent pas assez de preuves pour conclure. Peut-etre que l'echantillon etait tout simplement trop petit.
Tableau recapitulatif : quel test utiliser ?
# Resume sous forme de tableau (a copier dans vos notes)
tableau = """
| Objectif | Parametrique | Non Parametrique |
|---------------------------------|----------------------|------------------------|
| 1 echantillon vs valeur ref. | ttest_1samp | wilcoxon |
| 2 groupes independants | ttest_ind | mannwhitneyu |
| 2 mesures appariees | ttest_rel | wilcoxon |
| 3+ groupes independants | f_oneway (ANOVA) | kruskal |
| Independance (categoriques) | chi2_contingency | fisher_exact (2x2) |
| Correlation lineaire | pearsonr | — |
| Correlation monotone | — | spearmanr |
| Dependance fonctionnelle | — | chatterjeexi |
| IC pour n'importe quelle stat. | — | bootstrap |
| Test sans hypothese distrib. | — | permutation_test |
"""
print(tableau)
FAQ — Questions Frequentes
Quelle est la difference entre un test parametrique et non parametrique ?
En gros, un test parametrique suppose que vos donnees suivent une distribution specifique (generalement la loi normale) et utilise les parametres de cette distribution pour calculer la statistique de test. Exemples : test t, ANOVA, Pearson. Un test non parametrique ne fait aucune hypothese sur la distribution sous-jacente — il travaille generalement sur les rangs plutot que sur les valeurs brutes. Exemples : Mann-Whitney, Wilcoxon, Kruskal-Wallis, Spearman. En pratique, utilisez un test parametrique quand vos donnees sont approximativement normales (verifiez avec Shapiro-Wilk et un QQ-plot) et un test non parametrique dans le cas contraire. Et quand vous avez un doute — ce qui arrive souvent — le test non parametrique est le choix le plus sur.
Comment interpreter une p-value en Python ?
La p-value retournee par les fonctions SciPy represente la probabilite d'observer un resultat au moins aussi extreme que celui calcule, sous l'hypothese que H0 est vraie. Si p_value < alpha (generalement 0.05), on rejette H0 — les donnees suggerent un effet significatif. Si p_value >= alpha, on ne rejette pas H0 — les donnees ne fournissent pas assez de preuves. Mon conseil : rapportez toujours la p-value exacte (pas juste "p < 0.05") et accompagnez-la d'une mesure de taille d'effet. Sans taille d'effet, une p-value toute seule ne raconte que la moitie de l'histoire.
Quel test statistique choisir pour comparer deux groupes ?
Ca depend de deux choses : (1) les groupes sont-ils independants ou apparies ? et (2) les donnees sont-elles normalement distribuees ? Pour deux groupes independants avec donnees normales, utilisez stats.ttest_ind(). Pour des donnees non normales ou ordinales, stats.mannwhitneyu(). Pour des mesures appariees (memes sujets, avant/apres), stats.ttest_rel() si les differences sont normales, ou stats.wilcoxon() sinon. Et si vous n'etes pas sur de rien (ca arrive), la methode par permutation (method=stats.PermutationMethod()) dans ttest_ind est un excellent compromis — elle ne fait aucune hypothese distributionnelle et reste puissante.
Le test de Shapiro-Wilk est-il fiable pour les grands echantillons ?
Question piege ! Shapiro-Wilk est tres puissant pour les petits echantillons (n < 50), mais il devient paradoxalement trop sensible pour les grands (n > 5 000). Avec beaucoup de donnees, il rejettera presque systematiquement la normalite pour des ecarts minuscules qui n'ont aucune consequence pratique. Pour les grands echantillons, privilegiez la verification visuelle (QQ-plot, histogramme) et le bon sens : le theoreme central limite fait son travail des que n depasse environ 30. En resume : Shapiro-Wilk pour les petits echantillons, QQ-plot pour les grands. Et dans tous les cas, ne paniquez pas si la normalite est rejetee — demandez-vous plutot si l'ecart est suffisant pour affecter vos conclusions.
Comment gerer les comparaisons multiples avec SciPy ?
SciPy fournit scipy.stats.false_discovery_control() pour appliquer la correction de Benjamini-Hochberg (controle du FDR). Pour Bonferroni, c'est plus simple : il suffit de multiplier chaque p-value par le nombre de tests (ou de diviser alpha par ce nombre). En pratique, des que vous effectuez plus de 5 tests sur le meme jeu de donnees, appliquez une correction. C'est non negociable. Bonferroni est la plus conservative — moins de faux positifs, mais plus de faux negatifs. Benjamini-Hochberg est generalement preferee dans les analyses exploratoires car elle est moins punitive tout en controlant le taux de fausses decouvertes. Pour les comparaisons post-hoc apres une ANOVA, stats.tukey_hsd() integre deja la correction — une chose en moins a se soucier.