Simulationen

Mit Simulation (oder hier genauer Computer-Simulation) ist die Nachbildung eines in der realen Welt stattfindenden Prozesses gemeint. Einige Beispiele:

Die klassischen Standbeine der Naturwissenschaften sind Experiment und Theorie. Als drittes Standbein kommt mit zunehmenden Rechenkapazitäten die Simulation hinzu.1) Bisweilen können Simulationen kostspielige oder gefährliche Experimente ersetzen. Auch lässt sich mit Simulationen der Erfolg von Experimenten oder die Gültigkeit einer Theorie abschätzen.

Kopf oder Zahl: Simulation von Münz- und Würfelexperimenten

Motivation

Schätze:

  1. Wie oft muss man einen (Spiel-)Würfel im Schnitt werfen, bis zweimal direkt hintereinander eine Sechs (also 66) erscheint?
  2. Muss man einen Würfel im Schnitt gleich oft, weniger oft oder öfter werfen, bis das erste Mal eine Sechs direkt gefolgt von einer Fünf (also 65) erscheint?
  3. Wie oft muss man eine Münze im Schnitt werfen, bis zweimal direkt hintereinander Kopf (also KK) erscheint?

Kopf oder Zahl - vom Problem zum Programm

Experimentelle Lösung und Beschreibung des Experiments

Zweierarbeit:

  1. Beantworte experimentell: Wie oft muss man eine Münze im Schnitt werfen, bis zweimal hintereinander Kopf erscheint? (ca. 10 Minuten, mindestens 20 Experimente)
  2. Wenn ihr fertig seid: Beschreibt handschriftlich möglichst genau auf einem Blatt Papier, was ihr gemacht habt! (Denn wir wollen dies später einem Computer beibringen.)

Von der Beschreibung des Experiments zum Pseudo-Code

Gemeinsam: Experiment in Worten beschreiben, immer genauer, bis am Ende Pseudo-Code dasteht.

Beschreibung in der 2lW

Aufgabe: Pseudo-Code in ein Python-Programm übertragen

Wandle den zuvor erstellten Pseudo-Code in ein Python-Programm namens muenzwurf-simulation.py um. Wähle dein Niveau:

Höchstes Niveau: Selbständig programmieren

Höchstes Niveau: Selbständig programmieren

Schreibe selbst das entsprechende Programm.

Hinweise:

  • Um Zufallszahlen2) zu erzeugen:
    • Schreibe from random import * in die erste Zeile deines Programms.
    • Der Funktionsaufruf randrange(1, 3) liefert dann zufällig die Zahl 1 oder die Zahl 2 und beide Zahlen kommen gleich häufig vor.
  • Interpretiere die zufällig erzeugten Zahlen wie folgt: 1 steht für “Kopf”, 2 für “Zahl”.

Bemerkung: Gib Zwischenergebnisse aus, damit es bei hoher Anzahl von Simulationen etwas weniger langweilig ist, das Endergebnis abzuwarten (etwa alle 100000 Simulationen ein Zwischenergebnis).

Mittleres Niveau: Programmieren nach Anweisungen (vgl. Malen nach Zahlen)

Mittleres Niveau: Programmieren nach Anweisungen (vgl. Malen nach Zahlen)

Verwende das folgende Programmgerüst: Ersetze jedes Auftreten von “???” wie direkt davor beschrieben. Alle Kommentarzeilen (beginnend mit #) können bleiben.

muenzwurf-simulation-vorlage.py
# Vereinbarung:
# 1 = Kopf
# 2 = Zahl
 
# Import der Zufallsbibliothek
from random import *
 
# Anzahl der durchzuführenden Simulationen
ANZAHL_SIMULATIONEN = 100000
 
# Vereinbare eine Variable namens "gesamtzahl_wuerfe", 
# die die Gesamtzahl aller Münzwürfe zählt und setze sie auf Null.
???
 
# Beginn einer for-Schleife, die so oft ausgeführt wird,
# wie die Variable ANZAHL_SIMULATIONEN angibt.
# Nenne die Laufvariable simulationsnummer.
???
    # Gibt die Simulationsnummer aus und die aktuelle durchschnittliche Dauer, 
    # falls diese durch 10000 teilbar und positiv ist.
    if simulationsnummer % 100000 == 0:
        print(simulationsnummer)
    #    
    # Beginn des Einzelexperiments:
    # Vereinbare zwei Variablen namens
    # "letzter_wurf" und "vorletzter_wurf"
    # und weise ihnen jeweils als Startwert einen Fantasiewert zu, 
    # etwa 'nicht-definiert' oder 9999, (aber nicht 1 oder 2).
    ???
    ???
 
    # Vereinbare eine Variable "anzahl_wuerfe", deren Wert jeweils angibt, wie oft im
    # Einzelexperiment bereits die Münze geworfen wurde.
    ???
    # Beginn einer while-Schleife:
    # Der eingerückte Code-Block nach der mit "while" beginnenden Zeile
    # wird so lange durchgeführt, wie
    # die Bedingung ??? wahr ist. Wodurch musst du ??? ersetzen?
    while not (???):
        # Erzeugt zufällig die Zahl 1 (alias Kopf) oder 2 (alias Zahl) 
        # und speichert sie in der Variablen "aktueller_wurf".
        aktueller_wurf = randrange(1,7)
        # Passe die Variable anzahl_wuerfe an.
        ???
        # Aktualisiere die beiden Variablen "letzter_wurf" und "vorletzter_wurf":
        # Der letzte Wurf wird zum vorletzten Wurf,
        # der aktuelle Wurf wird zum letzten Wurf. 
        ???
        ???
    # Erhöhe die Variable "gesamtzahl_wuerfe" um die Anzahl
    # der im aktuellen Einzelexperiment getätigten Würfe.
    ???
 
# Berechne die durchschnittliche Dauer für zweimal Kopf 
# und gib sie aus!
???

Bemerkung: Die Variable anzahl_wuerfe benötigt man nicht unbedingt: Man kann auch bei jedem Wurf direkt die Variable gesamtzahl_wuerfe um Eins erhöhen.

Niedrigstes Level: Lehrperson programmiert vor

Niedrigstes Level: Lehrperson programmiert vor

Du programmierst mit und versuchst, das Programm zu verstehen. Ausserdem vergleichst du es mit dem Pseudo-Code.

Indem du dein Programm 1'000'000 Simulationen durchführen lässt, bekommst du eine Vermutung für die Antwort auf die folgende Frage.3)

Wie oft muss man eine Münze im Schnitt werfen, bis zweimal direkt hintereinander Kopf erscheint?

Was denkst du?

Wie oft muss man eine Münze im Schnitt werfen, bis direkt hintereinander Kopf-Zahl erscheint? 
Genauso lang wie für zweimal Kopf hintereinander?

Ändere dein Programm ein wenig, um diese Frage zu erforschen.

Bevor man ein kompliziertes Problem löst (und dafür etwa ein Computerprogramm schreibt), ist es stets eine gute Idee, das Problem auf einem Blatt Papier handschriftlich möglichst genau zu analysieren (und den beabsichtigten Programmablauf schrittweise immer genauer festzulegen (etwa: welche Variablen, Schleifen, Funktionen benötige ich?)).

Würfeln, bis zwei Sechser kommen

  • Öffne ein neues Python-Programm namens wuerfel-simulation.py und kopiere allen Code aus dem Programm muenzwurf-simulation.py in dieses Programm.
  • Ändere das Programm so, dass es per Simulation die Frage beantwortet: Wie oft muss man einen (Spiel-)Würfel im Schnitt werfen, bis zweimal direkt hintereinander eine Sechs erscheint?

Bonusaufgabe:

  • Wie oft muss man im Schnitt würfeln, bis dreimal direkt hintereinander eine Sechs erscheint?

Zusatzfragen

Münzwurf

Welche der Folgen KKK (= Kopf-Kopf-Kopf), KKZ, KZK, ZKK kommt im Schnitt am frühesten?

Würfeln

Es bieten sich jede Menge ähnliche Fragen an. Finde selbst eine oder suche dir eine aus der folgenden Liste aus!

Wie oft muss man einen Würfel im Schnitt werfen,

  1. bis insgesamt 10 Mal eine 6 gewürfelt wurde?
  2. bis insgesamt 20 Mal eine 6 gewürfelt wurde?
  3. bis eine 6 gewürfelt wird?
  4. bis zweimal direkt hintereinander dieselbe Zahl (also ein Pasch) erscheint?
  5. bis direkt hintereinander 1, 2 erscheint?
  6. bis direkt hintereinander 6, 6 oder direkt hintereinander 1, 2 erscheint?
  7. bis direkt hintereinander 6, 6 oder direkt hintereinander 1, 1 erscheint?
  8. bis zwei Päsche hintereinander erscheinen? (Entscheide selbst, ob dreimal hintereinander dieselbe Zahl (etwa 3, 3, 3) als zwei Päsche hintereinander zählen oder nicht.)
  9. bis 5 Mal hintereinander eine Sechs erscheint? (Empehlung, wenn bekannt: Verwende eine Liste!)
  10. bis 10 Mal (oder gar 1000 Mal) hintereinander eine Sechs erscheint? (Achtung: Nicht zu viele Simulationen durchführen.)

Welche Folge von drei Zahlen kommt im Schnitt am frühesten?

Wie viele Päsche (alias zweimal dieselbe Zahl direkt hintereinander) hat man im Schnitt geworfen, bevor man einen Sechserpasch wirft?

Wie lange dauert es im Schnitt, bis man eine “grosse Strasse” 123456 wirft? Wie viele “kleine Strassen” (also 12345 oder 23456) hat man im Schnitt vorher geworfen?

Wie lange dauert es im Schnitt, bis die Summe aller gewürfelten Zahlen 100 übersteigt?

Bei wieviel Prozent der Versuche kommt man beim wiederholten würfeln genau bei 100 an?

Würdest du darauf wetten, dass man in vier Würfen mit einem Würfel mindestens eine Sechs würfelt?

Würdest du darauf wetten, dass man in 38 Würfen mit einem Würfel mindestens zwei Sechser direkt hintereinander würfelt? (Man könnte 38 durch irgendeinen andere Zahl ersetzen.)

Wenn man mit jeweils mit zwei Würfeln gleichzeitig würfelt, wie lange muss man im Schnitt auf den Sechserpasch warten?

Warum "meist" ganze Zahlen als Ergebnis?

Bei vielen dieser Fragestellungen legen die Simulationen nahe, dass als Ergebnis eine ganze Zahl heraus kommt (also etwa 6 oder 42, aber nicht 43.5). Genauer vermute ich, dass dies bei allen Fragestellungen der Fall ist, wo man auf eine einzige Folge wartet.

Wenn das so ist (was man sich mathematisch recht leicht überlegen können sollte), gibt es dafür einen offensichtlichen Grund?

Eventuell: Exakte mathematische Lösung für die Münz- oder Würfelaufgabe

Per Zustandsdiagramm, Übergangswahrscheinlichkeiten, an Tafel. (Die wesentliche Idee: Für jeden Zustand $A$ eine Variable $a$ einführen, die angibt, wie lange man im Schnitt noch braucht (die Variablen der Zielzustände haben den Wert Null). Dann gilt etwa $a=\frac 16(1+b)+\frac 56 (1+c)=1+\frac 16b+\frac 56c$, wenn man vom Zustand $A$ mit Wahrscheinlichkeit $\frac 16$ in den Zustand $B$ gelangt und mit Wahrscheinlichkeit $\frac 56$ in den Zustand $C$. Beachte: Die beiden Einser kommen (bzw. der Einser kommt) daher, dass man genau einmal würfeln muss, um in den nächsten Zustand zu kommen. Es ergibt sich ein Gleichungssytem, das man löse.)

Bemerkungen:

Simulation von Roulette

Wenn man im Wesentlichen weiss, wie Roulette funktioniert, muss man von den Roulette-Regeln für die folgende Aufgabe nur das Folgende wissen (französisches Roulette ohne Prison-Regel):

Aufgabe: Verdopplungs-Strategie (= Doublieren)

Jemand vermutet (irrtümlich), dass er mit der folgenden Strategie langfristig beim Roulette gewinnen kann:

Er startet mit einem Anfangsvermögen von 100 Jetons.
Er beginnt mit einem Einsatz von 1 Jeton.
Solange er nicht alles Geld verloren hat oder sein Anfangsvermögen verdoppelt hat, macht er das folgende:
    Er setzt seinen Einsatz auf Rot.
    (Nun folgt der Wurf der Kugel.)
    Wenn die Kugel in einem roten Fach zu liegen kommt (er also gewinnt),
        startet er beim nächsten Mal wieder mit einem Einsatz von 1 Jeton;
    sonst (im Fall, dass die Kugel schwarz oder grün (= Null) liefert)
        verdoppelt er seinen Einsatz für das nächste Mal, falls er dafür genügend Geld übrig hat.
        Sonst setzt er beim nächsten Mal all sein verbliebenes Vermögen.        

Die Idee dahinter: Wenn er beispielsweise 5 Mal verloren hat, also 1+2+4+8+16 = 31 Jetons verloren hat, so setzt er beim nächsten Mal 32 Jetons und macht so seinen Verlust weg und gewinnt sogar einen Jeton dazu.

Schreibe ein Programm, das 10'000 Spielbankbesuche simuliert, jeweils mit einem Startvermögen von 100 Jetons. Auszugeben sind am Ende:

  • Durchschnittsgewinn pro Spielbankbesuch (dieser wird negativ sein - de facto verliert man also langfristig);
  • Anteil der Spielbankbesuche in Prozent, die mit einer Vermögensverdopplung enden.

Gerüst des fast fertigen Programms, falls benötigt

Gerüst des fast fertigen Programms, falls benötigt

Alles zwischen den Fragezeichen ist in Code umzusetzen.

roulette-simulation.py
from random import *
 
ANZAHL_SIMULATIONEN = 10000
ANFANGSVERMOEGEN = 100
 
anzahl_gewinne = 0
gesamt_gewinn = 0 # Kann und wird auch negativ sein.
 
for simulationsnummer in range(1, ANZAHL_SIMULATIONEN + 1):
    vermoegen = ANFANGSVERMOEGEN
    einsatz = 1
    while ??? weder pleite noch ANFANGSVERMOEGEN verdoppelt ???:
        if randrange(37) < 18:
            # die Zufallszahl ist eine der 18 Zahlen von 0 bis 17,
            # was wir als Rot interpretieren, 
            # d.h. der Spieler gewinnt und 
            # bekommt seinen Einsatz von der Bank.
            ??? Passe die Variable "vermoegen" an ???    
            ??? Passe die Variable "einsatz" an ??? 
        else:
            # Farbe ist Schwarz, 
            # d.h. der Spieler verliert seinen Einsatz.
            ??? Passe die Variable "vermoegen" an ???    
            einsatz = min(2 * einsatz, vermoegen)
    if ??? Vermögen verdoppelt ???:
        ??? Zähle den erzielten Gewinn zu "gesamt_gewinn" hinzu. ???
        ??? Erhöhe "anzahl_gewinne". ???
    else:
        # Alles verloren.
        ??? Passe die Variable "gesamt_gewinn" an ???
 
print(f'Anteil erfolgreicher Spielbankbesuche in Prozent: {100 * anzahl_gewinne / ANZAHL_SIMULATIONEN}')
print(f'Durchschnittsgewinn: {gesamt_gewinn / ANZAHL_SIMULATIONEN}')

Weitere mögliche Fragestellungen:

  • Wie wären die Gewinnchancen, wenn es die grüne Null nicht gäbe?
  • Was passiert, wenn man dieselbe Verdopplungsstrategie verwendet, jedoch erst bei einer Verzehnfachung des Anfangsvermögens nach Hause geht?
  • Wie viele Runden spielt man durchschnittlich im Gewinnfall?
  • Wie viele Runden spielt man durchschnittlich im Verlustfall?

Ideen

(eventuell: Listen und dann Ergebnis-Darstellung durch Säulendiagramme)

(in Schublade: Turtle-Grafik selbst schreiben, ausgehend von kantigrafik.)

Lösungsvorschlag

Simulationsprogramm für die durchschnittliche Dauer bis zweimal Kopf, zusätzlich mit Häufigkeits-Säulendiagramm

Simulationsprogramm für die durchschnittliche Dauer bis zweimal Kopf, zusätzlich mit Häufigkeits-Säulendiagramm

muenz-simulation.py
from random import *
from matplotlib import pyplot
 
gesamtzahl_wuerfe = 0
ANZAHL_SIMULATIONEN = 1000000
 
def zeichneSaeulendiagramm(xWerte, yWerte, titel, xBeschriftung, yBeschriftung, xSchrittweite):
    # xWerte: Liste der Werte auf der horizontalen Achse
    # yWerte: Liste der zugehörigen Funktionswerte
    # titel: Titel der Graphik
    # xBeschriftung: Text unter der horizontalen Achse
    # yBeschriftung: Text links der vertikalen Achse
    pyplot.figure("Säulendiagramm-Fenster")
    pyplot.title(titel)
    pyplot.xlabel(xBeschriftung)
    pyplot.ylabel(yBeschriftung)
    pyplot.bar(xWerte, yWerte, color='blue')
    # pyplot.xticks(xWerte, xWerte, rotation='horizontal')
    pyplot.xticks(range(min(xWerte), max(xWerte)+1, xSchrittweite))
    pyplot.tight_layout()
    pyplot.show()
 
haeufigkeit_bei_dauer = []
for simulationsnummer in range(ANZAHL_SIMULATIONEN):
    if simulationsnummer % 100000 == 0 and simulationsnummer > 0:
        print(simulationsnummer, gesamtzahl_wuerfe / simulationsnummer)
    letzter_wurf = 'nicht definiert'
    vorletzter_wurf = 'nicht definiert'
    anzahl_wuerfe = 0
    while not (letzter_wurf == 1 and vorletzter_wurf == 1):
        aktueller_wurf = randrange(1,3)
        anzahl_wuerfe = anzahl_wuerfe + 1
        vorletzter_wurf = letzter_wurf
        letzter_wurf = aktueller_wurf
    while anzahl_wuerfe > len(haeufigkeit_bei_dauer) - 1:
        haeufigkeit_bei_dauer.append(0)
    haeufigkeit_bei_dauer[anzahl_wuerfe] = haeufigkeit_bei_dauer[anzahl_wuerfe] + 1    
    gesamtzahl_wuerfe = gesamtzahl_wuerfe + anzahl_wuerfe
print(gesamtzahl_wuerfe / ANZAHL_SIMULATIONEN)
# print(haeufigkeit_bei_dauer)
zeichneSaeulendiagramm(range(len(haeufigkeit_bei_dauer)), haeufigkeit_bei_dauer, 'Anzahl der Experimente bei gegebener Anzahl von Würfen (Achtung, evtl. 10er-Potenzen auf vertikaler Achse)', 'Anzahl von Würfen', 'Anzahl der Experimente', 1)

Simulationsprogramm für Roulette mit Verdopplungsstrategie

Simulationsprogramm für Roulette mit Verdopplungsstrategie

roulette-simulation.py
from random import *
 
ANZAHL_SIMULATIONEN = 100000
ANFANGSVERMOEGEN = 100
 
anzahl_gewinne = 0
gesamt_gewinn = 0 
 
for simulationsnummer in range(1, ANZAHL_SIMULATIONEN + 1):
    vermoegen = ANFANGSVERMOEGEN
    einsatz = 1
    while 0 < vermoegen < 2 * ANFANGSVERMOEGEN:
        if randrange(37) < 18:
            # die Zufallszahl ist eine der 18 Zahlen von 0 bis 17,
            # was wir als Rot interpretieren, 
            # d.h. der Spieler gewinnt und 
            # bekommt seinen Einsatz von der Bank.
            vermoegen = vermoegen + einsatz
            einsatz = 1
        else:
            # Farbe ist Schwarz, 
            # d.h. der Spieler verliert seinen Einsatz.
            vermoegen = vermoegen - einsatz
            einsatz = min(2 * einsatz, vermoegen)
    if vermoegen >= 2 * ANFANGSVERMOEGEN:
        gesamt_gewinn = gesamt_gewinn + vermoegen - ANFANGSVERMOEGEN
        anzahl_gewinne = anzahl_gewinne + 1
    else:
        # Alles verloren.
        gesamt_gewinn = gesamt_gewinn - ANFANGSVERMOEGEN
 
print(f'Anteil erfolgreicher Spielbankbesuche in Prozent: {100 * anzahl_gewinne / ANZAHL_SIMULATIONEN}')
print(f'Durchschnittsgewinn: {gesamt_gewinn / ANZAHL_SIMULATIONEN}')

Zur Kursseite

2)
genauer handhelt es sich um Pseudozufallszahlen
3)
Die vorsichtige Formulierung (statt “beantworte die folgende Frage!”) ist gewählt, denn: (1) Wenn auch extrem unwahrscheinlich, könnte es beispielsweise sein, dass der Computer in jeder Simulation sofort zweimal Kopf geworfen hat. (2) Es könnte sein, dass die vom Computer erzeugten Pseudozufallszahlen nicht wirklich zufällig sind und etwa Kopf ein ganz kleines bisschen öfter vorkommt als Zahl. (3) Das Programm könnte falsch sein.