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.
| Kota | Rentang jarak ke pusat (km) | Penambah harga (juta) |
|---|---|---|
| Jakarta | 3–18 | 220 |
| Bogor | 28–60 | 35 |
| Depok | 14–32 | 60 |
| Tangerang | 16–38 | 50 |
| Bekasi | 18–42 | 45 |
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.
# 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:
== 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.1Mari kita bedah bagian-bagian yang memikul kontrak determinisme dan kekusahan data:
- Baris 13–19 dan 24–26 (
PARAM_KOTAdanrng.choice): kota diundi dengan probabilitas eksplisit — Jakarta 0,24 dan empat kota lain 0,16–0,20 — sehingga proporsi listing stabil tanpa asumsi tambahan. - Baris 27–31 (fitur numerik):
luasdanusiadiundi uniform, tetapikamardihitung dariluas / 45ditambah derau lalu dipotong ke rentang 1–5. Korelasi kamar–luas yang tinggi disengaja dan menjadi bahan diskusi multikolinearitas di Bagian 3. - Baris 32–46 (jarak dan fungsi harga): jarak diundi dari rentang
PARAM_KOTAper 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. - Baris 60–64 (satu permuntahan): seluruh indeks korupsi diambil dari satu
rng.permutationyang dipotong-potong tanpa tumpang tindih, sehingga tiap jenis kekusahan dapat dianalisis terpisah. - 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.
# 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:
== 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 rumahPembacaan skrip mengikuti nomor bagiannya:
- Bagian 3 (baris 20–21): baris
countpadadescribe()adalah detektor nilai hilang pertama —luas_m2hanya 388 danusia_bangunanhanya 392 dari 400 baris, selisihnya persis 12 dan 8. - 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.
- 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. - Bagian 7 (baris 46–65): metode IQR menandai 2 pencilan
luas_m2(354,1 dan 358,5 m²), 5 pencilan ringanjarak_pusat_km(ekor Bogor 57,4–59,5 km), dan nol pencilanharga_juta. IQR bekerja satu kolom saja; keanehan lintas kolom baru terlihat saat sisaan model dihitung. - 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:
| Temuan | Keputusan | Ditindaklanjuti di |
|---|---|---|
12 nilai luas_m2 dan 8 nilai usia_bangunan hilang | Imputasi median di dalam pipeline | Bagian 2 |
kota kategorikal lima nilai tanpa urutan | OneHotEncoder, bukan angka urut | Bagian 2 |
| 2 salah ketik luas dan 4 baris premium | Dipertahankan, didokumentasikan sebagai batasan | Bagian 3 dan 4 |
kamar_tidur berkorelasi 0,861 dengan luas_m2 | Lasso masuk daftar kandidat untuk pemangkasan fitur | Bagian 3 |
| Skala fitur berbeda jauh (meter, tahun, km) | StandardScaler untuk fitur numerik | Bagian 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 #
- pandas API: pandas.read_csv — pemuatan CSV dan penanganan nilai kosong.
- pandas API: DataFrame.describe — statistik deskriptif pemeriksaan ketiga.
- pandas User Guide: Group by — agregasi per kategori (statistik harga per kota).
- NumPy API: random.Generator
— generator acak modern pengganti
np.random.*.