Histogram dan boxplot memeriksa satu variabel; pertanyaan berikutnya yang hampir selalu menyusul adalah “apakah dua variabel numerik bergerak bersama?”. Scatter plot menjawabnya dengan satu titik per pengamatan, dan pada chapter Grafik Hubungan kita sudah membaca arah, bentuk, dan kekuatannya. Kini kita hadapi kondisi lapangannya: ratusan titik yang saling menutup, kategori yang perlu ikut tampil, dan satu pencilan yang mampu mengungkit keseluruhan kesimpulan. Pada chapter ini kita bedah scatter plot dari data 60 pelanggan Toko Kopi Kita — durasi kunjungan lawan nilai belanja — lengkap dengan jebakan pembacaannya.

Data 60 Pelanggan: Durasi dan Belanja #

Dugaan yang mau kita uji: pelanggan yang duduk lebih lama membelanja lebih besar. Datanya berupa 60 pengamatan — 40 pelanggan hari kerja dan 20 pelanggan akhir pekan — dengan durasi yang sengaja dibulatkan ke kelipatan lima menit, seperti yang lazim dicatat sistem kasir:

python
import numpy as np
import pandas as pd

rng = np.random.default_rng(42)
durasi_kerja = (rng.normal(32, 8, 40) / 5).round() * 5
durasi_akhir = (rng.normal(45, 10, 20) / 5).round() * 5
durasi = np.concatenate([durasi_kerja, durasi_akhir]).astype(int)
nilai = (durasi * 1.2 + rng.normal(0, 10, 60)).round().astype(int)
nilai = np.clip(nilai, 8, None)   # nilai belanja minimal 8 ribu
jenis = ["hari kerja"] * 40 + ["akhir pekan"] * 20

pelanggan = pd.DataFrame({"durasi": durasi, "nilai": nilai, "jenis": jenis})

print(pelanggan.head(8).to_string(index=False))
print()
print("Jumlah pelanggan            :", len(pelanggan))
print("Rentang durasi              :", pelanggan["durasi"].min(), "-",
      pelanggan["durasi"].max(), "menit")
print("Rentang nilai               :", pelanggan["nilai"].min(), "-",
      pelanggan["nilai"].max(), "ribu rupiah")
print("Korelasi durasi vs nilai (r):",
      round(pelanggan["durasi"].corr(pelanggan["nilai"]), 2))
ganda = pelanggan.duplicated(subset=["durasi", "nilai"]).sum()
print("Titik dengan posisi sama    :", ganda, "dari", len(pelanggan))

Output yang Dihasilkan:

text
 durasi  nilai      jenis
     35     25 hari kerja
     25     27 hari kerja
     40     50 hari kerja
     40     54 hari kerja
     15     25 hari kerja
     20     32 hari kerja
     35     39 hari kerja
     30     31 hari kerja

Jumlah pelanggan            : 60
Rentang durasi              : 15 - 60 menit
Rentang nilai               : 20 - 73 ribu rupiah
Korelasi durasi vs nilai (r): 0.82
Titik dengan posisi sama    : 8 dari 60

Mari kita telaah baris-barisnya:

  1. Baris 5-6 (dua populasi): 40 durasi hari kerja diambil dari sebaran berpusat 32 menit dan 20 durasi akhir pekan dari sebaran berpusat 45 menit — dua perilaku berbeda dalam satu tabel, seed 42 menjaga keduanya dapat direproduksi.
  2. Baris 7-9: nilai belanja dihitung mengikuti durasi (1,2 ribu per menit) plus derau, lalu dipotong bawah di 8 ribu; ketergantungan inilah yang akan tampak sebagai pola menanjak.
  3. Baris 12 (pd.DataFrame): tiga kolom — dua numerik untuk sumbu, satu kategorikal untuk warna pada bagian berikutnya.
  4. Baris 20 (duplicated(...).sum()): menghitung banyaknya baris dengan pasangan durasi-nilai yang persis sama — delapan dari 60 titik akan mendarat di posisi yang identik, bahan persoalan kita berikutnya.

Overplotting: Saat Titik Saling Menutup #

Satu titik yang bertumpuk persis di atas titik lain tidak menambah tinta apa pun — keduanya tampak sebagai satu titik. Pada data 60 pelanggan, delapan posisi dobel berarti delapan pengamatan berikutnya tidak terlihat; pada data ribuan, separuh titik bisa saja tidak terbaca. Solusinya transparansi: parameter alpha menurunkan kepekatan warna sehingga titik tunggal tampak pucat dan tumpukan titik tampak pekat — kepadatan menjadi terlihat:

python
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

rng = np.random.default_rng(42)
durasi_kerja = (rng.normal(32, 8, 40) / 5).round() * 5
durasi_akhir = (rng.normal(45, 10, 20) / 5).round() * 5
durasi = np.concatenate([durasi_kerja, durasi_akhir]).astype(int)
nilai = (durasi * 1.2 + rng.normal(0, 10, 60)).round().astype(int)
nilai = np.clip(nilai, 8, None)

fig, (kiri, kanan) = plt.subplots(1, 2, figsize=(9, 4),
                                  sharex=True, sharey=True)
kiri.scatter(durasi, nilai, s=90, color="tab:blue")
kiri.set_title("Tanpa transparansi")
kiri.set_xlabel("Durasi kunjungan (menit)")
kiri.set_ylabel("Nilai belanja (ribu rupiah)")
kanan.scatter(durasi, nilai, s=90, color="tab:blue", alpha=0.35,
              edgecolor="tab:blue")
kanan.set_title("alpha=0.35")
kanan.set_xlabel("Durasi kunjungan (menit)")
plt.show()

Kedua panel memuat titik-titik yang sama dan keduanya memperlihatkan pola menanjak dari kiri bawah (15 menit, sekitar 25 ribu) ke kanan atas (60 menit, sekitar 70 ribu). Bedanya ada pada kepadatan: panel kiri menampilkan titik-titik biru pekat yang seragam, sehingga posisi (40, 50) yang menampung tiga pelanggan tampak sama dengan posisi yang hanya dihuni satu pelanggan. Panel kanan menampakkan warna lebih tua pada posisi-posisi bertumpuk — pita titik pekat terlihat pada rentang durasi 30-50 — sementara titik tunggal tampil pucat; jumlah pelanggan per posisi kini ikut terbaca.

Mekanisme alpha sederhana: warna tiap titik dicampur dengan latar belakang sebesar 1 - alpha, dan tumpukan titik semi transparan menumpuk campurannya sehingga makin banyak titik di satu titik posisi, makin pekat warnanya. Dua pelengkapnya: edgecolor="tab:blue" memberi garis tepi pekat agar titik pucat tetap berbatas jelas, dan figsize=(9, 4) memberi ruang lebar — pada scatter plot, mempersempit bidang gambar sama saja dengan memaksa titik saling bertabrakan.

Warna per Jenis Hari #

Pola menanjak itu milik siapa — pelanggan hari kerja, akhir pekan, atau keduanya? Pertanyaan semacam ini dijawab dengan mengangkat kolom kategorikal sebagai warna. seaborn menyelesaikannya dalam satu panggilan dengan argumen hue:

python
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns

rng = np.random.default_rng(42)
durasi_kerja = (rng.normal(32, 8, 40) / 5).round() * 5
durasi_akhir = (rng.normal(45, 10, 20) / 5).round() * 5
durasi = np.concatenate([durasi_kerja, durasi_akhir]).astype(int)
nilai = (durasi * 1.2 + rng.normal(0, 10, 60)).round().astype(int)
nilai = np.clip(nilai, 8, None)
jenis = ["hari kerja"] * 40 + ["akhir pekan"] * 20

pelanggan = pd.DataFrame({"durasi": durasi, "nilai": nilai, "jenis": jenis})

sns.scatterplot(data=pelanggan, x="durasi", y="nilai", hue="jenis",
                alpha=0.6, s=70)
plt.title("Durasi Kunjungan vs Nilai Belanja")
plt.xlabel("Durasi kunjungan (menit)")
plt.ylabel("Nilai belanja (ribu rupiah)")
plt.show()

Gambar yang muncul memuat awan titik dengan dua warna yang terbagi rapi secara mendatar: titik biru (hari kerja) memadati separuh kiri mulai dari 15 sampai sekitar 50 menit, sedangkan titik oranye (akhir pekan) berkumpul di separuh kanan pada rentang 35-60 menit. Keduanya menanjak searah — garis imajiner tiap kelompok sama-sama menanjak — jadi hubungan durasi-belanja berlaku pada kedua jenis hari; yang membedakan hanyalah di mana kelompoknya berada. Legend warna muncul otomatis di sisi kanan karena seaborn membaca kolom hue sekaligus mengelola pemetaannya — pekerjaan yang pada chapter Grafik Hubungan kita lakukan manual dengan loop dan dictionary warna.

Pencilan yang Mengungkit Kesimpulan #

Satu titik di ujung berpengaruh lebih besar daripada kelihatannya. Koefisien korelasi memakai jarak kuadrat, sehingga satu titik yang jauh dari pusat data menyumbang pengaruh berlipat. Kita uji dengan menambahkan satu pelanggan rapat komunitas yang duduk 150 menit dan membelanja 320 ribu:

python
import numpy as np
import pandas as pd

rng = np.random.default_rng(42)
durasi_kerja = (rng.normal(32, 8, 40) / 5).round() * 5
durasi_akhir = (rng.normal(45, 10, 20) / 5).round() * 5
durasi = np.concatenate([durasi_kerja, durasi_akhir]).astype(int)
nilai = (durasi * 1.2 + rng.normal(0, 10, 60)).round().astype(int)
nilai = np.clip(nilai, 8, None)

pelanggan = pd.DataFrame({"durasi": durasi, "nilai": nilai})
rapat = pd.DataFrame({"durasi": [150], "nilai": [320]})
ada_rapat = pd.concat([pelanggan, rapat], ignore_index=True)

print("60 pelanggan biasa   -> r =",
      round(pelanggan["durasi"].corr(pelanggan["nilai"]), 2))
print("61 pelanggan + rapat -> r =",
      round(ada_rapat["durasi"].corr(ada_rapat["nilai"]), 2))
print("Rata-rata biasa      :", round(pelanggan["nilai"].mean(), 1),
      "ribu rupiah")
print("Rata-rata dengan rapat:", round(ada_rapat["nilai"].mean(), 1),
      "ribu rupiah")

Output yang Dihasilkan:

text
60 pelanggan biasa   -> r = 0.82
61 pelanggan + rapat -> r = 0.94
Rata-rata biasa      : 42.2 ribu rupiah
Rata-rata dengan rapat: 46.7 ribu rupiah

Satu pelanggan mengangkat korelasi dari 0,82 menjadi 0,94 — padahal hubungan durasi-belanja di antara 60 pelanggan biasanya tidak berubah sama sekali. Rata-rata nilai belanja juga tersentak dari 42,2 ke 46,7 ribu hanya oleh satu baris data. Kesimpulan “makin lama duduk makin besar belanja, dan hubungannya sangat kuat” tiba-tiba tampak kokoh, padahal kokohnya datang dari satu pengamatan yang berasal dari populasi berbeda — pelanggan rapat, bukan pelanggan umum.

Mari kita telaah baris-barisnya:

  1. Baris 12 (rapat): satu baris DataFrame baru berisi durasi 150 dan nilai 320 — ditempatkan terpisah agar jelas bahwa ia bukan bagian dari data utama.
  2. Baris 13 (pd.concat(..., ignore_index=True)): kedua tabel digabung menjadi 61 baris; ignore_index=True menyusun ulang indeks 0-60 agar rapi.
  3. Baris 15-18: dua korelasi dan dua rata-rata dicetak berdampingan agar selisihnya terbaca langsung.

Pola yang Perlu Diwaspadai #

Selain pencilan, ada dua bentuk yang sering membuat pembacaan scatter plot keliru. Pertama, hubungan melengkung: korelasi Pearson hanya mengukur pola lurus, sehingga data berbentuk lengkung penuh (misalnya durasi dan rasa nyaman — naik lalu turun) bisa menghasilkan r mendekati nol; titik-titiknya tetap harus dilihat sebagai gambar. Kedua, kelompok terpisah: dua gundukan titik yang masing-masing menanjak bisa menghasilkan r besar yang sebenarnya berasal dari jarak antar kelompok, bukan dari hubungan di dalam kelompok — seperti dua warna pada data kita yang terpisah mendatar. Kedua kasus ini menegaskan aturan dari chapter Grafik Hubungan: gambar dulu, angka rangkuman menyusul.

Rangkuman #

  • Scatter plot menempatkan satu pengamatan sebagai satu titik pada dua sumbu numerik; pembacaannya tetap arah, bentuk, dan kekuatan, kini ditambah kepadatan.
  • Overplotting menyembunyikan pengamatan: alpha 0,2-0,4, ukuran s kecil, figsize lapang, dan jitter untuk data diskret membuat tumpukan terbaca.
  • Argumen hue pada sns.scatterplot() mengangkat kolom kategorikal menjadi warna lengkap dengan legend otomatis.
  • Satu pencilan dapat mengungkit korelasi dan rata-rata (0,82 → 0,94 pada contoh kita); periksa asal-usulnya sebelum menyimpulkan.
  • Hubungan melengkung dan kelompok terpisah adalah dua pola yang membuat angka korelasi berbohong — selalu lihat gambarnya.

Sepanjang Section ini kita terus memanggil fig, ax, plt.subplots, dan kawan-kawannya tanpa membedahnya. Seluruh grafik yang kita buat sejauh ini berdiri di atas satu library: matplotlib. Saatnya kita buka dapurnya sendiri pada chapter berikutnya, Visualisasi dengan Matplotlib.

Referensi #

  1. Seaborn — scatterplot — dokumentasi resmi scatter plot seaborn, termasuk hue, size, dan alpha.
  2. Matplotlib — Plot Types — referensi resmi scatter plot dan parameter tampilannya.
  3. Matplotlib — User’s Guide — panduan resmi matplotlib, termasuk pengaturan transparansi dan ukuran marker.