# Praktik Data Preprocessing dengan Python: dari Missing Value sampai PCA

> *Catatan belajar plus kode langsung jalan — enam teknik inti data preprocessing yang paling sering dipakai sebelum data masuk ke model Machine Learning, lengkap dengan angka hasil eksekusi asli, bukan contoh yang cuma ditempel.*

Ada ungkapan yang sering diulang-ulang di dunia data science: "80% kerjaan AI itu ngurusin data, bukan model" (angka pastinya bervariasi tergantung sumber, tapi intinya sering disepakati). Biasanya yang dimaksud ya bagian ini: mengecek data kosong, membuang/menangani outlier, mengubah kategori dan teks jadi angka, memampatkan dimensi, dan menyimpan riwayat versi data. Semua praktik di bawah ini dijalankan langsung dengan Python (Pandas + Scikit-learn), pakai dua jenis data: dataset pelanggan sungguhan berisi 10.000 baris, dan beberapa contoh sintetis untuk memperjelas konsep.

> 📂 Notebook lengkapnya (sudah dieksekusi penuh, plus dataset-nya) ada di GitHub: [github.com/arielshakaramiro/data-preprocessing-praktik-arielshakaramiro](https://github.com/arielshakaramiro/data-preprocessing-praktik-arielshakaramiro)

## Daftar Isi

*   [1\. Data Collection & Storage: Konsep Singkat](#1-data-collection--storage-konsep-singkat)
    
*   [2\. Missing Values: Cek Dulu, Jangan Asumsi](#2-missing-values-cek-dulu-jangan-asumsi)
    
*   [3\. Mendeteksi & Menangani Outlier dengan IQR](#3-mendeteksi--menangani-outlier-dengan-iqr)
    
*   [4\. Feature Engineering: Kategori Jadi Angka](#4-feature-engineering-kategori-jadi-angka)
    
*   [5\. Text Preprocessing: Bag of Words & TF-IDF](#5-text-preprocessing-bag-of-words--tf-idf)
    
*   [6\. Mengurangi Dimensi Data dengan PCA](#6-mengurangi-dimensi-data-dengan-pca)
    
*   [7\. Konsep Data Versioning](#7-konsep-data-versioning)
    
*   [8\. Rangkuman & Langkah Selanjutnya](#8-rangkuman--langkah-selanjutnya)
    
*   [9\. Cheat Sheet: Checklist Preprocessing](#9-cheat-sheet-checklist-preprocessing)
    
*   [10\. Quiz Check](#10-quiz-check)
    

* * *

## 1\. Data Collection & Storage: Konsep Singkat

Sebelum masuk ke praktik, ada dua jenis data yang perlu dikenali:

*   **Unstructured Data** — data yang "bentuknya bebas", misalnya gambar, teks, audio. Manusia bisa langsung memahami maknanya tanpa struktur khusus (contoh: kita bisa langsung mengenali gambar kucing).
    
*   **Structured Data** — data yang rapi dalam bentuk tabel/baris-kolom, misalnya data transaksi atau data pelanggan. Maknanya sangat tergantung konteks bisnis, sehingga tidak selalu bisa dipahami "sekilas lihat" oleh orang awam.
    

Kedua jenis data ini butuh tempat penyimpanan yang **aman, mudah diakses, dan bisa di-versioning** (disimpan riwayat perubahannya) — konsep versioning ini dibahas lebih lanjut di bagian 7.

* * *

## 2\. Missing Values: Cek Dulu, Jangan Asumsi

Ada dua pendekatan umum menangani nilai kosong (`NaN`):

*   **Dropna** — buang baris/kolom yang datanya kosong. Cocok kalau yang hilang sedikit.
    
*   **Fillna** — isi nilai kosong dengan pengganti: mean, median, atau modus (nilai paling sering muncul).
    

**Langkah pertama yang sering dilewatkan pemula: cek dulu, jangan langsung asumsi datanya kotor.**

```python
sample_data = pd.read_csv('customers-10000.csv')
print(sample_data.isnull().sum())
```

*(Kode aslinya memuat dataset lewat link Google Drive; di sini ditulis seolah file lokal supaya gampang dicoba ulang — isinya persis sama, dataset publik 10.000 baris dari Datablist.)*

Hasil eksekusi pada dataset pelanggan (10.000 baris, 12 kolom — `Index`, `Customer Id`, `First Name`, `Last Name`, `Company`, `City`, `Country`, `Phone 1`, `Phone 2`, `Email`, `Subscription Date`, `Website`):

```plaintext
Missing value per kolom: 0 (semua kolom, 0.0%)
```

> 🤔 **Coba Tebak Dulu:** Menurutmu, dataset pelanggan publik seperti ini biasanya berapa persen datanya yang kosong?
> 
> Lihat Jawaban
> 
> Di kasus ini: **0%** — dataset ini memang dataset sintetis untuk latihan (dibuat dengan Faker), jadi sengaja dibuat rapi tanpa nilai kosong. Ini justru pelajaran penting: jangan pernah asumsikan sebuah dataset "pasti kotor". Selalu jalankan `.isnull().sum()` dulu sebelum memutuskan strategi apa pun — kalau ternyata bersih, kamu hemat waktu; kalau ternyata ada yang kosong, kamu tahu persis di kolom mana.

Untuk melihat kedua teknik penanganannya, dipakai data sintetis kecil yang sengaja dibuat berlubang:

```python
data = {
    'nama': ['Andi', 'Budi', 'Citra', 'Dewi', 'Eka'],
    'umur': [25, np.nan, 30, 22, np.nan],
    'gaji': [5000000, 6000000, np.nan, 4500000, 5200000]
}
df = pd.DataFrame(data)
```

| nama | umur | gaji |
| --- | --- | --- |
| Andi | 25 | 5.000.000 |
| Budi | NaN | 6.000.000 |
| Citra | 30 | NaN |
| Dewi | 22 | 4.500.000 |
| Eka | NaN | 5.200.000 |

Jumlah & persentase missing value per kolom (hasil eksekusi `df.isnull().sum()` dan `df.isnull().sum() / len(df) * 100`):

| Kolom | Jumlah Kosong | Persentase |
| --- | --- | --- |
| nama | 0 | 0% |
| umur | 2 | 40% |
| gaji | 1 | 20% |

**Opsi 1 —** `dropna()`**:**

```python
df_dropped = df.dropna()
```

Baris Budi, Citra, Eka ikut terbuang karena masing-masing punya satu sel kosong — dari 5 baris tersisa 2 baris. Efektif kalau data yang hilang cuma sedikit dan baris yang hilang tidak penting; boros kalau data berharga tapi kebetulan ada satu kolom bolong.

**Opsi 2 —** `fillna()` **dengan mean/median:**

```python
df_filled = df.copy()
df_filled['umur'] = df_filled['umur'].fillna(df_filled['umur'].mean())
df_filled['gaji'] = df_filled['gaji'].fillna(df_filled['gaji'].median())
```

Umur kosong diisi rata-rata umur yang ada, gaji kosong diisi nilai tengah (median). *(Tambahan penjelasan di luar kode aslinya: median dipilih untuk gaji karena secara umum lebih tahan terhadap nilai ekstrem dibanding mean — poin ini akan lebih jelas maknanya setelah bagian outlier di bawah.)*

* * *

## 3\. Mendeteksi & Menangani Outlier dengan IQR

Outlier adalah data yang nilainya jauh berbeda dari mayoritas. Aturan pengambilan keputusannya:

*   Outlier **jarang terjadi dan tidak berdampak ke bisnis** → boleh dibuang.
    
*   Outlier **mengganggu proses bisnis** → jangan dibuang begitu saja, perlu ditangani lebih hati-hati (di skala production, salah satu pendekatan lanjutan adalah *Autoencoder* — model kecil yang belajar pola data normal, sehingga data yang menyimpang jauh dari pola itu punya error rekonstruksi besar dan bisa ditandai sebagai outlier).
    

Untuk pemula, salah satu metode paling umum dan sederhana adalah **IQR (Interquartile Range)**. Simulasinya: 1000 data gaji berdistribusi normal (rata-rata Rp5.000.000, standar deviasi Rp1.000.000), lalu disisipkan 1 outlier ekstrem sebesar Rp50.000.000.

```python
# Membuat contoh data gaji, dengan 1 outlier yang sangat besar
np.random.seed(42)
gaji = np.random.normal(5000000, 1000000, 1000).tolist()
gaji.append(50000000)  # outlier: gaji yang jauh lebih besar dari yang lain
df_gaji = pd.DataFrame({'gaji': gaji})
```

Visualisasi cepat sebelum dihitung — kode boxplot-nya:

```python
plt.figure(figsize=(6, 4))
plt.boxplot(df_gaji['gaji'])
plt.title('Boxplot Gaji (perhatikan ada titik yang jauh di atas)')
plt.ylabel('Gaji')
plt.show()
```

![Boxplot Gaji menunjukkan satu titik outlier ekstrem jauh di atas kotak IQR](https://cdn.hashnode.com/uploads/covers/6a8ef2e3923670c989379174/dba698ff-d045-43e3-8f64-4ff0bdf1b38a.png align="center")

Baru dihitung batasnya secara matematis dengan IQR:

```python
# Deteksi outlier dengan metode IQR
Q1 = df_gaji['gaji'].quantile(0.25)
Q3 = df_gaji['gaji'].quantile(0.75)
IQR = Q3 - Q1
batas_bawah = Q1 - 1.5 * IQR
batas_atas = Q3 + 1.5 * IQR

outliers = df_gaji[(df_gaji['gaji'] < batas_bawah) | (df_gaji['gaji'] > batas_atas)]
```

Hasil eksekusi nyata (dengan `random seed 42`, jadi angkanya konsisten kalau dijalankan ulang):

| Metrik | Nilai |
| --- | --- |
| Q1 (kuartil bawah) | Rp4.353.427 |
| Q3 (kuartil atas) | Rp5.648.710 |
| IQR | Rp1.295.283 |
| Batas bawah | Rp2.410.503 |
| Batas atas | Rp7.591.634 |
| Outlier terdeteksi | **9 dari 1.001 data** |

> 🤔 **Coba Tebak Dulu:** Cuma disisipkan 1 outlier buatan (Rp50 juta). Menurutmu, IQR bakal mendeteksi tepat 1 outlier juga, atau bisa lebih?
> 
> Lihat Jawaban
> 
> Terdeteksi **9 outlier**, bukan cuma 1. Selain angka Rp50 juta yang memang disisipkan, ada 8 data lain (hasil distribusi normal biasa, bukan disengaja) yang kebetulan jatuh di luar rentang 1,5×IQR — dan posisinya di **dua sisi**: 4 di bawah batas bawah (sekitar Rp1,76–2,38 juta) dan 4 di atas batas atas (sekitar Rp7,63–8,85 juta). Ini pelajaran penting: metode IQR itu murni statistik, dia tidak tahu mana outlier yang "disengaja/anomali" dan mana yang cuma "kebetulan ekstrem karena sebaran data". Makanya sebelum membuang data yang ditandai IQR, tetap perlu dicek konteks bisnisnya — misalnya gaji Rp8 juta di dataset ini mungkin memang gaji direktur yang valid, bukan kesalahan input, dan gaji Rp1,8 juta mungkin memang pegawai magang.

Setelah dibuang, sisa data bersih:

```python
df_bersih = df_gaji[(df_gaji['gaji'] >= batas_bawah) & (df_gaji['gaji'] <= batas_atas)]
```

**992 dari 1.001 baris** tersisa.

* * *

## 4\. Feature Engineering: Kategori Jadi Angka

Model Machine Learning cuma mengerti angka, bukan teks kategori seperti "Apple" atau "Chicken". Dua cara paling umum mengonversinya:

| Teknik | Cara Kerja | Kapan Dipakai |
| --- | --- | --- |
| Label Encoding | Setiap kategori diberi 1 angka unik | Kategori yang punya urutan/tingkatan (rendah–sedang–tinggi) |
| One Hot Encoding | Setiap kategori jadi kolom sendiri berisi 0/1 | Kategori tanpa urutan (nama makanan, kota, dll) — lebih aman secara default |

Data contoh:

```python
food = pd.DataFrame({
    'Food Name': ['Apple', 'Chicken', 'Broccoli'],
    'Calories': [95, 231, 50]
})
```

| Food Name | Calories |
| --- | --- |
| Apple | 95 |
| Chicken | 231 |
| Broccoli | 50 |

**Label Encoding** (hasil eksekusi `LabelEncoder`):

| Food Name | Calories | Categorical # |
| --- | --- | --- |
| Apple | 95 | 0 |
| Chicken | 231 | 2 |
| Broccoli | 50 | 1 |

Perhatikan angkanya diurutkan alfabetis (Apple=0, Broccoli=1, Chicken=2) — bukan berdasarkan kalori atau urutan logis apa pun. Ini sumber bug klasik: kalau dipakai untuk kategori tanpa urutan, model bisa salah "mengira" Chicken (2) lebih besar/penting dua kali lipat dari Broccoli (1), padahal itu cuma nomor urut alfabet.

**One Hot Encoding** (hasil eksekusi `pd.get_dummies`):

| Calories | Food Name\_Apple | Food Name\_Broccoli | Food Name\_Chicken |
| --- | --- | --- | --- |
| 95 | 1 | 0 | 0 |
| 231 | 0 | 0 | 1 |
| 50 | 0 | 1 | 0 |

Tidak ada urutan tersirat — setiap kategori dapat kolom sendiri. Trade-off-nya: kalau kategorinya ratusan/ribuan (misalnya kode pos), jumlah kolom bisa meledak.

* * *

## 5\. Text Preprocessing: Bag of Words & TF-IDF

Sama seperti kategori, teks juga perlu diubah jadi vektor angka (vektorisasi). Korpus contoh (3 kalimat pendek Bahasa Indonesia):

```python
corpus = [
    "rumah ini bagus",        # d1
    "rumah saya makan nasi",  # d2
    "saya makan nasi"         # d3
]
```

**Bag of Words** — menghitung berapa kali tiap kata muncul di tiap dokumen:

```python
vectorizer = CountVectorizer()
bow_matrix = vectorizer.fit_transform(corpus)

bow_df = pd.DataFrame(
    bow_matrix.toarray(),
    columns=vectorizer.get_feature_names_out(),
    index=['d1', 'd2', 'd3']
)
```

Hasil eksekusi (`bow_df`):

|  | bagus | ini | makan | nasi | rumah | saya |
| --- | --- | --- | --- | --- | --- | --- |
| d1 | 1 | 1 | 0 | 0 | 1 | 0 |
| d2 | 0 | 0 | 1 | 1 | 1 | 1 |
| d3 | 0 | 0 | 1 | 1 | 0 | 1 |

BoW punya kelemahan: kata yang sering muncul di *semua* dokumen dianggap "penting" secara nilai, padahal belum tentu informatif untuk membedakan dokumen satu dengan lainnya.

**TF-IDF (Term Frequency–Inverse Document Frequency)** memperbaiki ini — kata yang muncul di banyak dokumen diberi bobot lebih rendah, kata yang unik/jarang diberi bobot lebih tinggi:

```python
tfidf_vectorizer = TfidfVectorizer()
tfidf_matrix = tfidf_vectorizer.fit_transform(corpus)

tfidf_df = pd.DataFrame(
    tfidf_matrix.toarray(),
    columns=tfidf_vectorizer.get_feature_names_out(),
    index=['d1', 'd2', 'd3']
).round(3)
```

Hasil eksekusi (`tfidf_df`):

|  | bagus | ini | makan | nasi | rumah | saya |
| --- | --- | --- | --- | --- | --- | --- |
| d1 | 0.623 | 0.623 | 0.000 | 0.000 | 0.474 | 0.000 |
| d2 | 0.000 | 0.000 | 0.500 | 0.500 | 0.500 | 0.500 |
| d3 | 0.000 | 0.000 | 0.577 | 0.577 | 0.000 | 0.577 |

> 🤔 **Coba Tebak Dulu:** Kata "rumah" muncul di d1 dan d2, sedangkan "bagus" cuma muncul di d1. Menurutmu, bobot TF-IDF kata "bagus" di d1 lebih tinggi atau lebih rendah dibanding bobot "rumah" di d1?
> 
> Lihat Jawaban
> 
> **Lebih tinggi** (0.623 vs 0.474). Karena "rumah" muncul di 2 dari 3 dokumen, ia dianggap kurang khas/kurang membedakan → bobotnya diturunkan. "Bagus" cuma muncul di 1 dokumen (d1) → dianggap lebih khas untuk dokumen itu → bobotnya lebih tinggi. Inilah inti IDF: makin jarang sebuah kata muncul di seluruh korpus, makin tinggi "nilai pembeda"-nya untuk dokumen tempat ia muncul.

> 💡 **Tambahan di luar materi utama:** Ada metode yang lebih canggih bernama **Word2Vec** — neural network kecil yang mempelajari makna kata dari kata-kata di sekitarnya, bukan sekadar menghitung frekuensi. Hasilnya, kata-kata bermakna mirip akan punya posisi vektor yang berdekatan (misalnya "Raja" dan "Ratu" berdekatan pada dimensi tertentu yang merepresentasikan "kekuasaan"). Ini topik lanjutan setelah menguasai BoW dan TF-IDF.

* * *

## 6\. Mengurangi Dimensi Data dengan PCA

Kadang data punya terlalu banyak kolom/fitur, sehingga sulit divisualisasikan atau bikin model lambat. **PCA (Principal Component Analysis)** memampatkan data ke dimensi lebih kecil sambil mempertahankan informasi paling penting.

Contoh: dataset bunga Iris (4 fitur: panjang & lebar kelopak/mahkota) dimampatkan jadi 2 dimensi.

```python
iris = load_iris()
X = iris.data   # 4 fitur: panjang & lebar kelopak/mahkota bunga
y = iris.target # jenis bunga

pca = PCA(n_components=2)
X_pca = pca.fit_transform(X)

plt.figure(figsize=(6, 5))
scatter = plt.scatter(X_pca[:, 0], X_pca[:, 1], c=y, cmap='viridis')
plt.xlabel('Principal Component 1')
plt.ylabel('Principal Component 2')
plt.title('Data Iris setelah PCA (4 dimensi -> 2 dimensi)')
plt.legend(handles=scatter.legend_elements()[0], labels=list(iris.target_names))
plt.show()

print(f"Dimensi awal: {X.shape[1]} fitur")
print(f"Dimensi setelah PCA: {X_pca.shape[1]} fitur")
print(f"Informasi yang berhasil dijaga: {pca.explained_variance_ratio_.sum()*100:.1f}%")
```

![Scatter plot data Iris setelah PCA, 3 spesies bunga terlihat mengelompok terpisah](https://cdn.hashnode.com/uploads/covers/6a8ef2e3923670c989379174/a143e63f-4778-4a20-a0a4-7333d823b77f.png align="center")

Output dari ketiga baris `print` di atas, plus rincian per komponen (`pca.explained_variance_ratio_`):

| Metrik | Nilai |
| --- | --- |
| Dimensi awal | 4 fitur |
| Dimensi setelah PCA | 2 fitur |
| Variansi dijaga oleh PC1 | 92.5% |
| Variansi dijaga oleh PC2 | 5.3% |
| **Total informasi yang dijaga** | **97.8%** |

Artinya: dari 4 kolom asli dipangkas jadi 2 kolom, tapi cuma kehilangan sekitar 2,2% informasi — trade-off yang biasanya sangat sepadan untuk keperluan visualisasi atau mempercepat training model. Dari scatter plot-nya juga terlihat: spesies *setosa* (cyan) sudah terpisah jelas dari dua spesies lain bahkan cuma dengan 2 dimensi, sementara *versicolor* (biru) dan *virginica* (ungu) sedikit tumpang tindih di tengah.

* * *

## 7\. Konsep Data Versioning

Analogi sederhana: menyimpan dokumen sebagai `laporan_v1`, lalu `laporan_v2` setelah direvisi — supaya kalau versi baru ternyata salah, masih bisa kembali ke versi lama. **Data versioning** menerapkan ide yang sama untuk dataset.

Kenapa penting untuk Machine Learning:

*   Kalau model tiba-tiba memburuk setelah data diperbarui, bisa **rollback** ke versi data sebelumnya.
    
*   Proses pengembangan model jadi lebih **tertrack** (mudah dilacak riwayatnya) — penting untuk debugging dan audit.
    

Contoh tools yang lazim dipakai di dunia nyata:

| Kategori | Contoh Tools |
| --- | --- |
| Versioning dataset | DVC, GitLab |
| Anotasi + versioning | CVAT, Roboflow, SuperAnnotate |
| Tracking eksperimen model | MLflow, Neptune.ai, Weights & Biases |

Simulasi paling sederhana: menyimpan tiap "versi" dataset sebagai file terpisah.

```python
import os
os.makedirs('data_versions', exist_ok=True)

df_v1 = pd.DataFrame({'nama': ['Apple', 'Chicken'], 'kalori': [95, 231]})
df_v1.to_csv('data_versions/dataset_v1.csv', index=False)

df_v2 = df_v1.copy()
df_v2.loc[len(df_v2)] = ['Broccoli', 50]
df_v2.to_csv('data_versions/dataset_v2.csv', index=False)
```

`dataset_v1.csv` berisi 2 baris (Apple, Chicken), `dataset_v2.csv` berisi 3 baris (Apple, Chicken, Broccoli) — perubahan kecil ("micro change") tersimpan sebagai file terpisah, sehingga versi lama tetap bisa diakses kapan saja.

> ⚠️ **Catatan transparansi:** contoh di atas cuma simulasi konsep dengan menyimpan file terpisah manual — bukan cara kerja tool versioning sungguhan seperti DVC (yang pakai sistem hashing dan penyimpanan terpisah dari Git). Ini disederhanakan supaya intinya mudah dipahami tanpa perlu instalasi tambahan.

* * *

## 8\. Rangkuman & Langkah Selanjutnya

| Tahap | Yang Dipelajari |
| --- | --- |
| Missing Values | `dropna()` dan `fillna()` |
| Outlier | Deteksi dengan metode IQR + boxplot |
| Feature Engineering | Label Encoding & One Hot Encoding |
| Text Preprocessing | Bag of Words & TF-IDF |
| Dimensionality Reduction | PCA |
| Data Versioning | Konsep menyimpan riwayat versi dataset |

Langkah lanjutan yang bisa dicoba sendiri:

*   Ganti contoh data di atas dengan dataset sendiri.
    
*   Eksplorasi library `gensim` untuk mempelajari Word2Vec lebih lanjut.
    
*   Coba tools open-source seperti **DVC** untuk praktik data versioning yang sesungguhnya.
    

* * *

## 9\. Cheat Sheet: Checklist Preprocessing

*   \[ \] **Cek missing value dulu** dengan `.isnull().sum()` sebelum asumsi data kotor
    
*   \[ \] Data hilang sedikit & tidak krusial → `dropna()`; data berharga/hilang banyak → `fillna()` (mean/median/modus)
    
*   \[ \] Deteksi outlier dengan IQR, tapi **cek konteks bisnis** sebelum membuang — outlier statistik ≠ otomatis kesalahan data, dan outlier bisa muncul di dua sisi (atas maupun bawah)
    
*   \[ \] Kategori berurutan (rendah–sedang–tinggi) → Label Encoding; kategori tanpa urutan → One Hot Encoding
    
*   \[ \] Teks pendek/simpel → Bag of Words; butuh membedakan kata "khas" vs kata umum → TF-IDF
    
*   \[ \] Fitur terlalu banyak → pertimbangkan PCA, cek berapa persen informasi yang masih terjaga
    
*   \[ \] Simpan riwayat versi data (bukan cuma versi kode) — supaya bisa rollback kalau model tiba-tiba memburuk
    

* * *

## 10\. Quiz Check

**1\. Kenapa mengecek** `.isnull().sum()` **lebih dulu itu penting, sebelum langsung pakai dropna/fillna?**

Karena tidak semua dataset otomatis "kotor" — pada contoh dataset pelanggan di atas, hasilnya 0% missing value di semua kolom. Kalau langsung menjalankan strategi pembersihan tanpa cek dulu, bisa jadi kamu membuang waktu (atau lebih parah, membuang data yang sebenarnya baik-baik saja) untuk masalah yang tidak ada.

**2\. Kenapa gaji yang kosong diisi dengan median, bukan mean?**

Median lebih tahan (robust) terhadap nilai ekstrem/outlier dibanding mean. Kalau ada satu gaji yang jauh lebih besar dari yang lain, mean akan "tertarik" ke atas dan jadi kurang representatif, sedangkan median tetap stabil di tengah data.

**3\. Metode IQR mendeteksi 9 outlier padahal cuma 1 yang disisipkan sengaja, dan posisinya tersebar di dua sisi. Apa artinya?**

Metode statistik seperti IQR tidak tahu mana data yang "sengaja aneh" dan mana yang "kebetulan berada di ujung distribusi normal" — baik di sisi atas maupun bawah. Semua yang berada di luar rentang 1,5×IQR ditandai sama. Karena itu, hasil deteksi outlier tetap perlu diverifikasi dengan pemahaman konteks bisnis sebelum diputuskan dibuang atau dipertahankan.

**4\. Kapan sebaiknya menghindari Label Encoding untuk kategori seperti nama kota atau nama makanan?**

Ketika kategori itu tidak punya urutan/tingkatan alami. Label Encoding memberi angka berurutan (0, 1, 2, ...) yang secara tidak sengaja bisa "dibaca" model sebagai hubungan matematis (misalnya kategori bernomor 2 dianggap "lebih besar" dari kategori bernomor 1), padahal urutan itu cuma kebetulan alfabetis. One Hot Encoding lebih aman untuk kasus ini.

**5\. Kenapa bobot TF-IDF kata yang muncul di banyak dokumen justru lebih rendah?**

Karena tujuan TF-IDF adalah mengukur seberapa "khas" sebuah kata untuk membedakan satu dokumen dari dokumen lain. Kata yang muncul di banyak/semua dokumen (seperti "rumah" di kasus ini) tidak banyak membantu membedakan dokumen, jadi diberi bobot (IDF) lebih rendah. Kata yang jarang muncul justru lebih informatif untuk membedakan dokumen tempat ia muncul, sehingga diberi bobot lebih tinggi.

* * *

*Semua angka dan chart pada tulisan ini adalah hasil eksekusi kode secara langsung (bukan simulasi manual), dengan* `random seed` *yang dicantumkan pada bagian outlier supaya bisa direproduksi.*
