7.2 Schellings Segregationsmodell
Contents
7.2 Schellings Segregationsmodell#
Im Folgenden programmieren wir eine Variante des sogenannten Segregrationsmodell von Schelling. Dieses wird so ziemlich in jeder Einführung in die agentenbasierte Modellierung in den Sozialwissenschaften erwähnt und wurde auch in diesem Lehrskript im ersten Kapitel kurz behandelt. Einen Original-Text von Schelling über das Segregationsmodell findest du z.B. hier.
Das Grundmodell geht so: Auf einer zweidimensionalen Landschaft (dem sogenannten Grid), die in viele gleichgroße Quadrate eingeteilt ist (auch Zellen oder Felder genannt), “wohnen” Agenten zweier unterschiedlicher Gruppen. Jeder Agent bewohnt dabei genau ein Feld. Jeder Agent hat das Bedürfnis, dass in der direkten Nachbarschaft d.h. in den 8 Zellen um die eigene Zelle (der sogenannten Moore-Umgebung) die eigene Gruppe mindestens einen bestimmten Anteil der Gesamtzahl an Agenten ausmacht. Falls der Anteil der eigenen Gruppe in der Nachbarschaft zu gering ist, dann sucht der Agent nach einem neuen Wohnort und zieht um.
Das Modell soll zeigen, dass eine stark segregierte Landschaft d.h. deutlich voneinander abgegrenzbare Wohngebiete, in denen entweder nur die eine oder nur die andere Gruppe wohnt, bereits bei einem geringen Bedürfnis nach Nachbarn der eigenen Gruppe (Homophilie) entsteht. Die unterschiedlichen Gruppen können beispielsweise für verschiedene soziale Milieus oder unterschiedliche ethnische Gruppen stehen. Schelling selbst nimmt vor allem die getrennten Wohngebiete der schwarzen und weißen Bevölkerung in den USA als Beispiel.
Eine mögliche Implementierung des Modells#
Das Segregationsmodell von Schelling kann - wie immer - auf sehr viele verschiedene Weisen in Code übersetzt werden. In der folgenden Implementierung des Modells werden nicht die Agenten, die auf den Zellen wohnen, als die in der Simulation primär handelnden Entitäten umgesetzt, sondern die Zellen selbst. Eine Zelle schaut während der Simulation z.B. darauf, ob die Zusammensetzung der Nachbarschaft den Ansprüchen des aktuell bewohnenden Agenten genügen und schickt diesen fort, wenn das nicht so ist. Die Zellen befinden sich während der Simulation in einer Matrix (geschachtelte Liste) und bilden so das Grid der Simulation.
Ich habe den Code für diese Simulation wieder in mehrere Funktionen unterteilt, welche ich im Folgenden durchspreche. Zunächst soll jedoch kurz auf die Repräsentation der Zellen als Dictionary eingegangen werden.
Die Zellen#
Die Zellen sind in dieser Umsetzung des Schelling-Modells die eigentlichen Akteure in der Simulation. Jede Zelle wird als dict
mit fünf Attributen repräsentiert. Unten sieht man eine beispielhafte Zelle, wobei teils als Platzhalter die erwarteten Datentypen eingesetzt wurden.
cell = {
"row": int,
"col": int,
"resident": int,
"share_of_same_group": float,
"neighbor_cells": [],
}
"row"
und"col"
geben die Zeilen- und Spaltenposition auf dem Grid an und werden zu Beginn festgelegt."resident"
symbolisiert den auf der Zelle lebenden Agenten. Dieser wird einfach als eine Zahl repräsentiert. Die Zahl0
steht für eine leere Zelle, die Zahl1
für einen Agenten der Gruppe 1 und die Zahl2
für einen Agenten der Gruppe 2.Unter
"share_of_same_group"
wird im Verlauf der Simulation eingespeichert, wie hoch der Anteil an Mitgliedern derselben Gruppe innerhalb der Nachbarschaft ist."neighbor_cells"
ist eine Liste, in welcher zu Beginn der Simulation alle benachbarten Zellen eingespeichert werden, damit eine Zelle einen schnellen und unkomplizierten Zugriff auf ihre Nachbarschaft hat.
Das Grid erstellen und besiedeln#
Die Funktion create_grid()
erstellt das Grid als geschachtelte Liste von Zellen.
Über die Argumente
n_rows
undn_cols
kann die Anzahl der Zeilen und Spalten des Grids festgelegt werden. Übern_group1
undn_group2
kann die Anzahl der Agenten aus Gruppe 1 und Gruppe 2 festgelegt werden.Als erstes wird innerhalb der Funktion
create_grid()
mittels zweier geschachtelter For-Loops eine Matrix bzw. das Grid erstellt, wobei auf jede Position der Matrix eine Zelle gesetzt wird. Die Positionen"row"
und"col"
werden entsprechend festgelegt und das Attribut"resident"
wird auf0
gesetzt. Das Endprodukt dieses For-Loops ist somit eine Matrix bzw. ein Grid bestehend aus unbewohnten Zellen.Um später einfacher mit den Zellen des Grids arbeiten zu können, wird danach eine Liste
cell_population
erstellt, in welche alle Zellen des Grids eingefügt werden. Somit hat man neben der Matrix, in welcher sich alle Zellen befinden auch eine flache, nicht geschachtelte Liste, in der sich ebenfalls alle Zellen bzw. Zeiger auf alle Zellen befinden.Wie eine solche flache Liste aller Zellen das Leben vereinfacht, zeigt sich bereits im nächsten Schritt. Dort gehe ich alle Zellen durch und identifiziere mittels der Funktion
find_neighbor_cells()
alle Nachbarzellen einer Zelle. Dank der Listecell_population
reicht nun ein For-Loop, um alle Zellen durchzugehen. Für das Durchgehen aller Zellen über die geschachtelte Listegrid
hätte man stattdessen zwei For-Loops gebraucht. Die Funktionfind_neighbor_cells()
wird weiter unten beschrieben.In den letzten zwei For-Loops werden den unbewohnten Zellen Agenten zugewiesen. Dafür wird innerhalb der For-Loops mittels der Funktion
get_empty_and_occupied_cells()
jeweils Listen von allen unbewohnten und bewohnten Zellen erstellt und in den Variablenempty_cells
undoccupied_cells
gespeichert. Dann wird für jeden für eine Gruppe zu erstellenden Agenten zufällig eine unbewohnte Zelle ausgewählt und das Attribut"resident"
in den jeweiligen Gruppencode umgewandelt.Mittels
return
wird das erstellte Grid in zweifacher Form ausgegeben: Zum einen als geschachtelte Liste (Matrix) von Zellengrid
, zum anderen als flache Liste von Zellencell_population
.
import random
import celluloid as cld
import matplotlib.pyplot as plt
def create_grid(n_rows, n_cols, n_group1, n_group2):
# Grid als Liste von Listen erstellen und auf jede Position eine unbewohnte Zelle setzen
grid = []
for i in range(n_rows):
row = []
for j in range(n_cols):
cell = {
"row": i,
"col": j,
"resident": 0,
"share_of_same_group": None,
"neighbor_cells": [],
}
row.append(cell)
grid.append(row)
# Alle Zellen auch in einer normalen, flachen Liste speichern,
# damit man einfacher per For-Loop alle Zellen durchgehen kann
cell_population = []
for row in grid:
for cell in row:
cell_population.append(cell)
# für jede Zelle die benachbarten Zellen finden
for cell in cell_population:
find_neighbor_cells(cell, grid)
# Agenten der Gruppe 1 auf Grid setzen
for i in range(n_group1):
empty_cells, occupied_cells = get_empty_and_occupied_cells(cell_population)
random_empty_cell = random.choice(empty_cells)
random_empty_cell["resident"] = 1
# Agenten der Gruppe 2 auf Grid setzen
for i in range(n_group2):
empty_cells, occupied_cells = get_empty_and_occupied_cells(cell_population)
random_empty_cell = random.choice(empty_cells)
random_empty_cell["resident"] = 2
return grid, cell_population
Benachbarte Zellen finden#
Die Funktion find_neighbor_cells()
dient dazu, alle benachbarten Zellen einer ausgewählten Zelle des Grids zu finden und dieser Zelle unter einem Attribut namens "neighbor_cells"
einzuspeichern. Das Grid wird dabei als Torus behandelt, sodass alle Agenten dieselbe Anzahl an Nachbarn aufweisen.
Die Funktion
find_neighbor_cells()
erwartet unter dem Argumentcell
die Zelle, deren Nachbarn gesucht werden sollen. Unter dem Argumentgrid
soll das Grid bzw. die Matrix von Zellen als geschachtelte Liste eingegeben werden.Zunächst wird die Anzahl der Zeilen und Spalten der Matrix
grid
ermittelt und untern_rows
undn_cols
gespeichert.Die Nachbarzellen einer bestimmten Zelle befinden sich jeweils eine Zeilenposition und/oder eine Spaltenposition rechts oder links bzw. oberhalb oder unterhalb von der Position der Zelle. Die Nachbarzelle oben links von einer Zelle findet findet man also z.B. indem man 1 von der Zeilenposition und 1 von der Spaltenposition der betrachteten Zelle abzieht. Die Position der Nachbarzelle rechts von einer Zelle findet man hingegen beispielsweise, wenn man in derselben Zeile bleibt und 1 auf die Spaltenposition addiert. Um alle 8 Nachbarpositionen durchzugehen, werden zunächst mittels zweier For-Loops alle möglichen Kombinationen der möglichen Positionsabweichungen
[-1, 0, 1]
für Zeile und Spalte erzeugt. Mittels If-Statement wird jedoch die Kombination 0 & 0 ausgeschlossen, weil dies keine Abweichung von der Position der betrachteten Zelle beinhaltet und sich somit die Zelle selbst als Nachbar finden würde.Aus den Positionsabweichungen werden dann in den Variablen
neighbor_row
undneighbor_col
die Zeilen- und Spaltenpositionen für eine Nachbarzelle zwischengespeichert. Die Zeilenposition der Nachbarzelle wird z.B. berechnet, indem der Zeilenpositioncell["row"]
die Zeilenabweichungrow_deviation
aufaddiert wird. Dieser berechnete Wert muss allerdings noch modulon_rows
genommen werden, um die Zeilenrandpositionen zu verknüpfen.Mithilfe der ermittelten Zeilen- und Spaltenpositionen
neighbor_row
undneighbor_col
wird mittels Listenindexierung auf die Nachbarzelle innerhalb der Matrixgrid
zugegriffen und der Variableneighbor_cell
zugewiesen. Diese wird anschließend der Liste von allen benachbarten Zellen, welche für jede Zelle untercell["neighbor_cells"]
zu finden ist, angehängt.
def find_neighbor_cells(cell, grid):
# Anzahl der Zeilen des Grids ermitteln
n_rows = len(grid)
# Anzahl der Spalten bzw. Zellen in einer Zeile des Grids anhand der ersten Zeile ermitteln
n_cols = len(grid[0])
# für jede Zweilenpositionsabweichung
for row_deviation in [-1, 0, 1]:
# für jede Spaltenpositionsabweichung
for col_deviation in [-1, 0, 1]:
# wenn nicht Zeilen- und Spaltenabweichung beide 0 sind
if not (row_deviation == 0 and col_deviation == 0):
# Zeilen- und Spaltenposition der Nachbarzelle berechnen
neighbor_row = (cell["row"] - row_deviation) % n_rows
neighbor_col = (cell["col"] - col_deviation) % n_cols
# Nachbarzelle im grid finden und der betrachteten Zelle in die Liste von Nachbarzellen einfügen
neighbor_cell = grid[neighbor_row][neighbor_col]
cell["neighbor_cells"].append(neighbor_cell)
Bewohnte und unbewohnte Zellen ausfindig machen#
An vielen Stellen in der Simulation ist es praktisch, gezielt auf alle bewohnten oder auf alle unbewohnten Zellen zuzugreifen. Die Funktion get_empty_and_occupied_cells()
geht alle Zellen der eingegebenen Liste cell_population
durch und prüft, ob die Zelle unbewohnt oder bewohnt ist, ordnet die Zellen dann entsprechend entweder der Liste empty_cells
oder der Liste occupied_cells
zu und gibt diese beiden Listen dann über das return-Statement aus.
def get_empty_and_occupied_cells(cell_population):
# Liste für alle unbewohnten Zellen
empty_cells = []
# Liste für alle bewohnten Zellen
occupied_cells = []
# Alle Zellen durchgehen und prüfen, ob Zelle unbewohnt. Dann entsprechender Liste anhängen.
for cell in cell_population:
if cell["resident"] == 0:
empty_cells.append(cell)
else:
occupied_cells.append(cell)
return empty_cells, occupied_cells
Die Nachbarschaft evaluieren#
Die Funktion eval_neighborhood()
berechnet für eine eingebene Zelle cell
den Anteil der Nachbarzellen, deren "resident"
aktuell zur selben Gruppe gehören. Dazu wird die Liste aller benachbarter Zellen der betrachteten Zelle durchgegangen und einerseits gezählt, wie viele Nachbarzellen generell bewohnt sind sowie andererseits, wie viele von den bewohnten Nachbarzellen Mitglieder derselben Gruppe beherbergen.
def eval_neighborhood(cell):
# Variable zur Zählung der aktuell bewohnten Nachbarzellen
n_neighbors = 0
# Variable zur Zählung der durch Mitglieder derselben Gruppe bewohnte Nachbarzellen
n_neighbors_of_same_group = 0
# für jede Nachbarzelle
for neighbor_cell in cell["neighbor_cells"]:
# wenn Zelle bewohnt, n_neighbors um 1 erhöhen
if neighbor_cell["resident"] != 0:
n_neighbors += 1
# wenn Zelle mit Mitglied derselben Gruppe bewohnt, dann n_neighbors_of_same_group um 1 erhöhen
if neighbor_cell["resident"] == cell["resident"]:
n_neighbors_of_same_group += 1
# Wenn es Nachbarn gibt
if n_neighbors > 0:
# Anteil derselben Gruppe unter Nachbarn berechnen und einspeichern
cell["share_of_same_group"] = n_neighbors_of_same_group / n_neighbors
# ansonten
else:
# "Missing" eintragen
cell["share_of_same_group"] = 999
Ein darstellbares Grid erstellen#
Um den aktuellen Zustand des Grids z.B. mit der Matplotlib-Funktion/Methode imshow()
grafisch darzustellen, muss das grid
in eine grafisch darstellbare Matrix überführt werden. Denn in grid
liegen zwar auf jeder Position Dictionaries, welche die Zellen-Informationen beinhalten, diese können aber von imshow()
nicht direkt in Farben übersetzt werden. Wir brauchen also eine Matrix, welche Farbinformationen auf ihren Zellen enthält. Im Segregationsmodell von Schelling wird in der Regel die Gruppenzugehörigkeit der auf den Zellen lebenden Agenten farblich unterschieden. Die untige Funktion create_color_matrix()
übersetzt daher das Grid mit allen Zelleninformationen in eine Matrix mit Farbinformationen, welche die Gruppenzugehörigkeit der bewohnenden Agenten widerspiegeln. Dazu geht die Funktion das eingegebene grid
durch, erstellt eine Matrix derselben Größe und befüllt die Positionen der Matrix je nach Gruppenzugehörigkeit des dort wohnenden Agenten mit einem bestimmten RGB-Farbcode.
def create_color_matrix(grid):
# Überliste für neue Matrix, in welcher Farbinfos gespeichert werden
color_matrix = []
# für jede Zeile des Grids
for row in grid:
# Unterliste für Zeile in Matrix
color_row = []
# für jede Zelle in der Zeile
for cell in row:
# Ist die Zelle unbewohnt?
if cell["resident"] == 0:
# RGB-Code für Farbe Schwarz anhängen
color_row.append([0, 0, 0])
# Wohnt hier ein Mitglied von Gruppe 1?
elif cell["resident"] == 1:
# RGB-Code für Farbe Rot anhängen
color_row.append([200, 0, 0])
# Wohnt hier ein Mitglied von Gruppe 2?
elif cell["resident"] == 2:
# RGB-Code für Farbe Blau anhängen
color_row.append([0, 0, 200])
# Zeile in Matrix einfügen
color_matrix.append(color_row)
return color_matrix
Das Modell ausführen#
Die Funktion run_model()
ist die Hauptfunktion des Modells, welche den primären Simulationsloop beinhaltet und alle oben beschriebenen Teilfunktionen ausführt.
Das Argument
threshold
erwartet einenfloat
von 0 bis 1 und steht für den Anteil an Mitgliedern derselben Gruppe, welchen jeder Agent mindestens in der Nachbarschaft haben möchte. Das Argumentticks
steht für die Anzahl der zu simulierenden Zeitschritte. Mithilfe des Argumentticks_per_snapshot
wird kontrolliert, wie viele Diagramme/Fotos während der Simulation erstellt werden sollen, welche später zur Animation zusammengesetzt werden.n_rows
,n_cols
,n_group1
,n_group2
geben die Zeilen- und Spaltenanzahl des Grids sowie die Anzahl der MItglieder von Gruppe 1 und 2 an.Vor dem Simulationsloop wird mithilfe der Funktion
create_grid()
das Grid erstellt, danach die benötigten Matplotlib-Objekte erstellt und eine Celluloid-Kamera auf das entsprechende Figure-Objekt ausgerichtet.Während des Simulationsloops werden zunächst alle aktuell bewohnten und unbewohnten Zellen mittels
get_empty_and_occupied_cells()
herausgesucht und in zwei Listen gespeichert. Von den bewohnten Zellen wird eine zufällig ausgesucht, welche ihre Nachbarschaft bezüglich der Gruppenzusammensetzung mittelseval_neighborhood()
evaluieren soll. Mittels einem daraufffolgendem If-Statement wird geprüft, ob die Nachbarschaft den Ansprüchen des Agenten genügt. Wenn nicht, dann wird eine zufällige neue Zelle herausgesucht, auf welche der Agent umzieht. Im letzten Teil des Simulationsloops wird schließlich noch die Matrix grafisch dargestellt und ein Foto von diesem Diagramm geschossen. Allerdings passiert dies nicht in jedem Zeitschritt, sondern nur alleticks_per_snapshot
Zeitschritte, was mithilfe des Modulo gesteuert wird.Nach dem Simulationsloop wird schließlich noch die Animation erstellt und auf der Festplatte gespeichert. Zusätzlich wird das Animationsobjekt noch mittels
return
ausgegeben, um die Animation z.B. hier in diesem Jupyter Notebook direkt weiterverwenden zu können. Außerdem wird das Diagramm, so wie es zum Schluss der Simulation aussieht, mitplt.show()
angezeigt, um den Endzustand der Simulation zu sehen.
def run_model(threshold, ticks, ticks_per_snapshot, n_rows=100, n_cols=100, n_group1=4000, n_group2=4000):
# Grid erstellen
grid, cell_population = create_grid(n_rows, n_cols, n_group1, n_group2)
# Matplotlib-Objekte erstellen, auf die während Simulation gezeichnet wird
leinwand, diagramm = plt.subplots()
# Celluloid-Kamera-Objekt erstellen, das während Simulation Fotos von Diagrammen macht
camera = cld.Camera(leinwand)
###############################################
# Simulationsloop
###############################################
# für jeden Zeitschritt
for tick in range(ticks):
# zwei Listen mit allen leeren und bewohnten Zellen erstellen
empty_cells, occupied_cells = get_empty_and_occupied_cells(cell_population)
# eine zufällige Zelle aussuchen
random_cell = random.choice(occupied_cells)
# ausgesuchte Zelle schaut, wie hoch der Anteil der eigenen Gruppe in Nachbarschaft ist
eval_neighborhood(random_cell)
# Wenn in Nachbarschaft zu wenige aus derselben Gruppe wie Agent sind
if random_cell["share_of_same_group"] < threshold:
# zufällige neue, unbewohnte Zelle aussuchen
new_cell = random.choice(empty_cells)
# der neuen Zelle den Agenten zuweisen
new_cell["resident"] = random_cell["resident"]
# den Agenten bei alter Zelle "löschen"
random_cell["resident"] = 0
# Alle X Zeitschritte
if tick % ticks_per_snapshot == 0:
# eine darstellbare Matrix erstellen
color_matrix = create_color_matrix(grid)
# Diagramm erstellen
diagramm.imshow(color_matrix)
# Foto von Diagramm schießen
camera.snap()
###############################################
# Nach Simulationsloop
###############################################
# Animation erstellen & speichern
animation = camera.animate()
animation.save("schelling.mp4", dpi=300, fps=10)
# Endzustand des Diagramms anzeigen
plt.show()
return animation
Schließlich führe ich das Modell durch den Aufruf der Funktion rund_model()
aus.
animation = run_model(threshold=0.5, ticks=100000, ticks_per_snapshot=1000)
from IPython.display import HTML
HTML(animation.to_jshtml())
(Übrigens kann man in dem obigen Player die Abspielgeschwindigkeit durch das Drücken auf + und - verändern.)
Aufgabe 1#
Lies dir den Code und die zugehörige Beschreibung durch und versuche alles so gut es geht nachzuvollziehen.
Installiere die Pakete Celluloid und ffmpeg.
Kopiere den obigen Code des Schelling-Modells in ein Python-Skript und führe diesen aus. Funktioniert es bei dir?
Führe die Funktion
run_model()
mit verschiedenen Werten fürthreshold
aus. Welchen Einfluss hat der Parameterthreshold
auf den Verlauf der Simulation?
Aufgabe 2#
Überlege dir eine nützliche Modifikation des Codes, setze diese um und beschreibe ganz kurz, warum du diese Modifikation für sinnvoll erachtest. Mögliche Modifikation des Codes könnten z.B. sein:
Man könnte eine Möglichkeit, die Besiedelungsdichte oder die Anzahl der Mitglieder von Gruppe 1 und 2 als Anteilswert angeben zu können, einbauen. Dadurch müsste man beim Ausführen der Simulation nicht vorher selbst berechnen, wie viele Agenten ich aus den jeweiligen Gruppen brauche, um das Grid z.B. zu 90% zu besiedeln und 10% der Zellen unbewohnt zu lassen.
Man könnte noch eine dritte oder vierte Gruppe das Grid besiedeln lassen (extrem fortgeschritten wäre es, wenn man eine Möglichkeit einbaut, wie man per Parameter die Anzahl der Gruppen bestimmen könnte)
Man könnte die Farben der Agenten ändern.
…
Aufgabe 3#
Im nächste Kapitel wird es darum gehen, ein Simulationsexperiment auf Basis des Schelling-Modells durchzuführen.
Überlege dir daher, welche Eigenschaften/Dynamiken des Modells systematisch innerhalb eines Simulationsexperimentes ausgewertet werden könnten. Denke also darüber nach, welche Eigenschaften des Schelling-Modells man als unabhängige Variable innerhalb eines Simulationsexperimentes variieren könnte, um den Effekt dieser unabhängigen Variable auf eine abhängige Variable zu messen.
Überlege dir, wie man die von dir gewählten abhängige und unabhängige Variable im Modell messen könnte, was man grob am Code verändern müsste und wie ein Simulationsexperiment aufgebaut sein müsste, um den Effekt der abhängigen auf die unabhängige Variable erfassen zu können.