Analyse de données Twitter avec R
Partie 1: Collecte des statuts Twitter liés à une conférence scientifique
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"))
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 cette série d’articles de blog, j’explorerai respectivement les caractéristiques des utilisateurs de Twitter qui ont Twitté en utilisant ces hashtags et le contenu des tweets.
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/.
@misc{Louve2020Analy,
author = {Louveaux M},
title = {Analyse de données Twitter avec R},
url = {https://marionlouveaux.fr/fr/blog/twitter-analysis/},
year = {2020}
}
Partager ce post
Twitter
Google+
Facebook
LinkedIn
Email