Data yang berhasil dimuat belum berarti data yang kita pahami. Eksplorasi data awal (exploratory data analysis, EDA) adalah tahap mengajukan pertanyaan terhadap data sebelum satu model pun dilatih: bagaimana strukturnya, sebaran nilainya, kualitasnya, dan hubungan antar kolomnya. Kesalahan yang paling sering terjadi adalah menjalankan EDA sebagai ritual grafik — membuat puluhan diagram tanpa pertanyaan, lalu tidak mengambil satu keputusan pun dari hasilnya. Pada chapter ini kita membangun kebiasaan yang berbeda: satu urutan pemeriksaan yang ringkas, setiap langkah menjawab pertanyaan spesifik, dan temuannya langsung menjadi keputusan untuk tahap persiapan data berikutnya.

Pola Pikir EDA: Berbasis Pertanyaan, Bukan Berbasis Grafik #

EDA yang produktif dimulai dari daftar pertanyaan, bukan dari daftar jenis grafik. Pertanyaan dasar yang berlaku untuk hampir semua data tabular hanya ada enam: seperti apa isinya, berapa besar dan bertipe apa strukturnya, bagaimana sebaran angkanya, apa saja isi kolom kategorikal, berapa banyak data yang hilang, dan kolom mana yang saling berhubungan. Setiap fungsi pandas yang kita pakai di chapter ini menjawab tepat satu pertanyaan tersebut. Grafik boleh menyusul untuk pertanyaan yang lebih spesifik — misalnya membandingkan sebaran gaji antar departemen — tetapi keputusan preprocessing seperti imputasi, penanganan pencilan, dan encoding sudah bisa diambil dari pemeriksaan tekstual di atas.

FungsiPertanyaan yang dijawab
head() / tail()Seperti apa tampilan baris awal dan akhir data?
info()Berapa baris dan kolom, tipe apa tiap kolom, berapa memori?
describe()Bagaimana sebaran kolom numerik (min, kuartil, max)?
value_counts()Apa saja isi kolom kategorikal dan seberapa sering muncul?
nunique()Berapa banyak nilai berbeda di tiap kolom?
isna().sum()Kolom mana yang kehilangan nilai dan berapa banyak?
corr(numeric_only=True)Kolom numerik mana yang berhubungan linier?

Kolom nunique() sering diabaikan padahal sangat menentukan: kolom dengan dua nilai unik adalah biner, puluhan nilai bisa jadi kategorikal, dan ribuan nilai unik pada kolom teks adalah sinyal kardinalitas tinggi yang perlu strategi encoding khusus — materi Chapter Encoding Variabel Kategorikal.

Membaca describe() lewat Kuartil #

Baris 25%, 50%, dan 75% pada describe() adalah kuartil: nilai batas yang membagi data terurut menjadi empat bagian sama banyak. Kuartil tengah (50%) adalah median — nilai tengah yang tidak tergeser oleh nilai ekstrem. Membandingkan mean dengan 50% memberi petunjuk kemiringan sebaran secara gratis: jika rata-rata jauh lebih besar daripada median, sebaran menumpuk di kiri dengan ekor panjang ke kanan — pola khas data pendapatan atau harga. Rentang antar-kuartil (75% dikurangi 25%) juga menjadi dasar aturan deteksi pencilan IQR yang akan kita pakai pada Chapter Menangani Outlier. Satu detail penting pada baris count: angka ini adalah jumlah nilai yang benar-benar tersedia, sehingga count yang lebih kecil dari jumlah baris langsung membocorkan adanya nilai hilang.

Contoh Kode: EDA Sistematis #

Contoh berikut membangun data 60 karyawan secara sintetis — kolom departemen, usia, masa_kerja, dan gaji_juta — lalu menjalankan tujuh pemeriksaan berurutan dari tabel di atas. Tiga nilai masa_kerja sengaja dibuat hilang agar tahap pemeriksaan kualitas benar-benar menemukan sesuatu.

python
# EDA sistematis pada data karyawan sintetis (60 baris, rng berseed 42).
# Urutan pemeriksaan: cuplikan -> struktur -> ringkasan -> kategori
# -> kualitas -> hubungan antar kolom numerik.

import numpy as np
import pandas as pd

rng = np.random.default_rng(42)

n = 60
departemen = rng.choice(["Keuangan", "Pemasaran", "Teknologi", "SDM"], size=n)
usia = rng.integers(low=22, high=56, size=n)
masa_kerja = rng.integers(low=0, high=15, size=n).astype(float)
gaji_juta = 6.0 + masa_kerja * 0.45 + rng.normal(0, 0.4, size=n)
gaji_juta = np.round(gaji_juta, 3)

# Tanam 3 nilai hilang pada posisi tetap (deterministik, bukan acak).
masa_kerja[[3, 17, 42]] = np.nan

df = pd.DataFrame(
    {
        "departemen": departemen,
        "usia": usia,
        "masa_kerja": masa_kerja,
        "gaji_juta": gaji_juta,
    }
)

print("== 1) Cuplikan data: head() ==")
print(df.head().to_string())

print("\n== 2) Struktur dan tipe: info() ==")
df.info()

print("\n== 3) Ringkasan kolom numerik: describe() ==")
print(df.describe().to_string())

print("\n== 4) Sebaran kolom kategorikal: value_counts() ==")
print(df["departemen"].value_counts().to_string())

print("\n== 5) Banyaknya nilai unik per kolom: nunique() ==")
print(df.nunique().to_string())

print("\n== 6) Kualitas data: isna().sum() ==")
print(df.isna().sum().to_string())

print("\n== 7) Korelasi antar kolom numerik: corr(numeric_only=True) ==")
print(df.corr(numeric_only=True).round(2).to_string())

Output yang Dihasilkan:

text
== 1) Cuplikan data: head() ==
  departemen  usia  masa_kerja  gaji_juta
0   Keuangan    53         6.0      8.892
1        SDM    47        10.0     10.679
2  Teknologi    34         9.0     10.316
3  Pemasaran    54         NaN      9.111
4  Pemasaran    35        12.0     11.231

== 2) Struktur dan tipe: info() ==
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 60 entries, 0 to 59
Data columns (total 4 columns):
 #   Column      Non-Null Count  Dtype
---  ------      --------------  -----
 0   departemen  60 non-null     object
 1   usia        60 non-null     int64
 2   masa_kerja  57 non-null     float64
 3   gaji_juta   60 non-null     float64
dtypes: float64(2), int64(1), object(1)
memory usage: 2.0+ KB

== 3) Ringkasan kolom numerik: describe() ==
            usia  masa_kerja  gaji_juta
count  60.000000   57.000000  60.000000
mean   39.000000    7.122807   9.084167
std     9.335553    4.092983   1.809263
min    22.000000    0.000000   5.775000
25%    31.000000    4.000000   7.828500
50%    38.500000    8.000000   9.102500
75%    48.000000   10.000000  10.396750
max    54.000000   14.000000  12.636000

== 4) Sebaran kolom kategorikal: value_counts() ==
departemen
SDM          17
Teknologi    16
Keuangan     14
Pemasaran    13

== 5) Banyaknya nilai unik per kolom: nunique() ==
departemen     4
usia          30
masa_kerja    15
gaji_juta     60

== 6) Kualitas data: isna().sum() ==
departemen    0
usia          0
masa_kerja    3
gaji_juta     0

== 7) Korelasi antar kolom numerik: corr(numeric_only=True) ==
            usia  masa_kerja  gaji_juta
usia        1.00        0.06       0.11
masa_kerja  0.06        1.00       0.98
gaji_juta   0.11        0.98       1.00

Bedah Kode Baris-per-Baris #

  1. Baris 8 (rng = np.random.default_rng(42)): seluruh data sintetis diambil dari satu generator berseed sehingga output contoh ini identik setiap dijalankan. Data 60 baris cukup kecil untuk dibaca penuh, tetapi cukup besar agar kuartil dan korelasi bermakna.
  2. Baris 11–15 (empat kolom): departemen diambil acak dari empat pilihan, usia dan masa_kerja dari rentang bilangan bulat, dan gaji_juta dibangun mengikuti rumus 6.0 + masa_kerja * 0.45 plus derau normal — relasi yang sengaja ditanam agar korelasi pada pemeriksaan ketujuh punya jawaban yang bisa diverifikasi.
  3. Baris 18 (masa_kerja[[3, 17, 42]] = np.nan): tiga baris tetap dijadikan nilai hilang. Posisinya ditulis eksplisit, bukan diacak, supaya contoh tetap deterministik.
  4. Baris 33 (df.info()): satu-satunya fungsi dalam urutan ini yang tidak perlu print() karena info() mencetak langsung. Perhatikan kolom Non-Null Count: masa_kerja hanya 57 dari 60.
  5. Baris 39 (value_counts()): menghitung frekuensi tiap kategori, terurut dari yang terbanyak — pemeriksaan cepat untuk keseimbangan kategori dan nilai liar seperti Jakrta vs Jakarta.
  6. Baris 48 (corr(numeric_only=True)): argumen numeric_only=True wajib pada pandas versi 2 ke atas agar kolom teks tidak memicu galat; hasilnya dibulatkan dua desimal agar tabel mudah dibaca.

Temuan dan Keputusan dari Output #

Output di atas mengandung beberapa temuan yang langsung menjadi keputusan. Pertama, isna().sum() menemukan tiga nilai masa_kerja hilang (5% dari baris) — cukup kecil untuk diimputasi, cukup besar untuk dihitung. Kedua, count pada describe() menunjukkan 57 untuk masa_kerja dan 60 untuk kolom lain, memastikan kehilangan data hanya di satu kolom. Ketiga, korelasi gaji_juta terhadap masa_kerja mencapai 0,98 sementara terhadap usia hanya 0,11 — gaji di data ini mengikuti masa kerja, bukan usia, sehingga usia bukan prediktor yang menjanjikan. Keempat, nunique() menunjukkan gaji_juta memiliki 60 nilai unik dari 60 baris — kolom numerik kontinu, bukan kategorikal — sedangkan departemen hanya memiliki 4 nilai yang seimbang antara 13 sampai 17 kemunculan, jadi tidak ada kategori langka yang perlu digabungkan.

Temuan tiga nilai masa_kerja yang hilang membawa kita ke keputusan preprocessing pertama yang sesungguhnya: bagaimana mengisi atau membuang data hilang tanpa membuat model berbohong. Materi tersebut dibahas tuntas pada Chapter Menangani Missing Values.

Referensi Resmi #