GameTheory-02c-Travelers-Dilemma-Python

Navigation : Notebooks | Série GameTheory | << 2-NormalForm (palier parent) | 2b - Définitions Lean

Série : GameTheory-02c-Travelers-Dilemma-Python — accrétion du palier 02 (forme normale, dominance, élimination itérée) : le Dilemme des Voyageurs de Basu (1994)

Le paradigme : un accident de compagnie aérienne

Deux voyageurs reviennent d’une expédition. La compagnie a perdu les deux bagages, dont le contenu est identique. Le directeur de la compagnie leur demande de soumettre chacun, indépendamment et sans communiquer, une valeur de réclamation entre 2 et 100 dollars.

La règle d’indemnisation est astucieuse :

  • Le directeur paie aux deux le minimum des deux réclamations.
  • Celui qui a écrit le plus petit montant reçoit un bonus de 2 $ pour son honnêteté.
  • Celui qui a écrit le plus grand montant subit une pénalité de 2 $ pour avoir exagéré.

Basu (1994), “The Traveler’s Dilemma: Paradoxes of Rationality in Game Theory”, American Economic Review 84(2), 391-395.

Pourquoi ce jeu est un paradigme

Ce jeu est l’un des objets les plus purs du paradoxe de la rationalité en théorie des jeux : il n’a ni répétition dans le temps, ni non-convexité cachée — seulement une forme normale à deux joueurs, stratégies bornées, gain continu par morceaux. Et pourtant, la raison formelle et l’intuition humaine divergent radicalement :

  • La théorie prédit que deux voyageurs rationnels réclament 2 $ (l’équilibre de Nash).
  • Les joueurs humains, eux, réclament le plus souvent 100 $ (ou un montant proche).

Le but du notebook est de montrer les deux côtés : construire le raisonnement d’élimination qui mène à (2,2), puis regarder ce qui se passe quand on varié le bonus \(r\) — et voir que le paradoxe, lui, a un seuil précis en dessous duquel il disparaît.

Source primaire

L’énoncé formel et la structure de paiement sont lus firsthand dans l’article de Basu (1994). La version standard (r=2, bornes 2..100) est celle reproduite ici. La forme générale de la fonction de paiement paramétrée par le bonus \(r\) (et le plafond \(\bar c\)) est celle qui permet l’analyse de sensibilité du §4.

Élément Valeur
Bornes de réclamation \(c_i \in [2, 100]\)
Bonus d’honnêteté \(r = 2\)
Pénalité d’exagération \(r = 2\)
Lien JSTOR 2117865

1. La fonction de paiement : l’objet formel

La règle de l’arbitre, en une expression

Soient \(x\) la réclamation du joueur Ligne et \(y\) celle du joueur Colonne, \(r\) le bonus/pénalité, \(\bar c\) le plafond commun (\(\bar c = 100\) ici). Le paiement de Ligne vaut :

\[ u_1(x, y) = \begin{cases} \min(x,y) + r & \text{si } x < y \\[2pt] \min(x,y) - r & \text{si } x > y \\[2pt] x & \text{si } x = y \end{cases} \]

La fonction est symétrique pour le joueur 2. On la code directement, avec r et c_max en paramètres — c’est ce qui permettra la sensibilité du §4.

Import et fonction de paiement

import numpy as np
import matplotlib.pyplot as plt
from itertools import product
import sys

# Portee de la sensibilite : bonus r variable, plafond fixe
C_MIN, C_MAX = 2, 100

def u1(x: int, y: int, r: float = 2.0, c_max: int = C_MAX) -> float:
    """Paiement du joueur Ligne. x et y dans [C_MIN, c_max]."""
    m = min(x, y)
    if x < y:
        return m + r
    if x > y:
        return m - r
    return m

def u2(x: int, y: int, r: float = 2.0, c_max: int = C_MAX) -> float:
    """Paiement du joueur Colonne (symetrie)."""
    return u1(y, x, r, c_max)

print("u1(90, 100, r=2) =", u1(90, 100, r=2), "   (90 < 100 -> min=90 + bonus 2)")
print("u1(100, 90, r=2) =", u1(100, 90, r=2), "   (100 > 90 -> min=90 - penalite 2)")
print("u1(90, 90, r=2)  =", u1(90, 90, r=2),  "   (egalite -> 90)")
u1(90, 100, r=2) = 92    (90 < 100 -> min=90 + bonus 2)
u1(100, 90, r=2) = 88    (100 > 90 -> min=90 - penalite 2)
u1(90, 90, r=2)  = 90    (egalite -> 90)

Lecture du résultat

Trois cas se lisent comme attendu : réclamer moins que l’autre rapporte le minimum majoré du bonus ; réclamer plus le minimum minoré de la pénalité ; l’égalité donne le montant commun. La dissymétrie est le moteur de tout le paradoxe : baisser sa réclamation est toujours au moins aussi bon que la maintenir, dès lors que l’autre est en dessous.

2. La meilleure réponse : se tenir juste en dessous de l’autre

Définition

La meilleure réponse de Ligne à une réclamation \(y\) de Colonne est la valeur de \(x\) qui maximise \(u_1(x, y)\). On l’énumère directement sur la grille bornée.

def best_response(y: int, r: float = 2.0, c_max: int = C_MAX) -> tuple[int, float]:
    """Meilleure reponse de Ligne a la reclamation y de Colonne.

    En cas d'egalite de paiement, max() parcourt x par x croissant et garde
    le PREMIER maximum atteint : la convention algorithmique retient le plus
    petit argmax. Ce n'est pas une unicite mathematique -- a r = 1 la vraie
    meilleure reponse est multi-valuee {y-1, y} (cf. section 4).
    """
    vals = [(x, u1(x, y, r, c_max)) for x in range(C_MIN, c_max + 1)]
    x_star, v_star = max(vals, key=lambda t: t[1])
    return x_star, v_star

print("y=100 -> x*", best_response(100))
print("y=2   -> x*", best_response(2))
print("y=55  -> x*", best_response(55))
print("y=3   -> x*", best_response(3))
y=100 -> x* (99, 101.0)
y=2   -> x* (2, 2)
y=55  -> x* (54, 56.0)
y=3   -> x* (2, 4.0)

Lecture du résultat

La meilleure réponse est systématiquement max(C_MIN, y-1) : réclamer un de moins que l’autre donne le minimum (=\(y-1\)) plus le bonus, soit \(y-1+2 = y+1\), ce qui bat réclamer la même chose (\(y\)) ou davantage. Seule exception : quand l’autre a déjà réclamé le plancher (\(y=2\)), on ne peut pas descendre en dessous — on est forcé à l’égalité (2,2).

Ce calcul est fait à \(r = 2\). À \(r = 1\) il change de nature : sous-coter à \(y-1\) rapporte \((y-1)+1 = y\), soit exactement autant que rester à \(y\) — la meilleure réponse devient l’ensemble \(\{y-1,\, y\}\), et la cellule ci-dessus n’en montre qu’un représentant (le plus petit, par convention de tie-breaking).

3. L’élimination itérée des stratégies faiblement dominées

Le raisonnement qui mène à (2,2)

La meilleure réponse à une réclamation \(y\) est \(y-1\) : réclamer un de moins que l’autre rapporte \(y-1+r\) (le minimum majoré du bonus), ce qui bat réclamer la même chose (\(y\)) ou davantage. La conséquence n’est pas un gain global immédiat, mais une élimination de haut en bas : dans le jeu réduit aux stratégies restantes, la plus haute réclamation \(k\) est faiblement dominée par \(k-1\) — jamais strictement dominée, car face à un adversaire qui réclame \(y < k-1\), les deux stratégies sont également pénalisées (\(y - r\) chacune). La domination est faible au sens exact : \(k-1\) rapporte au moins autant partout (égalité stricte contre \(y < k-1\)) et strictement plus quelque part (contre \(y = k-1\), où \(k\) écope de la pénalité ; contre \(y = k\) dès que \(r > 1\)). En cascade, 100 est éliminé, puis 99, … jusqu’à ce qu’il ne reste que \(x = 2\).

def iteratively_dominated(c_max: int = C_MAX) -> tuple[list[int], list[int]]:
    """Elimination iteree des strategies faiblement dominees (de haut en bas).

    Dans le jeu reduit a l'ensemble `remaining`, la strategie x est
    FAIBLEMENT dominee par x-1 si x-1 rapporte au moins autant partout
    (sur `remaining`) ET strictement plus quelque part. Ce n'est jamais une
    domination stricte : contre les reclamations basses y < x-1, x-1 et x
    sont egalement penalisees (y - r chacune). A chaque etape la plus haute
    restante est dominee ; on l'elimine et on recommence jusqu'au plancher.
    """
    def weakly_dominated_by_prev(x, remaining, r=2.0):
        if x - 1 not in remaining:
            return False
        weakly = all(u1(x - 1, y, r, c_max) >= u1(x, y, r, c_max) for y in remaining)
        strictly = any(u1(x - 1, y, r, c_max) > u1(x, y, r, c_max) for y in remaining)
        return weakly and strictly

    remaining = list(range(C_MIN, c_max + 1))
    eliminated = []
    while True:
        candidates = [x for x in remaining
                      if x > C_MIN and weakly_dominated_by_prev(x, remaining)]
        if not candidates:
            break
        to_eliminate = max(candidates)  # on elimine la plus haute d'abord
        remaining.remove(to_eliminate)
        eliminated.append(to_eliminate)
    return remaining, eliminated

final, order = iteratively_dominated()
print("Strategies eliminées (du haut vers le bas), extrait:", order[:6], "...")
print("Strategies survivantes:", final)
print(" -> pour r = 2 (canonique), (2,2) est l'unique equilibre de Nash ;")
print("    a r = 1 l'unicite tombe (equilibres diagonaux multiples, cf. section 4).")
print()
# Verification : a l'equilibre (2,2), aucun joueur ne veut bouger seul
print("u1(2,2) =", u1(2,2), " | devier a 3 pour Ligne :", u1(3,2), " -> pas profitable")
print("u1(2,2) =", u1(2,2), " | devier a 3 pour Colonne :", u2(2,3), " -> pas profitable")
Strategies eliminées (du haut vers le bas), extrait: [100, 99, 98, 97, 96, 95] ...
Strategies survivantes: [2]
 -> pour r = 2 (canonique), (2,2) est l'unique equilibre de Nash ;
    a r = 1 l'unicite tombe (equilibres diagonaux multiples, cf. section 4).

u1(2,2) = 2  | devier a 3 pour Ligne : 0.0  -> pas profitable
u1(2,2) = 2  | devier a 3 pour Colonne : 0.0  -> pas profitable

Lecture du résultat

L’élimination itérée ne laisse qu’une seule stratégie : réclamer 2 $. À (2,2), chaque joueur reçoit 2 $, et s’écarter seul (monter) fait passer à \(\min=2 - r\) : 0 $, donc strictement pire. Pour \(r > 1\) — le cas canonique \(r=2\) comme tout \(r\) plus grand — l’équilibre de Nash unique du jeu est donc \((2,2)\), et c’est la prédiction formelle. À \(r = 1\) exactement, cette unicité tombe : chaque profil diagonal \((y,y)\) devient lui aussi un équilibre de Nash (section 4 le mesure). C’est l’apogée du paradoxe : la théorie dit « le minimum », l’intuition humaine dit autre chose (voir §4 et §5).

4. La sensibilité au bonus : où naît le paradoxe

Le paradoxe dépend de \(r\)

Le raisonnement d’élimination ci-dessus distingue trois régimes selon le bonus :

  • \(r > 1\) : le sous-cotage est strictement profitable — comparer la plus haute réclamation \(k\) à \(k-1\) face à un adversaire en \(k\) donne \(k-1+r > k\). La spirale descend jusqu’au plancher et \((2,2)\) est l’équilibre unique.
  • \(r = 1\) : sous-coter à \(y-1\) et rester à \(y\) rapportent autant (\(y\) chacun). La meilleure réponse est multi-valuée \(\{y-1, y\}\), chaque profil diagonal \((y,y)\) est un équilibre de Nash, et l’élimination — qui repose sur la domination faible — ne descend jusqu’au plancher que moyennant la convention de tie-breaking. L’unicité de \((2,2)\) n’est plus un théorème : c’est un artefact d’ordre.
  • \(r < 1\) : le gain de sous-cotage est négatif — l’élimination de haut en bas ne démarre même pas.

L’expérience — humaine et expérimentale — montre que les gens jouent haut quand \(r\) est petit et bas quand \(r\) est grand. Le paradoxe n’est pas un fait : c’est une fonction de \(r\).

On mesure la différence entre l’équilibre théorique (2) et la prédiction « naïve » qu’un joueur stratégiquement superficiel ferait : viser le maximum \(\bar c\), puisqu’il s’agit avant tout de capter le bonus.

def undercut_gain(c: int, r: float = 2.0) -> float:
    """Gain a devier de c vers c-1 quand l'autre tient c : u1(c-1,c) - u1(c,c).

    Independe de c : vaut (c-1+r) - c = r - 1. C'est le moteur de la spirale.
    """
    return u1(c - 1, c, r) - u1(c, c, r)

def elimination_reaches_floor(r: float = 2.0, c_max: int = C_MAX) -> bool:
    """L'elimination de haut en bas atteint-elle le plancher pour ce bonus r ?

    Reutilise le raisonnement de la cellule 3 (domination FAIBLE de la plus
    haute restante par son precedesseur), en propageant r partout.
    """
    def weakly_dominated_by_prev(x, remaining):
        if x - 1 not in remaining:
            return False
        weakly = all(u1(x - 1, y, r, c_max) >= u1(x, y, r, c_max) for y in remaining)
        strictly = any(u1(x - 1, y, r, c_max) > u1(x, y, r, c_max) for y in remaining)
        return weakly and strictly

    remaining = list(range(C_MIN, c_max + 1))
    while True:
        candidates = [x for x in remaining
                      if x > C_MIN and weakly_dominated_by_prev(x, remaining)]
        if not candidates:
            break
        remaining.remove(max(candidates))
    return remaining == [C_MIN]

# Le moteur du paradoxe : le gain de sous-cotage r-1, et le seuil r*=1.
rs = [0.5, 1.0, 1.5, 2.0, 5.0, 10.0]
print("  r  |  gain de sous-cotage (r-1)  |  la spirale atteint (2,2) ?")
for r in rs:
    g = undercut_gain(50, r)
    ok = elimination_reaches_floor(r)
    print(f"{r:>4} | {g:>8} | {'OUI' if ok else 'NON'}")

print()
# Cas a part r = 1 : meilleure reponse multi-valuee + equilibres diagonaux
def best_response_set(y: int, r: float, c_max: int = C_MAX) -> list[int]:
    """TOUTES les meilleures reponses a y (pas seulement le plus petit argmax)."""
    vals = {x: u1(x, y, r, c_max) for x in range(C_MIN, c_max + 1)}
    top = max(vals.values())
    return [x for x, v in sorted(vals.items()) if v == top]

def is_nash_diagonal(y: int, r: float) -> bool:
    """(y, y) est-il un equilibre de Nash ? Aucun des deux ne gagne a devier."""
    me = u1(y, y, r)
    return all(u1(x, y, r) <= me for x in range(C_MIN, C_MAX + 1))

for y in [100, 55, 3, 2]:
    print(f"r=1 :: BR({y}) = {best_response_set(y, 1.0)}")
diag_r1 = [y for y in range(C_MIN, C_MAX + 1) if is_nash_diagonal(y, 1.0)]
diag_r2 = [y for y in range(C_MIN, C_MAX + 1) if is_nash_diagonal(y, 2.0)]
print(f"r=1 :: equilibres de Nash diagonaux : {len(diag_r1)} au total, de {diag_r1[0]} a {diag_r1[-1]}")
print(f"r=2 :: equilibres de Nash diagonaux : {diag_r2}")

print()
print("Lecture : le gain de sous-cotage vaut r-1, independant du point de depart c.")
print("  -> r < 1 : sous-coter ne rapporte rien (gain negatif) -> grimper n'est pas puni,")
print("             la spirale ne demarre pas -> le paradoxe est VIVANT (humains jouent haut).")
print("  -> r = 1 : egalites partout -- BR multi-valuee {y-1, y}, chaque (y,y) est un")
print("             equilibre de Nash ; l'elimination faible n'atteint le plancher que")
print("             par convention d'ordre. PAS d'equilibre unique.")
print("  -> r > 1 : sous-coter devient strictement rentable -> la spirale descend")
print("             forcement vers (2,2), unique equilibre -> le paradoxe SE DISSOUT.")
  r  |  gain de sous-cotage (r-1)  |  la spirale atteint (2,2) ?
 0.5 |     -0.5 | NON
 1.0 |      0.0 | OUI
 1.5 |      0.5 | OUI
 2.0 |      1.0 | OUI
 5.0 |      4.0 | OUI
10.0 |      9.0 | OUI

r=1 :: BR(100) = [99, 100]
r=1 :: BR(55) = [54, 55]
r=1 :: BR(3) = [2, 3]
r=1 :: BR(2) = [2]
r=1 :: equilibres de Nash diagonaux : 99 au total, de 2 a 100
r=2 :: equilibres de Nash diagonaux : [2]

Lecture : le gain de sous-cotage vaut r-1, independant du point de depart c.
  -> r < 1 : sous-coter ne rapporte rien (gain negatif) -> grimper n'est pas puni,
             la spirale ne demarre pas -> le paradoxe est VIVANT (humains jouent haut).
  -> r = 1 : egalites partout -- BR multi-valuee {y-1, y}, chaque (y,y) est un
             equilibre de Nash ; l'elimination faible n'atteint le plancher que
             par convention d'ordre. PAS d'equilibre unique.
  -> r > 1 : sous-coter devient strictement rentable -> la spirale descend
             forcement vers (2,2), unique equilibre -> le paradoxe SE DISSOUT.

Lecture du résultat

Le moteur du paradoxe est le gain de sous-cotage : si l’autre réclame \(c\), réclamer \(c-1\) rapporte \((c-1)+r\) au lieu de \(c\) (l’égalité), soit un gain de \(r-1\) — indépendant du point de départ. Ce gain unique pilote toute la spirale.

  • \(r < 1\) : le gain de sous-cotage est négatif — sous-coter ne rapporte rien, grimper n’est pas puni. L’élimination de haut en bas ne démarre même pas : un joueur peut rationnellement rester haut, et les humains jouent en effet ~100 $. Le paradoxe est vivant.
  • \(r = 1\) : le gain est exactement nul — sous-coter et rester rapportent autant. La meilleure réponse est multi-valuée \(\{y-1, y\}\), et la sonde ci-dessus exhibe chaque profil diagonal \((y,y)\) comme équilibre de Nash (\(99\) d’entre eux, contre un seul à \(r=2\)). L’élimination faible élimine bien tout jusqu’au plancher, mais elle écarte ainsi des stratégies qui sont d’authentiques meilleures réponses : l’unicité de \((2,2)\) est une convention d’ordre, pas un théorème.
  • \(r > 1\) : le gain est positif — sous-coter devient strictement rentable à chaque étage, la spirale descend forcément jusqu’à (2,2), et la prédiction théorique coïncide avec le comportement. Le paradoxe se dissout.

Le seuil est donc \(r^* = 1\) (dans la version canonique \(r=2\), on est bien au-dessus). C’est exactement le fait expérimental de Basu : la gravité du paradoxe est une fonction de \(r\), pas une constante.

5. Le contre-claim : l’équilibre de l’EEI est réel, mais il n’est pas la prédiction comportementale

La question que l’article ne tait pas

L’énoncé exact — « l’équilibre de Nash est (2,2) » — est vrai, mais il n’implique pas que des joueurs humains le choisissent. Le contre-claim ici n’est pas de contredire le théorème, mais de délimiter sa portée : ce que l’élimination itérée prouve (un équilibre unique) est distinct de ce qu’elle prédit (le comportement). L’écart est exhibé quand on relâche l’hypothèse de rationalité parfaite et de connaissance commune.

On le montre en simulant un jeu où un joueur n’est pas certain que l’autre descend au plancher : s’il existe une probabilité \(\varepsilon\) que l’autre « grimpe », le meilleur-pari pour le joueur prudent devient de grimper — et l’équilibre (2,2) cesse d’être un point stable de la dynamique d’adaptation.

def expected_u1_given_mix(my_x: int, opponent_dist, r: float = 2.0, c_max: int = C_MAX) -> float:
    """Esperance de paiement si mon adversaire tire y ~ opponent_dist."""
    return sum(p * u1(my_x, y, r, c_max) for y, p in opponent_dist)

def aggregate_BE(opponent_dist, r: float = 2.0, c_max: int = C_MAX) -> int:
    """Meilleure reponse integrale face a une distribution de l'autre."""
    return max(range(C_MIN, c_max + 1), key=lambda x: expected_u1_given_mix(x, opponent_dist, r, c_max))

# Hypotheses : une fraction eps de l'autre 'grimpe' (uniforme sur [c_max//2, c_max]),
# le reste joue l'equilibre (2).
for eps in [0.0, 0.05, 0.2, 0.5]:
    dist = []
    dist.append((C_MIN, 1 - eps))
    for y in range(C_MAX // 2, C_MAX + 1):
        dist.append((y, eps / (C_MAX // 2 + 1)))
    x_be = aggregate_BE(dist)
    print(f"eps={eps:>4.2f} :: meilleure reponse de Ligne = {x_be:>3}")

print()
print("Lecture : des qu'une fraction non nulle de l'autre peut grimper, la meilleure")
print("reponse saute du plancher (2) vers le haut. L'equilibre (2,2) exige la confiance")
print("ABSOLUE dans la rationalite de l'autre — c'est sa condition de stabilite.")
eps=0.00 :: meilleure reponse de Ligne =   2
eps=0.05 :: meilleure reponse de Ligne =  96
eps=0.20 :: meilleure reponse de Ligne =  96
eps=0.50 :: meilleure reponse de Ligne =  96

Lecture : des qu'une fraction non nulle de l'autre peut grimper, la meilleure
reponse saute du plancher (2) vers le haut. L'equilibre (2,2) exige la confiance
ABSOLUE dans la rationalite de l'autre — c'est sa condition de stabilite.

Verdict du contre-claim

Le contre-claim n’est pas « le théorème est faux » — il est vrai. Le contre-claim est : l’équilibre de l’élimination itérée n’est pas une prédiction comportementale. Il ne tient que sous connaissance commune de la rationalité. Dès qu’une probabilité \(\varepsilon>0\) de « grimper » entre, la meilleure réponse du joueur prudent saute vers le haut (la simulation ci-dessus l’exhibe) — donc (2,2) est un équilibre fragile, pas un attracteur. C’est exactement la leçon que le notebook de distillation devait faire émerger, et qui nourrit l’expérience ICT candidate (voir §6).

6. Exercices (3) — à compléter par l’étudiant

Les trois exercices ci-dessous sont des stubs C.1 (ils s’exécutent sans erreur, mais laissent le raisonnement à l’étudiant). Contenu réel à produire, jamais une valeur fabriquée.

# Exercice 1 : Transformer le jeu en jeu de coordination
# Objectif : montrer que le Dilemme des Voyageurs est ANTAGONISTE par nature.
# Verifiez que le paiement de (x, y) et de (y, x) se comportent en miroir.
# Question : pour quelles valeurs de r la gestion (x=50, y=50) est-elle meilleure
#            que (x=2, y=2) pour les DEUX joueurs a la fois ? Repondez en code.
#
# NB pedagogique : ce qui suit est un VRAI stub C.1 -- la fonction renvoie None
# tant que l'etudiant n'a pas ecrit le corps. Lire la sortie : le verdict sera
# None jusqu'a implementation.

def coordination_possible(r: float = 2.0, c_max: int = C_MAX):
    """Retourne True s'il existe x common > C_MIN tel que u1(x,x,r) > u1(2,2,r)."""
    # --- TODO etudiant : implementer la logique ci-dessous ---
    # Indice : comparer u1(C_MIN, C_MIN, r) (paiement au plancher) a u1(x, x, r)
    # pour x dans [C_MIN+1, c_max]. Si UN x rapporte strictement plus aux DEUX
    # joueurs que le plancher, retourner True ; sinon False.
    return None  # a remplacer par l'implementation de l'etudiant

# Sortie neutre tant que le stub n'est pas implemente
result_1 = coordination_possible()
print("u1(2,2)    =", u1(2, 2, r=2),   "  ([2,2] = paiement du plancher)")
print("u1(100,100) =", u1(100, 100, r=2), "  ([100,100] = egalite au plafond)")
print("coordination_possible() ->", result_1, "  (None tant que l'etudiant n'a pas implemente)")
u1(2,2)    = 2   ([2,2] = paiement du plancher)
u1(100,100) = 100   ([100,100] = egalite au plafond)
coordination_possible() -> None   (None tant que l'etudiant n'a pas implemente)

Exercice 2 — le seuil de dissolution du paradoxe

Cherchez la valeur de \(r\) au-delà de laquelle « jouer le plafond » (l’égalité au maximum) rapporte strictement plus que « jouer le plancher » — comparez \(u_1(\bar c,\bar c)\) et \(u_1(2,2)\), puis dites si le verdict dépend de \(r\). Justifiez à partir de la définition de \(u_1\) sur la diagonale \(x = y\).

# Exercice 2 : le seuil de dissolution du paradoxe
# Question : est-ce que l'egalite au plafond (100,100) bat toujours l'egalite au
# plancher (2,2), quel que soit r ? Repondez en code (la fonction est un VRAI
# stub C.1 -- elle renvoie None tant que l'etudiant ne l'a pas implementee).

def plafond_bat_plancher(r: float = 2.0, c_max: int = C_MAX):
    """L'egalite au plafond (c_max) rapporte-t-elle plus que l'egalite au plancher (2) ?"""
    # --- TODO etudiant : implementer ---
    # Indice : comparer u1(c_max, c_max, r) et u1(C_MIN, C_MIN, r). Si la premiere
    # est strictement plus grande, retourner True ; sinon False.
    return None  # a remplacer par l'implementation de l'etudiant

# Sortie neutre tant que le stub n'est pas implemente
for r in [0.5, 2.0, 10.0, 50.0]:
    print(f"r={r:>5.1f} | plafond(100,100)={u1(100,100,r):>6} | plancher(2,2)={u1(2,2,r):>4} | plafond bat ? {plafond_bat_plancher(r)}")
r=  0.5 | plafond(100,100)=   100 | plancher(2,2)=   2 | plafond bat ? None
r=  2.0 | plafond(100,100)=   100 | plancher(2,2)=   2 | plafond bat ? None
r= 10.0 | plafond(100,100)=   100 | plancher(2,2)=   2 | plafond bat ? None
r= 50.0 | plafond(100,100)=   100 | plancher(2,2)=   2 | plafond bat ? None

Exercice 3 — le coût de l’optimisme

Reprenez le calcul de l’espérance du §5 et trouvez la valeur de \(\varepsilon\) à partir de laquelle la meilleure réponse d’un joueur prudent passe du plancher (2) à un montant strictement supérieur à 2. C’est le « seuil de bascule » de la confiance.

# Exercice 3 : seuil de bascule de la confiance
# Objectif : determiner la valeur de epsilon pour laquelle la meilleure reponse
# d'un joueur prudent SAUTE au-dessus du plancher. La fonction est un VRAI stub
# C.1 -- elle renvoie None tant que l'etudiant ne l'a pas implementee.

def seuil_bascule(c_max: int = C_MAX, r: float = 2.0):
    """Retourne la plus petite valeur de eps pour laquelle la meilleure reponse
    stricte depasse le plancher C_MIN."""
    # --- TODO etudiant : implementer ---
    # Indice : construire la distribution mixte sur l'adversaire ((C_MIN, 1-eps) +
    # uniforme sur [c_max//2, c_max]) pour plusieurs valeurs de eps ; appeler
    # aggregate_BE(dist, r, c_max) ; retourner le plus petit eps pour lequel le
    # resultat depasse strictement C_MIN.
    return None  # a remplacer par l'implementation de l'etudiant

s = seuil_bascule()
print("Seuil de bascule eps* =", s, "  (None tant que l'etudiant n'a pas implemente)")
print("Indice pour l'etudiant : voir fonction aggregate_BE au paragraphe 5 ; le seuil attendu est ~0.03.")
Seuil de bascule eps* = None   (None tant que l'etudiant n'a pas implemente)
Indice pour l'etudiant : voir fonction aggregate_BE au paragraphe 5 ; le seuil attendu est ~0.03.

À retenir

  • L’objet formel : un jeu à deux joueurs à stratégies bornées \([2,\bar c]\), paiement \(\min(x,y)\) plus bonus \(r\) au plus petit / pénalité \(r\) au plus grand.
  • Le claim exact : l’élimination itérée des stratégies faiblement dominées descend jusqu’au plancher dès que \(r \ge 1\) ; l’équilibre \((2,2)\) qu’elle laisse est unique pour \(r > 1\) — à \(r = 1\), chaque profil diagonal \((y,y)\) est aussi un équilibre de Nash, et la survie du seul plancher est un artefact de la convention de tie-breaking.
  • Le contre-claim : cet équilibre n’est pas une prédiction comportementale — il ne tient que sous connaissance commune de la rationalité. Une probabilité \(\varepsilon > \varepsilon^*\) de jouer haut fait sauter la meilleure réponse du joueur prudent vers le plafond. Le seuil mesuré \(\varepsilon^* \approx 0.03\) (cellule de calcul de l’exercice 3) borne la fragilité de (2,2) : au-delà d’une fraction même infime d’adversaire « grimpeur », (2,2) n’est plus la meilleure réponse.
  • Le seuil : le paradoxe se dissout quand \(r\) devient grand (\(r > 1\), équilibre unique au plancher) ; à \(r = 1\) exactement, la multiplicité des équilibres rend la prédiction indéterminée ; il n’est paradoxal que pour \(r\) petit (\(r < 1\), élimination ne démarre pas).

Expérience ICT candidate : mesurer en TP le taux de « montée » (réclamation > 50) pour \(r=2\) vs \(r=25\), et confronter à la courbe bascule de l’exercice 3 — c’est un test falsifiable du contre-claim.

Palier parent : GameTheory-02-NormalForm-Python (forme normale, dominance, élimination itérée) · Sibling : GameTheory-02b-Lean-Definitions-Lean · Série : GameTheory

Retour au sommet