Chapter sebelumnya menutup dengan janji yang belum ditepati: pertanyaan intervensi bisa dijawab, tetapi tidak dengan membaca korelasi lebih hati-hati. Jawabannya dimulai dari cara kita mendefinisikan efek. Selama “efek program upgrade” didefinisikan sebagai selisih dua rata-rata grup, pembahasan akan terus berputar di sekitar komposisi grup. Kerangka potential outcomes — dikembangkan Jerzy Neyman untuk eksperimen pertanian dan dirumuskan ulang Donald Rubin untuk statistik sosial — menggeser definisi itu ke tempat yang seharusnya: perbandingan dua keadaan pada satuan yang sama. Chapter ini membangun kosakata lengkap kerangka tersebut, membuktikan masalah fundamentalnya lewat data sintetis yang kebenarannya kita ketahui, lalu menyusun tiga syarat identifikasi yang akan menjadi alat ukur untuk semua chapter berikutnya.

Mendefinisikan Efek: Dua Dunia pada Satu Pelanggan #

Untuk setiap pelanggan, potential outcomes mendefinisikan dua besaran: Y(1) adalah belanja bulanan jikalau pelanggan mengikuti program upgrade, dan Y(0) adalah belanja bulanan jikalau pelanggan yang sama tidak mengikuti program. Efek treatment individual (ITE, individual treatment effect) adalah selisih keduanya: Y(1) - Y(0). Karena setiap pelanggan punya nilainya sendiri, efek rata-rata di populasi (ATE, average treatment effect) adalah rata-rata seluruh ITE; efek rata-rata bagi para peserta (ATT, average treatment effect on the treated) adalah rata-rata ITE hanya pada mereka yang benar-benar mengikuti program.

Definisi seperti ini terasa boros — dua dunia untuk satu pertanyaan — tetapi justru borosnya yang jujur. Ketika manajemen bertanya “berapa efek program?”, kerangka ini memaksa kita menjawab: efek untuk siapa — seluruh populasi (ATE) atau para peserta (ATT)? Kedua angka itu berbeda setiap kali efeknya heterogen, dan keputusan bisnis yang berbeda membutuhkan angka yang berbeda: ekspansi program ke seluruh pelanggan butuh ATE, evaluasi program yang sudah berjalan butuh ATT.

Masalah Fundamental Inferensi Kausal #

Berikut letak masalah yang membuat inferensi kausal sulit sejak ratusan tahun: kita tidak pernah mengamati kedua hasil potensial pada satuan yang sama. Pelanggan yang mengikuti program menampilkan Y(1)-nya dan Y(0)-nya hilang; pelanggan yang tidak ikut menampilkan Y(0)-nya dan Y(1)-nya hilang. ITE untuk satu pelanggan mana pun karenanya tidak bisa dihitung dari data — bukan karena alat ukurnya kurang canggih, melainkan karena informasinya memang tidak ada di dunia ini. Holland menyebutnya the fundamental problem of causal inference: inferensi kausal adalah masalah data hilang dengan pola hilang yang tidak bisa diisi dari data itu sendiri.

Satu-satunya jalan adalah mengganti perbandingan yang mustahil (dua dunia pada satu pelanggan) dengan perbandingan yang mungkin (rata-rata dua grup), lalu memastikan perbandingan pengganti itu setara. Syarat kesetaraan itulah yang dirumuskan sebagai tiga asumsi identifikasi:

AsumsiMakna praktisJika dilanggar
KonsistensiY(1) teramati benar-benar hasil treatment yang didefinisikanEfek tercampur versi treatment yang berbeda-beda
ExchangeabilityGrup treatment dan kontrol sebanding di semua kovariatSelisih rata-rata bercampur perbedaan awal grup
PositivitySetiap kombinasi kovariat punya peluang menerima kedua statusGrup pembanding untuk satu subpopulasi tidak ada

Konsistensi terdengar sepele sampai kita menyadari “program upgrade” dalam praktik memuat belasan varian: versi aplikasi berbeda, durasi berbeda, layanan pelanggan berbeda. Exchangeability adalah asumsi berat yang kita bahas tuntas di bawah. Positivity melarang keadaan seperti “pelanggan langganan premium selalu menerima treatment” — grup pembandingnya kosong, sehingga efek untuk subpopulasi itu tidak teridentifikasi dari data observasional.

Kosakata Diagram Kausal: Konfounder dan Collider #

Potential outcomes dan diagram kausal (DAG, directed acyclic graph) adalah dua bahasa untuk masalah yang sama. DAG menggambar variabel sebagai simpul dan pengaruh kausal sebagai panah; dua pola kecil menutup hampir seluruh diskusi praktis:

text
KONFOUNDER: jalur belakang yang terbuka

    ikut_program ─────────────> belanja
         ^                        ^
         |                        |
         └────── kunjungan ───────┘


COLLIDER: simpul dengan dua panah masuk, jalur belakang tertutup

    diskon_musiman ──┐
                     ├──> pembatalan_order
    beban_server  ───┘

Konfounder kunjungan mengirim panah ke keduanya — treatment dan hasil — sehingga membuka jalur asosiasi yang bukan sebab-akibat; menyesuaikan analisis terhadap konfounder berarti menutup jalur itu. Collider berperilaku sebaliknya: pembatalan_order menerima panah dari dua induk yang saling bebas, dan jalur antara kedua induknya tertutup selama collider dibiarkan. Menyesuaikan — atau memfilter data — terhadap sebuah collider justru membuka jalur palsu: menganalisis hanya pesanan yang batal akan menghadirkan korelasi antara diskon musiman dan beban server padahal keduanya tidak berhubungan. Pustaka penemuan struktur kausal seperti algoritme PC membangun DAG semacam ini dari data, tetapi penalarannya — panah mana yang masuk akal secara domain — tetap tanggung jawab analis.

Contoh Kode: Data yang Efek Sejatinya Diketahui #

Kekuatan kerangka ini paling mudah dilihat pada data sintetis, karena pembuat data menyimpan kedua dunia sekaligus. Kita bangun 600 pelanggan dengan konfounder kunjungan yang menggerakkan peluang ikut program dan baseline belanja; efek per pelanggan dibuat heterogen mengikuti 3,0 + 0,8 × (kunjungan − 6) sehingga ATE populasi tepat 3,0:

python
# Kerangka potential outcomes (Neyman-Rubin) pada data sintetis:
# setiap pelanggan punya dua hasil potensial, Y(1) dan Y(0); dunia nyata
# hanya pernah menampilkan satu. Efek program upgrade dibuat diketahui.

import numpy as np
import pandas as pd

rng = np.random.default_rng(42)

n = 600
# Konfounder: frekuensi kunjungan situs per bulan (1-11 kali).
kunjungan = rng.integers(1, 12, n).astype(float)

# Peluang ikut program upgrade naik bersama kunjungan (penugasan tidak acak).
logit = -1.8 + 0.45 * (kunjungan - 4.0)
p_program = 1.0 / (1.0 + np.exp(-logit))
ikut = (rng.random(n) < p_program).astype(int)

# Dua hasil potensial. Efek per pelanggan (ITE) dibuat heterogen:
# 3.0 + 0.8 * (kunjungan - 6), jadi rata-rata populasi tepat 3.0
# karena rata-rata kunjungan adalah 6.
y0 = 80.0 + 12.0 * kunjungan + rng.normal(0.0, 15.0, n)
tau = 3.0 + 0.8 * (kunjungan - 6.0)
y1 = y0 + tau

# Dunia nyata: hanya satu hasil potensial yang pernah teramati.
y_teramati = np.where(ikut == 1, y1, y0)

df = pd.DataFrame({
    "kunjungan": kunjungan,
    "ikut": ikut,
    "y0": y0,
    "y1": y1,
    "tau": tau,
    "y_teramati": y_teramati,
})

# Tampilan tiga pelanggan: baris dunia-nyata menyembunyikan satu kolom.
tabel = df.iloc[[0, 1, 2]][["kunjungan", "ikut", "y0", "y1", "tau", "y_teramati"]].copy()
tabel.index = ["Budi", "Siti", "Dewi"]
print("=== Tiga pelanggan pertama (belanja bulanan, ratus ribu rupiah) ===")
print(tabel.round(1).to_string())
print("Kolom y0/y1/tau hanya terlihat karena ini simulasi; data nyata")
print("hanya memuat kolom y_teramati dan ikut.")

# Pandangan "pembuat data": karena simulasi menyimpan kedua dunia,
# ATE dan ATT sejati bisa dihitung langsung.
ate_sejati = float(df["tau"].mean())
att_sejati = float(df.loc[df.ikut == 1, "tau"].mean())
print(f"\nATE sejati (rata-rata tau seluruh populasi) : {ate_sejati:+.2f}")
print(f"ATT sejati (rata-rata tau peserta program)  : {att_sejati:+.2f}")

# Pandangan "analis data": hanya kolom ikut dan y_teramati yang tersedia.
naif = df.loc[df.ikut == 1, "y_teramati"].mean() - \
    df.loc[df.ikut == 0, "y_teramati"].mean()
print(f"Selisih rata-rata observasional (naif)      : {naif:+.2f}")
print(f"Bias selisih naif terhadap ATE sejati       : {naif - ate_sejati:+.2f}")

# Sumber bias: peserta program memang lebih sering berkunjung sejak awal.
print(f"Rata-rata kunjungan peserta program         : "
      f"{df.loc[df.ikut == 1, 'kunjungan'].mean():.2f}")
print(f"Rata-rata kunjungan non-peserta             : "
      f"{df.loc[df.ikut == 0, 'kunjungan'].mean():.2f}")

# Perspektif data hilang: setiap baris kehilangan tepat satu hasil potensial.
hilang = df.copy()
hilang.loc[hilang.ikut == 1, "y0"] = np.nan
hilang.loc[hilang.ikut == 0, "y1"] = np.nan
print(f"\nKolom y0 yang tak teramati: {hilang['y0'].isna().sum()} dari {n} baris")
print(f"Kolom y1 yang tak teramati: {hilang['y1'].isna().sum()} dari {n} baris")
print("Masalah kausal = masalah data hilang yang tak bisa diisi dari data itu sendiri.")

Output yang Dihasilkan:

text
=== Tiga pelanggan pertama (belanja bulanan, ratus ribu rupiah) ===
      kunjungan  ikut     y0     y1  tau  y_teramati
Budi        1.0     0   83.2   82.2 -1.0        83.2
Siti        9.0     1  187.3  192.7  5.4       192.7
Dewi        8.0     0  180.2  184.8  4.6       180.2
Kolom y0/y1/tau hanya terlihat karena ini simulasi; data nyata
hanya memuat kolom y_teramati dan ikut.

ATE sejati (rata-rata tau seluruh populasi) : +3.01
ATT sejati (rata-rata tau peserta program)  : +4.95
Selisih rata-rata observasional (naif)      : +50.18
Bias selisih naif terhadap ATE sejati       : +47.17
Rata-rata kunjungan peserta program         : 8.43
Rata-rata kunjungan non-peserta             : 4.66

Kolom y0 yang tak teramati: 215 dari 600 baris
Kolom y1 yang tak teramati: 385 dari 600 baris
Masalah kausal = masalah data hilang yang tak bisa diisi dari data itu sendiri.

Mari kita bedah skrip baris demi baris:

  1. Baris 12 (konfounder): rng.integers(1, 12, n) menghasilkan kunjungan bulanan 1–11 kali dengan rata-rata tepat 6 — angka yang membuat rumus efek menghasilkan ATE populasi 3,0.
  2. Baris 15–17 (penugasan tidak acak): peluang ikut program mengikuti fungsi logistik yang naik bersama kunjungan; pelanggan rajin jauh lebih mungkin masuk program — persis pola observasional di dunia nyata.
  3. Baris 22–24 (dua hasil potensial): y0 adalah baseline belanja, tau adalah efek individual, y1 = y0 + tau. Ketiganya disimpan untuk seluruh 600 pelanggan — sesuatu yang tidak pernah dimiliki data nyata.
  4. Baris 27 (y_teramati): np.where(ikut == 1, y1, y0) memilih satu dunia per pelanggan; inilah seluruh informasi yang tersedia bagi analis nyata.
  5. Baris 39–44 (tabel tiga pelanggan): tiga baris pertama diberi nama Budi, Siti, Dewi, dengan seluruh kolom terlihat hanya karena ini simulasi.
  6. Baris 48–57 (dua pandangan): pandangan pembuat data menghitung ATE/ATT sejati langsung dari kolom tau; pandangan analis menghitung selisih rata-rata observasional dari y_teramati saja.

Angka-angkanya memberi cerita yang utuh. Tabel tiga pelanggan menunjukkan heterogenitas: efek bagi Budi (kunjungan 1 kali) adalah −1,0, bagi Siti (9 kali) +5,4 — ITE memang berbeda-beda orang. ATE sejati +3,01 dan ATT sejati +4,95: para peserta program adalah pelanggan yang efeknya di atas rata-rata, sehingga ATT > ATE. Lalu selisih observasional naif membengkak menjadi +50,18 — lebih dari enam belas kali lipat ATE sejati. Baris rata-rata kunjungan menjelaskan seluruhnya: peserta program rata-rata berkunjung 8,43 kali sebulan, non-peserta 4,66 kali, dan dengan baseline 12 per kunjungan, selisih 3,77 kunjungan menyumbang ±45 satuan sebelum efek program ikut dihitung. Perspektif data hilang menutup demonstrasi: 215 baris kehilangan y0, 385 baris kehilangan y1, dan tidak satu pun nilai hilang itu bisa direkonstruksi dari kolom yang tersisa.

Dari tiga asumsi identifikasi, exchangeability adalah yang paling sulit dipenuhi lewat penyesuaian statistik semata — setiap kovariat yang tidak terukur menyisakan jalur belakang terbuka. Kabar baiknya, ada satu mekanisme yang membeli asumsi itu sekaligus untuk seluruh kovariat terukur maupun tak terukur: pengacakan. Menyerahkan status treatment kepada pelempar koin membuat grup treatment dan kontrol sebanding secara pembuktian, bukan sekadar secara asumsi. Mekanisme itu — eksperimen terkontrol, A/B testing — adalah materi Chapter A/B Testing.

Referensi Resmi #