38  Fonctions à fenêtre

Les opérateurs classiques tels que + ou - fonctionnent ligne à ligne et s’utilisent pour du calcul classique par exemple avec dplyr::mutate().

Dans le cadre des opérations groupées avec dplyr::summarise() (voir Section 8.3.2), nous avons abordé les fonctions d’agrégation telles que sum() ou mean() qui prennent un ensemble de valeurs et n’en renvoient qu’une seule.

Les fonctions à fenêtre, quant à elles, renvoient autant de valeurs que de valeurs en entrées, mais le calcul, au lieu de se faire ligne à ligne, tient compte des valeurs précédentes et suivantes. Ces fonctions sont donc sensibles au tri du tableau de données.

On peut distinguer les fonctions permettant d’accéder aux valeurs précédentes et suivantes, les fonctions de calcul d’un rang et les fonctions cumulatives.

38.1 Rappels à propos du tri

La fonction dplyr::arrange() permet de trier les valeurs d’un tableau de données. Par exemple, pour trier sur la longueur des pétales :

library(tidyverse)
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.1.4     ✔ readr     2.1.5
✔ forcats   1.0.0     ✔ stringr   1.5.1
✔ ggplot2   3.5.1     ✔ tibble    3.2.1
✔ lubridate 1.9.3     ✔ tidyr     1.3.1
✔ purrr     1.0.2     
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
iris |>
  arrange(Petal.Length) |> 
  head()
  Sepal.Length Sepal.Width Petal.Length Petal.Width Species
1          4.6         3.6          1.0         0.2  setosa
2          4.3         3.0          1.1         0.1  setosa
3          5.8         4.0          1.2         0.2  setosa
4          5.0         3.2          1.2         0.2  setosa
5          4.7         3.2          1.3         0.2  setosa
6          5.4         3.9          1.3         0.4  setosa

Pour un tri décroissant, on utilisera dplyr::desc().

iris |>
  arrange(desc(Petal.Length)) |> 
  head()
  Sepal.Length Sepal.Width Petal.Length Petal.Width   Species
1          7.7         2.6          6.9         2.3 virginica
2          7.7         3.8          6.7         2.2 virginica
3          7.7         2.8          6.7         2.0 virginica
4          7.6         3.0          6.6         2.1 virginica
5          7.9         3.8          6.4         2.0 virginica
6          7.3         2.9          6.3         1.8 virginica

Il est possible de fournir plusieurs variables de tri. Par exemple, pour trier sur l’espèce puis, pour les observations d’une même espace, selon la longueur du sépale de manière décroissante :

iris |>
  arrange(Species, desc(Sepal.Length)) |> 
  head()
  Sepal.Length Sepal.Width Petal.Length Petal.Width Species
1          5.8         4.0          1.2         0.2  setosa
2          5.7         4.4          1.5         0.4  setosa
3          5.7         3.8          1.7         0.3  setosa
4          5.5         4.2          1.4         0.2  setosa
5          5.5         3.5          1.3         0.2  setosa
6          5.4         3.9          1.7         0.4  setosa

Par défaut, arrange() ne tient pas compte des variables de groupement quand elles existent.

iris |> 
  group_by(Species) |> 
  arrange(desc(Sepal.Length))
# A tibble: 150 × 5
# Groups:   Species [3]
   Sepal.Length Sepal.Width Petal.Length Petal.Width Species  
          <dbl>       <dbl>        <dbl>       <dbl> <fct>    
 1          7.9         3.8          6.4         2   virginica
 2          7.7         3.8          6.7         2.2 virginica
 3          7.7         2.6          6.9         2.3 virginica
 4          7.7         2.8          6.7         2   virginica
 5          7.7         3            6.1         2.3 virginica
 6          7.6         3            6.6         2.1 virginica
 7          7.4         2.8          6.1         1.9 virginica
 8          7.3         2.9          6.3         1.8 virginica
 9          7.2         3.6          6.1         2.5 virginica
10          7.2         3.2          6           1.8 virginica
# ℹ 140 more rows

Pour inclure les variables de groupement dans le tri, il faut préciser .by_group = TRUE.

iris |> 
  group_by(Species) |> 
  arrange(desc(Sepal.Length), .by_group = TRUE)
# A tibble: 150 × 5
# Groups:   Species [3]
   Sepal.Length Sepal.Width Petal.Length Petal.Width Species
          <dbl>       <dbl>        <dbl>       <dbl> <fct>  
 1          5.8         4            1.2         0.2 setosa 
 2          5.7         4.4          1.5         0.4 setosa 
 3          5.7         3.8          1.7         0.3 setosa 
 4          5.5         4.2          1.4         0.2 setosa 
 5          5.5         3.5          1.3         0.2 setosa 
 6          5.4         3.9          1.7         0.4 setosa 
 7          5.4         3.7          1.5         0.2 setosa 
 8          5.4         3.9          1.3         0.4 setosa 
 9          5.4         3.4          1.7         0.2 setosa 
10          5.4         3.4          1.5         0.4 setosa 
# ℹ 140 more rows

38.2 Valeurs précédentes et suivantes

La fonction dplyr::lag() permet d’accéder à la valeur précédente d’un vecteur et la fonction dplyr::lead() à la valeur suivante. Il est donc prudent de toujours bien trier son tableau en amont. L’argument n permet d’accéder à la seconde valeur suivante, ou la troisième, etc.

d <- tibble(
  nom = c("marc", "marie", "antoine", "dominique", "michelle"),
  score = c(122, 182, 144, 167, 144),
  groupe = c("a", "a", "a", "b", "b")
)

d |> 
  arrange(desc(score)) |> 
  mutate(
    precedent = lag(nom),
    suivant = lead(nom),
    sur_suivant = lead(nom, n = 2)
  )
# A tibble: 5 × 6
  nom       score groupe precedent suivant   sur_suivant
  <chr>     <dbl> <chr>  <chr>     <chr>     <chr>      
1 marie       182 a      <NA>      dominique antoine    
2 dominique   167 b      marie     antoine   michelle   
3 antoine     144 a      dominique michelle  marc       
4 michelle    144 b      antoine   marc      <NA>       
5 marc        122 a      michelle  <NA>      <NA>       

À noter, cela génère des valeurs manquantes (NA) au début ou à la fin de la nouvelle variable.

38.3 Fonctions de rang

Les fonctions de rang vise à calculer le rang d’un individu, c’est-à-dire sa position quand le vecteur est trié d’une certaine manière. La fonction de base sous R est rank() qui propose plusieurs options. Mais l’on pourra se référer plus facilement aux différentes fonctions disponibles dans dplyr.

La première est dplyr::row_number() qui par défaut va numéroter les lignes du tableau selon le tri actuel.

d |> mutate(rang = row_number())
# A tibble: 5 × 4
  nom       score groupe  rang
  <chr>     <dbl> <chr>  <int>
1 marc        122 a          1
2 marie       182 a          2
3 antoine     144 a          3
4 dominique   167 b          4
5 michelle    144 b          5

On peut optionnellement lui passer une variable de tri pour le calcul du rang.

d |> mutate(rang = row_number(desc(score)))
# A tibble: 5 × 4
  nom       score groupe  rang
  <chr>     <dbl> <chr>  <int>
1 marc        122 a          5
2 marie       182 a          1
3 antoine     144 a          3
4 dominique   167 b          2
5 michelle    144 b          4

Ou encore trier notre tableau en amont.

d |> 
  arrange(desc(score)) |> 
  mutate(rang = row_number())
# A tibble: 5 × 4
  nom       score groupe  rang
  <chr>     <dbl> <chr>  <int>
1 marie       182 a          1
2 dominique   167 b          2
3 antoine     144 a          3
4 michelle    144 b          4
5 marc        122 a          5

Chaque rang est ici unique. En cas d’égalité, les individus sont classés selon l’ordre du tableau. Mais dans cet exemple, il semble injuste de classer Michelle derrière Antoine dans la mesure où ils ont eu le même score. On pourra alors utiliser dplyr::min_rank() qui attribue aux observations égales le premier rang. Ici, Michelle et Antoine seront tous les deux classés 3e et Marc classé 5e.

d |> 
  arrange(desc(score)) |> 
  mutate(rang = min_rank(desc(score)))
# A tibble: 5 × 4
  nom       score groupe  rang
  <chr>     <dbl> <chr>  <int>
1 marie       182 a          1
2 dominique   167 b          2
3 antoine     144 a          3
4 michelle    144 b          3
5 marc        122 a          5

Pour éviter la présence de sauts dans le classement et considéré Marc comme 4e, on utilisera dplyr::dense_rank().

d |> 
  arrange(desc(score)) |> 
  mutate(rang = dense_rank(desc(score)))
# A tibble: 5 × 4
  nom       score groupe  rang
  <chr>     <dbl> <chr>  <int>
1 marie       182 a          1
2 dominique   167 b          2
3 antoine     144 a          3
4 michelle    144 b          3
5 marc        122 a          4

Pour plus d’options, on aura recours à rank(), qui par défaut attribue un rang moyen.

d |> 
  arrange(desc(score)) |> 
  mutate(rang = rank(desc(score)))
# A tibble: 5 × 4
  nom       score groupe  rang
  <chr>     <dbl> <chr>  <dbl>
1 marie       182 a        1  
2 dominique   167 b        2  
3 antoine     144 a        3.5
4 michelle    144 b        3.5
5 marc        122 a        5  

Mais il est possible d’indiquer d’autres méthodes de traitement des égalités, par exemple l’utilisation du rang maximum (l’inverse de min_rank()).

d |> 
  arrange(desc(score)) |> 
  mutate(rang = rank(desc(score), ties.method = "max"))
# A tibble: 5 × 4
  nom       score groupe  rang
  <chr>     <dbl> <chr>  <int>
1 marie       182 a          1
2 dominique   167 b          2
3 antoine     144 a          4
4 michelle    144 b          4
5 marc        122 a          5

La fonction dplyr::percent_risk() renvoie un rang en pourcentage, c’est-à-dire une valeur numérique entre 0 et 1 où 0 représente le plus petit rang et 1 le plus grand.

d |> 
  arrange(desc(score)) |> 
  mutate(rang = percent_rank(desc(score)))
# A tibble: 5 × 4
  nom       score groupe  rang
  <chr>     <dbl> <chr>  <dbl>
1 marie       182 a       0   
2 dominique   167 b       0.25
3 antoine     144 a       0.5 
4 michelle    144 b       0.5 
5 marc        122 a       1   

Enfin, les rangs peuvent être calculés par groupe.

d |> 
  group_by(groupe) |>  
  mutate(rang = min_rank(desc(score))) |> 
  arrange(groupe, rang)
# A tibble: 5 × 4
# Groups:   groupe [2]
  nom       score groupe  rang
  <chr>     <dbl> <chr>  <int>
1 marie       182 a          1
2 antoine     144 a          2
3 marc        122 a          3
4 dominique   167 b          1
5 michelle    144 b          2

38.4 Fonctions cumulatives

R propose nativement plusieurs fonctions cumulatives comme la somme (cumsum()), le minimum (cummin()), le maximum (cummax()) ou encore le produit (cumprod()). dplyr fournit la moyenne cumulée (dplyr::cummean()). Le calcul s’effectue à chaque fois sur les premières lignes du tableau jusqu’à la ligne considérée.

d |> 
  mutate(
    sum = cumsum(score),
    mean = cummean(score),
    min = cummin(score),
    max = cummax(score),
    prod = cumprod(score)
  )
# A tibble: 5 × 8
  nom       score groupe   sum  mean   min   max        prod
  <chr>     <dbl> <chr>  <dbl> <dbl> <dbl> <dbl>       <dbl>
1 marc        122 a        122  122    122   122         122
2 marie       182 a        304  152    122   182       22204
3 antoine     144 a        448  149.   122   182     3197376
4 dominique   167 b        615  154.   122   182   533961792
5 michelle    144 b        759  152.   122   182 76890498048

Le résultat est de facto fortement dépendant du tri du tableau.

d |> 
  arrange(score) |> 
  mutate(
    sum = cumsum(score),
    mean = cummean(score),
    min = cummin(score),
    max = cummax(score),
    prod = cumprod(score)
  )
# A tibble: 5 × 8
  nom       score groupe   sum  mean   min   max        prod
  <chr>     <dbl> <chr>  <dbl> <dbl> <dbl> <dbl>       <dbl>
1 marc        122 a        122  122    122   122         122
2 antoine     144 a        266  133    122   144       17568
3 michelle    144 b        410  137.   122   144     2529792
4 dominique   167 b        577  144.   122   167   422475264
5 marie       182 a        759  152.   122   182 76890498048

Pour des tests sur des valeurs conditions, on pourra avoir recours à dplyr::cumany() ou dplyr::cumall().

d |> 
  mutate(
    cumany = cumany(score > 150)
  )
# A tibble: 5 × 4
  nom       score groupe cumany
  <chr>     <dbl> <chr>  <lgl> 
1 marc        122 a      FALSE 
2 marie       182 a      TRUE  
3 antoine     144 a      TRUE  
4 dominique   167 b      TRUE  
5 michelle    144 b      TRUE  

On peut, notamment dans des analyses longitudinales, avoir besoin de repérer chaque changement d’une certaine valeur. Dans le chapitre sur les conditions logiques, nous avions proposé une fonction is_different() permettant de comparer deux valeurs tout en tenant compte des valeurs manquantes (voir Section 37.2). Nous proposons ici une fonction cumdifferent() permettant de compter les changements de valeurs (et donc d’identifier les lignes continues ayant les mêmes valeurs). Cela est particulièrement utile dans le cadre d’analyses longitudinales.

is_different <- function(x, y) {
  (x != y & !is.na(x) & !is.na(y)) | xor(is.na(x), is.na(y))
}
cumdifferent <- function(x) {
  cumsum(is_different(x, lag(x)))
}
d <- d |> 
  arrange(score) |> 
  mutate(sous_groupe = cumdifferent(groupe))
d
# A tibble: 5 × 4
  nom       score groupe sous_groupe
  <chr>     <dbl> <chr>        <int>
1 marc        122 a                1
2 antoine     144 a                1
3 michelle    144 b                2
4 dominique   167 b                2
5 marie       182 a                3

Dans la cas présent, cela permet d’identifier des sous-groupes, i.e. des lignes contiguës ayant le même groupe : 1 est le sous-groupe de tête du groupe a, 2 le sous-groupe b et 3 le deuxième sous-groupe issu de a.

Une variante est la fonction num_cycle() ci-après1. On doit lui passer une condition / vecteur logique en entrée. Il numérote uniquement les sous-groupes remplissant la condition et renvoie NA sinon.

1 Ne pas oublier de copier également la fonction is_different().

num_cycle <- function(x) {
  if (!is.logical(x))
    stop("'x' should be logical.")
  res <- cumsum(x & is_different(x, lag(x)))
  res[!x] <- NA
  res
}
d |> 
  mutate(
    sous_groupe_a = num_cycle(groupe == "a"),
    sous_groupe_b = num_cycle(groupe == "b")
  )
# A tibble: 5 × 6
  nom       score groupe sous_groupe sous_groupe_a sous_groupe_b
  <chr>     <dbl> <chr>        <int>         <int>         <int>
1 marc        122 a                1             1            NA
2 antoine     144 a                1             1            NA
3 michelle    144 b                2            NA             1
4 dominique   167 b                2            NA             1
5 marie       182 a                3             2            NA