Méthode de Monte Carlo en Python : simulation, pi et intégration

Méthode de Monte Carlo en Python : simulation, pi et intégration

La méthode de Monte Carlo consiste à résoudre ou approximer un problème en utilisant beaucoup de tirages aléatoires. Au lieu de calculer directement une formule parfois difficile, on simule un grand nombre de cas, puis on estime le résultat à partir des fréquences observées.

En Python, c’est une méthode très utile pour apprendre les probabilités, les simulations, l’intégration numérique, l’incertitude et la différence entre approximation et résultat exact.

Exemple classique : estimer pi en tirant des points au hasard dans un carré.

import random


def estimer_pi(n=100_000, seed=42):
    random.seed(seed)
    dans_le_cercle = 0

    for _ in range(n):
        x = random.random()
        y = random.random()

        if x * x + y * y <= 1:
            dans_le_cercle += 1

    return 4 * dans_le_cercle / n


print(estimer_pi())

Le résultat ne sera pas exactement 3.14159. Il s’en rapproche quand le nombre de tirages augmente.

La réponse courte

Monte Carlo suit ce schéma :

  1. générer des valeurs aléatoires ;
  2. évaluer une condition ou une fonction ;
  3. répéter beaucoup de fois ;
  4. approximer le résultat avec une moyenne ou une proportion.

Pour estimer une probabilité :

probabilite = cas_favorables / nombre_de_simulations

Pour estimer une intégrale sur [a, b] :

integrale ~= (b - a) * moyenne(f(x))

La méthode est simple, mais elle converge lentement : pour diviser l’erreur typique par 10, il faut souvent environ 100 fois plus d’échantillons.

Estimer pi avec Monte Carlo

Imaginez un carré de côté 1, contenant un quart de cercle de rayon 1.

Si l’on tire un point (x, y) au hasard dans le carré :

  • il est dans le quart de cercle si x² + y² <= 1 ;
  • la proportion de points dans le quart de cercle approche pi / 4.

Donc :

pi ~= 4 * proportion

Code :

import random


def estimer_pi(n, seed=None):
    rng = random.Random(seed)
    dans_le_cercle = 0

    for _ in range(n):
        x = rng.random()
        y = rng.random()

        if x * x + y * y <= 1:
            dans_le_cercle += 1

    return 4 * dans_le_cercle / n

Test :

for n in (100, 1_000, 10_000, 100_000):
    print(n, estimer_pi(n, seed=123))

Vous verrez que l’estimation devient globalement plus stable quand n augmente, mais elle reste aléatoire.

Pourquoi utiliser un seed

Un seed permet de reproduire une simulation.

print(estimer_pi(10_000, seed=123))
print(estimer_pi(10_000, seed=123))

Les deux appels donnent le même résultat. C’est très utile pour tester, déboguer et comparer deux versions d’un algorithme.

Sans seed, les tirages changent à chaque exécution.

Dans un article, un notebook ou un test automatisé, fixez souvent un seed. Dans une simulation de production, choisissez la stratégie de hasard selon le contexte.

Version NumPy plus rapide

Pour beaucoup de tirages, NumPy est plus efficace que des boucles Python.

import numpy as np


def estimer_pi_numpy(n=1_000_000, seed=42):
    rng = np.random.default_rng(seed)
    points = rng.random((n, 2))

    distances = points[:, 0] ** 2 + points[:, 1] ** 2
    dans_le_cercle = distances <= 1

    return 4 * dans_le_cercle.mean()


print(estimer_pi_numpy())

Ici, NumPy génère tous les points sous forme de tableau. Le calcul est vectorisé, donc beaucoup plus rapide pour de grandes tailles.

default_rng() est l’API moderne recommandée pour créer un générateur aléatoire NumPy.

Intégration numérique avec Monte Carlo

Monte Carlo peut aussi approximer une intégrale.

Prenons :

intégrale de 0 à 1 de x² dx

La valeur exacte est 1/3.

Avec Monte Carlo, on tire des x uniformément dans [0, 1], puis on calcule la moyenne de .

import random


def integrer_x_carre(n=100_000, seed=42):
    rng = random.Random(seed)
    total = 0

    for _ in range(n):
        x = rng.random()
        total += x * x

    return total / n


print(integrer_x_carre())

Pour un intervalle [a, b], on multiplie par la largeur de l’intervalle.

def integrer(f, a, b, n=100_000, seed=42):
    rng = random.Random(seed)
    total = 0

    for _ in range(n):
        x = rng.uniform(a, b)
        total += f(x)

    return (b - a) * total / n


print(integrer(lambda x: x * x, 0, 1))

Ce n’est pas la méthode la plus précise en dimension 1, mais elle devient intéressante quand le nombre de dimensions augmente ou quand le problème est difficile à traiter analytiquement.

Simuler une probabilité

Monte Carlo est souvent utilisé pour estimer une probabilité.

Exemple : quelle est la probabilité d’obtenir au moins un 6 en lançant trois dés ?

import random


def proba_au_moins_un_six(n=100_000, seed=42):
    rng = random.Random(seed)
    succes = 0

    for _ in range(n):
        des = [rng.randint(1, 6) for _ in range(3)]

        if 6 in des:
            succes += 1

    return succes / n


print(proba_au_moins_un_six())

La valeur théorique est :

1 - (5/6)^3 ~= 0,4213

La simulation doit s’en approcher.

Mesurer l’erreur

Une simulation Monte Carlo ne donne pas seulement un nombre. Elle donne une estimation avec une incertitude.

Pour observer la variabilité, répétez plusieurs simulations avec des seeds différents.

estimations = [
    estimer_pi(10_000, seed=seed)
    for seed in range(10)
]

print(estimations)
print(sum(estimations) / len(estimations))

Si les estimations varient beaucoup, il faut augmenter n, améliorer la méthode ou mieux contrôler la variance.

Une règle pratique : l’erreur typique baisse souvent comme :

1 / racine(n)

Cela veut dire que Monte Carlo est robuste, mais pas miraculeux.

Complexité

Si vous faites n simulations et que chaque simulation coûte O(1), la complexité est :

O(n)

La mémoire dépend de l’implémentation :

  • une boucle Python peut rester en O(1) ;
  • une version NumPy vectorisée stocke souvent les échantillons, donc peut utiliser O(n) mémoire.

La version NumPy est souvent plus rapide, mais elle peut consommer plus de RAM.

Erreurs fréquentes

Croire qu’un résultat Monte Carlo est exact

Monte Carlo donne une approximation. Il faut parler d’estimation, d’erreur, de variance et de nombre d’échantillons.

Utiliser trop peu de tirages

Avec n = 100, l’estimation de pi peut être très approximative. C’est normal. Monte Carlo a besoin de nombreux tirages.

Ne pas fixer de seed pendant les tests

Sans seed, un test peut réussir une fois et échouer ensuite. Pour les tests, fixez un seed.

Utiliser random pour de la cryptographie

Le module random est fait pour la simulation, pas pour la sécurité. Pour des secrets, utilisez secrets.

Vectoriser sans penser à la mémoire

Avec NumPy, générer 100_000_000 points en une fois peut saturer la mémoire. Travaillez par lots si nécessaire.

Version NumPy par lots

Pour de très grands nombres de tirages, on peut traiter par blocs.

import numpy as np


def estimer_pi_batches(n=10_000_000, batch_size=100_000, seed=42):
    rng = np.random.default_rng(seed)
    dans_le_cercle = 0
    total = 0

    while total < n:
        taille = min(batch_size, n - total)
        points = rng.random((taille, 2))
        distances = points[:, 0] ** 2 + points[:, 1] ** 2

        dans_le_cercle += np.count_nonzero(distances <= 1)
        total += taille

    return 4 * dans_le_cercle / total

Cette version garde les bénéfices de NumPy sans tout charger en mémoire.

Pour aller plus loin

Monte Carlo est une idée simple, mais très puissante : remplacer un calcul difficile par beaucoup d’essais aléatoires bien contrôlés. En Python, elle se combine naturellement avec random, NumPy, les tableaux, les statistiques et les visualisations.

À lire ensuite :

La règle à retenir : Monte Carlo est utile quand l’échantillonnage est plus simple que le calcul exact. Mais il faut toujours surveiller le nombre de tirages, la variance, le seed et le coût.

Références