Dieses erste Kapitel liefert einen Überblick über zahlreiche Funktionen des Pakets quanteda, die gleichzeitig die Grundlage der automatisierten Inhaltsanalyse mit R bilden. Über quanteda hinaus werden im Verlauf dieser neunteiligen Einführung noch eine Reihe weiterer R-Bibliotheken verwendet, etwa für das Berechnen von Themenmodellen (Kapitel 5) und das überwachte maschinelle Lernen (Kapitel 6). In praktisch jeden Einheit relevant sind dabei die Pakete des tidyverse (vor allem ggplot, dplyr, stringr), durch die zahlreiche Funkionen wie Plotten, Textverarbeitung und Datenmanagement gegenüber den R-Basisfunktionen stark verbessert werden. Pakete für einzelne Teilbereiche die erst später eine Rolle spielen werden sind u.a. topicmodels und stm (Themenmodelle), RTextTools (überwachtes maschinelles Lernen), und spacyr (POS-Tagging und Named-Entity-Erkennung).
Die Basis der Analyse in diesem ersten Kapitel sind die beliebten Geschichten von Sherlock Holmes. Das Sherlock Holmes-Korpus besteht aus zwölf Erzählungen, die in dem 1892 erschienenem Band The Adentures of Sherlock Holmes zusammengefasst sind, und die man gemeinfrei unter anderem durch das Internet Archive herunterladen kann. Die für diese Einführung verwendete Fassung wurde zunächst dem Internet Archive entnommen und dann in zwölf Einzeldateien aufgeteilt. Natürlich können die vorgestellten Methoden auf die anderen hier behandelten Korpora angewandt werden – das Beispiel dient nur dazu, sich langsam an quanteda und die Grundlagen der computergestützen Inhaltsanalyse zu gewöhnen.
Sämtliche in dieser Einführung verwendeter Codebeispiele, Korpora und Lexika können hier heruntergeladen werden.
Zunächst werden die notwendigen Bibliotheken installiert (sofern noch nicht vorhanden) und anschließend geladen. Zudem wird vorbereitend die Theme-Einstellung für das Paket ggplot gesetzt (dies sorgt für hübschere Plots). Diesen Schritt wiederholen wir zu Beginn jedes Kapitels, in einigen Kapiteln werden noch weiteren Pakete gelanden.
# Installation und Laden der Bibliotheken
if(!require("quanteda")) install.packages("quanteda")
Lade nötiges Paket: quanteda
Package version: 1.3.4
Parallel computing: 2 of 4 threads used.
See https://quanteda.io for tutorials and examples.
Attache Paket: ‘quanteda’
The following object is masked from ‘package:utils’:
View
if(!require("readtext")) install.packages("readtext")
Lade nötiges Paket: readtext
if(!require("tidyverse")) install.packages("tidyverse")
Lade nötiges Paket: tidyverse
[30m── [1mAttaching packages[22m ─────────────────────────────────────────────────────────────────────── tidyverse 1.2.1 ──[39m
[30m[32m✔[30m [34mggplot2[30m 3.0.0 [32m✔[30m [34mpurrr [30m 0.2.5
[32m✔[30m [34mtibble [30m 1.4.2 [32m✔[30m [34mdplyr [30m 0.7.6
[32m✔[30m [34mtidyr [30m 0.8.1 [32m✔[30m [34mstringr[30m 1.3.1
[32m✔[30m [34mreadr [30m 1.1.1 [32m✔[30m [34mforcats[30m 0.3.0[39m
[30m── [1mConflicts[22m ────────────────────────────────────────────────────────────────────────── tidyverse_conflicts() ──
[31m✖[30m [34mdplyr[30m::[32mfilter()[30m masks [34mstats[30m::filter()
[31m✖[30m [34mdplyr[30m::[32mlag()[30m masks [34mstats[30m::lag()[39m
if(!require("RColorBrewer")) install.packages("RColorBrewer")
Lade nötiges Paket: RColorBrewer
theme_set(theme_bw())
Nachdem alle notwendigen Pakete geladen wurden, können wir nun die Daten einlesen und daraus ein quanteda-Korpus erstellen. Für das Einlesen der Plaintext-Dateien wird die Funktion readtext aus dem gleichnamigen Paket verwendet, durch die sich eine Reihe von Dateiformaten erfolgreich einlesen lassen (u.a. TXT, PDF, Word). Anschließend wird die Endung “.txt” aus dem Dokumentnamen entfernt. Schließlich wird die Variable korpus aufgerufen, was die wichtigen Eckdaten Dokumentanzahl und Docvars zurückliefert.
daten.sherlock <- readtext("daten/sherlock/romane/[0-9]*.txt") # Dateiname beginnt mit Zahl und endet mit .txt
daten.sherlock$doc_id <- str_sub(daten.sherlock$doc_id, start = 4, end = -5) # Dateiendung weglassen
korpus <- corpus(daten.sherlock, docid_field = "doc_id") # Korpus anlegen
docvars(korpus, "Textnummer") <- sprintf("%02d", 1:ndoc(korpus)) # Variable Textnummer generieren
korpus
Corpus consisting of 12 documents and 1 docvar.
In den folgenden Abschnitten werden häufig bereits verbereite Korpora geladen, d.h. der Befehl corpus() wird hier nicht mehr ausgeführt. Er ist aber im Vorfeld ausgeführt worden, um aus Textdatein auf der Festplatte oder Twitter-Daten in einem R-Data Frame ein quanteda-Korpus zu erstellen.
Die Funktionen ndoc(), ntoken(), ntype() und nsentence() geben die Anzahl der Dokumente, Tokens, Types und Sätze aus. Diese Statistiken können bequem gemeinsam mit Metadaten auf Dokumentebene durch die Funktion summary() zusammengefasst werden. Bei den meisten Korpora, die hier verwendet werden, liegt ein solcher Data Frame mit Statistiken zu jedem Text bereits bei.
korpus.stats <- summary(korpus, n = 1000000)
korpus.stats$Text <- reorder(korpus.stats$Text, 1:ndoc(korpus), order = T)
korpus.stats
Corpus consisting of 12 documents:
Text Types Tokens Sentences Textnummer
A Scandal in Bohemia 2145 10542 669 01
The Red-headed League 2087 11118 573 02
A Case of Identity 1750 8506 396 03
The Boscombe Valley Mystery 2096 11499 636 04
The Five Orange Pips 1925 8879 475 05
The Man with the Twisted Lip 2173 11160 586 06
The Adventure of the Blue Carbuncle 1926 9651 552 07
The Adventure of the Speckled Band 2232 11783 614 08
The Adventure of the Engineer's Thumb 1968 9999 508 09
The Adventure of the Noble Bachelor 1944 9987 540 10
The Adventure of the Beryl Coronet 1991 11669 626 11
The Adventure of the Copper Beeches 2110 12011 622 12
Source: /Users/cp/Dropbox/Lehre/inhaltsanalyse-mit-r/* on x86_64 by cp
Created: Sat Sep 8 14:39:49 2018
Notes:
Das Funktionsargument n = 1000000 wird hier nur deshalb verwendet, weil die Funktion summary() ansonsten nur maximal 100 Texte zusammenfasst. In diesem Fall reicht das zwar aus, aber bei größeren Datensätzen ist das eher unpraktisch. Technisch gesehen heißt diese Funktion summary.corpus() und ist eine an Korpus-Objekte angepasste Variante der Basisfunktion summary(), die auch sonst in R verwendet wird. Der Befehl reorder() wird verwendet um die Texte nach ihrer Nummerierung zu sortieren, statt alphabetisch nach Titel.
Der Inhalt der Variable korpus.stats kann natürlich auch geplottet werden, um einen anschaulichen Eindruck von der Korpusbeschaffenheit zu geben. Die folgenden Zeilen liefern die Anzahl der Tokens (laufende Wörter), die Anzahl der Types (einmalige Wörter), und Sätze pro Roman zurück (vgl. dazu diese Einführung). Schließlich wird noch das Verhältnis von Typen zu Tokens (oder die sog. Typ-Token-Relation) geplottet.
Grundlage solcher Plots sind praktisch immer Data Frame-Objekte (also Tabellen), die Informationen über Korpora, Texte, Wörter, Themen usw. enthalten, welche sich visuell darstellen lassen. Im Rest dieser Einführung gehe ich nicht im Detail darauf ein, wie die jeweiligen Plots genau konstruiert werden, allerdings lassen sich die meisten Daten auch (etwas weniger ansprechend) mit der R-internen Funktion plot() darstellen. Eine hilfreiche deutschsprachige Einführung in das Plotten mit ggplot2 findet sich hier. Viele der hier vorgestellten Plots stammen zudem direkt aus quanteda (beginnend mit textplot_).
ggplot(korpus.stats, aes(Text, Tokens, group = 1)) + geom_line() + geom_point() + theme(axis.text.x = element_text(angle = 45, vjust = 1, hjust = 1)) + ggtitle("Tokens pro Roman")
ggplot(korpus.stats, aes(Text, Types, group = 1)) + geom_line() + geom_point() + theme(axis.text.x = element_text(angle = 45, vjust = 1, hjust = 1)) + ggtitle("Types pro Roman")
ggplot(korpus.stats, aes(Text, Sentences, group = 1)) + geom_line() + geom_point() + theme(axis.text.x = element_text(angle = 45, vjust = 1, hjust = 1)) + ggtitle("Sätze pro Roman")
ggplot(korpus.stats, aes(Tokens, Types, group = 1, label = Textnummer)) + geom_smooth(method = "lm", se = FALSE) + geom_text(check_overlap = T) + ggtitle("Typ-Token-Relation pro Roman")
Diese Grafiken sind zunächst einmal nicht umwerfend informativ. Sie belegen lediglich, dass die Erzählungen ‘A Case of Identity’ und (in geringerem Maße) ‘The Five Orangen Pips’ deutlich kürzer sind als die anderen Texte, was sich auf allen drei Ebenen (Tokens, Types, Sätze) niederschlägt. Etwas interessanter wird es allerdings bei der Typ-Token-Relation: während drei Romane (mit den Nummern 3, 11 und 12) jeweils einen eher unterdurchschnittlichen TTR aufweisen, liegen weitere vier oberhalb der linearen Relation (1, 5, 6 und 8), während die verbleibenden sechs ziemlich genau dem Durchschnitt entsprechen. Über den TTR lassen sich Rückschlüssen über die Informationsdichte ziehen – dazu später noch mehr.
Korpora lassen sich in quanteda sehr leicht samplen, umformen und mit zusätlichen Metadaten versehen. Metadaten können wiederum genutzt werden, um das Korpus nach bestimmten Kriterien zu filtern.
str_sub(korpus[1], start = 1, end = 1000) # Anfang des ersten Romans wiedergeben
[1] "A Scandal in Bohemia\n\n To Sherlock Holmes she is always the woman. I have seldom\nheard him mention her under any other name. In his eyes she\neclipses and predominates the whole of her sex. It was not that\nhe felt any emotion akin to love for Irene Adler. All emotions,\nand that one particularly, were abhorrent to his cold, precise but\nadmirably balanced mind. He was, I take it, the most perfect\nreasoning and observing machine that the world has seen, but as\na lover he would have placed himself in a false position. He\nnever spoke of the softer passions, save with a gibe and a sneer.\nThey were admirable things for the observer -- excellent for draw-\ning the veil from men's motives and actions. But for the trained\nteasoner to admit such intrusions into his own delicate and finely\nadjusted temperament was to introduce a distracting factor which\nmight throw a doubt upon all his mental results. Grit in a\nsensitive instrument, or a crack in one of his own high-power\nlenses, would not be more "
Jeder Text lässt sich also anhand seiner Indizierung (etwa korpus[1] für den ersten Text) aufrufen.
Mittels corpus_reshape() lässt sich ein Korpus so umformen, dass jeder Satz ein eigenes Dokument ergibt. Alternative Argumente sind ‘paragraphs’ und ‘documents’ (so lässt sich ein Satz-Korpus wieder in seinen Anfangszustand zurückversetzen). Die Erstellung von Satz-Korpora ist für die Sentimentanalyse und das überwachte maschinelle Lernen von Interesse.
Die Beschriftung des Beispiels besteht hier aus der Variable docname und einer angehängten Zahl (eine 1 für den ersten Satz).
korpus.saetze <- corpus_reshape(korpus, to = "sentences")
korpus.saetze[1]
A Scandal in Bohemia.1
"A Scandal in Bohemia To Sherlock Holmes she is always the woman."
Mit corpus_sample() kann weiterhin ein zufälliges Sample aus dem Satz-Korpus gezogen werden.
zufallssatz <- corpus_sample(korpus.saetze, size = 1)
zufallssatz[1]
The Adventure of the Speckled Band.567
"With a grave face he lit the lamp and led the way down the corridor."
Anhand von corpus_subset kann ein Korpus nach Metadaten gefiltert werden. Hier geschieht dies mittels der neu erstellten binären Variable LangerSatz, die dann TRUE ist, wenn ein Satz >= 25 Tokens enthält). So lässt sich ein Teilkorpus zu bilden, in dem nur längere Sätze enthalten sind. Korpora lassen sich mit corpus_segment() auch nach bestimmten Kriterien aufspalten
docvars(korpus.saetze, "Zeichenanzahl") <- ntoken(korpus.saetze)
docvars(korpus.saetze, "LangerSatz") <- ntoken(korpus.saetze)>=25
korpus.saetze_lang <- corpus_subset(korpus.saetze, LangerSatz == TRUE)
korpus.saetze_lang[1:3]
A Scandal in Bohemia.6
"He was, I take it, the most perfect reasoning and observing machine that the world has seen, but as a lover he would have placed himself in a false position."
A Scandal in Bohemia.9
"But for the trained teasoner to admit such intrusions into his own delicate and finely adjusted temperament was to introduce a distracting factor which might throw a doubt upon all his mental results."
A Scandal in Bohemia.10
"Grit in a sensitive instrument, or a crack in one of his own high-power lenses, would not be more disturbing than a strong emotion in a nature such as his."
Unter Tokensiierung versteht man die Aufspaltung eines Textes in laufende Wörter oder sog. N-Gramme, also Sequenzen mehrerer Wörter in Folge. Die Funktion tokens() realisiert die Tokenisierung eines Korpus in quanteda.
meine.tokens <- tokens(korpus)
head(meine.tokens$`A Scandal in Bohemia`)
[1] "A" "Scandal" "in" "Bohemia" "To" "Sherlock"
Mittels der Funktion tokens lässt sich der Text über das Argument ngrams auch gleich in N-Gramme (Mehrwortsequenzen) aufspalten. Im folgenden Beispiel werden erst Bigramme von Anfang des ersten Textes angezeigt, und dann alle Sequenzen von einem, zwei oder drei Begriffen extrahiert (durch die Anwendung von head() sehen wir nur Trigramme, es sind aber auch kürzere Sequenzen vorhanden).
meine.tokens <- tokens(korpus, ngrams = 2)
head(meine.tokens$`A Scandal in Bohemia`)
[1] "A_Scandal" "Scandal_in" "in_Bohemia" "Bohemia_To" "To_Sherlock" "Sherlock_Holmes"
meine.tokens <- tokens(korpus, ngrams = 1:3)
head(meine.tokens$`A Scandal in Bohemia`)
[1] "A" "Scandal" "in" "Bohemia" "To" "Sherlock"
Hilfreich ist auch die Möglichkeit, bei der Tokenisierung bestimmte Begriffe zu entfernen oder zurückzubehalten.
meine.tokens <- tokens(korpus)
begriffe.behalten <- tokens_select(meine.tokens, c("holmes", "watson")) # Platzhalter mit padding = TRUE
head(begriffe.behalten$`A Scandal in Bohemia`)
[1] "Holmes" "Holmes" "Holmes" "Holmes" "Watson" "Watson"
begriffe.entfernen <- tokens_remove(meine.tokens, c("Sherlock", "in", "is", "the"))
head(begriffe.entfernen$`A Scandal in Bohemia`)
[1] "A" "Scandal" "Bohemia" "To" "Holmes" "she"
Die Funktion tokens() akzeptiert eine Reihe von Argumenten, mit denen ganze Klassen von Zeichenketten (Zahlen, Interpunktion, Symbole usw.) ausgeschlossen oder zurückbehalten werden können. Folgend werden zunächst Zahlen, Interpunktion und Symbole entfernt, dann mittels tokens_tolower() alle Wörter in Kleinschreibung umgewandelt und dann dann noch die Wörter ‘sherlock’ und ‘holmes’, sowei eine Reihe englischer Stoppwörter entfernt.
meine.tokens <- tokens(korpus, remove_numbers = TRUE, remove_punct = TRUE, remove_symbols = TRUE)
meine.tokens <- tokens_tolower(meine.tokens)
meine.tokens <- tokens_remove(meine.tokens, c(stopwords("english"), "sherlock", "holmes"))
head(meine.tokens$`A Scandal in Bohemia`)
[1] "scandal" "bohemia" "always" "woman" "seldom" "heard"
Das Resultat ist der Art von Daten mit denen man bei Verfahren wie der Anwendung von Lexika (Kapitel 2), der Berechnung von Themenmodellen (Kapitel 3) und dem überwachten maschinelle Lernen (Kapitel 4) arbeitet sehr ähnlich. Durch die Stoppwortentfernung und andere Schritte gehen syntaktische Informationen verloren, d.h. man kann nicht mehr nachvollziehen, wer was mit wem tut, oder wie der Text insgesamt argumentativ oder erzählerisch aufgebaut ist. Diese Informationen sind allerdings im ‘Bag-of-Words-Ansatz’, der in der automatisierten Inhaltsanalyse nahezu immer verwendet wird, nicht unbedingt relevant.
Wir kommen nun zu einer zentralen Datenstruktur von quanteda, die im Gegensatz zu den zuvor vorgestellten Einheiten praktisch in jedem Projekt vorkommt: die Document Feature-Matrize (DFM). Üblicherweise wird direkt nachdem ein Korpus angelegt wurde berechnet Eine DFM ist eine Tabelle, deren Zeilen diee Texte und deren Spalten die Wortfrequenzen enhalten. Dabei gehen Informationen darüber, wo in einem Text ein Wort vorkommt verloren (man spricht auch vom ‘Bag-of-Words-Ansatz’). Immer dann, wenn wir uns für die Beziehung von Wörtern zu Texten (und umgekehrt) interessieren, berechnen wir eine DFM.
meine.dfm <- dfm(korpus, remove_numbers = TRUE, remove_punct = TRUE, remove_symbols = TRUE, remove = stopwords("english"))
meine.dfm
Document-feature matrix of: 12 documents, 8,489 features (79.1% sparse).
Wichtig: Hier wird implizit der uns schon vertraute Befehl tokens() angewandt, um bestimmte Features zu entfernen. Vieles funktioniert bei DFMs analog zur Erstellung eines Korpus. So zählen die Funktionen ndoc() und nfeat() Dokumente und Features (Wörter).
ndoc(meine.dfm)
[1] 12
nfeat(meine.dfm)
[1] 8489
Mittels der Funktionen docnames() und featnames() lassen sich die Namen der Dokumente und Features ausgeben.
head(docnames(meine.dfm)) # In der DFM enthaltene Dokumente
[1] "A Scandal in Bohemia" "The Red-headed League" "A Case of Identity"
[4] "The Boscombe Valley Mystery" "The Five Orange Pips" "The Man with the Twisted Lip"
head(featnames(meine.dfm), 50) # Features in chronologischer Reihenfolge
[1] "scandal" "bohemia" "sherlock" "holmes" "always" "woman" "seldom"
[8] "heard" "mention" "name" "eyes" "eclipses" "predominates" "whole"
[15] "sex" "felt" "emotion" "akin" "love" "irene" "adler"
[22] "emotions" "one" "particularly" "abhorrent" "cold" "precise" "admirably"
[29] "balanced" "mind" "take" "perfect" "reasoning" "observing" "machine"
[36] "world" "seen" "lover" "placed" "false" "position" "never"
[43] "spoke" "softer" "passions" "save" "gibe" "sneer" "admirable"
[50] "things"
Die tabellarische Ansicht illustriert den Inhalt der DFM als Text-Wort-Matrix am besten. Die sparsity (“Spärlichkeit”) einer DFM beschreibt dabei den Anteil der leeren Zellen, also Wörter, die nur in sehr wenigen Texten vorkommen. Wie sich leicht ableiten lässt, werden DFMs sehr schnell sehr groß. Zum Glück macht sich quanteda eine Reihe von für den Nutzer unsichtbaren Funktionen aus anderen Paketen zunutze, um diesem Problem zu begegnen.
head(meine.dfm, n = 12, nf = 10) # Features und Texte als Matrix in chronologischer Reihenfolge
Document-feature matrix of: 12 documents, 10 features (30.8% sparse).
12 x 10 sparse Matrix of class "dfm"
features
docs scandal bohemia sherlock holmes always woman seldom heard mention name
A Scandal in Bohemia 4 8 11 47 5 12 3 8 1 6
The Red-headed League 0 0 10 51 5 0 0 15 0 6
A Case of Identity 0 2 7 46 7 10 0 5 0 1
The Boscombe Valley Mystery 1 0 10 43 5 1 0 10 0 3
The Five Orange Pips 1 0 10 25 5 1 0 5 1 5
The Man with the Twisted Lip 0 0 10 28 4 5 0 8 0 4
The Adventure of the Blue Carbuncle 0 0 10 34 5 0 1 3 0 10
The Adventure of the Speckled Band 0 0 9 55 8 5 1 20 0 6
The Adventure of the Engineer's Thumb 0 0 5 12 0 6 1 11 0 3
The Adventure of the Noble Bachelor 1 0 7 34 3 8 0 11 0 6
The Adventure of the Beryl Coronet 4 0 3 26 3 5 0 12 0 8
The Adventure of the Copper Beeches 0 1 2 42 7 8 0 5 0 4
Gleich an den ersten Blick fällt auf, das die Wörter ‘sherlock’ und ‘holmes’ in allen Romanen vorkommen, also sehr wenig distinktiv sind, weshalb wir sie unter Umständen zu den Stoppwörtern für dieses Korpus hinzufügen sollten.
Die Funktion topfeatures() zählt Features in der gesamten DFM aus. Die Funktion textstat_frequency() liefert zusätzlich noch den Rang (rank), die Anzahl der Dokumente, in denen das Feature vorkommt (docfreq) sowie Metadaten, nach denen bei der Zählung gefiltert wurde.
topfeatures(meine.dfm) # Features nach Frequenz
said upon holmes one man mr little now see may
485 465 443 372 290 275 269 234 229 197
worthaeufigkeiten <- textstat_frequency(meine.dfm) # Worthäufigkeiten
head(worthaeufigkeiten)
DFMs lassen sich mit dfm_sort leicht nach Dokument- und Feature-Frequenzen sortieren.
head(dfm_sort(meine.dfm, decreasing = TRUE, margin = "both"), n = 12, nf = 10)
Document-feature matrix of: 12 documents, 10 features (0% sparse).
12 x 10 sparse Matrix of class "dfm"
features
docs said upon holmes one man mr little now see may
The Adventure of the Speckled Band 44 41 55 33 11 5 17 21 22 19
The Adventure of the Copper Beeches 47 33 42 36 34 44 37 18 17 21
The Boscombe Valley Mystery 37 42 43 31 41 24 25 16 24 19
The Man with the Twisted Lip 28 54 28 36 30 20 21 27 18 15
The Adventure of the Beryl Coronet 45 33 26 32 27 20 22 29 20 25
The Red-headed League 51 50 51 29 25 55 25 14 23 8
A Scandal in Bohemia 33 25 47 27 23 9 14 17 15 21
The Adventure of the Engineer's Thumb 47 38 12 33 17 11 25 16 16 9
The Adventure of the Noble Bachelor 33 29 34 31 10 17 26 16 16 18
The Adventure of the Blue Carbuncle 43 38 34 38 37 17 24 33 27 7
The Five Orange Pips 32 47 25 29 19 3 5 12 16 24
A Case of Identity 45 35 46 17 16 50 28 15 15 11
Weiterhin lassen sich bestimmte Features einer DFM gezielt mittels dfm_select auswählen.
dfm_select(meine.dfm, pattern = "lov*")
Document-feature matrix of: 12 documents, 7 features (67.9% sparse).
12 x 7 sparse Matrix of class "dfm"
features
docs love lover lovely loves loved lovers loving
A Scandal in Bohemia 5 1 1 1 1 0 0
The Red-headed League 1 0 0 0 0 0 0
A Case of Identity 2 0 0 0 0 1 0
The Boscombe Valley Mystery 1 0 1 0 1 0 0
The Five Orange Pips 1 0 0 0 0 0 0
The Man with the Twisted Lip 0 0 0 0 0 0 0
The Adventure of the Blue Carbuncle 2 0 0 0 0 0 0
The Adventure of the Speckled Band 1 0 1 0 0 0 0
The Adventure of the Engineer's Thumb 1 0 0 0 0 0 0
The Adventure of the Noble Bachelor 1 2 1 0 0 0 1
The Adventure of the Beryl Coronet 3 4 0 2 3 0 2
The Adventure of the Copper Beeches 0 0 1 1 0 0 0
Die Funktion dfm_wordstem() reduziert Wörter auf ihre Stammform. Diese Funktion existiert in quanteda derzeit nur für Englisch und ist auch dort nur begrenzt zuverlässig, was die folgende Ausgabe gut illustriert (‘holm’ ist kein Wortstamm).
meine.dfm.stemmed <- dfm_wordstem(meine.dfm)
topfeatures(meine.dfm.stemmed)
said upon holm one man mr littl see now come
485 465 460 383 304 275 269 253 234 207
Ebenso wie bei Wortfrequenzen in Korpora ist die Gewichtung einer DFM nach relativen Wortfrequenzen und Verfahren wie TF-IDF oftmals sinnvoll. Die Gewichtung einer DFM funktioniert immer aufgrund der Wort-Text-Relation, weshalb topfeatures() in Kombination mit dfm_weight() merkwürdige Resultate produziert. Relative Frequenzen und TF-IDF sind nur kontrastiv innerhalb der Text in einem Korpus sinnvoll (hier für ‘A Scandal in Bohemia’), da für das gesamte Korpus relative Frequenz == absolute Frequenz
meine.dfm.proportional <- dfm_weight(meine.dfm, scheme = "prop")
topfeatures(meine.dfm) # absolute Frequenzen für das gesamte Korpus
said upon holmes one man mr little now see may
485 465 443 372 290 275 269 234 229 197
topfeatures(meine.dfm.proportional) # ...ergibt wenig Sinn
said upon holmes one man mr little now see may
0.12564554 0.12042666 0.11388128 0.09559304 0.07426168 0.07127181 0.06916820 0.06024412 0.05900373 0.05064617
topfeatures(meine.dfm.proportional[1,]) # ...ergibt mehr Sinn
holmes said one upon man may photograph street know
0.012339197 0.008663691 0.007088475 0.006563402 0.006038330 0.005513258 0.004988186 0.004725650 0.004725650
now
0.004463114
Im zweiten Beispiel sehen wir etwa, dass ‘A Scandal in Bohemia’ einen leicht höheren Anteil von Nennungen der Wortes ‘holmes’ hat, als dies im Gesamtkorpus der Fall ist. Dazu später noch etwas mehr.
Die Gewichtungsansätze Propmax und TF-IDF liefern relevante Wortmetriken, zum Beispiel für die Bestimmung von Stoppwörtern. Propmax skaliert die Worthäufigkeit relativ zum frequentesten Wort (hier ‘holmes’). Funktional ähneln sich TF-IDF und der später vorgestellte Keyness-Ansatz – beide finden besonders distinktive Terme.
meine.dfm.propmax <- dfm_weight(meine.dfm, scheme = "propmax")
topfeatures(meine.dfm.propmax[1,])
holmes said one upon man may photograph street know now
1.0000000 0.7021277 0.5744681 0.5319149 0.4893617 0.4468085 0.4042553 0.3829787 0.3829787 0.3617021
meine.dfm.tfidf <- dfm_tfidf(meine.dfm)
topfeatures(meine.dfm.tfidf)
simon rucastle mccarthy coronet lestrade hosmer clair k hunter wilson
42.08807 36.69216 34.53380 29.13789 28.01345 24.82117 24.82117 22.66281 22.66281 21.58362
Schließlich lässt sich mit dfm_trim() noch eine reduzierten Dokument-Feature-Matrix erstellen. Das ist dann sinnvoll, wenn man davon ausgeht, dass beispielsweise nur solche Begriffe eine Rolle spielen, die mindestes X mal im Gesamtkorpus vorkommen. Auch eine Mindestzahl oder ein Maximum an Dokumenten, in denen ein Begriff vorkommen muss oder darf, kann bestimmt werden. Schließlich lassen sich beide Filteroptionen auch proportional anwenden (vgl. Beispiel).
meine.dfm.trim <- dfm_trim(meine.dfm, min_docfreq = 11) # Features, die mindestens in 11 Romanen vorkommen
head(meine.dfm.trim, n = 12, nf = 10)
Document-feature matrix of: 12 documents, 10 features (2.5% sparse).
12 x 10 sparse Matrix of class "dfm"
features
docs sherlock holmes always heard name eyes whole felt one cold
A Scandal in Bohemia 11 47 5 8 6 9 4 2 27 2
The Red-headed League 10 51 5 15 6 10 9 4 29 1
A Case of Identity 7 46 7 5 1 6 3 3 17 1
The Boscombe Valley Mystery 10 43 5 10 3 6 2 0 31 1
The Five Orange Pips 10 25 5 5 5 5 2 2 29 1
The Man with the Twisted Lip 10 28 4 8 4 11 3 2 36 2
The Adventure of the Blue Carbuncle 10 34 5 3 10 2 0 4 38 5
The Adventure of the Speckled Band 9 55 8 20 6 11 1 2 33 3
The Adventure of the Engineer's Thumb 5 12 0 11 3 4 4 4 33 1
The Adventure of the Noble Bachelor 7 34 3 11 6 4 4 3 31 2
The Adventure of the Beryl Coronet 3 26 3 12 8 10 6 4 32 1
The Adventure of the Copper Beeches 2 42 7 5 4 9 7 2 36 1
meine.dfm.trim <- dfm_trim(meine.dfm, min_termfreq = 0.95, termfreq_type = "quantile") # Features im 95. Häufigkeitsperzentil (=Top 5% aller Features)
head(meine.dfm.trim, n = 12, nf = 10)
Document-feature matrix of: 12 documents, 10 features (4.17% sparse).
12 x 10 sparse Matrix of class "dfm"
features
docs sherlock holmes always woman heard name eyes whole felt one
A Scandal in Bohemia 11 47 5 12 8 6 9 4 2 27
The Red-headed League 10 51 5 0 15 6 10 9 4 29
A Case of Identity 7 46 7 10 5 1 6 3 3 17
The Boscombe Valley Mystery 10 43 5 1 10 3 6 2 0 31
The Five Orange Pips 10 25 5 1 5 5 5 2 2 29
The Man with the Twisted Lip 10 28 4 5 8 4 11 3 2 36
The Adventure of the Blue Carbuncle 10 34 5 0 3 10 2 0 4 38
The Adventure of the Speckled Band 9 55 8 5 20 6 11 1 2 33
The Adventure of the Engineer's Thumb 5 12 0 6 11 3 4 4 4 33
The Adventure of the Noble Bachelor 7 34 3 8 11 6 4 4 3 31
The Adventure of the Beryl Coronet 3 26 3 5 12 8 10 6 4 32
The Adventure of the Copper Beeches 2 42 7 8 5 4 9 7 2 36
DFMs lassen sich u.a. auch als Wortwolke der häufigsten Begriffe darstellen.
textplot_wordcloud(meine.dfm, max_words = 100, scale = c(5,1))
scale is deprecated; use min_size and max_size instead
Interessanter als die Darstellung des Gesamtkorpus ist hier der Vergleich. Das folgende Plot zeigt die distinktivsten Begriffe nach TF-IDF für vier Romane, wobei die Farbe den jeweiligen Roman kennzeichnet. Dass im Plot die Wortgröße nicht die absolute Frequenz anzeigt, sondern den TF-IDF-Wert, macht ein solches Plot für den unmittelbaren Vergleich nützlich.
textplot_wordcloud(meine.dfm.tfidf[1:4,], color = brewer.pal(4, "Set1"), comparison = T)
dfm has been previously weighted