Penutup Section interpretabilitas meninggalkan satu benang yang belum terselesaikan: feature importance, partial dependence, dan surrogate model semuanya menggambarkan asosiasi — fitur mana yang paling berguna untuk memprediksi — tetapi tidak satu pun menyatakan fitur mana yang benar-benar menggerakkan hasil. Sebuah dashboard bisa menunjukkan bahwa pengguna wishlist berbelanja 67 satuan lebih banyak tanpa menjawab pertanyaan manajemen yang sebenarnya: apakah fitur wishlist itu sendiri yang menaikkan belanja? Chapter ini membedah jurang antara korelasi dan kausalitas lewat tiga mekanisme konkret yang sama-sama mampu menghasilkan korelasi kuat tanpa hubungan sebab-akibat, semuanya dibangun dari data sintetis yang kebenarannya kita ketahui pasti. Pada akhirnya kita juga akan menegaskan kapan prediksi asosiasional justru tetap cukup — karena tidak semua pertanyaan adalah pertanyaan kausal.
Dua Pertanyaan yang Berbeda: Prediksi vs Intervensi #
Setiap analisis data pada dasarnya menjawab salah satu dari dua pertanyaan. Pertanyaan pertama adalah prediksi: “jika kondisi berjalan seperti biasa, apa yang akan terjadi pada pelanggan ini?” Pertanyaan kedua adalah intervensi: “jika kita mengubah satu hal — mengirim voucher, meluncurkan fitur, menaikkan harga — berapa perubahan yang akan terjadi?” Supervised machine learning menjawab pertanyaan pertama dengan sangat baik; model churn, model prediksi permintaan, dan model skor kredit semuanya bekerja pada asumsi bahwa masa depan menyerupai masa lalu.
Pertanyaan kedua lebih sulit karena jawabannya tidak bisa dibaca langsung dari data historis. Ketika tim produk bertanya “apakah program upgrade meningkatkan belanja?”, data yang tersedia hanya memuat pelanggan yang memilih ikut program dan yang tidak — dua kelompok yang sudah berbeda sebelum program dimulai. Membaca selisih rata-rata kedua kelompok sebagai “efek program” adalah kesalahan termahal dalam analisis data komersial.
| Aspek | Pertanyaan prediksi | Pertanyaan intervensi |
|---|---|---|
| Bentuk pertanyaan | “Berapa peluang pelanggan ini berhenti berlangganan?” | “Berapa peluangnya jika kita kirim voucher Rp50.000?” |
| Alat utama | Supervised learning | Eksperimen acak, metode kausal |
| Data historis | Cukup, selama pola stabil | Tidak cukup tanpa syarat tambahan |
| Galat umum | Membaca skor sebagai “penyebab” | Membaca selisih observasional sebagai “efek” |
Tiga mekanisme berikut menjelaskan mengapa asosiasi observasional bisa muncul tanpa sebab-akibat — dan ketiganya akan kita buktikan dengan angka, bukan hanya dengan slogan “korelasi bukan kausalitas”.
| Mekanisme | Sumber korelasi | Contoh di chapter ini |
|---|---|---|
| Konfounder | Variabel ketiga menggerakkan dua-duanya | Pendapatan menggerakkan pemakaian wishlist dan belanja |
| Tren bersama | Dua deret tumbuh mengikuti waktu yang sama | Kunjungan situs dan anggota komunitas |
| Agregasi (paradoks Simpson) | Komposisi grup yang tidak seimbang | Versi keranjang baru terpusat di segmen pengguna aktif |
Konfounder: Variabel Ketiga yang Menggerakkan Dua Hal Sekaligus #
Dugaan “pelanggan yang memakai fitur wishlist membeli lebih banyak” bisa benar sebagai asosiasi dan keliru sebagai kausalitas pada saat yang sama. Penjelasan alternatifnya ringkas: pelanggan berpendapatan tinggi punya dorongan belanja besar, jadi mereka memakai wishlist dan sekalian berbelanja banyak — wishlist hanyalah penanda, bukan pemicu. Variabel seperti pendapatan inilah yang disebut konfounder: variabel yang mendahului sekaligus memengaruhi kedua variabel yang kita bandingkan.
Untuk mengukur seberapa jauh pembacaan naif bisa tersesat, kita bangun data sintetis yang kebenarannya diketahui pasti. Efek kausal wishlist terhadap belanja sengaja dibuat tepat nol; pendapatan menggerakkan keduanya secara terpisah:
# Dua mekanisme penghasil korelasi semu: konfounder bersama dan tren waktu
# bersama. Efek kausal wishlist terhadap belanja sengaja dibuat NOL, dan dua
# deret yang tumbuh bersama sengaja tidak dihubungkan satu sama lain.
import numpy as np
import pandas as pd
import statsmodels.formula.api as smf
rng = np.random.default_rng(42)
# --- Bagian 1: konfounder pendapatan menggerakkan dua variabel sekaligus ---
n = 1200
pendapatan = rng.normal(9.0, 2.5, n).clip(3.0, 18.0) # juta rupiah/bulan
p_wishlist = 1.0 / (1.0 + np.exp(-(pendapatan - 9.0) / 2.0)) # makin kaya makin pakai
pakai_wishlist = (rng.random(n) < p_wishlist).astype(int)
belanja = 60.0 + 28.0 * pendapatan + rng.normal(0.0, 18.0, n) # efek wishlist = 0
df = pd.DataFrame({
"pendapatan": pendapatan,
"pakai_wishlist": pakai_wishlist,
"belanja": belanja,
})
naif = df.loc[df.pakai_wishlist == 1, "belanja"].mean() - \
df.loc[df.pakai_wishlist == 0, "belanja"].mean()
korelasi = np.corrcoef(df["pakai_wishlist"], df["belanja"])[0, 1]
print("=== Bagian 1: konfounder pendapatan (efek wishlist yang benar = 0) ===")
print(f"selisih rata-rata belanja (pakai - tidak pakai): {naif:+.1f} satuan")
print(f"korelasi pakai_wishlist vs belanja : {korelasi:.3f}")
print(f"rata-rata pendapatan pengguna wishlist : "
f"{df.loc[df.pakai_wishlist == 1, 'pendapatan'].mean():.2f} juta")
print(f"rata-rata pendapatan pengguna tanpa wishlist : "
f"{df.loc[df.pakai_wishlist == 0, 'pendapatan'].mean():.2f} juta")
# Kelompokkan berdasarkan rentang pendapatan: stratifikasi kasar mengecilkan
# selisih, tetapi di dalam rentang yang lebar pengguna wishlist masih sedikit
# lebih kaya sehingga sisa konfounding tertinggal.
df["rentang_pendapatan"] = pd.cut(
df["pendapatan"], bins=[3, 7, 9, 11, 18],
labels=["3-7", "7-9", "9-11", "11-18"])
print("\nSelisih belanja (pakai - tidak pakai) per rentang pendapatan:")
for label, grup in sorted(df.groupby("rentang_pendapatan", observed=True)):
selisih = grup.loc[grup.pakai_wishlist == 1, "belanja"].mean() - \
grup.loc[grup.pakai_wishlist == 0, "belanja"].mean()
print(f" pendapatan {label:6s} juta: {selisih:+7.1f} satuan "
f"({len(grup)} pelanggan)")
# Penyesuaian regresi dengan nilai pendapatan kontinu: karena model persis
# mengikuti bentuk data (linear dalam pendapatan), koefisien wishlist
# kembali ke nilai sebenarnya, nol.
model = smf.ols("belanja ~ pendapatan + pakai_wishlist", data=df).fit()
koef = model.params["pakai_wishlist"]
batas = model.conf_int().loc["pakai_wishlist"]
print("\nRegresi belanja ~ pendapatan + pakai_wishlist:")
print(f" koefisien wishlist : {koef:+.1f} satuan")
print(f" interval kepercayaan 95%: [{batas[0]:+.1f}, {batas[1]:+.1f}] "
f"(memuat nol)")
# --- Bagian 2: tren waktu bersama (common trend) ---
t = np.arange(360) # minggu ke-0 s.d. ke-359
kunjungan_situs = 420.0 + 3.2 * t + rng.normal(0.0, 18.0, 360)
anggota_komunitas = 150.0 + 1.1 * t + rng.normal(0.0, 9.0, 360)
korelasi_mentah = np.corrcoef(kunjungan_situs, anggota_komunitas)[0, 1]
korelasi_perubahan = np.corrcoef(np.diff(kunjungan_situs), np.diff(anggota_komunitas))[0, 1]
print("\n=== Bagian 2: dua deret yang tumbuh mengikuti waktu ===")
print(f"korelasi nilai mingguan (raw) : {korelasi_mentah:.3f}")
print(f"korelasi perubahan mingguan (differencing) : {korelasi_perubahan:.3f}")
print("Kedua deret dipicu pertumbuhan toko yang sama, bukan satu sama lain.")Output yang Dihasilkan:
=== Bagian 1: konfounder pendapatan (efek wishlist yang benar = 0) ===
selisih rata-rata belanja (pakai - tidak pakai): +67.1 satuan
korelasi pakai_wishlist vs belanja : 0.470
rata-rata pendapatan pengguna wishlist : 10.12 juta
rata-rata pendapatan pengguna tanpa wishlist : 7.74 juta
Selisih belanja (pakai - tidak pakai) per rentang pendapatan:
pendapatan 11-18 juta: +15.4 satuan (232 pelanggan)
pendapatan 3-7 juta: +11.2 satuan (259 pelanggan)
pendapatan 7-9 juta: +6.7 satuan (327 pelanggan)
pendapatan 9-11 juta: -0.0 satuan (372 pelanggan)
Regresi belanja ~ pendapatan + pakai_wishlist:
koefisien wishlist : -0.2 satuan
interval kepercayaan 95%: [-2.5, +2.1] (memuat nol)
=== Bagian 2: dua deret yang tumbuh mengikuti waktu ===
korelasi nilai mingguan (raw) : 0.996
korelasi perubahan mingguan (differencing) : -0.029
Kedua deret dipicu pertumbuhan toko yang sama, bukan satu sama lain.Mari kita telaah bagian-bagian yang membentuk cerita ini:
- Baris 13–16 (konstruksi data):
pendapatandiambil dari distribusi normal rata-rata Rp9 juta; peluang memakai wishlist mengikuti fungsi logistik yang naik bersama pendapatan. Baris terakhir adalah kunci kejujuran eksperimen — wishlist tidak muncul di rumus belanja sama sekali, jadi efek kausalnya memang nol. - Baris 24–26 (pembacaan naif): selisih rata-rata belanja kedua kelompok plus korelasi Pearson antara penanda pemakaian dan belanja.
- Baris 39–47 (stratifikasi kasar): pelanggan dikelompokkan ke empat rentang pendapatan, lalu selisih belanja dihitung ulang di dalam tiap rentang.
- Baris 52–58 (penyesuaian regresi):
smf.olsmemasukkan pendapatan kontinu; koefisienpakai_wishlistbeserta interval kepercayaannya menjadi pembacaan yang telah disesuaikan. - Baris 61–66 (mekanisme kedua): dua deret mingguan yang tumbuh mengikuti waktu — bahan bagian tren bersama di bawah.
Hasil Bagian 1 layak dibaca pelan-pelan. Pembacaan naif melaporkan selisih +67,1 satuan dan korelasi 0,470 — angka yang cukup meyakinkan untuk sebuah rapat keputusan fitur. Namun rata-rata pendapatan kedua kelompok adalah Rp10,12 juta lawan Rp7,74 juta; dengan 28 satuan per Rp1 juta pendapatan, selisih itu sudah menyumbang hampir seluruh “efek”. Stratifikasi per rentang pendapatan mengecilkan selisih menjadi +6,7 sampai +15,4 satuan — di rentang sempit 9–11 juta selisihnya −0,0. Penyesuaian regresi dengan nilai pendapatan kontinu menutup kisahnya: koefisien wishlist −0,2 dengan interval kepercayaan [−2,5; +2,1] yang memuat nol, tepat seperti kebenaran yang kita tanam di data.
Tren Bersama: Korelasi 0,996 Tanpa Hubungan Sebab-Akibat #
Bagian 2 skrip yang sama membangun dua deret mingguan: kunjungan situs dan jumlah anggota komunitas produk. Keduanya tumbuh linear mengikuti waktu selama 360 minggu dengan derau yang saling bebas — tidak ada rumus yang menghubungkan keduanya. Korelasi nilai mingguannya mencapai 0,996, angka yang di banyak dashboard langsung dianggap hubungan sangat kuat. Begitu selisih mingguan (differencing) menghapus tren bersama, korelasi jatuh ke −0,029: hampir nol, sebagaimana seharusnya dua deret yang memang tidak berhubungan.
Pelajarannya: setiap dua besaran yang tumbuh bersama sebuah bisnis — pendapatan, jumlah pengguna, volume ulasan — akan berkorelasi tinggi tanpa perlu saling memengaruhi. Waktu adalah konfounder yang paling sering terlupa karena tidak pernah tercatat sebagai kolom fitur. Differencing adalah heuristik yang berguna, bukan uji kausalitas formal; ia menghapus tren linear bersama, tetapi keputusan apakah tren itu kausal tetap harus dijawab dengan penalaran domain atau eksperimen.
Paradoks Simpson: Ketika Agregat Membalik Arah #
Mekanisme ketiga paling dramatis. Katakanlah Toko Ceria meluncurkan desain keranjang belanja baru secara bertahap — bukan acak: tim produk memprioritaskan pengguna aktif karena lebih responsif terhadap undangan uji coba. Setelah beberapa minggu, tingkat konversi dianalisis. Pembacaan agregat menyatakan versi baru menang; pembacaan per segmen menyatakan versi lama menang di kedua segmen. Keduanya dihitung dari data yang sama:
# Paradoks Simpson pada data sintetis Toko Ceria: desain keranjang belanja
# baru diluncurkan bertahap (bukan acak), lalu tingkat konversi dibaca
# dari dua sudut pandang -- agregat penuh vs per segmen pengguna.
import numpy as np
import pandas as pd
rng = np.random.default_rng(42)
n_aktif, n_baru = 3000, 3000
segmen = np.array(["aktif"] * n_aktif + ["baru"] * n_baru)
# Peluncuran bertahap: pengguna aktif jauh lebih mungkin mendapat versi baru.
p_versi = np.where(segmen == "aktif", 0.60, 0.12)
versi = np.where(rng.random(n_aktif + n_baru) < p_versi, "baru", "lama")
# Laju konversi per kombinasi segmen-versi: versi LAMA selalu lebih baik.
p_konversi = np.select(
[
(segmen == "aktif") & (versi == "lama"),
(segmen == "aktif") & (versi == "baru"),
(segmen == "baru") & (versi == "lama"),
],
[0.24, 0.19, 0.08],
default=0.04,
)
konversi = (rng.random(n_aktif + n_baru) < p_konversi).astype(int)
df = pd.DataFrame({"segmen": segmen, "versi": versi, "konversi": konversi})
def arah_teks(selisih):
if selisih > 0:
return "versi baru lebih baik"
return "versi lama lebih baik"
print("=== Pembacaan 1: agregat penuh (6.000 pengguna) ===")
agg = df.groupby("versi")["konversi"].agg(["count", "sum", "mean"])
for nama, baris in agg.iterrows():
print(f"versi {nama:4s}: {int(baris['count']):5d} pengguna, "
f"{int(baris['sum']):4d} konversi, laju {baris['mean']:.3f}")
selisih_agregat = agg.loc["baru", "mean"] - agg.loc["lama", "mean"]
print(f"selisih (baru - lama): {selisih_agregat:+.3f} -> {arah_teks(selisih_agregat)}")
print("\n=== Pembacaan 2: per segmen pengguna ===")
per_segmen = df.groupby(["segmen", "versi"])["konversi"].mean().unstack()
selisih_segmen = {}
for nama_segmen, baris in per_segmen.iterrows():
selisih = baris["baru"] - baris["lama"]
selisih_segmen[nama_segmen] = selisih
print(f"segmen {nama_segmen:5s}: lama {baris['lama']:.3f} vs baru {baris['baru']:.3f} "
f"(selisih {selisih:+.3f}) -> {arah_teks(selisih)}")
print("\n=== Komposisi siapa yang mendapat versi baru ===")
porsi = df.groupby("segmen")["versi"].apply(lambda s: (s == "baru").mean())
for nama_segmen, nilai in porsi.items():
print(f"segmen {nama_segmen:5s}: {nilai:.1%} pengguna menerima versi baru")
print("\nArah kesimpulan (positif = versi baru lebih baik):")
print(f" agregat penuh: {selisih_agregat:+.3f}")
for nama_segmen, nilai in sorted(selisih_segmen.items()):
print(f" segmen {nama_segmen:5s}: {nilai:+.3f}")
flip = (selisih_agregat > 0) and all(v < 0 for v in selisih_segmen.values())
print(f"Kesimpulan agregat berlawanan arah dengan kedua segmen: {flip}")Output yang Dihasilkan:
=== Pembacaan 1: agregat penuh (6.000 pengguna) ===
versi baru: 2178 pengguna, 341 konversi, laju 0.157
versi lama: 3822 pengguna, 514 konversi, laju 0.134
selisih (baru - lama): +0.022 -> versi baru lebih baik
=== Pembacaan 2: per segmen pengguna ===
segmen aktif: lama 0.246 vs baru 0.178 (selisih -0.067) -> versi lama lebih baik
segmen baru : lama 0.085 vs baru 0.047 (selisih -0.038) -> versi lama lebih baik
=== Komposisi siapa yang mendapat versi baru ===
segmen aktif: 60.5% pengguna menerima versi baru
segmen baru : 12.1% pengguna menerima versi baru
Arah kesimpulan (positif = versi baru lebih baik):
agregat penuh: +0.022
segmen aktif: -0.067
segmen baru : -0.038
Kesimpulan agregat berlawanan arah dengan kedua segmen: TrueBedah kodenya menegaskan letak mekanismenya:
- Baris 14–15 (penugasan tidak acak): peluang menerima versi baru 0,60 untuk pengguna aktif dan hanya 0,12 untuk pengguna baru — inilah komposisi yang tidak seimbang.
- Baris 18–27 (laju konversi): versi lama lebih baik di kedua segmen (0,24 lawan 0,19 pada segmen aktif; 0,08 lawan 0,04 pada segmen baru).
- Baris 32–35 (fungsi
arah_teks): arah kesimpulan ditentukan dari tanda selisih — sehingga baris terakhir skrip benar-benar memverifikasi pembalikan, bukan sekadar mengklaimnya. - Baris 39–53 (dua pembacaan): agregasi penuh membandingkan 2.178 lawan 3.822 pengguna tanpa memilah segmen; pembacaan per segmen membandingkan di dalam tiap segmen.
- Baris 65–66 (verifikasi pembalikan):
flipbernilaiTruehanya bila agregat positif sementara kedua segmen negatif.
Angkanya menunjukkan pembalikan yang tegas. Agregat: versi baru 0,157 lawan 0,134, selisih +0,022 — versi baru tampak unggul. Segmen aktif: 0,178 lawan 0,246, selisih −0,067. Segmen baru: 0,047 lawan 0,085, selisih −0,038. Penyebabnya murni komposisi: 60,5% pengguna aktif menerima versi baru sementara hanya 12,1% pengguna baru yang menerimanya, dan pengguna aktif jauh lebih mudah mengonversi. Rata-rata agregat versi baru terbawa ke atas oleh segmen yang memang rajin membeli — bukan oleh desain keranjannya. Segmen di sini berperan sebagai konfounder, dan pembalikan arah semacam ini dikenal sebagai paradoks Simpson.
Kapan Asosiasi Cukup dan Kapan Harus Kausal #
Setelah tiga mekanisme di atas, mudah sekali menyimpulkan bahwa seluruh analisis observasional tidak layak percaya. Itu juga keliru. Ketika model dipakai untuk memperkirakan keadaan tanpa mengubah apa pun — meramalkan permintaan pekan depan, memprioritaskan antrean tinjauan transaksi, memperkirakan nilai belanja pelanggan — asosiasi yang stabil justru tepat sasaran, dan menghapus fitur berkorelasi kuat demi “kebersihan kausal” hanya menurunkan performa. Masalah muncul tepat ketika keputusan yang diambil mengubah variabel masukan model itu sendiri: mengirim voucher karena model churn menandai pelanggan, lalu menilai keberhasilannya dari data historis yang tidak pernah memuat kondisi “voucher telah dikirim”.
Kabar baiknya: pertanyaan intervensi bisa dibuat presisi dan bisa dijawab — tetapi bukan dengan membaca korelasi lebih hati-hati, melainkan dengan kerangka yang mendefinisikan “efek” sebagai perbandingan dua keadaan yang sama-sama terdefinisi sekalipun hanya satu yang benar-benar terjadi. Kerangka itu, potential outcomes ala Neyman–Rubin, adalah materi Chapter Framework Potential Outcomes.
Referensi Resmi #
- numpy —
numpy.corrcoef— API resmi koefisien korelasi Pearson yang dipakai pada kedua bagian demo. - pandas —
pandas.DataFrame.groupby— API resmi agregasi yang menjadi mesin pembacaan agregat vs per segmen. - statsmodels —
statsmodels.formula.api.ols— API resmi regresi formula yang dipakai untuk penyesuaian konfounder.