Perilaku dasar NaN di sekitar groupby sudah kita pelajari pada Chapter GroupBy dan Missing Values di section GroupBy dan Agregasi: kunci kelompok yang kosong dibuang secara bawaan (dropna=True), dan fungsi agregasi statistik melewati NaN sesuai skipna. Chapter ini melangkah ke sisi yang belum tersentuh: bagaimana mengisi kekosongan secara per kelompok tanpa nilai antarsensor saling bocor, dan bagaimana menuntut kelengkapan data pada saat agregasi. Semua latihan memakai data dua sensor gudang selama tujuh hari pertama Januari 2026.

Bocornya Nilai Antar Kelompok #

DataFrame berikut menyelingi pembacaan Sensor Utara dan Sensor Selatan per tanggal, sehingga urutan baris tidak lagi dikelompokkan rapi per sensor. Persoalannya muncul ketika kita memanggil ffill() langsung pada kolom:

python
"""Mengisi kekosongan per kelompok sensor dengan groupby ffill dan transform."""
import pandas as pd
import numpy as np

suhu_utara = [22.5, 23.1, None, 24.0, 24.8, None, 25.5]
suhu_selatan = [None, 23.9, 24.4, None, 25.0, 24.8, 25.1]
kelep_utara = [61.0, 62.0, None, 63.0, 60.0, None, 59.0]
kelep_selatan = [58.0, None, 57.0, None, 56.0, 58.0, 57.0]

gudang = pd.DataFrame(
    {
        "tanggal": [
            f"2026-01-{d:02d}" for d in range(1, 8) for _ in range(2)
        ],
        "sensor": ["Utara", "Selatan"] * 7,
        "suhu": [v for pasangan in zip(suhu_utara, suhu_selatan) for v in pasangan],
        "kelembapan": [
            v for pasangan in zip(kelep_utara, kelep_selatan) for v in pasangan
        ],
    }
)

print("Data dua sensor yang diselingi per tanggal:")
print(gudang)
print()

print("ffill() global, tanpa memandang kelompok:")
print(gudang["suhu"].ffill().tolist())
print()

print("groupby('sensor').ffill(), tiap sensor meneruskan nilainya sendiri:")
print(gudang.groupby("sensor")["suhu"].ffill())
print()

median_grup = gudang.groupby("sensor")["suhu"].transform("median")
print("Median suhu per sensor:", end=" ")
print(gudang.groupby("sensor")["suhu"].median().to_dict())
isi_median = gudang.assign(suhu=gudang["suhu"].fillna(median_grup))
print("Hasil fillna dengan median per kelompok:")
print(isi_median[["sensor", "suhu"]])
print()

ringkasan = gudang.groupby("sensor")["kelembapan"].agg(
    jumlah_terisi="count",
    total="sum",
    total_lengkap=lambda s: s.sum(min_count=6),
)
print("Agregasi kelembapan dengan min_count=6:")
print(ringkasan)

Output yang Dihasilkan:

text
Data dua sensor yang diselingi per tanggal:
       tanggal   sensor  suhu  kelembapan
0   2026-01-01    Utara  22.5        61.0
1   2026-01-01  Selatan   NaN        58.0
2   2026-01-02    Utara  23.1        62.0
3   2026-01-02  Selatan  23.9         NaN
4   2026-01-03    Utara   NaN         NaN
5   2026-01-03  Selatan  24.4        57.0
6   2026-01-04    Utara  24.0        63.0
7   2026-01-04  Selatan   NaN         NaN
8   2026-01-05    Utara  24.8        60.0
9   2026-01-05  Selatan  25.0        56.0
10  2026-01-06    Utara   NaN         NaN
11  2026-01-06  Selatan  24.8        58.0
12  2026-01-07    Utara  25.5        59.0
13  2026-01-07  Selatan  25.1        57.0

ffill() global, tanpa memandang kelompok:
[22.5, 22.5, 23.1, 23.9, 23.9, 24.4, 24.0, 24.0, 24.8, 25.0, 25.0, 24.8, 25.5, 25.1]

groupby('sensor').ffill(), tiap sensor meneruskan nilainya sendiri:
0     22.5
1      NaN
2     23.1
3     23.9
4     23.1
5     24.4
6     24.0
7     24.4
8     24.8
9     25.0
10    24.8
11    24.8
12    25.5
13    25.1
Name: suhu, dtype: float64

Median suhu per sensor: {'Selatan': 24.8, 'Utara': 24.0}
Hasil fillna dengan median per kelompok:
     sensor  suhu
0     Utara  22.5
1   Selatan  24.8
2     Utara  23.1
3   Selatan  23.9
4     Utara  24.0
5   Selatan  24.4
6     Utara  24.0
7   Selatan  24.8
8     Utara  24.8
9   Selatan  25.0
10    Utara  24.0
11  Selatan  24.8
12    Utara  25.5
13  Selatan  25.1

Agregasi kelembapan dengan min_count=6:
         jumlah_terisi  total  total_lengkap
sensor                                      
Selatan              5  286.0            NaN
Utara                5  305.0            NaN

Mari kita telaah dua pengisian pertama pada output:

  1. Baris 28 (gudang["suhu"].ffill()): pengisian global menyalin nilai baris mana pun di atasnya. Posisi kedua (Selatan, 1 Januari) terisi 22.5 — angka milik Sensor Utara; posisi kelima (Utara, 3 Januari) terisi 23.9 dari Sensor Selatan.
  2. Baris 32 (gudang.groupby("sensor")["suhu"].ffill()): groupby membatasi pengisian di dalam kelompoknya masing-masing. Selatan 3 Januari terisi 23.1, tetapi Selatan 1 Januari tetap NaN karena kelompok Selatan memang belum punya nilai sebelumnya.

Hasil groupby().ffill() kembali dalam urutan index asli, sehingga bisa langsung ditempelkan ke DataFrame. Pola ini penting setiap kali baris data tercampur antar kelompok, misalnya hasil penggabungan beberapa file sensor atau transaksi lintas cabang. Kelemahannya tetap ada: lubang di awal kelompok tidak terisi, dan lubang panjang tetap menyalin satu nilai lama.

Mengisi dengan Statistik Kelompok #

Alternatif yang tidak bergantung pada urutan baris adalah mengisi dengan statistik kelompok. Kombinasi groupby() dan transform("median") menghasilkan Series sepanjang data asli yang berisi median kelompok masing-masing baris:

  1. Baris 35 (transform("median")): median Sensor Utara 24.0 (dari 22.5, 23.1, 24.0, 24.8, 25.5) dan Sensor Selatan 24.8 (dari 23.9, 24.4, 25.0, 24.8, 25.1).
  2. Baris 38 (gudang["suhu"].fillna(median_grup)): setiap NaN diganti median kelompoknya sendiri; Selatan 1 Januari terisi 24.8, bukan 24.0 milik Utara.

Median kelompok lebih tahan terhadap outlier daripada mean, dan transform menjamin hasilnya sejajar dengan baris asli tanpa perlu merge. Bila suatu kelompok kehilangan hampir semua nilainya, statistik kelompok pun menjadi tidak bisa dipercaya — saat itu baris-barisnya lebih baik dibiarkan kosong atau dibuang.

Pengisian bukan satu-satunya keputusan; kadang kita justru ingin agregasi menolak angka yang dibangun dari data setengah jadi. Parameter min_count pada sum() menetapkan jumlah nilai terisi minimum agar hasilnya sah. Baris 43-47 pada skrip mengagregasi kelembapan dengan tiga ukuran sekaligus: count menghitung lima nilai terisi per sensor, sum tetap menghasilkan total 305.0 untuk Utara dan 286.0 untuk Selatan, sedangkan sum(min_count=6) menghasilkan NaN untuk keduanya karena tidak ada kelompok yang mencapai enam nilai terisi. Dengan begitu laporan otomatis menandai angka yang tidak layak dipakai, alih-alih menampilkannya seolah lengkap.

Rujukan lengkap perilaku groupby ada pada panduan GroupBy dan pandas.DataFrame.groupby ; perilaku NaN pada kunci kelompok tetap merujuk panduan Working with missing data .

Sampai di sini lubang missing sudah kita tangani dalam skala kolom maupun kelompok. Tantangan berikutnya berbeda sifat: nilai yang justru terisi tetapi salah, seperti pembacaan sensor yang melompat ke 41.0 derajat. Dua chapter berikutnya membekali kita dengan dua metode deteksi nilai ekstrem, dimulai dari metode IQR yang paling tahan banting.