Sampai titik ini kita memfilter baris dengan boolean masking: df[(df["harga"] > 40000) & (df["jumlah"] >= 5)] menuntut penulisan nama DataFrame berulang dan tanda & alih-alih and. Method query() menyaring baris dengan satu string ekspresi yang jauh lebih ringkas, dan eval() menghitung aritmetika antarkolom dengan cara yang sama. Di balik keduanya berdiri mesin evaluasi numexpr yang bisa memangkas waktu dan memori saat DataFrame membesar. Chapter ini menguji kedua method pada DataFrame 60.000 baris transaksi, membedah sintaksnya, lalu menetapkan batas kapan keduanya benar-benar menguntungkan.

query(): Filter dengan Ekspresi String #

Script berikut membangun transaksi dengan random.default_rng(42) agar hasil deterministik, lalu memakai query() dalam beberapa varian.

python
import numpy as np
import pandas as pd

rng = np.random.default_rng(42)
n = 60_000
kota_pool = np.array(["Jakarta", "Bandung", "Surabaya", "Medan"])
df = pd.DataFrame({
    "kota": rng.choice(kota_pool, n),
    "harga": rng.integers(5_000, 50_000, n),
    "jumlah": rng.integers(1, 10, n),
    "diskon": rng.random(n).round(2),
})

print("numexpr aktif (default):", pd.get_option("compute.use_numexpr"))
print()

# query() memfilter baris dengan ekspresi string
terpilih = df.query("harga > 40000 and jumlah >= 5")
print("baris terpilih:", len(terpilih))
print(terpilih.head(3))
print()

# variabel Python dirujuk dengan awalan @
batas = 40_000
print("dengan @batas :", len(df.query("harga > @batas")))
print("dengan literal:", len(df.query("harga > 40000")))
print()

# in dan not in untuk keanggotaan list
prioritas = ["Jakarta", "Surabaya"]
print("in    :", len(df.query("kota in @prioritas")))
print("not in:", len(df.query("kota not in @prioritas")))
print()

# and/or ditulis apa adanya; & | juga sah
a = df.query("diskon > 0.9 or harga < 6000")
b = df.query("(diskon > 0.9) | (harga < 6000)")
print("or == | :", a.equals(b))
print()

# kolom dengan spasi dibungkus backtick
df2 = df.rename(columns={"harga": "harga jual"})
print("backtick:", len(df2.query("`harga jual` > 45000")))
print()

# eval() menghitung kolom baru dari ekspresi antarkolom
df.eval("total = harga * jumlah * (1 - diskon)", inplace=True)
print(df.head(3).round(2))
print()

# referensi nama yang tidak dikenal memunculkan UndefinedVariableError
try:
    df.query("harca > 1000")
except pd.errors.UndefinedVariableError as e:
    print("UndefinedVariableError:", str(e))

Output yang Dihasilkan:

text
numexpr aktif (default): True

baris terpilih: 7520
        kota  harga  jumlah  diskon
12  Surabaya  46692       5    0.51
18     Medan  46480       7    0.06
25  Surabaya  43024       5    0.71

dengan @batas : 13407
dengan literal: 13407

in    : 30104
not in: 29896

or == | : True

backtick: 6682

       kota  harga  jumlah  diskon      total
0   Jakarta   9000       8    0.10   64800.00
1     Medan  20620       2    0.74   10722.40
2  Surabaya  37313       7    0.29  185445.61

UndefinedVariableError: name 'harca' is not defined

Mari kita bedah bagian pentingnya:

  1. Baris 18 (and di dalam string): df.query("harga > 40000 and jumlah >= 5") menyaring 7.520 baris; operator logika and, or, dan not ditulis apa adanya, tanpa tanda kurung penjelas di tiap kondisi.
  2. Baris 25-26 (awalan @): variabel Python batas dirujuk sebagai @batas; hasilnya identik dengan literal 13407 baris, dan pemakaian variabel menjaga satu nilai ambang tetap konsisten antar script.
  3. Baris 31-32 (in/not in): keanggotaan list ditulis seperti Python biasa; kota in @prioritas mengambil 30.104 baris, dan not in menjadi kebalikannya 29.896.
  4. Baris 36-38 (setara |): or pada parser pandas diartikan sebagai | elementwise, sehingga kedua versi menghasilkan DataFrame yang equals.
  5. Baris 43 (backtick): nama kolom yang mengandung spasi dibungkus tanda kutip terbalik (backtick) di dalam ekspresi, sehingga kolom harga jual bisa dirujuk dengan aman; versinya menyaring 6.682 baris.

eval(): Aritmetika Antarkolom #

Baris 47 menambahkan kolom total lewat df.eval("total = harga * jumlah * (1 - diskon)", inplace=True). Bentuk penugasan di dalam string hanya diterima eval() dengan argumen inplace=True; tanpa penugasan, eval() mengembalikan Series hasil perhitungan. Angka pada output bisa diverifikasi manual: 9000 × 8 × (1 − 0.10) = 64800.00. Ekspresi juga boleh membandingkan kolom dengan kolom — df.eval("jumlah > diskon * 20") menghasilkan mask boolean tanpa menulis nama DataFrame berulang kali, pola yang pernah kita lihat sebagai kelemahan masking biasa pada ekspresi panjang.

Mesin numexpr di Balik Layar #

Baris 14 mencetak pd.get_option("compute.use_numexpr") yang bernilai True di lingkungan kita karena paket numexpr (versi 2.14.1) terpasang; itu engine default untuk eval(), query(), dan perbandingan antar-Series. Manfaatnya baru terasa pada skala besar: pada pengukuran sederhana dengan ekspresi aritmetika bertingkat di lima juta baris, engine numexpr menyelesaikan perhitungan sekitar dua kali lebih cepat daripada mode Python, dan puncak penggunaan memori turun dari sekitar 120 MB menjadi 80 MB karena array antara yang besar tidak dibuat penuh. Sebaliknya, untuk operasi sederhana atau data kecil, boolean masking biasa justru paling cepat — jadi pakailah query()/eval() demi kejelasan sintaks, dan biarkan numexpr menjadi bonus pada data besar.

Dengan query() dan eval(), ekspresi atas kolom kini bisa ditulis ringkas pada skala apa pun. Chapter berikutnya menyajikan cara baru menulis rumus kolom lewat pd.col(), fitur pandas 3.0, sebelum kita membedah fondasi di balik banyak fasilitas modern pandas: sistem extension dtypes yang menampung str, category, Int64, dtype pyarrow, dan sparse sekaligus.

Referensi resmi untuk chapter ini: pandas.DataFrame.query , pandas.DataFrame.eval , dan pandas.errors.UndefinedVariableError .