Data logger di lapangan tidak pernah sempurna. Ketika sensor suhu kehabisan daya atau koneksi ke pencatat terputus, banyak perangkat menulis angka penanda -999.0 sebagai pengganti pengukuran. Nilai sentinel seperti ini lolos dari pemeriksaan dtype karena berupa float sah, tetapi jika tidak ditangani, angka ekstrem itu menyeret seluruh statistik ke bawah.

Bagian ini menyimulasikan kegagalan tersebut pada dataset Bagian 1: Menyiapkan Data Cuaca: beberapa hari diberi nilai -999.0, disimpan ke data/cuaca_mentah.csv, lalu dimuat kembali persis seperti berkas riil. Kita mendeteksi sel rusak dengan mask boolean, mengganti sentinel menjadi nan, mengimputasi nilai hilang dengan rata-rata blok 30 hari, dan menutup dengan laporan kualitas data. Konsep nan sendiri sudah dibahas tuntas pada Chapter NaN dan Infinity; di sini kita memakainya dalam alur kerja sungguhan.

Menyimulasikan Kegagalan Sensor #

Kita merusak data secara terkendali: kolom suhu_rata gagal pada lima hari, kolom suhu_min gagal pada dua hari lainnya. Berkas hasilnya kita simpan lalu muat ulang agar skenarionya sama dengan data lapangan sesungguhnya:

python
import numpy as np

# Pembangun data cuaca 2025 (identik dengan Bagian 1).
rng = np.random.default_rng(42)
t = np.arange(365)
noise = rng.normal(0.0, 1.0, (365, 3)) * np.array([0.8, 0.6, 0.8])
musim = -np.cos(2 * np.pi * t / 365)
suhu_rata = 27.0 + 2.5 * musim + 0.001 * t + noise[:, 0]
suhu_min = suhu_rata - 3.5 + noise[:, 1]
suhu_max = suhu_rata + 4.0 + noise[:, 2]
curah_hujan = np.clip(rng.normal(5.0, 7.0, 365), 0.0, None)
curah_hujan *= 1 + 0.7 * np.cos(2 * np.pi * t / 365)
data = np.column_stack([suhu_rata, suhu_min, suhu_max, curah_hujan])

# Simulasi kegagalan sensor: nilai sentinel -999.
data_mentah = data.copy()
data_mentah[[45, 120, 176, 250, 310], 0] = -999.0
data_mentah[[176, 250], 1] = -999.0

np.savetxt("cuaca_mentah.csv", data_mentah, delimiter=",", fmt="%.2f",
           header="suhu_rata,suhu_min,suhu_max,curah_hujan", comments="")
muatan = np.loadtxt("cuaca_mentah.csv", delimiter=",", skiprows=1)
print("jumlah sel -999 per kolom:", np.sum(muatan == -999.0, axis=0))
print("indeks hari rusak:", np.where(muatan[:, 0] == -999.0)[0])

Output yang Dihasilkan:

text
jumlah sel -999 per kolom: [5 2 0 0]
indeks hari rusak: [ 45 120 176 250 310]

Poin penting dari blok ini:

  1. data.copy(): kita menggandakan data bersih terlebih dahulu (lihat Chapter View vs Copy) supaya data asli tetap utuh sebagai pembanding saat imputasi.
  2. data_mentah[[45, 120, ...], 0] = -999.0: fancy indexing dengan list indeks menugaskan satu nilai ke beberapa baris sekaligus pada kolom 0 — pola yang sama dipakai untuk kolom 1.
  3. np.sum(muatan == -999.0, axis=0): perbandingan menghasilkan mask boolean per sel, dan penjumlahannya sepanjang axis=0 menghitung berapa sel sentinel di tiap kolom: lima di suhu_rata, dua di suhu_min, nol di kolom lain.
  4. np.where(...)[0]: mengembalikan indeks hari yang rusak; indeks inilah yang nanti kita perbaiki.

Mengganti Sentinel dengan nan #

Angka -999.0 bukan pengukuran, maka langkah pertama adalah menandainya sebagai nilai hilang np.nan. Sebelum itu, mari kita lihat dahulu betapa merusaknya sentinel bila diabaikan:

python
import numpy as np

# Pembangun data 2025 + simulasi kegagalan sensor (sama seperti sebelumnya).
rng = np.random.default_rng(42)
t = np.arange(365)
noise = rng.normal(0.0, 1.0, (365, 3)) * np.array([0.8, 0.6, 0.8])
musim = -np.cos(2 * np.pi * t / 365)
suhu_rata = 27.0 + 2.5 * musim + 0.001 * t + noise[:, 0]
suhu_min = suhu_rata - 3.5 + noise[:, 1]
suhu_max = suhu_rata + 4.0 + noise[:, 2]
curah_hujan = np.clip(rng.normal(5.0, 7.0, 365), 0.0, None)
curah_hujan *= 1 + 0.7 * np.cos(2 * np.pi * t / 365)
data = np.column_stack([suhu_rata, suhu_min, suhu_max, curah_hujan])
data_mentah = data.copy()
data_mentah[[45, 120, 176, 250, 310], 0] = -999.0

suhu_rata_m = data_mentah[:, 0].copy()
print("mean tanpa penanganan :", round(suhu_rata_m.mean(), 2))
mask_rusak = suhu_rata_m == -999.0
suhu_rata_m[mask_rusak] = np.nan
print("mean setelah nan      :", round(suhu_rata_m.mean(), 2))
print("nanmean               :", round(np.nanmean(suhu_rata_m), 2))

Output yang Dihasilkan:

text
mean tanpa penanganan : 13.13
mean setelah nan      : nan
nanmean               : 27.19

Mari kita baca hasilnya:

  1. mean tanpa penanganan bernilai 13.13: lima nilai -999.0 menyeret rata-rata dari sekitar 27.2 °C turun ke 13.13 °C — kesimpulan “Kota Sukamaju sedang dingin-dinginya” lahir murni dari data rusak.
  2. suhu_rata_m[mask_rusak] = np.nan: boolean indexing mengganti seluruh sel sentinel dengan nan dalam satu baris; array float64 menampung nan tanpa konversi apa pun.
  3. mean setelah nan bernilai nan: satu nan saja menular ke seluruh agregasi — perilaku yang dibedah pada Chapter NaN dan Infinity.
  4. np.nanmean mengembalikan 27.19: agregasi NaN-aware menghitung rata-rata hanya dari sel valid, dan hasilnya sama persis dengan rata-rata data bersih (lihat Chapter NaN-aware Aggregation).

Imputasi Blok 30 Hari dan Laporan Kualitas #

Menghapus baris rusak membuang informasi cuaca kolom lain; mengganti dengan rata-rata tahunan menghapus musim. Kompromi yang masuk akal untuk data harian adalah imputasi blok: setiap nilai hilang diisi rata-rata blok 30 hari di sekitarnya, sehingga pola musimannya tetap terjaga:

python
import numpy as np

# Pembangun data 2025 + sentinel, sama seperti blok sebelumnya.
rng = np.random.default_rng(42)
t = np.arange(365)
noise = rng.normal(0.0, 1.0, (365, 3)) * np.array([0.8, 0.6, 0.8])
musim = -np.cos(2 * np.pi * t / 365)
suhu_rata = 27.0 + 2.5 * musim + 0.001 * t + noise[:, 0]
suhu_min = suhu_rata - 3.5 + noise[:, 1]
suhu_max = suhu_rata + 4.0 + noise[:, 2]
curah_hujan = np.clip(rng.normal(5.0, 7.0, 365), 0.0, None)
curah_hujan *= 1 + 0.7 * np.cos(2 * np.pi * t / 365)
data = np.column_stack([suhu_rata, suhu_min, suhu_max, curah_hujan])
data_mentah = data.copy()
data_mentah[[45, 120, 176, 250, 310], 0] = -999.0
data_mentah[[176, 250], 1] = -999.0

data_bersih = data_mentah.copy()
data_bersih[[45, 120, 176, 250, 310], 0] = np.nan
data_bersih[[176, 250], 1] = np.nan
for j in (0, 1):
    for i in np.where(np.isnan(data_bersih[:, j]))[0]:
        mulai_blok = (i // 30) * 30
        blok = data_bersih[mulai_blok:mulai_blok + 30, j]
        data_bersih[i, j] = np.nanmean(blok)
print("sisa nan:", np.isnan(data_bersih).sum())
print("imputasi kolom 0:", np.round(data_bersih[[45, 120, 310], 0], 2))
total = data_mentah.size
hilang = np.sum(data_mentah[:, :2] == -999.0)
print(f"kualitas data: {hilang} dari {total} sel ({hilang / total:.2%})")

Output yang Dihasilkan:

text
sisa nan: 0
imputasi kolom 0: [25.24 28.76 25.86]
kualitas data: 7 dari 1460 sel (0.48%)

Uraian langkah imputasi:

  1. Dua baris pertama penugasan: sentinel di kolom 0 dan 1 diganti nan pada indeks yang persis sama dengan hasil deteksi sebelumnya.
  2. for j in (0, 1) dan np.where(np.isnan(...)): kita menelusuri dua kolom suhu satu per satu; np.where memberi daftar indeks hari yang bernilai nan.
  3. mulai_blok = (i // 30) * 30: pembagian bilangan bulat memetakan hari ke blok 30 harinya — hari 45 jatuh di blok 30-59, hari 310 di blok 300-329.
  4. np.nanmean(blok): rata-rata blok dihitung mengabaikan sel nan lain yang mungkin ikut berada di blok tersebut, lalu hasilnya mengisi sel kosong.
  5. Laporan kualitas: tujuh sel hilang dari 1 460 sel (0.48%) — angka kecil, tetapi justru karena kecil, kesalahan penanganannya mudah lolos tanpa terlihat.

Imputasi suhu hari ke-45 menghasilkan 25.24 °C, masuk akal untuk blok akhir Februari; hari ke-120 (akhir April) menghasilkan 28.76 °C. Jika proyek menuntut penanganan yang lebih kaya — misalnya interpolasi antar tetangga atau kelas np.ma.MaskedArray — materi lanjurannya ada pada Chapter Masked Array dan Filtering Nilai Tidak Valid.

Data data_bersih inilah yang menjadi bahan Bagian 3: Menghitung Statistik. Dalam struktur proyek, seluruh blok bagian ini kita kemas sebagai src/bersihkan.py, lengkap dengan fungsi ganti_sentinel() dan imputasi_blok() yang bisa diuji ulang kapan saja.