Bab 2 ~5 jam

Python untuk Data Science

Kuasai dasar Python dan library inti — NumPy dan Pandas — yang menjadi tulang punggung setiap proyek data science.

Peta Ekosistem — Dimanakah Kita Berada?

  ┌─────────────────────────────────────────────────────────┐
  │                  APLIKASI & VISUALISASI                 │
  │   Streamlit · Dash · Matplotlib · Seaborn · Plotly      │
  ├─────────────────────────────────────────────────────────┤
  │                   MACHINE LEARNING                      │
  │        Scikit-learn · XGBoost · TensorFlow · PyTorch    │
  ├──────────────────┬──────────────────────────────────────┤
  │   PANDAS ◄ BAB INI  │         STATISTIKA               │
  │   Manipulasi     │    SciPy · statsmodels               │
  │   tabular        │                                      │
  ├──────────────────┤                                      │
  │  NUMPY ◄ BAB INI  │                                      │
  │   Komputasi      │                                      │
  │   numerik        │                                      │
  ├──────────────────┴──────────────────────────────────────┤
  │                     PYTHON DASAR ◄ BAB INI              │
  │         Variabel · Fungsi · Struktur Data · OOP         │
  └─────────────────────────────────────────────────────────┘

Bab ini membangun fondasi dari lapisan paling bawah hingga Pandas. Layer di atasnya akan kita pelajari di bab-bab selanjutnya.

2.1 Review Python Dasar

Refresh singkat fitur Python yang paling sering digunakan dalam pipeline data science.

Variabel & Tipe Data

# Variabel & tipe dasar nama = "Data Science" # str tahun = 2025 # int pi = 3.14159 # float aktif = True # bool kosong = None # NoneType # Type checking & casting type(pi) # <class 'float'> isinstance(tahun, int) # True str(tahun) # '2025' float('3.14') # 3.14

String & f-String

f-string (Python 3.6+) adalah cara paling readable untuk memformat output — sangat berguna saat membuat laporan.

kota = "Jakarta" populasi = 10_560_000 # underscore sebagai pemisah ribuan # f-string dasar print(f"Populasi {kota}: {populasi}") # Populasi Jakarta: 10560000 # f-string dengan format specifier print(f"Populasi {kota}: {populasi:,}") # Populasi Jakarta: 10,560,000 print(f"Akurasi: {0.9234:.2%}") # Akurasi: 92.34% print(f"RMSE: {1.2345:.3f}") # RMSE: 1.235 # String methods yang sering dipakai " Hello World ".strip() # 'Hello World' "hello".upper() # 'HELLO' "Na,Na,NaN".split(",") # ['Na', 'Na', 'NaN'] "-".join(["a", "b", "c"]) # 'a-b-c'

Struktur Data Inti

# === LIST === fitur = ["usia", "pendapatan", "skor"] nilai = [85, 92, 78, 95, 88] fitur.append("pendidikan") # tambah di akhir fitur.insert(0, "id") # sisip di posisi 0 sorted(nilai, reverse=True) # [95, 92, 88, 85, 78] # List comprehension — idiom paling Pythonic nilai_tinggi = [n for n in nilai if n >= 90] # [92, 95] nilai_normalisasi = [(n - min(nilai)) / (max(nilai) - min(nilai)) for n in nilai] # [0.389, 0.833, 0.0, 1.0, 0.556] # === DICTIONARY === mahasiswa = { "nama": "Budi", "usia": 25, "nilai": [85, 92] } mahasiswa["prodi"] = "Informatika" # tambah key mahasiswa.get("ipk", 0.0) # aman, default 0.0 mahasiswa.keys() # dict_keys(['nama', 'usia', 'nilai', 'prodi']) mahasiswa.values() # dict_values([...]) # Dict comprehension kuadrat = {x: x**2 for x in range(1, 6)} # {1: 1, 2: 4, 3: 9, 4: 16, 5: 25} # === SET === (himpunan, unik, tanpa duplikat) kategori = {"Teknik", "Marketing", "Teknik", "HR"} # {'Teknik', 'Marketing', 'HR'} # === TUPLE === (immutable, hashable) koordinat = (-6.2, 106.8) # (lat, lon) Jakarta

Ilustrasi: Struktur Data Python

LIST (terurut, bisa diubah)        DICT (key → value)
┌───┬───┬───┬───┐                ┌──────────┬────────┐
│ 85│ 92│ 78│ 95│                │ "nama"   │ "Budi" │
└───┴───┴───┴───┘                │ "usia"   │  25    │
  0   1   2   3  ← index         │ "nilai"  │[85,92] │
                                 └──────────┴────────┘

SET (unik, tidak terurut)       TUPLE (terurut, immutable)
  ┌───┐                            ┌──────┬──────┐
  │HR │                            │-6.2  │106.8 │
  ├───┤                            └──────┴──────┘
  │Tek│                               lat    lon
  ├───┤
  │Mkt│
  └───┘

Fungsi

# Fungsi dasar def rata_rata(nilai): return sum(nilai) / len(nilai) # Default argument & type hints (opsional tapi direkomendasikan) def hitung_rmse(aktual: list, prediksi: list, decimals: int = 4) -> float: """Hitung Root Mean Squared Error.""" n = len(aktual) mse = sum((a - p) ** 2 for a, p in zip(aktual, prediksi)) / n return round(mse ** 0.5, decimals) print(hitung_rmse([3, 5, 2], [2.5, 5.5, 1.8])) # 0.4359 print(hitung_rmse([3, 5, 2], [2.5, 5.5, 1.8], decimals=2)) # 0.44 # *args dan **kwargs def log_experimen(nama, **params): print(f"[{nama}]") for k, v in params.items(): print(f" {k} = {v}") log_experimen("RandomForest", n_estimators=100, max_depth=5) # [RandomForest] # n_estimators = 100 # max_depth = 5

Error Handling

Dalam pipeline data, file bisa hilang, format bisa salah, atau tipe data tidak sesuai. Gunakan try/except agar program tidak crash di tengah jalan.

def baca_data(path: str) -> pd.DataFrame: try: df = pd.read_csv(path) print(f"Berhasil memuat {len(df)} baris") return df except FileNotFoundError: print(f"ERROR: File '{path}' tidak ditemukan.") return pd.DataFrame() # return DataFrame kosong except pd.errors.EmptyDataError: print("ERROR: File kosong.") return pd.DataFrame()
Konvensi: Selalu gunakan import numpy as np dan import pandas as pd — alias ini bersifat universal di seluruh ekosistem data science. Jangan gunakan alias lain.

2.2 NumPy: Komputasi Numerik

NumPy menyediakan ndarray — array multidimensi berkinerja tinggi yang menjadi fondasi Pandas, Scikit-learn, dan hampir semua library ML.

Mengapa NumPy Lebih Cepat dari List?

Python List — setiap elemen adalah objek terpisah

  list_obj ──► [ptr]──► int_obj(1)    ← lokasi memori acak
              [ptr]──► int_obj(2)       tiap akses butuh lookup
              [ptr]──► int_obj(3)       overhead ~28 byte/angka
              [ptr]──► int_obj(4)

NumPy ndarray — blok memori kontigu bertipe tetap

  ndarray ──► [ 1 | 2 | 3 | 4 ]       ← blok C-contiguous
               ↑                       tipe tetap (int64 = 8 byte)
               satu pointer             vektorisasi via C/Fortran
               ke blok                  ~100x lebih cepat untuk operasi numerik

Membuat Array

import numpy as np # Dari list arr = np.array([1, 2, 3, 4, 5]) print(arr.shape) # (5,) print(arr.dtype) # int64 print(arr.ndim) # 1 # Pembuat array utama nol = np.zeros((3, 4)) # matriks 3×4 berisi 0.0 satu = np.ones((2, 3)) # matriks 2×3 berisi 1.0 iden = np.eye(3) # matriks identitas 3×3 acak = np.random.rand(3, 3) # uniform [0, 1) randn = np.random.randn(3, 3) # normal (μ=0, σ=1) # Deret & interval deret = np.arange(0, 10, 2) # [0, 2, 4, 6, 8] lin = np.linspace(0, 1, 5) # [0.0, 0.25, 0.5, 0.75, 1.0] # Dengan tipe data spesifik arr_f = np.array([1, 2, 3], dtype=np.float32) # hemat memori

Ilustrasi: Bentuk-Bentuk Array

1D — shape (5,)          2D — shape (3, 4)              3D — shape (2, 3, 2)
┌───┬───┬───┬───┬───┐      ┌───┬───┬───┬───┐          ┌───┬───┐
│ 1 │ 2 │ 3 │ 4 │ 5 │      │ 0 │ 0 │ 0 │ 0 │          │ 0 │ 1 │  ← slice 0
└───┴───┴───┴───┴───┘      ├───┼───┼───┼───┤          │ 2 │ 3 │
                           │ 0 │ 0 │ 0 │ 0 │          │ 4 │ 5 │
                           ├───┼───┼───┼───┤          └───┴───┘
                           │ 0 │ 0 │ 0 │ 0 │          ┌───┬───┐
                           └───┴───┴───┴───┘          │ 6 │ 7 │  ← slice 1
                                ↑ kolom              │ 8 │ 9 │
                          baris ↑                    │10 │11 │
                                                      └───┴───┘
Akses: arr[i]          Akses: arr[row, col]      Akses: arr[slice, row, col]

Operasi Vektorisasi

Operasi dilakukan elemen-per-elemen tanpa loop eksplisit. Ini yang membuat NumPy cepat.

a = np.array([1, 2, 3, 4]) b = np.array([10, 20, 30, 40]) # Aritmatika elemen-per-elemen a + b # [11, 22, 33, 44] a * b # [10, 40, 90, 160] a ** 2 # [1, 4, 9, 16] 100 - a # [99, 98, 97, 96] # Fungsi matematika (ufunc) np.sqrt(a) # [1.0, 1.414, 1.732, 2.0] np.log(a) # [0.0, 0.693, 1.099, 1.386] np.exp(a) # [2.718, 7.389, 20.086, 54.598] np.clip(a, 1, 3) # [1, 1, 3, 3] — batas bawah & atas # Agregasi —沿 axis m = np.array([[1,2,3], [4,5,6]]) m.sum() # 21 — total semua elemen m.sum(axis=0) # [5, 7, 9] — jumlah per kolom m.sum(axis=1) # [6, 15] — jumlah per baris m.mean(axis=0) # [2.5, 3.5, 4.5] a.std() # 1.118 a.argmax() # 3 — indeks elemen terbesar a.cumsum() # [1, 3, 6, 10] — kumulatif

Ilustrasi: Memahami Axis

m = array([[1, 2, 3],
           [4, 5, 6]])
      shape (2, 3)
      ↑     ↑
    axis=0  axis=1

axis=0 → gerak turun (per kolom)     axis=1 → gerak mendatar (per baris)
       ↓                                  →
      [1+4, 2+5, 3+6]                    [1+2+3, 4+5+6]
      = [5, 7, 9]                        = [6, 15]

Cara ingat: axis=N berarti "hilangkan dimensi ke-N"
  shape (2,3) → sum(axis=0) → shape (3,)  ← dimensi-0 hilang
  shape (2,3) → sum(axis=1) → shape (2,)  ← dimensi-1 hilang

Indexing & Slicing

matriks = np.array([[1,2,3], [4,5,6], [7,8,9]]) # Akses elemen tunggal [baris, kolom] matriks[0, 1] # 2 matriks[2, 2] # 9 # Akses baris / kolom penuh matriks[0] # [1, 2, 3] — baris pertama matriks[:, 0] # [1, 4, 7] — kolom pertama matriks[:, -1] # [3, 6, 9] — kolom terakhir # Slicing 2D [baris_start:stop, kolom_start:stop] matriks[0:2, 1:3] # [[2, 3], [5, 6]] # Boolean indexing — sangat berguna untuk filtering matriks[matriks > 5] # [6, 7, 8, 9] matriks[matriks % 2 == 0] # [2, 4, 6, 8] # Fancy indexing matriks[[0, 2]] # baris 0 dan 2 matriks[:, [0, 2]] # kolom 0 dan 2

Broadcasting

Broadcasting memungkinkan operasi antara array dengan bentuk berbeda — NumPy secara otomatis "memperluas" array yang lebih kecil.

# Contoh 1: skalar + array a = np.array([1, 2, 3]) a + 10 # [11, 12, 13] # Contoh 2: (3,) + (3,3) matriks = np.array([[1,2,3],[4,5,6],[7,8,9]]) baris = np.array([100, 200, 300]) matriks + baris # [[101, 202, 303], # [104, 205, 306], # [107, 208, 309]] # Contoh 3: (3,1) + (3,) → kolom ditambahkan ke setiap kolom kolom = np.array([[10], [20], [30]]) # shape (3,1) matriks + kolom # [[11, 12, 13], # [24, 25, 26], # [37, 38, 39]]

Ilustrasi: Cara Broadcasting Bekerja

Matriks (3,3)      +    Vektor baris (3,)

  1   2   3              100  200  300
  4   5   6         +         ↓   ↓   ↓      → vektor "disalin" ke setiap baris
  7   8   9

  ────────────         ──────────────
 101 202 303
 104 205 306
 107 208 309


Matriks (3,3)      +    Vektor kolom (3,1)

  1   2   3              10        ─┐
  4   5   6         +    20        ─┤  → vektor "disalin" ke setiap kolom
  7   8   9              30        ─┘

  ────────────         ──────────────
  11  12  13
  24  25  26
  37  38  39

Aturan: dimensi yang tidak cocok harus salah satunya = 1
  (3,3) + (3,)   → (3,3) + (1,3) ✓  valid
  (3,3) + (3,1)  → sama-sama 3 di axis-0, 1 vs 3 di axis-1 ✓  valid
  (3,3) + (4,)   → 3 ≠ 4, bukan 1 ✗  error!

Reshape & Transpose

arr = np.arange(12) # [0, 1, 2, ..., 11] # Reshape — ubah bentuk tanpa mengubah data m = arr.reshape(3, 4) # [[ 0, 1, 2, 3], # [ 4, 5, 6, 7], # [ 8, 9, 10, 11]] # -1 = "hitung otomatis" m.reshape(-1) # kembali ke 1D [0,1,...,11] m.reshape(4, -1) # (4, 3) — NumPy hitung 3 sendiri # Transpose — tukar baris & kolom m.T # [[ 0, 4, 8], # [ 1, 5, 9], # [ 2, 6, 10], # [ 3, 7, 11]] # Stack — gabung array a = np.array([1, 2, 3]) b = np.array([4, 5, 6]) np.vstack([a, b]) # [[1,2,3],[4,5,6]] — tumpuk vertikal np.hstack([a, b]) # [1,2,3,4,5,6] — tumpuk horizontal np.column_stack([a, b]) # [[1,4],[2,5],[3,6]] — jadi kolom

Contoh Praktis: Normalisasi Data

Dalam machine learning, fitur sering perlu dinormalisasi. Ini contoh min-max scaling murni dengan NumPy:

# Data: 5 sampel, 3 fitur (misal: usia, gaji, skor) X = np.array([ [25, 50, 80], [30, 65, 92], [35, 80, 78], [28, 55, 88], [40, 90, 95] ]) # Min-max scaling: (X - min) / (max - min) X_min = X.min(axis=0) # [25, 50, 78] — min per fitur X_max = X.max(axis=0) # [40, 90, 95] — max per fitur X_norm = (X - X_min) / (X_max - X_min) print(X_norm.round(2)) # [[0.0 , 0.0 , 0.15], # [0.33, 0.38, 0.71], # [0.67, 0.75, 0.0 ], # [0.2 , 0.12, 0.59], # [1.0 , 1.0 , 1.0 ]]
Kesalahan Umum: NumPy menggunakan urutan row-major (C-order). matriks[0] adalah baris pertama, bukan kolom. Jika Anda berasal dari MATLAB/R, ini sering membingungkan.

2.3 Pandas: Manipulasi Data

Pandas dibangun di atas NumPy. Jika NumPy adalah "array mentah", maka Pandas adalah "spreadsheet yang bisa diprogram" — lengkap dengan label baris, label kolom, dan tipe data per kolom.

Ilustrasi: Series vs DataFrame

pd.Series — 1D, punya index

  index →  0    1    2    3
          ┌────┬────┬────┬────┐
  nama   │Budi│Ani │Citra│Dedi│
          └────┴────┴────┴────┘
  dtype: object

pd.DataFrame — 2D, punya index + columns

           nama   usia  gaji    bagian
  index ↓  ┌──────┬─────┬───────┬──────────┐
      0    │ Budi │  25 │ 50jt  │ Teknik   │
      1    │ Ani  │  30 │ 65jt  │ Marketing│
      2    │ Citra│  35 │ 80jt  │ Teknik   │
      3    │ Dedi │  28 │ 55jt  │ Marketing│
           └──────┴─────┴───────┴──────────┘
            ↑ columns (setiap kolom = 1 Series)

Membuat DataFrame

import pandas as pd # Cara 1: dari dictionary (paling umum) df = pd.DataFrame({ 'nama': ['Budi', 'Ani', 'Citra', 'Dedi'], 'usia': [25, 30, 35, 28], 'gaji': [50_000_000, 65_000_000, 80_000_000, 55_000_000], 'bagian': ['Teknik', 'Marketing', 'Teknik', 'Marketing'] }) # Cara 2: dari list of dict data = [ {'nama': 'Budi', 'usia': 25}, {'nama': 'Ani', 'usia': 30}, ] df2 = pd.DataFrame(data) # Cara 3: dari NumPy array arr = np.random.rand(3, 3) df3 = pd.DataFrame(arr, columns=['A', 'B', 'C'])

Inspeksi Cepat

Lima perintah pertama yang selalu dijalankan setelah memuat data:

df.head(3) # 3 baris pertama df.tail(2) # 2 baris terakhir df.sample(3) # 3 baris acak — berguna untuk data besar df.info() # tipe data, non-null count, memori df.describe() # ringkasan statistik kolom numerik df.shape # (4, 4) — (baris, kolom) df.columns.tolist() # ['nama', 'usia', 'gaji', 'bagian'] df.dtypes # tipe data setiap kolom df.nunique() # jumlah nilai unik per kolom

Contoh Output: df.describe()

              usia           gaji
count    4.000000  4.000000e+00
mean    29.500000  6.250000e+07
std      4.358899  1.320475e+07
min     25.000000  5.000000e+07
25%     27.000000  5.375000e+07
50%     29.000000  6.000000e+07
75%     32.000000  7.000000e+07
max     35.000000  8.000000e+07

Kolom non-numerik (nama, bagian) tidak ditampilkan kecuali menggunakan include='all'.

Seleksi & Filtering

# Pilih kolom — hasilnya adalah Series df['nama'] # Pilih beberapa kolom — hasilnya DataFrame df[['nama', 'gaji']] # .loc — berbasis label (inklusif di kedua ujung) df.loc[0] # baris index 0 (semua kolom) df.loc[0:2, 'nama':'gaji'] # baris 0-2, kolom nama sampai gaji df.loc[df['usia'] > 28, 'nama'] # filter + pilih kolom # .iloc — berbasis posisi integer (eksklusif di ujung) df.iloc[0] # baris pertama df.iloc[0:2, 0:2] # baris 0-1, kolom 0-1 df.iloc[-1] # baris terakhir # Boolean filtering df[df['usia'] > 28] df[(df['usia'] > 28) & (df['bagian'] == 'Teknik')] # AND df[df['bagian'].isin(['Teknik', 'HR'])] # IN df[~df['bagian'].isin(['Teknik'])] # NOT IN (~ = negasi) # .query — sintaks SQL-like (lebih readable untuk kondisi kompleks) df.query("usia > 28 and bagian == 'Teknik'")

Ilustrasi: .loc vs .iloc

           nama   usia  gaji
  index ↓
      0    Budi    25   50M        .loc[0:2]     → baris index 0, 1, 2   (inklusif)
      1    Ani     30   65M        .iloc[0:2]    → baris posisi 0, 1        (eksklusif)
      2    Citra   35   80M
      3    Dedi    28   55M

Aturan praktis:
  • Gunakan .loc saat kerja dengan label/nama
  • Gunakan .iloc saat kerja dengan posisi angka
  • Selalu gunakan salah satu — hindari df[baris, kolom] langsung (bisa ambigu)

Menambah & Mengubah Kolom

# Kolom baru dari operasi lain df['gaji_juta'] = df['gaji'] / 1_000_000 df['pajak'] = df['gaji'] * 0.1 df['netto'] = df['gaji'] - df['pajak'] # Rename kolom df.rename(columns={'gaji': 'salary', 'usia': 'age'}, inplace=True) # Hapus kolom / baris df.drop(columns=['pajak']) df.drop(index=[0, 2]) # Rename index df.set_index('nama', inplace=True) # Sekarang 'Budi', 'Ani', dll. menjadi label baris df.reset_index(inplace=True) # kembalikan ke default 0,1,2,3

GroupBy — Split-Apply-Combine

Pola ini adalah jantung analisis agregasi. Bayangkan seperti pivot table di Excel, tapi jauh lebih powerful.

# Agregasi tunggal df.groupby('bagian')['gaji'].mean() # bagian # Marketing 60000000 # Teknik 65000000 # Name: gaji, dtype: int64 # Multiple agregasi df.groupby('bagian')['gaji'].agg(['mean', 'min', 'max', 'count']) # mean min max count # bagian # Marketing 60000000 55000000 65000000 2 # Teknik 65000000 50000000 80000000 2 # Agregasi berbeda per kolom df.groupby('bagian').agg({ 'usia': 'mean', 'gaji': ['mean', 'std'], 'nama': 'count' }) # Named aggregation (Python 3.8+) df.groupby('bagian').agg( rata_gaji=('gaji', 'mean'), max_gaji=('gaji', 'max'), jumlah=('nama', 'count') )

Ilustrasi: Mekanisme GroupBy

DATA ASLI
  Budi   Teknik     50M
  Ani    Marketing  65M
  Citra  Teknik     80M
  Dedi   Marketing  55M

        ↓ SPLIT — pisahkan berdasarkan 'bagian'

  ┌─────────────────┐    ┌─────────────────┐
  │ Teknik           │    │ Marketing        │
  │ Budi    50M     │    │ Ani     65M     │
  │ Citra   80M     │    │ Dedi    55M     │
  └────────┬────────┘    └────────┬────────┘
           ↓                      ↓
        ↓ APPLY — hitung mean per grup
           ↓                      ↓
        65M                    60M
           ↓                      ↓
        ↓ COMBINE — gabungkan hasil
           ↓                      ↓

  ┌──────────────┬───────────┐
  │ bagian       │ gaji_mean │
  ├──────────────┼───────────┤
  │ Marketing    │ 60000000  │
  │ Teknik       │ 65000000  │
  └──────────────┴───────────┘

Sorting & Ranking

# Sort by nilai df.sort_values('gaji', ascending=False) df.sort_values(['bagian', 'gaji'], ascending=[True, False]) # Sort by index df.sort_index() # Ranking df['gaji_rank'] = df['gaji'].rank(ascending=False, method='dense') # method='dense': 1,2,2,3 (tanpa gap) # method='min': 1,2,2,4 (ada gap) # Top-N per grup df.groupby('bagian').apply( lambda x: x.nlargest(1, 'gaji') ).reset_index(drop=True)

Kolom Kategorikal: Value Counts & Crosstab

# Frekuensi nilai df['bagian'].value_counts() # Teknik 2 # Marketing 2 # Proporsi (normalisasi) df['bagian'].value_counts(normalize=True) # Teknik 0.5 # Marketing 0.5 # Crosstab — tabel silang pd.crosstab(df['bagian'], df['level'], margins=True) # level Junior Senior All # bagian # Marketing 1 1 2 # Teknik 1 1 2 # All 2 2 4
Pola Anti-Lok: Hindari mengulang df[df['kolom'] == x] berkali-kali. Gunakan .groupby() atau .pivot_table() untuk agregasi, dan .query() untuk filtering yang kompleks.

2.4 Data I/O

Data science dimulai dan berakhir dengan membaca serta menulis data. Pandas mendukung puluhan format — berikut yang paling penting.

Membaca Data

# CSV — format paling umum df = pd.read_csv('data.csv') df = pd.read_csv('data.csv', sep=';') # pemisah kustom df = pd.read_csv('data.csv', nrows=1000) # baca 1000 baris pertama df = pd.read_csv('data.csv', usecols=['nama', 'usia'], # hanya kolom tertentu parse_dates=['tanggal'], # auto-parse ke datetime dtype={'id': str}) # override tipe data # Excel df = pd.read_excel('data.xlsx', sheet_name='Sheet1') # JSON df = pd.read_json('data.json') df = pd.read_json('api_response.json', lines=True) # JSON Lines # SQL import sqlite3 conn = sqlite3.connect('db.sqlite') df = pd.read_sql("SELECT * FROM users WHERE active = 1", conn) # Parquet — format kolom yang efisien (akan dibahas di bawah) df = pd.read_parquet('data.parquet') # Clipboard — sangat berguna untuk quick test df = pd.read_clipboard() # paste dari Excel/Google Sheets

Menulis Data

# CSV df.to_csv('keluaran.csv', index=False) # index=False umumnya diinginkan # Excel (butuh openpyxl) df.to_excel('keluaran.xlsx', sheet_name='Data', index=False) # Parquet — format pilihan untuk data perantara df.to_parquet('keluaran.parquet', index=False) # JSON df.to_json('keluaran.json', orient='records', indent=2) # SQL df.to_sql('tabel_hasil', conn, if_exists='replace', index=False)

Perbandingan Format File

Format    Baca     Tulis     Ukuran    Tipe Data    Kapan Pakai
─────────────────────────────────────────────────────────────────────────────────
CSV       Lambat    Lambat    Besar     Hilang       Interoperabilitas
Parquet   Cepat     Cepat     Kecil     Dipertahankan Pipeline perantara
Excel     Sedang    Sedang    Sedang      Hilang       Laporan ke bisnis
JSON      Sedang    Sedang    Besar     Hilang       API / web
SQLite    Cepat     Cepat     Kecil       Dipertahankan Aplikasi / query kompleks

Rekomendasi: CSV untuk input/output awal, Parquet untuk cache perantara.

Pola Praktis: Chunked Reading

Jika file CSV lebih besar dari RAM, baca per chunk:

chunk_size = 100_000 results = [] for chunk in pd.read_csv('data_besar.csv', chunksize=chunk_size): # Proses setiap chunk secara independen hasil = chunk[filter_kondisi(chunk)] results.append(hasil) df_final = pd.concat(results, ignore_index=True)
Tips Pro: Gunakan .parquet untuk penyimpanan perantara — 5-10× lebih cepat dibaca dari CSV, menjaga tipe data (tanggal tetap tanggal, integer tetap integer), dan kompresi ~75% lebih kecil. Satu-satunya kelemahan: tidak bisa dibuka langsung di Notepad.

2.5 Pola-Pola Penting

Pola-pola berikut muncul di hampir setiap proyek data science. Kuasai ini, dan Anda bisa menangani 80% kasus manipulasi data.

Pola 1: Apply — Transformasi Kolom

# Lambda sederhana df['kategori_gaji'] = df['gaji'].apply( lambda x: 'Tinggi' if x >= 70_000_000 else 'Rendah' ) # Fungsi bernama — lebih readable untuk logika kompleks def kategori_usia(usia): if usia < 25: return 'Muda' elif usia < 35: return 'Dewasa' else: return 'Senior' df['kategori_usia'] = df['usia'].apply(kategori_usia) # Apply per baris — akses beberapa kolom sekaligus df['gaji_per_usia'] = df.apply( lambda row: row['gaji'] / row['usia'], axis=1 )
Peringatan Performa: .apply() sebenarnya adalah loop yang disamarkan — tidak secepat operasi vektorisasi. Gunakan np.where(), .str accessor, atau operasi aritmatika langsung jika memungkinkan.
# Alternatif vektorisasi untuk apply di atas: # Daripada: df['kategori_gaji'] = df['gaji'].apply(lambda x: ...) # Gunakan np.where: df['kategori_gaji'] = np.where( df['gaji'] >= 70_000_000, 'Tinggi', 'Rendah' ) # Daripada: df['gaji_per_usia'] = df.apply(lambda row: ..., axis=1) # Gunakan operasi kolom langsung: df['gaji_per_usia'] = df['gaji'] / df['usia'] # np.select untuk >2 kondisi kondisi = [ df['usia'] < 25, df['usia'] < 35 ] pilihan = ['Muda', 'Dewasa'] df['kat_usia'] = np.select(kondisi, pilihan, default='Senior')

Pola 2: Menangani Data Hilang (Missing Values)

# Deteksi df.isnull().sum() # jumlah NaN per kolom df.isnull().mean() * 100 # persentase NaN per kolom df.isnull().sum().sum() # total NaN di seluruh DataFrame df[df.isnull().any(axis=1)] # baris yang mengandung NaN # Strategi 1: Hapus baris/kolom df.dropna(subset=['gaji']) # hapus baris tanpa gaji df.dropna(thresh=3) # hapus baris dengan < 3 nilai valid df.dropna(axis=1, thresh=100) # hapus kolom dengan < 100 valid # Strategi 2: Imputasi — isi nilai pengganti df['usia'].fillna(df['usia'].median()) # median (robust terhadap outlier) df['usia'].fillna(df['usia'].mean()) # mean df['bagian'].fillna(df['bagian'].mode[0]) # modus (untuk kategorikal) df['gaji'].fillna(0) # konstanta df[fillna({'usia': 25, 'gaji': 50_000_000}) # berbeda per kolom # Strategi 3: Forward/Backward fill (untuk time series) df['suhu'].ffill() # isi dengan nilai sebelumnya df['suhu'].bfill() # isi dengan nilai sesudahnya # Strategi 4: Interpolasi (untuk data kontinu) df['suhu'].interpolate(method='linear')

Ilustrasi: Strategi Menangani NaN

Data asli:  [25, NaN, 35, NaN, 28, 30]

dropna()     →  [25, 35, 28, 30]        baris hilang, bisa bikin bias
fillna(mean) →  [25, 29.5, 35, 29.5, 28, 30]  mean = (25+35+28+30)/4
fillna(med)  →  [25, 28.5, 35, 28.5, 28, 30]  median lebih aman dari outlier
ffill()      →  [25, 25, 35, 35, 28, 30]  cocok untuk time series
interpolate()→  [25, 30, 35, 31.5, 28, 30]  linear antara titik

Kapan pakai apa:
  • < 5% NaN & acak        → dropna aman
  • Numerik, ada outlier   → fillna(median)
  • Time series            → ffill() atau interpolate()
  • Kategorikal            → fillna(mode) atau "Unknown"

Pola 3: Operasi String

# .str accessor — vektorisasi untuk string df['nama'] = df['nama'].str.strip() # hapus spasi df['nama'] = df['nama'].str.upper() # huruf besar df['nama'] = df['nama'].str.title() "budi santoso" → "Budi Santoso" # Ekstraksi df['email'].str.split('@').str[0] # ambil username saja df[">kode'] = df['id_produk'].str.extract(r'([A-Z]+)') # regex # Filtering string df[df['nama'].str.contains('an', case=False)] df[df['email'].str.endswith('@gmail.com')] df[df['kode_pos'].str.match(r'^\d{5}$')] # tepat 5 digit

Pola 4: Merge & Join

Sama seperti SQL JOIN — menggabungkan DataFrame berdasarkan key.

# Data contoh df_karyawan = pd.DataFrame({ 'id': [1, 2, 3, 4], 'nama': ['Budi', 'Ani', 'Citra', 'Dedi'] }) df_gaji = pd.DataFrame({ 'id': [1, 2, 5], # perhatikan: id=5 baru, id=3,4 tidak ada 'gaji': [50, 65, 90] }) # INNER JOIN — hanya yang cocok di kedua tabel pd.merge(df_karyawan, df_gaji, on='id', how='inner') # id=1,2 saja # LEFT JOIN — semua dari kiri, NaN jika tidak cocok pd.merge(df_karyawan, df_gaji, on='id', how='left') # id=1,2,3,4 — gaji id=3,4 = NaN # RIGHT JOIN — semua dari kanan pd.merge(df_karyawan, df_gaji, on='id', how='right') # id=1,2,5 — nama id=5 = NaN # OUTER JOIN — semua dari kedua tabel pd.merge(df_karyawan, df_gaji, on='id', how='outer') # id=1,2,3,4,5 — ada NaN di kedua sisi # Key berbeda nama pd.merge(df1, df2, left_on='user_id', right_on='id') # Concat — tumpuk vertikal/horizontal tanpa key pd.concat([df_jan, df_feb], ignore_index=True) # vertikal (default) pd.concat([df_fitur, df_target], axis=1) # horizontal

Ilustrasi: Jenis-Jenis Join

df_karyawan (LEFT)          df_gaji (RIGHT)
┌────┬───────┐              ┌────┬──────┐
│ id │ nama  │              │ id │ gaji │
├────┼───────┤              ├────┼──────┤
│ 1  │ Budi  │              │ 1  │  50  │
│ 2  │ Ani   │              │ 2  │  65  │
│ 3  │ Citra │              │ 5  │  90  │
│ 4  │ Dedi  │              └────┴──────┘
└────┴───────┘

INNER (how='inner')         LEFT (how='left')
┌────┬──────┬──────┐           ┌────┬───────┬──────┐
│ id │ nama │ gaji │           │ id │ nama  │ gaji │
├────┼──────┼──────┤           ├────┼───────┼──────┤
│ 1  │ Budi │  50  │           │ 1  │ Budi  │  50  │
│ 2  │ Ani  │  65  │           │ 2  │ Ani   │  65  │
└────┴──────┴──────┘           │ 3  │ Citra │ NaN  │
                               │ 4  │ Dedi  │ NaNRIGHT (how='right')        └────┴───────┴──────┘
┌────┬───────┬──────┐
│ id │ nama  │ gaji │           OUTER (how='outer')
├────┼───────┼──────┤           ┌────┬───────┬──────┐
│ 1  │ Budi  │  50  │           │ id │ nama  │ gaji │
│ 2  │ Ani   │  65  │           ├────┼───────┼──────┤
│ 5  │ NaN  │  90  │           │ 1  │ Budi  │  50  │
└────┴───────┴──────┘           │ 2  │ Ani   │  65  │
                               │ 3  │ Citra │ NaN  │
                               │ 4  │ Dedi  │ NaN  │
                               │ 5  │ NaN  │  90  │
                               └────┴───────┴──────┘

Pola 5: Pivot Table & Melt

# pivot_table — seperti Excel pivot table pd.pivot_table( df, values='gaji', index='bagian', # baris columns='kategori_usia', # kolom aggfunc='mean', fill_value=0, margins=True # tambahkan baris/kolom total ) # melt — kebalikan pivot (wide → long) wide = pd.DataFrame({ 'nama': ['Budi'], 'mt': [85], 'kim': [78], 'fis": [92] }) long = wide.melt( id_vars=['nama'], value_vars=['mt', 'kim', 'fis'], var_name='mapel', value_name='nilai' ) # nama mapel nilai # 0 Budi mt 85 # 1 Budi kim 78 # 2 Budi fis 92

Pola 6: Method Chaining

Alih-alih membuat variabel perantara, rantai operasi bersama. Lebih readable dan mengurangi variabel sementara.

# ❌ Tidak chaining — banyak variabel sementara df2 = df.dropna(subset=['gaji']) df3 = df2[df2['gaji'] > 50_000_000] df4 = df3.assign(gaji_juta=df3['gaji'] / 1_000_000) hasil = df4.sort_values('gaji_juta', ascending=False) # ✅ Chaining — satu alur, mudah dibaca hasil = ( df .dropna(subset=['gaji']) .query("gaji > 50_000_000") .assign(gaji_juta=lambda d: d['gaji'] / 1_000_000) .sort_values('gaji_juta', ascending=False) .reset_index(drop=True) )
Tip: Gunakan tanda kurung ( ) untuk membungkus chain agar bisa pindah baris tanpa backslash. Dan gunakan lambda d: di dalam .assign() untuk merujuk ke DataFrame yang sedang dibangun, bukan DataFrame asli.

2.6 Latihan Praktik

Terapkan semua konsep di atas ke dataset nyata. Kerjakan secara berurutan — setiap langkah membangun di atas yang sebelumnya.

📌 Dataset: Titanic (Seaborn built-in)

import seaborn as sns df = sns.load_dataset('titanic') df.head()

Latihan 1 — Eksplorasi Awal

Jawab pertanyaan berikut (tanpa melihat kunci jawaban):

  • Berapa jumlah baris dan kolom?
  • Kolom mana yang memiliki missing values, dan berapa persennya?
  • Berapa rata-rata umur penumpang?
  • Berapa banyak penumpang di setiap kelas (pclass)?
Lihat kunci jawaban
df.shape # (891, 15) df.isnull().mean() * 100 # age 19.87% # deck 77.22% # embarked 0.22% df['age'].mean() # 29.699 df['pclass'].value_counts().sort_index() # 1: 216, 2: 184, 3: 491

Latihan 2 — GroupBy & Agregasi

Hitung tingkat keselamatan (survived.mean()) berdasarkan:

  • Kelas (pclass)
  • Jenis kelamin (sex)
  • Kombinasi kelas dan jenis kelamin
Lihat kunci jawaban
# Per kelas df.groupby('pclass')['survived'].mean() # 1: 0.630, 2: 0.473, 3: 0.242 # Per jenis kelamin df.groupby('sex')['survived'].mean() # female: 0.742, male: 0.189 # Kombinasi df.groupby(['pclass', 'sex'])['survived'].mean() # 1 female: 0.968, 1 male: 0.369 # 2 female: 0.921, 2 male: 0.157 # 3 female: 0.500, 3 male: 0.135 # Bonus: pivot table untuk tampilan lebih rapi pd.pivot_table(df, values='survived', index='pclass', columns='sex', aggfunc='mean')

Latihan 3 — Imputasi Cerdas

Isi missing value di kolom age dengan median per kelas (bukan median keseluruhan). Mengapa ini lebih baik?

Lihat kunci jawaban
# Hitung median per kelas median_per_kelas = df.groupby('pclass')['age'].transform('median') # Isi NaN dengan median kelas masing-masing df['age_filled'] = df['age'].fillna(median_per_kelas) # Verifikasi: tidak ada lagi NaN df['age_filled'].isnull().sum() # 0 # Mengapa lebih baik? # Kelas 1 median=37, Kelas 3 median=24 # Jika pakai median keseluruhan (28), kita under-estimate # umur penumpang kelas 1 dan over-estimate kelas 3.

Latihan 4 — Method Chaining Challenge

Dalam satu chain, buat DataFrame yang berisi: penumpang perempuan kelas 1 atau 2 yang selamat, dengan kolom tambahan age_group (Anak <18, Dewasa 18-60, Lansia >60), diurutkan berdasarkan umur menurun.

Lihat kunci jawaban
hasil = ( df .query("sex == 'female' and pclass in [1, 2] and survived == 1") .assign( age_filled=lambda d: d['age'].fillna( d.groupby('pclass')['age'].transform('median') ), age_group=lambda d: np.select( [d['age'] < 18, d['age'] <= 60], ['Anak', 'Dewasa'], default='Lansia' ) ) .sort_values('age', ascending=False) .reset_index(drop=True) [['name', 'pclass', 'age', 'age_group']] ) print(hasil.head()) print(hasil['age_group'].value_counts())
Challenge Tambahan: Simpan hasil latihan 4 ke file CSV dan Parquet, lalu bandingkan ukuran filenya. Coba juga baca kembali file Parquet dan verifikasi tipe data tetap konsisten.

Cheat Sheet: Bab 2

NUMPY                              PANDAS
np.array([1,2,3])                  df = pd.DataFrame({'a':[1,2]})
np.zeros((3,4)) / np.ones((2,3))   df.head() / df.info() / df.describe()
np.arange(0,10,2) / np.linspace()  df['kolom'] / df[['a','b']]
arr.shape / arr.dtype / arr.ndim   df.loc[row, col] / df.iloc[pos]
arr.sum(axis=0)                    df[df['x']>5] / df.query('x>5')
arr[mask]                          df.groupby('g')['v'].mean()
np.where(cond, a, b)               df.merge(a, b, on='key', how='left')
arr.reshape(3,-1) / arr.T          pd.concat([a,b], axis=0)

DATA I/O                           CLEANING
pd.read_csv('f.csv')                df.isnull().sum()
pd.read_parquet('f.parquet')       df.fillna(median) / df.dropna()
df.to_parquet('f.parquet')         df['col'].str.strip().str.upper()
                                   df.rename(columns={'old':'new'})
                                   df.drop(columns=['x'])