Matrices et tableaux de données
Passons maintenant aux données à deux dimensions, des lignes et des colonnes (pour une fois que c'est pas des trucs et des Hommes).
Matrices
Commençons par les matrices.
Création de matrices
Pour créer une matrice, une commande matrix(valeurs, nrow = nombre de lignes, ncol = nombre de colonnes):
m <- matrix(1:24, nrow = 4, ncol = 6)
m
# [,1] [,2] [,3] [,4] [,5] [,6]
#[1,] 1 5 9 13 17 21
#[2,] 2 6 10 14 18 22
#[3,] 3 7 11 15 19 23
#[4,] 4 8 12 16 20 24
Créer une matrice à partir de vecteurs est aussi tout à fait possible (c'est même souvent le plus simple). Cette fois-ci, nous allons prendre l'exemple d'un site fouillé en carroyage (la joie...) avec le nombre de mobilier mis au jour dans chaque carré. Les carrés sont nommés classiquement en lettres pour l'axe nord-sud et en chiffres pour l'axe est-ouest (parce que tous les sites paléo/méso sont hyper bien orientés, c'est bien connu) et l'emprise du site est de 6 carrés de 1m de côté sur 6, donc 36 mètres carrés si on sait faire un angle droit (allez, 3-4-5, c'est toujours mieux qu'à la boussole
). On va faire un vecteur par ligne (et je vais utiliser une fonction de génération aléatoire parce que je suis un petit fou (en vrai, comme ça je suis sûr de bien avoir 6 valeurs par vecteur, sinon je sais que je vais encore me planter)) :
A <- round(rnorm(6,10,2),0)
B <- round(rnorm(6,34,8),0)
C <- round(rnorm(6,43,7),0)
D <- round(rnorm(6,26,3),0)
E <- round(rnorm(6,52,12),0)
F <- round(rnorm(6,28,5),0)
site <- matrix(c(A,B,C,D,E,F), nrow=6, byrow=TRUE)
J'ai mis des nombres légèrement élevés et un écart-type faible histoire de ne pas avoir de nombre négatif (sinon, je pouvais les mettre au carré (avec ^2), je sais). Pour éviter les décimales, j'ai inséré ma génération aléatoire dans la fonction round() qui comprend principalement deux arguments, d'abord les valeurs, puis le nombre de décimales autorisées (ici, 0).
L'option byrow (rien à voir avec le romantisme anglais) permet de remplir le tableau par ligne et pas par colonnes qui est l'option par défaut.
Évidemment, il y a plein d'autres fonctions et surtout des arguments pour créer des matrices, mais je ne suis pas sûr qu'on en ait vraiment besoin dans le cadre de cette formation.
Nommage des lignes et des colonnes
Alors comme ça, c'est pas pratique, on voudrait pouvoir nommer les lignes et les colonnes. Bien sûr, il y a des commandes pour ça.
rownames(site) <- c("A","B","C","D","E","F")
colnames(site) <- c("1","2","3","4","5","6")
site
# 1 2 3 4 5 6
#A 5 10 11 6 8 15
#B 17 51 30 42 35 37
#C 50 38 34 29 33 40
#D 24 23 25 25 28 21
#E 55 56 72 60 53 57
#F 24 27 27 35 25 28
Pour les noms de colonnes, il est vrai que je n'étais pas obligé de mettre les nombres entre parenthèses. C'est surtout une façon de bien distinguer ces éléments qui sont sous forme de nombre, mais qui, ici, ont valeur d'identifiant (donc traitables comme du texte et ne devant pas servir aux calculs).
Maintenant que nous sommes dans des données à deux dimensions, c'est le moment de vous indiquer la commande View() qui affiche des données dans la même zone que le script et permet de visualiser des tableurs de façon un peu plus user friendly ("gentil pour les utilisateurices").
Observation de matrices et position
Quelques fonctions de description sont particulièrement utiles pour les matrices :
dim(site)
#[1] 6 6
# Renvoie le nombre de lignes suivi du nombre de colonnes (retenez bien cet ordre)
nrow(site)
[1] 6
# Le nombre de lignes
ncol(site)
#[1] 6
# Nombre de colonnes
Et si vous faites str() et surtout summary(), vous allez halluciner (en tout cas, moi, le summary() sur matrice et surtout (on le voit bientôt), sur un tableau de données, j'aime vraiment beaucoup).
str(site)
# num [1:6, 1:6] 5 17 50 24 55 24 10 51 38 23 ...
# - attr(*, "dimnames")=List of 2
# ..$ : chr [1:6] "A" "B" "C" "D" ...
# ..$ : chr [1:6] "1" "2" "3" "4" ...
summary(site)
# 1 2 3 4 5 6
# Min. : 5.00 Min. :10.00 Min. :11.00 Min. : 6.00 Min. : 8.00 Min. :15.00
# 1st Qu.:18.75 1st Qu.:24.00 1st Qu.:25.50 1st Qu.:26.00 1st Qu.:25.75 1st Qu.:22.75
# Median :24.00 Median :32.50 Median :28.50 Median :32.00 Median :30.50 Median :32.50
# Mean :29.17 Mean :34.17 Mean :33.17 Mean :32.83 Mean :30.33 Mean :33.00
# 3rd Qu.:43.50 3rd Qu.:47.75 3rd Qu.:33.00 3rd Qu.:40.25 3rd Qu.:34.50 3rd Qu.:39.25
# Max. :55.00 Max. :56.00 Max. :72.00 Max. :60.00 Max. :53.00 Max. :57.00
Vous avez vu, le summary, c'est pas mind blowing ? Et encore, vous n'avez pas vu sur les tableaux de données ! 
Pour trouver une position dans une matrice, c'est presque identique à la méthode pour un vecteur, sauf que nous somme ici à deux dimensions. On va taper matrice[ligne,colonne] avec soit une indication de ligne ou de colonne (et on laisse vide le côté qui n'est pas renseigné), soit les deux:
site[2,]
# 1 2 3 4 5 6
#17 51 30 42 35 37
# Nous retourne tous les éléments de la deuxième ligne
site[,4]
# A B C D E F
# 6 42 29 25 60 35
# Nous retourne tous les éléments de la quatrième colonne
site[2,4]
[1] 42
# Renvoie l'élément situé à la deuxième ligne et la quatrième colonne
Et puis une dernière chose que je n'avais pas mentionnée avant (parce que ça me semble simplement plus adapté dans un tableau à deux dimensions qu'un vecteur), on peut utiliser le nom d'une ligne ou d'une colonne pour observer une position :
site[,"4"]
# A B C D E F
# 6 42 29 25 60 35
# Bon, évidemment, le numéro de la colonne est le même que sa position, alors c'est pas flagrant, mais il faut me croire
site["D",]
# 1 2 3 4 5 6
#24 23 25 25 28 21
Opérations sur matrices
Comme pour les vecteurs, les opérations de base sont appliquées à l'ensemble des éléments d'une matrice :
site/2
1 2 3 4 5 6
A 2.5 5.0 5.5 3.0 4.0 7.5
B 8.5 25.5 15.0 21.0 17.5 18.5
C 25.0 19.0 17.0 14.5 16.5 20.0
D 12.0 11.5 12.5 12.5 14.0 10.5
E 27.5 28.0 36.0 30.0 26.5 28.5
F 12.0 13.5 13.5 17.5 12.5 14.0
# Oui, c'est pas vraiment une question intéressante...
Comme pour les vecteurs, on peut appliquer des opérations valeur par valeur (possédant la même position d'une matrice à l'autre), seule condition, elles doivent posséder les mêmes dimensions (toujours comme pour des vecteurs, vraiment rien de fifou).
site2 <- matrix(c(round(rnorm(6,78,12)), round(rnorm(6,68,14)), round(rnorm(6,104,21)), round(rnorm(6,98,13)), round(rnorm(6,48,9)), round(rnorm(6,63,9))), byrow=TRUE, nrow=6)
site-site2
# 1 2 3 4 5 6
#A -66 -84 -68 -70 -69 -52
#B -42 -6 -23 -17 -35 -44
#C -63 -49 -108 -39 -102 -50
#D -81 -90 -50 -64 -50 -79
#E 19 19 26 11 1 0
#F -44 -41 -36 -33 -33 -39
Mettons que le "site" soit une US et "site2", une US suivante, on peut commencer à faire quelques recherches matricielles bien sympas ! :-)
Quelques fonctions supplémentaires qui s'avèreront probablement bien pratique un jour :
colMeans(site)
# 1 2 3 4 5 6
#29.16667 34.16667 33.16667 32.83333 30.33333 33.00000
# Moyennes par colonne
rowMeans(site)
# A B C D E F
# 9.166667 35.333333 37.333333 24.333333 58.833333 27.666667
# Moyenne par ligne (wouah, on s'y attendait pas là)
diag(site)
#[1] 5 51 34 25 53 28
# Donne la diagonale de la matrice.
det(site)
#[1] 27800977
# Le coefficient de détermination de la matrice
Enfin, une commande GIGA utile (enfin surtout quand on tripatouille les données et qu'on veut les observer sous différents angles), transposer une matrice, la fonction t():
t(site)
# A B C D E F
#1 5 17 50 24 55 24
#2 10 51 38 23 56 27
#3 11 30 34 25 72 27
#4 6 42 29 25 60 35
#5 8 35 33 28 53 25
#6 15 37 40 21 57 28
Tableaux de données (Data frames)
Dans R, un data frame est un tableau de données dont chaque colonne ne peut contenir qu'un seul type de données, mais dont les colonnes qui le composent peuvent être de différents types, contrairement à une matrice qui ne contient qu'un seul type de données, comme nous venons de le voir. C'est logique, une matrice, c'est un objet mathématique qui sert aux calculs à travers ses deux dimensions. Un tableau de données, c'est le plus souvent des observations (ou des individus) et des variables, le plus souvent en lignes et en colonnes (dans cet ordre).
Commençons par créer nos vecteurs, à noter que pour un peu plus de simplicité, j'y vais colonne par colonne (et avec un exemple librement adapté de lectures de rapports de fouilles (et puis les structures, c'est quand même plus intéressant que les pots (et Moyen Âge > Antiquité
))):
NUM_US <- c("100","102","103","104","108","109","110","111")
interpretation <- c("fosse","fosse","trou de poteau","trou de poteau","fosse","trou de poteau","trou de poteau", "trou de poteau")
long.cm <- c(50, 80, 35, 42, 130, 48, 52, 60)
larg.cm <- c(52, 20,30, 38,80,46,38,52)
datation <- c("XVIe-XVIIe s.", "XVIe-XVIIe s.", "XVe s.", "XVIe-XVIIe s.", "XVIe-XVIIe s.", "XVIe-XVIIe s.", "XVe s.", "XVe s.")
presence.mob <- c(TRUE, TRUE, FALSE, FALSE, TRUE, TRUE, TRUE, FALSE)
Puis, pour créer le tableau de données, la commande data.frame(colonne1,colonne2, ...)
liste_US <- data.frame(NUM_US,interpretation,long.cm,larg.cm,datation,presence.mob)
Et vous devriez avoir un nouvel objet qui s'affiche dans la fenêtre d'environnement à la section data. Vous pouvez le déplier dans RStudio ou cliquer dessus pour l'afficher (ou utiliser View()).
Les colonnes des data frames sont toujours nommées à partir du vecteur qui la compose. Pour lui attribuer un autre nom, il faut précéder le nom du vecteur de nouveau nom = puis le nom du vecteur.
Mieux, utilisons str():
str(liste_US)
#'data.frame': 8 obs. of 5 variables:
# $ NUM_US : chr "100" "102" "103" "104" ...
# $ interpretation: chr "fosse" "fosse" "trou de poteau" "trou de poteau" ...
# $ long.cm : num 50 80 35 42 130 48 52 60
# $ larg.cm : num 52 20 30 38 80 46 38 52
# $ datation : chr "XVIe-XVIIe s." "XVIe-XVIIe s." "XVe s." "XVIe-XVIIe s."
# $ presence.mob : logi TRUE TRUE FALSE FALSE TRUE TRUE ...
Le rendu str() classique et maintenant summary():
summary(liste_US)
# NUM_US interpretation long.cm larg.cm datation presence.mob
# Length:8 Length:8 Min. : 35.00 Min. :20.0 Length:8 Mode :logical
# Class :character Class :character 1st Qu.: 46.50 1st Qu.:36.0 Class :character FALSE:3
# Mode :character Mode :character Median : 51.00 Median :42.0 Mode :character TRUE :5
# Mean : 62.12 Mean :44.5
# 3rd Qu.: 65.00 3rd Qu.:52.0
# Max. :130.00 Max. :80.0
Vous avez dû repérer le changement de comportement de la fonction summary() si on est sur du texte, des nombres ou du logique.
Position avec $
Une petite subtilité pratique des data frame, c'est qu'on peut cibler une colonne (donc une variable) avec le $. D'ailleurs, quand vous appelez un objet, en général l'autocomplétion s'affiche au bout de trois caractères (là Entrée pour avoir le nom complet de l'objet), tapez $ et la liste des colonnes devrait s'afficher, vous n'avez plus qu'à sélectionner la variable qui vous intéresse avec les flèches du clavier. Démonstration :
liste_US$NUM_US
[1] "100" "102" "103" "104" "108" "109" "110" "111"
Et c'est super pratique pour appliquer une fonction à une variable :
median(liste_US$long.cm)
[1] 51
Ajouter une ligne ou une colonne
On pourrait être tenté d'ajouter une ligne dans un tableau de données comme on le ferait dans une matrice :
liste_US[9,] <- c("204","trou de poteau",64,58,"XVe s.",FALSE)
Mais là, que se passe-t-il ? Si vous allez à nouveau observer votre tableau de données :
str(liste_US)
#'data.frame': 9 obs. of 6 variables:
# $ NUM_US : chr "100" "102" "103" "104" ...
# $ interpretation: chr "fosse" "fosse" "trou de poteau" "trou de poteau" ...
# $ long.cm : chr "50" "80" "35" "42" ...
# $ larg.cm : chr "52" "20" "30" "38" ...
# $ datation : chr "XVIe-XVIIe s." "XVIe-XVIIe s." "XVe s." "XVIe-XVIIe s." ...
# $ presence.mob : chr "TRUE" "TRUE" "FALSE" "FALSE" ...
Sacrebleu, toutes les variables sont maintenant des chr ?!?! En fait, c'est assez logique. Nous avons inséré cette ligne comme un vecteur et, nous l'avons vu, un vecteur ne peut contenir qu'un seul type de données. Autant, ce n'est pas grave pour une matrice, toutes les données sont nécessairement du même type, donc on a l'habitude d'insérer un vecteur qui contient un seul type de données. Dans un tableau de données, chaque variable possède son propre type et si on insère directement un vecteur, bah on ne peut justement pas avoir de types différents dans un même vecteur. On utilise donc deux commandes pour les lignes ou pour les colonnes, rbind() ("r" pour "row", donc les lignes) et cbind() ("c" pour colonne (oui, je sais, "column")).
Avant d'ajouter une ligne, il faut recréer notre tableau initial (le liste_US) parce qu'on a tout pété avec nos manipulations stupides. Ensuite, pour faire simple, deux solutions avec la commande rbind.
La méthode cowboy, avec un objet list(). On peut créer un objet list() en amont ou l'injecter directement dans rbind() (moi j'ai fait la deuxième parce que je suis un guedin, comme miaule mon chat
) :
liste_US <- rbind(liste_US,list("142","trou de poteau", 64,56,"XVe s.",FALSE))
On est bien passés à 9 observations de 6 variables.
Une méthode un peu plus safe mais moins rapide; on crée un deuxième tableau de données qui contient simplement notre nouvelle ligne et on l'ajoute comme une ligne. Il faut en revanche que les noms de colonne correspondent, donc c'est plutôt quand vous voulez ajouter plusieurs lignes d'un coup :
NUM_US <- c("201","203","204","208")
interpretation <- c("trou de poteau","fosse","trou de poteau","fosse")
long.cm <- c(70, 134, 82, 164)
larg.cm <- c(64, 31, 78, 92)
datation <- c("XVe s.", "XVe s.", "XVe s.", "XVe s.")
presence.mob <- c(TRUE, FALSE, FALSE, FALSE)
liste_US2 <- data.frame(NUM_US,interpretation,long.cm,larg.cm,datation,presence.mob)
liste_US3 <- rbind(liste_US,liste_US2)
Et voilà, j'ai un nouveau tableau de données qui contient toutes mes US. Alors oui, pour éviter d'avoir à regénérer toutes mes données depuis le début comme ce qu'on a fait juste avant, je crée un nouvel objet dès que je fais une modification. Ça ne prend pas de place dans l'ordinateur et ça évite de tout se retaper. Vous verrez que c'est une pratique fréquente chez les utilisateurices de R.
Les fonctions table et prop.table
On a bien vu que summary() sur des tableaux de données, c'était déjà fort chouette, mais il y a une fonction encore mieux : table(). Vous allez voir, c'est bien sympa
On va le tester d'abord sur une variable :
table(liste_US$interpretation)
# fosse trou de poteau
# 3 5
Déjà, la fonction nous retourne le nombre d'élément par catégorie, mais allons plus loin, nous allons croiser deux variables catégorielles (c'est l'équivalent du "tableau croisé" dans les logiciels de tableurs habituels) :
table(liste_US$interpretation,liste_US$datation)
#
# XVe s. XVIe-XVIIe s.
# fosse 0 3
# trou de poteau 3 2
Vous pouvez le stocker dans un objet si le cœur vous en dit. Et maintenant, on veut voir ces données sous la forme de proportions, il y a évidemment une fonction pour ça, prop.table(). Cette fonction a besoin d'une matrice, le plus simple est souvent de lui insérer une fonction table() (qui, elle, retourne une matrice de fréquences) :
prop.table(table(liste_US$interpretation,liste_US$datation))
#
# XVe s. XVIe-XVIIe s.
# fosse 0.000 0.375
# trou de poteau 0.375 0.250
Là, les proportions sont données par rapport au total général. Vous pouvez aussi choisir d'obtenir les proportions en ligne ou en colonne, c'est le deuxième argument de la fonction, avec 1 pour désigner les lignes et 2 pour les colonnes :
prop.table(table(liste_US$interpretation,liste_US$datation),1)
#
# XVe s. XVIe-XVIIe s.
# fosse 0.0 1.0
# trou de poteau 0.6 0.4
prop.table(table(liste_US$interpretation,liste_US$datation),2)
#
# XVe s. XVIe-XVIIe s.
# fosse 0.0 0.6
# trou de poteau 1.0 0.4
Ah, le résultat vous est donné entre 0 et 1, c'est pratique et on en a l'habitude, mais pour communiquer, c'est quand même parfois plus simple de donner une valeur de pourcentage qui va de 0 à 100. Pour ça, c'est pas compliqué, on n'a qu'à multiplier notre matrice par 100 (je vous ai dit qu'une opération avec un seul argument était appliquée à l'ensemble de la matrice (et puis, même si vous ne vous en souveniez plus, n'hésitez pas à tester, on est là pour explorer, chill)) :
prop.table(table(liste_US$interpretation,liste_US$datation))*100
#
# XVe s. XVIe-XVIIe s.
# fosse 0.0 37.5
# trou de poteau 37.5 25.0
C'est pas choupinou ?