3 Simulations de Monte-Carlo
Il est souvent utilse de faire des simulations de Monte-Carlo afin d’étudier les propriétés des estimateurs que nous rencontrerons, et de “visualiser” leurs comprtements. Ces propriétés (biais, convergence, efficacité etc.) sont des propriétés statistques qui ont trait à la distribution des estimateurs, vus comme une variables aléatoire. Une simulation de Monte-Carlo consiste en quelque sortes à tirer plein de valeurs de ces variables aléatoires afin d’étudier leurs distribution.
3.1 Un premier exemple
Votre cours de statistrique et de probabilité vous a (normalement) appris que la moyenne d’un échantillon de taille \(n\) issu d’une loi Normale \(N(\mu,\sigma^2)\) de moyenne \(\mu\) et d’écart-type \(\sigma\) est une variable aléatoire suivant une loi normale de moyenne \(\mu\) et d’écart-type \(\frac{\sigma}{\sqrt{n}}\). Utilisons R pour le vérifier.
Commençons par calculer la moyenne d’un échantillon de 100 tirages d’une \(N(0,1)\)
## [1] -0.03247968
On voit que la moyenne des 100 tirages n’est pas strictement égale à 0, et on ne voit pas bien comment juger de l’écart-type ou de la loi de probabilité de cette moyenne. Si on lance le code une seconde fois, le résultat va d’ailleurs changer :
## [1] -0.1270524
La raison est que la théorie nous donne les caractéristiques de la distribution de cette moyenne au travers d’un grand nombre d’échantillons de 1000 tirages d’une \(N(0,1)\). Nous allons donc répéter le code ci-dessous un grand nombre de fois (\(K\) fois), noter à chaque fois la moyenne obtenue, et étudier la distribution de ces \(K\) moyennes.
Une première façon de faire consiste à utiliser une boucle for :
K <- 10000 # On va faire 10000 réplications, on aura donc 10000 moyennes
n <- 100
mu <- 0
sigma <- 1
moyennes <- c() # On crée un vecteur vide qui contiendra les K moyennes
for (i in 1:K) { # on initialise la boucle
x <- rnorm(n,mean=mu,sd=sigma) # on tire l'ééchantillon
moyennes[i] <- mean(x) # on stocke la moyenne issue de la ième réplication à
# la ième position du vecteur "moyennes"
}
moyennes[1:10] # on affiche les 10 premières moyennes## [1] 0.04282867 0.21528147 -0.09064180 -0.02316018 0.01417983 0.01698663
## [7] 0.17348736 -0.01235884 0.23749812 -0.11105861
R propose toutefois une fonction dédiée à ce genre de répétition : replicate(). Elle prend deux arguments principaux : le nombre de réplications, et l’expression à répéter. Elle renvoie directement le vecteur des résultats, ce qui nous évite d’écrire la boucle et de gérer nous-mêmes le vecteur moyennes.
K <- 10000
n <- 100
mu <- 0
sigma <- 1
# À chaque réplication, on tire un échantillon et on en calcule la moyenne
moyennes <- replicate(K, mean(rnorm(n, mean = mu, sd = sigma)))
moyennes[1:10] # on affiche les 10 premières moyennes## [1] -0.08714515 0.08825767 -0.26370478 -0.08185618 -0.09904880 -0.06632692
## [7] -0.15473979 0.17106159 -0.09142349 -0.10830222
## [1] 7.841688e-05
## [1] 0.09971326
## [1] 0.1
hist(moyennes,prob=TRUE) # on trace l'histogramme des moyennes
lines(density(moyennes),col="red") # on y ajoute le tracé de la densité des moyennes
Ceux qui ont suivi le tutoriel “Visualize Data” peuvent utiliser les outils graphiques du package ggplot2
library(ggplot2)
ggplot(mapping=aes(x=moyennes)) +
geom_histogram(aes(y=..density..),fill="grey",color="black") +
geom_density(fill="blue",alpha=0.2)## Warning: The dot-dot notation (`..density..`) was deprecated in ggplot2 3.4.0.
## ℹ Please use `after_stat(density)` instead.
## This warning is displayed once per session.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.

NB : le y=..density.. indique à geom_histogram d’utiliser la densité en ordonnées, au lieu de la fréquence. Ça permet d’avoir l’histogramme et la courbe des densité à la même échelle.
On constate que 1. La moyenne des K=10000 moyennes est très proche de la moyenne théorique (0) 2. L’écart-type des K moyennes est très proche de l’écart-type théorique (0.1) 3. La distribution est proche de celle d’une loi normale
3.2 Généralisation : faire varier les paramètres
L’exemple précédent a montré que l’on pouvait assez facilement effectuer une simulation basique avec du code assez simple. Néanmoins, ce code ne permet de simuler qu’un seul scénario (valeurs de \(n\), de \(\mu\) et de \(\sigma\)) à la fois. Si on souhaite voir ce qu’il se passe lorsqu’on fait varier les paramètres de la simulation, il faudrait copier-coller le code de nombreuses fois et modifer à chaque fois les paramètres. De plus, il faudrait faire attention à sauvegarder les résultats dans des vecteurs différents à chaque fois.
Afin de s’éviter ces désagréments, nous allons automatiser ces tâches en combinant deux outils très répandus : replicate(), que nous venons de voir, et les fonctions map() / pmap() du package purrr (inclus dans le tidyverse), qui servent à appliquer une même fonction à toute une série de valeurs.
On commence par charger le tidyverse (voir les tutoriels)
On définit ensuite une fonction qui va effectuer le tirage de l’échantillon aléatoire, calculer la moyenne, et retourner le résultat :
simul_moyenne <- function(n,mu,sigma) { # Notre fonction s'appelle "simul_moyenne" et prend les
# arguments n, mu et sigma
tirages <- rnorm(n,mean=mu,sd=sigma) # on effectue le tirage aléatoire avec les valeurs données en argument
mean(tirages) # on retourne la moyenne
}On définit ensuite l’ensemble des “scénarios” de simulations, c’est à dire toutes les combinaisons des valeurs des paramètres que l’on veut faire varier. Ici on veut deux valeurs pour la taille d’échantillon, deux valeurs pour l’espérance, et trois pour l’écart-type. La fonction expand_grid() (du package tidyr, inclus dans le tidyverse) construit pour nous la data frame de toutes les combinaisons possibles :
## # A tibble: 12 × 3
## n mu sigma
## <dbl> <dbl> <dbl>
## 1 10 0 1
## 2 10 0 2
## 3 10 0 4
## 4 10 5 1
## 5 10 5 2
## 6 10 5 4
## 7 100 0 1
## 8 100 0 2
## 9 100 0 4
## 10 100 5 1
## 11 100 5 2
## 12 100 5 4
On obtient bien \(2 \times 2 \times 3 = 12\) scénarios, chacun correspondant à une ligne.
Il ne reste plus qu’à lancer, pour chaque scénario, replicate() afin d’obtenir les 10000 moyennes correspondantes. C’est le rôle de pmap() : cette fonction parcourt les lignes de notre data frame de scénarios et, pour chacune, appelle une fonction en lui passant les valeurs de n, mu et sigma de la ligne. Comme chaque scénario produit un vecteur de 10000 moyennes, on stocke ce vecteur dans une nouvelle colonne (une colonne-liste), que l’on “déplie” ensuite avec unnest() pour retrouver une data frame classique avec une ligne par réplication.
K <- 10000 # nombre de réplications par scénario
data_resultats <- scenarios %>%
mutate(moyenne = pmap(list(n, mu, sigma), # pour chaque ligne (n, mu, sigma)...
\(n, mu, sigma) replicate(K, simul_moyenne(n, mu, sigma)))) %>% # ...on fait K réplications
unnest(moyenne) # on déplie : une ligne par réplicationNB : la notation \(n, mu, sigma) ... est une façon abrégée d’écrire function(n, mu, sigma) ... (disponible depuis R 4.1). Les deux écritures sont strictement équivalentes ; on aurait donc aussi pu écrire function(n, mu, sigma) replicate(K, simul_moyenne(n, mu, sigma)).
Regardons ce que l’objet “data_resultats” contient. la fonction head() permet de lister les premières lignes d’une data frame, afin de vérifier rapidement son contenu.
## # A tibble: 6 × 4
## n mu sigma moyenne
## <dbl> <dbl> <dbl> <dbl>
## 1 10 0 1 -0.145
## 2 10 0 1 -0.0995
## 3 10 0 1 0.204
## 4 10 0 1 -0.219
## 5 10 0 1 0.0430
## 6 10 0 1 -0.606
## [1] 120000 4
On obtient une data frame bien pratique : une colonne par paramètre (n, mu, sigma) et une colonne moyenne contenant le résultat de chaque réplication.
On va maintenant calculer les moyennes et écart-types des 10000 moyennes calculées pour chaque scénario de simulation. On fait appel aux outils de “pipe” (%>%) et de groupe (group_by()) expliqués dans le tutoriel “Work With Data”. On en profite pour ajouter une variable “ecty_theor” donnant l’écart-type théorique de la distribution des moyennes
data_resultats %>%
mutate(ecty_theor=sigma/sqrt(n)) %>%
group_by(n,mu,sigma) %>%
summarise(moy=mean(moyenne),ecty=sd(moyenne),ecty_theor=mean(ecty_theor))## `summarise()` has regrouped the output.
## ℹ Summaries were computed grouped by n, mu, and sigma.
## ℹ Output is grouped by n and mu.
## ℹ Use `summarise(.groups = "drop_last")` to silence this message.
## ℹ Use `summarise(.by = c(n, mu, sigma))` for per-operation grouping
## (`?dplyr::dplyr_by`) instead.
## # A tibble: 12 × 6
## # Groups: n, mu [4]
## n mu sigma moy ecty ecty_theor
## <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 10 0 1 -0.000172 0.314 0.316
## 2 10 0 2 0.00683 0.625 0.632
## 3 10 0 4 0.00675 1.26 1.26
## 4 10 5 1 5.00 0.314 0.316
## 5 10 5 2 5.00 0.628 0.632
## 6 10 5 4 4.98 1.25 1.26
## 7 100 0 1 -0.000298 0.0996 0.1
## 8 100 0 2 -0.0000693 0.198 0.2
## 9 100 0 4 0.00808 0.400 0.4
## 10 100 5 1 5.00 0.0995 0.1
## 11 100 5 2 5.00 0.201 0.2
## 12 100 5 4 5.00 0.399 0.4
On constate que la moyenne des moyenne (colonne “moy”) est très proche de la moyenne théorique (“mu”), de même que l’écart-type de la distribution des moyennes (“ecty”) est très proche de l’écart-type théorique. Nos observations semblent bien coller avec la théorie.
On complète l’exercice en faisant un graphique de la fonction de densité estimées de nos résultats, pour chaque valeur de \(n\), \(\mu\) et \(\sigma\).
ggplot(data=data_resultats,mapping=aes(x=moyenne,group=sigma,color=factor(sigma))) +
geom_density() +
facet_wrap(mu ~ n,labeller=label_both)
3.3 Avec une régression par MCO
R permet de faire une régression linéaire par MCO avec la commande lm()(voir le chapitre sur la répression par MCO) ci-dessous
On va charger une base de données préinstallée avec R : les données “mtcars”
## mpg cyl disp hp drat wt qsec vs am gear carb
## Mazda RX4 21.0 6 160 110 3.90 2.620 16.46 0 1 4 4
## Mazda RX4 Wag 21.0 6 160 110 3.90 2.875 17.02 0 1 4 4
## Datsun 710 22.8 4 108 93 3.85 2.320 18.61 1 1 4 1
## Hornet 4 Drive 21.4 6 258 110 3.08 3.215 19.44 1 0 3 1
## Hornet Sportabout 18.7 8 360 175 3.15 3.440 17.02 0 0 3 2
## Valiant 18.1 6 225 105 2.76 3.460 20.22 1 0 3 1
Effectuons une régression linaire de, par exemple, “mpg” sur “cyl” et “disp”
##
## Call:
## lm(formula = mpg ~ cyl + disp, data = mtcars)
##
## Coefficients:
## (Intercept) cyl disp
## 34.66099 -1.58728 -0.02058
La sortie est assez minimale. Stockons cette régression dans un objet que nous appellerons “ma_regression” et faisons un summary() de ce dernier
##
## Call:
## lm(formula = mpg ~ cyl + disp, data = mtcars)
##
## Residuals:
## Min 1Q Median 3Q Max
## -4.4213 -2.1722 -0.6362 1.1899 7.0516
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 34.66099 2.54700 13.609 4.02e-14 ***
## cyl -1.58728 0.71184 -2.230 0.0337 *
## disp -0.02058 0.01026 -2.007 0.0542 .
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 3.055 on 29 degrees of freedom
## Multiple R-squared: 0.7596, Adjusted R-squared: 0.743
## F-statistic: 45.81 on 2 and 29 DF, p-value: 1.058e-09
On a déjà plus de détails.
On peut extraire divers éléments issus de lm() :
## (Intercept) cyl disp
## 34.66099474 -1.58727681 -0.02058363
## (Intercept) cyl disp
## 2.54700388 0.71184427 0.01025748
## cyl
## -1.587277
summary(ma_regression)$coef[,"Estimate"][["cyl"]] # la même valeur, mais sans le nom associé, notez les doubles crochets## [1] -1.587277
On va maintenant utiliser ces éléments pour construire une simulation de Monte-Carlo de la distribution de l’estimateur des MCO \(\hat\beta_x\) dans un cadre qui respecte les hypothèses de Gauss-Markov. On procède comme précédemment : une fonction qui simule un échantillon et retourne le \(\hat\beta_x\) estimé, puis replicate() pour répéter, et map() pour faire varier la taille d’échantillon.
library(tidyverse)
betareg<-function(n) {
x <- rnorm(n,mean=0,sd=1)
epsilon <- rnorm(n,mean=0,sd=1) # epsilon suit une loi normale non corrélée à x
y <- 1+x+epsilon
mareg <- lm(y~x)
coeffs <- summary(mareg)$coef[,"Estimate"]
betax <- coeffs[["x"]]
betax # on retourne le beta chapeau de x
}
n_grid <- c(10, 100) # les tailles d'échantillon que l'on veut comparerComme il n’y a ici qu’un seul paramètre qui varie (n), on peut se contenter de map() (au lieu de pmap()). Pour chaque valeur de n, on lance replicate() afin d’obtenir 10000 estimations de \(\hat\beta_x\). On range le tout dans une data frame que l’on déplie avec unnest(), exactement comme dans la section précédente.
K <- 10000 # nombre de réplications par scénario
data_resultats <- tibble(n = n_grid) %>%
mutate(betax = map(n, \(n) replicate(K, betareg(n)))) %>%
unnest(betax)## # A tibble: 6 × 2
## n betax
## <dbl> <dbl>
## 1 10 0.395
## 2 10 1.06
## 3 10 1.36
## 4 10 0.813
## 5 10 0.355
## 6 10 1.52
## # A tibble: 2 × 3
## n moy ecty
## <dbl> <dbl> <dbl>
## 1 10 1.000 0.386
## 2 100 1.00 0.100

Les résultats ci-dessus sont-ils ceux que l’on s’attendait à avoir ? Que nous dit la théorie ?
3.4 Pour résumer
La démarche générale d’une simulation de Monte-Carlo tient en quatre étapes :
| Étape | Outil utilisé |
|---|---|
| Fonction de simulation | une fonction qui retourne directement la valeur |
| Grille des scénarios | expand_grid(n = n_grid, ...) |
| Lancer les réplications | pmap(..., \(...) replicate(K, func(...))) |
Récupérer une data frame de résultats |
unnest() sur la colonne-liste |
L’avantage de cette approche est qu’elle ne repose que sur des fonctions de base de R et du tidyverse, que vous réutiliserez dans bien d’autres contextes.