Dua chapter sebelumnya melatih mendeteksi outlier dengan IQR dan z-score, tetapi mask True/False belum mengubah data apa pun. Setelah keputusan diambil bahwa sebuah nilai ekstrem tidak boleh tetap sebesar itu, alternatif selain membuang baris adalah membatasi nilainya. Method clip() melakukan pekerjaan itu: nilai di bawah batas bawah dinaikkan ke batas, nilai di atas batas atas diturunkan ke batas, dan barisnya tetap utuh. Chapter ini mempraktikkan dua gaya pembatasan: batas domain tetap dan winsorization berbasis persentil.

Clipping dengan Batas Domain #

Gudang menyimpan kopi hijau pada spesifikasi suhu 18-25 derajat Celsius. Ketika spesifikasi semacam ini menjadi acuan, clip() dengan batas tetap memaksa seluruh pembacaan masuk rentang tersebut:

python
"""Membatasi nilai ekstrem dengan clip(): batas domain dan winsorization persentil."""
import pandas as pd
import numpy as np

kalender = pd.date_range("2026-01-01", periods=14, freq="D")
suhu = pd.Series(
    [
        22.5, 23.1, np.nan, 24.0, 24.8, np.nan, np.nan,
        25.5, 41.0, 25.2, 12.0, 23.8, 24.1, np.nan,
    ],
    index=kalender,
    name="suhu",
)

terpotong = suhu.clip(lower=18, upper=25)
print("Suhu asli     :", suhu.tolist())
print("Suhu clip 18-25:", terpotong.tolist())
berubah = suhu.notna() & (terpotong != suhu)
print("Jumlah nilai terpotong :", int(berubah.sum()))
print("Mean sebelum :", round(suhu.mean(), 2), "-> sesudah:", round(terpotong.mean(), 2))
print()

kalender_k = pd.date_range("2026-01-01", periods=30, freq="D")
kelembapan = pd.Series(
    [
        61, 62, 60, 63, 59, 62, 61, 60, 64, 62,
        61, 63, 60, 59, 62, 61, 63, 60, 62, 61,
        89, 62, 60, 61, 63, 60, 59, 61, 62, 60,
    ],
    index=kalender_k,
    dtype="float64",
    name="kelembapan",
)

batas_bawah = kelembapan.quantile(0.05)
batas_atas = kelembapan.quantile(0.95)
menang = kelembapan.clip(lower=batas_bawah, upper=batas_atas)
print("Winsorization 5%/95%: batas =", round(batas_bawah, 2), "dan", round(batas_atas, 2))
print("Nilai yang dipotong :", kelembapan[menang != kelembapan].tolist())
print("Menjadi             :", menang[menang != kelembapan].tolist())
print("Jumlah nilai berubah:", int((menang != kelembapan).sum()))
print("Mean :", round(kelembapan.mean(), 2), "->", round(menang.mean(), 2))
print("Std  :", round(kelembapan.std(), 2), "->", round(menang.std(), 2))

Output yang Dihasilkan:

text
Suhu asli     : [22.5, 23.1, nan, 24.0, 24.8, nan, nan, 25.5, 41.0, 25.2, 12.0, 23.8, 24.1, nan]
Suhu clip 18-25: [22.5, 23.1, nan, 24.0, 24.8, nan, nan, 25.0, 25.0, 25.0, 18.0, 23.8, 24.1, nan]
Jumlah nilai terpotong : 4
Mean sebelum : 24.6 -> sesudah: 23.53

Winsorization 5%/95%: batas = 59.0 dan 63.55
Nilai yang dipotong : [64.0, 89.0]
Menjadi             : [63.55, 63.55]
Jumlah nilai berubah: 2
Mean : 62.1 -> 61.24
Std  : 5.25 -> 1.36

Mari kita telaah baris demi baris:

  1. Baris 15 (suhu.clip(lower=18, upper=25)): nilai 25.5, 41.0, dan 25.2 diturunkan menjadi 25.0, sedangkan 12.0 dinaikkan menjadi 18.0; keempat sel NaN tetap NaN karena clip() tidak menyentuh kekosongan.
  2. Baris 18-19 (hitung perubahan): suhu.notna() & (terpotong != suhu) menghitung empat nilai berubah; tanda notna() wajib ada karena NaN != NaN bernilai True dan akan menghitung kekosongan sebagai perubahan.
  3. Baris 20 (mean sebelum-sesudah): rata-rata bergeser dari 24.6 menjadi 23.53 — pembatasan satu sisi saja (atas) sudah menarik mean turun.

Perhatikan pelajaran penting dari output: 25.2 dan 25.5 ikut terpotong padahal keduanya bukan outlier. Batas domain adalah keputusan spesifikasi, bukan keputusan statistik — semua nilai di luar spesifikasi ditarik ke batas. Gunakan gaya ini hanya bila batasnya memang aturan keras, misalnya rentang penyimpanan yang disyaratkan, bukan sekadar nilai yang terlihat janggal.

Winsorization dengan Batas Persentil #

Winsorization membatasi nilai pada persentil data itu sendiri, lazimnya 5 persen dan 95 persen. Batasnya dihitung dengan quantile() — persentil adalah sisi lain dari kuantil yang sudah kita pakai pada metode IQR — lalu diberikan ke clip(). Bagian kedua skrip menerapkannya pada kelembapan 30 hari yang memuat kesalahan 89.0:

  1. Baris 35-36 (persentil 5%/95%): batas bawah 59.0 dan batas atas 63.55.
  2. Baris 37 (kelembapan.clip(...)): hanya dua nilai berubah — 64.0 dan 89.0 sama-sama menjadi 63.55; batas bawah 59.0 kebetulan sama dengan nilai minimum, sehingga tidak ada nilai bawah yang terpotong.
  3. Baris 42-43 (dampak): mean turun dari 62.1 ke 61.24, dan std anjlok dari 5.25 ke 1.36; outlier yang tadi menggelembungkan standard deviation kini tak berpengaruh.

Persentase 5/95 bukan aturan mati; 1/99 dan 10/90 juga umum dipakai, tergantung seberapa ekstrem data boleh dibenahi. Dokumentasinya ada pada pandas.Series.clip dan pandas.DataFrame.clip untuk menerapkan batas pada banyak kolom sekaligus; perhitungan persentilnya merujuk pandas.DataFrame.quantile .

clip() atau Buang Barisnya #

KeputusanPola kodeKonsekuensi
Winsorizations = s.clip(lower=b1, upper=b2)Jumlah baris tetap; ekor distribusi terpangkas
Buang bariss = s[(s >= b1) & (s <= b2)]Baris hilang dari semua kolom lain
Biarkans tanpa perubahanNilai sah bisa tetap dipertahankan bila bukan kesalahan

Winsorization cocok ketika barisnya masih bernilai — satu pembacaan sensor salah tidak menghapus tanggal, status, dan kelembapan pada baris yang sama. Membuang baris lebih tepat ketika seluruh baris tidak bisa dipercaya, misalnya duplikat atau data dari sensor mati. Dan jangan lupakan opsi ketiga: nilai 25.2 yang sedikit di atas spesifikasi bisa jadi kejadian nyata yang justru perlu dilaporkan.

Kini seluruh perangkat section ini sudah lengkap: mendeteksi pola missing, mengisi, interpolasi, mengelompokkan, mendeteksi outlier, dan membatasi nilai. Chapter penutup berikutnya menyusun semuanya menjadi satu kerangka keputusan yang bisa kita pegang setiap kali data kotor datang.