Tentang Proyek Portofolio Ini

Dokumen ini merupakan bagian dari rangkaian analisis peramalan kurs USD/IDR periode 2021–2025 menggunakan pemodelan stokastik Geometric Brownian Motion (GBM) dan Simulasi Monte Carlo. Proyek ini bertujuan untuk mengestimasi lintasan nilai tukar harian ke depan berdasarkan volatilitas dan drift historis.

Struktur Publikasi:

  1. [Data Wrangling] Persiapan & Transformasi Data
  2. [Statistik Deskriptif] Eksplorasi Deret Waktu
  3. [Uji Normalitas] Pengujian Asumsi Distribusi Log-Return
  4. [Simulasi] Estimasi Parameter & Monte Carlo
  5. [Analisis] Interval Kepercayaan & Proyeksi Final 🔗 Tautan Terkait: GitHub Repository | Dashboard Interaktif R Shiny

1 Latar Belakang

Nilai tukar USD/IDR merupakan salah satu indikator ekonomi yang sangat dinamis dan berpengaruh luas terhadap perekonomian Indonesia. Memahami pola pergerakannya secara historis adalah langkah pertama yang esensial sebelum membangun model prediktif.

Data yang digunakan dalam analisis ini adalah data historis harian kurs USD/IDR dari 1 Januari 2021 hingga 31 Desember 2025, bersumber dari Investing.com. Data mencakup 1.267 observasi dengan 7 variabel, namun analisis ini hanya memanfaatkan dua kolom utama: Tanggal (tanggal perdagangan) dan Terakhir (closing price harian).


2 Persiapan: Memuat Paket

Paket-paket berikut digunakan dalam tahap data wrangling ini:

# Manajemen data
library(dplyr)      # Manipulasi dataframe
library(lubridate)  # Parsing dan manipulasi tanggal
library(stringr)    # Manipulasi string

# Tabel output
library(knitr)      # Tabel output yang rapi
library(kableExtra) # Styling tabel HTML

Instalasi: Jalankan install.packages(c("dplyr", "lubridate", "stringr", "knitr", "kableExtra")) jika paket belum tersedia.


3 Tahap 1 — Import Data

3.1 Membaca File CSV

Data dibaca langsung dari file CSV hasil unduhan. Karena file menggunakan encoding UTF-8 dengan BOM (Byte Order Mark), parameter fileEncoding perlu disesuaikan.

df_raw <- read.csv(
  "../data/Data Historis USD_IDR.csv",
  header = TRUE,
  stringsAsFactors = FALSE,
  fileEncoding = "UTF-8-BOM"  # Menangani BOM pada file csv dari Investing.com

)

3.2 Inspeksi Awal

# Dimensi data
cat("Dimensi data:", nrow(df_raw), "baris x", ncol(df_raw), "kolom\n\n")
## Dimensi data: 1268 baris x 7 kolom
# Nama kolom
cat("Nama kolom:\n")
## Nama kolom:
print(names(df_raw))
## [1] "Tanggal"    "Terakhir"   "Pembukaan"  "Tertinggi"  "Terendah"  
## [6] "Vol."       "Perubahan."
# 6 baris pertama
kable(head(df_raw, 6),
      caption = "Tabel 1.1 — 6 Baris Pertama Data Mentah",
      align   = "c") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE)
Tabel 1.1 — 6 Baris Pertama Data Mentah
Tanggal Terakhir Pembukaan Tertinggi Terendah Vol. Perubahan.
31/12/2025 16.675,0 16.715,0 16.742,5 16.675,0 -0,51%
30/12/2025 16.760,0 16.780,0 16.789,0 16.760,0 -0,15%
29/12/2025 16.785,0 16.760,0 16.795,0 16.757,5 0,18%
26/12/2025 16.755,0 16.755,0 16.755,0 16.755,0 0,00%
25/12/2025 16.755,0 16.755,0 16.755,0 16.755,0 0,00%
24/12/2025 16.755,0 16.770,0 16.782,5 16.749,0 -0,09%
# 6 baris terakhir
kable(tail(df_raw, 6),
      caption = "Tabel 1.2 — 6 Baris Terakhir Data Mentah",
      align   = "c") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE)
Tabel 1.2 — 6 Baris Terakhir Data Mentah
Tanggal Terakhir Pembukaan Tertinggi Terendah Vol. Perubahan.
1263 08/01/2021 13.980,0 13.915,0 14.061,5 13.915,0 0,06K 0,65%
1264 07/01/2021 13.890,0 13.900,0 13.945,0 13.900,0 0,03K 0,07%
1265 06/01/2021 13.880,0 13.920,0 13.930,0 13.895,0 0,04K -0,14%
1266 05/01/2021 13.900,0 13.900,0 13.932,5 13.900,0 0,03K 0,11%
1267 04/01/2021 13.885,0 13.910,0 13.940,0 13.862,5 0,07K -2,31%
1268 01/01/2021 14.213,7 14.213,5 14.214,2 13.919,0 1,24%
# Tipe data setiap kolom
str(df_raw)
## 'data.frame':    1268 obs. of  7 variables:
##  $ Tanggal   : chr  "31/12/2025" "30/12/2025" "29/12/2025" "26/12/2025" ...
##  $ Terakhir  : chr  "16.675,0" "16.760,0" "16.785,0" "16.755,0" ...
##  $ Pembukaan : chr  "16.715,0" "16.780,0" "16.760,0" "16.755,0" ...
##  $ Tertinggi : chr  "16.742,5" "16.789,0" "16.795,0" "16.755,0" ...
##  $ Terendah  : chr  "16.675,0" "16.760,0" "16.757,5" "16.755,0" ...
##  $ Vol.      : chr  "" "" "" "" ...
##  $ Perubahan.: chr  "-0,51%" "-0,15%" "0,18%" "0,00%" ...

Temuan dari Inspeksi Awal

Berdasarkan inspeksi di atas, beberapa isu teridentifikasi:

# Kolom Isu Penanganan
1 Terakhir Format angka Indonesia: titik = pemisah ribuan, koma = desimal (contoh: "16.675,0") Konversi string → numerik
2 Tanggal Format string "DD/MM/YYYY" Konversi string → tipe Date
3 Vol. 766 nilai kosong Diabaikan (kolom tidak dipakai)
4 Urutan baris Terbaru ke terlama (descending) Dibalik menjadi ascending

4 Tahap 2 — Seleksi Kolom

Dari 7 kolom yang tersedia, hanya dua yang relevan untuk analisis ini.

df_selected <- df_raw %>%
  select(Tanggal, Terakhir)

cat("Kolom yang dipilih:", names(df_selected), "\n")
## Kolom yang dipilih: Tanggal Terakhir
cat("Jumlah baris:", nrow(df_selected), "\n")
## Jumlah baris: 1268
kable(head(df_selected, 8),
      caption = "Tabel 2.1 — Data Setelah Seleksi Kolom",
      align   = "c") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE)
Tabel 2.1 — Data Setelah Seleksi Kolom
Tanggal Terakhir
31/12/2025 16.675,0
30/12/2025 16.760,0
29/12/2025 16.785,0
26/12/2025 16.755,0
25/12/2025 16.755,0
24/12/2025 16.755,0
23/12/2025 16.770,0
22/12/2025 16.770,0

5 Tahap 3 — Konversi Format Angka

5.1 Logika Konversi

Kolom Terakhir menggunakan konvensi angka Indonesia: - Titik (.) → pemisah ribuan → harus dihapus - Koma (,) → pemisah desimal → harus diganti titik (.)

Setelah itu, string dapat dikonversi ke tipe numerik dengan as.numeric().

df_numeric <- df_selected %>%
  mutate(
    # Langkah 1: Hapus titik sebagai pemisah ribuan
    Terakhir_clean = str_remove_all(Terakhir, "\\."),
    # Langkah 2: Ganti koma desimal dengan titik
    Terakhir_clean = str_replace(Terakhir_clean, ",", "."),
    # Langkah 3: Konversi ke numerik
    Kurs = as.numeric(Terakhir_clean)
  ) %>%
  select(Tanggal, Kurs)

5.2 Verifikasi Konversi

# Cek apakah ada nilai NA yang tidak terduga setelah konversi
na_count <- sum(is.na(df_numeric$Kurs))
cat("Jumlah nilai NA pada kolom Kurs:", na_count, "\n")
## Jumlah nilai NA pada kolom Kurs: 0
if (na_count == 0) {
  cat("✓ Semua nilai berhasil dikonversi ke numerik tanpa NA.\n")
} else {
  cat("⚠ Terdapat", na_count, "nilai yang gagal dikonversi.\n")
}
## ✓ Semua nilai berhasil dikonversi ke numerik tanpa NA.
kable(head(df_numeric, 8),
      caption = "Tabel 3.1 — Data Setelah Konversi Numerik",
      align   = "c",
      digits  = 1) %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE)
Tabel 3.1 — Data Setelah Konversi Numerik
Tanggal Kurs
31/12/2025 16675
30/12/2025 16760
29/12/2025 16785
26/12/2025 16755
25/12/2025 16755
24/12/2025 16755
23/12/2025 16770
22/12/2025 16770

6 Tahap 4 — Konversi Format Tanggal

6.1 Parsing Tanggal

Kolom Tanggal bertipe karakter dengan format "DD/MM/YYYY". Fungsi dmy() dari paket lubridate digunakan untuk mem-parse format ini secara langsung.

df_dated <- df_numeric %>%
  mutate(
    Tanggal = dmy(Tanggal)   # dmy() = day-month-year parser dari lubridate
  )

6.2 Verifikasi Konversi Tanggal

cat("Tipe kolom Tanggal:", class(df_dated$Tanggal), "\n")
## Tipe kolom Tanggal: Date
na_date <- sum(is.na(df_dated$Tanggal))
cat("Jumlah NA pada kolom Tanggal:", na_date, "\n")
## Jumlah NA pada kolom Tanggal: 0
if (na_date == 0) {
  cat("✓ Semua tanggal berhasil dikonversi ke tipe Date.\n")
}
## ✓ Semua tanggal berhasil dikonversi ke tipe Date.
cat("\nRentang tanggal:\n")
## 
## Rentang tanggal:
cat("  Dari  :", format(min(df_dated$Tanggal), "%d %B %Y"), "\n")
##   Dari  : 01 January 2021
cat("  Hingga:", format(max(df_dated$Tanggal), "%d %B %Y"), "\n")
##   Hingga: 31 December 2025
kable(head(df_dated, 8),
      caption = "Tabel 4.1 — Data Setelah Konversi Tanggal",
      align   = "c") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE)
Tabel 4.1 — Data Setelah Konversi Tanggal
Tanggal Kurs
2025-12-31 16675
2025-12-30 16760
2025-12-29 16785
2025-12-26 16755
2025-12-25 16755
2025-12-24 16755
2025-12-23 16770
2025-12-22 16770

7 Tahap 5 — Pengurutan Data (Ascending)

Data awal diurutkan dari terbaru ke terlama (descending). Untuk analisis time series, data harus diurutkan kronologis dari terlama ke terbaru (ascending).

df_sorted <- df_dated %>%
  arrange(Tanggal)

7.1 Verifikasi Urutan

cat("5 baris PERTAMA setelah diurutkan (seharusnya data terlama):\n")
## 5 baris PERTAMA setelah diurutkan (seharusnya data terlama):
kable(head(df_sorted, 5),
      align = "c") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE)
Tanggal Kurs
2021-01-01 14213.7
2021-01-04 13885.0
2021-01-05 13900.0
2021-01-06 13880.0
2021-01-07 13890.0
cat("5 baris TERAKHIR setelah diurutkan (seharusnya data terbaru):\n")
## 5 baris TERAKHIR setelah diurutkan (seharusnya data terbaru):
kable(tail(df_sorted, 5),
      align = "c") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE)
Tanggal Kurs
1264 2025-12-25 16755
1265 2025-12-26 16755
1266 2025-12-29 16785
1267 2025-12-30 16760
1268 2025-12-31 16675
# Konfirmasi urutan monoton naik
is_sorted <- all(diff(as.numeric(df_sorted$Tanggal)) > 0)
cat("✓ Data terurut secara kronologis (ascending):", is_sorted, "\n")
## ✓ Data terurut secara kronologis (ascending): TRUE

8 Tahap 6 — Dataset Final & Validasi

8.1 Penamaan Dataset Bersih

# Dataset bersih siap analisis
df_usd_idr <- df_sorted

cat("=== DATASET FINAL: df_usd_idr ===\n")
## === DATASET FINAL: df_usd_idr ===
cat("Dimensi    :", nrow(df_usd_idr), "baris x", ncol(df_usd_idr), "kolom\n")
## Dimensi    : 1268 baris x 2 kolom
cat("Kolom      :", paste(names(df_usd_idr), collapse = ", "), "\n")
## Kolom      : Tanggal, Kurs
cat("Tipe data  :", paste(sapply(df_usd_idr, class), collapse = ", "), "\n")
## Tipe data  : Date, numeric
cat("Periode    :", format(min(df_usd_idr$Tanggal), "%d %B %Y"),
    "–", format(max(df_usd_idr$Tanggal), "%d %B %Y"), "\n")
## Periode    : 01 January 2021 – 31 December 2025
cat("Missing val:", sum(is.na(df_usd_idr)), "\n")
## Missing val: 0

8.2 Cek Duplikasi Tanggal

dup_count <- sum(duplicated(df_usd_idr$Tanggal))
cat("Jumlah tanggal duplikat:", dup_count, "\n")
## Jumlah tanggal duplikat: 0
if (dup_count == 0) {
  cat("✓ Tidak ada duplikasi tanggal. Setiap baris adalah hari perdagangan unik.\n")
}
## ✓ Tidak ada duplikasi tanggal. Setiap baris adalah hari perdagangan unik.

8.3 Cek Rentang Nilai Kurs

cat("Nilai minimum kurs:", format(min(df_usd_idr$Kurs), big.mark = ".", decimal.mark = ","), "IDR\n")
## Nilai minimum kurs: 13.880 IDR
cat("Nilai maksimum kurs:", format(max(df_usd_idr$Kurs), big.mark = ".", decimal.mark = ","), "IDR\n")
## Nilai maksimum kurs: 16.870 IDR
cat("Selisih min–max    :", format(max(df_usd_idr$Kurs) - min(df_usd_idr$Kurs),
                                   big.mark = ".", decimal.mark = ","), "IDR\n")
## Selisih min–max    : 2.990 IDR

8.4 Ringkasan Statistik Komprehensif

# Ringkasan statistik kolom Kurs menggunakan fungsi base R
kurs_summary <- data.frame(
  Statistik = c(
    "Jumlah Observasi",
    "Jumlah NA",
    "Nilai Minimum",
    "Kuartil 1 (Q1)",
    "Median",
    "Mean",
    "Kuartil 3 (Q3)",
    "Nilai Maksimum",
    "Standar Deviasi",
    "Rentang (Max - Min)"
  ),
  Nilai = c(
    nrow(df_usd_idr),
    sum(is.na(df_usd_idr$Kurs)),
    round(min(df_usd_idr$Kurs), 1),
    round(quantile(df_usd_idr$Kurs, 0.25), 1),
    round(median(df_usd_idr$Kurs), 1),
    round(mean(df_usd_idr$Kurs), 1),
    round(quantile(df_usd_idr$Kurs, 0.75), 1),
    round(max(df_usd_idr$Kurs), 1),
    round(sd(df_usd_idr$Kurs), 1),
    round(max(df_usd_idr$Kurs) - min(df_usd_idr$Kurs), 1)
  )
)

kable(kurs_summary,
      caption = "Tabel 6.1 — Ringkasan Statistik Kolom Kurs (IDR)",
      align   = c("l", "r"),
      col.names = c("Statistik", "Nilai (IDR)")) %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed", "bordered"),
                full_width = FALSE) %>%
  row_spec(c(3, 8), bold = TRUE, color = "#c0392b") %>%   # min & max
  row_spec(6, bold = TRUE, color = "#2c7bb6")             # mean
Tabel 6.1 — Ringkasan Statistik Kolom Kurs (IDR)
Statistik Nilai (IDR)
Jumlah Observasi 1268.0
Jumlah NA 0.0
Nilai Minimum 13880.0
Kuartil 1 (Q1) 14515.0
Median 15420.0
Mean 15358.2
Kuartil 3 (Q3) 16170.0
Nilai Maksimum 16870.0
Standar Deviasi 818.8
Rentang (Max - Min) 2990.0

8.5 Tampilan Dataset Final

kable(head(df_usd_idr, 10),
      caption = "Tabel 6.1 — 10 Baris Pertama Dataset Final (df_usd_idr)",
      align   = "c",
      digits  = 1,
      col.names = c("Tanggal", "Kurs Penutupan (IDR)")) %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed", "bordered"),
                full_width = FALSE) %>%
  column_spec(2, bold = TRUE, color = "#2c7bb6")
Tabel 6.1 — 10 Baris Pertama Dataset Final (df_usd_idr)
Tanggal Kurs Penutupan (IDR)
2021-01-01 14213.7
2021-01-04 13885.0
2021-01-05 13900.0
2021-01-06 13880.0
2021-01-07 13890.0
2021-01-08 13980.0
2021-01-11 14080.0
2021-01-12 14120.0
2021-01-13 14055.0
2021-01-14 14050.0

9 Ringkasan Tahapan Data Wrangling

Tabel berikut merangkum seluruh transformasi yang dilakukan dalam proses data wrangling ini:

Tabel 7.1 — Ringkasan Proses Data Wrangling
Tahap Aksi Input Output
1 Import data CSV File CSV mentah (7 kolom, 1.267 baris) df_raw (7 kolom, 1.267 baris)
2 Seleksi kolom df_raw (7 kolom) df_selected (2 kolom: Tanggal, Terakhir)
3 Konversi tipe numerik Kolom Terakhir: string format Indonesia Kolom Kurs: numerik (double)
4 Konversi tipe tanggal Kolom Tanggal: string ‘DD/MM/YYYY’ Kolom Tanggal: tipe Date
5 Pengurutan kronologis Data urutan descending (terbaru ke terlama) df_sorted (urutan ascending / kronologis)
6 Validasi dataset final df_sorted df_usd_idr — dataset bersih & siap analisis

10 Simpan Dataset Bersih

Untuk keperluan tahap analisis berikutnya, dataset bersih disimpan sebagai file RDS (format biner R yang efisien) dan CSV.

# Simpan sebagai RDS (direkomendasikan untuk lanjutan analisis R)
saveRDS(df_usd_idr, "df_usd_idr_clean.rds")

# Simpan juga sebagai CSV (untuk keperluan dokumentasi / portabilitas)
write.csv(df_usd_idr,
          "df_usd_idr_clean.csv",
          row.names = FALSE,
          fileEncoding = "UTF-8")

cat("✓ Dataset disimpan sebagai:\n")
## ✓ Dataset disimpan sebagai:
cat("  - df_usd_idr_clean.rds  (untuk analisis R selanjutnya)\n")
##   - df_usd_idr_clean.rds  (untuk analisis R selanjutnya)
cat("  - df_usd_idr_clean.csv  (untuk dokumentasi / ekspor)\n")
##   - df_usd_idr_clean.csv  (untuk dokumentasi / ekspor)

11 Langkah Selanjutnya

Dataset df_usd_idr yang dihasilkan dari proses ini siap digunakan untuk tahapan analisis berikutnya:

  • Bagian 2 — Statistik Deskriptif: Menghitung mean, median, standar deviasi, dan statistik log-return harian (\(r_t = \ln(S_t/S_{t-1})\)).
  • Bagian 3 — Visualisasi: Plot time series kurs, histogram log-return, dan QQ-plot (dibangun dengan R Shiny untuk interaktivitas penuh).
  • Bagian 4 — Uji Normalitas: Kolmogorov-Smirnov test sebagai validasi asumsi GBM.
  • Bagian 5 — Estimasi Parameter GBM & Simulasi Monte Carlo: Estimasi \(\mu\) (drift) dan \(\sigma\) (volatilitas) serta simulasi 1.000, 5.000, dan 10.000 iterasi.

# Informasi sesi R untuk reprodusibilitas
sessionInfo()
## R version 4.5.1 (2025-06-13 ucrt)
## Platform: x86_64-w64-mingw32/x64
## Running under: Windows 11 x64 (build 26200)
## 
## Matrix products: default
##   LAPACK version 3.12.1
## 
## locale:
## [1] LC_COLLATE=English_United States.utf8 
## [2] LC_CTYPE=English_United States.utf8   
## [3] LC_MONETARY=English_United States.utf8
## [4] LC_NUMERIC=C                          
## [5] LC_TIME=English_United States.utf8    
## 
## time zone: Asia/Jakarta
## tzcode source: internal
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
## [1] kableExtra_1.4.0 knitr_1.51       stringr_1.6.0    lubridate_1.9.4 
## [5] dplyr_1.2.0     
## 
## loaded via a namespace (and not attached):
##  [1] jsonlite_2.0.0     compiler_4.5.1     tidyselect_1.2.1   xml2_1.5.2        
##  [5] jquerylib_0.1.4    systemfonts_1.3.2  scales_1.4.0       textshaping_1.0.5 
##  [9] yaml_2.3.12        fastmap_1.2.0      R6_2.6.1           generics_0.1.4    
## [13] tibble_3.3.0       svglite_2.2.2      bslib_0.10.0       pillar_1.11.1     
## [17] RColorBrewer_1.1-3 rlang_1.1.7        cachem_1.1.0       stringi_1.8.7     
## [21] xfun_0.56          sass_0.4.10        otel_0.2.0         viridisLite_0.4.2 
## [25] timechange_0.3.0   cli_3.6.5          withr_3.0.2        magrittr_2.0.4    
## [29] digest_0.6.38      rstudioapi_0.18.0  lifecycle_1.0.5    vctrs_0.7.1       
## [33] evaluate_1.0.5     glue_1.8.0         farver_2.1.2       rmarkdown_2.30    
## [37] tools_4.5.1        pkgconfig_2.0.3    htmltools_0.5.9

Dokumen ini dibuat dengan R Markdown dan dapat direproduksi sepenuhnya dengan menjalankan ulang seluruh chunk secara berurutan.