Direkt zum Hauptbereich

Social Bots: Stimmungsmache im Iran

Im Iran finden derzeit Proteste gegen die Regierung statt. Im Zuge des so genannten „Arabischen Frühlings“ spielte Twitter eine nicht unwesentliche Rolle. Schon bei den Protesten 2009 gab es Berichte über den Einsatz von Social Bots, also automatisierten Fake-Accounts. Es liegt daher auf der Hand, zu fragen, ob sich derzeit Social Bots in die Meinungsbildung zum Thema Iran einmischen und wenn ja, ob sich ein Effekt auf den Diskurs nachweisen lässt. Im Folgenden wird anhand eines Samples von ca. 1.000.000 Tweets zum Thema „Iran“ nachgewiesen, dass Accounts aktiv sind, die mit hoher Wahrscheinlichkeit als Social Bots eingestuft werden müssen. Darüber hinaus wird mit Textmining-Methoden gezeigt, dass diese Social Bots zu einer negativen Stimmung (gemessen als Sentiment) in der Debatte beitragen. Damit wird ein nachweisbarer Effekt des Einsatzes von Social Bots in politischen Debatten aufgezeigt.

Datengrundlage

Die Daten wurden generiert, indem über die Streaming-API von Twitter in einem Zeitraum von 24 Stunden vom 30.12.2017 bis zum 31.12.2017 nach dem Wort „Iran“ gesucht wurde. Dabei wurden 899.745 Tweets mit Metadaten gespeichert.

Methode

Boterkennung

Die Existenz von Social Bots ist unstrittig, da es diverse Programme zur automatischen Steuerung von Fake-Accounts gibt und diese Programme auch eingesetzt werden und weil die Plattformbetreiber – wie Twitter – selbst angeben, gegen Social Bots vorzugehen.
Der Begriff des Social Bot ist aber unzureichend definiert. Unter Social Bots werden automatisierte Accounts verstanden, die vorgeben echte Nutzer zu sein. Es ist aber unklar, ab wann ein Account als automatisiert gilt. Teilweise werden „handgefertigte“ Inhalte einfach automatisch gepostet (über die Twitter API). Gleichzeitig kann auch ganz ohne eine Software ein hoher Grad an Automatisierung erreicht werden, zum Beispiel wenn Gruppen von Nutzern durch Copy&Paste sehr schnell große Mengen an Posts erzeugen. Auch die „Täuschungsabsicht“ ist nicht eindeutig, da die Motive der Nutzer unbekannt sind. In der Praxis ist es daher nahezu unmöglich, einen Account unzweifelhaft als Social Bot zu erkennen.
In der Forschung zu Social Bots haben sich zwei unterschiedliche Methoden herausgebildet: Machine Learning und Heuristiken. Beim Machine Learning wird – in der Regel auf Basis eines handcodierten Datensatzes – automatisch nach Mustern gesucht, anhand derer Social Bots durch einen Computer erkannt werden können. Die Schwierigkeit hierbei besteht darin, dass ein solches System nur Bots finden wird, die ähnlich sind zu den Daten, auf denen das System trainiert wurde. Der Heuristik-Ansatz arbeitet mit theoretisch hergeleiteten Regeln, die echte Twitternutzung von Social Bots unterscheiden sollen. Der Nachteil bei diesem Ansatz ist, dass die Regeln starr sind: Das System kann sich nicht durch neue Daten verbessern. Zudem ist es sehr schwierig einzuschätzen, welche Regel wie gut funktioniert und wie die Kombination von Regeln zu bewerten ist.
Für die folgende Untersuchung wurde ein Heuristik-Ansatz gewählt, da keine Trainingsdaten vorliegen, die auf den Fall Iran passen. Denn erstens können sich Social Bots sehr schnell verändern, indem ihre Steuerung angepasst wird. Es ist daher immer fraglich, ob historische Daten zur Klassifizierung aktueller Ereignisse genommen werden können. Zweitens ist das Verhalten von Twitternutzern sehr stark vom jeweiligen Kontext abhängig. In einem Diskurs zu den aktuellen Protesten im Iran werden sich die Nutzer vermutlich anders verhalten als in einem Diskurs zu einer Wahl, der sich über mehrere Wochen erstreckt. Auch hier stellt sich daher die Frage, ob Daten, die in einem anderen Kontext erhoben wurden, als Trainingsdaten verwendet werden können.
Im Folgenden wird mit vier unterschiedlichen Heuristiken gearbeitet: Analyse der Quelle (source), Verhältnis Freunde zu Followern, Anzahl der Tweets pro Tag und Textduplicate.
Wenn ein Tweet nicht über die Twitter-App gesendet wird, sondern über ein anderes Programm, dass sich der API bedient, dann wird dies in den Metadaten vermerkt (sofern dieser Vermerk nicht manipuliert wird). Eine Analyse dieser source Variable in den Iran-Daten hat 97 „verdächtige“ Quellen ergeben. Dabei handelt es sich zum Teil um bekannte Dienste zur Automatisierung (wie IFTTT), die allerdings auch von seriösen Accounts benutzt werden, zum Teil um Social Bot Software (wie twittbot). Es tauchen aber auch einige obskure Quellen auf wie www.AgendaOfEvil.com, pipes.cyberguerrilla.org und www.rightstreem.com. Daneben gibt es eine Reihe von alternativen Twitterapps und Medienseiten, bei denen nicht klar ist, ob sie zur Betreibung von Social Bots genutzt werden können. Insgesamt wurden 24.718 Tweets im Sample von diesen sources gesendet.
Das Verhältnis von Freunden und Followern ist auch sehr interessant. Früher war es so, dass Bots viele Freunde hatten (vielen Nutzern folgten), aber selbst wenig Follower hatten. Unsere Untersuchungen haben gezeigt, dass das Verhältnis heute bei Bots häufig ausgeglichen ist, weil die Bot-Software nur weiteren Nutzern folgt, wenn es auch neue Freunde gibt. Häufig folgen sich Bots auch einfach gegenseitig. Man kann also nach Nutzern Ausschau halten, deren Friend/Follower Ratio in etwa 1 ist. Da Accounts mit sehr wenigen Freunden und Followern zufällig eine solche Ratio aufweisen können, wurden nur Accounts mit mehr als 100 Followern einbezogen. Auf diese Weise wurden 58.372 Tweets identifiziert.
Die durchschnittliche Anzahl der Tweets, die ein Nutzer pro Tag twittert, wird ebenfalls häufig als Kriterium für Social Bots herangezogen. Die Idee dabei ist, dass Bots sich durch besonders starke Aktivitäten auszeichnen. Eine Frage ist allerdings, ab wie vielen Tweets ein Nutzer „auffällig“ ist. Das Oxford-Internet Institut verwendet einfach den kritischen Wert 50 Tweets pro Tag. Weniger beliebig wird die Heuristik wenn der kritische Wert berechnet wird. Dafür eigenen sich zwei unterschiedliche Ansätze. Man kann die Interquartile-Range berechnen (also den Abstand des oberen Viertels vom Median) und diesen Wert dann mit 1,5 multiplizieren. Dieses Maß ist ein klassisches Outliermaß in der Statistik. Alle Werte, die größer sind als das Anderthalbfache der IQR sind unverhältnismäßig weit vom Median entfernt. Im vorliegenden Sample wären dass alle Nutzer, die mehr als 54 Mal am Tag twittern. Noch konservativer ist es, die 5% der aktivsten Twittere als auffällig zu nehmen. Dieser Ansatz wurde hier verfolgt. Nutzer, die im Durchschnitt mehr als 165 Mal am Tag getwittert haben, wurden als Bot-verdächtig eingestuft.
Wenn Texte automatisch generiert (oder massenhaft verbreitet werden) kann es sein, dass identische Texte auftauchen. Twitter hat für das Teilen von Texten eigentlich die Retweet-Funktion. Wenn Texte identisch sind, ohne dass es sich um Retweets handelt, kann das ein Hinweis auf Automatisierung sein. Im vorliegenden Sample war das allerdings nur bei 820 Tweets der Fall.
Bei unterschiedlichen Heuristiken stellt sich die Frage, wie das Verhältnis der Regeln zueinander ist. Müssen alle Regeln in jedem Fall erfüllt sein, oder reicht es, wenn irgendeine Regel greift? Da Social Bots sehr unterschiedlich sein können, würde einiges an Genauigkeit verloren gehen, wenn nur nach den Fällen geschaut würde, in denen alle Regeln erfüllt sind. Wenn aber jede Regel für sich ausreicht, dann ist zum Beispiel klar, dass die 5% der aktivsten Nutzer automatisch als Bots eingestuft werden. Im Folgenden wurden zwei unterschiedliche Ansätze verfolgt: Erstens wurden alle Tweets, bei denen eine der Regeln gegriffen hat als verdächtig (suspicious) eingestuft. Zweitens wurden alle Tweets, bei denen mindestens zwei Heuristiken zutrafen, als Bots eingestuft.
Gerade Medienaccounts twittern häufig sehr viel und könnten so als Bots eingestuft werden (zumal sie auch meist automatisiert agieren). Meistens sind diese Accounts aber von Twitter verifiziert. Durch das Kriterium verified hat man also eine negative Heuristik, die Social Bots ausschließt.
Die Kombination dieser Heuristiken lieferte 118.071 suspicious Tweets und 10.126 Bot-Tweets.

Textmining

Um die Texte zu analysieren wurde zunächst ein bag-of-words-Ansatz gewählt. Dabei wird gezählt, welche Worte wie häufig in welchen Tweets vorkommen. Die Ergebnisse werden in Wordclouds präsentiert, bei denen für die Nicht-Bots, die Verdächtigen und die Bots die häufigsten Worte dargestellt werden (wobei die Größe der relativen Häufigkeit entspricht). Worte, die in sehr vielen Dokumenten vorkommen, sind häufig weniger aussagekräftig. Umgekehrt sind die Worte, die in einem Dokument besonders häufig sind, oft besonders wichtig für die Einschätzung. Anstelle der einfachen Häufigkeit verwendet man daher die term frequency–inverse document frequency (TFIDF).
In einem zweiten Schritt wurde untersucht, welche Worte häufig mit welchen anderen Worten gemeinsam auftauchen (co-occurence). Für die drei Gruppen wurden dann Graphen erstellt, die jeweils die 20 häufigsten Worte mit bis zu fünf Worten aus ihrem direktem Umfeld darstellen. Dadurch lässt sich sehen, ob dieselben Worte eventuell in einem unterschiedlichen Kontext benutzt worden ist.
Darüber hinaus wurde eine Sentiment-Analyse durchgeführt. Dafür werden die Worte mit einem Lexikon verglichen, dass für (nahezu) jedes (englische) Wort einen Wert ermittelt, wie positiv oder negativ das Wort verwendet wird. Ist die Summe dieser Werte für einen Text negativ, spricht man von einem negativen Sentiment.
Abschließend wurde untersucht, ob die Unterschiede, die sich bei der Sentiment-Analyse ergeben haben, zufällig sind oder nicht. Dazu wurde das Sentiment der Worte in den Gruppen Nicht-Bot, Verdächtig und Bot mit einem Kolgomorov-Smirnov-Test überprüft. Die Null-Hypothese ist dabei, dass die Daten aus der gleichen Verteilungsfunktion stammen. Der p-Wert zeigt, mit welcher Wahrscheinlichkeit die Null-Hypothese anzunehmen ist.

Ergebnisse

Die folgende Tabelle zeigt die 20 aktivsten Nutzer, die als Bots eingestuft wurden. Es wird deutlich, dass es sich definitiv um sehr aktive Accounts handelt, die sich stark von „normalen“ Nutzern unterscheiden.

screen_name TpD follower friends Source
Davewellwisher 1082.13116726835 27374 15854 <a href="https://ifttt.com" rel="nofollow">IFTTT</a>
TinaOrt79591465 291.44246031746 7492 7841 <a href="http://twitter.com/download/android" rel="nofollow">Twitter for Android</a>
americanshomer 310.317204301075 5636 5669 <a href="http://twitter.com/download/iphone" rel="nofollow">Twitter for iPhone</a>
BetigulCeylan 230.866009042335 2394 2277 <a href="http://twitter.com/download/iphone" rel="nofollow">Twitter for iPhone</a>
zyiteblog 632.971712538226 1688 3687 <a href="http://www.hootsuite.com" rel="nofollow">Hootsuite</a>
ErengwaM 268.792144026187 1177 1200 <a href="http://twitter.com/download/android" rel="nofollow">Twitter for Android</a>
PeggyRuppe 229.74679943101 6424 6347 <a href="http://twitter.com/download/iphone" rel="nofollow">Twitter for iPhone</a>
sturm_tracey 234.446078431373 2058 2105 <a href="https://mobile.twitter.com" rel="nofollow">Twitter Lite</a>
CityofInvestmnt 96.8357142857143 5180 5017 <a href="https://ifttt.com" rel="nofollow">IFTTT</a>
emet_news_press 519.184782608696 14137 3304 <a href="http://www.hootsuite.com" rel="nofollow">Hootsuite</a>
favoriteauntssi 476.096153846154 5045 5166 <a href="http://twitter.com/download/iphone" rel="nofollow">Twitter for iPhone</a>
Sakpol_SE 442.321109123435 4605 0 <a href="https://sakpol.se" rel="nofollow">Sakpol Magic</a>
dreamedofdust 626.168421052632 2640 84 <a href="https://ifttt.com" rel="nofollow">IFTTT</a>
NarrendraM 2034.36666666667 1952 251 <a href="https://ifttt.com" rel="nofollow">IFTTT</a>
YMcglaun 206.769795918367 10571 10559 <a href="http://twitter.com/download/android" rel="nofollow">Twitter for Android</a>
lynn_weiser 405.789724072312 20009 19756 <a href="http://twitter.com/download/iphone" rel="nofollow">Twitter for iPhone</a>
AngelaKorras 194.026874585269 3666 3768 <a href="http://www.echofon.com/" rel="nofollow">Echofon</a>
MarjanFa1 261.758865248227 6183 6059 <a href="http://twitter.com/download/android" rel="nofollow">Twitter for Android</a>
RichGossger 355.432432432432 587 571 <a href="http://twitter.com" rel="nofollow">Twitter Web Client</a>
sness5561_ness 451.463414634146 11994 12477 <a href="http://twitter.com/download/iphone" rel="nofollow">Twitter for iPhone</a>

Der Nutzer Sakpol_SE ist zwar ein Bot, sagt das aber auch in seiner Beschreibung und würde somit nicht als Social Bot zählen.

Die folgende Abbildung zeigt die Wordclouds für Nicht-Bots, Verdächtige und Bots:
Wordclouds

Besonders viel sticht hier erstmal nicht ins Auge. Zwar ist Trump bei den Bots stärker vertreten, die benutzten Worte scheinen sich allerdings nicht stark zu unterscheiden.

Die Co-occurence zeigt erste Unterschiede aber auch viele Ähnlichkeiten, was die folgenden Abbildungen verdeutlichen:



In allen drei Abbildungen findet sich der Zusammenhang von Obama mit Hezbollah. In dem Graph, der sich auf die Bots beschränkt, werden stärker Ökonomische Themen angesprochen und Trump nimmt einen größeren Stellenwert ein. Zudem taucht auch der Hashtag MAGA auf. Das Thema der Frauenrechte kommt nur in den Nicht-Bot-Daten vor. Insgesamt muss aber festgestellt werden, dass die Bots offenbar zu sehr ähnlichen Themen posten und sich daher die inhaltliche Darstellung der Proteste kaum ändert.
Anders sieht es aber aus, wenn man eine Sentiment-Analyse anschließt. Hier zeigt sich, dass die Bots mehr negative Worte verwenden, als die anderen Nutzer. Für die Nicht-Bots ergibt sich ein durchschnittliches Sentiment von -0,049, für die Suspicious-Gruppe -0,063 und für die Bots -0.094. Der KS-Test zwischen Nicht-Bots und Suspicious ergibt einen Wert von 0,042 mit einem p-Wert gegen Null. KS-Test zwischen Nicht-Bots und Bots fällt sogar noch deutlicher aus, mit einem Wert von 0,12 und ebenfalls einem p-Wert von Null. Es ist also sehr unwahrscheinlich, dass die Worte (und ihre positiven und negativen Werte) aus derselben Verteilung stammen.
Diese Unterschiede lassen sich auch visualisieren, indem man die empirische kumulative Dichtefunktion betrachtet.
Man sieht, dass die Dichtefunktion der Bots im negativen Bereich deutlich stärker ausgeprägt ist.

Fazit

Hier liegt ein empirisch überprüfbarer Befund vor, der zeigt, dass Social Bots die Stimmung in politischen Debatten verändern können. Geht man davon aus, dass Debatten auf Twitter einen politischen Effekt haben und das Stimmungen in Sozialen Netzwerken eine große Bedeutung zukommt, dann muss man folgern, dass Social Bots einen politischen Einfluss haben, zumindest im untersuchten Fall.
Zu bedenken ist allerdings, dass gerade im Bereich des Textmining sehr viele implizite methodische Entscheidungen getroffen werden (was zum Beispiel das Säubern der Texte anbelangt), die eventuell Einfluss auf die Ergebnisse haben können. Um die hier präsentierten Ergebnisse überprüfbar zu machen, werden daher im Folgenden die verwendeten Programmcodes aufgeführt.

Simon Hegelich, 01.01.2018

R-Codes

Functions

First some functions, I have used in the code. The formatTwitterDate Function was taken from https://rdrr.io/github/SMAPPNYU/smappR/man/format.twitter.date.html. Multiplot comes from http://www.cookbook-r.com/Graphs/Multiple_graphs_on_one_page_(ggplot2)/. Some very good ideas were taken from https://rpubs.com/imtiazbdsrpubs/230280.
### IranBot Functions

### URL parts
URL_parts <- function(x) {
m <- regexec("^(([^:]+)://)?([^:/]+)(:([0-9]+))?(/.*)", x)
parts <- do.call(rbind,
lapply(regmatches(x, m), `[`, c(3L, 4L, 6L, 7L)))
colnames(parts) <- c("protocol","host","port","path")
parts
}

### Format Twitter date
formatTwDate <- function(datestring, format="datetime"){
if (format=="datetime"){
date <- as.POSIXct(datestring, format="%a %b %d %H:%M:%S %z %Y")
}
if (format=="date"){
date <- as.Date(datestring, format="%a %b %d %H:%M:%S %z %Y")
}
return(date)
}

# Multiple plot function
#
# ggplot objects can be passed in ..., or to plotlist (as a list of ggplot objects)
# - cols: Number of columns in layout
# - layout: A matrix specifying the layout. If present, 'cols' is ignored.
#
# If the layout is something like matrix(c(1,2,3,3), nrow=2, byrow=TRUE),
# then plot 1 will go in the upper left, 2 will go in the upper right, and
# 3 will go all the way across the bottom.
#
multiplot <- function(..., plotlist=NULL, file, cols=1, layout=NULL) {
library(grid)

# Make a list from the ... arguments and plotlist
plots <- c(list(...), plotlist)

numPlots = length(plots)

# If layout is NULL, then use 'cols' to determine layout
if (is.null(layout)) {
# Make the panel
# ncol: Number of columns of plots
# nrow: Number of rows needed, calculated from # of cols
layout <- matrix(seq(1, cols * ceiling(numPlots/cols)),
ncol = cols, nrow = ceiling(numPlots/cols))
}

if (numPlots==1) {
print(plots[[1]])

} else {
# Set up the page
grid.newpage()
pushViewport(viewport(layout = grid.layout(nrow(layout), ncol(layout))))

# Make each plot, in the correct location
for (i in 1:numPlots) {
# Get the i,j matrix positions of the regions that contain this subplot
matchidx <- as.data.frame(which(layout == i, arr.ind = TRUE))

print(plots[[i]], vp = viewport(layout.pos.row = matchidx$row,
layout.pos.col = matchidx$col))
}
}
}

distill.cog = function(mat1, # input TCM ADJ MAT
title, # title for the graph
s, # no. of central nodes
k1, # max no. of connections
color){
library(igraph)
a = colSums(mat1) # collect colsums into a vector obj a
b = order(-a) # nice syntax for ordering vector in decr order

mat2 = mat1[b, b] # order both rows and columns along vector b

diag(mat2) = 0

## +++ go row by row and find top k adjacencies +++ ##

wc = NULL

for (i1 in 1:s){
thresh1 = mat2[i1,][order(-mat2[i1, ])[k1]]
mat2[i1, mat2[i1,] < thresh1] = 0 # neat. didn't need 2 use () in the subset here.
mat2[i1, mat2[i1,] > 0 ] = 1
word = names(mat2[i1, mat2[i1,] > 0])
mat2[(i1+1):nrow(mat2), match(word,colnames(mat2))] = 0
wc = c(wc,word)
} # i1 loop ends


mat3 = mat2[match(wc, colnames(mat2)), match(wc, colnames(mat2))]
ord = colnames(mat2)[which(!is.na(match(colnames(mat2), colnames(mat3))))] # removed any NAs from the list
mat4 = mat3[match(ord, colnames(mat3)), match(ord, colnames(mat3))]
graph <- graph.adjacency(mat4, mode = "undirected", weighted=T) # Create Network object
graph = simplify(graph)
V(graph)$color[1:s] = color[1]
V(graph)$color[(s+1):length(V(graph))] = color[2]

graph = delete.vertices(graph, V(graph)[ degree(graph) == 0 ]) # delete singletons?

plot(graph,
layout = layout.fruchterman.reingold,
main = title)

} # func ends


text.clean = function(x) # text data
{ require("tm")
x = gsub("<.*?>", " ", x) # regex for removing HTML tags
x = iconv(x, "latin1", "ASCII", sub="") # Keep only ASCII characters
x = gsub("[^[:alnum:]]", " ", x) # keep only alpha numeric
x = tolower(x) # convert to lower case characters
x = removeNumbers(x) # removing numbers
x = stripWhitespace(x) # removing white space
x = gsub("^\\s+|\\s+$", "", x) # remove leading and trailing white space
return(x)
}

DataBuilder
### Databuilder Iran BotAnalyse
# load packages
library(streamR)
library(ROAuth)
library(twitteR)

# authenticate Twitter API
requestURL <- "https://api.twitter.com/oauth/request_token"
accessURL <- "https://api.twitter.com/oauth/access_token"
authURL <- "https://api.twitter.com/oauth/authorize"
consumerKey <- "xxxxxyyyyyzzzzzz"
consumerSecret <- "xxxxxxyyyyyzzzzzzz111111222222"

token = "xxxxxyyyyyyyyyyyzzzzzzzzz"
tokenSecret = "xxxxxxxxxxxxxyyyyyyyyyyyzzzzzzzzz"

# get 24 hours of "Iran" from STREAMING-API
for(i in 1:24){
file = paste0("tweets", gsub(" |:", "-", Sys.time()), ".json")
track = "Iran"
follow = NULL
loc = NULL #c(50.33, 6.1, 52.36, 9.4)
lang = NULL
time = 60*60
tweets = NULL
filterStream(file.name = file, track = track,
follow = follow, locations = loc, language = lang,
timeout = time, tweets = tweets, oauth = Cred,
verbose = TRUE)
}


fl <- list.files()[grepl("tweets2017", list.files())]
df <- parseTweets(fl[1], verbose = FALSE)
for(i in 2:length(fl)){
df2 <- parseTweets(fl[i], verbose = FALSE)
df <- rbind(df, df2)
rm(list="df2")
}

saveRDS(df, "iranDF.rds")
gc()
}

Maincode

library(data.table)
library(tm)
library(SnowballC)
library(wordcloud)
library(RColorBrewer)
library(text2vec)
library(stringr)
library(RWeka)
library(tokenizers)
library(slam)
library(ggplot2)
library(igraph)
library(textir)
library(qdap)


df <- readRDS("iranDF.rds")
setDT(df)
class(df)
sources <- df[, URL_parts(source)[,2]]
sort(table(sources)[table(sources)>10], decreasing = T)
# Sources other than twitter.com are suspicious

#collect suspicious sources

auto <- c("publicize.wp.com", "www.echofon.com", "www.hootsuite.com", "tapbots.com",
"www.tweetcaster.com", "www.crowdfireapp.com", "ifttt.com", "twittbot.net", "software.complete.org",
"twicca.r246.jp", "roundteam.co", "twibble.io", "paper.li", "twitterrific.com", "mvilla.it",
"dlvrit.com", "bufferapp.com", "www.lost-property.eu", "www.newsdingo.com",
"www.news365247live.com", "corebird.baedert.org", "earthobservatory.ch",
"www.besoyepirozi.com","nosudo.co", "www.twitpane.com", "www.twhirl.org",
"www.handmark.com", "www.bennedemisim.com", "trendinalia.com", "news.quiboat.com",
"curiouscat.me", "www.occuworld.org", "www.yourlordthygod.com", "www.superinhuman.com",
"q-continuum.net", "https://socialscud.com/en", "www2.makebot.sh",
"www.socialnewsdesk.com", "www.echobox.com", "panel.socialpilot.co",
"leadstories.com", "www.powerapps.com", "www.tweetiumapp.com", "uk.reporte.us",
"www.socialjukebox.com", "www.ajaymatharu.com", "www.botize.com", "www.flipboard.com",
"www.tweetedtimes.com", "ingminds.com", "sakpol.se", "abraj.shahidvip.net",
"trueanthem.com", "janetter.net", "twittbot.net", "www.socialoomph.com",
"tweetlogix.com", "wezit.com", "pyraego.com", "twicca.r246.jp", "twibble.io",
"www.pwned.io", "www.samruston.co.uk", "xi.tv", "zou.tv", "www.AgendaOfEvil.com",
"pbump.com", "insubcontinent.com", "blog.christianebuddy.com", "www.asdf.com",
"www.zazoom.it", "www.titrespresse.com", "www.strictly-software.com",
"www.robinspost.com", "www.rightstreem.com", "www.myallies.com",
"www.informazione.it", "notiven.com", "www.rights.com", "drudgereportarchives.com",
"anonymo.us", "ctrlq.org", "KkimooKcomKkimooK.com", "f.tabtter.jp", "irna.ir",
"getfalcon.pro", "www.ucampaignapp.com", "www.iran-efshagari.com", "twblue.com.mx",
"www.thenewright.news", "todolist.x10.mx", "hamassenger.com", "megaph0ne.com",
"www.destroytwitter.com", "sinproject.net", "studio.twitter.com",
"www.dukascopy.com")

## Special look at:
# www.occuworld.org www.yourlordthygod.com www.superinhuman.com
# q-continuum.net www2.makebot.sh panel.socialpilot.co
# www.ajaymatharu.com ingminds.com sakpol.se abraj.shahidvip.net
# trueanthem.com janetter.net www.samruston.co.uk www.AgendaOfEvil.com
# pipes.cyberguerrilla.org www.rightstreem.com www.destroytwitter.com

# show URLs to check the source webpages
# unique(df$source[grepl("pipes.cyberguerrilla.org", df$source)])
# Tweets from these sources might be bots
botsAuto <- which(df[, grepl(paste(auto,collapse="|"), source)])

# Sometimes, bots use the same text, not marked as retweet.
botsDup <- which(duplicated(df$text)&df$retweet_count==0)

# bots post a lot (often). Take a look at distribution of
# tweets per user (logarithm to base 10)
hist(log10(df$statuses_count))

# Set time format to English
Sys.setlocale("LC_TIME", "C")

# Create column with age of accounts in days.
df$age <- sapply(df$user_created_at, function(x) Sys.Date() - as.Date(formatTwDate(x)))
# Divide number of tweets by days
df$TwPerDay <- df$statuses_count/df$age
# Take a look at the distribution
hist(log10(df$TwPerDay))
plot(df$TwPerDay)
# How many tweets per day is suspicious?
round(quantile(df$TwPerDay, probs= seq(0, 1, 0.05)))
# Take the upper five percent as Bots (without age 0)
hyp <- round(quantile(df$TwPerDay, probs= seq(0, 1, 0.05)))["95%"]
botsTWpD <- which(df$TwPerDay>hyp)

# Instead of taking the upper 5%, we could take 1.5 of InterQuartileRange,
# which is a classical outlier measurement.
IQR(df$TwPerDay)*1.5

# Friend/Follower ratio is important. Bots ratio is often around 1.
plot(df$favourites_count+1, df$friends_count+1, log = "xy")
abline(0,1)
# If FF-ratio is close to 1 and there are more than 100 followers
# it is likely a bot. Small number of followers could have FF-ratio
# close to 1, as well.
botsFF <- which(round((df$friends_count+1)/(df$followers_count+1),1)==1 &
df$followers_count>100)

# Combine all our bot-suspects.
# Any account, with any of the clues:
bots <- unique(c(botsDup, botsTWpD, botsFF, botsAuto))
# Only those accounts with at least two Bot-clues:
botsAnd <- c(botsDup,
botsTWpD,
botsFF,
botsAuto)[duplicated(c(botsDup,
botsTWpD,
botsFF,
botsAuto))]
# Media companies are using bots as well. But most of the times,
# they are verified users.
bots <- bots[-which(df$verified[bots]==T)]
botsAnd <- botsAnd[-which(df$verified[botsAnd]==T)]

Botsources <- URL_parts(df$source[bots])[,2]
sort(table(Botsources)[table(Botsources)>10], decreasing = T)

# Show 5 random bot texts.
df$text[bots][sample(length(bots), 5)]

# Get the 100 most active bots.
sort(table(df$screen_name[botsAnd]), decreasing = T)[1:100]
# Superb tutorial: http://www.rpubs.com/imtiazbdsrpubs/230283
# Build Wordcloud

#This function initialises the word_tokenizer which splits by spaces.
tok_fun = word_tokenizer # using word & not space tokenizers

#This function iterates over input objects
#This function creates iterators over input objects to vocabularies,
#corpora, or DTM and TCM matrices. This iterator is usually used in following functions :
#create_vocabulary, create_corpus, create_dtm, vectorizers, create_tcm
noBotTx <- df$text[-bots]

######
# as.vector(names(head(tsum, 100)))
stopw <- c("co", "https", "t", "the", "in", "of", "to", "a", "and", "is", "s",
"de", "are", "for", "on", "في","The", "that", "I", "amp","with",
"من", "have", "this", "you","it", "from", "en", "by", "This", "و",
"who", "be", "their", "as", "we", "da", "an", "la", "at", "all",
"they", "We", "was", "has", "no", "In", "will", "her", "don",
"than", "ve", "A", "iran", "İran", "about", "up", "over", "down",
"what", "i", "so", "but", "like", "would", "our", "They", "my", "can",
"2", "What", "off", "que", "them", "via", "1", "how", "You")
it_0 = itoken( noBotTx,
#preprocessor = text.clean,
tokenizer = tok_fun,
ids = rownames(df[-bots,]),
progressbar = T)
# func collects unique terms & corresponding statistics
#Creates a vocabulary of unique terms
vocab = create_vocabulary(it_0,
ngram = c(1L, 1L), #,
stopwords = stopw)

pruned_vocab = prune_vocabulary(vocab, # filters input vocab & throws out v frequent & v infrequent terms
doc_proportion_min = 0.001,
doc_proportion_max = 0.45)


# length(pruned_vocab); str(pruned_vocab)

vectorizer = vocab_vectorizer(pruned_vocab) # creates a text vectorizer func used in constructing a dtm/tcm/corpus

dtm_m = create_dtm(it_0, vectorizer) # high-level function for creating a document-term matrix

model_tfidf = TfIdf$new()
dtm.tfidf = model_tfidf$fit_transform(dtm_m)

tst = round(ncol(dtm.tfidf)/100)
a = rep(tst, 99)
b = cumsum(a);rm(a)
b = c(0,b,ncol(dtm.tfidf))

ss.col = c(NULL)
for (i in 1:(length(b)-1)) {
tempdtm = dtm.tfidf[,(b[i]+1):(b[i+1])]
s = colSums(as.matrix(tempdtm))
ss.col = c(ss.col,s)

}

tsum = ss.col

tsum = tsum[order(tsum, decreasing = T)] #terms in decreasing order of freq
head(tsum, 20)

## Bot Cloud

BotTx <- df$text[bots]
it_b = itoken( BotTx,
#preprocessor = text.clean,
tokenizer = tok_fun,
ids = rownames(df[bots,]),
progressbar = T)
# func collects unique terms & corresponding statistics
#Creates a vocabulary of unique terms
vocabB = create_vocabulary(it_b,
ngram = c(1L, 1L),
stopwords = stopw
)

pruned_vocabB = prune_vocabulary(vocabB, # filters input vocab & throws out v frequent & v infrequent terms
doc_proportion_min = 0.001,
doc_proportion_max = 0.45)


# length(pruned_vocab); str(pruned_vocab)

vectorizerB = vocab_vectorizer(pruned_vocabB) # creates a text vectorizer func used in constructing a dtm/tcm/corpus

dtm_b = create_dtm(it_b, vectorizerB) # high-level function for creating a document-term matrix

model_tfidfB = TfIdf$new()
dtm_b.tfidf = model_tfidfB$fit_transform(dtm_b)

tst = round(ncol(dtm_b.tfidf)/100)
a = rep(tst, 99)
b = cumsum(a);rm(a)
b = c(0,b,ncol(dtm_b.tfidf))

ss.col = c(NULL)
for (i in 1:(length(b)-1)) {
tempdtm = dtm_b.tfidf[,(b[i]+1):(b[i+1])]
s = colSums(as.matrix(tempdtm))
ss.col = c(ss.col,s)

}

tsumB = ss.col

tsumB = tsumB[order(tsumB, decreasing = T)] #terms in decreasing order of freq
head(tsumB)

BotATx <- df$text[botsAnd]
it_ba = itoken( BotATx,
#preprocessor = text.clean,
tokenizer = tok_fun,
ids = rownames(df[botsAnd,]),
progressbar = T)
# func collects unique terms & corresponding statistics
#Creates a vocabulary of unique terms
vocabBa = create_vocabulary(it_ba,
ngram = c(1L, 1L),
stopwords = stopw
)

pruned_vocabBa = prune_vocabulary(vocabBa, # filters input vocab & throws out v frequent & v infrequent terms
doc_proportion_min = 0.001,
doc_proportion_max = 0.45)


# length(pruned_vocab); str(pruned_vocab)

vectorizerBa = vocab_vectorizer(pruned_vocabBa) # creates a text vectorizer func used in constructing a dtm/tcm/corpus

dtm_ba = create_dtm(it_ba, vectorizerBa) # high-level function for creating a document-term matrix

model_tfidfBa = TfIdf$new()
dtm_ba.tfidf = model_tfidfBa$fit_transform(dtm_ba)

tst = round(ncol(dtm_ba.tfidf)/100)
a = rep(tst, 99)
b = cumsum(a);rm(a)
b = c(0,b,ncol(dtm_ba.tfidf))

ss.col = c(NULL)
for (i in 1:(length(b)-1)) {
tempdtm = dtm_ba.tfidf[,(b[i]+1):(b[i+1])]
s = colSums(as.matrix(tempdtm))
ss.col = c(ss.col,s)

}

tsumBa = ss.col

tsumBa = tsumBa[order(tsumBa, decreasing = T)] #terms in decreasing order of freq
head(tsumBa)

png("NoBotsCloud.png", type = "cairo", units = "cm", width = 30, height = 15, res = 600)
par(mfrow=c(1,3))
wordcloud(names(tsum), tsum, scale=c(2,0.5),0.05,
max.words=300, random.order=FALSE, rot.per=0.35,
colors=colorRampPalette(c("orange", "black"))(100), random.color = T) # Plot results in a word cloud
title(sub = "NoBots")
wordcloud(names(tsumB), tsumB, scale=c(2,0.5),0.05,
max.words=300, random.order=FALSE, rot.per=0.35,
colors=colorRampPalette(c("lightblue", "black"))(100), random.color = T) # Plot results in a word cloud
title(sub = "Suspicious")
wordcloud(names(tsumBa), tsumBa, scale=c(2,0.5),0.05,
max.words=300, random.order=FALSE, rot.per=0.35,
colors=colorRampPalette(c("red", "black"))(100), random.color = T) # Plot results in a word cloud
title(sub = "Bots")
dev.off()


# plot barchart for top tokens
png("WordList.png", type = "cairo", units = "cm", width = 30, height = 15, res = 600)

test = as.data.frame(round(tsum[1:40],0))
# New plot window
p1 <- ggplot(test, aes(x = rownames(test), y = test)) +
geom_bar(stat = "identity", fill = "orange") +
geom_text(aes(label = test), vjust= -0.20) +
coord_flip() +
theme(axis.text.x = element_text(angle = 90, hjust = 1))

# Term co-occurence matrix and co-occurence graphs
testB = as.data.frame(round(tsumB[1:40],0))
# New plot window
p2 <- ggplot(testB, aes(x = rownames(testB), y = testB)) +
geom_bar(stat = "identity", fill = "blue") +
geom_text(aes(label = test), vjust= -0.20) +
coord_flip() +
theme(axis.text.x = element_text(angle = 90, hjust = 1))

testBa = as.data.frame(round(tsumBa[1:40],0))
# New plot window
p3 <- ggplot(testBa, aes(x = rownames(testBa), y = testBa)) +
geom_bar(stat = "identity", fill = "red") +
geom_text(aes(label = test), vjust= -0.20) +
coord_flip() +
theme(axis.text.x = element_text(angle = 90, hjust = 1))
multiplot(p1, p2, p3,cols=3)
dev.off()

# Term co-occurence matrix and co-occurence graphs

vectorizerB = vocab_vectorizer(pruned_vocabB)

tcmB = create_tcm(it_b, vectorizerB,skip_grams_window = 5L) # func to build a TCM


tcm.matB = as.matrix(tcmB) # use tcm.mat[1:5, 1:5] to view


vectorizerBa = vocab_vectorizer(pruned_vocabBa)

tcmBa = create_tcm(it_ba, vectorizerBa,skip_grams_window = 5L) # func to build a TCM


tcm.matBa = as.matrix(tcmBa)


vectorizer = vocab_vectorizer(pruned_vocab)

tcm = create_tcm(it_0, vectorizer,skip_grams_window = 5L) # func to build a TCM


tcm.mat = as.matrix(tcm)






## adj.mat and distilled cog for tfidf DTMs ##

adj.mat = t(dtm.tfidf) %*% dtm.tfidf
diag(adj.mat) = 0
a0 = order(apply(adj.mat, 2, sum), decreasing = T)
adj.mat = as.matrix(adj.mat[a0[1:300], a0[1:300]])

adj.matB = t(dtm_b.tfidf) %*% dtm_b.tfidf
diag(adj.matB) = 0
a0 = order(apply(adj.matB, 2, sum), decreasing = T)
adj.matB = as.matrix(adj.matB[a0[1:300], a0[1:300]])

adj.matBa = t(dtm_ba.tfidf) %*% dtm_ba.tfidf
diag(adj.matBa) = 0
a0 = order(apply(adj.matBa, 2, sum), decreasing = T)
adj.matBa = as.matrix(adj.matBa[a0[1:300], a0[1:300]])

png("COGNo.png", type = "cairo", units = "cm", width = 20, height = 20, res = 600,
pointsize = 9)
distill.cog(adj.mat, 'COG-tfidf NoBots', 20, 5, c("green", "orange"))
dev.off()

png("COGSus.png", type = "cairo", units = "cm", width = 20, height = 20, res = 600,
pointsize = 9)
distill.cog(adj.matB, 'COG-tfidf Sucpicious', 20, 5, c("green", "lightblue"))
dev.off()

png("COGBot.png", type = "cairo", units = "cm", width = 20, height = 20, res = 600,
pointsize = 9)
distill.cog(adj.matB, 'COG-tfidf Bots', 20, 5, c("green", "red"))
dev.off()


t1 = Sys.time() # set timer
pol = polarity(text.clean(df$text[-bots]) ) # Calculate the polarity from qdap dictionary

Sys.time() - t1 # how much time did the above take?

head(pol$all)

pol$group$ave.polarity

polBots <- polarity(text.clean(df$text[bots]) )
polBots$group$ave.polarity

polBotsA <- polarity(text.clean(df$text[botsAnd]) )
polBotsA$group$ave.polarity

ks.test(polBots$all$polarity, pol$all$polarity)
ks.test(polBotsA$all$polarity, pol$all$polarity)

png("ECDF.png", type = "cairo", units = "cm", width = 21, height = 13, res = 600,
pointsize = 11)
plot(ecdf(x = pol$all$polarity),
verticals = TRUE,
do.points = FALSE,
main = "ECDF",
xlab="Polarity", ylab=" ",
col= "orange")
lines(ecdf(x = polBots$all$polarity), col = "lightblue", verticals = TRUE, do.points = FALSE)
lines(ecdf(x = polBotsA$all$polarity), col = "red", verticals = TRUE, do.points = FALSE)
legend("topleft", c("NoBots", "Suspicious", "Bots"),
fill = c("orange", "lightblue", "red"))
dev.off()

Beliebte Posts aus diesem Blog

Kritik an dem Science-Artikel der Priesemann-Gruppe „Inferring change points in the spread of COVID-19 reveals the effectiveness of interventions“

Der Science-Artikel von Dehning et al. (2020) gilt als Beleg für die Effektivität der Corona-Maßnahmen in Deutschland im März 2020. Wir glauben, dass der Artikel gravierende Fehler enthält und daher nichts darüber aussagt, ob insbesondere das Kontaktverbot vom 23.03.2020, irgendeinen Effekt hatte. Unsere Kritik haben wir bei Science eingereicht und sie ist hier zu finden: https://science.sciencemag.org/content/369/6500/eabb9789/tab-e-letters Im folgenden übersetze ich unseren Beitrag und gehe anschließend auf die Frage ein, wie Wissenschaft unter COVID-19-Bedingungen funktioniert und was daran bedenklich ist. Eine Kritik an ‘Inferring change points in the spread of COVID-19 reveals the effectiveness of interventions’ Wir haben den Artikel ‘Inferring change points in the spread of COVID-19 reveals the effectiveness of interventions’ analysiert und dabei gravierende Unstimmigkeiten im Design der Studie festgestellt: Anstatt das Datum der Wendepunkte (wann sich die COVID-19-Entwicklung i

Der Nutzerismus: Eine Ideologie mit totalitärem Potential

Ich glaube, dass wir derzeit den Aufstieg einer Ideologie erleben, die ich Nutzerismus nennen möchte. Hannah Arendt hat darauf hingewiesen, dass jede Ideologie zu einem totalitaristischen Regime führen kann und es gibt ernste Anzeichen, dass dies auch für den Nutzerismus gilt.  Was ist der Nutzerismus? Wie bei jeder Ideologie ist der Kerngedanke sehr einfach: Im Prinzip gibt es für alle gesellschaftlichen Probleme eine technische Lösung. Leider wenden die Menschen die richtigen Technologien nicht an. Sie nehmen ihre Rolle als Nutzer nicht wahr. Es geht dem Nutzerismus also um das Zusammenspiel von Mensch und Technik, allerdings immer wieder aus der gleichen Perspektive. Die Technik kommt vor als potentielle Lösung eines gesellschaftlichen Problems. Eventuell fehlt die perfekte Lösung noch, aber das ist dann als Auftrag an die Wissenschaft und die Ingenieure zu verstehen. Dieser Technikglaube hat etwas sehr Naives. Er abstrahiert zum Beispiel von allen Interessen, für die Technologien

Was man an der COVID-Politik über Faschismus lernen kann

Kritiker der Corona-Politik führen immer häufiger den Begriff Faschismus im Munde, um die politischen Maßnahmen zu beschreiben. Einerseits ist damit natürlich eine polemische Ablehnung verbunden: Wer will schon für Faschismus sein? Generell ist der moralische Vorwurf, etwas sei faschistisch oder faschistoid in der demokratischen Auseinandersetzung durchaus geläufig. Dabei wird jedoch meist auf etwas verwiesen, was zum demokratischen Staat dazu gehört und gerade keinen Faschismus begründet: Die Polizei, die das Gewaltmonopol durchsetzt, ist keine faschistische Organisation, ein Parlament, welches Bürgerrechte einschränkt, ist kein Beleg für die faschistische Aufhebung des Rechtsstaats und ein Medienartikel, der dazu aufruft, Bürger sollen Straftäter anzeigen, ist keine faschistische Propaganda, usw. All dies sind Beispiele für das Leben in demokratischen Gemeinwesen. Anstatt die Demokratie also immer gleich auf dem Weg in den Faschismus zu wähnen, wäre es angebracht, sich zu fragen, war