cassionAnalyse de données

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.

R90 min

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.

Animer cette leçon

La leçon en diaporama, la prose étant reléguée dans les notes du présentateur plutôt que projetée. Produit à partir de cette page, dont il ne peut donc pas s'écarter.

Lancer le diaporamaLire les diapositives

Le PDF ne requiert aucun logiciel et se projette depuis n'importe quel poste. Le fichier PowerPoint est fait pour être modifié : appliquez la charte de votre organisation, retirez une section pour une séance plus courte, ou fusionnez deux leçons en atelier.