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
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
()
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.
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 ggraph
in 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
)
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 mit
grid::arrow()
konstruiert wird und eineend_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
()
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
()
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).
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.
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
()
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
()
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.