Ibis: API Dataframe Python yang Portabel di Banyak Backend
Ibis adalah library dataframe Python yang memungkinkan Anda menulis kode analitik satu kali lalu menjalankannya di banyak mesin eksekusi, mulai dari DuckDB lokal di laptop hingga BigQuery, Snowflake, atau Spark di produksi. Daripada menulis ulang logika pandas menjadi SQL ketika data sudah tidak muat di memori, Anda cukup mendeskripsikan transformasinya dalam Python, dan Ibis akan mengompilasinya ke dialek backend mana pun yang Anda hubungkan. Tutorial ini membahas konsep intinya: deferred execution, portabilitas backend, dan contoh analitik menyeluruh yang praktis.
Masalah yang Diselesaikan Ibis
Banyak tim data menghadapi kendala yang sama. Anda membuat prototipe analisis dengan pandas karena cepat ditulis dan mudah dipahami. Dataset bertambah besar, tidak lagi muat di memori, dan sekarang Anda harus menerjemahkan kode pandas tersebut menjadi SQL agar bisa berjalan di dalam data warehouse. Kedua implementasi itu mulai berbeda, bug muncul saat penerjemahan, dan Anda harus memelihara dua versi dari logika yang sama.
Ibis menawarkan satu API dataframe yang berdiri di depan mesin eksekusi. Anda menulis ekspresi dalam Python, dan:
- Kode yang sama berjalan di mesin lokal (DuckDB) saat pengembangan dan di warehouse (BigQuery, Snowflake, Postgres) saat produksi.
- Komputasi berat terjadi di dalam mesin, dekat dengan data, bukan di proses Python Anda.
- Anda terhindar dari menerjemahkan pandas ke SQL secara manual, dan bisa menskalakan dari laptop ke warehouse cukup dengan mengganti koneksi.
Model berpikirnya lebih dekat ke query builder ketimbang pandas. Anda menyusun deskripsi sebuah komputasi, bukan langsung memmaterialisasi hasil antara.
Deferred Execution (Eksekusi Tertunda)
Ini adalah konsep paling penting di Ibis. Ketika Anda merangkai operasi seperti filter, select, dan groupby, tidak ada yang dihitung. Anda sedang membangun expression tree. Eksekusi baru terjadi ketika Anda secara eksplisit meminta hasilnya.
import ibis
con = ibis.duckdb.connect()
Ini membangun ekspresi. Belum ada query yang berjalan.
orders = con.table("orders")
expr = (
orders
.filter(orders.status == "completed")
.groupby("country")
.aggregate(total=orders.amount.sum())
)
Masih belum ada yang dieksekusi. expr hanyalah sebuah deskripsi.
print(type(expr)) #
Eksekusi terjadi di sini, mengembalikan pandas DataFrame.
df = expr.execute()
Titik pemicu yang benar-benar menjalankan query adalah:
.execute()— mengembalikan pandas DataFrame (default)..topandas()— output pandas secara eksplisit..topolars()— mengembalikan Polars DataFrame..topyarrow()— mengembalikan PyArrow Table..head().execute()— melihat sampel kecil.
Karena eksekusi tertunda, Ibis dapat mendorong seluruh pipeline ke mesin sebagai satu query yang teroptimasi. Inilah yang membuatnya skalabel: Anda tidak menarik data mentah ke Python lalu memfilternya di sana.
Instalasi
Ibis dipasang bersama satu atau lebih extra backend. Setiap backend yang ingin Anda gunakan adalah dependensi opsional, sehingga instalasinya tetap ramping.
# Library inti plus backend DuckDB (titik awal yang direkomendasikan)
pip install 'ibis-framework[duckdb]'
Tambahkan backend lain sesuai kebutuhan
pip install 'ibis-framework[bigquery]'
pip install 'ibis-framework[postgres]'
pip install 'ibis-framework[snowflake]'
pip install 'ibis-framework[polars]'
Beberapa backend sekaligus
pip install 'ibis-framework[duckdb,postgres,bigquery]'
Verifikasi instalasi:
import ibis
print(ibis.version)
DuckDB adalah backend lokal yang direkomendasikan. Ia berjalan in-process, tidak butuh server, dan mendukung jangkauan fitur SQL lengkap yang dikompilasi Ibis, sehingga ideal untuk pengembangan dan pengujian.
Menghubungkan ke Backend
Koneksi adalah objek yang tahu cara berbicara dengan sebuah mesin. API ekspresi yang sama bekerja terlepas dari koneksi mana yang Anda buat.
import ibis
DuckDB in-memory (bagus untuk eksperimen)
con = ibis.duckdb.connect()
DuckDB yang dipersistensi ke file
con = ibis.duckdb.connect("analytics.ddb")
Koneksi gaya URL (berfungsi lintas backend)
con = ibis.connect("duckdb://analytics.ddb")
Untuk backend warehouse, koneksi membawa kredensial dan detail proyek. Ekspresi yang Anda bangun setelahnya tetap identik.
# BigQuery
con = ibis.bigquery.connect(
projectid="my-gcp-project",
datasetid="analytics",
)
PostgreSQL
con = ibis.postgres.connect(
host="localhost",
user="analyst",
password="secret",
database="warehouse",
)
Snowflake
con = ibis.snowflake.connect(
user="analyst",
account="abc-xy123",
database="ANALYTICS",
warehouse="COMPUTEWH",
)
Polars (in-memory, bukan mesin SQL)
con = ibis.polars.connect()
Perhatikan bahwa beberapa backend, seperti Polars dan pandas, bukan mesin SQL. Ibis tetap memberi Anda API dataframe yang sama di atasnya, mengeksekusi melalui library native alih-alih menghasilkan SQL.
Membaca Data
Ibis dapat membaca tabel yang sudah ada di backend atau memuat file secara langsung. DuckDB membuat pembacaan file menjadi sangat praktis.
import ibis
con = ibis.duckdb.connect()
Mereferensikan tabel yang sudah ada di backend
orders = con.table("orders")
Membaca file secara langsung (DuckDB, di antaranya)
sales = con.readcsv("data/sales.csv")
events = con.readparquet("data/events/.parquet")
Daftar tabel yang terdaftar di koneksi
print(con.listtables())
Anda juga dapat membawa data in-memory sebagai memtable. Ini berguna untuk tabel lookup kecil atau data uji, dan berfungsi lintas backend.
import pandas as pd
import ibis
regions = pd.DataFrame(
{
"country": ["ID", "SG", "MY"],
"region": ["SEA", "SEA", "SEA"],
}
)
Membungkus pandas DataFrame menjadi tabel Ibis
regiontable = ibis.memtable(regions)
Operasi Tabel Inti
Kosakata operasinya akan terasa akrab jika Anda menguasai SQL atau pandas, tetapi semuanya tersusun secara lazy.
Select dan Filter
import ibis
con = ibis.duckdb.connect()
orders = con.table("orders")
Memilih kolom tertentu
expr = orders.select("orderid", "country", "amount")
Memfilter baris (beberapa kondisi digabung dengan & dan |)
expr = orders.filter(
(orders.amount > 100) & (orders.status == "completed")
)
Referensi Kolom Tertunda
Ibis menyediakan objek underscore yang merujuk pada "tabel saat ini" di dalam sebuah rantai. Objek ini memungkinkan Anda menulis pipeline yang mengalir tanpa harus menyebut nama tabel berulang kali.
from ibis import
expr = (
orders
.filter(
.status == "completed")
.mutate(netamount=.amount - .discount)
.filter(.netamount > 50)
.orderby(.netamount.desc())
.limit(10)
)
Di sini selalu menunjuk ke tabel yang dihasilkan langkah sebelumnya, termasuk kolom netamount yang ditambahkan oleh mutate, yang tidak ada di tabel orders asli.
Mutate, Order, dan Limit
expr = (
orders
.mutate(amountwithtax=orders.amount 1.11)
.orderby(ibis.desc("amountwithtax"))
.limit(20)
)
Agregasi dengan groupby
from ibis import
metrics = (
orders
.group
by("country")
.aggregate(
ordercount=.orderid.count(),
totalrevenue=.amount.sum(),
avgorder=.amount.mean(),
)
.orderby(.totalrevenue.desc())
)
Join
con = ibis.duckdb.connect()
orders = con.table("orders")
customers = con.table("customers")
joined = orders.join(
customers,
orders.customerid == customers.customerid,
how="inner",
)
result = joined.select(
"orderid",
"amount",
customers.name,
customers.segment,
)
Window Function
Window function menghitung nilai di sekumpulan baris yang berkaitan dengan baris saat ini, tanpa meringkasnya seperti yang dilakukan agregat.
from ibis import
Memberi peringkat order berdasarkan jumlah di tiap negara
ranked = orders.mutate(
rankincountry=ibis.rank().over(
ibis.window(groupby="country", orderby=.amount.desc())
)
)
Metode String dan Tanggal
Ibis menyediakan metode string dan temporal yang dikompilasi ke fungsi backend yang tepat.
from ibis import
expr = orders.mutate(
countrylower=.country.lower(),
nameclean=.customername.strip(),
haspromo=.notes.contains("PROMO"),
orderyear=.orderdate.year(),
ordermonth=.orderdate.month(),
dayssince=(ibis.now() - .orderdate).cast("int64"),
)
Pratinjau Secara Lazy dan Memeriksa SQL Hasil Kompilasi
Karena ekspresi tertunda, Anda dapat memeriksa apa yang akan dikirim Ibis ke mesin sebelum menjalankan sesuatu yang berat. Ini sangat berharga untuk debugging dan untuk memahami biaya.
from ibis import
expr = (
orders
.filter(
.status == "completed")
.groupby("country")
.aggregate(total=.amount.sum())
)
Pratinjau hanya beberapa baris (tetap mendorong query, dengan LIMIT)
print(expr.head(5).execute())
Memeriksa SQL hasil kompilasi tanpa mengeksekusi
print(ibis.tosql(expr))
Ekspresi yang sama dikompilasi menjadi SQL berbeda tergantung backend yang terhubung. Di bawah ini, Python yang identik menghasilkan dialek DuckDB dan BigQuery.
import ibis
expr = (
ibis.table({"country": "string", "amount": "float64"}, name="orders")
.groupby("country")
.aggregate(total=ibis..amount.sum())
)
print(ibis.tosql(expr, dialect="duckdb"))
print(ibis.tosql(expr, dialect="bigquery"))
-- dialek duckdb
SELECT
"country",
SUM("amount") AS "total"
FROM "orders"
GROUP BY 1
-- dialek bigquery
SELECT
country,
SUM(amount) AS total
FROM orders
GROUP BY 1
Perhatikan perbedaan pada quoting identifier ("..." versus ` ... ). Ibis menangani sintaks spesifik dialek, nama fungsi, dan cast tipe sehingga Anda tidak perlu memikirkannya.
Berganti Backend: Imbalan dari Portabilitas
Di sinilah Ibis membuktikan tempatnya dalam sebuah stack. Anda mengembangkan dan menguji terhadap DuckDB secara lokal, lalu mengarahkan kode yang sama ke BigQuery di produksi. Logika analitik tidak berubah, hanya koneksinya.
import ibis
from ibis import
def revenuebycountry(con, tablename="orders"):
"""Analitik agnostik-backend. Berfungsi di koneksi Ibis mana pun."""
orders = con.table(tablename)
return (
orders
.filter(.status == "completed")
.groupby("country")
.aggregate(
totalrevenue=.amount.sum(),
ordercount=.orderid.count(),
)
.orderby(.totalrevenue.desc())
)
Pengembangan: DuckDB lokal
devcon = ibis.duckdb.connect("local.ddb")
print(revenuebycountry(devcon).execute())
Produksi: BigQuery, fungsi yang sama, koneksi berbeda
prodcon = ibis.bigquery.connect(projectid="my-project", datasetid="analytics")
print(revenuebycountry(prodcon).execute())
Fungsi ini ditulis satu kali dan benar-benar portabel. Satu-satunya detail spesifik lingkungan adalah objek koneksi yang Anda berikan.
Interoperabilitas dengan pandas, Polars, dan PyArrow
Ibis dirancang untuk menyatu ke dalam stack data Python yang sudah ada, bukan menggantikannya. Anda dapat memindahkan data masuk dan keluar dari pandas, Polars, dan Arrow dengan leluasa.
import ibis
con = ibis.duckdb.connect()
orders = con.table("orders")
Keluar: pilih format output yang Anda inginkan
pdf = orders.limit(1000).topandas()
pldf = orders.limit(1000).topolars()
arrowtbl = orders.limit(1000).topyarrow()
Masuk: bungkus dataframe eksternal menjadi tabel Ibis
import pandas as pd
extra = pd.DataFrame({"id": [1, 2], "label": ["a", "b"]})
extratbl = ibis.memtable(extra)
Pola yang umum adalah menggunakan Ibis sebagai frontend yang menyerahkan SQL berat ke warehouse, lalu menarik hanya hasil agregat yang kecil ke pandas untuk plotting atau pelaporan, sehingga hanya ringkasannya yang melintasi jaringan.
Scalar Python UDF
Ketika sebuah backend tidak memiliki fungsi bawaan yang Anda butuhkan, Anda dapat mendefinisikan user-defined function skalar dalam Python. Dukungan dan performanya bergantung pada backend (DuckDB mendukung Python UDF dengan baik).
import ibis
from ibis import
@ibis.udf.scalar.python
def classifyamount(x: float) -> str:
if x >= 1000:
return "large"
elif x >= 100:
return "medium"
return "small"
con = ibis.duckdb.connect()
orders = con.table("orders")
expr = orders.mutate(sizebucket=classifyamount(.amount))
Utamakan operasi bawaan Ibis bila tersedia, karena ia dikompilasi ke fungsi native mesin dan berjalan lebih cepat dibanding UDF Python baris demi baris. Gunakan UDF hanya ketika logika tidak bisa diekspresikan dengan cara lain.
Jalan Keluar ke SQL Mentah
Ibis tidak mengunci Anda dari SQL mentah. Ketika Anda butuh konstruksi spesifik backend atau sekadar lebih suka menulis query secara manual, Anda dapat turun ke SQL dan tetap menyusun di atas hasilnya.
import ibis
from ibis import
con = ibis.duckdb.connect()
Menjalankan SQL mentah dan mendapatkan tabel Ibis kembali
top = con.sql(
"""
SELECT country, SUM(amount) AS total
FROM orders
WHERE status = 'completed'
GROUP BY country
"""
)
Lanjut membangun dengan API Ibis di atas hasil SQL
final = top.filter(.total > 10000).orderby(.total.desc())
Memberi nama (alias) ekspresi tabel untuk dipakai ulang atau di dalam con.sql
labeled = top.alias("countrytotals")
Pendekatan campuran ini bersifat praktis: gunakan API Ibis yang portabel untuk sebagian besar logika Anda, dan gunakan con.sql(...) hanya untuk bagian yang benar-benar butuh query tulisan tangan.
Contoh Menyeluruh: Analitik Penjualan di DuckDB
Contoh berikut berjalan sepenuhnya di instance DuckDB lokal. Ia membangun dua memtable, menggabungkan orders dengan customers, menghitung metrik berkelompok, dan memberi peringkat order di tiap segmen menggunakan window function.
import ibis
import pandas as pd
from ibis import
ibis.options.interactive = True # auto-eksekusi dan cetak rapi di REPL
con = ibis.duckdb.connect()
ordersdf = pd.DataFrame(
{
"orderid": [1, 2, 3, 4, 5, 6],
"customerid": [10, 10, 11, 12, 11, 12],
"amount": [120.0, 80.0, 500.0, 30.0, 220.0, 90.0],
"status": [
"completed",
"completed",
"completed",
"cancelled",
"completed",
"completed",
],
"orderdate": pd.todatetime(
[
"2026-01-05",
"2026-01-09",
"2026-02-01",
"2026-02-03",
"2026-02-15",
"2026-03-02",
]
),
}
)
customersdf = pd.DataFrame(
{
"customerid": [10, 11, 12],
"name": ["Andi", "Budi", "Citra"],
"segment": ["retail", "wholesale", "retail"],
}
)
orders = con.createtable("orders", ordersdf)
customers = con.createtable("customers", customersdf)
1) Gabungkan orders dengan customers, simpan hanya order completed
joined = (
orders
.filter(.status == "completed")
.join(customers, "customerid", how="inner")
)
2) Metrik berkelompok per segmen pelanggan
segmentmetrics = (
joined
.groupby("segment")
.aggregate(
revenue=.amount.sum(),
ordercount=.orderid.count(),
avgorder=.amount.mean(),
)
.orderby(.revenue.desc())
)
print(segmentmetrics.execute())
3) Peringkat tiap order berdasarkan amount dalam segmennya (window function)
ranked = joined.mutate(
rankinsegment=ibis.rank().over(
ibis.window(groupby="segment", orderby=.amount.desc())
)
).select("orderid", "name", "segment", "amount", "rankinsegment")
print(ranked.orderby(["segment", "rankinsegment"]).execute())
Periksa SQL yang benar-benar dijalankan DuckDB untuk langkah perankingan
print(ibis.tosql(ranked))
Pipeline ini terbaca seperti Python biasa, namun setiap langkah dieksekusi di dalam DuckDB sebagai satu query hasil kompilasi. Mengganti con dengan koneksi BigQuery akan menjalankan logika yang identik di warehouse.
Kapan Memakai Ibis vs Polars vs SQL Mentah
Setiap alat punya keunggulan masing-masing, dan keduanya sering berdampingan dalam satu proyek.
- Gunakan Ibis ketika Anda butuh satu basis kode analitik yang berjalan di banyak mesin, ketika data Anda ada di warehouse dan Anda ingin mendorong komputasi ke sana, atau ketika Anda ingin mengembangkan secara lokal di DuckDB lalu menerapkannya ke BigQuery atau Snowflake tanpa menulis ulang. Ibis adalah frontend, bukan mesin.
- Gunakan Polars ketika data Anda muat nyaman di satu mesin, Anda ingin performa in-process maksimal, dan Anda tidak butuh portabilitas backend. Polars adalah mesin dan sangat baik untuk kerja dataframe lokal yang cepat.
- Gunakan SQL mentah ketika sebuah query secara alami diekspresikan dalam SQL, bersifat spesifik backend, atau ketika tim Anda sudah memelihara aset SQL. Anda tetap bisa membungkus SQL itu di Ibis lewat con.sql(...)
untuk menyusun lebih lanjut.
Hibrida yang masuk akal: buat prototipe dan eksplorasi dengan Polars atau pandas, ekspresikan pipeline produksi di Ibis agar berjalan di warehouse, dan turun ke con.sql(...) untuk sesekali query yang butuh penyetelan manual.
Praktik Terbaik
- Kembangkan di DuckDB, terapkan di warehouse Anda. DuckDB memberi loop lokal yang cepat tanpa server sambil menjaga perilakunya dekat dengan mesin SQL produksi.
- Periksa SQL dengan ibis.tosql(expr)
sebelum menjalankan query mahal. Ia menunjukkan persis apa yang akan dieksekusi mesin dan membantu Anda menalar biaya. - Jaga komputasi berat tetap di mesin. Filter dan agregasi sebelum memanggil .topandas()
, sehingga hanya hasil kecil yang masuk ke Python. - Gunakan from ibis import
untuk rantai yang terbaca dan mengalir yang merujuk kolom antara, dan utamakan operasi bawaan dibanding UDF karena ia dikompilasi ke fungsi native mesin dan berjalan jauh lebih cepat daripada Python per baris. - Berikan koneksi sebagai parameter ke fungsi analitik Anda agar logika yang sama tetap agnostik-backend dan mudah diuji.
- Pin versi Ibis dan backend Anda di produksi, karena kompilasi dialek bisa berubah antarrilis.
Kesimpulan dan Poin Penting
Ibis memberi Anda satu API dataframe yang portabel yang dikompilasi ke dialek SQL mesin mana pun yang Anda hubungkan, sehingga Anda dapat menulis analitik satu kali dan menjalankannya dari laptop hingga warehouse.
- Ibis adalah frontend di atas mesin eksekusi, bukan mesin itu sendiri. Ia membangun ekspresi dan menyerahkan query hasil kompilasi ke backend seperti DuckDB, BigQuery, Postgres, Snowflake, dan Spark.
- Eksekusi bersifat tertunda. Anda menyusun expression tree, dan tidak ada yang berjalan sampai Anda memanggil .execute()
,.topandas(),.topolars(), atau.topyarrow(). - Kode yang sama berjalan di backend berbeda hanya dengan mengganti koneksi, dan inilah manfaat portabilitas yang utama.
- Anda dapat memeriksa SQL hasil kompilasi dengan ibis.to_sql(expr)
dan memastikan ekspresi yang sama menghasilkan dialek berbeda per backend. - Ibis berinteroperasi dengan rapi bersama pandas, Polars, dan PyArrow, serta menyediakan jalan keluar con.sql(...)` untuk SQL mentah saat dibutuhkan.
- Pilih Ibis untuk portabilitas dan pushdown skala warehouse, Polars untuk kerja lokal satu mesin yang cepat, dan SQL mentah di mana ia paling cocok, dengan memadukan keduanya sesuai kebutuhan proyek.