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.
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.