Optimasi tanpa pengukuran hanyalah tebakan. Setelah Chapter-Chapter sebelumnya menunjukkan pola yang cepat dan pola yang lambat, Chapter ini membahas cara mengukurnya dengan benar: magic %timeit untuk lingkungan Jupyter, modul timeit untuk skrip, dan time.perf_counter() sebagai stopwatch resolusi tinggi. Satu hal yang sengaja kita hindari di buku ini: mencetak angka durasi mentah. Durasi berubah-ubah antar mesin, antar beban CPU, bahkan antar eksekusi di mesin yang sama — jadi skrip di bawah mengukur sungguhan, tetapi mencetak struktur hasil yang deterministik: jenis nilai, kebenaran hasil, jumlah sampel, dan jumlah pemanggilan.

Tiga Alat Ukur, Tiga Lingkungan #

AlatLingkunganBentuk hasilCatatan
%timeit / %timeJupyter / IPythonringkasan statistik di seltidak berjalan di skrip .py
timeit.repeat()skriplist berisi durasi per sampelpilih min() sebagai estimasi terbaik
time.perf_counter()skripsatu durasi per pengukuranstopwatch umum, resolusi tinggi
cProfileskriplaporan durasi per fungsiuntuk mencari titik lambat di program besar

Mengukur Loop vs Vectorized #

Skrip berikut mengukur dua fungsi pada 100.000 baris — satu dengan loop itertuples, satu vectorized — lalu mencetak struktur hasilnya, bukan angka durasinya:

python
import time
import timeit

import numpy as np
import pandas as pd

rng = np.random.default_rng(42)
n = 100_000
df = pd.DataFrame(
    {
        "jumlah": rng.integers(1, 11, size=n),
        "harga": rng.choice([15000, 25000, 40000, 55000, 70000], size=n),
    }
)


# 1. Pola pengukuran dengan time.perf_counter()
def ukur(fungsi):
    mulai = time.perf_counter()
    hasil = fungsi()
    durasi = time.perf_counter() - mulai
    return hasil, durasi


def hitung_loop():
    hasil = []
    for baris in df.itertuples():
        hasil.append(baris.jumlah * baris.harga)
    return hasil


def hitung_vectorized():
    return (df["jumlah"] * df["harga"]).to_numpy()


hasil_loop, durasi_loop = ukur(hitung_loop)
hasil_vec, durasi_vec = ukur(hitung_vectorized)
print("perf_counter mengembalikan durasi float:", isinstance(durasi_loop, float))
print("kedua durasi positif:", durasi_loop > 0, durasi_vec > 0)
print("hasil loop == hasil vectorized:", np.array_equal(hasil_loop, hasil_vec))
print()

# 2. timeit.repeat: beberapa sampel pengulangan
waktu_vec = timeit.repeat(hitung_vectorized, number=5, repeat=3)
waktu_loop = timeit.repeat(hitung_loop, number=1, repeat=3)
print("timeit.repeat(number=5, repeat=3) menghasilkan:", len(waktu_vec), "nilai")
print("semua nilai > 0:", all(w > 0 for w in waktu_vec))
print("praktik: bandingkan min() antar pola, bukan satu nilai tunggal")
print("jumlah baris yang diukur:", f"{n:,}")
print()

# 3. Biaya yang terukur secara deterministik: jumlah pemanggilan
print("pemanggilan Python versi loop       :", f"{n:,}")
print("pemanggilan Python versi vectorized : 0")

Output yang Dihasilkan:

text
perf_counter mengembalikan durasi float: True
kedua durasi positif: True True
hasil loop == hasil vectorized: True

timeit.repeat(number=5, repeat=3) menghasilkan: 3 nilai
semua nilai > 0: True
praktik: bandingkan min() antar pola, bukan satu nilai tunggal
jumlah baris yang diukur: 100,000

pemanggilan Python versi loop       : 100,000
pemanggilan Python versi vectorized : 0

Mari kita telaah baris-baris pentingnya:

  1. Baris 18-22 (helper ukur()): fungsi ini membungkus pola perf_counter — catat waktu sebelum, jalankan fungsi, catat waktu sesudah, kembalikan hasil beserta durasinya. Pemisahan hasil dan durasi memudahkan pemeriksaan kebenaran di kemudian hari.
  2. Baris 25-33 (dua kandidat): hitung_loop() mengalikan per baris lewat itertuples, sedangkan hitung_vectorized() mengalikan dua Series dalam satu pemanggilan lalu mengubahnya ke array agar perbandingannya adil.
  3. Baris 36-40 (pengukuran pertama): kedua durasi adalah float dan bernilai positif (True), dan hasil kedua fungsi np.array_equal — sama persis. Pengukuran yang tidak menguji kebenaran hasil bisa saja mengukur kode yang salah.
  4. Baris 44-49 (timeit.repeat): number=5 berarti setiap sampel menjalankan fungsi lima kali, dan repeat=3 menghasilkan tiga sampel — jadi list berisi 3 nilai. Semua bernilai positif (True); pada praktiknya kita membandingkan min() antar pola, bukan satu nilai tunggal, karena sampel terendah paling bebas interferensi.
  5. Baris 53-54 (biaya deterministik): versi loop menjalankan 100.000 pemanggilan Python, versi vectorized tidak melakukan pemanggilan Python sama sekali. Selisih jumlah pemanggilan ini yang tetap benar di mesin mana pun — dan pada pengukuran kami, versi loop selesai ordo puluhan kali lebih lama.

Disiplin Pengukuran #

Empat kebiasaan membuat hasil pengukuran bisa dipercaya. Pertama, bekuakan data: gunakan seed tetap atau data yang sama persis untuk semua kandidat, sebab membandingkan operasi pada data berbeda adalah perbandingan yang tidak bermakna. Kedua, ulangi beberapa sampel dan bandingkan min(), bukan rata-rata — nilai terendah paling dekat dengan kemampuan asli kode. Ketiga, ukur pada ukuran data yang sama dan laporkan ukuran itu; ordo hasil berubah sesuai skala. Keempat, jangan bandingkan angka antar mesin — angka durasi hanya bermakna dalam satu lingkungan yang sama.

Dengan disiplin pengukuran ini, semua teknik Section ini punya dasar yang jelas: ukur, ganti pola Python-level dengan vectorized, tekan memori lewat dtype, lalu ukur lagi. Chapter berikutnya menutup rangkaian penghematan dengan dtype yang paling menguntungkan untuk kolom teks: category beserta penghitungan penghematannya.

Referensi resmi untuk pendalaman: panduan Enhancing performance dan panduan Scaling to large datasets .