A Lösungen der Übungsaufgaben

Kapitel 2: Objekte und Datenstrukturen

Lösung zur Übungsaufgabe 2.1:

Am sinnvollsten ist eine Liste list(), da diese heterogene Objekttypen beinhalten kann. Ein Dataframe lohnt sich bei nur einem Fall eher nicht.

myself <- list(
  name = "Julian", # Texte als character
  year = 1988L, # Jahr als numeric - oder noch präziser als Integer
  from_bavaria = FALSE # Binäre Entscheidung als logical
)

Auch wenn wir hier alle Werte z. B. als Text repräsentieren könnten, ist es immer sinnvoll, den Objekttypen zu verwenden, der am besten zu den Werten passt – numerische (year) und logische Objekte (from_bavaria) ermöglichen uns mehr Rechenoptionen, einfacheres Filtern von Datensätzen etc.


Lösung zur Übungsaufgabe 2.2:

values <- c(1.2, 1.3, 0.8, 0.7, 0.7, 1.5, 1.1, 1.0, 1.1, 1.2, 1.1)
average <- mean(values)
above_average <- values > average
sum(above_average) / length(values)
## [1] 0.6363636
  1. In der ersten Zeile ordnen wir values einen numerischen Vektor aus einigen Zahlen zu
  2. In der zweiten Zeile berechnen wir den Mittelwert von values und weisen diesen average zu.
  3. values > average prüft nun für jeden Wert in values, ob dieser größer als der Mittelwert (gespeichert in average ist). Dies erzeugt einen logical-Vektor, den wir above_average zuweisen.
  4. sum(above_average) zählt, wie viele TRUE-Werte in dem Vektor sind. Das ist darauf zurückzuführen, dass TRUE die numerische Entsprechung 1, FALSE die numerische Entsprechung 0 hat; sum() wandelt den logischen Vektor automatisch in einen numerischen um. Wir teilen dies durch die Anzahl der Werte in values und bekommen als Ergebnis, dass 63.6 % der Werte in values über dem Mittelwert liegen. (Etwas schneller hätten wir dieses Ergebnis auch bekommen, wenn wir die letzte Zeile durch mean(above_average) ersetzen.)

Lösung zur Übungsaufgabe 2.3:

str(mtcars)
## 'data.frame':    32 obs. of  11 variables:
##  $ mpg : num  21 21 22.8 21.4 18.7 18.1 14.3 24.4 22.8 19.2 ...
##  $ cyl : num  6 6 4 6 8 6 8 4 4 6 ...
##  $ disp: num  160 160 108 258 360 ...
##  $ hp  : num  110 110 93 110 175 105 245 62 95 123 ...
##  $ drat: num  3.9 3.9 3.85 3.08 3.15 2.76 3.21 3.69 3.92 3.92 ...
##  $ wt  : num  2.62 2.88 2.32 3.21 3.44 ...
##  $ qsec: num  16.5 17 18.6 19.4 17 ...
##  $ vs  : num  0 0 1 1 0 1 0 1 1 1 ...
##  $ am  : num  1 1 1 0 0 0 0 0 0 0 ...
##  $ gear: num  4 4 4 3 3 3 3 4 4 4 ...
##  $ carb: num  4 4 1 1 2 1 4 2 2 4 ...

mtcars enthält 11 Variablen, allesamt numeric, und 32 Fälle.

mean(mtcars$cyl)
## [1] 6.1875

Im Durchschnitt haben die Fahrzeuge ca. 6.2 Zylinder.

Um einen Teildatensatz cars_short, der lediglich die Variablen mpg und hp enthält, zu erstellen, führen viele Wege zum Ziel, z. B.:

cars_short <- mtcars[, c("mpg", "hp")]
cars_short <- mtcars[c("mpg", "hp")] # Steht nur eine Angabe in den eckigen Klammern, interpretiert R dies als Spaltenangabe
cars_short <- data.frame(mtcars$mpg, mtcars$hp)

Kapitel 3: Funktionen

Lösung zur Übungsaufgabe 3.1:

Um die gewünschte Sequenz zu erzeugen, benötigen wir die Argumente from (Startwert), to (Endwert) und by (Zunahmewert).

seq(from = 0, to = 100, by = 5)
##  [1]   0   5  10  15  20  25  30  35  40  45  50  55  60  65  70  75  80  85  90  95 100

Da es sich, wie wir der Funktionsdokumentation entnehmen können, dabei um die ersten drei Funktionsargumente handelt, können wir diese auch unbenannt übergeben:

seq(0, 100, 5)
##  [1]   0   5  10  15  20  25  30  35  40  45  50  55  60  65  70  75  80  85  90  95 100

Lösung zur Übungsaufgabe 3.2:

Unsere Funktion benötigt lediglich ein Argument, die Temperatur in Grad Fahrenheit (als numerischen Wert), und soll diese in Grad Celsius mit der Formel \(°C = (°F - 32) × 5/9\) umwandeln:

fahrenheit_to_celsius <- function(fahrenheit) {
  celsius <- (fahrenheit - 32) * 5/9
  return(celsius)
}

# Unsere neue Funktion kann sogar mehrere Temperaturwerte auf einmal umrechnen
fahrenheit_to_celsius(c(0, 50, 80, 100))
## [1] -17.77778  10.00000  26.66667  37.77778

Lösung zur Übungsaufgabe 3.3:

Für das erste zusätzliche Feature, der Anzahl der fehlenden Werte, müssen wir descriptives_vector lediglich ein Element hinzufügen (das wir z. B. Missing nennen), in dem eben diese Anzahl festgehalten wird. Mit der Funktion is.na() prüfen wir jeden Wert eines Vektors darauf, ob es sich um einen fehlenden Wert NA handelt, mit der Summenfunktion sum() können wir diese addieren. Wir ändern descriptives_vector daher wie folgt:

  descriptives_vector <- c(
    n = length(x),
    Missing = sum(is.na(x)), # Hier zählen wir die fehlenden Werte
    M = mean(x, na.rm = na.rm),
    SD = sd(x, na.rm = na.rm),
    Minimum = min(x, na.rm = na.rm), 
    Maximum = max(x, na.rm = na.rm),
    Median = median(x, na.rm = na.rm)
  )

Für das zweite zusätzliche Feature, Rundung auf eine gewünsche Anzahl an Nachkommastelle, benötigen wir die round()-Funktion, mit dem wir descriptives_vector abschließend runden, und ein zusätzliches Argument, mit dem die gewünschte Anzahl an Nachkommastellen übergeben werden kann. Da dieses Argument bei der round()-Funktion digits heißt, nennen wir es aus Konsistenzgründen auch in unserer Funktion so. Um standardmäßig auf zwei Nachkommastellen zu runden, geben wir dem Argument den Default-Wert 2. Der vollständige Funktionscode sieht also wie folgt aus:

descriptives <- function(x, na.rm = FALSE, digits = 2) { # Zusätzliches Argument digits mit Default-Wert 2
  
  # Vektor mit Variablenbeschreibung erstellen
  descriptives_vector <- c(
    n = length(x),
    Missing = sum(is.na(x)), # Hier zählen wir die fehlenden Werte
    M = mean(x, na.rm = na.rm),
    SD = sd(x, na.rm = na.rm),
    Minimum = min(x, na.rm = na.rm), 
    Maximum = max(x, na.rm = na.rm),
    Median = median(x, na.rm = na.rm)
  )
  
  # Vektor runden
  descriptives_vector <- round(descriptives_vector, digits = digits)
  
  return(descriptives_vector)
}

Wir haben nun eine flexibel einsetzbare Funktion, um schnell relevante Kennwerte einer numerischen Variablen zu erhalten:

descriptives(iris$Sepal.Length)
##       n Missing       M      SD Minimum Maximum  Median 
##  150.00    0.00    5.84    0.83    4.30    7.90    5.80
descriptives(mtcars$cyl, digits = 1)
##       n Missing       M      SD Minimum Maximum  Median 
##    32.0     0.0     6.2     1.8     4.0     8.0     6.0

Kapitel 4: Kontrollstrukturen

Lösung zur Übungsaufgabe 4.1:

Erneut gibt es verschiedene Möglichkeiten, den Entscheidungsbaum abzubilden. Wenn wir uns pro if () bzw. else () oder else if () auf das Prüfen einer Bedingung beschränken wollen, benötigen wir einen verschachtelten Baum, also eine Bedinung in einer Bedingung:

if (news_channel != "Internet") {             # Prüfen, ob news_channel NICHT "Internet" ist...
  news_category <- "Offline"                  # dann news_category "Offline" zuweisen
} else {                                      # Falls das nicht der Fall ist, also news_channel "Internet" ist..
  if (news_website == "Twitter") {            # dann prüfen wir ob news_category "Twitter" ist
    news_category <- "SNS"                    # falls das so ist, weisen wir news_category "SNS" zu
  } else if (news_website == "Facebook") {    # analog verfahren wir mit Facebook
    news_category <- "SNS"                    #
  } else if (news_website == "Instagram") {   # analog mit Instagram
    news_category <- "SNS"                    #
  } else {                                    # falls das alles nicht zutrifft
    news_category <- "Online: Sonstige"       # weisen wir "Online: Sonstige" zu
  }
}

Das erzeugt allerdings einen ziemlich langen Entscheidungsbaum und einige Redundanzen, da wir für "Twitter", "Facebook" und "Instagram" jeweils dieselbe Aktion, news_category <- "SNS" ausführen. Wir können diese Bedingungen also auch verknüpfen:

if (news_channel != "Internet") {             # Prüfen, ob news_channel NICHT "Internet" ist...
  news_category <- "Offline"                  # dann news_category "Offline" zuweisen
} else {                                      # Falls das nicht der Fall ist, also news_channel "Internet" ist..
  if (news_website == "Twitter" | news_website == "Facebook" | news_website == "Instagram") {
    news_category <- "SNS"                    # Alle Bedingungen mit ODER verbunden, dann SNS zuweisen
  } else {                                    # falls das nicht zutrifft
    news_category <- "Online: Sonstige"       # weisen wir "Online: Sonstige" zu
  }                                           # und haben uns einige Zeilen gespart
}

Tatsächlich können wir auch die Verschachtelung aufheben, da nach if (news_channel != "Internet") folgt, dass bei allen anschließenden else if()-Bedingungen news_channel == "Internet" ist:

if (news_channel != "Internet") {             
  news_category <- "Offline"                  
} else if (news_website == "Twitter" | news_website == "Facebook" | news_website == "Instagram") {
  news_category <- "SNS" 
} else {                                    
  news_category <- "Online: Sonstige" 
}

Und noch kürzer wir der Entscheidungsbaum, wenn wir den %in%-Operator verwenden:

SNS <- c("Twitter", "Facebook", "Instagram")

if (news_channel != "Internet") {             
  news_category <- "Offline"                  
} else if (news_website %in% SNS) {
  news_category <- "SNS" 
} else {                                    
  news_category <- "Online: Sonstige" 
}

Lösung zur Übungsaufgabe 4.2:

Beim ersten Platzhalter müssen wir einen for-Loop, wie in Kapitel 4.2.2 beschrieben, einfügen und uns für einen Namen für das Iterator-Objekt entscheiden. Da wir über den Vektor variables loopen, bietet sich der Singular variable an (aber natürlich funktioniert auch jeder andere Objektname). Diesen müssen wir dann bei den folgenden Platzhaltern ergänzen:

numeric_summary <- function(data) {
  
  # Alle Variablennamen in Vektor speichern
  variables <- names(data)
  
  # Leere Liste für Ausgabe vorbereiten
  summary_list <- list()
  
  # Über alle Variablen iterieren
  for (variable in variables) { # Wir loopen über variables
    variable_vector <- data[[variable]] # Und arbeiten nun immer mit dem Iterator-Objekt variable
    
    if (is.numeric(variable_vector)) { # Prüfen ob die Variable numerisch ist
      
      # Mittelwert und Standardabweichung dieser Variablen der summary_list hinzufügen
      summary_list[[variable]] <- c(
        M = mean(variable_vector),   
        SD = sd(variable_vector)
      )
    }
    
  }
  
  # Summary List ausgeben
  return(summary_list)
}

Diese Funktion erzeugt uns nun auf einen Schlag eine Kurzzusammenfassung anhand von Mittelwert und Standardabweichung aller numerischen Variablen in einem Datensatz:

numeric_summary(iris)
## $Sepal.Length
##         M        SD 
## 5.8433333 0.8280661 
## 
## $Sepal.Width
##         M        SD 
## 3.0573333 0.4358663 
## 
## $Petal.Length
##        M       SD 
## 3.758000 1.765298 
## 
## $Petal.Width
##         M        SD 
## 1.1993333 0.7622377
numeric_summary(mtcars)
## $mpg
##         M        SD 
## 20.090625  6.026948 
## 
## $cyl
##        M       SD 
## 6.187500 1.785922 
## 
## $disp
##        M       SD 
## 230.7219 123.9387 
## 
## $hp
##         M        SD 
## 146.68750  68.56287 
## 
## $drat
##         M        SD 
## 3.5965625 0.5346787 
## 
## $wt
##         M        SD 
## 3.2172500 0.9784574 
## 
## $qsec
##         M        SD 
## 17.848750  1.786943 
## 
## $vs
##         M        SD 
## 0.4375000 0.5040161 
## 
## $am
##         M        SD 
## 0.4062500 0.4989909 
## 
## $gear
##         M        SD 
## 3.6875000 0.7378041 
## 
## $carb
##      M     SD 
## 2.8125 1.6152

Kapitel 8: Daten laden, modifizieren und speichern

Lösung zur Übungsaufgabe 8.1:

Wir benötigen die read_csv()-Funktion, da alle Werte durch Kommas getrennt sind. Falls der Datensatz im Hauptverzeichnis des Projektordners liegt, genügt die Angabe von "facebook_europawahl.csv" als Argument:

facebook_europawahl <- read_csv("facebook_europawahl.csv")

Liegt der Datensatz in einem Unterordner, muss der Dateipfad entsprechend als Argument angepasst werden, z. b. "data/facebook_europawahl.csv".

## 
## -- Column specification ----------------------------------------------------------------------------------------------------------------------------------------
## cols(
##   id = col_double(),
##   URL = col_character(),
##   party = col_character(),
##   timestamp = col_datetime(format = ""),
##   type = col_character(),
##   message = col_character(),
##   link = col_character(),
##   comments_count = col_double(),
##   shares_count = col_double(),
##   reactions_count = col_double(),
##   like_count = col_double(),
##   love_count = col_double(),
##   wow_count = col_double(),
##   haha_count = col_double(),
##   sad_count = col_double(),
##   angry_count = col_double()
## )

Lösung zur Übungsaufgabe 8.2:

Um den Datensatz zu filtern, benötigen wir zunächst die Schreibweisen der Partei-Accounts. Hierzu bietet es sich an, die party-Variable auszuzählen:

count(facebook_europawahl, party)
## # A tibble: 14 x 2
##    party                               n
##    <chr>                           <int>
##  1 alternativefuerde                  79
##  2 B90DieGruenen                      67
##  3 CDU                                64
##  4 CSU                               103
##  5 DiePARTEI                          96
##  6 FamilienParteiDeutschlands         30
##  7 FDP                                94
##  8 freie.waehler.bundesvereinigung    30
##  9 linkspartei                        38
## 10 oedp.de                            71
## 11 Piratenpartei                      73
## 12 SPD                                49
## 13 tierschutzpartei                   33
## 14 VoltDeutschland                    75

Ebenfalls optional, aber hilfreich ist es, die entsprechenden Parteiseiten in einem Vektor zu speichern:

bt_parteien <- c("alternativefuerde", "B90DieGruenen", "CDU", "CSU", "FDP", "linkspartei", "SPD")

Nun filtern wir den Datensatz zunächst nach Parteien:

df_bt_parteien <- filter(facebook_europawahl, party %in% bt_parteien)

Dann wählen wir nur die gewünschten Variablen aus:

df_reduziert <- select(df_bt_parteien, party, timestamp, type,
                       comments_count, shares_count, reactions_count)

Und schließlich erzeugen wir die neue Variable total_count:

df_mit_tc <- mutate(df_reduziert,
                    total_count = sum(c(comments_count, shares_count, reactions_count), na.rm = TRUE))

df_mit_tc
## # A tibble: 494 x 7
##    party             timestamp           type  comments_count shares_count reactions_count total_count
##    <chr>             <dttm>              <chr>          <dbl>        <dbl>           <dbl>       <dbl>
##  1 B90DieGruenen     2019-04-28 06:00:01 video             70           28             215      871753
##  2 FDP               2019-04-28 11:49:59 photo             16            9             262      871753
##  3 CDU               2019-04-28 09:12:19 video            239          136             398      871753
##  4 SPD               2019-04-28 13:06:09 photo            180           54             699      871753
##  5 CSU               2019-04-28 08:21:00 photo            174           61             458      871753
##  6 alternativefuerde 2019-04-28 14:55:00 link            1163         1499            3944      871753
##  7 FDP               2019-04-28 06:18:18 photo             47          110             622      871753
##  8 FDP               2019-04-28 14:03:00 video            358           89             463      871753
##  9 FDP               2019-04-28 10:40:57 photo             14           19             226      871753
## 10 CSU               2019-04-28 12:35:00 photo            133           20             330      871753
## # ... with 484 more rows

Warum addieren wir nicht einfach alle Spalten ohne die Summenfunktion?

df_mit_total2 <- mutate(df_reduziert, total_count = comments_count + shares_count + reactions_count)

df_mit_total2
## # A tibble: 494 x 7
##    party             timestamp           type  comments_count shares_count reactions_count total_count
##    <chr>             <dttm>              <chr>          <dbl>        <dbl>           <dbl>       <dbl>
##  1 B90DieGruenen     2019-04-28 06:00:01 video             70           28             215         313
##  2 FDP               2019-04-28 11:49:59 photo             16            9             262         287
##  3 CDU               2019-04-28 09:12:19 video            239          136             398         773
##  4 SPD               2019-04-28 13:06:09 photo            180           54             699         933
##  5 CSU               2019-04-28 08:21:00 photo            174           61             458         693
##  6 alternativefuerde 2019-04-28 14:55:00 link            1163         1499            3944        6606
##  7 FDP               2019-04-28 06:18:18 photo             47          110             622         779
##  8 FDP               2019-04-28 14:03:00 video            358           89             463         910
##  9 FDP               2019-04-28 10:40:57 photo             14           19             226         259
## 10 CSU               2019-04-28 12:35:00 photo            133           20             330         483
## # ... with 484 more rows

Dies führt augenscheinlich zunächst zum gleichen Ergebnis, hat aber ein Problem: kommen in einer der drei Facebook-Metriken fehlende Werte in Form von NA vor, ist das Ergebnis in total_count ebenfalls NA. Der Summenfunktion sum() können wir mit dem Argument na.rm = TRUE mitteilen, dass fehlende Werte nicht berücksichtigt werden sollen:

# Fehlende Werte zählen:
sum(is.na(df_mit_tc$total_count))
sum(is.na(df_mit_total2$total_count))
## [1] 0
## [1] 5

Wählen wir diese +-Variante, haben wir also 5 fehlende Werte in unserem total_count, bei der ersten Variante keine.

Nun können wir den Datensatz abspeichern:

write_csv(df_mit_tc, "data/df_reduziert.csv")
saveRDS(df_mit_tc, "data/df_reduziert.rds")

Lösung zur Übungsaufgabe 8.3:

Zunächst wählen wir nur die Woche vor der Wahl aus. Hierzu können wir die timestamp-Variable anfiltern – Text, der wie ein Datum aussieht, wir dabei automatisch in ein Datum konvertiert:

df_woche_vor_wahl <- filter(facebook_europawahl, timestamp >= "2019-05-20")

Nun gruppieren wir den Datensatz nach party:

df_group_by_party <- group_by(df_woche_vor_wahl, party)

Und schließlich berechnen wir mit summarize() die gewünschten Kennwerte:

summarize(df_group_by_party,
          M_comments = mean(comments_count, na.rm = TRUE),
          SD_comments = sd(comments_count, na.rm = TRUE),
          M_shares = mean(shares_count, na.rm = TRUE),
          SD_shares = sd(shares_count, na.rm = TRUE),
          M_reactions = mean(reactions_count, na.rm = TRUE),
          SD_reactions = sd(reactions_count, na.rm = TRUE))
## # A tibble: 14 x 7
##    party                           M_comments SD_comments M_shares SD_shares M_reactions SD_reactions
##    <chr>                                <dbl>       <dbl>    <dbl>     <dbl>       <dbl>        <dbl>
##  1 alternativefuerde                 1210.        1250.     1819.    2491.        4690.        3109. 
##  2 B90DieGruenen                      106.          67.0     211.     244.         740.         682. 
##  3 CDU                                508.         897.      113.     293.         870.        1548. 
##  4 CSU                                182.         148.       58.5     45.3        579.         421. 
##  5 DiePARTEI                           92.5        137.      147.     175.        1874.        1879. 
##  6 FamilienParteiDeutschlands           0.714        1.50     14        9.78        14.9         14.9
##  7 FDP                                106.         108.      111.     107.         802.         564. 
##  8 freie.waehler.bundesvereinigung     10.7         12.3      25.7     20.3         86           33.7
##  9 linkspartei                        161.         100.      217.     176.        1176.        1142. 
## 10 oedp.de                              8.30        11.7      30.3     24.6        141.         108. 
## 11 Piratenpartei                       15.3         25.9      44.2     50.1        163.         213. 
## 12 SPD                                240.         277.      137.     120.         658.         510. 
## 13 tierschutzpartei                    82          110.      288      378.         999.        1193. 
## 14 VoltDeutschland                     27.7         70.8      54.7     88.7        316.         365.

Kapitel 9: Der Pipe-Operator %>%

Lösung zur Übungsaufgabe 9.1:

Dank Pipes können wir uns bei Übungsaufgabe 8.2 die ganzen Zwischendatensätze sparen. Es ist jedoch sinnvoll, vor dem Speichern ein Datensatz-Objekt zuzuweisen, da wir dieses auf zweierlei Arten speichern möchten. Auch nach dem erstmaligen Laden bietet es sich an, den Originaldatensatz zunächst als Objekt zuzuweisen:

facebook_europawahl <- read_csv("data/facebook_europawahl.csv")

bt_parteien <- c("alternativefuerde", "B90DieGruenen", "CDU", "CSU", "FDP", "linkspartei", "SPD")

df_reduziert <- facebook_europawahl %>% 
  filter(party %in% bt_parteien) %>% 
  select(party, timestamp, type, comments_count, shares_count, reactions_count) %>% 
  mutate(total_count = sum(c(comments_count, shares_count, reactions_count), na.rm = TRUE))
  
df_reduziert
## # A tibble: 494 x 7
##    party             timestamp           type  comments_count shares_count reactions_count total_count
##    <chr>             <dttm>              <chr>          <dbl>        <dbl>           <dbl>       <dbl>
##  1 B90DieGruenen     2019-04-28 06:00:01 video             70           28             215      871753
##  2 FDP               2019-04-28 11:49:59 photo             16            9             262      871753
##  3 CDU               2019-04-28 09:12:19 video            239          136             398      871753
##  4 SPD               2019-04-28 13:06:09 photo            180           54             699      871753
##  5 CSU               2019-04-28 08:21:00 photo            174           61             458      871753
##  6 alternativefuerde 2019-04-28 14:55:00 link            1163         1499            3944      871753
##  7 FDP               2019-04-28 06:18:18 photo             47          110             622      871753
##  8 FDP               2019-04-28 14:03:00 video            358           89             463      871753
##  9 FDP               2019-04-28 10:40:57 photo             14           19             226      871753
## 10 CSU               2019-04-28 12:35:00 photo            133           20             330      871753
## # ... with 484 more rows

Anschließend können wir den Datensatz wieder speichern:

write_csv(df_ungrouped, "data/df_reduziert.csv")
saveRDS(df_ungrouped, "data/df_reduziert.rds")

Die Schritte aus Übungsaufgabe 8.3 können wir ebenfalls in eine Pipe verpacken – da wir den Datensatz nicht speichern bzw. weiter mit diesem arbeiten, ist auch keine Objektzuweisung erforderlich:

facebook_europawahl %>% 
  filter(timestamp >= "2019-05-20") %>% 
  group_by(party) %>% 
  summarize(M_comments = mean(comments_count, na.rm = TRUE),
            SD_comments = sd(comments_count, na.rm = TRUE),
            M_shares = mean(shares_count, na.rm = TRUE),
            SD_shares = sd(shares_count, na.rm = TRUE),
            M_reactions = mean(reactions_count, na.rm = TRUE),
            SD_reactions = sd(reactions_count, na.rm = TRUE))
## # A tibble: 14 x 7
##    party                           M_comments SD_comments M_shares SD_shares M_reactions SD_reactions
##    <chr>                                <dbl>       <dbl>    <dbl>     <dbl>       <dbl>        <dbl>
##  1 alternativefuerde                 1210.        1250.     1819.    2491.        4690.        3109. 
##  2 B90DieGruenen                      106.          67.0     211.     244.         740.         682. 
##  3 CDU                                508.         897.      113.     293.         870.        1548. 
##  4 CSU                                182.         148.       58.5     45.3        579.         421. 
##  5 DiePARTEI                           92.5        137.      147.     175.        1874.        1879. 
##  6 FamilienParteiDeutschlands           0.714        1.50     14        9.78        14.9         14.9
##  7 FDP                                106.         108.      111.     107.         802.         564. 
##  8 freie.waehler.bundesvereinigung     10.7         12.3      25.7     20.3         86           33.7
##  9 linkspartei                        161.         100.      217.     176.        1176.        1142. 
## 10 oedp.de                              8.30        11.7      30.3     24.6        141.         108. 
## 11 Piratenpartei                       15.3         25.9      44.2     50.1        163.         213. 
## 12 SPD                                240.         277.      137.     120.         658.         510. 
## 13 tierschutzpartei                    82          110.      288      378.         999.        1193. 
## 14 VoltDeutschland                     27.7         70.8      54.7     88.7        316.         365.

Kapitel 10: Daten umstrukturieren und zusammenfügen

Lösung zur Übungsaufgabe 10.1:

Da wir den Datensatz vom Wide- ins Long-Format transformieren, benötigen wir die Funktion pivot_longer():

facebook_europawahl <- read_csv("data/facebook_europawahl.csv")

facebook_europawahl %>% 
  select(id, party, timestamp, comments_count, shares_count, reactions_count) %>% 
  pivot_longer(c(comments_count, shares_count, reactions_count), names_to = "metric")
## # A tibble: 2,706 x 5
##       id party            timestamp           metric          value
##    <dbl> <chr>            <dttm>              <chr>           <dbl>
##  1     1 oedp.de          2019-04-28 09:00:00 comments_count      0
##  2     1 oedp.de          2019-04-28 09:00:00 shares_count        4
##  3     1 oedp.de          2019-04-28 09:00:00 reactions_count     9
##  4     2 tierschutzpartei 2019-04-28 13:57:00 comments_count     17
##  5     2 tierschutzpartei 2019-04-28 13:57:00 shares_count      130
##  6     2 tierschutzpartei 2019-04-28 13:57:00 reactions_count   395
##  7     3 B90DieGruenen    2019-04-28 06:00:01 comments_count     70
##  8     3 B90DieGruenen    2019-04-28 06:00:01 shares_count       28
##  9     3 B90DieGruenen    2019-04-28 06:00:01 reactions_count   215
## 10     4 FDP              2019-04-28 11:49:59 comments_count     16
## # ... with 2,696 more rows

Lösung zur Übungsaufgabe 10.2:

Wir laden zunächst den zusätzlichen Datensatz:

facebook_codings <- read_csv("data/facebook_codings.csv")

facebook_codings
## # A tibble: 902 x 23
##       id topic100 topic200 topic310 topic320 topic331 topic332 topic330 topic341 topic342 topic343 topic340 topic350 topic360 topic370 topic380 topic391
##    <dbl>    <dbl>    <dbl>    <dbl>    <dbl>    <dbl>    <dbl>    <dbl>    <dbl>    <dbl>    <dbl>    <dbl>    <dbl>    <dbl>    <dbl>    <dbl>    <dbl>
##  1    34        0        1        0        0        1        1        1        0        0        1        1        0        0        0        1        0
##  2    62        1        0        0        0        0        0        1        0        0        0        0        0        0        0        0        0
##  3   122        0        1        0        0        0        0        1        0        0        0        0        0        0        0        0        0
##  4   178        0        0        0        0        0        0        1        0        1        0        0        0        0        0        1        0
##  5   300        1        0        0        0        0        0        1        0        0        0        0        0        0        0        0        0
##  6   303        0        1        0        0        0        0        1        0        0        0        0        0        0        0        0        0
##  7   419        0        1        0        0        0        0        1        0        0        0        0        1        0        0        0        0
##  8   421        1        0        0        0        0        0        1        0        0        0        0        0        0        0        0        0
##  9   429        0        0        0        0        0        0        1        0        0        0        0        0        0        0        0        0
## 10   448        1        0        0        0        0        0        1        0        0        0        0        1        0        0        0        0
## # ... with 892 more rows, and 6 more variables: topic392 <dbl>, topic390 <dbl>, topic400 <dbl>, topic300 <dbl>, topic998 <dbl>, topic999 <dbl>

Wie wir sehen, ist die id-Variable anders sortiert als im Datensatz facebook_europawahl. Wollen wir die Datensätze mittels bind_cols() zusammenfügen, müssten wir facebook_codings vorab mittels arrange() entsprechend facebook_europawahl sortieren.

Sicherer und genauso simpel ist allerdings left_join():

joined_df <- facebook_europawahl %>% 
  left_join(facebook_codings, by = "id")

joined_df
## # A tibble: 902 x 38
##       id URL   party timestamp           type  message link  comments_count shares_count reactions_count like_count love_count wow_count haha_count sad_count
##    <dbl> <chr> <chr> <dttm>              <chr> <chr>   <chr>          <dbl>        <dbl>           <dbl>      <dbl>      <dbl>     <dbl>      <dbl>     <dbl>
##  1     1 http~ oedp~ 2019-04-28 09:00:00 video "Guido~ http~              0            4               9          9          0         0          0         0
##  2     2 http~ tier~ 2019-04-28 13:57:00 photo "Aus u~ http~             17          130             395        354         23         3         11         2
##  3     3 http~ B90D~ 2019-04-28 06:00:01 video "Beim ~ http~             70           28             215        174         14         3         16         0
##  4     4 http~ FDP   2019-04-28 11:49:59 photo "Unser~ http~             16            9             262        254          7         0          1         0
##  5     5 http~ tier~ 2019-04-28 08:24:15 link  "Eine ~ http~              6           46             145        129         14         2          0         0
##  6     6 http~ CDU   2019-04-28 09:12:19 video "Freih~ http~            239          136             398        292          8         0         58         2
##  7     7 http~ SPD   2019-04-28 13:06:09 photo "Katar~ http~            180           54             699        576         34         4         79         1
##  8     8 http~ Pira~ 2019-04-28 17:36:30 video "Unser~ http~              0           NA               7          6          0         1          0         0
##  9     9 http~ DieP~ 2019-04-28 07:44:28 link  "Der a~ http~             35           76             612        509         49         0         54         0
## 10    10 http~ CSU   2019-04-28 08:21:00 photo "#Klar~ http~            174           61             458        334          3         5         90         2
## # ... with 892 more rows, and 23 more variables: angry_count <dbl>, topic100 <dbl>, topic200 <dbl>, topic310 <dbl>, topic320 <dbl>, topic331 <dbl>,
## #   topic332 <dbl>, topic330 <dbl>, topic341 <dbl>, topic342 <dbl>, topic343 <dbl>, topic340 <dbl>, topic350 <dbl>, topic360 <dbl>, topic370 <dbl>,
## #   topic380 <dbl>, topic391 <dbl>, topic392 <dbl>, topic390 <dbl>, topic400 <dbl>, topic300 <dbl>, topic998 <dbl>, topic999 <dbl>

Wir sehen, dass der neue Datensätze weiterhin 902 Zeilen hat, aber nun alle 38 Variablen aus beiden Datensätzen umfasst. Zur Sicherheit sollten wir überprüfen, ob doppelte Werte in der id-Variablen vorkommen, um auszuschließen, dass Fälle bei der Join-Operation verdoppelt wurden. Dafür können wir die distinct()-Funktion nutzen, die nur einzigartige Werte der angegebenen Variablen ausgibt:

joined_df %>% 
  distinct(id)
## # A tibble: 902 x 1
##       id
##    <dbl>
##  1     1
##  2     2
##  3     3
##  4     4
##  5     5
##  6     6
##  7     7
##  8     8
##  9     9
## 10    10
## # ... with 892 more rows

902 einzigartige Werte in id – es wurden also keine Fälle verdoppelt oder sind weggefallen.

Kapitel 11: Daten visualisieren

Für alle Aufgaben benötigen wir das Tidyverse und den Datensatz facebook_europawahl.csv. Zudem filtern wir in diesem nur die im Bundestag vertretenen Parteien an:

library(tidyverse)

bt_parteien <- c("alternativefuerde", "B90DieGruenen", "CDU", "CSU", "FDP", "linkspartei", "SPD")

facebook_europawahl <- read_csv("data/facebook_europawahl.csv") %>% 
  filter(party %in% bt_parteien)

Lösung zur Übungsaufgabe 11.1:

Als Aesthetics weisen wir die Kommentarzahl (comments_count) der x-Achse, die Anzahl an Shares (shares_count) der y-Achse zu (oder andersum). Für Punktediagramme benötigen wie das Geometric geom_point():

facebook_europawahl %>% 
  ggplot(aes(x = comments_count, y = shares_count)) +
  geom_point()
## Warning: Removed 5 rows containing missing values (geom_point).

An der Warnmeldung sehen wir im Übrigen, dass 5 Posts nicht abgebildet werden – hierbei handelt es sich um NA-Werte.

Lösung zur Übungsaufgabe 11.2:

Eine Möglichkeit, sowohl Partei (party) als auch Typ des Posts (type) eine Aesthetic zuzuweisen – z. B. color (Punkt- bzw. Linienfarbe) für die Partei, shape (Punktform) für die den Typ des Beitrags:

facebook_europawahl %>% 
  ggplot(aes(x = comments_count, y = shares_count, color = party, shape = type)) +
  geom_point()

Eine andere Möglichkeit besteht darin, zusätzlich mit Facets zu arbeiten:

facebook_europawahl %>% 
  ggplot(aes(x = comments_count, y = shares_count, color = party)) +
  geom_point() +
  facet_wrap(~type)

Lösung zur Übungsaufgabe 11.3:

Einige Möglichkeiten zur Verbesserung und Verschönerung des Plots:

  • Verwendung eines Themes
  • Achsen-/Skalenbeschriftungen
  • Verwendung der tatsächlichen Parteifarben
  • Gleiche Skalierung von x- und y-Achse (da gleiche zugrundeliegende Einheit); der bisher noch unbekannte Befehl coord_fixed() sorgt dafür, dass Einheiten auf der x- und y-Achse gleich dargestellt werden:
facebook_europawahl %>% 
  ggplot(aes(x = comments_count, y = shares_count, color = party, shape = type)) +
  geom_point() +
  scale_y_log10(name = "Anzahl der Shares", ) +
  scale_x_log10(name = "Anzahl der Kommentare",) +
  scale_color_manual(name = "Partei",
                     values = c("CDU" = "#000000",
                                "CSU" = "#6E6E6E",
                                "SPD" = "#FE2E2E",
                                "alternativefuerde" = "#81BEF7",
                                "FDP" = "#FFFF00",
                                "linkspartei" = "#DF01A5",
                                "B90DieGruenen" = "#01DF01"),
                     labels = c("alternativefuerde" = "AfD", 
                                "linkspartei" = "Linke", 
                                "B90DieGruenen" = "Grüne")) +
  scale_shape_discrete(name = "Art des Beitrags") +
  theme_bw() +
  coord_fixed()

Kapitel 12: Arbeiten mit Textdaten

Lösung zur Übungsaufgabe 12.1:

experiment <- tibble(experimentalgruppe = c("Gruppe A", "Gruppe B", "Gruppe A", "Gruppe C"))

Ziel war es, lediglich die Gruppenkennung in einer neuen Spalte hinzuzufügen. Dafür gibt es viele verschiedene Möglichkeiten, z. B.:

  • str_sub(): Lediglich das letzte Zeichen als Substring auswählen
experiment %>% 
  mutate(gruppe_kurz = str_sub(experimentalgruppe, -1, -1))
## # A tibble: 4 x 2
##   experimentalgruppe gruppe_kurz
##   <chr>              <chr>      
## 1 Gruppe A           A          
## 2 Gruppe B           B          
## 3 Gruppe A           A          
## 4 Gruppe C           C
  • str_replace(): "Gruppe " durch einen leeren String "" ersetzen:
experiment %>% 
  mutate(gruppe_kurz = str_replace(experimentalgruppe, "Gruppe ", ""))
## # A tibble: 4 x 2
##   experimentalgruppe gruppe_kurz
##   <chr>              <chr>      
## 1 Gruppe A           A          
## 2 Gruppe B           B          
## 3 Gruppe A           A          
## 4 Gruppe C           C

Lösung zur Übungsaufgabe 12.2:

imdb_urls <- c(
  "https://www.imdb.com/title/tt6751668/?ref_=hm_fanfav_tt_4_pd_fp1",
  "https://www.imdb.com/title/tt0260991/",
  "www.imdb.com/title/tt7282468/reviews",
  "https://m.imdb.com/title/tt4768776/"
)

Zur Extraktion der IDs bietet sich str_extract() an mit RegEx an. Mit dem RegEx-String "tt\\d{7}" matchen wir jegliche IMDb-ID, die immer dem Schema "tt", gefolgt von 7 Ziffern folgen:

imdb_urls %>% 
  str_extract("tt\\d{7}")
## [1] "tt6751668" "tt0260991" "tt7282468" "tt4768776"

Lösung zur Übungsaufgabe 12.3:

adressen = c(
    "Platz der Republik 1, D-11011 Berlin",
    "Dr.-Karl-Renner-Ring 3, A-1017 Wien",
    "Bundesplatz 3, CH-3005 Bern"
  )

Sinnvoll ist es, nach und nach die einzelnen Adress-Bestandteile auszuwählen.

  • Der Straßenname ist dabei der komplizierteste Part, da er aus Groß- und Kleinbuchstaben, Bindestrichen, Leerzeichen und Punkten bestehen kann. Eine Möglichkeit ist es daher, all diese Zeichen als eigene Übereinstimmungsgruppe zu definieren: [A-Za-z-\\s\\.]+. Da keine Ziffern im Straßennamen vorkommen, können wir das jedoch abkürzen, indem wir für den Straßennamen alles matchen, was keine Ziffer ist: \\D+. Durch Klammern können wir angegeben, dass dies der erste Bestandteil der Adresse ist, den wir extrahieren möchten: "(\\D+)".
  • Es folgt (in diesem Beispiel) stets Whitespace und die Hausnummer, was wir mit \\s\\d+ matchen können. Da wir das Leerzeichen nicht extrahieren möchten, ziehen wir die nächsten Klammern lediglich um das \\d+: "(\\D+)\\s(\\d+)".
  • Es folgen nun ein Komma, Whitespace und ein oder zwei Großbuchstaben für den Ländercode; letztere können wir beispielsweise mit [A-Z]{1,2} matchen. Erneut wollen wir nur die 1-2 Großbuchstaben extrahieren: "(\\D+)\\s(\\d+),\\s([A-Z]{1,2})".
  • Nun kommt ein Bindestrich und die 4-5-stellige Postleitzahl: "(\\D+)\\s(\\d+),\\s([A-Z]{1,2})-(\\d{4,5})".
  • Schließlich folgt noch ein Whitespace und die Stadt, die wir z. B. schnell mittels \\D+ (alles außer Ziffern) matchen können: "(\\D+)\\s(\\d+),\\s([A-Z]{1,2})-(\\d{4,5})\\s(\\D+)"

Diesen String können wir nun str_match() übergeben:

adr_string <- "(\\D+)\\s(\\d+),\\s([A-Z]{1,2})-(\\d{4,5})\\s(\\D+)"

adressen %>% 
  str_match(adr_string)
##      [,1]                                   [,2]                   [,3] [,4] [,5]    [,6]    
## [1,] "Platz der Republik 1, D-11011 Berlin" "Platz der Republik"   "1"  "D"  "11011" "Berlin"
## [2,] "Dr.-Karl-Renner-Ring 3, A-1017 Wien"  "Dr.-Karl-Renner-Ring" "3"  "A"  "1017"  "Wien"  
## [3,] "Bundesplatz 3, CH-3005 Bern"          "Bundesplatz"          "3"  "CH" "3005"  "Bern"

Als Resultat erhalten wir eine Matrix, in der in der ersten Spalten der komplette gematchte String sowie in den folgenden Spalten die einzelnen gematchten Bestandteile, definiert durch runde Klammern () stehen.

Kapitel 15: Web Scraping

Für die Lösungen wird das Package polite in Kombination mit rvest verwendet. Die Extraktion der HTML-Elemente unterscheidet sich jedoch nicht, wenn nur rvest verwendet wird.

library(polite)
library(rvest)

Lösung zur Übungsaufgabe 15.1:

Wir besuchen zunächst den Artikel und finden über SelectorGadget oder die Untersuchen-Funktion heraus, dass

  • der Artikel in der CSS-Klasse .sz-article steht
  • die gesuchten Inhalte in den CSS-Klassen .css-11lvjqt (Datum und Uhrzeit), .css-1keap3i (Kicker), .css-1kuo4az (Überschrift) und .css-1psf6fc (Lead) stehen.

Nun stellen wir uns mit bow() dem Server vor (wenn nur rvest genutzt wird, wird dieser Schritt übersprungen):

url <- "https://www.sueddeutsche.de/sport/hsv-kiel-hecking-ausgleich-1.4931360"
sz1 <- bow(url)

Und scrapen die Seite (analog zu read_html() in rvest):

html_content <- scrape(sz1)

Nun extrahieren wir die gewünschten Elemente:

html_content %>% 
  html_nodes(".css-11lvjqt, .css-1keap3i, .css-1kuo4az, .css-1psf6fc") %>% 
  html_text() %>% 
  str_squish()
## [1] "9. Juni 2020, 9:20 Uhr"                                                                                                                                                                           
## [2] "HSV in der zweiten Liga"                                                                                                                                                                          
## [3] "\"Das sind Dinge, die sehr, sehr weh tun\""                                                                                                                                                       
## [4] "So wird es eng mit dem Aufstieg: In einer wilden Schlussphase kassiert der Hamburger SV gegen Holstein Kiel den Ausgleich in der 93. Minute - die Partie endet 3:3. Trainer Hecking wirkt ratlos."

Lösung zur Übungsaufgabe 15.2:

Ein Problem ist hier, dass viele Links auf der Seite stehen, wir aber nur einen abgreifen möchten. Dies können wir erreichen, indem wir zunächst lediglich den Artikel selbst über die Klasse .sz-article auswählen, dann nur die Textabsätze mit dem HTML-Tag p und schließlich Links mit dem HTML-Tag a:

html_content %>% 
  html_nodes(".sz-article") %>% 
  html_nodes("p") %>% 
  html_nodes("a") %>% 
  html_attr("href")
## character(0)

Lösung zur Übungsaufgabe 15.3:

Wir wandeln den obigen Code in eine Funktion um:

scrape_sz <- function(url) {
  # Vorstellen
  sz <- bow(url)
  
  # Scrapen
  html_content <- scrape(sz)
  
  # Interessierende HTML-Elemente extrahieren
  info <- html_content %>% 
    html_nodes(".css-11lvjqt, .css-1keap3i, .css-1kuo4az, .css-1psf6fc") %>% 
    html_text() %>% 
    str_squish()
  
  # In Tibble umwandeln
  info_tibble <- tibble(
    release = info[1],
    kicker = info[2],
    headline = info[3],
    lead = info[4]
  )

  # Tibble zurückgeben
  return(info_tibble)
}

Und testen dies am zweiten Artikel:

scrape_sz("https://www.sueddeutsche.de/sport/stuttgart-hsv-2-bundesliga-castro-1.4921867")
## # A tibble: 1 x 4
##   release           kicker      headline                      lead                                                                                              
##   <chr>             <chr>       <chr>                         <chr>                                                                                             
## 1 28. Mai 2020, 22~ 2. Bundesl~ Castro schockt den HSV in de~ Der VfB Stuttgart liegt gegen den HSV nach 45 Minuten schon mit 0:2 zurück - doch dann kommen die~

Kapitel 17: Automatisierte Inhaltsanalyse: Einführung und Grundbegriffe

Für alle Aufgaben benötigen wir Quanteda und müssen den Facebook-Datensatz wie gewohnt filtern:

library(tidyverse)
library(quanteda)

bt_parteien <- c("alternativefuerde", "B90DieGruenen", "CDU", "CSU", "FDP", "linkspartei", "SPD")

facebook_europawahl <- read_csv("data/facebook_europawahl.csv") %>% 
  filter(party %in% bt_parteien)

Lösung zur Übungsaufgabe 17.1:

Wir erstellen das Korpus-Objekt mit der corpus()-Funktion.

fb_corpus <- corpus(facebook_europawahl, docid_field = "id", text_field = "message")
## Warning: NA is replaced by empty string

Lösung zur Übungsaufgabe 17.2:

Die einzelnen Schritte zur Tokenisierung können wir in eine Pipe packen:

fb_tokens <- tokens(fb_corpus,                 # Erzeuge Tokens
                       remove_punct = TRUE,  
                       remove_numbers = TRUE, 
                       remove_symbols = TRUE,
                       remove_url = TRUE) %>% 
  tokens_tolower() %>%                         # Kleinschreibung
  tokens_remove(stopwords("german")) %>%       # Deutsche Stoppwörter entfernen
  tokens_ngrams(n = c(1, 2, 3))                # Erzeuge Uni-, Bi- und Trigramme

Lösung zur Übungsaufgabe 17.3:

Zunächst erstellen wir die DFM:

fb_dfm <- dfm(fb_tokens)

Die Top-Features pro Partei:

topfeatures(fb_dfm, groups = "party")
## $alternativefuerde
##         afd     unseren        dass deutschland      finden  europawahl  kandidaten         mai        mehr       schon 
##          85          53          48          37          37          33          32          31          30          30 
## 
## $B90DieGruenen
##      europa      wählen         mai        grün   gruene.de       teile       innen klimaschutz       heute      freund 
##          24          23          18          18          17          17          16          14          14          14 
## 
## $CDU
##               #unsereuropa                  #26maicdu                     europa                        cdu                      heute 
##                         46                         38                         23                         21                         17 
##   <U+0001F1EA><U+0001F1FA>                   annegret          kramp-karrenbauer annegret_kramp-karrenbauer                 sicherheit 
##                         16                         16                         16                         16                         14 
## 
## $CSU
##       manfred         weber manfred_weber        europa    europawahl        markus         söder  markus_söder        bayern         heute 
##            41            41            41            35            33            24            20            20            20            19 
## 
## $FDP
##                         #chancennutzen                                 europa               <U+0001F1EA><U+0001F1FA>                                #ep2019 
##                                     75                                     59                                     52                                     51 
##                 #chancennutzen_#ep2019                        #europawahl2019                #ep2019_#europawahl2019 #chancennutzen_#ep2019_#europawahl2019 
##                                     48                                     36                                     32                                     32 
##                                   dass        europa_<U+0001F1EA><U+0001F1FA> 
##                                     24                                     22 
## 
## $linkspartei
##            europa             heute            martin        schirdewan martin_schirdewan          menschen             bernd         riexinger 
##                19                11                 8                 8                 8                 7                 7                 7 
##   bernd_riexinger             özlem 
##                 7                 7 
## 
## $SPD
##               europa #europaistdieantwort                 mehr             katarina             soziales               barley      katarina_barley 
##                   33                   24                   15                   13                   13                   12                   12 
##                dafür      soziales_europa                 geht 
##                   11                   10                    9

Das sich darunter fast keine Trigramme (außer eine Hashtag-Kominbation bei der FDP) befinden, ziehen wir daraus vorerst keinen Mehrwert. Die Zeichenkette \U0001f1ea\U0001f1fa verweist auf Fehler bei der Bereinigung der Texte – hierbei handelt es sich um den Code für das Europaflaggen-Emoji, der eigentlich durch remove_symbols = TRUE hätte entfernt werden sollen. Auch einige URL-Bestandteile wurden nicht korrekt entfernt, ebenso gibt es ein paar falsche Worttrennungen, z. B. wenn ein Gendersternchen enthalten war. Hier sollte also manuell nachgebessert werden.

Um die Hashtags zu analysieren, erstellen wir eine DFM, die nur diese enthält:

dfm_hashtags <- dfm_select(fb_dfm, "#*") # Wählt alle Hashtags aus

Wir können uns nun wieder mittels topfeatures() die häufigsten Hashtags ausgeben lassen:

topfeatures(dfm_hashtags) # allgemein
##                         #chancennutzen                                #ep2019                 #chancennutzen_#ep2019                           #unsereuropa 
##                                     75                                     51                                     48                                     47 
##                              #26maicdu                        #europawahl2019                #ep2019_#europawahl2019 #chancennutzen_#ep2019_#europawahl2019 
##                                     38                                     37                                     32                                     32 
##                   #europaistdieantwort                                #europa 
##                                     24                                     23
topfeatures(dfm_hashtags, groups = "party") # getrennt nach Partei
## $alternativefuerde
##                         #greding                  #greding_bayern            #greding_bayern_heute                     #grundrechte 
##                                1                                1                                1                                0 
##                   #chancennutzen                           #bpt19            #chancennutzen_#bpt19                     #unsereuropa 
##                                0                                0                                0                                0 
##               #unsereuropa_steht #unsereuropa_steht_freiheitliche 
##                                0                                0 
## 
## $B90DieGruenen
##                           #europawahl                               #europa                          #klimaschutz                         #zusammenhalt 
##                                    11                                    10                                     3                                     3 
##                       #europawahl_mai                 #europa_einzigartiges #europa_einzigartiges_friedensprojekt            #klimaschutz_#zusammenhalt 
##                                     3                                     3                                     3                                     2 
##                            #briefwahl                     #europawahl_teile 
##                                     2                                     1 
## 
## $CDU
##              #unsereuropa                 #26maicdu    #unsereuropa_#26maicdu #unsereuropa_<U+0001F1EA><U+0001F1FA> #unsereuropa_<U+0001F1EA><U+0001F1FA>_#26maicdu                   #europa 
##                        46                        38                        11                        10                         8                         7 
##             #thepowerofwe                      #cdu    #26maicdu_#unsereuropa                  #frieden 
##                         6                         5                         4                         3 
## 
## $CSU
##                           #klartext                   #klartext_unseres #klartext_unseres_spitzenkandidaten                   #klartext_bayerns 
##                                  16                                   8                                   8                                   6 
## #klartext_bayerns_ministerpräsident                          #csutvtipp                          #wahlarena                       #miasanbayern 
##                                   6                                   4                                   4                                   3 
##                        #unsereuropa                            #tvduell 
##                                   1                                   1 
## 
## $FDP
##                         #chancennutzen                                #ep2019                 #chancennutzen_#ep2019                        #europawahl2019 
##                                     75                                     51                                     48                                     36 
##                #ep2019_#europawahl2019 #chancennutzen_#ep2019_#europawahl2019                                  #live                                 #bpt19 
##                                     32                                     32                                      6                                      4 
##                           #reneweurope                            #teameurope 
##                                      4                                      3 
## 
## $linkspartei
##                 #1europafueralle                     #grundrechte                   #chancennutzen                           #bpt19 
##                                1                                0                                0                                0 
##            #chancennutzen_#bpt19                     #unsereuropa               #unsereuropa_steht #unsereuropa_steht_freiheitliche 
##                                0                                0                                0                                0 
##                          #ep2019                  #europawahl2019 
##                                0                                0 
## 
## $SPD
##      #europaistdieantwort                   #europa              #grundgesetz            #sozialklimbim #europa_<U+0001F1EA><U+0001F1FA>                     #rezo 
##                        24                         6                         3                         2                         2                         2 
##                      #evp                #evp_sagen            #europa_europa #europaistdieantwort_mehr 
##                         1                         1                         1                         1

Kapitel 18: Textdeskription und einfache Textvergleiche

Auch hier benötigen wir Quanteda und müssen den Facebook-Datensatz wie gewohnt filtern. Zudem laden wir auch Tidytext:

library(tidyverse)
library(tidytext)
library(quanteda)

bt_parteien <- c("alternativefuerde", "B90DieGruenen", "CDU", "CSU", "FDP", "linkspartei", "SPD")

facebook_europawahl <- read_csv("data/facebook_europawahl.csv") %>% 
  filter(party %in% bt_parteien)

Lösung zur Übungsaufgabe 18.1:

Wir erzeugen zunächst wie gehabt Korpus-, Tokens- und DFM-Objekte:

fb_corpus <- corpus(facebook_europawahl, docid_field = "id", text_field = "message")
## Warning: NA is replaced by empty string
fb_tokens <- tokens(fb_corpus, 
                    remove_punct = TRUE,  
                    remove_numbers = TRUE, 
                    remove_symbols = TRUE,
                    remove_url = TRUE) %>% 
  tokens_tolower() %>% 
  tokens_remove(stopwords("german"))

fb_dfm <- dfm(fb_tokens)

Zunächst ein Blick auf die einfachen Worthäufigkeiten:

featfreq(fb_dfm) %>% 
  tidy() %>%      
  arrange(desc(x))
## # A tibble: 6,896 x 2
##    names                      x
##    <chr>                  <dbl>
##  1 "europa"                 208
##  2 "dass"                   101
##  3 "europawahl"              96
##  4 "mehr"                    92
##  5 "heute"                   92
##  6 "afd"                     90
##  7 "\U0001f1ea\U0001f1fa"    87
##  8 "wählen"                  79
##  9 "#chancennutzen"          75
## 10 "unseren"                 72
## # ... with 6,886 more rows

Wenig überraschend fallen Begriffe wie “Europa”, “Deutschland” und “Europawahl” sehr häufig. Es zeigen sich aber auch bereits ein paar Probleme, die wir bereits von der vorherigen Übung kennen, z. B. dass Stoppwörter wie “dass” weiterhin im Datensatz verbleiben. Auch das Europaflaggen-Emoji wird offenbar sehr häufig verwendet.

Wir können uns die wichtigsten Begriffe auch als Wordcloud anzeigen lassen:

textplot_wordcloud(fb_dfm, max_words = 100)

Kollokationen verweisen vor allem auf die Namen der Kandidat*innen sowie beliebte Hashtag-Kombinationen:

textstat_collocations(fb_tokens) %>% 
  as_tibble() %>% 
  arrange(desc(count))
## # A tibble: 1,007 x 6
##    collocation                   count count_nested length lambda     z
##    <chr>                         <int>        <int>  <dbl>  <dbl> <dbl>
##  1 "manfred weber"                  55            0      2  13.2   8.68
##  2 "#chancennutzen #ep2019"         48            0      2   9.60 15.8 
##  3 "#ep2019 #europawahl2019"        32            0      2   9.47 16.1 
##  4 "unseren kandidaten"             30            0      2   8.13 13.9 
##  5 "europa \U0001f1ea\U0001f1fa"    27            0      2   3.77 15.5 
##  6 "finden infos"                   27            0      2   8.97 14.5 
##  7 "infos europawahl"               26            0      2   7.54 13.0 
##  8 "europawahl unseren"             24            0      2   4.75 17.3 
##  9 "markus söder"                   23            0      2  10.8  11.4 
## 10 "europawahl manfred"             17            0      2   4.58 14.7 
## # ... with 997 more rows

Kookkurenzen zeigen, dass wohl vor allem die AfD thematisiert wurde (ob durch sich selbst oder andere Parteien, ergeht hieraus nicht). Auch hier sehen wir, dass noch URL-Bestandteile ("https") im Datensatz verbleiben; diese sollten also noch manuell gefiltert werden.

fcm(fb_tokens) %>% 
  tidy() %>% 
  arrange(desc(count))
## # A tibble: 472,434 x 3
##    document               term       count
##    <chr>                  <chr>      <dbl>
##  1 "afd"                  afd          109
##  2 "europa"               europa        96
##  3 "afd"                  unseren       85
##  4 "\U0001f1ea\U0001f1fa" europa        70
##  5 "unseren"              kandidaten    60
##  6 "afd"                  finden        59
##  7 "afd"                  europawahl    59
##  8 "#chancennutzen"       europa        58
##  9 "ab"                   afd           58
## 10 "manfred"              weber         58
## # ... with 472,424 more rows

Für Keyness-Analysen benötigen wir zunächst eine nach Parteien gruppierte DFM:

fb_dfm_grouped <- dfm(fb_tokens, groups = "party")

Wir können uns nun die Keywords je Partei ausgeben lassen – im Beispiel für SPD und Grüne:

textstat_keyness(fb_dfm_grouped, target = "SPD") %>% 
  as_tibble()
## # A tibble: 6,896 x 5
##    feature               chi2        p n_target n_reference
##    <chr>                <dbl>    <dbl>    <dbl>       <dbl>
##  1 #europaistdieantwort 385.  0.             24           0
##  2 katarina             171.  0.             13           2
##  3 barley               143.  0.             12           3
##  4 soziales             130.  0.             13           6
##  5 bullmann             100.  0.              7           0
##  6 udo                  100.  0.              7           0
##  7 andrea                83.7 0.              6           0
##  8 nahles                70.2 0.              6           1
##  9 sozialen              53.3 2.90e-13        7           5
## 10 europa                41.6 1.15e-10       33         175
## # ... with 6,886 more rows
textstat_keyness(fb_dfm_grouped, target = "B90DieGruenen") %>% 
  as_tibble()
## # A tibble: 6,896 x 5
##    feature       chi2     p n_target n_reference
##    <chr>        <dbl> <dbl>    <dbl>       <dbl>
##  1 grün         219.      0       18           0
##  2 gruene.de    206.      0       17           0
##  3 freund       167.      0       14           0
##  4 innen        158.      0       16           3
##  5 zusammenhalt 154.      0       14           1
##  6 teile        152.      0       17           5
##  7 ska          116.      0       10           0
##  8 #europawahl   88.8     0       11           4
##  9 robert        81.0     0        9           2
## 10 habeck        78.3     0        8           1
## # ... with 6,886 more rows

Neben wenigen inhaltlichen Begriffe ("soziales", "zusammenhalt") werden die Listen dominiert durch Eigennamen; um tatsächliche inhaltliche Keywords zu bestimmen, würde es sich daher lohnen, Namen von Kandidat*innen, Parteien etc. aus der DFM zu löschen.

Kapitel 19: Diktionärbasierte Ansätze

Erneut hier benötigen wir Quanteda und müssen den Facebook-Datensatz wie gewohnt filtern. Zudem laden wir auch Tidytext:

library(tidyverse)
library(tidytext)
library(quanteda)

bt_parteien <- c("alternativefuerde", "B90DieGruenen", "CDU", "CSU", "FDP", "linkspartei", "SPD")

facebook_europawahl <- read_csv("data/facebook_europawahl.csv") %>% 
  filter(party %in% bt_parteien)

Wir laden außerdem, wie in der Aufgabenstellung angegeben, die Dictionaries:

sentiws_pos <- read_delim("data/SentiWS_v2.0_Positive.txt", col_names = c("word", "value", "flections"), delim = "\t") %>% 
  mutate(sentiment = "positive")
sentiws_neg <- read_delim("data/SentiWS_v2.0_Negative.txt", col_names = c("word", "value", "flections"), delim = "\t") %>% 
  mutate(sentiment = "negative")

sentiws <- sentiws_pos %>% 
  bind_rows(sentiws_neg) %>% 
  separate(word, c("word", "type"), sep = "\\|") %>% 
  mutate(word = str_c(word, flections, sep = ",")) %>% 
  select(-flections, -type) %>% 
  separate_rows(word, sep = ",") %>% 
  na.omit()

Außerdem erzeugen wir einen Korpus der Facebook-Posts:

fb_corpus <- corpus(facebook_europawahl, docid_field = "id", text_field = "message")
## Warning: NA is replaced by empty string

Lösung zur Übungsaufgabe 19.1:

Wir extrahieren zunächst die positiven und negativen Begriffe als Vektoren mittels filter() und pull():

senti_pos <- sentiws %>% 
  filter(sentiment == "positive") %>% 
  pull(word, sentiment)

senti_neg <- sentiws %>% 
  filter(sentiment == "negative") %>% 
  pull(word, sentiment)

Mit beiden Vektoren können wir nun ein Quanteda-Dictionary mittels dictionary() erstellen. dictionary() konvertiert automatisch in Kleinschreibung – soll dies nicht geschehen, kann das mit dem Argument tolower = FALSE angepasst werden. Für unsere Zwecke ist eine Konvertierung in Kleinschreibung aber sinnvoll, da wir auch für die DFM in der Regel alle Wörter in Kleinschreibung umwandeln.

sentiment_dictionary <- dictionary(list(
  positiv = senti_pos,
  negativ = senti_neg
))

sentiment_dictionary
## Dictionary object with 2 key entries.
## - [positiv]:
##   - abmachung, abmachungen, abschluß, abschluss, abschlusse, abschlusses, abschlüsse, abschlüssen, abstimmung, abstimmungen, aktivität, aktivitäten, aktualisierung, aktualisierungen, aktualität, aktualitäten, akzeptanz, akzeptanzen, andrang, andrangs [ ... and 16,311 more ]
## - [negativ]:
##   - abbau, abbaus, abbaues, abbauen, abbaue, abbauten, abbruch, abbruches, abbrüche, abbruchs, abbrüchen, abbruche, abdankung, abdankungen, abdämpfung, abdämpfungen, abfall, abfalles, abfälle, abfalls [ ... and 17,846 more ]

Um die absoluten Häufigkeiten auszuzählen, genügt der dfm()-Befehl mit Angabe unseres Dictionaries und einer Gruppierung nach party:

dfm(fb_corpus, dictionary = sentiment_dictionary, groups = "party")
## Document-feature matrix of: 7 documents, 2 features (0.0% sparse) and 1 docvar.
##                    features
## docs                positiv negativ
##   alternativefuerde     637     467
##   B90DieGruenen         119      26
##   CDU                   224      28
##   CSU                   197      47
##   FDP                   444      90
##   linkspartei            88      42
## [ reached max_ndoc ... 1 more document ]

Das Verhältnis von positivem zu negativem Sentiment erhalten wir durch anschließende Gewichtung mit dfm_weight():

dfm(fb_corpus, dictionary = sentiment_dictionary, groups = "party") %>% 
  dfm_weight(scheme = "prop")
## Document-feature matrix of: 7 documents, 2 features (0.0% sparse) and 1 docvar.
##                    features
## docs                  positiv   negativ
##   alternativefuerde 0.5769928 0.4230072
##   B90DieGruenen     0.8206897 0.1793103
##   CDU               0.8888889 0.1111111
##   CSU               0.8073770 0.1926230
##   FDP               0.8314607 0.1685393
##   linkspartei       0.6769231 0.3230769
## [ reached max_ndoc ... 1 more document ]

Die AfD verzeichnet also den größten Anteil negativen Sentiments, gefolgt von der Linkspartei. Alle anderen Parteien kommunizieren sehr positiv.

Interessieren wir uns für den Anteil, den positive und negative Begriffe am Gesamt-Wortschatz der Parteien-Posts ausmachen, gewichten wir die DFM vor der Anwendung des Dictionaries:

dfm(fb_corpus, groups = "party") %>% 
  dfm_weight(scheme = "prop") %>% 
  dfm(dictionary = sentiment_dictionary)
## Document-feature matrix of: 7 documents, 2 features (0.0% sparse) and 1 docvar.
##                    features
## docs                   positiv    negativ
##   alternativefuerde 0.03748571 0.02828571
##   B90DieGruenen     0.04621212 0.01060606
##   CDU               0.08509848 0.01412114
##   CSU               0.06289111 0.01752190
##   FDP               0.07307567 0.01607665
##   linkspartei       0.04972973 0.02540541
## [ reached max_ndoc ... 1 more document ]

Lösung zur Übungsaufgabe 19.2:

Um den SentiWS als gewichtetes Lexikon zu nutzen und die Polaritätswerte zu berücksichtigen, benutzen wir Tidytext. Zunächst Tokenisieren wir unseren Datensatz. Auch dabei wird direkt in Kleinschreibung konvertiert:

tidy_facebook <- facebook_europawahl %>% 
  unnest_tokens(word, message) %>% 
  select(id, party, word)

tidy_facebook
## # A tibble: 32,031 x 3
##       id party         word       
##    <dbl> <chr>         <chr>      
##  1     3 B90DieGruenen beim       
##  2     3 B90DieGruenen wahlkampf  
##  3     3 B90DieGruenen camp       
##  4     3 B90DieGruenen in         
##  5     3 B90DieGruenen berlin     
##  6     3 B90DieGruenen waren      
##  7     3 B90DieGruenen gestern    
##  8     3 B90DieGruenen hunderte   
##  9     3 B90DieGruenen freiwillige
## 10     3 B90DieGruenen die        
## # ... with 32,021 more rows

Per inner_join() können wir nun die Sentimentwerte anfügen:

tidy_sentiments <- tidy_facebook %>% 
  inner_join(sentiws)
## Joining, by = "word"
tidy_sentiments
## # A tibble: 1,969 x 5
##       id party         word           value sentiment
##    <dbl> <chr>         <chr>          <dbl> <chr>    
##  1     3 B90DieGruenen freiwillige   0.004  positive 
##  2     3 B90DieGruenen mobilisieren  0.004  positive 
##  3     3 B90DieGruenen freiwilligen  0.004  positive 
##  4     3 B90DieGruenen freiwillige   0.004  positive 
##  5     3 B90DieGruenen gewinnen      0.004  positive 
##  6     4 FDP           neuer         0.004  positive 
##  7     6 CDU           gute          0.372  positive 
##  8     7 SPD           gut           0.372  positive 
##  9     7 SPD           zusammenhält  0.0834 positive 
## 10    10 CSU           langsam      -0.0167 negative 
## # ... with 1,959 more rows

Schließlich gruppieren wir per group_by() nach party und berechnen den Mittelwert des Sentiments:

tidy_sentiments %>% 
  group_by(party) %>% 
  summarise(mean_sentiment = mean(value), .groups = "drop")
## # A tibble: 7 x 2
##   party             mean_sentiment
##   <chr>                      <dbl>
## 1 alternativefuerde        -0.0277
## 2 B90DieGruenen             0.0530
## 3 CDU                       0.0643
## 4 CSU                       0.0125
## 5 FDP                       0.0341
## 6 linkspartei               0.0425
## 7 SPD                       0.0925

Auch hier weist die AfD das negativste Sentiment auf. Allerdings erscheint die Kommunikation der Linkspartei auf diesem Wege deulich positiver als zuvor.

Kapitel 21: Topic Modeling

Wie in der Aufgabenstellung geschrieben, laden wir die Daten dieses Mal aus dem quanteda.corpora-Package. Wir laden außerdem die bereits bekannten Packages zum tidyverse zum Datenhandling, quanteda zur Vorbereitung der Textdaten, sowie stm für das Topic Modeling.

library(tidyverse)
library(stm)
library(quanteda)

guardian_corpus <- quanteda.corpora::download("data_corpus_guardian")

guardian_corpus

Lösung zur Übungsaufgabe 21.1:

Die Preprocessing-Schritte führen wir wie gehabt mit Quanteda durch. Wir erzeugen zunächst eine DFM mit dfm() und können dabei irrelevante Token wie Satzzeichen, Zahlen, Symbole und Stoppwörter entfernen. Um die Berechnung zu erleichtern, trimmen wir die DFM zusätzlich um besonders seltene und häufige Wörter mit dfm_trim() (ich habe mich hier für alle Wörter die in mehr als 50% der Artikel sowie in weniger als 2% der Artikel vorkommen entschieden). Schließlich muss die DFM noch mit convert() in ein Format konvertiert werden, das das stm-Package erwartet.

# DFM Erzeugen
guardian_dfm <- dfm(guardian_corpus, 
                    remove_punct = TRUE, 
                    remove_numbers = TRUE, 
                    remove_symbols = TRUE,
                    remove_url = TRUE,
                    remove = stopwords("english"))


# Trimmen
trimmed_dfm <- dfm_trim(guardian_dfm,
                        min_docfreq = 0.02, 
                        max_docfreq = 0.50,
                        docfreq_type = "prop")


# Konvertieren
stm_dfm <- convert(trimmed_dfm, to = "stm")

Nun berechnen wir das Modell mit K = 20 Themen. Da die Berechnung eine Zeit dauern kann, ist es sinnvoll, das Modellobjekt im Anschluss abzuspeichern.

guardian_model <- stm(stm_dfm$documents, stm_dfm$vocab, K = 20)
saveRDS(guardian_model, "data/stm_guardian_model.rds")

Anschließend können wir uns eine Übersicht der wichtigsten Wörter je Thema mittels labelTopics() ausgeben lassen:

labelTopics(guardian_model)
## Topic 1 Top Words:
##       Highest Prob: year, game, friday, music, team, best, christmas 
##       FREX: music, game, christmas, stores, football, tv, store 
##       Lift: sorry, music, football, players, game, christmas, stores 
##       Score: sorry, game, stores, music, store, players, retailers 
## Topic 2 Top Words:
##       Highest Prob: party, labour, vote, election, leader, mps, cameron 
##       FREX: corbyn, labour, mps, party, tory, mp, conservative 
##       Lift: corbyn, electorate, ukip, tory, tories, miliband, lib 
##       Score: electorate, labour, corbyn, party, vote, election, ukip 
## Topic 3 Top Words:
##       Highest Prob: growth, year, market, economy, prices, markets, uk 
##       FREX: prices, growth, markets, economy, quarter, market, inflation 
##       Lift: ftse, forecasts, output, pound, monetary, economists, recession 
##       Score: ftse, growth, prices, eurozone, markets, inflation, oil 
## Topic 4 Top Words:
##       Highest Prob: us, security, military, syria, war, foreign, attacks 
##       FREX: syria, military, isis, russian, de, syrian, islamic 
##       Lift: de, afghanistan, syria, fighters, russian, troops, isis 
##       Score: de, syria, isis, syrian, military, russian, islamic 
## Topic 5 Top Words:
##       Highest Prob: climate, energy, countries, change, world, global, china 
##       FREX: climate, energy, gas, emissions, environmental, china, development 
##       Lift: climate, india, environmental, carbon, emissions, energy, gas 
##       Score: india, climate, emissions, energy, china, carbon, countries 
## Topic 6 Top Words:
##       Highest Prob: government, minister, australia, monday, australian, bill, law 
##       FREX: monday, australian, australia, labor, bill, legislation, federal 
##       Lift: monday, australians, australian, malcolm, labor, australia, abbott 
##       Score: monday, australian, labor, australia, minister, senate, abbott 
## Topic 7 Top Words:
##       Highest Prob: health, better, care, nhs, services, staff, doctors 
##       FREX: better, health, nhs, doctors, care, patients, mental 
##       Lift: patients, better, doctors, cancer, health, nhs, mental 
##       Score: better, nhs, health, patients, doctors, care, mental 
## Topic 8 Top Words:
##       Highest Prob: says, can, london, get, money, now, years 
##       FREX: games, buy, space, london, products, says, small 
##       Lift: games, design, space, waste, product, products, buy 
##       Score: games, says, products, london, business, buy, product 
## Topic 9 Top Words:
##       Highest Prob: city, many, country, video, south, rights, years 
##       FREX: video, church, city, park, gay, religious, protests 
##       Lift: video, protests, church, religious, gay, protesters, park 
##       Score: video, muslim, church, protesters, religious, gay, city 
## Topic 10 Top Words:
##       Highest Prob: like, think, just, can, us, even, going 
##       FREX: really, think, thing, things, like, something, feel 
##       Lift: watching, film, stuff, book, feels, thinking, books 
##       Score: watching, film, like, think, really, story, bbc 
## Topic 11 Top Words:
##       Highest Prob: children, school, food, water, local, child, education 
##       FREX: children, water, school, food, students, schools, girls 
##       Lift: storm, girls, students, children, water, schools, school 
##       Score: storm, children, water, school, food, girls, schools 
## Topic 12 Top Words:
##       Highest Prob: block-time, published-time, says, updated-timeupdated, photograph, today, morning 
##       FREX: block-time, published-time, updated-timeupdated, photograph, pic.twitter.com, today's, morning 
##       Lift: block-time, updated-timeupdated, published-time, today's, pic.twitter.com, photograph, getty 
##       Score: block-time, published-time, today's, updated-timeupdated, pic.twitter.com, says, photograph 
## Topic 13 Top Words:
##       Highest Prob: eu, uk, european, europe, britain, cameron, british 
##       FREX: eu, europe, european, refugees, britain, brexit, migrants 
##       Lift: cars, eu, refugees, migrants, migration, europe, italy 
##       Score: cars, eu, european, brexit, refugees, greece, referendum 
## Topic 14 Top Words:
##       Highest Prob: information, data, online, media, users, using, internet 
##       FREX: internet, users, google, apple, information, facebook, app 
##       Lift: weekly, user, google, privacy, app, users, internet 
##       Score: weekly, google, users, app, apple, data, facebook 
## Topic 15 Top Words:
##       Highest Prob: told, family, two, back, home, time, man 
##       FREX: late, son, man, father, died, friends, went 
##       Lift: late, son, sister, daughter, brother, father, bus 
##       Score: late, family, died, mother, daughter, man, son 
## Topic 16 Top Words:
##       Highest Prob: tax, government, pay, budget, cuts, spending, workers 
##       FREX: tax, cuts, housing, budget, income, osborne, spending 
##       Lift: innovation, tax, councils, income, osborne's, budget, cuts 
##       Score: innovation, tax, osborne, budget, cuts, income, housing 
## Topic 17 Top Words:
##       Highest Prob: trump, clinton, republican, sanders, campaign, obama, donald 
##       FREX: clinton, republican, sanders, trump, cruz, hillary, trump's 
##       Lift: bernie, hillary, republican, sanders, trump's, clinton, cruz 
##       Score: bernie, trump, clinton, sanders, cruz, republican, hillary 
## Topic 18 Top Words:
##       Highest Prob: police, court, officers, case, investigation, told, evidence 
##       FREX: police, officers, arrested, prison, investigation, trial, criminal 
##       Lift: custody, suspect, jury, prosecution, prosecutors, officers, police 
##       Score: suspect, police, officers, court, arrested, investigation, prison 
## Topic 19 Top Words:
##       Highest Prob: company, bank, business, companies, executive, chief, financial 
##       FREX: company, bank, banks, banking, executive, shares, shareholders 
##       Lift: investments, executives, shareholders, directors, banking, company's, profit 
##       Score: investments, bank, shareholders, company, shares, customers, companies 
## Topic 20 Top Words:
##       Highest Prob: women, report, found, research, year, number, years 
##       FREX: women, study, research, drug, drugs, report, female 
##       Lift: equivalent, researchers, gender, study, drugs, drug, women 
##       Score: equivalent, women, drug, report, drugs, violence, research