Analyse de données Twitter avec R

Partie 1: Collecte des statuts Twitter liés à une conférence scientifique

Marion Louveaux

20 minutes


Dans cet article de blog, j’utilise le package {rtweet} pour explorer les statuts Twitter collectés lors d’une conférence scientifique. J’ai divisé cet article en trois parties. Voici la partie 1.

Twitter est l’un des rares médias sociaux utilisés dans la communauté scientifique. Les utilisateurs ayant un profil scientifique sur Twitter communiquent sur la publication récente d’articles de recherche, les outils qu’ils utilisent, par exemple des logiciels ou des microscopes, les séminaires et conférences auxquels ils assistent ou leur vie de scientifique. Par exemple, sur mon compte Twitter personnel, je partage mes articles de blog, mes documents de recherche et mes diapositives, et je retweete (= je partage) ou je like (= j’aime) les sujets en lien avec la programmation avec R ou l’analyse d’images biologiques.
Twitter archive tous les tweets et offre une API pour effectuer des recherches sur ces données. Le package {rtweet} fournit une interface entre l’API de Twitter et R.

J’ai recueilli des données lors de la conférence 2020 NEUBIAS qui s’est tenue un peu plus tôt cette année à Bordeaux. NEUBIAS, pour “Network of EUropean BioImage AnalystS”, est un réseau scientifique créé en 2016 et soutenu jusqu’à cette année par les fonds européens COST. Les bioimage analysts extraient et visualisent des données provenant d’images biologiques (principalement des images de microscopie mais pas exclusivement) en utilisant des algorithmes et des logiciels d’analyse d’images développés par des laboratoires de recherche en analyse d’image pour répondre à des questions biologiques, soit pour leurs propres recherches en biologie, soit pour d’autres scientifiques. Je me considère comme une bioimage analyst, et je suis une membre active de NEUBIAS depuis 2017. J’ai notamment contribué à la création d’un réseau local de bioimage analysts lors de mon post-doctorat à Heidelberg de 2016 à 2019 et à la co-organisation de deux écoles thématiques NEUBIAS. J’ai également donné des cours et TD lors de trois écoles thématiques NEUBIAS. De plus, j’ai récemment co-créé un bot Twitter appelé Talk_BioImg, qui retweete le hashtag #BioimageAnalysis, afin d’encourager les gens de cette communauté à se connecter sur Twitter (voir “Announcing the creation of a Twitter bot retweeting #BioimageAnalysis” and “Create a Twitter bot on a raspberry Pi 3 using R”, en anglais, pour plus d’informations).

Dans cette première partie, j’explique comment j’ai collecté et agrégé les données, principalement comment :
1) j’ai identifié les hashtags de la conférence pour cibler ma recherche.
2) j’ai récolté les statuts Twitter, c’est-à-dire les tweets, retweets et citations (retweets avec commentaires), pour ces hashtags en interrogeant manuellement l’API Twitter durant 12 jours.
3) j’ai agrégé ces données. Comme j’interrogeais l’API Twitter tous les 2 ou 3 jours, certains statuts sont apparus plusieurs fois. J’ai choisi de ne conserver que les occurrences les plus récentes de chaque statut.

Packages

Pour collecter les statuts de Twitter et obtenir des capture d’écran de certains statuts, j’utilise le package {rtweet}. Pour stocker et lire les données au format RDS, j’utilise {readr}. Pour manipuler et nettoyer les données, j’utilise {dplyr}, {forcats}, {purrr} et {tidyr}. Pour visualiser les données collectées, j’utilise {ggplot2}, {ggtext}, {grid}, {lubridate} et {RColorBrewer}.

library(dplyr)
library(forcats)
library(here)
library(ggplot2)
library(ggtext)
library(glue)
library(grid)
library(kableExtra)
library(lubridate)
library(magick)
library(purrr)
library(RColorBrewer)
library(readr)
library(rtweet)
library(tidyr)

Graphiques: Thème et palette

Le code ci-dessous définit un thème et une palette de couleurs communs à toutes les graphiques. La fonction theme_set() de {ggplot2} définit le thème pour tous les graphiques.

# Define a personnal theme
custom_plot_theme <- function(...){
  theme_classic() %+replace%
    theme(panel.grid = element_blank(),
          axis.line = element_line(size = .7, color = "black"),
          axis.text = element_text(size = 11),
          axis.title = element_text(size = 12),
          legend.text = element_text(size = 11),
          legend.title = element_text(size = 12),
          legend.key.size = unit(0.4, "cm"),
          strip.text.x = element_text(size = 12, colour = "black", angle = 0),
          strip.text.y = element_text(size = 12, colour = "black", angle = 90))
}

## Set theme for all plots 
theme_set(custom_plot_theme())

# Define a palette for graphs
greenpal <- colorRampPalette(brewer.pal(9,"Greens"))

Identification des hashtags de la conférence

Pour annoncer l’un des événements de la conférence, le “call4help”, une session durant le symposium consacrée à la discussion de cas concrets difficiles à analyser et provenant de projets de recherche en cours, l’un des organisateurs a spontanément utilisé le hashtag #neubias2020.

m <- tweet_shot(statusid_or_url = "https://twitter.com/jan_eglinger/status/1233069442189463553?s=19")
image_write(m, "tweet1.png")

Un jour plus tard, un autre organisateur a annoncé une poignée de hashtags officiels à utiliser pendant la conférence : #neubiasBordeaux, #neubias_Bdx, #neubiasBdx, et #neubias2020_Bdx.

m <- tweet_shot(statusid_or_url = "https://twitter.com/fab_cordelieres/status/1233435244851843077")
image_write(m, "tweet2.png")

J’ai décidé de collecter les statuts contenant au moins un des hashtags suivants : #neubiasBordeaux, #neubias_Bdx, #neubiasBdx, #neubias2020_Bdx, ou #neubias2020. A partir de maintenant, j’appellerai ces cinq hashtags les “hashtags de la conférence”.

Recherche des statuts Twitter contenant les hashtags de la conférence

J’ai commencé à collecter les statuts Twitter, c’est-à-dire les tweets, les retweets et les citations (retweets avec commentaires), pendant la conférence et j’ai arrêté 5 jours après le dernier jour de la conférence. L’API Twitter ne donne accès aux tweets que pour les 6 à 9 derniers jours précédant la recherche. Pour m’assurer de collecter le plus de tweets possible, j’ai donc manuellement recherché tous les 2-3 jours les tweets contenant les hashtags de la conférence avec la fonction search_tweets2() de {rtweet}. J’ai stocké les résultats de la recherche sous forme de fichiers RDS dans un sous-dossier appelé data_Neubias. Les données originales peuvent être téléchargées ici.

sr_neubiasBdx <- search_tweets2(c("#neubiasBordeaux", "#neubias_Bdx", "#neubiasBdx", "#neubias2020_Bdx", "neubias2020"), n = 18000, retryonratelimit = TRUE)

write_rds(sr_neubiasBdx, path = file.path("data_neubias", paste0(Sys.Date(), "_sr_neubiasBdx.rds")))

Notez que pour utiliser {rtweet}, vous devez avoir un compte Twitter. La première fois que vous exécutez la fonction search_tweets2(), {rtweet} vous redirige vers votre compte Twitter pour obtenir un token personnel. Ce token doit être stocké dans votre fichier .Renviron. Vous pouvez ouvrir et éditer le fichier .Renviron avec la commande usethis::edit_r_environ().

J’ai récupéré un dataframe par recherche. Chaque dataframe contient 91 variables, donnant une quantité importante d’informations sur le statut, notamment sur son contenu (texte intégral, hyperliens, hashtags, médias tels que des images ou des vidéos,…), sur son auteur, et sur le nombre de fois qu’il a été retweeté, retweeté avec commentaire ou liké. Par exemple, regardons les premières lignes du dataframe collecté le 6 mars.

sr_neubiasBdx <- read_rds(path = "/data_neubias/2020-03-06_sr_neubiasBdx.rds")

sr_neubiasBdx %>% 
  glimpse()

Comme il y a 91 variables, j’utilise la fonction glimpse() de {dplyr} pour les afficher de manière condensée. Cliquez ci-contre sur code pour voir le résultat.


```
## Observations: 2,694
## Variables: 91
## $ user_id                  "42737563", "42737563", "42737563", "42737563…
## $ status_id                "1236031392246648832", "1236031350085431297",…
## $ created_at               2020-03-06 20:50:22, 2020-03-06 20:50:12, 20…
## $ screen_name              "fab_cordelieres", "fab_cordelieres", "fab_co…
## $ text                     "Fabrice de Chaumont @fabdechaumont starts th…
## $ source                   "TweetDeck", "TweetDeck", "TweetDeck", "Tweet…
## $ display_text_width       140, 140, 140, 130, 140, 140, 140, 140, 140, …
## $ reply_to_status_id       NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ reply_to_user_id         NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ reply_to_screen_name     NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ is_quote                 FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FAL…
## $ is_retweet               TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRU…
## $ favorite_count           0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
## $ retweet_count            1, 1, 7, 4, 4, 3, 3, 5, 7, 1, 1, 1, 1, 1, 1, …
## $ quote_count              NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ reply_count              NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ hashtags                 ["neubiasBordeaux", "neubiasBordeaux", NA, <…
## $ symbols                  [NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ urls_url                 [NA, NA, NA, NA, "livemousetracker.org", NA,…
## $ urls_t.co                [NA, NA, NA, NA, "https://t.co/PUPMq9t49g", …
## $ urls_expanded_url        [NA, NA, NA, NA, "https://livemousetracker.o…
## $ media_url                [NA, NA, NA, "http://pbs.twimg.com/tweet_vid…
## $ media_t.co               [NA, NA, NA, "https://t.co/pn4IYRraVO", NA, …
## $ media_expanded_url       [NA, NA, NA, "https://twitter.com/martinjone…
## $ media_type               [NA, NA, NA, "photo", NA, NA, NA, NA, NA, NA…
## $ ext_media_url            [NA, NA, NA, "http://pbs.twimg.com/tweet_vid…
## $ ext_media_t.co           [NA, NA, NA, "https://t.co/pn4IYRraVO", NA, …
## $ ext_media_expanded_url   [NA, NA, NA, "https://twitter.com/martinjone…
## $ ext_media_type           NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ mentions_user_id         [<"1189027725211820032", "2785982550">, "282…
## $ mentions_screen_name     [<"anna_medyukhina", "fabdechaumont">, "MaKa…
## $ lang                     "en", "en", "en", "en", "en", "en", "en", "en…
## $ quoted_status_id         NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ quoted_text              NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ quoted_created_at        NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, …
## $ quoted_source            NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ quoted_favorite_count    NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ quoted_retweet_count     NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ quoted_user_id           NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ quoted_screen_name       NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ quoted_name              NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ quoted_followers_count   NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ quoted_friends_count     NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ quoted_statuses_count    NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ quoted_location          NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ quoted_description       NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ quoted_verified          NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ retweet_status_id        "1235879491504869376", "1235880480416894976",…
## $ retweet_text             "Fabrice de Chaumont @fabdechaumont starts th…
## $ retweet_created_at       2020-03-06 10:46:46, 2020-03-06 10:50:42, 20…
## $ retweet_source           "Twitter for Android", "TweetDeck", "Twitter …
## $ retweet_favorite_count   6, 3, 15, 9, 7, 4, 8, 11, 13, 5, 9, 5, 7, 6, …
## $ retweet_retweet_count    1, 1, 7, 4, 4, 3, 3, 5, 7, 1, 1, 1, 1, 1, 1, …
## $ retweet_user_id          "1189027725211820032", "2829155332", "5678688…
## $ retweet_screen_name      "anna_medyukhina", "MaKaefer", "martinjones78…
## $ retweet_name             "Anna Medyukhina", "Marie - not a queue jumpe…
## $ retweet_followers_count  60, 284, 1487, 1487, 1487, 1487, 751, 81, 394…
## $ retweet_friends_count    138, 408, 3788, 3788, 3788, 3788, 2185, 93, 5…
## $ retweet_statuses_count   97, 2198, 6994, 6994, 6994, 6994, 2451, 45, 4…
## $ retweet_location         "Memphis, TN", "", "Bishopstoke, Hampshire, U…
## $ retweet_description      "scientist, bioimage analyst, runner", "Image…
## $ retweet_verified         FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FAL…
## $ place_url                NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ place_name               NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ place_full_name          NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ place_type               NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ country                  NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ country_code             NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ geo_coords               [, , , ,  [, , , ,  [,  "https://twitter.com/fab_cordelieres/status/1…
## $ name                     "Fabrice Cordelieres", "Fabrice Cordelieres",…
## $ location                 "Bordeaux, France", "Bordeaux, France", "Bord…
## $ description              "Trained as a cell biologist, working as a Bi…
## $ url                      NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ protected                FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FAL…
## $ followers_count          673, 673, 673, 673, 673, 673, 673, 673, 673, …
## $ friends_count            725, 725, 725, 725, 725, 725, 725, 725, 725, …
## $ listed_count             2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, …
## $ statuses_count           807, 807, 807, 807, 807, 807, 807, 807, 807, …
## $ favourites_count         1106, 1106, 1106, 1106, 1106, 1106, 1106, 110…
## $ account_created_at       2009-05-26 22:13:16, 2009-05-26 22:13:16, 20…
## $ verified                 FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FAL…
## $ profile_url              NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ profile_expanded_url     NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ account_lang             NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ profile_banner_url       NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ profile_background_url   "http://abs.twimg.com/images/themes/theme1/bg…
## $ profile_image_url        "http://pbs.twimg.com/profile_images/37880000…
## $ query                    "#neubiasBordeaux", "#neubiasBordeaux", "#neu…
```

On the 6th of March, I collected 2694 Twitter statuses.

Rassemblement de tous les statuts récoltés

Comme j’interrogeais l’API Twitter tous les 2 ou 3 jours, la plupart des tweets ont été collectés au moins deux fois. J’ai créé la fonction read_and_add_harvesting_date() pour ajouter la date de récolte comme variable supplémentaire et garder toujours la dernière version récoltée.

#' Read the RDS files and add the date contained in the filename
#'
#' @param RDS_file path to RDS file
#' @param split_date_pattern part of the filename which is not the date
#' @param RDS_dir  path to directory containing RDS file
#'
#' @return a tibble containing the content of RDS file
#' @export
#'
#' @examples
read_and_add_harvesting_date <- function(RDS_file, split_date_pattern, RDS_dir) {
  RDS_filename <- gsub(RDS_file, pattern = paste0(RDS_dir, "/"), replacement ="")
  RDS_date <- gsub(RDS_filename, pattern = split_date_pattern, replacement = "")
  readRDS(RDS_file) %>%
    mutate(harvest_date = as.Date(RDS_date))
}

RDS_file_sr_neubiasBdx <- grep(list.files(here("data_neubias"), full.names = TRUE),
                                 pattern = "neubias",
                                 value = TRUE)
split_date_pattern_sr_neubiasBdx <- "_sr_neubiasBdx.rds"
RDS_dir_name_sr_neubiasBdx <- here("data_neubias")


all_neubiasBdx <- pmap_df(list(RDS_file_sr_neubiasBdx,
                               split_date_pattern_sr_neubiasBdx,
                               RDS_dir_name_sr_neubiasBdx), read_and_add_harvesting_date)

Avec la fonction read_and_add_harvesting_date(), j’ai ensuite réuni tous les statuts récoltés dans dataframe unique.

all_neubiasBdx_unique <- all_neubiasBdx %>% 
  arrange(desc(harvest_date)) %>% # take latest harvest date
  distinct(status_id, .keep_all = TRUE) # .keep_all to keep all variables

write_rds(all_neubiasBdx_unique, path = file.path("data_out_neubias", "all_neubiasBdx_unique.rds"))

J’ai désormais regroupé tous les statuts Twitter que j’ai récoltés pendant 12 jours. J’utilise à nouveau glimpse() pour afficher toutes les variables.

all_neubiasBdx_unique %>% 
  glimpse()

Cliquez sur code ci-contre pour voir le résultat.


```
## Observations: 2,629
## Variables: 92
## $ user_id                  "2785982550", "2785982550", "9408326384908943…
## $ status_id                "1237770746740555777", "1236050321799024641",…
## $ created_at               2020-03-11 16:01:57, 2020-03-06 22:05:36, 20…
## $ screen_name              "fabdechaumont", "fabdechaumont", "BlkHwk0ps"…
## $ text                     "#neubiasBordeaux having a lasting impact 😂 @…
## $ source                   "Twitter for Android", "Twitter for Android",…
## $ display_text_width       76, 140, 140, 140, 140, 140, 139, 140, 139, 1…
## $ reply_to_status_id       NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ reply_to_user_id         NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ reply_to_screen_name     NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ is_quote                 FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FAL…
## $ is_retweet               TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRU…
## $ favorite_count           0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 10,…
## $ retweet_count            1, 2, 4, 4, 3, 7, 8, 2, 4, 4, 7, 3, 7, 3, 2, …
## $ quote_count              NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ reply_count              NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ hashtags                 ["neubiasBordeaux", "neubiasBordeaux", NA, "…
## $ symbols                  [NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ urls_url                 [NA, NA, NA, NA, "NIS.ai", <"NIS.ai", "micro…
## $ urls_t.co                [NA, NA, NA, NA, "https://t.co/lmbELKBgwW", …
## $ urls_expanded_url        [NA, NA, NA, NA, "http://NIS.ai", <"http://N…
## $ media_url                [NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ media_t.co               [NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ media_expanded_url       [NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ media_type               [NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ ext_media_url            [NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ ext_media_t.co           [NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ ext_media_expanded_url   [NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ ext_media_type           NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ mentions_user_id         [<"1943817554", "2785982550">, "2829155332",…
## $ mentions_screen_name     [<"pseudoobscura", "fabdechaumont">, "MaKaef…
## $ lang                     "en", "en", "en", "en", "en", "en", "en", "en…
## $ quoted_status_id         NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ quoted_text              NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ quoted_created_at        NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, …
## $ quoted_source            NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ quoted_favorite_count    NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ quoted_retweet_count     NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ quoted_user_id           NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ quoted_screen_name       NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ quoted_name              NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ quoted_followers_count   NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ quoted_friends_count     NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ quoted_statuses_count    NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ quoted_location          NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ quoted_description       NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ quoted_verified          NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ retweet_status_id        "1237710280009945089", "1235880480416894976",…
## $ retweet_text             "#neubiasBordeaux having a lasting impact 😂 @…
## $ retweet_created_at       2020-03-11 12:01:40, 2020-03-06 10:50:42, 20…
## $ retweet_source           "Twitter Web App", "TweetDeck", "Twitter for …
## $ retweet_favorite_count   6, 6, 8, 6, 8, 16, 19, 2, 7, 9, 24, 3, 16, NA…
## $ retweet_retweet_count    1, 2, 4, 4, 3, 7, 8, 2, 4, 4, 7, 3, 7, NA, NA…
## $ retweet_user_id          "1943817554", "2829155332", "2829155332", "56…
## $ retweet_screen_name      "pseudoobscura", "MaKaefer", "MaKaefer", "mar…
## $ retweet_name             "Christopher Schmied", "Marie - not a queue j…
## $ retweet_followers_count  416, 293, 293, 1499, 293, 1499, 68, 293, 293,…
## $ retweet_friends_count    576, 423, 423, 3805, 423, 3805, 144, 423, 423…
## $ retweet_statuses_count   468, 2205, 2205, 7053, 2205, 7053, 98, 2205, …
## $ retweet_location         "Berlin, Germany", "", "", "Bishopstoke, Hamp…
## $ retweet_description      "Data Scientist | Bioimage Analyst @LeibnizFM…
## $ retweet_verified         FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FAL…
## $ place_url                NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ place_name               NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ place_full_name          NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ place_type               NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ country                  NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ country_code             NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ geo_coords               [, , , ,  [, , , ,  [,  "https://twitter.com/fabdechaumont/status/123…
## $ name                     "Fabrice de Chaumont", "Fabrice de Chaumont",…
## $ location                 "", "", "Just were I have to be.", "Just were…
## $ description              "Playing with mice", "Playing with mice", "Lo…
## $ url                      NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ protected                FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FAL…
## $ followers_count          77, 77, 298, 298, 298, 298, 298, 298, 298, 29…
## $ friends_count            45, 45, 204, 204, 204, 204, 204, 204, 204, 20…
## $ listed_count             0, 0, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 0, 5, 5, …
## $ statuses_count           63, 63, 15726, 15726, 15726, 15726, 15726, 15…
## $ favourites_count         71, 71, 12, 12, 12, 12, 12, 12, 12, 12, 12, 1…
## $ account_created_at       2014-09-02 13:44:31, 2014-09-02 13:44:31, 20…
## $ verified                 FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FAL…
## $ profile_url              NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ profile_expanded_url     NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ account_lang             NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ profile_banner_url       "https://pbs.twimg.com/profile_banners/278598…
## $ profile_background_url   "http://abs.twimg.com/images/themes/theme1/bg…
## $ profile_image_url        "http://pbs.twimg.com/profile_images/12363584…
## $ query                    "#neubiasBordeaux", "#neubiasBordeaux", "#neu…
## $ harvest_date             2020-03-11, 2020-03-11, 2020-03-11, 2020-03-…
```

Le jeu de données final contient 92 variables et 2629 statuts Twitter.

total_tweet_number <- all_neubiasBdx_unique %>% 
  filter(!is_retweet) %>% 
  pull(status_id) %>% 
  unique() %>% 
  length()

total_retweet_number <- all_neubiasBdx_unique %>% 
  filter(is_retweet) %>% 
  pull(status_id) %>% 
  unique() %>% 
  length()

Plus précisément, parmi les 2629 statuts Twitter, seulement 661 sont des tweets originaux ou des retweets avec commentaires et 1968 sont des retweets.

Nombre de tweets et de retweets au cours de la conférence

Plusieurs événements ont eu lieu au cours de la conférence :
- deux écoles thématiques en biomage analyse et un “taggathon” pour identifier les ressources en bioimage analyse et mettre à jour la base de données en ligne du samedi 29 février au mardi 3 mars (matin) que je regrouperai sous le terme de “training schools”.
- un évènement satellite le mardi 3 mars (après-midi) et un symposium du mercredi 4 mars au vendredi 6 mars que je regrouperai sous le terme de “symposium”.

J’étais curieuse de voir l’évolution du nombre de statuts et de retweets avec commentaires comparé au nombre de retweets au cours de la conférence.

nb_days <- floor(as.numeric(max(all_neubiasBdx_unique$created_at) - min(all_neubiasBdx_unique$created_at)))

df_per_slot <- all_neubiasBdx_unique %>% 
  mutate(
    datetime = as_datetime(created_at),
    slot = round_time(datetime, n = "6 hours")
  ) %>% 
  count(is_retweet, slot) 

df_annotate_text <- tibble(
  x = c(ymd_hm(c("2020-03-03 12:00", "2020-03-05 18:00")),
        mean(ymd_hm(c("2020-02-29 06:00", "2020-03-03 12:00"))), 
            mean(ymd_hm(c("2020-03-06 12:00", "2020-03-03 12:00")))
  ),
  y = c(190, 180, 210, 210),
  label = c("Satellite meeting", "Gala dinner", "TRAINING SCHOOLS", "SYMPOSIUM")
)


df_annotate_curve <- tibble(
 x = ymd_hm(c("2020-03-03 12:00", "2020-03-05 18:00")),
  y = c(190, 180)-5,
 xend = x,
 yend = y-20
)

ylim_max <-  225

ggplot(df_per_slot) +
  aes(x = slot, y = n, color = is_retweet) +
  geom_rect(aes(
    xmin = ymd_hm("2020-02-29 06:00"), xmax = ymd_hm("2020-03-03 12:00"),
    ymin = 0, ymax = ylim_max
  ),
  fill = "grey80", colour = NA
  ) +
  geom_rect(aes(
    xmin = ymd_hm("2020-03-03 12:00"), xmax = ymd_hm("2020-03-06 12:00"),
    ymin = 0, ymax = ylim_max
  ),
  fill = "grey90", colour = NA
  ) +
  geom_line(size = 1.2) +
  geom_point() +
  geom_text(
    data = df_annotate_text, aes(x = x, y = y, label = label),
    hjust = "center", size = 4, color = "grey20"
  ) +
  geom_curve(
    data = df_annotate_curve,
    aes(x = x, y = y, xend = xend, yend = yend),
    size = 0.6, curvature = 0,
    arrow = arrow(length = unit(2, "mm")), color = "grey20"
  ) +
  scale_x_datetime(
    date_breaks = "1 day", date_labels = "%b-%d",
    guide = guide_axis(n.dodge = 2)
  ) +
  scale_color_manual(
    labels = c(`FALSE` = "Tweet", `TRUE` = "Retweet"),
    values = c("#00441B", "#5DB86A")
  ) +
  scale_y_continuous(expand = c(0, 0), limits = c(0, ylim_max)) +
  labs(
    x = NULL, y = NULL,
    title = glue("Frequency of Twitter statuses containing NEUBIAS conference hashtags"),
    subtitle = glue(
      "Count of <span style = 'color:#00441B;'>tweets </span>",
      "and <span style = 'color:#5DB86A;'>retweets</span> per 6 hours over {nb_days} days"
    ),
    caption = "<i>\nSource: Data collected from Twitter's REST API via rtweet</i>",
    colour = "Type"
  ) +
  theme(
    plot.subtitle = element_markdown(),
    plot.caption = element_markdown(),
    legend.position = "none"
  )

Identification du tweet le plus retweeté

Comme il y avait beaucoup de retweets, j’étais aussi curieuse de voir quel tweets ont été les plus retweetés et je souhaitais afficher le tweet le plus retweeté.

most_retweeted <- all_neubiasBdx_unique %>% 
  filter(is_retweet == FALSE) %>% 
  arrange(desc(retweet_count)) 


most_retweeted %>% 
  select(status_id, created_at, screen_name, retweet_count, favorite_count) %>% 
  head(10) %>% 
knitr::kable()
status_id created_at screen_name retweet_count favorite_count
1234405016603107328 2020-03-02 09:07:44 pseudoobscura 34 74
1234401337741316096 2020-03-02 08:53:07 MarionLouveaux 21 36
1236023660852514821 2020-03-06 20:19:39 fab_cordelieres 19 60
1235252471104229382 2020-03-04 17:15:13 MarionLouveaux 18 38
1234806841009475584 2020-03-03 11:44:27 martinjones78 17 21
1233069442189463553 2020-02-27 16:40:38 jan_eglinger 16 12
1235502652693323776 2020-03-05 09:49:21 matuskalas 14 24
1234403570969128961 2020-03-02 09:02:00 pseudoobscura 13 24
1235865025157328896 2020-03-06 09:49:17 martinjones78 13 30
1235167276153831425 2020-03-04 11:36:41 Zahady 13 45

Pour obtenir une capture d’écran du tweet le plus retweeté, j’utilise la fonction tweet_shot() de {rtweet} et je stocke l’image au format .png à l’aide de la fonction image_write() du package {magick}.

m <- tweet_shot(statusid_or_url = most_retweeted$status_id[1])
image_write(m, "tweet3.png")

Conclusion

Dans cette première partie, j’ai expliqué comment j’ai collecté et agrégé les statuts Twitter récoltés dans le cadre d’une conférence scientifique. Premièrement, j’ai identifié les hashtags proposés par les organisateurs de la conférence et j’ai décidé de limiter ma recherche à ces hashtags. Deuxièmement, j’ai interrogé manuellement l’API Twitter en utilisant la fonction search_tweets2() du package {rtweet}. Troisièmement, j’ai rassemblé ces données dans une unique dataframe et visualisé l’évolution du nombre de tweets et retweets au cours de la conférence.

Dans la deuxième et la troisième partie de cet article de blog, j’explorerai respectivement les caractéristiques des utilisateurs de Twitter qui ont Twitté en utilisant ces hashtags et le contenu des statuts.

Remerciements

Je tiens à remercier le Dr. Sébastien Rochette pour son aide sur {ggplot2} et {magick}.

Ressources

Je recommande vivement la lecture de la vignette du package {rtweet}.



Citation :

Merci de citer ce travail avec :
Louveaux M. (2020, Mar. 24). "Analyse de données Twitter avec R". Retrieved from https://marionlouveaux.fr/fr/blog/twitter-analysis/.

Citation BibTex :
@misc{Louve2020Analy,
    author = {Louveaux M},
    title = {Analyse de données Twitter avec R},
    url = {https://marionlouveaux.fr/fr/blog/twitter-analysis/},
    year = {2020}
  }
comments powered by Disqus