Une planche de Galton est un dispositif qui illustre la convergence d'une loi binomiale vers une loi normale.
Des clous sont plantés sur la partie supérieure de la planche, de telle sorte qu'une bille lâchée sur la planche passe soit à droite soit à gauche pour chaque rangée de clous. Dans la partie inférieure les billes sont rassemblées en fonction du nombre de passages à gauche et de passages à droite qu'elles ont fait.
Ainsi chaque colonne correspond à un résultat possible d'une expérience binomiale (en tant qu'une expérience de Bernoulli répétée) et on peut remarquer que la répartition des billes dans les cases approche la forme d'une courbe de Gauss :
L'objectif de ce billet est de montrer comment simuler dans un formulaire Tkinter le déplacement aléatoire des billes sur une planche de Galton, pour obtenir à la fin, en bas de la planche, une répartition des billes suivant approximativement une loi normale.
Note : si vous souhaitez avoir plus d'information sur le sujet je vous invite à consulter la page Wikipedia Planche de Galton.
II. Classe PlancheGalton
Un widget canevas est un objet du module Tkinter permettant de dessiner des formes géométriques (rectangles, lignes, disques, etc.) et de les manipuler (personnalisation, déplacement, suppression, etc.) sur une surface.
On souhaite dans notre cas dessiner une planche de Galton sur une zone d'affichage d'un formulaire en utilisant un objet Canvas. Pour cela, on va créer une classe fille PlancheGalton qui va hériter de l'ensemble des attributs et des méthodes de la classe mère Canvas :
On va donc commencer par ajouter en haut de notre module l'instruction :
Code Python : | Sélectionner tout |
from tkinter import Canvas
Notre classe comportera donc un constructeur, c'est à dire une méthode particulière __init__() dont le code est exécuté quand la classe est instanciée.
Elle va nous permettre de définir en particulier le nombre de niveaux ou de rangées de clous sur la planche de Galton au moment de la création de l'objet :
Code Python : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | class PlancheGalton(Canvas): # la classe hérite de tous les attributs et méthodes de Canvas def __init__(self, parent, width, height, background, nb_niveaux=24): # exécution de ma méthode __init__() de la classe Canvas Canvas.__init__(self, parent, width=width, height=height, background=background) # définit le nombre de rangées de clous ou de niveaux de la planche de Galton self.nb_niveaux = nb_niveaux # définit la hauteur des colonnes du bas self.hauteur_colonnes = nb_niveaux # dessine la planche de Galton avec nb_niveaux self.redessiner() |
Elle contient également une méthode pour dessiner les rangées de clous sur la planche et une autre pour afficher une bille à une position précise :
Code Python : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | class PlancheGalton(Canvas): # la classe hérite de tous les attributs et méthodes de Canvas ... def dessiner_bille(self, position_x, position_y, couleur): # largeur et hauteur du Canvas w = self.winfo_width() h = self.winfo_height() # valeurs mini et maxi sur le Canvas suivant x et y min_x = -self.nb_niveaux-1; max_x = self.nb_niveaux + 1.05 min_y = -0.05; max_y = self.nb_niveaux + self.hauteur_colonnes + 1 # fonction de conversion des x et des y par rapport à la largeur w et la hauteur h du Canvas tx = lambda x: w * (x - min_x)/(-min_x + max_x) ty = lambda y: h * (y - min_y)/(-min_y + max_y) # l'écart entre 2 clous horizontaux sur la planche de Galton vaut 2 # la hauteur entre 2 niveaux/rangée de clous vaut 1 # valeurs des coordonnées x et y x = position_x y = min_y + position_y + 1.5 # écart entre 2 rangée de clous ecart_y = 1 # définition du rayon du cercle en fonction de l'espace entre les clous donc du nombre de niveaux r = int(0.85*(ty(ecart_y))/2) # trace le cercle de rayon r et centré sur le point de coordonnées (tx(x), ty(y)) self.creer_cercle(tx(x), ty(y), r, fill=couleur, outline=couleur) ... |
Le module contenant notre classe est enregistré sous le même nom PlancheGalton.py.
III. Formulaire Tkinter
On va présenter maintenant les différents composants du formulaire, puis on va décrire le code permettant de simuler le déplacement aléatoire des billes sur le planche de Galton.
III-A. Composants du formulaire
Il comporte sur sa partie supérieure des boutons de commandes pour lancer, arrêter ou initialiser la simulation.
Deux zones de texte permettent en plus de définir le nombre de rangées de clous horizontales sur la planche de Galton, et le nombre de billes pris en compte lors de la simulation.
Enfin, le reste du formulaire contient le widget canevas construit à partir de la classe PlancheGalton et sur lequel on va simuler le déplacement aléatoire des billes.
On a donc besoin d'ajouter en haut du module du formulaire des lignes de code permettant d'importer Tkinter et notre classe PlancheGalton contenue dans le fichier PlancheGalton.py :
Code Python : | Sélectionner tout |
1 2 | import tkinter from PlancheGalton import PlancheGalton # on importe notre classe PlancheGalton à partir du fichier PlancheGalton.py |
Ensuite, les différents objets sont ajoutés dans le formulaire Tkinter au moment de sa création, c'est à dire quand sa méthode __init__() est executée :
Code Python : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | class SimulateurPlancheGalton(Tk): def __init__(self): # instantiation de la classe parente Tk.__init__(self) # conteneur vertical occupant tout le formulaire et destiné à contenir le widget à canvas verticalPane = PanedWindow( self, orient=VERTICAL ) # conteneur horizontal placé en haut du formulaire pour les différents contrôles du formulaire horizontalPane = PanedWindow( verticalPane, orient=HORIZONTAL ) # création des boutons de commande btnLancer=Button( horizontalPane, text="Lancer", command=self.btnLancerClicked, width = 10) btnArreter=Button( horizontalPane, text="Arreter", command=self.btnArreterClicked, width = 10) btnInit=Button( horizontalPane, text="Initialiser", command=self.btnInitClicked, width = 10) # création des labels et des zones de texte lblNbNiveaux = Label( horizontalPane, text = "Nombre de niveaux") self.txtNbNiveaux= Text(horizontalPane, height = 1, width = 11) lblNbBilles = Label( horizontalPane, text = "Nombre de billes") self.txtNbBilles = Text(horizontalPane, height = 1, width = 11) # création d'un bouton supplémentaire permettant de finaliser la simulation btnFinaliser=Button( horizontalPane, text="Finaliser", command=self.btnFinaliserClicked, width = 11) # ajout des contrôles au conteneur horizontal horizontalPane.add( btnLancer ) horizontalPane.add( btnArreter ) horizontalPane.add( btnInit ) horizontalPane.add( lblNbNiveaux ) horizontalPane.add( self.txtNbNiveaux ) horizontalPane.add( lblNbBilles ) horizontalPane.add( self.txtNbBilles ) horizontalPane.add( btnFinaliser ) # ajout du conteneur horizontal verticalPane.add( horizontalPane ) verticalPane.pack(expand=True, fill='both') # création de l'objet PlancheGalton self.planche_galton = PlancheGalton(verticalPane, 700, 700, "white", nb_niveaux=24) # ajout de l'objet au conteneur vertical verticalPane.add( self.planche_galton ) verticalPane.pack(expand=True, fill='both') # met à jour l'objet Canvas pour récupérer les bonnes valeurs de winfo_width() et winfo_height() self.planche_galton.update() self.planche_galton.redessiner() # définition du titre du formulaire self.title( "Planche de Galton V1.0" ) # saisie des valeurs par défaut dans les zones de texte txtNbNiveaux et txtNbBilles self.txtNbNiveaux.insert(END,self.planche_galton.nb_niveaux) self.txtNbBilles.insert(END,100) # état du simulateur mis par défaut sur "Désactivé" self.etat_simulateur = "Désactivé" |
Aperçu du formulaire :
III-B. Contrôle du simulateur
On associe ensuite du code Python aux boutons de commande permettant de lancer, d'arrêter ou d'initialiser le simulateur :
Code Python : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | def btnLancerClicked(self): if self.etat_simulateur == "Désactivé": # initialisation de la simulation self.init_simulation() # lancement de la simulation : état "Activé" self.etat_simulateur = "Activé" # exécute la méthode simuler() de l'objet self.simuler() def btnArreterClicked(self): # arrêt de la simulation : état "Interrompu" self.etat_simulateur = "Interrompu" def btnInitClicked(self): # initialisation de la simulation : état "Désactivé" self.etat_simulateur = "Désactivé" # initialise les paramètres de la simulation et redessine la planche de Galton self.init_simulation() |
III-C. Simulation du déplacement aléatoire des billes sur la planche
Dans cette partie, on va d'abord tirer au hasard les trajets des billes sur la planche, pour ensuite simuler la descente des billes à partir du tirage obtenu.
III-C-1. Tirage aléatoire des trajets des billes
Pour chaque bille, on parcourt les niveaux ou rangées de la planche de Galton, et pour chaque niveau on tire au hasard une valeur entre 0 et 1 :
- 0 : descente à gauche du clou ;
- 1 : descente à droite du clou.
On utilise pour cela la fonction choice du module random :
Code Python : | Sélectionner tout |
dep_x = random.choice([0,1])
On ajoute ensuite la valeur obtenue à une liste représentant le trajet de la bille :
Code Python : | Sélectionner tout |
trajet.append(dep_x)
Une fois la bille arrivée en bas de la planche, on obtient une liste de 0 et de 1 correspondant au parcours complet de la bille sur la planche :
[0, 0, 0, 0] : représente 4 descentes à gauche ;
[0, 0, 0, 1] : représente 3 descente à gauche et 1 à droite ;
[0, 0, 1, 1] : représente 2 descente à gauche et 2 à droite ;
etc..
Pour connaitre l'indice de la colonne finale dans laquelle tombe la bille, il suffit de faire la somme des 1 de la liste obtenue :
Code Python : | Sélectionner tout |
indice_colonne = sum(trajet)
[0, 0, 0, 0] -> 0
[0, 0, 0, 1] -> 1
[0, 0, 1, 1] -> 2
etc..
Note : cet indice de colonne représente également le nombre de cas favorables ou de succès dans un schéma de bernoulli pour lequel le nombre d'épreuves correspond au nombre de rangées de clous.
On donne maintenant la fonction complète qui effectue le tirage aléatoire des trajets des billes :
Code Python : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | def tirer_trajets(nb_niveaux, nb_billes): # initialise la liste des résultats avec des colonnes à 0 liste_resultats = [0]*(nb_niveaux+1) # initialise la liste des trajets des billes tirage_trajets=[] # initialise le générateur aléatoire random.seed() # parcours des indices des billes : 0, 1, 2, .., nb_billes-1 for indice_bille in range(nb_billes): # initialisation de la liste représentant le trajet aléatoire de la bille trajet = [] # parcours des indices des niveaux : 0, 1, 2, .., nb_niveaux-1 for indice_niveau in range(nb_niveaux): # on tire au hasard une valeur entre 0 et 1 (0 : descente vers la gauche du clou, 1 : descendre vers la droite) dep_x = random.choice([0,1]) # ajout du choix au trajet trajet.append(dep_x) # ajout du trajet à la liste tirage_trajets.append(trajet) # indice de la colonne finale égal à la somme des 0 et des 1 de la liste obtenue indice_colonne = sum(trajet) # incrémentation du nombre de billes dans la colonne liste_resultats[indice_colonne] += 1 # renvoi des listes obtenues return (tirage_trajets, liste_resultats) |
III-C-2. Simulation des trajets des billes à partir du tirage précédent
On dispose à ce stade de la liste des trajets des billes qui sont représentés par des séquences de 0 et de 1. Par exemple pour une planche à 4 niveaux on peut avoir [0, 1, 0, 1], [0, 1, 1, 1], etc.
Il ne reste donc plus qu'à dessiner les billes à leurs positions successives sur le canvas en parcourant les séquences de 0 et de 1.
On ajoute pour cela une méthode simuler() à la classe SimulateurPlancheGalton permettant de simuler le déplacement aléatoire des billes sur la planche de Galton :
Code Python : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | def simuler(self): # si une simulation est en cours if self.etat_simulateur == "Activé": en_cours = False # parcours des indices des billes for indice_bille in range(self.nb_billes): # position actuelle ou prochaine position de la bille : (position_x, position_y) position_x, position_y = self.positions_billes[indice_bille][0], self.positions_billes[indice_bille][1] # si la bille est à cette position if self.tableau_etats[position_y][position_x] == indice_bille: position_suiv_x = position_x if position_y < self.planche_galton.nb_niveaux: dep_x = self.tirage_trajets[indice_bille][position_y] # dep_x = random.choice([0,1]) pour une simulation en temps réel if dep_x==0: # si le caractère de descente vers la gauche est choisi position_suiv_x = position_x - 1 # déplacement vers la gauche else: # sinon position_suiv_x = position_x + 1 # déplacement vers la droite position_suiv_y = position_y + 1 # déplacement vers le bas # si pas de bille à la position (position_suiv_x, position_suiv_y) if self.tableau_etats[position_suiv_y][position_suiv_x] is None: en_cours = True # efface la bille à la position de coordonnées (position_x, position_y) self.planche_galton.dessiner_bille(position_x, position_y, "white") # met l'état de la position dans le tableau à None self.tableau_etats[position_y][position_x] = None # affiche une bille bleu à la position de coordonnées (position_suiv_x, position_suiv_y) self.planche_galton.dessiner_bille(position_suiv_x, position_suiv_y, "blue") self.positions_billes[indice_bille] = (position_suiv_x, position_suiv_y) if position_suiv_y<(self.planche_galton.nb_niveaux + self.planche_galton.hauteur_colonnes - 1): self.tableau_etats[position_suiv_y][position_suiv_x] = indice_bille else: self.tableau_etats[position_suiv_y][position_suiv_x] = -1 # sinon, si pas de bille à la position de coordonnées (position_x, position_y) elif self.tableau_etats[position_y][position_x] is None and (position_y==0): en_cours = True # on positionne la bille à cet emplacement self.tableau_etats[position_y][position_x] = indice_bille self.positions_billes[indice_bille] = (position_x, position_y) self.planche_galton.dessiner_bille(position_x, position_y, "blue") # si au moins une bille est toujours en progression sur la planche de Galton if en_cours: self.after(250, self.simuler) # else: self.etat_simulateur = "Désactivé" self.finaliser_simulation() # finalise la simulation |
Cette méthode est exécutée toutes les 250 millisecondes grâce à la commande :
Code Python : | Sélectionner tout |
self.after(250, self.simuler)
Note : quand le nombre de billes sur la planche devient important, il se produit un ralentissement de la procédure, c'est pourquoi on a prévu une fonction permettant de finaliser la simulation et ainsi d'obtenir directement le résultat dans les colonnes du bas :
Code Python : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | def finaliser_simulation(self): # finalisation de la descente des billes sur la planche de Galton # comparaison entre la répartition des billes obtenue dans les colonnes du bas de la plannche et une répartition suivant une loi normale # nombre de colonnes du bas et dernière ligne de la planche de Galton nb_colonnes = self.planche_galton.nb_niveaux + 1 position_max_y = self.planche_galton.nb_niveaux + self.planche_galton.hauteur_colonnes # moyenne et écart-type de la loi normale m = self.planche_galton.nb_niveaux/2.0; s = pow(self.planche_galton.nb_niveaux,0.5)/2.0 # parcours des indices de colonne : 0, 1, 2, .., nb_colonnes-1 for indice_colonne in range(nb_colonnes): # position par rapport à l'axe x position_x = 2*indice_colonne - self.planche_galton.nb_niveaux # évaluation du nombre de billes attendu dans la colonne repérée par indice_colonne : # nombre approximatif de billes évalué à l'aide de la fonction de répartition de la loi normale nb_billes_col = (stats.norm.cdf(indice_colonne+1-0.5, loc=m, scale=s) - stats.norm.cdf(indice_colonne-0.5, loc=m, scale=s))*self.nb_billes # position supérieure et inférieure du rectangle représentant le nombre de billes donné par la fonction de répartition de la loi normale position_y1 = position_max_y - nb_billes_col position_y2 = position_max_y # dessine un rectangle de coordonnées (position_x-0.8, position_y1, position_x+0.8, position_y2) : hauteur donnée par la fonction de répartition de la loi normale self.planche_galton.dessiner_rectangle(position_x-0.8, position_y1, position_x+0.8, position_y2, "orange") position_min_y = position_max_y - self.resultats[indice_colonne] # parcours des positions suivant l'axe y for position_y in range(position_min_y, position_max_y): # dessine la bille à la position de coordonnées (position_x, position_y) self.planche_galton.dessiner_bille(position_x, position_y, "blue") # traçage de la courbe de Gauss # parcours des indices de colonne : 0, 1, 2, .., nb_colonnes-1 for indice_colonne in range(nb_colonnes): # traçage de la courbe sur l'intervalle [a, b] a = indice_colonne-0.5 ; b=indice_colonne+1-0.5 # pas de subdivision pas = (b-a)/40 # parcours des subdivisions de l'intervalle [a,b] for i in range(40): # calcul des coordonnées de x et y x = a + i*pas # valeur de y obtenue à l'aide de la fonction de densité de la loi normale y = stats.norm.pdf(x, loc=m, scale=s)*self.nb_billes # positions correspondantes sur le Canvas position_x = 2*x - self.planche_galton.nb_niveaux position_y = position_max_y - y # dessine un point rouge de coordonnées (position_x, position_y) self.planche_galton.dessiner_point(position_x, position_y, "red") |
Il s'agit donc d'une simulation pré-enregistrée, mais vous pouvez également changer dans la fonction simuler() la commande :
Code Python : | Sélectionner tout |
dep_x = self.tirage_trajets[indice_bille][position_y]
Code Python : | Sélectionner tout |
dep_x = random.choice([0,1])
Pour obtenir ainsi une simulation en temps-réel.
IV. Modules de test
On a donc besoin de 2 modules pour réaliser cette simulation :
- PlancheGalton.py est utilisé pour dessiner la planche de Galton sur une zone d'affichage ;
- SimulateurPlancheGalton.py permet de simuler dans un formulaire Tkinter la descente aléatoire des billes sur une planche de Galton.
Si vous le souhaitez, vous pouvez télécharger le dossier complet pour effectuer les tests :
On teste maintenant le simulateur en choisissant par exemple 24 rangées de clous pour la planche de Galton, et 100 billes en entrée.
Après avoir lancé la simulation, on peut visualiser la descente aléatoire des billes sur la planche de Galton :
A la fin, on obtient une répartition des billes suivant approximativement une loi normale :
V. Conclusion
Après avoir créé la classe PlancheGalton, nous avons pu l'utiliser pour simuler sur un formulaire Tkinter une planche de Galton.
Chacun pourra ensuite librement personnaliser ou améliorer la simulation en ajoutant par exemple un léger effet de rebond des billes sur les clous.
Sources :
https://fr.wikipedia.org/wiki/Planche_de_Galton
https://fr.wikipedia.org/wiki/%C3%89...a_de_Bernoulli
https://docs.python.org/fr/3.11/library/tkinter.html
https://www.tutorialspoint.com/python/tk_canvas.htm
https://www.mathweb.fr/euclide/simul...ton-en-python/
Téléchargement :