Pengalaman umum pemula NumPy: kode yang ditulis dengan loop for berjalan beberapa ratus kali lebih lambat daripada versi satu baris yang dihitung sekaligus. Selisih ini bukan trik — ia lahir dari cara kerja interpreter Python dan pustaka C di balik NumPy. Istilah untuk gaya perhitungan sekali jalan itu adalah vectorization.

Bab ini membandingkan keduanya pada tugas yang sama, membuktikan hasilnya identik, mengukur selisih waktunya secara adil, lalu menjelaskan mengapa selisihnya bisa sebesar itu dan kapan ia membesar.

Tugas yang Sama, Dua Gaya #

Kita hitung diskon 10 persen untuk beberapa harga. Versi loop mengisi array hasil satu per satu; versi vectorized menuliskan operasinya sekali untuk seluruh array:

python
import numpy as np

harga = np.array([15000, 24000, 8000], dtype=np.float64)
diskon = np.empty_like(harga)
for i in range(harga.size):
    diskon[i] = harga[i] * 0.9
diskon_vektor = harga * 0.9
print(diskon)
print(diskon_vektor)
print(np.array_equal(diskon, diskon_vektor))

Output yang Dihasilkan:

text
[13500. 21600.  7200.]
[13500. 21600.  7200.]
True

Bedah baris demi baris:

  1. Baris 4 (np.empty_like): array hasil dialokasikan lebih dulu — pola preallocation yang menjaga loop tetap hemat memori.
  2. Baris 5-6: loop Python mengambil elemen satu per satu; setiap harga[i] membuka objek scalar, * 0.9 dievaluasi oleh interpreter, dan hasilnya ditulis kembali.
  3. Baris 7 (harga * 0.9): satu ekspresi menyuruh NumPy mengalikan seluruh elemen dalam satu pass di level C.
  4. Baris 10 (np.array_equal): hasil kedua gaya identik bit demi bit — vectorization mengubah gaya penulisan, bukan hasilnya.

Mengukur Selisihnya #

Sekarang tugas yang sama diukur dengan timeit pada 100.000 harga:

python
# notest
import numpy as np
import timeit

harga = np.arange(100_000, dtype=np.float64) + 1.0

def dengan_loop():
    total = 0.0
    for nilai in harga:
        total += nilai * 1.1
    return total

def dengan_vektor():
    return (harga * 1.1).sum()

t_loop = min(timeit.repeat(dengan_loop, number=1, repeat=5))
t_vek = min(timeit.repeat(dengan_vektor, number=100, repeat=5)) / 100
print(f"Loop Python   : {t_loop * 1000:.2f} ms")
print(f"Vectorization : {t_vek * 1000:.4f} ms")
print(f"Rasio         : {t_loop / t_vek:.0f}x")

Output yang Dihasilkan:

text
# (ilustrasi — angka waktu berbeda di tiap komputer)
Loop Python   : 9.18 ms
Vectorization : 0.0448 ms
Rasio         : 205x

Mengapa Selisihnya Bisa Sebesar Itu #

Tiga biaya yang dibayar loop Python dan dihindari vectorization:

  1. Interpreting per elemen: interpreter membaca bytecode loop berulang-ulang; versi NumPy menerjemahkan ekspresi sekali, lalu loop-nya berjalan sebagai kode C yang sudah dikompilasi.
  2. Objek scalar Python: setiap elemen array yang disentuh loop berubah menjadi objek np.float64 lengkap dengan penghitungan referensinya; operasi vectorized bekerja langsung pada byte mentah.
  3. Pemeriksaan di tiap langkah: loop manual memeriksa batas indeks dan tipe pada setiap iterasi; ufunc NumPy memeriksa sekali di awal lalu menyapu memori secara berurutan.

Operasi seperti harga * 1.1 dikerjakan oleh ufunc (universal function) — mekanisme yang dibedah pada Chapter Fungsi Universal (ufunc).

Semakin besar data, semakin dominan manfaat vectorization:

python
# notest
import numpy as np
import timeit

for n in (1_000, 100_000):
    x = np.arange(n, dtype=np.float64)
    t_loop = min(timeit.repeat(lambda: sum(x.tolist()), number=1, repeat=3))
    t_vek = min(timeit.repeat(lambda: x.sum(), number=100, repeat=3)) / 100
    print(f"n={n:>7,}: loop {t_loop * 1000:8.3f} ms | vektor {t_vek * 1000:8.5f} ms | rasio {t_loop / t_vek:6.0f}x")

Output yang Dihasilkan:

text
# (ilustrasi — angka waktu berbeda di tiap komputer)
n=  1,000: loop    0.023 ms | vektor  0.00101 ms | rasio     23x
n=100,000: loop    2.451 ms | vektor  0.01787 ms | rasio    137x

Pada seribu elemen, biaya tetap memanggil fungsi masih terasa sehingga rasinya “hanya” dua puluhan; pada seratus ribu elemen, biaya per elemen Python mendominasi dan rasio membesar. Kesimpulannya bukan “loop dilarang”, melainkan: jangan memindahkan pekerjaan per elemen ke Python bila ada ufunc atau operasi array yang setara.

Boolean Masking vs Filtering Manual #

Pola yang sama berlaku untuk menyaring data. Filtering dengan list comprehension mengeksekusi kondisi di level Python, sedangkan mask boolean dikerjakan NumPy sekaligus:

python
import numpy as np

nilai = np.array([70, 45, 88, 60, 95, 55])
lulus_np = nilai[nilai >= 70]
lulus_py = [int(x) for x in nilai if x >= 70]
print(lulus_np)
print(lulus_py)
print(np.array_equal(lulus_np, np.array(lulus_py)))

Output yang Dihasilkan:

text
[70 88 95]
[70, 88, 95]
True
  1. Baris 4 (nilai[nilai >= 70]): perbandingan menghasilkan mask boolean, lalu mask itu dipakai memilih elemen — dua operasi array, tanpa loop Python.
  2. Baris 5: list comprehension mengiterasi array; setiap x adalah scalar NumPy yang dibuka satu per satu.
  3. Baris 8: hasil keduanya sama. Perbedaannya di sisi biaya: versi masking berjalan di level C dan mengembalikan salinan array (lihat Chapter Menghindari Copy Array).
AspekLoop PythonVectorization
Bahasa yang berjalaninterpreter Pythonkode C bawaan NumPy
Objek per elemenscalar Python barutidak ada
Penulisaneksplisit per elemensatu ekspresi
Skala data yang idealkecil, logika rumitpuluhan ribu elemen ke atas
Keterbacaan kondisi rumitlebih mudahbutuh np.where atau mask