1. Introduzione

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:

  1. la valutazione in stelle
  2. la data di recensione
  3. il nickname

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)

2. Text Mining

2.1 Data Pre-processing

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.

2.1.1. Normalizzazione semplice

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:

  • toLower: rende omogeneo il corpus trasformando l’intero testo in minuscolo
  • replacePunctuation: rimuove la punteggiatura
  • removeSymbols: rimuove alcuni simboli (es. €)
  • removeNumbers: rimuove i numeri
  • removeWords: rimuove le stopwords in un duplice modo:
    • Tramite il set di stopwords in italiano integrate nella libreria tm
    • Tramite un vettore chiamato 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.)
  • stripWhiteSpace: trova spazi bianchi multipli e li sostituisce con un solo spazio bianco
  • trimws: elimina eventuali spazi bianchi all’inizio e alla fine di una frase

2.1.2. Normalizzazione morfologica

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:

  • Partendo dal corpus originale aggrega tutti i contents del corpus in un’unica lista.
  • Viene passata questa lista alla funzione 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.

  • Viene dunque ricostruito il corpus di partenza con i documenti lemmatizzati. Dal momento che questo processo potrebbe introdurre delle stopwords, si applica alla fine il pre-processing ancora una volta.

2.2 Analisi esplorativa

2.2.1. Creazione di nuvole di parole (wordclouds)

2.2.1.1. Corpus 1

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:

2.2.1.2. Corpus 2

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.

2.2.1.3. Comparazione tra i due corpus

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

2.2.2. Creazione della mappa di parole (analisi delle corrispondenze lessicali)

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.

2.2.3. Clustering

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:

  • Clustering Partizionale: K-means, CLARA, PAM
  • Clustering gerarchico: agglomerativo (ascendente), divisivo (discendente)
  • Clusering basato sulla densità: DBSCAN, DJCLUSTER

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.

2.2.3.1. K-means

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:

  • si riassegna l’oggetto al cluster il cui centroide è più vicino
  • si ricalcola un nuovo centroide per ogni cluster

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)

2.2.3.2. Hierarchical Clustering

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"))

2.2.3.3 Kmeans vs Hierarchical Clustering

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:

  • L’algoritmo K-means è sicuramente preferibile quando si hanno enormi quantità di dati, perchè la sua complessità nel tempo è lineare, cioè O(n) mentre quella del hierarchical clustering è quadratica, cioè O(\(n^{2}\)).
  • In K-means, dal momento che si parte con una scelta casuale di clusters, i risultati prodotti facendo girare l’algoritmo più volte potrebbe essere differenti (per questo settiamo il seme prima dell’esecuzione). Hierarchical clustering è più facilmente riproducibile.
  • K-means performa meglio quando la forma dei cluster è iper-sferica (cerchi nel piano 2D o sfere nel piano 3D). I dati presi in esame per esempio non godevano di questa proprietà come si più vedere dai confini delle aree dei clusters che non sono ellittici ma segmentati.
  • K-means richiede la conoscenza preliminare del parametro K, ovvero il numero di cluster in cui saranno divisi i dati. In hierarchical clustering si può valutare, in base all’osservazione e all’interpretazione del dendogramma, il numero di clusters più appropriato. Dai risultati appena mostrati si può vedere come prendendo in considerazione lo stesso numero di cluster, hierarchical clustering abbia prodotto risultati più omogeneneamente distribuiti tra i clusters, mentre nel K-means si sono creati cluster molto numerosi ed altri contenenti un solo termine o due.

2.2.3.4. DBScan

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:

  • eps: specifica quanto vicini dovrebbero essere due punti per essere considerati parte dello stesso cluster. Questo vuol dire che se la distanza tra due punti è minore o uguale a questo valore, allore questi punti sono considerati vicini.
  • minPoints: minimo numero di punti per formare una regione di densità. Quindi per esempio, settando questo valore a 5, c’è bisogno almeno di 5 punti per formare una regione di densità

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.

2.3. Ulteriori Analisi

Le analisi che seguono sono aggiuntive ed esulano dallo scopo del progetto ma possono essere ulteriormente utili ai fini dell’analisi testuale.

2.3.1. Analisi degli n-grams

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)

2.3.1.1. Bigram

Visualizzazione tramite wordcloud:

I dati sono stati rappresentati anche come una rete di parole con struttura a grafo:

2.3.1.2. Trigram

Visualizzazione tramite wordcloud:

I dati sono stati rappresentati anche come una rete di parole con struttura a grafo:

3. Sviluppi futuri

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: