Simulations probabilistes Tuto

Simulations probabilistes

Durée estimée
10 minutes
Votre progression

Créez un compte gratuit pour suivre votre avancement et reprendre où vous avez laissé.

Créer un compte

Simuler le hasard avec Python

Quand on jette une pièce ou un dé, on ne peut pas prévoir le résultat — mais on sait qu'à la longue, environ une fois sur deux on obtient pile, et environ une fois sur six on obtient un 1. Ce passage du « cas par cas imprévisible » à des fréquences stables est au cœur des probabilités. Et avec Python, on peut le voir se produire en direct, en simulant des milliers d'expériences en moins d'une seconde.

À la fin de cette fiche, tu sauras tirer un nombre au hasard, simuler un lancer de pièce ou de dé, calculer la fréquence d'un événement sur un grand nombre d'essais, observer la loi des grands nombres sur un graphique, estimer $\pi$ par la méthode de Monte-Carlo, et faire faire une marche aléatoire à un personnage.

Remarque

Comment lire cette fiche. Tu vas rencontrer deux types d'exercices à la suite des explications :

  • Complète le programme — 1 ou 2 mots à compléter, juste après chaque nouveau concept.
  • À toi de jouer — un petit programme à finir, en fin de section.

Chaque cellule s'exécute en cliquant sur Exécuter. Comme on tire des nombres au hasard, le résultat change à chaque exécution — c'est normal, et c'est même tout l'intérêt.

Tirer un nombre au hasard

Le module random de la bibliothèque standard fournit tout ce qu'il faut pour générer du hasard. Trois fonctions suffisent pour 90 % des situations :

  • random.random() renvoie un flottant tiré au hasard dans $[0, 1[$.
  • random.randint(a, b) renvoie un entier tiré au hasard dans $[a, b]$ (bornes incluses).
  • random.choice(liste) choisit un élément au hasard dans une liste.

            

Relance plusieurs fois la cellule : les valeurs changent à chaque exécution. C'est exactement ce qu'on attend du hasard.

Complète le programme

Tire un entier au hasard entre 1 et 100 (bornes incluses) et affiche-le :


        
Voir la solution
import random
print(random.randint(1, 100))

Complète le programme

On joue à pierre-feuille-ciseaux contre l'ordinateur. Fais-lui choisir une option au hasard :


        
Voir la solution
import random
print(random.choice(['pierre', 'feuille', 'ciseaux']))

À toi de jouer

Simule un lancer de deux dés à 6 faces et affiche les deux valeurs ainsi que leur somme.


        
Voir la solution
import random

de1 = random.randint(1, 6)
de2 = random.randint(1, 6)
print('Dé 1 :', de1)
print('Dé 2 :', de2)
print('Somme :', de1 + de2)

Remarque

Spécificité Python — un hasard qui n'en est pas vraiment. Les nombres renvoyés par random sont produits par un algorithme déterministe (un générateur pseudo-aléatoire). Si tu fixes la « graine » avec random.seed(42) avant tes tirages, tu obtiendras toujours exactement la même séquence d'un run à l'autre. C'est ce qui permet de reproduire une simulation pour la déboguer — un luxe que tu n'as pas avec un vrai dé.

Compter les succès sur un grand nombre d'essais

L'intérêt d'une simulation, c'est de répéter une expérience beaucoup de fois et de compter les cas favorables. La structure typique :

  • une variable de comptage initialisée à $0$
  • une boucle for qui répète l'expérience $N$ fois
  • à chaque tour, on incrémente le compteur si l'événement est réalisé
  • à la fin, la fréquence est `compteur / N`

            

Relance la cellule plusieurs fois : la fréquence reste très proche de $0{,}5$. C'est la loi des grands nombres : sur un grand nombre d'essais, la fréquence d'un événement s'approche de sa probabilité.

Complète le programme

Simule 10 000 lancers d'un dé à 6 faces et compte combien de fois on obtient un 6. Calcule la fréquence (elle doit être proche de $\dfrac{1}{6} \approx 0{,}167$).


        
Voir la solution
import random

N = 10000
six = 0
for _ in range(N):
    if random.randint(1, 6) == 6:
        six += 1

print('Fréquence du 6 :', six / N)

Complète le programme

Simule 10 000 lancers d'un dé et compte combien de fois on obtient un nombre pair. La fréquence doit être proche de $0{,}5$.


        
Voir la solution
import random

N = 10000
pairs = 0
for _ in range(N):
    if random.randint(1, 6) % 2 == 0:
        pairs += 1

print('Fréquence des pairs :', pairs / N)

À toi de jouer

On lance deux dés à 6 faces et on s'intéresse à l'événement « la somme vaut 7 ». La probabilité théorique est $\dfrac{6}{36} = \dfrac{1}{6}$. Vérifie-le sur 100 000 tirages.


        
Voir la solution
import random

N = 100000
succes = 0
for _ in range(N):
    if random.randint(1, 6) + random.randint(1, 6) == 7:
        succes += 1

print('Fréquence de la somme 7 :', succes / N)
print('Théorique :', 1 / 6)

Remarque

Spécificité Python — la variable _. Quand tu écris for _ in range(N):, le tiret bas est une convention qui signifie « je ne vais pas utiliser cette variable, je m'en sers juste pour répéter N fois ». Techniquement, _ est une variable comme une autre, mais aucun outil de relecture (linter) ne se plaindra de son non-usage. C'est un idiome très répandu en Python — bien plus rare en Java ou en C où l'on est obligé d'écrire for (int i = 0; i < N; i++) même si l'on ignore i.

Voir la loi des grands nombres se former

La fréquence devient stable quand $N$ grandit. Pour le voir, on enregistre la fréquence après chaque lancer et on la trace en fonction du nombre d'essais. La courbe doit converger visuellement vers $0{,}5$ pour une pièce équilibrée.


            

Au début, la courbe oscille fortement : avec 10 lancers, obtenir 7 piles est crédible et donne une fréquence de $0{,}7$. Mais dès que $N$ dépasse quelques milliers, la courbe se rapproche inexorablement de $0{,}5$ — c'est la loi des grands nombres en action.

Complète le programme

Reprends le code ci-dessus et adapte-le à un dé à 6 faces : trace la fréquence d'apparition du 6 en fonction du nombre de lancers. Elle doit converger vers $\dfrac{1}{6}$.


        
Voir la solution
import random
import matplotlib.pyplot as plt

N = 5000
six = 0
frequences = []
for k in range(1, N + 1):
    if random.randint(1, 6) == 6:
        six += 1
    frequences.append(six / k)

plt.plot(frequences)
plt.axhline(1 / 6, color='red', linestyle='--', label='1/6')
plt.legend()
plt.grid(True)
plt.show()

Complète le programme

On peut superposer plusieurs simulations pour voir que toutes finissent par converger. Lance 3 simulations indépendantes de 3000 lancers de pièce et trace les 3 courbes sur un même graphique.


        
Voir la solution
import random
import matplotlib.pyplot as plt

N = 3000
for run in range(3):
    piles = 0
    freq = []
    for k in range(1, N + 1):
        if random.choice(['pile', 'face']) == 'pile':
            piles += 1
        freq.append(piles / k)
    plt.plot(freq, label=f'simulation {run + 1}')

plt.axhline(0.5, color='black', linestyle='--')
plt.legend()
plt.grid(True)
plt.show()

À toi de jouer

Simule une pièce truquée qui tombe sur pile avec une probabilité $p = 0{,}3$. Astuce : tirer un flottant avec random.random() et tester s'il est inférieur à $0{,}3$. Trace la convergence sur 5000 lancers et vérifie que la courbe tend vers $0{,}3$.


            

Remarque

Spécificité Python — la convergence n'est pas magique. La courbe se rapproche de la probabilité, mais elle ne s'y stabilise jamais exactement : à $N = 10\,000$ lancers, l'écart typique entre fréquence et probabilité est de l'ordre de $\dfrac{1}{\sqrt{N}}$, soit $\approx 0{,}01$. Pour gagner un facteur 10 en précision, il faut multiplier $N$ par 100. C'est ce qu'on appelle la vitesse de convergence des simulations Monte-Carlo, et c'est leur principal défaut.

Histogramme : voir la distribution

Plutôt que de compter un seul événement, on peut s'intéresser à toute la distribution : combien de 1, de 2, de 3, etc. Pour cela, on stocke tous les résultats dans une liste et on demande à matplotlib de tracer un histogramme.


            

Les six barres sont à peu près de même hauteur ($\approx 1667$) : le dé est équilibré. Mais sur la somme de deux dés, la distribution n'est plus uniforme — on a beaucoup plus de 7 que de 2 ou de 12. C'est l'occasion de visualiser la forme triangulaire caractéristique :


            

Complète le programme

Trace l'histogramme de 20 000 tirages de random.random(). La distribution doit être à peu près uniforme sur $[0, 1[$ : toutes les barres à la même hauteur.


        
Voir la solution
import random
import matplotlib.pyplot as plt

N = 20000
valeurs = [random.random() for _ in range(N)]

plt.hist(valeurs, bins=20, edgecolor='black')
plt.title('Distribution uniforme')
plt.show()

Complète le programme

Trace l'histogramme de la somme de trois dés sur 50 000 tirages. La distribution n'est plus triangulaire mais commence à ressembler à une cloche.


        
Voir la solution
import random
import matplotlib.pyplot as plt

N = 50000
sommes = [random.randint(1, 6) + random.randint(1, 6) + random.randint(1, 6) for _ in range(N)]

plt.hist(sommes, bins=range(3, 20), edgecolor='black', align='left')
plt.title('Somme de 3 dés')
plt.show()

À toi de jouer

Simule 30 000 fois l'expérience suivante : on lance 10 pièces équilibrées et on compte le nombre de piles obtenu (entre 0 et 10). Trace l'histogramme. Tu dois obtenir une cloche centrée sur 5 — c'est la loi binomiale $\mathcal{B}(10\,;\,0{,}5)$.


            

Remarque

Spécificité Python — les compréhensions de liste pour simuler. L'expression [random.randint(1, 6) for _ in range(N)] crée toute la liste des résultats en une ligne. C'est concis, lisible, et plus rapide qu'un for explicite avec .append. Ce style « déclaratif » (« la liste de tous les tirages ») est très Pythonique. Java ou C imposeraient une boucle explicite avec un tableau pré-alloué.

Estimer $\pi$ par la méthode de Monte-Carlo

On peut estimer $\pi$ en lançant des fléchettes au hasard sur un carré contenant un quart de disque, puis en comptant la proportion qui tombe dans le quart de disque. C'est la méthode de Monte-Carlo, une des techniques numériques les plus utilisées en physique, finance et IA.

Le principe. Soit le carré $[0, 1] \times [0, 1]$ d'aire 1 et le quart de disque de centre $O$ et de rayon 1, contenu dans le carré, d'aire $\dfrac{\pi}{4}$. Si l'on tire un point $(x, y)$ au hasard dans le carré, la probabilité qu'il tombe dans le quart de disque vaut $\dfrac{\pi}{4}$. Donc, sur $N$ tirages, la fréquence des points dans le disque approche $\dfrac{\pi}{4}$, et $4 \times \text{fréquence}$ approche $\pi$.


            

Avec 100 000 tirages, on obtient typiquement $\pi \approx 3{,}14$ — exact à deux décimales. Pour visualiser, on peut colorer les points selon qu'ils tombent dans le disque ou pas :


            

Complète le programme

Reprends l'estimation de $\pi$ et compare-la à la valeur réelle pour $N = 1000$, puis $N = 100\,000$. Plus $N$ est grand, plus l'estimation est précise.


            

Complète le programme

La méthode marche pour n'importe quelle aire. Estime l'aire de la zone $\{(x, y) \in [0, 1]^2 \mid y \leqslant x^2\}$ (sous la parabole $y = x^2$). La valeur exacte est $\dfrac{1}{3}$.


            

À toi de jouer

Trace la convergence de l'estimation de $\pi$ : sur 5000 tirages, enregistre l'estimation après chaque tirage et trace la courbe. Ajoute une ligne horizontale rouge à $\pi$ pour comparer.


            

Remarque

Spécificité Python — Monte-Carlo, partout. La méthode que tu viens d'utiliser pour $\pi$ est universelle : elle permet d'estimer n'importe quelle aire, n'importe quelle intégrale, et plus généralement la valeur moyenne d'une variable aléatoire — y compris en dimension élevée où les méthodes classiques (rectangles, trapèzes) deviennent inutilisables. C'est l'outil de base de la simulation en physique des particules, en finance (pricing d'options) et dans tous les calculs bayésiens en IA.

Marche aléatoire

Imagine un personnage placé en $0$ sur une droite. À chaque pas, on tire pile ou face : pile $\to$ il avance d'un pas, face $\to$ il recule d'un pas. Où sera-t-il après 1000 pas ? La réponse est… on ne sait pas, mais on peut le simuler.


            

Relance la cellule plusieurs fois : la position finale fluctue énormément. En moyenne sur beaucoup de marches, elle vaut $0$, mais l'écart-type vaut $\sqrt{N}$ — on s'éloigne de l'origine, lentement mais sûrement, en $\sqrt{N}$.

Complète le programme

Modifie la marche aléatoire pour qu'elle ait une dérive : à chaque pas, probabilité $0{,}55$ d'avancer, $0{,}45$ de reculer. La trajectoire doit tendre à monter.


            

Complète le programme

Trace 5 marches aléatoires de 500 pas sur le même graphique. Elles partent toutes de 0, mais s'écartent rapidement.


            

À toi de jouer

Simule une marche aléatoire en 2D : à chaque pas, on choisit au hasard une direction parmi nord, sud, est, ouest. Trace la trajectoire (les positions $(x, y)$) sur 2000 pas avec plt.plot(xs, ys). Force le repère orthonormé pour ne pas déformer la marche.


        
Voir la solution
import random
import matplotlib.pyplot as plt

N = 2000
x, y = 0, 0
xs, ys = [0], [0]
for _ in range(N):
    direction = random.choice(['N', 'S', 'E', 'O'])
    if direction == 'N':
        y += 1
    elif direction == 'S':
        y -= 1
    elif direction == 'E':
        x += 1
    else:
        x -= 1
    xs.append(x)
    ys.append(y)

plt.plot(xs, ys)
plt.gca().set_aspect('equal')
plt.title('Marche aléatoire 2D')
plt.grid(True)
plt.show()

Remarque

Spécificité Python — un déballage discret. L'écriture x, y = 0, 0 affecte simultanément $0$ aux deux variables. Et xs.append(x) modifie la liste en place, sans créer de copie : on dit que les listes sont mutables. Cette mutabilité est très efficace en mémoire (pas de recopie à chaque pas) mais demande de la vigilance : deux variables qui pointent vers la même liste partagent toutes les modifications.

Pour aller plus loin

Reproductibilité avec random.seed. Pour qu'une simulation donne toujours le même résultat — utile en débogage ou pour partager une figure exacte avec un collègue — il suffit de fixer la graine du générateur en début de programme :


            

Les deux listes sont rigoureusement identiques. C'est une garantie qu'aucun générateur de hasard physique ne pourra t'offrir. En JavaScript, Math.random() ne supporte même pas le seeding standardisé — il faut passer par une bibliothèque tierce (`seedrandom`).

Tirage avec poids — random.choices. Pour tirer un élément d'une liste avec des probabilités inégales, random.choices (avec un « s ») accepte un argument weights et un nombre de tirages k. C'est exactement la bonne fonction pour simuler une variable aléatoire discrète définie par sa loi.


            

Lois continues : random.gauss et random.uniform. Le module random propose plusieurs lois standard. La plus utile : random.gauss(mu, sigma) qui tire selon la loi normale de moyenne $\mu$ et d'écart-type $\sigma$. Trace son histogramme : tu retrouves la fameuse cloche.


            

Vectorisation avec numpy.random. Pour des simulations massives (millions de tirages), la boucle Python devient lente. numpy.random fournit les mêmes fonctions, mais elles renvoient un tableau entier en une seule passe — souvent 50× plus rapide :


            

Simuler une chaîne de Markov. Une chaîne de Markov est un système qui passe d'un état à un autre selon des probabilités fixes. Exemple : la météo simplifiée. Si aujourd'hui il fait beau, demain $80\,\%$ de chances qu'il fasse encore beau ; s'il pleut, $60\,\%$ de chances qu'il pleuve encore. À long terme, quelle proportion de jours ensoleillés ?


            

La proportion stationnaire vaut théoriquement $\dfrac{2}{3}$. C'est ce genre de simulation qui sert à modéliser des files d'attente, la propagation d'épidémies, ou les algorithmes de PageRank.

À toi de jouer

Estime la probabilité que la somme de 4 dés dépasse 18, en utilisant numpy.random pour faire 1 million de tirages d'un coup. Bonus : essaie d'écrire la simulation sans aucune boucle Python (uniquement des opérations vectorisées numpy).


        
Voir la solution
import numpy as np

N = 1_000_000
des = np.random.randint(1, 7, size=(4, N))
sommes = des.sum(axis=0)
proba = np.mean(sommes > 18)
print('P(somme > 18) ≈', proba)

Ce que tu as appris

  • random.random(), random.randint(a, b) et random.choice(liste) couvrent l'essentiel des tirages au hasard.
  • Une simulation = une boucle for _ in range(N): qui répète l'expérience et incrémente un compteur.
  • La fréquence d'un événement sur un grand nombre d'essais s'approche de sa probabilité — c'est la loi des grands nombres, visualisable en traçant la fréquence cumulée.
  • plt.hist(donnees, bins=...) affiche l'histogramme d'une liste de résultats : la forme de la distribution apparaît au premier coup d'œil.
  • La méthode de Monte-Carlo estime une aire, une intégrale ou une probabilité en tirant des points au hasard et en comptant les cas favorables.
  • Une marche aléatoire se simule pas à pas, en empilant les positions dans une liste qu'on trace ensuite.
  • Pour des simulations massives, numpy.random vectorise et accélère 50× ; random.seed permet de reproduire une simulation à l'identique.

Et après…

Tu vas plonger dans la cryptographie avec Python — chiffrer un message avec César et Vigenère, manipuler les caractères et le modulo, et casser un code par analyse de fréquences.