rues de Paris

rues de Paris

Cette étude se concentre sur l’analyse des intitulés des offres de logements sur Airbnb, rédigés par leurs propriétaires. Il sont la quintessence de ce qu’il peuvent offrir, balancent entre la description objective des attributs de l’offre (un 2 pièce à Montmartre) et la synthèse de ses qualité objective (charmant). Nous voulons voir ce qu’exprime ces condensés.

On aurait bien sur avoir des textes plus longs. Ici il se limitent à une centaines de caractères et pas plus de dix mots. C’est la contrainte du jeu de données dont nous avons présenté l’analyse spatiale et approfondi la question de la formation des prix.

Dans cette perspective, c’est l’occasion d’examiner plus qualitativement la nature des offres en étudiant les mots qui les définissent, On poursuivra suivra la logique géographique en exploitant les 80 quartiers définis par AirBnB, mais aussi celle des prix, l’organisation spatiale : de centre à périphérie, qui n’exclue pas une multipolarité : le Marais, Montmartre, les Champs Elysées en étant les principaux.

C’est surtout l’occasion d’étudier le package quanteda mais aussi tidytext , deux avancées récente dans le domaine du traitement textuel avec r, qui prennent la succession de tm notamment. Des langages plus condensés, qui abandonnenty l’idée du mot pour celle plus générale et abstraites du Token.

En terme d’analyse, nous resterons dans le cadre du “bagofword”, et n’exploiterons peu les ressources de l’analyse positionnelle. La première s’appuie sur les similarités issue de fréquences et de probabilité, n’assignant aucun ordre entre les tokens d’un même ensemble de mots faisant partie du corpus. Les tokens peuvent être des des mots, des ngrams, des syllabes, des phrases. Les sacs de mots peuvent êtres des phrases, des k-groupes de mots, des mots, des paragraphes, des documents. Le corpus est l’ensemble des documents, l’ensembles des token. A chaque échelle on s’intéresse aux fréquences relatives et aux co-occurences.

L’analyse positionnelle donne une importance à l’ordre des mots qui traduit souvent une grammaire implicite. L’ordre des phrases : sujet Verbe Complément en français, SCV en allemand. quand les tokens dans l’ordre sont caractérisés par des attributs grammaticaux, on peut décrire de manière hiérarchiques les éléments d’une phrase qui comme la musique est ordonnée par la séquence. Ce peut être pûrement ordinale, la musique y ajoute une idée cardinale : l’unité de temps est donnée par le réglages d’un métronome.

L’outillage

Voici l’ensemble des packages employés et quelques recodages. En particulier les gamme de prix qui découlent de l’étuide sur la formation des prix

#option pour le tricotage
knitr::opts_chunk$set(echo = TRUE, include=TRUE,message=FALSE,warning=FALSE,cache=TRUE)
#chargement des packages
library(Rcmdr)              # la bouée de sauvetage
## Loading required package: splines
## Loading required package: RcmdrMisc
## Loading required package: car
## Loading required package: carData
## Loading required package: sandwich
## Loading required package: effects
## lattice theme set by effectsTheme()
## See ?effectsTheme for details.
## L'interface graphique de R Commander n'est utilisable que dans des sessions interactives
## 
## Attaching package: 'Rcmdr'
## The following object is masked from 'package:car':
## 
##     Confint
library(corrplot)           # un accesoire visuel pour l'analyse des correlations
## corrplot 0.84 loaded
library(tidyverse)          # la mode pour r c'est le tidy et il y a ggplot2 pour la viz
## -- Attaching packages ---------------------------------- tidyverse 1.2.1 --
## v ggplot2 3.1.0     v purrr   0.2.5
## v tibble  1.4.2     v dplyr   0.7.8
## v tidyr   0.8.2     v stringr 1.3.1
## v readr   1.3.1     v forcats 0.3.0
## -- Conflicts ------------------------------------- tidyverse_conflicts() --
## x dplyr::filter() masks stats::filter()
## x dplyr::lag()    masks stats::lag()
## x dplyr::recode() masks car::recode()
## x purrr::some()   masks car::some()
library(ggplot2)
library(gridExtra)          # c'est pour mettre plusieurs graphiques en un seul
## 
## Attaching package: 'gridExtra'
## The following object is masked from 'package:dplyr':
## 
##     combine
library(dendextend)         # pour de plus beaux arbres
## 
## ---------------------
## Welcome to dendextend version 1.9.0
## Type citation('dendextend') for how to cite the package.
## 
## Type browseVignettes(package = 'dendextend') for the package vignette.
## The github page is: https://github.com/talgalili/dendextend/
## 
## Suggestions and bug-reports can be submitted at: https://github.com/talgalili/dendextend/issues
## Or contact: <tal.galili@gmail.com>
## 
##  To suppress this message use:  suppressPackageStartupMessages(library(dendextend))
## ---------------------
## 
## Attaching package: 'dendextend'
## The following object is masked from 'package:stats':
## 
##     cutree
library(Rcpp)               # j'ai du avoir besoin de ça
library(topicmodels)        # pour trouver des sujets de conversations
library(corpustools)        # on a toujours besoin de l'outil du voisin
## Loading required package: Matrix
## 
## Attaching package: 'Matrix'
## The following object is masked from 'package:tidyr':
## 
##     expand
## Loading required package: data.table
## 
## Attaching package: 'data.table'
## The following object is masked from 'package:dendextend':
## 
##     set
## The following objects are masked from 'package:dplyr':
## 
##     between, first, last
## The following object is masked from 'package:purrr':
## 
##     transpose
library(Rtsne)              # c'est du mds à la sauce relativité - un modèle de champs?
library(ldatuning)          # des sujets oui mais combiens?
library(tidytext)           # dans le tidy il y a le rameau du texte
## 
## Attaching package: 'tidytext'
## The following object is masked from 'package:corpustools':
## 
##     get_stopwords
library(quanteda)           # le plus top des accessoires et les modèles les plus originaux
## Package version: 1.3.14
## Parallel computing: 2 of 4 threads used.
## See https://quanteda.io for tutorials and examples.
## 
## Attaching package: 'quanteda'
## The following object is masked from 'package:utils':
## 
##     View
library(textcat)            # identification des langues
#library(quanteda.dictionaries) #attention aux langues!
library(knitr)
#chargement du fichier 
library(readr) #pour lire le fichier csv
B_17_07 <- read_csv("C:/Users/UserPC/Documents/airbnb/data/tomslee_airbnb_paris_1478_2017-07-25.csv")
## Parsed with column specification:
## cols(
##   .default = col_double(),
##   room_type = col_character(),
##   country = col_logical(),
##   city = col_character(),
##   borough = col_logical(),
##   neighborhood = col_character(),
##   bathrooms = col_logical(),
##   minstay = col_logical(),
##   name = col_character(),
##   property_type = col_character(),
##   last_modified = col_datetime(format = ""),
##   location = col_character()
## )
## See spec(...) for full column specifications.
#on recode le prix en 5 plages ( voir étuides formation des prix)
P <-B_17_07 #pour simplifier le code une lettre suffit
P$priceb[P$price<61]<-" 20-60 et moins"
## Warning: Unknown or uninitialised column: 'priceb'.
P$priceb[P$price>60 & P$price<100]<-" 60-100"
P$priceb[P$price>99 & P$price<150]<-"100-150"
P$priceb[P$price>149 & P$price<250]<-"150-250"
P$priceb[P$price>249]<-"250 et plus"
Table <- with(P, table(priceb))
cat("\ncounts:\n")
## 
## counts:
print(Table)
## priceb
##  20-60 et moins          60-100         100-150         150-250 
##           19130           24773           14670            7750 
##     250 et plus 
##            3835
Corpus<-corpus(P,text_field="name") #corpus de base qui sera filtré

Volumétrie

L’analyse textuelle demande d’être attentif à la volumétrie. Analyser l’oeuvre de Jame Joyces, composée de nouvelle (les gens de Dublin) et d’odyssée (bien sur Ulysse) est une tâche bien différente que d’analyser 70 158 télégrammes.

  • le nombre de mots (et plus généralement de tokens)
  • le nombre de documents
  • le nombre de groupes de documents ou de séquences de documents

La caractéristique est que ce sont de sparses matrix. Des tableaux creux qui prennent un volume important (ici de plusieurs dizaines de Mo pour un fichier d’origine de quelques centaines de ko) et souligne le fait que traiter des données textuelles, c’est d’abord suivre un chemin d’expansion avant de penser à réduire.

L’analyse de la volumétrie doit nous permettre d’apprécier les bons ordres de grandeur.

Le nombre de caractères

Examinons d’abord la longueur des intitulés. Le maximum est de 50 caractères. Deux populations se manifestent : l’une tend à employer tous les caractères possibles, l’autre trouve son mode vers 33 caractères. Deux groupes d’annonces? des courtes? des longues?

P$nbcar<-as.numeric(nchar(P$name))

ggplot(data = P, aes(x=nbcar))+geom_histogram(fill="grey")+ labs(title = "Distribution des intitulés par nombre de caractères", x="nombre de caractères", y="nombre d'annonces")+theme_bw()+xlim(0,50)

###par niveaux de prix

On se contente d’ajouter la couche facet_grid. Et rien de particulier n’émerge.

ggplot(data = P, aes(x=nbcar))+geom_histogram(fill="grey")+ labs(title = "Distribution des intitulés par nombre de caractères", x="nombre de caractères", y="nombre d'annonces")+theme_bw()+xlim(0,50)+facet_grid(priceb~.)

par localisation

df<-aggregate(nbcar~neighborhood,data=P,FUN=mean)
ggplot(data = df, aes(x=reorder(neighborhood,nbcar),y=nbcar))+geom_point()+ labs(title = "Nombre de caractères", x="nombre de caractères", y="")+theme_bw()+coord_flip()+theme(axis.text=element_text(size=6))

Le nombre de mots

Pour compter les mots, il faut transformer la chaine de caractère en Document F Matrix. On obtient donc un tableau (virtuel) de 70158 lignes et 11546 colonnes dont comprenant en plus de 800 millions d’éléments (le nombre de fois où un term apparait dans un document) et qui représente physiquement un fichier de 32.9 mb. En fait, c’est une abstraction dans la mesure où le codage du tableau suit les techniques du sparse matrix. Une grande partie des cellules et contiennent la valeur zero. On obtient une valeur de 99.94% de cases vides !

Examinons dans la foulée la distributions des termes. * Paris emporte la palme avec près de 17000 citations, soit plus d’une offre sur 4. Studio, appartement (et ses variantes) suivent. * Les termes suivants se distribuent en noms de lieux et localisation (in, centre..), en adjectifs ( cosy, charmant,..), en caractéristiques des logements (nombre de pièces). * Le corpus est clairement bilingue. Pour approfondir un modèle d’identification des langues pourrait être utile. Il existe un package pour celà ! le package textcat de Hornik, K., Mair, P., Rauch, J., Geiger, W., Buchta, C., & Feinerer, I. The textcat Package for n-Gram Based Text Categorization in R. Journal of Statistical Software, 52, 1-17.

df<-dfm(Corpus)
ndoc(df)
## [1] 70158
nfeat(df)
## [1] 11546
sparsity(df)
## [1] 0.9994813
cf<-as.data.frame(topfeatures(df, n = 100, decreasing = TRUE, scheme = c("count","docfreq"), groups = NULL))
cf$term<-row.names(cf)

cf$frequence<-cf[,1]
g<-ggplot(cf,aes(x=reorder(term,frequence),y=frequence))+geom_point()+coord_flip()+theme_minimal()+theme(text = element_text(size=7))
g

Avant d’aller plus loin dans l’analyse, on va donc séparer notre corpus en deux ensembles linguistiques, qui d’ailleurs ne correspondent pas seulement à des langues dont on doit tenir compte des spécificités (distribution du vocabulaire, grammaire..) mais aussi à deux ensembles d’offres : l’une qui s’adresse délibéremment aux étrangers - est-ce pour le plaisir de la découverte des autres où parce qu’on espère mieux valoriser le prix?, l’autre qui ne fait pas cet effort et s’offre à qui peut les comprendre. On s’attend dans le premier cas à une plus grande élaboration des intitulés des offres.

Le français est en tête mais ne couvrent que 23000 offres, l’anglais environ 17000, le catalan de manière surprenante représente prêt de 11000 offres. Il y a sans doute une erreur d’identification, il est plus probable que ce soit l’espagnol. Il en est de même plus les offres en latin !

La cause vient certainement de la nature de notre corpus qui ne comprend pas de phrases complétement articulées et se présente comme des suites de mots courants.

Dans la suite on traitera autant que possible ces deux corpus en parallèle.

P$langue<-textcat(P$name) #attention c'est long - une bonne diaines de minutes.
P$langue<-as.factor(P$langue)

Table <- with(P, table(langue))
cat("\ncounts:\n")
## 
## counts:
print(Table)
## langue
##           afrikaans            albanian              basque 
##                 358                   2                 101 
##             bosnian              breton             catalan 
##                   8                 295               10987 
##      croatian-ascii     czech-iso8859_2              danish 
##                  17                  64                 984 
##               dutch             english           esperanto 
##                1860               16866                 291 
##            estonian             finnish              french 
##                  92                  30               23434 
##             frisian              german           hungarian 
##                 480                2626                  41 
##           icelandic          indonesian               irish 
##                   4                 528                 260 
##             italian               latin             latvian 
##                 599                1350                  33 
##          lithuanian               malay                manx 
##                  53                  28                 136 
##      middle_frisian              nepali           norwegian 
##                1485                  69                  25 
##              polish          portuguese            romanian 
##                   5                 993                1312 
##           rumantsch            sanskrit               scots 
##                1113                 208                 817 
##        scots_gaelic       serbian-ascii        slovak-ascii 
##                 205                  10                 166 
##  slovak-windows1250     slovenian-ascii slovenian-iso8859_2 
##                 311                   3                1020 
##             spanish             swahili             swedish 
##                 207                 129                  61 
##             tagalog             turkish               welsh 
##                 136                   3                 193
Pays<-as.data.frame(Table)

g<-ggplot(Pays,aes(x=reorder(langue,Freq),y=Freq))+geom_point()+coord_flip()+theme_minimal()+theme(text = element_text(size=9))
g

P_f <-subset(P,langue=="french")
P_e <-subset(P,langue=="english")

Analyse de la lisibilité

Le propre d’une annonce est de séduire en quelques mots. Elle doit être immédiatement lisible. Une annonce se lit-elle mieux que les autres ? est-elle caractérisée par une meilleure note? plus de commentaires? Compte tenu de la nature des textes, courts par nature et fortement associés aux destinations (quartier)

La question de la lisibilité est justement étudiée depuis longtemps en pédagogie qui fournit une littérature abondante sur sa mesure.

La fonction textstat_readability de quanteda fournit de nombreux indicateurs à cette fin. On en retient quatre d’entre eux :

  • ARI : devient un classique du genre
  • Coleman : un classique
  • le nombre moyen de syllabes par mot : un des éléments qui contribuent aux scores (simplicité des mots)
  • le nombre moyen de mots par phrase : qui contribue aussi au score (complexité)

Leur dispersion est représentée par de simples histogrammes.

P_f$nbcar<-as.numeric(nchar(P_f$name))
R_f<-subset(P_f,select=c(name,neighborhood,overall_satisfaction,reviews,priceb,price,langue,nbcar),nbcar>5)

ggplot(data = P_f, aes(x=nbcar))+geom_histogram(fill="grey")+theme_bw()

Corpus_f<-corpus(R_f,text_field="name")
read_f<- textstat_readability(Corpus_f, measure = c("ARI",
  "Bormuth",  "Coleman",  "Dale.Chall", "Fucks",  "SMOG",
  "meanSentenceLength", "meanWordSyllables"), remove_hyphens = TRUE,
  min_sentence_length = 0, max_sentence_length = 2000,
  intermediate = FALSE)
read_f<-cbind(R_f,read_f)

g1<-ggplot(data = read_f, aes(x=Coleman))+geom_histogram(fill="grey")+theme_bw()
g2<-ggplot(data = read_f, aes(x=ARI))+geom_histogram(fill="grey")+theme_bw()+xlim(-10,50)
g3<-ggplot(data = read_f, aes(x=meanWordSyllables))+geom_histogram(fill="grey")+theme_bw()+xlim(0,5)
g4<-ggplot(data = read_f, aes(x=meanSentenceLength))+geom_histogram(fill="grey")+theme_bw()+xlim(0,15)

grid.arrange(g1,g2, g3, g4,nrow=2) #utilise gridextra

On peut comparer les deux corpus. Le français est moins lisible que le corpus anglais, il comprend moins de mots mais des mots plus longs, pour un nombre de caractères proches. Notre test ici est limité dans la mesure où la différence ne dépend pas seulement de ma compétence des scripteurs mais aussi de la nature de la langue.

#traitement du corpus anglais
P_e$nbcar<-as.numeric(nchar(P_e$name))
R_e<-subset(P_e,select=c(name,neighborhood,overall_satisfaction,reviews,priceb,price,langue,nbcar),nbcar>5)

Corpus_e<-corpus(R_e,text_field="name")
read_e<- textstat_readability(Corpus_e, measure = c("ARI",
  "Bormuth",  "Coleman",  "Dale.Chall", "Fucks",  "SMOG",
  "meanSentenceLength", "meanWordSyllables"), remove_hyphens = TRUE,
  min_sentence_length = 0, max_sentence_length = 2000,
  intermediate = FALSE)
read_e<-cbind(R_e,read_e)

#test
read_c<-rbind(read_e,read_f)
ggplot(read_c, aes(x=ARI, fill=langue)) +
  geom_density(alpha = 0.7, position="dodge")+xlim(0,50)

t.test(nbcar~langue, alternative='two.sided', conf.level=.95, var.equal=FALSE, data=read_c)
## 
##  Welch Two Sample t-test
## 
## data:  nbcar by langue
## t = 17.981, df = 37330, p-value < 2.2e-16
## alternative hypothesis: true difference in means is not equal to 0
## 95 percent confidence interval:
##  1.330630 1.656207
## sample estimates:
## mean in group english  mean in group french 
##              36.62499              35.13157
t.test(ARI~langue, alternative='two.sided', conf.level=.95, var.equal=FALSE, data=read_c)
## 
##  Welch Two Sample t-test
## 
## data:  ARI by langue
## t = -36.477, df = 39730, p-value < 2.2e-16
## alternative hypothesis: true difference in means is not equal to 0
## 95 percent confidence interval:
##  -2.127050 -1.910121
## sample estimates:
## mean in group english  mean in group french 
##              5.532108              7.550693
t.test(meanSentenceLength~langue, alternative='two.sided', conf.level=.95, var.equal=FALSE, data=read_c)
## 
##  Welch Two Sample t-test
## 
## data:  meanSentenceLength by langue
## t = 34.263, df = 37633, p-value < 2.2e-16
## alternative hypothesis: true difference in means is not equal to 0
## 95 percent confidence interval:
##  0.5370400 0.6022114
## sample estimates:
## mean in group english  mean in group french 
##              6.181964              5.612338
t.test(meanWordSyllables~langue, alternative='two.sided', conf.level=.95, var.equal=FALSE, data=read_c)
## 
##  Welch Two Sample t-test
## 
## data:  meanWordSyllables by langue
## t = -50.516, df = 40071, p-value < 2.2e-16
## alternative hypothesis: true difference in means is not equal to 0
## 95 percent confidence interval:
##  -0.2203060 -0.2038486
## sample estimates:
## mean in group english  mean in group french 
##              1.711244              1.923321

Pour l’analyse, il faut savoir que ces indicateurs anciens ont souvent été étalonnés en fonction de niveaux de compétences scolaires de lecture, ainsi un niveau de langage de 10 correspond à un age de 12 ans. On lira avec profit ce document

Dans notre exemple cette référence a certainement peu de sens, puisque justement le but pour attirer est d’employer des phrases simples, on peut vouloir parler bébé à des adultes pour qu’il comprennent sans ambiguité, ou pour les tromper. “Demain on rase gratis”, le péremptoire de la formule est performatif s’il y a un biais de positivité (on entend mieux les messages positifs que négatifs)

Cependant ils peuvent être utiles pour distinguer les différences annonces et on peut se demander dans quelle mesure ils sont liés à la qualité ou à la réputation. Examinons les corrélations.

#library(corrplot)
M<-subset(read_c, select=c(overall_satisfaction,reviews,price,ARI,meanWordSyllables,meanSentenceLength,nbcar))
C<-cor(M, use="complete")

corrplot(C, method = "circle")

Elles sont presque inexistantes, ce n’est peut-être pas aussi étonnant que celà et ça laisse de l’espoir aux maladroits. Mais si on s’intéresse aux prix. Même si les différences sont faibles, elle sont consistantes : les logements les plus chers ont un peu moins de syllabes par mots, et ont un nombre de mots par phrase plus élevé.

En dépit de ces très faibles corrélations représentons en fonction de niffférents niveaux de prix les valeurs des indicateurs de lisibilité. On fait un test rapide par une Anova qui est très significative, même si la variance expliquée est très faible. Nous ne calculons pas la taille de l’effet, elle est extremement faible, mais sa configuration est consistante :

  • plus l’offre est chère plus on emploie de mots : la valeur demande du détail.
  • plus elle est chère plus courts sont les mots : la valeur demande de la précision
  • plus elle est chère plus la lisibilité est faible au sens d’ARI. La valeur n’est pas lisible par tous.
Anova01 <- aov(meanWordSyllables ~ priceb+langue, data=read_c)
summary(Anova01)
##                Df Sum Sq Mean Sq  F value Pr(>F)    
## priceb          4      2     0.6    2.984 0.0179 *  
## langue          1    440   440.0 2347.438 <2e-16 ***
## Residuals   40293   7552     0.2                    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Ag<- aggregate(cbind(nbcar,meanWordSyllables,meanSentenceLength) ~ priceb+langue, data=read_c, FUN=mean)
Ag <- melt(Ag, id=c("priceb","langue")) 
g1<-ggplot(Ag,aes(x=priceb,y=value,color =langue))+geom_line(size=2, aes(group=langue) )+ facet_grid(variable~.,scales = "free")
g1

g6<-ggplot(Ag,aes(x=reorder(langue,value), y=value))+geom_violin(col="orange")+facet_grid(variable~.,scales = "free")
g6

La tokenisation

Un token c’est un jeton. Une pièce, un bout. Un mot, un bigram, une phrase, un caractère, n’importe quelle unité de fragment de texte. C’est aussi l’unité d’analyse textuelle la plus fondamentale. Dans l’analyse précedente, on a prix le texte lui-même.

A chaque token on peut associer des attributs : un sentiment, une forme verbale, une origine géographique…

Pour les créer quanteda offre une solution bien utile avec la fonction tokens , on en profite pour retirer la ponctuation, mais on laisse les nombres qui indiquent souvent le nombre de pièces!

Mais d’abord on visualise le résultat avec la technique du key word in context (kwic) et on vérifie immédiatement notre hypothèses. Les nombres peuvent aussi indiquer les surfaces.

https://link.springer.com/article/10.3758/s13428-014-0529-0

toks <- tokens(Corpus_f, remove_punct = TRUE)
head(toks[[1]], 100)
## [1] "Salon"      "dans"       "colocation"
kw <- kwic(toks, pattern =  'pièce')
head(kw, 20)
##                                                  
##    [text338, 3]                  Joli 2 | pièce |
##    [text905, 3]               Superbe 4 | pièce |
##   [text2057, 3]               Beau deux | pièce |
##   [text3808, 3]              Logement 4 | pièce |
##   [text5226, 3]           appartement 2 | pièce |
##   [text6782, 3]              Charmant 3 | Pièce |
##   [text6904, 3]               Beau deux | pièce |
##   [text7510, 4]       Bel appartement 2 | pièce |
##   [text7766, 3]                  Beau 3 | pièce |
##   [text7936, 5]     Charme d'un grand 2 | pièce |
##   [text7949, 4]  Charmant appartement 2 | pièce |
##   [text7960, 4]       Bel appartement 3 | pièce |
##   [text8303, 3]              Charmant 2 | pièce |
##   [text8389, 2]                       2 | pièce |
##   [text8644, 3]           Appartement 2 | pièce |
##   [text9575, 4]   Grand appartement une | pièce |
##  [text10167, 3]              Charmant 2 | pièce |
##  [text10309, 3]                  Beau 2 | pièce |
##  [text10335, 3]           Lumineux deux | pièce |
##  [text11823, 5] Charmant et joyeux deux | pièce |
##                                 
##  avec balcon dans un quartier   
##  refait à neuf                  
##  au cœur du de Paris            
##  de 84 m2 habitable avec        
##  Paris 13 BNF                   
##  proche Montmartre              
##  parfait pour voyage en couple  
##  de 42 m à Alesia               
##  dans le 9ème                   
##  type loft                      
##  du 16ème                       
##  60m au coeur du 16ème          
##  proche tour eiffel             
##  proche Invalides et Tour Eiffel
##  paris 17                       
##  45m2                           
##  au coeur de Paris              
##  proche montmartre              
##  en plein Paris                 
## 

Cette solution est d’autant plus utile qu’elle propose une fonction qui calcule la vraisemblance de certains bi ou trigrammes d’être en fait des expressions composées qu’on tiendra pour un seul token, par la technique de la collocation.

The lambda computed for a size = K-word target multi-word expression the coefficient for the K-way interaction parameter in the saturated log-linear model fitted to the counts of the terms forming the set of eligible multi-word expressions. This is the same as the “lambda” computed in Blaheta and Johnson’s (2001), where all multi-word expressions are considered (rather than just verbs, as in that paper). The z is the Wald z-statistic computed as the quotient of lambda and the Wald statistic for lambda as described below.

  • count : le nombre de fois où les deux mots sont consécutifs
  • count-nested : ???
  • length : longueur de la collocation
  • lambda : paramètre d’un modèle log linéraire
  • z : statistique de significance

La dernière ligne de code permet d’ajouter au fichiers des tokens les multigrammes ( mots composés) dont la statistique z est supérieur à 5.

col <- toks %>% 
       tokens_remove(stopwords("fr")) %>% 
       tokens_select(pattern = "^[A-Z]", valuetype = "regex", case_insensitive = TRUE, padding = TRUE) %>% 
       tokens_remove(c("paris")) %>% 
       textstat_collocations(min_count = 30,size=2, tolower = FALSE)
head(col,50)
##             collocation count count_nested length    lambda        z
## 1       Bel appartement   711            0      2  5.261319 53.45753
## 2           Tour Eiffel   508            0      2  9.391122 46.12782
## 3       Charmant studio   364            0      2  3.004893 44.43013
## 4            bien situé   138            0      2  6.552407 42.23968
## 5           Sacré Coeur   160            0      2  7.714008 42.05676
## 6            St Germain   122            0      2  6.262172 40.94468
## 7         Saint Germain   113            0      2  5.769159 39.42887
## 8           plein coeur   236            0      2  3.635314 39.01312
## 9           deux pièces   301            0      2  3.137405 38.10827
## 10             Très bel   100            0      2  6.967842 37.80594
## 11         tout confort    99            0      2  6.640478 37.54998
## 12 Charmant appartement   429            0      2  2.207349 37.45485
## 13         apartment in   113            0      2  4.552679 36.38885
## 14          Beau studio   178            0      2  3.312268 35.32188
## 15      Buttes Chaumont   332            0      2 10.737616 34.94865
## 16    proche Montmartre   304            0      2  2.330903 33.80065
## 17           sous toits   162            0      2  8.446556 33.69124
## 18        Belle chambre    90            0      2  6.359784 32.51868
## 19       Quartier Latin    76            0      2  7.578422 31.92693
## 20         Place Vosges    92            0      2  7.351664 31.80828
## 21       Chambre privée   105            0      2  4.768692 31.62772
## 22          proche Tour   142            0      2  3.123679 30.51000
## 23      pied Montmartre   163            0      2  3.107145 29.99720
## 24    Jardin Luxembourg    52            0      2  7.042674 29.77717
## 25        Butte Cailles    79            0      2  8.642374 29.75794
## 26       calme lumineux   194            0      2  2.483072 29.65941
## 27            St Martin    56            0      2  5.817654 29.42712
## 28            Gare Nord    67            0      2  7.550144 29.32889
## 29     Appartement cosy   309            0      2  2.073091 28.84381
## 30          tour Eiffel    76            0      2  5.372517 28.79615
## 31         plein centre    94            0      2  3.487038 28.49631
## 32      Logement entier    47            0      2  6.697464 28.44651
## 33         Germain Prés   100            0      2  8.353622 28.29548
## 34     idéalement situé    86            0      2  7.463172 28.07992
## 35         Nid douillet    42            0      2  7.241336 28.03049
## 36            Gare Lyon    80            0      2  8.265337 28.02177
## 37       Champs Elysées    88            0      2  9.860625 27.93450
## 38         Saint Martin    52            0      2  5.451035 27.81049
## 39    Grand appartement   160            0      2  2.803993 27.52241
## 40          refait neuf   144            0      2  9.030769 27.18740
## 41         Parc Monceau    37            0      2  6.398831 26.79537
## 42     Porte Versailles    55            0      2  7.856337 26.77382
## 43        Père Lachaise   200            0      2 11.390004 26.64164
## 44       apartment near    57            0      2  4.447267 26.54373
## 45    Atelier d'artiste    37            0      2  7.166376 26.51947
## 46   Appartement charme   243            0      2  2.129817 26.11018
## 47            très bien    53            0      2  4.342797 26.10105
## 48          Canal Saint    44            0      2  4.985228 25.91873
## 49          Joli studio   117            0      2  2.781359 25.90486
## 50         coeur Marais   150            0      2  2.403180 25.79787
comp_toks <- tokens_compound(toks, pattern = col[col$z > 5])

Analyse de la diversité lexicale

Le comptage des mots ne prends pas en compte leur signification, pas même le lexique. Un mot en deux syllabes comme “apax” (mot unique d’un texte) est plus rare que “cycas” (plante) et bien plus rare que “ara” (perroquet). C’est ce que poursuit l’analyse de la diversité lexicale.

la fonction textstat_lexdiv s’appuie sur Tweedie. F.J. & Baayen, R.H. (1998). How Variable May a Constant Be? Measures of Lexical Richness in Perspective. Computers and the Humanities, 32(5), 323–352.

leurs définitions sont données ici https://www.rdocumentation.org/packages/quanteda/versions/1.3.13/topics/textstat_lexdiv exemple utilisation

https://royalsocietypublishing.org/doi/10.1098/rsos.160140

dfmlex<-dfm(comp_toks, tolower = TRUE,stem=FALSE)
lexdiv <- textstat_lexdiv(dfmlex) #c est long aussi
tail(lexdiv, 5)
##        document TTR         C        R     CTTR        U    S        K
## 23429 text23429 1.0 1.0000000 3.000000 2.121320      Inf    1 1111.111
## 23430 text23430 0.9 0.9542425 2.846050 2.012461 21.85435 -Inf 1200.000
## 23431 text23431 1.0 1.0000000 2.449490 1.732051      Inf    1 1666.667
## 23432 text23432 1.0 1.0000000 2.449490 1.732051      Inf    1 1666.667
## 23433 text23433 1.0 1.0000000 2.828427 2.000000      Inf    1 1250.000
##                D        Vm    Maas     lgV0    lgeV0
## 23429 0.00000000 0.0000000 0.00000      Inf      Inf
## 23430 0.02222222 0.0942809 0.21391 3.191085 7.347746
## 23431 0.00000000 0.0000000 0.00000      Inf      Inf
## 23432 0.00000000 0.0000000 0.00000      Inf      Inf
## 23433 0.00000000 0.0000000 0.00000      Inf      Inf
lex<-cbind(read_f,lexdiv)
lex<-subset(lex,select=c(neighborhood,priceb,C,CTTR,Maas))
divlex<-"Maas"
ggplot(lex,aes(x=Maas))+geom_histogram()

lex0<-aggregate(Maas~priceb,data=lex,FUN=mean)

ggplot(lex0,aes(x=priceb,y=Maas))+geom_point()+ylim(0,0.01)

lex1<-aggregate(Maas~neighborhood,data=lex,FUN=mean)
ggplot(lex1, aes(x = reorder(neighborhood,Maas), y = Maas)) +
    geom_point() + 
    scale_y_continuous(limits = c(0,0.02), breaks = c(seq(0,0.02, .001))) +
    xlab(NULL) + 
    ylab("Frequency") +
    theme(axis.text.x = element_text(angle = 90, hjust = 1, size=5),axis.text.y =  element_text(size=6))+
    coord_flip()+labs(title="Diversité lexicale - corpus français")

##wordcloud

pour un premier aperçu des données après tokenisation, le wordcloud est particulièrement pertinent. On en produit un sur le lexique brut, puis sur le lexique “stemmisé”.

Il est paramétré de manière à ce que les mots fréquents ’ Paris, appartement…) ne soient pas représentés. Une valeur de 8% semble convenir comme frequence maximale d’apparition dans les documents. Dès qu’un terme apparait dans plus de 8% des document, il est écarté. Des critères plus simples de fréquences mini et maxi auraient pu être choisis.

“Un appartement charmant dans le marais” résume les choses

#wordcloud et fréquence

set.seed(100) #pour rehouer le hasard de la même mani-re et etproduire les resultats
dfm<-dfm(comp_toks, tolower = TRUE,stem=FALSE) %>%
  dfm_trim(min_termfreq = 0.92, termfreq_type = "quantile", max_docfreq = 0.08, docfreq_type = "prop")

textplot_wordcloud(dfm,min_count = 50,     color = c('pink', 'red',  'orange','purple', 'blue'))

# avec stemming 
set.seed(100) #pour rehouer le hasard de la même mani-re et etproduire les resultats
dfm<-dfm(comp_toks, tolower = TRUE,stem=TRUE) %>%
  dfm(remove = stopwords('fr'), remove_punct = TRUE) %>%
  dfm_trim(min_termfreq = 0.92, termfreq_type = "quantile", max_docfreq = 0.08, docfreq_type = "prop")

textplot_wordcloud(dfm,min_count = 50,     color = c('pink', 'red',  'orange','purple', 'blue'))

Cette représentation globalisante peut être fractionnés selon les textes. En segmentant en fonction de la classe de prix, on fait emerger un vocabulaire plus différenciés : à moin de 60 euros c’est cosy, ça devient charmant dans la tranche 60-100, plus familial, en gardant le charme pour les 100-150 euros, et pour les prix les plus élevés devient luxueux et magnifiques.

dfmComp <- dfm(Corpus_f, groups = "priceb")
textplot_wordcloud(dfmComp, comparison = TRUE, max_words = 200,labelsize = 0.9)

Si les nuages de points sont plaisants, de simples diagrammes de fréquence peuvent être utiles pour distinguer les catégories, quand elles sont nombreuses. Dans l’exemple suivante on représente pour un élément de lexique, leur fréquence pour chacun des quartiers de Paris. Pour le lemme “charm”, c’est la porte de clignancourt et grandes carrières qui remportent la palme. Etonnant? Peut-être pas si le manque de charme de l’environnement est composé par l’agencement intérieur. Il va falloir vérifier.

freq_dfm <- textstat_frequency(dfm, groups = "neighborhood")
term<-"charmant"
freq_room <- subset(freq_dfm, freq_dfm$feature %in% paste0(term))  #attention à la graphie si stemming

ggplot(freq_room, aes(x = reorder(group,frequency), y = frequency)) +
    geom_point() + 
    scale_y_continuous(limits = c(0, 50), breaks = c(seq(0, 250, 10))) +
    xlab(NULL) + 
    ylab("Frequency") +
    theme(axis.text.x = element_text(angle = 90, hjust = 1, size=5),axis.text.y =  element_text(size=6))+
    coord_flip()+labs(title=paste0(term))

On peut s’interroger cependant sur la pertinence de la fréquence qui peut dépendre simplement du nombre d’offres par quartier, on va donc rapporter cette fréquence au nombre de commentaires (le taux calculé est en pour 1000 com. simplement pour une raison d’échelle). On garde le même ordre de fréquence absolue.

P$n<-1
P$neighborhood<-as.factor(P$neighborhood)
freq_room$group<-as.factor(freq_room$group)
neigh<- aggregate(n ~ neighborhood, data=P, FUN=sum)

neigh<-merge(neigh,freq_room, by.x="neighborhood",by.y="group",all.x=TRUE)

neigh$frequenceRelative<-neigh$frequency*1000/neigh$n

g<-ggplot(neigh, aes(x = reorder(neighborhood,frequency), y = frequenceRelative)) +
    geom_point() + geom_point(aes(y=frequency),col="red")+
    scale_y_continuous(limits = c(0, 100), breaks = c(seq(0, 250, 5))) +
    xlab(NULL) + 
    ylab("Frequency") +
    theme(axis.text.x = element_text(angle = 90, hjust = 1, size=5),axis.text.y =  element_text(size=6))+coord_flip()+labs(title=paste0(term))
g

C’est un paysage différent qui apparaît

POS

finalement cleanNLP a faire

library(cleanNLP)
text<-P_f$name

cnlp_init_udpipe(model_name = "french")
obj <- cnlp_annotate(text, as_strings = TRUE)
Vocab<-cnlp_get_token(obj)

Table <- with(Vocab, table(upos))
ling<-as.data.frame(Table)
g1<-ggplot(ling,aes(x=reorder(upos,Freq),y=Freq))+geom_point()+coord_flip()+theme_minimal()+theme(text = element_text(size=9))
g1

#on se concentre sur les adjectifs
Vocab_adj<-subset(Vocab, upos=="ADJ")
Table <- with(Vocab_adj, table(lemma))
cat("\ncounts:\n")
## 
## counts:
ling<-as.data.frame(Table)
ling<-subset(ling,Freq>50)
g2<-ggplot(ling,aes(x=reorder(lemma,Freq),y=Freq))+geom_point()+coord_flip()+theme_minimal()+theme(text = element_text(size=8))
g2

#

solutions : * spacy mais demande conda * qdap a l’air riche avec une mesure de lisibilité https://www.rdocumentation.org/packages/qdap/versions/2.3.0 ou http://trinker.github.io/qdap/vignettes/qdap_vignette.html#tools voir cKorpus

coreNLP est la références.

Analyse du sentiment

Le problème de la langue. Un manque de solution française?

faire un paquet sur le NRC et revenir aux essai de typologie des émotions. Rappeler la thèse évolutionniste etc.

soigner les couleurs ( roue des emotions) de Kuchnik

pour le français :

http://www.lexicoder.com/

https://www.poltext.org/fr/donnees-et-analyses/lexicoder

http://www.bnosac.be/index.php/blog/54-sentiment-analysis-and-parts-of-speech-tagging-in-dutch-french-english-german-spanish-italian

le package syuzhet

Liu, Bing. 2010. “Sentiment Analysis and Subjectivity.” Handbook of natural language processing 2: 627–66.

http://www.bnosac.be/index.php/blog/54-sentiment-analysis-and-parts-of-speech-tagging-in-dutch-french-english-german-spanish-italian

https://cran.r-project.org/web/packages/syuzhet/vignettes/syuzhet-vignette.html http://rstudio-pubs-static.s3.amazonaws.com/283881_efbb666d653a4eb3b0c5e5672e3446c6.html https://rstudio-pubs-static.s3.amazonaws.com/236096_2ef4566f995e48c1964013310bf197f1.html

library(syuzhet)
method <- "nrc"
lang <- "french"
phrase<-as.character(P_f$name)
my_text_values_french<- get_sentiment(phrase, method=method, language=lang)
my_text_values_french[1:10]
##  [1] -1  0  3  0  0 -1  0  0 -1  0
sent<-as.data.frame(my_text_values_french)
sent$sentiment<-as.numeric(sent$my_text_values_french)
sent<-cbind(P_f,sent)
ggplot(sent, aes(x=sentiment))+geom_density()

emotions <- get_nrc_sentiment(phrase)
emo_bar = colSums(emotions)
emo_sum = data.frame(count=emo_bar, emotion=names(emo_bar))
emo_sum$emotion = factor(emo_sum$emotion, levels=emo_sum$emotion[order(emo_sum$count, decreasing = TRUE)])
ggplot(emo_sum, aes(x=emotion, y=count))+geom_bar(stat="identity")

sent2<-cbind(P_f,emotions)
Ag<- aggregate(cbind(positive,joy,trust) ~ priceb, data=sent2, FUN=mean)
Ag<-melt(Ag) # avec library(reshape2)
g3<-ggplot(Ag,aes(x=priceb,y=value,group= variable))+geom_line(aes(color=variable),size=2) 
g3

Ag<- aggregate(cbind(positive,joy,trust) ~ neighborhood, data=sent2, FUN=mean)
Ag<-melt(Ag)
g4<-ggplot(Ag,aes(x=reorder(neighborhood,value),y=value,group= variable))+geom_line(aes(color=variable),size=2) +coord_flip()
g4

Analyse sémantique latente (LSA)

On lira xxx.

https://link.springer.com/article/10.3758/s13428-014-0529-0

L’analyse sémantique latente est en fait un modèle SVD ( Singular Vector Decomposition) familiers à ceux qui connaissent l’analyse en composante principales. La différence est que dans le cas de l’ACP la décomposition en vecteurs singuliers se fait sur la matrice de variance covariance (si X est le tableau de donnée S=XXT), alors que le SVD traite le tableau d’occurences des termes dans les textes.( pour aller plus loin voir par exemple).

L’objectif est de décomposer une matrice tdm (term document matrix) en trois éléments comme l’illustre le diagramme suivant

  • A est la matrice de données
  • U les représentations des textes dans un nombre limité de facteurs
  • S c’est une matrice diagonale des facteurs, où les éléments correspondent aux valeurs singulières
  • V est la représentation des termes dans l’espace des facteurs
SVD

SVD

voir aussi LSAfun

#analyse semantique latente
mylsa <- textmodel_lsa(dfm)
proxD<-mylsa$docs[, 1:10]
head(proxD,4)
##                [,1]         [,2]         [,3]          [,4]          [,5]
## text1 -0.0001878500 0.0005755395 0.0009463354  0.0014204373  6.007494e-05
## text2 -0.0031401650 0.0091664063 0.0148308124  0.0221445983  1.595682e-03
## text3 -0.0033449501 0.0042947307 0.0043082674 -0.0057504263 -2.851496e-02
## text4 -0.0002121712 0.0003665611 0.0004834493  0.0003779183 -1.900751e-04
##                [,6]          [,7]          [,8]          [,9]
## text1  0.0001566646  0.0001953389 -0.0001484608 -4.898794e-05
## text2  0.0028134582  0.0043559701 -0.0021378774 -2.267445e-03
## text3  0.0016837947  0.0074462856  0.0006432559 -3.037543e-03
## text4 -0.0000708453 -0.0004022677  0.0002069604 -2.725559e-04
##               [,10]
## text1  9.495191e-05
## text2  2.681886e-04
## text3 -4.863501e-03
## text4  6.828086e-05
proxF<-mylsa$features[, 1:10]
head(proxF,5)
##                  [,1]        [,2]        [,3]          [,4]          [,5]
## salon    -0.003356907 0.005639154 0.008151956  0.0104323380  0.0017668203
## coloc    -0.005136010 0.015593680 0.025047191  0.0365842511  0.0001279467
## chambr   -0.141970508 0.338167572 0.520291576  0.7329879909  0.0503278888
## charmant -0.140646514 0.145877173 0.142477332 -0.1898646828 -0.8949721081
## idéal    -0.010582567 0.012564271 0.008664437 -0.0004748927 -0.0043912573
##                   [,6]          [,7]          [,8]          [,9]
## salon     0.0004003886 -0.0001758058  0.0005299847 -0.0004890783
## coloc     0.0043429499  0.0056026010 -0.0045024230 -0.0008000028
## chambr    0.0851831671  0.1210150895 -0.0572042478 -0.0596661295
## charmant  0.0547583435  0.2294265139  0.0207214204 -0.0981049082
## idéal    -0.0037780358 -0.0225580336 -0.0035095021  0.0181742400
##                 [,10]
## salon    -0.001117773
## coloc     0.003588535
## chambr    0.006978588
## charmant -0.142090183
## idéal     0.015536074

On utilise ces résultats pour faire une analyse des proximités et représenter les similarités entre les documents.

Plutôt que d’utiliser les classiques “Multidimensional Scaling”" (MDS) on opte, car le nombre d’objets textes est élevé, pour une méthode t-SNE dont la remarquable propriété de “gondoler” l’espace en fonction de la distribution des points : quand localement la densité des points est élevée ( c’est le sens de t-Distributed Stochastic Neighbor Embedding (t-SNE) les distances sont majorées, alors que pour des paquets éloignées elles seront minorées. (c’est en fait une théorie générale de la relativité qui s’oppose au modèle newtownien). Elle permet de représenter de grands ensembles d’objets (nous en avons 70158 individus et 1200 termes) en contrôlant le paramètre clé, celui de la perplexité (une sorte de cintrage de l’espace qui donne un poids plus fort en fonction de la densité locale des points).

En voici l’analyse sur les textes (documents) vérifier que tsne marche sur des matric e rect et calcule

#rtsne
tsne_out <- Rtsne(proxD, dims = 2, initial_dims = 100,
  perplexity = 20, theta = 0.5, check_duplicates = FALSE,
  pca = TRUE, max_iter = 300)

x<-tsne_out$Y
x<-as.data.frame(x)
x$F1<-x[,1]
x$F2<-x[,2]
x<-cbind(R_f,x)

df<-subset(x,neighborhood=="Sorbonne") #pour un quartier
library(ggrepel)
ggplot(df,aes(x = F1,y=F2))+geom_point()

De manière symétrique on peut l’effectuer sur les termes ( ici features) https://link.springer.com/article/10.3758/s13428-014-0529-0

prox<-mylsa$features[, 1:10]
terms<-as.data.frame(prox)

#rtsne
tsne_out <- Rtsne(terms, dims = 2, initial_dims = 50,
  perplexity = 20, theta = 0.5, check_duplicates = FALSE,
  pca = TRUE, max_iter = 300)
plot(tsne_out$Y)

x<-tsne_out$Y

terms$term<-row.names(prox)
plot<-cbind(x,terms)
plot$F1<-plot[,1]
plot$F2<-plot[,2]

ggplot(plot,aes(x = F1,y=F2))+geom_point()+geom_text(aes(label=term),hjust=0, vjust=0, size=3)

#explorer des zones
ggplot(plot,aes(x = F1,y=F2))+geom_point()+geom_text(aes(label=term),hjust=0, vjust=0, size=3)+xlim(5,10)+ylim(0,5)
## Warning: Removed 336 rows containing missing values (geom_point).
## Warning: Removed 336 rows containing missing values (geom_text).

ggplot(plot,aes(x = F1,y=F2))+geom_point()+geom_text(aes(label=term),hjust=0, vjust=0, size=3)+xlim(-15,-5)+ylim(-2,-1)
## Warning: Removed 358 rows containing missing values (geom_point).
## Warning: Removed 358 rows containing missing values (geom_text).

La LSA se prêt aussi à une analyse typologie hiérarchique

df<-plot[,3:13]
Clust<-hclust(dist(df), method = "ward.D2")
d<-as.dendrogram(Clust)
plot(d, horiz = TRUE)

h<-2
plot(cut(d, h=h)$upper, 
     main=paste("Upper tree of cut at h=",h), horiz=TRUE)

plot(cut(d, h=h)$lower[[1]], 
     main=paste("First branch of cut at h=",h), horiz=TRUE)

plot(cut(d, h=h)$lower[[2]], 
     main=paste("Upper tree of cut at h=",h), horiz=TRUE)

plot(cut(d, h=h)$lower[[3]], 
     main="third branch of lower tree with cut at h=75", horiz=TRUE)

##LDA

Et bien sur du lda dont l’article originel de xx . On en trouvera une présentation en français à la fois simple et détaillée sur le plan mathématique

le principe est finalement simple : chaque mot est susceptible d’appartenir à K concepts de même que chaque document. La probabilité est multivariée et on comprend que ce soit la loi de dirichlet qui en modèlise avec la plus grande pertinence cette distribution multiple. Le nombre de modalités est cependant très élevé dans notre cas : 70000 documents est 20000?? termes.

dtm <- convert(dfm, to = "topicmodels")
lda <- LDA(dtm, k = 12)
terms(lda, 10)
##       Topic 1           Topic 2       Topic 3     Topic 4    Topic 5     
##  [1,] "agréabl"         "pie"         "cosi"      "coeur"    "bel_appart"
##  [2,] "proch"           "tour_eiffel" "marai"     "marai"    "charm"     
##  [3,] "duplex"          "lumineux"    "apart"     "charm"    "charmant"  
##  [4,] "charmant"        "cosi"        "coeur"     "proch"    "cœur"      
##  [5,] "cosi"            "chambr"      "centr"     "bastill"  "4"         
##  [6,] "charm"           "nation"      "nation"    "cœur"     "lumineux"  
##  [7,] "marai"           "3"           "près"      "chambr"   "marai"     
##  [8,] "calm"            "cœur"        "calm"      "joli"     "centr"     
##  [9,] "buttes_chaumont" "deux"        "batignoll" "spacieux" "coeur"     
## [10,] "chambr"          "grand"       "quartier"  "m2"       "neuf"      
##       Topic 6           Topic 7      Topic 8    Topic 9      Topic 10
##  [1,] "calm"            "coeur"      "charmant" "gare"       "beau"  
##  [2,] "marai"           "toit"       "m2"       "coeur"      "duplex"
##  [3,] "charmant_studio" "calm"       "chambr"   "charmant"   "marai" 
##  [4,] "beau"            "sous"       "butt"     "calm"       "charm" 
##  [5,] "coeur"           "chambr"     "caill"    "duplex"     "loft"  
##  [6,] "chambr"          "bel_appart" "calm"     "bel_appart" "3"     
##  [7,] "piec"            "charm"      "vue"      "parisien"   "chambr"
##  [8,] "parisien"        "rue"        "bastill"  "chambr"     "apart" 
##  [9,] "batignoll"       "marai"      "centr"    "joli"       "joli"  
## [10,] "proch"           "joli"       "charm"    "quartier"   "cosi"  
##       Topic 11          Topic 12     
##  [1,] "coeur"           "coeur"      
##  [2,] "marai"           "vue"        
##  [3,] "11ème"           "lumineux"   
##  [4,] "bastill"         "balcon"     
##  [5,] "vue"             "centr"      
##  [6,] "centr"           "charmant"   
##  [7,] "charmant_studio" "spacieux"   
##  [8,] "place"           "tour_eiffel"
##  [9,] "bel_appart"      "chambr"     
## [10,] "joli_appart"     "proch"
#https://www.tidytextmining.com/topicmodeling.html
ap_topics <- tidy(lda, matrix = "beta")
ap_topics
## # A tibble: 4,344 x 3
##    topic term        beta
##    <int> <chr>      <dbl>
##  1     1 salon 0.000112  
##  2     2 salon 0.000933  
##  3     3 salon 0.00155   
##  4     4 salon 0.00000349
##  5     5 salon 0.000952  
##  6     6 salon 0.00178   
##  7     7 salon 0.000318  
##  8     8 salon 0.0000550 
##  9     9 salon 0.000334  
## 10    10 salon 0.000581  
## # ... with 4,334 more rows
ap_top_terms <- ap_topics %>%
  group_by(topic) %>%
  top_n(10, beta) %>%
  ungroup() %>%
  arrange(topic, -beta)

ap_top_terms %>%
  mutate(term = reorder(term, beta)) %>%
  ggplot(aes(term, beta, fill = factor(topic))) +
  geom_col(show.legend = FALSE) +
  facet_wrap(~ topic, scales = "free") +
  coord_flip()

L’inconvénient de la méthode LDA est que le nombre de topic latent est un paramètre fixé à priori et sans indications.

Il est cependant possible par une méthode de gille (on évalue successivement les différents nombres de topic) d’identifier un nombre optimal de topic. Une méthode disponible sur r est proposée par la library ldatuning qui proposent 4 métriques d’avaluation pour différentes méthodes d’estimation ( dans notre cas c’est gibbs qui est testé). Attention la procédure peut être gourmande en temps, dans notre cas (et avec notre équipement) c’est une affaire de dizaines de minutes de calcul, - faites attention!

#result <- FindTopicsNumber(
#  dfm,
#  topics = seq(from = 5, to = 12, by = 2),
#  metrics = c("Griffiths2004", "CaoJuan2009", "Arun2010", "Deveaud2014"),
#  method = "Gibbs",
# control = list(seed = 77),
#  mc.cores = 2L,
#  verbose = TRUE
#)
#FindTopicsNumber_plot(result)

Conclusion