R Markdown

Dieser Rapport dient als Analyse und Auswertung der Kundenbewertungen für das Restaurant Santa Lucia in Winterthur. In diesem Bericht werden verschiedene Aspekte der Kundenrezensionen untersucht, um Einblicke in die Meinungen und Erfahrungen der Gäste zu gewinnen.

Wir beginnen mit dem einlesen des CSV.

dat <- read.csv("C:/Users/JChar/OneDrive/Documents/zhaw/Sem 8/MQM/Santa_Lucia.csv", encoding = "UTF-8", row.names = NULL)

str(dat)
## 'data.frame':    140 obs. of  11 variables:
##  $ X             : int  0 1 2 3 4 5 6 7 8 9 ...
##  $ date          : chr  "9. Mai 2023" "3. Mai 2023" "22. März 2023" "26. Februar 2023" ...
##  $ rating        : int  50 50 30 50 50 50 30 20 50 50 ...
##  $ title         : chr  "Sehr empfehlenswert" "Siamo tornati nuovamente, al Santa Lucia winterthur, dop" "Accoglienza Italiana ???" "Gerne wieder" ...
##  $ review        : chr  "Wir wurden sehr herzlich empfangen und hatten einen tollen Abend. Die Pizzas von Santa Lucia sind sehr lecker!" "Siamo tornati con la famiglia, Martedì a pranzo abbiamo visto aria di cambiamento, più sorrisi servizio miglior"| __truncated__ "Siamo stati a cena, con la famiglia, il mangiare buono ..ma' nessuna atmosfera Italiana in Sala, Il piccolo Dir"| __truncated__ "Natürlich und wie immer bei Santa Lucia, gemütlich, sehr lecker und presiwert. Sehr gerne wieder, hoffentlich bald ." ...
##  $ language      : chr  "de" "it" "it" "de" ...
##  $ restName      : chr  "Santa_Lucia" "Santa_Lucia" "Santa_Lucia" "Santa_Lucia" ...
##  $ value         : logi  NA NA NA NA NA NA ...
##  $ service       : logi  NA NA NA NA NA NA ...
##  $ food          : logi  NA NA NA NA NA NA ...
##  $ review_deutsch: chr  "Wir wurden sehr herzlich empfangen und hatten einen tollen Abend. Die Pizzas von Santa Lucia sind sehr lecker!" "Als wir mit der Familie zurückkamen, sahen wir am Dienstag zum Mittagessen einen Hauch von Veränderung, mehr Lä"| __truncated__ "Wir haben mit der Familie zu Abend gegessen, das Essen war gut.. aber keine italienische Atmosphäre im Speisesa"| __truncated__ "Natürlich und wie immer bei Santa Lucia, gemütlich, sehr lecker und presiwert. Sehr gerne wieder, hoffentlich bald ." ...

Der vorliegende Datensatz enthält Informationen zu 140 Kundenbewertungen des Restaurants Santa Lucia. Die Variablen umfassen das Datum der Bewertung, die Bewertungspunktzahl, den Titel der Bewertung, den Text der Bewertung, die Sprache, den Namen des Restaurants sowie weitere Kategorien wie “value”, “service” und “food”. Die Analyse dieser Daten ermöglicht es uns, wichtige Erkenntnisse über die Erfahrungen der Gäste im Santa Lucia zu gewinnen. Wir können Trends in den Bewertungspunktzahlen erkennen und herausfinden, welche Aspekte des Restaurants besonders positiv oder negativ bewertet werden.

summary(nchar(dat$review))
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##    79.0   141.5   262.5   335.0   398.0  1910.0
hist(nchar(dat$review), main="Zeichenlänge der Reviews", xlab="Länge des Reviews")

Es ist zu erwarten gewesen, dass die meisten Bewertungen eher kurz ausfallen. Nun wird die durchschnittliche Länge der Rezession mit dem Rating verglichen.

dat$review[1]
## [1] "Wir wurden sehr herzlich empfangen und hatten einen tollen Abend. Die Pizzas von Santa Lucia sind sehr lecker!"
library(dplyr)
## 
## Attache Paket: 'dplyr'
## Die folgenden Objekte sind maskiert von 'package:stats':
## 
##     filter, lag
## Die folgenden Objekte sind maskiert von 'package:base':
## 
##     intersect, setdiff, setequal, union
library(ggplot2)

# Durchschnittliche Länge für jedes Rating-Level berechnen
average_length <- dat %>%
  group_by(rating) %>%
  summarize(average_length = mean(nchar(review)))

# Plot erstellen
ggplot(average_length, aes(x = rating, y = average_length)) +
  geom_line() +
  geom_point() +
  xlab("Rating-Level") +
  ylab("Durchschnittliche Länge der Bewertung") +
  ggtitle("Durchschnittliche Länge der Bewertung nach Rating-Level")

Man sieht, dass eine schlechtes Rating im Schnitt eine längere Rezession mit sich zieht. Das ist auch verständlich, da man auch deteilierter beschreiben möchte, was einem nicht gefallen hat.

Wenn man sich anschauen möchte, in welcher Sprache die Rezessionen geschrieben wurden, gibt es auf Tripadvisor bei gewissen Rezessionen ein Problem bei der Identifizierung.

barplot(table(dat$language), exclude = NULL, main="Sprachen gemäss Tripadvisor", las=1)

Von Tripadvisor konnten mehr als 30 Reviews keiner Sprache zugewiesen werden. Womöglich liegt es daran, dass die Rezession in einer Sprache ist, welche zusätzlich noch italienische Essensbezeichnungen für die Rezession verwendet. Wir versuchen mit Textcat eine Analyse zu machen, um zu sehen, ob es damit die “Unkown” einer Sprache zuweisen kann.

library("textcat")
dat$lang_cat <- textcat(dat$review)
par(mar=c(7,4,2,1))
barplot(table(dat$lang_cat), exclude = NULL, main="Sprachen gemäss textcat", las=2)

Die Auswertung zeigt, dass die meisten “Unkown” nun als deutsch sprachige Rezessionen bestimmt worden sind. die Tabelle unten zeigt nochmal die genauen Zahlen auf.

table(dat$language, dat$lang_cat)
##          
##           catalan english french german italian portuguese
##   de            0       0      0     77       0          0
##   en            0       9      0     10       0          0
##   fr            0       0      3      0       0          0
##   it            1       0      0      0       5          0
##   pt            0       0      0      0       0          1
##   unknown       0       1      0     32       1          0

Die Rezessionen werden für den weiteren Verlauf auf deutsch verwerdent. Unten ist noch ein Beispiel von einer italienischen Rezession die auf deutsch übersetzt wurde.

## [1] "Als wir mit der Familie zurückkamen, sahen wir am Dienstag zum Mittagessen einen Hauch von Veränderung, mehr Lächeln, besseren Service und neues Personal sowie einen neuen 5-Sterne-Koch"

Nun wird eine Sentimentanalyse durchgeführt. Für das brauchen wird zuerst eine negative Wortliste und eine positive Wortliste.

neg <- read.table(file="C:/Users/JChar/OneDrive/Documents/zhaw/Sem 8/MQM/SentiWS_v1.8c_Negative.txt", fill=TRUE,
                  encoding = "UTF-8")
## erstes Wort extrahieren
neg[, paste("Form", 0, sep="_")] <- sapply(neg$V1, FUN=function(x)
  unlist(strsplit(x, split = "\\|"))[1])
# maximal Anzahl Worte in Spalte 3
library(stringr)
max_wort <- max(str_count(neg$V3, ","))+1 
library(tidyr)
# Wörter in Spalten aufteilen
neg <- neg |> separate(V3,  into=paste("Form", 1:max_wort, sep="_"),
                       sep=",", fill="right")
# reshape
neg_resh <- reshape(neg, idvar = "V1", varying = list(paste(
  "Form", 0:max_wort, sep="_")),
  v.names = "Wort", direction = "long")
# leere Werte löschen
neg_resh <- neg_resh[!(is.na(neg_resh$Wort) | neg_resh$Wort==""),
                     c("Wort", "V2")]
# Wörter sortieren
neg_resh <- neg_resh[order(neg_resh$Wort),]
neg_resh$Wort <- gsub("ß", "ss", neg_resh$Wort)
rownames(neg_resh) <- NULL
colnames(neg_resh)[2] <- "Wert"

pos <- read.table(file="C:/Users/JChar/OneDrive/Documents/zhaw/Sem 8/MQM/SentiWS_v1.8c_Positive.txt", fill=TRUE,
                  encoding = "UTF-8")
pos[, paste("Form", 0, sep="_")] <- sapply(pos$V1, FUN=function(x)
  unlist(strsplit(x, split = "\\|"))[1])
max_wort <- max(str_count(pos$V3, ","))+1 
pos <- pos |> separate(V3,  into=paste("Form", 1:max_wort, sep="_"),
                       sep=",", fill="right")
pos_resh <- reshape(pos, idvar = "V1", varying = list(paste(
  "Form", 0:max_wort, sep="_")),
        v.names = "Wort", direction = "long")
pos_resh <- pos_resh[!(is.na(pos_resh$Wort) | pos_resh$Wort==""), c("Wort", "V2")]
pos_resh <- pos_resh[order(pos_resh$Wort),]
pos_resh$Wort <- gsub("ß", "ss", pos_resh$Wort)
rownames(pos_resh) <- NULL
colnames(pos_resh)[2] <- "Wert"

woerterbuch <- rbind(neg_resh, pos_resh)
# negativste Worte
head(woerterbuch [order(woerterbuch $Wert),], n=10)
##             Wort    Wert
## 4825      Gefahr -1.0000
## 4841    Gefahren -1.0000
## 9715      Schuld -0.9686
## 9718    Schulden -0.9686
## 12523    unnötig -0.9463
## 12524   unnötige -0.9463
## 12525  unnötigem -0.9463
## 12526  unnötigen -0.9463
## 12527  unnötiger -0.9463
## 12528 unnötigere -0.9463
# positivste Worte
head(woerterbuch [order(woerterbuch $Wert, decreasing = TRUE),], n=10)
##               Wort Wert
## 20907     gelungen    1
## 20908    gelungene    1
## 20909   gelungenem    1
## 20910   gelungenen    1
## 20911   gelungener    1
## 20912  gelungenere    1
## 20913 gelungenerem    1
## 20914 gelungeneren    1
## 20915 gelungenerer    1
## 20916 gelungeneres    1
woerterbuch <- aggregate(Wert~Wort, data=woerterbuch, FUN="mean")

Die Negativen werden mit den Positiven zu einem Wörterbuch zusammen getragen.

library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ forcats   1.0.0     ✔ readr     2.1.4
## ✔ lubridate 1.9.2     ✔ tibble    3.2.1
## ✔ purrr     1.0.1     
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag()    masks stats::lag()
## ℹ Use the ]8;;http://conflicted.r-lib.org/conflicted package]8;; to force all conflicts to become errors

Nun werden die Wörter noch zusammen geführt, die die selbe Bedeutung haben. Anschliessen werden die Rezessionen analysiert, um zu schauen, wie hoch der Sentimentwert über die Rezessionen ist.

library(tidytext)
dat$review_de <- gsub("ß", "ss", dat$review_de)
dat$review_de <- gsub("à", "a", dat$review_de)
DTM <- dat |>
  unnest_tokens(output = word, input = "review_de", to_lower = FALSE) |>
  inner_join(y=woerterbuch, by=c("word" = "Wort")) 
Sentiment <- aggregate(Wert~X, data=DTM, FUN=sum)
hist(Sentiment$Wert)

summary(Sentiment$Wert)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
## -1.5024  0.3743  0.9138  0.9879  1.3821  5.8514

Der Median liegt bei 0.91. Somit kann man sagen, dass das Sentiment positiv bei Santa Lucia ist. Es ist wichtig zu beachten, dass die Sentimentanalyse auf einer lexikalischen Ebene stattfindet und möglicherweise nicht alle Feinheiten der Bewertungen erfasst.

dat <- merge(dat, Sentiment, by="X", all.x=TRUE)
# Review ohne Wertung auf neutral stellen
dat$Wert[is.na(dat$Wert)] <- 0 
library(ggplot2)
ggplot(dat, aes(x=Wert, fill=as.factor(rating))) +
  geom_density(alpha=.4)

Man sieht hier dass das Maximun der Werte aller ratings zwischen 0 und 1 sind. Das 2 Sterne Rating hat aber noch ein lokales maxim bei weniger als -1. Es gaht wahrscheinlich Besucher, die sich bei ihrer Kritik positiv oder ironisch ausgedrückt haben, weshalb das Maximum trotzdem zwischen 0 und 1 liegt.

Als nächstes werden die Bewertung pro Jahr zusammen getragen und der durchschnittswert berechnet über die Zeit.

library(ggplot2)
library(dplyr)
library(lubridate)

# Das Datumsformat in R konvertieren
dat$date <- as.Date(dat$date, format = "%d. %B %Y")

# Die Bewertungen nach Jahr und Rating gruppieren und den Durchschnitt berechnen
avg_ratings <- dat %>%
  mutate(year = year(date)) %>%
  group_by(year) %>%
  summarise(avg_rating = mean(rating))

# Das Liniendiagramm des Durchschnitts der Bewertungen über den Zeitverlauf in Jahren erstellen
ggplot(avg_ratings, aes(x = year, y = avg_rating)) +
  geom_line() +
  labs(x = "Jahr", y = "Durchschnittliche Bewertung") +
  theme_minimal() +
  scale_x_continuous(breaks = avg_ratings$year, labels = as.character(avg_ratings$year))

Man kann schön erkennen das sich der Durchschnitt der Bewertungen seit 2016 in einem positiven Trend befindet. Die Bewertung von Santa Lucia war auch noch in keinem Jahr negativ sind nur im oberen neutralen Bereich.

Im Code unten wird der Datensatz in Trainings- und Testdaten mit einer Aufteilung von 70% für das Training und 30% für das Testen aufgeteilt. Durch diese Analyse können die häufigsten Wörter im Text der Bewertungen identifiziert werden. Dies kann helfen, wichtige Themen, Trends oder Schlagwörter im Zusammenhang mit dem Restaurant zu erkennen.

dat$binaer <- factor(dat$rating>=40, labels=c("neg", "pos"))
# install.packages("caret")
# install.packages("SnowballC")
# install.packages("stopwords")
library(caret)
## Lade nötiges Paket: lattice
## 
## Attache Paket: 'caret'
## Das folgende Objekt ist maskiert 'package:purrr':
## 
##     lift
library(SnowballC)
library(stopwords)
table(dat$binaer)
## 
## neg pos 
##  32 108
set.seed(17)
train_index <- createDataPartition(dat$binaer, p = 0.7, list = FALSE)
train_set <- dat[train_index, ]
test_set <- dat[-train_index, ]
word_list_count <- train_set |> 
  unnest_tokens(word, "review_de", strip_numeric = TRUE) |>
  filter(!(word %in% c(stopwords(source = "snowball", language = "de")))) |>
  mutate(stem = wordStem(word, language ="de")) |>
  count(stem) |>
  filter(n >= 10)

head(word_list_count[order(word_list_count$n, decreasing = TRUE),], n=20)
##          stem  n
## 17        gut 80
## 31      pizza 61
## 10        ess 44
## 41     servic 42
## 12 freundlich 39
## 25      lucia 35
## 38      santa 34
## 36 restaurant 32
## 18        imm 29
## 40      schon 27
## 5      bedien 23
## 39    schnell 22
## 34      preis 21
## 4     bahnhof 18
## 19    italien 18
## 2     ambient 17
## 46 winterthur 17
## 30       pizz 16
## 23       leck 15
## 24      lokal 15

In den Bewertungen gehen die meisten auf die Pizza ein, was auch das Hauptgeschäfft von Santa Lucia ausmacht. Somit könnte es für das Restaurant hilfreich sein, zu wissen was die Kunden über Ihre Pizza denken, um sich darin zu verbessern. Der Service ist auch sehr häufig erwähnt. Nun kann man mit einer genaueren Analyse herausfinden welche Begriffe in postiven oder in negativen Ratings erwähnt wurden.

library(dplyr)
library(tidytext)

# Laden Sie eine Liste von Stop-Wörtern
stop_words <- tidytext::stop_words

# Benutzerdefinierte Liste von Wörtern, die nicht relevant sind
custom_stop_words <- c("sehr", "die", "der", "ist", "das", "war", "wir", "mit", "auch")

# Extrahieren Sie positive Wörter
positive_words <- dat %>%
  filter(binaer == "pos") %>%
  select(review_de) %>%
  unnest_tokens(output = "word", input = "review_de") %>%
  anti_join(stop_words) %>%
  filter(!word %in% custom_stop_words) %>%
  count(word, sort = TRUE)
## Joining with `by = join_by(word)`
# Extrahieren Sie negative Wörter
negative_words <- dat %>%
  filter(binaer == "neg") %>%
  select(review_de) %>%
  unnest_tokens(output = "word", input = "review_de") %>%
  anti_join(stop_words) %>%
  filter(!word %in% custom_stop_words) %>%
  count(word, sort = TRUE)
## Joining with `by = join_by(word)`
# Top 10 der positiven Wörter
top_positive_words <- head(positive_words, 10)

# Top 10 der negativen Wörter
top_negative_words <- head(negative_words, 10)

# Ergebnisse anzeigen
top_positive_words
##       word   n
## 1      und 200
## 2      ein  50
## 3    essen  50
## 4    pizza  48
## 5      für  47
## 6      gut  47
## 7      ich  47
## 8  service  45
## 9       es  44
## 10      zu  33
top_negative_words
##     word  n
## 1    und 85
## 2  nicht 49
## 3   aber 36
## 4  pizza 31
## 5     es 27
## 6     zu 27
## 7    ich 24
## 8   eine 23
## 9    gut 21
## 10 waren 19

Wenn man sich anschaut, welche Worter am meisten bei positiven Bewertungen vorkommen, dann findet man mit 50 Erwähnungen das Essen. Mit 48 Mal die Pizza und mit 45 der Service. Bei den negativen Bewertungen taucht ebenfalls die Pizza auf, aber nur mit 31 Erwähnungen. Somit gibt es Lob aber auch Kritik an der man als Restaurant wachsen kann.

Die Anwendung eines Bag-of-Words-Ansatzes auf die Bewertungen kann nützlich sein, um wichtige Informationen aus den Rezensionen zu extrahieren und Textdaten in eine für die Analyse geeignete Form zu bringen.

## Bag-of-Word
# install.packages("MLmetrics")
library(MLmetrics)
## 
## Attache Paket: 'MLmetrics'
## Die folgenden Objekte sind maskiert von 'package:caret':
## 
##     MAE, RMSE
## Das folgende Objekt ist maskiert 'package:base':
## 
##     Recall
word_list <- train_set |> 
  unnest_tokens(word, "review_de", strip_numeric = TRUE) |>
  filter(!(word %in% c(stopwords(source = "snowball", language = "de")))) |>
  mutate(stem = wordStem(word, language ="de")) |>
  count(stem) |>
  filter(n >= 10) |>
  pull(stem)

bow_features <- train_set |>
  unnest_tokens(word, "review_de") |>
  mutate(stem = wordStem(word, language ="de")) |>   
  filter(stem %in% word_list) |>     
  count(X, stem) |>                 
  spread(stem, n) |>                 
  map_df(replace_na, 0)

fitControl <- trainControl(method = "cv", classProbs = TRUE,
                           summaryFunction = multiClassSummary, number=5)

res_svm_bow <- train(y=train_set$binaer, x=as.data.frame(bow_features[, -1]), 
                     method="svmLinear", 
                  trControl = fitControl, 
                  tuneGrid=data.frame(C=c(0.1, 0.5, 1, 10)), 
                  metric="AUC")
res_svm_bow
## Support Vector Machines with Linear Kernel 
## 
## 99 samples
## 47 predictors
##  2 classes: 'neg', 'pos' 
## 
## No pre-processing
## Resampling: Cross-Validated (5 fold) 
## Summary of sample sizes: 80, 79, 78, 80, 79 
## Resampling results across tuning parameters:
## 
##   C     logLoss    AUC        prAUC      Accuracy   Kappa       F1       
##    0.1  0.4689014  0.7881667  0.6501451  0.8197494  0.29141450  0.5460317
##    0.5  0.4901395  0.7598333  0.6128436  0.7982206  0.22358134  0.3500000
##    1.0  0.5042114  0.7593333  0.6115671  0.7886967  0.14995830  0.4523810
##   10.0  0.5021604  0.7658333  0.6097423  0.7781704  0.07692308  0.5000000
##   Sensitivity  Specificity  Pos_Pred_Value  Neg_Pred_Value  Precision  Recall
##   0.23         1.0000000    1.0000000       0.8121849       1.0000000  0.23  
##   0.18         0.9866667    0.8750000       0.7992673       0.8750000  0.18  
##   0.14         0.9866667    0.8333333       0.7931704       0.8333333  0.14  
##   0.08         0.9866667    0.6666667       0.7828763       0.6666667  0.08  
##   Detection_Rate  Balanced_Accuracy
##   0.05157895      0.6150000        
##   0.04057644      0.5833333        
##   0.03105263      0.5633333        
##   0.02000000      0.5333333        
## 
## AUC was used to select the optimal model using the largest value.
## The final value used for the model was C = 0.1.

AUC wurde verwendet, um das optimale Modell zu wählen. Das Modell mit dem größten AUC-Wert wurde ausgewählt. In diesem Fall betrug der optimale Wert für den Hyperparameter C 0.1. Der AUC-Wert von 0.788 deutet darauf hin, dass das Modell eine gute Trennfähigkeit zwischen den Klassen hat. Die Accuracy von 0.820 zeigt eine hohe Genauigkeit des Modells.

Als letzte Analyse wollen wir auf die Themen in den Rezessionen eingehen und heraus finden, über welche Themen geschrieben wurde. dafür werden wir eine Textmodelierung machen

# install.packages("topicmodels")
library(tm)
## Lade nötiges Paket: NLP
## 
## Attache Paket: 'NLP'
## Das folgende Objekt ist maskiert 'package:ggplot2':
## 
##     annotate
## 
## Attache Paket: 'tm'
## Das folgende Objekt ist maskiert 'package:stopwords':
## 
##     stopwords
library(topicmodels)

set.seed(42)  # Seed für die Reproduzierbarkeit festlegen

# Vorbereitung der Textdaten für die Themenmodellierung
corpus <- Corpus(VectorSource(train_set$review_de))
corpus <- tm_map(corpus, content_transformer(tolower))
corpus <- tm_map(corpus, removeNumbers)
corpus <- tm_map(corpus, removePunctuation)
corpus <- tm_map(corpus, removeWords, stopwords("de"))
corpus <- tm_map(corpus, stripWhitespace)

# Erstellen einer Document-Term-Matrix
dtm <- DocumentTermMatrix(corpus)

# Erstellen des LDA-Modells
lda_model <- LDA(dtm, k = 3, control = list(seed = 42))  # Anzahl der gewünschten Themen

# Anzeigen der Themen und ihrer Top-Wörter
topics <- terms(lda_model, 7)  # Anzahl der Top-Wörter pro Thema
topics
##      Topic 1      Topic 2   Topic 3     
## [1,] "essen"      "pizza"   "pizza"     
## [2,] "santa"      "gut"     "service"   
## [3,] "restaurant" "essen"   "gut"       
## [4,] "lucia"      "immer"   "essen"     
## [5,] "pizza"      "lucia"   "restaurant"
## [6,] "pizzeria"   "santa"   "mal"       
## [7,] "lokal"      "service" "empfehlen"

Thema 1: Es scheint sich um die allgemeine Erfahrung mit dem Essen im Restaurant zu drehen. Thema 2: Hier könnten die Bewertungen und Meinungen der Kunden über die Qualität der Pizza und den Service des Restaurants im Vordergrund stehen. Thema 3: Es könnte sich um Kundenmeinungen handeln, welche allgemeine Empfehlung des Restaurants aussprechen.

Die Analyse der Bewertungen des Restaurants Santa Lucia lieferte wertvolle Einblicke in Kundenmeinungen. Positive Bewertungen überwogen. Sentiment- und Themenanalyse identifizierten wichtige Wörter und Themen. Diese Informationen können dem Restaurant helfen, seine Stärken zu betonen und die Kundenzufriedenheit zu verbessern.