Analyse de tweets par NLP

1 Introduction

On applique des traitements NLP des modules spaCy et Top2vec a des tweets. Pour recuperer les tweets avec le module tweepy on a cree un compte developpeur Twitter, cf https://developer.twitter.com/fr/developer-terms/policy. Lors de cette creation, 4 cles vous sont fournies : 2 cles d’acces non confidentielles, analogues a des logins (consumer key et access key), et 2 cles secretes (consumer secret et access secret). On les a stockees dans un fichier ‘tokens.txt’.

2 Chargement des modules Python et des cles secretes Twitter

import numpy as np
import pandas as pd
import os
import datetime as dttm
import pickle
import timeit
from scipy.cluster.hierarchy import dendrogram, linkage, fcluster
from scipy.spatial.distance import squareform
import matplotlib.pyplot as plt
from pyvis.network import Network

import tweepy as tw
import spacy
from spacymoji import Emoji

from top2vec import Top2Vec
from matplotlib.backends.backend_pdf import PdfPages

pd.set_option("display.max_columns", None)
pd.set_option("max_colwidth", 180)

# on charge le modele francais de taille moyenne
nlp = spacy.load("fr_core_news_md")
# pour identifier les emojis avant tout autre traitement
_ = nlp.add_pipe("emoji", first=True)
# le pipeline complete par le traitement des emoji
nlp.pipe_names
['emoji', 'tok2vec', 'morphologizer', 'parser', 'ner', 'attribute_ruler', 'lemmatizer']
# chemin vers les donnnees et les cles de Twitter
working_dir = 'donnees_twitter'

Les cles sont stockees dans le fichier comme suit.

CONS_KEY = cle1_sans_guillemets_autour
CONS_SECRET = cle2_sans_guillemets_autour
ACCESS_KEY = cle3_sans_guillemets_autour
ACCESS_SECRET = cle4_sans_guillemets_autour

On charge les cles.

with open(os.path.join(working_dir,'tokens.txt')) as f:  
  for line in f:
    key, value = line.replace('\n', '').split(' = ')
    os.environ[key] = value

On s’authentifie.

auth = tw.OAuthHandler(os.environ['CONS_KEY'], os.environ['CONS_SECRET'])
auth.set_access_token(os.environ['ACCESS_KEY'], os.environ['ACCESS_SECRET'])
api = tw.API(auth, wait_on_rate_limit=True)

3 Recuperation des tweets

3.1 Interrogation de l’API Twitter

On recupere les tweets de la derniere semaine en se limitant aux champs utiles, cf https://developer.twitter.com/en/docs/twitter-api/v1/data-dictionary/object-model/tweet.

Le mot de recherche et le volume voulu.

mot = "euro"
mot_fichier = "euro"
nb_tweets = 1500

Interrogation de l’API, compter une minute.

# on enleve les retweets
search_words = mot + "-filter:retweets"

date_since = dttm.date.strftime(dttm.date.today() - dttm.timedelta(8), "%Y-%m-%d")

tweets = tw.Cursor(api.search,
              q = search_words,
              lang = "fr").items(nb_tweets)

tic = timeit.default_timer()
tweets = list(tweets)
toc = timeit.default_timer()
toc - tic

len(tweets)

dtf = pd.json_normalize([r._json for r in tweets])

# on sauvegarde les champs utiles en csv
dtf[["text", "created_at", "user.screen_name", "in_reply_to_screen_name"]
].to_csv(os.path.join(working_dir, mot_fichier + '.csv'), index = False) 

3.2 Le dataframe des tweets

On recharge les tweets.

dtf = pd.read_csv(os.path.join(working_dir, mot_fichier + '.csv'))
dtf.head()
dtf.tail()
                                                                                                                                           text  \
0                                                            @DidiDeschamps tu as la meilleure équipe au monde tu as droit à remporter cet euro   
1  #Euro2020: Quelle erreur des Russes…. #Zobnin fait une passe en retrait mais directement dans les pieds de Poulsen!… https://t.co/1yafA8JUdA   
2                                                                               Pour l'euro, je suis heureux qu'on soit déja qualifié #euro2021   
3  Bonjour, je met en vente mon set volant / pedale et levier de vitesse g920 pour xbox et pc. Acheter il y a 1mois se… https://t.co/4EpfkpcKGK   
4                                        L’Euro c’est le seul moment où on peut voir des grands inconnus marqués des buts de top player #RUSDEN   

                       created_at user.screen_name in_reply_to_screen_name  
0  Tue Jun 22 08:24:57 +0000 2021           FMK790           DidiDeschamps  
1  Tue Jun 22 08:24:51 +0000 2021  20minutesOnline                     NaN  
2  Tue Jun 22 08:24:48 +0000 2021  the_boys_situat                     NaN  
3  Tue Jun 22 08:24:47 +0000 2021        TheFazzer                     NaN  
4  Tue Jun 22 08:24:30 +0000 2021       bosterenzo                     NaN  
                                                                                                                                                text  \
1495        Joachim Mununga : "Boyata, je pense que c’est le plus complémentaire pour jouer avec Vertonghen et Alderweireld" https://t.co/KO9dqI10pa   
1496                                             Suarez qui égalise pendant que Benzema galère a mettre son premier à l’euro https://t.co/dfHBvWv3vY   
1497    @LeRenar12908442 @CmoiCdo @BarbosaLXXV @ActuFoot_ @mohamedbouhafsi Je ne souhaite absolument pas la défaite de l’Ed… https://t.co/kLtrgd2bSc   
1498  🇪🇸 @EURO2020 : ESPAGNE-POLOGNE 1-1 (GROUPE E) 🇵🇱\n\n➡️Retour sur le match !\n\n#GroupeE #BK #TeamBK #Soccer #Football… https://t.co/fDNouhMHmy   
1499        Euro-2021 : la France qualifiée pour les huitièmes avant même son dernier match de poule https://t.co/IXJK8I9ZAt https://t.co/nT05ME2Wx3   

                          created_at user.screen_name in_reply_to_screen_name  
1495  Mon Jun 21 22:28:06 +0000 2021        RTBFsport                     NaN  
1496  Mon Jun 21 22:28:02 +0000 2021        Brobbey01                     NaN  
1497  Mon Jun 21 22:27:51 +0000 2021     AlexDybala95         LeRenar12908442  
1498  Mon Jun 21 22:27:41 +0000 2021      BillionKeys                     NaN  
1499  Mon Jun 21 22:27:38 +0000 2021        F24videos                     NaN  

Les 1500 tweets vont du lundi 21 juin 22h 27mn au mardi 22 8h 25mn en heure UTC, soit en heure francaise le mardi 22 entre 00h 27mn et 10h 25mn.

dtf.created_at.min()
dtf.created_at.max()

dtf.created_at =  pd.to_datetime("2021/06/" + dtf.created_at.str.slice(8,19), 
  format = "%Y/%m/%d %H:%M:%S") + dttm.timedelta(hours = 2)
'Mon Jun 21 22:27:38 +0000 2021'
'Tue Jun 22 08:24:57 +0000 2021'

4 Attributs d’un texte

On utilise deux des tweets pour illustrer les differents traitements NLP. Chaque tweet est transforme en la liste de ses composantes (= mots, ponctuations) et le modele qu’on a charge au debut de ce document permet de predire la nature des composantes (nom, verbe), les dependances syntaxiques, les entites nommees …

doc0 = nlp(dtf.text.iloc[5])
doc = nlp(dtf.text.iloc[68])
doc0
doc
Euro: sept nouveaux qualifiés pour les huitièmes, dont la France․.. sans jouer! https://t.co/yZTcg4wtk1 via @nouvelliste
La Belgique va gagner l’Euro!! Regardez la puissance du MOUVEMENT HISTORIQUE 🏴‍☠️🇧🇪

Quelques attributs, cf https://spacy.io/usage/rule-based-matching.

pd.DataFrame([[token.text, token._.is_emoji, token.is_punct, token.is_stop, token.shape_] for token in doc],
columns = ['texte', 'emoji', 'ponctuation', 'stopword', 'forme'])

pd.DataFrame([[token.i, token.text, token.pos_, token.lemma_, token.dep_, token.head.text] for token in doc],
columns = ['index', 'texte', 'nature', 'lemme', 'fontion', 'dependance'])

pd.DataFrame([[token.text, token.like_url, token.is_alpha, token.like_num, token.is_space] for token in doc0],
columns = ['texte', 'url', 'alphanum', 'numerique', 'espace'])
         texte  emoji  ponctuation  stopword  forme
0           La  False        False      True     Xx
1     Belgique  False        False     False  Xxxxx
2           va  False        False      True     xx
3       gagner  False        False     False   xxxx
4           l’  False        False      True     x’
5         Euro  False        False     False   Xxxx
6            !  False         True     False      !
7            !  False         True     False      !
8     Regardez  False        False     False  Xxxxx
9           la  False        False      True     xx
10   puissance  False        False     False   xxxx
11          du  False        False      True     xx
12   MOUVEMENT  False        False     False   XXXX
13  HISTORIQUE  False        False     False   XXXX
14        🏴‍☠️   True        False     False   🏴‍☠️
15          🇧🇪   True        False     False     🇧🇪
    index       texte nature       lemme fontion dependance
0       0          La    DET          le     det   Belgique
1       1    Belgique  PROPN    Belgique   nsubj         va
2       2          va   VERB       aller    ROOT         va
3       3      gagner   VERB      gagner   xcomp         va
4       4          l’    DET          l’     det       Euro
5       5        Euro   NOUN        euro     obj     gagner
6       6           !  PUNCT           !   punct         va
7       7           !  PUNCT           !   punct         va
8       8    Regardez    ADP    regardez    ROOT   Regardez
9       9          la    DET          le     det  puissance
10     10   puissance   NOUN   puissance     obj   Regardez
11     11          du    ADP          de    case  MOUVEMENT
12     12   MOUVEMENT   NOUN   mouvement    nmod  puissance
13     13  HISTORIQUE    ADJ  historique    amod  MOUVEMENT
14     14        🏴‍☠️   NOUN        🏴‍☠️   punct         🇧🇪
15     15          🇧🇪   NOUN          🇧🇪    ROOT         🇧🇪
                      texte    url  alphanum  numerique  espace
0                      Euro  False      True      False   False
1                         :  False     False      False   False
2                      sept  False      True       True   False
3                  nouveaux  False      True      False   False
4                 qualifiés  False      True      False   False
5                      pour  False      True      False   False
6                       les  False      True      False   False
7                 huitièmes  False      True      False   False
8                         ,  False     False      False   False
9                      dont  False      True      False   False
10                       la  False      True      False   False
11                  France․  False     False      False   False
12                       ..  False     False      False   False
13                     sans  False      True      False   False
14                    jouer  False      True      False   False
15                        !  False     False      False   False
16  https://t.co/yZTcg4wtk1   True     False      False   False
17                      via  False      True      False   False
18             @nouvelliste  False     False      False   False

Pour des explications de certaines abreviations.

spacy.explain("nsubj")
spacy.explain("nmod")
spacy.explain("case")
spacy.explain("cop")
'nominal subject'
'modifier of nominal'
'case marking'
'copula'

Les entites reconnues.

pd.DataFrame([[ent.text, ent.label_] for ent in doc.ents], columns =  ['entite', 'nature'])
                 entite nature
0              Belgique    LOC
1              l’Euro!!   MISC
2  MOUVEMENT HISTORIQUE   MISC
spacy.explain("LOC")
spacy.explain("PER")
spacy.explain("MISC")
'Non-GPE locations, mountain ranges, bodies of water'
'Named person or family.'
'Miscellaneous entities, e.g. events, nationalities, products or works of art'

5 Similarites entre tweets

5.1 Matrice de distance

Calcul des distances entre deux tweets par la methode similarity :

  • Chaque composante d’un tweet est transformee par un algorithme de type word2vec en un vecteur d’un espace de grande dimension (par exemple 300)
  • Chaque tweet est transforme en le centre de gravite du nuage des vecteurs des composantes du tweet
  • La distance “cosinus de l’angle” est appliquee aux 2 centres de gravite
tic = timeit.default_timer()
docs = [nlp(dtf.text[k]) for k in range(nb_tweets)]
toc = timeit.default_timer()
toc - tic 

matrice = np.diag(np.ones(nb_tweets))

tic = timeit.default_timer()
for i in range(nb_tweets):
  x = docs[i]
  for j in range(i+1, nb_tweets):
    matrice[i,j] = x.similarity(docs[j])
  for j in range(i):
    matrice[i,j] = matrice[j,i]
toc = timeit.default_timer()
toc - tic 
20.120643400000006
10.2007476

Un exemple de vecteur.

doc[8]
doc[8].vector
Regardez
array([ 0.79321  ,  0.95185  , -1.1069   , -0.45713  ,  0.62773  ,
        1.946    ,  0.46065  , -1.6571   , -0.23949  , -0.18132  ,
       -0.22801  , -0.22944  ,  0.12723  ,  0.088278 ,  1.2252   ,
       -1.0708   ,  0.46161  , -1.1268   , -0.63179  , -0.36074  ,
       -0.38548  ,  0.24211  ,  0.4949   , -0.17168  ,  0.39266  ,
       -0.71156  , -0.5419   ,  1.0834   ,  0.75061  ,  0.52643  ,
        0.11928  ,  0.1293   , -0.23025  , -1.3616   , -0.29727  ,
        1.2538   , -0.23961  ,  1.1043   ,  0.95408  , -1.3896   ,
       -1.5353   ,  0.0088851,  0.15345  , -0.47984  ,  0.8927   ,
        1.0114   , -2.0591   , -0.032777 ,  0.51549  ,  0.93054  ,
       -0.9798   , -2.3503   ,  0.13563  , -1.3584   , -1.1956   ,
        0.20982  , -2.1452   ,  0.42829  , -0.66647  ,  0.069482 ,
       -0.5313   ,  1.7977   , -0.53366  , -0.33859  , -2.0559   ,
        1.5184   ,  0.30163  , -0.61943  ,  0.46154  ,  0.28295  ,
       -0.81895  ,  0.28327  , -0.020459 , -1.094    ,  0.70109  ,
        0.89367  , -0.12854  , -2.5037   , -0.57653  ,  0.94503  ,
        0.12362  ,  0.99971  ,  0.60756  ,  2.651    , -0.21889  ,
       -0.27665  ,  0.89356  , -0.25228  ,  0.13447  ,  1.555    ,
       -2.9365   , -0.32621  ,  1.0715   , -0.60142  , -2.0295   ,
        1.1299   ,  0.096798 ,  0.044715 ,  0.53194  , -1.2044   ,
        0.053932 ,  0.27724  ,  1.3245   , -0.35753  ,  0.33475  ,
        0.082249 ,  0.42002  , -0.23329  , -0.043796 ,  1.5891   ,
        1.0969   , -1.1258   , -1.1286   ,  1.8783   ,  0.66122  ,
       -0.60732  ,  0.062721 , -0.9256   , -1.7266   ,  0.65355  ,
       -0.50595  , -1.7203   ,  0.23784  , -0.45568  , -2.3789   ,
        2.541    ,  0.539    ,  0.99548  , -1.1909   , -0.35436  ,
       -0.066093 ,  1.0007   , -0.23881  , -0.030909 ,  0.18711  ,
        0.12907  ,  0.87953  ,  0.50748  ,  1.8736   , -0.40152  ,
       -0.17476  , -1.0705   , -2.453    ,  0.8858   , -0.23732  ,
        0.42749  ,  0.085325 ,  1.51     ,  1.5668   , -0.16242  ,
       -0.011907 ,  0.11196  ,  0.52844  , -0.26746  ,  0.19964  ,
       -0.81189  ,  0.75148  ,  1.2699   ,  0.59237  , -1.1861   ,
       -0.96782  ,  0.85239  , -0.16554  ,  0.075135 , -0.25966  ,
        0.33738  ,  0.18222  , -1.3915   ,  0.96295  , -0.7081   ,
        1.6575   , -0.36899  ,  0.54342  , -0.96251  ,  1.5675   ,
        1.1882   ,  0.77105  ,  0.1901   ,  0.065428 ,  1.3529   ,
       -0.50661  ,  1.5316   ,  2.0397   ,  0.86335  , -1.1311   ,
        2.2651   , -0.14806  ,  0.3397   ,  2.3393   ,  0.13634  ,
        1.1003   , -0.48229  ,  0.032001 , -2.0498   , -0.24062  ,
        0.24028  , -0.60262  ,  0.40457  , -0.7555   , -0.90226  ,
        1.7338   ,  0.28738  ,  0.29228  ,  1.04     , -2.517    ,
        0.18479  ,  0.053454 , -1.6878   ,  0.66571  , -2.2276   ,
       -0.91446  ,  0.699    ,  2.5515   , -1.2558   , -2.2668   ,
       -0.77857  , -0.45876  ,  0.45907  ,  1.2054   , -1.0605   ,
       -0.02248  , -0.6608   ,  1.4666   , -1.3927   , -1.3929   ,
       -0.64604  ,  1.2649   , -0.81053  ,  0.61458  , -0.12841  ,
       -0.11389  ,  0.14234  ,  0.33481  ,  1.1168   ,  0.64395  ,
       -1.0912   , -1.4982   , -0.41059  ,  2.3496   , -2.5213   ,
       -2.7719   ,  1.1356   , -0.031763 , -1.0589   , -0.081639 ,
       -0.37828  ,  0.45378  ,  0.92991  ,  0.27377  ,  0.050234 ,
        1.6672   , -0.95015  ,  0.29714  , -1.5286   ,  1.2891   ,
       -0.1645   , -1.3745   ,  0.74126  , -1.2292   ,  0.37063  ,
        0.095942 ,  0.7153   , -0.90496  , -0.76003  ,  0.18375  ,
        1.4571   , -0.53981  , -0.29554  ,  0.67795  ,  1.0498   ,
        0.47981  ,  0.7117   , -0.50379  , -1.2355   ,  0.94785  ,
       -0.91392  ,  0.23728  ,  0.64474  , -0.61146  , -0.63721  ,
        1.8344   ,  1.2227   , -0.46766  ,  0.84134  ,  0.70637  ,
       -0.29943  ,  0.076421 , -1.3627   , -0.12433  , -0.11689  ,
       -0.32179  ,  0.89101  , -0.51407  , -1.5856   , -0.80922  ,
       -1.6279   , -0.21633  ,  1.8286   ,  0.83546  , -0.52643  ],
      dtype=float32)

Tweets similaires ou tres differents.

matrice.max()
print("----------------------")
a, b = np.unravel_index(matrice.argmax(), matrice.shape)
docs[a]
print("----------------------")
docs[b]
print("----------------------")
docs[a].similarity(docs[b])

# quelquefois des petits pbs d'arrondis qui font planter la suite
matrice[matrice > 1] = 1
1.0000000800123225
----------------------
Le prix a augmenté 😁
Valeur actuelle du Bitcoin: €27508.54 à 09:47
Variation 📈 (+0.07%)
----------------------
Le prix a augmenté 😁
Valeur actuelle du Bitcoin: €27488.61 à 09:32
Variation 📈 (+0.36%)
----------------------
1.0000000800123225
matrice.min()
print("----------------------")
a, b = np.unravel_index(matrice.argmin(), matrice.shape)
docs[a]
print("----------------------")
docs[b]
print("----------------------")
docs[a].similarity(docs[b])
-0.5968111558796538
----------------------
| #LastminuteAmeland #Ameland
| chalet Chalet T46
|  399 Euro
| vr 02/07 - vr 09/07
| https://t.co/YxGn4DIzDj https://t.co/188R6Sejtz
----------------------
@Manuctn10 Surtout qu’à un moment c’est pas comme si on avait fait un Euro enormissime incroyable on a surtout bien serré les fesses
----------------------
-0.5968111558796538

5.2 CAH

On choisit de separer les twets en 3 segments.

link = linkage(squareform(1 - matrice, force = 'tovector', checks = False), 'ward')

# dendrogramme
# coupures a 3 clusters
plt.figure(figsize=(25, 10))
_ = dendrogram(link)
plt.show()

k = 3
clusters = pd.Series(fcluster(link, k, criterion = 'maxclust'))
dtf["cluster"] = list(clusters)

# taille des clusters
clusters.value_counts()
<Figure size 2500x1000 with 0 Axes>

3    703
1    632
2    165
dtype: int64

5.3 Description des segments

La repartition temporelle des tweets semble assez proche entre les differents segments.

dtf.created_at.hist(by = dtf.cluster, xlabelsize = 4, xrot = 0, layout = (4,1), bins = 20);
plt.show()

Mots-cles les plus frequents dans chaque cluster apres suppression des termes inutiles.

dtf_stats = pd.concat([pd.DataFrame([[ligne, clusters[ligne], token.lemma_, token._.is_emoji, token.like_url, token.is_punct, token.is_stop, token.is_space] 
for token in docs[ligne] if (not token.is_stop and not token.is_punct and not token.like_url and not token._.is_emoji and not token.is_space)],
columns = ['tweet_id', 'cluster', 'lemme', 'emoji', 'url', 'ponctuation', 'stopword', 'espace']) for ligne in range(nb_tweets)])

# on enleve les fils
dtf_stats["fil"] = dtf_stats.lemme.str.startswith("@")
dtf_stats = dtf_stats[~dtf_stats["fil"]]
dtf_stats = dtf_stats[['tweet_id', 'cluster', 'lemme']]

# dedup
dtf_stats.drop_duplicates(inplace = True)

top10 = dtf_stats.groupby(["cluster", "lemme"]).size().reset_index(name = 'freq').sort_values(['cluster', 'freq'], ascending = False)
top10.groupby("cluster").head(10)
      cluster      lemme  freq
3404        3       euro   329
2744        3       Euro   194
2762        3     France   112
3450        3     finale   105
2602        3       2021   100
4000        3   qualifié    97
3560        3   huitième    96
2600        3       2020    75
3729        3      match    66
2670        3   Belgique    65
2287        2       euro    73
2536        2     valeur    41
2054        2       Euro    40
2021        2    Bitcoin    39
2167        2     actuel    39
2451        2       prix    39
2540        2  variation    39
1922        2       2020    31
2048        2       EURO    21
2185        2  augmenter    20
753         1       euro   402
172         1       Euro    47
1099        1      match    47
1767        1          y    43
849         1     gagner    32
1799        1     équipe    32
182         1     France    30
780         1    falloir    29
779         1      faire    28
1030        1          l    26

On obtient une premiere vision des segments

  • cluster 1 de 632 tweets : il semble concerner principalement l’equipe de France
  • cluster 2 de 165 tweets : il porte sur la monnaie “euro”, pas la competition “Euro”
  • cluster 3 de 703 tweets : il est question de plusieurs equipes, de differentes annees et de plusieurs phases de la competition

Pour mieux interpreter les segments on tire aleatoirement 10 tweets de chaque segment.

# tirage aleatoire de chaque cluster
extraits = dtf[["cluster", "user.screen_name", "text"]]

extraits_tweets = extraits.groupby("cluster").sample(10, random_state = 1234)

"cluster " + extraits_tweets.cluster.astype(str) + " - user " + extraits_tweets[
  'user.screen_name'] + " - " + extraits_tweets.text
1338        cluster 1 - user jisunglazed - et dire que l'année prochaine on pourra plus parler d'animes en cours d'anglais parce qu'elle arrête la section euro... déjà envie de crever
597           cluster 1 - user alyly_222 - J'entend un français dire que la Belgique est nulle parce qu'elle a pas gagné la Coupe du monde je lui rappelle  qu… https://t.co/zJIxWhrrSO
1105        cluster 1 - user constantscln - @ledoumbe J’ai juré le duel Benzema-Giroud c’est plus important à leurs yeux qu’un titre à l’Euro, ils veulent une… https://t.co/GOyCEvD7uB
855     cluster 1 - user 20_times_I_Said - @CremDyl @FrereAmelie @ElevenSportsBEf ces phase de poule ne ressemblent à rien. pour faire ca autant qualifier plu… https://t.co/DENSmvNJB4
1017        cluster 1 - user Akrapo_ - français: je le parle déjà\ntechnologie: j'ai déjà un ordi\nhistoire: ils sont tous morts\ngéographie: j'ai un GPS\n mu… https://t.co/UeGG2ohr08
446         cluster 1 - user Elow__06340 - @leperedeCherrad @Yoyoslim1 Offensivement on a vu personne depuis le début de l’euro. Facile de tomber QUE sur Benz… https://t.co/iOmCbmA5Vy
1229                                                                          cluster 1 - user Joao92_Hds - Jsuis sur les champs la pendant 2sec j'ai cru l'Algérie avais gagner l'euro
1456          cluster 1 - user JuniorRvp - @midouly Ahh quand tu me parles de profil on peut être d’accord mais côté niveau il n’y a jamais eu débat. La seule… https://t.co/TL574i2Evz
437         cluster 1 - user antoinevidu - @JulienDenoel @adrienrenkin 95% de centres ratés alors que c'est le seul truc offensif qu'il essayait, c'est pas fo… https://t.co/L4FKxKxfID
616     cluster 1 - user CaptainArmorica - @kamastaw C'est vraiment un bel euro au contraire je trouve. Et même les plus petites nations sont intéressantes pa… https://t.co/7N5woYGizE
655                                                                cluster 2 - user Nabilaouais - 0€ euro d’amende pendant confinement et couvre feu depuis ca existe ! Formidable hmdl
331         cluster 2 - user Network_Easy - EURO 2020 RUSSIE-DANEMARK Groupe B: Hello les amis me voila lance dans ma nouvelle aventure dans PES 21 nous voila… https://t.co/4HTJtmWklH
1418      cluster 2 - user AssohNicaise - 🥳🧐NE PANIQUER  PLUS\n     Logiciel : N-SOS , vient a votre  secoure  et  vous  sort de la…\n  B N : les appareils ne… https://t.co/NL234f2Bxk
1219                                                               cluster 2 - user euro_btc - Le prix a augmenté 😁\nValeur actuelle du Bitcoin: €26734.3 à 01:30\nVariation 📈 (+0.54%)
676                                                                              cluster 2 - user d3_ghie - Final de l'euro \nFrance 🇲🇫 vs L'Allemagne 🇩🇪\n#France #Allemagne #EURO2020
733                                                  cluster 2 - user DussolAlexis - #Thaïlande | Un #lion prédit les #matchs de l'Euro du week-end | La Presse https://t.co/U3Oo2w3dvu
753                                                           cluster 2 - user giuloc - Euro 2020 - La belle leçon d’italien du professeur Mancini - 20 minutes https://t.co/bGldIBm79E
708       cluster 2 - user MrGyzmo84 - @ID7Kun Si on reprend le contexte : \n\n- L'Euro se déroule dans plusieurs pays (c'est nul !!).\n- La chaleur.\n- La pa… https://t.co/5HEBLjSrdI
118                                                                  cluster 2 - user monnaiecollect1 - VALEUR 2 EURO ALLEMAGNE 2010 D 6 300 000 ? https://t.co/NWA5BUhhDM via @YouTube
640                                              cluster 2 - user monnaiecollect1 - POURQUOI cette Pièce de 1 EURO FRANCE 2001 vaut plus de 700€ ? https://t.co/jjOzESthWt via @YouTube
668                                                                                              cluster 3 - user Jordydu59120 - Imaginez Lille gagne l’euro 🧐😂 https://t.co/L7LxXF0lIG
628                                                                               cluster 3 - user fhardes - @GegeStyle On a besoin de sommeil entre euro et nba. Première nuit de 8h!!
63          cluster 3 - user Thb_Caillet - Vegedream à la conquête de l'Europe! "Ramenez la coupe à la maison" dans le top 50 Shazam monde grâce à son émergen… https://t.co/eEXuwr2YwK
147                                                                      cluster 3 - user actudesbleus - K. Coman : Oh non Dembouz ne peut plus faire l’euro 😢😞 https://t.co/367ncsiFin
1483          cluster 3 - user LoopHaiti - Près de la moitié des places en huitièmes de finale ont été attribuées lundi, avec sept sélections qualifiées en un… https://t.co/MaX1pclsid
630                           cluster 3 - user franceinfo - Euro 2021 : le difficile travail de mémoire du onze d’or hongrois, mythe déchu du football mondial… https://t.co/f3NmrU5k4h
370                                                                cluster 3 - user sports_ouest - Euro 2021. Un hommage à Philousports avant France-Portugal ? https://t.co/uMPepLLdSX
1245                                                                                           cluster 3 - user ImNotReda - @IssaLhd full maquillage aussi fake que la turquie a l’euro
40               cluster 3 - user 24matins_sport - Euro : l’Angleterre vise la tête, la Croatie joue la sienne https://t.co/s8oX4pOFxX #Foot #Croatie #Euro2020 https://t.co/daWtZDPYoA
874        cluster 3 - user MarieMarilou_ - Jeremy Doku sur son petit nuage après son premier match à l'Euro 2020: "Une bonne expérience pour moi" - RTL sport… https://t.co/inTZlRDbJu
dtype: object

On voit des differences plus precises entre segments

  • cluster 1 : des tweets ecrits sur l’Euro par des particuliers, avec beaucoup de “je”
  • cluster 2 : un melande de tweets sur l’Euro et de tweets qui parle d’argent (amende, prix, collectionneur de pieces)
  • cluster 3 : des tweets sur l’Euro rediges par des journalistes (RTL sport, actudesbleus, franceinfo, sports_ouest, …) au ton plus neutre, “Euro 2021” et “Euro 2020” ressortent deux fois chacun

On a ainsi obtenu une segmentation de bonne qualite alors que la situation n’etait pas simple

  • les textes sont tronques a 140 caracteres par l’API de Twitter
  • les tweets peuvent etre tres mal orthographies : “Jsuis sur les champs la pendant 2sec j’ai cru …”
  • on applique la methode de Ward, qui attend une metrique euclidienne, a des cosinus entre centres de gravite d’embeddings par reseaux de neurones !
  • on n’a meme pas applique de pretraitement comme la suppression des mots inutiles
  • les auteurs du module spaCy indiquent que la similarite entre documents est un sujet difficile en NLP : https://spacy.io/usage/linguistic-features#similarity-expectations

6 Clustering des tweets avec Top2vec

Cet algorithme effectue toutes les taches necessaires a un clustering sans aucun parametre a ajuster par l’utilisateur. Dans le cas des tweets le traitement s’avere malheureusement decevant. Pretraiter les tweets en enlevant les stopwords, url, et emoji et en lemmatisant les termes restants ameliore partiellement la situation.

Attention car l’algorithme n’est pas deterministe, cf https://github.com/ddangelov/Top2Vec/issues/86. Lancer plusieurs fois la commande Top2Vec donnera des nombres de clusters (=“topics”) differents ! Il faut donc sauvegarder le modele sur disque une fois cree.

Les tweets sans et avec pretraitement.

texte_brut = list(dtf.text)

# ss les stopwords
texte_nettoye = [" ".join([str(token.lemma_) for token in docs[ligne] if (
  not token.is_stop and not token.is_punct and not token.like_url and not token._.is_emoji and not token.is_space)]) for ligne in range(nb_tweets)]
  
texte_brut[0]
texte_nettoye[0]
'@DidiDeschamps tu as la meilleure équipe au monde tu as droit à remporter cet euro'
'@didideschamp meilleur équipe monde droit remporter euro'

Les deux modeles.

modele_brut = Top2Vec(texte_brut)
modele_nettoye = Top2Vec(texte_nettoye)
2021-08-03 17:44:40,535 - top2vec - INFO - Pre-processing documents for training
2021-08-03 17:44:40,638 - top2vec - INFO - Creating joint document/word embedding
2021-08-03 17:44:43,769 - top2vec - INFO - Creating lower dimension embedding of documents
2021-08-03 17:44:58,062 - top2vec - INFO - Finding dense areas of documents
2021-08-03 17:44:58,177 - top2vec - INFO - Finding topics
2021-08-03 17:44:58,195 - top2vec - INFO - Pre-processing documents for training
2021-08-03 17:44:58,256 - top2vec - INFO - Creating joint document/word embedding
2021-08-03 17:45:00,748 - top2vec - INFO - Creating lower dimension embedding of documents
2021-08-03 17:45:07,993 - top2vec - INFO - Finding dense areas of documents
2021-08-03 17:45:08,103 - top2vec - INFO - Finding topics

Nombre et taille de clusters (entre 2 et 8 clusters en general sur ces donnees).

topic_sizes, topic_nums = modele_brut.get_topic_sizes()
pd.DataFrame({"num_cluster": topic_nums, "taille_cluster": topic_sizes})

topic_sizes, topic_nums = modele_nettoye.get_topic_sizes()
pd.DataFrame({"num_cluster": topic_nums, "taille_cluster": topic_sizes})
   num_cluster  taille_cluster
0            0             585
1            1             466
2            2             449
   num_cluster  taille_cluster
0            0             826
1            1             674

Echantillons de tweets avec un score de representativite decroissant.

tweets_brut = pd.DataFrame(columns = ["num_cluster", "tweet", "score"])

for i in range(modele_brut.get_num_topics()):
  documents, document_scores, document_ids = modele_brut.search_documents_by_topic(topic_num = i, num_docs = 5)
  tweets_brut = pd.concat([tweets_brut, pd.DataFrame({"num_cluster" : i, "tweet": documents, "score": document_scores})])

"cluster " + tweets_brut.num_cluster.astype(str) + " - score " + tweets_brut[
  'score'].astype(str) + " - " + tweets_brut.tweet
0                                                                                                                  cluster 0 - score 0.31573457 - @L_BLK413 surprise de l’euro✨✨
1    cluster 0 - score 0.25234914 - @KobyCujoh @clementdetp Quand bien même la Belgique lapiderait la France à l'Euro y'aurait toujours 0 CDM et le tatouage sera encore valable
2     cluster 0 - score 0.23729327 - Euro 2021 : MON PRONO DANEMARK RUSSIE !: Voici mon pronostic pour le match entre le Danemark et la Russie ! Qui va… https://t.co/xhgoAsKvUL
3     cluster 0 - score 0.23663124 - Cet euro reste un bon exercice avant d'affronter la France, le 7 octobre en demi-finale de  Nations League #belfra… https://t.co/YCI5nAjCQZ
4    cluster 0 - score 0.2331247 - Il a une carrière le pauvre !! Apres ouinil il a fait bcp d abus... Mais ca explique pas ttes ses blessures....\nBon… https://t.co/yrJxScmdTo
0    cluster 1 - score 0.33373678 - @Plumosaure2 @depaylegoat @Sashimig @AngeDiMarinho Quel raisonnement ne tient pas la route ? Si tu me dis que c'est… https://t.co/WBvyknw2xN
1                                                             cluster 1 - score 0.30442056 - Euro 2021: le fond, mais pas la forme pour la Nati https://t.co/ecEolRF7AE @arcinfo
2                                                  cluster 1 - score 0.3007415 - On a gagné la big cdm sans lui je doute qu’on ai besoin de lui a l’euro https://t.co/2Rlr3p5pf8
3    cluster 1 - score 0.27668858 - Finlande-Belgique Euro 2021: Les Belges finissent le boulot, la Finlande éliminée après avoir bien résisté... Le ma… https://t.co/PHc9JarNQj
4    cluster 1 - score 0.27665848 - @AfterRMC @DanielRiolo hé Daniel  RIOLO  au.lieu  de te masturber sur le nom de neymar  mbappé  branle quoi  à l'eu… https://t.co/coJduf2nmc
0       cluster 2 - score 0.320181 - @Itadorijj @ActuBarcaFR Télécharge l'application Star Times et tu regardes le match sans problème sans pub pendant… https://t.co/Uzy1OniOh2
1     cluster 2 - score 0.29392558 - L'UEFA interdit finalement au stade de Munich d'être éclairé aux couleurs LGBT+ pour le match Allemagne-Hongrie.\n\nhttps://t.co/J7Ow3E7fQe
2                                                       cluster 2 - score 0.27663442 - Le prix a augmenté 😁\nValeur actuelle du Bitcoin: €27485.48 à 04:01\nVariation 📈 (+0.23%)
3    cluster 2 - score 0.27619216 - @migwa971 @Paristeamfr Il a déjà parlé et a dit clairement ce qu’il avait à dire et en ce moment il est concentré s… https://t.co/SIzJOBOfcE
4     cluster 2 - score 0.26978457 - @RMadridHebdo @SafSafizi28 @AlfredoPedulla J'aimerai tellement qu'il vienne cet été,personne d'autre si il y'a une… https://t.co/XkK2t2aAM0
dtype: object
tweets_nettoye = pd.DataFrame(columns = ["num_cluster", "tweet", "score"])

for i in range(modele_nettoye.get_num_topics()):
  documents, document_scores, document_ids = modele_nettoye.search_documents_by_topic(topic_num = i, num_docs = 5)
  tweets_nettoye = pd.concat([tweets_nettoye, pd.DataFrame({"num_cluster" : i, "tweet": documents, "score": document_scores})])

"cluster " + tweets_nettoye.num_cluster.astype(str) + " - score " + tweets_nettoye[
  'score'].astype(str) + " - " + tweets_nettoye.tweet
0    cluster 0 - score 0.207342 - @sofoot @RougeDirect lamentable boycotton Hongrie Orban pays piétiner valeur fraternité
1                    cluster 0 - score 0.203793 - prix diminuer valeur actuel Bitcoin euro 27363.39 03:30 variation -1.14
2                 cluster 0 - score 0.1944428 - @marwan_xv @bigrom71289687 @l_interist vouloir cest quils favori gagner l
3                           cluster 0 - score 0.18286806 - @OthFCB @franekfoot voir niveau euro pue merde rajouter équipe
4                                 cluster 0 - score 0.17554188 - @_befoot vouloir kanté ballon or saison demi teinte dire
0                                            cluster 1 - score 0.40033513 - euro-2021 danemark héroïque Autriche qualifié
1                               cluster 1 - score 0.36257046 - @xima597 @NeymarLeRoiXMb7 Hein cite gros équipe jouer copa
2                                                        cluster 1 - score 0.3499338 - Philippe clement Belgium EURO 2000
3                     cluster 1 - score 0.3280332 - @zaaaklaklaaa attendre adversaire claquer euro faire avis debile ca f
4                                                cluster 1 - score 0.3107319 - @issalhd full maquillage fake turquie euro
dtype: object

Mots les plus frequents de chaque cluster.

topic_words, _, topic_nums = modele_brut.get_topics(modele_brut.get_num_topics())
"cluster " + pd.Series(topic_nums.astype(str)) + " - " + pd.Series(map(" ".join, topic_words))
0    cluster 0 - avant qualifies euro avec ils fait des un qu en danemark huitiemes le deja equipe que ce la dans on et les ou si co va via jouer gagne je meme plus au qualifiee fin...
1    cluster 1 - on sans fait via que une co pas va meme mais si qualifies deja ca dans le qualifiee vous apres france huitiemes equipe finale ne match de son danemark cet ai pour i...
2    cluster 2 - match dans deja euro qu vous au avant le se des tout ils de ce pas bleus mais via ne pour je finale apres les et avec son on belgique danemark si qui huitiemes qual...
dtype: object
topic_words, _, topic_nums = modele_nettoye.get_topics(modele_nettoye.get_num_topics())
"cluster " + pd.Series(topic_nums.astype(str)) + " - " + pd.Series(map(" ".join, topic_words))
0    cluster 0 - finale deja danemark equipe huitieme gagner euro qualifier qualifie match belgique jouer france
1    cluster 1 - jouer match gagner equipe danemark deja belgique huitieme euro qualifie finale qualifier france
dtype: object

Les nuages de mots. Sans pretraitement les clusters font ressortir trop de stopwords, avec pretraitement c’est mieux mais les differents clusters sont tres proches les uns des autres, leurs mots-cles etant “france, danemark, belgique, euro, equipe, match, huitieme, final, qualifier, jouer, gagner”.

for topic in range(modele_brut.get_num_topics()):
    modele_brut.generate_topic_wordcloud(topic)
  
for topic in range(modele_nettoye.get_num_topics()):
    modele_nettoye.generate_topic_wordcloud(topic)
    
pp = PdfPages(os.path.join(os.getcwd(), 'graphiques_NLP', 'nuage_brut.pdf'))
for i in range(modele_brut.get_num_topics()):
  plot_topic = modele_brut.generate_topic_wordcloud(i)
  pp.savefig(plot_topic)
pp.close()

pp = PdfPages(os.path.join(os.getcwd(), 'graphiques_NLP', 'nuage_nettoye.pdf'))
for i in range(modele_nettoye.get_num_topics()):
  plot_topic = modele_nettoye.generate_topic_wordcloud(i)
  pp.savefig(plot_topic)
pp.close()

Le nuage de mots des tweets bruts.

Le nuage de mots des tweets lemmatises.

7 Reseau des tweetos

Preparation des donnees.

reseau = dtf.copy()[["user.screen_name", "in_reply_to_screen_name"]]
reseau.dropna(inplace = True)

reseau = reseau.groupby(["in_reply_to_screen_name", "user.screen_name"]).size().reset_index(name = "freq")
reseau.rename(columns = {"in_reply_to_screen_name": "Target",
                          "user.screen_name": "Source",
                          "freq": "Weight"
                          }, inplace = True)

# on exclut les aretes isolees du graphe
target_unique = reseau.groupby("Source").size().reset_index(name = "nb_target")
target_unique = target_unique[target_unique.nb_target == 1]
source_unique = reseau.groupby("Target").size().reset_index(name = "nb_source")
source_unique = source_unique[source_unique.nb_source == 1]

reseau = reseau[~(reseau.Source.isin(target_unique.Source) & reseau.Target.isin(source_unique.Target))]

Construction du reseau.

G = Network(directed = True, width = '100%')

G.force_atlas_2based()
G.set_edge_smooth('dynamic')

for i in range(len(reseau)):
  src = reseau.Source.iloc[i]
  dst = reseau.Target.iloc[i]
  # conversion numpy --> Python pour eviter plantage
  w = reseau.Weight.iloc[i].item()
  
  if reseau.Target.iloc[i] == 'ActuFoot_':
    G.add_node(src, color = 'firebrick')
    G.add_node(dst, color = 'firebrick')
  elif reseau.Target.iloc[i] == 'EspoirsduFoot':
    G.add_node(src, color = 'deeppink')
    G.add_node(dst, color = 'deeppink')
  elif reseau.Target.iloc[i] == 'RMCsport':
    G.add_node(src, color = 'navy')
    G.add_node(dst, color = 'navy')
  elif reseau.Target.iloc[i] == 'WilooFootball':
    G.add_node(src, color = 'blue')
    G.add_node(dst, color = 'blue')
  elif reseau.Target.iloc[i] == 'WinamaxSport':
    G.add_node(src, color = 'orange')
    G.add_node(dst, color = 'orange')
  elif reseau.Target.iloc[i] == '_BeFoot':
    G.add_node(src, color = 'red')
    G.add_node(dst, color = 'red')  
  else:
    G.add_node(src)
    G.add_node(dst)
  
  G.add_edge(src, dst, value = w)
  
G.write_html(os.path.join(os.getcwd(), 'graphiques_NLP', 'visu_network.html'))

Le graphique montre que les principaux “influenceurs” sont ActuFoot_, EspoirsduFoot, RMCsport, WinamaxSport, WilooFootball et _BeFoot. On peut zoomer sur ce graphique et deplacer certains noeuds pour etudier de plus pres les relations entre certains auteurs des tweets.

0%

retour au debut du document