klippy::klippy()

Grundlagen der Sentiment-Analyse {.smaller}

Sentiment-Analysen erfreuen sich großer Beliebtheit. Text Mining-Blogs zeigen in vielen Varianten die Möglichkeiten, die Variation von Bewertungen von Texten mit einem numerischen Indikator zu erfassen und Veränderungen im Zeitverlauf zu analysieren und darzustellen.

Welche Kinofilme werden besonders gut oder besonders schlecht bewertet? Dies lässt sich anhand von Filmbesprechungen untersuchen. Wie ist die Resonanz von Kunden auf ein neu in den Markt eingeführtes Produkt? Dazu lassen sich Kommentare in sozialen Medien untersuchen. Es gibt sicherlich eine Palette nützlicher Einsatzszenarien für Sentiment-Analysen, gerade auch jenseits der Wissenschaft.

Welchen Nutzen haben Sentiment-Analysen in wissenschaftlichen Arbeiten? Zentrale Fragen sind hier, was man eigentlich misst, wenn man "Sentiments" misst. Ist die Validität der Messung gegeben, misst man was man glaubt zu messen? Aus den Antworten leitet sich ab, wann und wie Sentiment-Analysen gut begründet als Forschungsinstrument eingesetzt werden können. Wesentlich ist dabei folgender Unterschied: (a) Diktionärsbasierte Verfahren messen anhand von Listen mit positivem / negativem Vokabular. (b) Machine Learning-basierte Verfahren gehen aus von Trainingsdaten mit bekannten Bewertungen und treffen qua Algorithmus Ableitungen für neu zu bewertende Texte.

In dieser Anleitung arbeiten wir mit einem - deutlich einfacheren - diktionärsbasierten Verfahren und einem klassischen Diktionär, "SentiWS".

Erforderliche Installationen / Pakete {.smaller}

Die folgenden Erläuterungen nutzen das polmineR-Paket und das GermaParl-Korpus. Die Installation wird in einem eigenen Foliensatz erläutert. Ergänzend nutzen wir die folgenden Pakete:

Der folgende Code prüft, ob diese Pakete installiert sind und nimmt die Installation vor, falls erforderlich.

required_packages <- c("zoo", "magrittr", "devtools")
for (pkg in required_packages)
  if (!pkg %in% rownames(installed.packages())) install.packages(pkg)

Bitte beachten Sie, dass die Funktionalität für den folgenden Workflow erst mit polmineR-Version r as.package_version("0.7.9.9005") zur Verfügung steht. Installieren Sie bei Bedarf die aktuelle Entwicklungsversion von polmineR.

if (packageVersion("polmineR") < as.package_version("0.7.9.9005"))
  devtools::install_github("PolMine/polmineR", ref = "dev")

Los geht's {.smaller}

Die erforderlichen Pakete werden nun geladen.

library(zoo, quietly = TRUE, warn.conflicts = FALSE)
library(devtools)
library(magrittr)
library(data.table)
library(xts)

Wir laden auch polmineR, wodurch das GermaParl-Korpus anschließend verfügbar ist.

library(polmineR)

Wir holen 'get_sentiws' von GitHub {.smaller}

Als Diktionär nutzen wir in der folgenden exemplarischen Analyse den Sentiment-Wortschatz des Leipziger Wortschatz-Projekts, kurz "SentiWS". Eine Erläuterung finden Sie hier. Die Daten stehen unter einer CC-BY-SA-NC Lizenz zur Verfügung und können im wissenschaftlichen Kontext ohne lizenzrechtliche Einschränkungen verwendent werden. Erforderlich ist selbstverständlich, dass die Daten korrekt zitiert werden.

SentiWS kann als zip-Datei heruntergeladen werden. Um die Dinge weiter zu vereinfachen ist als Gist bei der GitHub-Präsenz des PolMine-Projekts eine Funktion hinterlegt, die den Download erledigt und automatisch eine tabellarische Datenstruktur generiert, wie wir sie benötigen werden.

gist_url <- "https://gist.githubusercontent.com/PolMine/70eeb095328070c18bd00ee087272adf/raw/c2eee2f48b11e6d893c19089b444f25b452d2adb/sentiws.R"
devtools::source_url(gist_url) # danach ist Funktion verfügbar
SentiWS <- get_sentiws()

Damit ist mit dem Objekt SentiWS das Diktionär verfügbar.

SentiWS: Ein Blick in die Daten {.smaller}

Das SentiWS-Objekt ist ein data.table. Wir nutzen das anstelle eines klassischen data.frame, weil das später das Matching der Daten (Worte im Korpus und Worte im Diktionär) erleichtert und beschleunigt. Damit wir verstehen, womit wir arbeiten, werfen wir einen schnellen Blick in die Daten.

head(SentiWS, 10)

SentiWS: Ein Blick in die Daten, Teil 2 | Gewichtung von Worten im Diktionär {.smaller}

In der letzten Spalte ("weight") sehen Sie, dass Worten im Diktionär eine Gewichtung zugewiesen ist. Diese kann positiv sein (bei "positivem" Vokabular) oder negativ (bei "negativen" Worten). Außerdem sehen Sie, dass neben der Wortform ("word"-Spalte) auch Lemmata aufgeführt sind sowie eine Part-of-Speech-Klassifikation. Letzteres macht eine eindeutige Zuordnung möglich. Die Spalte "base" mit einem logischen Wert (TRUE/FALSE) ergibt sich technischer aus der Umwandlung der vom Leipziger Wortschatzprojekt heruntergeladenen Tabelle in die extensive Form: In der Roh-Tabelle findet sich in jeder Zeile ein Lemma. Dementsprechend können wir prüfen, wie viele positive bzw. negative Worte als Grundform in der Tabelle sind.

vocab <- c(
  positive = nrow(SentiWS[base == TRUE][weight > 0]),
  negative = nrow(SentiWS[base == TRUE][weight < 0])
  )
vocab

Untersuchung des Wortumfeldes | Positives/negatives Vokabular {.smaller}

options("polmineR.left" = 10L)
options("polmineR.right" = 10L)
df <- context("GERMAPARL", query = "Islam", p_attribute = c("word"), verbose = FALSE) %>%
  partition_bundle(node = FALSE) %>%
  set_names(s_attributes(., s_attribute = "date")) %>%
  weigh(with = SentiWS) %>% summary()

Die tabellarischen Daten der Sentiment-Analyse | Positive und negative Begriffe {.smaller}

Der df-data.frame führt die Statistik des Wortumfelds eines jeden Auftretens von "Islam" im Korpus auf. Um die Dinge einfach zu halten, arbeiten wir zunächst nicht mit den Gewichtungen, sondern nur mit dem positiven bzw. negativen Worten. Wir vereinfachen die Tabelle entsprechend und sehen sie an.

df <- df[, c("name", "size", "positive_n", "negative_n")] 
head(df, n = 12)

Aggregation {.smaller}

df[["year"]] <- as.Date(df[["name"]]) %>% format("%Y-01-01")
df_year <- aggregate(df[,c("size", "positive_n", "negative_n")], list(df[["year"]]), sum)
colnames(df_year)[1] <- "year"
df_year$negative_share <- df_year$negative_n / df_year$size
df_year$positive_share <- df_year$positive_n / df_year$size
Z <- zoo(
  x = df_year[, c("positive_share", "negative_share")],
  order.by = as.Date(df_year[,"year"])
)

Visualisierung von Aggregationen {.smaller}

plot(
  Z, ylab = "polarity", xlab = "year", main = "Word context of 'Islam': Share of positive/negative vocabulary",
  cex = 0.8, cex.main = 0.8
)

Wie gut sind eigentlich die Ergebnisse? | Kwic-Ausgabe der positiven und negativen Worte {.smaller}

Aber was verbirgt sich eigentlich hinter den numerischen Werten der ermittelten Sentiment-Scores? Um dem nachzugehen, nutzen wir die Möglichkeit von polmineR, eine KWIC-Ausgabe entsprechend einer Positiv-Liste (Vektor mit erforderlichen Worten) zu reduzieren, Worte farblich zu codieren und über Tooltips (hier: Wortgewichte) weitergehende Informationen einzublenden. Also: Fahren Sie auch mit der Maus auf die angemarkerten Worte!

words_positive <- SentiWS[weight > 0][["word"]]
words_negative <- SentiWS[weight < 0][["word"]]
kwic("GERMAPARL", query = "Islam", positivelist = c(words_positive, words_negative)) %>%
  highlight(lightgreen = words_positive, orange = words_negative) %>%
  tooltips(setNames(SentiWS[["word"]], SentiWS[["weight"]]))

Das Ergebnis (ein 'htmlwidget') folgt auf der nächsten Folie.

Sentiment-Analyse: Qualitative Evaluation | Das positive und negative Wortumfeld {.smaller}

options("polmineR.pagelength" = 7L)
kwic("GERMAPARL", query = "Islam", positivelist = c(words_positive, words_negative)) %>%
  highlight(lightgreen = words_positive, orange = words_negative) %>%
  tooltips(setNames(SentiWS[["word"]], SentiWS[["weight"]]))

Vorgehen Sentiment-Analyse über Partition {.smaller}

p <- partition("GERMAPARL", parliamentary_group = "CDU/CSU", interjection = FALSE)
df <- context(p, query = "Islam", p_attribute = c("word"), verbose = FALSE) %>%
  partition_bundle(node = FALSE) %>%
  set_names(s_attributes(., s_attribute = "date")) %>%
  weigh(with = SentiWS) %>%
  summary() %>%
  subset(select = c("name", "size", "positive_n", "negative_n"))

Funktion für Aggregationen {.smaller}

time_index <- as.Date(df[["name"]]) # 
df[["name"]] <- NULL # Spalte 'name' nicht mehr benötig, wird gelöscht
tseries <- as.xts(df, order.by = time_index) # Umwandlung in Zeitreihen-Objekt
aggregate_time_series <- function(x, aggregation){
  y <- switch(
    aggregation,
    week = aggregate(x, {a <- lubridate::ymd(paste(lubridate::year(index(x)), 1, 1, sep = "-")); lubridate::week(a) <- lubridate::week(index(x)); a}),
    month = aggregate(x, as.Date(as.yearmon(index(x)))),
    quarter = aggregate(x, as.Date(as.yearqtr(index(x)))),
    year = aggregate(x, as.Date(sprintf("%s-01-01", gsub("^(\\d{4})-.*?$", "\\1", index(x)))))
    )
  y$negative_share <- -1 * (y$negative_n / y$size)
  y$positive_share <- y$positive_n / y$size
  as.xts(y)
}

Aggregationsniveau Jahr {.smaller}

- In einem ersten Durchlauf sehen wir das Ergebnis auf dem Ergebnis des Aggregationsniveau eines Jahres an. wzxhzdk:22 - Wir erzeugen ein Zeitreihen-Digramm. wzxhzdk:23

Vergleich verschiedener Aggregationen {.smaller}

par(mfrow = c(2,2))
for (aggregation_level in c("week", "month", "quarter", "year")){
  x <- aggregate_time_series(tseries, aggregation_level)
  x <- x[,c("positive_share", "negative_share")]

  y <- plot(
    x,
    multi.panel = FALSE,
    ylab = "polarity",
    xlab = "year",
    main = aggregation_level,
    cex = 0.3,
    cex.main = 0.3,
    ylim = c(-0.05, 0.05),
    type = "l"
  )
  show(y)
}

Sentiment-Analyse zu "Islam" | Aggregation nach Woche, Monat, Quartal, Jahr {.flexbox .vcenter}

par(mfrow = c(2,2))
for (aggregation_level in c("week", "month", "quarter", "year")){
  x <- aggregate_time_series(tseries, aggregation_level)
  x <- x[,c("positive_share", "negative_share")]

  y <- plot(
    x,
    multi.panel = FALSE,
    ylab = "polarity",
    xlab = "year",
    main = aggregation_level,
    cex = 0.3,
    cex.main = 0.3,
    ylim = c(-0.05, 0.05),
    type = "l"
  )
  show(y)
}

Diskussion {.smaller}

Literatur {.smaller}



PolMine/UCSSR documentation built on June 13, 2022, 10:23 p.m.