Tabel jawaban pada Bagian 8 sudah lengkap, tetapi menyampaikannya kepada pembaca laporan lewat tabel angka membutuhkan usaha — grafik menerjemahkannya dalam sekali pandang. Pada bagian ini kita membuat tiga grafik untuk laporan akhir: scatter plot jam belajar versus nilai yang juga memeriksa klaim linearitas dari Bagian 7, grafik batang rata-rata kedua kelas, dan scatter plot bertambah garis regresi. Ketiganya mengikuti kaidah yang pernah kita pelajari pada chapter Scatter Plot . Sesuai kontrak proyek, blok plot cukup dijalankan sampai tuntas tanpa keluaran teks, sehingga bentuk grafiknya kita deskripsikan dengan prosa.

Scatter Plot: Jam Belajar versus Nilai per Kelas #

Grafik pertama memvisualkan jawaban P1 sekaligus P3: sumbu mendatar memuat jam belajar, sumbu tegak memuat nilai, dan warna titik membedakan kelas:

python
import matplotlib.pyplot as plt
import pandas as pd


def muat_bersih(path):
    """Muat data mentah dan terapkan seluruh langkah pembersihan."""
    df = pd.read_csv(path).drop_duplicates().reset_index(drop=True)
    df["kelas"] = df["kelas"].str.upper()
    df["kehadiran_persen"] = pd.to_numeric(df["kehadiran_persen"].replace("-", pd.NA))
    df = df.dropna(subset=["kehadiran_persen"]).reset_index(drop=True)
    df["kehadiran_persen"] = df["kehadiran_persen"].astype(int)
    return df


bersih = muat_bersih("../data/nilai_siswa.csv")

fig, ax = plt.subplots(figsize=(7, 4.5))
for kelas, warna in (("A", "#2a6f4e"), ("B", "#c07a3a")):
    kelompok = bersih[bersih["kelas"] == kelas]
    ax.scatter(kelompok["jam_belajar_mingguan"], kelompok["nilai_ujian"],
               color=warna, s=70, alpha=0.8, label=f"kelas {kelas}")
ax.set_title("Jam Belajar Mingguan vs Nilai Ujian\n30 siswa kelas A dan B")
ax.set_xlabel("Jam belajar per minggu")
ax.set_ylabel("Nilai ujian")
ax.grid(alpha=0.3)
ax.legend()
fig.tight_layout()
plt.show()

Bentuk yang muncul adalah jawaban visual P1: tiga puluh titik membentuk pita yang menanjak dari kiri bawah ke kanan atas — siswa yang belajar sedikit menghuni ujung bawah (Zaki di pojok 2 jam bernilai 48, Elsa 3 jam bernilai 44), dan perpindahan ke kanan hampir selalu diikuti kenaikan ke atas hingga Nia di pojok 16 jam bernilai 91. Pandangan yang sama sekaligus menjawab P3: titik hijau (kelas A) dan jingga (kelas B) bercampur sepanjang pita tanpa garis batas — tidak ada wilayah yang dikuasai satu kelas, versi gambar dari p = 0,44 pada Bagian 8. Beberapa hal dari kodenya:

  1. Perulangan atas pasangan ("A", hijau) dan ("B", jingga) menggambar dua seri pada satu sumbu; label pada tiap pemanggilan scatter mengisi legend secara otomatis, sehingga keterangan kelas tidak perlu ditulis manual.
  2. s=70 memperbesar ukuran penanda dan alpha=0.8 sedikit membuatnya tembus pandang — dengan hanya 30 titik, ukuran penala standar terlihat kecil pada kanvas selebar ini.
  3. Titik-titik yang berjalan searah pita juga membenarkan klaim linearitas saat memilih korelasi Pearson pada Bagian 7: tidak tampak pola melengkung yang berarti, jadi garis lurus adalah bentuk yang wajar.

Grafik Batang: Rata-rata Nilai per Kelas #

Jawaban P3 berupa satu p-value; perbandingannya paling jujur ditampilkan lewat dua batang dengan sumbu penuh. Grafik kecil ini menjadi penutup bagian temuan karena ringkas dan lugas:

python
import matplotlib.pyplot as plt
import pandas as pd


def muat_bersih(path):
    """Muat data mentah dan terapkan seluruh langkah pembersihan."""
    df = pd.read_csv(path).drop_duplicates().reset_index(drop=True)
    df["kelas"] = df["kelas"].str.upper()
    df["kehadiran_persen"] = pd.to_numeric(df["kehadiran_persen"].replace("-", pd.NA))
    df = df.dropna(subset=["kehadiran_persen"]).reset_index(drop=True)
    df["kehadiran_persen"] = df["kehadiran_persen"].astype(int)
    return df


bersih = muat_bersih("../data/nilai_siswa.csv")

rata_kelas = bersih.groupby("kelas")["nilai_ujian"].mean()
rata_total = bersih["nilai_ujian"].mean()

fig, ax = plt.subplots(figsize=(5.5, 4))
batang = ax.bar(rata_kelas.index, rata_kelas.values, color=["#2a6f4e", "#c07a3a"])
ax.bar_label(batang, fmt="%.1f", padding=3)
ax.axhline(rata_total, color="#888888", linestyle="--", linewidth=1,
           label=f"rata-rata seluruh siswa {rata_total:.1f}")
ax.set_title("Rata-rata Nilai Ujian per Kelas")
ax.set_xlabel("Kelas")
ax.set_ylabel("Rata-rata nilai ujian")
ax.set_ylim(0, 100)
ax.grid(axis="y", alpha=0.3)
ax.legend()
fig.tight_layout()
plt.show()

Gambar yang terbentuk menampilkan dua batang yang nyaris setinggi: kelas A di 70,6 dan kelas B di 66,9 pada skala penuh 0 sampai 100, dengan garis putus-putus rata-rata keseluruhan 68,7 berdiri persis di antara keduanya. Jarak puncak kedua batang tampak kecil dibanding tinggi batangnya — dan itulah pesan jujurnya: selisih 3,7 poin pada sebaran setiap kelas yang mencapai belasan poin memang bukan hal yang mencolok, sejalan dengan kesimpulan uji-t yang tidak menemukan perbedaan bermakna. Dua keputusan pada kodenya menopang kejujuran itu:

  1. ax.set_ylim(0, 100) mengunci sumbu tegak pada skala nilai ujian yang sesungguhnya, bukan rentang sempit di sekitar kedua rata-rata.
  2. bar_label menempelkan angka tepat di ujung batang sehingga pembaca yang ingin tahu selisihnya tidak perlu mengira-gira dari garis.

Scatter Plot dengan Garis Regresi #

Grafik ketiga menggabungkan jawaban P1 dan P4 dalam satu gambar: sebaran titik seperti grafik pertama, kini dengan garis hasil regresi Bagian 8 menindihnya:

python
import matplotlib.pyplot as plt
import pandas as pd
from scipy import stats


def muat_bersih(path):
    """Muat data mentah dan terapkan seluruh langkah pembersihan."""
    df = pd.read_csv(path).drop_duplicates().reset_index(drop=True)
    df["kelas"] = df["kelas"].str.upper()
    df["kehadiran_persen"] = pd.to_numeric(df["kehadiran_persen"].replace("-", pd.NA))
    df = df.dropna(subset=["kehadiran_persen"]).reset_index(drop=True)
    df["kehadiran_persen"] = df["kehadiran_persen"].astype(int)
    return df


bersih = muat_bersih("../data/nilai_siswa.csv")

hasil = stats.linregress(bersih["jam_belajar_mingguan"], bersih["nilai_ujian"])
rentang = range(int(bersih["jam_belajar_mingguan"].min()),
                int(bersih["jam_belajar_mingguan"].max()) + 1)
garis = [hasil.intercept + hasil.slope * jam for jam in rentang]

fig, ax = plt.subplots(figsize=(7, 4.5))
ax.scatter(bersih["jam_belajar_mingguan"], bersih["nilai_ujian"],
           color="#4a5d7a", s=70, alpha=0.8, label="siswa")
ax.plot(rentang, garis, color="#b03a2e", linewidth=2,
        label=f"garis regresi: {hasil.slope:.2f} x jam + {hasil.intercept:.1f}")
ax.set_title("Garis Regresi Nilai Ujian atas Jam Belajar\n(R-kuadrat = "
             f"{hasil.rvalue ** 2:.2f})")
ax.set_xlabel("Jam belajar per minggu")
ax.set_ylabel("Nilai ujian")
ax.grid(alpha=0.3)
ax.legend()
fig.tight_layout()
plt.show()

Garis merah miring membelah awan titik dari kiri bawah ke kanan atas dengan kemiringan yang persis dibaca legend: nilai = 2,88 × jam + 42,1. Titik-titik biru kebanyakan berhimpit di sekitar garis — hanya satu-dua siswa menjauh, misalnya Maya yang hadir sangat rajin tetapi bernilai 72 di bawah garis — itulah wujud dari R-kuadrat 0,88 pada judul grafik. Perhatikan dua keputusan pentingnya:

  1. Garis hanya digambar pada rentang 2 sampai 16 jam — range dari jam minimum sampai maksimum data. Garis tidak diperpanjang ke nol atau ke 25 jam, karena di luar rentang itu kita tidak punya satu titik data pun untuk menopangnya; memanjangkannya sama dengan menampilkan ekstrapolasi seolah-olah fakta.
  2. Persamaan dan R-kuadrat ditulis pada judul serta legend, bukan disimpan di teks terpisah — pembaca grafik selalu bertanya “seberapa pas garisnya”, dan judul menjawabnya tanpa berpindah halaman.

Tiga grafik ini melengkapi tabel Bagian 8: satu menampilkan sebaran beserta warna kelas, satu membandingkan kelompok dengan jujur, satu merangkai model perkiraan. Angka dan gambar kini bercerita tentang hal yang sama; tinggal menentukan kalimat apa yang boleh diucapkan dari cerita itu — pekerjaan Bagian 10: Menginterpretasikan Hasil .