Skip to main content

Command Palette

Search for a command to run...

Praktik Data Preprocessing dengan Python: dari Missing Value sampai PCA

Updated
16 min readView as Markdown
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

Daftar Isi


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.

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):

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:

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():

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:

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.

# 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:

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

Baru dihitung batasnya secara matematis dengan IQR:

# 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:

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:

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):

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:

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:

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.

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

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.

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.

More from this blog

S

Shaka's AI Journal

30 posts

A personal AI engineering journal — documenting hands-on learning in computer vision, deep learning, data pipelines, and model deployment. Study notes, working code, and honest write-ups from coursework and independent projects, published in Indonesian and English.