Visualisierung als Netzwerk

In der vorherigen Einheit wurden Topics für einzelne Dokumente ausgegeben. Das ist zwar durchaus aufschlussreich, ist bei großen Corpora aber ein zeitaufwändiger Prozess. Nicht nur zeitökonomisch, auch analytisch kann es Gründe geben, anders auf Topic Models zu gucken: Wenn es darum geht, größere Strukturen sichtbar zu machen, ist eine abstraktere Betrachtung der Ergebnisse interessant.

Visualisierungen können helfen, Strukturen in großen Datenmengen sichtbar zu machen. Sie sind dabei nicht nur die Repräsentation eines Analyseergebnisses, sondern Teil des Analyseprozesses selbst. Hierfür gibt es den Begriff der visual analytics. Die Informationen, die ein Topic Model enthält, können mit Hilfe von Visualisierungen sichtbar gemacht werden. Eine Möglichkeit ist hierbei die Darstellung als Netzwerk.

Für eine interessante Diskussion unterschiedlicher Visualisierungsmöglichkeiten siehe auch diesen Blogpost.

Zunächst wird das zuvor erstellt Topic Model wieder geladen:

In [1]:
from pickle import load

with open('../Daten/topicmodel20.pickle', 'rb') as picklefile:
    ldamodel = load(picklefile)

Die zentralen Informationen des Modells sind in zwei Matrizen gespeichert:

In [2]:
ldamodel.topic_word_.shape
Out[2]:
(20, 12470)
In [3]:
ldamodel.doc_topic_.shape
Out[3]:
(793, 20)

Die Matrizen beinhalten die Beziehungen zwischen Topics und Wörtern (welche Wörter gehören zu welchem Topic?) sowie Dokumenten und Topics (welche Topics gehören zu welchem Dokument?). Dabei gibt es keine Trennlinie, die eine klare Antwort auf diese Fragen gibt, die Werte in den Matrizen sind vielmehr graduell.

Für die spätere Verwendung lässt sich aus den Dimensionen der Matrizen die Anzahl der Topics, Wörter und Dokumente ermitteln:

In [4]:
n_topics, n_words = ldamodel.topic_word_.shape
n_docs, n_topics = ldamodel.doc_topic_.shape

Es ist zu vermuten, dass der Zusammenhang von Dokumenten und Topics nicht ganz zufällig ist. Es könnte interessant sein, zu sehen, welche Topics sich etwa ähnlich auf die Dokumente verteilen. Wenn zwei Topics eine ähnliche Dokumentzuordnung aufweisen, ist zu vermuten, dass sie auch inhaltlich einen Zusammenhang haben.

Ein relativ simples Verfahren wäre etwa, sich für jedes Dokument die drei (vier, fünf) wichtigsten Topics ausgeben zu lassen und diese als miteinander verbunden zu verstehen. Daraus ließe sich ein Netzwerk von Topics erstellen, dessen Kanten die Zuordnung zu gleichen Dokumenten wären.

Eine andere Möglichkeit ist ein statistischer Ansatz. So lassen sich aus den Dokumentverteilungen Korrelationen berechnen: Die doc_topic-Tabelle enthält Dokumente als Zeilen und Topics als Spalten. Wenn man nun paarweise die Korrelationen zwischen Topics (Spalten) berechnet, lässt sich dies als Hinweis auf die Beziehungsstärke zweier Topics verstehen. Dieses Verfahren nutzt mehr Informationen als der erste Ansatz (nicht nur die ersten drei Topics), und kann Beziehungen gradueller darstellen (nämlich als Korrelationsstärke).

Ein pandas.DataFrame stellt eine komfortable Möglichkeit bereit, Korrelationen zwischen Spalten zu berechnen. Hier wird standardmäßig Pearson’s R verwendet. Das Ergebnis ist eine quadratische Matrix von Topic zu Topic, die die Korrelationsstärke aller Paare beinhaltet.

In [5]:
import pandas as pd
doc_topic = pd.DataFrame(ldamodel.doc_topic_)
topic_correlations = doc_topic.corr()
topic_correlations
Out[5]:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
0 1.000000 -0.046037 -0.092154 -0.054809 -0.074486 -0.092227 -0.075900 0.081877 -0.097106 -0.092181 0.145256 -0.069027 -0.096715 -0.015690 0.038101 0.027816 -0.093973 -0.020310 0.017073 -0.007606
1 -0.046037 1.000000 0.015910 0.013029 0.051465 -0.221919 -0.156615 -0.071046 -0.306478 0.352847 0.023583 -0.149760 0.149946 -0.125236 -0.084288 0.211448 0.040988 -0.387260 0.189204 -0.082722
2 -0.092154 0.015910 1.000000 -0.084795 -0.169895 0.065095 0.074355 -0.267901 -0.102433 0.051146 -0.143905 0.003991 -0.156059 -0.115390 -0.190055 -0.173780 0.141879 0.077396 -0.110363 -0.108688
3 -0.054809 0.013029 -0.084795 1.000000 0.176423 -0.060649 -0.136340 -0.079810 0.086960 -0.099084 -0.128556 -0.122615 -0.099861 -0.094066 0.002449 -0.134176 -0.166321 0.085062 0.037938 0.142498
4 -0.074486 0.051465 -0.169895 0.176423 1.000000 -0.177401 -0.228778 0.122726 -0.052339 -0.149671 -0.054109 -0.229278 0.369994 -0.169986 -0.016972 0.094508 -0.241540 -0.002689 0.053644 0.128961
5 -0.092227 -0.221919 0.065095 -0.060649 -0.177401 1.000000 0.174341 -0.277070 0.073112 0.067616 -0.204496 0.020051 -0.194757 -0.084190 0.108813 -0.235330 0.068408 0.052090 -0.133075 -0.158225
6 -0.075900 -0.156615 0.074355 -0.136340 -0.228778 0.174341 1.000000 -0.388917 -0.056819 -0.055354 -0.206522 0.255388 -0.214728 -0.173994 -0.131025 -0.260035 0.347137 0.127709 -0.150352 -0.138332
7 0.081877 -0.071046 -0.267901 -0.079810 0.122726 -0.277070 -0.388917 1.000000 -0.331410 -0.160540 0.299851 -0.419594 0.328426 0.401612 0.257765 0.394403 -0.351516 -0.335808 0.143992 0.019484
8 -0.097106 -0.306478 -0.102433 0.086960 -0.052339 0.073112 -0.056819 -0.331410 1.000000 -0.142047 -0.228579 0.368601 -0.226814 -0.144194 -0.059629 -0.284502 -0.100755 0.315410 -0.146497 -0.100164
9 -0.092181 0.352847 0.051146 -0.099084 -0.149671 0.067616 -0.055354 -0.160540 -0.142047 1.000000 -0.096648 -0.087590 -0.134896 -0.105823 0.041108 -0.151921 0.091004 -0.113039 -0.100372 -0.105938
10 0.145256 0.023583 -0.143905 -0.128556 -0.054109 -0.204496 -0.206522 0.299851 -0.228579 -0.096648 1.000000 -0.203576 0.052516 -0.020949 -0.107546 0.266068 -0.188390 -0.221826 -0.044903 0.244375
11 -0.069027 -0.149760 0.003991 -0.122615 -0.229278 0.020051 0.255388 -0.419594 0.368601 -0.087590 -0.203576 1.000000 -0.219200 -0.160224 -0.229033 -0.252958 0.139841 0.065245 -0.151338 -0.162975
12 -0.096715 0.149946 -0.156059 -0.099861 0.369994 -0.194757 -0.214728 0.328426 -0.226814 -0.134896 0.052516 -0.219200 1.000000 -0.019043 -0.061039 0.488193 -0.227845 -0.295322 -0.063644 -0.005547
13 -0.015690 -0.125236 -0.115390 -0.094066 -0.169986 -0.084190 -0.173994 0.401612 -0.144194 -0.105823 -0.020949 -0.160224 -0.019043 1.000000 0.191867 -0.008642 -0.094975 -0.052916 -0.067576 -0.140832
14 0.038101 -0.084288 -0.190055 0.002449 -0.016972 0.108813 -0.131025 0.257765 -0.059629 0.041108 -0.107546 -0.229033 -0.061039 0.191867 1.000000 -0.097123 -0.134975 -0.026454 0.000497 -0.190876
15 0.027816 0.211448 -0.173780 -0.134176 0.094508 -0.235330 -0.260035 0.394403 -0.284502 -0.151921 0.266068 -0.252958 0.488193 -0.008642 -0.097123 1.000000 -0.246870 -0.375589 0.211580 0.022674
16 -0.093973 0.040988 0.141879 -0.166321 -0.241540 0.068408 0.347137 -0.351516 -0.100755 0.091004 -0.188390 0.139841 -0.227845 -0.094975 -0.134975 -0.246870 1.000000 -0.003519 -0.154029 -0.175658
17 -0.020310 -0.387260 0.077396 0.085062 -0.002689 0.052090 0.127709 -0.335808 0.315410 -0.113039 -0.221826 0.065245 -0.295322 -0.052916 -0.026454 -0.375589 -0.003519 1.000000 -0.173605 -0.060250
18 0.017073 0.189204 -0.110363 0.037938 0.053644 -0.133075 -0.150352 0.143992 -0.146497 -0.100372 -0.044903 -0.151338 -0.063644 -0.067576 0.000497 0.211580 -0.154029 -0.173605 1.000000 0.021418
19 -0.007606 -0.082722 -0.108688 0.142498 0.128961 -0.158225 -0.138332 0.019484 -0.100164 -0.105938 0.244375 -0.162975 -0.005547 -0.140832 -0.190876 0.022674 -0.175658 -0.060250 0.021418 1.000000

Eine solche Tabelle lässt sich direkt als Netzwerk interpretieren: In der Netzwerktheorie ist eine quadratische Matrix eine Repräsentation der Kanten. Die Zellwerte stellen dann das Kantengewicht dar.

Da in dieser Matrix jedoch alle Topics miteinander verbunden sind, ist es nützlich, zuvor etwas zu filtern. Hier werden alle negativen oder sehr schwachen Korrelationen gelöscht, die entsprechenden Topics sind im Netzwerk also nicht verbunden.

In [6]:
topic_correlations[topic_correlations < 0.1] = 0

Die Diagonale der Korrelationsmatrix ist per Definition 1. Da im Netzwerk aber Beziehungen der Knoten zu sich selbst (sogenannte loops) in diesem Fall nicht sinnvoll sind, werden sie ebenfalls gelöscht.

Ein DataFrame erlaubt dies nicht direkt, aber numpy stellt hierfür eine Funktion bereit. Sie funktioniert aber nur mit einer Matrix, nicht mit einem DataFrame. Hier hilft ein Kniff: DataFrame.values enthält eine Matrix-Repräsentation des DataFrame.

In [7]:
import numpy as np

np.fill_diagonal(topic_correlations.values, 0)
topic_correlations
Out[7]:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
0 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.145256 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
1 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.352847 0.000000 0.000000 0.149946 0.000000 0.000000 0.211448 0.000000 0.000000 0.189204 0.000000
2 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.141879 0.000000 0.000000 0.000000
3 0.000000 0.000000 0.000000 0.000000 0.176423 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.142498
4 0.000000 0.000000 0.000000 0.176423 0.000000 0.000000 0.000000 0.122726 0.000000 0.000000 0.000000 0.000000 0.369994 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.128961
5 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.174341 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.108813 0.000000 0.000000 0.000000 0.000000 0.000000
6 0.000000 0.000000 0.000000 0.000000 0.000000 0.174341 0.000000 0.000000 0.000000 0.000000 0.000000 0.255388 0.000000 0.000000 0.000000 0.000000 0.347137 0.127709 0.000000 0.000000
7 0.000000 0.000000 0.000000 0.000000 0.122726 0.000000 0.000000 0.000000 0.000000 0.000000 0.299851 0.000000 0.328426 0.401612 0.257765 0.394403 0.000000 0.000000 0.143992 0.000000
8 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.368601 0.000000 0.000000 0.000000 0.000000 0.000000 0.315410 0.000000 0.000000
9 0.000000 0.352847 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
10 0.145256 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.299851 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.266068 0.000000 0.000000 0.000000 0.244375
11 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.255388 0.000000 0.368601 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.139841 0.000000 0.000000 0.000000
12 0.000000 0.149946 0.000000 0.000000 0.369994 0.000000 0.000000 0.328426 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.488193 0.000000 0.000000 0.000000 0.000000
13 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.401612 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.191867 0.000000 0.000000 0.000000 0.000000 0.000000
14 0.000000 0.000000 0.000000 0.000000 0.000000 0.108813 0.000000 0.257765 0.000000 0.000000 0.000000 0.000000 0.000000 0.191867 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
15 0.000000 0.211448 0.000000 0.000000 0.000000 0.000000 0.000000 0.394403 0.000000 0.000000 0.266068 0.000000 0.488193 0.000000 0.000000 0.000000 0.000000 0.000000 0.211580 0.000000
16 0.000000 0.000000 0.141879 0.000000 0.000000 0.000000 0.347137 0.000000 0.000000 0.000000 0.000000 0.139841 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
17 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.127709 0.000000 0.315410 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
18 0.000000 0.189204 0.000000 0.000000 0.000000 0.000000 0.000000 0.143992 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.211580 0.000000 0.000000 0.000000 0.000000
19 0.000000 0.000000 0.000000 0.142498 0.128961 0.000000 0.000000 0.000000 0.000000 0.000000 0.244375 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000

Die solchermaßen ausgedünnte Matrix kann direkt als Netzwerk visualisiert werden.

In [8]:
%matplotlib inline
import networkx as nx

topic_graph = nx.Graph(topic_correlations.values)
nx.draw(topic_graph, with_labels=True)

Eine erste Betrachtung zeigt, dass es hier zwei untereinander kaum verbundene Cluster gibt. Betrachtet man die entsprechenden Topics, lassen sich die Cluster grob einem kulturpolitischen Feld (oben) und einem wirtschaftspolitischen Feld (unten) zurechnen.

In ähnlicher Weise können auch die Korrelationen nicht von Topics, sondern von Dokumenten berechnet und visualisiert werden. Zwei Dokumente können dann als ähnlich angesehen werden, wenn sie eine ähnliche Topic-Verteilung aufweisen.

Die Dokumentkorrelationen erhält man, wenn man die doc_topic-Tabelle transponiert, bevor man die Korrelationen berechnet. Das Ergebnis ist dann eine quadratische Matrix von Dokumenten.

In [9]:
doc_correlations = doc_topic.transpose().corr()

doc_correlations[doc_correlations < .8] = 0
np.fill_diagonal(doc_correlations.values, 0)
doc_correlations.shape
Out[9]:
(793, 793)

Um das Netzwerk leichter interpretieren zu können, werden die Knoten des Netzwerkes entsprechend der Dokumenttitel benannt.

In [10]:
import pandas as pd

data = pd.read_csv("../Daten/Reden.csv", parse_dates=['date'], encoding='utf-8')
titles = data['title']

doc_graph = nx.Graph(doc_correlations.values)
doc_graph = nx.relabel_nodes(doc_graph, titles)

Das Netzwerk ist mit 793 Knoten zu groß, um es ohne weiteres mit den Möglichkeiten von networkx zu zeichnen. Für die explorative Analyse bietet sich ein Programm wie Gephi an. Daher wird das Netzwerk in eine GraphML-Datei exportiert und in Gephi weiter analysiert.

In [11]:
nx.write_graphml(doc_graph, 'docs.graphml')

In Gephi wurden nur zwei Schritte vorgenommen: Ein Layout wurde mit dem ForceAtlas-Algorithmus berechnet, und es wurden Communities eng zusammenhängender Dokumente ermittelt. (Dieser zweite Schritt wäre ebenso mit dem Paket python-louvain möglich gewesen, das den gleichen Algorithmus implementiert. Dafür siehe die Einheit zu »Text als Netzwerk«.)

Um das Ergebnis leicht im Notebook anzeigen zu können, wurde es mit dem Sigma-Plugin aus Gephi in eine HTML-Datei exportiert, die hier eingebunden wird.

In [12]:
from IPython.core.display import HTML

HTML('<iframe src="network/index.html" width="100%" height="600" seamless>Netzwerk kann nicht angezeigt werden.</iframe>')
Out[12]:

Bei Betrachtung der einzelnen Cluster zeigt sich insgesamt ein relativ deutlicher inhaltlicher Zusammenhang. So sind etwa die Dokumente des currygelben Clusters 12 (oben in der Mitte) mit Kultur und insbesondere mit Film und Filmförderung befasst, während das violette Cluster 9 (unten rechts) Themen aus der Verteidigungspolitik beinhaltet.