Stacked bar pada chapter sebelumnya menggabungkan dua variabel dalam satu batang: hari dan jenis minuman. Data nyata jarang berhenti di dua variabel — satu tabel menu bisa memuat harga, jumlah terjual, jenis, dan rating sekaligus — dan memaksakan semuanya ke satu grafik sederhana justru membuat temuan penting tersembunyi. Pada chapter ini kita memperluas kotak peralatan visualisasi ke lebih dari dua variabel: warna dan ukuran titik pada scatter plot, dua seri batang berdampingan, heatmap untuk matriks, serta panel kecil yang diulang per kategori.

Warna dan Ukuran Titik: Dua Variabel Tambahan pada Scatter Plot #

Scatter plot dasar memuat dua variabel: sumbu mendatar dan sumbu tegak. Dua saluran persepsi masih tersedia untuk memuat variabel lain — warna titik untuk kategori, dan besar titik untuk nilai numerik — sehingga satu bidang mampu memuat empat variabel sekaligus. Data latihan kita adalah rekap delapan menu Toko Kopi Kita:

python
import pandas as pd

menu = pd.DataFrame({
    "nama": ["es kopi susu", "kopi susu gula aren", "kopi tubruk",
             "cappuccino", "matcha latte", "es teh melati",
             "cokelat panas", "teh tawar"],
    "jenis": ["kopi", "kopi", "kopi", "kopi",
              "non-kopi", "non-kopi", "non-kopi", "non-kopi"],
    "harga": [22, 20, 12, 24, 24, 10, 20, 8],
    "terjual": [182, 121, 95, 88, 76, 64, 58, 42],
    "rating": [4.8, 4.7, 4.4, 4.6, 4.3, 4.0, 4.5, 3.9],
})

print(menu.to_string(index=False))
print()
print("Korelasi harga vs terjual:", round(menu["harga"].corr(menu["terjual"]), 2))

Output yang Dihasilkan:

text
               nama    jenis  harga  terjual  rating
       es kopi susu     kopi     22      182     4.8
kopi susu gula aren     kopi     20      121     4.7
        kopi tubruk     kopi     12       95     4.4
         cappuccino     kopi     24       88     4.6
       matcha latte non-kopi     24       76     4.3
      es teh melati non-kopi     10       64     4.0
      cokelat panas non-kopi     20       58     4.5
          teh tawar non-kopi      8       42     3.9

Korelasi harga vs terjual: 0.46

Mari kita telaah baris-barisnya:

  1. Baris 3-12 (pd.DataFrame({...})): delapan menu dengan lima kolom — bentuk “satu baris satu menu” yang siap digambar; pada analisis nyata tabel seperti ini adalah hasil agregasi groupby() dari tabel transaksi.
  2. Baris 14 (to_string(index=False)): tabel dicetak tanpa indeks agar lima kolomnya terbaca dalam lebar halaman.
  3. Baris 16 (corr()): korelasi harga dengan penjualan bernilai 0,46 — positif tetapi lemah, pertanda ada struktur lain yang mengatur penjualan selain harga.

Struktur lain itu terlihat begitu jenis minuman kita angkat sebagai warna dan rating sebagai ukuran titik:

python
import matplotlib.pyplot as plt
import pandas as pd

menu = pd.DataFrame({
    "nama": ["es kopi susu", "kopi susu gula aren", "kopi tubruk",
             "cappuccino", "matcha latte", "es teh melati",
             "cokelat panas", "teh tawar"],
    "jenis": ["kopi", "kopi", "kopi", "kopi",
              "non-kopi", "non-kopi", "non-kopi", "non-kopi"],
    "harga": [22, 20, 12, 24, 24, 10, 20, 8],
    "terjual": [182, 121, 95, 88, 76, 64, 58, 42],
    "rating": [4.8, 4.7, 4.4, 4.6, 4.3, 4.0, 4.5, 3.9],
})

warna = {"kopi": "tab:brown", "non-kopi": "tab:green"}

fig, ax = plt.subplots(figsize=(6, 4))
for jenis, bagian in menu.groupby("jenis"):
    ax.scatter(bagian["harga"], bagian["terjual"],
               s=(bagian["rating"] - 3.5) * 150,
               color=warna[jenis], label=jenis, alpha=0.8,
               edgecolor="white")

ax.set_title("Harga, Penjualan, dan Rating Delapan Menu")
ax.set_xlabel("Harga (ribu rupiah)")
ax.set_ylabel("Gelas terjual sepekan")
ax.legend(title="Jenis minuman")
plt.show()

Gambar yang muncul memuat delapan titik dalam dua warna: titik cokelat (minuman kopi) dan titik hijau (non-kopi). Kelompok cokelat menempati kiri atas — es kopi susu (harga 22, terjual 182) berdiri paling tinggi — sedangkan kelompok hijau berbaris di kanan bawah dengan penjualan di bawah 80 gelas. Korelasi harga-penjualan yang tadinya tampak lemah ternyata adalah dua pola berbeda: menu kopi murah laris, menu non-kopi mahal pun penjualannya rendah. Titik es kopi susu juga paling gemuk karena ratingnya tertinggi (4,8); teh tawar (3,9) menjadi titik terkecil.

Tiga keputusan teknisnya:

  1. Baris 18-22 (loop groupby("jenis")): titik digambar per kelompok jenis agar tiap kelompok mendapat warna sendiri sekaligus masuk ke legend secara otomatis lewat label=jenis.
  2. Baris 20 (s=(bagian["rating"] - 3.5) * 150): ukuran titik dihitung dari rating — 3,9 menghasilkan titik seluas 60 poin persegi, 4,8 menghasilkan 195; pengurangan 3,5 menjaga titik berrating rendah tetap terlihat, bukan nyaris nol.
  3. Baris 21-22 (alpha=0.8, edgecolor="white"): transparansi ringan dan garis tepi putih membuat titik yang berhimpit tetap terbaca batasnya.

Grouped Bar: Menyandingkan Dua Seri #

Ketika tiap kategori memiliki dua angka yang sejenis — pekan lalu dan pekan ini, dua cabang toko, target dan realisasi — grouped bar menaruh dua batang berdampingan dalam satu kategori. Perbandingan antar seri menjadi perbandingan panjang batang yang berdekatan, dan perbandingan antar kategori tetap terjaga karena pasangan batangnya sejajar:

python
import matplotlib.pyplot as plt
import numpy as np

menu = ["es kopi susu", "gula aren", "kopi tubruk", "matcha latte"]
pekan_lalu = [165, 108, 102, 84]
pekan_ini = [182, 121, 95, 76]

posisi = np.arange(len(menu))

fig, ax = plt.subplots(figsize=(6.5, 3.5))
ax.bar(posisi - 0.2, pekan_lalu, width=0.4, color="lightgray",
       label="pekan lalu")
ax.bar(posisi + 0.2, pekan_ini, width=0.4, color="tab:blue",
       label="pekan ini")
ax.set_xticks(posisi, menu)
ax.bar_label(ax.containers[0], padding=2)
ax.bar_label(ax.containers[1], padding=2)
ax.set_title("Empat Menu Terlaris: Pekan Lalu vs Pekan Ini")
ax.set_xlabel("Menu")
ax.set_ylabel("Gelas terjual")
ax.set_ylim(0, 205)
ax.legend()
plt.show()

Gambar yang dihasilkan memuat empat pasang batang: batang abu-abu (pekan lalu) dan batang biru (pekan ini) berdampingan. Tiga pasang pertama memperlihatkan es kopi susu naik dari 165 ke 182, gula aren naik dari 108 ke 121, dan kopi tubruk justru turun dari 102 ke 95; matcha latte turun dari 84 ke 76. Kenaikan dan penurunan terbaca dari pasangan batang yang bersebelahan, tanpa perlu memutar angka.

Mekanisme penggesernya berada pada tiga baris: np.arange(len(menu)) menghasilkan posisi [0, 1, 2, 3], lalu posisi - 0.2 dan posisi + 0.2 menggeser tiap seri ke kiri dan kanan setengah lebar batang (0,4), sehingga kedua batang menempel rapat tanpa bertumpuk. ax.set_xticks(posisi, menu) kemudian menuliskan nama menu tepat di tengah tiap pasangan.

Heatmap: Matriks Hari dan Jam #

Dua saluran sebelumnya menambahkan satu-dua variabel. Untuk matriks — dua kategori bersilangan dengan satu nilai di tiap selnya — heatmap mengambil pendekatan berbeda: nilai sel dipetakan ke warna, dan seluruh pola matriks terlihat sekaligus. Pertanyaan contohnya: kapan Toko Kopi Kita ramai, per hari dan per jam?

Bahan bakunya adalah tabel panjang satu-baris-satu-catatan, lalu pd.pivot_table() memutar tabel itu menjadi matriks hari × jam:

python
import pandas as pd

hari = ["Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu", "Minggu"]
jam = [7, 9, 11, 13, 15, 17, 19]
nilai = [
    16, 31, 24, 18, 20, 27, 12,   # Senin
    17, 32, 23, 19, 21, 28, 13,   # Selasa
    15, 30, 25, 18, 20, 26, 12,   # Rabu
    18, 33, 24, 20, 22, 29, 14,   # Kamis
    19, 34, 26, 21, 24, 33, 18,   # Jumat
    22, 36, 30, 26, 30, 41, 26,   # Sabtu
    23, 35, 31, 27, 31, 40, 24,   # Minggu
]

catatan = pd.DataFrame({
    "hari": [h for h in hari for _ in jam],
    "jam": jam * len(hari),
    "pelanggan": nilai,
})

ramai = pd.pivot_table(catatan, index="hari", columns="jam",
                       values="pelanggan")
ramai = ramai.reindex(hari).astype(int)

print(ramai.to_string())
print()
print("Jam tersibuk rata-rata:", ramai.mean().idxmax(), "sekitar",
      round(ramai.mean().max(), 1), "pelanggan")
print("Hari tersibuk         :", ramai.mean(axis=1).idxmax())

Output yang Dihasilkan:

text
jam     7   9   11  13  15  17  19
hari                              
Senin   16  31  24  18  20  27  12
Selasa  17  32  23  19  21  28  13
Rabu    15  30  25  18  20  26  12
Kamis   18  33  24  20  22  29  14
Jumat   19  34  26  21  24  33  18
Sabtu   22  36  30  26  30  41  26
Minggu  23  35  31  27  31  40  24

Jam tersibuk rata-rata: 9 sekitar 33.0 pelanggan
Hari tersibuk         : Sabtu

Mari kita telaah baris-barisnya:

  1. Baris 14-18 (pd.DataFrame({...})): tabel panjang 49 baris dibangun dengan pemahaman list — kolom hari mengulang tiap nama hari tujuh kali, kolom jam mengulang daftar jam tujuh kali, dan kolom pelanggan menampung 49 nilainya; bentuk panjang inilah format asli tabel catatan kasir.
  2. Baris 20-21 (pd.pivot_table(...)): tabel panjang diputar menjadi matriks — index="hari" menjadi baris, columns="jam" menjadi kolom, dan values="pelanggan" mengisi selnya.
  3. Baris 22 (reindex(hari).astype(int)): pivot_table mengurutkan hari secara alfabetis (Kamis, Minggu, Rabu, …) sehingga kita susun ulang mengikuti urutan kalender, lalu astype(int) mengembalikan tipe bilangan bulat karena hasil pivot berupa bilangan desimal.

Matriksnya siap; sekarang kita gambar dengan ax.imshow(), yang menerjemahkan tiap sel menjadi warna:

python
import matplotlib.pyplot as plt
import pandas as pd

hari = ["Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu", "Minggu"]
jam = [7, 9, 11, 13, 15, 17, 19]
nilai = [
    16, 31, 24, 18, 20, 27, 12,   # Senin
    17, 32, 23, 19, 21, 28, 13,   # Selasa
    15, 30, 25, 18, 20, 26, 12,   # Rabu
    18, 33, 24, 20, 22, 29, 14,   # Kamis
    19, 34, 26, 21, 24, 33, 18,   # Jumat
    22, 36, 30, 26, 30, 41, 26,   # Sabtu
    23, 35, 31, 27, 31, 40, 24,   # Minggu
]
catatan = pd.DataFrame({
    "hari": [h for h in hari for _ in jam],
    "jam": jam * len(hari),
    "pelanggan": nilai,
})
ramai = pd.pivot_table(catatan, index="hari", columns="jam",
                       values="pelanggan")
ramai = ramai.reindex(hari).astype(int)

fig, ax = plt.subplots(figsize=(6.5, 4))
gambar = ax.imshow(ramai.values, cmap="YlOrRd", aspect="auto")

ax.set_xticks(range(len(jam)), jam)
ax.set_yticks(range(len(hari)), hari)
ax.set_title("Pelanggan per Jam dan Hari")
ax.set_xlabel("Jam")
ax.set_ylabel("Hari")

for i in range(len(hari)):
    for j in range(len(jam)):
        ax.text(j, i, ramai.values[i, j], ha="center", va="center",
                fontsize=9)

fig.colorbar(gambar, ax=ax, label="pelanggan")
plt.show()

Gambar yang muncul berupa kisi 7×7 yang diisi warna kuning-merah: makin pekat, makin ramai. Dua kolom pekat terlihat jelas — kolom jam 09 (puncak pagi, terpekat pada Sabtu dengan 36) dan kolom jam 17 (puncak sore) — dengan lembah terang di kolom jam 13 (siang) dan baris Senin yang paling kuning sebagai hari paling sepi. Angka di tiap sel memungkinkan pembacaan nilai persis, sedangkan warna memperlihatkan bentuk besarnya: dua puncak harian yang menanjak menjelang akhir pekan.

Perhatikan tiga baris kuncinya: ax.imshow(ramai.values, ...) menggambar matriks sel utuh dari larik NumPy-nya; loop ganda ax.text(j, i, ...) menuliskan angka di pusat tiap sel dengan koordinat (kolom, baris) — kebalikan dari kebiasaan (baris, kolom); dan fig.colorbar(gambar, ...) menambahkan batang warna agar intensitas warna punya skala yang bisa dibaca.

Small Multiples: Satu Grafik per Panel #

Ketika jumlah kombinasinya makin banyak, warna dan ukuran cepat kehabisan daya muat. Pola small multiples mengambil jalan lain: satu jenis grafik diulang di beberapa panel kecil, satu panel per kategori, dengan sumbu yang sama. Mata pembaca cukup belajar membaca satu panel, lalu membandingkan antar panel:

python
import matplotlib.pyplot as plt
import pandas as pd

menu = pd.DataFrame({
    "nama": ["es kopi susu", "kopi susu gula aren", "kopi tubruk",
             "cappuccino", "matcha latte", "es teh melati",
             "cokelat panas", "teh tawar"],
    "jenis": ["kopi", "kopi", "kopi", "kopi",
              "non-kopi", "non-kopi", "non-kopi", "non-kopi"],
    "harga": [22, 20, 12, 24, 24, 10, 20, 8],
    "terjual": [182, 121, 95, 88, 76, 64, 58, 42],
    "rating": [4.8, 4.7, 4.4, 4.6, 4.3, 4.0, 4.5, 3.9],
})

fig, panel = plt.subplots(1, 2, figsize=(10, 3.5), sharey=True)

for ax, (jenis, bagian) in zip(panel, menu.groupby("jenis")):
    ax.bar(bagian["nama"], bagian["terjual"], color="tab:blue")
    ax.bar_label(ax.containers[0], padding=2)
    ax.set_title(f"minuman {jenis}")
    ax.set_xlabel("Menu")
    ax.tick_params(axis="x", labelrotation=20)

panel[0].set_ylabel("Gelas terjual sepekan")
fig.suptitle("Penjualan Menu per Jenis Minuman")
fig.tight_layout()
plt.show()

Gambar yang dihasilkan memuat dua panel berbagi sumbu tegak: panel kiri “minuman kopi” berisi empat batang dengan es kopi susu tertinggi (182) dan cappuccino terendah (88); panel kanan “minuman non-kopi” berisi empat batang yang seluruhnya di bawah 80 gelas, dipimpin matcha latte (76) dan diakhiri teh tawar (42). Karena sumbu tegaknya dibagi (sharey=True), perbandingan lintas panel sah secara visual: batang tertinggi di panel kanan pun masih di bawah setengah batang es kopi susu.

Tiga baris pentingnya: plt.subplots(1, 2, ..., sharey=True) menyiapkan dua bidang berdampingan dengan satu sumbu tegak bersama; zip(panel, menu.groupby("jenis")) memasangkan tiap bidang dengan satu kelompok jenis; dan fig.suptitle() memberi judul induk yang meliputi kedua panel, karena judul tiap panel hanya menjelaskan bagiannya sendiri.

Rangkuman #

  • Scatter plot memuat empat variabel sekaligus: dua sumbu, warna titik untuk kategori, dan besar titik (s=) untuk nilai numerik — pesan utama tetap dibawa posisi, bukan besar titik.
  • Grouped bar menyandingkan dua seri pada kategori yang sama lewat posisi batang yang digeser (posisi - 0.2 dan posisi + 0.2); legend menjadi wajib begitu seri lebih dari satu.
  • pd.pivot_table() memutar tabel panjang menjadi matriks; ax.imshow() menggambar matriks itu sebagai warna, ditambah angka per sel dan fig.colorbar() sebagai skalanya.
  • Small multiples mengulang satu grafik per kategori pada panel-panel dengan sumbu bersama — solusi paling tahan banting saat kombinasinya banyak.

Teknik-teknik di atas memperlihatkan pola ramai dan sepi toko kita lewat matriks hari-jam. Heatmap tadi memetakan dua kategori bersilangan; namun untuk memeriksa satu variabel numerik secara saksama, dua grafik tertua masih menjadi standar praktiknya. Kita bedah keduanya pada chapter berikutnya, Histogram dan Boxplot.

Referensi #

  1. Matplotlib — Plot Types — referensi resmi scatter plot (parameter s= dan warna) serta bar chart.
  2. Pandas — DataFrame.pivot_table — dokumentasi resmi pivot tabel sebagai bahan baku heatmap.
  3. Matplotlib — pyplot API Summary — ringkasan fungsi pyplot, termasuk imshow dan colorbar.