Chapter 9 Travel and Accomodation
9.1 Topic Modelling and Sentiment Analysis on Hotel Reviews Data in Europe
9.1.1 Background
Benua Eropa merupakan benua yang menjadi destinasi impian bagi para wisatawan di berbagai belahan dunia. Hal ini karena Benua Eropa memiliki banyak situs wisata yang menarik perhatian akan keunikan dan keindahannya. Tentunya para wisatawan membutuhkan tempat untuk singgah sementara selama menikmati liburan. Untuk meningkatkan kepuasan para wisatawan, dibutuhkan pemilihan hotel yang tepat agar dapat memberikan wisatawan pengalaman beristirahat yang terbaik. Untuk itu, melihat seberapa baik tingkat pelayanan hotel secara teliti tentunya diperlukan. Tingkat pelayanan hotel dapat dilihat salah-satunya melalui rating dari hotel tersebut. Meskipun demikian, rating dari hotel tidak sepenuhnya merepresentasikan tingkat kelayakan dari hotel tersebut, terutama untuk fasilitas-fasilitasnya secara spesifik. Maka dari itu, perlu dilakukan analisis sentimen berdasarkan ulasan dari wisatawan lain yang pernah singgah di hotel tersebut. Sentimen analisis diperlukan untuk memberikan penilaian terhadap fasilitas-fasilitas dan pelayanan hotel yang disediakan karena sering kali kita tidak ingin membaca setiap ulasan hotel-hotel tersebut satu per satu. Harapannya, dengan analisis ini, hasil penilaian dari setiap aspek pelayanan dari hotel-hotel ini dapat dijadikan pertimbangan dalam memilih hotel dengan pengalaman menginap yang terbaik.
9.1.2 Data Preparation
Dataset yang digunakan adalah dataset yang bersumber dari kaggle (https://www.kaggle.com/jiashenliu/515k-hotel-reviews-data-in-europe) yang memuat 515 ribu ulasan dari hotel-hotel yang berada di ibukota negara-negara di Benua Eropa.
import sys
print(sys.executable)
#> /Users/ariqleesta/opt/anaconda3/envs/ariq/bin/python
# Environment
import os
# Data Manipulation Tools
import pandas as pd
import numpy as np
from fuzzymatcher import link_table, fuzzy_left_join
import difflib
import re # for regular expressions
= 1000
pd.options.display.max_rows = 100
pd.options.display.max_columns
# Data Visualization Tools
import matplotlib.pyplot as plt
import seaborn as sns
#%matplotlib inline
'bmh')
plt.style.use(
# libraries for displaying images
from IPython.display import HTML, Image
from IPython.core.display import HTML
from IPython.display import display
import selenium
# NLP Module
import nltk # Natural language processing toolkit
from nltk import FreqDist # Frequency distribution
from nltk.corpus import stopwords
from wordcloud import WordCloud,STOPWORDS
from textblob import TextBlob
from nltk.stem import WordNetLemmatizer
from nltk import sent_tokenize
from math import pi
import pickle
import pyLDAvis
# Network
#> /Library/Frameworks/R.framework/Versions/4.1/Resources/library/reticulate/python/rpytools/loader.py:39: DeprecationWarning: the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses
#> module = _import(
from nltk import bigrams, ngrams
import networkx as nx
import itertools
import collections
# Location Modules
import folium
# Ignore Warnings
import warnings
"ignore")
warnings.filterwarnings("ignore", category= DeprecationWarning) warnings.filterwarnings(
= pd.read_csv('https://archive.org/download/hotel-reviews/Hotel_Reviews.csv')
df print(df.shape)
#> (515738, 17)
2) df.head(
#> Hotel_Address \
#> 0 s Gravesandestraat 55 Oost 1092 AA Amsterdam ...
#> 1 s Gravesandestraat 55 Oost 1092 AA Amsterdam ...
#>
#> Additional_Number_of_Scoring Review_Date Average_Score Hotel_Name \
#> 0 194 8/3/2017 7.7 Hotel Arena
#> 1 194 8/3/2017 7.7 Hotel Arena
#>
#> Reviewer_Nationality Negative_Review \
#> 0 Russia I am so angry that i made this post available...
#> 1 Ireland No Negative
#>
#> Review_Total_Negative_Word_Counts Total_Number_of_Reviews \
#> 0 397 1403
#> 1 0 1403
#>
#> Positive_Review \
#> 0 Only the park outside of the hotel was beauti...
#> 1 No real complaints the hotel was great great ...
#>
#> Review_Total_Positive_Word_Counts \
#> 0 11
#> 1 105
#>
#> Total_Number_of_Reviews_Reviewer_Has_Given Reviewer_Score \
#> 0 7 2.9
#> 1 7 7.5
#>
#> Tags days_since_review \
#> 0 [' Leisure trip ', ' Couple ', ' Duplex Double... 0 days
#> 1 [' Leisure trip ', ' Couple ', ' Duplex Double... 0 days
#>
#> lat lng
#> 0 52.360576 4.915968
#> 1 52.360576 4.915968
len(df['Hotel_Address'].unique())
#> 1493
Dataset ini memiliki 515738 baris dan 17 kolom dengan jumlah hotel sebanyak 1493. Sebelum memulai analisis lebih lanjut, kita perlu mengetahui tipe data dari setiap kolom dataset tersebut.
df.dtypes
#> Hotel_Address object
#> Additional_Number_of_Scoring int64
#> Review_Date object
#> Average_Score float64
#> Hotel_Name object
#> Reviewer_Nationality object
#> Negative_Review object
#> Review_Total_Negative_Word_Counts int64
#> Total_Number_of_Reviews int64
#> Positive_Review object
#> Review_Total_Positive_Word_Counts int64
#> Total_Number_of_Reviews_Reviewer_Has_Given int64
#> Reviewer_Score float64
#> Tags object
#> days_since_review object
#> lat float64
#> lng float64
#> dtype: object
Kemudian kita perlu meninjau mengenai missing value atau data yang hilang dari dataset tersebut. Pengamatan data yang hilang dapat dilakukan secara visual dengan bantuan library missingno.
import missingno as msno
plt.figure()
= (20,8)) msno.matrix(df, figsize
#> <AxesSubplot:>
plt.show()
sum() df.isnull().
#> Hotel_Address 0
#> Additional_Number_of_Scoring 0
#> Review_Date 0
#> Average_Score 0
#> Hotel_Name 0
#> Reviewer_Nationality 0
#> Negative_Review 0
#> Review_Total_Negative_Word_Counts 0
#> Total_Number_of_Reviews 0
#> Positive_Review 0
#> Review_Total_Positive_Word_Counts 0
#> Total_Number_of_Reviews_Reviewer_Has_Given 0
#> Reviewer_Score 0
#> Tags 0
#> days_since_review 0
#> lat 3268
#> lng 3268
#> dtype: int64
Kolom lat dan lng yang merupakan komponen koordinat dari dataset tidak tersedia untuk beberapa hotel. Jumlah baris yang tidak memiliki data koordinat sangat sedikit jika dibandingkan total data keseluruhan sehingga baris-baris ini dapat dihilangkan karena koordinat diperlukan untuk memberikan informasi lokasi dari hotel tersebut kepada para wisatawan.
# drop null coordinates
= True) df.dropna(inplace
9.1.3 Exploratory Data Analysis
Kata No Negative pada kolom Negative Review dan No Positive pada kolom Positive Review menandakan bahwa review tersebut tidak mengandung ulasan positif atau ulasan negatif. Sehingga kata No Negative dan No Positive dapat diganti dengan empty string saja.
# replace No Negative and No Positive with empty string
'Negative_Review'] = df['Negative_Review'].replace({'No Negative': ''})
df['Positive_Review'] = df['Positive_Review'].replace({'No Positive': ''}) df[
Berikut adalah distribusi dari nilai rating rata-rata yang diberikan oleh pemberi ulasan terkait hotel-hotel pada dataset ini. Average Score memiliki rentang 5.2 untuk nilai terendah dan 9.9 untuk nilai tertinggi.
# Plot average score
= df[["Hotel_Name","Average_Score"]].drop_duplicates()
data_plot = plt.subplots(figsize= (30,7))
fig, ax = ax,x = "Average_Score",data=data_plot) sns.countplot(ax
#> <AxesSubplot:xlabel='Average_Score', ylabel='count'>
'Average Score') ax.set_xlabel(
#> Text(0.5, 0, 'Average Score')
'Score Counts') ax.set_ylabel(
#> Text(0, 0.5, 'Score Counts')
plt.show()
Berikut akan dilakukan analisis teks, tahap-tahap yang akan dilakukan antara lain:
* Mendefinisikan Stopword
Stopword adalah kata-kata yang tidak atau sedikit mengandung makna dari kalimat keseluruhan. Stopword biasanya berupa partikel (eg: a, an, the, is, am, are) atau kata penghubung (eg: and, or, of).
* Membuat semua kata menjadi huruf kecil/lowercase
Untuk mempermudah memproses data tekstual, perlu dilakukan lowercasing karena algoritma dalam text processing membedakan huruf besar dan huruf kecil atau case sensitive.
* Menghilangkan character yang tidak diinginkan
Character yang tidak diinginkan bisa berupa simbol simbol seperti titik, koma, tanda seru, tanda tanya, dan lain-lain.
* Menghilangkan kata-kata yang pendek (kurang dari 4 huruf)
* Lemmatizing
Lemmatizing adalah mengembalikan kosa kata menjadi kata dasar dari kata tersebut. Sebagai contoh,, kata studying dan studied memiliki kata dasar yang sama yaitu study.
* Menghilangkan Stopwords
- Tokenization Tokenization adalah proses memecah kalimat menjadi kumpulan kata-kata. Kumpulan kata-kata dalam satu kalimat dimuat dalam satu buah iterable atau list. Sebagai contoh, untuk kata yang telah di lemmatizing, “Hotel service good” menjadi [“Hotel,” “Service,” “Good”].
# A function to remove stopwords
def remove_stopwords(rev):
= " ".join([i for i in rev if i not in stop_words])
rev_new return rev_new
# A function to count the most frequent words
def freq_words(x, terms = 30):
= ' '.join([text for text in x])
all_words = all_words.split()
all_words
= FreqDist(all_words)
fdist = pd.DataFrame({'word':list(fdist.keys()), 'count':list(fdist.values())})
words_df
# selecting top 20 most frequent words
= words_df.nlargest(columns="count", n = terms)
d 1, figsize=(20,5))
plt.figure(= sns.barplot(data=d, x= "word", y = "count")
ax set(ylabel = 'Count')
ax.= 90)
plt.xticks(rotation
plt.show()
# A function to draw word cloud
def wordcloud_draw(data, color = 'black'):
= ' '.join(data)
words = ' '.join([word for word in words.split()
cleaned_word if 'http' not in word
and not word.startswith('@')
and not word.startswith('#')
and word != 'RT'
])
= WordCloud(collocations=False,
wordcloud =STOPWORDS,
stopwords=color,
background_color=500,
width=100,).generate(cleaned_word)
height1,figsize=(13, 13))
plt.figure(
plt.imshow(wordcloud)'off')
plt.axis(
plt.show()
# Create a function to get the subjectivity
def getSubjectivity(text):
return TextBlob(text).sentiment.subjectivity
# Create a function to get the polarity
def getPolarity(text):
return TextBlob(text).sentiment.polarity
= df[['Hotel_Name', 'Negative_Review', 'Positive_Review']]
df_reviews
df_reviews.head()
# Define stopwords
#> Hotel_Name Negative_Review \
#> 0 Hotel Arena I am so angry that i made this post available...
#> 1 Hotel Arena
#> 2 Hotel Arena Rooms are nice but for elderly a bit difficul...
#> 3 Hotel Arena My room was dirty and I was afraid to walk ba...
#> 4 Hotel Arena You When I booked with your company on line y...
#>
#> Positive_Review
#> 0 Only the park outside of the hotel was beauti...
#> 1 No real complaints the hotel was great great ...
#> 2 Location was good and staff were ok It is cut...
#> 3 Great location in nice surroundings the bar a...
#> 4 Amazing location and building Romantic setting
= stopwords.words('english')
stop_words
# make entire text lowercase
'Positive_Review'] = [r.lower() for r in df_reviews['Positive_Review']]
df_reviews['Negative_Review'] = [r.lower() for r in df_reviews['Negative_Review']]
df_reviews[
# Remove unwanted characters, numbers and symbols
'Positive_Review'] = df_reviews['Positive_Review'].str.replace("[^a-zA-Z#]", " ")
df_reviews['Negative_Review'] = df_reviews['Negative_Review'].str.replace("[^a-zA-Z#]", " ")
df_reviews[
# remove short words (length < 4)
'Positive_Review'] = df_reviews['Positive_Review'].apply(lambda x: ' '.join([w for w in x.split() if len(w)>3]))
df_reviews['Negative_Review'] = df_reviews['Negative_Review'].apply(lambda x: ' '.join([w for w in x.split() if len(w)>3]))
df_reviews[
# Lemmatizing
= WordNetLemmatizer()
lemmatizer 'Positive_Review'] = [' '.join([lemmatizer.lemmatize(word) for word in r.split(' ')]) for r in df_reviews['Positive_Review']]
df_reviews['Negative_Review'] = [' '.join([lemmatizer.lemmatize(word) for word in r.split(' ')]) for r in df_reviews['Negative_Review']]
df_reviews[
# remove stopwords from the text
'Positive_Review'] = [remove_stopwords(r.split()) for r in df_reviews['Positive_Review']]
df_reviews['Negative_Review'] = [remove_stopwords(r.split()) for r in df_reviews['Negative_Review']]
df_reviews[
# Tokenization
'Positive_Review'] = [r.split() for r in df_reviews['Positive_Review']]
df_reviews['Negative_Review'] = [r.split() for r in df_reviews['Negative_Review']] df_reviews[
def flatten(data):
= []
l for i in data:
for word in i:
l.append(word)return l
= flatten(df_reviews['Positive_Review'].tolist())
pos = flatten(df_reviews['Negative_Review'].tolist()) neg
Berikut adalah grafik frekuensi tertinggi untuk kata-kata yang berada pada kolom Positive Review dan Negative Review. Pada kolom Positive Review, kata yang paling banyak diucapkan adalah staff, location, dan room, sedangkan Pada kolom Negative Review, kata yang paling banyak diucapkan adalah room, hotel, dan breakfast.
30)# Frequency distribution of common words in positive reviews freq_words(pos,
30) # Checking frequency of most used words in negative reviews freq_words(neg,
Kita dapat melihat kata-kata yang sering muncul dengan bantuan visualisasi wordcloud. Kata yang lebih sering muncul akan berukuran lebih besar daripada yang lain.
# Using wordcloud to visually represent the text data
print("Positive reviews")
#> Positive reviews
'white') wordcloud_draw(pos,
# Using wordcloud to visually represent the text data
print("Negative reviews")
#> Negative reviews
wordcloud_draw(neg)
### Modelling
Dalam menyarankan para wisatawan mengenai hotel dengan pelayanan terbaik, kita perlu meninjau terlebih dahulu aspek-aspek apa yang relevan dalam ulasan-ulasan tersebut sebelum bisa dilanjutkan ke penilaian. Aspek-aspek ini dapat berupa fasilitas-fasilitas atau pelayanan yang diberikan oleh hotel tersebut tergantung seberapa banyak dan relevan hal tersebut dibicarakan di dalam ulasan. Sebagai contoh, anggaplah kita tidak mengetahui fasilitas-fasilitas apa yang diberikan hotel. Dari review tersebut ada yang membicarakan mengenai kamar, kolam renang, kafe, rollercoaster bahkan kebun binatang. Bagaimana kita tahu bahwa topik yang relevan adalah kamar, kolam renang, dan kafe? Bisa saja suatu hotel memiliki rollercoaster atau berada di dekat kebun binatang. Untuk menentukan topik-topik yang relevan dalam penilaian, dibutuhkan suatu proses bernama Topic Modelling. Topic Modelling menilai seberapa relevan suatu kata terhadap kalimat yang membawa pengaruh terbesar dalam pemaknaan suatu kalimat. Salah satu metode dalam Topic Modelling dan yang akan digunakan dalam analisis ini adalah metode Latent Dirichlet Allocation (LDA). LDA memodelkan topik berdasarkan probabilitas dengan mengasumsikan setiap topik merupakan gabungan dari beberapa pasangan kata, dan setiap dokumen merupakan gabungan dari beberapa topik.
Untuk melakukan Topic Modelling dengan metode LDA dapat dilakukan dengan bantuan suatu library bernama gensim.
import gensim
from gensim.utils import simple_preprocess
= gensim.models.ldamodel.LdaModel LDA
import gensim.corpora as corpora
# Create Dictionary
= corpora.Dictionary(df_reviews['Positive_Review'])
id2word_pos = corpora.Dictionary(df_reviews['Negative_Review'])
id2word_neg # Create Corpus
= df_reviews['Positive_Review']
texts_pos = df_reviews['Negative_Review']
texts_neg # Term Document Frequency
= [id2word_pos.doc2bow(text) for text in texts_pos]
corpus_pos = [id2word_neg.doc2bow(text) for text in texts_neg] corpus_neg
from pprint import pprint
# number of topics
= 10
num_topics # Build LDA model
= gensim.models.LdaMulticore(corpus=corpus_pos,
lda_model_pos =id2word_pos,
id2word=num_topics)
num_topics
= gensim.models.LdaMulticore(corpus=corpus_neg,
lda_model_neg =id2word_neg,
id2word=num_topics) num_topics
Berikut adalah hasil pemilihan 10 kumpulan topik untuk ulasan positif
pprint(lda_model_pos.print_topics())
#> [(0,
#> '0.067*"hotel" + 0.035*"staff" + 0.025*"excellent" + 0.023*"room" + '
#> '0.022*"location" + 0.018*"comfortable" + 0.015*"best" + 0.013*"helpful" + '
#> '0.012*"recommend" + 0.012*"barcelona"'),
#> (1,
#> '0.057*"room" + 0.032*"view" + 0.029*"staff" + 0.025*"hotel" + '
#> '0.023*"amazing" + 0.019*"great" + 0.019*"lovely" + 0.011*"beautiful" + '
#> '0.010*"birthday" + 0.010*"friendly"'),
#> (2,
#> '0.057*"room" + 0.023*"nice" + 0.021*"hotel" + 0.015*"staff" + 0.014*"check" '
#> '+ 0.012*"coffee" + 0.012*"free" + 0.009*"good" + 0.008*"small" + '
#> '0.008*"night"'),
#> (3,
#> '0.043*"hotel" + 0.033*"staff" + 0.028*"stay" + 0.028*"service" + '
#> '0.016*"everything" + 0.016*"time" + 0.012*"every" + 0.012*"excellent" + '
#> '0.010*"best" + 0.009*"thing"'),
#> (4,
#> '0.172*"staff" + 0.106*"friendly" + 0.094*"helpful" + 0.069*"location" + '
#> '0.038*"great" + 0.030*"room" + 0.025*"hotel" + 0.016*"clean" + 0.014*"nice" '
#> '+ 0.014*"excellent"'),
#> (5,
#> '0.041*"well" + 0.034*"staff" + 0.031*"hotel" + 0.028*"comfy" + 0.025*"room" '
#> '+ 0.023*"really" + 0.020*"helpful" + 0.016*"great" + 0.013*"like" + '
#> '0.013*"bed"'),
#> (6,
#> '0.107*"breakfast" + 0.087*"good" + 0.052*"location" + 0.052*"staff" + '
#> '0.042*"excellent" + 0.027*"great" + 0.026*"room" + 0.025*"hotel" + '
#> '0.021*"friendly" + 0.020*"value"'),
#> (7,
#> '0.116*"room" + 0.061*"clean" + 0.060*"location" + 0.056*"comfortable" + '
#> '0.054*"great" + 0.042*"nice" + 0.041*"good" + 0.035*"staff" + '
#> '0.023*"excellent" + 0.022*"everything"'),
#> (8,
#> '0.078*"hotel" + 0.029*"room" + 0.029*"nice" + 0.022*"design" + '
#> '0.016*"clean" + 0.014*"staff" + 0.013*"street" + 0.013*"modern" + '
#> '0.012*"nothing" + 0.011*"liked"'),
#> (9,
#> '0.092*"location" + 0.040*"station" + 0.037*"good" + 0.034*"close" + '
#> '0.026*"great" + 0.026*"metro" + 0.022*"hotel" + 0.019*"walk" + 0.016*"city" '
#> '+ 0.016*"near"')]
Berikut adalah hasil pemilihan 10 kumpulan topik untuk ulasan negatif
pprint(lda_model_neg.print_topics())
#> [(0,
#> '0.041*"room" + 0.026*"pool" + 0.025*"parking" + 0.023*"window" + '
#> '0.023*"hotel" + 0.022*"expensive" + 0.021*"noise" + 0.019*"price" + '
#> '0.019*"night" + 0.014*"open"'),
#> (1,
#> '0.036*"room" + 0.030*"bathroom" + 0.030*"coffee" + 0.020*"breakfast" + '
#> '0.017*"could" + 0.017*"door" + 0.014*"would" + 0.014*"shower" + '
#> '0.014*"food" + 0.013*"facility"'),
#> (2,
#> '0.075*"room" + 0.035*"hotel" + 0.011*"night" + 0.010*"could" + '
#> '0.009*"booked" + 0.008*"double" + 0.007*"would" + 0.007*"star" + '
#> '0.007*"staff" + 0.007*"time"'),
#> (3,
#> '0.053*"room" + 0.038*"shower" + 0.026*"need" + 0.021*"floor" + '
#> '0.020*"water" + 0.019*"hotel" + 0.018*"bathroom" + 0.017*"lift" + '
#> '0.015*"good" + 0.012*"clean"'),
#> (4,
#> '0.144*"room" + 0.096*"small" + 0.083*"breakfast" + 0.018*"expensive" + '
#> '0.015*"little" + 0.014*"view" + 0.014*"size" + 0.011*"price" + '
#> '0.011*"bathroom" + 0.010*"included"'),
#> (5,
#> '0.032*"room" + 0.028*"hotel" + 0.021*"little" + 0.020*"location" + '
#> '0.018*"could" + 0.016*"area" + 0.013*"pillow" + 0.011*"breakfast" + '
#> '0.011*"city" + 0.011*"small"'),
#> (6,
#> '0.034*"staff" + 0.032*"hotel" + 0.014*"breakfast" + 0.012*"reception" + '
#> '0.011*"room" + 0.011*"would" + 0.011*"service" + 0.010*"check" + '
#> '0.009*"time" + 0.008*"booking"'),
#> (7,
#> '0.079*"room" + 0.034*"wifi" + 0.016*"time" + 0.012*"free" + 0.012*"check" + '
#> '0.011*"water" + 0.010*"work" + 0.010*"even" + 0.008*"slow" + 0.008*"cold"'),
#> (8,
#> '0.274*"nothing" + 0.058*"everything" + 0.034*"like" + 0.027*"really" + '
#> '0.021*"perfect" + 0.020*"great" + 0.019*"thing" + 0.016*"hotel" + '
#> '0.016*"anything" + 0.014*"good"'),
#> (9,
#> '0.074*"room" + 0.027*"night" + 0.025*"noisy" + 0.017*"service" + '
#> '0.015*"floor" + 0.014*"hotel" + 0.012*"morning" + 0.012*"door" + '
#> '0.010*"work" + 0.009*"noise"')]
Berikut adalah potongan kode untuk melakukan visualisasi pemilihan topik dengan model LDA.
# Visualize the topics
def ldaviz(lda_model, corpus, id2word, num_topics, filename):
pyLDAvis.enable_notebook()= os.path.join(os.getcwd()+str(num_topics))
LDAvis_data_filepath # # this is a bit time consuming - make the if statement True
# # if you want to execute visualization prep yourself
if True:
= pyLDAvis.gensim.prepare(lda_model, corpus, id2word)
LDAvis_prepared with open(LDAvis_data_filepath, 'wb') as f:
pickle.dump(LDAvis_prepared, f)# load the pre-prepared pyLDAvis data from disk
with open(LDAvis_data_filepath, 'rb') as f:
= pickle.load(f)
LDAvis_prepared return pyLDAvis.save_html(LDAvis_prepared, os.getcwd()+ '/' + filename +'.html')
#return LDAvis_prepared
# Visualization for Positive Reviews
# ldaviz(lda_model_pos, corpus_pos, id2word_pos, num_topics, 'ldavizpos')
# Visualization for Negative Reviews
# daviz(lda_model_neg, corpus_neg, id2word_neg, num_topics, 'ldavizneg')
Topik-topik tersebut di extract dan dihitung skor kumulatif untuk masing-masing topik.
# Extract all topics from negative and positive
= []
topics_pos = []
topics_neg
= []
scores_pos = []
scores_neg
for pos, neg in zip(lda_model_pos.print_topics(), lda_model_neg.print_topics()):
= re.findall("(?<=\")[a-z]+(?=\")",pos[1])
topic_pos = topics_pos + topic_pos
topics_pos
= re.findall("\d\.\d+",pos[1])
score_pos = scores_pos + score_pos
scores_pos
= re.findall("(?<=\")[a-z]+(?=\")",neg[1])
topic_neg = topics_neg + topic_neg
topics_neg
= re.findall("\d\.\d+",pos[1])
score_neg = scores_neg + score_neg
scores_neg
= pd.DataFrame({'Topics':topics_pos, 'Score':scores_pos})
positive_topics 'Score'] = positive_topics['Score'].astype(float)
positive_topics[= positive_topics.groupby('Topics').sum().reset_index()\
positive_topics 'Score',ascending = False)
.sort_values(
= pd.DataFrame({'Topics':topics_neg, 'Score':scores_neg})
negative_topics 'Score'] = negative_topics['Score'].astype(float)
negative_topics[= negative_topics.groupby('Topics').sum().reset_index()\
negative_topics 'Score',ascending = False) .sort_values(
Maka diperoleh kumpulan topik beserta score seberapa relevan suatu topik terhadap ulasan yang diberikan yang ditabulasikan sebagai berikut.
#inner join to see topics that belongs to each other
'inner', 'Topics') pd.merge(positive_topics, negative_topics,
#> Topics Score_x Score_y
#> 0 staff 0.419 0.115
#> 1 room 0.363 0.687
#> 2 hotel 0.337 0.221
#> 3 location 0.295 0.028
#> 4 great 0.180 0.014
#> 5 good 0.174 0.021
#> 6 breakfast 0.107 0.187
#> 7 clean 0.093 0.009
#> 8 everything 0.038 0.029
#> 9 view 0.032 0.030
#> 10 service 0.028 0.060
#> 11 really 0.023 0.022
#> 12 city 0.016 0.013
#> 13 time 0.016 0.089
#> 14 check 0.014 0.079
#> 15 like 0.013 0.029
#> 16 coffee 0.012 0.029
#> 17 nothing 0.012 0.078
#> 18 free 0.012 0.056
#> 19 thing 0.009 0.013
#> 20 small 0.008 0.119
#> 21 night 0.008 0.073
Dalam hal ini, kita dapat memilih secara manual atau memilih dengan mengambil topik dengan skor yang paling besar. Tahap ini disebut Topic Labelling. Namun dalam hal ini kita cukup memilih topik secara manual dengan mengandalkan pengetahuan kita mengenai fasilitas apa yang sering atau selalu disediakan oleh pihak hotel.
# Topic labelling
= ['staff', 'room', 'breakfast', 'service', 'view', 'restaurant', 'bathroom', 'pool'] aspects
Untuk memperoleh hasil yang merepresentasikan tiap hotel, alangkah lebih semua review dijadikan menjadi satu dokumen untuk tiap hotel. Sehingga kita dapat memperoleh satu buah dokumen penuh berisi review untuk tiap-tiap hotel.
= df_reviews.groupby('Hotel_Name')['Negative_Review','Positive_Review'].sum() df_reviews
Dari aspek-aspek ini, kita akan menentukan word correlation dari aspek yang kita inginkan, yaitu mencari kata sebelum dan sesudah dari topik yang diinginkan. Hal ini bertujuan untuk melihat hubungan dari aspek (fasilitas) yang diberikan hotel terhadap kata yang diberikan oleh wisatawan melalui ulasannnya. Sebagai contoh, kita ingin melihat ruangan, namun kita perlu tahu ruangan itu seperti apa menurut para wisatawan. Bisa jadi ruangan itu baik, buruk, sempit, luas, mengecewakan, dan lain-lain. Metode ini bernama ngram yaitu memasangkan kata dengan kata disebelahnya (baik sebelum atau sesudahnya).
Kita ambil contoh untuk satu dokumen (satu buah hotel). Misalkan aspek yang ingin dilihat adalah room dengan jumlah kata dalam satu pasangan n adalah 2.
# let's take a look at example
# if we want to extract only room
= 2
n = df_reviews['Negative_Review'][2]
example = [list(ngrams(w,n)) for w in [example]]
terms_ngram = [i for i in terms_ngram[0] if 'room' in i]
room_ngram 5] room_ngram[:
#> [('disappointing', 'room'), ('room', 'really'), ('time', 'room'), ('room', 'furniture'), ('special', 'room')]
Untuk mendapatkan sebagian dokumen yang hanya membicarakan room, kita perlu menggabungkan semua ngram menjadi satu dokumen dengan metode chain.
# Flatten list of room ngrams in clean list
= list(itertools.chain(*room_ngram))
room_ngram_flatten 6] room_ngram_flatten[:
#> ['disappointing', 'room', 'room', 'really', 'time', 'room']
Kita dapat menghitung jumlah pasangan kata yang sama dalam satu dokumen dengan menggunakan method collections.Counter.
= collections.Counter(room_ngram)
ngram_counts = pd.DataFrame(ngram_counts.most_common(20),
ngram_df =['ngram', 'count'])
columns
ngram_df.head()
#> ngram count
#> 0 (room, small) 27
#> 1 (small, room) 19
#> 2 (room, little) 10
#> 3 (room, service) 7
#> 4 (room, really) 6
Setelah itu, kita akan melakukan visualisasi mengenai word correlation dari aspek room yang kita pilih sebelumnya dengan bantuan library networkx. Sebelum, melakukan visualisasi, tabel frekuensi ngram_df alangkah lebih baik diubah ke dalam bentuk dictionary. Hasil dari visualisasi i ni menampilkan korelasi kata-kata terhadap aspek yang kita pilih. Semakin dekat kata-kata tersebut satu sama lain, semakin tinggi frekuensi dari kemunculan pasangan kata tersebut.
# we visualize
# transform to dict first
= ngram_df.set_index('ngram').T.to_dict('records')
d 0] d[
#> {('room', 'small'): 27, ('small', 'room'): 19, ('room', 'little'): 10, ('room', 'service'): 7, ('room', 'really'): 6, ('facility', 'room'): 6, ('room', 'room'): 6, ('room', 'could'): 5, ('room', 'quite'): 5, ('room', 'floor'): 5, ('clothes', 'room'): 4, ('kettle', 'room'): 4, ('room', 'night'): 4, ('view', 'room'): 4, ('room', 'breakfast'): 4, ('machine', 'room'): 4, ('great', 'room'): 3, ('room', 'size'): 3, ('chair', 'room'): 3, ('standard', 'room'): 3}
# Create network plot
= nx.Graph()
G
# Create connections between nodes
for k, v in d[0].items():
0], k[1], weight=(v * 10))
G.add_edge(k[
= plt.subplots(figsize=(10, 8))
fig, ax
= nx.spring_layout(G, k=2)
pos
# Plot networks
nx.draw_networkx(G, pos,=16,
font_size=3,
width='grey',
edge_color='purple',
node_color= False,
with_labels =ax)
ax
# Create offset labels
for key, value in pos.items():
= value[0]+.135, value[1]+.045
x, y
ax.text(x, y,=key,
s=dict(facecolor='red', alpha=0.25),
bbox='center', fontsize=13)
horizontalalignment
#> Text(0.14436154090192493, 0.06555010897030616, 'room')
#> Text(0.28680637251916874, -0.17276045649959254, 'small')
#> Text(0.47002549236053687, 0.25281448052717426, 'little')
#> Text(-0.01847479663151988, -0.4204990266494702, 'service')
#> Text(-0.3705627484918037, -0.046311204674404194, 'really')
#> Text(0.3338863524039992, 0.6175683243713621, 'facility')
#> Text(0.8250464326914789, -0.03044772100437307, 'could')
#> Text(-0.10193372231177167, 0.6471794795180892, 'quite')
#> Text(-0.47186554967651484, 0.3415539660921315, 'floor')
#> Text(-0.44472746553720455, 0.7377357891708914, 'clothes')
#> Text(0.863841141665385, -0.37479852558852506, 'kettle')
#> Text(-0.5643140145322153, -0.3907875873310931, 'night')
#> Text(0.1692306925681048, 0.9618663785160028, 'view')
#> Text(0.9868681356503629, 0.3148908193849318, 'breakfast')
#> Text(0.7444601619841278, 0.7061054057529598, 'machine')
#> Text(0.21873476803355257, -0.8945533008493735, 'great')
#> Text(0.672590186510921, -0.7501811063538283, 'size')
#> Text(-0.8649999999999999, 0.10037108836747693, 'chair')
#> Text(-0.31397298010853186, -0.8102969117206656, 'standard')
plt.show()
Agar script dapat digunakan berulang-ulang, alangkah lebih baik jika dimuat dalam bentuk function. Dengan function, kita juga dapat mengganti nilai n dalam ngrams sesuai kebutuhan.
## create functions to be utilized for further observation
# Function to create ngrams (for only 1 topics)
def create_ngrams(text_array, n, aspect):
= [list(ngrams(w,n)) for w in [text_array]]
terms_ngram = [i for i in terms_ngram[0] if aspect in i]
ngram return ngram
#create_ngrams(example, 2, aspect = 'room')
def ngrams_flatten(ngram):
# Flatten list of bigrams in clean tweets
= list(itertools.chain(*ngram))
ngram return ngram
# Function to count the frequency
def ngrams_frequency(ngram, num_most_common):
= collections.Counter(ngram)
ngram_counts = pd.DataFrame(ngram_counts.most_common(num_most_common),
ngram_df =['ngram', 'count'])
columnsreturn ngram_df
#ngrams_frequency(create_ngrams(example, 2, aspect = 'room'), 10)
# Create network plot
def plot_network(dataframe):
try:
# transform to dict first
= dataframe.set_index('ngram').T.to_dict('records')
d 0]
d[= nx.Graph()
G
# Create connections between nodes
for k, v in d[0].items():
0], k[1], weight=(v * 10))
G.add_edge(k[
= plt.subplots(figsize=(10, 8))
fig, ax
= nx.spring_layout(G, k=2)
pos
# Plot networks
nx.draw_networkx(G, pos,=16,
font_size=3,
width='grey',
edge_color='purple',
node_color= False,
with_labels =ax)
ax
# Create offset labels
for key, value in pos.items():
= value[0]+.135, value[1]+.045
x, y
ax.text(x, y,=key,
s=dict(facecolor='red', alpha=0.25),
bbox='center', fontsize=13)
horizontalalignment
plt.show()
except:
return "No Particular Topic"
#let's see what they said about staff
3, aspect = 'staff'), 25)) plot_network(ngrams_frequency(create_ngrams(example,
# How about breakfast?
4, aspect = 'breakfast'), 25)) plot_network(ngrams_frequency(create_ngrams(example,
Kita telah mendapatkan aspek-aspek yang akan kita jadikan variabel penilaian terkait hotel di dataset ini yang telah disimpan di dalam variabel aspects. Berdasarkan aspek-aspek tersebut, dilakukan sentimen analisis untuk menilai seberapa baik fasilitas dan pelayanan hotel berdasarkan review yang disajikan dalam sebuah nilai. Hasil dari sentiment analysis memiliki rentang -1 (paling buruk) sampai 1 (paling baik) yang nantinya dinormalisasi dalam rentang 0 - 10.
# let's do sentiment analysis for each aspects of each hotels
aspects
#> ['staff', 'room', 'breakfast', 'service', 'view', 'restaurant', 'bathroom', 'pool']
Sebelum melakukan analisis sentimen, kolom Negative Review dan Positive Review digabungkan agar tidak terjadi bias dalam penilaian. Selanjutnya untuk setiap aspek dimuat dalam ngrams dengan n = 3 (biasa disebut trigrams). Hasil dari trigrams ini digabungkan (diratakan dengan metode flatten) sehingga didapat satu dokumen baru yang hanya membahas aspek tersebut. Satu dokumen tersebut dihitung skor sentimennya berdasarkan polarity atau ekspresi sentimen dari wisatawan terkait aspek tersebut.
# Combine the review
'Review'] = df_reviews['Negative_Review'] + df_reviews['Positive_Review']
df_reviews[
#plot_network(ngrams_frequency(create_ngrams(example, 3, aspect = 'staff'), 25))
# extract each ngrams from each Hotel Reviews based on aspects
from tqdm import tqdm
= 3
n for aspect in tqdm(aspects):
# create empty list to store sentiment score
= []
sent_list for i in df_reviews['Review']:
# create ngrams
= create_ngrams(i, n, aspect = aspect)
ngram_i # flatten ngrams to make clean list
= ngrams_flatten(ngram_i)
ngram_flatten_i # Get polarity
= getPolarity(' '.join(ngram_flatten_i))
polarity_i
sent_list.append(polarity_i)
= sent_list df_reviews[aspect]
#>
0%| | 0/8 [00:00<?, ?it/s]
12%|#2 | 1/8 [00:11<01:21, 11.71s/it]
25%|##5 | 2/8 [00:31<01:37, 16.29s/it]
38%|###7 | 3/8 [00:38<01:01, 12.36s/it]
50%|##### | 4/8 [00:42<00:35, 8.86s/it]
62%|######2 | 5/8 [00:45<00:20, 6.71s/it]
75%|#######5 | 6/8 [00:48<00:10, 5.47s/it]
88%|########7 | 7/8 [00:51<00:04, 4.84s/it]
100%|##########| 8/8 [00:54<00:00, 3.99s/it]
100%|##########| 8/8 [00:54<00:00, 6.76s/it]
Sehingga didapat nilai untuk masing-masing aspek untuk setiap hotel yang dimuat pada dataframe berikut.
2) df_reviews.head(
#> Negative_Review \
#> Hotel_Name
#> 11 Cadogan Gardens [thought, prise, drink, little, excessive, not...
#> 1K Hotel [conditioning, room, work, despite, complainin...
#>
#> Positive_Review \
#> Hotel_Name
#> 11 Cadogan Gardens [particularly, impressed, warm, welcome, recei...
#> 1K Hotel [location, good, close, marais, arrondissement...
#>
#> Review \
#> Hotel_Name
#> 11 Cadogan Gardens [thought, prise, drink, little, excessive, not...
#> 1K Hotel [conditioning, room, work, despite, complainin...
#>
#> staff room breakfast service view \
#> Hotel_Name
#> 11 Cadogan Gardens 0.502761 0.202778 0.119945 0.484459 0.177778
#> 1K Hotel 0.421448 0.286418 0.104862 0.162868 -0.093750
#>
#> restaurant bathroom pool
#> Hotel_Name
#> 11 Cadogan Gardens 0.465476 0.018318 0.0
#> 1K Hotel 0.448333 0.332811 0.0
Perlu diperhatikan bahwa nilai sentimen masih berupa nilai dalam rentang -1 hingga 1. Untuk mempermudah pemilihan hotel, alangkah lebih baik nilai sentimen dinormalisasi menjadi skor dengan rentang 0 - 10.
# Normalize sentiment in range of 0 - 10
def normalizeSentiment(x):
if max(x) <= 1:
return (x-(-1))/(1+1)*10
else:
return (x)
= df_reviews[aspects].apply(normalizeSentiment)
df_reviews[aspects] 2) df_reviews.head(
#> Negative_Review \
#> Hotel_Name
#> 11 Cadogan Gardens [thought, prise, drink, little, excessive, not...
#> 1K Hotel [conditioning, room, work, despite, complainin...
#>
#> Positive_Review \
#> Hotel_Name
#> 11 Cadogan Gardens [particularly, impressed, warm, welcome, recei...
#> 1K Hotel [location, good, close, marais, arrondissement...
#>
#> Review \
#> Hotel_Name
#> 11 Cadogan Gardens [thought, prise, drink, little, excessive, not...
#> 1K Hotel [conditioning, room, work, despite, complainin...
#>
#> staff room breakfast service view \
#> Hotel_Name
#> 11 Cadogan Gardens 7.513805 6.013889 5.599727 7.422297 5.888889
#> 1K Hotel 7.107240 6.432088 5.524312 5.814342 4.531250
#>
#> restaurant bathroom pool
#> Hotel_Name
#> 11 Cadogan Gardens 7.327381 5.091588 5.0
#> 1K Hotel 7.241667 6.664053 5.0
Gabungkan kolom-kolom yang memuat informasi tambahan yang dibutuhkan ke dataframe.
= ['Average_Score', 'Total_Number_of_Reviews','Hotel_Name', 'Hotel_Address','lat','lng']
col_df
= pd.merge(df_reviews, df[col_df].drop_duplicates(), how = 'left', on = 'Hotel_Name')
df_merge
def join_list(my_list):
return " ".join(my_list)
'Review'] = df_merge['Review'].apply(lambda x: join_list(x))
df_merge['Negative_Review'] = df_merge['Negative_Review'].apply(lambda x: join_list(x))
df_merge['Positive_Review'] = df_merge['Positive_Review'].apply(lambda x: join_list(x))
df_merge[
3) df_merge.head(
#> Hotel_Name \
#> 0 11 Cadogan Gardens
#> 1 1K Hotel
#> 2 25hours Hotel beim MuseumsQuartier
#>
#> Negative_Review \
#> 0 thought prise drink little excessive nothing p...
#> 1 conditioning room work despite complaining fix...
#> 2 breakfast included buffet really expensive bre...
#>
#> Positive_Review \
#> 0 particularly impressed warm welcome received l...
#> 1 location good close marais arrondissement fitn...
#> 2 cool vintage style middle museum quarter metro...
#>
#> Review staff room \
#> 0 thought prise drink little excessive nothing p... 7.513805 6.013889
#> 1 conditioning room work despite complaining fix... 7.107240 6.432088
#> 2 breakfast included buffet really expensive bre... 7.261356 6.139688
#>
#> breakfast service view restaurant bathroom pool Average_Score \
#> 0 5.599727 7.422297 5.888889 7.327381 5.091588 5.0 8.7
#> 1 5.524312 5.814342 4.531250 7.241667 6.664053 5.0 7.7
#> 2 7.106359 6.979958 7.849841 7.266420 6.738946 5.0 8.8
#>
#> Total_Number_of_Reviews Hotel_Address \
#> 0 393 11 Cadogan Gardens Sloane Square Kensington an...
#> 1 663 13 Boulevard Du Temple 3rd arr 75003 Paris France
#> 2 4324 Lerchenfelder Stra e 1 3 07 Neubau 1070 Vienna...
#>
#> lat lng
#> 0 51.493616 -0.159235
#> 1 48.863932 2.365874
#> 2 48.206474 16.354630
9.1.4 Dashboard Ideas
Salah satu cara terbaik untuk memberikan informasi adalah melalui dashboard interaktif yang dapat dioperasikan langsung oleh penggunanya. Tentunya kemudahan yang diberikan akan meningkatkan penyerapan informasi oleh pengguna. berikut adalah beberapa ide-ide fitur yang akan dimuat di dalam dashboard tersebut.
# Function for dashboard
def radar_plot(data): # data categories must be stored as index
=(10,10))
plt.figure(figsize# number of variable
=s.index
categories= len(s)
N
# We are going to plot the first line of the data frame.
# But we need to repeat the first value to close the circular graph:
= s.iloc[:,0].values.flatten().tolist()
values+= values[:1]
values
values
# What will be the angle of each axis in the plot? (we divide the plot / number of variable)
= [n / float(N) * 2 * pi for n in range(N)]
angles += angles[:1]
angles
# Initialise the spider plot
= plt.subplot(111, polar=True)
ax
# Draw one axe per variable + add labels labels yet
-1], categories, color='blue', size=20)
plt.xticks(angles[:
# Draw ylabels
90)
ax.set_rlabel_position(2,4,6,8], ["2","4","6","8"], color="grey", size=20)
plt.yticks([0,10)
plt.ylim(
# Plot data
=2, linestyle='solid')
ax.plot(angles, values, linewidth
# Fill area
'b', alpha=0.1)
ax.fill(angles, values,
def plot_coordinate(lat, lng , name = pd.Series(['']), address = pd.Series(['']), zoom_start = 10, color = 'red', fill_color = 'blue'):
= folium.Map(location=[lat,lng], zoom_start=zoom_start)
m
folium.CircleMarker(0], lng.values[0]],
[lat.values[=10,
radius=color,
color='Name: ' + name.values[0] + '\n\n Address: ' + address.values[0],
popup= True,
fill = fill_color,
fill_color =0.6
fill_opacity
).add_to(m)return m
Search bar berfungsi untuk melakukan pemenggalan data (querying) untuk mengambil data yang menjadi fokus bagi penggunanya.
# Search Bar
= '25hours Hotel beim MuseumsQuartier'
search
= df_merge[df_merge['Hotel_Name'] == search]
search_data search_data
#> Hotel_Name \
#> 2 25hours Hotel beim MuseumsQuartier
#>
#> Negative_Review \
#> 2 breakfast included buffet really expensive bre...
#>
#> Positive_Review \
#> 2 cool vintage style middle museum quarter metro...
#>
#> Review staff room \
#> 2 breakfast included buffet really expensive bre... 7.261356 6.139688
#>
#> breakfast service view restaurant bathroom pool Average_Score \
#> 2 7.106359 6.979958 7.849841 7.26642 6.738946 5.0 8.8
#>
#> Total_Number_of_Reviews Hotel_Address \
#> 2 4324 Lerchenfelder Stra e 1 3 07 Neubau 1070 Vienna...
#>
#> lat lng
#> 2 48.206474 16.35463
Average score menampilkan rating rata-rata yang diberikan oleh wisatawan terdahulu terhadap pengalaman singgah di hotel tersebut.
# Average Score
'Average_Score'].values[0] search_data[
#> 8.8
Jumlah orang yang memberikan ulasan juga perlu ditampilkan. Semakin banyak ulasan, semakin reliable penilaian tersebut.
# Total Number of Reviews
'Total_Number_of_Reviews'].values[0] search_data[
#> 4324
Titik koordinat diperlukan untuk menyampaikan informasi mengenai lokasi dari hotel tersebut. Hal ini juga bertujuan untuk memberikan informasi mengenai kondisi di sekitar hotel terkait seberapa strategis lokasi hotel tersebut (dekat dengan pusat wisata, pusat pembelanjaan, dan lain-lain).
= 15) plot_coordinate(search_data.lat, search_data.lng, search_data.Hotel_Name, search_data.Hotel_Address, zoom_start
Hasil dari analisis sentimen terkait aspek (fasilitas dan pelayanan) hotel dimuat dalam bentuk visualisasi radar plot untuk mempermudah pengambilan keputusan dalam memilih hotel yang akan dijadikan tempat singgah sementara.
# Sentiment Score
= search_data[aspects].T
s radar_plot(s)
Diagram jaringan (network diagram) untuk melihat korelasi kata-kata dengan aspek yang dipilih.
#let's see what they said about staff
'Review'].values[0].split(), 3, aspect = 'staff'), 20)) plot_network(ngrams_frequency(create_ngrams(search_data[
#let's see what they said about room
'Review'].values[0].split(), 3, aspect = 'room'), 20)) plot_network(ngrams_frequency(create_ngrams(search_data[
#let's see what they said about breakfast
'Review'].values[0].split(), 3, aspect = 'breakfast'), 20)) plot_network(ngrams_frequency(create_ngrams(search_data[
#let's see what they said about service
'Review'].values[0].split(), 3, aspect = 'service'), 20)) plot_network(ngrams_frequency(create_ngrams(search_data[
#let's see what they said about view
'Review'].values[0].split(), 3, aspect = 'view'), 20)) plot_network(ngrams_frequency(create_ngrams(search_data[
#let's see what they said about restaurant
'Review'].values[0].split(), 3, aspect = 'restaurant'), 20)) plot_network(ngrams_frequency(create_ngrams(search_data[
#let's see what they said about pool
'Review'].values[0].split(), 3, aspect = 'pool'), 20)) plot_network(ngrams_frequency(create_ngrams(search_data[
Wordcloud untuk review hotel tersebut untuk melihat kata-kata yang paling sering muncul dalam ulasan wisatawan.
# Draw WordCloud
'Review'].values[0].split(),'white') wordcloud_draw(search_data[
Fungsi untuk mengekspor dataset yang telah diolah untuk pembuatan dashboard.
#df_merge.to_csv('dashboard_data.csv')
#df_merge.to_excel('dashboard_data.xlsx')
Hasil dari dashboard yang telah dirancang dapat dilihat di laman berikut: https://europehoteldashboard.herokuapp.com/
9.1.5 Conclusion
Hasil dari analisis ini berupa dashboard yang dapat digunakan para wisatawan untuk menentukan hotel mana yang terbaik dipilih sebagai tempat tinggal sementara selama menikmati liburan. Tentunya dengan adanya dashboard ini, diharapkan para wisatawan mendapatkan pengalaman terbaik dalam menginap. Dashboard ini menampilkan penilaian tidak hanya melalui rating score tapi juga mengenai nilai sentimen para wisatawan terdahulu terkait fasilitas-fasilitas dan pelayanan pada hotel tersebut. Fasilitas-fasilitas dan pelayanan yang menjadi aspek penilaian antara lain ruangan, staf hotel, kolam renang, kamar mandi, restoran, pemandangan, pelayanan, dan sarapan.