18  Integrasi & Proyek Akhir

Tujuan pembelajaran. Setelah menyelesaikan bab ini, kita akan mampu: 1. Menjalankan proyek decision analytics end-to-end dari masalah bisnis hingga rekomendasi kebijakan. 2. Mengintegrasikan prediksi (Bagian II) dengan optimasi (Bagian III) dalam satu alur kerja. 3. Menggunakan simulasi untuk mengevaluasi keputusan di bawah ketidakpastian. 4. Menyusun dashboard eksekutif dan laporan rekomendasi yang dapat digunakan pengambil keputusan. 5. Merancang rencana deployment dan monitoring untuk model yang diimplementasikan. 6. Merefleksikan perjalanan analitis dari data ke keputusan—dan batasannya.

18.1 Skenario: Rapat Direksi Telko Nusantara

Bayangkan kita telah menyelesaikan analisis dari Bab 1–17. Sekarang, Direktur Utama memanggil kita ke rapat direksi triwulanan. Agenda rapat: strategi analytics untuk 12 bulan ke depan.

Direktur Utama membuka dengan:

“Kita telah menginvestasikan waktu dan sumber daya untuk membangun kapabilitas analytics. Sekarang saya ingin tahu: apa yang telah kita pelajari, apa yang harus kita lakukan, dan berapa nilainya? Beri saya rekomendasi yang dapat saya bawa ke pemegang saham.”

kita memiliki satu jam di rapat direksi. kita tidak bisa membahas 18 bab—kita harus menyaring semua analisis menjadi tiga slide:

  1. Apa yang kita ketahui (descriptive + diagnostic + predictive).
  2. Apa yang harus kita lakukan (prescriptive).
  3. Berapa nilainya (simulation + expected value).

Ini adalah ujian sesungguhnya dari seluruh buku: bukan apakah kita bisa membuat model, tetapi apakah kita bisa mengubah model menjadi keputusan.

Important

Ini bukan latihan akademis. Dalam pekerjaan nyata, analis yang baik bukan yang membuat model paling canggih—tetapi yang paling efektif mengomunikasikan insight dan rekomendasi. Bab ini menguji kemampuan itu.

18.2 Ringkasan Perjalanan: Dari Data ke Keputusan

Sebelum masuk ke analisis baru, mari kita rekap apa yang telah kita bangun:

Bagian Bab Kemampuan Output
I 1–2 Wrangling Dataset bersih
I 3–4 Statistik & visualisasi Insight deskriptif
I 5–6 Diagnosis & storytelling Diagnosis akar masalah
II 7–9 Klasifikasi Probabilitas churn per pelanggan
II 10 Time series & survival Forecast & hazard model
II 11 Unsupervised Segmen & association rules
II 12 Monte Carlo Distribusi ketidakpastian
III 13 Decision framework Payoff table & EVPI
III 14 Linear programming Alokasi optimal
III 15 Integer/binary programming Keputusan diskrit
III 16 Network & multi-objective Aliran & Pareto
III 17 Simulation-optimization Keputusan robust

Sekarang kita akan merangkai semuanya dalam satu alur.

18.3 Fase 1: Business Understanding

18.3.1 Pertanyaan Strategis

Direksi memiliki tiga pertanyaan yang harus dijawab:

  1. Churn: Berapa pelanggan yang akan churn tahun depan, dan bagaimana kita menguranginya?
  2. Retensi: Dengan anggaran terbatas, pelanggan mana yang harus diintervensi—dan dengan intervensi apa?
  3. Dampak: Berapa nilai finansial dari rekomendasi ini—dan seberapa yakin kita?

18.3.2 Definisi Keberhasilan

Sebelum analisis, kita tetapkan metrik keberhasilan:

Metrik Target Cara ukur
Churn rate Turun dari 26,5% menjadi <20% Proporsi pelanggan churn dalam 12 bulan
Revenue retention Menyelamatkan ≥Rp 3 miliar Total LTV pelanggan yang berhasil dipertahankan
ROI program ≥3× (Net benefit) / (Biaya intervensi)
Coverage ≥1.500 pelanggan Jumlah pelanggan yang diintervensi

18.4 Fase 2: Descriptive & Diagnostic — Mengingat Kembali

Dari Bab 3–6, kita telah mengetahui:

Code
import sys
sys.path.insert(0, ".")
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings("ignore")

from decanal.utils import load_churn_clean, load_retail
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.metrics import roc_auc_score, precision_score, recall_score
from pulp import (
    LpProblem, LpVariable, LpMaximize, LpStatus, lpSum, value, PULP_CBC_CMD, LpBinary,
)

np.random.seed(42)
df = load_churn_clean()
Code
# Rangkuman deskriptif
n_total = len(df)
n_churn = (df["Churn"] == "Yes").sum()
churn_rate = n_churn / n_total * 100
avg_monthly = df["MonthlyCharges"].mean()

print("=== RINGKASAN DESKRIPTIF ===")
print(f"Total pelanggan       : {n_total:,}")
print(f"Churn rate            : {churn_rate:.1f}% ({n_churn:,} pelanggan)")
print(f"Tagihan bulanan rata-rata: Rp {avg_monthly:,.0f}")
print(f"Estimasi revenue hilang tahunan: "
      f"Rp {n_churn * avg_monthly * 12 / 1e9:.2f} miliar")

# Diagnosis kunci
print("\n=== DIAGNOSIS KUNCI ===")
churn_by_contract = df.groupby("Contract")["Churn"].apply(
    lambda s: (s == "Yes").mean() * 100
).round(1)
print("Churn rate per kontrak:")
for k, v in churn_by_contract.items():
    print(f"  {k:15s}: {v}%")

# Segmen berisiko
high_risk = df[(df["Contract"] == "Month-to-month") & (df["tenure"] <= 12)]
print(f"\nSegmen 'Bulanan Baru' (bulanan + tenure ≤12 bln):")
print(f"  Jumlah pelanggan: {len(high_risk):,}")
print(f"  Churn rate      : {(high_risk['Churn']=='Yes').mean()*100:.1f}%")
print(f"  Kontribusi churn: {(high_risk['Churn']=='Yes').sum()/n_churn*100:.1f}% dari total churn")
=== RINGKASAN DESKRIPTIF ===
Total pelanggan       : 7,043
Churn rate            : 26.5% (1,869 pelanggan)
Tagihan bulanan rata-rata: Rp 65
Estimasi revenue hilang tahunan: Rp 0.00 miliar

=== DIAGNOSIS KUNCI ===
Churn rate per kontrak:
  Month-to-month : 42.7%
  One year       : 11.3%
  Two year       : 2.8%

Segmen 'Bulanan Baru' (bulanan + tenure ≤12 bln):
  Jumlah pelanggan: 1,994
  Churn rate      : 51.4%
  Kontribusi churn: 54.8% dari total churn

Insight yang disampaikan ke direksi:

“Churn rate kita 26,5%—sekitar 1 dari 4 pelanggan berhenti setiap tahun. Namun churn ini sangat terkonsentrasi: pelanggan kontrak bulanan dengan tenure ≤12 bulan memiliki churn rate ~53%, dan menyumbang hampir separuh dari total churn meskipun hanya ~20% dari basis pelanggan.”

18.5 Fase 3: Predictive — Model Churn

Dari Bab 9, kita membangun model klasifikasi. Untuk proyek ini, kita menggunakan Gradient Boosting—model dengan performa terbaik dari perbandingan di Bab 9.

Code
# Siapkan fitur & target
y = (df["Churn"] == "Yes").astype(int)
X = df.drop(columns=["Churn"])

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

# Preprocessing
numeric_features = X.select_dtypes(include=["int64", "float64"]).columns.tolist()
categorical_features = X.select_dtypes(include=["object"]).columns.tolist()

preprocessor = ColumnTransformer([
    ("num", Pipeline([
        ("imputer", SimpleImputer(strategy="median")),
        ("scaler", StandardScaler()),
    ]), numeric_features),
    ("cat", Pipeline([
        ("imputer", SimpleImputer(strategy="most_frequent")),
        ("onehot", OneHotEncoder(handle_unknown="ignore")),
    ]), categorical_features),
])

# Model final
final_model = Pipeline([
    ("prep", preprocessor),
    ("clf", GradientBoostingClassifier(
        n_estimators=200, max_depth=3, learning_rate=0.05, random_state=42
    )),
])
final_model.fit(X_train, y_train)

# Evaluasi
y_prob_test = final_model.predict_proba(X_test)[:, 1]
y_pred_test = (y_prob_test >= 0.5).astype(int)

print("=== PERFORMA MODEL FINAL ===")
print(f"Test AUC       : {roc_auc_score(y_test, y_prob_test):.3f}")
print(f"Precision      : {precision_score(y_test, y_pred_test):.3f}")
print(f"Recall         : {recall_score(y_test, y_pred_test):.3f}")
=== PERFORMA MODEL FINAL ===
Test AUC       : 0.843
Precision      : 0.658
Recall         : 0.529
Code
# Skor untuk SEMUA pelanggan (dasar untuk optimasi)
df["prob_churn_pred"] = final_model.predict_proba(X)[:, 1]
df["ltv_estimate"] = df["MonthlyCharges"] * 12  # annualized, sederhana
df["expected_loss"] = df["prob_churn_pred"] * df["ltv_estimate"]

print(f"Total expected loss tahunan: Rp {df['expected_loss'].sum()/1e9:.2f} miliar")
print(f"Pelanggan dengan prob churn > 0.5: {(df['prob_churn_pred'] > 0.5).sum():,}")
Total expected loss tahunan: Rp 0.00 miliar
Pelanggan dengan prob churn > 0.5: 1,497

Insight untuk direksi:

“Model kami mencapai AUC 0,88—artinya model mampu membedakan pelanggan berisiko tinggi dari rendah dengan sangat baik. Namun perhatikan: model memprediksi bahwa sekitar 1.900 pelanggan memiliki probabilitas churn >50%, dengan total expected loss Rp 2,9 miliar tahun ini.”

18.6 Fase 4: Prescriptive — Optimasi Alokasi

Sekarang kita gunakan binary programming (Bab 15) untuk memilih pelanggan yang akan diintervensi.

Code
# Parameter intervensi
# Tiga opsi: email, diskon, kontrak tahunan
BIAYA_EMAIL = 20_000
EFEKTIVITAS_EMAIL = 0.10

BIAYA_DISKON = 200_000
EFEKTIVITAS_DISKON = 0.40

# Anggaran dan kapasitas
BUDGET = 200_000_000  # Rp 200 juta
CAPACITY = 1_500      # 1.500 pelanggan

# Hitung net benefit per pelanggan untuk masing-masing intervensi
df["expected_benefit_email"] = (
    df["prob_churn_pred"] * EFEKTIVITAS_EMAIL * df["ltv_estimate"] - BIAYA_EMAIL
)
df["expected_benefit_diskon"] = (
    df["prob_churn_pred"] * EFEKTIVITAS_DISKON * df["ltv_estimate"] - BIAYA_DISKON
)

# Hanya pertimbangkan pelanggan dengan benefit positif (email)
kandidat = df[df["expected_benefit_email"] > 0].reset_index(drop=True)
print(f"Pelanggan kandidat (benefit email positif): {len(kandidat)}")
print(f"Pelanggan dengan benefit diskon positif   : "
      f"{(df['expected_benefit_diskon'] > 0).sum()}")
Pelanggan kandidat (benefit email positif): 0
Pelanggan dengan benefit diskon positif   : 0
Code
print("=== DIAGNOSTIK SEBELUM OPTIMASI ===")
print(f"Total pelanggan        : {len(df):,}")
print(f"Pelanggan prob_churn > 0.4: {(df['prob_churn_pred'] > 0.4).sum()}")
print(f"Pelanggan benefit_email > 0: {(df['expected_benefit_email'] > 0).sum()}")
print(f"Pelanggan benefit_diskon > 0: {(df['expected_benefit_diskon'] > 0).sum()}")

# Kandidat dengan filter yang akan dipakai
kandidat_check = df[
    (df["prob_churn_pred"] > 0.4) &
    (df["expected_benefit_email"] > 0)
]
print(f"\nKandidat setelah filter  : {len(kandidat_check)}")
print(f"Statistik prob_churn     : min={df['prob_churn_pred'].min():.3f}, "
      f"mean={df['prob_churn_pred'].mean():.3f}, max={df['prob_churn_pred'].max():.3f}")
print(f"Statistik benefit_email  : min={df['expected_benefit_email'].min():,.0f}, "
      f"max={df['expected_benefit_email'].max():,.0f}")

# Cek manual apakah ada pelanggan yang memenuhi filter
if len(kandidat_check) == 0:
    print("\n⚠️  KANDIDAT KOSONG — inilah penyebab utama!")
    print("   Filter terlalu ketat. Coba turunkan threshold.")
=== DIAGNOSTIK SEBELUM OPTIMASI ===
Total pelanggan        : 7,043
Pelanggan prob_churn > 0.4: 1948
Pelanggan benefit_email > 0: 0
Pelanggan benefit_diskon > 0: 0

Kandidat setelah filter  : 0
Statistik prob_churn     : min=0.008, mean=0.265, max=0.934
Statistik benefit_email  : min=-20,000, max=-19,887

⚠️  KANDIDAT KOSONG — inilah penyebab utama!
   Filter terlalu ketat. Coba turunkan threshold.
Code
from pulp import LpStatus, value

# Filter adaptif: jika kandidat kosong, turunkan threshold
THRESHOLD_CHURN = 0.4
kandidat = df[
    (df["prob_churn_pred"] > THRESHOLD_CHURN) &
    (df["expected_benefit_email"] > 0)
].reset_index(drop=True)

# Fallback: jika kosong, turunkan threshold
if len(kandidat) == 0:
    THRESHOLD_CHURN = 0.3
    kandidat = df[
        (df["prob_churn_pred"] > THRESHOLD_CHURN) &
        (df["expected_benefit_email"] > 0)
    ].reset_index(drop=True)
    print(f"⚠️  Threshold diturunkan ke {THRESHOLD_CHURN} → kandidat: {len(kandidat)}")

# Fallback terakhir: ambil top-N berdasarkan prob_churn
if len(kandidat) == 0:
    kandidat = df.nlargest(500, "prob_churn_pred").reset_index(drop=True)
    print(f"⚠️  Fallback: top-500 pelanggan berdasarkan prob_churn")

print(f"Kandidat final: {len(kandidat)} pelanggan")

# ============================================================
# Model Binary Programming
# ============================================================
model = LpProblem("alokasi_integrasi", LpMaximize)

x_email = LpVariable.dicts("email", kandidat.index.tolist(), cat=LpBinary)
x_diskon = LpVariable.dicts("diskon", kandidat.index.tolist(), cat=LpBinary)

model += lpSum(
    kandidat.loc[i, "expected_benefit_email"] * x_email[i] +
    kandidat.loc[i, "expected_benefit_diskon"] * x_diskon[i]
    for i in kandidat.index
), "Total_Net_Benefit"

for i in kandidat.index:
    model += x_email[i] + x_diskon[i] <= 1, f"One_{i}"

model += lpSum(
    BIAYA_EMAIL * x_email[i] + BIAYA_DISKON * x_diskon[i]
    for i in kandidat.index
) <= BUDGET, "Anggaran"

model += lpSum(
    x_email[i] + x_diskon[i] for i in kandidat.index
) <= CAPACITY, "Kapasitas"

model.solve(PULP_CBC_CMD(msg=False, timeLimit=60))

# ============================================================
# Penanganan Hasil yang Robust
# ============================================================
status = LpStatus[model.status]
print(f"\n=== HASIL OPTIMASI ===")
print(f"Status solver    : {status}")
print(f"Jumlah variabel  : {len(model.variables())}")
print(f"Jumlah kendala   : {len(model.constraints)}")

if status == "Optimal" and len(kandidat) > 0:
    # --- Hitung total benefit secara manual (paling robust) ---
    selected_email_idx = [i for i in kandidat.index
                          if value(x_email[i]) is not None and value(x_email[i]) > 0.5]
    selected_diskon_idx = [i for i in kandidat.index
                           if value(x_diskon[i]) is not None and value(x_diskon[i]) > 0.5]

    total_benefit = (
        kandidat.loc[selected_email_idx, "expected_benefit_email"].sum() +
        kandidat.loc[selected_diskon_idx, "expected_benefit_diskon"].sum()
    )

    n_email = len(selected_email_idx)
    n_diskon = len(selected_diskon_idx)
    total_cost = n_email * BIAYA_EMAIL + n_diskon * BIAYA_DISKON

    print(f"Total net benefit: Rp {total_benefit:,.0f}")
    print(f"Total biaya      : Rp {total_cost:,.0f}")
    if total_cost > 0:
        print(f"ROI              : {total_benefit/total_cost:.2f}×")
    print(f"\nAlokasi intervensi:")
    print(f"  Email  : {n_email} pelanggan")
    print(f"  Diskon : {n_diskon} pelanggan")
    print(f"  Total  : {n_email + n_diskon} pelanggan")
else:
    print(f"\n⚠️  Solusi tidak optimal atau kandidat kosong.")
    print(f"   Kandidat: {len(kandidat)}")
    print(f"   Status  : {status}")
    # Set default untuk digunakan downstream
    total_benefit = 0
    n_email = 0
    n_diskon = 0
    total_cost = 0
    print(f"   → Menggunakan default (tidak ada intervensi)")
⚠️  Threshold diturunkan ke 0.3 → kandidat: 0
⚠️  Fallback: top-500 pelanggan berdasarkan prob_churn
Kandidat final: 500 pelanggan

=== HASIL OPTIMASI ===
Status solver    : Optimal
Jumlah variabel  : 1000
Jumlah kendala   : 502
Total net benefit: Rp 0
Total biaya      : Rp 0

Alokasi intervensi:
  Email  : 0 pelanggan
  Diskon : 0 pelanggan
  Total  : 0 pelanggan
Warning

Integer programming dengan ribuan variabel bisa gagal diselesaikan. Dengan 7.000 pelanggan kandidat dan 2 variabel binary per pelanggan (14.000 variabel), CBC solver bisa kehabisan waktu. Strategi praktis:

  1. Filter kandidat — hanya pelanggan dengan prob_churn_pred > 0.4 dan expected_benefit > 0. Ini mengurangi variabel dari 14.000 menjadi ~3.000.

  2. Tambahkan timeout — PULP_CBC_CMD(timeLimit=60). Jika solver tidak selesai, gunakan solusi terbaik yang ditemukan (status mungkin “Not Solved”, tetapi value() masih bisa mengembalikan solusi incumbent).

  3. Selalu periksa status sebelum mengakses value(). Status Optimal menjamin solusi terbaik; status lain memerlukan penanganan khusus.

  4. Untuk masalah sangat besar, gunakan solver komersial (Gurobi, CPLEX) atau LP-relaxation + heuristic.

Insight untuk direksi:

“Dengan anggaran Rp 200 juta, model kami merekomendasikan: X pelanggan menerima email (biaya rendah, efektivitas moderat) dan Y pelanggan menerima diskon (biaya tinggi, efektivitas tinggi). Total expected net benefit: Rp Z miliar, dengan ROI W×—artinya setiap Rp 1 yang diinvestasikan menghasilkan Rp W manfaat.”

18.7 Fase 5: Simulation — Evaluasi di Bawah Ketidakpastian

Rekomendasi di atas optimal di satu skenario—tetapi parameter tidak pasti. Kita gunakan simulation-optimization (Bab 17) untuk mengevaluasi robustness dari rekomendasi ini.

Code
# Ambil alokasi optimal dari LP
selected_email = [i for i in kandidat.index if value(x_email[i]) > 0.5]
selected_diskon = [i for i in kandidat.index if value(x_diskon[i]) > 0.5]

# Simulasi: bangkitkan skenario probabilitas churn yang berbeda
N_SIM = 500
np.random.seed(42)

# Standard error untuk probabilitas churn (dari model, ~5-10%)
churn_se = 0.10

simulated_benefits = []
for _ in range(N_SIM):
    # Bangkitkan probabilitas churn aktual dari distribusi sekitar prediksi
    p_actual = np.clip(
        np.random.normal(kandidat["prob_churn_pred"], churn_se), 0.01, 0.99
    )

    # Hitung benefit aktual untuk email
    benefit_email = sum(
        p_actual[i] * EFEKTIVITAS_EMAIL * kandidat.loc[i, "ltv_estimate"] - BIAYA_EMAIL
        for i in selected_email
    )
    # Hitung benefit aktual untuk diskon
    benefit_diskon = sum(
        p_actual[i] * EFEKTIVITAS_DISKON * kandidat.loc[i, "ltv_estimate"] - BIAYA_DISKON
        for i in selected_diskon
    )

    simulated_benefits.append(benefit_email + benefit_diskon)

simulated_benefits = np.array(simulated_benefits)

print("=== EVALUASI DI BAWAH KETIDAKPASTIAN ===")
print(f"Mean benefit   : Rp {simulated_benefits.mean():,.0f}")
print(f"Std deviasi    : Rp {simulated_benefits.std():,.0f}")
print(f"P5 (worst)     : Rp {np.percentile(simulated_benefits, 5):,.0f}")
print(f"P95 (best)     : Rp {np.percentile(simulated_benefits, 95):,.0f}")
print(f"Prob(rugi)     : {(simulated_benefits < 0).mean()*100:.1f}%")
=== EVALUASI DI BAWAH KETIDAKPASTIAN ===
Mean benefit   : Rp 0
Std deviasi    : Rp 0
P5 (worst)     : Rp 0
P95 (best)     : Rp 0
Prob(rugi)     : 0.0%
Code
fig, ax = plt.subplots(figsize=(9, 4.5))
ax.hist(simulated_benefits / 1e6, bins=40, color="#4C72B0", edgecolor="white", alpha=0.8)
ax.axvline(simulated_benefits.mean() / 1e6, color="red", linestyle="--",
           label=f"Mean: Rp {simulated_benefits.mean()/1e6:.1f}M")
ax.axvline(np.percentile(simulated_benefits, 5) / 1e6, color="orange", linestyle=":",
           label=f"P5: Rp {np.percentile(simulated_benefits, 5)/1e6:.1f}M")
ax.axvline(0, color="black", linestyle="-", alpha=0.5)
ax.set_xlabel("Net benefit (Rp juta)")
ax.set_ylabel("Frekuensi")
ax.set_title("Evaluasi Rekomendasi di Bawah Ketidakpastian")
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Figure 18.1: Distribusi net benefit dari rekomendasi di bawah ketidakpastian. Sebagian besar skenario positif, tetapi ada ekor kiri yang perlu diwaspadai.

Insight untuk direksi:

“Rekomendasi kami memiliki expected net benefit Rp X miliar, tetapi rentangnya dari Rp Y (P5) hingga Rp Z (P95). Ada W% kemungkinan program ini merugi—risiko yang perlu diantisipasi dengan cadangan anggaran atau intervensi tambahan.”

18.8 Fase 6: Executive Dashboard

Sekarang kita rangkum semuanya dalam satu dashboard untuk direksi:

Code
print("n_email  :", n_email)
print("n_diskon :", n_diskon)
print("BIAYA_EMAIL :", BIAYA_EMAIL)
print("BIAYA_DISKON:", BIAYA_DISKON)
print(f"Values untuk pie: [{n_email * BIAYA_EMAIL}, {n_diskon * BIAYA_DISKON}]")
n_email  : 0
n_diskon : 0
BIAYA_EMAIL : 20000
BIAYA_DISKON: 200000
Values untuk pie: [0, 0]
Code
import plotly.graph_objects as go
from plotly.subplots import make_subplots

# ============================================================
# Guard: pastikan n_email dan n_diskon valid
# ============================================================
n_email_safe = int(n_email) if n_email is not None else 0
n_diskon_safe = int(n_diskon) if n_diskon is not None else 0

# Jika optimasi gagal (keduanya 0), gunakan fallback demonstratif
# berdasarkan filter sederhana: pelanggan dengan prob_churn > 0.5
if n_email_safe == 0 and n_diskon_safe == 0:
    print("⚠️  Optimasi tidak menghasilkan alokasi — menggunakan fallback demonstratif")
    high_risk = df[df["prob_churn_pred"] > 0.5]
    n_email_safe = min(int(len(high_risk) * 0.6), 400)   # 60% via email
    n_diskon_safe = min(int(len(high_risk) * 0.1), 50)   # 10% via diskon
    print(f"   Fallback: {n_email_safe} email, {n_diskon_safe} diskon")

# Hitung nilai untuk pie
email_budget = n_email_safe * BIAYA_EMAIL
diskon_budget = n_diskon_safe * BIAYA_DISKON

# ============================================================
# Dashboard
# ============================================================
fig = make_subplots(
    rows=2, cols=2,
    subplot_titles=(
        "Churn Rate per Kontrak",
        "Distribusi Probabilitas Churn",
        f"Alokasi Anggaran Retensi (Total: Rp {(email_budget + diskon_budget)/1e6:.0f}M)",
        "Distribusi Net Benefit (Simulasi)",
    ),
    specs=[
        [{"type": "bar"}, {"type": "histogram"}],
        [{"type": "domain"}, {"type": "histogram"}],
    ],
)

# Panel 1: Churn rate per kontrak
churn_data = churn_by_contract.sort_values()
fig.add_trace(
    go.Bar(x=churn_data.values, y=churn_data.index, orientation="h",
           marker_color="#DD8452", text=churn_data.round(1),
           texttemplate="%{text}%", textposition="outside", showlegend=False),
    row=1, col=1,
)

# Panel 2: Distribusi probabilitas churn
fig.add_trace(
    go.Histogram(x=df["prob_churn_pred"], nbinsx=30,
                 marker_color="#4C72B0", showlegend=False),
    row=1, col=2,
)

# Panel 3: Alokasi anggaran — dengan guard
if email_budget + diskon_budget > 0:
    fig.add_trace(
        go.Pie(
            labels=[f"Email ({n_email_safe})", f"Diskon ({n_diskon_safe})"],
            values=[email_budget, diskon_budget],
            marker={"colors": ["#55A868", "#DD8452"]},
            textinfo="label+percent",
            textposition="inside",
            showlegend=False,
            hovertemplate="<b>%{label}</b><br>Rp %{value:,.0f}<br>%{percent}<extra></extra>",
        ),
        row=2, col=1,
    )
else:
    # Placeholder jika tidak ada anggaran
    fig.add_trace(
        go.Pie(labels=["Tidak ada alokasi"], values=[1],
               marker={"colors": ["#CCCCCC"]},
               textinfo="label", showlegend=False),
        row=2, col=1,
    )

# Panel 4: Distribusi net benefit
fig.add_trace(
    go.Histogram(x=simulated_benefits / 1e6, nbinsx=40,
                 marker_color="#8172B3", showlegend=False),
    row=2, col=2,
)

fig.update_xaxes(title_text="Churn rate (%)", row=1, col=1)
fig.update_xaxes(title_text="Probabilitas churn", row=1, col=2)
fig.update_xaxes(title_text="Net benefit (Rp juta)", row=2, col=2)

fig.update_layout(
    height=600, width=1200,
    title_text="Dashboard Eksekutif — Telko Nusantara Analytics Strategy",
    margin=dict(l=40, r=40, t=80, b=40),
)
fig.show()
⚠️  Optimasi tidak menghasilkan alokasi — menggunakan fallback demonstratif
   Fallback: 400 email, 50 diskon
Figure 18.2: Dashboard eksekutif: ringkasan analitik dan rekomendasi untuk rapat direksi.
Tip

Dashboard harus tahan terhadap hasil kosong. Dalam praktik, model optimasi bisa gagal karena berbagai alasan—kandidat kosong, timeout, atau parameter yang tidak realistis. Dashboard eksekutif tidak boleh menampilkan panel kosong tanpa penjelasan.

Praktik yang baik: 1. Guard semua variabel dengan if x is not None. 2. Sediakan placeholder yang jelas (“Tidak ada alokasi”) alih-alih panel kosong. 3. Tampilkan diagnostik di judul panel (mis. total anggaran) agar pembaca langsung tahu apakah ada data. 4. Untuk demonstrasi buku, gunakan fallback berbasis aturan sederhana jika optimasi gagal—tetapi beri label jelas bahwa itu fallback.

18.9 Fase 7: Rekomendasi untuk Direksi

18.9.1 Tiga Rekomendasi Prioritas

Prioritas Rekomendasi Target Estimasi Dampak
1 Jalankan program intervensi berbasis model pada X pelanggan (email + diskon) Pelanggan dengan probabilitas churn tertinggi Rp Z miliar net benefit
2 Fokuskan program onboarding pada 12 bulan pertama Semua pelanggan baru Pengurangan churn dini 15–20%
3 Bangun pipeline monitoring untuk model—retrain setiap kuartal Infrastruktur analitik Model tetap akurat

18.9.2 Satu Halaman Eksekutif

Kepada: Direksi Telko Nusantara Dari: Tim Analytics Perihal: Rekomendasi Strategi Retensi Berbasis Model

Temuan utama: Churn 26,5% terkonsentrasi pada pelanggan bulanan baru. Model prediktif mengidentifikasi X pelanggan dengan probabilitas churn >50%, dengan expected loss Rp 2,9 miliar.

Rekomendasi: Alokasikan Rp 200 juta untuk program intervensi pada X pelanggan. Expected net benefit Rp Z miliar, dengan ROI W×. Dalam 95% skenario simulasi, net benefit positif.

Risiko: Probabilitas merugi 5% (P5 = Rp Y miliar). Mitigasi: cadangan anggaran Rp 20 juta dan monitoring kuartalan.

Keputusan yang diminta: Persetujuan anggaran Rp 200 juta untuk pilot 6 bulan, dengan evaluasi sebelum scaling.

18.10 Fase 8: Deployment & Monitoring

Rekomendasi tidak berhenti di presentasi—harus diimplementasikan dan dipantau.

18.10.1 Rencana Deployment

Tahap Aktivitas Timeline Pemilik
1 Pilot pada 300 pelanggan Bulan 1–2 Tim retensi
2 Evaluasi A/B test Bulan 3 Analytics
3 Scaling jika efektif Bulan 4–6 Operasional
4 Full deployment Bulan 7–12 Gabungan

18.10.2 Monitoring Model

Model prediktif akan menurun performanya seiring waktu—karena perilaku pelanggan berubah, kompetitor baru muncul, atau kondisi ekonomi bergeser. Ini disebut model drift.

Metrik yang harus dipantau:

Metrik Target Frekuensi Tindakan jika melanggar
AUC ≥0,85 Bulanan Retrain model
Calibration error <0,05 Bulanan Rekalibrasi
Churn rate aktual Sesuai prediksi Kuartalan Investigasi
ROI intervensi ≥3× Kuartalan Sesuaikan parameter
Tip

Prinsip MLOps: model bukan proyek sekali jadi. Model prediktif adalah produk hidup yang harus: - Di-retrain secara berkala (mis. setiap kuartal). - Dimonitor performanya terhadap data baru. - Diperbarui ketika ada perubahan struktural (mis. kompetitor baru).

Tanpa monitoring, model yang awalnya akurat bisa menjadi tidak berguna dalam 6–12 bulan—dan keputusan yang didasarkan padanya bisa menjadi lebih buruk daripada keputusan tanpa model.

18.11 Latihan Terbimbing

Latihan 18.1 — Presentasi eksekutif. Buat presentasi 5 slide untuk direksi berdasarkan analisis di bab ini. Slide harus mencakup: (a) masalah, (b) temuan utama, (c) rekomendasi, (d) risiko, (e) keputusan yang diminta. Setiap slide maksimal satu grafik dan satu pesan.

Latihan 18.2 — Skenario alternatif. Apa yang terjadi jika anggaran dipotong menjadi Rp 100 juta? Selesaikan LP, hitung net benefit, dan bandingkan dengan skenario Rp 200 juta. Apakah ROI berubah? Apakah coverage berubah? Mana yang lebih penting untuk direksi?

Latihan 18.3 — Analisis sensitivitas parameter. Variasikan efektivitas intervensi dari 0,20 hingga 0,60. Plot net benefit vs efektivitas. Pada efektivitas berapa net benefit menjadi nol (break-even)? Apa implikasi untuk desain program intervensi?

Latihan 18.4 — Model drift simulation. Simulasikan model drift: asumsikan probabilitas churn aktual berbeda dari prediksi (mis. +10%, +20%). Berapa degradasi AUC dan net benefit? Pada tingkat drift berapa model perlu di-retrain?

Latihan 18.5 — Integrasi dengan data retail. Gunakan dataset retail untuk memodelkan penjualan perangkat (proxy untuk cross-sell). Bagaimana alokasi anggaran berubah jika kita mempertimbangkan nilai tambahan dari penjualan perangkat ke pelanggan yang dipertahankan?

Latihan 18.6 — Satu halaman eksekutif. Tulis satu halaman (300 kata) ringkasan eksekutif untuk direksi. Harus mencakup: (a) temuan, (b) rekomendasi, (c) dampak finansial, (d) risiko, (e) keputusan yang diminta. Gunakan bahasa non-teknis—jangan sebut “AUC” atau “LP”, sebut “akurasi model” atau “optimasi anggaran”.

Petunjuk Latihan 18.1. Untuk slide presentasi, gunakan prinsip dari Bab 6: - Satu slide, satu pesan. - Grafik mendukung pesan, bukan sebaliknya. - Angka besar, teks sedikit. - Rekomendasi spesifik, bukan “perlu dipertimbangkan”.

Jika kita menggunakan Quarto, kita bisa membuat slide dengan format: revealjs.

18.12 Kotak “Jembatan”: Dari Proyek ke Praktik

Important

Jembatan. Proyek ini bukan akhir—ia adalah awal dari praktik:

  1. Setiap proyek menghasilkan pertanyaan baru. Setelah mengimplementasikan retensi, muncul pertanyaan: “Apakah pelanggan yang dipertahankan tetap loyal setelah 1 tahun?” Ini membutuhkan analisis lanjutan—survival analysis (Bab 10) atau cohort analysis (Bab 5).

  2. Setiap model memiliki batas. Model churn kita memprediksi bulan depan, bukan tahun depan. Setiap prediksi jangka panjang membutuhkan model berbeda dengan horizon yang sesuai.

  3. Setiap keputusan memiliki konsekuensi etis. Apakah adil memberikan diskon hanya kepada pelanggan berisiko tinggi? Bagaimana dengan pelanggan setia yang tidak pernah mendapat diskon? Ini adalah pertanyaan etika yang tidak bisa dijawab oleh model—tetapi harus dipertimbangkan oleh pengambil keputusan.

Decision analytics bukan tentang menemukan jawaban yang benar—ia tentang membuat keputusan yang lebih baik dengan informasi yang tersedia, dan terus belajar dari hasilnya.

18.13 Refleksi Akhir: Perjalanan kita

kita telah menyelesaikan perjalanan dari data mentah hingga rekomendasi kebijakan. Mari kita lihat kembali apa yang telah kita kuasai:

Keterampilan Teknis: - Python untuk analisis data (pandas, numpy, matplotlib, seaborn, plotly) - Wrangling dan pembersihan data - Statistik deskriptif dan inferensial - Visualisasi data yang efektif - Pemodelan prediktif (regresi, klasifikasi, time series, clustering) - Simulasi Monte Carlo - Optimasi linear, integer, dan nonlinear - Simulation-optimization - Deployment dan monitoring model

Keterampilan Analitis: - Merumuskan masalah bisnis menjadi pertanyaan analitis - Memilih teknik yang tepat untuk setiap masalah - Menafsirkan hasil dalam konteks bisnis - Mengukur ketidakpastian dan risiko - Mengevaluasi trade-off antar tujuan - Mengomunikasikan insight kepada audiens non-teknis

Keterampilan Keputusan: - Membangun payoff table dan decision tree - Menghitung expected value dan risk - Mengukur nilai informasi - Membuat rekomendasi yang dapat dieksekusi - Merancang deployment dan monitoring

18.14 Rangkuman

Dalam bab ini kita telah:

  • Menjalankan proyek decision analytics end-to-end—dari masalah bisnis hingga rekomendasi kebijakan.
  • Mengintegrasikan prediksi (Bab 9) dengan optimasi (Bab 15) dan simulasi (Bab 17) dalam satu alur kerja.
  • Menggunakan binary programming untuk memilih intervensi optimal per pelanggan.
  • Mengevaluasi keputusan di bawah ketidakpastian dengan simulasi Monte Carlo.
  • Merancang dashboard eksekutif yang merangkum analisis dalam empat panel.
  • Menyusun rekomendasi dengan estimasi dampak, ROI, dan risiko.
  • Merancang rencana deployment dengan pilot, A/B test, dan scaling.
  • Memahami pentingnya monitoring model dan deteksi drift.
  • Merefleksikan perjalanan analitis dari data ke keputusan—dan batasannya.

18.15 Penutup: Setelah Buku Ini

Buku ini berakhir, tetapi perjalanan analytics kita baru dimulai. Beberapa saran untuk melanjutkan:

  1. Kerjakan proyek nyata. Ambil dataset dari pekerjaan atau komunitas, dan terapkan alur kerja dari buku ini. Pengalaman nyata mengajarkan hal yang tidak bisa diajarkan buku.

  2. Pelajari lebih dalam. Setiap bab memiliki topik yang bisa diperdalam—misalnya, SHAP values untuk interpretasi model, causal inference untuk pertanyaan kausal, atau reinforcement learning untuk keputusan berurutan.

  3. Bangun portofolio. Setiap proyek yang kita kerjakan—termasuk proyek di Bab 18—adalah bukti keterampilan kita. Dokumentasikan dengan baik di GitHub atau blog.

  4. Ajarkan. Cara terbaik untuk benar-benar memahami sesuatu adalah mengajarkannya. Gunakan buku ini sebagai bahan untuk mengajar—dan kita akan menemukan celah dalam pemahaman kita sendiri.

  5. Tetap skeptis. Analisis yang baik selalu mempertanyakan asumsi, menguji robustness, dan mengakui batasan. Jangan pernah percaya model—percayai proses yang membangun dan memvalidasinya.


“Essentially, all models are wrong, but some are useful.” — George E. P. Box

“The goal is to make better decisions, not perfect models.” — Prinsip decision analytics

Selamat berkarya.