Proyek keempat melangkah dari menjelaskan masa lalu ke memperkirakan masa depan: kita membangun model prediksi harga komoditas dengan fitur deret waktu. Alurnya utuh satu lingkaran — merumuskan masalah prediksi, mengumpulkan data historis, memvalidasi dan membersihkannya, mengeksplorasi pola deret, membuat fitur lag, membangun baseline naive, melatih model machine learning, mengevaluasi secara jujur dengan pembagian kronologis, menganalisis kesalahan, membahas ketidakpastian prakiraan secara terbuka, lalu menutupnya dengan laporan. Seluruh angka dihitung oleh satu pipeline yang deterministik, sehingga siapa pun dapat mengulang hasilnya dari berkas yang sama.

Cerita: Belanja Cabai Bu Warni #

Proyek ini melanjutkan kisah Warung Nasi Bu Warni di Surabaya. Pada proyek sebelumnya kita membuktikan bahwa cabai merah adalah bahan paling bergejolak di dapurnya — koefisien variasinya 28,91%, jauh di atas beras dan gula pasir. Bu Warni kini mengajukan pertanyaan yang lebih tajam: daripada hanya tahu harga sudah melonjak, ia ingin tahu sebelum melonjak. Berapa perkiraan harga cabai minggu depan, agar ia bisa memutuskan hari ini: belanja berapa kilo, menunda atau mengejar, dan kapan menu andalan perlu digeser.

Untuk itu kita memakai catatan harga mingguan cabai merah selama 36 minggu — W1 sampai W36, dari minggu yang dimulai 2 Juni 2025 sampai minggu yang dimulai 2 Februari 2026. Deret ini memuat tren naik ringan, gelombang musiman 8-mingguan yang mengikuti siklus panen, satu lonjakan tajam saat awal musim hujan, dan — sebagaimana data dunia nyata — dua masalah kecil pencatatan yang harus kita temukan dan perbaiki lebih dulu.

Isi Proyek Ini #

Proyek tersusun atas tiga berkas pendukung dan dua belas bagian pengerjaan:

BerkasPeran
data/harga_mingguan.csvDataset mentah: harga mingguan cabai merah, 38 baris
src/prediksi_harga.pyPipeline utuh dari muat data sampai penulisan laporan
laporan/laporan-akhir.txtLaporan akhir yang ditulis otomatis oleh pipeline
part-1-… sampai part-12-…Tahapan pengerjaan berurutan, satu topik per bagian

Untuk menjalankan pipeline di komputer Anda, unduh ketiga berkas pendukung dari halaman ini (tautan unduh ada pada kepala tiap blok kode di bawah), susun dengan struktur folder yang sama, lalu jalankan dari folder proyek:

bash
python3 src/prediksi_harga.py

Pipeline ini deterministik: tanpa akses jaringan, tanpa pembacaan jam sistem, tanpa angka acak, dan selalu menghasilkan laporan yang identik dari data yang sama.

Dataset Harga Mingguan #

Berkas data/harga_mingguan.csv berbentuk deret mingguan: satu baris per minggu, dengan tiga kolom.

KolomMakna
mingguPenanda minggu W1–W36; W1 adalah minggu yang dimulai 2 Juni 2025
tanggal_mulaiTanggal Senin tempat minggu itu dimulai, format YYYY-MM-DD
harga_ribuHarga rata-rata cabai merah minggu itu, ribu rupiah per kilogram

Dengan 36 minggu, seharusnya ada tepat 36 baris. Berikut isi lengkapnya — perhatikan bahwa barisnya lebih banyak dari 36, dan ada satu sel yang bukan angka:

csv
minggu,tanggal_mulai,harga_ribu
W1,2025-06-02,40.1
W2,2025-06-09,40.1
W3,2025-06-16,40.1
W4,2025-06-23,38.8
W5,2025-06-30,36.4
W6,2025-07-07,35.9
W7,2025-07-14,37.2
W8,2025-07-21,38.6
W9,2025-07-28,39.9
W10,2025-08-04,41.4
W10,2025-08-04,41.4
W10,2025-08-04,41.4
W11,2025-08-11,40.7
W12,2025-08-18,38.5
W13,2025-08-25,37.6
W14,2025-09-01,36.8
W15,2025-09-08,37.2
W16,2025-09-15,39.6
W17,2025-09-22,41.2
W18,2025-09-29,41.5
W19,2025-10-06,51.0
W20,2025-10-13,53.1
W21,2025-10-20,47.2
W22,2025-10-27,39.9
W23,2025-11-03,37.6
W24,2025-11-10,40.1
W25,2025-11-17,41.8
W26,2025-11-24,42.2
W27,2025-12-01,-
W28,2025-12-08,40.5
W29,2025-12-15,38.2
W30,2025-12-22,38.1
W31,2025-12-29,39.1
W32,2026-01-05,40.2
W33,2026-01-12,42.5
W34,2026-01-19,43.0
W35,2026-01-26,41.9
W36,2026-02-02,40.9

Pipeline Prediksi #

Seluruh alur dirangkai dalam satu skrip, src/prediksi_harga.py. Skrip ini memuat data, memeriksa masalahnya, membersihkannya, membaca pola deret, membuat fitur lag, membagi latih/uji secara kronologis, menghitung baseline naive, melatih LinearRegression, mengevaluasi MAE dan RMSE, membedah residu, memprakirakan 4 minggu ke depan beserta intervalnya, lalu menulis laporan lengkap ke laporan/laporan-akhir.txt.

python
"""prediksi_harga.py: pipeline prediksi harga cabai merah Warung Nasi Bu Warni.

Pipeline ini menjalankan alur utuh satu proyek prediksi deret waktu:
muat data mentah, periksa masalah, bersihkan, eksplorasi deret, buat fitur
lag, pisahkan latih/uji secara kronologis, hitung baseline naive, latih
LinearRegression, evaluasi MAE dan RMSE, analisis residu, prakirakan 4
minggu ke depan beserta interval ketidakpastiannya, lalu tulis laporan
lengkap ke laporan/laporan-akhir.txt.

Cara menjalankan (dari folder proyek prediksi-harga-komoditas):

    python3 src/prediksi_harga.py

Pipeline ini deterministik: tidak membaca jam sistem, tidak mengakses
jaringan, tidak memakai angka acak, dan selalu menghasilkan laporan yang
identik dari data yang sama.
"""

from datetime import timedelta
from pathlib import Path

import numpy as np
import pandas as pd
from sklearn.linear_model import LinearRegression

BERKAS_DATA = Path("data/harga_mingguan.csv")
BERKAS_LAPORAN = Path("laporan/laporan-akhir.txt")

NAMA_KOMODITAS = "cabai merah"
MINGGU_UJI = 6          # 6 minggu terakhir sebagai data uji
HORIZON = 4             # prakiraan 4 minggu ke depan
PEKAN_W1 = "2 Juni 2025"
PEKAN_W36 = "2 Februari 2026"


def muat_data() -> pd.DataFrame:
    """Baca data mentah harga mingguan dari berkas CSV."""
    df = pd.read_csv(BERKAS_DATA)
    print(f"[1] Muat data mentah   : {df.shape[0]} baris, {df.shape[1]} kolom")
    return df


def periksa_masalah(df: pd.DataFrame) -> dict:
    """Periksa urutan minggu, duplikat, sel '-', dan kewajaran rentang harga."""
    nomor = df["minggu"].str.extract(r"(\d+)")[0].astype(int)
    rentang = df["tanggal_mulai"].agg(["min", "max"])
    angka = pd.to_numeric(df["harga_ribu"], errors="coerce")

    hitung = df["minggu"].value_counts()
    minggu_dup = sorted(hitung[hitung > 1].index.tolist())
    minggu_strip = df.loc[df["harga_ribu"] == "-", "minggu"].tolist()

    masalah = {
        "baris": df.shape[0],
        "dup": int(df.duplicated(subset=["minggu"]).sum()),
        "minggu_dup": minggu_dup,
        "minggu_strip": minggu_strip,
        "urutan_utuh": nomor.is_unique and nomor.min() == 1,
        "awal": rentang["min"],
        "akhir": rentang["max"],
        "harga_min": float(angka.min()),
        "harga_max": float(angka.max()),
    }
    print(
        f"[2] Temuan validasi    : {masalah['dup']} baris duplikat ({', '.join(minggu_dup)});"
        f" 1 sel harga '-' ({', '.join(minggu_strip)});"
    )
    print(
        f"    urutan minggu W1-W{nomor.max()} utuh, {rentang['min']} s.d. {rentang['max']};"
        f" harga wajar {masalah['harga_min']:.1f}-{masalah['harga_max']:.1f} ribu"
    )
    return masalah


def bersihkan(df: pd.DataFrame) -> pd.DataFrame:
    """Buang duplikat minggu, ubah '-' menjadi NaN, lalu interpolasi linear."""
    bersih = df.drop_duplicates(subset=["minggu"], keep="first").copy()
    bersih["harga_ribu"] = pd.to_numeric(bersih["harga_ribu"], errors="coerce")
    bersih = bersih.sort_values("tanggal_mulai").reset_index(drop=True)
    kosong = bersih.loc[bersih["harga_ribu"].isna(), "minggu"].tolist()
    bersih["harga_ribu"] = bersih["harga_ribu"].interpolate().round(1)
    keterangan = "; ".join(f"{m} = {bersih.loc[bersih['minggu'] == m, 'harga_ribu'].iloc[0]:.1f}" for m in kosong)
    print(f"[3] Pembersihan        : {len(bersih)} baris bersih; '-' diisi interpolasi ({keterangan})")
    return bersih


def eksplorasi(bersih: pd.DataFrame) -> dict:
    """Ringkas statistik deret: rentang, tren, lonjakan, korelasi lag-1."""
    y = bersih["harga_ribu"].to_numpy()
    kemiringan = float(np.polyfit(np.arange(1, len(y) + 1), y, 1)[0])
    korelasi = float(np.corrcoef(y[:-1], y[1:])[0, 1])
    puncak_i = int(np.argmax(y))
    info = {
        "min": float(y.min()),
        "maks": float(y.max()),
        "mean": float(y.mean()),
        "tren": kemiringan,
        "lag1": korelasi,
        "puncak_minggu": bersih["minggu"].iloc[puncak_i],
        "puncak": float(y[puncak_i]),
    }
    print(
        f"[4] Eksplorasi         : harga {info['min']:.1f}-{info['maks']:.1f}"
        f" (mean {info['mean']:.2f}); tren +{info['tren']:.3f} ribu/minggu;"
    )
    print(
        f"    korelasi lag-1 {info['lag1']:.3f}; lonjakan W19-W21"
        f" (puncak {info['puncak_minggu']} {info['puncak']:.1f})"
    )
    return info


def buat_fitur(bersih: pd.DataFrame) -> pd.DataFrame:
    """Susun fitur lag_1, lag_2, minggu_ke dengan target harga minggu itu."""
    fitur = pd.DataFrame(
        {
            "lag_1": bersih["harga_ribu"].shift(1),
            "lag_2": bersih["harga_ribu"].shift(2),
            "minggu_ke": bersih["harga_ribu"].index + 1,
            "target": bersih["harga_ribu"],
            "minggu": bersih["minggu"],
        }
    ).dropna().reset_index(drop=True)
    fitur["minggu_ke"] = fitur["minggu_ke"].astype(int)
    print(
        f"[5] Fitur lag          : {len(fitur)} baris (lag_1, lag_2, minggu_ke),"
        f" target W{fitur['minggu_ke'].min()}-W{fitur['minggu_ke'].max()}"
    )
    return fitur


def split_kronologis(fitur: pd.DataFrame) -> tuple[pd.DataFrame, pd.DataFrame]:
    """Pisahkan 6 minggu terakhir sebagai uji; sisanya untuk melatih."""
    latih = fitur.iloc[:-MINGGU_UJI].copy()
    uji = fitur.iloc[-MINGGU_UJI:].copy()
    print(
        f"[6] Split kronologis   : latih W{latih['minggu_ke'].min()}-W{latih['minggu_ke'].max()}"
        f" ({len(latih)} baris), uji W{uji['minggu_ke'].min()}-W{uji['minggu_ke'].max()} ({len(uji)} baris)"
    )
    return latih, uji


def metrik(aktual: np.ndarray, prediksi: np.ndarray) -> tuple[float, float]:
    """Hitung MAE dan RMSE."""
    selisih = aktual - prediksi
    mae = float(np.mean(np.abs(selisih)))
    rmse = float(np.sqrt(np.mean(selisih**2)))
    return mae, rmse


def baseline_naive(uji: pd.DataFrame) -> dict:
    """Prediksi naive: harga minggu ini = prakiraan minggu depan."""
    aktual = uji["target"].to_numpy()
    prediksi = uji["lag_1"].to_numpy()
    mae, rmse = metrik(aktual, prediksi)
    print(f"[7] Baseline naive     : MAE {mae:.3f} ribu, RMSE {rmse:.3f} ribu")
    return {"mae": mae, "rmse": rmse, "prediksi": prediksi}


def latih_model(latih: pd.DataFrame) -> tuple[LinearRegression, float]:
    """Latih LinearRegression pada fitur lag."""
    FITUR_KOLOM = ["lag_1", "lag_2", "minggu_ke"]
    model = LinearRegression()
    model.fit(latih[FITUR_KOLOM], latih["target"])
    mae_latih, _ = metrik(latih["target"].to_numpy(), model.predict(latih[FITUR_KOLOM]))
    print(
        f"[8] Latih model        : koefisien lag_1 {model.coef_[0]:.3f},"
        f" lag_2 {model.coef_[1]:.3f}, minggu_ke {model.coef_[2]:.3f};"
    )
    print(
        f"    intersep {model.intercept_:.3f}; MAE pada data latih {mae_latih:.3f} ribu"
    )
    return model, mae_latih


def evaluasi(model: LinearRegression, latih: pd.DataFrame, uji: pd.DataFrame, dasar: dict) -> dict:
    """Bandingkan MAE dan RMSE model melawan baseline naive pada data uji."""
    prediksi = model.predict(uji[["lag_1", "lag_2", "minggu_ke"]]).ravel()
    aktual = uji["target"].to_numpy()
    mae, rmse = metrik(aktual, prediksi)
    selisih_persen = (dasar["mae"] - mae) / dasar["mae"] * 100
    print(
        f"[9] Evaluasi uji       : MAE model {mae:.3f} vs baseline {dasar['mae']:.3f}"
        f" ({selisih_persen:+.1f}%); RMSE {rmse:.3f} vs {dasar['rmse']:.3f}"
    )
    return {"mae": mae, "rmse": rmse, "prediksi": prediksi, "aktual": aktual, "perbaikan": selisih_persen}


def analisis_kesalahan(model: LinearRegression, latih: pd.DataFrame, uji: pd.DataFrame, hasil: dict) -> dict:
    """Bedah residu: bias data uji dan minggu latih dengan galat terbesar."""
    residu_uji = hasil["aktual"] - hasil["prediksi"]
    bias = float(residu_uji.mean())
    terlampaui = int((residu_uji < 0).sum())
    diremehkan_i = int(np.argmax(residu_uji))

    pred_latih = model.predict(latih[["lag_1", "lag_2", "minggu_ke"]])
    residu_latih = latih["target"].to_numpy() - pred_latih.ravel()
    terburuk_i = int(np.argmax(np.abs(residu_latih)))
    terburuk_minggu = latih["minggu"].iloc[terburuk_i]
    terburuk_nilai = float(residu_latih[terburuk_i])
    terburuk_aktual = float(latih["target"].iloc[terburuk_i])
    terburuk_prediksi = float(pred_latih.ravel()[terburuk_i])

    print(
        f"[10] Analisis galat    : residu uji {residu_uji.min():+.2f} s.d. {residu_uji.max():+.2f};"
        f" melebih-lebihkan di {terlampaui} dari {len(uji)} minggu"
        f" (bias {bias:+.2f});"
    )
    print(
        f"     residu latih terburuk {terburuk_minggu} ({terburuk_nilai:+.2f}:"
        f" aktual {terburuk_aktual:.1f}, prakiraan {terburuk_prediksi:.1f})"
    )
    return {
        "residu_uji": residu_uji,
        "bias": bias,
        "diremehkan_minggu": uji["minggu"].iloc[diremehkan_i],
        "diremehkan": float(residu_uji[diremehkan_i]),
        "terlampaui": terlampaui,
        "terburuk_minggu": terburuk_minggu,
        "terburuk": terburuk_nilai,
        "terburuk_aktual": terburuk_aktual,
        "terburuk_prediksi": terburuk_prediksi,
    }


def prakiraan(bersih: pd.DataFrame, model: LinearRegression, galat: dict) -> pd.DataFrame:
    """Prakirakan 4 minggu ke depan dengan interval dari simpangan residu."""
    sigma = float(np.std(galat["residu_uji"], ddof=1))
    riwayat = bersih["harga_ribu"].tolist()
    tanggal = pd.to_datetime(bersih["tanggal_mulai"]).iloc[-1]
    baris = []
    for h in range(1, HORIZON + 1):
        fitur_baru = pd.DataFrame(
            {"lag_1": [riwayat[-1]], "lag_2": [riwayat[-2]], "minggu_ke": [len(riwayat) + 1]}
        )
        titik = float(model.predict(fitur_baru)[0])
        lebar = 1.96 * sigma * np.sqrt(h)
        tanggal = tanggal + timedelta(days=7)
        baris.append(
            {
                "minggu": f"W{36 + h}",
                "tanggal": tanggal.strftime("%Y-%m-%d"),
                "titik": round(titik, 1),
                "bawah": round(titik - lebar, 1),
                "atas": round(titik + lebar, 1),
            }
        )
        riwayat.append(titik)
    prakiraan_df = pd.DataFrame(baris)
    print(f"[11] Prakiraan {HORIZON} minggu (interval 95% dari simpangan residu {sigma:.3f}):")
    for _, b in prakiraan_df.iterrows():
        print(f"     {b['minggu']} ({b['tanggal']}): {b['titik']:.1f}  [{b['bawah']:.1f}, {b['atas']:.1f}]")
    return prakiraan_df


def susun_laporan(
    masalah: dict,
    info: dict,
    dasar: dict,
    model: LinearRegression,
    hasil: dict,
    galat: dict,
    prakiraan_df: pd.DataFrame,
) -> str:
    """Susun isi laporan akhir sebagai teks polos."""
    pertama = prakiraan_df.iloc[0]
    baris = [
        "=" * 68,
        "LAPORAN AKHIR - PREDIKSI HARGA CABAI MERAH",
        "Warung Nasi Bu Warni, Surabaya",
        f"Data: 36 minggu, W1 (mulai {PEKAN_W1}) s.d. W36 (mulai {PEKAN_W36})",
        "=" * 68,
        "",
        "1. RINGKASAN DATA",
        "-" * 68,
        "Sumber        : catatan harga mingguan cabai merah di pasar",
        "                sekitar warung (harga rata-rata per minggu)",
        "Satuan        : ribu rupiah per kilogram",
        f"Data mentah   : {masalah['baris']} baris (W10 tercatat 3 kali;",
        "                harga W27 tidak tercatat, tertulis '-')",
        "Pembersihan   : 2 baris duplikat W10 dibuang; sel '-' W27",
        "                diisi interpolasi linear (41.4); hasil 36 baris",
        f"Kisaran harga : {info['min']:.1f} sampai {info['maks']:.1f}"
        f" (rata-rata {info['mean']:.2f})",
        f"Pola          : tren naik ringan +{info['tren']:.3f} ribu/minggu,",
        "                musiman 8-mingguan ringan, lonjakan W19-W21",
        f"                (puncak {info['puncak_minggu']} {info['puncak']:.1f})",
        "",
        "2. METODE PREDIKSI",
        "-" * 68,
        "Fitur         : lag_1, lag_2 (harga 1-2 minggu sebelumnya),",
        "                minggu_ke (nomor minggu, menangkap tren)",
        "Pembagian     : kronologis 30/6 - 30 minggu pertama untuk",
        "                melatih (28 baris fitur), 6 minggu terakhir",
        "                (W31-W36) untuk menguji",
        "Baseline      : naive - harga minggu ini dipakai sebagai",
        "                prakiraan minggu depan",
        "Model         : LinearRegression pada fitur lag",
        f"Koefisien     : lag_1 {model.coef_[0]:.3f}, lag_2 {model.coef_[1]:.3f},",
        f"                minggu_ke {model.coef_[2]:.3f}, intersep {model.intercept_:.3f}",
        "",
        "3. HASIL EVALUASI PADA 6 MINGGU UJI (W31-W36)",
        "-" * 68,
        f"Model         : MAE {hasil['mae']:.3f}, RMSE {hasil['rmse']:.3f}",
        f"Baseline naive: MAE {dasar['mae']:.3f}, RMSE {dasar['rmse']:.3f}",
        f"Kesimpulan    : model lebih baik {abs(hasil['perbaikan']):.1f}% pada MAE",
        "                (menang tipis); prakiraan rata-rata meleset",
        f"                sekitar {hasil['mae']:.1f} ribu rupiah per minggu",
        "",
        "4. ANALISIS KESALAHAN",
        "-" * 68,
        f"- Model melebih-lebihkan harga di {galat['terlampaui']} dari 6 minggu uji",
        f"  (bias rata-rata {galat['bias']:+.2f} ribu).",
        f"- Satu-satunya minggu yang diremehkan adalah {galat['diremehkan_minggu']}",
        f"  ({galat['diremehkan']:+.2f}), yaitu minggu dengan kenaikan paling",
        "  tajam di jendela uji.",
        f"- Galat latih terburuk: {galat['terburuk_minggu']} ({galat['terburuk']:+.2f}) saat awal",
        f"  lonjakan musim hujan - aktual {galat['terburuk_aktual']:.1f},"
        f" prakiraan {galat['terburuk_prediksi']:.1f};",
        "  model lag terlambat satu minggu saat harga berbalik cepat.",
        "",
        "5. PRAKIRAAN 4 MINGGU KE DEPAN",
        "-" * 68,
        "Interval 95% = titik prakiraan +/- 1.96 x simpangan residu",
        "dikali akar horizon; makin jauh horizon, makin lebar interval.",
    ]
    for _, b in prakiraan_df.iterrows():
        baris.append(
            f"{b['minggu']} ({b['tanggal']}) : {b['titik']:.1f} ribu"
            f"  [{b['bawah']:.1f}, {b['atas']:.1f}]"
        )
    baris += [
        "",
        "6. REKOMENDASI BELANJA UNTUK BU WARNI",
        "-" * 68,
        f"R1  Pakai prakiraan {pertama['minggu']} sebagai patokan belanja pekan",
        f"    depan: anggarkan cabai pada kisaran {pertama['bawah']:.1f}-"
        f"{pertama['atas']:.1f} ribu per kg.",
        "R2  Untuk horizon 2-4 minggu, baca intervalnya, bukan angka",
        "    titiknya saja: batas bawah sebagai skenario hemat, batas",
        "    atas sebagai skenario waspada.",
        f"R3  Waspadai peralihan musim: saat awal lonjakan {galat['terburuk_minggu']} model",
        f"    pernah meremehkan harga {galat['terburuk']:.1f} ribu - bila ada",
        "    kabar gangguan pasokan, beli lebih awal dari prakiraan.",
        "R4  Catat harga mingguan baru dan jalankan ulang pipeline:",
        "    prakiraan menempel pada data terbaru.",
        "",
        "7. KETERBATASAN",
        "-" * 68,
        "- 36 minggu (4.5 siklus musiman) masih sedikit untuk model",
        "  deret waktu; pola musiman tahunan belum terpisah dari tren.",
        "- Interval dihitung dari 6 residu uji saja - estimasi kasar",
        "  yang sebaiknya dibaca sebagai kisaran, bukan jaminan.",
        "- Model tidak memiliki fitur fase musiman dan cuaca; prakiraan",
        "  4 minggu condong mengikuti tren, bukan siklus panen.",
        "- Satu nilai (W27 41.4) adalah hasil interpolasi, bukan harga",
        "  yang benar-benar tercatat.",
        "",
        "Diproses oleh : src/prediksi_harga.py (pipeline deterministik)",
        "Data          : data/harga_mingguan.csv",
    ]
    return "\n".join(baris) + "\n"


def main() -> None:
    """Jalankan seluruh alur pipeline dan tulis laporan."""
    mentah = muat_data()
    masalah = periksa_masalah(mentah)
    bersih = bersihkan(mentah)
    info = eksplorasi(bersih)
    fitur = buat_fitur(bersih)
    latih, uji = split_kronologis(fitur)
    dasar = baseline_naive(uji)
    model, mae_latih = latih_model(latih)
    hasil = evaluasi(model, latih, uji, dasar)
    galat = analisis_kesalahan(model, latih, uji, hasil)
    prakiraan_df = prakiraan(bersih, model, galat)

    laporan = susun_laporan(masalah, info, dasar, model, hasil, galat, prakiraan_df)
    BERKAS_LAPORAN.parent.mkdir(parents=True, exist_ok=True)
    BERKAS_LAPORAN.write_text(laporan, encoding="utf-8")
    print(f"[12] Laporan ditulis ke : {BERKAS_LAPORAN}")


if __name__ == "__main__":
    main()

Output yang Dihasilkan:

text
[1] Muat data mentah   : 38 baris, 3 kolom
[2] Temuan validasi    : 2 baris duplikat (W10); 1 sel harga '-' (W27);
    urutan minggu W1-W36 utuh, 2025-06-02 s.d. 2026-02-02; harga wajar 35.9-53.1 ribu
[3] Pembersihan        : 36 baris bersih; '-' diisi interpolasi (W27 = 41.4)
[4] Eksplorasi         : harga 35.9-53.1 (mean 40.56); tren +0.089 ribu/minggu;
    korelasi lag-1 0.728; lonjakan W19-W21 (puncak W20 53.1)
[5] Fitur lag          : 34 baris (lag_1, lag_2, minggu_ke), target W3-W36
[6] Split kronologis   : latih W3-W30 (28 baris), uji W31-W36 (6 baris)
[7] Baseline naive     : MAE 1.167 ribu, RMSE 1.288 ribu
[8] Latih model        : koefisien lag_1 1.151, lag_2 -0.647, minggu_ke 0.072;
    intersep 18.890; MAE pada data latih 1.181 ribu
[9] Evaluasi uji       : MAE model 1.080 vs baseline 1.167 (+7.4%); RMSE 1.151 vs 1.288
[10] Analisis galat    : residu uji -1.50 s.d. +0.27; melebih-lebihkan di 5 dari 6 minggu (bias -0.99);
     residu latih terburuk W19 (+9.64: aktual 51.0, prakiraan 41.4)
[11] Prakiraan 4 minggu (interval 95% dari simpangan residu 0.641):
     W37 (2026-02-09): 41.5  [40.3, 42.8]
     W38 (2026-02-16): 42.9  [41.2, 44.7]
     W39 (2026-02-23): 44.3  [42.1, 46.4]
     W40 (2026-03-02): 44.9  [42.4, 47.4]
[12] Laporan ditulis ke : laporan/laporan-akhir.txt

Dua belas langkah tersebut adalah peta jalan seluruh proyek. Langkah [1] kita kerjakan pada Bagian 2, [2] pada Bagian 3, [3] pada Bagian 4, [4] pada Bagian 5, [5]–[6] pada Bagian 6, [7] pada Bagian 7, [8] pada Bagian 8, [9] pada Bagian 9, [10] pada Bagian 10, [11] pada Bagian 11, dan [12] pada Bagian 12 — sedangkan Bagian 1 merumuskan masalah yang mengikat semuanya. Perhatikan inti ceritanya sejak dini: model LinearRegression mengalahkan baseline naive hanya tipis (MAE 1.080 lawan 1.167), dan prakiraan 4 minggu disertai interval yang melebar — proyek ini sama pentingnya dengan kejujurannya.

Laporan Akhir #

Pipeline menutup kerjanya dengan menulis laporan/laporan-akhir.txt — laporan teks polos berisi ringkasan data, metode, hasil evaluasi, analisis kesalahan, prakiraan 4 minggu beserta intervalnya, rekomendasi belanja, dan keterbatasan. Seluruh angka pada laporan diambil dari perhitungan pipeline, bukan diketik manual.

text
====================================================================
LAPORAN AKHIR - PREDIKSI HARGA CABAI MERAH
Warung Nasi Bu Warni, Surabaya
Data: 36 minggu, W1 (mulai 2 Juni 2025) s.d. W36 (mulai 2 Februari 2026)
====================================================================

1. RINGKASAN DATA
--------------------------------------------------------------------
Sumber        : catatan harga mingguan cabai merah di pasar
                sekitar warung (harga rata-rata per minggu)
Satuan        : ribu rupiah per kilogram
Data mentah   : 38 baris (W10 tercatat 3 kali;
                harga W27 tidak tercatat, tertulis '-')
Pembersihan   : 2 baris duplikat W10 dibuang; sel '-' W27
                diisi interpolasi linear (41.4); hasil 36 baris
Kisaran harga : 35.9 sampai 53.1 (rata-rata 40.56)
Pola          : tren naik ringan +0.089 ribu/minggu,
                musiman 8-mingguan ringan, lonjakan W19-W21
                (puncak W20 53.1)

2. METODE PREDIKSI
--------------------------------------------------------------------
Fitur         : lag_1, lag_2 (harga 1-2 minggu sebelumnya),
                minggu_ke (nomor minggu, menangkap tren)
Pembagian     : kronologis 30/6 - 30 minggu pertama untuk
                melatih (28 baris fitur), 6 minggu terakhir
                (W31-W36) untuk menguji
Baseline      : naive - harga minggu ini dipakai sebagai
                prakiraan minggu depan
Model         : LinearRegression pada fitur lag
Koefisien     : lag_1 1.151, lag_2 -0.647,
                minggu_ke 0.072, intersep 18.890

3. HASIL EVALUASI PADA 6 MINGGU UJI (W31-W36)
--------------------------------------------------------------------
Model         : MAE 1.080, RMSE 1.151
Baseline naive: MAE 1.167, RMSE 1.288
Kesimpulan    : model lebih baik 7.4% pada MAE
                (menang tipis); prakiraan rata-rata meleset
                sekitar 1.1 ribu rupiah per minggu

4. ANALISIS KESALAHAN
--------------------------------------------------------------------
- Model melebih-lebihkan harga di 5 dari 6 minggu uji
  (bias rata-rata -0.99 ribu).
- Satu-satunya minggu yang diremehkan adalah W33
  (+0.27), yaitu minggu dengan kenaikan paling
  tajam di jendela uji.
- Galat latih terburuk: W19 (+9.64) saat awal
  lonjakan musim hujan - aktual 51.0, prakiraan 41.4;
  model lag terlambat satu minggu saat harga berbalik cepat.

5. PRAKIRAAN 4 MINGGU KE DEPAN
--------------------------------------------------------------------
Interval 95% = titik prakiraan +/- 1.96 x simpangan residu
dikali akar horizon; makin jauh horizon, makin lebar interval.
W37 (2026-02-09) : 41.5 ribu  [40.3, 42.8]
W38 (2026-02-16) : 42.9 ribu  [41.2, 44.7]
W39 (2026-02-23) : 44.3 ribu  [42.1, 46.4]
W40 (2026-03-02) : 44.9 ribu  [42.4, 47.4]

6. REKOMENDASI BELANJA UNTUK BU WARNI
--------------------------------------------------------------------
R1  Pakai prakiraan W37 sebagai patokan belanja pekan
    depan: anggarkan cabai pada kisaran 40.3-42.8 ribu per kg.
R2  Untuk horizon 2-4 minggu, baca intervalnya, bukan angka
    titiknya saja: batas bawah sebagai skenario hemat, batas
    atas sebagai skenario waspada.
R3  Waspadai peralihan musim: saat awal lonjakan W19 model
    pernah meremehkan harga 9.6 ribu - bila ada
    kabar gangguan pasokan, beli lebih awal dari prakiraan.
R4  Catat harga mingguan baru dan jalankan ulang pipeline:
    prakiraan menempel pada data terbaru.

7. KETERBATASAN
--------------------------------------------------------------------
- 36 minggu (4.5 siklus musiman) masih sedikit untuk model
  deret waktu; pola musiman tahunan belum terpisah dari tren.
- Interval dihitung dari 6 residu uji saja - estimasi kasar
  yang sebaiknya dibaca sebagai kisaran, bukan jaminan.
- Model tidak memiliki fitur fase musiman dan cuaca; prakiraan
  4 minggu condong mengikuti tren, bukan siklus panen.
- Satu nilai (W27 41.4) adalah hasil interpolasi, bukan harga
  yang benar-benar tercatat.

Diproses oleh : src/prediksi_harga.py (pipeline deterministik)
Data          : data/harga_mingguan.csv

Dua Belas Bagian Pengerjaan #

Proyek dikerjakan berurutan; setiap bagian membangun di atas hasil bagian sebelumnya:

  1. Bagian 1: Merumuskan Masalah Prediksi — keputusan belanja apa yang ingin dibantu, horizon berapa minggu, dan kenapa prakiraan bukan kepastian.
  2. Bagian 2: Mengumpulkan Data Historis — dari mana deret harga mingguan berasal dan bagaimana strukturnya.
  3. Bagian 3: Memvalidasi Data — memeriksa urutan minggu, duplikat, sel kosong, dan kewajaran rentang harga.
  4. Bagian 4: Membersihkan Data — membuang duplikat, mengisi sel kosong dengan interpolasi, lalu memverifikasi hasilnya.
  5. Bagian 5: Eksplorasi Deret Waktu — membaca tren, musiman, dan lonjakan dari grafik serta korelasi lag-1.
  6. Bagian 6: Membuat Fitur Prediksi — mengubah deret menjadi tabel fitur lag dan membagi latih/uji secara kronologis.
  7. Bagian 7: Membangun Baseline — prakiraan naive sebagai patokan yang wajib dikalahkan.
  8. Bagian 8: Melatih Model — LinearRegression pada fitur lag dan membaca koefisiennya.
  9. Bagian 9: Mengevaluasi Prediksi — MAE dan RMSE model melawan baseline pada 6 minggu uji.
  10. Bagian 10: Menganalisis Kesalahan — membedah residu per minggu dan menemukan pola galatnya.
  11. Bagian 11: Membahas Ketidakpastian — interval prakiraan yang melebar dan bahasa pelaporan yang jujur.
  12. Bagian 12: Menyusun Laporan — membaca laporan akhir, menimbang rekomendasinya, dan merefleksikan proyek.

Mulailah dari Bagian 1 dan kerjakan sesuai urutan. Seluruh berkas proyek juga dapat diunduh dari bagian Berkas Sumber di bawah halaman ini, serta dari halaman setiap bagian.

# Bagian 1: Merumuskan Masalah Prediksi

Ubah kebutuhan prakiraan belanja Bu Warni menjadi spesifikasi prediksi yang jelas: target, horizon, dan ukuran keberhasilan.

Buka →
# Bagian 2: Mengumpulkan Data Historis

Telusuri asal deret harga mingguan cabai merah, catat asal-usulnya, dan kenali struktur CSV deret waktu.

Buka →
# Bagian 3: Memvalidasi Data

Periksa deret mentah secara sistematis: urutan minggu, selang tanggal, duplikat, sel kosong, dan kewajaran rentang harga.

Buka →
# Bagian 4: Membersihkan Data

Jalankan rencana pembersihan Bagian 3: buang baris kembar, interpolasi sel kosong, konversi tipe, lalu verifikasi 36 baris bersih.

Buka →
# Bagian 5: Eksplorasi Deret Waktu

Baca pola deret harga yang bersih: gambar grafik garisnya, ukur tren dan korelasi lag-1, lalu kenali gelombang musiman dan lonjakan W19-W21.

Buka →
# Bagian 6: Membuat Fitur Prediksi

Ubah deret menjadi tabel fitur lag_1, lag_2, minggu_ke, pahami bahaya kebocoran masa depan, lalu bagi latih/uji secara kronologis 28/6.

Buka →
# Bagian 7: Membangun Baseline

Bangun prakiraan naive last-value sebagai patokan, ukur kualitasnya dengan MAE dan RMSE pada 6 minggu uji, dan tetapkan skor yang wajib dikalahkan.

Buka →
# Bagian 8: Melatih Model

Latih LinearRegression pada fitur lag, baca koefisiennya sebagai level, momentum, dan tren, lalu bandingkan MAE latih model melawan naive.

Buka →
# Bagian 9: Mengevaluasi Prediksi

Uji model dan baseline naive pada 6 minggu uji W31-W36: tabel prakiraan per minggu, MAE dan RMSE, lalu baca kemenangan tipis 7,4% secara jujur.

Buka →
# Bagian 10: Menganalisis Kesalahan

Bedah residu per minggu: bias prakiraan yang terlalu tinggi, W33 yang diremehkan, lonjakan W19 dengan galat 9,64 ribu, dan pelajaran dari titik balik arah.

Buka →
# Bagian 11: Membahas Ketidakpastian

Bangun interval 95% dari simpangan residu, prakirakan 4 minggu dengan interval yang melebar mengikuti akar horizon, lalu susun bahasa pelaporan yang jujur.

Buka →
# Bagian 12: Menyusun Laporan

Baca laporan akhir yang ditulis pipeline: bedah struktur tujuh bagiannya, verifikasi klaim 7,4%, timbang rekomendasi belanja, lalu refleksikan seluruh proyek.

Buka →