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.
# 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:
== 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.0Mari kita bedah skrip pembangkit data baris-per-baris:
- 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. - 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. - Baris 44–45 (
rng.multinomial): jumlah anggota tiap kelompok diundi sekali dari distribusi multinomial. - Baris 51–60 (loop per kelompok): tiap fitur ditarik dari
rng.normallalu dipangkasnp.clipke rentang wajar — frekuensi 1 sampai 48 pesanan, rata belanja Rp120.000 sampai Rp700.000, recency 1 sampai 160 hari. - 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. - Baris 80–82 (
rng.permutation): urutan baris diacak agar CSV tidak terkelompok menurut segmen laten — pembacaanhead(5)pun tetap mewakili campuran populasi. - 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.
# 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:
== 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.000Bedah kode skrip eksplorasi:
- 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. - Baris 20–21 (
isnadanduplicated): 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. - Baris 29–34 (
s.skew()): kemiringan tiap fitur dicetak berdampingan dengan statistik ringkas. Angka inilah temuan paling menentukan bagian ini:total_belanja_ribuberskewness 1,016 — miring kanan — sementara tiga fitur lain di bawah 0,9. - 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.
- Baris 45–56 (fungsi
ringkas_segmen): satu fungsi dipakai ulang untuk dua segmentasi berbeda; pemeriksaanlen(sub) == 0menjaga fungsi tetap hidup saat sebuah segmen kosong. - 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. - 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:
| Kelompok | Jumlah pelanggan | Total belanja rata-rata | Frekuensi rata-rata |
|---|---|---|---|
| Recency 0–30 hari | 164 | Rp7.092.000 | 16,7 |
| Recency 31–60 hari | 43 | Rp3.821.000 | 10,9 |
| Recency 61–160 hari | 93 | Rp2.152.000 | 6,8 |
| Frekuensi 1–4 pesanan | 146 | Rp985.000 | 2,7 |
| Frekuensi 5–14 pesanan | 48 | Rp3.467.000 | 9,8 |
| Frekuensi 15+ pesanan | 106 | Rp11.483.000 | 28,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 #
- pandas.DataFrame.skew — API — mengukur kemiringan distribusi tiap kolom.
- pandas.cut — API — segmentasi kolom numerik menjadi kelompok berlabel.
- pandas.DataFrame.corr — API — matriks korelasi Pearson antar fitur.