Kapitel 4. Beziehungen zwischen Wörtern: N-Gramme und Korrelationen

Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com

Bislang haben wir Wörter als einzelne Einheiten betrachtet und ihre Beziehungen zu Gefühlen oder Dokumenten untersucht. Viele interessante Textanalysen basieren jedoch auf den Beziehungen zwischen Wörtern, z. B. der Frage, welche Wörter unmittelbar auf andere folgen oder welche Wörter in denselben Dokumenten gemeinsam vorkommen.

In diesem Kapitel lernen wir einige der Methoden kennen, die tidytext für die Berechnung und Visualisierung von Beziehungen zwischen Wörtern in deinem Textdatensatz bietet. Dazu gehört das Argument token = "ngrams", das Tokenisierungen nach Paaren benachbarter Wörter vornimmt, anstatt nach einzelnen Wörtern. Außerdem stellen wir zwei neue Pakete vor: ggraph von Thomas Pedersen, das ggplot2 erweitert, um Netzwerkdiagramme zu erstellen, undwidyr, das paarweise Korrelationen und Abstände in einem Tidy-Datenrahmen berechnet. Zusammen erweitern diese Pakete unseren Werkzeugkasten für die Untersuchung von Text innerhalb des Tidy Data Frameworks.

Tokenisierung durch N-Gramm

Wir haben die Funktion unnest_tokens zur Tokenisierung nach Wörtern oder manchmal auch nach Sätzen verwendet, was für die Arten von Sentiment- und Häufigkeitsanalysen, die wir bisher gemacht haben, nützlich ist. Wir können die Funktion aber auch verwenden, um aufeinanderfolgende Wortfolgen, sogenannten-Gramme, zu tokenisieren. Wenn wir sehen, wie oft auf das Wort X das Wort Y folgt, können wir ein Modell der Beziehungen zwischen den Wörtern erstellen.

Dazu fügen wir die Option token = "ngrams" zu unnest_tokens() hinzu und setzen n auf die Anzahl der Wörter, die wir in jedem n-Gramm erfassen wollen. Wenn wir n auf 2 setzen, untersuchen wir Paare von zwei aufeinanderfolgenden Wörtern, die oft "Bigramme" genannt werden:

library(dplyr)
library(tidytext)
library(janeaustenr)

austen_bigrams <- austen_books() %>%
  unnest_tokens(bigram, text, token = "ngrams", n = 2)

austen_bigrams
## # A tibble: 725,048 × 2
##                   book          bigram
##                 <fctr>           <chr>
## 1  Sense & Sensibility       sense and
## 2  Sense & Sensibility and sensibility
## 3  Sense & Sensibility  sensibility by
## 4  Sense & Sensibility         by jane
## 5  Sense & Sensibility     jane austen
## 6  Sense & Sensibility     austen 1811
## 7  Sense & Sensibility    1811 chapter
## 8  Sense & Sensibility       chapter 1
## 9  Sense & Sensibility           1 the
## 10 Sense & Sensibility      the family
## # ... with 725,038 more rows

Diese Datenstruktur ist immer noch eine Variante des aufgeräumten Textformats. Sie ist als ein Token pro Zeile strukturiert (wobei zusätzliche Metadaten wie book erhalten bleiben), aber jedes Token steht jetzt für ein Bigram.

Hinweis

Beachte, dass sich diese Bigramme überschneiden: "Sinn und" ist ein Token, während "und Sensibilität" ein anderes ist.

Zählen und Filtern von N-Grammen

Unsere üblichen Tidy-Tools eignen sich auch für die Analyse von n-Grammen. Die häufigsten Bigramme können wir mit count() von dplyr untersuchen:

austen_bigrams %>%
  count(bigram, sort = TRUE)
## # A tibble: 211,237 × 2
##      bigram     n
##       <chr> <int>
## 1    of the  3017
## 2     to be  2787
## 3    in the  2368
## 4    it was  1781
## 5      i am  1545
## 6   she had  1472
## 7    of her  1445
## 8    to the  1387
## 9   she was  1377
## 10 had been  1299
## # ... with 211,227 more rows

Wie zu erwarten, sind viele der häufigsten Bigramme Paare aus gewöhnlichen (uninteressanten) Wörtern, wie z.B. "of the" und "to be", was wir "Stoppwörter" nennen (siehe Kapitel 1). Dies ist ein guter Zeitpunkt, um tidyrsseparate() zu verwenden, das eine Spalte anhand eines Trennzeichens in mehrere Spalten aufteilt. So können wir die Spalte in zwei Spalten aufteilen, "Wort1" und "Wort2", und dann die Fälle entfernen, in denen eines der beiden ein Stoppwort ist.

library(tidyr)

bigrams_separated <- austen_bigrams %>%
  separate(bigram, c("word1", "word2"), sep = " ")

bigrams_filtered <- bigrams_separated %>%
  filter(!word1 %in% stop_words$word) %>%
  filter(!word2 %in% stop_words$word)

# new bigram counts:
bigram_counts <- bigrams_filtered %>%
  count(word1, word2, sort = TRUE)

bigram_counts
## Source: local data frame [33,421 x 3]
## Groups: word1 [6,711]
##
##      word1     word2     n
##      <chr>     <chr> <int>
## 1      sir    thomas   287
## 2     miss  crawford   215
## 3  captain wentworth   170
## 4     miss woodhouse   162
## 5    frank churchill   132
## 6     lady   russell   118
## 7     lady   bertram   114
## 8      sir    walter   113
## 9     miss   fairfax   109
## 10 colonel   brandon   108
## # ... with 33,411 more rows

Wir können sehen, dass Namen (egal ob Vor- und Nachname oder mit Anrede) die häufigsten Paare in Jane Austen-Büchern sind.

Bei anderen Analysen möchten wir vielleicht mit den rekombinierten Wörtern arbeiten. Die Funktion unite() von tidyr ist die Umkehrung von separate() und ermöglicht es uns, die Spalten wieder zu einer zusammenzufassen. So können wir mit "separate/filter/count/unite" die häufigsten Bigramme finden, die keine Stoppwörter enthalten.

bigrams_united <- bigrams_filtered %>%
  unite(bigram, word1, word2, sep = " ")

bigrams_united
## # A tibble: 44,784 × 2
##                   book                   bigram
## *               <fctr>                    <chr>
## 1  Sense & Sensibility              jane austen
## 2  Sense & Sensibility              austen 1811
## 3  Sense & Sensibility             1811 chapter
## 4  Sense & Sensibility                chapter 1
## 5  Sense & Sensibility             norland park
## 6  Sense & Sensibility surrounding acquaintance
## 7  Sense & Sensibility               late owner
## 8  Sense & Sensibility             advanced age
## 9  Sense & Sensibility       constant companion
## 10 Sense & Sensibility             happened ten
## # ... with 44,774 more rows

Bei anderen Analysen bist du vielleicht an den häufigsten Trigrammen interessiert, also an aufeinanderfolgenden Sequenzen von drei Wörtern. Wir können diese finden, indem wirn = 3 einstellen.

austen_books() %>%
  unnest_tokens(trigram, text, token = "ngrams", n = 3) %>%
  separate(trigram, c("word1", "word2", "word3"), sep = " ") %>%
  filter(!word1 %in% stop_words$word,
         !word2 %in% stop_words$word,
         !word3 %in% stop_words$word) %>%
  count(word1, word2, word3, sort = TRUE)
## Source: local data frame [8,757 x 4]
## Groups: word1, word2 [7,462]
##
##        word1     word2     word3     n
##        <chr>     <chr>     <chr> <int>
## 1       dear      miss woodhouse    23
## 2       miss        de    bourgh    18
## 3       lady catherine        de    14
## 4  catherine        de    bourgh    13
## 5       poor      miss    taylor    11
## 6        sir    walter    elliot    11
## 7        ten  thousand    pounds    11
## 8       dear       sir    thomas    10
## 9     twenty  thousand    pounds     8
## 10   replied      miss  crawford     7
## # ... with 8,747 more rows

Bigramme analysieren

Dieses Ein-Bigramm-pro-Zeile-Format ist hilfreich für explorative Analysen des Textes. Ein einfaches Beispiel: Wir könnten uns für die häufigsten "Straßen" interessieren, die in jedem Buch erwähnt werden.

bigrams_filtered %>%
  filter(word2 == "street") %>%
  count(book, word1, sort = TRUE)
## Source: local data frame [34 x 3]
## Groups: book [6]
##
##                   book       word1     n
##                 <fctr>       <chr> <int>
## 1  Sense & Sensibility    berkeley    16
## 2  Sense & Sensibility      harley    16
## 3     Northanger Abbey    pulteney    14
## 4     Northanger Abbey      milsom    11
## 5       Mansfield Park     wimpole    10
## 6    Pride & Prejudice gracechurch     9
## 7  Sense & Sensibility     conduit     6
## 8  Sense & Sensibility        bond     5
## 9           Persuasion      milsom     5
## 10          Persuasion      rivers     4
## # ... with 24 more rows

Ein Bigramm kann auch als Begriff in einem Dokument behandelt werden, so wie wir einzelne Wörter behandelt haben. Wir können uns zum Beispiel die tf-idf-Werte(Kapitel 3) von Bigrammen in den Austen-Romanen ansehen. Diese tf-idf-Werte können innerhalb jedes Buchs visualisiert werden, genau wie bei den Wörtern(Abbildung 4-1).

bigram_tf_idf <- bigrams_united %>%
  count(book, bigram) %>%
  bind_tf_idf(bigram, book, n) %>%
  arrange(desc(tf_idf))

bigram_tf_idf
## Source: local data frame [36,217 x 6]
## Groups: book [6]
##
##                   book            bigram     n         tf      idf     tf_idf
##                 <fctr>             <chr> <int>      <dbl>    <dbl>      <dbl>
## 1           Persuasion captain wentworth   170 0.02985599 1.791759 0.05349475
## 2       Mansfield Park        sir thomas   287 0.02873160 1.791759 0.05148012
## 3       Mansfield Park     miss crawford   215 0.02152368 1.791759 0.03856525
## 4           Persuasion      lady russell   118 0.02072357 1.791759 0.03713165
## 5           Persuasion        sir walter   113 0.01984545 1.791759 0.03555828
## 6                 Emma    miss woodhouse   162 0.01700966 1.791759 0.03047722
## 7     Northanger Abbey       miss tilney    82 0.01594400 1.791759 0.02856782
## 8  Sense & Sensibility   colonel brandon   108 0.01502086 1.791759 0.02691377
## 9                 Emma   frank churchill   132 0.01385972 1.791759 0.02483329
## 10   Pride & Prejudice    lady catherine   100 0.01380453 1.791759 0.02473439
## # ... with 36,207 more rows
tmwr 0401
Abbildung 4-1. Die 12 Bigramme mit dem höchsten tf-idf aus jedem Jane Austen-Roman

Wie wir bereits in Kapitel 3 festgestellt haben, sind die Einheiten, die jedes Austen-Buch auszeichnen, fast ausschließlich Namen. Es gibt auch einige Paare aus einem gemeinsamen Verb und einem Namen, wie z. B. "antwortete Elizabeth" in Stolz und Vorurteil oder "rief Emma" in Emma.

Es hat Vor- und Nachteile, das tf-idf von Bigrammen statt einzelner Wörter zu untersuchen. Paare von aufeinanderfolgenden Wörtern können eine Struktur erfassen, die nicht vorhanden ist, wenn man nur einzelne Wörter zählt, und sie können einen Kontext liefern, der die Token verständlicher macht (zum Beispiel ist "pulteney street" in Northanger Abbey informativer als "pulteney"). Allerdings ist die Anzahl der Bigramme auchgeringer: Ein typisches Zwei-Wort-Paar ist seltener als jedes der einzelnen Wörter. Daher können Bigramme besonders nützlich sein, wenn du einen sehr großen Textdatensatz hast.

Bigramme zur Bereitstellung von Kontext in der Sentiment-Analyse verwenden

Bei unserer Stimmungsanalyse in Kapitel 2 wurde einfach das Auftreten von positiven oder negativen Wörtern anhand eines Referenzlexikons gezählt. Eines der Probleme bei diesem Ansatz ist, dass der Kontext eines Wortes fast genauso wichtig sein kann wie sein Vorkommen. Zum Beispiel werden die Wörter "glücklich" und "mögen" als positiv gewertet, selbst in einem Satz wie "Ich bin nicht glücklich und mag es nicht!"

Jetzt, wo wir die Daten in Bigramme eingeteilt haben, ist es einfach zu erkennen, wie oft Wörtern ein Wort wie "nicht" vorausgeht.

bigrams_separated %>%
  filter(word1 == "not") %>%
  count(word1, word2, sort = TRUE)
## Source: local data frame [1,246 x 3]
## Groups: word1 [1]
##
##    word1 word2     n
##    <chr> <chr> <int>
## 1    not    be   610
## 2    not    to   355
## 3    not  have   327
## 4    not  know   252
## 5    not     a   189
## 6    not think   176
## 7    not  been   160
## 8    not   the   147
## 9    not    at   129
## 10   not    in   118
## # ... with 1,236 more rows

Wenn wir die Bigram-Daten einer Sentiment-Analyse unterziehen, können wir untersuchen, wie oft den sentimentalen Wörtern ein "nicht" oder andere verneinende Wörter vorangestellt sind. Auf diese Weise können wir ihren Beitrag zur Stimmungsbewertung ignorieren oder sogar umkehren.

Für die Stimmungsanalyse verwenden wir das AFINN-Lexikon, das, wie du dich vielleicht erinnerst, für jedes Wort einen numerischen Stimmungswert angibt, wobei positive oder negative Zahlen die Richtung der Stimmung anzeigen.

AFINN <- get_sentiments("afinn")

AFINN
## # A tibble: 2,476 × 2
##          word score
##         <chr> <int>
## 1     abandon    -2
## 2   abandoned    -2
## 3    abandons    -2
## 4    abducted    -2
## 5   abduction    -2
## 6  abductions    -2
## 7       abhor    -3
## 8    abhorred    -3
## 9   abhorrent    -3
## 10     abhors    -3
## # ... with 2,466 more rows

Dann können wir die häufigsten Wörter untersuchen, denen ein "nicht" vorausging und die mit einer Stimmung verbunden waren.

not_words <- bigrams_separated %>%
  filter(word1 == "not") %>%
  inner_join(AFINN, by = c(word2 = "word")) %>%
  count(word2, score, sort = TRUE) %>%
  ungroup()

not_words
## # A tibble: 245 × 3
##      word2 score     n
##      <chr> <int> <int>
## 1     like     2    99
## 2     help     2    82
## 3     want     1    45
## 4     wish     1    39
## 5    allow     1    36
## 6     care     2    23
## 7    sorry    -1    21
## 8    leave    -1    18
## 9  pretend    -1    18
## 10   worth     2    17
## # ... with 235 more rows

Das häufigste sentimentale Wort, das auf "nicht" folgte, war zum Beispiel "wie", das normalerweise einen (positiven) Wert von 2 hätte.

Es lohnt sich zu fragen, welche Wörter den größten Beitrag in die "falsche" Richtung geleistet haben. Um das zu berechnen, können wir ihre Punktzahl mit der Anzahl ihrer Auftritte multiplizieren (so dass ein Wort mit einer Punktzahl von +3, das 10 Mal vorkommt, genauso viel Einfluss hat wie ein Wort mit einer Stimmungszahl von +1, das 30 Mal vorkommt). Wir visualisieren das Ergebnis mit einem Balkendiagramm(Abbildung 4-2).

not_words %>%
  mutate(contribution = n * score) %>%
  arrange(desc(abs(contribution))) %>%
  head(20) %>%
  mutate(word2 = reorder(word2, contribution)) %>%
  ggplot(aes(word2, n * score, fill = n * score > 0)) +
  geom_col(show.legend = FALSE) +
  xlab("Words preceded by \"not\"") +
  ylab("Sentiment score * number of occurrences") +
  coord_flip()
tmwr 0402
Abbildung 4-2. Die 20 Wörter, auf die "nicht" folgt, hatten den größten Einfluss auf die Stimmungsbewertung, entweder in positiver oder negativer Richtung

Die Bigramme "nicht mögen" und "nicht helfen" waren die überwältigendsten Ursachen für Fehlidentifikationen, die den Text viel positiver erscheinen lassen, als er ist. Wir sehen aber auch, dass Formulierungen wie "keine Angst" und "nicht fehlschlagen" den Text manchmal negativer erscheinen lassen, als er ist.

"Nicht" ist nicht der einzige Begriff, der einen Kontext für das folgende Wort liefert. Wir könnten vier (oder mehr) gängige Wörter auswählen, die das nachfolgende Wort negieren, und sie alle auf einmal mit demselben Ansatz der Verknüpfung und Zählung untersuchen.

negation_words <- c("not", "no", "never", "without")

negated_words <- bigrams_separated %>%
  filter(word1 %in% negation_words) %>%
  inner_join(AFINN, by = c(word2 = "word")) %>%
  count(word1, word2, score, sort = TRUE) %>%
  ungroup()

Dann können wir uns ansehen, welche Wörter am häufigsten auf eine bestimmte Verneinung folgen(Abbildung 4-3). Während "nicht mögen" und "nicht helfen" immer noch die beiden häufigsten Beispiele sind, können wir auch Paarungen wie "nicht toll" und "nie geliebt" sehen. Wir könnten dies mit den Ansätzen in Kapitel 2 kombinieren, um die AFINN-Werte jedes Wortes, das auf eine Negation folgt, umzukehren. Dies sind nur einige Beispiele dafür, wie die Suche nach aufeinanderfolgenden Wörtern den Kontext für Textmining-Methoden liefern kann.

tmwr 0403
Abbildung 4-3. Die häufigsten positiven oder negativen Wörter, die auf Verneinungen wie "nie", "nein", "nicht" und "ohne" folgen

Visualisierung eines Netzwerks von Bigrammen mit ggraph

Wir möchten vielleicht alle Beziehungen zwischen den Wörtern gleichzeitig visualisieren und nicht nur die wichtigsten auf einmal. Eine gängige Visualisierung ist die Anordnung der Wörter in einem Netzwerk oder "Graphen", wobei wir uns hier auf einen Graphen nicht im Sinne einer Visualisierung beziehen, sondern als eine Kombination aus verbundenen Knoten. Ein Graph kann aus einem aufgeräumten Objekt konstruiert werden, da er drei Variablen hat:

von

Der Knoten, von dem eine Kante kommt

zu

Der Knoten, zu dem eine Kante führt

Gewicht

Ein numerischer Wert, der mit jeder Kante verbunden ist

Das igraph-Paket hat viele leistungsstarke Funktionen zur Bearbeitung und Analyse von Netzwerken. Eine Möglichkeit, ein igraph-Objekt aus aufgeräumten Daten zu erstellen, ist die Funktion graph_from_data_frame(), die einen Datenrahmen mit Kanten und Spalten für "von", "bis" und Kantenattribute (in diesem Fall n) annimmt:

library(igraph)

# original counts
bigram_counts
## Source: local data frame [33,421 x 3]
## Groups: word1 [6,711]
##
##      word1     word2     n
##      <chr>     <chr> <int>
## 1      sir    thomas   287
## 2     miss  crawford   215
## 3  captain wentworth   170
## 4     miss woodhouse   162
## 5    frank churchill   132
## 6     lady   russell   118
## 7     lady   bertram   114
## 8      sir    walter   113
## 9     miss   fairfax   109
## 10 colonel   brandon   108
## # ... with 33,411 more rows
# filter for only relatively common combinations
bigram_graph <- bigram_counts %>%
  filter(n > 20) %>%
  graph_from_data_frame()

bigram_graph
## IGRAPH DN-- 91 77 --
## + attr: name (v/c), n (e/n)
## + edges (vertex names):
##  [1] sir     ->thomas     miss    ->crawford   captain ->wentworth
##  [4] miss    ->woodhouse  frank   ->churchill  lady    ->russell
##  [7] lady    ->bertram    sir     ->walter     miss    ->fairfax
## [10] colonel ->brandon    miss    ->bates      lady    ->catherine
## [13] sir     ->john       jane    ->fairfax    miss    ->tilney
## [16] lady    ->middleton  miss    ->bingley    thousand->pounds
## [19] miss    ->dashwood   miss    ->bennet     john    ->knightley
## [22] miss    ->morland    captain ->benwick    dear    ->mis
## + ... omitted several edges

igraph hat zwar Plot-Funktionen eingebaut, aber die sind nicht das, wofür das Paket gedacht ist. Deshalb haben viele andere Pakete Visualisierungsmethoden für Grafikobjekte entwickelt. Wir empfehlen das Paket ggraph (Pedersen 2017), weil es diese Visualisierungen in Form der Grammatik von Grafiken implementiert, die wir bereits aus ggplot2 kennen.

Wir können ein igraph-Objekt mit der Funktion ggraphin einen ggraph umwandeln und ihm dann Ebenen hinzufügen, ähnlich wie in ggplot2. Für einen einfachen Graphen müssen wir zum Beispiel drei Ebenen hinzufügen: Knoten, Kanten und Text(Abbildung 4-4).

library(ggraph)
set.seed(2017)

ggraph(bigram_graph, layout = "fr") +
  geom_edge_link() +
  geom_node_point() +
  geom_node_text(aes(label = name), vjust = 1, hjust = 1)
tmwr 0404
Abbildung 4-4. Häufige Bigramme in Stolz und Vorurteil, die mehr als 20 Mal vorkommen und bei denen kein Wort ein Stoppwort ist

In Abbildung 4-4, können wir einige Details der Textstruktur visualisieren. Wir sehen zum Beispiel, dass Anreden wie "Miss", "Lady", "Sir" und "Colonel" gemeinsame Knotenpunkte bilden, auf die oft Namen folgen. Wir sehen auch Paare oder Triolen entlang der Außenseite, die gemeinsame kurze Phrasen bilden ("halbe Stunde", "tausend Pfund" oder "kurze Zeit/Pause").

Zum Schluss führen wir noch ein paar Polierarbeiten durch, um das Diagramm besser aussehen zu lassen(Abbildung 4-5):

  • Wir fügen die edge_alpha Ästhetik zur Link-Ebene hinzu, um Links transparent zu machen, je nachdem wie häufig oder selten das Bigram ist.

  • Wir fügen die Richtungsabhängigkeit mit einem Pfeil hinzu, der mitgrid::arrow() konstruiert wird und eine end_cap Option enthält, die dem Pfeil sagt, dass er enden soll, bevor er den Knoten berührt.

  • Wir basteln an den Optionen für die Knotenebene, um die Knoten attraktiver zu machen.

  • Wir fügen ein Thema hinzu, das für das Plotten von Netzwerken nützlich ist, theme_void().

set.seed(2016)

a <- grid::arrow(type = "closed", length = unit(.15, "inches"))

ggraph(bigram_graph, layout = "fr") +
  geom_edge_link(aes(edge_alpha = n), show.legend = FALSE,
                 arrow = a, end_cap = circle(.07, 'inches')) +
  geom_node_point(color = "lightblue", size = 5) +
  geom_node_text(aes(label = name), vjust = 1, hjust = 1) +
  theme_void()
tmwr 0405
Abbildung 4-5. Häufige Bigramme in Stolz und Vorurteil, mit etwas Politur

Du musst vielleicht ein bisschen mit ggraph experimentieren, um deine Netzwerke in ein vorzeigbares Format wie dieses zu bringen, aber die Netzwerkstruktur ist eine nützliche und flexible Methode, um relationale Daten zu visualisieren.

Hinweis

Beachte, dass dies eine Visualisierung einer Markov-Kette ist, ein gängiges Modell in der Textverarbeitung. In einer Markov-Kette hängt jede Wortwahl nur von dem vorhergehenden Wort ab. In diesem Fall würde ein Zufallsgenerator, der diesem Modell folgt, "dear", dann "sir", dann "william/walter/thomas/thomas's" ausspucken, indem er jedes Wort mit den häufigsten Wörtern, die ihm folgen, verknüpft. Um die Visualisierung interpretierbar zu machen, haben wir uns dafür entschieden, nur die häufigsten Wortverbindungen zu zeigen, aber man könnte sich ein riesiges Diagramm vorstellen, das alle im Text vorkommenden Verbindungen darstellt.

Bigramme in anderen Texten visualisieren

Wir haben uns viel Mühe gegeben, um Bigramme in einem Textdatensatz zu bereinigen und zu visualisieren, also fassen wir sie in einer Funktion zusammen, damit wir sie leicht auf andere Textdatensätze anwenden können.

Hinweis

Damit es einfach ist, die Funktionen count_bigrams() und visualize_bigrams() selbst zu nutzen, haben wir auch die dafür notwendigen Pakete neu geladen.

library(dplyr)
library(tidyr)
library(tidytext)
library(ggplot2)
library(igraph)
library(ggraph)

count_bigrams <- function(dataset) {
  dataset %>%
    unnest_tokens(bigram, text, token = "ngrams", n = 2) %>%
    separate(bigram, c("word1", "word2"), sep = " ") %>%
    filter(!word1 %in% stop_words$word,
           !word2 %in% stop_words$word) %>%
    count(word1, word2, sort = TRUE)
}

visualize_bigrams <- function(bigrams) {
  set.seed(2016)
  a <- grid::arrow(type = "closed", length = unit(.15, "inches"))

  bigrams %>%
    graph_from_data_frame() %>%
    ggraph(layout = "fr") +
    geom_edge_link(aes(edge_alpha = n), show.legend = FALSE, arrow = a) +
    geom_node_point(color = "lightblue", size = 5) +
    geom_node_text(aes(label = name), vjust = 1, hjust = 1) +
    theme_void()
}

An dieser Stelle könnten wir Bigramme in anderen Werken visualisieren, wie zum Beispiel in der King James Bible(Abbildung 4-6):

# the King James version is book 10 on Project Gutenberg:
library(gutenbergr)
kjv <- gutenberg_download(10)
library(stringr)

kjv_bigrams <- kjv %>%
  count_bigrams()

# filter out rare combinations, as well as digits
kjv_bigrams %>%
  filter(n > 40,
         !str_detect(word1, "\\d"),
         !str_detect(word2, "\\d")) %>%
  visualize_bigrams()
tmwr 0406
Abbildung 4-6. Gerichtetes Diagramm der häufigen Bigramme in der King James Bible, das diejenigen zeigt, die mehr als 40 Mal vorkommen

Abbildung 4-6 zeigt also einen gemeinsamen "Bauplan" für die Sprache in der Bibel, insbesondere für "dein" und "du" (die man wahrscheinlich als Stoppwörter bezeichnen könnte!). Du kannst das Gutenbergr-Paket und die Funktionen count_bigrams/visualize_bigrams verwenden, um Bigramme in anderen klassischen Büchern, die dich interessieren, zu visualisieren.

Zählen und Zuordnen von Wortpaaren mit dem Widyr-Paket

Die Tokenisierung nach n-Gram ist eine nützliche Methode, um Paare benachbarter Wörter zu untersuchen. Wir können uns aber auch für Wörter interessieren, die in bestimmten Dokumenten oder Kapiteln gemeinsam vorkommen, auch wenn sie nicht nebeneinander stehen.

Aufgeräumte Daten sind eine nützliche Struktur, um zwischen Variablen zu vergleichen oder nach Zeilen zu gruppieren, aber es kann schwierig sein, zwischen Zeilen zu vergleichen: zum Beispiel, um zu zählen, wie oft zwei Wörter im selben Dokument vorkommen, oder um zu sehen, wie sie korreliert sind. Für die meisten Operationen zur Ermittlung von paarweisen Zählungen oder Korrelationen müssen die Daten zunächst in eine breite Matrix umgewandelt werden.

In Kapitel 5 werden wir einige der Möglichkeiten untersuchen, wie aufgeräumter Text in eine breite Matrix umgewandelt werden kann, aber in diesem Fall ist das nicht notwendig. Das Paketwidyr erleichtert Operationen wie das Berechnen von Zählungen und Korrelationen, indem es das Muster "Daten erweitern, eine Operation durchführen und dann die Daten neu ordnen" vereinfacht(Abbildung 4-7). Wir werden uns auf eine Reihe von Funktionen konzentrieren, die paarweise Vergleiche zwischen Gruppen von Beobachtungen durchführen (z. B. zwischen Dokumenten oder Textabschnitten).

tmwr 0407
Abbildung 4-7. Die Philosophie hinter dem Paket widyr, das Operationen wie das Zählen und Korrelieren von Wertepaaren in einem aufgeräumten Datensatz durchführen kann. Das widyr-Paket "gießt" einen aufgeräumten Datensatz zunächst in eine breite Matrix, führt eine Operation wie eine Korrelation durch und ordnet das Ergebnis dann wieder ein.

Zählen und Korrelieren zwischen Abschnitten

Betrachte das Buch Stolz und Vorurteil, das in 10-zeilige Abschnitte unterteilt ist, wie wir es (mit größeren Abschnitten) für die Stimmungsanalyse in Kapitel 2 getan haben. Es könnte uns interessieren, welche Wörter innerhalb desselben Abschnitts häufig vorkommen.

austen_section_words <- austen_books() %>%
  filter(book == "Pride & Prejudice") %>%
  mutate(section = row_number() %/% 10) %>%
  filter(section > 0) %>%
  unnest_tokens(word, text) %>%
  filter(!word %in% stop_words$word)

austen_section_words
## # A tibble: 37,240 × 3
##                 book section         word
##               <fctr>   <dbl>        <chr>
## 1  Pride & Prejudice       1        truth
## 2  Pride & Prejudice       1  universally
## 3  Pride & Prejudice       1 acknowledged
## 4  Pride & Prejudice       1       single
## 5  Pride & Prejudice       1   possession
## 6  Pride & Prejudice       1      fortune
## 7  Pride & Prejudice       1         wife
## 8  Pride & Prejudice       1     feelings
## 9  Pride & Prejudice       1        views
## 10 Pride & Prejudice       1     entering
## # ... with 37,230 more rows

Eine nützliche Funktion von widyr ist die Funktion pairwise_count(). Das Präfix pairwise_ bedeutet, dass sie eine Zeile für jedes Wortpaar in der Variablen word ergibt. So können wir gemeinsame Wortpaare zählen, die im selben Abschnitt vorkommen.

library(widyr)

# count words co-occuring within sections
word_pairs <- austen_section_words %>%
  pairwise_count(word, section, sort = TRUE)

word_pairs
## # A tibble: 796,008 × 3
##        item1     item2     n
##        <chr>     <chr> <dbl>
## 1      darcy elizabeth   144
## 2  elizabeth     darcy   144
## 3       miss elizabeth   110
## 4  elizabeth      miss   110
## 5  elizabeth      jane   106
## 6       jane elizabeth   106
## 7       miss     darcy    92
## 8      darcy      miss    92
## 9  elizabeth   bingley    91
## 10   bingley elizabeth    91
## # ... with 795,998 more rows

Beachte, dass die Eingabe eine Zeile für jedes Paar aus einem Dokument (einem 10-zeiligen Abschnitt) und einem Wort hatte, während die Ausgabe eine Zeile für jedes Wortpaar hat. Auch das ist ein ordentliches Format, aber mit einer ganz anderen Struktur, die wir nutzen können, um neue Fragen zu beantworten.

Wir können zum Beispiel sehen, dass das häufigste Wortpaar in einem Abschnitt "Elizabeth" und "Darcy" ist (die beiden Hauptfiguren). Wir können leicht die Wörter finden, die am häufigsten mit Darcy vorkommen.

word_pairs %>%
  filter(item1 == "darcy")
## # A tibble: 2,930 × 3
##    item1     item2     n
##    <chr>     <chr> <dbl>
## 1  darcy elizabeth   144
## 2  darcy      miss    92
## 3  darcy   bingley    86
## 4  darcy      jane    46
## 5  darcy    bennet    45
## 6  darcy    sister    45
## 7  darcy      time    41
## 8  darcy      lady    38
## 9  darcy    friend    37
## 10 darcy   wickham    37
## # ... with 2,920 more rows

Prüfung der paarweisen Korrelation

Paare wie "Elizabeth" und "Darcy" sind die am häufigsten gemeinsam auftretenden Wörter, aber das ist nicht besonders aussagekräftig, da sie auch die häufigsten Einzelwörter sind. Stattdessen sollten wir dieKorrelation zwischen den Wörtern untersuchen, die angibt, wie oft sie zusammen und wie oft sie einzeln vorkommen.

Hier konzentrieren wir uns auf denPhi-Koeffizienten, ein gängiges Maß für die binäre Korrelation. Der phi-Koeffizient gibt an, wie viel wahrscheinlicher es ist, dass entweder beide Wörter X und Y vorkommen oderkeines von beiden, als dass eines ohne das andere auftritt.

Siehe Tabelle 4-1.

Tabelle 4-1. Werte für die Berechnung des phi-Koeffizienten
Hat Wort Y Kein Wort Y Gesamt

Hat Wort X

n11

n10

n1-

Kein Wort X

n01

n00

n0-

Gesamt

n - 1

n - 0

n

Zum Beispiel steht n11 für die Anzahl der Dokumente, in denen sowohl das Wort X als auch das Wort Y vorkommen, n00 für die Anzahl der Dokumente, in denen keines der beiden Wörter vorkommt, und n10 und n01 für die Fälle, in denen das eine ohne das andere vorkommt. In Bezug auf diese Tabelle ist der phi-Koeffizient:

Hinweis

Der phi-Koeffizient entspricht der Pearson-Korrelation, von der du vielleicht schon gehört hast, wenn er auf binäre Daten angewendet wird.

Mit der Funktion pairwise_cor() in widyr können wir den Phi-Koeffizienten zwischen Wörtern finden, der darauf basiert, wie oft sie im selben Abschnitt vorkommen. Die Syntax ist ähnlich wie bei pairwise_count().

# we need to filter for at least relatively common words first
word_cors <- austen_section_words %>%
  group_by(word) %>%
  filter(n() >= 20) %>%
  pairwise_cor(word, section, sort = TRUE)

word_cors
## # A tibble: 154,842 × 3
##        item1     item2 correlation
##        <chr>     <chr>       <dbl>
## 1     bourgh        de   0.9508501
## 2         de    bourgh   0.9508501
## 3     pounds  thousand   0.7005808
## 4   thousand    pounds   0.7005808
## 5    william       sir   0.6644719
## 6        sir   william   0.6644719
## 7  catherine      lady   0.6633048
## 8       lady catherine   0.6633048
## 9    forster   colonel   0.6220950
## 10   colonel   forster   0.6220950
## # ... with 154,832 more rows

Dieses Ausgabeformat ist hilfreich für die Erkundung. Wir könnten zum Beispiel die Wörter finden, die am stärksten mit einem Wort wie "Pfund" korrelieren, indem wir einefilter Operation durchführen.

word_cors %>%
  filter(item1 == "pounds")
## # A tibble: 393 × 3
##     item1     item2 correlation
##     <chr>     <chr>       <dbl>
## 1  pounds  thousand  0.70058081
## 2  pounds       ten  0.23057580
## 3  pounds   fortune  0.16386264
## 4  pounds   settled  0.14946049
## 5  pounds wickham's  0.14152401
## 6  pounds  children  0.12900011
## 7  pounds  mother's  0.11905928
## 8  pounds  believed  0.09321518
## 9  pounds    estate  0.08896876
## 10 pounds     ready  0.08597038
## # ... with 383 more rows

So können wir bestimmte interessante Wörter auswählen und die anderen Wörter finden, die am meisten mit ihnen verbunden sind(Abbildung 4-8).

word_cors %>%
  filter(item1 %in% c("elizabeth", "pounds", "married", "pride")) %>%
  group_by(item1) %>%
  top_n(6) %>%
  ungroup() %>%
  mutate(item2 = reorder(item2, correlation)) %>%
  ggplot(aes(item2, correlation)) +
  geom_bar(stat = "identity") +
  facet_wrap(~ item1, scales = "free") +
  coord_flip()
tmwr 0408
Abbildung 4-8. Wörter aus Stolz und Vorurteil, die am stärksten mit "Elizabeth", "Pfund", "verheiratet" und "Stolz" korreliert sind

So wie wir ggraph benutzt haben, um Bigramme zu visualisieren, können wir es auch benutzen, um die Korrelationen und Cluster von Wörtern zu visualisieren, die vom Paket widyr gefunden wurden(Abbildung 4-9).

set.seed(2016)

word_cors %>%
  filter(correlation > .15) %>%
  graph_from_data_frame() %>%
  ggraph(layout = "fr") +
  geom_edge_link(aes(edge_alpha = correlation), show.legend = FALSE) +
  geom_node_point(color = "lightblue", size = 5) +
  geom_node_text(aes(label = name), repel = TRUE) +
  theme_void()
tmwr 0409
Abbildung 4-9. Wortpaare in "Stolz und Vorurteil", die mit einer Wahrscheinlichkeit von mindestens 0,15 innerhalb desselben 10-Zeilen-Abschnitts vorkommen

Anders als bei der Bigram-Analyse sind die Beziehungen hier symmetrisch und nicht gerichtet (es gibt keine Pfeile). Wir können auch sehen, dass es zwar häufig Namens- und Titelpaare gibt, die die Bigram-Paare dominieren, wie z. B. "Colonel/Fitzwilliam", aber auch Wortpaare, die nahe beieinander liegen, wie z. B. "Spaziergang" und "Park" oder "Tanz" und "Ball". "

Zusammenfassung

In diesem Kapitel wurde gezeigt, dass der Ansatz des aufgeräumten Textes nicht nur für die Analyse einzelner Wörter, sondern auch für die Untersuchung der Beziehungen und Verbindungen zwischen Wörtern nützlich ist. Bei diesen Beziehungen kann es sich um n-Gramme handeln, mit denen wir sehen können, welche Wörter tendenziell nach anderen erscheinen, oder um Kookkurrenzen und Korrelationen für Wörter, die in unmittelbarer Nähe zueinander erscheinen. In diesem Kapitel wurde auch das Paket ggraph vorgestellt, mit dem diese beiden Arten von Beziehungen als Netzwerke visualisiert werden können. Diese Netzwerkvisualisierungen sind ein flexibles Werkzeug, um Beziehungen zu erforschen, und werden in den Fallstudien in späteren Kapiteln eine wichtige Rolle spielen.

Get Text Mining mit R now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.