Notebook Bab 1 - Dari Data Mentah ke Representasi Model

Open In Colab

Notebook Bab 1 ini punya dua bagian. Bagian Demo tinggal Anda jalankan lalu amati keluarannya; bagian Mini Project berisi soal dan data yang Anda kerjakan sendiri.

Demo memakai snapshot UCI Online Retail. Tiap baris awal adalah baris produk dalam invoice. Kita mengubah log peristiwa itu menjadi matriks fitur per pelanggan pada index_time, lalu memakai logistic regression sebagai penggaris tetap untuk membandingkan representasi agregat dengan satu baris mentah terakhir.

Persiapan

from pathlib import Path
import numpy as np
import pandas as pd
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, roc_auc_score

RANDOM_STATE = 42
rng = np.random.default_rng(RANDOM_STATE)


from pathlib import Path
import json
import urllib.request
import urllib.parse

DATA_BASE_URL = 'https://raw.githubusercontent.com/muhammad-zainal-muttaqin/NulisBuku/main/website/notebooks/data/section1'


def section_data_dir(name):
    """Folder data Bagian 1: pakai salinan lokal bila ada; jika tidak (mis. di
    Google Colab), unduh berkas dari repo GitHub sesuai manifest."""
    for base in (Path('data/section1'), Path('../data/section1')):
        if (base / name).exists():
            return base / name
    cache = Path('_nb_data') / name
    if not (cache / 'manifest.json').exists():
        cache.mkdir(parents=True, exist_ok=True)
        base_url = DATA_BASE_URL + '/' + name
        manifest = json.loads(urllib.request.urlopen(base_url + '/manifest.json').read().decode('utf-8'))
        for rel in manifest:
            dest = cache / rel
            dest.parent.mkdir(parents=True, exist_ok=True)
            if not dest.exists():
                url = base_url + '/' + '/'.join(urllib.parse.quote(seg) for seg in rel.split('/'))
                urllib.request.urlretrieve(url, dest)
        (cache / 'manifest.json').write_text(json.dumps(manifest), encoding='utf-8')
    return cache

DATA_DIR = section_data_dir('ch01_online_retail')
print(f'Setup selesai. Data: {DATA_DIR}')
Setup selesai. Snapshot: data\section1\ch01_online_retail\online_retail.parquet

Section 1 - Demo: Dari Log Transaksi ke Matriks Fitur

Log invoice mentah

Dataset awal berupa event log: satu invoice dapat punya banyak baris produk, dan satu pelanggan dapat muncul berkali-kali sepanjang waktu. Ini adalah bentuk mentah yang belum menjadi satu baris belajar.

retail = pd.read_parquet(DATA_DIR / 'online_retail.parquet')
retail['InvoiceDate'] = pd.to_datetime(retail['InvoiceDate'])
retail = retail.dropna(subset=['CustomerID']).copy()
retail['CustomerID'] = retail['CustomerID'].astype(int)
retail['purchase_line'] = (~retail['is_cancellation']) & (retail['Quantity'] > 0) & (retail['UnitPrice'] > 0)
retail['purchase_value'] = np.where(retail['purchase_line'], retail['Quantity'] * retail['UnitPrice'], 0.0)

summary = pd.Series({
    'baris_invoice_produk': len(retail),
    'pelanggan_unik': retail['CustomerID'].nunique(),
    'invoice_unik': retail['InvoiceNo'].nunique(),
    'tanggal_awal': retail['InvoiceDate'].min().date(),
    'tanggal_akhir': retail['InvoiceDate'].max().date(),
})
print(summary.to_string())
print('\nContoh baris mentah:')
print(retail[['InvoiceNo', 'CustomerID', 'StockCode', 'Quantity', 'InvoiceDate', 'UnitPrice', 'Country']].head(5).to_string(index=False))
baris_invoice_produk        406829
pelanggan_unik                4372
invoice_unik                 22190
tanggal_awal            2010-12-01
tanggal_akhir           2011-12-09

Contoh baris mentah:
InvoiceNo  CustomerID StockCode  Quantity         InvoiceDate  UnitPrice        Country
   536365       17850    85123A         6 2010-12-01 08:26:00       2.55 United Kingdom
   536365       17850     71053         6 2010-12-01 08:26:00       3.39 United Kingdom
   536365       17850    84406B         8 2010-12-01 08:26:00       2.75 United Kingdom
   536365       17850    84029G         6 2010-12-01 08:26:00       3.39 United Kingdom
   536365       17850    84029E         6 2010-12-01 08:26:00       3.39 United Kingdom

Agregasi ke matriks fitur per pelanggan

Fungsi berikut membuat tabel belajar pada sebuah index_time. Fitur hanya memakai invoice sebelum waktu itu. Target melihat 30 hari setelahnya: apakah pelanggan melakukan pembelian lagi dalam horizon tersebut?

def build_learning_table(index_time):
    horizon_end = index_time + pd.Timedelta(days=30)
    history = retail[retail['InvoiceDate'] < index_time].copy()
    future = retail[(retail['InvoiceDate'] >= index_time) &
                    (retail['InvoiceDate'] < horizon_end) &
                    (retail['purchase_line'])]

    table = history.groupby('CustomerID').agg(
        jumlah_baris=('InvoiceNo', 'size'),
        jumlah_invoice=('InvoiceNo', 'nunique'),
        total_belanja=('purchase_value', 'sum'),
        rata_harga_unit=('UnitPrice', 'mean'),
        ragam_produk=('StockCode', 'nunique'),
        jumlah_pembatalan=('is_cancellation', 'sum'),
        transaksi_terakhir=('InvoiceDate', 'max'),
    )
    table['hari_sejak_transaksi_terakhir'] = (index_time - table['transaksi_terakhir']).dt.days
    table = table.drop(columns='transaksi_terakhir').fillna(0)

    label = pd.Series(0, index=table.index, name='beli_lagi_30_hari')
    label.loc[label.index.intersection(future['CustomerID'].unique())] = 1

    last_line = history.sort_values('InvoiceDate').groupby('CustomerID').tail(1).set_index('CustomerID')
    last_line = last_line[['Quantity', 'UnitPrice', 'abs_line_value', 'is_cancellation']].reindex(table.index).fillna(0)
    return table, label, last_line

train_time = pd.Timestamp('2011-06-01')
test_time = pd.Timestamp('2011-09-01')
X_train_table, y_train, X_train_last = build_learning_table(train_time)
X_test_table, y_test, X_test_last = build_learning_table(test_time)

print(f'Cutoff latih: {train_time.date()} -> {X_train_table.shape[0]} pelanggan, target positif {y_train.mean():.1%}')
print(f'Cutoff uji:   {test_time.date()} -> {X_test_table.shape[0]} pelanggan, target positif {y_test.mean():.1%}')
print('\nContoh matriks fitur pada cutoff uji:')
print(X_test_table.head(8).round(2).to_string())
Cutoff latih: 2011-06-01 -> 2767 pelanggan, target positif 27.4%
Cutoff uji:   2011-09-01 -> 3360 pelanggan, target positif 28.8%

Contoh matriks fitur pada cutoff uji:
            jumlah_baris  jumlah_invoice  total_belanja  rata_harga_unit  ragam_produk  jumlah_pembatalan  hari_sejak_transaksi_terakhir
CustomerID                                                                                                                              
12346                  2               2       77183.60             1.04             1                  1                            225
12347                124               5        2790.86             2.80            82                  0                             29
12348                 28               3        1487.24             4.86            22                  0                            148
12350                 17               1         334.40             3.84            17                  0                            210
12352                 48               8        1561.81            39.58            26                 10                            162
12353                  4               1          89.00             6.07             4                  0                            104
12354                 58               1        1079.40             4.50            58                  0                            132
12355                 13               1         459.40             4.20            13                  0                            114

Representasi menentukan batas atas

Model, target, dan protokol waktunya kita tahan tetap. Yang dibedakan hanya representasi: matriks fitur agregat per pelanggan dibandingkan dengan satu baris transaksi terakhir per pelanggan.

fixed_model = Pipeline([
    ('scaler', StandardScaler()),
    ('logreg', LogisticRegression(max_iter=1000)),
])
fixed_model.fit(X_train_table, y_train)
proba_agregat = fixed_model.predict_proba(X_test_table)[:, 1]

fixed_model_raw = Pipeline([
    ('scaler', StandardScaler()),
    ('logreg', LogisticRegression(max_iter=1000)),
])
fixed_model_raw.fit(X_train_last, y_train)
proba_mentah = fixed_model_raw.predict_proba(X_test_last)[:, 1]

hasil = pd.DataFrame({
    'representasi': ['Agregat riwayat pelanggan', 'Satu baris transaksi terakhir'],
    'akurasi': [
        accuracy_score(y_test, fixed_model.predict(X_test_table)),
        accuracy_score(y_test, fixed_model_raw.predict(X_test_last)),
    ],
    'roc_auc': [
        roc_auc_score(y_test, proba_agregat),
        roc_auc_score(y_test, proba_mentah),
    ],
})
print(hasil.round(3).to_string(index=False))
                 representasi  akurasi  roc_auc
    Agregat riwayat pelanggan    0.758    0.741
Satu baris transaksi terakhir    0.712    0.509

🔎 Amati. Matriks fitur agregat membawa riwayat pelanggan sebelum index_time: frekuensi invoice, total belanja, ragam produk, pembatalan, dan recency. Satu baris transaksi terakhir membuang sebagian besar konteks itu. Model yang sama, target yang sama, dan batas waktu yang sama memberi hasil berbeda karena representasi yang masuk ke model berbeda.

Section 2 - Mini Project

Soal

Anda menerima log klik (clickstream) sebuah situs, satu baris per klik, dengan kolom id_sesi, menit, halaman, dan durasi_detik. Tujuannya memprediksi konversi (1 = sesi berakhir dengan pembelian).

Tugas:

  1. Ubah log peristiwa menjadi matriks fitur per sesi (minimal 5 fitur turunan, misalnya jumlah klik, total durasi, ragam halaman, durasi rata-rata).
  2. Latih satu baseline classifier pada matriks itu.
  3. Bandingkan dengan prediksi dari satu peristiwa mentah saja.

Luaran: kode agregasi, akurasi kedua pendekatan, dan 2-3 kalimat kesimpulan.

Kriteria penilaian: (a) agregasi benar ke tingkat sesi; (b) minimal 5 fitur turunan; (c) evaluasi pada data uji terpisah.

# DATA AWAL (jangan diubah) - clickstream: satu baris per klik.
n_sesi = 400
konversi_sesi = {s: int(rng.random() < 0.35) for s in range(1, n_sesi + 1)}
baris = []
for s in range(1, n_sesi + 1):
    for _ in range(int(rng.integers(1, 20))):
        menit = round(float(rng.exponential(2.0)), 2)
        halaman = rng.choice(['home', 'produk', 'keranjang', 'promo', 'akun'])
        durasi = int(rng.integers(3, 120) + (30 if konversi_sesi[s] else 0))
        baris.append((s, menit, halaman, durasi))
clicks = pd.DataFrame(baris, columns=['id_sesi', 'menit', 'halaman', 'durasi_detik'])
clicks['konversi'] = clicks['id_sesi'].map(konversi_sesi)
print('Clickstream:', clicks.shape, '| sesi:', clicks['id_sesi'].nunique())
clicks.head()
Clickstream: (4095, 5) | sesi: 400
id_sesi menit halaman durasi_detik konversi
0 1 0.04 promo 62 1
1 1 2.42 akun 51 1
2 1 3.86 produk 41 1
3 1 1.28 promo 89 1
4 1 1.09 produk 135 1
# Kerjakan di sini.
# Petunjuk: clicks.groupby('id_sesi').agg(...) untuk membuat matriks fitur per sesi.