statistica

Clustering delle keyword: raggruppare migliaia di query con K-means e clustering gerarchico

Capita con ogni progetto un po’ serio: si esporta l’elenco delle keyword da Search Console o da un tool, e ci si ritrova davanti a migliaia di righe. Tremila, diecimila query. Leggerle una per una è impensabile, e raggrupparle a mano “a sentimento” è un lavoro lento, soggettivo e impossibile da rifare.
Eppure quel raggruppamento ci serve: vogliamo capire quali grandi famiglie di ricerche esistono nel nostro mercato, per decidere dove creare contenuti, quali pagine costruire, su cosa puntare.
La domanda è: possiamo lasciare che siano i dati a rivelarci i gruppi, invece di imporli noi? Trasformare quella montagna di query in pochi insiemi omogenei è il lavoro del clustering delle keyword.

Abbiamo già affrontato un problema vicino, classificare l’intento di una query con il Naive Bayes — ma lì avevamo un ingrediente che oggi ci manca: un insieme di esempi già etichettati da cui imparare. Qui le etichette non ce le ha date nessuno. È il territorio del clustering, uno degli strumenti più usati del machine learning, e in questo articolo lo costruiamo in R con i suoi due algoritmi classici: il K-means e il clustering gerarchico.

Di cosa parleremo:


Raggruppare senza etichette: l’idea del clustering

La differenza con il Naive Bayes è di principio, non di dettaglio. Lì facevamo apprendimento supervisionato: avevamo query già marcate come informazionali, navigazionali o transazionali, e insegnavamo all’algoritmo a riconoscere le nuove. Qui facciamo apprendimento non supervisionato: nessuno ci ha detto quali e quanti gruppi esistano. Il clustering non verifica un’etichetta che già conosciamo: cerca una struttura che non sapevamo ci fosse.

Per raggruppare servono due cose. La prima è descrivere ogni keyword con dei numeri: nel nostro esempio useremo il volume di ricerca, il costo per clic (cpc), la posizione media e il numero di parole.
La seconda è una nozione di distanza: due keyword sono “vicine” se i loro numeri si somigliano. La distanza più comune è quella euclidea, la stessa che useremmo su una mappa, solo calcolata in uno spazio a quattro dimensioni (una per ogni metrica).

C’è però una trappola da disinnescare subito. Il volume si misura in decine di migliaia, il cpc in centesimi di euro: lasciate così, le distanze sarebbero dominate dal volume, e il cpc non conterebbe quasi nulla.
Prima di calcolare qualsiasi distanza dobbiamo mettere tutte le variabili sulla stessa scala, standardizzandole — in R con la funzione scale(), che a ogni colonna sottrae la media e la divide per la deviazione standard. Solo a quel punto un euro di differenza nel cpc e diecimila ricerche di differenza nel volume “pesano” allo stesso modo.


K-means: i centroidi e il problema di scegliere k

L’idea del K-means è quasi ingenua nella sua semplicità. Decidiamo in quanti gruppi (k) vogliamo dividere i dati; l’algoritmo piazza k punti-rappresentanti, i centroidi, e poi ripete due passi finché le cose si stabilizzano: assegna ogni keyword al centroide più vicino, poi sposta ogni centroide nel centro delle keyword che gli sono state assegnate. Ad ogni giro i gruppi diventano un po’ più coesi, finché non si muovono più.

Ciò che l’algoritmo cerca di minimizzare, a parole, è la dispersione interna dei gruppi: la somma delle distanze (al quadrato) di ogni punto dal centroide del proprio cluster. In formula:

\( \text{WCSS} = \sum_{k=1}^{K} \sum_{x \in C_k} \lVert x – \mu_k \rVert^2 \\ \)

dove \( C_k \) è il k-esimo cluster, \( \mu_k \) il suo centroide e la doppia sommatoria scorre tutti i punti di tutti i gruppi. Più la WCSS (within-cluster sum of squares) è bassa, più i gruppi sono compatti.

Resta il punto dolente: k lo dobbiamo decidere noi, prima di iniziare. Un aiuto viene dal metodo del gomito: proviamo diversi valori di k e guardiamo come cala la WCSS. All’inizio aggiungere un cluster aiuta molto, poi i miglioramenti si fanno marginali; il “gomito” della curva — il punto dove la discesa si appiattisce — suggerisce un k ragionevole. Costruisco la tabella di keyword e calcolo la WCSS da 1 a 6 gruppi in R:

kw <- data.frame(
  keyword = c("scarpe running","scarpe running uomo","nike pegasus","nike pegasus 40",
              "migliori scarpe trail 2026","come scegliere scarpe running",
              "scarpe running offerta","comprare scarpe trail online",
              "differenza scarpe trail e strada","scarpe running pronazione",
              "asics gel nimbus","saucony endorphin","recensione scarpe trail",
              "scarpe running scontate","negozio scarpe running milano"),
  volume   = c(40000,18000,12000,8000,2400,1900,3200,880,1300,2100,9000,4000,1500,2600,720),
  cpc      = c(0.45,0.55,0.30,0.35,0.40,0.10,0.95,1.10,0.08,0.30,0.28,0.33,0.15,0.90,0.85),
  posizione= c(3.1,4.2,2.0,5.5,8.1,11.2,6.0,9.4,14.0,7.3,2.5,6.8,12.1,5.9,4.7),
  n_parole = c(2,3,2,3,5,5,3,4,6,3,3,2,3,3,4)
)

# standardizzo le quattro metriche (scale diverse -> stesso peso)
X <- scale(kw[, c("volume","cpc","posizione","n_parole")])

# metodo del gomito: WCSS per k da 1 a 6
set.seed(1)
wss <- sapply(1:6, function(k) kmeans(X, centers = k, nstart = 10)$tot.withinss)
round(wss, 1)
# [1] 56.0 34.4 20.7 12.4  8.7  6.9

Il calo è ripido fino a tre gruppi (56 → 34 → 21) e poi rallenta nettamente (21 → 12 → 9 → 7). Il gomito non è mai una linea netta — è una lettura, non un teorema — ma qui indica con ragionevolezza k = 3. Lancio allora il K-means con tre centroidi:

set.seed(1)
km <- kmeans(X, centers = 3, nstart = 25)
kw$cluster <- km$cluster
table(km$cluster)
# 1 2 3
# 4 7 4

n.b. l’argomento nstart = 25 fa ripartire l’algoritmo 25 volte da centroidi iniziali diversi, tenendo la soluzione migliore: il K-means può infatti incagliarsi in un minimo locale a seconda di dove parte, e ripartire più volte è la difesa standard. Il set.seed(1) serve solo a rendere l’esempio riproducibile (compresa la numerazione dei cluster, che di per sé è arbitraria).


Leggere i cluster: chi sono questi gruppi?

Avere tre gruppi non serve a niente finché non capiamo cosa rappresentano. Il modo più diretto è guardare le metriche medie di ciascun cluster.
Le calcolo sulla scala originale (non quella standardizzata, che è illeggibile):

aggregate(kw[, c("volume","cpc","posizione","n_parole")],
          by = list(cluster = kw$cluster), FUN = mean)
#   cluster volume  cpc posizione n_parole
# 1       1   1775 0.18     11.35     4.75
# 2       2  13300 0.37      4.49     2.57
# 3       3   1850 0.95      6.50     3.50

Adesso i gruppi parlano.
Il cluster 2 raccoglie le query a volume altissimo (in media 13.300 ricerche), corte (due-tre parole), ben posizionate e con cpc modesto: sono le teste generiche e di marca — “scarpe running”, “nike pegasus”, “asics gel nimbus”. Il cluster 1 ha volumi bassi, query lunghe (quasi cinque parole), posizioni arretrate e cpc minimo: è il long-tail informazionale — “come scegliere scarpe running”, “differenza scarpe trail e strada”, “recensione scarpe trail”. Il cluster 3 si distingue per un cpc altissimo (0,95 €) e contiene le query a chiaro taglio commerciale — “scarpe running offerta”, “comprare scarpe trail online”, “scarpe running scontate”, “negozio scarpe running milano”.

Vale la pena fermarsi un istante su cosa è appena successo. Senza dare all’algoritmo alcuna etichetta, i tre gruppi che emergono ricalcano da vicino una distinzione per intento — ricerca informativa, teste generiche e di marca, query commerciali — vicina a quella che con il Naive Bayes avevamo invece dovuto insegnargli a colpi di esempi.
L’allineamento, attenzione, non è magia: emerge perché le metriche che abbiamo scelto (cpc, lunghezza, posizione) tracciano indirettamente l’intento, non perché il clustering lo conosca — di significato delle query, qui, non ne ha vista neppure una parola. La lettura resta comunque immediatamente operativa: il cluster informativo chiede articoli e guide, quello commerciale pagine prodotto e di offerta, quello ad alto volume pagine pilastro robuste.


Clustering gerarchico: il dendrogramma

Il K-means ci ha costretti a scegliere k in anticipo. Il clustering gerarchico ribalta l’approccio: non decide nulla a priori, e costruisce invece un albero completo di raggruppamenti. Parte da ogni keyword come gruppo a sé, poi fonde via via le due più vicine, poi le due più vicine fra quelle rimaste, e così via fino a un unico grande gruppo. Il risultato è un dendrogramma: un albero che mostra a quale “altezza” (cioè a quale distanza) ogni fusione avviene.

Lo costruisco in R calcolando prima la matrice delle distanze, poi l’albero:

d  <- dist(X)                       # distanze euclidee fra le keyword standardizzate
hc <- hclust(d, method = "ward.D2") # albero gerarchico (criterio di Ward)
plot(hc, labels = kw$keyword)       # il dendrogramma

Il bello del dendrogramma è che la decisione su quanti gruppi tenere la prendiamo dopo averlo visto, semplicemente “tagliandolo” a un’altezza scelta: un taglio basso lascia molti gruppetti, un taglio alto pochi gruppi grandi. Taglio a tre gruppi e confronto con il K-means:

kw$cluster_hc <- cutree(hc, k = 3)
table(kw$cluster_hc)
# 1 2 3
# 8 3 4

La partizione non è identica a quella del K-means (qui i gruppi sono da 8, 3 e 4 keyword), ed è normale: i due metodi ottimizzano criteri diversi e su pochi dati le differenze si notano.
Ma la sostanza dei raggruppamenti — il blocco transazionale ad alto cpc, le teste ad alto volume, la coda informazionale — resta riconoscibile in entrambi. Quando due metodi diversi convergono sulla stessa storia, possiamo fidarci un po’ di più di quella storia.


Quale metodo, e le trappole

La scelta fra i due, nella pratica, segue poche regole semplici. Il K-means è veloce ed efficiente anche su decine di migliaia di keyword, ma vuole che gli si dica k e tende a costruire gruppi di forma “sferica” e di dimensioni simili. Il gerarchico non chiede k in anticipo e regala il dendrogramma — prezioso per vedere come i gruppi si annidano l’uno nell’altro — ma diventa pesante quando le keyword sono troppe. La pratica più comune: esplorare in gerarchico su un campione per farsi un’idea del numero di gruppi, poi applicare il K-means all’intero set con il k così individuato.

Un avvertimento, il più importante di tutti: il clustering trova sempre dei gruppi, anche quando non ce ne sono. Diamogli del rumore puro e ce lo dividerà diligentemente in k cluster ordinati. Il numero di gruppi, le metriche scelte per descrivere le keyword, la standardizzazione: sono tutte decisioni nostre, e ognuna cambia il risultato. Un raggruppamento non è una verità scoperta nei dati, è un’ipotesi di lavoro che ha senso solo se regge alla prova del buon senso di business. Se un cluster non riusciamo a spiegarlo a parole, probabilmente non esiste davvero.

C’è poi una questione di dimensioni. Qui abbiamo usato quattro metriche, ma nella realtà operativa le variabili descrittive di una keyword possono essere molte di più, e con tante dimensioni le distanze perdono di significato (tutto tende a sembrare ugualmente lontano).
È esattamente il problema che l’analisi delle componenti principali sa alleggerire, comprimendo molte metriche in poche componenti prima di passare il testimone al clustering.


Prova tu

Il modo migliore per capire il clustering è vederlo cambiare risposta al variare delle scelte. Riprendendo il codice qui sopra:

  1. Salta la standardizzazione: lancia il K-means direttamente su kw[, c("volume","cpc","posizione","n_parole")] senza scale(). I gruppi crollano tutti sul volume? È la dimostrazione pratica di perché si standardizza.
  2. Cambia k: prova con quattro o cinque gruppi e rileggi le medie. Il cluster informazionale si spezza in sotto-temi sensati o stai solo tagliando il rumore?
  3. Cambia il criterio di fusione del gerarchico: method = "complete" o "average" al posto di "ward.D2". Il dendrogramma cambia forma? E i gruppi tagliati a k=3?

Suggerimento: tieni sempre d’occhio le medie per cluster con aggregate(). È lì, e non nel codice, che si decide se un raggruppamento è utile o è solo una partizione elegante di niente.


Abbiamo raggruppato le keyword in base a come si comportano — volume, costo, posizione.
Ma resta fuori la cosa che per un SEO conta di più: il loro significato. Due query possono avere metriche diverse e voler dire la stessa cosa, oppure metriche simili e intenti opposti. Raggruppare per senso, e non solo per numeri, significa trasformare il testo stesso delle query in qualcosa di misurabile: è il mestiere del text mining, dove le parole diventano vettori e la similarità si calcola sul linguaggio. Ed è da lì che ripartiremo.


Per approfondire

Se vuoi approfondire il clustering — K-means, metodi gerarchici, la scelta del numero di gruppi e le insidie dell’interpretazione — An Introduction to Statistical Learning di James, Witten, Hastie e Tibshirani dedica all’apprendimento non supervisionato un capitolo limpido, con i laboratori in R che ricalcano esattamente i passi che abbiamo visto qui. È il riferimento che consiglio a chi vuole passare dall’esempio giocattolo al clustering su dati veri.

Paolo Gironi

Recent Posts

CTR atteso vs reale: trovare le pagine che rendono meno della loro posizione

Chi passa le giornate dentro Search Console conosce quel piccolo fastidio: una pagina è stabilmente…

1 giorno ago

Naive Bayes: classificare l’intento delle query con il teorema di Bayes

Nell'articolo sul multi-armed bandit abbiamo usato Bayes per decidere fra varianti: spostare il traffico verso…

2 giorni ago

Multi-armed bandit: ottimizzare le varianti mentre il test è ancora in corso

Nell'articolo sull'A/B test bayesiano abbiamo confrontato due varianti a campione fisso: si raccolgono i dati…

3 giorni ago

A/B test bayesiano: non solo “se” B è meglio di A, ma “di quanto”

Abbiamo avuto modo di esaminare, nell'articolo sull'A/B testing classico, come confrontare due varianti con il…

4 giorni ago

Stima bayesiana di un conversion rate: quanto possiamo fidarci dei pochi dati che abbiamo

Abbiamo avuto modo di esaminare, nell'articolo sulle fondamenta della statistica bayesiana, come l'aggiornamento bayesiano funzioni…

5 giorni ago

Il peeking problem: perché sbirciare l’A/B test gonfia i falsi positivi

Il 21 gennaio 2015 Optimizely — una delle piattaforme di A/B testing più usate al…

7 giorni ago