Chapter 4 Pipe natif, within() et with()

La syntaxe de R base peut être rendue plus lisible avec le pipe natif de R |>, ainsi que les fonctions within() et with().

Nous allons reprendre les éléments de l’analyse réalisée au chapitre 3 en utilisant cette syntaxe complémentaire.

Commençons par supprimer l’ensemble des éléments de l’environnement, puis importons à nouveau les bases df_1 et meta_df_1 :

### Vider l'environnement
rm(list = ls())

### Importer df_1 et meta_df_1
df_1 <- read.csv2("data/df_1.csv")
meta_df_1 <- read.csv2("data/meta_df_1.csv")

4.1 Pipe natif de R

Le principe du “pipe” est une syntaxe de programmation qui a été initialement introduite avec le package maggritr au sein de la famille de packages du “tidyverse”. Ce principe a été repris dans R base à partir de sa version 4.1.0.

Le pipe natif s’écrit avec une barre verticale suivi d’un signe supérieur |>.

La syntaxe est la suivante :

  • L’élément qui se site à gauche du pipe (LHS, left hand side) …
  • … est transmis au premier argument de la fonction à droite du pipe (RDS, right hand side).

x |> funct(arguments = ...) est équivalent à funct(x, argument = ...)

Par exemple :

df_1 |> head() # est équivalent de head(df_1)
##   subjid sex  imc trait pas
## 1      1   0 24.8     2 140
## 2      2   0 24.1     3 109
## 3      3   0 26.4     1 156
## 4      4   0 23.3     2 124
## 5      5   0 25.4     2 131
## 6      6   1 25.0     3 148
df_1 |> tail()  # est équivalent de tail(df_1)
##     subjid sex  imc trait pas
## 295    295   1 24.1     1 148
## 296    296   0 18.7     3 121
## 297    297   0 23.3     3 111
## 298    298   1 27.5     3 134
## 299    299   1 24.7     2 158
## 300    300   1 22.8     1 147
df_1 |> str() # est équivalent de str(df_1)
## 'data.frame':    300 obs. of  5 variables:
##  $ subjid: int  1 2 3 4 5 6 7 8 9 10 ...
##  $ sex   : int  0 0 0 0 0 1 0 0 0 0 ...
##  $ imc   : num  24.8 24.1 26.4 23.3 25.4 25 25.2 21.5 21.8 25.9 ...
##  $ trait : int  2 3 1 2 2 3 3 3 1 1 ...
##  $ pas   : int  140 109 156 124 131 148 125 117 132 133 ...

Cela permet d’avoir un code dont la décomposition est plus lisible, en enchaînant les pipes les uns après les autres :

# au lieu d'écrire : 
round(mean(df_1$imc, na.rm = TRUE), digits = 1)
## [1] 24.5
# on peut écrire, de manière équivalente, avec 2 pipes qui s'enchaînent :
df_1$imc |> 
  mean(na.rm = TRUE) |>
  round(digits = 1)
## [1] 24.5
# df_1$imc a été pris comme premier argument de mean(x, na.rm = TRUE)
# puis le résultat a été pris comme premier argument de round(x, digits = 1)

Au lieu de “transférer” l’élément à gauche du pipe au premier argument de la fonction à droite du pipe, il est possible de le transférer à n’importe quel argument à droite du pipe à l’aide du “placeholder” _ (underscore).

x |> funct(y, argument = _) est équivalent à funct(y, argument = x). Par exemple :

paste0("la moyenne de l'IMC est ",
       round(mean(df_1$imc, na.rm = TRUE), digits = 1),
       " kg/m2")
## [1] "la moyenne de l'IMC est 24.5 kg/m2"
### on peut placer le résultat de la moyenne arrondi en 2ème position des 
### arguments dans la fonction paste0() avec le placeholder "_"
df_1$imc |> 
  mean(na.rm = TRUE) |>
  round(digits = 1) |>
  paste0("la moyenne de l'IMC est ", 
         a = _, # argument = placeholder 
         " kg/m2")
## [1] "la moyenne de l'IMC est 24.5 kg/m2"

4.2 Fonction within() : créer/modifier des variables dans une base de données

La fonction within() permet d’éviter d’utiliser l’opérateur dollar $ lorsque vous souhaitez :

  • créer de nouvelles variables dans une base de données,
  • modifier des variables déjà existantes,
  • transformer les données selon des tâches spécifiques.

La syntaxe within(df, newvar <- ...) va créer la variable newvar dans la base df. Pour sauvegarder cette nouvelle variable dans la base, il faudra assigner le résultat à la base souhaitée :

df_1 <- within(df_1, 
               sexL <- factor(sex,
                              labels = meta_df_1$labs[meta_df_1$var == "sex"]))
### est équivalent : 
# df_1$sexL <- factor(df_1$sex,
#                     labels = meta_df_1$labs[meta_df_1$var == "sex"])

On peut créer plusieurs variables en même temps, en indiquant l’ensemble des commandes entre parenthèses (note : les variables sont incluses dans l’ordre inverse de création dans la commande) :

df_1 <- within(df_1, {
  sexL <- factor(sex, 
                 labels = meta_df_1$labs[meta_df_1$var == "sex"])
  traitL <- factor(trait,
                   labels = meta_df_1$labs[meta_df_1$var == "trait"])
  obesite <- ifelse(imc >= 30, 1, 0)
  imc_cl <- rep(NA, nrow(df_1))
  imc_cl[df_1$imc < 18.5] <- 1
  imc_cl[df_1$imc >= 18.5 & df_1$imc < 25] <- 2
  imc_cl[df_1$imc >= 25 & df_1$imc < 30] <- 3
  imc_cl[df_1$imc >= 30] <- 4
})

head(df_1)
##   subjid sex  imc trait pas     sexL imc_cl obesite       traitL
## 1      1   0 24.8     2 140  Féminin      2       0 Traitement A
## 2      2   0 24.1     3 109  Féminin      2       0 Traitement B
## 3      3   0 26.4     1 156  Féminin      3       0      Placebo
## 4      4   0 23.3     2 124  Féminin      2       0 Traitement A
## 5      5   0 25.4     2 131  Féminin      3       0 Traitement A
## 6      6   1 25.0     3 148 Masculin      3       0 Traitement B

On voit que l’opérateur dollar $ n’a été utilisé que pour aller récupérer l’information des labels dans la base de méta-données, ainsi que pour la sélection par indexation pour créer la variable imc_cl.

On peut également modifier une variable (l’opérateur dollar $ n’est utilisé que pour la sélection par indexation):

### Remplacer la valeur de PAS de l'individu n°137 par 123 mmHg :
df_1 <- within(df_1,
               pas[df_1$subjid == 137] <- 123)

4.3 Fonction with() : analyser des variables dans une base de données

La fonction with() permet d’éviter d’utiliser l’opérateur $ lorsque l’on souhaite :

  • faire des calculs statistiques appliqués aux variables d’une base de données
  • faire des calculs temporaires (quand il n’y a pas besoin de modifier les données de manière permanente)

Par exemple, pour calculer la moyenne de l’IMC dans la base df_1 :

# la commande ...
mean(df_1$imc) 
## [1] 24.481
# ... est équivalente à la commande 
with(df_1, mean(imc))
## [1] 24.481

On peut combiner la fonction with() avec le pipe natif :

# la commande suivante ...
paste0("la moyenne de l'IMC est égale à ", 
       round(mean(df_1$imc, na.rm = TRUE),
             digits = 1),
       " kg/m2")
## [1] "la moyenne de l'IMC est égale à 24.5 kg/m2"
# ... est équivalente à ...
with(df_1,
     imc |> 
       mean(na.rm = TRUE) |>
       round(digits = 1) |>
       paste0("la moyenne de l'IMC est égale à ", 
              a = _, # résultat positionné avec le "placeholder" underscore
              " kg/m2"))
## [1] "la moyenne de l'IMC est égale à 24.5 kg/m2"

4.4 Exemples d’applications

Nous pouvons utiliser ces éléments de syntaxe pour les analyses réalisées au chapitre 2.

### On reprend notre fonction "maison" d'analyse quantitative univariée 
univ_quanti <- function(x, dig = 2, remove_miss = TRUE, details = TRUE) { 
  # calculer les effectifs, la moyenne, l'écart type et les quantiles
  n <- length(x[!is.na(x)])    
  moy <- mean(x, na.rm = remove_miss)
  sd <- sd(x, na.rm = remove_miss)
  q <- quantile(x, probs = c(0, 0.25, 0.5, 0.75, 1), na.rm = remove_miss)
  
  # stocker les résultat dans le vecteur "param"
  if (details == TRUE) {
    param <- c(n, 
               round(moy, digits = dig), 
               round(sd, digits = dig), 
               q)
  } else {
    param <- c(n, 
               round(moy, digits = dig), 
               round(sd, digits = dig))
  }
  
  # ajouter un nom à chaque élément du vecteur
  if (details == TRUE) {
    names(param) <- c("N", "mean", "sd", "min", "Q1", "median", "Q3", "max")
  } else {
    names(param) <- c("N", "mean", "sd")
  }
  
  # retourne les résultats
  return(param)
}

4.4.1 Analyses univariées

Pour les variables quantitatives IMC et PAS :

with(df_1,
     # pour des analyses répétées sur plusieurs élémnets de df_1,
     # on peut les stocker dans une liste
     list(IMC = univ_quanti(imc, dig = 1, remove_miss = TRUE, details = TRUE),
          PAS = univ_quanti(pas, dig = 1, remove_miss = TRUE, details = TRUE))
     )
## $IMC
##      N   mean     sd    min     Q1 median     Q3    max 
##  300.0   24.5    3.1   15.4   22.3   24.6   26.4   32.5 
## 
## $PAS
##      N   mean     sd    min     Q1 median     Q3    max 
##  300.0  137.1   16.8   92.0  125.0  138.0  149.0  177.0

Pour les variables qualitatives (dans leur format de facteurs sexL et traitL) :

with(df_1,
     list(SEX = cbind(N = table(sexL),
                      pct = round(prop.table(table(sexL)) * 100, digits = 1),
                      deparse.level = 2), # ajoute les noms de colonnes
          TRAITEMENT = cbind(N = table(traitL),
                             pct = round(prop.table(table(traitL)) * 100, 
                                         digits = 1), 
                             deparse.level = 2)))
## $SEX
##            N pct
## Féminin  153  51
## Masculin 147  49
## 
## $TRAITEMENT
##                N  pct
## Placebo      120 40.0
## Traitement A  91 30.3
## Traitement B  89 29.7

4.4.2 Analyses bivariées

4.4.2.1 Comparer deux moyennes

Comparer la PAS en fonction du sexe :

### Tableau descriptif bi-varié
with(df_1,
     pas |>
       aggregate(by = list(sexL), 
                 FUN = univ_quanti, # fonction à utiliser
                 dig = 1, remove_miss = TRUE, details = FALSE)) 
##    Group.1   x.N x.mean  x.sd
## 1  Féminin 153.0  132.4  16.8
## 2 Masculin 147.0  142.0  15.5
### Test de Student
with(df_1,
     t.test(pas ~ sexL)) 
## 
##  Welch Two Sample t-test
## 
## data:  pas by sexL
## t = -5.1382, df = 297.44, p-value = 5.029e-07
## alternative hypothesis: true difference in means between group Féminin and group Masculin is not equal to 0
## 95 percent confidence interval:
##  -13.233087  -5.903634
## sample estimates:
##  mean in group Féminin mean in group Masculin 
##               132.4248               141.9932
### On peut récupérer uniquement la p-value
# rappel : la p-value peut être récupéré avec l'opérateur dollar $
#          appliqué au résultat de la fonction t.test
with(with(df_1,
          t.test(pas ~ sexL)), 
     p.value)
## [1] 5.028976e-07
### Test de Levene pour vérifier l'égalité des variances
with(df_1,
     pas |> car::leveneTest(group = sexL))
## Levene's Test for Homogeneity of Variance (center = median)
##        Df F value Pr(>F)
## group   1  1.0178 0.3139
##       298
### Test de Wilcoxon si les conditions d'application ne sont pas vérifiées
with(df_1,
     wilcox.test(pas ~ sexL))
## 
##  Wilcoxon rank sum test with continuity correction
## 
## data:  pas by sexL
## W = 7609.5, p-value = 1.29e-06
## alternative hypothesis: true location shift is not equal to 0

4.4.2.2 Comparer 3 moyennes ou plus

Comparer la PAS en fonction du traitement :

### Tableau descriptif bi-varié
with(df_1,
     pas |>
       aggregate(by = list(traitL), 
                 FUN = univ_quanti, # fonction à utiliser
                 dig = 1, remove_miss = TRUE, details = FALSE)) 
##        Group.1   x.N x.mean  x.sd
## 1      Placebo 120.0  141.4  15.6
## 2 Traitement A  91.0  130.7  16.4
## 3 Traitement B  89.0  137.8  17.0
### appliquer le test d'Anova au modèle linéaire pas ~ traitL
with(df_1,
     lm(pas ~ traitL) |>
       anova())
## Analysis of Variance Table
## 
## Response: pas
##            Df Sum Sq Mean Sq F value    Pr(>F)    
## traitL      2   5940 2970.18  11.215 2.016e-05 ***
## Residuals 297  78656  264.83                      
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
### Vérification des conditions d'applications
### 1) égalité des variances par le test de Levene
with(df_1,
     pas |> car::leveneTest(group = traitL))
## Levene's Test for Homogeneity of Variance (center = median)
##        Df F value Pr(>F)
## group   2  0.6669  0.514
##       297
### 2) normalité des résidus du modèle linéaire
res <- with(with(df_1, lm(pas ~ traitL)),
            residuals)
qqnorm(res)
qqline(res)

### Réaliser un test de Kruskal-Wallis si les conditions ne sont pas vérifiées
with(df_1,
     kruskal.test(pas ~ traitL))
## 
##  Kruskal-Wallis rank sum test
## 
## data:  pas by traitL
## Kruskal-Wallis chi-squared = 19.04, df = 2, p-value = 7.336e-05

4.4.2.3 Comparer des pourcentages

Description de la répartition du sexe en fonction du traitement, et application du test du Chi-2.

### Tableau descriptif bivarié
with(df_1, 
     paste0(table(sexL, traitL),
            " (",
            round(prop.table(table(sexL, trait), margin = 2) * 100,
                  digits = 1),
            "%)") |>
       matrix(nrow = 2, ncol = 3, byrow = FALSE, 
              dimnames = dimnames(table(sexL, traitL))))
##           traitL
## sexL       Placebo      Traitement A Traitement B
##   Féminin  "57 (47.5%)" "46 (50.5%)" "50 (56.2%)"
##   Masculin "63 (52.5%)" "45 (49.5%)" "39 (43.8%)"
### test du chi-2
with(df_1, 
     table(sexL, traitL) |>
       chisq.test())
## 
##  Pearson's Chi-squared test
## 
## data:  table(sexL, traitL)
## X-squared = 1.5512, df = 2, p-value = 0.4604
# effectifs attendus :
with(with(df_1, 
          table(sexL, traitL) |>
            chisq.test()),
     expected)
##           traitL
## sexL       Placebo Traitement A Traitement B
##   Féminin     61.2        46.41        45.39
##   Masculin    58.8        44.59        43.61

4.4.2.4 Corrélations

Estimation des corrélations de Pearson et de Spearman, avec test de leurs hypothèse nulles \(\rho = 0\).

with(df_1,
     cor.test(imc, pas, method = "pearson"))
## 
##  Pearson's product-moment correlation
## 
## data:  imc and pas
## t = 3.8534, df = 298, p-value = 0.0001427
## alternative hypothesis: true correlation is not equal to 0
## 95 percent confidence interval:
##  0.1072647 0.3231284
## sample estimates:
##       cor 
## 0.2178593
with(df_1,
     cor.test(imc, pas, method = "spearman"))
## Warning in cor.test.default(imc, pas, method = "spearman"): Impossible de
## calculer la p-value exacte avec des ex-aequos
## 
##  Spearman's rank correlation rho
## 
## data:  imc and pas
## S = 3522826, p-value = 0.0001503
## alternative hypothesis: true rho is not equal to 0
## sample estimates:
##      rho 
## 0.217141

4.4.3 Analyse multivariée

Estimation de la régression linéaire multiple de la PAS en fonction du traitement, ajusté sur le sexe et l’IMC :

with(df_1,
     lm(pas ~ traitL + sexL + imc) |>
       summary())
## 
## Call:
## lm(formula = pas ~ traitL + sexL + imc)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -38.871 -10.083  -0.342  10.941  41.171 
## 
## Coefficients:
##                    Estimate Std. Error t value Pr(>|t|)    
## (Intercept)        109.5668     7.1403  15.345  < 2e-16 ***
## traitLTraitement A -10.9623     2.1246  -5.160 4.55e-07 ***
## traitLTraitement B  -2.8373     2.1374  -1.327 0.185388    
## sexLMasculin         8.5634     1.7796   4.812 2.39e-06 ***
## imc                  1.1240     0.2904   3.870 0.000134 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 15.24 on 295 degrees of freedom
## Multiple R-squared:  0.1902, Adjusted R-squared:  0.1792 
## F-statistic: 17.32 on 4 and 295 DF,  p-value: 8.911e-13