Tutorial für Einsteiger*innen (ausführlich)
In diesem Beispiel werden wir die grundlegenden Funktionen von
dssTools
kennen lernen und einmal die grobe Suite durchlaufen.
Grundsätzlich untergliedert sich das Tutorial in vier Schritte:
- Vorbereitung (Installation und Tipps)
- Einlesen (Erstellung eines eigenen Graphen)
- Verarbeitung (Festlegung von Positionen)
- Produkt (Erzeugung von unterschiedlichen Abbildungen)
Die Übergabe zwischen den Schritten findet immer mit einem nx.Graph
(oder
nx.DiGraph
, falls gerichtet) statt. Veränderungen werde über die Graph-,
Node- und Edgeattribute geschrieben und ausgelesen.
Hinweis
Falls das etwas kompliziert klingen sollte: Das ist nicht weiter schlimm, in der Nutzung hat man mit diesen Prinzipien wenig zu tun.
Fangen wir also in einem sauberen Zustand mit den Vorbereitungen an:
0. Vorbereitung
Zuerst musst du dssTools
und NetworkX
installieren. Für die Verwaltung von
Packages empfiehlt sich pip
. Für die Erläuterung der Installation
mit pip
hier klicken.
Das funktioniert über deine IDE (vermutlich PyCharm) oder dein Terminal so:
Grundsätzlich empfiehlt es
sich, einen eigenen Ordner uebung-einstieg
für das Projekt anzulegen, den wir in der
Folge mit Dateien und weiteren Ordner füllen werden. Im weiteren wird
dieser Ordner als Wurzelordner bezeichnet.
Hinweis
Es kann sich lohnen, den eigenen Fortschritt über ein Git-Repository zu tracken. Sollte das gewünscht sein, dann wäre das jetzt der richtige Zeitpunkt dafür.
Wenn alles geklappt hat, kannst du in dem Wurzelordner eine Python-Datei
mit Namen main.py
anlegen. Füge Folgendes oben in die Datei ein:
Wir importieren sowohl networkx
als auch dsstools
für diese Datei, um sie hier nutzen zu können. Beide Packages erhalten ein sogenanntes Alias. Das Package networkx
wird im Import innerhalb Python immer als networkx
geschrieben und für gewöhnlich mit einem Alias nx
versehen, um die Schreibweise abzukürzen (analog dsstools
). Um nun zum Beispiel auf die Klasse Graph
aus networkx
zuzugreifen, kann man statt networkx.Graph
jetzt immer nx.Graph
schreiben. Aliases sparen also einfach Schreibarbeit.
Führe diese Datei aus. Es sollten keine Fehler auftreten. Falls Fehler
wie ImportError
auftreten, bitte noch einmal die Installation der
einzelnen Packages überprüfen.
Falls keine Probleme auftreten, kannst du zum nächsten Schritt übergehen.
1. Einlesen: Erstellung eines eigenen Graphen
- Einfachen Graphen selbst erstellen
- Sollte auch Attribute erhalten, die wir später im Zeichnen nutzen können
Um die nächsten Schritte besser nachvollziehen zu können, wollen wir hier einmal damit einsteigen, wie ein network-Graph
überhaupt erstellt wird. Im folgend füllen wir als die main.py
Stück für die Stück mit den folgenden Codeschnipseln.
Am Anfang das Graph-Objekt. Mit dem nachfolgend Code erzeugst du erstmal einen 'leeren' Graphen:
Diesen Graphen füllen wir jetzt mit ein paar Nodes:
Hinweis
Dieser Code ist gut für das Verständnis, nicht aber unbedingt best practice. Normalerweise könnte man hierfür die
Methode graph.add_nodes_from(["A", "B", "C", "D"])
auf eine Liste an Knoten-Namen aufrufen.
In einem nächsten Schritt stellen wir die Edges zwischen den Nodes her:
Hinweis
Auch hier gäbe es wieder einen best practice Weg, bei dem die Nodes und Edges direkt als eines hinzugefügt werden
über graph.add_edges_from([("A", "B"), ("B", "C"), ("C", "D"), ("D", "A")])
, jedoch wollen wir hier die Schritte
einzeln zeigen."
Bisher haben wir jetzt Nodes, die über die gerade definierten Edges verbunden sind. In einem richtigen Netzwerk haben
Nodes allerdings oftmals noch Attribute, die für die Analyse wichtig sind. Im Fall einer semantischen Netzwerkanalyse
könnte man zum Beispiel jedes Wort als Node vorliegen haben und dann das Attribut Organisation
, da effektive
ausdrückt, welchen Akteur:innen dieses Wort zuzuordnen ist.
Ein Weg, Attribute hinzuzufügen, sieht so aus:
graph.nodes["A"]["Unser Attribute"] = "Wert 1"
graph.nodes["B"]["Unser Attribute"] = "Wert 2"
graph.nodes["C"]["Unser Attribute"] = "Wert 3"
graph.nodes["D"]["Unser Attribute"] = "Wert 1" # Bewusst gleich zu Node A (siehe unten)
Hinweis
In den meisten reellen Beispielen haben Nodes oftmals dieselben Attribute, um sie vergleichbar zu machen. In dem Beispiel von oben hätte zum Beispiel jedes Wort das Attribut Organisation, einfach deshalb, weil sie immer einer Organisation zuzuordnen wären. Der Wert wiederrum kann durchaus unterschiedlich sein.
Nun haben wir unseren ersten sehr simplen Graphen erstellt. Mit Hilfe der print
-Funktion können wir uns nun schon
einiges über den Graphen anschauen. Das soll dann nach all diesen Schritten so aussehen:
print(graph) # --> "Graph with 4 nodes and 4 edges"
print(graph.nodes) # --> ['A', 'B', 'C', 'D']
print(graph.edges) # --> [('A', 'B'), ('A', 'D'), ('B', 'C'), ('C', 'D')]
Mit diesem graph
-Objekt können wir nun die nächsten Schritte beginnen.
2. Verarbeitung: Positionen festlegen
Hier legen wir die Positionen der Nodes im Graphen fest. Dafür gibt es zwei Möglichkeiten:
NetworkX
: Einfach und bereits installiert.Graphviz
: Fortgeschritten und benötigt unter Windows eine komplizierte Installation. Sie liefert allerdings bessere Positionierungen.
In diesem Beispiel werden wird dementsprechend die Positionierung mit
NetworkX
verwenden. Das funktioniert wie folgt:
Hinweis
In dem Beispielcode steht der Name position.json
. Dabei ist .json
einfach eine Dateiendung wie zum Beispiel .pdf
oder auch .docx
(für
Worddateien). Hier werden nur eben anstatt reinem Text verschiedene
Informationen über einen Graph gespeichert, die für Computer so einfacher
strukturiert sind. Dieses Format ist ähnlich zum einem Dictionary in Python
und wird daher gerne und häufig verwendet.
layouter = dts.Layouter()
positions = layouter.read_or_create_layout("positions.json", graph, seed=1234, k=1)
Zuerst wird ein Layouter-Objekt erstellt und als layouter
bezeichnet. Dieser Layouter legt die Positionierung der Knoten im Graphen fest und stellt uns dafür Einstellungsmöglichkeiten zur Verfügung. In der nächsten Zeile wird
mittels der Objektmethode layouter.read_or_create_layout()
ein Layout erzeugt (Layout entspricht dabei der Positionierung).
Dieses erhält vier Argumente:
- Den Pfad, in dem die Positionsdatei geschrieben wird,
- den Graphen selbst und
- einen sog. Seed. Der Seed bestimmt dabei die "Zufälligkeit" der Erzeugung.
k
ist für das genutzte Layout relevant und bestimmt die optimale Distanz zwischen Knoten. Falls Knoten zu eng beeinander liegen, kann eine Erhöhung dieses Wertes zu besseren Abbildungen führen.
Zudem gibt layouter.read_or_create_layout()
ein Dictionary aus, was wir zudem der Variable positions
zuweisen. Nach Ausführung dieses Codeschnipsels sollte in deinem Verzeichnis eine positions.json
neben deiner main.py
liegen:
Warnung
read_or_create_layout()
liest ein bestehendes Layout ein, wenn es unter
dem Pfad eine bestehende Datei findet. Ansonsten schreibt es diese neu. Sollen
immer neue Abbildungen erzeugt werden und die bestehenden Dateien
überschrieben werden, so muss das Argument overwrite=True
übergeben werden:
Hinweis
Indem der Seed mittels eines Loops iteriert wird, lassen sich unterschiedliche Positionierungen miteinander vergleichen. Bspw. könnte das so aussehen, um 20 unterschiedliche Abbildungen zu erzeugen (Danach sollten 20 neue Dateien in deinem Verzeichnis liegen):
3. Produkt: Erzeugung von Abbildungen
Jetzt kommen wir auch schon zum Zeichnen. Dazu erzeugen wir zuerst einen
ImageGenerator. Das ist ein Objekt aus dsstools
, in dem alle
Einstellungen für die jeweilige Abbildung eingestellt werden. Zuerst wird
es mittels des Graphen initialisiert:
Darüber hinaus müssen wir als Erstes die Positionierungen festlegen:
Hinweis
Im Setzen der Positionen mittels image_generator.nodes.set_positions()
kann entweder das Dict
oder der Pfad zur Positionsdatei übergeben werden. Die Datei wird
deshalb erstellt, damit innerhalb von Teams die Position festgelegt und
über Git getrackt werden kann.
Nun können wir einzelne Einstellungen auswählen. Bspw. könnten wir alle Kanten grau einfärben:
image_generator.edges.set_colors()
erhält dabei ein Argument in Form von
"grey"
. Das bedeutet, dass für alle Kanten der Wert
"grey"
als Farbe angewendet wird.
Für die Einfärbung von Knoten entlang eines vorhandenen, codierten Attributes:
Hinweis
Hier greifen wir auf das Attribut des Knoten zu, welches wir oben so erstellt haben. Natürlich können auch eigene Attribute an die Netzwerkelemente angehängt und dargestellt werden.
Die Größe der Knoten möchten wir entlang des Degree skalieren (Degree ist dabei ein Stichwort, welches die Berechnung des Degrees intern anstößt). Die outrange
definiert wird groß der kleinste und größte Knoten maximal sein können:
Hinweis
Es funktionieren ebenfalls indegree
, outdegree
, betweenness
, closeness
als automatisch zu berechnende Stichwörter.
Zuletzt wird die Abbildung gezeichnet und dann in eine Datei geschrieben:
Hinweis
Das Gleiche lässt sich auch in einem Aufwasch schreiben: Um mehrzeiligen Code in Python zu verfassen, müssen die Zeilen immer mit einer Klammer eingefasst sein.
image_generator = dts.ImageGenerator(graph)
(
image_generator.nodes.set_positions(positions)
.set_colors(dts.qualitative("Unser Attribute", cmap="Set2"))
.set_sizes(dts.sequential("degree", out_range=(100, 500)))
)
image_generator.edges.set_colors("grey")
image_generator.draw().write_file("./abbildung.svg")
Damit sollte sich im Wurzelordner eine neue Datei mit Namen abbildung.svg
finden, die das gezeichnete Netzwerk enthält. Die gesamte Python-Datei lässt sich
hier herunterladen.
Der Wurzelordner sieht jetzt so aus:
4. Anwendungsbeispiel: Ein Workflow
Oft erstellt man Graphen nicht selbst, sondern nutzt bereits vorhandene Daten. Um das einmal nachvollziehen, erstellt einen neuen Ordner uebung-import
und darin wieder eine main.py
mit den bekannten Imports (s.o.). Bekanntes Bild:
Einlesen eines bestehenden Graphen
Für das Einlesen bieten sich mehrere Optionen an, sowohl in dssTools
als auch in
NetworkX
. Da es sich hier um ein einfaches Beispiel handelt, wollen wir ein
vorgefertigtes Netzwerk nutzen. Lade dafür diese
Datei herunter und
speichere sie im Wurzelordner.
Der Wurzelordner uebung-import
sollte jetzt so aussehen (dein Skript & die neue Datei):
Jetzt lässt sich diese Datei in Python einlesen. Da wir vorerst keine
Sonderfunktionen benötigen, nutzen wir die
Standardfunktion
für diesen Zweck in NetworkX
:
Weitere Verarbeitung
Jetzt muss auch hier wiederum eine Positions-Datei wie oben erstellt werden. Dabei kannst du wie bisher vorgehen. Eine Möglichkeit ist in der nächsten Box aufgeführt. Probiere aber gern zuerst einmal selbst aus!
Mögliche Lösung
import networkx as nx
import dsstools as dts
layouter = dts.Layouter()
positions = layouter.read_or_create_layout("positions.json", graph, seed=1234, k=1)
# Erzeuge ein ImageGenerator-Objekt. Ein ImageGenerator erzeugt mit einem Graph und
# verschieden Einstellungen eine (!) Abbildung.
image_generator = dts.ImageGenerator(graph)
# Setze die Positionen im ImageGenerator.
image_generator.nodes.set_positions(positions)
Jetzt wollen wir als Nächstes Farben und Größen setzen. Überlege dir eine Kombination und führe sie aus.
Mögliche Lösung
# Lege die Farben für die Kanten fest.
image_generator.edges.set_colors("grey")
# Lege die Farben für die Knoten fest. Wir nutzen ein qualitatives Mapping. Das
# bedeutet, dass mehrere Kategorien unterschiedliche eingefärbt werden.
image_generator.nodes.set_colors(dts.qualitative("stage_of_sf", cmap="Set2"))
# Lege die Knotengröße fest. Wir nutzen ein sequentielles Mapping. Das bedeutet, dass
# kontinuierliche Werte auf eine von uns gewählte Skala übertragen werden (`out_range`).
# In diesem Fall würde also das geringste Degree den Knotenradius 5, das höchste Degree
# im Netzwerk den Knotenradius 500 bedeuten.
image_generator.nodes.set_sizes(dts.sequential("degree", out_range=(5, 500)))
Zuletzt wollen wir noch die Datei an einen beliebigen Ort schreiben. Überlege odr sieh noch einmal nach, wie das funktioniert!
Mögliche Lösung
Die ganze mögliche Lösung kannst du hier herunterladen.