Stand: 4. April 2022

Die Kunst des Zählens

  • Die Möglichkeiten der Arbeit mit Korpora gehen weit über das Zählen hinaus. Worte (und komplexere lexikalische Einheiten) zu zählen, ist jedoch die grundlegende Operation für komplexere algorithmische Analysen und kann bereits selbst zu aussagekräftigen Analysen führen.

  • Das Zählen kann als Messvorgang verstanden werden. Wie bei allen anderen Auswertungsschritten sollte jede Zähloperation mit der Frage nach der Validität verbunden sein. Ist sichergestellt, dass ich messe, was ich meine zu messen? Gerade durch die Variation der Ausdrucksmöglichkeiten natürlicher Sprache ist das nicht trivial.

  • Es ist zu unterscheiden zwischen absoluten Häufigkeiten (count) und relativen Frequenzen (frequencies, Normalisierung der Häufigkeit durch Division mit Korpus- bzw. Subkorpusgröße, meist als freq abgekürzt). In Analysen ist inhaltlich zu begründen, weshalb man mit Häufigkeiten oder Frequenzen arbeitet.

  • Die grundlegenden hier erläuterten Methoden sind count(), dispersion() und as.TermDocumentMatrix(). Wie alle anderen Basis-Methoden des polmineR-Pakets sind diese für Korpora, corpus und partition-Objekte verfügbar.

  • Als Beispiele dienen u.a.: Die Gewinnung von Zeitreihen, die diktionärsbasierte Klassifikation von Partitionen.

Initialisierung

  • Die Beispiele basieren auf dem GermaParl-Korpus. Der Datensatz ist nach dem Laden von polmineR verfügbar.
library(polmineR)
  • Außerdem werden die Pakete data.table, xts und lubridate benutzt. Bei Bedarf werden diese installiert und dann geladen.
for (pkg in c("data.table", "xts", "lubridate"))
  if (!pkg %in% rownames(installed.packages())) install.packages(pkg)

library(data.table)
library(xts)
  • Hinweis: lubridate laden wir hier nicht, um Konflikte mit gleichlautenden Funktionen des data.table-Pakets zu vermeiden.

Grundlagen des Zählens: Die count()-Methode

  • Die einfachste Operation ist, mit der count()-Methode die Häufigkeit des Auftretens eines Begriffs (query) zu zählen.
count("GERMAPARL", query = "Fluchtursachen")
##             query count         freq
## 1: Fluchtursachen   709 7.018849e-06
  • Die Spalte count gibt die (absolute) Häufigkeit an, die Spalte freq die (relative) Frequenz. Die Frequenz ergibt sich, indem man die Häufigkeit durch die Korpusgröße teilt.
count("GERMAPARL", query = "Fluchtursachen")[["count"]] / size("GERMAPARL")
## [1] 7.018849e-06
  • Als query kann auch ein character-Vektor mit mehreren Suchanfragen übergeben werden.
count("GERMAPARL", query = c("Fluchtursachen", "Herkunftsländer"))
##              query count         freq
## 1:  Fluchtursachen   709 7.018849e-06
## 2: Herkunftsländer   209 2.069026e-06

Nutzung der Ergebnisse eines Zählvorgangs

  • In einem kleinen Beispiel wollen wir ansehen, wie häufig bestimmte Begriffe genutzt werden.
queries <- c(
  "Asylanten", "Asylbewerber", "Asylsuchende", "Aslyberechtigte",
  "Flüchtlinge", "Geflüchtete", "Migranten", "Schutzsuchende"
  )
dt <- count("GERMAPARL", query = queries)
  • Der Rückgabewert der count()-Methode ist ein data.table. Dieser kann verlustfrei in einen data.frame umgewandelt werden, den wir sortieren.
df <- as.data.frame(dt)
df <- df[order(df$count, decreasing = TRUE),] # Sortierung
  • Damit sind wir auch schon so weit, ein Balkendiagramm erstellen zu können (folgende Folie).
par(mar = c(8,4,2,2)) # Vergrößerung Rand unten => genug Platz für Beschriftung
barplot(height = df$count, names.arg = df$query, las = 2)

Häufigkeit von Begriffen zu Asyl und Flucht

count()-Methode und partition-Objekte

  • Die count()-Methode kann auf partition()-Objekte genauso wie auf Korpora angewendet werden.
bt2015 <- partition("GERMAPARL", year = 2015)
count(bt2015, query = "Flüchtlinge")
##          query       match count         freq
## 1: Flüchtlinge Flüchtlinge  2171 0.0004184849
  • Es ist übrigens möglich, die partition()-Methode und die count()-Methode in einer “Pipe” zu verbinden (Diese wird durch das magrittr Paket bereitgestellt, das mit polmineR installiert wird). Eine Pipe ermöglicht, Funktionen oder Methoden mit dem Pipe-Operator (%>%) zu verketten, wobei dann der Rückgabewert einer Methode zum (ersten) Argument der darauffolgenden Methode werden kann.
partition("GERMAPARL", year = 2015) %>%
  count(query = "Flüchtlinge")
##          query       match count         freq
## 1: Flüchtlinge Flüchtlinge  2171 0.0004184849

Beispiel: Variation des Sprachgebrauchs

  • Das illustrieren wir durch eine kleine Analyse der Variation des Sprachgebrauchs der Fraktionen zu Asyl und Flucht in 2015.
queries <- c("Flüchtlinge", "Asylbewerber", "Asylsuchende", "Geflüchtete", "Migranten")

par(
  mar = c(8,5,2,2), # Anpassung Ränder => Beschriftung vollständig sichtbar
  mfrow = c(2,2) # Ausgabe verschiedener Balkendiagramme in ein Feld
)

for (pg in c("CDU/CSU", "GRUENE", "SPD", "LINKE")){
  dt <- partition("GERMAPARL", parliamentary_group = pg, year = 2016) %>%
    count(query = queries)
  barplot(
    height = dt$freq * 100000, names.arg = dt$query, # Beschriftung mit Suchbegriffen
    las = 2, # Drehung Beschriftung um 90 Grad für Lesbarkeit
    main = pg,
    xlab = "Frequenz der Begriffe (pro 100.000 Token)",
    ylim = c(0, 50) # einheitliche Skalierung y-Achse für Vergleichbarkeit
    )
}

Sprachliche Variation zwischen Parteien

Nutzung von regulären Ausdrücken und CQP

  • Die count()-Methode akzeptiert für das Argument query auch die Syntax des Corpus Query Processor (CQP). Diese wird noch in einem folgenden Foliensatz erklärt! In ihrer einfachsten Verwendung lassend sich mit CQP reguläre Ausdrücke verwenden. Der Suchbegriff wird dann in einfache Anführungszeichen gesetzt und das Argument cqp auf TRUE gesetzt.
count("GERMAPARL", query = "'Flüchtling.*'", cqp = TRUE) # mit CQP-Syntax
##             query count         freq
## 1: 'Flüchtling.*' 14690 0.0001454258
  • Eine Aufschlüsselung, was mit einem regulären Ausdruck getroffen wird, erhält man, wenn das Argument breakdown auf TRUE gesetzt wird.
dt <- count("GERMAPARL", query = "'Flüchtling.*'", cqp = TRUE, breakdown = TRUE)
  • Dieses Ergebnis (Tabelle auf der folgenden Seite) ist eine Warnung, dass wir in den Beispielen zuvor mit den einfachen Suchbegriffen Variation des Sprachgebrauchs sehr ungenau gemessen haben! Denn wir sehen hier - unter anderem die Flektionen von “Flüchtling” (neben “Flüchtlinge”: “Flüchtlingen”, “Flüchtling” etc.).

Treffer für regulären Ausdruck

Zählung über positionale Attribute

  • Es gibt zwei Lösungen für das Problem, dass die Worte in einem Korpus mit verschiedenen Flektionen auftreten können: Man kann mit der Lemmatisierung arbeiten, die über das positionale Attribute ‘lemma’ angesprochen werden kann, oder treffsichere reguläre Ausdrücke entwickeln.

  • Zur Erinnerung: “Lemmatisierung” bedeutet, dass eine Wortform auf die nicht flektierte Grundform zurückgeführt wird. CWB-indizierte Korpora, das von polmineR verwendete Datenformat, können das positionale Attribut ‘lemma’ enthalten. Bei der count()-Methode wird auf dieses durch Angabe des Argument p_attribute (Wert: “lemma”) zugegriffen.

count("GERMAPARL", query = "Flüchtling", p_attribute = "lemma")
##         query count         freq
## 1: Flüchtling 10231 0.0001012833
  • Dies entspricht - fast - den Varianten, die wir bei der Aufschlüsselung auf der vorangegangenen Seite gesehen haben (durch breakdown = TRUE): Über das Lemma werden neben “Flüchtling” auch “Flüchtlinge”, “Flüchtlingen” auch kleingeschriebene Varianten erfasst (“flüchtling”, “flüchtlings”, “flüchtlinge”), die über Unsauberkeiten im Korpus auftreten (vgl. Anhang).

Oder doch reguläre Ausdrücke?

  • Bei “Geflüchtete” treffen wir jedoch auf folgendes Problem. Bei Wortneuschöpfungen wie dieser (“Geflüchtete” tritt im Bundestag regelmäßig erst ab 2012 auf) ist die Lemmatisierung oftmals nicht erfolgreich. Dies sehen wir, wenn wir mit der klassischen grep()-Funktion in den auftretenden Wortformen bzw. den Lemmata suchen.
terms("GERMAPARL", p_attribute = "word") %>% grep("Geflüchtet", ., value = TRUE)
## [1] "Geflüchteten"           "Geflüchtete"            "Geflüchteter"          
## [4] "Geflüchtetenhilfe"      "Geflüchtetenmannschaft" "DDR-Geflüchteten"
terms("GERMAPARL", p_attribute = "lemma") %>% grep("Geflüchtet", ., value = TRUE)
## character(0)
  • Die Flektionen von “der/die Geflüchtete” treffen wir am besten mit dem regulären Ausdruck “Geflüchtete(|r|n)”. Wir erzielen so 266 Treffer (statt 92 ohne CQP/regulären Ausdruck), was der Sache nach einen deutlichen Unterschied macht.
count("GERMAPARL", query = '"Geflüchtete(|r|n)"', cqp = TRUE)
##                  query count         freq
## 1: "Geflüchtete(|r|n)"   266 2.633306e-06

Sprachliche Variation: Matching von Flektionen

  • Das Beispiel von gerade eben führen wir nun noch einmal durch mit regulären Ausdrücken, die Flektionen erfassen. Vielleicht sind die Unterschiede doch etwas größer als zunächst angenommen?
queries <- c(
  Flüchtlinge = '"Flüchtling(|e|s|en)"',
  Asylbewerber = '"Asylbewerber(|s|n|in|innen)"',
  Asylsuchende = '"Asylsuchende(|n|r)"',
  Geflüchtete = '"^Geflüchtete(|r|n)$"',
  Migranten = '"^Migrant(|en)$"'
  )
par(mar = c(6,5,2,2), mfrow = c(2,2),  cex = 0.6)
for (pg in c("CDU/CSU", "GRUENE", "SPD", "LINKE")){
  partition("GERMAPARL", parliamentary_group = pg, year = 2015:2016, interjection = FALSE) %>%
    count(query = unname(queries), cqp = TRUE, p_attribute = "word") -> dt
  barplot(
    height = dt$freq * 100000,
    names.arg = names(queries),
    las = 2, main = pg,
    ylim = c(0, 50)
  )
}

Sprachliche Variation, Zweiter Anlauf

Zwischenfazit und “Learnings”

  • Worte zu zählen geht schnell und man hat schnell eine nette Visualisierung fabriziert. Valide Aussagen selbst über scheinbar einfache Dinge (wie sprachliche Variation zwischen Parteien oder Sprachwandel über Zeit) erfordern gleichwohl Sorgfalt, wie die vorangegangenen Beispiele zeigen.

  • Mit der Lemmatisierung im Korpus zu arbeiten, kann eine effiziente Lösung sein, um die Flektionen eines Wortes im Korpus zu erfassen. Ein mögliches Problem ist jedoch, dass Wortneuschöpfungen unter Umständen nicht lemmatisiert werden konnten.

  • Eine mögliche Alternative ist die sorgfältige Entwicklung regulärer Ausdrücke, um damit verschiedene sprachliche Varianten zu erfassen. Die Potentiale der CQP-Syntax wurden hier nur angerissen. Relevant kann insbesondere noch die Möglichkeit sein, Mehrworteinheiten zu erfassen (z.B. “Menschen mit Migrationshintergrund”).

Häufigkeitsverteilungen

  • Diachrone und synchrone Analysen von Sprache sind bei der Analyse von Korpora von grundlegender Bedeutung. Sie dienen der Untersuchung von Sprachwandel (“diachron”, d. h. im Zeitverlauf) und der Varation des Sprachgebrauchs (zur gleichen Zeit, also “synchron”) zwischen Akteuren.

  • Die dispersion()-Methode ermöglicht die effiziente Zählungen von Häufigkeiten über ein oder zwei Dimensionen (konkret: S-Attribute).

dt <- dispersion("GERMAPARL", query = "Flüchtlinge", s_attribute = "year")
head(dt) # wir betrachten nur den Anfang der Tabelle
##          query year count
## 1: Flüchtlinge 1996   217
## 2: Flüchtlinge 1997   129
## 3: Flüchtlinge 1998   151
## 4: Flüchtlinge 1999   368
## 5: Flüchtlinge 2000   161
## 6: Flüchtlinge 2001   127
  • Die Nutzung der CQP-Syntax und regulärer Ausdrücke ist bei der dispersion()-Methode wie bei der count()-Methode möglich.

Einfache Visualisierung der Häufigkeiten

  • Anders als bei der count()-Methode kann mit dem Argument freq angefordert werden, dass als Normalisierung (relative) Frequenzen berechnet werden sollen (freq = TRUE).
dt <- dispersion("GERMAPARL", query = "Flüchtlinge", s_attribute = "year", freq = TRUE)
  • Der Rückgabewert der dispersion()-Methode ist wie bei der count()-Methode ein data.table. Die verlustfreie Umwandlung mit as.data.frame() ist möglich.

  • Das Ergebnis der Verteilungsanalyse lässt sich schnell und einfach als Balkendiagramm visualisieren. (Aus der Abbildung auf der folgenden Folie geht die Resonanz des Themas Flucht und Asyl im Bundestag deutlich hervor.)

barplot(
  height = dt[["freq"]] * 100000,
  names.arg = dt[["year"]],
  las = 2, ylab = "Treffer pro 100.000 Worte"
  )

Flucht und Asyl im Bundestag, nach Jahren

Häufigkeitsverteilung über zwei Dimensionen

  • Der Analyse fügen wir jetzt noch als zweite Dimension eine Differenzierung nach Parteien hinzu.
dt <- dispersion("GERMAPARL", query = '"[fF]lüchtling(|e|s|en)"', cqp = TRUE, s_attribute = c("year", "party"))
## Warning in .local(.Object, ...): There is a zero-length character vector for
## s_attribute party, this will result in a column V1 (V2, V3, ...).
  • Für die Arbeit mit Zeitreihen-Daten nutzen wir das xts-Paket. Wir erzeugen nun ein xts-Objekt auf Basis der vorliegenden Kreuztabelle mit den Häufigkeiten und schauen, wie das aussieht.
ts <- xts(x = dt[,c("CDU", "CSU", "FDP", "GRUENE", "SPD")],
          order.by = as.Date(sprintf("%s-01-01", dt[["year"]]))
          )
head(ts)
##            CDU CSU FDP GRUENE SPD
## 1996-01-01  60  15  37     69  74
## 1997-01-01  37  15  22     47  30
## 1998-01-01  48   3  35     56  44
## 1999-01-01 115  16  15     59 215
## 2000-01-01  38   4  14     54  79
## 2001-01-01  18   3   8     51  79

Visualisierung mit xts

  • Besser sind die Dinge über ein Zeitreihen-Diagramm zu erkennen.
plot.xts(
  ts,
  multi.panel = TRUE,
  col = c("black",
          "black",
          "blue",
          "green",
          "red"),
  lwd = 2,
  yaxs = "r"
  )

Eine datumsgenaue Zeitreihe

par(mar = c(4,2,2,2))
dt <- dispersion("GERMAPARL", query = '"[fF]lüchtling(|e|s|en)"', cqp = TRUE, s_attribute = "date")
dt <- dt[!is.na(as.Date(dt[["date"]]))]
ts <- xts(x = dt[["count"]], order.by = as.Date(dt[["date"]]))
plot(ts)

  • Ist das schon aussagekräftig genug? Wir sollten daher die Zählung auf einen größeren Zeitraum aggregieren.

Aggregation nach Woche - Monat - Quartal - Jahr

  • Als Zeiteinheit für eine Aggregation über den einzelnen Tag hinaus werden wir Woche, Monat, Quartal und Jahr verwenden. Für die Wochen brauchen wir das lubridate-Paket.

  • Nun legen wir aggregierte Zeitreihenobjekte an. Der Code hierfür ist bewusst kompakt und vielleicht nicht auf Anhieb verständlich. Im Zweifelsfall … per copy & paste nutzen!

ts_week <- aggregate(ts, {a <- lubridate::ymd(paste(lubridate::year(index(ts)), 1, 1, sep = "-")); lubridate::week(a) <- lubridate::week(index(ts)); a})
ts_month <- aggregate(ts, as.Date(as.yearmon(index(ts))))
ts_qtr <- aggregate(ts, as.Date(as.yearqtr(index(ts))))
ts_year <- aggregate(ts, as.Date(sprintf("%s-01-01", gsub("^(\\d{4})-.*?$", "\\1", index(ts)))))
  • Welche Aggregation der Zeitreihe ist aussagekräftig? Dafür plotten wir die Zeitreihen in einem 2*2-Feld.
par(mfrow = c(2,2), mar = c(2,2,3,1))
plot(as.xts(ts_week), main = "Aggregation: Woche")
plot(as.xts(ts_month), main = "Aggregation: Monat");
plot(as.xts(ts_qtr), main = "Aggregation: Quartal")
plot(as.xts(ts_year), main = "Aggregation: Jahr")

Aggregation nach Woche - Monat - Quartal - Jahr

Arbeit mit Zeitreihen: “Learnings”

  • Die Analyse von Verteilungen nach verschiedenen strukturellen Attributen ist Grundlage diachroner und synchroner Analysen. Schwerpunkt der Beispiele waren Zeitreihen-Daten. Hierfür empfiehlt es sich, mit spezialisierten Paketen (wie xts oder zoo) zu arbeiten.

  • Sprachliche Zeitreihen-Daten sind Beobachtungen, die unregelmäßig gemacht werden. Temperatur-Messungen werden täglich durchgeführt, doch der Bundestag tagt nicht täglich und Zeitungen erscheinen meist an Sonn- und Feiertagen nicht. Daher ist es für die Analyse und Visualisierung relevant, eine Aggregation der Daten für ein größeres Zeitintervall als den Tag durchzuführen (Woche, Monat, Quartal, Jahr). Auch deswegen empfiehlt es sich, spezialisierte Pakete (xts oder zoo) zu verwenden.

  • Gerade diachrone Analysen sollten den möglichen Bedeutungswandel von Begriffen im Blick behalten: War vor zehn oder zwanzig Jahren mit einem politischen Schlagwort das gemeint, was heute darunter verstanden wird? Mit Zählungen valide zu messen, wird oft auch bedeuten, eben nicht nur zu zählen, sondern zumindest über stichprobenartige Konkordanzanalysen sicherzustellen, dass relevanter Bedeutungswandel nicht übersehen wird.

Diktionärsbasierte Klassifikation I

Für Fortgeschrittene

Zählungen können nicht nur über Korpora und partition-Objekte durchgeführt werden, sondern auch über partition_bundle-Objekte. Dafür gibt es verschiedene Einsatzszenarien. Hier folgt ein Basis-Rezept für eine diktionärsbasierte Klassifikation. Der erste Schritt ist, ein partition_bundle mit den nach Daten und Tagesordnungspunkten unterteilten Partitionen eines Korpus aufzubereiten (hier nur für 2016).

bt2016 <- partition("GERMAPARL", year = 2016)
pb <- partition_bundle(bt2016, s_attribute = "date")
nested <- lapply(
  pb@objects,
  function(x) partition_bundle(x, s_attribute = "agenda_item", verbose = F)
)
debates <- flatten(nested)
names(debates) <- paste(
  blapply(debates, function(x) s_attributes(x, "date")),
  blapply(debates, function(x) name(x)), 
  sep = "_"
)

Diktionärsbasierte Klassifikation II

  • Als (Pseudo-)Diktionär verwenden wir hier eine primitive Liste mit vier Schlagworten.
dict <- c("Asyl", "Flucht", "Flüchtlinge", "Geflüchtete")
  • Wir führen eine Zählung über das “debates”-partition_bundle durch und sortieren das daraus resultierende data.table in absteigender Reihenfolge. Das partition_bundle mit allen Debatten kann mit den Namen jener Partitionen indiziert werden, deren Diktionärs-Score über einem Schwellenwert (hier: 25) liegen.
dt <- count(debates, query = dict) %>% setorderv(cols = "TOTAL", order = -1L)
debates_mig <- debates[[ subset(dt, TOTAL >= 25)[["partition"]] ]]
  • Für die Festlegung eines Schwellenwerts zwischen einschlägigen und nicht-einschlägigen Debatten kann es hilfreich sein, eine Visualisierung (z.B. Balkendiagramm, barplot(height = dt[["TOTAL"]])) heranzuziehen. Eine Volltextanzeige mit Hervorhebung von Termen eines Diktionärs kann am Ende zur Validierung der Auswahlentscheidungen herangezogen werden.
debates_mig[[1]] %>% read() %>% highlight(yellow = dict)

Zählen aller Worte in Korpus / Partition

  • Wird bei der count()-Methode das Argument query nicht angegeben, so wird eine Zählung über das gesamte Korpus bzw. ein partition-Objekt durchgeführt. Mit dem Argument p_attribute wird angegeben, über welches positionale Attribut (P-Attribut) gezählt werden soll. Der Rückgabewert einer solchen Zählung ist ein count-Objekt.
p <- partition("GERMAPARL", year = 2008, interjection = FALSE)
cnt <- count(p, p_attribute = "word")
sum(cnt[["count"]]) == size(p)
## [1] TRUE
  • Es ist möglich, mehr als nur ein P-Attribute bei dem Argument p_attribute anzugeben. Meist wird das eine Kombination von “word” und “pos”, oder von “lemma” und “pos” sein. Eine solche Zählung kann mit der subset()-Methode gefiltert werden, siehe dazu das folgende Beispiel.
bt2008 <- partition("GERMAPARL", year = 2008, interjection = FALSE)
dt <- count(bt2008, p_attribute = c("word", "pos")) %>% subset(pos %in% c("NN", "ADJA")) %>%
  as.data.table() %>% setorderv(cols = "count", order = -1L) %>% head()

Gekonnt Zählen (nicht nur) für Algorithmen

  • Zählungen aller Token in einer Partition sind Grundlage zum Beispiel für Verfahren der Term-Extraktion, oder können Grundlage für die Aufbereitung von Term-Dokument-Matrizen sein, die etwa als Ausgangspunkt einer Anwendung von topicmodel-Algorithmen dienen.

  • Im polmineR-Paket ist die as.TermDocumentMatrix()-Methode der Standard-Weg zur Aufbereitung von Term-Dokument-Matrizen. Die Methode kann auf count_bundle- oder partition_bundle-Objekte angewendet werden, oder auf einen character-Vektor, der ein Korpus angibt. Siehe hierzu die Dokumentation der genannten Methoden!

  • Das Zählen ist von grundlegender Bedeutung bei der Analyse von Korpora. Diese Folien sollten vermitteln, wie das mit polmineR umgesetzt wird. Die wichtige Botschaft: Auch diese scheinbar einfache Operation kann ohne konzeptionelle Überlegungen und sprachliches Fingerspitzengespür zu schlechter Forschung ohne Validitätsanspruch führen.

  • Für die Validierung von Zählergebnissen kann die Nutzung von Konkordanzen (nächster Foliensatz) entscheidend sein. Die CQP-Syntax, die hier nur angerissen wurde, wird im übernächsten Foliensatz erläutert.

Anhang

  • Welche Wortformen werden von einem Lemma erfasst?
word <- get_token_stream("GERMAPARL", p_attribute = "word")
Encoding(word) <- registry_get_encoding("GERMAPARL")
lemma <- get_token_stream("GERMAPARL", p_attribute = "lemma")
Encoding(lemma) <- registry_get_encoding("GERMAPARL")

dt <- data.table(word = word, lemma = lemma)

token <- "Flüchtling"
q <- iconv(token, from = "UTF-8", to = "latin1")
dt2 <- dt[lemma == q]
dt2[, .N, by = .(word)]

Literatur

Baker, Paul. 2008. Using Corpora in Discourse Analysis, Ch. 3. Continuum Discourse Series. London: Continuum International Publishing.

Manderscheid, Katharina. 2019. “Text Mining.” In Handbuch Methoden Der Empirischen Sozialforschung, 1103–16. Wiesbaden: Springer Fachmedien Wiesbaden.

Manheim, Jarol B. 2008. “Empirical Political Analysis : Quantitative and Qualitative Research Methods.” New York [u.a]: Pearson Longman.

Proksch, Sven-Oliver. 2020. “Computergestützte Textanalysen.” In Handbuch Methoden Der Politikwissenschaft, 817–35. Wiesbaden: Springer Fachmedien Wiesbaden.