Leçon 1 sur 8
Unité · Regardez-la d'abord
Une moyenne de 29,6 et une médiane de zéro
Le dénombrement moyen d'E. coli dans les ménages testés vaut 29,6 UFC/100 mL. La médiane vaut 0. Plus de la moitié de l'échantillon n'a aucune contamination détectable, et la moyenne décrit une queue qui monte à 608.
Calculez le résumé en dernier
import pandas as pd
wash = pd.read_csv("wash-household-survey-2024.v1.csv")
ecoli = wash["ecoli_cfu_100ml"].dropna()
print(ecoli.describe().round(1))
library(dplyr)
wash |> filter(!is.na(ecoli_cfu_100ml)) |>
summarise(n = n(), mean = mean(ecoli_cfu_100ml),
median = median(ecoli_cfu_100ml), sd = sd(ecoli_cfu_100ml))
| Valeur | |
|---|---|
| n | 802 |
| Moyenne | 29,6 |
| Médiane | 0,0 |
| Écart-type | 85,6 |
| Maximum | 608 |
La moyenne vaut 29,6 et la médiane 0. Ces deux nombres décrivent les mêmes 802 ménages, et un seul des deux en décrit un.
Plus de la moitié des ménages testés n’ont aucune E. coli détectable. La moyenne est ce qu’on obtient en moyennant un grand groupe de zéros avec un petit groupe de très grands nombres, et elle atterrit dans une zone où presque personne ne se trouve.
Regardez la forme
bins = [-1, 0, 10, 100, 1000]
labels = ["0 (none detected)", "1-10", "11-100", ">100"]
print(pd.cut(ecoli, bins, labels=labels).value_counts().reindex(labels))
print(f"\nskew: {ecoli.skew():.2f}")
wash |> filter(!is.na(ecoli_cfu_100ml)) |>
count(band = cut(ecoli_cfu_100ml, c(-1, 0, 10, 100, Inf)))
| Bande | Ménages | Part |
|---|---|---|
| 0 — rien de détecté | 430 | 53,6 % |
| 1–10 | 173 | 21,6 % |
| 11–100 | 139 | 17,3 % |
| >100 | 60 | 7,5 % |
Asymétrie +4,04. Une distribution symétrique a une asymétrie proche de zéro ; au-delà d’environ +1, la moyenne et la médiane répondent à des questions différentes.
C’est pourquoi le cours EAH rapportait des classes de risque et non une moyenne. Les classes ne sont pas un choix de présentation — ce sont le seul résumé qui survit à une distribution de cette forme.
Trois formes et ce que chacune exige
Passez les mêmes trois nombres sur quatre indicateurs de quatre cours et le motif saute aux yeux.
import numpy as np
def profile(series, name):
s = series.dropna()
return {
"indicator": name, "n": len(s),
"mean": round(s.mean(), 1), "median": round(s.median(), 1),
"skew": round(s.skew(), 2),
}
points = pd.read_csv("water-point-monitoring-2024.v1.csv")
protection = pd.read_csv("protection-referrals-2024.v1.csv")
print(pd.DataFrame([
profile(wash["ecoli_cfu_100ml"], "E. coli, CFU/100mL"),
profile(points["days_since_breakdown"], "days a water point is down"),
profile(protection["days_to_first_service"], "days to first service"),
profile(wash["litres_per_person_day"], "litres per person per day"),
]))
# One function, four indicators, four different answers about which summary
# to report.
| Indicateur | n | Moyenne | Médiane | Asymétrie |
|---|---|---|---|---|
| E. coli, UFC/100 mL | 802 | 29,6 | 0,0 | +4,04 |
| Jours d’arrêt d’un point d’eau | 709 | 125,7 | 56,0 | +1,64 |
| Jours jusqu’au premier service | 728 | 10,1 | 9,0 | +0,52 |
| Litres par personne et par jour | 2 403 | 24,2 | 23,7 | +8,54 |
Le délai jusqu’au premier service est presque symétrique — moyenne 10,1, médiane 9,0, asymétrie +0,52. Une moyenne est un résumé honnête et un écart-type signifie quelque chose.
Les jours d’arrêt d’un point d’eau sont fortement asymétriques à droite — une moyenne de 125,7 contre une médiane de 56, parce qu’une poignée de points abandonnés sont en panne depuis plus de 600 jours. Rapportez la médiane et les quartiles.
Les litres par personne et par jour paraissent presque symétriques et portent une asymétrie de +8,54. C’est cette combinaison qui est intéressante.
Quand l’asymétrie est l’erreur de données
litres = wash["litres_per_person_day"].dropna()
print(f"median {litres.median():.1f}, p90 {litres.quantile(0.90):.1f}, "
f"max {litres.max():.1f}")
print(f"above 80: {(litres > 80).sum()} households")
wash |> summarise(median = median(litres_per_person_day),
p90 = quantile(litres_per_person_day, 0.9),
max = max(litres_per_person_day))
Médiane 23,7, quatre-vingt-dixième centile 33,7, maximum 277,6. La statistique d’asymétrie ne décrit pas la population ; elle détecte onze mauvaises lignes.
Ces onze sont les ménages dont la consommation totale a été saisie dans une colonne par personne — le défaut qu’enseignait le cours EAH. Il arrive ici par l’autre bout : une asymétrie très en décalage avec l’écart interquartile est un signal de qualité des données avant d’être un constat de distribution.
clean = litres[litres <= 80]
print(f"after removing 11 rows: skew {clean.skew():.2f}, "
f"mean {clean.mean():.1f}, median {clean.median():.1f}")
# Recompute after the correction and see whether the shape was real.
Onze lignes sur 2 403 et l’asymétrie passe de +8,54 à +0,06. Moyenne 23,5, médiane 23,6 — une distribution symétrique depuis le début, portant une forme qui appartenait entièrement à une erreur d’unité.
Calculez l’asymétrie, puis décidez si c’est un constat ou un bogue. Les deux sont courants et se distinguent en regardant les valeurs extrêmes, non en regardant la statistique.
Ce qu’il faut rapporter pour chaque forme
| Forme | Rapportez | Ne rapportez pas |
|---|---|---|
| À peu près symétrique | Moyenne et écart-type | — |
| Asymétrique à droite | Médiane et écart interquartile | Une moyenne sans la médiane à côté |
| Gonflée en zéros | La part à zéro, puis la distribution du reste | Une moyenne, tout court |
| Proportion bornée | La proportion et son intervalle | Un écart-type |
E. coli est gonflée en zéros, ce qui est un cas distinct de la simple asymétrie : 53,6 % de l’échantillon se trouve exactement sur une valeur, et aucun résumé continu ne décrit cela. Le rapport en deux parties — combien sont à zéro, et parmi les autres, à quel point — est le seul honnête.
E. coli at point of collection, 802 households tested
No detectable E. coli 53.6% 430 households
Among the 372 with any detected:
median 13 CFU/100 mL
interquartile range 5 to 49
maximum 608
Mean over all 802 households is 29.6 CFU/100 mL. It is not reported as a
summary because 53.6% of the sample sits at zero and the mean falls in a
range occupied by almost no household.
L’histogramme qu’il faut toujours tracer
import matplotlib.pyplot as plt
fig, axes = plt.subplots(1, 2, figsize=(9, 3))
axes[0].hist(ecoli, bins=40)
axes[0].set_title("E. coli, raw")
axes[1].hist(np.log1p(ecoli), bins=40)
axes[1].set_title("log(1 + E. coli)")
plt.tight_layout()
hist(wash$ecoli_cfu_100ml, breaks = 40)
hist(log1p(wash$ecoli_cfu_100ml), breaks = 40)
Tracez-le avant de résumer, à chaque fois, et ne le mettez pas dans le rapport. L’histogramme est un instrument pour vous, non un constat pour le lecteur — son rôle est de vous dire quel résumé est honnête, et une fois qu’il l’a fait, le résumé entre et l’histogramme reste dehors.
Deux minutes d’observation auraient évité chaque erreur de cette leçon, et c’est tout l’argument en faveur de cette habitude.
La suite
Vous savez maintenant quel nombre unique décrit un indicateur. La leçon suivante porte sur l’ampleur de son erreur possible, et sur la notation qui la transporte dans une phrase sur laquelle un lecteur peut agir.