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:
- [Data Wrangling] Persiapan & Transformasi Data
- [Statistik Deskriptif] Eksplorasi Deret Waktu
- [Uji Normalitas] Pengujian Asumsi Distribusi Log-Return
- [Simulasi] Estimasi Parameter & Monte Carlo
- [Analisis] Interval Kepercayaan & Proyeksi Final 🔗 Tautan Terkait: GitHub Repository | Dashboard Interaktif R Shiny—
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).
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 HTMLInstalasi: Jalankan
install.packages(c("dplyr", "lubridate", "stringr", "knitr", "kableExtra"))jika paket belum tersedia.
Data dibaca langsung dari file CSV hasil unduhan. Karena file
menggunakan encoding UTF-8 dengan BOM (Byte Order Mark),
parameter fileEncoding perlu disesuaikan.
## Dimensi data: 1268 baris x 7 kolom
## Nama kolom:
## [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)| 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)| 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% |
## '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%" ...
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 |
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
## 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)| 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 |
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)# 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)| 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 |
Kolom Tanggal bertipe karakter dengan format
"DD/MM/YYYY". Fungsi dmy() dari paket
lubridate digunakan untuk mem-parse format ini
secara langsung.
## Tipe kolom Tanggal: Date
## Jumlah NA pada kolom Tanggal: 0
## ✓ Semua tanggal berhasil dikonversi ke tipe Date.
##
## Rentang tanggal:
## Dari : 01 January 2021
## 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)| 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 |
Data awal diurutkan dari terbaru ke terlama (descending). Untuk analisis time series, data harus diurutkan kronologis dari terlama ke terbaru (ascending).
## 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 |
## 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
## === DATASET FINAL: df_usd_idr ===
## Dimensi : 1268 baris x 2 kolom
## Kolom : Tanggal, Kurs
## 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
## Missing val: 0
## 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.
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
# 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| 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 |
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")| 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 |
Tabel berikut merangkum seluruh transformasi yang dilakukan dalam proses data wrangling ini:
| 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 |
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:
## - df_usd_idr_clean.rds (untuk analisis R selanjutnya)
## - df_usd_idr_clean.csv (untuk dokumentasi / ekspor)
Dataset df_usd_idr yang dihasilkan dari proses ini siap
digunakan untuk tahapan analisis berikutnya:
# 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.9Dokumen ini dibuat dengan R Markdown dan dapat direproduksi sepenuhnya dengan menjalankan ulang seluruh chunk secara berurutan.