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.
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é
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.
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.
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))
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")
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 :
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 :
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
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.
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])
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
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.
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 :
https://www.poltext.org/fr/donnees-et-analyses/lexicoder
le package syuzhet
Liu, Bing. 2010. “Sentiment Analysis and Subjectivity.” Handbook of natural language processing 2: 627–66.
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
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
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)