Dieses Werk ist lizenziert unter einer
Creative Commons Namensnennung 4.0 International Lizenz.
Unter Programmieren versteht man ganz allgemein die Tätigkeit, einem Computer Anweisungen zu geben. Das Eintippen einer Rechenanweisung in einen Taschenrechner lässt sich also schon als Programmieren betrachten. Man erklärt dem Computer die Berechnung, die man gerne durchführen möchte, und der Computer erledigt die Rechenarbeit. Einfache Taschenrechner kennen jedoch nur wenige Befehle, meist nur Grundrechenarten. Aufwendigere kennen zum Beispiel Winkelfunktionen oder die Möglichkeit, Zahlen zu speichern und wieder abzurufen. Will man noch komplexere Berechnungen durchführen, ist man auf „echte“ Computer angewiesen.
Im Unterschied zum Taschenrechner mit seinen Tasten für jede Grundrechenart, muss dem Computer nun allerdings der Umstand, dass zum Beispiel die Wurzel einer Zahl berechnet werden soll, mithilfe einer Programmiersprache mitgeteilt werden. Dies könnte zum Beispiel der Befehl sqrt(x) sein, den es wohl – abhängig von der so genannten Syntax einer Sprache – in der einen oder anderen Form in jeder Programmiersprache gibt. Das hat den Vorteil, dass die Kenntnis einer Programmiersprache in der Regel dafür sorgt, dass auch andere Programmiersprachen leicht erlernt werden. Darüber hinaus steigern Programmierkenntnisse die allgemeine Problemlösungskompetenz, die Abstraktionsfähigkeit und die präzise Ausdrucksweise im wissenschaftlichen Kontext.
Die Vorlesung – und damit dieses Skriptum – beziehen sich auf die Programmiersprache Python. Dies hat mehrere Gründe: Python ( https://www.python.org ) ist eine universelle, vielseitig einsetzbare Programmiersprache, die
Anaconda (https://www.anaconda.com/) ist eine sogenannte Python-Distribution, die eine Vielzahl von Werkzeugen vereint, die für das Programmieren mit Python notwendig sind. Anaconda ist frei (d.h. kostenlos), sehr leicht zu installieren, und verfügbar für Windows, Mac-OS und Linux. Anaconda enthält mit über 150 mitgelieferten Modulen (packages) im Prinzip alles (und viel mehr), was zum Programmieren im Bereich der USW-Systemwissenschaften nötig ist.
Vor allem enthält Anaconda bereits auch eine sehr komfortable Ein- und Ausgabe-Umgebung (d.h. eine Entwicklungsumgebung) namens Jupyter Notebook , die einfach im gewohnten Browser läuft, wie er für das tägliche Surfen im Internet verwendet wird (Google-Chrome, Firefox, Internetexplorer ...). Das heißt, nach Installation von Anaconda sind im Prinzip keine zusätzlichen oder ungewohnten Programme mehr notwendig, um sofort mit Python zu arbeiten zu beginnen. Im Folgenden wird nun der Installationsvorgang für Anaconda mit Python 3.7. auf einem Windows-Computer beschrieben.
Eine weitere Windows-Installationsbeschreibung findet sich hier: https://docs.continuum.io/anaconda/install/windows
Für die Mac-OS-Installation siehe hier: https://docs.continuum.io/anaconda/install/mac-os
Für Linux siehe hier: https://docs.continuum.io/anaconda/install/linux
Nach erfolgreicher Installation sollten sie in Ihrem Windows Startmenü einen Menüpunkt Anaconda sehen, der seinerseits einen Menü - Unterpunkt Jupyter Notebook enthält.
Das Jupyter Notebook ist eine sehr komfortable Ein-und Ausgabe-Umgebung für die Programmiersprache Python , d.h. eine Entwicklungsumgebung, die im bereits auf ihrem Computer installierten Browser läuft. Alle in diesem Skriptum vorkommenden Beispiele wurden mit dem Jupyter Notebook geschrieben und werden auch in diesem angezeigt.
Klicken Sie , um das Jupyter Notebook zu starten, im Windows-Startmenü unter Anaconda auf den Menüpunkt Jupyter Notebook.
Es öffnet sich ein Fenster, das zuerst schwarz ist, und sich dann mit weißem Text füllt. Dieses Fenster darf nicht geschlossen werden, solange Sie mit dem Jupyter Notebook arbeiten möchten. Es handelt sich hierbei um eine Anzeige für den so genannten Kernel , also den Prozess, in dem die eigentlichen Berechnungen durchgeführt werden. Zusätzlich öffnet sich Ihr Browser, bzw. ein neuer Tab in Ihrem Browser.
Um ein neues Jupyter Notbook zu öffnen, klicken Sie im rechten Bereich dieses Browser-Tabs auf das Dropdown-Menü „New“ und wählen Sie „Python 3“ aus.
Jupyter-Notebooks bestehen aus Zellen. Es gibt drei Typen von Zellen:
Um eine In-Zelle auszuführen klickt man einfach in die gewünschte Zelle und drückt die Shift- und die Enter-Taste gleichzeitig. Es wird automatisch eine Out-Zelle produziert, in der das Ergebnis der In-Zelle steht.
Auch ohne jegliche Programmierkenntnisse kann man so schon Python benutzen, wenn auch nur als Taschenrechner:
7 * 7 - 7
((12 + 144 + 20 + 3 * 4**0.5) / 7) + 5 * 11
# Exponenten werden in Python als x**y angegeben. 4**0.5 bedeutet also 4 hoch 0.5.
# Der Text hinter einer Raute (#) wird vom Computer ignoriert und ist als Information für den Menschen gedacht.
Python kann aber natürlich viel mehr als ein Taschenrechner. In eine Zelle kann man nicht nur einzelne Rechnungen schreiben, sondern ganze Programme oder vollständige Simulationen. Versuchen wir eine ganz simple Simulation zu erstellen, um die Grundbefehle von Python zu lernen. Nehmen wir an, wir wollen die Populationsentwicklung in einem Froschteich simulieren. In allererster Näherung gehen wir davon aus, das sich jedes Jahr 3 neue Frösche im Teich ansiedeln.
Um Zahlen (wie z.B. die Anzahl der Frösche in einem Teich) zu speichern muss eine so genannte Variable angelegt werden. Das ist sozusagen der Name unter dem die Zahl gespeichert ist. Die Zuweisung von Variablenname zur Zahl passiert in Python mit dem = Zeichen. Wichtig ist, dass der Name links vom = Zeichen steht, die Zahl die dort gespeichert werden soll, rechts. Wir starten unsere Teichsimulation also indem wir die Anzahl der Frösche auf 0 setzen.
froschanzahl = 0
Um eine Variable zu ändern kann man einfach den aktuellen Wert mit dem neuen Wert überschreiben. Auch das geschieht mit dem = Zeichen.
# Jahr 1:
froschanzahl = 3
Um Variablen wieder anzuzeigen benutzt man den Befehl print
. Dieser Befehl schreibt den aktuellen Wert der abgefragten Variable in die Ausgabezeile. Schreiben wir also print(froschanzahl)
, sollte das das Ergebnis 3 bringen.
print(froschanzahl)
Das ist sehr hilfreich für unsere weiter Simulation, denn so können wir die aktuelle Zahl der Frösche benutzen, um die neue zu berechnen. Laut unserer Annahme ist die Zahl der Frösche in jedem Jahr um 3 höher als zuvor, mathematisch ausgedrückt also froschanzahl + 3
. Somit sieht unsere Froschsimulation so aus:
froschanzahl = 0
#Jahr 1
froschanzahl = froschanzahl + 3
#Jahr 2
froschanzahl = froschanzahl + 3
#Jahr 3
froschanzahl = froschanzahl + 3
#Jahr 4
froschanzahl = froschanzahl + 3
#Jahr 5
froschanzahl = froschanzahl + 3
#Um ein Ergebnis auszugeben benutzen wir den Befehl "print"
print(froschanzahl)
In dieser Version ist unsere Froschsimulation nicht nur sehr unspektakulär, sondern auch recht umständlich. Sie ist jedoch ein guter Ausgangpunkt für eine besser Simulation. Als ersten Schritt wäre es schön, wenn wir die Anzahl der Jahre, die simuliert werden sollen, einstellen könnten und nicht immer exakt 5 Jahre simulieren müssen. Dazu benötigen wir ein wichtiges Konzept: die For-Schleife.
For-Schleifen werden benutzt um einen Befehl mehrmals hintereinander ausführen zu lassen, in unserem Fall das Hinzuzählen von Fröschen. So würde unser Programm mit einer For-Schleife aussehen:
froschanzahl = 0
for it in range(5):
froschanzahl = froschanzahl + 3
print(froschanzahl)
Das ist deutlich kompakter, dadurch aber auch ein wenig komplizierter. Die erste Zeile ist exakt gleich, wir setzen froschanzahl
auf 0. In der nächsten Zeile leitet der Befehl for
die For-Schleife ein. Lesen könnte man diese Zeile als: Mache folgenden Befehl für jede ganze Zahl it
, die kleiner ist als 5, also ingesamt 5 mal (für 0,1,2,3,4). Nach dem Doppelpunkt kommt in der nächsten Zeile der Befehl, der ausgeführt werden soll. Zum Schluss lassen wir uns wieder das Ergebnis ausgeben.
Woher weiß Python jetzt aber, das wir den letzten Befehl (das print
) nicht auch 5 mal ausgeführt haben wollen? Das passiert mittels Einrückungen (tab-Taste). Befehle, die direkt untereinander stehen gehören für Python zusammen. Das macht Pythoncode automatisch gut lesbar und man benötigt keine Klammern.
Würden wir die Ausgabe wirklich gerne nach jedem Jahr, und nicht erst am Schluss haben, können wir den print-Befehl mit der Tabulatortaste einrücken. So gehört er zur For-Schleife:
froschanzahl = 0
for it in range(5):
froschanzahl = froschanzahl + 3
print(froschanzahl)
Ob Befehle innerhalb oder außerhalb einer For-Schleife stehen macht einen großen Unterschied und ist eine häufige Fehlerquelle. Würde man beispielsweise auch die Zeile froschanzahl = 0
in die For-Schleife einbauen, würde die Zahl der Frösche bei jedem Durchlauf der Schleife auf 0 gesetzt werden:
for it in range(5):
froschanzahl = 0
froschanzahl = froschanzahl + 3
print(froschanzahl)
Dennoch bietet das Verwenden einer For-Schleife einen riesigen Vorteil: Wir müssen nur mehr eine einzige Zahl ändern um die Zahl der simulierten Jahre zu verändern. Wollen wir beispielweise 10 Jahre simulieren:
froschanzahl = 0
for it in range(10):
froschanzahl = froschanzahl + 3
print(froschanzahl)
Zu einem schönen Stil beim Programmieren gehört es, solche Zahlen, die man öfter ändern möchte an den Anfang des Programms zu stellen. Wir definieren also eine neue Variable, damit wir die Zahl nicht mitten im Code ändern müssen, sondern schön übersichtlich am Anfang. Mit einem Kommentar wissen auch zukünftige Benutzer, was diese Variable genau macht.
simulationszeit = 10
#simulationszeit: Die Zeit in Jahren, die der Froschteich simuliert wird
froschanzahl = 0
for it in range(simulationszeit):
froschanzahl = froschanzahl + 3
print(froschanzahl)
Mit diesem Programm können wir nun auch sehr lange Zeitbereiche simulieren:
simulationszeit = 1000
#simulationszeit: Die Zeit in Jahren, die der Froschteich simuliert wird
froschanzahl = 0
for it in range(simulationszeit):
froschanzahl = froschanzahl + 3
print(froschanzahl)
In dieser Simulation ist das Endergebnis weniger spannend als die zeitliche Entwicklung. Es wäre also interessant die Froschanzahl zu jedem Zeitpunkt auszugeben. Am einfachsten verschieben wir den print Befehl in die For-Schleife:
simulationszeit = 30
#simulationszeit: Die Zeit in Jahren, die der Froschteich simuliert wird
froschanzahl = 0
for it in range(simulationszeit):
froschanzahl = froschanzahl + 3
print(froschanzahl), #ein Beistrich hinter einem Print-Befehl schreibt die Ergebnisse, statt untereinander, in eine Zeile
Auf diese Weise sieht man wie sich die Froschpopulation entwickelt, da wir jeden Wert einzeln ausgegeben haben. Der Nachteil an dieser Variante: Wirklich gespeichert haben wir immer nur ein Zahl. Am Ende der Simulation sehen wir zwar viele Zahlen, gespeichert (und somit zum Weiterarbeiten verfügbar) ist aber nur die letzte. Eine schönere Lösung ist das Verwenden von einer Liste.
Listen bestehen aus mehreren aufeinanderfolgenden Werten. Um eine Liste zu erstellen, fängt man am besten mit einer leeren Liste an und hängt dann immer wieder neue Elemente an. Auch Listen bekommen, so wie Variablen, einen eindeutigen Namen. Leere Listen erstellt man mit name=[]
und neu Einträge fügt man mit dem append-Befehl hinzu. Für unser Beispiel:
simulationszeit = 30
#simulationszeit: Die Zeit in Jahren, die der Froschteich simuliert wird
froschanzahl = 0
froschanzahl_liste = [] #leere liste wird erstellt
froschanzahl_liste.append(froschanzahl) #erster Eintrag (0 Frösche) wird angehängt
for it in range(simulationszeit):
froschanzahl = froschanzahl + 3
#am Ende jedes Schleifendurchgangs wird die aktuelle Zahl an die liste angehängt
froschanzahl_liste.append(froschanzahl)
print(froschanzahl_liste)
Nun kann man schon grob erkennen was passiert, viel vorstellen kann man sich aber noch nicht. Schön wäre eine grafische Darstellung. Und das ist in Python zum Glück sehr einfach.
Der Befehl zum Erstellen von Plots lautet "plot". Dieser Befehl ist jedoch nicht standardmäßg in jedem Pythonprogramm vorhanden, sondern muss in der Regel erst aus einem sogenannten Paket importiert werden. Pakete sind sozusagen Sammlungen von Befehlen.
Der plot Befehl selbst funktioniert dann ganz einfach: In Klammer schreibt man einfach die Liste von Zahlen, die man darstellen möchte.
import matplotlib.pyplot as plt #wir importieren das Modul matplotlib.pyplot und geben ihm die Abkürzung plt
# die nächste Zeile bewirkt, dass die Grafiken direkt in der Zelle angezeigt werden
%matplotlib inline
simulationszeit = 30
#simulationszeit: Die Zeit in Jahren, die der Froschteich simuliert wird
froschanzahl = 0
froschanzahl_liste = [] #leere Liste wird erstellt
froschanzahl_liste.append(froschanzahl) #erster Eintrag (0 Frösche) wird angehängt
for it in range(simulationszeit):
froschanzahl = froschanzahl + 3
#am Ende jedes Schleifendurchgangs wird die aktuelle Zahl an die Liste angehängt
froschanzahl_liste.append(froschanzahl)
plt.plot(froschanzahl_liste)
Eine Grafik ist schon viel anschaulicher als eine Liste von Zahlen. Natürlich kann man diese Grafik noch deutlich verbessern. Bei Fröschen würde es sich beispielsweise anbieten, die Linie grün darzustellen. Solche zusätzlichen Optionen kann man im Plot-Befehl einfach nach einem Beistrich anfügen:
plt.plot(froschanzahl_liste, color = 'green')
Man beachte, dass man die Farbe selbst unter Anführungsstrichen schreiben muss, da das Programm sonst nach einer Variable suchen würde, die green heißt.
Eine weitere mögliche Verbesserung wäre eine Beschriftung der Achsen:
plt.plot(froschanzahl_liste, color = 'green')
plt.xlabel('Zeit (Jahre)')
plt.ylabel('Frösche')
Durch Variablen können wir Werte einem Namen zuordnen. Mit
varname = 42
weisen wir der Variable varname
den Wert 42 zu und überschreiben den aktuell dort gespeichert Wert, wenn dort schon etwas gespeichert ist.
Mit
varname = varname + 1
erhöhen wir den Wert, der unter varname
gespeichert ist um 1.
Den aktuellen Wert der Variable varname
kann mit
print(varname)
angezeigt werden.
Mit For-Schleifen ist es möglich gleiche oder ähnliche Befehl oftmals hintereinander auszuführen. Um einen Befehl also beispielsweise 100 mal abarbeiten zu lassen, verwenden wir:
for it in range(100):
befehl
Man beachte die Einrückung des Befehls, die zeigt, das sich der Befehl innerhalb der Schleife befindet.
In Listen können mehrere Werte unter nur einem Namen abgespeichert werden. Leere Listen erstellt man mit
listenname = []
Einen neuen Eintrag (z.B. neueselement
) zur Liste hinzufügen kann man dann mit
listenname.append(neueselement)
Um innerhalb eines Jupyter-Notebooks den Plotbefehl benutzen zu können, verwenden wir die Zeilen
import matplotlib.pyplot as plt
%matplotlib inline
am Anfang des Programms. Danach können wir mit dem Befehl
plt.plot(listenname)
die Liste mit dem Namen listenname
grafisch darstellen. Zusatzoptionen wie Farbe können nach einem Beistrich übergeben werden:
plt.plot(listenname, color='green')
In dieser Einheit möchten wir uns genauer mit verschiedenen Möglichkeiten der Populationsentwicklung auseinandersetzen. Mit dem Beispiel des Froschteiches haben wir bereits eine sehr einfache Form der Populationsentwicklung kennengelernt, so genanntes:
Wir ziehen nun als weiteres Beispiel die Vermehrung von Kaninchen heran.
import matplotlib.pyplot as plt #wir importieren das paket matplotlib.pyplot und geben ihm die abkürzung plt
#die nächste Zeile bewirkt, dass die Grafiken direkt in der Zelle angezeigt werden
%matplotlib inline
simulationszeit = 30
#simulationszeit: Die Zeit in Jahren, die die Kaninchen simuliert werden
kaninchenanzahl = 0
kaninchenanzahl_liste = [] #leere liste wird erstellt
kaninchenanzahl_liste.append(kaninchenanzahl) #erster Eintrag (0 Kaninchen) wird angehängt
for it in range(simulationszeit):
kaninchenanzahl = kaninchenanzahl + 1
#am ende jedes schleifendurchgangs wird die aktuelle zahl an die liste angehängt
kaninchenanzahl_liste.append(kaninchenanzahl)
plt.plot(kaninchenanzahl_liste)
Diese Form von Wachstum war für einen Froschteich zwar eine gute Näherung, wenn wir annehmen, dass die Frösche dort einfach zuwandern. Unsere Kaninchenpopulation vermehrt sich aber durch Fortpflanzung. Das heißt, je mehr Kaninchen es gibt, um so schneller kommen neue hinzu.
Dies führt zu sogenanntem exponentiellen Wachstum.
Von exponentellem Wachstum spricht man, wenn das Wachstum einer Größe proportional zu der Größe selbst ist. In Python ausgedrückt also:
kaninchen = kaninchen + x * kaninchen
wobei x ein Wachstumsparameter ist, den wir beliebig wählen können. Möchten wir zum Beispiel, dass sich die Anzahl der Kanichen in jedem Zeitschritt verdoppelt, können wir als Wachstumsparameter 1 wählen:
kaninchen = kaninchen + 1 * kaninchen
was man auch als kaninchen = 2 * kaninchen
schreiben könnte. Möchten wir jeden Zeitschritt ein Wachstum von 10% wählen wir x als 0.1:
kaninchen = kaninchen + 0.1 * kaninchen
Machen wir dazu eine Simulation:
import matplotlib.pyplot as plt
%matplotlib inline
simulationszeit = 10
#simulationszeit: Die Zeit in Jahren, die die Kaninchen simuliert werden
kaninchenanzahl = 1
kaninchenanzahl_liste = [] #leere Liste wird erstellt
kaninchenanzahl_liste.append(kaninchenanzahl) #erster Eintrag (1 Kaninchen) wird angehängt
for it in range(simulationszeit):
kaninchenanzahl = kaninchenanzahl + kaninchenanzahl * 0.1 #neue kaninchenanzahl wird berechnet
#am ende jedes schleifendurchgangs wird die aktuelle zahl an die liste angehängt
kaninchenanzahl_liste.append(kaninchenanzahl)
plt.plot(kaninchenanzahl_liste)
Exponentielles Wachstum ist eine recht gute Näherung an das echte Verhalten einer Population. Ein Problem entsteht aber, wenn wir sehr lange Zeiträume betrachten. Setzen wir die Simulationszeit einmal auf 150 und sehen was passiert:
import matplotlib.pyplot as plt
%matplotlib inline
simulationszeit = 150
#simulationszeit: Die Zeit in Jahren, die die Kaninchen simuliert werden
kaninchenanzahl = 1
kaninchenanzahl_liste = [] #leere Liste wird erstellt
kaninchenanzahl_liste.append(kaninchenanzahl) #erster Eintrag (1 Kaninchen) wird angehängt
for it in range(simulationszeit):
kaninchenanzahl = kaninchenanzahl + kaninchenanzahl * 0.1 #neue kaninchenanzahl wird berechnet
#am ende jedes schleifendurchgangs wird die aktuelle zahl an die liste angehängt
kaninchenanzahl_liste.append(kaninchenanzahl)
plt.plot(kaninchenanzahl_liste)
Das Wachstum wird immer schneller und die Kaninchenpopulation explodiert. Das ist ein unrealistisches Verhalten, denn jedes Ökosystem hat eine gewisse Kapazitätsgrenze, also eine maximale Anzahl an Individuen, die im System leben können (aufgrund von Nahrung- oder Unterschlupfangebot). Auch das sollten wir unser Programm einbauen. Nehmen wir an es kann in unserem System nie mehr als 1000 Kaninchen geben. Wenn diese Zahl überschritten ist, sollen keine neuen Kaninchen dazukommen. Um das ins Programm einzubauen, brauchen wir eine neue Struktur, die so genannte If-Abfrage.
If-Abfragen
sind "Wenn-Dann-Fragen", mit denen wir Befehle an Bedingungen knüpfen können. Sie haben stets folgenden Aufbau:
if BEDINGUNG:
BEFEHL
BEFEHL
meint hierbei irgendeine Anweisung an Python, also zum Beispiel das Ändern einer Variable, einen Printbefehl oder etwas Ähnliches. Eine If-Abfrage
kann auch mehrere Befehle enthalten.
Eine BEDINGUNG
kann alles sein, was entweder wahr
oder falsch
sein kann. Oftmal benutzt man zur Definition einer Bedingung eine Gleichung. Man überprüft also etwa, ob es wahr
ist, dass eine Variable x
gleich groß ist wie eine Variable y
. Noch häufiger werden Ungleichungen benutzt. Man überprüft also, ob es wahr
ist, dass eine Variable x
größer (oder kleiner) als eine Variable y
ist.
Im folgenden Beispiel betrachten wir eine If-Abfrage
in einer For-Schleife. Die For-Schleife erhöht die Zahl der Kaninchen fortlaufend um 1, die If-Abfrage
führt einen Printbefehl aus, allerdings nur dann, wenn die Zahl der Hasen größer ist als 100.
kaninchen = 0 # Variablen-Definition, d.h. Zuweisung des Wertes 0 and die Variable hasen
for it in range(105):
kaninchen = kaninchen + 1
if kaninchen == 100: # Vergleich des Wertes der Variable hasen mit dem Wert 100
print("Es gibt jetzt EXAKT 100 Kaninchen!")
Dieses Beispiel demonstriert den wichtigen Unterschied zwischen dem einfachen = Zeichen und dem doppelten = Zeichen noch einmal: Das einfache = wird benutzt um einer Variable (kaninchen
) einen Wert zu zuweisen. Das doppelte = Zeichen wird dagegen für einen Vergleich verwendet.
Es können auch mehrere Befehle unter eine If-Abfrage
gestellt werden. Ähnlich wie bei der For-Schleife wird hier durch Einrücken signalisiert, ob ein Befehl Teil der If-Abfrage
ist, oder nicht:
kaninchen = 95
for it in range(10):
kaninchen = kaninchen + 1
if kaninchen > 100:
print("Es gibt jetzt mehr als 100 Kaninchen!")
print("Die exakte Zahl der Kaninchen ist ")
print(kaninchen)
Wenn wir die beiden zuletzt stehenden Befehle nicht einrücken, dann sind sie nicht mehr Teil der If-Abfrage
und werden in jedem Fall ausgeführt, egal wie viele Kaninchen es gibt:
kaninchen = 95
for it in range(10):
kaninchen = kaninchen + 1
if kaninchen > 100:
print("Es gibt jetzt mehr als 100 Kaninchen!")
print("Die exakte Zahl der Kaninchen ist ")
print(kaninchen)
Wenn wir die beiden Befehle noch einmal nach außen verschieben, stehen sie auch außerhalb der For-Schleife und werden somit nur einmal ausgeführt, und zwar nachdem die For-Schleife abgeschlossen ist:
kaninchen = 95
for it in range(10):
kaninchen = kaninchen + 1
if kaninchen > 100:
print("Es gibt jetzt mehr als 100 Kaninchen!")
print("Die exakte Zahl der Kaninchen ist ")
print(kaninchen)
Nun, da wird If-Abfragen verstehen können wir die Beschränkung auf 1000 Individuen in unser Programm einbauen: Nur wenn es noch weniger als 1000 Kaninchen gibt soll die Population weiter anwachsen:
import matplotlib.pyplot as plt
%matplotlib inline
simulationszeit = 100
#simulationszeit: Die Zeit in Jahren, die die Kaninchen simuliert werden
kaninchenanzahl = 1
kaninchenanzahl_liste = [] #leere Liste wird erstellt
kaninchenanzahl_liste.append(kaninchenanzahl) #erster Eintrag (1 Kaninchen) wird angehängt
for it in range(simulationszeit):
if kaninchenanzahl < 1000: #NUR WENN es weniger als 1000 Kaninchen gibt
kaninchenanzahl = kaninchenanzahl + kaninchenanzahl * 0.1 #neue kaninchenanzahl wird berechnet
#am ende jedes schleifendurchgangs wird die aktuelle zahl an die liste angehängt
kaninchenanzahl_liste.append(kaninchenanzahl)
plt.plot(kaninchenanzahl_liste)
Neben dem exponentiellen Wachstum gibt es aber noch andere Methoden um das Wachstum einer Population zu beschreiben. Mann kann auch von der Idee ausgehen, dass die momentane Population einerseits von der Population aus dem letzten Zeitschritt, andererseits aber auch von der Population aus dem vorletzten Zeitschritt abhängt.
Dies führt zur sogenannten
In diesem Model errechnet sich die Anzahl der Kaninchen im aktuellen Jahr aus der Anzahl der Kaninchen aus dem Vorjahr plus der Anzahl der Kaninchen aus dem vorletzen Jahr. Um so etwas zu programmieren, müssen wir aber zuerst mehr über Listen in Python lernen:
Bislang wissen wir, wie man leere Listen erstellt und Elemente zu Listen hinzufügt. Man kann aber natürlich auch spezifische Listeneinträge abfragen. Möchte man beispielsweise das siebente Element der Liste kaninchenanzahl_liste
wissen, kann man es mit kaninchenanzahl_liste[6]
abrufen.
Warum nicht 7?
In Programmiersprachen ist es üblich, das erste Element einer Liste stets mit der Nummer 0 zu speichern, in diesem Fall also als kaninchenanzahl_liste[0]
. Das ist anfangs zwar verwirrend, für spätere Anwendungen jedoch eine recht sinnvolle Konvention.
kaninchenanzahl_liste[0]
For-Schleifen haben wir schon kennen gelernt, eine wichtige Eigenschaft haben wir aber bislang ignoriert. Die Zahl it, die sozusagen mitzählt wie oft ein Befehl schon ausgeführt worden ist, darf auch innerhalb eines Befehls benutzt werden:
for it in range(5):
print(it)
Wenn wir diese beiden Kenntnisse nun kombinieren, haben wir eine Möglichkeit gefunden, wie wir innerhalb einer Schleife auf vorherige Listeneinträge zugreifen können. Das ist genau das, was wir für unsere Fibonacci-Folge benötigen.
Eine weitere wichtige Eigenschaft von for-Schleifen ist, dass it nicht unbedingt bei 0 anfangen muss. Möchten wir beispielsweise erst ab Jahr 2 simulieren, so können wir die Schleife auch erst bei Jahr zwei beginnen lassen:
for it in range(2,5):
print(it)
import matplotlib.pyplot as plt
%matplotlib inline
simulationszeit = 10
# Simulationszeit: Die Zeit in Jahren, die die Kaninchenfortpflanzung simuliert wird
kaninchenanzahl = 1
kaninchenanzahl_liste = [] #leere liste wird erstellt
kaninchenanzahl_liste.append(kaninchenanzahl) #erster Eintrag (1 Kaninchen) wird angehängt
kaninchenanzahl = 1
kaninchenanzahl_liste.append(kaninchenanzahl)#zweiter Eintrag (1 Kaninchen) wird angehängt
#wir benötigen hier zwei Kaninchen, damit sie sich auch vermehren können
for it in range(2, simulationszeit):
letztesjahr = kaninchenanzahl_liste[it - 1] # Kaninchenanzahl vom letzten jahr wird berechnet
vorletztesjahr = kaninchenanzahl_liste[it - 2] # Kaninchenanzahl vom vorletzten jahr wird berechnet
kaninchenanzahl = letztesjahr + vorletztesjahr # neue Kaninchenanzahl wird berechnet
# am Ende jedes Schleifendurchgangs wird die aktuelle Zahl an die Liste angehängt
kaninchenanzahl_liste.append(kaninchenanzahl)
plt.plot(kaninchenanzahl_liste)
Natürlich können wir auch hier eine Beschränkung des Wachstums durch eine If-Abfrage einbauen, ganz gleich wie beim exponentiellen Wachstum:
import matplotlib.pyplot as plt
%matplotlib inline
simulationszeit = 20
# Simulationszeit: Die Zeit in Jahren, die die Kaninchenfortpflanzung simuliert wird
kaninchenanzahl = 1
kaninchenanzahl_liste = [] #leere liste wird erstellt
kaninchenanzahl_liste.append(kaninchenanzahl) #erster Eintrag (1 Kaninchen) wird angehängt
kaninchenanzahl = 1
kaninchenanzahl_liste.append(kaninchenanzahl)#zweiter Eintrag (1 Kaninchen) wird angehängt
#wir benötigen hier zwei Kaninchen, damit sie sich auch vermehren können
for it in range(2, simulationszeit):
if kaninchenanzahl < 1000: #NUR WENN es weniger als 1000 Kaninchen gibt
letztesjahr = kaninchenanzahl_liste[it - 1] # Kaninchenanzahl vom letzten jahr wird berechnet
vorletztesjahr = kaninchenanzahl_liste[it - 2] # Kaninchenanzahl vom vorletzten jahr wird berechnet
kaninchenanzahl = letztesjahr + vorletztesjahr # neue Kaninchenanzahl wird berechnet
# am Ende jedes Schleifendurchgangs wird die aktuelle Zahl an die Liste angehängt
kaninchenanzahl_liste.append(kaninchenanzahl)
plt.plot(kaninchenanzahl_liste)
Was aber, wenn wir nun sozusagen in den interessanten Teil des Wachstums (zum Beispiel den Knick) hineinzoomen möchten. Dazu wird es notwendig sein, nicht nur einzelne Elemente der Liste auszuwählen, sondern ganze Stücke. Das funktioniert so:
Wir können aber auch mehrere Elemente auf einmal auswählen. Dazu schreiben wir in eckige Klammern das erste Element das wir haben möchten, dann einen Doppelpunkt, und dann das erste Element das wir nicht mehr haben wollen. Das Element mit dem Index 10 und das mit dem Index 11 bekommen wir also mit
print(kaninchenanzahl_liste[14:19])
Wird vor dem Doppelpunkt keine Zahl geschrieben, wird beim Element mit dem Index Null begonnen. Wird nach dem Doppelpunkt keine Zahl geschrieben, wird bis zum letzen Element alles ausgewählt.
print(kaninchenanzahl_liste[:3])
print(kaninchenanzahl_liste[17:])
Python akzeptiert auch negative Zahlen als Index. Der Index -1 bezeichnet dabei das letzte Element einer Liste, der Index -2 das vorletzte und so weiter. Das ist besonders praktisch, wenn man z.b. die letzten 5 Einträge der Liste auswählen möchte:
print(kaninchenanzahl_liste[-5:])
Das Auswählen von Listenteilen funktioniert natürlich nicht nur in Kombination mit dem print-Befehl, sondern auch mit allen anderen Befehlen, zum Beispiel plot. Wir können also nun auf den relevanten Teil unserer Grafik zoomen:
# Listen werden geplottet
plt.plot(kaninchenanzahl_liste[14:19])
Bei linearem Wachstum kommt zu einer Population in jedem Zeitschritt eine konstante Anzahl hinzu:
kaninchen = kaninchen + x
wobei x angibt wie viele Individuen hinzukommen, also wie schnell die Population wächst.
Bei exponentiellem Wachstum kommt zur Population kein konstanter Wert hinzu, sondern ein Wert, der direkt von der aktuellen Population abhängt. Die genaue Abhängigkeit von Wachstum und momentaner Population gibt der so genannte Wachstumsparameter an:
kaninchen = kaninchen + kaninchen * wachstumsparameter
If-Abfragen
können benutzt werden, um Anweisungen und Befehle nur unter gewissen Bedingungen ausführen zu lassen. Sie haben den Aufbau
if BEDINGUNG:
BEFEHL
Die Bedingungen sind meist Ungleichungen (hasen >= 100
) oder Gleichungen (hasen == 100
), die entweder wahr oder falsch sein können. Wichtig ist, bei einem Vergleich das doppelte =-Zeichen zu verwenden.
Die Laufvariable (im Beispiel it
) kann innerhalb der Schleife benutzt werden. So liefert die Schleife
for it in range(3):
print(it)
das Ergebnis
0
1
2
Um die einzelnen Elemente einer Liste zu verwenden oder auszugeben, benutzt man eckige Klammern:
listenname[2]
liefert zum Beispiel das dritte(!) Element. Achtung: Das erste Element in der Liste hat stets den Index 0, das zweite den Index 1, und so weiter.
Wenn wir mehrere Elemente einer Liste auswählen möchten, können wir
listenname[a:b]
verwenden, wobei a
das erste Element ist, das wir haben möchten und b
das erste, das nicht mehr Teil der Auswahl sein soll. Wenn wir a
frei lassen, wird beim Element mit Index 0 begonnen, wenn wir b frei lassen wird bis zum letzten Element ausgewählt. Der Index -1 meint immer das letze Element einer Liste, -2 das vorletzte und so weiter.
In diesem Kapitel werden wir unser Wissen über For-Schleifen und If-Abfragen weiter ausbauen, um die Schadstoffstatistik einer Firma auszuwerten. Die Schadstoffwerte sind in einer Liste gespeichert. In einem ersten Schritt sollen wir einfach eine Warnung ausgeben, wenn der Wert den Grenzwert von 100 Einheiten überschreitet. Dazu reicht eine If-Abfrage.
Da wir ein Programm haben wollen, das für Listen von jeder Länge funktioniert, können wir bei der Länge der For-Schleife nicht fix 10 schreiben, sondern lassen uns die Länge der Liste mit len
ausrechnen.
werte = [89,96,125,88,110,112,99,84,50,130]
for it in range(len(werte)): #Schleife hat die Länge der Liste
if werte[it] > 100:
print("Zu hohe Schadstoffwerte am Tag",it,"!")
Wenn unser Programm aber nur Alarm schlagen soll, wenn der Wert an zwei Tagen hintereinander überschritten wird, dann brauchen wir eine weitere If-Abfrage. Außerdem müssen wir den Tag 0 dann ausnehmen, da es ja keinen Tag -1 gibt.
werte = [89,96,125,88,110,112,99,84,50,130]
for it in range(1,len(werte)): #Schleife hat die Länge der Liste
if werte[it] > 100:
if werte[it-1] > 100:
print("Zu hohe Schadstoffwerte am Tag" ,it,"!")
Man kann sich vorstellen, dass so eine Struktur relativ schnell unübersichtlich wird, vor allem wenn man viele Bedingungen hat. Deswegen kann man solche If-Abfragen auf verkürzt schreiben: Man kann die einzelnen Bedingungen mit einem logischen und (and) und einem logischen oder (or) verknüpfen:
werte = [89,96,125,88,110,112,99,84,50,130]
for it in range(1,len(werte)): #Schleife hat die Länge der Liste
if werte[it] > 100 and werte[it-1] > 100:
print("Zu hohe Schadstoffwerte am Tag",it,"!")
Damit lassen sich auch kompliziertere Verknüpfungen erstellen, die man über runde Klammern verbinden kann. Wir könnten also zum Beispiel verlangen, dass zusätzlich eine Warnung geschrieben wird, wenn der Wert größer als 120 ist. Ist der Wert am Tag davor aber kleiner als 50, gibt es keinen Alarm. Das würde so aussehen:
werte = [89,96,125,88,110,112,99,84,50,130]
for it in range(1,len(werte)): #Schleife hat die Länge der Liste
if (werte[it] > 100 and werte[it-1] > 100) or (werte[it] > 120 and werte[it-1] > 50):
print("Zu hohe Schadstoffwerte am Tag",it,"!")
Als nächstes wollen wir die Tage, die einen Alarm ausgelöst haben, grafisch darstellen. Wir legen dazu eine Liste an, in die wir jedes mal die Zahl 1 schreiben, wenn der Alarm ausgelöst wurde, und 0 wenn er nicht ausgelöst wurde. Der erste Teil ist einfach, wir können jedes mal direkt nach dem Printbefehl eine 1 in die Liste schreiben. Aber wie identifizieren wir die Tage ohne Alarm? Wir könnten noch einmal eine ähnliche If-Abfrage bauen und einfach "umdrehen". Aber reicht es, einfach die Größer- und Kleiner-Zeichen umzudrehen? Was wenn wir genau den Grenzwert treffen? Und wie funktioniert das mit dem or? In jedem Fall wäre so eine Lösung sehr kompliziert und fehleranfällig. Deswegen gibt es eine besser Lösung: Jede if-Abfrage kann auch mit einem else ausgestattet werden, also einem Befehl, der gemacht werden soll, wenn die If-Abfrage nicht erfüllt ist. Vom Einrücken her ist dieses else auf gleicher Höhe wie das if, zu dem es gehört:
import matplotlib.pyplot as plt
%matplotlib inline
werte = [89,96,125,88,110,112,99,84,50,130]
protokoll=[]
for it in range(1,len(werte)): #Schleife hat die Länge der Liste
if (werte[it] > 100 and werte[it-1] > 100) or (werte[it] > 120 and werte[it-1] > 50):
print("Zu hohe Schadstoffwerte am Tag",it,"!")
protokoll.append(1)
else:
protokoll.append(0)
plt.plot(protokoll)
Hierbei sind wir aber von einer perfekten Datenlage ausgegangen. Echte Daten sind selten so vollständig. Man kann davon ausgehen, dass an einigen Tagen keine Messung durchgeführt wurde. Solche Tage würden dann einfach als 0 in unserer Liste auftauchen, und nicht einfach nur selbst keinen Alarm auslösen, sondern auch Auswirkungen auf den Tag danach haben:
import matplotlib.pyplot as plt
%matplotlib inline
werte = [89,96,125,88,110,0,112,99,84,50,0,130]
protokoll=[]
for it in range(1,len(werte)): #Schleife hat die Länge der Liste
if (werte[it] > 100 and werte[it-1] > 100) or (werte[it] > 120 and werte[it-1] > 50):
print("Zu hohe Schadstoffwerte am Tag" ,it, "!")
protokoll.append(1)
else:
protokoll.append(0)
plt.plot(protokoll)
Was könnten wir in so einem Fall machen? Eine Möglichkeit ist zu interpolieren, also zu versuchen die fehlenden Daten anhand der existierenden Daten abzuschätzen. Das Einfachste wäre anzunehmen, dass die fehlenden Schadstoffwerte immer genau der Mittelwert aus dem Wert vom Vortag und dem Wert vom Tag danach ist. Bereinigen wir unsere Liste auf diese Art und plotten wir die entstehenden Listen:
import matplotlib.pyplot as plt
%matplotlib inline
werte = [89,96,125,88,110,0,112,99,84,50,0,130]
werte_neu=[]
for it in range(len(werte)):
if werte[it] == 0:
mittelwert = (werte[it-1] + werte[it+1]) / 2
werte_neu.append(mittelwert)
else:
werte_neu.append(werte[it])
plt.plot(werte)
plt.plot(werte_neu)
Es gibt natürlich weiter Möglichkeiten zu interpolieren. Man könnte zum Beispiel Ableitungen betrachten, oder mehr Werte miteinbeziehen. Fürs erste bleiben wir aber bei dieser Methode und benutzen sie für unsere Auswertung:
import matplotlib.pyplot as plt
%matplotlib inline
werte = [89,96,125,88,110,0,112,99,84,50,0,130]
werte_neu=[]
for it in range(len(werte)):
if werte[it] == 0:
mittelwert = (werte[it-1] + werte[it+1]) / 2
werte_neu.append(mittelwert)
else:
werte_neu.append(werte[it])
werte = werte_neu
protokoll=[]
for it in range(1,len(werte)): #Schleife hat die Länge der Liste
if (werte[it] > 100 and werte[it-1] > 100) or (werte[it] > 120 and werte[it-1] > 50):
print("Zu hohe Schadstoffwerte am Tag",it,"!")
protokoll.append(1)
else:
protokoll.append(0)
plt.plot(protokoll)
Mit Hilfe dieser Interpolation können wir nun also auch mit Datensätzen arbeiten, die mit 0en aufgefüllt wurden. Echte Datensätz sind aber meist noch schwieriger zu handhaben. Zum Beispiel können wir auf Daten treffen, die nicht einmal eine Zahl sind, sondern einen ganz anderen Datentyp haben, zum Beispiel Zeichenketten. Das wäre noch schlimmer für unser Programm, denn es führt nicht zu falschen Ergebnissen, sondern zu einer Fehlermeldung und somit einem Absturz, wie wir hier sehen:
import matplotlib.pyplot as plt
%matplotlib inline
werte = [89,96,"keine Messung",125,88,110,0,112,"44",99,84,50,0,130,[99,88],125,"🐇",120]
werte_neu=[]
for it in range(len(werte)):
if werte[it] == 0:
mittelwert = (werte[it-1] + werte[it+1]) / 2
werte_neu.append(mittelwert)
else:
werte_neu.append(werte[it])
werte = werte_neu
protokoll=[]
for it in range(1,len(werte)): #Schleife hat die Länge der Liste
if (werte[it] > 100 and werte[it-1] > 100) or (werte[it] > 120 and werte[it-1] > 50):
print("Zu hohe Schadstoffwerte am Tag",it, "!")
protokoll.append(1)
else:
protokoll.append(0)
plt.plot(protokoll)
Das Problem hier ist, dass man eine Zeichenkette nicht mit einer Zahl vergleichen kann und es zu einer Fehlermeldung kommt. Wir müssten die Daten also vor dem Bereinigen noch einmal "vorbereinigen". Leider können wir damit nicht ausschließen, dass es noch zu weiteren Fehlermeldungen kommt. Dennoch hätten wir gerne ein Programm, das nicht abstürzt, auch wenn es einmal zu einem Fehler kommt. Hier gibts es einen guten Ausweg: Wir versuchen unsere Rechenoperation durchzuführen, aber wenn irgendwo ein Fehler auftritt, müssen wir den Wert aus der Datenbank mit dem interpolierten Wert ersetzen.
Dafür gibt es in Python try und except. Zuerst wird versucht den Teil des Codes, der unter try steht auszuführen. Wenn das aber zu einer Fehlermeldung führt, wird das gemacht, was unter except steht. Für unseren Fall würde das so aussehen:
import matplotlib.pyplot as plt
%matplotlib inline
werte = [89,96,"keine Messung",125,88,110,0,112,"44",99,84,50,0,130,[99,88],125,"🐇",120]
protokoll=[]
for it in range(1,len(werte)): #Schleife hat die Länge der Liste
try: #Dieser Codeblock wird versucht
if (werte[it] > 100 and werte[it-1] > 100) or (werte[it] > 120 and werte[it-1] > 50):
print("Zu hohe Schadstoffwerte am Tag" ,it, "!")
protokoll.append(1)
else:
protokoll.append(0)
except: #Wenn der obere Block einen Fehler verursacht, wird dieser Block ausgeführt
werte[it] = (werte[it-1] + werte[it + 1]) / 2
if (werte[it] > 100 and werte[it-1] > 100) or (werte[it] > 120 and werte[it-1] > 50):
print("Zu hohe Schadstoffwerte am Tag" ,it, "!")
protokoll.append(1)
else:
protokoll.append(0)
plt.plot(protokoll)
Auf diese Art ersparen wir uns das Vorbereinigen der Liste. Aber Achtung, die 0-Werte lösen hier keinen Fehler aus und werden somit auch nicht interpoliert. Wir könnten zwar die 0-Werte vorher gesondert ausfiltern, es geht aber auch in der gleichen try-except Struktur, die wir schon haben. Dazu ist es jedoch nötig, eine eigene Fehlermeldung zu definieren. Das funktioniert in Python mit raise Exception(). Dieser Befehl bricht das Programm ab und gibt eine Fehlermeldung aus. Innerhalb von unserem try-except führt es aber nur dazu, dass wir in den except-Block springen.
Bauen wir also unseren eigenen "Diese Zahl darf nicht 0 sein"-Fehler ein:
import matplotlib.pyplot as plt
%matplotlib inline
werte = [89,96,"keine Messung",125,88,110,0,112,"44",99,84,50,0,130,[99,88],125,"🐇",120]
protokoll=[]
for it in range(1,len(werte)): #Schleife hat die Länge der Liste
try: #Dieser Codeblock wird versucht
if werte[it] == 0:
raise Exception("Diese Zahl darf nicht 0 sein!")
if (werte[it] > 100 and werte[it-1] > 100) or (werte[it] > 120 and werte[it-1] > 50):
print("Zu hohe Schadstoffwerte am Tag" ,it, "!")
protokoll.append(1)
else:
protokoll.append(0)
except: #Wenn der obere Block einen Fehler verursacht, wird dieser Block ausgeführt
werte[it] = (werte[it-1] + werte[it + 1]) / 2
if (werte[it] > 100 and werte[it-1] > 100) or (werte[it] > 120 and werte[it-1] > 50):
print("Zu hohe Schadstoffwerte am Tag" ,it, "!")
protokoll.append(1)
else:
protokoll.append(0)
plt.plot(protokoll)
Achtung: Das Arbeiten mit try und except führt zwar zu einfachen und eleganten Lösungen, ist aber relativ gefährlich: Auch wenn man Programmierfehler macht, werden sie durch except einfach abgefangen und man sieht nicht mehr gut was passiert ist. Deswegen sollte man besonders aufpassen, wenn man mit try und except arbeitet. Dazu ist es wichtig, dass wir uns genauer ansehen, wie das mit Fehlern in Python eigentlich funktioniert:
testwerte = [99, 120, 0, 101, "k.A.", 140]
for it in range(len(testwerte)):
if testwerte[it] > 100:
print("Alarm!")
Hier wird ein Fehler ausgelöst, wenn wir zum Eintrag "k.A." kommen. Dabei handelt es sich um eine Zeichenkette, die wir also nicht mit der Zahl 100 vergleichen können. Es handelt sich also um den falschen Datentyp, deswegen heißt der Fehler "TypeError". Auch hier können wir unseren eigenen Fehlertyp einbauen:
testwerte = [99, 120, 0, 101, "k.A.", 140]
for it in range(len(testwerte)):
if testwerte[it] == 0:
raise Exception("Dieser Wert darf nicht 0 sein!")
if testwerte[it] > 100:
print("Alarm!")
Nun bekommen wir unsere individualisierte Fehlermeldung. Bauen wir nun try-except ein. Der Einfachheit halber werden wir als Alternative einfach nur Text ausgeben, der sagt, dass ein Eintrag übersprungen wurde.
testwerte = [99, 120, 0, 101, "k.A.", 140]
for it in range(len(testwerte)):
try:
if testwerte[it] == 0:
raise Exception("Dieser Wert darf nicht 0 sein!")
if testwerte[it] > 100:
print("Alarm!")
except:
print("Ein Eintrag wurde übersprungen!")
Dieses Programm ist jetzt aber recht gefährlich, denn wir sehen die Fehlermeldungen nicht mehr. Sollten wir also einen Fehler beim Programmieren machen, wird dieser genauso rausgefiltert:
testwerte = [99, 120, 0, 101, "k.A.", 140]
for it in range(len(testwerte)):
try:
if testwerte[it] == 0:
raise Exception("Dieser Wert darf nicht 0 sein!")
if testwerte > 100: #ACHTUNG: Hier ist jetzt ein Fehler eingebaut: Wir haben das [it] entfernt.
print("Alarm!")
except:
print("Ein Eintrag wurde übersprungen!")
Um so etwas zu verhindern, oder zumindest die Chance, dass es unbemerkt passiert, zu verringern, empfiehlt es sich die Fehlermeldungen, die entstehen, zumindest auszugeben. Das Programm wird nicht unterbrochen, man sieht aber trotzdem was passiert ist. Obwohl wir die Fehlermeldung also überspringen, haben wir eien Teil der Vorteile eine Fehlermeldung.
Dazu müssen wir unsere try-except Struktur ein wenig ausbauen. Wir möchten die genaue Fehlermeldung ausgeben. Dazu benutzt man except Exception as errormsg
und kann daraufhin den genauen Fehlertext, der in der Variable errormsg
gespeichert wurde, verwenden.
testwerte = [99, 120, 0, 101, "k.A.", 140]
for it in range(len(testwerte)):
try:
if testwerte[it] == 0:
raise Exception("Dieser Wert darf nicht 0 sein!")
if testwerte[it] > 100:
print("Alarm!")
except Exception as errormsg:
print("Ein Eintrag wurde übersprungen! Der Fehler war:")
print(errormsg)
Um das eigentliche Protokoll vom Fehlerprotokoll zu trennen, können wir eine eigene Liste dafür anfangen, die wir nur bei Bedarf anschauen können:
testwerte = [99, 120, 0, 101, "k.A.", 140]
fehlerlog=[]
for it in range(len(testwerte)):
print("Tag",it)
try:
if testwerte[it] == 0:
raise Exception("Dieser Wert darf nicht 0 sein!")
if testwerte[it] > 100:
print("Alarm!")
else:
print("Kein Alarm!")
except Exception as errormsg:
print("Fehler!")
fehlerlog.append(it)
fehlerlog.append("Ein Eintrag wurde übersprungen! Der Fehler war:")
fehlerlog.append(errormsg)
else: #auch das except kann ein else haben, das ausgeführt wird, wenn das except nicht ausgelöst wurde
fehlerlog.append(it)
fehlerlog.append("Fehlerfrei!")
print("Fehlerprotokoll:")
print(fehlerlog)
Auf diese Art können wir relativ gut mit Fehlern umgehen. Try-except bietet jedoch noch viel mehr Möglichkeiten: Wir könnten die Art der Fehler unterscheiden und je nach Fehlertyp andere Lösungsmöglichkeiten finden. Generell muss man beim Benutzen von try und except aber vorsichtig sein: Meist ist eine Fehlermeldung ein Problem, dass man beheben sollte, indem man am Programm so lange etwas ändert, bis eben keine Fehlermeldungen mehr entstehen. Dennoch gibt es Situationen in denen (potentielle) Fehlermeldungen unvermeidbar sind, zum Beispiel wenn Daten von außen eingelesen werden und man noch nicht wissen kann, was das für Daten sein werden. Hier ist dann sinnvoll, mit try und except vorzubeugen.
Die If-Else-Abfrage ist eine Erweiterung der If-Abfrage. Sie erweitert diese Struktur um zusätzliche Befehle, die ausgeführt werden sollen, wenn die Bedingung nicht wahr ist. Sie hat den Aufbau:
if BEDINGUNG:
BEFEHL WENN WAHR
else:
BEFEHL WENN FALSCH
Mit and und or kann man mehrere Bedingungen miteinander verknüpfen, entweder so, dass alle Bedingungen erfüllt sein müssen, oder so, dass nur eine erfüllt werden muss.
Mittels try
und except
ist es möglich ein Programm trotz Fehlermeldung weiterlaufen zu lassen. Tritt im Try-Block ein Fehler auf, springt das Programm in den Except-block. Damit wir aber totzdem noch merken, dass es zu einem Fehler kommt, ist es sinnvoll die Fehlermeldung zumindest mitzuprotokollieren. Dazu verwendet man except Exception as errormsg:
und hat dann Zugriff auf die Fehlermeldung unter dem Variablennamen errormsg
.
Wir haben in einem vorherigen Kapitel dieses Skriptums bereits die Entwicklung einer Tierpopulationen betrachtet. Dies war aber noch stark vereinfach, denn meist hat das Wachstum eine Abhängigkeit von anderen Größen, zum Beispiel der Population einer anderen Tierart. Solche Abhängigkeiten und Beinflussungen unterschiedlicher Dynamiken sind gewissermaßen der Normalfall in den Systemen, die für die Systemwissenschaften interessant sind. Der Systembegriff geht ja eben davon aus, dass interagierende, also sich wechselseitig beeinflussende Dynamiken in ihrem Zusammenwirken etwas generieren, das ohne dieses Zusammenwirken nicht, oder zumindest nicht so, beobachtet werden kann.
In Bezug auf diese sich wechselseitig beeinflussenden Dynamiken spricht man von gekoppelten Dynamiken. Und die Mathematik kennt zu ihrer Analyse die Methode der gekoppelten Differentialgleichungen. Das systemwissenschaftliche Standardbeispiel für solche gekoppelten Differentialgleichungssysteme sind Räuber-Beute-Systeme, für die es in der Regel allerdings keinen analytischen (rein mathematischen) Lösungsweg gibt. Im Folgenden wollen wir deshalb ein historisches Beispiel eines solchen Räuber-Beute-Systems mit Hilfe von Python simulieren.
Beginnen wir ähnlich, wie wir in einem früheren Kapitel Hasen simuliert haben. Innerhalb einer For-Schleife, wächste die Population von Hasen exponentiell. ZUsätzlich bauen wir auch noch eine zweite Tierart, die Luchse, ein.
# Importieren von matplotlib
import matplotlib.pyplot as plt
%matplotlib inline
# Startbedingungen für Hasen
hasen = 1000 # wie viele Hasen sind am Anfang vorhanden
hasen_liste = [] # leere Liste
hasen_liste.append(hasen) #speichert den ersten Eintrag in der Liste
hasen_wachstum = 0.01 # Wachstumsrate, wie schnell vermehren sich die Hasen
# Startbedingungen für Luchse
luchse = 100 # wie viele Luchse sind am Anfang vorhanden
luchs_liste = [] # leere Liste
luchs_liste.append(luchse) #speichert den ersten Eintrag in der Liste
luchs_wachstum = 0.005 # Wachstumsrate, wie schnell vermehren sich die Luchse
for it in range(365): # Schleife über 365 Tage
# Populuationsgleichungen:
# population = population + wachstum
# population = population + wachstumrate * population
hasen = hasen + hasen_wachstum * hasen
luchse = luchse + luchs_wachstum * luchse
# je aktuelle Population wird an die Listen angefügt
hasen_liste.append(hasen)
luchs_liste.append(luchse)
# Listen werden geplottet (lw gibt die Stärke der Linien an)
plt.plot(hasen_liste, color = "gray", lw = 2)
plt.plot(luchs_liste, color = "brown", lw = 2)
Nun müssen wir auch noch den Term in die Gleichung einbauen, der beschreibt, dass Tiere sterben. Das möchten wir elegant ausdrücken.
Wir wollen folgende Informationen in die Gleichung einbauen:
Wenn es viele Hasen gibt, können sich die Luchse schneller vermehren.
Wenn es viele Luchse gibt, sterben mehr Hasen.
Wie bauen wir das nun mathematisch ein? Ohne Beeinflussung der Tierarten untereinander wäre das
$$\Delta\ Hasen = a * Hasen - {b} * Hasen $$ $$\Delta\ Luchse = {c} * Luchse - d * Luchse $$
Nun möchten wir diese zusätzliche Abhängigkeit einbauen:
$$\Delta\ Hasen = a * Hasen - \color{darkred}{ \tilde b * Hasen * Luchse}$$ $$\Delta\ Luchse = \color{darkred}{ \tilde c * Luchse * Hasen} - d * Luchse $$
# Importieren von matplotlib
import matplotlib.pyplot as plt
%matplotlib inline
# Startbedingungen für Hasen
hasen = 1000 # wie viele Hasen sind am Anfang vorhanden
hasen_liste = [] # leere Liste
hasen_liste.append(hasen) #speichert den ersten Eintrag in der Liste
hasen_wachstum = 0.005 # Wachstumsrate, wie schnell vermehren sich die Hasen
hasen_sterben = 0.005 / 100
# Startbedingungen für Luchse
luchse = 100 # wie viele Luchse sind am Anfang vorhanden
luchs_liste = [] # leere Liste
luchs_liste.append(luchse) #speichert den ersten Eintrag in der Liste
luchs_wachstum = 0.005 / 1000
luchs_sterben = 0.01
for it in range(10 * 365): # Schleife über 10 * 365 Tage
# Populuationsgleichungen:
# population = population + wachstum - sterben
hasen = hasen + hasen_wachstum * hasen - hasen_sterben * hasen * luchse
luchse = luchse + luchs_wachstum * luchse * hasen - luchs_sterben * luchse
# je aktuelle Population wird an die Listen angefügt
hasen_liste.append(hasen)
luchs_liste.append(luchse)
# Listen werden geplottet (lw gibt die Stärke der Linien an)
plt.plot(hasen_liste, color = "gray", lw = 2)
plt.plot(luchs_liste, color = "brown", lw = 2)
Einen kleinen Fehler machen wir hierbei aber noch:
Wenn wir die neue Hasenpopulation ausrechnen, verwenden wir korrekterweise die Hasen und Luchspopulation aus dem letzten Zeitschritt. Dann überschreiben wir die Hasenzahl. Wenn wir dann also in der nächsten Zeile die Luchse berechnen, verwenden wir schon die neue Zahl der Hasen. Hier sollten wir eigentlich die Zahl vom Vortag benutzen.
Also ersetzen wir hasen
durch hasen_liste[it-1]
und luchse
durch luchs_liste[it-1]
.
Zusätzlich müssen wir mit unserer Schleife nicht mehr bei 0, sondern bei 1 beginnen.
# Importieren von matplotlib
import matplotlib.pyplot as plt
%matplotlib inline
# Startbedingungen für Hasen
hasen = 1000 # wie viele Hasen sind am Anfang vorhanden
hasen_liste = [] # leere Liste
hasen_liste.append(hasen) #speichert den ersten Eintrag in der Liste
hasen_wachstum = 0.005 # Wachstumsrate, wie schnell vermehren sich die Hasen
hasen_sterben = 0.005 / 100
# Startbedingungen für Luchse
luchse = 100 # wie viele Luchse sind am Anfang vorhanden
luchs_liste = [] # leere Liste
luchs_liste.append(luchse) #speichert den ersten Eintrag in der Liste
luchs_wachstum = 0.005 / 1000
luchs_sterben = 0.01
for it in range(1,10 * 365): # Schleife über 10 * 365 Tage
# Populuationsgleichungen:
# population = population + wachstum - sterben
hasen = hasen_liste[it-1] + hasen_wachstum * hasen_liste[it-1] - hasen_sterben * hasen_liste[it-1] * luchs_liste[it-1]
luchse = luchs_liste[it-1] + luchs_wachstum * luchs_liste[it-1] * hasen_liste[it-1] - luchs_sterben * luchs_liste[it-1]
# je aktuelle Population wird an die Listen angefügt
hasen_liste.append(hasen)
luchs_liste.append(luchse)
# Listen werden geplottet (lw gibt die Stärke der Linien an)
plt.plot(hasen_liste, color = "gray", lw = 2)
plt.plot(luchs_liste, color = "brown", lw = 2)
Diese Berechnung ist nun tagesgenau. Wenn wir genauere Ergebnisse haben möchten, könnten wir unsere Zeitschritte verkürzen, also auf Stunden, Minuten oder Sekunden. Wir könnten auch unendlich kleine Zeitschritte machen. Die Differenzengleichung würde dann zu einer Differentialgleichung werden.
Dieses System aus gekoppelten Differentialgleichungen ist allgemein bekannt. Es heißt Lotka-Volterra-System:
$$\frac{dH}{dt}=\alpha*H-\beta*H*L$$
$$\frac{dL}{dt}=\gamma*L*H-\phi*L$$
Fürs erste sind wir aber mit der Rechengenauigkeit unserer Differenzengleichungen zufrieden. Für ein Lotka-Volterra-System gibt es 3 Regeln. Wir können im Folgenden überprüfen, ob diese Regeln auch für unser System aus Differenzengleichungen gelten.
Lotka-Volterra-Regel 1
Die Räuber- und die Beute-Populationen oszillieren periodisch und zueinander zeitlich versetzt. Die Räuber-Population läuft der Beute-Population zeitlich etwas hinterher.
Um das zu überprüfen, müssen wir unsere grafische Darstellung ein wenig verbessern. Da es sehr viel weniger Luchse als Hasen gibt, ist es schwer die Werte zu vergleichen. Besser wäre es, wenn wir nicht die absolute Population, sondern die relative Population, also die aktuelle Population dividiert durch die Startpopulation darstellen.
Was wir also machen möchten, ist jeden Eintrag der Populationslisten durch 1000 bzw. durch 100 zu dividieren. Mit Listen funktioniert das leider nicht so einfach, wohl aber mit numpy-arrays. Wir konvertieren unsere Listen also zu numpy-arrays, die wir dann ohne Probleme dividieren können:
# Importieren von matplotlib
import matplotlib.pyplot as plt
%matplotlib inline
import numpy as np
# Startbedingungen für Hasen
hasen = 1000 # wie viele Hasen sind am Anfang vorhanden
hasen_liste = [] # leere Liste
hasen_liste.append(hasen) #speichert den ersten Eintrag in der Liste
hasen_wachstum = 0.005 # Wachstumsrate, wie schnell vermehren sich die Hasen
hasen_sterben = 0.005 / 100
# Startbedingungen für Luchse
luchse = 100 # wie viele Luchse sind am Anfang vorhanden
luchs_liste = [] # leere Liste
luchs_liste.append(luchse) #speichert den ersten Eintrag in der Liste
luchs_wachstum = 0.005 / 1000
luchs_sterben = 0.01
for it in range(1,10 * 365): # Schleife über 10 * 365 Tage
# Populuationsgleichungen:
# population = population + wachstum - sterben
hasen = hasen_liste[it-1] + hasen_wachstum * hasen_liste[it-1] - hasen_sterben * hasen_liste[it-1] * luchs_liste[it-1]
luchse = luchs_liste[it-1] + luchs_wachstum * luchs_liste[it-1] * hasen_liste[it-1] - luchs_sterben * luchs_liste[it-1]
# je aktuelle Population wird an die Listen angefügt
hasen_liste.append(hasen)
luchs_liste.append(luchse)
# Listen werden zu Arrays konvertiert und normiert, d.h durch die Startpopulation dividiert.
hasen_array = np.array(hasen_liste)
hasen_array = hasen_array / 1000
luchs_array = np.array(luchs_liste)
luchs_array = luchs_array / 100
# Listen werden geplottet (lw gibt die Stärke der Linien an)
plt.plot(hasen_array, color = "gray", lw = 2)
plt.plot(luchs_array, color = "brown", lw = 2)
Hier sehen wir eindeutig: Es gibt Oszillationen und das Maximum der Jägerpopulation is immer etwas nach dem Maximum der Beutepopulation. Die erste Lotka-Volterra-Regel wird also korrekt in unserem Modell wiedergegeben.
Lotka-Volterra-Regel 2
Die durchschnittlichen Größen der beiden Populationen bleiben über längere Zeiträume konstant, auch wenn die Maxima und Minima sehr unterschiedlich sind.
Um das zu überprüfen müssen wir längere Zeiträume betrachten. Innerhalb der ersten zwar Oszillationen sieht es zwar so aus, als würde die durchschnittliche Population konstant bleiben, wir sollten usn aber sicherheitshalber längere Zeiträume ansehen. Erhöhen wir den Simulationszeitraum auf 100 Jahre.
# Importieren von matplotlib
import matplotlib.pyplot as plt
%matplotlib inline
import numpy as np
# Startbedingungen für Hasen
hasen = 1000 # wie viele Hasen sind am Anfang vorhanden
hasen_liste = [] # leere Liste
hasen_liste.append(hasen) #speichert den ersten Eintrag in der Liste
hasen_wachstum = 0.005 # Wachstumsrate, wie schnell vermehren sich die Hasen
hasen_sterben = 0.005 / 100
# Startbedingungen für Luchse
luchse = 100 # wie viele Luchse sind am Anfang vorhanden
luchs_liste = [] # leere Liste
luchs_liste.append(luchse) #speichert den ersten Eintrag in der Liste
luchs_wachstum = 0.005 / 1000
luchs_sterben = 0.01
for it in range(1,100 * 365): # Schleife über 10 * 365 Tage
# Populuationsgleichungen:
# population = population + wachstum - sterben
hasen = hasen_liste[it-1] + hasen_wachstum * hasen_liste[it-1] - hasen_sterben * hasen_liste[it-1] * luchs_liste[it-1]
luchse = luchs_liste[it-1] + luchs_wachstum * luchs_liste[it-1] * hasen_liste[it-1] - luchs_sterben * luchs_liste[it-1]
# je aktuelle Population wird an die Listen angefügt
hasen_liste.append(hasen)
luchs_liste.append(luchse)
# Listen werden zu Arrays konvertiert und normiert, d.h durch die Startpopulation dividiert.
hasen_array = np.array(hasen_liste)
hasen_array = hasen_array / 1000
luchs_array = np.array(luchs_liste)
luchs_array = luchs_array / 100
# Listen werden geplottet (lw gibt die Stärke der Linien an)
plt.plot(hasen_array, color = "gray", lw = 2)
plt.plot(luchs_array, color = "brown", lw = 2)
Hier sehen wir sehr deutlich, dass das Maximum immer größer wird. Die zweite Lotka-Volterra-Regel wird in unserem Modell also verletzt. Woran liegt das? Der erste Verdacht ist unsere Vereinfachung, dass wir anstelle einer Differentialgleichung nur eine tagesgenaue Differentialgleichung benutzen. Um festzustellen ob wirklich das für dieses Verhalten verantwortlich ist, müssen wir eine bessere Methode finden, um längere Zeitentwicklungen grafisch darzustellen. Wir werden im Folgenden die sogenannte Phasenraumdarstellung benutzen.
In unseren üblichen Grafiken tragen wir immer die Zeit auf der einen Achse und die Populationen auf der anderen Achse auf. Das ist aber nicht die einzige Möglichkeit, die wir haben. Wir könnten auch die eine Achse für die Jäger und die andere Achse für die Beutetiere verwenden. Dann stellen wir nicht mehr die Zeitentwicklung darn, sondern viel mehr die Population der Jäger und Beute, die uns während der ganzen Zeitentwicklung untergekommen sind.
Sollten wir immer konstante Maxima und Miminma haben, sollte die Phasenraumdarstellung eine geschlossene Form haben, ähnlich einem Kreis. Wenn wir immer größere Werte bekommen, sehen wir eine Spirale. Machen wir nun also zur Probe einen Phasenraumplot:
plt.plot(hasen_array,luchs_array)
plt.title("Phasenraumdarstellung")
plt.xlabel("Beute")
plt.ylabel("Räuber")
Wir sehen hier also eindeutig eine Spirale, die Maxima werden also immer größer. Um festzustellen ob wirklich unsere Rechengenauigkeit das Problem verursacht, können wir diese einfach erhöhen. Verwenden wir statt ganze Tage als Zeitschritte nun das Hundertstel eines Tages. Wir müssen also 100 mal mehr Zeitschritte machen, dafür die einzelnen Änderungen pro Zeitschritt durch 100 dividieren.
# Importieren von matplotlib
import matplotlib.pyplot as plt
%matplotlib inline
import numpy as np
# Startbedingungen für Hasen
hasen = 1000 # wie viele Hasen sind am Anfang vorhanden
hasen_liste = [] # leere Liste
hasen_liste.append(hasen) #speichert den ersten Eintrag in der Liste
hasen_wachstum = 0.005 # Wachstumsrate, wie schnell vermehren sich die Hasen
hasen_sterben = 0.005 / 100
# Startbedingungen für Luchse
luchse = 100 # wie viele Luchse sind am Anfang vorhanden
luchs_liste = [] # leere Liste
luchs_liste.append(luchse) #speichert den ersten Eintrag in der Liste
luchs_wachstum = 0.005 / 1000
luchs_sterben = 0.01
for it in range(1,100 * 365 * 100): # Schleife über 10 * 365 Tage
# Populuationsgleichungen:
# population = population + wachstum - sterben
hasen = hasen_liste[it-1] + \
1/100 * (hasen_wachstum * hasen_liste[it-1] - hasen_sterben * hasen_liste[it-1] * luchs_liste[it-1])
luchse = luchs_liste[it-1] + \
1/100 * (luchs_wachstum * luchs_liste[it-1] * hasen_liste[it-1] - luchs_sterben * luchs_liste[it-1])
# je aktuelle Population wird an die Listen angefügt
hasen_liste.append(hasen)
luchs_liste.append(luchse)
# Listen werden zu Arrays konvertiert und normiert, d.h durch die Startpopulation dividiert.
hasen_array = np.array(hasen_liste)
hasen_array = hasen_array / 1000
luchs_array = np.array(luchs_liste)
luchs_array = luchs_array / 100
plt.plot(hasen_array,luchs_array)
plt.title("Phasenraumdarstellung")
plt.xlabel("Beute")
plt.ylabel("Räuber")
Nun ist die Phasenraumdarstellung wirklich eine geschlossene Figur. Die Rechengenauigkeit macht also wirklich einen Unterschied. Mit erhöhter Rechengenauigkeit ist nun also auch die zweite Lotka-Volterra-Regel erfüllt.
Lotka-Volterra-Regel 3
Werden Räuber- und Beute-Population gleichzeitig um den gleichen Prozentanteil dezimiert, so steigt der Mittelwert der Beutepopulation kurzfristig an, und der Mittelwert der Räuberpopulation sinkt kurzfristig ab.
Auch diesen Effekt können wir ganz einfach ausprobieren. Eine Periode (also die Zeit zwischen zwei Punkten, in denen sowohl Räuber als auch Beutepopulation normiert den Wert 1 haben) dauert in unserem Modell 972 Zeiteinheiten. Wir können also den Mittelwert der ersten 972 Zeiteinheiten ausrechnen, dann die Populationen dezimiren, und denn die Mittelwerte der zweiten 972 Zeitschritte ausrechnen. Mittelwerte von arrays berechnet man am besten mit dem numpy-Befehl np.mean
. Damit kann man auch den Mittelwert von bestimmten Teilen eines Arrays ausrechnen lassen. Dazu schreibt man np.mean(liste[anfang:ende])
also zum Beispiel np.mean(hasen_array[0:972])
# Importieren von matplotlib
import matplotlib.pyplot as plt
%matplotlib inline
import numpy as np
# Startbedingungen für Hasen
hasen = 1000 # wie viele Hasen sind am Anfang vorhanden
hasen_liste = [] # leere Liste
hasen_liste.append(hasen) #speichert den ersten Eintrag in der Liste
hasen_wachstum = 0.005 # Wachstumsrate, wie schnell vermehren sich die Hasen
hasen_sterben = 0.005 / 100
# Startbedingungen für Luchse
luchse = 100 # wie viele Luchse sind am Anfang vorhanden
luchs_liste = [] # leere Liste
luchs_liste.append(luchse) #speichert den ersten Eintrag in der Liste
luchs_wachstum = 0.005 / 1000
luchs_sterben = 0.01
for it in range(1,8 * 365): # Schleife über 8 * 365 Tage
# Populuationsgleichungen:
# population = population + wachstum - sterben
hasen = hasen_liste[it-1] + hasen_wachstum * hasen_liste[it-1] - hasen_sterben * hasen_liste[it-1] * luchs_liste[it-1]
luchse = luchs_liste[it-1] + luchs_wachstum * luchs_liste[it-1] * hasen_liste[it-1] - luchs_sterben * luchs_liste[it-1]
#Dezimieren der Population:
if it == 972:
hasen = hasen * 0.2
luchse = luchse * 0.2
# je aktuelle Population wird an die Listen angefügt
hasen_liste.append(hasen)
luchs_liste.append(luchse)
# Listen werden zu Arrays konvertiert und normiert, d.h durch die Startpopulation dividiert.
hasen_array = np.array(hasen_liste)
hasen_array = hasen_array / 1000
luchs_array = np.array(luchs_liste)
luchs_array = luchs_array / 100
# Listen werden geplottet (lw gibt die Stärke der Linien an)
plt.plot(hasen_array, color = "gray", lw = 2)
plt.plot(luchs_array, color = "brown", lw = 2)
Hmittel1=np.mean(hasen_array[0:972])
Hmittel2=np.mean(hasen_array[972:2*972])
Lmittel1=np.mean(luchs_array[0:972])
Lmittel2=np.mean(luchs_array[972:2*972])
print("Hasenmittelwert ändert sich von")
print(Hmittel1)
print("auf")
print(Hmittel2)
print("")
print("Luchsmittelwert ändert sich von")
print(Lmittel1)
print("auf")
print(Lmittel2)
Wir sehen, auch die dritte Lotka-Volterra-Regel wird von unserem Modell erfüllt.
Räuber-Beute-Systeme von Tierpopulationen werden mathematisch mit gekoppelten Differentialgleichungssystemen dargestellt und, weil zumeist keine analytische Lösung möglich ist, am Computer als Differenzengleichungssystem simuliert.
In dieser Darstellungsweise werden nicht mehrere Variablen gegen die Zeit aufgetragen, sondern eine Variable gegen eine andere. Bei Räuber-Beute-Systemen wird beispielsweise die Beute-Population auf der x-Achse und die Räuber-Population auf der y-Achse dargestellt. Man kann den Plot dann etwa so lesen: Zu jedem Wert der Beutepopulation auf der x-Achse, finden wir auf der y-Achse wie die Räuberpopulation unter diesen Bedingungen war. Die Zeit selbst sehen wir auf diesem Plot aber nicht.
Arrays lassen sich im Gegensatz zu Listen mit Zahlen multiplizieren und durch Zahlen dividieren. Dabei wird jedes Element im Array einzeln behandelt, ähnlich wie es bei einem Vektor der Fall wäre.
Mittelwerte von Arrays oder Teilen von Arrays kann man mit np.mean(arrayname[anfang:ende])
berechnen.
Wie wir schon in den bisherigen Kapitel gesehen haben, interessieren sich die Systemwissenschaften vor allem für dynamische Systeme, d.h. für Systeme, deren Variablen oder Zustände sich verändern. Um Veränderungen, Bewegungen oder Entwicklungen zu erfassen, stellt die Mathematik eine elaborierte Methodik bereit, die Differential- und Integralrechnung. Beginnend mit diesem Kapitel beschäftigen wir uns mit dieser für die Systemwissenschaften so zentralen Methode, dies allerdings nicht primär aus der Perspektive der Mathematik, sondern eher aus der der Möglichkeiten, die der Computer bietet.
Als Beispiele betrachten wir in diesem Kapitel den Prozess der Stromerzeugung mithilfe eines Solarpanels und den Betrieb eines Elektrofahrzeugs. Wir nehmen - um auch dies in Python kennenzulernen - an, dass dafür das Einlesen und Bearbeiten von Daten aus externen Dateien notwendig ist, d.h. aus Dateien, die wir am Computer gespeichert haben, aber noch nicht im Rahmen des Jupyter-Notebooks berücksichtigt haben (die entsprechenden Dateien findet man hier (Rechtsklick + Ziel speichern unter): solargrob.txt
, solarfein.txt
und batterie.txt
).
Zuerst interessiert uns hier, wieviel Energie ein Solarpanel liefert. In der Datei solargrob.txt
wurde mitprotokolliert, wie viel Leistung das Panel zu welcher Stunde der Tageszeit liefert. Wir benötigen also eine Methode, wie wir diese Datei, bzw. den Inhalt dieser Datei in ein Jupyter-Notebook laden, um ihn mit Python bearbeiten zu können. Für das so genannte Einlesen von Daten gibt es in Python vorgefertigte Pakete. Eines davon trägt den Namen csv
(für comma separated values), und ermöglicht es, Inhalte aus Dateien aufzubereiten.
In einem ersten Schritt laden wir dieses Paket in unser Notebook, zusammen mit dem bereits bekannten Paket matplotlib
für wissenschaftliches Zeichnen.
import csv
import matplotlib.pyplot as plt
%matplotlib inline
Im nächsten Schritt greifen wir auf die besagte Textdatei zu. (Die Datei solargrob.txt
muss dazu im gleichen Ordner wie das Jupyter-Notebook gespeichert sein).
In der Datei sind 24 Einträge, einer für jede volle Stunde eines Messtages. Jeder Eintrag gibt die Leistung des Solarpanels zu dieser Stunde in Watt an. Um eine Datei in Python zu öffnen und ihr einen Namen zu geben (z.B. inputfile
) kann man den Befehl with open(filename.txt) as inputfile
verwenden. Alle Befehle, die diesen Namen inputfile
verwenden sollen, müssen dazu eingerückt werden, ähnlich wie bei einer For-Schleife.
Wir öffnen die Datei und versuchen, ihren Inhalt mit dem print
-Befehl auszugeben:
import csv
import matplotlib.pyplot as plt
%matplotlib inline
with open('solargrob.txt') as inputfile: #solargrob.txt wird geöffnet und heißt im programm jetzt inputfile
print(inputfile) #inputfile soll geprintet werden
Wir sehen: der print
-Befehl liefert nicht das gewünschte Ergebnis. Wir sehen keine 24 Zahlen, sondern nur die Speicher-Adresse, unter der diese Datei Computer-intern geführt wird. Um den Inhalt der Textdatei zu extrahieren, ist vielmehr ein weiterer Schritt notwendig. Wir müssen über die gesamte Datei, Zeile für Zeile, iterieren und jede Zeile für sich ausgeben. Dazu benutzen wir wieder eine For-Schleife, kombiniert mit dem neuen Befehl csv.reader
, der Inhalte aus Dateien liest.
import csv
import matplotlib.pyplot as plt
%matplotlib inline
with open('solargrob.txt') as inputfile: # solargrob.txt wird geöffnet und heißt im programm jetzt inputfile
for row in csv.reader(inputfile): # für jede Zeile innerhalb von inputfile
print(row) # wird die Zeile ausgegeben
Das Ergebnis ist schon eher das, was wir erwarten. Wir können Zahlen erkennen. Die ersten Zahlen sind allerdings sehr klein (e-08 am Ende einer Zahl bedeutet, dass die vorne stehende Zahl mit 10^-8 multipliziert wird, also mit 0.00000001). Das Maximum der Zahlen liegt beim 13ten Eintrag. Es handelt sich um die Leistungsdaten eines Solarpanels.
Dennoch ist diese Ausgabe noch nicht perfekt, denn die Zahlen stehen innerhalb einiger Sonderzeichen, die uns beim Bearbeiten, also zum Beispiel beim Zeichnen eines Leistungsverlaufs, stören. Der Umstand, dass jeder Eintrag in eckigen Klammern steht, sagt uns, dass es sich bei den Einträgen um Listen handelt mit jeweils nur einem Eintrag. Hätten wir eine Datei mit mehreren Einträgen pro Zeile, wäre das vielleicht eine vernünftige Notation. In unserem Fall hätten wir aber lieber einzelne Einträge, und keine Listen der Länge 1. Wir spezifizieren also, dass wir von der ganze Zeile, immer nur den ersten (und einzigen) Eintrag, also den Eintrag mit dem Index 0 brauchen:
import csv
import matplotlib.pyplot as plt
%matplotlib inline
with open('solargrob.txt') as inputfile: # solargrob.txt wird geöffnet und heißt im programm jetzt inputfile
for row in csv.reader(inputfile): # für jede Zeile innerhalb von inputfile
print(row[0]) # wird der erste Eintrag jeder Zeile ausgegeben
Das sieht nun schon besser aus. Als nächstes sollten wir noch den Datentyp überprüfen. Beim Einlesen von Daten geht Python nämlich oftmals davon aus, dass es sich dabei um Text (einfach gesagt: um Buchstaben, so genannte strings
) handelt.
Merke: Der Datentyp gibt an, in welcher Weise ein Datum - etwa eine Zahl oder ein Buchstabe - am Computer gespeichert wird. Gewöhnlich können Programmiersprachen den Datentyp nicht selbständig erkennen. Eine '1' kann zum Beispiel als Buchstabe (string
) oder als ganze Zahl (integer
) oder auch als relle Zahl (float
) abgespeichert sein. Was für uns nahezu identisch aussieht und leicht in seiner Bedeutung verstanden wird, sind für den Computer drei völlig verschiedene Objekte.
Deswegen sollten wir den Typ der Variablen, die ausgegeben werden soll, mit dem Befehl type
abfragen. Da uns hier, um den Typ festzustellen, auch schon wenige Einträge reichen, begrenzen wir unsere Ausgabe mithilfe eines Zählers c
und einer if-Abfrage
auf fünf.
import csv
import matplotlib.pyplot as plt
%matplotlib inline
c = 0
with open('solargrob.txt') as inputfile:
for row in csv.reader(inputfile):
if c < 5:
print(row[0])
print(type(row[0])) # type ermittelt den Typ einer Variable
c = c + 1
Wie vermutet, sind die Daten als Text (d.h. als <type 'str'>
) abgespeichert, nicht aber als Zahlen, mit denen man rechnen kann. Wir müssen diesen Text also zuerst in Fließkomma- (sprich reelle) Zahlen verwandeln, damit wir damit arbeiten können. Dies geschieht in Python mit dem Befehl float
(Wir demonstrieren dies neuerlich nur für die ersten fünf Einträge und verwandeln die restlichen Einträge dann erst im nächsten Schritt unten in reelle Zahlen):
import csv
import matplotlib.pyplot as plt
%matplotlib inline
c = 0
with open('solargrob.txt') as inputfile:
for row in csv.reader(inputfile):
if c < 5:
print(float(row[0])) # float konvertiert den Text in eine Zahl
print(type(float(row[0])))
c = c + 1
Als letzten Schritt unserer Datenvorbehandlung möchten wir die Zahlen nicht einfach nur auf den Bildschirm schreiben, sondern in einer Liste speichern, damit wir damit auch wirklich arbeiten können. Wir erstellen also eine leere Liste und füllen sie sodann mit den umgewandelten Einträgen. Erst dann lesen wir die gesamte Liste mithilfe des print
-Befehls aus:
import csv
import matplotlib.pyplot as plt
%matplotlib inline
solargrob = [] # eine leere Liste wird erstellt
with open('solargrob.txt') as inputfile:
for row in csv.reader(inputfile):
solargrob.append(float(row[0])) # der erste Eintrag jeder Zeile wird als Zahl an die Liste angehängt
print(solargrob) # die gesamte Liste wird gedruckt
Nun können wir mit diesen Messdaten arbeiten. Wir können zum Beispiel einen Plot erstellen, der uns zeigt zu welcher Tageszeit das Solarpanel wie viel Leistung erbringt. Dazu können wir den Plotbefehl plt.fill
benutzen, der im Unterschied zu plt.plot
die Fläche unter einer Kurve miteinfärbt.
import csv
import matplotlib.pyplot as plt
%matplotlib inline
solargrob = []
with open('solargrob.txt') as inputfile:
for row in csv.reader(inputfile):
solargrob.append(float(row[0]))
plt.fill(range(24), solargrob, color = "orange") # fill funktioniert ähnlich wie plot, nur wird die Fläche gefüllt
plt.ylabel("Leistung / Watt")
plt.xlabel("Zeit / Stunden")
plt.xlim(0, 23)
Wir können nun den zeitlichen Verlauf der Leistung deutlich erkennen. Zur Spitzenzeit um 12h liefert das Panel 1000 Watt (W), also genau 1 Kilowatt (kW). Was aber ist seine Gesamtleistung über 24 Stunden hinweg?
Wenn das Panel 24 Stunden lang stets 1000 Watt liefern würde, wäre die Berechnung der Gesamtleistung einfach. Die wäre dann einfach
$$1 kW * 24 h = 24 kWh$$
Wie wir im Plot sehen können, ist die Leistung aber zu jeder Tageszeit anders. Wir können also nicht 24 mal den gleichen Wert aufsummieren, um zur Gesamtleistung zu kommen. Wir müssen vielmehr für jede Stunde den Wert berücksichtigen, der in unserer Liste gespeichert ist. Wir haben in dieser Liste 24 Einträge und kennen somit die Leistung zu jeder Stunde der Tageszeit. Offensichtlich sollten wir also einfach die Einträge unserer Liste aufsummieren können, um die Tagesgesamtleistung zu bestimmen. Dieses Aufsummieren bedeutet praktisch, dass wir damit die gelb gezeichnete Fläche unter der Kurve näherungsweise bestimmen. (Nur näherungsweise deswegen, weil wir ja genaugenommen nur die Leistungsdaten zu jeder vollen Stunde berücksichtigen und nicht zB um 12.30h. Siehe dazu gleich unten).
Zum Berechnen der Fläche unter einer Kurve verwendet die Mathematik die Integralrechnung. Unser "Aufsummieren einer Liste" entspricht diesem Zweck. Man spricht diesbezüglich von der Methode der numerischen Integration.
Python stellt für diesen Vorgang des Aufsummierens den einfachen Befehl sum
zur Verfügung. Im Folgenden benutzen wir diesen Befehl, um festzustellen, wieviel Leistung das Solarpanel über den Tag hinweg liefert:
import csv
import matplotlib.pyplot as plt
%matplotlib inline
solargrob = []
with open('solargrob.txt') as inputfile:
for row in csv.reader(inputfile):
solargrob.append(float(row[0]))
gesamtleistung = sum(solargrob) #sum addiert alle Elemente der Liste = numerische Integration
print(gesamtleistung)
In Summe produziert unser Solarpanel also etwa 3741 Wattstunden an einem Tag. Dieses Resultat ist freilich noch sehr ungenau. Warum? Wie gesagt, gehen wir bisher in dieser Betrachtung davon aus, dass es für jede Stunde des Tages einen fixen Leistungswert gibt. Und wir nehmen implizit an, dass dieser Wert innerhalb dieser Stunde konstant bleibt. Das ist natürlich in der Realität nicht der Fall.
Wir können den Effekt unserer Vereinfachung mit einem so genannten Bar-Plot
darstellen. Damit können wir die Daten in Form von Rechtecken darstellen, also genau so, wie auch unser Aufsummieren funktioniert. Man sieht sofort, dass das nur eine Näherungslösung sein kann:
plt.bar(range(24), solargrob, width = 1.0, color = "orange")
plt.xlim(0, 23)
plt.ylabel("Leistung / Watt")
plt.xlabel("Zeit / Stunden")
Um die Genauigkeit unserer Ergebnisse zu erhöhen, haben wir prinzipiell zwei Möglichkeiten. Entweder wir finden bessere Integrationsmethoden oder wir erhöhen einfach die Zeitauflösung unserer Messdaten.
Hier machen wir Zweiteres. Glücklicherweise können wir auf genauere Daten zurückgreifen. Zusätzlich zu der Datei, die wir schon eingelesen haben, liegt eine Datei vor, die minuten-genaue Daten enthält, die also 24 * 60 = 1440
Einträge hat. Im Folgenden lesen wir auch diese Datei in unseren Python-Code ein und erstellen einen Plot.
(Die Umrechnung von Stunden in Minuten muss hier nicht vorgezogen werden. Der Befehl range(1440)
kann auch als range(24 * 60)
geschrieben werden.)
import csv
import matplotlib.pyplot as plt
%matplotlib inline
solarfein = []
with open('solarfein.txt') as inputfile:
for row in csv.reader(inputfile):
solarfein.append(float(row[0]))
plt.fill(range(24 * 60),solarfein, color = "orange")
plt.ylabel("Leistung / Watt")
plt.xlabel("Zeit / Minuten")
plt.xlim(0, 24 * 60)
In dieser Auflösung sehen wir eine Reihe von Details, zum Beispiel, die Bewölkung, die offensichtlich den Vormittag kurz getrübt hat.
Es liegt nun nahe, auch mit diesen Daten eine numerische Integration durchzuführen, um die Gesamtleistung zu berechnen. Aber Vorsicht! Mit den gröberen Daten hatten wir 24 Zahlen aufsummiert, die größte davon war 1000. Nun wollen wir 1440 Zahlen aufsummieren, von denen wiederum die größte 1000 ist. Das kann kein ähnliches Ergebnis liefern. Offensichtlich haben wir etwas übersehen?
Für die gröberen Daten war unsere Argumentation wie folgt: Wenn das Solarpanel eine Stunde lang 1000 Watt liefert, produziert es eine Kilowattstunde. Deswegen war es zulässig, die Werte in der Liste einfach aufzuaddieren.
Nun liegen uns aber minutengenaue Daten vor. Die Aussage "Wenn ein Solarpanel eine Minute lang 1000 Watt liefert, produziert es eine Kilowattstunde." wäre falsch. Richtig ist: "Wenn ein Solarpanel eine Minute lang 1000 Watt liefert, produziert es eine Kilowattminute, also ein Sechzigstel einer Kilowattstunde."
Die Werte, die in unserer Liste stehen haben also die falschen Einheit. Gegeben sind Kilowattminuten, wir hätten aber gerne Kilowattstunden, damit wir das Ergebnis der numerischen Integration (des Aufsummierens) mit unserer ursprünglichen Berechnung vergleichen können. Wir müssen diesen Umrechnungsfaktor (1/60) also miteinbeziehen und können erst danach summieren. Dazu erstellen wir eine For-Schleife, die uns jeden Wert der ursprünglichen Liste umrechnet und in einer neuen Liste speichert. Wie lange muss diese Schleife nun aber laufen? Das kommt auf die Länge von solarfein
an. Diese Länge bekommen wir mit len(solarfein)
:
import csv
import matplotlib.pyplot as plt
%matplotlib inline
solarfein = []
with open('solarfein.txt') as inputfile:
for row in csv.reader(inputfile):
solarfein.append(float(row[0]))
solarfeinumgerechnet = []
for it in range(len(solarfein)):
solarfeinumgerechnet.append(solarfein[it]/60) # jeder Wert wird durch 60 dividiert und dann an die neue Liste gehängt
gesamtleistungfein = sum(solarfeinumgerechnet)
print(gesamtleistungfein)
Wir sehen also, dass unser ursprüngliches Ergebnis (3.7 kW) noch relativ weit vom genaueren Ergebnis (4.1 kW) entfernt war. Außerdem fällt uns bei Betrachtung des Plots auf, dass der Großteil des Stroms zur Mittagszeit produziert wird. Interessant wäre nun etwa zu wissen, wie viel Prozent des Gesamtstroms wirklich zwischen 11 und 13 Uhr erzeugt werden.
Dazu erstellen wir abermals eine Summe, integrieren also numerisch. Diesmal wollen wir aber nicht das Integral über den gesamten Zeitraum, sondern nur über einen Teil. Dazu wählen wir diesen Teil der Liste aus. Wie geschieht dies?
Wir haben schon gelernt, dass wir einzelne oder mehrere Einträge einer Liste addressieren können. listenname[4]
liefert zum Beispiel den Eintrag mit dem Index 4, also den 5-ten Eintrag einer Liste. Alle Elemente von 4 (einschließlich) bis 20 (ausschließlich) erhält man zum Beispiel durch listenname[4:20]
. Dieses Wissen können wir nun nutzen, um die Stromproduktion zur Mittagszeit zu berechnen:
import csv
import matplotlib.pyplot as plt
%matplotlib inline
solarfein = []
with open('solarfein.txt') as inputfile:
for row in csv.reader(inputfile):
solarfein.append(float(row[0]))
solarfeinumgerechnet = []
for wert in solarfein:
solarfeinumgerechnet.append(wert/60)
gesamtleistungfein = sum(solarfeinumgerechnet) #gesamte Summe
print(gesamtleistungfein)
gesamtleistungmittag = sum(solarfeinumgerechnet[11 * 60 : 13 * 60]) #Summe der Werte zwischen 11 und 13 Uhr
print(gesamtleistungmittag)
relation = gesamtleistungmittag / gesamtleistungfein * 100 #Berechnen des prozentuellen Anteils
print("Relation in Prozent:")
print(relation)
Etwa 46% des Gesamtstroms unseres Solarpanels werden also zur Mittagszeit produziert. Elektrische Geträte ausschließlich über das Solarpanel zu betreiben, scheint damit nicht ratsam, da diese Geräte nur zu Mittag gut funktionieren würden. Es könnte also vorteilhaft sein, den zu Mittag produzierten Strom in einer Batterie zu speichern, und dann wieder abzugeben, wann er gebraucht wird.
Wir könnten so zum Beispiel ein kleines Elektroauto betreiben.
Sehen wir uns diese Möglichkeit und speziell den Verbrauch dieses Fahrzeugs genauer an.
Zusätzlich zur Stromproduktion eines Solarpanels haben wir auch die Messdaten des Stromverbrauchs eines kleinen Elektrofahrzeugs zur Verfügung (laden sie die Datei batterie.txt
aus dem Moodle-Repositorium herunter und speichern sie sie im Notebook-Ordner). Für eine Testfahrt wurde das Auto voll aufgeladen und dann nach jedem gefahrenen Meter der aktuelle Batteriestand protokolliert. Das Ergebnis sind zig-tausende Datenpunkte, die wir nicht mehr einfach per Hand auswerten können. In einem ersten Schritt wollen wir die Daten wieder in unseren Python-Code einlesen und grafisch darstellen:
import csv
import matplotlib.pyplot as plt
%matplotlib inline
batterie = []
with open('batterie.txt') as inputfile:
for row in csv.reader(inputfile):
batterie.append(float(row[0]))
plt.plot(batterie, color = 'crimson', lw = 1.5)
plt.ylabel("Ladung / Wattstunden")
Anhand dieses Plots lässt sich schon einiges Über das Testfahrzeug sagen. Bei voller Batterie besitzt es eine Ladung von 16000 Wattstunden, also 16 Kilowattstunden. Um voll aufgeladen zu sein, müsste es also etwa 4 Tage lang über unser Solarpanel laden.
Außerdem sehen wir, dass das Auto bei der Testfahrt genau 100000 Meter, also 100 Kilometer zurückgelegt hat. Nach dieser Distanz beträgt die verbleibende Ladung etwa 3 Kilowattstunden, es wurden also 13 Kilowattstunden verbraucht. Durch simple Division können wir feststellen, dass der mittlere Verbrauch des E-Autos etwa 13 Kilowattstunden auf 100 Kilometer, also 0.13 Kilowattstunden auf 1 Kilometer, beträgt.
Ebenfalls fällt auf: Der Verbrauch ist nicht durchgehend gleich. Manchmal sinkt der Batteriestand schneller, manchmal langsamer. Das kann mehrere Gründe haben: starkes Beschleunigen, aber auch das Halten einer hohen Geschwindigkeit erhöhen den Verbrauch.
Wir könnten uns nun also fragen, an welchen Abschnitten der Teststrecke der Verbrauch besonders hoch, bzw. besonders gering war. Dazu müssen wir allerdings definieren, dass mit Verbrauch die Ladungsverlust-Rate der Batterie gemeint ist. Grafisch ausgedrückt: Wie steil ist die Kurve der Funktion, die den Batterieverbrauch anzeigt. Mathematisch augedrückt: Wie groß ist die Änderung bzw. die Ableitung der Funktion an jedem Punkt.
Das heißt, wir suchen die Ableitung der Funktion "Batterieladung". Die Funktion selbst sagt uns wie viel Ladung in der Batterie ist, die Ableitung sagt uns, wie sich diese Ladung verändert. Sie zeigt uns den Verbrauch. Mathematisch nennt man die Methode, um diesen Verbrauch zu ermitteln, numerisches Differenzieren.
Die Steigung einer Kurve (einer Funktion) auszurechnen, ist am Computer nicht schwierig. Wir kennen die Batterieladung an jedem Punkt unserer Teststrecke. Die Änderung der Batterieladung errechnet sich dann aus der Ladung an diesem Punkt minus der Ladung am unmittelbar nachfolgenden Punkt.
Auch für diese Art des numerischen Differenzierens gibt es einen Python-Befehl, der sich, zusammen mit vielen anderen Befehlen zur numerischen Berechnung im Paket numpy
(numerical python) findet. Wir importieren dieses Paket und benutzen den Befehl diff
um die Ableitung der Ladung, also den Verbrauch, auszurechnen.
Dabei ist es wichtig, auf das Vorzeichen zu achten: die Steigung unserer Kurve ist natürlich negativ (die Ladung wird immer weniger), der Verbrauch ist aber als positive Größe definiert. Man sagt
"Mein Auto verbraucht 5 Liter pro 100 Kilometer."
und nicht
"Der Tank meines Autos verändert sich um -5 Liter auf 100 Kilometer."
Der von uns gesuchte Verbrauch berechnet sich also als "Steigung mal -1". Im Folgenden zeichnen wir die Plots für die Batterieladung und für den Batterieverbrauch übereinander.
import numpy as np
import csv
import matplotlib.pyplot as plt
%matplotlib inline
batterie = []
with open('batterie.txt') as inputfile:
for row in csv.reader(inputfile):
batterie.append(float(row[0]))
plt.plot(batterie, color = 'crimson', lw = 1.5)
plt.ylabel("Ladung / Wattstunden")
plt.title('Batterieladung')
verbrauch = -1 * np.diff(batterie) # np.diff berechnet die Ableitung, also die Steigung. Der Verbrauch ist -1 mal Steigung
plt.figure()
plt.plot(verbrauch, color = 'crimson', lw = 1.5)
plt.ylabel("Verbrauch / Wattstunden pro Meter")
plt.xlabel("Strecke / Meter")
plt.title('Batterieverbrauch')
Im direkten Vergleich sehen wir, wie aussagekräftig der Verbrauch im Vergleich zur Ladung ist. In der Verbrauchskurve können wir direkt ablesen, dass der Verbrauch meistens etwa 0.12 Wattstunden pro Meter (= 0.12 Kilowattstunden pro Kilometer) ist. Außerdem sehen wir sehr genau an welchen Stellen der Verbrauch erhöht, bzw. gering ist. Der höchste Verbrauch auf dieser Teststrecke lag bei über 0.30 Wattstunden pro Meter.
Die Beziehung, in der in diesem Beispiel Ladung und Verbrauch der Batterie stehen, entsprich mathematisch der von Integral und Differential. Ersteres gibt einen Bestand an (Engl. "stock"), hier den Bestand an bestimmten Punkten der Wegstrecke, zweiteres bezeichnet eine Veränderung (Engl. "flow"), bzw. eine Veränderungsrate, über diese Punkte der Wegstrecke.
So wie der Verbrauch aus der Information zur Ladung an verschiedenen Punkten der Wegstrecke errechnet werden kann, so kann auch aus dem Verbrauch zurück auf die Ladung geschlossen werden. Dazu wird wieder integriert. Konzeptionell ist dies in diesem Fall allerdings etwas Anderes, als das Integral, das beim Solarpanel benutzt wurde. Beim Solarpanel haben wir die Fläche unter der Kurve errechnet. Es handelte sich da um ein so genanntes bestimmtes Integral, das als Lösung eine Zahl liefert (z.B. die Gesamtstromproduktion).
Hier hatten wir allerdings die momentane Ladung zu jedem Zeitpunkt gegeben. Wir suchen hier also keine Fläche, sondern die so genannte Stammfunktion des Verbrauchs, also die Ladung. Es handelt sich hierbei um ein unbestimmtes Integral, das als Lösung eine Funktion hat.
Das Paket numpy
enthält einen Befehl, der die Stammfunktion einer Funktion über eine Methode berechnet, die sich "kummulative Summe" nennt. Vereinfacht gesagt, wird dabei zu jedem Zeitschritt errechnet, wie groß der gesamte Verbrauch zu allen vorhergehenden Zeitschritten war. Der Befehl dazu lautet cumsum
und liefert als Ergebnis wieder eine Liste. Versuchen wir damit aus dem Verbrauch zurück auf die Ladung zu rechnen:
import numpy as np
import csv
import matplotlib.pyplot as plt
%matplotlib inline
batterie = []
with open('batterie.txt') as inputfile:
for row in csv.reader(inputfile):
batterie.append(float(row[0]))
# 1. die Ladung
# plt.plot(batterie, color = 'crimson', lw = 1.5)
# plt.ylabel("Ladung / Wattstunden")
# plt.title('Batterieladung')
# 2. der Verbrauch
verbrauch = -1 * np.diff(batterie)
plt.figure()
plt.plot(verbrauch, color = 'crimson', lw = 1.5)
plt.ylabel("Verbrauch / Wattstunden pro Meter")
plt.xlabel("Strecke / Meter")
plt.title('Batterieverbrauch')
# 3. und wieder die Ladung
batterie2 = np.cumsum(-verbrauch) # np.cumsum berechnet die kumulative Summe, um die Stammfunktion zu bestimmen
plt.figure()
plt.plot(batterie2, color = 'crimson', lw = 1.5)
plt.ylabel("Ladung / Wattstunden")
plt.xlabel("Strecke / Meter")
plt.title('Batterieladung')
Ausgehend vom Verbrauch ist es uns hier also gelungen, den Ladestand der Batterie an den einzelnen Punkten der Wegstrecke zu rekonstruieren.
Eine wichtige Information ist uns dabei allerdings verloren gegangen: wenn wir nur den Verbrauch in Betracht ziehen, können wir nicht sagen, wie viel Ladung zu Beginn in der Batterie war. In der vorliegenden Berechnung geht unser Integral davon aus, dass die Batterie am Anfang der Wegstrecke die Ladung 0 hatte, womit die Ladung insgesamt negativ wird.
Dies ist kein grundsätzlicher Fehler, sondern eine intrinsische Eigenschaft der Integralrechnung: bei Berechnung eines unbestimmten Integrals, ist das Ergebnis immer nur bis auf eine Konstante korrekt. Diese Konstante nennt man in der Mathematik "Integrationskonstante". Es ist zu beachten, dass es eine solche Konstante gibt und ein unbestimmtes Integral zwar immer die korrekte Funktion liefert, der Startwert dieser Funktion aber von Null verscheiden sein kann.
import csv
datenliste = []
with open('solargrob.txt') as inputfile:
for row in csv.reader(inputfile):
datenliste.append(float(row[0]))
Die entstehende Liste kann dann beispielsweise innerhalb einer For-Schleife weiterverarbeitet werden.
Um aus einer Liste einen Teil der Einträge auszuwählen, steht der Befehl listenname[anfang:ende]
zur Verfügung. Um beispielsweise die ersten 10 Elemente der Liste solarstrom
auszuwählen, schreibt man solarstrom[0:10]
(Vorsicht: man erhält damit die Einträge einschließlich des Elements solarstrom[0]
, aber ausschließlich des Elements solarstrom[10]
).
Um die Fläche unter einer Kurve zu berechnen, also ein bestimmtes Integral, steht der Befehl sum
zur Verfügung. Das Ergebnis eines bestimmten Integrals ist eine Zahl. Wichtig dabei ist es, die richtigen Einheiten zu verwenden, bzw. in geometrischer Interpretation, zusätzlich zur Höhe der Rechtecke, die aufsummiert werden, auch die Breite korrekt zu bestimmen (also ob ein Rechteck eine Wattstunde breit ist, oder nur 1/60 Wattstunde).
Zusätzlich zum bestimmten Integral kann auch das unbestimmte Integral berechnet werden. Das unbestimmte Integral liefert als Lösung immer eine Funktion, also in unserem Fall eine Liste. Zum Berechnen eines unbestimmten Integrals, also einer Stammfunktion, steht der Befehl cumsum
aus dem Paket numpy
zur Verfügung. Die resultierende Stammfunktion ist aber immer nur bis auf eine sogenannte Integrationskonstante korrekt.
Um die Ableitung (also die Steigung in jedem Punkt) einer Funktion oder Zeitreihe zu berechnen, um also zu differenzieren, steht der Befehl diff
aus dem Paket numpy
zur Verfügung. So wie im vorliegenden Beispiel aus der Batterieladung der Verbrauch errechnet wurde, so lässt sich analog zum Beispiel auch ein zurückgelegter Weg in Geschwindigkeit oder eine Geschwindigkeit in Beschleunigung umrechnen.
In den bisher betrachteten Beispielen systemwissenschaftlicher Modellbildung sind wir davon ausgegangen, dass wir die Daten, die wir dafür benötigen, kennen, dass wir also zum Beispiel wissen, wie groß die Wachstumsrate oder auch die Anfangsgröße einer Tierpopulation ist. Dies ist leider aber nicht immer der Fall. Oftmals haben wir bestenfalls eine mehr oder wenigerer gute Vorstellung über ein Intervall, innerhalb dessen sich die benötigten Daten bewegen. In diesen Fällen können wir Zufallszahlen heranziehen, um unser Modell an realistische Gegebenheiten anzunähern. Zum Glück stellt uns auch dafür Python die nötigen Werkzeuge zur Verfügung.
Um dies zu demonstrieren, wollen wir uns im Folgenden mit einem Energienetzwerk beschäftigen, bestehend aus Wind-, Wasser- und Kohlekraftwerken. Wir werden die Produktion dieser Kraftwerke dem Verbrauch gegenüberstellen und Strategien finden, wie man den Bedarf möglichst gut decken kann, und möglichst wenig fossile Brennstoffe verwenden zu müssen.
Wir beginnen unser diesbezügliches Modell ganz einfach mit einer For-Schleife, die Stunde für Stunde über den Simulationszeitraum iteriert. In dieses Grundgerüst bauen wir auch schon ein Wasserkraftwerk ein, das konstant Strom liefert. Um den produzierten Strom grafisch darzustellen, verwenden wir einen so genannten Stack-Plot
:
import matplotlib.pyplot as plt
%matplotlib inline
simulationszeit = 100
wasserkraft_liste = []
for zeit in range(simulationszeit):
wasserkraft = 100
wasserkraft_liste.append(wasserkraft)
plt.stackplot(range(simulationszeit), wasserkraft_liste)
Ein Wasserkraftwerk, das konstant die selbe Menge Strom liefert ist zwar parktisch, leider aber nicht realistisch. In der Realität wird es immer kleine Schwankungen geben, die wir auch in unsere Simulation einbauen sollten. Dazu benutzen wir Zufallszahlen.
Für unser Wasserkraftwerk nehmen wir an, dass die Leistung meistens etwa 100 MW beträgt. Kleine Abweichungen werden häufiger sein, große Abweichungen seltener. Wir haben es also mit einer so genannten Normal- oder auch Gauß-Verteilung zu tun. Gauß-verteilte Zufallszahlen generiert man in Python mit dem Befehl random.gauss(mittelwert, standardabweichung)
aus dem Packet random
, das wir zunächst importieren müssen. Als Mittelwert verwenden wir 1 (mal 100 MW), als Standardabweichung können wir 0.1 benutzen (also 10%). Vereinfacht gesagt heißt das, dass die Zahlen, die damit generiert werden, im Durchschnitt 10% vom Mittelwert entfernt liegen, und zwar, je größer die Abweichung ist, umso seltener.
import matplotlib.pyplot as plt
import random
%matplotlib inline
simulationszeit = 100
wasserkraft_liste = []
for zeit in range(simulationszeit):
wasserkraft = 100 * random.gauss(1, 0.1)
wasserkraft_liste.append(wasserkraft)
plt.stackplot(range(simulationszeit), wasserkraft_liste)
Neben einem Wasserkraftwerk möchten wir nun auch ein Windkraftwerk in unser Stromnetz einbauen. Die Leistung von Windkraftwerken ist stärker vom Zufall abhängig: bei Windstille produzieren sie gar nichts, aber auch zu starker Wind macht ihnen Probleme. Ein Gauß-Verteilung ist hier also nicht sinnvoll, wenn wir abbilden wollen, dass ein Windkraftwerk hin und wieder auch keinen Strom liefert.
Wir können dazu aber gleich-verteilte Zufallszahlen heranziehen. Bei gleichverteilten Zufallszahlen ist jede Zahl gleich wahrscheinlich. Der folgende Plot illustriert den Unterschied zwischen Gauß-verteilten und gleich-verteilten Zufallszahlen. Wir verwenden den Befehl random.uniform(0, 20)
, um reelle Zahlen zwischen 0 und (ausschließlich) 20 zu erzeugen. Diese werden zu 90 hinzu addiert, um eine ebenfalls um 100 variierende Verteilung zu erzeugen. Zum Plotten verwenden wir hier den Befehl hist
, der ein Histogramm der erzeugten Daten zeichnet.
gauss=[]
gleich=[]
for it in range(500000):
gauss.append(random.gauss(100, 10))
gleich.append(90 + random.uniform(0, 20))
plt.hist(gauss)
plt.title('Gauss-verteilt')
plt.figure()
plt.hist(gleich);
plt.title('Gleich-verteilt')
Unser Windkraftwerk soll eine maximale Leistung von 7 MW besitzen. Bei Windstille kann es aber vorkommen, dass auch gar kein Strom geliefert wird. Wir verwenden für die Leistung des Windkraftwerks also eine Zufallszahl zwischen 0 und 7, die wir mit dem Befehl random.uniform()
erzeugen. Pythons Stackplot liefert uns die bequeme Möglichkeit, die zusätzlich zu den 100% Wasserkraft erzeugte Windenergie einfach grafisch auf die Wasserkraft auf zu addieren. Zur Unterscheidung der beiden Energiearten verwenden wir zwei unterschiedliche Blautöne: darkblue
und skyblue
.
import matplotlib.pyplot as plt
import random
%matplotlib inline
simulationszeit = 100
wasserkraft_liste = []
windkraft_liste = []
for zeit in range(simulationszeit):
wasserkraft = 100 * random.gauss(1, 0.1)
wasserkraft_liste.append(wasserkraft)
windkraft = random.uniform(0, 7)
windkraft_liste.append(windkraft)
# '#cceeff' steht für ein etwas helleres blau
plt.stackplot(range(simulationszeit), wasserkraft_liste, windkraft_liste, colors = ('darkblue', 'skyblue'))
Wir sehen, dass der Beitrag eines einzelnen Windkraftwerks, im Vergleich zu einem Wasserkraftwerk, nur eher wenig ausmacht. Wir sollten unser Modell also ausbauen, sodass wir die Zahl der jeweiligen Kraftwerke bestimmen können. Das wird es uns später erlauben zu bestimmen, wie viele Kraftwerke wir von welchem Typ benötigen.
Wir gehen hier im Folgenden zunächst von 10 Windkraftwerken und einem Wasserkarftwerk aus.
import matplotlib.pyplot as plt
import random
%matplotlib inline
simulationszeit = 100
windkraft_anzahl = 10
wasserkraft_anzahl = 1
wasserkraft_liste = []
windkraft_liste = []
for zeit in range(simulationszeit):
wasserkraft = wasserkraft_anzahl * 100 * random.gauss(1, 0.1)
wasserkraft_liste.append(wasserkraft)
windkraft = windkraft_anzahl * random.uniform(0, 7)
windkraft_liste.append(windkraft)
plt.stackplot(range(simulationszeit), wasserkraft_liste, windkraft_liste, colors=('darkblue', 'skyblue'))
Sollten wir nun als Vorgabe haben, dass zu jeder Zeit mindestens 150 MW Strom verfügbar sein sollen, so sehen wir, dass die Anzahl unserer Windkraftwerke noch nicht ausreicht:
import matplotlib.pyplot as plt
import random
%matplotlib inline
simulationszeit = 100
windkraft_anzahl = 10
wasserkraft_anzahl = 1
wasserkraft_liste = []
windkraft_liste = []
for zeit in range(simulationszeit):
wasserkraft = wasserkraft_anzahl * 100 * random.gauss(1, 0.1)
wasserkraft_liste.append(wasserkraft)
windkraft = windkraft_anzahl * random.uniform(0, 7)
windkraft_liste.append(windkraft)
plt.stackplot(range(simulationszeit), wasserkraft_liste, windkraft_liste, colors = ('darkblue', 'skyblue'))
# zeichne eine rote Linie in der Höhe von 150
plt.plot((0, 100),(150, 150), c = "red", lw = 2)
Wir müssen die Anzahl an Windkraftwerken also noch ein wenig erhöhen:
import matplotlib.pyplot as plt
import random
%matplotlib inline
simulationszeit = 100
windkraft_anzahl = 30
wasserkraft_anzahl = 1
wasserkraft_liste = []
windkraft_liste = []
for zeit in range(simulationszeit):
wasserkraft = wasserkraft_anzahl * 100 * random.gauss(1, 0.1)
wasserkraft_liste.append(wasserkraft)
windkraft = windkraft_anzahl * random.uniform(0, 7)
windkraft_liste.append(windkraft)
plt.stackplot(range(simulationszeit), wasserkraft_liste, windkraft_liste, colors = ('darkblue', 'skyblue'))
plt.plot((0, 100), (150, 150), c = "red", lw = 2)
Wir sehen, dass, auch wenn wir 30 Windkraftwerke vorsehen und wir damit zu manchen Zeiten doppelt so viel Strom produzieren wie benötigt, es immer noch Zeiten gibt, zu denen wir die 150 MW Marke nicht erreichen. Wo liegt das Problem?
Das Problem liegt in der Eigenart der Windenergie. Bei Windstille produziert keines der 30 Windkraftwerke Strom. Wir haben beim Erstellen unseres Modells implizit angenommen, dass alle Windkraftwerke in der selben Region liegen und damit von den selben Windverhältnissen betroffen sind. In unserem Programmcode verwenden alle Kraftwerke die gleiche Zufallszahl: Wenn also in einer Stunde "Windstille" gewürfelt wird, dann gilt das für alle Kraftwerke.
Wir könnten aber auch annehmen, dass die Krafwerke so weit voneinander entfernt stehen, dass es für jedes Kraftwerk andere Windverhältnisse gibt. Aber ändert das etwas? Wird dann im Mittel nicht genau gleich viel Strom produziert?
Probieren wir es aus: Ändern wir unser Programm, sodass für jedes Kraftwerk eine eigene Zufallszahl vorgibt, wieviel Strom gerade produziert wird.
import matplotlib.pyplot as plt
import random
%matplotlib inline
simulationszeit = 100
windkraft_anzahl = 30
wasserkraft_anzahl = 1
wasserkraft_liste = []
windkraft_liste = []
for zeit in range(simulationszeit):
wasserkraft = wasserkraft_anzahl * 100 * random.gauss(1, 0.1)
wasserkraft_liste.append(wasserkraft)
windkraft = 0
for kraftwerke in range(windkraft_anzahl):
windkraft = windkraft + random.uniform(0, 7)
windkraft_liste.append(windkraft)
plt.stackplot(range(simulationszeit), wasserkraft_liste, windkraft_liste, colors = ('darkblue', 'skyblue'))
plt.plot((0, 100), (150, 150), c = "red", lw = 2)
Wir sehen: obwohl im Mittel gleich viel Strom produziert wird, macht die Annahme, dass die Windkraftwerke von unterschiedlichen Windverhältnissen betroffen sind, einen großen Unterschied: wenn wir für jedes Kraftwerk eine eigene Zufallszahl erzeugen, mittelt sich das Ergebnis besser. Es gibt weniger Spitzen und weniger Senken, die Stromproduktion ist näher am Mittelwert.
Das ist eine grundlegende Eigenschaft von Zufallszahlen: Wenn man sehr viele davon verwendet, ist das Ergebnis immer sehr nahe am Mittelwert der möglichen Resultate.
Mit dem nun vorliegenden Modell schaut es also so aus, als würden wir sicherstellen können, dass immer 150 MW Strom verfügbar sind. Was passiert aber, wenn wir miteinbeziehen, dass Windkraftwerke einmal defekt werden können? Wie bauen wir so ein Zufallsereignis in unsere Simulation ein?
Wir haben bisher gelernt, wie man zufällige Zahlen erstellt. Was aber, wenn wir keine Zahl erzeugen wollen, sondern ein Ereignis - z.B. einen Defekt in einem Windkraftwerk -, das mit bestimmter Wahrscheinlichkeit auftritt? Wir können dazu Zufallszahlen mit If-Abfragen
kombinieren.
Dazu benutzen wir den Umstand, dass 10% der Zahlen von 1 bis 100 kleiner oder gleich 10 sind. Und 50% der Zahlen sind kleinergleich 50. Im Code könnten wir also schreiben:
chance = 50
zufallszahl = random.uniform(0, 100)
if zufallszahl <= chance:
print("KOPF!")
else:
print("ZAHL!")
Das heißt, wir vergleichen hier die prozentuelle Chance eines Zufallsereignisses mit einer Zufallszahl und können somit sicher sein, dass dieses Ereignis auch mit genau dieser Chance passiert.
Bauen wir so eine Chance in unsere Simulation ein. Wir nehmen an, dass jede Stunde eine 10%-Chance besteht, dass ein Defekt im Windkraftwerk für einen Ausfall der Stromversorgung sorgt:
import matplotlib.pyplot as plt
import random
%matplotlib inline
simulationszeit = 100
windkraft_anzahl = 30
wasserkraft_anzahl = 1
wasserkraft_liste = []
windkraft_liste = []
for zeit in range(simulationszeit):
wasserkraft = wasserkraft_anzahl * 100 * random.gauss(1, 0.1)
wasserkraft_liste.append(wasserkraft)
windkraft = 0
for kraftwerke in range(windkraft_anzahl):
if random.uniform(0, 100) <= 10:
# STÖRUNG: kein Strom wird produziert
windkraft = windkraft + 0
else:
# Normalbetrieb:
windkraft = windkraft + random.uniform(0, 7)
windkraft_liste.append(windkraft)
plt.stackplot(range(simulationszeit), wasserkraft_liste, windkraft_liste, colors = ('darkblue', 'skyblue'))
plt.plot((0, 100), (150, 150), c = "red", lw = 2)
Wir sehen, dass wir, obwohl es manchmal schon knapp wird, die Stromversorgung meistens noch sicherstellen können. Erhöhen wir aber die Ausfallchance zum Beispiel auf 50%, so sieht die Sache anders aus.
import matplotlib.pyplot as plt
import random
%matplotlib inline
simulationszeit = 100
windkraft_anzahl = 30
wasserkraft_anzahl = 1
wasserkraft_liste = []
windkraft_liste = []
for zeit in range(simulationszeit):
wasserkraft = wasserkraft_anzahl * 100 * random.gauss(1, 0.1)
wasserkraft_liste.append(wasserkraft)
windkraft = 0
for kraftwerke in range(windkraft_anzahl):
if random.uniform(0, 100) <= 50:
# STÖRUNG: kein Strom wird produziert
windkraft = windkraft + 0
else:
# Normalbetrieb:
windkraft = windkraft + random.uniform(0, 7)
windkraft_liste.append(windkraft)
plt.stackplot(range(simulationszeit), wasserkraft_liste, windkraft_liste, colors = ('darkblue', 'skyblue'))
plt.plot((0, 100), (150, 150), c = "red", lw = 2)
Um noch etwas mehr über Zufallsgeneratoren zu lernen, wollen wir im Folgenden noch ein weiteres Kraftwerk in unser Stromnetz einbauen. Wir nehmen dazu ein eher unzuverlässiges Kohlekraftwerk an, das nur drei Betriebsmodi hat: "Stillstand" (0 MW), "Normalbetrieb" (20 MW) und "Hochbetrieb" (70 MW). In unserem Beispiel treten diese drei Betriebsmodi zufällig auf, kein anderer MW-Wert kann produziert werden.
Wie würden wir das am besten umsetzen? Eine Zufallszahl zwischen 0 und 70 wäre schon einmal falsch, 3 aufeinanderfolgende 33%-Chancen wären auch nicht richtig, da in dem Fall ja mehrere Betriebsmodi auf einmal möglich wären. In Pyhton gibt es glücklichweise eine ganz einfache Lösung für solche Probleme: der Befehl random.choice((a, b, c))
trifft eine zufällige Entscheidung zwischen a, b und c, wobei a, b und c nicht einmal Zahlen sein müssen. Bauen wir damit das Kraftwerk in unsere Simulation ein.
import matplotlib.pyplot as plt
import random
%matplotlib inline
simulationszeit = 100
windkraft_anzahl = 30
wasserkraft_anzahl = 1
wasserkraft_liste = []
windkraft_liste = []
kohlekraft_liste = []
for zeit in range(simulationszeit):
wasserkraft = wasserkraft_anzahl * 100 * random.gauss(1, 0.1)
wasserkraft_liste.append(wasserkraft)
windkraft = 0
for kraftwerke in range(windkraft_anzahl):
if random.uniform(0, 100) <= 50:
# STÖRUNG: kein Strom wird produziert
windkraft = windkraft + 0
else:
# Normalbetrieb:
windkraft = windkraft + random.uniform(0, 7)
windkraft_liste.append(windkraft)
kohlekraft = random.choice((0, 20, 70))
kohlekraft_liste.append(kohlekraft)
plt.stackplot(range(simulationszeit), kohlekraft_liste, wasserkraft_liste, windkraft_liste,
colors = ('black', 'darkblue','skyblue'))
plt.plot((0, 100), (150, 150), c = "red", lw = 2)
Freilich gibt es noch Probleme, die wir mit unserer bisherigen Kenntnis von Zufallszahlen noch nicht darstellen können: Nehmen wir etwa an, es gäbe innerhalb des Simulationszeitraumes eine Stunde, in der das Stromnetz eine besondere Last erfährt. Der Verbrauch steigt hierbei auf 200 MW. Wir wissen jedoch nicht, wann genau diese Stunde sein wird, wir wissen nur dass es eine solche Stunde gibt.
Wäre diese Stunde ganz am Anfang der Simulationszeit, wäre es ein einfaches Problem:
import matplotlib.pyplot as plt
import random
%matplotlib inline
simulationszeit = 100
windkraft_anzahl = 30
wasserkraft_anzahl = 1
wasserkraft_liste = []
windkraft_liste = []
kohlekraft_liste = []
verbrauch_liste = []
for zeit in range(simulationszeit):
verbrauch_liste.append(150)
wasserkraft = wasserkraft_anzahl * 100 * random.gauss(1, 0.1)
wasserkraft_liste.append(wasserkraft)
windkraft = 0
for kraftwerke in range(windkraft_anzahl):
if random.uniform(0, 100) <= 50:
# STÖRUNG: kein Strom wird produziert
windkraft = windkraft + 0
else:
#Normalbetrieb:
windkraft = windkraft + random.uniform(0, 7)
windkraft_liste.append(windkraft)
kohlekraft= random.choice((0, 20, 70))
kohlekraft_liste.append(kohlekraft)
plt.stackplot(range(simulationszeit), kohlekraft_liste, wasserkraft_liste, windkraft_liste,
colors = ('black', 'darkblue', 'skyblue'))
#erstes element von verbrauch_liste auf 200 setzen:
verbrauch_liste[0] = 200
plt.plot(verbrauch_liste, c= "red", lw = 2)
Was aber, wenn diese überhöhte Energienachfrage einfach irgendwann auftritt? In diesem Fall hilft uns der Python-Befehl random.shuffle(liste)
. Dieser mischt die Einträge einer Liste durcheinander. Damit kann man nicht nur Karten mischen...
karten = ['Herz 2', 'Herz 3', 'Herz 4','Herz 5','Herz 6','Herz 7']
random.shuffle(karten)
print(karten)
... sondern auch unser oben genanntes Problem lösen:
import matplotlib.pyplot as plt
import random
%matplotlib inline
simulationszeit = 100
windkraft_anzahl = 30
wasserkraft_anzahl = 1
wasserkraft_liste = []
windkraft_liste = []
kohlekraft_liste = []
verbrauch_liste = []
for zeit in range(simulationszeit):
verbrauch_liste.append(150)
wasserkraft = wasserkraft_anzahl * 100 * random.gauss(1, 0.1)
wasserkraft_liste.append(wasserkraft)
windkraft = 0
for kraftwerke in range(windkraft_anzahl):
if random.uniform(0, 100) <= 50:
# STÖRUNG: kein Strom wird produziert
windkraft = windkraft + 0
else:
# Normalbetrieb:
windkraft = windkraft + random.uniform(0, 7)
windkraft_liste.append(windkraft)
kohlekraft= random.choice((0, 20, 70))
kohlekraft_liste.append(kohlekraft)
plt.stackplot(range(simulationszeit), kohlekraft_liste, wasserkraft_liste, windkraft_liste,
colors=('black','darkblue','skyblue'))
# erstes Element der verbrauch_liste auf 200 setzen:
verbrauch_liste[0]=200
# shuffeln der gesamten Liste
random.shuffle(verbrauch_liste)
plt.plot(verbrauch_liste, c = "red", lw = 2)
Zufallszahlen werden in Python mit dem Paket random
generiert. Es gibt verschiedene Befehle, die unterschiedliche Verteilungen von Zufallszahlen erzeugen.
Gleich-verteilte (reelle) Zufallszahlen erstellt man mit random.uniform(a, b)
, wobei a
die kleinste mögliche Zahl ist und b
die obere Grenze markiert, bis zu der (aber ausschließlich der) Zufallszahlen erzeugt werden. Alle Zahlen von a
bis (exklusive) b
treten gleich wahrscheinlich auf. Somit lassen sich auch prozentuelle Wahrscheinlichkeiten leicht implementieren: Da 10% aller Zahlen zwischen 0 und 100 kleiner als 10 sind, und in gleichverteilten Zufallszahlen alle Zahlen gleich wahrscheinlich sind, wird die if-Abfrage if random.uniform(0, 100) < 10
in etwa 10 Prozent der Fälle true
liefern.
Normal-verteilte Zufallszahlen erstellt man mit random.gauss(m, s)
, wobei m
den Mittelwert der erzeugten Verteilung angibt und s
die Standardabweichung. Der genaue Wertebereich ist also schwer abzuschätzen, auch bei random.gauss(100, 10)
ist es theoretisch möglich, einen Wert kleiner als 50 zu bekommen. Die Chance dafür ist aber verschwindend gering. Je näher die Zahl am Mittelwert ist, umso wahrscheinlicher tritt sie auf. Rund 70% der Werte werden im Intervall m $\pm$ s liegen, also in diesem Beispiel zwischen 90 und 110. Etwa 95% liegen im Intervall m $\pm$ 2s (also 80 bis 120) und über 99% im Intervall m $\pm$ 3s (also 70 bis 130).
Bei jedem Aufruf einer Zufallsfunktion wird eine neue Zahl generiert. Wenn man ein Programm mit Zufallszahlen mehrmals hintereinander ausführt, bekommt man im Allgemeinen unterschiedliche Ergebnisse.
Zufallsereignisse
Auch zufällige Ereignisse erzeugt man am besten mit Zufallszahlen: Da x% aller Zahlen von 1 bis 100 kleiner sind als x, können wir mit if random.uniform(0, 100) <= chance
jede beliebige prozentuelle Chance simulieren.
Random Choice
random.choice
wählt ein zufälliges Element von mehreren Elementen aus. Jedes Element ist gleich wahrscheinlich und die Elemente müssen nicht zwangsläufig Zahlen sein.
Random Shuffle
random.shuffle
mischt eine Liste. Die Elemente bleiben gleich, die Position der Elemente ändert sich. Das ist hilfreich, wenn bekannt ist, welcher Wert der Liste wie oft auftritt, die genaue Position des Wertes aber zufällig sein soll.
Listen, die aus vielen Zahlen bestehen, können wir als Histogramme darstellen. Histogramme zeigen uns welche Zahlenbereiche häufiger vorkommen udn welche weniger häufig. Ein großer Balken in einem Hisogramm zeigt als nicht, dass die Zahl groß ist, sondern dass Zahlen aus diesem Bereich häufig sind. Histogramme erstellt man mit plt.hist(NameDerListe)
Eine sehr zentrale Methode für den Umgang mit Daten und Inhalten in den Systemwissenschaften stellt das Rechnen mit Vektoren und Matrizen dar. Python bietet auch hierfür die benötigten Werkzeuge. Um diese kennenzulernen, beschäftigen wir uns im folgenden Beispiel mit der nachhaltigen Bewirtschaftung eines Waldes. Uns interessiert insbesondere, wie der Wald (nach)wächst.
Beginnen wir mit einem ganz einfachen, eindimensionalem Wald-Modell: Wir betrachten also nur eine "Reihe" von Bäumen. In unserem Modell brauchen Bäume Platz um gut wachsen zu können, zwei Bäume dürfen also nie an benachbarten Stellen stehen. Dazu legen wir mit Python eine Liste an, in die wir eine Eins schreiben, wenn sich an einer entsprechenden Stelle ein Baum befindet, und eine Null, wenn nicht. Der Einfachheit halber wechseln wir zunächst einfach ab: Baum, kein Baum, Baum, kein Baum,....
import matplotlib.pyplot as plt
%matplotlib inline
wald1d = []
for posx in range(15):
wald1d.append(1)
wald1d.append(0)
plt.bar(range(len(wald1d)), wald1d, color='green')
So sieht der Wald aber noch sehr "künstlich" aus. Wir könnten den Wald auch ein wenig zufälliger wachsen lassen: Wir könnten links beginnen und dann Feld für Feld mit einer gewissen Wahrscheinlichkeit einen Baum wachsen lassen:
import matplotlib.pyplot as plt
# ein Paket zum Erzeugen von und Rechnen mit Zufallszahlen
import random
%matplotlib inline
wald1d = []
for posx in range(30):
if random.uniform(0, 100) <= 50:
wald1d.append(1)
else:
wald1d.append(0)
plt.bar(range(len(wald1d)), wald1d, color = 'green')
So sieht der Wald zwar zufällig aus, aber noch nicht richtig natürlich: Bäume wachsen nicht so gerne direkt nebeneinander, da sie sich dabei gegenseitig Schatten machen. Wie können wir diesen Umstand in unser Waldmodell einbauen? Die Wahrscheinlichkeit, dass an einer bestimmten Position ein Baum wachsen kann, hängt scheinbar davon ab, ob an der Position daneben schon ein Baum steht oder nicht. Für die Entscheidung für einen Baum an Position x
, brauchen wir also Daten zu Position x - 1
.
Dies ist kein Problem. Wir haben unseren ganzen Wald in einer Liste, also mathematisch gesehen in einem Vektor gespeichert. Wir erhalten diese Information also, indem wir einfach das je vorherige Vektorelement betrachten. Nehmen wir an, dass ein Baum nur noch eine 20%-Chance hat zu wachsen, wenn an der vorhergehenden Position schon ein Baum steht. Dagegen könnte die Chance auf 90% steigen, wenn das Nachbarfeld frei ist. Das könnten wir wie folgt in unseren Python-Code einbauen:
import matplotlib.pyplot as plt
import random
%matplotlib inline
wald1d = [1]
for posx in range(1, 30):
# Beachte: das Zeichen == steht in den meisten Programmiersprachen für eine tatsächliche Gleichheit,
# während das Zeichen = für eine Zuweisung eines Werte zu einer Variable steht.
if wald1d[posx - 1] == 1:
if random.uniform(0,100) <= 20:
wald1d.append(1)
else:
wald1d.append(0)
else:
if random.uniform(0,100) <= 90:
wald1d.append(1)
else:
wald1d.append(0)
plt.bar(range(len(wald1d)), wald1d, color = 'green')
Das wäre also unser ein-dimensionaler Wald. Wir wollen ihn nun auf zwei Dimensionen erweitern.
Bisher haben wir unseren "Wald" in einer Liste, bzw. mathematisch in einem Vektor gespeichert. In der Mathematik ist ein Vektor ein Schema aus untereinanderstehenden Zahlen. Ein ganzer Wald besteht aber in der Regel aus mehreren Reihen von Bäumen. Um ihn darzustellen, benötigen wir für jede Baumreihe einen eigenen Vektor. Einfacher und übersichtlicher ist es, wenn wir diese Zahlenreihen nebeneinanderschreiben. Damit entsteht ein zweidimensionales Zahlenschema, eine sogenannte Matrix:
Ein 4x4 Wald in Vektorschreibweise: $$\vec{v_1}= \left( \begin{array}{c} 1\\ 0\\ 0\\ 1\\ \end{array}\right)\ \ \ \ \ \vec{v_2}= \left( \begin{array}{c} 1\\ 1\\ 0\\ 1\\ \end{array}\right)\ \ \ \ \ \vec{v_3}= \left( \begin{array}{c} 0\\ 0\\ 1\\ 0\\ \end{array}\right)\ \ \ \ \ \vec{v_4}= \left( \begin{array}{c} 1\\ 0\\ 1\\ 0\\ \end{array}\right) $$
Der selbe 4x4 Wald in Matrixschreibweise: $$\vec{v_1}= \left( \begin{array}{c c c} 1&1&0&1\\ 0&1&0&0\\ 0&0&1&1\\ 1&1&0&0\\ \end{array}\right)$$
Eine Matrix ist im Prinzip nichts anderes, als eine Liste von Listen. Mit dem Python-Paket numpy
bekommen wir Zugriff auf zahlreiche "Werkzeuge" zum erleichterten Arbeiten mit Matrizen. Das Anlegen einer leeren 4x4 Matrix würde beispielsweise ohne numpy
so aussehen:
wald2d = [[0,0,0,0], [0,0,0,0], [0,0,0,0], [0,0,0,0]]
was für größere Matrizen zunehmend unübersichtlich würde. Einfacher geht es mit numpy
:
wald2d = np.zeros([4,4])
Das Lesen und Ändern von Matrixeinträgen funktioniert ähnlich wie das Lesen und Ändern von Vektor-, sprich Listenelementen: Der erste Eintrag eines Vektors (einer Liste) ist der Eintrag vektorname[0]
. In einer Matrize lautet der erste Eintrag links oben matrixname[0][0]
.
Wenden wir dieses Wissen über Matritzen jetzt auf unser Waldmodell an: Erstellen wir zuerst einen leeren Wald, also eine 10x10 Matrix, die mit Nullen gefüllt ist.
Im nächsten Schritt sollte an jede Stelle eine zufällige Zahl (entweder 1 oder 0) gesetzt werden.
int(random.uniform(0, 2)
erzeugt uns genau so eine Zahl (genaugenommen eine Zahle zwischen 0 und 2, die dann abgerundet wird). Aber wie machen wir das für jeden Eintrag der Matrix?
Der Befehl ist immer der gleiche, aber das Ziel des Befehls ändert sich fortlaufend, nämlich vom Element [0][0]
, zu [0][1]
, zu [0][2]
und so weiter. Um also alle Matrizenstellen mit Zufallszahlen füllen, macht es Sinn eine sogenannte Doppelschleife zu verwenden.
Eine Doppelschleife ist eine Schleife innerhalb einer Schleife. Die innere Schleife führt den gewünschten Befehl für eine ganze Zeile der Matrix aus, die äußere Schleife sorgt dafür, dass die innere Schleife für jede Zeile ausgeführt wird. Wichtig dabei ist, dass die Durchlaufvariablen (it
) nicht den gleichen Namen haben dürfen.
Wenn der Wald fertig erstellt ist, geben wir ihn mit einem einfachen Printbefehl an den Bildschirm aus.
import matplotlib.pyplot as plt
import random
import numpy as np
%matplotlib inline
# erzeuge eine 'leere', sprich mit Nullen gefüllte Matrix
wald2d=np.zeros([10,10])
# erste Schleife für die verschiedenen Reihen
for it in range(10):
# zweite Schleife für die Elemente in den Reihen
for jt in range(10):
wald2d[it][jt] = int(random.uniform(0, 2))
print(wald2d)
Man kann sich so schon ein wenig vorstellen, wie der Wald aussehen könnte. Besser wäre aber eine grafische Ausgabe. Ein Befehl, den wir zum Plotten von Matrizen verwenden können, heißt plt.matshow
.
Damit können wir einerseits angeben, welche Matrix geplottet werden soll, andererseits auch in welcher Farbe. Die Farbdarstellung in dieser Art von Plots erlaubt es Farben in Abhängigkeit der Größe eines Matrizeneintrages zu gestalten. dazu wird ein Farbverlauf definiert. Obwohl wir hier einstweilen nur Nullen und Einsen als Einträge haben, wählen wir einen Farbverlauf, der von Weiß nach Grün geht. Er heißt plt.cm.Greens
.
import matplotlib.pyplot as plt
import random
import numpy as np
%matplotlib inline
wald2d = np.zeros([10, 10])
for it in range(10):
for jt in range(10):
wald2d[it][jt] = int(random.uniform(0, 2))
plt.matshow(wald2d, cmap = plt.cm.Greens)
Bisher berücksichtigen wir zur Darstellung unseres Waldes nur die Werte 0 und 1 (kein Baum, Baum). Wenn wir aber zum Beispiel auch Daten über die Größe oder die Dichte der Bäume in unserem Wald hätten, so könnten wir dies mit Werten zwischen 0 und 1 repräsentieren. Zur Darstellung müssen wir dank Farbverlauf sonst nichts an unserem Programm ändern.
import matplotlib.pyplot as plt
import random
import numpy as np
%matplotlib inline
wald2d=np.zeros([10, 10])
for it in range(10):
for jt in range(10):
wald2d[it][jt] = random.uniform(0, 1)
plt.matshow(wald2d, cmap = plt.cm.Greens)
In diesem Plot sehen wir nun dunklere und hellere Bereiche des Waldes. Anhand dieser Darstellung könnten wir uns überlegen, in welchen Bereichen es Sinn machen würde, Holz aus dem Wald zu entfernen. Praktisch wäre es, die besonderns dunklen Bereiche des Waldes durch Fällen von Bäumen zu lichten, damit dort wieder neue Bäume wachsen können.
Wie finden wir nun aber zuverlässig die dunklen Bereiche des Waldes? Einfach nur eine große Zahl in der Matrix zu suchen, reicht nicht aus: Damit könnte es sein, dass wir einen vereinzelten großen Baum fällen, der gar keine Nachbarn hat. Wir müssen also bei unserer Suche sicherstellen, dass auch auf den je benachbarten Bereichen große Bäume stehen.
Dazu müssen wir zuerst eine Grenze festlegen, ab der wir einen Baum als "groß" einstufen. In diesem Beispiel nehmen wir den Wert 0.75 an. Nachdem wir den Wald generiert haben, iterieren wir in einer weiteren Doppelschleife durch alle Positionen des Waldes und überprüfen, ob ein Baum an dieser Stelle "groß" ist. Sodann überprüfen wir zusätzlich alle vier Nachbarn an dieser Position. Steht an einem dieser Nachbarorte auch ein großer Baum, so wird der ursprüngliche, also der mittlere Baum gefällt.
In unserer Matrizendarstellung sind die vier Nachbarn des Baumes wald2d[it][jt]
wald2d[it+1][jt]
wald2d[it-1][jt]
wald2d[it][jt+1]
wald2d[it][jt-1]
Um nun den Wald vor und nach diesem Auslichten zu betrachten, erstellen wir zwei Plots. Wir verwenden dazu den Befehl plt.show()
, der den Plot zeichnet, bevor das restliche Programm ausgeführt wird.
import matplotlib.pyplot as plt
import random
import numpy as np
%matplotlib inline
wald2d = np.zeros([10, 10])
# Wald wir generiert
for it in range(10):
for jt in range(10):
wald2d[it][jt] = random.uniform(0, 1)
# erste Darstellung
plt.matshow(wald2d, cmap = plt.cm.Greens)
plt.show()
# Wald wird ausgelichtet
for it in range(1, 9):
for jt in range(1, 9):
if wald2d[it][jt] > 0.75:
if wald2d[it+1][jt] > 0.75:
wald2d[it][jt] = 0
if wald2d[it-1][jt] > 0.75:
wald2d[it][jt] = 0
if wald2d[it][jt+1] > 0.75:
wald2d[it][jt] = 0
if wald2d[it][jt-1] > 0.75:
wald2d[it][jt] = 0
# zweite Darstellung
plt.matshow(wald2d, cmap = plt.cm.Greens)
Um in diesen Plots zu sehen, wo genau die Bäume gefällt wurden, muss man sehr genau hinschauen. Lassen sich die Orte, an denen gefällt wurde, einfacher finden? Gesucht ist also eine Methode zur Darstellung des Unterschieds zwischen erstem und zweiten Plot.
Unterschiede sind mathematisch ausgedrückt Differenzen. Das Rechnen mit Matrizen bietet Vorteil, eine Matrix einfach von einer anderen subtrahieren zu können. Wir bauen diese Möglichkeit in das Programm ein und erstellen einen Plot der uns sagt, wo Bäume gefällt wurden.
Dazu müssen wir den zunächst generierten Wald unter einem eigenen Namen zwischenspeichern, damit wir Ihn beim Fällen nicht überschreiben.
import matplotlib.pyplot as plt
import random
import numpy as np
%matplotlib inline
wald2d = np.zeros([10, 10])
for it in range(10):
for jt in range(10):
wald2d[it][jt] = random.uniform(0, 1)
# erste Darstellung, der generierte Wald
plt.matshow(wald2d, cmap = plt.cm.Greens)
plt.show()
# Zwischenspeichern
wald2dalt = np.array(wald2d)
for it in range(1, 9):
for jt in range(1, 9):
if wald2d[it][jt] > 0.75:
if wald2d[it+1][jt] > 0.75:
wald2d[it][jt] = 0
if wald2d[it-1][jt] > 0.75:
wald2d[it][jt] = 0
if wald2d[it][jt+1] > 0.75:
wald2d[it][jt] = 0
if wald2d[it][jt-1] > 0.75:
wald2d[it][jt] = 0
# zweite Darstellung, der gelichtete Wald
plt.matshow(wald2d, cmap = plt.cm.Greens)
# dritte Darstellung, die Differenz, diesmal in Rot-Tönen
plt.matshow(wald2dalt - wald2d, cmap = plt.cm.Reds)
Wir könnten auch versuchen, den größten Baum im Wald zu finden. Dazu müssen wir wieder eine Doppelschleife verwenden, und somit alle Bäume abgehen. Wir merken uns immer die Größe des größten Baumes, dem wir bereits begegnet sind. Wenn der aktuelle Baum größer ist, wird diese Größe ersetzt.
import matplotlib.pyplot as plt
import random
import numpy as np
%matplotlib inline
wald2d = np.zeros([10, 10])
for it in range(10):
for jt in range(10):
wald2d[it][jt] = random.uniform(0, 1)
maximum = 0
#Wir setzen den Wert für den größten Baum auf 0
#Somit wird der erste Baum garantiert größer sein un diesen Wert überschreiben
for it in range(10):
for jt in range(10):
# Diese Schleife läuft nun im Gegensatz zur vorherigen über alle Einträge, also auch die Ränder.
if wald2d[it][jt] > maximum:
maximum = wald2d[it][jt]
# Nach der Doppelschleife geben wir den finalen Wert des größten Baumes aus:
print("Größter Baum:")
print(maximum)
Vektoren sind so etwas wie Listen, deren Einträge in der Mathematik gewöhnlich übereinandergeschrieben werden. $\vec{v_1}= \left( \begin{array}{c} 1\\ 0\\ 0\\ 1\\ \end{array}\right)\ \ \ \ \ $
Durch Vektoren lässt sich genauso itierieren, wie durch Listen. Beachte, dass das erste Element (der erste Eintrag) in einem Vektor als 'nullter' Eintrag gezählt wird (vektorname[0]
). Der zweite Eintrag hat somit die Bezeichnung vektorname[1]
, usw.
Matrizen sind Vektoren von Vektoren, oder am Computer: Listen von Listen. $\vec{v_1}= \left( \begin{array}{c c c} 1&1&0&1\\ 0&1&0&0\\ 0&0&1&1\\ 1&1&0&0\\ \end{array}\right)$
Matrizen-Einträge werden mit matrixname[0][0]
, matrixname[0][1]
, .... matrixname[n][m]
adressiert. Zum Iterieren durch eine Matrix kann eine Doppelschleife verwendet werden.
Auf Matrizen können eine Reihe von Grundrechenarten (Addieren, Subtrahieren, Multiplizieren ...) angewendet werden.
Zum Erzeugen von Zufallszahlen kann das Python-Paket (bzw. Modul) random
verwendet werden.
In diesem Kapitel werden wir das Recyclen von Rohstoffen betrachten und dabei etwas über Functions in Python lernen.
Nehmen wir an es gibt einen Produktionsprozess, der einen gewissen Rohstoff benötigt. Diesen Rohstoff kann man in einer Reinheit von 90% käuflich erwerben. Nach dem Produktionsprozess kann man den Rohstoff wiedergewinnen und theoretisch weiterbenutzen, er verliert aber ein Viertel seiner Reinheit. Mathematisch können wir das so ausdrücken:
reinheit = 0.9
reinheit_neu = reinheit * 0.75
print(reinheit_neu)
Der Rohstoff hat nun also nur mehr eine Reinheit von 67.5%. Wenn wir ihn so weiter verwenden, würde er sehr schnell eine Reinheit haben, mit der der Prozess nicht mehr funktioniert. Was aber, wenn wir ihn mit neuem Rohstoff mischen?
reinheit = 0.9
reinheit_rec = reinheit * 0.75
reinheit_neu = 0.5 * (reinheit + reinheit_rec) #Mittelwert
print(reinheit_neu)
Nun hätten wir eine Reinheit von über 78% nach dem ersten Wiederverwenden. Interessant wäre nun herauszufinden, was passiert, wenn wir diese Schritte immer weiter wiederholen, also den Rohstoff immer wieder verwenden und neu mischen. Wie schnell fällt die Reinheit ab? Erreicht sie irgendwann 0?
Um das herauszufinden, können wir eine Liste und eine For-Schleife verwenden:
import matplotlib.pyplot as plt
%matplotlib inline
reinheit = 0.9
reinheit_liste = [reinheit] #eine Liste mit einem Eintrag wird erstellt
for it in range(10):
reinheit = 0.5 * (0.9 + reinheit * 0.75)
reinheit_liste.append(reinheit)
plt.plot(reinheit_liste)
Es sieht so aus, also würde die Reinheit nie unter einen gewissen Wert fallen. Scheinbar bildet sich ein Gleichgewicht aus. Egal wie oft wir den Prozess wiederholen, wir kommen nicht unter 70 Prozent. Hat das etwas damit zu tun, dass der Prozess die Reinheit um 25% reduziert? Was hätte ein anderer Parameterwert für eine Auswirkung? Das könnten wir einfach überprüfen, indem wir einfach den Parameter von ändern, zum Beispiel auf 0.8:
import matplotlib.pyplot as plt
%matplotlib inline
reinheit = 0.9
reinheit_liste = [reinheit]
for it in range(10):
reinheit = 0.5 * (0.9 + reinheit * 0.8)
reinheit_liste.append(reinheit)
plt.plot(reinheit_liste)
Tatsächlich ändert sich der Fixpunkt. Wir wollen nun mehrere Parameterwerte in einem Plot vergleichen. Die einfachste Lösung wäre, die Forschleife oft zu kopieren und den Parameter jedes mal zu ändern. Wir könnten das Programm auch in eine For-Schleife legen, aber das Ändern des Parameters wird dadurch sehr umständlich. Glücklichweise gibt es eine bessere Lösung: Wir benutzen Functions.
Functions (nicht zu verwechseln mit mathematischen Funktionen) sind Programmierstrukturen, die kompliziertere Prozesse automatisch ausführen. Ähnlich wie mathematische Funktionen können sie einen Input und einen Output haben. Wir können ihnen einen Namen geben und sie somit platzsparend und übersichtlich aufrufen.
Functions sind immer dann empfehlenswert, wenn man einen Prozess an unterschiedlichen Stellen im Code braucht. Functions werden in Python mit dem Wort def deklariert. Man schreibt def namederfunction():
, die Function selbst wird dann eingerückt. Bauen wir nun unsere Recycling-Prozess in eine Function um.
import matplotlib.pyplot as plt
%matplotlib inline
#eine eigene Function wird definiert:
def recyceln():
reinheit = 0.9
reinheit_liste = [reinheit]
for it in range(10):
reinheit = 0.5 * (0.9 + reinheit * 0.8)
reinheit_liste.append(reinheit)
plt.plot(reinheit_liste)
Man beachte: Nur durch das Definieren der Function wird sie noch nicht ausgeführt. Dazu müssen wir sie dezidiert aufrufen. Das macht man mit dem Namen der Funktion gefolgt von runden Klammern:
import matplotlib.pyplot as plt
%matplotlib inline
def recyceln():
reinheit = 0.9
reinheit_liste = [reinheit]
for it in range(10):
reinheit = 0.5 * (0.9 + reinheit * 0.8)
reinheit_liste.append(reinheit)
plt.plot(reinheit_liste)
recyceln()
Was haben wir durch diese Function nun eigentlich gewonnen? Eine wichtige Eigenschaft von Functions ist, dass sie einen Inputwert haben können. Momentan verwendet unsere Function immer einen Parameter von 0.8, um zu berechnen wie viel Reinheit pro Schritt verloren wird. Diesen Parameter können wir umschreiben, sodass er als Inputparameter verwendet wird. Inputparameter werden wie folgt eingebaut:
Bei der Definition der Function kommt der Name des Parameters in die runde Klammer. Dieser Name kann dann innerhalb der Funktionsdefiniton benutzt werden.
Wenn die Function dann aufgerufen wird, gibt man den Wert den der Parameter haben soll, an.
Für unsere Recyclingfunction würde das also so aussehen:
import matplotlib.pyplot as plt
%matplotlib inline
def recyceln(verlustparameter):
reinheit = 0.9
reinheit_liste = [reinheit]
for it in range(10):
reinheit = 0.5 * (0.9 + reinheit * verlustparameter)
reinheit_liste.append(reinheit)
plt.plot(reinheit_liste)
recyceln(0.9)
recyceln(0.8)
recyceln(0.7)
Somit können wir sehr elegant verschiedene Verlustparameter miteinander vergleichen. Im Plot wäre es aber schön, wenn wir die Linien beschriften würden, damit wir wissen, welche Linie zu welchem Parameterwert gehört. Functions können wir einfach anpassen: Wenn wir in der Definition etwas ändern, ist diese Änderung für alle Aufrufe gültig.
import matplotlib.pyplot as plt
%matplotlib inline
def recyceln(verlustparameter):
reinheit = 0.9
reinheit_liste = [reinheit]
for it in range(10):
reinheit = 0.5 * (0.9 + reinheit * verlustparameter)
reinheit_liste.append(reinheit)
plt.plot(reinheit_liste, label = verlustparameter) #wir fügen eine Beschriftung hinzu
plt.legend() #die Beschriftungen sollen in einer Legende angezeigt werden
recyceln(0.9)
recyceln(0.8)
recyceln(0.7)
Functions können wir auch beliebig ausbauen. Wir können zum Beispiel einen zweiten Inputparameter einbauen. Weitere Inputparameter werden einfach mit Beistrichen getrennt. Bauen wir nun auch den Parameter, der uns sagt wir rein die frische Resource ist, als Parameter ein.
import matplotlib.pyplot as plt
%matplotlib inline
def recyceln(verlustparameter, startreinheit):
reinheit = startreinheit
reinheit_liste = [reinheit]
for it in range(10):
reinheit = 0.5 * (startreinheit + reinheit * verlustparameter)
reinheit_liste.append(reinheit)
plt.plot(reinheit_liste, label = verlustparameter)
plt.legend()
recyceln(0.9,1.0)
recyceln(0.8,1.0)
recyceln(0.7,1.0)
Zusätzlich zum Inputwert kann eine Function auch einen Outputwert haben. Das ist sozusagen das Ergebnis der Function, also das, was die Function an das Hauptprogramm zurückgeben soll. In unserem Fall wäre das zum Beispiel der letzte Reinheitswert unserer Zeitreihe. Der Outputwert wird mit dem Wort return am Ende der Definition festgelegt. Immer wenn wir eine weitere Function (zum Beispiel print
) auf eine andere Function anwenden, wenden wir sie eigentlich auf den Outputwert an.
import matplotlib.pyplot as plt
%matplotlib inline
def recyceln(verlustparameter, startreinheit):
reinheit = startreinheit
reinheit_liste = [reinheit]
for it in range(10):
reinheit = 0.5 * (startreinheit + reinheit * verlustparameter)
reinheit_liste.append(reinheit)
plt.plot(reinheit_liste, label = verlustparameter)
plt.legend()
return reinheit_liste[-1] #das Element -1 einer Liste ist das letzte Element der Liste, egal wie lang die Liste ist
print(recyceln(0.9,1.0))
print(recyceln(0.8,1.0))
print(recyceln(0.7,1.0))
Darüber hinaus ist es möglich Functions innerhalb von Functions zu verwenden. Wir können das Zusammenmischen der Rohstoffe in eine eigene Function auslagern:
import matplotlib.pyplot as plt
%matplotlib inline
def mischen(aktuell,start,verlust):
neu = 0.5 * (start + aktuell * verlust)
return neu
def recyceln(verlustparameter, startreinheit):
reinheit = startreinheit
reinheit_liste = [reinheit]
for it in range(10):
reinheit = mischen(reinheit,startreinheit,verlustparameter)
reinheit_liste.append(reinheit)
plt.plot(reinheit_liste, label = verlustparameter)
plt.legend()
return reinheit_liste[-1]
print(recyceln(0.9,1.0))
print(recyceln(0.8,1.0))
print(recyceln(0.7,1.0))
Analysieren wir nun einen anderen Recycle-Prozess. Wir produzieren Glasflaschen, die dann genutzt werden. Während der Nutzung lagern sich im Glas insgesamt 10 Milligram an Schadstoffen ab. Die Flaschen werden eingeschmolzen und neue Flaschen daraus gemacht. Dabei werden 15% der Schadstoffe entfernt. Wie lange dauert es, bis mehr als 50 Milligram in einer Flasche sind?
Versuchen wir dieses Problem mit Functions zu lösen. Schreiben wir zuerst eine Function, die den Verschmutzungsprozess beschreibt, und eine Function, die den Einschmelzprozess beschreibt.
Dann können wir diese Prozesse in eine For-Schleife geben und plotten was passiert.
import matplotlib.pyplot as plt
%matplotlib inline
def verschmutzen(startwert):
return startwert + 10
def schmelzen(startwert):
return startwert * 0.85
schadstoff = 0
schadstoff_liste = [schadstoff]
for it in range(10):
schadstoff = verschmutzen(schadstoff)
schadstoff = schmelzen(schadstoff)
schadstoff_liste.append(schadstoff)
plt.plot(schadstoff_liste)
Wenn wir die Funktionen verschachteln wird es deutlich kompakter. Die Functions werden von innen nach außen abgearbeitet:
import matplotlib.pyplot as plt
%matplotlib inline
def verschmutzen(startwert):
return startwert + 10
def schmelzen(startwert):
return startwert * 0.85
schadstoff = 0
schadstoff_liste = [schadstoff]
for it in range(10):
schadstoff = schmelzen(verschmutzen(schadstoff))
schadstoff_liste.append(schadstoff)
plt.plot(schadstoff_liste)
Wir sehen also: nach 10 Schritten ist der Grenzwert noch nicht überschritten. Wie können wir nun aber herausfinden, wann genau dieser Wert das erste mal überschritten wird? Wir könnten einfach die For-Schleife länger laufen lassen und so lange herumprobieren, bis wir einen geeigneten Wert finden. Es gibt aber eine bessere Lösung: Immer, wenn wir nicht wissen wie oft eine Schleife durchlaufen werden soll, sondern nur unter welcher Bedingung sie abgebrochen werden sollte (in unserem Fall beim Erreichen von 50 mg Schadstoff) kann man eine While-Schleife verwenden.
Bei der While-Schleife geben wir keine Anzahl an Schleifendurchgängen vor, sondern eine Bedingung. So lange diese Bedinung noch erfüllt ist wird die Schleife weiterlaufen. Man benutzt While-Schleifen mit
while bedingung:
anweisungen
Für unser Beispiel also:
import matplotlib.pyplot as plt
%matplotlib inline
def verschmutzen(startwert):
return startwert + 10
def schmelzen(startwert):
return startwert * 0.85
schadstoff = 0
schadstoff_liste = [schadstoff]
while schadstoff < 50:
schadstoff = schmelzen(verschmutzen(schadstoff))
schadstoff_liste.append(schadstoff)
plt.plot(schadstoff_liste)
While-Schleifen haben keine integrierte Variable, die mitzählt, wie oft die Schleife durchlaufen worden ist. Wenn wir so etwas brauchen (wie hier um die Anzahl der Wiederverwendungen zu finden) können wir das aber sehr einfach einbauen:
import matplotlib.pyplot as plt
%matplotlib inline
def verschmutzen(startwert):
return startwert + 10
def schmelzen(startwert):
return startwert * 0.85
schadstoff = 0
schadstoff_liste = [schadstoff]
counter = 0
while schadstoff < 50:
schadstoff = schmelzen(verschmutzen(schadstoff))
schadstoff_liste.append(schadstoff)
counter = counter + 1
plt.plot(schadstoff_liste)
print("Nach")
print(counter)
print("Prozessen ist der Schadstoffgrenzwert überschritten.")
Vorsicht bei der Verweundung von While-Schleifen: Wenn die Bedingung der While-Schleife schlecht gewählt wurde, kann es sein, dass sie für immer wahr bleibt. Dann läuft die While-Schleife unendlich lang weiter.
Man sollte While-Schleifen also nur dann verwenden, wenn wir ganz sicher sind, dass die Bedingung irgendwann einmal nicht mehr erfüllt sein wird. Wenn wir überhaupt nicht wissen, was mit unserem System passieren wird sollte man While-Schleifen vermeiden und eher auf For-Schleifen ausweichen.
Mit Functions können wir Prozesse, die wir oft brauchen, einmalig definieren und dann oftmals wiederverwenden. Wir können sozusagen unsere eigenen Pythonbefehle zusammenbauen. Functions können Inputwerte haben, damit kann man sie anpassen. Außerdem gibt es Outputwerte, die wieder an das Hauptprogramm übergeben werden. Andere Variablen, die innerhalb der Function erstellt werden exisiteren nur innerhalb der Function und sind vom Hauptprogramm nicht aufrufbar.
Functions werden mit
def functionname(input1,input2,input3):
Anweisungen
return outputvariable
definiert.
While-Schleifen werden, ähnlich wie For-Schleifen dazu benutzt Anweisung oftmals zu wiederholen. Bei der For-Schleife muss man angeben, wie oft sie durchlaufen werden muss. Die While-Schleife läuft im Gegensatz dazu so lange, bis eine Bedingung, die man wählen muss, nicht mehr erfüllt ist.
Somit ist das Verwenden der While-Schleife sehr gefährlich: Bleibt die Bedingung immer erfüllt (durch einen Denk- oder einen Programmierfehler) wird die Schleife niemals fertig und der Computer kann abstürzen, wenn man nicht schnell genug den Kernel beendet.
Im letzten Kapitel haben wir Functions kennengelernt. Damit konnten wir eigene Befehle programmieren, die Input- und Outputwerte haben können.
Wir haben auch gesehen, dass eine Function auch eine andere Function aufrufen kann. Das könnte uns auf eine interessante Fragestellung bringen: Was passiert, wenn eine Function sich selbst aufruft? Kann das überhaupt funktionieren, oder bauen wir damit automatisch immer eine Endlosschleife?
Tatsächlich ist es möglich, dass wir Functions bauen, die sich selbst aufrufen. Streng genommen handelt es sich bei so etwas um eine Rekursion. Eine Rekursion tritt immer dann auf, wenn man eine Operation durchführt und dann die gleiche Operation auf den Output der ersten Operation anwendet und immer so weiter. Ähnlich wie bei While-Schleifen müssen wir aber aufpassen, dass wir keine Endlosschleifen produzieren. Functions, die garantiert sich selbst aufrufen werden immer Endlosschleifen produzieren. Functions die sich selbst nur unter einer gewissen Bedingung aufrufen sind für Rekursionen geeignet.
Versuchen wir nun eine solche Rekursion zu programmieren. Wir möchten eine Function haben, die einen Countdown herunterzählt, beginnend von einer beliebigen Zahl. Die Function soll also, wenn sie als Input 0 bekommt, die Zahl 0 ausgeben. Wenn Sie einen anderen Input bekommt, soll sie einerseits diesen Input ausgeben, andererseits auch sich selbst aufrufen. Der Input, mit dem sie sich selbst aufruft soll ihr eigener Input - 1 sein, sodass heruntergezählt wird.
#wir definieren die Function countdown
def countdown(zahl):
#in jedem fall möchten wir den input printen
print(zahl)
#wenn wir noch nicht bei 0 angekommen sind,
#ruft die Funcstion sich selbst auf, aber
#mit einem um 1 reduzierten Input.
if zahl != 0: #wenn der input 0 ist
countdown(zahl-1)
#nach der Definition müssen wir unsere Function auch aufrufen:
#den Input können wir frei wählen
countdown(10)
Unsere Countdownfunction scheint zu funktionieren. Sie zählt von einer beliebigen Zahl abwärts, indem sie sich immer wieder selbst aufruft. Wir benötigen also keine Schleife. Dennoch kann es passieren, dass wir eine Endlosschleife bauen, nämlich wenn wir nie auf den Input 0 kommen, weil wir die Function zum Beispiel mit einem anderen Input starten: (der Printbefehl wurde hier sicherheitshlaber auskommentiert)
#wir definieren die Function countdown
def countdown(zahl):
#in jedem fall möchten wir den input printen
#print(zahl)
#wenn wir noch nicht bei 0 angekommen sind,
#ruft die Funcstion sich selbst auf, aber
#mit einem um 1 reduzierten Input.
if zahl != 0: #wenn der input 0 ist
countdown(zahl-1)
#nach der Definition müssen wir unsere Function auch aufrufen:
#den Input können wir frei wählen
countdown(10.5)
Nach einiger Zeit liefert Python eine Fehlermeldung:
RecursionError: maximum recursion depth exceeded
Im Gegensatz zu While-Schleifen, sind Rekursionen in Python besser gesichert. Wenn es zu zu vielen Selbstaufrufen kommt, erkennt das Python als Endlosschleife und bricht die Berechnung ab, bevor der Computer einfriert.
Das ist aber nur einer der Vorteile von Rekursionen gegenüber Schleifen. Rekursionen können auch deutlich kompliziertere Aufgaben erfüllen, die mit Schleifen nur umständlich lösbar sind. Denken wir zurück an die Fibonaccifolge, also
0,1,2,3,5,8,13,...
Wenn wir nun zum Beispiel wissen wollen, welche die 20ste Zahl in dieser Folge ist, könnten wir eine Schleife bauen, die 20 Fibonacci-Zahlen ausrechnet. Eleganter geht das mit einer Rekursion: Dazu müssen wir nur folgende drei Dinge wissen.
Die erste Fibonaccizahl ist 0.
Die zweite Fibonaccizahl ist 1.
Die $n$-te Fibonccizahl kann man ausrechnen, indem man die $(n-1)$-te zur $(n-2)$-ten addiert.
Als Rekursion geschrieben:
def fibonacci(n):
ret = 0 #wir legen eine Variable an, die wir auf 0 setzen
#wir kennen die erste Fibonaccizahl
if n == 0:
ret = 0
#wir kennen die zweite Fibonaccizahl
if n == 1:
ret = 1
#alle anderen können wir rekursiv ausrechnen
if n > 1:
ret = fibonacci(n-1) + fibonacci(n-2)
return ret
fibonacci(20)
Dank der Rekursion benötigen wir keine Schleife, keine Liste und keine Variablen die uns alte Fibonacci-Zahlen speichern.
Auf ähnlich Weise können wir auch kompliziertere Zahlenmengen finden. Besonders spanned ist zum Beispiel die Mandelbrot-Menge. Um zu verstehen, was die Mandelbrotmenge genau ist, müssen wir uns zuerst mit der Mandelbrot-Folge auseinandersetzen. Die Mandelbrot-Folge für die Zahl c wird wie folgt berechnet:
$$z_{n+1} = z_n^2 + c$$
Ähnlich wie bei der Fibonaccifolge brauchen wir also das $n$-te Element um das $n+1$ste zu errechnen.
Die Mandelbrot-Menge ist nun die Menge aller Zahlen, bei denen keines der Elemente dieser Reihe größer wird als 2.
Für reelle Zahlen können wir uns diese Folge noch im Kopf ausrechnen:
$$1^2 + 1 = 2,\ \ \ \ 2^2 + 1 = 3,\ \ \ \ 3^2 + 1 = 10,\ \ \ \ ...$$
$$0^2 + 0 = 0,\ \ \ \ 0^2 + 0 = 0,\ \ \ \ 0^2 + 0 = 0,\ \ \ \ ...$$
$$-1^2 - 1 = 0,\ \ \ \ 0^2 - 1 = -1,\ \ \ \ -1^2 - 1 = 0,\ \ \ \ ...$$
c = 1 ist also nicht in der Mandebrot-Menge, denn die Elemente der Reihe werden unendlich groß.
c = 0 ist in der Mandelbrot-Menge, denn die Elemente sind alle 0.
c = -1 ist in der Mandelbrot-Menge, denn die Elemente sind immer abwechseln 0 und -1.
Was ist nun aber mit den Zahlen dazwischen? Und mit komplexen Zahlen? Das wird im Kopf sehr aufwendig, weswegen wir ein Programm schreiben, das die Arbeit für uns erledigt.
Zuerst brauchen wir eine Doppelschleife, die uns über einen interessanten Bereich der komplexen Ebene wandert. Später soll für jede Zahl die vorkommt bestimmt werden, ob Sie in der Mandelbrot-Menge ist oder nicht. Fürs erste begnügen wir uns damit einfach den Absolutbetrag der Zahl darzustellen. In Python kann man komplexe Zahlen mit complex(realteil,imaginärteil)
definieren und den Absolutbetrag mit abs
berechnen.
Wir untersuchen komplexe Zahlen mit einem Realteil zwischen -2 und 0.5 und einem Imaginärteil zwischen -1.25 und 1.25. Wir hätten gern eine Genauigkeit von 0.01. Um Zahlen aus diesen Intervallen zu erstellen ist der range
Befehl unzureichend. Wir benötigen den Befehl np.arange
mit dem man Startwert, Endwert und Schrittweite angeben kann.
import matplotlib.pyplot as plt
%matplotlib inline
import numpy as np
#wir definieren den Realteil der Zahlen, die wir untersuchen wollen:
r_bereich = np.arange(-2,0.5,0.01)
#und den Imaginärteil der Zahlen, die wir untersuchen wollen.
i_bereich = np.arange(-1.25,1.25,0.01)
#Wir legen eine Matrix in der richtigen Größe an, die mit 0en gefüllt ist.
mandelbrot = np.zeros([len(i_bereich),len(r_bereich)])
#mit einer doppelschleifen gehen wir über alle Zahlen
for it in range(len(i_bereich)):
for jt in range(len(r_bereich)):
#und speichern den absolutbetrag der Zahl in die Matrix
mandelbrot[it,jt]= abs(complex(r_bereich[jt],i_bereich[it]))
plt.pcolormesh(r_bereich,i_bereich,mandelbrot)
#der Befehl pcolormesh funktioniert ähnlich wie imshow,
#nur können wir hier x und y Intervalle für die Beschriftung übergeben
plt.ylabel("Imaginärteil")
plt.xlabel("Realteil")
Nun steht also das Grundgerüst unseres Programms. Wir wandern über einen Bereich der komplexen Ebene und zeichnen das Ergebnis einer Operation. Momentan ist die Operation einfach der Absolutbetrag der Zahl. Wir werden Sie im nächsen Schritt aber durch eine andere Information ersetzen: Nämlich wie viele Schritte es braucht, bis ein Element der Mandelbrotfolge größer als 2 wird.
Dazu müssen wir zuerst eine Function bauen, die uns die Mandelbrotfolge ausrechnet. Auch hier beitet sich eine Rekursion an. Wenn wir das $n$-te Element der Mandelbrotfolge der Zahl $c$ haben wollen, können wir das einfach berechnen, indem wir das $(n-1)$-te Element quadrieren und $+\ c$ rechnen. Das "nullte" Element der Mandelbrotfolge ist immer die Zahl $c$ selbst.
Mathematisch ausgedrückt:
$$M_0 = c $$ $$M_n = \left(M_{n-1}\right)^2 + c $$
Das können wir als Function wie folgt schreiben:
def mandelbrotelement(c, it):
# das nullte element ist die Zahl selbst
if it == 0:
return c
else:
#sonst müssen wir es aus dem vorherigen Element ausrechnen
return mandelbrotelement(c, it - 1) ** 2 + c
mandelbrotelement(complex(-1,0),10)
Unsere Rekursion scheint zu funktionieren. Das 10te Element der Mandelbrotreihe der Zahl -1
ist wieder -1
.
Mit dieser Function können wir eine weitere Function schreiben, die uns die ersten 50 Elemente der Mandelbrotreihe ausrechnet:
def mandelbrotfolge(c):
ret = [] # wir erstellen eine leere Liste
for it in range(50):
#wir füllen diese Liste mit den Elementen der Mandelbrotfolge
z=mandelbrotelement(c,it)
ret.append(z)
return ret
def mandelbrotelement(c, it):
if it == 0:
return c
else:
return mandelbrotelement(c, it - 1) ** 2 + c
mandelbrotfolge(complex(0.1,0.1))
Solche Functions, die sich selbst rekursiv aufrufen, sehen zwar sehr elegant aus, haben in der Praxis aber meist einige Nachteile. Somit ist es für viele Anwendung, vor allem in Python, sinnvoll, dass wir die Rekursion nicht durch rekursives Aufrufen einer Funktion, sondern durch eine Iteration durchführen. Wir starten also nicht beim letzten Element und arbeiten uns vor zum Element 0, sondern wir starten am Anfang und iterieren so lange, bis wir bei dem Element sind, das uns interessiert. Der gleiche Rekursive Prozess wie oben, jetzt als Iteration ausgedrückt:
def mandelbrotfolge(c):
ret = [c]
for it in range(1,50):
z = ret[it - 1]**2 + c
ret.append(z)
return ret
mandelbrotfolge(complex(0.1,0.1))
Wir sind aber eigentlich nicht an der gesamten Folge interessiert, sondern nur an den Elementen vor dem ersten Element das größer ist als 2. Somit müssen wir für viele Zahlen garnicht die ganze For-Schleife durchlaufen: Immer wenn wir eine Zahl finden, die größer ist als 2 könnten wir die Schleife eigentlich abbrechen, um Rechenzeit zu sparen. Wie unterbrechen wir aber eine Schleife, die eigentlich bis 50 läuft schon vorzeitig?
Dafür gibt es in Python den Befehl break
. break
benutzt man innerhalb von Schleifen, um die Schleife vorzeitig abzubrechen. Brechen wir unsere Schleife nun ab, sobald wir ein Element ausrechnen, das größer ist als 2:
def mandelbrotfolge(c):
ret = [c]
for it in range(1,50):
z = ret[it - 1]**2 + c
ret.append(z)
if abs(z)>2:
break
return ret
mandelbrotfolge(complex(0.5,0.5))
Das ist eigentlich schon alles, was wir zum Zeichnen der Mandelbrotmenge benötigen. Für jede Zahl, die wir untersuchen, schauen wir die Mandelbrotfolge an. Da wir automatisch abbrechen, sobald eine Zahl zu groß ist, ist die Länge der Liste, die unsere Funkcion zurückgibt, automatisch die Anzahl der Zahlen in der Folge, die kleiner sind als 2. Für Zahlen, die Teil der Mandelbrotmenge sind liefert unsere Function also die volle Länge von 50.
Schreiben wir also eine Function, die die Länge der Liste liefert. Diese Function bauen wir dann in unser ursprüngliches Gerüst ein, um die komplexe Zahlenebene nach Zahlen die Teil der Mandelbrotmenge sind, zu untersuchen. Die Form die entsteht ist erstaunlich.
import matplotlib.pyplot as plt
%matplotlib inline
import numpy as np
def mandelbrotfolge(c):
ret = [c]
for it in range(1,50):
z = ret[it - 1]**2 + c
ret.append(z)
if abs(z)>2:
break
return ret
def check_mandelbrot(c):
#gibt die Länge der Liste zurück
return len(mandelbrotfolge(c))
r_bereich = np.arange(-2,0.5,0.01)
i_bereich = np.arange(-1.25,1.25,0.01)
mandelbrot = np.zeros([len(i_bereich),len(r_bereich)])
for it in range(len(i_bereich)):
for jt in range(len(r_bereich)):
mandelbrot[it,jt]= check_mandelbrot(complex(r_bereich[jt],i_bereich[it]))
plt.pcolormesh(r_bereich,i_bereich,mandelbrot)
plt.ylabel("Imaginärteil")
plt.xlabel("Realteil")
Wir können auch hineinzoomen und uns einen winzigen Teil der komplexen Ebene ansehen. Egal wie sehr wir hineinzoomen, wir werden immer feinere Strukturen erkennen.
import matplotlib.pyplot as plt
%matplotlib inline
import numpy as np
def mandelbrotfolge(c):
ret = [c]
for it in range(1,50):
z = ret[it - 1]**2 + c
ret.append(z)
if abs(z)>2:
break
return ret
def check_mandelbrot(c):
return len(mandelbrotfolge(c))
r_bereich = np.arange(-1.49,-1.47,0.00005)
i_bereich = np.arange(-0.005,0.005,0.00005)
mandelbrot = np.zeros([len(i_bereich),len(r_bereich)])
for it in range(len(i_bereich)):
for jt in range(len(r_bereich)):
mandelbrot[it,jt]= check_mandelbrot(complex(r_bereich[jt],i_bereich[it]))
plt.pcolormesh(r_bereich,i_bereich,mandelbrot)
plt.ylabel("Imaginärteil")
plt.xlabel("Realteil")
import matplotlib.pyplot as plt
%matplotlib inline
import numpy as np
def mandelbrotfolge(c):
ret = [c]
for it in range(1,50):
z = ret[it - 1]**2 + c
ret.append(z)
if abs(z)>2:
break
return ret
def check_mandelbrot(c):
return len(mandelbrotfolge(c))
r_bereich = np.arange(-0.75,-0.746,0.00002)
i_bereich = np.arange(0.105,0.11,0.00002)
mandelbrot = np.zeros([len(i_bereich),len(r_bereich)])
for it in range(len(i_bereich)):
for jt in range(len(r_bereich)):
mandelbrot[it,jt]= check_mandelbrot(complex(r_bereich[jt],i_bereich[it]))
plt.pcolormesh(r_bereich,i_bereich,mandelbrot)
plt.ylabel("Imaginärteil")
plt.xlabel("Realteil")
Warum die Mandelbrotmenge genau diese fraktale Form hat, kann nicht erklärt werden. Sie dient aber als eindrucksvolles Beispiel, dass sehr simple Rekursionen zu außerordentlicher Komplexität führen können und die Komplexität nicht durch den Menschen entsteht, sondern von Natur aus vorhanden ist, und nur entdeckt werden muss.
Von Rekursionen spricht man, wenn man eine Function in sich selbst einsetzt. Dabei muss man aber, ähnlich wie bei While-Schleifen aufpassen, dass man keine Endlosschleifen produziert. Dafür ist es wichtig, dass es zumindest eine Bedingung gibt, bei der die Function sich selbst nicht aufruft, und diese Bedingung auch garantiert irgendwann erreicht wird. Meist ist es jedoch besser, dass man rekursive Aufrufe vermeidet, und die Rekursion auf eine Iteration umschreibt.
Komplexe Zahlen können in Python mit complex(Realteil, Imaginärteil)
eingegeben werden. Dann können die normalen Rechenoperationen verwendet werden (+,*,abs(),...) und Pyhton geht korrekt mit komplexen Zahlen um. Anstelle vom in der Mathematik üblichen $i$, verwendet Python beim Ausgeben von komplexen Zahlen das eher in der Technik verbreitete $j$ für die imaginäre Einheit $\sqrt{-1}$.
Mit np.arange
kann man Zahlenintervall von beliebeiger Genauigkeit erstellen, über die man dann iterieren kann. Die Syntax dazu lautet np.arange(startwert, zielwert, schrittweite)
.
Oft möchte man Schleifen vorzeitig unterbrechen, obwohl die primäre Abbruchbedingung noch nicht erreicht ist. Dazu gibt es in Python den Befehl break
. break
unterbricht die aktuelle Schleife und der Code direkt unterhalb der Schleife wird ganz normal weiter ausgeführt.
Modernes Programmieren ist zumeist objektorientiert. Das heißt der Fokus liegt nicht so sehr auf fest vorgelegten Abläufen und Rechenoperationen, die abgearbeitet werden, sondern auf Objekten, die definiert werden, Eigenschaften und Fähigkeit haben und sehr vielseitig benutzt werden können.
Auch in Python kann man sehr leicht eigene Objekte erstellen. Ein Objekt gehört immer einer Klasse an, die man auch selbst definieren kann.
Im Folgenden werden wir Objekte und Klassen nutzen um ein Verkehrssystem zu simulieren.
Dieses Verkehrssystem wird sehr rudimentär sein. Es wird Personen geben, die einen Heimatort und einen Arbeitsort besitzen, und zwischen diesen Orten hin- und herpendeln. Eine Person wird immer ein Objekt einer Klasse sein und Eigenschaften (z.B. Heimatort) und Fähigkeiten - beim Programmieren eher Methoden genannt - (z.b. zur Arbeit fahren) haben.
Fangen wir einfach an: Wir definieren die Klasse Person mit dem Wort class
. Danach können wir Methoden für diese Klasse definieren. Eine Methode, die eigentlich alle Klassen haben ist die Inititalisierungmethode. Mit dieser Methode kann man neue Objekte von dieser Klasse erstellen. In Python trägt diese Initialisierungsmethode immer den gleichen Namen wie die Klasse selbst. Wenn wir die Klasse Person
nennen, können wir also mit dem Befehl Person()
neue Personen erstellen. Der Name von dieser Initialisierungsmethode ist in Python einheitlich und lautet __init__
, alle anderen Methoden dürfen wir selbst benennen. In der Initalisierungsmethode werden meist die Eigenschaften dieses Objekts definiert. Die Eigenschaft home
würde man hierbei als self.home
bezeichnen. Das heißt, jede Person hat ein Zuhause, aber nicht unbedingt das gleiche. Beim Inititalisieren einer Eigenschaft setzen wir den Wert, der diese Eigenschaft für das aktuelle Objekt hat, fest, also self.home
.
Wir erstellen nun also die Klasse Person
und legen die Initialisierungsmethode fest. Dort bekommen die Personen eine zufällige Heimatadresse. Methoden werden ähnlich definiert wie Functions, der einzige Unterschied ist, dass Methoden als erste Inputvariable immer self
haben, also sich selbst. Immer wenn also eine Methode aufgerufen wird, wird automatisch übergeben, welches Objekt dieser Klasse (also für uns welche Person) die Methode aufgerufen hat.
import matplotlib.pyplot as plt
%matplotlib inline
import random
class Person:
def __init__(self):
#eine neue Person wird initialisiert
#sie bekommt eine zufällig Heimadresse
self.home = [random.randint(0,10),random.randint(0,10)]
Wie beim Definieren von Funktion, macht das Definieren von Klassen an sich noch nichts. Sie müssen immer erst im Hauptprogramm aufgerufen werden. Erstellen wir nun zur Probe eine Person und zeichen wir ihre Heimatadresse als Scatterplot.
import matplotlib.pyplot as plt
%matplotlib inline
import random
class Person:
def __init__(self):
#eine neue Person wird initialisiert
#sie bekommt eine zufällig Heimadresse
self.home = [random.randint(0,10),random.randint(0,10)]
#----Hier startet das Hauptprogramm----
#wir erstellen ein Objekt der Klasse person mit dem Namen person1
person1 = Person()
#Dann zeichnen wir den Heimatort von person1
#s ist die Größe des Markers, die Form "s" steht für square
plt.scatter(person1.home[0],person1.home[1], s = 100, marker = "s", c = "forestgreen")
Wir haben nun erfolgreich ein Objekt der Klasse Person
erstellt und die Heimatadresse gezeichnet. Eleganter wäre das aber, wenn wir das Zeichnen nicht im Hauptprogramm machen, sondern es als Methode erstellen: Alle Personen sollen die Fähigkeit erhalten sich selbst zu zeichnen. Wir nennen diese Methode zeichnen
und definieren sie direkt nach __init__
. Immer wenn wir diese Methode dann aufrufen möchten schreiben wir NameDerPerson.zeichnen()
.
import matplotlib.pyplot as plt
%matplotlib inline
import random
class Person:
def __init__(self):
#eine neue Person wird initialisiert
#sie bekommt eine zufällig Heimadresse
self.home = [random.randint(0,10),random.randint(0,10)]
def zeichnen(self):
#Die Personen zeichnen ihre Heimatadresse
#s ist die Größe des Markers, die Form "s" steht für square
plt.scatter(self.home[0],self.home[1], s = 100, marker = "s", c = "forestgreen")
#----Hier startet das Hauptprogramm----
#wir erstellen ein Objekt der Klasse person mit dem Namen person1
person1 = Person()
#Dann zeichnen wir den Heimatort von person1
person1.zeichnen()
Mit dieser eleganten Lösung können wir nun auch mehrere Objekte der gleichen Klasse einfach erstellen und verwalten. Wir erstellen eine leere Liste und fügen dann 20 Objekte der Klasse Person
hinzu. In einer Schleife rufen wir sie dann alle auf und lassen sie zeichnen.
import matplotlib.pyplot as plt
%matplotlib inline
import random
class Person:
def __init__(self):
#eine neue Person wird initialisiert
#sie bekommt eine zufällig Heimadresse
self.home = [random.randint(0,10),random.randint(0,10)]
def zeichnen(self):
#Die Personen zeichnen ihre Heimatadresse
#s ist die Größe des Markers, die Form "s" steht für square
plt.scatter(self.home[0],self.home[1], s = 100, marker = "s", c = "forestgreen")
#----Hier startet das Hauptprogramm----
#Wir erstellen eine leere Liste
allepersonen=[]
#Wir fügen neue Personen
for it in range(20):
allepersonen.append(Person())
for it in range(20):
allepersonen[it].zeichnen()
Die letzte Schleife läuft hierbei über eine Liste von Objekten und ruft im it-ten Schritt das it-te Objekt auf um etwas damit zu machen. Diese Struktur ist so häufig, dass es in Python dafür eine Kurzschreibweise gibt. Statt
for it in range(20):
$\ \ \ \ \ \ $Befehl für Objekt das unter listenname[it] gespeichert ist
kann man verkürz schreiben
for obj in listenname:
$\ \ \ \ \ \ $Befehl für obj
, wobei obj
eine frei wählbare Bezeichnung ist.
Für unser Programm wäre diese Kurzschreibweise also:
import matplotlib.pyplot as plt
%matplotlib inline
import random
class Person:
def __init__(self):
#eine neue Person wird initialisiert
#sie bekommt eine zufällig Heimadresse
self.home = [random.randint(0,10),random.randint(0,10)]
def zeichnen(self):
#Die Personen zeichnen ihre Heimatadresse
#s ist die Größe des Markers, die Form "s" steht für square
plt.scatter(self.home[0],self.home[1], s = 100, marker = "s", c = "forestgreen")
#----Hier startet das Hauptprogramm----
#Wir erstellen eine leere Liste
allepersonen=[]
#Wir fügen neue Personen
for it in range(20):
allepersonen.append(Person())
for p in allepersonen:
p.zeichnen()
Zusätzlich zu einer Heimatadresse bekommen alle Personen nun auch einen Arbeitsort. In unserem Modell gibt es 3 Arbeitsorte, die unterschiedlich groß sind. Ein Arbeitsort befindet sich auf Position [7,9], einer auf [4,1] und einer auf [5,5].
Beim Intitialisieren bekommen alle Personen also auch einen Arbeitsort aus einer Liste der Arbeitsorte zugewiesen. Da nicht alle Arbeitsorte gleich oft besucht werden, kommen auch nicht alle Orte gleich oft in der Liste vor.
Auch in der Methode zeichnen
nehmen wir eine Änderung vor: Auch der Arbeitsplatz soll gezeichnet werden.
import matplotlib.pyplot as plt
%matplotlib inline
import random
class Person:
def __init__(self):
#eine neue Person wird initialisiert
#sie bekommt eine zufällig Heimadresse
self.home = [random.randint(0,10),random.randint(0,10)]
#und eine Arbeitsadresse aus einer Liste
self.work = random.choice([[5,5],[5,5],[5,5],[4,1],[4,1],[7,9]])
def zeichnen(self):
#Die Personen zeichnen ihre Heimatadresse
#s ist die Größe des Markers, die Form "s" steht für square
plt.scatter(self.home[0],self.home[1], s = 100, marker = "s", c = "forestgreen")
#Und ihren Arbeitsort
plt.scatter(self.work[0],self.work[1], s = 100, marker = "s", c = "deepskyblue")
#----Hier startet das Hauptprogramm----
#Wir erstellen eine leere Liste
allepersonen=[]
#Wir fügen neue Personen
for it in range(20):
allepersonen.append(Person())
for p in allepersonen:
p.zeichnen()
Nun möchten wir noch eine wichtige Methode in unser Verkehrssystem einbauen. Personen sollen die Fähigkeit bekommen von ihrem Heimatort zum Arbeitsort zu fahren. Damit sehen wir dann, welche Straßen der Modellstadt häufig benutzt werden und welche nicht so häufig.
Für das Fahren benutzen wir die sogenannte Manhattan-Metrik: In dieser Metrik ist es nur erlaubt entlang der x-Achse und entlang der y-Achse zu fahren, nicht aber diagonal. Unsere Personen fahren also zuerst in x-Richtung bei konstantem y, dann in y-Richtung bei konstantem x.
Das Fahren selbst implementieren wir einfach als Linienplots zusätzlich zu unserem bisherigen Plot.
Erstellen wir nun die Methode fahren
und rufen wir sie für alle Personen auf.
import matplotlib.pyplot as plt
%matplotlib inline
import random
class Person:
def __init__(self):
#eine neue Person wird initialisiert
#sie bekommt eine zufällig Heimadresse
self.home = [random.randint(0,10),random.randint(0,10)]
#und eine Arbeitsadresse aus einer Liste
self.work = random.choice([[5,5],[5,5],[5,5],[4,1],[4,1],[7,9]])
def zeichnen(self):
#Die Personen zeichnen ihre Heimatadresse
#s ist die Größe des Markers, die Form "s" steht für square
plt.scatter(self.home[0],self.home[1], s = 100, marker = "s", c = "forestgreen")
#Und ihren Arbeitsort
plt.scatter(self.work[0],self.work[1], s = 100, marker = "s", c = "deepskyblue")
def fahren(self):
#Wir zeichnen zwei Linien:
#Linie 1 geht in der x-Richtung von home nach work, y bleibt konstant bei home
#Linie 2 geht in y-Richtung von home nach work, x bleibt konstant bei work
plt.plot([self.home[0],self.work[0]],[self.home[1],self.home[1]],c = "black", lw = 4)
plt.plot([self.work[0],self.work[0]],[self.home[1],self.work[1]],c = "black", lw = 4)
#----Hier startet das Hauptprogramm----
#Wir erstellen eine leere Liste
allepersonen=[]
#Wir fügen neue Personen
for it in range(20):
allepersonen.append(Person())
for p in allepersonen:
p.zeichnen()
p.fahren()
Nun sehen wir welche Strecken benutzt werden, aber nicht unbedingt welche wie oft benutzt wird, da wir oft Linien übereinderzeichnen und der Plot keine Auskunft darüber gibt, wie viele Linien übereinander gezeichnet wurden.
Dieses Problem können wir mit dem Attribut alpha
lösen. Das alpha
einer Linie sagt uns die Deckkraft. Der Wert 0.1 sagt, dass nur 10 Prozent Deckkraft benutzt werden, also erst wenn wir 10 Linien übereinander zeichnen, sieht die Linie wirklich schwarz aus, sonst sieht man Teile des weißen Hintergrunds durchschimmern. Damit sollten wir mehr über unser Verkehrssystem lernen können.
import matplotlib.pyplot as plt
%matplotlib inline
import random
class Person:
def __init__(self):
#eine neue Person wird initialisiert
#sie bekommt eine zufällig Heimadresse
self.home = [random.randint(0,10),random.randint(0,10)]
#und eine Arbeitsadresse aus einer Liste
self.work = random.choice([[5,5],[5,5],[5,5],[4,1],[4,1],[7,9]])
def zeichnen(self):
#Die Personen zeichnen ihre Heimatadresse
#s ist die Größe des Markers, die Form "s" steht für square
plt.scatter(self.home[0],self.home[1], s = 100, marker = "s", c = "forestgreen")
#Und ihren Arbeitsort
plt.scatter(self.work[0],self.work[1], s = 100, marker = "s", c = "deepskyblue")
def fahren(self):
#Wir zeichnen zwei Linien:
#Linie 1 geht in der x-Richtung von home nach work, y bleibt konstant bei home
#Linie 2 geht in y-Richtung von home nach work, x bleibt konstant bei work
plt.plot([self.home[0],self.work[0]],[self.home[1],self.home[1]],c = "black", alpha = 0.1, lw = 4)
plt.plot([self.work[0],self.work[0]],[self.home[1],self.work[1]],c = "black", alpha = 0.1, lw = 4)
#----Hier startet das Hauptprogramm----
#Wir erstellen eine leere Liste
allepersonen=[]
#Wir fügen neue Personen
for it in range(20):
allepersonen.append(Person())
for p in allepersonen:
p.zeichnen()
p.fahren()
Noch mehr Informationen über das Verkehrssystem würden wir erhalten, wenn wir die zurückgelegte Gesamtstrecke berechnen würden. Dazu werden wir in einem ersten Schritt eine Methode einbauen, die zu jeder Person die Länge des Arbeitsweges ausgibt.
import matplotlib.pyplot as plt
%matplotlib inline
import random
class Person:
def __init__(self):
#eine neue Person wird initialisiert
#sie bekommt eine zufällig Heimadresse
self.home = [random.randint(0,10),random.randint(0,10)]
#und eine Arbeitsadresse aus einer Liste
self.work = random.choice([[5,5],[5,5],[5,5],[4,1],[4,1],[7,9]])
def zeichnen(self):
#Die Personen zeichnen ihre Heimatadresse
#s ist die Größe des Markers, die Form "s" steht für square
plt.scatter(self.home[0],self.home[1], s = 100, marker = "s", c = "forestgreen")
#Und ihren Arbeitsort
plt.scatter(self.work[0],self.work[1], s = 100, marker = "s", c = "deepskyblue")
def fahren(self):
#Wir zeichnen zwei Linien:
#Linie 1 geht in der x-Richtung von home nach work, y bleibt konstant bei home
#Linie 2 geht in y-Richtung von home nach work, x bleibt konstant bei work
plt.plot([self.home[0],self.work[0]],[self.home[1],self.home[1]],c = "black", alpha = 0.1, lw = 4)
plt.plot([self.work[0],self.work[0]],[self.home[1],self.work[1]],c = "black", alpha = 0.1, lw = 4)
def arbeitsweg(self):
#der Weg errechnet sieh hier aus dem Abstand in x-Richtung + dem Abstand in y-Richtung
dist = abs(self.home[0]-self.work[0]) + abs(self.home[1]-self.work[1])
return dist
#----Hier startet das Hauptprogramm----
#Wir erstellen eine leere Liste
allepersonen=[]
#Wir fügen neue Personen
for it in range(20):
allepersonen.append(Person())
for p in allepersonen:
p.zeichnen()
p.fahren()
Nun schreiben wir noch eine Function, die uns alle Strecken aufsummiert und die Summe auf den Bildschirm ausgibt. Achtung: Diese Function ist keine Methode der Klasse, denn eine einzelne Person kann nicht über die Strecken aller anderen Personen bescheidwissen. Deswegen ist die Defintion dieser Funktion nicht eingerückt, also nicht teil der Klasse:
import matplotlib.pyplot as plt
%matplotlib inline
import random
class Person:
def __init__(self):
#eine neue Person wird initialisiert
#sie bekommt eine zufällig Heimadresse
self.home = [random.randint(0,10),random.randint(0,10)]
#und eine Arbeitsadresse aus einer Liste
self.work = random.choice([[5,5],[5,5],[5,5],[4,1],[4,1],[7,9]])
def zeichnen(self):
#Die Personen zeichnen ihre Heimatadresse
#s ist die Größe des Markers, die Form "s" steht für square
plt.scatter(self.home[0],self.home[1], s = 100, marker = "s", c = "forestgreen")
#Und ihren Arbeitsort
plt.scatter(self.work[0],self.work[1], s = 100, marker = "s", c = "deepskyblue")
def fahren(self):
#Wir zeichnen zwei Linien:
#Linie 1 geht in der x-Richtung von home nach work, y bleibt konstant bei home
#Linie 2 geht in y-Richtung von home nach work, x bleibt konstant bei work
plt.plot([self.home[0],self.work[0]],[self.home[1],self.home[1]],c = "black", alpha = 0.1, lw = 4)
plt.plot([self.work[0],self.work[0]],[self.home[1],self.work[1]],c = "black", alpha = 0.1, lw = 4)
def arbeitsweg(self):
#der Weg errechnet sieh hier aus dem Abstand in x-Richtung + dem Abstand in y-Richtung
dist = abs(self.home[0]-self.work[0]) + abs(self.home[1]-self.work[1])
return dist
def gesamtstrecke():
#wir berechnen die Gesamtstrecke
gesamt = 0
#Wir gehen in einer Schleife über alle Personen
for p in allepersonen:
#und summieren die Arbeitswege
gesamt = gesamt + p.arbeitsweg()
print("Gesamtstrecke:")
print(gesamt)
#----Hier startet das Hauptprogramm----
#Wir erstellen eine leere Liste
allepersonen=[]
#Wir fügen neue Personen
for it in range(20):
allepersonen.append(Person())
for p in allepersonen:
p.zeichnen()
p.fahren()
gesamtstrecke()
Hier steht nun die Gesamtstrecke über dem Plot, obwohl wir die gesamtstrecke
erst danach aufrufen. Das hat damit zu tun, dass Python standardmäßig mit den Plots wartet und sie erst auf den Bildschirm gibt, wenn alle anderen Berechnungen fertig sind. Mit dem Befehl plt.show()
können wir einen Plot sofort anzeigen lassen.
import matplotlib.pyplot as plt
%matplotlib inline
import random
class Person:
def __init__(self):
#eine neue Person wird initialisiert
#sie bekommt eine zufällig Heimadresse
self.home = [random.randint(0,10),random.randint(0,10)]
#und eine Arbeitsadresse aus einer Liste
self.work = random.choice([[5,5],[5,5],[5,5],[4,1],[4,1],[7,9]])
def zeichnen(self):
#Die Personen zeichnen ihre Heimatadresse
#s ist die Größe des Markers, die Form "s" steht für square
plt.scatter(self.home[0],self.home[1], s = 100, marker = "s", c = "forestgreen")
#Und ihren Arbeitsort
plt.scatter(self.work[0],self.work[1], s = 100, marker = "s", c = "deepskyblue")
def fahren(self):
#Wir zeichnen zwei Linien:
#Linie 1 geht in der x-Richtung von home nach work, y bleibt konstant bei home
#Linie 2 geht in y-Richtung von home nach work, x bleibt konstant bei work
plt.plot([self.home[0],self.work[0]],[self.home[1],self.home[1]],c = "black", alpha = 0.1, lw = 4)
plt.plot([self.work[0],self.work[0]],[self.home[1],self.work[1]],c = "black", alpha = 0.1, lw = 4)
def arbeitsweg(self):
#der Weg errechnet sieh hier aus dem Abstand in x-Richtung + dem Abstand in y-Richtung
dist = abs(self.home[0]-self.work[0]) + abs(self.home[1]-self.work[1])
return dist
def gesamtstrecke():
#wir berechnen die Gesamtstrecke
gesamt = 0
#Wir gehen in einer Schleife über alle Personen
for p in allepersonen:
#und summieren die Arbeitswege
gesamt = gesamt + p.arbeitsweg()
print("Gesamtstrecke:")
print(gesamt)
#----Hier startet das Hauptprogramm----
#Wir erstellen eine leere Liste
allepersonen=[]
#Wir fügen neue Personen
for it in range(20):
allepersonen.append(Person())
for p in allepersonen:
p.zeichnen()
p.fahren()
plt.show()
gesamtstrecke()
Nun möchten wir versuchen, ob wir diesen Verkehrssystem noch ein wenig optimieren können. Viele Personen leben weit weg von ihrem Arbeitsort und könnten sich durch Umziehen viel Strecke sparen. Schreiben wir also eine Methode, die den Heimatort einer Person probeweise ändert. Sollte sich der Arbeitsweg dadurch verlängert haben, wird die Änderung wieder rückgängig gemacht.
Diese Methode sollte aus 3 Teilen bestehen: Zuerst merken wir uns die aktuelle Strecke und Heimatadresse. Dann ändern wir die Heimatadresse auf einen neuen zufälligen Wert. Im letzten Schritt wird die Änderung rückgängig gemacht, wenn sie unvorteilhaft war (dafür benötigen wir die Informationen über alte Strecke und Heimatadresse).
Wir definieren diese Methode und geben unseren Personen dann 10 mal die Chance umzuziehen. Danach wiederholen wir den Plot und die Streckenberechnung, um zu sehen welchen Effekt diese Änderung hatte.
import matplotlib.pyplot as plt
%matplotlib inline
import random
class Person:
def __init__(self):
#eine neue Person wird initialisiert
#sie bekommt eine zufällig Heimadresse
self.home = [random.randint(0,10),random.randint(0,10)]
#und eine Arbeitsadresse aus einer Liste
self.work = random.choice([[5,5],[5,5],[5,5],[4,1],[4,1],[7,9]])
def zeichnen(self):
#Die Personen zeichnen ihre Heimatadresse
#s ist die Größe des Markers, die Form "s" steht für square
plt.scatter(self.home[0],self.home[1], s = 100, marker = "s", c = "forestgreen")
#Und ihren Arbeitsort
plt.scatter(self.work[0],self.work[1], s = 100, marker = "s", c = "deepskyblue")
def fahren(self):
#Wir zeichnen zwei Linien:
#Linie 1 geht in der x-Richtung von home nach work, y bleibt konstant bei home
#Linie 2 geht in y-Richtung von home nach work, x bleibt konstant bei work
plt.plot([self.home[0],self.work[0]],[self.home[1],self.home[1]],c = "black", alpha = 0.1, lw = 4)
plt.plot([self.work[0],self.work[0]],[self.home[1],self.work[1]],c = "black", alpha = 0.1, lw = 4)
def arbeitsweg(self):
#der Weg errechnet sieh hier aus dem Abstand in x-Richtung + dem Abstand in y-Richtung
dist = abs(self.home[0]-self.work[0]) + abs(self.home[1]-self.work[1])
return dist
def umziehen(self):
#Methode um den Heimatort zu wechseln
#wir speichern den aktuellen Weg und den aktuellen Heimatort
alterweg = self.arbeitsweg()
oldhome = self.home
#die Person zieht auf ein zufällige Position um
self.home = [random.randint(0,10),random.randint(0,10)]
#Wenn der Weg dadurch länger geworden ist...
if self.arbeitsweg() > alterweg:
#... wird die Änderung rückgängig gemacht
self.home = oldhome
def gesamtstrecke():
#wir berechnen die Gesamtstrecke
gesamt = 0
#Wir gehen in einer Schleife über alle Personen
for p in allepersonen:
#und summieren die Arbeitswege
gesamt = gesamt + p.arbeitsweg()
print("Gesamtstrecke:")
print(gesamt)
#----hier beginnt das eigentliche Programm----
#Wir erstellen eine leere Liste
allepersonen=[]
#Wir fügen neue Personen
for it in range(20):
allepersonen.append(Person())
#Wir iterieren über alle Personen
for p in allepersonen:
#Jede Person benutzt die Methode zeichnen und fahren
p.zeichnen()
p.fahren()
#Der Plot wird angezeigt
plt.show()
#Die Gesamtstrecke wird berechnet
gesamtstrecke()
#Wir lassen die Personen umziehen
for it in range(10):
for p in allepersonen:
p.umziehen()
#Eine neue Figur wird erstellt
plt.figure()
#Wieder wird gezeichnet und die Gesamtstrecke berechnet
for p in allepersonen:
p.zeichnen()
p.fahren()
plt.show()
gesamtstrecke()
Dieses Modell könnten wir nun natürlich beliebig erweitern. Wir können neue Methoden und Eigenschaften einbauen, oder die bestehenden verfeinern. Methoden können (gleich wie Functions) auch mehrere Inputwerte haben. Natürlich kann ein Programm auch mehrere verschiedene Klassen beinhalten.
Ein großer Vorteil von Klassen und Objekten: Das Hauptprogramm bleibt immer sehr gut lesbar und es gibt eine strikte Trennung von was passieren soll (Hauptprogramm) und wie es passieren soll (Methoden und Functions). Somit kann man ein Hauptprogramm meist benutzen, ohne dass man die einzelnen Functions und Methoden bis ins letzte Detail verstehen muss.
Dieses Konzept ist mehr oder weniger das Grundkonzept von modernem Programmieren: Schon von Anfang an haben wir zum Beispiel mit der Function plot
Grafiken erstellt und Elemente zu einem Objekt der Klasse Liste hinzugefügt, ohne dass wir uns je mit der genauen Definition dieser Klassen beschäftigen mussten. Das Verwenden von vorgefertigen Functions, Klassen und sogar ganzen Bibliotheken und packages macht modernes Programmieren überhaupt erst möglich.
In Python können wir Klassen definieren und dann Objekte erstellen, die diesen Klassen angehören. Jede Klasse wird durch class NameDerKlasse:
definiert und hat eine Initialisierungs-Methode namens __init__
die dann vom Hauptprogramm mit dem Namen der Klasse aufgerufen wird. Durch diese Methode wird ein neues Objekt dieser Klasse erstellt, das unter einem Namen oder in einer Liste gespeichert werden kann.
Eigenschaften sind Variablen, die alle Objekte von einer Klasse haben. So hat zum Beispiel jedes Objekt der Klasse "Person" in unserem Beispiel einen Heimatort. Diese Eigenschaften werden meistens in der Initialisierungsmethode erstellt und gespeichert. Eigenschaften werden innerhalb der Definitionen mit self.NameDerEigenschaft
aufgerufen und verwändert. Außerhalb der Definition muss man auch angeben auf welches Objekt der Klasse man sich bezieht, also NameDesObjekts.NameDerEigenschaft
.
Methoden sind sozusagen Functions innerhalb von Klassen. Sie können Input- und Outputvariablen haben und werden ähnlich wie übliche Functions definiert. Die erste Inputvariable ist bei Methoden jedoch immer self
.
Methoden werden innerhalb einer Klassendefinition mit NameDerMethode(self,inputA,inputB,inputC,...):
definiert und dann im Haupprogramm mit NameDesObjekts.NameDerMethode(inputA,inputB,inputC,...)
aufgerufen. Man beachte, dass man beim Aufrufen kein self
mehr benötigt, da der Name des Objekts schon vor dem Punkt steht.
Schleifen, die über eine Liste von Objekten laufen und im i-ten Schritt das i-te Objekt aufrufen um etwas damit zu machen, lassen sich verkürzt schreiben. Statt
for it in range(20):
$\ \ \ \ \ \ $Befehl für Objekt das unter listenname[it] gespeichert ist
kann man immer
for obj in listenname:
$\ \ \ \ \ \ $Befehl für obj
schreiben, wobei obj
eine frei wählbare Bezeichnung ist.
In diesem Kapitel nutzen wir unser Wissen über Klassen und andere Pythonstrukturen um ein einfaches Verkehrsmodell zu erstellen. Wir möchten herausfinden wie viele direkte (und für e-Autos auch indirekte) Emissionen Fahrzeuge verursachen und einige Szenarios durchrechnen.
Beginnen werden wir mit einer einfachen Klasse namens Car
. Fürs erste soll diese Klasse nur einen Initialisierungsmethode bekommen und 3 Eigenschaften haben: age
, size
und distance
. Diese drei Größen sind die Argumente der Initialisierungsmethode. Danach erstellen wir ein Testobjekt dieser Klasse und lassen uns die Eigenschaften wieder ausgeben.
class Car:
def __init__(self, age, size, distance):
self.age = age
self.size = size
self.distance = distance
testcar = Car(2,"m",5000)
print(testcar.age,testcar.size,testcar.distance)
Das nächste was für uns relevante wäre, wären die CO2-Emissionen des Autos, die es pro Kilometer verursacht. Diese Eigenschaft werden wir aber nicht einfach einlesen, sondern aus den vorhandenen Informationen (also Größe und Alter) abschätzen. Dazu erstellen wir eine eigene Methode. Die Zahlenwerte, die wir angenommen haben orientieren sich an Hofer, Jäger, Füllsack: Large scale simulation of CO2 emissions caused by urban car traffic.
class Car:
def __init__(self, age, size, distance):
self.age = age
self.size = size
self.distance = distance
self.calcemissions()
def calcemissions(self):
if self.size == "s":
self.emissions = 120 * (1 + age * 0.02) #g/km
elif self.size == "m":
self.emissions = 140 * (1 + age * 0.02)
elif self.size == "l":
self.emissions = 180 * (1 + age * 0.02)
testcar = Car(2,"m",5000)
print(testcar.age,testcar.size,testcar.distance,testcar.emissions)
Hier müssen wir aber wieder vorsichtig sein: Wenn unter Size etwas steht, was wir nicht erwarten (wie z.B. "groß" oder "👶"), überspringen wir alle ifs und die Eigenschaft emissions
wird nicht gesetzt. Wir brauchen also eine Fehlermeldung, die auf dieses Problem hinweist, um zu verhindern, dass wir mit fehlenden oder falschen Daten weiterarbeiten.
class Car:
def __init__(self, age, size, distance):
self.age = age
self.size = size
self.distance = distance
self.calcemissions()
def calcemissions(self):
if self.size == "s":
self.emissions = 120 * (1 + age * 0.02) #g/km
elif self.size == "m":
self.emissions = 140 * (1 + age * 0.02)
elif self.size == "l":
self.emissions = 180 * (1 + age * 0.02)
else:
raise Exception("Unknown car size: " + str(self.size) )
testcar = Car(2,"👶",5000)
print(testcar.age,testcar.size,testcar.distance,testcar.emissions)
Nun, wo wir jedem Fahrzeug einen Emissionswert zuweisen können, können wir auch eine Methode bauen, die diesen Wert zurückgibt. Das ist alles was wir brauchen, um die Gesamtemissionen einer Fahrzeugflotte zu berechnen.
Um die Methode zu testen, erstellen wir auch gleich eine Population an Fahrzeugen.
import random
class Car:
def __init__(self, age, size, distance):
self.age = age
self.size = size
self.distance = distance
self.calcemissions()
def calcemissions(self):
if self.size == "s":
self.emissions = 120 * (1 + age * 0.02) #g/km
elif self.size == "m":
self.emissions = 140 * (1 + age * 0.02)
elif self.size == "l":
self.emissions = 180 * (1 + age * 0.02)
else:
raise Exception("Unknown car size: " + str(self.size) )
def reportemissions(self):
return self.distance * self.emissions
def makepopulation (distmean,diststd):
allcars = []
for it in range(10000):
age = random.uniform(0,20)
dist = random.gauss(distmean,diststd)
size = random.choice(["s","m","l"])
allcars.append(Car(age,size,dist))
return allcars
distmean = 5500
diststd = 50
#overall emissions
allcars = makepopulation(distmean, diststd)
em_sum = 0
for car in allcars:
em_sum = em_sum + car.reportemissions()
print(em_sum / 1000 / 10000, "kg pro Person")
Interessant wird dieses Modell aber erst, wenn wir verschiedene Szenarios rechnen können, und diese miteinander vergleichen. Hier haben wir sehr viele Möglichkeiten. Wir könnten beispielsweise anschauen was passiert, wenn alle Personen, deren Auto älter ist als 10 Jahre, ein neues Auto kaufen. Alternativ können wir betrachten, was passiert wenn 25% der Personen auf mehr Busfahrten umsteigen, was ihre jährliche Fahrstrecke auf 10% reduziert. Für beide Szenarien brauchen wir neue Methoden: Eine die das Alter eines Fahrzeugs auf 0 setzt und eine die die gefahrene Strecke verändert.
Mit diesen neuen Methoden ist das Erstellen und Auswerten der Szenarien einfach:
import random
class Car:
def __init__(self, age, size, distance):
self.age = age
self.size = size
self.distance = distance
self.calcemissions()
def calcemissions(self):
if self.size == "s":
self.emissions = 120 * (1 + age * 0.02) #g/km
elif self.size == "m":
self.emissions = 140 * (1 + age * 0.02)
elif self.size == "l":
self.emissions = 180 * (1 + age * 0.02)
else:
raise Exception("Unknown car size: " + str(self.size) )
def reportemissions(self):
return self.distance * self.emissions
def buynewcar(self):
self.age = 0
self.calcemissions()
def usebus(self):
self.distance = self.distance / 10
def makepopulation (distmean,diststd):
allcars = []
for it in range(10000):
age = random.uniform(0,20)
dist = random.gauss(distmean,diststd)
size = random.choice(["s","m","l"])
allcars.append(Car(age,size,dist))
return allcars
distmean = 5500
diststd = 50
#overall emissions
allcars = makepopulation(distmean, diststd)
em_sum = 0
for car in allcars:
em_sum = em_sum + car.reportemissions()
print("Baseline: ", em_sum / 1000 / 10000, "kg pro Person")
#Szenario 1: Autos älter als 10 Jahre werden ersetzt
allcars = makepopulation(distmean, diststd)
em_sum = 0
for car in allcars:
if car.age > 10:
car.buynewcar()
em_sum = em_sum + car.reportemissions()
print("Szenario 1:",em_sum / 1000 / 10000, "kg pro Person")
#Szenario 2: 25% benutzen einen Bus
allcars = makepopulation(distmean, diststd)
em_sum = 0
for car in allcars:
if random.uniform(0,100)<20:
car.usebus()
em_sum = em_sum + car.reportemissions()
print("Szenario 2:",em_sum / 1000 / 10000, "kg pro Person")
Nun können wir noch Elektroautos in unser Modell einbauen. Das wirft eine interessante Frage auf: Können wir die Klasse Car
auch für E-Autos verwenden? Dafür spricht, dass wir die meisten Eigenschaften und Methoden genau gleich verwenden könnten. Dagegen spricht, dass das Berechnen der Emissionen bei E-Autos anders funktioniert. Hier hilft uns das Konzept von Erbschaft. Wir können in Python, basierend auf eine Klasse, eine Unterklasse erstellen, die alle Eigenschaften und Methoden der ursprünglichen Klasse erbt, aber darüber hinaus auch noch weitere Eigenschaften und Methoden besitzen kann. Außerdem ist es möglich, exisitierende Methoden für die Unterklasse anzupassen.
In unserem Modell können wir also die Unterklasse Ecar
erstellen, die alles von Car
erbt, aber eine andere Methode reportemissions
besitzt:
class Ecar(Car):
def reportemissions(self):
return "noch unbekannt"
Wie man sieht ist diese Klassendefinition sehr schlank, da wir das meiste aus Car
erben. Nun müssen wir uns nur mehr überlegen, wie wir die Emissionen des E-Autos berechnen. Die Sache ist natürlich ein wenig komplizierter, denn die Emissionen entstehen nicht direkt auf der Straße, sondern wo auch immer der Strom erzeugt wurde, mit dem das Fahrzeug geladen wurde. Es kommt also ganz stark auf den Energiemix an, der verwendet wurde.
Die gesuchte Methode wird also so aussehen, dass wir die Distanz mit einer Zahl multiplizieren, die uns sagt wie viel Emissionen pro Kilometer durch die Stromerzeugung entstanden sind. Das korrekt durchzuführen ist natürlich schwierig, aber mit einem Life-Cycle-Assessement (LCA) können wir zumindest unterschiedliche Arten Strom zu produzieren vergleichen. Das gibt uns auch Anhaltspunkte für eine grobe Abschätzung der verursachten Emissionen. Details zu diesem Prozess findet man zum Beispiel in Turconi, Boldrin, Astrup.
Hier nehmen wir einen Verbrauch von 0.2 kWh pro Kilometer an und vergleichen den Energiemix von Graz, Wien und den USA. Wir finden folgende Werte:
Diese Werte verwenden wir in der Methode reportemissions
.
Um die E-Autos dann auch zu testen, müssen wir unsere Function makepopulation
noch ein wenig anpassen. Wir benötigen zusätzlich den Prozentsatz der vorhandenen Elektroautos. Diese können wir für Österreich mit 0.4% abschätzen.
import random
class Car:
def __init__(self, age, size, distance):
self.age = age
self.size = size
self.distance = distance
self.calcemissions()
def calcemissions(self):
if self.size == "s":
self.emissions = 120 * (1 + age * 0.02) #g/km
elif self.size == "m":
self.emissions = 140 * (1 + age * 0.02)
elif self.size == "l":
self.emissions = 180 * (1 + age * 0.02)
else:
raise Exception("Unknown car size: " + str(self.size) )
def reportemissions(self):
return self.distance * self.emissions
def buynewcar(self):
self.age = 0
self.calcemissions()
def usebus(self):
self.distance = self.distance / 10
class Ecar(Car):
def reportemissions(self):
return self.distance * emix
def makepopulation (echance,distmean,diststd):
allcars = []
for it in range(10000):
age = random.uniform(0,20)
dist = random.gauss(distmean,diststd)
size = random.choice(["s","m","l"])
if random.uniform(0,100)<echance:
allcars.append(Ecar(age,size,dist))
else:
allcars.append(Car(age,size,dist))
return allcars
emix = 2.5 #GRAZ: 90% Wasser (10g/kWh), 5% Wind (30g/kWh), 5% Biomasse(50g/kWh)
#emix = 45 #WIEN: 45% Wasser, 45% Erdgas (500g/kWh), 5% Wind, 5% Biomasse
#emix = 100 #USA: 37% Öl (700g/kWh), 30% Gas (500g/kWh), 15% Kohle (800g/kWh), 10% Nuklear(20g/kWh), 8% renewable (30g/kWh)
echance = 0.4
distmean = 5500
diststd = 50
#overall emissions
allcars = makepopulation(echance, distmean, diststd)
em_sum = 0
for car in allcars:
em_sum = em_sum + car.reportemissions()
print("Baseline: ", em_sum / 1000 / 10000, "kg pro Person")
#Szenario 1: Autos älter als 10 Jahre werden ersetzt
allcars = makepopulation(echance, distmean, diststd)
em_sum = 0
for car in allcars:
if car.age > 10:
car.buynewcar()
em_sum = em_sum + car.reportemissions()
print("Szenario 1:",em_sum / 1000 / 10000, "kg pro Person")
#Szenario 2: 25% benutzen einen Bus
allcars = makepopulation(echance, distmean, diststd)
em_sum = 0
for car in allcars:
if random.uniform(0,100) < 20:
car.usebus()
em_sum = em_sum + car.reportemissions()
print("Szenario 2:",em_sum / 1000 / 10000, "kg pro Person")
Somit ist es uns gelungen ein ganz einfaches, makroskopisches Verkehrsmodell in Python zu bauen. Dies lässt sich natürlich beliebig erweitern. Die Befehle und Strukturen die auftreten, werden jedoch auch bei äußerst komplexen Programmen die selben bleiben: Wenn man Klassen, Functions, Schleifen, Abfragen und Datentypen verstanden hat, kann man prinzipiell jedes nur denkbare Programm schreiben.
Initialisierungsmethoden können auch Argumente verlangen. Das ist völlig analog zu Functions mit
def __init__(self,arg1,arg2,arg3)
möglich. Diese Argumente (ohne self
) müssen dann aber auch beim Erstellen eines Objekts übergeben werden:
obj = NameDerKlasse(arg1,arg2,arg3)
Wenn man eine neue Klasse erstellen möchte, die auf einer existierenden Klasse basieren soll, gibt es das Konzept der Erbschaft. Damit können wir alle Methoden und Eigenschaften der Grundklasse übernehmen, diese aber ändern und ausbauen.
Class NameDerNeuenKlasse(NameDerGrundklasse):
Egal, was programmiert werden soll, ob ein Modell erstellt wird, eine Simulation durchgeführt oder statistische Daten ausgewertet werden: am Ende sollen die Ergebnisse meist visualisiert werden. Die Darstellung von Ergebnissen - das "Plotten" - ist oft ebenso wichtig, wie die Ergebnisse selbst und es gibt unzählige Arten des Visualisierens.
Einige wichtige Regeln zum Erstellen von Grafiken sollten, unabhängig von der Art der Darstellung, immer beachtet werden:
Im Folgenden geben wir eine Übersicht über die gängigsten Arten, Ergebnisse zu visualisieren. Einige davon haben wir bereits kennengelernt, anderen sind wir noch nicht begegnet.
Eine Übersicht über alle Farben, die in Matplotlib einen Eigennamen haben finden Sie hier: https://matplotlib.org/examples/color/named_colors.html
import matplotlib.pyplot as plt
%matplotlib inline
testdaten = [1, 2, 3, 4, 7, 8, 7, 5, 4, 2]
plt.xlabel("Das ist die Beschriftung der x-Achse")
plt.ylabel("Das ist die Beschriftung der y-Achse")
plt.title("Titel des Plots")
plt.plot(testdaten, color = "forestgreen", lw = 2, marker = "*", markersize = 10, label = "Linienbeschriftung")
plt.legend(loc = "best")
Ein Linienplot ist eine der einfachsten Arten, Daten zu visualisieren. Immer wenn man eine Größe (z.B. Anzahl der Frösche) hat, die man gegen eine andere Größe auftragen möchte (z.B. Zeit) bietet sich ein Linienplot an.
import matplotlib.pyplot as plt
%matplotlib inline
testdaten = [1, 4, 8, 6, 7, 5, 6, 5, 4, 1]
plt.xlabel("Das ist die Beschriftung der x-Achse")
plt.ylabel("Das ist die Beschriftung der y-Achse")
plt.title("Titel des Plots")
plt.fill(testdaten, color = "orange", label = "Beschriftung")
plt.legend(loc = "best")
Der Fill-Plot ist konzeptionell ähnlich wie der Linienplot, nur möchte man hier eher auf die Fläche unter der Kurve hinweisen, als auf die Kurve selbst.
import matplotlib.pyplot as plt
%matplotlib inline
testdaten = [1, 1, 1, 1, 2, 2, 1, 1, 3]
testdaten2 = [1, 3, 1, 1, 2, 2, 1, 3, 1]
testdaten3 = [1, 2, 1, 1, 1, 2, 1, 1, 1]
plt.xlabel("Das ist die Beschriftung der x-Achse")
plt.ylabel("Das ist die Beschriftung der y-Achse")
plt.title("Titel des Plots")
plt.stackplot(range(len(testdaten)), testdaten, testdaten2, testdaten3, labels = ("B1","B2","B3"),
colors=["chocolate","peru","burlywood"])
plt.legend(loc = "best")
Beim Stackplot können wir mehrere Fill-Plots übereinanderstapeln. Der Stackplot ist relativ schwer zu lesen, da man ihn leicht mit einem Linienplot verwechselt. In unserem Beispiel, würde man also fälschlicherweise glauben, dass die Größe B3 am Anfang den Wert 3 hat. In wirklichkeit ist der Plot aber so zu lesen: B3 geht von 2 bis 3, hat also den Wert 1. Auch Steigungen kann man in einem Stackplot nicht mehr korrekt ablesen. Stackplots sind also mit Vorsicht zu genießen. Wo sie aber doch Sinn machen, ist wenn man Größen hat, die sich immer auf eine gemeinsame Summe ergänzen. Zum Beispiel den prozentuellen Anteil einer gewissen Heiz-Technologie. Alle Technologien sollten sich immer auf 100% ergänzen:
import matplotlib.pyplot as plt
%matplotlib inline
tec1 = [54,50,45,42,30,25,20,11,8]
tec2 = [26,26,28,31,44,50,53,62,61]
tec3 = [15,14,15,14,14,13,14,15,18]
tec4 = [ 5,10,12,13,12,12,13,12,13]
plt.xlabel("Das ist die Beschriftung der x-Achse")
plt.ylabel("Das ist die Beschriftung der y-Achse")
plt.title("Titel des Plots")
plt.stackplot(range(len(tec1)), tec1, tec2, tec3, tec4, labels=("Kohle","Fernwaerme","Oel","Sonstige"),
colors = ["brown","lightsalmon","black","gray"])
plt.legend(loc = "lower left")
Alle bisherigen Plots hatten mehr oder weniger das gleiche Anwendungsgebiet: Wir möchten eine Größe $y$ (Anzahl der Frösche) einer anderen Größe $x$ (Zeit) eindeutig zuordnen. Was aber, wenn diese Zuordnung nicht eindeutig ist. Was ist, wenn wir die Frösche wiegen und ihr Alter bestimmen wollen. In diesem Fall ist es nicht so, dass wir jedem Alter ein exaktes Gewicht zuordnen können: Frösche mit gleichem Alter können unterschiedliches Gewicht haben, und Frösche mit unterschiedlichem Alter könnten durchaus das gleiche Gewicht haben. Was passiert also, wenn wir alle Frösche im Teich wiegen, und sowohl ihr Alter als auch ihr Gewicht grafisch darstellen wollen? Versuchen wir zuerst einen Linienplot.
import matplotlib.pyplot as plt
%matplotlib inline
# Die Daten sind wie folgt aufgebaut:
# froschgewicht = (gewicht von frosch1, gewicht von frosch2, gewicht von frosch3, ...)
# froschalter = (alter von frosch1, alter von frosch2, alter von frosch3, ...)
froschgewicht = (100,105,106,105,107,105,111,112,104,122,80,111,75,101)
froschalter = (3, 5, 5, 4, 6, 8, 10, 5, 3, 7, 2, 4, 3, 5)
plt.plot(froschalter, froschgewicht, color = "green")
plt.xlabel("Alter (Jahre)")
plt.ylabel("Gewicht (g)")
Dieser Plot ist prinzipiell nicht falsch, er ist nur sehr verwirrend. Die Verbindungslinien suggerieren, dass die Frösche, die zufällig nacheinander gewogen wurden, irgendetwas miteinander zu tun hätten. Die Reihenfolge der Frösche hat in diesem Plot aber keine Bedeutung, die Frösche hätten auch in ganz anderer Reihenfolge gewogen werden können. Um diesen Plot zu reparieren, sollten wir also die Verbundungslinien entfernen und einfach nur für jeden Frosch eine Markierung vornehmen. Damit entsteht ein so genannter Scatter-Plot.
import matplotlib.pyplot as plt
%matplotlib inline
# Die Daten sind wie folgt aufgebaut:
# froschgewicht = (gewicht von frosch1, gewicht von frosch2, gewicht von frosch3, ...)
# froschalter = (alter von frosch1, alter von frosch2, alter von frosch3, ...)
froschgewicht = (100,105,106,105,107,105,111,112,104,122,80,111,75,101)
froschalter= (3, 5, 5, 4, 6, 8, 10, 5, 3, 7, 2, 4, 3, 5)
plt.scatter(froschalter, froschgewicht, color = "green", marker = "x")
plt.xlabel("Alter (Jahre)")
plt.ylabel("Gewicht (g)")
Ein Klassiker unter den Visualisierungen, der sehr gut geeignet ist, um prozentuelle Anteile darzustellen, ist das Kuchen- oder auch Tortendiagramm. Als wissenschaftliches Diagramm ist es nur bedingt geeignet, da man konkrete Größen nur schwer ablesen kann. Für Präsentationen, oder um sich einen groben Überblick über Daten zu verschaffen, eignet es sich aber recht gut:
import matplotlib.pyplot as plt
%matplotlib inline
labels = ('Helle Froesche', 'Dunkel Froesche', 'Gelbe Froesche', 'Pfeilgiftfroesche')
farben = ("lightgreen","darkgreen","yellow", "orangered")
anzahl = [42, 32, 20, 6]
explode = (0, 0, 0, 0.2) # Hervorgehoben werden nur Pfeilgiftfrösche
plt.pie(anzahl, explode = explode, labels = labels, autopct = '%1.1f%%',
shadow = True, colors = farben)
plt.axis('equal') # macht den Plot quadratisch
Oft möchte man mehrere Grafiken nebeneinander darstellen. Dafür eignen sich so genannte subplots
. Die Syntax dafür ist ein wenig gewöhnungsbedürftig. Subplots werden immer mit drei Zahlen definiert, wobei zum Beispiel die Zahlenfolge (2, 3, 4) - oder alternativ auch einfach (234) - bedeutet, dass sämtliche Plots, die dargestellt werden sollen, in 2 Zeilen und 3 Spalten angordnet werden und aktuell (mit der letzten Zahl 4) der 4te von 6 Plots (2 mal 3) angesprochen wird.
Beim Definieren von Subplots wird oftmals mit fig = plt.figure(figsize=(10, 3))
zunächst eine allgemeine Zeichenfläche festgelegt. figsize
definiert dabei die Größe der gesamten Zeichenfläche und der Befehl subplots_adjust(hspace = 0.5)
erlaubt es, die Abstände zwischen den Subplots zu definieren, hier zum Beispiel die Höhenabstände (hspace
). Mit Befehlen wie ax1 = fig.add_subplot(2, 3, 1)
wird sodann der jeweilige Subplot in diese allgemeine Zeichenfläche eingefügt - hier also der Subplot ax1
an die erste Stelle der Plots.
Merke: wenn statt der matplotlib.pyplot
-Abkürzung plt
ein eigener Plotname definiert wird, wie hier etwa ax1
, so müssen sämtliche Plot-Beschriftungen mit set_
vorgenommen werden, also zum Beispiel ax1.set_title('Plot 1')
.
import matplotlib.pyplot as plt
%matplotlib inline
testdaten=[2,3,4,4,2]
# definiere eine allgemeine Zeichenfläche
fig = plt.figure(figsize=(10, 4))
# lege Höhenabstände zwischen den Subplots fest
plt.subplots_adjust(hspace = 0.5)
# füge ersten Subplot zur allgemeinen Zeichenfläche hinzu
ax1 = fig.add_subplot(2, 3, 1)
ax1.plot(testdaten, color = "lime")
ax1.set_title('Plot 1')
# zweiter Subplot
ax2 = fig.add_subplot(2, 3, 2)
ax2.fill(testdaten, color = "green")
ax2.set_title('Plot 2')
# dritter Subplot
ax3 = fig.add_subplot(2, 3, 3)
ax3.plot(testdaten, color = "orange", lw=4)
ax3.set_title('Plot 3')
# vierter Subplot
ax4 = fig.add_subplot(2, 3, 4)
ax4.fill(testdaten, color = "salmon")
ax4.set_title('Plot 4')
# fünfter Subplot
ax5 = fig.add_subplot(2, 3, 5)
ax5.plot(testdaten, color = "black", marker="+", ms=20)
ax5.set_title('Plot 5')
# sechster Subplot
ax6 = fig.add_subplot(2, 3, 6)
ax6.fill(testdaten, color = "gold")
ax6.set_title('Plot 6')
Das Polardiagramm sieht zwar ähnlich aus, wie ein Kuchendiagramm, hat aber eine grundlegend andere Bedeutung. Wichtig ist hier der Winkel: jedem Winkel (0 - 360 Grad) wird eine bestimmte Größe zugeordnet. In der Physik wird dieser Plot zum Beispiel verwendet, um darzustellen, in welche Richtung ein Objekt wie viel Strahlung emittiert. Als einfacheres Beispiel könnte man damit auch das Sichtfeld verschiedener Tiere vergleichen:
import matplotlib.pyplot as plt
%matplotlib inline
# definiere eine allgemeine Zeichenfläche
fig = plt.figure(figsize=(15,5))
# füge einen Sub-Plot in die allgemeine Zeichenfläche ein, als ersten in einer ein-reihigen Zeile mit drei Plätzen = (1, 3, 1)
ax1 = fig.add_subplot(1, 3, 1, projection='polar')
# der Plot wird als Polarplot initialisiert
ax1.bar(0, 1, 3.1415/2, bottom = 0.0, color = "lightgreen")
# Für einen Polarplot lautet die Syntax:
# plt.bar(Um wieviel Grad ist die Fläche zentriert, Radius der Fläsche, welchen Winkel hat die Fläche)
# Beachte: Die Winkel werden hier nicht in Grad, sondern als Bogenmaß angegeben,
# also 360° = 2 * pi ~ 2 * 3.1415
ax1.set_title("Frosch") # Beachte: mit subplot muss die Beschriftung mit set_ erfolgen, also zB set_title
# füge einen weiteren Sub-Plot hinzu, als zweiten in einer ein-reihigen Zeile mit drei Plätzen = (1, 3, 2)
ax2 = fig.add_subplot(1, 3, 2, projection='polar')
ax2.bar(0, 1, 3.1415, bottom = 0.0, color = "darkgreen")
ax2.set_title("Kroete")
# füge noch einen weiteren Sub-Plot hinzu, als dritten in einer ein-reihigen Zeile mit drei Plätzen = (1, 3, 3)
ax3 = fig.add_subplot(1, 3, 3, projection='polar')
ax3.bar(0, 1, 2 * 3.1415, bottom = 0.0, color = "lime")
ax3.set_title("Chamaeleon")
Histogramme eigenen sich, um darzustellen, wie oft ein gewisser Wert in einer Liste vorkommt. Nehmen wir an, wir wissen, dass es eine Bevölkerungsexplosion in unserem Froschteich gab, wir wissen aber nicht genau, in welchem Jahr sie stattgefunden hat. Um diese Frage zu beantworten, fangen wir alle Frösche im Teich und bestimmen ihr Alter. Um unsere Forschungsfrage zu beantworten, stellen wir die resultierende Liste sodann als Histogramm dar:
import matplotlib.pyplot as plt
%matplotlib inline
froschalter=(15,16,15,17,14,13,11,14,14,12,13,1,1,2,3,4,4,5,6,2,1,1,1,3,4,5,14,14,10,10,14,14,11,11,14,
14,13,12,14,13,12,11,11,10,7,6,8,9,13,13,13,13,5,4,3,2,4,5,6,7,8,12,12,9,4,12,2,3,4,5,6,7,8,9,10,10,
6,7,8,4,5,9,8,2,3,4,5,6,7,8,13,13,12,12,13,13,13,19,18)
# Syntax: plt.hist(Liste, Anzahl der Balken, kleinster und größter Wert, Farbe)
plt.hist(froschalter, 18, range = (0.5, 18.5), color = "green")
plt.xlim(1,19)
plt.ylim(0,15)
# zeichne eine rote Vertikale, um die Bevölkerungsexplosion vor ca 15 Jahren zu markieren
plt.plot((14.5, 14.5),(0, 16), color = "red", lw = 3)
plt.ylabel("Anzahl")
plt.xlabel("Alter")
Wir erkennen aus dem Histogramm, dass viele Frösche im Teich aktuell 14 Jahre alt sind, aber nur sehr sehr wenige 15 Jahre. Vor 14 Jahren wurden also wesentlich mehr Frösche geboren, als vor 15 Jahren. Die Bevölkerungsexplosion fand also offensichtlich vor ca 15 Jahren statt.
Box- und Violinplots eignen sich, ähnlich wie Histogramme, dazu Verteilungen darzustellen. Boxplots erlauben es überdies, mehrere Verteilungen miteinander zu vergleichen. Sie zeigen den Mittelwert einer Verteilung (rote Linie), den Bereich, in dem der Großteil der Werte liegt (schwarze Box), aber auch die Extremwerte und Ausreißer.
Sehr ähnlich sind Violinplots, mit dem Unterscheid, dass sie detailliertere Informationen zur Verteilung liefern. Ähnlich dem Histogramm zeigen sie (stark geglättet), in welchen Bereichen viele, und in welchen wenige Werte vorliegen.
Im folgenden vergleichen wir das Gewicht der Frösche in 3 verschiedenen Teichen, einmal mit Box- und einmal mit Violinplots:
import matplotlib.pyplot as plt
%matplotlib inline
teich1=(110,105,106,104,110,115,113,112,113,103,104,110,109,108,105,102,110,110,110,111,109,111,120)
teich2=(90,99,100,102,103,103,104,102,110,90,95,98,97,93,98,110,105,106,102,89)
teich3=(120,122,123,122,120,122,123,122,119,80,85,80,81,82,83,82,84,85)
fig = plt.figure(figsize=(12,4))
# erster Plot
ax1 = fig.add_subplot(1, 2, 1)
ax1.boxplot((teich1, teich2, teich3))
ax1.set_title("Boxplot")
ax1.set_ylabel("Gewicht (g)")
# zweiter Plot
ax2 = fig.add_subplot(1, 2, 2)
ax2.violinplot((teich1, teich2, teich3))
ax2.set_title("Violinplot")
ax2.set_ylabel("Gewicht (g)")
Beim plotten drei-dimensionaler Daten möchte man in der Regel jedem $x/y$-Daten-Paar einen weiteren $z$-Wert zuweisen, so wie etwa den Geo-Koordinaten auf topologischen Karten eine Seehöhe zugewiesen wird. Um diese Plots eindrucksvoll darzustellen, importieren wir im Folgenden vorgefertige Beispieldaten aus dem matplotlib.cbook
mit dem Befehl get_sample_data
.
import matplotlib.pyplot as plt
%matplotlib inline
# importiere Beispielsdaten
from matplotlib.cbook import get_sample_data
import numpy as np
data = np.load(get_sample_data('jacksboro_fault_dem.npz')) # liest einen Beispieldatensatz ein
z = data['elevation'] # speichert die Höheninformation des Datensatzes unter z
fig = plt.figure(figsize=(12, 8))
ax1 = fig.add_subplot(2, 2, 1)
ax1.imshow(z, cmap = plt.cm.terrain) # colormap terrain: z.B. für Karten
ax2 = fig.add_subplot(2, 2, 2)
ax2.imshow(z, cmap = plt.cm.viridis) # colormap viridis: für abstrakte Daten
ax3 = fig.add_subplot(2, 2, 3)
ax3.imshow(z, cmap = plt.cm.gist_earth) # colormap earth: für realistische Karten
ax4 = fig.add_subplot(2, 2, 4)
ax4.imshow(z, cmap = plt.cm.ocean) # colormap ocean: Blautöne Für Meeresdarstellungen
Neben den hier vorgestellten Möglichkeiten gibt es noch viele mehr, um Daten zu visualisieren. Einen guten Überblick liefert die Plot-Gallerie von matplotlib
unter https://matplotlib.org/gallery.html, die zu jedem Plot auch den Code bereitstellt, mit dem der Plot erzeugt wird.
Darüber hinaus gibt es auch andere spezialisierte Python-Pakete, die sich der Datenvisualisierung widmen. Beispiele sind seaborn
, zum Darstellen von statistischen Daten, oder bokeh
, das interaktive Visualisierungen im Browser erlaubt.
Linienplots sind einfache Plots, die immer dann verwendet werden, wenn eine Größe eindeutig einer anderen Größe zugeordnet werden soll.
Mit Scatterplots werden Datenpaare (z.B. Alter und Gewicht) dargestellt, die ohne eindeutige Zuordnung zusammengehören. Beispiel: ein drei Jahre alter Frosch muss nicht immer exakt 111 Gramm haben.
Mit Histogrammen, sowie Box- oder Violinplots werden Verteilungen dargestellt. Während ein Histogramm eine exakte Verteilung darstellt, lassen sich mit Box- und Violinplots mehrere Verteilungen miteinander vergleichen. Boxplots zeigen wichtige Charakteristika deutliche (Mittelwert, Standardabweichung, Ausreißer), Violinplots zeigen mehr Details der Verteilung.
Subplots erlauben es, mehrere Grafiken nebeneinander darzustellen. Subplots werden immer mit drei Zahlen bezeichnet: subplot(a, b, c)
, wobei die Grafiken in einer Matrix mit $a$ Zeilen und $b$ Spalten organisiert sind und der jeweils gerade angesprochene Plot den Index $c$ hat.
Python lässt sich auch sehr gut dazu nutzen große Datenmengen zu analysieren und zu bearbeiten. Ganz egal wie die Daten vorliegen (csv, excel,...), viele Datentypen lassen sich importieren und weiterverarbeiten. Das Pythonpaket, das man üblichweise dazu verwendet nennt sich pandas
.
Wir werden in Folge pandas ein wenig kennenlernen, indem wir den World Happiness Report untersuchen. Diesen Datensatz, und noch viele viele weitere, findet man zum Beispiel frei verfügbar auf Kaggle. Dort gibt es auch Datensätze über alle Apps im Google Play Store, Filme, Börsendaten Development Indikatoren oder die Oberflächentemperatur der Erde.
Zuerst werden wir den Datensatz importieren. Das funktioniert einerseits mit Daten auf der eigenen Festplatte, andererseits aber auch mit Datein im Internet:
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
url="https://s3.amazonaws.com/happiness-report/2018/WHR2018Chapter2OnlineData.xls"
df=pd.read_excel(url)
Nun sind die Daten des World-Happiness-Report 2018 unter df
gespeichert. Diese Abkürzung steht für DataFrame, das ist der Datentyp den Pandas benutzt. Man kann ihn sich wie eine Excel-Tabelle vorstellen: Die Daten sind nach Zeilen und Spalten sortiert und können als Einträge entweder Zahlen oder Zeichenketten besitzen.
Da der Datensatz relativ groß ist, macht es wenig Sinn alle Daten auf einmal zu printen. Um aber trotzdem einen Einblick in den Datensatz zu bekommen, und zu bestätigen, dass das Einlesen funktioniert hat, kann man den Befehl head
benutzen, um sich die ersten paar Einträge des Datensatzes anzeigen zu lassen.
df.head()
So sehen wir schon, welche Daten hier auftreten. Jede Zeile beinhaltet den Namen des Landes und das Jahr, gefolgt von vielen möglichen Indikatoren von Glücklichkeit, wie zum Beispiel die Lebenserwartung oder das Vertrauen in die Regierung.
Einen genaueren Überblick erhalten wir mit dem Befehl describe
:
df.describe()
Dieser Befehl rechnet uns für jede Spalte relevante Indikatoren aus, wie zum Beispiel die Zahl der Einträge (count), den Mittelwert (mean), die Standardabweichung (std), aber auch detailiertere Informatioen über die Verteilung. Spalten wo diese Berechnugen nicht möglich sind (z.B. Name das Landes) werden ausgelassen. Somit sehen wir zum Beispiel, dass die mittlere gesunde Lebenserwartung bei etwa 62 Jahren liegt, es aber auch zumindest einen Eintrag in der Datenbank gibt, wo dieser Wert unter 40 Jahren liegt. Es wäre natürlich besonders interessant herauszufinden, welche Einträge das sind.
Solche Abfragen (also alle Zeilen, wo die Spalte X einen Wert von Y oder kleiner hat) sind mit Pandas sehr einfach möglich.
Wir schreiben:
df[df[NameDerSpalte] < Mindestwert]
df[df["Healthy life expectancy at birth"] < 40]
Ähnlich können wir den Datensatz auch auf ein bestimmtes Jahr einschränken, zum beispiel den aktuellsten Wert des Datensatzes, das Jahr 2018:
df[df["year"] == 2017].head()
Oft ist es auch sinnvoll Visualisierungen zu verwenden, um die Daten darzustellen. Wir könnten uns beispielweise den GINI Index als Histogramm anzeigen lassen:
df[df["year"] == 2017]['GINI index (World Bank estimate), average 2000-15'].hist()
Um Zusammenhänge und Korrelationen festzustellen kann man Scatterplots verwenden. Damit ist es möglich einen Wert auf der y-Achse und einen auf der x-Achse aufzutragen. Wenn eine Punktwolke ensteht gibt es keinen Zusammenhang zwischen diesen Größen. Je eher die Verteilung einer Geraden gleicht, um so stärker ist der Zusammenhang.
Betrachten wir den Zusammenhang zwischen BIP/Person und Lebenserwartung einerseits und den Zusammenhang zwischen "Social Support" und Lebenserwartung andererseits.
df[df["year"] == 2017].plot.scatter(x="Log GDP per capita", y = "Healthy life expectancy at birth")
df[df["year"] == 2017].plot.scatter(x="Social support", y = "Healthy life expectancy at birth")
Natürlich braucht man nicht für jede Analysie den kompletten Datensatz. Für genauere Betrachtungen macht es Sinn die Daten einzuschränken. Wir könnten also nur Deutschland, Österreich und die Schweiz betrachten.
Zuerst sehen wir uns den Mittelwert der Korruption für diese Länder über alle Jahre hinweg an. Dazu benutzen wir den Befehl mean()
und schreiben das Ergebnis direkt mit print
auf den Bildschirm.
print("Corruption Austria: ",df[df["country"]=="Austria"]["Perceptions of corruption"].mean())
print("Corruption Germany: ",df[df["country"]=="Germany"]["Perceptions of corruption"].mean())
print("Corruption Switzerland:",df[df["country"]=="Switzerland"]["Perceptions of corruption"].mean())
Da wir uns auf drei Länder eingeschränkt haben, können wir nun auch detaillierte Analysen durchführen, ohne dass das Ergebnis unübersichtlich wird. Anstelle des Mittelwertes, können wir also den zeitlichen Verlauf des Korruptionsindex betrachten.
Dazu benutzen wir den plot
Befehl von pandas, der ein wenig anders aufgebaut ist, als der von matplotlib. Als x und y übergibt man Beispielweise keine Liste, sondern nur den Namen der Spalte, unter der man die relevanten Datenpunkte findet. Außerdem erstellt dieser Befehl immer eine neue Achse. Um dieses Verhalten zu unterdrücken, damit wir alle Daten in einer Figur sehen, müssen wir der Achse einen Namen geben, sobald sie erstellt wird (ax1) und allen weiteren Plotbefehlen mit ax=ax1
sagen, dass sie die existierende Achse weiterverwenden sollen.
ax1=df[df["country"]=="Austria"].plot(x="year",y="Perceptions of corruption",label="Austria")
df[df["country"]=="Germany"].plot(x="year",y="Perceptions of corruption",label="Germany",ax=ax1)
df[df["country"]=="Switzerland"].plot(x="year",y="Perceptions of corruption",label="Switzerland",ax=ax1)
plt.title("Perceptions of corruption")
Pandas bietet natürlich noch viele weitere Möglichkeiten um Daten zu analysierenm zu verarbeiten und aufzubereiten. Eine vollständige Übersicht findet man im Pandas User Guide.
Pandas kann eine Vielzahl von Datentypen einlesen und in Dataframes konvertieren. Die wichtigsten Befehle dazu sind pd.read_excel
und pd.read_csv
.
Man kann einen Dataframe auf Zeilen mit gewissen Eigenschaften einschränken, indem man
df[df[spaltenname]==wert]
benutzt. Analog funktionert das auch mit <= und >=.
Um nicht die ganze Zeile auszuwälen, sondern nur den Eintrag in einer gewissen Spalte schreibt man
df[df[spaltenname]==wert][spaltenname2]
Um Daten mit Pandas zu visualisieren gibt es viele Möglichkeiten:
df.hist()
)df.plot.scatter(x = spalte1,y = spalte2)
)df.plot(x = spalte1,y = spalte2)
)sympy
¶Bisher haben wir mit Python immer numerische Berechnungen durchgeführt. Das heißt, alle Differentiale wurden vom Computer als kleine Differenzen genähert und irrationale Zahlen, wie zum Beispiel Wurzeln wurden schlichtweg gerundet. Das ist zwar die einfachste Art, wie ein Computer Mathematik betreiben kann, nicht aber die einzige. Mit Python ist es auch möglich, mathematische Probleme analytisch zu lösen, also so, wie sie ein Mensch lösen würde. So können wir Stammfunktionen berechnen, Gleichungen auflösen und komplexe mathematische Probleme lösen, für die ein Mensch Tage brauchen würde.
Fangen wir an, indem wir den Unterschied zwischen numerischen und analytischen Berechnungen genauer betrachten. Als Beispiel soll ein einfacher Prozess, das Ziehen einer Quadratwurzel, dienen. Berechnen wir die Wurzel aus 9 mit dem Standard-Mathematikpaket math
.
import math
math.sqrt(9)
Das funktioniert problemlos, ganz genau gleich wie bei jedem Taschenrechner. Wie wir alle wissen, ist die Wurzel aus 9 exakt 3, denn 3 mal 3 ist 9. Hier muss nicht gerundet werden und wir können mit unserem Ergebnis zufrieden sein. Was aber, wenn wir an der Wurzel aus 8 interessiert sind, die bekannterweise keine ganzzahlige Lösung besitzt?
import math
math.sqrt(8)
Hier bekommen wir zwar eine näherungsweise Lösung, korrekt ist dieses Ergebnis aber nicht mehr. Die Wurzel aus 8 ist nicht exakt 2.8284271247461903, es würden noch unendlich viele Nachkommastellen folgen. Wieso kann das ein Problem werden? Versuchen wir einmal die Wurzel aus 8 mit der Wurzel aus 8 zu multiplizieren. Wir (als Menschen) müssen die Wurzel aus 8 dazu nicht kennen, wir wissen ja, dass die Wurzel aus 8 mal der Wurzel aus 8 wieder 8 sein muss.
$$\sqrt{8}*\sqrt{8}=\sqrt{8*8}=\sqrt{8^2}=8 $$
Wurzel und Quadrat heben sich sozusagen auf. Wenn wir diesen Zusammenhang schon verstehen, ohne überhaupt im Kopf die Wurzel ziehen zu können, dann sollte ein Computer mit dieser Aufgabe doch auch kein Problem haben, oder?
import math
math.sqrt(8) * math.sqrt(8)
Scheinbar doch. Python behauptet felsenfest, dass die Wurzel aus 8 zum Quadrat ein bisschen größer ist als 8. Der Fehler ist klein, aber man muss ihn ernst nehmen. In einem umfangreichen Programm werden solche Berechnungen nämlich sehr oft hintereinander ausgeführt, und der Fehler schaukelt sich auf. Und solche Fehler entstehen natürlich nicht nur beim Wurzel ziehen, sondern bei jeder Operation, die in irgendeiner Form runden muss. Schon die Zahl 1/3 kann nicht mehr exakt gespeichert werden, da man für die Darstellung als 0.33333333333..... unendlich viele Stellen bräuchte um exakt zu sein.
Wie kann man dieses Problem lösen? Mit analytischen Berechnungen! Wenn Python verstehen würde, was 1/3 bedeutet, also nicht nur eine Zahl mit endlich vielen Stellen, sondern die Rechenoperation, die die Zahl 1 in 3 gleich große Teile teilt, wären wir nicht mehr auf Runden angewiesen. Noch schöner wäre es, wenn Python auch die Operation des Wurzelziehens verstehen würde, sodass das Quadrat einer Wurzel automatisch immer wieder die Zahl selbst ergeben würde. All das, und noch viel mehr ist möglich, mit dem Python-Paket sympy
.
Wir sehen uns dieses Paket im Folgenden genauer an und benutzen es als erstes, um damit eine Wurzel zu ziehen:
import sympy
sympy.sqrt(9)
Unser erstes erfreuliches Ergebnis: Die Wurzel aus 9 ist nach wie vor 3. Was passiert aber mit der Wurzel aus 8?
import sympy
sympy.sqrt(8)
Dieses Ergebnis sieht zwar sonderbar aus, ist aber absolut korrekt. Die Wurzel aus 8 ist exakt doppelt so groß wie die Wurzel aus 2.
Für Mathematiker: $\sqrt{8}=\sqrt{4*2}=\sqrt{4}*\sqrt{2}=2*\sqrt{2}$
Der Vorteil, den wir hier haben ist offensichtlich: Dieses Ergebnis ist exakt, wir müssen nicht runden. Und wenn wir wollen, können wir das Ergebnis immer noch in eine Dezimalzahl umrechnen:
import sympy
float(sympy.sqrt(8))
Nun könnten wir noch überprüfen, ob das Quadrieren einer Wurzel so besser funktioniert:
import sympy
sympy.sqrt(8) * sympy.sqrt(8)
Offensichtlich. Der Vorteil dieser Berechnung ist noch gering, denn die Wurzel aus 8 kann man auch im Kopf umformen (siehe die Berechnung weiter oben). Hier kommt dann aber der Vorteil eines Computers zum Tragen: Wenn das Prinzip funktioniert, sind kompliziertere Berechnungen nicht wirklich schwieriger als einfache Berechnungen, wie folgendes Beispiel zeigt:
import sympy
sympy.sqrt(1264)
Die Wurzel aus 1264 ist also 4 mal die Wurzel aus 79. Auch mit viel Übung kann man solche Berechnungen nicht ohne weiteres im Kopf durchführen. Das Paketsympy
kann das - und noch viel mehr. Besonders spannend wird es, wenn wir selbst Variablen definieren. Dafür gibt es den Befehl symbols
, der Python erklärt, dass es sich bei diesen Variablen um allgemeine Variablen handelt, in denen kein Wert gespeichert ist. Trotzdem kann Python damit rechnen. Wir können einen Ausdruck speichern, der x und y beinhaltet und Python kann damit arbeiten, obwohl wir x und y keine Werte zugewiesen haben, sondern sie nur als allgemeine Symbole verwenden:
from sympy import *
x = symbols("x")
y = symbols("y")
ausdruck = 3 * x - 4 * y
ausdruck
ausdruck - 1
ausdruck + 3 * y
ausdruck * x # die Lösung wird hier noch nicht ausmultipliziert
expand(ausdruck * x) # expand führt die Mutliplikation aus
Man sieht sofort: Python versteht nun wirklich die Rechenoperationen, und obwohl der Variablen x kein Wert zugeordnet wurde, ist klar, dass x mal x das gleiche ist wie x-Quadrat. Das können wir zum Beispiel nutzen um sehr komplizierte Ausdrücke mit dem Befehl factor
zu vereinfachen:
from sympy import *
x = symbols("x")
y = symbols("y")
z = symbols("z")
ausdruck = -x * y * z**2 + 2 * x * y * z - x * z**2 + 2 * x * z + y * z**2 - 2 * y * z + z**2 - 2 * z
factor(ausdruck)
Der ausgeprochen komplizierte Ausdruck
-x * y * z**2 + 2 * x * y * z - x * z**2 + 2 * x * z + y * z**2 - 2 * y * z + z**2 - 2 * z
lässt sich also kompakt schreiben als
-z*(x - 1)*(y + 1)*(z - 2)
Das ist zwar hilfreich, in der Praxis wird man aber sehr selten in die Situation kommen, solche Vereinfachungen durchführen zu müssen (außer vielleicht im Rahmen von Mathematiklehrveranstaltungen). Viel öfter möchte man zum Beispiel Gleichungen lösen.
Während zur Lösung quadratischer Gleichungen noch Formeln bereitstehen, sind Gleichungen 3-ter Ordnung, wie beispielsweise
$$3x^3+x^2-x=0 $$
auf dem Papier ungleich schwerer zu lösen. Schneller geht es mit dem sympy
-Befehl solve
. Dieser Befehl benötigt nur die Gleichung und als zweites Argument die Variable, die ausgerechnet werden soll:
from sympy import *
x = symbols("x")
solve(3 * x**3 + x**2 - x , x)
Die drei Lösungen der Gleichung $3x^3+x^2-x=0 $ sind also $x=0$, $x=-\frac{1}{6} + \frac{\sqrt{13}}{6}$ und $x=-\frac{1}{6} - \frac{\sqrt{13}}{6}$
Seinen größten Vorteil aus der Perspektive der Systemwissenschaften spielt sympy
beim Berechnen von Differentialen und Integralen aus. Das numerische Differenzieren und Integrieren haben wir bereits in früheren Kapiteln besprochen. Die Lösungen dort waren allerdings immer Zahlen oder Listen von Zahlen. Allgemeine, analytische Erkenntnisse, wie die, dass das Integral von $\cos{(x)}$ exakt $\sin{(x)}$ ist, konnten wir so nicht gewinnen. Mit Hilfe von sympy's diff
und integrate
Befehlen können wir auch sehr komplizierte Ableitungen und Integrale analytisch durchführen und bekommen als Lösungen keine Zahlen, sondern vollständige, allgemein gültige Funktionen:
from sympy import *
x = symbols("x")
diff(sin(x))
integrate(cos(x))
diff(sin(3 * x) * x**2)
integrate(sin(x) * cos(x))
diff(x**3 + log(x) * sin(x) + exp(x**2))
Aber Achtung: Integrale, die analytisch nicht lösbar sind, kann auch Python nicht lösen. Differenzieren läuft immer nach sehr simplen Regeln ab, und prinzipiell kann man mit diesen Regeln jede Funktion ableiten. Integrieren ist jedoch mehr ein kreativer Prozess, und hierbei können Zusammenhänge auftauchen, die sich einfach nicht berechnen lassen. Das hat nichts damit zu tun, dass die richtigen "Rechenregeln" nur einfach noch nicht gefunden wurden. Es ist vielmehr eine Eigenschaft der Integralrechnung selbst. Unlösbaren Integrale müssen nicht schrecklich kompliziert sein. Schon das einfache Beispiel $\frac{\sin(x)}{\log(x)}$ ist nicht mehr integrierbar:
integrate(sin(x) / log(x))
Wenn die obige Zelle ausgeführt wird, zeigt sich, dass Python eine Zeitlang rechnet und offensichtlich versucht das Integral zu lösen. Wenn allerdings keine partielle Integration, keine Substitutionen oder andere Tricks mehr weiterhelfen, kapituliert Python (und auch jegliche andere Mathematik-Software und das menschliche Hirn). Als Lösung bekommt man dann die Aussage:
$$\int \frac{\sin(x)}{\log(x)} dx = \int \frac{\sin(x)}{\log(x)} dx $$
Das ist natürlich (per Definition) nicht falsch, bringt uns aber auch nicht wirklich weiter. Wenn wir dieses Integral in einem bestimmten Bereich lösen wollen, sind wir wieder auf numerische Methoden angewiesen. Diese liefern zwar keine schönen Funktionen, sondern immer nur gerundete Zahlen. Dafür funktionieren sie aber wirklich immer. Man sieht also, dass sowohl numerische, als auch analytische Methoden ihre Daseinsberechtigung haben, und man sich je nach Problem für eine der beiden Herangehensweisen entscheiden sollte. Gerade im mathematischen Bereich hat das Arbeiten mit analytischen Funktionen große Vorteile.
Die Anwedungsgebiete von sympy
enden aber nicht mit Differenzieren und Integrieren. Man kann damit auch Grenzwerte berechnen, Differentialgleichungen lösen, Eigenwerte und Eigenvektoren ausrechnen oder (wenn man das wirklich möchte ;-) eine Bessel-Funktion in eine sphärische Bessel-Funktion transformieren.
Sympy
erlaubt es zum Beispiel auch mit Matrizen zu rechnen. Alle wichtigen Rechenoperationen werden unterstützt. Matrizen werden in eckigen Klammer geschrieben, wobei jede Zeile ihrerseits in eckige Klammern gesetzt wird. Sympy
betrachtet Matritzen also eigentlich als Listen von Listen. Die Matrix
$$M=\left(\begin{array}{ccc}1&3&2\\1&1&1\\2&3&1\end{array}\right) $$
kann demnach geschrieben werden als:
M = Matrix([[1,3,2],[1,1,1],[2,3,1]])
M
Grundrechenarten funktionieren ganz intuitiv mit den dazugehörigen Operatoren +, * und -:
M + M
3 * M
M - M
Auch die (per Hand oft langwierige) Matrix-Multiplikation lässt sich mit sympy
einfach handhaben.
M = Matrix([[1,3,2],[1,1,1],[2,3,1]])
N = Matrix([[4,1,2],[1,2,1],[1,2,4]])
M * N
N * M
Als schnelle Überprüfung, ob es sich wirklich um eine echte Matrixmultiplikation handelt, und nicht einfach nur die Elemente miteinander multipliziert werden, kann man M * N
ausrechnen lassen, und das Ergebnis mit N * M
vergleichen. In unserem Beispiel sehen wir, dass M * N
nicht das gleiche ist wie N * M
, ganz so wie es sich für Matrixmultiplikationen gehört.
Aber auch erweiterte Rechenoperationen sind möglich. Beispielsweise das Berechnen von Determinanten, welches unter anderem zum Lösen von Gleichungssystemen mit der Cramerschen Regel (https://de.wikipedia.org/wiki/Cramersche_Regel) benötigt wird:
M.det()
Auch das Berechnen von Eigenwerten ist möglich:
M.eigenvals()
Das Paket sympy
ermöglicht es mit Python analytische Berechnungen durchzuführen. Das hat den Vorteil, dass Zahlen nicht gerundet werden, sondern Brüche oder Wurzeln exakt verwendet werden können. So werden nicht nur Rundungsfehler verhindert, sondern viele Rechenoperationen möglich, die numerisch nicht, oder nur näherungsweise gelingen.
Das Lösen von Gleichungen erfordert numerisch in der Regel Tricks und Kreativität. Mit sympy
lassen sich Gleichungen aber exakt lösen. Symbole, die in diesen Gleichungen vorkommen, werden zunächst mit dem Befehl symbols
definiert und sodann mit dem Befehl solve(gleichung, variable)
einer Lösung nach der gesuchten Variablen zugeführt.
Die Ableitung einer Funktion wird mit diff
berechnet, wobei auch hier die vorkommenden Variablen zuvor mit symbols
zu deklarieren sind.
Das Integrieren, also das Suchen der Stammfunktion, geschieht mit dem Befehl integrate
.
Merke: Nicht alle Funktionen können analytisch integriert werden. Für viele existiert keine analytische Lösung. Für solche Probleme muss auf numerische Methoden zurückgegriffen werden. Diese benötigen zwar längere Rechenzeit und liefern Lösungen nur näherungsweise und in bestimmten Intervallen. Dafür funktionieren sie in der Regel immer.
Eine besonders fruchtbare Form, die Komponenten eines Systems in ihren Wechselwirkungen zu untersuchen, bietet die Netzwerkforschung. Ähnlich wie die Systemwissenschaft findet die Netzwerkforschung ihren Gegenstand in unterschiedlichsten Bereichen. Computer, die miteinander kommunizieren, bilden Computernetzwerke. Menschen, die miteinander in Beziehung stehen, bilden soziale Netzwerke. Tiere und Pflanzen, die sich einen Lebensraum teilen, bilden ökologische Netzwerke. All diese Netzwerke, obwohl sie eigentlich grundverschieden scheinen, haben gemeinsame Eigenschaften und folgen ähnlichen Regeln.
In den letzten Jahren gewinnt der Netzwerkbegriff in den Wissenschaften enorm an Bedeutung. Es ist deshalb kaum verwunderlich, dass auch Python mittlerweile spezialisierte Pakete bereithält, die sich der Darstellung und Analyse von Netzwerken widmen. Wir werden uns im Folgenden eines dieser Paket etwas genauer ansehen.
Von Netzwerken spricht man immer dann, wenn Objekte als Knoten (oder auf Englisch auch **nodes** oder **vertices**) vorliegen, die mit anderen solchen Objekten in irgendeiner Weise verbunden sind. Die Verbindungen werden im Englischen **links** oder auch **edges** genannt.
Charakterisiert werden Netzwerke durch ihre Verbindungsstruktur, also durch die Information darüber, welche Knoten mit welchen anderen Knoten auf welche Weise verbunden sind. Aus dieser Verbindungsstruktur ergibt sich eine Vielzahl unterschiedlicher Netzwerktypen, von denen wir einige weiter unten detaillierter besprechen.
Zunächst sehen wir uns nun aber einige Grundlagen des Python-Pakets NetworkX
an, das auf die Darstellung und Analyse von Netzwerken spezialisiert ist und bereits im installierten Anaconda-Paket enthalten ist.
Für den Anfang betrachten wir ein sehr einfaches Netzwerk: Das Netzwerk soll aus drei Personen bestehen: Alice, Bob und Carol. Diese drei Personen sind die Knoten (nodes) unseres Netzwerks. Alle sind miteinander befreundet, was wir in unserem "Freundschaftsnetzwerk" durch Verbindungen (edges) darstellen.
Wir beginnen damit, das Paket NetworkX
zu importieren und zunächst ein leeres Netzwerk anzulegen. Dieses füllen wir sodann mit nodes und edges:
import networkx as nx
G = nx.Graph() # erstelle ein neues Netzwerk mit dem Namen G
Im nächsten Schritt fügen wir drei Knoten hinzu: Alice, Bob und Carol.
G.add_node("Alice") # füge dem Netzwerk G den Knoten "Alice" hinzu
G.add_node("Bob")
G.add_node("Carol")
Um das Netzwerk grafisch darzustellen, benötigen wir abermals das Paket matplotlib
, das sehr gut mit networkx
harmoniert.
import matplotlib.pyplot as plt
%matplotlib inline
nx.draw_networkx(G, node_size = 1600) # zeichne das Netzwerk G. Die Knoten sollen die Größe 1600 haben.
Wir sehen: die Knoten werden korrekt dargestellt. Was noch fehlt, sind die Verbindungslinien zwischen den Knoten. Diese werden mit dem Befehl G.add_edge(von, zu)
hinzugefügt:
G.add_edge("Alice","Bob") # füge dem Netzwerk G eine neue Verbindung zwischen Alice und Bob hinzu.
G.add_edge("Bob","Carol")
G.add_edge("Carol","Alice")
nx.draw_networkx(G, node_size = 1600)
Somit ist unser (sehr einfaches) Netzwerk korrekt dargestellt. Wenn wir die obige Code-Zelle mehrmals ausführen, fällt auf, dass die Positionen der einzelnen Knoten jedesmal neu vergeben werden. Sie werden zufällig generiert. In dieser Darstellung haben sie keine Bedeutung, wichtig ist nur, wer mit wem verbunden ist, nicht an welchem Ort sie sich befinden.
Fügen wir unserem Netzwerk eine weitere Person hinzu: Dan. Dan ist nur mit Carol befreundet, kennt sonst aber niemanden aus dem Netzwerk. Wir machen also nur eine Verbindung zwischen Carol und Dan:
G.add_node("Dan")
G.add_edge("Carol","Dan")
nx.draw_networkx(G, node_size = 1600)
Um unser Netzwerk noch etwas zu erweitern, fügen wir noch zwei weitere Personen hinzu: Eve ist eine Freundin von Alice und Carol. Frank ist nur mit Dan befreundet:
G.add_node("Eve")
G.add_edge("Eve","Alice")
G.add_edge("Eve","Carol")
G.add_node("Frank")
G.add_edge("Frank","Dan")
nx.draw_networkx(G, node_size = 1600)
Dieses soziale Netzwerk ist nun schon hinreichend komplex, um daran unterschiedliche Darstellungsarten auszuprobieren. Bisher waren die Positionen der Knoten in der Grafik dem Zufall überlassen. Man kann sie aber auch mit so genannten "Layouts" arrangieren. Die Knoten werden dann nach bestimmten Regeln angeordnet, was spezielle Netzwerkeigenschaften verdeutlichen kann, oder einfach nur die Netzwerke generell übersichtlicher gestaltet.
Eines der übersichtlichsten Layouts (und deswegen auch die Standardeinstellung in NetworkX) ist das Spring-Layout. Es heißt so, weil die Verbindungslinien als Federn (springs) interpretiert werden, was zur Folge hat, dass zwei verbundene Knoten, die sich zu nahe sind, vom Algorithmus, der das Netzwerk generiert, auseinander gezogen werden, und wenn sie zu weit voneinander entfernt sind, zusammengezogen werden. Zumeist liefert dies ein recht übersichtliches Bild.
Layouts werden innerhalb des draw
-Befehls mit dem Argument pos
definiert (Merke: wir haben das Netzwerk G
oben bereits in all seinen Bestandteilen definiert und brauchen es deshalb in Folge nur mehr in seinen unterschiedlichen Layouts aufzurufen):
# nx.spring_layout(G) errechnet das Springlayout für das Netzwerk
nx.draw_networkx(G, node_size = 1600, pos = nx.spring_layout(G))
Etwas weniger übersichtlich ist das Layout "random". Hier werden die Positionen der Knoten absolut zufällig gewählt, was dazu führt, dass die Verbindungslinien kreuz und quer laufen können. Der Vorteil dieses Layouts ist aber, dass es kaum Rechenzeit benötigt und somit gern bei großen Netzwerken benutzt wird, bei denen man den einzelnen Verbindungslinien ohnehin nur schwer folgen kann.
nx.draw_networkx(G, node_size = 1600, pos = nx.random_layout(G))
Beim Circular-Layout werden alle Knoten in einem Kreis angeordnet. Hier liegt der Vorteil darin, schnell erkennen zu können, wer mit wem verbunden ist.
nx.draw_networkx(G, node_size = 1600, pos = nx.circular_layout(G))
Was lässt sich nun mit einem solchen Netzwerk machen? Zunächst könnten wir versuchen, bestimmte Netzwerkeigenschaften festzustellen. Für unser soziales Netzwerk könnten wir zum Beispiel herausfinden, wie die Freundschaften im Netzwerk verteilt sind. Wir wollen also wissen, wie viele Verbindungslinien die einzelnen Knoten haben. Im Englischen spricht man hierbei vom degree eines Knoten. Wir erhalten diese Information mit dem Befehl nx.degree(G)
:
nx.degree(G)
Die Antwort ist so zu lesen: Der Knoten mit dem Namen "Alice" hat degree 3 (also drei Freunde), "Bob" hat degree 2 (zwei Freunde), und so weiter.
Oft ist man aber nur an den Zahlen selbst interessiert, nicht unbedingt daran, wie die Knoten heißen. Dazu lässt sich der Befehl .values()
wie folgt hinzufügen:
list(dict(nx.degree(G)).values())
Als Antwort ergibt sich eine Liste, die sich recht einfach in ein Histogramm verwandeln lässt:
plt.hist(list(dict(nx.degree(G)).values()), 4, range = (0.5, 4.5), color = "darkred")
plt.xlabel('Zahl der Freunde')
plt.ylabel('Zahl der Personen')
Das Histogramm zeigt, dass es drei Personen gibt, die genau zwei Freunde haben und jeweils eine Person, die einen, drei und vier Freunde hat. Richtig interessant wird eine solche Analyse erst mit großen Netzwerken mit einigen hunderten oder tausenden Knoten. Solche Netzwerke werden in der Regel nicht mehr manuell eingegeben. Es wird also nicht mehr jeder Knoten und jede Verbindung einzeln definiert, sondern ein so genannter Netzwerkgenerator verwendet.
Netzwerkgeneratoren eignen sich insbesondere dazu, bestimmte Archetypen von Netzwerken zu erstellen, die in der Natur, aber auch in der Gesellschaft oder in technischen Zusammenhängen immer wieder auftreten. Diese Archetypen zeichnen sich durch ganz bestimmte Netzwerktopologien aus, die in verschiedensten Kontexten in ähnlicher Weise auftreten.
Einer der verbreitetsten Netzwerk-Archetypen ist das so genannte Small-World-Netzwerk, das seinen Namen einem berühmt gewordenen Experiment von Stanley Milgram verdankt (siehe: http://systems-sciences.uni-graz.at/etextbook/networks/networks_2.html). Es zeichnet sich durch relativ geringe durchschnittliche Vernetzungsdichte aus, die allerdings von Regionen hoher Vernetzungsdichte unterbrochen ist, wobei diese Regionen ihrerseits über kurze Pfade mit anderen solchen dichten Regionen verbunden sind. Die meisten Knoten in solchen Netzwerken haben nur recht wenige Verbindungen. Einige wenige haben dagegen viele und ganz wenige Knoten haben sehr viele Verbindungen. Diese sehr wenigen, überaus gut vernetzten Knoten werden als Hubs bezeichnet. Viele soziale Netzwerke, aber auch das Internet oder viele Gen-Netzwerke haben diese Small-World-Struktur.
Der Python-Befehl zum Erzeugen solcher Netzwerke ist ein wenig sperrig. Die Namen der Entwickler des Algorithmus zur Generierung dieses Netzwerktyps findet in ihm Platz: connected_watts_strogatz_graph
. Als Argumente braucht dieser Generator die Anzahl der Knoten im Netzwerk, die mittlere Anzahl der Verbindungen pro Knoten, und eine Zahl, die festlegt wie groß die Hubs werden dürfen, die entstehen.
# erzeugt ein Small-world-Netzwerk mit 50 Knoten, und durchschnittlich 5 Verbindungen pro Knoten
# die dritte Zahl gibt die Chance an, dass ein Knoten mit einem seiner Verbindungen an einen Hub andockt.
G = nx.connected_watts_strogatz_graph(50, 5, 0.40)
nx.draw_networkx(G)
Durch die Größe des Netzwerks ist die Darstellung nun ein wenig unübersichtlich geworden, und wir können nicht mehr ganz klar erkennen, wer mit wem verbunden ist, bzw. welcher Knoten viele Verbindungen hat und welcher wenige. Übersichtlicher könnte dies werden, wenn wir die Größe der Knoten in der Darstellung davon abhängig machen, welchen degree
sie haben:
import networkx as nx
import matplotlib.pyplot as plt
%matplotlib inline
G = nx.connected_watts_strogatz_graph(50, 5, 0.40)
deg = dict(nx.degree(G)).values()
size = [] # leere Liste wird angelegt
# für jeden Wert in der Degreeliste wird ein Eintrag in die Sizeliste angefügt
for wert in deg:
size.append(50 * wert**1.5)
nx.draw_networkx(G, node_size = size)
Nun sieht man anhand der Größe der Knoten, dass einige Knoten deutlich mehr Verbindungen haben als andere. Diese Hubs des Netzwerks können großen Einfluss auf das Gesamtsystem haben.
In hierarchischen Netzwerken hat zumeist nur ein Knoten eine besondere Stellung. Dieser zentralste Konten steht in Verbindung zu weniger wichtigen Knoten, die ihrerseits in Verbindung zu weiteren, noch weniger zentralen Knoten stehen.
Solche hierarchischen Strukturen finden sich vielfach in Organisiationen, aber auch zum Beispiel in Nahrungsketten im Tierreich. Der NetworkX-Befehl zum Erstellen von hierarchischen Netzwerken lautet balanced_tree
. Als Argumente werden die Anzahl der Knoten-Verbindungen in die jeweils untere Ebene der Hierarchie und die Zahl der Ebenen benötigt.
import networkx as nx
import matplotlib.pyplot as plt
%matplotlib inline
# ein hierarchisches Netzwerk mit 2 Verbindungen "nach unten" und 4 Ebenen
G = nx.balanced_tree(2, 4)
nx.draw_networkx(G)
Besonders übersichtlich wird die Darstellung eines hirarchischen Netzwerks mit speziellem Layout. Da der genaue Aufbau der Funktion für dieses Layout den Rahmen dieses Kapitels sprengt, behandeln wir ihn im Folgenden als "Blackbox". Das heißt, wir zeigen die Funktion und ihren Output im Folgenden, erklären aber nicht wie sie genau funktioniert.
import networkx as nx
import matplotlib.pyplot as plt
%matplotlib inline
#BLACKBOXFUNKTION, die man nicht verstehen muss
def hierarchy_pos(G, root, width=1., vert_gap = 0.2, vert_loc = 0, xcenter = 0.5,
pos = None, parent = None):
if pos == None:
pos = {root:(xcenter,vert_loc)}
else:
pos[root] = (xcenter, vert_loc)
neighbors = list(G.neighbors(root))
if parent != None:
neighbors.remove(parent)
if len(neighbors) != 0:
dx = width/len(neighbors)
nextx = xcenter - width/2 - dx/2
for neighbor in neighbors:
nextx += dx
pos = hierarchy_pos(G,neighbor, width = dx, vert_gap = vert_gap,
vert_loc = vert_loc-vert_gap, xcenter = nextx, pos = pos,
parent = root)
return pos
# ENDE DER BLACKBOXFUNTKION
G=nx.balanced_tree(2, 4)
nx.draw_networkx(G, pos = hierarchy_pos(G, 0))
Auch eine einfache Gitterstruktur (Grid) lässt sich als Netzwerk betrachten. Solche Strukturen treten zum Beispiel in Kristallen auf, aber auch überall sonst, wo Objekte bevorzugt mit ihren nächsten Nachbarn interagieren.
Der Befehl zum Erstellen von Grid-Netzwerken lautet grid_2d_graph
und braucht als Argumente die Länge und die Breite des Gitters.
import networkx as nx
import matplotlib.pyplot as plt
%matplotlib inline
G = nx.grid_2d_graph(4, 7) # erzeugt ein 4 mal 7 Grid
# Spectral-layout für passende Grid-Form
nx.draw_networkx(G,pos = nx.spectral_layout(G), node_size = 800)
Ein so genanntes Caveman-Netzwerk zeichnet sich durch mehrere dichter vernetzte Knoten-Gruppen aus, so genannte Cluster, die untereinander nur wenig miteinander verbunden sind.
Der Befehl zum erzeugen solcher Netzwerke lautet connected_caveman_graph
und benötigt als Argumente die Zahl der Cluster und die Größe der Cluster.
import networkx as nx
import matplotlib.pyplot as plt
%matplotlib inline
G=nx.connected_caveman_graph(4, 5) # Netzwerk aus 4 Clustern mit je 5 Knoten
nx.draw_networkx(G)
Versuchen wir nun, unser bisheriges Wissen über Netzwerke in einem einfachen Anwendungsbeispiel zu nutzen. Wir erstellen ein fiktives Computer-Netzwerk und untersuchen, wie sich ein Software-Virus in diesem Netzwerk ausbreiten würde.
Als Netzwerktypus wählen wir ein Caveman-Netz, in dem vier über das Internet verbundene Unternehmen jeweils sieben Computer in einem firmeneigenen Intranet betreiben.
import networkx as nx
import matplotlib.pyplot as plt
%matplotlib inline
G = nx.connected_caveman_graph(4, 7)
# speichere die Knoten-Positionen, um in allen folgenden Darstellungen das selbe Netzwerk zu sehen
position = nx.spring_layout(G)
nx.draw_networkx(G, pos = position, node_color = "green")
Mit dem folgenden Code fügen wir für jeden Knoten ein Attribut hinzu, in dem später gespeichert wird, ob der jeweilige Computer vom Software-Virus infiziert ist oder nicht. Wir iterieren dazu mit einer Schleife über die Zahl der Knoten des Gesamtnetzwerks (G.number_of_nodes()
), definieren dabei für jeden Knoten das neue Attribut "infiziert" und setzen es auf "nein". Dies geschieht mit G.node[it]["infiziert"] = "nein"
.
# iteriere über alle Knoten und gib ihnen ein neues Attribut namens "infiziert"
# setze dieses neue Attribut auf "nein"
for it in range(G.number_of_nodes()):
G.node[it]["infiziert"] = "nein"
Wir überprüfen diese Operation, indem wir einen beliebigen Knoten - den Knoten mit dem Index 0 - auf seine Atribute befragen:
G.node[0] # frage ab welche Daten im Knoten mit Index 0 gespeichert sind
In ähnlicher Weise lassen sich beliebig viele Daten in einem Knoten speichern. Hier begnügen wir uns aber mit nur einem Attribut.
Im nächsten Schritt infizieren wir einen Knoten (einen Computer) mit dem Software-Virus, zum Beispiel den Knoten mit dem Index 8:
# setze "infiziert" bei Knoten 8 auf "ja":
G.node[8]["infiziert"] = "ja"
# frage ab welche Daten im Knoten 8 gespeichert sind
G.node[8]
Wenn wir nun das Gesamtnetzwerk auf Virusbefall überprüfen sollen, würden wir wohl alle Knoten mithilfe einer For-Schleife befragen:
for it in range(G.number_of_nodes()):
print(G.node[it]), # das Komma am Ende des Print-Befehls schreibt Output fortlaufend in die selbe Zeile
Übersichtlich ist das so natürlich nicht. Schöner wäre es, diese Information mit der Farbe der Knoten widerzugeben. Wir legen dazu eine neue Liste an, in der die Farben der Knoten gespeichert werden: grün für "nicht infiziert" und rot für "infiziert":
farben = []
for it in range(G.number_of_nodes()):
# frage den Wert des Attributes "infiziert" ab:
if G.node[it]["infiziert"] == "ja":
farben.append("red")
if G.node[it]["infiziert"] == "nein":
farben.append("green")
# anstelle der einen Farbe "Grün", plotten wir nun mit der Liste "farben".
# für jeden Knoten gibt es genau einen Eintrag
nx.draw_networkx(G, pos = position, node_color = farben)
In dieser Darstellung ist der infizierte Knoten klar erkennbar.
Wie würde sich ein solcher Software-Virus ausbreiten? Die Epidemiologie kennt viele Modelle zur Infektionsausbreitung. Wir verwenden hier zunächst das einfachste: In jedem Zeitschritt werden alle Knoten infiziert, die mit einem infizierten Knoten verbunden sind.
Dafür nutzen wir einen neuen Befehl: G.neigbors(n)
liefert uns alle Nachbarn (also verbundene Knoten) eines Knoten n
.
Wenn wir nun fünf Zeitschritte dieses einfachen Infektionsmodells simulieren wollen, ergeben sich einige komplexe Verschachtelungen von If-Abfragen und For-Schleifen. Wir sehen uns diese im folgenden Code-Segment genauer an:
# definiere eine allgemeine Zeichenfläche
fig = plt.figure(figsize=(15, 10))
n = 1 # Zähler zum Mitzählen der Subplots
# erster Subplot
ax = fig.add_subplot(2, 3, n)
nx.draw_networkx(G, pos = position, node_color = farben)
# simuliere 5 Zeitschritte
for zeit in range(5):
n += 1 # setze Zähler zum Zählen der Subplots um 1 hinauf
# for-Schleife über alle Knoten
# die Laufvariable nennen wir inf, da wir hier nur die infizierten Knoten betrachten
for inf in range(G.number_of_nodes()):
# wenn der Knoten infizierte ist,
if G.node[inf]["infiziert"] == "ja":
# gehen wir in einer weiteren Schleife über alle Nachbarn des infizierten Knoten:
for opfer in G.neighbors(inf):
# und wenn dieser Knoten noch nicht infiziert ist
if G.node[opfer]["infiziert"] == "nein":
# wird sie in diesem Zeitschritt neu infiziert:
G.node[opfer]["infiziert"] = "neu"
# Ende der if-Abfrage
# Ende der for-Schleife über die Opfer
# Ende der if-Abfrage, die feststellt, ob ein Knoten infiziert war
# Ende der for-Schleife über alle Knoten
# hier sind wir wieder in der for-Schleife über 5 Zeitschritte
farben = []
for it in range(G.number_of_nodes()):
# die neu infizierten Knoten bekommen die Farbe Orange, damit wir die Ausbreitung besser verfolgen können
if G.node[it]["infiziert"] == "ja":
farben.append("red")
if G.node[it]["infiziert"] == "nein":
farben.append("green")
if G.node[it]["infiziert"] == "neu":
farben.append("orange")
# neuer Subplot
ax = fig.add_subplot(2, 3, n)
nx.draw_networkx(G, pos = position, node_color = farben)
# hier ändern wir nun alle "neu" infizierten Knoten auf infizierte ("ja"),
# damit sie im nächsten Schrit rot statt orange erscheinen und ihrerseits weitere Knoten infizieren
for it in range(G.number_of_nodes()):
if G.node[it]["infiziert"] == "neu":
G.node[it]["infiziert"] = "ja"
Mit diesem sehr einfachen Modell können wir also ansatzweise bereits Aspekte der Ausbreitung eines Netzwerk-Virus analysieren. Versuchen sie selbst, dieses Modell zu erweitern, zum Beispiel größere oder andere Netzwerke zu generieren, oder die Knoten mit zusätzlichen Attributen - Antivirenprogramme, Firewalls etc. - auszustatten.
In der Netzwerkforschung werden Objekte als Knoten (nodes) aufgefasst, die mit anderen Knoten verbunden sind. Entscheidend sind hierbei nicht so sehr die speziellen Eigenschaften der Knoten, als vielmehr die Verbindungsstruktur (die Toplogie eines Netzwerks), die sich aus der Zahl der Verbindungen (links, edges) der Knoten - dem so genannten degree - und aller Nachbarknoten ergibt. Bestimmte Netzwerktypen, wie etwa Small-World- oder hierarchische Netzwerke, liegen Phänomenen in unterschiedlichsten Kontexten zugrunde.
In Python steht das Paket NetworkX
zur Darstellung und Analyse von Netzwerken zur Verfügung.
Mit dem Befehl G = nx.Graph()
wird ein neues, leeres Netzwerk mit dem Namen G generiert. Neue Knoten können mit dem Befehl G.add_node("Name_des_neuen_Knoten")
hinzu gefügt werden und mit G.add_edge("Knoten_1","Knoten_2")
mit anderen Knoten verbunden werden.
Um spezielle Netzwerktypen zu erstellen, stehen so genannte Netzwerkgeneratoren bereit, die teils zufällige, teils deterministische Netzwerke generieren. Häufig verwendete Generatoren sind die für Small-World-Netzwerke (connected_watts_strogatz_graph
) und für hierarchische Netzwerke (balanced_tree
).
Um über alle Knoten in einem Netzwerk zu iterieren, steht der Schleifen-Befehl for it in range(G.number_of_nodes()):
bereit. Um einem Knoten ein Attribut zu zuweisen, das zusätzliche Informationen enthält, steht der Befehl G.node["Name_des_Knoten"]["Name_des_Attributs"] = "Wert_des_Attributs"
bereit.
Hiermit endet diese Einführung in das (wissenschaftliche) Programmieren. Wir kennen nun die wichtigsten Strukturen und Konzepte:
Darüber hinaus gibt es aber noch Unmengen mehr zu lernen.
Um mehr über Programmieren mit Python zu lernen, gibt es viele sinnvolle Online-Ressourcen:
https://docs.python.org/3/tutorial/index.html
https://www.w3schools.com/python/
https://www.codecademy.com/learn/learn-python-3
Eine Übersicht über diese und weitere Möglichkeiten etwas über Python zu lernen, findet man hier:
https://docs.python-guide.org/intro/learning/
Wer nur einen bestimmte Struktur sucht, findet sie am einfachsten in der Python Language Reference. Dort sind alle Strukturen der Sprache mit kurzen Erklärungen aufgelistet.
https://docs.python.org/3/reference/index.html
Alle in Python inkludierten Functions findet man in der Functions Reference:
https://www.w3schools.com/python/python_ref_functions.asp
Zusätzlich zu reiner Programmiertechnik kann man sich auch Kenntnisse bezüglich Algorithmen aneignen. Diese kann man dann auch in anderen Programmiersprachen anwenden und komplizierte Probleme lösen. Einen Einstieg in diese Thematik findet man zum Beispiel hier:
https://www.khanacademy.org/computing/computer-science/algorithms
Neben Python gibt es natürlich auch noch viele andere Programmiersprachen. Wer Python beherrscht, und somit ein grundlegendes Verständnis vom Programmieren an sich besitzt, kann sehr einfach eine zweite Sprache lernen. Im wissenschaftlichen Kontext besonders relevant sind:
C++ (universelle, weitverbreitete Programmiersprache)
Matlab (für numerische Berechnungen mit Matrizen in Technik und Ingenieurwesen)
https://learntocode.mathworks.com/
Mathematica (für analytische Berechnungen und mathematische Ableitungen)
http://www.wolfram.com/language/fast-introduction-for-programmers/en/
R (Für Statistik und Datenanalyse)
https://swirlstats.com/students.html
Wenn der erste Schritt einmal geschafft ist, eignet sich Programmieren auch sehr um es durch "Learning by Doing" zu erlernen. Am besten sucht man sich ein Thema oder ein Projekt, an dem man wirklich interessiert ist, und versucht dazu ein Programm zu erstellen. Wenn das Grundgerüst einmal funktioniert kann man es noch beliebig ausbauen und wird dabei sehr viel an Programmiertechnik üben und lernen, ohne dass man die Freude am Arbeiten verliert.
Befehl | Relevantes Kapitel |
---|---|
and |
Kapitel 4 |
arange |
Kapitel 10 |
break |
Kapitel 10 |
class |
Kapitel 11 |
csv.reader |
Kapitel 6 |
def |
Kapitel 9 |
else |
Kapitel 4 |
except |
Kapitel 4 |
for |
Kapitel 2 |
if |
Kapitel 4 |
len |
Kapitel 4 |
liste.append |
Kapitel 1 |
np.array |
Kapitel 5 |
np.cumsum |
Kapitel 6 |
np.diff |
Kapitel 6 |
np.zeros |
Kapitel 8 |
or |
Kapitel 4 |
plt.hist |
Kapitel 7 |
plt.plot |
Kapitel 1 |
random.gauss |
Kapitel 7 |
random.normal |
Kapitel 7 |
range |
Kapitel 1 |
sum |
Kapitel 6 |
try |
Kapitel 4 |
while |
Kapitel 9 |