Model tidak bisa membaca tabel transaksi mentah. Model membaca fitur: kolom angka ringkas per entitas — misalnya “berapa kali pelanggan ini berbelanja 30 hari terakhir” — yang harus dihitung ulang dari event mentah setiap saat. Pekerjaan menghitung, menjadwalkan, dan mendokumentasikan transformasi tersebut adalah tugas feature pipeline, layer pertama yang kita bangun di antara data warehouse dan model. Chapter ini membangun satu feature pipeline utuh untuk fitur klasik RFM, lalu menunjukkan mengapa identitas fitur harus menyimpan parameter dan versinya, bukan sekadar namanya.

Dari Event Menjadi Fitur #

RFM (recency, frequency, monetary) adalah contoh fitur paling tua sekaligus paling sering dipakai: seberapa baru pelanggan berbelanja, seberapa sering, dan seberapa besar nilainya. Tiga angka tersebut tidak ada di tabel orders; ketiganya adalah hasil agregasi event pesanan per pelanggan dalam jendela waktu tertentu. Feature pipeline adalah job terjadwal yang membaca sumber event, memotong waktunya, lalu menulis hasil agregasi sebagai tabel fitur snapshot — konsep pipeline batch yang sudah kita kenal, tetapi dengan output yang dirancang untuk model, bukan untuk dashboard.

Snapshot dengan Cutoff Waktu #

Skrip lengkap pada berkas scratch membuat 97 pesanan dari 6 pelanggan TokoKita selama 30 hari dengan generator ber-seed, lalu menghitung snapshot RFM per 1 November 2026. Blok berikut memotong bagian generator dan menampilkan inti perhitungannya:

python
CUTOFF = datetime(2026, 11, 1)
JENDELA = 30  # hari


def hitung_rfm(orders, cutoff, jendela):
    """Recency/frequency/monetary per pelanggan hanya dari data sebelum cutoff."""
    mulai = cutoff - timedelta(days=jendela)
    dalam = orders[(orders["order_ts"] >= mulai) & (orders["order_ts"] < cutoff)]
    agg = dalam.groupby("customer_id").agg(
        frequency=("order_id", "count"),
        monetary=("total", "sum"),
        terakhir=("order_ts", "max"),
    )
    agg["recency_days"] = (cutoff - agg["terakhir"]).dt.days
    return agg[["recency_days", "frequency", "monetary"]].sort_index()


rfm = hitung_rfm(orders, CUTOFF, JENDELA)
print(f"orders: {len(orders)} baris, snapshot per {CUTOFF:%Y-%m-%d}")
print(rfm.to_string())

Mari kita telaah baris-baris kode di atas:

  1. Baris 1-2 (CUTOFF, JENDELA): cutoff adalah batas atas snapshot, dan jendela 30 hari adalah satu-satunya parameter yang membedakan fitur ini dari fitur RFM berminggu-minggu.
  2. Baris 7 (dalam = orders[...]): filter memakai interval setengah terbuka — batas bawah ikut (>=), batas atas tidak (<) — sehingga pesanan tepat pukul 00:00 tanggal cutoff masuk ke snapshot berikutnya, bukan snapshot ini.
  3. Baris 8-12 (agg = dalam.groupby ...): satu groupby menghasilkan frequency (jumlah pesanan), monetary (total nilai), dan transaksi terakhir tiap pelanggan.
  4. Baris 13 (recency_days): recency dihitung dari jarak cutoff ke transaksi terakhir; pelanggan yang baru berbelanja kemarin mendapat angka kecil.

Output yang Dihasilkan:

text
orders: 97 baris, snapshot per 2026-11-01
             recency_days  frequency  monetary
customer_id                                   
Andi                    2         11   1905000
Budi                    1         16   3340000
Dewi                    0         18   3260000
Fajar                   3         13   2315000
Rina                    0         21   3220000
Siti                    0         18   2905000

Fitur adalah Kode Ber-Parameter #

Nama rfm_frequency saja tidak cukup mengidentifikasi fitur: frekuensi 30 hari dan frekuensi 7 hari adalah dua fitur berbeda yang kebetulan bersaudara. Karena itu identitas fitur harus menyimpan parameter dan versinya:

python
# Metadata fitur: nama + parameter + versi. Fitur adalah kode ber-parameter,
# jadi identitasnya harus menyimpan parameternya, bukan hanya namanya.
FITUR = {
    "rfm_recency_days": {"sumber": "orders", "jendela_hari": 30, "versi": 1},
    "rfm_frequency": {"sumber": "orders", "jendela_hari": 30, "versi": 1},
    "rfm_monetary": {"sumber": "orders", "jendela_hari": 30, "versi": 1},
}
print()
print("contoh metadata:", FITUR["rfm_frequency"])

Output yang Dihasilkan:

text
contoh metadata: {'sumber': 'orders', 'jendela_hari': 30, 'versi': 1}

Metadata sederhana ini menjadi fondasi bagi chapter-chapter selanjutnya: versi 1 memudahkan kita menaikkan versi saat logika berubah, dan parameter jendela memastikan tidak ada tim yang diam-diam memakai rfm_frequency versi 7 hari untuk model yang dilatih dengan versi 30 hari.

Satu Logika, Dua Mesin #

Perhitungan di atas berjalan di pandas, tetapi bentuk produksinya biasanya SQL pada engine analitik. Kedua bentuk tersebut wajib menghasilkan nilai yang persis sama, jadi skrip menutup dengan mengulang perhitungan yang sama sebagai query DuckDB :

python
# Jalur SQL yang sama di DuckDB -- bentuk produksi dari perhitungan ini.
rfm_sql = duckdb.sql(
    """
    SELECT customer_id,
           date_diff('second', max(order_ts), TIMESTAMP '2026-11-01 00:00') // 86400
               AS recency_days,
           count(*) AS frequency,
           sum(total) AS monetary
    FROM orders
    WHERE order_ts >= TIMESTAMP '2026-11-01 00:00' - INTERVAL 30 DAY
      AND order_ts <  TIMESTAMP '2026-11-01 00:00'
    GROUP BY customer_id
    ORDER BY customer_id
    """
).df().set_index("customer_id")

sama = (rfm.to_numpy() == rfm_sql[rfm.columns].to_numpy()).all()
print(f"pandas dan DuckDB menghasilkan nilai yang sama: {sama}")

Output yang Dihasilkan:

text
pandas dan DuckDB menghasilkan nilai yang sama: True

Klausa WHERE pada query menyalin interval setengah terbuka versi pandas, dan date_diff ... // 86400 membagi selisih detik menjadi hari agar recency berdefinisi sama. Perbandingan sama = ... memverifikasi kedua jalur nilai demi nilai dengan API to_numpy() dari pandas — pola uji paritas yang akan kita angkat menjadi kewajiban di chapter Feature Consistency.

Fitur kini terhitung, tetapi snapshot RFM ini baru milik satu model dan masih disimpan sebagai file biasa. Ketika puluhan model dan beberapa tim membutuhkan fitur yang sama, kita butuh satu tempat resmi untuk menyimpan, menemukan, dan menyajikannya baik untuk training maupun serving — tugas feature store pada chapter berikutnya.