Eine Sentiment-Analyse funktioniert im Grunde wiefolgt: die einzelnen Wörter werden eines Textes werden mit bestimmten Bibliotheken abgeglichen und eine Einteilung in "positiv/negativ" oder ein hinterlegter Sentiment-Wert abgegriffen. Die Summe dieser Werte ergibt schließlich den Sentiment-Score des ganzen Texts. Für englische Texte sind in R bereits Bibliotheken hinterlegt (z.B. im Package tidytext). Für deutsche Texte habe ich auf meiner Recherche die Bibliothek SentiWS der Universität Leipzig gefunden. Die rund 16.000 positiven und 18.000 negativen Wörter sind mit einer Wertspanne von -1 zu 1 hinterlegt.
Das Problem ist, dass diese in zwei Textdateien kommen, deren Format erst aufbereitet werden muss. So sieht die Bibliothek beim Einlesen aus:
Mit folgendem Code habe ich mir die Bibliothek operationalisiert:library(dplyr)
# SentiWS - Dateien hier runterladen: https://wortschatz.uni-leipzig.de/en/download
# a) negative Wörter
# die Textdatei einlesen
negativ <- read.table("~/.../Sentiment Analysis/SentiWS_v2.0_Negative.txt", fill = TRUE)
# zuerst separieren wir die Wörter in V1
neg1 <- negativ %>%
select(V1, V2) %>% #wir brauchen nur diese beiden Spalten
mutate(V1 = as.character(V1)) %>% #benötigt für den nächsten Schritt
mutate(V1 = sub("\\|.*","\\",V1)) %>% #bereinigt ohne den Anhang nach "|"
`colnames<-`(c("word", "sentiment")) #Spalten werden umbenannt
# nun separieren wir die Wörter in V2
einzel_negativ <- strsplit(as.character(negativ$V3), split = ",") #die aufgelisteten Wörter werden getrennt
neg2 <- data.frame(V1 = rep(negativ$V2, sapply(einzel_negativ, length)), V3 = unlist(einzel_negativ)) %>% #und mit den Werten in V2 wieder zusammengefügt
`colnames<-`(c("sentiment", "word")) #Spalten werden umbenannt
# b) positive Wörter
# die Textdatei einlesen
positiv <- read.table("~/.../Sentiment Analysis/SentiWS_v2.0_Positive.txt", fill = TRUE)
# zuerst separieren wir die Wörter in V1
pos1 <- positiv %>%
select(V1, V2) %>% #wir brauchen nur diese beiden Spalten
mutate(V1 = as.character(V1)) %>% #benötigt für den nächsten Schritt
mutate(V1 = sub("\\|.*","\\",V1)) %>% #bereinigt ohne den Anhang nach "|"
`colnames<-`(c("word", "sentiment")) #Spalten werden umbenannt
# nun separieren wir die Wörter in V2
einzel_positiv <- strsplit(as.character(positiv$V3), split = ",") #die aufgelisteten Wörter werden getrennt
pos2 <- data.frame(V1 = rep(positiv$V2, sapply(einzel_positiv, length)), V3 = unlist(einzel_positiv)) %>% #und mit den Werten in V2 wieder zusammengefügt
`colnames<-`(c("sentiment", "word")) #Spalten werden umbenannt (Achtung, andere Reihenfolge)
# c) gemeinsames Lexikon aus den vier Dataframes
SentiWS_df <- rbind(neg1 %>%
mutate(Polarität = "negative"),
neg2%>%
mutate(Polarität = "negative"),
pos1 %>%
mutate(Polarität = "positive"),
pos2 %>%
mutate(Polarität = "positive")) %>%
mutate("word" = as.character(word))
SentiWS_df <- SentiWS_df[!duplicated(SentiWS_df$word),] #manche Wörter kommen durch die Umwandlung dopppelt vor; jeweils der erste wird behalten
Nun haben wir folgendes Format, mit dem wir arbeiten können:
Im Folgenden benutzen wir diese Bibliothek für die Analyse der Tweets des Manager Magazins und der Wirtschaftswoche für 2020. Mal schauen, was wir über die allgemeine Wirtschaftslage in Zeiten von Corona herausfinden können:
Interessant ist, dass die positiven Wörter konstant häufiger benutzt werden als negative Wörter wie Plot 1 zeigt. Doch bedenken wir, dass das Lexikon mehr negative als positive Wörter hat, so macht ein Blick auf die relative Frequenz Sinn. Mit Blick auf das Verhältnis der positiven zu negativen Wörtern können wir zugleich die Wörter ohne Ausprägung auslassen. Da wir uns für die Stimmung interessieren, macht dies Sinn. Plot 2 zeigt, dass es einzelne Tage sind, an denen die Anzahl positiver Wörter signifikant höher ist als die negativer Wörter.
Interessant wird Plot 3, wo wir auf die Verwendung beider Polaritäten verzichten und die negative Polarität als die Invertierung der Anzahl positiver Wörter benutzen und vice versa. Die skalierten Werte ergibt sich ähnelnde Verläufe mit durchwegs negativem Score. Der Verlauf ähnelt dem eigentlichen Sentiment-Wert, mit welchem die Wörter hinterlegt sind. Wir sehen den negativen Verlauf - trotz Ausreißer mit negativem Trend in Plot 4. Dies bedeutet, dass die wenigen negativen Wörter erheblicher ins Gewicht fallen (also gravierender sind) als die positiven. Dieses neue Bild passt auch gut zur aktuellen Wirtschaftslage.
Kann man aus den Scores noch mehr lesen? Ich habe geschaut, wie der Zusammenhang zum DAX im gleichen Zeitraum aussieht. Die Korrelationsmatrix zeigt allerdings einen schwachen (blau - positiv & rot - negativ) und nicht-signifikanten Zusammenhang (Größe und Transparenz). Somit kann man die Scores zwar nicht als Stimmungsbarometer der deutschen Wirtschaft nehmen; die Intention ist aber auch nicht gegeben.
Und hier noch der Code in RStudio
# für die Verbindung zu Twitter
library(rtweet)
# für basics
library(dplyr)
# für die Textanalyse
library(stopwords)
library(tidytext)
library(scales)
# für die Data Prep
library(tidyverse)
# für die Aktiendaten
library(quantmod)
library(ISOweek)
# für die Korrelation
library(Hmisc)
library(corrplot)
# Twitter-Daten laden & aufbereiten
#tweets <- search_tweets("Lufthansa", n = 18000, lang="de") ## search for tweets containing the word data
#tweets_lufthansa <- tweets
tweets <- get_timelines(c("manager_magazin","wiwo"), n = 10000)
custom_stop_words <- bind_rows(tibble(word = c("twitter", "tco"), lexicon = c("custom")),
tibble(word = stopwords("de"), lexicon = c("stopwords")))
tweets_words <- tweets %>%
mutate(tweet_number = row_number())%>%
select(tweet_number, text, created_at)%>%
as_tibble() %>%
mutate(text = str_replace_all(text, "[^\x01-\x7F]", ""),
text = str_replace_all(text, "\\.|[[:digit:]]+", ""),
text = str_replace_all(text, "https|amp|t.co", ""),
text = gsub("http.*","", text),
text = gsub("https.*","", text),
text = str_replace_all(text,"&|<|>", ""))
tweets_words <- tweets_words %>%
unnest_tokens(word, text) %>%
anti_join(custom_stop_words, by = "word")
# Sentiment-Analyse
tweets_sentiment <- tweets_words %>%
left_join(SentiWS_df, by="word")
# Plot1 - Anzahl positiver & negativer Wörter pro Tag
tweets_sentiment %>%
drop_na() %>%
mutate("created_at" = as.Date(created_at)) %>%
group_by(created_at) %>%
count(Polarität) %>%
ggplot(aes(x=created_at, y=n, group=Polarität, color=Polarität)) +
geom_line(size=0.6, alpha=0.6)+
geom_smooth(span=0.2, se=FALSE, size=0.8)+
scale_colour_brewer(palette = "Set1") +
theme_minimal() +
labs(
x = NULL, y = NULL,
title = "Anzahl positiver & negativer Wörter",
subtitle = "aggregiert pro Tag",
caption = "Plot 1"
)
# Plot2 - Verhältnis positiver zu negativen Wörtern pro Tag
tweets_sentiment %>%
drop_na() %>%
mutate("created_at" = as.Date(created_at)) %>%
group_by(created_at) %>%
count(Polarität) %>%
spread(Polarität, n, fill=0) %>%
mutate(relation = positive/negative) %>%
ggplot(aes(x=created_at, y=relation, group=1)) +
geom_line(size=1, color="#004C99")+
theme_minimal() +
labs(
x = NULL, y = NULL,
title = "Verhältnis positiver zu negativen Wörtern",
subtitle = "aggregiert pro Tag",
caption = "Plot 2"
)
# Plot3 - Sentiment-Score pro Tag
tweets_sentiment2 <-tweets_sentiment %>%
drop_na() %>%
mutate("created_at" = as.Date(created_at)) %>%
group_by(created_at) %>%
count(Polarität) %>%
spread(Polarität, n, fill=0)
tweets_sentiment2$score_p <- rescale(tweets_sentiment2$positive, to=c(-1,1)) #bei dieser Anwendungen wird die negative Polarität schlicht die Invertierung des positiven Sentiments
tweets_sentiment2$score_n <- rescale(tweets_sentiment2$negative, to=c(-1,1)) #bei dieser Anwendungen wird die negative Polarität schlicht die Invertierung des positiven Sentiments
tweets_sentiment2 %>%
ggplot(aes(x=created_at, y=score_n, group=1)) +
geom_line(size=0.6, alpha=0.4, color="#004C99")+
geom_line(aes(y=score_p, group=1),size=0.6, alpha=0.4, color="#990000")+
geom_smooth(span=0.2, se=FALSE)+
geom_smooth(aes(y=score_p),span=0.2, se=FALSE, color="#990000")+
labs(
x = NULL, y = NULL,
title = "Sentiment-Score",
subtitle = "aggregiert pro Tag; blau - Invertierung der positiven Wörter & rot - Invertierung der negativen Wörter",
caption = "Plot 3"
)
# Plot 4 - Sentiment-Wert pro Tag
tweets_sentiment %>%
drop_na() %>%
mutate("created_at" = as.Date(created_at)) %>%
group_by(created_at) %>%
summarise(sentiment = sum(sentiment)) %>%
ggplot(aes(x=created_at, y=sentiment, group=1)) +
geom_line(size=0.6, alpha=0.6, color="#004C99")+
geom_smooth(span=0.2, se=FALSE)+
theme_minimal() +
labs(
x = NULL, y = NULL,
title = "Sentiment-Wert",
subtitle = "aggregiert pro Tag",
caption = "Plot 4"
)
# Zusammenhang zum Dax
l<- c("^GDAXI")
start_date = "2019-11-05"
getSymbols(l,src="yahoo", from=start_date)
l[1] <- "GDAXI"
GDAXI <- data.frame(date=index(GDAXI), coredata(GDAXI))
# den Sentiment-Score zum Dataset mit den Sentiment-Scores dazu
data_sent <- tweets_sentiment %>%
select(created_at, sentiment) %>%
drop_na() %>%
mutate("created_at" = as.Date(created_at)) %>%
group_by(created_at) %>%
summarise(sentiment = sum(sentiment))
joined_data <- left_join(tweets_sentiment2, data_sent, by="created_at")
joined_data <- left_join(joined_data, GDAXI, by=c("created_at" = "date")) %>%
filter(complete.cases(GDAXI.Close))
# Plot 5 - Correlation Matrix
res2 <- rcorr(as.matrix(joined_data[2:ncol(joined_data)]))
#Positive correlations in blue, negative in red Color intensity & size of circle proportional to correlation coefficients
corrplot(res, type = "upper", order = "hclust",
tl.col = "black", tl.srt = 45)
Notiz: in wissenschaftlichen Artikeln soll das Lexikon wiefolgt zitiert werden:
R. Remus, U. Quasthoff & G. Heyer: SentiWS - a Publicly Available German-language Resource for Sentiment Analysis.
In: Proceedings of the 7th International Language Ressources and Evaluation (LREC'10), 2010
Photo by Ingo Joseph from Pexels