Analisis yang baik tidak dimulai dari mean, melainkan dari pemeriksaan. Data nyata hampir selalu datang dengan masalah kualitas hasil ekspor — baris tertulis dua kali, sel kosong, atau penulisan kategori yang tidak seragam — dan menghitung statistik dari data bermasalah hanya menghasilkan angka yang tampak meyakinkan padahal keliru. Bagian ini memuat berkas karyawan.csv dari PT Maju Bersama, memeriksa empat jenis masalah tersebut dengan alat bawaan pandas, lalu membersihkannya selangkah demi selangkah. Prinsip kerja kita sepanjang bagian ini: setiap langkah pembersihan harus diikuti angka yang membuktikan efeknya, sehingga pada akhir bagian kita memiliki dataset bersih 29 baris yang dapat dipertanggungjawabkan.

Dataset Karyawan PT Maju Bersama #

PT Maju Bersama adalah perusahaan fiktif yang baru saja mengekspor data kepegawaian dari sistem HR-nya ke format CSV. Berkas tersebut memuat 30 baris data dengan tujuh kolom berikut:

KolomArtiSatuan / jenis
idkode karyawanteks, format P001 sampai P029
namanama karyawanteks
departemenunit kerjateks: Produksi, Keuangan, Pemasaran, SDM
umurusia karyawantahun
masa_kerjalama bekerja di perusahaantahun
gajigaji bulananjuta Rp
skor_kinerjahasil penilaian kinerjaskala 0-100

Berkas lengkapnya tersedia di bawah ini — unduh karyawan.csv dan letakkan di folder kerja Anda, karena seluruh proyek dibangun di atas berkas ini:

csv karyawan.csv
id,nama,departemen,umur,masa_kerja,gaji,skor_kinerja
P001,Budi Santoso,Produksi,34,6,11.5,79
P002,Agus Wijaya,Produksi,28,3,9.4,74
P003,Joko Prasetyo,Produksi,41,12,14.6,81
P004,Tono Hartono,Produksi,25,1,8.0,70
P005,Eko Saputra,Produksi,38,9,13.5,83
P006,Hendra Gunawan,Produksi,31,5,10.8,77
P007,Udin Samsudin,Produksi,46,16,16.2,80
P008,Wahyu Nugroho,Produksi,52,20,18.8,85
P009,Yanto Kurniawan,Produksi,24,2,8.7,72
P010,Siti Rahmawati,Keuangan,35,8,12.0,76
P011,Dewi Lestari,Keuangan,29,4,9.8,73
P012,Rina Marlina,Keuangan,,6,11.0,75
P013,Fajar Ramadhan,Keuangan,42,14,15.0,78
P014,Indra Maulana,Keuangan,33,7,11.2,74
P015,Prakasa Wibowo,Keuangan,56,22,45.0,88
P016,Maya Anggraini,Pemasaran,30,5,12.3,86
P017,Rian Hidayat,Pemasaran,27,3,11.0,84
P018,Gita Permata,Pemasaran,36,10,14.8,89
P019,Nanda Pratama,Pemasaran,32,7,13.2,87
P020,Citra Kirana,pemasaran,26,2,10.2,82
P021,Bayu Setiawan,Pemasaran,39,13,16.4,90
P022,Laila Sari,Pemasaran,30,6,12.4,85
P023,Oka Mahendra,Pemasaran,44,18,18.8,91
P024,Tuti Handayani,SDM,37,9,11.7,71
P025,Yuni Astuti,SDM,28,3,8.5,68
P026,Putri Ayu,SDM,33,6,10.2,70
P027,Dedi Kurnia,SDM,40,11,12.4,
P028,Wawan Susanto,SDM,45,15,14.7,73
P029,Sari Wulandari,SDM,26,1,7.5,67
P017,Rian Hidayat,Pemasaran,27,3,11.0,84

Perhatikan dua hal dari tampilan berkas di atas. Pertama, jumlah baris datanya 30 — satu lebih banyak daripada jumlah kode karyawan unik P001 sampai P029. Kedua, dua sel terlihat kosong: umur pada baris P012 dan skor kinerja pada baris P027. Keduanya baru petunjuk kasat mata; kebenarannya akan kita konfirmasi lewat alat inspeksi yang memeriksa seluruh baris sekaligus, bukan lewat pengamatan manual yang tidak mungkin dilakukan pada data ribuan baris.

Memuat Data dengan read_csv #

Memuat CSV ke pandas hanya butuh satu baris. Jika Anda mengunduh berkasnya, jalankan df = pd.read_csv("karyawan.csv") dari folder yang sama dengan berkas tersebut — pola yang telah diperkenalkan pada chapter Analisis Data dengan pandas. Contoh-contoh pada laman ini memakai pola setara yang tidak bergantung lokasi berkas: isi CSV disematkan sebagai teks Python, lalu dibungkus io.StringIO menjadi objek serupa berkas di memori. read_csv memperlakukan objek itu sama persis seperti berkas di disk, sehingga hasil kedua cara identik.

python
import pandas as pd
import io

isi_csv = """id,nama,departemen,umur,masa_kerja,gaji,skor_kinerja
P001,Budi Santoso,Produksi,34,6,11.5,79
P002,Agus Wijaya,Produksi,28,3,9.4,74
P003,Joko Prasetyo,Produksi,41,12,14.6,81
P004,Tono Hartono,Produksi,25,1,8.0,70
P005,Eko Saputra,Produksi,38,9,13.5,83
P006,Hendra Gunawan,Produksi,31,5,10.8,77
P007,Udin Samsudin,Produksi,46,16,16.2,80
P008,Wahyu Nugroho,Produksi,52,20,18.8,85
P009,Yanto Kurniawan,Produksi,24,2,8.7,72
P010,Siti Rahmawati,Keuangan,35,8,12.0,76
P011,Dewi Lestari,Keuangan,29,4,9.8,73
P012,Rina Marlina,Keuangan,,6,11.0,75
P013,Fajar Ramadhan,Keuangan,42,14,15.0,78
P014,Indra Maulana,Keuangan,33,7,11.2,74
P015,Prakasa Wibowo,Keuangan,56,22,45.0,88
P016,Maya Anggraini,Pemasaran,30,5,12.3,86
P017,Rian Hidayat,Pemasaran,27,3,11.0,84
P018,Gita Permata,Pemasaran,36,10,14.8,89
P019,Nanda Pratama,Pemasaran,32,7,13.2,87
P020,Citra Kirana,pemasaran,26,2,10.2,82
P021,Bayu Setiawan,Pemasaran,39,13,16.4,90
P022,Laila Sari,Pemasaran,30,6,12.4,85
P023,Oka Mahendra,Pemasaran,44,18,18.8,91
P024,Tuti Handayani,SDM,37,9,11.7,71
P025,Yuni Astuti,SDM,28,3,8.5,68
P026,Putri Ayu,SDM,33,6,10.2,70
P027,Dedi Kurnia,SDM,40,11,12.4,
P028,Wawan Susanto,SDM,45,15,14.7,73
P029,Sari Wulandari,SDM,26,1,7.5,67
P017,Rian Hidayat,Pemasaran,27,3,11.0,84
"""

df = pd.read_csv(io.StringIO(isi_csv))

print("jumlah baris :", df.shape[0])
print("jumlah kolom :", df.shape[1])
print()
print(df.head())

Output yang Dihasilkan:

text
jumlah baris : 30
jumlah kolom : 7

     id           nama departemen  umur  masa_kerja  gaji  skor_kinerja
0  P001   Budi Santoso   Produksi  34.0           6  11.5          79.0
1  P002    Agus Wijaya   Produksi  28.0           3   9.4          74.0
2  P003  Joko Prasetyo   Produksi  41.0          12  14.6          81.0
3  P004   Tono Hartono   Produksi  25.0           1   8.0          70.0
4  P005    Eko Saputra   Produksi  38.0           9  13.5          83.0

Mari kita bedah hasilnya:

  1. df.shape: 30 baris dan 7 kolom — jumlah barisnya sesuai tampilan berkas.
  2. Kolom umur tampil sebagai 34.0 dan skor_kinerja sebagai 79.0: tanda kedua kolom itu dibaca sebagai float64, bukan int64. Penyebabnya sudah dikenal dari chapter pandas: satu sel kosong pada sebuah kolom mengubah seluruh kolom menjadi float, karena kolom integer tidak dapat memuat NaN.
  3. Lima baris pertama terlihat sehat: masalah yang tersimpan di baris lain — dan di kolom kategori — baru akan muncul setelah inspeksi yang lebih teliti.

Inspeksi Kesehatan Data #

Sebelum menghitung statistik apa pun, kita ajukan empat pertanyaan standar kualitas data: apakah tipe setiap kolom sesuai harapan, adakah sel kosong, adakah baris duplikat, dan apakah nilai kategori konsisten? pandas menyediakan satu alat untuk tiap pertanyaan: dtypes, isna().sum(), duplicated().sum(), dan unique().

python
import pandas as pd
import io

isi_csv = """id,nama,departemen,umur,masa_kerja,gaji,skor_kinerja
P001,Budi Santoso,Produksi,34,6,11.5,79
P002,Agus Wijaya,Produksi,28,3,9.4,74
P003,Joko Prasetyo,Produksi,41,12,14.6,81
P004,Tono Hartono,Produksi,25,1,8.0,70
P005,Eko Saputra,Produksi,38,9,13.5,83
P006,Hendra Gunawan,Produksi,31,5,10.8,77
P007,Udin Samsudin,Produksi,46,16,16.2,80
P008,Wahyu Nugroho,Produksi,52,20,18.8,85
P009,Yanto Kurniawan,Produksi,24,2,8.7,72
P010,Siti Rahmawati,Keuangan,35,8,12.0,76
P011,Dewi Lestari,Keuangan,29,4,9.8,73
P012,Rina Marlina,Keuangan,,6,11.0,75
P013,Fajar Ramadhan,Keuangan,42,14,15.0,78
P014,Indra Maulana,Keuangan,33,7,11.2,74
P015,Prakasa Wibowo,Keuangan,56,22,45.0,88
P016,Maya Anggraini,Pemasaran,30,5,12.3,86
P017,Rian Hidayat,Pemasaran,27,3,11.0,84
P018,Gita Permata,Pemasaran,36,10,14.8,89
P019,Nanda Pratama,Pemasaran,32,7,13.2,87
P020,Citra Kirana,pemasaran,26,2,10.2,82
P021,Bayu Setiawan,Pemasaran,39,13,16.4,90
P022,Laila Sari,Pemasaran,30,6,12.4,85
P023,Oka Mahendra,Pemasaran,44,18,18.8,91
P024,Tuti Handayani,SDM,37,9,11.7,71
P025,Yuni Astuti,SDM,28,3,8.5,68
P026,Putri Ayu,SDM,33,6,10.2,70
P027,Dedi Kurnia,SDM,40,11,12.4,
P028,Wawan Susanto,SDM,45,15,14.7,73
P029,Sari Wulandari,SDM,26,1,7.5,67
P017,Rian Hidayat,Pemasaran,27,3,11.0,84
"""

df = pd.read_csv(io.StringIO(isi_csv))

print(df.dtypes)
print()
print("sel kosong per kolom :")
print(df.isna().sum())
print()
print("baris duplikat       :", df.duplicated().sum())
print("kategori departemen  :", df["departemen"].unique().tolist())

Output yang Dihasilkan:

text
id               object
nama             object
departemen       object
umur            float64
masa_kerja        int64
gaji            float64
skor_kinerja    float64
dtype: object

sel kosong per kolom :
id              0
nama            0
departemen      0
umur            1
masa_kerja      0
gaji            0
skor_kinerja    1
dtype: int64

baris duplikat       : 1
kategori departemen  : ['Produksi', 'Keuangan', 'Pemasaran', 'pemasaran', 'SDM']

Empat temuan inspeksi:

  1. Tipe kolom: umur dan skor_kinerja menjadi float64 — konsekuensi sel kosong pada masing-masing kolom. masa_kerja tetap int64 karena lengkap, dan gaji memang float64 sejak awal karena nilainya pecahan.
  2. Sel kosong: tepat dua — umur sebanyak 1 (milik P012) dan skor_kinerja sebanyak 1 (milik P027); seluruh kolom lainnya lengkap.
  3. Baris duplikat: satu baris identik penuh dengan baris lain — P017 tertulis dua kali di bagian akhir berkas, pola khas ekspor yang menulis ulang satu baris.
  4. Kategori tidak konsisten: departemen memiliki lima nilai unik padahal seharusnya empat. Nilai pemasaran dengan huruf kecil dan Pemasaran dibaca sebagai dua kategori berbeda — bila dibiarkan, groupby pada Bagian 2 akan membagi karyawan Pemasaran menjadi dua kelompok semu dan menyembunyikan sebagian data dari agregasi.

Membersihkan Data Selangkah demi Selangkah #

Urutan pembersihan yang kita pakai: buang duplikat lebih dulu, lalu standarkan kategori, terakhir isi data hilang. Urutan ini bukan selera — pengisi nilai untuk sel kosong harus dihitung dari data yang sudah benar, sehingga duplikat dan kategori diselesaikan sebelum median dihitung. Setiap langkah pada blok berikut mencetak angka verifikasinya sendiri.

python
import pandas as pd
import io

isi_csv = """id,nama,departemen,umur,masa_kerja,gaji,skor_kinerja
P001,Budi Santoso,Produksi,34,6,11.5,79
P002,Agus Wijaya,Produksi,28,3,9.4,74
P003,Joko Prasetyo,Produksi,41,12,14.6,81
P004,Tono Hartono,Produksi,25,1,8.0,70
P005,Eko Saputra,Produksi,38,9,13.5,83
P006,Hendra Gunawan,Produksi,31,5,10.8,77
P007,Udin Samsudin,Produksi,46,16,16.2,80
P008,Wahyu Nugroho,Produksi,52,20,18.8,85
P009,Yanto Kurniawan,Produksi,24,2,8.7,72
P010,Siti Rahmawati,Keuangan,35,8,12.0,76
P011,Dewi Lestari,Keuangan,29,4,9.8,73
P012,Rina Marlina,Keuangan,,6,11.0,75
P013,Fajar Ramadhan,Keuangan,42,14,15.0,78
P014,Indra Maulana,Keuangan,33,7,11.2,74
P015,Prakasa Wibowo,Keuangan,56,22,45.0,88
P016,Maya Anggraini,Pemasaran,30,5,12.3,86
P017,Rian Hidayat,Pemasaran,27,3,11.0,84
P018,Gita Permata,Pemasaran,36,10,14.8,89
P019,Nanda Pratama,Pemasaran,32,7,13.2,87
P020,Citra Kirana,pemasaran,26,2,10.2,82
P021,Bayu Setiawan,Pemasaran,39,13,16.4,90
P022,Laila Sari,Pemasaran,30,6,12.4,85
P023,Oka Mahendra,Pemasaran,44,18,18.8,91
P024,Tuti Handayani,SDM,37,9,11.7,71
P025,Yuni Astuti,SDM,28,3,8.5,68
P026,Putri Ayu,SDM,33,6,10.2,70
P027,Dedi Kurnia,SDM,40,11,12.4,
P028,Wawan Susanto,SDM,45,15,14.7,73
P029,Sari Wulandari,SDM,26,1,7.5,67
P017,Rian Hidayat,Pemasaran,27,3,11.0,84
"""

df = pd.read_csv(io.StringIO(isi_csv))

print("baris sebelum pembersihan     :", len(df))
df = df.drop_duplicates()  # langkah 1: buang baris duplikat
print("baris setelah buang duplikat  :", len(df))
print()

kecil = df["departemen"].str.islower()  # langkah 2: temukan kategori huruf kecil
print("id dengan kategori huruf kecil :", df.loc[kecil, "id"].tolist())
df.loc[kecil, "departemen"] = df.loc[kecil, "departemen"].str.capitalize()
print("kategori setelah standarisasi  :", sorted(df["departemen"].unique()))
print()

med_umur = df["umur"].median()  # langkah 3: siapkan nilai pengisi sel kosong
df["umur"] = df["umur"].fillna(med_umur)
med_skor = df.loc[df["departemen"] == "SDM", "skor_kinerja"].median()
df["skor_kinerja"] = df["skor_kinerja"].fillna(med_skor)
print("umur P012 diisi median umur    :", med_umur)
print("skor P027 diisi median SDM     :", med_skor)
print()

print("sel kosong tersisa     :", int(df.isna().sum().sum()))
print("baris duplikat tersisa :", int(df.duplicated().sum()))
print()
print("ringkasan dataset bersih:")
print(df[["umur", "masa_kerja", "gaji", "skor_kinerja"]].describe().round(2))

Output yang Dihasilkan:

text
baris sebelum pembersihan     : 30
baris setelah buang duplikat  : 29

id dengan kategori huruf kecil : ['P020']
kategori setelah standarisasi  : ['Keuangan', 'Pemasaran', 'Produksi', 'SDM']

umur P012 diisi median umur    : 33.5
skor P027 diisi median SDM     : 70.0

sel kosong tersisa     : 0
baris duplikat tersisa : 0

ringkasan dataset bersih:
        umur  masa_kerja   gaji  skor_kinerja
count  29.00       29.00  29.00         29.00
mean   35.19        8.41  13.43         78.55
std     8.10        5.78   6.75          7.20
min    24.00        1.00   7.50         67.00
25%    29.00        4.00  10.20         73.00
50%    33.50        7.00  12.00         78.00
75%    40.00       12.00  14.70         85.00
max    56.00       22.00  45.00         91.00

Mari kita bedah tiap langkah:

  1. Buang duplikat: jumlah baris turun dari 30 ke 29. drop_duplicates mempertahankan kemunculan pertama dan membuang salinan kedua P017; seluruh kolomnya identik, jadi tidak ada informasi yang hilang.
  2. Standarkan kategori: str.islower() menghasilkan barisan benar/salah per baris — True hanya untuk nilai yang seluruhnya huruf kecil, yaitu P020. str.capitalize() kemudian diterapkan hanya pada baris itu, sehingga pemasaran menjadi Pemasaran dan nilai unik kembali menjadi empat kategori resmi.
  3. Isi umur P012 dengan median: 28 nilai umur yang terisi menghasilkan median 33.5 — jumlah data genap, sehingga median adalah rata-rata dua nilai tengah, aturan yang berasal dari chapter Median. Nilai 33.5 itulah yang mengisi sel kosong P012.
  4. Isi skor P027 dengan median departemen: skor SDM yang terdiri dari 67, 68, 70, 71, dan 73 memiliki median 70, dan nilai itulah yang mengisi P027. Kita memilih median per departemen — bukan median seluruh perusahaan yang bernilai 78 — karena Bagian 2 akan menunjukkan skor rata-rata antar departemen berbeda jauh; median global akan menempatkan nilai yang terlalu tinggi pada seorang karyawan SDM.
  5. Pemeriksaan akhir: sel kosong tersisa 0, duplikat tersisa 0, dan tabel ringkasan menghitung statistik dari 29 baris bersih. Gaji memperlihatkan mean 13.43 di atas median 12.00 — petunjuk awal sebaran yang condong ke kanan — dengan maksimum 45.00 yang jauh melampaui kuartil atas 14.70. Kedua petunjuk ini menjadi agenda Bagian 2.

Mengisi sel kosong adalah keputusan, bukan prosedur baku. Alternatifnya — membuang seluruh baris dengan dropna — akan menghapus enam kolom data yang sah pada P012 dan P027 hanya karena satu sel kosong di masing-masing baris, dan dataset menyusut menjadi 27 baris. Kita memilih mengisi karena kedua baris tetap menyumbang gaji dan masa kerja yang asli, dengan konsekuensi dua nilai (33.5 dan 70) berstatus perkiraan hasil isian. Trade-off dropna lawan fillna telah dibahas pada chapter Analisis Data dengan pandas, dan pada laporan akhir Bagian 4 kedua angka itu kita sebut eksplisit sebagai nilai isian, bukan hasil pengukuran.

Dataset bersih 29 baris kini menjadi acuan seluruh sisa proyek. Bagian 2 menghitung statistik, korelasi, dan outlier di atasnya — dan setiap contoh pada bagian berikutnya menyematkan dataset yang sama ini sebagai teks CSV.