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.
library(polmineR)
for (pkg in c("data.table", "xts", "lubridate")) if (!pkg %in% rownames(installed.packages())) install.packages(pkg) library(data.table) library(xts)
count()
-Methode {.smaller}count()
-Methode die Häufigkeit des Auftretens eines Begriffs (query
) zu zählen.count("GERMAPARL", query = "Fluchtursachen")
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")
query
kann auch ein character
-Vektor mit mehreren Suchanfragen übergeben werden.count("GERMAPARL", query = c("Fluchtursachen", "Herkunftsländer"))
queries <- c( "Asylanten", "Asylbewerber", "Asylsuchende", "Aslyberechtigte", "Flüchtlinge", "Geflüchtete", "Migranten", "Schutzsuchende" ) dt <- count("GERMAPARL", query = queries)
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
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)
par(mar = c(8,3,2,2)) # Vergrößerung Rand unten => genug Platz für Beschriftung barplot(height = df$count, names.arg = df$query, las = 2)
count()
-Methode und partition
-Objekte {.smaller}count()
-Methode kann auf partition()
-Objekte genauso wie auf Korpora angewendet werden. bt2015 <- partition("GERMAPARL", year = 2015) count(bt2015, query = "Flüchtlinge")
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")
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 ) }
queries <- c("Flüchtlinge", "Asylbewerber", "Asylsuchende", "Geflüchtete", "Migranten") par( mar = c(6,5,2,2), mfrow = c(2,2), cex = 0.6 ) for (pg in c("CDU/CSU", "GRUENE", "SPD", "LINKE")){ p <- partition( "GERMAPARL", parliamentary_group = pg, year = 2016, interjection = FALSE ) dt <- count(p, query = queries) barplot( height = dt$freq * 100000, names.arg = dt$query, las = 2, main = pg, ylim = c(0, 50) ) }
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
breakdown
auf TRUE
gesetzt wird.dt <- count("GERMAPARL", query = "'Flüchtling.*'", cqp = TRUE, breakdown = TRUE)
DT::datatable(dt)
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")
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).grep()
-Funktion in den auftretenden Wortformen bzw. den Lemmata suchen.terms("GERMAPARL", p_attribute = "word") %>% grep("Geflüchtet", ., value = TRUE) terms("GERMAPARL", p_attribute = "lemma") %>% grep("Geflüchtet", ., value = TRUE)
r count("GERMAPARL", query = "Geflüchtete")[["count"]]
ohne CQP/regulären Ausdruck), was der Sache nach einen deutlichen Unterschied macht.count("GERMAPARL", query = '"Geflüchtete(|r|n)"', cqp = TRUE)
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) ) }
queries <- c( Flüchtlinge = '"[fF]lü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) ) }
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").
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
dispersion()
-Methode wie bei der count()
-Methode möglich.par(mfrow = c(1,1))
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" )
barplot( height = dt[["freq"]] * 100000, names.arg = dt[["year"]], las = 2, ylab = "Treffer pro 100.000 Worte" )
dt <- dispersion("GERMAPARL", query = '"[fF]lüchtling(|e|s|en)"', cqp = TRUE, s_attribute = c("year", "party"))
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)
xts
{.columns-2}plot.xts( ts, multi.panel = TRUE, col = c("black", "black", "blue", "green", "red"), lwd = 2, yaxs = "r" )
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)
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)))))
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")
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")
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.
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 = "_" )
dict <- c("Asyl", "Flucht", "Flüchtlinge", "Geflüchtete")
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"]] ]]
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)
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)
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()
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.
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)]
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.