Obiettivo del progetto è un’analisi di text mining sulle recensioni postate su Trip Advisor per i ristoranti dei quartieri di Roma. In particolare, ho scelto di analizzare le recensioni dei ristoranti del quartiere Prenestino-Centocelle, raccogliendo i dati secondo due diverse strategie, per avere a disposizione due corpus e procedere ad un’analisi più ricca ed interessante. Le strategie utilizzate per la raccolta dei dati sono state le seguenti:
Per ogni commento, inoltre, sono state collezionate anche le meta-informazioni:
L’algoritmo di web scraping utilizzato per collezionare queste informazioni è stato scritto in Python e sfrutta le librerie Selenium
per la navigazione dinamica delle pagine web, e BeautifulSoup
, per l’individuazione e la raccolta dei dati. I dati sono stati salvati in due diversi file .csv, denominati reviews1
(dati raccolti secondo la strategia 1) e reviews2
(dati raccolti secondo la strategia 2)
Dopo aver caricato i file csv con i due dataset, è stato effettuato un veloce data cleaning trasformando il punteggio delle review in valori da 1 a 5 ed interpretando correttamente il formato della data.
Di seguito una riga di esempio del dataset:
## rating date user
## 1 5 2020-02-12 Stefano R
## review
## 1 Ci sono entrato casualmente passando di lì ed ora é una deviazione obbligata! Fa delle pizze squisite a prezzi irrisori,anche su ordinazione. Sicuramente tra le migliori 3 che ho mai mangiato in vita mia!!
Infine sono stati creati i due corpus, a partire dalla colonna review
di ciascun dataset:
corpus1 <- Corpus(VectorSource(reviews1$review)) %>% dataPreProcessing() %>% lemmatize()
corpus2 <- Corpus(VectorSource(reviews2$review)) %>% dataPreProcessing() %>% lemmatize()
Come si può vedere ai due corpus vengono applicate due funzioni: dataPreProcessing()
e lemmatize()
che si occupano rispettivamente di eseguire la normalizzazione semplice e quella morfologica dei corpus; verranno approfondite in dettaglio nei prossimi paragrafi.
Per applicare la normalizzazione semplice sui corpus è stata creata la seguente funzione:
dataPreProcessing <- function(corpus){
corpus <- tm_map(corpus, tolower)
corpus <- tm_map(corpus, replacePunctuation)
corpus <- tm_map(corpus, removeSymbols)
corpus <- tm_map(corpus, removeNumbers)
corpus <- tm_map(corpus, removeWords, stopwords("italian"))
corpus <- tm_map(corpus, removeWords, stop_commenti)
corpus <- tm_map(corpus, stripWhitespace)
corpus <- tm_map(corpus, trimws)
return(corpus)
}
La funzione prende in input un corpus e restituisce un nuovo corpus a cui vengono applicate le seguenti trasformazioni:
tm
stop_commenti
creato appositamente per lo scopo, che attinge ad una lista scaricabile qui e a cui sono state aggiunte altre parole ritenute opportune (preposizioni, verbi modali etc.)La lemmatizzazione è il processo di riduzione di una forma flessa di una parola alla sua forma canonica, detta lemma (es. svolgo -> svolgere). Si differenzia dallo stemming, che invece riduce la parola alla sua forma radice, detta tema (es. svolgo -> svolg). In ambito text mining di solito viene valutato caso per caso quale valga la pena utilizzare, o se utilizzare entrambi. In questo caso, tenendo anche conto delle successivi analisi sul testo in termini grafici e lessicali, la lemmatizzazione si rivela più efficace. Le librerie ad oggi disponibili per R e Python non sono ancora in grado di dare risultati molto soffisfacenti per la lingua italiana; tuttavia Python ha a disposizione la libreria spaCy
che implementa un lemmatizzatore in italiano con cui si ottengono risultati soddisfacenti. In questo progetto è stata utilizzata la versione di spaCy fruibile per R, ovvero spicyr()
, che offre, oltre alle funzioni di lemmatizzazione, anche il parsing del testo in token e l’individuazione delle POS (part of speech). Per applicare la lemmatizzazione sul corpus è stata creata la seguente funzione:
lemmatize <- function(corpus){
# create a list with all corpus contents
corpus_txt <- c()
for(i in 1:length(corpus)){
corpus_txt[i] <- corpus[[i]]$content
}
# parse the list and replace each token with its lemma, except for masculine singular sostantives
parsed_df <- spacy_parse(corpus_txt, tag = TRUE, entity = FALSE)
parsed_df$lemma <- ifelse(parsed_df$tag == "S__Gender=Masc|Number=Sing", parsed_df$token, parsed_df$lemma)
# re-create corpus e apply pre-processing once again
agg <- aggregate(lemma~doc_id, data = parsed_df, paste0, collapse=" ")
new_corpus <- Corpus(VectorSource(agg$lemma))
new_corpus <- dataPreProcessing(new_corpus)
return(new_corpus)
}
La funzione sostanzialmente implementa le seguenti azioni:
spacy_parse()
che produrrà una tabella con il l’id documento, la singola parola (token), la parola lemmatizzata (lemma) e il dettaglio del POS (tag). Di seguito un campione del risultato:## doc_id sentence_id token_id token lemma pos
## 1 text1 1 1 andare andare VERB
## 2 text1 1 2 trovare trovare VERB
## 3 text1 1 3 nonna nonna NOUN
## 4 text1 1 4 pranzo pranzare NOUN
## 5 text1 1 5 primi primo ADJ
## 6 text1 1 6 fatti fatto NOUN
## 7 text1 1 7 mano mano NOUN
## 8 text1 1 8 condimenti condimento NOUN
## 9 text1 1 9 freschi fresco ADJ
## 10 text1 1 10 prezzi prezzo NOUN
## tag
## 1 V__VerbForm=Inf
## 2 V__VerbForm=Inf
## 3 S__Gender=Fem|Number=Sing
## 4 S__Gender=Masc|Number=Sing
## 5 NO__Gender=Masc|Number=Plur|NumType=Ord
## 6 S__Gender=Masc|Number=Plur
## 7 S__Gender=Fem|Number=Sing
## 8 S__Gender=Masc|Number=Plur
## 9 A__Gender=Masc|Number=Plur
## 10 S__Gender=Masc|Number=Plur
Come si può notare, soprattutto per nomi maschili singolari spesso la lemmatizzazione è grossolana o imprecisa (es. ‘pranzo’ inteso come sostantivo diventa ‘pranzare’), per cui è stata adottata la scelta di non applicare la lemmatizzazione a questi casi, lasciando la parola nella forma originale. Anche questa scelta in qualche caso porta ad una piccola perdita di informazioni dovuta al fatto che anche il POS non è sempre preciso (es. alcune parole taggate come sostantivi in realtà sono verbi o viceversa), ma in generale questa strategia si rivela un buon compromesso.
Per prima cosa, si vedono le 10 parole più frequenti per il corpus 1:
Per procedere alla creazione della wordcloud per prima cosa si crea, a partire dal corpus, una matrice di termini x documenti
(tdm) dove ogni documento è la singola recensione.
La matrice è creata mediante la seguente funzione:
ndocs <-length(corpus1)
# ignore extremely rare words i.e. terms that appear in less then 2% of the documents
minTermFreq <- ndocs * 0.02
# ignore overly common words i.e. terms that appear in more than 80% of the documents
maxTermFreq <- ndocs * .80
corpus1_tdm <- TermDocumentMatrix(corpus1,
control = list(
wordLengths=c(3, 20),
bounds = list(global = c(minTermFreq, maxTermFreq),
weighting=weightTfIdf)))
Essa è stata parametrizzata di modo da considerare parole di lunghezza non inferiore alle tre e non superiore alle venti lettere. I parametri minTermFreq
e maxTermFreq
sono calcolati a partire dalla lunghezza del corpus e rappresentano i due limiti di frequenza inferiore e superiore, cioè non vengono considerate parole estremamente rare (presenti in meno del 2% dei documenti) o estremamente frequenti (presenti in oltre l’80% dei documenti). Infine per dare un peso ai termini presenti nei documenti si utilizza il metodo Tf-idf (term frequency-inverse document frequency); Tf-idf è una funzione utilizzata in information retrieval per misurare l’importanza di un termine rispetto ad un documento o ad una collezione di documenti. Tale funzione aumenta proporzionalmente al numero di volte in cui il termine è contenuto nel documento, ma cresce in maniera inversamente proporzionale con la frequenza del termine nella collezione. L’idea alla base di questo comportamento è di dare più importanza ai termini che compaiono nel documento, ma che in generale sono poco frequenti (che rappresentano dunque, il cosiddetto linguaggio specifico).
A partire da questa matrice, possiamo generare la wordcloud, mostrando i primi 100 termini più frequenti:
Per il secondo corpus si ripetono i passi già applicati al corpus 1. Di seguito le 10 parole più frequenti per il corpus 2:
Viene quindi generata la wordcloud prendendo in considerazione i primi 100 termini più frequenti:
Come è normale aspettarsi le parole nei due corpus sono sostanzialmente le stesse, anche se cambiano leggermente le frequenze con cui compaiono.
La funzione comparison.cloud
confronta la frequenza con cui un termine è stato utilizzato in due o più documenti tracciando la differenza tra l’uso di una parola al loro interno. Si applica ad un corpus creato a partire dall’unione dei corpora 1 e 2.
La funzione commonality.cloud
è complementare alla commonality.cloud e mostra solo le parole che appaiono in tutti i documenti e le loro combinazioni attraverso essi. La commonality.cloud è usata per mostrare i concetti che si sovrappongono tra due documenti.
Il Pyramid plot è un altro modo per visualizzare le parole in comune tra due corpora mostrando anche le differenze in termini di frequenza della parola in ognuno dei due dataset. Si tratta di due barplot speculari che mostrano le parole più frequenti nei corpora ordinandole in modo ascendente (da qui la forma a piramide)
## [1] 5.1 4.1 4.1 2.1
L’obiettivo dell’analisi delle corrispondenze lessicali è quello di analizzare le relazioni tra le modalità di due (o più) caratteri qualitativi. L’analisi delle corrispondenze mira ad individuare la struttura dell’associazione interna a una tabella di contingenza tramite la rappresentazione grafica delle modalità dei due caratteri in uno spazio di dimensionalità minima (solitamente il piano cartesiano).
L’analisi delle corrispondenze lessicali è stata implementata a partire da una matrice parole x valutazione in stelle
, quindi sono state suddivise le reviews presenti nel dataset in base al punteggio assegnato dall’utente ed è stato creato un corpus a partire da questo dataset. Per procedere con l’analisi è stata utilizzata la funzione CA()
della libreria factoMineR
.
Vengono mostrati di seguito i risultati dettagliati, la mappa simmetrica e quella asimmetrica per il corpus 1:
## **Results of the Correspondence Analysis (CA)**
## The row variable has 52 categories; the column variable has 5 categories
## The chi square of independence between the two variables is equal to 409.7713 (p-value = 6.290447e-16 ).
## *The results are available in the following objects:
##
## name description
## 1 "$eig" "eigenvalues"
## 2 "$col" "results for the columns"
## 3 "$col$coord" "coord. for the columns"
## 4 "$col$cos2" "cos2 for the columns"
## 5 "$col$contrib" "contributions of the columns"
## 6 "$row" "results for the rows"
## 7 "$row$coord" "coord. for the rows"
## 8 "$row$cos2" "cos2 for the rows"
## 9 "$row$contrib" "contributions of the rows"
## 10 "$call" "summary called parameters"
## 11 "$call$marge.col" "weights of the columns"
## 12 "$call$marge.row" "weights of the rows"
##
## Call:
## CA(X = dfr1)
##
## The chi square of independence between the two variables is equal to 409.7713 (p-value = 6.290447e-16 ).
##
## Eigenvalues
## Dim.1 Dim.2 Dim.3 Dim.4
## Variance 0.049 0.023 0.017 0.009
## % of var. 49.969 23.764 17.269 8.998
## Cumulative % of var. 49.969 73.733 91.002 100.000
##
## Rows (the 10 first)
## Iner*1000 Dim.1 ctr cos2 Dim.2 ctr cos2
## accogliere | 3.138 | -0.407 6.243 0.983 | 0.040 0.125 0.009 |
## amico | 0.414 | 0.105 0.323 0.385 | 0.072 0.319 0.181 |
## antipasto | 1.773 | 0.171 0.732 0.204 | -0.262 3.603 0.477 |
## aperitivo | 1.394 | -0.080 0.166 0.059 | -0.208 2.356 0.397 |
## assaggiare | 0.492 | -0.161 0.497 0.498 | -0.127 0.645 0.308 |
## assolutamente | 1.513 | -0.107 0.216 0.071 | 0.296 3.515 0.546 |
## bello | 0.379 | -0.141 0.342 0.446 | 0.016 0.009 0.006 |
## birra | 1.542 | -0.252 2.951 0.945 | -0.042 0.169 0.026 |
## carne | 3.278 | 0.165 0.690 0.104 | -0.157 1.324 0.095 |
## cenare | 1.300 | 0.062 0.132 0.050 | 0.030 0.066 0.012 |
## Dim.3 ctr cos2
## accogliere -0.017 0.030 0.002 |
## amico 0.103 0.902 0.372 |
## antipasto -0.091 0.593 0.057 |
## aperitivo 0.178 2.381 0.292 |
## assaggiare 0.064 0.229 0.079 |
## assolutamente -0.248 3.397 0.383 |
## bello -0.148 1.081 0.487 |
## birra 0.042 0.235 0.026 |
## carne 0.385 10.887 0.567 |
## cenare -0.265 7.060 0.928 |
##
## Columns
## Iner*1000 Dim.1 ctr cos2 Dim.2 ctr cos2
## cinque | 19.555 | -0.177 34.402 0.869 | 0.066 10.128 0.122 |
## quattro | 15.064 | 0.072 2.563 0.084 | -0.216 49.124 0.766 |
## tre | 13.963 | 0.255 11.707 0.414 | -0.142 7.608 0.128 |
## due | 22.736 | 0.512 19.586 0.426 | 0.162 4.103 0.042 |
## uno | 27.566 | 0.428 31.743 0.569 | 0.282 29.036 0.248 |
## Dim.3 ctr cos2
## cinque 0.015 0.697 0.006 |
## quattro -0.036 1.906 0.022 |
## tre 0.006 0.018 0.000 |
## due 0.564 68.869 0.517 |
## uno -0.238 28.509 0.177 |
L’inerzia totale (ovvero misura del grado di dispersione del profilo attorno al profilo medio) spiegata dalla mappa è il 73,8%; sacrificando una dimensione si perde circa il 30% dell’inerzia dei profili. Analizzando la posizione delle parole si può notare i termini prendere, menù, potere, cibo, mangiare, piccolo e cinese, sulla destra del piano, si contrappongono a termini come eccellere, ottimo, super, simpatico, accogliere, sicuramente. La più grande differenza tra le parole associate a punteggi alti o a punteggi bassi, e quindi in generale la principale fonte di variabilità della tabella, va ricercata in questi estremi. Dall’osservazione della posizione dei punteggi si nota come i punteggi da 1 a 4 sono tutti nella parte destra del piano, mentre il punteggio 5 è a sinistra; volendo trovare una regola che sintetizzi questa disposizione di punteggi si potrebbe dire che avviene una classificazione dicotomica tra punteggi alti e punteggi medio-bassi. Passando alla seconda dimensione, essa distingue abbastanza bene i punteggi assegnati dall’utente; le parole che si trovano sullo stesso asse orizzontale hanno percentuali di frequenza abbastanza simili anche se appaiono distanziate rispetto all’asse verticale; per esempio le parole cibo, potere e menù hanno relativamente più occorrenze in recesioni con punteggi bassi, e cinese ha relativamente più occorrenze in recensioni con punteggi più alti, ma tutte queste parole hanno percentuali di occorrenza simile.
Vengono mostrati di seguito i risultati dettagliati, la mappa simmetrica e quella asimmetrica per il corpus 2:
## **Results of the Correspondence Analysis (CA)**
## The row variable has 154 categories; the column variable has 5 categories
## The chi square of independence between the two variables is equal to 1089.945 (p-value = 2.380367e-29 ).
## *The results are available in the following objects:
##
## name description
## 1 "$eig" "eigenvalues"
## 2 "$col" "results for the columns"
## 3 "$col$coord" "coord. for the columns"
## 4 "$col$cos2" "cos2 for the columns"
## 5 "$col$contrib" "contributions of the columns"
## 6 "$row" "results for the rows"
## 7 "$row$coord" "coord. for the rows"
## 8 "$row$cos2" "cos2 for the rows"
## 9 "$row$contrib" "contributions of the rows"
## 10 "$call" "summary called parameters"
## 11 "$call$marge.col" "weights of the columns"
## 12 "$call$marge.row" "weights of the rows"
##
## Call:
## CA(X = dfr2)
##
## The chi square of independence between the two variables is equal to 1089.945 (p-value = 2.380367e-29 ).
##
## Eigenvalues
## Dim.1 Dim.2 Dim.3 Dim.4
## Variance 0.042 0.021 0.018 0.010
## % of var. 45.709 23.077 20.191 11.023
## Cumulative % of var. 45.709 68.786 88.977 100.000
##
## Rows (the 10 first)
## Iner*1000 Dim.1 ctr cos2 Dim.2 ctr cos2
## abbondare | 0.132 | 0.031 0.010 0.031 | -0.160 0.511 0.819
## accogliere | 0.453 | -0.159 0.791 0.730 | -0.046 0.130 0.061
## accompagnare | 0.140 | -0.158 0.151 0.451 | -0.039 0.018 0.027
## alto | 0.396 | 0.097 0.102 0.107 | -0.265 1.508 0.803
## ambiente | 0.246 | 0.094 0.121 0.206 | -0.023 0.014 0.012
## amico | 0.215 | -0.088 0.166 0.324 | 0.113 0.549 0.540
## ampio | 0.068 | 0.053 0.022 0.136 | -0.100 0.159 0.492
## andare | 1.245 | 0.266 1.179 0.396 | 0.293 2.840 0.481
## andato | 0.288 | 0.196 0.286 0.415 | 0.037 0.021 0.015
## antipasto | 1.056 | 0.120 0.242 0.096 | -0.366 4.415 0.882
## Dim.3 ctr cos2
## abbondare | -0.063 0.091 0.128 |
## accogliere | 0.015 0.015 0.006 |
## accompagnare | 0.054 0.040 0.053 |
## alto | 0.038 0.036 0.017 |
## ambiente | -0.077 0.184 0.138 |
## amico | -0.014 0.009 0.008 |
## ampio | -0.075 0.103 0.280 |
## andare | -0.039 0.057 0.008 |
## andato | -0.192 0.620 0.397 |
## antipasto | 0.018 0.013 0.002 |
##
## Columns
## Iner*1000 Dim.1 ctr cos2 Dim.2 ctr cos2
## cinque | 11.333 | -0.121 24.054 0.887 | 0.039 4.850 0.090
## quattro | 16.534 | 0.153 12.262 0.310 | -0.204 42.944 0.548
## tre | 22.018 | 0.521 39.237 0.745 | -0.018 0.095 0.001
## due | 21.344 | 0.477 12.930 0.253 | 0.508 29.091 0.288
## uno | 20.209 | 0.694 11.517 0.238 | 0.698 23.020 0.240
## Dim.3 ctr cos2
## cinque | 0.008 0.265 0.004 |
## quattro | -0.018 0.380 0.004 |
## tre | 0.058 1.098 0.009 |
## due | -0.624 50.057 0.433 |
## uno | 0.944 48.201 0.440 |
Per ragioni grafiche, sono state visualizzate sulla mappa solo le prime 50 (di 154) parole del dataset. L’inerzia totale spiegata dalla mappa è il 68,8%; sacrificando una dimensione si perde circa il 30% dell’inerzia dei profili. Possono essere fatte considerazioni simili a quelli fatti sulla mappa del corpus 1 per quanto riguarda la disposizione delle parole sull’asse orizzontale e verticale. Si può notare che nella mappa di questo corpus, le classi di punteggio sono meglio distinte e più distanziate tra di loro.
Nell’ambito del text mining, le tecniche di clustering sono algoritmi di machine learning di tipo non supervisionato, che classificano e raggruppano insiemi di parole in base, generalmente, a degli indici di similarità. Esistono diverse tipologie di clustering e diversi algoritmi; i più noti sono:
In questo progetto verrà utilizzato un algoritmo per ogni tipologia (k-means, hierarchical, dbscan), e verranno confrontati i risultati. Il dataset preso in considerazione è stato creato a partire dal merge dei due corpora.
L’algoritmo k-means partiziona gli oggetti in cluster in base alla loro similarità. Anzitutto, bisogna specificare il numero di cluster in cui i dati andranno raggruppati; inizialmente l’algoritmo assegna casualmente ogni osservazione ad un cluster, e trova i centroidi di ogni cluster, successivamente vengono iterati i seguenti steps:
Questi due steps vengono ripetuti finchè la varianza all’interno del cluster non può essere ridotta oltremodo. La varianza all’interno dei clusters (within) è calcolata come somma della distanza euclidea tra gli oggetti ed i centroidi dei relativi clusters.
Una questione fondamentale è come determinare il valore del parametro k, ovvero il numero dei clusters. Empiricamente, andrebbe scelto il numero di cluster per cui l’aggiunta di un ulteriore cluster non comporterebbe una migliore modellazione dei dati, ovvero, non spiegherebbe ulteriormente la varianza. Esistono vari metodi per determinare il numero ottimale dei cluster, in questa analisi viene utilizzato l’elbow method; viene simulato l’algoritmmo con diversi valori di k e si osserva il grafico:
#kmeans – determine the optimum number of clusters (elbow method)
#look for “elbow” in plot of summed intra-cluster distances (withinss) as fn of k
#Elbow Method for finding the optimal number of clusters
# Compute and plot wss for k = 2 to k = 8.
k.max <- 8
wss <- sapply(1:k.max,
function(k){kmeans(all_rev_corpus_m, k, nstart=50,iter.max = 15 )$tot.withinss})
plot(1:k.max, wss,
type="b", pch = 19, frame = FALSE,
xlab="Number of clusters K",
ylab="Total within-clusters sum of squares")
abline(v=4, col="blue", lty=2)
Il punto in cui il grafico mostra il “gomito”, ovvero il punto dopo il quale la decrescita della curva tende a stabilizzarsi, rappresenta il punto in cui la distanza intra-cluster è ottimale. In questo caso il gomito non è particolarmente visibile, ma si nota che dopo il valore 4-5 la funzione descresce più lentamente, per cui prenderemo 5 come valore di k.
set.seed(123)
clustering.kmeans <- eclust(all_rev_corpus_m, "kmeans", k = 5, nstart = 50)
I metodi gerarchici generano una struttura ad albero; La maggior parte di essi utilizza algoritmi agglomerativi dove, partendo da una situazione in cui ciascun oggetto corrisponde ad un cluster, per passi successivi, gli oggetti si fondono per formare cluster sempre più grandi sino ad arrivare ad un unico cluster (strategia bottom-up). Gli algoritmi divisivi utilizzano una strategia top-down, dove partendo da un solo cluster iniziale contenente tutti i dati osservati si procede per partizioni successive sino ad arrivare ad un cluster per ciascun oggetto. In entrambi gli algoritmi, un cluster ad un livello della gerarchia comprenderà tutti i cluster di livello più basso. Questo significa che se un oggetto è assegnato ad un certo cluster, questo non potrà mai essere riassegnato ad un altro cluster. Questa è una distinzione importante rispetto ai metodi non-gerarchici (come il k-means).
I metodi gerarchici utilizzano una matrice di distanze come input per l’algoritmo di clustering. La scelta delle metriche appropriate influenzerà la forma dei clusters. Esistono diverse distanze con si può costruire la matrice: euclidea, di Manhattan, di Mahalanobis, coseno (che non è propriamente una distanza ma una similarità). Inoltre si deve stabilire il criterio con cui verranno calcolate le distanze (legame singolo, completo, medio, del centroide, metodo di Ward).
In questa analisi viene utilizzata la similarità coseno, generalmente vantaggiosa come metrica per misurare la distanza quando la grandezza dei vettori non è importante (e questo di solito è il caso in cui si lavora con dati testuali rappresentati da frequenze); queste distanze saranno calcolate con il criterio di Ward che minimizza la varianza intra-cluster.
# Cosine distance matrix
distMatrix = dist(all_rev_corpus_m, method = "cosine")
clustering.hierarchical <- hcut(distMatrix, k = 5, stand = TRUE, hc_method="ward.D2")
fviz_dend(clustering.hierarchical, rect = TRUE, cex = 0.4,
k_colors = c("#00AFBB", "#EB8C21", "#2E9FDF", "#E7B800", "#FC4E07"))
Di seguito si possono vedere le prime 10 parole presenti in ogni cluster per i due algoritmi.
Kmeans:
## cluster
## pizza 1
## locale 2
## sicuramente 3
## trovare 3
## centocelle 3
## tornare 3
## veramente 3
## amico 3
## cenare 3
## consigliare 3
## gentile 3
## mangiare 3
## prezzo 4
## qualità 4
## ottimo 4
## entrare 5
## migliorare 5
## passare 5
## squisito 5
## andare 5
## condimento 5
## fresco 5
## insomma 5
## mano 5
## materia 5
Hierarchical clustering:
## cluster
## entrare 1
## aspettare 1
## bevanda 1
## confermare 1
## conto 1
## prenotazione 1
## finire 1
## persona 1
## appena 1
## chiedere 1
## migliorare 2
## condimento 2
## fresco 2
## mano 2
## materia 2
## norma 2
## pranzo 2
## tiramisù 2
## cuocere 2
## pietanza 2
## passare 3
## squisito 3
## insomma 3
## aperitivo 3
## taglieri 3
## curare 3
## giovane 3
## numeroso 3
## possibilità 3
## ritornare 3
## pizza 4
## prezzo 4
## sicuramente 4
## andare 4
## qualità 4
## trovare 4
## centocelle 4
## locale 4
## tornare 4
## amico 4
## aspettativa 5
## aspetto 5
## farcire 5
## ristorante 5
## costo 5
## datare 5
## pagare 5
## carta 5
## chiamare 5
## problema 5
In base a quanto visto finora possiamo fare le seguenti osservazioni:
DBSCAN è uno degli algoritmi più conosciuti di clustering basato su densità; esso raggruppa punti che sono vicini l’un l’altro basandosi su una distanza (tra quelle nominate prima) ed un numero minimo di punti. Inoltre marca come outliers i punti che si trovano nelle regioni a bassa densità. A differenza degli altri algoritmi, DBSCAN non produce un numero pre-determinato di clusters, ma individua quanti più clusters possibili basandosi su due parametri:
Come in ogni problema di data mining, la scelta dei parametri è la parte più delicata. In particolare, per il valore eps, se questo è troppo piccolo molti dei dati non verranno clusterizzati, ma saranno considerati outliers perchè non soddisfano il minimo numero di punti per creare una regione di densità. D’altra parte, se il valore di eps è troppo alto, i cluster si fonderanno e la maggior parte degli oggetti sarà all’interno dello stesso cluster. Il parametro eps dovrebbe essere scelto in base alle distanze sul dataset.
Per quanto riguarda invece il parametro minPoints, in linea generale esso dovrebbe essere calcolato in base al numero di dimensioni del dataset (D), secondo la regola per cui minPoints ≥ D + 1. Valori più alti si usano di solito per dataset con molto “rumore” di modo che vengano formati più cluster significativi. Il numero minimo dovrebbe essere 3, ma maggiore è il dataset, e maggiore è il numero di minPoints che dovrebbero essere scelti.
Per determinare eps verrà utilizzato anche stavolta una sorta di metodo elbow che cosiste nel calcolare la distanza tra i k-nearest-neighbors (i punti più vicini ad un punto k) in una matrice di punti. L’idea è calcolare la media delle distanze di ogni punto dal suo k-nearest-neighbor; il valore di k sarà specificato a priori e corrisponde ai minPoints. Le k-distanze vengono quindi graficate ed il punto del grafico in cui si riconosce il “gomito” corrisponde al valore ottimale di eps.
Viene posto a 5 il valore di minPoints ed ottengo il seguente grafico:
La scelta dei parametri si è rivelata ottimale, perchè come si può vedere vengono individuati 5 clusters (i punti neri non rientrano in nessun cluster perchè si trovano in zone a bassa intensità), in accordo con le considerazioni fatte nei precedenti algoritmi.
Le analisi che seguono sono aggiuntive ed esulano dallo scopo del progetto ma possono essere ulteriormente utili ai fini dell’analisi testuale.
L’analisi delle parole prese a coppie, a terne, o in generale a tuple, è utile a capire come all’interno di un corpus alcune parole assumano un certo valore anche in relazione alle parole a cui sono più spesso associate. In questo documento procederemo ad analizzare i bigrams (coppie di parole) e i trigrams (terne di parole)
Visualizzazione tramite wordcloud:
I dati sono stati rappresentati anche come una rete di parole con struttura a grafo:
Visualizzazione tramite wordcloud:
I dati sono stati rappresentati anche come una rete di parole con struttura a grafo:
Avendo a disposizione nel dataset anche la variabile temporale (data della recensione), possiamo sfruttarla per analizzare nel tempo il trends delle recensioni o di alcune parole specifiche. Questa analisi potrebbe rivelarsi utile, se calata su un singolo ristorante, a comprendere nel tempo quali sono le parole più usate da un utente per descrivere il ristorante (e studiare questo andamento anche il relazione ad altri fattori come per esempio nuova apertura, cambio di gestione, restyling del ristorante, cambio dello chef, etc.). Ancora, di potrebbero confrontare i trends relativi alle recensioni di un determinato quartiere di Roma con quelli dei ristoranti di altri quartieri di Roma, e vedere come cambiano i dati a seconda della zona (e studiare questo parametro per portare alla luce eventuali differenze socio-economico-culturali tra un quartiere e l’altro).
## [1] "2013-05-28"
## [1] "2020-02-23"
Il grafico seguente mostra le parole che hanno avuto trend crescente molto veloce nel corso degli anni:
E quelle che invece hanno trend crescente più lento nel corso degli anni:
Possono essere valutate anche coppie di parole per studiarne la differenza in termini di interesse nelle recensioni utente:
Per ragioni di tempo non sono state implementate ulteriori applicazioni di quest’analisi testuale. Tuttavia si riportano una serie di possibili applicazioni che potrebbero scaturire a partire da quest’analisi: