from pathlib import Path
import json
import pandas as pd
import matplotlib.pyplot as plt
plt.style.use('seaborn-v0_8-whitegrid')
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 cacheNotebook Bab 16 - Rekayasa Fitur Otomatis
Notebook Bab 16 ini punya dua bagian. Bagian Demo tinggal Anda jalankan lalu amati keluarannya; bagian Mini Project berisi soal yang Anda kerjakan sendiri.
Demo memakai audit fitur otomatis yang sudah dibekukan dari snapshot UCI Online Retail: unitnya pelanggan pada index_time, targetnya future_active_60d, dan kandidat fiturnya dinilai dengan delta-CV. Tidak ada instalasi featuretools atau unduhan data saat notebook dijalankan.
Persiapan
Section 1 - Demo: Kandidat Otomatis + Kurasi Manusia
Data: audit fitur dari Online Retail
Bab ini memakai tabel pelanggan/cutoff dari Bab 6. Mesin membangkitkan kandidat transformasi, rasio, kalender, identifier, dan fitur masa depan. Manusia lalu memutuskan mana yang diterima, ditinjau ulang, atau ditolak.
DATA_DIR = section_data_dir('ch16_online_retail')
stats = json.loads((DATA_DIR / 'verified_stats.json').read_text(encoding='utf-8'))
summary = pd.read_csv(DATA_DIR / 'autofe_summary_metrics.csv')
candidates = pd.read_csv(DATA_DIR / 'autofe_candidate_scores.csv')
audit_matrix = pd.read_parquet(DATA_DIR / 'autofe_feature_matrix.parquet')
print(f"Baris audit: {stats['rows']} | train={stats['train_rows']} | holdout={stats['test_rows']}")
print(f"Target: {stats['target']} | rate={stats['target_rate_all']:.3f}")
print(f"Kandidat otomatis: {stats['candidate_count']} | diterima manusia: {stats['accepted_candidate_count']}")
print('Fixed ruler:', stats['fixed_ruler'])
audit_matrix[['CustomerID', 'index_time', 'future_active_60d', 'invoice_count_90d', 'gross_value_90d', 'days_since_last_invoice']].head()Baris audit: 20603 | train=13602 | holdout=7001
Target: future_active_60d | rate=0.428
Kandidat otomatis: 15 | diterima manusia: 6
Fixed ruler: SimpleImputer + StandardScaler + LogisticRegression(class_weight='balanced')
| CustomerID | index_time | future_active_60d | invoice_count_90d | gross_value_90d | days_since_last_invoice | |
|---|---|---|---|---|---|---|
| 0 | 12346 | 2011-04-01 | 0 | 2.0 | 77183.60 | 72 |
| 1 | 12347 | 2011-04-01 | 1 | 1.0 | 475.39 | 64 |
| 2 | 12348 | 2011-04-01 | 1 | 1.0 | 227.44 | 65 |
| 3 | 12350 | 2011-04-01 | 0 | 1.0 | 334.40 | 57 |
| 4 | 12352 | 2011-04-01 | 0 | 8.0 | 1561.81 | 9 |
Delta-CV: baseline, kandidat terkurasi, dan kebocoran
print(summary.to_string(index=False, float_format=lambda x: f'{x:.4f}'))
plot_df = summary.copy()
fig, axes = plt.subplots(1, 2, figsize=(11, 4))
axes[0].barh(plot_df['representation'], plot_df['cv_roc_auc'], color='#4c78a8')
axes[0].set_xlim(0.68, 1.01)
axes[0].set_xlabel('CV ROC-AUC')
axes[0].set_title('Train-only expanding cutoff CV')
for i, value in enumerate(plot_df['cv_roc_auc']):
axes[0].text(value + 0.004, i, f'{value:.3f}', va='center')
axes[1].barh(plot_df['representation'], plot_df['holdout_roc_auc'], color='#f58518')
axes[1].set_xlim(0.68, 1.01)
axes[1].set_xlabel('Holdout ROC-AUC')
axes[1].set_title('Future holdout: Sep-Oct 2011')
for i, value in enumerate(plot_df['holdout_roc_auc']):
axes[1].text(value + 0.004, i, f'{value:.3f}', va='center')
plt.tight_layout()
plt.show() representation feature_count cv_roc_auc cv_delta_vs_base holdout_roc_auc holdout_delta_vs_base fold_scores
base_manual_90d 5 0.7278 0.0000 0.7162 0.0000 [0.7248216952521381, 0.7260069272450033, 0.7324563165294236]
curated_auto_candidates 12 0.7297 0.0019 0.7215 0.0053 [0.7248314172830704, 0.730655684887469, 0.7335328789077391]
broad_nonleaky_auto_candidates 19 0.7296 0.0019 0.7202 0.0040 [0.72324888669243, 0.732950021360819, 0.7326829828135312]
unsafe_with_future_leakage 21 0.9997 0.2719 1.0000 0.2838 [1.0, 0.9991209656320271, 1.0]

Audit kandidat: skor bukan keputusan akhir
view_cols = ['candidate', 'family', 'decision', 'cv_delta_vs_base', 'holdout_delta_vs_base', 'reason']
print(candidates[view_cols].head(12).to_string(index=False, float_format=lambda x: f'{x:+.4f}'))
color_map = {
'accept': '#54a24b',
'review': '#eeca3b',
'reject_semantic': '#b279a2',
'reject_leakage': '#e45756',
}
plot_df = candidates.sort_values('cv_delta_vs_base')
fig, ax = plt.subplots(figsize=(8, 6))
ax.barh(plot_df['candidate'], plot_df['cv_delta_vs_base'], color=plot_df['decision'].map(color_map))
ax.axvline(0, color='black', linewidth=0.8)
ax.set_xlabel('Delta CV ROC-AUC vs baseline')
ax.set_title('Kandidat otomatis harus diaudit manusia')
plt.tight_layout()
plt.show() candidate family decision cv_delta_vs_base holdout_delta_vs_base reason
future_positive_invoices_60d leakage reject_leakage +0.2722 +0.2838 Directly counts the future event used to define the target.
future_gross_value_60d leakage reject_leakage +0.2700 +0.2818 Uses the future label window and is unavailable at prediction time.
log1p_days_since_last_invoice transform accept +0.0005 +0.0012 Row-wise monotonic transform of a known-at-cutoff recency feature.
recency_weighted_gross_180d aggregation accept +0.0005 +0.0002 Summarizes older transactions with recency weighting before the cutoff.
gross_per_active_day_90d ratio accept +0.0004 -0.0002 Normalizes 90-day value by the number of active historical days.
recent_value_share_30_180 ratio accept +0.0003 +0.0033 Compares recent value with longer history using only pre-cutoff events.
cyclic_index_month calendar accept +0.0001 +0.0004 Encodes the cutoff month as a cyclic calendar signal.
gross_x_unique_90d interaction review +0.0001 +0.0001 Statistically valid but less interpretable than normalized ratios.
has_recent_invoice_30d threshold accept +0.0000 +0.0006 Turns recent activity into a simple known-at-cutoff flag.
unique_stock_per_invoice_90d ratio review -0.0005 -0.0004 Plausible diversity ratio, but the fixed ruler does not reward it here.
value_per_item_90d ratio review -0.0007 +0.0006 Plausible price-intensity ratio; holdout delta is tiny.
items_per_invoice_90d ratio review -0.0007 -0.0001 Plausible, but redundant with existing 90-day count and item features.

?? Amati. Kandidat terkurasi memberi kenaikan kecil tetapi nyata pada holdout masa depan. Kandidat yang memakai jendela masa depan terlihat hampir sempurna, justru karena bocor.
customer_id_numericjuga ditolak walaupun bisa dihitung, karena ID bukan besaran numerik. Di Bab 16, skor adalah bahan audit; keputusan akhir tetap menggabungkan validasi, ketersediaan saat inferensi, dan makna domain.
Section 2 - Mini Project
Soal
Gunakan tabel audit yang sama untuk membuat keputusan kurasi fitur otomatis.
Tugas:
- Pilih 5-8 kandidat dari
mini_candidatesyang menurut Anda layak masuk tahap berikutnya. - Pisahkan alasan penolakan menjadi minimal dua kategori: bocor dan tidak bermakna secara semantik.
- Tulis ringkasan 3-4 kalimat: apakah Anda akan menerima kandidat berdasarkan skor saja, atau perlu aturan manusia tambahan?
Luaran: daftar kandidat diterima/ditolak, alasan singkat, dan ringkasan keputusan.
Kriteria penilaian: (a) fitur masa depan ditolak; (b) identifier tidak diperlakukan sebagai angka biasa; (c) keputusan menyebut metric sekaligus makna fitur.
# DATA AWAL (jangan diubah)
mini_candidates = candidates.copy()
mini_matrix = audit_matrix.copy()
print('Kandidat tersedia:', len(mini_candidates))
mini_candidates[['candidate', 'family', 'decision', 'cv_delta_vs_base', 'holdout_delta_vs_base']].head(10)Kandidat tersedia: 15
| candidate | family | decision | cv_delta_vs_base | holdout_delta_vs_base | |
|---|---|---|---|---|---|
| 0 | future_positive_invoices_60d | leakage | reject_leakage | 0.272238 | 0.283773 |
| 1 | future_gross_value_60d | leakage | reject_leakage | 0.270015 | 0.281773 |
| 2 | log1p_days_since_last_invoice | transform | accept | 0.000528 | 0.001205 |
| 3 | recency_weighted_gross_180d | aggregation | accept | 0.000519 | 0.000165 |
| 4 | gross_per_active_day_90d | ratio | accept | 0.000392 | -0.000175 |
| 5 | recent_value_share_30_180 | ratio | accept | 0.000253 | 0.003316 |
| 6 | cyclic_index_month | calendar | accept | 0.000106 | 0.000381 |
| 7 | gross_x_unique_90d | interaction | review | 0.000071 | 0.000059 |
| 8 | has_recent_invoice_30d | threshold | accept | 0.000023 | 0.000624 |
| 9 | unique_stock_per_invoice_90d | ratio | review | -0.000522 | -0.000411 |
# Kerjakan di sini.
# Petunjuk: mulai dari mini_candidates, kelompokkan berdasarkan decision/family,
# lalu jelaskan mana yang Anda terima, tinjau ulang, atau tolak.