# -----------------------------------------------------------
# Install paket yang diperlukan (jika belum terpasang)
# -----------------------------------------------------------
pkgs <- c("readxl","forecast","ggplot2","dplyr","tidyr",
"zoo","knitr","kableExtra","scales","patchwork")
new_pkgs <- pkgs[!sapply(pkgs, requireNamespace, quietly=TRUE)]
if (length(new_pkgs)) install.packages(new_pkgs)
library(readxl); library(forecast); library(ggplot2)
library(dplyr); library(tidyr); library(zoo)
library(knitr); library(kableExtra); library(scales)
library(patchwork)
theme_set(theme_bw(base_size = 11))# ================================================================
# SESUAIKAN PATH FILE EXCEL DI BAWAH INI
# ================================================================
FILE_PATH <- "C:/Users/ADVAN/Downloads/harga_cpo (2).xlsx" # letakkan file xlsx di working directory
SHEET_NUM <- 3 # "Rata-rata Bulanan"
N_TEST <- 20 # jumlah bulan data uji
# ----------------------------------------------------------------
# Fungsi parse angka format Indonesia
# (titik = pemisah ribuan, koma = desimal)
# ----------------------------------------------------------------
parse_indo <- function(x) {
if (is.character(x)) {
x <- trimws(x)
x <- gsub("\\.", "", x) # hapus pemisah ribuan
x <- gsub(",", ".", x) # ganti koma desimal → titik
}
as.numeric(x)
}
# ----------------------------------------------------------------
# Fungsi tambah 1 bulan ke objek Date (tanpa lubridate)
# ----------------------------------------------------------------
add_month <- function(d, n = 1) {
m <- as.integer(format(d, "%m")) + n
y <- as.integer(format(d, "%Y"))
while (m > 12) { m <- m - 12; y <- y + 1 }
while (m < 1) { m <- m + 12; y <- y - 1 }
as.Date(sprintf("%04d-%02d-01", y, m))
}
# ----------------------------------------------------------------
# Muat data
# ----------------------------------------------------------------
df_raw <- read_excel(FILE_PATH, sheet = SHEET_NUM)
colnames(df_raw) <- c("Bulan", "Rotterdam", "MDEX", "ICDX")
df_raw <- df_raw %>% mutate(across(c(Rotterdam, MDEX, ICDX), parse_indo))
# Data di sheet urut terbaru → terlama, balik ke kronologis
df <- df_raw[nrow(df_raw):1, ]
# Parse nama bulan Indonesia → Date
bln_map <- c(
Januari="01",Februari="02",Maret="03",April="04",
Mei="05",Juni="06",Juli="07",Agustus="08",
September="09",Oktober="10",November="11",Desember="12"
)
parse_bulan <- function(s) {
parts <- strsplit(trimws(s), " ")[[1]]
as.Date(sprintf("%s-%s-01", parts[2], bln_map[parts[1]]))
}
df$Tanggal <- as.Date(sapply(df$Bulan, parse_bulan))
df <- df[, c("Tanggal","Rotterdam","MDEX","ICDX")]
cat("Rentang data:", format(min(df$Tanggal),"%B %Y"),
"hingga", format(max(df$Tanggal),"%B %Y"), "\n")## Rentang data: December 2019 hingga May 2026
## Total observasi: 78 bulan
# ----------------------------------------------------------------
# Penanganan outlier Rotterdam (nilai ~6909 pada Jun 2024)
# ----------------------------------------------------------------
out_idx <- which(df$Rotterdam > 5000)
if (length(out_idx) > 0) {
cat("\n⚠️ Outlier terdeteksi – Rotterdam",
format(df$Tanggal[out_idx], "%B %Y"),
"| Nilai asli:", round(df$Rotterdam[out_idx], 2),
"→ diganti interpolasi linear\n")
df$Rotterdam[out_idx] <- NA
df$Rotterdam <- as.numeric(na.approx(df$Rotterdam, na.rm = FALSE))
}##
## ⚠️ Outlier terdeteksi – Rotterdam June 2024 | Nilai asli: 10154375 → diganti interpolasi linear
# ----------------------------------------------------------------
# Split train / test
# ----------------------------------------------------------------
n_total <- nrow(df)
n_train <- n_total - N_TEST
df_train <- df[1:n_train, ]
df_test <- df[(n_train + 1):n_total, ]
cat("\nData latih:", format(min(df_train$Tanggal),"%B %Y"),
"–", format(max(df_train$Tanggal),"%B %Y"),
"(", n_train, "bulan)\n")##
## Data latih: December 2019 – September 2024 ( 58 bulan)
cat("Data uji :", format(min(df_test$Tanggal),"%B %Y"),
"–", format(max(df_test$Tanggal),"%B %Y"),
"(", N_TEST, "bulan)\n")## Data uji : October 2024 – May 2026 ( 20 bulan)
df %>% head(5) %>%
kbl(caption = "5 Observasi Pertama (Desember 2019 – ...)") %>%
kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE)| Tanggal | Rotterdam | MDEX | ICDX |
|---|---|---|---|
| 2019-12-01 | 665.00 | 602.74 | 597.58 |
| 2020-01-01 | 735.88 | 652.47 | 640.86 |
| 2020-02-01 | 840.62 | 740.26 | 771.11 |
| 2020-03-01 | 761.47 | 676.66 | 717.23 |
| 2020-04-01 | 644.26 | 569.75 | 590.41 |
df_long <- df %>%
pivot_longer(c(Rotterdam,MDEX,ICDX), names_to="Pasar", values_to="Harga") %>%
mutate(Pasar = factor(Pasar, levels=c("Rotterdam","MDEX","ICDX")))
label_pasar <- c(
Rotterdam = "Rotterdam (EU CIF, USD/ton)",
MDEX = "MDEX Malaysia (USD/ton)",
ICDX = "ICDX Indonesia (USD/ton)"
)
ggplot(df_long, aes(x=Tanggal, y=Harga, color=Pasar)) +
geom_line(linewidth = 0.85) +
geom_point(size = 1.1) +
geom_vline(xintercept = as.numeric(df_test$Tanggal[1]),
linetype="dashed", color="firebrick", linewidth=0.7) +
annotate("text", x=df_test$Tanggal[1], y=Inf,
label=" Data Uji →", hjust=0, vjust=1.5, color="firebrick", size=3.5) +
facet_wrap(~Pasar, ncol=1, scales="free_y",
labeller=labeller(Pasar=label_pasar)) +
scale_color_manual(values=c(Rotterdam="#1f77b4",MDEX="#d62728",ICDX="#2ca02c")) +
labs(title="Harga Rata-rata Bulanan CPO – Tiga Pasar Internasional",
x=NULL, y="Harga (USD/ton)") +
theme(legend.position="none", strip.text=element_text(face="bold"))# ----------------------------------------------------------------
# Metrik kesalahan
# ----------------------------------------------------------------
calc_metrics <- function(actual, predicted) {
valid <- !is.na(actual) & !is.na(predicted)
e <- actual[valid] - predicted[valid]
c(RMSE = sqrt(mean(e^2)),
MSE = mean(e^2),
MAPE = mean(abs(e / actual[valid])) * 100)
}
# ----------------------------------------------------------------
# Uji Ljung-Box (H0: tidak ada autokorelasi)
# Lolos jika p-value > 0.05
# ----------------------------------------------------------------
uji_lb <- function(resid, model_name = "", lag = NULL) {
r <- na.omit(resid)
if (is.null(lag)) lag <- min(10, max(1, floor(length(r)/5)))
if (length(r) <= lag) lag <- max(1, length(r) - 1)
bt <- Box.test(r, lag=lag, type="Ljung-Box")
lolos <- bt$p.value > 0.05
cat(sprintf(" %-35s | p = %.4f | %s\n",
model_name, bt$p.value,
ifelse(lolos, "✅ Tidak ada autokorelasi",
"❌ Ada autokorelasi (residual tidak acak)")))
list(p_value = bt$p.value, lolos = lolos)
}
# ----------------------------------------------------------------
# Double Moving Average
# ----------------------------------------------------------------
dma_compute <- function(y, m) {
ma1 <- as.numeric(stats::filter(y, rep(1/m, m), sides=1))
ma2 <- as.numeric(stats::filter(ma1, rep(1/m, m), sides=1))
list(ma1=ma1, ma2=ma2, m=m, y=y)
}
dma_forecast_h <- function(obj, h) {
valid <- which(!is.na(obj$ma1) & !is.na(obj$ma2))
if (!length(valid)) return(rep(NA, h))
T_v <- tail(valid, 1)
a <- 2*obj$ma1[T_v] - obj$ma2[T_v]
b <- (2/(obj$m - 1)) * (obj$ma1[T_v] - obj$ma2[T_v])
a + b * seq_len(h)
}
dma_residuals <- function(y, m) {
obj <- dma_compute(y, m)
n <- length(y)
# Fitted 1-step: a_{t-1} + b_{t-1}
a_vec <- 2*obj$ma1 - obj$ma2
b_vec <- (2/(m-1)) * (obj$ma1 - obj$ma2)
fitted_vals <- c(NA, head(a_vec + b_vec, n - 1))
y - fitted_vals
}
# ----------------------------------------------------------------
# Rolling one-step-ahead forecast (level)
# fn_pred(y_available) → angka prediksi satu langkah
# ----------------------------------------------------------------
rolling_os <- function(y_train, y_test, fn_pred) {
n <- length(y_test)
p <- numeric(n)
for (i in seq_len(n)) {
y_now <- c(y_train, if (i > 1) y_test[1:(i-1)] else NULL)
p[i] <- tryCatch(fn_pred(y_now), error=function(e) NA_real_)
}
p
}
# ----------------------------------------------------------------
# ts() dari vektor + tanggal awal
# ----------------------------------------------------------------
make_ts <- function(y, d_start) {
ts(y,
start = c(as.integer(format(d_start,"%Y")),
as.integer(format(d_start,"%m"))),
frequency = 12)
}Model yang dibangun di bagian ini langsung pada level harga (tanpa diferensiasi):
MARKETS <- c("Rotterdam","MDEX","ICDX")
results <- list() # kumpulkan semua hasil
for (mkt in MARKETS) {
cat("\n\n## Pasar:", mkt, "{.tabset}\n\n")
y_train <- df_train[[mkt]]
y_test <- df_test[[mkt]]
n_tr <- length(y_train)
ts_train <- make_ts(y_train, df_train$Tanggal[1])
res_mkt <- list()
cat("### Uji Diagnostik\n\n")
cat("**Uji Ljung-Box pada residual fitted values (p > 0,05 = lolos)**\n\n```\n")
# ============================================================
## Naive Simple
# ============================================================
ns_fit <- naive(ts_train, h=N_TEST)
ns_resid <- residuals(ns_fit)
ns_diag <- uji_lb(ns_resid, "Naive Simple")
ns_os <- rolling_os(y_train, y_test, function(y) tail(y,1))
res_mkt[["Naive Simple"]] <- list(
lolos = ns_diag$lolos,
resid = as.numeric(ns_resid),
fcast_ms = as.numeric(ns_fit$mean),
fcast_os = ns_os
)
# ============================================================
## Naive Trend
# ============================================================
nt_fitted <- c(NA, NA, y_train[1:(n_tr-2)] + diff(y_train, differences=1)[1:(n_tr-2)])
# Koreksi: Y_hat_{t} = 2*Y_{t-1} - Y_{t-2}
nt_fitted2 <- c(NA, NA, 2*y_train[2:(n_tr-1)] - y_train[1:(n_tr-2)])
nt_resid <- y_train - nt_fitted2
nt_diag <- uji_lb(nt_resid, "Naive Trend")
slope_tr <- y_train[n_tr] - y_train[n_tr-1]
nt_fcast_ms <- y_train[n_tr] + slope_tr * seq_len(N_TEST)
nt_os <- rolling_os(y_train, y_test, function(y) {
n <- length(y)
if (n < 2) y[n] else 2*y[n] - y[n-1]
})
res_mkt[["Naive Trend"]] <- list(
lolos = nt_diag$lolos,
resid = nt_resid,
fcast_ms = nt_fcast_ms,
fcast_os = nt_os
)
# ============================================================
## Naive Drift
# ============================================================
nd_fit <- rwf(ts_train, h=N_TEST, drift=TRUE)
nd_resid <- residuals(nd_fit)
nd_diag <- uji_lb(nd_resid, "Naive Drift")
nd_os <- rolling_os(y_train, y_test, function(y) {
n <- length(y)
if (n < 2) y[n] else y[n] + (y[n]-y[1])/(n-1)
})
res_mkt[["Naive Drift"]] <- list(
lolos = nd_diag$lolos,
resid = as.numeric(nd_resid),
fcast_ms = as.numeric(nd_fit$mean),
fcast_os = nd_os
)
# ============================================================
## Holt's Method – Grid Search (alpha x beta, step 0.05)
# ============================================================
# ============================================================
# Holt's Method – Grid Search (REVISI AMAN)
# ============================================================
alpha_grid <- seq(0.05, 0.95, by=0.05)
beta_grid <- seq(0.05, 0.95, by=0.05)
best_holt <- list(aic=Inf, alpha=NA, beta=NA, fit=NULL)
for (al in alpha_grid) {
for (bt in beta_grid) {
# Gunakan tryCatch untuk menangkap error numerik
tryCatch({
hf <- holt(ts_train, alpha=al, beta=bt, h=N_TEST, initial="simple")
aic_val <- hf$model$aic
# Penanganan NA: Pastikan aic_val ada dan tidak NA sebelum dibandingkan
if (!is.null(aic_val) && !is.na(aic_val)) {
if (aic_val < best_holt$aic) {
best_holt <- list(aic=aic_val, alpha=al, beta=bt, fit=hf)
}
}
}, error = function(e) { NULL }) # Abaikan kombinasi yang error
}
}
# Fallback: Jika grid search gagal total, gunakan optimasi otomatis dari fungsi holt
if (is.null(best_holt$fit)) {
best_holt$fit <- holt(ts_train, h=N_TEST)
best_holt$alpha <- best_holt$fit$model$par["alpha"]
best_holt$beta <- best_holt$fit$model$par["beta"]
}
# Jalankan uji diagnostik hanya jika model berhasil terbentuk
res_resid_holt <- residuals(best_holt$fit)
uji_lb(res_resid_holt,
sprintf("Holt (α=%.2f, β=%.2f)", best_holt$alpha, best_holt$beta))
holt_diag <- uji_lb(residuals(best_holt$fit), "") # simpan hasil
# Ulang agar lebih clean: re-run diam-diam
{
r_holt <- na.omit(residuals(best_holt$fit))
lag_h <- min(10, max(1, floor(length(r_holt)/5)))
bt_h <- Box.test(r_holt, lag=lag_h, type="Ljung-Box")
holt_diag <- list(p_value=bt_h$p.value, lolos=bt_h$p.value>0.05)
}
holt_os <- rolling_os(y_train, y_test, function(y) {
ts_y <- make_ts(y, df$Tanggal[1])
tryCatch(
as.numeric(holt(ts_y, alpha=best_holt$alpha, beta=best_holt$beta,
h=1, initial="simple")$mean),
error=function(e) tail(y,1)
)
})
res_mkt[["Holt"]] <- list(
lolos = holt_diag$lolos,
resid = as.numeric(residuals(best_holt$fit)),
fcast_ms = as.numeric(best_holt$fit$mean),
fcast_os = holt_os,
alpha = best_holt$alpha,
beta = best_holt$beta
)
# ============================================================
## Double Moving Average – Grid Search m
# ============================================================
m_grid <- seq(2, min(floor(n_tr/4), 12))
best_dma <- list(rmse=Inf, m=NA)
for (m in m_grid) {
resid_d <- dma_residuals(y_train, m)
rmse_d <- sqrt(mean(resid_d^2, na.rm=TRUE))
if (!is.na(rmse_d) && rmse_d < best_dma$rmse) {
best_dma <- list(rmse=rmse_d, m=m)
}
}
dma_res_best <- dma_residuals(y_train, best_dma$m)
uji_lb(dma_res_best, sprintf("DMA (m=%d)", best_dma$m))
{
r_dma <- na.omit(dma_res_best)
lag_d <- min(10, max(1, floor(length(r_dma)/5)))
bt_d <- Box.test(r_dma, lag=lag_d, type="Ljung-Box")
dma_diag <- list(p_value=bt_d$p.value, lolos=bt_d$p.value>0.05)
}
dma_obj <- dma_compute(y_train, best_dma$m)
dma_fcast_ms <- dma_forecast_h(dma_obj, N_TEST)
dma_os <- rolling_os(y_train, y_test, function(y) {
dma_forecast_h(dma_compute(y, best_dma$m), 1)[1]
})
res_mkt[["DMA"]] <- list(
lolos = dma_diag$lolos,
resid = dma_res_best,
fcast_ms = dma_fcast_ms,
fcast_os = dma_os,
m = best_dma$m
)
cat("```\n")
results[[mkt]] <- res_mkt
}Uji Ljung-Box pada residual fitted values (p > 0,05 = lolos)
Naive Simple | p = 0.7185 | ✅ Tidak ada autokorelasi
Naive Trend | p = 0.0013 | ❌ Ada autokorelasi (residual tidak acak)
Naive Drift | p = 0.7185 | ✅ Tidak ada autokorelasi
Holt (α=0.82, β=0.00) | p = 0.8859 | ✅ Tidak ada autokorelasi
| p = 0.8859 | ✅ Tidak ada autokorelasi
DMA (m=3) | p = 0.0413 | ❌ Ada autokorelasi (residual tidak acak)
Uji Ljung-Box pada residual fitted values (p > 0,05 = lolos)
Naive Simple | p = 0.3685 | ✅ Tidak ada autokorelasi
Naive Trend | p = 0.0125 | ❌ Ada autokorelasi (residual tidak acak)
Naive Drift | p = 0.3685 | ✅ Tidak ada autokorelasi
Holt (α=0.99, β=0.00) | p = 0.3261 | ✅ Tidak ada autokorelasi
| p = 0.3261 | ✅ Tidak ada autokorelasi
DMA (m=2) | p = 0.0204 | ❌ Ada autokorelasi (residual tidak acak)
Uji Ljung-Box pada residual fitted values (p > 0,05 = lolos)
Naive Simple | p = 0.7123 | ✅ Tidak ada autokorelasi
Naive Trend | p = 0.0118 | ❌ Ada autokorelasi (residual tidak acak)
Naive Drift | p = 0.7123 | ✅ Tidak ada autokorelasi
Holt (α=1.00, β=0.00) | p = 0.7694 | ✅ Tidak ada autokorelasi
| p = 0.7694 | ✅ Tidak ada autokorelasi
DMA (m=2) | p = 0.1241 | ✅ Tidak ada autokorelasi
Data didiferensikan orde-1 (\(\Delta Y_t = Y_t - Y_{t-1}\)) sebelum pemodelan.
Model yang dibangun:
Ramalan level diperoleh dengan rekonstruksi: \(\hat{Y}_{T+h} = Y_T + \sum_{i=1}^{h} \hat{\Delta Y}_{T+i}\)
par(mfrow=c(3,2), mar=c(3,4,3,1))
for (nm in c("Rotterdam","MDEX","ICDX")) {
dy <- diff(df_train[[nm]])
plot(dy, type="l", main=paste("Δ Harga –", nm), ylab="Δ (USD/ton)",
col="steelblue", lwd=1.2)
abline(h=0, col="red", lty=2)
acf(dy, lag.max=20, main=paste("ACF Δ –", nm), col="steelblue", lwd=2)
}for (mkt in MARKETS) {
cat("\n\n## Pasar:", mkt, "(Diferensial) {.tabset}\n\n")
cat("### Uji Diagnostik\n\n")
cat("**Uji Ljung-Box (p > 0,05 = lolos)**\n\n```\n")
y_train <- df_train[[mkt]]
y_test <- df_test[[mkt]]
n_tr <- length(y_train)
dy_train <- diff(y_train)
n_dtr <- length(dy_train)
y_last <- tail(y_train, 1)
# ============================================================
## Simple Average (pada data diferensial)
# ============================================================
sa_mean <- mean(dy_train)
sa_resid <- dy_train - sa_mean # residual = Δy - mean(Δy)
sa_diag <- uji_lb(sa_resid, "Simple Average (diff)")
sa_fcast_ms <- y_last + cumsum(rep(sa_mean, N_TEST))
sa_os <- rolling_os(y_train, y_test, function(y) {
tail(y,1) + mean(diff(y))
})
results[[mkt]][["SA_diff"]] <- list(
lolos = sa_diag$lolos,
resid = sa_resid,
fcast_ms = sa_fcast_ms,
fcast_os = sa_os
)
# ============================================================
## Moving Average pada data diferensial – Grid Search m
# ============================================================
m_grid2 <- seq(2, min(floor(n_dtr/3), 12))
best_ma_d <- list(rmse=Inf, m=NA)
for (m in m_grid2) {
# Fitted: rollmean, align=right → nilai t = rata-rata m pengamatan sebelumnya
ma_v <- as.numeric(rollmean(dy_train, k=m, align="right", fill=NA))
# 1-step ahead: fitted[t] = MA_{t-1} → shift kanan 1
fit_v <- c(NA, head(ma_v, n_dtr-1))
res_v <- dy_train - fit_v
rmse_v <- sqrt(mean(res_v^2, na.rm=TRUE))
if (!is.na(rmse_v) && rmse_v < best_ma_d$rmse) {
best_ma_d <- list(rmse=rmse_v, m=m)
}
}
# Residual dengan m terbaik
ma_v_best <- as.numeric(rollmean(dy_train, k=best_ma_d$m, align="right", fill=NA))
ma_res_d <- dy_train - c(NA, head(ma_v_best, n_dtr-1))
ma_diag_d <- uji_lb(ma_res_d,
sprintf("Moving Average diff (m=%d)", best_ma_d$m))
{
r <- na.omit(ma_res_d); lag <- min(10, max(1,floor(length(r)/5)))
bt <- Box.test(r, lag=lag, type="Ljung-Box")
ma_diag_d <- list(p_value=bt$p.value, lolos=bt$p.value>0.05)
}
last_ma_val <- tail(na.omit(ma_v_best), 1)
ma_fcast_ms <- y_last + cumsum(rep(last_ma_val, N_TEST))
ma_os <- rolling_os(y_train, y_test, function(y) {
dy <- diff(y)
ma_now <- if (length(dy) >= best_ma_d$m) mean(tail(dy, best_ma_d$m)) else mean(dy)
tail(y,1) + ma_now
})
results[[mkt]][["MA_diff"]] <- list(
lolos = ma_diag_d$lolos,
resid = ma_res_d,
fcast_ms = ma_fcast_ms,
fcast_os = ma_os,
m = best_ma_d$m
)
# ============================================================
## SES pada data diferensial – Grid Search alpha
# ============================================================
# ============================================================
# SES pada data diferensial – Grid Search alpha (REVISI)
# ============================================================
alpha_grid_s <- seq(0.05, 0.95, by=0.05)
best_ses_d <- list(aic=Inf, alpha=NA, fit=NULL)
# Pastikan dy_train valid
dy_train_clean <- na.omit(dy_train)
if (length(dy_train_clean) > 0) {
ts_dy <- ts(dy_train_clean, frequency=1)
for (al in alpha_grid_s) {
tryCatch({
sf <- ses(ts_dy, alpha=al, h=N_TEST, initial="simple")
aic_val <- sf$model$aic
if (!is.null(aic_val) && !is.na(aic_val) && aic_val < best_ses_d$aic) {
best_ses_d <- list(aic=aic_val, alpha=al, fit=sf)
}
}, error=function(e) NULL)
}
}
# Fallback jika grid search gagal
if (is.null(best_ses_d$fit) && length(dy_train_clean) > 0) {
best_ses_d$fit <- ses(ts_dy, h=N_TEST)
best_ses_d$alpha <- best_ses_d$fit$model$par["alpha"]
}
# Diagnostik
if (!is.null(best_ses_d$fit)) {
uji_lb(residuals(best_ses_d$fit), sprintf("SES diff (α=%.2f)", best_ses_d$alpha))
# Simpan hasil diagnostik
r_ses <- na.omit(residuals(best_ses_d$fit))
lag_s <- min(10, max(1, floor(length(r_ses)/5)))
bt_s <- Box.test(r_ses, lag=lag_s, type="Ljung-Box")
ses_diag_d <- list(p_value=bt_s$p.value, lolos=bt_s$p.value > 0.05)
ses_diff_fcast_ms <- as.numeric(best_ses_d$fit$mean)
ses_fcast_ms_lv <- y_last + cumsum(ses_diff_fcast_ms)
# Perbaikan pada rolling_os untuk SES diff
ses_os <- rolling_os(y_train, y_test, function(y) {
dy_now <- diff(y)
if (length(dy_now) < 1) return(tail(y, 1)) # Proteksi jika diff kosong
ts_dy2 <- ts(dy_now, frequency=1)
pred <- tryCatch(
as.numeric(ses(ts_dy2, alpha=best_ses_d$alpha, h=1, initial="simple")$mean),
error=function(e) mean(dy_now) # Fallback ke rata-rata jika ses gagal
)
tail(y,1) + pred
})
results[[mkt]][["SES_diff"]] <- list(
lolos = ses_diag_d$lolos,
resid = residuals(best_ses_d$fit),
fcast_ms = ses_fcast_ms_lv,
fcast_os = ses_os,
alpha = best_ses_d$alpha
)
}
cat("```\n")
}Uji Ljung-Box (p > 0,05 = lolos)
Simple Average (diff) | p = 0.7185 | ✅ Tidak ada autokorelasi
Moving Average diff (m=11) | p = 0.9697 | ✅ Tidak ada autokorelasi
SES diff (α=0.00) | p = 0.7185 | ✅ Tidak ada autokorelasi
Uji Ljung-Box (p > 0,05 = lolos)
Simple Average (diff) | p = 0.3685 | ✅ Tidak ada autokorelasi
Moving Average diff (m=3) | p = 0.1769 | ✅ Tidak ada autokorelasi
SES diff (α=0.00) | p = 0.3686 | ✅ Tidak ada autokorelasi
Uji Ljung-Box (p > 0,05 = lolos)
Simple Average (diff) | p = 0.7123 | ✅ Tidak ada autokorelasi
Moving Average diff (m=10) | p = 0.4692 | ✅ Tidak ada autokorelasi
SES diff (α=0.00) | p = 0.7123 | ✅ Tidak ada autokorelasi
model_labels <- c(
"Naive Simple" = "Naive Simple",
"Naive Trend" = "Naive Trend",
"Naive Drift" = "Naive Drift",
"Holt" = "Holt's Method",
"DMA" = "Double MA",
"SA_diff" = "Simple Avg (Diff)",
"MA_diff" = "Moving Avg (Diff)",
"SES_diff" = "SES (Diff)"
)
all_mdl_keys <- names(model_labels)
diag_rows <- lapply(MARKETS, function(mkt) {
lapply(all_mdl_keys, function(mdl) {
r <- results[[mkt]][[mdl]]
if (is.null(r)) return(NULL)
data.frame(
Pasar = mkt, Model = model_labels[mdl],
Lolos = ifelse(r$lolos, "✅ Lolos", "❌ Tidak Lolos"),
stringsAsFactors = FALSE
)
})
})
diag_df <- do.call(rbind, Filter(Negate(is.null), unlist(diag_rows, recursive=FALSE)))
diag_wide <- diag_df %>%
pivot_wider(names_from=Pasar, values_from=Lolos)
kbl(diag_wide,
caption="Hasil Uji Diagnostik Ljung-Box (H₀: tidak ada autokorelasi residual)") %>%
kable_styling(bootstrap_options=c("striped","hover","bordered"), full_width=FALSE) %>%
column_spec(1, bold=TRUE)| Model | Rotterdam | MDEX | ICDX |
|---|---|---|---|
| Naive Simple | ✅ Lolos | ✅ Lolos | ✅ Lolos |
| Naive Trend | ❌ Tidak Lolos | ❌ Tidak Lolos | ❌ Tidak Lolos |
| Naive Drift | ✅ Lolos | ✅ Lolos | ✅ Lolos |
| Holt’s Method | ✅ Lolos | ✅ Lolos | ✅ Lolos |
| Double MA | ❌ Tidak Lolos | ❌ Tidak Lolos | ✅ Lolos |
| Simple Avg (Diff) | ✅ Lolos | ✅ Lolos | ✅ Lolos |
| Moving Avg (Diff) | ✅ Lolos | ✅ Lolos | ✅ Lolos |
| SES (Diff) | ✅ Lolos | ✅ Lolos | ✅ Lolos |
Metrik dihitung untuk semua model (dengan catatan status diagnostik). Model terbaik dipilih berdasarkan RMSE terendah.
eval_list <- list()
for (mkt in MARKETS) {
y_test <- df_test[[mkt]]
for (mdl in all_mdl_keys) {
r <- results[[mkt]][[mdl]]
if (is.null(r)) next
if (is.null(r$fcast_ms) || is.null(r$fcast_os)) next
ms <- calc_metrics(y_test, r$fcast_ms)
os <- calc_metrics(y_test, r$fcast_os)
eval_list[[length(eval_list)+1]] <- data.frame(
Pasar = mkt,
Model = model_labels[mdl],
Diagnostik = ifelse(r$lolos, "✅","❌"),
RMSE_ms = round(ms["RMSE"],4),
MSE_ms = round(ms["MSE"],4),
MAPE_ms = round(ms["MAPE"],4),
RMSE_os = round(os["RMSE"],4),
MSE_os = round(os["MSE"],4),
MAPE_os = round(os["MAPE"],4),
row.names = NULL,
stringsAsFactors = FALSE
)
}
}
eval_df <- do.call(rbind, eval_list)
# Tampilkan per pasar
for (mkt in MARKETS) {
cat("\n**Pasar:", mkt, "**\n\n")
tbl_mkt <- eval_df %>% filter(Pasar == mkt) %>% select(-Pasar) %>%
arrange(RMSE_ms)
print(
kbl(tbl_mkt,
col.names = c("Model","Diag","RMSE","MSE","MAPE (%)","RMSE","MSE","MAPE (%)"),
caption = paste("Evaluasi –", mkt)) %>%
kable_styling(bootstrap_options=c("striped","hover"), full_width=FALSE) %>%
add_header_above(c(" "=2, "Multi-step Ahead"=3, "One-step Ahead"=3)) %>%
row_spec(which(tbl_mkt$RMSE_ms == min(tbl_mkt$RMSE_ms)), bold=TRUE,
background="#d4edda", color="#155724")
)
}##
## **Pasar: Rotterdam **
##
## <table class="table table-striped table-hover" style="color: black; width: auto !important; margin-left: auto; margin-right: auto;">
## <caption>Evaluasi – Rotterdam</caption>
## <thead>
## <tr>
## <th style="empty-cells: hide;border-bottom:hidden;" colspan="2"></th>
## <th style="border-bottom:hidden;padding-bottom:0; padding-left:3px;padding-right:3px;text-align: center; " colspan="3"><div style="border-bottom: 1px solid #ddd; padding-bottom: 5px; ">Multi-step Ahead</div></th>
## <th style="border-bottom:hidden;padding-bottom:0; padding-left:3px;padding-right:3px;text-align: center; " colspan="3"><div style="border-bottom: 1px solid #ddd; padding-bottom: 5px; ">One-step Ahead</div></th>
## </tr>
## <tr>
## <th style="text-align:left;"> Model </th>
## <th style="text-align:left;"> Diag </th>
## <th style="text-align:right;"> RMSE </th>
## <th style="text-align:right;"> MSE </th>
## <th style="text-align:right;"> MAPE (%) </th>
## <th style="text-align:right;"> RMSE </th>
## <th style="text-align:right;"> MSE </th>
## <th style="text-align:right;"> MAPE (%) </th>
## </tr>
## </thead>
## <tbody>
## <tr>
## <td style="text-align:left;font-weight: bold;color: rgba(21, 87, 36, 255) !important;background-color: rgba(212, 237, 218, 255) !important;"> Naive Trend </td>
## <td style="text-align:left;font-weight: bold;color: rgba(21, 87, 36, 255) !important;background-color: rgba(212, 237, 218, 255) !important;"> ❌ </td>
## <td style="text-align:right;font-weight: bold;color: rgba(21, 87, 36, 255) !important;background-color: rgba(212, 237, 218, 255) !important;"> 98.4948 </td>
## <td style="text-align:right;font-weight: bold;color: rgba(21, 87, 36, 255) !important;background-color: rgba(212, 237, 218, 255) !important;"> 9701.224 </td>
## <td style="text-align:right;font-weight: bold;color: rgba(21, 87, 36, 255) !important;background-color: rgba(212, 237, 218, 255) !important;"> 5.9785 </td>
## <td style="text-align:right;font-weight: bold;color: rgba(21, 87, 36, 255) !important;background-color: rgba(212, 237, 218, 255) !important;"> 77.5572 </td>
## <td style="text-align:right;font-weight: bold;color: rgba(21, 87, 36, 255) !important;background-color: rgba(212, 237, 218, 255) !important;"> 6015.121 </td>
## <td style="text-align:right;font-weight: bold;color: rgba(21, 87, 36, 255) !important;background-color: rgba(212, 237, 218, 255) !important;"> 5.4862 </td>
## </tr>
## <tr>
## <td style="text-align:left;"> Moving Avg (Diff) </td>
## <td style="text-align:left;"> ✅ </td>
## <td style="text-align:right;"> 152.8808 </td>
## <td style="text-align:right;"> 23372.545 </td>
## <td style="text-align:right;"> 10.2187 </td>
## <td style="text-align:right;"> 73.7927 </td>
## <td style="text-align:right;"> 5445.360 </td>
## <td style="text-align:right;"> 4.8189 </td>
## </tr>
## <tr>
## <td style="text-align:left;"> Naive Drift </td>
## <td style="text-align:left;"> ✅ </td>
## <td style="text-align:right;"> 158.3522 </td>
## <td style="text-align:right;"> 25075.418 </td>
## <td style="text-align:right;"> 10.5997 </td>
## <td style="text-align:right;"> 72.7591 </td>
## <td style="text-align:right;"> 5293.880 </td>
## <td style="text-align:right;"> 4.7708 </td>
## </tr>
## <tr>
## <td style="text-align:left;"> Simple Avg (Diff) </td>
## <td style="text-align:left;"> ✅ </td>
## <td style="text-align:right;"> 158.3522 </td>
## <td style="text-align:right;"> 25075.418 </td>
## <td style="text-align:right;"> 10.5997 </td>
## <td style="text-align:right;"> 72.7591 </td>
## <td style="text-align:right;"> 5293.880 </td>
## <td style="text-align:right;"> 4.7708 </td>
## </tr>
## <tr>
## <td style="text-align:left;"> SES (Diff) </td>
## <td style="text-align:left;"> ✅ </td>
## <td style="text-align:right;"> 158.3953 </td>
## <td style="text-align:right;"> 25089.072 </td>
## <td style="text-align:right;"> 10.6027 </td>
## <td style="text-align:right;"> 84.2519 </td>
## <td style="text-align:right;"> 7098.389 </td>
## <td style="text-align:right;"> 5.4478 </td>
## </tr>
## <tr>
## <td style="text-align:left;"> Holt's Method </td>
## <td style="text-align:left;"> ✅ </td>
## <td style="text-align:right;"> 167.8618 </td>
## <td style="text-align:right;"> 28177.579 </td>
## <td style="text-align:right;"> 11.2783 </td>
## <td style="text-align:right;"> 94.4996 </td>
## <td style="text-align:right;"> 8930.171 </td>
## <td style="text-align:right;"> 6.2909 </td>
## </tr>
## <tr>
## <td style="text-align:left;"> Naive Simple </td>
## <td style="text-align:left;"> ✅ </td>
## <td style="text-align:right;"> 227.1504 </td>
## <td style="text-align:right;"> 51597.308 </td>
## <td style="text-align:right;"> 15.8750 </td>
## <td style="text-align:right;"> 74.6941 </td>
## <td style="text-align:right;"> 5579.211 </td>
## <td style="text-align:right;"> 4.8662 </td>
## </tr>
## <tr>
## <td style="text-align:left;"> Double MA </td>
## <td style="text-align:left;"> ❌ </td>
## <td style="text-align:right;"> 371.8764 </td>
## <td style="text-align:right;"> 138292.072 </td>
## <td style="text-align:right;"> 26.8102 </td>
## <td style="text-align:right;"> 110.3024 </td>
## <td style="text-align:right;"> 12166.628 </td>
## <td style="text-align:right;"> 7.6963 </td>
## </tr>
## </tbody>
## </table>
## **Pasar: MDEX **
##
## <table class="table table-striped table-hover" style="color: black; width: auto !important; margin-left: auto; margin-right: auto;">
## <caption>Evaluasi – MDEX</caption>
## <thead>
## <tr>
## <th style="empty-cells: hide;border-bottom:hidden;" colspan="2"></th>
## <th style="border-bottom:hidden;padding-bottom:0; padding-left:3px;padding-right:3px;text-align: center; " colspan="3"><div style="border-bottom: 1px solid #ddd; padding-bottom: 5px; ">Multi-step Ahead</div></th>
## <th style="border-bottom:hidden;padding-bottom:0; padding-left:3px;padding-right:3px;text-align: center; " colspan="3"><div style="border-bottom: 1px solid #ddd; padding-bottom: 5px; ">One-step Ahead</div></th>
## </tr>
## <tr>
## <th style="text-align:left;"> Model </th>
## <th style="text-align:left;"> Diag </th>
## <th style="text-align:right;"> RMSE </th>
## <th style="text-align:right;"> MSE </th>
## <th style="text-align:right;"> MAPE (%) </th>
## <th style="text-align:right;"> RMSE </th>
## <th style="text-align:right;"> MSE </th>
## <th style="text-align:right;"> MAPE (%) </th>
## </tr>
## </thead>
## <tbody>
## <tr>
## <td style="text-align:left;font-weight: bold;color: rgba(21, 87, 36, 255) !important;background-color: rgba(212, 237, 218, 255) !important;"> Moving Avg (Diff) </td>
## <td style="text-align:left;font-weight: bold;color: rgba(21, 87, 36, 255) !important;background-color: rgba(212, 237, 218, 255) !important;"> ✅ </td>
## <td style="text-align:right;font-weight: bold;color: rgba(21, 87, 36, 255) !important;background-color: rgba(212, 237, 218, 255) !important;"> 95.2610 </td>
## <td style="text-align:right;font-weight: bold;color: rgba(21, 87, 36, 255) !important;background-color: rgba(212, 237, 218, 255) !important;"> 9074.657 </td>
## <td style="text-align:right;font-weight: bold;color: rgba(21, 87, 36, 255) !important;background-color: rgba(212, 237, 218, 255) !important;"> 7.0157 </td>
## <td style="text-align:right;font-weight: bold;color: rgba(21, 87, 36, 255) !important;background-color: rgba(212, 237, 218, 255) !important;"> 62.0851 </td>
## <td style="text-align:right;font-weight: bold;color: rgba(21, 87, 36, 255) !important;background-color: rgba(212, 237, 218, 255) !important;"> 3854.559 </td>
## <td style="text-align:right;font-weight: bold;color: rgba(21, 87, 36, 255) !important;background-color: rgba(212, 237, 218, 255) !important;"> 5.1596 </td>
## </tr>
## <tr>
## <td style="text-align:left;"> Double MA </td>
## <td style="text-align:left;"> ❌ </td>
## <td style="text-align:right;"> 104.8396 </td>
## <td style="text-align:right;"> 10991.351 </td>
## <td style="text-align:right;"> 8.9151 </td>
## <td style="text-align:right;"> 69.1219 </td>
## <td style="text-align:right;"> 4777.839 </td>
## <td style="text-align:right;"> 5.5949 </td>
## </tr>
## <tr>
## <td style="text-align:left;"> Holt's Method </td>
## <td style="text-align:left;"> ✅ </td>
## <td style="text-align:right;"> 106.1584 </td>
## <td style="text-align:right;"> 11269.611 </td>
## <td style="text-align:right;"> 7.7831 </td>
## <td style="text-align:right;"> 64.1131 </td>
## <td style="text-align:right;"> 4110.485 </td>
## <td style="text-align:right;"> 4.8112 </td>
## </tr>
## <tr>
## <td style="text-align:left;"> Naive Trend </td>
## <td style="text-align:left;"> ❌ </td>
## <td style="text-align:right;"> 117.6731 </td>
## <td style="text-align:right;"> 13846.951 </td>
## <td style="text-align:right;"> 10.3364 </td>
## <td style="text-align:right;"> 53.2096 </td>
## <td style="text-align:right;"> 2831.257 </td>
## <td style="text-align:right;"> 4.3404 </td>
## </tr>
## <tr>
## <td style="text-align:left;"> SES (Diff) </td>
## <td style="text-align:left;"> ✅ </td>
## <td style="text-align:right;"> 123.6882 </td>
## <td style="text-align:right;"> 15298.760 </td>
## <td style="text-align:right;"> 9.7340 </td>
## <td style="text-align:right;"> 63.6458 </td>
## <td style="text-align:right;"> 4050.787 </td>
## <td style="text-align:right;"> 4.7708 </td>
## </tr>
## <tr>
## <td style="text-align:left;"> Naive Drift </td>
## <td style="text-align:left;"> ✅ </td>
## <td style="text-align:right;"> 123.6896 </td>
## <td style="text-align:right;"> 15299.108 </td>
## <td style="text-align:right;"> 9.7342 </td>
## <td style="text-align:right;"> 53.4082 </td>
## <td style="text-align:right;"> 2852.431 </td>
## <td style="text-align:right;"> 4.2889 </td>
## </tr>
## <tr>
## <td style="text-align:left;"> Simple Avg (Diff) </td>
## <td style="text-align:left;"> ✅ </td>
## <td style="text-align:right;"> 123.6896 </td>
## <td style="text-align:right;"> 15299.108 </td>
## <td style="text-align:right;"> 9.7342 </td>
## <td style="text-align:right;"> 53.4082 </td>
## <td style="text-align:right;"> 2852.431 </td>
## <td style="text-align:right;"> 4.2889 </td>
## </tr>
## <tr>
## <td style="text-align:left;"> Naive Simple </td>
## <td style="text-align:left;"> ✅ </td>
## <td style="text-align:right;"> 165.4448 </td>
## <td style="text-align:right;"> 27371.987 </td>
## <td style="text-align:right;"> 14.5378 </td>
## <td style="text-align:right;"> 54.1408 </td>
## <td style="text-align:right;"> 2931.230 </td>
## <td style="text-align:right;"> 4.3666 </td>
## </tr>
## </tbody>
## </table>
## **Pasar: ICDX **
##
## <table class="table table-striped table-hover" style="color: black; width: auto !important; margin-left: auto; margin-right: auto;">
## <caption>Evaluasi – ICDX</caption>
## <thead>
## <tr>
## <th style="empty-cells: hide;border-bottom:hidden;" colspan="2"></th>
## <th style="border-bottom:hidden;padding-bottom:0; padding-left:3px;padding-right:3px;text-align: center; " colspan="3"><div style="border-bottom: 1px solid #ddd; padding-bottom: 5px; ">Multi-step Ahead</div></th>
## <th style="border-bottom:hidden;padding-bottom:0; padding-left:3px;padding-right:3px;text-align: center; " colspan="3"><div style="border-bottom: 1px solid #ddd; padding-bottom: 5px; ">One-step Ahead</div></th>
## </tr>
## <tr>
## <th style="text-align:left;"> Model </th>
## <th style="text-align:left;"> Diag </th>
## <th style="text-align:right;"> RMSE </th>
## <th style="text-align:right;"> MSE </th>
## <th style="text-align:right;"> MAPE (%) </th>
## <th style="text-align:right;"> RMSE </th>
## <th style="text-align:right;"> MSE </th>
## <th style="text-align:right;"> MAPE (%) </th>
## </tr>
## </thead>
## <tbody>
## <tr>
## <td style="text-align:left;font-weight: bold;color: rgba(21, 87, 36, 255) !important;background-color: rgba(212, 237, 218, 255) !important;"> Holt's Method </td>
## <td style="text-align:left;font-weight: bold;color: rgba(21, 87, 36, 255) !important;background-color: rgba(212, 237, 218, 255) !important;"> ✅ </td>
## <td style="text-align:right;font-weight: bold;color: rgba(21, 87, 36, 255) !important;background-color: rgba(212, 237, 218, 255) !important;"> 73.1041 </td>
## <td style="text-align:right;font-weight: bold;color: rgba(21, 87, 36, 255) !important;background-color: rgba(212, 237, 218, 255) !important;"> 5344.208 </td>
## <td style="text-align:right;font-weight: bold;color: rgba(21, 87, 36, 255) !important;background-color: rgba(212, 237, 218, 255) !important;"> 5.3428 </td>
## <td style="text-align:right;font-weight: bold;color: rgba(21, 87, 36, 255) !important;background-color: rgba(212, 237, 218, 255) !important;"> 62.7612 </td>
## <td style="text-align:right;font-weight: bold;color: rgba(21, 87, 36, 255) !important;background-color: rgba(212, 237, 218, 255) !important;"> 3938.971 </td>
## <td style="text-align:right;font-weight: bold;color: rgba(21, 87, 36, 255) !important;background-color: rgba(212, 237, 218, 255) !important;"> 5.4686 </td>
## </tr>
## <tr>
## <td style="text-align:left;"> Naive Drift </td>
## <td style="text-align:left;"> ✅ </td>
## <td style="text-align:right;"> 73.9778 </td>
## <td style="text-align:right;"> 5472.718 </td>
## <td style="text-align:right;"> 5.3995 </td>
## <td style="text-align:right;"> 52.4639 </td>
## <td style="text-align:right;"> 2752.456 </td>
## <td style="text-align:right;"> 4.5278 </td>
## </tr>
## <tr>
## <td style="text-align:left;"> Simple Avg (Diff) </td>
## <td style="text-align:left;"> ✅ </td>
## <td style="text-align:right;"> 73.9778 </td>
## <td style="text-align:right;"> 5472.718 </td>
## <td style="text-align:right;"> 5.3995 </td>
## <td style="text-align:right;"> 52.4639 </td>
## <td style="text-align:right;"> 2752.456 </td>
## <td style="text-align:right;"> 4.5278 </td>
## </tr>
## <tr>
## <td style="text-align:left;"> SES (Diff) </td>
## <td style="text-align:left;"> ✅ </td>
## <td style="text-align:right;"> 73.9882 </td>
## <td style="text-align:right;"> 5474.259 </td>
## <td style="text-align:right;"> 5.4001 </td>
## <td style="text-align:right;"> 62.7605 </td>
## <td style="text-align:right;"> 3938.885 </td>
## <td style="text-align:right;"> 5.4686 </td>
## </tr>
## <tr>
## <td style="text-align:left;"> Moving Avg (Diff) </td>
## <td style="text-align:left;"> ✅ </td>
## <td style="text-align:right;"> 82.6896 </td>
## <td style="text-align:right;"> 6837.566 </td>
## <td style="text-align:right;"> 7.8208 </td>
## <td style="text-align:right;"> 54.1314 </td>
## <td style="text-align:right;"> 2930.209 </td>
## <td style="text-align:right;"> 4.6591 </td>
## </tr>
## <tr>
## <td style="text-align:left;"> Naive Simple </td>
## <td style="text-align:left;"> ✅ </td>
## <td style="text-align:right;"> 96.3729 </td>
## <td style="text-align:right;"> 9287.731 </td>
## <td style="text-align:right;"> 8.4865 </td>
## <td style="text-align:right;"> 52.4980 </td>
## <td style="text-align:right;"> 2756.039 </td>
## <td style="text-align:right;"> 4.4899 </td>
## </tr>
## <tr>
## <td style="text-align:left;"> Naive Trend </td>
## <td style="text-align:left;"> ❌ </td>
## <td style="text-align:right;"> 181.4598 </td>
## <td style="text-align:right;"> 32927.666 </td>
## <td style="text-align:right;"> 18.5431 </td>
## <td style="text-align:right;"> 64.3121 </td>
## <td style="text-align:right;"> 4136.043 </td>
## <td style="text-align:right;"> 5.0660 </td>
## </tr>
## <tr>
## <td style="text-align:left;"> Double MA </td>
## <td style="text-align:left;"> ✅ </td>
## <td style="text-align:right;"> 196.5525 </td>
## <td style="text-align:right;"> 38632.868 </td>
## <td style="text-align:right;"> 19.9913 </td>
## <td style="text-align:right;"> 68.1661 </td>
## <td style="text-align:right;"> 4646.613 </td>
## <td style="text-align:right;"> 5.6483 </td>
## </tr>
## </tbody>
## </table>
# Pilih berdasarkan RMSE terkecil (multi-step)
best_ms_df <- eval_df %>%
group_by(Pasar) %>%
slice_min(RMSE_ms, n=1) %>%
select(Pasar, Model, Diagnostik, RMSE_ms, MSE_ms, MAPE_ms) %>%
ungroup()
best_os_df <- eval_df %>%
group_by(Pasar) %>%
slice_min(RMSE_os, n=1) %>%
select(Pasar, Model, Diagnostik, RMSE_os, MSE_os, MAPE_os) %>%
ungroup()
kbl(best_ms_df,
caption="Model Terbaik – Multi-step Ahead") %>%
kable_styling(bootstrap_options=c("striped","hover","bordered"), full_width=FALSE) %>%
column_spec(1:2, bold=TRUE)| Pasar | Model | Diagnostik | RMSE_ms | MSE_ms | MAPE_ms |
|---|---|---|---|---|---|
| ICDX | Holt’s Method | ✅ | 73.1041 | 5344.208 | 5.3428 |
| MDEX | Moving Avg (Diff) | ✅ | 95.2610 | 9074.657 | 7.0157 |
| Rotterdam | Naive Trend | ❌ | 98.4948 | 9701.224 | 5.9785 |
kbl(best_os_df,
caption="Model Terbaik – One-step Ahead") %>%
kable_styling(bootstrap_options=c("striped","hover","bordered"), full_width=FALSE) %>%
column_spec(1:2, bold=TRUE)| Pasar | Model | Diagnostik | RMSE_os | MSE_os | MAPE_os |
|---|---|---|---|---|---|
| ICDX | Naive Drift | ✅ | 52.4639 | 2752.456 | 4.5278 |
| ICDX | Simple Avg (Diff) | ✅ | 52.4639 | 2752.456 | 4.5278 |
| MDEX | Naive Trend | ❌ | 53.2096 | 2831.257 | 4.3404 |
| Rotterdam | Naive Drift | ✅ | 72.7591 | 5293.880 | 4.7708 |
| Rotterdam | Simple Avg (Diff) | ✅ | 72.7591 | 5293.880 | 4.7708 |
test_dates <- df_test$Tanggal
plot_list <- lapply(MARKETS, function(mkt) {
y_test <- df_test[[mkt]]
best_mdl <- best_ms_df$Model[best_ms_df$Pasar == mkt]
# Temukan key dari label
mdl_key <- names(model_labels)[model_labels == best_mdl]
fcast_ms <- results[[mkt]][[mdl_key]]$fcast_ms
fcast_os <- results[[mkt]][[mdl_key]]$fcast_os
df_p <- data.frame(
Tanggal = rep(test_dates, 3),
Nilai = c(y_test, fcast_ms, fcast_os),
Seri = rep(c("Aktual","Multi-step","One-step"), each=N_TEST)
)
ggplot(df_p, aes(x=Tanggal, y=Nilai, color=Seri, linetype=Seri)) +
geom_line(linewidth=0.9) +
geom_point(size=1.5) +
scale_color_manual(values=c(Aktual="black","Multi-step"="red","One-step"="blue")) +
scale_linetype_manual(values=c(Aktual="solid","Multi-step"="dashed","One-step"="dotted")) +
labs(title=paste(mkt, "–", best_mdl),
subtitle=sprintf("RMSE multi-step=%.2f | RMSE one-step=%.2f",
results[[mkt]][[mdl_key]]$fcast_ms |> (\(f) calc_metrics(y_test,f)["RMSE"])(),
results[[mkt]][[mdl_key]]$fcast_os |> (\(f) calc_metrics(y_test,f)["RMSE"])()),
x=NULL, y="USD/ton", color=NULL, linetype=NULL) +
theme(legend.position="top")
})
(plot_list[[1]] / plot_list[[2]] / plot_list[[3]]) +
plot_annotation(title="Perbandingan Ramalan pada Data Uji")Model terbaik (multi-step) dilatih ulang pada seluruh data (79 bulan), kemudian digunakan untuk meramal Juli 2026 – Januari 2028 (19 bulan ke depan).
last_date <- max(df$Tanggal)
n_horizon <- 19 # Jul 2026 – Jan 2028
# Buat tanggal forecast
fc_dates <- Reduce(function(d, i) c(d, add_month(tail(d,1))),
seq_len(n_horizon - 1),
init = add_month(last_date))
final_fcast <- list()
for (mkt in MARKETS) {
y_all <- df[[mkt]]
n_all <- length(y_all)
y_last_all <- tail(y_all, 1)
best_mdl <- best_ms_df$Model[best_ms_df$Pasar == mkt]
mdl_key <- names(model_labels)[model_labels == best_mdl]
cat(sprintf("\n%-12s → Model terbaik: %s\n", mkt, best_mdl))
ts_all <- make_ts(y_all, df$Tanggal[1])
fcast_vals <- tryCatch({
switch(mdl_key,
"Naive Simple" = rep(y_last_all, n_horizon),
"Naive Trend" = y_last_all + (y_all[n_all]-y_all[n_all-1]) * seq_len(n_horizon),
"Naive Drift" = {
drift <- (y_last_all - y_all[1])/(n_all-1)
y_last_all + drift * seq_len(n_horizon)
},
"Holt" = {
al <- results[[mkt]][["Holt"]]$alpha
bt <- results[[mkt]][["Holt"]]$beta
as.numeric(holt(ts_all, alpha=al, beta=bt, h=n_horizon, initial="simple")$mean)
},
"DMA" = {
m <- results[[mkt]][["DMA"]]$m
dma_forecast_h(dma_compute(y_all, m), n_horizon)
},
"SA_diff" = {
mean_dy <- mean(diff(y_all))
y_last_all + cumsum(rep(mean_dy, n_horizon))
},
"MA_diff" = {
m <- results[[mkt]][["MA_diff"]]$m
last_ma <- mean(tail(diff(y_all), m))
y_last_all + cumsum(rep(last_ma, n_horizon))
},
"SES_diff" = {
al <- results[[mkt]][["SES_diff"]]$alpha
ts_dy <- ts(diff(y_all), frequency=1)
pd <- as.numeric(ses(ts_dy, alpha=al, h=n_horizon, initial="simple")$mean)
y_last_all + cumsum(pd)
},
rep(y_last_all, n_horizon) # fallback
)
}, error = function(e) {
cat(" ⚠️ Error:", conditionMessage(e), "→ fallback Naive Simple\n")
rep(y_last_all, n_horizon)
})
final_fcast[[mkt]] <- fcast_vals
}##
## Rotterdam → Model terbaik: Naive Trend
##
## MDEX → Model terbaik: Moving Avg (Diff)
##
## ICDX → Model terbaik: Holt's Method
# ----------------------------------------------------------------
# Plot gabungan: historis + forecast
# ----------------------------------------------------------------
plot_fcast_list <- lapply(MARKETS, function(mkt) {
fv <- final_fcast[[mkt]]
df_h <- data.frame(Tanggal=df$Tanggal, Nilai=df[[mkt]], Tipe="Historis")
df_f <- data.frame(Tanggal=fc_dates, Nilai=fv, Tipe="Ramalan")
df_c <- rbind(df_h, df_f)
best_mdl <- best_ms_df$Model[best_ms_df$Pasar == mkt]
ggplot(df_c, aes(x=Tanggal, y=Nilai, color=Tipe)) +
geom_line(linewidth=0.9) +
geom_point(data=df_f, size=2.5, shape=16) +
geom_vline(xintercept=as.numeric(last_date), linetype="dotted",
color="grey40", linewidth=0.7) +
scale_color_manual(values=c(Historis="steelblue", Ramalan="firebrick")) +
labs(title=paste(mkt, "–", best_mdl),
x=NULL, y="USD/ton", color=NULL) +
theme(legend.position="top")
})
(plot_fcast_list[[1]] / plot_fcast_list[[2]] / plot_fcast_list[[3]]) +
plot_annotation(
title = "Ramalan Harga CPO – Juli 2026 hingga Januari 2028",
caption = "Garis putus-putus vertikal: batas data historis"
)tbl_final <- data.frame(
Bulan = format(fc_dates, "%B %Y"),
Rotterdam = round(final_fcast[["Rotterdam"]], 2),
MDEX = round(final_fcast[["MDEX"]], 2),
ICDX = round(final_fcast[["ICDX"]], 2)
)
kbl(tbl_final,
col.names = c("Bulan","Rotterdam (USD/ton)","MDEX (USD/ton)","ICDX (USD/ton)"),
caption = "Ramalan Harga Rata-rata Bulanan CPO (Juli 2026 – Januari 2028)") %>%
kable_styling(bootstrap_options=c("striped","hover","bordered"), full_width=FALSE) %>%
column_spec(1, bold=TRUE) %>%
row_spec(nrow(tbl_final), bold=TRUE, background="#fff3cd")| Bulan | Rotterdam (USD/ton) | MDEX (USD/ton) | ICDX (USD/ton) |
|---|---|---|---|
| June 2026 | 1689.43 | 1190.16 | 999.04 |
| July 2026 | 1842.75 | 1243.54 | 1042.02 |
| August 2026 | 1996.07 | 1296.93 | 1085.00 |
| September 2026 | 2149.39 | 1350.32 | 1127.98 |
| October 2026 | 2302.71 | 1403.70 | 1170.95 |
| November 2026 | 2456.03 | 1457.09 | 1213.93 |
| December 2026 | 2609.35 | 1510.48 | 1256.91 |
| January 2027 | 2762.67 | 1563.86 | 1299.89 |
| February 2027 | 2915.99 | 1617.25 | 1342.87 |
| March 2027 | 3069.31 | 1670.64 | 1385.85 |
| April 2027 | 3222.63 | 1724.02 | 1428.83 |
| May 2027 | 3375.95 | 1777.41 | 1471.81 |
| June 2027 | 3529.27 | 1830.80 | 1514.79 |
| July 2027 | 3682.59 | 1884.18 | 1557.77 |
| August 2027 | 3835.91 | 1937.57 | 1600.75 |
| September 2027 | 3989.23 | 1990.96 | 1643.73 |
| October 2027 | 4142.55 | 2044.34 | 1686.71 |
| November 2027 | 4295.87 | 2097.73 | 1729.69 |
| December 2027 | 4449.19 | 2151.12 | 1772.67 |
param_list <- lapply(MARKETS, function(mkt) {
mdl_key <- names(model_labels)[model_labels == best_ms_df$Model[best_ms_df$Pasar==mkt]]
r <- results[[mkt]][[mdl_key]]
params <- switch(mdl_key,
"Holt" = sprintf("α=%.2f, β=%.2f", r$alpha, r$beta),
"DMA" = sprintf("m=%d", r$m),
"MA_diff" = sprintf("m=%d", r$m),
"SES_diff" = sprintf("α=%.2f", r$alpha),
"–"
)
data.frame(Pasar=mkt, `Model (Multi-step)`=best_ms_df$Model[best_ms_df$Pasar==mkt],
Parameter=params,
`RMSE Test`=round(best_ms_df$RMSE_ms[best_ms_df$Pasar==mkt],4),
check.names=FALSE, stringsAsFactors=FALSE)
})
kbl(do.call(rbind, param_list),
caption="Ringkasan Parameter Model Terbaik per Pasar") %>%
kable_styling(bootstrap_options=c("striped","hover","bordered"), full_width=FALSE) %>%
column_spec(1:2, bold=TRUE)| Pasar | Model (Multi-step) | Parameter | RMSE Test |
|---|---|---|---|
| Rotterdam | Naive Trend | – | 98.4948 |
| MDEX | Moving Avg (Diff) | m=3 | 95.2610 |
| ICDX | Holt’s Method | α=1.00, β=0.00 | 73.1041 |
sessionInfo()
## R version 4.4.1 (2024-06-14 ucrt)
## Platform: x86_64-w64-mingw32/x64
## Running under: Windows 11 x64 (build 26200)
##
## Matrix products: default
##
##
## locale:
## [1] LC_COLLATE=English_Indonesia.utf8 LC_CTYPE=English_Indonesia.utf8
## [3] LC_MONETARY=English_Indonesia.utf8 LC_NUMERIC=C
## [5] LC_TIME=English_Indonesia.utf8
##
## time zone: Asia/Jakarta
## tzcode source: internal
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] patchwork_1.3.2 scales_1.4.0 kableExtra_1.4.0 knitr_1.50
## [5] zoo_1.8-14 tidyr_1.3.2 dplyr_1.2.1 ggplot2_4.0.3
## [9] forecast_8.24.0 readxl_1.4.5
##
## loaded via a namespace (and not attached):
## [1] sass_0.4.10 generics_0.1.4 xml2_1.3.8 stringi_1.8.7
## [5] lattice_0.22-6 digest_0.6.37 magrittr_2.0.3 evaluate_1.0.4
## [9] grid_4.4.1 RColorBrewer_1.1-3 fastmap_1.2.0 cellranger_1.1.0
## [13] jsonlite_2.0.0 nnet_7.3-19 purrr_1.0.4 viridisLite_0.4.3
## [17] textshaping_1.0.1 jquerylib_0.1.4 cli_3.6.3 rlang_1.2.0
## [21] withr_3.0.2 cachem_1.1.0 yaml_2.3.10 tools_4.4.1
## [25] parallel_4.4.1 colorspace_2.1-1 curl_6.4.0 vctrs_0.7.1
## [29] R6_2.6.1 lifecycle_1.0.5 stringr_1.6.0 tseries_0.10-61
## [33] pkgconfig_2.0.3 urca_1.3-4 pillar_1.11.0 bslib_0.9.0
## [37] gtable_0.3.6 glue_1.8.0 quantmod_0.4.28 Rcpp_1.0.13
## [41] systemfonts_1.3.2 xfun_0.56 tibble_3.2.1 lmtest_0.9-40
## [45] tidyselect_1.2.1 rstudioapi_0.17.1 farver_2.1.2 nlme_3.1-164
## [49] htmltools_0.5.8.1 labeling_0.4.3 svglite_2.2.2 rmarkdown_2.29
## [53] xts_0.14.1 timeDate_4041.110 fracdiff_1.5-3 compiler_4.4.1
## [57] S7_0.2.1 quadprog_1.5-8 TTR_0.24.4