Bagian ini memulai proyek dari titik nol: belum ada data, belum ada asumsi, dan belum ada model. Kita akan membangkitkan dataset harga rumah Jabodetabek secara deterministik, lalu menginterogasinya dengan eksplorasi data awal (EDA) sebagaimana kita memperlakukan data nyata. Sasarannya bukan sekadar angka, melainkan daftar keputusan preprocessing yang dibangun di atas bukti. Semua temuan pada bagian ini menjadi bahan baku langsung bagi Bagian 2: Preprocessing dan Pipeline.

Membangkitkan Dataset yang Deterministik #

Proyek memakai dataset sintetis demi tiga kebutuhan: hasil yang direproduksi bit demi bit, struktur harga yang diketahui sehingga kualitas model dinilai jujur, dan kendali penuh atas kekusahan data. Pembangkitnya sederhana: 400 listing lima fitur, harga mengikuti fungsi linear atas fitur numerik ditambah penambah harga per kota dan derau Gaussian. Kota tidak sekadar label — setiap kota membawa rentang jarak ke pusat sendiri sehingga kota dan jarak_pusat_km saling terkait sejak lahir.

KotaRentang jarak ke pusat (km)Penambah harga (juta)
Jakarta3–18220
Bogor28–6035
Depok14–3260
Tangerang16–3850
Bekasi18–4245

Kekusahan disisipkan terkendali: 12 baris tanpa luas_m2, 8 baris tanpa usia_bangunan, 4 baris premium tanpa penjelasan fitur, dan 2 baris salah ketik luas — satu nol ekstra pada entri. Skrip berikut membangkitkan sekaligus menyimpan dataset ke data/rumah.csv; jalankan dari root folder proyek.

python
# Pembangkit dataset harga rumah Jabodetabek yang deterministik.
# 400 baris, 6 kolom. Harga dibangun dari fungsi linear atas fitur
# numerik + pengaruh kota + derau Gaussian, lalu disisipi nilai
# hilang dan pencilan realistis agar eksplorasi data bermakna.

import numpy as np
import pandas as pd

BERKAS_CSV = "data/rumah.csv"
N_BARIS = 400

# Parameter per kota: (jarak min km, jarak maks km, penambah harga juta).
PARAM_KOTA = {
    "Jakarta": (3.0, 18.0, 220.0),
    "Bogor": (28.0, 60.0, 35.0),
    "Depok": (14.0, 32.0, 60.0),
    "Tangerang": (16.0, 38.0, 50.0),
    "Bekasi": (18.0, 42.0, 45.0),
}

rng = np.random.default_rng(42)

# ---- Fitur dasar -------------------------------------------------------
kota = rng.choice(
    list(PARAM_KOTA), size=N_BARIS, p=[0.24, 0.16, 0.20, 0.20, 0.20]
)
luas = rng.uniform(45.0, 240.0, N_BARIS).round(1)
kamar = np.clip(
    np.round(luas / 45.0 + rng.normal(0.0, 0.6, N_BARIS)), 1, 5
).astype(int)
usia = rng.uniform(0.0, 35.0, N_BARIS).round(1)
jarak = np.array(
    [rng.uniform(PARAM_KOTA[k][0], PARAM_KOTA[k][1]) for k in kota]
).round(1)
tambah_kota = np.array([PARAM_KOTA[k][2] for k in kota])

# ---- Harga: fungsi linear + pengaruh kota + derau ----------------------
harga = (
    300.0
    + 12.0 * luas
    + 20.0 * kamar
    - 3.0 * usia
    - 6.0 * jarak
    + tambah_kota
    + rng.normal(0.0, 250.0, N_BARIS)
).round(1)

df = pd.DataFrame(
    {
        "luas_m2": luas,
        "kamar_tidur": kamar,
        "usia_bangunan": usia,
        "jarak_pusat_km": jarak,
        "kota": kota,
        "harga_juta": harga,
    }
)

# ---- Sisipkan nilai hilang dan pencilan secara terkendali --------------
perm = rng.permutation(N_BARIS)
idx_luas_hilang = perm[:12]     # survey tidak mencatat luas
idx_usia_hilang = perm[12:20]   # sertifikat tanpa tahun bangunan
idx_premium = perm[20:24]       # premium tak terjelaskan (kawasan elite)
idx_typo = perm[24:26]          # salah ketik luas: satu nol ekstra

df.loc[idx_luas_hilang, "luas_m2"] = np.nan
df.loc[idx_usia_hilang, "usia_bangunan"] = np.nan
tambahan_premium = rng.uniform(650.0, 1100.0, 4).round(1)
df.loc[idx_premium, "harga_juta"] = (
    df.loc[idx_premium, "harga_juta"] + tambahan_premium
).round(1)
df.loc[idx_typo, "luas_m2"] = rng.uniform(280.0, 380.0, 2).round(1)

df.to_csv(BERKAS_CSV, index=False)

# ---- Ringkasan pembangkitan --------------------------------------------
print(f"== {BERKAS_CSV} berhasil ditulis ==")
print(f"ukuran        : {df.shape[0]} baris x {df.shape[1]} kolom")
print(
    "nilai hilang  : "
    + ", ".join(f"{k}={v}" for k, v in df.isna().sum().items() if v > 0)
)
print(
    f"pencilan      : {len(idx_premium)} baris premium "
    f"+{tambahan_premium.min():.0f}..{tambahan_premium.max():.0f} juta, "
    f"{len(idx_typo)} baris salah ketik luas "
    f"{df.loc[idx_typo, 'luas_m2'].min():.0f}-"
    f"{df.loc[idx_typo, 'luas_m2'].max():.0f} m2"
)
print(
    "sebaran kota  : "
    + ", ".join(
        f"{k}={v}" for k, v in df["kota"].value_counts().sort_index().items()
    )
)
print(
    f"harga (juta)  : min {df['harga_juta'].min():.1f} | "
    f"rata-rata {df['harga_juta'].mean():.1f} | "
    f"median {df['harga_juta'].median():.1f} | "
    f"maks {df['harga_juta'].max():.1f}"
)

Output yang Dihasilkan:

text
== data/rumah.csv berhasil ditulis ==
ukuran        : 400 baris x 6 kolom
nilai hilang  : luas_m2=12, usia_bangunan=8
pencilan      : 4 baris premium +726..1013 juta, 2 baris salah ketik luas 354-358 m2
sebaran kota  : Bekasi=75, Bogor=58, Depok=86, Jakarta=100, Tangerang=81
harga (juta)  : min 156.4 | rata-rata 1958.6 | median 1949.1 | maks 3885.1

Mari kita bedah bagian-bagian yang memikul kontrak determinisme dan kekusahan data:

  1. Baris 13–19 dan 24–26 (PARAM_KOTA dan rng.choice): kota diundi dengan probabilitas eksplisit — Jakarta 0,24 dan empat kota lain 0,16–0,20 — sehingga proporsi listing stabil tanpa asumsi tambahan.
  2. Baris 27–31 (fitur numerik): luas dan usia diundi uniform, tetapi kamar dihitung dari luas / 45 ditambah derau lalu dipotong ke rentang 1–5. Korelasi kamar–luas yang tinggi disengaja dan menjadi bahan diskusi multikolinearitas di Bagian 3.
  3. Baris 32–46 (jarak dan fungsi harga): jarak diundi dari rentang PARAM_KOTA per baris — rumah Jakarta tidak pernah 50 km dari pusat. Harga dirakit dari koefisien 12 juta per m², 20 juta per kamar, −3 juta per tahun, −6 juta per km, penambah kota, dan derau; angka-angka ini adalah kunci jawaban yang membuat evaluasi Bagian 4 dapat diverifikasi.
  4. Baris 60–64 (satu permuntahan): seluruh indeks korupsi diambil dari satu rng.permutation yang dipotong-potong tanpa tumpang tindih, sehingga tiap jenis kekusahan dapat dianalisis terpisah.
  5. Baris 66–72 (penyisipan): np.nan, premium 650–1.100 juta, dan luas salah ketik 280–380 m². Premium sengaja bukan pencilan harga ekstrem — nilainya masih dalam sebaran wajar karena rumah besar memang mahal; status pencilannya baru muncul lewat sisaan prediksi di Bagian 4.

Interogasi Data dengan Sembilan Pemeriksaan #

Dataset yang baru ditulis tidak dipercaya begitu saja. Kita membacanya kembali dari disk untuk memastikan format tersimpan benar, lalu menjalankan sembilan pemeriksaan baku — dari bentuk dan tipe hingga distribusi kamar — persis yang kita pakai pada data nyata. Seluruh pemeriksaan memakai pandas murni dan mencetak angka, bukan grafik, agar keluaran tetap deterministik.

python
# Eksplorasi data awal (EDA) untuk dataset harga rumah.
# Memeriksa bentuk, tipe kolom, statistik deskriptif, nilai hilang,
# statistik harga per kota, korelasi terhadap harga, pencilan metode
# IQR, dan daftar listing dengan harga tertinggi.

import pandas as pd

BERKAS_CSV = "data/rumah.csv"

df = pd.read_csv(BERKAS_CSV)

print("== 1. Bentuk dan tipe kolom ==")
print(f"baris x kolom : {df.shape[0]} x {df.shape[1]}")
for kolom, tipe in df.dtypes.items():
    print(f"  {kolom:15s}: {tipe}")

print("\n== 2. Lima baris pertama ==")
print(df.head().to_string(index=False))

print("\n== 3. Statistik deskriptif kolom numerik ==")
print(df.describe().round(1).to_string())

print("\n== 4. Nilai hilang per kolom ==")
for kolom in df.columns:
    n_hilang = int(df[kolom].isna().sum())
    print(f"  {kolom:15s}: {n_hilang}")

print("\n== 5. Statistik harga per kota ==")
stat_kota = (
    df.groupby("kota")["harga_juta"]
    .agg(["count", "mean", "median", "std"])
    .round(1)
)
print(stat_kota.to_string())

print("\n== 6. Korelasi Pearson terhadap harga_juta ==")
korelasi = (
    df.corr(numeric_only=True)["harga_juta"]
    .drop("harga_juta")
    .sort_values(ascending=False)
    .round(3)
)
for kolom, nilai in korelasi.items():
    print(f"  {kolom:15s}: {nilai:+.3f}")

print("\n== 7. Pencilan metode IQR (di luar Q1-1.5xIQR .. Q3+1.5xIQR) ==")
for kolom in [
    "luas_m2",
    "kamar_tidur",
    "usia_bangunan",
    "jarak_pusat_km",
    "harga_juta",
]:
    q1 = df[kolom].quantile(0.25)
    q3 = df[kolom].quantile(0.75)
    iqr = q3 - q1
    batas_bawah = q1 - 1.5 * iqr
    batas_atas = q3 + 1.5 * iqr
    n_pencilan = int(
        ((df[kolom] < batas_bawah) | (df[kolom] > batas_atas)).sum()
    )
    print(
        f"  {kolom:15s}: {n_pencilan:2d} baris di luar "
        f"[{batas_bawah:.1f}, {batas_atas:.1f}]"
    )

print("\n== 8. Lima listing dengan harga tertinggi ==")
kolom_tampil = [
    "kota",
    "luas_m2",
    "kamar_tidur",
    "usia_bangunan",
    "jarak_pusat_km",
    "harga_juta",
]
print(df.nlargest(5, "harga_juta")[kolom_tampil].to_string(index=False))

print("\n== 9. Distribusi kamar_tidur ==")
for kamar, n in df["kamar_tidur"].value_counts().sort_index().items():
    print(f"  {kamar} kamar : {n} rumah")

Output yang Dihasilkan:

text
== 1. Bentuk dan tipe kolom ==
baris x kolom : 400 x 6
  luas_m2        : float64
  kamar_tidur    : int64
  usia_bangunan  : float64
  jarak_pusat_km : float64
  kota           : object
  harga_juta     : float64

== 2. Lima baris pertama ==
 luas_m2  kamar_tidur  usia_bangunan  jarak_pusat_km      kota  harga_juta
   103.9            3           21.1            37.2 Tangerang      2020.1
   224.1            5           17.0            18.8     Depok      2917.7
   197.4            5            9.1            28.3    Bekasi      2557.7
    66.6            2           14.7            25.5 Tangerang       808.5
   239.4            5           26.5             6.8   Jakarta      3493.8

== 3. Statistik deskriptif kolom numerik ==
       luas_m2  kamar_tidur  usia_bangunan  jarak_pusat_km  harga_juta
count    388.0        400.0          392.0           400.0       400.0
mean     141.7          3.1           17.6            25.3      1958.6
std       59.6          1.3           10.2            12.2       784.4
min       45.2          1.0            0.0             3.0       156.4
25%       91.2          2.0            8.4            16.7      1342.2
50%      141.3          3.0           16.8            24.8      1949.1
75%      191.3          4.0           27.0            32.8      2598.3
max      358.5          5.0           35.0            59.5      3885.1

== 4. Nilai hilang per kolom ==
  luas_m2        : 12
  kamar_tidur    : 0
  usia_bangunan  : 8
  jarak_pusat_km : 0
  kota           : 0
  harga_juta     : 0

== 5. Statistik harga per kota ==
           count    mean  median    std
kota
Bekasi        75  1929.5  1975.1  812.9
Bogor         58  1916.4  1874.4  734.1
Depok         86  1930.3  1969.0  777.1
Jakarta      100  2114.3  2044.4  855.7
Tangerang     81  1853.8  1867.7  693.9

== 6. Korelasi Pearson terhadap harga_juta ==
  luas_m2        : +0.886
  kamar_tidur    : +0.837
  usia_bangunan  : -0.024
  jarak_pusat_km : -0.126

== 7. Pencilan metode IQR (di luar Q1-1.5xIQR .. Q3+1.5xIQR) ==
  luas_m2        :  2 baris di luar [-59.0, 341.5]
  kamar_tidur    :  0 baris di luar [-1.0, 7.0]
  usia_bangunan  :  0 baris di luar [-19.5, 55.0]
  jarak_pusat_km :  5 baris di luar [-7.4, 56.9]
  harga_juta     :  0 baris di luar [-542.0, 4482.5]

== 8. Lima listing dengan harga tertinggi ==
   kota  luas_m2  kamar_tidur  usia_bangunan  jarak_pusat_km  harga_juta
Jakarta    239.8            5           17.4            17.8      3885.1
Jakarta    239.5            5           17.0            11.2      3771.5
Jakarta    211.4            5           11.0             8.7      3763.5
 Bekasi    208.6            5           14.6            29.4      3755.7
  Bogor    235.7            5           32.6            43.0      3637.5

== 9. Distribusi kamar_tidur ==
  1 kamar : 60 rumah
  2 kamar : 80 rumah
  3 kamar : 96 rumah
  4 kamar : 83 rumah
  5 kamar : 81 rumah

Pembacaan skrip mengikuti nomor bagiannya:

  1. Bagian 3 (baris 20–21): baris count pada describe() adalah detektor nilai hilang pertama — luas_m2 hanya 388 dan usia_bangunan hanya 392 dari 400 baris, selisihnya persis 12 dan 8.
  2. Bagian 5 (baris 28–34): Jakarta unggul pada rata-rata (2.114,3 juta) dan median (2.044,4 juta); Tangerang paling terjangkau. Selisih antar kota terbaca paling jelas pada median karena tidak terpengaruh ekor sebaran.
  3. Bagian 6 (baris 36–44): korelasi Pearson memeringkat fitur — luas_m2 +0,886, kamar_tidur +0,837, usia_bangunan −0,024, jarak_pusat_km −0,126. Dua yang terakhir tampak nyaris tak berguna padahal koefisien pembangkitnya nyata; penyebabnya dibahas pada bagian temuan.
  4. Bagian 7 (baris 46–65): metode IQR menandai 2 pencilan luas_m2 (354,1 dan 358,5 m²), 5 pencilan ringan jarak_pusat_km (ekor Bogor 57,4–59,5 km), dan nol pencilan harga_juta. IQR bekerja satu kolom saja; keanehan lintas kolom baru terlihat saat sisaan model dihitung.
  5. Bagian 8 (baris 67–76): lima listing termahal adalah rumah 208–240 m² berkamar lima, mayoritas di Jakarta — pola yang konsisten dengan fungsi harga, bukan anomali.

Temuan Awal dan Keputusan yang Dibawa #

Korelasi marginal jarak_pusat_km yang lemah (−0,126) adalah temuan paling instruktif. Rumah Bogor jauh dari pusat tetapi penambah harganya kecil; rumah Jakarta dekat dengan penambah besar — dua efek yang nyaris saling menutupi di data mentah. Setelah kota masuk model sebagai variabel kontrol, efek jarak dalam satu kota muncul utuh: confounding klasik dan alasan memakai model multivariabel, bukan korelasi satu-per-satu.

Setiap temuan eksplorasi diterjemahkan menjadi keputusan yang dieksekusi di bagian berikutnya:

TemuanKeputusanDitindaklanjuti di
12 nilai luas_m2 dan 8 nilai usia_bangunan hilangImputasi median di dalam pipelineBagian 2
kota kategorikal lima nilai tanpa urutanOneHotEncoder, bukan angka urutBagian 2
2 salah ketik luas dan 4 baris premiumDipertahankan, didokumentasikan sebagai batasanBagian 3 dan 4
kamar_tidur berkorelasi 0,861 dengan luas_m2Lasso masuk daftar kandidat untuk pemangkasan fiturBagian 3
Skala fitur berbeda jauh (meter, tahun, km)StandardScaler untuk fitur numerikBagian 2

Satu keputusan penting justru berupa penolakan: pencilan tidak dibuang. Salah ketik luas idealnya diperbaiki di hulu, sedangkan premium adalah variasi pasar sah yang tak terjelaskan fitur; keduanya dipertahankan agar Bagian 3 dan 4 dapat menunjukkan beda ketahanan model terhadap baris seperti itu.

Eksplorasi menghasilkan resep siap dieksekusi: median untuk imputasi, scaler untuk numerik, encoder untuk kategori, dan janji bahwa semuanya dipelajari hanya dari data latih. Merangkai resep itu menjadi satu objek yang aman dari data leakage menanti di Bagian 2: Preprocessing dan Pipeline.

Referensi Resmi #