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:
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:
[13500. 21600. 7200.]
[13500. 21600. 7200.]
TrueBedah baris demi baris:
- Baris 4 (
np.empty_like): array hasil dialokasikan lebih dulu — pola preallocation yang menjaga loop tetap hemat memori. - Baris 5-6: loop Python mengambil elemen satu per satu; setiap
harga[i]membuka objek scalar,* 0.9dievaluasi oleh interpreter, dan hasilnya ditulis kembali. - Baris 7 (
harga * 0.9): satu ekspresi menyuruh NumPy mengalikan seluruh elemen dalam satu pass di level C. - 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:
# 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:
# (ilustrasi — angka waktu berbeda di tiap komputer)
Loop Python : 9.18 ms
Vectorization : 0.0448 ms
Rasio : 205xMengapa Selisihnya Bisa Sebesar Itu #
Tiga biaya yang dibayar loop Python dan dihindari vectorization:
- Interpreting per elemen: interpreter membaca bytecode loop berulang-ulang; versi NumPy menerjemahkan ekspresi sekali, lalu loop-nya berjalan sebagai kode C yang sudah dikompilasi.
- Objek scalar Python: setiap elemen array yang disentuh loop berubah menjadi objek
np.float64lengkap dengan penghitungan referensinya; operasi vectorized bekerja langsung pada byte mentah. - 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:
# 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:
# (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 137xPada 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:
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:
[70 88 95]
[70, 88, 95]
True- Baris 4 (
nilai[nilai >= 70]): perbandingan menghasilkan mask boolean, lalu mask itu dipakai memilih elemen — dua operasi array, tanpa loop Python. - Baris 5: list comprehension mengiterasi array; setiap
xadalah scalar NumPy yang dibuka satu per satu. - Baris 8: hasil keduanya sama. Perbedaannya di sisi biaya: versi masking berjalan di level C dan mengembalikan salinan array (lihat Chapter Menghindari Copy Array).
| Aspek | Loop Python | Vectorization |
|---|---|---|
| Bahasa yang berjalan | interpreter Python | kode C bawaan NumPy |
| Objek per elemen | scalar Python baru | tidak ada |
| Penulisan | eksplisit per elemen | satu ekspresi |
| Skala data yang ideal | kecil, logika rumit | puluhan ribu elemen ke atas |
| Keterbacaan kondisi rumit | lebih mudah | butuh np.where atau mask |