Penerapan Model ARIMA–GARCH dalam Peramalan Kurs Jual Rupiah terhadap Dolar AS Periode Januari 2015 – September 2025

LIBRARY

library(readxl)
library(dplyr)
library(tidyr)
library(tibble)
library(lubridate)
library(stringr)
library(scales)
library(ggplot2)
library(gridExtra)
library(reshape2)
library(tseries)
library(urca)
library(forecast)
library(lmtest)
library(TSA)
library(xts)
library(zoo)
library(FinTS)
library(rugarch)
library(knitr)
library(kableExtra)
library(performance)

A.DATA

1.1. IMPORT DATA

# read data
kursUSD <- read_xlsx("D:/UNY/MySta/SEM 5/Ekonometrika/Time Series ARCH GARCH/Kurs_Transaksi_USD_2015-2025.xlsx", sheet = 1)
kursUSD
## # A tibble: 2,623 Ă— 5
##       NO Nilai `Kurs Jual` `Kurs Beli` Tanggal              
##    <dbl> <dbl>       <dbl>       <dbl> <chr>                
##  1     1     1      16763.      16597. 9/30/2025 12:00:00 AM
##  2     2     1      16859.      16691. 9/29/2025 12:00:00 AM
##  3     3     1      16836.      16668. 9/26/2025 12:00:00 AM
##  4     4     1      16763.      16597. 9/25/2025 12:00:00 AM
##  5     5     1      16719.      16553. 9/24/2025 12:00:00 AM
##  6     6     1      16690.      16524. 9/23/2025 12:00:00 AM
##  7     7     1      16661.      16495. 9/22/2025 12:00:00 AM
##  8     8     1      16580.      16416. 9/19/2025 12:00:00 AM
##  9     9     1      16512.      16348. 9/18/2025 12:00:00 AM
## 10    10     1      16467.      16303. 9/17/2025 12:00:00 AM
## # ℹ 2,613 more rows
str(kursUSD)
## tibble [2,623 Ă— 5] (S3: tbl_df/tbl/data.frame)
##  $ NO       : num [1:2623] 1 2 3 4 5 6 7 8 9 10 ...
##  $ Nilai    : num [1:2623] 1 1 1 1 1 1 1 1 1 1 ...
##  $ Kurs Jual: num [1:2623] 16763 16859 16836 16763 16719 ...
##  $ Kurs Beli: num [1:2623] 16597 16691 16668 16597 16553 ...
##  $ Tanggal  : chr [1:2623] "9/30/2025 12:00:00 AM" "9/29/2025 12:00:00 AM" "9/26/2025 12:00:00 AM" "9/25/2025 12:00:00 AM" ...

1.2. DATA MANIPULATION

# struktur awal
glimpse(kursUSD)
## Rows: 2,623
## Columns: 5
## $ NO          <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,…
## $ Nilai       <dbl> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1…
## $ `Kurs Jual` <dbl> 16763.40, 16858.88, 16835.76, 16763.40, 16719.18, 16690.03…
## $ `Kurs Beli` <dbl> 16596.60, 16691.13, 16668.24, 16596.60, 16552.82, 16523.97…
## $ Tanggal     <chr> "9/30/2025 12:00:00 AM", "9/29/2025 12:00:00 AM", "9/26/20…
head(kursUSD)
## # A tibble: 6 Ă— 5
##      NO Nilai `Kurs Jual` `Kurs Beli` Tanggal              
##   <dbl> <dbl>       <dbl>       <dbl> <chr>                
## 1     1     1      16763.      16597. 9/30/2025 12:00:00 AM
## 2     2     1      16859.      16691. 9/29/2025 12:00:00 AM
## 3     3     1      16836.      16668. 9/26/2025 12:00:00 AM
## 4     4     1      16763.      16597. 9/25/2025 12:00:00 AM
## 5     5     1      16719.      16553. 9/24/2025 12:00:00 AM
## 6     6     1      16690.      16524. 9/23/2025 12:00:00 AM
# Parsing tanggal & buat kolom Date
kursUSD <- kursUSD %>%
  mutate(
    Tanggal_raw = Tanggal,
    # parse mm/dd/YYYY hh:mm:ss AM/PM
    Tanggal_parsed = mdy_hms(Tanggal_raw, tz = "UTC"),
    Date = as_date(Tanggal_parsed)
  )

# Konversi kolom numeric yang akan dipakai
kursUSD <- kursUSD %>%
  mutate(
    Kurs_Jual = as.numeric(`Kurs Jual`),
    Kurs_Beli = as.numeric(`Kurs Beli`)
  )

# Sorting (urutan ascending berdasarkan Date)
kursUSD <- kursUSD %>%
  arrange(Date)

# Cek duplikat tanggal
dups <- kursUSD %>% add_count(Date) %>% filter(n > 1)

if (nrow(dups) > 0) {
  print(head(dups, 10))
} else {
  print(0)
}
## [1] 0
# Cek missing per kolom
missing_summary <- sapply(kursUSD, function(x) sum(is.na(x)))
missing_summary
##             NO          Nilai      Kurs Jual      Kurs Beli        Tanggal 
##              0              0              0              0              0 
##    Tanggal_raw Tanggal_parsed           Date      Kurs_Jual      Kurs_Beli 
##              0              0              0              0              0

1.3. EXPLORATORY DATA ANALYSIS (EDA)

# Statistik Deskriptif
summary(kursUSD$Kurs_Jual)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   12506   13685   14393   14537   15256   17028
# Visualisasi Time Series
ggplot(kursUSD, aes(x = Date, y = Kurs_Jual)) +
  geom_line(color = "darkblue", linewidth = 0.8) +
  labs(
    title = "Kurs Jual USD terhadap Rupiah (2015–2025)",
  
      x = "Tahun",
    y = "Nilai Kurs Jual (Rp)"
  ) +
  scale_y_continuous(labels = comma) +
  scale_x_date(
    date_breaks = "1 year",
    date_labels = "%Y",
    limits = as.Date(c("2015-01-01", "2025-12-31"))
  ) +
  theme_minimal() +
  theme(
    axis.text.x = element_text(angle = 45, hjust = 1)
  )

# Distribusi Data
# Histogram + density plot
ggplot(kursUSD, aes(x = Kurs_Jual)) +
  geom_histogram(bins = 40, fill = "skyblue", color = "white", alpha = 0.7) +
  geom_density(color = "black", linewidth = 0.8) +
  labs(title = "Distribusi Nilai Kurs Jual USD", x = "Kurs Jual (Rp)", y = "Frekuensi") +
  theme_minimal()

# Outlier Detection
# Boxplot
ggplot(kursUSD, aes(y = Kurs_Jual, x = "")) +
  geom_boxplot(fill = "skyblue", width = 0.3, outlier.color = "red", outlier.size = 2) +
  labs(title = "Boxplot Kurs Jual USD", y = "Nilai Kurs Jual (Rp)", x = "") +
  theme_minimal()

1.4. PARTISI DATA

library(dplyr)
library(tseries)

df <- kursUSD %>% arrange(Date) %>% select(Date, Kurs_Jual)

# Train/Test split (time-ordered, 80% train, 20% test)
n_total <- nrow(df)
n_train <- floor(0.8 * n_total)
train_df <- df[1:n_train, ]
test_df  <- df[(n_train + 1):n_total, ]

cat("Total data:", n_total, "\n")
## Total data: 2623
cat("Jumlah Data Training:", n_train, "\n")
## Jumlah Data Training: 2098
cat("Jumlah Data Testing:", n_total - n_train, "")
## Jumlah Data Testing: 525
# time series object untuk arima
train_ts <- ts(train_df$Kurs_Jual)
test_ts <- ts(test_df$Kurs_Jual)

#  xts untuk GARCH (agar tanggal tidak hilang)
library(xts)

train_xts <- xts(train_df$Kurs_Jual, order.by = as.Date(train_df$Date))
test_xts  <- xts(test_df$Kurs_Jual,  order.by = as.Date(test_df$Date))

1.5. PLOT DATA PARTISI

library(dplyr)
library(ggplot2)
library(scales)

# bertipe Date
train_df <- train_df %>% mutate(Date = as.Date(Date))
test_df  <- test_df  %>% mutate(Date = as.Date(Date))

# combined data
combined <- bind_rows(
  train_df %>% mutate(set = "Train"),
  test_df  %>% mutate(set = "Test")
)

# tanggal pemisah
separator_date <- min(test_df$Date)

#  legend
p_combined <- ggplot(combined, aes(x = Date, y = Kurs_Jual, color = set)) +
  geom_line(size = 0.8) +
  # Pemisah_data_train
  geom_vline(aes(xintercept = as.numeric(separator_date), color = "Pemisah_data_train"),
             linetype = "dashed", size = 0.9, show.legend = TRUE) +
  scale_color_manual(
    name = "colour",
    values = c(
      "Train" = "darkblue",
      "Test"  = "darkred",
      "Pemisah_data_train" = "darkgreen"
    ),
    breaks = c("Train", "Test", "Pemisah_data_train")
  ) +
  labs(
    title = "Kurs Jual USD terhadap Rupiah (Training & Testing)",
    x = "Tahun",
    y = "Nilai Kurs Jual (Rp)"
  ) +
  scale_y_continuous(labels = comma) +
  scale_x_date(
    date_breaks = "1 year",
    date_labels = "%Y",
    limits = as.Date(c("2015-01-01", "2025-12-31"))
  ) +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

p_combined

B. STASIONERITAS DATA

2.1. Plot ACF Awal

acf(train_ts, main="Plot ACF Kurs Jual USD terhadap Rupiah")

Interpretasi: Plot ACF menunjukkan penurunan autokorelasi yang lambat (tailing off) dan tidak cepat menuju nol, yang mengindikasikan bahwa data belum stasioner pada level (kemungkinan mengandung tren).

2.2. Uji Ketidakstasioneran

Untuk memastikan secara statistik, dilakukan beberapa pengujian formal yaitu ADF Test, KPSS Test, dan Phillips-Perron Test.

a. Augmented Dickey-Fuller (ADF) Test
library(tseries)
adf.test(train_ts)
## 
##  Augmented Dickey-Fuller Test
## 
## data:  train_ts
## Dickey-Fuller = -4.4762, Lag order = 12, p-value = 0.01
## alternative hypothesis: stationary

Interpretasi: Karena p-value < 0.05, maka dapat ditolak hipotesis nol (Hâ‚€) bahwa terdapat unit root. Ini menunjukkan indikasi awal bahwa data cenderung stasioner pada level, meskipun hasil ini perlu dikonfirmasi dengan uji lain (karena ADF kadang sensitif terhadap tren deterministik).

b. KPSS Test
tseries::kpss.test(train_ts)
## 
##  KPSS Test for Level Stationarity
## 
## data:  train_ts
## KPSS Level = 14.928, Truncation lag parameter = 8, p-value = 0.01

nterpretasi: Pada KPSS, H₀ = data stasioner pada level. Karena p-value < 0.05, maka H₀ ditolak. Ini berarti data tidak stasioner pada level. ⇒ Hasil ini bertolak belakang dengan ADF, sehingga dilakukan uji lanjutan pada data setelah diferensiasi.

c. Phillips-Perron (PP) Test
tseries::pp.test(train_ts)
## 
##  Phillips-Perron Unit Root Test
## 
## data:  train_ts
## Dickey-Fuller Z(alpha) = -31.619, Truncation lag parameter = 8, p-value
## = 0.01
## alternative hypothesis: stationary

Interpretasi: PP test juga menunjukkan hasil signifikan (p < 0.05) yang mengindikasikan data stasioner. Namun karena KPSS menyatakan sebaliknya, maka dilakukan differencing untuk memastikan.

2.3. Uji Setelah Differencing (Ordo 1)

# Differencing Ordo 1
dif1 <- diff(train_ts, differences = 1)
plot.ts(dif1, xlab="Waktu", ylab="Kurs Jual USD Diff Ordo 1")

acf(dif1, main="Plot ACF Kurs Jual USD setelah Differencing Ordo 1")

a. Uji ADF Setelah Differencing
adf.test(dif1)
## 
##  Augmented Dickey-Fuller Test
## 
## data:  dif1
## Dickey-Fuller = -12.034, Lag order = 12, p-value = 0.01
## alternative hypothesis: stationary

Interpretasi: Setelah dilakukan differencing ordo 1, data menjadi stasioner (p < 0.05).

b. Uji KPSS Setelah Differencing
tseries::kpss.test(dif1)
## 
##  KPSS Test for Level Stationarity
## 
## data:  dif1
## KPSS Level = 0.031213, Truncation lag parameter = 8, p-value = 0.1

Interpretasi: Nilai p-value > 0.05 ⇒ gagal menolak H₀ ⇒ data stasioner setelah differencing.

c. Phillips-Perron (PP) Test
tseries::pp.test(dif1)
## 
##  Phillips-Perron Unit Root Test
## 
## data:  dif1
## Dickey-Fuller Z(alpha) = -1953.7, Truncation lag parameter = 8, p-value
## = 0.01
## alternative hypothesis: stationary
d. Uji ur.df Setelah Differencing
library(urca)
summary(ur.df(as.numeric(dif1), type="none", lags=12))
## 
## ############################################### 
## # Augmented Dickey-Fuller Test Unit Root Test # 
## ############################################### 
## 
## Test regression none 
## 
## 
## Call:
## lm(formula = z.diff ~ z.lag.1 - 1 + z.diff.lag)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -387.70  -25.65    2.83   29.01  471.65 
## 
## Coefficients:
##               Estimate Std. Error t value Pr(>|t|)    
## z.lag.1      -0.734117   0.061094 -12.016   <2e-16 ***
## z.diff.lag1  -0.125745   0.059243  -2.123   0.0339 *  
## z.diff.lag2  -0.082092   0.057378  -1.431   0.1527    
## z.diff.lag3  -0.055006   0.055319  -0.994   0.3202    
## z.diff.lag4  -0.034647   0.053233  -0.651   0.5152    
## z.diff.lag5  -0.003903   0.050509  -0.077   0.9384    
## z.diff.lag6  -0.023671   0.047874  -0.494   0.6210    
## z.diff.lag7   0.003576   0.044880   0.080   0.9365    
## z.diff.lag8  -0.033375   0.041840  -0.798   0.4251    
## z.diff.lag9   0.043644   0.038247   1.141   0.2540    
## z.diff.lag10  0.032493   0.034065   0.954   0.3403    
## z.diff.lag11  0.049125   0.028954   1.697   0.0899 .  
## z.diff.lag12  0.006822   0.021949   0.311   0.7560    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 59.83 on 2071 degrees of freedom
## Multiple R-squared:  0.4356, Adjusted R-squared:  0.432 
## F-statistic: 122.9 on 13 and 2071 DF,  p-value: < 2.2e-16
## 
## 
## Value of test-statistic is: -12.0163 
## 
## Critical values for test statistics: 
##       1pct  5pct 10pct
## tau1 -2.58 -1.95 -1.62
# Uji 2 arah (alpha =0.01)
qnorm(0.005)
## [1] -2.575829

Interpretasi: Karena -12.0163 < -2.58, maka H₀ (unit root) ditolak. ⇒ Data stasioner setelah diferensiasi pertama.

C. PENDUGA PARAMETER DAN PENENTUAN MODEL TERBAIK

3.1. Identifikasi Model Tentatif: Plot ACF, PACF, dan EACF

# Plot ACF
acf(dif1, main="Plot ACF Kurs Jual USD setelah Diff 1", lag.max = 30)

# Plot PACF
pacf(dif1, main="Plot PACF Kurs Jual USD setelah Diff 1", lag.max = 30)

# Plot EACF
eacf(dif1, ar.max = 10,ma.max = 10)
## AR/MA
##    0 1 2 3 4 5 6 7 8 9 10
## 0  x x o o o o o o x o o 
## 1  x o o o o o o o x o o 
## 2  x x o o o o o o x o o 
## 3  x o o o o o o o x o o 
## 4  x o x x o o o o x o o 
## 5  x x o x o o o o o o o 
## 6  x x o x x x o o o o o 
## 7  x x o o x x o o o o o 
## 8  x x o x x x x x o o o 
## 9  x x x x x x x x o o o 
## 10 x o x x x o x x x o o

Interpretasi Plot: - Garis biru putus-putus menunjukkan batas signifikansi 95% (±1.96/sqrt(n)). - Batang yang melewati batas tersebut berarti signifikan secara statistik. - Pada plot ACF dan PACF, lag 1 terlihat signifikan sedangkan lag 2 dan 9 tidak menunjukkan pola sistematis (kemungkinan fluktuasi acak). - Plot ACF setelah diferensiasi pertama (diff 1) menunjukkan pola cut off setelah lag 1, sedangkan setelahnya nilai ACF cenderung menurun perlahan dan berada dalam batas signifikansi. → Ini mengindikasikan adanya komponen MA(1). - Plot PACF menunjukkan spike signifikan pada lag 1, kemudian cepat turun dan berfluktuasi kecil di sekitar nol. → Ini mengindikasikan adanya komponen AR(1). - Berdasarkan EACF, tanda “o” pertama (pola segitiga kosong) muncul sekitar posisi (p,q) = (1,1) atau (1,2).

Maka model tentatif yang dapat diuji meliputi: ARIMA(1,1,1) ARIMA(1,1,2) ARIMA(2,1,1)

3.2. Pendugaan Parameter Model

library(forecast)
library(lmtest)

# Model kandidat berdasarkan hasil identifikasi ACF, PACF, dan EACF
# Model estimation
model_111 <- arima(train_ts, order = c(1,1,1), include.mean = TRUE, method = "ML")
model_112 <- arima(train_ts, order = c(1,1,2), include.mean = TRUE, method = "ML")
model_211 <- arima(train_ts, order = c(2,1,1), include.mean = TRUE, method = "ML")

# Display parameters
model_111
## 
## Call:
## arima(x = train_ts, order = c(1, 1, 1), include.mean = TRUE, method = "ML")
## 
## Coefficients:
##          ar1      ma1
##       0.5846  -0.4530
## s.e.  0.1186   0.1309
## 
## sigma^2 estimated as 3598:  log likelihood = -11560.65,  aic = 23125.3
model_112
## 
## Call:
## arima(x = train_ts, order = c(1, 1, 2), include.mean = TRUE, method = "ML")
## 
## Coefficients:
##          ar1      ma1      ma2
##       0.7360  -0.6001  -0.0380
## s.e.  0.1526   0.1555   0.0382
## 
## sigma^2 estimated as 3596:  log likelihood = -11560.1,  aic = 23126.2
model_211
## 
## Call:
## arima(x = train_ts, order = c(2, 1, 1), include.mean = TRUE, method = "ML")
## 
## Coefficients:
##          ar1      ar2      ma1
##       0.8233  -0.0502  -0.6872
## s.e.  0.1941   0.0455   0.1923
## 
## sigma^2 estimated as 3595:  log likelihood = -11560.04,  aic = 23126.09
# Uji signifikansi
lmtest::coeftest(model_111)
## 
## z test of coefficients:
## 
##     Estimate Std. Error z value  Pr(>|z|)    
## ar1  0.58464    0.11862  4.9286 8.284e-07 ***
## ma1 -0.45305    0.13091 -3.4607 0.0005389 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
lmtest::coeftest(model_112)
## 
## z test of coefficients:
## 
##      Estimate Std. Error z value  Pr(>|z|)    
## ar1  0.736000   0.152609  4.8228 1.416e-06 ***
## ma1 -0.600142   0.155453 -3.8606 0.0001131 ***
## ma2 -0.038029   0.038182 -0.9960 0.3192427    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
lmtest::coeftest(model_211)
## 
## z test of coefficients:
## 
##      Estimate Std. Error z value  Pr(>|z|)    
## ar1  0.823254   0.194074  4.2420 2.216e-05 ***
## ar2 -0.050173   0.045451 -1.1039  0.269642    
## ma1 -0.687170   0.192288 -3.5736  0.000352 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

3.3. Pemilihan Model Terbaik

a. Model terbaik dari model tentatif
model_comp <- data.frame(
  Model = c("ARIMA(1,1,1)", "ARIMA(1,1,2)", "ARIMA(2,1,1)"),
  AIC = c(AIC(model_111), AIC(model_112), AIC(model_211)),
  BIC = c(BIC(model_111), BIC(model_112), BIC(model_211)),
  logLik = c(logLik(model_111), logLik(model_112), logLik(model_211))
)
print(model_comp)
##          Model      AIC      BIC    logLik
## 1 ARIMA(1,1,1) 23127.30 23144.25 -11560.65
## 2 ARIMA(1,1,2) 23128.20 23150.79 -11560.10
## 3 ARIMA(2,1,1) 23128.09 23150.68 -11560.04

Model terbaik model tentatif adalah ARIMA(1,1,1) karena:

  • Memiliki AIC paling kecil (23127.3).
  • Semua parameternya signifikan secara statistik (p < 0.05).
  • Model lebih sederhana (parsimonious) dibanding kandidat lain.
b. Verifikasi Best Model dengan auto.arima()
auto.arima(train_ts, d = 1, trace = TRUE)
## 
##  Fitting models using approximations to speed things up...
## 
##  ARIMA(2,1,2) with drift         : Inf
##  ARIMA(0,1,0) with drift         : 23170.65
##  ARIMA(1,1,0) with drift         : 23124.65
##  ARIMA(0,1,1) with drift         : 23132.14
##  ARIMA(0,1,0)                    : 23169.45
##  ARIMA(2,1,0) with drift         : 23120.91
##  ARIMA(3,1,0) with drift         : 23121.67
##  ARIMA(2,1,1) with drift         : 23119.76
##  ARIMA(1,1,1) with drift         : 23117.15
##  ARIMA(1,1,2) with drift         : 23118.83
##  ARIMA(0,1,2) with drift         : 23125.54
##  ARIMA(1,1,1)                    : 23115.52
##  ARIMA(0,1,1)                    : 23130.78
##  ARIMA(1,1,0)                    : 23123.17
##  ARIMA(2,1,1)                    : 23118.08
##  ARIMA(1,1,2)                    : 23117.16
##  ARIMA(0,1,2)                    : 23124.11
##  ARIMA(2,1,0)                    : 23119.34
##  ARIMA(2,1,2)                    : Inf
## 
##  Now re-fitting the best model(s) without approximations...
## 
##  ARIMA(1,1,1)                    : 23127.32
## 
##  Best model: ARIMA(1,1,1)
## Series: train_ts 
## ARIMA(1,1,1) 
## 
## Coefficients:
##          ar1      ma1
##       0.5790  -0.4469
## s.e.  0.1196   0.1317
## 
## sigma^2 = 3601:  log likelihood = -11560.65
## AIC=23127.31   AICc=23127.32   BIC=23144.25

D. DIAGNOSTIK MODEL

4.1. Cek Asumsi Residual

a. Analisis Eksploratif Residual

Berikut dilakukan pemeriksaan asumsi model secara eksploratif melalui Normal Q-Q Plot, Plot ACF, dan PACF.

model_111r <- arima(train_ts, order = c(1,1,1), include.mean = TRUE, method = "ML")
residual_111 <- model_111r$residuals

# Eksplorasi
par(mfrow=c(1,1))
qqnorm(residual_111)
qqline(residual_111, col = "blue", lwd = 2)

ts.plot(residual_111, main="Plot Residual ARIMA(1,1,1)", ylab="Residual", col="black")

acf(residual_111)

pacf(residual_111)

Interpretasi: - Berdasarkan hasil diatas, secara eksploratif Normal Q-Q Plot menunjukkan bahwa sisaan tidak mengikuti sebaran normal karena terdapat titik-titik yang tidak berada di sekitar garis. Penyimpangan titik dari garis diagonal menunjukkan adanya heavy tail pada residual, yang umum terjadi pada data keuangan dengan volatilitas tinggi. - plot ACF dan PACF terlihat bahwa terdapat lag yang signifikan yakni pada lag 9 dan 32. Hal ini menunjukkan bahwa terdapat sedikit gejala autokorelasi pada sisaan. Selanjutnya, untuk memastikan kembali akan dilakukan uji asumsi.

b. Uji Statistik Residual

Uji Autokorelasi (Ljung–Box Test)

Box.test(residual_111, lag = 30 ,type = "Ljung")
## 
##  Box-Ljung test
## 
## data:  residual_111
## X-squared = 41.232, df = 30, p-value = 0.08312

Berdasarkan hasil uji Ljung-Box di atas, diperoleh p-value = 0.08312 > 0.05 sehingga gagal menolak H0. Artinya, tidak terdapat autokorelasi signifikan pada sisaan model ARIMA(1,1,1). Dengan demikian, asumsi white noise residual terpenuhi.

Uji Normalitas (Kolmogorov–Smirnov Test)

ks.test(scale(residual_111), "pnorm")
## 
##  Asymptotic one-sample Kolmogorov-Smirnov test
## 
## data:  scale(residual_111)
## D = 0.085351, p-value = 1.062e-13
## alternative hypothesis: two-sided

Berdasarkan hasil uji Kolmogorov-Smirnov di atas dapat disimpulkan bahwa H0 ditolak karena p-value < 0.05 sehingga sisaan dari model ARIMA (1,1,1) tidak mengikuti sebaran Normal.

Uji Nilai Tengah Sisaan

# Uji nilai tengah sisaan
t.test(residual_111, mu = 0, alternative = "two.sided")
## 
##  One Sample t-test
## 
## data:  residual_111
## t = 0.68116, df = 2097, p-value = 0.4958
## alternative hypothesis: true mean is not equal to 0
## 95 percent confidence interval:
##  -1.675881  3.459641
## sample estimates:
## mean of x 
## 0.8918802

Berdasarkan hasil uji nilai tengah sisaan di atas dapat disimpulkan bahwa H0 tidak ditolak karena p-value > 0.05 sehingga nilai tengah sisaan sama dengan nol → tidak ada bias sistematis.

Kesimpulan Sementara Diagnostik Residual:

Berdasarkan hasil uji eksploratif dan statistik, residual model ARIMA(1,1,1) tidak memiliki autokorelasi yang signifikan dan memiliki nilai tengah nol, namun tidak berdistribusi normal. Kondisi ini mengindikasikan kemungkinan adanya heteroskedastisitas (volatilitas berubah-ubah), sehingga perlu dilakukan uji ARCH/GARCH pada tahap berikutnya.

4.2. Uji Heteroskedastisitas (ARCH Effect Test)

a. Mean Model Test (uji awal heteroskedastisitas)

Jika p-value < 0.05 → ada indikasi efek ARCH.

library(FinTS)
ArchTest(residual_111, lags = 30, demean=TRUE)
## 
##  ARCH LM-test; Null hypothesis: no ARCH effects
## 
## data:  residual_111
## Chi-squared = 607.6, df = 30, p-value < 2.2e-16

p-value < 0.05 → tolak H0 → ada indikasi heteroskedastisitas → lanjut ke model GARCH.

b. Uji Ordo ARCH–GARCH Optimal

uji ARCH menunjukkan p-value < 0.05, maka lanjut untuk menentukan ordo terbaik (p,q) pada model GARCH.

Membandingkan AIC/BIC dari beberapa ordo. misal (1,1), (1,2), (2,1), (2,2)

#  PEMILIHAN MODEL GARCH BERDASARKAN AIC (p,q)

library(fGarch)
library(texreg)

# Model 1: ARCH(1)
arch1 <- garchFit(~ arma(1,1) + garch(1,0),
                  data = residual_111, trace = FALSE)
extract(arch1,
        include.nobs = FALSE,
        include.aic = TRUE,
        include.loglik = FALSE)
## 
## 
##               coef.        s.e.          p
## mu        0.9772589  1.63539457 0.55012844
## ar1      -0.5293635  0.31893567 0.09695844
## ma1       0.5108796  0.31996521 0.11033876
## omega  2151.0515409 93.30622857 0.00000000
## alpha1    0.4123800  0.04586699 0.00000000
## 
##          GOF dec. places
## AIC 10.84818        TRUE
# Model 2: GARCH(1,1)
garch11 <- garchFit(~ arma(1,1) + garch(1,1),
                    data = residual_111, trace = FALSE)
extract(garch11,
        include.nobs = FALSE,
        include.aic = TRUE,
        include.loglik = FALSE)
## 
## 
##             coef.        s.e.            p
## mu      0.2730082  0.30855366 3.762646e-01
## ar1     0.6216782  0.12403834 5.387167e-07
## ma1    -0.6925774  0.11402331 1.247635e-09
## omega  96.0088453 21.41688382 7.365000e-06
## alpha1  0.1211486  0.01786907 1.203460e-11
## beta1   0.8516833  0.02081923 0.000000e+00
## 
##          GOF dec. places
## AIC 10.67331        TRUE
# Model 3: GARCH(1,2)
garch12 <- garchFit(~ arma(1,1) + garch(1,2),
                    data = residual_111, trace = FALSE)
extract(garch12,
        include.nobs = FALSE,
        include.aic = TRUE,
        include.loglik = FALSE)
## 
## 
##              coef.        s.e.            p
## mu       0.2889341  0.31545011 3.596972e-01
## ar1      0.6027082  0.11648167 2.287979e-07
## ma1     -0.6817271  0.10594034 1.234655e-10
## omega  120.3835461 26.78305261 6.964659e-06
## alpha1   0.1609991  0.02400998 2.007083e-11
## beta1    0.4209053  0.11054126 1.402787e-04
## beta2    0.3839581  0.10118561 1.478863e-04
## 
##          GOF dec. places
## AIC 10.66894        TRUE
# Model 4: GARCH(2,1)
garch21 <- garchFit(~ arma(1,1) + garch(2,1),
                    data = residual_111, trace = FALSE)
extract(garch21,
        include.nobs = FALSE,
        include.aic = TRUE,
        include.loglik = FALSE)
## 
## 
##              coef.        s.e.            p
## mu      0.27420159  0.30991274 3.762805e-01
## ar1     0.62069868  0.12536242 7.374428e-07
## ma1    -0.69169003  0.11499034 1.796634e-09
## omega  96.37699299 29.70072207 1.174763e-03
## alpha1  0.12144479  0.02197681 3.275358e-08
## alpha2  0.00000001  0.03536109 9.999998e-01
## beta1   0.85125321  0.03299679 0.000000e+00
## 
##          GOF dec. places
## AIC 10.67413        TRUE

Interpretasi – Uji Ordo ARCH–GARCH Optimal

Berdasarkan hasil perbandingan nilai Akaike Information Criterion (AIC) dari beberapa ordo model ARCH–GARCH, diketahui bahwa model GARCH(1,2) memiliki nilai AIC paling kecil (10.66894). Dengan demikian, model GARCH(1,2) dengan mean model ARMA(1,1) dipilih sebagai model terbaik untuk memodelkan variansi residual data.

c. Diagnostik Model GARCH:

Plot Residual Terstandarisasi

# Residual terstandarisasi
res_std <- residuals(garch12, standardize = TRUE)

# Plot residual
plot(res_std, type = "l", col = "blue",
     main = "Standardized Residuals GARCH(1,2)",
     ylab = "Residual", xlab = "waktu")
abline(h = 0, col = "red", lty = 2)

Plot ACF dari Residual Terstandarisasi

acf(res_std, main = "ACF of Standardized Residuals")

QQ-Plot Residual Terstandarisasi

qqnorm(res_std, main = "QQ Plot of Standardized Residuals")
qqline(res_std, col = "blue", lwd = 2)

Uji Ljung–Box (Autokorelasi Residual)

Box.test(res_std, lag = 30, type = "Ljung-Box")
## 
##  Box-Ljung test
## 
## data:  res_std
## X-squared = 42.951, df = 30, p-value = 0.05918

Jika p-value > 0.05 → tidak ada autokorelasi. ✅ Model sudah mampu menangkap dinamika mean dengan baik.

Uji ARCH–LM (Efek ARCH Tersisa)

library(FinTS)
ArchTest(res_std, lags = 30)
## 
##  ARCH LM-test; Null hypothesis: no ARCH effects
## 
## data:  res_std
## Chi-squared = 15.556, df = 30, p-value = 0.9862

hasil uji ARCH ulang ( p-value = 0.9862 > 0.05), maka: Tidak ada lagi efek ARCH yang signifikan pada residual model GARCH(1,2), menandakan model GARCH(1,2) telah berhasil menangkap dinamika volatilitas data.

E. PERAMALAN DAN EVALUASI MODEL

5.1. Perbandingan Ramalan

a. Model ARIMA (1,1,1)
# MODEL ARIMA(1,1,1) — Nilai Aktual vs Dugaan

library(forecast)

# Fit model ARIMA
fit_arima <- Arima(train_ts, order = c(1,1,1))

# Dugaan (fitted values)
dugaan_arima <- fitted(fit_arima)

# Gabungkan dengan nilai aktual
df_dugaan_arima <- data.frame(
  Periode = time(train_ts),
  Aktual = as.numeric(train_ts),
  Dugaan_ARIMA = as.numeric(dugaan_arima)
)

head(df_dugaan_arima, 10)
##    Periode Aktual Dugaan_ARIMA
## 1        1  12536     12523.46
## 2        2  12652     12537.50
## 3        3  12721     12668.79
## 4        4  12796     12737.71
## 5        5  12795     12813.39
## 6        6  12703     12802.64
## 7        7  12631     12694.26
## 8        8  12671     12617.58
## 9        9  12643     12670.29
## 10      10  12680     12638.98
# Fitted (dugaan) dari model ARIMA
fit_arima <- Arima(train_ts, order = c(1,1,1))
dugaan_arima <- fitted(fit_arima)

# Plot nilai aktual vs dugaan
par(mfrow = c(1,1))
plot(train_ts, type = "l", col = "black", lwd = 2,
     xlab = "Waktu", ylab = "Kurs Jual (Rp/USD)",
     main = "Nilai Aktual vs Dugaan ARIMA(1,1,1)")
lines(dugaan_arima, col = "skyblue", lwd = 2)
legend("topleft", legend = c("Aktual", "Dugaan ARIMA"),
       col = c("black", "skyblue"), lty = 1, lwd = 2, bty = "n")

Berdasarkan plot data aktual vs dugaan di atas terlihat bahwa nilai dan pola dugaan yang dihasilkan dari model ARIMA (1,1,1) mendekati nilai aktualnya.

# Forecast ARIMA(1,1,1)
fc_plot <- forecast(fit_arima, h=length(test_df$Kurs_Jual))
plot(fc_plot,
     main = "Peramalan Model ARIMA(1,1,1)",
     ylab = "Kurs Jual", xlab = "waktu")

b. Model ARIMA (1,1,1) - GARCH(1,2)
# MODEL ARIMA(1,1,1)-GARCH(1,2) — Nilai Aktual vs Dugaan

library(rugarch)
library(xts)

# Spesifikasi model
spec_garch <- ugarchspec(
  variance.model = list(model = "sGARCH", garchOrder = c(1,2)),
  mean.model = list(armaOrder = c(1,1), include.mean = TRUE),
  distribution.model = "norm"
)

# Estimasi model
fit_garch <- ugarchfit(spec_garch, data = train_xts)

# Fitted (dugaan)
dugaan_garch <- fitted(fit_garch)

# aktual & dugaan dalam dataframe
df_dugaan_garch <- data.frame(
  Tanggal = index(train_xts),
  Aktual = as.numeric(train_xts),
  Dugaan_ARIMA_GARCH = as.numeric(dugaan_garch)
)

head(df_dugaan_garch, 10)
##       Tanggal Aktual Dugaan_ARIMA_GARCH
## 1  2015-01-02  12536           12527.24
## 2  2015-01-05  12652           12536.64
## 3  2015-01-06  12721           12660.39
## 4  2015-01-07  12796           12725.41
## 5  2015-01-08  12795           12801.13
## 6  2015-01-09  12703           12794.55
## 7  2015-01-12  12631           12696.34
## 8  2015-01-13  12671           12626.25
## 9  2015-01-14  12643           12674.25
## 10 2015-01-15  12680           12640.73
# FIT MODEL ARIMA(1,1,1)-GARCH(1,2)
library(rugarch)

# Spesifikasi model
spec_garch <- ugarchspec(
  variance.model = list(model = "sGARCH", garchOrder = c(1,2)),
  mean.model = list(armaOrder = c(1,1), include.mean = TRUE),
  distribution.model = "norm"
)

# Estimasi model pada data training (xts)
fit_garch <- ugarchfit(spec_garch, data = train_xts)

dugaan_garch <- fitted(fit_garch)
dugaan_garch_xts <- xts(as.numeric(dugaan_garch), order.by = index(train_xts))


# Plot nilai aktual vs dugaan
plot(train_xts, type = "l", col = "black", lwd = 2,
     xlab = "Tanggal", ylab = "Kurs Jual",
     main = "Nilai Aktual vs Dugaan ARIMA(1,1,1)-GARCH(1,2)")

lines(dugaan_garch_xts, col = "orange", lwd = 2)
legend("topleft", legend = c("Aktual", "Dugaan ARIMA-GARCH"),
       col = c("black", "orange"), lty = 1, lwd = 2, bty = "n")

Perbandingan Ramalan ARIMA(1,1,1)-GARCH(1,2) dengan Data Aktual

# Forecast sepanjang test set
h <- length(test_df$Kurs_Jual)

# Forecast GARCH untuk periode test
fc_garch <- ugarchforecast(fit_garch, n.ahead = h)
fc_mean <- as.numeric(fitted(fc_garch))
fc_sigma <- as.numeric(sigma(fc_garch))

# Tanggal ramalan = tanggal di test set
future_dates <- as.Date(test_df$Date)

# Plot aktual vs hasil ramalan
plot(index(train_xts), train_xts, type = "l", col = "black", lwd = 2,
     xlab = "Tahun", ylab = "Kurs Jual",
     main = "Perbandingan Ramalan ARIMA(1,1,1)-GARCH(1,2) dengan Data Aktual")
lines(future_dates, fc_mean, col = "orange", lwd = 2)
lines(future_dates, fc_mean + 1.96*fc_sigma, col = "red", lty = 2)
lines(future_dates, fc_mean - 1.96*fc_sigma, col = "red", lty = 2)
lines(future_dates, test_df$Kurs_Jual, col = "blue", lwd = 2)

legend("topleft", 
       legend = c("Training", "Data Test Aktual", "Ramalan", "95% Interval"),
       col = c("black", "blue", "orange", "red"),
       lty = c(1,1,1,2), lwd = 2, bty = "n")

5.2. Evaluasi Model

Perbandingan evaluasi model ARIMA dan ARIMA–GARCH
library(forecast)
library(rugarch)
library(Metrics)

# MODEL 1: ARIMA(1,1,1)
fit_arima <- Arima(train_ts, order = c(1,1,1))
fc_arima  <- forecast(fit_arima, h = length(test_ts))

# MODEL 2: ARIMA(1,1,1)-GARCH(1,2)
library(rugarch)
spec_garch <- ugarchspec(
  variance.model = list(model = "sGARCH", garchOrder = c(1,2)),
  mean.model = list(armaOrder = c(1,1), include.mean = TRUE),
  distribution.model = "norm"
)

fit_garch <- ugarchfit(spec = spec_garch, data = train_xts)
fc_garch  <- ugarchforecast(fit_garch, n.ahead = length(test_xts))
fc_garch_values <- as.numeric(fitted(fc_garch))


# Evaluasi AIC dan BIC
aic_bic <- data.frame(
  Model = c("ARIMA(1,1,1)", "ARIMA(1,1,1)-GARCH(1,2)"),
  AIC   = c(AIC(fit_arima), infocriteria(fit_garch)[1]),
  BIC   = c(BIC(fit_arima), infocriteria(fit_garch)[2])
)
aic_bic
##                     Model         AIC         BIC
## 1            ARIMA(1,1,1) 23127.30767 23144.25245
## 2 ARIMA(1,1,1)-GARCH(1,2)    10.66838    10.68722
# Evaluasi akurasi model
eval_metrics <- data.frame(
  Model = c("ARIMA(1,1,1)", "ARIMA(1,1,1)-GARCH(1,2)"),
  RMSE = c(rmse(as.numeric(test_ts), as.numeric(fc_arima$mean)),
           rmse(as.numeric(test_ts), as.numeric(fc_garch_values))),
  MAE  = c(mae(as.numeric(test_ts), as.numeric(fc_arima$mean)),
           mae(as.numeric(test_ts), as.numeric(fc_garch_values))),
  MAPE = c(mape(as.numeric(test_ts), as.numeric(fc_arima$mean)),
           mape(as.numeric(test_ts), as.numeric(fc_garch_values)))
)

eval_metrics
##                     Model     RMSE      MAE       MAPE
## 1            ARIMA(1,1,1) 1127.478 1035.981 0.06392005
## 2 ARIMA(1,1,1)-GARCH(1,2) 1101.809 1008.060 0.06217679