Reading time ~ 10 minutes ->

1. Calculateur de probabilité euromillions : Introduction

Les jeux de hasard et des super loteries font rêver énormément de joueurs dans le monde. Il faut dire que les sommes annoncées pour les rangs de gains et plus particulièrement pour le rang 1 sont élevées. Prenez l’EuroMillions, le gain maximum possible est de 190,000,000 €. Ce qui fait le jeu de hasard avec le plus gros gain en Europe.

Nous savons pourtant qu’il s’agit avant tout de probabilités, qu’il existe des millions de combinaisons et que les chances de remporter le gros lot sont assez minces. Prenez le rang 1, la chance de gagner le jackpot équivaut à 1 chance sur 139,838,160.

Mais qu’est-ce la probabilité ? Simplement exprimé, c’est un indicateur mesurant la chance ou le risque qu’un événement se produise. Mathématiquement, c’est un nombre compris entre 0 et 1. Plus la valeur est proche de 1, plus la chance que l’événement se produise est grande. C’est aussi le rapport du nombre de cas favorable au nombre de cas possible.

Pour gagner au rang 1, il faut trouver la bonne combinaison, à savoir les 5 numéros et les 2 étoiles. Si on s’intéresse à la probabilité qu’un numéro donné soit tiré (laissons les étoiles de côté) nous devons calculer le nombre de grilles possibles ou ce numéro apparaît. Avant d’aller plus loin, intéressons-nous à l’analyse combinatoire.

2. Analyse combinatoire

L’analyse combinatoire fournit entre autres des formules pour calculer le nombre d’arrangements possibles de n objets pris k à la fois ou le nombre de combinaisons de n objets pris k à la fois ou le nombre de permutations des n éléments d’un ensemble. Allons-y !

2.1. Permutations sans répétition

La permutation c’est lorsque nous cherchons le nombre de réarrangement possible d’objets et cela de manière différente. Ici l’ordre à son importance et donc nous comptabilisons toutes les façons d’ordonner les objets observés. Pour le calcul de ce nombre, nous utilisons la factorielle. La factorielle d’un nombre entier s’écrit avec un point d’exclamation n!. Cette notion a été introduite par Christian Kramp en 1808. La factorielle va être utilisée dans les différentes formules de ce post.

Par ex. pour calculer le nombre de mots possibles avec 10 lettres sans lettre répétée, nous calculons la factorielle de 10, soit 10! mots possibles.

permutation <- function(n) {factorial(n)}
permutation(10)
## [1] 3628800

Cela nous donne 3,628,800 façons d’écrire un mot avec 10 lettres données.

2.2. Arrangement

L’arrangement est le tirage d’une quantité d’objets avec ordre dans un ensemble d’objets. Comme le tiercé dans l’ordre par ex. En réalité, une permutation est un arrangement avec le tirage de tous les objets. Pour l’exemple précédent cela consiste à écrire nos mots avec toutes les lettres données. Des mots de 10 lettres donc.

La formule :

arrangement <- function(n, k) {
  
  factorial(n) / (factorial(n - k))
  
}

Ex.

arrangement(10, 10)
## [1] 3628800

Si nous composons des mots de 3 lettres uniquement, nous avons 720 combinaisons possibles avec les 10 lettres données et différentes.

arrangement(10, 3)
## [1] 720

2.3. Combinaisons

La combinaison est le tirage d’une quantité d’objets sans ordre dans un ensemble d’objets, par ex. {a, b} et {b, a} est compté qu’une seule fois. C’est ce qui nous intéresse pour l’EuroMillions car l’ordre du tirage importe peu. Un tirage de 5 boules dans un ensemble de 50. C’est le jackpot peu importe l’ordre.

Voici la formule :

combinaison <- function(n, k) {
  
  factorial(n) / (factorial(k) * (factorial(n - k)))
  
}
combinaison(10, 3)
## [1] 120

Le nombre de combinaison est 6x moins que l’arrangement, car on supprime toutes les combinaisons obtenues avec 3 lettres identiques. Combien-avons nous de combinaisons permutations pour 3 lettres ? Nous en avons 3! donc 6 (3x2).

3. Jouons à l’EuroMillions

3.1. Trouver les 5 numéros et les 2 étoiles

Souvenez-vous, une grille est composée de 5 numéros et de 2 étoiles. Intéressons-nous à la première partie et le nombre de combinaisons possibles (l’ordre n’a pas d’importantce) d’une grille de 5 numéros parmi les 50 boules dansantes devant nous.

combinaison(50, 5)
## [1] 2118760

Nous avons donc 2,118,760 grilles possibles. Choisissons le numéro 50 par ex. Quel est le nombre de grille où le numéro 50 apparait ? Pour faciliter le calcul, enlevons le numéro 50 de notre urne, ce qui laisse 49 boules. Combien existe-t-il de grilles possibles ?

combinaison(49, 4)
## [1] 211876

Il existe 211,876 grilles. N’oublions pas que l’ordre n’a pas d’importance. Rajoutons le 50 à ces grilles cela nous fait toujours 211,876 combinaisons sur les 2,118,760. Donc une probabilité de 211,876 / 2,118,760. Ce qui équivaut à :

211876 / 2118760
## [1] 0.1

1 chance sur 10 que le numéro 50 sorte à chaque tirage. Il en va de même pour tous les numéros.

3.2. La loi hypergéométrique

La loi hypergéométrique permet de calculer la probabilité d’un tirage dans un tirage. Nous pourrions calculer la probabilité d’avoir 1, 2, 3, 4 ou 5 bons numéros dans un tirage de 5 parmi les 50 numéros. Bien souvent la loi hypergéométrique est illustrée avec des boules blanches et noires dans une urne. Nous allons ici considérer que les boules gagnantes sont nos boules blanches et les autres les boules noires. Ce qui nous donne :

Dans l’urne il y a 50 boules x = 50, dont 5 gagnantes m = 5, Nous tirons 5 boules n = 5, et nous souhaitons avoir 1 boule gagnante parmi les 5 boules tirées k = 1.

La formule est la suivante :

\(\Large P(k) = \frac{\binom{m}{k}\binom{x-m}{n-k}}{\binom{x}{n}}\)

La fonction :

hyper <- function(x, m, n, k) {
  combinaison(m, k) * combinaison(x - m, n - k) / combinaison(x, n)
  }

Testons et examinons la probabilité d’avoir un numéro gagnant dans 1 grille :

hyper(x = 50, m = 1, n = 5, k = 1)
## [1] 0.1

Nous obtenons bien 0.1. Ceci est identique au calcul précédant où nous avons divisés le nombre de grille à 49 numéros avec le nombre de grilles avec 50 boules. Remplaçons nos valeurs x, m, n et k dans la formule :

\(\Large P(k) = \frac{\binom{1}{1}\binom{50-1}{5-1}}{\binom{50}{5}}\)

Nous retrouvons notre résultat 211876 / 2118760.

3.3. Vérification

J’aime faire une simulation pour corroborer ce résultat. L’approche est toute simple, elle consiste à simuler des grilles de 5 numéros. Je map la fonction sample sur un vecteur (1:1000). Ce qui me donne une liste de 100,000 grilles de 5 boules.

grid <- map(1:100000, ~ sample(50, 5))
head(grid, 3)
## [[1]]
## [1] 34  3 21 43 36
## 
## [[2]]
## [1] 18 43  7  4 50
## 
## [[3]]
## [1]  8 48 14 46 23

Générons le numéro gagnant :

winner <- sample(50, 1)
winner
## [1] 12

Et comparons chaque grille avec le numéro gagnant grâce à la fonction %in%. Le résultat de la comparaison est une valeur logique TRUE ou FALSE.

res <- map(grid, ~ winner %in% .)

head(res, 3)
## [[1]]
## [1] FALSE
## 
## [[2]]
## [1] FALSE
## 
## [[3]]
## [1] FALSE

Pour rappel, nous cherchons la probabilité d’avoir 1 bon numéro dans une grille simple. Nous simulons donc un nombre important de grilles simples (en réalité nous devrions tendre vers l’infini pour s’approcher du résulat de notre formule hypergéométrique). Nous comparons, puis calculons la valeur de chaque grille. Ce qui nous intéresse ce sont les grilles avec la valeur égale à 1, c’est à dire dont le résulat de notre comparaison équivaux à 1 TRUE sur les 4, soit un numéro qui apparait dans la combinaison au tirage gagnant.

Pour cela, je transforme les valeurs logiques en numériques puis cacule la somme de chaque grille, compare si la somme équivaux à 1, si oui, j’obtiens un TRUE, si non un FALSE que je transforme à nouveau en numérique (donc de valeur 0 ou 1). L’astuce consiste ensuite à calculer la moyenne pour obtenir le ratio des grilles gagnantes.

map(res, as.numeric) %>% 
  map(sum) %>% 
  map(~ . == 1) %>% 
  as.numeric() %>% 
  mean() 
## [1] 0.09813

CQFD, le résultat s’approche du résultat de notre fonction hypergéométrique, à savoir 0.1. Ou simplement dit, 1 grille sur 10 contient le numéro gagnant.

Calculons d’autres probabilités de gain de l’euro million. Petit détail qui a son importance, je me mets dans les conditions de l’euro million soit 5 numéros et 2 étoiles donc en multipliant les 2 probabilités. Je me suis intérrogé sur les raisons de le faire même lorsque la condition des étoiles vaut 0, k = 0. En réalité, si on compare les 2 résultats, on peut s’apercevoir que la probabilité n’est pas la même. Mais Pourquoi, puisque nous cherchons la probabilité d’obtenir seulement 2 numéros gagnants dans la grille ? Après réflexion, c’est assez logique pusique la probabilité d’avoir 0 étoiles ne vaut pas 0 mais bien 0.68 :

hyper(x = 12, m = 2, n = 2, k = 0)
## [1] 0.6818182

Si la combinaison gagnante ne comprenait que 5 numéros, notre chance d’obtenir 2 bons numéros serait de 1 chance sur 15. Mais l’ajout des 2 étoiles fait “tomber” la probabilité à 1 chance sur 22 même avec 0 étoile de trouvée. Logique car il y une probabilité d’obtenir 1 étoiles voir 2 donc il faut considérer aussi la probabilité d’en avoir 0. Voyons cela :

Avec les étoiles en plus :

1 / (hyper(x = 50, m = 5, n = 5, k = 2) * hyper(x = 12, m = 2, n = 2, k = 0))
## [1] 21.89933

Sans les étoile :

1 / hyper(x = 50, m = 5, n = 5, k = 2)
## [1] 14.93136

3.4. Quelques combinaisons et leur probabilité :

  1. 2 numéros gagnants et 0 étoile (k = 2 et k = 0) : 1 chance sur 22
1 / (hyper(x = 50, m = 5, n = 5, k = 2) * hyper(x = 12, m = 2, n = 2, k = 0))
## [1] 21.89933
  1. 2 numéros gagnants et 1 étoile (k = 2 et k = 0) : 1 chance sur 49
1 / (hyper(x = 50, m = 5, n = 5, k = 2) * hyper(x = 12, m = 2, n = 2, k = 1))
## [1] 49.27349
  1. 4 numéros gagnants et 2 étoiles (k = 2 et k = 0) : 1 chance sur 621,502
1 / (hyper(x = 50, m = 5, n = 5, k = 4) * hyper(x = 12, m = 2, n = 2, k = 2))
## [1] 621502.9
  1. 5 numéros gagnants et 0 étoile (k = 2 et k = 0) : 1 chance sur 3,107,515
1 / (hyper(x = 50, m = 5, n = 5, k = 5) * hyper(x = 12, m = 2, n = 2, k = 0))
## [1] 3107515
  1. 5 numéros gagnants et 2 étoiles (k = 2 et k = 0) : 1 chance sur 139,838,160
1 / (hyper(x = 50, m = 5, n = 5, k = 5) * hyper(x = 12, m = 2, n = 2, k = 2))
## [1] 139838160

4. Simulateur

Pour réaliser notre simulateur, nous allons écrire une fonction principale simulant un tirage et une comparaison avec un certain nombre de grilles (l’argument de notre fonction sera le nombre de grilles). Nous ajouterons aussi une table afin d’attribuer un gain à chaque grille.

4.1. Le tirage

Nous utilisons la fonction sample.int pour générer 5 numéros sans répétition (replace = FALSE). Idem pour les 2 étoiles

draw_n <- sample.int(50, 5, replace = FALSE)
draw_s <- sample.int(12, 2, replace = FALSE)

4.2. La grille

Créons d’abord l’argument grid avec la valeur 1 de départ qui définira le nombre de grilles jouées :

grid <- 1

Initialisons 2 listes de longueur grid. Ceci nous servira de conteneur pour l’ensemble des grilles générées :

grid_n <- vector("list", grid)
grid_s <- vector("list", grid)

Puis une boucle pour générer nos grilles :

  for(i in 1:grid)  {

      grid_n[[i]] <- sample.int(50, 5, replace = FALSE) %>% sort()
      grid_s[[i]] <- sample.int(12, 2, replace = FALSE) %>% sort()
  }

Vérifions :

head(grid_n, 1)
## [[1]]
## [1] 14 25 43 45 46
head(grid_s, 1)
## [[1]]
## [1]  9 12

4.3. Le résultat

Vient le moment de vérifier si nos grilles comportent la combinaison gagnante. Nous allons comparer chaque numéro de chaque grille avec le tirage. Nous obtiendrons avec la fonction %in% une valeur logique TRUE si les numéros correspondent, FALSE dans le cas contraire. Nous avons appliqué le même principe précédemment avec comme différence le fait que nous calculons la somme de chaque comparatif obtenu via la fonction sum directement. La fonction map exécute notre fonction sur chaque grille de la liste. Si nous obtenons une valeur de 5, cela signifie que les 5 numéros sont gagnants.

result_n <- grid_n %>% map(~ sum(. %in% draw_n))
result_s <- grid_s %>% map(~ sum(. %in% draw_s))

4.4. Les gains

Maintenant nous transformons nos 2 listes en une table de 2 colonnes dont chaque rang correspond à une grille. Nous ajoutons la valeur d’achat de la grille et une colonne supplémentaire avec le gain obtenu. Pour cela nous utilisons la fonction case_when pour attribuer le gain en fonction de la valeur des 2 premières colonnes.

  tibble("n" = flatten_int(result_n), "e" = flatten_int(result_s)) %>% 
  mutate(cost = 2.5) %>% 
  mutate(gain = case_when(
                  
              n == 0 & e == 0 ~ 0,
              n == 0 & e == 1 ~ 0,
              n == 0 & e == 2 ~ 0,
              n == 1 & e == 0 ~ 0,
              n == 1 & e == 1 ~ 0,
              n == 1 & e == 2 ~ 0,
              n == 2 & e == 0 ~ 4,
              n == 2 & e == 1 ~ 8,
              n == 1 & e == 2 ~ 10,
              n == 3 & e == 0 ~ 12,
              n == 3 & e == 1 ~ 14,
              n == 2 & e == 2 ~ 19,
              n == 3 & e == 2 ~ 59,
              n == 4 & e == 0 ~ 101,
              n == 4 & e == 1 ~ 201,
              n == 4 & e == 2 ~ 4143,
              n == 5 & e == 0 ~ 51792,
              n == 5 & e == 1 ~ 310751,
              n == 5 & e == 2 ~ 1700000
              )) 
## # A tibble: 1 × 4
##       n     e  cost  gain
##   <int> <int> <dbl> <dbl>
## 1     0     0   2.5     0

4.5. La fonction

Combinons maintenant l’ensemble dans une fonction. Nous ajoutons juste une synthèse des résultats avec la fonction summarize.

play <- function(grid = 1) {
  
  draw_n <- sample.int(50, 5, replace = FALSE) %>% sort()
  draw_s <- sample.int(12, 2, replace = FALSE) %>% sort()
  
  grid_n <- vector("list", grid)
  grid_s <- vector("list", grid)
  
  for(i in 1:grid)  {

      grid_n[[i]] <- sample.int(50, 5, replace = FALSE) %>% sort()
      grid_s[[i]] <- sample.int(12, 2, replace = FALSE) %>% sort()
  }
  

  result_n <- grid_n %>% map(~ sum(. %in% draw_n))
  result_s <- grid_s %>% map(~ sum(. %in% draw_s))
  
  tibble("n" = flatten_int(result_n), "e" = flatten_int(result_s)) %>% 
  mutate(cost = 2.5) %>% 
  mutate(gain = case_when(
                  
              n == 0 & e == 0 ~ 0,
              n == 0 & e == 1 ~ 0,
              n == 0 & e == 2 ~ 0,
              n == 1 & e == 0 ~ 0,
              n == 1 & e == 1 ~ 0,
              n == 1 & e == 2 ~ 0,
              n == 2 & e == 0 ~ 4,
              n == 2 & e == 1 ~ 8,
              n == 1 & e == 2 ~ 10,
              n == 3 & e == 0 ~ 12,
              n == 3 & e == 1 ~ 14,
              n == 2 & e == 2 ~ 19,
              n == 3 & e == 2 ~ 59,
              n == 4 & e == 0 ~ 101,
              n == 4 & e == 1 ~ 201,
              n == 4 & e == 2 ~ 4143,
              n == 5 & e == 0 ~ 51792,
              n == 5 & e == 1 ~ 310751,
              n == 5 & e == 2 ~ 1700000
              )) %>% 
  summarise(t_cost = sum(cost), t_gain = sum(gain)) %>%
  mutate(bank = t_gain - t_cost)
  
}

Testons notre fonction pour 10 grilles :

play(10)
## # A tibble: 1 × 3
##   t_cost t_gain  bank
##    <dbl>  <dbl> <dbl>
## 1     25      4   -21

Et pour 1,000 grilles jouées dans un tirage :

play(1000)
## # A tibble: 1 × 3
##   t_cost t_gain  bank
##    <dbl>  <dbl> <dbl>
## 1   2500    457 -2043

4.6. Une autre fonction avec notre fonction

Comparer n grilles à 1 tirage est sympa, mais comment faire si nous souhaitons simuler plusieurs tirage ? La programmation fonctionnelle va nous permettre d’y arriver. Le livre Advanced R (Hadley Wickham) traite très bien ce sujet.

2 arguments : le nombre de tirages et le nombre de grilles par tirage. Le coeur de la fonction c’est l’exécution de notre fonction play sur un vecteur de longueur draw.

multi_play <- function(draw = draw, grid = grid) {

    map_dfr(rep(grid, draw), play)
  
}

Résultat pour la participation à 2 tirages avec 2 grilles à chaque fois :

multi_play(2, 2)
## # A tibble: 2 × 3
##   t_cost t_gain  bank
##    <dbl>  <dbl> <dbl>
## 1      5      0    -5
## 2      5      0    -5

4.7. Représentation graphique d’1 joueur.

Si nous souhaitons voir l’évolution de gain d’un jour régulier participant à 20 tirages avec 10 grilles à chaque tirage :

multi_play(draw = 20, grid = 10) %>% 
  mutate(cumsum_cost = cumsum(t_cost), cumsum_gain = cumsum(t_gain)) %>%
  rowid_to_column() %>% 
  pivot_longer(cols = 5:6) %>% 
  ggplot() +
  aes(rowid, col = name, value) +
  geom_line() +
  geom_point() +
  theme_ipsum_rc()

Ce joueur n’est pas très chanceux, mais peut être considéré comme un heureux gagnant à l’EuroMillions.

4.8. Appelons 20 joueurs

Pour l’expérience suivante, nous allons demander à 20 joueurs de jouer plus ou moins 1 an (100 tirages) avec le nombre de grille souhaité (un joueur peut jouer une grille et jusqu’à dix grilles).

players <-tibble(draw = rep(100, 20), grid = sample(10, 20, replace = TRUE)) %>%
  rowid_to_column() %>% 
  mutate(game = map2(draw, grid, multi_play)) %>% 
  unnest(c(game)) %>% 
  group_by(rowid) %>% 
  mutate(n = 1:100) %>% 
  mutate(cumsum_cost = cumsum(t_cost), cumsum_gain = cumsum(t_gain)) %>%
  pivot_longer(cols = 8:9)

Représentation graphique :

players %>% 
  ggplot() +
  aes(n, value, group = interaction(rowid, name), col = name) +
  geom_line(size = 1) +
  facet_wrap(~rowid) +
  theme_ipsum_rc()

Quel joueur à le meilleur rapport gain / dépense ?

player_rank <- players %>% 
  group_by(rowid) %>% 
  summarise(cost = sum(t_cost), gain = sum(t_gain)) %>% 
  mutate(ratio = gain / cost, avg = 2.5 * ratio) %>%
  arrange(desc(ratio))
player_rank
## # A tibble: 20 × 5
##    rowid  cost  gain ratio   avg
##    <int> <dbl> <dbl> <dbl> <dbl>
##  1    14  1000   246 0.246 0.615
##  2    19  2000   458 0.229 0.572
##  3     4  2500   528 0.211 0.528
##  4    11  4000   838 0.210 0.524
##  5     7  2000   398 0.199 0.498
##  6    17   500    96 0.192 0.48 
##  7    12  4500   792 0.176 0.44 
##  8    13  5000   866 0.173 0.433
##  9     3  4000   692 0.173 0.432
## 10     9  1000   172 0.172 0.43 
## 11    20  2500   428 0.171 0.428
## 12     5  3500   596 0.170 0.426
## 13     2  4500   736 0.164 0.409
## 14    16  5000   784 0.157 0.392
## 15    18  3500   528 0.151 0.377
## 16     6  5000   752 0.150 0.376
## 17    15  4000   574 0.144 0.359
## 18    10  1000   128 0.128 0.32 
## 19     1  3500   404 0.115 0.289
## 20     8  1000   104 0.104 0.26

Personne n’a récupéré sa mise. Le gain moyen par grille pour l’ensemble des joueurs est de :

player_rank %>% 
  summarise(avg_gain = mean(avg))
## # A tibble: 1 × 1
##   avg_gain
##      <dbl>
## 1    0.429

Le taux de gain pour chaque euro investi :

player_rank %>% 
  summarise(sum_cost = sum(cost), sum_gain = sum(gain)) %>% 
  mutate(rate = scales::percent(sum_gain / sum_cost)) %>% 
  select(rate)
## # A tibble: 1 × 1
##   rate 
##   <chr>
## 1 17%

4.9. Espérance de gain

Nous pouvons calculer l’espérance de gain pour un pari. L’espérence permet de comparer des jeux de hasard dans un casino par exemple.

  • Si E > 0, il devrait participer à cet événement.
  • Si E < 0, il ne devrait pas participer à cet événement.

Par contre, lorsque nous obtenons E = 0, cela signifie que l’événement est équitable. La valeur indique le montant que nous pouvons espérer gagner en moyenne par participation. La formule est simple :

E = probabilité de gagner x montant gagné par pariprobabilité de perdre x montant perdu par pari

Calculons l’espérance de gain pour le rang 1 et le mega Jackpot de 190,000,000 € :

p <-(hyper(50, m = 5, n = 5, k = 5) * hyper(12, m = 2, n = 2, k = 2))
q <- 1-p
g <- 190000000
c <- 2.5


(p * g) - (q * c)
## [1] -1.141286

Nous pouvons dire qu’en moyenne nous pouvons perdre 1.14 € par grille de 2.5 € jouée en visant le jackpot. Si nous voulions l’équité pour ce jeu, le coût de la grille devrait être de 1.358714.

L’espérance de gain pour le dernier rang est de :

p <-(hyper(50, m = 5, n = 5, k = 2) * hyper(12, m = 2, n = 2, k = 0))
q <- 1-p
g <- 4
c <- 2.5


(p * g) - (q * c)
## [1] -2.203187

Nous voici arrivés à la fin de cet article. J’espère que cela vous a permis, tout comme à moi, de mieux comprendre le calcul de probabilité. Si vous avez aimé, partagez ou laissez un commentaire.

Bonne chance !