6  Nettoyage et préparation des données

6.1 Introduction

Des données mal préparées produisent des analyses trompeuses. Ce chapitre enseigne le nettoyage et la transformation avec dplyr, tidyr, stringr et lubridate : renommer, filtrer, créer des variables, traiter les NA, détecter doublons et aberrants, joindre des tables et pivoter.

6.2 Objectifs pédagogiques

  • Examiner et comprendre la structure d’un jeu de données
  • Chaîner des verbes dplyr (%>% / |>)
  • Traiter NA, doublons et valeurs aberrantes
  • Manipuler textes et dates
  • Agréger, joindre et pivoter
  • Construire un pipeline reproductible

6.3 Prérequis

  1. Packages : tidyverse, janitor (utile), lubridate.
library(tidyverse)
library(lubridate)
if (requireNamespace("janitor", quietly = TRUE)) {
  library(janitor)
} else {
  message("Package janitor non installé : clean_names() sera illustré sans exécution forcée.")
}

6.4 Qualité des données

Une bonne qualité implique : types corrects, noms cohérents, peu de NA injustifiés, pas de doublons involontaires, valeurs dans des plages plausibles, documentation des règles de nettoyage.

Règle d’or

Ne modifiez jamais le fichier brut. Créez une version nettoyée via un script.

6.5 Examiner la structure

ventes <- read_csv("data/ventes.csv", show_col_types = FALSE)
glimpse(ventes)
#> Rows: 201
#> Columns: 7
#> $ id_vente      <chr> "V0001", "V0002", "V0003", "V0004", "V0005", "V0006", "V…
#> $ date          <date> 2023-03-07, 2023-02-06, 2023-02-14, 2023-07-07, 2023-09…
#> $ region        <chr> "Nord", "Nord", "Nord", "Ouest", NA, "Ouest", "Nord", "S…
#> $ produit       <chr> "Cahier", "Agenda", "Stylo", "Marqueur", "Cahier", "Clas…
#> $ quantite      <dbl> 6, 19, 4, 20, 10, 6, 14, 3, 18, 1, 17, 9, 15, 9, 7, 1, 8…
#> $ prix_unitaire <dbl> 7.32, 5.45, 9.78, 3.16, 13.57, 12.47, 19.46, 23.10, 16.9…
#> $ montant       <dbl> 43.92, 103.55, 39.12, 63.20, 135.70, 74.82, 272.44, 69.3…
skim_lite <- summary(ventes)

6.6 Renommer, sélectionner, filtrer, trier

ventes_n <- ventes |>
  rename(ca = montant) |>
  select(id_vente, date, region, produit, quantite, ca) |>
  filter(!is.na(region), ca > 0) |>
  arrange(desc(ca))

head(ventes_n, 3)
#> # A tibble: 3 × 6
#>   id_vente date       region produit  quantite    ca
#>   <chr>    <date>     <chr>  <chr>       <dbl> <dbl>
#> 1 V0072    2023-03-14 Est    Cahier         20  483.
#> 2 V0141    2023-10-23 Sud    Marqueur       18  445.
#> 3 V0109    2023-08-01 Nord   Cahier         19  431.
Vocabulaire dplyr
  • select : colonnes
  • filter : lignes
  • mutate : créer / modifier des variables
  • arrange : tri
  • summarise + group_by : agrégats

6.7 Créer des variables et modifier les types

ventes2 <- ventes |>
  mutate(
    date = as.Date(date),
    mois = floor_date(date, "month"),
    trimestre = quarter(date),
    categorie_ca = case_when(
      montant < 20 ~ "petit",
      montant < 100 ~ "moyen",
      TRUE ~ "gros"
    ),
    region = factor(region)
  )
table(ventes2$categorie_ca)
#> 
#>  gros moyen petit 
#>   104    77    20

6.8 Traiter les valeurs manquantes

colSums(is.na(ventes))
#>      id_vente          date        region       produit      quantite 
#>             0             0             2             0             1 
#> prix_unitaire       montant 
#>             0             0
# Options selon le contexte :
# 1. Filtrer
ventes_complet <- ventes |> filter(!is.na(region), !is.na(quantite))

# 2. Imputer une valeur (avec prudence)
ventes_imp <- ventes |>
  mutate(quantite = replace_na(quantite, median(quantite, na.rm = TRUE)))

# 3. Documenter et garder les NA pour analyse de non-réponse
Ne pas imputer « par réflexe »

Remplacer tous les NA par 0 peut biaiser une moyenne. Choisissez une règle motivée et notez-la dans le rapport.

6.9 Doublons

sum(duplicated(ventes))
#> [1] 1
ventes_uniques <- ventes |> distinct()
nrow(ventes) - nrow(ventes_uniques)
#> [1] 1

6.10 Valeurs aberrantes

demo <- read_csv("data/demographie.csv", show_col_types = FALSE)

# Règle IQR pour l'âge
Q1 <- quantile(demo$age, 0.25)
Q3 <- quantile(demo$age, 0.75)
IQR_age <- Q3 - Q1
bornes <- c(Q1 - 1.5 * IQR_age, Q3 + 1.5 * IQR_age)
bornes
#>   25%   75% 
#> -15.5 108.5
aberrants_age <- demo |> filter(age < bornes[1] | age > bornes[2])
aberrants_age
#> # A tibble: 1 × 6
#>      id   age sexe  ville revenu_mensuel niveau_etudes
#>   <dbl> <dbl> <chr> <chr>          <dbl> <chr>        
#> 1     3   150 Femme Paris           1104 Bac

Les aberrants ne sont pas automatiquement des erreurs : un revenu très élevé peut être réel. Examinez avant de supprimer.

6.11 Chaînes de caractères (stringr)

produits <- ventes |>
  mutate(
    produit_min = str_to_lower(produit),
    commence_c = str_detect(produit, "^C"),
    longueur = str_length(produit)
  )
head(produits[, c("produit", "produit_min", "commence_c")], 4)
#> # A tibble: 4 × 3
#>   produit  produit_min commence_c
#>   <chr>    <chr>       <lgl>     
#> 1 Cahier   cahier      TRUE      
#> 2 Agenda   agenda      FALSE     
#> 3 Stylo    stylo       FALSE     
#> 4 Marqueur marqueur    FALSE

6.12 Dates (lubridate)

meteo <- read_csv("data/meteo.csv", show_col_types = FALSE) |>
  mutate(
    date = ymd(date),
    annee = year(date),
    mois = month(date, label = TRUE, abbr = TRUE),
    jour_semaine = wday(date, label = TRUE)
  )
head(meteo[, c("date", "mois", "jour_semaine")], 3)
#> # A tibble: 3 × 3
#>   date       mois  jour_semaine
#>   <date>     <ord> <ord>       
#> 1 2023-01-01 janv  "dim\\."    
#> 2 2023-01-02 janv  "lun\\."    
#> 3 2023-01-03 janv  "mar\\."

6.13 Regrouper et résumer

ca_region <- ventes |>
  filter(!is.na(region)) |>
  group_by(region) |>
  summarise(
    n = n(),
    ca_total = sum(montant, na.rm = TRUE),
    ca_moyen = mean(montant, na.rm = TRUE),
    .groups = "drop"
  ) |>
  arrange(desc(ca_total))
ca_region
#> # A tibble: 4 × 4
#>   region     n ca_total ca_moyen
#>   <chr>  <int>    <dbl>    <dbl>
#> 1 Ouest     59    7621.     129.
#> 2 Est       51    7164.     140.
#> 3 Sud       42    5995.     143.
#> 4 Nord      47    5096.     108.

6.14 Fusionner plusieurs tables

clients <- read_csv("data/clients.csv", show_col_types = FALSE)
commandes <- read_csv("data/commandes.csv", show_col_types = FALSE)

commandes_enrichies <- commandes |>
  left_join(clients, by = "client_id")

head(commandes_enrichies, 3)
#> # A tibble: 3 × 6
#>   commande_id client_id montant date       nom       segment    
#>   <chr>       <chr>       <dbl> <date>     <chr>     <chr>      
#> 1 CMD001      C004         271. 2023-02-16 Client_4  Association
#> 2 CMD002      C028         286. 2023-09-11 Client_28 Association
#> 3 CMD003      C018         190. 2023-02-07 Client_18 Particulier
# CA par segment
commandes_enrichies |>
  group_by(segment) |>
  summarise(ca = sum(montant), n = n(), .groups = "drop")
#> # A tibble: 3 × 3
#>   segment        ca     n
#>   <chr>       <dbl> <int>
#> 1 Association 9855.    36
#> 2 Entreprise  3747.    13
#> 3 Particulier 6393.    31

Types de jointures : left_join, inner_join, full_join, anti_join (non appariés).

6.15 Large ↔︎ long (tidyr)

# Exemple long → large
ca_long <- ca_region |> select(region, ca_total)
ca_wide <- ca_long |>
  pivot_wider(names_from = region, values_from = ca_total)
ca_wide
#> # A tibble: 1 × 4
#>   Ouest   Est   Sud  Nord
#>   <dbl> <dbl> <dbl> <dbl>
#> 1 7621. 7164. 5995. 5096.
# Large → long
ca_long2 <- ca_wide |>
  pivot_longer(everything(), names_to = "region", values_to = "ca_total")
ca_long2
#> # A tibble: 4 × 2
#>   region ca_total
#>   <chr>     <dbl>
#> 1 Ouest     7621.
#> 2 Est       7164.
#> 3 Sud       5995.
#> 4 Nord      5096.

6.16 Pipeline de préparation reproductible

preparer_ventes <- function(chemin = "data/ventes.csv") {
  read_csv(chemin, show_col_types = FALSE) |>
    distinct() |>
    mutate(
      date = as.Date(date),
      region = str_trim(region)
    ) |>
    filter(!is.na(region), !is.na(quantite), montant > 0) |>
    mutate(
      mois = floor_date(date, "month"),
      produit = factor(produit),
      region = factor(region)
    )
}

ventes_clean <- preparer_ventes()
glimpse(ventes_clean)
#> Rows: 197
#> Columns: 8
#> $ id_vente      <chr> "V0001", "V0002", "V0003", "V0004", "V0006", "V0007", "V…
#> $ date          <date> 2023-03-07, 2023-02-06, 2023-02-14, 2023-07-07, 2023-04…
#> $ region        <fct> Nord, Nord, Nord, Ouest, Ouest, Nord, Sud, Est, Ouest, O…
#> $ produit       <fct> Cahier, Agenda, Stylo, Marqueur, Classeur, Stylo, Stylo,…
#> $ quantite      <dbl> 6, 19, 4, 20, 6, 14, 3, 18, 1, 17, 9, 15, 9, 7, 1, 8, 11…
#> $ prix_unitaire <dbl> 7.32, 5.45, 9.78, 3.16, 12.47, 19.46, 23.10, 16.95, 11.3…
#> $ montant       <dbl> 43.92, 103.55, 39.12, 63.20, 74.82, 272.44, 69.30, 305.1…
#> $ mois          <date> 2023-03-01, 2023-02-01, 2023-02-01, 2023-07-01, 2023-04…
write_csv(ventes_clean, "data/ventes_clean.csv")

6.17 Démonstration complète

demo_clean <- demo |>
  filter(age >= 18, age <= 100) |>
  mutate(
    revenu_mensuel = if_else(revenu_mensuel > 20000, NA_real_, revenu_mensuel),
    sexe = factor(sexe),
    ville = factor(ville)
  )

demo_clean |>
  group_by(ville) |>
  summarise(
    n = n(),
    age_moyen = mean(age),
    revenu_median = median(revenu_mensuel, na.rm = TRUE),
    .groups = "drop"
  )
#> # A tibble: 5 × 4
#>   ville         n age_moyen revenu_median
#>   <fct>     <int>     <dbl>         <dbl>
#> 1 Lille        24      49.4         1994.
#> 2 Lyon         40      44.6         2289 
#> 3 Marseille    34      49.1         2118.
#> 4 Paris        25      45.6         2338 
#> 5 Toulouse     26      45.4         2257

6.18 Erreurs fréquentes

Erreur Cause Solution
filter(region == NA) Mauvaise syntaxe is.na(region) / !is.na
Jointure qui multiplie les lignes Clé non unique Vérifier count(client_id)
summarise sans group_by voulu Oubli Contrôler le nombre de lignes résultat
Dates en character Pas de parsing ymd() / as.Date()

6.19 Exercices

6.19.1 Niveau 1

  1. À partir de ventes, sélectionnez produit et montant, triez par montant décroissant.
  2. Comptez les NA de region.

6.19.2 Niveau 2

  1. Calculez le CA total par produit.
  2. Joignez commandes et clients et calculez le montant moyen par segment.

6.19.3 Niveau 3

  1. Nettoyez data/brut/ventes_brutes.csv : noms propres (clean_names), types, suppression doublons, filtre NA.
  2. Pour demographie, créez tranche_age (18–30, 31–50, 51+) et un résumé du revenu médian par tranche.

6.20 Corrections

ventes |> select(produit, montant) |> arrange(desc(montant)) |> head(5)
#> # A tibble: 5 × 2
#>   produit  montant
#>   <chr>      <dbl>
#> 1 Cahier      483.
#> 2 Marqueur    445.
#> 3 Cahier      431.
#> 4 Marqueur    405.
#> 5 Cahier      386
sum(is.na(ventes$region))
#> [1] 2
ventes |>
  group_by(produit) |>
  summarise(ca = sum(montant, na.rm = TRUE), .groups = "drop")
#> # A tibble: 5 × 2
#>   produit     ca
#>   <chr>    <dbl>
#> 1 Agenda   4354.
#> 2 Cahier   5520.
#> 3 Classeur 4978.
#> 4 Marqueur 5856.
#> 5 Stylo    5326.
commandes |>
  left_join(clients, by = "client_id") |>
  group_by(segment) |>
  summarise(montant_moyen = mean(montant), .groups = "drop")
#> # A tibble: 3 × 2
#>   segment     montant_moyen
#>   <chr>               <dbl>
#> 1 Association          274.
#> 2 Entreprise           288.
#> 3 Particulier          206.
brut_clean <- read_csv("data/brut/ventes_brutes.csv", show_col_types = FALSE) |>
  janitor::clean_names() |>
  rename(
    id_vente = id,
    date = date_vente,
    quantite = qte,
    montant = total
  ) |>
  mutate(date = as.Date(date), produit = str_trim(produit)) |>
  distinct() |>
  filter(!is.na(region))
nrow(brut_clean)
#> [1] 198
demo |>
  mutate(
    tranche_age = case_when(
      age <= 30 ~ "18-30",
      age <= 50 ~ "31-50",
      TRUE ~ "51+"
    )
  ) |>
  group_by(tranche_age) |>
  summarise(revenu_median = median(revenu_mensuel, na.rm = TRUE), n = n(), .groups = "drop")
#> # A tibble: 3 × 3
#>   tranche_age revenu_median     n
#>   <chr>               <dbl> <int>
#> 1 18-30                2226    37
#> 2 31-50                2133    44
#> 3 51+                  2215    69

6.21 Points clés

  • Un pipeline clair bat une série de clics manuels.
  • Chaque transformation doit être justifiée et vérifiable.
  • Jointures et pivots sont au cœur de la mise en forme analytique.

6.22 Ressources

  • Wickham et al. (2019), documentation dplyr / tidyr
  • Package janitor pour les noms de colonnes

6.23 Suite

Chapter 7 : résumer et interpréter avec des statistiques descriptives.