Question 1

Calculs vectoriels et règle de recyclage

1.1

Soit la matrice suivante :

mat <- matrix(c(2, 5, 3, 4, 6, 5, 4, 3, 1, 2, 9, 8), nrow = 3, ncol = 4)
mat
##      [,1] [,2] [,3] [,4]
## [1,]    2    4    4    2
## [2,]    5    6    3    9
## [3,]    3    5    1    8

Pour chaque colonne de cette matrice (sauf la dernière), calculez sa somme terme à terme avec la colonne suivante (colonne 1 + colonne 2, colonne 2 + colonne 3, colonne 3 + colonne 4), de façon à obtenir le résultat suivant :

##      [,1] [,2] [,3]
## [1,]    6    8    6
## [2,]   11    9   12
## [3,]    8    6    9

Votre solution doit tenir en une seule commande et ne pas contenir de boucle.

Remarque : Ce calcul ressemble au calcul effectué par la fonction diff lorsqu’elle reçoit en entrée une matrice. Cependant, des additions sont demandées au lieu de soustractions, et les opérations doivent être effectuées avec des paires de colonnes au lieu de paires de lignes.

Solution :

mat <- matrix(c(2, 5, 3, 4, 6, 5, 4, 3, 1, 2, 9, 8), nrow = 3, ncol = 4)

mat[, -ncol(mat)] + mat[, -1]
##      [,1] [,2] [,3]
## [1,]    6    8    6
## [2,]   11    9   12
## [3,]    8    6    9

1.2

Divisez chaque élément de la matrice mat par le numéro de sa ligne et arrondissez le résultat obtenu à l’entier supérieur, de façon à obtenir le résultat suivant :

##      [,1] [,2] [,3] [,4]
## [1,]    2    4    4    2
## [2,]    3    3    2    5
## [3,]    1    2    1    3

Votre solution doit tenir en une seule commande et ne pas contenir de boucle.

Solution :

ceiling(mat/(1:nrow(mat)))
##      [,1] [,2] [,3] [,4]
## [1,]    2    4    4    2
## [2,]    3    3    2    5
## [3,]    1    2    1    3

# La règle de recyclage s'applique en transformant le vecteur 1:nrow(mat) en une 
# matrice de la même dimension que mat. La matrice est remplie colonne par colonne 
# en répétant le vecteur 1:nrow(mat) autant de fois que nécessaire. Ensuite une 
# division terme à terme est effectuée entre les deux matrice. Finalement, la fonction
# ceiling effectue un arrondissement pour chacun des termes de la matrice obtenue.

Données airquality

Les questions suivantes vous feront manipuler les données stockées dans le data frame airquality, inclus dans le package datasets de l’installation de base de R. Si ce package est chargé dans votre session R (ce qui est fait par défaut lors de l’ouverture d’une nouvelle session R), vous avez accès à l’objet airquality. Il contient des observations de quantité d’ozone et de radiation solaire dans l’air de New York pour chaque jour des mois de mai à septembre en 1973. Le data frame comprend aussi des mesures de vitesse du vent et de température.

Question 2

Conditions logiques

2.1

Est-ce que le jeu de données airquality contient des données manquantes ? Si oui, pour quelles variables ?

Bien qu’il soit possible de répondre à ces questions uniquement en regardant les données, vous devez écrire du code R qui répond à ces questions.

Solution :

str(airquality)
## 'data.frame':    153 obs. of  6 variables:
##  $ Ozone  : int  41 36 12 18 NA 28 23 19 8 NA ...
##  $ Solar.R: int  190 118 149 313 NA NA 299 99 19 194 ...
##  $ Wind   : num  7.4 8 12.6 11.5 14.3 14.9 8.6 13.8 20.1 8.6 ...
##  $ Temp   : int  67 72 74 62 56 66 65 59 61 69 ...
##  $ Month  : int  5 5 5 5 5 5 5 5 5 5 ...
##  $ Day    : int  1 2 3 4 5 6 7 8 9 10 ...

# Est-ce que le jeu de données airquality contient des données manquantes?
any(is.na(airquality))
## [1] TRUE
# Réponse : oui, au moins une donnée manquante est présente dans ce data frame.

# Si oui, pour quelles variables?
names(which(apply(is.na(airquality), MARGIN = 2, FUN = any)))
## [1] "Ozone"   "Solar.R"
# Réponse : les variables Ozone et Solar.R

2.2

Si nous voulions traiter ces données, il faudrait décider quoi faire avec les données manquantes. La solution la plus simple serait de les omettre.

Lesquelles des commandes suivantes permettent de retirer complètement les lignes contenant au moins une donnée manquante du data frame airquality ?

# a)
airquality[airquality$Ozone != NA & airquality$Solar.R != NA, ]
# b)
airquality[! is.na(airquality$Ozone) && ! is.na(airquality$Solar.R), ]
# c)
airquality[! is.na(airquality$Ozone) & ! is.na(airquality$Solar.R), ]
# d)
airquality[! is.na(airquality$Ozone) | ! is.na(airquality$Solar.R), ]
# e)
airquality[! (is.na(airquality$Ozone) | is.na(airquality$Solar.R)), ]
# f)
airquality[- which(is.na(airquality$Ozone) | is.na(airquality$Solar.R)), ]
# g)
subset(airquality, subset = ! is.na(Ozone) & ! is.na(Solar.R))
# h)
na.omit(airquality)

Solution :

# a)
test1 <- airquality[airquality$Ozone != NA & airquality$Solar.R != NA, ]
str(test1) # ne fonctionne pas, 
           # c'est la fonction is.na qui sert à tester si une donnée est NA

# b)
test2 <- airquality[!is.na(airquality$Ozone) && !is.na(airquality$Solar.R), ]
str(test2) # ne fonctionne pas, 
           # l'opérateur logique && retourne un seul logique, l'unique TRUE retourné
           # par !is.na(airquality$Ozone) && !is.na(airquality$Solar.R) est recyclé

# c)
test3 <- airquality[!is.na(airquality$Ozone) & !is.na(airquality$Solar.R), ]
str(test3) # fonctionne, la condition logique est bien écrite

# d)
test4 <- airquality[!is.na(airquality$Ozone) | !is.na(airquality$Solar.R), ]
str(test4) # ne fonctionne pas, 
           # retire seulement les lignes pour lesquelles les deux données sont manquantes

# e)
test5 <- airquality[!(is.na(airquality$Ozone) | is.na(airquality$Solar.R)), ]
str(test5) # fonctionne, la condition logique est bien écrite

# f)
test6 <- airquality[-which(is.na(airquality$Ozone) | is.na(airquality$Solar.R)), ]
str(test6) # fonctionne, mais l'utilisation de which n'est pas nécessaire
           # -which(**) a le même effet que !(**), qui est plus succinct 

# g)
test7 <- subset(airquality, subset = !is.na(Ozone) & !is.na(Solar.R))
str(test7) # fonctionne, la condition logique est bien écrite
           # la fonction subset permet des écritures plus courtes, car les noms des
           # colonnes du data frame fourni en premier argument sont directement
           # accessibles (donc plus besoin des airquality$ dans la condition logique)

# h)
test8 <- na.omit(airquality)
str(test8) # fonctionne -> solution la plus générale, car elle ne dépend pas des noms des
           # variables sur lesquelles tester la présence de NA, ni du nombre de variables

Question 3

Implémentation d’une formule mathématique

Supposez que vous devez programmer vous-même le calcul de l’écart-type. La formule mathématique à programmer est celle qui se trouve dans la section « Background » de la page web suivante : https://en.wikipedia.org/wiki/Unbiased_estimation_of_standard_deviation#Background

Contraintes :

  • travailler de façon vectorielle (ne pas faire de boucle),
  • ne pas utiliser de fonctions pour le calcul de statistiques (mean ou sd par exemple), mais vous pouvez utiliser des fonctions pour des calculs mathématiques (sum par exemple).

À faire :

Écrivez un programme R qui calcule l’écart-type des observations de la variable Ozone dans le data frame airquality. Pour simplifier la programmation, vous pouvez travailler sur le vecteur x suivant :

x <- airquality$Ozone

Solution :

x <- airquality$Ozone

# Il faut d'abord calculer la moyenne
n <- sum(!is.na(x))
# Dénominateur = nombre d'observations non manquantes
xMoy <- sum(x, na.rm = TRUE)/n
xMoy
## [1] 42.12931

# Ensuite nous calculons l'écart-type
xSD <- sqrt(sum((x - xMoy)^2, na.rm = TRUE)/(n - 1))
xSD
## [1] 32.98788

# Est-ce que nous obtenons le même résultat que la fonction sd?
sd(x, na.rm = TRUE)
## [1] 32.98788
# oui!

# Autre solution
y <- na.omit(x)
n <- length(y)
yMoy <- sum(y)/n
ySD <- sqrt(sum((y - yMoy)^2)/(n - 1))
ySD
## [1] 32.98788

Question 4

Distributions de probabilité

4.1

Soit X une variable aléatoire suivant une distribution Poisson de paramètre \(\lambda\) (argument lambda) égal à 5. Combien vaut la probabilité que cette variable aléatoire prenne la valeur \(x\), pour \(x \in \{3, 4, 5\}\) ?

Solution

# X ~ Poisson(5)
# P(X = 3), P(X = 4), P(X = 5)
dpois(3:5, lambda = 5)
## [1] 0.1403739 0.1754674 0.1754674

4.2

Soit X une variable aléatoire suivant une distribution exponentielle. Combien vaut la probabilité que cette variable aléatoire prenne une valeur inférieure ou égale à 3 si le paramètre \(\lambda\) (argument rate) de la distribution vaut 0.2 et s’il vaut 0.5 ?

Solution

# P(X <= 3) pour
# X ~ Exponentielle(0.2) et
# X ~ Exponentielle(0.5)
pexp(3, rate = c(0.2, 0.5))
## [1] 0.4511884 0.7768698

4.3

Supposons que vous venez d’effectuer un test d’hypothèses. Sous \(H_0\), l’hypothèse nulle du test, la statistique du test, notée \(X\), suit une loi de Fisher à 2 et 10 degrés de liberté. La valeur observée de la statistique de test est \(x_{obs} = 4.3\). Le seuil observée (p-value) du test est donc la probabilité suivante : \(P(X \ge 4.3)\) sachant que \(X \sim F_{2,10}\). Obtenez la valeur de ce seuil observé.

Note : Étant donné que \(X\) est une variable aléatoire continue, \(P(X = x)\) vaut approximativement 0.

Solution

# X ~ Fisher(2, 10)
# P(X >= 4.3) approx P(X > 4.3)
1 - pf(4.3, df1 = 2, df2 = 10)
# ou
pf(4.3, df1 = 2, df2 = 10, lower.tail = FALSE)
## [1] 0.04491954

4.4

Si vous choisissiez d’utiliser une valeur critique plutôt qu’un seuil observée pour émettre une conclusion dans le test d’hypothèses précédent, vous compareriez alors la valeur observée de votre statistique de test à un quantile de la loi Fisher à 2 et 10 degrés de liberté. Pour un seuil de 5%, cette valeur critique serait donc la valeur \(q\) pour laquelle \(P(X \ge q) = 0.05\). Calculez cette valeur critique.

Solution

# X ~ Fisher(2, 10)
# q tel que P(X > q) = 0.05, car P(X >= q) approx P(X > q)
qf(0.05, df1 = 2, df2 = 10, lower.tail = FALSE)
# ou
qf(1 - 0.05, df1 = 2, df2 = 10)
## [1] 4.102821

Question 5

Génération de nombres pseudo-aléatoires

5.1

Générez n = 100 observations tirées d’une loi normale standard.

Tracez ensuite l’histogramme de ces observations, auquel vous devez superposer la courbe de densité de la loi normale standard. Voici le code pour produire ce graphique, en supposant que les observations générées sont stockées dans un vecteur nommé obs

hist(obs, freq = FALSE)
curve(dnorm, add = TRUE)

Produisez ensuite le même graphique, mais à partir de n = 1000 observations tirées d’une loi normale standard.

Puis refaites encore le graphique, cette fois à partir de n = 10000 observations, toujours tirées d’une loi normale standard.

Qu’observez-vous ?

Solution

n <- 100        # nombres d'observations à générer
n <- 1000
n <- 10000
obs <- rnorm(n)   # génération aléatoire

# Comparons les observations aléatoires obtenues à la distribution
# théorique en superposant une courbe de densité de la loi normale 
# à l'histogramme des observations.
hist(obs, freq = FALSE)
curve(dnorm, add = TRUE)

# Nous pouvons observer l'effet de la loi des grands nombres : 
# plus n est grand, plus la distribution empirique s'approche 
# de la distribution théorique.

5.2

Avant de générer pseudo-aléatoirement 6 observations d’une loi de Student à 15 degrés de liberté, je vais fixer le germe du générateur de nombres pseudo-aléatoires avec la commande :

set.seed(35)

Quel sera le germe utilisé pour cette génération pseudo-aléatoire ?

Solution

set.seed(35)
.Random.seed # il est important de placer cette commande avant l'appel à rt
##   [1]       10403         624  2068089839 -1125240476 -1801215723  -146075950 ...
x <- rt(6, 15)

Données concernant les résultats d’étudiants portugais dans un cours de mathématiques

Pour les questions suivantes, vous devez importer en R les données du fichier student-mat.csv téléchargeable sur le site web du cours. Il s’agit de résultats d’étudiants portugais dans un cours de mathématiques.

Source des données et description des variables : https://archive.ics.uci.edu/ml/datasets/Student+Performance#

student <- read.csv2("student-mat.csv")
str(student)
## 'data.frame':    395 obs. of  33 variables:
##  $ school    : Factor w/ 2 levels "GP","MS": 1 1 1 1 1 1 1 1 1 1 ...
##  $ sex       : Factor w/ 2 levels "F","M": 1 1 1 1 1 2 2 1 2 2 ...
##  $ age       : int  18 17 15 15 16 16 16 17 15 15 ...
##  $ address   : Factor w/ 2 levels "R","U": 2 2 2 2 2 2 2 2 2 2 ...
##  $ famsize   : Factor w/ 2 levels "GT3","LE3": 1 1 2 1 1 2 2 1 2 1 ...
##  $ Pstatus   : Factor w/ 2 levels "A","T": 1 2 2 2 2 2 2 1 1 2 ...
##  $ Medu      : int  4 1 1 4 3 4 2 4 3 3 ...
##  $ Fedu      : int  4 1 1 2 3 3 2 4 2 4 ...
##  $ Mjob      : Factor w/ 5 levels "at_home","health",..: 1 1 1 2 3 4 3 3 4 3 ...
##  $ Fjob      : Factor w/ 5 levels "at_home","health",..: 5 3 3 4 3 3 3 5 3 3 ...
##  $ reason    : Factor w/ 4 levels "course","home",..: 1 1 3 2 2 4 2 2 2 2 ...
##  $ guardian  : Factor w/ 3 levels "father","mother",..: 2 1 2 2 1 2 2 2 2 2 ...
##  $ traveltime: int  2 1 1 1 1 1 1 2 1 1 ...
##  $ studytime : int  2 2 2 3 2 2 2 2 2 2 ...
##  $ failures  : int  0 0 3 0 0 0 0 0 0 0 ...
##  $ schoolsup : Factor w/ 2 levels "no","yes": 2 1 2 1 1 1 1 2 1 1 ...
##  $ famsup    : Factor w/ 2 levels "no","yes": 1 2 1 2 2 2 1 2 2 2 ...
##  $ paid      : Factor w/ 2 levels "no","yes": 1 1 2 2 2 2 1 1 2 2 ...
##  $ activities: Factor w/ 2 levels "no","yes": 1 1 1 2 1 2 1 1 1 2 ...
##  $ nursery   : Factor w/ 2 levels "no","yes": 2 1 2 2 2 2 2 2 2 2 ...
##  $ higher    : Factor w/ 2 levels "no","yes": 2 2 2 2 2 2 2 2 2 2 ...
##  $ internet  : Factor w/ 2 levels "no","yes": 1 2 2 2 1 2 2 1 2 2 ...
##  $ romantic  : Factor w/ 2 levels "no","yes": 1 1 1 2 1 1 1 1 1 1 ...
##  $ famrel    : int  4 5 4 3 4 5 4 4 4 5 ...
##  $ freetime  : int  3 3 3 2 3 4 4 1 2 5 ...
##  $ goout     : int  4 3 2 2 2 2 4 4 2 1 ...
##  $ Dalc      : int  1 1 2 1 1 1 1 1 1 1 ...
##  $ Walc      : int  1 1 3 1 2 2 1 1 1 1 ...
##  $ health    : int  3 3 3 5 5 5 3 1 1 5 ...
##  $ absences  : int  6 4 10 2 4 10 0 6 0 0 ...
##  $ G1        : int  5 5 7 15 6 15 12 6 16 14 ...
##  $ G2        : int  6 5 8 14 10 15 12 5 18 15 ...
##  $ G3        : int  6 6 10 15 10 15 11 6 19 15 ...

Question 6

Tests statistiques

Dans ces données, est-ce qu’il y a une corrélation de Pearson significativement différente de 0 entre les résultats à la première période (variable G1) et ceux à la deuxième période (variable G2) ?

Solution

# 1- Visualisation des données
pairs(formula = ~ G1 + G2 + G3, data = student)

# 2- Calcul de corrélations
cor(student[, c("G1", "G2", "G3")])

# 3- Test demandé
cor.test(formula = ~ G1 + G2, data = student)
##  Pearson's product-moment correlation
## 
## data:  G1 and G2
## t = 32.278, df = 393, p-value < 2.2e-16
## alternative hypothesis: true correlation is not equal to 0
## 95 percent confidence interval:
##  0.8226117 0.8770475
## sample estimates:
##       cor 
## 0.8521181

# Avec un p-value de 2.2e-16, oui, cette corrélation est 
# significativement différente de 0.

Question 7

Ajustement de modèles

7.1

Ajustez un modèle linéaire classique (avec erreurs normales) sur ces données. Dans ce model, la note finale (variable G3) des étudiants est la variable réponse. Ce modèle doit comporter un terme pour l’école fréquentée par les étudiants (variable catégorique school) et un autre terme pour l’âge des étudiants (variable numérique age).

Solution

# ---- 1- Visualisation des données : relations bivariées ----

# G3 versus school
boxplot(G3 ~ school, data = student)

# G3 versus age
plot(formula = G3 ~ age, data = student)
# ou
plot(x = student$age, y = student$G3)
# ou
with(student, plot(age, G3))

# Ajout d'un peu de bruit pour mieux voir les observations supperposées
with(student, plot(jitter(age), jitter(G3)))

# Autre option
boxplot(formula = G3 ~ age, data = student)
# Attention, le boxplot pour age = 20 représente en fait seulement 3 observations

# ---- 2- Ajustement du modèle ----
mod <- lm(formula = G3 ~ school + age, data = student)
mod
## Call:
## lm(formula = G3 ~ school + age, data = student)
## 
## Coefficients:
## (Intercept)     schoolMS          age  
##     20.4924       0.2661      -0.6054  
    
# Comment visualiser les résultats des tests sur les coefficients ?
summary(mod)
## Call:
## lm(formula = G3 ~ school + age, data = student)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -11.4111  -1.5948   0.1943   3.1943   9.6160 
## 
## Coefficients:
##             Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  20.4924     3.2018   6.400 4.43e-10 ***
## schoolMS      0.2661     0.7677   0.347  0.72904    
## age          -0.6054     0.1932  -3.133  0.00186 ** 
## ---
## Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1
## 
## Residual standard error: 4.532 on 392 degrees of freedom
## Multiple R-squared:  0.02641,    Adjusted R-squared:  0.02144 
## F-statistic: 5.316 on 2 and 392 DF,  p-value: 0.005273

# ---- 3- Validation des postulats du modèle ----
# Je n'entre pas dans les détails ici. Voici un exemple non complet.
plot(formula = G3 ~ age, data = student)
abline(a = coef(mod)["(Intercept)"], b = coef(mod)["age"])
plot(mod)

# ---- 4- Modification du modèle ou des données au besoin ----
# Exemples questions 11 et 12

7.2

Selon le modèle ajusté à la question précédente, est-ce que la note finale des étudiants varie significativement au seuil de 5% selon l’école fréquentée ?

Solution

coef(summary(mod))
##               Estimate Std. Error   t value     Pr(>|t|)
## (Intercept) 20.4924417  3.2017529  6.400382 4.430048e-10
## schoolMS     0.2661383  0.7677302  0.346656 7.290357e-01
## age         -0.6054218  0.1932361 -3.133067 1.859921e-03

coef(summary(mod))["schoolMS", "Pr(>|t|)"]
## [1] 0.7290357

coef(summary(mod))["schoolMS", "Pr(>|t|)"] <= 0.05
## [1] FALSE

# Seuil observé du test sur schoolMS : 0.7290357 > 0.05
# Réponse : Non

7.3

Selon ce modèle, est-ce que la note finale des étudiants varie significativement au seuil de 5% selon leur âge ?

Solution

coef(summary(mod))
##               Estimate Std. Error   t value     Pr(>|t|)
## (Intercept) 20.4924417  3.2017529  6.400382 4.430048e-10
## schoolMS     0.2661383  0.7677302  0.346656 7.290357e-01
## age         -0.6054218  0.1932361 -3.133067 1.859921e-03

coef(summary(mod))["age", "Pr(>|t|)"]
## [1] 0.001859921

coef(summary(mod))["age", "Pr(>|t|)"] <= 0.05
## [1] TRUE

# Seuil observé du test sur age : 0.00186 <= 0.05
# Réponse : Oui

Question 8

Arguments accompagnant les formules

Ajustez de nouveau le même modèle qu’à la question 8, en vous limitant cette fois aux étudiants pour lesquels :

  • la note finale est non nulle et
  • l’âge est inférieur à 20.

Selon ce modèle, est-ce que la note finale des étudiants varie significativement au seuil de 5% selon leur âge ?

Solution

# - Retirer les individus ayant obtenus 0 ainsi que ceux de 20 ans ou plus
mod <- lm(
  formula = G3 ~ school + age,
  data = student, 
  subset = G3 > 0 & age < 20
)

plot(
  formula = G3 ~ age, 
  data = student, 
  subset = G3 > 0 & age < 20
)
abline(
  a = coef(mod)["(Intercept)"], 
  b = coef(mod)["age"]
)

summary(mod)
## Call:
## lm(formula = G3 ~ school + age, data = student, subset = G3 > 
##     0 & age < 20)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -7.4143 -2.1347 -0.4143  2.2441  8.2441 
## 
## Coefficients:
##             Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  17.5379     2.5465   6.887 2.66e-11 ***
## schoolMS     -0.2983     0.5808  -0.513   0.6079    
## age          -0.3602     0.1544  -2.332   0.0202 *  
## ---
## Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1
## 
## Residual standard error: 3.184 on 349 degrees of freedom
## Multiple R-squared:  0.02129,    Adjusted R-squared:  0.01568 
## F-statistic: 3.795 on 2 and 349 DF,  p-value: 0.02341

# Seuil observé du test sur age : 0.0202 < 0.05
# Effet de l'âge encore significatif, mais moins fort qu'auparavant.

Question 9

Formules

En revenant à l’utilisation de toutes les observations, ajustez de nouveau le modèle de la question 8 en modifiant une chose : la variable age doit être traitée en tant que variable catégorique (comme la variable school) plutôt que numérique.

Selon ce modèle, est-ce que la note finale des étudiants varie significativement au seuil de 5% selon leur âge ?

Solution

# - Traiter l'âge comme une variable catégorique
mod <- lm(
  formula = G3 ~ school + factor(age), 
  data = student
)
summary(mod)
## Call:
## lm(formula = G3 ~ school + factor(age), data = student)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -11.2561  -1.4868   0.3099   2.9712   9.5132 
## 
## Coefficients:
##               Estimate Std. Error t value Pr(>|t|)    
## (Intercept)    11.2561     0.5007  22.481  < 2e-16 ***
## schoolMS        0.2033     0.7884   0.258  0.79664    
## factor(age)16  -0.2273     0.6696  -0.339  0.73450    
## factor(age)17  -1.0055     0.6854  -1.467  0.14319    
## factor(age)18  -1.7693     0.7478  -2.366  0.01847 *  
## factor(age)19  -3.0986     1.0706  -2.894  0.00401 ** 
## factor(age)20   2.6084     2.7165   0.960  0.33755    
## factor(age)21  -4.4594     4.6292  -0.963  0.33599    
## factor(age)22  -3.2561     4.5615  -0.714  0.47577    
## ---
## Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1
##
## Residual standard error: 4.534 on 386 degrees of freedom
## Multiple R-squared:  0.0405, Adjusted R-squared:  0.02061 
## F-statistic: 2.037 on 8 and 386 DF,  p-value: 0.04124

# Afin de faire un test global sur le facteur age
anova(mod)
## Analysis of Variance Table
## 
## Response: G3
##              Df Sum Sq Mean Sq F value  Pr(>F)  
## school        1   16.8  16.759  0.8153 0.36713  
## factor(age)   7  318.2  45.451  2.2110 0.03266 *
## Residuals   386 7935.0  20.557                  
## ---
## Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

# ou, sans passer par la fonction lm :
mod2 <- aov(
  formula = G3 ~ school + factor(age), 
  data = student
)
summary(mod2)
##              Df Sum Sq Mean Sq F value Pr(>F)  
## school        1     17   16.76   0.815 0.3671  
## factor(age)   7    318   45.45   2.211 0.0327 *
## Residuals   386   7935   20.56                 
## ---
## Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

# Seuil observé du test sur age : 0.0327 < 0.05
# Effet de l'âge significatif, mais encore moins fort qu'auparavant.

Données sur la production de fruits dans le monde

La semaine dernière, nous avons importé en R des données sur la production de fruits dans le monde provenant du site web de la FAO (Organisation des Nations unies pour l’alimentation et l’agriculture, http://www.fao.org/faostat/fr/#data/QC).

J’ai nettoyé ces données. Seules les variables Region, Item, Annee et Production ont été conservées. Aussi, j’y ai ajouté une variable : Superficie. Les variables de ces données sont ainsi définies :

  • Region : Région du monde, presque toujours un pays, mais parfois un territoire rattaché à un pays;
  • Item : Nom du ou des fruits, ou du groupe de fruits;
  • Annee : Année (2014, 2015 ou 2016);
  • Production : Quantité de fruits produits, en tonnes;
  • Superficie : Superficie de la région, en km2.

J’ai enregistré une copie des données en format rds dans le fichier fruitsFinal.rds. Téléchargez le fichier fruitsFinal.rds à partir du site web du cours dans le contenu de cette semaine et chargez en R l’objet qu’il contient. Nommez cet objet fruits.

fruits <- readRDS("C:/coursR/fruitsFinal.rds")
str(fruits)
## 'data.frame':    7758 obs. of  5 variables:
##  $ Region    : chr  "Afghanistan" "Afghanistan" "Afghanistan" "Afghanistan" ...
##  $ Item      : chr  "Fruits, agrumes nda" "Fruits, agrumes nda" "Figues" "Figues" ...
##  $ Annee     : int  2016 2015 2014 2015 2015 2014 2015 2014 2016 2014 ...
##  $ Production: int  17472 3043 6100 4996 50657 3000 87686 46186 17894 23561 ...
##  $ Superficie: num  652230 652230 652230 652230 652230 ...

Question 10

Exploration d’un jeu de données

Combien le jeu de données fruits contient-il d’observations (de lignes) par année ?

Solution :

# Fréquences des années
table(fruits$Annee)
## 2014 2015 2016 
## 2602 2583 2573 
# années pratiquement également représentées

Question 11

Exploration d’un jeu de données

Calculez le nombre d’observations par région dans le jeu de données fruits.

Est-ce vrai de dire que le Canada a plus d’observations que la Bolivie?

Solution :

# Fréquences des régions (nombre d'observations par région dans les données)
freqRegion <- table(fruits$Region)

str(freqRegion)  # retourne un objet de classe table
##  'table' int [1:210(1d)] 42 72 54 66 46 12 18 18 63 45 ...
##  - attr(*, "dimnames")=List of 1
##   ..$ : chr [1:210] "Afghanistan" "Afrique du Sud" "Albanie" "Algerie" ...

freqRegion      # difficile à visualiser dans ce sens, sortie non montrée ici

# nous transformons la table en data frame pour faciliter sa visualisation
freqRegionDF <- as.data.frame(freqRegion)
freqRegionDF
##                                           Var1 Freq
## 1                                  Afghanistan   42
## 2                               Afrique du Sud   72
## 3                                      Albanie   54
## 4                                      Algerie   66
## 5                                    Allemagne   46
## 6                                       Angola   12
## 7                           Antigua-et-Barbuda   18
## 8                              Arabie saoudite   18
## 9                                    Argentine   63
## 10                                     Armenie   45
## ...
nrow(freqRegionDF)  # 210 régions
## [1] 210

# Pour trouver la réponse à la question concernant le Canada et la Bolivie,
# il faut d'abord réaliser que la chaîne de caractères représentant la Bolivie 
# n'est pas simplement "Bolivie".
# Il s'agit plutôt de "Bolivie (Etat plurinational de)".

freqRegion[["Canada"]] > freqRegion[["Bolivie (Etat plurinational de)"]]
## FALSE
# Réponse : Faux, le Canada a moins d'observations que la Bolivie

# Nous aurions aussi pu utiliser la capacité de l'opérateur [[ à faire de l'identification 
# à partir de noms partiels si la valeur TRUE est fournie à son argument exact
freqRegion[["Bolivie", exact = FALSE]]

# Nous verrons la semaine prochaine des fonctions qui permettraient aussi d'identifier 
# de façon automatique l'observation associée à la Bolivie, par exemple :
freqRegion[[grep(pattern = "Bolivie", x = names(freqRegion))]]

Question 12

Exploration d’un jeu de données

Quelle(s) région(s) présente(nt) le plus petit nombre d’observations ?

Solution :

# Identification de la ou des régions avec la fréquence la plus faible
min(freqRegion)  
# NON, nous venons d'identifier la fréquence minimale, 
# mais pas la ou les régions associées à cette fréquence.

names(which.min(freqRegion))
## [1] "Bermudes"

# Attention!
# which.max et which.min retournent seulement la position du premier max ou min rencontré.

# Ici, le minimum est présent plus d'une fois.
names(freqRegion[freqRegion == min(freqRegion)])
##  [1] "Bermudes"                 "Chine - RAS de Hong-Kong" "Erythree"                
##  [4] "Iles Vierges americaines" "Islande"                  "Lesotho"                 
##  [7] "Liechtenstein"            "Mongolie"                 "Nauru"                   
## [10] "Saint-Pierre-et-Miquelon"

Question 13

Exploration d’un jeu de données

Combien de valeurs manquantes ou nulles (valeur de zéro) contient la variable Production du jeu de données fruits ?

Solution :

# Nombre de valeurs manquantes :
sum(is.na(fruits$Production))
## [1] 123
# ou encore en utilisant la sortie de summary
summary(fruits$Production)
##     Min.  1st Qu.   Median     Mean  3rd Qu.     Max.     NA's 
##       0     1620    14247   334390   113542 79043138      123 

# Nombre de valeurs nulles
sum(fruits$Production == 0, na.rm = TRUE)
## [1] 50

# Nombre de valeurs rencontrant une des deux conditions
sum(is.na(fruits$Production) | fruits$Production == 0)
## [1] 173

Question 14

Exploration d’un jeu de données

Pour quelles régions le nombre d’observations diffère-t-il entre les années ?

Solution :

# Calculons d'abord les fréquences croisées entre Region et Annee,
# c'est-à-dire le nombre d'observations par région pour chacune des années.
freqRegionAnnee <- xtabs( ~ Region + Annee, data = fruits)
# Équivalent à
# freqRegionAnnee <- table(Region = fruits$Region, Annee = fruits$Annee)

freqRegionAnnee

# Nous pouvons répondre à la question en observant le tableau de résultats.
# Mais ce tableau est plutôt gros! Il n'est pas affiché ici.

# Nous pouvons aussi faire un test automatiquement, ce qui est encore mieux.


# ---- Solution 1 ----

# Calculer des variances par ligne, puis identifier lignes pour lesquelles
# la variance est non nulles
# (variance non nulle => valeurs différentes entre les années)
test <- apply(freqRegionAnnee, MARGIN = 1, FUN = var) != 0

# Identification des régions pour lesquels la variance est non nulle
rownames(freqRegionAnnee)[test]
##  [1] "Allemagne"                 "Australie"                 "Autriche"                 
##  [4] "Belarus"                   "Chine, Taiwan Province de" "Danemark"                 
##  [7] "Finlande"                  "Guam"                      "Hongrie"                  
## [10] "Irlande"                   "Japon"                     "Koweit"                   
## [13] "Republique de Coree"       "Royaume-Uni"               "Samoa americaines"        
## [16] "Slovenie"                  "Soudan du Sud"             "Tokelaou"                 
## [19] "Turquie"    

# ou en utilisant la fonction which
names(which(test))
##  même sortie que ci-dessus  


# ---- Solution 2 ----

# Calculer le nombre de valeurs uniques par lignes, puis identifier les lignes
# avec un nombre de valeurs uniques supérieur à 1.
freqUnique <- apply(freqRegionAnnee, MARGIN = 1, FUN = unique)
# le résultat est une liste, avec un élément par région
# élément = vecteur des valeurs uniques

# Calculons la longueur de ce vecteur par région
nbreFreqUnique <- sapply(freqUnique, FUN = length)

# Indentification des régions avec plus d'une valeur de fréquence
names(which(nbreFreqUnique > 1))
##  [1] "Allemagne"                 "Australie"                 "Autriche"                 
##  [4] "Belarus"                   "Chine, Taiwan Province de" "Danemark"                 
##  [7] "Finlande"                  "Guam"                      "Hongrie"                  
## [10] "Irlande"                   "Japon"                     "Koweit"                   
## [13] "Republique de Coree"       "Royaume-Uni"               "Samoa americaines"        
## [16] "Slovenie"                  "Soudan du Sud"             "Tokelaou"                 
## [19] "Turquie"  

Question 15

Exploration d’un jeu de données

Est-ce vrai de dire que la Superficie est manquante pour 6 régions dans le jeu de données fruits ?

Solution :

summary(fruits$Superficie)
##     Min.  1st Qu.   Median     Mean  3rd Qu.     Max.     NA's 
##       12    43094   236800  1011222   752618 17098242      263 
# Retourne le nombre de données manquantes, mais il peut y avoir
# plus d'une donnée manquante par région

regionNA <- unique(fruits$Region[is.na(fruits$Superficie)])
regionNA
## [1] "Guadeloupe"                    "Guam"                         
## [3] "Guyane francaise"              "Martinique"                   
## [5] "Reunion"                       "Territoire palestinien occupe"

# Test formel
length(regionNA) == 6
## [1] TRUE

# Réponse : L'affirmation est vraie.

Réponses à quelques questions intéressantes

Question 16

Exploration d’un jeu de données

Est-ce que le Canada produit des fruits clairement identifiés comme étant des oranges ?

Solution :

any(fruits$Region == "Canada" & fruits$Item == "Oranges")
## [1] FALSE

# Réponse : NON, car il n'y a aucune ligne dans notre jeu de données combinant 
#                Région = Canada et Item = Oranges

# Remarque : Si nous avions obtenu la réponse TRUE, il aurait fallu s'assurer que 
# les lignes combinant Région = Canada et Item = Orange contiennent une valeur de 
# production non manquante et non nulle.

Question 17

Exploration d’un jeu de données

17.1

Quelle(s) région(s) a(ont) produit le plus grand nombre d’items distincts en 2016 ?

Solution :

# D'abord, limitons-nous à l'année 2016
fruits2016 <- subset(fruits, subset = Annee == 2016)
# La commande suivante fait le même travail.
fruits2016 <- fruits[fruits$Annee == 2016, ]

# Puis, retirons du jeu de données les lignes avec une production nulle ou manquante.
fruits2016p <- subset(fruits2016, subset = !is.na(Production) & Production != 0)
# La commande suivante fait le même travail.
fruits2016p <- fruits2016[!is.na(fruits2016$Production) & fruits2016$Production != 0, ]


# ---- Solution 1 ----

# Isolons les valeurs uniques d'item par région.
itemUnique <- tapply(fruits2016p$Item, INDEX = fruits2016p$Region, FUN = unique)

# Ensuite calculons le nombre d'items uniques par région.
nbreUnique <- sapply(itemUnique, length)

# Finalement, isolons la ou les régions associées au nombre le plus grand.
sum(nbreUnique == max(nbreUnique))
## [1] 1
# Ce nombre maximum est observé pour un seul pays.
# Nous pouvons donc utiliser which.max pour l'identifier
names(which.max(nbreUnique))
## [1] "Espagne"

# Réponse : L'Espagne


# ---- Solution 2 ----

# Fréquences croisées entre les régions et les items
croisees <- table(Region = fruits2016p$Region, Item = fruits2016p$Item)

# Vérification
# Dans le jeu de données fruits, est-ce possible de retrouver une région combinée à 
# un item sur plus d'une ligne?
# Si oui, des fréquences de croisées sont supérieures à 1. Est-ce le cas?
table(croisees)
## croisees
##    0    1 
## 5624 2527 
# Non, croisees comprend uniquement des fréquences nulles et des 1.

# Alors la somme des lignes des fréquences croisées correspond au nombre d'items  
# distincts produits par chaque région en 2016
nbreItems <- rowSums(croisees)

# Identification de la région avec le nombre max d'items
names(which.max(nbreItems))
## [1] "Espagne"

17.2

Quelle(s) région(s) a(ont) produit la troisième plus grande variété d’items distincts en 2016 ?

Solution :

# Nous pourrions ordonner en ordre décroissant les nombres d'items
nbreItemsSorted <- sort(nbreItems, decreasing = TRUE)
head(nbreItemsSorted, n = 15)

# Le troisième plus grand nombre d'items distincts est 28.
unique(nbreItemsSorted)[3]

# Les régions avec ce nombre d'items distincts sont les suivants :
names(nbreItemsSorted)[nbreItemsSorted == unique(nbreItemsSorted)[3]]

Question 18

Exploration d’un jeu de données

Quelle(s) région(s) a(ont) produit la plus grande quantité de fruits en moyenne par année (moyenne, entre les trois années, de la production totale, peu importe l’item) ?

Solution :

# Il faut d'abord calculer la production totale par région et année
prodTot <- aggregate(Production ~ Region + Annee, 
                     data = fruits, 
                     FUN = sum, na.rm = TRUE)

# Remarque : cette commande est équivalente
prodTot2 <- aggregate(fruits$Production, 
                      by = list(Region = fruits$Region, Annee = fruits$Annee), 
                      FUN = sum, na.rm = TRUE)

# Est-ce que les résultats sont vraiment les mêmes?
all.equal(prodTot, prodTot2)
## [1] "Names: 1 string mismatch"                                               
## [2] "Attributes: < Component “row.names”: Numeric: lengths (627, 630) differ >"
## [3] "Component 1: Lengths (627, 630) differ (string compare on first 627)"     
## [4] "Component 1: 544 string mismatches"                                       
## [5] "Component 2: Numeric: lengths (627, 630) differ"                          
## [6] "Component 3: Numeric: lengths (627, 630) differ"  

# Non! Les résultats ne sont pas de même dimension (prodTot2 a plus de lignes). 
# Pourquoi?

head(prodTot)
##           Region Annee Production
## 1    Afghanistan  2014    1714868
## 2 Afrique du Sud  2014    7060920
## 3        Albanie  2014     708365
## 4        Algerie  2014    5798981
## 5      Allemagne  2014    2821172
## 6         Angola  2014    4393539
head(prodTot2)
##          Group.1 Group.2       x
## 1    Afghanistan    2014 1714868
## 2 Afrique du Sud    2014 7060920
## 3        Albanie    2014  708365
## 4        Algerie    2014 5798981
## 5      Allemagne    2014 2821172
## 6         Angola    2014 4393539
# À part des différences dans un nom de colonne, les 6 premières 
# lignes des deux fichiers sont identiques.

# Afin d'identifier les lignes de prodTot2 et prodTot qui diffèrent, 
# nous pourrions procéder comme suit :

# 1 - Concaténer en une seule chaîne de caractères toutes les données 
#     sur une ligne, et ce, pour les deux data frames
prodTotConcat <- apply(prodTot, MARGIN = 1, FUN = paste, collapse = "_")
prodTot2Concat <- apply(prodTot2, MARGIN = 1, FUN = paste, collapse = "_")

# 2 - Identifier les chaînes de caractères différentes
setdiff(prodTotConcat, prodTot2Concat)
## character(0)

# Interprétation de cette sortie : 
# toutes les observations dans prodTot se trouvent dans prodTot2

setdiff(prodTot2Concat, prodTotConcat)
## [1] "Iles Vierges americaines_2014_        0"
## [2] "Iles Vierges americaines_2015_        0" 
## [3] "Iles Vierges americaines_2016_        0"

# Interprétation de cette sortie : 
# Les trois observations de plus dans prodTot2Concat contiennent des
# valeurs totales de production nulles, en fait obtenues du calcul
sum(NA, na.rm = TRUE)
## [1] 0

# Cette différence s'explique par l'argument na.action présent dans 
# la version de aggregate qui prend en entrée une formule, mais absent 
# dans la version de aggregate qui prend les arguments x, by et FUN.
# La version de aggregate qui prend en entrée une formule applique
# par défaut la fonction na.omit sur le jeu de données fourni à
# l'argument data (mais restreint aux variables concernées).

# Nous pouvons travailler avec prodTot, parce que nous souhaitons laisser 
# tomber les observations manquantes.

# Fin de la remarque


# Ensuite calculons la production totale moyenne par année.
# Nous pourrions utiliser tapply, by ou aggregate.
prodTotMoy <- tapply(prodTot$Production, INDEX = prodTot$Region, FUN = mean)
summary(prodTotMoy)
##     Min.   1st Qu.    Median      Mean   3rd Qu.      Max. 
##        3     41071    465151   4071896   2386799 259149141 
        
# Quelle(s) région(s) est(sont) associée(s) au maximum?
names(prodTotMoy[prodTotMoy == max(prodTotMoy)])
## [1] "Chine, continentale"

# Réponse : La Chine

Question plus difficile

À faire seulement si vous n’êtes pas trop découragé!

Question 19

Exploration d’un jeu de données

Identifiez le ou les items les plus produits (en termes de production annuelle moyenne) par chacune des régions.

Est-ce vrai de dire que l’item le plus produit au Canada est la pomme ?

Solution :

# Tout d'abord, retirons du jeu de données les observations
# avec une valeur de production manquante
fruitsNoNA <- subset(fruits, subset = !is.na(Production))

# Ensuite, calculons la production moyenne par année pour toutes
# les combinaisons de région et item.
prodMoy <- tapply(X = fruitsNoNA$Production,
                  INDEX = list(Region = fruitsNoNA$Region, Item = fruitsNoNA$Item),
                  FUN = mean)
# Le résultat est une matrice avec les régions en lignes et les items en colonnes.

# Identifions la production moyenne maximale par région. 
prodMoyMax <- apply(prodMoy, MARGIN = 1, FUN = max, na.rm = TRUE)

# Comparons prodMoy à prodMoyMax pour identifier les items
# associés aux productions maximales par région.
itemMaxId <- apply(prodMoy == prodMoyMax, MARGIN = 1, FUN = which)
# Ici, la règle de recyclage a été appliquée pour répéter le vecteur
# prodMoyMax autant de fois qu'il y a de colonnes dans prodMoy.

# La fonction which appliquée ligne par ligne à la matrice logique
# a permis d'obtenir les indices des items associés aux productions
# moyennes maximales par région.
# En fait, on obtient un seul item à production maximal par région.
# Les maximums étaient donc uniques par région.

# Les indices dans itemMaxId font référence à l'odre des items dans 
# les colonnes de prodMoy
itemMax <- colnames(prodMoy)[itemMaxId]
names(itemMax) <- names(itemMaxId)

# Le vecteur itemMax contient, pour chaque région, l'item avec la 
# production annuelle moyenne maximale.
itemMax
##    Afghanistan 
##      "Raisins" 
## Afrique du Sud 
##      "Raisins" 
##        Albanie 
##    "Pasteques" 
##        Algerie 
##    "Pasteques" 
##      Allemagne 
##      "Raisins" 
##         Angola 
##      "Bananes"
## ...

itemMax[["Canada"]]
## [1] "Pommes"

# Réponse : L'affirmation est vraie.

Algèbre linéaire

Matière non évaluée, à faire seulement si c’est pertinent pour vous.

Question 20 :

Utilisez une fonction présentée dans le section « Algèbre linéaire » des notes de cours de cette semaine pour calculer en une seule commande la distance euclidienne entre toutes les paires d’éléments du vecteur suivant :

x <- c(5, 3, 8, 4, 6, 2)

Rappel : la distance euclidienne entre deux points x1 et x2 dans un espace à une seule dimension est la racine carrée de (x2 - x1)^2 (en 1D la distance euclidienne revient donc à une différence en valeur absolue).

Vous devriez arriver aux mêmes valeurs (pas au même format de sortie) que celles obtenues avec la commande suivante :

dist(x)

Solution

x <- c(5, 3, 8, 4, 6, 2)

sqrt(outer(x, x, FUN = "-")^2)
#ou
abs(outer(x, x, FUN = "-"))
##      [,1] [,2] [,3] [,4] [,5] [,6]
## [1,]    0    2    3    1    1    3
## [2,]    2    0    5    1    3    1
## [3,]    3    5    0    4    2    6
## [4,]    1    1    4    0    2    2
## [5,]    1    3    2    2    0    4
## [6,]    3    1    6    2    4    0

dist(x, diag = TRUE)
##   1 2 3 4 5 6
## 1 0          
## 2 2 0        
## 3 3 5 0      
## 4 1 1 4 0    
## 5 1 3 2 2 0  
## 6 3 1 6 2 4 0

# La fonction dist affiche seulement un triangle
# (le triangle inférieur par défaut) de la matrice 
# de distance obtenue puisque celle-ci est symétrique.

Question 21 :

La matrice suivante est orthogonale :

A <- matrix(c(cos(2), sin(2), -sin(2), cos(2)), nrow = 2, ncol = 2)
A
##            [,1]       [,2]
## [1,] -0.4161468 -0.9092974
## [2,]  0.9092974 -0.4161468

Une matrice orthogonale \(Q\) possède la propriété que \(Q^t Q = QQ^t = I\), où \(I\) est la matrice identité et l’exposant \(t\) représente la transposée de la matrice.

En conséquence, l’inverse de la matrice est égal à sa transposée (\(Q^{-1} = Q^t\)) pour une matrice othogonale.

Vérifiez si la matrice A respecte bien ces propriétés.

Solution

A <- matrix(c(cos(2), sin(2), -sin(2), cos(2)), nrow = 2, ncol = 2)

all.equal(A%*%t(A), diag(2))  # Vérification que AtA=I
## [1] TRUE
all.equal(t(A)%*%A, diag(2))  # Vérification que AAt=I
## [1] TRUE
all.equal(solve(A), t(A))  # Vérification que inverse de A = At
## [1] TRUE