Broadcasting praktis

Normalisasi baris dan jarak berpasangan dengan broadcasting.

Daftar isi

Normalisasi per baris dan perhitungan jarak berpasangan adalah dua pola broadcasting yang paling sering muncul dalam pekerjaan data. Keduanya menggantikan perulangan bersarang dengan operasi array yang dieksekusi di kode internal NumPy. Keduanya juga dibangun dari gerakan yang sama, yaitu menyisipkan dimensi 1 pada posisi yang tepat, lalu membiarkan broadcasting menyebarkan nilainya. Topik ini membangun kedua pola tersebut pada data kecil berseed, mulai dari bentuk antara yang terlibat sampai keluaran akhirnya.

Normalisasi per baris#

Standardisasi (standardization) per baris mengurangi setiap elemen dengan rata-rata barisnya, lalu membaginya dengan simpangan baku baris yang sama. Notasi xˉi\bar{x}_i menyatakan rata-rata baris ke-i, sedangkan sis_i menyatakan simpangan baku baris yang sama.

zij=xij−xˉisi z_{ij} = \frac{x_{ij} - \bar{x}_i}{s_i}

Data contohnya adalah pengukuran tiga sensor suhu selama empat jam, yang dibuat dengan default_rng(42) agar keluarannya dapat direproduksi. Rata-rata dan simpangan dihitung per baris dengan axis=1 dan keepdims=True, sehingga hasil agregasinya tetap berbentuk (3, 1) dan siap disiarkan melawan (3, 4). Dengan matriks (m, n) berukuran apa pun, pola yang sama menghasilkan agregasi (m, 1), sehingga tidak ada angka yang perlu diubah saat ukuran data bertambah.

python
import numpy as np

rng = np.random.default_rng(42)
suhu = np.round(rng.normal(25, 3, size=(3, 4)), 1)
print("data:")
print(suhu)
rata = suhu.mean(axis=1, keepdims=True)
simpangan = suhu.std(axis=1, keepdims=True)
z = (suhu - rata) / simpangan
print("rata-rata per baris:")
print(np.round(rata, 2))
print("z-skor:")
print(np.round(z, 2))
text Keluaran
data:
[[25.9 21.9 27.3 27.8]
 [19.1 21.1 25.4 24.1]
 [24.9 22.4 27.6 27.3]]
rata-rata per baris:
[[25.72]
 [22.42]
 [25.55]]
z-skor:
[[ 0.08 -1.65  0.68  0.9 ]
 [-1.34 -0.54  1.2   0.68]
 [-0.31 -1.5   0.98  0.83]]

Setelah dibagi dengan simpangan baku, setiap baris z memiliki rata-rata 0 dan simpangan 1 sesuai tujuan standardisasi. Nilai 0.08 berarti pengukuran pertama sensor pertama berada sedikit di atas rata-rata barisnya, sedangkan -1.65 berada jauh di bawahnya. Rata-rata per barisnya, yaitu 25.72, 22.42, dan 25.55, sesuai dengan pembuatan data di sekitar angka 25. Simpangan yang digunakan adalah simpangan populasi, yaitu pembagi n secara bawaan NumPy, sehingga nilainya konsisten dengan fungsi std tanpa argumen tambahan. Standardisasi per baris bermanfaat apabila setiap baris mewakili sumber dengan skala berbeda, sehingga perbandingan antarbaris menjadi adil.

Alternatif penulisan tanpa keepdims adalah mengambil hasil agregasi lalu menyisipkan dimensi secara manual, yaitu suhu.mean(axis=1)[:, np.newaxis]. Keduanya menghasilkan bentuk (3, 1) yang sama, sehingga pilihannya hanya soal gaya. Yang tidak dapat diganti adalah bentuk agregasinya, karena (3,) tidak akan pernah cocok dengan dimensi terakhir (3, 4).

Jarak berpasangan antar titik#

Jarak antar setiap pasangan titik dihitung dengan rumus jarak Euclidean (Euclidean distance), yaitu akar dari jumlah kuadrat selisih koordinatnya.

d(pi,pj)=∑k=12(xik−xjk)2 d(p_i, p_j) = \sqrt{\sum_{k=1}^{2} \left(x_{ik} - x_{jk}\right)^2}

Empat titik pada bidang disimpan sebagai array (4, 2). Selisih antar titik dihitung dengan broadcasting tiga dimensi sekaligus, yaitu (4, 1, 2) dikurangi (1, 4, 2), sehingga menghasilkan array selisih berbentuk (4, 4, 2). Kuadrat selisihnya dijumlahkan pada sumbu terakhir, lalu diakarkan. Reduksinya menggunakan axis=-1, yaitu sumbu terakhir yang memuat pasangan koordinat, sehingga sumbu baris dan kolom matriks jarak tetap utuh. Hasil akhirnya adalah matriks jarak berbentuk (4, 4), yaitu satu sel untuk setiap pasangan titik.

python
import numpy as np

rng = np.random.default_rng(42)
titik = np.round(rng.uniform(0, 10, size=(4, 2)), 1)
print("titik:")
print(titik)
selisih = titik[:, np.newaxis, :] - titik[np.newaxis, :, :]
jarak = np.sqrt((selisih ** 2).sum(axis=-1))
print("matriks jarak:")
print(np.round(jarak, 2))
text Keluaran
titik:
[[7.7 4.4]
 [8.6 7. ]
 [0.9 9.8]
 [7.6 7.9]]
matriks jarak:
[[0.   2.75 8.68 3.5 ]
 [2.75 0.   8.19 1.35]
 [8.68 8.19 0.   6.96]
 [3.5  1.35 6.96 0.  ]]

Matriks jarak bersifat simetris dan diagonalnya nol, karena jarak satu titik dengan dirinya sendiri selalu nol. Nilai 2.75 pada posisi [0, 1] adalah jarak antara titik [7.7, 4.4] dan titik [8.6, 7.0]. Satu sel matriks dapat diverifikasi dengan menghitung ulang pasangannya secara manual.

python
import numpy as np

p = np.array([7.7, 4.4])
q = np.array([8.6, 7.0])
d = np.sqrt(((p - q) ** 2).sum())
print(np.round(d, 2))
text Keluaran
2.75

Hasilnya sama dengan nilai [0, 1] pada matriks jarak, sehingga pola tiga dimensinya dapat dipercaya. Verifikasi lain yang murah adalah memastikan simetrinya, misalnya dengan membandingkan matriks dengan transposnya. Verifikasi semacam ini layak dilakukan setiap kali pola broadcasting bertingkat disusun pertama kali. Perlu diperhatikan pula ukuran antaranya, karena array selisih tumbuh sebanding dengan kuadrat banyaknya titik, yaitu m * m * d elemen. Pada empat titik, array selisih hanya memuat 32 angka, sedangkan pada seribu titik array yang sama memuat dua juta angka.

Membandingkan dengan perulangan eksplisit#

Broadcasting sering lebih mudah dipahami apabila dibandingkan dengan versi perulangannya. Perbandingan ini juga menegaskan bahwa broadcasting bukan sekadar trik penulisan, melainkan cara menata perhitungan yang setara secara matematis. Mekanisme jarak berpasangan dapat dihitung ulang dengan dua perulangan bersarang pada tiga titik yang jaraknya mudah dihitung manual, yaitu (0, 0), (3, 4), dan (6, 8).

python
import numpy as np

titik = np.array([[0.0, 0.0], [3.0, 4.0], [6.0, 8.0]])
jarak = np.zeros((3, 3))
for i in range(3):
    for j in range(3):
        jarak[i, j] = np.sqrt(((titik[i] - titik[j]) ** 2).sum())
print(np.round(jarak, 2))
text Keluaran
[[ 0.  5. 10.]
 [ 5.  0.  5.]
 [10.  5.  0.]]

Versi perulangan mengalokasikan array kosong lebih dahulu, lalu mengisi setiap sel satu per satu. Versi broadcasting menyusun seluruh selisih sekaligus dan menjalankan reduksinya di kode internal NumPy. Kesetaraan kedua versi dapat dipastikan dengan np.allclose apabila keduanya dihitung pada data yang sama. Untuk tiga titik, perbedaannya tidak terasa, sedangkan untuk ribuan titik versi broadcasting jauh lebih ringkas dan lebih cepat. Jarak 5 antara (0, 0) dan (3, 4) dapat diverifikasi dengan teorema Pythagoras, sehingga kedua versi saling menguatkan. Perulangan dua tingkat seperti ini adalah bentuk yang paling sering digantikan broadcasting dalam pekerjaan data sehari-hari.

Ringkasan#

  • Standardisasi per baris memerlukan keepdims=True agar hasil agregasinya berbentuk (n, 1) dan dapat disiarkan melawan array aslinya.
  • Jarak berpasangan dibangun dari selisih (m, 1, d) - (1, m, d) yang disiarkan menjadi (m, m, d), lalu direduksi pada sumbu terakhir.
  • Kedua pola menggantikan perulangan bersarang dengan broadcasting, dan setiap bentuk antaranya dapat diperiksa dengan shape.

Referensi#