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 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
- Packages :
tidyverse,janitor(utile),lubridate.
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.
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.
select: colonnesfilter: lignesmutate: créer / modifier des variablesarrange: trisummarise+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éponseRemplacer 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
- À partir de
ventes, sélectionnezproduitetmontant, triez par montant décroissant. - Comptez les NA de
region.
6.19.2 Niveau 2
- Calculez le CA total par
produit. - Joignez
commandesetclientset calculez le montant moyen parsegment.
6.19.3 Niveau 3
- Nettoyez
data/brut/ventes_brutes.csv: noms propres (clean_names), types, suppression doublons, filtre NA. - Pour
demographie, créeztranche_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
janitorpour les noms de colonnes
6.23 Suite
Chapter 7 : résumer et interpréter avec des statistiques descriptives.