18 Textdeskription und einfache Textvergleiche

Nachdem wir im vergangenen Kapitel die Grundlagen und -begriffe der automatisierten Inhaltsanalyse kennengelernt haben, setzen wir uns nun etwas intensiver mit der deskriptiven Analyse von Texten auseinander und werden auch einige einfache Möglichkeiten betrachten, Texte bzw. Dokumente miteinander zu vergleichen.

Wir arbeiten erneut mit den Tweets von Trump und Biden und führen daher zunächst die uns bereits bekannten Schritte zur Aufbereitung des Tweet-Korpus durch:

# Setup
library(tidyverse)
library(tidytext)
library(quanteda)

# Daten einlesen
tweets <- read_csv("data/trump_biden_tweets_2020.csv")

# Korpus erzeugen
tweets_corpus <- corpus(tweets, docid_field = "id", text_field = "content")

# Tokens erzeugen
tweets_tokens <- tokens(tweets_corpus, 
                       remove_punct = TRUE,   
                       remove_numbers = TRUE,
                       remove_symbols = TRUE, 
                       remove_url = TRUE) %>% 
  tokens_tolower() %>% 
  tokens_remove(stopwords("english"))

# DFM erzeugen
tweets_dfm <- dfm(tweets_tokens)

18.1 Worthäufigkeiten

Wir haben bereits im vergangenen Kapitel gesehen, dass wir anhand der DFM schon simple Worthäufigkeiten auszählen können. Allgemein erhalten wir in Quanteda die absoluten Feature-Häufigkeiten mit featfreq().

An dieser Stelle sei außerdem auf die Funktion tidy() aus dem Tidytext-Package verwiesen, die Output vieler Quanteda-Funktionen automatisch in tidy data konvertieren kann und uns somit den weiteren Umgang mit den Daten erleichert:

featfreq(tweets_dfm) %>% 
  tidy() %>%       # In tidy data konvertieren 
  arrange(desc(x)) # Absteigend nach Anzahl sortieren
## # A tibble: 8,534 x 2
##    names         x
##    <chr>     <dbl>
##  1 great       619
##  2 trump       512
##  3 president   449
##  4 people      423
##  5 thank       380
##  6 donald      352
##  7 need        338
##  8 just        325
##  9 now         308
## 10 country     296
## # ... with 8,524 more rows

Quanteda bietet außerdem einige rudimentäre Möglichkeiten, Textdaten zu visualisieren. Erinnert sich noch jemand an den Trend, alles in Wortwolken zu visualisieren? Mit textplot_wordcloud() erzeugen wir eine solche:

textplot_wordcloud(tweets_dfm, max_words = 100)

An dieser Stelle ist es sinnvoll, eine zweite DFM zu erstellen, die nicht auf den einzelnen Dokumenten (= einzelne Tweets), sondern auf den beiden Accounts basiert. Dies führt dazu, dass alle Tweets eines Accounts als ein langes Dokument betrachtet werden, ermöglicht uns aber bereits einfache Vergleiche zwischen den beiden Accounts. Dies erreichen wir im dfm()-Befehl mit dem Argument groups, wobei wir basierend auf unseren Docvars gruppieren können:

tweets_dfm_grouped <- dfm(tweets_tokens, groups = "account")
tweets_dfm_grouped
## Document-feature matrix of: 2 documents, 8,534 features (36.8% sparse) and 1 docvar.
##                  features
## docs              final fundraising deadline just hours away need help every donation
##   JoeBiden            9           4        4  132     5   46  282  120   151        8
##   realDonaldTrump     5           0        0  193     2   30   56   72    33        0
## [ reached max_nfeat ... 8,524 more features ]

Diese DFM hat also nur noch zwei Zeilen (= zwei Dokumente, eines pro Account) und summiert die Feature-Häufigkeiten über alle Tweets, getrennt nach Account, hinweg.

Wir können nun die Wortwolke auch für unsere beiden Accounts getrennt erzeugen lassen, indem wir das Argument comparison = TRUE verwenden – mit der alten DFM auf Tweet-Ebene würde hier für jedes Dokument (Tweet) getrennt eine Wolke erzeugt werden, was natürlich kaum sinnvoll darstellbar und interpretierbar wäre.

textplot_wordcloud(tweets_dfm_grouped, 
                   max_words = 100, 
                   comparison = TRUE, 
                   color = c("blue", "red"))

18.2 Konkordanzen (Keywords in context)

Durch den Bag-of-Word-Approach verlieren wir den Kontext, in dem Begriffe fallen. Daher kann es oft sinnvoll sein, für ausgewählte Schlüsselbegriffe auch die zugehörigen Textstellen samt Kontext auszugeben. Dies wird auch als Konkordanz bezeichnet.

In Quanteda können wir über die Funktion kwic() (für Keywords in context) solche Konkordanzen ausgeben lassen. Dies kann anhand eines Korpus- oder eines Token-Objekts geschehen; da wir die Tokens bereits um Stoppwörter bereinigt haben, ist es hier sinnvoller den Korpus zu nutzen, damit sich der Kontext im ursprünglichen Satzzusammenhang erschließen lässt. Als zweites Argument benötigen wir noch das Suchmuster, nach dem gesucht werden soll – in der Regel also ein bestimmter Schlüsselbegriff (kwic() ist per Default case-insesitive, ignoriert also Groß- und Kleinschreibung, sodass wir mit “news” auch “News” und “NEWS” finden):

kwic(tweets_corpus, "news") %>% 
  as_tibble()
## # A tibble: 248 x 7
##    docname  from    to pre                                    keyword post                            pattern
##    <chr>   <int> <int> <chr>                                  <chr>   <chr>                           <fct>  
##  1 36         10    10 off on commenting on the               news    tonight until we know more      news   
##  2 150         2     2 Great                                  news    out of Utah . No                news   
##  3 282        21    21 already - but I've got                 news    for them : We're not            news   
##  4 446         5     5 I'm heartbroken at the                 news    of yet another mass shooting    news   
##  5 674        28    28 live-tweeting her appearances on cable news    . We need a president           news   
##  6 882         3     3 This morning's                         news    of another rise in unemployment news   
##  7 943         2     2 Today's                                news    that more than 22 million       news   
##  8 1113       15    15 numbers we see in the                  news    are more than just statistics   news   
##  9 1268       17    17 toll we see in the                     news    is so much more than            news   
## 10 1435        7     7 , I've got some big                    news    : Next week , I'm               news   
## # ... with 238 more rows

Zu den Informationen, die wir erhalten, zählen die Dokument-ID, die Textstelle (in Tokens) in diesem Dokument, ab dem unser Begriff auftritt, sowie der vorherige und nachfolgende Satzkontext (per Default bis zu 5 Wörter, kann mit dem window-Argument angepasst werden).

Das Suchmuster kann einen RegEx-Ausdruck beinhalten, um beispielsweise schnell nach allen Hashtags zu suchen:

kwic(tweets_corpus, "#*") %>% 
  as_tibble()
## # A tibble: 374 x 7
##    docname  from    to pre                               keyword    post                  pattern
##    <chr>   <int> <int> <chr>                             <chr>      <chr>                 <fct>  
##  1 74          9     9 step on stage for tonight's       #DemDebate "in Iowa . Make sure" #*     
##  2 75         42    42 be Commander in Chief .           #DemDebate ""                    #*     
##  3 77         49    49 an increasingly dangerous world . #DemDebate ""                    #*     
##  4 78         47    47 advance our common security .     #DemDebate ""                    #*     
##  5 79         18    18 of the American people .          #DemDebate ""                    #*     
##  6 80         53    53 Kim regime's bad behavior .       #DemDebate ""                    #*     
##  7 81         39    39 don't share our values .          #DemDebate ""                    #*     
##  8 82         38    38 system easier to navigate .       #DemDebate ""                    #*     
##  9 83         12    12 prices with drug companies .      #DemDebate ""                    #*     
## 10 86         41    41 affordable for every parent .     #DemDebate ""                    #*     
## # ... with 364 more rows

18.3 Kollokationen

Als Kollokation wird das gemeinsame Auftreten von zwei oder mehr Wörtern bezeichnet. Wir können uns solche Kollokationen mit textstat_collocations() ausgeben lassen, wobei auch hier entweder ein Korpus- oder ein Token-Objekt angegeben werden muss. Hier zunächst mit dem Korpus:

textstat_collocations(tweets_corpus) %>% 
  as_tibble() %>% 
  arrange(desc(count))
## # A tibble: 11,805 x 6
##    collocation  count count_nested length lambda     z
##    <chr>        <int>        <int>  <dbl>  <dbl> <dbl>
##  1 of the         434            0      2  1.75  31.2 
##  2 in the         396            0      2  1.91  32.2 
##  3 thank you      309            0      2  8.11  34.8 
##  4 donald trump   302            0      2  8.17  47.7 
##  5 will be        254            0      2  4.33  49.9 
##  6 to the         251            0      2  0.469  6.96
##  7 for the        245            0      2  1.67  22.9 
##  8 on the         229            0      2  2.05  26.2 
##  9 is a           225            0      2  2.36  30.7 
## 10 we need        223            0      2  5.04  42.1 
## # ... with 11,795 more rows

Das sagt uns also noch nicht sonderlich viel über die Dokumente aus, da die häufigsten Kollokationen aus Stoppwörtern (“of the”, “in the” etc.) bestehen. Wir können dies umgehen, indem wir die bereits um diese Wörter bereinigten Tokens übergeben:

textstat_collocations(tweets_tokens) %>% 
  as_tibble() %>% 
  arrange(desc(count))
## # A tibble: 6,231 x 6
##    collocation       count count_nested length lambda     z
##    <chr>             <int>        <int>  <dbl>  <dbl> <dbl>
##  1 donald trump        302            0      2   7.50  44.4
##  2 fake news           151            0      2   7.67  38.4
##  3 white house         145            0      2   9.28  30.7
##  4 complete total      104            0      2   8.93  31.2
##  5 total endorsement   103            0      2   8.42  32.9
##  6 united states        87            0      2   8.63  28.7
##  7 american people      79            0      2   4.37  29.8
##  8 health care          73            0      2   7.12  32.4
##  9 president trump      65            0      2   3.26  22.7
## 10 military vets        60            0      2   7.64  28.8
## # ... with 6,221 more rows

Eine andere Möglichkeit besteht darin, nicht nach der absoluten Häufigkeit, sondern nach dem ebenfalls berechneten Lambda-Koeffizienten zu sortieren. Dieser fällt, vereinfacht gesagt, umso höher aus, je wahrscheinlicher exakt diese Kombination aus Wörtern ist (so gehören etwa sowohl “donald trump” als auch “president trump” zu den absolut am häufigsten vorhandenen Kollokationen; diese weisen aber ein geringeres Lambda auf, da “trump” sowohl mit “donald” als auch mit “president” auftritt und entsprechend die Wahrscheinlichkeit geringer ist als bei Wortpaaren, die nahezu immer in dieser Kombination in diesem Korpus auftreten, z. B. “oval office”). Mit dem min_count-Argument können wir festlegen, dass die jeweilige Kollokation mindestens x mal im Korpus vorkommen muss:

textstat_collocations(tweets_corpus, min_count = 10) %>% 
  as_tibble() %>% 
  arrange(desc(lambda))
## # A tibble: 1,200 x 6
##    collocation     count count_nested length lambda     z
##    <chr>           <int>        <int>  <dbl>  <dbl> <dbl>
##  1 town hall          28            0      2   15.1  9.19
##  2 swine flu          10            0      2   14.1  8.48
##  3 prime minister     11            0      2   13.3  8.66
##  4 THANK YOU          50            0      2   13.3 14.2 
##  5 witch hunt         21            0      2   13.1  8.89
##  6 approval rating    26            0      2   12.8 13.3 
##  7 george floyd       14            0      2   12.8  8.63
##  8 FAKE NEWS          17            0      2   12.2 14.1 
##  9 oval office        12            0      2   11.8  8.05
## 10 KEEP AMERICA       13            0      2   11.6  7.97
## # ... with 1,190 more rows

Zwar werden in der Regel Kollokationen von zwei Wörtern untersucht, mit dem size-Argument können wir aber auch das gemeinsame Auftreten von mehr als zwei Wörtern ausgeben lassen:

textstat_collocations(tweets_tokens, size = 4) %>% 
  as_tibble() %>% 
  arrange(desc(count))
## # A tibble: 2,249 x 6
##    collocation                      count count_nested length lambda      z
##    <chr>                            <int>        <int>  <dbl>  <dbl>  <dbl>
##  1 approval rating republican party    20            0      4   9.43  2.28 
##  2 radical left nothing democrats      19            0      4   2.43  0.671
##  3 donald trump white house            15            0      4   6.15  1.62 
##  4 end gun violence epidemic           15            0      4   1.82  0.441
##  5 white house news conference         14            0      4   1.51  0.414
##  6 get donald trump white              12            0      4   1.25  0.347
##  7 help keep momentum going            12            0      4  -1.44 -0.436
##  8 rating republican party thank       12            0      4  -1.87 -0.462
##  9 complete total endorsement vote     10            0      4  -3.91 -1.00 
## 10 military vets second amendment       9            0      4   8.45  2.30 
## # ... with 2,239 more rows

18.4 Kookkurenzen

Während bei Kollokationen zwei oder mehr Wörter genau in dieser Wortfolge gemeinsam auftreten müssen, untersucht man mittels Kookkurenzen das gemeinsame Auftreten von Wörtern (oder anderen lexikalischen Einheiten) innerhalb einer höher geordneten Einheit, z. B. in einem Dokument. Hierfür wird zunächst eine Co-occurence-Matrix aufgestellt, die für jeden Token prüft, wie oft dieser mit jeweils allen anderen Tokens im Korpus gemeinsam in einem Dokument auftritt. Das Ergebnis ist also eine Matrix, die genausoviele Zeilen wie Spalten (= alle Tokens im Korpus) aufweist. Wir können diese Matrix mit der Funktion fcm() (für feature co-occurence matrix) erstellen:

tweets_com <- fcm(tweets_tokens)
tweets_com
## Feature co-occurrence matrix of: 8,534 by 8,534 features.
##              features
## features      final fundraising deadline just hours away need help every donation
##   final           0           2        2    7     1    3    6    7     8        3
##   fundraising     0           0        3    3     1    1    2    4     2        2
##   deadline        0           0        0    2     1    1    2    5     2        2
##   just            0           0        0   14     2   22   39   50    37        6
##   hours           0           0        0    0     0    1    4    4     2        1
##   away            0           0        0    0     0    8   13   10     8        3
##   need            0           0        0    0     0    0   40   73    43        6
##   help            0           0        0    0     0    0    0   21    28        9
##   every           0           0        0    0     0    0    0    0    14        6
##   donation        0           0        0    0     0    0    0    0     0        0
## [ reached max_feat ... 8,524 more features, reached max_nfeat ... 8,524 more features ]

Auch hier können wir wieder die tidy()-Funktion aus dem Tidytext-Package nutzen, um schnell die häufigsten Kookkurenzen zu erhalten – zu beachten ist hier, dass die Reihenfolge der Wörter keine Rolle spielt:

tweets_com %>% 
  tidy() %>% 
  arrange(desc(count))
## # A tibble: 370,365 x 3
##    document    term      count
##    <chr>       <chr>     <dbl>
##  1 donald      trump       329
##  2 news        fake        191
##  3 white       house       154
##  4 trump       president   153
##  5 endorsement complete    113
##  6 need        president   110
##  7 total       complete    108
##  8 endorsement total       107
##  9 care        health       98
## 10 united      states       95
## # ... with 370,355 more rows

18.5 Textkomplexität

um die Komplexität von Texten zu quantifizieren, gibt es mehrere Herangehensweisen, wobei insbesondere die folgenden beiden weit verbreitet sind:

  • Lesbarkeit: Hier wird quantifiziert, wie einfach ein Text lesbar ist. Das wohl bekannteste Lesbarkeitsmaß ist der Flesch Reading Ease (FRE), bei dem die durchschnittliche Satzlänge in Wörtern und die durchschnittliche Silbenzahl pro Wort miteinander verrechnet werden. In Quanteda können zahlreiche Lesbarkeitsmaße mit der Funktion textstat_readability() berechnet werden – für kurze Tweets sind solche Berechnungen aber weniger sinnvoll, weshalb dies hier ausgespart wird.
  • Lexikalische Diversität: Hier wird quantifiziert, wie vielfältig (‘lexically rich’) ein Text ist. Das wohl bekannteste Maß ist das Type-Token-Ratio (TTR), wobei einfach die Anzahl an Types, also einzigartigen Tokens, durch die Anzahl an Token geteilt wird. Ein hohes TTR steht demnach für einen großen Wortschatz, wohingegen ein geringes TTR dafür spricht, dass sich viele Wörter häufig wiederholen. Allerdings ist zu beachten, dass das TTR von der Textlänge beeinflusst wird, da es naturgemäß immer schwieriger wird, keine Wörter mehrfach zu verwenden, je länger ein Text ist. Mit der Funktion textstat_lexdiv() lassen sich neben dem TTR (Default-Maß) daher auch noch einige andere Maße berechnen.
textstat_lexdiv(tweets_dfm_grouped)
##          document       TTR
## 1        JoeBiden 0.1736629
## 2 realDonaldTrump 0.1801794

18.6 Keyness

Während die bisherigen Auswertungen und Maße auch zur Beschreibung von einzelnen Texten bzw. Dokumenten oder gesamten Korpora verwendet werden können, lernen wir nun ein erstes Vergleichsmaß kennen. Mit Keyness wird quantifiziert, wie distinkt ein Begriff für einen Text im Vergleich zu allen anderen Texten im Korpus ist. Es geht also nicht nur darum, dass ein Wort häufig in einem Text vorkommt, sondern zugleich eher selten in den Vergleichstexten ist und somit besonders gut geeignet ist, um den Zieltext zu identifzieren. Wörter mit hoher Keyness können entsprechend als Keyword für diesen Text bezeichnet werden.

Keyness-Maße werden berechnet, indem die Worthäufigkeiten im Zieltext mit den erwarteten Worthäufigkeiten im Vergleichskorpus in einem statistischen Test (z. B. Chi²-Test oder Likelihood-Ratio-Test) verglichen werden. In Quanteda können wir die Keyness mit der Funktion textstat_keyness() berechnen, wobei eine DFM als erstes Argument sowie mit dem Argument target das Zieldokument angegeben wird (alle anderen Dokumente dienen dann jeweils als Vergleichsdokumente). Per Default wird der Chi²-Test genutzt, andere Testverfahren können über das measure-Argument angefordert werden.

Um besonders distinkte Begriffe für Joe Biden bzw. Donald Trump auszuwerten, müssen wir wieder die gruppierte DFM nutzen, sodass alle Tweets eines Accounts als “ein” Dokument gezählt werden:

textstat_keyness(tweets_dfm_grouped, target = "JoeBiden") %>% 
  as_tibble()
## # A tibble: 8,534 x 5
##    feature    chi2     p n_target n_reference
##    <chr>     <dbl> <dbl>    <dbl>       <dbl>
##  1 donald     452.     0      342          10
##  2 trump      274.     0      396         116
##  3 need       246.     0      282          56
##  4 crisis     196.     0      157           8
##  5 nation     195.     0      188          24
##  6 health     140.     0      127          13
##  7 president  135.     0      307         142
##  8 every      125.     0      151          33
##  9 covid-19   115.     0       99           8
## 10 folks      109.     0       77           0
## # ... with 8,524 more rows
textstat_keyness(tweets_dfm_grouped, target = "realDonaldTrump") %>% 
  as_tibble()
## # A tibble: 8,534 x 5
##    feature    chi2        p n_target n_reference
##    <chr>     <dbl>    <dbl>    <dbl>       <dbl>
##  1 great     346.  0.            589          30
##  2 news      144.  0.            238          10
##  3 fake      139.  0.            196           0
##  4 total      87.6 0.            138           4
##  5 democrats  86.3 0.            143           6
##  6 complete   83.5 0.            125           2
##  7 thank      81.9 0.            309          71
##  8 @foxnews   76.7 0.            108           0
##  9 military   74.9 0.            120           4
## 10 vets       61.0 5.55e-15       86           0
## # ... with 8,524 more rows

Ein mittels textstat_keyness() erzeugtes Objekt kann zudem der Funktion textplot_keyness() übergeben werden, um die Keywords auch grafisch darzustellen:

textstat_keyness(tweets_dfm_grouped, target = "realDonaldTrump") %>% 
  textplot_keyness(n = 10, color = c("red", "blue"))

18.7 Übungsaufgaben

Erstellen Sie für die folgende Übungsaufgabe eine eigene Skriptdatei oder eine R-Markdown-Datei und speichern diese als ue18_nachname.R bzw. ue18_nachname.Rmd ab.

Laden Sie den Datensatz facebook_europawahl.csv und filtern Sie lediglich Posts der im Bundestag vertretenen Parteien.


Übungsaufgabe 18.1 Textdeskription:

Führen Sie selbstständig eine Textdeskription der Facebook-Posts (und die dazu notwendigen Vorbereitungsschritte) durch. Welche Verfahren bieten sich dafür an? Welche Probleme fallen Ihnen dabei auf?

Betrachten Sie abschließend Keywords für mindestens drei der im Datensatz vorhandenen Parteien. Beschreiben Sie zudem Möglichkeiten, wie man die Ergebnisse (noch) aussagekräftiger gestalten könnte.