Comparez des lois entières avec Kolmogorov-Smirnov et les noyaux
Jusqu’ici, nous avons comparé des positions : une médiane, un décalage, un rang moyen. Mais on veut parfois comparer des lois entières. Les temps entre deux requêtes sur un serveur suivent-ils bien une loi exponentielle, comme le suppose le modèle de dimensionnement ? Les latences mesurées en Europe et aux États-Unis ont-elles la même distribution, pas seulement la même médiane ?
Ce chapitre présente deux outils complémentaires. Le test de Kolmogorov-Smirnov compare des fonctions de répartition. L’estimation de densité par noyau dessine la forme d’une loi à partir des données, sans lui imposer de famille paramétrique.
Le test de Kolmogorov-Smirnov à un échantillon
Section intitulée « Le test de Kolmogorov-Smirnov à un échantillon »Le fil rouge : des temps entre requêtes
Section intitulée « Le fil rouge : des temps entre requêtes »Un générateur de charge est censé envoyer des requêtes selon un processus de Poisson d’intensité 1 par seconde : les temps entre deux requêtes devraient suivre une loi exponentielle de moyenne 1 s, de fonction de répartition . On relève dix intervalles (en secondes), déjà triés :
0,12 ; 0,35 ; 0,48 ; 0,71 ; 0,95 ; 1,30 ; 1,62 ; 2,10 ; 2,85 ; 3,90
La statistique D
Section intitulée « La statistique D »Au chapitre 1, nous avons défini la fonction de répartition empirique , qui monte de à chaque observation. Si les données suivent , devrait rester proche de partout. Le test de Kolmogorov-Smirnov mesure le plus grand écart vertical entre les deux courbes :
où désigne la plus grande valeur atteinte quand parcourt tous les réels. Comme est en escalier, il suffit de regarder, en chaque observation , les écarts juste avant et juste après le saut :
La loi de D sous l’hypothèse nulle
Section intitulée « La loi de D sous l’hypothèse nulle »Le résultat remarquable, dû à Kolmogorov, est que si est continue et entièrement spécifiée, la loi de sous ne dépend pas de . Le test est donc libre de distribution. Pour petit, la loi exacte est tabulée ou calculée numériquement ; pour grand, suit approximativement la loi de Kolmogorov, et la valeur critique à 5 % vaut environ .
Ici, la valeur critique exacte à 5 % pour est d’environ 0,41 (l’approximation donne 0,43). Avec , on est loin du seuil : la p-valeur exacte vaut environ 0,60. Rien ne contredit la loi exponentielle.
Le piège des paramètres estimés
Section intitulée « Le piège des paramètres estimés »Et si l’on ne connaît pas la moyenne de la loi exponentielle ? La tentation est de l’estimer sur les données (ici, la moyenne vaut 1,438 s), puis de lancer le test avec cette loi ajustée. On obtient et une p-valeur affichée de 0,997.
Kolmogorov-Smirnov à deux échantillons
Section intitulée « Kolmogorov-Smirnov à deux échantillons »Le même principe compare deux échantillons, sans référence théorique. Avec et les fonctions de répartition empiriques des deux échantillons :
Sous (les deux échantillons viennent de la même loi continue), la loi de ne dépend pas non plus de cette loi.
Quelle différence avec Mann-Whitney ? Mann-Whitney est surtout sensible à un décalage de position. Kolmogorov-Smirnov détecte n’importe quelle différence entre les lois : position, dispersion ou forme. En contrepartie, pour un pur décalage, il est moins puissant que Mann-Whitney. Il est aussi plus sensible au centre de la loi qu’aux queues, où les deux répartitions sont toutes deux proches de 0 ou de 1.
import numpy as npfrom scipy import stats
x = np.array([0.12, 0.35, 0.48, 0.71, 0.95, 1.30, 1.62, 2.10, 2.85, 3.90])print(stats.kstest(x, "expon")) # D ≈ 0.227, p ≈ 0.60 (loi exponentielle de moyenne 1)
europe = [82, 88, 92, 95, 97, 101, 105, 110]usa = [99, 108, 115, 118, 122, 125, 131, 140]print(stats.ks_2samp(europe, usa)) # D = 0.75, p ≈ 0.019x <- c(0.12, 0.35, 0.48, 0.71, 0.95, 1.30, 1.62, 2.10, 2.85, 3.90)ks.test(x, "pexp", rate = 1) # D = 0.227, p ≈ 0.60
europe <- c(82, 88, 92, 95, 97, 101, 105, 110)usa <- c(99, 108, 115, 118, 122, 125, 131, 140)ks.test(europe, usa) # D = 0.75, p ≈ 0.019En data science, le test à deux échantillons sert souvent à détecter une dérive de données (data drift) : on compare, variable par variable, la distribution des données de production à celle des données d’entraînement.
Estimer une densité par noyau
Section intitulée « Estimer une densité par noyau »Un histogramme donne une idée de la forme d’une loi, mais il dépend beaucoup du choix des classes et produit une courbe en escalier. L’estimation de densité par noyau (kernel density estimation, KDE) le remplace par une courbe lisse.
Le principe
Section intitulée « Le principe »On pose une petite « bosse » sur chaque observation, puis on additionne toutes les bosses :
- est le noyau (kernel), une densité symétrique : le plus souvent la densité normale centrée réduite , ou le noyau d’Epanechnikov pour ;
- est la largeur de bande (bandwidth), qui règle la largeur de chaque bosse ;
- le facteur garantit que est d’intégrale 1.
Le choix de h compte plus que celui du noyau
Section intitulée « Le choix de h compte plus que celui du noyau »Le noyau a peu d’influence sur le résultat ; la largeur de bande, énormément. C’est un compromis biais-variance :
- trop petit : chaque observation produit un pic étroit, la courbe est hérissée (sous-lissage, forte variance) ;
- trop grand : toutes les bosses se fondent, la courbe est trop lisse et gomme les modes réels (sur-lissage, fort biais).
Sur nos six temps de trajet, avec , la densité estimée vaut 0,46 en mais seulement 0,01 en : deux pics séparés. Avec , elle vaut 0,20 en et 0,19 en : une seule bosse aplatie.
Des règles automatiques proposent un raisonnable quand la loi n’est pas trop éloignée d’une normale :
- la règle de Silverman : , où est l’écart-type empirique et IQR l’écart interquartile. C’est le défaut de R (
bw.nrd0). Ici, , IQR (quartiles calculés à la manière de R), d’où ; - la règle de Scott : , le défaut de
scipy.stats.gaussian_kde, ici .
Pour une loi multimodale ou très asymétrique, ces règles lissent trop ; on préfère alors la validation croisée ou la méthode de Sheather-Jones (bw = "SJ" en R).
import numpy as npfrom scipy import stats
t = np.array([2.1, 2.5, 3.0, 3.2, 4.8, 5.5])kde = stats.gaussian_kde(t) # règle de Scott par défautprint(kde([3.0, 4.0])) # environ [0.26 0.19]print(kde.factor * t.std(ddof=1)) # largeur de bande effective, environ 0.94
kde_fin = stats.gaussian_kde(t, bw_method=0.3 / t.std(ddof=1)) # h = 0.3print(kde_fin([3.0, 4.0])) # environ [0.46 0.01]Attention à la convention de scipy : bw_method est un facteur multiplié par l’écart-type des données, pas la largeur de bande elle-même. En R, density prend directement :
t <- c(2.1, 2.5, 3.0, 3.2, 4.8, 5.5)d <- density(t) # noyau gaussien, h de Silverman (bw.nrd0)d$bw # environ 0.83plot(d); rug(t)plot(density(t, bw = 0.3)) # sous-lissage : deux picsEn résumé
Section intitulée « En résumé »- Le test de Kolmogorov-Smirnov mesure le plus grand écart vertical entre fonctions de répartition : .
- Sa loi sous ne dépend pas de si celle-ci est continue et fixée à l’avance ; avec des paramètres estimés sur les données, il faut Lilliefors ou un bootstrap.
- La version à deux échantillons détecte toute différence de lois (position, dispersion, forme), mais est moins puissante que Mann-Whitney pour un simple décalage.
- Ne pas rejeter une loi ne prouve pas qu’elle est la bonne, surtout avec peu de données.
- L’estimation par noyau lisse les données ; la largeur de bande compte bien plus que le noyau.
scipy.stats.kstest,ks_2samp,gaussian_kde; en R,ks.testetdensity.
Exercices
Section intitulée « Exercices »Exercice · Calculer D à la main
facileOn teste si quatre valeurs, 0,1 ; 0,4 ; 0,5 ; 0,9, suivent la loi uniforme sur , pour laquelle . Calculez .
Voir la solution
En chaque observation , on calcule et :
| 1 | 0,1 | 0,15 | 0,10 |
| 2 | 0,4 | 0,10 | 0,15 |
| 3 | 0,5 | 0,25 | 0,00 |
| 4 | 0,9 | 0,10 | 0,15 |
, atteint juste après (trois valeurs sur quatre sont inférieures ou égales à 0,5, alors qu’on en attendrait la moitié). C’est très loin de la valeur critique à 5 % pour (environ 0,62) : on ne rejette pas l’uniformité.
En code
import numpy as npfrom scipy import stats
x = np.sort([0.1, 0.4, 0.5, 0.9])n = len(x)i = np.arange(1, n + 1)print(i / n - x) # [0.15 0.1 0.25 0.1 ]print(x - (i - 1) / n) # [0.1 0.15 0. 0.15]
res = stats.kstest(x, "uniform") # loi uniforme sur [0, 1]print(res.statistic, res.pvalue) # 0.25 0.906print(stats.kstwo.ppf(0.95, n)) # valeur critique à 5 % : 0.624x <- sort(c(0.1, 0.4, 0.5, 0.9))n <- length(x); i <- 1:ni / n - x # 0.15 0.10 0.25 0.10x - (i - 1) / n # 0.10 0.15 0.00 0.15
ks.test(x, "punif") # D = 0.25, p-value = 0.9062Exercice · Une densité par noyau à la main
moyenAvec les six temps de trajet de l’exemple (2,1 ; 2,5 ; 3,0 ; 3,2 ; 4,8 ; 5,5), estimez la densité en avec le noyau d’Epanechnikov pour (0 sinon) et . Comparez au résultat gaussien avec .
Voir la solution
Avec , : 0,9 ; 0,5 ; 0 ; −0,2 ; −1,8 ; −2,5. Seules les quatre premières valeurs vérifient :
- ;
- ;
- ;
- .
Somme : 2,175. Donc . C’est presque exactement la valeur obtenue avec le noyau gaussien et (0,363) : un noyau d’Epanechnikov de largeur 1 a un écart-type de , comparable à celui du noyau gaussien de largeur 0,5. Ce qui compte, c’est l’échelle de lissage, pas la forme du noyau.
En code
import numpy as npfrom scipy import stats
t = np.array([2.1, 2.5, 3.0, 3.2, 4.8, 5.5])
def epanechnikov(u): return np.where(np.abs(u) <= 1, 0.75 * (1 - u ** 2), 0.0)
h = 1contrib = epanechnikov((3 - t) / h)print(contrib) # [0.1425 0.5625 0.75 0.72 0. 0.]print(contrib.sum() / (len(t) * h)) # 0.3625
# Noyau gaussien avec h = 0.5, pour comparerprint(stats.norm.pdf((3 - t) / 0.5).sum() / (len(t) * 0.5)) # 0.3629t <- c(2.1, 2.5, 3.0, 3.2, 4.8, 5.5)epanechnikov <- function(u) ifelse(abs(u) <= 1, 0.75 * (1 - u^2), 0)
h <- 1contrib <- epanechnikov((3 - t) / h)contrib # 0.1425 0.5625 0.7500 0.7200 0 0sum(contrib) / (length(t) * h) # 0.3625
# Noyau gaussien avec h = 0.5, pour comparersum(dnorm((3 - t) / 0.5)) / (length(t) * 0.5) # 0.3629Exercice · Choisir entre KS et Mann-Whitney
difficileDeux serveurs ont la même latence médiane, 100 ms. Sur le serveur 1, les latences sont très regroupées (entre 95 et 105 ms). Sur le serveur 2, elles sont très dispersées (entre 40 et 160 ms), mais symétriques autour de 100 ms.
- Un test de Mann-Whitney sur deux grands échantillons a-t-il de bonnes chances de détecter une différence ? Et Kolmogorov-Smirnov ?
- Lequel choisiriez-vous pour surveiller une dérive des latences en production ?
Voir la solution
- Mann-Whitney porte sur . Par symétrie autour de la même médiane, une latence du serveur 2 a à peu près une chance sur deux de dépasser une latence du serveur 1 : , et le test ne détectera pratiquement rien, même avec beaucoup de données. Kolmogorov-Smirnov compare les fonctions de répartition entières : celle du serveur 1 monte brutalement autour de 100 ms, celle du serveur 2 monte doucement de 40 à 160 ms. L’écart maximal est grand (juste avant 95 ms, par exemple, alors que vaut près de la moitié), et le test rejettera nettement.
- Pour une surveillance de dérive, on veut détecter tout changement de distribution, y compris une hausse de variabilité sans changement de médiane (qui dégrade l’expérience des utilisateurs dans la queue). Kolmogorov-Smirnov est donc plus adapté, en complément d’un suivi direct des quantiles élevés (95e ou 99e centile).
En code
import numpy as npfrom scipy import stats
# Même médiane (100 ms), dispersions très différentesrng = np.random.default_rng(0)serveur1 = rng.uniform(95, 105, size=500)serveur2 = rng.uniform(40, 160, size=500)
print(stats.mannwhitneyu(serveur1, serveur2).pvalue) # 0.94 : rien détectéres = stats.ks_2samp(serveur1, serveur2)print(res.statistic, res.pvalue) # D = 0.466, p ≈ 2e-49# Même médiane (100 ms), dispersions très différentesset.seed(0)serveur1 <- runif(500, 95, 105)serveur2 <- runif(500, 40, 160)
wilcox.test(serveur1, serveur2)$p.value # 0.96 : rien détectéks.test(serveur1, serveur2) # D = 0.462, p-value < 2.2e-16