Belajar Python untuk mempercepat penelitian data mining Anda.
Dari nol sampai bisa mengerjakan skripsi: dasar Python, Pandas, visualisasi, machine learning, clustering, time series, text mining, deep learning, RAG, hingga cara merapikan riset. Semua dikerjakan di Google Colab dengan data di Google Drive.
Cara memakai modul ini
Baca dan salin
Tiap bab berisi penjelasan singkat dan blok kode. Klik Salin lalu tempel ke Colab.
Atau unduh notebook
Setiap bab punya tombol Unduh notebook. Unggah ke Drive, buka dengan Colab, jalankan sel demi sel.
Kerjakan latihan
Coba dulu tanpa melihat kunci. Lalu uji diri dengan kuis dan ceklis di akhir bab.
Terapkan ke topik
Lihat peta bab ke topik pada katalog, lalu kerjakan salah satu proyek akhir.
Pilih jalur belajar
Jalur cepat, 8 minggu
| Minggu | Materi | Fokus |
|---|---|---|
| 1 | Bab 0 dan 1 | Colab, Drive, dasar Python |
| 2 | Bab 2 dan 3 | NumPy dan Pandas |
| 3 | Bab 4 | Visualisasi dan EDA |
| 4 | Bab 5 | Preprocessing dan pencegahan kebocoran data |
| 5 sampai 6 | Bab 6 | Machine learning dan evaluasi |
| 7 | Satu bab sesuai topik | Bab 7, 8, 9, atau 10 |
| 8 | Bab 12 dan proyek akhir | Merapikan dan melaporkan |
Jalur lengkap, 12 minggu
| Minggu | Materi | Fokus |
|---|---|---|
| 1 | Bab 0 dan 1 (awal) | Colab, Drive, variabel, koleksi data |
| 2 | Bab 1 (akhir) dan 2 | Fungsi, error, NumPy |
| 3 | Bab 3 | Pandas |
| 4 | Bab 4 | Visualisasi dan EDA |
| 5 | Bab 5 | Preprocessing |
| 6 sampai 7 | Bab 6 | Machine learning |
| 8 | Bab 7 | Clustering dan association rule |
| 9 | Bab 8 | Time series |
| 10 | Bab 9 | Text mining |
| 11 | Bab 10 dan 11 | Deep learning dan RAG |
| 12 | Bab 12 dan proyek akhir | Praktik riset dan demo |
Catatan kejujuran
- Data latihan bersifat sintetis atau bawaan scikit-learn, sehingga aman dari sisi privasi. Angka hasilnya untuk belajar, bukan temuan ilmiah.
- Semua blok kode yang tidak bertanda "Khusus Colab" sudah dijalankan berurutan di Python 3.12 dan berhasil. Blok bertanda "Khusus Colab" (pemasangan pustaka, model Hugging Face, PyTorch, GPU) belum dijalankan oleh penyusun; nama model dan versi pustaka mungkin perlu disesuaikan.
- Versi pustaka di Colab dapat berbeda dari lingkungan pengujian, sehingga angka bisa sedikit berbeda.
Lampiran
Panduan Google Colab dan Google Drive
Halaman ini merangkum kebiasaan kerja di Google Colab yang membuat penelitian Anda lancar dan aman dari kehilangan data.
1. Alur kerja harian
- Buka notebook dari Drive (klik dua kali, Open with → Google Colaboratory).
- Pilih runtime (CPU atau GPU) bila perlu.
- Jalankan sel Persiapan di bagian atas (mount Drive, variabel
BASE,SEED). - Muat data dari
BASE/data. Jangan mengubah data mentah; simpan data bersih sebagai file baru. - Kerjakan analisis. Simpan tabel ke
hasil/, gambar kegambar/, model kemodel/. - Sebelum selesai: Runtime → Restart and run all untuk memastikan notebook berjalan dari atas ke bawah.
- File → Save dan pastikan notebook berada di Drive.
2. Tiga cara mengambil data
a. Dari komputer sendiri (unggah manual ke Drive). Cara paling stabil: unggah file ke folder skripsi_datamining/data lewat drive.google.com, lalu baca dengan pd.read_csv(BASE / "data" / "nama.csv"). Untuk unggahan cepat sementara:
from google.colab import files
unggahan = files.upload() # pilih file dari komputer
nama_file = next(iter(unggahan))
import pandas as pd
df = pd.read_csv(nama_file) # file tersimpan di /content (sementara!)
df.to_csv(BASE / "data" / nama_file, index=False) # salin ke Drive agar permanen
b. Dari tautan langsung (bila situs mengizinkan unduhan):
import pandas as pd
url = "https://contoh.go.id/data/harga_pangan.csv" # ganti dengan tautan asli
df = pd.read_csv(url) # tambahkan sep=";" bila pemisahnya titik koma
df.to_csv(BASE / "data" / "harga_pangan.csv", index=False) # simpan salinan untuk reproduktibilitas
c. Dari repositori dataset. Simpan salinan hasil unduhan di data/ dan catat nama, versi, tanggal akses, dan lisensinya.
# --- UCI Machine Learning Repository ---
%pip install -q ucimlrepo
from ucimlrepo import fetch_ucirepo
uci = fetch_ucirepo(id=697) # cek id dataset di halaman UCI; 697 = dropout mahasiswa
X, y = uci.data.features, uci.data.targets
# --- Kaggle (dataset publik) ---
%pip install -q kagglehub
import kagglehub
folder = kagglehub.dataset_download("pemilik/nama-dataset") # salin slug dari URL Kaggle
# Beberapa dataset meminta login: jalankan kagglehub.login() lebih dulu.
# --- Hugging Face ---
from datasets import load_dataset
hf = load_dataset("indonlp/indonlu", "smsa", trust_remote_code=True)
3. Mengelola batas Colab
| Masalah | Penyebab umum | Cara mengatasi |
|---|---|---|
| Sesi terputus, variabel hilang | Tidak aktif terlalu lama atau batas waktu sesi | Simpan hasil antara ke Drive secara berkala; siapkan sel yang bisa dijalankan ulang |
| RAM habis (sesi crash) | Data terlalu besar | Baca sebagian (nrows=, usecols=), ambil sampel, ubah tipe (float32, category), hapus objek dengan del lalu gc.collect() |
| GPU tidak tersedia | Kuota GPU gratis dibatasi dan tidak dijamin | Coba lagi nanti, kurangi ukuran batch, kecilkan model, atau pakai CPU untuk prototipe |
| Latihan model lama | Data besar atau model berat | Uji pada sampel kecil lebih dulu, simpan checkpoint tiap beberapa epoch |
ModuleNotFoundError |
Pustaka belum terpasang | %pip install nama-pustaka, lalu restart runtime bila diminta |
FileNotFoundError |
Drive belum di-mount atau jalur salah | Jalankan sel mount, cek BASE dan list(BASE.iterdir()) |
import gc
# contoh menghemat memori
df = pd.read_csv(BASE / "data" / "besar.csv", usecols=["a", "b", "c"], nrows=200_000)
df["c"] = df["c"].astype("category")
df["a"] = pd.to_numeric(df["a"], downcast="float")
print(round(df.memory_usage(deep=True).sum() / 1e6, 1), "MB")
del df; gc.collect()
4. Template notebook untuk setiap eksperimen
Berkas template_notebook.ipynb di paket unduhan sudah berisi kerangka: judul, tujuan, sel persiapan, konfigurasi, bagian CRISP-DM, dan sel untuk menyimpan hasil. Salin file itu untuk setiap eksperimen baru dan beri nama bertanggal, misalnya 2026-10-05_churn_rf.ipynb.
Kebiasaan yang disarankan:
- Satu notebook untuk satu pertanyaan penelitian.
- Fungsi yang dipakai berulang dipindah ke sel khusus di bagian atas.
- Hasil akhir ditulis ke
hasil/dengan nama yang jelas, bukan hanya dilihat di layar. - Bila notebook membesar, pisah menjadi:
01_eda,02_modeling,03_evaluasi.
5. Kesalahan yang paling sering terjadi pada pemula
| Gejala | Kemungkinan penyebab | Perbaikan |
|---|---|---|
NameError: name 'df' is not defined |
Sel sebelumnya belum dijalankan | Jalankan sel dari atas, atau Run all |
| Hasil berubah setiap dijalankan | Tidak ada seed | Tetapkan random_state=SEED dan np.random.default_rng(SEED) |
| Skor sangat tinggi (mendekati 100%) | Kebocoran data atau target ikut menjadi fitur | Periksa urutan split dan kolom fitur (Bab 5) |
| Model mengabaikan kelas minoritas | Data tidak seimbang | Class weight, SMOTE dalam pipeline, ubah metrik (Bab 5) |
SettingWithCopyWarning |
Mengubah potongan DataFrame | Tambahkan .copy() |
Kolom tidak ditemukan (KeyError) |
Salah ketik atau spasi tersembunyi | Cek df.columns, gunakan .str.strip() |
| Grafik tidak muncul | Lupa plt.show() atau backend |
Panggil plt.show(); di Colab biasanya otomatis |
| Colab meminta izin Drive berulang | Sesi baru | Wajar; jalankan ulang sel mount |
Bab 0 dari 12
Persiapan: Google Colab dan Google Drive
Tujuan belajar
- Membuka dan menjalankan notebook di Google Colab
- Menghubungkan (mount) Google Drive dan membaca/menyimpan file
- Menyusun struktur folder proyek skripsi
- Memahami cara mencatat versi pustaka
Studi kasus: Menyiapkan folder proyek skripsi di Drive dan membaca file pertama
1. Apa itu Google Colab?
Google Colab adalah "buku catatan" Python yang berjalan di peramban. Komputernya milik Google, jadi laptop yang sederhana pun cukup. Satu notebook tersusun dari sel: sel teks (penjelasan) dan sel kode (program).
Cara memakainya:
- Buka colab.research.google.com, masuk dengan akun Google, pilih New notebook.
- Klik sel kode, lalu tekan Shift + Enter untuk menjalankannya.
- Menu Runtime → Change runtime type dipakai untuk memilih CPU atau T4 GPU (GPU hanya perlu untuk deep learning).
- Untuk membuka notebook dari modul ini: unggah file
.ipynbke Google Drive, klik dua kali, lalu pilih Open with → Google Colaboratory.
Aturan penting: variabel hanya hidup selama sesi. Jika sesi terputus atau runtime di-restart, variabel hilang dan sel harus dijalankan ulang dari atas. Karena itu, simpan hasil penting ke Google Drive.
2. Menghubungkan Google Drive
Drive adalah tempat penyimpanan permanen. Perintah drive.mount akan meminta izin akses; klik Connect to Google Drive dan izinkan. Setelah itu, Drive Anda muncul sebagai folder /content/drive/MyDrive.
Sel di bawah dipakai di awal setiap notebook dalam modul ini. Ia juga tetap berjalan bila dicoba di laptop (memakai folder lokal).
# @title Persiapan: hubungkan Google Drive dan siapkan folder kerja
from pathlib import Path
try:
from google.colab import drive
drive.mount("/content/drive")
BASE = Path("/content/drive/MyDrive/skripsi_datamining")
except ImportError: # dijalankan di luar Colab (misalnya laptop)
BASE = Path("./skripsi_datamining")
for sub in ["data", "hasil", "model", "gambar"]:
(BASE / sub).mkdir(parents=True, exist_ok=True)
SEED = 42 # angka acak tetap agar hasil bisa diulang
print("Folder kerja:", BASE.resolve() if not str(BASE).startswith("/content") else BASE)
3. Struktur folder proyek
Rapikan sejak awal agar skripsi mudah diulang dan diperiksa pembimbing:
skripsi_datamining/
├── data/ # data mentah (jangan diubah) dan data bersih
├── hasil/ # tabel metrik, grafik, ekspor Excel
├── model/ # model terlatih (.joblib)
├── gambar/ # gambar untuk naskah skripsi
└── requirements.txt
Kode di atas sudah membuat folder ini.
4. Mengecek versi Python dan pustaka
import sys, platform
import numpy, pandas, sklearn
print("Python :", sys.version.split()[0], "-", platform.system())
print("NumPy :", numpy.__version__)
print("Pandas :", pandas.__version__)
print("scikit-learn:", sklearn.__version__)
Catat versi ini di bab metodologi atau lampiran skripsi. Versi pustaka yang berbeda kadang menghasilkan angka yang sedikit berbeda.
5. Studi kasus: menulis dan membaca file di Drive
import pandas as pd
# 1. Buat tabel kecil
df = pd.DataFrame({"nama": ["Ani", "Budi", "Citra"], "nilai": [85, 72, 90]})
# 2. Simpan ke folder data di Drive
df.to_csv(BASE / "data" / "contoh.csv", index=False)
# 3. Baca kembali
hasil = pd.read_csv(BASE / "data" / "contoh.csv")
print(hasil)
# 4. Lihat isi folder data
print(sorted(p.name for p in (BASE / "data").iterdir()))
6. Memasang pustaka tambahan
Colab sudah membawa pandas, scikit-learn, dan matplotlib. Pustaka lain dipasang dengan %pip install. Sel berikut hanya berjalan di Colab, jadi tidak perlu dijalankan di laptop.
%pip install -q ucimlrepo kagglehub imbalanced-learn mlxtend shap optuna
# menyimpan daftar versi paket utama ke Drive (untuk lampiran skripsi)
!pip freeze | grep -iE "^(numpy|pandas|scikit-learn|matplotlib|seaborn)==" > "/content/drive/MyDrive/skripsi_datamining/requirements.txt"
7. Tiga cara mengambil data
| Sumber | Cara singkat |
|---|---|
| Google Drive | Unggah file ke data/, lalu pd.read_csv(BASE / "data" / "nama.csv") |
| Tautan langsung | pd.read_csv("https://.../data.csv") bila situs mengizinkan |
| Repositori | UCI: fetch_ucirepo(id=...); Kaggle: kagglehub.dataset_download("pemilik/nama"); Hugging Face: load_dataset("nama") |
Selalu baca lisensi dataset dan simpan salinan di data/ agar penelitian dapat diulang.
Latihan
Buat folder baru bernama laporan di dalam BASE, lalu cetak daftar semua folder di BASE.
Lihat kunci jawaban
nama_folder = BASE / "laporan"
nama_folder.mkdir(exist_ok=True)
print(sorted(p.name for p in BASE.iterdir() if p.is_dir()))
Simpan DataFrame nilai tiga mahasiswa ke file Excel hasil/nilai.xlsx, lalu baca kembali.
Lihat kunci jawaban
import pandas as pd
df = pd.DataFrame({"nama": ["Ani", "Budi", "Citra"], "nilai": [85, 72, 90]})
df.to_excel(BASE / "hasil" / "nilai.xlsx", index=False)
print(pd.read_excel(BASE / "hasil" / "nilai.xlsx"))
Kesalahan yang sering terjadi
- Lupa menjalankan sel `mount` sehingga folder Drive tidak ditemukan (FileNotFoundError).
- Menjalankan sel tidak berurutan sehingga variabel belum terdefinisi (NameError).
- Menyimpan hasil hanya di `/content/` (folder sementara) sehingga hilang saat sesi berakhir.
- Salah ketik jalur atau nama file; Python membedakan huruf besar dan kecil.
Kuis singkat
1. Mengapa hasil penting sebaiknya disimpan ke Google Drive?
2. Pintasan untuk menjalankan sel dan pindah ke sel berikutnya?
3. Runtime GPU paling dibutuhkan untuk...
Ceklis: saya sudah bisa...
Bab 1 dari 12
Dasar-dasar Python
Tujuan belajar
- Memakai variabel, tipe data, dan f-string
- Mengelola data dengan list, tuple, dictionary, dan set
- Membuat keputusan (if) dan pengulangan (for, while)
- Menulis fungsi dan list comprehension
- Menangani error dengan try/except
Studi kasus: Rekap nilai mahasiswa dengan dictionary dan fungsi
1. Variabel dan tipe data
Variabel adalah kotak berlabel untuk menyimpan nilai. Empat tipe dasar: int (bilangan bulat), float (desimal), str (teks), dan bool (benar/salah).
nama = "Sari"
umur = 21
ipk = 3.45
aktif = True
print(type(nama), type(umur), type(ipk), type(aktif))
print(f"{nama} berumur {umur} tahun, IPK {ipk:.2f}, aktif: {aktif}") # f-string
print(umur + 1, umur / 2, umur // 2, umur % 2, umur ** 2) # operator
2. String
Teks di Python bisa dipotong, digabung, dan diubah. Ini penting untuk bab text mining.
kalimat = " Aplikasinya BAGUS banget! "
print(kalimat.strip().lower())
print(kalimat.replace("banget", "sekali"))
print(kalimat.split())
print("bagus" in kalimat.lower())
print(kalimat.strip()[0:10]) # potongan karakter
3. Koleksi data: list, tuple, dictionary, set
| Struktur | Ciri | Contoh |
|---|---|---|
list |
berurutan, bisa diubah | [85, 90, 78] |
tuple |
berurutan, tidak bisa diubah | (1.5, 2.5) |
dict |
pasangan kunci: nilai | {"nama": "Ani"} |
set |
unik, tanpa urutan | {1, 2, 3} |
nilai = [85, 90, 78]
nilai.append(95)
print(nilai, len(nilai), nilai[0], nilai[-1], nilai[1:3])
titik = (1.5, 2.5) # tidak bisa diubah
mhs = {"nama": "Ani", "prodi": "Informatika", "ipk": 3.6}
mhs["angkatan"] = 2022
print(mhs["nama"], list(mhs.keys()))
kota = {"Manado", "Gorontalo", "Manado"}
print(kota) # duplikat otomatis hilang
4. Percabangan dan pengulangan
Blok kode ditentukan oleh indentasi (menjorok 4 spasi), bukan kurung kurawal.
skor = 78
if skor >= 85:
grade = "A"
elif skor >= 70:
grade = "B"
else:
grade = "C"
print(grade)
for angka in range(1, 6):
print(angka, end=" ")
print()
total, i = 0, 1
while i <= 5:
total += i
i += 1
print("total =", total)
for i, n in enumerate(["Ani", "Budi", "Citra"], start=1):
print(i, n)
5. Fungsi dan list comprehension
Fungsi membungkus langkah yang sering diulang. List comprehension adalah cara singkat membuat list baru.
def rata_rata(daftar):
"""Menghitung rata-rata dari sebuah list angka."""
return sum(daftar) / len(daftar)
print(rata_rata([85, 90, 78]))
kuadrat = [x ** 2 for x in range(1, 6)]
genap = [x for x in range(1, 11) if x % 2 == 0]
print(kuadrat, genap)
kapital = {k: v.upper() for k, v in {"a": "satu", "b": "dua"}.items()}
print(kapital)
6. Menangani error
Error bukan musuh. Baca pesan terakhir pada error; ia biasanya menyebut baris dan jenis masalah.
def bagi(a, b):
try:
return a / b
except ZeroDivisionError:
print("Pembagi tidak boleh nol")
return None
print(bagi(10, 2))
print(bagi(10, 0))
7. Modul dan pustaka
import memanggil kode milik orang lain. Pustaka bawaan Python: math, random, json, pathlib. Pustaka data science (NumPy, Pandas, ...) dibahas di bab berikutnya.
import math, random, json
print(math.sqrt(144), math.pi)
random.seed(SEED)
print(random.choice(["Ani", "Budi", "Citra"]), random.randint(1, 10))
8. Studi kasus: rekap nilai mahasiswa
data_nilai = {
"Ani": [85, 90, 78],
"Budi": [70, 65, 80],
"Citra": [95, 88, 92],
"Dedi": [55, 60, 48],
}
def predikat(rata):
if rata >= 85: return "A"
if rata >= 70: return "B"
if rata >= 55: return "C"
return "D"
rekap = {}
for nama, nilai in data_nilai.items():
rata = sum(nilai) / len(nilai)
rekap[nama] = {"rata": round(rata, 2), "predikat": predikat(rata)}
ranking = sorted(rekap.items(), key=lambda x: x[1]["rata"], reverse=True)
for urut, (nama, info) in enumerate(ranking, start=1):
print(f"{urut}. {nama:<6} {info['rata']:>6} ({info['predikat']})")
# simpan hasil ke Drive
with open(BASE / "hasil" / "rekap_nilai.json", "w", encoding="utf-8") as f:
json.dump(rekap, f, indent=2, ensure_ascii=False)
print("Tersimpan di", BASE / "hasil" / "rekap_nilai.json")
Latihan
Buat fungsi hitung_ipk(nilai_huruf) yang menerima list seperti ["A", "B", "A", "C"] (A=4, B=3, C=2, D=1) dan mengembalikan rata-ratanya.
Lihat kunci jawaban
def hitung_ipk(nilai_huruf):
bobot = {"A": 4, "B": 3, "C": 2, "D": 1}
return sum(bobot[h] for h in nilai_huruf) / len(nilai_huruf)
print(hitung_ipk(["A", "B", "A", "C"]))
Dari kalimat "saya suka belajar data mining dan saya suka python", hitung berapa kali tiap kata muncul memakai dictionary.
Lihat kunci jawaban
kalimat = "saya suka belajar data mining dan saya suka python"
hitung = {}
for kata in kalimat.split():
hitung[kata] = hitung.get(kata, 0) + 1
print(hitung)
Dengan list comprehension, ambil semua nilai di bawah 60 dari [45, 80, 59, 90, 60, 30].
Lihat kunci jawaban
nilai = [45, 80, 59, 90, 60, 30]
print([n for n in nilai if n < 60])
Kesalahan yang sering terjadi
- Indentasi tidak konsisten (campur spasi dan tab) menyebabkan IndentationError.
- Memakai `=` (isi variabel) padahal maksudnya `==` (membandingkan).
- Mengubah list saat sedang diulang dengan for.
- Lupa tanda titik dua `:` di akhir baris if, for, atau def.
Kuis singkat
1. Struktur data yang menyimpan pasangan kunci dan nilai adalah...
2. Hasil dari `[x*2 for x in range(3)]` adalah...
3. Apa fungsi `try/except`?
4. Hasil `10 // 3`?
Ceklis: saya sudah bisa...
Bab 2 dari 12
NumPy: Dasar Komputasi Numerik
Tujuan belajar
- Membuat dan memanipulasi array NumPy
- Melakukan operasi vektor tanpa loop
- Menghitung statistik dasar dengan sumbu (axis)
- Memakai bilangan acak dengan seed
Studi kasus: Statistik deskriptif 100 nilai ujian
1. Mengapa NumPy?
NumPy menyimpan angka dalam array yang jauh lebih cepat daripada list. Hampir semua pustaka data science (Pandas, scikit-learn) dibangun di atasnya.
import numpy as np
a = np.array([1, 2, 3, 4, 5])
m = np.array([[1, 2, 3], [4, 5, 6]])
print(a.shape, m.shape, m.ndim, m.dtype)
print(np.zeros((2, 3)))
print(np.arange(0, 10, 2), np.linspace(0, 1, 5))
2. Operasi vektor (tanpa loop)
harga = np.array([10000, 25000, 40000])
jumlah = np.array([3, 1, 2])
print(harga * jumlah) # perkalian per elemen
print((harga * jumlah).sum()) # total belanja
print(harga * 1.1) # naik 10%
print(np.sqrt(np.array([4, 9, 16])))
3. Indexing dan filter
x = np.array([12, 45, 7, 88, 23, 56])
print(x[0], x[-1], x[1:4])
print(x[x > 20]) # filter dengan kondisi (boolean mask)
x2 = x.copy()
x2[x2 < 20] = 0
print(x2)
m = np.arange(12).reshape(3, 4)
print(m)
print(m[1, 2], m[:, 0], m[0, :])
4. Statistik dan sumbu (axis)
axis=0 bekerja ke bawah (per kolom), axis=1 bekerja ke samping (per baris).
nilai = np.array([[80, 90, 70], [60, 75, 65], [95, 85, 90]])
print("rata-rata semua :", nilai.mean())
print("per kolom (mtk..):", nilai.mean(axis=0))
print("per mahasiswa :", nilai.mean(axis=1))
print(nilai.max(), nilai.min(), nilai.std().round(2), np.median(nilai))
5. Bilangan acak dan seed
Data acak dipakai untuk simulasi dan pembagian data. Seed membuat hasil acak bisa diulang. Ini wajib untuk penelitian yang dapat direproduksi.
rng = np.random.default_rng(42)
print(rng.integers(1, 100, size=5))
print(rng.normal(loc=70, scale=10, size=3).round(1))
rng2 = np.random.default_rng(42) # seed sama -> hasil sama
print(rng2.integers(1, 100, size=5))
6. Studi kasus: statistik 100 nilai ujian
rng = np.random.default_rng(SEED)
nilai = np.clip(rng.normal(72, 12, 100), 0, 100).round(1)
print("jumlah :", nilai.size)
print("rata2 :", nilai.mean().round(2))
print("median :", np.median(nilai))
print("std :", nilai.std().round(2))
print("min/max :", nilai.min(), nilai.max())
print("persentil 25/75:", np.percentile(nilai, [25, 75]))
lulus = nilai >= 60
print("persen lulus:", (lulus.mean() * 100).round(1), "%")
# normalisasi min-max dan z-score (dipakai di bab preprocessing)
minmax = (nilai - nilai.min()) / (nilai.max() - nilai.min())
z = (nilai - nilai.mean()) / nilai.std()
print(minmax[:3].round(3), z[:3].round(3))
np.save(BASE / "data" / "nilai_ujian.npy", nilai)
Latihan
Buat array 1 sampai 20, lalu tampilkan hanya bilangan yang habis dibagi 3.
Lihat kunci jawaban
import numpy as np
a = np.arange(1, 21)
print(a[a % 3 == 0])
Untuk matriks 4x3 acak (seed 1, bilangan bulat 0-100), hitung rata-rata tiap kolom dan indeks baris dengan jumlah terbesar.
Lihat kunci jawaban
import numpy as np
rng = np.random.default_rng(1)
m = rng.integers(0, 101, size=(4, 3))
print(m)
print(m.mean(axis=0))
print(m.sum(axis=1).argmax())
Kesalahan yang sering terjadi
- Mengira `axis=0` berarti per baris (yang benar: per kolom).
- Lupa `.copy()` sehingga mengubah array asli tanpa sadar.
- Memakai loop for pada array besar padahal operasi vektor jauh lebih cepat.
Kuis singkat
1. Untuk rata-rata tiap kolom pada matriks, gunakan...
2. Mengapa memakai seed?
3. Filter `x[x > 20]` mengembalikan...
Ceklis: saya sudah bisa...
Bab 3 dari 12
Pandas: Membaca dan Membersihkan Data
Tujuan belajar
- Membaca CSV dan Excel dari Google Drive
- Menjelajah data: head, info, describe
- Memilih dan memfilter data
- Menangani data kosong dan duplikat
- Meringkas dengan groupby dan menggabungkan tabel (merge)
- Bekerja dengan tanggal
Studi kasus: Membersihkan data akademik mahasiswa (sintetis)
1. DataFrame
Pandas menyediakan DataFrame, yaitu tabel seperti Excel dengan baris dan kolom bernama. Ini alat utama pekerjaan data mining.
Agar kita berlatih dengan data yang mirip kenyataan, sel di bawah membuat data mahasiswa sintetis (bukan data asli, jadi aman dari sisi privasi) dan menyimpannya sebagai CSV di Drive. Kita sengaja menambahkan data kosong dan duplikat.
import numpy as np
import pandas as pd
def buat_data_mahasiswa(n=400, seed=42):
"""Membuat data mahasiswa SINTETIS (bukan data asli) untuk latihan."""
rng = np.random.default_rng(seed)
prodi = rng.choice(["Informatika", "Sistem Informasi", "Manajemen", "Akuntansi"], n, p=[.35, .25, .2, .2])
angkatan = rng.choice([2019, 2020, 2021, 2022], n)
ipk1 = np.clip(rng.normal(3.0, 0.5, n), 1.0, 4.0).round(2)
ipk2 = np.clip(ipk1 + rng.normal(0, 0.3, n), 1.0, 4.0).round(2)
hadir = np.clip(rng.normal(80, 12, n), 20, 100).round(0)
beasiswa = rng.choice(["Ya", "Tidak"], n, p=[.2, .8])
bekerja = rng.choice(["Ya", "Tidak"], n, p=[.25, .75])
jarak = np.clip(rng.gamma(2, 6, n), 0.5, 80).round(1)
skor = (-3.0 * (ipk2 - 2.6) - 0.05 * (hadir - 75) + 0.8 * (bekerja == "Ya")
- 0.9 * (beasiswa == "Ya") + 0.02 * jarak - 1.2)
p = 1 / (1 + np.exp(-skor))
dropout = rng.random(n) < p
return pd.DataFrame({
"nim": [f"{a}{i:04d}" for i, a in enumerate(angkatan)],
"prodi": prodi, "angkatan": angkatan,
"tgl_masuk": pd.to_datetime(angkatan.astype(str) + "-09-01"),
"ipk_smt1": ipk1, "ipk_smt2": ipk2, "kehadiran": hadir,
"beasiswa": beasiswa, "bekerja": bekerja, "jarak_km": jarak,
"status": np.where(dropout, "Dropout", "Bertahan"),
})
df_awal = buat_data_mahasiswa(400, seed=SEED)
# rusak sedikit agar mirip data lapangan
rng = np.random.default_rng(SEED)
df_kotor = df_awal.copy()
df_kotor.loc[rng.choice(400, 25, replace=False), "ipk_smt2"] = np.nan
df_kotor.loc[rng.choice(400, 15, replace=False), "kehadiran"] = np.nan
df_kotor = pd.concat([df_kotor, df_kotor.sample(8, random_state=1)], ignore_index=True) # 8 baris duplikat
df_kotor.to_csv(BASE / "data" / "mahasiswa_kotor.csv", index=False)
print("Tersimpan:", (BASE / "data" / "mahasiswa_kotor.csv").name, df_kotor.shape)
2. Membaca file dari Drive
df = pd.read_csv(BASE / "data" / "mahasiswa_kotor.csv", parse_dates=["tgl_masuk"])
# Untuk Excel: pd.read_excel(BASE / "data" / "file.xlsx", sheet_name="Sheet1")
# Untuk pemisah titik koma: pd.read_csv(..., sep=";")
print(df.head())
3. Mengenal data (langkah wajib pertama)
print(df.shape) # (baris, kolom)
df.info() # tipe data dan jumlah nilai terisi
print(df.describe().round(2)) # statistik kolom numerik
print(df["status"].value_counts())
print(df["prodi"].value_counts(normalize=True).round(3))
4. Memilih dan memfilter
print(df["ipk_smt1"].head(3)) # satu kolom
print(df[["nim", "prodi", "ipk_smt1"]].head(3)) # beberapa kolom
print(df.loc[0:2, ["nim", "status"]]) # baris/kolom berdasarkan label
print(df.iloc[0:2, 0:3]) # berdasarkan posisi
tinggi = df[(df["ipk_smt1"] >= 3.5) & (df["prodi"] == "Informatika")]
print(len(tinggi), "mahasiswa Informatika dengan IPK smt1 >= 3.5")
print(df.query("status == 'Dropout' and bekerja == 'Ya'").shape[0], "dropout yang bekerja")
5. Data kosong dan duplikat
Data kosong (NaN) tidak boleh diabaikan begitu saja. Pilihannya: hapus baris, isi dengan statistik (rata-rata/median), atau isi dengan model. Pilihan harus dijelaskan di skripsi.
print(df.isna().sum())
print("duplikat:", df.duplicated().sum())
bersih = df.drop_duplicates().copy()
bersih["ipk_smt2"] = bersih["ipk_smt2"].fillna(bersih["ipk_smt2"].median())
bersih["kehadiran"] = bersih["kehadiran"].fillna(bersih["kehadiran"].median())
print("setelah dibersihkan:", bersih.shape, "| kosong:", int(bersih.isna().sum().sum()))
6. Kolom baru, groupby, dan merge
bersih["tren_ipk"] = bersih["ipk_smt2"] - bersih["ipk_smt1"]
bersih["kehadiran_rendah"] = bersih["kehadiran"] < 70
ringkas = (bersih.groupby("prodi")
.agg(jumlah=("nim", "count"),
ipk_rata2=("ipk_smt2", "mean"),
persen_dropout=("status", lambda s: (s == "Dropout").mean() * 100))
.round(2)
.sort_values("persen_dropout", ascending=False))
print(ringkas)
# merge: gabungkan dengan tabel fakultas
fakultas = pd.DataFrame({"prodi": ["Informatika", "Sistem Informasi", "Manajemen", "Akuntansi"],
"fakultas": ["Ilmu Komputer", "Ilmu Komputer", "Ekonomi", "Ekonomi"]})
gabung = bersih.merge(fakultas, on="prodi", how="left")
print(gabung.groupby("fakultas")["ipk_smt2"].mean().round(2))
# tabel silang
print(pd.crosstab(bersih["bekerja"], bersih["status"], normalize="index").round(3))
7. Tanggal
bersih["tahun_masuk"] = bersih["tgl_masuk"].dt.year
bersih["bulan_masuk"] = bersih["tgl_masuk"].dt.month_name()
print(bersih[["tgl_masuk", "tahun_masuk", "bulan_masuk"]].drop_duplicates().head())
print(bersih.groupby("tahun_masuk")["status"].apply(lambda s: (s == "Dropout").mean()).round(3))
8. Studi kasus: simpan data bersih untuk bab berikutnya
bersih.to_csv(BASE / "data" / "mahasiswa_bersih.csv", index=False)
ringkas.to_excel(BASE / "hasil" / "ringkasan_prodi.xlsx")
print("Data bersih:", bersih.shape)
print(ringkas)
Latihan
Tampilkan lima mahasiswa dengan ipk_smt2 tertinggi (kolom nim, prodi, ipk_smt2).
Lihat kunci jawaban
print(bersih.nlargest(5, "ipk_smt2")[["nim", "prodi", "ipk_smt2"]])
Hitung rata-rata jarak rumah (jarak_km) untuk mahasiswa dropout dan bertahan.
Lihat kunci jawaban
print(bersih.groupby("status")["jarak_km"].mean().round(2))
Buat kolom kategori_ipk: "Tinggi" jika ipk_smt2 >= 3.5, "Sedang" jika >= 2.75, selain itu "Rendah". Hitung jumlah tiap kategori.
Lihat kunci jawaban
import numpy as np
bersih["kategori_ipk"] = np.where(bersih["ipk_smt2"] >= 3.5, "Tinggi", np.where(bersih["ipk_smt2"] >= 2.75, "Sedang", "Rendah"))
print(bersih["kategori_ipk"].value_counts())
Kesalahan yang sering terjadi
- Memakai `and`/`or` pada filter DataFrame; yang benar `&` dan `|` dengan tanda kurung.
- Lupa `.copy()` sehingga muncul SettingWithCopyWarning.
- Mengisi data kosong sebelum membagi data latih dan uji (menyebabkan kebocoran data, dibahas di Bab 5).
- Membaca CSV dengan pemisah yang salah sehingga semua data menumpuk di satu kolom.
Kuis singkat
1. Perintah untuk melihat jumlah data kosong per kolom?
2. Untuk menggabungkan dua tabel berdasarkan kolom yang sama, gunakan...
3. Filter dua kondisi sekaligus yang benar adalah...
Ceklis: saya sudah bisa...
Bab 4 dari 12
Visualisasi Data dan EDA
Tujuan belajar
- Membuat grafik dengan Matplotlib, Seaborn, dan Plotly
- Memahami distribusi, korelasi, dan outlier
- Menjalankan checklist EDA sebelum modeling
- Menyimpan gambar untuk naskah skripsi
Studi kasus: EDA data dropout mahasiswa (sintetis)
1. Mengapa EDA?
Exploratory Data Analysis (EDA) adalah tahap "berkenalan" dengan data sebelum membuat model. Tujuannya menemukan masalah (data kosong, outlier, kelas tidak seimbang) dan petunjuk (variabel yang tampak berhubungan dengan target).
Kita memakai data mahasiswa sintetis dengan target status (Dropout atau Bertahan).
import numpy as np
import pandas as pd
def buat_data_mahasiswa(n=400, seed=42):
"""Membuat data mahasiswa SINTETIS (bukan data asli) untuk latihan."""
rng = np.random.default_rng(seed)
prodi = rng.choice(["Informatika", "Sistem Informasi", "Manajemen", "Akuntansi"], n, p=[.35, .25, .2, .2])
angkatan = rng.choice([2019, 2020, 2021, 2022], n)
ipk1 = np.clip(rng.normal(3.0, 0.5, n), 1.0, 4.0).round(2)
ipk2 = np.clip(ipk1 + rng.normal(0, 0.3, n), 1.0, 4.0).round(2)
hadir = np.clip(rng.normal(80, 12, n), 20, 100).round(0)
beasiswa = rng.choice(["Ya", "Tidak"], n, p=[.2, .8])
bekerja = rng.choice(["Ya", "Tidak"], n, p=[.25, .75])
jarak = np.clip(rng.gamma(2, 6, n), 0.5, 80).round(1)
skor = (-3.0 * (ipk2 - 2.6) - 0.05 * (hadir - 75) + 0.8 * (bekerja == "Ya")
- 0.9 * (beasiswa == "Ya") + 0.02 * jarak - 1.2)
p = 1 / (1 + np.exp(-skor))
dropout = rng.random(n) < p
return pd.DataFrame({
"nim": [f"{a}{i:04d}" for i, a in enumerate(angkatan)],
"prodi": prodi, "angkatan": angkatan,
"tgl_masuk": pd.to_datetime(angkatan.astype(str) + "-09-01"),
"ipk_smt1": ipk1, "ipk_smt2": ipk2, "kehadiran": hadir,
"beasiswa": beasiswa, "bekerja": bekerja, "jarak_km": jarak,
"status": np.where(dropout, "Dropout", "Bertahan"),
})
import matplotlib.pyplot as plt
import seaborn as sns
sns.set_theme(style="whitegrid", context="notebook")
df = buat_data_mahasiswa(400, seed=SEED)
print(df.shape)
print(df["status"].value_counts())
2. Matplotlib: grafik dasar
plt.subplots membuat kanvas. Selalu beri judul dan label sumbu karena grafik tanpa label tidak bisa dipakai di skripsi.
fig, ax = plt.subplots(1, 2, figsize=(10, 3.6))
df["status"].value_counts().plot(kind="bar", ax=ax[0], color=["#0e2250", "#c98a0a"])
ax[0].set_title("Jumlah mahasiswa per status"); ax[0].set_xlabel(""); ax[0].set_ylabel("Jumlah")
ax[1].hist(df["ipk_smt2"], bins=20, color="#0b8f6b", edgecolor="white")
ax[1].set_title("Distribusi IPK semester 2"); ax[1].set_xlabel("IPK"); ax[1].set_ylabel("Frekuensi")
plt.tight_layout()
plt.savefig(BASE / "gambar" / "eda_dasar.png", dpi=150)
plt.show()
3. Seaborn: distribusi dan perbandingan kelompok
fig, ax = plt.subplots(1, 3, figsize=(14, 3.8))
sns.boxplot(data=df, x="status", y="ipk_smt2", ax=ax[0])
ax[0].set_title("IPK smt 2 menurut status")
sns.kdeplot(data=df, x="kehadiran", hue="status", fill=True, common_norm=False, ax=ax[1])
ax[1].set_title("Kehadiran menurut status")
sns.countplot(data=df, x="bekerja", hue="status", ax=ax[2])
ax[2].set_title("Bekerja vs status")
plt.tight_layout(); plt.show()
4. Korelasi
Korelasi mengukur hubungan linear antar-variabel numerik (−1 sampai +1). Korelasi bukan sebab-akibat.
num = df[["ipk_smt1", "ipk_smt2", "kehadiran", "jarak_km"]].copy()
num["dropout"] = (df["status"] == "Dropout").astype(int)
korelasi = num.corr().round(2)
print(korelasi["dropout"].sort_values())
plt.figure(figsize=(5.5, 4.2))
sns.heatmap(korelasi, annot=True, cmap="RdBu_r", center=0, vmin=-1, vmax=1)
plt.title("Matriks korelasi"); plt.tight_layout(); plt.show()
5. Outlier dengan aturan IQR
def batas_iqr(s):
q1, q3 = s.quantile([.25, .75])
iqr = q3 - q1
return q1 - 1.5 * iqr, q3 + 1.5 * iqr
bawah, atas = batas_iqr(df["jarak_km"])
outlier = df[(df["jarak_km"] < bawah) | (df["jarak_km"] > atas)]
print(f"Batas normal jarak: {bawah:.1f} - {atas:.1f} km; outlier: {len(outlier)} baris")
6. Plotly: grafik interaktif
import plotly.express as px
fig = px.scatter(df, x="ipk_smt2", y="kehadiran", color="status",
hover_data=["prodi", "bekerja"], title="IPK vs kehadiran")
fig.write_html(BASE / "gambar" / "scatter_interaktif.html") # bisa dibuka di peramban
# fig.show() # tampilkan di notebook
print("Grafik interaktif tersimpan.")
7. Studi kasus: checklist EDA
Jalankan lima pertanyaan ini pada setiap dataset skripsi dan tuliskan temuannya di bab pembahasan.
print("1. Ukuran data :", df.shape)
print("2. Data kosong :", int(df.isna().sum().sum()))
print("3. Duplikat :", int(df.duplicated().sum()))
print("4. Keseimbangan kelas:")
print(df["status"].value_counts(normalize=True).round(3))
print("5. Dropout per prodi:")
print(df.groupby("prodi")["status"].apply(lambda s: (s == "Dropout").mean()).round(3).sort_values(ascending=False))
Latihan
Buat boxplot jarak_km menurut status dan simpan sebagai gambar/jarak_status.png.
Lihat kunci jawaban
plt.figure(figsize=(4.5, 3.5))
sns.boxplot(data=df, x="status", y="jarak_km")
plt.title("Jarak rumah menurut status")
plt.savefig(BASE / "gambar" / "jarak_status.png", dpi=150)
plt.show()
Hitung persentase dropout untuk mahasiswa penerima beasiswa dan bukan penerima.
Lihat kunci jawaban
print(df.groupby("beasiswa")["status"].apply(lambda s: (s == "Dropout").mean() * 100).round(1))
Kesalahan yang sering terjadi
- Grafik tanpa judul, label sumbu, atau satuan.
- Menyimpulkan sebab-akibat dari korelasi.
- Memakai diagram lingkaran untuk banyak kategori (sulit dibaca).
- Lupa `plt.tight_layout()` sehingga label saling menimpa.
Kuis singkat
1. Boxplot paling cocok untuk melihat...
2. Korelasi 0,9 antara dua variabel berarti...
3. Apa yang dilakukan `plt.savefig`?
Ceklis: saya sudah bisa...
Bab 5 dari 12
Preprocessing dan Feature Engineering
Tujuan belajar
- Menangani data kosong, kategori, dan skala dengan benar
- Mengenali dan mencegah kebocoran data (data leakage)
- Menangani data tidak seimbang dengan SMOTE atau class weight
- Menyusun Pipeline yang rapi dan bisa diulang
Studi kasus: Menyiapkan data churn pelanggan
1. Prinsip utama
Algoritma hanya memahami angka. Karena itu data harus diubah: nilai kosong diisi, kategori diubah jadi angka, skala disamakan. Aturan emas: semua penyesuaian (median, skala, oversampling) dipelajari hanya dari data latih, lalu diterapkan ke data uji. Jika tidak, terjadi kebocoran data dan skor menjadi terlalu bagus.
Kita memakai data churn pelanggan sintetis.
import numpy as np
import pandas as pd
def buat_data_churn(n=1500, seed=42):
"""Membuat data pelanggan telekomunikasi SINTETIS untuk latihan."""
rng = np.random.default_rng(seed)
tenure = rng.integers(1, 72, n)
kontrak = rng.choice(["Bulanan", "1 tahun", "2 tahun"], n, p=[.55, .25, .2])
internet = rng.choice(["Fiber", "DSL", "Tidak ada"], n, p=[.45, .4, .15])
bayar = rng.choice(["Transfer", "Kartu kredit", "E-wallet"], n)
biaya = np.round(rng.normal(70, 25, n).clip(20, 140), 1)
keluhan = rng.poisson(1.0, n)
skor = (-0.045 * tenure + 0.012 * (biaya - 70) + 0.55 * keluhan
+ 1.2 * (kontrak == "Bulanan") - 0.9 * (kontrak == "2 tahun")
+ 0.4 * (internet == "Fiber") - 2.3)
churn = rng.random(n) < 1 / (1 + np.exp(-skor))
df = pd.DataFrame({
"pelanggan_id": [f"C{i:05d}" for i in range(n)],
"tenure_bulan": tenure, "biaya_bulanan": biaya, "kontrak": kontrak,
"internet": internet, "metode_bayar": bayar, "keluhan": keluhan,
"churn": np.where(churn, "Ya", "Tidak"),
})
df.loc[rng.choice(n, 60, replace=False), "biaya_bulanan"] = np.nan # sengaja ada data kosong
return df
df = buat_data_churn(1500, seed=SEED)
print(df.head(3))
print(df["churn"].value_counts(normalize=True).round(3))
print(df.isna().sum()[df.isna().sum() > 0])
2. Pisahkan data lebih dulu
stratify=y menjaga proporsi kelas tetap sama pada data latih dan uji.
from sklearn.model_selection import train_test_split
X = df.drop(columns=["pelanggan_id", "churn"])
y = (df["churn"] == "Ya").astype(int)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, stratify=y, random_state=SEED)
print(X_train.shape, X_test.shape, y_train.mean().round(3), y_test.mean().round(3))
3. Imputasi, encoding, dan scaling dengan ColumnTransformer
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import OneHotEncoder, StandardScaler
kolom_num = ["tenure_bulan", "biaya_bulanan", "keluhan"]
kolom_kat = ["kontrak", "internet", "metode_bayar"]
prep = ColumnTransformer([
("num", Pipeline([("isi", SimpleImputer(strategy="median")),
("skala", StandardScaler())]), kolom_num),
("kat", OneHotEncoder(handle_unknown="ignore"), kolom_kat),
])
X_train_p = prep.fit_transform(X_train) # belajar HANYA dari data latih
X_test_p = prep.transform(X_test) # hanya menerapkan
print(X_train_p.shape, X_test_p.shape)
print(prep.get_feature_names_out()[:6])
4. Data tidak seimbang
Jika hanya 12% pelanggan churn, model "tebak semua tidak churn" sudah akurat 88% tetapi tidak berguna. Tiga cara menanganinya: class weight, SMOTE (membuat contoh sintetis kelas minoritas), dan mengatur threshold. Metrik yang tepat: recall, F1, PR-AUC, bukan akurasi.
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import f1_score, recall_score, precision_score, accuracy_score
from imblearn.pipeline import Pipeline as ImbPipeline
from imblearn.over_sampling import SMOTE
def laporan(nama, model):
pred = model.predict(X_test)
print(f"{nama:<22} acc={accuracy_score(y_test, pred):.3f} "
f"prec={precision_score(y_test, pred, zero_division=0):.3f} "
f"recall={recall_score(y_test, pred):.3f} f1={f1_score(y_test, pred):.3f}")
polos = Pipeline([("prep", prep), ("model", LogisticRegression(max_iter=1000))])
berbobot = Pipeline([("prep", prep), ("model", LogisticRegression(max_iter=1000, class_weight="balanced"))])
smote = ImbPipeline([("prep", prep), ("smote", SMOTE(random_state=SEED)),
("model", LogisticRegression(max_iter=1000))])
for nama, m in [("Tanpa penanganan", polos), ("Class weight", berbobot), ("SMOTE di pipeline", smote)]:
m.fit(X_train, y_train)
laporan(nama, m)
Perhatikan: akurasi turun (model tidak lagi sekadar menebak "tidak churn"), tetapi recall kelas churn naik tajam. Angka ini berasal dari data sintetis yang sinyalnya lemah, jadi F1 tetap rendah; yang penting adalah membaca arah perubahannya. Pipeline dari imblearn menjalankan SMOTE hanya pada data latih di dalam setiap tahap pelatihan.
5. Demonstrasi kebocoran data
Kesalahan umum: melakukan SMOTE atau scaling pada seluruh data sebelum membagi. Data uji ikut "bocor" ke pelatihan sehingga skornya menipu.
from sklearn.ensemble import RandomForestClassifier
X_num = pd.get_dummies(X.fillna(X.median(numeric_only=True)), drop_first=True).astype(float)
# CARA SALAH: SMOTE sebelum split
Xb, yb = SMOTE(random_state=SEED).fit_resample(X_num, y)
Xtr, Xte, ytr, yte = train_test_split(Xb, yb, test_size=0.2, random_state=SEED)
salah = RandomForestClassifier(random_state=SEED).fit(Xtr, ytr)
print("SALAH (SMOTE sebelum split) F1:", round(f1_score(yte, salah.predict(Xte)), 3))
# CARA BENAR: split dulu, SMOTE hanya pada data latih
Xtr2, Xte2, ytr2, yte2 = train_test_split(X_num, y, test_size=0.2, stratify=y, random_state=SEED)
Xs, ys = SMOTE(random_state=SEED).fit_resample(Xtr2, ytr2)
benar = RandomForestClassifier(random_state=SEED).fit(Xs, ys)
print("BENAR (split dulu) F1:", round(f1_score(yte2, benar.predict(Xte2)), 3))
Skor pada cara yang salah tampak jauh lebih tinggi karena data uji berisi tetangga sintetis dari data latih (selisihnya sangat besar pada data ini; pada data nyata biasanya lebih kecil, tetapi tetap menyesatkan). Skor yang lebih rendah pada cara benar adalah gambaran jujur.
6. Feature engineering sederhana
fe = X_train.copy()
fe["biaya_per_bulan_langganan"] = fe["biaya_bulanan"] / fe["tenure_bulan"]
fe["pelanggan_baru"] = (fe["tenure_bulan"] <= 6).astype(int)
fe["banyak_keluhan"] = (fe["keluhan"] >= 3).astype(int)
print(fe[["tenure_bulan", "biaya_bulanan", "biaya_per_bulan_langganan", "pelanggan_baru", "banyak_keluhan"]].head())
7. Studi kasus: simpan pipeline persiapan
import joblib
joblib.dump(prep, BASE / "model" / "preprocessor_churn.joblib")
df.to_csv(BASE / "data" / "churn_sintetis.csv", index=False)
print("Preprocessor dan data tersimpan.")
Latihan
Ganti SimpleImputer(strategy="median") dengan "mean" pada pipeline numerik, latih ulang model polos, dan bandingkan F1-nya.
Lihat kunci jawaban
prep2 = ColumnTransformer([
("num", Pipeline([("isi", SimpleImputer(strategy="mean")), ("skala", StandardScaler())]), kolom_num),
("kat", OneHotEncoder(handle_unknown="ignore"), kolom_kat),
])
m2 = Pipeline([("prep", prep2), ("model", LogisticRegression(max_iter=1000))]).fit(X_train, y_train)
print(round(f1_score(y_test, m2.predict(X_test)), 3))
Cari threshold probabilitas (0.2 sampai 0.6) yang memberi F1 terbaik pada model berbobot.
Lihat kunci jawaban
import numpy as np
proba = berbobot.predict_proba(X_test)[:, 1]
for t in np.arange(0.2, 0.65, 0.1):
print(round(t, 1), round(f1_score(y_test, proba >= t), 3))
Kesalahan yang sering terjadi
- Melakukan scaling atau imputasi pada seluruh data sebelum split (kebocoran data).
- Memakai SMOTE pada data uji.
- Memakai akurasi sebagai satu-satunya metrik pada data tidak seimbang.
- Meng-encode kategori dengan angka 1, 2, 3 padahal tidak berurutan (pakai One-Hot).
Kuis singkat
1. Kapan `fit` scaler dijalankan?
2. Metrik yang tepat untuk data sangat tidak seimbang?
3. Keuntungan Pipeline?
Ceklis: saya sudah bisa...
Bab 6 dari 12
Machine Learning dengan scikit-learn
Tujuan belajar
- Melatih dan mengevaluasi model klasifikasi dan regresi
- Memakai cross-validation dan memilih metrik yang tepat
- Melakukan tuning hyperparameter (GridSearch dan Optuna)
- Menginterpretasi model dengan feature importance dan SHAP
- Menyimpan dan memuat model
Studi kasus: Klasifikasi kanker payudara (dataset nyata bawaan scikit-learn)
1. Alur kerja machine learning
data → split → pipeline (prep + model) → evaluasi → tuning → interpretasi → simpan
Dataset yang dipakai adalah Breast Cancer Wisconsin (data nyata, 569 pasien, sudah ada di scikit-learn sehingga tidak perlu diunduh). Hasil hanya untuk latihan akademik, bukan alat diagnosis.
import numpy as np, pandas as pd
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
data = load_breast_cancer(as_frame=True)
X, y = data.data, data.target # y: 0 = ganas (malignant), 1 = jinak (benign)
print(X.shape, y.value_counts().to_dict())
print(list(X.columns[:5]))
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, stratify=y, random_state=SEED)
2. Baseline dulu
Selalu bandingkan model dengan baseline sederhana. Model yang tidak lebih baik dari tebakan kelas mayoritas belum berguna.
from sklearn.dummy import DummyClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import StratifiedKFold, cross_val_score
model = {
"Baseline (mayoritas)": DummyClassifier(strategy="most_frequent"),
"Logistic Regression": make_pipeline(StandardScaler(), LogisticRegression(max_iter=2000)),
"Random Forest": RandomForestClassifier(n_estimators=200, random_state=SEED),
"Gradient Boosting": GradientBoostingClassifier(random_state=SEED),
}
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=SEED)
hasil = {}
for nama, m in model.items():
skor = cross_val_score(m, X_train, y_train, cv=cv, scoring="f1")
hasil[nama] = (skor.mean(), skor.std())
print(f"{nama:<22} F1 = {skor.mean():.3f} (+/- {skor.std():.3f})")
3. Evaluasi pada data uji
Pahami confusion matrix: TP (benar terdeteksi), FP (salah alarm), FN (terlewat), TN. Pada kesehatan, FN biasanya lebih berbahaya sehingga recall penting.
from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score, ConfusionMatrixDisplay
import matplotlib.pyplot as plt
terbaik = model["Random Forest"].fit(X_train, y_train)
pred = terbaik.predict(X_test)
proba = terbaik.predict_proba(X_test)[:, 1]
print(classification_report(y_test, pred, target_names=["ganas", "jinak"]))
print("ROC-AUC:", round(roc_auc_score(y_test, proba), 4))
print(confusion_matrix(y_test, pred))
ConfusionMatrixDisplay.from_predictions(y_test, pred, display_labels=["ganas", "jinak"], cmap="Blues")
plt.title("Confusion matrix - Random Forest")
plt.savefig(BASE / "gambar" / "cm_rf.png", dpi=150, bbox_inches="tight")
plt.show()
4. Tuning hyperparameter
Hyperparameter adalah pengaturan model yang kita tentukan sendiri (misal jumlah pohon). GridSearchCV mencoba semua kombinasi; Optuna mencari lebih cerdas.
from sklearn.model_selection import GridSearchCV
grid = GridSearchCV(
RandomForestClassifier(random_state=SEED),
param_grid={"n_estimators": [100, 200], "max_depth": [None, 5, 10], "min_samples_leaf": [1, 3]},
scoring="f1", cv=3, n_jobs=-1)
grid.fit(X_train, y_train)
print("Terbaik:", grid.best_params_, "F1 CV =", round(grid.best_score_, 3))
import optuna
optuna.logging.set_verbosity(optuna.logging.WARNING)
def objective(trial):
m = RandomForestClassifier(
n_estimators=trial.suggest_int("n_estimators", 50, 300),
max_depth=trial.suggest_int("max_depth", 3, 15),
min_samples_leaf=trial.suggest_int("min_samples_leaf", 1, 8),
random_state=SEED, n_jobs=-1)
return cross_val_score(m, X_train, y_train, cv=3, scoring="f1").mean()
studi = optuna.create_study(direction="maximize", sampler=optuna.samplers.TPESampler(seed=SEED))
studi.optimize(objective, n_trials=12)
print("Terbaik:", studi.best_params, "F1 =", round(studi.best_value, 3))
5. Interpretasi: feature importance dan SHAP
Reviewer skripsi sering bertanya "fitur mana yang paling berpengaruh dan mengapa?". SHAP menjelaskan kontribusi tiap fitur pada tiap prediksi.
importansi = pd.Series(terbaik.feature_importances_, index=X.columns).sort_values(ascending=False)
print(importansi.head(5).round(3))
plt.figure(figsize=(6, 3.5))
importansi.head(10)[::-1].plot(kind="barh", color="#0b8f6b")
plt.title("10 fitur terpenting (Random Forest)"); plt.tight_layout(); plt.show()
import shap
explainer = shap.TreeExplainer(terbaik)
sv = explainer(X_test)
nilai = sv.values[:, :, 1] if sv.values.ndim == 3 else sv.values # kelas "jinak"
shap.summary_plot(nilai, X_test, show=False, max_display=10)
plt.title("SHAP summary"); plt.tight_layout()
plt.savefig(BASE / "gambar" / "shap_summary.png", dpi=150, bbox_inches="tight")
plt.show()
6. Sekilas regresi
from sklearn.datasets import load_diabetes
from sklearn.metrics import mean_squared_error, r2_score, mean_absolute_error
Xd, yd = load_diabetes(return_X_y=True, as_frame=True)
Xd_tr, Xd_te, yd_tr, yd_te = train_test_split(Xd, yd, test_size=0.2, random_state=SEED)
reg = RandomForestRegressor = None
from sklearn.ensemble import RandomForestRegressor
reg = RandomForestRegressor(n_estimators=200, random_state=SEED).fit(Xd_tr, yd_tr)
p = reg.predict(Xd_te)
print("RMSE:", round(mean_squared_error(yd_te, p) ** 0.5, 2), "| MAE:", round(mean_absolute_error(yd_te, p), 2), "| R2:", round(r2_score(yd_te, p), 3))
7. Studi kasus: simpan dan pakai kembali model
import joblib
akhir = grid.best_estimator_.fit(X_train, y_train)
joblib.dump(akhir, BASE / "model" / "rf_kanker.joblib")
# ---- nanti / di notebook lain ----
muat = joblib.load(BASE / "model" / "rf_kanker.joblib")
contoh = X_test.iloc[[0]]
print("Prediksi:", "jinak" if muat.predict(contoh)[0] == 1 else "ganas",
"| peluang jinak:", round(muat.predict_proba(contoh)[0, 1], 3))
tabel = pd.DataFrame(hasil, index=["F1 rata-rata", "F1 std"]).T.round(3)
tabel.to_excel(BASE / "hasil" / "perbandingan_model.xlsx")
print(tabel)
Latihan
Tambahkan model SVC(probability=True) dengan StandardScaler ke perbandingan, hitung F1 cross-validation.
Lihat kunci jawaban
from sklearn.svm import SVC
m = make_pipeline(StandardScaler(), SVC(probability=True, random_state=SEED))
print(round(cross_val_score(m, X_train, y_train, cv=cv, scoring="f1").mean(), 3))
Hitung recall kelas ganas (label 0) dari model terbaik pada data uji.
Lihat kunci jawaban
from sklearn.metrics import recall_score
print(round(recall_score(y_test, pred, pos_label=0), 3))
Kesalahan yang sering terjadi
- Menyetel hyperparameter memakai data uji (harus memakai validasi atau cross-validation).
- Melaporkan hanya akurasi.
- Tidak memakai baseline pembanding.
- Menganggap feature importance sebagai sebab-akibat.
Kuis singkat
1. Tujuan cross-validation?
2. Pada skrining penyakit, kesalahan yang biasanya paling berbahaya?
3. SHAP dipakai untuk...
Ceklis: saya sudah bisa...
Bab 7 dari 12
Clustering dan Association Rule
Tujuan belajar
- Melakukan K-Means dan memilih jumlah klaster
- Memvisualisasikan klaster dengan PCA
- Mengenal DBSCAN untuk bentuk klaster tidak beraturan
- Menemukan aturan asosiasi dengan FP-Growth
Studi kasus: Segmentasi pelanggan RFM dan market basket analysis
1. Clustering (tanpa label)
Clustering mengelompokkan data yang mirip tanpa kolom target. Karena tidak ada kunci jawaban, kita menilai dengan metrik internal (silhouette, Davies-Bouldin) dan interpretasi bisnis.
Kita mulai dengan transaksi ritel sintetis untuk analisis RFM: Recency (kapan terakhir beli), Frequency (berapa sering), Monetary (berapa nilai belanja).
import numpy as np, pandas as pd
import matplotlib.pyplot as plt
rng = np.random.default_rng(SEED)
tipe = rng.choice(["setia", "biasa", "baru", "hampir_hilang"], 600, p=[.15, .4, .25, .2])
param = {"setia": (12, 15, 400_000), "biasa": (5, 40, 150_000), "baru": (2, 10, 100_000), "hampir_hilang": (1, 150, 120_000)}
baris = []
for i, t in enumerate(tipe):
n_trx, hari_terakhir, rata = param[t]
n = max(1, rng.poisson(n_trx))
for _ in range(n):
baris.append({"pelanggan": f"P{i:04d}",
"hari_lalu": int(np.clip(rng.exponential(hari_terakhir) + rng.integers(0, 20), 0, 365)),
"nilai": float(rng.gamma(2, rata / 2))})
trx = pd.DataFrame(baris)
print(trx.shape, trx["pelanggan"].nunique(), "pelanggan")
rfm = trx.groupby("pelanggan").agg(
recency=("hari_lalu", "min"),
frequency=("hari_lalu", "count"),
monetary=("nilai", "sum")).round(0)
print(rfm.describe().round(0))
2. Skala dan memilih jumlah klaster
K-Means sensitif terhadap skala dan data miring. Kita ambil logaritma lalu standardisasi. Jumlah klaster dipilih dengan silhouette dan elbow, lalu dicek maknanya.
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score, davies_bouldin_score
X = StandardScaler().fit_transform(np.log1p(rfm))
for k in range(2, 8):
km = KMeans(n_clusters=k, n_init=10, random_state=SEED).fit(X)
print(f"k={k} silhouette={silhouette_score(X, km.labels_):.3f} DBI={davies_bouldin_score(X, km.labels_):.3f} inertia={km.inertia_:.0f}")
3. Membentuk dan menafsirkan segmen
Pada keluaran di atas, silhouette tertinggi ada pada k=2, tetapi dua segmen terlalu kasar untuk strategi pemasaran. Kita memilih k=4 karena nilainya masih wajar dan segmennya dapat dijelaskan. Inilah alasan jumlah klaster tidak boleh ditentukan oleh satu angka saja.
km = KMeans(n_clusters=4, n_init=10, random_state=SEED).fit(X)
rfm["segmen"] = km.labels_
profil = rfm.groupby("segmen").agg(jumlah=("recency", "size"), recency=("recency", "mean"),
frequency=("frequency", "mean"), monetary=("monetary", "mean")).round(0)
print(profil)
Beri nama segmen berdasarkan profil (misal: segmen dengan recency kecil, frequency dan monetary besar = "Pelanggan setia"). Nama ini adalah interpretasi Anda, bukan keluaran algoritma.
from sklearn.decomposition import PCA
p = PCA(n_components=2, random_state=SEED).fit(X)
Z = p.transform(X)
print("Varians dijelaskan:", p.explained_variance_ratio_.round(3))
plt.figure(figsize=(6, 4.2))
sc = plt.scatter(Z[:, 0], Z[:, 1], c=km.labels_, cmap="viridis", s=14)
plt.title("Segmen pelanggan (proyeksi PCA)"); plt.xlabel("PC1"); plt.ylabel("PC2")
plt.legend(*sc.legend_elements(), title="Segmen"); plt.tight_layout()
plt.savefig(BASE / "gambar" / "segmen_rfm.png", dpi=150)
plt.show()
4. DBSCAN untuk bentuk tidak beraturan
from sklearn.datasets import make_moons
from sklearn.cluster import DBSCAN
Xm, _ = make_moons(400, noise=0.07, random_state=SEED)
lab_km = KMeans(2, n_init=10, random_state=SEED).fit_predict(Xm)
lab_db = DBSCAN(eps=0.2, min_samples=5).fit_predict(Xm)
print("Klaster K-Means:", len(set(lab_km)), "| klaster DBSCAN:", len(set(lab_db) - {-1}), "| noise:", int((lab_db == -1).sum()))
5. Association rule (FP-Growth)
Aturan asosiasi menjawab "siapa yang membeli A cenderung membeli B". Tiga ukuran utama: support (seberapa sering muncul), confidence (peluang B bila A), lift (seberapa kuat dibanding kebetulan; lift > 1 berarti berhubungan).
rng = np.random.default_rng(SEED)
keranjang = []
for _ in range(800):
isi = set(map(str, rng.choice(["Susu", "Roti", "Telur", "Kopi", "Gula", "Teh", "Mie", "Sabun"], size=rng.integers(1, 4), replace=False)))
if "Roti" in isi and rng.random() < .7: isi.add("Selai")
if "Kopi" in isi and rng.random() < .6: isi.add("Gula")
if "Mie" in isi and rng.random() < .5: isi.add("Telur")
keranjang.append(sorted(isi))
print(keranjang[:4])
from mlxtend.preprocessing import TransactionEncoder
from mlxtend.frequent_patterns import fpgrowth, association_rules
te = TransactionEncoder()
tabel = pd.DataFrame(te.fit(keranjang).transform(keranjang), columns=te.columns_)
sering = fpgrowth(tabel, min_support=0.05, use_colnames=True)
try:
aturan = association_rules(sering, metric="lift", min_threshold=1.2, num_itemsets=len(tabel))
except TypeError: # versi mlxtend lama
aturan = association_rules(sering, metric="lift", min_threshold=1.2)
aturan["aturan"] = aturan["antecedents"].apply(lambda s: ", ".join(sorted(s))) + " -> " + aturan["consequents"].apply(lambda s: ", ".join(sorted(s)))
print(aturan.sort_values("lift", ascending=False)[["aturan", "support", "confidence", "lift"]].head(6).round(3).to_string(index=False))
6. Studi kasus: laporan segmen dan aturan
profil.to_excel(BASE / "hasil" / "profil_segmen_rfm.xlsx")
aturan[["aturan", "support", "confidence", "lift"]].round(3).to_csv(BASE / "hasil" / "aturan_asosiasi.csv", index=False)
print("Tersimpan:", sorted(p.name for p in (BASE / "hasil").iterdir()))
Latihan
Jalankan K-Means dengan k=3 pada data RFM dan bandingkan silhouette dengan k=4.
Lihat kunci jawaban
for k in (3, 4):
lab = KMeans(k, n_init=10, random_state=SEED).fit_predict(X)
print(k, round(silhouette_score(X, lab), 3))
Ubah min_support menjadi 0.1. Berapa itemset yang ditemukan dibanding 0.05?
Lihat kunci jawaban
print(len(fpgrowth(tabel, min_support=0.05)), len(fpgrowth(tabel, min_support=0.1)))
Kesalahan yang sering terjadi
- Tidak melakukan scaling sebelum K-Means.
- Memilih k hanya dari elbow tanpa memeriksa makna bisnis.
- Menganggap lift tinggi pada support sangat kecil sebagai temuan kuat.
- Menafsirkan sumbu PCA sebagai variabel asli.
Kuis singkat
1. Lift > 1 pada aturan A → B berarti...
2. Silhouette mendekati 1 berarti...
3. Algoritma yang mampu menandai noise?
Ceklis: saya sudah bisa...
Bab 8 dari 12
Time Series dan Peramalan
Tujuan belajar
- Mengelola indeks tanggal dan resampling
- Mengenali tren dan musiman
- Membuat baseline dan mengevaluasi dengan validasi berurutan
- Meramal dengan Holt-Winters, SARIMAX, dan model machine learning
Studi kasus: Peramalan harga cabai (data sintetis)
1. Data berurutan waktu itu khusus
Pada time series, urutan penting. Dua aturan besar: (1) jangan mengacak data saat membagi latih/uji; data uji harus setelah data latih, (2) bandingkan selalu dengan baseline naif.
Kita membuat harga cabai harian sintetis dengan tren naik, pola musiman tahunan, dan gangguan acak.
import numpy as np, pandas as pd
import matplotlib.pyplot as plt
rng = np.random.default_rng(SEED)
tgl = pd.date_range("2022-01-01", "2024-12-31", freq="D")
t = np.arange(len(tgl))
harga = (40000 + 8 * t + 9000 * np.sin(2 * np.pi * t / 365.25 - 1)
+ 2500 * (tgl.dayofweek == 4) + rng.normal(0, 2500, len(tgl)))
seri = pd.Series(harga.round(-2), index=tgl, name="harga_cabai")
seri.iloc[rng.choice(len(seri), 12, replace=False)] = np.nan # hari tanpa data
print(seri.head(3)); print("kosong:", int(seri.isna().sum()))
2. Membersihkan dan meresampling
seri = seri.interpolate(method="time") # isi hari kosong dengan interpolasi waktu
mingguan = seri.resample("W").mean()[:"2024-12-31"] # buang minggu terakhir yang tidak lengkap
bulanan = seri.resample("MS").mean()
print(mingguan.shape, bulanan.shape)
fig, ax = plt.subplots(figsize=(10, 3.6))
seri.plot(ax=ax, alpha=.35, label="harian"); mingguan.plot(ax=ax, label="mingguan")
seri.rolling(30).mean().plot(ax=ax, label="rata-rata 30 hari", color="#c98a0a")
ax.set_title("Harga cabai (Rp/kg)"); ax.legend(); plt.tight_layout()
plt.savefig(BASE / "gambar" / "harga_cabai.png", dpi=150); plt.show()
3. Tren dan musiman
from statsmodels.tsa.seasonal import seasonal_decompose
dekomposisi = seasonal_decompose(bulanan, model="additive", period=12)
print("Kekuatan musiman (std):", round(dekomposisi.seasonal.std(), 0))
print("Tren awal -> akhir:", round(dekomposisi.trend.dropna().iloc[0]), "->", round(dekomposisi.trend.dropna().iloc[-1]))
4. Baseline dan metrik
Kita bagi data mingguan: 2022 sampai 2023 untuk latih, 2024 untuk uji. Baseline: (a) naif (minggu ini = minggu lalu), (b) musiman naif (minggu ini = minggu yang sama tahun lalu).
from sklearn.metrics import mean_absolute_error, mean_absolute_percentage_error
latih = mingguan[:"2023-12-31"]
uji = mingguan["2024-01-01":]
print(len(latih), "minggu latih |", len(uji), "minggu uji")
def skor(nama, aktual, prediksi):
mae = mean_absolute_error(aktual, prediksi)
mape = mean_absolute_percentage_error(aktual, prediksi) * 100
print(f"{nama:<28} MAE = {mae:>8.0f} MAPE = {mape:.2f}%")
return mae
hasil = {}
hasil["Naif"] = skor("Naif (nilai terakhir)", uji, np.repeat(latih.iloc[-1], len(uji)))
musiman = latih.iloc[-52:].values[:len(uji)]
hasil["Musiman naif"] = skor("Musiman naif (tahun lalu)", uji, musiman)
5. Holt-Winters dan SARIMAX
from statsmodels.tsa.holtwinters import ExponentialSmoothing
hw = ExponentialSmoothing(latih, trend="add", seasonal="add", seasonal_periods=52).fit()
pred_hw = hw.forecast(len(uji))
hasil["Holt-Winters"] = skor("Holt-Winters", uji, pred_hw)
from statsmodels.tsa.statespace.sarimax import SARIMAX
# dipakai data bulanan agar musiman 12 lebih ringan
lat_b, uji_b = bulanan[:"2023-12-01"], bulanan["2024-01-01":]
sar = SARIMAX(lat_b, order=(1, 1, 1), seasonal_order=(1, 1, 0, 12)).fit(disp=False)
pred_sar = sar.forecast(len(uji_b))
mape = mean_absolute_percentage_error(uji_b, pred_sar) * 100
print(f"SARIMAX (bulanan) MAPE = {mape:.2f}%")
6. Machine learning untuk time series
Ubah masalah menjadi tabel: fitur = nilai lag (masa lalu) dan kalender, target = nilai sekarang. Validasi memakai TimeSeriesSplit, bukan pengacakan.
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.model_selection import TimeSeriesSplit
d = pd.DataFrame({"y": mingguan})
for lag in (1, 2, 4, 52):
d[f"lag_{lag}"] = d["y"].shift(lag)
d["minggu_ke"] = d.index.isocalendar().week.astype(int).values
d = d.dropna()
fitur = [c for c in d.columns if c != "y"]
tr, te = d[:"2023-12-31"], d["2024-01-01":]
tscv = TimeSeriesSplit(n_splits=4)
for i, (a, b) in enumerate(tscv.split(tr)):
m = GradientBoostingRegressor(random_state=SEED).fit(tr.iloc[a][fitur], tr.iloc[a]["y"])
e = mean_absolute_percentage_error(tr.iloc[b]["y"], m.predict(tr.iloc[b][fitur])) * 100
print(f"lipatan {i+1}: MAPE {e:.2f}%")
gb = GradientBoostingRegressor(random_state=SEED).fit(tr[fitur], tr["y"])
hasil["Gradient Boosting (lag)"] = skor("Gradient Boosting + lag", te["y"], gb.predict(te[fitur]))
Catatan: pada pengujian ini fitur lag_1 memakai nilai aktual minggu lalu (prediksi satu langkah ke depan). Untuk peramalan banyak minggu ke depan, prediksi harus dimasukkan kembali sebagai lag (recursive) atau dipakai model per-horizon.
Untuk pustaka Prophet (opsional): %pip install prophet, kolom harus bernama ds dan y, lalu Prophet().fit(df).predict(...).
7. Studi kasus: tabel perbandingan untuk skripsi
tabel = pd.Series(hasil, name="MAE (Rp)").round(0).sort_values().to_frame()
print(tabel)
tabel.to_excel(BASE / "hasil" / "perbandingan_peramalan.xlsx")
plt.figure(figsize=(9, 3.4))
uji.plot(label="aktual", color="black")
pd.Series(pred_hw.values, index=uji.index).plot(label="Holt-Winters")
pd.Series(gb.predict(te[fitur]), index=te.index).plot(label="Gradient Boosting")
plt.title("Aktual vs prediksi 2024"); plt.legend(); plt.tight_layout()
plt.savefig(BASE / "gambar" / "prediksi_2024.png", dpi=150); plt.show()
Latihan
Hitung rata-rata bergerak 4 minggu dari mingguan dan tampilkan 3 nilai terakhir.
Lihat kunci jawaban
print(mingguan.rolling(4).mean().tail(3).round(0))
Cari bulan dengan harga rata-rata tertinggi dari seri.
Lihat kunci jawaban
print(seri.groupby(seri.index.month).mean().idxmax())
Kesalahan yang sering terjadi
- Mengacak data (train_test_split biasa) pada time series.
- Tidak membandingkan dengan baseline naif.
- Memakai informasi masa depan sebagai fitur (misal rata-rata seluruh periode).
- Melaporkan akurasi satu langkah sebagai kemampuan meramal jangka panjang.
Kuis singkat
1. Cara membagi data time series yang benar?
2. Baseline naif berarti...
3. MAPE 5% berarti...
Ceklis: saya sudah bisa...
Bab 9 dari 12
Text Mining Bahasa Indonesia
Tujuan belajar
- Membersihkan dan menormalkan teks bahasa Indonesia (termasuk bahasa gaul)
- Mengubah teks menjadi angka dengan TF-IDF
- Membuat dan mengevaluasi klasifikasi sentimen
- Mengenal IndoBERT dan alur fine-tuning dengan Hugging Face
Studi kasus: Analisis sentimen ulasan aplikasi
1. Alur text mining
teks mentah → pembersihan → normalisasi → representasi angka (TF-IDF/embedding) → model → evaluasi
Kita membuat ulasan aplikasi sintetis (dengan bahasa gaul, singkatan, tautan, dan emoji) agar bisa berlatih tanpa mengunduh data. Untuk skripsi, ganti dengan ulasan yang Anda kumpulkan (misalnya dengan google-play-scraper) atau dataset seperti IndoNLU SmSA. Periksa aturan situs dan lisensi sebelum mengambil data.
import numpy as np, pandas as pd, re
def buat_ulasan(n=700, seed=42):
rng = np.random.default_rng(seed)
pos = ["aplikasinya bagus banget", "mantap fiturnya lengkap", "sangat membantu dan mudah dipakai",
"tampilan keren dan cepat", "recommended bgt prosesnya cepet", "top deh adminnya ramah",
"puas sekali dengan layanannya", "gampang dipake dan ga ribet"]
neg = ["aplikasinya lemot bgt", "sering error dan force close", "susah login kecewa",
"update terbaru malah jelek", "ga bisa dibuka parah", "banyak iklan dan lambat",
"pelayanan buruk tidak recommended", "tdk bisa transfer, kecewa berat"]
tambah = ["menurut saya", "sejak update kemarin", "btw", "jujur aja", "di hp saya", "tolong diperbaiki yg ini", "semoga makin baik"]
noise = ["", "", " 😊", " 😡", " http://t.co/abc123", " @cs_aplikasi", " #review"]
teks, label = [], []
for _ in range(n):
y = rng.random() < 0.5
kal = rng.choice(pos if y else neg)
if rng.random() < .6: kal = rng.choice(tambah) + " " + kal
if rng.random() < .3: kal = kal + " " + rng.choice(tambah)
teks.append(str(kal).upper() if rng.random() < .1 else str(kal) + str(rng.choice(noise)))
label.append("positif" if y else "negatif")
df = pd.DataFrame({"ulasan": teks, "sentimen": label})
balik = rng.random(n) < 0.06 # 6% label sengaja keliru (noise anotasi)
df.loc[balik, "sentimen"] = df.loc[balik, "sentimen"].map({"positif": "negatif", "negatif": "positif"})
return df
df = buat_ulasan(700, SEED)
df.to_csv(BASE / "data" / "ulasan_sintetis.csv", index=False)
print(df.sample(5, random_state=1).to_string())
print(df["sentimen"].value_counts())
2. Pembersihan dan normalisasi
Urutan yang umum: huruf kecil → hapus URL, mention, hashtag, emoji, tanda baca → normalisasi kata gaul → hapus stopword.
Peringatan penting: jangan menghapus kata negasi seperti tidak, bukan, jangan, belum. Kata itu membalik makna kalimat.
SLANG = {"bgt": "banget", "banget": "sekali", "ga": "tidak", "gak": "tidak", "nggak": "tidak", "tdk": "tidak",
"yg": "yang", "cepet": "cepat", "dipake": "dipakai", "hp": "ponsel", "btw": "ngomong-ngomong"}
STOPWORD = {"yang", "dan", "di", "ke", "dari", "ini", "itu", "untuk", "dengan", "pada", "saya", "deh",
"sih", "aja", "kok", "nya", "ngomong-ngomong"} # "tidak" SENGAJA tidak dimasukkan
def bersihkan(teks):
t = teks.lower()
t = re.sub(r"https?://\S+|www\.\S+", " ", t) # URL
t = re.sub(r"@\w+|#\w+", " ", t) # mention dan hashtag
t = re.sub(r"[^a-z\s]", " ", t) # angka, emoji, tanda baca
kata = [SLANG.get(k, k) for k in t.split()]
kata = [k for k in kata if k not in STOPWORD and len(k) > 1]
return " ".join(kata)
contoh = "Aplikasinya BAGUS bgt!! 😊 cepet, ga ribet http://t.co/abc @cs #review"
print(bersihkan(contoh))
df["bersih"] = df["ulasan"].apply(bersihkan)
print(df[["ulasan", "bersih"]].head(4).to_string())
3. TF-IDF: mengubah teks menjadi angka
TF-IDF memberi bobot tinggi pada kata yang sering muncul di satu dokumen tetapi jarang di dokumen lain. ngram_range=(1, 2) menyertakan pasangan kata seperti "tidak bisa".
from sklearn.feature_extraction.text import TfidfVectorizer
tfidf = TfidfVectorizer(ngram_range=(1, 2), min_df=2)
M = tfidf.fit_transform(df["bersih"])
print(M.shape, "| kosong:", round(1 - M.nnz / (M.shape[0] * M.shape[1]), 3))
print(list(tfidf.get_feature_names_out()[:10]))
4. Melatih dan membandingkan model
from sklearn.model_selection import train_test_split, cross_val_score, StratifiedKFold
from sklearn.pipeline import make_pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.svm import LinearSVC
from sklearn.naive_bayes import MultinomialNB
from sklearn.metrics import classification_report, ConfusionMatrixDisplay
X_train, X_test, y_train, y_test = train_test_split(df["bersih"], df["sentimen"], test_size=0.2,
stratify=df["sentimen"], random_state=SEED)
kandidat = {
"Logistic Regression": make_pipeline(TfidfVectorizer(ngram_range=(1, 2), min_df=2), LogisticRegression(max_iter=1000)),
"Linear SVM": make_pipeline(TfidfVectorizer(ngram_range=(1, 2), min_df=2), LinearSVC()),
"Naive Bayes": make_pipeline(TfidfVectorizer(ngram_range=(1, 2), min_df=2), MultinomialNB()),
}
cv = StratifiedKFold(5, shuffle=True, random_state=SEED)
for nama, m in kandidat.items():
s = cross_val_score(m, X_train, y_train, cv=cv, scoring="f1_macro")
print(f"{nama:<20} F1-macro = {s.mean():.3f} (+/- {s.std():.3f})")
model = kandidat["Logistic Regression"].fit(X_train, y_train)
pred = model.predict(X_test)
print(classification_report(y_test, pred))
# kata yang paling menentukan
vec, lr = model.named_steps["tfidfvectorizer"], model.named_steps["logisticregression"]
fitur = np.array(vec.get_feature_names_out())
urut = np.argsort(lr.coef_[0])
print("Cenderung NEGATIF:", list(fitur[urut[:6]]))
print("Cenderung POSITIF:", list(fitur[urut[-6:]]))
5. Memakai model pada teks baru
baru = ["Aplikasinya bagus bgt, cepet!", "Sering error, kecewa banget", "tidak bagus sama sekali"]
proba = model.predict_proba([bersihkan(t) for t in baru])
for t, p, c in zip(baru, model.predict([bersihkan(t) for t in baru]), proba):
print(f"{t!r:<38} -> {p} (yakin {c.max():.2f})")
import joblib
joblib.dump(model, BASE / "model" / "sentimen_tfidf_lr.joblib")
Perhatikan kalimat ketiga: "tidak bagus sama sekali" diprediksi positif. Penyebabnya, data latih sintetis kita tidak pernah memuat pola "tidak bagus", jadi model hanya melihat kata "bagus". Ini pelajaran penting: model hanya sebaik data latihnya. Solusinya menambah contoh negasi ke data latih, lalu melatih ulang. Pada data nyata, selalu periksa contoh-contoh salah prediksi (analisis error).
Catatan: tiga model pada langkah 4 menghasilkan skor hampir identik karena data sintetis ini sederhana dan berpola template. Pada data nyata, perbedaannya biasanya lebih terlihat.
6. Kata dasar (stemming) dan IndoBERT
Untuk bahasa Indonesia, stemming (mengembalikan ke kata dasar) memakai pustaka Sastrawi. Pada model transformer seperti IndoBERT, stemming dan penghapusan stopword umumnya tidak diperlukan karena model mempelajari konteks kalimat utuh.
Sel di bawah memerlukan internet dan model dari Hugging Face, sehingga hanya berjalan di Colab. Nama model dapat berubah; jika gagal, cari model sentimen bahasa Indonesia di huggingface.co/models.
%pip install -q PySastrawi transformers
from Sastrawi.Stemmer.StemmerFactory import StemmerFactory
stemmer = StemmerFactory().create_stemmer()
print(stemmer.stem("pembayarannya sering terlambat dan membingungkan"))
# Inferensi dengan model transformer yang sudah dilatih (tanpa latihan sendiri)
from transformers import pipeline
klasifikasi = pipeline("text-classification", model="w11wo/indonesian-roberta-base-sentiment-classifier")
print(klasifikasi(["aplikasinya bagus banget", "sering error dan bikin kecewa"]))
7. Fine-tuning IndoBERT (kerangka, perlu GPU)
Aktifkan Runtime → T4 GPU. Kerangka ini memakai ulasan_sintetis.csv; ganti dengan data Anda sendiri.
%pip install -q transformers datasets evaluate accelerate
import numpy as np, pandas as pd, evaluate
from datasets import Dataset
from sklearn.model_selection import train_test_split
from transformers import (AutoTokenizer, AutoModelForSequenceClassification,
TrainingArguments, Trainer, DataCollatorWithPadding)
NAMA_MODEL = "indobenchmark/indobert-base-p1"
data = pd.read_csv(BASE / "data" / "ulasan_sintetis.csv")
data["label"] = (data["sentimen"] == "positif").astype(int)
tr, te = train_test_split(data[["ulasan", "label"]], test_size=0.2, stratify=data["label"], random_state=42)
tok = AutoTokenizer.from_pretrained(NAMA_MODEL)
enkode = lambda b: tok(b["ulasan"], truncation=True, max_length=64)
ds_tr = Dataset.from_pandas(tr.reset_index(drop=True)).map(enkode, batched=True)
ds_te = Dataset.from_pandas(te.reset_index(drop=True)).map(enkode, batched=True)
model = AutoModelForSequenceClassification.from_pretrained(NAMA_MODEL, num_labels=2)
f1 = evaluate.load("f1")
hitung = lambda p: f1.compute(predictions=np.argmax(p[0], axis=1), references=p[1], average="macro")
args = TrainingArguments(output_dir=str(BASE / "model" / "indobert_sentimen"), num_train_epochs=3,
per_device_train_batch_size=16, learning_rate=3e-5,
eval_strategy="epoch", save_strategy="no", report_to="none", seed=42)
# Jika muncul error tentang 'eval_strategy', ganti dengan 'evaluation_strategy' (versi lama).
trainer = Trainer(model=model, args=args, train_dataset=ds_tr, eval_dataset=ds_te,
data_collator=DataCollatorWithPadding(tok), compute_metrics=hitung)
trainer.train()
print(trainer.evaluate())
Latihan
Tambahkan "tidak" ke STOPWORD, bersihkan ulang, dan latih model. Apakah F1-macro berubah? (Pada data sintetis ini perubahannya kecil karena hanya sedikit ulasan yang memuat negasi; jelaskan mengapa pada data nyata dampaknya bisa jauh lebih besar.)
Lihat kunci jawaban
STOP_ASLI = set(STOPWORD)
STOPWORD.add("tidak")
X2 = df["ulasan"].apply(bersihkan)
Xa, Xb, ya, yb = train_test_split(X2, df["sentimen"], test_size=0.2, stratify=df["sentimen"], random_state=SEED)
m = make_pipeline(TfidfVectorizer(ngram_range=(1, 2), min_df=2), LogisticRegression(max_iter=1000)).fit(Xa, ya)
from sklearn.metrics import f1_score
print(round(f1_score(yb, m.predict(Xb), average="macro"), 3))
STOPWORD.clear(); STOPWORD.update(STOP_ASLI)
Hitung 5 kata paling sering muncul pada ulasan negatif setelah pembersihan (pakai collections.Counter).
Lihat kunci jawaban
from collections import Counter
c = Counter(" ".join(df.loc[df["sentimen"] == "negatif", "bersih"]).split())
print(c.most_common(5))
Kesalahan yang sering terjadi
- Menghapus kata negasi sebagai stopword.
- Membersihkan teks dengan aturan yang berbeda antara data latih dan data baru.
- Melatih TF-IDF pada seluruh data sebelum split (kebocoran).
- Mengumpulkan data media sosial tanpa memeriksa ketentuan layanan dan privasi.
Kuis singkat
1. Mengapa kata "tidak" berbahaya jika dihapus?
2. TF-IDF memberi bobot tinggi pada kata yang...
3. Pada model IndoBERT, stopword removal...
Ceklis: saya sudah bisa...
Bab 10 dari 12
Deep Learning dan Computer Vision Dasar
Tujuan belajar
- Memahami neuron, lapisan, epoch, loss, dan overfitting
- Melatih jaringan saraf sederhana
- Memahami transfer learning untuk klasifikasi citra
- Menyiapkan dataset citra di Google Drive dan melatih model di GPU Colab
Studi kasus: Klasifikasi citra dengan transfer learning
1. Konsep inti
- Neuron menghitung jumlah berbobot lalu menerapkan fungsi aktivasi.
- Lapisan (layer) adalah kumpulan neuron; jaringan dalam punya banyak lapisan.
- Loss mengukur seberapa salah prediksi; latihan berusaha memperkecilnya dengan gradient descent.
- Epoch adalah satu putaran melihat seluruh data latih; batch adalah potongan data per langkah.
- Overfitting terjadi bila model hafal data latih tetapi buruk pada data baru. Tandanya: loss latih terus turun, loss validasi mulai naik.
Kita mulai dengan jaringan saraf sederhana di scikit-learn (cukup CPU) pada 1.797 citra angka tulisan tangan 8x8 piksel.
import numpy as np, matplotlib.pyplot as plt
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
from sklearn.neural_network import MLPClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import make_pipeline
from sklearn.metrics import accuracy_score
digits = load_digits()
X, y = digits.data, digits.target
print(X.shape, "-> tiap citra 8x8 = 64 piksel")
fig, ax = plt.subplots(1, 6, figsize=(9, 1.8))
for a, img, lab in zip(ax, digits.images[:6], y[:6]):
a.imshow(img, cmap="gray_r"); a.set_title(lab); a.axis("off")
plt.show()
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, stratify=y, random_state=SEED)
mlp = make_pipeline(StandardScaler(),
MLPClassifier(hidden_layer_sizes=(64, 32), max_iter=300, early_stopping=True, random_state=SEED))
mlp.fit(X_train, y_train)
print("Akurasi latih:", round(accuracy_score(y_train, mlp.predict(X_train)), 3))
print("Akurasi uji :", round(accuracy_score(y_test, mlp.predict(X_test)), 3))
2. Dari piksel ke citra nyata: CNN dan transfer learning
Citra nyata (224x224x3) terlalu besar untuk jaringan biasa. CNN (Convolutional Neural Network) membaca pola lokal seperti tepi dan tekstur. Melatih CNN dari nol butuh jutaan citra, jadi kita memakai transfer learning: memulai dari model (misal MobileNet, EfficientNet) yang sudah dilatih pada ImageNet, mengganti lapisan terakhir, lalu melatih sedikit pada data kita.
Pola kerja standar:
- Siapkan folder
train/danval/, tiap kelas satu subfolder. - Augmentasi (putar, balik, ubah cahaya) agar model tahan variasi.
- Bekukan model dasar, latih lapisan baru, evaluasi, lalu (opsional) buka sebagian lapisan.
3. Membuat dataset citra latihan di Drive
Agar Anda bisa mencoba alur lengkap tanpa mengunduh data, sel ini membuat citra sintetis sederhana (lingkaran vs persegi). Untuk skripsi, ganti folder ini dengan dataset asli (misalnya PlantVillage dari Kaggle) dengan struktur yang sama.
from PIL import Image, ImageDraw
import numpy as np
def buat_dataset_bentuk(akar, per_kelas=60, ukuran=96, seed=42):
rng = np.random.default_rng(seed)
for split, n in [("train", per_kelas), ("val", per_kelas // 3)]:
for kelas in ["lingkaran", "persegi"]:
folder = akar / split / kelas
folder.mkdir(parents=True, exist_ok=True)
for i in range(n):
warna = tuple(int(v) for v in rng.integers(50, 255, 3))
lat = tuple(int(v) for v in rng.integers(0, 60, 3))
img = Image.new("RGB", (ukuran, ukuran), lat)
d = ImageDraw.Draw(img)
r = int(rng.integers(14, 34)); cx, cy = (int(v) for v in rng.integers(36, 60, 2))
kotak = [cx - r, cy - r, cx + r, cy + r]
d.ellipse(kotak, fill=warna) if kelas == "lingkaran" else d.rectangle(kotak, fill=warna)
img.save(folder / f"{kelas}_{i:03d}.png")
AKAR = BASE / "data" / "bentuk"
buat_dataset_bentuk(AKAR)
print({p.name: len(list(p.glob("*.png"))) for p in sorted((AKAR / "train").iterdir())})
4. Transfer learning dengan PyTorch (jalankan di Colab dengan GPU)
Sel berikut memakai PyTorch dan torchvision yang sudah terpasang di Colab. Aktifkan Runtime → T4 GPU untuk dataset besar; dataset latihan di atas cukup cepat bahkan di CPU.
import torch, torch.nn as nn
from torch.utils.data import DataLoader
from torchvision import datasets, transforms, models
device = "cuda" if torch.cuda.is_available() else "cpu"
print("Perangkat:", device)
torch.manual_seed(42)
norm = transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
tf_train = transforms.Compose([transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(),
transforms.RandomRotation(15), transforms.ToTensor(), norm])
tf_val = transforms.Compose([transforms.Resize((224, 224)), transforms.ToTensor(), norm])
ds_train = datasets.ImageFolder(AKAR / "train", tf_train)
ds_val = datasets.ImageFolder(AKAR / "val", tf_val)
dl_train = DataLoader(ds_train, batch_size=16, shuffle=True)
dl_val = DataLoader(ds_val, batch_size=32)
print("Kelas:", ds_train.classes)
model = models.mobilenet_v3_small(weights=models.MobileNet_V3_Small_Weights.DEFAULT)
for p in model.parameters():
p.requires_grad = False # bekukan model dasar
model.classifier[3] = nn.Linear(model.classifier[3].in_features, len(ds_train.classes))
model = model.to(device)
opt = torch.optim.Adam(model.classifier.parameters(), lr=1e-3)
loss_fn = nn.CrossEntropyLoss()
for epoch in range(5):
model.train()
for xb, yb in dl_train:
xb, yb = xb.to(device), yb.to(device)
opt.zero_grad(); loss = loss_fn(model(xb), yb); loss.backward(); opt.step()
model.eval(); benar = total = 0
with torch.no_grad():
for xb, yb in dl_val:
benar += (model(xb.to(device)).argmax(1).cpu() == yb).sum().item(); total += len(yb)
print(f"epoch {epoch+1}: loss latih terakhir {loss.item():.3f} | akurasi validasi {benar/total:.3f}")
torch.save(model.state_dict(), BASE / "model" / "mobilenet_bentuk.pt")
5. Untuk skripsi Anda
- Bagi data per objek/sumber (bukan per foto) bila ada foto berulang dari satu objek, agar tidak bocor.
- Laporkan akurasi, F1-macro per kelas, dan confusion matrix; sertakan contoh salah klasifikasi.
- Gunakan Grad-CAM untuk menunjukkan area citra yang dilihat model.
- Untuk deteksi objek (mis. APD), pakai pustaka
ultralytics(YOLO) dengan format anotasi YOLO. - Simpan checkpoint ke Drive setelah tiap beberapa epoch karena sesi Colab dapat terputus.
Latihan
Ubah hidden_layer_sizes MLP menjadi (128,) dan (64, 32, 16). Bandingkan akurasi uji.
Lihat kunci jawaban
for h in [(128,), (64, 32, 16)]:
m = make_pipeline(StandardScaler(), MLPClassifier(hidden_layer_sizes=h, max_iter=300, early_stopping=True, random_state=SEED)).fit(X_train, y_train)
print(h, round(accuracy_score(y_test, m.predict(X_test)), 3))
Tampilkan satu citra dari AKAR/train/lingkaran memakai matplotlib.
Lihat kunci jawaban
from PIL import Image
import matplotlib.pyplot as plt
file = sorted((AKAR / "train" / "lingkaran").glob("*.png"))[0]
plt.imshow(Image.open(file)); plt.axis("off"); plt.show()
Kesalahan yang sering terjadi
- Melatih CNN dari nol pada dataset kecil (pakai transfer learning).
- Lupa mengubah mode `model.eval()` saat evaluasi.
- Membagi foto dari satu objek ke latih dan uji sekaligus.
- Lupa menyalakan GPU sehingga latihan sangat lambat.
Kuis singkat
1. Tanda overfitting?
2. Keuntungan transfer learning?
3. Augmentasi bertujuan...
Ceklis: saya sudah bisa...
Bab 11 dari 12
Pengenalan LLM dan RAG
Tujuan belajar
- Memahami embedding dan pencarian semantik
- Memecah dokumen menjadi potongan (chunk)
- Membangun retrieval dan menyusun prompt untuk RAG
- Mengevaluasi retrieval dengan hit rate
Studi kasus: Tanya jawab dari dokumen pedoman akademik
1. Apa itu RAG?
Retrieval-Augmented Generation (RAG) membuat LLM menjawab berdasarkan dokumen Anda:
dokumen → potong (chunk) → indeks → [pertanyaan] → cari potongan relevan → prompt + potongan → LLM → jawaban + sumber
Bagian tersulit dan paling menentukan adalah retrieval (mencari potongan yang tepat). Karena itu kita membangunnya dulu dengan TF-IDF (dapat diuji offline), lalu menggantinya dengan embedding neural.
Dokumen di bawah adalah pedoman kampus fiktif untuk latihan.
pedoman = """
Pasal 1 Cuti Akademik. Mahasiswa dapat mengajukan cuti akademik paling lama dua semester berturut-turut. Pengajuan dilakukan paling lambat dua minggu sebelum perkuliahan dimulai melalui portal akademik dan disetujui ketua program studi.
Pasal 2 Batas Studi. Program sarjana ditempuh paling lama tujuh tahun. Mahasiswa yang melewati batas studi dinyatakan berhenti kuliah kecuali mendapat perpanjangan dari dekan.
Pasal 3 Skripsi. Mahasiswa dapat mengambil skripsi setelah menempuh minimal 110 SKS dengan IPK minimal 2,50. Skripsi dibimbing oleh dua dosen pembimbing dan diuji dalam sidang terbuka.
Pasal 4 Beasiswa. Beasiswa prestasi diberikan kepada mahasiswa dengan IPK minimal 3,50 dan tidak sedang menerima beasiswa lain. Pendaftaran dibuka setiap awal semester ganjil.
Pasal 5 Kehadiran. Mahasiswa wajib hadir minimal 75 persen dari total pertemuan agar dapat mengikuti ujian akhir semester. Ketidakhadiran karena sakit harus disertai surat dokter.
Pasal 6 Pindah Program Studi. Perpindahan program studi hanya dapat dilakukan setelah semester dua dan sebelum semester lima, dengan persetujuan kedua ketua program studi.
Pasal 7 Pembayaran. Keterlambatan pembayaran biaya kuliah lebih dari empat minggu menyebabkan status mahasiswa tidak aktif pada semester berjalan.
"""
print(len(pedoman.split()), "kata")
2. Chunking
Potong dokumen menjadi bagian kecil yang utuh maknanya. Terlalu besar: retrieval kabur. Terlalu kecil: konteks hilang. Praktik umum: 200 sampai 500 kata dengan sedikit tumpang tindih. Untuk dokumen bersusun pasal, memotong per pasal sering paling baik.
import re
def potong_per_pasal(teks):
bagian = [b.strip() for b in re.split(r"\n\s*\n", teks.strip()) if b.strip()]
return [{"id": i, "teks": b, "judul": b.split(".")[0]} for i, b in enumerate(bagian)]
def potong_jendela(teks, ukuran=60, tumpang=15):
kata = teks.split()
langkah = ukuran - tumpang
return [" ".join(kata[i:i + ukuran]) for i in range(0, max(1, len(kata) - tumpang), langkah)]
chunk = potong_per_pasal(pedoman)
print(len(chunk), "chunk;", [c["judul"] for c in chunk])
print(len(potong_jendela(pedoman)), "chunk dengan jendela geser")
3. Retrieval sederhana dengan TF-IDF
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
vec = TfidfVectorizer(ngram_range=(1, 2), sublinear_tf=True)
matriks = vec.fit_transform([c["teks"] for c in chunk])
def cari(pertanyaan, k=2):
skor = cosine_similarity(vec.transform([pertanyaan]), matriks)[0]
idx = np.argsort(skor)[::-1][:k]
return [(chunk[i]["judul"], round(float(skor[i]), 3), chunk[i]["teks"]) for i in idx]
for judul, skor, _ in cari("Berapa lama cuti akademik boleh diambil?"):
print(judul, skor)
4. Menyusun prompt RAG
Prompt yang baik: (1) memberi peran, (2) menyisipkan konteks bernomor, (3) memerintahkan hanya menjawab dari konteks, (4) meminta menyebut sumber, dan (5) menyuruh mengaku tidak tahu bila jawaban tidak ada.
def susun_prompt(pertanyaan, k=2):
hasil = cari(pertanyaan, k)
konteks = "\n\n".join(f"[{i+1}] {teks}" for i, (_, _, teks) in enumerate(hasil))
return (
"Kamu adalah asisten layanan akademik. Jawab dalam bahasa Indonesia hanya berdasarkan konteks berikut. "
"Sebutkan nomor sumber, misalnya [1]. Jika jawaban tidak ada dalam konteks, katakan bahwa informasinya tidak ditemukan.\n\n"
f"KONTEKS:\n{konteks}\n\nPERTANYAAN: {pertanyaan}\nJAWABAN:"
)
print(susun_prompt("Berapa IPK minimal untuk mengambil skripsi?"))
5. Mengevaluasi retrieval
Buat 10 sampai 30 pasangan pertanyaan dan pasal yang benar. Ukur hit rate@k: berapa persen pertanyaan yang pasal benarnya masuk k hasil teratas. Ini jauh lebih objektif daripada menilai jawaban dengan perasaan.
uji = [
("Berapa lama cuti akademik maksimal?", "Pasal 1 Cuti Akademik"),
("Kapan saya boleh pindah jurusan?", "Pasal 6 Pindah Program Studi"),
("Syarat mengambil skripsi apa saja?", "Pasal 3 Skripsi"),
("Berapa persen kehadiran agar bisa ikut UAS?", "Pasal 5 Kehadiran"),
("Apa akibatnya kalau telat bayar kuliah?", "Pasal 7 Pembayaran"),
("Siapa yang boleh dapat beasiswa prestasi?", "Pasal 4 Beasiswa"),
("Berapa tahun batas maksimal kuliah S1?", "Pasal 2 Batas Studi"),
]
for k in (1, 2):
hit = np.mean([any(j == benar for j, _, _ in cari(q, k)) for q, benar in uji])
print(f"hit rate@{k} = {hit:.2f}")
6. Meningkatkan retrieval dengan embedding (perlu internet, Colab)
TF-IDF hanya mencocokkan kata yang sama. Embedding mengubah teks menjadi vektor makna sehingga "berhenti kuliah" cocok dengan "putus studi". Model multibahasa seperti intfloat/multilingual-e5-small mendukung bahasa Indonesia. Model ini meminta awalan query: untuk pertanyaan dan passage: untuk dokumen.
%pip install -q sentence-transformers faiss-cpu
from sentence_transformers import SentenceTransformer
import faiss, numpy as np
enc = SentenceTransformer("intfloat/multilingual-e5-small")
vek = enc.encode(["passage: " + c["teks"] for c in chunk], normalize_embeddings=True)
indeks = faiss.IndexFlatIP(vek.shape[1]); indeks.add(np.asarray(vek, dtype="float32"))
def cari_semantik(pertanyaan, k=2):
q = enc.encode(["query: " + pertanyaan], normalize_embeddings=True)
skor, idx = indeks.search(np.asarray(q, dtype="float32"), k)
return [(chunk[i]["judul"], round(float(s), 3)) for s, i in zip(skor[0], idx[0])]
print(cari_semantik("Apa syarat berhenti sementara dari kampus?"))
7. Menghasilkan jawaban dengan LLM (Colab dengan GPU)
Anda dapat memakai LLM open-source kecil melalui transformers atau LLM melalui API. Contoh di bawah memakai model kecil yang muat di GPU T4; pastikan model dan versinya masih tersedia.
%pip install -q transformers accelerate
from transformers import pipeline
gen = pipeline("text-generation", model="Qwen/Qwen2.5-1.5B-Instruct", device_map="auto")
pertanyaan = "Berapa IPK minimal untuk mengambil skripsi?"
pesan = [{"role": "user", "content": susun_prompt(pertanyaan)}]
keluaran = gen(pesan, max_new_tokens=200, do_sample=False)
print(keluaran[0]["generated_text"][-1]["content"])
8. Catatan untuk riset
- Evaluasi dua lapis: kualitas retrieval (hit rate, MRR) dan kualitas jawaban (kesesuaian dengan sumber/faithfulness, dinilai manusia atau pustaka seperti Ragas).
- Bandingkan variasi: ukuran chunk, TF-IDF vs embedding, jumlah k, dengan atau tanpa reranking.
- Uji pertanyaan yang jawabannya tidak ada di dokumen; sistem yang baik harus menolak menjawab.
- Untuk PDF, ekstrak teks dengan
pypdfataupdfplumber, lalu periksa hasil ekstraksi (tabel dan kolom sering berantakan). - Jangan mengunggah dokumen rahasia atau data pribadi ke layanan LLM pihak ketiga tanpa izin.
Latihan
Ubah k pada retrieval menjadi 3 dan hitung hit rate@3.
Lihat kunci jawaban
print(np.mean([any(j == benar for j, _, _ in cari(q, 3)) for q, benar in uji]))
Tambahkan dua pertanyaan uji baru (parafrase yang tidak memakai kata persis dari pasal) dan lihat apakah TF-IDF masih berhasil.
Lihat kunci jawaban
baru = [("Boleh gak istirahat kuliah setahun?", "Pasal 1 Cuti Akademik"), ("Kalau saya sakit apa yang harus dibawa?", "Pasal 5 Kehadiran")]
for q, benar in baru:
print(q, "->", [j for j, _, _ in cari(q, 2)], "| benar:", benar)
Kesalahan yang sering terjadi
- Menilai sistem RAG hanya dari beberapa contoh yang bagus.
- Chunk terlalu besar atau memotong kalimat di tengah.
- Tidak menguji pertanyaan yang jawabannya tidak ada.
- Mengirim data pribadi ke layanan LLM eksternal.
Kuis singkat
1. Bagian RAG yang paling menentukan mutu jawaban?
2. Hit rate@k mengukur...
3. Keunggulan embedding dibanding TF-IDF?
Ceklis: saya sudah bisa...
Bab 12 dari 12
Praktik Riset: Reproduktibilitas hingga Demo
Tujuan belajar
- Membuat eksperimen yang dapat diulang (seed, versi, konfigurasi)
- Menyusun tabel hasil dan mencatat metrik
- Menulis metodologi berbasis CRISP-DM
- Membuat demo sederhana (Gradio) dan dokumentasi (model card)
Studi kasus: Merapikan satu notebook menjadi lampiran skripsi
1. Mengapa reproduktibilitas?
Penguji dapat bertanya, "Bisakah hasil ini diulang?" Jawabannya harus ya. Tiga syarat: data yang jelas versinya, kode yang berurutan dan bersih, serta lingkungan yang tercatat (versi Python dan pustaka, seed).
import os, random, json, hashlib, platform, sys, datetime
import numpy as np, pandas as pd
def tetapkan_seed(seed=42):
random.seed(seed); np.random.seed(seed)
os.environ["PYTHONHASHSEED"] = str(seed)
try: # bila PyTorch terpasang
import torch; torch.manual_seed(seed)
except ImportError:
pass
return seed
tetapkan_seed(SEED)
def catat_lingkungan(seed=SEED, file_data=None):
import sklearn
info = {"waktu": datetime.datetime.now().isoformat(timespec="seconds"),
"python": sys.version.split()[0], "os": platform.system(),
"numpy": np.__version__, "pandas": pd.__version__, "scikit-learn": sklearn.__version__, "seed": seed}
if file_data and os.path.exists(file_data):
info["data_md5"] = hashlib.md5(open(file_data, "rb").read()).hexdigest()
return info
print(catat_lingkungan())
2. Konfigurasi eksperimen di satu tempat
KONFIG = {
"judul": "Prediksi kanker payudara (latihan)",
"seed": SEED,
"test_size": 0.2,
"cv_lipatan": 5,
"metrik_utama": "f1",
"model": {"rf": {"n_estimators": 200}, "lr": {"max_iter": 2000}},
}
with open(BASE / "hasil" / "konfig.json", "w", encoding="utf-8") as f:
json.dump(KONFIG, f, indent=2, ensure_ascii=False)
print(json.dumps(KONFIG, indent=2, ensure_ascii=False)[:200], "...")
3. Menjalankan eksperimen dan mencatat hasil
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split, StratifiedKFold, cross_validate
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import f1_score, recall_score, precision_score
X, y = load_breast_cancer(return_X_y=True, as_frame=True)
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=KONFIG["test_size"], stratify=y, random_state=KONFIG["seed"])
model = {
"Logistic Regression": make_pipeline(StandardScaler(), LogisticRegression(**KONFIG["model"]["lr"])),
"Random Forest": RandomForestClassifier(random_state=SEED, **KONFIG["model"]["rf"]),
}
cv = StratifiedKFold(KONFIG["cv_lipatan"], shuffle=True, random_state=SEED)
baris = []
for nama, m in model.items():
hasil_cv = cross_validate(m, X_tr, y_tr, cv=cv, scoring=["f1", "recall", "precision"])
m.fit(X_tr, y_tr); p = m.predict(X_te)
baris.append({"Model": nama,
"F1 (CV)": f"{hasil_cv['test_f1'].mean():.3f} ± {hasil_cv['test_f1'].std():.3f}",
"Recall (CV)": f"{hasil_cv['test_recall'].mean():.3f}",
"F1 (uji)": round(f1_score(y_te, p), 3),
"Recall (uji)": round(recall_score(y_te, p), 3),
"Presisi (uji)": round(precision_score(y_te, p), 3)})
tabel = pd.DataFrame(baris).set_index("Model")
print(tabel.to_string())
4. Menyimpan tabel dan log untuk lampiran
Simpan tabel sebagai Excel/CSV lalu tempel ke naskah. Simpan juga catatan lingkungan bersama hasilnya.
tabel.to_excel(BASE / "hasil" / "tabel_hasil_utama.xlsx")
tabel.to_csv(BASE / "hasil" / "tabel_hasil_utama.csv")
log = catat_lingkungan(SEED)
log["hasil_uji"] = tabel["F1 (uji)"].to_dict()
with open(BASE / "hasil" / "log_eksperimen.json", "w", encoding="utf-8") as f:
json.dump(log, f, indent=2, ensure_ascii=False)
try:
print(tabel.to_latex()) # untuk naskah LaTeX (perlu jinja2)
except Exception as e:
print("to_latex tidak tersedia:", type(e).__name__)
5. Kerangka bab metodologi (CRISP-DM)
| Subbab | Isi yang harus ada |
|---|---|
| Pemahaman masalah | Latar belakang, rumusan masalah, tujuan, ukuran keberhasilan |
| Pemahaman data | Sumber, lisensi, jumlah data, variabel, distribusi kelas, hasil EDA |
| Persiapan data | Pembersihan, encoding, scaling, penanganan imbalance, urutan pembagian data |
| Pemodelan | Algoritma, alasan pemilihan, hyperparameter dan cara tuning, baseline |
| Evaluasi | Metrik dan alasan, cross-validation, uji signifikansi, analisis kesalahan, interpretasi (SHAP) |
| Implementasi | Demo, keterbatasan, saran penggunaan |
| Etika | Privasi, bias, keterbatasan generalisasi |
6. Demo interaktif dengan Gradio (Colab)
Gradio membuat antarmuka web dalam beberapa baris dan dapat dibagikan sementara lewat tautan publik (share=True). Sel ini memuat model dari Bab 6, jadi jalankan dulu Bab 6 sampai model rf_kanker.joblib tersimpan di Drive. Untuk demo permanen, pakai Streamlit Community Cloud atau Hugging Face Spaces dari repositori GitHub.
%pip install -q gradio
import gradio as gr, joblib
from sklearn.datasets import load_breast_cancer
model_demo = joblib.load(BASE / "model" / "rf_kanker.joblib") # dari Bab 6
nama_fitur = list(load_breast_cancer().feature_names)
def prediksi(*nilai):
peluang = model_demo.predict_proba([list(nilai)])[0]
return {"jinak": float(peluang[1]), "ganas": float(peluang[0])}
demo = gr.Interface(fn=prediksi,
inputs=[gr.Number(label=n) for n in nama_fitur[:5]] + [gr.Number(label=n, value=0) for n in nama_fitur[5:]],
outputs=gr.Label(num_top_classes=2),
title="Demo klasifikasi (hanya untuk penelitian, bukan alat diagnosis)")
demo.launch(share=True)
7. Model card singkat
Sertakan dokumen ini di repositori agar pembaca tahu batas penggunaan model.
model_card = f"""# Model Card: {KONFIG['judul']}
## Tujuan
Model dikembangkan untuk penelitian skripsi. Bukan alat pengambilan keputusan klinis.
## Data
Sumber: Breast Cancer Wisconsin (Diagnostic), scikit-learn. Jumlah: {len(X)} baris, {X.shape[1]} fitur.
Pembagian: {int((1 - KONFIG['test_size']) * 100)}% latih / {int(KONFIG['test_size'] * 100)}% uji, stratified, seed {SEED}.
## Kinerja (data uji)
{tabel.to_string()}
## Keterbatasan
Data berasal dari satu sumber; belum divalidasi pada populasi lain.
## Reproduksi
Lihat konfig.json dan log_eksperimen.json.
"""
(BASE / "hasil" / "MODEL_CARD.md").write_text(model_card, encoding="utf-8")
print(model_card[:300])
8. Studi kasus: daftar periksa merapikan notebook
- Restart runtime lalu Run all; notebook harus berjalan dari atas sampai bawah tanpa error.
- Sel pertama: judul, tujuan, tanggal, versi pustaka.
- Semua jalur file memakai
BASE, tidak ada jalur pribadi. - Satu seed, satu konfigurasi.
- Hapus sel percobaan yang tidak dipakai; beri judul pada tiap bagian.
- Simpan hasil (tabel, gambar, model, log) ke Drive.
- Unggah ke GitHub (tanpa data pribadi) dan tuliskan tautannya di lampiran.
- Sitasi dataset dan pustaka yang dipakai.
Latihan
Jalankan ulang eksperimen dengan seed 7 dan seed 99 untuk Random Forest saja, dan bandingkan F1 uji. Berapa variasinya?
Lihat kunci jawaban
for s in (7, 99):
Xa, Xb, ya, yb = train_test_split(X, y, test_size=0.2, stratify=y, random_state=s)
m = RandomForestClassifier(random_state=s, n_estimators=200).fit(Xa, ya)
print(s, round(f1_score(yb, m.predict(Xb)), 3))
Tambahkan model ketiga (GradientBoostingClassifier) ke tabel hasil dengan cara yang sama.
Lihat kunci jawaban
from sklearn.ensemble import GradientBoostingClassifier
gb = GradientBoostingClassifier(random_state=SEED).fit(X_tr, y_tr)
print(round(f1_score(y_te, gb.predict(X_te)), 3))
Kesalahan yang sering terjadi
- Melaporkan satu kali percobaan tanpa seed dan tanpa variasi.
- Notebook yang hanya berjalan bila sel dijalankan tidak berurutan.
- Mengunggah data pribadi ke GitHub.
- Klaim berlebihan (misal "model siap dipakai di rumah sakit").
Kuis singkat
1. Mengapa mencatat versi pustaka?
2. Apa isi utama model card?
3. Tanda notebook yang rapi?
Ceklis: saya sudah bisa...
Penghubung ke katalog
Dari bab ke topik skripsi
Setiap kelompok topik pada katalog riset membutuhkan bab tertentu. Kolom "Bab wajib" adalah minimum yang perlu dikuasai; "Bab pendukung" memperkuat kualitas riset.
| Kelompok topik katalog | Bab wajib | Bab pendukung | Catatan |
|---|---|---|---|
| Klasifikasi dan Prediksi | Bab 3, 4, 5, 6 | Bab 12 | Baseline, cross-validation, dan SHAP wajib ada |
| Clustering dan Segmentasi | Bab 3, 4, 5, 7 | Bab 12 | Fokus pada scaling dan interpretasi klaster |
| Association dan Pattern Mining | Bab 3, 7 | Bab 12 | Sequential dan high-utility butuh pustaka khusus (PrefixSpan, PAMI) |
| Text Mining dan NLP Indonesia | Bab 3, 5, 6, 9 | Bab 10, 12 | IndoBERT dan fine-tuning memakai GPU |
| Time Series dan Forecasting | Bab 3, 4, 8 | Bab 6, 12 | Selalu bandingkan dengan baseline naif |
| Anomali, Fraud, dan Keamanan Siber | Bab 5, 6 | Bab 7, 12 | Data tidak seimbang; metrik PR-AUC dan recall |
| Educational Data Mining | Bab 3, 4, 5, 6 | Bab 7, 12 | Perhatikan privasi data mahasiswa |
| Kesehatan dan Sosial | Bab 3, 4, 5, 6 | Bab 7, 12 | Sertakan analisis interpretasi dan etika |
| Recommender System | Bab 3, 6, 7 | Bab 11 | Kemiripan (cosine) dan embedding; pustaka khusus (Surprise, LightFM) dipelajari dari dokumentasinya |
| Computer Vision | Bab 5, 6, 10 | Bab 12 | Wajib GPU T4 untuk dataset besar |
| Graph, Network, dan Process Mining | Bab 3, 4 | Bab 6 | NetworkX, PM4Py, dan PyTorch Geometric belum dibahas di modul ini |
| LLM, RAG, dan Generative AI | Bab 9, 11 | Bab 10, 12 | Butuh GPU untuk model besar; evaluasi retrieval dan jawaban |
| Lingkungan, Pertanian, dan Smart City | Bab 3, 4, 6, 8 | Bab 10 | Sering memakai data spasial dan deret waktu |
Batasan modul: pustaka khusus seperti NetworkX, PM4Py, PyTorch Geometric, Surprise, atau LightFM tidak dibahas; pelajari dari dokumentasinya setelah menguasai dasar di modul ini.
Latihan mandiri
Empat proyek akhir mini
Pilih satu proyek yang paling dekat dengan topik skripsi Anda. Kerjakan di satu notebook memakai template_notebook.ipynb. Kunci proyek tidak disediakan; tujuannya agar Anda terbiasa bekerja mandiri.
Proyek 1: Klasifikasi Tabular Lengkap
Data: Predict Students' Dropout and Academic Success (UCI) atau Telco Customer Churn (Kaggle)
Bab yang dipakai: 3, 4, 5, 6, 12
- EDA dengan checklist Bab 4 dan tiga temuan menarik
- Pipeline preprocessing tanpa kebocoran data
- Baseline dan minimal tiga model, cross-validation
- Tuning dengan Optuna atau GridSearch
- Interpretasi SHAP dan analisis kesalahan
Keluaran: Notebook bersih, tabel hasil, dua gambar, model card satu halaman
Proyek 2: Analisis Sentimen Teks Indonesia
Data: IndoNLU SmSA (Hugging Face) atau ulasan aplikasi yang Anda kumpulkan secara sah
Bab yang dipakai: 5, 6, 9, 12
- Pembersihan teks dan kamus slang buatan sendiri
- Baseline TF-IDF + Logistic Regression/SVM
- Model transformer (inferensi atau fine-tuning)
- Perbandingan hasil dan analisis 20 contoh salah prediksi
Keluaran: Notebook, tabel perbandingan, daftar analisis kesalahan, demo Gradio
Proyek 3: Peramalan Deret Waktu
Data: Harga pangan (PIHPS Bank Indonesia) atau harga saham/kripto via yfinance
Bab yang dipakai: 3, 4, 8, 12
- Pembersihan dan resampling
- Baseline naif dan musiman naif
- Minimal dua model statistik/ML dengan validasi berurutan
- Analisis periode dengan kesalahan terbesar
Keluaran: Notebook, grafik aktual vs prediksi, tabel MAE/MAPE
Proyek 4: Klasifikasi Citra dengan Transfer Learning
Data: Subset PlantVillage atau Rice Leaf Diseases (Kaggle), 3 sampai 5 kelas
Bab yang dipakai: 5, 6, 10, 12
- Susun folder train/val/test dan augmentasi
- Transfer learning MobileNet atau EfficientNet
- Confusion matrix dan F1 per kelas
- Grad-CAM atau contoh gambar salah klasifikasi
Keluaran: Notebook, model tersimpan di Drive, ringkasan hasil
Rubrik penilaian mandiri (100 poin)
| Aspek | Poin |
|---|---|
| Data dan EDA | 20 |
| Preprocessing tanpa kebocoran data | 20 |
| Pemodelan dan evaluasi | 30 |
| Interpretasi dan analisis kesalahan | 15 |
| Reproduktibilitas dan kerapian | 15 |