Setiap analisis pelanggan dimulai dari kebiasaan yang sama: muat data, periksa kualitasnya, dan pahami distribusinya sebelum satu pun model disentuh. Pada proyek segmentasi, langkah ini memikul beban tambahan — tidak ada kolom label yang bisa dirata-ratakan per segmen, sehingga seluruh intuisi tentang “pelanggan seperti apa yang ada” harus dibangun murni dari distribusi fitur perilaku. Pada bagian ini kita bangkitkan dataset RFM 300 pelanggan ritel online, memverifikasi kebersihan datanya, lalu mengukur kemiringan distribusi, konsentrasi pendapatan, dan korelasi antar fitur — tiga temuan yang akan menentukan seluruh keputusan preprocessing di bagian berikutnya.

Membangkitkan Dataset Pelanggan RFM #

Proyek ini memakai data sintetis demi reproduksibilitas penuh: satu generator np.random.default_rng(42) menentukan seluruh nilai, sehingga siapa pun yang menjalankan skrip mendapat berkas CSV identik bit demi bit. Skrip menyusun empat kelompok perilaku laten yang wajar pada ritel online — pelanggan besar yang loyal, pembeli hemat yang jarang datang, pelanggan baru, dan pelanggan yang mulai mengendur — lalu menarik fitur tiap anggota dari distribusi normal kelompoknya dengan simpangan yang cukup lebar agar tepi kelompok tumpang tindih.

Satu keputusan desain menjadi kunci cerita unsupervised: kolom kelompok laten tidak pernah ditulis ke CSV. Pembangkit data mengetahui keanggotaan segmen, tetapi berkas yang dibaca analisis tidak menyimpannya — persis seperti data pelanggan sungguhan yang tidak hadir dengan label bawaan. Skrip tetap mencetak komposisi kelompok laten sebagai referensi pemeriksaan.

python
# Bangkitkan dataset pelanggan ritel online sintetis untuk proyek
# segmentasi. Deterministik penuh: satu generator rng berseed 42.
# Jalankan dari root proyek: python3 src/01_bangun_data.py
# Keluaran: data/pelanggan.csv (300 baris, 4 fitur RFM).
#
# Empat kelompok laten dibangun dari perilaku belanja yang berbeda:
#   1. loyal-besar  : sering belanja, nilai besar, transaksi terbaru.
#   2. hemat-jarang : jarang belanja, nilai kecil, lama tidak kembali.
#   3. baru         : baru bergabung, frekuensi masih rendah, aktif.
#   4. mengendur    : dulu aktif, kini jarang kembali.
# Kelompok laten TIDAK disimpan ke CSV - inilah kondisi unsupervised:
# model clustering harus menemukannya sendiri dari empat fitur RFM.

import os

import numpy as np
import pandas as pd

SEED = 42
N = 300
KOLOM = [
    "total_belanja_ribu",
    "frekuensi_pesanan",
    "hari_sejak_transaksi",
    "rata_belanja_ribu",
]

# Parameter tiap kelompok laten: (proporsi, mean/std frekuensi,
# mean/std rata belanja, mean/std hari sejak transaksi).
# Std yang lebar sengaja dipilih agar kelompok tumpang tindih di tepi:
# clustering tetap bermakna tanpa menjadi steril.
KELOMPOK = [
    ("loyal-besar", 0.24, (32.0, 5.5), (430.0, 65.0), (9.0, 4.0)),
    ("hemat-jarang", 0.30, (3.2, 1.1), (240.0, 50.0), (85.0, 24.0)),
    ("baru", 0.26, (2.5, 1.0), (500.0, 110.0), (6.0, 3.0)),
    ("mengendur", 0.20, (13.5, 3.2), (360.0, 65.0), (62.0, 16.0)),
]

rng = np.random.default_rng(SEED)

# ---------- Bangkitkan tiap kelompok laten ----------
# Jumlah anggota tiap kelompok diundi sekali lewat multinomial, lalu
# fitur tiap anggota ditarik dari distribusi normal kelompoknya.
proporsi = [k[1] for k in KELOMPOK]
jumlah_anggota = rng.multinomial(N, proporsi)

frekuensi = []
rata = []
hari = []
kelompok_asal = []
for (nama, _p, (mu_f, sd_f), (mu_r, sd_r), (mu_h, sd_h)), n in zip(
    KELOMPOK, jumlah_anggota
):
    f = np.clip(np.round(rng.normal(mu_f, sd_f, size=n)), 1, 48).astype(int)
    r = np.clip(np.round(rng.normal(mu_r, sd_r, size=n)), 120, 700)
    h = np.clip(np.round(rng.normal(mu_h, sd_h, size=n)), 1, 160).astype(int)
    frekuensi.append(f)
    rata.append(r)
    hari.append(h)
    kelompok_asal.extend([nama] * n)

frekuensi = np.concatenate(frekuensi)
rata_belanja = np.concatenate(rata)
hari_sejak = np.concatenate(hari)

# Total belanja = frekuensi x rata belanja, persis seperti agregasi
# tabel transaksi menjadi fitur RFM.
total_belanja = (frekuensi * rata_belanja).astype(int)

pelanggan = pd.DataFrame(
    {
        "total_belanja_ribu": total_belanja,
        "frekuensi_pesanan": frekuensi,
        "hari_sejak_transaksi": hari_sejak,
        "rata_belanja_ribu": rata_belanja,
    }
)

# Acak urutan baris agar CSV tidak terkelompok menurut segmen laten.
urutan = rng.permutation(N)
pelanggan = pelanggan.iloc[urutan].reset_index(drop=True)
kelompok_asal = [kelompok_asal[i] for i in urutan]

os.makedirs("data", exist_ok=True)
pelanggan.to_csv("data/pelanggan.csv", index=False)

# ---------- Ringkasan untuk pemeriksaan ----------
print("== Ringkasan pembangkitan data ==")
print(f"jumlah baris : {len(pelanggan)}")
print(f"jumlah kolom : {pelanggan.shape[1]}")
print(f"kolom        : {list(pelanggan.columns)}")
print(f"berkas       : data/pelanggan.csv")

print("\n== Komposisi kelompok laten (tidak disimpan di CSV) ==")
print(f"{'kelompok':14s} {'n':>4s} {'frek':>6s} {'rata':>7s} {'hari':>6s}")
for nama, _p, (mu_f, _), (mu_r, _), (mu_h, _) in KELOMPOK:
    mask = np.array([k == nama for k in kelompok_asal])
    sub = pelanggan[mask]
    print(
        f"{nama:14s} {int(mask.sum()):4d} {sub['frekuensi_pesanan'].mean():6.1f}"
        f" {sub['rata_belanja_ribu'].mean():7.1f}"
        f" {sub['hari_sejak_transaksi'].mean():6.1f}"
    )

print("\n== Statistik keempat fitur (seluruh data) ==")
for kolom in KOLOM:
    s = pelanggan[kolom]
    print(
        f"{kolom:22s}: min {s.min():7.1f}  rata-rata {s.mean():8.1f}"
        f"  maks {s.max():8.1f}"
    )

print("\n== Lima baris pertama ==")
print(pelanggan.head(5).to_string(index=False))

Output yang Dihasilkan:

text
== Ringkasan pembangkitan data ==
jumlah baris : 300
jumlah kolom : 4
kolom        : ['total_belanja_ribu', 'frekuensi_pesanan', 'hari_sejak_transaksi', 'rata_belanja_ribu']
berkas       : data/pelanggan.csv

== Komposisi kelompok laten (tidak disimpan di CSV) ==
kelompok          n   frek    rata   hari
loyal-besar      78   32.2   421.4    8.8
hemat-jarang     77    3.4   242.1   83.4
baru             84    2.5   486.5    5.6
mengendur        61   14.1   359.3   57.8

== Statistik keempat fitur (seluruh data) ==
total_belanja_ribu    : min   197.0  rata-rata   5091.7  maks  21340.0
frekuensi_pesanan     : min     1.0  rata-rata     12.8  maks     44.0
hari_sejak_transaksi  : min     1.0  rata-rata     37.0  maks    135.0
rata_belanja_ribu     : min   124.0  rata-rata    381.0  maks    700.0

== Lima baris pertama ==
 total_belanja_ribu  frekuensi_pesanan  hari_sejak_transaksi  rata_belanja_ribu
               1020                  3                     5              340.0
                894                  3                    90              298.0
               2316                  4                     4              579.0
               5598                 18                    79              311.0
              13930                 35                     1              398.0

Mari kita bedah skrip pembangkit data baris-per-baris:

  1. Baris 19–20 (SEED = 42, N = 300): konstanta ukuran dataset. Seed dipakai satu kali di baris 39 untuk membangun generator; tidak ada sumber acak lain di seluruh skrip.
  2. Baris 32–37 (KELOMPOK): tabel parameter empat kelompok laten — proporsi, pasangan mean/std frekuensi, rata belanja, dan hari sejak transaksi. Proporsi 0,24/0,30/0,26/0,20 menghasilkan komposisi 78/77/84/61 pelanggan pada eksekusi berseed 42.
  3. Baris 44–45 (rng.multinomial): jumlah anggota tiap kelompok diundi sekali dari distribusi multinomial.
  4. Baris 51–60 (loop per kelompok): tiap fitur ditarik dari rng.normal lalu dipangkas np.clip ke rentang wajar — frekuensi 1 sampai 48 pesanan, rata belanja Rp120.000 sampai Rp700.000, recency 1 sampai 160 hari.
  5. Baris 68 (total_belanja): total belanja dihitung sebagai frekuensi × rata belanja, meniru agregasi tabel transaksi; ketergantungan ini nanti terlihat pada korelasi 0,974 di skrip eksplorasi.
  6. Baris 80–82 (rng.permutation): urutan baris diacak agar CSV tidak terkelompok menurut segmen laten — pembacaan head(5) pun tetap mewakili campuran populasi.
  7. Baris 94–103 (komposisi laten): skrip mencetak rata-rata fitur tiap kelompok sebagai referensi pemeriksaan, tetapi kolom tersebut tidak ikut ditulis ke CSV.

Memeriksa Kualitas dan Distribusi Perilaku #

Dataset yang baru dibangkitkan atau baru diterima dari pihak lain wajib melewati gerbang kualitas yang sama: dimensi tabel, nilai hilang, baris ganda, dan tipe kolom. Setelah gerbang itu terlewati, eksplorasi bergerak ke pertanyaan substantif RFM: seberapa miring distribusinya, seberapa terkonsentrasi pendapatannya, dan bagaimana profil pelanggan bila dikelompokkan menurut recency serta frekuensi.

python
# Eksplorasi data pelanggan: muat data/pelanggan.csv, periksa kualitasnya,
# lalu kenali distribusi fitur RFM, konsentrasi pendapatan, dan korelasi
# antar fitur. Semua angka dihitung dari berkas CSV hasil
# src/01_bangun_data.py.
# Jalankan dari root proyek: python3 src/02_eksplorasi.py

import pandas as pd

df = pd.read_csv("data/pelanggan.csv")
FITUR = [
    "total_belanja_ribu",
    "frekuensi_pesanan",
    "hari_sejak_transaksi",
    "rata_belanja_ribu",
]

# ---------- Kualitas data ----------
print("== Muat data ==")
print(f"bentuk tabel : {df.shape[0]} baris x {df.shape[1]} kolom")
print(f"nilai hilang : {int(df.isna().sum().sum())}")
print(f"baris ganda : {int(df.duplicated().sum())}")
print("\n== Tipe kolom ==")
for nama, tipe in df.dtypes.items():
    print(f"{nama:22s}: {str(tipe)}")

# ---------- Statistik dan kemiringan fitur ----------
print("\n== Statistik fitur RFM ==")
print(f"{'fitur':22s} {'min':>8s} {'rata-rata':>9s} {'maks':>8s} {'skewness':>9s}")
for kolom in FITUR:
    s = df[kolom]
    print(
        f"{kolom:22s} {s.min():8.1f} {s.mean():9.1f} {s.max():8.1f}"
        f" {s.skew():9.3f}"
    )

# ---------- Konsentrasi pendapatan ----------
total_semua = df["total_belanja_ribu"].sum()
urut_besar = df.sort_values("total_belanja_ribu", ascending=False)
print("\n== Konsentrasi pendapatan (total_belanja_ribu) ==")
for label, n in [("10% teratas", 30), ("25% teratas", 75), ("50% teratas", 150)]:
    pangsa = urut_besar["total_belanja_ribu"].head(n).sum() / total_semua
    print(f"{label:12s}: {pangsa:.1%} dari seluruh pendapatan")

# ---------- Pelanggan per kelompok recency dan frekuensi ----------
def ringkas_segmen(segmen, urutan, label_segmen):
    print(f"\n== Profil per {label_segmen} ==")
    print(f"{'segmen':10s} {'n':>4s} {'total':>9s} {'frek':>6s} {'rata':>7s}")
    for seg in urutan:
        sub = df[segmen == seg]
        if len(sub) == 0:
            continue
        print(
            f"{str(seg):10s} {len(sub):4d} {sub['total_belanja_ribu'].mean():9.1f}"
            f" {sub['frekuensi_pesanan'].mean():6.1f}"
            f" {sub['rata_belanja_ribu'].mean():7.1f}"
        )

segmen_hari = pd.cut(
    df["hari_sejak_transaksi"],
    bins=[-1, 30, 60, 160],
    labels=["0-30", "31-60", "61-160"],
)
ringkas_segmen(segmen_hari, ["0-30", "31-60", "61-160"], "recency (hari)")

segmen_frek = pd.cut(
    df["frekuensi_pesanan"],
    bins=[0, 4, 14, 48],
    labels=["1-4", "5-14", "15+"],
)
ringkas_segmen(segmen_frek, ["1-4", "5-14", "15+"], "frekuensi pesanan")

# ---------- Korelasi antar fitur ----------
print("\n== Korelasi Pearson antar fitur ==")
print(df.corr(numeric_only=True).round(3).to_string())

Output yang Dihasilkan:

text
== Muat data ==
bentuk tabel : 300 baris x 4 kolom
nilai hilang : 0
baris ganda : 0

== Tipe kolom ==
total_belanja_ribu    : int64
frekuensi_pesanan     : int64
hari_sejak_transaksi  : int64
rata_belanja_ribu     : float64

== Statistik fitur RFM ==
fitur                       min rata-rata     maks  skewness
total_belanja_ribu        197.0    5091.7  21340.0     1.016
frekuensi_pesanan           1.0      12.8     44.0     0.819
hari_sejak_transaksi        1.0      37.0    135.0     0.725
rata_belanja_ribu         124.0     381.0    700.0     0.320

== Konsentrasi pendapatan (total_belanja_ribu) ==
10% teratas : 31.5% dari seluruh pendapatan
25% teratas : 67.2% dari seluruh pendapatan
50% teratas : 90.8% dari seluruh pendapatan

== Profil per recency (hari) ==
segmen        n     total   frek    rata
0-30        164    7092.0   16.7   453.3
31-60        43    3821.1   10.9   326.5
61-160       93    2151.7    6.8   278.8

== Profil per frekuensi pesanan ==
segmen        n     total   frek    rata
1-4         146     985.2    2.7   380.1
5-14         48    3467.1    9.8   338.5
15+         106   11483.4   28.1   401.5

== Korelasi Pearson antar fitur ==
                      total_belanja_ribu  frekuensi_pesanan  hari_sejak_transaksi  rata_belanja_ribu
total_belanja_ribu                 1.000              0.974                -0.397              0.269
frekuensi_pesanan                  0.974              1.000                -0.347              0.140
hari_sejak_transaksi              -0.397             -0.347                 1.000             -0.666
rata_belanja_ribu                  0.269              0.140                -0.666              1.000

Bedah kode skrip eksplorasi:

  1. Baris 9 (pd.read_csv): seluruh angka bagian ini dihitung dari berkas CSV — bukan dari array di memori skrip pembangkit — sehingga eksplorasi dan pembangkitan data terpisah bersih.
  2. Baris 20–21 (isna dan duplicated): dua gerbang kualitas. Nilai hilang dan baris ganda sama-sama nol; data sintetis memang bersih, tetapi kebiasaan memeriksanya tetap wajib karena data produksi tidak pernah sebersih ini.
  3. Baris 29–34 (s.skew()): kemiringan tiap fitur dicetak berdampingan dengan statistik ringkas. Angka inilah temuan paling menentukan bagian ini: total_belanja_ribu berskewness 1,016 — miring kanan — sementara tiga fitur lain di bawah 0,9.
  4. Baris 37–42 (konsentrasi pendapatan): data diurutkan menurun menurut total belanja, lalu pangsa pendapatan kelompok puncak dihitung. Hasilnya 25% pelanggan teratas menyumbang 67,2% pendapatan — pola Pareto yang menjadikan segmentasi layak dikerjakan.
  5. Baris 45–56 (fungsi ringkas_segmen): satu fungsi dipakai ulang untuk dua segmentasi berbeda; pemeriksaan len(sub) == 0 menjaga fungsi tetap hidup saat sebuah segmen kosong.
  6. Baris 58–70 (pd.cut): recency dibelah menjadi 0–30, 31–60, 61–160 hari dan frekuensi menjadi 1–4, 5–14, 15+ pesanan. Batas dipilih sebelum melihat hasil agar tidak tergoda menyetelnya.
  7. Baris 74 (df.corr): matriks korelasi Pearson 4×4 menutup eksplorasi dengan peta ketergantungan antar fitur.

Temuan yang Membentuk Keputusan Berikutnya #

Empat temuan dari output di atas akan menuntun seluruh proyek. Tabel berikut merangkum profil pelanggan menurut recency dan frekuensi:

KelompokJumlah pelangganTotal belanja rata-rataFrekuensi rata-rata
Recency 0–30 hari164Rp7.092.00016,7
Recency 31–60 hari43Rp3.821.00010,9
Recency 61–160 hari93Rp2.152.0006,8
Frekuensi 1–4 pesanan146Rp985.0002,7
Frekuensi 5–14 pesanan48Rp3.467.0009,8
Frekuensi 15+ pesanan106Rp11.483.00028,1

Pertama, pendapatan terkonsentrasi tajam: 10% pelanggan teratas menyumbang 31,5% pendapatan dan 25% teratas menyumbang 67,2%. Kedua, total_belanja_ribu miring kanan (skewness 1,016) dengan rentang 197 hingga 21.340 — ekor panjang pelanggan besar inilah alasan transformasi log pada Bagian 2. Ketiga, korelasi total_belanja_ribu dengan frekuensi_pesanan mencapai 0,974, konsekuensi wajar total = frekuensi × rata belanja; sementara hari_sejak_transaksi berkorelasi −0,666 dengan rata_belanja_ribu — pelanggan yang lama tak kembali cenderung bernilai pesanan kecil. Keempat, simpangan total_belanja_ribu (sekitar 5.100) ratusan kali simpangan frekuensi_pesanan (sekitar 12) — perbedaan skala yang akan mendominasi jarak Euclidean bila tidak distandarkan.

Data sudah ada dan karakternya dikenal: bersih, miring pada dimensi uang, terkonsentrasi pada segelintir pelanggan, dan berskala timpang antar fitur. Langkah berikutnya adalah menyiapkan mesinnya — transformasi log1p pada fitur miring dan StandardScaler pada seluruh fitur — lalu membuktikan bahwa KMeans pada data mentah dan data terskala menghasilkan segmen berbeda sama sekali. Semua itu dikerjakan pada Bagian 2: Preprocessing dan Scaling.

Referensi Resmi #