Aide-mémoire SAS - R - pandas

SAS
R
Pandas

Un aide-mémoire pour les statisticiens traduisant des codes standards en SAS, en R, suivant 4 environnements (R base, tidyverse, data.table, arrow/duckdb) et en python pandas.

Auteur·rice·s
Affiliations

Nassab ABDALLAH

Dares/SD-STRP/SCS,

Damien EUZENAT

Dares/SD-SEPEFP/DIP,

Sébastien LI-THIAO-TE

Clotilde NIETGE

Date de publication

30 avril 2025

L’aide-mémoire a pour but de fournir des codes écrits en SAS et d’en donner la traduction en différents environnements R :

et en python pandas.

Les codes traduits sont typiques de la production statistique ou la réalisation d’études descriptives.

Ce document vise à faciliter la compréhension ou la traduction de codes ainsi que le passage d’un langage présenté à un autre. Il s’adresse notamment aux utilisateurs d’un de ces langages qui souhaitent comprendre ou traduire des codes écrits dans un autre langage.

Il se veut complémentaire de la documentation en ligne en français Utilit’R, née à l’Insee (https://www.book.utilitr.org/). Le lecteur est invité à s’y référer pour obtenir des informations importantes sur l’utilisation de R et qui ne sont pas discutées dans ce document, comme l’importation de données en R (https://www.book.utilitr.org/03_fiches_thematiques/fiche_import_fichiers_plats).

Enfin, si vous souhaitez collaborer à cet aide-mémoire ou nous faire part de votre avis, n’hésitez pas à nous contacter via nos adresses email.

1 Importation des packages

1.1 Installation des packages

Des informations sur l’installation des packages en R sont disponibles sur le site Utilit’R : https://book.utilitr.org/01_R_Insee/Fiche_installer_packages.html.

/* Sans objet pour SAS */
# Les packages doivent au préalable être installés sur le disque dur
# Pour installer un package :
# install.packages("nom_du_package")
# Les packages doivent au préalable être installés sur le disque dur
# Pour installer un package :
# install.packages("nom_du_package")
# Les packages doivent au préalable être installés sur le disque dur
# Pour installer un package :
# install.packages("nom_du_package")
# Les packages doivent au préalable être installés sur le disque dur
# Pour installer un package :
# install.packages("nom_du_package")
# Commande à écrire dans le prompt d'Anaconda
# Pour installer un package :
# pip install nom_du_package

1.2 Importation des packages

/* Sans objet pour SAS */
# Sans objet pour R-Base

# Cependant, on importe le package lubridate pour faciliter la gestion des dates
library(lubridate)

# Documentation de R base
?"[.data.frame"
# Chargement des packages
# Le tidyverse proprement dit
library(tidyverse)
# Les packages importés par le tidyverse sont :
# - dplyr (manipulation de données)
# - tidyr (réorganisation de bases de données)
# - readr (importation de données)
# - purrr (permet de réaliser des boucles)
# - tibble (format de données tibble, complémentaire du data.frame)
# - stringr (manipulation de chaînes de caractères)
# - ggplot2 (création de graphiques)
# - forcats (gestion des formats "factors")

# Pour manipuler les dates
library(lubridate)
# Pour utiliser le pipe %>%
library(magrittr)

# Documentation de tidyverse
vignette("dplyr")
library(data.table)
# Pour manipuler les dates
library(lubridate)

# Documentation de data.table
?'[.data.table'
#library(duckdb)
#library(arrow)
import pandas as pd
import numpy as np
from datetime import datetime

1.3 Documentation (Utilit’R, cheatsheets, etc.)

1.4 Documentation pour RStudio

Sans objet pour SAS.

Si vous utilisez l’IDE RStudio : https://rstudio.github.io/cheatsheets/rstudio-ide.pdf

Si l’encodage du fichier de programme ouvert ne convient pas, Fichier -> Reopen with Enccoding… -> et choisir le bon encodage.

Plusieurs raccourcis clavier sont notamment très utiles :

Raccourcis RStudio
Raccourci Effet
Alt et - ->
Ctrl et Shift et m %>%
Ctrl et Entrée Exécuter le code sélectionné ou de la ligne où se trouve le curseur
Alt et Entrée Exécuter le code jusqu’à la ligne où se trouve le curseur
Ctrl et Shift et a Reformater automatiquement le code sélectionné pour qu’il soit plus lisible
Alt et flèche de droite ou de gauche Aller directement à la fin (flèche de droite) ou au début (flèche de gauche) de la ligne
Alt et flèche du haut ou du bas Intervertir la ligne avec celle du dessus (flèche du haut) ou du dessous (flèche du bas)
Ctrl et flèche de droite ou de gauche Passer d’un mot à l’autre de la ligne
Alt et déplacement du curseur de la souris en haut ou bas Permet de modifier simultanément le même emplacement de plusieurs lignes successives
Ctrl et Shift et U Mettre en minuscule les caractères sélectionnés
Ctrl et L Vider la console
Ctrl et Shift et F10 Relancer une session R

Si vous utilisez l’IDE RStudio : https://rstudio.github.io/cheatsheets/rstudio-ide.pdf

Si l’encodage du fichier de programme ouvert ne convient pas, Fichier -> Reopen with Enccoding… -> et choisir le bon encodage.

Plusieurs raccourcis clavier sont notamment très utiles :

Raccourcis RStudio
Raccourci Effet
Alt et - ->
Ctrl et Shift et m %>%
Ctrl et Entrée Exécuter le code sélectionné ou de la ligne où se trouve le curseur
Alt et Entrée Exécuter le code jusqu’à la ligne où se trouve le curseur
Ctrl et Shift et a Reformater automatiquement le code sélectionné pour qu’il soit plus lisible
Alt et flèche de droite ou de gauche Aller directement à la fin (flèche de droite) ou au début (flèche de gauche) de la ligne
Alt et flèche du haut ou du bas Intervertir la ligne avec celle du dessus (flèche du haut) ou du dessous (flèche du bas)
Ctrl et flèche de droite ou de gauche Passer d’un mot à l’autre de la ligne
Alt et déplacement du curseur de la souris en haut ou bas Permet de modifier simultanément le même emplacement de plusieurs lignes successives
Ctrl et Shift et U Mettre en minuscule les caractères sélectionnés
Ctrl et L Vider la console
Ctrl et Shift et F10 Relancer une session R

Si vous utilisez l’IDE RStudio : https://rstudio.github.io/cheatsheets/rstudio-ide.pdf

Si l’encodage du fichier de programme ouvert ne convient pas, Fichier -> Reopen with Enccoding… -> et choisir le bon encodage.

Plusieurs raccourcis clavier sont notamment très utiles :

Raccourcis RStudio
Raccourci Effet
Alt et - ->
Ctrl et Shift et m %>%
Ctrl et Entrée Exécuter le code sélectionné ou de la ligne où se trouve le curseur
Alt et Entrée Exécuter le code jusqu’à la ligne où se trouve le curseur
Ctrl et Shift et a Reformater automatiquement le code sélectionné pour qu’il soit plus lisible
Alt et flèche de droite ou de gauche Aller directement à la fin (flèche de droite) ou au début (flèche de gauche) de la ligne
Alt et flèche du haut ou du bas Intervertir la ligne avec celle du dessus (flèche du haut) ou du dessous (flèche du bas)
Ctrl et flèche de droite ou de gauche Passer d’un mot à l’autre de la ligne
Alt et déplacement du curseur de la souris en haut ou bas Permet de modifier simultanément le même emplacement de plusieurs lignes successives
Ctrl et Shift et U Mettre en minuscule les caractères sélectionnés
Ctrl et L Vider la console
Ctrl et Shift et F10 Relancer une session R

Sans objet pour pandas.

2 Importation des données

2.1 Mode d’emploi de l’aide-mémoire

Les codes informatiques sont appliqués sur une base de données illustrative fictive sur les formations. Cette base est importée à cette étape. Aussi, pour répliquer les codes sur sa machine, le lecteur doit d’abord exécuter le code d’importation de la base de données ci-dessous.

Les codes sont majoritairement exécutables indépendamment les uns des autres. Les codes de la partie “Les jointures de bases” nécessitent cependant l’importation des bases réalisée lors de la première section de la partie.

2.2 Création d’une base de données d’exemple

/* Données fictives sur des formations */
data donnees_sas;
  infile cards dsd dlm='|';
  format Identifiant $3. Sexe 1. CSP $1. Niveau $30. Date_naissance ddmmyy10. Date_entree ddmmyy10. Duree Note_Contenu Note_Formateur Note_Moyens
         Note_Accompagnement Note_Materiel poids_sondage 4.1 CSPF $25. Sexef $5.;
  input Identifiant $ Sexe CSP $ Niveau $ Date_naissance :ddmmyy10. Date_entree :ddmmyy10. Duree Note_Contenu Note_Formateur Note_Moyens
        Note_Accompagnement Note_Materiel poids_sondage CSPF $ Sexef $;
  cards;
  173|2|1|Qualifié|17/06/1998|01/01/2021|308|12|6|17|4|19|117.1|Cadre|Femme
  173|2|1|Qualifié|17/06/1998|01/01/2022|365|6||12|7|14|98.3|Cadre|Femme
  173|2|1|Qualifié|17/06/1998|06/01/2022|185|8|10|11|1|9|214.6|Cadre|Femme
  173|2|1|Non qualifié|17/06/1998|02/01/2023|365|14|15|15|10|8|84.7|Cadre|Femme
  174|1|1|Qualifié|08/12/1984|17/08/2021|183|17|18|20|15|12|65.9|Cadre|Homme
  175|1|1|Qualifié|16/09/1989|21/12/2022|730|5|5|8|4|9|148.2|Cadre|Homme
  198|2|3|Non qualifié|17/03/1987|28/07/2022|30|10|10|10|16|8|89.6|Employé|Femme
  198|2|3|Qualifié|17/03/1987|17/11/2022|164|11|7|6|14|13|100.3|Employé|Femme
  198|2|3|Qualifié|17/03/1987|21/02/2023|365|9|20|3|4|17|49.3|Employé|Femme
  168|1|2|Qualifié|30/07/2002|04/09/2019|365|18|11|20|13|15|148.2|Profession intermédiaire|Homme
  211|2|3|Non qualifié||17/12/2021|135|16|16|15|12|9|86.4|Employé|Femme
  278|1|5|Qualifié|10/08/1948|07/06/2018|365|14|10|6|8|12|99.2|Retraité|Homme
  347|2|5|Qualifié|13/09/1955||180|12|5|7|11|12|105.6|Retraité|Femme
  112|1|3|Non qualifié|13/09/2001|02/03/2022|212|3|10|11|9|8|123.1|Employé|Homme
  112|1|3|Non qualifié|13/09/2001|01/03/2021|365|7|13|8|19|2|137.4|Employé|Homme
  112|1|3|Qualifié|13/09/2001|01/12/2023|365|9|||||187.6|Employé|Homme
  087|2|4|Non qualifié|||365||10||||87.3|Ouvrier|Femme
  087|2|4|Non qualifié||31/10/2020|365|||11|||87.3|Ouvrier|Femme
  099|1|4|Qualifié|06/06/1998|01/03/2021|364|12|11|10|12|13|169.3|Ouvrier|Homme
  099|1|4|Qualifié|06/06/1998|01/03/2022|364|12|11|10|12|13|169.3|Ouvrier|Homme
  099|1|4|Qualifié|06/06/1998|01/03/2023|364|12|11|10|12|13|169.3|Ouvrier|Homme
  187|2|2|Qualifié|05/12/1986|01/01/2022|364|10|10|10|10|10|169.3|Profession intermédiaire|Femme
  187|2|2|Qualifié|05/12/1986|01/01/2023|364|10|10|10|10|10|234.1|Profession intermédiaire|Femme
  689|1|1||01/12/2000|06/11/2017|123|9|7|8|13|16|189.3|Cadre|Homme
  765|1|4|Non qualifié|26/12/1995|17/04/2020|160|13|10|12|18|10|45.9|Ouvrier|Homme
  765|1|4|Non qualifié|26/12/1995|17/04/2020|160|13|10|12|18|10|45.9|Ouvrier|Homme
  765|1|4|Non qualifié|26/12/1995|17/04/2020|160|13|10|12|18|10|45.9|Ouvrier|Homme
  ;
run;

/* Ajout de variables utiles */
data donnees_sas;
  set donnees_sas;
  /* Date de sortie du dispositif : ajout de la durée à la date d'entrée */
  format date_sortie ddmmyy10.;
  date_sortie = intnx('day', date_entree, duree);
  /* Âge à l'entrée dans le dispositif */
  Age = intck('year', date_naissance, date_entree);
run;
# Données fictives sur des formations
library(lubridate)
donnees_rbase <- data.frame(
  Identifiant = c("173", "173", "173", "173", "174", "175", "198", "198", "198", "168", "211", "278", "347", "112", "112", "112", "087", "087", "099", "099", "099", "187", "187", "689", "765", "765", "765"),
  Sexe = c("2", "2", "2", "2", "1", "1", "2", "2", "2", "1", "2", "1", "2", "1", "1", "1", "2", "2", "1", "1", "1", "2", "2", "1", "1", "1", "1"),
  CSP = c("1", "1", "1", "1", "1", "1", "3", "3", "3", "2", "3", "5", "5", "3", "3", "3", "4", "4", "4", "4", "4", "2", "2", "1", "4", "4", "4"),
  Niveau = c("Qualifié", "Qualifié", "Qualifié", "Non qualifié", "Qualifié", "Qualifié", "Non qualifié", "Qualifié", "Qualifié", "Qualifié", "Non qualifié", "Qualifié", "Qualifié", "Non qualifié", 
             "Non qualifié", "Qualifié", "Non qualifié", "Non qualifié", "Qualifié", "Qualifié", "Qualifié", "Qualifié", "Qualifié", NA, "Non qualifié", "Non qualifié", "Non qualifié"),
  Date_naissance = c("17/06/1998", "17/06/1998", "17/06/1998", "17/06/1998", "08/12/1984", "16/09/1989", "17/03/1987", "17/03/1987", "17/03/1987", "30/07/2002", NA, "10/08/1948", 
                     "13/09/1955", "13/09/2001", "13/09/2001", "13/09/2001", NA, NA, "06/06/1998", "06/06/1998", "06/06/1998", "05/12/1986", "05/12/1986", "01/12/2000", "26/12/1995", "26/12/1995", "26/12/1995"),
  Date_entree = c("01/01/2021", "01/01/2022", "06/01/2022", "02/01/2023", "17/08/2021", "21/12/2022", "28/07/2022", "17/11/2022", "21/02/2023", "04/09/2019", "17/12/2021", "07/06/2018", NA, "02/03/2022", "01/03/2021", "01/12/2023", NA, 
                  "31/10/2020", "01/03/2021", "01/03/2022", "01/03/2023", "01/01/2022", "01/01/2023", "06/11/2017", "17/04/2020", "17/04/2020", "17/04/2020"),
  Duree = c("308", "365", "185", "365", "183", "730", "30", "164", "365", "365", "135", "365", "180", "212", "365", "365", "365", "365", "364", "364", "364", "364", "364", "123", "160", "160", "160"),
  Note_Contenu = c("12", "6", "8", "14", "17", "5", "10", "11", "9", "18", "16", "14", "12", "3", "7", "9", NA, NA, "12", "12", "12", "10", "10", "9", "13", "13", "13"),
  Note_Formateur = c("6", NA, "10", "15", "18", "5", "10", "7", "20", "11", "16", "10", "5", "10", "13", NA, "10", NA, "11", "11", "11", "10", "10", "7", "10", "10", "10"),
  Note_Moyens = c("17", "12", "11", "15", "20", "8", "10", "6", "3", "20", "15", "6", "7", "11", "8", NA, NA, "11", "10", "10", "10", "10", "10", "8", "12", "12", "12"),
  Note_Accompagnement = c("4", "7", "1", "10", "15", "4", "16", "14", "4", "13", "12", "8", "11", "9", "19", NA, NA, NA, "12", "12", "12", "10", "10", "13", "18", "18", "18"),
  Note_Materiel = c("19", "14", "9", "8", "12", "9", "8", "13", "17", "15", "9", "12", "12", "8", "2", NA, NA, NA, "13", "13", "13", "10", "10", "16", "10", "10", "10"),
  poids_sondage = c("117.1", "98.3", "214.6", "84.7", "65.9", "148.2", "89.6", "100.3", "49.3", "148.2", "86.4", "99.2", "105.6", "123.1", "137.4", "187.6", "87.3", "87.3",
                    "169.3", "169.3", "169.3", "169.3", "234.1", "189.3", "45.9", "45.9", "45.9"),
  CSPF = c("Cadre", "Cadre", "Cadre", "Cadre", "Cadre","Cadre", "Employé", "Employé", "Employé", "Profession intermédiaire", "Employé", "Retraité", "Retraité", "Employé",
           "Employé", "Employé", "Ouvrier", "Ouvrier", "Ouvrier", "Ouvrier", "Ouvrier", "Profession intermédiaire", "Profession intermédiaire", "Cadre", "Ouvrier", "Ouvrier",
           "Ouvrier"),
  Sexef = c("Femme", "Femme", "Femme", "Femme", "Homme", "Homme", "Femme", "Femme", "Femme", "Homme", "Femme", "Homme", "Femme", "Homme", "Homme", "Homme", "Femme", "Femme",
            "Homme", "Homme", "Homme", "Femme", "Femme", "Homme", "Homme", "Homme", "Homme")
)

# Mise en forme des données

# R est sensible à la casse, il est pertinent d'harmoniser les noms des variables en minuscule
colnames(donnees_rbase) <- tolower(colnames(donnees_rbase))

# On a importé toutes les variables en format caractère
# On convertit certaines variables en format numérique
enNumerique <- c("duree", "note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
donnees_rbase[, enNumerique] <- lapply(donnees_rbase[, enNumerique], as.integer)
donnees_rbase$poids_sondage <- as.numeric(donnees_rbase$poids_sondage)

# On récupère les variables dont le nom débute par le mot "date"
enDate <- names(donnees_rbase)[grepl("date", tolower(names(donnees_rbase)))]
# On exprime les dates en format Date
donnees_rbase[, enDate] <- lapply(donnees_rbase[, enDate], lubridate::dmy)

# Date de sortie du dispositif
donnees_rbase$date_sortie <- donnees_rbase$date_entree + lubridate::days(donnees_rbase$duree)

# Âge à l'entrée dans le dispositif
donnees_rbase$age <- floor(lubridate::time_length(difftime(donnees_rbase$date_entree, donnees_rbase$date_naissance), "years"))
# Données fictives sur des formations
library(tidyverse)
library(lubridate)
donnees_tidyverse <- tibble(
  Identifiant = c("173", "173", "173", "173", "174", "175", "198", "198", "198", "168", "211", "278", "347", "112", "112", "112", "087", "087", "099", "099", "099", "187", "187", "689", "765", "765", "765"),
  Sexe = c("2", "2", "2", "2", "1", "1", "2", "2", "2", "1", "2", "1", "2", "1", "1", "1", "2", "2", "1", "1", "1", "2", "2", "1", "1", "1", "1"),
  CSP = c("1", "1", "1", "1", "1", "1", "3", "3", "3", "2", "3", "5", "5", "3", "3", "3", "4", "4", "4", "4", "4", "2", "2", "1", "4", "4", "4"),
  Niveau = c("Qualifié", "Qualifié", "Qualifié", "Non qualifié", "Qualifié", "Qualifié", "Non qualifié", "Qualifié", "Qualifié", "Qualifié", "Non qualifié", "Qualifié", "Qualifié", "Non qualifié", 
             "Non qualifié", "Qualifié", "Non qualifié", "Non qualifié", "Qualifié", "Qualifié", "Qualifié", "Qualifié", "Qualifié", NA, "Non qualifié", "Non qualifié", "Non qualifié"),
  Date_naissance = c("17/06/1998", "17/06/1998", "17/06/1998", "17/06/1998", "08/12/1984", "16/09/1989", "17/03/1987", "17/03/1987", "17/03/1987", "30/07/2002", NA, "10/08/1948", 
                     "13/09/1955", "13/09/2001", "13/09/2001", "13/09/2001", NA, NA, "06/06/1998", "06/06/1998", "06/06/1998", "05/12/1986", "05/12/1986", "01/12/2000", "26/12/1995", "26/12/1995", "26/12/1995"),
  Date_entree = c("01/01/2021", "01/01/2022", "06/01/2022", "02/01/2023", "17/08/2021", "21/12/2022", "28/07/2022", "17/11/2022", "21/02/2023", "04/09/2019", "17/12/2021", "07/06/2018", NA, "02/03/2022", "01/03/2021", "01/12/2023", NA, 
                  "31/10/2020", "01/03/2021", "01/03/2022", "01/03/2023", "01/01/2022", "01/01/2023", "06/11/2017", "17/04/2020", "17/04/2020", "17/04/2020"),
  Duree = c("308", "365", "185", "365", "183", "730", "30", "164", "365", "365", "135", "365", "180", "212", "365", "365", "365", "365", "364", "364", "364", "364", "364", "123", "160", "160", "160"),
  Note_Contenu = c("12", "6", "8", "14", "17", "5", "10", "11", "9", "18", "16", "14", "12", "3", "7", "9", NA, NA, "12", "12", "12", "10", "10", "9", "13", "13", "13"),
  Note_Formateur = c("6", NA, "10", "15", "18", "5", "10", "7", "20", "11", "16", "10", "5", "10", "13", NA, "10", NA, "11", "11", "11", "10", "10", "7", "10", "10", "10"),
  Note_Moyens = c("17", "12", "11", "15", "20", "8", "10", "6", "3", "20", "15", "6", "7", "11", "8", NA, NA, "11", "10", "10", "10", "10", "10", "8", "12", "12", "12"),
  Note_Accompagnement = c("4", "7", "1", "10", "15", "4", "16", "14", "4", "13", "12", "8", "11", "9", "19", NA, NA, NA, "12", "12", "12", "10", "10", "13", "18", "18", "18"),
  Note_Materiel = c("19", "14", "9", "8", "12", "9", "8", "13", "17", "15", "9", "12", "12", "8", "2", NA, NA, NA, "13", "13", "13", "10", "10", "16", "10", "10", "10"),
  poids_sondage = c("117.1", "98.3", "214.6", "84.7", "65.9", "148.2", "89.6", "100.3", "49.3", "148.2", "86.4", "99.2", "105.6", "123.1", "137.4", "187.6", "87.3", "87.3",
                    "169.3", "169.3", "169.3", "169.3", "234.1", "189.3", "45.9", "45.9", "45.9"),
  CSPF = c("Cadre", "Cadre", "Cadre", "Cadre", "Cadre","Cadre", "Employé", "Employé", "Employé", "Profession intermédiaire", "Employé", "Retraité", "Retraité", "Employé",
           "Employé", "Employé", "Ouvrier", "Ouvrier", "Ouvrier", "Ouvrier", "Ouvrier", "Profession intermédiaire", "Profession intermédiaire", "Cadre", "Ouvrier", "Ouvrier",
           "Ouvrier"),
  Sexef = c("Femme", "Femme", "Femme", "Femme", "Homme", "Homme", "Femme", "Femme", "Femme", "Homme", "Femme", "Homme", "Femme", "Homme", "Homme", "Homme", "Femme", "Femme",
            "Homme", "Homme", "Homme", "Femme", "Femme", "Homme", "Homme", "Homme", "Homme")
)

# Mise en forme des données

# R est sensible à la casse, il est pertinent d'harmoniser les noms des variables en minuscule
donnees_tidyverse <- donnees_tidyverse %>% rename_with(tolower)

# On a importé toutes les variables en format caractère
# On convertit certaines variables en format numérique
enNumerique <- c("duree", "note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
# On convertit certaines variables au format date
# On récupère d'abord les variables dont le nom débute par le mot "date"
enDate <- names(donnees_tidyverse)[grepl("^date", tolower(names(donnees_tidyverse)))]

# Conversion proprement dite
donnees_tidyverse <- donnees_tidyverse %>%  
  mutate(across(all_of(enNumerique), as.integer)) %>% 
  mutate(poids_sondage = as.numeric(poids_sondage)) %>% 
  mutate(across(all_of(enDate), lubridate::dmy))

# Date de sortie du dispositif
donnees_tidyverse <- donnees_tidyverse %>% 
  mutate(date_sortie = date_entree + lubridate::days(duree)) %>% 
  # Âge à l'entrée dans le dispositif
  mutate(age = as.period(interval(start = date_naissance, end = date_entree))$year)
# Données fictives sur des formations
library(data.table)
library(lubridate)
donnees_datatable <- data.table(
  Identifiant = c("173", "173", "173", "173", "174", "175", "198", "198", "198", "168", "211", "278", "347", "112", "112", "112", "087", "087", "099", "099", "099", "187", "187", "689", "765", "765", "765"),
  Sexe = c("2", "2", "2", "2", "1", "1", "2", "2", "2", "1", "2", "1", "2", "1", "1", "1", "2", "2", "1", "1", "1", "2", "2", "1", "1", "1", "1"),
  CSP = c("1", "1", "1", "1", "1", "1", "3", "3", "3", "2", "3", "5", "5", "3", "3", "3", "4", "4", "4", "4", "4", "2", "2", "1", "4", "4", "4"),
  Niveau = c("Qualifié", "Qualifié", "Qualifié", "Non qualifié", "Qualifié", "Qualifié", "Non qualifié", "Qualifié", "Qualifié", "Qualifié", "Non qualifié", "Qualifié", "Qualifié", "Non qualifié", 
             "Non qualifié", "Qualifié", "Non qualifié", "Non qualifié", "Qualifié", "Qualifié", "Qualifié", "Qualifié", "Qualifié", NA, "Non qualifié", "Non qualifié", "Non qualifié"),
  Date_naissance = c("17/06/1998", "17/06/1998", "17/06/1998", "17/06/1998", "08/12/1984", "16/09/1989", "17/03/1987", "17/03/1987", "17/03/1987", "30/07/2002", NA, "10/08/1948", 
                     "13/09/1955", "13/09/2001", "13/09/2001", "13/09/2001", NA, NA, "06/06/1998", "06/06/1998", "06/06/1998", "05/12/1986", "05/12/1986", "01/12/2000", "26/12/1995", "26/12/1995", "26/12/1995"),
  Date_entree = c("01/01/2021", "01/01/2022", "06/01/2022", "02/01/2023", "17/08/2021", "21/12/2022", "28/07/2022", "17/11/2022", "21/02/2023", "04/09/2019", "17/12/2021", "07/06/2018", NA, "02/03/2022", "01/03/2021", "01/12/2023", NA, 
                  "31/10/2020", "01/03/2021", "01/03/2022", "01/03/2023", "01/01/2022", "01/01/2023", "06/11/2017", "17/04/2020", "17/04/2020", "17/04/2020"),
  Duree = c("308", "365", "185", "365", "183", "730", "30", "164", "365", "365", "135", "365", "180", "212", "365", "365", "365", "365", "364", "364", "364", "364", "364", "123", "160", "160", "160"),
  Note_Contenu = c("12", "6", "8", "14", "17", "5", "10", "11", "9", "18", "16", "14", "12", "3", "7", "9", NA, NA, "12", "12", "12", "10", "10", "9", "13", "13", "13"),
  Note_Formateur = c("6", NA, "10", "15", "18", "5", "10", "7", "20", "11", "16", "10", "5", "10", "13", NA, "10", NA, "11", "11", "11", "10", "10", "7", "10", "10", "10"),
  Note_Moyens = c("17", "12", "11", "15", "20", "8", "10", "6", "3", "20", "15", "6", "7", "11", "8", NA, NA, "11", "10", "10", "10", "10", "10", "8", "12", "12", "12"),
  Note_Accompagnement = c("4", "7", "1", "10", "15", "4", "16", "14", "4", "13", "12", "8", "11", "9", "19", NA, NA, NA, "12", "12", "12", "10", "10", "13", "18", "18", "18"),
  Note_Materiel = c("19", "14", "9", "8", "12", "9", "8", "13", "17", "15", "9", "12", "12", "8", "2", NA, NA, NA, "13", "13", "13", "10", "10", "16", "10", "10", "10"),
  poids_sondage = c("117.1", "98.3", "214.6", "84.7", "65.9", "148.2", "89.6", "100.3", "49.3", "148.2", "86.4", "99.2", "105.6", "123.1", "137.4", "187.6", "87.3", "87.3",
                    "169.3", "169.3", "169.3", "169.3", "234.1", "189.3", "45.9", "45.9", "45.9"),
  CSPF = c("Cadre", "Cadre", "Cadre", "Cadre", "Cadre","Cadre", "Employé", "Employé", "Employé", "Profession intermédiaire", "Employé", "Retraité", "Retraité", "Employé",
           "Employé", "Employé", "Ouvrier", "Ouvrier", "Ouvrier", "Ouvrier", "Ouvrier", "Profession intermédiaire", "Profession intermédiaire", "Cadre", "Ouvrier", "Ouvrier",
           "Ouvrier"),
  Sexef = c("Femme", "Femme", "Femme", "Femme", "Homme", "Homme", "Femme", "Femme", "Femme", "Homme", "Femme", "Homme", "Femme", "Homme", "Homme", "Homme", "Femme", "Femme",
            "Homme", "Homme", "Homme", "Femme", "Femme", "Homme", "Homme", "Homme", "Homme")
)

# Mise en forme des données

# R est sensible à la casse, il est pertinent d'harmoniser les noms des variables en minuscule
colnames(donnees_datatable) <- tolower(colnames(donnees_datatable))

# On a importé toutes les variables en format caractère

# On convertit certaines variables en format numérique
enNumerique <- c("duree", "note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
donnees_datatable[, (enNumerique) := lapply(.SD, as.integer), .SDcols = enNumerique]
# Autre solution
# En data.table, les instructions débutant par set modifient les éléments par référence, c'est-à-dire sans copie.
# Ceci est plus efficace pour manipuler des données volumineuses.
for (j in enNumerique) {
  set(donnees_datatable, j = j, value = as.numeric(donnees_datatable[[j]]))
}
donnees_datatable[, poids_sondage := as.numeric(poids_sondage)]

# On récupère les variables dont le nom débute par le mot "date"
varDates <- names(donnees_datatable)[grepl("date", tolower(names(donnees_datatable)))]
# On exprime les dates en format Date
donnees_datatable[, (varDates) := lapply(.SD, lubridate::dmy), .SDcols = varDates]

# Date de sortie du dispositif
donnees_datatable[, date_sortie := date_entree + lubridate::days(duree)]

# Âge à l'entrée dans le dispositif
donnees_datatable[, age := floor(lubridate::time_length(difftime(donnees_datatable$date_entree, donnees_datatable$date_naissance), "years"))]

Duckdb est un serveur SQL séparé de la session R. Les calculs sont effectués en dehors de R et l’espace mémoire est distinct de celui de R. Au lieu d’accéder directement aux données, il faut passer par un objet connection qui contient l’adresse du serveur, un peu comme lorsque l’on se connecte à un serveur web. Ici en particulier, il est nécessaire de transférer les données vers duckdb.

# Ouvrir une connexion au serveur duckdb
con <- DBI::dbConnect(duckdb::duckdb()); 

# On "copie" les données dans une table du nom table_duckdb
# Données fictives sur des formations
con %>% duckdb::duckdb_register(name = "table_duckdb", df = donnees_tidyverse)

con %>% tbl("table_duckdb")

# Fermer la connexion au serveur duckdb
DBI::dbDisconnect(con, shutdown = TRUE)

Pour la suite, on suppose que la connexion est ouverte sous le nom con, et que les données sont accessibles par la requête requete_duckdb. Le code modifiera la requête, mais pas la table dans le serveur SQL.

con <- DBI::dbConnect(duckdb::duckdb()); 
con %>% duckdb::duckdb_register(name = "table_duckdb", df = donnees_tidyverse)
requete_duckdb <- con %>% tbl("table_duckdb")

N.B. Duckdb est envisagé pour des traitements sans charger des données en mémoire, par exemple en lisant directement un fichier .parquet sur le disque dur. Dans ce cas, les opérations sont effectuées à la volée, mais n’affectent pas les fichiers source.

donnees_python = pd.DataFrame({
    "Identifiant": ["173", "173", "173", "173", "174", "175", "198", "198", "198", "168", "211", "278", "347", "112", "112", "112", "087", "087", "099", "099", "099", "187", "187", "689", "765", "765", "765"],
    "Sexe": ["2", "2", "2", "2", "1", "1", "2", "2", "2", "1", "2", "1", "2", "1", "1", "1", "1", "1", "1", "1", "1", "2", "2", "1", "1", "1", "1"],
    "CSP": ["1", "1", "1", "1", "1", "1", "4", "4", "4", "2", "3", "5", "5", "3", "3", "3", "3", "3", "3", "3", "3", "2", "2", "1", "4", "4", "4"],
    "Niveau": ["Qualifié", "Qualifié", "Qualifié", "Non qualifié", "Qualifié", "Qualifié", "Non qualifié", "Qualifié", "Qualifié", "Qualifié", "Non qualifié", "Qualifié", "Qualifié", "Non qualifié", 
             "Non qualifié", "Qualifié", "Non qualifié", "Non qualifié", "Qualifié", "Qualifié", "Qualifié", "Qualifié", "Qualifié", None, "Non qualifié", "Non qualifié", "Non qualifié"],
    "Date_naissance": ["17/06/1994", "17/06/1995", "17/06/1998", "17/06/1998", "08/12/1984", "16/09/1989", "17/03/1987", "17/03/1987", "17/03/1987", "30/07/2002", None, "10/08/1948", "13/09/1955", "13/09/2001", "13/09/2001", "13/09/2001", None, None, "06/06/1998", "06/06/1998", "06/06/1998", "05/12/1986", "05/12/1986", "01/12/2000", "26/12/1995", "26/12/1995", "26/12/1995"],
    "Date_entree": ["01/01/2021", "01/01/2021", "06/01/2022", "02/01/2023", "17/08/2021", "21/12/2022", "28/07/2022", "17/11/2022", "21/02/2023", "04/09/2019", "17/12/2021", "07/06/2018", None, "02/03/2022", "01/03/2021", "01/12/2023", None, "31/10/2020", "01/03/2021", "01/03/2022", "01/03/2023", "01/01/2022", "01/01/2023", "06/11/2017", "17/04/2020", "17/04/2020", "17/04/2020"],
    "Duree": ["308", "365", "185", "365", "183", "730", "30", "164", "365", "365", "135", "365", "180", "212", "365", "365", "365", "365", "364", "364", "364", "364", "364", "123", "160", "160", "160"],
    "Note_Contenu": ["12", "6", "8", "14", "17", "5", "10", "11", "9", "18", "16", "14", "12", "3", "7", None, None, None, "12", "12", "12", "10", "10", "9", "13", "13", "13"],
    "Note_Formateur": ["6", None, "10", "15", "18", "5", "10", "7", "20", "11", "16", "10", "5", "10", "13", None, None, None, "11", "11", "11", "10", "10", "7", "10", "10", "10"],
    "Note_Moyens": ["17", "12", "11", "15", "20", "8", "10", "6", "3", "20", "15", "6", "7", "11", "8", None, None, None, "10", "10", "10", "10", "10", "8", "12", "12", "12"],
    "Note_Accompagnement": ["4", "7", "1", "10", "15", "4", "16", "14", "4", "13", "12", "8", "11", "9", "19", None, None, None, "12", "12", "12", "10", "10", "13", "18", "18", "18"],
    "Note_Materiel": ["19", "14", "9", "8", "12", "9", "8", "13", "17", "15", "9", "12", "12", "8", "2", None, None, None, "13", "13", "13", "10", "10", "16", "10", "10", "10"],
    "poids_sondage": ["117.1", "98.3", "214.6", "84.7", "65.9", "148.2", "89.6", "100.3", "49.3", "148.2", "86.4", "99.2", "105.6", "123.1", "137.4", "187.6", "87.3", "87.3", "169.3", "169.3", "169.3", "169.3", "234.1", "189.3", "45.9", "45.9", "45.9"],
    "CSPF": ["Cadre", "Cadre", "Cadre", "Cadre", "Cadre","Cadre", "Employé", "Employé", "Employé", "Profession intermédiaire", "Employé", "Retraité", "Retraité", "Employé", "Employé", "Employé", "Ouvrier", "Ouvrier", "Ouvrier", "Ouvrier", "Ouvrier", "Profession intermédiaire", "Profession intermédiaire", "Cadre", "Ouvrier", "Ouvrier", "Ouvrier"],
    "Sexef": ["Femme", "Femme", "Femme", "Femme", "Homme", "Homme", "Femme", "Femme", "Femme", "Homme", "Femme", "Homme", "Femme", "Homme", "Homme", "Homme", "Femme", "Femme", "Homme", "Homme", "Homme", "Femme", "Femme", "Homme", "Homme", "Homme", "Homme"]
})

# Mise en forme des données

# Python est sensible à la casse, il est pertinent d'harmoniser les noms des variables en minuscule
donnees_python.columns = donnees_python.columns.str.lower()

# On convertit certaines variables en format numérique
enNumerique = ["duree", "note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel"]
donnees_python[enNumerique] = donnees_python[enNumerique].astype(float)
donnees_python['poids_sondage'] = donnees_python['poids_sondage'].astype(float)

# Colonnes à convertir en date
enDate = ['date_naissance', 'date_entree']
donnees_python[enDate] = donnees_python[enDate].apply(pd.to_datetime, format='%d/%m/%Y', errors='coerce')

# Date de sortie du dispositif
donnees_python['date_sortie'] = donnees_python['date_entree'] + pd.to_timedelta(donnees_python['duree'], unit='D')

# Âge à l'entrée dans le dispositif
donnees_python['age'] = np.floor((donnees_python['date_entree'] - donnees_python['date_naissance']).dt.days / 365.25).astype('Int64')

2.3 Manipulation du format de la base de données

Sans objet pour SAS.

# On vérifie que la base importée est bien un data.frame
is.data.frame(donnees_rbase)

# Format de la base
class(donnees_rbase)
# On vérifie que la base importée est bien un tibble
is_tibble(donnees_tidyverse)

# Transformation en tibble, le format de Tidyverse
donnees_tidyverse <- as_tibble(donnees_tidyverse)

# Format de la base
class(donnees_tidyverse)
# On vérifie que la base est bien un data.table
is.data.table(donnees_datatable)

# Transformation en data.frame
setDF(donnees_datatable)
is.data.frame(donnees_datatable)

# Transformation en data.table
# En data.table, les instructions débutant par set modifient les éléments par référence, c'est-à-dire sans copie.
# Ceci est plus efficace pour manipuler des données volumineuses.
setDT(donnees_datatable)
is.data.table(donnees_datatable)
# Autre possibilité
donnees_datatable <- as.data.table(donnees_datatable)

# La data.table est une liste
is.list(donnees_datatable)

# Format de la base
class(donnees_datatable)
type(donnees_python)

2.4 Importation de données extérieures

2.4.1 Documentation Utilit’R

Sans objet pour SAS.

Importer des données extérieures dans R est sans doute la première tâche à laquelle est confronté l’utilisateur de ce logiciel. Ce point important est décrit sur le site Utilit’R : https://www.book.utilitr.org/03_fiches_thematiques/fiche_import_fichiers_plats.

Pour importer des fichiers :

Quelques éléments additionnels non couverts dans Utilit’R sont présentés ici.

Importer des données extérieures dans R est sans doute la première tâche à laquelle est confronté l’utilisateur de ce logiciel. Ce point important est décrit sur le site Utilit’R : https://www.book.utilitr.org/03_fiches_thematiques/fiche_import_fichiers_plats.

Pour importer des fichiers :

Quelques éléments additionnels non couverts dans Utilit’R sont présentés ici.

Importer des données extérieures dans R est sans doute la première tâche à laquelle est confronté l’utilisateur de ce logiciel. Ce point important est décrit sur le site Utilit’R : https://www.book.utilitr.org/03_fiches_thematiques/fiche_import_fichiers_plats.

Pour importer des fichiers :

Quelques éléments additionnels non couverts dans Utilit’R sont présentés ici.

Sans objet pour pandas.

2.4.2 Importer les premières lignes de la base

Sans objet pour SAS.

La fonction readLines() de R peut s’avérer utile lors de l’importation de fichiers très volumineux. Elle permet de n’importer que les premières lignes du fichier, sans importer l’ensemble de la base, et ainsi de visualiser rapidement le contenu des données et la nature de l’éventuel séparateur de colonnes.

Les options de la fonction utiles sont :

  • con : chemin du fichier à importer
  • n : nombre maximal de lignes du fichier lues
  • encoding : définir l’encodage du fichier (“UTF-8” ou “latin1”)

La fonction readLines() de R peut s’avérer utile lors de l’importation de fichiers très volumineux. Elle permet de n’importer que les premières lignes du fichier, sans importer l’ensemble de la base, et ainsi de visualiser rapidement le contenu des données et la nature de l’éventuel séparateur de colonnes.

Les options de la fonction utiles sont :

  • con : chemin du fichier à importer
  • n : nombre maximal de lignes du fichier lues
  • encoding : définir l’encodage du fichier (“UTF-8” ou “latin1”)

La fonction readLines() de R peut s’avérer utile lors de l’importation de fichiers très volumineux. Elle permet de n’importer que les premières lignes du fichier, sans importer l’ensemble de la base, et ainsi de visualiser rapidement le contenu des données et la nature de l’éventuel séparateur de colonnes.

Les options de la fonction utiles sont :

  • con : chemin du fichier à importer
  • n : nombre maximal de lignes du fichier lues
  • encoding : définir l’encodage du fichier (“UTF-8” ou “latin1”)

2.4.3 Importer toute la base

/* Importer un fichier xls */
/* proc import out = NomBaseImportee 
   datafile = "CHEMIN DE LA BASE"
   DBMS = XLS REPLACE;
  run; */
  
/* Importer un fichier avec séparateur | */
/* data NomDeLaBase;
     infile "CHEMIN DE LA BASE IMPORTEE" dlm = "|" missover dsd firstobs = 2;
     informat VARIABLES;
     format VARIABLES;
     input VARIABLES;
   run; */

On utilisera les fonctions read.table, read.csv et read.csv2.

On utilisera la fonction fread : https://book.utilitr.org/03_Fiches_thematiques/Fiche_import_fichiers_plats.html#importer-un-fichier-avec-le-package-data.table.

Une option utile non présentée dans le lien est : keepLeadingZeros. Si cette option est valorisée à TRUE, les valeurs numériques précédées par des 0 seront importées sous forme caractère et le zéro initial sera conservé.

On utilisera les fonctions pd.read_csv() pour lire les fichiers CSV.

#Pour les fichiers SAS :
#from sas7bdat import SAS7BDAT
#import retrying

 #file_path = 'chemin/nom_fichier.sas7bdat'
 #with SAS7BDAT(file_path) as reader:
 #   data = reader.to_data_frame()

3 Préambule

3.1 Chemin du bureau de l’utilisateur

/* On vide la log */
dm "log; clear; ";
/* On récupère déjà l'identifiant de l'utilisateur (systèmes Windows) */
%let user = &sysuserid;

/* Chemin proprement dit */
%let bureau = C:\Users\&user.\Desktop;
libname bur "&bureau.";
# On récupère déjà l'identifiant de l'utilisateur
user <- Sys.getenv("USERNAME")

# Chemin proprement dit
chemin <- file.path("C:/Users", user, "Desktop")
# On récupère déjà l'identifiant de l'utilisateur
user <- Sys.getenv("USERNAME")

# Chemin proprement dit
chemin <- file.path("C:/Users", user, "Desktop")
# On récupère déjà l'identifiant de l'utilisateur
user <- Sys.getenv("USERNAME")

# Chemin proprement dit
chemin <- file.path("C:/Users", user, "Desktop")
# On récupère déjà l'identifiant de l'utilisateur
user = os.getenv('USERNAME')

# Chemin en texte
chemin = "C:/Users/" + str(user) + "/Desktop"

3.2 Affichage de l’année

/* Année courante */
%let annee = %sysfunc(year(%sysfunc(today())));
/* & (esperluette) indique à SAS qu'il doit remplacer an par sa valeur définie par le %let */
%put Année : &annee.;

/* Autre possibilité */
data _null_;call symput('annee', strip(year(today())));run;
%put Année (autre méthode) : &annee.;

/* Année passée */
%put Année passée : %eval(&annee. - 1);
# Année courante
annee <- lubridate::year(Sys.Date())
sprintf("Année : %04d", annee)
print(paste0("Année : ", annee))

# Autre possibilité
print(paste0("Année : ", format(Sys.Date(), "%Y")))

# Année passée
annee_1 <- annee - 1
paste0("Année passée : ", annee_1)
# Année courante
annee <- lubridate::year(Sys.Date())
sprintf("Année : %04d", annee)
print(paste0("Année : ", annee))

# Autre possibilité
print(paste0("Année : ", format(Sys.Date(), "%Y")))

# Année passée
annee_1 <- annee - 1
paste0("Année passée : ", annee_1)
# Année courante
annee <- lubridate::year(Sys.Date())
sprintf("Année : %04d", annee)
print(paste0("Année : ", annee))

# Autre possibilité
print(paste0("Année : ", format(Sys.Date(), "%Y")))

# Année passée
annee_1 <- annee - 1
paste0("Année passée : ", annee_1)
# Année courante
annee = datetime.now().year
# Afficher l'année actuelle
print("Année :", annee)

# Année passée
annee_1 = annee - 1
print("Année passée :", annee_1)

3.3 Construction des instructions if / else

%macro Annee(an);
  %if &an. >= 2024 %then %put Nous sommes en 2024 ou après !;
  %else %put Nous sommes avant 2024 !;
%mend Annee;
%Annee(&annee.);
# Construction incorrecte ! Le else doit être sur la même ligne que le {
#if (annee >= 2024) {
#  print("Nous sommes en 2024 ou après !")
#}
#else {
#  print("Nous sommes avant 2024 !")
#}

# Construction correcte ! Le else doit être sur la même ligne que le {
if (annee >= 2024) {
  print("Nous sommes en 2024 ou après !")
} else {
  print("Nous sommes avant 2024 !")
}
# Construction incorrecte ! Le else doit être sur la même ligne que le {
#if (annee >= 2024) {
#  print("Nous sommes en 2024 ou après !")
#}
#else {
#  print("Nous sommes avant 2024 !")
#}

# Construction correcte ! Le else doit être sur la même ligne que le {
if (annee >= 2024) {
  print("Nous sommes en 2024 ou après !")
} else {
  print("Nous sommes avant 2024 !")
}
# Construction incorrecte ! Le else doit être sur la même ligne que le {
#if (annee >= 2024) {
#  print("Nous sommes en 2024 ou après !")
#}
#else {
#  print("Nous sommes avant 2024 !")
#}

# Construction correcte ! Le else doit être sur la même ligne que le {
if (annee >= 2024) {
  print("Nous sommes en 2024 ou après !")
} else {
  print("Nous sommes avant 2024 !")
}
if annee >= 2024:
    print("Nous sommes en 2024 ou après !")
else:
    print("Nous sommes avant 2024 !")

3.4 Répertoire de travail

/* Afficher le répertoire de travail par défaut (la Work) */
%let chemin_work = %sysfunc(pathname(work));
%put &chemin_work.;

/* Autre solution */
proc sql;
  select path from dictionary.libnames where libname = "WORK";
quit;

/* Définir le répertoire de travail, si besoin */
/* libname "nom du répertoire"; */
# Afficher le répertoire de travail
getwd()

# Définir le répertoire de travail, si besoin
#setwd(dir = "nom du répertoire")
# Afficher le répertoire de travail
getwd()

# Définir le répertoire de travail, si besoin
#setwd(dir = "nom du répertoire")
# Afficher le répertoire de travail
getwd()

# Définir le répertoire de travail, si besoin
#setwd(dir = "nom du répertoire")
# Afficher le répertoire de travail
os.getcwd()

3.5 Autres points à connaître

Mise en garde : certains codes SAS pourraient aussi avec profit être écrits en langage SAS IML (Interactive Matrix Language). Cet aide-mémoire n’ayant pas vocation à être un dictionnaire SAS, cette méthode d’écriture n’est pas proposée ici.

R base est réputé plus lent que ses concurrents, ce qui est souvent vrai. Mais certaines fonctions en R base peuvent être très rapides (rowsum, rowSums, colSums, rowMeans, colMeans, tapply, etc.)

# Le pipe permet d'enchaîner des opérations sur une même base.
# Il n'est pas réservé au tidyverse, et peut être utilisé avec R-Base et data.table.
1:10 |> sum()

tidyverse promeut l’utilisation du pipe (%>%), qui permet d’enchaîner des opérations sur une même base modifiée successivement. 2 types de pipes existent, le pipe de magrittr (%>%) et le pipe de R-Base (|>, à partir de la version 4.1) Les fonctionnalités simples des deux opérateurs sont identiques, mais il existe des différences. Dans cet aide-mémoire, le pipe de magrittr (%>%) est privilégié.

Le tidyverse peut s’utiliser sans pipe, mais le pipe simplifie la gestion des programmes. Les autres environnements (R base, data.table) peuvent aussi se présenter avec le pipe.

# Principe de base de data.table
# dt[i, j, by = ]
#   dt : nom de la base en format data.table (instruction FROM de SQL)
#   i : sélection de lignes (instructions WHERE et ORDER de SQL)
#   j : sélection et manipulation de colonnes (instruction SELECT de SQL)
#   by = : groupements (instruction GROUP BY de SQL)

# L'instruction HAVING de SQL peut être obtenue par une seconde instruction de sélection, par exemple :
# dt[i, j, by = ][SOMME > VALEUR]

4 Informations sur la base de données

4.1 Avoir une vue d’ensemble des données

/* Statistiques globales sur les variables numériques */
proc means data = donnees_sas n mean median min p10 p25 median p75 p90 max;var _numeric_;run;

/* Statistiques globales sur les variables caractères */
proc freq data = donnees_sas;tables _character_ / missing;run;
# Informations sur les variables
str(donnees_rbase)

# Statistiques descriptives des variables de la base
summary(donnees_rbase)
library(Hmisc)
Hmisc::describe(donnees_rbase)

# Visualiser la base de données
View(donnees_rbase)
# Informations sur les variables
donnees_tidyverse %>% str()
donnees_tidyverse %>% glimpse()

# Statistiques descriptives des variables de la base
donnees_tidyverse %>% summary()

# Visualiser la base de données
donnees_tidyverse %>% View()
# Informations sur les variables
str(donnees_datatable)

# Statistiques descriptives des variables de la base
summary(donnees_datatable)

# Visualiser la base de données
View(donnees_datatable)

On accède aux données du serveur SQL DuckDB au travers de l’objet requete_duckdb, qui est une requête (avec l’adresse du serveur) et non pas un dataframe ou un tibble. Comme l’accès n’est pas direct, la plupart des fonctions du tidyverse fonctionnent, mais opèrent sur “l’adresse du serveur DuckDB” au lieu d’opérer sur les valeurs (nombres, chaînes de caractères). A part glimpse, la plupart des fonctions ne renvoient pas un résultat exploitable.

# Informations sur les variables
# requete_duckdb %>% str() 
requete_duckdb %>% glimpse() # préférer glimpse()
# requete_duckdb %>% summary()
# requete_duckdb %>% View() 
# Informations sur les variables
# donnees_python.info()

# Statistiques descriptives des variables de la base
# donnees_python.describe()

4.2 Afficher le type des variables

proc contents data = donnees_sas;run;
sapply(donnees_rbase, class)
purrr::map(donnees_tidyverse, class)
class(donnees_tidyverse)
donnees_datatable[, lapply(.SD, class)]

On ne peut pas appliquer directement la fonction class sur un objet de type connection. Cependant, DuckDB affiche le type des variables dans un print. On peut également appliquer la fonction class sur un extrait des données (après collect).

purrr::map(requete_duckdb %>% select(c(1,2)) %>% head() %>% collect(), class)
class(requete_duckdb)
### Afficher le type des variables :
donnees_python.dtypes

4.3 Extraire les x premières lignes de la base (10 par défaut)

%let x = 10;
proc print data = donnees_sas (firstobs = 1 obs = &x.);run;
/* Ou alors */
data Lignes&x.;set donnees_sas (firstobs = 1 obs = &x.);proc print;run;
x <- 10
donnees_rbase[1:x, ]
head(donnees_rbase, x)
x <- 10
donnees_tidyverse %>% 
  slice(1:x)
x <- 10
donnees_datatable[, first(.SD, x)]
donnees_datatable[, .SD[1:x]]
first(donnees_datatable, x)
head(donnees_datatable, x)

DuckDB affiche les dix premières lignes par défaut lorsque l’on évalue une requête, comme indiqué dans le code ci-dessous.

requete_duckdb
# Ceci est équivalent au code suivant
# requete_duckdb %>% print(n=10)

Attention, comme il n’y a pas d’ordre en SQL, il faut ordonner les lignes si on veut un résultat reproductible. C’est une opération qui peut être couteuse en temps CPU.

requete_duckdb %>% arrange(duree) %>% print()

L’objet requete_duckdb est bien une requête (i.e. une liste à deux éléments) même si on peut en afficher le résultat avec la fonction print. Notamment, les informations restent dans la mémoire de DuckDB. Il faut demander explicitement le transfert du résultat vers la session R avec la fonction collect(). On obtient alors un objet de type data.frame ou au lieu de tbl_duckdb_connection.

class(requete_duckdb)
resultat_tibble <- requete_duckdb %>% collect()
class(resultat_tibble)

La fonction collect() transfère l’ensemble des données. Pour obtenir uniquement 10 lignes, il faut utiliser l’une des fonctions slice_* (cf documentation). On conseille slice_min ou slice_max qui indiquent explicitement l’ordre utilisé.

requete_duckdb %>% slice_max(duree, n=4, with_ties=FALSE) # with_ties = TRUE retourne les cas d'égalité, donc plus de 4 lignes

En DuckDB et/ou sur un serveur SQL, on déconseille les fonctions head (qui ne respecte pas toujours l’ordre indiqué par arrange) ou top_n (superseded). La fonction slice en fonctionne pas : elle ne peut pas respecter l’ordre.

x = 10
donnees_python.head(x)
# Autre méthode : la spécificité de Python est que l'on commence à compter à partir de 0
# La première ligne se situe en position 0
donnees_python.iloc[0:x, :]

4.4 Extraire les x dernières lignes de la base (10 par défaut)

%let x = 10;
proc sql noprint;select count(*) into :total_lignes from donnees_sas;quit;
%let deb = %eval(&total_lignes. - &x. + 1);
data Lignes_&x.;set donnees_sas (firstobs = &deb. obs = &total_lignes.);run;
x <- 10
tail(donnees_rbase, x)

# Autre possibilité
donnees_rbase[ ( nrow(donnees_rbase) - x ) : nrow(donnees_rbase), ]

# Les parenthèses sont importantes. Comparer les deux expressions ! Bon exemple du recycling
( nrow(donnees_rbase) - x ) : nrow(donnees_rbase)
nrow(donnees_rbase) - x : nrow(donnees_rbase)
x <- 10
donnees_tidyverse %>% 
  slice( (n() - x) : n())
x <- 10
donnees_datatable[, last(.SD, x)]
donnees_datatable[, tail(.SD, x)]
last(donnees_datatable, x)
tail(donnees_datatable, x)

Mêmes remarques que pour les premières lignes : il n’y a pas d’ordre a priori en SQL. On conseille slice_min ou slice_max qui indiquent explicitement l’ordre utilisé, et l’on déconseille slice et tail.

requete_duckdb %>% slice_min(duree, n=5, with_ties=FALSE) # with_ties = TRUE retourne les cas d'égalité, donc plus de 5 lignes
x = 10
donnees_python.tail(x)

4.5 Nombre de lignes et de colonnes dans la base

/* Nombre de lignes */
proc sql;select count(*) as Nb_Lignes from donnees_sas;quit;
proc sql;
  select count(*) as Nb_Lignes, count(distinct identifiant) as Nb_Identifiants
  from donnees_sas;
quit;

/* Nombre de colonnes */
proc sql;select count(*) as Nb_Colonnes from Var;run;
# Les syntaxes dim(donnees_rbase)[1] et dim(donnees_rbase)[2] sont plus rapides que nrow() et ncol()
sprintf("Nombre de lignes : %d | Nombre de colonnes : %d", dim(donnees_rbase)[1], dim(donnees_rbase)[2])
sprintf("Nombre de lignes : %d | Nombre de colonnes : %d", nrow(donnees_rbase), ncol(donnees_rbase))
sprintf("Nombre de lignes : %d | Nombre de colonnes : %d",
        donnees_tidyverse %>% nrow(),
        donnees_tidyverse %>% ncol())

# Nombre de lignes
donnees_tidyverse %>% nrow()
# Nombre de colonnes
donnees_tidyverse %>% ncol()
dim(donnees_datatable) ; dim(donnees_datatable)[1] ; dim(donnees_datatable)[2]
dim(donnees_datatable) ; nrow(donnees_datatable) ; ncol(donnees_datatable)
sprintf("Nombre de lignes : %d | Nombre de colonnes : %d", dim(donnees_datatable)[1], dim(donnees_datatable)[2])

# Autre solution rapide pour le nombre de lignes
donnees_datatable[, .N]

Duckdb/SQL ne connaît pas le nombre de lignes sans un calcul. Il faut faire count().

#Nombre de lignes
requete_duckdb %>% nrow() # retourne NA
requete_duckdb %>% count() # correct

#Nombre de colonnes
requete_duckdb %>%  ncol()
donnees_python.shape
print('Nombre de lignes : ' + str(donnees_python.shape[0]))
print('Nombre de colonnes : ' + str(donnees_python.shape[1]))

4.6 Les variables de la base

/* Par ordre d'apparition dans la base */
proc contents data = donnees_sas out = Var noprint;run;
proc sql;select name into :nom_col separated by " " from Var order by varnum;run;

/* On affiche les noms des variables */
%put Liste des variables : &nom_col.;

/* Par ordre alphabétique */
proc contents data = donnees_sas out = Var noprint;run;
proc sql;select name into :nom_col separated by " " from Var;run;

/* On affiche les noms des variables */
%put Liste des variables : &nom_col.;

/* On supprime la base Var temporaire */
proc datasets lib = Work nolist;delete Var;run;
# Les variables par ordre d'apparition dans la base
names(donnees_rbase)
colnames(donnees_rbase)

# Les variables par ordre alphabétique
ls(donnees_rbase)
sort(colnames(donnees_rbase))
# Les variables par ordre d'apparition dans la base
donnees_tidyverse %>% names()
donnees_tidyverse %>% colnames()

# Les variables par ordre alphabétique
donnees_tidyverse %>% colnames() %>% sort()
# Les variables par ordre d'apparition dans la base
names(donnees_datatable)
colnames(donnees_datatable)

# Les variables par ordre alphabétique
sort(colnames(donnees_datatable))
requete_duckdb %>% colnames()
donnees_python.columns

4.7 Mettre les noms des variables en minuscule

R est sensible à la casse, il est pertinent d’harmoniser les noms des variables en minuscule.

Sans objet, SAS n’est pas sensible à la casse.

colnames(donnees_rbase) <- tolower(colnames(donnees_rbase))

# Autre possibilité
setNames(donnees_rbase, tolower(names(donnees_rbase)))
donnees_tidyverse <- donnees_tidyverse %>% rename_with(tolower)

# Autre solution
donnees_tidyverse <- donnees_tidyverse %>% 
  magrittr::set_colnames(value = casefold(colnames(.), upper = FALSE))
colnames(donnees_datatable) <- tolower(colnames(donnees_datatable))
donnees_python.columns = donnees_python.columns.str.lower()

4.8 Nombre d’identifiants uniques et de lignes dans la base

proc sql;
  select count(*) as Nb_Lignes, count(distinct identifiant) as Nb_Identifiants_Uniques
  from donnees_sas;
quit;
sprintf("La base de données contient %d lignes et %d identifiants uniques !",
        nrow(donnees_rbase),
        length(unique(donnees_rbase$identifiant)))
sprintf("La base de données contient %d lignes et %d identifiants uniques !",
        donnees_tidyverse %>% nrow(),
        donnees_tidyverse %>% select(identifiant) %>%
          n_distinct()
        )
# Autre solution pour le nombre d'identifiants uniques
donnees_tidyverse %>% select(identifiant) %>% n_distinct()
donnees_tidyverse %>% distinct(identifiant) %>% nrow()
sprintf("La base de données contient %d lignes et %d identifiants uniques !",
        nrow(donnees_datatable),
        donnees_datatable[, uniqueN(identifiant)])
requete_duckdb %>% nrow()
requete_duckdb %>% distinct(identifiant) %>% count()

Note : on a vu que nrow ne fonctionne pas en DuckDB.

(donnees_python['identifiant']).nunique()

4.9 Quelle est la position de la variable date_entree ?

%let var = date_entree;
proc contents data = donnees_sas out = Var noprint;run;
proc sql;
  select varnum as Position from Var where lowcase(NAME) = "&var.";
run;
variable <- "date_entree"
pos <- match(variable, names(donnees_rbase))
sprintf("La variable %s se trouve en colonne n°%s !", variable, pos)
variable <- "date_entree"
pos <- match(variable, donnees_tidyverse %>% colnames())
sprintf("La variable %s se trouve en colonne n°%s !", variable, pos)
variable <- "date_entree"
pos <- match(variable, names(donnees_datatable))
sprintf("La variable %s se trouve en colonne n°%s !", variable, pos)
variable <- "date_entree"
pos <- match(variable, requete_duckdb %>% colnames())
sprintf("La variable %s se trouve en colonne n°%s !", variable, pos)
# Attention, Python commence à compter à partir de 0
# Si date_entree est la première colonne, alors on affichera 0
variable = "date_entree"
pos = donnees_python.columns.get_loc(variable)
print(f"La variable {variable} se trouve en colonne n°{pos} !")

4.10 Variables qui débutent par le mot Note

proc contents data = donnees_sas out = Variables;run;
proc sql;select Name from Variables where upcase(substr(Name, 1, 4)) = "NOTE";run;
grep("^note", names(donnees_rbase), ignore.case = TRUE, value = TRUE)

# Autre possibilité
names(donnees_rbase)[grepl("^note", names(donnees_rbase), ignore.case = TRUE)]
names(donnees_tidyverse) %>% str_subset("^note")
grep("^note", names(donnees_datatable), ignore.case = TRUE, value = TRUE)

# Autre possibilité
names(donnees_datatable)[grepl("^note", names(donnees_datatable), ignore.case = TRUE)]
import re

# Obtenir les noms des colonnes qui commencent par "note" en ignorant la casse
columns_with_note = list(filter(lambda col: re.match(r'^note', col, re.IGNORECASE), donnees_python.columns))
columns_with_note

4.11 Variables qui se terminent par le mot Naissance

proc contents data = donnees_sas out = Variables;run;
proc sql;
  select Name from Variables
  where upcase(substr(NAME, length(Name) - %length(NAISSANCE) + 1, length(name))) = "NAISSANCE";
run;
grep("naissance$", names(donnees_rbase), ignore.case = TRUE, value = TRUE)

# Autre possibilité
names(donnees_rbase)[grepl("naissance$", names(donnees_rbase), ignore.case = TRUE)]
names(donnees_tidyverse) %>% str_subset("naissance$")
grep("naissance$", names(donnees_datatable), ignore.case = TRUE, value = TRUE)

# Autre possibilité
names(donnees_datatable)[grepl("naissance$", names(donnees_datatable), ignore.case = TRUE)]
columns_with_naissance = list(filter(lambda col: re.search(r'naissance$', col, re.IGNORECASE), donnees_python.columns))
columns_with_naissance

5 Sélection de colonnes

5.1 Sélectionner une colonne par sa position

%let pos = 1;
proc contents data = donnees_sas out = Var noprint;run;

proc sql noprint;
  select name into :nom_col separated by " "
  from Var
  where varnum = &pos.;
run;

data Colonnes;set donnees_sas (keep = &nom_col.);run;
proc datasets lib = Work nolist;delete Var;run;
pos <- 1
# Résultat sous forme de vecteur caractère
id <- donnees_rbase[[pos]] ; class(id)
id <- donnees_rbase[, pos] ; class(id)

# Résultat sous forme de data.frame
id <- donnees_rbase[pos] ; class(id)
# Attention, utilisation du drop = FALSE étrange
# En fait, l'affectation par [] a pour option par défaut drop = TRUE. Ce qui implique que si l'affectation renvoie un data.frame d'1 seule colonne, l'objet sera transformé en objet plus simple (vecteur en l'occurrence)
id <- donnees_rbase[, pos, drop = FALSE] ; class(id)
# Sous forme de vecteur
id <- donnees_tidyverse %>% pull(1)
class(id)
pos <- 1
id <- donnees_tidyverse %>% pull(all_of(pos))
class(id)

# Sous forme de tibble
id <- donnees_tidyverse %>% select(1)
class(id)
pos <- 1
id <- donnees_tidyverse %>% select(all_of(pos))
class(id)
pos <- 1
# Résultat sous forme de vecteur caractère
id <- donnees_datatable[[pos]] ; class(id)

# Résultat sous forme de data.table
id <- donnees_datatable[pos] ; class(id)

En DuckDB, il y a une vraie différence entre select et pull. Dans le premier cas, les calculs restent du côté DuckDB, et c’est donc le moteur SQL qui continue à exécuter les calculs. Avec pull, le résultat est un tibble et les données sont transférées à la session R.

requete_duckdb %>% select(3)
# # Source:   SQL [?? x 1]
# # Database: DuckDB v0.10.2 [sebastien.li-thiao-t@Windows 10 x64:R 4.3.2/:memory:]
#   csp  
#   <chr>
# 1 1    
# 2 1    
# 3 1    
# 4 1    
# # ℹ more rows
requete_duckdb %>% pull(3)
#  [1] "1" "1" "1" "1" "1" "1" "3" "3" "3" "2" "3" "5" "5" "3" "3" "3" "4" "4" "4"
# [20] "4" "4" "2" "2" "1" "4" "4" "4"
pos = 0 # Contrairement à R, le compte commence à partir de 0 en Python

# Résultat sous forme de vecteur caractère
donnees_python.iloc[:, pos]

# Résultat sous forme de data.frame
donnees_python.iloc[:, [pos]]

5.2 Sélectionner une colonne par son nom

data Colonnes;set donnees_sas (keep = identifiant);run;
data Colonnes;set donnees_sas;keep identifiant;run;
# Résultat sous forme de vecteur caractère
id <- donnees_rbase$identifiant ; class(id)
id <- donnees_rbase[["identifiant"]] ; class(id)
id <- donnees_rbase[, "identifiant"] ; class(id)

# Résultat sous forme de data.frame
id <- donnees_rbase["identifiant"] ; class(id)
# Attention, utilisation du drop = FALSE étrange
# En fait, l'affectation par [] a pour option par défaut drop = TRUE. Ce qui implique que si l'affectation renvoie
# un data.frame d'1 seule colonne, l'objet sera transformé en objet plus simple (vecteur en l'occurrence)
class(donnees_rbase[, "identifiant", drop = FALSE])
id <- donnees_rbase["identifiant"] ; class(id)
id <- donnees_rbase[, "identifiant", drop = FALSE] ; class(id)
# Sous forme de vecteur
id <- donnees_tidyverse %>% pull(identifiant)
id <- donnees_tidyverse %>% pull("identifiant")

# Sous forme de tibble
id <- donnees_tidyverse %>% select(identifiant)
id <- donnees_tidyverse %>% select("identifiant")
# Résultat sous forme de vecteur caractère
id <- donnees_datatable$identifiant ; class(id)
id <- donnees_datatable[["identifiant"]] ; class(id)
id <- donnees_datatable[, identifiant] ; class(id)

# Résultat sous forme de data.table
id <- donnees_datatable[, "identifiant"] ; class(id)
id <- donnees_datatable[, .SD, .SDcols = "identifiant"] ; class(id)
# Ne fonctionnent pas !
#id <- donnees_datatable[, .("identifiant")] ; class(id)
#id <- donnees_datatable[J("identifiant")] ; class(id)
#id <- donnees_datatable[, list("identifiant")] ; class(id)
#id <- donnees_datatable[list("identifiant")] ; class(id)
requete_duckdb %>% select(identifiant)
requete_duckdb %>% select("identifiant") # déconseillé
requete_duckdb %>% select(any_of("identifiant"))

Note : certaines fonction du tidyverse nécessitent de passer par les opérateurs any_of ou all_of pour ce genre d’opérations (distinct par exemple). On conseille de le faire aussi pour select.

# Résultat sous forme de vecteur caractère
donnees_python["identifiant"]
donnees_python.identifiant

# Résultat sous forme de data.frame
donnees_python[["identifiant"]]

5.3 Selection de colonnes par un vecteur contenant des chaînes de caractères

%let var = identifiant Sexe note_contenu;
data Colonnes;
  /* Sélection de colonnes */
  set donnees_sas (keep = &var.);
  /* Autre solution */
  keep &var.;
run;
variable <- "identifiant"
# Résultat sous forme de vecteur caractère
id <- donnees_rbase[, variable] ; class(id)
id <- donnees_rbase[[variable]] ; class(id)

# Résultat sous forme de data.frame
id <- donnees_rbase[variable] ; class(id)
# Attention, utilisation du drop = FALSE étrange
# En fait, l'affectation par [] a pour option par défaut drop = TRUE. Ce qui implique que si l'affectation renvoie un data.frame d'1 seule colonne, l'objet sera transformé en objet plus simple (vecteur en l'occurrence)
id <- donnees_rbase[, variable, drop = FALSE] ; class(id)
variable <- "identifiant"
# Sous forme de vecteur
id <- donnees_tidyverse %>% pull(all_of(variable))
# Sous forme de tibble
id <- donnees_tidyverse %>% select(all_of(variable))
# Résultat sous forme de vecteur caractère
variable <- "identifiant"
id <- donnees_datatable[[variable]] ; class(id)
id <- donnees_datatable[, get(variable)] ; class(id)

# Résultat sous forme de data.table
id <- donnees_datatable[, ..variable] ; class(id)
id <- donnees_datatable[, variable, with = FALSE] ; class(id)
id <- donnees_datatable[, .SD, .SDcols = variable] ; class(id)
id <- donnees_datatable[, variable, env = list(variable = as.list(variable))] ; class(id)

# Attention, ces syntaxes ne fonctionnent pas ! Il faut nécessairement passer par les syntaxes au-dessus.
#id <- donnees_datatable[, .(variable)] ; class(id)
#id <- donnees_datatable[, list(variable)] ; class(id)
variable <- c("identifiant","duree")
requete_duckdb %>% select(any_of(variable))
variable = 'identifiant'

# Résultat sous forme de vecteur caractère
donnees_python[nom_var]

# Résultat sous forme de data.frame
donnees_python[[nom_var]]

5.4 Sauf certaines variables

%let var = identifiant Sexe note_contenu;
data Colonnes;set donnees_sas (drop = &var.);run;
variable <- c("identifiant", "sexe", "note_contenu")
exclusion_var <- donnees_rbase[, setdiff(names(donnees_rbase), variable)]

# Ne fonctionnent pas !
#exclusion_var <- donnees_rbase[, -c(variable)]
#exclusion_var <- donnees_rbase[, !c(variable)]
variable <- c("identifiant", "sexe", "note_contenu")
exclusion_var <- donnees_tidyverse %>% select(!all_of(variable))
exclusion_var <- donnees_tidyverse %>% select(-all_of(variable))
variable <- c("identifiant", "sexe", "note_contenu")
exclusion_var <- donnees_datatable[, !..variable]

Les opérateurs - et ! fonctionnent.

requete_duckdb %>% select(!identifiant)
requete_duckdb %>% select(-all_of(variable))
variable = ["identifiant", "sexe_red", "note_contenu"]
donnees_python.drop(columns=variable, axis = 0)
# En ajoutant l'argument inplace = True à la fonction .drop(), la base de données est directement modifiée en supprimant les variables du vecteur

5.5 Sélectionner la 3e colonne

proc contents data = donnees_sas out = Var noprint;run;

proc sql noprint;
  select name into :nom_col separated by " "
  from Var
  where varnum = 3;
run;

data Col3;set donnees_sas (keep = &nom_col.);run;
col3 <- donnees_rbase[, 3]

# Autre possibilité
col3 <- donnees_rbase[3]
col3 <- donnees_tidyverse %>% pull(3)

# Autre possibilité
col3 <- donnees_tidyverse %>% select(3)
col3 <- donnees_datatable[, 3]
requete_duckdb %>% select(3)
# Attention, en Python, la position de la 3e colonne est 2
pos = 3
donnees_python.iloc[:, pos-1]

5.6 Sélectionner plusieurs colonnes

%let var = identifiant note_contenu sexe;
data Colonnes;set donnees_sas (keep = &var.);run;

/* Autre solution */
/* En SQL, les variables sélectionnées dans l'instruction SELECT sont séparées par des virgules. On ajoute des virgules entre les variables. */
proc sql;
  create table Colonnes as
  select %sysfunc(tranwrd(&var., %str( ), %str(, )))
  from donnees_sas;
quit;
cols <- c("identifiant", "note_contenu", "sexe")
colonnes <- donnees_rbase[, cols]

# Autre possibilité
colonnes <- donnees_rbase[cols]
cols <- c("identifiant", "note_contenu", "sexe")
# Plusieurs possibilités
colonnes <- donnees_tidyverse %>% select(all_of(cols))
colonnes <- donnees_tidyverse %>% select(any_of(cols))
colonnes <- donnees_tidyverse %>% select({{ cols }})
colonnes <- donnees_tidyverse %>% select(!!cols)
cols <- c("identifiant", "note_contenu", "sexe")
# Plusieurs écritures possibles

# Ecriture cohérente avec la logique data.table
colonnes <- donnees_datatable[, .SD, .SDcols = cols]

# Ecriture avec with = FALSE : désactive la possibilité de se référer à des colonnes sans les guillemets
colonnes <- donnees_datatable[, cols, with = FALSE]

# Ecriture avec mget
colonnes <- donnees_datatable[, mget(cols)]

# Ecriture un peu contre-intuitve. Attention ! L'écriture est bien ..cols, et non ..(cols) !!
# Les syntaxes donnees_datatable[, ..(cols)] et donnees_datatable[, .(cols)] ne fonctionnent pas
colonnes <- donnees_datatable[, ..cols]
cols <- c("identifiant", "note_contenu", "sexe")
# Plusieurs possibilités
requete_duckdb %>% select(all_of(cols))
requete_duckdb %>% select(any_of(cols))
requete_duckdb %>% select({{ cols }})
requete_duckdb %>% select(!!cols)
cols = ["identifiant", "note_contenu", "sexe"]
colonnes = donnees_python[cols]

5.7 Sélectionner les colonnes qui débutent par le mot Note

/* 1ère solution */
data Selection_Variables;set donnees_sas (keep = Note:);run;

/* 2e solution */
proc contents data = donnees_sas out = Var noprint;run;
proc sql;
  select name into :var_notes separated by " "
  from Var where substr(upcase(name), 1, 4) = "NOTE" order by varnum;
run;
proc datasets lib = Work nolist;delete Var;run;
data donnees_sas_Notes;set donnees_sas (keep = &var_notes.);run;
varNotes <- donnees_rbase[grepl("^note", names(donnees_rbase), ignore.case = TRUE)]

# Autre possibilité
varNotes <- donnees_rbase[substr(tolower(names(donnees_rbase)), 1, 4) == "note"]
varNotes <- donnees_tidyverse %>% select(starts_with("note"))
# 1ère méthode
cols <- names(donnees_datatable)[substr(names(donnees_datatable), 1, 4) == "note"]
# Ou encore
cols <- names(donnees_datatable)[names(donnees_datatable) %like% "^note"]

sel <- donnees_datatable[, .SD, .SDcols = cols]

# 2e méthode
sel <- donnees_datatable[, .SD, .SDcols = patterns("^note")]
requete_duckdb %>% select(starts_with("note"))
varNotes = donnees_python[list(filter(lambda col: re.match(r'^note', col, re.IGNORECASE), donnees_python.columns))]

5.8 Sélectionner les colonnes qui ne débutent pas par le mot Note

data Selection_Variables;set donnees_sas (drop = Note:);run;
varNotes <- donnees_rbase[! grepl("^note", names(donnees_rbase), ignore.case = TRUE)]

# Autre possibilité
varNotes <- donnees_rbase[substr(tolower(names(donnees_rbase)), 1, 4) != "note"]
varNotes <- donnees_tidyverse %>% select(-starts_with("note"))
varNotes <- donnees_tidyverse %>% select(!starts_with("note"))
cols <- grep("^note", names(donnees_datatable), value = TRUE, ignore.case = TRUE)
sel <- donnees_datatable[, .SD, .SDcols = -cols]
sel <- donnees_datatable[, .SD, .SDcols = -patterns("^note")]

# Autre possibilité
sel <- donnees_datatable[, grep("^note", names(donnees_datatable)) := NULL]
requete_duckdb %>% select(-starts_with("note"))
requete_duckdb %>% select(!starts_with("note"))
varNotes = donnees_python.drop(columns=list(filter(lambda col: re.match(r'^note', col, re.IGNORECASE), donnees_python.columns)), 
                               axis = 0)

5.9 Sélectionner l’ensemble des variables numériques de la base

data Colonnes;set donnees_sas (keep = _numeric_);run;
varNumeriques <- donnees_rbase[, sapply(donnees_rbase, is.numeric), drop = FALSE]
varNumeriques <- donnees_tidyverse %>% select_if(is.numeric)
varNumeriques <- donnees_tidyverse %>% select(where(is.numeric))
sel <- donnees_datatable[, .SD, .SDcols = is.numeric]
requete_duckdb %>% select_if(is.numeric)
# requete_duckdb %>% select(where(is.numeric))
varNumeriques = donnees_python.select_dtypes(include='number')

5.10 Sélectionner l’ensemble des variables de format “Date”

proc contents data = donnees_sas out = Var noprint;run;
proc sql noprint;
  select name into :nom_col separated by " "
  from Var where format not in ("$", "");
run;
data Colonnes;set donnees_sas (keep = &nom_col.);run;
proc datasets lib = Work nolist;delete Var;run;
varDates <- donnees_rbase[, sapply(donnees_rbase, is.Date), drop = FALSE]
varDates <- Filter(is.Date, donnees_rbase)
varDates <- donnees_tidyverse %>% select(where(is.Date))
varDates <- donnees_tidyverse %>% select_if(is.Date)
var_dates <- donnees_datatable[, .SD, .SDcols = is.Date]
requete_duckdb %>% select_if(is.Date)
# requete_duckdb %>% select(where(is.Date))
varDates = donnees_python.select_dtypes(include=['datetime64[ns]'])

6 Sélection de lignes

6.1 Sélectionner des lignes par leur numéro

6.1.1 3e ligne

data Ligne3; set donnees_sas (firstobs = 3 obs = 3); run;
ligne3 <- donnees_rbase[3, ]
ligne3 <- donnees_tidyverse %>% slice(3)
ligne3 <- donnees_datatable[3, ]
ligne3 <- donnees_datatable[3]

DuckDB, moteur SQL, ne respecte pas l’ordre des lignes. Il faut passer par un filtre ou choisir explicitement un ordre.

donnees_python.iloc[2] # En Python, la troisieme ligne est en position 2

6.1.2 3 premières lignes et 3 premières colonnes

proc contents data = donnees_sas out = Var noprint;run;
proc sql noprint;
  select name into :nom_col separated by " " from Var
  where 1 <= varnum <= 3;
run;
data Top3;
  set donnees_sas (firstobs = 1 obs = 3 keep = &nom_col.);
run;
proc datasets lib = Work nolist;delete Var;run;
top3 <- donnees_rbase[1:3, 1:3]
top3 <- donnees_tidyverse %>% slice(1:3) %>% select(1:3)
top3 <- donnees_datatable[1:3, 1:3]

DuckDB, moteur SQL, ne respecte pas l’ordre des lignes. Il faut passer par un filtre ou choisir explicitement un ordre.

top3 = donnees_python.iloc[:3, :3]

6.2 Sélectionner des lignes par condition

6.2.1 Entrées en 2023

data En2023;
  set donnees_sas (where = (year(date_entree) = 2023));
run;
# Bonnes écritures, qui excluent les valeurs manquantes
en2023 <- donnees_rbase[lubridate::year(donnees_rbase$date_entree) %in% c(2023), ]
en2023 <- donnees_rbase[which(lubridate::year(donnees_rbase$date_entree) == 2023), ]
en2023 <- subset(donnees_rbase, lubridate::year(donnees_rbase$date_entree) == 2023)
en2023 <- donnees_tidyverse %>% filter(lubridate::year(date_entree) == 2023)
# Pas de problème avec les valeurs manquantes comme pour la syntaxe en R-Base
# Une fonction year() est déjà implémentée en data.table, l'usage de lubridate est inutile
en2023 <- donnees_datatable[data.table::year(date_entree) == 2023, ]
en2023 <- donnees_datatable[data.table::year(date_entree) == 2023]
en2023 <- subset(donnees_datatable, data.table::year(date_entree) == 2023)
requete_duckdb %>% filter(lubridate::year(date_entree) == 2023)
en2023 = donnees_python[donnees_python['date_entree'].dt.year == 2023]

6.2.2 Entrées entre 2021 et 2023

data Entre2021_2023;
  set donnees_sas (where = (2021 <= year(date_entree) <= 2023));
run;
entre2021_2023 <- donnees_rbase[lubridate::year(donnees_rbase$date_entree) %in% 2021:2023, ]
entre2021_2023 <- donnees_rbase[lubridate::year(donnees_rbase$date_entree) >= 2021 & lubridate::year(donnees_rbase$date_entree) <= 2023, ]
entre2021_2023 <- donnees_tidyverse %>% filter(between(lubridate::year(date_entree), 2021, 2023))
entre2021_2023 <- donnees_tidyverse %>% filter(lubridate::year(date_entree) %in% 2021:2023)
entre2021_2023 <- donnees_tidyverse %>% filter(lubridate::year(date_entree) >= 2021, lubridate::year(date_entree) <= 2023)
# Une fonction year() est déjà implémentée en data.table, l'usage de lubridate est inutile
entre2021_2023 <- donnees_datatable[data.table::year(date_entree) %in% 2021:2023]
entre2021_2023 <- donnees_datatable[between(data.table::year(date_entree), 2021, 2023)]
requete_duckdb %>% filter(between(lubridate::year(date_entree), 2021, 2023))
en2021_2023 = donnees_python[(donnees_python['date_entree'].dt.year >= 2021) &
                            (donnees_python['date_entree'].dt.year <= 2023)]

6.3 Sélectionner des lignes suivant de multiples conditions

/* Femmes entrées avant 2023 */
/* Ecriture correcte */
data Avant2023_Femme;
  set donnees_sas (where = (year(date_entree) < 2023 and not missing(date_entree) and sexe = 2));
run;

/* Ecriture incorrecte. Les valeurs manquantes sont considérées comme des nombres négatifs faibles, et inférieurs à 2023. */
/* Elles sont sélectionnées dans le code suivant : */
data Avant2023_Femme;
  set donnees_sas (where = (year(date_entree) < 2023 and sexe = 2));
run;
# Femmes entrées avant 2023
avant2023_femme <- subset(donnees_rbase, lubridate::year(date_entree) < 2023 & sexe == "2")

# Autre solution
avant2023_femme <- with(donnees_rbase, donnees_rbase[which(lubridate::year(date_entree) < 2023 & sexe == "2"), ])
# Femmes entrées avant 2023
avant2023_femme <- donnees_tidyverse %>% 
  filter(lubridate::year(date_entree) < 2023 & sexe == "2")
avant2023_femme <- donnees_tidyverse %>% 
  filter(lubridate::year(date_entree) < 2023, sexe == "2")
# Femmes entrées avant 2023
# Une fonction year() est déjà implémentée en data.table, l'usage de lubridate est inutile
avant2023_femme <- donnees_datatable[data.table::year(date_entree) < 2023 & sexe == "2"]
avant2023_femme <- subset(donnees_datatable, data.table::year(date_entree) < 2023 & sexe == "2")
requete_duckdb %>% 
  filter(lubridate::year(date_entree) < 2023 & sexe == "2") # Femmes entrées avant 2023
avant2023_femme = donnees_python[(donnees_python['date_entree'].dt.year < 2023) &
                            (donnees_python['sexe'] == "2")]

6.4 Sélectionner des lignes par référence : lignes de l’identifiant “087”

%let var = identifiant;
%let sel = 087;

data Selection;
  set donnees_sas;
  if &var. in ("&sel.");
run;

/* Autre solution */
data Selection;
  set donnees_sas (where = (&var. in ("&sel.")));
run;
variable <- "identifiant"
sel <- "087"
donnees_rbase[donnees_rbase[, variable] %in% sel, ]

# Autre solution
subset(donnees_rbase, get(variable) %in% sel)
donnees_tidyverse %>% filter(identifiant %in% c("087")) %>% select(identifiant)
donnees_tidyverse %>% filter(identifiant == "087") %>% select(identifiant)

# Essayons désormais par variable
variable <- "identifiant"
sel <- "087"
donnees_tidyverse %>% filter(if_any(variable, ~ .x %in% sel)) %>% select(all_of(variable))
donnees_tidyverse %>% filter(get(variable) %in% sel) %>% select(all_of(variable))
variable <- "identifiant"
sel <- "087"
donnees_datatable[donnees_datatable[[variable]] %chin% sel, ]
donnees_datatable[get(variable) %chin% sel, ]
requete_duckdb %>% filter(identifiant %in% c("087")) %>% select(identifiant)
requete_duckdb %>% filter(identifiant == "087") %>% select(identifiant)
# Essayons désormais par variables
variable <- "identifiant"
sel <- "087"
requete_duckdb %>% select(any_of(variable))
variable = "identifiant"
sel = "087"
donnees_python[donnees_python[variable] == sel]
donnees_python[donnees_python[variable].isin([sel])]

6.5 Sélectionner des lignes et des colonnes

%let cols = identifiant note_contenu sexe;
data Femmes;
  set donnees_sas (where = (Sexe = 2) keep = &cols.);
run;

/* Autre solution */
data Femmes;
  set donnees_sas;
  if Sexe = 2;
  keep &cols.;
run;

/* Par nom ou par variable */
%let var = identifiant Sexe note_contenu;
data Femmes;
  /* Sélection de colonnes */
  set donnees_sas (keep = &var.);
  /* Sélection de lignes respectant une certaine condition */
  if Sexe = "2";
  /* Création de colonne */
  note2 = note_contenu / 20 * 5;
  /* Suppression de colonnes */
  drop Sexe;
  /* Selection de colonnes */
  keep identifiant Sexe note_contenu;
run;
cols <- c("identifiant", "note_contenu", "sexe", "date_naissance")
femmes <- donnees_rbase[donnees_rbase$sexe %in% c("2"), cols]
femmes <- subset(donnees_rbase, sexe %in% c("2"), cols)
cols <- c("identifiant", "note_contenu", "sexe", "date_naissance")
femmes <- donnees_tidyverse %>% filter(sexe == "2") %>% select(all_of(cols))
femmes <- donnees_tidyverse %>% filter(sexe == "2") %>% select({{cols}})
cols <- c("identifiant", "note_contenu", "sexe", "date_naissance")
femmes <- donnees_datatable[sexe == "2", ..cols]
femmes <- subset(donnees_datatable, sexe %in% c("2"), cols)
cols <- c("identifiant", "note_contenu", "sexe", "date_naissance")
requete_duckdb %>% filter(sexe == "2") %>% select(all_of(cols))
requete_duckdb %>% filter(sexe == "2") %>% select({{cols}})
cols = ["identifiant", "note_contenu", "sexe", "date_naissance"]
femmes = donnees_python[donnees_python["sexe"] == "2"][cols]

6.6 Sélectionner des lignes selon une condition externe

On souhaite sélectionner des colonnes selon une condition, mais cette condition est située à l’extérieur des opérateurs de manipulation des données.

%let condition = sexe = 2;
data Femmes;
  set donnees_sas (where = (&condition.));
run;
condition <- substitute(sexe == "2")
femmes <- subset(donnees_rbase, eval(condition))

# Autre solution
condition <- quote(sexe == "2")
femmes <- subset(donnees_rbase, eval(condition))
condition <- expr(sexe == "2")
femmes <- donnees_tidyverse %>% 
  filter(!!condition)
condition <- quote(sexe == "2")
femmes <- donnees_datatable[condition, , env = list(condition = condition)]
femmes <- donnees_datatable[eval(condition)]
filter_condition <- . %>% filter(sexe == "2")
requete_duckdb %>% filter_condition()
condition = lambda df: df['sexe'] == "2"
femmes = donnees_python[condition(donnees_python)]

7 Manipulation des lignes et des colonnes

7.1 Renommer des variables

On renomme sexe en sexe2, puis on renomme à son tour sexe2 en sexe.

data donnees_sas;
  set donnees_sas (rename = (sexe = sexe2));
  rename sexe2 = sexe;
run;
# On renomme la variable sexe en sexe_red
names(donnees_rbase)[names(donnees_rbase) == "sexe"] <- "sexe_red"

# On la renomme en sexe
names(donnees_rbase)[names(donnees_rbase) == "sexe_red"] <- "sexe"
# On renomme la variable sexe en sexe_red
donnees_tidyverse <- donnees_tidyverse %>%
  rename(sexe_red = sexe)

# On la renomme en sexe
donnees_tidyverse <- donnees_tidyverse %>%
  rename(sexe = sexe_red)
# On renomme la variable sexe en sexe_red
names(donnees_datatable)[names(donnees_datatable) == "sexe"] <- "sexe_red"

# On la renomme en sexe
names(donnees_datatable)[names(donnees_datatable) == "sexe_red"] <- "sexe"

# Autre solution
# En data.table, les instructions débutant par set modifient les éléments par référence, c'est-à-dire sans copie.
# Ceci est plus efficace pour manipuler des données volumineuses.
setnames(donnees_datatable, "sexe", "sexe_red")
setnames(donnees_datatable, "sexe_red", "sexe")

En dplyr/arrow/duckdb, le renommage n’est pas persistant, i.e. la variable requete_duckdb n’est pas modifiée par la fonction rename.

# On renomme la variable sexe en sexe_red
requete_duckdb %>% rename(sexe_red = sexe)
# Renommer la colonne sexe en sexe_red
donnees_python = donnees_python.rename(columns={'sexe': 'sexe_red'})

# On la renomme en sexe
donnees_python = donnees_python.rename(columns={'sexe_red': 'sexe'})

7.2 Créer des variables avec des conditions

data Civilite;
  set donnees_sas;
  
  /* 1ère solution (if) */
  format Civilite $20.;
  if      Sexe = 2 then Civilite = "Mme";
  else if Sexe = 1 then Civilite = "M";
  else                  Civilite = "Inconnu";
  
  /* 2e solution (do - end) */
  if      Sexe = 2 then do;
    Civilite2 = "Mme";
  end;
  else if Sexe = 1 then do;
    Civilite2 = "M";
  end;
  else do;
    Civilite2 = "Inconnu";
  end;
  
  /* 3e solution (select) */
  format Civilite3 $20.;
  select;
    when      (Sexe = 2) Civilite3 = "Mme";
    when      (Sexe = 1) Civilite3 = "M";
    otherwise            Civilite3 = "Inconnu";
  end;
  
  keep Sexe Civilite Civilite2 Civilite3;run;
run;
donnees_rbase$civilite <- ifelse(donnees_rbase$sexe == "2", "Mme",
                                 ifelse(donnees_rbase$sexe == "1", "M",
                                        "Inconnu"))

# Autre solution (rapide)
donnees_rbase$civilite                            <- "Inconnu"
donnees_rbase$civilite[donnees_rbase$sexe == "1"] <- "M"
donnees_rbase$civilite[donnees_rbase$sexe == "2"] <- "Mme"
donnees_tidyverse <- donnees_tidyverse %>%
  mutate(civilite = case_when(sexe == "2" ~ "Mme",
                              sexe == "1" ~ "M",
                              TRUE        ~ "Inconnu")
)

donnees_tidyverse <- donnees_tidyverse %>%
    mutate(civilite = if_else(sexe == "2", "Mme",
                              if_else(sexe == "1", "M",
                                      "Inconnu")))
donnees_datatable[, civilite := fcase(sexe == "2", "Mme",
                                      sexe == "1", "M",
                                      is.na(sexe), "Inconnu")]

Note : l’opération n’est pas persistante, i.e. l’objet requete_duckdb n’est pas modifié

requete_duckdb %>%
  mutate(civilite = case_when(sexe == "2" ~ "Mme",
                              sexe == "1" ~ "M",
                              .default = "Inconnu"))

requete_duckdb %>%
  mutate(civilite = if_else(sexe == "2", "Mme",
                            if_else(sexe == "1", "M",
                                    "Inconnu")))
# Avec un mapping : 
mapping = {'2': 'Mme', '1': 'M'}
donnees_python['civilite'] = donnees_python['sexe'].map(mapping).filna('Inconnu')

# Avec une fonction apply/lambda et les condition IF/ELSE :
donnees_python['civilite'] = donnees_python['sexe'].apply(
    lambda x: 'Mme' if x == '2' else ('M' if x == '1' else 'Inconnu')
)

7.3 Formater les modalités des valeurs discrètes ou caractères

7.3.1 Création des formats

/* Utilisation des formats */
proc format;
  /* Variable discrète */
  value sexef
  1 = "Homme"
  2 = "Femme";

  /* Variable caractère */
  value $ cspf
  '1' = "Cadre"
  '2' = "Profession intermédiaire"
  '3' = "Employé"
  '4' = "Ouvrier"
  '5' = "Retraité";
run;
sexef <- c("1" = "Homme", "2" = "Femme")
cspf  <- c("1" = "Cadre", "2" = "Profession intermédiaire", "3" = "Employé", "4" = "Ouvrier", "5" = "Retraité")
sexef_format <- c("1" = "Homme", "2" = "Femme")
cspf_format  <- c("1" = "Cadre", "2" = "Profession intermédiaire", "3" = "Employé", "4" = "Ouvrier", "5" = "Retraité")
sexeform <- c("1" = "Homme", "2" = "Femme")
cspform  <- c("1" = "Cadre", "2" = "Profession intermédiaire", "3" = "Employé", "4" = "Ouvrier", "5" = "Retraité")

Préférer case_match quand il s’agit de valeurs déterminées.

requete_duckdb %>% 
  mutate(sexef = case_when(
    sexef=="1" ~ "Homme",
    sexef=="2" ~ "Femme",
    .default = sexef),
    
         cspf = case_match(csp,
    "1" ~ "Cadre",
    "2" ~ "Profession intermédiaire",
    "3" ~ "Employé",
    "4" ~ "Ouvrier",
    "5" ~ "Retraité",
    .default = csp)) %>% 
  select(Sexe, sexef, csp, cspf)
# On créée les formats sous type de dictionnaire
sexef_format = {
                "1": "Homme", 
                "2": "Femme"
                }
cspf_format = {
    "1": "Cadre", 
    "2": "Profession intermédiaire", 
    "3": "Employé", 
    "4": "Ouvrier", 
    "5": "Retraité"
}

7.3.2 Utiliser les formats (valeurs discrètes ou caractères)

Nécessite le lancement des formats à l’étape précédente.

data donnees_sas;
  set donnees_sas;
  /* Exprimer dans le format sexef (Hommes / Femmes) */
  format Sexef $25.;
  Sexef = put(Sexe, sexef.);
  /* On exprime la CSP en texte dans une variable CSPF avec le format */
  format CSPF $25.;
  CSPF = put(CSP, $cspf.);
run;
# On exprime CSP et sexe en variable formatée
donnees_rbase$cspf  <- cspf[donnees_rbase$csp]
donnees_rbase$sexef <- sexef[donnees_rbase$sexe]
# On exprime CSP et sexe en variable formatée
donnees_tidyverse <- donnees_tidyverse %>% 
  mutate(sexef = sexef_format[sexe],
         cspf = cspf_format[csp])

# Autre solution
# Les éventuelles valeurs manquantes sont conservées en NA
donnees_tidyverse <- donnees_tidyverse %>% 
  mutate(
    sexef = case_when(
      sexe == "1" ~ "Homme",
      sexe == "2" ~ "Femme",
      TRUE        ~ sexe),
    
    cspf = case_when(
      csp == "1" ~ "Cadre",
      csp == "2" ~ "Profession intermédiaire",
      csp == "3" ~ "Employé",
      csp == "4" ~ "Ouvrier",
      csp == "5" ~ "Retraité",
      TRUE       ~ csp)
    )

# Syntaxe pour attribuer une valeur aux NA
valeurAuxNA <- donnees_tidyverse %>% 
  mutate(sexef = case_when(
    sexe == "1" ~ "Homme",
    sexe == "2" ~ "Femme",
    is.na(x)    ~ "Inconnu",
    TRUE        ~ sexe))
# On exprime CSP et sexe en variable formatée
donnees_datatable[, `:=` (cspf = cspform[csp], sexef = sexeform[sexe])]
donnees_python['sexef'] = donnees_python['sexe'].map(sexef_format)
# On peut aussi utiliser replace : donnees_python['sexef'] = donnees_python['sexe'].replace(sexef_format)
donnees_python['cspf'] = donnees_python['csp'].map(cspf_format)

7.4 Formater les modalités des valeurs continues

/* Âge formaté */
/* Fonctionne aussi sur le principe du format */
proc format;
  /* Variable continue */
  value agef
  low-<26 = "1. De 15 à 25 ans"
  26<-<50 = "2. De 26 à 49 ans"
  50-high = "3. 50 ans ou plus";
run;

data donnees_sas;
  set donnees_sas;
  /* Âge formaté */
  Agef = put(Age, agef.);
run;
# Âge formaté
# L'option right = TRUE implique que les bornes sont ]0; 25] / ]25; 49] / ]49; Infini[
agef <- cut(donnees_rbase$age, 
            breaks         = c(0, 25, 49, Inf),
            right          = TRUE,
            labels         = c("1. De 15 à 25 ans", "2. De 26 à 49 ans", "3. 50 ans ou plus"), 
            ordered_result = TRUE)

# Autres solutions
donnees_rbase$agef[donnees_rbase$age < 26]                           <- "1. De 15 à 25 ans"
# 26 <= donnees_rbase$age < 50 ne fonctionne pas, il faut passer en 2 étapes
donnees_rbase$agef[26 <= donnees_rbase$age & donnees_rbase$age < 50] <- "2. De 26 à 49 ans"
donnees_rbase$agef[donnees_rbase$age >= 50]                          <- "3. 50 ans ou plus"

donnees_rbase$agef <- ifelse(donnees_rbase$age < 26, "1. De 15 à 25 ans",
                             ifelse(26 <= donnees_rbase$age & donnees_rbase$age < 50, "2. De 26 à 49 ans",
                                    ifelse(donnees_rbase$age >= 50, "3. 50 ans ou plus",
                                           NA_integer_)))
# Âge formaté
donnees_tidyverse <- donnees_tidyverse %>%
  mutate(agef = case_when(
    age < 26             ~ "1. De 15 à 25 ans",
    age >= 26 & age < 50 ~ "2. De 26 à 49 ans",
    age >= 50            ~ "3. 50 ans ou plus")
    )
# Âge formaté
donnees_datatable[, agef := fcase(age < 26,             "1. De 15 à 25 ans",
                                  26 <= age & age < 50, "2. De 26 à 49 ans",
                                  age >= 50,            "3. 50 ans ou plus")]

# Autre solution
age_fmt <- c(
  c(mapply(`=`, 0:25,   rep("1. De 15 à 25 ans", 26))),
  c(mapply(`=`, 26:49,  rep("2. De 26 à 49 ans", 24))),
  c(mapply(`=`, 50:100, rep("3. 50 ans ou plus", 51)))
  )
donnees_datatable[, agef := age_fmt[age]]

Préférer case_match quand il s’agit de valeurs déterminées.

# Âge formaté
requete_duckdb %>%
  mutate(agef = case_when(
    age < 26             ~ "1. De 15 à 25 ans",
    age >= 26 | age < 50 ~ "2. De 26 à 49 ans",
    age >= 50            ~ "3. 50 ans ou plus")) %>% 
  select(age, agef)
# Pour les bins : [0, 26, 51] correspond à [0, 26[, [26, 51[, etc
donnees_python['agef'] = pd.cut(donnees_python['age'], 
                                   bins=[0, 26, 51, float('inf')], 
                                   labels=["1. De 15 à 25 ans", "2. De 26 à 49 ans", "3. 50 ans ou plus"], 
                                   right=False)

7.5 Changer le type d’une variable

data donnees_sas;
  set donnees_sas;
  
  /* Transformer la variable Sexe en caractère */
  Sexe_car = put(Sexe, $1.);
  
  /* Transformer la variable Sexe_car en numérique */
  Sexe_num = input(Sexe_car, 1.);
  
  /* Transformer une date d'un format caractère à un format Date */
  format date $10.;
  date = "01/01/2000";
  format date_sas yymmdd10.;
  date_sas = input(date, ddmmyy10.);
run;
# Transformer la variable sexe en numérique
donnees_rbase$sexe_numerique <- as.numeric(donnees_rbase$sexe)

# Transformer la variable sexe_numerique en caractère
donnees_rbase$sexe_caractere <- as.character(donnees_rbase$sexe_numerique)

# Transformer une date d'un format caractère à un format Date
donnees_rbase$date_r <- lubridate::dmy("01/01/2000")
# Transformer la variable sexe en numérique
donnees_tidyverse <- donnees_tidyverse %>%  
  mutate(sexe_numerique = as.numeric(sexe))

# Transformer la variable sexe_numerique en caractère
donnees_tidyverse <- donnees_tidyverse %>%
  mutate(sexe_caractere = as.character(sexe_numerique))

# Transformer une date d'un format caractère à un format Date
donnees_tidyverse <- donnees_tidyverse %>%  
  mutate(date_r = lubridate::dmy("01/01/2000"))
# Transformer la variable sexe en numérique
donnees_datatable[, sexe_numerique := as.numeric(sexe)]

# Transformer la variable sexe_numerique en caractère
donnees_datatable[, sexe_caractere := as.character(sexe_numerique)]

# Transformer une date d'un format caractère à un format Date
donnees_datatable[, date_r := lubridate::dmy("01/01/2000")]
requete_duckdb %>%  
  mutate(sexe_numerique = as.numeric(sexe)) %>% # Transformer la variable sexe en numérique
  mutate(sexe_caractere = as.character(sexe_numerique)) %>% # Transformer la variable sexe_numerique en caractère
  select(starts_with("sexe")) %>% print(n=5)

En DuckDB, plusieurs syntaxes sont possibles pour transformer une chaîne de caractères en date si la chaîne de caractères est au format YYYY-MM-DD. Dans le cas contraire, passer par la fonction strptime de DuckDB pour indiquer le format de la date.

# Transformer une date d'un format caractère à un format Date
requete_duckdb %>%  
  mutate(date_0 = as.Date("2000-01-01")) %>% 
  mutate(date_1 = as.Date(strptime("01/01/2000","%d/%m/%Y"))) %>% 
  # mutate(date_r = lubridate::dmy("01/01/2000")) %>% # no known SQL translation
  select(starts_with("date"))

Note : duckdb fait des conversions de type implicitement, mais seulement les conversions incontestables. Il faudra souvent préciser le type des variables.

# Transformer la variable sexe en numérique
donnees_python['sexe_numerique'] = pd.to_numeric(donnees_python['sexe'])

# Transformer la variable sexe_numerique en caractère
donnees_python['sexe_caractere'] = donnees_python['sexe_numerique'].astype(str)

# Transformer une date d'un format caractère à un format Date
donnees_python['date_r'] = pd.to_datetime('01/01/2000', format='%d/%m/%Y')

7.6 Changer le type de plusieurs variables

/* On souhaite réexprimer certaines dates dans un autre format */
%let en_date = date_naissance date_entree;

data Changer_plusieurs_formats;
  set donnees_sas;
  
  %macro it;
    %local i j;
    %do i = 1 %to %sysfunc(countw(&en_date.));
      %let j = %scan(&en_date., &i.);
          format &j. yymmdd10.;
    %end;
  %mend it;
  %it;
  
run;
enNumerique <- c("duree", "note_contenu", "note_formateur")
enDate <- c('date_naissance', 'date_entree')

changer_plusieurs_formats <- donnees_rbase
changer_plusieurs_formats[, enNumerique] <- lapply(changer_plusieurs_formats[, enNumerique], as.integer)
changer_plusieurs_formats[, enDate]      <- lapply(changer_plusieurs_formats[, enDate], format, "%d/%m/%Y")
enNumerique <- c("duree", "note_contenu", "note_formateur")
enDate <- c('date_naissance', 'date_entree')

changer_plusieurs_formats <- donnees_tidyverse %>%
  mutate(across(all_of(enNumerique), as.integer)) %>% 
  mutate(across(all_of(enDate), ~ format(.x, "%d/%m/%Y")))
enNumerique <- c("duree", "note_contenu", "note_formateur")
enDate <- c('date_naissance', 'date_entree')

changer_plusieurs_formats <- copy(donnees_datatable)
changer_plusieurs_formats[, (enNumerique) := lapply(.SD, as.integer),         .SD = enNumerique]
changer_plusieurs_formats[, (enDate)      := lapply(.SD, format, "%d/%m/%Y"), .SD = enDate]
enNumerique <- c("duree", "note_contenu", "note_formateur")
enDate <- c('date_naissance', 'date_entree')

requete_duckdb %>%  
  mutate_at(enNumerique, as.integer) %>% 
  mutate_at(enDate, as.character) %>% 
  mutate_at(enDate, ~ as.Date(strptime(.,'%Y-%m-%d'))) %>% # strptime est une fonction duckdb
  select(enNumerique, enDate) %>% print(n=5)

7.7 Créer et supprimer des variables

7.7.1 1er exemple

/* Manipulation de colonnes par référence */
data Creation;
  set donnees_sas;
  note_contenu2 = note_contenu / 20 * 5;
  note_formateur2 = note_formateur / 20 * 5;
  /* Suppression des variables créées */
  drop note_contenu2 note_formateur2;
run;
donnees_rbase$note2 <- donnees_rbase$note_contenu / 20 * 5
# Le with permet de s'affranchir des expressions "donnees_rbase$"
with(donnees_rbase, note2 <- note_contenu / 20 * 5)

# On ne peut pas utiliser transform pour des variables récemment créées
#donnees_rbase <- transform(donnees_rbase, note3 = note_contenu ** 2, note3 = log(note3))
donnees_rbase <- transform(donnees_rbase, note2 = note_contenu / 20 * 5)

# Suppression de variables
donnees_rbase$note2 <- NULL
donnees_tidyverse <- donnees_tidyverse %>% 
  mutate(note2 = note_contenu / 20 * 5)

# Suppression de variables
donnees_tidyverse <- donnees_tidyverse %>% 
  select(-note2)
# Création de variables
donnees_datatable[, note2 := note_contenu / 20 * 5]

# Suppression de variables
donnees_datatable[, note2 := NULL]

Note : l’opération n’est pas persistante, i.e. l’objet requete_duckdb n’est pas modifié

# Création de la colonne note2
requete_duckdb %>% 
  mutate(note2 = as.integer(note_contenu) / 20 * 5) %>% 
  select(note2)

# Suppression de colonnes
requete_duckdb %>% select(-contains("date"), -starts_with("note"))
# Création de la colonne note2
donnees_python['note2'] = donnees_python['note_contenu'] / 20 * 5

# Suppression de variables :
donnees_python.drop(['note2'], axis = 1, inplace = True)

7.7.2 2e exemple

/* Création et suppressions de plusieurs variables */
data donnees_sas;
  set donnees_sas;
  note_contenu2 = note_contenu / 20 * 5;
  note_formateur2 = note_formateur / 20 * 5;  
                           
  /* Suppression des variables créées */
  drop note_contenu2 note_formateur2;
run;
# Création et suppressions de plusieurs variables
donnees_rbase <- transform(donnees_rbase, 
                           note_contenu2   = note_contenu / 20 * 5,
                           note_formateur2 = note_formateur / 20 * 5)

# Suppression des variables créées
variable <- c("note_contenu2", "note_formateur2")
donnees_rbase[, variable] <- NULL
# Création et suppressions de plusieurs variables
donnees_tidyverse <- donnees_tidyverse %>% 
  mutate(note_contenu2 = note_contenu / 20 * 5,
         note_formateur2 = note_formateur / 20 * 5)

# Suppression des variables créées
variable <- c("note_contenu2", "note_formateur2")
donnees_tidyverse <- donnees_tidyverse %>% 
  select(-all_of(variable))
# Création et suppressions de plusieurs variables
donnees_datatable[, c("note_contenu2", "note_formateur2") := list(note_contenu / 20 * 5, note_formateur / 20 * 5)]
donnees_datatable[, `:=` (note_contenu2 = note_contenu / 20 * 5, note_formateur2 = note_formateur / 20 * 5)]

# Suppression des variables créées
donnees_datatable[, c("note_contenu2", "note_formateur2") := NULL]

# Ou par référence extérieure
variable <- c("note_contenu2", "note_formateur2")
donnees_datatable[, `:=` (note_contenu2 = note_contenu / 20 * 5, note_formateur2 = note_formateur / 20 * 5)]
donnees_datatable[, (variable) := NULL]
# À FAIRE : à compléter !
# Création de la colonne note2
requete_duckdb %>% 
  mutate(note2 = as.integer(Note_Contenu) / 20 * 5) %>% 
  select(note2)

# Suppression de colonnes
#requete_duckdb %>% select(- CSP, -contains("Date"), -starts_with("Note"))
# Création des colonnes note_contenu2 et note_formateur2
donnees_python = donnees_python.assign(
                                    note_contenu2 = donnees_python['note_contenu'] / 20 * 5,
                                    note_formateur2 = donnees_python['note_formateur'] / 20 * 5
                                    )

# Suppression des variables nouvellement crées
donnees_python.drop(columns=['note_contenu2', 'note_formateur2'], axis = 1, inplace = True)

7.8 On souhaite réexprimer toutes les notes sur 100 et non sur 20

%let notes = Note_Contenu   Note_Formateur Note_Moyens     Note_Accompagnement     Note_Materiel;
/* On supprime d'abord les doubles blancs entre les variables */
%let notes = %sysfunc(compbl(&notes.));
/* On affiche les notes dans la log de SAS */
%put &notes;

/* 1ère solution : avec les array */
/* Les variables sont modifiées dans cet exemple */
data Sur100_1;
  set donnees_sas;
  array variables (*) &notes.;
  do increment = 1 to dim(variables);
    variables[increment] = variables[increment] / 20 * 100;
  end; 
  drop increment;
run;

/* 2e solution : avec une macro */
/* De nouvelles variables sont ajoutées dans cet exemple */
data Sur100_2;
  set donnees_sas;
  
  %macro Sur100;
    %do i = 1 %to %sysfunc(countw(&notes.));
      %let note = %scan(&notes., &i.);
      &note._100 = &note. / 20 * 100;
    %end;
  %mend Sur100;
  
  %Sur100;
run;
notes <- names(donnees_rbase)[grepl("^note", names(donnees_rbase))]

# Les variables sont modifiées dans cet exemple
sur100 <- donnees_rbase[, notes] / 20 * 100

# On  souhaite conserver les notes sur 100 dans d'autres variables, suffixées par _100
donnees_rbase[, paste0(notes, "_100")] <- donnees_rbase[, notes] / 20 * 100
# Les variables sont modifiées dans cet exemple
sur100 <- donnees_tidyverse %>% 
  mutate(across(starts_with("note"), ~ .x / 20 * 100))
# Autre solution
sur100 <- donnees_tidyverse %>% 
  select(starts_with("note")) %>%
  purrr::modify(~ .x / 20 * 100)

# On  souhaite conserver les notes sur 100 dans d'autres variables, suffixées par _100
donnees_tidyverse <- donnees_tidyverse %>% 
  mutate(across(starts_with("note"), ~ .x / 20 * 100, .names = "{.col}_100"))
notes <- names(donnees_datatable)[grepl("^note", names(donnees_datatable))]

# Les variables sont modifiées dans cet exemple
sur100 <- copy(donnees_datatable)
sur100 <- sur100[, (notes) := lapply(.SD, function(x) x / 20 * 100), .SDcols = notes]
sur100 <- sur100[, (notes) := lapply(.SD, function(x) x / 20 * 100), .SD = notes]

# Ou encore, plus simple
# Dans cet exemple, les notes dans la base donnees_datatable ne sont pas changées
sur100 <- sur100[, lapply(.SD, function(x) x / 20 * 100), .SDcols = patterns("^note")]

# On  souhaite conserver les notes sur 20 dans d'autres variables, suffixées par _20
donnees_datatable[, (paste0(notes, "_100")) := lapply(.SD, function(x) x / 20 * 100), .SDcols = notes]

# Autre possibilité en utilisant l'instruction set, très rapide
for (j in notes) {
  set(x = donnees_datatable, j = paste0(j, "_100"), value = donnees_datatable[[j]] / 20 * 100)
}
requete_duckdb %>% 
  mutate(across(starts_with("note"), ~ as.numeric(.x)/20*100)) %>% 
  select(starts_with("note"))
# Sélectionner les colonnes dont les noms commencent par "note"
notes = [col for col in donnees_python.columns if col.startswith('note')]

# Transformer les colonnes sélectionnées
sur100 = donnees_python[notes] / 20 * 100

# Ajouter les nouvelles colonnes avec un suffixe "_100"
for col in notes:
    donnees_python[f"{col}_100"] = sur100[col]

7.9 Mettre un 0 devant un nombre

data Zero_devant;
  set donnees_sas (keep = date_entree);
  /* Obtenir le mois et la date */
  Mois = month(date_entree);
  Annee = year(date_entree);
  /* Mettre le mois sur 2 positions (avec un 0 devant si le mois <= 9) : format prédéfini z2. */
  Mois_a = put(Mois, z2.);
  drop Mois;
  rename Mois_a = Mois;
run;
donnees_rbase$mois <- sprintf("%02d", lubridate::month(donnees_rbase$date_entree))
donnees_tidyverse <- donnees_tidyverse %>% 
  mutate(mois = sprintf("%02d", lubridate::month(date_entree)))

# Autre solution
donnees_tidyverse <- donnees_tidyverse %>% 
  mutate(mois = lubridate::month(date_entree),
         mois = ifelse(str_length(mois) < 2, paste0("0", mois), mois))
# Une fonction month() est déjà implémentée en data.table, l'usage de lubridate est inutile
donnees_datatable[, mois := sprintf("%02d", data.table::month(date_entree))]
requete_duckdb %>% 
  mutate(mois = stringr::str_pad(as.character(month(date_entree)), width = 2L, pad = "0")) %>% 
  select(mois, date_entree)
# Extraire le mois et l'année
donnees_python['mois'] = donnees_python['date_entree'].dt.month
donnees_python['annee'] = donnees_python['date_entree'].dt.year

# Mettre le numéro du mois sur 2 positions (avec un 0 devant si le mois <= 9)
donnees_python['mois'] = donnees_python['mois'].fillna(0).astype(int).apply(lambda x: f"{x:02d}")

7.10 Arrondir une valeur numérique

data Arrondis;
  set donnees_sas (keep = poids_sondage);
  /* Arrondi à l'entier le plus proche */
  poids_arrondi_0 = round(poids_sondage);
  /* Arrondi à 1 chiffre après la virgule */
  poids_arrondi_1 = round(poids_sondage, 0.1);
  /* Arrondi à 2 chiffre après la virgule */
  poids_arrondi_2 = round(poids_sondage, 0.01);
  /* Arrondi à l'entier inférieur */
  poids_inf = floor(poids_sondage);
  /* Arrondi à l'entier supérieur */
  poids_sup = ceil(poids_sondage);  
run;
# Arrondi à l'entier le plus proche
poids_arrondi_0 <- round(donnees_rbase$poids_sondage, 0)
# Arrondi à 1 chiffre après la virgule
poids_arrondi_1 <- round(donnees_rbase$poids_sondage, 1)
# Arrondi à 2 chiffre après la virgule
poids_arrondi_2 <- round(donnees_rbase$poids_sondage, 2)
# Arrondi à l'entier inférieur
poids_inf <- floor(donnees_rbase$poids_sondage)
# Arrondi à l'entier supérieur
poids_sup <- ceiling(donnees_rbase$poids_sondage)
donnees_tidyverse <- donnees_tidyverse %>% 
  # Arrondi à l'entier le plus proche
  mutate(poids_arrondi_0 = round(poids_sondage, 0)) %>% 
  # Arrondi à 1 chiffre après la virgule
  mutate(poids_arrondi_1 = round(poids_sondage, 1)) %>% 
  # Arrondi à 2 chiffre après la virgule
  mutate(poids_arrondi_2 = round(poids_sondage, 2)) %>% 
  # Arrondi à l'entier inférieur
  mutate(poids_inf = floor(poids_sondage)) %>% 
  # Arrondi à l'entier supérieur
  mutate(poids_sup = ceiling(poids_sondage))
donnees_tidyverse %>% select(starts_with("poids"))
# Arrondi à l'entier le plus proche
donnees_datatable[, poids_arrondi_0 := round(poids_sondage, 0)]
# Arrondi à 1 chiffre après la virgule
donnees_datatable[, poids_arrondi_1 := round(poids_sondage, 1)]
# Arrondi à 2 chiffre après la virgule
donnees_datatable[, poids_arrondi_2 := round(poids_sondage, 2)]
# Arrondi à l'entier inférieur
donnees_datatable[, poids_inf := floor(poids_sondage)]
# Arrondi à l'entier supérieur
donnees_datatable[, poids_sup := ceiling(poids_sondage)]
requete_duckdb %>% 
  mutate( # la fonction round de duckdb ne prend pas l'argument digits, mais la traduction fonctionne
    poids_arrondi_0 = round(as.numeric(poids_sondage),  0),
    poids_arrondi_1 = round(as.numeric(poids_sondage),  1),
    poids_arrondi_2 = round(as.numeric(poids_sondage), -1),
    poids_floor     = floor(as.numeric(poids_sondage)    ),
    poids_ceiling   = ceiling(as.numeric(poids_sondage)  )
    ) %>% 
  select(starts_with("poids"))
# Arrondi à l'entier le plus proche
donnees_python['poids_arrondi_0'] = donnees_python['poids_sondage'].round(0)

# Arrondi à 1 chiffre après la virgule
donnees_python['poids_arrondi_1'] = donnees_python['poids_sondage'].round(1)

# Arrondi à 2 chiffres après la virgule
donnees_python['poids_arrondi_2'] = donnees_python['poids_sondage'].round(2)

# Arrondi à l'entier inférieur
donnees_python['poids_inf'] = np.floor(donnees_python['poids_sondage'])

# Arrondi à l'entier supérieur
donnees_python['poids_sup'] = np.ceil(donnees_python['poids_sondage'])

7.11 Corriger une valeur de la base

On souhaite corriger une valeur dans la base. La note_contenu de l’identifiant 168 est en fait 8 et non 18.

data donnees_sas_corr;
  set donnees_sas;
  if identifiant = "168" then note_contenu = 8;
run;
donnees_rbase_cor <- donnees_rbase
donnees_rbase_cor[donnees_rbase_cor$identifiant == "168", "note_contenu"] <- 8

https://dplyr.tidyverse.org/reference/rows.html Note : rows_update ne modifie pas l’objet.

donnees_tidyverse_cor %>% 
  rows_update(tibble(identifiant = "168", note_contenu = 8), by = "identifiant") # guillemets nécessaires

# Autre solution, qui n'est pas du pur Tidyverse
donnees_tidyverse_cor <- donnees_tidyverse
donnees_tidyverse_cor[donnees_tidyverse_cor$identifiant == "168", "note_contenu"] <- 8
donnees_datatable_cor <- copy(donnees_datatable)
donnees_datatable_cor[identifiant == "168", note_contenu := 8]

https://dplyr.tidyverse.org/reference/rows.html

C’est compliqué de modifier efficacement une valeur en duckDB.

# Exemple avec rows_update
con %>% duckdb::duckdb_register(name = "temp", df = tibble(identifiant = "168", note_contenu = 8), overwrite = TRUE)
requete_duckdb %>% 
  rows_update(con %>% tbl("temp"), by = "identifiant", unmatched = "ignore") %>% # guillemets nécessaires
  filter(identifiant == "168")

# Il vaut mieux écrire du SQL ou bien faire plusieurs modifications avec case_when
requete_duckdb %>% 
  mutate(note_contenu = case_when(
    identifiant == "168" ~ 8,
    .default = note_contenu)) %>% 
  filter(identifiant == "168")

Note : l’opération n’est pas persistante, i.e. l’objet requete_duckdb n’est pas modifié

donnees_python_cor = donnees_python.copy()
donnees_python_cor.loc[donnees_python_cor['identifiant'] == '168', 'note_contenu'] = 8

7.12 Sélectionner la première colonne non manquante d’une liste de colonnes (fonction coalesce())

On souhaite sélectionner la première note (parmi note_materiel, note_accompagnement, note_moyens, note_formateur et note_contenu) qui ne soit pas une valeur manquante (NA).

%let notes = note_materiel, note_accompagnement, note_moyens, note_formateur, note_contenu;

proc sql;
  create table Premiere_Note_Non_Manquante as
  select identifiant, coalesce(&notes.) as Premiere_Note_Non_Manquante
  from donnees_sas;
quit;
notes <- c("note_materiel", "note_accompagnement", "note_moyens", "note_formateur", "note_contenu")
premiere_note_non_manquante <- donnees_rbase
premiere_note_non_manquante$premiere_note_non_manquante <- apply(premiere_note_non_manquante[, notes], 
                                                                 1, 
                                                                 function(x) x[!is.na(x)][1])
premiere_note_non_manquante <- premiere_note_non_manquante[, c("identifiant", "premiere_note_non_manquante")]

# On peut aussi utiliser la fonction dplyr::coalesce()
notes <- c("note_materiel", "note_accompagnement", "note_moyens", "note_formateur", "note_contenu")
premiere_note_non_manquante <- donnees_rbase
premiere_note_non_manquante$premiere_note_non_manquante <- do.call(dplyr::coalesce, donnees_rbase[, notes])
premiere_note_non_manquante <- premiere_note_non_manquante[, c("identifiant", "premiere_note_non_manquante")]
# Première solution
premiere_note_non_manquante <- donnees_tidyverse %>% 
  mutate(premiere_note_non_manquante = coalesce(note_materiel, note_accompagnement, note_moyens, note_formateur, note_contenu)) %>% 
  select(identifiant, premiere_note_non_manquante)

# Ou alors, si les variables figurent dans un vecteur extérieur
notes <- c("note_materiel", "note_accompagnement", "note_moyens", "note_formateur", "note_contenu")
premiere_note_non_manquante <- donnees_tidyverse %>% 
  mutate(premiere_note_non_manquante = purrr::reduce(pick(all_of(notes)), coalesce)) %>% 
  select(identifiant, premiere_note_non_manquante)
notes <- c("note_materiel", "note_accompagnement", "note_moyens", "note_formateur", "note_contenu")
premiere_note_non_manquante <- donnees_datatable[, premiere_note_non_manquante := fcoalesce(.SD), .SDcols = notes][, .SD, .SDcols = c("identifiant", "premiere_note_non_manquante")]

7.13 Créer des colonnes à partir de chaînes de caractères

On souhaite placer chacun des mots d’une colonne de chaînes de caractère dans une colonne propre.

/* Exemple de base de données */
data Colonne_A_Decouper;
  infile cards dsd dlm='|';
  format Texte $36.;
  input Texte $;
  cards;
  Premier Deuxième
  Premier Deuxième Troisième Quatrieme
;
run;

/* On récupère le nombre maximal de mots */
proc sql noprint;select max(countw(Texte, ' ')) into :nbMots from Colonne_A_Decouper;quit;

/* On découpe la chaîne en autant de variables que de mots */
data Colonne_A_Decouper;
  set Colonne_A_Decouper;
  %macro it;
    %local i;
    %do i = 1 %to &nbMots.;
      Var&i = scan(Texte, &i., ' ');
    %end;
  %mend it;
  %it;
run;
colonne_a_decouper <- data.frame(texte = c("Premier Deuxième", "Premier Deuxième Troisième Quatrieme"))
motsDecoupes <- strsplit(colonne_a_decouper[, "texte"], " ")
nbMots <- sapply(motsDecoupes, length) |> max()
# Utiliser rbind créé du recycling. Pour maintenir des NA lorsque les variables n'existent pas, on doit utiliser cette astuce.
# Principe de fonctionnement : indexer un vecteur avec une valeur plus grande que sa taille retourne un NA à cet endroit
# Avec lapply(motsDecoupes, `[`, seq_len(nbMots)), on complète les lignes incomplètes avec des NA
colonne_a_decouper <- do.call(rbind, lapply(motsDecoupes, `[`, seq_len(nbMots))) |> as.data.frame()
colonne_a_decouper <- tibble(texte = c("Premier Deuxième", "Premier Deuxième Troisième Quatrieme"))
# Le problème avec la version tidyverse est que le nombre de variables créées n'est pas établi automatiquement. Il faut le calculer prélablement dans la variable nbMots
nbMots <- colonne_a_decouper %>%
  mutate(nbMots = stringr::str_split(texte, " ")) %>%
  pull(nbMots) %>% 
  purrr::map_int(function(x) length(x)) |> 
  max()
colonne_a_decouper <- colonne_a_decouper %>% tidyr::separate(col = texte, into = paste0("V", 1:nbMots), sep = " ")
colonne_a_decouper <- data.table(texte = c("Premier Deuxième", "Premier Deuxième Troisième Quatrieme"))
colonne_a_decouper <- colonne_a_decouper[, tstrsplit(texte, " ")]

8 Manipulation de dates

Pour en savoir plus sur le fonctionnement des dates en R : https://book.utilitr.org/03_Fiches_thematiques/Fiche_donnees_temporelles.html.

8.1 Créer une date à partir d’une chaîne de caractères

Créer le 31 décembre de l’année sous forme de date.

/* Pour créer une date avec l'année courante */
%let an = %sysfunc(year(%sysfunc(today())));
data donnees_sas;
  set donnees_sas;
  /* Deux manières de créer une date */
  format Decembre_31_&an._a Decembre_31_&an._b ddmmyy10.;
  Decembre_31_&an._a = "31dec&an."d;
  /* mdy pour month, day, year (pas d'autre alternative, ymd par exemple n'existe pas) */
  Decembre_31_&an._b = mdy(12, 31, &an.);
run;
# Pour créer une date avec l'année courante
annee <- format(Sys.Date(), "%Y")
as.Date(paste0(annee, "-12-31"), origin = "1970-01-01")
lubridate::ymd(paste0(annee, "-12-31"))
lubridate::dmy(paste0("31/12/", annee))
lubridate::mdy(paste0("12.31.", annee))
# Pour créer une date avec l'année courante
annee <- format(Sys.Date(), "%Y")
as.Date(paste0(annee, "-12-31"))
lubridate::ymd(paste0(annee, "-12-31"))
lubridate::dmy(paste0("31/12/", annee))
lubridate::mdy(paste0("12.31.", annee))
# Pour créer une date avec l'année courante
annee <- format(Sys.Date(), "%Y")
as.Date(paste0(annee, "-12-31"))
lubridate::ymd(paste0(annee, "-12-31"))
lubridate::dmy(paste0("31/12/", annee))
lubridate::mdy(paste0("12.31.", annee))
# Pour créer une date avec l'année courante
requete_duckdb %>% 
  mutate(exemple1 = as.Date("2024/07/14"),
         exemple2 = as.Date(strptime("01/01/2000", "%d/%m/%Y"))) %>% 
  # mutate(date_r = lubridate::dmy("01/01/2000")) %>% # no known SQL translation
  select(contains("exemple"))
# Pour créer une date avec l'année courante
annee = datetime.now().year

# Méthode 1 : Utiliser pandas pour créer une date
pd.to_datetime(f"{annee}-12-31")
pd.to_datetime(f"31/12/{annee}", dayfirst=True, format="%d/%m/%Y")
pd.to_datetime(f"12.31.{annee}", format="%m.%d.%Y")

# Méthode 2 : Utiliser datetime pour créer une date
datetime.strptime(f"{annee}-12-31", "%Y-%m-%d")

8.2 Calculer sur des dates

Attention, calculer sur des dates est un peu compliqué à cause de cas particuliers. Par exemple, le 29 février, les années bisextiles, le calcul des mois, des semaines, les fuseaux horaires, etc. Calculer en nombre de jours ou secondes ne pose pas de problème en général.

8.2.1 Écart entre deux dates

data donnees_sas;
  set donnees_sas;
  /* Durée (en année) entre 2 dates */
  /* Âge à l'entrée dans le dispositif */
  Age = intck('year', date_naissance, date_entree);
  /* En mois */
  Age_mois = intck('month', date_naissance, date_entree);
  /* En jours */
  Age_jours   = intck('days', date_naissance, date_entree);
  Age_jours_2 = date_entree - date_naissance;
run;
# Durée (en année) entre 2 dates
# Âge à l'entrée dans le dispositif
donnees_rbase$age <- floor(lubridate::time_length(difftime(donnees_rbase$date_entree, donnees_rbase$date_naissance), "years"))

# En mois
donnees_rbase$age_mois <- floor(lubridate::time_length(difftime(donnees_rbase$date_entree, donnees_rbase$date_naissance), "months"))

# En jours
donnees_rbase$age_jours   <- floor(lubridate::time_length(difftime(donnees_rbase$date_entree, donnees_rbase$date_naissance), "days"))
donnees_rbase$age_jours_2 <- donnees_rbase$date_entree - donnees_rbase$date_naissance
# Durée (en année) entre 2 dates
# Âge à l'entrée dans le dispositif
donnees_tidyverse <- donnees_tidyverse %>% 
  mutate(age = floor(lubridate::time_length(difftime(date_entree, date_naissance), "years")))

# En mois
donnees_tidyverse <- donnees_tidyverse %>% 
  mutate(age_mois = floor(lubridate::time_length(difftime(date_entree, date_naissance), "months")))

# En jours
donnees_tidyverse <- donnees_tidyverse %>% 
  mutate(age_jours = floor(lubridate::time_length(difftime(date_entree, date_naissance), "days")))
donnees_tidyverse <- donnees_tidyverse %>% 
  mutate(age_jours_2 = date_entree - date_naissance)
# Durée (en année) entre 2 dates
# Âge à l'entrée dans le dispositif
donnees_datatable[, age := floor(lubridate::time_length(difftime(date_entree, date_naissance), "years"))]

# En mois
donnees_datatable[, age_mois := floor(lubridate::time_length(difftime(date_entree, date_naissance), "months"))]

# En jours
donnees_datatable[, age_jours := floor(lubridate::time_length(difftime(date_entree, date_naissance), "days"))]
donnees_datatable[, age_jours_2 := date_entree - date_naissance]
# Durée entre deux dates
requete_duckdb %>% 
  mutate(duree_annees = year(age(date_entree,date_naissance)),
         duree_mois = month(age(date_entree,date_naissance)),
         ) %>% 
  select(contains("duree_"))
donnees_python['age_jours'] = (donnees_python['date_entree'] - donnees_python['date_naissance']).dt.days
# Remplacer les valeurs NaN par 0
donnees_python['age_jours'] = np.floor(donnees_python['age_jours'].fillna(0)).astype(int)

8.2.2 Ajouter une durée à une date

/* On utilise ici %sysevalf et non %eval pour des calculs avec des macro-variables non entières */
%let sixmois = %sysevalf(365.25/2);
%put sixmois : &sixmois.;
data donnees_sas;
  set donnees_sas;
  /* Date de sortie du dispositif : ajout de la durée à la date d'entrée */
  format date_sortie ddmmyy10.;
  date_sortie = intnx('day', date_entree, duree);
  
  /* Date 6 mois après la sortie */
  format Date_6mois ddmmyy10.;
  Date_6mois   = intnx('month', date_sortie, 6);
  
  /* Ajout de jours, cette fois */
  format Date_6mois_2 ddmmyy10.;
  Date_6mois_2 = intnx('days', date_sortie, &sixmois.);
run;
# Date de sortie du dispositif
donnees_rbase$date_sortie <- donnees_rbase$date_entree + lubridate::days(donnees_rbase$duree)

# Date 6 mois après la sortie
donnees_rbase$date_6mois   <- donnees_rbase$date_sortie %m+% months(6)
donnees_rbase$date_6mois   <- lubridate::add_with_rollback(donnees_rbase$date_sortie, months(6))
donnees_rbase$date_6mois_2 <- donnees_rbase$date_sortie + lubridate::days(round(365.25/2))
# Date de sortie du dispositif
donnees_tidyverse <- donnees_tidyverse %>% 
  mutate(date_sortie = date_entree + lubridate::days(duree))

# Date 6 mois après la sortie
donnees_tidyverse <- donnees_tidyverse %>% 
  mutate(date_6mois = date_sortie %m+% months(6))
donnees_tidyverse <- donnees_tidyverse %>% 
  mutate(date_6mois = lubridate::add_with_rollback(date_sortie, months(6)))
donnees_tidyverse <- donnees_tidyverse %>% 
  mutate(date_6mois_2 = date_sortie + lubridate::days(round(365.25/2)))
# Date de sortie du dispositif
donnees_datatable[, date_sortie := date_entree + lubridate::days(duree)]

# Date 6 mois après la sortie
donnees_datatable[, date_6mois   := date_sortie %m+% months(6)]
donnees_datatable[, date_6mois   := lubridate::add_with_rollback(date_sortie, months(6))]
donnees_datatable[, date_6mois_2 := date_sortie + lubridate::days(round(365.25/2))]
requete_duckdb %>% 
  mutate(date_sortie = date_entree + duree,
         date_6mois = date_sortie + to_months(6L)) %>% # préciser le type de 6
  select(date_sortie, date_6mois)
from datetime import timedelta
from dateutil.relativedelta import relativedelta

# Date de sortie du dispositif
donnees_python['date_sortie'] = donnees_python['date_entree'] + pd.to_timedelta(donnees_python['duree'], unit='D')

# Ajouter une colonne date_6mois qui est la date six mois après date_sortie
donnees_python['date_6mois'] = donnees_python['date_sortie'] + pd.DateOffset(months=6)

8.3 Formater les dates

/* On utilise ici %sysevalf et non %eval pour des calculs avec des macro-variables non entières */
%let sixmois = %sysevalf(365.25/2);
%put sixmois : &sixmois.;
data donnees_sas;
  set donnees_sas;
  
  /* Âge à l'entrée dans le dispositif */
  Age = intck('year', date_naissance, date_entree);
  
  /* Âge formaté */
  Agef = put(Age, agef.);
  
  /* Date de sortie du dispositif : ajout de la durée à la date d'entrée */
  format date_sortie ddmmyy10.;
  date_sortie = intnx('day', date_entree, duree);
  /* La durée du contrat est-elle inférieure à 6 mois ? */
  Duree_Inf_6_mois = (Duree < &sixmois. & Duree ne .);
  
  /* Deux manières de créer une date */
  format Decembre_31_&an._a Decembre_31_&an._b ddmmyy10.;
  Decembre_31_&an._a = "31dec&an."d;
  
  /* mdy pour month, day, year (pas d'autre alternative, ymd par exemple n'existe pas) */
  Decembre_31_&an._b = mdy(12, 31, &an.); 
  
  /* Date 6 mois après la sortie */
  format Date_6mois ddmmyy10.;
  Date_6mois = intnx('month', date_sortie, 6);
run;
/* Ventilation pondérée (cf. infra) */
proc freq data = donnees_sas;tables apres_31_decembre;weight poids_sondage;run;
# Âge à l'entrée dans le dispositif
donnees_rbase$age <- floor(lubridate::time_length(difftime(donnees_rbase$date_entree, donnees_rbase$date_naissance), "years"))

# Âge formaté
donnees_rbase$agef[donnees_rbase$age < 26]                           <- "1. De 15 à 25 ans"
# 26 <= donnees_rbase$age < 50 ne fonctionne pas, il faut passer en 2 étapes
donnees_rbase$agef[26 <= donnees_rbase$age & donnees_rbase$age < 50] <- "2. De 26 à 49 ans"
donnees_rbase$agef[donnees_rbase$age >= 50]                          <- "3. 50 ans ou plus"

# Autre solution
# L'option right = TRUE implique que les bornes sont ]0; 25] / ]25; 49] / ]49; Infini[
agef <- cut(donnees_rbase$age, 
            breaks = c(0, 25, 49, Inf),
            right = TRUE,
            labels = c("1. De 15 à 25 ans", "2. De 26 à 49 ans", "3. 50 ans ou plus"), 
            ordered_result = TRUE)

# Manipuler les dates
sixmois <- 365.25/2

# La durée du contrat est-elle inférieure à 6 mois ?
donnees_rbase$duree_inf_6_mois <- ifelse(donnees_rbase$duree < sixmois, 1, 0)

# Date de sortie du dispositif
donnees_rbase$date_sortie <- donnees_rbase$date_entree + lubridate::days(donnees_rbase$duree)

# Date 6 mois après la sortie
donnees_rbase$date_6mois <- donnees_rbase$date_sortie + lubridate::month(6)
# Âge à l'entrée dans le dispositif
donnees_tidyverse <- donnees_tidyverse %>% 
  mutate(age = as.period(interval(start = date_naissance, end = date_entree))$year)

# Âge formaté
donnees_tidyverse <- donnees_tidyverse %>%
  mutate(agef = case_when(
    age < 26             ~ "1. De 15 à 25 ans",
    age >= 26 & age < 50 ~ "2. De 26 à 49 ans",
    age >= 50            ~ "3. 50 ans ou plus")
    )

# Manipuler les dates
sixmois <- 365.25/2
# La durée du contrat est-elle inférieure à 6 mois ?
donnees_tidyverse <- donnees_tidyverse %>% 
  mutate(duree_inf_6_mois = case_when(duree <  sixmois ~ 1,
                                      duree >= sixmois ~ 0))
donnees_tidyverse %>% pull(duree_inf_6_mois) %>% table()

# Date de sortie du dispositif
donnees_tidyverse <- donnees_tidyverse %>% 
  mutate(date_sortie = date_entree + lubridate::days(duree))

# Date 6 mois après la sortie
donnees_tidyverse <- donnees_tidyverse %>% 
  mutate(date_6mois = date_sortie + lubridate::month(6))
# Âge à l'entrée dans le dispositif
donnees_datatable[, age := floor(lubridate::time_length(difftime(donnees_datatable$date_entree, donnees_datatable$date_naissance), "years"))]

# Âge formaté
donnees_datatable[, agef := fcase(age < 26,             "1. De 15 à 25 ans",
                                  26 <= age & age < 50, "2. De 26 à 49 ans",
                                  age >= 50,            "3. 50 ans ou plus")]

# Manipuler les dates
sixmois <- 365.25/2
# La durée du contrat est-elle inférieure à 6 mois ?
donnees_datatable[, duree_inf_6_mois := ifelse(duree >= sixmois, 1, 0)]
donnees_datatable[, duree_inf_6_mois := fifelse(duree >= sixmois, 1, 0)]
donnees_datatable[, duree_inf_6_mois := fcase(duree >= sixmois, 1,
                                              duree <  sixmois, 0)]
# Date de sortie du dispositif
donnees_datatable[, date_sortie := date_entree + lubridate::days(duree)]

# Date 6 mois après la sortie
donnees_datatable[, date_6mois := date_sortie + lubridate::month(6)]
# Création de la colonne age 
requete_duckdb %>% 
  mutate(age = year(age(date_entree,date_naissance))) %>% 
  select(age)

# Âge formaté
requete_duckdb %>%
  mutate(age = year(age(date_entree,date_naissance))) %>% 
  mutate(agef = case_when(
    age < 26 ~ "1. De 15 à 25 ans",
    age >= 26 | age < 50 ~  "2. De 26 à 49 ans",
    age >= 50 ~ "3. 50 ans ou plus")) %>% 
  select(age, agef)
from datetime import timedelta
from dateutil.relativedelta import relativedelta

# Calculer la durée en jours pour six mois
sixmois = 365.25 / 2

# La durée du contrat est-elle inférieure à 6 mois ?
donnees_python['duree_inf_6_mois'] = np.where(donnees_python['duree'] < sixmois, 1, 0)

# Créer une date spécifique (31 décembre de l'année en cours)
donnees_python['date_specifique'] = pd.to_datetime(donnees_python['date_entree'].dt.year.fillna(0).astype(int).astype(str) + "-12-31", format='%Y-%m-%d', errors='coerce')

9 Manipulation de chaînes de caractères

En R, la manipulation des chaînes de caractères passe par deux librairies principales, R base et stringr. Ces librairies sont transversales à dplyr / data.table / duckdb, on peut mélanger sans difficulté, et la séparation en onglets est un peu artificielle dans ce chapitre. Il reste préférable de s’accorder sur un style de programmation homogène. En duckdb, certaines fonctions ne sont pas disponibles, et nous proposons des alternatives.

Les fonctions de R base sont souvent mieux connues (notamment dans les tutoriels et cours de programmation). La librairie stringr est intéressante car les noms des fonctions sont plus simples et plus homogènes. Cette librairie est efficace, car implémentée au-dessus de stringi, librairie qui pourra être utile pour certains traitements complexes (l’inversion d’une chaîne, l’encodage des caractères, les accents par exemple).

Pour en savoir plus sur le fonctionnement des chaînes de caractères en R : https://book.utilitr.org/03_Fiches_thematiques/Fiche_donnees_textuelles.html.

9.1 Majuscule, minuscule

9.1.1 Majuscule

data donnees_sas;
  set donnees_sas;
  CSP_majuscule = upcase(CSPF);
run;
donnees_rbase$csp_maj <- toupper(donnees_rbase$cspf)
donnees_tidyverse <- donnees_tidyverse %>% 
  mutate(csp_maj = toupper(cspf))
donnees_datatable[, csp_maj := toupper(cspf)]
requete_duckdb %>% mutate(csp_maj = toupper(cspf)) %>% select(csp_maj)
donnees_python['csp_maj'] = donnees_python['cspf'].str.upper()

9.1.2 Minuscule

data donnees_sas;
  set donnees_sas;
  CSP_minuscule = lowcase(CSPF);
run;
donnees_rbase$csp_min <- tolower(donnees_rbase$cspf)
donnees_tidyverse <- donnees_tidyverse %>% 
  mutate(csp_maj = tolower(cspf))
# En minuscule
donnees_datatable[, csp_min := tolower(cspf)]
requete_duckdb %>% mutate(csp_maj = tolower(cspf)) %>% select(csp_maj)
donnees_python['csp_min'] = donnees_python['cspf'].str.lower()

9.1.3 Première lettre en majuscule

data donnees_sas;
  set donnees_sas;
  Niveau = propcase(Niveau);
run;
# 1ère lettre en majuscule, autres lettres en minuscule
donnees_rbase$niveau <- paste0(
  toupper(substr(donnees_rbase$niveau, 1, 1)),
  tolower(substr(donnees_rbase$niveau, 2, length(donnees_rbase$niveau)))
  )

# Autre solution
donnees_rbase$niveau <- tools::toTitleCase(donnees_rbase$niveau)
# 1ère lettre en majuscule, autres lettres en minuscule
donnees_tidyverse <- donnees_tidyverse %>%  
  mutate(niveau = str_to_title(niveau))
# 1ère lettre en majuscule, autres lettres en minuscule
donnees_datatable[, niveau := paste0(toupper(substr(niveau, 1, 1)), tolower(substr(niveau, 2, length(niveau))))]
requete_duckdb %>% 
  # mutate(csp_maj = str_to_title(cspf)) %>% # fonction non traduite
  mutate(
    l_niveau = as.integer(length(niveau)-1),
    niveau = paste0(toupper(substr(niveau, 1, 1)), tolower(right(niveau, l_niveau)))) %>% 
  # note : on utilise la fonction duckdb right car substr semble ne pas accepter un paramètre variable
  select(l_niveau, niveau)
donnees_python['niveau'] = donnees_python['cspf'].str.capitalize()

9.2 Nombre de caractères dans une chaîne de caractères

data donnees_sas;
  set donnees_sas;
  taille_id = length(identifiant);
run;
donnees_rbase$taille_id <- nchar(donnees_rbase$identifiant)
donnees_tidyverse <- donnees_tidyverse %>% 
  mutate(taille_id = nchar(identifiant))
donnees_tidyverse <- donnees_tidyverse %>% 
  mutate(taille_id = str_split(identifiant, '') %>% 
           lengths)
donnees_datatable[, taille_id := nchar(identifiant)]
requete_duckdb %>% mutate(taille_id = nchar(identifiant)) %>% select(taille_id) %>% print()
donnees_python['taille_id'] = donnees_python['identifiant'].str.len()

9.3 Remplacer une chaîne de caractères par une autre

On souhaite remplacer le mot qualifie par le mot Qualifié.

data A_Corriger;
  infile cards dsd dlm='|';
  format A_corriger $8.;
  input A_corriger $;
  cards;
  Qualifie
  qualifie
  Qualifie
  QUALIFIE
;
run;

data A_Corriger;
  set A_Corriger;
  Corrige = lowcase(A_corriger);
  Corrige = tranwrd(Corrige, "qualifie", "Qualifié");
run;
# Le mot Qualifié n'a pas d'accent : on le corrige
aCorriger <- c("Qualifie", "qualifie", "Qualifie", "QUALIFIE")

# [Q-q] permet de représenter Q ou q, et donc de prendre en compte Qualifie et qualifie
gsub("[Q-q]ualifie", "Qualifié", tolower(aCorriger))
# Le mot Qualifié n'a pas d'accent : on le corrige
aCorriger <- c("Qualifie", "qualifie", "Qualifie", "QUALIFIE")

# [Q-q] permet de représenter Q ou q, et donc de prendre en compte Qualifie et qualifie
aCorriger %>% tolower() %>% str_replace_all("[Q-q]ualifie", "Qualifié")
# Le mot Qualifié n'a pas d'accent : on le corrige
aCorriger <- c("Qualifie", "qualifie", "Qualifie", "QUALIFIE")

# [Q-q] permet de représenter Q ou q, et donc de prendre en compte Qualifie et qualifie
gsub("[Q-q]ualifie", "Qualifié", tolower(aCorriger))
requete_duckdb %>% mutate(niveau = stringr::str_replace_all(niveau, "[Q-q]ualifie", "Qualifié")) %>% select(niveau)
aCorriger = ["Qualifie", "qualifie", "Qualifie", "QUALIFIE"]
[re.sub(r'[qQ]ualifie', 'Qualifié', mot.lower()) for mot in aCorriger]

9.4 Extraire des éléments d’une chaîne de caractères

Le comportement de la fonction substr est différent entre SAS et R :

  • en SAS, dans substr(extrait, 2, 3), le 2 correspond à la position du 1er caractère à récupérer, le 3 au nombre total de caractères extrait à partir du 2e => Le résultat est xtr

  • en R, dans substr(“extrait”, 2, 3), le 2 correspond à la position du 1er caractère à récupérer, le 3 à la position du dernier caractère => Le résultat est “xt”.

data Exemple_texte;
  set Exemple_texte;
  
  /* Extraire les 2e, 3e et 4e caractère du mot extrait */
  /* Fonction tranwrd (TRANslate WoRD) */
  /* 2 correspond à la position du 1er caractère à récupérer, 3 le nombre total de caractères à partir du 2e */
  extrait = substr(extrait, 2, 3);
run;
proc print data = Exemple_texte;run;
# Extraire les 2e, 3e et 4e caractères de Concatener
# 2 correspond à la position du 1er caractère à récupérer, 5 la position du dernier caractère
extrait <- substr("extrait", 2, 5)
# Extraire les 2e, 3e et 4e caractères de Concatener
# 2 correspond à la position du 1er caractère à récupérer, 5 la position du dernier caractère
extrait <- str_sub("extrait", 2, 5)
# Extraire les 2e, 3e et 4e caractères de texte
# 2 correspond à la position du 1er caractère à récupérer, 5 la position du dernier caractère
extrait <- substr("extrait", 2, 5)
requete_duckdb %>% mutate(niveau = stringr::str_sub(niveau, 2, 5)) %>% select(niveau)
# La position 1 en Python correspond au 2eme élément
extrait = "extrait"[1:5]

9.5 Enlever les blancs superflus d’une chaîne de caractères

data Exemple_texte;
  Texte = "              Ce   Texte   mériterait   d être   corrigé                  ";
run;

data Exemple_texte;
  set Exemple_texte;
  /* Enlever les blancs au début et à la fin de la chaîne de caractère */
  Enlever_Blancs_Initiaux = strip(Texte);
  
  /* Enlever les doubles blancs dans la chaîne de caractères */
  Enlever_Blancs_Entre = compbl(Enlever_Blancs_Initiaux);
  
  /* Enlever doubles blancs */
  /* REVOIR !!!!! */
  Enlever_Doubles_Blancs = compress(Texte, "  ", "t");
run;
proc print data = Exemple_texte;run;
# Enlever les blancs au début et à la fin de la chaîne de caractère
texte  <- "              Ce   Texte   mériterait   d être   corrigé   "

# "\\s+" est une expression régulière indiquant 1 ou plusieurs espaces successifs
# Le gsub remplace 1 ou plusieurs espaces successifs par un seul espace
# trimws enlève les espaces au début et à la fin d'une chaîne de caractère 
texte <- gsub("\\s+", " ", trimws(texte))
# Enlever les blancs au début et à la fin de la chaîne de caractère
texte  <- "              Ce   Texte   mériterait   d être   corrigé  "

# str_squish() supprime les espaces blancs au début et à la fin, et remplace tous les espaces blancs internes par un seul espace
texte <- str_squish(texte)
# Enlever les blancs au début et à la fin de la chaîne de caractère
texte  <- "              Ce   Texte   mériterait   d être   corrigé  "

# "\\s+" est une expression régulière indiquant 1 ou plusieurs espaces successifs
# Le gsub remplace 1 ou plusieurs espaces successifs par un seul espace
# trimws enlève les espaces au début et à la fin d'une chaîne de caractère 
texte <- gsub("\\s+", " ", trimws(texte))
requete_duckdb %>% mutate(niveau = stringr::str_squish(niveau)) %>% select(niveau)
# Enlever les blancs au début et à la fin de la chaîne de caractère
texte = "              Ce   Texte   mériterait   d être   corrigé   "
texte = re.sub(r'\s+', ' ', texte).strip()

9.6 Concaténer des chaînes de caractères

data Exemple_texte;
  Texte1 = "Ce texte";
  Texte2 = "va être";
  Texte3 = "concaténé";
  Texte4 = "";
run;

data Exemple_texte;
  set Exemple_texte;
  
  /* Trois méthodes pour concaténer des chaînes de caractères */
  Concatener  = Texte1||" "||Texte2;
  Concatener2 = Texte1!!" "!!Texte2;
  Concatener3 = catx(" ", Texte1, Texte2);
  
  /* Effet des valeurs manquantes */
  /* Le séparateur est enlevé lors d'une concaténation avec une chaîne de caractère vide */
  Concatener4 = catx("-", Texte4, Texte3);
run;
proc print data = Exemple_texte;run;
# Concaténer des chaînes de caractères
texte1 <- "Ce texte"
texte2 <- "va être"
texte3 <- "concaténé"
texte4 <- ""

concatene <- paste(texte1, texte2, texte3, sep = " ")
paste0(texte1, " ", texte2, " ", texte3)

# Effet des valeurs manquantes : le délimiteur (ici -) apparaît avec la concaténation avec le caractère manquant
paste(texte4, texte3, sep = "-")
# Concaténer des chaînes de caractères
texte1 <- "Ce texte"
texte2 <- "va être"
texte3 <- "concaténé"
texte4 <- ""

concatene <- str_flatten(c(texte1, texte2, texte3), collapse = " ")

# Effet des valeurs manquantes : le délimiteur (ici -) apparaît avec la concaténation avec le caractère manquant
str_flatten(c(texte4, texte3), collapse = "-")
# Concaténer des chaînes de caractères
texte1 <- "Ce texte"
texte2 <- "va être"
texte3 <- "concaténé"
texte4 <- ""

concatene <- paste(texte1, texte2, texte3, sep = " ")
paste0(texte1, " ", texte2, " ", texte3)

# Effet des valeurs manquantes : le délimiteur (ici -) apparaît avec la concaténation avec le caractère manquant
paste(texte4, texte3, sep = "-")
requete_duckdb %>% mutate(niveau = paste0(niveau,niveau)) %>% select(niveau)
# Concaténer des chaînes de caractères
texte1 = "Ce texte"
texte2 = "va être"
texte3 = "concaténé"
texte4 = ""

concatene = ' '.join([texte1, texte2, texte3])

# Effet des valeurs manquantes : le délimiteur (ici -) apparaît avec la concaténation avec le caractère manquant
'-'.join([texte4, texte3])

9.7 Transformer plusieurs caractères différents

Supprimer les accents, cédilles, caractères spéciaux.

data Exemple_texte;
  set Exemple_texte;
  /* Transformer plusieurs caractères différents */
  /* On transforme le é en e, le â en a, le î en i, ... */
  texte = "éèêëàâçîô";
  texte_sans_accent = translate(texte, "eeeeaacio", "éèêëàâçîô");
run;
proc print data = Exemple_texte;run;
# Transformer plusieurs caractères différents
texte <- "éèêëàâçîô"
chartr("éèêëàâçîô", "eeeeaacio", texte)
# Transformer plusieurs caractères différents
texte <- "éèêëàâçîô"
chartr("éèêëàâçîô", "eeeeaacio", texte)
# Transformer plusieurs caractères différents
texte <- "éèêëàâçîô"
chartr("éèêëàâçîô", "eeeeaacio", texte)
requete_duckdb %>% mutate(niveau = strip_accents(niveau)) %>% select(niveau) # strip_accents est une fonction duckdb
# chartr n'est pas traduite en duckdb
texte = "éèêëàâçîô"
texte.replace("éèêëàâçîô", "eeeeaacio")

9.8 Découper une chaîne de caractères selon un caractère donné

Découper une phrase selon les espaces pour isoler les mots.

data Mots;
  delim = " ";
  Texte = "Mon texte va être coupé !";
  
  /* Chaque mot dans une variable */
  %macro Decouper;
    %do i = 1 %to %sysfunc(countw(Texte, delim));
      Mot&i. = scan(Texte, &i., delim);
    %end;
  %mend Decouper;
  %Decouper;
  
  /* Les mots empilés */
  nb_mots = countw(Texte, delim);
  do nb = 1 to nb_mots;
    mots = scan(Texte, nb, delim);
    output;
  end;
run;
proc print data = Mots;run;
texte  <- "Mon texte va être coupé !"
unlist(strsplit(texte, split = " "))
texte  <- "Mon texte va être coupé !"
str_split(texte, pattern = " ") %>% unlist()
texte  <- "Mon texte va être coupé !"
unlist(strsplit(texte, split = " "))
requete_duckdb %>% mutate(niveau = string_split(niveau, " ")) %>% select(niveau) # string_split est une fonction duckdb
# `str_split()` is not available in this SQL variant
# strsplit n'est pas disponible non plus

N.B. On obtient une seule colonne contenant des listes (de chaînes de caractères). DuckDB sait gérer des types complexes dans des cases, tout comme dplyr, mais c’est plus difficile à manipuler.

texte = "Mon texte va être coupé !"
texte.split()

9.9 Inverser une chaîne de caractères

data Mots;
  Texte = "Mon texte va être inversé !";
  x = left(reverse(Texte));
run;
proc print data = Mots;run;
texte <- "Mon texte va être inversé !"
inverserTexte <- function(x) {
  sapply(
    lapply(strsplit(x, NULL), rev),
    paste, collapse = "")
  }
inverserTexte(texte)
library(stringi)
texte <- "Mon texte va être inversé !"
stringi::stri_reverse(texte)
texte <- "Mon texte va être inversé !"
inverserTexte <- function(x) {
  sapply(
    lapply(strsplit(x, NULL), rev),
    paste, collapse = "")
}
inverserTexte(texte)
requete_duckdb %>% mutate(niveau = reverse(niveau)) %>% select(niveau) # reverse est une fonction duckdb
# stri_reverse : No known SQL translation
texte = "Mon texte va être inversé !"
texte[::-1]

10 Les valeurs manquantes

10.1 Repérer les valeurs manquantes (variables Age et Niveau)

Lignes où les variables Age ou Niveau sont manquantes.

data Manquant;
  set donnees_sas;
  /* 1ère solution */
  if missing(age) or missing(Niveau) then missing1 = 1;else missing1 = 0;
  /* 2e solution */
  if age = . or Niveau = '' then missing2 = 1;else missing2 = 0;
  keep Age Niveau Missing1 Missing2;
run;
# Mauvaise méthode pour repérer les valeurs manquantes
manquant <- donnees_rbase[donnees_rbase$age == NA | donnees_rbase$niveau == NA, ]

# Bonne méthode pour repérer les valeurs manquantes
manquant <- donnees_rbase[is.na(donnees_rbase$age) | is.na(donnees_rbase$niveau), ]
# Mauvaise méthode pour repérer les valeurs manquantes
manquant <- donnees_tidyverse %>%
  filter(age == NA | niveau == NA)

# Bonne méthode pour repérer les valeurs manquantes
manquant <- donnees_tidyverse %>%
  filter(is.na(age) | is.na(niveau))
# Mauvaise méthode pour repérer les valeurs manquantes
manquant <- donnees_datatable[age == NA | niveau == NA]

# Bonne méthode pour repérer les valeurs manquantes
manquant <- donnees_datatable[is.na(age)]
donnees_datatable[, manquant := fifelse(is.na(age) | is.na(niveau), 1, 0)]

10.2 Nombre et proportion de valeurs manquantes par variable

10.2.1 Pour l’ensemble des variables

/* Une solution possible */
%macro Iteration(base = donnees_sas);
  %local nbVar;
  proc contents data = donnees_sas out = ListeVar noprint;run;
  proc sql noprint;select count(*) into :nbVar from ListeVar;quit;
  
  %do i = 1 %to &nbVar.;
    data _null_;
      set ListeVar (firstobs = &i. obs = &i.);
      call symput('var', name);
    run;
    proc sql;
      select max("&var.") as Variable, sum(missing(&var.)) as Manquants, sum(missing(&var.)) / count(*) * 100 as Prop_Manquants
      from &base.;
    quit;
  %end;
  
  proc datasets lib = work nolist;delete ListeVar;run;
%mend Iteration;

%Iteration;
# Nombre de valeurs manquantes
colSums(is.na(donnees_rbase))
apply(is.na(donnees_rbase), 2, sum)

# Proportion de valeurs manquantes
colMeans(is.na(donnees_rbase)) * 100
apply(is.na(donnees_rbase), 2, mean) * 100
# Nombre et proportion de valeurs manquantes
donnees_tidyverse %>%
  reframe(across(everything(), ~c( sum(is.na(.x)), mean(is.na(.x) * 100)) ))

# Proportion de valeurs manquantes
donnees_tidyverse %>%
  reframe(across(everything(), ~c( sum(is.na(.x)), mean(is.na(.x) * 100)) ))

# Autres solutions
donnees_tidyverse %>% map(~c( sum(is.na(.x)), mean(is.na(.x) * 100)))
# Obsolète
donnees_tidyverse %>% summarise_each(funs(mean(is.na(.)) * 100))
# Nombre et proportion de valeurs manquantes
donnees_datatable[, lapply(.SD, function(x) c(sum(is.na(x)), mean(is.na(x)) * 100))]

10.2.2 Pour les variables numériques ou dates

/* Partie "Missing Values" en bas du tableau consacré à la variable */
proc univariate data = donnees_sas;var _numeric_;run;
apply(is.na(
  donnees_rbase[sapply(donnees_rbase, function(x) is.numeric(x) | lubridate::is.Date(x))]
  ), 
  2, 
  function(x) c( sum(x), mean(x) * 100 ) )

# Autres solutions
sapply(
  donnees_rbase[sapply(donnees_rbase, function(x) is.numeric(x) | lubridate::is.Date(x))],
  function(x) c( sum(is.na(x)), mean(is.na(x)) * 100 ) )
sapply(
  donnees_rbase[sapply(donnees_rbase, function(x) is.numeric(x) | lubridate::is.Date(x))],
  function(x) c (sum(is.na(x)), sum(is.na(x)) / length(x) * 100) )
donnees_tidyverse %>%
  summarise(across(where(~ is.numeric(.x) | lubridate::is.Date(.x)),
                   c(~sum(is.na(.x)), ~mean(is.na(.x)))))
donnees_tidyverse %>%
  summarise(across(where(~ is.numeric(.x) | lubridate::is.Date(.x)),
                   list(~sum(is.na(.x)), ~sum(is.na(.x)) / length(.x))))
donnees_datatable[, lapply(.SD, function(x) mean(is.na(x)) * 100),
                  .SDcols = function(x) c(lubridate::is.Date(x) | is.numeric(x))]

10.3 Incidence des valeurs manquantes

/* En SAS, les valeurs manquantes sont des nombres négatifs faibles */
data Valeur_Manquante;
  set donnees_sas;
  /* Lorsque Age est manquant (missing), Jeune_Correct vaut 0 mais Jeune_Incorrect vaut 1 */
  /* En effet, pour SAS, un Age manquant est une valeur inférieure à 0, donc bien inférieure à 25.
     Donc la variable Jeune_Incorrect vaut bien 1 pour les âges inconnus */
  Jeune_Incorrect = (Age <= 25);
  Jeune_Correct   = (0 <= Age <= 25);
run;

/* On affiche les résultats */
proc print data = Valeur_Manquante (keep  = Age Jeune_Correct Jeune_Incorrect
                                    where = (missing(Age)));
run;
proc freq data = Valeur_Manquante;tables Jeune_Incorrect Jeune_Correct;run;
# Une somme avec NA donne NA en résultat
mean(donnees_rbase$note_formateur)
# Sauf avec l'option na.rm = TRUE
mean(donnees_rbase$note_formateur, na.rm = TRUE)
# Une somme avec NA donne NA en résultat
donnees_tidyverse %>% pull(note_formateur) %>% mean()
# Sauf avec l'option na.rm = TRUE
donnees_tidyverse %>% pull(note_formateur) %>% mean(na.rm = TRUE)

# Attention, en tidyverse, les syntaxes suivantes ne fonctionnent pas !
# donnees_tidyverse %>% mean(note_formateur)
# donnees_tidyverse %>% mean(note_formateur, na.rm = TRUE)
# Une somme avec NA donne NA en résultat
donnees_datatable[, mean(note_formateur)]
# Sauf avec l'option na.rm = TRUE
donnees_datatable[, mean(note_formateur, na.rm = TRUE)]

10.4 Remplacer les valeurs manquantes d’une seule variable par 0

%let var = note_contenu;
data donnees_sas_sans_missing;
  set donnees_sas;
  if missing(&var.) then &var. = 0;
  /* Ou alors */
  if &var. = . then &var. = 0;
  /* Ou encore */
  if note_contenu = . then note_contenu = 0;
run;
variable <- "note_contenu"
donnees_rbase_sans_na <- donnees_rbase
donnees_rbase_sans_na[is.na(donnees_rbase_sans_na[, variable]), variable] <- 0

# Autres solutions
donnees_rbase_sans_na[, variable][is.na(donnees_rbase_sans_na[, variable])] <- 0
donnees_rbase_sans_na[, variable] <- replace(donnees_rbase_sans_na[, variable],
                                             is.na(donnees_rbase_sans_na[, variable]), 0)

# Ou alors
donnees_rbase_sans_na <- donnees_rbase
donnees_rbase_sans_na$note_contenu[is.na(donnees_rbase_sans_na$note_contenu)] <- 0
variable <- "note_contenu"
donnees_tidyverse_sans_na <- donnees_tidyverse %>% 
  mutate(across(variable,  ~tidyr::replace_na(.x, 0)))

# Ou alors
donnees_tidyverse_sans_na <- donnees_tidyverse %>% 
  mutate(note_contenu = tidyr::replace_na(note_contenu, 0))
variable <- "note_contenu"
donnees_datatable[, replace(.SD, is.na(.SD), 0), .SDcols = variable]
donnees_datatable[, lapply(.SD, function(x) fifelse(is.na(x), 0, x)), .SDcols = variable]
donnees_datatable[, lapply(.SD, \(x) fifelse(is.na(x), 0, x)), .SDcols = variable]

# Ou alors
donnees_datatable[, replace(.SD, is.na(.SD), 0), .SDcols = "note_contenu"]

10.5 Remplacer toutes les valeurs numériques manquantes par 0

/* On sélectionne toutes les variables numériques */
proc contents data = donnees_sas out = Var noprint;run;
proc sql noprint;
  select name into :nom_col separated by " " from Var where format = "";
run;

data donnees_sas_sans_missing;
  set donnees_sas;
  
  %macro Missing;
    %local i var;
    %do i = 1 %to %sysfunc(countw(&nom_col.));
      %let var = %scan(&nom_col., &i);
      if missing(&var.) then &var. = 0;
    %end;
  %mend Missing;
  %Missing;
  
run;
proc datasets lib = Work nolist;delete Var;run;
# Dans le cas des dates, la valeur manquante a été remplacée par 1970-01-01
donnees_rbase_sans_na <- donnees_rbase
donnees_rbase_sans_na[is.na(donnees_rbase_sans_na)] <- 0

# On remplace seulement les valeurs numériques par 0
donnees_rbase_sans_na <- donnees_rbase
varNumeriques <- sapply(donnees_rbase, is.numeric)
donnees_rbase_sans_na[, varNumeriques][is.na(donnees_rbase_sans_na[, varNumeriques])] <- 0

# Autre solution, avec replace
donnees_rbase_sans_na[, varNumeriques] <- lapply(donnees_rbase_sans_na[, varNumeriques],
                                                 function(x) {replace(x, is.na(x), 0)})
# On remplace seulement les valeurs numériques par 0
donnees_tidyverse_sans_na <- donnees_tidyverse %>% 
  mutate(across(where(is.numeric), ~tidyr::replace_na(.x, 0)))

# Autres façons d'écrire les fonctions anonymes
# La méthode complète
donnees_tidyverse_sans_na <- donnees_tidyverse %>% 
  mutate(across(where(is.numeric), function(x) tidyr::replace_na(x, 0)))
# Une autre façon de raccourcir (depuis R 4.1)
# \(x) est un raccourci pour function(x)
donnees_tidyverse_sans_na <- donnees_tidyverse %>% 
  mutate(across(where(is.numeric), \(x) tidyr::replace_na(x, 0)))
# Autre solution
donnees_tidyverse_sans_na <- donnees_tidyverse %>%
  purrr::modify_if(is.numeric, ~tidyr::replace_na(.x, 0))
donnees_datatable_sans_na <- copy(donnees_datatable)
setnafill(donnees_datatable[, .SD, .SDcols = is.numeric], fill = 0)

# Autre solution
donnees_datatable_sans_na <- copy(donnees_datatable)
cols <- colnames(donnees_datatable_sans_na[, .SD, .SDcols = is.numeric])
donnees_datatable_sans_na[, (cols) := lapply(.SD, function(x) fifelse(is.na(x), 0, x)), .SDcols = cols]

# Ensemble des colonnes
donnees_datatable_sans_na <- copy(donnees_datatable)
donnees_datatable_sans_na[is.na(donnees_datatable_sans_na)] <- 0

10.6 Supprimer les lignes où une certaine variable est manquante

On souhaite supprimer toutes les lignes où la variable age est manquante.

data age_non_manquant;
  set donnees_sas (where = (age ne .));
  /* Ou alors */
  if age ne .;
run;
age_non_manquant <- donnees_rbase[complete.cases(donnees_rbase[, "age"]), ]
age_non_manquant <- donnees_rbase[! is.na(donnees_rbase[, "age"]), ]
age_non_manquant <- donnees_tidyverse %>% drop_na(age)
age_non_manquant <- donnees_tidyverse %>% filter(!is.na(age))
age_non_manquant <- na.omit(donnees_datatable, cols = c("age"))
age_non_manquant <- donnees_datatable[! is.na(age), ]

10.7 Supprimer les lignes où au moins une variable de la base est manquante

On souhaite supprimer toutes les lignes où au moins une variable de la base est manquante.

data non_manquant;
  set donnees_sas;
  if cmiss(of _all_) then delete;
run;
non_manquant <- donnees_rbase[complete.cases(donnees_rbase), ]
non_manquant <- donnees_tidyverse %>% drop_na()
non_manquant <- na.omit(donnees_datatable)

11 Les tris

11.1 Trier les colonnes de la base

11.1.1 Mettre identifiant et date_entree au début de la base

%let colTri = identifiant date_entree;
data donnees_sas;
  retain &colTri.;
  set donnees_sas;
run;

/* Autre solution */
proc sql;
  create table donnees_sas as
  /* Dans la proc SQL, les variables doivent être séparées par des virgules */
  /* On remplace les blancs entre les mots par des virgules pour la proc SQL */
  select %sysfunc(tranwrd(&colTri., %str( ), %str(, ))), * from donnees_sas;
quit;
colTri <- c("identifiant", "date_entree")
donnees_rbase <- donnees_rbase[, union(colTri, colnames(donnees_rbase))]

# Autres possibilités, plus longues !
donnees_rbase <- donnees_rbase[, c(colTri, setdiff(colnames(donnees_rbase), colTri))]
donnees_rbase <- donnees_rbase[, c(colTri, colnames(donnees_rbase)[! colnames(donnees_rbase) %in% colTri])]
donnees_tidyverse <- donnees_tidyverse %>%
  relocate(identifiant, date_entree)

# Autres solutions
colTri <- c("identifiant", "date_entree")
donnees_tidyverse <- donnees_tidyverse %>%
  relocate(all_of(colTri))
donnees_tidyverse_tri <- donnees_tidyverse %>% 
  select(all_of(colTri), everything())
colTri <- c("identifiant", "date_entree")
tri <- union(colTri, colnames(donnees_datatable))
donnees_datatable <- donnees_datatable[, ..tri]

# Autre solution, à privilégier
# En data.table, les instructions débutant par set modifient les éléments par référence, c'est-à-dire sans copie. Ceci est plus efficace pour manipuler des données volumineuses.
setcolorder(donnees_datatable, colTri)
requete_duckdb %>% 
  mutate_at(enDate, ~ as.Date(strptime(.,'%d/%m/%Y'))) %>% # strptime est une fonction duckdb
  select(identifiant, date_entree, everything())

requete_duckdb %>% 
  mutate_at(enDate, ~ as.Date(strptime(.,'%d/%m/%Y'))) %>% # strptime est une fonction duckdb
  relocate(identifiant, date_entree)
colTri = ["identifiant", "date_entree"]

cols = colTri + [col for col in donnees_python.columns if col not in colTri]
donnees_python = donnees_python[cols]

11.1.2 Mettre la variable poids_sondage au début de la base

data donnees_sas;
  retain poids_sondage;
  set donnees_sas;
run;
donnees_rbase[, union("poids_sondage", colnames(donnees_rbase))]
donnees_tidyverse <- donnees_tidyverse %>%
  relocate(poids_sondage)
setcolorder(donnees_datatable, "poids_sondage")
cols = ['poids_sondage'] + [col for col in donnees_python.columns if col != 'poids_sondage']
donnees_python = donnees_python[cols]

11.1.3 Mettre la variable poids_sondage après la variable date_naissance

proc contents data = donnees_sas out = var;run;

proc sql noprint;
  select name into :var separated by " "
  from var
  where varnum <= (select varnum from var where lowcase(name) = "date_naissance")
  order by varnum;
quit;

data donnees_sas;
  retain &var. poids_sondage;
  set donnees_sas;
run;
varAvant <- c( colnames(donnees_rbase)[1 : which("date_naissance" == colnames(donnees_rbase))], "poids_sondage" )
donnees_rbase <- donnees_rbase[, c(varAvant, setdiff(colnames(donnees_rbase), varAvant))]
donnees_tidyverse <- donnees_tidyverse %>%
  relocate(poids_sondage, .after = date_naissance)
setcolorder(donnees_datatable, "poids_sondage", after = "date_naissance")
# Trouver l'index de la colonne 'date_naissance'
date_naissance_index = donnees_python.columns.get_loc('date_naissance')

# Sélectionner toutes les colonnes jusqu'à 'date_naissance' inclus
varAvant = list(donnees_python.columns[:date_naissance_index + 1]) + ['poids_sondage']

# Réorganiser les colonnes du DataFrame
donnees_python = donnees_python[varAvant + [col for col in donnees_python.columns if col not in varAvant]]

11.1.4 Mettre la variable poids_sondage à la fin de la base

proc contents data = donnees_sas out = var;run;
proc sql noprint;
  select name into :var separated by " " from var
  where lowcase(name) ne "poids_sondage" order by varnum;
quit;
data donnees_sas;
  retain &var. poids_sondage;
  set donnees_sas;
run;
donnees_rbase <- donnees_rbase[, c(setdiff(colnames(donnees_rbase), "poids_sondage"), "poids_sondage")]
donnees_tidyverse <- donnees_tidyverse %>%
  relocate(poids_sondage, .after = last_col())
setcolorder(donnees_datatable, c(setdiff(colnames(donnees_datatable), "poids_sondage"), "poids_sondage"))
cols = [col for col in donnees_python.columns if col != 'poids_sondage'] + ['poids_sondage']
donnees_python = donnees_python[cols]

11.2 Trier les lignes de la base

11.2.1 Tri par ordre croissant d’identifiant et date_entree

/* 1ère possibilité */
proc sort data = donnees_sas;by Identifiant Date_entree;run;

/* 2e possibilité */
proc sql;
  create table donnees_sas as select * from donnees_sas
  order by Identifiant, Date_entree;
quit;
# Tri par ordre croissant
# L'option na.last = FALSE (resp. TRUE) indique que les valeurs manquantes doivent figurer à la fin (resp. au début) du tri, que le tri soit croissant ou décroissant
donnees_rbase <- donnees_rbase[order(donnees_rbase$identifiant, donnees_rbase$date_entree, na.last = FALSE), ]
# Tri par ordre croissant
donnees_tidyverse <- donnees_tidyverse %>% 
  arrange(identifiant, date_entree)
# Tri par ordre croissant
# L'option na.last = FALSE (resp. TRUE) indique que les valeurs manquantes doivent figurer à la fin (resp. au début) du tri, que le tri soit croissant ou décroissant
donnees_datatable <- donnees_datatable[order(identifiant, date_entree, na.last = FALSE)]

# En data.table, les instructions débutant par set modifient les éléments par référence, c'est-à-dire sans copie.
# Ceci est plus efficace pour manipuler des données volumineuses.
setorder(donnees_datatable, "identifiant", "date_entree", na.last = FALSE)
setorder(donnees_datatable, identifiant, date_entree, na.last = FALSE)
setorderv(donnees_datatable, cols = c("identifiant", "date_entree"), order = c(1L, 1L), na.last = FALSE)
# Mettre les na en premier
donnees_python = donnees_python.sort_values(by=['identifiant', 'date_entree'], na_position='first')

11.2.2 Tri par ordre décroissant

/* Idem par ordre croissant d'identifiant et ordre décroissant de date d'entrée */

/* 1ère possibilité */
proc sort data = donnees_sas;by Identifiant descending Date_entree;run;

/* 2e possibilité */
proc sql;
  create table donnees_sas as select * from donnees_sas
  order by Identifiant, Date_entree desc;
quit;
# Tri par ordre croissant de identifiant et décroissant de date_entree
donnees_rbase <- donnees_rbase[
  order(donnees_rbase$identifiant, donnees_rbase$date_entree, 
        na.last = FALSE, 
        decreasing = c(FALSE, TRUE), 
        method = "radix"
        )
  , ]

# Autre possibilité : - devant la variable (uniquement pour les variables numériques)
donnees_rbase <- donnees_rbase[
  order(donnees_rbase$identifiant, -donnees_rbase$duree, 
        na.last = FALSE)
  , ]
# Tri par ordre croissant de identifiant et décroissant de date_entree
donnees_tidyverse <- donnees_tidyverse %>% 
  arrange(identifiant, desc(date_entree))
# Tri par ordre croissant de identifiant et décroissant de date_entree (- avant le nom de la variable)
donnees_datatable <- donnees_datatable[order(identifiant, -date_entree, na.last = FALSE)]
setorder(donnees_datatable, "identifiant", -"date_entree", na.last = FALSE)
setorder(donnees_datatable, identifiant, -date_entree, na.last = FALSE)
setorderv(donnees_datatable, cols = c("identifiant", "date_entree"), order = c(1L, -1L), na.last = FALSE)
donnees_python = donnees_python.sort_values(by=['identifiant', 'date_entree'], na_position='first', ascending=[True, False])

11.3 Incidence des valeurs manquantes dans les tris

/* Dans SAS, les valeurs manquantes sont considérées comme des valeurs négatives */

/* Elles sont donc situées en premier dans un tri par ordre croissant ... */
proc sort data = donnees_sas;by identifiant date_entree;run;proc print;run;

/* ... et en dernier dans un tri par ordre décroissant */
proc sort data = donnees_sas;by identifiant descending date_entree;run;
proc print;run;
# Les valeurs manquantes sont situées en dernier dans un tri par ordre croissant ou décroissant (car par défaut l'option na.last = TRUE) ...
donnees_rbase <- donnees_rbase[order(donnees_rbase$identifiant, donnees_rbase$date_entree), ]

# SAS considère les valeurs manquantes comme des nombres négatifs faibles.
# Pour mimer le tri par ordre croissant en SAS :
donnees_rbase <- donnees_rbase[order(donnees_rbase$identifiant, donnees_rbase$date_entree, na.last = FALSE), ]

# Pour mimer le tri par ordre décroissant en SAS :
donnees_rbase <- donnees_rbase[order(donnees_rbase$identifiant, is.na(donnees_rbase$date_entree), donnees_rbase$date_entree,
                                     na.last = FALSE,
                                     decreasing = c(FALSE, FALSE, TRUE),
                                     method = "radix"), ]
# Attention, avec arrange, les variables manquantes (NA) sont toujours classées en dernier, même avec desc()
donnees_tidyverse <- donnees_tidyverse %>% 
  arrange(identifiant, date_entree)
donnees_tidyverse <- donnees_tidyverse %>% 
  arrange(identifiant, desc(date_entree))

# Or, SAS considère les valeurs manquantes comme des nombres négatifs faibles.
# Elles sont donc classées en premier dans un tri par ordre croissant, et en dernier dans un tri par ordre décroissant

# Pour mimer le tri par ordre croissant en SAS : les valeurs manquantes de date_entree sont classées en premier
donnees_tidyverse <- donnees_tidyverse %>% 
  arrange(identifiant, !is.na(date_entree), date_entree)

# Pour mimer le tri par ordre décroissant en SAS
donnees_tidyverse <- donnees_tidyverse %>% 
  arrange(identifiant, desc(date_entree))
# Les valeurs manquantes sont situées en dernier dans un tri par ordre croissant ou décroissant (car par défaut l'option na.last = TRUE) ...
donnees_datatable <- donnees_datatable[order(identifiant, date_entree)]

# SAS considère les valeurs manquantes comme des nombres négatifs faibles.
# Pour mimer le tri par ordre croissant en SAS :
setorderv(donnees_datatable, cols = c("identifiant", "date_entree"), order = c(1L, 1L), na.last = FALSE)

# Pour mimer le tri par ordre décroissant en SAS :
donnees_datatable[, date_entree_na := is.na(date_entree)]
setorderv(donnees_datatable, cols = c("identifiant", "date_entree_na", "date_entree"), order = c(1L, 1L, -1L), na.last = FALSE)
donnees_datatable[, date_entree_na := NULL]
# Les valeurs manquantes sont situées en dernier dans un tri par ordre croissant ou décroissant (car par défaut l'option na.last = TRUE) ...
requete_duckdb %>% 
  arrange(Identifiant, Note_Contenu) %>% 
  select(Identifiant, Note_Contenu)
  
# Pour mimer le tri par ordre croissant en SAS :
# Note : il faut faire select d'abord, sinon il y a une erreur quand "! is.na()" est dans la liste des colonnes
requete_duckdb %>% 
  select(Identifiant, Note_Contenu) %>% 
  arrange(Identifiant, ! is.na(Note_Contenu), Note_Contenu)

# Pour mimer le tri par ordre décroissant en SAS :
# Note : il faut faire select d'abord, sinon il y a une erreur quand "! is.na()" est dans la liste des colonnes
requete_duckdb %>% 
  select(Identifiant, Note_Contenu) %>% 
  arrange(Identifiant, is.na(Note_Contenu), Note_Contenu)
# Les valeurs manquantes sont situées en dernier dans un tri par ordre croissant ou décroissant
donnees_python = donnees_python.sort_values(by=['identifiant', 'date_entree'])

# SAS considère les valeurs manquantes comme des nombres négatifs faibles.
# Pour mimer le tri par ordre croissant en SAS : ajouter l'option na_position = 'first'
donnees_python = donnees_python.sort_values(by=['identifiant', 'date_entree'], na_position='first')

# Pour mimer le tri par ordre décroissant en SAS :
donnees_python = donnees_python.sort_values(by=['identifiant', 'date_entree'], ascending=[True, False])

11.4 Trier par ordre croissant de toutes les variables de la base

proc sort data = donnees_sas;by _all_;run;
tri_toutes_variables <- donnees_rbase[order(colnames(donnees_rbase), na.last = FALSE)]
tri_toutes_variables <- donnees_tidyverse %>% 
  arrange(pick(everything()))
tri_toutes_variables <- donnees_tidyverse %>% 
  arrange(across(everything()))
tri_toutes_variables <- setorderv(donnees_datatable, na.last = FALSE)
donnees_python = donnees_python.sort_values(by=list(donnees_python.columns), na_position='first')

12 Les doublons

12.1 Doublons pour toutes les colonnes

/* On extrait seulement les doublons, pas la première occurrence */

/* On récupère déjà la dernière variable de la base (on en aura besoin plus loin) */
proc contents data = donnees_sas out = Var noprint;run;
proc sql noprint;
  select name into :derniere_var
  from Var
  where varnum = (select max(varnum) from Var);
quit;
proc sort data = donnees_sas;by &nom_col.;run;
data Doublons;
  set donnees_sas;
  by &nom_col.;
  if not (first.&derniere_var. and last.&derniere_var.);
run;
# On extrait seulement les doublons, pas la première occurrence
doublons <- donnees_rbase[duplicated(donnees_rbase), ]
# On extrait seulement les doublons, pas la première occurrence
donnees_tidyverse %>% 
  group_by(across(everything())) %>% 
  filter(n() > 1) %>% 
  slice(-1) %>% 
  ungroup()

# Autre solution
doublons <- donnees_tidyverse %>%  
  group_by_all() %>% 
  filter(n() > 1) %>%
  slice(-1) %>%
  ungroup()
# On extrait seulement les doublons, pas la première occurrence
doublons <- donnees_datatable[duplicated(donnees_datatable), ]
# On extrait seulement les doublons, pas la première occurrence
doublons = donnees_python[donnees_python.duplicated()]

12.2 Doublons pour une ou plusieurs colonnes

/* On extrait seulement les doublons, pas la première occurrence */
%let var = identifiant;
proc sort data = donnees_sas;by &var.;run;
data doublons;
  set donnees_sas;
  by &var.;
  if not first.&var.;
run;

/* À FAIRE : nodupkey ??? */
# On extrait seulement les doublons, pas la première occurrence
variable <- "identifiant"
doublons <- donnees_rbase[duplicated(donnees_rbase[, variable]), ]
# On extrait seulement les doublons, pas la première occurrence
variable <- "identifiant"
doublons <- donnees_tidyverse %>%  
  group_by(across(variable)) %>% 
  filter(n() > 1) %>%
  slice(-1) %>%
  ungroup()
# On extrait seulement les doublons, pas la première occurrence
variable <- "identifiant"
doublons <- donnees_datatable[duplicated(donnees_datatable[, ..variable]), ]
# On extrait seulement les doublons, pas la première occurrence
variable = "identifiant"
doublons = donnees_python[donnees_python[variable].duplicated()]

12.3 Récupérer toutes les lignes pour les identifiants en doublon

%let var = identifiant;
/* On groupe par la colonne identifiant, et si on aboutit à strictement plus d'une ligne, c'est un doublon */
proc sql;
  create table enDouble as
  select * from donnees_sas
  group by &var.
  having count(*) > 1;
quit;
variable <- "identifiant"
enDouble <- donnees_rbase[donnees_rbase[, variable] %in% 
                            donnees_rbase[duplicated(donnees_rbase[, variable]), variable]]
variable <- "identifiant"
enDouble <- donnees_tidyverse %>%  
  group_by(across(variable)) %>% 
  filter(n() > 1) %>%
  ungroup()
variable <- "identifiant"
enDouble <- donnees_datatable[donnees_datatable[[variable]] %chin%
                                donnees_datatable[[variable]][duplicated(donnees_datatable[[variable]])], ]
variable = 'identifiant'

# Identifier les valeurs dupliquées
doublons_values = donnees_python[variable][donnees_python[variable].duplicated()]

# Filtrer les lignes qui contiennent ces valeurs dupliquées
enDouble = donnees_python[donnees_python[variable].isin(doublons_values)]

12.4 Récupérer toutes les lignes pour les identifiants sans doublon

%let var = identifiant;
proc sql;
  create table sansDouble as
  select * from donnees_sas
  group by &var.
  having count(*) = 1;
quit;
variable <- "identifiant"
sansDouble <- donnees_rbase[! donnees_rbase[, variable] %in%
                              donnees_rbase[duplicated(donnees_rbase[, variable]), variable]]
variable <- "identifiant"
sansDouble <- donnees_tidyverse %>%  
  group_by(across(variable)) %>% 
  filter(n() == 1) %>%
  ungroup()
variable <- "identifiant"
sansDouble <- donnees_datatable[! donnees_datatable[[variable]] %chin%
                                  donnees_datatable[[variable]][duplicated(donnees_datatable[[variable]])], ]
sansDouble <- donnees_datatable[donnees_datatable[[variable]] %notin%
                                donnees_datatable[[variable]][duplicated(donnees_datatable[[variable]])], ]
variable = 'identifiant'

# Identifier les valeurs dupliquées
doublons_values = donnees_python[variable][donnees_python[variable].duplicated()]

# Filtrer les lignes qui contiennent ces valeurs dupliquées
sansDouble = donnees_python[~donnees_python[variable].isin(doublons_values)]

12.5 Suppression des doublons pour l’ensemble des variables

/* 1ère méthode */
proc sort data = donnees_sas nodupkey;
  by _all_;
run;

/* 2e méthode, avec first. et last. (cf. infra) */
/* On récupère déjà la dernière variable de la base (on en aura besoin plus loin) */
proc contents data = donnees_sas out = Var noprint;run;
proc sql noprint;
  select name into :derniere_var from Var
  where varnum = (select max(varnum) from Var);
quit;
proc sql noprint;
  select name into :nom_col separated by " " from Var order by varnum;
quit;
%put Dernière variable de la base : &derniere_var.;
proc sort data = donnees_sas;by &nom_col.;run;
data sansDouble;
  set donnees_sas;
  by &nom_col.;
  if first.&derniere_var.;
run;
donnees_rbase_sansdoublon <- unique(donnees_rbase)
donnees_rbase_sansdoublon <- donnees_rbase[! duplicated(donnees_rbase), ]

# Autre solution (équivalente à la solution first. de SAS)
donnees_rbase_sansdoublon <- donnees_rbase[order(colnames(donnees_rbase), na.last = FALSE), ]
donnees_rbase_sansdoublon <- donnees_rbase[! duplicated(donnees_rbase[, colnames(donnees_rbase)], fromLast = TRUE), ]
donnees_tidyverse <- donnees_tidyverse %>% 
  arrange(pick(everything())) %>% 
  distinct()

# Autre solution
donnees_tidyverse_sansdoublon <- donnees_tidyverse %>% 
  arrange(across(everything())) %>% 
  distinct()
donnees_datatable_sansdoublon <- unique(donnees_datatable)
donnees_datatable_sansdoublon <- donnees_datatable[! duplicated(donnees_datatable), ]
donnees_python_sansdoublon = donnees_python.drop_duplicates()

12.6 Suppression des doublons pour une seule variable

proc sort data = donnees_sas;by _all_;run;
data sansDouble;
  set donnees_sas;
  by _all_;
  if first.identifiant;
run;
donnees_rbase <- donnees_rbase[order(colnames(donnees_rbase), na.last = FALSE), ]
sansDouble <- donnees_rbase[! duplicated(donnees_rbase$identifiant), , drop = FALSE]
# L'option .keep_all = TRUE est nécessaire 
# À FAIRE : REVOIR LE TRI PAR RAPPORT A SAS !!!
sansDouble <- donnees_tidyverse %>% 
  arrange(pick(everything())) %>% 
  distinct(identifiant, .keep_all = TRUE)
sansDouble <- donnees_tidyverse %>% 
  arrange(across(everything())) %>% 
  distinct(identifiant, .keep_all = TRUE)
setorderv(donnees_datatable, cols = colnames(donnees_datatable), na.last = FALSE)
sansDouble <- donnees_datatable[! duplicated(donnees_datatable[, c("identifiant")]), ]
# Trier le DataFrame par toutes les colonnes avec les valeurs NaN en premier
donnees_python_sorted = donnees_python.sort_values(by=donnees_python.columns.tolist(), na_position='first')

# Supprimer les doublons en gardant la première occurrence pour chaque identifiant
sansDouble = donnees_python_sorted.drop_duplicates(subset=['identifiant'], keep='first')

12.7 Identifiants uniques

proc sql;
  create table id as select distinct identifiant from donnees_sas order by identifiant;
quit;

/* Autre possibilité */
proc sort data = donnees_sas;by identifiant;run;
data id;
  set donnees_sas (keep = identifiant);
  by identifiant;
  if first.identifiant;
run;
# Sous forme de data.frame
unique(donnees_rbase["identifiant"])

# Sous forme de vecteur
unique(donnees_rbase[, "identifiant"])
unique(donnees_rbase[["identifiant"]])
# Sous forme de tibble
donnees_tidyverse %>%
  distinct(identifiant)
# Sous forme de vecteur
donnees_tidyverse %>% distinct(identifiant) %>% pull()
# Sous forme de data.table
unique(donnees_datatable[, "identifiant"])
# Sous forme de vecteur
unique(donnees_datatable[["identifiant"]])
# Sous forme de liste (vecteur) :
list(pd.unique(donnees_python['identifiant']))

# Dataframe
# Convertir les valeurs uniques en DataFrame
donnees_python[['identifiant']].drop_duplicates().reset_index(drop=True)

12.8 Nombre de lignes uniques, sans doublon

proc contents data = donnees_sas out = Var noprint;run;
proc sql noprint;select name into :nom_col separated by ", " from Var order by varnum;quit;
proc sql;
  select count(*) as Nb_Lignes_Uniques
  from (select &nom_col., count(*) from donnees_sas group by &nom_col.);
quit;
nrow(unique(donnees_rbase))
donnees_tidyverse %>%
  distinct() %>% 
  nrow()
uniqueN(donnees_datatable)
donnees_python.drop_duplicates().shape[0]

13 Transposer une base

13.1 Transposer une base

/* On commence déjà par calculer un tableau croisé comptant les occurrences */
proc freq data = donnees_sas;table Sexef * cspf / out = Nb;run;
proc sort data = Nb;by cspf Sexef;run;
proc print data = Nb;run;

/* On transpose le tableau */
proc transpose data = Nb out = transpose;
  by cspf;
  var count;
  id Sexef;
run;
data transpose;set transpose (drop = _name_ _label_);run;
proc print data = transpose;run;
# On commence déjà par calculer un tableau croisé comptant les occurrences
# as.data.frame.matrix est nécessaire, car le résultat de xtabs est un array
nb <- as.data.frame.matrix(xtabs( ~ cspf + sexef, data = donnees_rbase))

# On transpose le tableau
# t() renvoie un objet matrix, d'où le as.data.frame
nb_transpose <- as.data.frame(t(nb))
# On commence déjà par calculer un tableau croisé comptant les occurrences
nb <- donnees_tidyverse %>% 
  count(cspf, sexef) %>% 
  spread(sexef, n)

# On transpose le tableau (on fait passer sexef en ligne et cspf en colonne)
nb_transpose <- nb %>% 
  # Créer les combinaisons de cspf et sexef en ligne
  pivot_longer(cols = -cspf, names_to = "sexef") %>% 
  # Mettre sexef en ligne et cspf en colonne
  pivot_wider(names_from = cspf, values_from = value, values_fill = 0)

# Autre solution avec les packages janitor et sjmisc
library(janitor)
library(sjmisc)
nb <- donnees_tidyverse %>%
  janitor::tabyl(cspf, sexef) %>% 
  # colonne cspf comme nom de ligne
  column_to_rownames(var = "cspf")
nb_transpose <- nb %>%
  sjmisc::rotate_df()
# Etablissement d'un tableau croisé comptant les occurrences
nb <- donnees_datatable[, .N, by = list(cspf, sexef)]
nb <- dcast(nb, cspf ~ sexef, value.var = "N")

# On transpose le tableau
transpose(nb, keep.names = "sexef", make.names = "cspf")
# Autre solution
dcast(melt(nb, id.vars = "cspf", variable.name = "sexef"), sexef ~ cspf)
# Tableau croisé en python :
nb = pd.crosstab(donnees_python['cspf'], donnees_python['sexef'])
# Transposer le tableau croisé
nb_transpose = nb.T

13.2 Passer d’une base en largeur (wide) à une base en longueur (long)

/* Note moyenne par identifiant */
/* On va créer une base Wide avec les notes en colonne et les identifiants en ligne */
%let notes = note_contenu note_formateur note_moyens note_accompagnement note_materiel;
proc sort data = donnees_sas;by identifiant;run;
proc means data = donnees_sas mean noprint;var &notes.;output out = Temp;by identifiant;run;
data Wide;
  set Temp (where = (_STAT_ = "MEAN") drop = _TYPE_ _FREQ_);
  keep identifiant &notes.;
  drop _STAT_;
run;

/* On passe de Wide à Long */
/* On met les notes en ligne */
proc transpose data = Wide out = Long;by Identifiant;var &notes.;run;

Lien utile : https://stats.oarc.ucla.edu/r/faq/how-can-i-reshape-my-data-in-r/.

# On souhaite mettre les notes en ligne et non en colonne
varNotes <- c("note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
# Note moyenne par identifiant
wide_rbase <- aggregate(donnees_rbase[, varNotes], donnees_rbase[, "identifiant", drop = FALSE], mean, na.rm = TRUE)

long_rbase <- reshape(data          = wide_rbase,
                      varying       = varNotes,
                      v.names       = "notes",
                      timevar       = "type_note",
                      times         = varNotes,
                      new.row.names = NULL,
                      direction     = "long")
long_rbase <- long_rbase[order(long_rbase$identifiant), ]
row.names(long_rbase) <- NULL
# On souhaite mettre les notes en ligne et non en colonne
varNotes <- c("note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
# Note moyenne par identifiant
wide_tidyverse <- donnees_tidyverse %>% 
  group_by(identifiant) %>% 
  summarise(across(all_of(varNotes), ~ mean(.x, na.rm = TRUE)))

# On l'exprime en format long
# Mise en garde : ne pas écrire value_to !
long_tidyverse <- wide_tidyverse %>% 
  pivot_longer(cols      = !identifiant,
               names_to  = "type_note",
               values_to = "note") %>% 
  arrange(type_note, identifiant)
# On souhaite mettre les notes en ligne et non en colonne
# Note moyenne par identifiant
varNotes <- c("note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
wide_datatable <- donnees_datatable[, lapply(.SD, mean, na.rm = TRUE), by = identifiant, .SDcols = varNotes]

long_datatable <- melt(wide_datatable,
                       id.vars       = c("identifiant"),
                       measure.vars  = varNotes,
                       variable.name = "type_note",
                       value.name    = "note")
# On souhaite mettre les notes en ligne et non en colonne
varNotes = ["note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel"]

# Calculer la note moyenne par identifiant
wide_python = donnees_python.groupby('identifiant')[varNotes].mean().reset_index()

# Transformer les données de large à long
long_python = wide_python.melt(id_vars=['identifiant'], 
                             value_vars=varNotes, 
                             var_name='type_note', 
                             value_name='notes')

# Trier par identifiant
long_python = long_python.sort_values(by='identifiant').reset_index(drop=True)

13.3 Passer d’une base en longueur (long) à une base en largeur (wide)

Le code précédent doit être lancé au préalable.

/* On souhaite mettre les notes en ligne et non en colonne */
/* On commence par calculer les notes moyennes par identifiant */
%let notes = note_contenu note_formateur note_moyens note_accompagnement note_materiel;
proc sort data = donnees_sas;by identifiant;run;
proc means data = donnees_sas mean noprint;var &notes.;output out = Temp;by identifiant;run;
data Wide;
  set Temp (where = (_STAT_ = "MEAN") drop = _TYPE_ _FREQ_);
  keep identifiant &notes.;
  drop _STAT_;
run;

/* On passe de Wide à Long */
proc transpose data = Wide out = Long;by Identifiant;var &notes.;run;
data Long;set Long (rename = (_NAME_ = Type_Note COL1 = Note));run;

/* On passe de Long à Wide */
proc transpose data = Long out = Wide;
  by Identifiant;
  var Note;
  id Type_Note;
run;

Lien utile : https://stats.oarc.ucla.edu/r/faq/how-can-i-reshape-my-data-in-r/.

# Passer de long à wide : on souhaite revenir à la situation initiale
wide_rbase <- reshape(long_rbase,
                      timevar = "type_note",
                      idvar = c("identifiant", "id"),
                      direction = "wide")
# Passer de long à wide : on souhaite revenir à la situation initiale
# Mise en garde : ne pas écrire value_from !
wide_tidyverse <- pivot_wider(long_tidyverse, 
                              names_from  = type_note,
                              values_from = note)
wide_datatable <- dcast(long_datatable, identifiant ~ type_note, value.var = "note")
wide_python = long_python.pivot_table(index='identifiant', 
                                      columns='type_note', 
                                      values='notes').reset_index()

14 Gestion par groupe

14.1 Numéroter les lignes

14.1.1 Numéroter les lignes de la base

data donnees_sas;
  set donnees_sas;
  Num_observation = _n_;
run;

/* Autre solution */
proc sql noprint;select count(*) into :nbLignes from donnees_sas;quit;
data numLigne;do Num_observation = 1 to &nbLignes.;output;end;run;
/* Autre possibilité */
data _NULL_;
  set donnees_sas nobs = n;
  call symputx('nbLignes', n);
run;
%put Nombre de lignes : &nbLignes.;

/* Le merge "simple" (sans by) va seulement concaténer les deux bases l'une à côté de l'autre */
data donnees_sas;
  merge donnees_sas numLigne;
run;
# Numéro de l'observation : 2 manières différentes
donnees_rbase$num_observation <- seq(1, nrow(donnees_rbase))
donnees_rbase$num_observation <- seq_len(nrow(donnees_rbase))
donnees_rbase$num_observation <- row.names(donnees_rbase)
# Numéro de l'observation
donnees_tidyverse <- donnees_tidyverse %>% 
  # Pour trier les données de la même façon que SAS
  arrange(identifiant, !is.na(date_entree), date_entree) %>% 
  mutate(num_observation = row_number())
# Numéro de l'observation : 2 manières différentes
donnees_datatable[, num_observation := .I]
donnees_datatable[, num_observation := seq_len(.N)]
# Python commence le compte à 0 (penser à ajouter 1 pour coïncider avec la numérotation de R)
donnees_python['num_observation'] = range(1, len(donnees_python) + 1)

donnees_python['num_observation'] = donnees_python.index + 1

14.1.2 Numéroter les contrats de l’individu

/* Numéro du contrat de chaque individu, contrat trié par date d'entrée */
proc sort data = donnees_sas;by identifiant date_entree;run;

data donnees_sas;
  set donnees_sas;
  by identifiant date_entree;
  retain num_contrat;
  if first.identifiant then num_contrat = 1;
  else                      num_contrat = num_contrat + 1;
run;
# Numéro du contrat de chaque individu, contrat trié par date d'entrée
donnees_rbase <- donnees_rbase[order(donnees_rbase$identifiant, donnees_rbase$date_entree, na.last = FALSE), ]
donnees_rbase$un <- 1
# MISE EN GARDE : en utilisant la fonction ave, toujours précéder la fonction de FUN = !
donnees_rbase$numero_contrat <- ave(donnees_rbase$un, donnees_rbase$identifiant, FUN = cumsum)
donnees_rbase$un <- NULL

# Autre solution
# Utiliser seq_along ne nécessite pas un tri préalable !
donnees_rbase$numero_contrat <- as.numeric(ave(donnees_rbase$identifiant, donnees_rbase$identifiant, FUN = seq_along))

# Autre solution : order pour éviter le as.numeric
donnees_rbase <- donnees_rbase[order(donnees_rbase$identifiant, donnees_rbase$date_entree, na.last = FALSE), ]
donnees_rbase$numero_contrat <- ave(order(donnees_rbase$date_entree), donnees_rbase$identifiant, FUN = seq_along)
# Numéro du contrat de chaque individu, contrat trié par date d'entrée
# arrange() va permettre de trier les observations par identifiant et date d'entrée 
donnees_tidyverse <- donnees_tidyverse %>% 
  group_by(identifiant) %>% 
  # Pour trier les données de la même façon que SAS
  arrange(identifiant, !is.na(date_entree), date_entree) %>% 
  mutate(numero_contrat = row_number()) %>% 
  ungroup()
# À FAIRE : Dans group_by, à quoi sert le drop ?
# Numéro du contrat de chaque individu, contrat trié par date d'entrée
setorder(donnees_datatable, "identifiant", "date_entree", na.last = FALSE)
donnees_datatable[, numero_contrat := seq_len(.N), by = identifiant]
# Beaucoup plus lent, à déconseiller !
donnees_datatable[, numero_contrat := rowid(identifiant)]

# Les seuls numéros de colonnes
rowidv(donnees_datatable, identifiant)
# 1. Trier les données par 'identifiant' et 'date_entree'
donnees_python = donnees_python.sort_values(by=['identifiant', 'date_entree'])

# 2. Créer le numéro de contrat
donnees_python['numero_contrat'] = donnees_python.groupby('identifiant').cumcount() + 1

14.2 Première et dernière ligne par identifiant

14.2.1 Première ligne par identifiant

proc sort data = donnees_sas;by identifiant date_entree;run;
/* L'instruction options permet de ne pas afficher d'erreur si la variable numero_contrat n'existe pas */
options dkricond=nowarn dkrocond=nowarn;
data donnees_sas;
  set donnees_sas (drop = numero_contrat);
  by identifiant date_entree;
  retain numero_contrat 0;
  if first.identifiant then numero_contrat = 1;
  else                      numero_contrat = numero_contrat + 1;
run;

options dkricond=warn dkrocond=warn;
/* Pour trier les colonnes */
data donnees_sas;
  retain identifiant date_entree numero_contrat numero_contrat;
  set donnees_sas;
run;
donnees_rbase <- donnees_rbase[order(donnees_rbase$identifiant, donnees_rbase$date_entree, na.last = FALSE), ]
donnees_rbase[! duplicated(donnees_rbase$identifiant), , drop = FALSE]
donnees_tidyverse %>% 
  group_by(identifiant) %>% 
  # Pour trier les données de la même façon que SAS
  arrange(identifiant, !is.na(date_entree), date_entree) %>%
  filter(row_number() == 1) %>% 
  ungroup()

# Autres solutions
donnees_tidyverse %>% 
  group_by(identifiant) %>% 
  # Pour trier les données de la même façon que SAS
  arrange(identifiant, !is.na(date_entree), date_entree) %>%
  slice(1) %>% 
  ungroup()

donnees_tidyverse %>%  
  group_by(identifiant) %>% 
  # Pour trier les données de la même façon que SAS
  arrange(identifiant, !is.na(date_entree), date_entree) %>%
  slice_head(n = 1) %>% 
  ungroup()

donnees_tidyverse %>%  
  group_by(identifiant) %>% 
  # Pour trier les données de la même façon que SAS
  arrange(identifiant, !is.na(date_entree), date_entree) %>%
  filter(row_number() == nth(row_number(), 1)) %>%
  ungroup()
donnees_datatable[, .SD[1], by = identifiant]

# On peut aussi utiliser keyby si l'on souhaite que les résultats soient triés par la variable de groupement (ici identifiant)
donnees_datatable[, .SD[1], keyby = identifiant]
donnees_python.drop_duplicates(subset='identifiant', keep='first')

14.2.2 Dernière ligne par identifiant

proc sort data = donnees_sas;by identifiant date_entree;run;
/* L'instruction options permet de ne pas afficher d'erreur si la variable numero_contrat n'existe pas */
options dkricond=nowarn dkrocond=nowarn;
data donnees_sas;
  set donnees_sas (drop = numero_contrat);
  by identifiant date_entree;
  retain numero_contrat 0;
  if first.identifiant then numero_contrat = 1;
  else                      numero_contrat = numero_contrat + 1;
run;

options dkricond=warn dkrocond=warn;
/* Pour trier les colonnes */
data donnees_sas;
  retain identifiant date_entree numero_contrat numero_contrat;
  set donnees_sas;
run;
donnees_rbase <- donnees_rbase[order(donnees_rbase$identifiant, donnees_rbase$date_entree, na.last = FALSE), ]
donnees_rbase[! duplicated(donnees_rbase$identifiant, fromLast = TRUE), , drop = FALSE]
donnees_tidyverse %>% 
  group_by(identifiant) %>% 
  # Pour trier les données de la même façon que SAS
  arrange(identifiant, !is.na(date_entree), date_entree) %>%
  filter(row_number() == n()) %>% 
  ungroup()

# Autres solutions
donnees_tidyverse %>%  
  group_by(identifiant) %>% 
  # Pour trier les données de la même façon que SAS
  arrange(identifiant, !is.na(date_entree), date_entree) %>%
  slice(n()) %>% 
  ungroup()

donnees_tidyverse %>%  
  group_by(identifiant) %>% 
  # Pour trier les données de la même façon que SAS
  arrange(identifiant, !is.na(date_entree), date_entree) %>%
  filter(row_number() == nth(row_number(), -1)) %>%
  ungroup()
donnees_datatable[, .SD[.N], by = identifiant]
donnees_python.drop_duplicates(subset='identifiant', keep='last')

14.3 Le premier contrat, le dernier contrat, ni le premier ni le dernier contrat de chaque individu

proc sort data = donnees_sas;by identifiant date_entree;run;
data donnees_sas;
  set donnees_sas;
  by identifiant date_entree;
  Premier_Contrat = (first.identifiant = 1);
  Dernier_Contrat = (last.identifiant = 1);
  Ni_Prem_Ni_Der  = (first.identifiant = 0 and last.identifiant = 0);
run;
donnees_rbase <- donnees_rbase[order(donnees_rbase$identifiant, donnees_rbase$date_entree, na.last = FALSE), ]
donnees_rbase$premier_contrat <- ifelse(! duplicated(donnees_rbase$identifiant, fromLast = FALSE), 
                                        1, 0)
donnees_rbase$dernier_contrat <- ifelse(! duplicated(donnees_rbase$identifiant, fromLast = TRUE), 
                                        1, 0)
donnees_rbase$ni_prem_ni_der  <- ifelse(! c(! duplicated(donnees_rbase$identifiant, fromLast = FALSE) | ! duplicated(donnees_rbase$identifiant, fromLast = TRUE)), 
                                        1, 0)
# Premier contrat
donnees_tidyverse <- donnees_tidyverse %>% 
  # Pour trier les données de la même façon que SAS
  arrange(identifiant, !is.na(date_entree), date_entree) %>% 
  group_by(identifiant) %>% 
  mutate(premier_contrat = if_else(row_number() == 1, 1, 0)) %>% 
  ungroup()

# Dernier contrat
donnees_tidyverse <- donnees_tidyverse %>% 
  # Pour trier les données de la même façon que SAS
  arrange(identifiant, !is.na(date_entree), date_entree) %>% 
  group_by(identifiant) %>% 
  mutate(dernier_contrat = if_else(row_number() == n(), 1, 0)) %>% 
  ungroup()

# Ni le premier, ni le dernier contrat
donnees_tidyverse <- donnees_tidyverse %>% 
  # Pour trier les données de la même façon que SAS
  arrange(identifiant, !is.na(date_entree), date_entree) %>% 
  group_by(identifiant) %>% 
  mutate(ni_prem_ni_der = if_else( ! (row_number() == n() | row_number() == 1), 1, 0)) %>% 
  ungroup()
donnees_datatable <- donnees_datatable[order(identifiant, date_entree, na.last = FALSE)]
donnees_datatable[, premier_contrat := fifelse(! duplicated(identifiant, fromLast = FALSE), 
                                               1, 0)]
donnees_datatable[, dernier_contrat := fifelse(! duplicated(identifiant, fromLast = TRUE), 
                                               1, 0)]
donnees_datatable[, ni_prem_ni_der  := fifelse(! c(! duplicated(identifiant, fromLast = FALSE) | ! duplicated(identifiant, fromLast = TRUE)), 
                                               1, 0)]
# 1. Trier les données par 'identifiant' et 'date_entree'
donnees_python = donnees_python.sort_values(by=['identifiant', 'date_entree'])

# Premier contrat
donnees_python['premier_contrat'] = 1 - donnees_python.duplicated(subset='identifiant', keep='first').astype(int)

# Dernier contrat
donnees_python['dernier_contrat'] = 1 - donnees_python.duplicated(subset='identifiant', keep='last').astype(int)

# Ni premier ni dernier contrat
donnees_python['ni_prem_ni_der'] = (~donnees_python['premier_contrat'].astype(bool) & ~donnees_python['dernier_contrat'].astype(bool)).astype(int)

14.4 Sélection de lignes par identifiant

14.4.1 Les 2 premières lignes de chaque identifiant

/* Numéro du contrat */
proc sort data = donnees_sas;by identifiant date_entree;run;
data donnees_sas;
  set donnees_sas;
  by identifiant date_entree;
  retain num_contrat;
  if first.identifiant then num_contrat = 1;
  else                      num_contrat = num_contrat + 1;
run;

proc sort data = donnees_sas;by identifiant numero_contrat;run;
proc sql;
  select * from donnees_sas group by identifiant
  having numero_contrat <= 2;
quit;
donnees_rbase <- donnees_rbase[order(donnees_rbase$identifiant, donnees_rbase$date_entree, na.last = FALSE), ]

# En utilisant la fonction by
deux_premieres_lignes <- Reduce(rbind, by(donnees_rbase, donnees_rbase["identifiant"], head, 2))

# En utilisant la fonction split pour découper par identifiant, et en ne retenant que les deux premières lignes des groupes créés
deux_premieres_lignes <- do.call(rbind, 
                                 lapply(
                                   split(donnees_rbase, donnees_rbase$identifiant), head, 2
                                   ))

# On peut aussi utiliser les numéros de contrat
donnees_rbase$un <- 1L
# MISE EN GARDE : en utilisant la fonction ave, toujours précéder la fonction de FUN = !
donnees_rbase$numero_contrat <- ave(donnees_rbase$un, donnees_rbase$identifiant, FUN = cumsum)
deux_premieres_lignes <- donnees_rbase[which(donnees_rbase$numero_contrat <= 2), ]
donnees_rbase$un <- NULL

# Version en R Base
#https://stackoverflow.com/questions/14800161/select-the-top-n-values-by-group
deux_premieres_lignes <- donnees_tidyverse %>% 
  # Pour trier les données de la même façon que SAS
  arrange(identifiant, !is.na(date_entree), date_entree) %>% 
  group_by(identifiant) %>% 
  slice(1:2) %>% 
  ungroup()
deux_premieres_lignes <- donnees_datatable[, .SD[1:2], by = identifiant]
deux_premieres_lignes = (donnees_python
            .sort_values(by=['identifiant', 'date_entree'], ascending=[True, True], na_position='last')
            .groupby('identifiant')
            .head(2)
            .reset_index(drop=True)
)

14.4.2 Les 2 dernières lignes de chaque identifiant

/* Numéro du contrat */
proc sort data = donnees_sas;by identifiant date_entree;run;
data donnees_sas;
  set donnees_sas;
  by identifiant date_entree;
  retain num_contrat;
  if first.identifiant then num_contrat = 1;
  else                      num_contrat = num_contrat + 1;
run;

proc sort data = donnees_sas;by identifiant numero_contrat;run;
proc sql;
  select * from donnees_sas group by identifiant
  having numero_contrat >= count(*) - 1;
quit;
deux_dernieres_lignes <- donnees_rbase[unlist(tapply(seq_len(nrow(donnees_rbase)), 
                                                     donnees_rbase$identifiant, 
                                                     function(x) tail(x, 2))), ]

# Version en R Base
#https://stackoverflow.com/questions/14800161/select-the-top-n-values-by-group
# À FAIRE : ne fait pas la même-chose !
deux_dernieres_lignes <- donnees_tidyverse %>% 
  # Pour trier les données de la même façon que SAS
  arrange(identifiant, !is.na(date_entree), date_entree) %>% 
  group_by(identifiant) %>% 
  slice(n() - 2) %>% 
  ungroup()
deux_dernieres_lignes <- donnees_datatable[, tail(.SD, 2), by = identifiant]
deux_dernieres_lignes = (donnees_python
            .sort_values(by=['identifiant', 'date_entree'], ascending=[True, True], na_position='last')
            .groupby('identifiant')
            .tail(2)
            .reset_index(drop=True)
)

14.4.3 2e ligne de l’individu (et rien si l’individu a 1 seule ligne)

/* Numéro du contrat */
proc sort data = donnees_sas;by identifiant date_entree;run;
data donnees_sas;
  set donnees_sas;
  by identifiant date_entree;
  retain numero_contrat 0;
  if first.identifiant then numero_contrat = 1;
  else                      numero_contrat = numero_contrat + 1;
run;

/* 2 stratégies possibles */
data Deuxieme_Contrat;
  set donnees_sas;
  if numero_contrat = 2;
run;

data Deuxieme_Contrat;
  set donnees_sas (where = (numero_contrat = 2));
run;
donnees_rbase <- donnees_rbase[order(donnees_rbase$identifiant, donnees_rbase$date_entree, na.last = FALSE), ]
donnees_rbase[unlist(tapply(seq_len(nrow(donnees_rbase)), donnees_rbase$identifiant, function(x) head(x, 2))), ]

# Avec le numéro de contrat
donnees_rbase$un <- 1L
# MISE EN GARDE : en utilisant la fonction ave, toujours précéder la fonction de FUN = !
donnees_rbase$numero_contrat <- ave(donnees_rbase$un, donnees_rbase$identifiant, FUN = cumsum)
deuxieme_ligne <- donnees_rbase[donnees_rbase$numero_contrat == 2, ]
donnees_rbase$un <- NULL
donnees_tidyverse %>% 
  group_by(identifiant) %>% 
  # Pour trier les données de la même façon que SAS
  arrange(identifiant, !is.na(date_entree), date_entree) %>% 
  filter(row_number() == 2) %>% 
  ungroup()
deuxieme_ligne <- donnees_datatable[, .SD[2], by = identifiant]
deuxieme_ligne_par_groupe = (
    donnees_python
    .sort_values(by=['identifiant', 'date_entree'], ascending=[True, True], na_position='last')
    .groupby('identifiant')
    .nth(1)  # 1 correspond à la deuxieme ligne
    .reset_index()
)

14.4.4 L’avant-dernière ligne de l’individu (et rien si l’individu a 1 seul contrat)

/* Nécessite d'avoir le numéro du contrat */
proc sql;
  select * from donnees_sas group by identifiant
  having numero_contrat = count(*) - 1;
quit;
donnees_rbase[unlist(tapply(seq_len(nrow(donnees_rbase)), donnees_rbase$identifiant, function(x) x[length(x)-1])), ]
donnees_tidyverse %>% 
  group_by(identifiant) %>% 
  # Pour trier les données de la même façon que SAS
  arrange(identifiant, !is.na(date_entree), date_entree) %>% 
  filter(row_number() == nth(row_number(), -2))
donnees_datatable[, .SD[.N-1], by = identifiant]
deuxieme_ligne_par_groupe = (
    donnees_python
    .sort_values(by=['identifiant', 'date_entree'], ascending=[True, True], na_position='last')
    .groupby('identifiant')
    .nth(1)  # 1 correspond à la deuxieme ligne
    .reset_index()
)

14.5 Sélection par groupement

14.5.1 Personnes qui ont eu au moins une entrée en 2022

/* Personnes qui ont eu au moins une entrée en 2022 */
proc sql;
  select *
  from donnees_sas
  group by identifiant
  having sum(year(date_entree) = 2022) >= 1;
quit;
# Personnes qui ont eu au moins une entrée en 2022
auMoins2022 <- subset(donnees_rbase, identifiant %in% unique(identifiant[lubridate::year(date_entree) %in% c(2022)]))

# Autre solution : ne semble possible que pour une seule variable
# MISE EN GARDE : en utilisant la fonction ave, toujours précéder la fonction de FUN = !
auMoins2022 <- donnees_rbase[with(donnees_rbase, ave(lubridate::year(date_entree) %in% c(2022), identifiant, FUN = any)), ]
auMoins2022 <- <