Label cluster dari Bagian 6 hanyalah angka 0, 1, 2 — dan Bu Rina tidak bisa menjual kopi kepada angka. Pada bagian ini kita mengubah label teknis itu menjadi segmen yang berwajah: profil angkanya disusun, menu favoritnya dibaca, lalu namanya diberikan lewat aturan yang berbasis angka supaya penamaan bisa diulang siapa pun. Bagian ini juga tempat pengetahuan domain Bu Rina masuk: angka mengusulkan pola, pengalaman pemilik toko yang memastikan pola itu masuk akal. Prinsip-prinsip pembacaannya dibahas pada chapter Interpretasi Hasil Clustering .

Profil Angka Tiap Cluster #

Profil dibangun dengan agregasi groupby — teknik yang sama dengan yang dipakai profil_segmen() pada src/segmentasi.py, sehingga angka di halaman ini dan di laporan akhir dijamin sejalan:

python
import pandas as pd
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler


def muat_bersih(path):
    """Muat data mentah pelanggan dan terapkan seluruh langkah pembersihan."""
    df = pd.read_csv(path)
    df = df.drop_duplicates().reset_index(drop=True)
    df["kunjungan_30hari"] = pd.to_numeric(df["kunjungan_30hari"], errors="coerce")
    df = df.dropna(subset=["kunjungan_30hari"]).reset_index(drop=True)
    df["kota"] = df["kota"].str.strip().str.title()
    df["kunjungan_30hari"] = df["kunjungan_30hari"].astype(int)
    return df


bersih = muat_bersih("../data/pelanggan.csv")
bersih["belanja_per_kunjungan"] = (
    bersih["belanja_ribu"] / bersih["kunjungan_30hari"]
)
fitur = bersih[["kunjungan_30hari", "belanja_per_kunjungan"]]

skala = StandardScaler().fit_transform(fitur)
label = KMeans(n_clusters=3, random_state=42, n_init="auto").fit_predict(skala)
bersih["cluster"] = label

profil = bersih.groupby("cluster").agg(
    jumlah=("pelanggan", "count"),
    kunjungan_rata=("kunjungan_30hari", "mean"),
    kunjungan_rentang=("kunjungan_30hari",
                       lambda s: f"{s.min()}-{s.max()}"),
    bpk_rata=("belanja_per_kunjungan", "mean"),
    bpk_rentang=("belanja_per_kunjungan",
                 lambda s: f"{s.min():.0f}-{s.max():.0f}"),
    total_rata=("belanja_ribu", "mean"),
)
print(profil.round(2).to_string())

Output yang Dihasilkan:

text
         jumlah  kunjungan_rata kunjungan_rentang  bpk_rata bpk_rentang  total_rata
cluster                                                                            
0            10           21.40             18-25     21.30       18-25      454.10
1             8            4.50               3-6    115.00      80-150      502.50
2            12           10.58              8-14     43.58       32-58      448.25

Tiga profil yang terbaca dari tabel tersebut:

  1. Cluster 0 — rajin datang, belanja kecil. Sepuluh pelanggan dengan 18-25 kunjungan sebulan (rata-rata 21,40 — nyaris setiap hari buka) dan belanja 18-25 ribu per kunjungan. Total bulanannya 454,10 ribu.
  2. Cluster 1 — jarang datang, belanja besar. Delapan pelanggan dengan cuma 3-6 kunjungan, tetapi 80-150 ribu terbelanja setiap kali datang. Totalnya justru tertinggi: 502,50 ribu.
  3. Cluster 2 — tengah-tengah. Dua belas pelanggan, kelompok terbesar, dengan kunjungan 8-14 kali dan belanja 32-58 ribu per kunjungan.

Perhatikan kembali kolom total_rata: 454,10; 502,50; 448,25 — nyaris sama. Inilah pembuktian angka atas firasat Bagian 1: kalau Bu Rina hanya melihat total belanja bulanan, ketiga segmen ini menyatu jadi satu kerumunan. Pembeda yang sesungguhnya adalah pasangan frekuensi kali nilai per kunjungan.

Warna dari Menu Favorit dan Anggota #

Angka membangun kerangka; menu favorit dan nama anggota memberi warna. Kita rangkum menu favorit tiap cluster — diurutkan dari yang terbanyak, seri dipecah secara alfabetis — beserta tiga contoh anggotanya:

python
import pandas as pd
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler


def muat_bersih(path):
    """Muat data mentah pelanggan dan terapkan seluruh langkah pembersihan."""
    df = pd.read_csv(path)
    df = df.drop_duplicates().reset_index(drop=True)
    df["kunjungan_30hari"] = pd.to_numeric(df["kunjungan_30hari"], errors="coerce")
    df = df.dropna(subset=["kunjungan_30hari"]).reset_index(drop=True)
    df["kota"] = df["kota"].str.strip().str.title()
    df["kunjungan_30hari"] = df["kunjungan_30hari"].astype(int)
    return df


bersih = muat_bersih("../data/pelanggan.csv")
bersih["belanja_per_kunjungan"] = (
    bersih["belanja_ribu"] / bersih["kunjungan_30hari"]
)
fitur = bersih[["kunjungan_30hari", "belanja_per_kunjungan"]]

skala = StandardScaler().fit_transform(fitur)
label = KMeans(n_clusters=3, random_state=42, n_init="auto").fit_predict(skala)
bersih["cluster"] = label

for c in range(3):
    anggota = bersih[bersih["cluster"] == c]
    hitung_menu = anggota["menu_favorit"].value_counts()
    urutan = sorted(hitung_menu.items(), key=lambda x: (-x[1], x[0]))
    teks_menu = ", ".join(f"{menu} ({n})" for menu, n in urutan)
    contoh = ", ".join(anggota["pelanggan"].head(3))
    print(f"cluster {c} ({len(anggota)} pelanggan)")
    print(f"  menu favorit  : {teks_menu}")
    print(f"  contoh anggota: {contoh}")

Output yang Dihasilkan:

text
cluster 0 (10 pelanggan)
  menu favorit  : kopi susu (6), americano (4)
  contoh anggota: Siti Rahmawati, Budi Santoso, Dewi Lestari
cluster 1 (8 pelanggan)
  menu favorit  : americano (3), kopi susu (2), teh tawar (2), matcha latte (1)
  contoh anggota: Bambang Sutrisno, Ratna Dewi, Tono Hartono
cluster 2 (12 pelanggan)
  menu favorit  : matcha latte (4), teh tawar (4), americano (2), kopi susu (2)
  contoh anggota: Andi Firmansyah, Nurul Aini, Dimas Prasetyo

Dua pengamatan dari warna tersebut:

  1. Menu favorit tidak memisahkan segmen. Kopi susu dan americano hadir di ketiga cluster; keduanya memang menu terlaris toko. Menu tidak masuk daftar fitur clustering, dan keluaran ini menegaskan keputusan itu tepat.
  2. Menu tetap memberi rasa khas. Cluster 2 satu-satunya yang didominasi matcha latte dan teh tawar — segmen menengah ini paling beragam selera. Informasi seperti ini membantu menyusun isi promo, meski bukan pembeda utama segmen.

Aturan Penamaan Berbasis Angka #

Nama segmen harus bisa dipertanggungjawabkan. Alih-alih menamai berdasarkan selera, src/segmentasi.py memakai aturan dua baris yang baca profil: kunjungan rata-rata 15 ke atas berarti pelanggan nyaris harian; selain itu, belanja per kunjungan 70 ribu ke atas berarti belanja sekali besar; sisanya adalah penengah. Kita jalankan aturan yang sama:

python
import pandas as pd
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler


def muat_bersih(path):
    """Muat data mentah pelanggan dan terapkan seluruh langkah pembersihan."""
    df = pd.read_csv(path)
    df = df.drop_duplicates().reset_index(drop=True)
    df["kunjungan_30hari"] = pd.to_numeric(df["kunjungan_30hari"], errors="coerce")
    df = df.dropna(subset=["kunjungan_30hari"]).reset_index(drop=True)
    df["kota"] = df["kota"].str.strip().str.title()
    df["kunjungan_30hari"] = df["kunjungan_30hari"].astype(int)
    return df


bersih = muat_bersih("../data/pelanggan.csv")
bersih["belanja_per_kunjungan"] = (
    bersih["belanja_ribu"] / bersih["kunjungan_30hari"]
)
fitur = bersih[["kunjungan_30hari", "belanja_per_kunjungan"]]

skala = StandardScaler().fit_transform(fitur)
label = KMeans(n_clusters=3, random_state=42, n_init="auto").fit_predict(skala)
bersih["cluster"] = label

profil = bersih.groupby("cluster").agg(
    jumlah=("pelanggan", "count"),
    kunjungan_rata=("kunjungan_30hari", "mean"),
    bpk_rata=("belanja_per_kunjungan", "mean"),
)


def namai(baris):
    """Beri nama segmen dari profil angkanya (aturan pipeline segmentasi.py)."""
    if baris["kunjungan_rata"] >= 15:
        return "Langganan Harian"
    if baris["bpk_rata"] >= 70:
        return "Pembeli Stok"
    return "Pengunjung Sesekali"


profil["segmen"] = profil.apply(namai, axis=1)
print("Hasil penamaan tiap cluster:")
print(profil.round(2).to_string())

Output yang Dihasilkan:

text
Hasil penamaan tiap cluster:
         jumlah  kunjungan_rata  bpk_rata               segmen
cluster                                                       
0            10           21.40     21.30     Langganan Harian
1             8            4.50    115.00         Pembeli Stok
2            12           10.58     43.58  Pengunjung Sesekali

Aturan sederhana itu menghasilkan pemetaan yang stabil: cluster 0 menjadi Langganan Harian, cluster 1 menjadi Pembeli Stok, dan cluster 2 menjadi Pengunjung Sesekali. Karena aturannya tertulis sebagai kode, siapa pun yang menjalankan ulang pipeline akan mendapat nama yang persis sama — berbeda dari penamaan yang lahir dari rapat.

Di titik inilah pengetahuan domain menguji interpretasi. Bu Rina membaca profil itu dan mengangguk: Langganan Harian adalah para pekerja kantor di sekitar toko yang berhenti tiap pagi; Pengunjung Sesekali adalah pelanggan sore hari; dan Pembeli Stok — pelanggan yang datang jarang tetapi membawa belanja besar — ia duga membawa pulang biji kopi untuk diseduh di rumah. Dugaan biji kopi itu masuk akal, tetapi perlu diingat kolom menu_favorit hanya mencatat menu minuman; karena itu laporan akhir akan menyebutnya sebagai dugaan, bukan fakta dari data.

Memasangkan Segmen dengan Ide Program #

Tiga segmen kini bernama, dan tiga ide program Bu Rina dari Bagian 1 akhirnya menemukan alamatnya:

SegmenIde program yang cocokDasar angka
Langganan Harian (10 orang, 33,3%)Kartu prabayar: deposit misal Rp 450.000 untuk 20 kali kunjunganRutinitas 21,4 kunjungan membuat paket deposit terasa hemat dan mengunci kebiasaan mereka
Pembeli Stok (8 orang, 26,7%)Paket biji kopi 2x200 gram atau langganan biji bulananBelanja 115 ribu per kunjungan jauh di atas segmen lain — tawarkan barang bernilai besar, bukan diskon gelas
Pengunjung Sesekali (12 orang, 40,0%)Penawaran akhir pekan Sabtu-MingguSegmen terbesar dengan kunjungan 10,6 kali sebulan; menambah satu-dua kunjungan saja sudah berdampak besar

Ketiga kecocokan itu lahir dari angka yang sama yang kita hitung sejak Bagian 4 — tidak ada satu pun klaim yang berdiri tanpa bilangan. Kini seluruh bahan laporan tersedia: masalah, data, metode, segmen, dan rekomendasi. Tersisa satu langkah merangkainya menjadi dokumen yang bisa dibaca sambil berdiri di balik kasir — kita kerjakan pada Bagian 9: Menyusun Laporan .