Twitter in segmenten 2: hiërarchisch clusteren in R

Twitter in segmenten 2: hiërarchisch clusteren in R

Welkom op deze post over het clusteren van data met hierarchical clustering. In mijn vorige post heb ik Twitter-data binnengehaald en voorbereid, vandaag ga ik een eerste poging doen om de data te clusteren. Dit is een verkennend onderzoek waarbij ik vrijwel geen verwachtingen heb, wat het misschien wel des te leuker maakt.

Iets over clusteren
Clusteranalyse is een vorm van unsupervised machine learning. Dit klinkt spannender dan het is – het houdt eigenlijk gewoon in dat je de computer structuren in de data laat zoeken zonder dat hier een vooraf bepaalde uitkomst aan hangt. In het geval van clusteranalyse zoekt het algoritme overeenkomsten tussen observaties en worden op basis hiervan segmenten gemaakt. In de praktijk vind ik clusteranalyse overigens helemaal niet zo ‘unsupervised’ – de methode is afhankelijk van de variabelen die je zelf selecteert en bovendien moet je veel keuzes maken wat betreft de precieze methode, de grootte van clusters en de hoeveelheid clusters. Dit soort keuzes zijn afhankelijk van wat je met clusteranalyse wil bereiken. Als je een klantsegmentatie wil maken, is het bijvoorbeeld niet erg waardevol om iedereen op te splitsen in twee clusters. In mijn geval heb ik geen bepaald doel voor ogen, dus ik kan in principe alle kanten op wat betreft relevante variabelen, methodes en hoeveelheid clusters, hoewel het me niet handig lijkt om meer dan 10 clusters te creëren of om erg kleine clusters te hebben. In deze post ga ik observaties segmenteren door hiërarchisch te clusteren.

Iets over hiërarchisch clusteren
Hiërarchisch clusteren gebruikt als input een afstandsmatrix bestaande uit de afstanden (oftewel de verschillen) tussen alle observaties op basis van de geselecteerde variabelen. Vervolgens worden de observaties die het meest met elkaar overeenkomen samen geclusterd. Het resultaat is een dendrogram, een soort boom waarin alle observaties via takken uiteindelijk samen komen. De hoogte van de boom representeert de variatie. Clusteren kan met een simpele functie in het basispakket van R, genaamd hclust(). Wat clusteren lastig maakt, is het kiezen van een methode. Zoals gezegd maakt hiërarchisch clusteren gebruik van een afstandsmatrix. In elke stap worden vervolgens de clusters met de kortste afstand gepaard. Er zijn verschillende manieren om naar de afstand tussen 2 clusters te kijken, en dat kan dan ook worden in het argument method binnen hclust(). De meest gebruikte methode is complete, wat inhoudt dat de afstand gezien wordt als de afstand tussen 2 elementen (een in elke cluster) die het verst van elkaar vandaan liggen. Als deze maximum afstand tussen bepaalde clusters het kortst is, worden deze bij elkaar gevoegd. Andere methodes zijn o.a. single, waarbij juist gekeken wordt naar naar de elementen die het dichtst bij elkaar liggen, en average, waarbij de gemiddelde afstand wordt berekend. Ook kun je kiezen voor Ward’s method, hierbij worden clusters gevormd op basis van een minimum aan variatie binnen groepen. Een geheel andere benadering is divisive clustering. Waar bij methodes als complete, single en average wordt uitgegaan van observaties die worden samengevoegd, werkt divisive clustering andersom door eerst alle observaties samen te groeperen, en vervolgens op basis van de grootste verschillen te splitsen. Divisive clustering kan met het pakket cluster, maar ik ga hier verder in deze post niet op in. Voor deze post gebruik ik de pakketten cluster, ggplot2,
factoextra, dplyr, reshape en dendextend.

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

Standaardiseren
Mijn dataset dat ik ga clusteren bestaat 414 Twitter accounts, waarover ik in mijn vorige post informatie heb verzameld. Ik heb toen 14 variabelen gemaakt, waaronder het aantal tweets per dag, hoe vaak ze in bepaalde tijdvakken posten, hoeveel van hun tweets favorites en retweets krijgen (variabel ’trendsetter’) en hoeveel volgers ze hebben tegenover het aantal vrienden (variabel ‘populair’). Omdat sommige variabelen procenten zijn en anderen absolute getallen, ga ik alle variabelen eerst standaardiseren.

#omzetten naar matrix en namen geven
werkbestand3 <- as.matrix(werkbestand2[,-1])
row.names(werkbestand3) <- werkbestand2$screen_name

#hiermee standaardiseer je de variabelen
scaled <- scale(werkbestand3)

Afstand en methode
Vervolgens maak je een afstandsmatrix met de functie dist(). Deze staat standaard ingesteld op ‘euclidean’, wat in mijn geval prima is omdat alle variabelen continu zijn.

#distance matrix maken
dist <- dist(scaled)

#clusteren
hcluster <- hclust(dist, method = "complete")

Deze distance matrix is de input voor het clusteren. De functie hiervoor is hclust(), waarbij je een methode kunt specificeren. Ik heb voor mijn data een aantal methodes geprobeerd: complete, average, single, DIANA (divisive) en Ward method. Omdat mijn dataset redelijk groot is en sommige observaties flink bleken af te wijken, waren de dendrogrammen die deze methodes produceerden niet even duidelijk over het aantal en de verdeling van de clusters. Ik was uiteindelijk het meest tevreden met de complete methode, dus deze zal ik bespreken.

#dendogram plotten
dendogram <- as.dendrogram(hcluster)
plot(dendogram)

Wat zegt deze dendrogram? Het liefst zie ik eigenlijk dat alle observaties op een lager niveau duidelijke takken vormen – dit betekent dat de variatie binnenin de clusters laag is, maar tussen de clusters hoog. In deze dendrogram is dit niet duidelijk het geval, takken blijven zich vormen op alle hoogtes. Ook zie ik liever niet dat er groepen zijn die uit slechts een of twee observaties bestaan. Dit is nu wel het geval, wat betekent dat een aantal enkelingen erg afwijken van de andere observaties. Op zich is dit niet per se een probleem, maar deze groepen zijn dan niet erg veelzeggend.

Boom omhakken
De volgende stap is om een punt te kiezen waar je de boom gaat afkappen. Dit kun je doen door het aantal clusters of de hoogte te specificeren. Omdat ik geen bepaald aantal clusters in gedachten heb, kies ik ervoor om dit op basis van de hoogte te doen. Dit is echter lastig, omdat er op alle hoogtes nog clusters gevormd worden. Ik kies er uiteindelijk voor om dit te doen op niveau 8. Dit kun je zo visualiseren:

dend_colored <- color_branches(dendogram, h = 8)
plot(dend_colored)

Zo wordt direct duidelijk welke clusters er gevormd zijn. Ik besluit verder te gaan met deze clusters en koppel deze aan mijn data.

# maak clusters op hoogte 8
clusters1 <- cutree(hcluster, h = 8)

# voeg de clusters toe aan het werkbestand
werkbestandclusters1 <- werkbestand2 %>%
mutate(cluster = clusters1) %>%
group_by(cluster) %>%
mutate(count = n())

#aggregeer om gemiddelden te krijgen per cluster 
aggwerkbestandclusters1 <- aggregate(werkbestandclusters1[, 2:16], list(werkbestandclusters1$cluster), mean)

#voeg de grootte van ieder cluster toe
aggwerkbestandclusters1 <- left_join(aggwerkbestandclusters1, distinct(werkbestandclusters1[,16:17]))

Visualiseren
Ik heb nu twee bestanden: in het werkbestand staan alle observaties en bij welk cluster ze horen, en in het geaggregeerde bestand staat de algemene informatie over de clusters. Ik zie dat ik 15 clusters in totaal heb, waarvan de kleinste uit een persoon bestaat en de grootste uit 118. De verdeling wordt duidelijk in onderstaande visualisatie.

#verdeling over clusters plotten
ggplot(aggwerkbestandclusters1, aes(cluster, count)) +
	geom_col(fill = "#bcbbea") + 
	xlab(NULL) + ylab("Aantal Twitteraars") + 
	scale_x_continuous(breaks = aggwerkbestandclusters1$cluster)

Met minder data en minder variabelen kun je een clusterverdeling het best plotten als scatterplot. In mijn geval zou dat echter resulteren in 7 plots met elk 400 puntjes in 15 kleuren, dus ik besluit de scores per variabel als staafdiagram te visualiseren. Let wel op, de schaalverdeling verschilt hierbij per variabel. Dit is nodig want sommige variabelen zijn procenten en andere niet.

#variabelen omdraaien
melt <- melt(aggwerkbestandclusters1[,2:16],  
	id.vars = 'cluster', 
	variable.name = 'variabel')

#variabelen plotten
ggplot(melt, aes(cluster,value)) + 
	geom_col(fill = "#bcbbea") + 
	facet_wrap(variable ~ ., scales = "free") +
	scale_x_continuous(breaks = aggwerkbestandclusters1$cluster)

Clusterverdeling
Zoals ik eerder aangaf, wilde ik eigenlijk minder dan 10 clusters en geen clusters die erg klein zijn – ik heb 15 clusters waarvan 9 kleiner zijn dan 10 accounts. Als ik kijk naar waar deze kleine clusters hoog op scoren, blijken dit voornamelijk de tijd variabelen te zijn. Zo scoort cluster 14 hoog op vroege nacht, 15 op late nacht, 11 op vroege ochtend, 5 op vroege middag, 6 op late middag en 7 op late avond – en al deze clusters hebben minder dan 10 leden. Deze clusters zijn dus gevormd op basis van één variabel, en lijken over het algemeen niet bijzonder hoog of laag te scoren op andere variabelen.

Ik ben dus nog niet helemaal tevreden met mijn clusterverdeling. Er zijn een aantal maatregelen die ik kan treffen: ik kan accounts die te veel afwijken uit mijn dataset halen, ik kan mijn dendrogram op een ander punt of op een andere manier afsnijden of ik kan mijn variabelen aanpassen. Om een beslissing hierover te kunnen maken, besluit ik een aantal kleine clusters nader te bestuderen. Zo zoek ik de accounts in de clusters die ‘s nachts actief zijn op Twitter. Ze lijken mij redelijk normale accounts – de een tweet vooral over voetbal, de ander richt zich op militairen en tweet o.a. dat de winnares van Heel Holland Bakt een korporaal in het leger is en de derde retweet veel politiek getinte berichten, maar ook berichten over zijn woonplaats en grappige foto’s. Deze accounts hebben als belangrijkste overeenkomst dat ze niet zo actief zijn, maar als ze tweeten ze dit vooral ‘s nachts doen. Misschien draaien deze personen nachtdiensten, of gebruiken ze Twitter als ze niet kunnen slapen. Ik had de tijd-variabelen toegevoegd met het idee dat er wellicht nep-accounts zijn die dag en nacht door blijven tweeten, maar dit lijkt niet het geval te zijn. De tijdsvariabelen zorgen echter wel voor ongewenst kleine groepen die inderdaad variëren qua moment van tweeten, maar die verder vrij gemiddeld zijn.

Ward methode
Daarom besluit ik de tijd-variabelen uit mijn dataset te halen. De weekend-variabel houd ik er wel in, omdat ik accounts wil onderscheiden die in hun vrije tijd eens rustig op Twitter gaan, of juist tijdens de drukke werkweek. Ik voer opnieuw de functie hclust() met methode ‘complete’  uit, en loop opnieuw bovenstaande stappen door. Het resulterende dendrogram is overzichtelijker dan de vorige, maar er zijn natuurlijk minder variabelen en dus minder variatie.

Omdat takken zich nog steeds op alle hoogtes blijven vormen en er geen duidelijk afsnijpunt is, probeer ik ook opnieuw een aantal andere methodes. Vooral de Ward-methode produceert een duidelijke dendrogram.

Rond de hoogte 11 hebben zich duidelijk een aantal aantal takken gevormd. Zoals gezegd baseert deze methode zich op zo min mogelijk variatie binnen de clusters, en met duidelijke, homogene groepen in het vooruitzicht, vind ik het een interessante optie om verder te verkennen. Ik snijd de boom daarom af op hoogte 11, en plot opnieuw de clusterverdeling en de scores op variabelen.

Ik houd uiteindelijk 7 clusters over- een veel behapbaarder aantal dus. Nog steeds heb ik 2 kleine clusters van elk 4 leden, maar dit zijn groepen bestaande uit zeer populaire accounts, waaronder bijvoorbeeld de officiële pagina van voetbalclub PSV.  Dit soort professioneel beheerde pagina’s verschillen aanzienlijk van de gemiddelde gebruiker, dus vind ik het acceptabel dat ze een eigen cluster krijgen. De andere groepen hebben tussen de 48 en 123 leden. Alle groepen hebben duidelijk onderscheidende kenmerken: cluster 1 tweet bijvoorbeeld ontzettend veel, terwijl cluster 3 het hoogste scoort op retweets en cluster 4 op replies. Cluster 5 scoort op alles vrij gemiddeld maar tweet aanzienlijk minder in het weekend, terwijl cluster 2 dan juist het meest actief is. De kleine clusters 6 en 7 zijn populaire accounts met als verschil dat minder tweets van groep 7 gefavorited en geretweet worden.

Tenslotte maak ik een boxplot om de verdeling van de clusters over de variabelen te bekijken.

#boxplot van alle variabelen
ggplot(melt2, aes(cluster,value, group = cluster)) +
	geom_boxplot(fill = "#bcbbea") + 
	facet_wrap(variable ~ ., scales = "free") + 
	scale_x_continuous(breaks = melt$cluster)


Zoals je hierboven kunt zien, zijn de meeste clusters niet helemaal homogeen en allemaal hebben ze een aantal outliers. Dit is echter te verwachten wanneer je een dataset van meer dan 400 observaties terugbrengt naar 7 clusters. Ik heb zelf nog een aantal andere methodes en afsnijpunten geprobeerd en uitgewerkt, maar deze methode en deze variabelen werkten veruit het beste voor me. Mijn doel was immers om minder dan 10 clusters en niet te veel kleine clusters te hebben, en dat is bij deze gelukt.

Visualiseren als scatterplot
Zoals ik eerder aangaf kan clusteren vaak het best worden gevisualiseerd middels een scatterplot. Met mijn hoeveelheid clusters en aantal variabelen is dat helaas wat lastiger, omdat je dan flink wat visualisaties met verwarrend veel puntjes creëert. Is je data er wel geschikt voor, dan wil ik nog even een functie demonstreren: fviz_cluster() van het package factoextra. Deze functie is gemaakt om clusters te visualiseren als scatterplot, en zou er dan ook heel tof uitzien als ik minder clusters had. Voor nu kies ik twee variabelen om deze functie te demonstreren. Je ziet hieronder dat de clusters elkaar flink overlappen, dit komt doordat er meer variabelen hebben meegespeeld bij het vormen van de clusters dan de twee die ik nu plot. Met een kleinere dataset is het wellicht ook leuk om de labels aan te laten staan: je ziet dan direct wie er in welk cluster valt.

fviz_cluster(object = list(data = werkbestandclusters1[,2:7],
	cluster = werkbestandclusters1$cluster), 
	choose.vars = c("tweetsperdag", "procentretweets"),
	labelsize = 0, 
	stand = FALSE)

In mijn volgende post ga ik via de k-means methode een clusterindeling maken, ik ben benieuwd of daar een zelfde soort verdeling uit gaat komen. Als je tips/opmerkingen/vragen voor me hebt, mail me op gibbon@datagibbon.nl.