Was du hier lernst
- Simulation statt Formel
- Zufallszahlen mit random
- Mengen (set)
- Klassen mit Zählern
- Gesetz der großen Zahlen
- Diagramm selbst zeichnen
So löst die Simulation das Rätsel
Das Ziegenproblem stammt aus der amerikanischen Spielshow »Let's Make a Deal«. Drei Tore, hinter einem steht ein Auto, hinter den beiden anderen je eine Ziege. Du wählst ein Tor. Der Moderator, der weiß, wo das Auto steht, öffnet eines der beiden anderen – immer eines mit Ziege. Dann darfst du wechseln. Solltest du?
Fast jeder denkt: Zwei Tore sind noch zu, also steht es 50 zu 50, Wechseln bringt nichts. Als die Kolumnistin Marilyn vos Savant 1990 das Gegenteil schrieb, bekam sie Tausende empörte Briefe, darunter von Mathematikern. Statt zu streiten, lässt das Programm zwei Spieler je 1000 Runden spielen und zählt einfach mit.
1. Ein Spieler als Klasse
Jeder Spieler merkt sich zwei Dinge: das Tor, auf das er gerade tippt, und wie oft er schon gewonnen hat. Dazu kommen drei Methoden:
def newGuess(self):
self.guess = random.randint(1, 3)
def saveResult(self, winning):
if winning == self.guess:
self.wins += 1
newGuess wählt zu Beginn jeder Runde zufällig ein Tor, saveResult zählt einen Sieg, wenn das Auto hinter dem gewählten Tor steht. player1 wechselt nach dem Öffnen immer, player2 bleibt immer bei seiner ersten Wahl.
2. Der Moderator ist die Hauptschleife
Für den Moderator gibt es keine eigene Klasse, seine Arbeit steht direkt in der Schleife. Erst wird das Auto versteckt, dann sucht er ein Tor zum Öffnen:
winning = random.randint(1, 3)
allNumbers = set((1, 2, 3))
numbersToRemove = set((winning, player1.guess))
allNumbers -= numbersToRemove
knownNumber = allNumbers.pop()
Das ist Mengenlehre in Python. allNumbers enthält alle Tore, davon werden das Tor mit dem Auto und das Tor des Spielers abgezogen. Was übrig bleibt, darf der Moderator öffnen: ein Tor, wenn der Spieler danebenlag, zwei, wenn er richtig lag. pop() nimmt eines davon heraus.
3. Wechseln mit Mengen
changeMind nutzt denselben Trick. Von den drei Toren fallen das eigene und das geöffnete weg – genau ein Tor bleibt übrig, und das ist die neue Wahl:
def changeMind(self, knownNumber):
allNumbers = set((1, 2, 3))
allNumbers.remove(self.guess)
allNumbers.remove(knownNumber)
self.guess = allNumbers.pop()
Der Moderator richtet sich nur nach player1. Das ist in Ordnung, denn player2 wechselt ohnehin nie: Für ihn spielt es keine Rolle, welches Tor geöffnet wird.
4. Ein Diagramm, das mitwächst
Nach jeder Runde berechnet das Programm die Gewinnquote beider Spieler, also Siege geteilt durch gespielte Runden. toScreen macht daraus einen Punkt: Die Rundenzahl bestimmt, wie weit rechts er liegt, die Quote, wie hoch. Alle fünf Runden wird das ganze Diagramm neu gezeichnet, mit Gitter, gestrichelten Linien bei zwei Dritteln und einem Drittel und den beiden Kurven:
screen.lines(switchPoints, SWITCH, 3)
screen.lines(stayPoints, STAY, 3)
Am Anfang springen die Kurven wild hin und her – nach wenigen Runden kann alles passieren. Mit jeder weiteren Runde werden die Ausschläge kleiner, und die Kurven nähern sich den gestrichelten Linien. Das ist das Gesetz der großen Zahlen bei der Arbeit.
5. Warum zwei Drittel?
Die Simulation beweist nichts, aber sie zeigt, wohin die Reise geht. Die Erklärung ist einfacher als gedacht. Mit einem Drittel Wahrscheinlichkeit liegst du mit deiner ersten Wahl richtig – dann verlierst du durch das Wechseln. In zwei Dritteln der Fälle liegst du falsch. Dann stehen hinter den beiden anderen Toren das Auto und eine Ziege, der Moderator muss die Ziege zeigen, und das verbleibende Tor ist garantiert das Auto. Wechseln gewinnt also genau dann, wenn die erste Wahl falsch war: in zwei von drei Fällen.
Das Wissen des Moderators
Der Knackpunkt ist, dass der Moderator das Auto kennt und es nie zeigt. Sein Öffnen ist keine Zufallsinformation, sondern ein Hinweis. Würde er ahnungslos irgendein Tor öffnen, wäre es tatsächlich 50 zu 50 – das kannst du unter »Probier mal« ausprobieren.
6. Jetzt du
Nach der Simulation kannst du selbst spielen: Tippe ein Tor an, der Moderator öffnet eine Ziege, dann entscheidest du dich durch einen zweiten Tipp. Unten zählt das Programm getrennt, wie oft du mit Wechseln und wie oft mit Bleiben gewonnen hast.
Ehrlich zählen
Spiel zehn Runden, in denen du immer wechselst, und zehn, in denen du immer bleibst. Bei so wenigen Runden kann der Zufall dich noch täuschen – aber auf Dauer gewinnt Wechseln etwa doppelt so oft.
Weiterlesen: der Satz von Bayes
Dass sich die Chancen ändern, wenn der Moderator eine Tür öffnet, lässt sich auch ohne Simulation ausrechnen – mit dem Satz von Bayes: Neue Information verschiebt die Wahrscheinlichkeiten. Eine Einführung in Prior, Likelihood und Posterior findest du bei smartlytics.de.
Probier mal
Jede Variante ändert den Code oben. Ein Klick auf »Ausprobieren« übernimmt die Änderung in den Editor und startet das Programm. »Zurücksetzen« holt das Original zurück.
-
01Zehnmal so viele Runden
Mit 10 000 Runden liegen die Kurven am Ende fast genau auf den gestrichelten Linien. Je mehr Versuche, desto näher kommt die gemessene Quote der wahren Wahrscheinlichkeit – das Gesetz der großen Zahlen.
- rounds = 1000 # + rounds = 10000 # - roundsPerFrame = 5 # + roundsPerFrame = 50 # -
02Nur 30 Runden – in Zeitlupe
Mit wenigen Runden ist der Zufall stärker als die Wahrscheinlichkeit. Starte die Variante mehrmals: Manchmal liegt sogar »bleiben« vorn. Genau deshalb braucht eine Simulation viele Durchläufe, bevor man ihr trauen kann.
- rounds = 1000 # + rounds = 30 # - roundsPerFrame = 5 # + roundsPerFrame = 1 # - await screen.frame(60) + await screen.frame(6) -
03Der ahnungslose Moderator
Was, wenn der Moderator nicht weiß, wo das Auto steht, und einfach irgendein anderes Tor öffnet? Deckt er dabei das Auto auf, zählt die Runde nicht. Beide Spieler starten jetzt mit demselben Tor – und plötzlich ist es wirklich egal: Beide Kurven laufen auf 50 Prozent zu. Das Wissen des Moderators macht den Unterschied.
- allNumbers = set((1, 2, 3)) - numbersToRemove = set((winning, player1.guess)) - allNumbers -= numbersToRemove - knownNumber = allNumbers.pop() + player2.guess = player1.guess # beide starten mit demselben Tor + # Der Moderator weiß nicht, wo das Auto steht: + knownNumber = random.choice([n for n in (1, 2, 3) if n != player1.guess]) + if knownNumber == winning: + continue # Auto aufgedeckt: Diese Runde zählt nicht -
04Ein Moderator, der würfelt
Liegt der Kandidat richtig, stehen hinter beiden anderen Toren Ziegen.
allNumbers.pop()nimmt dann irgendein Element der Menge – welches, legt Python nicht fest, bei kleinen Zahlen ist es in der Praxis immer das kleinste. Mitrandom.choiceentscheidet der Moderator wirklich zufällig. Am Ergebnis ändert das nichts: Für die Gewinnchance ist egal, welche Ziege er zeigt.- knownNumber = allNumbers.pop() + knownNumber = random.choice(sorted(allNumbers))
Vom Buch in den Browser
Das Original aus dem Buch ist ein reines Konsolenprogramm: Es spielt 1000 Runden und gibt am Ende zwei Zeilen aus. Die Klasse Player und die Schleife mit dem Moderator sind hier unverändert übernommen, die beiden Ausgaben am Ende ebenfalls. Neu sind das Diagramm, das während der Simulation mitwächst, und das Spiel zum Selbstausprobieren.
| Im Buch | Hier |
|---|---|
if __name__=='__main__': mit der Schleife |
Funktion simulate(), die zwischendurch zeichnet |
print "player 1 won ", player1.wins, " times" |
print("player 1 won ", player1.wins, " times") |
| Ergebnis nur als Zahl | zusätzlich als Diagramm und in Prozent |
| – | selbst spielen mit Klick oder Fingertipp |
Damit die Kurven entstehen, während das Programm rechnet, steckt die Schleife in einer async-Funktion. Alle fünf Runden zeichnet sie das Diagramm und gibt dem Browser mit await screen.frame(60) Gelegenheit, es anzuzeigen. Ohne diese Pause wären die 1000 Runden in einem Wimpernschlag vorbei, und du sähest nur das Endergebnis.
Python 3 ändert am Kern des Programms nichts, außer dass print jetzt eine Funktion ist. Die Mengenoperationen mit set, remove, -= und pop funktionierten schon in Python 2.6 genauso.
Beim Diagramm hilft ein kleiner Trick für große Rundenzahlen: pointStep sorgt dafür, dass jede Kurve höchstens etwa 1000 Punkte bekommt. Bei 10 000 Runden wird deshalb nur jede zehnte Runde eingezeichnet – mehr Punkte, als die Leinwand Pixel breit ist, würde man ohnehin nicht sehen.
Original aus dem Buch ansehen ziegenproblem.py · Python 2
import random
class Player(object):
def __init__(self):
self.guess = 0
self.wins = 0
def newGuess(self):
self.guess = random.randint(1, 3)
def changeMind(self, knownNumber):
allNumbers = set((1, 2, 3))
allNumbers.remove(self.guess)
allNumbers.remove(knownNumber)
self.guess = allNumbers.pop()
def saveResult(self, winning):
if winning == self.guess:
self.wins += 1
if __name__=='__main__':
player1 = Player()
player2 = Player()
x = 0
while x < 1000:
player1.newGuess()
player2.newGuess()
winning = random.randint(1, 3)
allNumbers = set((1, 2, 3))
numbersToRemove = set((winning, player1.guess))
allNumbers -= numbersToRemove
knownNumber = allNumbers.pop()
player1.changeMind(knownNumber)
player1.saveResult(winning)
player2.saveResult(winning)
x += 1
print "player 1 won ", player1.wins, " times"
print "player 2 won ", player2.wins, " times"