Leçon 6 sur 8
Unité · Les verbes
Là où naissent la plupart des erreurs d'indicateurs
group_by() et summarise() — le regroupement qui survit à l'appel, la clé manquante qui devient son propre groupe, et les trois comportements par défaut exactement inverses de ceux de pandas.
Un indicateur, ce sont deux nombres
Presque tout chiffre rapporté dans ce secteur est un numérateur sur un dénominateur, et presque toute dispute sur un chiffre est une dispute sur le dénominateur. La conséquence pratique : calculez les deux dans le même appel.
library(dplyr)
by_commune <- muac |>
mutate(
assessed = !is.na(muac_mm) | !is.na(oedema),
gam = assessed & (muac_mm < 125 | oedema == "true")
) |>
group_by(commune) |>
summarise(
screened = n(),
assessed = sum(assessed, na.rm = TRUE),
gam_cases = sum(gam, na.rm = TRUE),
.groups = "drop"
) |>
mutate(gam_rate = round(gam_cases / assessed, 3))
Trois colonnes, et chacune porte quelque chose. screened est le nombre
d’enfants venus ; assessed le nombre ayant produit une mesure exploitable, et
c’est le dénominateur ; gam_cases est le numérateur. Deux calculs séparés
divergent — l’un reçoit un filtre que l’autre n’a pas — et le rapport devient un
chiffre que personne ne peut reconstituer.
n() et les deux façons de compter
summarise(muac, rows = n(), measured = sum(!is.na(muac_mm)))
n() compte les lignes du groupe, y compris celles à valeur manquante. Sommer un
booléen compte les TRUE. Cette distinction fait la différence entre les deux
dénominateurs ci-dessus, et mérite d’être énoncée à voix haute chaque fois que
vous employez l’un des deux.
sum() sur un booléen contenant NA renvoie NA — R, encore une fois,
bruyant :
sum(c(TRUE, NA))
#> [1] NA
sum(c(TRUE, NA), na.rm = TRUE)
#> [1] 1
Recourez à na.rm = TRUE délibérément, en sachant qu’il retire la ligne du
décompte au lieu de la compter comme FALSE. Ce sont deux affirmations
différentes.
Le regroupement survit, et c’est l’erreur la plus fréquente
C’est le point à intérioriser. summarise() retire la dernière variable de
regroupement et laisse les autres :
out <- muac |>
mutate(month = format(screening_date, "%Y-%m")) |>
group_by(commune, month) |>
summarise(n = n())
`summarise()` has regrouped the output.
ℹ Summaries were computed grouped by commune and month.
ℹ Output is grouped by commune.
ℹ Use `summarise(.groups = "drop_last")` to silence this message.
class(out)[1]
#> [1] "grouped_df"
dplyr::group_vars(out)
#> [1] "commune"
Le résultat est encore groupé par commune. Chaque verbe qui suit opère silencieusement par commune :
out |> mutate(share = n / sum(n)) # part dans la commune, non dans le district
out |> arrange(desc(n)) # trie a l'interieur de chaque commune
out |> slice_max(n, n = 5) # top 5 par commune, non au total
Chacune de ces lignes produit un chiffre plausible qui répond à une autre question que la vôtre. Rien ne lève d’erreur.
Dites ce que vous voulez, à chaque fois :
summarise(..., .groups = "drop") # resultat degroupe — la reponse habituelle
summarise(..., .groups = "drop_last") # garder tout sauf la derniere (le defaut)
summarise(..., .groups = "keep") # tout garder
Ou évitez complètement cet état avec le regroupement par opération, apparu dans dplyr 1.1 :
muac |>
summarise(n = n(), .by = c(commune, month))
.by groupe pour ce seul appel et renvoie un résultat dégroupé.
Préférez-le dans du code neuf : sans état résiduel, il n’y a pas d’erreur de
cette forme.
Une clé manquante devient son propre groupe
Ici, R et pandas prennent des paris opposés.
d <- tibble::tibble(g = c("a", NA, "a"), v = c(1, 2, 3))
d |> group_by(g) |> summarise(s = sum(v), .groups = "drop")
# A tibble: 2 × 2
g s
<chr> <dbl>
1 a 4
2 <NA> 2
Le groupe NA est conservé. pandas l’écarte par défaut : la même opération
dans les deux langages vous donne donc un ensemble de lignes différent et un
total différent.
Le comportement de R est le plus sûr, et dans ce secteur la clé manquante est
généralement la plus intéressante : une formation sanitaire sans district saisi,
un ménage sans code de site, un enfant sans commune. Mais cela signifie qu’un
tableau peut arriver dans un rapport avec une ligne <NA> que personne n’avait
prévu de publier.
d |> filter(!is.na(g)) |> group_by(g) |> summarise(s = sum(v), .groups = "drop")
L’exclure est légitime. L’exclure sans le remarquer, c’est ainsi que les parties cessent de faire le tout. Préférez une assertion là où la clé devrait être complète :
stopifnot(!any(is.na(muac$commune)))
Niveaux de facteur inutilisés : count() écarte, table() conserve
Le second renversement, et R n’est pas cohérent avec lui-même ici — raison pour laquelle la leçon sur les facteurs l’aborde et celle-ci le répète.
d <- tibble::tibble(k = factor(c("a", "b", "a"), levels = c("a", "b", "c")))
nrow(count(d, k)) #> 2 — le niveau vide a disparu
nrow(count(d, k, .drop = FALSE)) #> 3 — conserve
length(table(d$k)) #> 3 — conserve
Les deux sont justes pour des questions différentes, et c’est la question même que
le cours Python soulève à propos d’observed=. Rapporter sur les formations
ayant transmis des données veut le niveau vide en moins ; rapporter la couverture
face à une liste de formations qui auraient dû transmettre le veut présent, car
une formation à zéro ligne est justement le constat.
Passez .drop explicitement dans tout ce qui produit un tableau rapporté.
mutate() groupé : une statistique de groupe sur chaque ligne
summarise() réduit ; mutate() sur un tableau groupé renvoie une valeur par
ligne d’origine. C’est ainsi qu’on compare une ligne à son propre groupe sans
jointure :
muac |>
group_by(commune) |>
mutate(
commune_mean = mean(muac_mm, na.rm = TRUE),
vs_commune = muac_mm - commune_mean
) |>
ungroup()
C’est le contrôle qu’emploie le projet Analyse d’enquête SMART pour repérer une équipe mesurant un demi-kilo trop léger.
Noter le ungroup(). Un tableau groupé qui s’échappe dans la suite d’un
script est la même erreur que ci-dessus, arrivant par une autre porte.
count() quand c’est tout ce que vous voulez
muac |> count(commune, outcome, sort = TRUE)
muac |> count(commune, wt = muac_mm) # sommer une colonne plutot que compter
count() groupe, compte et dégroupe en un appel : il ne peut donc pas laisser
d’état derrière lui. Là où un décompte suffit, c’est le meilleur verbe.
Deux clés, et le tableau qu’attend un rapport
by_commune_month <- muac |>
mutate(month = format(screening_date, "%Y-%m")) |>
summarise(cases = sum(gam, na.rm = TRUE), .by = c(commune, month))
wide <- by_commune_month |>
tidyr::pivot_wider(names_from = month, values_from = cases, values_fill = 0)
values_fill = 0 est sans risque ici, car une commune-mois sans cas en avait
réellement zéro. Ce serait faux pour un taux, où l’absence signifie « non
calculé » et non « zéro pour cent » — distinction à vérifier chaque fois que vous
y recourez.
Écrire le résultat
readr::write_csv(by_commune, here::here("outputs", "tables", "gam_by_commune.csv"))
Pour un tableau destiné à être lu plutôt que calculé, écrivez la définition avec lui :
definitions <- tibble::tibble(
indicateur = "MAG",
numerateur = "PB < 125 mm ou oedemes bilateraux prenant le godet",
denominateur = "enfants avec une mesure de PB ou une evaluation d'oedemes saisie"
)
readr::write_csv(definitions, here::here("outputs", "tables", "definitions.csv"))
Un nombre et sa définition voyagent ensemble, sinon la dispute mensuelle sur le dénominateur recommence.
Les trois renversements, en un seul endroit
| pandas | R / dplyr | |
|---|---|---|
Valeur manquante dans mean() |
ignorée en silence | renvoie NA jusqu’à na.rm |
| Clé de groupe manquante | écartée | conservée comme groupe à part |
| Catégorie inutilisée | écartée par observed=True |
conservée par table(), écartée par count() |
Aucun n’est meilleur. Chacun est un pari différent sur ce que le silence doit signifier, et savoir dans quel pari on se trouve constitue l’essentiel du métier.
Ce qui vient ensuite
Le tableau agrège correctement et les deux moitiés de chaque taux proviennent d’un
seul appel. L’unité suivante restructure : pivot_longer() sur le groupe répété
aplati sous lequel arrive un export CommCare ou Kobo, puis la fonction qui fait
tourner tout le nettoyage au trimestre suivant.