Bulan 2: Statistik Deskriptif
Abbas belajar membaca dan merangkum data secara matematis, dari ukuran pemusatan hingga sebaran, distribusi, dan korelasi antar variabel.
Apa yang Abbas Pelajari Bulan Ini
Di bulan ini, Abbas tidak hanya menghafalkan rumus. Abbas akan belajar kapan suatu ukuran lebih tepat dipakai dibanding yang lain, dan bagaimana data bisa "berbohong" jika salah dibaca.
Kompetensi yang Abbas Kuasai di Akhir Bulan
- Menghitung dan menginterpretasikan mean, median, dan mode dari sebuah dataset nyata
- Memilih ukuran pemusatan yang tepat tergantung karakteristik data (ada outlier atau tidak)
- Menjelaskan sebaran data menggunakan range, varians, standar deviasi, dan IQR
- Membaca dan membuat boxplot serta histogram untuk melihat distribusi data secara visual
- Memahami konsep distribusi normal dan mengapa distribusi ini sering muncul di dunia nyata
- Menghitung dan menginterpretasikan korelasi antara dua variabel secara benar
- Menghindari jebakan umum: korelasi bukan kausalitas, mean yang menyesatkan karena outlier
- Mengimplementasikan semua kalkulasi di atas menggunakan Python (NumPy dan statistik bawaan)
Minggu 1: Statistik Deskriptif: Mean, Median, Mode
Minggu pertama membangun fondasi paling dasar: ukuran pemusatan data. Abbas akan belajar menghitung, membedakan, dan memilih ukuran yang paling jujur untuk berbagai jenis data.
Abbas mulai dengan pertanyaan mendasar: kenapa kita tidak cukup melihat semua data satu per satu?
- Baca artikel pendek tentang perbedaan statistik deskriptif vs inferensial (cukup 15 menit)
- Buat dataset manual: catat nilai ulangan 10 teman (boleh fiksi), simpan sebagai list Python
- Eksplorasi fungsi bawaan Python:
len(),sum(),min(),max()pada list tersebut - Tulis di notebook: apa pertanyaan yang bisa dijawab hanya dari 4 fungsi itu?
# Dataset nilai ulangan 10 siswa
nilai = [72, 85, 60, 91, 78, 85, 55, 88, 70, 85]
# Eksplorasi dasar
print("Jumlah siswa:", len(nilai))
print("Total nilai:", sum(nilai))
print("Nilai terendah:", min(nilai))
print("Nilai tertinggi:", max(nilai))
print("Rentang nilai:", max(nilai) - min(nilai))Abbas menghitung mean secara manual dan dengan Python, lalu langsung melihat bagaimana satu angka ekstrem bisa merusak cerita.
- Hitung mean dataset nilai dari Hari 1 secara manual (jumlah dibagi banyak data)
- Verifikasi dengan Python menggunakan
sum()/len()dan juga modulstatistics - Tambahkan satu outlier ke dataset (misalnya nilai 20) dan hitung ulang mean, amati perubahan
- Cari contoh nyata dari berita atau kehidupan sehari-hari di mana "rata-rata" bisa menyesatkan
import statistics
nilai = [72, 85, 60, 91, 78, 85, 55, 88, 70, 85]
# Hitung mean
mean_manual = sum(nilai) / len(nilai)
mean_lib = statistics.mean(nilai)
print(f"Mean manual: {mean_manual:.2f}")
print(f"Mean library: {mean_lib:.2f}")
# Tambah outlier
nilai_dengan_outlier = nilai + [20]
mean_baru = statistics.mean(nilai_dengan_outlier)
print(f"\nSetelah outlier ditambah:")
print(f"Mean baru: {mean_baru:.2f}")
print(f"Perubahan: {mean_baru - mean_manual:.2f} poin")Abbas belajar median sebagai alternatif yang lebih "jujur" untuk data yang memiliki outlier atau distribusi condong.
- Hitung median secara manual: urutkan data, ambil nilai tengah (atau rata-rata dua nilai tengah jika genap)
- Bandingkan median dataset asli vs dataset dengan outlier, perhatikan bedanya dengan mean
- Gunakan
statistics.median()untuk memverifikasi - Diskusikan di notebook: kapan Abbas lebih memilih median daripada mean?
import statistics
nilai = [72, 85, 60, 91, 78, 85, 55, 88, 70, 85]
nilai_dengan_outlier = nilai + [20]
# Perbandingan mean vs median
print("=== Dataset Asli ===")
print(f"Mean: {statistics.mean(nilai):.2f}")
print(f"Median: {statistics.median(nilai):.2f}")
print("\n=== Setelah Outlier (nilai 20) ===")
print(f"Mean: {statistics.mean(nilai_dengan_outlier):.2f}")
print(f"Median: {statistics.median(nilai_dengan_outlier):.2f}")
# Mean turun drastis, median hampir tidak berubah
print("\n=> Median lebih stabil terhadap outlier")Abbas mempelajari mode, yang berguna khususnya untuk data kategorikal dan distribusi multimodal.
- Hitung mode dari dataset nilai secara manual (hitung frekuensi masing-masing nilai)
- Gunakan
statistics.mode()danstatistics.multimode()untuk dataset yang memiliki beberapa mode - Buat contoh data kategorikal: warna baju favorit 20 orang, cari mode-nya
- Pahami mengapa mode adalah satu-satunya ukuran pemusatan yang berlaku untuk data nominal
import statistics
from collections import Counter
nilai = [72, 85, 60, 91, 78, 85, 55, 88, 70, 85]
# Mode nilai ujian
print("Mode nilai:", statistics.mode(nilai))
print("Semua mode:", statistics.multimode(nilai))
# Frekuensi manual
frekuensi = Counter(nilai)
print("\nFrekuensi tiap nilai:")
for angka, jumlah in sorted(frekuensi.items()):
print(f" {angka}: {'*' * jumlah} ({jumlah}x)")
# Contoh data kategorikal
warna = ["biru", "merah", "biru", "hijau", "biru", "merah", "merah", "biru"]
print("\nWarna favorit mode:", statistics.mode(warna))Abbas mengintegrasikan ketiga ukuran pemusatan dalam satu analisis menggunakan dataset yang lebih besar dan nyata.
- Unduh atau buat dataset sederhana: harga jajanan di kantin sekolah (minimal 20 item)
- Hitung ketiga ukuran pemusatan dan bandingkan hasilnya
- Buat fungsi Python
ringkasan_pemusatan(data)yang mengembalikan dictionary berisi ketiganya - Tulis interpretasi: apa cerita yang bisa Abbas sampaikan dari ketiga angka itu?
import statistics
def ringkasan_pemusatan(data, label="Data"):
"""Mengembalikan ringkasan mean, median, mode."""
hasil = {
"mean": statistics.mean(data),
"median": statistics.median(data),
"mode": statistics.multimode(data),
"n": len(data)
}
print(f"\n=== Ringkasan Pemusatan: {label} ===")
print(f"Jumlah data : {hasil['n']}")
print(f"Mean : {hasil['mean']:.2f}")
print(f"Median : {hasil['median']:.2f}")
print(f"Mode : {hasil['mode']}")
return hasil
# Harga jajanan kantin (dalam ribuan rupiah)
harga_kantin = [3, 5, 5, 7, 3, 10, 5, 8, 3, 5,
6, 4, 5, 3, 7, 5, 4, 6, 5, 25]
ringkasan_pemusatan(harga_kantin, "Harga Jajanan Kantin")Minggu 2: Sebaran Data
Ukuran pemusatan saja tidak cukup. Dua dataset bisa memiliki mean yang sama tetapi distribusi yang sangat berbeda. Minggu ini Abbas belajar mengukur dan memahami sebaran data.
Abbas memulai dengan ukuran sebaran paling sederhana: range, lalu langsung melihat keterbatasannya.
- Buat dua dataset dengan mean yang sama tapi "terasa" berbeda saat dibaca satu per satu
- Hitung range (max - min) untuk keduanya, amati apakah range sudah cukup membedakan
- Diskusikan kasus nyata: dua kelas dengan nilai rata-rata sama, tapi satu lebih "merata" dari yang lain
- Catat di notebook: apa informasi yang tidak bisa ditangkap oleh range saja?
import statistics
# Dua kelas dengan mean sama tapi sebaran berbeda
kelas_A = [70, 72, 74, 75, 76, 78, 78, 79, 80, 78]
kelas_B = [40, 50, 60, 70, 80, 90, 95, 98, 82, 75]
for nama, data in [("Kelas A", kelas_A), ("Kelas B", kelas_B)]:
print(f"\n{nama}:")
print(f" Data : {sorted(data)}")
print(f" Mean : {statistics.mean(data):.2f}")
print(f" Range : {max(data) - min(data)}")
print("\n=> Mean sama, range berbeda. Tapi apakah range cukup?")
Abbas mempelajari dua ukuran sebaran paling penting dalam statistik: varians dan standar deviasi, lengkap dengan intuisi mengapa satuan standar deviasi lebih mudah dipahami.
- Hitung varians secara manual: rata-rata kuadrat deviasi setiap titik dari mean
- Hitung standar deviasi sebagai akar kuadrat varians
- Bandingkan hasil manual dengan
statistics.stdev()danstatistics.pstdev()(pahami bedanya: sampel vs populasi) - Interpretasikan: standar deviasi berarti "rata-rata seberapa jauh data dari mean"
import statistics
import math
data = [70, 72, 74, 75, 76, 78, 78, 79, 80, 78]
mean = statistics.mean(data)
# Varians manual (populasi)
deviasi_kuadrat = [(x - mean) ** 2 for x in data]
varians_manual = sum(deviasi_kuadrat) / len(data)
stdev_manual = math.sqrt(varians_manual)
print(f"Mean: {mean:.2f}")
print(f"\nDeviasi tiap titik: {[round(x - mean, 2) for x in data]}")
print(f"Deviasi kuadrat: {[round(d, 2) for d in deviasi_kuadrat]}")
print(f"\nVarians (populasi) manual : {varians_manual:.4f}")
print(f"Stdev (populasi) manual : {stdev_manual:.4f}")
print(f"\nVarians (sampel) library : {statistics.variance(data):.4f}")
print(f"Stdev (sampel) library : {statistics.stdev(data):.4f}")pstdev() / pvariance() saat data adalah seluruh populasi (semua siswa di kelas itu). Gunakan stdev() / variance() saat data adalah sampel dari populasi yang lebih besar (30 siswa mewakili seluruh angkatan).
Abbas mempelajari interquartile range (IQR) sebagai ukuran sebaran yang tahan terhadap outlier, dan menggunakannya untuk mendeteksi nilai-nilai ekstrem secara sistematis.
- Pahami konsep kuartil: Q1 (25%), Q2 (median/50%), Q3 (75%)
- Hitung IQR = Q3 - Q1 untuk dataset nilai ujian
- Terapkan aturan outlier: nilai di luar
[Q1 - 1.5*IQR, Q3 + 1.5*IQR]dianggap outlier - Identifikasi outlier dari dataset harga kantin yang dibuat di Minggu 1
import statistics
def hitung_kuartil(data):
data_sorted = sorted(data)
n = len(data_sorted)
q1 = statistics.median(data_sorted[:n//2])
q2 = statistics.median(data_sorted)
q3 = statistics.median(data_sorted[(n+1)//2:])
return q1, q2, q3
harga = [3, 5, 5, 7, 3, 10, 5, 8, 3, 5,
6, 4, 5, 3, 7, 5, 4, 6, 5, 25]
q1, q2, q3 = hitung_kuartil(harga)
iqr = q3 - q1
batas_bawah = q1 - 1.5 * iqr
batas_atas = q3 + 1.5 * iqr
print(f"Q1: {q1}, Q2: {q2}, Q3: {q3}")
print(f"IQR: {iqr}")
print(f"Batas normal: [{batas_bawah:.2f}, {batas_atas:.2f}]")
outlier = [x for x in harga if x < batas_bawah or x > batas_atas]
print(f"\nOutlier terdeteksi: {outlier}")Abbas mempelajari boxplot sebagai cara visual untuk melihat median, IQR, dan outlier dalam satu gambar sederhana.
- Gambar boxplot secara manual di kertas: garis median, kotak IQR, whisker, dan titik outlier
- Install dan gunakan
matplotlibuntuk membuat boxplot pertama Abbas - Buat perbandingan boxplot dua dataset: Kelas A vs Kelas B dari Hari 1
- Latihan membaca boxplot orang lain: cari 1 boxplot dari artikel atau laporan, deskripsikan apa yang terlihat
import matplotlib.pyplot as plt
kelas_A = [70, 72, 74, 75, 76, 78, 78, 79, 80, 78]
kelas_B = [40, 50, 60, 70, 80, 90, 95, 98, 82, 75]
# Boxplot perbandingan
fig, ax = plt.subplots(figsize=(7, 5))
ax.boxplot(
[kelas_A, kelas_B],
labels=["Kelas A", "Kelas B"],
patch_artist=True,
boxprops=dict(facecolor="#e8f4f8")
)
ax.set_title("Perbandingan Sebaran Nilai Dua Kelas")
ax.set_ylabel("Nilai")
ax.grid(axis="y", alpha=0.4)
plt.tight_layout()
plt.savefig("boxplot_kelas.png", dpi=100)
plt.show()
print("Boxplot disimpan sebagai boxplot_kelas.png")Abbas membuat fungsi analisis sebaran lengkap yang mengintegrasikan semua ukuran yang dipelajari minggu ini.
- Buat fungsi
laporan_sebaran(data, label)yang mencetak semua ukuran sebaran dalam format yang rapi - Terapkan fungsi itu pada tiga dataset berbeda: nilai ujian, harga kantin, dan satu dataset pilihan Abbas
- Buat satu boxplot untuk masing-masing dataset dan simpan sebagai file gambar
- Tulis narasi singkat untuk tiap dataset: "Data ini tergolong... karena standar deviasinya..."
import statistics
import math
def laporan_sebaran(data, label="Dataset"):
data_s = sorted(data)
n = len(data_s)
q1 = statistics.median(data_s[:n//2])
q3 = statistics.median(data_s[(n+1)//2:])
iqr = q3 - q1
print(f"\n{'='*40}")
print(f"Laporan Sebaran: {label}")
print(f"{'='*40}")
print(f"N : {n}")
print(f"Range : {max(data) - min(data)}")
print(f"Varians (samp) : {statistics.variance(data):.4f}")
print(f"Stdev (samp) : {statistics.stdev(data):.4f}")
print(f"Q1 : {q1}")
print(f"Median (Q2) : {statistics.median(data)}")
print(f"Q3 : {q3}")
print(f"IQR : {iqr}")
batas_atas = q3 + 1.5 * iqr
batas_bawah = q1 - 1.5 * iqr
outlier = [x for x in data if x < batas_bawah or x > batas_atas]
print(f"Outlier : {outlier if outlier else 'Tidak ada'}")
nilai = [72, 85, 60, 91, 78, 85, 55, 88, 70, 85]
harga = [3, 5, 5, 7, 3, 10, 5, 8, 3, 5, 6, 4, 5, 3, 7, 5, 4, 6, 5, 25]
laporan_sebaran(nilai, "Nilai Ujian")
laporan_sebaran(harga, "Harga Jajanan Kantin")Minggu 3: Distribusi dan Visualisasi Dasar
Minggu ini Abbas naik level: dari menghitung angka ke memahami bentuk data secara visual. Abbas akan belajar distribusi normal, histogram, dan bagaimana bentuk distribusi memengaruhi pilihan analisis.
Abbas membuat histogram pertamanya dan belajar membaca bentuk distribusi dari gambar.
- Pahami perbedaan histogram dengan bar chart: histogram untuk data kontinu, bar chart untuk kategoris
- Buat histogram nilai ujian 30 siswa menggunakan
matplotlib - Coba ubah jumlah bin (10, 5, 20) dan amati bagaimana bentuk grafik berubah
- Identifikasi dari histogram: apakah distribusinya simetris, condong kiri, atau condong kanan?
import matplotlib.pyplot as plt
import random
# Buat dataset nilai 30 siswa
random.seed(42)
nilai_30 = [random.gauss(75, 10) for _ in range(30)]
nilai_30 = [max(0, min(100, round(n))) for n in nilai_30]
fig, axes = plt.subplots(1, 3, figsize=(14, 4))
for ax, bins, title in zip(axes, [5, 10, 20],
["5 Bin", "10 Bin", "20 Bin"]):
ax.hist(nilai_30, bins=bins, color="#4a90d9", edgecolor="white", alpha=0.85)
ax.set_title(f"Histogram {title}")
ax.set_xlabel("Nilai")
ax.set_ylabel("Frekuensi")
ax.grid(axis="y", alpha=0.3)
plt.tight_layout()
plt.savefig("histogram_bins.png", dpi=100)
plt.show()
print("Disimpan: histogram_bins.png")Abbas mempelajari distribusi normal, memahami mengapa distribusi ini begitu sering muncul di alam dan data sosial, serta mengenal aturan 68-95-99.7.
- Baca singkat tentang Central Limit Theorem: mengapa rata-rata banyak sampel cenderung berdistribusi normal
- Gambar kurva distribusi normal menggunakan
numpydanmatplotlib - Tandai zona: 1 stdev (68%), 2 stdev (95%), 3 stdev (99.7%) pada kurva
- Cari 3 contoh nyata fenomena yang berdistribusi normal di kehidupan sehari-hari
import numpy as np
import matplotlib.pyplot as plt
# Parameter distribusi normal
mu, sigma = 75, 10
x = np.linspace(mu - 4*sigma, mu + 4*sigma, 300)
y = (1 / (sigma * np.sqrt(2 * np.pi))) * np.exp(-0.5 * ((x - mu) / sigma)**2)
fig, ax = plt.subplots(figsize=(9, 5))
ax.plot(x, y, color="#2c6fad", linewidth=2)
# Zona 68%, 95%, 99.7%
for n_std, color, label in [(1, "#cce5ff", "68% (1 stdev)"),
(2, "#e8f4f8", "95% (2 stdev)"),
(3, "#f5fafe", "99.7% (3 stdev)")]:
ax.fill_between(x, y,
where=(x >= mu - n_std*sigma) & (x <= mu + n_std*sigma),
color=color, label=label)
ax.axvline(mu, color="#555", linestyle="--", linewidth=1, label=f"Mean = {mu}")
ax.set_title("Distribusi Normal: Aturan 68-95-99.7")
ax.set_xlabel("Nilai")
ax.set_ylabel("Densitas")
ax.legend(fontsize=9)
ax.grid(alpha=0.3)
plt.tight_layout()
plt.savefig("distribusi_normal.png", dpi=100)
plt.show()Abbas mempelajari skewness (kemiringan distribusi) dan belajar membaca apa yang distribusi condong kiri atau kanan katakan tentang data.
- Pahami arti positif skew (ekor di kanan) dan negatif skew (ekor di kiri)
- Buat dua dataset yang condong berbeda arah menggunakan NumPy
- Hitung skewness menggunakan rumus manual atau
scipy.stats.skew() - Hubungkan skewness dengan posisi relatif mean dan median: mana yang lebih besar saat skew positif?
import numpy as np
import matplotlib.pyplot as plt
# Distribusi condong kanan (positif skew): banyak nilai kecil, sedikit sangat besar
data_skew_kanan = np.random.exponential(scale=5, size=300)
# Distribusi condong kiri (negatif skew): banyak nilai besar, sedikit sangat kecil
data_skew_kiri = 10 - np.random.exponential(scale=2, size=300)
fig, axes = plt.subplots(1, 2, figsize=(11, 4))
for ax, data, title, color in zip(
axes,
[data_skew_kanan, data_skew_kiri],
["Skew Positif (condong kanan)", "Skew Negatif (condong kiri)"],
["#f4a261", "#457b9d"]
):
ax.hist(data, bins=30, color=color, edgecolor="white", alpha=0.85)
ax.axvline(np.mean(data), color="red", linestyle="--", label=f"Mean={np.mean(data):.1f}")
ax.axvline(np.median(data), color="green", linestyle="-.", label=f"Median={np.median(data):.1f}")
ax.set_title(title)
ax.legend(fontsize=8)
ax.grid(axis="y", alpha=0.3)
plt.tight_layout()
plt.savefig("skewness_compare.png", dpi=100)
plt.show()Abbas mempelajari korelasi Pearson sebagai cara mengukur kekuatan dan arah hubungan antara dua variabel numerik, sekaligus memahami jebakan terbesar: korelasi bukan berarti kausalitas.
- Pahami skala korelasi: -1 (negatif sempurna), 0 (tidak ada hubungan), +1 (positif sempurna)
- Buat scatter plot empat pasang variabel dengan tingkat korelasi berbeda
- Hitung korelasi secara manual menggunakan rumus kovarians dibagi perkalian standar deviasi
- Cari satu contoh "spurious correlation" yang lucu dari tylervigen.com atau sumber lain
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(7)
n = 50
# Empat skenario korelasi
jam_belajar = np.random.uniform(1, 10, n)
nilai_tinggi = jam_belajar * 8 + np.random.normal(0, 5, n) # r ~ +0.95
nilai_sedang = jam_belajar * 4 + np.random.normal(0, 15, n) # r ~ +0.5
nilai_tidak = np.random.uniform(50, 100, n) # r ~ 0
nilai_negatif = -jam_belajar * 6 + 90 + np.random.normal(0, 8, n) # r ~ -0.9
skenario = [
(nilai_tinggi, "Korelasi Kuat Positif"),
(nilai_sedang, "Korelasi Sedang Positif"),
(nilai_tidak, "Hampir Tidak Berkorelasi"),
(nilai_negatif, "Korelasi Kuat Negatif"),
]
fig, axes = plt.subplots(2, 2, figsize=(10, 8))
for ax, (y, title) in zip(axes.flat, skenario):
r = np.corrcoef(jam_belajar, y)[0, 1]
ax.scatter(jam_belajar, y, alpha=0.6, color="#4a90d9", s=30)
ax.set_title(f"{title}
r = {r:.2f}", fontsize=10)
ax.set_xlabel("Jam Belajar")
ax.set_ylabel("Nilai")
ax.grid(alpha=0.3)
plt.tight_layout()
plt.savefig("korelasi_scatter.png", dpi=100)
plt.show()Abbas membuat satu output visual lengkap yang menggabungkan histogram, boxplot, dan scatter plot dalam satu figure, menjadi mini laporan analisis distribusi.
- Pilih satu dataset (nilai 30 siswa atau harga kantin) sebagai bahan utama
- Buat figure dengan 3 subplot: histogram, boxplot horizontal, dan scatter plot nilai vs nomor urut
- Tambahkan anotasi teks langsung di grafik: nilai mean, median, dan stdev
- Simpan hasil sebagai PNG resolusi tinggi, ini menjadi portofolio visual pertama Abbas
import numpy as np
import matplotlib.pyplot as plt
import statistics
random_seed = 42
np.random.seed(random_seed)
nilai = sorted([max(40, min(100, round(np.random.normal(75, 12))))
for _ in range(30)])
mean_val = statistics.mean(nilai)
median_val = statistics.median(nilai)
stdev_val = statistics.stdev(nilai)
fig = plt.figure(figsize=(14, 5))
fig.suptitle("Dashboard Distribusi: Nilai Ujian 30 Siswa", fontsize=13, fontweight="bold")
# Subplot 1: Histogram
ax1 = fig.add_subplot(1, 3, 1)
ax1.hist(nilai, bins=8, color="#4a90d9", edgecolor="white", alpha=0.85)
ax1.axvline(mean_val, color="red", linestyle="--", linewidth=1.5, label=f"Mean={mean_val:.1f}")
ax1.axvline(median_val, color="green", linestyle="-.", linewidth=1.5, label=f"Median={median_val:.1f}")
ax1.set_title("Histogram")
ax1.legend(fontsize=8)
ax1.grid(axis="y", alpha=0.3)
# Subplot 2: Boxplot
ax2 = fig.add_subplot(1, 3, 2)
ax2.boxplot(nilai, vert=False, patch_artist=True,
boxprops=dict(facecolor="#cce5ff"))
ax2.set_title("Boxplot")
ax2.set_xlabel("Nilai")
ax2.grid(axis="x", alpha=0.3)
ax2.annotate(f"Stdev = {stdev_val:.1f}", xy=(0.5, 0.1),
xycoords="axes fraction", ha="center", fontsize=9)
# Subplot 3: Scatter
ax3 = fig.add_subplot(1, 3, 3)
ax3.scatter(range(1, 31), nilai, color="#e07b39", s=40, alpha=0.8)
ax3.axhline(mean_val, color="red", linestyle="--", linewidth=1, label=f"Mean")
ax3.set_title("Nilai per Urutan Rank")
ax3.set_xlabel("Rank Siswa")
ax3.set_ylabel("Nilai")
ax3.legend(fontsize=8)
ax3.grid(alpha=0.3)
plt.tight_layout()
plt.savefig("dashboard_distribusi.png", dpi=150)
plt.show()
print("Disimpan: dashboard_distribusi.png")Minggu 4: Python untuk Statistik
Minggu penutup bulan ini Abbas menyatukan semua yang dipelajari ke dalam kode Python yang rapi, reproducible, dan bisa digunakan kembali. Fokus: NumPy, alur analisis terstruktur, dan mini project akhir bulan.
Abbas beralih dari modul statistics bawaan Python ke NumPy, yang jauh lebih cepat dan fleksibel untuk data berskala besar.
- Pahami perbedaan NumPy array dengan Python list: broadcasting, operasi vektor, efisiensi memori
- Konversi dataset nilai dan harga ke NumPy array, bandingkan kecepatan operasi sederhana
- Gunakan
np.mean(),np.median(),np.std(),np.percentile()untuk menggantikan kalkulasi manual - Buat array 2D (matriks) dari dataset dua variabel dan hitung statistik per baris dan per kolom
import numpy as np
# Konversi ke NumPy array
nilai = np.array([72, 85, 60, 91, 78, 85, 55, 88, 70, 85])
# Statistik dasar NumPy
print("=== NumPy Statistics ===")
print(f"Mean : {np.mean(nilai):.2f}")
print(f"Median : {np.median(nilai):.2f}")
print(f"Stdev (pop) : {np.std(nilai):.2f}")
print(f"Stdev (samp): {np.std(nilai, ddof=1):.2f}")
print(f"Varians : {np.var(nilai, ddof=1):.2f}")
print(f"Min / Max : {np.min(nilai)} / {np.max(nilai)}")
print(f"Q1 / Q3 : {np.percentile(nilai, 25)} / {np.percentile(nilai, 75)}")
# Operasi vektor: normalisasi nilai ke skala 0-1
nilai_norm = (nilai - np.min(nilai)) / (np.max(nilai) - np.min(nilai))
print(f"
Nilai ternormalisasi:")
print(np.round(nilai_norm, 3))ddof=1 di np.std() berarti degrees of freedom = 1, yang menghasilkan standar deviasi sampel (sama dengan statistics.stdev()). Tanpa ddof, NumPy menghitung standar deviasi populasi secara default.
Abbas mempelajari teknik filtering berbasis kondisi di NumPy, yang menjadi fondasi penting sebelum belajar Pandas di bulan mendatang.
- Gunakan boolean masking untuk memfilter array: pilih semua nilai di atas mean
- Hitung statistik untuk subset data yang difilter
- Gunakan
np.where()untuk membuat label berdasarkan kondisi (lulus/tidak lulus) - Latihan: dari 100 nilai random, hitung berapa persen yang masuk zona mean +/- 1 stdev
import numpy as np
np.random.seed(21)
nilai_100 = np.random.normal(loc=75, scale=10, size=100).clip(0, 100).round().astype(int)
mean_val = np.mean(nilai_100)
stdev_val = np.std(nilai_100, ddof=1)
# Boolean masking
di_atas_mean = nilai_100[nilai_100 > mean_val]
di_bawah_mean = nilai_100[nilai_100 <= mean_val]
print(f"Mean: {mean_val:.2f}, Stdev: {stdev_val:.2f}")
print(f"Di atas mean: {len(di_atas_mean)} siswa ({len(di_atas_mean)}%)")
print(f"Di bawah mean: {len(di_bawah_mean)} siswa")
# Zona 1 stdev
dalam_1_stdev = nilai_100[
(nilai_100 >= mean_val - stdev_val) &
(nilai_100 <= mean_val + stdev_val)
]
print(f"
Dalam 1 stdev: {len(dalam_1_stdev)} siswa ({len(dalam_1_stdev):.0f}%)")
# Label lulus/tidak
label = np.where(nilai_100 >= 70, "Lulus", "Tidak Lulus")
lulus = np.sum(label == "Lulus")
print(f"
Lulus (>=70): {lulus} dari 100 siswa")df[df["kolom"] > nilai], mekanismenya sama persis dengan yang dipelajari hari ini.
Abbas belajar menghitung matriks korelasi untuk banyak variabel sekaligus dan memvisualisasikannya sebagai heatmap, teknik yang sangat umum dipakai dalam eksplorasi data awal.
- Buat dataset multi-variabel: 4 kolom (jam belajar, nilai, jam tidur, jarak ke sekolah) untuk 50 siswa
- Hitung matriks korelasi menggunakan
np.corrcoef() - Visualisasikan sebagai heatmap menggunakan
matplotlib.imshow() - Interpretasikan: pasangan variabel mana yang paling berkorelasi? Apakah arahnya masuk akal?
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(5)
n = 50
jam_belajar = np.random.uniform(1, 8, n)
nilai = jam_belajar * 7 + np.random.normal(0, 8, n)
jam_tidur = np.random.uniform(5, 9, n)
jarak_km = np.random.uniform(1, 20, n)
# Stack jadi matriks (4 x n), lalu transpose ke (n x 4)
data_matrix = np.column_stack([jam_belajar, nilai, jam_tidur, jarak_km])
labels = ["Jam Belajar", "Nilai", "Jam Tidur", "Jarak (km)"]
# Correlation matrix: np.corrcoef butuh (n_vars x n_obs)
corr_matrix = np.corrcoef(data_matrix.T)
fig, ax = plt.subplots(figsize=(6, 5))
im = ax.imshow(corr_matrix, cmap="coolwarm", vmin=-1, vmax=1)
plt.colorbar(im, ax=ax)
ax.set_xticks(range(4))
ax.set_yticks(range(4))
ax.set_xticklabels(labels, rotation=30, ha="right", fontsize=9)
ax.set_yticklabels(labels, fontsize=9)
for i in range(4):
for j in range(4):
ax.text(j, i, f"{corr_matrix[i, j]:.2f}",
ha="center", va="center", fontsize=9,
color="white" if abs(corr_matrix[i, j]) > 0.6 else "black")
ax.set_title("Correlation Matrix: Variabel Siswa")
plt.tight_layout()
plt.savefig("heatmap_korelasi.png", dpi=120)
plt.show()Abbas mempelajari z-score sebagai cara mengubah data ke skala standar sehingga variabel dari satuan berbeda bisa dibandingkan secara adil.
- Pahami formula z-score:
(x - mean) / stdev, artinya "berapa stdev x dari mean" - Hitung z-score manual untuk dataset nilai, kemudian verifikasi dengan NumPy
- Interpretasikan: z-score = 2 artinya nilai tersebut 2 standar deviasi di atas rata-rata
- Gunakan z-score untuk membandingkan performa dua siswa di mata pelajaran yang berbeda skalanya
import numpy as np
nilai_mat = np.array([85, 72, 90, 65, 78])
nilai_ing = np.array([75, 80, 70, 90, 65])
nama_siswa = ["Ari", "Budi", "Citra", "Dian", "Eka"]
def z_score(data):
return (data - np.mean(data)) / np.std(data, ddof=1)
z_mat = z_score(nilai_mat)
z_ing = z_score(nilai_ing)
print(f"{'Nama':<8} {'Mat':>6} {'z-Mat':>7} {'Ing':>6} {'z-Ing':>7} {'Total z':>8}")
print("-" * 48)
for i, nama in enumerate(nama_siswa):
total_z = z_mat[i] + z_ing[i]
print(f"{nama:<8} {nilai_mat[i]:>6} {z_mat[i]:>+7.2f} "
f"{nilai_ing[i]:>6} {z_ing[i]:>+7.2f} {total_z:>+8.2f}")
# Siapa yang performanya paling baik secara keseluruhan?
terbaik = nama_siswa[np.argmax(z_mat + z_ing)]
print(f"
Performa terbaik keseluruhan: {terbaik}")Abbas mengintegrasikan semua yang dipelajari bulan ini menjadi satu skrip analisis lengkap yang bisa langsung dijalankan ulang dengan dataset berbeda.
- Pilih satu dataset nyata minimal 50 baris (nilai ujian sekolah, data cuaca, harga pasar, atau dari Kaggle)
- Jalankan analisis lengkap: pemusatan, sebaran, distribusi, korelasi antar variabel
- Buat figure empat panel: histogram, boxplot, scatter matrix, heatmap korelasi
- Tulis ringkasan analisis minimal 200 kata dalam bentuk markdown di file
laporan_bulan02.md - Simpan semua kode di
analisis_bulan02.pydengan komentar yang jelas di setiap blok
import numpy as np
import matplotlib.pyplot as plt
import statistics
# ============================================================
# Mini Project Bulan 2: Analisis Deskriptif Dataset Nilai SMA
# Ganti dataset di bawah dengan data nyata pilihan Abbas
# ============================================================
np.random.seed(99)
# Dataset contoh: nilai 60 siswa, 3 mata pelajaran
mat = np.random.normal(72, 12, 60).clip(30, 100).round().astype(int)
ing = np.random.normal(68, 15, 60).clip(30, 100).round().astype(int)
bio = np.random.normal(75, 10, 60).clip(30, 100).round().astype(int)
variabel = {"Matematika": mat, "Inggris": ing, "Biologi": bio}
# --- Ringkasan statistik ---
print("=" * 55)
print(f"{'Variabel':<14} {'Mean':>7} {'Median':>8} {'Stdev':>7} {'IQR':>7}")
print("-" * 55)
for nama, data in variabel.items():
q1, q3 = np.percentile(data, [25, 75])
print(f"{nama:<14} {np.mean(data):>7.2f} {np.median(data):>8.2f} "
f"{np.std(data, ddof=1):>7.2f} {q3-q1:>7.2f}")
# --- Figure empat panel ---
fig, axes = plt.subplots(2, 2, figsize=(13, 9))
fig.suptitle("Laporan Statistik Deskriptif: Nilai 60 Siswa", fontsize=13, fontweight="bold")
# Panel 1: Histogram
ax = axes[0, 0]
for nama, data, color in zip(variabel.keys(), variabel.values(),
["#4a90d9", "#e07b39", "#5cb85c"]):
ax.hist(data, bins=12, alpha=0.55, label=nama, color=color, edgecolor="white")
ax.set_title("Distribusi Nilai per Mata Pelajaran")
ax.legend(fontsize=8)
ax.grid(axis="y", alpha=0.3)
# Panel 2: Boxplot
ax = axes[0, 1]
ax.boxplot(list(variabel.values()), labels=list(variabel.keys()),
patch_artist=True,
boxprops=dict(facecolor="#e8f4f8"))
ax.set_title("Boxplot Perbandingan")
ax.grid(axis="y", alpha=0.3)
# Panel 3: Scatter Mat vs Ing
ax = axes[1, 0]
ax.scatter(mat, ing, alpha=0.6, color="#4a90d9", s=35)
r = np.corrcoef(mat, ing)[0, 1]
ax.set_title(f"Scatter: Matematika vs Inggris (r={r:.2f})")
ax.set_xlabel("Matematika")
ax.set_ylabel("Inggris")
ax.grid(alpha=0.3)
# Panel 4: Heatmap korelasi
ax = axes[1, 1]
corr = np.corrcoef(np.column_stack([mat, ing, bio]).T)
nama_list = list(variabel.keys())
im = ax.imshow(corr, cmap="coolwarm", vmin=-1, vmax=1)
plt.colorbar(im, ax=ax)
ax.set_xticks(range(3))
ax.set_yticks(range(3))
ax.set_xticklabels(nama_list, rotation=20, ha="right", fontsize=8)
ax.set_yticklabels(nama_list, fontsize=8)
for i in range(3):
for j in range(3):
ax.text(j, i, f"{corr[i,j]:.2f}", ha="center", va="center", fontsize=9,
color="white" if abs(corr[i, j]) > 0.6 else "black")
ax.set_title("Heatmap Korelasi")
plt.tight_layout()
plt.savefig("laporan_bulan02.png", dpi=150)
plt.show()
print("Selesai. Simpan juga laporan_bulan02.md dengan interpretasi teks.")Buku Pendukung Bulan 02
Empat buku ini dipilih khusus untuk memperkuat pemahaman Abbas tentang statistik deskriptif, distribusi, dan cara membaca data secara kritis. Tidak harus dibaca tuntas, cukup bab yang relevan per minggu.
Tools yang Digunakan
Bulan ini Abbas memperkenalkan beberapa library Python yang akan menjadi alat kerja utama sepanjang sisa program. Pelajari instalasi dan cara impor masing-masing sejak awal.
statisticsmean(), median(), stdev(), dan multimode(). Cocok untuk dataset kecil dan eksplorasi awal sebelum beralih ke NumPy.pip install numpy.pip install matplotlib. Abbas akan terus menggunakannya hingga Bulan 6.pip install jupyter.Tips Praktis dan Jebakan Pemula
Statistik deskriptif terlihat sederhana, tapi penuh jebakan yang bahkan analis berpengalaman kadang terjatuh di dalamnya. Bagian ini merangkum hal-hal yang sering salah dipahami pemula, lengkap dengan cara menghindarinya.
scipy.stats.gmean([1.30, 1.50]) - 1. Mean aritmetik pada rasio pertumbuhan bisa melebih-lebihkan hasil secara sistematis dan menyesatkan kesimpulan akhir.
ddof=1 untuk data sampel di NumPy.
Saat Abbas menganalisis sebagian data (bukan seluruh populasi), selalu gunakan np.std(data, ddof=1) dan np.var(data, ddof=1). Tanpa ddof=1, NumPy meremehkan sebaran data karena membagi dengan N bukan N-1. Perbedaan ini penting terutama saat n kecil, misalnya kurang dari 30 data.
np.percentile(data, 75) - np.percentile(data, 25) kadang menghasilkan nilai berbeda dari perhitungan manual karena NumPy menggunakan interpolasi linear secara default. Jika Abbas butuh hasil yang konsisten dengan kalkulasi tangan, gunakan np.percentile(data, 75, method='midpoint') agar hasilnya sama dengan metode kuartil paling umum diajarkan di sekolah.
scipy.stats.describe() adalah pintasan yang sering dilupakan.
Daripada menghitung mean, varians, min, max, dan skewness satu per satu, cukup panggil from scipy import stats; stats.describe(data). Fungsi ini mengembalikan semua ringkasan sekaligus dalam satu objek. Abbas bisa langsung akses hasilnya dengan desc.mean, desc.variance, desc.skewness, dan desc.kurtosis.
cv = stdev / mean * 100. CV adalah standar deviasi dalam bentuk persentase dari mean, sehingga dua variabel dengan satuan berbeda bisa dibandingkan keragamannya secara adil dan setara.
scipy.stats.spearmanr(x, y). Spearman lebih tahan terhadap outlier karena bekerja pada ranking, bukan nilai asli data.
k = 1 + 3.322 * log10(n)) adalah titik awal yang baik secara matematis, tapi Abbas tetap harus mencoba beberapa pilihan bin dan memilih yang paling informatif untuk konteks datanya.
Latihan Tambahan: Tantangan Lebih Dalam
Latihan berikut dirancang untuk Abbas yang sudah menyelesaikan semua materi empat minggu dan ingin menguji pemahaman lebih jauh. Setiap latihan membutuhkan pemahaman lintas topik dan kemampuan memilih alat yang tepat untuk konteks yang diberikan.
Abbas membuat analisis statistik deskriptif komprehensif menggunakan scipy.stats.describe() dan menambahkan uji normalitas Shapiro-Wilk untuk memverifikasi apakah distribusi nilai mendekati normal secara statistik.
import numpy as np
from scipy import stats
import matplotlib.pyplot as plt
np.random.seed(77)
# Dataset nilai 40 siswa, 4 mata pelajaran
n = 40
nilai = {
"Matematika" : np.random.normal(70, 12, n).clip(30, 100).round().astype(int),
"Bahasa Indo": np.random.normal(78, 8, n).clip(40, 100).round().astype(int),
"IPA" : np.random.normal(65, 15, n).clip(30, 100).round().astype(int),
"IPS" : np.random.normal(75, 10, n).clip(40, 100).round().astype(int),
}
print(f"{'Mapel':<14} {'Mean':>6} {'Stdev':>7} {'Skew':>7} {'Kurt':>7} {'Normal?':>9}")
print("-" * 56)
for mapel, data in nilai.items():
desc = stats.describe(data)
skew = stats.skew(data)
kurt = stats.kurtosis(data)
# Shapiro-Wilk: p > 0.05 berarti distribusi tidak berbeda signifikan dari normal
_, p = stats.shapiro(data)
normal = "Ya" if p > 0.05 else "Tidak"
print(f"{mapel:<14} {desc.mean:>6.1f} {np.sqrt(desc.variance):>7.2f} "
f"{skew:>7.2f} {kurt:>7.2f} {normal:>7} (p={p:.3f})")
# Visualisasi distribusi tiap mapel dengan kurva normal teoritis
fig, axes = plt.subplots(2, 2, figsize=(12, 8))
fig.suptitle("Distribusi Nilai per Mata Pelajaran", fontsize=12, fontweight="bold")
for ax, (mapel, data) in zip(axes.flat, nilai.items()):
ax.hist(data, bins=10, color="#4a90d9", edgecolor="white", alpha=0.8, density=True)
xmin, xmax = ax.get_xlim()
x = np.linspace(xmin, xmax, 100)
ax.plot(x, stats.norm.pdf(x, np.mean(data), np.std(data, ddof=1)),
color="red", linewidth=2, linestyle="--", label="Normal teoritis")
ax.axvline(np.mean(data), color="#e07b39", linewidth=1.5,
label=f"Mean={np.mean(data):.1f}")
ax.set_title(mapel)
ax.legend(fontsize=7)
ax.grid(axis="y", alpha=0.3)
plt.tight_layout()
plt.savefig("analisis_scipy_nilai.png", dpi=120)
plt.show()
print("Selesai. Catat nilai p Shapiro-Wilk dan interpretasinya di notebook.")Abbas membandingkan tiga metode deteksi outlier pada dataset nilai yang sama dan menganalisis kapan masing-masing metode lebih tepat digunakan berdasarkan karakteristik data yang dihadapi.
import numpy as np
from scipy import stats
np.random.seed(13)
nilai_bersih = np.random.normal(75, 10, 45).clip(50, 100).round().astype(int)
outlier_vals = np.array([15, 20, 108, 112]) # nilai ekstrem sengaja dimasukkan
data = np.concatenate([nilai_bersih, outlier_vals])
print(f"Dataset: {len(data)} nilai, dengan {len(outlier_vals)} outlier buatan\n")
# Metode 1: IQR (threshold di luar 1.5 * IQR dari Q1 atau Q3)
q1, q3 = np.percentile(data, [25, 75])
iqr = q3 - q1
outlier_iqr = data[(data < q1 - 1.5 * iqr) | (data > q3 + 1.5 * iqr)]
# Metode 2: Z-Score standar (threshold |z| lebih dari 3)
z_scores = np.abs(stats.zscore(data))
outlier_z = data[z_scores > 3]
# Metode 3: Modified Z-Score berbasis MAD, lebih robust dari z-score biasa
median_val = np.median(data)
mad = np.median(np.abs(data - median_val)) # Median Absolute Deviation
mod_z = 0.6745 * (data - median_val) / (mad + 1e-9)
outlier_mod_z = data[np.abs(mod_z) > 3.5]
print("=== Hasil Deteksi Outlier ===")
print(f"Metode IQR : {sorted(outlier_iqr)}")
print(f"Metode Z-Score : {sorted(outlier_z)}")
print(f"Metode Modified Z : {sorted(outlier_mod_z)}")
print(f"Outlier yang ditanam: {sorted(outlier_vals)}")
# Bandingkan statistik sebelum dan sesudah hapus outlier (metode IQR)
batas_bawah = q1 - 1.5 * iqr
batas_atas = q3 + 1.5 * iqr
data_clean = data[(data >= batas_bawah) & (data <= batas_atas)]
print(f"\n{'Metrik':<10} {'Semua Data':>12} {'Setelah IQR':>13}")
print("-" * 38)
for nama, fn in [("Mean", np.mean), ("Median", np.median),
("Stdev", lambda x: np.std(x, ddof=1))]:
print(f"{nama:<10} {fn(data):>12.2f} {fn(data_clean):>13.2f}")Abbas mensimulasikan Central Limit Theorem secara langsung: mengambil banyak sampel dari distribusi tidak normal (bimodal), menghitung mean tiap sampel, dan membuktikan bahwa distribusi mean-nya mendekati normal seiring bertambahnya ukuran sampel.
import numpy as np
import matplotlib.pyplot as plt
from scipy import stats
np.random.seed(42)
# Populasi bimodal: kelompok nilai rendah dan nilai tinggi dalam satu sekolah
populasi = np.concatenate([
np.random.normal(45, 8, 300), # kelompok siswa nilai rendah
np.random.normal(88, 7, 300), # kelompok siswa nilai tinggi
]).clip(0, 100).round().astype(int)
print(f"Populasi: n={len(populasi)}, mean={np.mean(populasi):.2f}, stdev={np.std(populasi):.2f}")
ukuran_sampel = [5, 15, 30]
n_simulasi = 1000
fig, axes = plt.subplots(2, len(ukuran_sampel) + 1, figsize=(14, 8))
fig.suptitle("Simulasi Central Limit Theorem: Distribusi Nilai Bimodal",
fontsize=11, fontweight="bold")
# Tampilkan distribusi populasi asli
axes[0, 0].hist(populasi, bins=30, color="#e07b39", edgecolor="white", alpha=0.85)
axes[0, 0].set_title("Distribusi Populasi\n(Bimodal, tidak normal)")
axes[0, 0].set_xlabel("Nilai")
axes[0, 0].grid(axis="y", alpha=0.3)
axes[1, 0].axis("off")
axes[1, 0].text(0.5, 0.5,
f"Populasi\nn = {len(populasi)}\nmean = {np.mean(populasi):.1f}\nstdev = {np.std(populasi):.1f}",
ha="center", va="center", fontsize=10,
bbox=dict(boxstyle="round", facecolor="#f5f5f5"))
for col, n in enumerate(ukuran_sampel, start=1):
sample_means = np.array([
np.mean(np.random.choice(populasi, n, replace=False))
for _ in range(n_simulasi)
])
mu = np.mean(sample_means)
sigma = np.std(sample_means)
ax = axes[0, col]
ax.hist(sample_means, bins=30, color="#4a90d9", edgecolor="white", alpha=0.85, density=True)
x = np.linspace(mu - 4*sigma, mu + 4*sigma, 200)
ax.plot(x, stats.norm.pdf(x, mu, sigma), color="red", linewidth=2, label="Normal fit")
ax.set_title(f"Mean Sampel (n={n})\n{n_simulasi} simulasi")
ax.legend(fontsize=7)
ax.grid(axis="y", alpha=0.3)
ax2 = axes[1, col]
ax2.axis("off")
_, p_sw = stats.shapiro(sample_means[:50])
ax2.text(0.5, 0.5,
f"n per sampel = {n}\nmean means = {mu:.2f}\nstdev means = {sigma:.2f}\nShapiro p = {p_sw:.3f}",
ha="center", va="center", fontsize=9,
bbox=dict(boxstyle="round", facecolor="#e8f4f8"))
plt.tight_layout()
plt.savefig("clt_simulasi.png", dpi=120)
plt.show()
print("Simpan gambar dan tulis interpretasi: apakah CLT terbukti untuk data bimodal?")Abbas mensimulasikan skenario nyata: kepala sekolah ingin tahu kelas mana yang performanya paling merata dan paling tinggi. Abbas harus memberikan analisis lengkap dengan argumen yang didukung data statistik konkret, bukan hanya intuisi.
import numpy as np
from scipy import stats
import matplotlib.pyplot as plt
np.random.seed(55)
# Tiga kelas dengan karakteristik yang sengaja berbeda
kelas = {
"X-A": np.random.normal(80, 6, 35).clip(55, 100).round().astype(int), # Tinggi, merata
"X-B": np.random.normal(72, 14, 35).clip(40, 100).round().astype(int), # Sedang, beragam
"X-C": np.concatenate([ # Bimodal
np.random.normal(60, 5, 20),
np.random.normal(90, 5, 15)
]).clip(40, 100).round().astype(int),
}
# Ringkasan statistik komprehensif per kelas
print(f"{'Kelas':<8} {'Mean':>6} {'Median':>8} {'Stdev':>7} {'CV%':>6} {'IQR':>6} {'Skew':>7}")
print("-" * 50)
for nama, data in kelas.items():
q1, q3 = np.percentile(data, [25, 75])
cv = np.std(data, ddof=1) / np.mean(data) * 100
print(f"{nama:<8} {np.mean(data):>6.1f} {np.median(data):>8.1f} "
f"{np.std(data, ddof=1):>7.2f} {cv:>6.1f} {q3-q1:>6.1f} {stats.skew(data):>7.2f}")
# Visualisasi: histogram, boxplot, dan dot plot untuk tiap kelas
colors = {"X-A": "#4a90d9", "X-B": "#e07b39", "X-C": "#5cb85c"}
fig, axes = plt.subplots(3, 3, figsize=(13, 10))
fig.suptitle("Analisis Kompetitif: Perbandingan Tiga Kelas", fontsize=12, fontweight="bold")
for row, (nama, data) in enumerate(kelas.items()):
q1, q3 = np.percentile(data, [25, 75])
iqr = q3 - q1
out_mask = (data < q1 - 1.5 * iqr) | (data > q3 + 1.5 * iqr)
# Kolom 0: Histogram
axes[row, 0].hist(data, bins=10, color=colors[nama], edgecolor="white", alpha=0.85)
axes[row, 0].axvline(np.mean(data), color="red", linestyle="--", linewidth=1.5,
label=f"Mean={np.mean(data):.1f}")
axes[row, 0].axvline(np.median(data), color="green", linestyle="-.", linewidth=1.5,
label=f"Median={np.median(data):.1f}")
axes[row, 0].set_title(f"Histogram {nama}")
axes[row, 0].legend(fontsize=7)
axes[row, 0].grid(axis="y", alpha=0.3)
# Kolom 1: Boxplot
axes[row, 1].boxplot(data, patch_artist=True,
boxprops=dict(facecolor=colors[nama], alpha=0.5))
axes[row, 1].set_title(f"Boxplot {nama}")
axes[row, 1].set_ylabel("Nilai")
axes[row, 1].grid(axis="y", alpha=0.3)
# Kolom 2: Dot plot dengan highlight outlier
jitter = np.random.uniform(-0.15, 0.15, len(data))
axes[row, 2].scatter(jitter, data, alpha=0.6, color=colors[nama], s=25)
if out_mask.any():
axes[row, 2].scatter(jitter[out_mask], data[out_mask],
color="red", s=60, zorder=5, label="Outlier")
axes[row, 2].legend(fontsize=7)
axes[row, 2].axhline(np.mean(data), color="black", linestyle="--", linewidth=1)
axes[row, 2].set_title(f"Dot Plot {nama}")
axes[row, 2].set_ylabel("Nilai")
axes[row, 2].set_xticks([])
axes[row, 2].grid(axis="y", alpha=0.3)
plt.tight_layout()
plt.savefig("perbandingan_kelas.png", dpi=120)
plt.show()
# Rekomendasi otomatis berdasarkan CV sebagai ukuran kemerataan
cv_kelas = {nama: np.std(d, ddof=1) / np.mean(d) * 100 for nama, d in kelas.items()}
paling_merata = min(cv_kelas, key=cv_kelas.get)
paling_tinggi = max(kelas, key=lambda k: np.mean(kelas[k]))
print(f"\nKelas paling merata (CV terkecil): {paling_merata} (CV={cv_kelas[paling_merata]:.1f}%)")
print(f"Kelas rata-rata tertinggi : {paling_tinggi} (mean={np.mean(kelas[paling_tinggi]):.1f})")