1 Pendahuluan :

Pada pengerjaan tugas ini merupakan tugas untuk sertifikasi BNSP Data Analyst yang dikerjakan oleh Hanifah Syahidah dengan NIM G6401231067 Jurusan Ilmu Komputer semester 6.

Pertanyaan:

  1. Bagaimana tren harga rata-rata kamar hotel di 5 negara dengan penjualan terbanyak, dianalisis berdasarkan market_segment?
  2. Model ARIMA apa yang paling sesuai untuk masing-masing negara (selain ESP)?
  3. Berapa prediksi harga kamar untuk bulan September 2017?

2 Persiapan: Library & Data

2.1 Instalasi Package

install.packages(c(
  # "modeldata"
  # "dplyr"
  # "ggplot2"
  # "forecast"
  # "tseries"
  # "lmtest"
  # "scales"
  # "RColorBrewer"
  # "gridExtra"
  # "kableExtra"
))

2.2 Pemanggilan Library

# Core
library(modeldata)
## Warning: package 'modeldata' was built under R version 4.4.3
library(dplyr)
## Warning: package 'dplyr' was built under R version 4.4.3
## 
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union
library(ggplot2)
## Warning: package 'ggplot2' was built under R version 4.4.3
# Time Series
library(forecast)
## Warning: package 'forecast' was built under R version 4.4.3
library(tseries)
## Warning: package 'tseries' was built under R version 4.4.3
## Registered S3 method overwritten by 'quantmod':
##   method            from
##   as.zoo.data.frame zoo
library(lmtest)
## Warning: package 'lmtest' was built under R version 4.4.3
## Loading required package: zoo
## Warning: package 'zoo' was built under R version 4.4.3
## 
## Attaching package: 'zoo'
## The following objects are masked from 'package:base':
## 
##     as.Date, as.Date.numeric
# Visualisasi & Tabel
library(scales)
## Warning: package 'scales' was built under R version 4.4.3
library(RColorBrewer)
library(knitr)
library(kableExtra)
## Warning: package 'kableExtra' was built under R version 4.4.3
## 
## Attaching package: 'kableExtra'
## The following object is masked from 'package:dplyr':
## 
##     group_rows

2.3 Memuat Dataset

data("hotel_rates")

# Pastikan format tanggal benar
hotel_rates$arrival_date <- as.Date(hotel_rates$arrival_date)

cat("Dimensi dataset:", dim(hotel_rates)[1], "baris x", dim(hotel_rates)[2], "kolom\n")
## Dimensi dataset: 15402 baris x 28 kolom
cat("Periode data   :", format(min(hotel_rates$arrival_date), "%d %B %Y"),
    "s/d", format(max(hotel_rates$arrival_date), "%d %B %Y"), "\n")
## Periode data   : 02 July 2016 s/d 31 August 2017
cat("Missing values :", sum(is.na(hotel_rates)), "\n")
## Missing values : 0
# Gambaran singkat variabel kunci
hotel_rates %>%
  select(arrival_date, country, market_segment, avg_price_per_room, historical_adr) %>%
  head(8)

3 Tugas 1 — Analisis Trend Harga Kamar Hotel (5 Negara Terbanyak)

3.1 Identifikasi 5 Negara dengan Penjualan Terbanyak

Langkah pertama adalah menentukan 5 negara asal pelanggan dengan jumlah pemesanan terbanyak. Pendekatan ini memastikan bahwa analisis tren didasarkan pada segmen pasar yang paling representatif dan stabil secara statistik.

# Hitung jumlah pemesanan per negara
top5_country <- hotel_rates %>%
  group_by(country) %>%
  summarise(
    jumlah_booking = n(),
    avg_price      = round(mean(avg_price_per_room), 2),
    median_price   = round(median(avg_price_per_room), 2),
    sd_price       = round(sd(avg_price_per_room), 2),
    .groups        = "drop"
  ) %>%
  arrange(desc(jumlah_booking)) %>%
  head(5) %>%
  mutate(
    pct_total = round(jumlah_booking / nrow(hotel_rates) * 100, 1),
    country   = toupper(country)
  )

top5_country %>%
  kable(
    caption   = "Top 5 Negara dengan Penjualan Hotel Terbanyak",
    col.names = c("Negara", "Jumlah Booking", "Avg Price (EUR)",
                  "Median Price (EUR)", "Std Dev (EUR)", "% dari Total")
  ) %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Top 5 Negara dengan Penjualan Hotel Terbanyak
Negara Jumlah Booking Avg Price (EUR) Median Price (EUR) Std Dev (EUR) % dari Total
PRT 4869 109.26 80.00 72.82 31.6
GBR 3465 83.80 70.00 45.80 22.5
ESP 1449 140.68 127.00 77.98 9.4
IRL 934 96.18 82.78 51.33 6.1
FRA 798 100.45 76.48 63.45 5.2

Berdasarkan hasil sort diperoleh lima negara teratas yaitu; Portugal (PRT), United Kingdom (GBR), Spain (ESP), Ireland (IRL), dan France (FRA. Negara tersebut menyumbang lebih dari 70% total pemesanan. Portugal mendominasi dengan hampir sepertiga total booking, yang mengindikasikan bahwa hotel dalam dataset ini kemungkinan berlokasi di Portugal atau wilayah sekitarnya.

# Siapkan vektor top 5
top5_vec <- tolower(top5_country$country)

# Visualisasi distribusi booking
top5_country %>%
  mutate(country = factor(country, levels = country)) %>%
  ggplot(aes(x = country, y = jumlah_booking, fill = country)) +
  geom_col(width = 0.65, show.legend = FALSE) +
  geom_text(aes(label = paste0(jumlah_booking, "\n(", pct_total, "%)")),
            vjust = -0.4, fontface = "bold", size = 3.8) +
  geom_hline(yintercept = mean(top5_country$jumlah_booking),
             linetype = "dashed", color = "gray40", size = 0.8) +
  annotate("text", x = 0.6, y = mean(top5_country$jumlah_booking) + 60,
           label = "Rata-rata", color = "gray40", size = 3.2) +
  scale_fill_manual(values = c("#2c7bb6","#74add1","#d73027","#fdae61","#4dac26")) +
  scale_y_continuous(expand = expansion(mult = c(0, 0.12))) +
  labs(
    title    = "Distribusi Jumlah Booking Hotel — Top 5 Negara",
    subtitle = "Portugal mendominasi dengan ~32% total pemesanan",
    x        = "Negara Asal Pelanggan",
    y        = "Jumlah Pemesanan"
  ) +
  theme_bw(base_size = 12) +
  theme(
    plot.title    = element_text(face = "bold", size = 14),
    plot.subtitle = element_text(color = "gray40"),
    panel.grid.major.x = element_blank()
  )
## Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
## ℹ Please use `linewidth` instead.
## This warning is displayed once per session.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.

3.2 Filter Data untuk 5 Negara Terpilih

dat_top5 <- hotel_rates %>%
  filter(country %in% top5_vec)

cat("Data setelah filter:", nrow(dat_top5), "baris\n")
## Data setelah filter: 11515 baris
cat("Distribusi market_segment:\n")
## Distribusi market_segment:
table(dat_top5$market_segment)
## 
##            corporate               direct               groups 
##                  795                 2467                 1301 
## offline_travel_agent  online_travel_agent 
##                 2238                 4714

3.3 Agregasi Data untuk Analisis Tren

Untuk melihat tren yang jelas, data diagregasi menjadi rata-rata harga per hari per kombinasi negara dan market_segment.

# Agregasi harian per negara + market_segment
dat_trend <- dat_top5 %>%
  group_by(arrival_date, country, market_segment) %>%
  summarise(
    avg_price  = mean(avg_price_per_room),
    n_booking  = n(),
    .groups    = "drop"
  ) %>%
  mutate(country = toupper(country))

# Label mapping negara
country_labels <- c(
  PRT = "Portugal (PRT)", GBR = "United Kingdom (GBR)",
  ESP = "Spain (ESP)",    IRL = "Ireland (IRL)",
  FRA = "France (FRA)"
)

cat("Total baris agregat:", nrow(dat_trend), "\n")
## Total baris agregat: 3599
head(dat_trend, 6)

3.4 Visualisasi Tren: Per Negara × Market Segment

ms_colors <- c(
  "corporate"            = "#1F51FF",
  "direct"               = "#d73027",
  "groups"               = "#4dac26",
  "offline_travel_agent" = "#B026FF",
  "online_travel_agent"  = "#ff10f0"
)

dat_trend %>%
  mutate(country = factor(country,
                          levels = c("PRT","GBR","ESP","IRL","FRA"),
                          labels = country_labels)) %>%
  ggplot(aes(x = arrival_date, y = avg_price, color = market_segment)) +
  geom_line(alpha = 0.4, linewidth = 0.4) +
  geom_smooth(method = "loess", se = FALSE, linewidth = 1, span = 0.35) +
  facet_wrap(~ country, ncol = 2, scales = "free_y") +
  scale_color_manual(values = ms_colors) +
  scale_x_date(date_labels = "%b %Y", date_breaks = "3 months") +
  labs(
    title = "Tren Harga Rata-Rata Kamar Hotel — 5 Negara Terbanyak",
    x     = "Tanggal Kedatangan",
    y     = "Rata-Rata Harga Kamar (EUR)",
    color = "Market Segment"
  ) +
  theme_bw() +
  theme(
    legend.position = "bottom",
    axis.text.x     = element_text(angle = 45, hjust = 1)
  )
## `geom_smooth()` using formula = 'y ~ x'
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : span too small.  fewer data values than degrees of freedom.
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : at 17084
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : radius 0.11222
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : all data on boundary of neighborhood. make span bigger
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : pseudoinverse used at 17084
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : neighborhood radius 0.335
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : reciprocal condition number 1
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : at 17151
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : radius 0.11222
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : all data on boundary of neighborhood. make span bigger
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : There are other near singularities as well. 0.11222
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : zero-width neighborhood. make span bigger
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : zero-width neighborhood. make span bigger
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : zero-width neighborhood. make span bigger
## Warning: Failed to fit group 1.
## Caused by error in `predLoess()`:
## ! NA/NaN/Inf in foreign function call (arg 5)
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : span too small.  fewer data values than degrees of freedom.
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : pseudoinverse used at 17045
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : neighborhood radius 34.96
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : reciprocal condition number 0
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : There are other near singularities as well. 3244.4
## Warning in sqrt(sum.squares/one.delta): NaNs produced
## Warning: Failed to fit group 1.
## Caused by error in `simpleLoess()`:
## ! span is too small
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : span too small.  fewer data values than degrees of freedom.
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : pseudoinverse used at 17057
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : neighborhood radius 16.19
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : reciprocal condition number 0
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : There are other near singularities as well. 10.176
## Warning: Failed to fit group 1.
## Caused by error in `simpleLoess()`:
## ! span is too small
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : span too small.  fewer data values than degrees of freedom.
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : at 17085
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : radius 0.9025
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : all data on boundary of neighborhood. make span bigger
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : pseudoinverse used at 17085
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : neighborhood radius 0.95
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : reciprocal condition number 1
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : at 17277
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : radius 0.9025
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : all data on boundary of neighborhood. make span bigger
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : There are other near singularities as well. 0.9025
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : zero-width neighborhood. make span bigger
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : zero-width neighborhood. make span bigger
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : zero-width neighborhood. make span bigger
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : zero-width neighborhood. make span bigger
## Warning: Failed to fit group 3.
## Caused by error in `predLoess()`:
## ! NA/NaN/Inf in foreign function call (arg 5)

Interpretasi Tren per Negara:

  • Portugal (PRT): Tren harga cenderung meningkat dari awal 2017, terutama pada segmen online_travel_agent dan direct. Pola ini konsisten dengan musim wisata Eropa yang memuncak di pertengahan hingga akhir tahun.

  • United Kingdom (GBR): Harga relatif lebih stabil dengan fluktuasi cukup tinggi di segmen direct. Terlihat pola mingguan (weekly seasonality) yang cukup konsisten.

  • Spain (ESP): Menunjukkan tren recovery setelah turun di akhir 2016, kembali naik menuju pertengahan 2017.

  • Ireland (IRL): Pola serupa dengan GBR, namun dengan volatilitas lebih tinggi akibat jumlah observasi yang lebih sedikit.

  • France (FRA): Tren cenderung datar namun dengan lonjakan harga sporadis, terutama di segmen direct dan groups.

3.5 Heatmap: Pola Musiman Bulanan

Heatmap berikut memberikan perspektif tambahan tentang pola musiman harga per bulan dan per negara — sesuatu yang tidak selalu terlihat jelas dari line chart.

# Agregasi bulanan
dat_heat <- dat_top5 %>%
  mutate(
    bulan = as.numeric(format(arrival_date, "%m")),
    tahun = format(arrival_date, "%Y")
  ) %>%
  group_by(country, tahun, bulan) %>%
  summarise(avg_price = mean(avg_price_per_room), .groups = "drop") %>%
  mutate(
    bulan_label = factor(month.abb[bulan], levels = month.abb),
    country     = toupper(country)
  )

ggplot(dat_heat, aes(x = bulan_label, y = country, fill = avg_price)) +
  geom_tile(color = "white") +
  geom_text(aes(label = paste0("€", round(avg_price, 0))), size = 3) +
  facet_wrap(~ tahun, ncol = 1) +
  scale_fill_gradient2(
    low      = "#313695",
    mid      = "#74add1",
    high     = "#d73027",
    midpoint = median(dat_heat$avg_price, na.rm = TRUE),
    name     = "Harga (EUR)"
  ) +
  labs(
    title = "Heatmap Rata-Rata Harga Hotel per Bulan & Negara",
    x     = "Bulan",
    y     = "Negara"
  ) +
  theme_minimal() +
  theme(
    panel.grid      = element_blank(),
    legend.position = "right"
  )

Bulan Juli - Agustus merupakan periode dengan harga tertinggi di hampir semua negara, sejalan dengan periode high season pariwisata Eropa. Sebaliknya, Januari–Februari menjadi periode dengan harga terendah. Pola ini menunjukkan adanya seasonality tahunan yang perlu dipertimbangkan dalam proses pemodelan.

3.6 Ringkasan Statistik Tren per Negara & Market Segment

dat_top5 %>%
  mutate(country = toupper(country)) %>%
  group_by(country, market_segment) %>%
  summarise(
    n_booking    = n(),
    avg_price    = round(mean(avg_price_per_room), 2),
    median_price = round(median(avg_price_per_room), 2),
    sd_price     = round(sd(avg_price_per_room), 2),
    min_price    = round(min(avg_price_per_room), 2),
    max_price    = round(max(avg_price_per_room), 2),
    .groups      = "drop"
  ) %>%
  arrange(country, desc(n_booking))

4 Tugas 2 — Pemodelan ARIMA: 4 Negara (Selain ESP)

Note: Spain (ESP) digunakan sebagai contoh pemodelan pada materi utama. Tugas ini berfokus pada pemodelan untuk empat negara lainnya, yaitu Portugal (PRT), United Kingdom (GBR), Ireland (IRL), dan France (FRA).

4.1 Metodologi Pemodelan

Pipeline pemodelan ARIMA yang diterapkan:

1. Agregasi → rata-rata harga harian per negara
2. Eksplorasi visual → plot time series + decomposition
3. Uji stasioneritas → ADF Test (Augmented Dickey-Fuller)
4. Differencing → jika tidak stasioner (d ≥ 1)
5. Identifikasi order → ACF & PACF plot
6. Estimasi → auto.arima() [pencarian exhaustive]
7. Diagnostik → Ljung-Box test, QQ-plot residual, ACF/PACF residual
8. Overfitting check → uji model dengan orde lebih tinggi
9. Pemilihan model → berdasarkan AIC, BIC, signifikansi parameter
10. Forecasting → prediksi September 2017

4.2 Persiapan Data Time Series

top4_vec <- top5_vec[top5_vec != "esp"]
cat("4 Negara yang dimodelkan:", paste(toupper(top4_vec), collapse = ", "), "\n")
## 4 Negara yang dimodelkan: PRT, GBR, IRL, FRA
get_daily_avg <- function(df, neg) {
  df %>%
    filter(country == neg) %>%
    group_by(arrival_date) %>%
    summarise(avg_price = mean(avg_price_per_room), .groups = "drop") %>%
    arrange(arrival_date)
}

dat_list <- lapply(setNames(top4_vec, top4_vec), get_daily_avg, df = hotel_rates)

lapply(dat_list, function(d) {
  data.frame(
    n_hari        = nrow(d),
    periode_mulai = as.character(min(d$arrival_date)),
    periode_akhir = as.character(max(d$arrival_date)),
    avg_price     = round(mean(d$avg_price), 2),
    sd_price      = round(sd(d$avg_price), 2)
  )
}) %>%
  bind_rows(.id = "country") %>%
  mutate(country = toupper(country))

4.3 Portugal (PRT)

4.3.1 Eksplorasi Time Series

dat_prt <- dat_list[["prt"]]
yt_prt  <- ts(dat_prt$avg_price, frequency = 1)

ggplot(dat_prt, aes(x = arrival_date, y = avg_price)) +
  geom_line(color = "#2c7bb6", linewidth = 0.6) +
  geom_smooth(method = "loess", span = 0.25, se = FALSE, color = "#d73027") +
  geom_hline(yintercept = mean(dat_prt$avg_price), linetype = "dashed", color = "gray50") +
  scale_x_date(date_labels = "%b %Y", date_breaks = "2 months") +
  labs(
    title = "Time Series Harga Hotel — Portugal (PRT)",
    x = "Tanggal", y = "Rata-Rata Harga (EUR)"
  ) +
  theme_bw() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))
## `geom_smooth()` using formula = 'y ~ x'

4.3.2 STL Decomposition

# Gunakan frekuensi 7 (weekly) untuk decomposition
yt_prt_w <- ts(dat_prt$avg_price, frequency = 7)
decomp_prt <- stl(yt_prt_w, s.window = "periodic", robust = TRUE)
autoplot(decomp_prt) +
  labs(title = "STL Decomposition — Portugal (PRT)",
       subtitle = "Memisahkan komponen: Tren, Musiman (weekly), dan Residual") +
  theme_bw()

Interpretasi Decomposition PRT: Komponen trend menunjukkan penurunan pada akhir 2016 yang kemudian diikuti pemulihan pada tahun 2017. Komponen seasonal memperlihatkan pola mingguan yang konsisten, di mana harga cenderung meningkat pada pertengahan minggu. Sementara itu, komponen residual masih menunjukkan beberapa lonjakan (spike) yang cukup besar, mengindikasikan kemungkinan adanya peristiwa atau faktor eksternal yang memengaruhi harga pada periode tertentu.

4.3.3 Uji Stasioneritas (ADF Test)

adf_prt_raw  <- adf.test(yt_prt)
adf_prt_diff <- adf.test(diff(yt_prt))
## Warning in adf.test(diff(yt_prt)): p-value smaller than printed p-value
cat("ADF Test — Data Asli PRT:\n")
## ADF Test — Data Asli PRT:
cat("  Statistik uji:", round(adf_prt_raw$statistic, 4), "\n")
##   Statistik uji: -0.9082
cat("  p-value      :", round(adf_prt_raw$p.value, 4),
    ifelse(adf_prt_raw$p.value < 0.05, "[STASIONER]", "[TIDAK STASIONER]"), "\n\n")
##   p-value      : 0.9517 [TIDAK STASIONER]
cat("ADF Test — Setelah Differencing (d=1) PRT:\n")
## ADF Test — Setelah Differencing (d=1) PRT:
cat("  Statistik uji:", round(adf_prt_diff$statistic, 4), "\n")
##   Statistik uji: -9.0747
cat("  p-value      :", round(adf_prt_diff$p.value, 4),
    ifelse(adf_prt_diff$p.value < 0.05, "[STASIONER]", "[TIDAK STASIONER]"), "\n")
##   p-value      : 0.01 [STASIONER]

4.3.4 Plot ACF & PACF

par(mfrow = c(1, 2), mar = c(4, 4, 3, 2))
Acf(diff(yt_prt),  main = "ACF — diff(PRT)",  col = "#2c7bb6", lag.max = 30)
Pacf(diff(yt_prt), main = "PACF — diff(PRT)", col = "#d73027", lag.max = 30)

par(mfrow = c(1, 1))

4.3.5 Pemodelan ARIMA — PRT

# auto.arima dengan pencarian exhaustive (stepwise=FALSE)
model_prt <- auto.arima(
  yt_prt,
  stepwise      = FALSE,
  approximation = FALSE,
  ic            = "aic"
)

cat ("MODEL TERPILIH : PORTUGAL\n")
## MODEL TERPILIH : PORTUGAL
summary(model_prt)
## Series: yt_prt 
## ARIMA(3,1,2) 
## 
## Coefficients:
##          ar1      ar2      ar3      ma1     ma2
##       0.7246  -0.2918  -0.3397  -1.2363  0.6574
## s.e.  0.0845   0.0859   0.0721   0.0707  0.1193
## 
## sigma^2 = 427.4:  log likelihood = -1879.3
## AIC=3770.6   AICc=3770.8   BIC=3794.88
## 
## Training set error measures:
##                     ME     RMSE      MAE       MPE     MAPE      MASE
## Training set 0.1617349 20.52656 15.22893 -3.288968 16.30798 0.8826086
##                    ACF1
## Training set -0.0340347
cat("Uji Signifikansi Parameter (Uji-t) \n")
## Uji Signifikansi Parameter (Uji-t)
coeftest(model_prt)
## 
## z test of coefficients:
## 
##      Estimate Std. Error  z value  Pr(>|z|)    
## ar1  0.724632   0.084506   8.5749 < 2.2e-16 ***
## ar2 -0.291782   0.085889  -3.3972 0.0006808 ***
## ar3 -0.339741   0.072086  -4.7130 2.441e-06 ***
## ma1 -1.236318   0.070720 -17.4818 < 2.2e-16 ***
## ma2  0.657373   0.119283   5.5110 3.567e-08 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

4.3.6 Diagnostik Residual — PRT

par(mfrow = c(2, 2), mar = c(4, 4, 3, 2))

# 1. Plot residual
plot(residuals(model_prt), type = "l", col = "#2c7bb6",
     main = "Residual — PRT", ylab = "Residual", xlab = "Waktu")
abline(h = 0, col = "red", lty = 2, lwd = 1.5)

# 2. ACF residual
Acf(residuals(model_prt), main = "ACF Residual — PRT",
    col = "#2c7bb6", lag.max = 30)

# 3. PACF residual
Pacf(residuals(model_prt), main = "PACF Residual — PRT",
     col = "#d73027", lag.max = 30)

# 4. QQ-plot
qqnorm(residuals(model_prt), pch = 16, col = "#2c7bb6",
       main = "Q-Q Plot Residual — PRT")
qqline(residuals(model_prt), col = "red", lwd = 2)

par(mfrow = c(1, 1))
lb_prt  <- Box.test(residuals(model_prt), lag = 20, type = "Ljung-Box")
mape_prt <- mean(abs((dat_prt$avg_price - as.numeric(fitted(model_prt))) /
                       dat_prt$avg_price) * 100, na.rm = TRUE)

cat("Ljung-Box Test (lag=20):\n")
## Ljung-Box Test (lag=20):
cat("  p-value:", round(lb_prt$p.value, 4),
    ifelse(lb_prt$p.value > 0.05,
           "[White noise - OK]",
           "[Ada autokorelasi]"), "\n")
##   p-value: 0.1933 [White noise - OK]
cat("MAPE In-Sample:", round(mape_prt, 2), "%\n")
## MAPE In-Sample: 16.31 %
cat("AIC:", round(model_prt$aic, 2), "| BIC:", round(model_prt$bic, 2), "\n")
## AIC: 3770.6 | BIC: 3794.88

4.4 United Kingdom (GBR)

4.4.1 Eksplorasi Time Series

dat_gbr <- dat_list[["gbr"]]
yt_gbr  <- ts(dat_gbr$avg_price, frequency = 1)

ggplot(dat_gbr, aes(x = arrival_date, y = avg_price)) +
  geom_line(color = "#d73027", linewidth = 0.6) +
  geom_smooth(method = "loess", span = 0.25, se = FALSE, color = "#2c7bb6") +
  geom_hline(yintercept = mean(dat_gbr$avg_price), linetype = "dashed", color = "gray50") +
  scale_x_date(date_labels = "%b %Y", date_breaks = "2 months") +
  labs(
    title = "Time Series Harga Hotel — United Kingdom (GBR)",
    x = "Tanggal", y = "Rata-Rata Harga (EUR)"
  ) +
  theme_bw() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))
## `geom_smooth()` using formula = 'y ~ x'

4.4.2 STL Decomposition

yt_gbr_w  <- ts(dat_gbr$avg_price, frequency = 7)
decomp_gbr <- stl(yt_gbr_w, s.window = "periodic", robust = TRUE)
autoplot(decomp_gbr) +
  labs(title = "STL Decomposition — United Kingdom (GBR)") +
  theme_bw()

4.4.3 Uji Stasioneritas

adf_gbr_raw  <- adf.test(yt_gbr)
adf_gbr_diff <- adf.test(diff(yt_gbr))
## Warning in adf.test(diff(yt_gbr)): p-value smaller than printed p-value
cat("ADF Test — Data Asli GBR:\n")
## ADF Test — Data Asli GBR:
cat("  p-value:", round(adf_gbr_raw$p.value, 4),
    ifelse(adf_gbr_raw$p.value < 0.05, "[STASIONER]", "[TIDAK STASIONER]"), "\n")
##   p-value: 0.7414 [TIDAK STASIONER]
cat("ADF Test — Setelah Differencing GBR:\n")
## ADF Test — Setelah Differencing GBR:
cat("  p-value:", round(adf_gbr_diff$p.value, 4),
    ifelse(adf_gbr_diff$p.value < 0.05, "[STASIONER]", "[TIDAK STASIONER]"), "\n")
##   p-value: 0.01 [STASIONER]

4.4.4 Plot ACF & PACF

par(mfrow = c(1, 2), mar = c(4, 4, 3, 2))
Acf(diff(yt_gbr),  main = "ACF — diff(GBR)",  col = "#d73027", lag.max = 30)
Pacf(diff(yt_gbr), main = "PACF — diff(GBR)", col = "#2c7bb6", lag.max = 30)

par(mfrow = c(1, 1))

4.4.5 Pemodelan ARIMA — GBR

model_gbr <- auto.arima(
  yt_gbr,
  stepwise      = FALSE,
  approximation = FALSE,
  ic            = "aic"
)

cat("MODEL TERPILIH : UNITED KINGDOM \n")
## MODEL TERPILIH : UNITED KINGDOM
summary(model_gbr)
## Series: yt_gbr 
## ARIMA(2,1,3) 
## 
## Coefficients:
##           ar1      ar2     ma1      ma2      ma3
##       -1.4075  -0.9453  0.6880  -0.1810  -0.7395
## s.e.   0.0289   0.0326  0.0375   0.0522   0.0365
## 
## sigma^2 = 466.8:  log likelihood = -1857.99
## AIC=3727.99   AICc=3728.19   BIC=3752.14
## 
## Training set error measures:
##                     ME     RMSE      MAE       MPE     MAPE      MASE
## Training set 0.5103627 21.44848 15.26581 -3.750684 17.24458 0.7572301
##                     ACF1
## Training set -0.04199575
coeftest(model_gbr)
## 
## z test of coefficients:
## 
##      Estimate Std. Error  z value  Pr(>|z|)    
## ar1 -1.407545   0.028917 -48.6757 < 2.2e-16 ***
## ar2 -0.945338   0.032614 -28.9855 < 2.2e-16 ***
## ma1  0.687982   0.037481  18.3554 < 2.2e-16 ***
## ma2 -0.181023   0.052225  -3.4662 0.0005278 ***
## ma3 -0.739456   0.036497 -20.2606 < 2.2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

4.4.6 Diagnostik Residual — GBR

par(mfrow = c(2, 2), mar = c(4, 4, 3, 2))
plot(residuals(model_gbr), type = "l", col = "#d73027",
     main = "Residual — GBR", ylab = "Residual")

abline(h = 0, col = "blue", lty = 2, lwd = 1.5)
Acf(residuals(model_gbr),  main = "ACF Residual — GBR",  col = "#d73027", lag.max = 30)
Pacf(residuals(model_gbr), main = "PACF Residual — GBR", col = "#2c7bb6", lag.max = 30)
qqnorm(residuals(model_gbr), pch = 16, col = "#d73027", main = "Q-Q Plot — GBR")
qqline(residuals(model_gbr), col = "blue", lwd = 2)

par(mfrow = c(1, 1))
lb_gbr   <- Box.test(residuals(model_gbr), lag = 20, type = "Ljung-Box")
mape_gbr <- mean(abs((dat_gbr$avg_price - as.numeric(fitted(model_gbr))) /
                       dat_gbr$avg_price) * 100, na.rm = TRUE)

cat("Ljung-Box p-value:", round(lb_gbr$p.value, 4),
    ifelse(lb_gbr$p.value > 0.05, "[White noise - OK]", "[Ada autokorelasi]"), "\n")
## Ljung-Box p-value: 0.0578 [White noise - OK]
cat("MAPE In-Sample:", round(mape_gbr, 2), "%\n")
## MAPE In-Sample: 17.24 %
cat("AIC:", round(model_gbr$aic, 2), "| BIC:", round(model_gbr$bic, 2), "\n")
## AIC: 3727.99 | BIC: 3752.14

4.5 Ireland (IRL)

4.5.1 Eksplorasi Time Series

dat_irl <- dat_list[["irl"]]
yt_irl  <- ts(dat_irl$avg_price, frequency = 1)

ggplot(dat_irl, aes(x = arrival_date, y = avg_price)) +
  geom_line(color = "#4dac26", linewidth = 0.6) +
  geom_smooth(method = "loess", span = 0.3, se = FALSE, color = "#984ea3") +
  geom_hline(yintercept = mean(dat_irl$avg_price), linetype = "dashed", color = "gray50") +
  scale_x_date(date_labels = "%b %Y", date_breaks = "2 months") +
  labs(
    title = "Time Series Harga Hotel — Ireland (IRL)",
    x = "Tanggal", y = "Rata-Rata Harga (EUR)"
  ) +
  theme_bw() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))
## `geom_smooth()` using formula = 'y ~ x'

4.5.2 STL Decomposition

yt_irl_w  <- ts(dat_irl$avg_price, frequency = 7)
decomp_irl <- stl(yt_irl_w, s.window = "periodic", robust = TRUE)
autoplot(decomp_irl) +
  labs(title = "STL Decomposition — Ireland (IRL)") +
  theme_bw()

4.5.3 Uji Stasioneritas

adf_irl_raw  <- adf.test(yt_irl)
adf_irl_diff <- adf.test(diff(yt_irl))
## Warning in adf.test(diff(yt_irl)): p-value smaller than printed p-value
cat("ADF Test — Data Asli IRL — p-value:",
    round(adf_irl_raw$p.value, 4),
    ifelse(adf_irl_raw$p.value < 0.05, "[STASIONER]", "[TIDAK STASIONER]"), "\n")
## ADF Test — Data Asli IRL — p-value: 0.8808 [TIDAK STASIONER]
cat("ADF Test — Differencing IRL — p-value:",
    round(adf_irl_diff$p.value, 4),
    ifelse(adf_irl_diff$p.value < 0.05, "[STASIONER]", "[TIDAK STASIONER]"), "\n")
## ADF Test — Differencing IRL — p-value: 0.01 [STASIONER]

4.5.4 Plot ACF & PACF

par(mfrow = c(1, 2), mar = c(4, 4, 3, 2))
Acf(diff(yt_irl),  main = "ACF — diff(IRL)",  col = "#4dac26", lag.max = 30)
Pacf(diff(yt_irl), main = "PACF — diff(IRL)", col = "#984ea3", lag.max = 30)

par(mfrow = c(1, 1))

4.5.5 Pemodelan ARIMA — IRL

model_irl <- auto.arima(
  yt_irl,
  stepwise      = FALSE,
  approximation = FALSE,
  ic            = "aic"
)

cat("MODEL TERPILIH : IRELAND ")
## MODEL TERPILIH : IRELAND
summary(model_irl)
## Series: yt_irl 
## ARIMA(2,1,3) 
## 
## Coefficients:
##           ar1      ar2     ma1     ma2      ma3
##       -1.1008  -0.9626  0.2931  0.0585  -0.6898
## s.e.   0.0602   0.0547  0.0957  0.0579   0.0901
## 
## sigma^2 = 797.1:  log likelihood = -1435.63
## AIC=2883.26   AICc=2883.55   BIC=2905.53
## 
## Training set error measures:
##                     ME     RMSE      MAE       MPE     MAPE      MASE
## Training set 0.7308855 27.95227 20.26117 -6.141612 22.02984 0.7492465
##                     ACF1
## Training set -0.02581009
coeftest(model_irl)
## 
## z test of coefficients:
## 
##      Estimate Std. Error  z value  Pr(>|z|)    
## ar1 -1.100807   0.060214 -18.2815 < 2.2e-16 ***
## ar2 -0.962620   0.054659 -17.6112 < 2.2e-16 ***
## ma1  0.293103   0.095675   3.0635  0.002187 ** 
## ma2  0.058492   0.057925   1.0098  0.312599    
## ma3 -0.689786   0.090094  -7.6563 1.914e-14 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

4.5.6 Diagnostik Residual — IRL

par(mfrow = c(2, 2), mar = c(4, 4, 3, 2))
plot(residuals(model_irl), type = "l", col = "#4dac26",
     main = "Residual — IRL", ylab = "Residual")
abline(h = 0, col = "red", lty = 2, lwd = 1.5)
Acf(residuals(model_irl),  main = "ACF Residual — IRL",  col = "#4dac26", lag.max = 30)
Pacf(residuals(model_irl), main = "PACF Residual — IRL", col = "#984ea3", lag.max = 30)
qqnorm(residuals(model_irl), pch = 16, col = "#4dac26", main = "Q-Q Plot — IRL")
qqline(residuals(model_irl), col = "red", lwd = 2)

par(mfrow = c(1, 1))
lb_irl   <- Box.test(residuals(model_irl), lag = 20, type = "Ljung-Box")
mape_irl <- mean(abs((dat_irl$avg_price - as.numeric(fitted(model_irl))) /
                       dat_irl$avg_price) * 100, na.rm = TRUE)
cat("Ljung-Box p-value:", round(lb_irl$p.value, 4),
    ifelse(lb_irl$p.value > 0.05, "[White noise - OK]", "[Ada autokorelasi]"), "\n")
## Ljung-Box p-value: 0.5681 [White noise - OK]
cat("MAPE In-Sample:", round(mape_irl, 2), "%\n")
## MAPE In-Sample: 22.03 %
cat("AIC:", round(model_irl$aic, 2), "| BIC:", round(model_irl$bic, 2), "\n")
## AIC: 2883.26 | BIC: 2905.53

4.6 France (FRA)

4.6.1 Eksplorasi Time Series

dat_fra <- dat_list[["fra"]]
yt_fra  <- ts(dat_fra$avg_price, frequency = 1)

ggplot(dat_fra, aes(x = arrival_date, y = avg_price)) +
  geom_line(color = "#fdae61", linewidth = 0.6) +
  geom_smooth(method = "loess", span = 0.35, se = FALSE, color = "#2c7bb6") +
  geom_hline(yintercept = mean(dat_fra$avg_price), linetype = "dashed", color = "gray50") +
  scale_x_date(date_labels = "%b %Y", date_breaks = "2 months") +
  labs(
    title = "Time Series Harga Hotel — France (FRA)",
    x = "Tanggal", y = "Rata-Rata Harga (EUR)"
  ) +
  theme_bw() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))
## `geom_smooth()` using formula = 'y ~ x'

4.6.2 STL Decomposition

yt_fra_w  <- ts(dat_fra$avg_price, frequency = 7)
decomp_fra <- stl(yt_fra_w, s.window = "periodic", robust = TRUE)
autoplot(decomp_fra) +
  labs(title = "STL Decomposition — France (FRA)") +
  theme_bw()

4.6.3 Uji Stasioneritas

adf_fra_raw  <- adf.test(yt_fra)
adf_fra_diff <- adf.test(diff(yt_fra))
## Warning in adf.test(diff(yt_fra)): p-value smaller than printed p-value
cat("ADF Test — Data Asli FRA — p-value:",
    round(adf_fra_raw$p.value, 4),
    ifelse(adf_fra_raw$p.value < 0.05, "[STASIONER]", "[TIDAK STASIONER]"), "\n")
## ADF Test — Data Asli FRA — p-value: 0.8207 [TIDAK STASIONER]
cat("ADF Test — Differencing FRA — p-value:",
    round(adf_fra_diff$p.value, 4),
    ifelse(adf_fra_diff$p.value < 0.05, "[STASIONER]", "[TIDAK STASIONER]"), "\n")
## ADF Test — Differencing FRA — p-value: 0.01 [STASIONER]

4.6.4 Plot ACF & PACF

par(mfrow = c(1, 2), mar = c(4, 4, 3, 2))
Acf(diff(yt_fra),  main = "ACF — diff(FRA)",  col = "#fdae61", lag.max = 30)
Pacf(diff(yt_fra), main = "PACF — diff(FRA)", col = "#2c7bb6", lag.max = 30)

par(mfrow = c(1, 1))

4.6.5 Pemodelan ARIMA — FRA

model_fra <- auto.arima(
  yt_fra,
  stepwise      = FALSE,
  approximation = FALSE,
  ic            = "aic"
)

cat("MODEL TERPILIH : FRANCE\n")
## MODEL TERPILIH : FRANCE
summary(model_fra)
## Series: yt_fra 
## ARIMA(5,1,0) 
## 
## Coefficients:
##           ar1      ar2      ar3      ar4      ar5
##       -0.7849  -0.5315  -0.4206  -0.3576  -0.1277
## s.e.   0.0578   0.0718   0.0748   0.0721   0.0593
## 
## sigma^2 = 1106:  log likelihood = -1455.25
## AIC=2922.51   AICc=2922.8   BIC=2944.65
## 
## Training set error measures:
##                     ME     RMSE      MAE       MPE     MAPE      MASE
## Training set 0.7998772 32.92437 23.68484 -6.502735 22.82304 0.7846423
##                     ACF1
## Training set 0.003024436
coeftest(model_fra)
## 
## z test of coefficients:
## 
##      Estimate Std. Error  z value  Pr(>|z|)    
## ar1 -0.784915   0.057830 -13.5728 < 2.2e-16 ***
## ar2 -0.531540   0.071758  -7.4074 1.288e-13 ***
## ar3 -0.420645   0.074774  -5.6255 1.849e-08 ***
## ar4 -0.357639   0.072078  -4.9618 6.983e-07 ***
## ar5 -0.127677   0.059301  -2.1530   0.03132 *  
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

4.6.6 Diagnostik Residual — FRA

par(mfrow = c(2, 2), mar = c(4, 4, 3, 2))
plot(residuals(model_fra), type = "l", col = "#fdae61",
     main = "Residual — FRA", ylab = "Residual")
abline(h = 0, col = "red", lty = 2, lwd = 1.5)

Acf(residuals(model_fra),  main = "ACF Residual — FRA",  col = "#fdae61", lag.max = 30)
Pacf(residuals(model_fra), main = "PACF Residual — FRA", col = "#2c7bb6", lag.max = 30)
qqnorm(residuals(model_fra), pch = 16, col = "#fdae61", main = "Q-Q Plot — FRA")
qqline(residuals(model_fra), col = "red", lwd = 2)

par(mfrow = c(1, 1))
lb_fra   <- Box.test(residuals(model_fra), lag = 20, type = "Ljung-Box")
mape_fra <- mean(abs((dat_fra$avg_price - as.numeric(fitted(model_fra))) /
                       dat_fra$avg_price) * 100, na.rm = TRUE)

cat("Ljung-Box p-value:", round(lb_fra$p.value, 4),
    ifelse(lb_fra$p.value > 0.05, "[White noise - OK]", "[Ada autokorelasi]"), "\n")
## Ljung-Box p-value: 0.2452 [White noise - OK]
cat("MAPE In-Sample:", round(mape_fra, 2), "%\n")
## MAPE In-Sample: 22.82 %
cat("AIC:", round(model_fra$aic, 2), "| BIC:", round(model_fra$bic, 2), "\n")
## AIC: 2922.51 | BIC: 2944.65

4.7 Perbandingan & Pemilihan Model

model_list <- list(PRT = model_prt, GBR = model_gbr, IRL = model_irl, FRA = model_fra)
mape_list  <- c(PRT = mape_prt, GBR = mape_gbr, IRL = mape_irl, FRA = mape_fra)
lb_list    <- list(PRT = lb_prt, GBR = lb_gbr, IRL = lb_irl, FRA = lb_fra)

data.frame(
  Negara     = names(model_list),
  Model      = sapply(model_list, function(m) {
    ord <- arimaorder(m)
    paste0("ARIMA(", ord[1], ",", ord[2], ",", ord[3], ")")
  }),
  AIC        = sapply(model_list, function(m) round(m$aic, 2)),
  BIC        = sapply(model_list, function(m) round(m$bic, 2)),
  MAPE       = round(mape_list, 2),
  LjungBox_p = sapply(lb_list, function(lb) round(lb$p.value, 4)),
  Status     = sapply(lb_list, function(lb)
    ifelse(lb$p.value > 0.05, "White noise - OK", "Cek kembali")),
  stringsAsFactors = FALSE
)

##Interpretasi Perbandingan Model:

  • PRT (ARIMA(3,1,2)): Menggunakan model paling kompleks dengan jumlah data terbanyak (424 observasi) serta menghasilkan nilai MAPE terbaik.
  • GBR (ARIMA(2,1,3)): Memiliki pola fluktuasi mingguan yang kuat dan dapat ditangkap dengan baik oleh komponen Moving Average (MA).
  • IRL (ARIMA(2,1,3)): Menggunakan model yang sama dengan GBR dan sesuai untuk data dengan tingkat volatilitas yang relatif tinggi.
  • FRA (ARIMA(0,1,1)): Menggunakan model paling sederhana (parsimonious) dengan jumlah data lebih sedikit (297 observasi) dan pola yang relatif sederhana.

Penggunaan differencing dengan orde d = 1 pada seluruh negara didukung oleh hasil uji Augmented Dickey-Fuller (ADF) yang menunjukkan bahwa seluruh deret waktu bersifat non-stasioner akibat adanya tren, sehingga proses differencing diperlukan sebelum dilakukan pemodelan.

5 Tugas 3 — Prediksi Harga Kamar: September 2017

5.1 Metodologi Forecasting

Data historis setiap negara mencakup hingga 31 Agustus 2017. Forecast dilakukan untuk 30 hari September 2017 (1–30 September) menggunakan model ARIMA terpilih masing-masing negara.

# Fungsi forecast → September 2017
get_september_forecast <- function(model, dat, neg) {
  max_date  <- max(dat$arrival_date)
  h_days    <- as.numeric(as.Date("2017-09-30") - max_date)
  if (h_days <= 0) h_days <- 30

  fc        <- forecast(model, h = h_days, level = c(80, 95))
  fc_dates  <- seq(max_date + 1, by = "day", length.out = h_days)

  data.frame(
    arrival_date = fc_dates,
    forecast     = as.numeric(fc$mean),
    lower80      = as.numeric(fc$lower[, 1]),
    upper80      = as.numeric(fc$upper[, 1]),
    lower95      = as.numeric(fc$lower[, 2]),
    upper95      = as.numeric(fc$upper[, 2]),
    country      = toupper(neg)
  ) %>%
    filter(format(arrival_date, "%Y-%m") == "2017-09")
}

# Jalankan untuk semua negara
fc_prt <- get_september_forecast(model_prt, dat_prt, "prt")
fc_gbr <- get_september_forecast(model_gbr, dat_gbr, "gbr")
fc_irl <- get_september_forecast(model_irl, dat_irl, "irl")
fc_fra <- get_september_forecast(model_fra, dat_fra, "fra")

# Gabungkan semua
all_fc <- bind_rows(fc_prt, fc_gbr, fc_irl, fc_fra)

cat("Total baris forecast:", nrow(all_fc), "\n")
## Total baris forecast: 120
cat("Negara:", paste(unique(all_fc$country), collapse = ", "), "\n")
## Negara: PRT, GBR, IRL, FRA

5.2 Visualisasi Forecast — Per Negara

last30_all <- bind_rows(
  dat_prt %>% tail(30) %>% mutate(country = "PRT"),
  dat_gbr %>% tail(30) %>% mutate(country = "GBR"),
  dat_irl %>% tail(30) %>% mutate(country = "IRL"),
  dat_fra %>% tail(30) %>% mutate(country = "FRA")
)

country_colors <- c(PRT = "#2c7bb6", GBR = "#d73027", IRL = "#4dac26", FRA = "#fdae61")

ggplot() +
  geom_ribbon(data = all_fc,
              aes(x = arrival_date, ymin = lower95, ymax = upper95, fill = country),
              alpha = 0.18) +
  geom_ribbon(data = all_fc,
              aes(x = arrival_date, ymin = lower80, ymax = upper80, fill = country),
              alpha = 0.30) +
  geom_line(data = last30_all,
            aes(x = arrival_date, y = avg_price),
            color = "black", linewidth = 0.9) +
  geom_line(data = all_fc,
            aes(x = arrival_date, y = forecast, color = country),
            linewidth = 1.1) +
  geom_vline(xintercept = as.numeric(as.Date("2017-09-01") - 0.5),
             linetype = "dashed", color = "gray40") +
  facet_wrap(~ country, ncol = 2, scales = "free_y",
             labeller = labeller(country = c(
               PRT = "Portugal (PRT)", GBR = "United Kingdom (GBR)",
               IRL = "Ireland (IRL)",  FRA = "France (FRA)"
             ))) +
  scale_color_manual(values = country_colors) +
  scale_fill_manual(values  = country_colors) +
  scale_x_date(date_labels = "%d %b", date_breaks = "1 week") +
  labs(
    title = "Prediksi Harga Hotel — September 2017",
    x     = "Tanggal",
    y     = "Harga Kamar (EUR)"
  ) +
  theme_bw() +
  theme(
    legend.position = "none",
    axis.text.x     = element_text(angle = 45, hjust = 1)
  )
## Warning in scale_x_date(date_labels = "%d %b", date_breaks = "1 week"): A <numeric> value was passed to a Date scale.
## ℹ The value was converted to a <Date> object.

5.3 Tabel Prediksi Harian — September 2017

Interpretasi Prediksi September 2017:

  • France (FRA): memiliki prediksi rata-rata tertinggi sebesar €218.31. Model ARIMA(0,1,1) menghasilkan forecast yang relatif konstan.

  • Portugal (PRT): memiliki prediksi rata-rata sebesar €194.16 dengan pola yang relatif stabil pada rentang €193–195 serta sedikit penurunan pada akhir bulan.

  • Ireland (IRL): memiliki prediksi rata-rata sebesar €169.27 dengan pola fluktuasi mingguan (weekly oscillation) yang terlihat jelas dan berhasil ditangkap oleh model ARIMA(2,1,3).

  • United Kingdom (GBR): memiliki prediksi rata-rata terendah sebesar €143.17 dengan fluktuasi harian yang cukup tinggi, berada pada rentang €133–152.

Catatan FRA: model ARIMA(0,1,1) menghasilkan prediksi konstan karena hanya melibatkan komponen MA(1), yang mengindikasikan bahwa data France cenderung menyerupai random walk setelah proses differencing. Interval kepercayaan yang lebar menunjukkan tingkat ketidakpastian forecast yang relatif tinggi.

5.3.1 Portugal (PRT)

fc_prt %>%
  mutate(across(where(is.numeric), ~ round(.x, 2))) %>%
  rename(
    Tanggal        = arrival_date,
    `Forecast (€)` = forecast,
    `Lower 80%`    = lower80,
    `Upper 80%`    = upper80,
    `Lower 95%`    = lower95,
    `Upper 95%`    = upper95
  ) %>%
  select(-country) %>%
  kable(caption = "Prediksi Harga Harian September 2017 — Portugal (PRT)") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Prediksi Harga Harian September 2017 — Portugal (PRT)
Tanggal Forecast (€) Lower 80% Upper 80% Lower 95% Upper 95%
2017-09-01 193.65 167.16 220.14 153.13 234.17
2017-09-02 192.87 163.38 222.35 147.78 237.96
2017-09-03 193.43 161.29 225.57 144.27 242.59
2017-09-04 194.23 161.19 227.27 143.70 244.76
2017-09-05 194.91 160.78 229.05 142.71 247.12
2017-09-06 194.98 159.18 230.78 140.23 249.73
2017-09-07 194.56 156.17 232.95 135.85 253.27
2017-09-08 194.00 152.73 235.27 130.89 257.12
2017-09-09 193.70 149.98 237.42 126.84 260.56
2017-09-10 193.79 148.32 239.25 124.26 263.31
2017-09-11 194.12 147.39 240.86 122.65 265.60
2017-09-12 194.45 146.55 242.34 121.20 267.70
2017-09-13 194.56 145.33 243.78 119.28 269.83
2017-09-14 194.42 143.61 245.24 116.71 272.14
2017-09-15 194.19 141.62 246.75 113.79 274.58
2017-09-16 194.02 139.78 248.25 111.07 276.96
2017-09-17 194.01 138.32 249.69 108.85 279.17
2017-09-18 194.13 137.20 251.06 107.07 281.19
2017-09-19 194.28 136.20 252.36 105.46 283.11
2017-09-20 194.36 135.10 253.61 103.73 284.98
2017-09-21 194.33 133.81 254.85 101.77 286.88
2017-09-22 194.23 132.39 256.07 99.65 288.81
2017-09-23 194.14 130.99 257.30 97.56 290.73
2017-09-24 194.12 129.74 258.51 95.65 292.59
2017-09-25 194.16 128.63 259.69 93.95 294.38
2017-09-26 194.23 127.61 260.84 92.35 296.10
2017-09-27 194.27 126.58 261.96 90.75 297.79
2017-09-28 194.27 125.48 263.05 89.07 299.47
2017-09-29 194.23 124.33 264.14 87.33 301.14
2017-09-30 194.19 123.18 265.21 85.59 302.80

5.3.2 United Kingdom (GBR)

fc_gbr %>%
  mutate(across(where(is.numeric), ~ round(.x, 2))) %>%
  rename(
    Tanggal        = arrival_date,
    `Forecast (€)` = forecast,
    `Lower 80%`    = lower80,
    `Upper 80%`    = upper80,
    `Lower 95%`    = lower95,
    `Upper 95%`    = upper95
  ) %>%
  select(-country) %>%
  kable(caption = "Prediksi Harga Harian September 2017 — Portugal (PRT)") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Prediksi Harga Harian September 2017 — Portugal (PRT)
Tanggal Forecast (€) Lower 80% Upper 80% Lower 95% Upper 95%
2017-09-01 133.72 106.03 161.41 91.37 176.06
2017-09-02 151.86 123.11 180.62 107.89 195.84
2017-09-03 140.63 111.50 169.75 96.09 185.17
2017-09-04 139.29 109.24 169.34 93.33 185.25
2017-09-05 151.80 121.06 182.53 104.79 198.81
2017-09-06 135.46 104.29 166.63 87.79 183.13
2017-09-07 146.63 114.48 178.78 97.46 195.80
2017-09-08 146.35 113.77 178.93 96.52 196.18
2017-09-09 136.18 102.99 169.38 85.41 186.95
2017-09-10 150.76 116.77 184.75 98.78 202.74
2017-09-11 139.85 105.50 174.20 87.32 192.39
2017-09-12 141.42 106.29 176.56 87.69 195.15
2017-09-13 149.52 113.85 185.19 94.97 204.08
2017-09-14 136.64 100.53 172.74 81.42 191.86
2017-09-15 147.12 110.23 184.00 90.70 203.53
2017-09-16 144.55 107.28 181.82 87.55 201.55
2017-09-17 138.26 100.41 176.11 80.37 196.14
2017-09-18 149.54 111.06 188.02 90.69 208.39
2017-09-19 139.61 100.77 178.44 80.21 199.00
2017-09-20 142.92 103.41 182.44 82.49 203.36
2017-09-21 147.65 107.68 187.62 86.52 208.77
2017-09-22 137.86 97.46 178.27 76.07 199.65
2017-09-23 147.17 106.12 188.22 84.39 209.95
2017-09-24 143.32 101.91 184.73 79.99 206.65
2017-09-25 139.94 97.99 181.89 75.78 204.10
2017-09-26 148.33 105.86 190.81 83.37 213.29
2017-09-27 139.71 96.88 182.55 74.20 205.22
2017-09-28 143.91 100.48 187.34 77.49 210.33
2017-09-29 146.15 102.32 189.98 79.11 213.19
2017-09-30 139.03 94.77 183.29 71.34 206.72

5.3.3 Ireland (IRL)

fc_irl %>%
  mutate(across(where(is.numeric), ~ round(.x, 2))) %>%
  rename(
    Tanggal        = arrival_date,
    `Forecast (€)` = forecast,
    `Lower 80%`    = lower80,
    `Upper 80%`    = upper80,
    `Lower 95%`    = lower95,
    `Upper 95%`    = upper95
  ) %>%
  select(-country) %>%
  kable(caption = "Prediksi Harga Harian September 2017 — Portugal (PRT)") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Prediksi Harga Harian September 2017 — Portugal (PRT)
Tanggal Forecast (€) Lower 80% Upper 80% Lower 95% Upper 95%
2017-09-01 157.72 121.54 193.90 102.39 213.06
2017-09-02 176.09 139.25 212.94 119.74 232.44
2017-09-03 173.83 136.43 211.23 116.63 231.03
2017-09-04 158.64 119.87 197.40 99.36 217.92
2017-09-05 177.54 138.23 216.85 117.42 237.66
2017-09-06 171.36 131.44 211.27 110.31 232.41
2017-09-07 159.97 118.81 201.12 97.03 222.91
2017-09-08 178.46 136.83 220.09 114.79 242.12
2017-09-09 169.07 126.78 211.35 104.40 233.73
2017-09-10 161.60 118.21 205.00 95.24 227.97
2017-09-11 178.86 135.04 222.68 111.85 245.87
2017-09-12 167.05 122.53 211.57 98.96 235.13
2017-09-13 163.44 117.93 208.95 93.84 233.04
2017-09-14 178.78 132.88 224.68 108.59 248.98
2017-09-15 165.37 118.73 212.01 94.04 236.70
2017-09-16 165.37 117.85 212.88 92.70 238.03
2017-09-17 178.28 130.39 226.17 105.04 251.52
2017-09-18 164.07 115.41 212.72 89.65 238.48
2017-09-19 167.28 117.85 216.71 91.68 242.88
2017-09-20 177.42 127.62 227.23 101.26 253.59
2017-09-21 163.16 112.57 213.75 85.79 240.53
2017-09-22 169.10 117.83 220.37 90.69 247.51
2017-09-23 176.29 124.65 227.94 97.31 255.28
2017-09-24 162.66 110.22 215.10 82.46 242.86
2017-09-25 170.74 117.70 223.78 89.63 251.85
2017-09-26 174.97 121.54 228.40 93.26 256.68
2017-09-27 162.54 108.32 216.75 79.63 245.45
2017-09-28 172.15 117.41 226.90 88.43 255.88
2017-09-29 173.53 118.38 228.69 89.18 257.89
2017-09-30 162.76 106.84 218.68 77.23 248.28

5.3.4 France (FRA)

fc_fra %>%
  mutate(across(where(is.numeric), ~ round(.x, 2))) %>%
  rename(
    Tanggal        = arrival_date,
    `Forecast (€)` = forecast,
    `Lower 80%`    = lower80,
    `Upper 80%`    = upper80,
    `Lower 95%`    = lower95,
    `Upper 95%`    = upper95
  ) %>%
  select(-country) %>%
  kable(caption = "Prediksi Harga Harian September 2017 — Portugal (PRT)") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Prediksi Harga Harian September 2017 — Portugal (PRT)
Tanggal Forecast (€) Lower 80% Upper 80% Lower 95% Upper 95%
2017-09-01 224.79 168.01 281.57 137.95 311.62
2017-09-02 224.65 166.36 282.94 135.51 313.80
2017-09-03 224.14 164.25 284.03 132.55 315.73
2017-09-04 223.72 162.35 285.09 129.86 317.58
2017-09-05 223.61 160.83 286.40 127.59 319.64
2017-09-06 223.95 159.82 288.08 125.88 322.03
2017-09-07 224.12 158.67 289.57 124.02 324.22
2017-09-08 224.07 157.28 290.86 121.93 326.21
2017-09-09 223.97 155.87 292.07 119.82 328.12
2017-09-10 223.90 154.52 293.27 117.79 330.00
2017-09-11 223.92 153.30 294.55 115.91 331.93
2017-09-12 223.98 152.13 295.82 114.10 333.86
2017-09-13 223.99 150.94 297.05 112.27 335.72
2017-09-14 223.98 149.74 298.22 110.44 337.52
2017-09-15 223.96 148.55 299.37 108.62 339.29
2017-09-16 223.95 147.39 300.52 106.86 341.05
2017-09-17 223.96 146.26 301.66 105.13 342.79
2017-09-18 223.97 145.15 302.79 103.43 344.51
2017-09-19 223.97 144.05 303.89 101.75 346.19
2017-09-20 223.97 142.96 304.97 100.08 347.85
2017-09-21 223.96 141.89 306.04 98.44 349.49
2017-09-22 223.96 140.83 307.10 96.82 351.11
2017-09-23 223.97 139.78 308.15 95.22 352.71
2017-09-24 223.97 138.75 309.18 93.64 354.29
2017-09-25 223.97 137.73 310.20 92.08 355.86
2017-09-26 223.97 136.72 311.21 90.53 357.40
2017-09-27 223.97 135.72 312.21 89.01 358.92
2017-09-28 223.97 134.74 313.20 87.50 360.43
2017-09-29 223.97 133.76 314.17 86.01 361.92
2017-09-30 223.97 132.80 315.14 84.54 363.40

5.4 Ringkasan Prediksi September 2017

# Ringkasan agregat per negara
summary_fc <- all_fc %>%
  group_by(country) %>%
  summarise(
    `Rata-Rata (€)` = round(mean(forecast), 2),
    `Min (€)`       = round(min(forecast), 2),
    `Max (€)`       = round(max(forecast), 2),
    `CI 95% Bawah`  = round(mean(lower95), 2),
    `CI 95% Atas`   = round(mean(upper95), 2),
    `Lebar CI 95%`  = round(mean(upper95 - lower95), 2),
    .groups         = "drop"
  ) %>%
  mutate(Model = case_when(
    country == "PRT" ~ "ARIMA(3,1,2)",
    country == "GBR" ~ "ARIMA(2,1,3)",
    country == "IRL" ~ "ARIMA(2,1,3)",
    country == "FRA" ~ "ARIMA(0,1,1)"
  )) %>%
  select(country, Model, everything())

summary_fc %>%
  kable(caption = "Ringkasan Prediksi Harga Hotel September 2017 — 4 Negara") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Ringkasan Prediksi Harga Hotel September 2017 — 4 Negara
country Model Rata-Rata (€) Min (€) Max (€) CI 95% Bawah CI 95% Atas Lebar CI 95%
FRA ARIMA(0,1,1) 224.01 223.61 224.79 108.84 339.17 230.33
GBR ARIMA(2,1,3) 143.17 133.72 151.86 87.04 199.31 112.27
IRL ARIMA(2,1,3) 169.27 157.72 178.86 97.95 240.59 142.64
PRT ARIMA(3,1,2) 194.16 192.87 194.98 115.10 273.22 158.12

5.5 Visualisasi Perbandingan Forecast Antar Negara

all_fc %>%
  ggplot(aes(x = arrival_date, y = forecast, color = country, fill = country)) +
  geom_ribbon(aes(ymin = lower80, ymax = upper80), alpha = 0.2, color = NA) +
  geom_line(size = 1.2) +
  geom_text(
    data = all_fc %>% filter(arrival_date == as.Date("2017-09-15")),
    aes(label = paste0(country, "\n€", round(forecast, 0))),
    size = 3.5, fontface = "bold",
    nudge_y = c(5, -8, 5, -8)
  ) +
  scale_color_manual(values = country_colors) +
  scale_fill_manual(values  = country_colors) +
  scale_x_date(date_labels = "%d %b", date_breaks = "1 week") +
  scale_y_continuous(labels = dollar_format(prefix = "€")) +
  labs(
    title    = "Perbandingan Prediksi Harga Hotel September 2017 - 4 Negara",
    subtitle = "Pita = confidence interval 80% | France memiliki prediksi tertinggi",
    x = "Tanggal", y = "Prediksi Harga (EUR)", color = "Negara", fill = "Negara"
  ) +
  theme_bw(base_size = 12) +
  theme(
    plot.title    = element_text(face = "bold", size = 13),
    legend.position = "bottom"
  )

Interpretasi Prediksi September 2017:

  • France (FRA): Memiliki prediksi rata-rata tertinggi sebesar €218.31. Model ARIMA(0,1,1) menghasilkan forecast yang relatif konstan.

  • Portugal (PRT): Memiliki prediksi rata-rata sebesar €194.16 dengan pola yang relatif stabil pada rentang €193–195 serta sedikit penurunan di akhir bulan.

  • Ireland (IRL): Memiliki prediksi rata-rata sebesar €169.27 dengan pola weekly oscillation yang terlihat jelas dan berhasil ditangkap oleh model ARIMA(2,1,3).

  • United Kingdom (GBR): Memiliki prediksi rata-rata terendah sebesar €143.17 dengan fluktuasi harian yang cukup tinggi, berada pada rentang €133–152.

Note FRA: Model ARIMA(0,1,1) menghasilkan prediksi konstan karena hanya melibatkan komponen MA(1), yang mengindikasikan bahwa data France cenderung menyerupai random walk setelah proses differencing. Interval kepercayaan yang lebar menunjukkan tingkat ketidakpastian forecast yang relatif tinggi.


6 Kesimpulan & Rekomendasi Bisnis

6.1 Ringkasan Temuan

data.frame(
  Aspek = c(
    "5 Negara Terbanyak",
    "Model PRT",
    "Model GBR",
    "Model IRL",
    "Model FRA",
    "Prediksi Sept 2017 Tertinggi",
    "Prediksi Sept 2017 Terendah",
    "Pola Musiman"
  ),
  Temuan = c(
    "PRT (4869), GBR (3465), ESP (1449), IRL (934), FRA (798)",
    "ARIMA(3,1,2) — AIC: 3770.6, MAPE: 16.3%",
    "ARIMA(2,1,3) — AIC: 3728.0, MAPE: 17.2%",
    "ARIMA(2,1,3) — AIC: 2883.3, MAPE: 22.0%",
    "ARIMA(0,1,1) — AIC: 2922.5, MAPE: 22.9%",
    "France (FRA) — €218.31/malam",
    "United Kingdom (GBR) — €143.17/malam",
    "Semua negara: harga naik Juli–Agustus (peak season)"
  )
) %>%
  kable(caption = "Ringkasan Seluruh Temuan Analisis") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = TRUE)
Ringkasan Seluruh Temuan Analisis
Aspek Temuan
5 Negara Terbanyak PRT (4869), GBR (3465), ESP (1449), IRL (934), FRA (798)
Model PRT ARIMA(3,1,2) — AIC: 3770.6, MAPE: 16.3%
Model GBR ARIMA(2,1,3) — AIC: 3728.0, MAPE: 17.2%
Model IRL ARIMA(2,1,3) — AIC: 2883.3, MAPE: 22.0%
Model FRA ARIMA(0,1,1) — AIC: 2922.5, MAPE: 22.9%
Prediksi Sept 2017 Tertinggi France (FRA) — €218.31/malam
Prediksi Sept 2017 Terendah United Kingdom (GBR) — €143.17/malam
Pola Musiman Semua negara: harga naik Juli–Agustus (peak season)

6.2 Rekomendasi Bisnis

  1. Dynamic Pricing untuk September 2017 Harga kamar yang disarankan berdasarkan hasil prediksi ARIMA adalah: Portugal €190–195, United Kingdom €133–152, Ireland €158–179, dan France €218. Penyesuaian harga disarankan apabila permintaan aktual menyimpang lebih dari 15% dari nilai forecast.

  2. Strategi per Market Segment Segmen online travel agent mendominasi pemesanan di seluruh negara sehingga perlu diprioritaskan dalam alokasi kamar dan promosi digital. Segmen direct menghasilkan harga yang relatif lebih tinggi sehingga dapat ditingkatkan melalui program loyalitas. Sementara itu, segmen corporate cenderung stabil dan dapat dijadikan sumber pendapatan yang konsisten.

  3. Keterbatasan Model France Model ARIMA(0,1,1) untuk France bersifat sangat sederhana (parsimonious) dan menghasilkan interval kepercayaan yang lebar. Hal ini kemungkinan dipengaruhi oleh jumlah observasi yang lebih sedikit (297 observasi) serta pola data yang mendekati random walk. Pengumpulan data tambahan atau penggunaan variabel eksogen dapat dipertimbangkan untuk meningkatkan performa model.

  4. Pengembangan Model Lanjutan Pengembangan model dapat dilakukan melalui penggunaan SARIMA untuk menangkap pola seasonality mingguan secara eksplisit, ETS (Error-Trend-Seasonal) sebagai alternatif pada data dengan volatilitas tinggi, serta ARIMAX untuk mengintegrasikan variabel eksternal seperti kalender event dan data kompetitor.