Vom 'bag-of-words' zur algorithmischen Textanalyse {.smaller}

Initialisierung {.smaller}

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)
}

Dünnbesetzte, umwandelbare Matrizen {.smaller}

Direttissima {.smaller}

dtm <- polmineR::as.DocumentTermMatrix("GERMAPARL", p_attribute = "word", s_attribute = "date")

Flexibilität qua partition_bundle {.smaller}

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")

Mit as.speeches() zum partition_bundle {.smaller}

doit <- !file.exists("~/lab/tmp/lda_bt2015speeches_pos.RData")
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")

Schrumpfung der Matrix {.smaller}

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]

Weitere Filter-Schritte {.smaller}

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]

Berechnung eines Topic Models {.smaller}

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/bt2015speeches_lds.RData")
} else {
  lda <- readRDS(file = "~/lab/tmp/bt2015speeches_lds.RData")
}
lda_terms <- terms(lda, 10)

Topic-Term-Matrix {.smaller}

n_terms <- 5L
lda_terms <- terms(lda, n_terms)
y <- t(lda_terms)
colnames(y) <- paste("Term", 1:n_terms, sep = " ")
DT::datatable(y)

Filtern anhand von Part-of-Speech-Annotationen {.smaller}

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})

Das nächste Topic-Modell {.smaller}

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")
}

Topic-Term-Matrix {.smaller}

n_terms <- 5L
lda_terms <- terms(lda, n_terms)
y <- t(lda_terms)
colnames(y) <- paste("Term", 1:n_terms, sep = " ")
DT::datatable(y)

Datentransformation: Zur Document-Feature-Matrix I {.smaller}

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)

Datentransformation: Zur Document-Feature-Matrix II - Filtern {.smaller}

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]

Datentransformation: Zur Document-Feature-Matrix III - Typumwandlung {.smaller}

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
  )
)

Anwendungsfall Wordfish I {.smaller}

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.

Anwendungsfall Wordfish II {.smaller}

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.

Anwendungsfall Wordfish II (Fortsetzung) {.smaller}

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

Anwendungsfall Wordfish III {.smaller}

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)

Anwendungsfall Wordfish IV {.smaller}

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.

Anwendungsfall Wordfish V {.smaller}

quanteda.textplots::textplot_scale1d(wfm_1, margin = "features",
                 highlighted = c("Solidität", "Vermögenssteuer", "Klimakiller",
                                "Industriestandort", "Freiheit", "Solidarität"))

Fazit {.smaller}

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.

Literatur {.smaller}



PolMine/UCSSR documentation built on June 13, 2022, 10:23 p.m.