Wir möchten das twitterverhalten der Mitglieder des Europäischen Parlaments (MEPs) untersuchen und wollen dabei von Beginn an R verwenden. Wir wollen einen möglichst reproduzierbaren Datensatz, welcher auf die Daten des aktuellen Europaparlaments zurückgreift. Wir nutzen dafür Techniken des Web-Scrapings und greifen auf das Packet rvest zu. Außerdem nutzen wir das Paket rtweet um später die Tweets von den jeweiligen Abgeordneten zugreifen zu können. Um unsere Anfragen etwas zu beschleunigen, nutzen wir furrr – mit diesem können wir Funktionen parallel ausführen. Zuletzt nutzen wir auch die Packete des tidyverse um unsere Daten zu bearbeiten und zu visualisieren.

library(tidyverse)
library(rvest)
library(rtweet)
library(furrr)
library(rlist)
plan(multiprocess)

Twitteraccounts der MEPs

Auf der Seite des Europaparlaments findet man nicht nur eine aktuelle Liste aller Abgeordneten, es finden sich auch viele Details auf den Seiten der individuellen MEPs. So findet sich bei Asim Ademov auch dessen Link auf sein Twitterprofil.

Profil des Abgeordneten Asim Ademov

Mit rvest können wir uns zunächst die Links zu den Profilen aller MEPs besorgen.

mep_website <- read_html("http://www.europarl.europa.eu/meps/en/full-list.html?filter=all&leg=") %>%
  html_nodes(".mep_name") %>%
  html_nodes("a") %>%
  html_attr("href") %>%
  paste0("http://www.europarl.europa.eu", .)

Wir waren erfolgreich und konnten die Links auf die Detailseiten der 751 Abgeordneten abgreifen. Um zu bestimmen, welche html_nodes einer Webseite abgegriffen werden sollen, nutze ich SelectorGadget. Dieses Tool habe ich als Addon in Chrome installiert und kann somit die Nodes sehr einfach identifizieren. Eine kurze Videoanleitung findet sich auf der Webseite.

Nachdem wir die Links auf die Profile der Abgeordneten haben, können wir nun die Links auf die Twitterprofile extrahieren. Wir greifen noch ein paar weitere Daten ab, etwa die Fraktions- und Parteizugehörigkeit und das Land des Abgeordneten. Hierzu basteln wir uns eine kleine Funktion, welche wir später mit furrr aufrufen werden.

mep_data_df <- function(x){
  details <- read_html(x)
  
  id <- x %>% str_extract("-?\\d+")
  name <- details %>% html_nodes(".mep_name") %>% html_text(trim = TRUE)
  party <- details %>% html_nodes(".name_pol_group") %>% html_text(trim = TRUE)
  group_short <- details %>% html_nodes(".group") %>% html_attr("class") %>% str_remove("group ")
  country <- details %>% html_nodes(".noflag") %>% html_text() %>% str_extract("\\w+")
  twitter_link <- details %>% html_nodes(".link_collection_noborder .link_twitt") %>% html_attr("href") %>% str_trim() %>% list()
  
  data_frame(id, name, party, group_short, country, twitter_link)
}
mep_details <- future_map_dfr(mep_website, ~mep_data_df(.), .progress = TRUE)

Wir haben insgesamt 517 Abgeordnete mit einem Twitter-Profil. Dies ist ein bisschen weniger als in anderen Liste, beispielsweise der offiziellen Liste der twitternden MEPs des EP-Accounts auf Twitter. Wir müssten überprüfen, wie aktuell die Liste ist und ob ausgeschiedene MEPs noch in dieser Liste zu finden sind.

Für das weitere Vorgehen nutzen wir die von uns erstellte Liste. Wir kontrollieren schon mal, welche Fraktion denn mit den meisten Twitterprofilen aktiv ist.

mep_details %>%
  unnest() %>%
  group_by(group_short) %>%
  summarise(twitter_accounts = n()) %>%
  ggplot(aes(x = reorder(group_short, -twitter_accounts), y = twitter_accounts, fill = group_short)) +
  geom_col() +
  geom_text(aes(label = twitter_accounts), vjust = -0.5) + 
  theme_minimal() +
  labs(title = "Twitter Accounts by European Parliament Group", 
       subtitle = paste0("Total Number of Accounts on Twitter: ", nrow(mep_details %>% unnest() %>% distinct())),
       x = "Group",
       y = "Twitter Accounts") +
  guides(fill=FALSE)

Wenig überraschend sind die besonders großen Fraktionen mit mehr Twitteraccounts vertreten. Es ist allerdings überraschend, dass sich bei der überwiegend rechtsextremen ENF noch wenig Twitteraccounts als bei den Fraktionslosen (NI) finden.

Twitter-Daten

Nachdem wir nun alle Twitter-Accounts der Abgeordneten haben, könnten wir nun alle Tweets abgreifen. Bei weniger als 52 Accounts wäre dies auch sehr schnell erledigt – wollen wir aber mehr als 52 Accounts überprüfen, zwingt uns Twitter dazu, eine 15-minütige Pause einzulegen. Dazu später mehr. Wir stoßen nämich auf eine weitere Problematik: Die Abgeordneten haben die Links auf ihre jeweiligen Twitteraccounts nämlich nicht gleich abgespeichert. So finden sich bei manchen Abgeordneten auch noch weitere Argumente im Link (bspw. https://twitter.com/trebesiusmdep?lang=de) oder nur der Twitteraccount als “Link” (@carolinapunset) – und wir haben noch ganz kreative Ausfälle (https://twitter @wajid4europe).

Wir können dieses Problem mit Regular Expressions lösen, wir ziehen uns nämlich alles nach der domain “twitter.com/”.

mep_details <- mep_details %>%
  unnest() %>%
  mutate(screen_name = str_remove(twitter_link, "\\?lang=[a-z]{2}")) %>% # removes the "?lang=es" at the end of the link
  mutate(screen_name = str_remove(screen_name, "\\/media")) %>% # removes the media at the end of some links
  mutate(screen_name = str_remove(screen_name, "\\/$")) %>% # removes slashes at the end
  mutate(screen_name = str_extract(screen_name, "\\w+$")) %>% # extracts the last word before the end of the string
  mutate(screen_name = str_to_lower(screen_name))

Leider müssen wir aber feststellen, dass manche Accounts doppelt vorkommen.

mep_details %>%
  group_by(screen_name) %>% 
  filter(n() > 1) %>%
  select(name, group_short, screen_name)
## # A tibble: 7 x 3
## # Groups:   screen_name [3]
##   name                                 group_short screen_name    
##   <chr>                                <chr>       <chr>          
## 1 BeatrizBECERRA BASTERRECHEA          aldeadle    beatrizbecerrab
## 2 BeatrizBECERRA BASTERRECHEA          aldeadle    beatrizbecerrab
## 3 AgustínDÍAZ DE MERA GARCÍA CONSUEGRA epp         ppegrupo       
## 4 Luisde GRANDES PASCUAL               epp         ppegrupo       
## 5 Francisco JoséMILLÁN MON             epp         ppegrupo       
## 6 CarolinaPUNSET                       aldeadle    carolinapunset 
## 7 CarolinaPUNSET                       aldeadle    carolinapunset

Insgesamt drei Abgeordnete verweise auf das Profil der Fraktion statt auf einen individuellen Account (möglicherweise verweist auch ein Abgeordneter der S&D oder der anderen Fraktionen auf das Fraktionsprofil). Wir filtern die Fraktionsaccounts deshalb raus.

ep_group_accounts <- c("ppegrupo", "eppgroup", "theprogressives", "aldegroup", "guengl", "enf_ep", "efdgroup", "ecrgroup")

mep_details <- mep_details %>%
  filter(!screen_name %in% ep_group_accounts) %>%
  distinct(screen_name, .keep_all = TRUE)

Es bleiben 512 Accounts über. Tatsächlich haben also nur diese drei Accounts auf den Fraktionsaccount verwiesen und zwei Accounts waren doppelt vertreten.

Nachdem wir das erledigt haben, können wir endlich die letzten 3.200 Tweets eines jeden Abgeordneten abgreifen (so viel lässt die Twitter API maximal zu). Aber wie bereits angesprochen, müssen wir nach 52 Accounts immer eine 15-minütige Pause einlegen. Wir machen dies mit einer sogenannten For-Loop. Und diese sieht so aus:

mep_tweets <- vector("list", length(mep_details$screen_name))

for (i in seq_along(mep_tweets)) {
  mep_tweets[[i]] <- try(get_timeline(mep_details$screen_name[i], n = 3200))
  if (i %% 52L == 0L) {
    rl <- rate_limit("get_timeline")
    Sys.sleep(as.numeric(rl$reset, "secs"))
  }
  ## print update message
  cat(i, " ")
}

mep_tweets <- mep_tweets %>%
  bind_rows()

Diese For-Loop habe allerdings nicht ich geschrieben, sie stammt von dem Entwickler von rtweet Michael W. Kearney und wurde in einem Github-Thread beschrieben. Vielen Dank dafür.

Wir erstellen uns zunächst einen leeren Vektor mit 512 Elementen. Anschließend füllen wir diesen Vektor über die Funktion get_timeline() mit n = 3200 Tweets pro Account. Wir wollen nicht, dass unsere Schleife bei einem fehler abbricht, deshalb setzen wir die Funktion try() ein – hier gibt es nur eine Warnmeldung wenn ein Account beispielsweise gelöscht oder auf privat gestellt wurde.

Bei 512 Accounts dauert das ganze etwa 3 Stunden.

Wir waren aber erfolgreich und konnten insgesamt 1.126.584 Tweets von 490 Accounts herunterladen. Das heißt, dass 22 MEPs einen Twitteraccount angegeben haben, aber nichts getwittert haben (oder ihren Account auf “privat” gesetzt haben).

Zetzt verbinden wir noch die beiden Datensätze – die Twitterdaten und die Daten des Europaparlaments.

mep_tweets <- mep_tweets %>%
  mutate(lower_screen_name = str_to_lower(screen_name)) %>%
  left_join(mep_details %>% select(screen_name, party, group_short, country) %>%
              mutate(country_ep = country), by = c("lower_screen_name" = "screen_name")) %>%
  select(-lower_screen_name)

Zuletzt speichern wir alle relevanten Daten in einem Ordner ab und können im weiteren Verlauf darauf zurückgreifen.

saveRDS(mep_tweets, "data/mep_tweets.RDS")
saveRDS(mep_details, "data/mep_details_clean.RDS")