Apa yang akan Abbas kuasai bulan ini?

Mindset bulan ini: pandas bukan pengganti Excel. Ini adalah cara berpikir baru tentang data sebagai objek yang bisa dimanipulasi secara programatik. Analisis yang butuh 1 jam di Sheets bisa selesai dalam 5 menit dengan pandas.
  • Membuat dan membaca DataFrame dari CSV, Excel, dan dictionary Python
  • Memilih data dengan iloc (posisi numerik) vs loc (label nama) tanpa bingung
  • Filter baris dengan boolean indexing dan query() yang kompleks
  • Transformasi kolom dengan apply(), lambda, dan string methods (.str.)
  • Parse dan manipulasi data tanggal/waktu dengan datetime dan resample()
  • Aggregasi data dengan groupby() dan agg() seperti pivot table otomatis
  • Menggabungkan tabel dengan merge() (ekuivalen JOIN di SQL) dan concat()
  • Mendeteksi dan menangani missing values, outlier IQR, dan duplikat data

Minggu 1: DataFrame Fundamentals

Minggu 1 5 Hari: Anatomi DataFrame dari Dalam ke Luar
Hari 1

Series vs DataFrame: Dua Senjata Utama

Konsep: Series = satu kolom. DataFrame = tabel penuh. Hampir semua pekerjaan pandas bekerja dengan DataFrame.
import pandas as pd

# Dari dictionary
df = pd.DataFrame({
    'nama': ['Abbas', 'Budi', 'Cici'],
    'nilai': [88, 75, 92]
})

# Dari file
df = pd.read_csv('nilai.csv')
df = pd.read_excel('data.xlsx')
Trigger: Apa yang terjadi kalau dua kolom di dictionary punya jumlah elemen berbeda? Coba dan baca error-nya. Error ini mengajarkan sesuatu fundamental tentang bagaimana pandas bekerja.
Hari 2

iloc vs loc: Dua Cara Memilih Data

Aturan gampang: iloc = angka posisi (0, 1, 2...). loc = nama label (kolom, index). Kalau ragu, pakai loc dengan nama kolom.
# iloc: posisi [baris, kolom]
df.iloc[0]           # baris pertama
df.iloc[1:4, :2]   # baris 1-3, 2 kolom pertama

# loc: nama label
df.loc[0, 'nama']        # baris 0, kolom 'nama'
df.loc[:, ['nama', 'nilai']]  # semua baris, 2 kolom
Challenge: Buat DataFrame 5 siswa. Pilih: (1) siswa ke-3. (2) Kolom nilai dan kelas semua siswa. (3) Siswa 2-4 kolom nama.
Hari 3

Ritual Eksplorasi: head(), info(), describe()

Ritual wajib: Setiap dapat dataset baru, jalankan 4 baris ini sebelum apapun.
df.head(5)      # 5 baris pertama
df.info()       # tipe data, non-null count, memori
df.describe()   # statistik kolom numerik
df.shape        # (baris, kolom)
df.dtypes       # tipe setiap kolom
df.isnull().sum()  # jumlah NaN per kolom
Hari 4

Boolean Filtering: WHERE ala SQL

# Filter tunggal
df[df['nilai'] > 80]

# Ganda: & = AND, | = OR, ~ = NOT
df[(df['nilai'] > 80) & (df['kelas'] == '11A')]

# isin() dan str.contains()
df[df['kelas'].isin(['11A', '11B'])]
df[df['nama'].str.contains('Ab', na=False)]

# query() lebih mudah dibaca
df.query('nilai > 80 and kelas == "11A"')
Hari 5

Latihan: Eksplorasi Dataset Titanic

Load dataset: pd.read_csv('https://raw.githubusercontent.com/datasciencedojo/datasets/master/titanic.csv')

Jawab 5 pertanyaan ini dengan pandas:

  1. Berapa total penumpang? Berapa selamat vs tidak?
  2. Persen penumpang perempuan yang selamat vs laki-laki?
  3. Penumpang termuda dan tertua?
  4. Rata-rata harga tiket per kelas (1st/2nd/3rd)?
  5. Kolom mana yang punya missing values dan berapa banyak?
Trigger: Temuan apa yang paling mengejutkan? Pola apa yang tidak Abbas duga? Tulis 3 insight dalam kalimat lengkap.

Minggu 2: Transformasi Data

Minggu 2 5 Hari: apply, lambda, string, datetime
Hari 1

Tambah dan Ubah Kolom

# Kolom baru dari kolom lain
df['total'] = df['uts'] * 0.4 + df['uas'] * 0.6

# Kolom kategori dengan lambda
df['grade'] = df['nilai'].apply(
    lambda x: 'A' if x >= 85 else 'B' if x >= 70 else 'C'
)

# Hapus kolom dan rename
df = df.drop(columns=['kol_tidak_perlu'])
df = df.rename(columns={'old': 'new'})
Hari 2

apply() per Baris: Akses Banyak Kolom Sekaligus

def kategori_siswa(row):
    if row['nilai'] >= 85 and row['hadir'] >= 90:
        return 'Berprestasi'
    elif row['nilai'] < 60:
        return 'Perlu perhatian'
    return 'Normal'

df['kategori'] = df.apply(kategori_siswa, axis=1)
Challenge: Buat kolom 'status_kelulusan': "Lulus dengan pujian" (nilai 90+, hadir 95+), "Lulus" (nilai 70+), "Tidak Lulus" (lainnya).
Hari 3

String Operations: .str.

df['nama'].str.upper()
df['nama'].str.strip()
df['nama'].str.contains('Abbas', na=False)
df['nama'].str.replace(',', '')
df['nama'].str.split(' ').str[0]  # ambil kata pertama
Hari 4

Datetime: Tanggal Bukan Sekadar String

# Parse string jadi datetime
df['tgl'] = pd.to_datetime(df['tgl'])

# Ekstrak komponen
df['tahun'] = df['tgl'].dt.year
df['bulan'] = df['tgl'].dt.month
df['hari']  = df['tgl'].dt.day_name()

# Resample per bulan
df.set_index('tgl').resample('M')['revenue'].sum()
Hari 5

Latihan: Bersihkan Dataset Penjualan

Dataset penjualan fiktif dengan 5 masalah sengaja dibuat:

  1. Kolom tanggal masih string "2025-01-15" (parse ke datetime)
  2. Nama produk campur huruf besar-kecil (standardisasi ke Title Case)
  3. Harga pakai titik: "1.500" (ubah ke integer 1500)
  4. Tambah kolom: bulan, tahun, total = qty * harga
  5. Tambah kategori harga: "Murah", "Sedang", "Mahal"

Minggu 3: groupby dan merge

Minggu 3 5 Hari: groupby, merge, concat, pivot
Hari 1

groupby() + agg(): Pivot Table Otomatis

# Sederhana
df.groupby('kelas')['nilai'].mean()

# Banyak aggregasi
df.groupby('kelas').agg({
    'nilai': ['mean', 'max', 'count'],
    'hadir': 'mean'
}).reset_index()

# Group by dua kolom
df.groupby(['kelas', 'semester'])['nilai'].mean()
Hari 2

merge(): JOIN Tabel Persis seperti SQL

# INNER JOIN
result = pd.merge(df_siswa, df_nilai,
                  on='id_siswa', how='inner')

# LEFT JOIN
result = pd.merge(df_siswa, df_nilai,
                  on='id_siswa', how='left')

# Key berbeda nama
result = pd.merge(df_a, df_b,
    left_on='id', right_on='student_id')
Challenge: Buat 3 DataFrame: siswa, nilai, guru. Join ketiganya: nama siswa, kelas, nilai, nama guru kelas.
Hari 3

concat(): Gabungkan Vertikal

# Gabungkan baris dari banyak file
df_semua = pd.concat(
    [df_jan, df_feb, df_mar],
    ignore_index=True
)
Hari 4

pivot_table() dan crosstab()

pivot = df.pivot_table(
    values='nilai',
    index='kelas',
    columns='mapel',
    aggfunc='mean',
    fill_value=0
)

ct = pd.crosstab(
    df['kelas'], df['grade'], margins=True
)
Hari 5

Latihan: Analisis E-Commerce

Buat dataset e-commerce 50 baris (tanggal, produk, kategori, qty, harga, kota). Jawab:

  1. Total revenue per kategori
  2. Produk terlaris per bulan
  3. Kota dengan rata-rata order value tertinggi
  4. Pivot: revenue per kategori x bulan

Minggu 4: Data Cleaning Lanjutan

Minggu 4 5 Hari: Missing Values, Outlier, Mini Project
Hari 1

Missing Values: 3 Strategi Penanganan

Tiga strategi: (1) Drop baris/kolom. (2) Fill dengan konstanta/median/mode. (3) Impute dengan prediksi. Pilih berdasarkan seberapa banyak data hilang dan mengapa.
# Deteksi
df.isnull().sum()
df.isnull().mean() * 100  # persen

# Drop
df.dropna(subset=['nilai'])  # hapus jika 'nilai' NaN

# Fill
df['nilai'].fillna(df['nilai'].median(), inplace=True)
df['kota'].fillna('Tidak Diketahui', inplace=True)
Hari 2

Deteksi Outlier: Metode IQR

Q1 = df['nilai'].quantile(0.25)
Q3 = df['nilai'].quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR

outliers = df[(df['nilai'] < lower) | (df['nilai'] > upper)]
print(f"{len(outliers)} outlier ditemukan")
Trigger: Apakah outlier selalu error? Bayangkan data gaji dengan Bill Gates di dalamnya. Apakah itu error yang harus dihapus, atau informasi valid?
Hari 3

Duplikat dan Anomali Kategorik

# Duplikat
df.duplicated().sum()
df = df.drop_duplicates(subset=['id_siswa'], keep='last')

# Anomali teks: 'jakarta', 'Jakarta', 'JAKARTA'
df['kota'].value_counts()
df['kota'] = df['kota'].str.title().str.strip()
Hari 4

Binning: Ubah Numerik jadi Kategori

# pd.cut: batas manual
df['grade'] = pd.cut(df['nilai'],
    bins=[0, 59, 74, 84, 100],
    labels=['D', 'C', 'B', 'A'])

# pd.qcut: batas berdasarkan quantile
df['kuartil'] = pd.qcut(df['nilai'], q=4,
    labels=['Q1', 'Q2', 'Q3', 'Q4'])
Hari 5 (Mini Project)

End-to-End Cleaning Dataset Publik Indonesia

Dataset: Download dari bps.go.id atau data.go.id: jumlah penduduk per provinsi, data ekspor-impor, atau data pendidikan. Dataset nyata selalu "kotor".

Pipeline yang harus Abbas selesaikan:

  1. Load data, cek shape, dtypes, missing values
  2. Parse kolom tanggal jika ada
  3. Standardisasi nama kolom (lowercase, underscore)
  4. Handle missing values dengan strategi yang tepat
  5. Hapus duplikat jika ada
  6. Deteksi dan catat outlier (jangan langsung hapus)
  7. Export ke CSV bersih: data_clean.csv
  8. Laporan singkat: apa yang ditemukan dan bagaimana ditangani
Challenge Final: Dari data bersih, buat 5 insight menggunakan groupby() dan analisis minggu ini.

Buku Pendukung Bulan 5

Python for Data Analysis, 3rd Edition
Wes McKinney (pencipta pandas). Referensi utama. Dari dasar hingga advanced.
Pandas Cookbook
Theodore Petrou. Format problem-solution, sangat praktis untuk masalah sehari-hari.
Python Data Science Handbook
Jake VanderPlas. Gratis online di jakevdp.github.io. Covers numpy, pandas, matplotlib, sklearn.
Data Wrangling with Python
Jacqueline Kazil. Fokus cleaning dan transformasi data messy yang realistis.

Tools yang Digunakan

pandas 2.x numpy openpyxl pyarrow Jupyter Notebook VS Code
Install sekaligus:
pip install pandas numpy openpyxl pyarrow jupyter

Tips Praktis dan Jebakan Pemula Pandas

Jebakan #1: SettingWithCopyWarning yang Membingungkan

Kalau Abbas memfilter DataFrame lalu mengubah nilainya, pandas kadang tidak menyimpan perubahan itu karena yang dimodifikasi adalah salinan sementara, bukan DataFrame asli. Ini tidak error, tapi hasilnya tidak sesuai harapan.

# SALAH: mungkin tidak tersimpan
df_baru = df[df['nilai'] > 70]
df_baru['grade'] = 'Lulus'  # SettingWithCopyWarning!

# BENAR: tambahkan .copy() setelah filter
df_baru = df[df['nilai'] > 70].copy()
df_baru['grade'] = 'Lulus'  # aman

Aturan Abbas: setiap kali memfilter DataFrame untuk diolah lebih lanjut, selalu tambahkan .copy() di akhir.

Jebakan #2: inplace=True Tidak Bisa Di-chain dan Sulit Di-debug

Banyak tutorial mengajarkan inplace=True, tapi ini membuat kode susah dibaca dan tidak bisa di-chain. Lebih baik assign kembali ke variabel yang sama.

# HINDARI: inplace=True
df.dropna(inplace=True)
df.reset_index(drop=True, inplace=True)

# LEBIH BAIK: assign kembali
df = df.dropna().reset_index(drop=True)

# Kenapa inplace berbahaya:
# df.sort_values('nilai', inplace=True).head()  -> TypeError!
# karena inplace=True mengembalikan None, bukan DataFrame
Tip #3: Selalu Cek dtype Sebelum Operasi String atau Numerik

Kalau kolom terbaca sebagai object padahal isinya angka, operasi matematika akan error. Sebaliknya, kalau kolom angka terbaca sebagai string, .str. tidak akan bekerja. Cek dulu sebelum jalan.

# Cek semua tipe sekaligus
print(df.dtypes)

# Konversi paksa ke numerik (salah jadi NaN, bukan error)
df['harga'] = pd.to_numeric(df['harga'], errors='coerce')

# Konversi ke string untuk operasi .str.
df['kode'] = df['kode'].astype('str')

# Diagnosa: kenapa harga tidak bisa dijumlah?
df['harga'].unique()[:5]  # cari nilai aneh seperti 'N/A', '-', ''
Jebakan #4: NaN Mengubah Kolom Integer Jadi Float

Ini mengejutkan banyak pemula. Kalau kolom integer (bilangan bulat) punya satu NaN saja, pandas otomatis mengubah seluruh kolom jadi float. Angka 5 menjadi 5.0.

import numpy as np

df = pd.DataFrame({'nilai': [85, 90, np.nan, 75]})
print(df['nilai'].dtype)  # float64, bukan int!

# Solusi pandas 1.0+: gunakan nullable integer
df['nilai'] = df['nilai'].astype('Int64')  # kapital I
print(df['nilai'].dtype)  # Int64, dengan NaN tetap ada

# Atau isi NaN dulu, baru convert ke int
df['nilai'] = df['nilai'].fillna(0).astype('int')
Tip #5: Selalu reset_index() Setelah groupby

Hasil groupby menjadikan kolom grup sebagai index (baris label), bukan kolom biasa. Ini membuat operasi selanjutnya jadi aneh. Selalu tambahkan reset_index() agar hasilnya rapi.

# Tanpa reset_index: 'kelas' jadi index, susah dipakai
hasil = df.groupby('kelas')['nilai'].mean()
print(hasil.index)  # Index(['11A', '11B', '11C'])

# Dengan reset_index: 'kelas' jadi kolom normal
hasil = df.groupby('kelas')['nilai'].mean().reset_index()
print(hasil.columns)  # ['kelas', 'nilai'] - mudah dipakai!

# Rename sekalian supaya rapi
hasil = hasil.rename(columns={'nilai': 'rata_nilai'})
Jebakan #6: merge() Bisa Menggandakan Baris Tanpa Disadari

Kalau kolom yang dipakai sebagai kunci JOIN punya nilai duplikat di kedua tabel, hasilnya akan lebih banyak dari yang diharapkan. Abbas harus selalu cek shape sebelum dan sesudah merge.

# Cek sebelum merge
print("df_a:", df_a.shape)
print("df_b:", df_b.shape)

result = pd.merge(df_a, df_b, on='id', how='left')

# Cek sesudah merge
print("result:", result.shape)
# Kalau result > df_a, berarti ada duplikat di df_b!

# Diagnosa: cek duplikat di kolom kunci
print(df_b['id'].duplicated().sum(), "duplikat di df_b")
Tip #7: Gunakan value_counts() untuk Audit Data Kategorik

Sebelum groupby atau analisis kategorik, Abbas harus tahu nilai unik apa saja yang ada dan seberapa sering muncul. value_counts() adalah alat diagnostik terkuat untuk ini.

# Lihat distribusi + persentase
df['kota'].value_counts()
df['kota'].value_counts(normalize=True) * 100  # persen

# Tangkap typo: 'jakarta', 'Jakarta', 'JAKARTA', 'Jakartaa'
df['kota'].value_counts().tail(10)  # nilai langka biasanya typo

# Cek nilai dengan frekuensi sangat rendah (kemungkinan error)
rare = df['kota'].value_counts()
rare[rare < 3]  # nilai yang muncul kurang dari 3x
Tip #8: Baca CSV dengan dtype Eksplisit untuk Menghindari Kejutan

Pandas menebak tipe data secara otomatis saat read_csv. Tebakan ini kadang salah, misalnya kode produk "007" dibaca sebagai angka 7, atau kolom campuran teks dan angka dibaca object. Lebih aman tentukan dtype dari awal.

# Tentukan dtype saat membaca
df = pd.read_csv('penjualan.csv', dtype={
    'kode_produk': 'str',    # jangan jadi angka
    'qty': 'int',
    'harga': 'float'
}, parse_dates=['tanggal'])  # parse datetime sekaligus

# Untuk file besar: baca sample dulu
df_sample = pd.read_csv('big_file.csv', nrows=100)
print(df_sample.dtypes)  # lihat tipe yang dideteksi

Kebiasaan ini juga membuat kode lebih cepat karena pandas tidak perlu menebak tipe dari setiap kolom.

Latihan Tambahan: Level Lebih Menantang

Cara menggunakan latihan ini: Kerjakan satu latihan per sesi. Jangan langsung lihat jawaban. Kalau stuck lebih dari 20 menit, baru lihat petunjuk. Tujuannya adalah terbiasa membaca error dan mencari solusinya sendiri.
Latihan A Titanic Survival Analysis: Lebih Dalam
Dataset
url = 'https://raw.githubusercontent.com/datasciencedojo/datasets/master/titanic.csv'
df = pd.read_csv(url)
df.head()
Soal 1

Buat Kolom Kategori Usia

Buat kolom baru age_group berdasarkan kolom Age. Gunakan pd.cut() dengan batas: 0-12 = "Anak", 13-17 = "Remaja", 18-59 = "Dewasa", 60+ = "Lansia". Hati-hati dengan NaN di kolom Age.

# Petunjuk: isi NaN dulu sebelum pd.cut
df['Age'] = df['Age'].fillna(df['Age'].median())

df['age_group'] = pd.cut(
    df['Age'],
    bins=[0, 12, 17, 59, 100],
    labels=['Anak', 'Remaja', 'Dewasa', 'Lansia']
)
Soal 2

Survival Rate per Kombinasi Gender + Kelas

Buat pivot table yang menunjukkan rata-rata survival rate untuk setiap kombinasi Sex dan Pclass. Hasilnya harus berbentuk tabel 2x3. Temukan pola: siapa yang punya survival rate tertinggi dan terendah?

pivot = df.pivot_table(
    values='Survived',
    index='Sex',
    columns='Pclass',
    aggfunc='mean'
).round(2)
print(pivot)
Soal 3 (Tantangan)

Keluarga Besar vs Keluarga Kecil: Siapa Lebih Selamat?

Kolom SibSp (saudara/pasangan) dan Parch (orang tua/anak) menunjukkan ukuran keluarga. Buat kolom family_size = SibSp + Parch + 1 (diri sendiri). Lalu buat kolom is_alone = 1 jika family_size == 1. Bandingkan survival rate antara yang sendirian vs yang punya keluarga.

df['family_size'] = df['SibSp'] + df['Parch'] + 1
df['is_alone'] = (df['family_size'] == 1).astype('int')

# Bandingkan survival rate
df.groupby('is_alone')['Survived'].mean().round(2)
Trigger: Apakah hasilnya sesuai dugaan Abbas? Coba cari tahu juga: apakah keluarga SANGAT besar (lebih dari 4 orang) justru survival rate-nya lebih rendah? Kenapa menurut Abbas?
Latihan B E-Commerce Multi-Tabel: Orders, Products, Customers
Setup Data

Buat 3 Tabel Sendiri

import pandas as pd
import numpy as np

np.random.seed(42)
n = 60

df_orders = pd.DataFrame({
    'order_id': range(1001, 1001 + n),
    'customer_id': np.random.randint(1, 11, n),
    'product_id': np.random.randint(1, 6, n),
    'qty': np.random.randint(1, 10, n),
    'tanggal': pd.date_range('2025-01-01', periods=n, freq='3D')
})

df_products = pd.DataFrame({
    'product_id': [1, 2, 3, 4, 5],
    'nama_produk': ['Buku', 'Pensil', 'Tas', 'Penggaris', 'Penghapus'],
    'kategori': ['Tulis', 'Tulis', 'Aksesoris', 'Tulis', 'Tulis'],
    'harga': [25000, 3000, 75000, 5000, 2000]
})

df_customers = pd.DataFrame({
    'customer_id': range(1, 11),
    'nama': ['Abbas', 'Budi', 'Cici', 'Dani', 'Eka',
              'Fani', 'Gita', 'Hadi', 'Ika', 'Joko'],
    'kota': ['Jakarta', 'Bandung', 'Surabaya', 'Jakarta', 'Medan',
             'Bandung', 'Jakarta', 'Yogyakarta', 'Surabaya', 'Medan']
})
Soal B1

Join Ketiga Tabel Menjadi Satu

Gabungkan df_orders, df_products, dan df_customers sehingga setiap baris order memiliki: nama pelanggan, kota, nama produk, kategori, harga, qty, dan total (qty * harga). Cek shape hasil: harus 60 baris.

# Join bertahap: orders + products dulu
df = pd.merge(df_orders, df_products, on='product_id', how='left')

# Lalu gabung dengan customers
df = pd.merge(df, df_customers, on='customer_id', how='left')

# Buat kolom total
df['total'] = df['qty'] * df['harga']

print(df.shape)   # harus (60, ...)
df.head()
Soal B2

Analisis Lengkap dari Data Gabungan

Dari DataFrame gabungan, jawab pertanyaan ini menggunakan groupby dan pivot:

  1. Siapa pelanggan dengan total belanja terbesar? (top 3)
  2. Produk apa yang menghasilkan revenue terbesar?
  3. Kota mana yang rata-rata order value-nya paling tinggi?
  4. Buat pivot: total revenue per kota vs kategori produk
  5. Bulan mana yang penjualan paling tinggi?
# Ekstrak bulan dari kolom tanggal dulu
df['bulan'] = df['tanggal'].dt.to_period('M')

# Contoh: top 3 pelanggan
top3 = df.groupby('nama')['total'].sum().sort_values(ascending=False).head(3)
print(top3)
Challenge: Buat fungsi laporan_bulanan(df, bulan) yang menerima nama bulan dan mengembalikan: total revenue, jumlah order, produk terlaris, dan pelanggan terbanyak belanja di bulan tersebut.
Latihan C Time Series: Analisis Tren Penjualan Harian
Konteks

Latihan ini mensimulasikan analisis yang sangat umum di dunia kerja: Abbas mendapat data penjualan harian selama 1 tahun dan diminta membuat laporan tren per minggu dan per bulan.

Setup Data
# Buat data penjualan harian 1 tahun
import pandas as pd
import numpy as np

np.random.seed(7)
dates = pd.date_range('2025-01-01', '2025-12-31', freq='D')
df_ts = pd.DataFrame({
    'tanggal': dates,
    'penjualan': np.random.randint(50, 300, len(dates)) +
                 (np.sin(np.arange(len(dates)) * 2 * np.pi / 30) * 50).astype('int')
})
Soal C1-C3

Resample dan Rolling Average

  1. Set kolom tanggal sebagai index dengan set_index('tanggal')
  2. Resample ke mingguan: total penjualan per minggu
  3. Resample ke bulanan: total penjualan per bulan
  4. Hitung rolling average 7 hari untuk menghaluskan fluktuasi harian
  5. Temukan: bulan dengan penjualan tertinggi dan terendah
df_ts = df_ts.set_index('tanggal')

# Resample mingguan dan bulanan
mingguan = df_ts.resample('W')['penjualan'].sum()
bulanan  = df_ts.resample('ME')['penjualan'].sum()

# Rolling average 7 hari
df_ts['rolling_7d'] = df_ts['penjualan'].rolling(window=7).mean()

# Bulan terbaik
print("Bulan terbaik:", bulanan.idxmax())
print("Bulan terlemah:", bulanan.idxmin())
Catatan: Di pandas 2.x, gunakan 'ME' (Month End) bukan 'M' yang sudah deprecated untuk resample. Kalau muncul FutureWarning, ini penyebabnya.
Boss Level Pipeline Cleaning End-to-End: Dataset Kotor Realistis
Skenario

Abbas mendapat file CSV dari rekan yang mengisi data manual selama 6 bulan. File ini punya semua jenis masalah yang sering ditemui di dunia nyata. Tugas Abbas: buat DataFrame akhir yang bersih dan bisa langsung dianalisis.

Buat Dataset Kotor
# Dataset kotor dengan 8 masalah berbeda
data_kotor = {
    'tanggal': ['2025-01-05', '05/02/2025', '2025-03-10',
                 '2025-01-05', '2025-04-20', 'tidak tahu'],
    'produk':   ['buku tulis', 'BUKU TULIS', 'Buku Tulis',
                 'buku tulis', 'Pensil  ', 'pensil'],
    'harga':    ['25.000', '25000', '25,000',
                 '25.000', '3000', 'N/A'],
    'qty':      [2, 5, 3, 2, 10, None],
    'kota':     ['jakarta', 'JAKARTA', 'Jakartaa',
                 'jakarta', 'bandung', 'Bandung']
}
df_kotor = pd.DataFrame(data_kotor)
Pipeline Cleaning
# Step 1: Identifikasi semua masalah
print(df_kotor.dtypes)
print(df_kotor.isnull().sum())
print(df_kotor.duplicated().sum())

# Step 2: Parse tanggal (toleransi format campur)
df_kotor['tanggal'] = pd.to_datetime(df_kotor['tanggal'],
                             dayfirst=False, errors='coerce')

# Step 3: Bersihkan harga (hapus titik dan koma, convert)
df_kotor['harga'] = (df_kotor['harga']
    .str.replace('.', '', regex=False)
    .str.replace(',', '', regex=False)
)
df_kotor['harga'] = pd.to_numeric(df_kotor['harga'], errors='coerce')

# Step 4: Standardisasi teks
df_kotor['produk'] = df_kotor['produk'].str.strip().str.title()
df_kotor['kota']   = df_kotor['kota'].str.strip().str.title()

# Step 5: Hapus baris dengan data kritis yang hilang
df_bersih = df_kotor.dropna(subset=['tanggal', 'harga'])

# Step 6: Hapus duplikat (baris 0 dan 3 identik)
df_bersih = df_bersih.drop_duplicates().reset_index(drop=True)

# Step 7: Isi qty yang kosong dengan median
df_bersih['qty'] = df_bersih['qty'].fillna(df_bersih['qty'].median())

# Step 8: Buat kolom total dan export
df_bersih['total'] = df_bersih['qty'] * df_bersih['harga']
df_bersih.to_csv('data_bersih.csv', index=False)

print("Data bersih:", df_bersih.shape)
df_bersih
Refleksi Akhir: Ada 8 masalah yang disengaja dalam dataset kotor di atas. Abbas bisa temukan berapa masalah sebelum melihat pipeline? Dan dari pipeline yang sudah ada, langkah mana yang paling sering Abbas lupa dalam pekerjaan sehari-hari?

Studi Kasus: Analisis Nilai Sekolah dari Nol ke Insight

Ini adalah latihan terpadu yang menggabungkan semua yang Abbas pelajari bulan ini. Dataset: 200 siswa, 5 mata pelajaran, selama 2 semester.

Dataset: Buat sendiri dengan kode di bawah, atau download dari Kaggle "Students Performance in Exams".
Studi Kasus Pipeline Lengkap: 8 Langkah
Langkah 1

Generate Dataset Realistis

import pandas as pd
import numpy as np

np.random.seed(42)
n = 200

df = pd.DataFrame({
    "id_siswa": range(1, n+1),
    "nama": [f"Siswa {i}" for i in range(1, n+1)],
    "kelas": np.random.choice(["10A","10B","11A","11B"], n),
    "matematika": np.random.normal(72, 15, n).clip(0,100).round(1),
    "bahasa": np.random.normal(78, 12, n).clip(0,100).round(1),
    "ipa": np.random.normal(70, 18, n).clip(0,100).round(1),
    "hadir_pct": np.random.uniform(60, 100, n).round(1),
})
# Tambah beberapa missing values (realistis)
df.loc[df.sample(10).index, "matematika"] = np.nan
df.to_csv("nilai_siswa.csv", index=False)
print(df.shape)
Langkah 2

EDA Awal: Kenali Data Sebelum Apapun

df = pd.read_csv("nilai_siswa.csv")
print("Shape:", df.shape)
print("
Missing values:")
print(df.isnull().sum())
print("
Statistik dasar:")
print(df.describe().round(2))
print("
Distribusi kelas:")
print(df["kelas"].value_counts())
Langkah 3

Cleaning: Handle Missing Values

# Isi NaN matematika dengan median per kelas
df["matematika"] = df.groupby("kelas")["matematika"].transform(
    lambda x: x.fillna(x.median())
)
print("Missing setelah cleaning:", df.isnull().sum().sum())
Kenapa median per kelas? Mengisi dengan median GLOBAL bisa bias kalau kelas berbeda punya kemampuan berbeda. Median per kelas lebih adil.
Langkah 4

Transformasi: Buat Kolom Baru yang Bermakna

# Rata-rata nilai semua mapel
df["nilai_rata"] = df[["matematika","bahasa","ipa"]].mean(axis=1).round(1)

# Kategori kehadiran
df["kategori_hadir"] = pd.cut(df["hadir_pct"],
    bins=[0,75,90,100],
    labels=["Sering Absen","Cukup Hadir","Rajin Hadir"])

# Status kelulusan
df["lulus"] = df["nilai_rata"] >= 70
Langkah 5-8

Analisis, Pivot, dan Export Laporan

# Rata-rata per kelas
laporan_kelas = df.groupby("kelas").agg({
    "nilai_rata": ["mean","min","max"],
    "lulus": "mean",  # persen lulus
    "id_siswa": "count"
}).round(2)
print(laporan_kelas)

# Korelasi kehadiran vs nilai
print("
Korelasi hadir-nilai:",
    df["hadir_pct"].corr(df["nilai_rata"]).round(3))

# Export
df.to_csv("nilai_bersih.csv", index=False)
laporan_kelas.to_excel("laporan_kelas.xlsx")
print("Laporan tersimpan!")
Challenge akhir: Dari dataset ini, temukan: (1) Apakah ada korelasi antara kehadiran dan nilai rata-rata? (2) Kelas mana yang punya distribusi nilai paling merata? (3) Siswa dengan nilai rendah di matematika, apakah juga rendah di IPA?
Trigger: Abbas sudah bisa melakukan analisis data end-to-end. Kalau ada guru yang minta Abbas bantu analisis nilai kelas, Abbas sudah bisa. Apa lagi yang ingin Abbas analisis dari data sekolahnya sendiri?

10 Jebakan Pemula pandas yang Sering Bikin Frustasi

Ini bukan daftar kesalahan -- ini peta jalan menghindari 80% frustrasi pandas.
1. SettingWithCopyWarning
Terjadi saat Abbas menulis df[df['a'] > 0]['b'] = 1 (chained assignment). Fix: gunakan df.loc[df['a'] > 0, 'b'] = 1
2. Lupa reset_index setelah groupby
groupby().mean() menghasilkan Series dengan index aneh. Selalu tambah .reset_index() di akhir.
3. Merge duplikasi baris tak terduga
Kalau key tidak unik di salah satu tabel, merge akan menghasilkan lebih banyak baris dari yang diharapkan. Selalu cek df.duplicated('key_col').sum() sebelum merge.
4. apply() sangat lambat untuk dataset besar
Kalau bisa, gunakan operasi vektor langsung: df['a'] + df['b'] jauh lebih cepat dari df.apply(lambda r: r['a'] + r['b'], axis=1)
5. NaN tidak sama dengan None dan tidak sama dengan 0
np.nan == np.nan menghasilkan False! Selalu gunakan pd.isna() atau df.isnull() untuk cek missing values.