Polars - Tutorial Lengkap Library DataFrame Ultra-Cepat
Daftar Isi
Pendahuluan
Polars adalah library DataFrame yang sangat cepat, ditulis dalam Rust dengan binding Python. Library ini memanfaatkan format memori kolumnar Apache Arrow dan mesin query multi-thread untuk memberikan performa yang seringkali 10-100x lebih cepat dari Pandas. Polars mendukung evaluasi eager dan lazy, menjadikannya ideal untuk analisis data eksploratif maupun pipeline data produksi.
Keunggulan utama Polars dibandingkan Pandas:
- Ditulis dalam Rust dengan interoperabilitas zero-copy melalui Apache Arrow
- Eksekusi multi-thread secara default
- Evaluasi lazy dengan optimasi query
- API yang konsisten tanpa kebingungan berbasis indeks
- Dukungan streaming untuk pemrosesan out-of-core
Prasyarat
- Python 3.8 atau lebih tinggi
- Pemahaman dasar tentang DataFrame dan data tabular
pip install polars
pip install polars[all] # Termasuk dependensi opsional (Excel, konektor database, dll.)
pip install pandas # Untuk benchmark perbandingan
pip install scikit-learn # Untuk contoh pipeline ML
Dasar-Dasar Polars
Membuat DataFrame
import polars as pl
Dari dictionary
df = pl.DataFrame({
"nama": ["Alice", "Bob", "Charlie", "Diana", "Eve"],
"umur": [30, 25, 35, 28, 32],
"departemen": ["Teknik", "Pemasaran", "Teknik", "Penjualan", "Pemasaran"],
"gaji": [95000, 65000, 105000, 72000, 78000],
"tanggalmasuk": ["2020-01-15", "2021-06-01", "2019-03-20", "2022-01-10", "2020-09-05"]
})
Konversi kolom tanggal
df = df.withcolumns(pl.col("tanggalmasuk").str.todate())
print(df)
print(f"Bentuk: {df.shape}")
print(f"Skema: {df.schema}")
print(f"Tipe data: {df.dtypes}")
Membaca Data
# CSV
df = pl.readcsv("data.csv")
Parquet (sangat direkomendasikan untuk performa)
df = pl.readparquet("data.parquet")
JSON
df = pl.readjson("data.json")
Dari Pandas
import pandas as pd
pandasdf = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
polarsdf = pl.frompandas(pandasdf)
Kembali ke Pandas jika diperlukan
pandaskembali = polarsdf.topandas()
Operasi Dasar
# Pilih kolom
df.select("nama", "gaji")
df.select(pl.col("nama"), pl.col("gaji"))
Filter baris
df.filter(pl.col("gaji") > 80000)
df.filter((pl.col("departemen") == "Teknik") & (pl.col("umur") > 30))
Urutkan
df.sort("gaji", descending=True)
df.sort(["departemen", "gaji"], descending=[False, True])
Tambah/modifikasi kolom
df.withcolumns(
(pl.col("gaji") 1.1).alias("gajisetelahkenaikan"),
pl.col("nama").str.touppercase().alias("namakapital"),
pl.lit("Aktif").alias("status")
)
Hapus kolom
df.drop("tanggalmasuk")
Ubah nama kolom
df.rename({"nama": "namakaryawan", "gaji": "gajitahunan"})
Statistik deskriptif
df.describe()
Evaluasi Lazy vs Eager
Evaluasi lazy adalah salah satu fitur paling kuat dari Polars. Fitur ini membangun rencana query yang dioptimasi sebelum dieksekusi:
import polars as pl
EAGER: operasi langsung dieksekusi
df = pl.readcsv("filebesar.csv")
result = df.filter(pl.col("jumlah") > 100).select("id", "jumlah").sort("jumlah")
LAZY: membangun rencana query, mengoptimasi, lalu mengeksekusi
result = (
pl.scancsv("filebesar.csv") # Mengembalikan LazyFrame
.filter(pl.col("jumlah") > 100)
.select("id", "jumlah")
.sort("jumlah")
.collect() # Memicu eksekusi
)
Inspeksi rencana query
querylazy = (
pl.scancsv("filebesar.csv")
.filter(pl.col("jumlah") > 100)
.select("id", "jumlah")
.sort("jumlah")
)
Lihat rencana yang dioptimasi
print(querylazy.explain()) # Rencana yang dioptimasi
print(querylazy.explain(optimized=False)) # Rencana tanpa optimasi
Optimizer melakukan beberapa transformasi:
- Predicate pushdown: Filter didorong sedini mungkin
- Projection pushdown: Hanya kolom yang diperlukan yang dibaca dari disk
- Type coercion: Optimasi tipe data otomatis
- Common subexpression elimination: Menggunakan kembali hasil yang sudah dihitung
# Contoh: Optimizer hanya akan membaca kolom "id" dan "jumlah" dari CSV
dan menerapkan filter saat membaca, bukan setelah memuat seluruh file
result = (
pl.scancsv("filebesar.csv")
.filter(pl.col("jumlah") > 100)
.select("id", "jumlah")
.collect()
)
Ekspresi dan Transformasi
Ekspresi adalah blok bangunan inti dari operasi Polars:
import polars as pl
df = pl.DataFrame({
"produk": ["A", "B", "A", "C", "B", "A", "C", "B"],
"kuantitas": [10, 20, 15, 5, 25, 30, 10, 15],
"harga": [9.99, 14.99, 9.99, 24.99, 14.99, 9.99, 24.99, 14.99],
"wilayah": ["Utara", "Selatan", "Utara", "Timur", "Selatan", "Timur", "Utara", "Timur"]
})
Ekspresi kolom
result = df.select(
pl.col("produk"),
pl.col("kuantitas"),
(pl.col("kuantitas") pl.col("harga")).alias("pendapatan"),
pl.col("kuantitas").sum().alias("totalkuantitas"),
pl.col("kuantitas").mean().alias("ratakuantitas"),
pl.col("kuantitas").std().alias("stdkuantitas"),
)
Ekspresi string
dfstring = pl.DataFrame({
"teks": ["Halo Dunia", "foo bar baz", "POLARS SANGAT CEPAT", "ilmu data"]
})
result = dfstring.select(
pl.col("teks"),
pl.col("teks").str.tolowercase().alias("hurufkecil"),
pl.col("teks").str.touppercase().alias("hurufbesar"),
pl.col("teks").str.split(" ").alias("kata"),
pl.col("teks").str.lenchars().alias("jumlahkarakter"),
pl.col("teks").str.contains("(?i)dunia|cepat").alias("cocokpola"),
pl.col("teks").str.replaceall(" ", "").alias("dengangarisbawah"),
)
Ekspresi kondisional (when/then/otherwise)
result = df.withcolumns(
pl.when(pl.col("kuantitas") > 20)
.then(pl.lit("Tinggi"))
.when(pl.col("kuantitas") > 10)
.then(pl.lit("Sedang"))
.otherwise(pl.lit("Rendah"))
.alias("tingkatpermintaan")
)
Ekspresi list/array
dflist = pl.DataFrame({
"tag": [["python", "rust"], ["java", "python"], ["rust", "go", "python"]],
"skor": [[90, 85, 92], [78, 88], [95, 91, 87, 93]]
})
result = dflist.select(
pl.col("tag").list.len().alias("jumlahtag"),
pl.col("tag").list.contains("python").alias("punyapython"),
pl.col("skor").list.mean().alias("rataskor"),
pl.col("skor").list.max().alias("skormaks"),
pl.col("skor").list.sort().alias("skorterurut"),
)
Ekspresi tanggal/waktu
dftanggal = pl.DataFrame({
"timestamp": pl.daterange(pl.date(2024, 1, 1), pl.date(2024, 12, 31), eager=True),
}).withcolumns(
pl.col("timestamp").dt.year().alias("tahun"),
pl.col("timestamp").dt.month().alias("bulan"),
pl.col("timestamp").dt.weekday().alias("harikerja"),
pl.col("timestamp").dt.quarter().alias("kuartal"),
pl.col("timestamp").dt.isleapyear().alias("tahunkabisat"),
)
Join dan Penggabungan DataFrame
# Contoh DataFrame
pesanan = pl.DataFrame({
"idpesanan": [1, 2, 3, 4, 5],
"idpelanggan": [101, 102, 101, 103, 104],
"idproduk": ["P1", "P2", "P3", "P1", "P2"],
"jumlah": [250.0, 150.0, 300.0, 250.0, 150.0]
})
pelanggan = pl.DataFrame({
"idpelanggan": [101, 102, 103, 105],
"nama": ["Alice", "Bob", "Charlie", "Eve"],
"tingkat": ["Gold", "Silver", "Gold", "Bronze"]
})
produk = pl.DataFrame({
"idproduk": ["P1", "P2", "P3"],
"namaproduk": ["Widget", "Gadget", "Doohickey"],
"kategori": ["Elektronik", "Elektronik", "Perkakas"]
})
Inner join
result = pesanan.join(pelanggan, on="idpelanggan", how="inner")
Left join
result = pesanan.join(pelanggan, on="idpelanggan", how="left")
Join bertingkat
result = (
pesanan
.join(pelanggan, on="idpelanggan", how="left")
.join(produk, on="idproduk", how="left")
)
Join dengan nama kolom berbeda
dfa = pl.DataFrame({"ida": [1, 2, 3], "nilaia": [10, 20, 30]})
dfb = pl.DataFrame({"idb": [1, 2, 4], "nilaib": [100, 200, 400]})
result = dfa.join(dfb, lefton="ida", righton="idb", how="outer")
Cross join
ukuran = pl.DataFrame({"ukuran": ["S", "M", "L"]})
warna = pl.DataFrame({"warna": ["Merah", "Biru"]})
kombinasi = ukuran.join(warna, how="cross")
Semi join (pertahankan baris dari kiri yang memiliki kecocokan di kanan)
pelangganaktif = pesanan.join(pelanggan, on="idpelanggan", how="semi")
Anti join (pertahankan baris dari kiri yang TIDAK memiliki kecocokan di kanan)
pesanantanpapelanggan = pesanan.join(pelanggan, on="idpelanggan", how="anti")
Penggabungan vertikal
df1 = pl.DataFrame({"a": [1, 2], "b": [3, 4]})
df2 = pl.DataFrame({"a": [5, 6], "b": [7, 8]})
gabungan = pl.concat([df1, df2])
Penggabungan horizontal
gabunganh = pl.concat([df1, df2], how="horizontal")
Group By dan Agregasi
penjualan = pl.DataFrame({
"tanggal": ["2024-01-15", "2024-01-15", "2024-02-10", "2024-02-10",
"2024-03-05", "2024-03-05", "2024-01-20", "2024-02-25"],
"produk": ["A", "B", "A", "B", "A", "B", "C", "C"],
"wilayah": ["Utara", "Selatan", "Utara", "Selatan", "Utara", "Selatan", "Timur", "Timur"],
"kuantitas": [100, 150, 200, 120, 180, 90, 50, 75],
"pendapatan": [1000, 2250, 2000, 1800, 1800, 1350, 1250, 1875]
})
Group by sederhana
result = penjualan.groupby("produk").agg(
pl.col("kuantitas").sum().alias("totalkuantitas"),
pl.col("pendapatan").sum().alias("totalpendapatan"),
pl.col("pendapatan").mean().alias("ratapendapatan"),
pl.len().alias("jumlahtransaksi"),
)
Group by dengan banyak kolom
result = penjualan.groupby("produk", "wilayah").agg(
pl.col("kuantitas").sum().alias("totalkuantitas"),
pl.col("pendapatan").sum().alias("totalpendapatan"),
)
Agregasi lanjutan
result = penjualan.groupby("produk").agg(
pl.col("kuantitas").sum().alias("totalqty"),
pl.col("pendapatan").sum().alias("totalpend"),
pl.col("pendapatan").min().alias("minpend"),
pl.col("pendapatan").max().alias("makspend"),
pl.col("pendapatan").std().alias("stdpend"),
pl.col("pendapatan").quantile(0.75).alias("q75pend"),
pl.col("wilayah").nunique().alias("wilayahdilayani"),
pl.col("tanggal").first().alias("penjualanpertama"),
pl.col("tanggal").last().alias("penjualanterakhir"),
)
Group by dengan pengurutan
result = (
penjualan
.groupby("produk")
.agg(pl.col("pendapatan").sum().alias("totalpendapatan"))
.sort("totalpendapatan", descending=True)
)
Group by dinamis (berbasis waktu)
penjualants = penjualan.withcolumns(pl.col("tanggal").str.todate())
bulanan = penjualants.groupbydynamic("tanggal", every="1mo").agg(
pl.col("pendapatan").sum().alias("pendapatanbulanan"),
pl.col("kuantitas").sum().alias("kuantitasbulanan"),
)
Fungsi Window
Fungsi window menghitung nilai di seluruh baris yang terkait dengan baris saat ini tanpa menciutkan DataFrame:
df = pl.DataFrame({
"karyawan": ["Alice", "Bob", "Charlie", "Diana", "Eve", "Frank"],
"departemen": ["Teknik", "Teknik", "Teknik", "Penjualan", "Penjualan", "Penjualan"],
"gaji": [95000, 85000, 105000, 72000, 78000, 68000],
"tahunmasuk": [2020, 2021, 2019, 2022, 2020, 2023]
})
over() - fungsi window Polars
result = df.withcolumns(
# Rata-rata gaji dalam setiap departemen
pl.col("gaji").mean().over("departemen").alias("ratagajidept"),
# Peringkat dalam departemen berdasarkan gaji
pl.col("gaji").rank(descending=True).over("departemen").alias("peringkatgaji"),
# Gaji minimum dan maksimum di departemen
pl.col("gaji").min().over("departemen").alias("gajimindept"),
pl.col("gaji").max().over("departemen").alias("gajimaksdept"),
# Jumlah karyawan di departemen
pl.len().over("departemen").alias("ukurandept"),
# Selisih dari rata-rata departemen
(pl.col("gaji") - pl.col("gaji").mean().over("departemen")).alias("gajivsrata"),
)
Fungsi window bergulir (rolling)
datats = pl.DataFrame({
"tanggal": pl.daterange(pl.date(2024, 1, 1), pl.date(2024, 3, 31), eager=True),
"nilai": [float(i) + (i % 7) 3.0 for i in range(91)]
})
result = datats.withcolumns(
pl.col("nilai").rollingmean(windowsize=7).alias("ratabergulir7h"),
pl.col("nilai").rollingsum(windowsize=7).alias("jumlahbergulir7h"),
pl.col("nilai").rollingstd(windowsize=7).alias("stdbergulir7h"),
pl.col("nilai").shift(1).alias("nilaisebelumnya"),
(pl.col("nilai") - pl.col("nilai").shift(1)).alias("perubahanharian"),
((pl.col("nilai") / pl.col("nilai").shift(1)) - 1).alias("perubahanpersen"),
)
Operasi kumulatif
result = df.sort("gaji").withcolumns(
pl.col("gaji").cumsum().alias("gajikumulatif"),
pl.col("gaji").cummax().alias("maksberjalan"),
pl.col("gaji").cummin().alias("minberjalan"),
pl.col("gaji").cumcount().alias("hitunganberjalan"),
)
Benchmark Polars vs Pandas
import polars as pl
import pandas as pd
import numpy as np
import time
def perbandinganbenchmark(jumlahbaris: int = 10000000):
# Buat data uji
np.random.seed(42)
data = {
"id": np.arange(jumlahbaris),
"kategori": np.random.choice(["A", "B", "C", "D", "E"], jumlahbaris),
"nilai1": np.random.randn(jumlahbaris),
"nilai2": np.random.randn(jumlahbaris),
"nilai3": np.random.uniform(0, 1000, jumlahbaris),
}
# Pandas
pdf = pd.DataFrame(data)
mulai = time.time()
pdfresult = (
pdf.groupby("kategori")
.agg({"nilai1": "mean", "nilai2": "sum", "nilai3": ["min", "max", "std"]})
)
waktupandas = time.time() - mulai
# Polars
pldf = pl.DataFrame(data)
mulai = time.time()
plresult = pldf.groupby("kategori").agg(
pl.col("nilai1").mean(),
pl.col("nilai2").sum(),
pl.col("nilai3").min().alias("nilai3min"),
pl.col("nilai3").max().alias("nilai3maks"),
pl.col("nilai3").std().alias("nilai3std"),
)
waktupolars = time.time() - mulai
print(f"Ukuran data: {jumlahbaris:,} baris")
print(f"Pandas: {waktupandas:.3f}d")
print(f"Polars: {waktupolars:.3f}d")
print(f"Percepatan: {waktupandas / waktupolars:.1f}x")
# Benchmark filter
mulai = time.time()
= pdf[(pdf["nilai1"] > 0) & (pdf["nilai3"] < 500)]
filterpandas = time.time() - mulai
mulai = time.time()
= pldf.filter((pl.col("nilai1") > 0) & (pl.col("nilai3") < 500))
filterpolars = time.time() - mulai
print(f"\nBenchmark filter:")
print(f"Pandas: {filterpandas:.3f}d")
print(f"Polars: {filterpolars:.3f}d")
print(f"Percepatan: {filterpandas / filterpolars:.1f}x")
# Benchmark pengurutan
mulai = time.time()
= pdf.sortvalues(["kategori", "nilai1"])
sortpandas = time.time() - mulai
mulai = time.time()
= pldf.sort(["kategori", "nilai1"])
sortpolars = time.time() - mulai
print(f"\nBenchmark pengurutan:")
print(f"Pandas: {sortpandas:.3f}d")
print(f"Polars: {sortpolars:.3f}d")
print(f"Percepatan: {sortpandas / sortpolars:.1f}x")
perbandinganbenchmark(10000000)
Integrasi Pipeline ML
import polars as pl
import numpy as np
from sklearn.modelselection import traintestsplit
from sklearn.preprocessing import StandardScaler
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classificationreport
Muat dan persiapkan data dengan Polars
df = pl.readcsv("datapelanggan.csv")
Rekayasa fitur dengan Polars
dffitur = df.withcolumns(
# Transformasi numerik
pl.col("jumlahpembelian").log().alias("logpembelian"),
(pl.col("jumlahpembelian") / pl.col("kunjungan")).alias("ratapembelianperkunjungan"),
pl.col("harisejakpembelianterakhir").clip(0, 365).alias("resensidibatasi"),
# Encoding kategorikal
pl.col("wilayah").cast(pl.Categorical).alias("wilayahkat"),
# Fitur berbasis waktu
pl.col("tanggaldaftar").str.todate().dt.year().alias("tahundaftar"),
pl.col("tanggaldaftar").str.todate().dt.month().alias("bulandaftar"),
# Fitur berbasis grup
pl.col("jumlahpembelian").mean().over("wilayah").alias("ratapembelianwilayah"),
pl.col("jumlahpembelian").rank().over("wilayah").alias("peringkatdiwilayah"),
)
Tentukan fitur dan target
kolomfitur = [
"logpembelian", "ratapembelianperkunjungan", "resensidibatasi",
"tahundaftar", "bulandaftar", "ratapembelianwilayah",
"peringkatdiwilayah", "kunjungan", "umur"
]
Konversi ke numpy untuk sklearn
X = dffitur.select(kolomfitur).tonumpy()
y = dffitur.select("churn").tonumpy().flatten()
Bagi data
Xtrain, Xtest, ytrain, ytest = traintestsplit(X, y, testsize=0.2, randomstate=42)
Skalakan fitur
scaler = StandardScaler()
Xtrainscaled = scaler.fittransform(Xtrain)
Xtestscaled = scaler.transform(Xtest)
Latih model
clf = RandomForestClassifier(nestimators=100, randomstate=42, njobs=-1)
clf.fit(Xtrainscaled, ytrain)
Evaluasi
ypred = clf.predict(Xtestscaled)
print(classificationreport(ytest, ypred))
Kepentingan fitur kembali di Polars
dfkepentingan = pl.DataFrame({
"fitur": kolomfitur,
"kepentingan": clf.featureimportances
}).sort("kepentingan", descending=True)
print(dfkepentingan)
Menangani Dataset Besar dengan Streaming
Polars dapat menangani dataset yang lebih besar dari RAM yang tersedia melalui streaming:
import polars as pl
Streaming: proses data dalam potongan tanpa memuat semuanya ke memori
result = (
pl.scancsv("datasetsangatbesar.csv") # Tidak dimuat ke memori
.filter(pl.col("status") == "aktif")
.groupby("kategori")
.agg(
pl.col("jumlah").sum().alias("totaljumlah"),
pl.col("jumlah").mean().alias("ratajumlah"),
pl.len().alias("hitungan")
)
.sort("totaljumlah", descending=True)
.collect(streaming=True) # Proses dalam mode streaming
)
Scan file Parquet (lebih efisien lagi)
result = (
pl.scanparquet("data/.parquet") # Pola glob untuk banyak file
.filter(pl.col("tahun") >= 2023)
.select("id", "kategori", "jumlah", "tahun")
.collect(streaming=True)
)
Tulis hasil langsung ke file (tidak pernah mewujudkan hasil penuh di memori)
(
pl.scancsv("inputbesar.csv")
.filter(pl.col("nilai") > 0)
.withcolumns(
(pl.col("nilai") 2).alias("nilaiganda")
)
.sinkparquet("outputdiproses.parquet")
)
Proses banyak file besar secara efisien
import glob
lazyframes = [pl.scanparquet(f) for f in glob.glob("data/bagian.parquet")]
gabungan = pl.concat(lazyframes)
result = (
gabungan
.groupby("idpelanggan")
.agg(
pl.col("jumlahpembelian").sum().alias("nilaiseumurhidup"),
pl.col("tanggalpembelian").max().alias("pembelianterakhir"),
pl.len().alias("totalpesanan")
)
.filter(pl.col("nilaiseumurhidup") > 1000)
.sort("nilaiseumurhidup", descending=True)
.head(1000)
.collect(streaming=True)
)
Konversi CSV besar ke Parquet untuk performa lebih baik
(
pl.scancsv("datalama.csv")
.sinkparquet(
"datalama.parquet",
compression="zstd",
rowgroupsize=100000
)
)
Praktik Terbaik
scan daripada read untuk dataset besar. Biarkan optimizer bekerja.apply() atau maprows() kapan pun memungkinkan.pl.Categorical untuk kolom string dengan sedikit nilai unik.over() untuk Fungsi Window: Daripada self-join atau operasi merge, gunakan over() untuk komputasi tingkat grup.explain() untuk memeriksa rencana query dan mengidentifikasi bottleneck.struct dan list: Polars secara native mendukung tipe data bersarang, menghindari kebutuhan workaround yang kompleks.scanparquet dengan pola glob untuk membiarkan Polars memparalelkan pembacaan.Kesimpulan
Polars merupakan kemajuan signifikan dalam ekosistem pemrosesan data Python. Mesin berbasis Rust, evaluasi lazy dengan optimasi query, dan dukungan streaming yang mulus menjadikannya pilihan ideal untuk beban kerja analitis maupun pipeline data produksi. Dengan mengadopsi praktik terbaik Polars -- evaluasi lazy, format file Parquet, pemikiran kolumnar, dan streaming -- Anda dapat memproses dataset dengan kecepatan berlipat ganda dibandingkan dengan alat tradisional, sambil menggunakan memori yang jauh lebih sedikit. Untuk proyek baru, Polars seharusnya menjadi library DataFrame default Anda.