Kapitel 8 Buchstaben in Romanen

8.1 Packages

library(tidyverse)
library(tidytext)
library(scales)
library(readtext)
library(rmarkdown)
# library(qdap) # syllable_count and syllable_sum
# library(quanteda) # nsyllable(tokens(txt))

Im Wikipedia-Artikel zum Thema Buchstabenhäufigkeit gibt die folgende Tabelle Auskunft über die Häufigkeit von Buchstaben in einer Stichprobe von deutschen Texten. Die Umlaute werden in dieser Tabelle als jeweils zwei Monophthonge gezählt.

library(readxl)
buchstabenhaeufigkeit <- 
  read_xlsx("data/wikipedia_buchstabenhaeufigkeit_deutsch.xlsx")
buchstabenhaeufigkeit %>% rmarkdown::paged_table()

Die ersten fünf Buchstaben haben einen Anteil von etwa der Hälfte, die häufigsten zehn Buchstaben decken etwa drei Viertel der relativen Buchstabenhäufigkeit in deutschen Texten ab.

Eine weitere Tabelle zeigt die Häufigkeit der Buchstaben in Texten aus einem Briefkorpus (Briefe aus den Jahren 1996-2004). In diesem Fall sind auch die Frequenzen der Umlaute erhoben worden. Die zehn häufigsten Buchstaben im Briefkorpus decken sich zum großen Teil mit denen im vorher gezeigten.

library(readxl)
buchstabenhaeufigkeit_briefe <- 
  read_xlsx("data/wikipedia_buchstabenhaeufigkeit_briefkorpus.xlsx")
buchstabenhaeufigkeit_briefe %>% rmarkdown::paged_table()

In einem anderen Wikipedia-Artikel mit dem Titel Frekvence črk werden die relativen Häufigkeiten der Buchstaben in slowenischen belletristischen Texten tabellarisch dargestellt und einigen anderen Sprachen gegenübergestellt. In dieser Tabelle fällt auf, dass die Graphme der Vollvokale a und o einen deutliche höheren Rang einnehmen als in den beiden Tabellen für deutsche Texte. Ähnlich wie in den Tabellen für die deutschen Texte ist wiederum, dass die Vokalgrapheme e und i zu den häufigsten gehören. Unter den Konsonantgraphemen sind auch hier n, s, r und t stark vertreten.

library(readxl)
buchstabenhaeufigkeit_slov <- 
  read_xlsx("data/wikipedia_frekvence_crk.xlsx")
buchstabenhaeufigkeit_slov %>% rmarkdown::paged_table()

Wir stellen uns die Aufgabe, die Buchstabenhäufigkeit in von uns ausgewählten Texten literarischer Prosa tabellarisch zusammenzustellen und mit denen im Wikipedia-Artikel zu vergleichen. In den folgenden Abschnitten beschäftigen wir uns mit der Häufigkeit von Vokalgraphemen, Konsonantengraphemen, Konsonantenverbindungen und Silben in tabellarischer und graphischer Form.

8.2 Datensatz lesen

Die readtext()-Funktion erlaubt Einlesen von mehreren Dateien auf einfache Art und Weise. Mit docvarsfrom erhalten wird eine neue Spalte in der Tabelle, die wir mit der Funktion rename() umbenennen. Mit encoding = “UTF-8” teilen wir dem Programm mit, wie der Text kodiert ist (Code Page).

novels_txt = readtext("data/books/*.txt", 
                      docvarsfrom = "filenames", 
                      encoding = "UTF-8") %>% 
  rename(title = docvar1)
novels_txt
## readtext object consisting of 2 documents and 1 docvar.
## # Description: df [2 x 3]
##   doc_id      text                title  
##   <chr>       <chr>               <chr>  
## 1 prozess.txt "\"Der Prozes\"..." prozess
## 2 tom.txt     "\"Tom Sawyer\"..." tom

8.3 Buchstaben extrahieren

8.3.1 aus Liste

Der reguläre Ausdruck [a-zA-Z] extrahiert nur Buchstaben des englischen Alphabets, [:alpha:] extrahiert dagegen auch nicht-englische Buchstaben, z.B. deutsche oder slowenische Sonderzeichen. Zahlen und andere spezielle Zeichen (z.B. Interpunktion) werden auf diese Weise nicht extrahiert.

Regex {1} (= default) extrahiert Einzelbuchstaben. Bei Verwendung von {2} werden jeweils zwei aufeinander folgende Buchstaben extrahiert.

Die Funktion tolower() sorgt dafür, dass Großbuchstaben in Kleinbuchstaben umgewandelt werden. Falls zwischen großen und kleinen Buchstaben unterschieden werden soll, entfernen wir diese Funktion aus dem Programmkode.

letters = tolower(novels_txt$text) %>% str_extract_all(pattern = "[:alpha:]{1}")
letters[[1]][1:10]
##  [1] "d" "e" "r" "p" "r" "o" "z" "e" "s" "s"
letters[[2]][1:9]
## [1] "t" "o" "m" "s" "a" "w" "y" "e" "r"

8.3.2 aus Datensatz

Tabellen und Graphiken erstellen ist leichter, wenn wir die Texte in Datensätze umwandeln, und zwar mit der Funktion as.data.frame().

novels = as.data.frame(novels_txt)

Mit der Funktion unnest_tokens() können wir auch Buchstaben isolieren und anschließend auszählen.

library(tidytext)

novels_character <- novels %>%
  unnest_tokens(character, text, token = "characters", to_lower = TRUE, drop = T)

head(novels_character)
##        doc_id   title character
## 1 prozess.txt prozess         d
## 2 prozess.txt prozess         e
## 3 prozess.txt prozess         r
## 4 prozess.txt prozess         p
## 5 prozess.txt prozess         r
## 6 prozess.txt prozess         o

8.4 Buchstaben zählen

Mit count() können wir die Häufigkeit einer Variable (hier: der Buchstaben) auszählen.

novels_character %>% 
  count(character, sort = TRUE) %>% head(3)
##   character      n
## 1         e 114769
## 2         n  70151
## 3         i  52767

Der tidytext-Tokenizer hat nicht nur Buchstaben, sondern auch Zahlen extrahiert. Da wir nur an der Häufigkeit von Buchstaben interessiert sind, filtern wir die Zahlen und andere Zeichen heraus. Dazu verwenden wir die Funktionen filter() und zusätzlich str_detect(), da wir für diese Aufgabe einen regulären Ausdruck nutzen wollen.

novels_character %>% 
  filter(str_detect(character, "[:alpha:]")) %>% 
  count(character, sort = T) %>% head(3)
##   character      n
## 1         e 114769
## 2         n  70151
## 3         i  52767

Ein paar Zeichen, die nicht zum deutschen Alphabet gehören und mit dem vorherigen Programm-Schritt nicht herausfiltern konnten, werden im nächsten Schritt ebenfalls herausgefiltert.

Wir speichern das Ergebnis als neue Tabelle mit dem Namen char_freq. Die zehn häufigsten Buchstaben in dieser Tabelle decken sich mit denen in den beiden eingangs gezeigten Tabellen aus dem Wikipedia-Artikel über Buchstabenhäufigkeit, insbesondere mit der, die auf einem Briefkorpus beruhte.

char_freq = novels_character %>% 
  filter(str_detect(character, "[:alpha:]")) %>% 
  filter(!str_detect(character, "é|á")) %>% 
  count(character, sort = T)

library(DT)
char_freq %>% 
    DT::datatable(fillContainer = FALSE, filter = "top",
                options = list(pageLength = 10))

Insgesamt haben wir 30 Buchstaben des deutschen Alphabets in den Romanen unterschieden. Aus wie vielen Buchstaben des deutschen Alphabets bestehen die Romane? Die Summe erhalten wir mit der Funktion summarise() - fast 700 Tausend.

char_freq %>% 
  summarise(total = sum(n))
##    total
## 1 694556

Es ist nun wirklich Zeit, mal ein Bild zu malen! Dazu verwenden wir das Programm (library) ggplot2, das im Programmbündel tidyverse enthalten ist.

Das Diagramm zeigt sehr deutlich, dass gewaltige Häufigkeitsunterschiede im deutschen Alphabet bestehen.

char_freq %>% 
  mutate(character = fct_reorder(character, n)) %>% # Sortieren nach Frequenz
  ggplot(aes(n, character, fill = character)) +
  geom_col() +
  theme(legend.position = "none")

Eine bessere Vorstellung von den Zahlenverhältnissen erhalten wir, wenn wir die mehrstelligen Zahlenwerte in Prozente umwandeln.

library(scales)
char_freq %>% 
  mutate(Prozent = n / sum(n)) %>% # Umwandlung in Prozente
  ungroup() %>% 
  mutate(character = fct_reorder(character, Prozent)) %>% # Sortieren nach Prozenten
  ggplot(aes(Prozent, character, fill = character)) +
  geom_col() +
  theme(legend.position = "none") +
  scale_x_continuous(labels = percent_format(
    decimal.mark = ",", accuracy = 1)) # Prozent-Format

Getrennte tabellarische Darstellung für die Texte:

novels_character %>% 
  group_by(doc_id) %>% 
  count(character, sort = TRUE) %>% 
  pivot_wider(names_from = doc_id, values_from = n) %>% 
  DT::datatable(fillContainer = FALSE, filter = "top",
                options = list(pageLength = 10))

Getrennte graphische Darstellung für die Texte:

library(scales)
novels_character %>% 
  group_by(doc_id) %>% 
  count(character, sort = TRUE) %>% 
  mutate(Prozent = n / sum(n)) %>% # Umwandlung in Prozente
  ungroup() %>% 
  mutate(character = fct_reorder(character, Prozent)) %>% # Sortieren nach Prozenten
  filter(Prozent > 0.0001) %>% 
  ggplot(aes(Prozent, character, fill = character)) +
  geom_col() +
  theme(legend.position = "none") +
  facet_wrap(~ doc_id, scales = "free")

  scale_x_continuous(labels = percent) # Prozent-Format
## <ScaleContinuousPosition>
##  Range:  
##  Limits:    0 --    1

8.5 Vokale

Betrachten wir zunächst nur die Buchstaben, die Vokale symbolisieren! Zu diesem Zweck bilden wir eine Vokalliste. Zwischen den Vokalen setzen wir das “oder”-Zeichen ein: den logischen Operator “|”.

vokale = "a|e|i|o|u|ä|ö|ü|y"

Die Vokalliste “vokale” verwenden wir mit den Funktionen filter() und str_detect().

library(scales)
char_freq %>% 
  filter(str_detect(character, vokale)) %>% 
  mutate(Prozent = n / sum(n)) %>% # Umwandlung in Prozente
  ungroup() %>% 
  mutate(character = fct_reorder(character, Prozent)) %>% # Sortieren nach Prozenten
  ggplot(aes(Prozent, character, fill = character)) +
  geom_col() +
  theme(legend.position = "none") +
  labs(y = "Vokale", x = "Häufigkeit in Romanen") +
  scale_x_continuous(labels = percent_format(accuracy = 1), 
                     breaks = seq(0, 0.50, 0.05)) # Prozent-Format

Am häufigsten kommt der Buchstabe “e” in den Romanen vor (fast 45%-iger Anteil unter den Vokalen!), am seltensten “y”, welches im Wesentlichen in Fremd- und Lehnwörtern auftritt.

8.6 Konsonanten

Welche Buchstaben, die Konsonanten symbolisieren, kommen am häufigsten vor? Zum Filtern verwenden wir wiederum die Vokalliste, dieses Mal allerdings mit Negationszeichen “!”.

library(scales)
char_freq %>% 
  filter(!str_detect(character, vokale)) %>% # Negationszeichen, daher Konsonanten beibehalten
  mutate(Prozent = n / sum(n)) %>% # Umwandlung in Prozente
  ungroup() %>% 
  mutate(character = fct_reorder(character, Prozent)) %>% # Sortieren nach Prozenten
  ggplot(aes(Prozent, character, fill = character)) +
  geom_col() +
  theme(legend.position = "none") +
  labs(y = "Konsonanten", x = "Häufigkeit in Romanen") +
  scale_x_continuous(labels = percent_format(accuracy = 1), breaks = seq(0, 0.50, 0.02)) # Prozent-Format und Einheiten

Der Buchstabe “n” kommt in den Romanen am häufigsten vor, gefolgt von den Buchstaben: “r, s, t, h, d”. Selten sind die Buchstaben: “x, q, p, ß, v”.

8.7 Vokal-Konsonant-Verhältnis

Welches Zahlenverhältnis besteht zwischen den Vokalen und Konsonanten?

21 konsonantische Buchstaben und 9 vokalische Buchstaben. Pro Silbe sind in den deutschen Texten 1 Vokal und ungefähr 2 Konsonanten zu erwarten, also Silbenstrukturen wie z.B. KVK, KKV, VKK.

bs_ratio = char_freq %>% 
  mutate(buchstabe = ifelse(str_detect(character, vokale), "Vokal", "Konsonant")) %>% 
  count(buchstabe) %>% 
  mutate(Prozent = n / sum(n))
  
bs_ratio
##   buchstabe  n Prozent
## 1 Konsonant 21     0.7
## 2     Vokal  9     0.3

Der höhere Anteil der Konsonanten entspricht der größeren Konsonantenmenge.

ggplot(bs_ratio, aes(x = "", y = Prozent, fill = buchstabe)) +
  geom_col(color = "black", size = 2) +
  coord_polar(theta = "y", start = -0 * pi / 180) +
  # scale_fill_discrete(labels = c("Konsonanten", "Vokale")) +
  scale_fill_manual(labels = c("Konsonanten", "Vokale"), 
                               values = c("#9E9AC8", "#6A51A3")) +
  theme(legend.position = "top", axis.text.y = element_blank(),
        axis.ticks = element_blank()) +
  labs(y = "", x = "Anteil % in Romanen") +
  scale_x_discrete(NULL, expand = c(0, 0)) +
  scale_y_continuous(
    labels = percent_format(accuracy = 1), 
    breaks = seq(0, 1, 0.1)) # Prozent-Format und Einheiten

Diese Zahlenwerte und -verhältnisse bilden einen möglichen Ausgangspunkt für intra- oder interlinguale Vergleiche.

8.8 Anzahl der Silben

Wie viele Silben kommen in den Romanen schätzungsweise vor und wie viele Buchstaben pro Silbe? Die genaue Bestimmung der Silbenanzahl für eine bestimmte Sprache kann aufgrund zahlreicher Besonderheiten ziemlich kompliziert sein. Die Anzahl der Silben schätzen wir daher mit einer Funktion des Programms nsyllable (Alternatives Programm: qdap).

Da wir die Silbenzählfunktion nur ein einziges Mal bemühen, rufen wir sie in der unten sichtbaren Form auf: nsyllable::nsyllable(buchstabenfolge).

novels_words = novels %>%
  unnest_tokens(word, text, token = "words", to_lower = TRUE, drop = T) %>% 
  mutate(syllables = nsyllable::nsyllable(as.character(word), language = "en")) %>% 
  mutate(letters = nchar(word))
  
novels_words %>% head(100) %>% 
  DT::datatable(fillContainer = FALSE, filter = "top",
                options = list(pageLength = 6))

Insgesamt (d.h. kumulativ gesehen) fast 139 Tausend Silben in den Romanen. Diese Zahl bietet einen möglichen Ausgangspunkt für Textvergleiche.

novels_words %>% 
  count(syllables) %>% 
  summarise(Silben = sum(n))
##   Silben
## 1 138811

Die meisten Wortformen in den Romanen bestehen aus einer Silbe (fast 60%) oder zwei Silben (fast 30%). Das ist typisch für deutsche Texte. Kurze Funktionswörter (meist eine Silbe) kommen wesentlich häufiger vor als andere Wortklassen (Substantive, Verben, Adjektive, Adverbien).

novels_words %>% 
  count(syllables) %>% 
  mutate(Prozent = n / sum(n)) %>% 
  ggplot(aes(syllables, Prozent, fill = factor(syllables))) +
  geom_col() +
  theme(legend.position = "none") +
  labs(x = "Silben") +
  scale_y_continuous(
    labels = percent, 
    breaks = seq(0, 0.75, 0.1)) # Prozent-Format und Einheiten
## Warning: Removed 1 rows containing missing values (position_stack).

Berücksichtig man lediglich distinktive Wortformen (also keine Wortwiederholungen), dann ergibt sich die folgende Verteilung, in der die Zweisilber (mehr als 30%) und Dreisilber (fast 30%) den größten Anteil haben.

novels_words %>% 
  distinct(word, .keep_all = T) %>%
  count(syllables) %>% 
  mutate(Prozent = n / sum(n)) %>% 
  ggplot(aes(syllables, Prozent, fill = factor(syllables))) +
  geom_col() +
  theme(legend.position = "none") +
  labs(x = "Silben") +
  scale_y_continuous(
    labels = percent, 
    breaks = seq(0, 0.75, 0.1)) # Prozent-Format und Einheiten
## Warning: Removed 1 rows containing missing values (position_stack).

8.9 Mittlere Wortlänge

Wir können die Wortlänge in geschriebenen Texten auf zumindest zwei grundlegende Arten messen: - die Anzahl der Silben pro Wort(form), - die Anzahl der Buchstaben pro Wort(form).

Die durchschnittliche Anzahl der Silben und Buchstaben pro Wort (distinkte Wortformen !) in den Romanen ist in der folgenden Tabelle zu sehen: - neben den Mittelwerten (Avg_Silben, Avg_Buchstaben) - auch die Standardabweichungen vom entsprechenden Mittelwert (Stdev_Silben, Stdev_Buchstaben). Die Mittelwerte oder arithmetischen Mittel können mit der Programmfunktion mean() berechnet werden, die Standardabweichungen mit der Funktion sd(). Die Standardabweichungen sind notwendig zur Feststellung nicht-zufälliger Unterschiede zwischen den Stichproben (d.h. den Romanen). Bei der Berechnung der Mittelwerte und Standardabweichungen geben wir dem Programm auch die Instruktion, etwaige leere Datenzeilen (NA) herauszufiltern, und zwar mit Hilfe von na.rm = TRUE. Wird dies unterlassen, kann dies dazu führen, dass ein Mittelwert bzw. Standardabweichung nicht berechnet werden kann.

In der folgenden Tabelle werden nur distinktive Wortformen (Types) berücksichtigt, d.h. als ob jede Wortform nur einmal pro Roman vorkommt.

novels_words %>% 
  group_by(title) %>% 
  distinct(word, .keep_all = T) %>%
  add_count(word) %>% 
  summarise(Avg_Silben = mean(syllables, na.rm = TRUE),
            Stdev_Silben = sd(syllables, na.rm = TRUE),
            Avg_Buchstaben = mean(letters, na.rm = TRUE),
            Stdev_Buchstaben = sd(letters, na.rm = TRUE)) %>% 
    DT::datatable(fillContainer = TRUE, filter = "top",
                options = list(pageLength = 4))

Die durchschnittliche Anzahl der Silben und Buchstaben pro Wortform (Token), bei Berücksichtigung von Wortwiederholungen in den Romanen, ist in der folgenden Tabelle zu sehen.

novels_words %>% 
  group_by(title) %>% 
  summarise(Avg_Silben = mean(syllables, na.rm = TRUE),
            Stdev_Silben = sd(syllables, na.rm = TRUE),
            Avg_Buchstaben = mean(letters, na.rm = TRUE),
            Stdev_Buchstaben = sd(letters, na.rm = TRUE)) %>% 
    DT::datatable(fillContainer = TRUE, filter = "top",
                options = list(pageLength = 4))

8.10 Testen von Mittelwertunterschieden

8.10.1 t-Test

Sind die berechneten Unterschiede zwischen den Mittelwerten relevant bzw. nicht-zufällig? Um diese Frage zu klären, kann man einen statistischen Test bemühen. Da wir lediglich zwei Samples (zwei Romanen) vergleichen wollen, kann uns ein parametrischer Test wie z.B. der t-Test Klarheit verschaffen. Wir verwenden die Programmfunktion t.test(). Der t-Test bestätigt, dass “Der Prozess” im Durchschnitt etwas längere Wörter aufweist (2,59 Silben pro Wort gegenüber 2,44 Silben pro Wort in “Tom Sawyer”) - wenn Anzahl distinktiver Wortformen verwendet.

syls = novels_words %>% 
  group_by(title) %>% 
  distinct(word, .keep_all = T) %>%
  add_count(word) %>% 
  drop_na() %>% 
  dplyr::select(title, word, syllables) %>% 
  pivot_wider(names_from = title, values_from = syllables)

t.test(syls$prozess, syls$tom, var.equal = TRUE) # significant
## 
##  Two Sample t-test
## 
## data:  syls$prozess and syls$tom
## t = 9.5813, df = 17554, p-value < 2.2e-16
## alternative hypothesis: true difference in means is not equal to 0
## 95 percent confidence interval:
##  0.1263172 0.1912927
## sample estimates:
## mean of x mean of y 
##  2.598953  2.440148

Wenn die Wiederholung von Wortformen berücksichtigt wird, bestätigt der t-Test ebenfalls einen signifikanten Unterschied zwischen den beiden Texten. Die durchschnittliche Anzahl der Wortsilben ist niedriger, da kürzere Wortformen (solche von Konjunktionen, Präpositionen, Artikeln und anderen Funktionswörtern) häufig vorkommen.

Schnelle Form des t-Tests:

t.test(syllables ~ title, data = novels_words, var.equal = TRUE)
## 
##  Two Sample t-test
## 
## data:  syllables by title
## t = 11.37, df = 137403, p-value < 2.2e-16
## alternative hypothesis: true difference in means between group prozess and group tom is not equal to 0
## 95 percent confidence interval:
##  0.04307118 0.06101337
## sample estimates:
## mean in group prozess     mean in group tom 
##              1.613350              1.561307

Dasselbe Ergebnis, aber aufwendiger zu programmieren, um den Datensatz in die entsprechende Form zu bringen:

prozess_syl <- novels_words %>% 
  filter(title == "prozess") %>% 
  dplyr::select(syllables) %>% 
  rename(prozess = syllables)
tom_syl <- novels_words %>% 
  filter(title == "tom") %>% 
  dplyr::select(syllables) %>% 
  rename(prozess = syllables)
  
t.test(prozess_syl, tom_syl, var.equal = T) # significant
## 
##  Two Sample t-test
## 
## data:  prozess_syl and tom_syl
## t = 11.37, df = 137403, p-value < 2.2e-16
## alternative hypothesis: true difference in means is not equal to 0
## 95 percent confidence interval:
##  0.04307118 0.06101337
## sample estimates:
## mean of x mean of y 
##  1.613350  1.561307

Der nächste t-Test bestätigt ebenfalls, dass “Der Prozess” im Durchschnitt längere Wörter aufweist (8,79 Buchstaben pro Wort gegenüber 8.36 Buchstaben pro Wort in “Tom Sawyer”.) Berücksichtigt wurden distinkte Wortformen (keine wiederholten Wortformen).

lets = novels_words %>% 
  group_by(title) %>% 
  distinct(word, .keep_all = T) %>%
  add_count(word) %>% 
  drop_na() %>% 
  dplyr::select(title, word, letters) %>% 
  pivot_wider(names_from = title, values_from = letters)

t.test(lets$prozess, lets$tom, var.equal = T) # significant
## 
##  Two Sample t-test
## 
## data:  lets$prozess and lets$tom
## t = 9.0317, df = 17554, p-value < 2.2e-16
## alternative hypothesis: true difference in means is not equal to 0
## 95 percent confidence interval:
##  0.3310081 0.5145045
## sample estimates:
## mean of x mean of y 
##  8.785878  8.363122

Wenn die Wiederholung von Wortformen berücksichtigt wird, bestätigt der t-Test wiederum einen signifikanten Unterschied zwischen den beiden Texten. Die durchschnittliche Anzahl der Buchstaben pro Wort ist niedriger, da kürzere Wortformen (solche von Konjunktionen, Präpositionen, Artikeln und anderen Funktionswörtern) häufig vorkommen.

Schnelle Form des t-Tests:

t.test(letters ~ title, data = novels_words, var.equal = TRUE)
## 
##  Two Sample t-test
## 
## data:  letters by title
## t = 5.0147, df = 138809, p-value = 5.319e-07
## alternative hypothesis: true difference in means between group prozess and group tom is not equal to 0
## 95 percent confidence interval:
##  0.04480651 0.10230526
## sample estimates:
## mean in group prozess     mean in group tom 
##              5.048860              4.975304

Dasselbe Ergebnis, aber aufwendiger zu programmieren, um den Datensatz in die entsprechende Form zu bringen:

prozess_let <- novels_words %>% 
  filter(title == "prozess") %>% 
  dplyr::select(letters) %>% 
  rename(prozess = letters)
tom_let <- novels_words %>% 
  filter(title == "tom") %>% 
  dplyr::select(letters) %>% 
  rename(prozess = letters)
  
t.test(prozess_let, tom_let, var.equal = TRUE) # significant
## 
##  Two Sample t-test
## 
## data:  prozess_let and tom_let
## t = 5.0147, df = 138809, p-value = 5.319e-07
## alternative hypothesis: true difference in means is not equal to 0
## 95 percent confidence interval:
##  0.04480651 0.10230526
## sample estimates:
## mean of x mean of y 
##  5.048860  4.975304

8.10.2 Lineare Regression

Hat man mehr als zwei Stichproben zu vergleichen, kann man eine lineare Regression durchführen, die auch das Testen von mehreren Einflussgrößen (Prädiktoren) erlaubt.

Hier folgt eine Demonstration anhand des bereits gehabten Datensatzes mit zwei Stichproben (Romanen). Die Ordinate im Koordinatensystem (Intercept, also der y-Abschnitt mit x = 0) ist bei zwei Stichproben gleich dem Mittelwert der ersten Stichprobe (title = “prozess”), d.h. 1,613350. Der geschätzte Mittelwert (Estimate) der zweiten Stichprobe (title = “tom”) ist um den Wert 0,052042 niedriger, d.h. 1,613350 - 0,052042 = 1,561308 (Dezimalkommas statt Dezimalpunkte!).

Der R-Quadrat-Wert (R-squared) ist allerdings sehr klein, d.h. dass der Prädiktor “title” (Roman) nur einen Bruchteil der festgestellten Mittelwertvarianz (Veränderungen der Mittelwerte) zu erklären vermag. Möglicherweise gibt es andere Prädiktoren, die die Mittelwertvarianz besser erklären.

m <- lm(syllables ~ title, data = novels_words)
summary(m)
## 
## Call:
## lm(formula = syllables ~ title, data = novels_words)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -0.6133 -0.6133 -0.5613  0.4387  6.3867 
## 
## Coefficients:
##              Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  1.613350   0.003183  506.85   <2e-16 ***
## titletom    -0.052042   0.004577  -11.37   <2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.8479 on 137403 degrees of freedom
##   (1406 observations deleted due to missingness)
## Multiple R-squared:  0.00094,    Adjusted R-squared:  0.0009327 
## F-statistic: 129.3 on 1 and 137403 DF,  p-value: < 2.2e-16
anova(m)
## Analysis of Variance Table
## 
## Response: syllables
##               Df Sum Sq Mean Sq F value    Pr(>F)    
## title          1     93  92.937  129.28 < 2.2e-16 ***
## Residuals 137403  98778   0.719                      
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Graphische Darstellung: der Mittelwertunterschied ist gering (nur 0,05 Silben), aber aufgrund der großen Stichproben statistisch signifikant. Der Faktor “title” erklärt nur einen verschwinded kleinen Bruchteil der Mittelwertunterschiede.

library(effects)
## Loading required package: carData
## lattice theme set by effectsTheme()
## See ?effectsTheme for details.
allEffects(m)
##  model: syllables ~ title
## 
##  title effect
## title
##  prozess      tom 
## 1.613350 1.561307
plot(allEffects(m))

Ergebnisse in Tabellenform:

summary(lm(syllables ~ title, data = novels_words)) %>% 
  broom::tidy() %>% 
    DT::datatable(fillContainer = TRUE, filter = "top",
                options = list(pageLength = 4))

Boxplot mit Jitterplot anhand des vollen Datensatzes: der Mittelwert ist hier der Median median() (d.h. ein Wert, der genau in der Mitte jeder Stichprobe liegt), das arithmetische Mittel / der Durchschnitt wird hier mit einem roten Quadrat symbolisiert. Der Median liegt in beiden Stichproben beim Wert 1, also weit unter dem jeweiligen Durchschnittswert. Dies zeigt, dass die Wortlängen nicht normalverteilt sind. Der Jitterplot veranschaulicht, dass der “Prozess” über mehr Wortformen mit 6, 7 oder 8 Silben verfügt.

novels_words %>% 
  group_by(title) %>% 
  ggplot(aes(title, syllables, fill = title, group = title)) +
  geom_jitter(width = 0.4, alpha = 0.5, color = "gray70") +
  geom_boxplot(notch = FALSE, width = 0.8) +
  stat_summary(fun.y="mean", color = "red", shape = 15)+
  expand_limits(y = -1) +
  scale_y_continuous(breaks = seq(-1, 8, 1)) +
  theme(legend.position = "none") +
  labs(y = "Mittlere Wortlänge (in Silben)", x = "Roman")
## Warning: `fun.y` is deprecated. Use `fun` instead.
## Warning: Removed 1406 rows containing non-finite values (stat_boxplot).
## Warning: Removed 1406 rows containing non-finite values (stat_summary).
## Warning: Removed 1406 rows containing missing values (geom_point).
## Warning: Removed 2 rows containing missing values (geom_segment).

Boxplot anhand der zusammengefassten Daten (Durchschnitt, Standardabweichung):

df = novels_words %>% 
  group_by(title) %>% 
  summarise(Avg_Silben = mean(syllables, na.rm = TRUE),
            Stdev_Silben = sd(syllables, na.rm = TRUE),
            Avg_Buchstaben = mean(letters, na.rm = TRUE),
            Stdev_Buchstaben = sd(letters, na.rm = TRUE))

df %>% ggplot(aes(title, Avg_Silben, fill = title, group = title)) +
  geom_pointrange(aes(ymin = Avg_Silben - Stdev_Silben, ymax = Avg_Silben + Stdev_Silben),
               stat="identity") +
  theme(legend.position = "none") +
  labs(y = "Mittlere Wortlänge (in Silben)")

df %>% ggplot(aes(title, fill = title, group = title)) +
  geom_boxplot(aes(lower = Avg_Silben - Stdev_Silben, upper = Avg_Silben + Stdev_Silben,
                   middle = Avg_Silben,
                   ymin = Avg_Silben - 3*Stdev_Silben, ymax = Avg_Silben + 3*Stdev_Silben),
               stat="identity") +
  theme(legend.position = "none") +
  labs(y = "Mittlere Wortlänge (in Silben)")

8.11 Quanteda-Funktionen

Eine alternative Berechnung der Anzahl der Buchstaben pro Wort mit quanteda (ohne t-Test).

Die Durchschnittswerte, die uns quanteda liefert, sind etwas höher als die tidyverse-Werte. Aber auch hier ist der Mittelwert für den “Prozess” höher als für “Tom Sawyer”.

library(quanteda)
library(quanteda.textstats)
corp = corpus(novels_txt)
stats = textstat_summary(corp)

stats %>% paged_table()
stats %>% 
  group_by(document) %>% 
  transmute(buchstaben = (chars-puncts)/tokens) %>% paged_table()

Die Durchschnittswerte unterscheiden sich in den Berechnungen (tidyverse vs. quanteda), was mit der verschiedenen Art der Tokenisierung und der Aussonderung von nicht relevanten Tokens und leeren Datenzeilen (NA) zu tun hat.

8.12 Konsonantenverbindungen

Welche Konsonantenverbindungen (Buchstabenverbindungen) kommen häufiger vor in den Texten? Wir zerlegen die Texte im Korpus in kleinere Einheiten (mittels tokens()), aber dieses Mal in alphanumerische Zeichen (Buchstaben). Anschließend wenden wir char_ngrams()-Funktion an, mit der man Verknüpfungen von Zeichen feststellen kann.

tok_ch = tokens(corp, what = "character", remove_punct = TRUE, remove_symbols = T, remove_numbers = T, remove_url = T, remove_separators = T)

ngrams_ch = char_ngrams(as.character(tok_ch), n = c(2,3,4), concatenator = "")

Wir wandeln die ngram-Liste in einen Datensatz um (mittels tibble()), was das Zählen mit einer tidyverse-Funktion ermöglicht.

ngrams_char = ngrams_ch %>% 
  as_tibble() %>% 
  rename(cluster = value)

ngrams_char %>% 
  count(cluster, sort = TRUE) %>% 
  head(10) %>% 
    DT::datatable(fillContainer = FALSE, filter = "top",
                options = list(pageLength = 10))

to be continued …

8.13 Datensatz-Variante

novels_words_char <- novels %>%
  unnest_tokens(word, text, token = "words", to_lower = TRUE, drop = T) %>% 
  mutate(Silben = nsyllable::nsyllable(as.character(word), language = "en")) %>% 
  unnest_tokens(character, word, token = "characters", to_lower = TRUE, drop = F) %>% 
  mutate(buchstabe = ifelse(str_detect(character, vokale), "Vokal", "Konsonant"))
  
head(novels_words_char) %>% 
  head(10) %>% 
  rmarkdown::paged_table()