Inhalt des Skriptums seit dem Sommersemester 2025
- Startseite
- Allgemeine Informationen zur Lehrveranstaltung
- Einfaches Python Setup und Wichtige Informationen
- 01 Einführung in algorithmisches Denken & LLMs
- 02 Algorithmenanalyse und Komplexitätstheorie
- 03 Vektoren, Matrizen und Vektorisierung in Python
- 04 Differenzieren und Integrieren in Python: Analytische und Numerische Methoden
- 05 Arbeiten mit Daten: Vorbereitung, Visualisierung, Analyse
- 06 Graphalgorithmen: Darstellungen, Traversierungen, Kürzeste Wege und Spannbäume
Hier finden Sie die Kapitel aus den Vergangenen Jahren (legacy):
- 0. Python Einführung und Checkup
- 1. Einführung und einfache Algorithmen
- 2. Differenzieren und Integrieren
- 3. Vektoren, Matrizen und Vektorisierung in Python
- 4. Datenanalyse bzw. Datenauswertung
- 5. Grundlagen der Optimierung und Gradient Descent
- 6. Stochastische Optimierung und Genetische Algorithmen
- 7. Monte-Carlo-Methoden – Simulation und Integration
- 8. Monte-Carlo-Methoden, Teil 2 – Monte-Carlo-Integration, Teil 2 und Random Walk
- 9. Unsupervised Machine Learning: Clustering von Daten
- 10. Supervised Machine Learning: Grundlagen
- 11. Einführung in künstliche neuronale Netzwerke
Die Jupyter-Notebooks zur Lehrveranstaltung finden Sie im zugehörigen GitHub-Repository.
6 Graphalgorithmen: Darstellungen, Traversierungen, Kürzeste Wege und Spannbäume¶
Der Einsatz von Graphen in unserer modernen Welt ist in vielen Bereichen gegenwärtig. Besonders nützlich sind sie bei der mathematischen Formulierungen von Zusammenhängen in Gruppen von Individuen. Das können zwar auch Menschen sein, aber Graphen lassen sich viel allgemeiner und vor allem abstrakter anwenden. Daher ist ihnen diese Einheit gewidmet. Code-Beispiele kommen wie gewohnt von Claude 3.7 Sonnet.
Nach dem Studium dieser Einheit sollten Sie zumindest folgende drei der wichtigsten Punkte hier verstehen:¶
Was Graphen sind und welche Eigenschaften sie characterisieren.
Wie Algorithmen aufgebaut sind, die Graphen beschreiben und analysieren können.
Welcherart Probleme man überhaupt lösen muss, wenn man sich mit Graphen beschäftigt, bzw. Graphen in Wissenschaft und Forschung einsetzt.
6.1 Ultra-Kurzer Crash-Kurs zum Thema Graphen(theorie)¶
In diesem Abschnitt werden wir zunächst die grundlegenden Konzepte der Graphentheorie kennenlernen. Graphen sind eine der mächtigsten und vielseitigsten Datenstrukturen in der Informatik und Mathematik. Sie eignen sich hervorragend zur Modellierung von Beziehungen zwischen Objekten und finden Anwendung in zahlreichen Bereichen wie sozialen Netzwerken, Verkehrsplanung, Molekularbiologie, Computernetzwerken und vielen weiteren Gebieten.
6.1.1 Grundlegende Begriffe und Definitionen, Arten von Graphen¶
Ein Graph $G = (V, E)$ besteht aus einer Menge von Knoten (oder Vertices) $V$ und einer Menge von Kanten (oder Edges) $E$, die Verbindungen zwischen diesen bzw. mancher dieser Knoten darstellen.
Bildquelle: https://upload.wikimedia.org/wikipedia/commons/5/5b/6n-graf.svg
Je nach Art der Kanten und weiteren Eigenschaften unterscheiden wir verschiedene Arten von Graphen:
Gerichtete vs. ungerichtete Graphen:
- In ungerichteten Graphen haben Kanten keine Richtung; eine Kante $(u,v)$ ist identisch mit $(v,u)$.
- In gerichteten Graphen (auch Digraphen genannt) haben Kanten eine bestimmte Richtung; eine Kante $(u,v)$ bedeutet eine Verbindung von $u$ nach $v$, aber nicht notwendigerweise umgekehrt.
Bildquellen: https://upload.wikimedia.org/wikipedia/commons/thumb/a/a2/Directed.svg/240px-Directed.svg.png, https://upload.wikimedia.org/wikipedia/commons/thumb/b/bf/Undirected.svg/240px-Undirected.svg.png
Gewichtete vs. ungewichtete Graphen:
- In ungewichteten Graphen haben alle Kanten den gleichen Wert oder die gleiche Wichtigkeit.
- In gewichteten Graphen wird jeder Kante ein Wert (Gewicht) zugeordnet, der Kosten, Distanz, Kapazität oder eine andere Metrik darstellen kann.
Bildquelle: https://upload.wikimedia.org/wikipedia/commons/thumb/5/58/Weighted_Graph.svg/320px-Weighted_Graph.svg.png
Zyklische vs. azyklische Graphen:
- Ein zyklischer Graph enthält mindestens einen Zyklus, d.h. einen Pfad, der bei einem Knoten beginnt und beim selben Knoten endet.
- Ein azyklischer Graph enthält keine Zyklen. Ein gerichteter azyklischer Graph (DAG) wird oft zur Darstellung von Abhängigkeiten verwendet.
Bildquelle: https://upload.wikimedia.org/wikipedia/commons/0/03/Directed_acyclic_graph_2.svg
Zusammenhängende vs. nicht zusammenhängende Graphen:
- Ein zusammenhängender Graph ist ein Graph, in dem von jedem Knoten ein Pfad zu jedem anderen Knoten existiert.
- Ein nicht zusammenhängender Graph besteht aus mehreren Komponenten ohne Verbindung untereinander.
Bipartite Graphen:
- Ein bipartiter Graph ist ein Graph, dessen Knoten in zwei disjunkte Mengen aufgeteilt werden können, sodass jede Kante einen Knoten aus der ersten Menge mit einem Knoten aus der zweiten Menge verbindet.
Planare Graphen:
- Ein planarer Graph kann in der Ebene gezeichnet werden, ohne dass sich Kanten kreuzen.
Bildquelle: https://upload.wikimedia.org/wikipedia/commons/thumb/8/83/Gem_graph.svg/320px-Gem_graph.svg.png
6.1.2 Grundlegende Grapheigenschaften und -operationen¶
Grad eines Knotens:
- Der Grad eines Knotens in einem ungerichteten Graphen ist die Anzahl der mit ihm verbundenen Kanten.
- In einem gerichteten Graphen unterscheiden wir zwischen Eingangsgrad (Anzahl der eingehenden Kanten) und Ausgangsgrad (Anzahl der ausgehenden Kanten).
In unserem Beispiel von oben hat z.B. Knoten 1 den Grad 2, Knoten 2 den Grad 3, Knoten 3 den Grad 2, usw.
Pfade und Zyklen:
- Ein Pfad ist eine Folge von Knoten, wobei aufeinanderfolgende Knoten durch Kanten verbunden sind.
- Ein einfacher Pfad enthält keine wiederholten Knoten.
- Ein Zyklus ist ein Pfad, der an seinem Ausgangspunkt endet.
Zusammenhang:
- Eine Zusammenhangskomponente ist ein maximaler zusammenhängender Teilgraph.
- Ein stark zusammenhängender gerichteter Graph ist ein Graph, in dem von jedem Knoten ein gerichteter Pfad zu jedem anderen Knoten existiert.
Operationen auf Graphen:
- Einfügen eines Knotens: Hinzufügen eines neuen Knotens zum Graphen.
- Entfernen eines Knotens: Entfernen eines Knotens und aller mit ihm verbundenen Kanten.
- Einfügen einer Kante: Hinzufügen einer neuen Verbindung zwischen zwei Knoten.
- Entfernen einer Kante: Entfernen einer bestehenden Verbindung zwischen zwei Knoten.
6.1.3 Außer Konkurrenz: Die Euler’sche Formel für planare Graphen in verschiedenen Topologien¶
Die Euler’sche Formel ist eine der schönsten und grundlegendsten Beziehungen in der Graphentheorie. Für einen zusammenhängenden planaren Graphen mit $V$ Knoten, $E$ Kanten und $F$ Flächen (einschließlich der äußeren Fläche) gilt:
$$V – E + F = 2$$
Diese Formel lässt sich auf verschiedene topologische Oberflächen verallgemeinern. Für eine Oberfläche vom Geschlecht $g$ (d.h. mit $g$ “Löchern”) gilt:
$$V – E + F = 2 – 2g$$
Zum Beispiel:
- Für die Ebene (Geschlecht 0): $V – E + F = 2$
- Für einen Torus (Geschlecht 1): $V – E + F = 0$
Die Formel hat tiefgreifende Konsequenzen, wie z.B. die Tatsache, dass es nur fünf regelmäßige Polyeder (platonische Körper) gibt (das kann man sich über die Eigeschaft der Regelmäßigkeit und der Anzahl der Flächen des Polyeders überlegen). Sie bildet auch die Grundlage für viele Sätze in der Graphentheorie, wie etwa, dass jeder planare Graph mit $n \geq 3$ Knoten höchstens $3n – 6$ Kanten haben kann.
6.2 Implementation von Graphen in Python¶
In diesem Abschnitt werden wir uns damit beschäftigen, wie Graphen in Python implementiert werden können. Die richtige Datenstruktur ist entscheidend für die Effizienz der Algorithmen, die wir später auf den Graphen anwenden möchten.
6.2.1 Darstellung von Graphen in Computercode¶
Es gibt verschiedene Möglichkeiten, Graphen in Computercode zu repräsentieren. Jede hat ihre Vor- und Nachteile in Bezug auf Speicherplatz und Zugriffszeit.
Adjazenzmatrix¶
Eine Adjazenzmatrix ist eine $n \times n$-Matrix (wobei $n$ die Anzahl der Knoten ist), in der der Eintrag an Position $(i,j)$ angibt, ob eine Kante zwischen den Knoten $i$ und $j$ existiert. Bei gerichteten Graphen steht der Eintrag nur an der Stelle, welche die korrekte Richtung angibt, also z.B. von Knoten $i$ nach $j$, aber nicht von $j$ nach $i$. Dadurch ist die Adjazenzmatrix für einen gerichteten Graphen nicht symmetrisch. Die Einträge sind:
- Bei ungewichteten Graphen: 1 für vorhandene Kante, 0 sonst
- Bei gewichteten Graphen: Gewicht für vorhandene Kante, 0 oder ∞ sonst
Hier ein Beispiel für eine Adjazenzmatrix eines ungewichteten ungerichteten Graphen:
A B C D
A [0 1 1 0]
B [1 0 1 1]
C [1 1 0 0]
D [0 1 0 0]
Vorteile:
- Schneller Zugriff (O(1)) zum Prüfen, ob eine Kante existiert
- Einfach zu implementieren
- Gut für dichte Graphen (viele Kanten)
Nachteile:
- Speicheraufwand von O(n²), ineffizient für große, dünn besetzte Graphen
- Aufwendiges Auflisten aller Nachbarn eines Knotens (O(n))
Adjazenzliste¶
Eine Adjazenzliste speichert für jeden Knoten eine Liste seiner Nachbarn. Hier ein einfaches Beispiel:
A -> [B, C]
B -> [A, C, D]
C -> [A, B]
D -> [B]
Vorteile:
- Speichereffizient für dünn besetzte Graphen (O(|V|+|E|))
- Schnelles (de facto unmittelbares) Auflisten der Nachbarn eines Knotens
- Gut für Traversierungsalgorithmen (siehe etwas weiter unten)
Nachteile:
- Langsamer beim Prüfen, ob eine bestimmte Kante existiert (O(d), wobei d der Grad des Knotens ist)
- Komplexere Implementierung als bei der Adjazenzmatrix
Inzidenzmatrix¶
Eine Inzidenzmatrix ist eine $n \times m$-Matrix (wobei $n$ die Anzahl der Knoten und $m$ die Anzahl der Kanten ist), in der der Eintrag an Position $(i,j)$ angibt, ob der Knoten $i$ mit der Kante $j$ verbunden ist.
- Bei ungerichteten Graphen: 1 wenn verbunden, 0 sonst
- Bei gerichteten Graphen: -1 für ausgehende Kante, 1 für eingehende Kante, 0 sonst
Hier wieder ein einfaches Beispiel für einen ungerichteten Graphen:
E1 E2 E3
V1 [0 1 0]
V2 [1 1 1]
V3 [0 0 1]
V4 [1 0 0]
Vorteile:
- Gut für Analysen auf Kanten-Ebene
- Besonders nützlich bei Mehrgraphen (mehrere Kanten zwischen denselben Knoten)
Nachteile:
- Sehr speicherintensiv für große Graphen
- Ineffizient für die meisten Graphoperationen
Implementierung in Python (Grundlegende Datenstrukturen)¶
Hier ein einfaches Beispiel für die Implementierung eines ungerichteten, ungewichteten Graphen als Adjazenzliste in Python:
# Einfache Implementierung einer Adjazenzliste
graph = {
'A': ['B', 'C'],
'B': ['A', 'C', 'D'],
'C': ['A', 'B'],
'D': ['B']
}
# Funktion zum Hinzufügen eines Knotens
def add_node(graph, node):
if node not in graph:
graph[node] = []
# Funktion zum Hinzufügen einer Kante (ungerichtet)
def add_edge(graph, node1, node2):
if node1 in graph and node2 in graph:
if node2 not in graph[node1]:
graph[node1].append(node2)
if node1 not in graph[node2]:
graph[node2].append(node1)
Und hier eine Implementierung als Adjazenzmatrix:
import numpy as np
# Einfache Implementierung einer Adjazenzmatrix
nodes = ['A', 'B', 'C', 'D']
n = len(nodes)
# Erstellen einer n x n Matrix mit Nullen
matrix = np.zeros((n, n), dtype=int)
# Indizierung der Knoten
node_indices = {nodes[i]: i for i in range(n)}
# Hinzufügen von Kanten
def add_edge_matrix(matrix, node1, node2, node_indices):
i = node_indices[node1]
j = node_indices[node2]
matrix[i][j] = 1
matrix[j][i] = 1 # Für ungerichtete Graphen
# Beispielkanten hinzufügen
add_edge_matrix(matrix, 'A', 'B', node_indices)
add_edge_matrix(matrix, 'A', 'C', node_indices)
add_edge_matrix(matrix, 'B', 'C', node_indices)
add_edge_matrix(matrix, 'B', 'D', node_indices)
print("Adjazenzmatrix:")
print(matrix)
Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions. Intel MKL WARNING: Support of Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) enabled only processors has been deprecated. Intel oneAPI Math Kernel Library 2025.0 will require Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions. Adjazenzmatrix: [[0 1 1 0] [1 0 1 1] [1 1 0 0] [0 1 0 0]]
6.2.2 Visualisierung von Graphen: Furball vs. Hiveplot¶
Die Visualisierung von Graphen ist ein wichtiges Werkzeug, um Strukturen und Muster zu erkennen. Es gibt verschiedene Ansätze, von denen einige für bestimmte Arten von Daten besser geeignet sind als andere.
“Furball”-Visualisierungen¶
Die traditionelle “Furball” (Haarball)-Visualisierung versucht, Knoten und Kanten in einer ästhetisch ansprechenden Weise darzustellen, oft mit Hilfe von kräftebasierten Algorithmen, die Knoten wie physikalische Teilchen behandeln.
Vorteile:
- Intuitiv zu interpretieren
- Kann visuelle Muster offenbaren
- Gut für kleinere Graphen
Nachteile:
- Wird schnell unübersichtlich bei großen oder dichten Graphen
- Layout-Algorithmen können instabil sein
- Kann zu visuellen Fehlinterpretationen führen
Hiveplots¶
Hiveplots sind eine alternative Visualisierungsmethode, die versucht, die Unordnung von Furball-Darstellungen zu vermeiden, indem sie Knoten entlang von Achsen anordnet, basierend auf Netzwerkeigenschaften wie Grad oder Clustering-Koeffizient. Hier ein Beispiel für eine konkrete Anwendung:
Bildquelle: https://hiveplot.com/img/hiveplot-ecoli.png
Vorteile:
- Reduziert visuelle Unordnung
- Ermöglicht systematischen Vergleich von Netzwerkstrukturen
- Skaliert besser mit der Netzwerkgröße
Nachteile:
- Weniger intuitiv für Anfänger
- Erfordert Entscheidungen über die Anordnung von Knoten
- Kann bestimmte globale Strukturen verschleiern
Visualisierung mit NetworkX und Matplotlib¶
NetworkX bietet in Verbindung mit Matplotlib einfache Möglichkeiten zur Visualisierung von Graphen:
!conda install networkx conda-forge::python-igraph conda-forge::holoviews conda-forge::pycairo conda-forge::fa2 -y
# !conda install conda-forge::pycairo -y
import networkx as nx
import matplotlib.pyplot as plt
# Erstellen eines Graphen
G = nx.Graph()
G.add_edges_from([('A', 'B'), ('A', 'C'), ('B', 'C'), ('B', 'D')])
# Visualisierung mit verschiedenen Layout-Algorithmen
plt.figure(figsize=(12, 8))
# Spring Layout (kräftebasiert)
plt.subplot(221)
nx.draw(G, pos=nx.spring_layout(G), with_labels=True, node_color='lightblue',
node_size=500, font_weight='bold')
plt.title("Spring Layout")
# Circular Layout
plt.subplot(222)
nx.draw(G, pos=nx.circular_layout(G), with_labels=True, node_color='lightgreen',
node_size=500, font_weight='bold')
plt.title("Circular Layout")
# Random Layout
plt.subplot(223)
nx.draw(G, pos=nx.random_layout(G), with_labels=True, node_color='lightcoral',
node_size=500, font_weight='bold')
plt.title("Random Layout")
# Spectral Layout
plt.subplot(224)
nx.draw(G, pos=nx.spectral_layout(G), with_labels=True, node_color='lightyellow',
node_size=500, font_weight='bold')
plt.title("Spectral Layout")
plt.tight_layout()
plt.show()

6.2.3 Komplexitätsvergleich der verschiedenen Darstellungen¶
Die Wahl der richtigen Datenstruktur hängt von den geplanten Operationen ab. Hier ist ein Vergleich der Zeitkomplexität für häufige Operationen:
Operation | Adjazenzmatrix | Adjazenzliste | Inzidenzmatrix |
---|---|---|---|
Speicherbedarf | O(V²) | O(V+E) | O(V×E) |
Kante einfügen | O(1) | O(1) | O(1) |
Kante entfernen | O(1) | O(E) | O(E) |
Kante prüfen | O(1) | O(deg(V)) | O(E) |
Alle Nachbarn finden | O(V) | O(deg(V)) | O(E) |
Alle Kanten finden | O(V²) | O(V+E) | O(V×E) |
Wobei:
- V = Anzahl der Knoten (Vertices)
- E = Anzahl der Kanten (Edges)
- deg(V) = Grad eines Knotens (typischerweise viel kleiner als V)
Hier noch einmal die wichtigsten Empfehlungen für die Auswahl geeigneter Datenstrukturen:
- Adjazenzmatrix: Gut für dichte Graphen und wenn schnelle Kanten-Lookups wichtig sind
- Adjazenzliste: Gut für dünn besetzte Graphen und wenn Nachbar-Iteration wichtig ist
- Inzidenzmatrix: Selten verwendet, nützlich für spezielle Anwendungen mit Fokus auf Kanten
6.2.4 Verwendung spezialisierter Bibliotheken¶
Python bietet mehrere leistungsstarke Bibliotheken für die Arbeit mit Graphen:
NetworkX¶
NetworkX (bereits oben kurz verwendet) ist eine umfassende Bibliothek für die Erstellung, Manipulation und Analyse komplexer Netzwerke. Es ist für allgemeine Graphalgorithmen optimiert und bietet eine benutzerfreundliche API.
import networkx as nx
# Erstellen eines Graphen
G = nx.Graph() # ungerichtet
# G = nx.DiGraph() # für gerichtete Graphen
# Hinzufügen von Knoten
G.add_node('A', role='source') # Knoten mit Attributen
G.add_nodes_from(['B', 'C', 'D'])
# Hinzufügen von Kanten
G.add_edge('A', 'B', weight=0.6) # Kante mit Gewicht
G.add_edges_from([('A', 'C'), ('B', 'C'), ('B', 'D')])
# Grundlegende Informationen
print(f"Knoten: {G.nodes()}")
print(f"Kanten: {G.edges()}")
print(f"Anzahl der Knoten: {G.number_of_nodes()}")
print(f"Anzahl der Kanten: {G.number_of_edges()}")
# Zugriff auf Nachbarn
print(f"Nachbarn von B: {list(G.neighbors('B'))}")
# Zentralitätsmaße
print(f"Betweenness Centrality: {nx.betweenness_centrality(G)}")
print(f"Degree Centrality: {nx.degree_centrality(G)}")
# Kürzeste Pfade
print(f"Kürzester Pfad von A nach D: {nx.shortest_path(G, 'A', 'D')}")
print(f"Alle kürzesten Pfade: {dict(nx.all_pairs_shortest_path(G))}")
Knoten: ['A', 'B', 'C', 'D'] Kanten: [('A', 'B'), ('A', 'C'), ('B', 'C'), ('B', 'D')] Anzahl der Knoten: 4 Anzahl der Kanten: 4 Nachbarn von B: ['A', 'C', 'D'] Betweenness Centrality: {'A': 0.0, 'B': 0.6666666666666666, 'C': 0.0, 'D': 0.0} Degree Centrality: {'A': 0.6666666666666666, 'B': 1.0, 'C': 0.6666666666666666, 'D': 0.3333333333333333} Kürzester Pfad von A nach D: ['A', 'B', 'D'] Alle kürzesten Pfade: {'A': {'A': ['A'], 'B': ['A', 'B'], 'C': ['A', 'C'], 'D': ['A', 'B', 'D']}, 'B': {'B': ['B'], 'A': ['B', 'A'], 'C': ['B', 'C'], 'D': ['B', 'D']}, 'C': {'C': ['C'], 'A': ['C', 'A'], 'B': ['C', 'B'], 'D': ['C', 'B', 'D']}, 'D': {'D': ['D'], 'B': ['D', 'B'], 'A': ['D', 'B', 'A'], 'C': ['D', 'B', 'C']}}
igraph¶
Python-igraph ist eine schnelle und effiziente Bibliothek, die besonders für große Graphen optimiert ist. Es bietet hervorragende Performance für rechenintensive Aufgaben.
import igraph as ig
# Erstellen eines Graphen
g = ig.Graph()
g.add_vertices(4) # Indizes 0-3 statt Bezeichnern
g.vs["name"] = ["A", "B", "C", "D"] # Namen zuweisen
g.add_edges([(0, 1), (0, 2), (1, 2), (1, 3)]) # Indizes nutzen
# Visualisierung
visual_style = {}
visual_style["vertex_size"] = 45
visual_style["vertex_label"] = g.vs["name"]
visual_style["edge_width"] = [1, 2, 3, 1]
visual_style["layout"] = g.layout("kk")
visual_style["bbox"] = (300, 300)
visual_style["margin"] = 40
ig.plot(g, **visual_style)
# Eigenschaften berechnen
print(f"Grad: {g.degree()}")
print(f"Durchmesser: {g.diameter()}")
print(f"Clustering-Koeffizient: {g.transitivity_undirected()}")
Grad: [2, 3, 2, 1] Durchmesser: 2 Clustering-Koeffizient: 0.6
PyViz Ecosystem (HoloViews/hvPlot)¶
Für fortgeschrittene interaktive Visualisierungen bietet das PyViz-Ökosystem (mit Paketen wie HoloViews und hvPlot) leistungsstarke Werkzeuge. Diese sind besonders für die explorative Datenanalyse von Graphen nützlich.
import networkx as nx
import holoviews as hv
from holoviews import opts
hv.extension('bokeh')
# Graph erstellen
G = nx.karate_club_graph() # Ein klassischer Beispielgraph
# In HoloViews-Format konvertieren
graph = hv.Graph.from_networkx(G, nx.layout.spring_layout)
# Visualisierung anpassen
graph.opts(
opts.Graph(width=600, height=400, xaxis=None, yaxis=None,
edge_line_width=1, node_size=10, node_color='category'))


Beim Arbeiten mit großen Graphen ist die Auswahl der richtigen Bibliothek entscheidend für die Performance. NetworkX ist gut für Prototyping und kleinere Graphen, während igraph für größere Graphen und rechenintensive Operationen optimiert ist. Das PyViz-Ökosystem glänzt bei interaktiven, datengetriebenen Visualisierungen.
6.3 Graphtraversierung: Breitensuche (BFS) und Tiefensuche (DFS)¶
Graphtraversierung bezeichnet den Prozess des systematischen Besuchens aller Knoten in einem Graphen. Die zwei fundamentalen Algorithmen zur Graphtraversierung sind die Breitensuche (Breadth-First Search, BFS) und die Tiefensuche (Depth-First Search, DFS). Diese Algorithmen sind nicht nur grundlegend für das Verständnis von Graphen, sondern dienen auch als Bausteine für komplexere Algorithmen.
6.3.1 Grundprinzipien und Algorithmen im Vergleich¶
Breitensuche (BFS)¶
Die Breitensuche beginnt an einem Startknoten und besucht zuerst alle direkten Nachbarn, bevor sie zu den Nachbarn der Nachbarn übergeht. Dies führt zu einer ebenenweisen Traversierung des Graphen, bei der zuerst alle Knoten mit Abstand 1 vom Startknoten, dann alle mit Abstand 2 usw. besucht werden.
Bildquelle: https://upload.wikimedia.org/wikipedia/commons/5/5d/Breadth-First-Search-Algorithm.gif
Eigenschaften der BFS:
- Verwendet eine Queue (FIFO – First In, First Out) zur Verwaltung der zu besuchenden Knoten
- Findet den kürzesten Pfad in ungewichteten Graphen
- Gut für die Suche nach Knoten in der Nähe des Startpunkts
- Speicherbedarf kann bei großen Graphen problematisch sein, da die Queue alle Knoten einer Ebene speichern muss
Tiefensuche (DFS)¶
Die Tiefensuche beginnt ebenfalls an einem Startknoten, geht aber so weit wie möglich entlang eines Pfades, bevor sie zurückkehrt und alternative Pfade erkundet. Dies führt zu einer pfadorientierten Traversierung des Graphen.
Bildquelle: https://upload.wikimedia.org/wikipedia/commons/7/7f/Depth-First-Search.gif
Eigenschaften der DFS:
- Verwendet einen Stack (LIFO – Last In, First Out) oder Rekursion für die Implementierung
- Kann effizienter sein, wenn man erwartet, dass Lösungen weit vom Startpunkt entfernt sind
- Gut für die Erkundung aller möglichen Pfade
- Ideal für Aufgaben wie die Erkennung von Zyklen oder topologische Sortierung
6.3.2 Implementierung von BFS mit Queue-Datenstruktur¶
Die Breitensuche wird typischerweise mit einer Queue implementiert, die das FIFO-Prinzip (First-In-First-Out) verwendet. Hier ist ein grundlegender Algorithmus für BFS:
- Initialisiere eine leere Queue und füge den Startknoten hinzu.
- Markiere den Startknoten als besucht.
- Solange die Queue nicht leer ist: a. Entferne den vordersten Knoten aus der Queue. b. Verarbeite den Knoten (z.B. gib ihn aus). c. Füge alle unbesuchten Nachbarn des Knotens zur Queue hinzu und markiere sie als besucht.
Implementierung in Python¶
from collections import deque
def bfs(graph, start):
"""
Führt eine Breitensuche auf dem Graphen durch, beginnend beim Startknoten.
Parameter:
graph -- Ein Graph als Adjazenzliste (Dictionary)
start -- Der Startknoten
Rückgabe:
Eine Liste mit den besuchten Knoten in der Reihenfolge ihres Besuchs
"""
# Initialisiere die Datenstrukturen
visited = set() # Menge zur Speicherung besuchter Knoten
queue = deque([start]) # Queue mit dem Startknoten
visited.add(start) # Markiere den Startknoten als besucht
# Liste zur Speicherung der Besuchsreihenfolge
traversal_order = []
# BFS-Algorithmus
while queue:
# Entferne den vordersten Knoten aus der Queue
current_node = queue.popleft()
traversal_order.append(current_node)
# Füge alle unbesuchten Nachbarn zur Queue hinzu
for neighbor in graph[current_node]:
if neighbor not in visited:
visited.add(neighbor)
queue.append(neighbor)
return traversal_order
# Beispielgraph
graph = {
'A': ['B', 'C'],
'B': ['A', 'D', 'E'],
'C': ['A', 'F'],
'D': ['B'],
'E': ['B', 'F'],
'F': ['C', 'E']
}
# BFS durchführen
bfs_order = bfs(graph, 'A')
print("BFS-Reihenfolge:", bfs_order)
BFS-Reihenfolge: ['A', 'B', 'C', 'D', 'E', 'F']
Berechnung der kürzesten Pfade mit BFS¶
Eine wichtige Anwendung von BFS ist die Berechnung der kürzesten Pfade in ungewichteten Graphen. Hier ist eine Implementierung, die nicht nur die Knoten besucht, sondern auch die kürzesten Pfade und Distanzen vom Startknoten zu allen erreichbaren Knoten berechnet:
def bfs_shortest_paths(graph, start):
"""
Berechnet die kürzesten Pfade von einem Startknoten zu allen anderen Knoten
in einem ungewichteten Graphen mittels BFS.
Parameter:
graph -- Ein Graph als Adjazenzliste (Dictionary)
start -- Der Startknoten
Rückgabe:
distances -- Dictionary mit Distanzen vom Startknoten
predecessors -- Dictionary mit Vorgängerknoten für die Pfadrekonstruktion
"""
# Initialisiere die Datenstrukturen
visited = {start} # Menge besuchter Knoten
queue = deque([(start, 0)]) # Queue mit (Knoten, Distanz)
# Distanzen und Vorgänger für die Pfadrekonstruktion
distances = {start: 0}
predecessors = {start: None}
while queue:
current_node, distance = queue.popleft()
for neighbor in graph[current_node]:
if neighbor not in visited:
visited.add(neighbor)
distances[neighbor] = distance + 1
predecessors[neighbor] = current_node
queue.append((neighbor, distance + 1))
return distances, predecessors
def reconstruct_path(predecessors, start, end):
"""
Rekonstruiert den Pfad vom Start- zum Zielknoten anhand der Vorgängerinformationen.
Parameter:
predecessors -- Dictionary mit Vorgängerknoten
start -- Startknoten
end -- Zielknoten
Rückgabe:
path -- Liste mit dem Pfad vom Start zum Ziel
"""
if end not in predecessors:
return None # Zielknoten ist nicht erreichbar
path = []
current = end
# Gehe vom Ziel zum Start zurück
while current != start:
path.append(current)
current = predecessors[current]
path.append(start) # Füge den Startknoten hinzu
path.reverse() # Drehe den Pfad um
return path
# Anwendung
distances, predecessors = bfs_shortest_paths(graph, 'A')
print("Distanzen von A:", distances)
# Kürzesten Pfad von A nach F berechnen
path_to_F = reconstruct_path(predecessors, 'A', 'F')
print("Kürzester Pfad von A nach F:", path_to_F)
Distanzen von A: {'A': 0, 'B': 1, 'C': 1, 'D': 2, 'E': 2, 'F': 2} Kürzester Pfad von A nach F: ['A', 'C', 'F']
6.3.3 Rekursive und iterative Implementierung von DFS¶
Die Tiefensuche kann sowohl rekursiv als auch iterativ mit einem Stack implementiert werden. Beide Ansätze sind äquivalent, haben aber unterschiedliche Vor- und Nachteile.
Rekursive Implementierung¶
Die rekursive Implementierung nutzt den Funktionsaufrufstack des Systems:
def dfs_recursive(graph, start, visited=None, traversal_order=None):
"""
Führt eine rekursive Tiefensuche auf dem Graphen durch.
Parameter:
graph -- Ein Graph als Adjazenzliste (Dictionary)
start -- Der aktuelle Knoten
visited -- Menge bereits besuchter Knoten
traversal_order -- Liste zur Speicherung der Besuchsreihenfolge
Rückgabe:
Die Liste mit den besuchten Knoten in DFS-Reihenfolge
"""
# Initialisiere visited und traversal_order beim ersten Aufruf
if visited is None:
visited = set()
if traversal_order is None:
traversal_order = []
# Markiere den aktuellen Knoten als besucht
visited.add(start)
traversal_order.append(start)
# Rekursiver Aufruf für alle unbesuchten Nachbarn
for neighbor in graph[start]:
if neighbor not in visited:
dfs_recursive(graph, neighbor, visited, traversal_order)
return traversal_order
# DFS rekursiv durchführen
dfs_recursive_order = dfs_recursive(graph, 'A')
print("Rekursive DFS-Reihenfolge:", dfs_recursive_order)
Rekursive DFS-Reihenfolge: ['A', 'B', 'D', 'E', 'F', 'C']
Vorteile:
- Eleganter und lesbarer Code
- Natürliche Umsetzung des Algorithmus
Nachteile:
- Risiko eines Stack-Overflows bei sehr tiefen Graphen
- Höherer Overhead durch Funktionsaufrufe
Iterative Implementierung¶
Die iterative Implementierung verwendet einen expliziten Stack:
def dfs_iterative(graph, start):
"""
Führt eine iterative Tiefensuche auf dem Graphen durch.
Parameter:
graph -- Ein Graph als Adjazenzliste (Dictionary)
start -- Der Startknoten
Rückgabe:
Eine Liste mit den besuchten Knoten in DFS-Reihenfolge
"""
# Initialisiere die Datenstrukturen
visited = set()
stack = [start] # Initialisiere den Stack mit dem Startknoten
traversal_order = []
while stack:
# Entferne den obersten Knoten vom Stack
current_node = stack.pop()
# Wenn der Knoten noch nicht besucht wurde
if current_node not in visited:
visited.add(current_node)
traversal_order.append(current_node)
# Füge alle unbesuchten Nachbarn zum Stack hinzu (in umgekehrter Reihenfolge)
for neighbor in reversed(graph[current_node]):
if neighbor not in visited:
stack.append(neighbor)
return traversal_order
# DFS iterativ durchführen
dfs_iterative_order = dfs_iterative(graph, 'A')
print("Iterative DFS-Reihenfolge:", dfs_iterative_order)
Iterative DFS-Reihenfolge: ['A', 'B', 'D', 'E', 'F', 'C']
Vorteile:
- Keine Gefahr eines Stack-Overflows
- Potentiell effizienter bei sehr tiefen Graphen
Nachteile:
- Etwas komplexere Implementierung
- Die Besuchsreihenfolge kann sich von der rekursiven Version unterscheiden
6.3.4 Vergleich und Visualisierung von BFS und DFS¶
Um die unterschiedlichen Traversierungsstrategien von BFS und DFS besser zu verstehen, ist es hilfreich, sie zu visualisieren. Hier ist ein Beispielcode, der beide Algorithmen auf demselben Graphen anwendet und die Traversierungsreihenfolge visualisiert:
import networkx as nx
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from collections import deque
def create_visualization(graph, start, algorithm="bfs"):
"""
Erstellt eine Animation der Graph-Traversierung.
Parameter:
graph -- Der Graph als Adjazenzliste
start -- Der Startknoten
algorithm -- "bfs" oder "dfs"
"""
# Erstelle einen NetworkX-Graphen
G = nx.Graph()
for node in graph:
for neighbor in graph[node]:
G.add_edge(node, neighbor)
# Berechne das Layout (Positionen der Knoten)
pos = nx.spring_layout(G, seed=42)
# Initialisiere Figure
fig, ax = plt.subplots(figsize=(10, 8))
# Sammle die Frames für die Animation
frames = []
if algorithm == "bfs":
# BFS-Animation
visited = set([start])
queue = deque([start])
# Anfangszustand
node_colors = ['red' if node == start else 'lightblue' for node in G.nodes()]
edge_colors = ['black' for _ in G.edges()]
frames.append((node_colors.copy(), edge_colors.copy()))
while queue:
current = queue.popleft()
for neighbor in graph[current]:
if neighbor not in visited:
visited.add(neighbor)
queue.append(neighbor)
# Aktualisiere Farben
node_colors = ['red' if node in visited else 'lightblue' for node in G.nodes()]
edge_colors = ['red' if (u, v) == (current, neighbor) or (u, v) == (neighbor, current)
else 'red' if any((u == node and v in visited and node in visited) or
(v == node and u in visited and node in visited)
for node in visited)
else 'black'
for u, v in G.edges()]
frames.append((node_colors.copy(), edge_colors.copy()))
else: # DFS
# DFS-Animation
visited = set()
stack = [start]
# Anfangszustand
node_colors = ['red' if node == start else 'lightblue' for node in G.nodes()]
edge_colors = ['black' for _ in G.edges()]
frames.append((node_colors.copy(), edge_colors.copy()))
while stack:
current = stack.pop()
if current not in visited:
visited.add(current)
for neighbor in reversed(graph[current]):
if neighbor not in visited:
stack.append(neighbor)
# Aktualisiere Farben
node_colors = ['red' if node in visited else 'lightblue' for node in G.nodes()]
edge_colors = ['red' if any((u == node and v in visited and node in visited) or
(v == node and u in visited and node in visited)
for node in visited)
else 'black'
for u, v in G.edges()]
frames.append((node_colors.copy(), edge_colors.copy()))
# Make sure we have frames before creating animation
if not frames:
print(f"Warning: No frames collected for {algorithm} animation!")
# Add at least one dummy frame to avoid errors
frames.append((['lightblue' for _ in G.nodes()], ['black' for _ in G.edges()]))
# Erstelle Animation
def update(frame_idx):
ax.clear()
node_colors, edge_colors = frames[frame_idx]
nx.draw(G, pos, with_labels=True, node_color=node_colors,
edge_color=edge_colors, node_size=500, font_weight='bold', ax=ax)
ax.set_title(f"{algorithm.upper()}-Traversierung (Schritt {frame_idx})")
return ax
# Use the correct number of frames
ani = animation.FuncAnimation(fig, update, frames=len(frames), interval=1000)
return ani
# BFS-Animation erstellen
bfs_animation = create_visualization(graph, 'A', "bfs")
# DFS-Animation erstellen
dfs_animation = create_visualization(graph, 'A', "dfs")
# Animationen speichern (optional)
bfs_animation.save('bfs_animation.gif', writer='pillow')
dfs_animation.save('dfs_animation.gif', writer='pillow')
plt.show()


Zusammenfassung der Unterschiede¶
Die Visualisierungen verdeutlichen die grundlegenden Unterschiede zwischen BFS und DFS:
BFS exploriert den Graphen ebenenweise, beginnend mit allen direkten Nachbarn des Startknotens, bevor es zu den Nachbarn der Nachbarn übergeht. Dies führt zu einem konzentrisch expandierenden Muster um den Startknoten herum.
DFS erkundet den Graphen pfadorientiert, indem es so weit wie möglich entlang eines Pfades geht, bevor es zurückkehrt und alternative Pfade erkundet. Dies führt zu einem eher linearen Muster, das sich verzweigt, wenn Sackgassen erreicht werden.
In unserem konkreten Beispiel ist der Unterschied konkret am Besuch von C sichtbar, der zu verschiedenen Zeiten an der Reihe ist. Beide Algorithmen besuchen alle Knoten, die vom Startknoten aus erreichbar sind (also all Knoten in diesem Beispielgraph), aber in unterschiedlicher Reihenfolge.
Die Wahl zwischen BFS und DFS hängt vom spezifischen Anwendungsfall ab:
- Verwenden Sie BFS, wenn Sie den kürzesten Pfad in einem ungewichteten Graphen finden möchten oder wenn das Ziel wahrscheinlich nahe am Startpunkt liegt.
- Verwenden Sie DFS, wenn Sie den Graphen vollständig erkunden möchten, wenn Pfade wichtiger sind als kürzeste Distanzen, oder wenn das Ziel wahrscheinlich tief im Graphen liegt.
6.4 Einführung in erweiterte Graphalgorithmen¶
In diesem Abschnitt führen wir zwei fundamentale Problemklassen in der Graphentheorie ein: das Kürzeste-Wege-Problem und das Problem des minimalen Spannbaums. Diese Probleme sind nicht nur theoretisch interessant, sondern haben auch zahlreiche praktische Anwendungen. Wir beschränken uns hier aber auf einen reinen Überblick und die Erwähnung der wichtigsten Beispiel-Algorithmen.
6.4.1 Das Kürzeste-Wege-Problem: Definition und Anwendungen¶
Das Kürzeste-Wege-Problem besteht darin, den Pfad mit minimaler Länge (oder minimalem Gewicht) zwischen zwei Knoten in einem Graphen zu finden. Je nach Problemstellung unterscheiden wir verschiedene Varianten:
- Single-Source Shortest Path (SSSP): Finde kürzeste Pfade von einem Startknoten zu allen anderen Knoten.
- Single-Target Shortest Path: Finde kürzeste Pfade von allen Knoten zu einem Zielknoten.
- Single-Pair Shortest Path: Finde den kürzesten Pfad zwischen einem spezifischen Startknoten und einem Zielknoten.
- All-Pairs Shortest Paths (APSP): Finde kürzeste Pfade zwischen allen Paaren von Knoten.
Die wichtigsten Algorithmen für das Kürzeste-Wege-Problem sind der Dijkstra-Algorithmus, der Bellman-Ford-Algorithmus, der Floyd-Warshall-Algorithmus und der $A^\ast$-Algorithmus. Wie diese Algorithmen im Detail funktionieren, sprengt den Rahmen unserer Lehrveranstaltung.
Praktische Anwendungen des Kürzeste-Wege-Problems:¶
Solche Anwendungen sind praktisch überall zu finden. Es ist auch instinktiv einsichtig, wo man überall ein System mit Hilfe eines Netzwerks beschreiben kann, und dann einen kürzesten oder möglichst kurzen Weg zwischen zwei oder mehreren Punkten im Netzwerk finden will. Hier sind einige konkrete Beispiele:
- Navigationssysteme: Berechnung der schnellsten Route zwischen zwei Orten.
- Netzwerkrouting: Finden des effizientesten Pfades für Datenpakete.
- Künstliche Intelligenz: Pathfinding in Spielen und Roboternavigation.
- Soziale Netzwerkanalyse: Berechnung der “Grad der Trennung” zwischen Personen.
- Logistik: Optimierung von Lieferketten und Transportrouten.
6.4.2 Minimale Spannbäume: Definition und Anwendungen¶
Ein Spannbaum (auf Englisch Spanning Tree) eines ungerichteten, zusammenhängenden Graphen ist ein Teilgraph, der alle Knoten verbindet und ein Baum ist (d.h., zusammenhängend und ohne Zyklen). Ein minimaler Spannbaum (Minimum Spanning Tree, MST) ist ein Spannbaum mit minimaler Summe der Kantengewichte. Er hat genau $|V| – 1$ Kanten (wobei $|V|$ die Anzahl der Knoten ist). Hier sehen Sie ein konkretes Beispiel für so einen MST:
Die wichtigsten Algorithmen für minimale Spannbäume sind beispielhaft der Kruskal-Algorithmus und der Prim-Algorithmus. Beide Algorithmen bauen den MST auf ihre jeweilige spezielle Weise aus allen Knoten und Kanten zusammen.
Auch für minimale Spannbäume gibt es reihenweise konkrete Anwendungen. Auch hier ist es einsichtig, dass ein Baum, der das ganze Netzwerk möglichst effizient durchzieht, eine praktische und interessante Fragestellung sein kann. Hier sind wieder einige konkrete Beispiele:
- Netzwerkdesign: Entwurf von kostengünstigen Kommunikations- oder Transportnetzwerken.
- Clusteranalyse: Identifikation von Clustern in Datenpunkten durch Entfernen teurer Kanten aus dem MST.
- Stromnetze: Planung von effizienten Stromversorgungsnetzen.
- Wasserleitungen, Gasleitungen: Planung von Versorgungsinfrastruktur.
- Approximationsalgorithmen: MSTs werden auch in Approximationsalgorithmen für NP-schwere Probleme wie das Traveling Salesman Problem verwendet.
6.4.3 Zusammenhang zwischen Graphtraversierung und erweiterten Algorithmen¶
Bisher haben wir sowohl die Traversierungsalgorithmen als auch die fortgeschritteneren Probleme gesondert betrachtet. Diese hängen jedoch zusammen, weil man in vielen komplexeren Situation einfach “nur” möglichst kurze Wege oder bestimmte andere Eigenschaften im Netzwerk herausfinden muss. Die grundlegenden Traversierungsalgorithmen BFS und DFS bilden dabei die Basis für viele der erweiterten Graphalgorithmen:
Zum Beispiel ist BFS die Grundlage für Kürzeste-Wege-Algorithmen, denn BFS findet automatisch den kürzesten Pfad in ungewichteten Graphen. Dijkstra’s Algorithmus kann dann etwa als eine Erweiterung von BFS für gewichtete Graphen betrachtet werden.
DFS kann man in ähnlicher Weise als Grundlage für Zyklen- und Strukturerkennung ansehen. DFS eignet sich z.B. hervorragend für die Erkennung von strukturellen Eigenschaften in Graphen, wie etwa Zyklen: Wenn DFS auf einen bereits auf dem aktuellen Pfad besuchten Knoten trifft, wurde ein Zyklus gefunden.
Eine analoge Ähnlichkeit verbindet sowohl BFS als auch DFS mit minimalen Spannbäumen. Insgesamt sehen wir also, wie wichtig es ist, die Grundlagen der Graph-Algorithmen gut zu verstehen. Mit diesem Wissen können wir dann im Bedarfsfall auch komplexere Algorithmen besser nachvollziehen und anpassen.
6.5 Übungsaufgabe: Analyse eines sozialen Netzwerks¶
In dieser Übungsaufgabe werden wir uns mit der Analyse eines “sozialen Netzwerks” beschäftigen. Konkret geht es dabei um die Marvel-Superhelden, deren “soziales Netzwerk” so organisiert ist, dass Charaktere miteinander verbunden sind, wenn sie in denselben Comics aufgetreten sind. Damit kann man bereits recht viel anstellen und hier ist wieder Ihre Kreativität gefragt.
6.5.1 Der Datensatz¶
Der Marvel-Netzwerk-Datensatz enthält Informationen über gemeinsame Auftritte von Marvel-Superhelden in Comics. Jeder Knoten repräsentiert einen Charakter, und eine Kante zwischen zwei Charakteren bedeutet, dass sie in mindestens einem Comic zusammen aufgetreten sind. Diese Daten findet man z.B. auch auf der Kaggle-Platform, konkret hier: https://www.kaggle.com/datasets/csanhueza/the-marvel-universe-social-network/ Bitte beachten Sie zur Nachverfolgung aller Quellen auch die Hinweise auf der hier verlinkten Webseite.
Um nicht mit der Konstruktion des Netzwerks kämpfen zu müssen, sondern gleich mit dem fertigen Beziehungs-Graph arbeiten zu können, starten wir damit. Die Datei wird hier zunächst eingelesen und dann extrahieren wir noch die Bezeichnungen der Knoten (also die Namen der Superhelden).
import pandas as pd
import networkx as nx
import matplotlib.pyplot as plt
from collections import Counter
import os
import pickle
# Datei einlesen
print("Lade Superhelden-Netzwerk...")
df = pd.read_csv(os.path.join('data','hero-network.csv'))
# Schneller Überblick über die Daten
print("\nErste paar Zeilen der Datei:")
print(df.head(10))
print("\nInformationen zum DataFrame:")
print(df.info())
# Überprüfen auf doppelte Einträge
duplicate_count = df.duplicated().sum()
print(f"\nAnzahl doppelter Einträge: {duplicate_count}")
if duplicate_count > 0:
print("Beispiele für doppelte Einträge:")
print(df[df.duplicated(keep=False)].sort_values(by=['hero1', 'hero2']).head(10))
# Erstelle eine Liste aller eindeutigen Helden
all_heroes = pd.concat([df['hero1'], df['hero2']]).unique()
print(f"\nAnzahl eindeutiger Helden: {len(all_heroes)}")
print(f"Beispiele für Heldennamen: {all_heroes[:5]}")
# Zähle, wie oft jeder Held im Datensatz vorkommt
hero_occurrences = Counter(pd.concat([df['hero1'], df['hero2']]))
# Top 10 Helden nach Häufigkeit anzeigen
print("\nTop 10 Helden nach Anzahl der Verbindungen:")
for hero, count in hero_occurrences.most_common(10):
print(f"{hero}: {count} Verbindungen")
# Überprüfen, ob es Paare gibt, die mehrfach vorkommen (implizite Gewichtung)
pair_counts = df.groupby(['hero1', 'hero2']).size().reset_index(name='weight')
multi_pairs = pair_counts[pair_counts['weight'] > 1]
print(f"\nAnzahl der Heldenpaare: {len(pair_counts)}")
print(f"Davon mit mehrfachen Verbindungen: {len(multi_pairs)}")
if len(multi_pairs) > 0:
print("\nBeispiele für Paare mit mehrfachen Verbindungen:")
print(multi_pairs.sort_values(by='weight', ascending=False).head(10))
# Erstelle eine bereinigte Version mit Gewichten
print("\nErstelle bereinigte Datenversion mit Gewichten...")
network_with_weights = pair_counts
else:
print("\nKeine mehrfachen Verbindungen gefunden, jedes Paar erscheint nur einmal.")
# Füge eine Gewichtsspalte mit einheitlichem Wert 1 hinzu
network_with_weights = pair_counts
# Check for self-loops
self_loops = df[df['hero1'] == df['hero2']]
print(f"\nAnzahl der Selbstverbindungen (Helden mit sich selbst verbunden): {len(self_loops)}")
if len(self_loops) > 0:
print("Beispiele für Selbstverbindungen:")
print(self_loops.head())
# Filtere Selbstverbindungen aus dem bereinigten DataFrame
network_with_weights = network_with_weights[network_with_weights['hero1'] != network_with_weights['hero2']]
print(f"Bereinigte Anzahl der Heldenpaare: {len(network_with_weights)}")
# Erstelle einen NetworkX-Graphen
G = nx.Graph()
# Füge Knoten hinzu (alle eindeutigen Helden)
G.add_nodes_from(all_heroes)
# Füge Kanten mit Gewichten hinzu
for _, row in network_with_weights.iterrows():
G.add_edge(row['hero1'], row['hero2'], weight=row['weight'])
# Grundlegende Netzwerkstatistiken
print("\nGrundlegende Netzwerkstatistiken:")
print(f"Anzahl der Knoten (Helden): {G.number_of_nodes()}")
print(f"Anzahl der Kanten (Verbindungen): {G.number_of_edges()}")
print(f"Ist das Netzwerk zusammenhängend? {nx.is_connected(G)}")
if not nx.is_connected(G):
components = list(nx.connected_components(G))
print(f"Anzahl der Zusammenhangskomponenten: {len(components)}")
print(f"Größe der größten Komponente: {len(max(components, key=len))}")
# Speichere den Graphen für spätere Verwendung
print("\nSpeichere Netzwerkdaten für weitere Analysen...")
# Speichervorgang hier optional implementieren, z.B.:
# nx.write_graphml(G, "hero_network.graphml")
# Oder als Pickle-Datei:
# import pickle
with open(os.path.join("data","hero_network.pickle"), "wb") as f:
pickle.dump(G, f)
print("\nDatenvorverarbeitung abgeschlossen. Bereit für weitere Analysen!")
Lade Superhelden-Netzwerk... Erste paar Zeilen der Datei: hero1 hero2 0 LITTLE, ABNER PRINCESS ZANDA 1 LITTLE, ABNER BLACK PANTHER/T'CHAL 2 BLACK PANTHER/T'CHAL PRINCESS ZANDA 3 LITTLE, ABNER PRINCESS ZANDA 4 LITTLE, ABNER BLACK PANTHER/T'CHAL 5 BLACK PANTHER/T'CHAL PRINCESS ZANDA 6 STEELE, SIMON/WOLFGA FORTUNE, DOMINIC 7 STEELE, SIMON/WOLFGA ERWIN, CLYTEMNESTRA 8 STEELE, SIMON/WOLFGA IRON MAN/TONY STARK 9 STEELE, SIMON/WOLFGA IRON MAN IV/JAMES R. Informationen zum DataFrame: <class 'pandas.core.frame.DataFrame'> RangeIndex: 574467 entries, 0 to 574466 Data columns (total 2 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 hero1 574467 non-null object 1 hero2 574467 non-null object dtypes: object(2) memory usage: 8.8+ MB None Anzahl doppelter Einträge: 350286 Beispiele für doppelte Einträge: hero1 hero2 188178 3-D MAN/CHARLES CHAN GORILLA-MAN 188206 3-D MAN/CHARLES CHAN GORILLA-MAN 211082 3-D MAN/CHARLES CHAN GORILLA-MAN 188177 3-D MAN/CHARLES CHAN HUMAN ROBOT 188207 3-D MAN/CHARLES CHAN HUMAN ROBOT 211080 3-D MAN/CHARLES CHAN HUMAN ROBOT 188179 3-D MAN/CHARLES CHAN JONES, RICHARD MILHO 188204 3-D MAN/CHARLES CHAN JONES, RICHARD MILHO 188202 3-D MAN/CHARLES CHAN MARVEL BOY III/ROBER 211081 3-D MAN/CHARLES CHAN MARVEL BOY III/ROBER Anzahl eindeutiger Helden: 6426 Beispiele für Heldennamen: ['LITTLE, ABNER' "BLACK PANTHER/T'CHAL" 'STEELE, SIMON/WOLFGA' 'RAVEN, SABBATH II/EL' 'IRON MAN IV/JAMES R.'] Top 10 Helden nach Anzahl der Verbindungen: CAPTAIN AMERICA: 16499 Verbindungen SPIDER-MAN/PETER PAR: 13717 Verbindungen IRON MAN/TONY STARK : 11817 Verbindungen THOR/DR. DONALD BLAK: 11427 Verbindungen THING/BENJAMIN J. GR: 10681 Verbindungen WOLVERINE/LOGAN : 10353 Verbindungen HUMAN TORCH/JOHNNY S: 10237 Verbindungen SCARLET WITCH/WANDA : 9911 Verbindungen MR. FANTASTIC/REED R: 9775 Verbindungen VISION : 9696 Verbindungen Anzahl der Heldenpaare: 224181 Davon mit mehrfachen Verbindungen: 85352 Beispiele für Paare mit mehrfachen Verbindungen: hero1 hero2 weight 142514 PATRIOT/JEFF MACE PATRIOT/JEFF MACE 1275 142513 PATRIOT/JEFF MACE MISS AMERICA/MADELIN 1267 124770 MISS AMERICA/MADELIN MISS AMERICA/MADELIN 672 124772 MISS AMERICA/MADELIN PATRIOT/JEFF MACE 627 196026 THING/BENJAMIN J. GR HUMAN TORCH/JOHNNY S 382 85445 HUMAN TORCH/JOHNNY S MR. FANTASTIC/REED R 366 196250 THING/BENJAMIN J. GR MR. FANTASTIC/REED R 365 129194 MR. FANTASTIC/REED R INVISIBLE WOMAN/SUE 362 85724 HUMAN TORCH/JOHNNY S THING/BENJAMIN J. GR 362 89234 INVISIBLE WOMAN/SUE HUMAN TORCH/JOHNNY S 353 Erstelle bereinigte Datenversion mit Gewichten... Anzahl der Selbstverbindungen (Helden mit sich selbst verbunden): 2232 Beispiele für Selbstverbindungen: hero1 hero2 8888 MISS AMERICA/MADELIN MISS AMERICA/MADELIN 8889 MISS AMERICA/MADELIN MISS AMERICA/MADELIN 8890 MISS AMERICA/MADELIN MISS AMERICA/MADELIN 8891 MISS AMERICA/MADELIN MISS AMERICA/MADELIN 8892 MISS AMERICA/MADELIN MISS AMERICA/MADELIN Bereinigte Anzahl der Heldenpaare: 224169 Grundlegende Netzwerkstatistiken: Anzahl der Knoten (Helden): 6426 Anzahl der Kanten (Verbindungen): 167207 Ist das Netzwerk zusammenhängend? False Anzahl der Zusammenhangskomponenten: 4 Größe der größten Komponente: 6408 Speichere Netzwerkdaten für weitere Analysen... Datenvorverarbeitung abgeschlossen. Bereit für weitere Analysen!
6.5.2 Nächste Schritte¶
Sie haben mit diesem Datensatz wieder vielfältige Möglichkeiten. Zunächst sollten Sie versuchen, einen Überblick zu bekommen, der über die bloßen Outputs der organisatorischen Code-Zelle von oben hinausgeht. Dafür eignet sich natürlich eine Visualisierung, doch Achtung: Dieser Datensatz ist sehr umfangreich und es empfiehlt sich, irgendeine Auswahl zu treffen. Diskutieren Sie also diese Siuation mit Ihrem LLM. Geben sie ihm die Outputs der Analyse am Ende des organisatorischen Code-Outputs und einigen Sie sich auf gangbare Wege für eine Visualisierung.
Danach diskutieren Sie, welche Analysen für so ein Superhelden-Social-Network interessant wären, und führen Sie diese durch. Visualisieren Sie, was Sie an interessanten Ergebnissen erhalten. Viel Spaß!
import networkx as nx
import matplotlib.pyplot as plt
import numpy as np
from fa2 import ForceAtlas2
import pandas as pd
from matplotlib.cm import get_cmap
from tqdm import tqdm
def visualize_hero_network(G, method="subset"):
"""
Visualisiert das Superhelden-Netzwerk mit verschiedenen Methoden.
Parameter:
G -- NetworkX Graph-Objekt
method -- Visualisierungsmethode: "subset", "largest_component", oder "community"
"""
print(f"Visualisiere Netzwerk mit Methode: {method}")
if method == "subset":
# 1. Visualisierung eines Teilgraphen (Top-Helden nach Grad)
print("Erstelle Teilgraph basierend auf Knotengrad...")
degrees = dict(G.degree())
top_nodes = sorted(degrees, key=degrees.get, reverse=True)[:100] # Top 100
subgraph = G.subgraph(top_nodes)
print(f"Teilgraph erstellt mit {subgraph.number_of_nodes()} Knoten und {subgraph.number_of_edges()} Kanten")
plt.figure(figsize=(15, 15))
pos = nx.spring_layout(subgraph, seed=42)
# Knotengröße basierend auf Grad
node_size = [20 + 0.5 * degrees[n] for n in subgraph.nodes()]
nx.draw_networkx(
subgraph,
pos=pos,
node_size=node_size,
node_color='lightblue',
edge_color='gray',
alpha=0.7,
width=0.3,
with_labels=False
)
# Nur für die Top 20 Knoten Namen anzeigen
top20 = top_nodes[:20]
nx.draw_networkx_labels(
subgraph,
pos,
{node: node for node in top20},
font_size=8,
font_weight='bold'
)
plt.title("Teilgraph der Top-100 Superhelden (nach Grad)")
plt.axis('off')
plt.tight_layout()
plt.savefig('top_heroes_network.png', dpi=300, bbox_inches='tight')
plt.show()
elif method == "largest_component":
# 2. Visualisierung der größten Zusammenhangskomponente mit ForceAtlas2
try:
# Finde die größte Zusammenhangskomponente
print("Identifiziere größte Zusammenhangskomponente...")
largest_cc = max(nx.connected_components(G), key=len)
largest_cc_graph = G.subgraph(largest_cc).copy()
# Wähle zufällig 1000 Knoten aus, wenn die Komponente zu groß ist
if largest_cc_graph.number_of_nodes() > 1000:
print("Komponente zu groß für Visualisierung, wähle 1000 zufällige Knoten...")
nodes = list(largest_cc_graph.nodes())
np.random.seed(42)
sampled_nodes = np.random.choice(nodes, 1000, replace=False)
largest_cc_graph = largest_cc_graph.subgraph(sampled_nodes)
print(f"Berechne Layout für Graphen mit {largest_cc_graph.number_of_nodes()} Knoten...")
# Konvertiere zu NumPy für ForceAtlas2
A = nx.to_numpy_array(largest_cc_graph)
nodes = list(largest_cc_graph.nodes())
# ForceAtlas2 Layout
forceatlas2 = ForceAtlas2(
outboundAttractionDistribution=True,
linLogMode=False,
adjustSizes=False,
edgeWeightInfluence=1.0,
jitterTolerance=1.0,
barnesHutOptimize=True,
barnesHutTheta=1.2,
multiThreaded=False
)
print("Berechne ForceAtlas2-Layout (kann einige Zeit dauern)...")
positions = forceatlas2.forceatlas2_networkx_layout(largest_cc_graph, pos=None, iterations=100)
# Visualisierung
plt.figure(figsize=(20, 20))
# Knotengröße basierend auf Degree-Centrality
centrality = nx.degree_centrality(largest_cc_graph)
node_size = [3000 * centrality[node] for node in largest_cc_graph.nodes()]
# Knotenfarbe basierend auf Clustering-Koeffizient
clustering = nx.clustering(largest_cc_graph)
node_color = [clustering[node] for node in largest_cc_graph.nodes()]
nx.draw_networkx_nodes(
largest_cc_graph,
positions,
node_size=node_size,
node_color=node_color,
cmap=plt.cm.viridis,
alpha=0.8
)
nx.draw_networkx_edges(
largest_cc_graph,
positions,
width=0.1,
alpha=0.1,
edge_color='gray'
)
# Beschriftungen für die Top-50 Knoten nach Zentralität
top_centrality = sorted(centrality.items(), key=lambda x: x[1], reverse=True)[:50]
labels = {node: node for node, _ in top_centrality}
nx.draw_networkx_labels(
largest_cc_graph,
positions,
labels=labels,
font_size=8,
font_weight='bold'
)
plt.title("Größte Zusammenhangskomponente des Superhelden-Netzwerks")
plt.axis('off')
plt.tight_layout()
plt.savefig('largest_component_network.png', dpi=300, bbox_inches='tight')
plt.show()
except ImportError:
print("ForceAtlas2 nicht verfügbar. Verwende Standard-Layout...")
# Fallback auf standard Spring-Layout
visualize_hero_network(G, "subset")
elif method == "community":
# 3. Visualisierung mit Community-Erkennung (auf einem Teilgraphen)
print("Führe Community-Erkennung auf einem Teilgraphen durch...")
# Verwende einen Teilgraphen von angemessener Größe
degrees = dict(G.degree())
top_nodes = sorted(degrees, key=degrees.get, reverse=True)[:500] # Top 500
subgraph = G.subgraph(top_nodes)
# Community-Erkennung
print(f"Erkenne Communities im Teilgraphen mit {subgraph.number_of_nodes()} Knoten...")
try:
from community import best_partition
partition = best_partition(subgraph)
# Anzahl der Communities
communities = set(partition.values())
print(f"Gefundene Communities: {len(communities)}")
# Visualisierung
plt.figure(figsize=(20, 20))
pos = nx.spring_layout(subgraph, seed=42, k=0.3)
# Farbzuordnung für Communities
cmap = get_cmap('tab20')
colors = [cmap(i % 20) for i in range(len(communities))]
# Zeichne Knoten mit Community-Farben
for i, comm in enumerate(communities):
list_nodes = [node for node, com_id in partition.items() if com_id == comm]
nx.draw_networkx_nodes(
subgraph,
pos,
nodelist=list_nodes,
node_color=[colors[i]] * len(list_nodes),
node_size=[100 + 10 * degrees[node] for node in list_nodes],
alpha=0.8,
label=f"Community {i+1}"
)
# Zeichne Kanten
nx.draw_networkx_edges(
subgraph,
pos,
width=0.2,
alpha=0.2,
edge_color='gray'
)
# Beschriftungen für die Top-50 Knoten nach Grad
top_degree = sorted(degrees.items(), key=lambda x: x[1], reverse=True)[:50]
top_nodes = [node for node, _ in top_degree if node in subgraph.nodes()]
labels = {node: node for node in top_nodes}
nx.draw_networkx_labels(
subgraph,
pos,
labels=labels,
font_size=8,
font_weight='bold'
)
plt.title("Superhelden-Netzwerk mit Community-Struktur (Top 500 Knoten)")
plt.axis('off')
plt.legend(scatterpoints=1, loc='lower left')
plt.tight_layout()
plt.savefig('community_network.png', dpi=300, bbox_inches='tight')
plt.show()
except ImportError:
print("Community-Erkennungspaket nicht verfügbar. Verwende einfachere Visualisierung...")
visualize_hero_network(G, "subset")
else:
print(f"Unbekannte Methode: {method}. Verwende 'subset' stattdessen.")
visualize_hero_network(G, "subset")
def create_2d_visualization_grid(G):
"""Erstellt ein 2x2-Grid mit verschiedenen Visualisierungen."""
plt.figure(figsize=(20, 20))
# 1. Subgraph der Top-100 Knoten nach Grad
degrees = dict(G.degree())
top_nodes = sorted(degrees, key=degrees.get, reverse=True)[:100]
subgraph = G.subgraph(top_nodes)
# Spring Layout für alle Visualisierungen verwenden
pos = nx.spring_layout(subgraph, seed=42)
# Plot 1: Grundvisualisierung mit Knotengröße nach Grad
plt.subplot(2, 2, 1)
node_size = [20 + 0.5 * degrees[n] for n in subgraph.nodes()]
nx.draw_networkx(
subgraph,
pos=pos,
node_size=node_size,
node_color='lightblue',
edge_color='gray',
alpha=0.7,
width=0.3,
with_labels=False
)
# Nur für die Top 10 Knoten Namen anzeigen
top10 = top_nodes[:10]
nx.draw_networkx_labels(
subgraph,
pos,
{node: node for node in top10},
font_size=8,
font_weight='bold'
)
plt.title("Top-100 Superhelden nach Anzahl der Verbindungen")
plt.axis('off')
# Plot 2: Betweenness Centrality
plt.subplot(2, 2, 2)
betweenness = nx.betweenness_centrality(subgraph)
node_color = [betweenness[n] for n in subgraph.nodes()]
nx.draw_networkx(
subgraph,
pos=pos,
node_size=node_size,
node_color=node_color,
cmap=plt.cm.YlOrRd,
edge_color='gray',
alpha=0.7,
width=0.3,
with_labels=False
)
# Top 10 nach Betweenness
top_betweenness = sorted(betweenness.items(), key=lambda x: x[1], reverse=True)[:10]
top_betweenness_nodes = [node for node, _ in top_betweenness]
nx.draw_networkx_labels(
subgraph,
pos,
{node: node for node in top_betweenness_nodes},
font_size=8,
font_weight='bold'
)
plt.title("Betweenness Centrality (Brücken im Netzwerk)")
plt.axis('off')
# Plot 3: Clustering Coefficient
plt.subplot(2, 2, 3)
clustering = nx.clustering(subgraph)
node_color = [clustering[n] for n in subgraph.nodes()]
nx.draw_networkx(
subgraph,
pos=pos,
node_size=node_size,
node_color=node_color,
cmap=plt.cm.Blues,
edge_color='gray',
alpha=0.7,
width=0.3,
with_labels=False
)
# Top 10 nach Clustering
top_clustering = sorted(clustering.items(), key=lambda x: x[1], reverse=True)[:10]
top_clustering_nodes = [node for node, _ in top_clustering]
nx.draw_networkx_labels(
subgraph,
pos,
{node: node for node in top_clustering_nodes},
font_size=8,
font_weight='bold'
)
plt.title("Clustering Coefficient (Dichte lokaler Verbindungen)")
plt.axis('off')
# Plot 4: Versuche Community-Erkennung
plt.subplot(2, 2, 4)
try:
from community import best_partition
partition = best_partition(subgraph)
communities = set(partition.values())
node_color = [partition[n] for n in subgraph.nodes()]
nx.draw_networkx(
subgraph,
pos=pos,
node_size=node_size,
node_color=node_color,
cmap=plt.cm.tab20,
edge_color='gray',
alpha=0.7,
width=0.3,
with_labels=False
)
plt.title(f"Community-Struktur ({len(communities)} Communities)")
except ImportError:
# Alternative: Eigenvektor-Zentralität
eigenvector = nx.eigenvector_centrality_numpy(subgraph)
node_color = [eigenvector[n] for n in subgraph.nodes()]
nx.draw_networkx(
subgraph,
pos=pos,
node_size=node_size,
node_color=node_color,
cmap=plt.cm.plasma,
edge_color='gray',
alpha=0.7,
width=0.3,
with_labels=False
)
# Top 10 nach Eigenvektor-Zentralität
top_eigenvector = sorted(eigenvector.items(), key=lambda x: x[1], reverse=True)[:10]
top_eigenvector_nodes = [node for node, _ in top_eigenvector]
nx.draw_networkx_labels(
subgraph,
pos,
{node: node for node in top_eigenvector_nodes},
font_size=8,
font_weight='bold'
)
plt.title("Eigenvektor-Zentralität (Einfluss im Netzwerk)")
plt.axis('off')
plt.tight_layout()
plt.savefig('hero_network_analysis_grid.png', dpi=300, bbox_inches='tight')
plt.show()
def create_heatmap_visualization(G):
"""Erstellt eine Heatmap der Adjazenzmatrix für einen Teilgraphen."""
# Wähle einen Teilgraphen der Top-50 Knoten
degrees = dict(G.degree())
top_nodes = sorted(degrees, key=degrees.get, reverse=True)[:50]
subgraph = G.subgraph(top_nodes)
# Erstelle Adjazenzmatrix
adj_matrix = nx.to_numpy_array(subgraph)
# Knotennamen für die Achsenbeschriftung
nodes_list = list(subgraph.nodes())
plt.figure(figsize=(15, 15))
plt.imshow(adj_matrix, cmap='Blues', interpolation='nearest')
# Achsenbeschriftungen mit Knotennamen (gekürzt)
shortened_names = [name[:15] + "..." if len(name) > 15 else name for name in nodes_list]
plt.xticks(range(len(nodes_list)), shortened_names, rotation=90, fontsize=8)
plt.yticks(range(len(nodes_list)), shortened_names, fontsize=8)
plt.title("Adjazenzmatrix der Top-50 Superhelden")
plt.colorbar(label="Verbindungsstärke")
plt.tight_layout()
plt.savefig('hero_adjacency_heatmap.png', dpi=300, bbox_inches='tight')
plt.show()
# Hauptfunktion, die alle Visualisierungen erstellt
def visualize_full_hero_network(G):
"""Erstellt verschiedene Visualisierungen des Superhelden-Netzwerks."""
print("Erstelle verschiedene Visualisierungen des Superhelden-Netzwerks...\n")
# 1. Visualisierung eines Teilgraphen
print("Visualisierung 1: Teilgraph der wichtigsten Superhelden")
visualize_hero_network(G, "subset")
# 2. Grid mit verschiedenen Zentralitätsmaßen
print("\nVisualisierung 2: Analyseraster mit verschiedenen Metriken")
create_2d_visualization_grid(G)
# 3. Heatmap-Visualisierung
print("\nVisualisierung 3: Heatmap der Adjazenzmatrix")
create_heatmap_visualization(G)
# 4. Versuch der ForceAtlas2-Visualisierung (wenn verfügbar)
try:
print("\nVisualisierung 4: Fortgeschrittene Visualisierung mit ForceAtlas2")
visualize_hero_network(G, "largest_component")
except Exception as e:
print(f"ForceAtlas2-Visualisierung fehlgeschlagen: {e}")
# 5. Community-Visualisierung (wenn verfügbar)
try:
print("\nVisualisierung 5: Community-Struktur des Netzwerks")
visualize_hero_network(G, "community")
except Exception as e:
print(f"Community-Visualisierung fehlgeschlagen: {e}")
print("\nAlle Visualisierungen abgeschlossen und gespeichert!")
# Beispielaufruf, wenn dieses Skript direkt ausgeführt wird
if __name__ == "__main__":
# Lade das Netzwerk (falls es noch nicht geladen wurde)
try:
# Versuche zuerst, einen vorhandenen Graphen zu laden
import pickle
with open(os.path.join("data","hero_network.pickle"), "rb") as f:
G = pickle.load(f)
print("Netzwerk aus Pickle-Datei geladen.")
except:
# Falls kein gespeicherter Graph existiert, erstelle einen neuen
print("Erstelle Netzwerk aus CSV-Datei...")
# Code zum Laden und Erstellen des Netzwerks hier einfügen...
# z.B.:
import pandas as pd
df = pd.read_csv('hero-network.csv')
G = nx.Graph()
for _, row in df.iterrows():
G.add_edge(row['hero1'], row['hero2'])
print(f"Netzwerk erstellt mit {G.number_of_nodes()} Knoten und {G.number_of_edges()} Kanten.")
# Erstelle Visualisierungen
visualize_full_hero_network(G)
Netzwerk aus Pickle-Datei geladen. Erstelle verschiedene Visualisierungen des Superhelden-Netzwerks... Visualisierung 1: Teilgraph der wichtigsten Superhelden Visualisiere Netzwerk mit Methode: subset Erstelle Teilgraph basierend auf Knotengrad... Teilgraph erstellt mit 100 Knoten und 4325 Kanten

Visualisierung 2: Analyseraster mit verschiedenen Metriken

Visualisierung 3: Heatmap der Adjazenzmatrix

Visualisierung 4: Fortgeschrittene Visualisierung mit ForceAtlas2 Visualisiere Netzwerk mit Methode: largest_component Identifiziere größte Zusammenhangskomponente... Komponente zu groß für Visualisierung, wähle 1000 zufällige Knoten... Berechne Layout für Graphen mit 1000 Knoten... Berechne ForceAtlas2-Layout (kann einige Zeit dauern)... ForceAtlas2-Visualisierung fehlgeschlagen: module 'networkx' has no attribute 'to_scipy_sparse_matrix' Visualisierung 5: Community-Struktur des Netzwerks Visualisiere Netzwerk mit Methode: community Führe Community-Erkennung auf einem Teilgraphen durch... Erkenne Communities im Teilgraphen mit 500 Knoten... Community-Erkennungspaket nicht verfügbar. Verwende einfachere Visualisierung... Visualisiere Netzwerk mit Methode: subset Erstelle Teilgraph basierend auf Knotengrad... Teilgraph erstellt mit 100 Knoten und 4325 Kanten
Alle Visualisierungen abgeschlossen und gespeichert!