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:
market_segment?## Warning: package 'modeldata' was built under R version 4.4.3
## 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
## Warning: package 'ggplot2' was built under R version 4.4.3
## Warning: package 'forecast' was built under R version 4.4.3
## Warning: package 'tseries' was built under R version 4.4.3
## Registered S3 method overwritten by 'quantmod':
## method from
## as.zoo.data.frame zoo
## 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
## Warning: package 'scales' was built under R version 4.4.3
## Warning: package 'kableExtra' was built under R version 4.4.3
##
## Attaching package: 'kableExtra'
## The following object is masked from 'package:dplyr':
##
## group_rows
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
## Missing values : 0
# Gambaran singkat variabel kunci
hotel_rates %>%
select(arrival_date, country, market_segment, avg_price_per_room, historical_adr) %>%
head(8)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)| 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.
dat_top5 <- hotel_rates %>%
filter(country %in% top5_vec)
cat("Data setelah filter:", nrow(dat_top5), "baris\n")## Data setelah filter: 11515 baris
## Distribusi market_segment:
##
## corporate direct groups
## 795 2467 1301
## offline_travel_agent online_travel_agent
## 2238 4714
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
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.
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.
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))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).
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
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))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'
# 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.
## Warning in adf.test(diff(yt_prt)): p-value smaller than printed p-value
## ADF Test — Data Asli PRT:
## 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]
## ADF Test — Setelah Differencing (d=1) PRT:
## 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]
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)# 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
## 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
## Uji Signifikansi Parameter (Uji-t)
##
## 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
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)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]
## MAPE In-Sample: 16.31 %
## AIC: 3770.6 | BIC: 3794.88
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'
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()## Warning in adf.test(diff(yt_gbr)): p-value smaller than printed p-value
## 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]
## 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]
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)model_gbr <- auto.arima(
yt_gbr,
stepwise = FALSE,
approximation = FALSE,
ic = "aic"
)
cat("MODEL TERPILIH : UNITED KINGDOM \n")## MODEL TERPILIH : UNITED KINGDOM
## 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
##
## 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
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)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]
## MAPE In-Sample: 17.24 %
## AIC: 3727.99 | BIC: 3752.14
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'
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()## 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]
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)model_irl <- auto.arima(
yt_irl,
stepwise = FALSE,
approximation = FALSE,
ic = "aic"
)
cat("MODEL TERPILIH : IRELAND ")## MODEL TERPILIH : IRELAND
## 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
##
## 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
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)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]
## MAPE In-Sample: 22.03 %
## AIC: 2883.26 | BIC: 2905.53
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'
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()## 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]
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)model_fra <- auto.arima(
yt_fra,
stepwise = FALSE,
approximation = FALSE,
ic = "aic"
)
cat("MODEL TERPILIH : FRANCE\n")## MODEL TERPILIH : FRANCE
## 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
##
## 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
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)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]
## MAPE In-Sample: 22.82 %
## AIC: 2922.51 | BIC: 2944.65
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:
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.
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
## Negara: PRT, GBR, IRL, FRA
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.
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.
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)| 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 |
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)| 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 |
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)| 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 |
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)| 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 |
# 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)| 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 |
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.
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)| 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) |
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.
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.
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.
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.