Pasar modal Indonesia, khususnya saham PT Bali Bintang Sejahtera Tbk (BOLA), merupakan instrumen investasi yang cukup populer. Fluktuasi harga saham yang dinamis memerlukan analisis mendalam untuk membantu investor membuat keputusan yang lebih tepat. Peramalan (forecasting) harga saham merupakan salah satu cara untuk memprediksi tren harga di masa depan berdasarkan data historis.
Dalam laporan ini, kami menggunakan data historis harga penutupan saham BOLA untuk melakukan peramalan menggunakan metode ARIMA (AutoRegressive Integrated Moving Average). ARIMA adalah salah satu model time series yang paling populer dan terbukti efektif untuk peramalan data deret waktu.
library(forecast)
library(tseries)
## Registered S3 method overwritten by 'quantmod':
## method from
## as.zoo.data.frame zoo
library(ggplot2)
# Membaca file CSV
data <- read.csv("C:/Users/qonit/Downloads/Data_Historis_BOLA.csv")
# Tampilkan beberapa baris pertama
head(data, 10)
## Tanggal Terakhir Pembukaan Tertinggi Terendah Vol. Perubahan.
## 1 28/05/2025 102 104 104 101 275,90K 0,00%
## 2 27/05/2025 102 103 104 101 394,60K -0,97%
## 3 26/05/2025 103 97 109 96 9,31M 6,19%
## 4 23/05/2025 97 96 98 96 107,70K 2,11%
## 5 22/05/2025 95 98 105 94 3,61M -3,06%
## 6 21/05/2025 98 96 99 95 1,54M 1,03%
## 7 20/05/2025 97 95 100 95 1,84M 1,04%
## 8 19/05/2025 96 95 96 95 368,20K 1,05%
## 9 16/05/2025 95 94 96 93 463,50K 1,06%
## 10 15/05/2025 94 96 97 94 407,70K -2,08%
# Konversi tanggal
data$Tanggal <- as.Date(data$Tanggal, format = "%d/%m/%Y")
# Urutkan berdasarkan tanggal (ascending)
data <- data[order(data$Tanggal), ]
# Ekstrak harga penutupan (kolom "Terakhir")
harga <- data$Terakhir
Jumlah data: 270
Harga min: 80
Harga max: 115
Mean: 96.5074074
Median: 96
Grafik berikut menunjukkan pergerakan harga saham BOLA dari waktu ke waktu:
# Convert to time series
ts_harga <- ts(harga)
# Plot data asli
plot(ts_harga,
main = "Harga Penutupan Saham BOLA",
xlab = "Periode",
ylab = "Harga (Rp)",
type = "l",
col = "blue",
lwd = 2)
grid()
Data menunjukkan fluktuasi harga yang signifikan
Ada indikasi trend dan volatilitas dalam data
Data perlu diperiksa stationeritas-nya sebelum modeling ARIMA
Stationeritas adalah prasyarat penting dalam analisis ARIMA. Data dikatakan stasioner jika mean, variance, dan autocorrelation-nya konstan sepanjang waktu.
# ADF Test untuk cek stationeritas
adf_test <- adf.test(harga)
print(adf_test)
##
## Augmented Dickey-Fuller Test
##
## data: harga
## Dickey-Fuller = -3.0308, Lag order = 6, p-value = 0.1418
## alternative hypothesis: stationary
d_parameter = 1
Data tidak stasioner (p-value >= 0.05) sehingga perlu differencing
# Jika tidak stationer, lakukan differencing
if(d_parameter == 1) {
harga_diff <- diff(harga, differences = 1)
adf_test_diff <- adf.test(harga_diff)
print(adf_test_diff)
if(adf_test_diff$p.value < 0.05) {
cat("\n✓ Data STATIONER (p-value < 0.05)\n")
d_parameter <- 1
}
else {
cat("\n✗ Data TIDAK STATIONER (p-value >= 0.05)\n")
cat(" Perlu differencing (d > 0)\n")}
# Plot data asli
plot(ts_harga, main = "Data Asli", type = "l", col = "blue")
# Plot differencing
plot(harga_diff, main = "Data Setelah Differencing (d=1)", type = "l", col = "red")
}
## Warning in adf.test(harga_diff): p-value smaller than printed p-value
##
## Augmented Dickey-Fuller Test
##
## data: harga_diff
## Dickey-Fuller = -7.6349, Lag order = 6, p-value = 0.01
## alternative hypothesis: stationary
##
##
## ✓ Data STATIONER (p-value < 0.05)
Jumlah data setelah differencing: 269
Hilang 1 observasi karena perhitungan selisih
Statistik Perubahan Harga:
Mean: 0.063197
Std dev: 2.5051631
Minimum: -9
Maksimum: 15
# ACF plot
acf(harga_diff, main = "ACF Plot", lag.max = 20)
# PACF plot
pacf(harga_diff, main = "PACF Plot", lag.max = 20)
Untuk mendapatkan evaluasi yang objektif dan realistis, data dibagi menjadi:
Training Data (80%): Digunakan untuk melatih model
Testing Data (20%): Digunakan untuk evaluasi pada data yang belum pernah dilihat sebelumnya
# Hitung ukuran masing-masing
n_total <- length(harga)
train_size <- round(0.8 * n_total)
test_size <- n_total - train_size
# Split data
train_data <- harga[1:train_size]
test_data <- harga[(train_size + 1):n_total]
# Visualisasi split
plot(ts(harga),
main = "Data Split: Training (Biru) vs Testing (Merah)",
xlab = "Periode",
ylab = "Harga (Rp)",
type = "l",
lwd = 1,
col = "gray")
lines(ts(train_data),
col = "steelblue",
lwd = 2)
lines((train_size + 1):(train_size + test_size),
test_data,
col = "darkred",
lwd = 2)
legend("topright",
legend = c("Training Data", "Testing Data"),
col = c("steelblue", "darkred"),
lty = 1,
lwd = 2)
grid(col = "gray80")
Jumlah data training: 216 (80%)
Jumlah data testing: 54 (20%)
# Auto ARIMA akan otomatis mencari p, d, q terbaik
model_auto <- auto.arima(train_data,
seasonal = FALSE,
stepwise = TRUE,
trace = TRUE)
##
## Fitting models using approximations to speed things up...
##
## ARIMA(2,1,2) with drift : 1002.287
## ARIMA(0,1,0) with drift : 1006.719
## ARIMA(1,1,0) with drift : 1006.401
## ARIMA(0,1,1) with drift : 1006.398
## ARIMA(0,1,0) : 1004.693
## ARIMA(1,1,2) with drift : 1008.043
## ARIMA(2,1,1) with drift : 1000.737
## ARIMA(1,1,1) with drift : 1007.747
## ARIMA(2,1,0) with drift : 1006.959
## ARIMA(3,1,1) with drift : 1007.887
## ARIMA(3,1,0) with drift : 1006.257
## ARIMA(3,1,2) with drift : Inf
## ARIMA(2,1,1) : 998.8087
## ARIMA(1,1,1) : 1005.729
## ARIMA(2,1,0) : 1004.912
## ARIMA(3,1,1) : 1005.775
## ARIMA(2,1,2) : 1000.444
## ARIMA(1,1,0) : 1004.385
## ARIMA(1,1,2) : 1006.004
## ARIMA(3,1,0) : 1004.168
## ARIMA(3,1,2) : Inf
##
## Now re-fitting the best model(s) without approximations...
##
## ARIMA(2,1,1) : 1004.684
##
## Best model: ARIMA(2,1,1)
summary(model_auto)
## Series: train_data
## ARIMA(2,1,1)
##
## Coefficients:
## ar1 ar2 ma1
## 0.7552 -0.0055 -0.8805
## s.e. 0.1244 0.0781 0.1044
##
## sigma^2 = 6.112: log likelihood = -498.25
## AIC=1004.49 AICc=1004.68 BIC=1017.98
##
## Training set error measures:
## ME RMSE MAE MPE MAPE MASE
## Training set 0.04485744 2.449281 1.617058 0.004448716 1.659647 1.016571
## ACF1
## Training set -0.001796969
Parameter yang dipilih:
p (AR): 2
d (I-differencing): 1
q (MA): 1
Model: ARIMA(2,1,1)
Persamaan model ARIMA(2,1,1) tanpa konstan:
\[ Y(T) = Y(t-1) + \phi_1 \Delta Y(t-1) + \phi_2 \Delta Y(t-2) + \theta_1 \varepsilon(t-1) + \varepsilon(t) \] Dimana:
\(Y(T)\) = harga pada periode \(t\)
\(Y(t-1)\) = harga periode sebelumnya
\(\phi_1\) = koefisien AR lag 1
\(\phi_2\) = koefisien AR lag 2
\(\theta_1\) = koefisien MA lag 1
\(\varepsilon(t)\) = error pada periode \(t\)
Periksa apakah residual model adalah white noise:
tsdiag(model_auto, gof.lag = 20)
Observasi:
Residual tersebar random sepanjang periode waktu (0-250)
Hampir semua residual berada dalam rentang -2 sampai +2
Tidak ada pola atau trend yang terlihat
Hanya ada beberapa spike minor di sekitar periode 100-120
Interpretasi:
Residual sudah white noise
Tidak ada autokorelasi yang signifikan
Tidak ada seasonal pattern
Tidak ada trend dalam residual
Model sudah menangkap pola data dengan baik
Observasi:
Semua lag berada dalam garis biru confidence interval
Tidak ada bar yang keluar dari gatas atas atau bawah
ACF hampir flat (rata) di sekitar 0
Lag 0 selalu 1.0 (standard untuk ACF)
Interpretasi:
Tidak ada autokorelasi residual
Tidak ada lag yang signifikan secara statistik
Residual benar-benar independent (saling tidak bergantung)
Ini membuktikan model ARIMA(2,1,1) sudah optimal
Tidak ada pola yang terlewatkan
Observasi:
Semua p-value berada jauh di atas 0.05 (sekitar 0.5-1.0)
Tidak ada satu pun yang turun ke bawah garis 0.05
p-value tetap tinggi dan stabil di semua lag
Interpretasi:
Residual definitif adalah white noise
Ljung-Box test tidak signifikan di semua lag
Secara statistik terbukti:
H₀: “Residual adalah white noise” DITERIMA
H₁: “Residual ada autokorelasi” DITOLAK
Confidence 95%: residual adalah pure random
Setelah model dilatih, dilakukan forecast untuk periode testing
# Forecast untuk test_size hari ke depan
forecast_test <- forecast(model_auto, h = test_size)
# Ambil nilai prediksi
forecast_values <- as.numeric(forecast_test$mean)
# Buat tabel perbandingan
comparison <- data.frame(
Hari = 1:test_size,
Prediksi = round(forecast_values, 0),
Aktual = test_data,
Error = round(test_data - forecast_values, 0),
Persen_Error = round(abs(test_data - forecast_values) / test_data * 100, 2)
)
Tabel Prediksi vs Aktual (10 baris awal)
print(head(comparison, 10))
## Hari Prediksi Aktual Error Persen_Error
## 1 1 89 91 2 1.98
## 2 2 89 91 2 1.81
## 3 3 89 92 3 2.76
## 4 4 90 90 0 0.50
## 5 5 90 91 1 1.53
## 6 6 90 90 0 0.38
## 7 7 90 90 0 0.34
## 8 8 90 91 1 1.41
## 9 9 90 90 0 0.29
## 10 10 90 92 2 2.44
Tabel Prediksi vs Aktual (10 baris akhir)
print(tail(comparison, 10))
## Hari Prediksi Aktual Error Persen_Error
## 45 45 90 94 4 4.47
## 46 46 90 95 5 5.48
## 47 47 90 96 6 6.46
## 48 48 90 97 7 7.43
## 49 49 90 98 8 8.37
## 50 50 90 95 5 5.48
## 51 51 90 97 7 7.43
## 52 52 90 103 13 12.82
## 53 53 90 102 12 11.96
## 54 54 90 102 12 11.96
Menghitung Metrik Akurasi (Out-of-Sample)
# Hitung error
errors <- test_data - forecast_values
# MAE (Mean Absolute Error)
mae_test <- mean(abs(errors))
# RMSE (Root Mean Squared Error)
rmse_test <- sqrt(mean(errors^2))
# MAPE (Mean Absolute Percentage Error)
mape_test <- mean(abs(errors / test_data)) * 100
MAE (Mean Absolute Error): 4.5200835
RMSE (Root Mean Squared Error): 5.5880863
MAPE (Mean Absolute Percentage Error): 4.8738392%
# Interpretasi akurasi
if(mape_test < 5) {
cat("MODEL SANGAT BAIK (MAPE < 5%)\n")
} else if(mape_test < 10) {
cat("MODEL BAIK (MAPE < 10%)\n")
} else if(mape_test < 20) {
cat("MODEL CUKUP (MAPE < 20%)\n")
} else {
cat("MODEL KURANG BAIK (MAPE >= 20%)\n")
}
## MODEL SANGAT BAIK (MAPE < 5%)
par(mfrow = c(3, 1), mar = c(4, 4, 3, 1))
# Plot 1: Prediksi vs Aktual
plot(1:test_size, test_data,
type = "l",
col = "steelblue",
lwd = 2.5,
main = "Testing Period: Prediksi vs Nilai Aktual",
xlab = "Hari (dalam Testing Period)",
ylab = "Harga (Rp)",
ylim = range(c(test_data, forecast_values), na.rm = TRUE))
lines(1:test_size, forecast_values,
col = "red",
lwd = 2.5,
lty = 2)
legend("topright",
legend = c("Aktual", "Prediksi"),
col = c("steelblue", "red"),
lty = c(1, 2),
lwd = 2.5,
cex = 1.2)
grid(col = "gray80")
# Plot 2: Forecast Error
plot(1:test_size, errors,
type = "l",
col = "darkred",
lwd = 2,
main = "Forecast Error (Aktual - Prediksi)",
xlab = "Hari (dalam Testing Period)",
ylab = "Error (Rp)")
abline(h = 0, col = "blue", lty = 2, lwd = 2)
abline(h = c(mae_test, -mae_test), col = "green", lty = 3, lwd = 1.5)
legend("topright",
legend = c("Error", "MAE Boundary"),
col = c("darkred", "green"),
lty = c(1, 3),
lwd = 2)
grid(col = "gray80")
# Plot 3: Histogram Error
hist(errors,
main = "Distribusi Error Testing Data",
xlab = "Error (Rp)",
ylab = "Frekuensi",
breaks = 15,
col = "lightblue",
border = "steelblue")
abline(v = 0, col = "red", lty = 2, lwd = 2)
abline(v = c(mae_test, -mae_test), col = "green", lty = 3, lwd = 1.5)
par(mfrow = c(1, 1))
Mean Error 2.0429048
Median Error 1.7250179
Std Dev Error 5.2501131
Min Error -9.7940779
Max Error 13.2027289
Setelah validasi berhasil, dilakukan retrain model menggunakan semua data untuk forecast periode mendatang:
# Retrain dengan semua data
model_final <- arima(harga, order = c(0, 1, 0), method = "ML")
# Forecast 15 hari ke depan
h_forecast <- 15
forecast_final <- forecast(model_final, h = h_forecast)
print(forecast_final)
## Point Forecast Lo 80 Hi 80 Lo 95 Hi 95
## 271 102 98.79445 105.2055 97.09754 106.9025
## 272 102 97.46667 106.5333 95.06688 108.9331
## 273 102 96.44783 107.5522 93.50869 110.4913
## 274 102 95.58891 108.4111 92.19508 111.8049
## 275 102 94.83218 109.1678 91.03777 112.9622
## 276 102 94.14805 109.8520 89.99148 114.0085
## 277 102 93.51892 110.4811 89.02931 114.9707
## 278 102 92.93335 111.0667 88.13375 115.8662
## 279 102 92.38336 111.6166 87.29262 116.7074
## 280 102 91.86317 112.1368 86.49706 117.5029
## 281 102 91.36841 112.6316 85.74038 118.2596
## 282 102 90.89566 113.1043 85.01738 118.9826
## 283 102 90.44224 113.5578 84.32393 119.6761
## 284 102 90.00595 113.9941 83.65668 120.3433
## 285 102 89.58497 114.4150 83.01286 120.9871
Tabel di atas menunjukkan hasil forecasting untuk 15 hari ke depan
# Buat tabel hasil forecasting
forecast_table <- data.frame(
Hari_Ke = 1:h_forecast,
Prediksi = round(as.numeric(forecast_final$mean), 0),
Batas_Bawah_95 = round(as.numeric(forecast_final$lower[, 2]), 0),
Batas_Atas_95 = round(as.numeric(forecast_final$upper[, 2]), 0)
)
print(forecast_table)
## Hari_Ke Prediksi Batas_Bawah_95 Batas_Atas_95
## 1 1 102 97 107
## 2 2 102 95 109
## 3 3 102 94 110
## 4 4 102 92 112
## 5 5 102 91 113
## 6 6 102 90 114
## 7 7 102 89 115
## 8 8 102 88 116
## 9 9 102 87 117
## 10 10 102 86 118
## 11 11 102 86 118
## 12 12 102 85 119
## 13 13 102 84 120
## 14 14 102 84 120
## 15 15 102 83 121
Keterangan:
Prediksi: Nilai ramalan titik (point forecast)
Batas_Bawah/Atas_95: Interval kepercayaan 95% (lebih luas, lebih konservatif)
par(mfrow = c(2, 1), mar = c(4, 4, 3, 1))
# Plot 1: Forecast dengan base R
plot(forecast_final,
main = "Forecasting Harga Saham BOLA - 15 Hari Ke Depan",
xlab = "Periode",
ylab = "Harga (Rp)",
lwd = 2)
grid(col = "gray80")
# Plot 2: Forecast dengan autoplot (lebih cantik)
autoplot(forecast_final) +
ggtitle("Forecasting Harga Saham BOLA - ARIMA(2,1,1)") +
xlab("Periode") +
ylab("Harga (Rp)") +
theme_minimal() +
theme(
plot.title = element_text(hjust = 0.5, size = 14, face = "bold"),
panel.grid.major = element_line(colour = "gray80"),
panel.grid.minor = element_line(colour = "gray90")
)
par(mfrow = c(1, 1))
Interpretasi Forecast: - Garis biru = prediksi titik (point
forecast)
Area gelap = 95% confidence interval (range paling mungkin)
Confidence interval membesar seiring periode yang lebih jauh (ketidakpastian meningkat)
Berdasarkan analisis ARIMA yang telah dilakukan:
✓ Model ARIMA(2,1,1) berhasil dibangun dan divalidasi
✓ Akurasi model pada testing data: MAPE = 4.8738392%
✓ Model menunjukkan performa yang baik untuk forecasting jangka pendek
✓ Residual model sudah white noise (model adequately captures pattern)