In der quantitativen Textanalyse gibt es eine Reihe von Algorithmen, die als Grundlage eine Übersetzung von Texten in sogenannte Term-Dokument-Matrizen erfordern. Dies gilt etwa bei Topic-Modellen, aber auch für viele Verfahren des maschinellen Lernens oder für die in der Politikwissenschaft gängigen Wordscore- und Wordfish-Verfahren.
Term-Dokument-Matrizen beruhen auf einem sogenannten 'bag-of-words'-Ansatz: Indem ein Text in einen Vektor mit Zählungen von Worten übersetzt wird, wird dessen grammatikalische Struktur und einschließlich der Sequenz des Textes aufgelöst. Ein Term-Dokument-Matrix führt die Vektor-Repräsentation von Texten zusammen, mit den Worten in den Reihen und Dokumenten in den Spalten. Jede Zelle der Matrix gibt an, wie oft Wort i in Dokument j auftritt.
Technisch müssen Term-Dokument-Matrizen als "dünnbesetzte Matrix" (sparse matrix) realisiert werden, weil bei einem ausdifferenzierten Vokabular bei Weitem nicht jedes Wort in jedem Dokument mindestens einmal auftrifft. Das polmineR-Paket nutzt dabei die TermDocumentMatrix
-Klasse des tm-Pakets, die als geringfügige Modifikation aus der simple_triplet_matrix
des slam-Pakets hervorgeht.
Ein Teil der im Folgenden verwendeten Funktionen (Berechnung aller Kookkurrenzen in einem Korpus/einer Partition) sind im polmineR-Paket ab Version 0.7.10.9006 enthalten. Bei Bedarf wird die polmineR-Entwicklungsversion installiert.
Die Beispiele des Foliensatzes basieren auf dem GermaParl-Korpus. Der Datensatz ist nach dem Laden von polmineR verfügbar.
if (packageVersion("polmineR") < package_version("0.7.10.9006")) devtools::install_github("PolMine/polmineR", ref = "dev") library(polmineR)
for (pkg in c("magrittr", "slam", "tm", "quanteda", "quanteda.textmodels", "Matrix", "topicmodels")){ if (!pkg %in% rownames(installed.packages())) install.packages(pkg) library(package = pkg, character.only = TRUE) }
Im polmineR-Paket stehen die Methoden as.TermDocumentMatrix()
und as.DocumentTermMatrix()
zur Verfügung, um Objekte der Klassen TermDocumentMatrix
oder DocumentTermMatrix
zu gewinnen.
Je nachdem, welches Paket für eine weitergehende algorithmische Analyse genutzt wird, können auch Klassen des Matrix-Pakets (sparseMatrix
) oder die document-feature matrix (dfm
) des quanteda-Pakets gefordert sein. Der Weg dahin führt über eine einfache Typumwandlung.
Wichtig für das Verständnis der TermDocumentMatrix
-Klasse des tm-Pakets ist, dass diese letztlich identisch ist mit der simple_triplet_matrix
des slam-Pakets und diesem nur ein Attribut mit der Angabe eines Gewichtungsfaktors hinzufügt. Dies ist grundsätzlich die Term-Frequenz.
Eine simple_triplet_matrix
wird definiert über drei Vektoren i, j, v. Der erste gibt die Reihe eines Wertes an, der zweite die Spalte eines Wertes und der dritte den Wert selbst. Indem nur definierte Werte der Matrix angegeben werden, lässt sich der Speicherplatzbedarf gering halten. Bei vielen Dokumenten und einem großen Vokabular könnten Term-Dokument-Matrizen ansonsten schnell riesig und zu große für den verfügbaren Speicher werden!
Der einfachste Weg zur Gewinnung einer DocumentTermMatrix
ist, die as.DocumentTermMatrix()
-Methode auf ein Korpus anzuwenden. Erforderlich ist nur die Angabe
eines p-Attributs (die token sind dann in den Spalten)
dtm <- polmineR::as.DocumentTermMatrix("GERMAPARL", p_attribute = "word", s_attribute = "date")
partition_bundle
{.smaller}Die beiden zunächst vorgestellten Nutzungsszenarien setzen voraus, dass ein bereits vorhandenes s-Attribut die Gliederung des Korpus / der Partitionen in Dokumente abbildet. Wenn sich die Definition der Dokumente für die Dokument-Term-Matrix erst aus einer Kombination von s-Attributen ergibt, kann die as.DocumentTermMatrix()
-Methode auch flexibel an einem partition_bundle
ansetzen, wobei jede erdenkliche Kombination von s-Attributen für die Bildung der partition
-Objekte im partition_bundle
herangezogen werden kann.
Das folgende Szenario illustriert die Verfahrensschritte. Wichtig ist, dass die partition
-Objekte im partition_bundle
zunächst um eine Zählung über das für die Zählung der Worthäufigkeiten angereichert werden müssen. Bei der as.DocumentTermMatrix()
-Methode gibt man dann über das Argument col
an, aus welcher Spalte (hier: die Zählung) die Zellen der Dokument-Term-Matrix gewonnnen werden.
bt16 <- partition("GERMAPARL", lp = 16, interjection = FALSE) bt16_speakers <- partition_bundle(bt16, s_attribute = "speaker", progress = TRUE) bt16_speakers <- enrich(bt16_speakers, p_attribute = "word", progress = TRUE) dtm <- polmineR::as.DocumentTermMatrix(bt16_speakers, col = "count")
as.speeches()
zum partition_bundle
{.smaller}doit <- !file.exists("~/lab/tmp/lda_bt2015speeches_pos.RData")
Im Fall von Plenarprotokollkorpora ist eine plausible Definition der Dokumente, die einer Dokument-Term-Matrix zugrunde liegen, die einzelne Rede von Abgeordneten. Ein Korpus bzw. ein partition
-Objekt können mit der as.speeches()
-Methode in ein partition_bundle
zerlegt werden.
Diese Zerlegung erfolgt anhand einer Heuristik, nach der als Rede der Beitrag eines Redners an einem Plenartag dient, der höchstens von 500 Worten anderer Redner unterbrochen wird.
Damit wird ausgeschlossen, dass kurze Unterbrechungen (Zwischenrufe, insbesondere auch Zwischenfragen) den Effekt haben, dass einzelne Redepassagen als eigenständige Reden begriffen werden, obwohl sie der Sache nach eine zusammenhängende Rede darstellen. Zugleich kann erkannt werden, wenn ein Sprecher in einer Sitzung zwei oder mehr verschiedene Reden gehalten hat.
bt2015 <- partition("GERMAPARL", year = 2015, interjection = FALSE) bt2015_speeches <- as.speeches(bt2015, s_attribute_date = "date", s_attribute_name = "speaker") bt2015_speeches <- enrich(bt2015_speeches, p_attribute = "word") dtm <- polmineR::as.DocumentTermMatrix(bt2015_speeches, col = "count")
Für die meisten Anwendungsszenarien (z.B. Topic Modelling) wird eine gänzlich ungefilterte Matrix unnötig gross sein, den Rechenaufwand unnötig erhöhen und durch "Rauschen" zu verunreinigten Ergebnissen führen. Es empfiehlt sich, eine Bereinigung um seltene Worte vorzunehmen, Rauschen und auch Worte auf einer Stopwort-Liste zu entfernen.
Mit dem folgenden ersten Filter-Schritt entfernen wir zunächst Dokumente, die unterhalb einer geforderten Mindestlänge bleiben (hier: 100 Worte). Die Länge des Dokuments ermitteln wir durch Aufsummierung der Häufigkeit der Token in den Reihen (row_sums
).
short_docs <- which(slam::row_sums(dtm) < 100) if (length(short_docs) > 0) dtm <- dtm[-short_docs,]
col_sums
). Diese Worte werden aus der Dokument-Term-Matrix (dtm
) entfernt.rare_words <- which(slam::col_sums(dtm) < 5) if (length(rare_words) > 0) dtm <- dtm[,-rare_words]
noise()
-Methode des polmineR-Pakets unterstützt die Identifikation "rauschiger" Worte in einem Vokabular (Token mit Sonderzeichen, Stopworte). Auch diese werden entfernt.noisy_tokens <- noise(colnames(dtm), specialChars = NULL, stopwordsLanguage = "de") noisy_tokens_where <- which(unique(unlist(noisy_tokens)) %in% colnames(dtm)) dtm <- dtm[,-noisy_tokens_where]
stopit <- tm::stopwords("de") stopit_upper <- paste(toupper(substr(stopit, 1, 1)), substr(stopit, 2, nchar(stopit)), sep = "") stopit_upper_where <- which(stopit_upper %in% colnames(dtm)) if (length(stopit_upper_where) > 0) dtm <- dtm[, -stopit_upper_where]
empty_docs <- which(slam::row_sums(dtm) == 0) if (length(empty_docs) > 0) dtm <- dtm[-empty_docs,]
topicmodels
-Paket.lda <- topicmodels::LDA( dtm, k = 200, method = "Gibbs", control = list(burnin = 1000, iter = 3L, keep = 50, verbose = TRUE) )
if (doit == TRUE){ saveRDS(lda, file = "~/lab/tmp/bt2015speeches_lds.RData") } else { lda <- readRDS(file = "~/lab/tmp/bt2015speeches_lds.RData") }
lda_terms <- terms(lda, 10)
n_terms <- 5L lda_terms <- terms(lda, n_terms) y <- t(lda_terms) colnames(y) <- paste("Term", 1:n_terms, sep = " ") DT::datatable(y)
pb <- partition("GERMAPARL", year = 2015, interjection = FALSE) %>% as.speeches(s_attribute_date = "date", s_attribute_name = "speaker") %>% enrich(p_attribute = c("word", "pos"), progress = TRUE) %>% subset(pos == "NN")
pb@objects <- lapply(pb@objects, function(x){x@stat[, "pos" := NULL]; x@p_attribute <- "word"; x})
dtm <- polmineR::as.DocumentTermMatrix(pb, col = "count") short_docs <- which(slam::row_sums(dtm) < 100) if (length(short_docs) > 0) dtm <- dtm[-short_docs,] rare_words <- which(slam::col_sums(dtm) < 5) if (length(rare_words) > 0) dtm <- dtm[,-rare_words] empty_docs <- which(slam::row_sums(dtm) == 0) if (length(empty_docs) > 0) dtm <- dtm[-empty_docs,] lda <- topicmodels::LDA( dtm, k = 200, method = "Gibbs", control = list(burnin = 1000, iter = 3L, keep = 50, verbose = TRUE) )
if (doit == TRUE){ saveRDS(lda, file = "~/lab/tmp/lda_bt2015speeches_pos.RData") } else { lda <- readRDS(file = "~/lab/tmp/lda_bt2015speeches_pos.RData") }
n_terms <- 5L lda_terms <- terms(lda, n_terms) y <- t(lda_terms) colnames(y) <- paste("Term", 1:n_terms, sep = " ") DT::datatable(y)
Es gibt zahlreiche R-Pakete, die computergestütze Textanalysen ermöglichen. Während die meisten Pakete eine Art von Term-Dokumenten-Matrix als Ausgangspunkt nutzen, kann der spezifische Matrixtyp variieren. Methoden des beliebten quanteda
-Paketes nutzen so eine Document-Feature-Matrix als Input. Mittels Typumwandlung können wir aus polmineR
ausgegbene Matrizen in eine Document-Feature-Matrix umwandeln.
Erstellen wir zunächst wie zuvor gezeigt ein partition-bundle
, das wir an dieser Stelle in eine transponierte, 'sparse' Matrix umwandeln.
pb <- partition("GERMAPARL", lp = 16, interjection = FALSE) %>% partition_bundle(s_attribute = "parliamentary_group") pb <- pb[[names(pb)[!names(pb) %in% c("", "fraktionslos")] ]] pb <- enrich(pb, p_attribute = "lemma") dtm <- polmineR::as.sparseMatrix(pb, col = "count") dtm <- Matrix::t(dtm)
Da die Berechnung eines Wordfish Models durchaus speicherintensiv sein kann, filtern wir die erstellte Document-Term-Matrix, um die Datenmenge zu reduzieren. Wir entfernen Stopwords, "noise" (siehe ?noise()
) sowie Worte, die weniger als 10 mal insgesamt vorkommen.
noise_to_drop <- polmineR::noise(colnames(dtm), specialChars = NULL, stopwordsLanguage = "de") noise_to_drop[["stopwords"]] <- c( noise_to_drop[["stopwords"]], paste( toupper(substr(noise_to_drop[["stopwords"]], 1, 1)), substr(noise_to_drop[["stopwords"]], 2, nchar(noise_to_drop[["stopwords"]])), sep = "" ) ) dtm <- dtm[,-which(colnames(dtm) %in% unique(unname(unlist(noise_to_drop))))] # remove rare words terms_to_drop_rare <- which(slam::col_sums(dtm) <= 10) if (length(terms_to_drop_rare) > 0) dtm <- dtm[,-terms_to_drop_rare]
Eine Document-Feature-Matrix ist intern anders geordnet als unsere Matrix. In folgender Typumwandlung wird die Document-Term-Matrix also in eine Document-Feature-Matrix übersetzt.
pg_dfm <- new( "dfm", i = dtm@i, p = dtm@p, x = dtm@x, Dim = dtm@Dim, Dimnames = list( docs = dtm@Dimnames$Docs, features = dtm@Dimnames$Terms ) )
Unter anderem bieten das quanteda.textmodels
Paket eine niedrigschwellige Implementierung von Wordfish. Wordfish ist ein bekanntes Modell zur Skalierung politischer Positionen. Hierbei werden Positionen unüberwacht aus Worthäufigkeiten geschlossen. Für einen Überblick über den zugrundeliegenden Algorithmus und eine Auswahl von Veröffentlichungen, die Wordfish nutzen, siehe hier.
Nun können wir ein erstes Wordfish-Modell berechnen.
wfm_1 <- quanteda.textmodels::textmodel_wordfish(pg_dfm, c(4, 1))
Wir können die summary()
Methode verwenden, um einen ersten Eindruck darüber zu erlangen, wie das Model die Fraktionen im politischen Raum angeordnet hat.
wordfish_summary <- summary(wfm_1)
Für die Analyse interessante Parameter sind theta und beta. Siehe die nächste Folie.
Theta gibt die Position eines Dokumentes (hier alle Debatten einer Fraktion) an.
wordfish_summary$estimated.document.positions
Beta beschreibt den Effekt individueller Wörter auf das Scaling.
head(wordfish_summary$estimated.feature.scores, 4) # show first 4 terms
Beide Werte können auf unterschiedliche Art und Weise visualisiert werden. Für Theta kann die implementierte Dotplot-Darstellung genutzt werden.
quanteda.textplots::textplot_scale1d(wfm_1, doclabels = pg_dfm@Dimnames$docs)
Hier zeigt sich, dass die Interpretation eines Wordfish-Modells sorgsam vorgegangen werden muss. Auf welcher Skala macht diese Skalierung Sinn?
Hier kann es helfen, sich die Betawerte je Term anzuschauen.
betaterm <- data.frame(terms = wfm_1$features, beta = wfm_1$beta)
head(betaterm[order(betaterm$beta),], 10) head(betaterm[order(betaterm$beta, decreasing = TRUE),], 10)
Bekannt sind die sogenannten Eifelturm-Darstellungen, die sich aus dem Wordfish-Modell ergeben. Hierbei wird auf der x-Achse abgetragen, inwiefern ein Wort auf die Skalierung aufläd. Auf der y-Achse wird die geschätzte Wordhäufigkeit psi dargestellt. Für die Skalierung spielen vor allem dokumentspezifische und selten vorkommende Terme eine Rolle, während häufig vorkommende und auf Dokumentebene gleichmäßig verteilte Begriffe eine geringe Rolle spielen.
quanteda.textplots::textplot_scale1d(wfm_1, margin = "features", highlighted = c("Solidität", "Vermögenssteuer", "Klimakiller", "Industriestandort", "Freiheit", "Solidarität"))
Die Abbildung wird auf der nächsten Folie angezeigt.
quanteda.textplots::textplot_scale1d(wfm_1, margin = "features", highlighted = c("Solidität", "Vermögenssteuer", "Klimakiller", "Industriestandort", "Freiheit", "Solidarität"))
Ob Term-Dokument-Matrix, Dokument-Term-Matrix oder Document-Feature-Matrix, ob sparse oder nicht: Die Darstellung von Texten in Matrizen mit Worten auf der einen und Dokumenten auf der anderen Seite ist für eine Vielzahl von Anwendungsbereichen in der computergestützen Textanalyse von enormer Bedeutung. polmineR bietet hierbei die Möglichkeit, Korpora in diese Formen zu überführen und für weitere Analysen nutzbar zu machen. Zu beachten ist, dass in dieser Darstellung Wortzusammenhänge aufgelöst werden. Diese bag-of-words-Ansätze stehen so im Gegensatz zum hermeneutisch-interpretativen von beispielsweise Keyword-in-Context-Analysen. Ein triangulatives Vorgehen kann im Sinne der Validierung deshalb nur angeraten werden.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.