Statistique inférentielle : estimer, tester, décider
Un modèle donne des nombres ; la statistique inférentielle dit lesquels sont crédibles. C'est ce qui sépare « la variante B a fait +2,3 % » de « la variante B est meilleure ». Ce module traite les p-values et les tests d'hypothèse pour ce qu'ils sont — des outils précis, très souvent mal interprétés — puis la question qui compte vraiment : quand a-t-on le droit de parler de cause ?
📋 Au programme
1. Échantillon, estimateur, variabilité
On veut connaître une quantité \(\theta\) d'une population — une moyenne, une proportion, un écart — mais on n'observe qu'un échantillon de taille \(n\). On calcule alors un estimateur \(\hat\theta\), et voici le point central : cet estimateur est lui-même une variable aléatoire. Un autre échantillon aurait donné une autre valeur. Toute la statistique inférentielle consiste à quantifier cette variabilité, pour savoir ce que l'on peut affirmer malgré elle.
import numpy as np
rng = np.random.default_rng(0)
# une population dont on connaît la vérité : moyenne 100, écart type 15
population = rng.normal(100, 15, size=1_000_000)
# on tire 10 000 échantillons de 30 individus et on regarde varier LA MOYENNE
moyennes = np.array([rng.choice(population, 30).mean() for _ in range(10_000)])
print(f"moyenne des moyennes : {moyennes.mean():.2f}") # ≈ 100 → sans biais
print(f"dispersion observée : {moyennes.std():.2f}") # ≈ 2.74
print(f"prédiction théorique : {15 / np.sqrt(30):.2f}") # ≈ 2.74 ✓
print(f"étendue : {moyennes.min():.1f} à {moyennes.max():.1f}")
# ≈ 89 à 111 : avec 30 individus, une moyenne d'échantillon peut s'écarter
# de 10 points de la vérité, sans que rien ne soit anormal.Deux résultats suffisent à presque tout. La loi des grands nombres garantit que \(\bar X_n \to \mu\). Le théorème central limite précise à quelle vitesse et sous quelle forme :
Sa portée est remarquable : quelle que soit la forme de la distribution d'origine — asymétrique, discrète, bimodale —, la moyenne d'un échantillon suffisamment grand suit approximativement une loi normale. C'est ce qui rend les outils qui suivent applicables sans connaître la loi des données.
L'erreur type \(\sigma/\sqrt{n}\) décroît en \(1/\sqrt{n}\) : diviser l'incertitude par deux demande quatre fois plus de données, par dix en demande cent fois plus. C'est la contrainte économique de fond de toute expérimentation, et elle explique pourquoi les gains de précision deviennent vite hors de prix.
Deux distinctions à ne pas confondre
Écart type (dispersion des individus) contre erreur type (dispersion de l'estimateur) : le premier ne diminue pas quand \(n\) augmente — les gens ne se ressemblent pas davantage parce qu'on en observe plus —, le second si. Biais (l'estimateur vise-t-il juste en moyenne ?) contre variance (est-il stable ?) : un estimateur légèrement biaisé mais de faible variance bat souvent un estimateur sans biais et instable. C'est exactement l'argument de la régularisation (module 14).
2. Intervalles de confiance
Plutôt qu'une valeur unique, on donne un intervalle qui reflète l'incertitude. Pour une moyenne :
La loi de Student remplace la loi normale parce que \(\sigma\) est lui-même estimé par \(s\), ce qui ajoute de l'incertitude ; l'écart n'est appréciable qu'en dessous de \(n \approx 30\), au-delà les deux coïncident presque.
from scipy import stats
couverts = 0
for _ in range(10_000):
ech = rng.normal(100, 15, size=30)
bas, haut = stats.t.interval(0.95, df=29, loc=ech.mean(),
scale=stats.sem(ech))
couverts += (bas <= 100 <= haut) # la vraie valeur est-elle dedans ?
print(f"couverture réelle : {couverts / 10_000:.1%}") # ≈ 95,0 % ✓Pour une proportion, l'intervalle normal usuel \(\hat p \pm z\sqrt{\hat p(1-\hat p)/n}\) se comporte mal quand \(\hat p\) est proche de 0 ou 1 : il peut sortir de l'intervalle [0, 1], et sa couverture réelle chute bien en dessous de 95 %. L'intervalle de Wilson corrige cela et devrait être le choix par défaut :
from statsmodels.stats.proportion import proportion_confint proportion_confint(count=7, nobs=100, method="wilson") # (0.034, 0.139) proportion_confint(count=7, nobs=100, method="normal") # (0.020, 0.120) — trop étroit
3. Le bootstrap
Pour une médiane, un coefficient de Gini, un R², un ratio de métriques — bref dès que la loi de l'estimateur n'a pas de forme analytique commode — il n'existe pas de formule d'intervalle. Le bootstrap résout cela par une idée d'une simplicité déconcertante : l'échantillon est notre meilleure approximation de la population, alors tirons dedans avec remise et regardons varier la statistique. On simule ainsi la variabilité d'échantillonnage sans nouvelle collecte.
import numpy as np
from scipy import stats
rng = np.random.default_rng(0)
x = rng.lognormal(mean=1.0, sigma=1.2, size=200) # distribution très asymétrique
# ── bootstrap "à la main", pour voir le mécanisme ──
idx = rng.integers(0, len(x), size=(10_000, len(x))) # 10 000 rééchantillonnages
medianes = np.median(x[idx], axis=1) # la statistique sur chacun
ic_percentile = np.percentile(medianes, [2.5, 97.5]) # l'intervalle empirique
# ── version scipy, avec correction BCa (biais + asymétrie) : plus fiable ──
res = stats.bootstrap((x,), np.median, confidence_level=0.95,
n_resamples=10_000, method="BCa", random_state=0)
print(ic_percentile, res.confidence_interval)Le bootstrap est d'une généralité rare, mais il a ses limites : il suppose des observations indépendantes (pour des séries temporelles, il faut un bootstrap par blocs — module 17), il est mauvais pour les statistiques d'extrêmes (un maximum ne se rééchantillonne pas bien, puisqu'aucun tirage ne peut dépasser la valeur observée), et il ne crée pas d'information : sur douze observations, il reste douze observations.
4. Tests d'hypothèse et p-value
La démarche est celle d'un raisonnement par l'absurde probabiliste :
- poser une hypothèse nulle \(H_0\) — « aucun effet », « les deux moyennes sont égales » ;
- choisir une statistique de test \(T\) dont on connaît la loi sous \(H_0\) ;
- calculer sa valeur \(t_{\text{obs}}\) sur les données observées ;
- évaluer à quel point cette valeur serait surprenante si \(H_0\) était vraie.
Cette dernière quantité est la p-value :
# Situation : groupe A (n=40) et groupe B (n=40), on observe un écart de moyennes.
ecart_observe = b.mean() - a.mean() # par exemple 1,8
# Sous H0, l'appartenance au groupe n'a AUCUN effet : les étiquettes sont
# interchangeables. On les mélange 100 000 fois pour voir quels écarts
# le hasard seul produit.
ensemble = np.concatenate([a, b])
ecarts_h0 = np.empty(100_000)
for i in range(100_000):
melange = rng.permutation(ensemble)
ecarts_h0[i] = melange[40:].mean() - melange[:40].mean()
p = np.mean(np.abs(ecarts_h0) >= abs(ecart_observe))
print(f"p-value (test de permutation) = {p:.4f}")
# La p-value est LITTÉRALEMENT la proportion de mondes "sans effet" dans lesquels
# le hasard produirait un écart au moins aussi grand que celui observé.Ce test de permutation est intéressant à double titre : il ne suppose aucune loi particulière, et il rend la définition de la p-value concrète — c'est une fréquence, comptée dans un monde où l'hypothèse nulle serait vraie.
• Ce n'est pas la probabilité que \(H_0\) soit vraie. Elle est calculée en supposant \(H_0\) vraie : c'est \(P(\text{données}\mid H_0)\), jamais \(P(H_0\mid\text{données})\). Confondre les deux, c'est confondre « la probabilité d'avoir de la fièvre quand on a la grippe » et « la probabilité d'avoir la grippe quand on a de la fièvre ».
• Ce n'est pas une mesure de la taille de l'effet. Avec \(n = 10^6\), une différence de 0,01 % devient « hautement significative » sans avoir le moindre intérêt pratique.
• \(p \gt 0{,}05\) ne prouve pas \(H_0\). Absence de preuve n'est pas preuve d'absence : c'est très souvent un simple manque de puissance.
• Le seuil de 0,05 est une convention de Fisher, pas une loi de la nature. Rien ne distingue \(p = 0{,}049\) de \(p = 0{,}051\).
Le réflexe sain consiste à toujours rapporter trois choses ensemble : la taille d'effet (l'écart observé, dans son unité métier), son intervalle de confiance, et la p-value. L'intervalle contient à lui seul presque toute l'information utile — un IC qui exclut zéro dit exactement la même chose qu'un test significatif, en plus informatif, puisqu'il indique aussi l'ordre de grandeur de l'effet.
5. Erreurs, puissance, taille d'échantillon
H0 vraie en réalité H0 fausse en réalité
Rejeter H0 ERREUR DE TYPE I (α) décision correcte
"fausse découverte" (puissance = 1 - β)
Ne pas rejeter H0 décision correcte ERREUR DE TYPE II (β)
"on est passé à côté"\(\alpha\) est choisi (5 % par convention) : c'est le taux de faux positifs qu'on accepte. \(\beta\) se subit, sauf si l'on dimensionne l'expérience à l'avance. La puissance \(1-\beta\) est la probabilité de détecter un effet qui existe réellement ; on la vise à 80 % ou 90 %.
Pour comparer deux moyennes, en notant \(d = |\mu_1-\mu_2|/\sigma\) la taille d'effet de Cohen (un écart exprimé en nombre d'écarts types), la taille par groupe nécessaire pour \(\alpha=5\%\) et une puissance de 80 % est bien approchée par :
Un effet « moyen » (\(d=0{,}5\)) demande donc environ 64 observations par groupe ; un petit effet (\(d=0{,}2\)) en demande 400 ; un très petit effet (\(d=0{,}05\)) en demande 6 400. Cette formule, faite de tête avant de lancer une expérience, évite de collecter des données qui n'avaient aucune chance de conclure.
from statsmodels.stats.power import TTestIndPower
n = TTestIndPower().solve_power(effect_size=0.3, alpha=0.05, power=0.8)
print(f"n = {np.ceil(n):.0f} par groupe") # 176
# vérification empirique : on simule 10 000 expériences de cette taille
n = 176
rejets = sum(
stats.ttest_ind(rng.normal(0, 1, n), rng.normal(0.3, 1, n)).pvalue < 0.05
for _ in range(10_000)
)
print(f"puissance simulée = {rejets / 10_000:.1%}") # ≈ 80 % ✓Le piège des études sous-dimensionnées
Une expérience à faible puissance ne se contente pas de « rater » des effets. Quand elle atteint quand même la significativité, c'est nécessairement parce que le hasard a gonflé l'estimation — sans quoi elle n'aurait pas franchi le seuil. Les effets publiés à partir de petits échantillons sont donc systématiquement surestimés (on parle d'erreur de type M, pour magnitude), et parfois même de signe inverse (type S). C'est l'un des moteurs de la crise de reproductibilité, et cela explique pourquoi tant de résultats spectaculaires ne se confirment pas à plus grande échelle.
6. Catalogue des tests usuels
Question Test Fonction
────────────────────────────────────────────────────────────────────────────────────
Moyenne = valeur de référence ? t à un échantillon stats.ttest_1samp
Deux groupes indépendants ? t de WELCH (défaut !) stats.ttest_ind(
equal_var=False)
Avant/après sur les mêmes sujets ? t apparié stats.ttest_rel
Idem, sans hypothèse de normalité ? Mann-Whitney / Wilcoxon stats.mannwhitneyu
Plus de deux groupes ? ANOVA / Kruskal-Wallis stats.f_oneway, kruskal
Deux variables catégorielles liées ? chi² d'indépendance stats.chi2_contingency
Idem, petits effectifs (< 5) ? test exact de Fisher stats.fisher_exact
Deux proportions (A/B test) ? test z de proportions proportions_ztest
Même distribution ? Kolmogorov-Smirnov stats.ks_2samp
Distribution normale ? Shapiro-Wilk stats.shapiro
Corrélation linéaire ? monotone ? Pearson / Spearman stats.pearsonr, spearmanrTrois remarques valent mieux qu'un long tableau.
Le test de Welch doit être le défaut pour comparer deux groupes : il ne suppose pas l'égalité des variances, et il ne perd presque rien en puissance quand celles-ci sont effectivement égales. Le test de Student classique, lui, donne des résultats faux quand les variances diffèrent et que les groupes sont de tailles inégales — situation courante.
Les tests de normalité sont contre-productifs sur de grands échantillons : ils rejettent systématiquement pour des écarts sans conséquence pratique, alors que le TCL rend justement le test de Student robuste à la non-normalité dès que \(n\) dépasse quelques dizaines. Un histogramme et un QQ-plot informent mieux qu'une p-value de Shapiro.
Enfin, les tests non paramétriques ne comparent pas des moyennes mais des rangs. Mann-Whitney répond à « une valeur tirée de A a-t-elle tendance à dépasser une valeur tirée de B ? », ce qui n'est pas la même question qu'une différence de moyennes — deux distributions de moyennes identiques mais de formes différentes peuvent donner un résultat significatif.
7. Comparaisons multiples et p-hacking
Tester 20 hypothèses au seuil de 5 % alors qu'aucune n'est vraie donne une probabilité \(1 - 0{,}95^{20} \approx 64\%\) d'obtenir au moins un « résultat significatif ». C'est mécanique, et c'est la raison pour laquelle explorer librement un jeu de données produit toujours quelque chose.
Bonferroni seuil α/m contrôle le FWER (probabilité d'AU MOINS
une fausse découverte). Simple, très conservateur.
Holm-Bonferroni séquentiel même garantie, uniformément plus puissant :
à préférer systématiquement à Bonferroni.
Benjamini-Hochberg contrôle le FDR accepte une PROPORTION q de faux parmi les
rejets. Le bon choix quand on teste des
centaines d'hypothèses (génomique, sélection
de variables).from statsmodels.stats.multitest import multipletests rejets, p_corriges, _, _ = multipletests(p_values, alpha=0.05, method="holm") rejets, p_corriges, _, _ = multipletests(p_values, alpha=0.05, method="fdr_bh")
La distinction entre les deux familles est importante. Bonferroni et Holm contrôlent la probabilité de commettre ne serait-ce qu'une fausse découverte : c'est adapté quand une erreur coûte cher (un essai clinique confirmatoire). Benjamini-Hochberg contrôle la proportion de fausses découvertes parmi les résultats déclarés positifs : sur 10 000 gènes testés, on accepte que 5 % des gènes retenus soient des faux positifs, ce qui est infiniment plus puissant et parfaitement raisonnable en phase exploratoire.
8. Corrélation, causalité, confondants
Une corrélation entre \(X\) et \(Y\) admet cinq explications : \(X\) cause \(Y\) ; \(Y\) cause \(X\) ; une variable \(Z\) cause les deux (confondant) ; l'échantillon a été sélectionné d'une façon qui crée l'association (biais de sélection) ; ou c'est le hasard. Rien dans les données seules ne permet de trancher — il faut une hypothèse sur le mécanisme, qui vient de la connaissance du domaine.
Le paradoxe de Simpson
Un traitement peut être meilleur dans chaque sous-groupe et pire globalement. L'exemple classique : un hôpital de pointe affiche une mortalité supérieure à celle d'une clinique de quartier, parce qu'il reçoit les cas graves. La gravité est un confondant, et l'agrégation inverse la conclusion. La leçon n'est pas « il faut toujours stratifier » — conditionner sur la mauvaise variable peut créer un biais, voir plus bas — mais « il faut un modèle causal explicite pour savoir sur quoi conditionner ».
L'outil formel est le graphe causal (DAG) : on y dessine les relations supposées, et des règles graphiques disent quelles variables ajuster. Trois structures élémentaires suffisent à comprendre l'essentiel.
CHAÎNE X ──► M ──► Y M est un MÉDIATEUR (l'effet passe par lui)
exemple : traitement → tension → infarctus
→ conditionner sur M SUPPRIME l'effet qu'on cherche à mesurer
FOURCHE X ◄── Z ──► Y Z est un CONFONDANT (cause commune)
exemple : ventes de glaces ◄ chaleur ► noyades
→ il FAUT conditionner sur Z (critère de la porte dérobée)
COLLISIONNEUR X ──► C ◄── Y C est un EFFET COMMUN
exemple : talent ► être recruté ◄ apparence
→ conditionner sur C CRÉE une corrélation artificielle entre X et Y
(parmi les recrutés, les moins talentueux sont les plus beaux :
c'est un artefact de sélection, pas une réalité)Le collisionneur est la source de biais la plus contre-intuitive, et la plus fréquente quand on « contrôle par tout ce qu'on a » : ajouter des variables à un modèle n'est pas neutre, cela peut fabriquer des associations qui n'existent pas.
La seule méthode qui élimine tous les confondants d'un coup, connus comme inconnus, est la randomisation : en tirant l'affectation au hasard, on rend le traitement indépendant de tout le reste, y compris de ce qu'on n'a pas mesuré. C'est pour cette raison que l'essai contrôlé randomisé — l'A/B test — est l'étalon-or.
Quand on ne peut pas randomiser, on s'appuie sur des méthodes quasi-expérimentales : différence-en-différences (deux groupes suivis avant et après), variables instrumentales, régression sur discontinuité, appariement sur score de propension. Toutes reposent sur des hypothèses non vérifiables à partir des données seules : elles doivent être argumentées explicitement, pas supposées silencieusement. Le module 11 est entièrement consacré à ces méthodes : ce qu'elles estiment exactement, ce qu'elles supposent, et comment vérifier ce qui est vérifiable.
9. Un A/B test de bout en bout
C'est l'exercice qui mobilise tout ce module. La séquence, dans l'ordre :
- Définir la métrique principale — une seule, décidée à l'avance. Les autres sont des garde-fous (métriques de non-régression) ou de l'exploration.
- Fixer l'effet minimal détectable — quel gain justifierait le changement ? Cette valeur vient du métier, pas des données.
- Dimensionner — en déduire \(n\) et la durée. Couvrir des semaines entières : le comportement du lundi n'est pas celui du samedi.
- Randomiser au bon niveau — par utilisateur, pas par session, sinon le même individu voit les deux variantes et les observations ne sont plus indépendantes.
- Vérifier l'équilibre — un test A/A (deux groupes identiques, qui ne doivent rien montrer) et un contrôle du ratio de répartition (sample ratio mismatch) détectent les bugs d'assignation, cause numéro un des résultats invraisemblables.
- Attendre la fin — regarder les résultats en continu et s'arrêter dès que \(p \lt 0{,}05\) fait exploser le taux de faux positifs bien au-delà de 5 %, parce qu'on multiplie les tests sans les compter. Si l'arrêt anticipé est nécessaire, il faut des méthodes conçues pour cela (tests séquentiels, bornes d'O'Brien-Fleming).
- Analyser — effet, intervalle de confiance, puis p-value.
import numpy as np
from statsmodels.stats.proportion import proportions_ztest, confint_proportions_2indep
conv = np.array([1183, 1279]) # conversions A, B
n = np.array([20_143, 20_089]) # visiteurs A, B
stat, p = proportions_ztest(conv, n)
lo, hi = confint_proportions_2indep(conv[1], n[1], conv[0], n[0], method="wald")
taux = conv / n
print(f"A = {taux[0]:.3%}, B = {taux[1]:.3%}")
print(f"écart absolu = {taux[1]-taux[0]:+.3%} IC95 = [{lo:+.3%}, {hi:+.3%}] p = {p:.4f}")
# A = 5.873%, B = 6.367%
# écart absolu = +0.494% IC95 = [+0.031%, +0.957%] p = 0.0366La lecture correcte de ce résultat : l'écart estimé est de +0,49 point, et les données sont compatibles avec un gain compris entre +0,03 et +0,96 point. C'est significatif au seuil de 5 %, mais l'intervalle frôle zéro — autrement dit, il reste possible que le vrai gain soit négligeable. La décision dépend donc du coût de mise en œuvre et du gain minimal jugé pertinent (fixé à l'étape 2), pas du seul verdict binaire du test.
Et l'approche bayésienne ?
Elle répond à la question qu'on se pose réellement : « quelle est la probabilité que B soit meilleure que A ? ». On pose une loi a priori sur les taux, on la met à jour par les données, et l'on obtient une loi a posteriori d'où se lisent directement \(P(p_B \gt p_A)\) et l'espérance de perte en cas de mauvais choix. Avantages : interprétation naturelle, arrêt anticipé sans pénalité formelle, intégration d'une connaissance préalable. Coûts : il faut assumer un a priori, et la communication d'un résultat bayésien exige un peu de pédagogie. Le module 10 le détaille, avec une implémentation en dix lignes.
Ce qu'il faut retenir du module
- Un estimateur est une variable aléatoire ; l'erreur type décroît en \(1/\sqrt{n}\), donc la précision coûte quadratiquement cher.
- Un IC porte sur la procédure, pas sur le paramètre ; Wilson pour les proportions, bootstrap quand il n'y a pas de formule.
- La p-value est \(P(\text{données}\mid H_0)\), rien d'autre. Rapporter effet + IC + p, dans cet ordre.
- Dimensionner avant de collecter : \(n \approx 16/d^2\). Une étude sous-puissante surestime les effets qu'elle détecte.
- Welch par défaut ; les tests de normalité sont inutiles sur grand échantillon.
- Corriger les comparaisons multiples : Holm pour quelques tests, Benjamini-Hochberg pour des centaines.
- Pas de causalité sans hypothèse causale : confondants à contrôler, collisionneurs à ne surtout pas contrôler, randomisation quand c'est possible.
- Dans un A/B test : une métrique, une durée fixée à l'avance, une randomisation par utilisateur, pas de coup d'œil anticipé.