Die Netflix-Serie "Haus des Geldes", bzw "Money Heist" auf englisch, ist äußerst erfolgreich. Gerade wurde die vierte Staffel veröffentlicht; gute Gelegenheit für mich, meine neuen Skills zur Textanalyse anzuwenden. Ich benutze die Beschreibung auf Wikipedia zur vierten Staffel, um zu sehen, wie diese beschrieben wird. So viel auch schon vorab: man kann erkennen, dass es ein Drama ist. Der Einfachheit halber im Englischen und im unten stehenden Code unter dem Punkt #text in R direkt reinkopiert.
Achtung, für manche sind hier evtl. Spoiler dabei! ;)
Achtung, für manche sind hier evtl. Spoiler dabei! ;)
Im nächsten Schritt (unter #creating a corpus) wird ein spezifisches Format für die Textanalyse erschaffen. Dieses Corpus-Format enthält den Text zusammen mit Metadaten. Zusammen mit den weiteren Schritten erhalten wir danach folgendes Format:
> corpus$content
[1] "part begins robbers rushing save nairobis life tokyo stages coup détat egocentric palermo walk bank restrained chair group help tokyo ..."
Hierbei ist zu beachten, dass ich die Stemming-Funktion deaktiviert habe; diese reduziert die Wörter auf ihren Wortstamm.
Im nächsten Schritt wird eine document-term matrix geschaffen, welche vor allem dann nützlich ist, wenn wir viele Texte haben - mit einem Dokument pro Reihe, einem Wort pro Spalte und einem Integer Wert zur Anzahl des spezifischen Worts in dem jeweiligen Dokument. Nun wie sieht es bei unserem Beispiel aus?
> findFreqTerms(dtm, lowfreq = 4) #words that occur at least n times
[1] "bank" "gandía" "group" "lisbon" "palermo" "panic" "police" "professor" "room"
[10] "tokyo"
Und die beiden Plot-Funktionen ergeben folgende Grafiken:
Klar, Wikipedia versucht vor allem die Staffel zu beschreiben. Aber ist auch eine Wertung dabei? Dies untersuchen wir im nächsten Schritt "Sentiment Analysis". Schauen wir uns erst das Ergebnis an:
> sent <- analyzeSentiment(dtm, language = "german")
> sent
WordCount SentimentGI NegativityGI PositivityGI SentimentHE NegativityHE PositivityHE SentimentLM NegativityLM
1 219 0.00913242 0.03652968 0.0456621 0 0 0 -0.03196347 0.03196347
PositivityLM RatioUncertaintyLM SentimentQDAP NegativityQDAP PositivityQDAP
1 0 0 0.00456621 0.02283105 0.02739726
Wir sehen hier die jeweils ein Gesamtergebnis und den Positiv- und Negativ-Score zu vier verschiedenen Methoden. Uns interessiert nur der jeweilige Gesamt Score. Zum Beispiel beim GI-Wert wird das Harvard-IV dictionary benutzt welches Wörter beinhaltet die mit positiver oder negativer Stimmung verbunden werden. Weitere Dictionaries sind das Henry´s Financial Dictionary, das Loughran-McDonald Financial dictionary und das QDAP dictionary. Die Ansätze können recherchiert werden; im Endeffekt wird bestimmten Wörtern ein Wert zugeschrieben und ein Gesamtwert von -1 (schlecht) zu +1 (positiv) berechnet.
Das Ergebnis zeigt, dass von den vier hinterlegten Methoden zwei leicht positiv, eines leicht negativ und eines gar keinen Score erbringt. In diesem Fall kann auch das "kein Score" als ein Ergebnis gewertet werden - und zwar als neutral. Insgesamt aber tatsächlich ein eher negatives Ergebnis - was auf die Handlungen in der Staffel zurückgeführt werden kann. Es ist ja auch ein Drama! Wäre der Text eine Kritik, so könnten wir noch mehr aus dem Sentiment-Score lesen.
> head(sentiment)
SentimentGI SentimentHE SentimentLM SentimentQDAP WordCount mean_sentiment
1 0.00913242 0 -0.03196347 0.00456621 219 -0.00456621
> convertToDirection(sentiment$mean_sentiment) #convert to direction
[1] negative
Levels: negative neutral positive
Und wie sieht es mit den Emotionen aus? Das syuzhet package benutzt hierfür das sogenannte NRC emotion lexicon, welches Wörter mit bestimmten Emotionen assoziiert. Die dokumentierte Plot-Funktion liefert folgendes Schaubild:
Tatsächlich, es scheint ein Drama zu sein, welches eine ganze Reihe an Emotionen im Hörer hervorrufen soll. Dies wäre hiermit zumindest methodisch hinterlegt.
Interessant wäre es nun, z.B. Staffel 4 mit Staffel 3 zu vergleichen. Hierfür gehen wir noch einmal zurück zu der Wordcloud-Funktion. Im Code zu sehen, können wir einmal eine Comparison und eine Commonality Cloud anfertigen und die Unterschiede bzw. Gemeinsamkeiten identifizieren. Hier die Ergebnisse:
Das Ergebnis dieser Comparison Cloud zeigt, dass z.B. das Wort "Helikopter" öfters bei Staffel 4 als bei Staffel 3 vorkommt. Dies deckt sich tatsächlich mit der Serie. Und bei der folgenden Commonality Cloud ist klar, dass z.B. der "Professor" eines der verbindenen Wörter ist.
Anbei noch der benutzte Code in RStudio:library("tm")
library("SentimentAnalysis")
library("syuzhet")
library("SnowballC")
library("wordcloud")
library("ggplot2")
library("tibble")
# TEXT
text <- c("Part 4 begins with the robbers rushing ...")
# CREATING A CORPUS (format for storing textual data for analysis)
corpus <- Corpus(VectorSource(text))
toSpace <- content_transformer(function (x , pattern ) gsub(pattern, " ", x)) # text clean ing
corpus <- tm_map(corpus, toSpace, "/") # text cleaning
corpus <- tm_map(corpus, toSpace, "€") # text cleaning
corpus <- tm_map(corpus, toSpace, "@") # text cleaning
corpus <- tm_map(corpus, toSpace, "\\|") # text cleaning
corpus <- tm_map(corpus, content_transformer(tolower)) # Convert the text to lower case
corpus <- tm_map(corpus, removeNumbers) # Remove numbers
corpus <- tm_map(corpus, removeWords, stopwords("english")) # Remove common stopwords
corpus <- tm_map(corpus, removeWords, c("blabla1", "blabla2")) # Remove own stop words by specifying them as a character vector
corpus <- tm_map(corpus, removePunctuation) # Remove punctuations
corpus <- tm_map(corpus, stripWhitespace) # Eliminate extra white spaces
#corpus <- tm_map(corpus, stemDocument) # stemming (collapsing words to a common root)
corpus$content
#CREATING A DOCUMENT-TERM MATRIX (DTM)
dtm <- TermDocumentMatrix(corpus)
inspect(dtm)
# ANALYSIS ON FREQUENCY
m <- as.matrix(dtm)
v <- sort(rowSums(m),decreasing=TRUE)
d <- data.frame(word = names(v),freq=v)
head(d, 10)
findFreqTerms(dtm, lowfreq = 4) #words that occur at least n times
barplot(d[1:10,]$freq, las = 2, names.arg = d[1:10,]$word,
col ="lightblue", main ="Most frequent words",
ylab = "Word frequencies")
set.seed(1234) #Generate the Word cloud
wordcloud(words = d$word, freq = d$freq, min.freq = 2,
max.words=200, random.order=FALSE, rot.per=0.35,
colors=brewer.pal(8, "Dark2"))
# SENTIMENT ANALYSIS
sent <- analyzeSentiment(dtm, language = "german")
sentiment <- dplyr::select(sent, SentimentGI, SentimentHE, SentimentLM, SentimentQDAP, WordCount) #removing the unnecessary “Negativity” and “Positivity” measures
sentiment <- dplyr::mutate(sentiment, mean_sentiment = rowMeans(sentiment[,-5])) #average of every row (no reason to rely on any dictionary more than others)
sentiment <- as.data.frame(sentiment) #as a dataframe
head(sentiment) #SentimentGI negative (<0), neutral (=0), positive(>0) with range from -1 to +1
convertToDirection(sentiment$mean_sentiment) #convert to direction
summary(sentiment$mean_sentiment) #of overall score
convertToDirection(sent$SentimentGI) #convert to direction
# EMOTIONS
sent2 <- get_nrc_sentiment(text)
sent3 <- as.data.frame(colSums(sent2))
sent3 <- rownames_to_column(sent3)
colnames(sent3) <- c("emotion", "count")
ggplot(sent3, aes(x = emotion, y = count, fill = emotion)) +
geom_bar(stat = "identity") +
theme_minimal() +
theme(legend.position="none", panel.grid.major = element_blank()) +
labs( x = "Emotion", y = "Total Count") +
ggtitle("Sentiment of Text") +
theme(plot.title = element_text(hjust=0.5))
# TEXT COMPARISON
text1 <- c("Part 3 begins two to three years after ...")
text2 <- c("Part 4 begins with the robbers rushing to save Nairobi's life, ...")
text <- c(text1, text2)
corpus <- Corpus(VectorSource(text)) %>%
tm_map(removePunctuation) %>%
tm_map(removeNumbers) %>%
tm_map(tolower) %>%
tm_map(removeWords, stopwords("english")) %>%
tm_map(stripWhitespace) %>%
tm_map(PlainTextDocument)
tdm <- TermDocumentMatrix(corpus) %>%
as.matrix()
colnames(tdm) <- c("Part 3","Part 4")
par(mar = rep(0, 4))
comparison.cloud(tdm, random.order=FALSE, colors = c("indianred3","lightsteelblue3"), title.size=2.5, max.words=400, res=1000)
commonality.cloud(tdm, random.order=FALSE, scale=c(5, .5),colors = brewer.pal(4, "Dark2"), max.words=400)