Pandas Advanced

5 jam12 min baca
Tujuan

GroupBy, merge, pivot, time series. Skill yang dipakai untuk feature engineering ML.

03 — Pandas Advanced

Estimasi: 5 jam Prasyarat: 02-pandas-basics.md Tujuan: GroupBy, merge, pivot, time series. Skill yang dipakai untuk feature engineering ML.


Kenapa Materi Ini Penting?

Kalau Pandas Basics adalah "baca dan bersihkan data", Pandas Advanced adalah "ubah data jadi sinyal yang bisa dipelajari model". GroupBy, merge, dan pivot adalah trio feature engineering yang akan kamu pakai di setiap project ML serius — dari analisis sales bulanan sampai persiapan data untuk fine-tuning LLM.

Analogi: kalau Basics itu mencuci dan memotong sayur, Advanced itu memasaknya jadi hidangan yang siap saji. Tanpa skill ini, kamu masih punya bahan mentah; dengan skill ini, kamu punya menu siap dimasukkan ke model.

Di dunia kerja, groupby muncul tiap kali bos minta "rata-rata penjualan per region per bulan", merge muncul tiap kamu butuh gabungkan tabel users + orders + products, dan pivot muncul tiap kamu butuh ubah orientasi data biar sesuai input ML library. Kuasai ini, dan kamu sudah 80% siap untuk role data analyst/ML engineer entry-level.


Peta Materi Pandas Advanced

flowchart TD
    A["Clean DataFrame"] --> B["GroupBy<br/>split-apply-combine"]
    A --> C["Merge / Join<br/>gabung 2 tabel"]
    A --> D["Pivot / Melt<br/>ubah orientasi"]
    A --> E["Time Series<br/>resample, rolling"]
    B --> F["Feature Engineering"]
    C --> F
    D --> F
    E --> F
    F --> G["ML-ready Dataset"]

Bagian 1 — GroupBy (Sangat Penting)

Konsep "Split-Apply-Combine"

Analogi: GroupBy = sortir piring berdasarkan jenis. Bayangkan kamu punya tumpukan 100 piring campur (gelas, mangkok, sendok). GroupBy melakukan 3 hal:

  1. Split — pisahkan jadi tumpukan-tumpukan kecil per jenis (gelas semua, mangkok semua, dst)
  2. Apply — hitung sesuatu per tumpukan (misal "ada berapa di tiap tumpukan")
  3. Combine — gabungkan hasilnya jadi satu ringkasan

Cara Membaca Diagram:

  • Node kiri = DataFrame mentah dengan banyak baris.
  • Node "Split by 'kota'" memecah ke beberapa grup.
  • Tiap grup diaplikasi agregasi mean(harga).
  • Combine + Result = ringkasan akhir per kota.

Walkthrough Step-by-Step:

  1. Split: pisahkan baris berdasarkan kunci grup (di sini: kota).
  2. Apply: jalankan fungsi (mean, sum, count, dll) di tiap grup.
  3. Combine: susun hasil per grup jadi DataFrame ringkasan.

Analogi Sehari-hari: sortir pakaian kotor per warna, cuci masing-masing tumpukan terpisah, lalu satukan kembali untuk dilipat.

Diagram statis Mermaid sebagai fallback:

flowchart LR
    A["DataFrame<br/>(100 baris campur)"] --> B["Split<br/>by 'kota'"]
    B --> C1["Bandung"]
    B --> C2["Jakarta"]
    B --> C3["Surabaya"]
    C1 --> D1["mean(harga)"]
    C2 --> D2["mean(harga)"]
    C3 --> D3["mean(harga)"]
    D1 --> E["Combine"]
    D2 --> E
    D3 --> E
    E --> F["Result<br/>(3 baris ringkasan)"]
import pandas as pd

df = pd.DataFrame({
    "kota": ["Bandung", "Jakarta", "Bandung", "Jakarta", "Bandung"],
    "produk": ["A", "A", "B", "B", "A"],
    "harga": [100, 150, 200, 180, 120],
})

# Group by kota, hitung mean harga per kota
df.groupby("kota")["harga"].mean()
# kota
# Bandung    140.0
# Jakarta    165.0

# Multiple aggregation
df.groupby("kota")["harga"].agg(["mean", "sum", "count"])

# Multiple kolom
df.groupby(["kota", "produk"])["harga"].mean()

# Pakai dict untuk specify per kolom
df.groupby("kota").agg({
    "harga": ["mean", "max"],
    "produk": "count",
})

Custom Aggregation

def harga_range(s):
    return s.max() - s.min()

df.groupby("kota")["harga"].agg(harga_range)

# Pakai lambda
df.groupby("kota")["harga"].agg(lambda s: s.max() - s.min())

Transform (Group-wise Transform)

# Add kolom yang berisi rata-rata grup
df["mean_kota"] = df.groupby("kota")["harga"].transform("mean")

# Z-score per group
df["harga_z"] = df.groupby("kota")["harga"].transform(lambda s: (s - s.mean()) / s.std())

Filter Groups

# Hanya ambil group yang punya >= 2 records
df.groupby("kota").filter(lambda g: len(g) >= 2)

Tabel Cepat: Aggregation Functions

Function Hasil Contoh
sum total total revenue per kota
mean rata-rata rata-rata harga per produk
median nilai tengah (tahan outlier) median income per region
count jumlah non-null berapa transaksi per user
nunique jumlah unik berapa produk berbeda per kota
min, max ekstrem harga termurah/termahal per kategori
std, var spread variasi harga per produk
first, last nilai pertama/terakhir sales pertama bulan ini
agg(["mean","sum"]) multiple hitung beberapa sekaligus

Bagian 2 — Merge & Join

Analogi: Merge = gabung dua tabel berdasarkan kunci yang sama. Bayangkan kamu punya 2 buku catatan: satu daftar pelanggan (user_id, nama), satu daftar transaksi (order_id, user_id, amount). Merge = bikin satu tabel baru di mana tiap transaksi diberi "stiker" nama pelanggannya.

Visualisasi Tipe Join

Cara Membaca Diagram:

  • Dua node kiri = tabel users dan orders (sumber data).
  • Empat node tengah = tipe join: INNER, LEFT, RIGHT, OUTER.
  • Empat node kanan = hasil tiap join (subset baris yang muncul).

Walkthrough Step-by-Step:

  1. INNER: hanya baris yang match di kedua tabel.
  2. LEFT: semua user, order yang match (NaN kalau tidak ada).
  3. RIGHT: semua order, user yang match (NaN kalau tidak ada).
  4. OUTER: union — semua dari kedua tabel, NaN di yang missing.

Analogi Sehari-hari: dua daftar tamu undangan dan daftar hadir. INNER = yang diundang dan datang. LEFT = semua yang diundang (catat hadir/tidak). RIGHT = semua yang datang (catat diundang atau tidak). OUTER = total semua nama dari kedua daftar.

Diagram statis Mermaid sebagai fallback:

flowchart LR
    subgraph Inner["INNER JOIN"]
        A1["users"] --> R1["hanya<br/>yang ADA<br/>di KEDUA"]
        B1["orders"] --> R1
    end
    subgraph Left["LEFT JOIN"]
        A2["users"] --> R2["semua users<br/>+ matching orders"]
        B2["orders"] -.->|"NaN kalau tidak match"| R2
    end
    subgraph Right["RIGHT JOIN"]
        A3["users"] -.->|"NaN kalau tidak match"| R3
        B3["orders"] --> R3["semua orders<br/>+ matching users"]
    end
    subgraph Outer["OUTER JOIN"]
        A4["users"] --> R4["SEMUA dari<br/>kedua tabel<br/>(NaN di yang missing)"]
        B4["orders"] --> R4
    end

Tabel Perbandingan Merge Types

Type Hasil Kapan Pakai
inner (default) hanya baris yang match di kedua tabel analisis user yang sudah transaksi
left semua dari kiri + matching dari kanan "show me all users, with their orders if any"
right semua dari kanan + matching dari kiri jarang, bisa di-flip jadi left
outer semua dari kedua sisi audit, cari yang missing di salah satu
cross semua kombinasi (cartesian) feature engineering, jarang
users = pd.DataFrame({
    "user_id": [1, 2, 3, 4],
    "nama": ["Budi", "Ani", "Cici", "Dodi"],
})

orders = pd.DataFrame({
    "order_id": [101, 102, 103, 104],
    "user_id": [1, 2, 1, 5],
    "amount": [100, 200, 150, 300],
})

# Inner join (default)
merged = users.merge(orders, on="user_id")
# Hanya user yang ada order

# Left join
users.merge(orders, on="user_id", how="left")
# Semua user, NaN kalau tidak ada order

# Right join
users.merge(orders, on="user_id", how="right")
# Semua order, NaN kalau user tidak ada

# Outer join
users.merge(orders, on="user_id", how="outer")
# Semua dari kedua side

# Different column names
users.merge(orders, left_on="user_id", right_on="user_id")

Concat (Stack DataFrames)

df1 = pd.DataFrame({"a": [1, 2], "b": [3, 4]})
df2 = pd.DataFrame({"a": [5, 6], "b": [7, 8]})

# Vertical (default)
pd.concat([df1, df2])
pd.concat([df1, df2], ignore_index=True)

# Horizontal
pd.concat([df1, df2], axis=1)

Bagian 3 — Pivot & Melt

Analogi: Pivot = ubah orientasi tabel seperti memutar kertas 90 derajat. Data yang tadinya "panjang ke bawah" (long format) jadi "lebar ke samping" (wide format). Melt = kebalikannya, ubah dari lebar jadi panjang.

Cara Membaca Diagram:

  • Dua node atas = bentuk LONG (banyak baris) dan WIDE (banyak kolom).
  • Edge pivot → = transformasi long → wide.
  • Edge ← melt = transformasi wide → long.
  • Dua node bawah = use case ideal masing-masing format.

Walkthrough Step-by-Step:

  1. Mulai dari long format (1 baris = 1 observasi dengan kategori).
  2. pivot() mengangkat kategori jadi kolom → wide format untuk laporan.
  3. melt() mengembalikan ke long format saat butuh plotting (seaborn, dll).

Analogi Sehari-hari: rapor sekolah. Long = "satu baris per murid per mata pelajaran". Wide = "satu baris per murid, kolom per pelajaran". Sama isinya, beda layout.

Diagram statis Mermaid sebagai fallback:

flowchart LR
    subgraph Long["LONG (banyak baris, sedikit kolom)"]
        L["tanggal | produk | sales<br/>2026-01 | A | 100<br/>2026-01 | B | 150<br/>2026-02 | A | 110<br/>2026-02 | B | 180"]
    end
    subgraph Wide["WIDE (sedikit baris, banyak kolom)"]
        W["tanggal | A | B<br/>2026-01 | 100 | 150<br/>2026-02 | 110 | 180"]
    end
    L -->|"pivot"| W
    W -->|"melt"| L

Kapan pakai mana?

  • Long format = bagus untuk plotting (seaborn cinta long format) dan storage database
  • Wide format = bagus untuk report ke human dan input beberapa ML library

Pivot

Long → Wide format.

df = pd.DataFrame({
    "tanggal": ["2026-01", "2026-01", "2026-02", "2026-02"],
    "produk": ["A", "B", "A", "B"],
    "sales": [100, 150, 110, 180],
})

df.pivot(index="tanggal", columns="produk", values="sales")
# produk      A    B
# tanggal
# 2026-01   100  150
# 2026-02   110  180

Pivot Table (dengan Aggregation)

df.pivot_table(
    index="tanggal",
    columns="produk",
    values="sales",
    aggfunc="sum"     # atau mean, max, dll
)

Melt

Wide → Long format (kebalikan pivot).

df_wide = pd.DataFrame({
    "tanggal": ["2026-01", "2026-02"],
    "A": [100, 110],
    "B": [150, 180],
})

df_long = df_wide.melt(id_vars=["tanggal"], var_name="produk", value_name="sales")
#   tanggal produk  sales
# 0 2026-01      A    100
# 1 2026-02      A    110
# 2 2026-01      B    150
# 3 2026-02      B    180

Bagian 4 — Time Series

Datetime Index

df = pd.DataFrame({
    "tanggal": pd.date_range("2026-01-01", periods=100, freq="D"),
    "value": np.random.randn(100).cumsum(),
})
df.set_index("tanggal", inplace=True)

Resample (Aggregate by Time Period)

# Daily → Weekly
df.resample("W").mean()       # weekly mean
df.resample("W").sum()        # weekly sum
df.resample("M").mean()       # monthly
df.resample("Q").mean()       # quarterly

# Custom aggregation
df.resample("W").agg({"value": ["mean", "max", "min"]})

Rolling Window

df["rolling_7d"] = df["value"].rolling(window=7).mean()       # 7-day moving avg
df["rolling_30d"] = df["value"].rolling(window=30).mean()
df["rolling_std"] = df["value"].rolling(window=7).std()

# Expanding window
df["cumulative_mean"] = df["value"].expanding().mean()

Shift & Diff

df["yesterday"] = df["value"].shift(1)
df["change"] = df["value"].diff()        # value - shift(1)
df["pct_change"] = df["value"].pct_change()

Time-based Filter

df["2026-01"]                    # January 2026
df["2026-01":"2026-03"]          # range
df.between_time("09:00", "17:00")  # untuk intraday

Bagian 5 — Categorical Data

df["kategori"] = df["kategori"].astype("category")
# Hemat memori + faster operations

# Ordering
df["size"] = pd.Categorical(df["size"], categories=["S", "M", "L", "XL"], ordered=True)
df.sort_values("size")

One-Hot Encoding (Untuk ML)

encoded = pd.get_dummies(df["kota"], prefix="kota")
df = pd.concat([df, encoded], axis=1)

# Drop original
df.drop("kota", axis=1, inplace=True)

Bagian 6 — Multi-Index

df = pd.DataFrame({
    "kota": ["Bandung", "Bandung", "Jakarta", "Jakarta"],
    "produk": ["A", "B", "A", "B"],
    "sales": [100, 150, 200, 180],
}).set_index(["kota", "produk"])

# Akses
df.loc["Bandung"]                # all Bandung
df.loc[("Bandung", "A")]         # specific
df.loc[("Bandung", "A"), "sales"]

# Cross-section
df.xs("A", level="produk")       # all kota dengan produk A

Bagian 7 — Window Functions

df["rank"] = df["sales"].rank()
df["rank_per_kota"] = df.groupby("kota")["sales"].rank()
df["pct_rank"] = df["sales"].rank(pct=True)

Bagian 8 — Performance Tips

Vectorize Don't Loop

# JELEK
for i, row in df.iterrows():
    df.at[i, "new"] = row["a"] + row["b"]

# BAIK
df["new"] = df["a"] + df["b"]

Categorical Saves Memory

df["status"] = df["status"].astype("category")

Use Vectorized String Ops

df["nama"].str.upper()           # bukan apply

Avoid Iterrows untuk Large Data

# JELEK (untuk data besar)
for i, row in df.iterrows():
    process(row)

# BAIK
df.apply(process, axis=1)        # masih OK

# LEBIH BAIK
df["result"] = some_vectorized_op(df["col"])

Read Big CSV in Chunks

chunks = pd.read_csv("big.csv", chunksize=10000)
for chunk in chunks:
    process(chunk)

Common Mistakes / FAQ

1. GroupBy Lupa Reset Index

result = df.groupby("kota")["harga"].mean()
# result punya 'kota' sebagai INDEX, bukan kolom

result = df.groupby("kota", as_index=False)["harga"].mean()
# atau .reset_index() — sekarang 'kota' jadi kolom biasa

2. Merge dengan Banyak Duplikat → Cartesian Explosion

Kalau kunci tidak unik di salah satu sisi, hasil merge bisa berlipat ganda.

# 3 user, masing-masing 2 order
# tapi user_id kebetulan duplikat di tabel users → tiap row x 2!
result = users.merge(orders, on="user_id", validate="one_to_many")
# validate akan raise error kalau asumsi tidak benar

Selalu pakai validate="one_to_one" / "one_to_many" / "many_to_one" di kode produksi.

3. Lupa Reset Index Setelah Concat

df = pd.concat([df1, df2])
# index jadi 0,1,2,...,0,1,2 — duplikat!
df = pd.concat([df1, df2], ignore_index=True)

4. Pivot Error karena Duplikat Kombinasi

# df.pivot tidak boleh ada (index, columns) duplikat
# pakai pivot_table dengan aggfunc kalau ada duplikat
df.pivot_table(index="tanggal", columns="produk", values="sales", aggfunc="sum")

5. Resample Tanpa DatetimeIndex → Error

# df.resample("W") cuma jalan kalau index = DatetimeIndex
df["tanggal"] = pd.to_datetime(df["tanggal"])
df = df.set_index("tanggal")
df.resample("W").mean()

6. iterrows di Data Besar

# JELEK — bisa 1000x lebih lambat
for i, row in df.iterrows():
    df.at[i, "new"] = row["a"] * row["b"]

# BAIK
df["new"] = df["a"] * df["b"]

7. pd.get_dummies Tanpa drop_first di Linear Model

# Untuk linear regression, hindari multicollinearity
encoded = pd.get_dummies(df["kota"], prefix="kota", drop_first=True)

Di tree-based model (XGBoost, RF) tidak masalah, drop_first opsional.


Cek Pemahaman

  • Bisa pakai groupby dengan multiple aggregation?
  • Tahu beda merge inner/left/right/outer?
  • Bisa pivot dan melt?
  • Bisa resample time series?
  • Tahu rolling window?
  • Bisa one-hot encoding untuk ML?

Challenge 4.3

Challenge 1 — Sales Analysis

df = pd.DataFrame({
    "tanggal": pd.date_range("2026-01-01", periods=180),
    "kota": np.random.choice(["Bandung", "Jakarta", "Surabaya"], 180),
    "produk": np.random.choice(["A", "B", "C"], 180),
    "qty": np.random.randint(1, 20, 180),
    "harga": np.random.choice([100, 150, 200], 180),
})
df["total"] = df["qty"] * df["harga"]
  1. Total revenue per kota
  2. Top 3 produk per kota
  3. Monthly revenue trend (resample)
  4. Rolling 7-day avg revenue
  5. Pivot: kota × produk = total

Challenge 2 — User-Order Join

Bikin 2 DataFrame: users, orders. Lakukan:

  • Inner join, left join
  • Find users tanpa order
  • Total spent per user
  • Top 5 spender

Challenge 3 — Time Series Forecast (Naive)

Dataset: daily temperature 2 tahun.

  1. Resample weekly
  2. Plot rolling 30-day mean
  3. Naive forecast: next day = 7-day rolling mean
  4. Hitung MAE

Challenge 4 — Feature Engineering for ML

Dataset Titanic:

  1. Buat fitur family_size, is_alone
  2. Bin age ke kategori (anak, dewasa, lansia)
  3. Extract title dari nama (Mr, Mrs, dll)
  4. One-hot encode kategorik
  5. Save processed CSV untuk siap input ML

Selanjutnya: 04-matplotlib.md