Le paradoxe des grenouilles

lundi 5 janvier 2015
par  Alain BUSSER

Une grenouille effectue des sauts (toujours vers l’avant) de longueur comprise entre 0 et 1 mètre, avec une répartition uniforme sur [0 ;1] :

  • la distance moyenne parcourue en deux sauts est 1 mètre ;
  • le nombre moyen de sauts nécessaires pour dépasser 1 mètre n’est pourtant pas 2...

La première question porte sur un calcul d’espérance : L’espérance de la somme de deux variables aléatoires est la somme de leurs deux espérances, or celles-ci valant 1/2, l’espérance de la somme est bien 1.

Par contre la seconde question porte sur l’espérance de la variable aléatoire « nombre de termes tel que la somme dépasse 1 » qui est un temps d’attente discret et aléatoire : Il s’agirait déjà de connaître sa loi de probabilité !

Le fait que la grenouille continue à sauter tant qu’elle n’a pas atteint 1 mètre suggère une approche algorithmique du problème, qui se montrera particulièrement facile avec la géométrie dynamique scriptée. Et particulièrement JavaScript qui a une syntaxe concise pour les boucles à condition d’arrêt.

Avec CaRMetal

La propriété importante des boucles de JavaScript qui sera utilisée ici est que ce sont des boucles à condition d’arrêt bien que le nom « for » soit trompeur. En fait, c’est le cas aussi en C... La syntaxe d’une boucle « for » est donc formée de trois parties, séparées par des points-virgule :

  1. L’initialisation (ici, remettre la grenouille G en (0,0) et le compteur n à 0) ;
  2. La condition (pour continuer à boucler ; ici, l’abscisse de la grenouille G doit être inférieure à 1) ;
  3. La modification : Ici, faire sauter la grenouille G et incrémenter le compteur n

Du coup, le corps de la boucle peut être vide (si ça gêne on peut y mettre un Println(X(« G »)) pour vérifier que tout fonctionne normalement). La traduction en CaRScript devient alors

for(Move("G",0,0),n=0;X("G")<1;Move("G",X("G")+Math.random(),0),n++);
Println(n)

(il peut être intéressant de mettre, soit dans la partie « modification » soit dans le corps (ici vide) de la boucle, un Pause(2000) qui permette de voir ce qui se passe). Ce script ne fonctionne que sur une figure CaRMetal comprenant un point appelé G, puisque le mouvement de G ne peut s’effectuer que si G existe !

Détail de ce mini-script :

  1. Les deux phases de l’initialisation sont séparées par des virgules, il y a Move(« G »,0,0) qui remet la grenouille à l’origine du repère, et n=0 qui réinitialise le nombre de sauts (au début de l’expérience, la grenouille a effectué 0 saut !)
  2. La condition est la plus simple des trois parties, pour savoir si la grenouille est arrivée au terme de son épreuve on compare son abscisse à 1 avec X(« G »)<1 ;
  3. La modification est elle aussi formée de deux actions séparées par une virgule :
    1. Le saut de la grenouille, consistant à incrémenter aléatoirement son abscisse avec Move("G",X("G")+Math.random(),0) ;
    2. L’incrémentation du compteur avec n++

Après la boucle on peut afficher n, nombre de sauts, avec Println(n). L’exécution du script dans CaRMetal crée une fenêtre d’affichage comprenant un nombre entier qui, le plus souvent, est égal à 2 ou 3...

On peut faire encore plus court

En stockant la variable n dans l’ordonnée de G, on a le script suivant :

for(Move("G",0,0);X("G")<1;Move("G",X("G")+Math.random(),Y("G")+1));
Println(Y("G"));

L’exécution du script est intéressante, parce qu’on voit les mouvements du point G, bien que rapides, dans CaRMetal. La grenouille n’est visiblement pas fatiguée par ces sauts :

Du coup, on peut mettre la mini-boucle dans une boucle sur le numéro de l’expérience, et faire un tableau d’effectifs. L’algorithme utilisé est le suivant :

  • On crée un tableau effectifs avec effectifs = new Array() ;
  • On met des 0 dedans, avec une boucle, pour l’initialiser (sinon l’incrémentation ne peut pas se faire, JavaScript ne pouvant ajouter 1 qu’à des nombres et pas à « undefined »).
  • On boucle sur la variable experience (initialisation à 0, condidtion : experience doit être inférieure à 1000, et la modification est une incrémentation de experience) ; dans le corps de la boucle on met juste le script précédent et l’incrémentation de l’effectif de n ;
  • affichage du tableau d’effectifs :
effectifs=new Array();
for(n=0;n<=10;n++){
	effectifs[n]=0;
}
for(experience=0;experience<1000;experience++){
	for(Move("G",0,0),n=0;X("G")<1;Move("G",X("G")+Math.random(),0),n++);
	effectifs[n]++;
}
Println(effectifs);

On obtient ce genre de tableau d’effectifs :

0 sauts 1 sauts 2 sauts 3 sauts 4 sauts 5 sauts 6 sauts 7 sauts 8 sauts 9 sauts 10 sauts
0 fois 0 fois 511 fois 337 fois 111 fois 35 fois 6 fois 0 fois 0 fois 0 fois 0 fois

Le diagramme en bâtons typique [1] :

image/svg+xml 0 1 2 3 4 5 6 7 8 9 10

On constate que jamais la grenouille n’a réussi à parcourir un mètre sans un seul saut, mais non plus en un seul saut : La probabilité qu’une variable aléatoire uniforme soit égale à 1 est nulle.

Pour calculer la moyenne, cette modification convient :

effectifs=new Array();
for(n=0;n<=10;n++){
	effectifs[n]=0;
}
moyenne=0;
for(experience=0;experience<1000;experience++){
	for(Move("G",0,0),n=0;X("G")<1;Move("G",X("G")+Math.random(),0),n++);
	moyenne += n;
}
moyenne /= 1000;
Println(moyenne);

Voici le fichier CaRMetal contenant le point G et tous ces scripts :

Avec Sophus

Puisqu’il est question de mouvement de la grenouille, l’affectation de variables n’est pas nécessaire dans ce problème, il est donc possible de le traiter aussi en Sophus. Cette fois-ci, la variable grenouille est un nombre et plus un point (c’est l’abscisse du point précédent). Le script de base :

grenouille = new Variable 0
n = new Variable 0
1.tantQuePlusGrandQue grenouille, ->
    augmenter grenouille, de, Math.random()
    incrémenter n
montrer n

Pour avoir les effectifs, on peut créer un tableau dans lequel on met des variables Sophus initialement nulles, et on fait

effectifs = new Array()
for n in [0..10]
    effectifs[n] = new Variable 0
1000.foisFaire ->
    grenouille = new Variable 0
    n = new Variable 0
    1.tantQuePlusGrandQue grenouille, ->
        augmenter grenouille, de, Math.random()
        incrémenter n
    incrémenter effectifs[n]
montrer effectifs

Et pour calculer la moyenne, on additionne avant de diviser la somme par 1000 :

moyenne = new Variable 0
1000.foisFaire ->
    grenouille = new Variable 0
    n = new Variable 0
    1.tantQuePlusGrandQue grenouille, ->
        augmenter grenouille, de, Math.random()
        incrémenter n
    augmenter moyenne, de, n
diviser moyenne, par, 1000
montrer moyenne

Avec DGPad

L’avantage de DGPad est qu’on peut expérimenter directement dans le corps de cet article. Cependant la boucle « for » avec initialisation multiple n’y fonctionne pas. Pour faire l’expérience une fois, on crée, après le point G, un DGWidget où on place le script suivant :

§ name="Allez grenouille, allez !" 
function abscisse(p){
  return(Coords(p)[0]);
}
Move("G",0,0)
compteur = 0
while(abscisse("G")<1){
    compteur++
    Move("G",abscisse("G")+Math.random(),0)
}
Println(compteur)
§

On constate

  • que le tout est entre « § »
  • que la première ligne définit le bouton qui va exécuter le script (si on clique dessus) ;
  • que les points-virgule à la fin des lignes ne sont pas nécessaires.

La figure obtenue est celle-ci :

En fait la figure a été enrichie

Un objet « timer » est utilisé pour qu’on aie le temps de voir la grenouille bouger :

// Coordinates System :
SetCoords(74,143,200);


// Geometry :
G=Point("G",1.4735294828768486,0);


// Styles :
STL(G,"c:#0000b2;s:6;sn:true;f:30");
SetCoordsStyle("isAxis:true;isGrid:true;isOx:true;isOy:true;isLockOx:false;isLockOy:false;centerZoom:false;color:#111111;fontSize:18;axisWidth:1;gridWidth:0.1");
SetGeneralStyle("background-color:#FAFAFA");


// Texts :
Text("\u00a7 name=\"Allez grenouille, allez !\"\nfunction abscisse(p){\n  return(Coords(p)[0]);\n}\nfunction unSaut(){\n    compteur++\n    Move(\"G\",abscisse(\"G\")+Math.random(),0);\n    if(abscisse(\"G\")>1) {alert(compteur); timer.stop()}\n}\nvar timer = Timer(2000)\nMove(\"G\",0,0)\nvar compteur = 0\nfor (n=0;n<=10;n++){ timer.push(unSaut)}\ntimer.start()\n\u00a7",129,34,220,46,"c:rgba(0,0,178,0.18);s:3;r:15;p:4");

Pour faire 100 simulations, on modifie le script ainsi :

§ name="Faire 100 fois l'expérience" 
function abscisse(p){
  return(Coords(p)[0]);
}
effectifs = [0,0,0,0,0,0,0,0,0,0,0]
for(experience=0;experience<100;experience++){
    Move("G",0,0)
    compteur = 0
    while(abscisse("G")<1){
        compteur++
        Move("G",abscisse("G")+Math.random(),0)
    }
    effectifs[compteur]++
}
Println(effectifs)
§

On obtient alors cette figure :

On constate que, contrairement à CaRMetal, on ne peut pas mettre de Pause dans le script, mais aussi que les mouvements ne se font pas en temps réels, mais une fois pour toutes à la fin du script. Alors autant renoncer à l’interprétation géométrique et simplement incrémenter une variable grenouille, qui, comme dans le cas de Sophus ci-dessus, est numérique. La rapidité obtenue permet maintenant de répéter l’expérience 1000 fois, et de déplacer au bout de ces 1000 fois, les extrémités « B »+n d’un diagramme en bâtons (chaque ordonnée est le quotient de l’effectif par 100 pour avoir une échelle confortable) :

§ name="Faire 1000 fois l'expérience" 
effectifs = [0,0,0,0,0,0,0,0,0,0,0]
for(experience=0;experience<1000;experience++){
    grenouille=0
    compteur = 0
    while(grenouille<1){
        compteur++
        grenouille += Math.random()
    }
    effectifs[compteur]++
}
for(n=0;n<=10;n++){
    Move("B"+n,n,effectifs[n]/100)
}
§

Le script permet donc de modifier le diagramme en bâtons à chaque clic :

Le code de la figure

// Coordinates System :
SetCoords(74,143,20);


// Geometry :
A0=Point("A0",0,0);
B0=Point("B0",0,0);
A1=Point("A1",1,0);
B1=Point("B1",1,0);
A2=Point("A2",2,0);
B2=Point("B2",2,5.24);
A3=Point("A3",3,0);
B3=Point("B3",3,3.19);
A4=Point("A4",4,0);
B4=Point("B4",4,1.19);
A5=Point("A5",5,0);
B5=Point("B5",5,0.3200000000000003);
A6=Point("A6",6,0);
B6=Point("B6",6,0.05999999999999943);
A7=Point("A7",7,0);
B7=Point("B7",7,0);
A8=Point("A8",8,0);
B8=Point("B8",8,0);
A9=Point("A9",9,0);
B9=Point("B9",9,0);
A10=Point("A10",10,0);
B10=Point("B10",10,0);
S0=Segment("S0",A0,B0);
S1=Segment("S1",A1,B1);
S2=Segment("S2",A2,B2);
S3=Segment("S3",A3,B3);
S4=Segment("S4",A4,B4);
S5=Segment("S5",A5,B5);
S6=Segment("S6",A6,B6);
S7=Segment("S7",A7,B7);
S8=Segment("S8",A8,B8);
S9=Segment("S9",A9,B9);
S10=Segment("S10",A10,B10);


// Styles :
STL(A0,"c:#0000b2;h:1;s:6;sn:true;f:30");
STL(B0,"c:#0000b2;h:1;s:6;sn:true;f:30");
STL(A1,"c:#0000b2;h:1;s:6;sn:true;f:30");
STL(B1,"c:#0000b2;h:1;s:6;sn:true;f:30");
STL(A2,"c:#0000b2;h:1;s:6;sn:true;f:30");
STL(B2,"c:#0000b2;h:1;s:6;sn:true;f:30");
STL(A3,"c:#0000b2;h:1;s:6;sn:true;f:30");
STL(B3,"c:#0000b2;h:1;s:6;sn:true;f:30");
STL(A4,"c:#0000b2;h:1;s:6;sn:true;f:30");
STL(B4,"c:#0000b2;h:1;s:6;sn:true;f:30");
STL(A5,"c:#0000b2;h:1;s:6;sn:true;f:30");
STL(B5,"c:#0000b2;h:1;s:6;sn:true;f:30");
STL(A6,"c:#0000b2;h:1;s:6;sn:true;f:30");
STL(B6,"c:#0000b2;h:1;s:6;sn:true;f:30");
STL(A7,"c:#0000b2;h:1;s:6;sn:true;f:30");
STL(B7,"c:#0000b2;h:1;s:6;sn:true;f:30");
STL(A8,"c:#0000b2;h:1;s:6;sn:true;f:30");
STL(B8,"c:#0000b2;h:1;s:6;sn:true;f:30");
STL(A9,"c:#0000b2;h:1;s:6;sn:true;f:30");
STL(B9,"c:#0000b2;h:1;s:6;sn:true;f:30");
STL(A10,"c:#0000b2;h:1;s:6;sn:true;f:30");
STL(B10,"c:#0000b2;h:1;s:6;sn:true;f:30");
STL(S0,"c:#0000b2;s:6;f:24");
STL(S1,"c:#0000b2;s:6;f:24");
STL(S2,"c:#0000b2;s:6;f:24");
STL(S3,"c:#0000b2;s:6;f:24");
STL(S4,"c:#0000b2;s:6;f:24");
STL(S5,"c:#0000b2;s:6;f:24");
STL(S6,"c:#0000b2;s:6;f:24");
STL(S7,"c:#0000b2;s:6;f:24");
STL(S8,"c:#0000b2;s:6;f:24");
STL(S9,"c:#0000b2;s:6;f:24");
STL(S10,"c:#0000b2;s:6;f:24");
SetCoordsStyle("isAxis:true;isGrid:true;isOx:true;isOy:true;isLockOx:false;isLockOy:false;centerZoom:false;color:#111111;fontSize:18;axisWidth:1;gridWidth:0.1");
SetGeneralStyle("background-color:#FAFAFA");


// Texts :
Text("\u00a7 name=\"Faire 1000 fois l'exp\u00e9rience\" \neffectifs = [0,0,0,0,0,0,0,0,0,0,0]\nfor(experience=0;experience<1000;experience++){\n    grenouille=0\n    compteur = 0\n    while(grenouille<1){\n        compteur++\n        grenouille += Math.random()\n    }\n    effectifs[compteur]++\n}\nfor(n=0;n<=10;n++){\n    Move(\"B\"+n,n,effectifs[n]/100)\n}\n\u00a7",103,176,220,46,"c:rgba(0,0,178,0.18);s:3;r:15;p:4");

Maintenant on peut modifier le script pour qu’il affiche la moyenne, ou même les moyennes sur plusieurs répétitions de 1000. Le nouveau script dans le widget :

§ name="200 calculs de moyenne" 
for(m=1;m<=200;m++){
    moyenne = 0
    for(experience=0;experience<1000;experience++){
        grenouille=0
        compteur = 0
        while(grenouille<1){
            compteur++
            grenouille += Math.random()
        }
        moyenne += compteur
    }
    moyenne /= 1000
    p=Point("P"+m,m/10,moyenne)
    STL(p,"c:#0000b2;s:1;f:30")
}
§

Et l’effet produit :

En cliquant plusieurs fois sur le bouton, on accumule les expériences et il est clair que la valeur moyenne est supérieure à 2 : Environ 2,8.

Théorie et prolongements

La somme de n variables uniformes suit une loi Irwin-Hall de paramètre n ce qui fait que, par exemple, la probabilité que la grenouille soit toujours à moins d’un mètre au bout de 4 sauts, est l’image de 1 par la fonction de répartition de la loi de paramètre 4 : L’inverse de 4 ! selon la formule donnée sur wikipedia (suivre le lien ci-dessus : La somme ne porte que sur le terme k=0). De même

  • La probabilité que la grenouille dépasse le mètre au bout d’un saut est 0
  • La probabilité qu’elle dépasse le mètre au bout de 2 sauts est 1/2=1/(2×0 !)
  • La probabilité qu’elle dépasse le mètre au bout de 3 sauts est 1/6=1/(3×1 !)
  • La probabilité qu’elle dépasse le mètre au bout de 4 sauts est 1/24=1/(4×2 !)
  • etc

L’espérance est donc la somme de termes du type (n+2)/((n+2)×n !=1/n ! : La somme des inverses des factorielles, qui, c’est bien connu, vaut e, soit environ 2,718.

Et la loi de l’abscisse finale de la grenouille ? Est-elle uniforme entre 1 et 2 ? Pour le voir, on peut encore modifier le widget pour qu’il dessine un histogramme sur 1000 valeurs à chaque clic ; le nouveau widget contient ceci :

§ name="dessiner un histogramme" 
effectifs = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
for(experience=0;experience<1000;experience++){
    grenouille=0
    while(grenouille<1){
        grenouille += Math.random()
    }
    effectifs[Math.floor((grenouille-1)*20)]++
}
for(n=0;n<20;n++){
    a=Point("A"+n,1+n/20,0)
    STL(a,"c:#0000b2;h:1;s:6;f:30")
    b=Point("B"+n,1+n/20,effectifs[n]/100)
    STL(b,"c:#0000b2;h:1;s:6;f:30")
    c=Point("C"+n,1.05+n/20,effectifs[n]/100)
    STL(c,"c:#0000b2;h:1;s:6;f:30")
    d=Point("D"+n,1.05+n/20,0)
    STL(d,"c:#0000b2;h:1;s:6;f:30")
    p=Polygon("Poly1","_a,_b,_c,_d")
    STL(p,"c:#006633;o:0.2;s:2;f:30")
}
§

La figure permet de superposer plusieurs histogrammes en cliquant plusieurs fois :

Il est clair que la loi n’est pas uniforme, la grenouille ayant rarement l’occasion de faire un grand saut depuisune position légèrement inférieure à 1 mètre. Serait-ce le retour de la loi de Xenakis ?

Le source de la figure des histogrammes

// Coordinates System :
SetCoords(-148,242,200);


// Geometry :
G=Point("G",1.0984841174121973,0);


// Styles :
STL(G,"c:#0000b2;h:1;s:6;sn:true;f:30");
SetCoordsStyle("isAxis:true;isGrid:true;isOx:true;isOy:true;isLockOx:false;isLockOy:false;centerZoom:false;color:#111111;fontSize:18;axisWidth:1;gridWidth:0.1");
SetGeneralStyle("background-color:#FAFAFA");


// Texts :
Text("\u00a7 name=\"dessiner un histogramme\" \neffectifs = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]\nfor(experience=0;experience<1000;experience++){\n    grenouille=0\n    while(grenouille<1){\n        grenouille += Math.random()\n    }\n    effectifs[Math.floor((grenouille-1)*20)]++\n}\nfor(n=0;n<20;n++){\n    a=Point(\"A\"+n,1+n/20,0)\n    STL(a,\"c:#0000b2;h:1;s:6;f:30\")\n    b=Point(\"B\"+n,1+n/20,effectifs[n]/100)\n    STL(b,\"c:#0000b2;h:1;s:6;f:30\")\n    c=Point(\"C\"+n,1.05+n/20,effectifs[n]/100)\n    STL(c,\"c:#0000b2;h:1;s:6;f:30\")\n    d=Point(\"D\"+n,1.05+n/20,0)\n    STL(d,\"c:#0000b2;h:1;s:6;f:30\")\n    p=Polygon(\"Poly1\",\"_a,_b,_c,_d\")\n    STL(p,\"c:#006633;o:0.2;s:2;f:30\")\n}\n\u00a7",114,310,220,46,"c:rgba(118,0,18,0.18);s:3;r:15;p:4");

Apparemment la loi n’est pas affine, comme le montre cet histogramme fait sur un million d’expériences (la grenouille est maintenant épuisée) avec alcoffeethmique :

image/svg+xml

Le source de cet histogramme

Le code CoffeeScript suivant a été placé dans alcoffeethmique :

tableau=[]
for n in [0...1000000]
    x = 0
    x += alea() until x>1
    tableau.push x
histogramme tableau, 1, 2, 100, 20000



[1obtenu en copiant-collant le tableau d’effectifs depuis CaRMetal jusqu’à alcoffeethmique puis en exécutant diagrammeBatonsTrie effectifs


Commentaires

Logo de Dominique Bernard
vendredi 8 mai 2015 à 15h59 - par  Dominique Bernard

Bonjour,

J’avais déjà entendu parler de ce problème que je trouve très intéressant. Merci pour cet article !
Une petite coquille s’est glissée dans le calcul de la probabilité que la grenouille atteigne le mètre en trois sauts : il s’agit de 1/3 et non 1/6 comme indiqué...
Je reprends cette activité à un niveau très élémentaire pour des élèves de lycée avec pour objectif la simulation, l’estimation d’une probabilité et le calcul dans les cas de deux sauts et trois sauts (en term).