Data Cleaning Lanjutan: Menangani Outlier dan Inkonsistensi pada Dataset Real-World
Dataset dari dunia nyata hampir tidak pernah dalam kondisi siap analisis. Nilai ekstrem yang tidak wajar, format penulisan yang tidak seragam, hingga baris duplikat yang lolos dari sistem input adalah masalah klasik yang harus diselesaikan sebelum data digunakan untuk modeling.
Teknik dasar seperti dropna() dan fillna() menangani missing values, tetapi tidak cukup untuk menghadapi outlier dan inkonsistensi struktural. Outlier dapat mendistorsi distribusi dan menyebabkan korelasi antar fitur menjadi menyesatkan. Inkonsistensi format membuat operasi join gagal dan agregasi menghasilkan angka yang salah.
Mendeteksi Outlier dengan Metode Statistik
Outlier adalah nilai yang berada jauh dari mayoritas observasi dalam dataset. Dalam konteks statistik, kita perlu mendefinisikan "jauh" secara kuantitatif. Dua metode yang paling umum digunakan adalah Interquartile Range (IQR) dan Z-Score.
Metode IQR menghitung kuartil pertama (Q1) dan kuartil ketiga (Q3), lalu menetapkan batas: bawah = Q1 − 1.5 × IQR dan atas = Q3 + 1.5 × IQR. Nilai di luar rentang ini adalah outlier. IQR cocok untuk data skewed karena tidak mengasumsikan distribusi normal.
Metode Z-Score menghitung jarak setiap nilai dari mean dalam satuan standard deviation dengan threshold ±3. Metode ini lebih cocok untuk data yang mendekati distribusi normal.
import pandas as pd
import numpy as np
np.random.seed(42)
n = 1000
data_normal = np.random.normal(50, 10, n)
data_skewed = np.random.exponential(20, n)
outlier_idx = np.random.choice(n, 30, replace=False)
data_normal[outlier_idx] = np.random.uniform(90, 120, 30)
data_skewed[outlier_idx] = np.random.uniform(120, 200, 30)
df = pd.DataFrame({
'nilai_normal': data_normal,
'nilai_skewed': data_skewed
})
# Deteksi outlier dengan IQR
def detect_outliers_iqr(df, column):
Q1 = df[column].quantile(0.25)
Q3 = df[column].quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
outliers = df[(df[column] < lower) | (df[column] > upper)]
return len(outliers), lower, upper
# Deteksi outlier dengan Z-Score
def detect_outliers_zscore(df, column, threshold=3):
mean = df[column].mean()
std = df[column].std()
z_scores = np.abs((df[column] - mean) / std)
outliers = df[z_scores > threshold]
return len(outliers), mean, std
iqr_count, _, _ = detect_outliers_iqr(df, 'nilai_normal')
z_count, _, _ = detect_outliers_zscore(df, 'nilai_normal')
print(f"Outlier terdeteksi (IQR) pada data normal: {iqr_count}")
print(f"Outlier terdeteksi (Z-Score) pada data normal: {z_count}")
iqr_count_skew, _, _ = detect_outliers_iqr(df, 'nilai_skewed')
z_count_skew, _, _ = detect_outliers_zscore(df, 'nilai_skewed')
print(f"Outlier terdeteksi (IQR) pada data skewed: {iqr_count_skew}")
print(f"Outlier terdeteksi (Z-Score) pada data skewed: {z_count_skew}")Output:
Outlier terdeteksi (IQR) pada data normal: 36
Outlier terdeteksi (Z-Score) pada data normal: 30
Outlier terdeteksi (IQR) pada data skewed: 74
Outlier terdeteksi (Z-Score) pada data skewed: 32Perhatikan bahwa pada data skewed, IQR cenderung lebih akurat karena tidak terpengaruh oleh ekstremitas distribusi, sementara Z-Score bisa menghasilkan false positive karena mean dan standard deviation ikut terdistorsi oleh outlier itu sendiri.
Strategi Menangani Outlier tanpa Kehilangan Informasi
Setelah outlier terdeteksi, keputusan handling harus didasarkan pada konteks bisnis dan proporsi outlier.
Trimming menghapus seluruh baris yang mengandung outlier. Aman hanya jika outlier di bawah 5% dataset. Jika terlalu banyak baris dihapus, kita kehilangan informasi dari kolom lain yang valid.
Capping (Winsorizing) mengganti nilai outlier dengan nilai batas IQR. Ukuran dataset tetap utuh, hanya nilai ekstrem yang dimodifikasi.
Imputation dengan median cocok untuk data highly skewed karena median lebih robust terhadap outlier dibandingkan mean.
Data Science with Python
Master the art of data analysis, visualization, and predictive modeling.
def handle_outliers(df, column, method='cap'):
Q1 = df[column].quantile(0.25)
Q3 = df[column].quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
if method == 'trim':
return df[(df[column] >= lower) & (df[column] <= upper)]
elif method == 'cap':
df_clean = df.copy()
df_clean[column] = df_clean[column].clip(lower, upper)
return df_clean
elif method == 'median_impute':
df_clean = df.copy()
median_val = df_clean[column].median()
mask = (df_clean[column] < lower) | (df_clean[column] > upper)
df_clean.loc[mask, column] = median_val
return df_clean
# Bandingkan statistik sebelum dan sesudah handling
print("Statistik awal - data skewed:")
print(df['nilai_skewed'].describe())
df_clean = handle_outliers(df, 'nilai_skewed', method='cap')
print("\nStatistik setelah capping:")
print(df_clean['nilai_skewed'].describe())
df_imputed = handle_outliers(df, 'nilai_skewed', method='median_impute')
print("\nStatistik setelah median imputation:")
print(df_imputed['nilai_skewed'].describe())Output:
Statistik awal - data skewed:
count 1000.000000
mean 24.411178
std 30.906848
min 0.064469
25% 6.013567
50% 15.177003
75% 29.823633
max 197.654124
Name: nilai_skewed, dtype: float64
Statistik setelah capping:
count 1000.000000
mean 20.850458
std 18.698742
min 0.064469
25% 6.013567
50% 15.177003
75% 29.823633
max 65.538731
Name: nilai_skewed, dtype: float64
Statistik setelah median imputation:
count 1000.000000
mean 17.123690
std 13.791139
min 0.064469
25% 6.013567
50% 15.163642
75% 24.273540
max 64.166951
Name: nilai_skewed, dtype: float64Perhatikan perubahan pada nilai max dan standard deviation setelah handling. Capping menurunkan nilai maksimum secara signifikan sementara median imputation menjaga distribusi tetap mendekati aslinya. Pilih strategi berdasarkan kebutuhan analisis: jika nilai ekstrem dianggap noise, gunakan capping; jika outlier mungkin mengandung sinyal, pertimbangkan imputation.
Mengidentifikasi Inkonsistensi dan Duplikasi Data
Inkonsistensi data sering kali lebih sulit dideteksi dibandingkan outlier karena tidak bisa diidentifikasi hanya dengan statistik deskriptif. Masalah ini muncul dalam berbagai bentuk: duplikasi baris, format yang tidak seragam, hingga kontradiksi logis antar kolom.
Duplikasi eksak terjadi ketika baris yang sama persis muncul lebih dari satu kali. Ini bisa disebabkan oleh kegagalan sistem yang menginsert data dua kali atau proses ETL yang tidak memiliki mekanisme deduplikasi. Fungsi drop_duplicates() menangani kasus ini dengan mudah.
Duplikasi parsial lebih kompleks: ID yang sama muncul dengan nilai berbeda di kolom lain. Kita perlu menentukan baris mana yang valid — bisa berdasarkan timestamp terbaru, sumber data yang lebih terpercaya, atau aturan bisnis tertentu.
Inkonsistensi format adalah masalah klasik pada data yang dikumpulkan dari berbagai sumber. Tanggal bisa ditulis dalam format DD/MM/YYYY di satu sistem dan YYYY-MM-DD di sistem lain. Kategori bisa memiliki variasi penulisan seperti "Laki-laki", "Male", "L", atau "Pria" yang semuanya merujuk pada konsep yang sama.
# Dataset sintetis dengan inkonsistensi
data = {
'id': [1, 2, 2, 3, 4, 5, 6],
'nama': ['Andi', 'Budi', 'Budi', 'Citra', 'Dewi', 'Eko', 'Fajar'],
'tanggal_lahir': ['1990-05-12', '1988/11/23', '1988-11-23',
'1995-07-30', '1992-01-15', '1993/03/08', '1994-12-20'],
'jenis_kelamin': ['L', 'Male', 'Pria', 'P', 'Perempuan', 'Laki-laki', 'M'],
'gaji': [5000000, 7500000, 7500000, 6200000, 8100000, 4500000, 9200000],
'umur': [34, 36, 36, 29, 33, 31, 30]
}
df_raw = pd.DataFrame(data)
# 1. Deteksi duplikasi eksak
exact_dup = df_raw.duplicated().sum()
print(f"Duplikasi eksak: {exact_dup} baris")
# 2. Deteksi duplikasi parsial (ID sama, kolom lain berbeda)
partial_dup = df_raw[df_raw.duplicated(subset=['id'], keep=False)]
print(f"\nDuplikasi parsial (ID sama):")
print(partial_dup)
# 3. Deteksi inkonsistensi format tanggal
def is_iso_date(val):
try:
pd.to_datetime(val, format='%Y-%m-%d')
return True
except:
return False
inconsistent_dates = df_raw[~df_raw['tanggal_lahir'].apply(is_iso_date)]
print(f"\nTanggal dengan format tidak standar: {len(inconsistent_dates)} baris")
# 4. Standarisasi kategori jenis kelamin
gender_map = {
'L': 'Laki-laki', 'Male': 'Laki-laki', 'M': 'Laki-laki', 'Pria': 'Laki-laki',
'P': 'Perempuan', 'Female': 'Perempuan', 'W': 'Perempuan'
}
df_raw['jenis_kelamin_clean'] = df_raw['jenis_kelamin'].map(gender_map)
print(f"\nKategori unik sebelum standarisasi: {df_raw['jenis_kelamin'].unique()}")
print(f"Kategori unik setelah standarisasi: {df_raw['jenis_kelamin_clean'].unique()}")Output:
Duplikasi eksak: 0 baris
Duplikasi parsial (ID sama):
id nama tanggal_lahir jenis_kelamin gaji umur
1 2 Budi 1988/11/23 Male 7500000 36
2 2 Budi 1988-11-23 Pria 7500000 36
Tanggal dengan format tidak standar: 2 baris
Kategori unik sebelum standarisasi: ['L' 'Male' 'Pria' 'P' 'Perempuan' 'Laki-laki' 'M']
Kategori unik setelah standarisasi: ['Laki-laki' 'Perempuan' nan]Hasil dari kode di atas memberi kita gambaran lengkap tentang kondisi data: berapa banyak duplikasi yang perlu dibersihkan, format mana yang perlu distandarisasi, dan kolom mana yang memerlukan pemetaan kategori. Informasi ini menjadi dasar untuk membangun pipeline pembersihan yang sistematis.
Membangun Pipeline Pembersihan Data yang Reusable
Membersihkan data secara manual setiap kali menerima dataset baru adalah pekerjaan yang repetitif dan rentan kesalahan. Solusi yang lebih baik adalah membangun sebuah pipeline yang menggabungkan semua teknik deteksi dan handling ke dalam satu fungsi yang reusable.

Gambar: Diagram alur proses data science yang menunjukkan tahapan dari pengumpulan data, pembersihan (data cleaning), analisis eksplorasi, hingga menghasilkan data product — Sumber: [Wikimedia Commons](https://commons.wikimedia.org/wiki/File:Data_visualization_process_v1.png) — Lisensi: CC BY-SA 3.0
Pipeline yang baik memiliki struktur: deteksi → report → handling → validasi ulang. Setiap tahap menghasilkan log sehingga kita tahu persis apa yang berubah dan mengapa. Ini penting untuk audit trail, terutama ketika data digunakan untuk keputusan bisnis.
import pandas as pd
import numpy as np
from datetime import datetime
def clean_dataset(df, outlier_method='cap', id_column=None):
"""
Pipeline pembersihan data: deteksi outlier, duplikasi, dan inkonsistensi.
Mengembalikan DataFrame bersih dan report perubahan.
"""
report = {'initial_rows': len(df), 'actions': []}
df_clean = df.copy()
# Step 1: Handle duplikasi eksak
dup_count = df_clean.duplicated().sum()
if dup_count > 0:
df_clean = df_clean.drop_duplicates()
report['actions'].append(f'Menghapus {dup_count} baris duplikat eksak')
# Step 2: Handle duplikasi parsial (keep first occurrence)
if id_column and id_column in df_clean.columns:
dup_partial = df_clean.duplicated(subset=[id_column], keep=False).sum()
if dup_partial > 0:
df_clean = df_clean.drop_duplicates(subset=[id_column], keep='first')
report['actions'].append(
f'Menghapus duplikasi parsial pada kolom "{id_column}", menyimpan first occurrence'
)
# Step 3: Handle outlier pada semua kolom numerik
numeric_cols = df_clean.select_dtypes(include=[np.number]).columns
for col in numeric_cols:
Q1 = df_clean[col].quantile(0.25)
Q3 = df_clean[col].quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
outlier_mask = (df_clean[col] < lower) | (df_clean[col] > upper)
n_outliers = outlier_mask.sum()
if n_outliers > 0:
if outlier_method == 'cap':
df_clean[col] = df_clean[col].clip(lower, upper)
report['actions'].append(
f'Kolom "{col}": {n_outliers} outlier di-capping (batas: {lower:.2f} - {upper:.2f})'
)
elif outlier_method == 'trim':
df_clean = df_clean[~outlier_mask]
report['actions'].append(
f'Kolom "{col}": {n_outliers} outlier dihapus'
)
report['final_rows'] = len(df_clean)
report['rows_removed'] = report['initial_rows'] - report['final_rows']
return df_clean, report
# Demonstrasi pipeline
np.random.seed(123)
demo_data = pd.DataFrame({
'id': [1, 1, 2, 3, 4, 5, 6, 7, 8, 9],
'score': [85, 85, 92, 78, 88, 200, 91, 76, 500, 83],
'age': [25, 25, 30, 22, 28, 35, 27, 24, -5, 31]
})
df_result, cleaning_report = clean_dataset(demo_data, id_column='id')
print("=== Report Pembersihan Data ===")
for action in cleaning_report['actions']:
print(f" - {action}")
print(f"\nBaris awal: {cleaning_report['initial_rows']}")
print(f"Baris akhir: {cleaning_report['final_rows']}")
print(f"Baris dihapus: {cleaning_report['rows_removed']}")Output:
=== Report Pembersihan Data ===
- Menghapus 1 baris duplikat eksak
- Kolom "score": 2 outlier di-capping (batas: 69.50 - 105.50)
- Kolom "age": 1 outlier di-capping (batas: 15.00 - 39.00)
Baris awal: 10
Baris akhir: 9
Baris dihapus: 1Pipeline ini menerima DataFrame mentah dan mengembalikan DataFrame bersih beserta report yang mendokumentasikan setiap perubahan. Kita bisa langsung mengintegrasikan fungsi clean_dataset() ke dalam workflow analisis harian sehingga setiap dataset yang masuk otomatis melalui proses validasi yang konsisten.
Kemampuan membersihkan data secara sistematis adalah fondasi dari setiap proyek data science yang kredibel. Di Rumah Coding, kami mengajarkan teknik-teknik data preprocessing ini dalam program Data Science yang mencakup seluruh pipeline: dari data wrangling, exploratory analysis, hingga model deployment. Pelajari langsung dengan dataset real-world dan bangun portofolio analisis data yang siap industri.
Course Terkait
Data Science with Python
Master the art of data analysis, visualization, and predictive modeling.
E-commerce Sales Dashboard
- Data Cleaning Pipeline
- Interactive Charts
- Sales Forecasting Model
Deep Learning Bootcamp
A beginner-friendly, highly interactive bootcamp designed to take you from foundational concepts to deploying real-world Artificial Intelligence applications. Through a completely project-based approach, you will master the core of Deep Learning, Artificial Neural Networks, and Computer Vision using Python and TensorFlow, ultimately building a professional-grade AI web application for your portfolio.
GreenGuard: Intelligent Plant Disease Diagnosis Web App
- Interactive Image Upload UI: A clean, user-friendly interface built with Streamlit that supports drag-and-drop image uploads directly from a computer or mobile phone.
- Real-Time AI Inference: Utilizes a lightweight, optimized CNN model (like MobileNetV2) to process the image and return a diagnosis in seconds without heavy server load.
- Confidence Scoring Dashboard: Visually displays the model's prediction probability (e.g., "95% confident this is Tomato Late Blight") using interactive progress bars or charts.
LLM Bootcamp
This project-based bootcamp is designed for beginners to dive practically into the world of Large Language Models (LLMs). Through hands-on building, you will learn how to interact with top-tier AI APIs, master prompt engineering, orchestrate complex workflows using LangChain, and implement Retrieval-Augmented Generation (RAG) to query your own documents. By the end of this course, you will have the skills to build, test, and deploy a fully functional, custom AI web application.
Domain-Specific AI Knowledge Assistant
- Dynamic Document Processing: A sidebar interface allowing users to upload new PDF or TXT files, which the app automatically chunks, embeds, and stores in the vector database.
- Context-Aware Chat UI: A modern chat interface built with Streamlit that maintains conversation history, allowing users to ask follow-up questions naturally.
- Strict Guardrails (Anti-Hallucination): System instructions designed so the AI politely declines to answer questions that fall outside the context of the uploaded documents.