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 :
- générer des valeurs aléatoires ;
- évaluer une condition ou une fonction ;
- répéter beaucoup de fois ;
- 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 x².
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 :
- np.pad / numpy.pad : ajouter du padding à un tableau NumPy
- Complexité algorithmique en Python : comprendre O(n), O(log n) et O(n²)
- Minimax en Python : comprendre l’algorithme avec un exemple de jeu
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
- Documentation Python officielle :
random, générer des nombres pseudo-aléatoires - Documentation NumPy :
numpy.random.Generator - Britannica : Monte Carlo method

