-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy path090_Textmining.Rmd
413 lines (241 loc) · 14.1 KB
/
090_Textmining.Rmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
```{r include=FALSE, cache=FALSE}
set.seed(1014)
options(digits = 3)
knitr::opts_chunk$set(
comment = "#>",
collapse = TRUE,
message = FALSE,
warning = FALSE,
cache = TRUE,
out.width = "70%",
fig.align = 'center',
fig.width = 6,
fig.asp = 0.618, # 1 / phi
fig.show = "hold",
size = "tiny"
)
script <- TRUE
library(methods) # sometimes not loaded, although built-in, appears like a bug
```
```{r include=FALSE, cache=FALSE}
set.seed(1014)
options(digits = 3)
knitr::opts_chunk$set(
comment = "#>",
collapse = TRUE,
message = FALSE,
warning = FALSE,
cache = TRUE,
out.width = "70%",
fig.align = 'center',
fig.width = 6,
fig.asp = 0.618, # 1 / phi
fig.show = "hold",
size = "tiny"
)
script <- TRUE
library(methods) # sometimes not loaded, although built-in, appears like a bug
```
# Vertiefung: Grundlagen des Textmining
```{r echo = FALSE, out.width = "30%", fig.align = "center"}
knitr::include_graphics("images/FOM.jpg")
```
```{r echo = FALSE, out.width = "10%", fig.align = "center"}
knitr::include_graphics("images/licence.png")
```
```{block2, ziele-textmining, type='rmdcaution', echo = TRUE}
Lernziele:
- Sie kennen zentrale Ziele und Begriffe des Textminings.
- Sie wissen, was ein 'tidy text dataframe' ist.
- Sie können Worthäufigkeiten auszählen.
- Sie können Worthäufigkeiten anhand einer Wordcloud visualisieren.
```
In diesem Kapitel benötigte R-Pakete:
```{r}
library(tidyverse) # Datenjudo
library(stringr) # Textverarbeitung
library(tidytext) # Textmining
library(pdftools) # PDF einlesen
library(downloader) # Daten herunterladen
library(lsa) # Stopwörter
library(SnowballC) # Wörter trunkieren
library(wordcloud) # Wordcloud anzeigen
```
Ein großer Teil der zur Verfügung stehenden Daten liegt nicht als braves Zahlenmaterial vor, sondern in "unstrukturierter" Form, z.B. in Form von Texten. Im Gegensatz zur Analyse von numerischen Daten ist die Analyse von Texten weniger verbreitet bisher. In Anbetracht der Menge und der Informationsreichhaltigkeit von Text erscheint die Analyse von Text als vielversprechend.
In gewisser Weise ist das Textmining ein alternative zu klassischen qualitativen Verfahren der Sozialforschung. Geht es in der qualitativen Sozialforschung primär um das Verstehen eines Textes, so kann man für das Textmining ähnliche Ziele formulieren. Allerdings: Das Textmining ist wesentlich schwächer und beschränkter in der Tiefe des Verstehens. Der Computer ist einfach noch (?) wesentlich *dümmer* als ein Mensch, zumindest in dieser Hinsicht. Allerdings ist er auch wesentlich *schneller* als ein Mensch, was das Lesen betrifft. Daher bietet sich das Textmining für das Lesen großer Textmengen an, in denen eine geringe Informationsdichte vermutet wird. Sozusagen maschinelles Sieben im großen Stil. Da fällt viel durch die Maschen, aber es werden Tonnen von Sand bewegt.
In der Regel wird das Textmining als *gemischte* Methode verwendet: sowohl qualitative als auch qualitative Aspekte spielen eine Rolle. Damit vermittelt das Textmining auf konstruktive Art und Weise zwischen den manchmal antagonierenden Schulen der qualitativ-idiographischen und der quantitativ-nomothetischen Sichtweise auf die Welt. Man könnte es auch als qualitative Forschung mit moderner Technik bezeichnen - mit den skizzierten Einschränkungen wohlgemerkt.
## Zentrale Begriffe
Die computergestützte Analyse von Texten speiste (und speist) sich reichhaltig aus Quellen der Linguistik; entsprechende Fachtermini finden Verwendung:
- Ein *Corpus* bezeichnet die Menge der zu analysierenden Dokumente; das könnten z.B. alle Reden der Bundeskanzlerin Angela Merkel sein oder alle Tweets von "\@realDonaldTrump".
- Ein *Token* (*Term*) ist ein elementarer Baustein eines Texts, die kleinste Analyseeinheit, häufig ein Wort.
- Unter *tidy text* versteht man einen Dataframe, in dem pro Zeile nur *ein* Token (z.B. Wort) steht [@Silge2016]. Synonym könnte man von einem "langen" Dataframe sprechen, so wie wir in Kapitel \@ref(Datenjudo) kennen gelernt haben.
## Grundlegende Analyse
### Tidy Text Dataframes
Wozu ist es nützlich, einen Text-Dataframe in einen langen Dataframe umzuwandeln? Der Grund ist, dass immer wenn nur ein Wort (allgemeiner: Term) pro Zelle steht, dann können wir die Spalte einfach auszählen. Wir können z.B. `count` nutzen, um zu zählen, wie häufig ein Wort vorkommt. Sprich: Sobald wir einen langen (Text-)Dataframe haben, können wir unsere bekannte Methoden einsetzen.
Basteln wir uns einen *tidy text* Dataframe. Wir gehen dabei von einem Vektor mit mehreren Text-Elementen aus, das ist ein realistischer Startpunkt. Unser Text-Vektor^[Nach dem Gedicht "Jahrgang 1899" von Erich Kästner] besteht aus 4 Elementen.
```{r}
text <- c("Wir haben die Frauen zu Bett gebracht,",
"als die Männer in Frankreich standen.",
"Wir hatten uns das viel schöner gedacht.",
"Wir waren nur Konfirmanden.")
```
Als nächstes machen wir daraus einen Dataframe.
```{r}
text_df <- data_frame(Zeile = 1:4,
text = text)
```
Diesen Mini-Datensatz finden Sie auch im Ordner `data` als `Brecht.csv`; nach bekannter Manier können Sie die CSV-Datei importieren:
```{r eval = FALSE}
text_df <- read.csv("data/Brecht.csv")
```
```{r echo = FALSE}
knitr::kable(text_df)
```
Übrigens, falls Sie eine beliebige Textdatei einlesen möchten, können Sie das so tun:
```{r}
text <- read_lines("data/Brecht.txt")
```
Der Befehl `read_lines` (aus `readr`^[Teil der Tidyverse-Familie]) liest Zeilen (Zeile für Zeile) aus einer Textdatei.
Dann "dehnen" wir den Dataframe zu einem *tidy text* Dataframe (s. Abb. \@ref(fig:tidytextdf)); das besorgt die Funktion `unnest_tokens`. 'unnest' heißt dabei so viel wie 'Entschachteln', also von breit auf lang dehnen. Mit 'tokens' sind hier einfach die Wörter gemeint (es könnten aber auch andere Analyseeinheiten sein, Sätze zum Beispiel).
```{r tidytextdf, echo = FALSE, fig.cap = "Illustration eines Tidy Text Dataframe"}
knitr::include_graphics("images/textmining/tidytext-crop.png")
```
```{r}
text_df %>%
unnest_tokens(output = wort, input = text) -> tidytext_df
tidytext_df %>% head
```
Der Parameter `output` sagt, wie neue 'saubere' (lange) Spalte heißen soll; `input` sagt der Funktion, welche Spalte sie als ihr Futter (Input) betrachten soll (welche Spalte in tidy text umgewandelt werden soll).
> In einem 'tidy text Dataframe' steht in jeder Zeile ein Wort (token) und die Häufigkeit des Worts im Dokument.
Überprüfen Sie, ob das stimmt: Betrachten Sie den Dataframe `tidytext_df`.
Das `unnest_tokens` kann übersetzt werden als "entschachtele" oder "dehne" die Tokens - so dass in *jeder Zeile* nur noch *ein Wort* (genauer: Token) steht. Die Syntax ist `unnest_tokens(Ausgabespalte, Eingabespalte)`. Nebenbei werden übrigens alle Buchstaben auf Kleinschreibung getrimmt.
Als nächstes filtern wir die Satzzeichen heraus, da die Wörter für die Analyse wichtiger (oder zumindest einfacher) sind.
```{r}
text_df %>%
unnest_tokens(wort, text) %>%
filter(str_detect(wort, "[a-z]"))
```
Das `"[a-z]"` steht für "alle Buchstaben von a-z". In Pseudo-Code heißt dieser Abschnitt:
```{block2, pseudo-unnest, type='rmdpseudocode', echo = TRUE}
Nehme den Datensatz "text_df" UND DANN
dehne die einzelnen Elemente der Spalte "text", so dass jedes Element seine eigene Spalte bekommt.
Ach ja: Diese "gedehnte" Spalte soll "Wort" heißen (weil nur einzelne Wörter drinnen stehen).
Ach ja 2: Diesees "dehnen" wandelt automatisch Groß- in Kleinbuchstaben um. UND DANN
filtere die Spalte "wort", so dass nur noch Kleinbuchstaben übrig bleiben. FERTIG.
```
### Text-Daten einlesen
Nun lesen wir Text-Daten ein; das können beliebige Daten sein^[Ggf. benötigen Sie Administrator-Rechte, um Dateien auf Ihre Festplatte zu speichern.]. Eine gewisse Reichhaltigkeit ist von Vorteil. Nehmen wir das Parteiprogramm der Partei AfD^[ <geladen am 1. März 2017 von https://www.alternativefuer.de/wp-content/uploads/sites/7/2016/05/2016-06-27_afd-grundsatzprogramm_web-version.pdf>]. Vor dem Hintergrund des Erstarkens des Populismus weltweit und der großen Gefahr, die davon ausgeht - man blicke auf die Geschichte Europas in der ersten Hälfte des 20. Jahrhunderts - ~~verdient~~erfordert der politische Prozess und speziell Neuentwicklungen darin unsere besondere Beachtung.
```{r}
afd_pfad <- "data/afd_programm.pdf"
afd_raw <- pdf_text(afd_pfad)
```
Mit `head(afd_raw)` können Sie sich den Beginn dieses Textvektor anzeigen lassen.
Für uns ist `pdf_text` sehr praktisch, da diese Funktion Text aus einer beliebige PDF-Datei in einen Text-Vektor einliest. `head(afd_raw, 1)` liest das 1. Element (und nur das erste) aus `afd_raw` aus.
Der Vektor `afd_raw` hat 96 Elemente (entsprechend der Seitenzahl des Dokuments); zählen wir die Gesamtzahl an Wörtern. Dazu wandeln wir den Vektor in einen tidy text Dataframe um. Auch die Stopwörter entfernen wir wieder wie gehabt.
```{r}
afd_df <- data_frame(Zeile = 1:96,
afd_raw)
afd_df %>%
unnest_tokens(output = token, input = afd_raw) %>%
dplyr::filter(str_detect(token, "[a-z]")) -> afd_df
count(afd_df)
```
Eine substanzielle Menge von Text. Was wohl die häufigsten Wörter sind?
### Worthäufigkeiten auszählen
```{r}
afd_df %>%
na.omit() %>% # fehlende Werte löschen
count(token, sort = TRUE)
```
Die häufigsten Wörter sind inhaltsleere Partikel, Präpositionen, Artikel... Solche sogenannten "Stopwörter" sollten wir besser herausfischen, um zu den inhaltlich tragenden Wörtern zu kommen. Praktischerweise gibt es frei verfügbare Listen von Stopwörtern, z.B. im Paket `lsa`.
```{r}
data(stopwords_de, package = "lsa")
stopwords_de <- data_frame(word = stopwords_de)
stopwords_de <- stopwords_de %>%
rename(token = word)
# Für das Joinen werden gleiche Spaltennamen benötigt
afd_df %>%
anti_join(stopwords_de) -> afd_df
```
Unser Datensatz hat jetzt viel weniger Zeilen; wir haben also durch `anti_join` Zeilen gelöscht (herausgefiltert). Das ist die Funktion von `anti_join`: Die Zeilen, die in beiden Dataframes vorkommen, werden herausgefiltert. Es verbleiben also nicht "Nicht-Stopwörter" in unserem Dataframe. Damit wird es schon interessanter, welche Wörter häufig sind.
```{r}
afd_df %>%
count(token, sort = TRUE) -> afd_count
```
```{r echo = FALSE}
afd_count %>%
top_n(10) %>%
knitr::kable(caption = "Die häufigsten Wörter")
```
Ganz interessant; aber es gibt mehrere Varianten des Themas "deutsch". Es ist wohl sinnvoller, diese auf den gemeinsamen Wortstamm zurückzuführen und diesen nur einmal zu zählen. Dieses Verfahren nennt man "stemming" oder "trunkieren".
```{r}
afd_df %>%
mutate(token_stem = wordStem(.$token, language = "german")) %>%
count(token_stem, sort = TRUE) -> afd_count
afd_count %>%
top_n(10) %>%
knitr::kable(caption = "Die häufigsten Wörter - mit 'stemming'")
```
Das ist schon informativer. Dem Befehl `SnowballC::wordStem` füttert man einen Vektor an Wörtern ein und gibt die Sprache an (Default ist Englisch). Denken Sie daran, dass `.` bei `dplyr` nur den Datensatz meint, wie er im letzten Schritt definiert war. Mit `.$token` wählen wir also die Variable `token` aus `afd_raw` aus.
### Visualisierung
Zum Abschluss noch eine Visualisierung mit einer "Wordcloud" dazu.
```{r debug-workcloud, eval=FALSE}
myword <-na.omit(afd_count$token_stem)
head(myword, 10)
myfreq <- na.omit(afd_count$n)
head(myfreq, 10)
```
```{r show-wordcloud}
wordcloud(words = afd_count$token_stem,
freq = afd_count$n,
max.words = 100,
scale = c(2,.5),
colors=brewer.pal(6, "Dark2"))
```
Man kann die Anzahl der Wörter, Farben und einige weitere Formatierungen der Wortwolke beeinflussen^[https://cran.r-project.org/web/packages/wordcloud/index.html
].
Weniger verspielt ist eine schlichte visualisierte Häufigkeitsauszählung dieser Art, z.B. mit Balkendiagrammen (gedreht).
```{r}
afd_count %>%
top_n(30) %>%
ggplot() +
aes(x = reorder(token_stem, n), y = n) +
geom_col() +
labs(title = "mit Trunkierung") +
coord_flip() -> p1
afd_df %>%
count(token, sort = TRUE) %>%
top_n(30) %>%
ggplot() +
aes(x = reorder(token, n), y = n) +
geom_col() +
labs(title = "ohne Trunkierung") +
coord_flip() -> p2
library(gridExtra)
grid.arrange(p1, p2, ncol = 2)
```
Die beiden Diagramme vergleichen die trunkierten Wörter mit den nicht trunkierten Wörtern. Mit `reorder` ordnen wir die Spalte `token` nach der Spalte `n`. `coord_flip` dreht die Abbildung um 90°, d.h. die Achsen sind vertauscht. `grid.arrange` packt beide Plots in eine Abbildung, welche 2 Spalten (`ncol`) hat.
## Aufgaben^[F, R, F, F, R, R, F, F]
```{block2, exercises-text, type='rmdexercises', echo = TRUE}
Richtig oder Falsch!?
1. Unter einem Token versteht man die größte Analyseeinheit in einem Text.
1. In einem tidytext Dataframe steht jedes Wort in einer (eigenen) Zeile.
1. Eine hinreichende Bedingung für einen tidytext Dataframe ist es, dass in jeder Zeile ein Wort steht (beziehen Sie sich auf den tidytext Dataframe wie in diesem Kapitel erörtert).
1. Gibt es 'Stop-Wörter' in einem Dataframe, dessen Text analysiert wird, so kommt es - per definitionem - zu einem Stop.
1. Mit dem Befehl `unnest_tokens` kann man einen tidytext Dataframe erstellen.
1. Balkendiagramme sind sinnvolle und auch häufige Diagrammtypen, um die häufigsten Wörter (oder auch Tokens) in einem Corpus darzustellen.
1. In einem 'tidy text Dataframe' steht in jeder Zeile ein Wort (token) *aber nicht* die Häufigkeit des Worts im Dokument.
1. Unter 'Stemming' versteht man (bei der Textanalyse), die Etymologie eines Wort (Herkunft) zu erkunden.
```
## Befehlsübersicht
Tabelle \@ref(tab:befehle-text) fasst die R-Funktionen dieses Kapitels zusammen.
```{r befehle-text, echo = FALSE}
df <- readr::read_csv("includes/Befehle_text.csv")
# library(pander)
# pander::cache.off()
# panderOptions("table.alignment.default", "left")
knitr::kable(data.frame(df), caption = "Befehle des Kapitels 'Textmining'")
# hier `pander`, weil `kable` keine breiten Zellen umbricht.
```
## Verweise
- Das Buch *Tidy Text Minig* [@tidytextminig] ist eine hervorragende Quelle vertieftem Wissens zum Textmining mit R.