Nilai null dan pembersihan

coalesce, dropna, fillna, dan pola data kotor.

Daftar isi

Data nyata hampir selalu membawa nilai kosong, duplikasi, dan isian yang tidak lengkap, dan Spark menyediakan satu keluarga alat untuk menanganinya. Topik ini membahas cara null berperilaku pada operasi DataFrame, pembersihan dengan dropna dan fillna, penggantian nilai cadangan dengan coalesce, serta pembuangan baris duplikat dengan dropDuplicates. Setelah membacanya, pola data kotor yang umum dapat dikenali dan dibersihkan tanpa menghilangkan baris yang sebenarnya valid.

Perilaku null pada operasi DataFrame#

Null pada Spark menyatakan ketiadaan nilai, dan perbandingan apa pun terhadap null menghasilkan null, bukan benar atau salah. Akibatnya baris dengan null justru terbuang ketika filter menuntut kondisi benar, karena null tidak pernah bernilai benar. Kebiasaan yang sehat adalah memeriksa kolom dengan F.isNull() dan F.isNotNull() secara eksplisit ketika keberadaan nilai kosong ikut menentukan logika.

Perilaku ini berbeda dari pandas. Pada pandas, nilai kosong bertipe NaN masih mengikuti aritmetika khusus, misalnya NaN + 1 menghasilkan NaN tanpa memunculkan error, sementara sebagian operasi lain mengabaikannya secara senyap. Pada Spark, aritmetika yang melibatkan null menghasilkan null, dan agregasi seperti F.avg mengabaikan null sesuai semantik SQL. Peralihan dari pandas ke Spark karena itu menuntut pemeriksaan ulang logika yang menyentuh nilai kosong.

Data kotor sebagai bahan pembersihan#

Berkas sensor_kotor.csv di bundle topik ini memuat enam catatan sensor suhu dengan empat cacat sekaligus, yaitu lokasi yang kosong pada S2, suhu yang kosong pada S3, cadangan yang kosong pada S4, dan satu baris S3 yang terduplikasi persis.

csv sensor_kotor.csv
sensor,lokasi,suhu,suhu_cadangan
S1,ruang-server,24.5,24.0
S2,,23.8,23.8
S3,perpustakaan,,25.1
S4,ruang-guru,24.0,
S5,ruang-server,25.2,25.0
S3,perpustakaan,,25.1

Berkas dibaca dengan schema eksplisit agar tipe kolomnya tetap double, lalu dibersihkan secara berurutan. Skrip memperlihatkan kondisi awal terlebih dahulu, karena keputusan pembersihan selalu mengikuti bentuk cacat yang benar-benar ada.

python pembersihan_sensor.py
from pyspark.sql import SparkSession
from pyspark.sql import functions as F

spark = (
    SparkSession.builder
    .master("local[2]")
    .appName("pembersihan-sensor")
    .config("spark.sql.shuffle.partitions", "2")
    .getOrCreate()
)

df = spark.read.schema(
    "sensor string, lokasi string, suhu double, suhu_cadangan double"
).option("header", "true").csv("sensor_kotor.csv")

print("=== kondisi awal ===")
df.orderBy("sensor", "suhu").show()

print("=== dropna pada kolom suhu ===")
df.dropna(subset=["suhu"]).orderBy("sensor").show()

print("=== fillna per kolom ===")
df.fillna({"lokasi": "tak-diketahui", "suhu_cadangan": 0.0}).orderBy("sensor", "suhu").show()

print("=== coalesce: suhu utama, cadangan sebagai pengganti ===")
df.withColumn("suhu_final", F.coalesce("suhu", "suhu_cadangan")) \
  .orderBy("sensor").show()

print("=== dropDuplicates ===")
df.dropDuplicates().orderBy("sensor", "suhu").show()

spark.stop()
text Keluaran
WARNING: Using incubator modules: jdk.incubator.vector
=== kondisi awal ===
+------+------------+----+-------------+
|sensor|      lokasi|suhu|suhu_cadangan|
+------+------------+----+-------------+
|    S1|ruang-server|24.5|         24.0|
|    S2|        NULL|23.8|         23.8|
|    S3|perpustakaan|NULL|         25.1|
|    S3|perpustakaan|NULL|         25.1|
|    S4|  ruang-guru|24.0|         NULL|
|    S5|ruang-server|25.2|         25.0|
+------+------------+----+-------------+

=== dropna pada kolom suhu ===
+------+------------+----+-------------+
|sensor|      lokasi|suhu|suhu_cadangan|
+------+------------+----+-------------+
|    S1|ruang-server|24.5|         24.0|
|    S2|        NULL|23.8|         23.8|
|    S4|  ruang-guru|24.0|         NULL|
|    S5|ruang-server|25.2|         25.0|
+------+------------+----+-------------+

=== fillna per kolom ===
+------+-------------+----+-------------+
|sensor|       lokasi|suhu|suhu_cadangan|
+------+-------------+----+-------------+
|    S1| ruang-server|24.5|         24.0|
|    S2|tak-diketahui|23.8|         23.8|
|    S3| perpustakaan|NULL|         25.1|
|    S3| perpustakaan|NULL|         25.1|
|    S4|   ruang-guru|24.0|          0.0|
|    S5| ruang-server|25.2|         25.0|
+------+-------------+----+-------------+

=== coalesce: suhu utama, cadangan sebagai pengganti ===
+------+------------+----+-------------+----------+
|sensor|      lokasi|suhu|suhu_cadangan|suhu_final|
+------+------------+----+-------------+----------+
|    S1|ruang-server|24.5|         24.0|      24.5|
|    S2|        NULL|23.8|         23.8|      23.8|
|    S3|perpustakaan|NULL|         25.1|      25.1|
|    S3|perpustakaan|NULL|         25.1|      25.1|
|    S4|  ruang-guru|24.0|         NULL|      24.0|
|    S5|ruang-server|25.2|         25.0|      25.2|
+------+------------+----+-------------+----------+

=== dropDuplicates ===
+------+------------+----+-------------+
|sensor|      lokasi|suhu|suhu_cadangan|
+------+------------+----+-------------+
|    S1|ruang-server|24.5|         24.0|
|    S2|        NULL|23.8|         23.8|
|    S3|perpustakaan|NULL|         25.1|
|    S4|  ruang-guru|24.0|         NULL|
|    S5|ruang-server|25.2|         25.0|
+------+------------+----+-------------+

Keluaran memperlihatkan bahwa setiap alat bekerja pada cacat yang berbeda. dropna(subset=["suhu"]) membuang kedua baris S3 yang suhunya kosong, tetapi tetap mempertahankan S2 yang lokasinya kosong dan S4 yang cadangannya kosong, karena pembuangan dibatasi pada kolom yang disebut. Tanpa argumen subset, seluruh baris yang memuat null di kolom mana pun akan dibuang, sehingga pada data ini hanya S1 dan S5 yang tersisa.

fillna, coalesce, dan dropDuplicates#

fillna mengganti null dengan nilai pengganti, dan bentuk kamusnya mengganti hanya kolom yang disebut dengan nilai bertipe yang cocok. Pada keluaran, lokasi S2 terisi tak-diketahui dan suhu_cadangan S4 terisi 0.0, sedangkan suhu S3 tetap NULL karena kamus tidak menyebut kolom itu. Nilai pengganti harus dipilih dengan sadar, karena angka 0 pada kolom suhu berarti suhu nol derajat, bukan ketiadaan pengukuran, dan makna semacam itu dapat merusak analisis di hilir.

F.coalesce("suhu", "suhu_cadangan") mengambil nilai pertama yang tidak null di antara kolom-kolomnya, sehingga menghasilkan kolom suhu_final yang selalu terisi pada data ini. Pola ini tepat ketika dua kolom memuat pengukuran yang sama dari sumber berbeda, dan kolom cadangan dipakai hanya bila kolom utama kosong. Pola itu berbeda dari fillna, karena coalesce membaca kolom lain sebagai pengganti, bukan menanamkan angka konstan.

dropDuplicates() membuang baris yang seluruh kolomnya identik, dan pada data ini satu baris S3 ganda terbuang. Bentuk dropDuplicates(["sensor"]) menyisakan satu baris per sensor, dan baris pertama yang ditemukan dipertahankan, sehingga bentuk ini dipakai ketika satu entitas boleh muncul berkali-kali tetapi ringkasannya cukup satu baris. Untuk kasus itu, urutan baris yang dipertahankan perlu dikunci terlebih dahulu, misalnya melalui window dengan row_number, bila baris yang dipertahankan harus yang terbaru.

Parameter thresh pada dropna melengkapi kedua alat pembuang baris tersebut. Nilai thresh=3 berarti baris bertahan hanya bila memuat minimal tiga kolom yang tidak null, sehingga baris dengan satu-dua kolom kosong tetap dipertahankan sementara baris yang nyaris kosong terbuang. Parameter ini berguna pada data lebar yang null-nya tersebar, karena membuang baris hanya karena satu kolom kosong terlalu agresif, dan menanamkan nilai pengganti pada semua kolom justru menyamarkan kualitas sumber data.

Kesalahan umum#

Kesalahan pertama adalah memakai dropna() tanpa argumen pada data yang null-nya wajar. Satu kolom yang memang boleh kosong membuat seluruh baris terbuang, dan baris valid yang hilang menyusutkan data secara senyap. Selalu sebut subset dan pertimbangkan thresh, yaitu jumlah minimum kolom yang tidak null agar baris bertahan.

Kesalahan kedua adalah mengisi null bertipe angka dengan 0 melalui fillna tanpa memikirkan maknanya. Rata-rata, minimum, dan perhitungan lain kemudian tercampur dengan nol artifisial. Untuk suhu, nilai pengganti yang lebih tepat adalah rata-rata kolom atau penanda terpisah, atau coalesce ke kolom cadangan bila tersedia.

Kesalahan ketiga adalah memperlakukan null sebagai string kosong "". Keduanya berbeda pada Spark, sehingga df["lokasi"] == "" tidak menangkap null, dan fillna("") mengubah null menjadi teks kosong yang menyamarkan sumbernya. Periksa keduanya dengan F.isNull() dan perbandingan string kosong secara terpisah saat memetakan cacat data.

Ringkasan#

  • Null tidak menyamai nilai apa pun, sehingga penyaringan dan gabungan terhadap null perlu F.isNull() dan F.isNotNull(), dan semantiknya berbeda dari NaN milik pandas.
  • dropna(subset=[...]) membuang baris hanya berdasarkan kolom yang disebut, sedangkan fillna({...}) menanamkan nilai pengganti per kolom dengan tipe yang cocok.
  • F.coalesce(a, b) mengambil nilai yang tidak null pertama antar kolom dan menjadi pola baku untuk kolom utama dengan cadangan.
  • dropDuplicates() membuang baris identik penuh, dan bentuk dengan daftar kolom menyisakan satu baris per kunci dengan mempertahankan baris pertama.

Referensi#