Fungsi window

rank, lag, dan total berjalan lintas partisi.

Daftar isi

Fungsi window menghitung nilai dari sekumpulan baris yang berdekatan menurut definisi jendela, tanpa memadatkan baris-baris itu menjadi satu. Topik ini membahas cara mendefinisikan jendela dengan Window.partitionBy dan orderBy, pemakaian rank, row_number, lag, serta total berjalan dengan kerangka baris rowsBetween. Setelah membacanya, peringkat per kelompok, perbandingan dengan baris sebelumnya, dan akumulasi berjalan dapat dihitung dalam satu transformasi yang stabil.

Anatomi definisi window#

Sebuah window didefinisikan oleh dua keputusan, yaitu baris mana yang membentuk satu kelompok dan urutan mana yang dipakai di dalam kelompok itu. Metode Window.partitionBy("kelas") menetapkan kelompok, dan metode .orderBy(...) menetapkan urutan di dalamnya. Kombinasi keduanya menghasilkan objek window yang diterima fungsi-fungsi seperti F.rank() melalui .over(window).

Perbedaannya dengan groupBy terletak pada hasilnya. Agregasi menyusutkan setiap kelompok menjadi satu baris, sedangkan fungsi window mempertahankan setiap baris dan menambahkan kolom hasil perhitungan jendelanya. Satu baris data siswa tetap satu baris keluaran, dengan kolom tambahan berisi peringkat atau total berjalan. Karena alasan itu, fungsi window adalah alat baku untuk peringkat, perbandingan antar baris, dan akumulasi yang tidak menghilangkan detail baris.

Pengurutan di dalam window bukan sekadar tampilan. Urutan yang ditetapkan pada window menjadi dasar perhitungan rank, lag, dan kerangka baris, sedangkan urutan keluaran akhir tetap ditentukan orderBy di luar window. Dua pengurutan ini melayani keperluan yang berbeda dan keduanya perlu ditulis dengan sadar.

Secara fisik, pengelompokan partitionBy menuntut baris dengan kunci yang sama dikumpulkan ke partisi yang sama terlebih dahulu, sama seperti agregasi. Karena itu transformasi window juga memicu shuffle, dan jumlah partisi hasilnya mengikuti spark.sql.shuffle.partitions. Pada data contoh yang kecil, biaya ini tidak terasa, tetapi kebiasaan mengurangi jumlah partisi untuk contoh tetap diterapkan agar task-tasknya tidak kosong melompong.

rank, row_number, dan total berjalan#

Skrip berikut memberi peringkat siswa di dalam kelasnya, membandingkan rank dengan row_number pada nilai kembar, dan menghitung total berjalan nilai per kelas. Data sengaja memuat nilai kembar pada kedua kelas agar perbedaan kedua fungsi peringkat terlihat.

python analisis_window.py
from pyspark.sql import SparkSession, Window
from pyspark.sql import functions as F

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

data = [
    (1, "Ali", "10A", 85),
    (2, "Budi", "10A", 85),
    (3, "Citra", "10A", 78),
    (4, "Dewi", "10B", 92),
    (5, "Eko", "10B", 88),
    (6, "Farah", "10B", 88),
]
df = spark.createDataFrame(data, "id long, nama string, kelas string, nilai long")

w_peringkat = Window.partitionBy("kelas").orderBy(F.desc("nilai"))
w_berjalan = (
    Window.partitionBy("kelas")
    .orderBy(F.desc("nilai"), F.asc("id"))
    .rowsBetween(Window.unboundedPreceding, Window.currentRow)
)

hasil = (
    df.withColumn("peringkat", F.rank().over(w_peringkat))
    .withColumn("baris_ke", F.row_number().over(w_peringkat))
    .withColumn("total_berjalan", F.sum("nilai").over(w_berjalan))
)
print("=== rank, row_number, total berjalan ===")
hasil.orderBy("kelas", "peringkat", "id").show()

print("=== lag: nilai sebelumnya ===")
df.withColumn("nilai_sebelumnya", F.lag("nilai", 1).over(w_peringkat)) \
  .orderBy("kelas", F.desc("nilai"), "id").show()

spark.stop()
text Keluaran
WARNING: Using incubator modules: jdk.incubator.vector
=== rank, row_number, total berjalan ===
+---+-----+-----+-----+---------+--------+--------------+
| id| nama|kelas|nilai|peringkat|baris_ke|total_berjalan|
+---+-----+-----+-----+---------+--------+--------------+
|  1|  Ali|  10A|   85|        1|       1|            85|
|  2| Budi|  10A|   85|        1|       2|           170|
|  3|Citra|  10A|   78|        3|       3|           248|
|  4| Dewi|  10B|   92|        1|       1|            92|
|  5|  Eko|  10B|   88|        2|       2|           180|
|  6|Farah|  10B|   88|        2|       3|           268|
+---+-----+-----+-----+---------+--------+--------------+

=== lag: nilai sebelumnya ===
+---+-----+-----+-----+----------------+
| id| nama|kelas|nilai|nilai_sebelumnya|
+---+-----+-----+-----+----------------+
|  1|  Ali|  10A|   85|            NULL|
|  2| Budi|  10A|   85|              85|
|  3|Citra|  10A|   78|              85|
|  4| Dewi|  10B|   92|            NULL|
|  5|  Eko|  10B|   88|              92|
|  6|Farah|  10B|   88|              88|
+---+-----+-----+-----+----------------+

Kolom peringkat dan baris_ke memperlihatkan perbedaan perilaku pada nilai kembar. Nilai 85 milik Ali dan Budi sama-sama mendapat peringkat 1, lalu Citra mendapat peringkat 3 karena rank memberi nomor yang sama pada nilai kembar sebelum melompat, sedangkan row_number memberi nomor berurutan tanpa melompat sehingga menghasilkan 1, 2, dan 3. Pemilihan di antara keduanya mengikuti makna yang diinginkan, yaitu rank untuk peringkat yang berbagi tempat dan row_number untuk penomoran yang pasti unik.

Kolom total_berjalan menjumlahkan nilai dari awal kelompok sampai baris sekarang, terlihat dari 85, 170, dan 248 pada kelas 10A. Akumulasi ini dimungkinkan oleh .rowsBetween(Window.unboundedPreceding, Window.currentRow) yang memperluas jendela dari baris pertama sampai baris sekarang, dengan penentu tambahan F.asc("id") pada orderBy agar dua baris bernilai sama memiliki urutan yang pasti.

lag: melihat baris sebelumnya#

Fungsi F.lag(kolom, n) mengambil nilai kolom dari baris ke-n sebelum baris sekarang di dalam jendela, dan baris pertama kelompok mendapat NULL karena tidak ada pendahulunya. Pada keluaran di atas, baris Citra mengambil nilai 85 dari Budi yang terurut tepat sebelumnya, dan lawan fungsinya yaitu F.lead mengambil nilai dari baris sesudahnya dengan perilaku yang simetris.

Satu idiom window yang juga patut dikenal adalah jendela tanpa orderBy untuk fungsi agregasi. Definisi Window.partitionBy("kelas") yang langsung diterima F.avg("nilai") menghasilkan rata-rata kelas pada setiap baris kelas itu, karena kerangka bawaannya mencakup seluruh kelompok. Pola ini sering dipakai untuk kolom pembanding, misalnya selisih nilai siswa terhadap rata-rata kelasnya, yang dihitung dengan satu withColumn pengurangan tanpa join ke tabel agregat terpisah.

Pasangan nilai sekarang dan sebelumnya adalah bahan dasar banyak analisis, misalnya selisih nilai antar baris atau deteksi perubahan kategori. Selisihnya dapat dihitung langsung dengan F.col("nilai") - F.col("nilai_sebelumnya") pada withColumn berikutnya, tanpa perhitungan jendela tambahan.

Kesalahan umum#

Kesalahan pertama adalah lupa orderBy pada definisi window yang memakai fungsi yang bergantung pada urutan seperti rank atau lag. Tanpa urutan, makna “sebelum” dan “setelah” tidak terdefinisi, dan hasilnya berubah antar eksekusi. Definisi window untuk fungsi semacam itu wajib memuat .orderBy, dan kunci tambahan yang unik seperti id sebaiknya disertakan agar baris kembar terurut pasti.

Kesalahan kedua adalah mengira total berjalan dapat dihitung dengan groupBy("kelas").sum("nilai"). Agregasi menghasilkan satu baris per kelompok dan kehilangan detail baris, sedangkan kebutuhan topik ini adalah setiap baris membawa akumulasi sampai dirinya. Bentuk yang benar memakai rowsBetween(Window.unboundedPreceding, Window.currentRow) seperti pada skrip.

Kesalahan ketiga adalah mengabaikan nilai kembar pada orderBy window. Baris dengan kunci urutan yang sama dapat saling menukar posisi antar eksekusi, sehingga row_number dan lag ikut berubah. Tambahkan kunci penentu unik pada .orderBy window, bukan hanya pada orderBy keluaran.

Ringkasan#

  • Window didefinisikan dengan Window.partitionBy(...).orderBy(...), dan fungsi seperti rank, row_number, lag, atau sum diterapkan melalui .over(window) tanpa memadatkan baris.
  • rank berbagi nomor pada nilai kembar lalu melompat, sedangkan row_number memberi nomor berurutan yang pasti unik.
  • Total berjalan memerlukan kerangka baris rowsBetween(Window.unboundedPreceding, Window.currentRow), dan lag mengambil nilai baris sebelumnya di dalam kelompok.
  • Urutan pada window dan urutan keluaran adalah dua hal berbeda, dan keduanya perlu kunci penentu unik agar hasil tetap stabil antar eksekusi.

Referensi#