cassionAnalyse de données

Leçon 2 sur 8

Unité · Un projet qui se rouvre

Lire un export avec readr et readxl

Des spécifications de colonnes plutôt que des devinettes, problems() comme habitude et non comme sauvetage, et les pièges d'Excel — feuilles multiples, en-tête qui n'est pas la ligne un, cellules fusionnées et dates en numéro de série.

R85 min

readr devine, et vous dit ce qu’il a deviné

library(readr)
library(here)

PATH <- here("data", "raw", "muac-screening-artibonite-2024.v1.csv")
muac <- read_csv(PATH)

read_csv() affiche la spécification qu’il a inférée. Ce message n’est pas un bruit à masquer — c’est le seul moment où le lecteur vous dit ce qu’il a décidé, et sur ce registre il décide huit choses :

child_id       character
commune        character
screening_date date
age_months     numeric
sex            character
muac_mm        numeric
oedema         character
outcome        character

L’essentiel est juste. Deux points méritent que vous en repreniez le contrôle.

La devinette porte sur les 1 000 premières lignes

guess_max vaut 1 000 par défaut. Une colonne vide sur les mille premières lignes puis numérique ensuite est devinée comme logique — car NA est logique — et chaque valeur ultérieure devient NA.

C’est l’équivalent readr de l’avertissement de types mixtes de pandas, et il échoue plus discrètement : vous obtenez une colonne pleine de valeurs manquantes et un tableau problems() que vous n’avez pas regardé.

# Deliberement : tout lire en texte, regarder, puis convertir.
peek <- read_csv(PATH, col_types = cols(.default = col_character()), n_max = 200)
print(peek)

Deux cents lignes suffisent à voir la forme et coûtent peu sur un gros fichier.

Déclarez les colonnes

muac <- read_csv(
  PATH,
  col_types = cols(
    child_id       = col_character(),
    commune        = col_character(),
    screening_date = col_date(format = "%Y-%m-%d"),
    age_months     = col_integer(),
    sex            = col_character(),
    muac_mm        = col_integer(),
    oedema         = col_character(),
    outcome        = col_character()
  ),
  na = c("", "NA", "-99")
)

Trois avantages.

Les zéros initiaux survivent. col_character() sur un identifiant est toute la leçon : un code de formation sanitaire 007 lu comme un nombre devient 7, la jointure à la liste des formations échoue exactement pour les codes qui portaient un zéro initial, et la défaillance ressemble à des formations manquantes plutôt qu’à une erreur de type.

La règle se généralise : si vous n’allez jamais faire d’arithmétique dessus, ce n’est pas un nombre. Codes de formations, numéros de téléphone, identifiants de grappes, numéros de ménages et codes administratifs sont du texte qui s’écrit avec des chiffres.

Le format de date est énoncé. Un format inféré peut changer d’un fichier à l’autre selon la composition des valeurs. 01/02/2024 est soit le 1er février, soit le 2 janvier, et le fichier ne vous dira pas lequel.

La sentinelle est déclarée. Ce registre code un PB non mesuré par -99. Lu sans na, c’est un nombre :

mean(muac$muac_mm)
#> [1] NA

Ce qui nous amène à la différence la plus importante avec pandas.

mean() renvoie NA, et c’est une qualité

pandas ignore les valeurs manquantes par défaut ; R non.

mean(muac$muac_mm)
#> [1] NA

mean(muac$muac_mm, na.rm = TRUE)
#> [1] 139.92

R est bruyant là où pandas est silencieux. Le NA est une question : savez-vous qu’il y a ici des valeurs manquantes, et avez-vous décidé de ce qu’elles signifient ? Y répondre par na.rm = TRUE est parfaitement légitime — y répondre sans avoir remarqué qu’on vous interrogeait, c’est ainsi qu’un dénominateur change en silence.

na.rm = TRUE retire les manquants du numérateur et du dénominateur. C’est généralement juste pour une moyenne et généralement faux pour un taux de couverture, où l’enfant non mesuré reste un enfant dépisté. Comptez-les :

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

na s’applique au fichier, non à une colonne

muac <- read_csv(PATH, na = c("", "NA", "-99"))

readr applique na à toutes les colonnes. pandas sait restreindre une sentinelle à une colonne avec na_values = {"muac_mm": ["-99"]} ; readr non. Si une autre colonne porte légitimement -99, traitez-la ensuite :

muac <- read_csv(PATH, na = c("", "NA")) |>
  dplyr::mutate(muac_mm = dplyr::na_if(muac_mm, -99L))

na_if() est la forme restreinte, à préférer dès que plus d’une colonne est numérique.

problems() est une habitude, non un sauvetage

Quand une valeur ne correspond pas à son type déclaré, readr ne s’arrête pas. Il consigne la ligne, la colonne, ce qu’il attendait et ce qu’il a trouvé.

issues <- problems(muac)
nrow(issues)
#> [1] 0

Zéro sur ce fichier, et c’est justement l’intérêt de la vérification : le chiffre n’a de sens que si vous le regardez à chaque fois. Une exécution qui a discrètement transformé 200 valeurs en NA ressemble en tout point à une exécution propre tant que vous ne demandez pas.

if (nrow(problems(muac)) > 0) {
  print(problems(muac))
  stop("L'entree ne correspond pas a sa specification de colonnes.")
}

Dans un script produisant un chiffre rapporté, ce stop() est correct. Refuser de s’exécuter vaut mieux que produire un tableau à colonne silencieusement vide.

Vérifier la lecture avant d’en faire quoi que ce soit

Quatre contrôles, une minute, et ils attrapent presque tout :

library(dplyr)

check <- function(df, expected_rows = NULL) {
  cat("lignes/colonnes ", nrow(df), ncol(df), "\n")
  print(dplyr::glimpse(df))
  print(colSums(is.na(df)))
  if (!is.null(expected_rows)) {
    stopifnot(nrow(df) == expected_rows)
  }
}

check(muac, expected_rows = 4218)

L’assertion sur le nombre de lignes est celle qu’on saute. Un fichier arrivant avec 4 190 lignes au lieu de 4 218 a perdu quelque chose entre le serveur et vous, et le script doit le dire plutôt que de rapporter discrètement une charge de cas plus faible.

Séparateurs, encodages et milliers

Un fichier produit sur une machine Windows française ou espagnole est très souvent séparé par des points-virgules, car la virgule y est le séparateur décimal.

epi <- read_csv2(PATH)                       # separateur ; decimale ,
epi <- read_delim(PATH, delim = "\t")        # tabulations

read_csv2() est la variante européenne — point-virgule et virgule — et elle existe précisément parce que le cas est assez courant pour mériter sa propre fonction.

L’encodage se manifeste dans les noms de communes : Gonaïves, L’Estère, Anse-Rouge.

muac <- read_csv(PATH, locale = locale(encoding = "UTF-8"))
muac <- read_csv(PATH, locale = locale(encoding = "latin1"))   # ce qu'Excel ecrit souvent

latin1 ne lève jamais d’erreur, car tout octet est un caractère latin1 valide. Cela en fait un mauvais diagnostic et un repli correct : si un fichier lu en latin1 affiche Gonaïves, il était en UTF-8 depuis le début et vous avez dit le contraire au lecteur.

Les séparateurs de milliers transforment un nombre en texte :

epi <- read_csv(PATH, locale = locale(grouping_mark = ","))

Sans cela, target_population arrive en "1,480" — une colonne de caractères sur laquelle toute opération arithmétique échoue ou concatène.

Excel

library(readxl)

excel_sheets(path)
#> [1] "Cover" "Data" "Codebook" "Sheet3"

data <- read_excel(path, sheet = "Data")

read_excel() sans sheet renvoie la première feuille, qui dans un classeur de programme est le plus souvent une page de garde. Listez toujours les feuilles d’abord.

L’en-tête n’est pas la ligne un

data <- read_excel(path, sheet = "Data", skip = 3)

Les classeurs de programme portent couramment un titre, une ligne de logo et une ligne vide au-dessus du véritable en-tête. Le symptôme, ce sont des noms de colonnes comme ...1, ...2.

Les cellules fusionnées

Une cellule fusionnée porte sa valeur en haut à gauche et rien dans le reste. Quand un nom de district est fusionné sur ses formations, seule la première ligne de chaque district en porte un :

data <- data |> tidyr::fill(district, .direction = "down")

fill() est correct ici et dangereux en général : il n’est juste que parce que les blancs viennent d’une fusion, non d’une non-réponse. Ne l’appliquez jamais à une colonne où un blanc pourrait signifier « non renseigné ».

Les dates en numéro de série

Excel stocke les dates comme un nombre de jours depuis le 30/12/1899. read_excel() les convertit généralement, mais une colonne typée en texte dans le classeur arrive sous forme de numéro de série brut.

data$screening_date <- as.Date(as.numeric(data$screening_date), origin = "1899-12-30")

Si une colonne de dates arrive en nombres à cinq chiffres, voilà pourquoi.

Les types sous Excel

data <- read_excel(
  path,
  sheet = "Data",
  col_types = c("text", "text", "date", "numeric", "text", "numeric", "text", "text")
)

readxl attend un vecteur positionnel plutôt qu’une spécification nommée : une colonne ajoutée à l’export décale donc tout ce qui suit. Vérifiez ncol() face à la longueur de votre vecteur, ou lisez en texte et convertissez avec dplyr.

La fonction de lecture, assemblée

Tout ce qui précède appartient à une fonction unique que les scripts partagent :

# R/read_register.R
read_register <- function(path) {
  muac <- readr::read_csv(
    path,
    col_types = readr::cols(
      child_id       = readr::col_character(),
      commune        = readr::col_character(),
      screening_date = readr::col_date(format = "%Y-%m-%d"),
      age_months     = readr::col_integer(),
      sex            = readr::col_character(),
      muac_mm        = readr::col_integer(),
      oedema         = readr::col_character(),
      outcome        = readr::col_character()
    ),
    na = c("", "NA", "-99")
  )

  if (nrow(readr::problems(muac)) > 0) {
    print(readr::problems(muac))
    stop("L'entree ne correspond pas a sa specification de colonnes.")
  }
  stopifnot(!any(is.na(muac$child_id)))

  muac
}

Une fonction, un seul endroit à corriger quand le prochain export changera.

Ce qui vient ensuite

Le CSV est entré et ses types sont justes. La leçon suivante traite les fichiers qui transportent plus que des valeurs — les exports Stata et SPSS, où 1 = Oui voyage avec les données et où haven est la raison de les lire sous R même si vous les analyserez ailleurs.

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.