Bagian 3 menobatkan RandomForestRegressor(max_depth=8, n_estimators=200) dengan R2 CV 0,8097 — tetapi mahkota itu dianugerahkan oleh data latih yang ikut menyeleksi konfigurasi model. Kini tiba saatnya pengadilan yang belum pernah ikut campur: 80 baris data uji yang disegel sejak Bagian 2, tidak pernah dilatih, tidak pernah di-grid-search. Bagian ini membuka segel itu, menilai tiga finalis (juara per keluarga dari Bagian 3) di atas data uji, membedah sisaan prediksi untuk memahami di mana model keliru, lalu menuangkan seluruh perjalanan proyek menjadi satu laporan teks yang bisa dibaca pemangku kepentingan tanpa membuka satu baris kode Python.
Data Uji sebagai Hakim Tunggal #
Mengapa penilaian akhir harus menunggu sampai titik ini? Karena setiap keputusan yang kita ambil sepanjang proyek — mempertahankan pencilan di Bagian 1, arsitektur preprocessing di Bagian 2, memilih RandomForest di Bagian 3 — semuanya dibuat sambil melihat data latih. Data uji adalah satu-satunya himpunan yang bebas dari kontaminasi keputusan tersebut. Jika model juara tampil baik di sana, kita punya bukti bahwa yang dipelajari adalah pola, bukan hafalan. Jika tampil lebih buruk daripada skor CV, itu sinyal peringatan bahwa optimasi kita menyetel ke derau. Evaluasi holdout bukan formalitas penutup; ia satu-satunya perkiraan jujur untuk performa model saat bertemu listing rumah baru.
Menilai Finalis dan Menulis Laporan #
Skrip bagian ini melakukan dua pekerjaan sekaligus. Pertama, ketiga finalis dilatih ulang pada seluruh data latih dengan konfigurasi juara masing-masing, diprediksi pada data uji, lalu dinilai tiga metrik: MAE (berapa juta rata-rata meleset), RMSE (menghukum galat besar lebih keras — ingat pencilan harga kita), dan R2. Kedua, seluruh temuan proyek dirangkai menjadi laporan/laporan-harga-rumah.txt: enam bagian berurutan dari deskripsi data, preprocessing, pemilihan model, evaluasi holdout, temuan utama, hingga batasan — ditulis ulang dari angka-angka yang dihitung saat itu juga, bukan diketik manual.
# Evaluasi holdout dan penyusun laporan akhir proyek harga rumah.
# Tiga model final (konfigurasi terbaik tiap keluarga dari Bagian 3)
# dilatih pada data latih lalu dinilai pada 80 baris data uji yang tidak
# pernah disentuh pelatihan. Hasilnya dirangkum menjadi
# laporan/laporan-harga-rumah.txt lalu dicetak ke layar.
import numpy as np
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.ensemble import RandomForestRegressor
from sklearn.impute import SimpleImputer
from sklearn.linear_model import Lasso, Ridge
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
BERKAS_CSV = "data/rumah.csv"
BERKAS_LAPORAN = "laporan/laporan-harga-rumah.txt"
FITUR_NUM = ["luas_m2", "kamar_tidur", "usia_bangunan", "jarak_pusat_km"]
FITUR_KAT = ["kota"]
def bangun_pra():
return ColumnTransformer(
transformers=[
(
"num",
Pipeline(
steps=[
("imputasi", SimpleImputer(strategy="median")),
("skala", StandardScaler()),
]
),
FITUR_NUM,
),
(
"kat",
OneHotEncoder(handle_unknown="ignore", sparse_output=False),
FITUR_KAT,
),
]
)
def fmt(nilai):
"""Format angka selalu dua desimal; '?' bila hilang."""
return f"{nilai:.1f}" if pd.notna(nilai) else "?"
df = pd.read_csv(BERKAS_CSV)
X = df.drop(columns=["harga_juta"])
y = df["harga_juta"]
X_latih, X_uji, y_latih, y_uji = train_test_split(
X, y, test_size=0.2, random_state=42
)
print("== Holdout disiapkan: 80 baris (test_size=0.2, random_state=42) ==")
# Konfigurasi terbaik per keluarga model hasil GridSearchCV Bagian 3.
FINALIS = [
("Ridge alpha=10", Ridge(alpha=10.0)),
(
"Lasso alpha=10",
Lasso(alpha=10.0, max_iter=10000, random_state=42),
),
(
"RandomForest depth=8, 200 pohon",
RandomForestRegressor(
max_depth=8, n_estimators=200, random_state=42, n_jobs=1
),
),
]
pipelines = []
hasil_uji = []
for nama, estimator in FINALIS:
pipeline = Pipeline(
steps=[("pra", bangun_pra()), ("model", estimator)]
)
pipeline.fit(X_latih, y_latih)
pred = pipeline.predict(X_uji)
mae = mean_absolute_error(y_uji, pred)
rmse = float(np.sqrt(mean_squared_error(y_uji, pred)))
r2 = r2_score(y_uji, pred)
pipelines.append(pipeline)
hasil_uji.append((nama, mae, rmse, r2))
print(
f" {nama:31s} MAE {mae:7.1f} RMSE {rmse:7.1f} R2 {r2:.4f}"
)
i_terbaik = min(range(len(hasil_uji)), key=lambda i: hasil_uji[i][2])
nama_terbaik, mae_t, rmse_t, r2_t = hasil_uji[i_terbaik]
pipeline_terbaik = pipelines[i_terbaik]
mae_relatif = mae_t / y_uji.mean() * 100.0
# ---- Diagnostik sisaan model terbaik -----------------------------------
pred_uji = pipeline_terbaik.predict(X_uji)
pred_uji = pd.Series(pred_uji, index=y_uji.index)
sisa_uji = (y_uji - pred_uji).round(1)
urut_sisa = sisa_uji.abs().sort_values(ascending=False).head(3)
pred_latih = pipeline_terbaik.predict(X_latih)
pred_latih = pd.Series(pred_latih, index=y_latih.index)
sisa_latih = (y_latih - pred_latih).round(1)
i_sisa_maks = sisa_latih.abs().idxmax()
# ---- Susun laporan -----------------------------------------------------
korelasi_luas_kamar = df["luas_m2"].corr(df["kamar_tidur"])
korelasi_jarak = df["jarak_pusat_km"].corr(df["harga_juta"])
kep = pd.Series(
pipeline_terbaik.named_steps["model"].feature_importances_,
index=pipeline_terbaik.named_steps["pra"].get_feature_names_out(),
).sort_values(ascending=False)
koef_ridge = pd.Series(
pipelines[0].named_steps["model"].coef_,
index=pipelines[0].named_steps["pra"].get_feature_names_out(),
).round(1)
L = []
L.append("=" * 72)
L.append(" LAPORAN PROYEK: PREDIKSI HARGA RUMAH JABODETABEK")
L.append(" Dataset sintetis deterministik (seed 42) - 400 baris")
L.append("=" * 72)
L.append("")
L.append("1. DATA")
L.append(f" Sumber : {BERKAS_CSV} (dibangkitkan src/01_bangun_data.py)")
L.append(" Ukuran : 400 baris x 6 kolom (5 fitur + target harga_juta)")
L.append(" Nilai hilang : luas_m2 = 12 baris, usia_bangunan = 8 baris")
L.append(" Keanealan : 2 baris salah ketik luas (354-359 m2),")
L.append(" 4 baris ber-premium tinggi tanpa penjelasan fitur")
L.append(
" Harga (juta) : min {:.1f} | rata-rata {:.1f} | median {:.1f}".format(
df["harga_juta"].min(),
df["harga_juta"].mean(),
df["harga_juta"].median(),
)
)
L.append(" maks {:.1f}".format(df["harga_juta"].max()))
L.append("")
L.append("2. PREPROCESSING")
L.append(" Numerik (4) : imputasi median + StandardScaler")
L.append(" Kategorikal : kota -> OneHotEncoder (5 kolom biner)")
L.append(" Arsitektur : ColumnTransformer di dalam Pipeline;")
L.append(" statistik dipelajari hanya dari data latih")
L.append("")
L.append("3. PEMILIHAN MODEL (Bagian 3: GridSearchCV, 3 lipatan, data latih)")
L.append(" Kandidat : Ridge, Lasso, RandomForestRegressor")
L.append(" Juara CV : RandomForestRegressor(max_depth=8,")
L.append(" n_estimators=200) - R2 CV 0.8097")
L.append("")
L.append("4. EVALUASI HOLDOUT (80 baris data uji, tak pernah dilatih)")
L.append(
" {:31s} {:>9s} {:>11s} {:>8s}".format(
"model", "MAE juta", "RMSE juta", "R2"
)
)
for nama, mae, rmse, r2 in hasil_uji:
L.append(
" {:31s} {:>9.1f} {:>11.1f} {:>8.4f}".format(nama, mae, rmse, r2)
)
L.append(f" Model final : {nama_terbaik}")
L.append(
" MAE relatif : {:.1f}% dari rata-rata harga data uji ({:.1f} juta)".format(
mae_relatif, y_uji.mean()
)
)
L.append("")
L.append("5. TEMUAN UTAMA")
L.append(
" (1) luas_m2 dominan: kepentingan fitur {:.4f}; koefisien Ridge".format(
kep["num__luas_m2"]
)
)
L.append(
" {:.1f} juta per satu simpangan baku luas (sekitar 59 m2).".format(
koef_ridge["num__luas_m2"]
)
)
L.append(
" (2) jarak_pusat_km: korelasi marginal {:+.3f} terlihat lemah,".format(
korelasi_jarak
)
)
L.append(
" tetapi koefisien Ridge {:+.1f} juta per simpangan baku setelah".format(
koef_ridge["num__jarak_pusat_km"]
)
)
L.append(" kota dikontrol - efek kota dan jarak saling menutupi.")
L.append(
" (3) kamar_tidur dan luas_m2 berkorelasi {:+.3f}; pada model".format(
korelasi_luas_kamar
)
)
L.append(
" linear keduanya berbagi bobot (kamar {:+.1f}), sedangkan".format(
koef_ridge["num__kamar_tidur"]
)
)
L.append(" Lasso memangkas usia_bangunan dan 3 dummy kota menjadi nol.")
L.append(
" (4) Salah ketik luas 358.5 m2 (Bagian 1) menghasilkan sisaan latih"
)
L.append(
" terbesar model final ({:+.1f} juta): luas tercatat sangat".format(
sisa_latih[i_sisa_maks]
)
)
L.append(" besar tetapi harga moderat - kombinasi membingungkan model.")
L.append(" (5) Sisaan data uji terbesar (faktor tak teramati):")
for i in urut_sisa.index:
baris = df.loc[i]
L.append(
" - {} {} m2: harga {:.1f}, prediksi {:.1f} (sisa {:+.1f})".format(
baris["kota"],
fmt(baris["luas_m2"]),
baris["harga_juta"],
pred_uji[i],
sisa_uji[i],
)
)
L.append("")
L.append("6. BATASAN")
L.append(" - Data sintetis (fungsi linear + derau Gaussian), bukan pasar nyata.")
L.append(" - 400 baris dan 5 kota Jabodetabek; generalisasi luar wilayah")
L.append(" tidak terjamin.")
L.append(" - Holdout tunggal tanpa interval ketidakpastian pada metrik.")
L.append(" - Pencilan dipertahankan (keputusan eksplisit); alternatifnya")
L.append(" memangkas atau memodelkan segmen listing mewah terpisah.")
L.append("")
L.append("Dibangkitkan ulang secara identik oleh src/05_laporan.py (seed 42).")
isi_laporan = "\n".join(L) + "\n"
with open(BERKAS_LAPORAN, "w", encoding="utf-8") as f:
f.write(isi_laporan)
print(f"\n== {BERKAS_LAPORAN} ditulis ({len(L)} baris) ==")
print("\n== Isi laporan ==")
print(isi_laporan, end="")Output yang Dihasilkan:
== Holdout disiapkan: 80 baris (test_size=0.2, random_state=42) ==
Ridge alpha=10 MAE 240.0 RMSE 309.3 R2 0.8072
Lasso alpha=10 MAE 229.2 RMSE 300.9 R2 0.8175
RandomForest depth=8, 200 pohon MAE 212.6 RMSE 284.1 R2 0.8374
== laporan/laporan-harga-rumah.txt ditulis (59 baris) ==
== Isi laporan ==
========================================================================
LAPORAN PROYEK: PREDIKSI HARGA RUMAH JABODETABEK
Dataset sintetis deterministik (seed 42) - 400 baris
========================================================================
1. DATA
Sumber : data/rumah.csv (dibangkitkan src/01_bangun_data.py)
Ukuran : 400 baris x 6 kolom (5 fitur + target harga_juta)
Nilai hilang : luas_m2 = 12 baris, usia_bangunan = 8 baris
Keanealan : 2 baris salah ketik luas (354-359 m2),
4 baris ber-premium tinggi tanpa penjelasan fitur
Harga (juta) : min 156.4 | rata-rata 1958.6 | median 1949.1
maks 3885.1
2. PREPROCESSING
Numerik (4) : imputasi median + StandardScaler
Kategorikal : kota -> OneHotEncoder (5 kolom biner)
Arsitektur : ColumnTransformer di dalam Pipeline;
statistik dipelajari hanya dari data latih
3. PEMILIHAN MODEL (Bagian 3: GridSearchCV, 3 lipatan, data latih)
Kandidat : Ridge, Lasso, RandomForestRegressor
Juara CV : RandomForestRegressor(max_depth=8,
n_estimators=200) - R2 CV 0.8097
4. EVALUASI HOLDOUT (80 baris data uji, tak pernah dilatih)
model MAE juta RMSE juta R2
Ridge alpha=10 240.0 309.3 0.8072
Lasso alpha=10 229.2 300.9 0.8175
RandomForest depth=8, 200 pohon 212.6 284.1 0.8374
Model final : RandomForest depth=8, 200 pohon
MAE relatif : 11.0% dari rata-rata harga data uji (1932.6 juta)
5. TEMUAN UTAMA
(1) luas_m2 dominan: kepentingan fitur 0.8665; koefisien Ridge
393.2 juta per satu simpangan baku luas (sekitar 59 m2).
(2) jarak_pusat_km: korelasi marginal -0.126 terlihat lemah,
tetapi koefisien Ridge -101.5 juta per simpangan baku setelah
kota dikontrol - efek kota dan jarak saling menutupi.
(3) kamar_tidur dan luas_m2 berkorelasi +0.861; pada model
linear keduanya berbagi bobot (kamar +332.3), sedangkan
Lasso memangkas usia_bangunan dan 3 dummy kota menjadi nol.
(4) Salah ketik luas 358.5 m2 (Bagian 1) menghasilkan sisaan latih
terbesar model final (-742.4 juta): luas tercatat sangat
besar tetapi harga moderat - kombinasi membingungkan model.
(5) Sisaan data uji terbesar (faktor tak teramati):
- Bekasi 161.7 m2: harga 3195.8, prediksi 2233.4 (sisa +962.4)
- Jakarta 50.8 m2: harga 413.4, prediksi 1161.9 (sisa -748.5)
- Tangerang 103.9 m2: harga 2020.1, prediksi 1361.0 (sisa +659.1)
6. BATASAN
- Data sintetis (fungsi linear + derau Gaussian), bukan pasar nyata.
- 400 baris dan 5 kota Jabodetabek; generalisasi luar wilayah
tidak terjamin.
- Holdout tunggal tanpa interval ketidakpastian pada metrik.
- Pencilan dipertahankan (keputusan eksplisit); alternatifnya
memangkas atau memodelkan segmen listing mewah terpisah.
Dibangkitkan ulang secara identik oleh src/05_laporan.py (seed 42).Mari kita bedah alur evaluasinya:
- Baris 51–56 (segel dibuka):
train_test_splitdenganrandom_state=42yang sama dengan Bagian 3 memastikan 80 baris data uji ini persis baris yang tidak pernah dilatih grid search. Konsistensi seed antar-skrip adalah syarat kejujuran holdout. - Baris 60–89 (finalis dinilai): ketiga model dilatih ulang penuh pada 320 baris latih lalu diprediksi pada data uji. Ketiga metrik dihitung:
mean_absolute_error, akarmean_squared_error(dihitung vianp.sqrt— sklearn 1.5 tidak menyediakanroot_mean_squared_errorbentuk lama), danr2_score. - Baris 91–104 (diagnostik sisaan): model terbaik dipilih berdasarkan RMSE terkecil, lalu sisaan (harga nyata dikurangi prediksi) diurutkan — tiga sisaan data uji terbesar dan satu sisaan latih terbesar akan masuk laporan sebagai jendela ke kelemahan model.
- Baris 118 dst. (laporan dirakit dari angka hidup): setiap baris laporan diformat dari variabel yang baru dihitung —
f-stringdan.format()menyisipkan kepentingan fitur, koefisien, korelasi. Laporan tidak bisa basi atau salah ketik karena ditulis ulang dari sumber kebenaran yang sama setiap kali skrip berjalan; ditutup kalimat “Dibangkitkan ulang secara identik oleh src/05_laporan.py (seed 42)”.
Membaca Evaluasi Holdout #
Angka-angka data uji mengabarkan kabar yang sehat. RandomForest menang dengan MAE 212,6 juta, RMSE 284,1 juta, dan R2 0,8374 — lebih tinggi daripada R2 CV 0,8097-nya sendiri. Kenaikan skor pada data uji bukan kebetulan baik semata: grid search memilih konfigurasi dengan bias sedikit lebih tinggi (max_depth=8), dan generalisasi ke data segar justru terbantu. Selisih MAE antar finalis (212,6 vs 229,2 vs 240,0) juga konsisten dengan urutan papan skor CV — keputusan Bagian 3 tahan uji. Yang paling bisa dibaca pemangku kepentingan: MAE relatif 11,0% — rata-rata prediksi meleset 11% dari harga rata-rata; untuk alat bantu penilaian awal listing, tingkat galat semacam itu layak diperhitungkan, dengan batasan di bawah.
Diagnostik sisaan memberi kedalaman tambahan. Sisaan latih terbesar (−742,4 juta) jatuh tepat pada baris salah ketik luas 358,5 m² dari Bagian 1 — luas tercatat raksasa tetapi harga moderat, kombinasi yang membingungkan model; pencilan yang kita sengaja pertahankan kini terbukti harganya, dan laporan mencatatnya sebagai temuan, bukan menyembunyikannya. Sementara tiga sisaan data uji terbesar (hingga +962,4 juta) terjadi pada listing dengan harga jauh di atas pola fiturnya — faktor tak teramati seperti kondisi interior atau posisi strategis mikro, pengingat jujur bahwa lima fitur belum menceritakan seluruh kisah pasar.
Anatomi Laporan untuk Pemangku Kepentingan #
Struktur enam bagian pada laporan-harga-rumah.txt mengikuti alur pertanggungjawaban analisis. Bagian 1 (DATA) memaparkan sumber, ukuran, kekusaran — pemangku kepentingan perlu tahu dasar bukti sebelum memercayai angka apa pun. Bagian 2 (PREPROCESSING) merangkum arsitektur tanpa jargon kode. Bagian 3 (PEMILIHAN MODEL) mencatat protokol perbandingan dan juara CV. Bagian 4 (EVALUASI HOLDOUT) adalah inti klaim performa — tabel tiga model lengkap dengan metrik dan MAE relatif. Bagian 5 (TEMUAN UTAMA) menerjemahkan model ke bahasa bisnis: luas menguasai harga, jarak berpengaruh setelah kota dikontrol, korelasi luas-kamar berbagi bobot. Bagian 6 (BATASAN) menutup dengan kejujuran: data sintetis, 400 baris, lima kota, holdout tunggal, pencilan dipertahankan — batas-batas yang menentukan seberapa jauh klaim boleh dibawa.
Proyek pertama tuntas: dari pembangkitan data, eksplorasi, pipeline aman leakage, pemilihan model berprotokol, hingga evaluasi holdout dan laporan yang menyusun semuanya. Pola yang barusan kita jalankan — data deterministik, keputusan berbasis bukti, evaluasi jujur — akan terpakai lagi, tetapi masalah berikutnya membawa satu tantangan yang mengubah medan: kelas target yang hanya muncul pada sebagian kecil pelanggan. Tanjungan berikutnya menanti di Proyek: Klasifikasi Churn Pelanggan.
Referensi Resmi #
- scikit-learn User Guide: Model Evaluation — definisi resmi MAE, MSE, dan R2 yang dipakai pada holdout.
- sklearn.metrics.mean_absolute_error — API — metrik galat absolut rata-rata.
- sklearn.metrics.r2_score — API — koefisien determinasi.
- pandas User Guide: Computational tools — korelasi dan statistik ringkas yang melengkapi laporan.