Kapitel 14 Textvergleich

14.1 Programme installieren

Installation von Programmen (Paketen): Wenn Sie die Programme bereits installiert haben, können Sie diesen Schritt überspringen.

Das Zeichen # im Programmblock (chunk) bedeutet, dass diese Zeile nicht ausgeführt wird. Entfernen Sie # vor dem Installationsbefehl, falls Sie ein Programm installieren möchten.

Der folgende Programmblock automatisiert die Installation von erwünschten, aber noch nicht installierten Programmen.

# install.packages("readtext")

## First specify the packages of interest
packages = c("tidyverse", "quanteda", "quanteda.textplots", 
             "quanteda.textstats", "wordcloud2", "tidytext", 
             "udpipe", "janitor", "scales", "widyr", "syuzhet", 
             "corpustools", "readtext")

# Install packages not yet installed
installed_packages <- packages %in% rownames(installed.packages())
if (any(installed_packages == FALSE)) {
  install.packages(packages[!installed_packages])
}

# Packages loading
invisible(lapply(packages, library, character.only = TRUE))

14.2 Programme laden

Zuerst müssen wir die Programme ausführen, die wir für die geplante Arbeit benötigen.

library(readtext)
library(quanteda)
library(quanteda.textstats)
library(quanteda.textplots)
library(tidyverse)
library(tidytext)
library(wordcloud2)
library(udpipe)
library(janitor)
library(scales)
library(widyr)
library(syuzhet)
library(corpustools)

14.3 Texte öffnen

txt = readtext("data/books/*.txt", encoding = "UTF-8")
txt
## readtext object consisting of 2 documents and 0 docvars.
## # Description: df [2 x 2]
##   doc_id      text               
##   <chr>       <chr>              
## 1 prozess.txt "\"Der Prozes\"..."
## 2 tom.txt     "\"Tom Sawyer\"..."

Alternativ können Sie die Texte auch aus dem Internet auf Ihren Computer laden und öffnen:

txt1 = readtext("https://raw.githubusercontent.com/tpetric7/tpetric7.github.io/main/data/books/prozess.txt", encoding = "UTF-8")
txt2 = readtext("https://raw.githubusercontent.com/tpetric7/tpetric7.github.io/main/data/books/tom.txt", encoding = "UTF-8")

# Datoteki združimo
txt = rbind(txt1,txt2)

14.4 Korpus anlegen

Wir erstellen ein Korpus, d.h. eine Textsammlung. Der Befehl im Programmbündel quanteda lautet corpus().

romane = corpus(txt)

Zusammenfassung einiger grundlegender quantitativer Merkmale des sprachlichen Materials mit Hilfe von zwei quanteda-Funktionen: - summary()
- textstat_summary().

(romanstatistik = textstat_summary(romane)
)
##      document  chars sents tokens types puncts numbers symbols urls tags emojis
## 1 prozess.txt 482722  3845  88010  7907  16380      10       0    0    0      0
## 2     tom.txt 460249  4652  85841  9860  18785       9       0    0    0      0
povzetek = summary(romane)
povzetek
## Corpus consisting of 2 documents, showing 2 documents:
## 
##         Text Types Tokens Sentences
##  prozess.txt  8507  88010      3845
##      tom.txt 10551  85841      4652

Anhand der zusammengefassten Korpusdaten können Sie z.B. die durchschnittliche Satzlänge in den Texten des Korpus berechnen:

povzetek %>% 
  group_by(Text) %>%
  mutate(dolzina_povedi = Tokens/Sentences)
## # A tibble: 2 x 5
## # Groups:   Text [2]
##   Text        Types Tokens Sentences dolzina_povedi
##   <chr>       <int>  <int>     <int>          <dbl>
## 1 prozess.txt  8507  88010      3845           22.9
## 2 tom.txt     10551  85841      4652           18.5

Wir könnten auch einen Indikator für die lexikalische Vielfalt in Texten berechnen, d. h. das Verhältnis zwischen Types und Tokens, was im Englischen als type token ratio (ttr) bezeichnet wird.

Es wird unterschieden zwischen Wörterbucheinheiten (Lemmata), Wortformtypen (Types) und Wortformen (Tokens).

So ist z. B. das deutsche Verb gehen eine Lexikoneinheit, die mehrere verschiedene Formen (Types) aufweist: z.B. gehe, gehst, geht, gehen, geht, ging, gingst, … gegangen.

Wortformen (Tokens): Einige Formen des Verbs (Types) kommen häufiger vor als andere, und erscheinen im ausgewählten Text nicht.

povzetek %>% 
  group_by(Text) %>% 
  mutate(ttr = Types/Tokens)
## # A tibble: 2 x 5
## # Groups:   Text [2]
##   Text        Types Tokens Sentences    ttr
##   <chr>       <int>  <int>     <int>  <dbl>
## 1 prozess.txt  8507  88010      3845 0.0967
## 2 tom.txt     10551  85841      4652 0.123

Das Programm quanteda verfügt über mehrere Optionen zur Ermittlung der lexikalischen Vielfalt, die aber eine Zerlegung der Texte in kleinere Einheiten, d. h. Tokens (Wörter, Satzzeichen usw.), erfordert. Für einige Merkmale müssen wir eine Dokumenthäufigkeitsmatrix (dfm, document frequency matrix) erstellen, in der festgehalten wird, wie oft eine Wortform in jedem Text der Textsammlung vorkommt.

14.5 Tokenisierung

Um mehr über die Texte herauszufinden, z.B. welche Wörter in den Texten vorkommen, müssen wir zunächst eine Liste von Texteinheiten (d.h. Wörter, Satzzeichen usw.) erstellen.

Wir zerlegen die Texte in Wortformen (z. B. mit Hilfe von Leerzeichen zwischen den Wortformen als Trennungszeichen). Für die Tokenisierung gibt es in quanteda den Befehl tokens().

besede = tokens(romane)
head(besede)
## Tokens consisting of 2 documents.
## prozess.txt :
##  [1] "Der"                 "Prozess"             "by"                 
##  [4] "Franz"               "Kafka"               "Aligned"            
##  [7] "by"                  ":"                   "bilingual-texts.com"
## [10] "("                   "fully"               "reviewed"           
## [ ... and 87,998 more ]
## 
## tom.txt :
##  [1] "Tom"           "Sawyer"        "by"            "Mark"         
##  [5] "Twain"         "Aligned"       "by"            ":"            
##  [9] "András"        "Farkas"        "("             "autoalignment"
## [ ... and 85,829 more ]

14.6 Tokenliste säubern

Wir können Nicht-Wörter aus der Wortliste entfernen:

besede = tokens(romane, remove_punct = T, remove_symbols = T, remove_numbers = T, remove_url = T)
head(besede)
## Tokens consisting of 2 documents.
## prozess.txt :
##  [1] "Der"                 "Prozess"             "by"                 
##  [4] "Franz"               "Kafka"               "Aligned"            
##  [7] "by"                  "bilingual-texts.com" "fully"              
## [10] "reviewed"            "Der"                 "Prozess"            
## [ ... and 71,608 more ]
## 
## tom.txt :
##  [1] "Tom"           "Sawyer"        "by"            "Mark"         
##  [5] "Twain"         "Aligned"       "by"            "András"       
##  [9] "Farkas"        "autoalignment" "Source"        "Project"      
## [ ... and 67,035 more ]

Wir können auch Wortformen ausschließen, die für die Inhaltsanalyse nicht erwünscht sind, sogenannte “Stoppwörter”. Auch englische Wörter, die in den ausgewählten deutschen Texten vorkommen, können entfernt werden. Wir verketten mehrere Einheiten mit Hilfe der c()-Funktion (engl. concatenate = verketten).

stoplist_de = c(stopwords("de"), "dass", "Aligned", "by", "autoalignment", "Source", "Project", 
                "bilingual-texts.com", "fully", "reviewed")
besede = tokens_select(besede, pattern = stoplist_de, selection = "remove")

Die folgende Wortiste, mit Hilfe der tokens()-Funktion angelegt, wird verwendet, um eine Konkordanz zu erstellen, d.h. eine Liste von Kontexten, in denen ein bestimmter Suchbegriff (z.B. ein Wort oder eine Wortgruppe) vorkommt.

stoplist_en = c("Aligned", "by", "autoalignment", "Source", "Project", 
                "bilingual-texts.com", "fully", "reviewed")

# Obdržali bomo ločila
woerter = tokens(romane, remove_symbols = T, remove_numbers = T, remove_url = T)
# Odstranili bomo angleške besede na začetku besedil
woerter = tokens_select(woerter, pattern = stoplist_en, selection = "remove", padding = TRUE)

14.7 Kwic

Um Konkordanzen zu erstellen, verfügt das Programm quanteda über die kwic()-Funktion (Schlüsselwort im Kontext-Ansichten). Es ist möglich, nach einzelnen Wörtern, Phrasen und beliebigen anderen Zeichen (z.B. Wildcards wie * ) zu suchen.

kwic(woerter, pattern = c("Frau", "Herr")) %>% head(3)
## Keyword-in-context with 3 matches.                                                          
##  [prozess.txt, 22] Kafka Verhaftung, Gespräch mit | Frau |
##  [prozess.txt, 54]      verhaftet. Die Köchin der | Frau |
##  [prozess.txt, 96] seinem Kopfkissen aus die alte | Frau |
##                                    
##  Grubach, dann Fräulein Brüstner   
##  Grubach, seiner Zimmervermieterin,
##  , die ihm gegenüber wohnte

Wir werden die Konkordanz in eine Tabelle (oder Datensatz) umwandeln, d.h. in ein data.frame oder tibble(). Dies hat z.B. den Vorteil, dass Spaltennamen (d.h. Variablen) angegeben werden.

Die kwic()-Funktion hat mehrere Optionen, z.B. case_insensitive = FALSE unterscheidet zwischen Groß- und Kleinschreibung. Der Standardwert ist TRUE, d.h. dass diese Unterscheidung (wie in Excel) nicht getroffen wird.

konkordanca = kwic(woerter, pattern = c("Frau", "Herr"), case_insensitive = FALSE) %>% 
  as_tibble()

konkordanca %>% rmarkdown::paged_table()

Mit dem Befehl count() kann man die Anzahl der im Korpus vorgefundenen Wortformen auszählen.

konkordanca %>% 
  count(keyword)
## # A tibble: 2 x 2
##   keyword     n
##   <chr>   <int>
## 1 Frau      132
## 2 Herr       94

Im nächsten Beispiel werden Wörter mit der Endung –in für Substantive gesucht, die weibliche Personennamen bezeichnen (z.B. Ärztin, Köchin, …).

(konkordanca2 = kwic(woerter, pattern = c("*in"), case_insensitive = FALSE) %>% 
  as_tibble()
)
## # A tibble: 4,100 x 7
##    docname      from    to pre                           keyword   post  pattern
##    <chr>       <int> <int> <chr>                         <chr>     <chr> <fct>  
##  1 prozess.txt    26    26 mit Frau Grubach , dann       Fräulein  Brüs~ *in    
##  2 prozess.txt    52    52 eines Morgens verhaftet . Die Köchin    der ~ *in    
##  3 prozess.txt    58    58 der Frau Grubach , seiner     Zimmerve~ , di~ *in    
##  4 prozess.txt    86    86 . K . wartete noch            ein       Weil~ *in    
##  5 prozess.txt   129   129 . Sofort klopfte es und       ein       Mann~ *in    
##  6 prozess.txt   134   134 ein Mann , den er             in        dies~ *in    
##  7 prozess.txt   143   143 niemals gesehen hatte , trat  ein       . Er~ *in    
##  8 prozess.txt   155   155 fest gebaut , er trug         ein       anli~ *in    
##  9 prozess.txt   292   292 zur Tür , die er              ein       weni~ *in    
## 10 prozess.txt   322   322 das Frühstück bringt . «      Ein       klei~ *in    
## # ... with 4,090 more rows

Leider enthält die obige Liste von Kontexten viele Wortformen, die keine weiblichen Personennamen darstellen (z. B. ein, und, …). Wenn wir eine genauere Liste haben wollen, müssen wir auf eine geeignetere Weise suchen, z.B. mit einer Reihe von Platzhaltern, den sogenannten regulären Ausdrücken (regular expressions, “regex”).

Sie können reguläre Ausdrücke auf dem Portal https://regex101.com/ ausprobieren und lernen.

In dem folgenden Recherchebeispiel arbeiten wir mit regulären Ausdrücken, um möglichst wenige falsche Treffer zu erhalten.

konkordanca2 = kwic(woerter, pattern = "\\A[A-Z][a-z]+[^Eae]in\\b",
                      valuetype = "regex", case_insensitive = FALSE) %>% 
  as_tibble() %>% 
  filter(keyword != "Immerhin", 
         keyword != "Darin",
         keyword != "Termin",
         keyword != "Worin",
         keyword != "Robin",
         keyword != "Medizin",
         keyword != "Austin",
         keyword != "Musselin",
         keyword != "Benjamin",
         keyword != "Franklin")

konkordanca2 %>% rmarkdown::paged_table()

Ein weiteres Beispiel für die Verwendung regulärer Ausdrücke bei der Textrecherche: Welches Diminutivsuffix ist in dem Korpus vorherrschend: –lein oder –chen ?

(konkordanca3a = kwic(woerter, "*lein",
                      valuetype = "glob", case_insensitive = FALSE) %>% 
  as_tibble() %>% 
   count(keyword, sort = TRUE)
)
## # A tibble: 6 x 2
##   keyword                      n
##   <chr>                    <int>
## 1 Fräulein                   112
## 2 allein                      49
## 3 klein                       10
## 4 Allein                       2
## 5 Äuglein                      1
## 6 Schreibmaschinenfräulein     1
(konkordanca3b <- kwic(woerter, "*chen",
                      valuetype = "glob", case_insensitive = FALSE) %>% 
  as_tibble() %>% 
   count(keyword, sort = T)
)
## # A tibble: 415 x 2
##    keyword      n
##    <chr>    <int>
##  1 machen     125
##  2 Mädchen    100
##  3 sprechen    57
##  4 bißchen     44
##  5 zwischen    43
##  6 solchen     38
##  7 Weilchen    33
##  8 Zeichen     31
##  9 Menschen    30
## 10 Burschen    28
## # ... with 405 more rows
(konkordanca3 <- kwic(woerter, 
                      pattern = c("\\A[A-Z][a-z]*[^aäeiouürs]chen\\b",
                                  "[A-Z]*[^kl]lein\\b"),
                      valuetype = "regex", case_insensitive = FALSE) %>% 
  as_tibble() %>% 
  filter(keyword != "Welchen", 
         keyword != "Manchen",
         keyword != "Solchen",
         keyword != "Fräulein")
)
## # A tibble: 74 x 7
##    docname      from    to pre                         keyword     post  pattern
##    <chr>       <int> <int> <chr>                       <chr>       <chr> <fct>  
##  1 prozess.txt    87    87 K . wartete noch ein        Weilchen    , sa~ "\\A[A~
##  2 prozess.txt   750   750 warf das Buch auf ein       Tischchen   und ~ "\\A[A~
##  3 prozess.txt  1740  1740 aufgeschreckt , die bei dem Tischchen   am o~ "\\A[A~
##  4 prozess.txt  2617  2617 , stand K . ein             Weilchen    lang~ "\\A[A~
##  5 prozess.txt  3323  3323 Stuhl und hielt ihn ein     Weilchen    mit ~ "\\A[A~
##  6 prozess.txt  3624  3624 hatte . Jetzt war das       Nachttisch~ von ~ "\\A[A~
##  7 prozess.txt  3799  3799 Gegenstände , die auf dem   Nachttisch~ lage~ "\\A[A~
##  8 prozess.txt  5805  5805 sagte K . nach einem        Weilchen    und ~ "\\A[A~
##  9 prozess.txt  6952  6952 , das früh auf dem          Tischchen   beim~ "\\A[A~
## 10 prozess.txt 10539 10539 . » Darf ich das            Nachttisch~ von ~ "\\A[A~
## # ... with 64 more rows

Im folgenden Beispiel suchen wir mit Hilfe regulärer Ausdrücke das Wort Frau + Nachname / Vorname. Hierbei ist zwingend erforderlich, case_insensitive = FALSE zu setzen, da das Programm zwischen Groß- und Kleinbuchstaben unterscheiden soll.

(konkordanca4 <- kwic(woerter, pattern = phrase("\\bFrau\\b ^[A-Z][^[:punct:]]"), 
                      valuetype = "regex", case_insensitive = FALSE) %>% 
  as_tibble()
)
## # A tibble: 61 x 7
##    docname      from    to pre                             keyword post  pattern
##    <chr>       <int> <int> <chr>                           <chr>   <chr> <fct>  
##  1 prozess.txt    22    23 Kafka Verhaftung , Gespräch mit Frau G~ , da~ "\\bFr~
##  2 prozess.txt    54    55 verhaftet . Die Köchin der      Frau G~ , se~ "\\bFr~
##  3 prozess.txt   416   417 im Nebenzimmer sind und wie     Frau G~ dies~ "\\bFr~
##  4 prozess.txt   551   552 Es war das Wohnzimmer der       Frau G~ , vi~ "\\bFr~
##  5 prozess.txt   700   701 . » Ich will doch               Frau G~ - « ~ "\\bFr~
##  6 prozess.txt  1647  1648 gerade die gegenüberliegende T~ Frau G~ woll~ "\\bFr~
##  7 prozess.txt  2868  2869 war , so konnte er              Frau G~ als ~ "\\bFr~
##  8 prozess.txt  5960  5961 . Im Vorzimmer öffnete dann     Frau G~ , di~ "\\bFr~
##  9 prozess.txt  6557  6558 in der ganzen Wohnung der       Frau G~ veru~ "\\bFr~
## 10 prozess.txt  6852  6853 , aber da er mit                Frau G~ spre~ "\\bFr~
## # ... with 51 more rows

14.8 Häufigkeit

Die Dokument-Frequenz-Matrix (dfm) ist der Ausgangspunkt für die Berechnung und grafische Darstellung verschiedener statistischer Größen, z.B. auch der Häufigkeit von Wortformen in den Texten des Korpus:

matrika = dfm(besede, tolower = FALSE) # za zdaj obdržimo velike začetnice

# Odstranimo besede, ki jih v vsebinski analizi ne potrebujemo (stopwords)
matrika = dfm_select(matrika, selection = "remove", pattern = stoplist_de)
matrika
## Document-feature matrix of: 2 documents, 15,185 features (39.73% sparse) and 0 docvars.
##              features
## docs          Prozess Franz Kafka Verhaftung Gespräch Frau Grubach Fräulein
##   prozess.txt       2    24     2         18       16  114      50      112
##   tom.txt           0     0     0          0        4   18       0        0
##              features
## docs          Brüstner Jemand
##   prozess.txt        1      2
##   tom.txt            0      1
## [ reached max_nfeat ... 15,175 more features ]

Das Programm quanteda verfügt über eine spezielle Funktion, die eine Liste von Wortformen und deren Häufigkeit erstellt, und zwar textstat_frequency().

library(quanteda.textstats)
library(quanteda.textplots)

pogostnost = textstat_frequency(matrika, groups = c("prozess.txt", "tom.txt"))

pogostnost %>% rmarkdown::paged_table()

Ein Diagramm mit den gebräuchlichsten Wortformen im Korpus:

pogostnost %>% 
  slice_max(order_by = frequency, n = 20) %>% 
  mutate(feature = reorder_within(feature, frequency, frequency, sep = ": ")) %>%
  # ggplot(aes(frequency, reorder(feature, frequency))) +
  ggplot(aes(frequency, feature)) +
  geom_col(fill="steelblue") +
  labs(x = "Frequency", y = "") +
  facet_wrap(~ group, scales = "free")

Falls erforderlich, kann die Liste der Wortformhäufigkeiten mit der filter()-Funktion in zwei separate Listen aufgeteilt werden.

pogost_tom = textstat_frequency(matrika, groups = c("prozess.txt", "tom.txt")) %>% 
  filter(group == "tom.txt")

pogost_tom %>% rmarkdown::paged_table()
pogost_prozess = textstat_frequency(matrika, groups = c("prozess.txt", "tom.txt")) %>% 
  filter(group == "prozess.txt")

pogost_prozess %>% rmarkdown::paged_table()

Verben des Sagens und des Denkens: Welche kommen in den ausgewählten Texten häufiger vor?

sagen = pogostnost %>%
   filter(str_detect(feature, "^(ge)?sag*"))
sagen %>% rmarkdown::paged_table()
reden = pogostnost %>% 
    filter(str_detect(feature, "^(ge)?rede*"))
reden %>% rmarkdown::paged_table()
fragen = pogostnost %>% 
    filter(str_detect(feature, "^(ge)?frag*"))
fragen %>% rmarkdown::paged_table()
antworten = pogostnost %>% 
    filter(str_detect(feature, "^(ge)?antwort*"))
antworten %>% rmarkdown::paged_table()
rufen = pogostnost %>% 
    filter(str_detect(feature, pattern = "^(ge)?ruf*", negate = FALSE)) %>% 
    filter(!str_detect(feature, "ruh|run|rum|rui|ruch"))
rufen %>% rmarkdown::paged_table()
verb1 = sagen %>% 
  group_by(group) %>% 
  summarise(freq = sum(frequency)) %>% 
  mutate(verb = "sagen")

verb2 = reden %>% 
  group_by(group) %>% 
  summarise(freq = sum(frequency)) %>% 
  mutate(verb = "reden")

verb3 = fragen %>% 
  group_by(group) %>% 
  summarise(freq = sum(frequency)) %>% 
  mutate(verb = "fragen")

verb4 = antworten %>% 
  group_by(group) %>% 
  summarise(freq = sum(frequency)) %>% 
  mutate(verb = "antworten")

verb5 = rufen %>% 
  group_by(group) %>% 
  summarise(freq = sum(frequency)) %>% 
  mutate(verb = "rufen")

Die fünf kleinen Tabellen können zu einer größeren zusammengefügt werden, z. B. mit der Funktion rbind() oder mit bind_rows().

glagoli = rbind(verb1, verb2, verb3, verb4, verb5)
glagoli %>% rmarkdown::paged_table()

Noch ein Diagramm:

glagoli %>% 
  ggplot(aes(freq, verb, fill = verb)) +
  geom_col() +
  facet_wrap(~ group) +
  theme(legend.position = "none")

Um den Vergleich von Texten zu erleichtern, kann die Tabelle auch umgestellt werden, und zwar mit der pivot_wider()-Funktion:

glagoli %>% 
  pivot_wider(id_cols = verb, names_from = group, values_from = freq) %>% rmarkdown::paged_table()

14.9 Kollokationen

Kollexeme sind Lexikoneinheiten, die zusammen verwendet werden. Kollokationen sind sprachliche Elemente, die gemeinsam vorkommen.

Statistische Definition: Wenn zwei Ausdrücke (z. B. “Guten Tag”) im Vergleich zu ihren unmittelbaren Nachbarn signifikant häufiger vorkommen, als man nach dem Zufall erwarten könnte, dann können sie als Kollokation betrachtet werden.

Linguistische Definition: Eine Kollokation ist eine semantisch verwandte Folge von Wörtern.

In quanteda steht uns die Funktion textstat_collocations() zur Auffindung von Kollokationen (im statistischen Sinne) zur Verfügung. “woerter” ist die Liste der Wortformen (padding = TRUE ist hier notwendigerweise zu setzen!), die wir oben erstellt haben.

Kollokationen mit zwei Gliedern:

coll_2 = textstat_collocations(woerter, size = 2, tolower = TRUE) # naredi male črke !

coll_2 %>% rmarkdown::paged_table()

Dreigliedrige Kollokationen:

coll_3 = textstat_collocations(woerter, size = 3, tolower = FALSE)

coll_3 %>% rmarkdown::paged_table()

Viergliedrige Kollokationen:

coll_4 = textstat_collocations(woerter, size = 4, tolower = FALSE)

coll_4 %>% rmarkdown::paged_table()

Mit welchen Wortformen kommen die synonymen Fragewörter warum und wieso in unserem Korpus häufiger gemeinsam vor?

warum <- coll_2 %>% 
  filter(str_detect(collocation, "^warum"))
warum %>% rmarkdown::paged_table()
wieso <- coll_2 %>% 
  filter(str_detect(collocation, "^wieso"))
wieso %>% rmarkdown::paged_table()

Kollokation von Nominalphrasen (NP). Im Deutschen werden substantivische Wörter groß geschrieben. Daher erstellen wir zunächst eine Liste der großgeschriebenen Wortformen im Korpus (woerter_caps). Daraus können wir eine Liste von Kollokationen erhalten (coll_caps2).

woerter_caps = tokens_select(woerter, pattern = "^[A-Z]", 
                                valuetype = "regex", 
                                case_insensitive = FALSE, 
                                padding = TRUE)

coll_caps2 = textstat_collocations(woerter_caps, size = 2, tolower = FALSE, min_count = 5)

coll_caps2 %>% rmarkdown::paged_table()

Es macht keinen Sinn, die Wortverbindung “Der/Die/Das + Substantiv” als Kollokation zu betrachten, da die überwiegende Mehrheit der Substantive im Deutschen mit dem Artikel auftritt.

Deshalb werden wir den großen Anfangsbuchstaben von einigen Funktionswörtern am Satzanfang (z.B. die Artikel Der, Die, Das und einige andere Wortformen), in Kleinbuchstaben umwandeln.

woerter_small = 
  tokens_replace(
    woerter, 
    pattern = c("Der","Die","Das","Des", "Wollen","Im","Zum", 
                "Kein","Jeden","Wenn", "Als", "Da","Aber", 
                "Und","Sehen"), 
    replacement = c("der","die","das", "des","wollen","im", 
                    "zum", "kein","jeden", "wenn", "als","da", 
                    "aber", "und","sehen"))

woerter_caps = tokens_select(woerter_small, pattern = "^[A-Z]", 
                             valuetype = "regex", 
                             case_insensitive = FALSE, 
                             padding = TRUE)

coll_caps2 = textstat_collocations(woerter_caps, size = 2, 
                                   tolower = FALSE, min_count = 5)

coll_caps2 %>% rmarkdown::paged_table()

14.10 Lemmatisierung

Ein Lemma ist eine Lexikoneinheit. Für Substantive wird gewöhnlich der Nominativ Singular, für Verben der Infinitiv und für Adjektive der Positiv im Nominativ Singular als Lemmaform verwendet.

Listen deutscher Lexikoneinheiten (Lemmata) oder anderer Sprachen kann man aus dem Internet auf die eigene Festplatte herunterladen. Im folgenden Programmblock setzen wir solch eine Lemmaliste aus dem Internet zur Lemmatisierung der Wortformen in unserem Korpus ein. Das bewerkstelligen wir mit der quanteda-Funktion tokens_replace().

# Preberi seznam slovarskih enot in pojavnic z diska
lemdict = read.delim2("data/lemmatization_de.txt", sep = "\t", encoding = "UTF-8", 
                      col.names = c("lemma", "word"), stringsAsFactors = F)

# Pretvori podatkovna niza v znakovna niza
lemma = as.character(lemdict$lemma) 
word = as.character(lemdict$word)

# Lematiziraj pojavnice v naših besedilih
lemmas <- tokens_replace(besede,
                             pattern = word,
                             replacement = lemma,
                             case_insensitive = TRUE, 
                             valuetype = "fixed")

Anschließen erstellen wir eine Matrix mit Lemmas (anstelle von Wortformen).

matrika_lem = dfm(lemmas, tolower = FALSE) # za zdaj obdržimo velike začetnice

# Odstranimo besede, ki jih v vsebinski analizi ne potrebujemo (stopwords)
matrika_lem = dfm_select(matrika_lem, selection = "remove", pattern = stoplist_de)
matrika_lem
## Document-feature matrix of: 2 documents, 10,072 features (38.04% sparse) and 0 docvars.
##              features
## docs          Prozess franzen Kafka Verhaftung Gespräch Frau Grubach Fräulein
##   prozess.txt       2      24     2         19       18  121      50      112
##   tom.txt           0       0     0          0        5   27       0        0
##              features
## docs          Brüstner Jemand
##   prozess.txt        1      2
##   tom.txt            0      1
## [ reached max_nfeat ... 10,062 more features ]

14.11 Wortwolken

Mit der quanteda-Funktion texplot_wordcloud() erstellen wir eine einfache Wortwolke aus der zuvor gespeicherten Dokument-Häufigkeits-Matrix, in der die Wortformen durch Lemmas ersetzt wurden.

textplot_wordcloud(matrika_lem, comparison = TRUE, adjust = 0.3, color = c("darkblue","darkgreen"),
                   max_size = 4, min_size = 0.75, rotation = 0.5, min_count = 30, max_words = 250)

Ästhetisch ansprechendere Wortwolken bilden wir mit dem Programm wortcloud2. Die erste Wortwolke zeigt Wörter aus dem ersten Text, die andere für den zweiten. In beiden Fällen verwenden wir die zuvor gespeicherte Dokument-Häufigkeits-Matrix, filtern sie jedoch durch die Hinzufügung eines Wertes in eckigen Klammern: matrika_lem[1,] bzw. matrika_lem[2,]. Diese Schreibweise in R besagt, dass alle Spalten der Matrix verwendet werden sollen, aber nur die erste bzw. zweite Zeile. In den Spalten der Matrix stehen nämlich die Lemmas, die erste Zeile bezieht sich auf den ersten Text und die zweite auf den zweiten Text.

# install.packages("wordcloud2)
matrika_lem_prozess = matrika_lem[1,]

set.seed(1320)
library(wordcloud2)
topfeat <- as.data.frame(topfeatures(matrika_lem_prozess, 100))
topfeat <- rownames_to_column(topfeat, var = "word")
wordcloud2(topfeat)
matrika_lem_tom = matrika_lem[2,]

set.seed(1321)
library(wordcloud2)
topfeat2 <- as.data.frame(topfeatures(matrika_lem_tom, 100))
topfeat2 <- rownames_to_column(topfeat2, var = "word")
wordcloud2(topfeat2)

14.12 Position im Text (xray)

Ein xray-Diagramm zeigt die Verteilung einer Zeichenkette in einem oder mehreren Texten des Korpus an. Im folgenden Beispiel mit der quanteda-Funktion textplot_xray() sieht man schematisch, an welchen Textstellen das deutsche Wort frau in den Texten erscheint. Derartige Diagramme sind auch unter anderen Bezeichnungen geläufig: z.B. Barcode- oder Strichcode-Diagramm, Dispersionsdiagramm. Eine ähnliche graphische Funktion, genannt MicroSearch, begegnet uns auch in Voyant Tools.

kwic_frau = kwic(lemmas, pattern = "frau")
textplot_xray(kwic_frau)

Die folgende Funktion dispersion_plot() haben wir selber zusammengestellt und soll wie die Python-Bibliothek NLTK möglichst benutzerfreundlich sein. Angegeben werden muss lediglich der Name eines beliebigen Textes und ein beliebiges Wort, dass im Text ausfindig gemacht werden soll. Unter der Haube der selbstgebastelten Funktion wird der Text in Wortformen zerlegt, eine KWIC()-Recherche durchgeführt und letztendlich ein xray-Diagramm geplottet. Vorausgesetzt werden die beiden Programme quanteda und quanteda.textplots.

dispersion_plot <- function(text, word){
  library(quanteda); library(quanteda.textplots)
  tokens({text}) %>% kwic({word}) %>% textplot_xray()
  }

Im folgenden Beispiel möchten wir wissen, wo in unseren beiden Texten die Phrase die frau vorkommt. So wie in der kWIC()-Funktion der quanteda-Bibliothek kommt die (phrase()-Funktion zum Einsatz, da wir nicht nach einem einzelnen Wort suchen sondern nach einer Wortverbindung.

dispersion_plot(txt$text, phrase("die frau"))

14.13 Lexikalische Vielfalt

Mit Hilfe der quanteda-Funktion textstat_lexdiv(), die als Eingabe eine Dokument-Frequenz-Matrix verlangt, können verschiedene Indices für lexikalische Diversität (Wortvielfalt, Wortreichtum) berechnet werden, d.h. wie viele verschiedene Wortformen in einem Text vorkommen. Der bekannteste Quotient zur Einschätzung des Formenreichtums ist das Type-Token-Verhältnis. Da aber letztgenanntes Verhältnis von der Länge des Textes bzw. der Größe des Textkorpus abhängt, sind auch andere Indices im Einsatz, um den eben genannten Nachteil zu kompensieren.

textstat_lexdiv(matrika, measure = "all")
##      document       TTR         C        R     CTTR        U         S        K
## 1 prozess.txt 0.2457355 0.8651285 44.68334 31.59590 33.50861 0.9039511 22.33655
## 2     tom.txt 0.2939104 0.8828536 54.69657 38.67631 38.75057 0.9176397 11.48121
##          I           D         Vm      Maas     lgV0    lgeV0
## 1 26.76129 0.002233722 0.04626902 0.1727515 7.795478 17.94975
## 2 73.92612 0.001148154 0.03284439 0.1606427 8.533417 19.64892

14.14 Textähnlichkeit

Mit der quanteda-Funktion textsat_simil() kann die Ähnlichkeit von Texten berechnet werden. Dieses Verfahren ist vor allem dann interessant, wenn wir mehr als zwei Texte vergleichen wollen. Deshalb fügen wir unserem bisherigen Textkorpus noch eine Novelle von Kafka hinzu.

# odpremo datoteko
verwandl = readtext("data/books/verwandlung/verwandlung.txt", encoding = "UTF-8")
# ustvarimo nov korpus
verw_corp = corpus(verwandl)
# združimo novi korpus s prrejšnjim
romane3 = romane + verw_corp
# tokenizacija
romane3_toks = tokens(romane3)
# ustvarimo matriko (dfm)
romane3_dfm = dfm(romane3_toks)

Das Ergebnis der Berechnung: Kafkas Novelle Die Verwandlung hat etwas mehr Ähnlichkeit mit Kafkas Roman Der Prozess als mit Twains Roman Tom Sawyer.

textstat_simil(romane3_dfm, method = "cosine", margin = "documents")
## textstat_simil object; method = "cosine"
##                 prozess.txt tom.txt verwandlung.txt
## prozess.txt           1.000   0.930           0.948
## tom.txt               0.930   1.000           0.933
## verwandlung.txt       0.948   0.933           1.000

Statt die textstat_simil()-Funktion mit ganzen Texten zu konfrontieren, kann man auch die Ähnlichkeit von ausgewählten Wortformen (margin = features) berechnen lassen.

# compute some term similarities
simil1 = textstat_simil(matrika, 
                        matrika[, c("Josef", "Tom", 
                                    "Sawyer", "Huck", "Finn")], 
                         method = "cosine", margin = "features")
head(as.matrix(simil1), 10)
##                Josef       Tom    Sawyer      Huck      Finn
## Prozess    0.9991331 0.0000000 0.0000000 0.0000000 0.0000000
## Franz      0.9991331 0.0000000 0.0000000 0.0000000 0.0000000
## Kafka      0.9991331 0.0000000 0.0000000 0.0000000 0.0000000
## Verhaftung 0.9991331 0.0000000 0.0000000 0.0000000 0.0000000
## Gespräch   0.9793983 0.2425356 0.2425356 0.2425356 0.2425356
## Frau       0.9933995 0.1559626 0.1559626 0.1559626 0.1559626
## Grubach    0.9991331 0.0000000 0.0000000 0.0000000 0.0000000
## Fräulein   0.9991331 0.0000000 0.0000000 0.0000000 0.0000000
## Brüstner   0.9991331 0.0000000 0.0000000 0.0000000 0.0000000
## Jemand     0.9122695 0.4472136 0.4472136 0.4472136 0.4472136

Die Unterschiedlichkeit oder Distanz von Texten kann man mit Hilfe der textstat_dist() berechnen lassen.

# plot a dendrogram after converting the object into distances
dist1 = textstat_dist(romane3_dfm, 
                      method = "euclidean", margin = "documents")
plot(hclust(as.dist(dist1)))

14.15 Schlüsselwörter (keywords)

Welche Wortformen können als Schlüsselwörter für einen Text angesehen werden, d.h. als Begriffe, die für einen bestimmten Text charakteristisch sind und ihn von anderen unterscheiden? Mit der quanteda-Funktion textstat_keyness() vergleichen wir einen Zieltext (target) mit einem Referenztext (reference).

key_tom <- textstat_keyness(matrika, target = "tom.txt")
key_tom %>% rmarkdown::paged_table()
key_prozess <- textstat_keyness(matrika, target = "prozess.txt")
key_prozess %>% rmarkdown::paged_table()
textplot_keyness(key_tom, key_tom$n_target == 1)

textplot_keyness(key_tom, key_prozess$n_target == 1)

textplot_keyness(key_tom)

textplot_keyness(key_prozess)

14.16 Lesbarkeit des Textes

Ein Lesbarkeitsindex (readibility index) gibt Auskunft darüber, wie schwer ein Lesetext zu verstehen ist. Es gibt eine ganze Reihe von Lesbarkeitsindices, eine ganze Palette davon macht auch quanteda verfügbar. Die meisten sind an die englische Sprache angepasst und haben daher für andere Sprachen eingeschränkte Aussagekraft. Grundlage für die verschiedenen Lesbarkeitsindices sind meist Größen, die sich auf die Satz- und Wortlänge beziehen. Sätze, die aus vielen Wörtern bestehen, und Wörter, die aus vielen Silben oder vielen Buchstaben bestehen, sind meist nicht so leicht und schnell zu verarbeiten wie kürzere Sätze und Wörter.

Der Flesch-Index ist einer bekanntesten Lesbarkeitsindices. Es gibt sogar eine Version, die an deutschsprachige Texte angepasst ist. In unserem Textkorpus zeigt sich, dass der Roman Der Prozess einen etwas niedrigeren Wert (52) hat als Tom Sawyer (61). Der niedrigere Indexwert bedeutet, dass Kafkas Prozess - wohl wegen der im Durchschnitt etwas längeren Sätze und Wörter - schwieriger zu lesen bzw. zu verstehen ist als Tom Sawyer.

textstat_readability(romane, measure = c("Flesch", "Flesch.Kincaid", "FOG", "FOG.PSK", "FOG.NRI"))
##      document   Flesch Flesch.Kincaid      FOG  FOG.PSK  FOG.NRI
## 1 prozess.txt 51.94715      10.644645 13.04497 6.390374 8545.508
## 2     tom.txt 60.58142       8.395483 10.61185 5.074038 6016.218

14.17 Kookurrenz-Netzwerk (FCM)

Die Feature-Kookurrenz-Matrix (FCM) gibt Auskunft darüber, welche Wörter in in einem Text oder Textkorpus häufiger miteinander verknüpft werden. Eine FCM wird in zwei Schritten ermittelt:
- Zunächst wird eine Liste von Ausdrücken (pattern, Muster) aus einer vorher gespeicherten Matrix (dfm) ausgewählt, und zwar mit der dfm_select()-Funktion,
- dann wird die Feature-Kookurrenz-Matrix mit Hilfe der fcm()-Funktion erstellt.

Hier folgt ein Beispiel für den zweiten Text (Tom Sawyer).

dfm_tags <- dfm_select(
  matrika[2,], 
  pattern = (c("tom", "huck", "*joe", "becky", "tante", "witwe", 
               "polly", "sid", "mary", "thatcher", "höhle", "herz", 
               "*schule", "katze", "geld", "zaun", "piraten", 
               "schatz")))
toptag <- names(topfeatures(dfm_tags, 50))
head(toptag)
## [1] "Tom"   "Huck"  "Joe"   "Becky" "Tante" "Sid"

Die FCM kann mit Hilfe der quanteda-Funktion textplot_network() graphisch dargestellt werden. Als Eingabe dient eine FCM, die aber vorher gefiltert werden muss, um Übersichtlichkeit oder Interpretierbarkeit zu gewährleisten.

# Construct feature-cooccurrence matrix (fcm) of tags
fcm_tom <- fcm(matrika[2,]) # besedilo 2 je tom.txt
head(fcm_tom)
## Feature co-occurrence matrix of: 6 by 15,185 features.
##             features
## features     Prozess Franz Kafka Verhaftung Gespräch Frau Grubach Fräulein
##   Prozess          0     0     0          0        0    0       0        0
##   Franz            0     0     0          0        0    0       0        0
##   Kafka            0     0     0          0        0    0       0        0
##   Verhaftung       0     0     0          0        0    0       0        0
##   Gespräch         0     0     0          0        6   72       0        0
##   Frau             0     0     0          0        0  153       0        0
##             features
## features     Brüstner Jemand
##   Prozess           0      0
##   Franz             0      0
##   Kafka             0      0
##   Verhaftung        0      0
##   Gespräch          0      4
##   Frau              0     18
## [ reached max_nfeat ... 15,175 more features ]
top_fcm <- fcm_select(fcm_tom, pattern = toptag)
textplot_network(top_fcm, min_freq = 0.6, edge_alpha = 0.8, edge_size = 5)

14.18 Grammatische Analyse

Spezielle Programme (z.B. spacyr oder udpipe) können zur grammatikalischen Analyse und Lemmatisierung von Wortformen eingesetzt werden. Das Programm spacyr verlangt eine zusätzliche Python-Umgebung. Es ist für verbreitete europäische Sprachen wie Englisch, Französich, Deutsch und einige weitere einsetzbar, aber leider nicht für Slowenisch. Das Programm udpipe hat zwei Vorteile: (a) es verlangt lediglich eine R-Installation und (b) es ist zur Zeit für mehr als sechzig Sprachen verfügbar, neben Englisch und Deutsch auch für Slowenisch.

14.18.1 Vorbereitung

Vor der ersten Benutzung müssen wir das deutsche Sprachmodell aus dem Internet herunterladen. Im nächsten Schritt laden wir das Sprachmodell in den Arbeitsspeicher unseres Computers, und zwar mit udpipe_load_model().

Der nachfolgende Programmblock überprüft zuerst, ob das gewünschte Modell für eine bestimmte Sprache bereits im Arbeitsverzeichnis auf der Festplatte unseres Computers gespeichert ist. Ist es noch nicht auf der Festplatte, wird das Sprachmodell heruntergeladen und anschließend in den Arbeitsspeicher geladen. Ist das Sprachmodell bereits im Arbeitsverzeichnis, wird es sofort in den Arbeitsspeicher geladen und nicht noch einmal aus dem Internet heruntergeladen.

library(udpipe)
destfile = "german-gsd-ud-2.5-191206.udpipe"

if(!file.exists(destfile)){
   sprachmodell <- udpipe_download_model(language = "german")
   udmodel_de <- udpipe_load_model(sprachmodell$file_model)
   } else {
  file_model = destfile
  udmodel_de <- udpipe_load_model(file_model)
}

Wenn sich das Sprachmodell bereits im Arbeitsverzeichnis Ihres Computers befindet, können Sie es auch auf folgende Weise ausführen:

Der nächste Programmschritt ist die Annotation der Texte, und zwar mit der Funktion udpipe_annotate(). Die Wortformen in den Texten werden nach nun verschiedenen grammatischen Kriterien bestimmt.

# Na začetku je readtext prebral besedila, shranili smo jih v spremenljivki "txt".
x <- udpipe_annotate(udmodel_de, x = txt$text, trace = TRUE)
## 2022-03-22 22:07:33 Annotating text fragment 1/2
## 2022-03-22 22:09:37 Annotating text fragment 2/2
# # samo prvo besedilo:
# x <- udpipe_annotate(udmodel_de, x = txt$text[1], trace = TRUE)

x <- as.data.frame(x)

Die Struktur des Datensatzes:

str(x)
## 'data.frame':    174925 obs. of  14 variables:
##  $ doc_id       : chr  "doc1" "doc1" "doc1" "doc1" ...
##  $ paragraph_id : int  1 1 1 1 1 1 1 1 1 1 ...
##  $ sentence_id  : int  1 1 1 1 1 1 1 1 1 1 ...
##  $ sentence     : chr  "Der Prozess by Franz Kafka Aligned by : bilingual-texts.com ( fully reviewed ) Der Prozess Franz Kafka 1 Verhaf"| __truncated__ "Der Prozess by Franz Kafka Aligned by : bilingual-texts.com ( fully reviewed ) Der Prozess Franz Kafka 1 Verhaf"| __truncated__ "Der Prozess by Franz Kafka Aligned by : bilingual-texts.com ( fully reviewed ) Der Prozess Franz Kafka 1 Verhaf"| __truncated__ "Der Prozess by Franz Kafka Aligned by : bilingual-texts.com ( fully reviewed ) Der Prozess Franz Kafka 1 Verhaf"| __truncated__ ...
##  $ token_id     : chr  "1" "2" "3" "4" ...
##  $ token        : chr  "Der" "Prozess" "by" "Franz" ...
##  $ lemma        : chr  "der" "Prozeß" "by" "Franz" ...
##  $ upos         : chr  "DET" "NOUN" "PROPN" "PROPN" ...
##  $ xpos         : chr  "ART" "NN" "NE" "NE" ...
##  $ feats        : chr  "Case=Nom|Definite=Def|Gender=Masc|Number=Sing|PronType=Art" "Case=Nom|Gender=Masc|Number=Sing" "Case=Nom|Gender=Masc|Number=Sing" "Case=Nom|Gender=Masc|Number=Sing" ...
##  $ head_token_id: chr  "2" "72" "2" "3" ...
##  $ dep_rel      : chr  "det" "nsubj" "appos" "flat" ...
##  $ deps         : chr  NA NA NA NA ...
##  $ misc         : chr  NA NA NA NA ...

So sieht der Datensatz mit annotatierten Wortformen und Lemmatisierung aus:

head(x, 10) %>% rmarkdown::paged_table()

14.18.2 Vergleich Nomen : Pronomen

Nach der automatischen, mit Überschallgeschwindigkeit durchgeführten Annotation können wir uns mit der Analyse grammatischer Kategorien von Wortformen und Lemmas beschäftigen.

Wie häufig kommen universalen Wortklassen (upos) in den Texten vor?

(tabela = x %>% 
  group_by(doc_id) %>% 
  count(upos) %>% 
  filter(!is.na(upos),
         upos != "PUNCT")
)
## # A tibble: 28 x 3
## # Groups:   doc_id [2]
##    doc_id upos      n
##    <chr>  <chr> <int>
##  1 doc1   ADJ    5284
##  2 doc1   ADP    6350
##  3 doc1   ADV    8387
##  4 doc1   AUX    4390
##  5 doc1   CCONJ  2425
##  6 doc1   DET    8050
##  7 doc1   NOUN  10705
##  8 doc1   NUM     155
##  9 doc1   PART   1984
## 10 doc1   PRON  11280
## # ... with 18 more rows
tabela %>% 
  mutate(upos = reorder_within(upos, n, n, sep = ": ")) %>% 
  ggplot(aes(n, upos, fill = upos)) +
  geom_col() +
  facet_wrap(~ doc_id, scales = "free") +
  theme(legend.position = "none") +
  labs(x = "Število pojavnic", y = "")

Zur besseren Vergleichbarkeit fügen wir noch die entsprechenden Prozentwerte hinzu.

(delezi = tabela %>% 
  mutate(prozent = n/sum(n)) %>% 
  pivot_wider(id_cols = upos, names_from = doc_id, values_from = n:prozent)
)
## # A tibble: 14 x 5
##    upos  n_doc1 n_doc2 prozent_doc1 prozent_doc2
##    <chr>  <int>  <int>        <dbl>        <dbl>
##  1 ADJ     5284   5539     0.0729        0.0818 
##  2 ADP     6350   5524     0.0877        0.0816 
##  3 ADV     8387   6706     0.116         0.0990 
##  4 AUX     4390   3386     0.0606        0.0500 
##  5 CCONJ   2425   3270     0.0335        0.0483 
##  6 DET     8050   6888     0.111         0.102  
##  7 NOUN   10705  10871     0.148         0.160  
##  8 NUM      155    306     0.00214       0.00452
##  9 PART    1984   1658     0.0274        0.0245 
## 10 PRON   11280   9027     0.156         0.133  
## 11 PROPN   2317   3919     0.0320        0.0579 
## 12 SCONJ   1687   1296     0.0233        0.0191 
## 13 VERB    9401   8669     0.130         0.128  
## 14 X         20    678     0.000276      0.0100

Welche Anteile haben die beiden Wortklassen Nomen und Pronomen in den beiden Texten?

delezi %>% 
  filter(upos %in% c("NOUN", "PRON"))
## # A tibble: 2 x 5
##   upos  n_doc1 n_doc2 prozent_doc1 prozent_doc2
##   <chr>  <int>  <int>        <dbl>        <dbl>
## 1 NOUN   10705  10871        0.148        0.160
## 2 PRON   11280   9027        0.156        0.133

Sind die Anteile der Nomina und Pronomina in beiden Texten ähnlich oder verschieden? Ein \(\chi^2\)-Test könnte uns eine erste Antwort auf diese Frage geben.

# za hi kvadrat test potrebujemo le drugi in tretji stolpec
nominal = delezi %>% 
  filter(upos %in% c("NOUN", "PRON")) %>% 
  dplyr::select(n_doc1, n_doc2) 

chisq.test(nominal)
## 
##  Pearson's Chi-squared test with Yates' continuity correction
## 
## data:  nominal
## X-squared = 147.38, df = 1, p-value < 2.2e-16

Die beiden Texte unterscheiden sich mit statistischer Signifikanz voneinander: \(\chi^2\) (1) = 147,38; p < 0,001. Die obige Häufigkeitstabelle zeigt, dass der Anteil der Pronomina im Prozess vergleichsweise höher ist als im Tom Sawyer. Für eine verlässliche sprachwissenschaftliche Interpretation, müsste man sich genauer anschauen, welche Pronomina und welche Nomina einen starken Einfluss auf dieses Zahlenverhältnis haben. Semantisch gesehen lassen sich Pronomina nicht so eindeutig auf ein Antezedens (vorangegangenes Bezugsobjekt) beziehen wie Nomina und daher weniger zuverlässige sprachliche Mittel als Nomina, insbesondere wenn die Distanz zwischen Antezedens und Pronomen größer ist oder aufgrund von konkurrierenden Bezugsobjekten schwierig ist. Formell betrachtet sind Pronomina allerdings weniger komplex als Nomina.

Wenn wir eine Wortart mit allen anderen im Datensatz vergleichen wollen, ist die Umrechnung komplizierter, denn wie in Excel müssen wir
- zuerst die Summe aller Wortarten berechnen, - dann die Anzahl der Pronomina bzw. Nomina von der Gesamtsumme subtrahieren,
- und letztendlich die Differenz in der 2x2-Tabelle für den \(\chi^2\)-Test berücksichtigen.

(zaimki = x %>% 
  group_by(doc_id) %>% 
  count(upos) %>% 
  filter(!is.na(upos),
         upos != "PUNCT") %>% 
  mutate(vsota = sum(n),
         no_noun = vsota - n[upos == "NOUN"],
         no_pron = vsota - n[upos == "PRON"]) %>% 
  filter(upos == "PRON") %>% 
  dplyr::select(doc_id, n, no_pron) %>% 
  pivot_longer(-doc_id, 
               names_to = 'kategorija', values_to = 'vrednost') %>%
  pivot_wider(id_cols = kategorija, 
              names_from = doc_id, values_from = vrednost)
)
## # A tibble: 2 x 3
##   kategorija  doc1  doc2
##   <chr>      <int> <int>
## 1 n          11280  9027
## 2 no_pron    61155 58710
(samostalniki = x %>% 
  group_by(doc_id) %>% 
  count(upos) %>% 
  filter(!is.na(upos),
         upos != "PUNCT") %>% 
  mutate(vsota = sum(n),
         no_noun = vsota - n[upos == "NOUN"],
         no_pron = vsota - n[upos == "PRON"]) %>% 
  filter(upos == "NOUN") %>% 
  dplyr::select(doc_id, n, no_noun) %>% 
  pivot_longer(-doc_id, 
               names_to = 'kategorija', values_to = 'vrednost') %>%
  pivot_wider(id_cols = kategorija, 
              names_from = doc_id, values_from = vrednost)
)
## # A tibble: 2 x 3
##   kategorija  doc1  doc2
##   <chr>      <int> <int>
## 1 n          10705 10871
## 2 no_noun    61730 56866

Wir führen zwei \(\chi^2\)-Tests durch, und zwar:
- einen, um die Anzahl der Pronomina mit der Anzahl anderer Wortarten zu vergleichen,
- und einen, um die Anzahl der Nomina mit anderen Wortarten zu vergleichen.

# izločimo prvi stolpec [, -1], za hi kvadrat test potrebujemo le drugi in tretji stolpec
chisq.test(zaimki[,-1])
## 
##  Pearson's Chi-squared test with Yates' continuity correction
## 
## data:  zaimki[, -1]
## X-squared = 142.36, df = 1, p-value < 2.2e-16
chisq.test(samostalniki[,-1])
## 
##  Pearson's Chi-squared test with Yates' continuity correction
## 
## data:  samostalniki[, -1]
## X-squared = 43.259, df = 1, p-value = 4.796e-11

Die beiden statistischen Tests zeigen einen statistisch signifikanten Unterschied zwischen den beiden Texten an. Allerdings ist statitische Signifikanz bei so großen Stichproben wahrscheinlicher als bei kleinen.

14.18.3 Konjunktionen im Vergleich

Als nächstes haben wir vor, die Anzahl der Sätze mit koordinierender oder subordinierender Konjunktion miteinander zu vergleichen.

Die Grundannahme ist, dass Parataxe leichter zu verstehen ist als Hypotaxe.

Parataxe bedeutet, dass zwei oder mehrere Sätze in einer sprachlichen Äußerungen nebengeordnet (koordiniert, gleichrangig) sind, Hypotaxe dagegen, das ein oder mehrere Sätze in einer sprachlichen Äußerung einem anderen Satz untergeordnet (subordiniert) ist.

In der folgenden Auszählung berücksichtigen wir lediglich Sätze, die mit einem Junktor (koordinierender Konjunktion, CCONJ) oder einem Subjunktor (subordinierender Konjunktion, SCONJ) eingeleitet sind. Das bedeutet, dass beispielweise Konjunktionaladverbien, die nebengeordnete Sätze miteinander verbinden können, oder Relativpronomen, die untergeordnete Sätze einleiten können, in der Auszählung nicht berücksichtigt werden.

Die Hypothesen, die beim statistischen Test geprüft werden sollen, lauten folgendermaßen:
- \(H_0\): Das zahlenmäßige Verhältnis zwischen Junktoren und Subjunktoren in den beiden Romanen ist gleich.
- \(H_1\): Das zahlenmäßige Verhältnis zwischen Junktoren und Subjunktoren in den beiden Romanen unterscheidet sich.

(vezniki = tabela %>% 
  filter(upos %in% c("CCONJ", "SCONJ")) %>% 
  mutate(prozent = n/sum(n)) %>% 
  pivot_wider(id_cols = upos, names_from = doc_id, values_from = n:prozent)
)
## # A tibble: 2 x 5
##   upos  n_doc1 n_doc2 prozent_doc1 prozent_doc2
##   <chr>  <int>  <int>        <dbl>        <dbl>
## 1 CCONJ   2425   3270        0.590        0.716
## 2 SCONJ   1687   1296        0.410        0.284

Die Prozentzahlen weisen darauf hin, dass der Anteil der koordinierenden Konjunktionen im Roman Prozess (doc1: ca. 59%) kleiner ist als im Roman Tom Sawyer (doc2: ca. 72%). Das könnte bedeuten, dass im Tom Sawyer mehr Parataxe verwendet wird als im Prozess. Da es sich aber um Stichproben beider Romane handelt, müssen wir einen geeigneten statistischen Test durchführen, um den beobachteten Prozentunterschied (Häufigkeitsunterschied) zu bestätigen.

Mit dem \(\chi^2\)-Test prüfen wir, ob der Häufigkeitsunterschied zwischen den beiden Romanstichproben signifikant ist, also nach statistischen Kriterien groß genug ist, um die die statistische Hypothese \(H_1\) zu bestätigen oder zu verwerfen.

chisq.test(vezniki[,c(2:3)])
## 
##  Pearson's Chi-squared test with Yates' continuity correction
## 
## data:  vezniki[, c(2:3)]
## X-squared = 152.74, df = 1, p-value < 2.2e-16

Der statistische Test bestätigt, dass der Unterschied zwischen den beiden Romanstichproben statistisch signifikant ist und dass damit die Hypothese \(H_1\) angenommen werden kann.

In dem soeben durchgeführten Test haben wir nur die Anzahl der Junktoren und Subjunktoren berücksichtigt. Ändert sich das statistische Ergebnis, wenn wir in der Tabelle auch die übrigen Wortarten einbeziehen? Das soll im folgenden Programmblock überprüft werden. Zuerst müssen wir die beiden relevanten Tabellen erstellen. Die erste enthält die Häufigkeiten von Junktoren im Vergleich zu Nicht-Junktoren, die zweite dagegen die Häufigkeiten von Subjunktoren im Vergleich zu Nicht-Junktoren.

(koord = tabela %>% 
  mutate(vsota = sum(n),
         no_cconj = vsota - n[upos == "CCONJ"],
         no_sconj = vsota - n[upos == "SCONJ"]) %>% 
  filter(upos == "CCONJ") %>% 
  dplyr::select(doc_id, n, no_cconj) %>% 
  pivot_longer(-doc_id, 
               names_to = 'kategorija', values_to = 'vrednost') %>%
  pivot_wider(id_cols = kategorija, 
              names_from = doc_id, values_from = vrednost)
)
## # A tibble: 2 x 3
##   kategorija  doc1  doc2
##   <chr>      <int> <int>
## 1 n           2425  3270
## 2 no_cconj   70010 64467
(subord = tabela %>% 
  mutate(vsota = sum(n),
         no_cconj = vsota - n[upos == "CCONJ"],
         no_sconj = vsota - n[upos == "SCONJ"]) %>% 
  filter(upos == "SCONJ") %>% 
  dplyr::select(doc_id, n, no_sconj) %>% 
  pivot_longer(-doc_id, 
               names_to = 'kategorija', values_to = 'vrednost') %>%
  pivot_wider(id_cols = kategorija, 
              names_from = doc_id, values_from = vrednost)
)
## # A tibble: 2 x 3
##   kategorija  doc1  doc2
##   <chr>      <int> <int>
## 1 n           1687  1296
## 2 no_sconj   70748 66441

Beide \(\chi^2\)-Tests bestätigen einen statistisch signifikanten Unterschied zwischen den beiden Romanen. Die Häufigkeits- bzw. Prozentzahlen deuten darauf hin, dass im Prozess mehr Subjunktoren verwendet werden als im Tom Sawyer und weniger Junktoren.

chisq.test(koord[,-1])
## 
##  Pearson's Chi-squared test with Yates' continuity correction
## 
## data:  koord[, -1]
## X-squared = 196.24, df = 1, p-value < 2.2e-16
chisq.test(subord[,-1])
## 
##  Pearson's Chi-squared test with Yates' continuity correction
## 
## data:  subord[, -1]
## X-squared = 28.843, df = 1, p-value = 7.849e-08

Das könnte man so verstehen, dass im Tom Sawyer Parataxe dominanter ist als im Prozess und dass der erste Roman leichter zu verstehen sein könnte als der letztere. Allerdings dürfen wir an dieser Stelle nicht vergessen, dass wir lediglich Stichproben erhoben haben. Wir haben ja lediglich Sätze berücksichtigt, die Junktoren oder Subjunktoren enthalten. Uneingeleitete Sätze oder Sätze, eingeleitet durch Konjunktionaladverbien, Relativpronomina u.a., haben wir in unserer Stichprobenerhebung nicht berücksichtigt. Bei Einbezug solcher Sätze könnte sich das Ergebnis wesentlich ändern.

14.18.4 Lexikalische Einheiten

Das Program udpipe hat jede Wortform einem Lemma (einer Lexikoneinheit) zugeordnet. Wie viele Lemmas enthalten die Texte? Zu welchen Wortarten gehören sie am häufigsten? Wir stellen die Verhältnisse tabellarisch und graphisch dar. Die allgegenwärtigen Interpunktionszeichen (Komma, Punkt usw.) werden herausgefiltert.

(tabela2 = x %>% 
  group_by(doc_id, upos) %>% 
    filter(!is.na(upos),
           upos != "PUNCT",
           upos != "X") %>% 
  distinct(lemma) %>% 
  count(lemma) %>% 
  summarise(lemmas = sum(n)) %>% 
  mutate(prozent = round(lemmas/sum(lemmas), 4)) %>% 
  arrange(-prozent)
)
## # A tibble: 26 x 4
## # Groups:   doc_id [2]
##    doc_id upos  lemmas prozent
##    <chr>  <chr>  <int>   <dbl>
##  1 doc2   NOUN    3401  0.361 
##  2 doc1   NOUN    2519  0.352 
##  3 doc1   VERB    1696  0.237 
##  4 doc1   ADJ     1528  0.213 
##  5 doc2   VERB    1934  0.206 
##  6 doc2   ADJ     1875  0.199 
##  7 doc2   PROPN    973  0.103 
##  8 doc1   ADV      605  0.0845
##  9 doc2   ADV      671  0.0713
## 10 doc1   PROPN    387  0.054 
## # ... with 16 more rows
tabela2 %>% 
  # slice_max(order_by = prozent, n=6) %>% 
  mutate(upos = reorder_within(upos, lemmas, paste("(",100*prozent,"%)"), sep = " ")) %>%
  ggplot(aes(prozent, upos, fill = upos)) +
  geom_col() +
  facet_wrap(~ doc_id, scales = "free") +
  theme(legend.position = "none") +
  scale_x_continuous(labels = percent_format()) +
  labs(x = "Anteil", y = "Wortklasse")

Aus unseren beiden Darstellungen der Lemmahäufigkeit ist ersichtlich, dass in beiden Texten Nomina (noun) am häufigsten vertreten sind (mehr als ein Drittel aller Lemmas), gefolgt von Verben und Adjektiven. Das ist auch für andere Texte das typische Bild, da es sich bei diesen drei Klassen um offene Wortklassen handelt. Funktionswörter gehören zu den geschlossenen Wortklassen, die nur aus relativ wenigen (oft unflektierten) Einheiten bestehen und im nur sehr geringen Maße erweiterbar sind. So machen die in fast allen Texten häufig auftretenden Pronomina (Personalpronomen, demonstrativpronomen, Possessivpronomen usw.) weniger als zwei Prozent aller Lemmas in den beiden Romanen aus. Entsprechendes ist auch bei den anderen Funktionswortklassen zu beobachten: sie haben eine hohe Vorkommenshäufigkeit (Tokenfrequenz), aber geringe Lemmahäufigkeit.

14.18.5 Wortkorrelationen

Bei der Inhaltsanalyse kann es von Nutzen sein, mehr darüber zu erfahren, welche Wörter im Text miteinander häufig verknüpft werden. Oben haben wir bereits nach Kollokationen in den beiden Romanen recherchiert. Eine ähnliche Methode ist die Korrelationsanalyse, die sich aber nicht auf das direkte Nacheinanderauftreten von Wörtern beschränkt. Unsere nächste Untersuchungsfrage lautet daher: Welche Worthäufigkeiten nehmen parallel zu oder ab? Können wir also paarweise Korrelationen von Wörtern nachweisen?

Zu diesem Zweck setzen wir das Programm widyr ein. Ein ähnliches Analysewerkzeug ist übrigens auch bei Voyant Tools zu finden. Im folgenden Programmchunk erstellen wir eine Tabelle, die miteinander auftretende Lemmas und ihre Korrelation anführt.

library(widyr)

# pairwise correlation
(correlations = x %>% 
  filter(dep_rel != "punct", dep_rel != "nummod") %>%
  mutate(lemma = tolower(lemma), token = tolower(token),
         lemma = str_trim(lemma), token = str_trim(token)) %>% 
  janitor::clean_names() %>%
  group_by(doc_id, lemma, token, sentence_id) %>% 
  # add_count(token) %>% 
  summarize(Freq = n()) %>% 
  arrange(-Freq) %>% 
  filter(Freq > 2) %>% 
  pairwise_cor(lemma, sentence_id, sort = TRUE) %>% 
  filter(correlation < 1 & correlation > 0.3)
)
## # A tibble: 2,592 x 3
##    item1          item2          correlation
##    <chr>          <chr>                <dbl>
##  1 verteidigung   natürlich            0.865
##  2 natürlich      verteidigung         0.865
##  3 stellvertreter direktor             0.812
##  4 direktor       stellvertreter       0.812
##  5 bürstner       fräulein             0.741
##  6 fräulein       bürstner             0.741
##  7 master         jim                  0.706
##  8 depot          jim                  0.706
##  9 eimer          jim                  0.706
## 10 glaskugel      jim                  0.706
## # ... with 2,582 more rows

Die Korrelationswerte liegen zwischen 1 und -1. Je stärker die Verbindung zwischen zwei Wortitems (Lemmas), umso höher ist der Korrelationswert. Positive Korrelatonswerte bedeuten, dass zwei Lemmas einander anziehen (d.h. häufiger miteinander auftreten), negative Korrelationswerte dagegen, dass sie einander abstoßen (d.h. seltener miteinander auftreten).

Als erstes Beispiel wählen wir das Lemma Zaun aus dem Roman Tom Sawyer. Welche Lemmas sind positiv oder negativ damit korreliert (d.h. treten häufger miteinander auf oder umgekehrt)?

correlations %>%
  filter(item1 == "zaun") %>%
  mutate(item2 = fct_reorder(item2, correlation)) %>%
  ggplot(aes(item2, correlation, fill = item2)) +
  geom_col(show.legend = F) +
  coord_flip() +
  labs(title = "What tends to appear with 'Zaun'?",
       subtitle = "Among elements that appeared in at least 2 sentences")

Die beiden Lemmas Spaß und Arbeit sind stärker mit Zaun korreliert. Das macht Sinn, wenn man das entsprechende Kapitel im Roman gelesen hat: Tom Sawyer hat von seiner Tante die Aufgabe erhalten, den Gartenzaun anzustreichen. Das macht Tom überhaupt keinen Spaß. Ein Junge kommt vorbei und Tom kommt eine Idee. Er tut so, als würde die Arbeit Spaß machen. Das war nur ein Trick von Tom. Tom gelang es, den anderen Jungen zu überzeugen, dass Zaunanstreichen Spaß macht. Der andere Junge übernahm Toms Arbeit.

Ein weiteres Beispiel aus Kafkas Roman Der Prozess wäre das Lemma Gericht.

correlations %>%
  filter(item1 == "gericht") %>%
  mutate(item2 = fct_reorder(item2, correlation)) %>%
  ggplot(aes(item2, correlation, fill = item2)) +
  geom_col(show.legend = F) +
  coord_flip() +
  labs(title = "What tends to appear with 'Gericht'?",
       subtitle = "Among elements that appeared in at least 2 sentences")

Im Diagramm sind mehrere Lemmas zu sehen, die mittelstark mit dem Lemma Gericht korrelieren. Am meisten Sinn ergeben in diesem Zusammenhang die Lemmas Anklage, unschuldig, frei und zwingen.

14.19 Sentiment

Stopnjo čustvenosti ali emocionalnosti besedila je mogoče določiti s sentimentnim slovarjem.

14.19.1 Version 1

Zuerst soll das nrc-Sentimentlexikon für Deutsch zum Einsatz kommen, das im Programmpaket von syuzhet enthalten ist. Für die Sentimentanalyse mit syuzhet wird ein Text erst einmal mit der Funktion get_sentences() in Sätze zerlegt.

library(syuzhet)

tom_v = get_sentences(txt$text[2]) # izberemo drugo besedilo: tom.txt
tom_v = (tom_v[-1]) # tako lahko izločimo prvo vrstico (uredniško pripombo)
head(tom_v[-1])
## [1] "Das eine oder das andere habe ich selbst erlebt , die anderen meine Schulkameraden ."                                                                                                                                                                                                                                                                                                   
## [2] "Huck Finn ist nach dem Leben gezeichnet , nicht weniger Tom Sawyer , doch entspricht dieser nicht einer bestimmten Persönlichkeit , sondern wurde mit charakteristischen Zügen mehrerer meiner Altersgenossen ausgestattet und darf daher jenem gegenüber als einigermaßen kompliziertes psychologisches Problem gelten ."                                                              
## [3] "Ich muß hier bemerken , daß zur Zeit meiner Erzählung -- vor dreißig bis vierzig Jahren -- unter den Unmündigen und Unwissenden des Westens noch die seltsamsten , unwahrscheinlichsten Vorurteile und Aberglauben herrschten ."                                                                                                                                                        
## [4] "Obwohl dies Buch vor allem zur Unterhaltung der kleinen Welt geschrieben wurde , so darf ich doch wohl hoffen , daß es auch von Erwachsenen nicht ganz unbeachtet gelassen werde , habe ich doch darin versucht , ihnen auf angenehme Weise zu zeigen , was sie einst selbst waren , wie sie fühlten , dachten , sprachen , und welcher Art ihr Ehrgeiz und ihre Unternehmungen waren ."
## [5] "Erstes Kapitel ."                                                                                                                                                                                                                                                                                                                                                                       
## [6] ", ,Tom !"

Die Funktion get_sentiment() weist den Wörtern in den Äußerungen einen positiven (+1), negativen (-1) oder neutralen (0) Stimmungswert (Sentiment, Emotionswert) zu. Anschließend werden diese Sentimentwerte summiert.

tom_values <- get_sentiment(tom_v, 
                            method = "nrc", language = "german")
length(tom_values)
## [1] 5047
tom_values[100:110]
##  [1]  0 -2  0  1  0  1  0  0  0  0  0

Wir binden die Äußerungen, die Emotionswerte und die Satzlänge in einen Datensatz ein. So lässt sich besser beurteilen, wie erfolgreich der Einsatz des Sentiment-Wörterbuchs in unserem Text war. Außerdem wollen wir auch einige Spalten umbenennen.

sentiment1 = cbind(tom_v, tom_values, ntoken(tom_v)) %>% 
  as.data.frame() %>% 
  rename(words = V3,
         text = tom_v,
         values = tom_values) %>% 
  mutate(doc_id = "tom.txt") %>% 
  rowid_to_column(var = "sentence")

# View(sentiment1)
sentiment1 %>% rmarkdown::paged_table()

Wiederholen Sie die obigen Programschritte für den anderen Text, den wir mit dem ersten vergleichen möchten, und zwar mit Kafaks Prozess.

prozess_v = get_sentences(txt$text[1]) # izberemo prvo besedilo: prozess.txt
prozess_v = (prozess_v[-1]) # tako lahko izločimo prvo vrstico (uredniško pripombo)
prozess_values <- get_sentiment(prozess_v, method = "nrc", language = "german")
sentiment2 = cbind(prozess_v, prozess_values, ntoken(prozess_v)) %>% 
  as.data.frame() %>% 
  rename(words = V3,
         text = prozess_v,
         values = prozess_values) %>% 
  mutate(doc_id = "prozess.txt") %>% 
  rowid_to_column(var = "sentence")

# View(sentiment2)
sentiment2 %>% rmarkdown::paged_table()

Durch Addition der Stimmungswerte lässt sich abschätzen, welcher Text einen größeren Anteil positiv bewerteter Wörter enthält. Zu diesem Zweck wollen wir die beiden Datensätze zusammenführen und außerdem das Format der Spalten “Wörter” und “Werte” anpassen.

sentiment = rbind(sentiment1, sentiment2) %>% as_tibble() %>% 
  mutate(values = parse_number(values),
         words = parse_number(words)) %>%
  dplyr::select(doc_id, sentence, words, values, text)

sentiment %>% rmarkdown::paged_table()

Das Ergebnis: Nach der obigen Methode ist der Durchschnitt der emotionalen Werte im Roman Prozess etwas höher (0,055) als im Roman Tom Sawyer ().

Dieses Ergebnis war unerwartet, denn Tom Sawyer enthält viele heitere Geschichten. Aber möglicherweise dominieren bestimmte Kapitel mit negativ konnotierten Wörtern (z.B. Flüche, heutzutage als rassistisch angesehene Wörter wie z.B. Nigger u.a.) und vielleicht auch die unheimlichen oder gefährlichen Begegnungen mit bestimmten Personen das Gesamtergebnis beeinflusst haben. Natürlich kann es durchaus sein, dass unser Sentimentlexikon zu viele Wortformen nicht erfasst hat und daher das Ergebnis verfälscht ist.

sentiment %>% 
  group_by(doc_id) %>% 
  summarise(polarnost = mean(values))
## # A tibble: 2 x 2
##   doc_id      polarnost
##   <chr>           <dbl>
## 1 prozess.txt    0.0550
## 2 tom.txt       -0.0109

Da wir die beiden Romane nicht so schnell (erneut) durchlesen können, sollten wir es noch auf andere Weise versuchen: Behandeln wir doch positive, neutrale und negative Werte getrennt und berücksichtigen wir dabei auch die Satzlänge!

sentiment1 = sentiment %>% 
  group_by(doc_id) %>% 
  mutate(positive = ifelse(values > 0, abs(values), 0),
         neutral = ifelse(values == 0, 1, 0),
         negative = ifelse(values < 0, abs(values), 0))
sentiment1 %>% 
  summarise(pos = mean(100*positive/words),
            neut = mean(100*neutral/words),
            neg = mean(100*negative/words))
## # A tibble: 2 x 4
##   doc_id        pos  neut   neg
##   <chr>       <dbl> <dbl> <dbl>
## 1 prozess.txt  2.30  4.34  2.13
## 2 tom.txt      2.63  6.77  2.81

Alle durchschnittlichen Sentimentwerte (egal, ob positiv, negativ oder neutral) sind in Tom Sawyer etwas höher als im Prozess. Am deutlichsten ist der Unterschied zwischen den als neutral bewerteten Wörtern.

Versuchen wir mit einem t-Test herauszufinden, welche Unterschiede statistisch signifikant sind! Da wir die Varianzen der Sentimentwerte nicht kennen, wählen wir den Welch-t-Test, der bei ungleichen Varianzen zum Einsatz kommt (var.equal = FALSE).

t.test(positive ~ doc_id, data = sentiment1, var.equal = FALSE)
## 
##  Welch Two Sample t-test
## 
## data:  positive by doc_id
## t = 3.8797, df = 7776.3, p-value = 0.0001054
## alternative hypothesis: true difference in means between group prozess.txt and group tom.txt is not equal to 0
## 95 percent confidence interval:
##  0.03729845 0.11348152
## sample estimates:
## mean in group prozess.txt     mean in group tom.txt 
##                 0.4797886                 0.4043987
t.test(negative ~ doc_id, data = sentiment1, var.equal = FALSE)
## 
##  Welch Two Sample t-test
## 
## data:  negative by doc_id
## t = 0.40793, df = 8554.9, p-value = 0.6833
## alternative hypothesis: true difference in means between group prozess.txt and group tom.txt is not equal to 0
## 95 percent confidence interval:
##  -0.03629760  0.05537492
## sample estimates:
## mean in group prozess.txt     mean in group tom.txt 
##                 0.4248349                 0.4152962
t.test(neutral ~ doc_id, data = sentiment1, var.equal = FALSE)
## 
##  Welch Two Sample t-test
## 
## data:  neutral by doc_id
## t = -5.1647, df = 8142.5, p-value = 2.465e-07
## alternative hypothesis: true difference in means between group prozess.txt and group tom.txt is not equal to 0
## 95 percent confidence interval:
##  -0.07649156 -0.03440200
## sample estimates:
## mean in group prozess.txt     mean in group tom.txt 
##                 0.4803170                 0.5357638

Zwei t-Tests weisen einen signifikanten Unterschied aus, und zwar bei positiven und neutralen Sentimentwerten. Bei negativen Sentimentwerten ist der Unterschied zwischen den beiden Romanen nicht signifikant.

Dieses Ergebnis entspricht eher unseren oben beschriebenen Erwartungen: Tom Sawyer scheint im Durchschnitt positivere und neutralere Sentimentwerte aufzuweisen als der Prozess, bei negativen Sentimentwerten konnte dagegen kein signifikanter Unterschied zwischen den beiden Romanen festgestellt werden.

Schauen wir uns nun einige der Sätze an, die negativ bewertet wurden:

sentiment1 %>% 
  filter(negative > 0) %>% 
  rmarkdown::paged_table()

14.19.2 Variante 2

Im zweiten Versuch wählen wir das nrc-Sentimentlexikon, und zwar mit Hilfe der Funktion get_nrc_sentiment().

tom_v = get_sentences(txt$text[2])
tom_nrc_values = get_nrc_sentiment(tom_v)
tom_joy_items = which(tom_nrc_values$joy > 0)
head(tom_v[tom_joy_items], 4)
## [1] "Obwohl dies Buch vor allem zur Unterhaltung der kleinen Welt geschrieben wurde , so darf ich doch wohl hoffen , daß es auch von Erwachsenen nicht ganz unbeachtet gelassen werde , habe ich doch darin versucht , ihnen auf angenehme Weise zu zeigen , was sie einst selbst waren , wie sie fühlten , dachten , sprachen , und welcher Art ihr Ehrgeiz und ihre Unternehmungen waren ."
## [2] ", Spare die Rute , und du verdirbst dein Kind ' , heißt es ."                                                                                                                                                                                                                                                                                                                           
## [3] "Er ist meiner toten Schwester Kind , ein armes Kind , und ich habe nicht das Herz , ihn irgendwie am Gängelband zu führen ."                                                                                                                                                                                                                                                            
## [4] "Es ist wohl hart für ihn , am Samstag stillzusitzen , wenn alle anderen Knaben Feiertag haben , aber er haßt Arbeit mehr als irgend sonst was , und ich will meine Pflicht an ihm tun , oder ich würde das Kind zu Grunde richten ."
nrc_sentiment = as.data.frame(cbind(tom_v, tom_nrc_values))
nrc_sentiment %>% rmarkdown::paged_table()

Die beiden folgenden linearen Regressionsanalysen zeigen, dass die positiven Sentimentwerte vor allem von den emotionalen Werten joy, trust, anticipation getragen werden, die negativen Sentimentwerte dagegen von den emotionalen Werten anger, disgust, fear, sadness. Der emotionale Wert surprise (Überraschung) scheint negativ mit dem positiven Sentiment korreliert zu sein. Überraschungen können natürlich nicht nur angenehm (positiv) sein, sondern auch unangenehm (negativ).

library(jtools)
mpos <- lm(positive ~ joy + trust + anticipation + surprise, 
           data = nrc_sentiment)
summ(mpos)
Observations 5048
Dependent variable positive
Type OLS linear regression
F(4,5043) 1302.59
R2 0.51
Adj. R2 0.51
Est. S.E. t val. p
(Intercept) 0.01 0.00 5.51 0.00
joy 0.95 0.02 46.99 0.00
trust 0.19 0.01 20.47 0.00
anticipation 0.09 0.02 3.74 0.00
surprise -0.06 0.03 -2.32 0.02
Standard errors: OLS
mneg <- lm(negative ~ anger + disgust + fear + sadness + surprise, 
           data = nrc_sentiment)
summ(mneg)
Observations 5048
Dependent variable negative
Type OLS linear regression
F(5,5042) 7713.63
R2 0.88
Adj. R2 0.88
Est. S.E. t val. p
(Intercept) 0.03 0.00 9.49 0.00
anger 0.41 0.02 18.79 0.00
disgust 0.17 0.02 9.13 0.00
fear 0.93 0.01 113.82 0.00
sadness 0.10 0.01 9.35 0.00
surprise 0.01 0.03 0.29 0.77
Standard errors: OLS

14.19.3 Variante 3

Im dritten Versuch soll ein Wortliste mit emotionalen Werten zum Einsatz kommen, und zwar das BAWLR.

# This lexicons contains values of Emotional valence and arousal ranging from 1 to 5.
# But this extended version contains also binary Emo_Val values (1, -1).
bawlr <- read.delim2("data/BAWLR_utf8.txt", sep = "\t", dec = ",", fileEncoding = "UTF-8", 
                     header = T, stringsAsFactors = T)
# # bawlr$EmoVal <- as.character(bawlr$EmoVal)
# # str(EmoVal)
# bawlr$EmoVal <- gsub('NEG', '-1', bawlr$EmoVal)
# bawlr$EmoVal <- gsub('POS', '1', bawlr$EmoVal)
# bawlr$EmoVal <- as.numeric(bawlr$EmoVal)
bawlr %>% rmarkdown::paged_table()

Machen wir zwei Listen, und zwar eine Liste mit positiv bewerteten Wörtern und eine mit negativ bewerteten Wörtern!

positive.words = bawlr %>% 
  mutate(WORD_LOWER = as.character(WORD_LOWER)) %>% 
  dplyr::select(EmoVal, WORD_LOWER) %>% 
  filter(EmoVal == "POS") %>% 
  dplyr::select(WORD_LOWER) %>% 
  filter(str_detect(WORD_LOWER, "[a-zA-Z]"))

negative.words = bawlr %>% 
  mutate(WORD_LOWER = as.character(WORD_LOWER)) %>% 
  dplyr::select(EmoVal, WORD_LOWER) %>% 
  filter(EmoVal == "NEG") %>% 
  dplyr::select(WORD_LOWER) %>% 
  filter(str_detect(WORD_LOWER, "[a-zA-Z]"))

Daraus erstellen wir ein quanteda Lexikon, und zwar mit der Funktion dictionary():

bawlr_dict = dictionary(list(positive = list(positive.words), negative = list(negative.words)))

Wir verwenden eine in vorherigen Abschnitten gebildete Matrix (dfm) mit Lexikoneinheiten (Lemmas), da das bawlr_dict-Wörterbuch nur die Grundform der Lemmata enthält.

matrika_lemmas = dfm(matrika_lem, tolower = TRUE)

result = matrika_lemmas %>% 
  dfm_lookup(bawlr_dict) %>% 
  convert(to = "data.frame") %>% 
  as_tibble
result
## # A tibble: 2 x 3
##   doc_id      positive negative
##   <chr>          <dbl>    <dbl>
## 1 prozess.txt     7330     4000
## 2 tom.txt         6386     3706

Wir können die Gesamtwortlänge hinzufügen, wenn das Ergebnis in Bezug auf die Länge der Texte normalisiert werden soll.

result = result %>% mutate(length=ntoken(matrika_lemmas))
result
## # A tibble: 2 x 4
##   doc_id      positive negative length
##   <chr>          <dbl>    <dbl>  <int>
## 1 prozess.txt     7330     4000  32058
## 2 tom.txt         6386     3706  33520

Normalerweise wollen wir den Gesamtstimmungswert berechnen. Dafür gibt es mehrere Möglichkeiten: z.B.
- die negativen Werte von den positiven zu subtrahieren und dann die Differenz durch die Summe der beiden Kategorien zu dividieren,
- die negativen Werte von den positiven Werten zu subtrahieren und dann die Differenz durch die Länge der Texte zu dividieren.

Wir können auch den Grad der Subjektivität berechnen, d.h. wie viele emotionale Werte insgesamt ausgedrückt werden:

result = result %>% mutate(sentiment1=(positive - negative) / (positive + negative))
result = result %>% mutate(sentiment2=(positive - negative) / length)
result = result %>% mutate(subjektivnost=(positive + negative) / length)
result %>% rmarkdown::paged_table()

Der Subjektivitätsgrad scheint demnach im Prozess etwas höher zu sein als im Tom Sawyer.

14.19.3.1 Farbliche Sentimentmarkierung

Das Programm corpustools ermöglicht die farbliche Kodierung der Textwörter gemäß den Stimmungswerten, die im Sentimentlexikon verzeichnet sind.

Der erste Schritt besteht darin, einen tcorpus anzulegen.

library(corpustools)
t = create_tcorpus(txt, doc_column="doc_id")

Der zweite Schritt ist eine Wortrecherche im tcorpus:

t$code_dictionary(bawlr_dict, column = 'bawlr')
t$set('sentiment', 1, subset = bawlr %in% c('positive','neg_negative'))
t$set('sentiment', -1, subset = bawlr %in% c('negative','neg_positive'))

Dies ermöglicht die Anzeige der farbkodierten Texte im “Viewer”-Fenster:

browse_texts(t, scale='sentiment')

Der farbkodierte Text kann in einem Webbrowser angezeigt und als HTML-Datei gespeichert werden:

browse_texts(t, scale='sentiment', filename = "sentiment_prozess_tom.html", 
             header = "Sentiment in Kafkas Prozess und Twains Tom Sawyer")