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

13 novembre 2024

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

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 Met en minuscule les caractères sélectionnés

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

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 Met en minuscule les caractères sélectionnés

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

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 Met en minuscule les caractères sélectionnés

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[, 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

Importer des données extérieures dans SAS ou R est sans doute la première tâche à laquelle est confronté l’utilisateur de ces logiciels. 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.

2.4.1 La fonction readLines() de R

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.2 Spécificité des environnements

/* 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")]

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 À FAIRE

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

# 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

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 = as.period(interval(start = date_naissance, end = date_entree))$year)

# En mois : À FAIRE

# En jours : À FAIRE
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)))
  )
# 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 := rowid(identifiant)]
donnees_datatable[, numero_contrat := seq_len(.N), by = 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 = ifelse(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 = ifelse(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 = ifelse( ! (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 <- subset(
  transform(donnees_rbase, 
            cond = ave(lubridate::year(date_entree), identifiant, FUN = function(x) sum(ifelse(x %in% c(2022), 1, 0)))),
  cond >= 1)
auMoins2022$cond <- NULL
# Personnes qui ont eu au moins une entrée en 2022
auMoins2022 <- donnees_tidyverse %>% 
  group_by(identifiant) %>%
  filter(any(lubridate::year(date_entree) == 2022)) %>% 
  ungroup()

# Ou plus simplement
auMoins2022 <- donnees_tidyverse %>% 
  filter(any(lubridate::year(date_entree) == 2022), .by = identifiant)
# Personnes qui ont eu au moins une entrée en 2022
# Une fonction year() est déjà implémentée en data.table, l'usage de lubridate est inutile
auMoins2022 <- donnees_datatable[, if (any(data.table::year(date_entree) %in% 2022)) .SD, by = identifiant]

# Autre solution
auMoins2022 <- donnees_datatable[, if (sum(data.table::year(date_entree) == 2022, na.rm = TRUE) > 0) .SD, by = identifiant]
auMoins2022 = (
    donnees_python
    .groupby('identifiant')
    .filter(lambda x: (x['date_entree'].dt.year == 2022).any())
)

14.5.2 Personnes qui ont suivi à la fois une formation qualifiée et une formation non qualifiée

proc sql;
  create table Qualif_Non_Qualif as
  select *
  from donnees_sas
  group by identifiant
  having sum(Niveau = "Non qualifie") >= 1 and sum(Niveau = "Non qualifie") >= 1;
quit;
# Personnes qui ont suivi à la fois une formation qualifiée et une formation non qualifiée
# MISE EN GARDE : en utilisant la fonction ave, toujours précéder la fonction de FUN = !
qualif_non_qualif <- subset(
  transform(donnees_rbase, 
            qualif     = ave(niveau, identifiant, FUN = function(x) sum(ifelse(x == "Qualifié", 1, 0), na.rm = TRUE)), 
            non_qualif = ave(niveau, identifiant, FUN = function(x) sum(ifelse(x == "Non Qualifié", 1, 0), na.rm = TRUE))),
  qualif >= 1 & non_qualif >= 1)
# Personnes qui ont suivi à la fois une formation qualifiée et une formation non qualifiée
qualif_non_qualif <- donnees_tidyverse %>% 
  group_by(identifiant) %>%
  filter(any(niveau == "Qualifié") & any(niveau == "Non qualifié")) %>% 
  ungroup()

# Ou plus simplement
qualif_non_qualif <- donnees_tidyverse %>% 
  filter(any(niveau == "Qualifié") & any(niveau == "Non qualifié"), .by = identifiant)
# Personnes qui ont suivi à la fois une formation qualifiée et une formation non qualifiée

# Méthode la plus simple
donnees_datatable[, if (sum(niveau == "Qualifié", na.rm = TRUE) > 0 & sum(niveau == "Non qualifié", na.rm = TRUE) > 0) .SD, by = identifiant]

# Autre méthode
donnees_datatable[, `:=` (qualif = sum(fifelse(niveau == "Qualifié", 1, 0), na.rm = TRUE),
                          non_qualif = sum(fifelse(niveau == "Non qualifié", 1, 0), na.rm = TRUE)),
                by = identifiant][qualif > 0 & non_qualif > 0]

# Autre méthode
donnees_datatable[, `:=` (qualif = sum(niveau == "Qualifié", na.rm = TRUE), non_qualif = sum(niveau == "Non qualifié", na.rm = TRUE)), by = identifiant][qualif > 0 & non_qualif > 0]

# Group by et Having de SQL
# https://github.com/Rdatatable/data.table/issues/788
qualif_non_qualif = (
    donnees_python
    .groupby('identifiant')
    .filter(lambda x: (x['niveau'] == 'Qualifié').any() and (x['niveau'] == 'Non qualifié').any())
)

14.5.3 Personnes qui ont suivi deux contrats, et seulement deux, dont l’un au moins a débuté en 2022

/* Personnes qui ont suivi deux contrats, et seulement deux, dont l'un au moins a débuté en 2022 */
proc sql;
  create table Deux_Contrats as
  select *
  from donnees_sas
  group by identifiant
  having count(*) = 2 and sum(year(date_entree) = 2022) >= 1;
quit;
# Personnes qui ont suivi deux contrats, et seulement deux, dont l'un au moins a débuté en 2022
# MISE EN GARDE : en utilisant la fonction ave, toujours précéder la fonction de FUN = !
deux_contrats <- subset(
  transform(donnees_rbase, 
            nb = ave(identifiant, identifiant, FUN = length), 
            an = ave(date_entree, identifiant, 
                     FUN = function(x) 
                       sum(ifelse(lubridate::year(x) == 2022, 1, 0), na.rm = TRUE))),
  nb == 2 & an >= 1)
# Personnes qui ont suivi deux contrats, et seulement deux, dont l'un au moins a débuté en 2022
deux_contrats <- donnees_tidyverse %>% 
  group_by(identifiant) %>%
  filter(n() == 2) %>% 
  filter(any(lubridate::year(date_entree) == 2022)) %>%
  ungroup()

# Ou plus simplement
deux_contrats <- donnees_tidyverse %>% 
  filter(any(lubridate::year(date_entree) == 2022 & n() == 2), .by = identifiant)
# Personnes qui ont suivi deux contrats, et seulement deux, dont l'un au moins a débuté en 2022
# Une fonction year() est déjà implémentée en data.table, l'usage de lubridate est inutile
donnees_datatable[, if (.N == 2 & sum(data.table::year(date_entree) == 2022, na.rm = TRUE) >= 1) .SD, by = identifiant]
deux_contrats = (
    donnees_python
    .groupby('identifiant')
    .filter(lambda x: len(x) == 2 and (x['date_entree'].dt.year == 2022).any())
)

14.6 Ajouter le nombre d’observations par CSP

proc sql;
  create table donnees_sas as
  select a.*, b.n
  from donnees_sas a left join
       (select CSPF, count(*) as n from donnees_sas group by CSPF) b on CSPF = CSPF
  order by identifiant;
quit;
# MISE EN GARDE : en utilisant la fonction ave, toujours précéder la fonction de FUN = !
donnees_rbase <- transform(donnees_rbase, 
                           n = ave(cspf, cspf, FUN = length))
donnees_tidyverse <- donnees_tidyverse %>% add_count(cspf)

# Autre solution
donnees_tidyverse <- donnees_tidyverse %>%
  group_by(cspf) %>%
  mutate(n = n()) %>% 
  ungroup()
donnees_datatable[, n := .N, by = cspf]
donnees_datatable[, n := length(identifiant), by = cspf]
donnees_python['n'] = donnees_python.groupby('cspf')['cspf'].transform('count')

14.7 Ajouter deux colonnes désignant la note moyenne et la somme de Note_Contenu, par individu

/* 1ère solution */
proc sort data = donnees_sas;by identifiant;run;
proc means data = donnees_sas mean noprint;
  var Note_Contenu;
  by identifiant;
  output out = Temp;
run;
data Temp;
  set Temp (where = (_STAT_ = "MEAN"));
  keep identifiant Note_Contenu;
  rename Note_Contenu = Note_Contenu_Moyenne;
run;
data donnees_sas;
  merge donnees_sas (in = a) Temp (in = b);
  by identifiant;
  if a;
run;

/* 2e solution : plus souple */
/* Pour supprimer la variable ajoutée lors de la 1ère solution */
data donnees_sas;
  set donnees_sas (drop = Note_Contenu_Moyenne Note_Contenu_Somme);
run;
proc sql;
  create table donnees_sas as
  select *
  from donnees_sas a left join
       (select identifiant,
               mean(Note_Contenu) as Note_Contenu_Moyenne,
               sum(Note_Contenu) as Note_Contenu_Somme
        from donnees_sas group by identifiant) b
       on a.identifiant = b.identifiant
  order by identifiant;
quit;
# MISE EN GARDE : en utilisant la fonction ave, toujours précéder la fonction de FUN = !
donnees_rbase <- transform(donnees_rbase, 
                           note_contenu_moyenne = ave(note_contenu, identifiant, FUN = mean, na.rm = TRUE), 
                           note_contenu_somme   = ave(note_contenu, identifiant, FUN = sum,  na.rm = TRUE))
donnees_tidyverse <- donnees_tidyverse %>%
  group_by(identifiant) %>%
  mutate(note_contenu_moyenne = mean(note_contenu, na.rm = TRUE),
         note_contenu_somme   = sum(note_contenu, na.rm = TRUE)) %>% 
  ungroup()
donnees_datatable[, `:=` (note_contenu_moyenne = mean(note_contenu, na.rm = TRUE),
                          note_contenu_somme = sum(note_contenu, na.rm = TRUE)), by = identifiant]

# Moyenne de chaque note par individu
notes <- tolower(c("Note_Contenu", "Note_Formateur", "Note_Moyens", "Note_Accompagnement", "Note_Materiel"))
donnees_datatable[, paste0(notes, "_m") := lapply(.SD, mean, na.rm = TRUE), .SDcols = notes, keyby = identifiant]
donnees_python['note_contenu_moyenne'] = donnees_python.groupby('identifiant')['note_contenu'].transform('mean')
donnees_python['note_contenu_somme'] = donnees_python.groupby('identifiant')['note_contenu'].transform('sum')

14.8 Ajouter une variable d’entrée initiale par individu

On souhaite ajouter dans la base une variable représentant la première date d’entrée de l’individu.

proc sort data = donnees_sas;by Identifiant date_entree;run;
data donnees_sas;
  set donnees_sas;
  by Identifiant date_entree;
  format premiere_entree ddmmyy10.;
  retain premiere_entree;
  if first.Identifiant then premiere_entree = date_entree;
  else                      premiere_entree = premiere_entree;
run;
donnees_rbase <- donnees_rbase[order(donnees_rbase$identifiant, donnees_rbase$date_entree, na.last = FALSE), ]
# MISE EN GARDE : en utilisant la fonction ave, toujours précéder la fonction de FUN = !
donnees_rbase <- transform(donnees_rbase,
                           premiere_entree = ave(date_entree, identifiant, FUN = function(x) head(x, 1)))

# Autre solution, sans le tri préalable
donnees_rbase <- transform(donnees_rbase,
                           premiere_entree = ave(date_entree, identifiant, FUN = function(x) min(x) ))
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(premiere_entree = head(date_entree, 1), .by = identifiant)

# Autre solution
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(premiere_entree = case_when(row_number() == 1 ~ date_entree,
                                     TRUE              ~ NA)) %>%
  fill(premiere_entree, .direction = c("down")) %>% 
  ungroup()
setorderv(donnees_datatable, c("identifiant", "date_entree"), na.last = FALSE)
donnees_datatable[, premiere_entree := head(date_entree, 1), by = identifiant]
donnees_python['premiere_entree'] = donnees_python.groupby('identifiant')['date_entree'].transform('min')

14.9 Ligne où se trouve une valeur maximale pour un individu (À REVOIR)

On cherche, pour chaque individu, la ligne où se trouve la valeur maximale de note_contenu.

https://stackoverflow.com/questions/24558328/select-the-row-with-the-maximum-value-in-each-group

/* On note un comportement un peu différent de R : la note de l'identifiant 087 est inconnue, et l'identifiant est conservé en SAS, pas en R */

/* Renvoie toutes les lignes où note_contenu est maximale, s'il y a plusieurs ex-aequo */
proc sort data = donnees_sas;by identifiant descending note_contenu;run;
data ligne_max_note_contenu;
  set donnees_sas;
  by identifiant descending note_contenu;
  if first.note_contenu;
run;

/* Renvoie seulement la première ligne en cas d'ex-aequo */
proc sort data = donnees_sas;by identifiant descending note_contenu;run;
data ligne_max_note_contenu;
  set donnees_sas;
  by identifiant descending note_contenu;
  if first.identifiant;
run;
# On note un comportement un peu différent de R : la note de l'identifiant 087 est inconnue, et la ligne est conservée en SAS, pas en R.
# Ceci est dû au fait que la fonction max ignore les NA.

# Renvoie toutes les lignes où note_contenu est maximale, s'il y a plusieurs ex-aequo
ligne_max_note_contenu <- merge(aggregate(note_contenu ~ identifiant, max, data = donnees_rbase), donnees_rbase)

# Autre solution
# MISE EN GARDE : en utilisant la fonction ave, toujours précéder la fonction de FUN = !
ligne_max_note_contenu <- donnees_rbase[with(donnees_rbase, which(note_contenu == ave(note_contenu, identifiant, FUN = max))), ]

# Renvoie seulement la première ligne en cas d'ex-aequo
donnees_rbase <- donnees_rbase[order(donnees_rbase$identifiant, -donnees_rbase$note_contenu), ]
ligne_max_note_contenu <- donnees_rbase[! duplicated(donnees_rbase$identifiant), ]

# Autre solution
ligne_max_note_contenu <- do.call(rbind, lapply(split(donnees_rbase, as.factor(donnees_rbase$identifiant)), function(x) {return(x[which.max(x$note_contenu), ])}))
# On note un comportement un peu différent de R : la note de l'identifiant 087 est inconnue, et l'identifiant est conservé en SAS, pas en R

# Renvoie toutes les lignes où note_contenu est maximale, s'il y a plusieurs ex-aequo
ligne_max_note_contenu <- donnees_tidyverse %>% 
  group_by(identifiant) %>% 
  slice_max(note_contenu)

# Renvoie seulement la première ligne en cas d'ex-aequo
ligne_max_note_contenu <- donnees_tidyverse %>%
     group_by(identifiant) %>%
     slice(which.max(note_contenu))
# On note un comportement un peu différent de R : la note de l'identifiant 087 est inconnue, et l'identifiant est conservé en SAS, pas en R

# Renvoie toutes les lignes où note_contenu est maximale, s'il y a plusieurs ex-aequo
ligne_max_note_contenu <- donnees_datatable[donnees_datatable[, .I[note_contenu == max(note_contenu)], by = identifiant]$V1]

# Renvoie seulement la première ligne en cas d'ex-aequo
ligne_max_note_contenu <- donnees_datatable[, .SD[which.max(note_contenu)], by = identifiant]
# Renvoie toutes les lignes où note_contenu est maximale, s'il y a plusieurs ex-aequo
ligne_max_note_contenu = (
    donnees_python
    .groupby('identifiant')
    .apply(lambda x: x[x['note_contenu'] == x['note_contenu'].max()], include_groups=False).reset_index()
)

14.10 Identifier les changements d’état

Numérote les états successifs identiques d’un même identifiant. À chaque changement d’état d’un même individu, la variable d’état est incrémentée d’une unité.

/* On suppose que l'on dispose d'une base sur le type de financement de la formation */
data Financement_sas;
  infile cards dsd dlm='|';
  format Identifiant $3. Date ddmmyy10.  Financement $10.;
  input  Identifiant $   Date :ddmmyy10. Financement $;
  cards;
  173|02/01/2022|Public
  173|18/07/2022|Public
  173|15/09/2022|Privé
  173|28/12/2022|Public
  173|02/04/2023|Privé
  173|06/06/2024|Privé
  211|02/07/2024|Privé
  ;
run;

proc sort data = Financement_sas;by Identifiant Date Financement;run;
data Financement_sas;
  set Financement_sas;
  Financement_1 = lag(Financement);
  by Identifiant;
  retain Etat;
  if      first.Identifiant            then Etat = 1;
  else if Financement = Financement_1  then Etat = Etat;
  else if Financement ne Financement_1 then Etat = Etat + 1;
run;
# On suppose que l'on dispose d'une base sur le type de financement de la formation
financement_rbase <- data.frame(
  identifiant = c(rep("173", 6), "211"),
  date = c("02/01/2022", "18/07/2022", "15/09/2022", "28/12/2022", "02/04/2023", "06/06/2024", "02/07/2024"),
  financement = c("Public", "Public", "Privé", "Public", "Privé", "Privé", "Privé")
)
financement_rbase$date <- lubridate::dmy(financement_rbase$date)
financement_rbase <- financement_rbase[order(financement_rbase$identifiant, financement_rbase$date, na.last = FALSE), ]
financement_rbase$etat <- rep(seq_along(rle(financement_rbase$financement)$values), 
                              times = rle(financement_rbase$financement)$lengths)
# On suppose que l'on dispose d'une base sur le type de financement de la formation
financement_tidyverse <- data.frame(
  identifiant = c(rep("173", 6), "211"),
  date = c("02/01/2022", "18/07/2022", "15/09/2022", "28/12/2022", "02/04/2023", "06/06/2024", "02/07/2024"),
  financement = c("Public", "Public", "Privé", "Public", "Privé", "Privé", "Privé")
)

financement_tidyverse %>% 
  arrange(identifiant, date) %>% 
  group_by(identifiant) %>% 
  mutate(etat = consecutive_id(financement))
# On suppose que l'on dispose d'une base sur le type de financement de la formation
financement_datatable <- data.table(
  identifiant = c(rep("173", 6), "211"),
  date = c("02/01/2022", "18/07/2022", "15/09/2022", "28/12/2022", "02/04/2023", "06/06/2024", "02/07/2024"),
  financement = c("Public", "Public", "Privé", "Public", "Privé", "Privé", "Privé")
)

setorder(financement_datatable, identifiant, date)
financement_datatable[, etat := rleid(financement_datatable), by = identifiant]
financement_python = pd.DataFrame({
    'identifiant': ['173']*6 + ['211'],
    'date': ['02/01/2022', '18/07/2022', '15/09/2022', '28/12/2022', '02/04/2023', '06/06/2024', '02/07/2024'],
    'financement': ['Public', 'Public', 'Privé', 'Public', 'Privé', 'Privé', 'Privé']
})

# Création de la fonction pour identifier les groupes consécutifs
def consecutive_id(series):
    return (series != series.shift()).cumsum()
  
  # Transformation des dates en format datetime
financement_python['date'] = pd.to_datetime(financement_python['date'], format='%d/%m/%Y')

# Tri des données par identifiant et date
financement_python = financement_python.sort_values(by=['identifiant', 'date'])

# Application de la fonction pour identifier les groupes consécutifs
financement_python['etat'] = (
    financement_python.groupby('identifiant')['financement']
    .transform(consecutive_id)
)

15 Gestion par rangées de lignes

15.1 Sélectionner les lignes avec au moins une note inférieure à 10

%let notes = Note_Contenu Note_Formateur Note_Moyens Note_Accompagnement Note_Materiel;
data Note_Inferieure_10;
  set donnees_sas;
  
  %macro Inf10;
    %global temp;
      %let temp = ;
    %do i = 1 %to %sysfunc(countw(&notes.));
      %let j = %scan(&notes., &i.);
        &j._inf_10 = (&j. < 10 and not missing(&j.));
        %let temp = &temp. &j._inf_10;
    %end;
  %mend Inf10;
  %Inf10;
  
  if sum(of &temp.) >= 1;
  drop &temp.;
run;
varNotes <- c("note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
donnees_rbase[apply(donnees_rbase[, varNotes], 1, function(x) any(x < 10, na.rm = TRUE)), ]
varNotes <- c("note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
donnees_tidyverse %>%
  filter(if_any(varNotes, ~ .x < 10))

# Autre solution
donnees_tidyverse %>%
  filter_at(varNotes, any_vars(. < 10))
varNotes <- c("note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
note_moins_10 <- donnees_datatable[donnees_datatable[, .I[rowSums(.SD < 10, na.rm = TRUE) >= 1], .SDcols = varNotes]]

# Autre solution
# Le Reduce(`|`, ...) permet d'appliquer la condition | (ou) à tous les élements de la ligne, qui sont une vérification d'une note < 10
note_moins_10 <- donnees_datatable[donnees_datatable[, Reduce(`|`, lapply(.SD, `<`, 10)), .SDcols = varNotes]]

# https://arelbundock.com/posts/datatable_rowwise/
varNotes = ["note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel"]
donnees_python[donnees_python[varNotes].apply(lambda x: (x < 10).any(), axis=1)]

15.2 Sélectionner les lignes avec toutes les notes supérieures à 10

%let notes = Note_Contenu Note_Formateur Note_Moyens Note_Accompagnement Note_Materiel;
data Note_Sup_10;
  set donnees_sas;
  
  %macro Sup10;
    %global temp;
      %let temp = ;
    %do i = 1 %to %sysfunc(countw(&notes.));
      %let j = %scan(&notes., &i.);
        &j._sup_10 = (&j. >= 10);
        %let temp = &temp. &j._sup_10;
    %end;
  %mend Sup10;
  %Sup10;
  
  if sum(of &temp.) = %sysfunc(countw(&notes.));
  drop &temp.;
run;
varNotes <- c("note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")

# Toutes les notes >= 10 et non manquantes
donnees_rbase[apply(donnees_rbase[, varNotes], 1, function(x) all(x >= 10 & ! is.na(x), na.rm = TRUE)), ]
varNotes <- c("note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")

# Toutes les notes >= 10 et non manquantes
donnees_tidyverse %>%
  filter(if_all(varNotes, ~ . >= 10))

# Autre solution
donnees_tidyverse %>%
  filter_at(varNotes, all_vars(. >= 10))
varNotes <- c("note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")

# Toutes les notes >= 10 et non manquantes
note_sup_10 <- donnees_datatable[
  donnees_datatable[, .I[rowSums(.SD >= 10, na.rm = TRUE) == length(varNotes)], .SDcols = varNotes]]

# Autre solution
note_sup_10 <- donnees_datatable[donnees_datatable[, Reduce(`&`, lapply(.SD, `>=`, 10)), .SDcols = varNotes]]
varNotes = ["note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel"]
donnees_python[donnees_python[varNotes].apply(lambda x: (x >= 10).all() and x.notna().all(), axis=1)]

15.3 Moyenne par ligne

Pour chaque observation, 5 notes sont renseignées. On calcule la moyenne de ces 5 notes pour chaque ligne.

15.3.1 Moyenne par ligne

%let notes = Note_Contenu Note_Formateur Note_Moyens Note_Accompagnement Note_Materiel;
data donnees_sas;
  set donnees_sas;
  
  /* 1ère solution */
  Note_moyenne    = mean(of &notes.);
  
  /* 2e solution : l'équivalent des list-comprehension de Python en SAS */
  %macro List_comprehension;
    Note_moyenne2 = mean(of %do i = 1 %to %sysfunc(countw(&notes.));
                                %let j = %scan(&notes., &i.);
                                          &j.
                                        %end;);;
  %mend List_comprehension;
  %List_comprehension;
run;
varNotes <- tolower(c("Note_Contenu", "Note_Formateur", "Note_Moyens", "Note_Accompagnement", "Note_Materiel"))

donnees_rbase$note_moyenne <- rowMeans(donnees_rbase[, varNotes], na.rm = TRUE)
# apply permet d'appliquer une fonctions aux lignes (1) ou colonnes (2) d'un data.frame
donnees_rbase$note_moyenne <- apply(donnees_rbase[, varNotes], 1, mean, na.rm = TRUE)
varNotes <- tolower(c("Note_Contenu", "Note_Formateur", "Note_Moyens", "Note_Accompagnement", "Note_Materiel"))
# Codes à privilégier
donnees_tidyverse <- donnees_tidyverse %>%
  mutate(note_moyenne = rowMeans(pick(all_of(varNotes)), na.rm = TRUE))
donnees_tidyverse <- donnees_tidyverse %>%
  mutate(note_moyenne = rowMeans(across(all_of(varNotes)), na.rm = TRUE))

# Alternative lente
# Noter l'utilisation de c_across dans ce cas de figure pour traiter automatiquement plusieurs variables
donnees_tidyverse <- donnees_tidyverse %>% 
  rowwise() %>% 
  mutate(note_moyenne = mean(c_across(all_of(varNotes)), na.rm = TRUE)) %>% 
  ungroup()
varNotes <- tolower(c("Note_Contenu", "Note_Formateur", "Note_Moyens", "Note_Accompagnement", "Note_Materiel"))
# On souhaite moyenner les notes par formation
donnees_datatable[, note_moyenne := rowMeans(.SD, na.rm = TRUE), .SDcols = varNotes]

# Manière alternative, qui ne semble pas fonctionner
#donnees_datatable[, note_moyenne := Reduce(function(...) sum(..., na.rm = TRUE), .SD),
#                  .SDcols = varNotes,
#                  by = 1:nrow(donnees_datatable)]
#donnees_datatable[, do.call(function(x, y) sum(x, y, na.rm = TRUE), .SD), .SDcols = varNotes, by = 1:nrow(donnees_datatable)]
varNotes = ["note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel"]
donnees_python['note_moyenne'] = donnees_python[varNotes].mean(axis=1, skipna=True)

15.3.2 Moyenne des moyennes par ligne

/* Note moyenne (moyenne des moyennes), non pondérée et pondérée */
proc means data = donnees_sas mean;var Note_moyenne;run;
proc means data = donnees_sas mean;var Note_moyenne;weight poids_sondage;run;
# Note moyenne (moyenne des moyennes), non pondérée et pondérée
mean(donnees_rbase$note_moyenne, na.rm = TRUE)
weighted.mean(donnees_rbase$note_moyenne, donnees_rbase$poids_sondage, na.rm = TRUE)
# Note moyenne (moyenne des moyennes) non pondérée
donnees_tidyverse %>% pull(note_moyenne) %>% mean(na.rm = TRUE)
donnees_tidyverse %>% summarise(Moyenne = mean(note_moyenne, na.rm = TRUE))

# Note moyenne (moyenne des moyennes) pondérée
donnees_tidyverse %>% summarise(Moyenne_ponderee = weighted.mean(note_moyenne, poids_sondage, na.rm = TRUE))
# Note moyenne (moyenne des moyennes), non pondérée et pondérée
donnees_datatable[, mean(note_moyenne, na.rm = TRUE)]
donnees_datatable[, weighted.mean(note_moyenne, poids_sondage, na.rm = TRUE)]
donnees_python['note_moyenne'].mean()
(donnees_python['note_moyenne'] * donnees_python['poids_sondage']).sum(skipna=True) / donnees_python['poids_sondage'].sum(skipna=True)

15.3.3 La moyenne par ligne est-elle supérieure à la moyenne ?

/* On crée une macro-variable SAS à partir de la valeur de la moyenne */
proc sql noprint;select mean(Note_moyenne) into :moyenne from donnees_sas;quit;
data donnees_sas;
  set donnees_sas;
  Note_Superieure_Moyenne = (Note_moyenne > &moyenne.);
run;
proc freq data = donnees_sas;tables Note_Superieure_Moyenne;run;
moyenne <- mean(donnees_rbase$note_moyenne, na.rm = TRUE)
donnees_rbase$note_superieure_moyenne <- ifelse(donnees_rbase$note_moyenne > moyenne, 1, 0)
table(donnees_rbase$note_superieure_moyenne, useNA = "always")
moyenne <- donnees_tidyverse %>% pull(note_moyenne) %>% mean(na.rm = TRUE)
donnees_tidyverse <- donnees_tidyverse %>% mutate(note_superieure_moyenne = ifelse(note_moyenne > moyenne, 1, 0))
donnees_tidyverse %>% pull(note_superieure_moyenne) %>% table(useNA = "always")
moyenne <- donnees_datatable[, mean(note_moyenne, na.rm = TRUE)]
donnees_datatable[, note_superieure_moyenne := fcase(note_moyenne >= moyenne, 1,
                                                     note_moyenne <  moyenne, 0)]
table(donnees_datatable$note_superieure_moyenne, useNA = "always")
moyenne = donnees_python['note_moyenne'].mean()
donnees_python['note_superieure_moyenne'] = (donnees_python['note_moyenne'] > moyenne).astype(int)

donnees_python['note_superieure_moyenne'].value_counts(dropna=False)

15.4 Moyenne pondérée par ligne

Pour chaque observation, 5 notes sont renseignées. On calcule la moyenne de ces 5 notes pour chaque ligne, mais cette fois-ci en pondérant chacune de ces notes.

/* On souhaite affecter les pondérations suivantes aux notes :
Note_Contenu : 30%, Note_Formateur : 20%, Note_Moyens : 25%, Note_Accompagnement : 15%, Note_Materiel : 10% */
/* Voici une solution possible. Une alternative intéressante serait de passer par IML (non traité ici) */
%let ponderation = 0.3 0.2 0.25 0.15 0.1;
%let notes = Note_Contenu Note_Formateur Note_Moyens Note_Accompagnement Note_Materiel;

data donnees_sas;
  set donnees_sas;
  
  %macro Somme_pond;
    %global temp;
      %let temp = ;
    %do i = 1 %to %sysfunc(countw(&notes.));
      %let k = %scan(&notes., &i.);
        %let l = %scan(&ponderation., &i., %str( ));
        &k._pond = &k. * &l.;
        %let temp = &temp. &k._pond;
      %end;
  %mend Somme_pond;
  %Somme_pond;
  
  Note_moyenne_pond = sum(of &temp.);
  drop &temp.;
run;
proc means data = donnees_sas mean;var Note_moyenne_pond;run;
# On souhaite affecter les pondérations suivantes aux notes :
# note_contenu : 30%, note_formateur : 20%, note_moyens : 25%, note_accompagnement : 15%, note_materiel : 10%
notes <- c("note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
ponderation <- c(note_contenu = 30, note_formateur = 20, note_moyens = 25, note_accompagnement = 15, note_materiel = 10) / 100
# On vérifie que la somme des poids vaut 1
sum(ponderation)

# La fonction RowMeans ne fonctionne plus, cette fois !
donnees_rbase$note_moyennepond <- apply(donnees_rbase[, notes], 1, function(x) weighted.mean(x, ponderation, na.rm = TRUE))

# Autre manière, en exploitant le calcul matriciel
# Ne fonctionne pas dans cet exemple, du fait des NA
as.matrix(donnees_rbase[, notes]) %*% as.matrix(ponderation)
# Produit élément par élément
# On peut procéder par produit tensoriel
# À REVOIR
as.matrix(donnees_rbase[, notes]) * matrix(t(as.matrix(ponderation)), nrow(donnees_rbase), length(notes))
# On souhaite affecter les pondérations suivantes aux notes :
# note_contenu : 30%, note_formateur : 20%, note_moyens : 25%, note_accompagnement : 15%, note_materiel : 10%
notes <- c("note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
ponderation <- c(note_contenu = 30, note_formateur = 20, note_moyens = 25, note_accompagnement = 15, note_materiel = 10) / 100
# On vérifie que la somme des poids vaut 1
sum(ponderation)

# La fonction RowMeans ne fonctionne plus, cette fois !

# Noter l'utilisation de c_across dans ce cas de figure pour traiter automatiquement plusieurs variables
donnees_tidyverse <- donnees_tidyverse %>%
  rowwise() %>%
  mutate(note_moyenne = weighted.mean(c_across(varNotes), ponderation, na.rm = TRUE)) %>% 
  ungroup()
# On souhaite affecter les pondérations suivantes aux notes :
# note_contenu : 30%, note_formateur : 20%, note_moyens : 25%, note_accompagnement : 15%, note_materiel : 10%
notes <- c("note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
ponderation <- c(note_contenu = 30, note_formateur = 20, note_moyens = 25, note_accompagnement = 15, note_materiel = 10) / 100
# On vérifie que la somme des poids vaut 1
sum(ponderation)

# La fonction RowMeans ne fonctionne plus, cette fois !
donnees_datatable[, note_moyenne_pond := rowSums(mapply(FUN = `*`, .SD, ponderation), na.rm = TRUE), .SDcols = notes]
# On souhaite affecter les pondérations suivantes aux notes :
# note_contenu : 30%, note_formateur : 20%, note_moyens : 25%, note_accompagnement : 15%, note_materiel : 10%
notes = ["note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel"]
ponderation_dict = {"note_contenu" : 0.3, "note_formateur" : 0.2, "note_moyens" : 0.25, "note_accompagnement" : 0.15, "note_materiel" : 0.1}
# On vérifie que la somme des poids vaut 1
sum(ponderation_dict.values())

# Extraire les pondérations dans le meme ordre que le vecteur de notes :
ponderation = np.array([ponderation_dict[note] for note in varNotes])

# Moyenne pondérée
donnees_python['note_moyenne'] = donnees_python[varNotes].apply(
    lambda row: np.average(row, weights=ponderation[:len(row.dropna())]) if len(row.dropna()) > 0 else np.nan,
    axis=1
)

16 Variable retardée (lag) et avancée (lead)

16.1 Variable retardée (lag)

/* La date de fin du contrat précédent (lag) */
/* Ecriture correcte d'un lag en SAS */
proc sort data = donnees_sas;by identifiant date_entree;run;
data donnees_sasBon;
  set donnees_sas;
  by identifiant date_entree;  
  format Date_fin_1 ddmmyy10.;
  Date_fin_1 = lag(Date_sortie);
  if first.identifiant then Date_fin_1 = .;
run;

/* Ecriture incorrecte d'un lag en SAS */
/* ATTENTION au lag DANS UNE CONDITION IF */
/* Il faut toujours "sortir" le lag de la condition IF */
proc sort data = donnees_sas;by identifiant date_entree;run;
data Lag_Bon;
  set donnees_sas (keep = identifiant date_entree date_sortie);
  format date_sortie_1 lag_faux lag_bon ddmmyy10.;
  /* Erreur */
  if date_entree = lag(date_sortie) + 1 then lag_faux = lag(date_sortie) + 1;
  /* Bonne écriture */
  date_sortie_1 = lag(date_sortie);
  if date_entree = date_sortie_1 + 1 then lag_bon = date_sortie_1 + 1;
run;
# La date de fin du contrat précédent
donnees_rbase <- donnees_rbase[order(donnees_rbase$identifiant, donnees_rbase$date_entre, na.last = FALSE), ]

# Il n'existe pas de fonction lag dans le R de base (à notre connaissance)
# Il faut soit utiliser un package, soit utiliser cette astuce
donnees_rbase$date_sortie_1 <- c(as.Date(NA), donnees_rbase$date_sortie[ seq(1, length(donnees_rbase$date_sortie) - 1)])
donnees_rbase$date_sortie_1 <- c(as.Date(NA), donnees_rbase$date_sortie[ 1:(length(donnees_rbase$date_sortie) - 1)])

# Ou, encore plus simple !
donnees_rbase$date_sortie_1 <- c(as.Date(NA), head(donnees_rbase$date_sortie, -1))
# La date de fin du contrat précédent
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(date_sortie_1 = lag(date_sortie))
# La date de fin du contrat précédent
setorderv(donnees_datatable, cols = c("identifiant", "date_entree"), order = c(1L, 1L), na.last = FALSE)
donnees_datatable[, date_sortie_1 := shift(.SD, n = 1, fill = NA, "lag"), .SDcols = "date_sortie"]
donnees_datatable[, .(date_sortie, date_sortie_1)]
donnees_python = donnees_python.sort_values(by=['identifiant', 'date_entree'], ascending=[True, True])
donnees_python['date_sortie_1'] = donnees_python['date_sortie'].shift(1)

16.2 Variable avancée (lead)

proc expand data= donnees_sas out = Lead;
  convert date_sortie = date_sortie__1 / transformout = (lead 1);
run;
# Il n'existe pas de fonction lead dans le R de base (à notre connaissance)
# La date du contrat futur (lead)
donnees_rbase$date_sortie__1 <- c(donnees_rbase$date_sortie[ 2:(length(donnees_rbase$date_sortie))], as.Date(NA))

# Ou, encore plus simple !
donnees_rbase$date_sortie_1 <- c(tail(donnees_rbase$date_sortie, -1), as.Date(NA))
# La date du contrat futur (lead)
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(date_sortie__1 = lead(date_sortie))
# La date du contrat futur (lead)
setorderv(donnees_datatable, cols = c("identifiant", "date_entree"), order = c(1L, 1L), na.last = FALSE)
donnees_datatable[, date_sortie__1 := shift(.SD, n = 1, fill = NA, "lead"), .SDcols = "date_sortie"]
donnees_datatable[, .(date_sortie, date_sortie__1)]
donnees_python = donnees_python.sort_values(by=['identifiant', 'date_entree'], ascending=[True, True])
donnees_python['date_sortie_1'] = donnees_python['date_sortie'].shift(-1)

17 Les jointures de bases

Pour fonctionner, les codes de cette partie nécessitent l’importation des bases d’exemple de la section “Importation de bases pour les jointures”.

17.1 Importation de bases pour les jointures

/* On suppose que l'on dispose d'une base supplémentaire avec les diplômes des personnes */
data Diplome;
  infile cards dsd dlm='|';
  format Identifiant $3. Diplome $20.;
  input Identifiant $ Diplome $;
  cards;
  173|Bac
  168|Bep-Cap
  112|Bep-Cap
  087|Bac+2
  689|Bac+2
  765|Pas de diplôme
  113|Bac
  999|Bac
  554|Bep-Cap
  ;
run;

/* On suppose que l'on dispose aussi d'une base supplémentaire indiquant la date d'une entrevue avec un conseiller */
data Entrevue;
  infile cards dsd dlm='|';
  format Identifiant $3. Date_entrevue ddmmyy10.;
  input Identifiant $ Date_entrevue ddmmyy10.;
  cards;
  173|06/08/2021
  168|17/10/2019
  087|12/06/2021
  689|28/03/2018
  099|01/09/2022
  765|01/10/2020
  ;
run;

/* On récupère un extrait de la base initiale */
data Jointure;
  set donnees_sas (keep = Identifiant Sexe date_entree date_sortie);
run;
# On suppose que l'on dispose d'une base supplémentaire avec les diplômes des personnes
diplome_rbase <- data.frame(identifiant = c("173", "168", "112", "087", "689", "765", "113", "999", "554"),
                      diplome = c("Bac", "Bep-Cap", "Bep-Cap", "Bac+2", "Bac+2", "Pas de diplôme", "Bac", "Bac", "Bep-Cap"))

# On suppose que l'on dispose d'une base supplémentaire indiquant la date d'une entrevue avec un conseiller
entrevue_rbase <- data.frame(identifiant = c("173", "168", "087", "689", "099", "765"),
                       date_entrevue = c("06/08/2021", "17/10/2019", "12/06/2021", "28/03/2018", "01/09/2022", "01/10/2020"))
entrevue_rbase$date_entrevue <- lubridate::dmy(entrevue_rbase$date_entrevue)

# On récupère un extrait de la base initiale
jointure_rbase <- donnees_rbase[, c("identifiant", "sexe", "date_entree", "date_sortie")]
# On suppose que l'on dispose d'une base supplémentaire avec les diplômes des personnes
diplome_tidyverse <- tibble(identifiant = c("173", "168", "112", "087", "689", "765", "113", "999", "554"),
                      diplome = c("Bac", "Bep-Cap", "Bep-Cap", "Bac+2", "Bac+2", "Pas de diplôme", "Bac", "Bac", "Bep-Cap"))

# On suppose que l'on dispose d'une base supplémentaire indiquant la date d'une entrevue avec un conseiller
entrevue_tidyverse <- tibble(identifiant = c("173", "168", "087", "689", "099", "765"),
                       date_entrevue = c("06/08/2021", "17/10/2019", "12/06/2021", "28/03/2018", "01/09/2022", "01/10/2020"))
entrevue_tidyverse <- entrevue_tidyverse %>% 
  mutate(date_entrevue = lubridate::dmy(date_entrevue))

# On récupère un extrait de la base initiale
variable <- c("identifiant", "sexe", "date_entree", "date_sortie")
jointure_tidyverse <- donnees_tidyverse %>%
  select(all_of(variable))
# On suppose que l'on dispose d'une base supplémentaire avec les diplômes des personnes
diplome_datatable <- data.table(identifiant = c("173", "168", "112", "087", "689", "765", "113", "999", "554"),
                                diplome = c("Bac", "Bep-Cap", "Bep-Cap", "Bac+2", "Bac+2", "Pas de diplôme", "Bac", "Bac", "Bep-Cap"))

# On suppose que l'on dispose d'une base supplémentaire indiquant la date d'une entrevue avec un conseiller
entrevue_datatable <- data.table(identifiant = c("173", "168", "087", "689", "099", "765"),
                                 date_entrevue = c("06/08/2021", "17/10/2019", "12/06/2021", "28/03/2018", "01/09/2022", "01/10/2020"))
entrevue_datatable[, date_entrevue := lubridate::dmy(date_entrevue)]

# On récupère un extrait de la base initiale
jointure_datatable <- donnees_datatable[, c("identifiant", "sexe", "date_entree", "date_sortie")]
# On suppose que l'on dispose d'une base supplémentaire avec les diplômes des personnes
diplome_python = pd.DataFrame({
    'identifiant': ["173", "168", "112", "087", "689", "765", "113", "999", "554"],
    'diplome': ["Bac", "Bep-Cap", "Bep-Cap", "Bac+2", "Bac+2", "Pas de diplôme", "Bac", "Bac", "Bep-Cap"]
})

# On suppose que l'on dispose d'une base supplémentaire indiquant la date d'une entrevue avec un conseiller
entrevue_python = pd.DataFrame({
    'identifiant': ["173", "168", "087", "689", "099", "765"],
    'date_entrevue': ["06/08/2021", "17/10/2019", "12/06/2021", "28/03/2018", "01/09/2022", "01/10/2020"]
})
entrevue_python['date_entrevue'] = pd.to_datetime(entrevue_python['date_entrevue'], format='%d/%m/%Y') # Conversion des dates en datetime

# On récupère un extrait de la base initiale
jointure_python = donnees_python[['identifiant', 'sexe', 'date_entree', 'date_sortie']]

17.2 Inner join : les seuls identifiants communs aux deux bases

/* Sont appariés les identifiants communs aux deux bases */
/* Le tri préalable des bases de données à joindre par la variable de jointure est nécessaire avec la stratégie merge */
proc sort data = Diplome;by identifiant;run;
proc sort data = Jointure;by identifiant;run;
data Inner_Join1;
  merge Jointure (in = a) Diplome (in = b);
  by identifiant;
  if a and b;
run;

/* Autre solution */
/* Le tri préalable des bases de données à joindre n'est pas nécessaire avec la jointure SQL */
proc sql;
  create table Inner_Join2 as
  select * from Jointure a inner join Diplome b on a.identifiant = b.identifiant
  order by a.identifiant;
quit;

proc print data = Inner_Join1 (obs = 10);run;
proc sql;select count(*) from Inner_Join1;quit;
proc sql;select count(*) from Inner_Join2;quit;
# Sont appariés les identifiants communs aux deux bases
innerJoin <- merge(jointure_rbase, diplome_rbase, by.x = "identifiant", by.y = "identifiant")
dim(innerJoin)
# Sont appariés les identifiants communs aux deux bases
innerJoin <- jointure_tidyverse %>% 
  inner_join(diplome_tidyverse, by = "identifiant")
dim(innerJoin)

# Autres solutions
innerJoin <- jointure_tidyverse %>% 
  inner_join(diplome_tidyverse, by = join_by(identifiant == identifiant))
dim(innerJoin)
innerJoin <- inner_join(jointure_tidyverse, diplome_tidyverse, by = "identifiant")
dim(innerJoin)
# Sont appariés les identifiants communs aux deux bases
innerJoin <- merge(jointure_datatable, diplome_datatable, by.x = "identifiant", by.y = "identifiant")
innerJoin <- jointure_datatable[diplome_datatable, nomatch = 0, on = list(identifiant == identifiant)]
innerJoin <- jointure_datatable[diplome_datatable, nomatch = 0, on = .(identifiant == identifiant)]
dim(innerJoin)
# Sont appariés les identifiants communs aux deux bases
inner_join = jointure_python.merge(diplome_python, 
                                  left_on='identifiant', 
                                  right_on = 'identifiant',
                                  how='inner')
inner_join.shape

17.3 Left join : les identifiants de la base de gauche

/* Sont appariés tous les identifiants de la base de gauche, et les correspondants éventuels de la base de droite */
/* Le tri préalable des bases de données à joindre par la variable de jointure est nécessaire avec la stratégie merge */
proc sort data = Diplome;by identifiant;run;
proc sort data = Jointure;by identifiant;run;
data Left_Join1;
  merge Jointure (in = a) Diplome (in = b);
  by identifiant;
  if a;
run;

/* Autre solution */
/* Le tri préalable des bases de données à joindre n'est pas nécessaire avec la jointure SQL */
proc sql;
  create table Left_Join2 as
  select * from Jointure a left join Diplome b on a.identifiant = b.identifiant
  order by a.identifiant;
quit;

proc print data = Left_Join1 (obs = 10);run;
proc sql;select count(*) from Left_Join1;quit;
proc sql;select count(*) from Left_Join2;quit;
# Sont appariés tous les identifiants de la base de gauche, et les correspondants éventuels de la base de droite
leftJoin <- merge(jointure_rbase, diplome_rbase, by.x = "identifiant", by.y = "identifiant", all.x = TRUE)
dim(leftJoin)
# Sont appariés tous les identifiants de la base de gauche, et les correspondants éventuels de la base de droite
leftJoin <- jointure_tidyverse %>% 
  left_join(diplome_tidyverse, by = "identifiant")
dim(leftJoin)

# Autres solutions
leftJoin <- jointure_tidyverse %>% 
  left_join(diplome_tidyverse, by = join_by(identifiant == identifiant))
dim(leftJoin)
leftJoin <- left_join(jointure_tidyverse, diplome_tidyverse, by = "identifiant")
dim(leftJoin)
# Sont appariés tous les identifiants de la base de gauche, et les correspondants éventuels de la base de droite
leftJoin <- merge(jointure_datatable, diplome_datatable, by.x = "identifiant", by.y = "identifiant", all.x = TRUE)
dim(leftJoin)
leftJoin <- diplome_datatable[jointure_datatable, on = .(identifiant == identifiant)]
dim(leftJoin)
# Sont appariés tous les identifiants de la base de gauche, et les correspondants éventuels de la base de droite
left_join = jointure_python.merge(diplome_python, 
                                  left_on='identifiant', 
                                  right_on = 'identifiant',
                                  how='left')
left_join.shape

17.4 Right join : les identifiants de la base de droite

/* Sont appariés tous les identifiants de la base de droite et les correspondants éventuels de la base de gauche */
/* Le tri préalable des bases de données à joindre par la variable de jointure est nécessaire avec la stratégie merge */
proc sort data = Diplome;by identifiant;run;
proc sort data = Jointure;by identifiant;run;
data Right_Join1;
  merge Jointure (in = a) Diplome (in = b);
  by identifiant;
  if b;
run;

/* Autre solution */
/* Le tri préalable des bases de données à joindre n'est pas nécessaire avec la jointure SQL */
proc sql;
  create table Right_Join2 as
  select * from Jointure a right join Diplome b on a.identifiant = b.identifiant
  order by a.identifiant;
quit;

proc print data = Right_Join1 (obs = 10);run;
proc sql;select count(*) from Right_Join1;quit;
proc sql;select count(*) from Right_Join2;quit;
# Sont appariés tous les identifiants de la base de droite et les correspondants éventuels de la base de gauche
rightJoin <- merge(jointure_rbase, diplome_rbase, by.x = "identifiant", by.y = "identifiant", all.y = TRUE)
dim(rightJoin)
# Sont appariés tous les identifiants de la base de droite et les correspondants éventuels de la base de gauche
rightJoin <- jointure_tidyverse %>% 
  right_join(diplome_tidyverse, by = "identifiant")
dim(rightJoin)

# Autre solution
rightJoin <- jointure_tidyverse %>% 
  right_join(diplome_tidyverse, by = join_by(identifiant == identifiant))
dim(rightJoin)
rightJoin <- right_join(jointure_tidyverse, diplome_tidyverse, by = "identifiant")
dim(rightJoin)
# Sont appariés tous les identifiants de la base de droite et les correspondants éventuels de la base de gauche
rightJoin <- merge(jointure_datatable, diplome_datatable, by.x = "identifiant", by.y = "identifiant", all.y = TRUE)
dim(rightJoin)
rightJoin <- jointure_datatable[diplome_datatable, on = .(identifiant == identifiant)]
dim(rightJoin)
# Sont appariés tous les identifiants de la base de droite et les correspondants éventuels de la base de gauche
right_join = jointure_python.merge(diplome_python, 
                                  left_on='identifiant', 
                                  right_on = 'identifiant',
                                  how='right')
right_join.shape

17.5 Full join : les identifiants des deux bases

/* Sont appariés les identifiants des deux bases */
/* Le tri préalable des bases de données à joindre par la variable de jointure est nécessaire avec la stratégie merge */
proc sort data = Diplome;by identifiant;run;
proc sort data = Jointure;by identifiant;run;
data Full_Join1;
  merge Jointure (in = a) Diplome (in = b);
  by identifiant;
  if a or b;
run;

/* Autre solution */
/* Le tri préalable des bases de données à joindre n'est pas nécessaire avec la jointure SQL */
proc sql;
  create table Full_Join2 as
  select coalesce(a.identifiant, b.identifiant) as Identifiant, *
  from Jointure a full outer join Diplome b on a.identifiant = b.identifiant
  order by calculated identifiant;
quit;

proc print data = Full_Join1 (obs = 10);run;
proc sql;select count(*) from Full_Join1;quit;
proc sql;select count(*) from Full_Join2;quit;
# Sont appariés les identifiants des deux bases
fullJoin <- merge(jointure_rbase, diplome_rbase, by.x = "identifiant", by.y = "identifiant", all = TRUE)
dim(fullJoin)
# Sont appariés les identifiants des deux bases
fullJoin <- jointure_tidyverse %>% 
  full_join(diplome_tidyverse, by = "identifiant")
dim(fullJoin)

# Autre solution
fullJoin <- jointure_tidyverse %>% 
  full_join(diplome_tidyverse, by = join_by(identifiant == identifiant))
dim(fullJoin)
fullJoin <- full_join(jointure_tidyverse, diplome_tidyverse, by = "identifiant")
dim(fullJoin)
# Sont appariés les identifiants des deux bases
fullJoin <- merge(jointure_datatable, diplome_datatable, by.x = "identifiant", by.y = "identifiant", all = TRUE)
dim(fullJoin)
# Sont appariés tous les identifiants de la base de droite et les correspondants éventuels de la base de gauche
full_join = jointure_python.merge(diplome_python, 
                                  left_on='identifiant', 
                                  right_on = 'identifiant',
                                  how='outer')
full_join.shape

17.6 Jointure de 3 bases ou plus en une seule opération (exemple avec inner join)

proc sort data = Jointure;by identifiant;run;
proc sort data = Diplome;by identifiant;run;
proc sort data = Entrevue;by identifiant;run;
data Inner_Join3;
  merge Jointure (in = a) Diplome (in = b) Entrevue (in = c);
  by identifiant;
  if a and b and c;
run;

/* Autre solution */
/* Le tri préalable des bases de données à joindre n'est pas nécessaire avec la jointure SQL */
proc sql;
  create table Inner_Join4 as
  select * from Jointure a inner join Diplome  b on a.identifiant = b.identifiant
                           inner join Entrevue c on a.identifiant = c.identifiant
  order by a.identifiant;
quit;

proc print data = Inner_Join4 (obs = 10);run;
proc sql;select count(*) from Inner_Join3;quit;
proc sql;select count(*) from Inner_Join4;quit;
# Utilisation de la fonction Reduce
# Elle applique successivement (et non simultanément, comme do.call) une fonction à tous les éléments d'une liste
innerJoin2 <- Reduce(function(x, y) merge(x, y, all = FALSE, by.x = "identifiant", by.y = "identifiant"),
                     list(jointure_rbase, diplome_rbase, entrevue_rbase))
dim(innerJoin2)
# Utilisation de la fonction reduce de purrr
# Elle applique successivement (et non simultanément, comme do.call) à tous les éléments d'une liste une fonction
innerJoin2 <- list(jointure_tidyverse, diplome_tidyverse, entrevue_tidyverse) %>% 
  purrr::reduce(dplyr::inner_join, by = join_by(identifiant == identifiant))
dim(innerJoin2)
# Utilisation de la fonction Reduce
# Elle applique successivement (et non simultanément, comme do.call) une fonction à tous les éléments d'une liste
innerJoin2 <- Reduce(function(x, y) merge(x, y, all = FALSE, by.x = "identifiant", by.y = "identifiant"),
                    list(jointure_datatable, diplome_datatable, entrevue_datatable))
dim(innerJoin2)
from functools import reduce
# Liste des DataFrames à joindre
dataframes = [jointure_python, diplome_python, entrevue_python]

## Methode 1 : avec la fonction reduce
# Fonction de jointure interne
def inner_join(x, y):
    return pd.merge(x, y, on='identifiant', how='inner')
# Application successive de la fonction de jointure à tous les éléments de la liste
inner_join2 = reduce(inner_join, dataframes)

## Méthode 2 : Sans la fonction reduce
# Initialisation du DataFrame résultant avec le premier DataFrame de la liste
inner_join2 = dataframes[0]

# Jointure successive des autres DataFrames
for df in dataframes[1:]:
    inner_join2 = pd.merge(inner_join2, df, on='identifiant', how='inner')

inner_join2.shape

17.7 Jointure sur inégalités

On cherche à obtenir les entrevues qui se sont déroulées durant le contrat. On cherche alors à apparier les bases Jointure et Entrevue par identifiant si la date d’entrevue de la base Entrevue est comprise entre la date d’entrée et la date de sortie de la base Jointure.

/* On associe l'entrevue au contrat au cours duquel elle a eu lieu */
proc sql;
  create table Inner_Join_Inegalite as
  select *
  from Jointure a inner join Entrevue b
       on a.identifiant = b.identifiant and a.date_entree <= b.date_entrevue <= a.date_sortie
  order by a.identifiant;
quit;
proc print data = Inner_Join_Inegalite (obs = 10);run;
proc sql;select count(*) from Inner_Join_Inegalite;quit;
# Ne semble pas natif en R-Base.
# Une proposition indicative où l'on applique la sélection après la jointure, ce qui ne doit pas être très efficace ...
innerJoinInegalite <- merge(jointure_rbase, entrevue_rbase, by = "identifiant")
innerJoinInegalite <- with(innerJoinInegalite,
                           innerJoinInegalite[which(date_entree <= date_entrevue & date_entrevue <= date_sortie), ])
dim(innerJoinInegalite)
innerJoinInegalite <- jointure_tidyverse %>% 
  inner_join(entrevue_tidyverse, join_by(identifiant == identifiant,
                                         date_entree <= date_entrevue,
                                         date_sortie >= date_entrevue))
dim(innerJoinInegalite)
# Attention, l'ordre des conditions doit correspondre à l'ordre des bases dans la jointure !
# Il semble que l'on soit forcé de spécifier tous les noms des colonnes, et ce qui est un peu problématique ...
# À FAIRE : Peut-on faire plus simplement ??
innerJoinInegalite <- jointure_datatable[entrevue_datatable,
                                         .(identifiant, sexe, date_entree, date_sortie, date_entrevue),
                                         on = .(identifiant, date_entree <= date_entrevue, date_sortie >= date_entrevue),
                                         nomatch = 0L
                                         ][order(identifiant)]
dim(innerJoinInegalite)
# En procédant en deux temps :
# Jointure interne sur la colonne 'identifiant'
intermediate_join = pd.merge(jointure_python, entrevue_python, on='identifiant', how='inner')

# Filtrage selon les conditions d'inégalité
#inner_join_inegalite = intermediate_join.query('date_entree <= date_entrevue <= date_sortie')

inner_join_inegalite.shape

17.8 Cross join : toutes les combinaisons possibles de CSP, sexe et diplôme

proc sql;
  create table CrossJoin as
  select *
  from (select distinct CSPF    from donnees_sas) cross join
       (select distinct Sexef   from donnees_sas) cross join
       (select distinct Diplome from Diplome)
  order by CSPF, Sexef, Diplome;
quit;
proc sql;select count(*) from CrossJoin;quit;
# Toutes les combinaisons possibles de CSP, sexe et diplome
crossJoin <- unique(expand.grid(donnees_rbase$cspf, donnees_rbase$sexef, diplome_rbase$diplome))
colnames(crossJoin) <- c("cspf", "sexef", "diplome")
dim(crossJoin)

# Autre solution
crossJoin2 <- unique(merge(donnees_rbase[, c("cspf", "sexef")], diplome_rbase[, "diplome"], by = NULL))
dim(crossJoin2)
# Toutes les combinaisons possibles de CSP, sexe et diplome
crossJoin <- donnees_tidyverse %>%
  select(cspf, sexef) %>% 
  cross_join(diplome_tidyverse %>% select(diplome)) %>% 
  distinct()
dim(crossJoin)

# Autres solutions
crossJoin <- cross_join(donnees_tidyverse %>% select(cspf, sexef), diplome_tidyverse %>% select(diplome)) %>% 
  distinct()
dim(crossJoin)

crossJoin <- donnees_tidyverse %>% 
  tidyr::expand(cspf, sexef, diplome_tidyverse$diplome) %>%
  distinct()
dim(crossJoin)

crossJoin <- tidyr::crossing(donnees_tidyverse %>% select(cspf, sexef), diplome_tidyverse %>% select(diplome)) %>%
  distinct()
dim(crossJoin)
crossJoin <- data.table::CJ(donnees_datatable[, cspf], donnees_datatable[, sexef], diplome_datatable[, diplome], unique = TRUE)
colnames(crossJoin) <- c("cspf", "sexef", "diplome")
dim(crossJoin)
# Toutes les combinaisons possibles de CSP, sexe et diplome
crossJoin = pd.MultiIndex.from_product([pd.unique(donnees_python['cspf']), 
                                           pd.unique(donnees_python['sexef']), 
                                           pd.unique(diplome_python['diplome'])], 
                                          names=['cspf', 'sexef', 'diplome']).to_frame(index=False)
crossJoin.shape

17.9 Semi join

/* Identifiants de la base de gauche qui ont un correspondant dans la base de droite */
proc sql;
  create table Semi_Join as select * from donnees_sas
  where Identifiant in (select distinct Identifiant from Diplome);
  select count(*) from Semi_Join;
quit;

/* Autre possibilité */
proc sql;
  create table Semi_Join as select * from donnees_sas a
  where exists (select * from Diplome b where (a.Identifiant = b.Identifiant));
  select count(*) from Semi_Join;
quit;
# Identifiants de la base de gauche qui ont un correspondant dans la base de droite
semiJoin <- donnees_rbase[donnees_rbase$identifiant %in% diplome_rbase$identifiant, ]
dim(semiJoin)
# Identifiants de la base de gauche qui ont un correspondant dans la base de droite
semiJoin <- donnees_tidyverse %>% 
  semi_join(diplome_tidyverse, join_by(identifiant == identifiant))
dim(semiJoin)
# Autre solution
semiJoin <- semi_join(donnees_tidyverse, diplome_tidyverse, join_by(identifiant == identifiant))
dim(semiJoin)
# Identifiants de la base de gauche qui ont un correspondant dans la base de droite
semiJoin <- donnees_datatable[identifiant %in% diplome_datatable[, identifiant], ]
dim(semiJoin)
# Toutes les combinaisons possibles de CSP, sexe et diplome
semiJoin = donnees_python[donnees_python['identifiant'].isin(diplome_python['identifiant'])]
semiJoin.shape

17.10 Anti join

/* Identifiants de la base de gauche qui n'ont pas de correspondant dans la base de droite */
proc sql;
  create table Anti_Join as select * from donnees_sas
  where Identifiant not in (select distinct Identifiant from Diplome);
  select count(*) from Anti_Join;
quit;
proc sql;
  create table Anti_Join as select * from donnees_sas a
  where not exists (select * from Diplome b where (a.Identifiant = b.Identifiant);
  select count(*) from Anti_Join;
quit;
# Identifiants de la base de gauche qui n'ont pas de correspondant dans la base de droite
antiJoin <- donnees_rbase[! donnees_rbase$identifiant %in% diplome_rbase$identifiant, ]
dim(antiJoin)
# Identifiants de la base de gauche qui n'ont pas de correspondant dans la base de droite
antiJoin <- donnees_tidyverse %>% 
  anti_join(diplome_tidyverse, join_by(identifiant == identifiant))
dim(antiJoin)

# Autre solution
antiJoin <- anti_join(donnees_tidyverse, diplome_tidyverse, join_by(identifiant == identifiant))
dim(antiJoin)
# Identifiants de la base de gauche qui n'ont pas de correspondant dans la base de droite
donnees_datatable[! diplome_datatable, on = "identifiant", j = ! "diplome"]

# Autre solution
antiJoin <- donnees_datatable[! identifiant %in% diplome_datatable[, identifiant], ]
dim(antiJoin)
# Toutes les combinaisons possibles de CSP, sexe et diplome
antiJoin = donnees_python[~donnees_python['identifiant'].isin(diplome_python['identifiant'])]
antiJoin.shape

17.11 Autres fonctions utiles

17.11.1 Concaténation des identifiants

proc sql;
  /* Concaténation des identifiants */
  select Identifiant from Jointure union all
  select Identifiant from Diplome order
  by identifiant;
quit;
# Concaténation des identifiants avec les doublons
sort(c(jointure_rbase$identifiant, diplome_rbase$identifiant))
# dplyr:: permet de s'assurer que ce sont les fonctions du Tidyverse (et non leurs homonymes de R-Base qui sont utilisées)

# Concaténation des identifiants
dplyr::union_all(jointure_tidyverse$identifiant, diplome_tidyverse$identifiant) %>% 
  sort()
# Les fonctions spécifiques à data.table fonctionnent avec des formats data.table, d'où la syntaxe un peu différente de R base

# Concaténation des identifiants
variable <- "identifiant"
sort(c(jointure_datatable[[variable]], diplome_datatable[[variable]]))
# Toutes les combinaisons possibles de CSP, sexe et diplome
# Concaténer les identifiants avec doublons
pd.concat([donnees_python['identifiant'], diplome_python['identifiant']]).sort_values()

17.11.2 Identifiants uniques des 2 bases

proc sql;
  /* Identifiants uniques des 2 bases */
  select distinct Identifiant from
  (select Identifiant from Jointure union select Identifiant from Diplome)
  order by identifiant;
quit;
# base:: permet de s'assurer que les fonctions proviennent de R base
# Des fonctions du même nom existent en Tidyverse, et tendent à prédominer si le package est lancé

# Identifiants uniques des 2 bases
sort(base::union(jointure_rbase$identifiant, diplome_rbase$identifiant))
sort(base::unique(c(jointure_rbase$identifiant, diplome_rbase$identifiant)))
# dplyr:: permet de s'assurer que ce sont les fonctions du Tidyverse (et non leurs homonymes de R-Base qui sont utilisées)

# Identifiants uniques des 2 bases
unique(dplyr::union_all(jointure_tidyverse$identifiant, diplome_tidyverse$identifiant)) %>% 
  sort()
dplyr::union(jointure_tidyverse$identifiant, diplome_tidyverse$identifiant) %>% 
  sort()
# Les fonctions spécifiques à data.table fonctionnent avec des formats data.table, d'où la syntaxe un peu différente de R base

# Identifiants uniques des 2 bases
variable <- "identifiant"
sort(unique(c(jointure_datatable[[variable]], diplome_datatable[[variable]])))
funion(jointure_datatable[, ..variable], diplome_datatable[, ..variable])
# Toutes les combinaisons possibles de CSP, sexe et diplome
# Concaténer les identifiants avec doublons
identifiants = pd.concat([donnees_python['identifiant'], diplome_python['identifiant']]).sort_values()

pd.Series(identifiants.unique())

17.11.3 Identifiants communs des 2 bases

proc sql;
  /* Identifiants communs des 2 bases */
  select Identifiant from Jointure intersect select Identifiant from Diplome
  order by identifiant;
quit;
# base:: permet de s'assurer que les fonctions proviennent de R base
# Des fonctions du même nom existent en Tidyverse, et tendent à prédominer si le package est lancé

# Identifiants communs des 2 bases
sort(base::intersect(jointure_rbase$identifiant, diplome_rbase$identifiant))
# dplyr:: permet de s'assurer que ce sont les fonctions du Tidyverse (et non leurs homonymes de R-Base qui sont utilisées)

# Identifiants communs des 2 bases
dplyr::intersect(jointure_tidyverse$identifiant, diplome_tidyverse$identifiant) %>% 
  sort()
# Les fonctions spécifiques à data.table fonctionnent avec des formats data.table, d'où la syntaxe un peu différente de R base

# Identifiants communs des 2 bases
variable <- "identifiant"
fintersect(jointure_datatable[, ..variable], diplome_datatable[, ..variable])[order(identifiant)]
# Identifiants communs des 2 bases
sorted(set(donnees_python['identifiant']).intersection(set(diplome_python['identifiant'])))

17.11.4 Identifiants dans Jointure mais pas Diplome

proc sql;
  /* Identifiants dans Jointure mais pas Diplome */
  select distinct Identifiant from Jointure where
  Identifiant not in (select distinct Identifiant from Diplome)
  order by identifiant;
  
  /* Autre possibilité */
  select Identifiant from Jointure except select Identifiant from Diplome;
quit;
# base:: permet de s'assurer que les fonctions proviennent de R base
# Des fonctions du même nom existent en Tidyverse, et tendent à prédominer si le package est lancé

# Identifiants dans Jointure mais pas Diplome
sort(base::setdiff(jointure_rbase$identifiant, diplome_rbase$identifiant))
# dplyr:: permet de s'assurer que ce sont les fonctions du Tidyverse (et non leurs homonymes de R-Base qui sont utilisées)

# Identifiants dans Jointure mais pas Diplome
dplyr::setdiff(jointure_tidyverse$identifiant, diplome_tidyverse$identifiant) %>% 
  sort()
# Les fonctions spécifiques à data.table fonctionnent avec des formats data.table, d'où la syntaxe un peu différente de R base

# Identifiants dans Jointure mais pas Diplome
variable <- "identifiant"
fsetdiff(jointure_datatable[, ..variable], diplome_datatable[, ..variable])[order(identifiant)]
# Identifiants communs des 2 bases
sorted(set(donnees_python['identifiant']) - set(diplome_python['identifiant']))

17.11.5 Identifiants dans Diplome mais pas Jointure

proc sql;
  /* Identifiants dans Diplome mais pas Jointure */
  select distinct Identifiant from Diplome
  where Identifiant not in (select distinct Identifiant from Jointure)
  order by identifiant;
  select Identifiant from Diplome except
  select Identifiant from Jointure order by identifiant;
quit;
# base:: permet de s'assurer que les fonctions proviennent de R base
# Des fonctions du même nom existent en Tidyverse, et tendent à prédominer si le package est lancé

# Identifiants dans Diplome mais pas Jointure
sort(base::setdiff(diplome_rbase$identifiant, jointure_rbase$identifiant))
# dplyr:: permet de s'assurer que ce sont les fonctions du Tidyverse (et non leurs homonymes de R-Base qui sont utilisées)

# Identifiants dans Diplome mais pas Jointure
dplyr::setdiff(diplome_tidyverse$identifiant, jointure_tidyverse$identifiant) %>% 
  sort()
# Les fonctions spécifiques à data.table fonctionnent avec des formats data.table, d'où la syntaxe un peu différente de R base

# Identifiants dans Diplome mais pas Jointure
variable <- "identifiant"
fsetdiff(diplome_datatable[, ..variable], jointure_datatable[, ..variable])[order(identifiant)]
# Identifiants communs des 2 bases
sorted(set(diplome_python['identifiant']) - set(donnees_python['identifiant']))

18 Concaténer et empiler des bases

18.1 Concaténer deux bases de données

On va mettre côte à côte (juxtaposer) le numéro de la ligne et la base de données.

18.1.1 Les deux bases concaténées ont le même nombre de lignes

/* Numéro de la ligne */
proc sql noprint;select count(*) into :tot from donnees_sas;run;
data Ajout;do Num_ligne = 1 to &tot.;output;end;run;

/* Le merge sans by va juxtaposer côte à côte les bases */
data Concatener;merge Ajout donnees_sas;run;
# Numéro de la ligne
ajout <- data.frame(num_ligne = seq_len(nrow(donnees_rbase)))
# cbind si les deux bases comprennent le même nombre de lignes
concatener <- cbind(ajout, donnees_rbase)
# Numéro de la ligne
ajout <- tibble(num_ligne = seq_len(nrow(donnees_tidyverse)))
# bind_cols si les deux bases comprennent le même nombre de lignes
concatener <- donnees_tidyverse %>% bind_cols(ajout)
# Numéro de la ligne
ajout <- data.table(num_ligne = seq_len(nrow(donnees_datatable)))
# data.frame::cbind si les deux bases comprennent le même nombre de lignes
concatener <- data.frame::cbind(ajout, donnees_datatable)
# Numéro de la ligne
ajout = pd.DataFrame({'num_ligne': range(1, len(donnees_python) + 1)})
# Concatener
concatener = pd.concat([ajout, donnees_python], axis=1)

18.1.2 Les deux bases concaténées n’ont pas le même nombre de lignes

/* Si l'une des bases comprend plus de ligne que l'autre, ajout d'une ligne de valeurs manquantes */
proc sql noprint;select count(*) + 1 into :tot from donnees_sas;run;
data Ajout;do Num_ligne = 1 to &tot.;output;end;run;
data Concatener;merge Ajout donnees_sas;run;
# Erreur si l'une des bases comprend plus de lignes que l'autre
ajout <- data.frame(num_ligne = seq_len(nrow(donnees_rbase) + 1))
# donnees_rbase_ajout <- cbind(ajout, donnees_rbase)

# Proposition de solution
cbind_alt <- function(liste) {
  # Nombre maximal de colonnes dans la liste de dataframes
  maxCol <- max(unlist(lapply(liste, nrow)))
  # Ajout d'une colonne de valeurs manquantes pour toutes les bases ayant moins de ligne que le maximum
  res <- lapply(liste, function(x) {
    for (i in seq_len(maxCol - nrow(x))) {
      x[nrow(x) + i, ] <- NA
    }
    return(x)
  })
  # On joint les résultats
  return(do.call(cbind, res))
}
concatener <- cbind_alt(list(ajout, donnees_rbase))
# Ne fonctionne si l'une des bases comprend plus de lignes que l'autre !
ajout <- tibble(num_ligne = seq_len(nrow(donnees_tidyverse) + 1))
#concatener <- donnees_tidyverse %>% bind_cols(ajout)

# cf. solution proposée dans R-Base
cbind_alt <- function(liste) {
  # Nombre maximal de colonnes dans la liste de dataframes
  maxCol <- max(unlist(lapply(liste, nrow)))
  # Ajout d'une colonne de valeurs manquantes pour toutes les bases ayant moins de ligne que le maximum
  res <- lapply(liste, function(x) {
    for (i in seq_len(maxCol - nrow(x))) {
      x[nrow(x) + i, ] <- NA
    }
    return(x)
  })
  # On joint les résultats
  return(bind_cols(res))
}
concatener <- cbind_alt(list(ajout, donnees_tidyverse))
# data.table::cbind fonctionne aussi avec des bases comportant un nombre différent de lignes
# Mais attention, le résultat n'est pas le même que sur SAS, il y a recycling
ajout <- data.table(num_ligne = seq_len(nrow(donnees_datatable) + 1))
concatener <- data.table::cbind(ajout, donnees_datatable)
# Si l'une des bases comprend plus de ligne que l'autre, ajout d'une ligne de valeurs manquantes, comme avec SAS

num_ligne = pd.DataFrame({'num_ligne': range(1, len(donnees_python) + 2)})
concatener = pd.concat([num_ligne, donnees_python], axis=1)

18.2 Empiler deux bases de données

On va empiler la moyenne des notes en dessous de la base des notes.

/* On sélectionne un nombre réduit de variables pour simplifier l'exemple */
%let notes = Note_Contenu Note_Formateur Note_Moyens Note_Accompagnement Note_Materiel;
data Notes;set donnees_sas (keep = identifiant &notes.);run;
/* Moyenne des notes */
proc means data = Notes noprint mean;var &notes.;output out = Ajout mean = &notes.;run;

/* On concatène avec les données. Valeur manquante si les variables ne correspondent pas */

/* L'instruction set permet de concaténer les bases */
data Empiler;set Notes Ajout (drop = _type_ _freq_);run;

/* Autre solution, proc append */
data Empiler;set Notes;run;
proc append base = Empiler data = Ajout force;run;
/* On renomme la ligne des moyennes ajoutée */
data Empiler;
  set Empiler nobs = nobs;
  if _N_ = nobs then Identifiant = "Moyenne";
run;
# On va empiler la somme des notes en dessous de la base des notes
varNotes <- c("note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
# Moyenne des notes
moyennes <- data.frame(t(colMeans(donnees_rbase[, varNotes], na.rm = TRUE)))
# On crée la base des notes
notes <- donnees_rbase[, varNotes]
# rbind lorsque les bases empilées ont le même nombre de colonne
empiler <- rbind(notes, moyennes)

# Mais, ne fonctionne plus si l'on concatène des bases de taille différente
notes <- donnees_rbase[, c("identifiant", varNotes)]
# Ne fonctionne pas
#empiler <- rbind(notes, moyennes)

# Une solution alternative, lorsque le nombre de colonnes diffère entre les deux bases
# Lorsque les variables ne correspondent pas, on les crée avec des valeurs manquantes, via setdiff
rbind_alt <- function(x, y) {
  rbind(data.frame(c(x, sapply(setdiff(names(y), names(x)), function(z) NA))),
        data.frame(c(y, sapply(setdiff(names(x), names(y)), function(z) NA)))
  )
  }
empiler <- rbind_alt(notes, moyennes)
# On renomme la ligne des moyennes ajoutée
empiler[nrow(empiler), "identifiant"] <- "Moyenne"
# On va empiler la somme des notes en dessous de la base des notes
varNotes <- c("note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
# Moyenne des notes
moyennes <- donnees_tidyverse %>% 
  summarise(across(varNotes, ~mean(., na.rm = TRUE)))
empiler <- donnees_tidyverse %>% 
  select(all_of(varNotes)) %>% 
  bind_rows(moyennes)

# Fonctionne toujours si l'on concatène des bases de taille différente
empiler <- donnees_tidyverse %>% 
  select(identifiant, all_of(varNotes)) %>% 
  bind_rows(moyennes)
empiler <- empiler %>% 
  # On renomme la ligne des moyennes ajoutée
  mutate(identifiant = ifelse(row_number() == nrow(empiler),
                              "Moyenne",
                              identifiant))
# On va empiler la somme des notes en dessous de la base des notes
varNotes <- c("note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
# Moyenne des notes
moyennes <- data.table(donnees_datatable[, lapply(.SD, mean, na.rm = TRUE), .SDcols = varNotes])

# On crée la base des notes
notes <- donnees_datatable[, mget(c("identifiant", varNotes))]

# Empilement proprement dit
empiler <- rbindlist(list(notes, moyennes), fill = TRUE)
# On renomme la ligne des moyennes ajoutée
set(empiler, i = nrow(empiler), j = "identifiant", value = "Moyenne")
# On va empiler la somme des notes en dessous de la base des notes
varNotes = ["note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel"]
# Moyenne des notes
moyennes = pd.DataFrame(donnees_python[varNotes].mean()).T
moyennes['identifiant'] = 'Moyenne'
# On crée la base des notes
notes = donnees_python[['identifiant'] + varNotes]

for col in notes.columns:
    if col not in moyennes.columns:
        moyennes[col] = np.nan

# Concaténer les DataFrames
empiler = pd.concat([notes, moyennes], ignore_index=True)

18.3 Ajouter une ligne de valeurs manquantes à une base de données

data Ajout;run;
data Ajout_Missing;set Jointure Ajout;run;
ajout_na <- donnees_rbase
ajout_na[nrow(ajout_na) + 1, ] <- NA
ajout_na <- donnees_tidyverse %>%
  bind_rows(tibble(NA))
ajout_na <- rbindlist(list(donnees_datatable, data.table(NA)), fill = TRUE)

19 Statistiques descriptives

19.1 Moyenne

19.1.1 Moyenne

proc means data = donnees_sas mean;var note_contenu;run;
proc sql;select mean(note_contenu) from donnees_sas;run;
# Importance du na.rm = TRUE en cas de variables manquantes
mean(donnees_rbase$note_contenu)
mean(donnees_rbase$note_contenu, na.rm = TRUE)

# La fonction mean de R est lente. Ecriture plus rapide
sum(donnees_rbase$note_contenu, na.rm = TRUE) / sum(! is.na(donnees_rbase$note_contenu))
# Importance du na.rm = TRUE en cas de variables manquantes
donnees_tidyverse %>% pull(note_contenu) %>% mean()
donnees_tidyverse %>% pull(note_contenu) %>% mean(na.rm = TRUE)

# Autres solutions
# Le chiffre est arrondi lorsqu'il est affiché, du fait des propriétés des tibbles
donnees_tidyverse %>% summarise(mean(note_contenu, na.rm = TRUE))
donnees_tidyverse %>% 
  summarise(across(note_contenu, ~mean(., na.rm = TRUE)))

# Attention, en tidyverse, les syntaxes suivantes ne fonctionnent pas !
# donnees_tidyverse %>% mean(note_contenu)
# donnees_tidyverse %>% mean(note_contenu, na.rm = TRUE)
# Importance du na.rm = TRUE en cas de variables manquantes
donnees_datatable[, mean(note_contenu)]
donnees_datatable[, mean(note_contenu, na.rm = TRUE)]

# Autre solution
donnees_datatable[, lapply(.SD, mean, na.rm = TRUE), .SDcols = "note_contenu"]

19.1.2 Moyenne par sélection

/* Ici pour les seules femmes */
proc means data = donnees_sas mean;
  var note_contenu;
  where sexef = "Femme";
run;
# Ici, pour les seules femmes
with(subset(donnees_rbase, sexef == "Femme"), mean(note_contenu, na.rm = TRUE))
mean(donnees_rbase[donnees_rbase$sexef == "Femme", "note_contenu"], na.rm = TRUE)
# Ici, pour les seules femmes
donnees_tidyverse %>%
  filter(sexef == "Femme") %>% 
  pull(note_contenu) %>%
  mean(na.rm = TRUE)

# Autres solutions
donnees_tidyverse %>%
  filter(sexef == "Femme") %>% 
  summarise(moyenne = mean(note_contenu, na.rm = TRUE))
donnees_tidyverse %>%
  filter(sexef == "Femme") %>% 
  summarise(across(note_contenu, ~ mean(., na.rm = TRUE)))
# Attention, syntaxe qui ne fonctionne qu'avec %>%, pas avec %>% !
donnees_tidyverse %>%
  filter(sexef == "Femme") %>% 
  {mean(.$note_contenu, na.rm = TRUE)}
# Ici, pour les seules femmes
donnees_datatable[sexef == "Femme", mean(note_contenu, na.rm = TRUE)]
donnees_datatable[sexef == "Femme", lapply(.SD, function(x) mean(x, na.rm = TRUE)), .SDcols = "note_contenu"]

19.2 Moyenne pondérée

19.2.1 Moyenne pondérée

proc means data = donnees_sas mean;
  var note_contenu;
  weight poids_sondage;
run;
weighted.mean(donnees_rbase$note_contenu, donnees_rbase$poids_sondage, na.rm = TRUE)

# Autre méthode, mais attention aux NA !!
with(donnees_rbase, sum(note_contenu * poids_sondage, na.rm = TRUE) / sum((!is.na(note_contenu)) * poids_sondage, na.rm = TRUE))
donnees_tidyverse %>%
  summarise(across(note_contenu, ~weighted.mean(., w = poids_sondage, na.rm = TRUE)))
donnees_datatable[, weighted.mean(note_contenu, poids_sondage, na.rm = TRUE)]

19.2.2 Moyenne pondérée par sélection

/* Par sélection (ici pour les seules femmes) */
proc means data = donnees_sas mean;
  var note_contenu;
  where sexef = "Femme";
  weight poids_sondage;
run;
# Par sélection (ici pour les seules femmes)
with(subset(donnees_rbase, sexef == "Femme"), weighted.mean(note_contenu, poids_sondage, na.rm = TRUE))
# Par sélection (ici pour les seules femmes)
donnees_tidyverse %>%
  filter(sexef == "Femme") %>%
  summarise(across(note_contenu, ~weighted.mean(., w = poids_sondage, na.rm = TRUE)))
# Par sélection (ici pour les seules femmes)
donnees_datatable[sexef == "Femme", weighted.mean(note_contenu, poids_sondage, na.rm = TRUE)]
donnees_datatable[sexef == "Femme", lapply(.SD, function(x) weighted.mean(x, poids_sondage, na.rm = TRUE)), .SDcols = "note_contenu"]

19.3 Moyenne de plusieurs variables

%let notes = Note_Contenu Note_Formateur Note_Moyens Note_Accompagnement Note_Materiel;
proc means data = donnees_sas mean;
  var &notes.;
run;
notes <- tolower(c("Note_Contenu", "Note_Formateur", "Note_Moyens", "Note_Accompagnement", "Note_Materiel"))

# Plusieurs solutions

# Sous forme de liste
lapply(donnees_rbase[, notes], mean, na.rm = TRUE)

# Sous forme de vecteur
sapply(donnees_rbase[, notes], mean, na.rm = TRUE)
apply(donnees_rbase[, notes], 2, mean, na.rm = TRUE)

# Si l'on souhaite renommer les colonnes
moyennes <- sapply(donnees_rbase[, notes], mean, na.rm = TRUE)
names(moyennes) <- paste("Moyenne", names(moyennes), sep = "_")
moyennes
notes <- tolower(c("Note_Contenu", "Note_Formateur", "Note_Moyens", "Note_Accompagnement", "Note_Materiel"))
donnees_tidyverse %>%
  summarise(across(all_of(notes), ~ mean(.x, na.rm = TRUE)))
# Si l'on souhaite renommer les colonnes
moyennes <- donnees_tidyverse %>%
  summarise(across(all_of(notes), ~ mean(.x, na.rm = TRUE), .names = "Moyenne_{.col}"))

# Autres solutions
# Obsolètes
donnees_tidyverse %>%
  summarise_at(notes, mean, na.rm = TRUE)
donnees_tidyverse %>%  
  select(starts_with("Note") & !ends_with("_100")) %>% 
  summarise_all(.funs = ~ mean(., na.rm = TRUE), .vars = notes)
# Si l'on souhaite renommer les colonnes
moyennes <- donnees_tidyverse %>%
  summarise_at(notes, mean, na.rm = TRUE) %>% 
  rename_with(~ paste("Moyenne", ., sep = "_"))
notes <- tolower(c("Note_Contenu", "Note_Formateur", "Note_Moyens", "Note_Accompagnement", "Note_Materiel"))
moyennes <- donnees_datatable[, lapply(.SD, mean, na.rm = TRUE), .SDcols = notes]

# Si l'on souhaite renommer les colonnes
setnames(moyennes, notes, paste("Moyenne", notes, sep = "_"))
moyennes

19.4 Moyenne pondérée de plusieurs variables

%let notes = Note_Contenu Note_Formateur Note_Moyens Note_Accompagnement Note_Materiel;
proc means data = donnees_sas mean;
  var &notes.;
  weight poids_sondage;
run;
with(donnees_rbase, sapply(donnees_rbase[, notes], function(x) weighted.mean(x, poids_sondage, na.rm = TRUE)))
notes <- tolower(c("Note_Contenu", "Note_Formateur", "Note_Moyens", "Note_Accompagnement", "Note_Materiel"))
donnees_tidyverse %>%
  summarise(across(notes, ~ weighted.mean(.x, poids_sondage, na.rm = TRUE)))

# Autre solution
donnees_tidyverse %>%
  summarise_at(notes, ~ weighted.mean(.x, poids_sondage, na.rm = TRUE))
moyennes <- donnees_datatable[, lapply(.SD, function(x) weighted.mean(x, poids_sondage, na.rm = TRUE)), .SDcols = notes]
moyennes

19.5 Nombreuses statistiques (somme, moyenne, médiane, mode, etc.)

/* Petite différence avec SAS sur le nombre de lignes du fait des valeurs manquantes */
/* Somme, moyenne, médiane, minimum, maximum, variance, écart-type, nombre de données non manquantes (n), nombre de données manquantes (nmiss), intervalle (entre max et min), mode */
%let notes = Note_Contenu Note_Formateur Note_Moyens Note_Accompagnement Note_Materiel;

/* Par la proc means, en un seul tableau */
proc means data = donnees_sas sum mean median min max var std n nmiss range mode;
  var &notes.;
run;

/* Par la proc univariate, variable par variable */
proc univariate data = donnees_sas;
  var &notes.;
run;
# Somme, moyenne, médiane, minimum, maximum, variance, écart-type, nombre de données (manquantes et non manquantes), nombre de valeurs manquantes, Intervalle (entre max et min), mode
# Petite différence avec SAS sur le nombre de lignes du fait des valeurs manquantes
# Une solution pour obtenir le mode en R est d'utiliser fmode du package collapse
library(collapse)
sapply(donnees_rbase[, notes], function(x) c("Somme"      = sum(x, na.rm = TRUE),
                                             "Moyenne"    = mean(x, na.rm = TRUE),
                                             "Médiane"    = median(x, na.rm = TRUE),
                                             "Min"        = min(x, na.rm = TRUE),
                                             "Max"        = max(x, na.rm = TRUE),
                                             # Pour la variance, la somme des carrés est divisée par n - 1, où n est le nombre de données
                                             "Variance"   = var(x, na.rm = TRUE),
                                             "Ecart-type" = sd(x, na.rm = TRUE),
                                             "N"          = length(x),
                                             "NMiss"      = sum(is.na(x)),
                                             "Intervalle" = max(x, na.rm = TRUE) - min(x, na.rm = TRUE),
                                             "Mode"       = collapse::fmode(x)
))
# Somme, moyenne, médiane, minimum, maximum, variance, écart-type, nombre de données (manquantes et non manquantes), nombre de valeurs manquantes, Intervalle (entre max et min), mode
# Petite différence avec SAS sur le nombre de lignes du fait des valeurs manquantes
# Une solution pour obtenir le mode en R est d'utiliser fmode du package collapse
library(collapse)
notes <- tolower(c("Note_Contenu", "Note_Formateur", "Note_Moyens", "Note_Accompagnement", "Note_Materiel"))
StatsDesc_tidyverse <- function(x) {
  c(
    Somme      = sum(x, na.rm = TRUE),
    Moyenne    = mean(x, na.rm = TRUE),
    Mediane    = median(x, na.rm = TRUE),
    Min        = min(x, na.rm = TRUE),
    Max        = max(x, na.rm = TRUE),
    Variance   = var(x, na.rm = TRUE),
    Ecart_type = sd(x, na.rm = TRUE),
    N          = length(x),
    NMiss      = sum(is.na(x)),
    Intervalle = max(x, na.rm = TRUE) - min(x, na.rm = TRUE),
    Mode       = collapse::fmode(x)
    )
}

# 1ère solution avec les notes en ligne et les statistiques en colonnes
donnees_tidyverse %>% 
  select(all_of(notes)) %>% 
  map(~ StatsDesc_tidyverse(.x)) %>% 
  bind_rows() %>% 
  bind_cols(tibble(Note = c(notes))) %>% 
  relocate(Note)

# 2e solution avec les notes en colonne
donnees_tidyverse %>%
  reframe(across(notes, ~ StatsDesc_tidyverse(.x))) %>% 
  bind_cols(tibble(Indicateur = c("Somme", "Moyenne", "Mediane", "Min", "Max", "Variance",
                                  "Ecart_type", "N", "NMiss", "Intervalle", "Mode"))) %>% 
  relocate(Indicateur)
# Somme, moyenne, médiane, minimum, maximum, variance, écart-type, nombre de données (manquantes et non manquantes), nombre de valeurs manquantes, Intervalle (entre max et min), mode
# Petite différence avec SAS sur le nombre de lignes du fait des valeurs manquantes
notes <- tolower(c("Note_Contenu", "Note_Formateur", "Note_Moyens", "Note_Accompagnement", "Note_Materiel"))
# Une solution pour obtenir le mode en R est d'utiliser fmode du package collapse
library(collapse)
moyennes <- donnees_datatable[, lapply(.SD, function(x) c(sum(x, na.rm = TRUE),
                                                          mean(x, na.rm = TRUE),
                                                          median(x, na.rm = TRUE),
                                                          min(x, na.rm = TRUE),
                                                          max(x, na.rm = TRUE),
                                                          var(x, na.rm = TRUE),
                                                          sd(x, na.rm = TRUE),
                                                          .N,
                                                          sum(is.na(x)),
                                                          max(x, na.rm = TRUE) - min(x, na.rm = TRUE),
                                                          collapse::fmode(x)
                                                          )),
                              .SDcols = notes]
cbind(data.table(Nom = c("Somme", "Moyenne", "Médiane", "Min", "Max", "Variance", "Ecart_type", "N", "NMiss", "Intervalle", "Mode")), moyennes)

# Autre solution
StatsDesc <- function(x) {
  list(
    Variable   = names(x),
    Somme      = lapply(x, sum, na.rm = TRUE),
    Moyenne    = lapply(x, mean, na.rm = TRUE),
    Mediane    = lapply(x, median, na.rm = TRUE),
    Min        = lapply(x, min, na.rm = TRUE),
    Max        = lapply(x, max, na.rm = TRUE),
    Variance   = lapply(x, var, na.rm = TRUE),
    Ecart_type = lapply(x, sd, na.rm = TRUE),
    N          = lapply(x, function(x) length(x)),
    NMiss      = lapply(x, function(x) sum(is.na(x))),
    Intervalle = lapply(x, function(x) max(x, na.rm = TRUE) - min(x, na.rm = TRUE)),
    Mode       = lapply(x, collapse::fmode)
    )
}
donnees_datatable[, StatsDesc(.SD), .SDcols = notes]

19.6 Nombreuses statistiques pondérées (somme, moyenne, médiane, mode, etc.)

/* Somme, moyenne, médiane, minimum, maximum, variance, écart-type, nombre de données non manquantes (n), nombre de données manquantes (nmiss), intervalle, mode */
/* Par la proc means, en un seul tableau */
/* L'option vardef = wgt permet de diviser la variable par la somme des poids et non le nombre de données, pour être cohérent
   avec R */
%let notes = Note_Contenu Note_Formateur Note_Moyens Note_Accompagnement Note_Materiel;

/* Par la proc means, en un seul tableau */
proc means data = donnees_sas sum mean median min max var std n nmiss range mode vardef = wgt;
  var &notes.;
  weight poids_sondage;
run;

/* Par la proc univariate, variable par variable */
proc univariate data = donnees_sas;
  var &notes.;
  weight poids_sondage;
run;
# Une solution pour obtenir les résultats pondérés est d'utiliser les fonctions du package collapse
# L'option na.rm est par défaut à TRUE dans le package
# Attention, dans le package, le poids s'indique par la variable w = 
library(collapse)
sapply(donnees_rbase[, notes], function(x) c("Somme"      = collapse::fsum(x, w = donnees_rbase$poids_sondage),
                                             "Moyenne"    = collapse::fmean(x, w = donnees_rbase$poids_sondage),
                                             "Médiane"    = collapse::fmedian(x, w = donnees_rbase$poids_sondage),
                                             "Min"        = collapse::fmin(x),
                                             "Max"        = collapse::fmax(x),
                                             # Pour la variance, la somme des carrés est divisée par n - 1, où n est le nombre de données
                                             "Variance"   = collapse::fvar(x, w = donnees_rbase$poids_sondage),
                                             "Ecart-type" = collapse::fsd(x, w = donnees_rbase$poids_sondage),
                                             "N"          = collapse::fnobs(x),
                                             "NMiss"      = collapse::fnobs(is.na(x)),
                                             "Intervalle" = collapse::fmax(x) - collapse::fmin(x),
                                             "Mode"       = collapse::fmode(x, w = donnees_rbase$poids_sondage)
))
# Une solution pour obtenir les résultats pondérés est d'utiliser les fonctions du package collapse
# L'option na.rm est par défaut à TRUE dans le package
# Attention, dans le package, le poids s'indique par la variable w = 
library(collapse)
notes <- tolower(c("Note_Contenu", "Note_Formateur", "Note_Moyens", "Note_Accompagnement", "Note_Materiel"))
StatsDescPond_tidyverse <- function(x, w) {
  c(
    Somme      = collapse::fsum(x, w = w),
    Moyenne    = collapse::fmean(x, w = w),
    Mediane    = collapse::fmedian(x, w = w),
    Min        = collapse::fmin(x),
    Max        = collapse::fmax(x),
    Variance   = collapse::fvar(x, w = w),
    Ecart_type = collapse::fsd(x, w = w),
    N          = collapse::fnobs(x),
    NMiss      = collapse::fnobs(is.na(x)),
    Intervalle = collapse::fmax(x) - collapse::fmin(x),
    Mode       = collapse::fmode(x, w = w)
  )
}
donnees_tidyverse %>%
  reframe(across(notes, ~ StatsDescPond_tidyverse(.x, poids_sondage))) %>% 
  bind_cols(tibble(Indicateur = c("Somme", "Moyenne", "Mediane", "Min", "Max", "Variance",
                                  "Ecart_type", "N", "NMiss", "Intervalle", "Mode"))) %>% 
  relocate(Indicateur)

# Autre solution
donnees_tidyverse %>% 
  select(all_of(notes)) %>% 
  map(~StatsDescPond_tidyverse(.x, donnees_tidyverse$poids_sondage)) %>% 
  bind_cols()%>% 
  bind_cols(tibble(Indicateur = c("Somme", "Moyenne", "Mediane", "Min", "Max", "Variance",
                                  "Ecart_type", "N", "NMiss", "Intervalle", "Mode"))) %>% 
  relocate(Indicateur)
# Une solution pour obtenir les résultats pondérés est d'utiliser les fonctions du package collapse
# L'option na.rm est par défaut à TRUE dans le package
# Attention, dans le package, le poids s'indique par la variable w = 
library(collapse)
# À FAIRE : y-a-t-il plus simple ???
# Est-on obligés d'utiliser systématiquement donnees_datatable$poids_sondage ?
notes <- tolower(c("Note_Contenu", "Note_Formateur", "Note_Moyens", "Note_Accompagnement", "Note_Materiel"))

StatsDescPond <- function(x, poids = donnees_datatable$poids_sondage) {
  list(
    Variables  = names(x),
    Somme      = collapse::fsum(x, w = poids),
    Moyenne    = collapse::fmean(x, w = poids),
    Mediane    = collapse::fmedian(x, w = poids),
    Min        = collapse::fmin(x),
    Max        = collapse::fmax(x),
    Variance   = collapse::fvar(x, w = poids),
    Ecart_type = collapse::fsd(x, w = poids),
    N          = collapse::fnobs(x),
    NMiss      = collapse::fnobs(is.na(x)),
    Intervalle = collapse::fmax(x) - collapse::fmin(x),
    Mode       = collapse::fmode(x, w = poids)
  )
}
donnees_datatable[, StatsDescPond(.SD), .SDcols = notes]

19.7 Variance et variance pondérée

19.7.1 Variance

proc means data = donnees_sas var;
  var note_contenu;
run;
var(donnees_rbase$note_contenu)
donnees_tidyverse %>% 
  summarise(var = var(note_contenu))
donnees_datatable[, var(note_contenu)]

19.7.2 Variance pondérée

proc means data = donnees_sas var vardef = wgt;
  var note_contenu;
  weight poids_sondage;
run;
library(collapse)
with(donnees_rbase, collapse::fvar(note_contenu, w = poids_sondage))
library(collapse)
donnees_tidyverse %>% 
  summarise(var = collapse::fvar(note_contenu, w = poids_sondage))
library(collapse)
donnees_datatable[, collapse::fvar(note_contenu, w = poids_sondage)]

19.7.3 Calcul “manuel” de la variance pondérée

La variance pondérée se calcule par la formule : \[ \frac{\sum_i{w_i \times (x_i - \bar{x}) ^2}}{\sum_i{w_i}} = \frac{\sum_i{w_ix_i^2}} {\sum_i{w_i}} - \bar{x}^2 \]

\(x_i\) désigne la variable et \(w_i\) le poids.

On la calcule “manuellement” en R pour confirmer le résultat.

proc means data = donnees_sas var vardef = wgt;
  var note_contenu;
  weight poids_sondage;
run;
x_2 <- with(donnees_rbase,
            sum(poids_sondage * note_contenu**2 * complete.cases(note_contenu, poids_sondage), na.rm = TRUE))
x_m <- with(donnees_rbase,
            sum(poids_sondage * note_contenu * complete.cases(note_contenu, poids_sondage), na.rm = TRUE))
p <- with(donnees_rbase,
          sum(poids_sondage * complete.cases(note_contenu, poids_sondage), na.rm = TRUE))
x_2 / p - (x_m / p) ** 2
x_2 <- donnees_tidyverse %>%  
  summarise(sum(poids_sondage * note_contenu**2 * complete.cases(note_contenu, poids_sondage), na.rm = TRUE)) %>% 
  pull()
x_m <- donnees_tidyverse %>%  
  summarise(sum(poids_sondage * note_contenu * complete.cases(note_contenu, poids_sondage), na.rm = TRUE)) %>% 
  pull()          
p <- donnees_tidyverse %>% 
  summarise(sum(poids_sondage * complete.cases(note_contenu, poids_sondage), na.rm = TRUE)) %>% 
  pull()
x_2 / p - (x_m / p) ** 2
x_2 <- donnees_datatable[, sum(poids_sondage * note_contenu**2 * complete.cases(note_contenu, poids_sondage),
                               na.rm = TRUE)]
x_m <- donnees_datatable[, sum(poids_sondage * note_contenu * complete.cases(note_contenu, poids_sondage),
                               na.rm = TRUE)]
p <- donnees_datatable[, sum(poids_sondage * complete.cases(note_contenu, poids_sondage), na.rm = TRUE)]
x_2 / p - (x_m / p) ** 2

19.8 Déciles et quantiles

/* On calcule déjà la moyenne des notes par individu */
%let notes = Note_Contenu Note_Formateur Note_Moyens Note_Accompagnement Note_Materiel;
data donnees_sas;
  set donnees_sas;
  Note_moyenne = mean(of &notes.);
run;

/* Déciles et quartiles de la note moyenne */

/* Par la proc means */
proc means data = donnees_sas StackODSOutput Min P10 P20 P30 P40 Median P60 P70 Q3 P80 P90 Max Q1 Median Q3 QRANGE;
  var Note_moyenne;
  ods output summary = Deciles_proc_means;
run;

/* Par la proc univariate */
proc univariate data = donnees_sas;
  var Note_moyenne;
  output out = Deciles_proc_univariate pctlpts=00 to 100 by 10 25 50 75 PCTLPRE=_; 
run;
# On calcule déjà la moyenne des notes par individu
notes <- tolower(c("Note_Contenu", "Note_Formateur", "Note_Moyens", "Note_Accompagnement", "Note_Materiel"))
donnees_rbase$note_moyenne <- rowMeans(donnees_rbase[, notes], na.rm = TRUE)

# Et les quantiles (déciles et quartiles)
quantile(donnees_rbase$note_moyenne, probs = c(seq(0, 1, 0.1), 0.25, 0.5, 0.75), na.rm = TRUE)

# Intervalle inter-quartile
IQR(donnees_rbase$note_moyenne, na.rm = TRUE)
# on calcule déjà la moyenne des notes par individu
varNotes <- c("note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
donnees_tidyverse <- donnees_tidyverse %>%
  mutate(note_moyenne = rowMeans(across(all_of(varNotes)), na.rm = TRUE))

# Et les quantiles (déciles et quartiles)
donnees_tidyverse %>%
  pull(note_moyenne) %>% 
  quantile(probs = c(seq(0, 1, 0.1), 0.25, 0.5, 0.75), na.rm = TRUE)

# Intervalle inter-quartile
donnees_tidyverse %>%
  pull(note_moyenne) %>% 
  IQR(na.rm = TRUE)
# on calcule déjà la moyenne des notes par individu
notes <- c("note_contenu","note_formateur","note_moyens","note_accompagnement","note_materiel")
donnees_datatable[, note_moyenne := rowMeans(.SD, na.rm = TRUE), .SDcols = notes]

# Et les quantiles (déciles et quartiles)
donnees_datatable[, quantile(.SD, probs = c(seq(0, 1, 0.1), 0.25, 0.5, 0.75), na.rm = TRUE), .SDcols = "note_moyenne"]
donnees_datatable[, lapply(.SD, quantile, probs = c(seq(0, 1, 0.1), 0.25, 0.5, 0.75), na.rm = TRUE), .SDcols = "note_moyenne"]

# Intervalle inter-quartile
donnees_datatable[, IQR(note_moyenne, na.rm = TRUE)]
donnees_datatable[, lapply(.SD, function(x) IQR(x, na.rm = TRUE)), .SDcols = "note_moyenne"]

19.9 Déciles et quantiles pondérés

/* On calcule déjà la moyenne des notes par individu */
%let notes = Note_Contenu Note_Formateur Note_Moyens Note_Accompagnement Note_Materiel;
data donnees_sas;
  set donnees_sas;
  Note_moyenne = mean(of &notes.);
run;

/* Par la proc means */
proc means data = donnees_sas StackODSOutput Min P10 P20 P30 P40 Median P60 P70 Q3 P80 P90 Max Q1 Median Q3 QRANGE;
  var Note_moyenne;
  ods output summary = Deciles_proc_means;
  weight poids_sondage;
run;

/* Par la proc univariate */
proc univariate data = donnees_sas;
  var Note_moyenne;
  output out = Deciles_proc_univariate pctlpts=00 to 100 by 10 25 50 75 PCTLPRE=_;
  weight poids_sondage;
run;
# Une solution pour obtenir les résultats pondérés est d'utiliser la fonction fquantile du package collapse
# L'option na.rm est par défaut à TRUE dans le package
library(collapse)

# On calcule déjà la moyenne des notes par individu
notes <- tolower(c("Note_Contenu", "Note_Formateur", "Note_Moyens", "Note_Accompagnement", "Note_Materiel"))
donnees_rbase$note_moyenne <- rowMeans(donnees_rbase[, notes], na.rm = TRUE)

# Les quantiles (déciles et quartiles)
collapse::fquantile(donnees_rbase$note_moyenne, w = donnees_rbase$poids_sondage,
                    probs = c(seq(0, 1, 0.1), 0.25, 0.5, 0.75))
# Une solution pour obtenir les résultats pondérés est d'utiliser les fonctions du package collapse
# L'option na.rm est par défaut à TRUE dans le package
library(collapse)

# On calcule déjà la moyenne des notes par individu
varNotes <- c("note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
donnees_tidyverse <- donnees_tidyverse %>%
  mutate(note_moyenne = rowMeans(across(all_of(varNotes)), na.rm = TRUE))

# Les quantiles (déciles et quartiles)
donnees_tidyverse %>%
  pull(note_moyenne) %>% 
  collapse::fquantile(probs = c(seq(0, 1, 0.1), 0.25, 0.5, 0.75), w = donnees_tidyverse$poids_sondage)
# Une solution pour obtenir les résultats pondérés est d'utiliser la fonction fquantile du package collapse
# L'option na.rm est par défaut à TRUE dans le package
library(collapse)

# On calcule déjà la moyenne des notes par individu
notes <- c("note_contenu","note_formateur","note_moyens","note_accompagnement","note_materiel")
donnees_datatable[, note_moyenne := rowMeans(.SD, na.rm = TRUE), .SDcols = notes]

# Les quantiles (déciles et quartiles)
donnees_datatable[, lapply(.SD, function(x) collapse::fquantile(x, w = poids_sondage,
                                                                probs = c(seq(0, 1, 0.1), 0.25, 0.5, 0.75)
                                                                )),
                           .SDcols = "note_moyenne"]

19.10 Rang de la note

Ajouter dans la base le rang de la note par ordre décroissant.

/* On calcule déjà la moyenne des notes par individu */
%let notes = Note_Contenu Note_Formateur Note_Moyens Note_Accompagnement Note_Materiel;
data donnees_sas;
  set donnees_sas;
  Note_moyenne = mean(of &notes.);
run;

proc rank data = donnees_sas out = donnees_sas descending;
  var note_moyenne;
  ranks rang_note_moyenne;
run;
# On calcule déjà la moyenne des notes par individu
notes <- tolower(c("Note_Contenu", "Note_Formateur", "Note_Moyens", "Note_Accompagnement", "Note_Materiel"))
donnees_rbase$note_moyenne <- rowMeans(donnees_rbase[, notes], na.rm = TRUE)

# Attention, en R, rank trie par ordre croissant par défaut, alors que le tri est par ordre décroissant en SAS
# On exprime le rang par ordre décroissant, avec le - devant
donnees_rbase$rang_note_moyenne <- rank(-donnees_rbase$note_moyenne)
# On calcule déjà la moyenne des notes par individu
varNotes <- c("note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
donnees_tidyverse <- donnees_tidyverse %>%
  mutate(note_moyenne = rowMeans(across(all_of(varNotes)), na.rm = TRUE)) %>% 
  # Attention, en R, rank trie par ordre croissant par défaut, alors que le tri est par ordre décroissant en SAS
  # On exprime le rang par ordre décroissant, avec le - devant
  mutate(rang_note_moyenne = rank(-note_moyenne))
# On calcule déjà la moyenne des notes par individu
notes <- c("note_contenu","note_formateur","note_moyens","note_accompagnement","note_materiel")
donnees_datatable[, note_moyenne := rowMeans(.SD, na.rm = TRUE), .SDcols = notes]

# Attention, en R, frank trie par ordre croissant par défaut, alors que le tri est par ordre décroissant en SAS
# On exprime le rang par ordre décroissant, avec le - devant
donnees_datatable[, rang_note_moyenne := frank(-note_moyenne)]

19.11 Covariance et corrélation linéaire

19.11.1 Covariance entre deux notes

/* Covariance et corrélation linéaire (Kendall, Pearson, Spearman) */
proc corr data = donnees_sas kendall pearson spearman cov;
  var note_contenu note_formateur;
run;
# Covariance (Kendall, Pearson, Spearman)
with(donnees_rbase,
     sapply(c("pearson", "spearman", "kendall"),
            function(x) cov(note_contenu, note_formateur, method = x, use = "complete.obs")))
# Covariance (Kendall, Pearson, Spearman)
methodes <- c("pearson", "spearman", "kendall")
methodes %>% 
  purrr::map(~ 
    donnees_tidyverse %>% 
      summarise(cov = cov(note_contenu, note_formateur, method = .x, use = "complete.obs"))) %>% 
  setNames(methodes) %>% 
  as_tibble()
# Covariance (Kendall, Pearson, Spearman)
methodes <- c("pearson", "spearman", "kendall")
setNames(donnees_datatable[, lapply(methodes,
                                    function(x) cov(note_contenu, note_formateur,
                                                    method = x,
                                                    use = "complete.obs"))],
         methodes)

19.11.2 Corrélation linéaire entre deux notes

/* Corrélation linéaire (Kendall, Pearson, Spearman) */
proc corr data = donnees_sas kendall pearson spearman cov;
  var note_contenu note_formateur;
run;
# Corrélation linéaire (Kendall, Pearson, Spearman)
with(donnees_rbase,
     sapply(c("pearson", "spearman", "kendall"),
            function(x) cor(note_contenu, note_formateur, method = x, use = "complete.obs")))
# Corrélation linéaire (Kendall, Pearson, Spearman)
methodes <- c("pearson", "spearman", "kendall")
methodes %>% 
  purrr::map(~ donnees_tidyverse %>% 
               summarise(cor = cor(note_contenu, note_formateur, method = .x, use = "complete.obs"))) %>% 
  setNames(methodes) %>% 
  as_tibble()
# Corrélation linéaire (Kendall, Pearson, Spearman)
methodes <- c("pearson", "spearman", "kendall")
setNames(donnees_datatable[, lapply(methodes,
                                    function(x) cor(note_contenu, note_formateur,
                                                    method = x,
                                                    use = "complete.obs"))],
         methodes)

19.12 Centrer et réduire les variables

On souhaite centrer (moyenne nulle) et réduire (écart-type égal à 1) les notes.

%let notes = Note_Contenu Note_Formateur Note_Moyens Note_Accompagnement Note_Materiel;
data centrer_reduire;set donnees_sas (keep = &notes.);run;
proc stdize data = centrer_reduire out = centrer_reduire method = std;
   var &notes.;
run;
notes <- tolower(c("Note_Contenu", "Note_Formateur", "Note_Moyens", "Note_Accompagnement", "Note_Materiel"))
centrer_reduire <- scale(donnees_rbase[, notes])

# Autre solution avec les fonctions apply et sweep
# Centrer la base
centrer <- sweep(donnees_rbase[, notes], 2, colMeans(donnees_rbase[, notes], na.rm = TRUE), FUN = "-")
# Réduire la base
# On calcule déjà l'écart-type par colonne
ecart_type <- t(apply(centrer, 2, sd, na.rm = TRUE))
# Et on réduit
centrer_reduire < sweep(centrer, 2, ecart_type, "/")
notes <- tolower(c("Note_Contenu", "Note_Formateur", "Note_Moyens", "Note_Accompagnement", "Note_Materiel"))
centrer_reduire <- donnees_tidyverse %>% 
  select(all_of(notes)) %>% 
  mutate(across(notes, ~ (.x - mean(.x, na.rm = TRUE)) / sd(.x, na.rm = TRUE) ))
notes <- tolower(c("Note_Contenu", "Note_Formateur", "Note_Moyens", "Note_Accompagnement", "Note_Materiel"))
centrer_reduire <- donnees_datatable[, lapply(.SD, function(x) (x - mean(x, na.rm = TRUE)) / sd(x, na.rm = TRUE)), .SDcols = notes]

20 Tableaux de fréquence (proc freq de SAS) pour 1 variable

20.1 Tableaux de fréquence pour 1 variable

proc freq data = donnees_sas;
  tables Sexe CSP;
  format Sexe sexef. CSP $cspf.;
run;
# Tableaux de fréquence (proc freq) (sans les valeurs manquantes)
table(donnees_rbase$cspf)
table(donnees_rbase$sexef)
# Autre syntaxe, donnant une mise en forme différente
ftable(donnees_rbase$cspf)
# Pour enlever les "donnees_rbase$", on peut utiliser with pour se placer dans l'environnement de donnees_rbase
with(donnees_rbase, table(cspf))

# Pour les proportions
prop.table(table(donnees_rbase$cspf)) * 100

# Devient plus difficile si l'on souhaite plus (sommes et proportions cumulées par exemple)
freq <- setNames(as.data.frame(table(donnees_rbase$cspf)), c("cspf", "Freq"))
freq <- transform(freq, Prop = Freq / sum(Freq) * 100)
freq <- transform(freq,
                  Freq_cum = cumsum(Freq),
                  Prop_cum = cumsum(Prop))
freq
donnees_tidyverse %>% 
  count(cspf) %>% 
  mutate(prop = n / sum(n) * 100,
         n_cum = cumsum(n),
         prop_cum = cumsum(prop))

# Ou alors
donnees_tidyverse %>% 
  group_by(cspf) %>% 
  summarise(n = n()) %>% 
  mutate(prop = n / sum(n) * 100,
         n_cum = cumsum(n),
         prop_cum = cumsum(prop)
  )
# Tableaux de fréquence (proc freq) (avec les valeurs manquantes)
donnees_datatable[, table(cspf) ]
donnees_datatable[, table(sexef) ]

# Pour les proportions
donnees_datatable[, prop.table(table(cspf)) ] * 100
donnees_datatable[, .(Nombre = .N,
                      Pourcentage = .N / length(donnees_datatable[, cspf]) * 100),
                  keyby = cspf]
donnees_datatable[, {tot = .N; .SD[, .(frac = .N / tot * 100), keyby = cspf]} ]

# Autre façon d'utiliser les méthodes de data.table, avec les fréquences et proportions cumulés
tab <- data.table::dcast(donnees_datatable, cspf ~ ., fun = length)
colnames(tab)[colnames(tab) == "."] <- "Nombre"
tab[, Prop := lapply(.SD, function(col) col / sum(col) * 100), .SDcols = is.numeric]
tab[, c("Freq_cum", "Prop_cum") := list(cumsum(Nombre), cumsum(Prop))]

20.2 Tableaux de fréquence avec les valeurs manquantes

proc freq data = donnees_sas;
  tables Sexe CSP / missing;
  format Sexe sexef. CSP $cspf.;
run;
table(donnees_rbase$cspf, useNA = "always")
prop.table(table(donnees_rbase$cspf, useNA = "always")) * 100
donnees_tidyverse %>% 
  count(cspf) %>% 
  mutate(prop = n / sum(n) * 100)
donnees_datatable[, table(cspf, useNA = "always") ]
donnees_datatable[, prop.table(table(cspf, useNA = "always"))] * 100
donnees_datatable[, .(Nombre = .N,
                      Pourcentage = .N / length(donnees_datatable[, cspf]) * 100),
                  keyby = cspf]
donnees_datatable[, {tot = .N; .SD[, .(frac = .N / tot * 100), keyby = cspf]} ]

20.3 Tableaux de fréquence triés par la modalité la plus courante

proc freq data = donnees_sas order = freq;
  tables Sexe CSP / missing;
  format Sexe sexef. CSP $cspf.;
run;
freq <- setNames(as.data.frame(table(donnees_rbase$cspf)), c("cspf", "Freq"))
freq <- transform(freq, Prop = Freq / sum(Freq) * 100)
freq[order(-freq$Freq), ]
donnees_tidyverse %>% 
  count(cspf) %>% 
  arrange(desc(n)) %>% 
  mutate(prop = n / sum(n) * 100,
         n_cum = cumsum(n),
         prop_cum = cumsum(prop))

# Autre solution
donnees_tidyverse %>% 
  count(cspf, sort = TRUE) %>% 
  mutate(prop = n / sum(n) * 100,
         n_cum = cumsum(n),
         prop_cum = cumsum(prop))
donnees_datatable[, .(Nombre = .N,
                      Pourcentage = .N / length(donnees_datatable[, cspf]) * 100),
                  keyby = cspf][order(-Nombre)]

20.4 Tableaux de fréquence avec la pondération

proc freq data = donnees_sas;
  tables Sexe CSP / missing;
  format Sexe sexef. CSP $cspf.;
  weight poids_sondage;
run;
xtabs(poids_sondage ~ cspf, data = donnees_rbase, addNA = TRUE)
prop.table(xtabs(poids_sondage ~ cspf, data = donnees_rbase, addNA = TRUE))
donnees_tidyverse %>% 
  count(cspf, wt = poids_sondage) %>% 
  mutate(prop = n / sum(n) * 100,
         n_cum = cumsum(n),
         prop_cum = cumsum(prop))
donnees_datatable[, xtabs(poids_sondage ~ cspf, data = donnees_datatable, addNA = TRUE) ]
donnees_datatable[, prop.table(xtabs(poids_sondage ~ cspf, data = donnees_datatable, addNA = TRUE)) ]
donnees_datatable[, .(prop = sum(poids_sondage, na.rm = TRUE) / sum(donnees_datatable[, poids_sondage]) * 100), keyby = cspf]
donnees_datatable[, {tot = sum(poids_sondage, na.rm = TRUE); .SD[, .(prop = sum(poids_sondage, na.rm = TRUE) / tot * 100), by = cspf]} ]

21 Tableaux de contingence (proc freq de SAS) pour 2 variables

Cette tâche, immédiate en SAS, est plus complexe à réaliser en R. Plusieurs stratégies, avec et sans packages, sont proposées ici.

21.1 Tableaux de contingence pour 2 variables

proc freq data = donnees_sas;
  tables Sexe * CSP / missing;
  format Sexe sexef. CSP $cspf.;
run;

/* Tableau de contingence (tableau croisé) sans les proportions lignes, colonnes et totales */
proc freq data = donnees_sas;
  tables CSP * Sexe  / missing nofreq norow nocol;
  format Sexe sexef. CSP $cspf.;
run;
# Tableau simple
table(donnees_rbase$cspf, donnees_rbase$sexef, useNA = "always")
# Tableau avec les sommes
addmargins(table(donnees_rbase$cspf, donnees_rbase$sexef, useNA = "always"))

# Proportions
tab <- table(donnees_rbase$cspf, donnees_rbase$sexef, useNA = "always")
# Proportions par case
addmargins(prop.table(tab)) * 100
# Proportions par ligne
addmargins(prop.table(tab, margin = 1)) * 100
# Proportions par colonne
addmargins(prop.table(tab, margin = 2)) * 100

# Solution alternative, sans pondération
tab <- xtabs(~ cspf + sexef, data = donnees_rbase)
addmargins(prop.table(tab)) * 100
addmargins(prop.table(tab, margin = 1), margin = 2) * 100
addmargins(prop.table(tab, margin = 2), margin = 1) * 100
# Tableau de fréquence
tab <- donnees_tidyverse %>% 
  group_by(cspf, sexef) %>% 
  summarise(prop = n(), .groups = "drop_last") %>% 
  ungroup() %>% 
  mutate(prop = prop / sum(prop) * 100) %>%
  spread(sexef, prop) %>% 
  mutate(Total = rowSums(across(where(is.numeric)), na.rm = TRUE))
tab <- bind_rows(tab, tab %>% 
                   summarise(across(where(is.numeric), \(x) sum(x, na.rm = TRUE)),
                             across(where(is.character), ~"Total"))
)
tab

# Proportions par ligne
donnees_tidyverse %>% 
  group_by(cspf, sexef) %>% 
  summarise(prop = n()) %>% 
  mutate(prop = prop / sum(prop) * 100) %>% 
  spread(sexef, prop)

# Proportions par colonne
donnees_tidyverse %>% 
  group_by(sexef, cspf) %>% 
  summarise(prop = n()) %>% 
  mutate(prop = prop / sum(prop) * 100) %>% 
  spread(sexef, prop)

1ère solution

# Tableau simple
donnees_datatable[, table(cspf, sexef, useNA = "always") ]
# Tableau avec les sommes
donnees_datatable[, addmargins(table(cspf, sexef, useNA = "always")) ]
# Proportions
tab <- donnees_datatable[, table(cspf, sexef, useNA = "always") ]
# Proportions par case
addmargins(prop.table(tab)) * 100
# Proportions par ligne
addmargins(prop.table(tab, margin = 1)) * 100
# Proportions par colonne
addmargins(prop.table(tab, margin = 2)) * 100

2e solution

# Solution alternative, sans pondération
tab <- donnees_datatable[, xtabs(~ cspf + sexef, data = donnees_datatable) ]
tab
addmargins(prop.table(tab)) * 100
addmargins(prop.table(tab, margin = 1), margin = 2) * 100
addmargins(prop.table(tab, margin = 2), margin = 1) * 100

3e solution

# Autre solution, avec les Grouping sets
tab <- data.table::cube(donnees_datatable, .(Nb = .N), by = c("cspf", "sexef"))
tab <- data.table::dcast(tab, cspf ~ sexef, value.var = "Nb")
# On harmonise le tableau
tab <- rbind(tab[2:nrow(tab)], tab[1,])
setcolorder(tab, c(setdiff(names(tab), "NA"), "NA"))
# On renomme la ligne et la colonne des totaux
tab[nrow(tab), 1] <- "Total"
names(tab)[which(names(tab) == "NA")] <- "Total"
tab

4e solution

# Autre façon d'utiliser les méthodes de data.table
tab_prop <- data.table::dcast(donnees_datatable, cspf ~ sexef, fun.aggregate = length)
# Proportion par ligne
tab_prop[, .SD / Reduce(`+`, .SD), cspf]
# Proportion par colonne
cols <- unique(donnees_datatable[, (sexef)])
tab_prop[, (lapply(.SD, function(col) col / sum(col))), .SDcols = cols]

# Pour avoir les sommes lignes
# À FAIRE : ne marche pas, à revoir !
#tab_prop <- data.table::dcast(donnees_datatable, cspf ~ sexef, fun.aggregate = length)
#tab_prop[, Total := rowSums(.SD), .SDcols = is.numeric]
#tab_prop <- rbind(tab_prop, tab_prop[, c(cspf = "Total", lapply(.SD, sum, na.rm = TRUE)),
#                                     .SDcols = is.numeric],
#                  fill = TRUE)
#tab_prop[, (lapply(.SD, function(col) col / sum(col))), .SDcols = -1]
## Pour avoir les sommes colonnes
#tab[, sum(.SD), by = 1:nrow(tab), .SDcols = is.numeric]
#tab[, (lapply(.SD, function(col) col / sum(col))), .SDcols = -1]
#
## Autre solution plus pratique avec data.table
## Manipuler des formules sur R
#variable <- c("cspf", "sexef")
#formule <- as.formula(paste(paste(variable, collapse = " + "), ".", sep = " ~ "))
#tab_prop <- data.table::dcast(donnees_datatable, formule, fun.aggregate = length)
#colnames(tab_prop)[colnames(tab_prop) == "."] <- "total"
#tab_prop[, prop := total / sum(total)]
## Le tableau est remis sous forme croisée
#tab_prop <- dcast(tab_prop, cspf ~ sexef, value.var = c("prop"), fill = 0)

21.2 Tableau de contingence avec pondération

proc freq data = donnees_sas;
  tables Sexe * CSP / missing;
  format Sexe sexef. CSP $cspf.;
  weight poids_sondage;
run;
tab <- xtabs(poids_sondage ~ cspf + sexef, data = donnees_rbase, addNA = TRUE)
tab
addmargins(prop.table(tab)) * 100
addmargins(prop.table(tab, margin = 1), margin = 2) * 100
addmargins(prop.table(tab, margin = 2), margin = 1) * 100
# Avec la fonction count
donnees_tidyverse %>% 
  count(cspf, sexef, wt = poids_sondage, name = "prop") %>% 
  ungroup() %>% 
  mutate(prop = prop / sum(prop) * 100) %>% 
  spread(sexef, prop)

# Avec la fonction summarise
donnees_tidyverse %>% 
  group_by(cspf, sexef) %>% 
  summarise(prop = sum(poids_sondage, na.rm = TRUE)) %>% 
  ungroup() %>% 
  mutate(prop = prop / sum(prop) * 100) %>% 
  spread(sexef, prop)

# Avec ajout des sommes par ligne et colonne
tab <- donnees_tidyverse %>% 
  count(cspf, sexef, wt = poids_sondage, name = "prop") %>% 
  ungroup() %>% 
  mutate(prop = prop / sum(prop) * 100) %>% 
  pivot_wider(names_from = sexef, values_from = prop) %>% 
  # Somme par ligne
  mutate(Total = rowSums(across(where(is.numeric)), na.rm = TRUE))
# Somme par colonne
tab <- bind_rows(tab, tab %>% 
                   summarise(across(where(is.numeric), sum, na.rm = TRUE),
                             across(where(is.character), ~"Total"))
            )
tab
tab <- donnees_datatable[, xtabs(poids_sondage ~ cspf + sexef, data = donnees_datatable, addNA = TRUE) ]
tab
addmargins(prop.table(tab)) * 100
addmargins(prop.table(tab, margin = 1), margin = 2) * 100
addmargins(prop.table(tab, margin = 2), margin = 1) * 100

21.3 Copier-coller le tableau dans un tableur (Excel, etc.)

/* Copier-coller le résultat sur la fenêtre html "Results Viewer" */
proc freq data = donnees_sas;
  tables Sexe * CSP / missing chisq;
  format Sexe sexef. CSP $cspf.;
run;
# On utilise les packages knitr et kableExtra
library(knitr)
library(kableExtra)

# Création d'un tableau
tab <- xtabs(~ cspf + sexef, data = donnees_rbase)
tab <- addmargins(prop.table(tab)) * 100

# Afficher de façon plus jolie un tableau
knitr::kable(tab)

# Copier-coller le résultat vers Excel
# Il suffit d'appliquer ce code ....
kableExtra::kable_paper(kableExtra::kbl(tab), "hover", full_width = F)
# ... et de copier-coller le résultat de la fenêtre Viewer vers Excel
# On utilise les packages knitr et kableExtra
library(knitr)
library(kableExtra)

# Création d'un tableau
tab <- donnees_tidyverse %>% 
  group_by(cspf, sexef) %>% 
  summarise(prop = n(), .groups = "drop_last") %>% 
  ungroup() %>% 
  mutate(prop = prop / sum(prop) * 100) %>% 
  spread(sexef, prop)

# Afficher de façon plus jolie un tableau
tab %>% knitr::kable()

# Copier-coller le résultat vers Excel
# Il suffit d'appliquer ce code ....
tab %>% 
  knitr::kable() %>% 
  kableExtra::kable_paper("hover", full_width = F)
# ... et de copier-coller le résultat de la fenêtre Viewer vers Excel
# On utilise les packages knitr et kableExtra
library(knitr)
library(kableExtra)

# Création d'un tableau
tab <- donnees_datatable[, xtabs(poids_sondage ~ cspf + sexef, data = donnees_datatable, addNA = TRUE) ]
tab <- 
addmargins(prop.table(tab)) * 100

# Afficher de façon plus jolie un tableau
knitr::kable(tab)

# Copier-coller le résultat vers Excel
# Il suffit d'appliquer ce code ....
kableExtra::kable_paper(kableExtra::kbl(tab), "hover", full_width = F)
# ... et de copier-coller le résultat de la fenêtre Viewer vers Excel

21.4 Tests d’associaton (Chi-Deux, etc.)

proc freq data = donnees_sas;
  tables Sexe * CSP / missing chisq;
  format Sexe sexef. CSP $cspf.;
run;
# Test du Khi-Deux
with(donnees_rbase, chisq.test(cspf, sexef))
summary(table(donnees_rbase$cspf, donnees_rbase$sexef))
# Test du Khi-Deux
chisq.test(donnees_tidyverse %>% pull(cspf), donnees_tidyverse %>% pull(sexef))
# Test du Khi-Deux
donnees_datatable[, chisq.test(cspf, sexef)]

21.5 Solutions avec package R permettant de pondérer

Autre possibilité, avec package R, pour avoir la même présentation que la proc freq de SAS.

5 packages paraissent pertinents : descr, flextable, questionr, survey, procs.

Des informations sur l’usage des packages en R sont disponibles sur le site Utilit’R : https://book.utilitr.org/03_Fiches_thematiques/Fiche_comment_choisir_un_package.html.

21.5.1 Package descr

Lien vers la documentation : https://cran.r-project.org/web/packages/descr/descr.pdf.

/* Sans objet pour SAS */
proc freq data = donnees_sas;
  tables Sexe * CSP / missing;
  format Sexe sexef. CSP $cspf.;
  weight poids_sondage;
run;
library(descr)
# Non pondéré
with(donnees_rbase, descr::crosstab(cspf, sexef,                         prop.r = TRUE, prop.c = TRUE, prop.t = TRUE))
# Pondéré
with(donnees_rbase, descr::crosstab(cspf, sexef, weight = poids_sondage, prop.r = TRUE, prop.c = TRUE, prop.t = TRUE))
# Sans les proportions par ligne et colonne
with(donnees_rbase, descr::crosstab(cspf, sexef, weight = poids_sondage, prop.r = FALSE, prop.c = FALSE, prop.t = TRUE))
library(descr)
# Non pondéré
with(donnees_tidyverse, descr::crosstab(cspf, sexef,                         prop.r = TRUE, prop.c = TRUE, prop.t = TRUE))
# Pondéré
with(donnees_tidyverse, descr::crosstab(cspf, sexef, weight = poids_sondage, prop.r = TRUE, prop.c = TRUE, prop.t = TRUE))
# Sans les proportions par ligne et colonne
with(donnees_tidyverse, descr::crosstab(cspf, sexef, weight = poids_sondage, prop.r = FALSE, prop.c = FALSE, prop.t = TRUE))
library(descr)
# Non pondéré
donnees_datatable[, descr::crosstab(cspf, sexef,                         prop.r = TRUE, prop.c = TRUE, prop.t = TRUE)]
# Pondéré
donnees_datatable[, descr::crosstab(cspf, sexef, weight = poids_sondage, prop.r = TRUE, prop.c = TRUE, prop.t = TRUE)]
# Sans les proportions par ligne et colonne
donnees_datatable[, descr::crosstab(cspf, sexef, weight = poids_sondage, prop.r = FALSE, prop.c = FALSE, prop.t = TRUE)]

21.5.2 Package flextable

Lien vers la documentation :

/* Sans objet pour SAS */
proc freq data = donnees_sas;
  tables Sexe * CSP / missing;
  format Sexe sexef. CSP $cspf.;
  weight poids_sondage;
run;
library(flextable)
# Non pondéré
flextable::proc_freq(donnees_rbase, "cspf", "sexef")
# Pondéré
flextable::proc_freq(donnees_rbase, "cspf", "sexef", weight = "poids_sondage")
# Sans les proportions par ligne et colonne
flextable::proc_freq(donnees_rbase, "cspf", "sexef", weight = "poids_sondage", include.row_percent = FALSE, include.column_percent = FALSE)
library(flextable)
# Non pondéré
flextable::proc_freq(donnees_tidyverse, "cspf", "sexef")
# Pondéré
flextable::proc_freq(donnees_tidyverse, "cspf", "sexef", weight = "poids_sondage")
# Sans les proportions par ligne et colonne
flextable::proc_freq(donnees_tidyverse, "cspf", "sexef", weight = "poids_sondage", include.row_percent = FALSE, include.column_percent = FALSE)
library(flextable)
# Non pondéré
flextable::proc_freq(donnees_datatable, "cspf", "sexef")
# Pondéré
flextable::proc_freq(donnees_datatable, "cspf", "sexef", weight = "poids_sondage")
# Sans les proportions par ligne et colonne
flextable::proc_freq(donnees_datatable, "cspf", "sexef", weight = "poids_sondage", include.row_percent = FALSE, include.column_percent = FALSE)

21.5.3 Package questionr

Lien vers la documentation : https://cran.r-project.org/web/packages/questionr/questionr.pdf.

/* Sans objet pour SAS */
proc freq data = donnees_sas;
  tables Sexe * CSP / missing;
  format Sexe sexef. CSP $cspf.;
  weight poids_sondage;
run;
library(questionr)
# Tableau croisé
# Sans pondération
tab <- with(donnees_rbase, questionr::wtd.table(cspf, sexef, useNA = "ifany"), na.rm = TRUE)
# Avec pondération
tab <- with(donnees_rbase, questionr::wtd.table(cspf, sexef, weights = poids_sondage, useNA = "ifany"), na.rm = TRUE)
tab
# Proportions
questionr::prop(tab)
# Proportions colonnes
questionr::cprop(tab)
# Proportions lignes
questionr::rprop(tab)
# Sans pondération
library(questionr)
# Tableau croisé
# Sans pondération
tab <- with(donnees_tidyverse, questionr::wtd.table(cspf, sexef, useNA = "ifany"), na.rm = TRUE)
# Avec pondération
tab <- with(donnees_tidyverse, questionr::wtd.table(cspf, sexef, weights = poids_sondage, useNA = "ifany"), na.rm = TRUE)
tab
# Proportions
questionr::prop(tab)
# Proportions colonnes
questionr::cprop(tab)
# Proportions lignes
questionr::rprop(tab)
# Sans pondération
library(questionr)
# Tableau croisé
# Sans pondération
tab <- donnees_datatable[, questionr::wtd.table(cspf, sexef, useNA = "ifany")]
# Avec pondération
tab <- donnees_datatable[, questionr::wtd.table(cspf, sexef, weights = poids_sondage, useNA = "ifany")]
tab
# Proportions
questionr::prop(tab)
# Proportions colonnes
questionr::cprop(tab)
# Proportions lignes
questionr::rprop(tab)

21.5.4 Package survey

Lien vers la documentation : https://cran.r-project.org/web/packages/survey/survey.pdf.

/* Sans objet pour SAS */
proc freq data = donnees_sas;
  tables Sexe * CSP / missing;
  format Sexe sexef. CSP $cspf.;
  weight poids_sondage;
run;
library(survey)
tab <- survey::svydesign(id = ~1, weights = ~poids_sondage, data = donnees_rbase)
survey::svytable(poids_sondage ~ sexef + cspf, design = tab)
# La syntaxe avec pipe n'est pas compatible avec le package survey
library(survey)
tab <- survey::svydesign(id = ~1, weights = ~poids_sondage, data = donnees_tidyverse)
survey::svytable(poids_sondage ~ sexef + cspf, design = tab)
library(survey)
tab <- survey::svydesign(id = ~1, weights = ~poids_sondage, data = donnees_datatable)
survey::svytable(poids_sondage ~ sexef + cspf, design = tab)

21.5.5 Package procs

Lien vers la documentation : https://cran.r-project.org/web/packages/procs/vignettes/procs-freq.html.

/* Sans objet pour SAS */
proc freq data = donnees_sas;
  tables Sexe * CSP / missing;
  format Sexe sexef. CSP $cspf.;
  weight poids_sondage;
run;
library(procs)
procs::proc_freq(donnees_rbase, tables = cspf * sexef, options = v(missing))
# Ne fonctionne pas avec le poids !!!
#procs::proc_freq(donnees_rbase, tables = cspf * sexef, weight = poids_sondage, options = v(missing))
library(procs)
procs::proc_freq(donnees_tidyverse, tables = cspf * sexef, options = v(missing))
# Ne fonctionne pas avec le poids !!!
#procs::proc_freq(donnees_tidyverse, tables = cspf * sexef, weight = poids_sondage, options = v(missing))
library(procs)
# Il semble nécessaire de convertire l'objet en data.frame
procs::proc_freq(setDF(donnees_datatable), tables = cspf * sexef, options = v(missing))
# Ne fonctionne pas avec le poids !!!
#procs::proc_freq(setDF(donnees_datatable), tables = cspf * sexef, weight = poids_sondage, options = v(missing))
# On reconvertit en data.table
setDT(donnees_datatable)

21.6 Solutions avec package R ne permettant apparemment pas de pondérer

Autres packages, qui semblent peu utiles, ne permettant apparemment pas de pondérer.

21.6.1 Package Janitor

Lien vers la documentation : https://cran.r-project.org/web/packages/janitor/vignettes/tabyls.html.

/* Sans objet pour SAS */
proc freq data = donnees_sas;
  tables Sexe * CSP / missing;
  format Sexe sexef. CSP $cspf.;
  weight poids_sondage;
run;
library(janitor)
# Attention, la fonction tabyl ne permet pas de pondérer
tab <- janitor::tabyl(donnees_rbase, cspf, sexef)
tab
janitor::adorn_totals(tab, c("row", "col"))
# Pourcentages
janitor::adorn_percentages(tab, denominator = "all", na.rm = TRUE)
# Pourcentages lignes
janitor::adorn_percentages(tab, denominator = "row", na.rm = TRUE)
# Pourcentages colonnes
janitor::adorn_percentages(tab, denominator = "col", na.rm = TRUE)
library(janitor)
# Attention, la fonction tabyl ne permet pas de pondérer
tab <- donnees_tidyverse %>% 
  janitor::tabyl(cspf, sexef) %>% 
  janitor::adorn_totals(c("row", "col"))
tab
# Pourcentages
tab %>% janitor::adorn_percentages(denominator = "all", na.rm = TRUE)
# Pourcentages lignes
tab %>% janitor::adorn_percentages(denominator = "row", na.rm = TRUE)
# Pourcentages colonnes
tab %>% janitor::adorn_percentages(denominator = "col", na.rm = TRUE)
library(janitor)
# Attention, la fonction tabyl ne permet pas de pondérer
tab <- janitor::tabyl(donnees_datatable, cspf, sexef)
tab
janitor::adorn_totals(tab, c("row", "col"))
# Pourcentages
janitor::adorn_percentages(tab, denominator = "all", na.rm = TRUE)
# Pourcentages lignes
janitor::adorn_percentages(tab, denominator = "row", na.rm = TRUE)
# Pourcentages colonnes
janitor::adorn_percentages(tab, denominator = "col", na.rm = TRUE)

21.6.2 Package crosstable

Lien vers la documentation : https://cran.r-project.org/web/packages/crosstable/crosstable.pdf.

/* Sans objet pour SAS */
proc freq data = donnees_sas;
  tables Sexe * CSP / missing;
  format Sexe sexef. CSP $cspf.;
  weight poids_sondage;
run;
library(crosstable)
crosstable::crosstable(donnees_rbase, cspf, by = sexef, showNA = "always", percent_digits = 0, percent_pattern ="{n} ({p_col}/{p_row})")
library(crosstable)
crosstable::crosstable(donnees_tidyverse, cspf, by = sexef, showNA = "always",
                       percent_digits = 0, percent_pattern ="{n} ({p_col}/{p_row})")
library(crosstable)
crosstable::crosstable(donnees_datatable, cspf, by = sexef, showNA = "always",
                       percent_digits = 0, percent_pattern ="{n} ({p_col}/{p_row})")

21.6.3 Package gmodels

Lien vers la documentation : https://cran.r-project.org/web/packages/gmodels/gmodels.pdf.

/* Sans objet pour SAS */
proc freq data = donnees_sas;
  tables Sexe * CSP / missing;
  format Sexe sexef. CSP $cspf.;
  weight poids_sondage;
run;
library(gmodels)
gmodels::CrossTable(donnees_rbase$cspf, donnees_rbase$sexef)
library(gmodels)
donnees_tidyverse %>% 
  summarise(gmodels::CrossTable(cspf, sexef))
library(gmodels)
gmodels::CrossTable(donnees_datatable$cspf, donnees_datatable$sexef)

21.6.4 Package gtsummary

Lien vers la documentation : https://cran.r-project.org/web/packages/gtsummary/gtsummary.pdf.

/* Sans objet pour SAS */
proc freq data = donnees_sas;
  tables Sexe * CSP / missing;
  format Sexe sexef. CSP $cspf.;
  weight poids_sondage;
run;
library(gtsummary)
# Pourcentages par case, colonne, ligne
gtsummary::tbl_cross(data = donnees_rbase, row = cspf, col = sexef, percent = c("cell"),   margin = c("column", "row"), missing = c("always"))
gtsummary::tbl_cross(data = donnees_rbase, row = cspf, col = sexef, percent = c("column"), margin = c("column", "row"), missing = c("always"))
gtsummary::tbl_cross(data = donnees_rbase, row = cspf, col = sexef, percent = c("row"),    margin = c("column", "row"), missing = c("always"))
library(gtsummary)
# Pourcentages par case, colonne, ligne
gtsummary::tbl_cross(data = donnees_tidyverse, row = cspf, col = sexef, percent = c("cell"),   margin = c("column", "row"), missing = c("always"))
gtsummary::tbl_cross(data = donnees_tidyverse, row = cspf, col = sexef, percent = c("column"), margin = c("column", "row"), missing = c("always"))
gtsummary::tbl_cross(data = donnees_tidyverse, row = cspf, col = sexef, percent = c("row"),    margin = c("column", "row"), missing = c("always"))
library(gtsummary)
# Pourcentages par case, colonne, ligne
gtsummary::tbl_cross(data = donnees_datatable, row = cspf, col = sexef, percent = c("cell"),  
                     margin = c("column", "row"), missing = c("always"))
gtsummary::tbl_cross(data = donnees_datatable, row = cspf, col = sexef, percent = c("column"),
                     margin = c("column", "row"), missing = c("always"))
gtsummary::tbl_cross(data = donnees_datatable, row = cspf, col = sexef, percent = c("row"),   
                     margin = c("column", "row"), missing = c("always"))

22 Tableaux de statistiques

22.1 Moyenne des notes par CSP (variable en ligne)

22.1.1 Moyenne non pondérée

/* Moyenne de note_contenu et nombre de personnes */

/* 1ère solution */
proc sort data = donnees_sas;by cspf;run;
proc means data = donnees_sas mean n;var note_contenu;class cspf;run;

/* 2e solution */
proc tabulate data = donnees_sas;
  var note_contenu;
  class cspf;
  table (cspf all = "Total"), note_contenu * (mean n);
run;

/* 3e solution */
proc sql;
  select cspf, mean(note_contenu) as note_contenu_moyenne, count(*) as N
  from donnees_sas
  group by cspf
  order by cspf;
quit;
# Moyenne de note_contenu et nombre de personnes
aggregate(note_contenu ~ cspf, donnees_rbase, function(x) c(Moyenne = mean(x, na.rm = TRUE), Nombre = length(x)))

# Moyenne de note_contenu
# Une seule variable, une seule variable de groupe, une seule fonction
aggregate(note_contenu ~ cspf, donnees_rbase, mean, na.rm = TRUE)

# rowsum (à ne pas confondre avec rowSums) calcule des sommes, et uniquement des sommes
rowsum(donnees_rbase$note_contenu, donnees_rbase$cspf, recorder = TRUE, na.rm = TRUE)
# Pour obtenir une moyenne, on peut écrire
rowsum(donnees_rbase$note_contenu, donnees_rbase$cspf, recorder = TRUE, na.rm = TRUE) / as.vector(table(donnees_rbase$cspf))

# Fonctions tapply et by
tapply(donnees_rbase$note_contenu, donnees_rbase$cspf, mean, na.rm = TRUE)
with(donnees_rbase, tapply(note_contenu, cspf, mean, na.rm = TRUE))
tapply(donnees_rbase$note_contenu, donnees_rbase$cspf, mean, na.rm = TRUE)
by(donnees_rbase$note_contenu, donnees_rbase$cspf, mean, na.rm = TRUE)

# Découpage avec la fonction split (très pratique en R base !)
sapply(split(donnees_rbase, donnees_rbase$cspf), function(x) mean(x$note_contenu, na.rm = TRUE))
# Moyenne de note_contenu et nombre de personnes
donnees_tidyverse %>% 
  group_by(cspf) %>% 
  summarise(Nombre = n(), Moyenne = mean(note_contenu, na.rm = TRUE))
# Ou alors :
summarise(Nombre = n(), Moyenne = mean(note_contenu, na.rm = TRUE), .by = cspf)

# Moyenne de note_contenu
# Une seule variable, une seule variable de groupe, une seule fonction
donnees_tidyverse %>% 
  group_by(cspf) %>% 
  summarise(Moyenne = mean(note_contenu, na.rm = TRUE))
donnees_tidyverse %>% 
  summarise(Moyenne = mean(note_contenu, na.rm = TRUE), .by = cspf)
# Moyenne de note_contenu et nombre de personnes
donnees_datatable[, .(note_contenu_moyenne = mean(note_contenu, na.rm = TRUE), N = .N), by = cspf]
donnees_datatable[, .(note_contenu_moyenne = mean(note_contenu, na.rm = TRUE), N = .N), keyby = "cspf"]

# Variables définies à part
varNotes <- "note_contenu"
var_groupe <- "cspf"
# À FAIRE : les deux variables sont empilées, pourquoi ??
donnees_datatable[, lapply(.SD, function(x) c(moyenne = mean(x, na.rm = TRUE), n = length(x))),
                  by = var_groupe,
                  .SDcols = varNotes]

22.1.2 Moyenne pondérée

/* Moyenne de note_contenu et nombre de personnes */
proc sort data = donnees_sas;by cspf;run;
proc means data = donnees_sas mean n;
  var note_contenu;class cspf;
  weight poids_sondage;
run;

/* Autre possibilité */
proc tabulate data = donnees_sas;
  var note_contenu;
  class cspf;
  weight poids_sondage;
  table (cspf all = "Total"), note_contenu * (mean n);
run;
# Avec la pondération : tapply ne fonctionne pas, il faut découper la base en facteurs avec split
sapply(split(donnees_rbase, donnees_rbase$cspf), function(x) weighted.mean(x$note_contenu, x$poids_sondage, na.rm = TRUE))
# À FAIRE : autre solution ?
# Avec la pondération
donnees_tidyverse %>% 
  group_by(cspf) %>% 
  summarise(Moyenne = weighted.mean(note_contenu, poids_sondage, na.rm = TRUE))
donnees_tidyverse %>% 
  summarise(Moyenne = weighted.mean(note_contenu, poids_sondage, na.rm = TRUE), .by = cspf)
# Avec la pondération
varNotes <- "note_contenu"
var_groupe <- "cspf"
donnees_datatable[, lapply(.SD, function(x) weighted.mean(x, poids_sondage, na.rm = TRUE)),
                  keyby = var_groupe,
                  .SDcols = varNotes]

22.2 Moyenne des notes par CSP et Sexe (variables en ligne)

%let var_notes = note_contenu note_formateur note_moyens note_accompagnement note_materiel;
%let var_groupe = cspf sexef;
proc sort data = donnees_sas;by &var_groupe.;run;
proc means data = donnees_sas mean n;
  class &var_groupe.;
  var &var_notes.;
  output out = Resultat;
run;

/* Autre solution */
%macro sel;
  %global select;
  %local i j;
  %let select = ;
  %do i = 1 %to %sysfunc(countw(&var_notes.));
    %let j = %scan(&var_notes., &i., %str( ));
    %let select = &select. mean(&j) as &j._moyenne,;
  %end;
%mend sel;
%sel;

%let group = %sysfunc(tranwrd(&var_groupe., %str( ), %str(, )));
proc sql;
  select &group., &select. count(*) as N
  from donnees_sas
  group by &group.
  order by &group.;
quit;
# Plusieurs solutions avec aggregate (plutôt lent)
aggregate(note_contenu ~ cspf + sexef, donnees_rbase, function(x) c(mean = mean(x), n = length(x)))
aggregate(cbind(note_contenu, note_materiel) ~ cspf + sexef, donnees_rbase, function(x) c(moyenne = mean(x, na.rm = TRUE), n = length(x)))

# Via les formules
variable <- c("note_contenu")
varGroupement <- c("cspf", "sexef")
formule <- as.formula(paste(variable, paste(varGroupement, collapse = " + "), sep = " ~ "))
aggregate(formule, donnees_rbase, function(x) c(moyenne = mean(x, na.rm = TRUE), n = length(x)))

# Avec by
by(donnees_rbase[, variable], donnees_rbase[, varGroupement], function(x) c(mean = mean(x, na.rm = TRUE), n = length(x)))

# Avec rowsum (à ne pas confondre avec rowSums)
# Somme
rowsum(donnees_rbase[, variable], interaction(donnees_rbase[, varGroupement], sep = "_", lex.order = TRUE))
# Moyenne
rowsum(donnees_rbase[, variable], interaction(donnees_rbase[, varGroupement], sep = "_")) / as.vector(table(interaction(donnees_rbase[, varGroupement])))
donnees_tidyverse %>% 
  group_by(cspf, sexef) %>% 
  summarise(Moyenne = mean(note_contenu, na.rm = TRUE), n = n())

# Autre solution : l'ordre des modalités est modifié
donnees_tidyverse %>% 
  summarise(Moyenne = mean(note_contenu, na.rm = TRUE), n = n(), .by = c(cspf, sexef))
donnees_datatable[, .(note_contenu_moyenne = mean(note_contenu, na.rm = TRUE), N = .N), keyby = c("cspf", "sexef")]

# Autre solution
data.table::dcast(donnees_datatable, cspf + sexef ~ ., value.var = "note_contenu", fun.aggregate = mean, na.rm = TRUE)

# Variables définies à part
varNotes <- c("note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
var_groupe <- c("cspf", "sexef")
# À FAIRE : les deux variables sont empilées, pourquoi ??
donnees_datatable[, lapply(.SD, function(x) list(moyenne = mean(x, na.rm = TRUE), n = length(x))),
                  keyby = var_groupe,
                  .SDcols = varNotes]

# Nombre de femmes par CSP
# Il y a un recycling de gender = "M", utile de le mentionner
donnees_datatable[, .(Femmes = sum(sexef == "Femme", na.rm = TRUE), Hommes = sum(sexef == "Homme", na.rm = TRUE)), by = .(cspf)]

# À FAIRE :
# Exemple avec les variables dans .SDcols
# data.table::setDT(DF)[, lapply(.SD, mean, na.rm = TRUE), .SDcols = c("x", "y"), by = list(g, h)]
# D'autres variations (par exemple, c(x, y) ou list("x", "y") ne fonctionnent pas !)

22.3 Tableaux croisés à 2 variables de groupement

proc tabulate data = donnees_sas;
  class cspf sexef;
  var note_contenu;
  table (cspf all = "Ensemble"), sexef * (note_contenu) * mean;
run;
# Tableau croisé Cspf par Sexef
varGroupement <- c("cspf", "sexef")
variable <- c("note_contenu")

# Solution avec tapply
tapply(donnees_rbase[, variable], donnees_rbase[varGroupement], function(x) moyenne = mean(x, na.rm = TRUE))

# Solution avec xtabs
xtabs(note_contenu ~ cspf + sexef, aggregate(note_contenu ~ cspf + sexef, data = donnees_rbase, FUN = mean, na.rm = TRUE))
# Ou, sous forme de formule
formule <- as.formula(paste(variable, paste(varGroupement, collapse = " + "), sep = " ~ "))
xtabs(formule, aggregate(formule, data = donnees_rbase, FUN = mean, na.rm = TRUE))
# Et si l'on souhaite un dataframe
as.data.frame.matrix(xtabs(formule, aggregate(formule, data = donnees_rbase, FUN = mean, na.rm = TRUE)))

# Solution avec aggregate, en calculant un tableau "long" et en le transformant en "wide"
tableau <- aggregate(note_contenu ~ cspf + sexef, data = donnees_rbase, FUN = mean, na.rm = TRUE)
tableau <- reshape(tableau, 
        timevar = varGroupement[2],
        idvar = varGroupement[1],
        direction = "wide")
tableau[is.na(tableau)] <- 0
# Tableau croisé Cspf par Sexef
varGroupement <- c("cspf", "sexef")
variable <- c("note_contenu")
donnees_tidyverse %>% 
  group_by(across(all_of(varGroupement))) %>% 
  summarise(across(all_of(variable), ~ mean(.x, na.rm = TRUE), .names = "Moyenne")) %>% 
  spread(varGroupement[2], Moyenne)

# Autre solution
donnees_tidyverse %>% 
  group_by(!!!syms(varGroupement)) %>% 
  summarise(Moyenne = mean(.data[[variable]], na.rm = TRUE)) %>% 
  spread(varGroupement[2], Moyenne)
# Tableau croisé Cspf par Sexef
varGroupement <- c("cspf", "sexef")
variable <- "note_contenu"
data.table::dcast(donnees_datatable, cspf ~ sexef, value.var = "note_contenu", fun.aggregate = mean, na.rm = TRUE)

# Avec références seulement
data.table::dcast(donnees_datatable, get(varGroupement[1]) ~ get(varGroupement[2]), value.var = variable,
                  fun.aggregate = mean, na.rm = TRUE)

# Autre solution, plus indirecte
# À FAIRE : attention, toujours utiliser lapply, même avec une seule variable ! LE DIRE !!!
tab <- donnees_datatable[, lapply(.SD, mean, na.rm = TRUE), keyby = varGroupement, .SDcols = "note_contenu"]
data.table::dcast(tab, get(varGroupement[1]) ~ get(varGroupement[2]), value.var = variable)

22.4 Tableaux croisés à 3 variables de groupement ou plus (1 variable en ligne, 2 en colonne par exemple)

/* Notes par croisement de CSP (en ligne) et de Sexe x Niveau */
%let notes = note_contenu note_formateur note_moyens note_accompagnement note_materiel;
proc tabulate data = donnees_sas;
  class cspf sexef;
  var &notes.;
  table (cspf all = "Ensemble"), sexef * (&notes.) * mean;
run;

/* Note_contenu par croisement de CSP (en ligne) et de Sexe x Niveau */
proc tabulate data = donnees_sas;
  class cspf sexef Niveau;
  var note_moyenne;
  table (cspf all = "Ensemble"), (sexef * Niveau) * (note_moyenne) * mean;
run;
# 1er exemple : CSPF en ligne, et chacune des 5 notes croisées avec le sexe en colonne
varNotes <- c("note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
var_groupe <- c("cspf", "sexef")

tableau <- aggregate(donnees_rbase[, varNotes], donnees_rbase[var_groupe], function(x) moyenne = mean(x, na.rm = TRUE))
reshape(tableau, 
        timevar = var_groupe[2],
        idvar = var_groupe[1],
        direction = "wide")

# 2e exemple : CSPF en ligne, et croisement Sexe x Qualifié en colonne, note_contenu sommée
# À FAIRE : proposer une fonction ?
formule <- as.formula("note_contenu ~ cspf + sexef + niveau")
tab <- xtabs(formule, aggregate(formule, data = donnees_rbase, FUN = mean, na.rm = TRUE))
nomsCol <- do.call(paste, c(expand.grid(dimnames(tab)[-1L]), sep = "_"))
nomsLig <- dimnames(tab)[[1L]]
# Transformation du tableau de résultats (en format array) vers un format matrix, puis dataframe
# Permet d'exprimer le array (matrice multidimensionnelle) en un tableau à deux dimensions
# On transforme le tableau en matrice ayant en nombre de lignes dim(tab)[1], c'est-à-dire le nombre de lignes du array
# et en nombre de colonnes le reste des variables
tab <- data.frame(matrix(tab, nrow = dim(tab)[1L]))
# Renommage des noms des colonnes de la base
colnames(tab) <- nomsCol
# Renommage des noms des lignes de la base
row.names(tab) <- nomsLig
# On annule les valeurs manquantes
tab[is.na(tab)] <- 0
tab
# À FAIRE : développer autour de cet exemple
# Avec 3 variables
xtabs(cbind(note_contenu, note_materiel) ~ cspf + sexef, donnees_rbase)
# 1er exemple : CSPF en ligne, et chacune des 5 notes croisées avec le sexe en colonne
varNotes <- c("note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
var_groupe <- c("cspf", "sexef")
donnees_tidyverse %>% 
  group_by(across(all_of(var_groupe))) %>% 
  summarise(across(all_of(varNotes), ~ mean(.x, na.rm = TRUE))) %>% 
  pivot_wider(names_from = sexef,
              values_from = all_of(varNotes))

# 2e exemple : CSPF en ligne, et croisement Sexe x Qualifié en colonne, note_contenu sommée
varNotes <- c("note_contenu")
var_groupe <- c("cspf", "sexef", "niveau")
donnees_tidyverse %>% 
  group_by(across(all_of(var_groupe))) %>% 
  summarise(across(all_of(varNotes), ~ mean(.x, na.rm = TRUE))) %>% 
  pivot_wider(names_from = c(sexef, niveau),
              values_from = all_of(varNotes),
              values_fill = 0)
# 1er exemple : CSPF en ligne, et chacune des 5 notes croisées avec le sexe en colonne
varNotes <- c("note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
var_groupe <- c("cspf", "sexef")
data.table::dcast(donnees_datatable, get(varGroupement[1]) ~ get(varGroupement[2]), value.var = varNotes,
                  fun.aggregate = mean, na.rm = TRUE)

# 2e exemple : CSPF en ligne, et croisement Sexe x Qualifié en colonne, note_contenu sommée
data.table::dcast(donnees_datatable, cspf ~ sexef + niveau, value.var = "note_contenu",
                  fun.aggregate = mean, na.rm = TRUE)

23 Boucles

23.1 Boucles imbriquées

data _null_;call symput('annee', strip(year(today())));run;

/* Ensemble des premiers jours de chaque mois entre 2020 et le 31 décembre de l'année courante */
%macro Boucles_Imbriquees(an_debut, an_fin);
  %local i j;
  %global liste_mois;
  %let liste_mois = ;
  
  %do i = &an_debut. %to &an_fin.;
    %do j = 1 %to 12;
        %let liste_mois = &liste_mois. %sysfunc(putn(%sysfunc(mdy(&j., 1, &i.)), ddmmyy10.));
      %end;
  %end;
%mend Boucles_Imbriquees;

%let annee = %sysfunc(year(%sysfunc(today())));
%Boucles_Imbriquees(an_debut = 2020, an_fin = &annee.);
%put &liste_mois.;
# Ensemble des premiers jours de chaque mois entre 2020 et l'année courante
annee <- lubridate::year(Sys.Date())
# 1ère solution avec for (lente, à déconseiller !)
listeMois <- c()
for (i in seq(2020, annee)) {
  for (j in 1:12) {
    listeMois <- as.Date(c(listeMois, lubridate::ymd(sprintf("%02d-%02d-01", i, j))), origin = "1970-01-01")
  }
}

# 2e  solution : 2 fonctions lapply imbriquées
listeMois <- as.Date(unlist(lapply(seq(2020, annee), 
                                   function(x) lapply(1:12, function(y) lubridate::ymd(sprintf("%02d-%02d-01", x, y))))),
                     origin = "1970-01-01")

# 3e solution : expand.grid
listeMois <- sort(as.Date(apply(expand.grid(seq(2020, annee), 1:12), 1, 
                                function(x) lubridate::ymd(sprintf("%02d-%02d-01", x[1], x[2]))),
                          origin = "1970-01-01"))

# 4e solution, la plus simple !
seq.Date(lubridate::ymd(sprintf("%02d-01-01", 2020)), lubridate::ymd(sprintf("%02d-12-01", annee)), by = "month")
# Ensemble des premiers jours de chaque mois entre 2020 et l'année courante
annee <- lubridate::year(Sys.Date())

# 1ère solution : 2 fonctions map imbriquées
listeMois <- purrr::map(seq(2020, annee), 
                        function(x) purrr::map(1:12,
                                               function(y) lubridate::ymd(sprintf("%02d-%02d-01", x, y)))) %>% 
  unlist() %>% 
  as.Date(, origin = "1970-01-01")

# 2e solution : expand_grid
listeMois <- tidyr::expand_grid(annee = seq(2020, annee), mois = 1:12) %>% 
  apply(1, function(x) lubridate::ymd(sprintf("%02d-%02d-01", x[1], x[2]))) %>% 
  as.Date(, origin = "1970-01-01") %>% 
  sort()

# 3e solution, la plus simple
seq.Date(lubridate::ymd(sprintf("%02d-01-01", 2020)), lubridate::ymd(sprintf("%02d-12-01", annee)), by = "month")
# Ensemble des premiers jours de chaque mois entre 2020 et l'année courante
annee <- lubridate::year(Sys.Date())

# 1ère solution avec for (lente, à déconseiller !)
listeMois <- c()
for (i in seq(2020, annee)) {
  for (j in 1:12) {
    listeMois <- as.Date(c(listeMois, lubridate::ymd(sprintf("%02d-%02d-01", i, j))), origin = "1970-01-01")
  }
}

# 2e  solution : 2 fonctions lapply imbriquées
listeMois <- as.Date(unlist(lapply(seq(2020, annee), 
                                   function(x) lapply(1:12, function(y) lubridate::ymd(sprintf("%02d-%02d-01", x, y))))),
                     origin = "1970-01-01")

# 3e solution : expand.grid
listeMois <- sort(as.Date(apply(expand.grid(seq(2020, annee), 1:12), 1, 
                                function(x) lubridate::ymd(sprintf("%02d-%02d-01", x[1], x[2]))),
                          origin = "1970-01-01"))

# 4e solution, la plus simple
seq.Date(lubridate::ymd(sprintf("%02d-01-01", 2020)), lubridate::ymd(sprintf("%02d-12-01", annee)), by = "month")

23.2 Boucles imbriquées (second exemple)

/* Itérer sur toutes les années et les trimestres d'une certaine plage */
/* On va afficher les noms base_AAAA_tT_nmax où AAAA désigne les années, T les trimestres, depuis 2020 */

%macro iteration(debut, fin);
  %global liste_an;
  %let liste_an = ;
  %do i = &debut. %to &fin.;
    %let liste_an = &liste_an.&i.-;
  %end;
%mend iteration;
%iteration(debut = 2020, fin = %sysfunc(year(%sysfunc(today()))));
%put &liste_an.;

%let liste_trim = 1 2 3 4;
%let liste_niv = max min;
/* Supposons que nous ayons des noms de fichier suffixés par AXXXX_TY_NZ, avec X l'année, Y le trimestre et
   Z max ou min. Par exemple, A2010_T2_NMax */
/* Pour obtenir l'ensemble de ces noms de 2010 à cette année */
%macro noms_fichiers(base = temp);
  %global res;
  %let res = ;
  /* 1ère boucle */
  %do j = 1 %to %sysfunc(countw(&liste_an., "-"));
    %let y = %scan(&liste_an., &j., "-"); /* année */
    /* 2e boucle */
    %do i = 1 %to 4;
      %let t = %scan(&liste_trim, &i.); /* trimestre */
      /* 3e boucle */
      %do g = 1 %to 2;
        %let n = %scan(&liste_niv., &g.); /* niveau */
            %let res = &res. &base._&y._t&t._n&n.;
        %end;
      %end;
  %end;
%mend noms_fichiers;

%noms_fichiers(base = base);
%put &res.;
# Itérer sur toutes les années et les trimestres d'une certaine plage
# on va afficher les noms base_AAAA_tT_nmax où AAAA désigne les années, T les trimestres, depuis 2020 
debut <- 2020
fin <- lubridate::year(Sys.Date())
res <- unlist(lapply(debut:fin, function(x) lapply(c("max", "min"), function(y)  sprintf("base_%4d_t%d_n%s", x, 1:4, y))))
# Itérer sur toutes les années et les trimestres d'une certaine plage
# on va afficher les noms base_AAAA_tT_nmax où AAAA désigne les années, T les trimestres, depuis 2020 
debut <- 2020
fin <- lubridate::year(Sys.Date())
listeMois <- purrr::map(debut:fin, 
                        function(x) purrr::map(c("max", "min"),
                                               function(y) sprintf("base_%4d_t%d_n%s", x, 1:4, y))) %>% 
                          unlist()
# Itérer sur toutes les années et les trimestres d'une certaine plage
# on va afficher les noms base_AAAA_tT_nmax où AAAA désigne les années, T les trimestres, depuis 2020 
debut <- 2020
fin <- lubridate::year(Sys.Date())
res <- unlist(lapply(debut:fin, function(x) lapply(c("max", "min"), function(y)  sprintf("base_%4d_t%d_n%s", x, 1:4, y))))

23.3 Boucles for

/* On va créer une base par année d'entrée */
proc sql noprint;
  select year(min(date_entree)), year(max(date_entree)) into :an_min, :an_max
  from donnees_sas;
quit;

%macro Base_par_mois(debut, fin);
  /* %local impose que an n'est pas de signification hors de la macro */
  %local an;
  /* %global impose que nom_bases peut être utilisé en dehors de la macro */
  %global nom_bases;
  /* On initalise la création de la macri-variable nom_bases */
  %let nom_bases = ;
  
  /* On itère entre &debut. et &fin. */
  %do an = &debut. %to &fin.;
    data Entree_&an.;
        set donnees_sas;
        if year(date_entree) = &an.;
      run;
      /* On ajoute à la macro-variable le nom de la base */
      %let nom_bases = &nom_bases. Entree_&an.;
  %end;
%mend Base_par_mois;

%Base_par_mois(debut = &an_min., fin = &an_max.);
%put &nom_bases.;

/* On va désormais empiler toutes les bases (concaténation par colonne) */
/* L'instruction set utilisée de cette façon permet cet empilement */
data concatene;
  set &nom_bases.;
run;
# On va créer une base par année d'entrée
anMin <- min(lubridate::year(donnees_rbase$date_entree), na.rm = TRUE)
anMax <- max(lubridate::year(donnees_rbase$date_entree), na.rm = TRUE)

for (i in anMin:anMax) {
  # assign permet de faire passer une chaîne de caractères en variable R
  assign(paste("entree", i, sep = "_"), donnees_rbase[which(lubridate::year(donnees_rbase$date_entree) == i), ])
}

# On va désormais empiler toutes les bases (concaténation par colonne)
# do.call applique la fonction rbind à l'ensemble des bases issues du lapply
# get permet de faire le chemin inverse de assign
concatene <- do.call(rbind, lapply(paste("entree", anMin:anMax, sep = "_"), get))
# À FAIRE : problème pour les entrées où la date est manquante
# On va créer une base par année d'entrée
anMin <- donnees_tidyverse %>% pull(date_entree) %>% lubridate::year() %>% min(na.rm = TRUE)
anMax <- donnees_tidyverse %>% pull(date_entree) %>% lubridate::year() %>% max(na.rm = TRUE)

for (i in anMin:anMax) {
  # assign permet de faire passer une chaîne de caractères en variable R
  assign(paste("entree", i, sep = "_"),
         donnees_tidyverse %>% filter(lubridate::year(date_entree) == as.name(i)))
}

# On va désormais empiler toutes les bases (concaténation par colonne)
# purrr::reduce applique la fonction bind_rows à l'ensemble des bases issues du purrr::map
# get permet de faire le chemin inverse de assign
concatene <- purrr::map(paste("entree", anMin:anMax, sep = "_"), get) %>% 
  purrr::reduce(bind_rows)
# On va créer une base par année d'entrée
anMin <- min(lubridate::year(donnees_datatable$date_entree), na.rm = TRUE)
anMax <- max(lubridate::year(donnees_datatable$date_entree), na.rm = TRUE)

for (i in anMin:anMax) {
  # assign permet de faire passer une chaîne de caractères en variable R
  assign(paste("entree", i, sep = "_"), donnees_datatable[lubridate::year(donnees_datatable$date_entree) == i, ])
}

# On va désormais empiler toutes les bases (concaténation par colonne)
# do.call applique la fonction rbind à l'ensemble des bases issues du lapply
# get permet de faire le chemin inverse de assign
concatene <- rbindlist(lapply(paste("entree", anMin:anMax, sep = "_"), get))

23.4 Boucles for (second exemple)

/* On recherche toutes les valeurs de CSP différentes et on les met dans une variable.
   On appelle la proc SQL :
   - utilisation du quit et non run à la fin
   - on récupère toutes les valeurs différentes de CSP, séparés par un espace (separated by)
   - s'il y a un espace dans les noms, on le remplace par _ 
   - on les met dans la macro-variable liste_csp
   - on trier la liste par valeur de CSP */
   
/* On crée une variable de CSP formaté sans les accents et les espaces */
data donnees_sas;
  set donnees_sas;
  /* SAS ne pourra pas créer des bases de données avec des noms accentués */
  /* On supprime dans le nom les lettres accentués. On le fait avec la fonction Translate */
  CSPF2 = tranwrd(strip(CSPF), " ", "_");
  CSPF2 = translate(CSPF2, "eeeeaacio", "éèêëàâçîô");
run;

/* Boucles et macros en SAS */
/* Les boucles ne peuvent être utilisées que dans le cadre de macros */
/* Ouverture de la macro */

%macro Boucles(base = donnees_sas, var = CSPF2);
  /* Les modalités de la variable */
  proc sql noprint;select distinct &var. into :liste separated by " " from &base. order by &var.;quit;
  /* On affiche la liste de ces modalités */
  %put &liste.;
  /* %let permet à SAS d'affecter une valeur à une variable en dehors d'une manipulation de base de données */
  /* %sysfunc indique à SAS qu'il doit utiliser la fonction countw dans le cadre d'une macro (pas important) */
  /* countw est une fonction qui compte le nombre de mots (séparés par un espace) d'une chaîne de caractères */
  /* => on compte le nombre de CSP différentes */
  %let nb = %sysfunc(countw(&liste.));
  %put Nombre de modalités différentes : &nb.;
  /* On itère pour chaque CSP différente ... */
  %do i = 1 %to &nb.;
    /* %scan : donne le i-ème mot de &liste. (les mots sont séparés par un espace) */
    /* => on récupère donc la CSP numéro i */
    %let j = %scan(&liste., &i.);
    %put Variable : &j.;
    /* On crée une base avec seulement les individus de la CSP correspondante */
    data &var.;set donnees_sas;if &var. = "&j.";run;
  %end;
/* Fermeture de la macro */
%mend Boucles;

/* Lancement de la macro */
%Boucles(base = donnees_sas, var = CSPF2);
# Base par CSP
for (i in unique(donnees_rbase$cspf)) {
  # Met en minuscule et enlève les accents
  nomBase <- tolower(chartr("éèêëàâçîô", "eeeeaacio", i))
  # assign permet de faire passer une chaîne de caractères en variable R
  assign(nomBase, donnees_rbase[which(donnees_rbase$cspf == i), ])
}
# Base par CSP
for (i in donnees_tidyverse %>% distinct(cspf) %>% pull()) {
  # Met en minuscule et enlève les accents
  nomBase <- chartr("éèêëàâçîô", "eeeeaacio", i) %>% tolower()
  # assign permet de faire passer une chaîne de caractères en variable R
  assign(nomBase, donnees_tidyverse %>% 
           filter(cspf == as.name(i)))
}
# Créer une base pour chaque individu d'une certaine CSP
for (i in unique(donnees_datatable$cspf)) {
  # Met en minuscule et enlève les accents
  nomBase <- tolower(chartr("éèêëàâçîô", "eeeeaacio", i))
  # assign permet de faire passer une chaîne de caractères en variable R
  assign(nomBase, donnees_datatable[donnees_datatable$cspf == i, ])
}

24 Fonctions SAS et R utiles

24.1 Mesurer la durée d’exécution d’un programme

%let temps_debut = %sysfunc(datetime());
proc sort data = donnees_sas;by identifiant date_entree;run;
%let temps_fin = %sysfunc(datetime());

%let duree = %sysevalf((&temps_fin. - &temps_debut.) / 60);
%put Durée exécution : &duree minutes;
system.time(donnees_rbase <- donnees_rbase[order(donnees_rbase$identifiant, donnees_rbase$date_entree, na.last = FALSE), ])

# Autre possibilité
debut <- Sys.time()
donnees_rbase <- donnees_rbase[order(donnees_rbase$identifiant, donnees_rbase$date_entree, na.last = FALSE), ]
fin <- Sys.time()
sprintf("Temps d'exécution : %s secondes !", fin - debut)
system.time(donnees_tidyverse <- donnees_tidyverse %>% 
              arrange(identifiant, date_entree))

# Autre possibilité
debut <- Sys.time()
donnees_tidyverse <- donnees_tidyverse %>% 
              arrange(identifiant, date_entree)
fin <- Sys.time()
sprintf("Temps d'exécution : %s secondes !", fin - debut)
system.time(setorder(donnees_datatable, "identifiant", "date_entree", na.last = FALSE))

# Autres possibilités
debut <- Sys.time()
setorder(donnees_datatable, "identifiant", "date_entree", na.last = FALSE)
fin <- Sys.time()
sprintf("Temps d'exécution : %s secondes !", fin - debut)

started.at <- proc.time()
setorder(donnees_datatable, "identifiant", "date_entree", na.last = FALSE)
timetaken(started.at)

24.2 Exécuter le code d’un autre fichier

/* include("chemin") */
# encoding permet de gérer l'encodage des caractères accentués
# echo = TRUE affiche le script dans la console
# max.deparse.length permet de s'assurer qu'un texte long est bien visible

# source("chemin", encoding = "utf-8", echo = TRUE, max.deparse.length = 1e3)
# encoding permet de gérer l'encodage des caractères accentués
# echo = TRUE affiche le script dans la console
# max.deparse.length permet de s'assurer qu'un texte long est bien visible

# source("chemin", encoding = "utf-8", echo = TRUE, max.deparse.length = 1e3)
# encoding permet de gérer l'encodage des caractères accentués
# echo = TRUE affiche le script dans la console
# max.deparse.length permet de s'assurer qu'un texte long est bien visible

# source("chemin", encoding = "utf-8", echo = TRUE, max.deparse.length = 1e3)

24.3 Nombre de lignes affectées par un changement

/* Ne semble pas exister nativement */
# Ne semble pas exister nativement
# Ne semble pas exister nativement
donnees_datatable[, sexef2 := tolower(sexef)]
sprintf("Nombre de lignes modifiées : %d", .Last.updated)
donnees_datatable[, sexef2 := NULL]

25 Créer ses propres fonctions

25.1 Documentation

Pour en savoir plus sur l’écriture de fonctions en R :

https://adv-r.hadley.nz/functions.html

Pour en savoir plus sur l’écriture de fonctions en data.table :

https://cran.r-project.org/web/packages/data.table/vignettes/datatable-programming.html

25.2 Sélection de données

On sélectionne des données (dans cet exemple, les femmes) dans une nouvelle base (dans cet exemple, extrait), via une fonction (une macro en SAS).

%macro Selection (BaseInitiale, BaseFinale, condition);
  data &BaseFinale.;
    set &BaseInitiale. (&condition.);
  run;
%mend Selection;
%Selection(BaseInitiale = donnees_sas, BaseFinale = extrait);
%Selection(BaseInitiale = donnees_sas, BaseFinale = extrait, condition = where = (sexe = 2));
Selection <- function(baseInitiale = donnees_rbase, condition) {
  return(eval(substitute(subset(baseInitiale, condition))))
}
extrait <- Selection(baseInitiale = donnees_rbase)
extrait <- Selection(baseInitiale = donnees_rbase, condition = sexe == "2")
Selection <- function(baseInitiale = donnees_tidyverse, condition = TRUE) {
  baseInitiale %>% 
    filter({{ condition }}) %>% 
    return()
}
extrait <- Selection(baseInitiale = donnees_tidyverse)
extrait <- Selection(baseInitiale = donnees_tidyverse, condition = sexe == "2")
Selection <- function(baseInitiale = donnees_datatable, condition) {
  baseInitiale[condition, , env = list(condition = condition)]
}
extrait <- Selection(baseInitiale = donnees_datatable)
extrait <- Selection(baseInitiale = donnees_datatable, condition = quote(sexe == "2"))

25.3 Moyenne d’un certain nombre de variables

%macro Moyenne (BaseInitiale, variables);
  proc means data = &BaseInitiale. mean;
    var &variables;
  run;
%mend Moyenne;
%Moyenne(BaseInitiale = donnees_sas, variables = note_contenu note_formateur);
Moyenne <- function(baseInitiale = donnees_rbase, variables) {
  moyennes <- unlist(lapply(baseInitiale[, variables], mean, na.rm = TRUE))
  names(moyennes) <- paste("moyenne", names(moyennes), sep = "_")
  return(moyennes)
}
Moyenne(baseInitiale = donnees_rbase, variables = c("note_contenu", "note_formateur"))
Moyenne <- function(baseInitiale = donnees_tidyverse, variables) {
  baseInitiale %>% 
    summarise(across({{ variables }}, function(x) mean(x,, na.rm = TRUE), .names = "Moyenne_{.col}")) %>% 
    return()
}
Moyenne(baseInitiale = donnees_tidyverse, variables = c("note_contenu", "note_formateur"))
Moyenne <- function(baseInitiale = donnees_datatable, variables) {
  moyennes <- baseInitiale[, lapply(.SD, mean, na.rm = TRUE), .SDcols = variables]
  setnames(moyennes, paste("Moyenne", variables, sep = "_"))
  return(moyennes)
}
Moyenne(baseInitiale = donnees_datatable, variables = c("note_contenu", "note_formateur"))

25.4 Fonction calculant un indicateur statistique

Cet exemple de fonction propose de calculer un indicateur statistique au choix (par exemple, moyenne, médiane, maximum, etc.) sur un certain nombre de variables numériques (au choix) d’une certaine base de données (au choix) avec éventuellement une sélection de lignes, et des arguments supplémentaires (notamment na.rm = TRUE) via le paramètre …

%macro CalculMoyenne (baseDonnees, variables, statistique, condition);
  proc means data = &baseDonnees. &statistique.;
    var &variables.;
  run;
%mend CalculMoyenne;

%CalculMoyenne(baseDonnees = donnees_sas, variables = note_formateur note_contenu);
%CalculMoyenne(baseDonnees = donnees_sas, variables = note_formateur note_contenu, statistique = mean sum median);
%CalculMoyenne(baseDonnees = donnees_sas, variables = note_formateur note_contenu, statistique = mean sum, condition = where sexef = "Femme");
CalculMoyenne <- function(baseDonnees, variable, statistique = "mean", ..., selection = TRUE) {
  baseDonnees <- eval(substitute(subset(baseDonnees, selection)))
  moyenne <- lapply(baseDonnees[, variable], get(statistique), ...)
  names(moyenne) <- paste(names(moyenne), statistique, sep = "_")
  moyenne <- data.frame(moyenne)
  return(moyenne)
}

CalculMoyenne(donnees_rbase, c("note_formateur", "note_contenu"))
CalculMoyenne(donnees_rbase, c("note_formateur", "note_contenu"), statistique = "median", na.rm = TRUE)
CalculMoyenne(donnees_rbase, c("note_formateur", "note_contenu"), "mean", na.rm = TRUE, selection = sexef == "Femme")
CalculMoyenne(donnees_rbase, c("note_formateur", "note_contenu"), "quantile", na.rm = TRUE, probs = seq(0, 1, 0.1), selection = sexef == "Femme")
CalculMoyenne <- function(baseDonnees, variable, statistique = "mean", ..., selection = TRUE) {
  moyenne <- baseDonnees %>% 
    filter({{ selection }}) %>% 
    summarise(across(variable, ~ get(statistique)(.x, ...), .names = "{.col}_{ {{ statistique }} }"))
  return(moyenne)
}

CalculMoyenne(donnees_rbase, c("note_formateur", "note_contenu"))
CalculMoyenne(donnees_rbase, c("note_formateur", "note_contenu"), statistique = "median", na.rm = TRUE)
CalculMoyenne(donnees_rbase, c("note_formateur", "note_contenu"), "mean", na.rm = TRUE, selection = sexef == "Femme")
CalculMoyenne(donnees_rbase, c("note_formateur", "note_contenu"), "quantile", na.rm = TRUE, probs = seq(0, 1, 0.1), selection = sexef == "Femme")
CalculMoyenne <- function(baseDonnees, variable, statistique = "mean", ..., selection = TRUE) {
  moyenne <- baseDonnees[selection, lapply(.SD, statistique, ...), .SDcols = variable, env = list(statistique = statistique, selection = selection)]
  setnames(moyenne, paste(names(moyenne), statistique, sep = "_"))
  return(moyenne)
}

CalculMoyenne(donnees_datatable, c("note_formateur", "note_contenu"))
CalculMoyenne(donnees_datatable, c("note_formateur", "note_contenu"), statistique = "median", na.rm = TRUE)
CalculMoyenne(donnees_datatable, c("note_formateur", "note_contenu"), "mean", na.rm = TRUE, selection = quote(sexef == "Femme"))
CalculMoyenne(donnees_datatable, c("note_formateur", "note_contenu"), "quantile", na.rm = TRUE, probs = seq(0, 1, 0.1), selection = quote(sexef == "Femme"))

Autres exemples de fonctions possibles : statistiques par groupes (proc tabulate), proc freq, ajout dans la base d’indicatrices de présence en stock à la fin du mois (%local).

26 Débogage

26.1 Outils d’aide au débogage

options symbolgen mprint mlogic;
%macro Debogage;
  %local phrase i j;
  %let phrase = Voici une phrase;
  %do i = 1 %to %sysfunc(countw(&phrase.));
    %let j = %scan(&phrase., &i.);
    %put Mot n°&i. = &j.;
  %end;
%mend Debogage;
%Debogage;
options nosymbolgen nomprint nomlogic;
#phrase <- c("voici", "une", "phrase")
#options(error=recover)
#for (i in phrase) print(k)
#options(error=NULL)

# À FAIRE : autres outils
#traceback()
#browser()
# À FAIRE : creuser
#phrase <- c("voici", "une", "phrase")
#options(error=recover)
#for (i in phrase) print(k)
#options(error=NULL)

# À FAIRE : autres outils
#traceback()
#browser()
#phrase <- c("voici", "une", "phrase")
#options(error=recover)
#for (i in phrase) print(k)
#options(error=NULL)

# À FAIRE : autres outils
#traceback()
#browser()

27 Points de vigilance en SAS

27.1 Emploi des guillemets et doubles guillemets

Une macro exprimée sous format caractère doit être entourée de ““, et non ’’.

/* Quelques points de vigilance en SAS (à ne connaître que si on est amené à modifier le programme SAS, pas utiles sinon) */
/* Double guillemets pour les macro-variables */
%let a = Bonjour;
%put '&a.'; /* Incorrect */
%put "&a."; /* Correct */
# Sans objet en R
# Sans objet en R
# Sans objet en R
# Sans objet en R

27.2 Macro-variable définie avec un statut global avant son appel dans le cadre d’un statut local

%macro test;
  %let reponse = oui;
%mend test;
%test;

/* 1. Erreur car &reponse. n'est défini que dans le cas d'un environnement local */ 
%put &reponse.;

/* 2. Défini auparavant dans un environnement global, elle change de valeur à l'appel de la fonction */
%let reponse = non;
%put Reponse : &reponse.;
%test;
%put Reponse après la macro : &reponse.;

/* 3. Problème corrigé, en imposant la variable à local dans la macro */
%macro test2;
  %local reponse;
  %let reponse = oui;
%mend test2;

%let reponse = non;
%put Reponse : &reponse.;
%test2;
%put Reponse après la macro : &reponse.;
# Sans objet en R
# Sans objet en R
# Sans objet en R
# Sans objet en R

28 Fin du programme

28.1 Taille des objets en mémoire

/* Taille d'une base de données */
proc sql;
  select libname, memname, filesize format = sizekmg., filesize format = sizek.
  from Dictionary.Tables
  where libname = "WORK" and memname = upcase("donnees_sas") and memtype = "DATA";
quit;
# Taille, en mémoire, d'une base (en Mb)
format(object.size(donnees_rbase), nsmall = 3, digits = 2, unit = "Mb")

# Taille des objets en mémoire, en Gb
sort( sapply(ls(), function(x) object.size(get(x)) ), decreasing = TRUE ) / 10**9
# Taille, en mémoire, d'une base (en Mb)
donnees_tidyverse %>% 
  object.size() %>% 
  format(nsmall = 3, digits = 2, unit = "Mb")

# Taille des objets en mémoire, en Gb
sort( sapply(ls(), function(x) object.size(get(x)) ), decreasing = TRUE ) / 10**9
# Liste des bases de données en mémoire
data.table::tables() 

# Taille, en mémoire, d'une base (en Mb)
format(object.size(donnees_datatable), nsmall = 3, digits = 2, unit = "Mb")

# Taille des objets en mémoire, en Gb
sort( sapply(ls(), function(x) object.size(get(x)) ), decreasing = TRUE ) / 10**9

28.2 Supprimer des bases

/* Supprimer une base */
proc datasets lib = work nolist;delete donnees_sas;run;

/* Supprimer toutes les bases dans la work */
proc datasets lib = work nolist kill;run;
# Supprimer une base
#rm(donnees_rbase)

# Supprimer toutes les bases et tous les objets de la mémoire vive
#rm(list = ls())
# Supprimer une base
#rm(donnees_tidyverse)

# Supprimer toutes les bases et tous les objets de la mémoire vive
#rm(list = ls())
# Supprimer une base
#rm(donnees_datatable)

# Supprimer toutes les bases et tous les objets de la mémoire vive
#rm(list = ls())