Ce document présente le modèle wordfish. Développés par des chercheurs en sciences politiques, ce modèle vise à déterminer les oppositions idéologiques des acteurs étudiés. Les premiers travaux utilisant le modèle s’intéressent aux évolutions des positionnements des partis politiques en Allemagne, en Grande-Bretagne, au Japon, en s’appuyant sur les manifestes des partis politiques. Nous trouverons à cette adresse quelques publications des auteurs du modèle. Le modèle permet de comparer les textes entre eux, sur la base des mots employés. Comparé à d’autres modèles de classification/comparaison de documents, celui-ci ne nécessite pas de connaître les positions a priori des acteurs, ni de connaitre les positions de départ. Au contraire, ce modèle se fonde sur la fréquence d’apparition des termes dans un discours pour le comparer aux autres discours et en déterminer sa position relative. Le modèle permet de représenter sur une échelle unidimensionnelle les positions des termes en fonction de leurs occurrences. Il est considéré que le nombre d’occurrence du mot j dans le discours de l’acteur i au temps t est fonction du contexte dans lequel le discours est produit, des effets propres du mot, de l’importance du mot pour discriminer la position de l’acteur et de la position de l’acteur au temps t. D’une manière formelle, nous obtenons : \[y_{i j t} = Poisson (\lambda_{i j t})\\ \lambda_{i j t} = exp(\alpha_{i t} + \psi_{j} + \beta_{j}*\omega_{i t})\] Avec : \(y_{i j t}\) , le nombre d’occurrences du terme j dans le discours d’un acteur i au temps t, \(\alpha_{i t}\), les effets du contexte sur l’acteur i au temps t, \(\psi_{j}\), les effets propres du terme j, \(\beta_{j}\), un estimateur de la spécificité du poids du terme j qui permet d’évaluer l’importance du terme pour discriminer les acteurs entre eux, \(\omega_{i t}\), la position estimée de l’acteur i au temps t.
Nous pouvons utiliser ce modèle pour déterminer les positionnements de différents acteurs entre eux, ainsi que leur évolution dans le temps.
Nous allons utiliser le modèle pour déterminer les positions des acteurs dans le conflit qui a pu opposer Uber à la ville de Paris, aux taxis et aux chauffeurs VTC. Les données collectées représentent les positions publiques, exprimées à travers des communiqués de presse, des posts de blog et des compte-rendus de débats, des différents acteurs identifiés dans le conflit. La période étudiée va de 2013 à 2017. Nous renvoyons les lecteurs intéressés par le détail des données à la thèse bientôt publiée de l’auteur de ce document.
library(tidyverse) #le langage
library(tidytext) #le langage pour le texte
library(quanteda) #le traitement des corpus
library(lubridate) #la gestion des dates
library(reshape2) #la transformation des données (pour les graphiques notamment)
library(wordcloud) # les nuages de mots
On pourra se référer à cette page pour l’import des données avec le package quanteda.
tbl_french <- list.files(pattern = "*.txt",recursive = TRUE, include.dirs = TRUE ) %>%
map_chr(~ read_file(.)) %>%
data_frame(text = .)%>%
mutate(doc=list.files(pattern="*.txt",recursive=TRUE))%>% #on crée une variable d'identification des documents
separate(doc, c("acteur","date"))%>% # variables acteur et date créées à partir de l'identification des documents
mutate(date=ymd(date, truncated=1))%>% # on modifie le format de la date
mutate(year=year(date))%>% #on extrait l'année
mutate(document=paste0("text", c(1:228) )) # on numérote les documents
tbl_french
## # A tibble: 228 x 5
## text acteur date year document
## <chr> <chr> <date> <dbl> <chr>
## 1 "Taxi/VTC : AlloCab.com au cœur des né~ Alloc~ 2013-03-13 2013 text1
## 2 "Taxi-VTC : Décret des 15 minutes susp~ Alloc~ 2013-05-02 2013 text2
## 3 "le 12 août 2013\r\n\r\nLes 15 minutes~ Alloc~ 2013-08-12 2013 text3
## 4 " le 17 octobre 2013\r\nTaxis vs VTC :~ Alloc~ 2013-10-17 2013 text4
## 5 "le 18 octobre 2013\r\nLe mouvement le~ Alloc~ 2013-10-18 2013 text5
## 6 "\r\nLes 15 Minutes des VTC : Allocab.~ Alloc~ 2014-01-13 2014 text6
## 7 "Les VTC plébiscités par les francilie~ Alloc~ 2014-04-14 2014 text7
## 8 "Rapport Thévenoud : sur la voie de l’~ Alloc~ 2014-04-24 2014 text8
## 9 "Les VTC exclus des voies réservées su~ Alloc~ 2014-05-28 2014 text9
## 10 "AlloCab.com défend l’activité des VTC~ Alloc~ 2014-09-01 2014 text10
## # ... with 218 more rows
#on récupère le dictionnaire des stopwords en français
sw<-get_stopwords("fr")
#on ajoute les mots manquants
sw<- bind_rows(data_frame(word = c("ville","l'autorité","a","c'est","n'est","d'un","d'une","n'a", "publié","parnathanaelle","leshem"),
lexicon = c("custom")),
sw)
#on transforme en format dictionnaire
swdic<-rename(sw, sentiment=lexicon)
swdic<-as.dictionary(swdic)
La fonction corpus() permet de transformer les données importées en corpus. Nous pouvons alors extraire les termes utilisés (tokens). Pour alléger le traitement, nous enlevons les stopwords et regroupons les termes qui apparaissent majoritairement ensemble. Nous pouvons ensuite transformer ces données en matrice documents-features, qui croise les documents et les occurrences des tokens.
#corpus
corpus<-corpus(tbl_french)
corpus
## Corpus consisting of 228 documents and 4 docvars.
#tokens
tok<-corpus%>%
tokens(remove_numbers = TRUE, remove_punct=TRUE, include_docvars = TRUE) %>%
tokens_tolower()%>%
tokens_remove(swdic)
#les mots apparaissant l'un après l'autre fréquemment
cap_toks <- tokens_select(tok, valuetype = 'regex', case_insensitive = FALSE, padding = TRUE)
cap_col <- textstat_collocations(cap_toks, min_count = 3, tolower = FALSE)
head(cap_col, 20)
## collocation count count_nested length lambda
## 1 madame maire 102 0 2 7.130781
## 2 mise place 70 0 2 6.492248
## 3 millions d'euros 75 0 2 8.212048
## 4 voie publique 71 0 2 8.639470
## 5 monsieur maire 60 0 2 5.471119
## 6 mme maire 58 0 2 5.420064
## 7 service public 49 0 2 5.982750
## 8 adjoint président 58 0 2 8.516859
## 9 prise charge 68 0 2 9.180832
## 10 président merci 44 0 2 6.276477
## 11 l'application uber 50 0 2 5.435656
## 12 merci madame 43 0 2 5.694768
## 13 transport personnes 47 0 2 5.204435
## 14 organisations professionnelles 51 0 2 8.905848
## 15 député thévenoud 34 0 2 6.538606
## 16 maire chers 45 0 2 6.511380
## 17 réservation préalable 60 0 2 8.499617
## 18 chauffeurs taxis 92 0 2 3.224827
## 19 proposition loi 44 0 2 5.779246
## 20 préfecture police 34 0 2 8.175681
## z
## 1 38.55068
## 2 35.52157
## 3 32.88583
## 4 32.42845
## 5 32.13760
## 6 31.58787
## 7 30.50014
## 8 30.35849
## 9 29.74271
## 10 29.52744
## 11 29.08181
## 12 28.77519
## 13 28.46932
## 14 27.48460
## 15 27.39897
## 16 27.38289
## 17 27.33616
## 18 27.17804
## 19 26.70940
## 20 26.46732
tok <- tokens_compound(tok, cap_col[cap_col$count > 10])
rm(cap_toks,cap_col)
head(tok$text101, 20)
## [1] "commission" "professionnelle" "taxi_parisien"
## [4] "décembre" "l'incompréhension" "totale"
## [7] "vivons" "tous" "taxi"
## [10] "manière" "préfecture" "vient"
## [13] "encore" "démontrer" "favorisant"
## [16] "création" "nouveaux" "doublages"
## [19] "contexte_économique" "morose"
#document-features matrix
dfm<-dfm(tok)
dfm
## Document-feature matrix of: 228 documents, 15,969 features (98.3% sparse).
#on groupe le dfm par acteur
dfmgroup<-dfm_group(dfm,groups = "acteur")
dfmgroup
## Document-feature matrix of: 8 documents, 15,969 features (77.9% sparse).
#on transforme pour ne garder que les 40 les plus importants pour chaque acteur (graphique trop gros sinon)
d<-tidy(dfmgroup)%>%
group_by(document)%>%
top_n(40)%>%
ungroup%>%
acast(term ~ document, value.var = "count", fill = 0)
#wordcloud
comparison.cloud(d,scale=c(2.5,0),
colors = brewer.pal(8,"Set2"),
title.size = 1,
use.r.layout = T,
title.bg.colors = NULL,
title.colors = "black")
#on groupe en fonction des acteurs et des années
dfmgroup<-dfm_group(dfm,groups = c("acteur","year"))
#le modèle wordfish
wf <- textmodel_wordfish(dfmgroup)
summary(wf)
##
## Call:
## textmodel_wordfish.dfm(x = dfmgroup)
##
## Estimated Document Positions:
## theta se
## Allocab.2013 0.253825 0.016934
## AutoritedelaConcurrence.2013 0.569488 0.016336
## CGT.2013 0.525453 0.008220
## UNIT.2013 0.315736 0.022592
## Allocab.2014 0.466104 0.010990
## CGT.2014 0.652792 0.002481
## ConseildeParis.2014 0.108230 0.006949
## UNIT.2014 0.439014 0.023072
## Allocab.2015 0.437370 0.021515
## AutoritedelaConcurrence.2015 0.462915 0.011412
## CFDT.2015 0.033061 0.037161
## CGT.2015 0.549698 0.003782
## ConseildeParis.2015 0.064664 0.007647
## FNAT.2015 0.460088 0.009784
## Uber.2015 -2.367378 0.025675
## UNIT.2015 0.381867 0.031445
## AutoritedelaConcurrence.2016 0.427943 0.022189
## CFDT.2016 -0.178917 0.023628
## CGT.2016 0.537610 0.003914
## ConseildeParis.2016 0.127276 0.010814
## Uber.2016 -3.041858 0.015384
## UNIT.2016 0.538355 0.011718
## CFDT.2017 0.009599 0.016620
## CGT.2017 0.577965 0.006986
## ConseildeParis.2017 0.157221 0.009370
## Uber.2017 -2.508120 0.014060
##
## Estimated Feature Scores:
## taxi vtc allocab.com cœur négociations l'assemblée nationale
## beta 6.0611 5.6418 0.6077 -0.08334 0.656 2.784 4.480
## psi 0.3779 0.8801 -0.3316 -1.31842 -1.446 -2.044 -0.727
## paris mars jour président-fondateur d'allocab.com rend
## beta -0.08235 -0.2996 -0.1714 0.576 0.6462 0.9043
## psi 3.17675 0.2821 0.7963 -1.574 -0.9908 -0.7071
## session travail député thévenoud l'ensemble acteurs première genre
## beta 0.2309 0.3003 8.543 6.171 0.4054 0.3646 -0.2739 0.1282
## psi -3.2776 2.0022 -2.650 -2.743 1.2687 0.9282 1.1718 -0.9607
## table ronde entre taxis l'objectif réconcilier d'échanger
## beta 0.149 0.5797 -0.1048 3.760 -0.008629 0.2309 0.5886
## psi -0.493 -1.9808 1.9637 1.622 0.142846 -3.2776 -1.9837
## paisiblement revendications
## beta 0.2309 1.8344
## psi -3.2776 -0.6276
#les étiquettes acteur et années
doclab <- paste(docvars(dfmgroup, "acteur"))
docyear<- paste(docvars(dfmgroup, "year"))
#positionnement des acteurs
textplot_scale1d(wf, doclabels = doclab)
#positionnement des mots
textplot_scale1d(wf, margin = "features",
groups = "acteur",
highlighted = c("taxis","vtc","licences","minutes","uber",
"chauffeur","maraude","ubereats",
"concurrence","emplois","travail","partagez","promotion",
"conseil_constitutionnel"))
#positionnement des acteurs
textplot_scale1d(wf, doclabels = doclab, groups=docyear)
#dans l'autre sens
textplot_scale1d(wf, doclabels = docyear, groups=doclab)
#on restreint
dfm2<-dfm_subset(dfmgroup,acteur=="CGT")
#petite visualisation
textplot_wordcloud(dfm2)
#le modèle wordfish
wf <- textmodel_wordfish(dfm2)
summary(wf)
##
## Call:
## textmodel_wordfish.dfm(x = dfm2)
##
## Estimated Document Positions:
## theta se
## CGT.2013 -0.9995 0.02038
## CGT.2014 -0.1416 0.01161
## CGT.2015 -0.9110 0.01023
## CGT.2016 1.1770 0.01281
## CGT.2017 0.8751 0.02705
##
## Estimated Feature Scores:
## taxi vtc cœur négociations l'assemblée nationale paris
## beta 0.4889 0.1494 -0.7306 -0.8808 1.6862 0.390 -0.2299
## psi 3.8986 4.0505 -1.5355 -1.6109 -0.4961 1.869 2.4797
## mars jour rend travail député thévenoud l'ensemble acteurs
## beta -0.1351 0.2711 0.3049 0.6909 0.983 0.1118 -0.1124 1.118
## psi 0.2977 1.1951 -0.3295 2.4506 2.061 0.6092 1.3331 1.361
## première genre table ronde entre taxis l'objectif d'échanger
## beta 0.4449 -0.09663 0.6406 -0.7306 0.221 0.4589 0.106 -1.77
## psi 1.4278 -0.38585 -0.3500 -1.5355 1.948 3.9263 -0.858 -2.86
## revendications l'issue réunion plusieurs groupes mis place
## beta 0.7007 0.09333 0.4392 0.969 0.2087 0.4472 0.1388
## psi 0.8024 -1.95839 1.0052 1.482 -0.1530 0.3630 1.4490
#les étiquettes années
docyear<- paste(docvars(dfm2, "year"))
#positionnement des acteurs
textplot_scale1d(wf, doclabels = docyear)
#positionnement des mots
textplot_scale1d(wf, margin = "features",
groups = "acteur",
highlighted = c("taxis","vtc","licences","minutes","uber",
"chauffeur","maraude",
"concurrence_déloyale", "plateforme",
"emplois","travail","loti","promotion",
"ads", "délégués","marché_maraude"))