Lima proyek sebelumnya melatih kita satu keterampilan demi satu keterampilan: regresi, klasifikasi data tak seimbang, segmentasi tanpa label, peramalan deret waktu, dan deteksi anomali. Proyek akhir ini merangkai semuanya menjadi satu jalur kerja utuh dari data mentah sampai sistem yang hidup: prediksi peluang keberhasilan kampanye pemasaran digital sebelum anggaran dihabiskan. Bagian pertama kembali ke kebiasaan yang paling menentukan mutu proyek — merumuskan masalah dengan tepat, menyiapkan data yang jujur, dan membaca strukturnya lebih dulu sebelum satu pun model disentuh — lalu ditutup dengan lima temuan yang langsung menjadi keputusan desain pipeline.

Merumuskan Masalah Sebelum Menyentuh Model #

Tim pemasaran sebuah perusahaan daring menjalankan ratusan kampanye iklan digital setiap kuartal, dan hanya sebagian mencapai target KPI-nya. Intuisi tim sering benar pada satu-dua dimensi, tetapi peluang keberhasilan sebuah kampanye ditentukan kombinasi anggaran, durasi tayang, kanal iklan, segmen audiens, musim promo, dan kualitas materi iklan sekaligus. Kombinasi enam dimensi tersebut melampaui kapasitas analisis manual, dan di titik inilah machine learning mengambil alih.

Kita merumuskannya sebagai klasifikasi biner dengan keluaran probabilitas: fitur kampanye masuk, angka peluang antara 0 dan 1 keluar. Probabilitas dipilih karena tim pemasaran membutuhkan gradasi — memprioritaskan kampanye berpeluang 0,91 di depan kampanye berpeluang 0,58 menuntut angka, bukan sekadar “ya” atau “tidak”. Dataset berisi 2000 kampanye historis yang hasilnya terverifikasi, dengan proporsi kelas positif 40,1% yang masih seimbang sehingga ROC-AUC dan average precision dapat dipercaya tanpa penyeimbangan kelas.

Membangkitkan Dataset Kampanye yang Deterministik #

Dataset proyek ini sintetis, dibangkitkan 01_bangun_data.py dengan satu generator np.random.default_rng(42). Data sintetis dipilih karena dua keunggulan pedagogisnya: setiap pembaca mendapat CSV yang identik bit demi bit, dan mekanisme pembentuk target diketahui penuh sehingga pada akhir proyek kita bisa menilai apakah model benar-benar menemukan pola yang tertanam. Seluruh keputusan pembangkitan — dari distribusi lognormal anggaran sampai tiga interaksi antar fitur — dituliskan eksplisit di dalam skrip.

python
# Bangkitkan dataset kampanye pemasaran digital sintetis untuk proyek
# akhir course. Deterministik penuh: satu generator np.random.default_rng(42)
# menentukan seluruh keacakan sehingga siapa pun yang menjalankan skrip
# mendapat CSV identik bit demi bit. Jalankan dari root proyek:
# python3 src/01_bangun_data.py
# Keluaran: data/kampanye.csv (2000 baris, 9 kolom).
#
# Struktur kolom:
#   id_kampanye  - nomor urut 1..2000 (penanda, bukan fitur).
#   anggaran_ribu - anggaran iklan dalam ribu rupiah, lognormal miring.
#   durasi_hari  - lama tayang kampanye, bilangan bulat 3..30 hari.
#   audiens_ribu - sasaran audiens dalam ribu orang; jangkauan membesar
#                  seiring anggaran (korelasi kuat, realistis).
#   ctr_persen   - click-through rate persen, lognormal terpotong 0,3..12.
#   kanal        - kategorikal 4 nilai: penelusuran/medsos/tampilan/email.
#   segmen       - kategorikal 3 nilai: akuisisi/retensi/reaktivasi.
#   musim        - kategorikal 2 nilai: reguler/promo.
#   berhasil     - target biner 0/1, apakah KPI kampanye tercapai.
#
# Mekanisme target (proses logistik, semua efek ditranspariskan):
#   - anggaran   : efek saturasi tanh - membesar lalu jenuh (nonlinear).
#   - ctr        : penggerak utama pada skala logaritmik.
#   - durasi     : efek puncak di sekitar 15 hari - terlalu pendek atau
#                  terlalu lama sama-sama menurunkan peluang (non-monoton).
#   - kanal      : penelusuran paling kuat, tampilan paling lemah.
#   - segmen x musim : kampanye akuisisi saat musim promo mendapat dorongan
#                  besar; reaktivasi kecil; retensi tidak berubah.
#   - kanal x ctr    : ctr tinggi menguatkan peluang di penelusuran, tetapi
#                  justru menurunkannya di tampilan (klik tanpa niat).
#
# Nilai hilang: 3% sel anggaran_ribu dan 2% sel ctr_persen dikosongkan
# agar pipeline Bagian 2 benar-benar membutuhkan imputasi.

import os

import numpy as np
import pandas as pd

SEED = 42
N = 2000
NAMA_BERKAS = "data/kampanye.csv"
NAMA_SEMENTARA = "data/kampanye.csv.tmp"

rng = np.random.default_rng(SEED)

# ---------- Fitur dasar ----------
anggaran = rng.lognormal(np.log(250.0), 0.55, N)          # median 250 ribu
durasi = rng.integers(3, 31, N)                           # 3..30 hari
audiens = anggaran * rng.uniform(1.2, 3.5, N)             # jangkauan ~ anggaran
ctr = np.clip(rng.lognormal(np.log(2.4), 0.38, N), 0.3, 12.0)
kanal = rng.choice(
    ["penelusuran", "medsos", "tampilan", "email"], N, p=[0.30, 0.35, 0.20, 0.15]
)
segmen = rng.choice(
    ["akuisisi", "retensi", "reaktivasi"], N, p=[0.45, 0.35, 0.20]
)
musim = rng.choice(["reguler", "promo"], N, p=[0.60, 0.40])

# ---------- Skala bantu untuk efek (standarisasi internal) ----------
z_ang = (np.log(anggaran) - np.log(anggaran).mean()) / np.log(anggaran).std()
z_ctr = (np.log(ctr) - np.log(ctr).mean()) / np.log(ctr).std()

# ---------- Efek-efek target ----------
efek_kanal = np.select(
    [kanal == "penelusuran", kanal == "medsos", kanal == "tampilan", kanal == "email"],
    [0.55, 0.25, -0.35, 0.0],
).astype(float)
interaksi_segmen_musim = np.select(
    [
        (segmen == "akuisisi") & (musim == "promo"),
        (segmen == "reaktivasi") & (musim == "promo"),
        (segmen == "retensi") & (musim == "promo"),
    ],
    [1.70, 0.50, 0.0],
    default=0.0,
).astype(float)
interaksi_kanal_ctr = np.select(
    [kanal == "penelusuran", kanal == "tampilan"],
    [1.00 * z_ctr, -1.30 * z_ctr],
    default=0.0,
).astype(float)
efek_durasi = -0.80 * (durasi - 15.0) ** 2 / 100.0       # puncak di 15 hari

# ---------- Proses logistik ----------
skor = (
    -0.45
    + 0.90 * np.tanh(0.9 * z_ang)
    + 0.70 * z_ctr
    + efek_durasi
    + efek_kanal
    + interaksi_segmen_musim
    + interaksi_kanal_ctr
)
peluang = 1.0 / (1.0 + np.exp(-skor))
berhasil = (rng.random(N) < peluang).astype(int)

# ---------- Sisipkan nilai hilang numerik ----------
posisi_hilang_anggaran = rng.choice(N, size=60, replace=False)   # 3%
posisi_hilang_ctr = rng.choice(N, size=40, replace=False)        # 2%
anggaran_hilang = anggaran.copy()
anggaran_hilang[posisi_hilang_anggaran] = np.nan
ctr_hilang = ctr.copy()
ctr_hilang[posisi_hilang_ctr] = np.nan

df = pd.DataFrame(
    {
        "id_kampanye": np.arange(1, N + 1),
        "anggaran_ribu": anggaran_hilang,
        "durasi_hari": durasi,
        "audiens_ribu": np.round(audiens, 1),
        "ctr_persen": ctr_hilang,
        "kanal": kanal,
        "segmen": segmen,
        "musim": musim,
        "berhasil": berhasil,
    }
)

# Tulis atomik: pembaca paralel tidak pernah melihat berkas setengah jadi.
os.makedirs("data", exist_ok=True)
df.to_csv(NAMA_SEMENTARA, index=False)
os.replace(NAMA_SEMENTARA, NAMA_BERKAS)

# ---------- Ringkasan untuk pemeriksaan ----------
print("== Ringkasan pembangkitan data ==")
print(f"jumlah baris : {len(df)}")
print(f"kolom        : {list(df.columns)}")
print(f"berkas       : {NAMA_BERKAS}")
print(f"berhasil = 1 : {int(df['berhasil'].sum())} baris "
      f"({df['berhasil'].mean():.1%} dari data)")
print(f"nilai hilang : anggaran_ribu {int(df['anggaran_ribu'].isna().sum())} sel, "
      f"ctr_persen {int(df['ctr_persen'].isna().sum())} sel, "
      f"lainnya 0 sel")
print(f"anggaran     : mean {df['anggaran_ribu'].mean():.1f} ribu, "
      f"median {df['anggaran_ribu'].median():.1f} ribu")
print(f"ctr          : mean {df['ctr_persen'].mean():.2f}%, "
      f"median {df['ctr_persen'].median():.2f}%")
print(f"korelasi audiens-anggaran (Spearman) : "
      f"{df[['audiens_ribu', 'anggaran_ribu']].corr(method='spearman').iloc[0, 1]:.3f}")

print("\n== Laju keberhasilan per kanal ==")
laju_kanal = df.groupby("kanal")["berhasil"].mean().sort_values(ascending=False)
for nama_kanal, laju in laju_kanal.items():
    print(f"{nama_kanal:12s}: {laju:.3f}")

print("\n== Laju keberhasilan segmen x musim ==")
tabel = df.pivot_table(index="segmen", columns="musim", values="berhasil", aggfunc="mean")
print(tabel.round(3).to_string())

print("\n== Lima baris pertama ==")
print(df.head(5).to_string(index=False))

Output yang Dihasilkan:

text
== Ringkasan pembangkitan data ==
jumlah baris : 2000
kolom        : ['id_kampanye', 'anggaran_ribu', 'durasi_hari', 'audiens_ribu', 'ctr_persen', 'kanal', 'segmen', 'musim', 'berhasil']
berkas       : data/kampanye.csv
berhasil = 1 : 802 baris (40.1% dari data)
nilai hilang : anggaran_ribu 60 sel, ctr_persen 40 sel, lainnya 0 sel
anggaran     : mean 281.7 ribu, median 246.3 ribu
ctr          : mean 2.61%, median 2.40%
korelasi audiens-anggaran (Spearman) : 0.869

== Laju keberhasilan per kanal ==
penelusuran : 0.475
medsos      : 0.417
email       : 0.348
tampilan    : 0.307

== Laju keberhasilan segmen x musim ==
musim       promo  reguler
segmen
akuisisi    0.675    0.371
reaktivasi  0.392    0.337
retensi     0.349    0.300

== Lima baris pertama ==
 id_kampanye  anggaran_ribu  durasi_hari  audiens_ribu  ctr_persen       kanal  segmen   musim  berhasil
           1     295.614225           23        1017.9    1.401727 penelusuran retensi reguler         0
           2     141.100113            4         318.9    3.676795      medsos retensi   promo         0
           3     377.741113           13        1011.5    3.780827 penelusuran retensi   promo         1
           4     419.377518           15        1389.4    2.713323 penelusuran retensi   promo         1
           5      85.489329           29         151.5    4.236923 penelusuran retensi reguler         0

Mari kita bedah skrip pembangkitan data:

  1. Baris 39–44 (konstanta dan generator): SEED = 42 dan rng = np.random.default_rng(SEED) — seluruh keacakan mengalir dari satu generator berseed, jadi skrip menghasilkan CSV yang sama di mesin mana pun.
  2. Baris 47–57 (fitur dasar): anggaran lognormal median 250 ribu, durasi 3–30 hari, audiens proporsional terhadap anggaran, CTR terpotong 0,3–12%, dan tiga fitur kategorikal.
  3. Baris 60–61 (skala bantu): z-score pada skala logaritmik untuk anggaran dan CTR — alat internal pembangkitan yang tidak keluar dari skrip.
  4. Baris 64–82 (efek-efek target): enam kelompok efek ditanam — efek kanal, interaksi segmen×musim (akuisisi saat promo +1,70), interaksi kanal×ctr (penelusuran menguatkan, tampilan menurunkan), dan kurva puncak durasi di hari ke-15.
  5. Baris 85–95 (proses logistik): efek dijumlah menjadi skor, ditransformasikan fungsi logistik menjadi peluang, lalu sampel Bernoulli rng.random(N) < peluang menentukan label — hubungan fitur-target jadi statistik, bukan deterministik.
  6. Baris 98–103 (nilai hilang): 3% sel anggaran dan 2% sel CTR dikosongkan setelah label terbentuk, meniru kerusakan data pelaporan yang tersebar acak.
  7. Baris 119–122 (penulisan atomik): CSV ditulis ke berkas sementara lalu dipindahkan dengan os.replace — penggantian atomik sehingga pembaca paralel tidak pernah menangkap berkas setengah jadi.

Gerbang Kualitas dan Pembacaan Struktur Data #

Skrip kedua bekerja seperti analis sungguhan: 02_eksplorasi.py hanya membaca data/kampanye.csv tanpa tahu apa-apa tentang mekanisme pembangkitan — pola yang meniru kenyataan, sebab di dunia nyata setiap klaim harus dibuktikan dari angka.

python
# Eksplorasi data kampanye: gerbang kualitas, profil distribusi, hubungan
# fitur-target, hingga interaksi kategorikal. Seluruh angka dihitung ulang
# dari data/kampanye.csv - skrip tidak membaca mekanisme pembangkitan di
# 01_bangun_data.py, meniru sikap analis yang hanya punya data mentah.
# Jalankan dari root proyek: python3 src/02_eksplorasi.py

import numpy as np
import pandas as pd

df = pd.read_csv("data/kampanye.csv")
FITUR_NUM = ["anggaran_ribu", "durasi_hari", "audiens_ribu", "ctr_persen"]
FITUR_KAT = ["kanal", "segmen", "musim"]

# ---------- Gerbang kualitas ----------
print("== Gerbang kualitas data ==")
print(f"dimensi      : {df.shape[0]} baris x {df.shape[1]} kolom")
print(f"id duplikat  : {int(df['id_kampanye'].duplicated().sum())}")
hilang = df.isna().sum()
print("nilai hilang : " + ", ".join(f"{k} {v}" for k, v in hilang.items() if v > 0))
print(f"target       : berhasil=1 {int(df['berhasil'].sum())} baris "
      f"({df['berhasil'].mean():.1%}), berhasil=0 "
      f"{int((1 - df['berhasil']).sum())} baris")

# ---------- Profil fitur numerik ----------
print("\n== Profil fitur numerik ==")
print(f"{'fitur':14s} {'n':>5s} {'mean':>9s} {'median':>9s} {'std':>8s} "
      f"{'min':>8s} {'maks':>9s} {'skew':>6s}")
for f in FITUR_NUM:
    s = df[f]
    print(
        f"{f:14s} {s.notna().sum():5d} {s.mean():9.1f} {s.median():9.1f} "
        f"{s.std():8.1f} {s.min():8.1f} {s.max():9.1f} {s.skew():6.2f}"
    )

# ---------- Korelasi Spearman ----------
print("\n== Korelasi Spearman (numerik + target) ==")
kolom_kor = FITUR_NUM + ["berhasil"]
matriks = df[kolom_kor].corr(method="spearman")
print("              " + "".join(f"{c[:9]:>10s}" for c in kolom_kor))
for baris_nama in kolom_kor:
    cells = "".join(f"{matriks.loc[baris_nama, c]:10.3f}" for c in kolom_kor)
    print(f"{baris_nama:14s}{cells}")

# ---------- Fitur numerik vs target ----------
print("\n== Rata-rata fitur numerik per kelas target ==")
print(f"{'fitur':14s} {'gagal':>9s} {'berhasil':>9s} {'selisih':>9s}")
for f in FITUR_NUM:
    gagal = df.loc[df["berhasil"] == 0, f].mean()
    sukses = df.loc[df["berhasil"] == 1, f].mean()
    print(f"{f:14s} {gagal:9.1f} {sukses:9.1f} {sukses - gagal:+9.1f}")

# ---------- Durasi per rentang: dugaan efek puncak ----------
print("\n== Laju keberhasilan per rentang durasi ==")
rentang = pd.cut(df["durasi_hari"], bins=[2, 9, 15, 21, 30], labels=["3-9", "10-15", "16-21", "22-30"])
laju_durasi = df.groupby(rentang, observed=False)["berhasil"].agg(["size", "mean"])
for idx, (n, laju) in laju_durasi.iterrows():
    print(f"hari {str(idx):7s}: {int(n):4d} kampanye, laju {laju:.3f}")

# ---------- Fitur kategorikal vs target ----------
print("\n== Fitur kategorikal vs target ==")
for f in FITUR_KAT:
    print(f"[{f}]")
    tabel = df.groupby(f)["berhasil"].agg(["size", "mean"]).sort_values("mean", ascending=False)
    for nilai, (n, laju) in tabel.iterrows():
        print(f"  {nilai:12s}: {int(n):4d} baris, laju {laju:.3f}")

# ---------- Interaksi segmen x musim ----------
print("\n== Interaksi segmen x musim (laju keberhasilan) ==")
pivot = df.pivot_table(index="segmen", columns="musim", values="berhasil", aggfunc="mean")
print(pivot.round(3).to_string())
delta = (pivot["promo"] - pivot["reguler"]).sort_values(ascending=False)
print("dorongan promo (promo - reguler):")
for nama_seg, d in delta.items():
    print(f"  {nama_seg:12s}: {d:+.3f}")

# ---------- Nilai hilang: dampak pilihan penanganan ----------
print("\n== Nilai hilang: dampak pilihan penanganan ==")
baris_lengkap = df.dropna()
print(f"buang baris ber-hilang : tersisa {len(baris_lengkap)} baris "
      f"({1 - len(baris_lengkap) / len(df):.1%} data terbuang)")
print(f"laju target lengkap    : {baris_lengkap['berhasil'].mean():.3f}")
print(f"laju target ter-hilang : "
      f"{df.loc[df.isna().any(axis=1), 'berhasil'].mean():.3f}")
for f in ["anggaran_ribu", "ctr_persen"]:
    print(f"median {f:14s}: {df[f].median():.1f} (kandidat imputasi)")

# ---------- Temuan utama ----------
print("\n== Temuan utama untuk tahap berikutnya ==")
print("1. anggaran_ribu dan ctr_persen punya nilai hilang -> pipeline perlu")
print("   SimpleImputer; median dipilih karena distribusi miring.")
print("2. audiens_ribu berkorelasi sangat kuat dengan anggaran_ribu")
print("   (Spearman 0.869) - kolinearitas yang aman bagi model pohon.")
print("3. durasi_hari berpuncak di rentang 10-15 hari - hubungan non-monoton")
print("   yang menyulitkan model linear murni.")
print("4. segmen x musim berinteraksi: akuisisi saat promo melonjak.")
print("5. skala fitur campuran (anggaran ratusan ribu vs ctr satuan persen)")
print("   -> StandardScaler wajib untuk model berbasis jarak/gradien.")

Output yang Dihasilkan:

text
== Gerbang kualitas data ==
dimensi      : 2000 baris x 9 kolom
id duplikat  : 0
nilai hilang : anggaran_ribu 60, ctr_persen 40
target       : berhasil=1 802 baris (40.1%), berhasil=0 1198 baris

== Profil fitur numerik ==
fitur              n      mean    median      std      min      maks   skew
anggaran_ribu   1940     281.7     246.3    166.1     33.6    1436.3   1.83
durasi_hari     2000      16.4      16.0      8.0      3.0      30.0   0.03
audiens_ribu    2000     657.9     536.7    443.8     58.7    4107.5   2.03
ctr_persen      1960       2.6       2.4      1.0      0.5       8.2   1.18

== Korelasi Spearman (numerik + target) ==
               anggaran_ durasi_ha audiens_r ctr_perse  berhasil
anggaran_ribu      1.000     0.002     0.869    -0.019     0.192
durasi_hari        0.002     1.000    -0.015    -0.001    -0.083
audiens_ribu       0.869    -0.015     1.000    -0.011     0.183
ctr_persen        -0.019    -0.001    -0.011     1.000     0.244
berhasil           0.192    -0.083     0.183     0.244     1.000

== Rata-rata fitur numerik per kelas target ==
fitur              gagal  berhasil   selisih
anggaran_ribu      257.1     318.5     +61.4
durasi_hari         16.9      15.5      -1.4
audiens_ribu       597.1     748.8    +151.7
ctr_persen           2.4       2.9      +0.5

== Laju keberhasilan per rentang durasi ==
hari 3-9    :  496 kampanye, laju 0.377
hari 10-15  :  445 kampanye, laju 0.497
hari 16-21  :  427 kampanye, laju 0.466
hari 22-30  :  632 kampanye, laju 0.309

== Fitur kategorikal vs target ==
[kanal]
  penelusuran :  585 baris, laju 0.475
  medsos      :  698 baris, laju 0.417
  email       :  310 baris, laju 0.348
  tampilan    :  407 baris, laju 0.307
[segmen]
  akuisisi    :  860 baris, laju 0.492
  reaktivasi  :  404 baris, laju 0.356
  retensi     :  736 baris, laju 0.319
[musim]
  promo       :  777 baris, laju 0.501
  reguler     : 1223 baris, laju 0.338

== Interaksi segmen x musim (laju keberhasilan) ==
musim       promo  reguler
segmen
akuisisi    0.675    0.371
reaktivasi  0.392    0.337
retensi     0.349    0.300
dorongan promo (promo - reguler):
  akuisisi    : +0.305
  reaktivasi  : +0.054
  retensi     : +0.050

== Nilai hilang: dampak pilihan penanganan ==
buang baris ber-hilang : tersisa 1900 baris (5.0% data terbuang)
laju target lengkap    : 0.400
laju target ter-hilang : 0.420
median anggaran_ribu : 246.3 (kandidat imputasi)
median ctr_persen    : 2.4 (kandidat imputasi)

== Temuan utama untuk tahap berikutnya ==
1. anggaran_ribu dan ctr_persen punya nilai hilang -> pipeline perlu
   SimpleImputer; median dipilih karena distribusi miring.
2. audiens_ribu berkorelasi sangat kuat dengan anggaran_ribu
   (Spearman 0.869) - kolinearitas yang aman bagi model pohon.
3. durasi_hari berpuncak di rentang 10-15 hari - hubungan non-monoton
   yang menyulitkan model linear murni.
4. segmen x musim berinteraksi: akuisisi saat promo melonjak.
5. skala fitur campuran (anggaran ratusan ribu vs ctr satuan persen)
   -> StandardScaler wajib untuk model berbasis jarak/gradien.

Bedah alur eksplorasi:

  1. Baris 14–22 (gerbang kualitas): dimensi, duplikasi ID, nilai hilang, dan keseimbangan target diperiksa lebih dulu. Hasilnya bersih: nol duplikat, 60 sel anggaran dan 40 sel CTR hilang.
  2. Baris 24–33 (profil numerik): statistik ringkas plus kemiringan. Anggaran (skew 1,83) dan audiens (2,03) miring ke kanan — alasan median menjadi pengisi nilai hilang, bukan mean yang tertiup ekor distribusi.
  3. Baris 35–42 (korelasi Spearman): audiens-anggaran berkorelasi 0,869 — kolinearitas kuat yang wajar (jangkauan dibeli dengan anggaran) tetapi harus diwaspadai saat membaca koefisien model linear.
  4. Baris 52–57 (rentang durasi): laju keberhasilan per rentang durasi — 0,377 (3–9 hari), 0,497 (10–15), 0,466 (16–21), 0,309 (22–30) — bukti empiris hubungan non-monoton: terlalu pendek dan terlalu lama sama-sama buruk.
  5. Baris 67–74 (interaksi): selisih laju promo–reguler per segmen (+0,305 akuisisi; +0,054 dan +0,050 segmen lainnya) menegaskan musim promo tidak merata dampaknya — interaksi nyata yang harus ditangkap model.
  6. Baris 76–85 (nilai hilang): laju target baris lengkap (0,400) hampir sama dengan baris ber-hilang (0,420) — kehilangan acak, imputasi aman, membuang 5% data tidak perlu.

Lima Temuan yang Menjadi Keputusan Desain #

Eksplorasi menghasilkan lima temuan, dan masing-masing diterjemahkan menjadi satu keputusan teknis pipeline — terjemahan inilah pembeda proyek yang matang dari proyek yang langsung melompat ke pelatihan model:

Temuan eksplorasiKeputusan desain
60 sel anggaran dan 40 sel CTR hilang, tersebar acakSimpleImputer(strategy="median") untuk fitur numerik
Distribusi miring dan skala campuranStandardScaler setelah imputasi
Tiga fitur kategorikal tanpa urutanOneHotEncoder(handle_unknown="ignore")
Durasi non-monoton, interaksi antar fiturkandidat model pohon diuji berdampingan model linear
Audiens berkorelasi 0,869 dengan anggarankedua fitur dipertahankan; koefisien linear dibaca hati-hati

Lima keputusan tersebut menanti wujud kodenya. Bagian 2: Pipeline Preprocessing merakitnya menjadi satu ColumnTransformer di dalam Pipeline scikit-learn — objek tunggal yang menelan DataFrame mentah, mengimputasi, menskala, meng-encode, lalu siap dilatih maupun dipakai inferensi tanpa kebocoran data.

Referensi Resmi #