Tabel angka pada Bagian 7 sudah menjawab kelima pertanyaan, tetapi membaca tabel membutuhkan usaha — apalagi bagi pembaca seperti Bu Rina yang sibuk melayani pelanggan. Grafik menerjemahkan angka menjadi bentuk yang bisa dipahami dalam hitungan detik: perbandingan menjadi panjang batang, ritme waktu menjadi naik-turun garis. Pada bagian ini kita membuat tiga grafik untuk laporan akhir, masing-masing dengan jenis yang dipilih mengikuti kaidah chapter Memilih Jenis Grafik . Seluruhnya dibuat dengan matplotlib langsung dari data bersih, sehingga grafik ikut terjamin konsisten dengan angka-angka sebelumnya.

Grafik Batang: Pendapatan per Menu #

Pertanyaan P1 membandingkan empat kategori — dan aturan praktisnya jelas: perbandingan antar kategori memakai grafik batang, bukan grafik garis (garis hanya untuk urutan waktu). Kita urutkan batang dari yang tertinggi supaya hirarki menunya terlihat sekilas:

python
import matplotlib.pyplot as plt
import pandas as pd


def muat_bersih(path):
    """Muat data mentah dan terapkan pembersihan hasil Bagian 5."""
    df = pd.read_csv(path).drop_duplicates().reset_index(drop=True)
    df["menu"] = df["menu"].str.lower()
    df["gelas"] = pd.to_numeric(df["gelas"].replace("-", pd.NA))
    valid = df[df["gelas"].notna() & (df["pendapatan_ribu"] > 0)]
    harga = (valid["pendapatan_ribu"] / valid["gelas"]).groupby(valid["menu"]).median()
    nol = df["pendapatan_ribu"] == 0
    df.loc[nol, "pendapatan_ribu"] = (
        df.loc[nol, "gelas"] * df.loc[nol, "menu"].map(harga)
    ).astype(int)
    kosong = df["gelas"].isna()
    df.loc[kosong, "gelas"] = (
        df.loc[kosong, "pendapatan_ribu"] / df.loc[kosong, "menu"].map(harga)
    )
    df["gelas"] = df["gelas"].astype(int)
    return df


bersih = muat_bersih("../data/penjualan_kotor.csv")
per_menu = bersih.groupby("menu")["pendapatan_ribu"].sum().sort_values(ascending=False)

fig, ax = plt.subplots(figsize=(7, 4))
batang = ax.bar(per_menu.index, per_menu.values, color="#6f4e37")
ax.bar_label(batang, fmt="%d", padding=3)
ax.set_title("Total Pendapatan per Menu\nToko Kopi Kita, 5-16 Januari 2026")
ax.set_xlabel("Menu")
ax.set_ylabel("Pendapatan (ribu rupiah)")
ax.set_ylim(0, per_menu.max() * 1.15)
ax.grid(axis="y", alpha=0.3)
fig.tight_layout()
plt.show()

Grafik yang terbentuk memperlihatkan empat batang cokelat yang tingginya menurun dari kiri ke kanan: kopi susu menonjol paling jauh (10.098), disusul matcha latte (7.275), americano (4.356), dan teh tawar yang paling pendek (1.920). Perbedaan panjang batang pertama dan terakhir itulah pesan utamanya — jawaban P1 terlihat tanpa membaca satu angka pun. Mari kita bedah kodenya:

  1. .sort_values(ascending=False): pengurutan dilakukan sebelum menggambar, sehingga batang otomatis tersusun dari tertinggi. Pembaca tidak perlu menyusun sendiri mana yang menang.
  2. ax.bar(...) lalu ax.bar_label(...): batang digambar dari per_menu, lalu bar_label menempelkan nilai di atas tiap ujung batang (padding=3 memberi jarak). Angka pada grafik membuat klaim “42,7%” bisa diperiksa tanpa kembali ke tabel.
  3. ax.set_ylim(0, ...): sumbu tegak dipaksa mulai dari nol dan diberi ruang 15% di atas. Kedua hal ini menjaga perbandingan panjang batang tetap jujur dan label tidak terpotong.
  4. ax.grid(axis="y", alpha=0.3): garis bantu horizontal tipis memudahkan pelacakan nilai tanpa bersaing dengan batangnya.

Grafik Garis: Ritme Pendapatan Harian #

Pertanyaan P2 sampai P4 membaca data berurutan waktu, dan untuk itu grafik garis adalah pilihan tepat: mata kita mengikuti naik-turunnya sebagai satu cerita. Kita gambar pendapatan 12 hari sebagai garis bertanda, menandai dua hari akhir pekan dengan titik jingga, menambahkan garis putus-putus rata-rata, dan memberi keterangan pada satu-satunya anomali:

python
import matplotlib.pyplot as plt
import pandas as pd


def muat_bersih(path):
    """Muat data mentah dan terapkan pembersihan hasil Bagian 5."""
    df = pd.read_csv(path).drop_duplicates().reset_index(drop=True)
    df["menu"] = df["menu"].str.lower()
    df["gelas"] = pd.to_numeric(df["gelas"].replace("-", pd.NA))
    valid = df[df["gelas"].notna() & (df["pendapatan_ribu"] > 0)]
    harga = (valid["pendapatan_ribu"] / valid["gelas"]).groupby(valid["menu"]).median()
    nol = df["pendapatan_ribu"] == 0
    df.loc[nol, "pendapatan_ribu"] = (
        df.loc[nol, "gelas"] * df.loc[nol, "menu"].map(harga)
    ).astype(int)
    kosong = df["gelas"].isna()
    df.loc[kosong, "gelas"] = (
        df.loc[kosong, "pendapatan_ribu"] / df.loc[kosong, "menu"].map(harga)
    )
    df["gelas"] = df["gelas"].astype(int)
    return df


bersih = muat_bersih("../data/penjualan_kotor.csv")
AKHIR_PEKAN = ("2026-01-10", "2026-01-11")

per_hari = bersih.groupby("tanggal")["pendapatan_ribu"].sum()
pekan = per_hari[per_hari.index.isin(AKHIR_PEKAN)]

fig, ax = plt.subplots(figsize=(8, 4.5))
ax.plot(per_hari.index, per_hari.values, marker="o", color="#2a6f4e",
        label="pendapatan harian")
ax.plot(pekan.index, pekan.values, "o", color="#c07a3a", markersize=10,
        label="akhir pekan", zorder=3)
ax.axhline(per_hari.mean(), color="#888888", linestyle="--", linewidth=1,
           label=f"rata-rata {per_hari.mean():.0f} ribu")
posisi = list(per_hari.index).index("2026-01-14")
ax.annotate("Rabu 14 Januari: 903 ribu",
            xy=(posisi, 903), xytext=(posisi - 3.8, 1250),
            arrowprops=dict(arrowstyle="->", color="#555555"))
ax.set_title("Pendapatan Harian Toko Kopi Kita, 5-16 Januari 2026")
ax.set_xlabel("Tanggal")
ax.set_ylabel("Pendapatan (ribu rupiah)")
ax.tick_params(axis="x", rotation=45)
ax.legend(loc="lower left")
fig.tight_layout()
plt.show()

Bentuk yang muncul adalah cerita 12 hari yang utuh: garis hijau berjalan datar di kisaran 1.500-2.000 pada hari kerja, menanjak tajam ke dua puncak berturut (titik jingga Sabtu 2.743 dan Minggu 2.915), lalu jatuh dalam pada Rabu 14 Januari hingga menjadi titik terendah 903 yang ditunjuk panah berlabel. Garis putus-putus abu-abu di tengah (rata-rata 1.971 ribu) membuat posisi tiap hari terbaca sekilas: hari kerja umumnya di sekitarnya, akhir pekan jauh di atasnya, anomali jauh di bawahnya. Beberapa hal penting dari kodenya:

  1. Dua panggilan plot pada satu sumbu: garis hijau menggambar seluruh hari; titik jingga menggambar ulang dua hari akhir pekan di posisi yang sama. Parameter zorder=3 memastikan titik jingga tampil di atas garis, bukan tertutup.
  2. axhline untuk pembanding: garis rata-rata memberi konteks tanpa penjelasan tambahan — pembaca langsung tahu hari mana di atas dan di bawah normal.
  3. annotate dengan panah: posisi sumbu kategorik dihitung lewat list(per_hari.index).index("2026-01-14") (menghasilkan 9), lalu teks ditempatkan agak ke kiri dan di atas dengan panah menunjuk titiknya. Satu anomali cukup diberi label; menandai semuanya justru mengaburkan.
  4. rotation=45: dua belas tanggal miring supaya tidak bertumpuk — label tanggal ISO memang panjang untuk lebar gambar 8 inci.

Grafik Batang: Hari Kerja vs Akhir Pekan #

Jawaban P3 berupa satu angka rasio 1,57 — angka itu paling mudah dikomunikasikan lewat dua batang saja. Grafik kecil ini biasanya dipakai sebagai penutup bagian temuan karena ringkas dan langsung:

python
import matplotlib.pyplot as plt
import pandas as pd


def muat_bersih(path):
    """Muat data mentah dan terapkan pembersihan hasil Bagian 5."""
    df = pd.read_csv(path).drop_duplicates().reset_index(drop=True)
    df["menu"] = df["menu"].str.lower()
    df["gelas"] = pd.to_numeric(df["gelas"].replace("-", pd.NA))
    valid = df[df["gelas"].notna() & (df["pendapatan_ribu"] > 0)]
    harga = (valid["pendapatan_ribu"] / valid["gelas"]).groupby(valid["menu"]).median()
    nol = df["pendapatan_ribu"] == 0
    df.loc[nol, "pendapatan_ribu"] = (
        df.loc[nol, "gelas"] * df.loc[nol, "menu"].map(harga)
    ).astype(int)
    kosong = df["gelas"].isna()
    df.loc[kosong, "gelas"] = (
        df.loc[kosong, "pendapatan_ribu"] / df.loc[kosong, "menu"].map(harga)
    )
    df["gelas"] = df["gelas"].astype(int)
    return df


bersih = muat_bersih("../data/penjualan_kotor.csv")
AKHIR_PEKAN = ("2026-01-10", "2026-01-11")

per_hari = bersih.groupby("tanggal")["pendapatan_ribu"].sum().reset_index()
kelompok = per_hari["tanggal"].isin(AKHIR_PEKAN).map(
    {True: "akhir pekan", False: "hari kerja"}
)
rata_rata = (
    per_hari.groupby(kelompok)["pendapatan_ribu"]
    .mean()
    .reindex(["hari kerja", "akhir pekan"])
)

fig, ax = plt.subplots(figsize=(5.5, 4))
batang = ax.bar(rata_rata.index, rata_rata.values, color=["#4a7c59", "#c07a3a"])
ax.bar_label(batang, fmt="%.1f", padding=3)
ax.set_title("Rata-rata Pendapatan Harian\nHari Kerja vs Akhir Pekan")
ax.set_xlabel("Kelompok hari")
ax.set_ylabel("Rata-rata pendapatan (ribu rupiah)")
ax.set_ylim(0, rata_rata.max() * 1.15)
ax.grid(axis="y", alpha=0.3)
fig.tight_layout()
plt.show()

Hasilnya dua batang: hijau “hari kerja” setinggi 1.799,1 dan jingga “akhir pekan” setinggi 2.829,0 — batang jingga lebih dari satu setengah kali batang hijau, tepat memvisualkan rasio 1,57 dari Bagian 7. Teknik pada kode ini satu langkah lebih maju dari grafik pertama: kolom boolean hasil isin() dipetakan menjadi label lewat .map({True: "akhir pekan", False: "hari kerja"}), lalu groupby menghitung rata-rata per label dan .reindex(...) menjamin urutan batang tetap “hari kerja” dahulu baru “akhir pekan” — bukan urutan alfabetis yang kebetulan.

Tiga grafik ini menutup kebutuhan visual laporan akhir: satu untuk struktur pendapatan, satu untuk ritme harian, satu untuk perbandingan kelompok hari. Angka, tabel, dan grafik kini berbicara tentang cerita yang sama. Tinggal satu langkah yang menentukan apakah cerita itu sampai ke pembacanya: merangkai temuan menjadi kalimat yang tertata — kita kerjakan pada Bagian 9: Menyusun Temuan .