Sentimentanalyse mit SentiWS in R

SentiWS ist der einzige deutsche Sentimentwortschatz unter freier Lizenz. Leider ist er für eine automatisierte Auswertung nicht optimal strukturiert. Ein einfaches R-Skript behebt das. Auch die Sentimentanalyse selbst ist kein Hexenwerk.

Sortieren von SentiWS

SentiWS wie es von der Uni Leipzig veröffentlicht wurde ist für eine automatische Auswertung schlecht strukturiert:

Abbau|NN	-0.058	Abbaus,Abbaues,Abbauen,Abbaue
Abbruch|NN	-0.0048	Abbruches,Abbrüche,Abbruchs,Abbrüchen
Abdankung|NN	-0.0048	Abdankungen
Abdämpfung|NN	-0.0048	Abdämpfungen
Abfall|NN	-0.0048	Abfalles,Abfälle,Abfalls,Abfällen
Abfuhr|NN	-0.3367	Abfuhren
Abgrund|NN	-0.3465

In der ersten Spalte der Textdatei ist die Grundform des Wortes und, durch Pipe getrennt, die Wortart. Der Sentiment-Score von -1 bis 1 ist in Spalte zwei und in der dritten Spalte die Flexionen des Worts. Um schnell durch die Datenbank iterieren zu können, wäre es sinnvoller, die Grundform ohne die Wortart in der ersten Spalte zu haben und sämtliche Flexionen mit ihrem eigenen Sentiment-Score ebenfalls in Spalte eins zu platzieren.

Die Wortart entfernt man am einfachsten mit einem regulären Ausruck (Regex) in einem Texteditor wie Gedit oder Notepad Plus. Regex in R können etwas frickelig sein, weil Sonderzeichen zuerst von R interpretiert werden, bevor diese als Regex verwendet werden. Mit folgendem Regex findet man alle Wortarten; Diese einfach durch nichts ersetzen:

\|[A-Z]*

Das folgende Skript ließt zuerst die beiden Textdateien der Uni Leipzig ein und teilt die dritte Spalte in ihre einzelnen Worte auf.

#manually delete the |NN stuff, it doesn't work very well in R. Regex: \|\|[A-Z]*

#load sentiment files
negativ <- read.table("SentiWS_v1.8c_Negative.txt", fill = TRUE)
positiv <- read.table("SentiWS_v1.8c_Positive.txt", fill = TRUE)

#split into single words
einzelworte_negativ <- strsplit(as.character(negativ$V3), split =",")
einzelworteframe_negativ <- as.data.frame(unlist(einzelworte_negativ))

einzelworte_positiv <- strsplit(as.character(positiv$V3), split =",")
einzelworteframe_positiv <- as.data.frame(unlist(einzelworte_positiv))

Anschließend iteriert R jeweils durch die Wortlisten positiv und negativ. Dabei wird für jede Flexion aus der dritten Spalte der Sentiment-Score in die zweite Spalte kopiert.

#takes the number of words and creates a data frame only with the sentiment scores as many times as the word inflection occurs.
number_words <- summary(einzelworte_negativ)

sentiment_score <- NULL
for (i in 1:1818) {
j <- 0
while (j < as.numeric(number_words[i])) {
 sentiment_score <- rbind(sentiment_score, negativ[i,2])
 j <- j+1
}
}

number_words <- summary(einzelworte_positiv)

sentiment_score <- NULL
for (i in 1:1650) {
  j <- 0
  while (j < as.numeric(number_words[i])) {
    sentiment_score <- rbind(sentiment_score, positiv[i,2])
    j <- j+1
  }
}

Anschließend werden die Wörter und die Scores zusammengefügt und unten an die bisherige Wortliste geschrieben.

#bind the sentiment score with the words
new_negativ <- cbind(as.character(einzelworteframe_negativ[,1]), sentiment_score)
new_negativ <- rbind(negativ[,1:2], new_negativ)

new_positiv <- cbind(as.character(einzelworteframe_positiv[,1]), sentiment_score)
new_positiv <- rbind(positiv[,1:2], new_positiv)

Die fertige und leichter zu handhabende Datei sieht nun folgendermaßen aus:

"","Wort","sentiment score"
"1","Abbau","-0.058"
"2","Abbruch","-0.0048"
"3","Abdankung","-0.0048"
[...]
"1819","Abbaus","-0.058"
"1820","Abbaues","-0.058"
"1821","Abbauen","-0.058"
"1822","Abbaue","-0.058"
[...]

R speichert in der CSV-Datei dabei auch den Score als Character, was mit as.numeric() vermieden werden könnte.

Die neuformatierte positive und negative Liste können hier heruntergeladen werden. Sie stehen wie SentiWS auch unter Creative Commons Attribution-Noncommercial-Share Alike 3.0 Unported License.

Sentimentanalyse

Im folgenden Beispiel ging es darum, aus einer JSON-Datei mit Kommentaren das Sentiment der Kommentare herauszufinden.

Mit der neu strukturierten Liste an Wörtern und Sentiment-Score ist es nun ein leichtes durch jedes Wort zu iterieren und es mit dem Wörterbuch abzugleichen. Dafür zuerst den Text und das Sentimentwörterbuch einlesen. Anschließend den Text von allem befreien, was die Texterkennung behindert. Dazu zählen vor allem Satzzeichen, weil sie direkt am Wort sind und dieses dann nicht erkannt würde.

require(jsonlite)

comments <- fromJSON("comments.json", flatten = TRUE)

#export actual comments
comments <- comments$threads$postings.content.body

#remove links and interpunctuation
comments <- gsub(" ?(f|ht)tp(s?)://(.*)[.][a-z]+", "", comments)
comments <- gsub("\\[.*?\\]", "", comments)
comments <- gsub("[^[:alnum:] ]", "", comments)

#import sentiment tables
negativ <- read.csv("new_negativ.csv", row.names = 1)
positiv <- read.csv("new_positiv.csv", row.names = 1)

Jetzt die Kommentare in einzelne Worte zerlegen…

#strip comments into single words
for (i in 1:length(comments)) {
  comments[i] <- strsplit(as.character(comments[i]), " ")
  i <- i + 1
}

…und mit dem Sentimentwörterbuch abgleichen.

liste <- NULL

for (i in 1:length(comments)) {
  score <- NULL
  #check if word in comment is in list. if yes, generate vector with scores
  for (word in unlist(comments[i])) {
    if (is.element(word, negativ[,1])) {
    score <- c(score, negativ[negativ[,1] == word,2])
    }
    if (is.element(word, positiv[,1])) {
      score <- c(score, positiv[positiv[,1] == word,2])
    }
  }
  var <- cbind(i, mean(score))
  if (!is.null(score))
    liste <- rbind(liste, var)
  else
    liste <- rbind(liste, cbind(i, 0))
  print(score)
  }

Für jeden Kommentar ist nun der Mittelwert der Sentiment-Werte in der Variable Liste abgelegt.

SentiWS wurde entwickelt von 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), pp. 1168-1171, 2010