Kapitel 1 - Programmiergrundlagen

Was ist Programmieren?

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 Programmiersprache Python

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

  1. in ihrer klaren und übersichtlichen Syntax als leicht erlernbar ist,
  2. damit in vielen Disziplinen mittlerweile zu einem wissenschaftlichen Standard geworden ist
  3. in all ihren Grundlagen offen und damit kostenlos zu verwenden ist
  4. eine umfangreiche Standardbibliothek und zahlreiche frei zugängliche Spezialmodule umfasst
  5. eine leicht installierbare und komfortabel im Browser laufende Programmierumgebung zur Verfügung stellt
  6. und von einer sehr großen Community beständig weiterentwickelt wird.

Anaconda

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

Anaconda Installation für Python 3.8

screenshot

  • Klicken Sie, wenn Sie einen entsprechenden Computer haben, auf den Button „64-bit graphical installer“ um Python 3.8 Version zu erhalten. Ansonsten wählen Sie die 32-bit-Version. Daraufhin startet der Download der Installationsdatei. Dies kann einige Minuten in Anspruch nehmen. Achten Sie darauf, dass der Pfad zu dem Ordner, in dem Sie Ihre Downloads speichern, und auch der Ordnername selbst keine Sonderzeichen (wie Umlaute etc.) oder Leerzeichen enthält. Sollte dies der Fall sein, so kopieren Sie die heruntergeladene Datei bevor Sie sie ausführen in einen entsprechenden Ordner.
  • Doppelklicken Sie auf die .exe Datei, die Sie heruntergeladen haben.
  • Klicken Sie im automatisch neu geöffneten Fenster auf den Button „Ausführen“.
  • Folgen Sie den weiteren Dialog-Fenstern.
  • Im Licence Agreement-Fenster klicken Sie auf den Button „I Agree“, wenn Sie den Lizenzbedingungen zustimmen.
  • Folgen Sie erneut den Dialog-Fenstern. Wir empfehlen die jeweils vorgeschlagenen Angaben zu übernehmen. Im Fenster Choose Install Location sollten sie erneut darauf achten, dass der Pfad zu dem Ordner, in den sie Anaconda installieren wollen, und auch der Ordnername selbst keine Sonderzeichen (Umlaute etc.) oder Leerzeichen enthalten. Mit einem Klick auf den Button „Browse...“ können Sie den automatisch vorgeschlagenen Ordner ändern und Ihren gewünschten Speicherort auswählen. Klicken sie erneut „Next >“ um mit der Installation fortzufahren.
  • Klicken Sie auf den Button „Install“ und schließlich „Finish“ um die Installation fertigzustellen.

Nach erfolgreicher Installation sollten sie in Ihrem Windows Startmenü einen Menüpunkt Anaconda sehen, der seinerseits einen Menü - Unterpunkt Jupyter Notebook enthält.

Jupyter Notebook

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.

Kapitel 2 – Einführung in Python – Der Froschteich

Verwenden von Jupyter-Notebooks

Jupyter-Notebooks bestehen aus Zellen. Es gibt drei Typen von Zellen:

  • Textzellen, so wie diese hier, in welche man Texte schreiben und formatieren kann
  • In-Zellen, in die man Python-Code schreiben kann
  • Out-Zellen, die das Ergebnis der darüberstehenden In-Zelle ausgeben

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:

In [1]:
7 * 7 - 7
Out[1]:
42
In [2]:
((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.
Out[2]:
81.0

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.

Variablen definieren

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.

In [3]:
froschanzahl = 0

Variablen verändern

Um eine Variable zu ändern kann man einfach den aktuellen Wert mit dem neuen Wert überschreiben. Auch das geschieht mit dem = Zeichen.

In [4]:
# Jahr 1:
froschanzahl = 3

Variablen abrufen

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.

In [5]:
print(froschanzahl)
3

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:

In [6]:
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)
15

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

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:

In [7]:
froschanzahl = 0
for it in range(5):
    froschanzahl = froschanzahl + 3
print(froschanzahl)
15

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.

Einrücken in Python

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:

In [8]:
froschanzahl = 0
for it in range(5):
    froschanzahl = froschanzahl + 3
    print(froschanzahl)
3
6
9
12
15

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:

In [9]:
for it in range(5):
    froschanzahl = 0
    froschanzahl = froschanzahl + 3
    print(froschanzahl)
3
3
3
3
3

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:

In [10]:
froschanzahl = 0
for it in range(10):
    froschanzahl = froschanzahl + 3
print(froschanzahl)
30

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.

In [11]:
simulationszeit = 10
#simulationszeit: Die Zeit in Jahren, die der Froschteich simuliert wird

froschanzahl = 0
for it in range(simulationszeit):
    froschanzahl = froschanzahl + 3
print(froschanzahl)
30

Mit diesem Programm können wir nun auch sehr lange Zeitbereiche simulieren:

In [12]:
simulationszeit = 1000
#simulationszeit: Die Zeit in Jahren, die der Froschteich simuliert wird

froschanzahl = 0
for it in range(simulationszeit):
    froschanzahl = froschanzahl + 3
print(froschanzahl)
3000

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:

In [13]:
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 
3 6 9 12 15 18 21 24 27 30 33 36 39 42 45 48 51 54 57 60 63 66 69 72 75 78 81 84 87 90

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 in Python

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:

In [14]:
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)
[0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54, 57, 60, 63, 66, 69, 72, 75, 78, 81, 84, 87, 90]

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.

Grafiken in Python

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.

In [2]:
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)
Out[2]:
[<matplotlib.lines.Line2D at 0x1ef1f2a71d0>]

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:

In [16]:
plt.plot(froschanzahl_liste, color = 'green')
Out[16]:
[<matplotlib.lines.Line2D at 0xb0f9828>]

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:

In [3]:
plt.plot(froschanzahl_liste, color = 'green')
plt.xlabel('Zeit (Jahre)')
plt.ylabel('Frösche')
Out[3]:
<matplotlib.text.Text at 0x1ef1f2d64e0>

Zusammenfassung

Variablen

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.

For-Schleifen

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.

Listen

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)

Grafiken

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')

Kapitel 3 - Populationsentwicklungen

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:

Lineares Wachstum

Wir ziehen nun als weiteres Beispiel die Vermehrung von Kaninchen heran.

In [2]:
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)
Out[2]:
[<matplotlib.lines.Line2D at 0x246c4dacba8>]

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.

Exponentielles 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:

In [1]:
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)
Out[1]:
[<matplotlib.lines.Line2D at 0x23810428a58>]

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:

In [4]:
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)
Out[4]:
[<matplotlib.lines.Line2D at 0x23810648c50>]

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- 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.

In [5]:
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!")
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:

In [6]:
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)
Es gibt jetzt mehr als 100 Kaninchen!
Die exakte Zahl der Kaninchen ist 
101
Es gibt jetzt mehr als 100 Kaninchen!
Die exakte Zahl der Kaninchen ist 
102
Es gibt jetzt mehr als 100 Kaninchen!
Die exakte Zahl der Kaninchen ist 
103
Es gibt jetzt mehr als 100 Kaninchen!
Die exakte Zahl der Kaninchen ist 
104
Es gibt jetzt mehr als 100 Kaninchen!
Die exakte Zahl der Kaninchen ist 
105

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:

In [8]:
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)
Die exakte Zahl der Kaninchen ist 
96
Die exakte Zahl der Kaninchen ist 
97
Die exakte Zahl der Kaninchen ist 
98
Die exakte Zahl der Kaninchen ist 
99
Die exakte Zahl der Kaninchen ist 
100
Es gibt jetzt mehr als 100 Kaninchen!
Die exakte Zahl der Kaninchen ist 
101
Es gibt jetzt mehr als 100 Kaninchen!
Die exakte Zahl der Kaninchen ist 
102
Es gibt jetzt mehr als 100 Kaninchen!
Die exakte Zahl der Kaninchen ist 
103
Es gibt jetzt mehr als 100 Kaninchen!
Die exakte Zahl der Kaninchen ist 
104
Es gibt jetzt mehr als 100 Kaninchen!
Die exakte Zahl der Kaninchen ist 
105

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:

In [10]:
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)
Es gibt jetzt mehr als 100 Kaninchen!
Es gibt jetzt mehr als 100 Kaninchen!
Es gibt jetzt mehr als 100 Kaninchen!
Es gibt jetzt mehr als 100 Kaninchen!
Es gibt jetzt mehr als 100 Kaninchen!
Die exakte Zahl der Kaninchen ist 
105

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:

In [15]:
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)
Out[15]:
[<matplotlib.lines.Line2D at 0x2381099eb00>]

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

Fibonacci-Folge

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:

Listen und Schleifen für Fortgeschrittene

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.

In [5]:
kaninchenanzahl_liste[0]
Out[5]:
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:

In [3]:
for it in range(5):
    print(it)
0
1
2
3
4

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:

In [4]:
for it in range(2,5):
    print(it)
2
3
4
In [5]:
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)
Out[5]:
[<matplotlib.lines.Line2D at 0xb05ff28>]

Natürlich können wir auch hier eine Beschränkung des Wachstums durch eine If-Abfrage einbauen, ganz gleich wie beim exponentiellen Wachstum:

In [2]:
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)
Out[2]:
[<matplotlib.lines.Line2D at 0x2ab3a336828>]

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

In [6]:
print(kaninchenanzahl_liste[14:19])
[610, 987, 1597, 1597, 1597]

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.

In [10]:
print(kaninchenanzahl_liste[:3])
print(kaninchenanzahl_liste[17:])
[1, 1, 2]
[1597, 1597, 1597]

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:

In [11]:
print(kaninchenanzahl_liste[-5:])
[987, 1597, 1597, 1597, 1597]

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:

In [12]:
# Listen werden geplottet    
plt.plot(kaninchenanzahl_liste[14:19])
Out[12]:
[<matplotlib.lines.Line2D at 0x2ab3a44e2b0>]

Zusammenfassung

Lineares Wachstum

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.

Exponentielles Wachstum

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

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.

For-Schleifen für Fortgeschrittene

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

Listen für Fortgeschrittene

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.

Auswählen von Teilen einer Liste

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.

Kapitel 4 - Schadstoffprotokolle analysieren

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.

In [12]:
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,"!") 
Zu hohe Schadstoffwerte am Tag 2 !
Zu hohe Schadstoffwerte am Tag 4 !
Zu hohe Schadstoffwerte am Tag 5 !
Zu hohe Schadstoffwerte am Tag 9 !

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.

In [13]:
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,"!") 
Zu hohe Schadstoffwerte am Tag 5 !

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:

In [14]:
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,"!") 
Zu hohe Schadstoffwerte am Tag 5 !

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:

In [15]:
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,"!") 
Zu hohe Schadstoffwerte am Tag 2 !
Zu hohe Schadstoffwerte am Tag 5 !

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:

In [16]:
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)
Zu hohe Schadstoffwerte am Tag 2 !
Zu hohe Schadstoffwerte am Tag 5 !
Out[16]:
[<matplotlib.lines.Line2D at 0x1835005f2e8>]

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:

In [33]:
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)
Zu hohe Schadstoffwerte am Tag 2!
Out[33]:
[<matplotlib.lines.Line2D at 0x2ab3a52cac8>]

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:

In [37]:
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)
Out[37]:
[<matplotlib.lines.Line2D at 0x2ab3a571128>]

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:

In [17]:
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)
Zu hohe Schadstoffwerte am Tag 2 !
Zu hohe Schadstoffwerte am Tag 5 !
Zu hohe Schadstoffwerte am Tag 6 !
Zu hohe Schadstoffwerte am Tag 11 !
Out[17]:
[<matplotlib.lines.Line2D at 0x18350082a20>]

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:

In [18]:
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)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-18-55dec0f13f96> in <module>()
     16 
     17 for it in range(1,len(werte)): #Schleife hat die Länge der Liste
---> 18     if (werte[it] > 100 and werte[it-1] > 100) or (werte[it] > 120 and werte[it-1] > 50):
     19         print("Zu hohe Schadstoffwerte am Tag",it, "!")
     20         protokoll.append(1)

TypeError: '>' not supported between instances of 'str' and 'int'

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:

In [19]:
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)
Zu hohe Schadstoffwerte am Tag 3 !
Zu hohe Schadstoffwerte am Tag 8 !
Zu hohe Schadstoffwerte am Tag 14 !
Zu hohe Schadstoffwerte am Tag 15 !
Zu hohe Schadstoffwerte am Tag 16 !
Zu hohe Schadstoffwerte am Tag 17 !
Out[19]:
[<matplotlib.lines.Line2D at 0x183500fe8d0>]

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:

In [20]:
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)
Zu hohe Schadstoffwerte am Tag 3 !
Zu hohe Schadstoffwerte am Tag 6 !
Zu hohe Schadstoffwerte am Tag 7 !
Zu hohe Schadstoffwerte am Tag 8 !
Zu hohe Schadstoffwerte am Tag 13 !
Zu hohe Schadstoffwerte am Tag 14 !
Zu hohe Schadstoffwerte am Tag 15 !
Zu hohe Schadstoffwerte am Tag 16 !
Zu hohe Schadstoffwerte am Tag 17 !
Out[20]:
[<matplotlib.lines.Line2D at 0x1835015cdd8>]

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:

In [56]:
testwerte = [99, 120, 0, 101, "k.A.", 140]

for it in range(len(testwerte)):
    if testwerte[it] > 100:
        print("Alarm!")
    
Alarm!
Alarm!
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-56-09c2b072c013> in <module>()
      2 
      3 for it in range(len(testwerte)):
----> 4     if testwerte[it] > 100:
      5         print("Alarm!")
      6 

TypeError: '>' not supported between instances of 'str' and 'int'

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:

In [60]:
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!")
Alarm!
---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
<ipython-input-60-cd0f50d30d93> in <module>()
      3 for it in range(len(testwerte)):
      4     if testwerte[it] == 0:
----> 5         raise Exception("Dieser Wert darf nicht 0 sein!")
      6     if testwerte[it] > 100:
      7         print("Alarm!")

Exception: Dieser Wert darf nicht 0 sein!

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.

In [62]:
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!")
Alarm!
Ein Eintrag wurde übersprungen!
Alarm!
Ein Eintrag wurde übersprungen!
Alarm!

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:

In [66]:
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!")
Ein Eintrag wurde übersprungen!
Ein Eintrag wurde übersprungen!
Ein Eintrag wurde übersprungen!
Ein Eintrag wurde übersprungen!
Ein Eintrag wurde übersprungen!
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.

In [72]:
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)
Alarm!
Ein Eintrag wurde übersprungen! Der Fehler war:
Dieser Wert darf nicht 0 sein!
Alarm!
Ein Eintrag wurde übersprungen! Der Fehler war:
'>' not supported between instances of 'str' and 'int'
Alarm!

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:

In [25]:
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)
Tag 0
Kein Alarm!
Tag 1
Alarm!
Tag 2
Fehler!
Tag 3
Alarm!
Tag 4
Fehler!
Tag 5
Alarm!
Fehlerprotokoll:
[0, 'Fehlerfrei!', 1, 'Fehlerfrei!', 2, 'Ein Eintrag wurde übersprungen! Der Fehler war:', Exception('Dieser Wert darf nicht 0 sein!'), 3, 'Fehlerfrei!', 4, 'Ein Eintrag wurde übersprungen! Der Fehler war:', TypeError("'>' not supported between instances of 'str' and 'int'"), 5, 'Fehlerfrei!']

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.

Zusammenfassung

If-Else Abfragen

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

and / or

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.

  • wahr and wahr = wahr
  • wahr and falsch = falsch
  • wahr or wahr = wahr
  • wahr or falsch = wahr
  • falsch or falsch = falsch

Try und Except

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.

Kapitel 5 - Gekoppelte Differentialgleichungen: Räuber-Beute-Systeme

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.

In [1]:
# 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)
Out[1]:
[<matplotlib.lines.Line2D at 0x26d15b5bc18>]

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 $$

In [36]:
# 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)
Out[36]:
[<matplotlib.lines.Line2D at 0x26d250d65f8>]

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.

In [40]:
# 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)
Out[40]:
[<matplotlib.lines.Line2D at 0x26d15b5b588>]

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:

In [44]:
# 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)
Out[44]:
[<matplotlib.lines.Line2D at 0x26d2543dfd0>]

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.

In [49]:
# 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)
Out[49]:
[<matplotlib.lines.Line2D at 0x26d253ed208>]

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.

Phasenraumdarstellung

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:

In [50]:
plt.plot(hasen_array,luchs_array)
plt.title("Phasenraumdarstellung")
plt.xlabel("Beute")
plt.ylabel("Räuber")
Out[50]:
<matplotlib.text.Text at 0x26d26a399b0>

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.

In [63]:
# 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")
Out[63]:
<matplotlib.text.Text at 0x26d0b1e2d30>

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])

In [1]:
# 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)
Hasenmittelwert ändert sich von
1.960012221793669
auf
2.71757133048013

Luchsmittelwert ändert sich von
0.9988842085172154
auf
0.855243907572326

Wir sehen, auch die dritte Lotka-Volterra-Regel wird von unserem Modell erfüllt.

Zusammenfassung

Gekoppelte Differentialgleichungssysteme

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.

Phasenraumdarstellung

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.

Die Lotka-Volterra-Regeln

  • 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.
  • Die durchschnittlichen Größen der beiden Populationen bleiben über längere Zeiträume konstant, auch wenn Maxima und Minima unterschiedlich sind.
  • 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.

Arrays

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.

Kapitel 6 - Differenzieren und Integrieren - ein Solarauto

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).

Einlesen von Daten

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.

In [1]:
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:

In [2]:
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
<open file 'solargrob.txt', mode 'r' at 0x000000000A4DCA50>

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.

In [3]:
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
['3.77513454428e-08']
['1.74472887359e-06']
['5.77774851942e-05']
['0.00137095908638']
['0.0233091011429']
['0.283962983903']
['2.47875217667']
['15.503853599']
['69.4834512228']
['223.130160148']
['313.417119033']
['446.481724891']
['1000.0']
['846.481724891']
['513.417119033']
['223.130160148']
['69.4834512228']
['15.503853599']
['2.47875217667']
['0.283962983903']
['0.0233091011429']
['0.00137095908638']
['5.77774851942e-05']
['1.74472887359e-06']

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:

In [4]:
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
3.77513454428e-08
1.74472887359e-06
5.77774851942e-05
0.00137095908638
0.0233091011429
0.283962983903
2.47875217667
15.503853599
69.4834512228
223.130160148
313.417119033
446.481724891
1000.0
846.481724891
513.417119033
223.130160148
69.4834512228
15.503853599
2.47875217667
0.283962983903
0.0233091011429
0.00137095908638
5.77774851942e-05
1.74472887359e-06

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.

In [5]:
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  
3.77513454428e-08
<type 'str'>
1.74472887359e-06
<type 'str'>
5.77774851942e-05
<type 'str'>
0.00137095908638
<type 'str'>
0.0233091011429
<type 'str'>

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):

In [6]:
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  
3.77513454428e-08
<type 'float'>
1.74472887359e-06
<type 'float'>
5.77774851942e-05
<type 'float'>
0.00137095908638
<type 'float'>
0.0233091011429
<type 'float'>

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:

In [7]:
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
[3.77513454428e-08, 1.74472887359e-06, 5.77774851942e-05, 0.00137095908638, 0.0233091011429, 0.283962983903, 2.47875217667, 15.503853599, 69.4834512228, 223.130160148, 313.417119033, 446.481724891, 1000.0, 846.481724891, 513.417119033, 223.130160148, 69.4834512228, 15.503853599, 2.47875217667, 0.283962983903, 0.0233091011429, 0.00137095908638, 5.77774851942e-05, 1.74472887359e-06]

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.

In [8]:
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)
Out[8]:
(0, 23)

Numerisches Integrieren

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:

In [9]:
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)
3741.60752731

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:

In [10]:
plt.bar(range(24), solargrob, width = 1.0, color = "orange")
plt.xlim(0, 23)
plt.ylabel("Leistung / Watt")
plt.xlabel("Zeit / Stunden")
Out[10]:
<matplotlib.text.Text at 0xad61630>

Erhöhen der Genauigkeit

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.)

In [11]:
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)
Out[11]:
(0, 1380)

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):

In [5]:
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)
4086.1419317488526

Wir sehen also, da