Twitter in segmenten 3: clusteren met k-means en K-medioids

Twitter in segmenten 3: clusteren met k-means en K-medioids

Welkom bij mijn derde post over Twitter-accounts segmenteren in R! Na mijn vorige post over hiërarchisch clusteren, wil ik vandaag een andere manier van clusteren uitproberen: k-means clusteren en k-medioid clusteren. Dit zijn geheel andere benaderingen voor het groeperen van observaties en ik ben dan ook benieuwd of de resultaten vergelijkbaar zijn met de resultaten van hiërarchisch clusteren.

Iets over k-means
Clusteren door middel van k-means verschilt wezenlijk van hiërarchisch clusteren. Waar bij hiërarchisch clusteren observaties die op elkaar lijken bij elkaar worden gegroepeerd en zo langzaamaan clusters vormen, specificeer je bij k-means van tevoren al het aantal clusters. Het k-means algoritme kent als eerste random posities toe aan dit aantal cluster-centers. Vervolgens wordt van alle observaties de afstanden naar deze cluster-centers berekend en komen de observaties te vallen onder het cluster waar ze het dichtst bij liggen. Per cluster wordt dan van alle observaties een gemiddelde berekend en in de volgende stap verschuiven de cluster-centers naar deze nieuwe locatie. Daarna wordt opnieuw bekeken welke observaties nu bij welk cluster horen, en wat de gemiddelde positie hiervan is. Uiteindelijk zijn de optimale waarden gelokaliseerd en vallen alle observaties onder het dichtstbijzijnde cluster. In R is k-means clusteren eenvoudig uit te voeren met de basisfunctie kmeans(), en voor deze post gebruik ik verder ggplot2, dplyr, reshape, cluster, purrr en factoextra.

library(ggplot2)
library(dplyr)
library(reshape)
library(factoextra)
library(cluster)
library(purrr)

Clusters kiezen met kmeans
De data die ik ga gebruiken is opnieuw mijn Twitter-dataset bestaande uit 414 accounts, die ik in een eerdere post heb binnengehaald, voorbereid en de treffende naam werkbestand3 heb gegeven. In eerste instantie had ik 14 variabelen, maar omdat een aantal tijd-variabelen erg kleine clusters creëerden, heb ik deze eruit gehaald en houd ik 6 variabelen over. Dit zijn het aantal tweets per dag, procent retweets, procent replies, procent tweets in het weekend, populariteit van het account (populair) en het aantal populaire tweets (trendsetter). Omdat dit verschillende meet-eenheden zijn, ga ik mijn data eerst standaardiseren.

scaled <- scale(werkbestand3)

Zoals ik al schreef, kies je bij k-means van tevoren een clusteraantal. Soms weet je dit of heb je al een vermoeden, maar in mijn dataset is dit niet het geval: ik wil k-means juist gebruiken om mijn data te verkennen. Een manier om dit op te lossen is door k-means meerdere keren uit te voeren met verschillende aantallen clusters, om vervolgens de variatie binnen de clusters te plotten om te zien op welk punt deze het sterkst daalt.

#voer k-means voor 20 waarden uit
tot_withinss <- map_dbl(1:20,  function(k){
	model <- kmeans(x = scaled, 
		centers = k, 
		iter.max = 400, 
		nstart = 50)
model$tot.withinss
})

#verzamel de data in een dataframe
elbow_df <- data.frame(k = 1:20, 
	tot_withinss = tot_withinss)

#plot de data in een scree plot
ggplot(elbow_df, aes(x = k, y = tot_withinss)) + 
	geom_line() + 
	scale_x_continuous(breaks = 1:20) + 
	ylab(NULL)

Deze scree plot is helaas niet makkelijk te interpreteren. Normaal gesproken zoek je naar een ‘elleboog’ in de data, een punt waar de daling sterk afneemt, maar dat punt is nu niet direct duidelijk. De variatie daalt zeer sterk tot 4 clusters en daalt vervolgens iets minder sterk tot 6. Bij cluster 7 lijkt de daling iets af te nemen, maar van 7 tot 8 is de daling weer wat sterker. Daarna neemt de daling sterker af. Aan de hand van dit plot lijken 4, 6 of 8 clusters mij realistische opties. Voor nu kies ik voor 8 clusters.

#voer kmeans uit met 8 clusters
clust_km8 <- kmeans(scaled, 
	centers = 8, 
	nstart = 50, 
	iter.max = 500)

Daarna voeg ik deze toe aan mijn dataset en maak ik twee bestanden: een met de informatie over de clusters en een met alle accounts en de clusters waartoe ze behoren.

#koppel clusters aan dataset
werkbestandclusters2 <- werkbestand2b %>%
mutate(cluster = clust_km6$cluster) %>%
group_by(cluster) %>%
mutate(count = n())

#maak een geaggregeerd bestand met informatie over clusters
aggwerkbestandclusters2 <- aggregate(werkbestandclusters2[, 2:8], 
	list(werkbestandclusters2$cluster), 
	mean)

aggwerkbestandclusters2 <- left_join(aggwerkbestandclusters2, 
	distinct(werkbestandclusters2[,8:9]))

Vervolgens visualiseer ik de cluster-scores per variabel, de verdeling over de clusters en maak ik een boxplot om de verdeling per variabel te bekijken.

#plot de scores per variabel
melt3 <- melt(aggwerkbestandclusters2[,2:8],  
	id.vars = 'cluster', 
	variable.name = 'variabel')
ggplot(melt3, aes(cluster,value)) + 
	geom_col(fill = "#bcbbea") + 
	facet_wrap(variable ~ ., scales = "free") +
	scale_x_continuous(breaks = melt$cluster)
#plot de verdeling over de clusters
ggplot(aggwerkbestandclusters2, aes(cluster, count)) +
	geom_col(fill = "#bcbbea") + 
	xlab(NULL) + ylab("Aantal Twitteraars") +
	scale_x_continuous(breaks = aggwerkbestandclusters2$cluster)
#creeer boxplot
melt4 <- melt(werkbestandclusters2[,2:8],  
	id.vars = 'cluster')
ggplot(melt4, aes(cluster,value, group = cluster)) +
	geom_boxplot(fill = "#bcbbea") + 
	facet_wrap(variable ~ ., scales = "free") + 
	scale_x_continuous(breaks = melt$cluster)

In de vorige post gaf ik aan dat ik graag minder dan 10 clusters wil, en niet te veel kleine. Het resultaat van de k-means methode valt binnen mijn doel, waarbij de grootste bestaat uit 104 accounts en waarbij er twee kleine clusters zijn van 4 en 5 accounts. De grootste groep – cluster 6 – lijkt voornamelijk te bestaan uit accounts die niet veel tweeten maar waarbij bijna alles een retweet is. Cluster 5 onderscheidt zich door heel veel te tweeten, cluster 2 tweet veel replies, cluster 1 is vooral actief in het weekend en cluster 4 tweet relatief veel populaire berichten. De accounts in clusters 3 en 8 zijn populair, met als verschil dat cluster 8 meer populaire berichten tweet. Cluster 7 kenmerkt zich door nergens erg hoog op te scoren.

In mijn vorige post gaf ik het ook al aan, maar clusterverdelingen zijn goed te visualiseren met de functie fviz_cluster(), waarbij je in principe alleen een model en een dataset hoeft te specificeren. Mijn dataset heeft eigenlijk te veel variabelen hiervoor, maar ik zal het ter illustratie laten zien met de variabelen tweets per dag en procent retweets.

fviz_cluster(clust_km8, 
	werkbestandclusters2, 
	choose.vars = c("tweetsperdag", "procentretweets"),
	labelsize = 0, stand = FALSE)

Clusters kiezen met k-medioid
Een nadeel van k-means is dat het optimale clusteraantal niet altijd even duidelijk is, en dat het algoritme gevoelig kan zijn voor outliers omdat het cluster-centers berekent op basis van gemiddelden. Een aan k-means gerelateerd algoritme dat daar beter mee kan omgaan is PAM (Partitioning Around Mediods). Dit algoritme plaatst geen centers gebaseerd op gemiddelden, maar kiest een representatieve observatie uit de dataset als center (medioid). Vervolgens worden clusters gevormd door alle observaties aan het dichtstbijzijnde medioid toe te wijzen. Daarna wordt berekend of de afstand tot de dichtstbijzijnde mediod verminderd kan worden als een andere observatie de nieuwe medioid zou worden. Als dit het geval is, verandert de medioid totdat er uiteindelijk geen optimalisatie meer kan plaatsvinden. PAM produceert ook per observatie een score op basis van de afstand naar de observaties binnen en buiten het cluster: de silhouette width. Deze score ligt tussen de -1 en +1, waarbij -1 een slechte match met het cluster aangeeft en +1 een goede. De gemiddelde silhouette width van het gehele model is een indicatie van hoe goed observaties bij de clusters aansluiten. Aan de hand hiervan kun je dan ook een keuze maken wat betreft het aantal clusters.

# voer PAM uit voor 20 waarden 
sil_width <- map_dbl(2:20,  function(k){
	model <- pam(scaled, k = k)
	model$silinfo$avg.width
})

# Maak een dataframe met k en de silhouette width
sil_df <- data.frame(k = 2:20,
	sil_width = sil_width
)

# Plot de silhouette width
ggplot(sil_df, aes(x = k, y =sil_width)) + 
	geom_line() + 
	scale_x_continuous(breaks = 2:20)

Uit deze plot blijkt dat 4 het optimale aantal clusters is, want daar is de silhoutte width immers het hoogst. Daar moet wel bij vermeld worden dat de onderlinge verschillen vrij laag zijn – het verschil tussen 4 en 6 clusters is niet eens 0.01 punt. Hoewel 6 dus ook geen slechte keuze zou zijn, ga ik verder met 4 clusters. Ik koppel de clusters aan mijn dataset en voeg bovenstaande stappen opnieuw uit om wat visualisaties van de clusterverdeling te maken.

#koppel clusters aan datasetwerkbestand
clusters3 <- werkbestand2b %>%
mutate(cluster = pammodel$clustering) %>%
group_by(cluster) %>%
mutate(count = n())

Zoals je ziet is het resultaat 4 clusters van vergelijkbare grootte. Cluster 1 kenmerkt zich door een hoog aantal tweets, cluster 2 onderscheidt zich door een hoog aantal retweets, cluster 3 heeft een hoge populariteit en cluster 4 tweet veel replies.

Je ziet dat de boxplots wat breder en de clusters in de clusterplot wat groter zijn. De variatie binnen clusters is dus duidelijk hoger, maar dat is te verwachten. Ik heb immers 414 accounts teruggebracht naar slechts 4 clusters.

Dat brengt me aan het einde van deze post over clusteren met k-means en k-medioids! Ik heb in deze en vorige post in totaal 3 clusterverdelingen gemaakt, die allemaal van cluster-aantal verschillen. Dus welke oplossing is nou de beste? Het voordeel van een klein aantal clusters is uiteraard dat het het makkelijkst is om mee te werken, maar met een groter aantal is er minder variatie binnen de clusters. De ideale oplossing hangt er daarom van af wat je ermee wil doen. In mijn volgende post ga ik daar wat dieper op in en kies ik uiteindelijk het model dat het beste voor mij werkt. Vervolgens ga ik de clusters wat verder verkennen door inhoudelijk daar hun tweet-gedrag te kijken.