Mesin prediksi tidak memakan deret mentah; mesin itu memakan tabel. Pada bagian ini kita mengubah 36 baris deret waktu menjadi tabel fitur bergaya supervised learning: setiap baris memuat informasi masa lalu sebagai feature dan harga minggu berjalan sebagai target. Dua keputusan menunggu di sini dan keduanya menentukan kejujuran seluruh proyek: fitur apa yang boleh dipakai, dan bagaimana data dibagi tanpa membocorkan masa depan.

Dari Deret menjadi Tabel Fitur #

Ide dasarnya adalah jendela geser. Untuk memprediksi harga minggu ke-t, kita membuka jendela ke belakang: harga minggu t−1, harga minggu t−2, dan posisi kalender t itu sendiri. Deret digeser satu dan dua langkah dengan shift(), lalu setiap baris berdiri sendiri sebagai contoh latih:

Baris fiturlag_1 (harga W−1)lag_2 (harga W−2)minggu_ketarget (harga W)
W340,1 (W2)40,1 (W1)340,1
W440,1 (W3)40,1 (W2)438,8
W538,8 (W4)40,1 (W3)536,4

Blok berikut membangun tabel itu dari data bersih:

python
import pandas as pd

# Resep pembersihan hasil Bagian 4: duplikat dibuang, '-' diinterpolasi.
df = pd.read_csv("../data/harga_mingguan.csv")
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)
bersih["harga_ribu"] = bersih["harga_ribu"].interpolate().round(1)

# Jendela geser: setiap baris membawa harga 1-2 minggu sebelumnya sebagai fitur.
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"Deret 36 baris menjadi tabel fitur {len(fitur)} baris (W1 dan W2 tak punya riwayat penuh).")
print()
print(fitur.head(6).to_string(index=False))
print()
print(fitur.tail(3).to_string(index=False))

Output yang Dihasilkan:

text
Deret 36 baris menjadi tabel fitur 34 baris (W1 dan W2 tak punya riwayat penuh).

 lag_1  lag_2  minggu_ke  target minggu
  40.1   40.1          3    40.1     W3
  40.1   40.1          4    38.8     W4
  38.8   40.1          5    36.4     W5
  36.4   38.8          6    35.9     W6
  35.9   36.4          7    37.2     W7
  37.2   35.9          8    38.6     W8

 lag_1  lag_2  minggu_ke  target minggu
  42.5   40.2         34    43.0    W34
  43.0   42.5         35    41.9    W35
  41.9   43.0         36    40.9    W36

Mari kita telaah hasilnya:

  1. 34 baris, bukan 36: shift(1) dan shift(2) mengisi dua baris pertama dengan NaN karena W1 dan W2 belum punya riwayat dua minggu penuh; dropna() membuangnya. Harga bayaran ini wajar dan jauh lebih jujur daripada mengarang nilai lag untuk minggu paling awal.
  2. Setiap baris kini mandiri: baris W4 memuat lag_1 40,1 (harga W3) dan lag_2 40,1 (harga W2) — kebetulan sama karena tiga minggu pertama memang bertahan di 40,1. Model membaca baris ini seperti membaca baris pelanggan pada data tabular biasa.
  3. Kolom minggu dipisahkan dari fitur: kolom itu hanya penanda; ia ikut dibawa agar tabel mudah dibaca manusia, bukan untuk dipelajari model.

Tiga Fitur dan Perannya #

Pilihan fitur mengikuti temuan Bagian 5. lag_1 membawa level terkini deret — prediktor terkuat sebagaimana ditunjukkan korelasi lag-1 0,728. lag_2 memperluas memori: bersama lag_1, keduanya memungkinkan model membaca arah pergerakan (harga sedang menanjak atau melandai), bukan hanya posisinya. minggu_ke adalah bilangan bulat 3–36 yang tumbuh satu tiap minggu — kendaraan sederhana untuk tren +0,089 ribu per minggu yang sudah kita ukur.

Mengapa berhenti di dua lag? Dengan hanya 34 baris contoh, setiap fitur tambahan memakan derajat kebebasan dan membuka pintu overfitting — model hafal derau, bukan pola. Prinsip hemat ini kita pegang sampai akhir proyek: fitur sedikit yang bisa dijelaskan lebih berharga daripada fitur banyak yang buram.

Bahaya Pembagian Acak: Kebocoran Masa Depan #

Tabel fitur kini tampak seperti data tabular biasa — dan di sinilah jebakannya. Kebiasaan standar pada data tabular adalah train_test_split acak. Blok berikut mencobanya sekadar untuk melihat akibatnya:

python
import pandas as pd
from sklearn.model_selection import train_test_split

# Resep pembersihan hasil Bagian 4: duplikat dibuang, '-' diinterpolasi.
df = pd.read_csv("../data/harga_mingguan.csv")
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)
bersih["harga_ribu"] = bersih["harga_ribu"].interpolate().round(1)

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)

# Pembanding 1: pembagian ACAK seperti pada data tabular biasa.
latih_acak, uji_acak = train_test_split(fitur, test_size=6, random_state=42)
print("Pembagian acak (random_state=42):")
print("  data uji   :", sorted(uji_acak["minggu"].tolist()))
print(f"  data latih : {len(latih_acak)} minggu tersebar dari W{latih_acak['minggu_ke'].min()} sampai W{latih_acak['minggu_ke'].max()}")
print(f"  ada minggu latih yang lebih baru daripada seluruh minggu uji : {bool((latih_acak['minggu_ke'] > uji_acak['minggu_ke'].max()).any())}")
print()

# Pembanding 2: pembagian KRONOLOGIS - 6 minggu terakhir menguji.
latih = fitur.iloc[:-6].copy()
uji = fitur.iloc[-6:].copy()
print("Pembagian kronologis:")
print(f"  data latih : W{latih['minggu_ke'].min()} sampai W{latih['minggu_ke'].max()} ({len(latih)} baris)")
print(f"  data uji   : W{uji['minggu_ke'].min()} sampai W{uji['minggu_ke'].max()} ({len(uji)} baris)")
print(f"  data latih berhenti tepat sebelum data uji dimulai          : {latih['minggu_ke'].max() < uji['minggu_ke'].min()}")

Output yang Dihasilkan:

text
Pembagian acak (random_state=42):
  data uji   : ['W11', 'W18', 'W22', 'W27', 'W29', 'W30']
  data latih : 28 minggu tersebar dari W3 sampai W36
  ada minggu latih yang lebih baru daripada seluruh minggu uji : True

Pembagian kronologis:
  data latih : W3 sampai W30 (28 baris)
  data uji   : W31 sampai W36 (6 baris)
  data latih berhenti tepat sebelum data uji dimulai          : True

Pembagian acak menjadikan W11 dan W18 sebagai data uji, sementara W31 sampai W36 — minggu-minggu yang lebih baru — justru dipakai melatih model. Model yang diuji seperti ini tidak memprediksi masa depan; ia menjawab soal yang jawabannya sudah pernah dilihatnya. Skor evaluasinya pasti indah, dan pasti menipu. Kebocoran seperti ini disebut data leakage: informasi dari masa uji merembes ke masa latih.

Pembagian Kronologis 28/6 #

Pembagian yang jujur memotong deret pada satu titik waktu: 28 baris fitur pertama (W3–W30) untuk melatih, 6 baris terakhir (W31–W36) untuk menguji. Dari 36 minggu data, 30 minggu pertama menyumbang baris latih — dua minggu awal hilang sebagai harga shift. Rasio ini memang miring ke data latih, tetapi enam minggu uji cukup untuk satu laporan jujur pertama, dan setiap minggu uji benar-benar tak tersentuh model sebelum diuji. Prinsip membagi menurut waktu — beserta varian time series split untuk evaluasi berlapis — dibahas tuntas pada chapter Validasi Deret Waktu .

Ringkasan Bagian 6 #

Deret 36 baris telah menjadi tabel fitur 34 baris: lag_1 dan lag_2 membawa harga dua minggu terakhir, minggu_ke membawa tren. Pembagian acak terbukti membocorkan masa depan ke data latih, sehingga kita memakai pembagian kronologis — 28 baris latih (W3–W30) dan 6 baris uji (W31–W36).

Dengan tabel dan aturan main yang jujur ini, kita perlu satu patokan sebelum melatih model apa pun. Patokan itu — prakiraan naive — dibangun pada Bagian 7: Membangun Baseline .