Twitter in segmenten 1: tweets minen en variabelen maken

Twitter in segmenten 1: tweets minen en variabelen maken

In tijden van nepnieuws, trollenlegers, Cambridge Analytica en bots, verbaast het me soms wat mensen allemaal argeloos de wereld in tweeten. Zo viel het mij tijdens mijn vorige onderzoekje op Twitter over Brexit op dat er een vrij sterke aanwezigheid van extreem-rechts was binnen deze discussie, waarbij ondanks alle perikelen aan de overkant van de Noordzee in harde taal geroepen werd om een Nexit. Bij het nader bekijken van deze profielen bleken sommigen zo actief in het retweeten dat ik me afvroeg of deze wel echt waren. Want is het normaal om 70 tweets per dag te retweeten? Of om maar 12 mensen te volgen maar wel 4000 volgers te hebben? In alle eerlijkheid – ik weet het niet. Maar ik raakte wel geïntrigeerd. Daarom leek het me interessant om een cluster analyse hierop los te laten om te onderzoeken of er inderdaad bepaalde segmenten te onderscheiden zijn op basis van gebruik. Vervolgens wil ik het beste model kiezen, en onderzoeken of er verschillen zijn tussen de inhoud van tweets. Zijn groepen die veel retweeten inderdaad vaker FVD stemmers? Of retweeten ze gewoon filmpjes van De Luizenmoeder? Dat ga ik in de komende posts uitzoeken. In deze eerste post haal ik de data van Twitter binnen en bereid ik deze voor op wat ik wil analyseren.

Connectie met Twitter
Twitter-data minen is super makkelijk. Ik had in een vorige post al laten zien hoe je data in R binnenhaalt vanaf Twitter, maar nog even in het kort: maak een developers account via https://developer.twitter.com/, maak hierbinnen een app aan, bekijk je tokens en vul deze in binnen de R-omgeving. Het maken van een verbinding met Twitter vanuit R deed ik vorige keer met het pakket twitteR, dit keer gebruik ik echter rtweet omdat er in dit pakket standaard meer informatie per tweet meekomt (88 variabelen maar liefst). Als je vooral geïnteresseerd bent in de tekst zelf is twitteR dus prima, maar rtweet is handig als je ook de meta-informatie zoals volgers en vrienden wilt analyseren. Verder gebruik ik voor deze post dplyr en lubridate.

library(dplyr)
library(rtweet)
library(lubridate)

Met rtweet maak je zo een verbinding tussen R en Twitter.

#vul de persoonlijke codes en naam van je app in
create_token(app="***",
consumer_key="***", 
consumer_secret="***", 
access_token="***", 
access_secret="***")

Tweets met populaire hashtags binnenhalen
Vervolgens kun je tweets gaan binnenhalen. Mijn plan is om eerst te zoeken op recente tweets, om vervolgens meer tweets van deze gebruikers binnen te halen. Om een breed, in ieder geval enigszins actief sample binnen te halen, besluit ik te zoeken op de 20 meest populaire hashtags van de afgelopen 24 uur (een overzicht hiervan kun je vinden op https://twimmer.com/). Dit zijn redelijk uiteenlopende onderwerpen – van programma’s als De Luizenmoeder tot politieke partijen als VVD tot populaire voetbalwedstrijden. Van alle onderwerpen besluit ik de 30 meest recente tweets op te halen. Ik had graag een nog groter sample gewild, maar in een volgende stap ga ik van al deze gebruikers meer tweets ophalen, en Twitter heeft daar een limiet aan gehangen waardoor dit al flink veel tijd gaat kosten.

twitter_handles <- c("#utrpsv", "#luizenmoeder", "#wnlopzondag", "#wnl", "#deluizenmoeder", "#stemzeweg","#waterstof", "#var", "#fvd", "#vvd", "#tegenlicht", "#stempvv", "#grovit", "#uwv", "#buitenhof", "#baudet", "#klimaatwet", "#giletsjaunes", "#wkafstanden", "#cerclu")

#recente nederlandstalige tweets met keywords uit twitter_handles
for(handle in twitter_handles) {
	result <- search_tweets(handle, n = 30 , 
	include_rts = TRUE, 
	lang = "nl", 
	type = "recent")
result$Source <- handle
df_name <- handle
assign(df_name, result)
}

Het resultaat is per hashtag een dataframe. Mij viel op dat sommige niet 30 tweets lang waren, ik vermoed dat dat komt doordat er op dat moment minder recente tweets zijn. In mijn geval wil ik liever één bestand, dus bind ik ze aan elkaar.

totaaltweets <- do.call("rbind", list(`#wnlopzondag`, `#wnl`, `#wkafstanden`, `#waterstof`, `#vvd`, `#var`, `#uwv`, `#utrpsv`,`#tegenlicht`,`#stemzeweg`, `#stempvv`, `#grovit`, `#klimaatwet`, `#giletsjaunes`, `#fvd`, `#deluizenmoeder`, `#cerclu`, `#buitenhof`, `#baudet`, `#luizenmoeder`))

Ik heb in totaal 592 tweets in dataframe totaaltweets. Echter kan het zijn dat gebruikers dubbel zijn binnengekomen door meerdere hashtags te gebruiken, of binnen korte tijd over meerdere onderwerpen te tweeten. Door de count-functie te gebruiken zie ik hoeveel gebruikers ik daadwerkelijk over heb.

gebruikers <- totaaltweets %>%
count(user_id, sort = TRUE)

Alle tweets van gebruikers ophalen
Ik zie dat ik 430 gebruikers overhoud, waarvan er één maar liefst 12 keer voorkomt in mijn sample. Vervolgens wil ik alle tweets – oftewel timelines – van deze gebruikers ophalen. Dit doe ik met een for-loop, waarbij de code voor elke user_id opnieuw wordt uitgevoerd. Ik liep zelf echter tegen het probleem aan dat Twitter je niet meer dan 52 timelines tegelijk laat minen, maar ik vond daar hier een antwoord op. De onderstaande code duurt lang om uit te voeren want er is een wachttijd, maar met 430 timelines is dat nog te overzien.

tmls <- vector("list", length(users))

#timelines ophalen
for (i in seq_along(tmls)) {
    tmls[[i]] <- get_timeline(users[i], n = 500)
    if (i %% 52L == 0L) {
        rl <- rate_limit("get_timeline")
        Sys.sleep(as.numeric(rl$reset, "secs"))
    }
    ## print update message
    cat(i, " ")
}

#samenvoegen naar een dataframe
tmls <- do_call_rbind(tmls) 

Dit levert een totaal van ruim 200 000 tweets op. Ik kon in de get_timeline helaas geen datum aangeven, maar voor dit onderzoek ben ik geïnteresseerd in wat er in een week is getweet. Met onderstaande functie filter ik de tweets van voor maandag 4 februari 12.00 uur en na maandag 11 februari 12.00 uur eruit.

alletweets <- tmls %>% 
filter(created_at > ymd_hms("2019-02-04 12:00:00"),
created_at < ymd_hms("2019-02-11 12:00:00"))

Ik houd zo’n 74 000 tweets over. Het gemiddelde aantal tweets is 173 per gebruiker, hoewel sommigen daar ver onder zitten met slechts 1 tweet, en anderen ver erboven met 600. Wat overigens opmerkelijk is aangezien ik een maximum van 500 had ingesteld, maar ik kan niet helemaal duiden waardoor dit komt.

Variabelen voor clusteranalyse
Nu ik een complete dataset heb met tweets van de accounts die ik ga onderzoeken, is het tijd om de variabelen voor het clusteren te gaan maken. Ik ben geïnteresseerd in hoe vaak een gebruiker per dag tweet, hoe vaak dit een retweet of reply is en wat de tijd van het tweeten is. Daarbij kijk ik naar of iemand in het weekend of doordeweeks actief is en in welk tijdvak iemand het meest tweet. Dit kan interessant zijn als blijkt dat leden van een bepaald cluster vooral ‘s nachts actief zijn, omdat zij zich wellicht in een andere tijdzone bevinden of misschien zelfs bots zijn (of gewoon slaapproblemen hebben). Verder ben ik benieuwd naar of ze populair zijn wat betreft volgers (een positief nummer is hier een indicatie van meer volgers hebben dan dat een account zelf volgt) en welk percentage van hun tweets geretweet en gefavorited worden. Hiervoor voer ik flink wat mutaties uit.

werkbestand <- alletweets %>%
    group_by(user_id) %>%
    mutate(aantaltweets = n()) %>%
    mutate(tweetsperdag = aantaltweets/7) %>%
    mutate(procentretweets = sum(is_retweet == TRUE)/aantaltweets) %>%
    mutate(procentreplys = sum(!is.na(reply_to_status_id))/aantaltweets) %>%
    mutate(procentweekend = sum(created_at > as.Date("2019-02-09") & 
		created_at < as.Date("2019-02-11"))/aantaltweets) %>%
    mutate(tijd = hour(created_at)+1) %>%
    mutate(vroegenacht = sum(tijd == 24 | 
		tijd < 3)/aantaltweets) %>%
    mutate(latenacht = sum(tijd >= 3 & 
		tijd < 6)/aantaltweets) %>%
    mutate(vroegeochtend = sum(tijd >= 6 & 
		tijd < 9)/aantaltweets) %>%
    mutate(lateochtend = sum(tijd >= 9 & 
		tijd < 12)/aantaltweets) %>%
    mutate(vroegemiddag = sum(tijd >= 12 & 
		tijd < 15)/aantaltweets) %>%
    mutate(latemiddag = sum(tijd >= 15 & 
		tijd < 18)/aantaltweets) %>%
    mutate(vroegeavond = sum(tijd >= 18 & 
		tijd < 21)/aantaltweets) %>%
    mutate(lateavond = sum(tijd >= 21 & 
		tijd < 24)/aantaltweets) %>%
    mutate(populair = followers_count/friends_count) %>%
    mutate(trendsetter = sum(is_retweet == FALSE & 
		is_quote == FALSE & 
		favorite_count > 0 & 
		retweet_count > 0)/aantaltweets)

Aangezien dit nog meer kolommen aan een al groot bestand heeft toegevoegd, is het handig om even een klein bestandje met alleen de relevante variabelen te maken. Ik heb de inhoud van alle tweets per gebruiker niet meer nodig, dus gebruik ik de distinct functie om alle gebruikers maar 1 keer mee te nemen.

#nieuw bestand met variabelen per gebruiker
werkbestand2 <- werkbestand[,c(4,90:93, 95:104)] %>%
filter(!screen_name == "***",
!screen_name == "***",
!screen_name == "***") %>%
distinct() %>%|
mutate_if(is.numeric, ~round(., 4))

Ter verduidelijking, ik filter drie gebruikers uit mijn dataset omdat een van hen niet in het Nederlands leek te tweeten, en twee anderen volgden niemand waardoor de populariteit score niet berekend kon worden. Verder rond ik de nummers af naar vier cijfers achter de komma.

Data verkennen
Ten slotte maak ik van elke variabel een boxplot om te kijken naar de verdeling en of er outliers zijn. Een aantal variabelen hebben geen outliers, een aantal hebben er één of twee die alsnog dicht bij de groep liggen en een aantal variabelen hebben outliers die een stuk verder weg liggen van de meerderheid, zoals onderstaande tijdsvariabelen.

boxplot(werkbestand2[6:13], col = "#bcbbea")

Dit kan komen doordat deze variabelen zijn gemaakt door een getal te delen door het aantal tweets. Als iemand in zeven dagen maar één keer heeft getweet en dit was op de vroege ochtend, heb je onmiddelijk een outlier. Dit komt bij alle variabelen die op deze manier gemaakt zijn voor. Daarom besluit ik gebruikers die maar één keer hebben getweet (minder dan 0.15 tweets per dag dus) eruit te filteren.

werkbestand2 <- werkbestand2 %>%
filter(!tweetsperdag < 0.15)

Een andere variabel waarbij de outliers opvallen is de populariteit. Dit is berekend door het aantal volgers te delen door het aantal dat een account zelf volgt. Bij de meeste accounts ligt dit redelijk dicht bij elkaar, maar bij populaire accounts van organisaties of bedrijven kan dit anders liggen. Stel dat een account 10 000 volgers heeft, maar zelf slechts 100 accounts volgt, is zijn score 1000. Dit is dan al gelijk een outlier. Zo wordt ook duidelijk in de boxplot.

Daarom besluit ik hier de score van extreem populaire accounts (scores boven de 50) af te ronden naar  50.

werkbestand2$populair[werkbestand2$populair > 50] <- 50

Deze twee maatregelen lossen helaas niet alle outliers op, maar verder wil ik niet gaan. Het is immers geen foutieve data en wellicht komen er tijdens het clusteren wel interessante patronen met deze outliers aan het licht.

Ik hoop dat deze post nuttig/interessant was! Mocht je vragen/opmerkingen/tips hebben, mail me via gibbon@datagibbon.nl. In mijn volgende post ga ik deze data gebruiken om een clusterverdeling te maken door middel van hiërarchisch clusteren.