Broadcasting skalar

Interaksi array dengan skalar pada operasi aritmetika.

Daftar isi

Operasi antara array dan skalar adalah bentuk broadcasting yang paling sederhana dan paling sering digunakan. Pola seperti a + 10 sudah digunakan sejak modul dasar array, tetapi mekanisme yang membuat satu bilangan dapat dijumlahkan ke seluruh elemen belum dijelaskan. Topik ini membahas cara NumPy memperlakukan skalar pada operasi aritmetika, yaitu sebagai operand berdimensi nol, beserta alasan pola ini hemat memori.

Operasi aritmetika dengan skalar#

Menambahkan, mengalikan, atau memangkatkan array dengan satu bilangan diterapkan ke setiap elemen secara seragam. Seluruh operator aritmetika Python, yaitu +, -, *, /, //, %, dan **, berlaku antara array dan skalar dengan mekanisme yang sama. Tidak ada perulangan yang ditulis secara eksplisit, dan bentuk array tidak berubah.

python
import numpy as np

a = np.arange(5)
print("a      :", a)
print("a + 10 :", a + 10)
print("a * 2  :", a * 2)
print("a ** 2 :", a ** 2)
text Keluaran
a      : [0 1 2 3 4]
a + 10 : [10 11 12 13 14]
a * 2  : [0 2 4 6 8]
a ** 2 : [ 0  1  4  9 16]

Setiap elemen a mengalami operasi yang sama dengan bilangan 10 atau 2, dan keluarannya selalu array baru dengan bentuk yang sama. Skalar yang dilibatkan juga tidak harus bilangan bulat. Ketika array bilangan bulat bertemu skalar pecahan, tipe hasilnya naik ke tipe yang lebih umum agar bagian desimalnya tetap tersimpan.

python
import numpy as np

a = np.arange(5)
print("a + 10.5 :", a + 10.5)
print("a / 2    :", a / 2)
text Keluaran
a + 10.5 : [10.5 11.5 12.5 13.5 14.5]
a / 2    : [0.  0.5 1.  1.5 2. ]

Penjumlahan dengan 10.5 menghasilkan array float karena tipe asal a tidak dapat menampung bagian desimalnya. Pada array berdimensi dua, perilaku ini identik karena tidak ada dimensi yang perlu dicocokkan secara khusus.

Operator perbandingan mengikuti pola yang sama, yaitu skalar diterapkan ke setiap elemen dan menghasilkan array boolean.

python
import numpy as np

a = np.arange(5)
print("a > 2  :", a > 2)
print("a == 3 :", a == 3)
text Keluaran
a > 2  : [False False False  True  True]
a == 3 : [False False False  True False]

Setiap sel boolean menjawab pertanyaan perbandingan untuk elemen pada posisi yang sama. Array boolean seperti ini menjadi bahan utama penyaringan data, dan pola skalar pada perbandingannya persis sama dengan pola pada aritmetika.

python
import numpy as np

m = np.arange(6).reshape(2, 3)
print(m / 2)
text Keluaran
[[0.  0.5 1. ]
 [1.5 2.  2.5]]

Pembagian m / 2 menghasilkan array bertipe pecahan karena pembagian sejati (true division) selalu menghasilkan float, meskipun m berisi bilangan bulat. Skalar 2 tetap diterapkan pada keenam elemen tanpa terkecuali.

Skalar sebagai operand berdimensi nol#

Skalar dalam NumPy diperlakukan sebagai array berdimensi nol dengan bentuk (). Objek np.array(10) memperlihatkan bentuk ini secara eksplisit.

python
import numpy as np

skalar = np.array(10)
print("bentuk :", skalar.shape)
print("ndim   :", skalar.ndim)
print("10 + a :", 10 + np.arange(5))
text Keluaran
bentuk : ()
ndim   : 0
10 + a : [10 11 12 13 14]

Objek np.array(10) adalah array sungguhan yang bentuknya () dan berdimensi 0, sehingga setara dengan skalar Python pada operasi apa pun. Aturan penyelarasan melapisi bentuk () dengan dimensi bernilai 1 sebanyak yang diperlukan, sehingga bentuk nol dimensi kompatibel dengan bentuk apa pun. Fungsi np.broadcast_shapes memperlihatkan penyelarasan ini tanpa perlu membuat array sama sekali.

python
import numpy as np

print(np.broadcast_shapes((), (3, 4)))
print(np.broadcast_shapes((), (5,)))
text Keluaran
(3, 4)
(5,)

Bentuk () dilapisi menjadi (1, 1) ketika bertemu (3, 4), lalu kedua dimensi 1 diperluas menjadi 3 dan 4. Mekanismenya sama dengan penyelarasan pada topik aturan broadcasting, hanya kali ini tanpa dimensi yang ditulis secara eksplisit pada operandnya.

Perlu dibedakan dari array satu elemen dengan bentuk (1,). Bentuk (1,) adalah array satu dimensi yang kebetulan panjangnya satu, sedangkan () adalah array tanpa sumbu sama sekali. Keduanya tetap dapat disiarkan karena nilai 1 pada setiap dimensi selalu dapat diperluas.

python
import numpy as np

print(np.broadcast_shapes((1,), (3, 4)))
text Keluaran
(3, 4)

Bentuk (1,) dilapisi menjadi (1, 1) terlebih dahulu, lalu keduanya diperluas seperti skalar. Perbedaan () dan (1,) jarang terasa pada operasi tunggal, tetapi keduanya penting dibedakan saat bentuk antara diperiksa untuk melacak masalah.

Skalar tidak dijadikan array penuh#

NumPy tidak membangun array berisi nilai yang sama sebanyak elemen a sebelum menjumlahkan. Skalar Python tidak diubah menjadi array terlebih dahulu, melainkan dibaca ulang pada setiap elemen oleh kode internal ufunc, serupa dengan perilaku stride 0 yang digunakan np.broadcast_to. Perilaku inilah yang membuat a + 10 tetap efisien pada array besar, karena memori tambahan hanya digunakan untuk array keluaran.

Hasil yang sama dapat dibuat secara eksplisit dengan np.full, tetapi cara itu mengalokasikan array operand baru.

python
import numpy as np

a = np.arange(5)
penuh = np.full(5, 10)
print("a        :", a)
print("penuh    :", penuh)
print("a + penuh:", a + penuh)
text Keluaran
a        : [0 1 2 3 4]
penuh    : [10 10 10 10 10]
a + penuh: [10 11 12 13 14]

Keluarannya identik dengan a + 10, tetapi penuh menempati memori untuk lima elemen, sedangkan skalar hanya memerlukan satu nilai. Selisihnya terasa pada array besar, karena array penuh tumbuh sebanding dengan banyaknya elemen, sedangkan skalar tetap satu bilangan. Perbedaan ini murni perkara memori dan keterbacaan, karena hasil perhitungannya tidak pernah berbeda.

Kesalahan umum#

Kesalahan yang sering terjadi adalah menggabungkan operator penugasan in-place (in-place) dengan skalar pecahan pada array bilangan bulat. Operator += dianggap menulis ulang array asal, sehingga tipe datanya tidak boleh berubah.

python
import numpy as np

a = np.arange(5)
try:
    a += 0.5
except Exception as e:
    print(f"{type(e).__name__}: {e}")
text Keluaran
UFuncTypeError: Cannot cast ufunc 'add' output from dtype('float64') to dtype('int64') with casting rule 'same_kind'

Pesan tersebut menyatakan bahwa keluaran add bertipe float64 tidak dapat dituangkan kembali ke int64. Perbaikannya adalah menggunakan penjumlahan biasa, yaitu a = a + 0.5, yang membuat array baru bertipe float tanpa menimpa array asalnya. Alternatif lain adalah mengonversi array terlebih dahulu dengan a = a.astype(float) sebelum operator += digunakan.

Kesalahan kedua yang lazim adalah mengira operasi dengan skalar mengubah array asalnya. Operasi aritmetika selalu menghasilkan array baru, sedangkan array asal tetap utuh.

python
import numpy as np

a = np.arange(5)
b = a + 10
print("a tetap :", a)
print("b baru  :", b)
text Keluaran
a tetap : [0 1 2 3 4]
b baru  : [10 11 12 13 14]

Variabel b menampung hasil penjumlahan, sedangkan a tidak tersentuh. Apabila array asal memang ingin diperbarui, gunakan a += 10 dengan pertimbangan tipe data seperti pada kesalahan pertama di atas.

Ringkasan#

  • Skalar diperlakukan sebagai array berdimensi nol dengan bentuk (), sehingga kompatibel dengan bentuk array apa pun.
  • Operasi dengan skalar diterapkan ke setiap elemen tanpa mengubah bentuk array.
  • Skalar tidak dijadikan array penuh, sehingga memori tambahan hanya digunakan untuk array keluaran.

Referensi#