Listen, Agenten & Populationen#

Nachdem wir nun über die grundlegensten Datentypen und Funktionen in Python Bescheid wissen, steigen wir in diesem Kapitel tiefer in die Funktionsweise eines ganz bestimmten und überaus wichtigen Datentyps ein: Listen. Listen sind in Python - sowie in fast jeder anderen Programmiersprache - ein unverzichtbarer Bestandteil. Listen kann man als eine Art von Datencontainer ansehen, in welchen wir alle möglichen Objekte ablegen können. Listen helfen uns damit dabei, die Objekte unseres Programmes sinnvoll zu strukturieren, damit wir z.B. den Überblick behalten oder auch alle Objekte innerhalb einer Liste auf einmal greifen und mit diesen irgendwas machen können.

Wiederholung: Listen#

Zur Erinnerung: Listen werden in Python durch zwei eckige Klammern erzeugt. Der Inhalt der Listen - irgendwelche Objekte - steht zwischen diesen beiden eckigen Klammern und wird jeweils mit einem Komma getrennt. Dies ist beispielsweise eine leere Liste komplett ohne Inhalt: []. Dies ist eine Liste mit 3 Objekten: ["Haus", 123, False]. Listen kann man natürlich auch als Variable speichern:

meine_tolle_liste = ["Haus", 123, False]

… und dann unter dem Variablennamen wieder abrufen:

meine_tolle_liste
['Haus', 123, False]

Die Liste meine_tolle_liste ermöglicht es also, die 3 Objekte "Haus", 123, False kompakt zu einem Paket zusammenzuschnüren und unter einem Namen zu speichern. Diese Eigenschaft ist allgemein für die Organisation unserer Programme sehr, sehr praktisch.

Wir werden im Verlauf dieses Kapitels u.a. noch lernen, wie wir auf die einzelnen Elemente zugreifen und wie wir weitere Objekte hinzufügen. Nun reiße ich aber erstmal kurz an, wofür wir Listen im Rahmen der Programmierung von ABM meist brauchen.

Wofür wir Listen brauchen#

Ganz allgemein kann man sagen, dass wir Listen im Kontext von ABM brauchen, weil ABMs Computerprogramme sind und man in Computerprogrammen eben Listen braucht. Punkt. Das stimmt zwar. Und wir werden Listen auch an allen Enden und Ecken brauchen, doch gibt es zwei klassische Anwendungsfälle für Listen im Bereich von ABM. Einerseits können wir unsere Gruppen bzw. Populationen von Agenten innerhalb von Listen organisieren. Andererseits können wir auch die Agenten selbst als Listen in unserem Programm repräsentieren, wobei jedes Element in der Liste dann eine Eigenschaft eines Agenten repräsentieren würde.

Doch beginnen wir von vorne. Bei agentenbasierter Modellierung geht es darum, das Handeln und Interagieren vieler Akteure mithilfe eines Computerprogrammes zu simulieren. Das heißt, wir müssen die handelnden Akteure - die Agenten - irgendwie in unserem Programm mittels Objekte repräsentieren. Wie machen wir das?

Im einfachsten Fall könnten wir Agenten einfach mit einem einzigen Wert, z.B. einer Zahl, darstellen. Diese Zahl könnte dann z.B. für eine Meinung des Agenten zur “Corona-Politik” der Bundesregierung auf einer Skala von 0 bis 10 stehen. Das hier wäre z.B. ein Agent mit einem Meinungswert von 4: 4. Das hier wäre ein Agent mit einem Meinungswert von 7: 7. Das hier wäre ein Agent mit einem Meinungswert von 2: 2. Mit so vereinzelten und namenlosen Agenten können wir aber nicht wirklich was anfangen, da wir sie nicht wirklich “packen” können, um mit ihnen irgendwas zu machen, und zudem könnten wir Agenten mit derselben Meinung nicht unterscheiden. Eine Lösung wäre es, jeden Agenten unter eigenen Variable zu speichern. Das wird ab einer bestimmten Anzahl von Agenten auch ziemlich unpraktikabel. Und hier kommen die Listen ins Spiel!

In der Regel speichern wir unsere Agenten in Listen ab, wie ich es hier tue:

population = [4, 2, 7]
population
[4, 2, 7]

Wir haben nun unsere vereinzelten und namenlosen Agenten, welche wir nur über eine einzige Eigenschaft darstellen, kompakt in einer Liste namens population gespeichert. So verlieren wir niemanden aus den Augen und wir können irgendwelche Aktionen immer für die ganze Liste ausführen lassen (was wir in den kommenden Kapiteln lernen werden). Nun können wir auch beliebig viele Agenten (auch mit derselben Meinung) in der Liste speichern:

population = [4, 2, 7, 1, 5, 3, 2, 2, 5, 6, 7, 3]
population
[4, 2, 7, 1, 5, 3, 2, 2, 5, 6, 7, 3]

Nun haben wir es geschafft, mehrere Agenten mit einer Eigenschaft kompakt und praktisch in einer Liste zu speichern. Doch meist sollen unsere Agenten mehr als eine Eigenschaft aufweisen. Sie sollen z.B. nicht nur eine Meinung zum Thema X, sondern z.B. auch ein Alter aufweisen. Daher kann es Sinn machen, auch die Agenten selbst als Liste zu repräsentieren und alle Agenten-Eigenschaften innerhalb einer Liste zusammenzufassen. Schauen wir uns das mal an einem anderen Beispiel an.

Angenommen ein Agent verfügt über folgende drei Eigenschaften:

  • Infektionsstatus (mit den möglichen Ausprägungen "infizierbar", "infiziert", "genesen" und "geimpft")

  • Symptome (mit den möglichen Ausprägungen True und False)

  • Tage seit der Infektion (Diese Eigenschaft macht natürlich nur Sinn, wenn der Agent bereits infiziert wurde. Dann sollte die Anzahl der Tage seit der Infektion als ganze Zahl d.h. als int repräsentiert werden. Falls der Agent jedoch bisher noch nie infiziert wurde, dann setzen wir den Platzhalter None, den man wie ein “Missing” in der Statistik verstehen kann, ein.)

Wir könnten diese Eigenschaften eines Agenten nun in einer Liste sammeln. Ein Agent könnte dann mit folgender Liste repräsentiert werden:

["infizierbar", False, None]

Diese Liste repräsentiert nun für uns einen Agenten, welcher theoretisch mit dem Virus infizierbar ist, aktuell keine Symptome aufweist und keine Tage seit der Ansteckung aufweist, da er noch nie infiziert war.

Natürlich könnte man auch diesen Agenten wieder unter einer Variable speichern:

agent1 = ["infizierbar", False, None]
agent1
['infizierbar', False, None]

Ein anderer Agent könnte zum Beispiel so aussehen:

agent2 = ["infiziert", True, 5]
agent2
['infiziert', True, 5]

Dies wäre für uns ein Agent, der aktuell infiziert ist, Symptome einer Krankheit aufweist und dessen Infektion 5 Tage her ist.

Ein dritter Agent könnte beispielsweise infiziert sein, keine Symptome aufweisen und erst vor 2 Tagen angesteckt worden sein:

agent3 = ["infiziert", False, 2]
agent3
['infiziert', False, 2]

Um sinnvoll mit diesen Agenten “arbeiten” zu können, bietet es sich dann auch hier wieder an, die Agenten innerhalb einer weiteren Liste zusammenzufassen. Dies könnte dann so aussehen:

population = [agent1, agent2, agent3]
population
[['infizierbar', False, None], ['infiziert', True, 5], ['infiziert', False, 2]]

Was wir hier bei der Liste population nun sehen, ist eine Datenstruktur, mit der wir noch öfter zu tun haben werden, die uns manchmal etwas Kopfzerbrechen bereiten wird und mit der wir uns daher so früh wie möglich anfreunden sollten. Es ist eine Liste von Listen bzw. eine sogenannte geschachtelte Liste. Wir haben also eine Liste, deren Inhalt selbst wieder aus Listen besteht. Wir werden uns weiter unten in diesem Kapitel und auch in folgenden Kapiteln noch ausgiebig mit dieser Datenstruktur beschäftigen.

An dieser Stelle möchte ich nur eine Sache erwähnen: Diese Datenstruktur verdeutlicht, aus welchen Ebenen/Bestandteilen/Objekten/Paketen ein großer Teil unseres Programmes in aller Regel bestehen wird bzw. wie wir unsere virtuelle Welt in unserem Computer organisieren:

  • Auf der untersten bzw. innersten Ebene sind die Eigenschaften unserer Agenten.

  • Dieses Innenleben der Agenten schnüren wir dann jeweils zu einem Paket bzw. Objekt zusammen (in diesem Fall zu Listen), welche dann jeweils einen Agenten als eine Einheit repräsentieren.

  • Die Agenten-Objekte wiederum fassen wir dann meist auch wieder in einer oder mehreren Liste(n) zusammen, um sinnvoll mit der ganzen Gruppe von Agenten arbeiten zu können

Übrigens

Oben sieht das Erstellen der Population von Agenten und den jeweils einzelnen Eigenschaften einzelner Agenten noch nach viel Getippe aus. In den nächsten Kapitel werden wir jedoch lernen, wie wir sehr große Populationen von Agenten relativ entspannt “automatisch” erstellen lassen können, ohne jeden Agenten einzeln selbst explizit definieren oder benennen zu müssen.

Zugegeben

Die Repräsentation der Agenten als Listen ist nicht sonderlich elegant. Wir müssen uns ja z.B. merken, welcher Stelle in der Liste welche Eigenschaft der Agenten steht. U.a. deshalb ist es auf lange Sicht unpraktisch, die Agenten als Listen darzustellen. Wir werden später die Agenten durch andere Objekt-Klassen ersetzen. Am Anfang macht es allerdings Sinn, nicht nur, weil wir Listen sowieso zwingend kennenlernen müssen, sondern auch, weil es eine relativ unkomplizierte und schnell erlernbare Form der Agenten-Repräsentation “mit einfachen Mitteln” ist.

Noch ein paar Worte zur Repräsentation von Agenten#

Wenn wir Agenten einfach nur als einzelne Werte oder Listen von Werten darstellen, dann stellen wir zunächst eigentlich nur die “passiven” Eigenschaften der Agenten mit relativ “passiven” Daten dar. Da fehlt aber doch noch etwas? Ein Agent ist doch ein handelnder Akteur und nicht nur ein passiver Datensatz! Ein Agent sollte ja beispielsweise nicht nur die passive Eigenschaft haben, eine Meinung zu besitzen, sondern auch die aktive Eigenschaft, diese beispielsweise auch kundtun oder ändern zu können. Und ein Agent darf in Simulationen nicht nur einen Infektionsstatus haben, sondern muss auch die aktive Eigenschaft, andere Agenten anstecken zu können, besitzen.

Den aktiven Teil der Agenten werden wir meist als Funktionen, also Programmanweisungen darüber, wie die Daten/Eigenschaften der Agenten verarbeitet werden sollen, repräsentieren. Zum Beispiel könnten wir die Handlung “anstecken” darstellen, indem wir mithilfe einer Funktion den Infektionsstatus "infiziert" vom einen Agenten auf einen anderen Agenten mit dem Status "infizierbar" übertragen, sodass dieser zweite Agent dann auch den Status "infiziert" aufweist.

Zu Beginn werden wir dabei Funktionen und Daten jedoch noch relativ getrennt voneinander organisieren d.h. wir werden an der einen Stelle im Code die “passiven” Eigenschaften der Agenten in irgendeiner Form als Daten darstellen und an der anderen Stelle im Code die potentiellen “Handlungen” der Agenten als Funktionen implementieren. Erst während der Simulation, wenn ein Agent handelt, werden wir Daten und Funktionen zusammenbringen. Dann wird die entsprechende Funktion auf die entsprechenden Daten des handelnden Agenten (z.B. der ansteckende Agent) sowie der an einer Interaktion beteiligten Agenten (z.B. der angesteckte Agent) angewendet.

In einer ähnlichen Logik werden wir zwar immer die Handlungen der Agenten in den Simulationen darstellen, dennoch werden wir später die Trennung zwischen passiven und aktiven Eigenschaften aufheben. Wir werden dann Objekte definieren, die sowohl die passiven Daten als auch die aktiven Funktionen bzw. Handlungen der Agenten vereinen. Letztlich macht das für die Simulationsergebnisse keinen Unterschied, es ist nur eine bessere Form der Code-Organisation und “natürlichere” Form der Darstellung handelnder Akteure. Wie man das macht, werden wir im Kapitel zur “Objektorientierten Programmierung” lernen. Weil man dazu aber einiges an Wissen über die Grundlagen von Python braucht, werden wir zu Beginn die passiven Eigenschaften und die aktiven Eigenschaften der Agenten noch getrennt implementieren z.B. indem wir die Daten eines Agenten innerhalb einer Liste abspeichern, wie wir in diesem Kapitel lernen werden.

Übrigens

Auch wenn wir selbst erst in späteren Kapiteln lernen werden, wie man Objekte definiert, die sowohl passive Daten als auch aktive Funktionen innerhalb eines Objektes integrieren, werden wir diese Art von Objekten dennoch schon vorher verwenden. Denn Python ist voll mit diesen Objekten. Auch Listen selbst verfügen übrigens über eine “aktive” Seite, welche Listen überhaupt erst so praktisch machen und wir noch in diesem Kapitel kennenlernen werden.