#JOBIM2018 : une étude de réseau

Bon­jour à tou·te·s !

Comme une par­tie de la com­mu­nau­té bio­in­for­ma­tique fran­çaise, et pro­ba­ble­ment du lec­to­rat de ce blog, je me suis ren­du à la 19è édi­tion des Jour­nées Ouvertes en Bio­lo­gie, Infor­ma­tique et Mathé­ma­tiques (JOBIM). Celle-ci se tenait à Mar­seille, au Palais du Pha­ro, et pour celles et ceux d'entre vous curieux·ses de connaître le conte­nu scien­ti­fique, voi­là le résu­mé des inter­ven­tions. Et pour l'ambiance, voi­là les pho­tos !

Si vous sui­vez ce blog depuis long­temps, vous n'êtes pas sans savoir qu'on aime ana­ly­ser les tweets : c'était le cas pour JOBIM2013, JOBIM/​ECCB2014, JOBIM2015… En effet, la com­mu­nau­té bio­in­for­ma­tique fran­co­phone gazouille pas mal, et il est pos­sible de récu­pé­rer auto­ma­ti­que­ment les don­nées de ce réseau social. Alors cette année on remet le cou­vert, mais de manière un peu plus détaillée, et sous forme de réseau ! Et, cerise sur le gâteau, on en fait un tuto­riel. On va ici concen­trer sur deux mots-dièse (ou hash­tag, pour les anglo­phones) : #JOBIM2018 et #dark­jo­bim (l'existence de ce mot-dièse trol­lesque est un peu sombre, mais un bioin­fo-twit­tos a une piste).

Alors, dans cet article, nous allons :

  1. télé­char­ger les tweets des mots-dièse sélec­tion­nés
  2. les syn­thé­ti­ser sous forme de trois réseaux (i. mots-dièse, ii. twit­tos et iii. mots-diè­se/t­wit­tos, un réseau qu'on dit "bipar­tite")
  3. inter­pré­ter les résul­tats !

Allez hop, c'est par­ti ! 💪

Téléchargement des tweets

Pour cette par­tie, quelques notions de Python seront néces­saires. Si vous n'êtes pas sûr·e d'avoir les bases, n'hésitez pas à fouiller par­mi les articles qui concernent ce lan­gage de pro­gram­ma­tion.

L'idée est d'utiliser l'API de Twit­ter via le paquet Twee­py. Une API, pour Appli­ca­tion Pro­gram­ming Inter­face, est comme son nom l'indique une inter­face qui nous per­met, dans ce cas pré­cis, d'accéder de manière pro­gram­ma­tique au réseau social. Avec Twee­py, on peut twee­ter, suivre des gens, faire des recherches… Tout ! Ou presque : l'API de Twit­ter, ou plu­tôt sa ver­sion gra­tuite, a quelques limi­ta­tions, comme l'impossibilité de recher­cher des tweets plus vieux que 7 jours à par­tir de la date actuelle. Mais dans notre cas c'est suf­fi­sant : j'ai lan­cé la requête à la fin de la confé­rence.

Pour uti­li­ser l'API de Twit­ter, il faut pas­ser par trois étapes : créer un compte Twit­ter, deman­der l'accès Déve­lop­peur à Twit­ter, et géné­rer des jetons d'identification. La pre­mière étape (obli­ga­toire depuis juillet 2018 seule­ment, et requé­rant d'écrire un texte de 300 carac­tères) se passe ici. Une fois votre compte vali­dé, vous pour­rez créer une appli­ca­tion, ici, et géné­rer 4 iden­ti­fiants : les clés d'API (une publique et une secrète) et les jetons d'accès (un public et un secret). Conser­vez ces 4 iden­ti­fiants, ce sont eux qui vous per­met­tront de vous connec­ter à Twit­ter via Twee­py.

Allez, après toutes ces for­ma­li­tés, met­tons un peu les mains dans le cam­bouis ! Je ne vais pas dans cet article insé­rer tout le code rédi­gé pour ce petit pro­jet, mais seule­ment les élé­ments les plus impor­tants. Pour les détails et l'implémentation, vous pou­vez jeter un œil à l'intégralité du code,  dis­po­nible en ligne sur Github.

Le pre­mier bout de code impor­tant à avoir, c'est donc la connexion à Twit­ter !

import tweepy
import json # On va récupérer les tweets au format JSON

# Identifiants obtenus lors de l'enregistrement de l'application
consumer_key = "xxx"
consumer_secret = "xxx"
access_token = "xxx"
access_token_secret = "xxx"

# Connexion à l'API
auth = tweepy.OAuthHandler(consumer_key, consumer_secret)
auth.set_access_token(access_token, access_token_secret)
api = tweepy.API(auth)

# On peut tester en récupérant des tweets de notre propre timeline
public_tweets = api.home_timeline()
for tweet in public_tweets:
print(tweet.text)

Maintenant qu'on est connecté, on va pouvoir faire des requêtes sur les tweets d'un mot-dièse particulier, et les enregistrer dans des dictionnaires. Cela nous permettra de générer les différents réseaux qu'on veut faire à partir d'une seule requête sur Twitter. Pour ceci, la méthode à suivre est relativement simple : elle fait appel à l'objet Cursor de Tweepy, qui permet de récupérer les résultats d'une requête de manière paginée.

# On sauvegarde les tweets dans un dictionnaire
all_tweets = {}

# On boucle sur les éléments résultant de Cursor
for tweet in tweepy.Cursor(api.search, q="#jobim2018").items():
# On définit un tweet comme un dictionnaire
dic = {}

# On peut accéder aux informations du tweet comme ceci :
dic['id'] = tweet._json['id']
dic['text'] = tweet._json['text']
dic['user'] = tweet._json['user']['screen_name']
dic['date'] = tweet._json['created_at']
dic['fav'] = tweet._json['favorite_count']
dic['rt'] = tweet._json['retweet_count']

# Pour les mots-dièse et les mentions, c'est plus compliqué :
# il peut y en avoir plusieurs, alors on va les stocker sous
# forme de listes
hashtags = tweet._json['entities']['hashtags']
hashtags_list = []
for h in hashtags:
hashtags_list.append(h['text'])
dic['hashtags'] = hashtags_list

mentions = tweet._json['entities']['user_mentions']
mentions_list = []
for m in mentions:
mentions_list.append(m['screen_name'])
dic['mentions'] = mentions_list

# On insère dans le dictionnaire des tweets le tweet construit
all_tweets[id] = dic

Ensuite, vu que Twitter ne nous laisse pas accéder à l'intégralité de sa base de données, j'ai enregistré au format JSON cette requête. J'ai ainsi récupéré 669 tweets contenants les mots-dièse #JOBIM2018 et/ou #darkjobim, entre le 1er et le 6 juillet 2018. Le fichier JSON est accessible ici.

Génération des réseaux

Ça y est, on a nos données brutes ! 🎉 Dans cette section, on va les transformer un peu, pour qu'on puisse les visualiser sous forme de réseaux. Pour rappel, on va en construire trois : un réseau de mots-dièse, un réseau des twittos et un réseau qui associe les deux. On pourra donc avoir deux types de nœuds : d'une part les mots-dièse et d'autre part les twittos, qui peuvent être les auteurs des tweets ou bien en mention dans les tweets.

Réseau des mots-dièse

Ce premier réseau ne va donc contenir que des mots-dièse : ce seront les nœuds du réseau. Ils seront connectés par des arêtes, qui n'existeront qu'entre les mots-dièse apparaissant en même temps dans au moins un tweet (notion co-occurrence).

Pour ce faire, on va lire les tweets un par un, et à chaque fois qu'on voit plusieurs mots-dièse dans un même tweet, on met à jour un dictionnaire. S'il s'agit d'un nouveau lien, on crée une entrée dont la clé sera le nom des deux nœuds. Sinon, on met à jour l'entrée déjà existante, qui contient quelques valeurs (nombre de tweets, de retweets, de favoris...). Voilà ce que ça donne en Python :

# On sauvegarde le réseau dans ce dictionnaire
net = {}
for id in tweets.keys():
# On ne garde que la version des mots-dièse en minuscule, pour éviter les doublons
h = list(map(lambda x:x.lower(), tweets[id]['hashtags']))

# S'il y a plus de 2 mots-dièse, on doit ajouter des liens pour toutes les combinaisons :
# on calcule ces combinaisons ici
comb_h = list(combinations(h,2))

# On trie les mots-dièse par ordre alphabétique pour éviter les doublons
# ainsi : #a ↔ #b et #b ↔ #a donneront tous deux : "#a #b"
for pair in comb_h:
# (on ne peut pas changer la valeur d'un tuple, alors on transforme en string pour plus tard)
pair0 = pair[0]
pair1 = pair[1]

# Parfois un hashtag et un utilisateur ont le même nom, on vérifie ça et on ajoute un suffixe pour différencier
if pair0 in unique_mentions:
pair0 = pair0 + ' (hashtag)'
if pair1 in unique_mentions:
pair1 = pair1 + ' (hashtag)'

# Finalement on trie alphabétiquement
if (pair1 < pair0):
entry = pair1 + "\t" + pair0
else:
entry = pair0 + "\t" + pair1

# On récupère des métriques du tweet, pour pondérer
rt = tweets[id]['rt']
fav = tweets[id]['fav']

# On enregistre le tout dans un dico de dico
if entry not in net.keys():
net[entry] = {'n': 1, 'fav': fav, 'rt': rt, 'score': 1+fav+rt}
else:
net[entry]['n'] += 1
net[entry]['fav'] += fav
net[entry]['rt'] += rt
net[entry]['score'] = net[entry]['n'] + net[entry]['fav'] + net[entry]['rt']

Vous l'aurez remarqué : j'enregistre pour chaque association entre deux mots-dièse plusieurs valeurs : le nombre de tweets (

n

), le nombre de fois que le tweet a été aimé (

fav

) et retweeté (

rt

). Je propose également un

score

(discutable) qui somme le tout. Tout cela va nous servir lors de la visualisation.

On a donc notre réseau de mots-dièse, avec en prime des caractéristiques (appelées "attributs") pour chaque arête. Pour des questions de visualisation, on aimerait aussi avoir des attributs par nœuds, pour savoir combien de fois un mot-dièse a été tweeté par exemple. On fait ça comme ça :

# On sauvegarde les attributs des nœuds dans un... dictionnaire, oui encore !
attr = {}
for id in tweets.keys():
# On ne garde que la version des mots-dièse en minuscule, pour éviter les doublons
h = list(map(lambda x:x.lower(), tweets[id]['hashtags']))

# ... et les utilisateurs et mentions dans les tweets
m = tweets[id]['mentions'] + [tweets[id]['user']]
m = list(map(lambda x:x.lower(), m))

# Pour chaque mot-dièse, on crée ou met à jour une entrée
for h2 in h:
# Parfois un hashtag et un utilisateur ont le même nom, on vérifie ça et on ajoute un suffixe pour différencier
if h2 in unique_mentions:
h2 = h2 + ' (hashtag)'

rt = tweets[id]['rt']
fav = tweets[id]['fav']

# Nouvelle entrée
if h2 not in attr.keys():
attr[h2] = {'type': 'hashtag', 'tweets': 1, 'fav': fav, 'rt': rt, 'score': 1*(fav+rt)}
# Mise à jour d'une entrée
else:
attr[h2]['tweets'] += 1
attr[h2]['fav'] += fav
attr[h2]['rt'] += rt
attr[h2]['score'] = attr[h2]['mentions'] * (attr[h2]['fav'] + attr[h2]['rt'])

# Même chose pour les twittos
for m2 in m:
if m2 in unique_hashtags:
m2 = m2 + ' (user)'

rt = tweets[id]['rt']
fav = tweets[id]['fav']

if m2 not in attr.keys():
attr[m2] = {'type': 'user', 'tweets': 1, 'fav': fav, 'rt': rt, 'score': 1*(fav+rt)}
else:
attr[m2]['mentions'] += 1
attr[m2]['fav'] += fav
attr[m2]['rt'] += rt
attr[m2]['score'] = attr[m2]['tweets'] + attr[m2]['fav'] + attr[m2]['rt']

Le code pour générer les deux autres réseaux sont très similaires, vous pouvez jeter un œil dans le code en ligne (fonctions

tweets_to_mentions()

et

tweets_to_bipartite()

).

Écriture du fichier

Nous avons à présent nos jolis dictionnaires Python qui rassemblent nos nœuds et nos arêtes, représentant nos différents réseaux. Et maintenant ? Pour visualiser tout ça, il faut convertir cette information en un format que votre logiciel favori supporte. Dans mon cas, j'ai choisi Gephi, notamment parce qu'un super article (encore un !) parle de lui sur ce blog.

Note : d'autres logiciels sont évidemment disponibles, je vous encourage à lire un article, tout aussi génial, qui fait le tour d'horizon des outils de visualisation des réseaux biologiques.
De plus, le code disponible en ligne supporte également l'export au format Cytoscape.

On va utiliser le format GDF, qui ressemble à ça :

nodedef> name VARCHAR, score DOUBLE
a,1
b,2
c,1
d,9
edgedef> node1 VARCHAR, node2 VARCHAR, color VARCHAR
a,b,blue
b,c,blue
b,d,red

Une première partie (

nodedef

) définit les nœuds; il est possible d'ajouter autant d'attributs qu'on veut. S'ensuit la partie définissant les arêtes (

edgedef

), avec également la possibilité d'ajouter des attributs.

Dans un premier temps, on va donc lister les nœuds présents et leurs attributs, puis les arêtes :

# On ouvre notre fichier d'export
f = open("jobim2018_hashtags.gdf", 'w')

### NŒUDS ###
#############
# On charge ici le nom des attributs, sous forme de liste
col_attributes = attr[list(attr)[0]]
col_attributes_list = list(col_attributes.keys())

# On écrit l'en-tête du fichier, qui contient le nom des attributs,
# avec le type de variable auquel ils correspondent
f.write('nodedef>')
f.write('name VARCHAR')
for col in col_attributes_list:
if isinstance(col_attributes[col], str):
col_type = "VARCHAR"
elif isinstance(col_attributes[col], int):
col_type = "DOUBLE"
else:
col_type = "VARCHAR"
f.write(', ' + col + ' ' + col_type)

# Les attributs sont générés pour tous les tweets, et si on ne veut
# que les mots-dièse, on aura aussi des attributs pour les twittos.
# Solution : on ne garde que les nœuds présents dans le réseau. Ici
# on liste tous les nœuds présents
present_nodes = list(map(lambda key: key.split("\t"), net.keys()))
present_nodes = [item for sublist in present_nodes for item in sublist] # Ça c'est pour désimbriquer les listes
present_nodes = set(present_nodes) # Et ça pour ne garder que les éléments uniques

# Finalement on écrit les nœuds et leurs attributs
for node in attr.keys():
# On vérifie que le nœud est dans le réseau
if node in present_nodes:

# On écrit le nom du nœud
f.write('\n' + node)

# Et on boucle sur ses attributs
for col in col_attributes_list:
f.write(',' + str(attributes[node][col]))

### ARÊTES ###
##############
# On charge ici le nom des attributs, sous forme de liste
col_edges = net[list(net)[0]]
col_edges_list = list(col_edges.keys())

# On écrit "l'en-tête" de la deuxième partie de fichier
f.write('\nedgedef>')
f.write('node1 VARCHAR, node2 VARCHAR')
for col in col_edges_list:
if isinstance(col_edges[col], str):
col_type = "VARCHAR"
elif isinstance(col_edges[col], int):
col_type = "DOUBLE"
else:
col_type = "VARCHAR"
f.write(', ' + col + ' ' + col_type)

# On écrit les arêtes et leurs attributs
for edge in net.keys():
f.write('\n' + edge.replace('\t', ','))
for col in col_edges_list:
f.write(',' + str(net[edge][col]))
f.write('\n')
f.close()

Ayé, après ça vous devriez avoir un super fichier de réseau, lisible par Gephi ! Il ne vous reste plus qu'à l'ouvrir pour le visualiser. 😎 N'oubliez pas le didacticiel Gephi sur le blog si vous êtes perdu·e.

Résultats et discussion

Le réseau des mots-dièse

Voilà donc à quoi ressemble notre réseau final :

Réseau des mots-dièse de JOBIM2018
Réseau des mots-dièse de JOBIM2018. La taille des nœuds représente le nombre de tweets contenant le mot-dièse. L'épaisseur des arêtes montre le "score" vu précédemment. Fichier SVG.Fichier Gephi.

Ici la taille des nœuds représente le nombre de tweets comportant le mot-dièse, mais visuellement les autres métriques donnent un résultat très similaire. On voit donc deux gros paquets, autour des deux mots-dièse qu'on a sélectionné.
Côté #darkjobim, les discussions semblaient plutôt légères : ça parlait cigales, blackmagic, disruptif, jojobimbim et blockchain quantique. Quelques tweets associaient #darkjobim et #jobim2018, à propos de Bioinfuse, bioinformaticsjob, jebif...

Bon, et pour #jobim2018 alors ?  Eh bien d'après les mots-dièse, une partie des tweets étaient sérieux (synteny, rstats, microbiote, sfbi, AG). Pour le reste... voyez par vous-même. 🤐

Le réseau des twittos

Concernant les interactions entre twittos eux-mêmes, ça part nettement plus dans tous les sens :

Réseau des mots-dièse de JOBIM2018.
Réseau des twittos de JOBIM2018. La taille des nœuds représente le nombre de tweets rédigés par le twittos. L'épaisseur des arêtes montre le "score" vu précédemment. Fichier SVG. Fichier Gephi.

On voit que certains twittos ont été assez prolifiques (je ne donnerai pas de noms). Le réseau est formé d'un grand module interconnecté (on appelle ça une composante connexe), et d'autres plus petites avec deux ou trois twittos seulement. Et en un coup d'œil à ce réseau, on note que le hub Pasteur porte également bien son nom ! 😬

N'hésitez pas à télécharger les fichiers Gephi et les ouvrir avec le logiciel pour naviguer facilement dans le réseau.

Bon, et qu'est-ce que les twittos se sont racontés alors ? On va le voir avec le prochain réseau !

Le réseau bipartite : mots-dièse - twittos

Réseau bipartite des mots-dièse et des twittos de JOBIM2018.
Réseau bipartite des mots-dièse et des twittos de JOBIM2018. Fichier SVG. Fichier Gephi.

Dans ce réseau on a deux types de nœuds (mots-dièse et twittos), et on ne peut pas connecter deux nœuds d'un même type. Par conséquent, ce qu'on voit c'est littéralement "qui a dit quoi" ! Beaucoup de twittos n'ont utilisé que le mot-dièse #jobim2018 (le groupe de nœuds vers la droite du réseau). Et puis d'autres ont visiblement eu plus d'imagination. 🗣

 

Conclusion

Pfiou, on aura vu pas mal de choses dans cet article mine de rien ! Comment récupérer des tweets et les enregistrer dans un fichier JSON, comment à partir de ces tweets établir différents réseaux, et comment les visualiser. Et avec tout ça on aura eu un petit aperçu de JOBIM2018 ! Les techniques utilisées, surtout pour générer et visualiser les réseaux, peuvent être adaptables pour étudier des réseaux biologiques (interaction protéine-protéine, similarité de séquences...)

Pour ce petit projet, des améliorations sont possibles, en voici quelques-unes (n'hésitez pas à en ajouter dans les commentaires) :

  • un petit nombre de mots-dièse (ou twittos) concentrent beaucoup de tweets, ce qui rend l'échelle de la taille des nœuds assez peu représentative (on ne voit pas la différence entre 1 et 20 tweets). On pourrait changer cette échelle.
  • il y a d'autres mots intéressants dans les tweets que les mots-dièse, on pourrait aussi les utiliser.

Je rappelle que le code que j'ai écrit est en ligne à cette adresse : https://github.com/bioinfo-fr/hashtag-network

Enfin, petit coup de pub à une école d'été à laquelle j'ai pu participer sans laquelle rien n'aurait été possible grâce à laquelle j'ai appris à construire des réseaux : Evolunet. 🤓

Encore une fois, énorme merci aux relecteurs et admins (en l'occurrence Mathurin & Yoann M.) ! 💜

Alors, arriverez-vous à reproduire tout ça lors la prochaine conférence à laquelle vous participerez ? 😃



Pour continuer la lecture :


Commentaires

2 réponses à “#JOBIM2018 : une étude de réseau”

  1. Sup' dude !

    J'profite du fait que j'ai vou­lu essayer de suivre ton tuto, et j'me deman­dais : com­ment tu fais pour gérer les rate limit ? 😮

    1. Mer­ci pour ton inté­rêt ! 🙂
      Effec­ti­ve­ment le "rate limit" est un peu contrai­gnant : par fenêtre de 15 minutes, seul un cer­tain nombre de requêtes est pos­sible (https://​deve​lo​per​.twit​ter​.com/​e​n​/​d​o​c​s​/​b​a​s​i​c​s​/​r​a​t​e​-​l​i​m​i​t​ing).
      Pas de solu­tion, à part la par­ci­mo­nie dans les tests, mal­heu­reu­se­ment…

Laisser un commentaire