library(tidyverse)
library(tokenizers) # Tokenisieren
library(tidymodels) # Rezepte fĂŒr Textverarbeitung
library(tidyverse)
library(tidytext) # Textanalyse-Tools
library(hcandersenr) # Textdaten: MĂ€rchen von H.C. Andersen
library(SnowballC) # Stemming
library(lsa) # Stopwörter
library(easystats) # Komfort fĂŒr deskriptive Statistiken, wie `describe_distribution`
library(textclean) # Emojis ersetzen
library(wordcloud) # unĂŒbersichtlich, aber manche mögen es
4 Textmining 1
Bild von mcmurryjulie auf Pixabay
4.1 Vorab
4.1.1 Lernziele
- Die vorgestellten Techniken des Textminings mit R anwenden können
4.1.2 Begleitliteratur
Lesen Sie in Hvitfeldt und Silge (2021) Kap. 2 zur Vorbereitung.
4.1.3 Benötigte R-Pakete
4.2 Einfache Methoden des Textminings
Definition 4.1 (Natural Language Processing) Die Analyse von Texten (natĂŒrlicher Sprache) mit Hilfe von Methoden des Maschinellen Lernens bezeichnet man (auch) als Natural Language Processing (NLP).
4.2.1 Tokenisierung
Erarbeiten Sie dieses Kapitel: Hvitfeldt und Silge (2021), Kap. 2
Wie viele Zeilen hat das MĂ€rchen âThe Fir treeâ (in der englischen Fassung?)
4.2.2 Stopwörter entfernen
Erarbeiten Sie dieses Kapitel: s. Hvitfeldt und Silge (2021), Kap. 3
Eine alternative Quelle von Stopwörtern - in verschiedenen Sprachen - biwetet das Paket quanteda
:
word <chr> | ||||
---|---|---|---|---|
aber | ||||
alle | ||||
allem | ||||
allen | ||||
aller | ||||
alles |
Es bestehst (in der deutschen Version) aus 231 Wörtern.
4.2.3 Wörter zÀhlen
Ist der Text tokenisiert, kann man einfach mit âBordmittelnâ die Wörter zĂ€hlen (Bordmittel aus dem Tidyverse, in diesem Fall).
hc_andersen_count <-
hcandersen_de %>%
filter(book == "Das Feuerzeug") %>%
unnest_tokens(output = word, input = text) %>%
anti_join(stop2) %>%
count(word, sort = TRUE)
## Joining with `by = join_by(word)`
hc_andersen_count %>%
head()
word <chr> | n <int> | |||
---|---|---|---|---|
soldat | 35 | |||
sagte | 28 | |||
hund | 23 | |||
prinzessin | 17 | |||
hexe | 16 | |||
feuerzeug | 14 |
Zur Visualisierung eignen sich Balkendiagramme, s. Abbildung fig-hcandersen-count.
Dabei macht es Sinn, aus word
einen Faktor zu machen, denn Faktorstufen kann man sortieren, zumindest ist das die einfachste Lösung in ggplot2
(wenn auch nicht super komfortabel).
Eine (beliebte?) Methode, um WorthĂ€ufigkeiten in Corpora darzustellen, sind Wortwolken, s. Abbildung fig-wordcloud1. Es sei hinzugefĂŒgt, dass solche Wortwolken nicht gerade optimale perzeptorische QualitĂ€ten aufweisen.
wordcloud(words = hc_andersen_count$word,
freq = hc_andersen_count$n,
max.words = 50,
rot.per = 0.35,
colors = brewer.pal(8, "Dark2"))
4.2.4 tf-idf
4.2.4.1 Grundlagen
Was sind âhĂ€ufigeâ Wörter? Wörter wie âundâ oder âderâ sind sehr hĂ€ufig, aber sie nicht spezifisch fĂŒr bestimmte Texte. Sie sind sozusagen âAllerweltswörterâ. Man könnte argumentieren, was wir suchen, sind nicht einfach hĂ€ufige Wörter, sondern Wörter, die relevant sind. Unter ârelevantâ könnte man verstehen âhĂ€ufig aber spezifischâ.
Da kommt tf-idf
ins Spiel (term frequencyâinverse document frequency). tf-idf
ist eine Möglichkeit, Wörter die hĂ€ufig und gleichzeitig spezifisch fĂŒr einen Text sind, aufzufinden.
Betrachten wir als Beispiel Tabelle tbl-shakespeare (Quelle). Das Wort âRomeoâ ist hoch spezifisch in dem Sinne, dass sein idf
hoch ist (relativ zu den anderen Wörtern). Im Gegensatz dazu ist das Wort âgoodâ nicht spezifisch fĂŒr ein bestimmtes StĂŒck von Shakespeare - der IDF-Wert ist sehr klein.
Definition 4.2 (Text Frequency (WorthÀufigkeit)) Die WorthÀufigkeit (VorkommenshÀufigkeit) eines Wortes (oder Terms, Tokens)
Manchmal wird auch eine logarithmierte Version verwendet.
Definition 4.3 (Inverse Document Frequency (Inverse DokumenthÀufigkeit)) Die IDF eines Wortes hÀngt erstens von der Gesamtzahl der Dokumente (nicht Wörter) im Corpus,
Definition 4.4 (TF-IDF) TF-IDF ist ein Maà zur Beurteilung der Relevanz von Wörtern eines Corpus.
4.2.4.2 Beispiel zur tf-idf
#library(hcandersenr)
data(hcandersen_de)
hca_count <-
hcandersen_de |>
unnest_tokens(word, text) |>
count(book, word, sort = TRUE) |> # ZÀhle Wörter pro Buch
ungroup() # um gleich nur nach BĂŒcher zu gruppieren
head(hca_count)
book <chr> | word <chr> | n <int> | ||
---|---|---|---|---|
Die Eisjungfrau | und | 644 | ||
Die Schneekönigin | und | 586 | ||
Moorkönigs Tochter | und | 581 | ||
Eine Geschichte aus den SanddĂŒnen | und | 566 | ||
Die Eisjungfrau | die | 541 | ||
Moorkönigs Tochter | die | 502 |
Die Gesamtzahl der Wörter pro Buch:
words_total <-
hca_count |>
group_by(book) |>
summarise(total = sum(n))
words_total |>
head() # die ersten paar von 150 BĂŒchern
book <chr> | total <int> | ||
---|---|---|---|
"Tanze, tanze, PĂŒppchen mein!" | 352 | ||
Alles am rechten Platz | 2947 | ||
Am Ă€uĂersten Meer | 723 | ||
Anne Lisbeth | 4036 | ||
Aufgeschoben ist nicht aufgehoben | 1016 | ||
Das ABC Buch | 1135 |
Dann fĂŒgen wir die Spalte mit der Gesamt-Wortzahl des jeweiligen Buches zur Tabelle hca_count
hinzu:
hca_count <-
hca_count |>
left_join(words_total)
## Joining with `by = join_by(book)`
head(hca_count)
book <chr> | word <chr> | n <int> | total <int> | |
---|---|---|---|---|
Die Eisjungfrau | und | 644 | 17240 | |
Die Schneekönigin | und | 586 | 11163 | |
Moorkönigs Tochter | und | 581 | 13059 | |
Eine Geschichte aus den SanddĂŒnen | und | 566 | 12635 | |
Die Eisjungfrau | die | 541 | 17240 | |
Moorkönigs Tochter | die | 502 | 13059 |
hca_count <-
hca_count |>
bind_tf_idf(term = word, document = book, n = n)
hca_count |>
arrange(-tf_idf) |>
head()
book <chr> | word <chr> | n <int> | total <int> | |
---|---|---|---|---|
Der Halskragen | halskragen | 20 | 801 | |
Frag die GrĂŒnwarenfrau! | mohrrĂŒbe | 5 | 220 | |
VÀnö und GlÀnö | glÀnö | 14 | 630 | |
Das Liebespaar (Kreisel und Ball) | kreisel | 16 | 782 | |
Der kleine Klaus und der groĂe Klaus | klaus | 101 | 4290 | |
Zwei Jungfern | jungfer | 15 | 574 |
4.2.5 Stemming (Wortstamm finden)
Erarbeiten Sie dieses Kapitel: Hvitfeldt und Silge (2021), Kap. 4
Vertiefende Hinweise zum UpSet plot finden Sie hier, Lex u. a. (2014).
FĂŒr welche Sprachen gibt es Stemming im Paket SnowballC
?
library(SnowballC)
getStemLanguages()
## [1] "arabic" "basque" "catalan" "danish" "dutch"
## [6] "english" "finnish" "french" "german" "greek"
## [11] "hindi" "hungarian" "indonesian" "irish" "italian"
## [16] "lithuanian" "nepali" "norwegian" "porter" "portuguese"
## [21] "romanian" "russian" "spanish" "swedish" "tamil"
## [26] "turkish"
Einfacher Test: Suchen wir den Wordstamm fĂŒr das Wort âwissensdurstigenâ, wie in âdie wissensdurstigen Studentis löcherten dis armi Professiâ1.
wordStem("wissensdurstigen", language = "german")
## [1] "wissensdurst"
Werfen Sie mal einen Blick in das Handbuch von SnowballC.
4.2.6 Fallstudie AfD-Parteiprogramm
Daten einlesen:
d_link <- "https://raw.githubusercontent.com/sebastiansauer/pradadata/master/data-raw/afd_2022.csv"
afd <- read_csv(d_link, show_col_types = FALSE)
Wie viele Seiten hat das Dokument?
nrow(afd)
## [1] 190
Und wie viele Wörter?
Aus breit mach lang, oder: wir tokenisieren (nach Wörtern):
afd %>%
unnest_tokens(output = token, input = text) %>%
filter(str_detect(token, "[a-z]")) -> afd_long
Stopwörter entfernen:
Wörter zÀhlen:
token <chr> | n <int> | |||
---|---|---|---|---|
afd | 174 | |||
deutschland | 113 | |||
wollen | 66 | |||
euro | 60 | |||
bĂŒrger | 57 | |||
eu | 54 |
Wörter trunkieren:
4.2.7 Stringverarbeitung
Erarbeiten Sie dieses Kapitel: Wickham und Grolemund (2016), Kap. 14
4.2.7.1 RegulĂ€rausdrĂŒcke
Das "[a-z]"
in der Syntax oben steht fĂŒr âalle Buchstaben von a-zâ. D iese flexible Art von âString-Verarbeitung mit Jokernâ nennt man RegulĂ€rausdrĂŒcke (regular expressions; regex). Es gibt eine ganze Reihe von diesen RegulĂ€rausdrĂŒcken, die die Verarbeitung von Texten erleichert. Mit dem Paket stringr
geht das - mit etwas Ăbung - gut von der Hand. Nehmen wir als Beispiel den Text eines Tweets:
string <- "Correlation of unemployment and #AfD votes at #btw17: ***r = 0.18***\n\nhttps://t.co/YHyqTguVWx"
Möchte man Ziffern identifizieren, so hilft der ReulÀrausdruck [:digit:]
:
âGibt es mindestens eine Ziffer in dem String?â
str_detect(string, "[:digit:]")
## [1] TRUE
âFinde die Position der ersten Ziffer! Welche Ziffer ist es?â
str_locate(string, "[:digit:]")
## start end
## [1,] 51 51
str_extract(string, "[:digit:]")
## [1] "1"
âFinde alle Ziffern!â
str_extract_all(string, "[:digit:]")
## [[1]]
## [1] "1" "7" "0" "1" "8"
âFinde alle Stellen an denen genau 2 Ziffern hintereinander folgen!â
str_extract_all(string, "[:digit:]{2}")
## [[1]]
## [1] "17" "18"
Der QuantitÀtsoperator {n}
findet alle Stellen, in der der der gesuchte Ausdruck genau
âZeig die Hashtags!â
str_extract_all(string, "#[:alnum:]+")
## [[1]]
## [1] "#AfD" "#btw17"
Der Operator [:alnum:]
steht fĂŒr âalphanumerischer Charakterâ - also eine Ziffer oder ein Buchstabe; synonym hĂ€tte man auch \\w
schreiben können (w wie word). Warum werden zwei Backslashes gebraucht? Mit \\w
wird signalisiert, dass nicht der Buchstabe w, sondern etwas Besonderes, eben der Regex-Operator \w
gesucht wird.
âZeig die URLs!â
str_extract_all(string, "https?://[:graph:]+")
## [[1]]
## [1] "https://t.co/YHyqTguVWx"
Das Fragezeichen ?
ist eine QuantitÀtsoperator, der einen Treffer liefert, wenn das vorherige Zeichen (hier s) null oder einmal gefunden wird. [:graph:]
ist die Summe von [:alpha:]
(Buchstaben, groĂ und klein), [:digit:]
(Ziffern) und [:punct:]
(Satzzeichen u.Ă€.).
âZĂ€hle die Wörter im String!â
âLiefere nur Buchstabenfolgen zurĂŒck, lösche alles ĂŒbrigeâ
str_extract_all(string, "[:alpha:]+")
## [[1]]
## [1] "Correlation" "of" "unemployment" "and" "AfD"
## [6] "votes" "at" "btw" "r" "https"
## [11] "t" "co" "YHyqTguVWx"
Der QuantitÀtsoperator +
liefert alle Stellen zurĂŒck, in denen der gesuchte Ausdruck einmal oder hĂ€ufiger vorkommt. Die Ergebnisse werden als Vektor von Wörtern zurĂŒckgegeben. Ein anderer QuantitĂ€tsoperator ist *
, der fĂŒr 0 oder mehr Treffer steht. Möchte man einen Vektor, der aus Stringen-Elementen besteht zu einem Strring zusammenfĂŒngen, hilft paste(string)
oder str_c(string, collapse = " ")
.
str_replace_all(string, "[^[:alpha:]+]", "")
## [1] "CorrelationofunemploymentandAfDvotesatbtwrhttpstcoYHyqTguVWx"
Mit dem Negationsoperator [^x]
wird der RegulÀrausrck x
negiert; die Syntax oben heiĂt also âersetze in string
alles auĂer Buchstaben durch Nichtsâ. Mit âNichtsâ sind hier Strings der LĂ€nge Null gemeint; ersetzt man einen belieibgen String durch einen String der LĂ€nge Null, so hat man den String gelöscht.
Das Cheatsheet zur Strings bzw zu stringr
von RStudio gibt einen guten Ăberblick ĂŒber Regex; im Internet finden sich viele Beispiele.
4.2.7.2 Regex im Texteditor
Einige Texteditoren unterstĂŒtzen Regex, so auch RStudio.
Das ist eine praktische Sache. Ein Beispiel: Sie haben eine Liste mit Namen der Art:
- Nachname1, Vorname1
- Nachname2, Vorname2
- Nachname3, Vorname3
Und Sie möchten jetzt aber die Liste mit Stil Vorname Nachname sortiert haben.
RStudio mit Regex machtâs möglich, s. Abbildung fig-vorher-regex.
4.2.8 Emoji-Analyse
Eine einfache Art, Emojis in einer Textmining-Analyse zu verarbeiten, bietet das Paket textclean
:
fls <- system.file("docs/emoji_sample.txt", package = "textclean")
x <- readLines(fls)[1]
x
## [1] "Proin đ ut maecenas đ condimentum đ purus eget. Erat, đvitae nunc elit. Condimentum đą semper iaculis bibendum sed tellus. Ut suscipit interdumđ in. Faucibđ us nunc quis a vitae posuere. đ Eget amet sit condimentum non. Nascetur vitae âč et. Auctor ornare âș vestibulum primis justo congue đurna ac magna. Quam đ„ pharetra đ eros đfacilisis ac lectus nibh est đvehicula đ ornare! Vitae, malesuada đ erat sociosqu urna, đ nec sed ad aliquet đź ."
replace_emoji(x)
## [1] "Proin d??? ut maecenas d??? condimentum d??? purus eget. Erat, d???vitae nunc elit. Condimentum d??c semper iaculis bibendum sed tellus. Ut suscipit interdumd??? in. Faucibd??? us nunc quis a vitae posuere. d??? Eget amet sit condimentum non. Nascetur vitae ^a?^1 et. Auctor ornare ^a?o vestibulum primis justo congue d???urna ac magna. Quam d??yen pharetra d??? eros d???facilisis ac lectus nibh est d???vehicula d??? ornare! Vitae, malesuada d??? erat sociosqu urna, d??? nec sed ad aliquet d??(R) ."
replace_emoji_identifier(x)
## [1] "Proin d??? ut maecenas d??? condimentum d??? purus eget. Erat, d???vitae nunc elit. Condimentum d??c semper iaculis bibendum sed tellus. Ut suscipit interdumd??? in. Faucibd??? us nunc quis a vitae posuere. d??? Eget amet sit condimentum non. Nascetur vitae ^a?^1 et. Auctor ornare ^a?o vestibulum primis justo congue d???urna ac magna. Quam d??yen pharetra d??? eros d???facilisis ac lectus nibh est d???vehicula d??? ornare! Vitae, malesuada d??? erat sociosqu urna, d??? nec sed ad aliquet d??(R) ."
4.2.9 Text aufrÀumen
Eine Reihe generischer Tests bietet das Paket textclean
von Tyler Rinker:
Hier ist ein âunaufgerĂ€umeterâ Text:
x <- c("i like", "<p>i want. </p>. thet them ther .", "I am ! that|", "", NA,
""they" they,were there", ".", " ", "?", "3;", "I like goud eggs!",
"bi\xdfchen Z\xfcrcher", "i 4like...", "\\tgreat", "She said \"yes\"")
Lassen wir uns dazu ein paar Diagnostiken ausgeben.
Encoding(x) <- "latin1"
x <- as.factor(x)
check_text(x)
##
## =============
## NON CHARACTER
## =============
##
## The text variable is not a character column (likely `factor`):
##
##
## *Suggestion: Consider using `as.character` or `stringsAsFactors = FALSE` when reading in
## Also, consider rerunning `check_text` after fixing
##
##
## =====
## DIGIT
## =====
##
## The following observations contain digits/numbers:
##
## 10, 13
##
## This issue affected the following text:
##
## 10: 3;
## 13: i 4like...
##
## *Suggestion: Consider using `replace_number`
##
##
## ========
## EMOTICON
## ========
##
## The following observations contain emoticons:
##
## 6
##
## This issue affected the following text:
##
## 6: "they" they,were there
##
## *Suggestion: Consider using `replace_emoticons`
##
##
## =====
## EMPTY
## =====
##
## The following observations contain empty text cells (all white space):
##
## 1
##
## This issue affected the following text:
##
## 1: i like
##
## *Suggestion: Consider running `drop_empty_row`
##
##
## =======
## ESCAPED
## =======
##
## The following observations contain escaped back spaced characters:
##
## 14
##
## This issue affected the following text:
##
## 14: \tgreat
##
## *Suggestion: Consider using `replace_white`
##
##
## ====
## HTML
## ====
##
## The following observations contain HTML markup:
##
## 2, 6
##
## This issue affected the following text:
##
## 2: <p>i want. </p>. thet them ther .
## 6: "they" they,were there
##
## *Suggestion: Consider running `replace_html`
##
##
## ==========
## INCOMPLETE
## ==========
##
## The following observations contain incomplete sentences (e.g., uses ending punctuation like '...'):
##
## 13
##
## This issue affected the following text:
##
## 13: i 4like...
##
## *Suggestion: Consider using `replace_incomplete`
##
##
## =============
## MISSING VALUE
## =============
##
## The following observations contain missing values:
##
## 5
##
## *Suggestion: Consider running `drop_NA`
##
##
## ========
## NO ALPHA
## ========
##
## The following observations contain elements with no alphabetic (a-z) letters:
##
## 4, 7, 8, 9, 10
##
## This issue affected the following text:
##
## 4:
## 7: .
## 8:
## 9: ?
## 10: 3;
##
## *Suggestion: Consider cleaning the raw text or running `filter_row`
##
##
## ==========
## NO ENDMARK
## ==========
##
## The following observations contain elements with missing ending punctuation:
##
## 1, 3, 4, 6, 8, 10, 12, 14, 15
##
## This issue affected the following text:
##
## 1: i like
## 3: I am ! that|
## 4:
## 6: "they" they,were there
## 8:
## 10: 3;
## 12: biĂchen ZĂŒrcher
## 14: \tgreat
## 15: She said "yes"
##
## *Suggestion: Consider cleaning the raw text or running `add_missing_endmark`
##
##
## ====================
## NO SPACE AFTER COMMA
## ====================
##
## The following observations contain commas with no space afterwards:
##
## 6
##
## This issue affected the following text:
##
## 6: "they" they,were there
##
## *Suggestion: Consider running `add_comma_space`
##
##
## =========
## NON ASCII
## =========
##
## The following observations contain non-ASCII text:
##
## 12
##
## This issue affected the following text:
##
## 12: biĂchen ZĂŒrcher
##
## *Suggestion: Consider running `replace_non_ascii`
##
##
## ==================
## NON SPLIT SENTENCE
## ==================
##
## The following observations contain unsplit sentences (more than one sentence per element):
##
## 2, 3
##
## This issue affected the following text:
##
## 2: <p>i want. </p>. thet them ther .
## 3: I am ! that|
##
## *Suggestion: Consider running `textshape::split_sentence`
4.2.10 Diverse Wortlisten
Tyler Rinker stellt mit dem Paket lexicon
eine Zusammenstellung von Wortlisten zu diversen Zwecken zur VerfĂŒgung. Allerding nur fĂŒr die englische Sprache.
4.2.11 One-hot-Enkodierung (Dummy-Variablen)
Viele Methoden des Maschinellen Lernens können keine nominalen Variablen verarbeiten. Daher mĂŒssen nominale Variablen vorab hĂ€ufig erst in numerische Variablen umgewandelt werden. In der Textanalyse
Nehmen wir als einfaches Beispiel den berĂŒhmten Iris-Datensatz:
Tabelle im Lang-Format
id <int> | Species <fct> | |
---|---|---|
1 | setosa | |
51 | versicolor | |
101 | virginica |
Tabelle mit Spalte âSpeciesâ im One-Hot-Format
id <int> | Species_setosa <dbl> | |
---|---|---|
1 | 1 | |
51 | 0 | |
101 | 0 |
4.2.11.1 Tidymodels
Tidymodels bieten eine recht komfortable Methode zur One-Hot-Transformation:
iris_rec <-
recipe( ~ ., data = iris) |>
step_dummy(Species, one_hot = TRUE)
iris_rec_prepped <-
iris_rec |> prep()
iris_baked <-
iris_rec_prepped |> bake(new_data = NULL)
iris_baked |>
select(starts_with("Species")) |>
head()
Species_setosa <dbl> | Species_versicolor <dbl> | Species_virginica <dbl> | |
---|---|---|---|
1 | 0 | 0 | |
1 | 0 | 0 | |
1 | 0 | 0 | |
1 | 0 | 0 | |
1 | 0 | 0 | |
1 | 0 | 0 |
tidymodels
ist primĂ€r fĂŒr das professionelle Modellieren angelegt; âmal ebenâ eine One-Hot-Enkodierung durchzufĂŒhren, braucht ein paar Zeilen Code.
Ohne den Parameter one_hot = TRUE
wĂŒrden anstelle von
Mal sehen, ob es auch schneller geht, also mit weniger Zeilen Code.
4.2.11.2 Base R: model.matrix
iris_one_hot_matrix <- model.matrix(~ 0 + Species, data = iris_mini)
iris_one_hot_matrix
## Speciessetosa Speciesversicolor Speciesvirginica
## 1 1 0 0
## 2 0 1 0
## 3 0 0 1
## attr(,"assign")
## [1] 1 1 1
## attr(,"contrasts")
## attr(,"contrasts")$Species
## [1] "contr.treatment"
Das + 0
sorgt dafĂŒr, dass bei
model.matrix
gibt eine Matrix zurĂŒck; diese wandlen wir noch in einen Dataframe um:
iris_one_hot_matrix |>
as_tibble() |>
mutate(id = 1:n()) |>
select(id, everything())
id <int> | Speciessetosa <dbl> | Speciesversicolor <dbl> | Speciesvirginica <dbl> | |
---|---|---|---|---|
1 | 1 | 0 | 0 | |
2 | 0 | 1 | 0 | |
3 | 0 | 0 | 1 |
4.2.11.3 Paket fastDummies
Das Paket fastDummies
bietet ebenfalls eine recht einfache Konvertierung zu One-Hot-Enkodierung:
library(fastDummies)
dummy_cols(iris_mini, select_columns = "Species") |>
select(starts_with("Species"))
Species <fct> | Species_setosa <int> | Species_versicolor <int> | Species_virginica <int> |
---|---|---|---|
setosa | 1 | 0 | 0 |
versicolor | 0 | 1 | 0 |
virginica | 0 | 0 | 1 |
4.2.12 Sentimentanalyse
4.2.12.1 EinfĂŒhrung
Eine weitere interessante Analyse ist, die âStimmungâ oder âEmotionenâ (Sentiments) eines Textes auszulesen. Die AnfĂŒhrungszeichen deuten an, dass hier ein MaĂ an VerstĂ€ndnis suggeriert wird, welches nicht (unbedingt) von der Analyse eingehalten wird. Jedenfalls ist das Prinzip der Sentiment-Analyse im einfachsten Fall so:
- Schau dir jeden Token aus dem Text an.
- PrĂŒfe, ob sich das Wort im Lexikon der Sentiments wiederfindet.
- Wenn ja, dann addiere den Sentimentswert dieses Tokens zum bestehenden Sentiments-Wert.
- Wenn nein, dann gehe weiter zum nÀchsten Wort.
- Liefere zum Schluss die Summenwerte pro Sentiment zurĂŒck.
Es gibt Sentiment-Lexika, die lediglich einen Punkt fĂŒr âpositive Konnotationâ bzw. ânegative Konnotationâ geben; andere Lexiko weisen differenzierte GefĂŒhlskonnotationen auf. Wir nutzen hier das deutsche Sentimentlexikon sentiws
(Remus, Quasthoff, und Heyer 2010). Sie können das Lexikon als CSV hier herunterladen:
sentiws <- read_csv("https://osf.io/x89wq/?action=download")
Den Volltext zum Paper finden Sie z.B. hier.
Alternativ können Sie die Daten aus dem Paket pradadata
laden. Allerdings mĂŒssen Sie dieses Paket von Github installieren:
install.packages("devtools", dep = TRUE)
devtools::install_github("sebastiansauer/pradadata")
data(sentiws, package = "pradadata")
Tabelle tbl-afdcount zeigt einen Ausschnitt aus dem Sentiment-Lexikon SentiWS.
neg_pos <chr> | word <chr> | value <dbl> | inflections <chr> |
---|---|---|---|
neg | Abbau | -0.0580 | Abbaus,Abbaues,Abbauen,Abbaue |
neg | Abbruch | -0.0048 | Abbruches,AbbrĂŒche,Abbruchs,AbbrĂŒchen |
neg | Abdankung | -0.0048 | Abdankungen |
neg | AbdÀmpfung | -0.0048 | AbdÀmpfungen |
neg | Abfall | -0.0048 | Abfalles,AbfÀlle,Abfalls,AbfÀllen |
neg | Abfuhr | -0.3367 | Abfuhren |
4.2.12.2 Ungewichtete Sentiment-Analyse
Nun können wir jedes Token des Textes mit dem Sentiment-Lexikon abgleichen; dabei zĂ€hlen wir die Treffer fĂŒr positive bzw. negative Terme. Zuvor mĂŒssen wir aber noch die Daten (afd_long
) mit dem Sentimentlexikon zusammenfĂŒhren (joinen). Das geht nach bewĂ€hrter Manier mit inner_join
; âinnerâ sorgt dabei dafĂŒr, dass nur Zeilen behalten werden, die in beiden Dataframes vorkommen. Tabelle Tabelle tbl-afdsenti zeigt Summe, Anzahl und Anteil der Emotionswerte.
Wir nutzen die Tabelle afd_long
, die wir oben definiert haben.
afd_long %>%
inner_join(sentiws, by = c("token" = "word")) %>%
select(-inflections) -> afd_senti # die Spalte brauchen wir nicht
## Warning in inner_join(., sentiws, by = c(token = "word")): Detected an unexpected many-to-many relationship between `x` and `y`.
## âč Row 9101 of `x` matches multiple rows in `y`.
## âč Row 3190 of `y` matches multiple rows in `x`.
## âč If a many-to-many relationship is expected, set `relationship =
## "many-to-many"` to silence this warning.
afd_senti %>%
group_by(neg_pos) %>%
summarise(polarity_sum = sum(value),
polarity_count = n()) %>%
mutate(polarity_prop = (polarity_count / sum(polarity_count)) %>% round(2)) ->
afd_senti_tab
Die Analyse zeigt, dass die emotionale Bauart des Textes durchaus interessant ist: Es gibt viel mehr positiv getönte Wörter als negativ getönte. Allerdings sind die negativen Wörter offenbar deutlich stĂ€rker emotional aufgeladen, denn die Summe an Emotionswert der negativen Wörter ist (ĂŒberraschenderweise?) deutlich gröĂer als die der positiven.
Betrachten wir also die intensivsten negativ und positive konnotierten Wörter nÀher.
afd_senti %>%
distinct(token, .keep_all = TRUE) %>%
mutate(value_abs = abs(value)) %>%
top_n(20, value_abs) %>%
pull(token)
## [1] "ungerecht" "besonders" "gefĂ€hrlich" "ĂŒberflĂŒssig" "behindern"
## [6] "gelungen" "brechen" "unzureichend" "gemein" "verletzt"
## [11] "zerstören" "trennen" "falsch" "vermeiden" "zerstört"
## [16] "schwach" "belasten" "schÀdlich" "töten" "verbieten"
Diese âHitlisteâ wird zumeist (19/20) von negativ polarisierten Begriffen aufgefĂŒllt, wobei âbesondersâ ein Intensivierwort ist, welches das Bezugswort verstĂ€rt (âbesonders gefĂ€hrlichâ). Das Argument keep_all = TRUE
sorgt dafĂŒr, dass alle Spalten zurĂŒckgegeben werden, nicht nur die durchsuchte Spalte token
. Mit pull
haben wir aus dem Dataframe, der von den dplyr-Verben ĂŒbergeben wird, die Spalte pull
âherausgezogenâ; hier nur um Platz zu sparen bzw. der Ăbersichtlichkeit halber.
Nun könnte man noch den erzielten âNetto-Sentimentswertâ des Corpus ins VerhĂ€ltnis setzen Sentimentswert des Lexikons: Wenn es insgesamt im Sentiment-Lexikon sehr negativ zuginge, wĂ€re ein negativer Sentimentwer in einem beliebigen Corpus nicht ĂŒberraschend. describe_distribution
aus easystats gibt uns einen Ăberblick der ĂŒblichen deskriptiven Statistiken.
Variable | Mean | SD | IQR | Range | Skewness | Kurtosis | n | n_Missing |
---|---|---|---|---|---|---|---|---|
value | -0.05 | 0.20 | 0.05 | (-1.00, 1.00) | -0.68 | 2.36 | 3468 | 0 |
Insgesamt ist das Lexikon ziemlich ausgewogen; negative Werte sind leicht in der Ăberzahl im Lexikon. Unser Corpus hat eine Ă€hnliche mittlere emotionale Konnotation wie das Lexikon:
4.2.13 Weitere Sentiment-Lexika
Tyler Rinker stellt das Paket sentimentr
zur VerfĂŒgung. Matthew Jockers stellt das Paket Syushet
zur VerfĂŒgung.
4.2.14 Google Trends
Eine weitere Möglichkeit, âWorthĂ€ufigkeitenâ zu identifizieren ist Google Trends. Dieser Post zeigt Ihnen eine Einsatzmöglichkeit.
4.3 Aufgaben
4.4 Fallstudie Hate-Speech
4.4.1 Daten
Es finden sich mehrere DatensÀtze zum Thema Hate-Speech im öffentlichen Internet, eine Quelle ist Hate Speech Data, ein Repositorium, das mehrere DatensÀtze beinhaltet.
- Kaggle Hate Speech and Offensive Language Dataset
- Bretschneider and Peters Prejudice on Facebook Dataset
- Daten zum FachartikelâLarge Scale Crowdsourcing and Characterization of Twitter Abusive Behaviorâ
FĂŒr Textmining kann eine Liste mit anstöĂigen (obszönen) Wörten nĂŒtzlich sein, auch wenn man solche Dinge ungern anfĂ€sst, verstĂ€ndlicherweise. Jenyay bietet solche Listen in verschiedenen Sprachen an. Die Liste von KDNOOBW sieht sehr Ă€hnlich aus (zumindest die deutsche Version). Eine lange Sammlung deutscher Schimpfwörter findet sich im insult.wiki; Ă€hnlich bei Hyperhero.
Twitterdaten dĂŒrfen nur in âdehydrierterâ Form weitergegeben werden, so dass kein RĂŒckschluss von ID zum Inhalt des Tweets möglich ist. Daher werden öffentlich nur die IDs der Tweets, als einzige Information zum Tweet, also ohne den eigentlichen Inhalt des Tweets, bereitgestellt.
Ăber die Twitter-API kann man sich, wie oben dargestellt, dann die Tweets wieder ârehydrierenâ, also wieder mit dem zugehörigen Tweet-Text (und sonstigen Infos des Tweets) zu versehen.
4.4.2 Grundlegendes Text Mining
Wenden Sie die oben aufgefĂŒhrten Techniken des grundlegenden Textminings auf einen der oben dargestellten Hate-Speech-DatensĂ€tze an. Erstellen Sie ein (HTML-Dokument) mit Ihren Ergebnissen. Stellen Sie die Ergebnisse auf dem Github-Repo dieses Kurses ein. Vergleichen Sie Ihre Lösung mit den Lösungen der anderen Kursmitglieder.
Wir nutzen noch nicht eigene Daten, die wir von Twitter ausgelesen haben, das heben wir uns fĂŒr spĂ€ter auf.
4.5 Vertiefung
Julia Silge bietet eine nette Fallstudie zu den Themen in Taylor Swifts Liefern.
Apropos Themenanalyse: Alternativ zum klassischen, probabilistischen Themen-Modellierung kann man pretrainierte Wort-Einbettungen verwenden. Dieses Paper gibt eine EinfĂŒhrung.
Hauptkomponenten-Analyse (Principal Component Analysis, PCA) ist eine zentrale Methode der Datenanalyse. Diese Fallstudie stellt eine einfache Anwendung - ohne tiefere theoretische ErlÀuterung - vor.
Silge und Robinson (2017) geben eine EinfĂŒhrung in die Textanalyse mit Hilfe von Tidy-Prinzipien.