Statistik pada Bagian 3: Menganalisis Statistik sudah memetakan masa lalu; pertanyaan tim kini mengarah ke depan: dalam kisaran berapa harga 30 hari ke depan? Jawaban semacam itu tidak bisa dibaca dari tabel — ia harus dijalankan. Model paling sederhana yang jujur adalah random walk: harga besok adalah harga hari ini dikalikan (1 + drift + guncangan), persis mekanisme pembangun deret pada Bagian 1: Menyiapkan Data Harga, hanya saja kini np.cumprod berjalan 30 langkah ke depan mulai dari harga penutupan.

Seluruh masukan model diambil dari statistik historis, bukan dari perasaan: drift harian terukur (pct.mean) dan volatilitas harian (pct.std) dari Bagian 2: Menghitung Perubahan Harga. Generator np.random.default_rng(7) menjaga seluruh proyeksi tetap dapat direproduksi (Chapter Reproducibility dan Random Seed dan default_rng dan Generator; penerapannya pada konteks lebih luas dibahas di Chapter NumPy untuk Simulasi). Dalam struktur proyek, blok-blok bagian ini kita kemas sebagai src/simulasi.py dan pemeriksaannya sebagai tests/pemeriksaan_simulasi.py.

Titik Awal dan Parameter Simulasi #

Proyeksi berangkat dari kondisi terakhir yang benar-benar diketahui — penutupan 26 Desember — sambil membawa dua statistik historis sebagai mesinnya:

python
import numpy as np

# Pembangun deret harga 2025 (identik dengan Bagian 1).
rng = np.random.default_rng(42)
awal = np.array([15000.0, 45000.0, 18000.0])
drift = np.array([0.0003, 0.0012, 0.0004])
sigma = np.array([0.006, 0.030, 0.010])
musim = 1 - np.array([0.01, 0.15, 0.03]) * np.cos(2 * np.pi * np.arange(360) / 360)[:, None]
guncangan = drift + rng.normal(0.0, sigma, (360, 3))
harga = awal * np.cumprod(1 + guncangan, axis=0) * musim

pct = harga[1:] / harga[:-1] - 1
mulai = harga[-1]
drift_h = pct.mean(axis=0)
vol = pct.std(axis=0, ddof=1)
print("harga terakhir     :", np.round(mulai, 2))
print("drift historis %/hr:", np.round(drift_h * 100, 4))
print("volatilitas harian :", np.round(vol, 4))

Output yang Dihasilkan:

text
harga terakhir     : [16825.21 33284.1  16810.88]
drift historis %/hr: [ 0.0361  0.0118 -0.0082]
volatilitas harian : [0.0062 0.029  0.0096]

Rinciannya:

  1. mulai = harga[-1]: baris terakhir deret; titik jangkar seluruh lintasan simulasi.
  2. drift_h = pct.mean(axis=0): tren harian yang terukur: beras +0.0361%, cabai +0.0118%, minyak goreng −0.0082% per hari — kecil, bahkan berbeda tanda. Ini rata-rata aritmetika perubahan harian, bukan pertumbuhan majemuk: cabai yang sangat volatil ditutup 26% di bawah pembukaannya (Bagian 1: Menyiapkan Data Harga) meski drift hariannya tetap positif kecil.
  3. vol = pct.std(axis=0, ddof=1): asumsi bahwa guncangan 30 hari ke depan sebesar masa lalu — asumsi paling berani dalam model ini, dan layak dinyatakan terang-terangan.

Tiga Skenario Drift #

Satu lintasan tunggal terlalu percaya diri, maka kita jalankan tiga skenario yang hanya berbeda drift: optimis (tren terukur +0.20%/hari), netral (tren terukur berlanjut apa adanya), dan pesimis (tren terukur −0.20%/hari). Kunci perbandingan yang adil ada pada generator: rng baru dengan seed yang sama dibuat di dalam loop sehingga ketiga skenario menggambar guncangan yang identik — selisih hasilnya murni efek drift:

python
import numpy as np

# Pembangun deret harga 2025 (identik dengan Bagian 1).
rng = np.random.default_rng(42)
awal = np.array([15000.0, 45000.0, 18000.0])
drift = np.array([0.0003, 0.0012, 0.0004])
sigma = np.array([0.006, 0.030, 0.010])
musim = 1 - np.array([0.01, 0.15, 0.03]) * np.cos(2 * np.pi * np.arange(360) / 360)[:, None]
guncangan = drift + rng.normal(0.0, sigma, (360, 3))
harga = awal * np.cumprod(1 + guncangan, axis=0) * musim

pct = harga[1:] / harga[:-1] - 1
mulai = harga[-1]
drift_h = pct.mean(axis=0)
vol = pct.std(axis=0, ddof=1)
for label, d in (("optimis", drift_h + 0.002), ("netral", drift_h), ("pesimis", drift_h - 0.002)):
    rng_sim = np.random.default_rng(7)
    faktor = 1 + d + rng_sim.normal(0.0, vol, (30, 3))
    akhir = mulai * np.cumprod(faktor, axis=0)[-1]
    print(f"{label:<8}", np.round(akhir, 2))

Output yang Dihasilkan:

text
optimis  [16796.11 41232.09 16395.93]
netral   [15816.96 38844.42 15439.37]
pesimis  [14893.11 36590.66 14536.87]

Harga akhir per skenario dirangkum dalam tabel:

SkenarioDrift harianberas (Rp)cabai (Rp)minyak goreng (Rp)
optimisdrift_h + 0.20%16 79641 23216 396
netraldrift_h15 81738 84415 439
pesimisdrift_h - 0.20%14 89336 59114 537

Mekanismenya:

  1. Tuple skenario: drift netral adalah drift_h sendiri — “tren historis berlanjut”; optimis dan pesimis menggesernya ±0.002 per hari.
  2. rng_sim = np.random.default_rng(7) di dalam loop: seed di-reset tiap skenario sehingga rng_sim.normal(0.0, vol, (30, 3)) menghasilkan matriks guncangan yang sama persis untuk ketiganya.
  3. np.cumprod(faktor, axis=0)[-1]: tiga puluh langkah random walk dari mulai; hanya harga akhir yang diambil.
  4. Bacaan: urutan optimis > netral > pesimis berlaku pada ketiga komoditas — terjamin karena faktor harian optimis lebih besar setiap hari. Tetapi dibanding titik awal, netral berakhir −5.99% (beras), +16.71% (cabai), dan −8.16% (minyak goreng): tiga puluh tarikan seed 7 kebetulan condong negatif untuk beras dan minyak goreng. Satu lintasan adalah satu kebetulan, bukan ekspektasi.

Membandingkan dengan Tren Historis #

Seberapa besar andil drift dibanding kebisingan dalam 30 hari? Bandingkan simulasi netral dengan proyeksi tanpa keacakan — harga awal dikalikan faktor tren 30 kali:

python
import numpy as np

# Pembangun deret harga 2025 (identik dengan Bagian 1).
rng = np.random.default_rng(42)
awal = np.array([15000.0, 45000.0, 18000.0])
drift = np.array([0.0003, 0.0012, 0.0004])
sigma = np.array([0.006, 0.030, 0.010])
musim = 1 - np.array([0.01, 0.15, 0.03]) * np.cos(2 * np.pi * np.arange(360) / 360)[:, None]
guncangan = drift + rng.normal(0.0, sigma, (360, 3))
harga = awal * np.cumprod(1 + guncangan, axis=0) * musim

pct = harga[1:] / harga[:-1] - 1
mulai = harga[-1]
drift_h = pct.mean(axis=0)
vol = pct.std(axis=0, ddof=1)
prediksi = mulai * (1 + drift_h) ** 30
rng_sim = np.random.default_rng(7)
netral = mulai * np.cumprod(1 + drift_h + rng_sim.normal(0.0, vol, (30, 3)), axis=0)[-1]
print("prediksi tren   :", np.round(prediksi, 2))
print("simulasi netral :", np.round(netral, 2))
print("selisih         :", np.round(netral - prediksi, 2))
print("rentang 1 sigma :", np.round(mulai * vol * np.sqrt(30), 2))

Output yang Dihasilkan:

text
prediksi tren   : [17008.5  33401.72 16769.79]
simulasi netral : [15816.96 38844.42 15439.37]
selisih         : [-1191.54  5442.7  -1330.42]
rentang 1 sigma : [ 572.66 5284.09  883.26]

Pembacaannya:

  1. mulai * (1 + drift_h) ** 30: lintasan deterministik “seandainya tren terukur berjalan sempurna” — +1.09% (beras), +0.35% (cabai), −0.24% (minyak goreng) dalam sebulan.
  2. mulai * vol * np.sqrt(30): derau random walk tumbuh sebanding akar waktu, bukan sebanding waktu (Chapter Normal Distribution); hasilnya perkiraan rentang 1 sigma harga akhir dalam rupiah.
  3. Selisih −1 191.54, +5 442.70, −1 330.42 berjarak 2.08, 1.03, dan 1.51 sigma dari prediksi — wajar untuk satu lintasan. Yang mengejutkan adalah skalanya: kontribusi drift 30 hari cabai hanya sekitar +118 rupiah, sedangkan 1 sigma-nya ±5 284 — dalam horizon sebulan, tren cabai praktis tak terlihat di balik kebisingannya.

Verifikasi Proyek #

Klaim utama proyek — simulasi yang deterministik dan skenario yang terurut — patut diuji otomatis, bukan dilihat sekilas. Dalam struktur proyek, pemeriksaan ini tersimpan sebagai tests/pemeriksaan_simulasi.py dan memakai numpy.testing (Chapter numpy.testing dan assert_allclose) mengikuti pola Chapter Reproducible Numerical Tests:

python
import numpy as np
from numpy.testing import assert_allclose, assert_array_equal

# Pembangun deret harga 2025 (identik dengan Bagian 1).
rng = np.random.default_rng(42)
awal = np.array([15000.0, 45000.0, 18000.0])
drift = np.array([0.0003, 0.0012, 0.0004])
sigma = np.array([0.006, 0.030, 0.010])
musim = 1 - np.array([0.01, 0.15, 0.03]) * np.cos(2 * np.pi * np.arange(360) / 360)[:, None]
guncangan = drift + rng.normal(0.0, sigma, (360, 3))
harga = awal * np.cumprod(1 + guncangan, axis=0) * musim

pct = harga[1:] / harga[:-1] - 1
vol = pct.std(axis=0, ddof=1)
mulai = harga[-1]
drift_h = pct.mean(axis=0)

def jalankan(arah):
    rng_sim = np.random.default_rng(7)
    faktor = 1 + arah + rng_sim.normal(0.0, vol, (30, 3))
    return mulai * np.cumprod(faktor, axis=0)

proyeksi = jalankan(drift_h)
assert proyeksi.shape == (30, 3)
assert_array_equal(proyeksi, jalankan(drift_h))
assert np.all(proyeksi > 0)
assert np.all(jalankan(drift_h + 0.002) > jalankan(drift_h - 0.002))
assert_allclose(np.round(vol, 4), [0.0062, 0.0290, 0.0096])
print("Semua pemeriksaan lulus")

Output yang Dihasilkan:

text
Semua pemeriksaan lulus

Isi tiap pemeriksaan:

  1. jalankan(arah): fungsi simulasi berparameter drift — inti src/simulasi.py dalam wujud mini.
  2. assert_array_equal(proyeksi, jalankan(drift_h)): simulasi dijalankan ulang dan dituntut sama bit demi bit — uji determinisme yang gagal seketika bila seed tidak di-set.
  3. np.all(jalankan(drift_h + 0.002) > jalankan(drift_h - 0.002)): urutan skenario diverifikasi pada setiap hari, bukan hanya di harga akhir.
  4. assert_allclose(np.round(vol, 4), [0.0062, 0.0290, 0.0096]): mengikat hasil ke angka volatilitas yang dicetak Bagian 2: Menghitung Perubahan Harga — konsistensi antar bagian ikut teruji.

Laporan #

Seluruh temuan proyek dirangkum ke dalam laporan/ringkasan.md. Tabel berikut menghimpun angka-angka kunci dari empat bagian — semuanya direproduksi dari seed yang sama:

Besaranberascabaiminyak goreng
Harga pembukaan 2025 (Rp)15 00045 00018 000
Harga penutupan 26 Desember (Rp)16 82533 28416 811
Rentang teramati (Rp)14 764 - 16 82530 267 - 52 83016 042 - 21 901
Volatilitas harian0.00620.02900.0096
Rata-rata / median harga (Rp)15 616 / 15 65439 532 / 38 48818 347 / 18 073
Korelasi perubahan dengan beras1.0000.063-0.012
Simulasi 30 hari, optimis (Rp)16 79641 23216 396
Simulasi 30 hari, netral (Rp)15 81738 84415 439
Simulasi 30 hari, pesimis (Rp)14 89336 59114 537

Empat temuan utamanya:

  • Volatilitas adalah pembeda utama: cabai (0.0290) hampir lima kali beras (0.0062); lonjakan hariannya mencapai +3 043.55 (16 Februari) dan penurunannya −3 744.06 (13 November — tepat di hari terendahnya).
  • Tren 2025 terukur kecil: drift harian hanya antara −0.0082% dan +0.0361%; beras ditutup justru di titik tertingginya (26 Desember), sedangkan puncak cabai (23 April) dan minyak goreng (3 Juli) terjadi di pertengahan tahun.
  • Korelasi antar komoditas dekat nol (−0.012 s.d. 0.063 pada perubahan harian): ketiganya digerakkan guncangan independen, bukan faktor bersama.
  • Horizon 30 hari milik kebisingan: rentang 1 sigma (±573 hingga ±5 284 Rp) melampaui kontribusi tren; skenario tetap berguna sebagai kisaran, bukan ramalan.

Proyek selesai di sini: dari pembangun deret di src/persiapan_harga.py hingga tabel di laporan/ringkasan.md, setiap angka lahir dari seed yang sama dan dapat direproduksi siapa pun — standar kerja numerik yang pantas dibawa pulang dari kursus ini.