Die Beziehung von Texten und Wörtern zueinander lässt sich neben den bisher vorgestellen Techniken auch noch mit einem weiteren Ansatz untersuchen. Die Netzwerkanalyse stellt eine sehr wichtige Methode der Sozialwissenschaften dar, die grundsätzlich für die Untersuchung von Akteursbeziehungen von großer Relevanz ist. Sie ist aber darüber hinaus auch durchaus für die automatisierte Inhaltsanalyse von Bedeutung, zum einen, weil sich mit ihr etwa abbilden lässt, welche Akteure zusammen in einem Text genannte werden, aber auch dann, wenn beispielsweise ermittelt werden soll, wer ähnliche Begriffe verwendet.

Bereits im zweiten Kapitel wurde eine der wichtigsten Komponenten von quanteda eingeführt: die DFM. In diesem Kapitel wenden wir uns nun dem Pendant der DFM für die Untersuchung der Übereinstimmung von Wörtern zu, der Feature Co-occurance Matrix (FCM). Eine FCM enthält in ihren Zeilen und Spalten jeweils alle Wörter, die im Korpus vorkommen.

if(!require("quanteda")) {install.packages("quanteda"); library("quanteda")}
if(!require("tidyverse")) {install.packages("tidyverse"); library("tidyverse")}
if(!require("devtools")) {install.packages("devtools"); devtools::install_github("cbail/textnets")}
if(!require("textnets")) {devtools::install_github("cbail/textnets"); library("textnets")}
## Warning in library(package, lib.loc = lib.loc, character.only = TRUE,
## logical.return = TRUE, : there is no package called 'textnets'
## ggrepel     (0.8.0 -> 0.8.1) [CRAN]
## hunspell    (NA    -> 3.0  ) [CRAN]
## janeaustenr (NA    -> 0.1.5) [CRAN]
## networkD3   (NA    -> 0.4  ) [CRAN]
## tidytext    (NA    -> 0.2.0) [CRAN]
## tokenizers  (NA    -> 0.2.1) [CRAN]
## 
##   There is a binary version available but the source version is
##   later:
##         binary source needs_compilation
## ggrepel  0.8.0  0.8.1              TRUE
## 
## 
## The downloaded binary packages are in
##  /var/folders/t1/_sb1n3xn1xz5x02pt1lnlm480000gp/T//RtmpQc9qqr/downloaded_packages
##   
   checking for file ‘/private/var/folders/t1/_sb1n3xn1xz5x02pt1lnlm480000gp/T/RtmpQc9qqr/remotes99f1edd3b06/cbail-textnets-bc688a8/DESCRIPTION’ ...
  
✔  checking for file ‘/private/var/folders/t1/_sb1n3xn1xz5x02pt1lnlm480000gp/T/RtmpQc9qqr/remotes99f1edd3b06/cbail-textnets-bc688a8/DESCRIPTION’ (335ms)
## 
  
─  preparing ‘textnets’:
## 
  
   checking DESCRIPTION meta-information ...
  
✔  checking DESCRIPTION meta-information
## 
  
─  checking for LF line-endings in source and make files and shell scripts
## 
  
─  checking for empty or unneeded directories
## 
  
─  looking to see if a ‘data/datalist’ file should be added
## 
  
─  building ‘textnets_0.1.1.tar.gz’
## 
  
   
## 
## Warning: replacing previous import 'dplyr::union' by 'igraph::union' when
## loading 'textnets'
## Warning: replacing previous import 'dplyr::as_data_frame' by
## 'igraph::as_data_frame' when loading 'textnets'
## Warning: replacing previous import 'dplyr::groups' by 'igraph::groups' when
## loading 'textnets'
theme_set(theme_bw())

Netzwerke mit quanteda

Nachdem wir die notwendigen Bibliotheken gelanden haben, greifen wir eneut auf die Variante des Sherlock Holmes-Korpus zurück, welche die zwölf Romane in 174 Abschnitte gleicher Länge unterteilt. Wir erstellen eine DFM auf Basis von Bigrammen (Zweiwort-Folgen) in der wir die Unterscheidung zwischen Groß- und Kleinschreibung aufrechterhalten.

# Laden der Sherlock Holmes-Daten (bereits als RData-File gespeichert)
load("daten/sherlock/sherlock.absaetze.RData")

# Berechnen einer DFM
meine.dfm <- dfm(korpus, remove_numbers = TRUE, remove_punct = TRUE, remove_symbols = TRUE, tolower = F, ngrams = 2)

Dann Laden wir eine einfache Liste von Personen, welche in den Romanen vorkommen. Durch unterschiedliche Varianten des gleichen Namens (Sherlock Holmes vs. Mr. Holmes vs. Sherlock) ist dies Methode nicht ganz akkurat, es genügt aber für eine einfache Demonstration.

personen <- scan("daten/sherlock/sherlock.personen.txt", what = "char", sep = "\n", quiet = T)
meine.dfm.personen <- dfm_select(meine.dfm, personen, selection = "keep", case_insensitive = F)

Nun bereichnen wir eine Feature Cooccurance Matrix (FCM) welche auf der bereits vorhandenen DFM basiert. Dann plotten wir das Ergebnis als Netzwerk mit der quanteda-eigenen Funktion textplot_network().

meine.fcm <- fcm(meine.dfm.personen)
textplot_network(meine.fcm)
## Registered S3 method overwritten by 'network':
##   method            from    
##   summary.character quanteda

Auch wenn man sich noch nicht eingehend mit Netzwerken beschäftigt hat, lässt sich das Ergebnis leicht intuitiv interpretieren. Zwei Namen (“Knoten”) sind dann mit einer Linie verbunden wenn sie innerhalb des gleichen Dokuments (in diesem Fall also im gleichen Absatz) vorkommen. Verbindungslinien (“Kanten”) sind dort dicker, wo zwei Namen mehrmals gemeinsam erwähnt werden.

Die FCM ist die Grundlage des Netzwerkes. Während die Zeilen einer DFM die Dokumente und die Spalten die Wörter enthalten, enthält eine FCM Wörter als Zeilen und Spalten, und die Anzahl der Kookkurenzen als Zelleninhalt. Stimmen der Inhalt von Zeile und Spalte überein, wird einfach die Gesamtanzahl der Treffer im Korpus bezeichnet.

convert(meine.fcm, to = "data.frame")

Netzwerke mit qtextnets (eigener Code)

Was lässt sich aus der Kookkurenz von Begriffen in einem größeren Korpus ableiten? Wir laden folgend das Bundestags-Korpus und extrahieren zentrale Begriffe nach Redner mittels TD-IDF, um diese Frage zu beantworten. Zudem greifen wir an dieser Stell auf eine Gruppen von Funktionen zurück, die nicht aus quanteda stammen, sondern von dem R-Paket textnets inspiriert sind. Mit Hilfe von textnets lassen sich nicht nur die Beziehungen von Begriffen zu einander, sondern auch die Beziehung von Begriffen und Texten sowie von Texten auf Grundlage geteilter Begriffe darstellen. In unserer Implementation (abgelegt unter verschiedenes/qtextnets.R) gibt es dementsprechend drei Funktionen:

Jede dieser Funktionen erwartet eine DFM als Input und berechnet aus dieser mit dem Paket igraph eine ungerichtetes Netzwerk. Folgend werden die drei Funktionen mit Beispielen illustriert.

Zunächst berechnen wir zwei DFMs: eine nach Parteien (Variable “party”) und eine weitere nach MdB (“speaker_cleaned”).

source("verschiedenes/qtextnets.R", echo = F)
## 
## Attaching package: 'scales'
## The following object is masked from 'package:purrr':
## 
##     discard
## The following object is masked from 'package:readr':
## 
##     col_factor
## 
## Attaching package: 'igraph'
## The following objects are masked from 'package:dplyr':
## 
##     as_data_frame, groups, union
## The following objects are masked from 'package:purrr':
## 
##     compose, simplify
## The following object is masked from 'package:tidyr':
## 
##     crossing
## The following object is masked from 'package:tibble':
## 
##     as_data_frame
## The following object is masked from 'package:quanteda':
## 
##     as.igraph
## The following objects are masked from 'package:stats':
## 
##     decompose, spectrum
## The following object is masked from 'package:base':
## 
##     union
load("daten/bundestag/bundestag.korpus.RData")
korpus.bundestag <- corpus_subset(korpus.bundestag, type == "speech")
meine.dfm.partei <-
  dfm(korpus.bundestag,
      groups = "party",
      remove_numbers = TRUE,
      remove_punct = TRUE,
      remove_symbols = TRUE,
      remove = stopwords("german")) %>%
  dfm_trim(min_termfreq = 7, max_termfreq = 2283) %>%
  dfm_tfidf(.)
meine.dfm.mdb <-
  dfm(korpus.bundestag,
      groups = "speaker_cleaned",
      remove_numbers = TRUE,
      remove_punct = TRUE,
      remove_symbols = TRUE,
      remove = stopwords("german")) %>%
  dfm_trim(min_docfreq = 19, max_docfreq = 311) %>% # min = 3% aller MdBs, max = nicht mehr als die Hälfte aller MdBs
  dfm_tfidf(.)

Nun verwenden zunächst die 50 distinktivsten Begriffe nach TF-IDF, um Unterschiede zwischen den Parteien bezüglich der Begriffsverwendung aufzuzeigen.

netzwerk.begriffe.parteien <- scan("daten/bundestag/netzwerk_begriffe_parteien.txt", what = "char", sep = "\n", quiet = T)
meine.dfm.netzwerk <- dfm_keep(meine.dfm.partei, netzwerk.begriffe.parteien)
textplot_mixednet(meine.dfm.netzwerk, main = "Partei-Begriffsnetzwerk der distinktivsten Begriffe nach TF-IDF im Bundestagskorpus")

Wie man schnell erkennt, sind Begriffe wie “Wertentscheidung” (CSU) oder “Millionärssteuer” (LINKE) recht klar mit der Parteizugehörigkeit verknüpft. Ein änhlicher Ansatz lässt sich auch mit Bezug auf einzelne MdBs anstatt von Parteien wählen, auch hier wieder mit einem sehr kleinen Lexikon von nur 50 Begriffen.

netzwerk.begriffe.mdbs <- scan("daten/bundestag/netzwerk_begriffe_abgeordnete.txt", what = "char", sep = "\n", quiet = T)
meine.dfm.netzwerk <- dfm_keep(meine.dfm.mdb, netzwerk.begriffe.mdbs) %>% 
  dfm_subset(docnames(.) %in% c("Sigmar Gabriel", "Thomas Oppermann", "Hubertus Heil", "Johannes Kahrs", "Dr. Anton Hofreiter", "Katrin Göring-Eckardt", "Volker Beck", "Dr. Joachim Pfeiffer", "Michael Grosse-Brömer", "Dr. Thomas de Maizière", "Alexander Dobrindt", "Max Straubinger", "Dr. Dietmar Bartsch", "Heike Hänsel", "Klaus Ernst", "Dr. Angela Merkel,"))
textplot_mixednet(meine.dfm.netzwerk, main = "MdB-Begriffsnetzwerk der distinktivsten Begriffe nach TF-IDF im Bundestagskorpus", maxdocs = 16)

Es lassen sich sowohl fachliche Schwerpunkte als auch z.T. Rückschlüsse über die Parteizugehörigkeit treffen, wobei das erste Netzwerk hier aufschlussreicher ist. Schließlich lassen sich auch die Beziehungen zwischen Begriffen analysieren, um Verwendungsmuster zu identifizieren. Dabei wird lediglich die Ähnlichkeit, welche Verfahren wie die in Kapitel 2 beschriebeben Funktionen textstat_dist() und textstat_simil() angewendet, um ein Netzwerk zu berechnen.

meine.dfm <- dfm(korpus.bundestag) %>% 
  dfm_keep(., unique(c(netzwerk.begriffe.mdbs, netzwerk.begriffe.parteien)))
textplot_termnet(meine.dfm, "Begriffsnetzwerk distinktivster Begriffe im Bundestagskorpus")

Ein sehr ähnliches Ergebnis lässt sich auch mit der Funktion textplot_network() erzielen, welche auf der gleichen Grundlage beruht. Schließlich lässt sich auch noch ein reines Dokumentennetzwerk plotten, hier als Kreis angeordnet.

sprecher <- names(head(sort(table(korpus.bundestag.stats$speaker_cleaned[korpus.bundestag.stats$type=="speech"]), decreasing = T), 50))
meine.dfm.netzwerk <- dfm_subset(meine.dfm.mdb, docnames(meine.dfm.mdb) %in% sprecher)
textplot_docnet(meine.dfm.netzwerk, main = "MdB-Netzwerk nach geteilten Begriffen im Bundestagskorpus")

Abschließend lässt sich festhalten, das Netzwerke ein weiteres nützliches Werkzeug für die Analyse von Wort- und Textbeziehungen darstellen. Die Netzwerkanalyse kennt natürlich noch zahlreiche weitere relevante Techniken, allerdings besteht zum Teil einer Überlappung mit Metriken für die Erfassung von Wort- und Textähnlichkeit, für die die Netzwerkvisualisierung lediglich eine weitere Präsentationsform bietet. “Gemischte” Netzwerke, etwa aus Akteuren und Begriffen sind insofern ein interessantes Instrument, als dass wir gerade im Bereich der digitalen Kommunikation oftmals den Zusammenhang zwischen Nutzeraccounts oder Quellen zur Begriffswahl untersuchen, also in der Regel zahlreiche relevante Metadaten zu Texten vorliegen.