1 - Introduction

Au cours de cette séance, nous allons continuer à nous familiariser avec les packages du tidyverse et ggplot2 pour explorer des données spatiales à l’échelle d’un site. Nous utiliserons le même jeu de données que dans la séance 1 et 2.

2 - Automatiser un code : le principe des boucles

Nous entrons à présent dans la dernière partie de ce TP, et peut-être la plus utile… l’automatisation d’un code ! Ce processus signifie que l’on va demander au code de réaliser plusieurs fois la même opération mais sur des données différentes. Pour cela, il existe deux fonctions permettant de répéter un bout de code tant qu’une certaine condition ne sera pas atteinte : for() et while(). Nous allons les découvrir ensemble sur des exemples très simples, puis nous verrons un exemple avec le jeux de données archéologique.

2.1 - Boucles for()

Une boucle for() permet de répéter un bout de code tant qu’une certaine valeur (par convention, on l’appelle i) se situe dans un certain intervalle.

Créons un objet x contenant la valeur “0”.

x <- 0

Nous allons y ajouter des valeurs à l’aide d’une boucle :

i <- 35 # tout d'abord, on initialise la valeur i

for(i in 35:100) # conditions de la boucle : tant que i se trouve dans ("in") l'intervalle 1 à 5 (défini par ":"), répéter le code suivant.
  { # on ouvre la boucle 
  x <- c(x, i) # on place la valeur i dans l'objet x. 
  
  i <- i + 1 # on incrémente i de 1
}

Affichons le résultat :

x
##  [1]   0  35  36  37  38  39  40  41  42  43  44  45  46  47  48  49  50  51  52
## [20]  53  54  55  56  57  58  59  60  61  62  63  64  65  66  67  68  69  70  71
## [39]  72  73  74  75  76  77  78  79  80  81  82  83  84  85  86  87  88  89  90
## [58]  91  92  93  94  95  96  97  98  99 100

Nous avons bien réussi à créer une suite de nombres ! Cet exemple est certes trivial, mais nous verrons un peu plus tard comment l’appliquer sur des données et avec des opérations plus complexes.

Petite note à ce stade : la dernière ligne de notre boucle, i <- i + 1, est facultative dans R. La fonction for() permet en effet d’incrémenter automatique la valeur i de 1 à chaque fin de boucle.
Voici la preuve :

y <- 0

for(i in 1:5) { 
  y <- c(y, i) 
}

y # même résultat !
## [1] 0 1 2 3 4 5

2.2 - Boucles while()

L’autre type de boucle est moins fréquemment utilisée, mais peut toujours être utile. Il s’agit d’une boucle while() : c’est à dire que l’on va répéter le code tant qu’une certaine condition est remplie. Une fois qu’elle ne l’est plus, la boucle est terminée.

Essayons avec un autre exemple simple : nous allons créer un code qui va afficher une valeur tant que celle-ci est inférieure à 5.

i <- 1 # initialisation de la valeur

while(i <= 5) # cette ligne de code se lit : tant que i est inférieur ou égal à 5, répéter le code suivant
  { # début de la boucle 
  print(i) # afficher "i" dans la console
  
  i <- i + 1 # incrémenter i de 1
             # ATTENTION: l'incrémentation ne se fait pas automatiquement avec while() !!!
} # fin de la boucle
## [1] 1
## [1] 2
## [1] 3
## [1] 4
## [1] 5

Si vous consultez la console, vous remarquez que l’on a affiché la valeur i tant que celle-ci est inférieure à 5 !

Dans la mesure du possible, nous vous conseillons d’utiliser plutôt les boucles for(), car ce sont elles qui sont plus utilisée dans la communauté. while() est également très bien et vous pouvez arriver au même résultat qu’avec une boucle for(), mais l’utilisation de for() fait vraiment partie de la “culture R” en tant que culture de programmation !

2.3 - Exemple de boucle simple : le TP de la séance 1 !

Reprenons ici l’un des exercices proposés dans le premier TP du workshop. Pour rappel, nous travaillons avec le jeu de données DartPoints. Commençons donc par ouvrir le jeu de données.

library(archdata)
data("DartPoints")

Maintenant, nous allons créer un tableau résumant la médiane et l’écart-type des différentes mesures prises sur les pointes de chaque type. Commençons par créer un tableau vide, dont chaque ligne correspond à un type de pointe, et chaque colonne correspond à une valeur statistique.

col <- c("Length_Median", "Length_Range", "Width_Median", "Width_Range", "Thickness_Median", "Thickness_Range")
row <- levels(DartPoints$Name)

df <- data.frame(matrix(nrow = length(row), ncol = length(col)))

colnames(df) <- col
rownames(df) <- row

df
##            Length_Median Length_Range Width_Median Width_Range Thickness_Median
## Darl                  NA           NA           NA          NA               NA
## Ensor                 NA           NA           NA          NA               NA
## Pedernales            NA           NA           NA          NA               NA
## Travis                NA           NA           NA          NA               NA
## Wells                 NA           NA           NA          NA               NA
##            Thickness_Range
## Darl                    NA
## Ensor                   NA
## Pedernales              NA
## Travis                  NA
## Wells                   NA

Notre tableau est créé et il est pour l’instant vide. Remplissons pour commencer la première colonne.

# On commence par sélectionner les lignes qui correspondent aux pointes de type "Darl" dans le jeu de données.
Darl <- DartPoints[DartPoints$Name == "Darl", ]

# Puis on créé les stats pour chaque valeur et on la stocke dans la première ligne de la colonne correspondante.
df$Length_Median[1] <- median(Darl$Length) 
df$Length_Range[1] <- max(Darl$Length) - min(Darl$Length)
df$Width_Median[1] <- median(Darl$Width) 
df$Width_Range[1] <- max(Darl$Width) - min(Darl$Width)
df$Thickness_Median[1] <- median(Darl$Thickness) 
df$Thickness_Range[1] <- max(Darl$Thickness) - min(Darl$Thickness)

df
##            Length_Median Length_Range Width_Median Width_Range Thickness_Median
## Darl               40.15         23.6        16.95         8.8             5.85
## Ensor                 NA           NA           NA          NA               NA
## Pedernales            NA           NA           NA          NA               NA
## Travis                NA           NA           NA          NA               NA
## Wells                 NA           NA           NA          NA               NA
##            Thickness_Range
## Darl                   4.1
## Ensor                   NA
## Pedernales              NA
## Travis                  NA
## Wells                   NA

Faire cela pour tous les types serait très long et verbeux (ie., trop de lignes de code!), ce qui rend le code moins lisible et moins facilement adaptable ! Or, nous allons exécuter les mêmes opérations en changeant uniquement le type de pointe. C’est un cas idéal pour créer un loop. / Voyons donc comment faire :

for(i in 1:nrow(df)){ # on veut que le code se répète pour chaque type de pointe ; nous demandons donc à la machine de répéter le code suivant tant que i se trouve dans l'intervalle [1 ; nombre de types (=nombre de lignes dans le tableau, d'où la fonction nrow())]
  
  type <- DartPoints[DartPoints$Name == levels(DartPoints$Name)[i], ] # on sélectionne les lignes correspondant au                                                                         ième type 
  
  # on calcule les statistiques et on les range dans la ième ligne pour chaque colonne 
  df$Length_Median[i] <- median(type$Length) 
  df$Length_Range[i] <- max(type$Length) - min(type$Length)
  df$Width_Median[i] <- median(type$Width) 
  df$Width_Range[i] <- max(type$Width) - min(type$Width)
  df$Thickness_Median[i] <- median(type$Thickness) 
  df$Thickness_Range[i] <- max(type$Thickness) - min(type$Thickness)
}

Vérifions notre travail…

df
##            Length_Median Length_Range Width_Median Width_Range Thickness_Median
## Darl               40.15         23.6        16.95         8.8             5.85
## Ensor              42.30         20.3        21.10         9.8             6.25
## Pedernales         55.75         74.1        26.85        30.8             8.10
## Travis             49.10         29.4        21.10         4.5             8.30
## Wells              53.65         24.2        23.50        10.7             7.05
##            Thickness_Range
## Darl                   4.1
## Ensor                  2.8
## Pedernales             4.3
## Travis                 3.2
## Wells                  3.5

Notre tableau s’est rempli automatiquement avec les bonnes valeurs ! / Bravo, vous avez réalisé votre première boucle sur R !

2.4 - Exemple sur le jeu de données du site de la Forêt

Mettons en pratique ce concept sur le jeu de données du site de la Forêt. Nous allons créer plusieurs plans de répartition en fonction de la dimensions des pièces. /

Préparons un peu notre script en ouvrant des bibliothèques

## ── 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

A présent, ouvrons le jeu de données

jdd <- as_tibble(read.csv("data/data_exemple.csv", header = TRUE, sep = ","))
head(jdd)
## # A tibble: 6 × 8
##   numero type          x_cm  y_cm longueur_cm largeur_cm silex  raccord
##    <int> <chr>        <int> <int>       <dbl>      <dbl> <chr>    <int>
## 1      1 gravette        10    60         5          1.5 type A      NA
## 2      2 gravette        30    50         5.9        1.8 type C      NA
## 3      3 picardie        50    80         3.2        1.2 type B       1
## 4      4 percoir        260   100         4.7        2.1 type B      NA
## 5      5 burin          240   120         6          2.4 type B       2
## 6      6 burin diedre   280   150         8          3.5 type A      NA
jdd$type_fac <- factor(jdd$type, 
                        # Généralement, les levels sont dans l'ordre alphabétique ; il est possible de changer leur   ordre si vous le voulez !
                       levels = c("nucleus a eclats laminaires", "burin du Raysse", 
                                  "eclat", "lame", "chute de burin", "burin", 
                                  "burin diedre", "percoir", "picardie", "gravette"))
levels(jdd$type_fac)
##  [1] "nucleus a eclats laminaires" "burin du Raysse"            
##  [3] "eclat"                       "lame"                       
##  [5] "chute de burin"              "burin"                      
##  [7] "burin diedre"                "percoir"                    
##  [9] "picardie"                    "gravette"

Tout a l’air en ordre ! Nous pouvons donc commencer :) /

Je copie-colle tout d’abord le code qui permet de créer notre fond de plan. Un petit message d’erreur s’affiche, indiquant que les levels des valeurs ne correspondent pas aux échelles de couleur manuelles. C’est tout à fait normal car nous n’avons pas encore “nourrit” notre code avec les enregistrements ! Nous verrons plus tard comment les ajouter au moyen d’une boucle et de la fonctionnalité “+” de ggplot2

carres.x <- c("A", "B", "C", "D")
carres.y <- c("1", "2", "3", "4")
shape.pal <- c("nucleus a eclats laminaires" = 7, "burin du Raysse" = 12, 
               "eclat" = 22, "lame" = 23, "chute de burin" = 21, "burin" = 8, 
               "burin diedre" = 24, "percoir" = 10, "picardie" = 14, 
               "gravette" = 3)
color.pal <- c("type A" = "#7AD151FF", "type B" = "#2A788EFF", "type C" = "#440154FF")

plan <- ggplot2::ggplot() +
  theme_linedraw() +
  theme(axis.ticks = element_blank(), 
        axis.text.x = element_blank(), 
        axis.text.y = element_blank(), 
        panel.grid.major = element_line(),
        panel.grid.minor = element_blank()) + 
  
  scale_x_continuous(limits = c(0, 400), breaks = seq(0, 400, by = 100)) +
  scale_y_continuous(limits = c(0, 400), breaks = seq(0, 400, by = 100)) +
  
  ylab(element_blank()) +
  xlab(element_blank()) +
  annotate(geom = "text", x = c(seq(from = 50, to = 400, by = 100)), y = 10, label = carres.x) +
  annotate(geom = "text", x = 10, y = c(seq(from = 50, to = 400, by = 100)), label = carres.y) +
  
  scale_shape_manual(values = shape.pal) +
  scale_color_manual(values = color.pal) +
  scale_fill_manual(values = color.pal)

plan
## Warning: No shared levels found between `names(values)` of the manual scale and the
## data's shape values.
## Warning: No shared levels found between `names(values)` of the manual scale and the
## data's colour values.
## Warning: No shared levels found between `names(values)` of the manual scale and the
## data's fill values.

Tout d’abord, rafraîchissons nous la mémoire sur les statistiques basiques du jeu de données.

summary(jdd)
##      numero       type                x_cm            y_cm        longueur_cm  
##  Min.   : 1   Length:33          Min.   : 10.0   Min.   : 50.0   Min.   : 1.4  
##  1st Qu.: 9   Class :character   1st Qu.: 50.0   1st Qu.: 90.0   1st Qu.: 4.5  
##  Median :17   Mode  :character   Median :210.0   Median :280.0   Median : 5.9  
##  Mean   :17                      Mean   :174.8   Mean   :217.6   Mean   : 5.8  
##  3rd Qu.:25                      3rd Qu.:260.0   3rd Qu.:340.0   3rd Qu.: 7.4  
##  Max.   :33                      Max.   :290.0   Max.   :390.0   Max.   :10.2  
##                                                                                
##    largeur_cm       silex              raccord               type_fac 
##  Min.   :0.500   Length:33          Min.   :1.0   eclat          :15  
##  1st Qu.:1.900   Class :character   1st Qu.:1.0   lame           : 8  
##  Median :2.800   Mode  :character   Median :1.5   percoir        : 2  
##  Mean   :2.976                      Mean   :1.5   gravette       : 2  
##  3rd Qu.:3.700                      3rd Qu.:2.0   burin du Raysse: 1  
##  Max.   :7.100                      Max.   :2.0   (Other)        : 4  
##                                     NA's   :29    NA's           : 1

La longueur des pièces se situe entre 1.4 et 10.2 cm. Je vous propose donc de créer un plan pour des intervalles de longueurs tous les 2 cm. Nous allons donc devoir combiner les commandes de subsetting et le loop for().

i <- 1
pace <- 2 # ici il s'agit de la valeur d'intervalle ; dans ce cas concret, il est nécessaire de la spécifier étant donné qu'elle est différente de la valeur par défaut (1)

Je crée également un dossier afin de pouvoir sauvegarder les plots au fur et à mesure.

dir.create("figures/plans_fct_longueur")
## Warning in dir.create("figures/plans_fct_longueur"):
## 'figures/plans_fct_longueur' existe déjà

Démarrons la boucle !

for(i in 1:10) { # Cette boucle va s'exécuter tant que i sera compris entre 1 et 10
  
  ## Je commence par créer les bornes de l'intervalle i 
  borne.min <- i # le minimum est i 
  borne.max <- i + pace # et le maximum est i + pace, soit i + 2
  
  ## Ensuite, je subset le jeux de données pour ne sélectionner que les pièces dont la longueur est comprise entre la borne min et la borne max de mon intervalle. 
  repart <- subset(jdd, longueur_cm >= borne.min & longueur_cm <= borne.max)
  
  ## maintenant, on crée le plan de répartition avec les données sélectionnées
  plan +
    geom_point(data = repart, mapping = aes(x = x_cm, y = y_cm, color = silex, 
                                              fill = silex, shape = type_fac, size = 1)) +
    # titre et légende
    labs(title = paste0("Répartition spatiale des pièces dont L est comprise entre ", borne.min,  " et ", borne.max, "cm"),
         ### NB : ici, on va utiliser la fonction paste0() pour adapter le titre de la figure à chaque intervalle. 
         ### Cette fonction est extrêmement utile, retenez la ! Elle permet de coller côte à côte des objets en les transformant en texte. 
         color = "Matière première",
         shape = "Catégorie d'objet") +
    guides(fill = "none", size = "none")
    
  ## Enfin, on sauvegarde la figure dans le dossier "plans_fct_longueur" en adaptant son titre avec paste() - cette fonction est un peu différente de paste0() en cela qu'elle recquiert que l'on précise un séparateur à ajouter entre les blocs de texte. Ici ce sera "_" (argument "sep"). 
  ggsave(paste("figures/plans_fct_longueur/repart_pieces_entre ", borne.min, "et", borne.max, ".png", sep = "_"), 
         width = 6, height = 4)
}
## Warning: Removed 1 row containing missing values or values outside the scale range
## (`geom_point()`).
## Removed 1 row containing missing values or values outside the scale range
## (`geom_point()`).

FELICITATIONS ! Vous avez fait vos premières boucles sur R !!