pandas adalah library Python untuk mengolah tabel di memori. Tabelnya disebut DataFrame: baris dan kolom, dan tiap kolom punya satu tipe. Satu kolomnya disebut Series. Bayangin spreadsheet yang kamu kendalikan lewat kode: tiap klik (urutkan, saring, hapus duplikat, jumlahkan) jadi satu baris kode. Besok, waktu datanya bertambah, semua langkah itu tinggal dijalankan ulang.
🧪 Tebakan cepat: extract Toko Buku berisi 23 buku. Sebelum dibersihkan,
df.groupby("city")["qty"].sum()menjumlah buku per kota. Kalau semua hasilnya dijumlahkan lagi, totalnya berapa: 23, 21, atau error? Tebak dulu, jawabannya ada di bagian "groupby: GROUP BY versi pandas".
DataFrame pertama dari CSV
Kita pakai order_lines.csv dari pelajaran sebelumnya: 19 baris pesanan Toko Buku, dengan tiga baris kotornya. Semua output di sini dari pandas 3.0.6 (rilis terbaru waktu pelajaran ini ditulis), dipasang pakai pip install pandas di virtual environment.
import pandas as pd
df = pd.read_csv("order_lines.csv", parse_dates=["created_at"])
print(df.shape)
print(df.dtypes)
(19, 9)
item_id int64
order_id int64
created_at datetime64[us]
customer str
city str
book_id int64
title str
qty int64
price int64
dtype: object
import pandas as pdmemuat library-nya dengan nama pendekpd.pd.read_csv(...)membaca file jadi DataFrame dan menyimpannya di variabeldf.parse_dates=["created_at"](sebuah list, ditulis dengan kurung siku) menyuruh kolom itu dibaca sebagai tanggal.df.shapeadalah (baris, kolom): 19 baris, 9 kolom.df.dtypesmenunjukkan tipe tiap kolom:int64bilangan bulat,datetime64[us]tanggal,strteks. Di pandas 3.0 teks punya tipestrsendiri; versi lama menulisnyaobject.
Tipe di atas ditebak pandas dari isinya. Kamu bisa menyebutkannya sendiri dengan dtype={"price": "int64"} (sebuah dict: pasangan nama dan nilai dalam kurung kurawal). Hasilnya untuk file yang harganya Rp 89.000: ValueError: invalid literal for int() with base 10: 'Rp 89.000'. Bagus, error-nya kelihatan. Tapi untuk file yang harganya 89.000, int64 pun gak menolong: pandas membacanya 89. Yang menolong itu thousands=".", yang ngasih tahu pandas bahwa titik itu pemisah ribuan.
Membersihkan tiga baris kotor
print(df["city"].isna().sum())
print(df[df.duplicated(subset=["order_id", "book_id"], keep=False)][["item_id", "order_id", "book_id", "qty"]])
df = df.drop_duplicates(subset=["order_id", "book_id"])
df["city"] = df["city"].fillna("Tidak diketahui")
2
item_id order_id book_id qty
7 8 6 2 2
8 9 6 2 2
df["city"]mengambil satu kolom (Series)..isna()bilangTrueuntuk tiap nilai kosong, dan.sum()menghitungTrue-nya: 2, pesanan Maya.df.duplicated(subset=[...], keep=False)menandai baris yangorder_iddanbook_id-nya kembar.df[...]dengan tanda itu menyaring barisnya: item 8 dan 9. Angka paling kiri (7 dan 8) adalah index, nomor baris milik pandas.drop_duplicates(...)membuang kembarannya dan menyisakan yang pertama: 18 baris. Hasilnya disimpan lagi kedf.fillna("Tidak diketahui")mengganti nilai kosong. Hasilnya ditulis balik ke kolomnya dengandf["city"] = ....
merge: harga dari katalog
Harga 950000 kita betulkan dengan mengambil harga dari books.csv (kolom book_id, title, author, catalog_price):
buku = pd.read_csv("books.csv")
df = df.merge(buku[["book_id", "catalog_price"]], on="book_id", how="left")
df["total"] = df["qty"] * df["catalog_price"]
mergeituJOIN-nya pandas.on="book_id"kolom kuncinya,how="left"artinya semua barisdfdipertahankan (kayakLEFT JOIN).df.merge(buku, ...)sama denganpd.merge(df, buku, ...).buku[["book_id", "catalog_price"]]cuma mengambil dua kolom, biartitlegak muncul dobel.- Operasi pada kolom berlaku untuk semua baris sekaligus:
df["qty"] * df["catalog_price"]mengalikan 18 pasang angka dalam satu baris kode.
Jangan tertukar dengan concat (menumpuk dua tabel atas-bawah atau kiri-kanan, tanpa mencocokkan kunci) dan join (menggabungkan lewat index). append sudah dihapus sejak pandas 2.0.
groupby: GROUP BY versi pandas
print(df.groupby("city").agg(terjual=("qty", "sum"), omzet=("total", "sum")))
terjual omzet
city
Bandung 5 550000
Jakarta 9 957000
Surabaya 5 493000
Tidak diketahui 2 214000
groupby("city") membagi baris per kota, .agg(nama=("kolom", "fungsi")) menghitung tiap kelompok. Totalnya 21 buku dan Rp 2.214.000, cocok dengan hasil SQL di pelajaran pertama.
Sekarang jalankan groupby yang sama sebelum dibersihkan:
city
Bandung 5
Jakarta 11
Surabaya 5
Name: qty, dtype: int64
5 + 11 + 5 = 21, padahal datanya 23 buku. Itu jawaban tebakannya: gak ada error, 2 buku Maya hilang. SQL bikin satu kelompok NULL, tapi groupby di pandas membuang baris yang kuncinya kosong. Pakai fillna dulu, atau groupby("city", dropna=False) biar kelompok NaN muncul.
Sekarang pertanyaan pemilik: buku terlaris per kota per minggu.
df["minggu"] = df["created_at"].dt.to_period("W").dt.start_time
per_judul = df.groupby(["minggu", "city", "title"], as_index=False).agg(terjual=("qty", "sum"))
terlaris = (
per_judul.sort_values(["minggu", "city", "terjual", "title"], ascending=[True, True, False, True])
.groupby(["minggu", "city"])
.head(1)
)
print(terlaris.to_string(index=False))
minggu city title terjual
2026-09-28 Bandung Bumi Manusia 2
2026-09-28 Jakarta Laskar Pelangi 3
2026-09-28 Surabaya Cantik Itu Luka 2
2026-09-28 Tidak diketahui Laskar Pelangi 1
2026-10-05 Bandung Ronggeng Dukuh Paruk 2
2026-10-05 Jakarta Cantik Itu Luka 2
2026-10-05 Surabaya Laskar Pelangi 1
2026-10-05 Tidak diketahui Bumi Manusia 1
.dt.to_period("W").dt.start_timemembulatkan tanggal ke hari Senin minggunya ("W"di pandas berarti minggu yang berakhir hari Minggu).sort_valuesmengurutkan: terjual terbanyak dulu di tiap kota, judul sebagai pemutus seri..groupby([...]).head(1)mengambil baris pertama tiap minggu dan kota.- Surabaya minggu 5 Oktober sebenarnya seri: Laskar Pelangi 1, Ronggeng Dukuh Paruk 1.
head(1)cuma menampilkan satu, jadi serinya tersembunyi. Pelajaran SQL berikutnya menanganinya denganRANK().
Copy-on-Write di pandas 3.0
Di pandas 3.0, Copy-on-Write jadi satu-satunya mode: tiap hasil saring atau ambil kolom berlaku kayak salinan. Jadi kode yang menyaring lalu mengisi dalam dua langkah gak pernah mengubah df:
df[df["city"].isna()]["city"] = "Tidak diketahui"
pandas 3.0.6 memberi peringatan ChainedAssignmentError: A value is being set on a copy of a DataFrame or Series through chained assignment., dan df tetap punya 2 kota kosong. df["city"].fillna("Tidak diketahui", inplace=True) juga gak mengubah apa-apa. Tulis dalam satu langkah: df.loc[df["city"].isna(), "city"] = "Tidak diketahui", atau df["city"] = df["city"].fillna(...) kayak tadi. SettingWithCopyWarning yang dulu sering muncul di pandas 1 dan 2 sudah dihapus.
💡 Kapan Polars atau DuckDB lebih tepat
pandas memuat seluruh tabel ke memori dan sebagian besar kerjanya pakai satu core CPU. Untuk ribuan sampai beberapa juta baris itu gak masalah.
- Polars: library DataFrame yang ditulis di Rust, pakai semua core, dan punya mode lazy yang mengoptimalkan seluruh langkah sebelum dijalankan. Pilih ini kalau pandas mulai lambat di jutaan baris dan kamu tetap mau gaya DataFrame.
- DuckDB: SQL langsung ke file CSV dan Parquet, bahkan ke DataFrame pandas, dan bisa mengolah data yang lebih besar dari RAM. Pilih ini kalau kerjanya
GROUP BYdanJOIN, apalagi kalau timnya lebih fasih SQL. Contohnya, kedfyang belum dibersihkan:duckdb.sql("SELECT COALESCE(city, 'Tidak diketahui') AS city, SUM(qty) AS terjual FROM (SELECT DISTINCT order_id, book_id, city, qty FROM df) GROUP BY 1 ORDER BY 1")memberi Bandung 5, Jakarta 9, Surabaya 5, Tidak diketahui 2.
Edit di Playground pada Lab membuka versi SQL dari kode pandas-nya, karena Playground pelajaran ini menjalankan SQL.
⚠️ Jebakan yang sering ketemu
groupbymembuang kunci kosong. KotaNaNhilang tanpa error.fillnadulu, ataudropna=False.89.000jadi 89. Bahkan dengandtype={"price": "int64"}. Pakaithousands=".".- Mengisi lewat dua kurung siku berantai.
df[...]["city"] = ...daninplace=Truedi satu kolom gak mengubahdfdi pandas 3.0. Pakai.locatau tulis balik kolomnya. head(1)menyembunyikan seri. Kalau dua judul sama-sama terlaris, salah satunya hilang.
TL;DR: pandas mengolah tabel (DataFrame) di memori dengan Python. read_csv membaca file; sebutkan tipenya dengan dtype dan pemisah ribuan dengan thousands=".", karena 89.000 dibaca 89. Bersihkan dengan drop_duplicates(subset=["order_id", "book_id"]), fillna("Tidak diketahui"), dan harga katalog lewat merge(..., on="book_id", how="left"), JOIN-nya pandas. groupby(...).agg(...) itu GROUP BY, tapi dia membuang kunci kosong: sebelum dibersihkan totalnya 21, bukan 23. Di pandas 3.0 Copy-on-Write berlaku, jadi df[...]["kolom"] = ... gak pernah mengubah df; pakai .loc. Untuk jutaan baris atau kerja yang SQL banget, Polars atau DuckDB lebih cocok.