← Retour au portfolio RSS

· 18 min de lecture

M9 · Statistique inférentielle pour comparer des modèles

Intervalles de confiance, bootstrap, McNemar, t corrigé et Kolmogorov-Smirnov : prouver qu'un modèle est vraiment meilleur qu'un autre, et détecter un drift.

Mastering AI · Required Maths · Semaine 3 (probabilités et statistiques) · Partie 9

Objectif de l’étape

« LightGBM obtient un F1 de 0,681, la régression logistique 0,582 : LightGBM est meilleur. » Cette phrase ne vaut rien tant qu’on ne sait pas si l’écart dépasse le bruit. Un score de test est une estimation calculée sur un échantillon fini : avec un autre jeu de test, il aurait été différent. Cet article donne les outils pour affirmer « A est meilleur que B » avec une rigueur défendable devant un jury de mémoire.

À la fin de cet article, tu sauras :

  • expliquer ce qu’est un estimateur, son biais et son erreur standard ;
  • construire un intervalle de confiance et dire ce que « 95 % » signifie vraiment ;
  • construire un intervalle par bootstrap pour n’importe quelle métrique (F1 compris) ;
  • formuler un test d’hypothèse et interpréter une p-valeur sans contresens ;
  • comparer deux modèles sur le même jeu de test (bootstrap apparié, McNemar) et sur plusieurs plis de validation croisée (t corrigé, 5x2cv, Wilcoxon) ;
  • détecter un drift avec Kolmogorov-Smirnov et te méfier des comparaisons multiples.

Notions clés

1. Estimateur, biais et erreur standard

Intuition. Le F1 que tu mesures sur ton jeu de test n’est pas « le F1 du modèle » : c’est une estimation de la performance qu’il aurait sur toutes les régions mémoire possibles. Change le jeu de test, et le chiffre bouge. L’erreur standard mesure de combien il bouge typiquement.

Un estimateur θ ^ \hat\theta est une fonction des données qui vise une quantité inconnue θ \theta . Deux propriétés comptent :

  • le biais 𝔼 [ θ ^ ] − θ \mathbb{E}[\hat\theta] - \theta : se trompe-t-il en moyenne ? ;
  • l’erreur standard S E ( θ ^ ) = V a r ( θ ^ ) \mathrm{SE}(\hat\theta) = \sqrt{\mathrm{Var}(\hat\theta)} : de combien fluctue-t-il d’un échantillon à l’autre ?

Pour une accuracy mesurée sur n n exemples, chaque prédiction est correcte ou non : c’est une Bernoulli de paramètre p p (M7). L’accuracy est leur moyenne, donc :

S E ( p ^ ) = p ( 1 − p ) n \mathrm{SE}(\hat p) = \sqrt{\frac{p(1 - p)}{n}}

Une accuracy de 0,80 mesurée sur 200 exemples a une erreur standard d’environ 0,028 : le deuxième chiffre après la virgule est déjà incertain.

2. L’intervalle de confiance, et ce qu’il signifie vraiment

Intuition. Plutôt qu’un chiffre, on donne une fourchette. Grâce au théorème central limite (M7), une moyenne suit approximativement une loi normale, et 95 % de la masse d’une loi normale tient à ± 1 , 96 \pm 1{,}96 écart-type de son centre :

x ¯ ± 1 , 96 s n \bar x \pm 1{,}96\,\frac{s}{\sqrt{n}}

où s s est l’écart-type empirique. Pour une proportion, s / n s/\sqrt n devient p ^ ( 1 − p ^ ) / n \sqrt{\hat p(1 - \hat p)/n} (intervalle de Wald).

Ce que « 95 % » veut dire. Ce n’est pas « il y a 95 % de chances que la vraie valeur soit dans cet intervalle ». La vraie valeur est fixe ; c’est l’intervalle qui est aléatoire. La phrase correcte : si on répétait l’expérience un grand nombre de fois, 95 % des intervalles construits ainsi contiendraient la vraie valeur. On le vérifie par simulation plus bas.

La largeur décroît en 1 / n 1/\sqrt n : pour diviser l’incertitude par 10, il faut 100 fois plus d’exemples de test.

3. Le bootstrap : des intervalles sans formule

Intuition. Pour le F1, l’AUC ou une médiane, il n’y a pas de formule simple d’erreur standard. Idée d’Efron (1979) : le jeu de test est notre meilleure image de la population. On rééchantillonne avec remise n n exemples parmi les n n du jeu de test, on recalcule la métrique, et on recommence B B fois (typiquement B = 1 000 B = 1\,000 à 10 000 10\,000 ). La dispersion des B B valeurs imite la dispersion qu’on aurait observée avec de nouveaux jeux de test.

L’intervalle percentile à 95 % est simplement :

[ q 2 , 5 % ( θ ^ 1 * , … , θ ^ B * ) , q 97 , 5 % ( θ ^ 1 * , … , θ ^ B * ) ] \Big[\, q_{2{,}5\,\%}\big(\hat\theta^{*}_1, \dots, \hat\theta^{*}_B\big),\ q_{97{,}5\,\%}\big(\hat\theta^{*}_1, \dots, \hat\theta^{*}_B\big) \,\Big]

Limite importante : le bootstrap suppose des exemples indépendants. Des agrégations successives d’une même région mémoire sont autocorrélées (M7) : il faut alors rééchantillonner par blocs (des régions entières, ou des tranches de temps) plutôt qu’exemple par exemple.

4. Tests d’hypothèse et p-valeur

Intuition. On joue l’avocat du diable : on suppose qu’il n’y a aucune différence (l’hypothèse nulle H 0 H_0 ) et on se demande si ce qu’on a observé serait surprenant dans ce monde-là.

  • La p-valeur est la probabilité, si H 0 H_0 est vraie, d’observer un résultat au moins aussi extrême que celui qu’on a obtenu.
  • Si p < α p < \alpha (souvent α = 0 , 05 \alpha = 0{,}05 ), on rejette H 0 H_0 .

Deux façons de se tromper :

H 0 H_0 vraie (pas de différence) H 0 H_0 fausse (vraie différence)
On rejette H 0 H_0 Erreur de type I (faux positif), probabilité α \alpha Bonne décision, probabilité = puissance 1 − β 1 - \beta
On ne rejette pas Bonne décision Erreur de type II (faux négatif), probabilité β \beta

Trois contresens à éviter :

  • la p-valeur n’est pas P ( H 0 ∣ données ) P(H_0 \mid \text{données}) . C’est P ( données au moins aussi extrêmes ∣ H 0 ) P(\text{données au moins aussi extrêmes} \mid H_0) : pour inverser la flèche, il faudrait Bayes et un a priori (M8) ;
  • « non significatif » ne veut pas dire « pas de différence » : peut-être que le test manquait de puissance (trop peu d’exemples) ;
  • « significatif » ne veut pas dire « important » : avec un million d’exemples, un écart de F1 de 0,001 devient significatif. Regarde toujours la taille de l’effet et son intervalle, pas seulement p p .

5. Comparer deux modèles sur le même jeu de test

Intuition. Les deux modèles sont évalués sur les mêmes exemples. Les exemples faciles sont réussis par les deux, les très difficiles ratés par les deux : ces exemples-là ne disent rien sur la différence. Il faut exploiter cet appariement, sinon on noie la différence dans le bruit commun.

Bootstrap apparié. À chaque rééchantillonnage, on tire les mêmes indices pour les deux modèles et on calcule directement Δ F 1 = F 1 B − F 1 A \Delta F_1 = F_1^{B} - F_1^{A} . Si l’intervalle de Δ F 1 \Delta F_1 exclut 0, la différence est significative.

Test de McNemar. On ne regarde que les désaccords : n 01 n_{01} exemples où A a raison et B tort, n 10 n_{10} où c’est l’inverse. Sous H 0 H_0 , chaque désaccord penche d’un côté ou de l’autre avec probabilité 1 / 2 1/2 , donc n 01 n_{01} suit une binomiale ℬ︀ ( n 01 + n 10 , 1 / 2 ) \mathcal{B}(n_{01} + n_{10}, 1/2) . Pour de grands effectifs, on utilise l’approximation :

χ 2 = ( | n 01 − n 10 | − 1 ) 2 n 01 + n 10 ∼ χ 1 2 \chi^2 = \frac{\big(|n_{01} - n_{10}| - 1\big)^2}{n_{01} + n_{10}} \sim \chi^2_1

C’est le test recommandé par Dietterich (1998) quand on ne peut entraîner chaque modèle qu’une fois.

Bootstrap apparié : distribution de ΔF1 sur 2 000 rééchantillonnages du jeu de test. LightGBM − LR brute : distribution entièrement à droite de 0, IC 95 % +0,059 ; +0,138. LightGBM − LR + features : distribution centrée sur 0, IC 95 % −0,026 ; +0,015.

6. Comparer sur plusieurs plis de validation croisée

Intuition. Avec une validation croisée, on obtient une différence d j d_j par pli. Le réflexe est un t-test apparié sur ces différences. Problème : les plis partagent une grande partie de leurs données d’entraînement, donc les d j d_j sont corrélés et leur variance est sous-estimée. Le t-test naïf est beaucoup trop optimiste (M7 l’avait annoncé : la corrélation empêche la variance de baisser en 1 / n 1/n ).

Deux corrections classiques, pour J J différences d j d_j de moyenne d ¯ \bar d et de variance empirique σ ^ 2 \hat\sigma^2 :

t naïf = d ¯ σ ^ 2 / J t corrigé = d ¯ ( 1 J + n test n train ) σ ^ 2 \begin{aligned} t_{\text{naïf}} &= \frac{\bar d}{\sqrt{\hat\sigma^2 / J}} \\ t_{\text{corrigé}} &= \frac{\bar d}{\sqrt{\left(\frac{1}{J} + \frac{n_{\text{test}}}{n_{\text{train}}}\right)\hat\sigma^2}} \end{aligned}

Le t corrigé de Nadeau et Bengio (2003) ajoute le terme n test / n train n_{\text{test}}/n_{\text{train}} pour tenir compte du chevauchement ; il se compare à une loi de Student à J − 1 J - 1 degrés de liberté.

Le 5x2cv de Dietterich : 5 répétitions d’une validation croisée à 2 plis. Pour la répétition i i , on note d i ( 1 ) , d i ( 2 ) d_i^{(1)}, d_i^{(2)} les deux différences, d ¯ i \bar d_i leur moyenne et s i 2 = ( d i ( 1 ) − d ¯ i ) 2 + ( d i ( 2 ) − d ¯ i ) 2 s_i^2 = (d_i^{(1)} - \bar d_i)^2 + (d_i^{(2)} - \bar d_i)^2 . Alors :

t 5 × 2 = d 1 ( 1 ) 1 5 ∑ i = 1 5 s i 2 t_{5\times 2} = \frac{d_1^{(1)}}{\sqrt{\frac{1}{5}\sum_{i=1}^{5} s_i^2}}

suit approximativement une loi de Student à 5 degrés de liberté.

Enfin, le test de Wilcoxon (signé des rangs) ne suppose pas de loi normale : il ne regarde que le signe et le rang des différences. Avec 10 différences toutes positives, sa p-valeur exacte vaut 2 / 2 10 ≈ 0 , 002 2/2^{10} \approx 0{,}002 : c’est le plus petit p p possible avec 10 plis.

7. Kolmogorov-Smirnov pour détecter un drift

Intuition. En production, la loi des features peut changer (nouveau workload, nouvelle version du noyau). Le test de Kolmogorov-Smirnov à deux échantillons compare deux distributions entières, pas seulement leurs moyennes. Sa statistique est l’écart maximal entre les deux fonctions de répartition empiriques :

D = max x | F réf ( x ) − F prod ( x ) | D = \max_x \big|F_{\text{réf}}(x) - F_{\text{prod}}(x)\big|

D D mesure la taille du drift (entre 0 et 1), la p-valeur dit seulement s’il est détectable. Avec beaucoup de données, le moindre drift devient significatif : en monitoring (partie 28), on alerte sur un seuil de D D , pas sur p < 0 , 05 p < 0{,}05 .

8. Comparaisons multiples

Intuition. Si tu fais 20 tests à α = 0 , 05 \alpha = 0{,}05 alors que rien n’est vrai, tu as en moyenne un faux positif. La probabilité d’en avoir au moins un est :

1 − ( 1 − α ) m = 1 − 0 , 95 20 ≈ 64 % 1 - (1 - \alpha)^m = 1 - 0{,}95^{20} \approx 64\,\%

C’est le piège classique du tuning : tester 20 variantes d’hyperparamètres sur le même jeu de test et garder « la meilleure ». La correction de Bonferroni teste chaque hypothèse au seuil α / m \alpha / m : simple, conservatrice, et elle ramène le risque global sous α \alpha . Mieux encore : garder un jeu de test qu’on ne regarde qu’une fois (partie 18).


Exemple de code

Intervalle de confiance d’une accuracy, et sa couverture

import numpy as np

rng = np.random.default_rng(0)
p_vrai = 0.80                      # vraie accuracy du modèle (inconnue en pratique)

# 1. un seul jeu de test de 200 exemples
correct = rng.binomial(1, p_vrai, 200)     # 1 = prédiction correcte
acc = correct.mean()
se = np.sqrt(acc * (1 - acc) / 200)
print(f"accuracy = {acc:.3f} | erreur standard = {se:.4f} | IC 95 % = [{acc - 1.96 * se:.3f} ; {acc + 1.96 * se:.3f}]")

# 2. la largeur de l'intervalle en fonction de n
for n in (200, 2_000, 20_000):
    print(f"n = {n:>6} : demi-largeur ≈ ±{1.96 * np.sqrt(p_vrai * (1 - p_vrai) / n):.3f}")

# 3. que veut dire « 95 % » ? on répète l'expérience 10 000 fois
n, couverts = 200, 0
for _ in range(10_000):
    a = rng.binomial(1, p_vrai, n).mean()
    s = np.sqrt(a * (1 - a) / n)
    couverts += (a - 1.96 * s <= p_vrai <= a + 1.96 * s)
print(f"taux de couverture : {couverts / 10_000:.1%}")

Résultat :

  • le premier jeu de test donne une accuracy de 0,725, une erreur standard de 0,0316 et l’intervalle [ 0 , 663 ; 0 , 787 ] [0{,}663 ; 0{,}787] . Pas de chance : il ne contient pas la vraie valeur 0,80. C’est exactement ce que « 95 % » autorise : environ un intervalle sur vingt rate la cible, et on ne sait jamais lequel ;
  • la demi-largeur vaut ± 0 , 055 \pm 0{,}055 pour 200 exemples, ± 0 , 018 \pm 0{,}018 pour 2 000 et ± 0 , 006 \pm 0{,}006 pour 20 000 : × 10 \times 10 en données, ÷ 10 ≈ 3 , 2 \div \sqrt{10} \approx 3{,}2 en incertitude ;
  • sur 10 000 répétitions, 94,0 % des intervalles contiennent 0,80. Un peu moins que 95 % : l’intervalle de Wald est légèrement trop étroit pour une proportion avec n = 200 n = 200 (l’exercice 2 propose mieux).

Le fil rouge : trois modèles, un jeu de test

On génère des régions mémoire synthétiques. Une région est chaude dans la prochaine fenêtre si elle a été beaucoup accédée récemment, si son dernier accès est proche, ou si elle est périodique et que son pic approche. Cette dernière règle est une interaction : un arbre la trouve seul, une régression logistique ne la voit que si on la lui donne.

import numpy as np
import lightgbm as lgb
from scipy import stats
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import f1_score
from sklearn.model_selection import train_test_split


def make_regions(n, rng):
    """Régions mémoire synthétiques : label 1 = chaude dans la prochaine fenêtre."""
    taux = rng.gamma(0.6, 4, n)                          # popularité très inégale
    acces_recents = rng.poisson(taux)
    depuis_dernier = np.round(rng.exponential(30 / (1 + acces_recents)), 1)
    periodique = rng.binomial(1, 0.25, n)
    avant_pic = rng.integers(0, 12, n)                   # pas restants avant le prochain pic
    logit = (0.9 * np.log1p(acces_recents) - 0.05 * depuis_dernier
             + 3.0 * periodique * (avant_pic < 3) - 1.5 + rng.normal(0, 0.8, n))
    X = np.column_stack([acces_recents, depuis_dernier, periodique, avant_pic])
    return X, (logit > 0).astype(int)


def features_metier(X):
    """Ce qu'un humain ajouterait : log des accès et interaction « périodique et pic imminent »."""
    return np.column_stack([np.log1p(X[:, 0]), X[:, 1], X[:, 2], X[:, 2] * (X[:, 3] < 3)])


rng = np.random.default_rng(0)
X, y = make_regions(5000, rng)
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.3, random_state=0, stratify=y)

preds = {
    "LR brute": LogisticRegression(max_iter=1000).fit(Xtr, ytr).predict(Xte),
    "LR + features": LogisticRegression(max_iter=1000).fit(features_metier(Xtr), ytr).predict(features_metier(Xte)),
    "LightGBM": lgb.LGBMClassifier(n_estimators=200, learning_rate=0.05, num_leaves=15,
                                   verbose=-1).fit(Xtr, ytr).predict(Xte),
}

# bootstrap apparié : on rééchantillonne les MÊMES indices pour tous les modèles
B, n_test = 2000, len(yte)
idx = rng.integers(0, n_test, (B, n_test))
f1_boot = {nom: np.array([f1_score(yte[i], p[i]) for i in idx]) for nom, p in preds.items()}
for nom, p in preds.items():
    lo, hi = np.percentile(f1_boot[nom], [2.5, 97.5])
    print(f"{nom:<14} F1 = {f1_score(yte, p):.3f}  IC 95 % bootstrap [{lo:.3f} ; {hi:.3f}]")


def comparer(a, b):
    diff = f1_boot[b] - f1_boot[a]
    lo, hi = np.percentile(diff, [2.5, 97.5])
    ok_a, ok_b = preds[a] == yte, preds[b] == yte
    n01, n10 = int(np.sum(ok_a & ~ok_b)), int(np.sum(~ok_a & ok_b))   # désaccords
    p_mcnemar = stats.binomtest(n01, n01 + n10, 0.5).pvalue
    print(f"{b} − {a} : ΔF1 = {f1_score(yte, preds[b]) - f1_score(yte, preds[a]):+.3f}, "
          f"IC 95 % [{lo:+.3f} ; {hi:+.3f}] | McNemar : {n01} contre {n10}, p = {p_mcnemar:.2g}")


comparer("LR brute", "LightGBM")
comparer("LR + features", "LightGBM")

Environ 22 % des régions sont chaudes. Sur les 1 500 régions de test :

Modèle F1 IC 95 % bootstrap
LR brute 0,582 [ 0 , 535 ; 0 , 629 ] [0{,}535 ; 0{,}629]
LR + features 0,686 [ 0 , 645 ; 0 , 727 ] [0{,}645 ; 0{,}727]
LightGBM 0,681 [ 0 , 639 ; 0 , 723 ] [0{,}639 ; 0{,}723]
Comparaison Δ F 1 \Delta F_1 IC 95 % apparié McNemar ( n 01 n_{01} contre n 10 n_{10} ) p
LightGBM − LR brute +0,099 [ + 0 , 059 ; + 0 , 138 ] [+0{,}059 ; +0{,}138] 44 contre 84 0,00052
LightGBM − LR + features −0,005 [ − 0 , 026 ; + 0 , 015 ] [-0{,}026 ; +0{,}015] 28 contre 25 0,78

Deux conclusions très différentes :

  • LightGBM bat nettement la LR brute : l’intervalle apparié est loin de 0 et McNemar donne p = 0 , 0005 p = 0{,}0005 . Sur les 128 régions où les deux modèles sont en désaccord, LightGBM a raison 84 fois ;
  • LightGBM et la LR avec les bonnes features sont à égalité : l’écart de 0,005 est noyé dans le bruit ( p = 0 , 78 p = 0{,}78 ). Annoncer « la LR fait mieux » serait faux, annoncer « LightGBM est inutile » aussi : on ne peut pas les départager avec ce jeu de test.

Remarque aussi la force de l’appariement : les intervalles individuels des deux derniers modèles font environ 0,08 de large, mais l’intervalle de leur différence ne fait que 0,04. Comparer deux intervalles qui se chevauchent n’est pas un test : seul l’intervalle de la différence compte.

Plusieurs plis : t naïf, t corrigé, 5x2cv et Wilcoxon

# suite du bloc précédent (make_regions, X, y déjà définis)
from sklearn.model_selection import StratifiedKFold

diffs = np.zeros((5, 2))
for r in range(5):
    plis = StratifiedKFold(n_splits=2, shuffle=True, random_state=r).split(X, y)
    for k, (tr, te) in enumerate(plis):
        f_lr = f1_score(y[te], LogisticRegression(max_iter=1000).fit(X[tr], y[tr]).predict(X[te]))
        f_gb = f1_score(y[te], lgb.LGBMClassifier(n_estimators=200, learning_rate=0.05, num_leaves=15,
                                                  verbose=-1).fit(X[tr], y[tr]).predict(X[te]))
        diffs[r, k] = f_gb - f_lr

d = diffs.ravel()
t_naif = d.mean() / (d.std(ddof=1) / np.sqrt(len(d)))
t_nb = d.mean() / np.sqrt((1 / len(d) + 1.0) * d.var(ddof=1))     # Nadeau-Bengio, n_test/n_train = 1
s2 = ((diffs - diffs.mean(axis=1, keepdims=True)) ** 2).sum(axis=1)
t_5x2 = diffs[0, 0] / np.sqrt(s2.mean())                            # Dietterich, 5 ddl
print(f"ΔF1 moyen sur 10 plis = {d.mean():+.3f} (min {d.min():+.3f}, max {d.max():+.3f})")
print(f"t naïf      = {t_naif:5.2f}, p = {2 * stats.t.sf(abs(t_naif), 9):.2g}")
print(f"t corrigé   = {t_nb:5.2f}, p = {2 * stats.t.sf(abs(t_nb), 9):.2g}")
print(f"t 5x2cv     = {t_5x2:5.2f}, p = {2 * stats.t.sf(abs(t_5x2), 5):.2g}")
print(f"Wilcoxon    : p = {stats.wilcoxon(d).pvalue:.2g}")

Sur les 10 plis, LightGBM bat la LR brute de + 0 , 083 +0{,}083 en moyenne (entre + 0 , 054 +0{,}054 et + 0 , 095 +0{,}095 selon le pli).

Test Statistique p
t apparié naïf (9 ddl) 20,82 6 , 4 × 10 − 9 6{,}4 \times 10^{-9}
t corrigé de Nadeau-Bengio (9 ddl) 6,28 0,00014
t 5x2cv de Dietterich (5 ddl) 6,07 0,0018
Wilcoxon signé des rangs — 0,002

Les quatre tests concluent dans le même sens, mais le t naïf est trois fois trop confiant (20,8 contre 6,3) : il traite 10 plis qui se chevauchent comme 10 expériences indépendantes. Ici, l’effet est assez grand pour que ça ne change pas la décision. Sur un écart plus fin, le t naïf déclarerait significatif ce qui ne l’est pas : c’est pour ça qu’on utilise le t corrigé ou le 5x2cv dans un mémoire.

Drift et comparaisons multiples

import numpy as np
from scipy import stats

rng = np.random.default_rng(0)

# drift : le temps depuis le dernier accès, avant et après un changement de workload
reference = rng.exponential(10, 2000)          # semaine d'entraînement
meme_loi = rng.exponential(10, 2000)           # semaine suivante, rien n'a changé
derive = rng.exponential(12, 2000)             # nouveau workload : accès 20 % plus espacés
for nom, x in (("même loi", meme_loi), ("dérive", derive)):
    res = stats.ks_2samp(reference, x)
    print(f"{nom:<9} : D = {res.statistic:.3f}, p = {res.pvalue:.2g}")

# comparaisons multiples : 20 features de bruit pur testées contre le label
n_exp, m, alpha = 2000, 20, 0.05
au_moins_un, au_moins_un_bonf = 0, 0
for _ in range(n_exp):
    label = rng.binomial(1, 0.3, 200)
    bruit = rng.normal(size=(200, m))
    p = np.array([stats.ttest_ind(bruit[label == 1, j], bruit[label == 0, j]).pvalue for j in range(m)])
    au_moins_un += (p < alpha).any()
    au_moins_un_bonf += (p < alpha / m).any()
print(f"au moins un faux positif : {au_moins_un / n_exp:.1%} (théorie {1 - (1 - alpha) ** m:.1%}) | "
      f"avec Bonferroni : {au_moins_un_bonf / n_exp:.1%}")
  • Même loi : D = 0 , 022 D = 0{,}022 , p = 0 , 74 p = 0{,}74 , rien à signaler. Dérive de 20 % sur la moyenne : D = 0 , 072 D = 0{,}072 , p = 6 , 2 × 10 − 5 p = 6{,}2 \times 10^{-5} , détectée avec 2 000 points par semaine.
  • Comparaisons multiples : avec 20 features de bruit pur, 64,0 % des expériences trouvent au moins une feature « significative » (la théorie donne 64,2 %). Avec Bonferroni ( α / 20 = 0 , 0025 \alpha / 20 = 0{,}0025 ), on retombe à 5,5 %.

Exercice pratique

  1. À la main. Un modèle a une accuracy de 0,80. Combien d’exemples de test faut-il pour que la demi-largeur de l’intervalle de confiance à 95 % soit inférieure à 0,01 ? Et à 0,001 ?
  2. Un meilleur intervalle. Refais la simulation de couverture avec n = 30 n = 30 et une vraie accuracy de 0,95. Compare la couverture de l’intervalle de Wald avec celle de l’intervalle de Wilson (stats.binomtest(k, n).proportion_ci(method="wilson")). Lequel tient ses promesses ?
  3. Bootstrap d’autres métriques. Sur le jeu de test du fil rouge, construis les intervalles bootstrap de la précision et du rappel de LightGBM. Lequel est le plus large, et pourquoi ? (Indice : sur combien d’exemples chacun est-il calculé ?)
  4. Peu de données. Refais le 5x2cv avec make_regions(600, ...) au lieu de 5 000, pour LightGBM contre LR brute puis contre LR + features. Les conclusions tiennent-elles ? Que disent le t naïf et le t corrigé ?
  5. Fil rouge. Sur le dataset de la partie 14, coupe la trace en deux moitiés temporelles et applique Kolmogorov-Smirnov à chaque feature. Combien de features dérivent au seuil 0,05, puis après Bonferroni ? Classe-les par D D plutôt que par p p .

Indice pour la question 1 : n = 1 , 96 2 × 0 , 8 × 0 , 2 / 0 , 01 2 n = 1{,}96^2 \times 0{,}8 \times 0{,}2 / 0{,}01^2 , soit environ 6 150 exemples. Pour 0,001, cent fois plus.


Ressources pour aller plus loin

  • Dietterich, Approximate Statistical Tests for Comparing Supervised Classification Learning Algorithms (Neural Computation, 1998) : McNemar et 5x2cv.
  • Nadeau et Bengio, Inference for the Generalization Error (Machine Learning, 2003) : le t corrigé.
  • Efron et Tibshirani, An Introduction to the Bootstrap (1993).
  • Demšar, Statistical Comparisons of Classifiers over Multiple Data Sets (JMLR, 2006) : Wilcoxon, Friedman et les comparaisons multiples.
  • Wasserstein et Lazar, The ASA Statement on p-Values (2016) : ce qu’une p-valeur dit, et ce qu’elle ne dit pas.

Ce que tu dois savoir faire avant de passer à la suite

  • Calculer l’erreur standard d’une accuracy et expliquer la décroissance en 1 / n 1/\sqrt n
  • Construire un intervalle de confiance et dire correctement ce que « 95 % » signifie
  • Construire un intervalle bootstrap pour une métrique sans formule (F1, AUC)
  • Définir p-valeur, erreur de type I, erreur de type II et puissance
  • Comparer deux modèles sur un même jeu de test avec un bootstrap apparié et McNemar
  • Expliquer pourquoi le t-test naïf sur des plis est trop optimiste, et utiliser le t corrigé ou le 5x2cv
  • Détecter un drift avec Kolmogorov-Smirnov et corriger des comparaisons multiples

Partie suivante : M10 · Descente de gradient et ses variantes. Comment on trouve concrètement le minimum de toutes ces fonctions de perte.