Zum Inhalt

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:

pip install dsstools
pip install networkx

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:

import networkx as nx
import dsstools as dts

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:

graph = nx.Graph()

Diesen Graphen füllen wir jetzt mit ein paar Nodes:

graph.add_node("A")
graph.add_node("B")
graph.add_node("C")
graph.add_node("D")

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:

graph.add_edge("A", "B")
graph.add_edge("B", "C")
graph.add_edge("C", "D")
graph.add_edge("D", "A")

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:

  1. NetworkX: Einfach und bereits installiert.
  2. 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:

uebung-einstieg
├── main.py
└── positions.json

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:

positions = layouter.read_or_create_layout(f"positions.json", graph, seed=i, overwrite=True)

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

position_list = []
for i in range(0,20):
    positions = layouter.read_or_create_layout(f"positions_{i}.json", graph, seed=i)
    position_list.append(positions)

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:

image_generator = dts.ImageGenerator(graph)

Darüber hinaus müssen wir als Erstes die Positionierungen festlegen:

image_generator.nodes.set_positions(positions)

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("grey")

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:

image_generator.nodes.set_colors(dts.qualitative("Unser Attribute", cmap="Set2"))

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:

image_generator.nodes.set_sizes(dts.sequential("degree", out_range=(100, 500)))

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:

image_generator.draw().write_file("./abbildung.svg")

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:

uebung-einstieg
├── abbildung.svg
├── main.py
└── positions.json

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:

uebung-import
└── main.py

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

uebung-import
├── example_graph.gexf
└── main.py

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:

graph = nx.read_gexf("./example_graph.gexf")

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
# Wir zeichnen und schreiben die Datei. Zeichnen und Schreiben finden getrennt statt, da
# es möglich ist, eine Abbildung in mehreren Ebenen zu komponieren.
image_generator.draw().write_file("./abbildung.svg")

Die ganze mögliche Lösung kannst du hier herunterladen.