Bagian 2 menutup dengan arsitektur yang teruji: ColumnTransformer di dalam Pipeline, sembilan fitur bersih, dan model Ridge sementara pada R2 CV 0,8078. Pertanyaan yang tersisa sekarang pertanyaan pemilihan estimator: apakah model linear sudah cukup, atau justru pohon keputusan yang lebih tahan terhadap dua baris salah ketik luas yang sengaja kita pertahankan? Bagian ini menjawabnya dengan cara yang tidak bisa dibantah — ketiga keluarga model dipertandingkan dalam satu GridSearchCV dengan protokol identik, pada data latih yang sama, dengan skor yang sama. Tidak ada model yang menang karena kebetulan; yang menang adalah keluarga model beserta konfigurasinya terbaik menurut cross-validation.

Tiga Keluarga Kandidat dan Alasannya #

Pilihan kandidat bukan asal kumpulkan model, melainkan tiga jawaban berbeda atas sifat data yang kita temukan di Bagian 1. Ridge menjawab multikolinearitas luas_m2–kamar_tidur (korelasi +0,861) dengan penalti L2 yang menjaga koefisien tetap stabil. Lasso menjawab kelebihan dimensi relatif (sembilan fitur hasil encoding, beberapa di antaranya lemah) dengan penalti L1 yang memangkas koefisien nol persis seperti teori di Section Regresi. RandomForestRegressor menjawab salah ketik luas 354–359 m² dan premium tak terjelaskan — pohon keputusan membelah ruang fitur secara lokal sehingga beberapa baris ekstrem tidak menyeret garis prediksi seluruh populasi.

KandidatJawaban atas masalah dataRisiko yang dijaga
RidgeMultikolinearitas fitur numerikKoefisien tak stabil pada fitur berkorelasi
LassoDimensi encoding berlebihFitur lewat-tampak dikira berpengaruh
RandomForestRegressorPencilan dan non-linearitasGaris lurus dipaksa ke baris ekstrem

GridSearchCV dengan Estimator yang Ditukar #

Trik kunci skrip bagian ini ada pada bentuk param_grid sebagai daftar kamus: setiap kamus mengganti estimator di slot "model" sekaligus hyperparameternya. Dengan satu GridSearchCV kita menguji empat nilai alpha untuk Ridge, empat untuk Lasso, dan empat kombinasi n_estimators × max_depth untuk RandomForest — dua belas kandidat, tiga lipatan, semua di bawah protokol yang persis sama. Data uji 20% tetap disegel: grid search hanya menyentuh 320 baris data latih, sehingga penilaian di Bagian 4 nanti tetap jujur.

python
# Pemilihan model: Ridge, Lasso, dan RandomForestRegressor dibandingkan
# dalam satu GridSearchCV (cv=3, KFold shuffle seed 42, grid kecil).
# Estimator ditukar lewat kunci "model" pada param_grid. Grid search
# hanya menyentuh data latih; 20% data uji disimpan untuk Bagian 4.

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.model_selection import GridSearchCV, KFold, train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler

BERKAS_CSV = "data/rumah.csv"
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 string_hp(params):
    """Rangkai hyperparameter (tanpa kunci estimator) jadi satu string."""
    return ", ".join(
        f"{k.split('__')[1]}={v}" for k, v in params.items() if k != "model"
    )


df = pd.read_csv(BERKAS_CSV)
X = df.drop(columns=["harga_juta"])
y = df["harga_juta"]

pipeline = Pipeline(steps=[("pra", bangun_pra()), ("model", Ridge())])

param_grid = [
    {"model": [Ridge()], "model__alpha": [0.1, 1.0, 10.0, 100.0]},
    {
        "model": [Lasso(max_iter=10000, random_state=42)],
        "model__alpha": [0.1, 1.0, 10.0, 100.0],
    },
    {
        "model": [RandomForestRegressor(random_state=42, n_jobs=1)],
        "model__n_estimators": [100, 200],
        "model__max_depth": [8, None],
    },
]

X_latih, X_uji, y_latih, y_uji = train_test_split(
    X, y, test_size=0.2, random_state=42
)
cv = KFold(n_splits=3, shuffle=True, random_state=42)

cari = GridSearchCV(pipeline, param_grid, cv=cv, scoring="r2")
cari.fit(X_latih, y_latih)
hasil = cari.cv_results_
keluarga = [type(p["model"]).__name__ for p in hasil["params"]]

# ---- 1. Papan skor seluruh kandidat ------------------------------------
print("== 1. Seluruh kandidat (R2 CV, 3 lipatan, 320 baris data latih) ==")
urut = sorted(
    range(len(keluarga)), key=lambda i: -hasil["mean_test_score"][i]
)
for i in urut:
    print(
        f"{keluarga[i]:22s} {string_hp(hasil['params'][i]):36s} "
        f"{hasil['mean_test_score'][i]:.4f}  {hasil['std_test_score'][i]:.4f}"
    )

# ---- 2. Terbaik per keluarga -------------------------------------------
print("\n== 2. Terbaik per keluarga model ==")
for fam in ["Ridge", "Lasso", "RandomForestRegressor"]:
    idx = [i for i, k in enumerate(keluarga) if k == fam]
    terbaik = max(idx, key=lambda i: hasil["mean_test_score"][i])
    print(
        f"{fam:22s} {string_hp(hasil['params'][terbaik]):36s} "
        f"R2 CV {hasil['mean_test_score'][terbaik]:.4f}"
    )

# ---- 3. Juara keseluruhan ----------------------------------------------
print("\n== 3. Juara keseluruhan ==")
print(f"estimator    : {type(cari.best_estimator_.named_steps['model']).__name__}")
print(f"konfigurasi  : {cari.best_estimator_.named_steps['model']}")
print(f"R2 CV latih  : {cari.best_score_:.4f}")

# ---- 4. Kepentingan fitur juara ----------------------------------------
nama_fitur = cari.best_estimator_.named_steps["pra"].get_feature_names_out()
kepentingan = (
    pd.Series(
        cari.best_estimator_.named_steps["model"].feature_importances_,
        index=nama_fitur,
    )
    .sort_values(ascending=False)
    .round(4)
)
print("\n== 4. Kepentingan fitur model juara ==")
for nama, nilai in kepentingan.items():
    print(f"  {nama:25s}: {nilai:.4f}")

# ---- 5. Koefisien terbaik Lasso vs Ridge -------------------------------
tabel_koef = pd.DataFrame(index=nama_fitur)
for nama_model, estimator in [
    ("lasso_a10", Lasso(alpha=10.0, max_iter=10000, random_state=42)),
    ("ridge_a10", Ridge(alpha=10.0)),
]:
    pelatih = Pipeline(
        steps=[("pra", bangun_pra()), ("model", estimator)]
    )
    pelatih.fit(X_latih, y_latih)
    tabel_koef[nama_model] = pelatih.named_steps["model"].coef_
print("\n== 5. Koefisien Lasso vs Ridge (alpha=10, fitur terskala) ==")
tabel_koef = tabel_koef.round(2)
for nama, baris in tabel_koef.iterrows():
    print(f"  {nama:25s}: lasso {baris['lasso_a10']:8.2f}  ridge {baris['ridge_a10']:8.2f}")
print(
    "\nfitur dengan koefisien nol di Lasso: "
    + ", ".join(nama for nama in tabel_koef.index if tabel_koef.loc[nama, "lasso_a10"] == 0.0)
)

Output yang Dihasilkan:

text
== 1. Seluruh kandidat (R2 CV, 3 lipatan, 320 baris data latih) ==
RandomForestRegressor  max_depth=8, n_estimators=200        0.8097  0.0347
RandomForestRegressor  max_depth=8, n_estimators=100        0.8096  0.0352
RandomForestRegressor  max_depth=None, n_estimators=100     0.8095  0.0329
RandomForestRegressor  max_depth=None, n_estimators=200     0.8090  0.0336
Ridge                  alpha=10.0                           0.7960  0.0070
Lasso                  alpha=10.0                           0.7958  0.0070
Ridge                  alpha=1.0                            0.7895  0.0088
Lasso                  alpha=1.0                            0.7895  0.0089
Ridge                  alpha=0.1                            0.7884  0.0093
Lasso                  alpha=0.1                            0.7884  0.0093
Ridge                  alpha=100.0                          0.7725  0.0149
Lasso                  alpha=100.0                          0.7648  0.0215

== 2. Terbaik per keluarga model ==
Ridge                  alpha=10.0                           R2 CV 0.7960
Lasso                  alpha=10.0                           R2 CV 0.7958
RandomForestRegressor  max_depth=8, n_estimators=200        R2 CV 0.8097

== 3. Juara keseluruhan ==
estimator    : RandomForestRegressor
konfigurasi  : RandomForestRegressor(max_depth=8, n_estimators=200, n_jobs=1, random_state=42)
R2 CV latih  : 0.8097

== 4. Kepentingan fitur model juara ==
  num__luas_m2             : 0.8665
  num__jarak_pusat_km      : 0.0565
  num__kamar_tidur         : 0.0359
  num__usia_bangunan       : 0.0284
  kat__kota_Depok          : 0.0041
  kat__kota_Bekasi         : 0.0028
  kat__kota_Tangerang      : 0.0027
  kat__kota_Bogor          : 0.0017
  kat__kota_Jakarta        : 0.0014

== 5. Koefisien Lasso vs Ridge (alpha=10, fitur terskala) ==
  num__luas_m2             : lasso   403.90  ridge   393.23
  num__kamar_tidur         : lasso   324.25  ridge   332.30
  num__usia_bangunan       : lasso    -0.00  ridge     0.08
  num__jarak_pusat_km      : lasso  -107.44  ridge  -101.51
  kat__kota_Bekasi         : lasso     0.00  ridge    21.56
  kat__kota_Bogor          : lasso    -0.00  ridge   -20.28
  kat__kota_Depok          : lasso    -0.00  ridge   -28.23
  kat__kota_Jakarta        : lasso    52.48  ridge    79.74
  kat__kota_Tangerang      : lasso    -2.14  ridge   -52.80

fitur dengan koefisien nol di Lasso: num__usia_bangunan, kat__kota_Bekasi, kat__kota_Bogor, kat__kota_Depok

Mari kita bedah bagian-bagian yang memikul keputusan:

  1. Baris 20–39 (bangun_pra): arsitektur preprocessing Bagian 2 dipaketkan sebagai fungsi — dipanggil ulang tiga kali di skrip ini (grid search, Lasso, Ridge pembanding) tanpa risiko salinan yang tidak sinkron. Perubahan preprocessing kini berlaku serentak pada seluruh kandidat.
  2. Baris 55–66 (param_grid bertingkat): daftar kamus memungkinkan kunci "model" menukar estimator sekaligus membawa model__alpha atau model__n_estimators miliknya masing-masing. Lasso memakai max_iter=10000 agar konvergen pada data terskala; RandomForestRegressor mengunci random_state=42, n_jobs=1 demi determinisme.
  3. Baris 68–74 (protokol seragam): pembagian 80/20 dan KFold(n_splits=3, shuffle=True, random_state=42) ditetapkan sekali di luar kandidat — semua model dinilai pada lipatan yang persis sama, sehingga selisih skor murni perbedaan model, bukan perbedaan keberuntungan pembagian.
  4. Baris 78–87 (papan skor lengkap): seluruh 12 kandidat dicetak terurut beserta simpangan baku antar lipatan. Membaca papan penuh — bukan hanya pemenang — adalah disiplin penting: kita melihat bahwa selisih keempat varian RandomForest (0,8090–0,8097) jauh lebih kecil daripada selisihnya dengan Ridge (0,7960), artinya konfigurasi hutan tidak terlalu sensitif.

Membaca Hasil: Papan Skor dan Sang Juara #

Papan skor mengisahkan tiga fakta. Pertama, RandomForest menang tipis di atas model linear — 0,8097 berbanding 0,7960 — selisih 0,014 yang konsisten: seluruh varian hutan (0,8090–0,8097) berada di atas seluruh varian Ridge dan Lasso terbaik. Kemenangan tipis tetapi berlapis. Kedua, alpha=10 adalah titik manis kedua model linear: alpha terlalu kecil (0,1) membiarkan koefisien bergoyang, alpha terlalu besar (100) menekan model menjadi terlalu kaku dan skor jatuh ke 0,7725 — kurva U khas regularisasi dari Section Regresi. Ketiga, hutan tidak terlalu peduli kedalaman: max_depth=8 dan None berselisih kurang dari 0,001; membatasi kedalaman cukup untuk mencegah hafalan tanpa menutup sinyal.

Bagian 4 dan 5 keluaran skrip menambah dua kacamata interpretasi. Kepentingan fitur juara menegaskan num__luas_m2 berdiri di 0,8665 — empat perlima keputusan model digerakkan satu fitur — disusul jarak_pusat_km 0,0565; lima dummy kota nyaris nol. Sementara kontras koefisien Lasso vs Ridge (keduanya alpha=10) memperlihatkan seleksi yang jujur: Lasso memangkas usia_bangunan dan tiga dummy kota (Bekasi, Bogor, Depok) menjadi tepat nol, menandakan fitur tersebut tidak membawa sinyal melebihi derau pada data 320 baris ini. Ridge membiarkan koefisien kecil tersebar — dua karakter yang sudah kenal kita dari teori regularisasi, kini terlihat hidup di data sendiri.

Model juara telah terpilih dengan protokol yang tidak bisa diganggu gugat: RandomForestRegressor(max_depth=8, n_estimators=200) pada R2 CV 0,8097. Namun angka itu masih angka data latih — lipatan-lipatan yang, sejujurnya, sudah ikut menyeleksi konfigurasi model lewat grid search. Pengadilan terakhir menanti di luar sana: 80 baris data uji yang sejak Bagian 2 disegel dan tidak pernah disentuh estimator mana pun. Bagian 4 membuka segel itu, menilai finalis pada data uji, lalu merangkai seluruh temuan proyek menjadi satu laporan yang bisa dibaca pemangku kepentingan.

Referensi Resmi #