cassionAnalyse de données

Leçon 5 sur 8

Unité · Les verbes

Les verbes dplyr, employés délibérément

filter, select, mutate et arrange — ce que chacun fait d'une valeur manquante, pourquoi if_else() refuse ce que ifelse() accepte en silence, et le across() qui applique une règle à plusieurs colonnes.

R85 min

Cinq verbes et un tube

library(dplyr)

muac |>
  filter(!is.na(muac_mm)) |>
  mutate(gam = muac_mm < 125) |>
  group_by(commune) |>
  summarise(assessed = n(), cases = sum(gam)) |>
  arrange(desc(cases / assessed))

Chaque verbe prend un tableau de données et en renvoie un. Le tube passe le résultat au suivant. Voilà toute la grammaire ; le reste de cette leçon porte sur ce que fait chaque verbe quand les données ne sont pas propres — c’est-à-dire toujours.

|> est le tube natif de R, disponible depuis la 4.1. %>% de magrittr le précède et se comporte de façon quasi identique pour ce type de code ; si les scripts de votre équipe l’emploient, tout ce qui suit fonctionne tel quel.

filter() écarte les valeurs manquantes, en silence

nrow(muac)
#> [1] 4218

nrow(filter(muac, muac_mm < 125))
#> [1] 319

sum(is.na(muac$muac_mm))
#> [1] 72

Une comparaison avec NA vaut NA, et filter() ne conserve que les lignes TRUE. Ces 72 enfants non mesurés ont donc disparu, sans que rien ne le dise.

C’est le même comportement que pandas, et cela mérite d’être énoncé dans les deux langages parce que c’est la façon la plus rapide de changer un dénominateur par accident. Soyez explicite sur ce que vous vouliez dire :

# Enfants mesures en dessous de 125 mm.
filter(muac, muac_mm < 125)

# Enfants dont on ne sait pas qu'ils sont a 125 mm ou plus — non mesures compris.
filter(muac, is.na(muac_mm) | muac_mm < 125)
#> 391 lignes

Soixante-douze lignes séparent ces deux questions. Laquelle vous voulez dépend de savoir si un enfant non mesuré est un non-cas ou un inconnu, et c’est une décision qui relève d’un journal de nettoyage, non d’un comportement par défaut.

select() et ses assistants

muac |> select(child_id, commune, muac_mm)
muac |> select(-oedema)
muac |> select(starts_with("muac"), ends_with("_date"))
muac |> select(where(is.numeric))

select() renomme en sélectionnant, ce qui est la manière tidy de corriger une fois pour toutes les noms de colonnes d’un export :

epi |> select(facility = facility_id, month = period, doses = doses_administered)

Deux habitudes à prendre. Sélectionnez avant de joindre, pour que le résultat ne traîne pas quarante colonnes dont vous ne vouliez pas. Et ne sélectionnez jamais par position — select(3:5) casse le jour où l’export gagne une colonne, et il casse en silence puisque le code s’exécute toujours.

mutate() et les deux fonctions conditionnelles

muac <- muac |>
  mutate(
    gam = muac_mm < 125,
    sam = muac_mm < 115,
    band = case_when(
      muac_mm < 115 ~ "severe",
      muac_mm < 125 ~ "moderate",
      .default = "normal"
    )
  )

case_when() évalue dans l’ordre et la première correspondance l’emporte : < 115 doit donc venir en premier. Son .default attrape tout le reste, y compris les NA, ce qui n’est presque jamais souhaité :

case_when(c(110, 130, NA) < 125 ~ "case", .default = "not")
#> [1] "case" "not"  "not"

La mesure manquante est devenue "not". Traitez-la explicitement :

band = case_when(
  is.na(muac_mm) ~ NA_character_,
  muac_mm < 115  ~ "severe",
  muac_mm < 125  ~ "moderate",
  .default = "normal"
)

Placer la branche is.na() en premier est l’habitude à prendre. C’est le même piège que le default de np.select dans le cours Python, et il vaut d’être reconnu aux deux endroits.

if_else() plutôt que ifelse()

ifelse(c(TRUE, FALSE), 1L, "x")
#> [1] "1" "x"

Le ifelse() de R de base a silencieusement fondu un entier et une chaîne en un vecteur de caractères. Une colonne que vous croyiez numérique est désormais du texte, et vous l’apprendrez trois étapes plus loin quand une somme échouera.

if_else(c(TRUE, FALSE), 1L, "x")
#> Error: Can't combine `true` <integer> and `false` <character>.

Le if_else() de dplyr refuse. Préférez-le partout, pour la raison sur laquelle ce cours revient sans cesse : une erreur là où la faute est commise vaut mieux qu’une mauvaise réponse trois étapes plus loin.

if_else() accepte aussi un argument missing =, qui permet de dire ce qu’une condition NA doit produire plutôt que de la laisser se propager.

across() : une règle, plusieurs colonnes

muac |>
  summarise(across(c(muac_mm, age_months), ~ mean(.x, na.rm = TRUE)))
#> # A tibble: 1 × 2
#>   muac_mm age_months
#> 1    140.       30.9

across() est ce qui évite qu’un script de nettoyage soit quinze lignes presque identiques.

survey |> mutate(across(where(is.character), stringr::str_squish))

survey |> mutate(across(starts_with("fcs_"), ~ if_else(.x > 7, NA_real_, .x)))

Cette seconde ligne est la règle sécurité alimentaire du cours Python pour les données de programme, en une instruction : une valeur de consommation supérieure à sept jours est impossible et doit devenir manquante plutôt qu’être plafonnée, sur les huit groupes alimentaires à la fois.

La forme ~ .x abrège une fonction à un argument. \(x) ... est l’écriture plus récente de R de base et fonctionne à l’identique :

survey |> mutate(across(starts_with("fcs_"), \(x) if_else(x > 7, NA_real_, x)))

arrange()

muac |> arrange(commune, desc(screening_date))

NA se classe en dernier quel que soit le sens, ce qui est généralement souhaitable et masque parfois un problème — une colonne entièrement manquante paraît triée.

Trier range des lignes. Cela ne dit rien de la réalité des écarts entre lignes voisines, point que développe le projet Tableau de bord de programme nutritionnel, où neuf communes sur douze ont des intervalles de confiance qui se recouvrent.

distinct() et le comptage

n_distinct(muac$commune)
#> [1] 12

muac |> distinct(child_id, .keep_all = TRUE)

distinct() sans .keep_all = TRUE ne renvoie que les colonnes nommées, ce qui surprend qui attendait des lignes dédupliquées. Avec, c’est la première ligne de chaque groupe qui est conservée — triez donc d’abord si le choix de la survivante compte.

muac |> count(commune, sort = TRUE)

count() équivaut à group_by() |> summarise(n = n()) |> ungroup(), et c’est le bon outil chaque fois que c’est tout ce que vous voulez.

Les jointures, et le contrôle qui les accompagne

daily <- attendance |>
  left_join(roster, by = "student_id")

La défaillance à prévenir est une jointure qui multiplie les lignes parce que le côté droit a des clés dupliquées. dplyr avertit, mais un avertissement dans un long script défile :

before <- nrow(attendance)
daily <- attendance |> left_join(roster, by = "student_id")
stopifnot(nrow(daily) == before)

Cette assertion est l’équivalent R du validate="many_to_one" de pandas, et elle a attrapé deux élèves inscrits deux fois dans le projet Assiduité scolaire — un transfert non enregistré ayant laissé les deux inscriptions actives.

anti_join() est la façon la plus rapide de voir ce qui n’a pas correspondu :

attendance |> anti_join(roster, by = "student_id") |> distinct(student_id)

Un résultat vide signifie que chaque ligne de présence a trouvé son élève. Un résultat non vide est une liste à rapporter à qui tient la liste des élèves.

Ce qui vient ensuite

Les verbes sont acquis. La leçon suivante est celle vers laquelle ils mènent et celle où naissent la plupart des erreurs d’indicateurs : group_by() et summarise(), et les trois choses que R fait des clés manquantes, des niveaux inutilisés et du regroupement résiduel — à chaque fois à l’inverse de pandas.

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.