Promosi yang sama untuk semua pelanggan adalah pemborosan: pelanggan yang tiap pekan datang butuh perlakuan yang berbeda dari pelanggan yang tiga bulan tak muncul. Segmentasi memecah masalah itu dengan membagi pelanggan ke dalam kelompok yang polanya mirip, sehingga tiap kelompok bisa disasarkan dengan tindakan yang sesuai. Pada chapter ini kita membangun segmentasi pelanggan Toko Kopi Kita dari jejak transaksinya dengan kerangka RFM — recency, frequency, monetary — lalu memberi skor lewat pd.cut dan pd.qcut, menyusun segmen dengan aturan yang bisa kita jelaskan, dan memprofil tiap segmen sebelum akhirnya membandingkan cara ini dengan clustering.

Konsep RFM: Tiga Pertanyaan tentang Setiap Pelanggan #

RFM meringkas perilaku belanja setiap pelanggan menjadi tiga angka yang menjawab tiga pertanyaan:

DimensiPertanyaanCara hitung dari data transaksi
RecencySudah berapa hari sejak transaksi terakhirnya?batas analisis dikurangi tanggal transaksi terakhir
FrequencySudah berapa kali pelanggan bertransaksi?banyaknya baris transaksi atas namanya
MonetarySudah berapa besar total belanjanya?penjumlahan nilai transaksinya

Ketiganya dipilih karena saling melengkapi. Recency menangkap kehangatan hubungan — pelanggan yang baru datang kemarin masih dekat di ingatan. Frequency menangkap kebiasaan — pelanggan yang sudah dua puluh kali datang punya pola yang berbeda dari yang baru sekali. Monetary menangkap nilai — pelanggan dengan total belanja besar layak dilayani lebih. Pelanggan yang bagus di ketiga dimensi sekaligus adalah aset; yang buruk di ketiganya perlu perhatian berbeda; dan yang mencampur — misalnya dulu besar tapi lama tak kembali — justru sering paling menarik untuk diselidiki.

Dari Transaksi Menjadi Tabel RFM #

Bahan kita adalah catatan 40 transaksi dari 10 pelanggan selama hampir tiga bulan, tersimpan sebagai berkas CSV pada bundle chapter ini:

csv
tanggal,pelanggan,total
2025-10-19,tono,44
2025-10-20,tono,46
2025-10-21,tono,43
2025-10-22,tono,47
2025-10-24,lina,26
2025-10-26,agus,40
2025-10-30,joko,28
2025-11-03,budi,52
2025-11-05,lina,24
2025-11-06,agus,44
2025-11-07,dewi,38
2025-11-10,siti,45
2025-11-11,joko,32
2025-11-12,rani,35
2025-11-17,agus,39
2025-11-18,budi,55
2025-11-20,joko,30
2025-11-21,dewi,42
2025-11-24,siti,50
2025-11-26,rani,33
2025-11-28,agus,45
2025-11-29,mira,36
2025-12-01,budi,48
2025-12-05,dewi,40
2025-12-08,siti,47
2025-12-10,rani,38
2025-12-11,mira,40
2025-12-14,budi,60
2025-12-16,dewi,39
2025-12-18,mira,38
2025-12-19,siti,52
2025-12-22,rani,34
2025-12-27,budi,51
2025-12-29,farhan,50
2025-12-30,dewi,41
2026-01-02,siti,48
2026-01-05,farhan,54
2026-01-08,budi,58
2026-01-10,siti,48
2026-01-12,budi,61

Satu baris transaksi belum bercerita apa-apa tentang pelanggan; kita perlu meringkasnya per pelanggan. groupby dengan agregasi bernama melakukan semuanya sekaligus:

python
import pandas as pd

transaksi = pd.read_csv("transaksi_rfm.csv", parse_dates=["tanggal"])
batas_data = pd.Timestamp("2026-01-15")

rfm = transaksi.groupby("pelanggan").agg(
    transaksi_terakhir=("tanggal", "max"),
    frekuensi=("tanggal", "count"),
    monetary=("total", "sum"),
)
rfm["recency"] = (batas_data - rfm["transaksi_terakhir"]).dt.days

print("Jumlah transaksi:", len(transaksi), "| jumlah pelanggan:", len(rfm))
print(rfm[["recency", "frekuensi", "monetary"]].sort_values("recency"))

Output yang Dihasilkan:

text
Jumlah transaksi: 40 | jumlah pelanggan: 10
           recency  frekuensi  monetary
pelanggan                              
budi             3          7       385
siti             5          6       290
farhan          10          2       104
dewi            16          5       200
rani            24          4       140
mira            28          3       114
agus            48          4       168
joko            56          3        90
lina            71          2        50
tono            85          4       180

Bedah kode di atas:

  1. pd.read_csv(..., parse_dates=["tanggal"]) memuat berkas sekaligus mengubah kolom tanggal menjadi tipe datetime — tanpa itu, pengurangan tanggal hanya mengurangi teks.
  2. Agregasi bernama pada agg() memasangkan nama kolom hasil dengan pasangan ("kolom sumber", "fungsi"): transaksi_terakhir mengambil tanggal maksimum, frekuensi menghitung baris, monetary menjumlahkan nilai. Satu groupby menghasilkan ketiga kolom sekaligus.
  3. rfm["recency"] menghitung selisih hari antara batas analisis dan transaksi terakhir lewat .dt.days. Angka 85 milik tono berarti transaksi terakhirnya 85 hari sebelum batas — pelanggan yang paling lama absen.
  4. sort_values("recency") menyusun tabel dari pelanggan paling hangat ke paling dingin, sehingga gradasi hubungan langsung terlihat.

Memberi Skor dengan cut dan qcut #

Tabel RFM berisi angka mentah dengan satuan berbeda-beda — hari, kali, ribu rupiah — sehingga belum bisa dibandingkan langsung. Kita terjemahkan tiap dimensi menjadi skor 1 sampai 3. Untuk itu pandas menyediakan dua pisau yang berbeda:

AlatDasar pembagianKapan cocok
pd.cutbatas rentang yang kita tetapkanambang sudah punya makna bisnis (mis. “lebih dari 30 hari = dingin”)
pd.qcutkuantil data itu sendiriingin kelompok yang berukuran sebanding (mis. sepertiga teratas)

Untuk recency kita pakai pd.cut dengan ambang bisnis — 14 hari dan 30 hari; untuk frequency dan monetary kita pakai pd.qcut agar tiap tingkat skor terisi kurang lebih sama banyak:

python
import pandas as pd

transaksi = pd.read_csv("transaksi_rfm.csv", parse_dates=["tanggal"])
batas_data = pd.Timestamp("2026-01-15")
rfm = transaksi.groupby("pelanggan").agg(
    transaksi_terakhir=("tanggal", "max"),
    frekuensi=("tanggal", "count"),
    monetary=("total", "sum"),
)
rfm["recency"] = (batas_data - rfm["transaksi_terakhir"]).dt.days

rfm["skor_R"] = pd.cut(rfm["recency"], bins=[-1, 14, 30, 1000], labels=[3, 2, 1]).astype(int)
rfm["skor_F"] = pd.qcut(rfm["frekuensi"].rank(method="first"), q=3, labels=[1, 2, 3]).astype(int)
rfm["skor_M"] = pd.qcut(rfm["monetary"].rank(method="first"), q=3, labels=[1, 2, 3]).astype(int)

print(rfm[["recency", "frekuensi", "monetary", "skor_R", "skor_F", "skor_M"]].sort_values("recency"))

Output yang Dihasilkan:

text
           recency  frekuensi  monetary  skor_R  skor_F  skor_M
pelanggan                                                      
budi             3          7       385       3       3       3
siti             5          6       290       3       3       3
farhan          10          2       104       3       1       1
dewi            16          5       200       2       3       3
rani            24          4       140       2       2       2
mira            28          3       114       2       2       2
agus            48          4       168       1       2       2
joko            56          3        90       1       1       1
lina            71          2        50       1       1       1
tono            85          4       180       1       3       3

Bedah kode dan hasilnya:

  1. pd.cut(rfm["recency"], bins=[-1, 14, 30, 1000], labels=[3, 2, 1]) memetakan recency ke skor: 0-14 hari → 3, 15-30 hari → 2, lebih dari 30 hari → 1. Batas bawah -1 memastikan nilai 0 ikut ke rentang pertama; batas atas 1000 cukup lebar untuk data kita. Perhatikan arah labelnya dibalik — makin segar makin tinggi skornya.
  2. .rank(method="first") pada kolom frekuensi menangani nilai seri: dua pelanggan dengan 4 transaksi mendapat nomor urut berbeda sesuai urutan alfabet, sehingga qcut tidak tersandung batas kuantil yang tumpang tindih.
  3. pd.qcut(..., q=3) membagi pelanggan menjadi tiga kelompok berukuran sama berdasarkan peringkatnya. Hasilnya pada kolom skor_F: farhan dan lina (2 transaksi) berada di tingkat 1; agus, rani, dan tono di tingkat 2; dewi, siti, dan budi di tingkat 3.
  4. Tabel skor memunculkan karakter yang mulai berlapis: budi dan siti bersih di ketiga dimensi; farhan segar tetapi jarang; tono — recency terburuk (85 hari) namun monetary terbaik ketiga (skor 3). Satu dimensi saja tidak akan menangkap tono; gabungan ketiganya menangkapnya dengan jelas.

Menyusun Segmen dan Memprofilnya #

Skor akhirnya perlu menjadi segmen bernama yang bisa dipahami tim pemasaran. Kita terjemahkan skor menjadi segmen lewat aturan — memakai numpy.select yang membaca daftar kondisi berurutan:

python
import numpy as np
import pandas as pd

transaksi = pd.read_csv("transaksi_rfm.csv", parse_dates=["tanggal"])
batas_data = pd.Timestamp("2026-01-15")
rfm = transaksi.groupby("pelanggan").agg(
    transaksi_terakhir=("tanggal", "max"),
    frekuensi=("tanggal", "count"),
    monetary=("total", "sum"),
)
rfm["recency"] = (batas_data - rfm["transaksi_terakhir"]).dt.days
rfm["skor_R"] = pd.cut(rfm["recency"], bins=[-1, 14, 30, 1000], labels=[3, 2, 1]).astype(int)
rfm["skor_F"] = pd.qcut(rfm["frekuensi"].rank(method="first"), q=3, labels=[1, 2, 3]).astype(int)
rfm["skor_M"] = pd.qcut(rfm["monetary"].rank(method="first"), q=3, labels=[1, 2, 3]).astype(int)

rfm["segmen"] = np.select(
    [
        (rfm["skor_R"] == 3) & (rfm["skor_F"] >= 2),
        (rfm["skor_R"] == 3) & (rfm["skor_F"] == 1),
        rfm["skor_R"] == 1,
    ],
    ["pelanggan utama", "pelanggan baru", "berisiko hilang"],
    default="perlu dijaga",
)

profil = rfm.groupby("segmen").agg(
    jumlah=("recency", "size"),
    recency=("recency", "mean"),
    frekuensi=("frekuensi", "mean"),
    monetary=("monetary", "mean"),
).round(1)
print(profil)
print()
for segmen in sorted(rfm["segmen"].unique()):
    anggota = sorted(rfm.index[rfm["segmen"] == segmen].tolist())
    print("%-16s: %s" % (segmen, ", ".join(anggota)))

Output yang Dihasilkan:

text
                 jumlah  recency  frekuensi  monetary
segmen                                               
berisiko hilang       4     65.0        3.2     122.0
pelanggan baru        1     10.0        2.0     104.0
pelanggan utama       2      4.0        6.5     337.5
perlu dijaga          3     22.7        4.0     151.3

berisiko hilang : agus, joko, lina, tono
pelanggan baru  : farhan
pelanggan utama : budi, siti
perlu dijaga    : dewi, mira, rani

Bedah kode dan pembacaan hasilnya:

  1. np.select(kondisi, pilihan, default=...) mengevaluasi daftar kondisi dari atas: pelanggan dengan recency segar dan frekuensi minimal 2 menjadi pelanggan utama; yang segar tetapi baru sekali-kali datang menjadi pelanggan baru; yang recency-nya buruk menjadi berisiko hilang; sisanya perlu dijaga. Aturan seperti ini transparan — siapa pun bisa menjelaskan mengapa seorang pelanggan masuk segmen tertentu.
  2. Profil per segmen merangkum karakter rata-ratanya: pelanggan utama rata-rata bertransaksi 6,5 kali dengan monetary 337,5 ribu; berisiko hilang sudah rata-rata 65 hari tidak muncul.
  3. Daftar anggota memperlihatkan siapa saja di tiap segmen — dan di sinilah tono muncul sebagai temuan paling berharga: pelanggan dengan skor monetary 3 yang sudah 85 hari tidak kembali. Rata-rata monetary segmen berisiko hilang (122) menyembunyikan tono; daftar anggota membuka tabirnya.

Segmen baru berguna bila tiap segmen punya tindakan yang jelas:

SegmenCiriTindakan yang masuk akal
pelanggan utamasegar, sering, besarjaga pengalaman; akses prioritas menu baru
pelanggan barusegar tetapi jarangtawaran kunjungan kedua; kenalkan program loyalitas
perlu dijagamulai menjauhpengingat personal; promo terbatas waktu
berisiko hilanglama tidak kembalikampanye menarik kembali; telusuri keluhan lama

Dari Aturan ke Clustering #

Segmentasi di atas berdiri di atas dua pilihan yang kita buat: ambang recency (14 dan 30 hari) dan aturan penamaan segmen. Pendekatan berbasis aturan seperti ini mudah dijelaskan dan cepat dibangun, tetapi ambangnya tetap tebakan terdidik. Clustering — yang akan kita pelajari pada Section Unsupervised Learning — mengambil jalan berbeda: algoritma seperti K-Means membiarkan pola di data yang menentukan kelompoknya, tanpa ambang dari kita. Imbalannya adalah interpretasi: kelompok hasil clustering harus kita maknai setelahnya, sedangkan segmen RFM bermakna sejak awal. Keduanya bukan saingan — praktikum yang baik sering memulai dari aturan RFM, lalu menguji apakah clustering menemukan potongan pasar yang terlewat.

Rangkuman #

  • RFM meringkas pelanggan menjadi tiga angka: recency (segar tidaknya), frequency (sering tidaknya), monetary (besar tidaknya) — dihitung dari data transaksi lewat groupby dengan agregasi bernama.
  • pd.cut membagi menurut ambang yang kita tetapkan; pd.qcut membagi menurut kuantil data. Nilai seri pada qcut ditangani dengan rank(method="first").
  • Segmen disusun dari skor dengan aturan yang transparan — np.select membaca daftar kondisi berurutan — lalu diprofil per segmen untuk memastikan tiap segmen bermakna dan bisa ditindaklanjuti.
  • Profil rata-rata bisa menyembunyikan kasus menarik seperti tono; selalu periksa daftar anggota segmen, bukan hanya ringkasannya.
  • Pendekatan berbasis aturan melengkapinya clustering nanti: aturan transparan sejak awal, clustering menemukan pola yang tidak kita duga.

Kita kini punya cara membagi dunia menjadi kelompok yang bermakna — dan sepanjang Section ini kita telah menghasilkan banyak temuan: hubungan kategorikal, perbedaan antar kelompok, segmen pelanggan. Pertanyaan yang tersisa sebelum melaporkan: temuan mana yang benar-benar tahan uji, dan bagaimana kita mengujinya tanpa terjebak mengarang cerita setelah melihat hasil? Jawabannya adalah disiplin analisis konfirmasi pada chapter berikutnya. Lanjut ke chapter Analisis Konfirmasi .

Referensi #

  1. pandas — API Reference: DataFrame.groupby
  2. scikit-learn — Clustering