---
jupyter: decanal
---
# Linear Programming: Optimasi di Bawah Kendala {#sec-linear-programming}
::: {.callout-note appearance="simple"}
**Tujuan pembelajaran.** Setelah menyelesaikan bab ini, kita akan mampu:
1. Memformulasikan masalah keputusan sebagai model **linear programming (LP)** dengan variabel keputusan, fungsi tujuan, dan kendala.
2. Menyelesaikan LP dua variabel secara **grafis** untuk membangun intuisi.
3. Mengimplementasikan LP dengan **PuLP** dan menafsirkan solusinya.
4. Membedakan **solusi optimal** dari **solusi yang layak**, dan mengenali kasus **infeasible** serta **unbounded**.
5. Melakukan **analisis sensitivitas** dan menafsirkan **shadow price**.
6. Menerapkan LP pada dua kasus nyata: **alokasi anggaran retensi** dan **optimasi bauran produksi**.
7. Memahami kapan LP cukup, dan kapan kita membutuhkan **integer programming** (Bab 15).
:::
## Mengapa Linear Programming?
Di Bab 13 kita mempelajari cara membuat keputusan untuk **satu** pelanggan. Tetapi keputusan bisnis nyata selalu melibatkan **banyak keputusan sekaligus** dengan **sumber daya terbatas**:
- 1.000 pelanggan berisiko, tetapi anggaran hanya cukup untuk menghubungi 200.
- 20 produk yang bisa diproduksi, tetapi kapasitas mesin terbatas.
- 5 gudang yang bisa melayani 50 toko, dengan biaya transportasi berbeda-beda.
**Linear programming** adalah teknik optimasi paling fundamental untuk masalah seperti ini. Ia memaksimalkan atau meminimalkan **fungsi tujuan linear** di bawah **kendala linear**.
::: {.callout-important}
**Tiga alasan LP tetap relevan di era machine learning:**
1. **Solusi optimal terjamin.** Tidak seperti heuristic atau machine learning, LP menjamin solusi terbaik (global optimum) dalam waktu yang wajar.
2. **Interpretasi jelas.** Setiap kendala memiliki **shadow price** yang menunjukkan berapa nilai marginal dari satu unit tambahan sumber daya—wawasan yang tidak bisa diberikan model black-box.
3. **Fondasi untuk semua optimasi.** Integer programming (Bab 15), network flow (Bab 16), dan banyak teknik lain adalah perluasan dari LP. Memahami LP berarti memahami fondasi seluruh Bagian III.
:::
## Anatomi Model Linear Programming
Setiap model LP memiliki **tiga komponen**:
| Komponen | Pertanyaan | Contoh |
|---|---|---|
| **Variabel keputusan** | Apa yang bisa kita kendalikan? | Berapa pelanggan di segmen A yang diintervensi? |
| **Fungsi tujuan** | Apa yang ingin kita maksimalkan/minimalkan? | Total expected revenue yang diselamatkan |
| **Kendala** | Apa batasannya? | Anggaran, kapasitas tim, waktu |
### Asumsi Linear Programming
LP valid hanya jika **tiga asumsi** terpenuhi:
1. **Proporsionalitas.** Kontribusi setiap variabel keputusan terhadap tujuan dan kendala bersifat **linear** (berbanding lurus).
2. **Aditivitas.** Total kontribusi adalah **jumlah** dari kontribusi individual—tidak ada interaksi.
3. **Divisibilitas.** Variabel keputusan dapat berupa **pecahan** (continuous).
::: {.callout-warning}
**Asumsi divisibilitas sering dilanggar di dunia nyata.** Kita tidak bisa mengintervensi 2,7 pelanggan—harus bilangan bulat. Jika variabel harus bilangan bulat, kita membutuhkan **integer programming** (Bab 15). Namun, untuk jumlah besar, solusi LP sering cukup dekat dengan solusi integer yang dapat dibulatkan—dan LP jauh lebih cepat diselesaikan.
:::
## Contoh Klasik: Optimasi Bauran Produksi
Mari mulai dengan contoh klasik yang membangun intuisi. Sebuah pabrik memproduksi **dua produk** dengan sumber daya terbatas.
### Formulasi
**Variabel keputusan:**
- $x_1$ = jumlah produk A yang diproduksi
- $x_2$ = jumlah produk B yang diproduksi
**Fungsi tujuan:** maksimalkan profit
$$\max \, Z = 40x_1 + 30x_2$$
(Produk A memberi profit Rp 40.000/unit; B memberi Rp 30.000/unit)
**Kendala:**
- Waktu mesin: $2x_1 + x_2 \leq 100$ jam
- Tenaga kerja: $x_1 + 2x_2 \leq 80$ jam
- Bahan baku: $x_1 + x_2 \leq 60$ kg
- Non-negatif: $x_1, x_2 \geq 0$
### Solusi Grafis
Untuk dua variabel, kita bisa memvisualisasikan **feasible region** (daerah layak) dan mencari titik yang memaksimalkan fungsi tujuan.
```{python}
#| label: setup
import sys
sys.path.insert(0, ".")
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings("ignore")
# Konfigurasi visual
plt.rcParams["figure.dpi"] = 100
COLOR_PRIMARY = "#4C72B0"
COLOR_ACCENT = "#DD8452"
```
```{python}
#| label: fig-lp-graphical
#| fig-cap: "Solusi grafis LP. Daerah abu-abu adalah feasible region; garis putus-putus adalah kurva isoprofit. Solusi optimal berada di titik sudut (corner) daerah layak."
#| fig-width: 9
#| fig-height: 6
fig, ax = plt.subplots(figsize=(9, 6))
# Batas sumbu
x_max = 70
x = np.linspace(0, x_max, 400)
# Kendala 1: 2x1 + x2 <= 100 → x2 = 100 - 2x1
c1 = 100 - 2 * x
# Kendala 2: x1 + 2x2 <= 80 → x2 = (80 - x1) / 2
c2 = (80 - x) / 2
# Kendala 3: x1 + x2 <= 60 → x2 = 60 - x1
c3 = 60 - x
# Plot kendala
ax.plot(x, c1, label="2x₁ + x₂ = 100 (mesin)", color=COLOR_PRIMARY, linewidth=2)
ax.plot(x, c2, label="x₁ + 2x₂ = 80 (tenaga kerja)", color=COLOR_ACCENT, linewidth=2)
ax.plot(x, c3, label="x₁ + x₂ = 60 (bahan baku)", color="#55A868", linewidth=2)
# Feasible region (irisan semua kendala)
feasible = np.minimum(np.minimum(c1, c2), c3)
feasible = np.maximum(feasible, 0)
feasible[x > 60] = 0 # batasi x1
ax.fill_between(x, 0, feasible, alpha=0.2, color="gray", label="Feasible region")
# Titik sudut yang mungkin
corners = [
(0, 0),
(0, 40), # dari x1+2x2=80 → x2=40
(30, 30), # irisan x1+x2=60 dan x1+2x2=80? cek: 30+60=90>80, jadi bukan
(40, 20), # 2x1+x2=100 & x1+x2=60 → x1=40, x2=20
(50, 0),
]
# Sebenarnya hitung ulang titik sudut yang benar
# Irisan 2x1+x2=100 dan x1+x2=60: x1=40, x2=20
# Irisan 2x1+x2=100 dan x1+2x2=80: selesaikan
# 2x1+x2=100 → x2=100-2x1
# x1+2(100-2x1)=80 → x1+200-4x1=80 → -3x1=-120 → x1=40, x2=20
# Irisan x1+x2=60 dan x1+2x2=80: 60-x2+2x2=80 → x2=20, x1=40
# Jadi (40,20) adalah irisan ketiga kendala
corners = [(0, 0), (0, 40), (40, 20), (50, 0)]
for cx, cy in corners:
ax.plot(cx, cy, "o", color="red", markersize=8, zorder=5)
# Hitung Z di titik ini
z = 40 * cx + 30 * cy
ax.annotate(f"({cx},{cy})\nZ={z}", (cx, cy), textcoords="offset points",
xytext=(8, 8), fontsize=8)
# Garis isoprofit optimal
# Z optimal di (40,20) = 40*40 + 30*20 = 2200
# garis: 40x1 + 30x2 = 2200 → x2 = (2200 - 40x1)/30
iso = (2200 - 40 * x) / 30
ax.plot(x, iso, "r--", alpha=0.7, label="Isoprofit optimal (Z=2200)")
ax.set_xlim(0, 65)
ax.set_ylim(0, 65)
ax.set_xlabel("x₁ (unit Produk A)")
ax.set_ylabel("x₂ (unit Produk B)")
ax.set_title("Solusi Grafis Linear Programming")
ax.legend(loc="upper right", fontsize=8)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
```
**Temuan:** Solusi optimal adalah **$x_1 = 40, x_2 = 20$** dengan profit **$Z = 2.200$**. Titik ini berada di **irisan** kendala mesin dan tenaga kerja—kedua kendala ini **aktif** (binding), sementara kendala bahan baku **tidak aktif** (ada slack).
::: {.callout-tip}
**Teorema fundamental LP.** Untuk masalah LP dengan feasible region yang **terbatas** dan **convex**, solusi optimal **selalu** berada di salah satu **titik sudut** (corner point) feasible region. Inilah mengapa metode simpleks hanya perlu memeriksa titik-titik sudut—tidak perlu memeriksa seluruh ruang solusi.
Ini juga menjelaskan mengapa LP bisa diselesaikan dengan cepat bahkan untuk ribuan variabel: jumlah titik sudut tumbuh secara polinomial, bukan eksponensial.
:::
## Implementasi dengan PuLP
Sekarang kita implementasikan masalah yang sama dengan **PuLP**—library optimasi Python yang memungkinkan kita mendefinisikan LP secara deklaratif.
```{python}
#| label: install-pulp
#| eval: false
# Jika belum terinstal:
# pip install pulp
```
```{python}
#| label: pulp-basic
from pulp import (
LpProblem, LpVariable, LpMaximize, LpMinimize, LpStatus,
lpSum, value, PULP_CBC_CMD
)
# Definisikan model
model = LpProblem("produksi_optimal", LpMaximize)
# Variabel keputusan
x1 = LpVariable("x1_produkA", lowBound=0, cat="Continuous")
x2 = LpVariable("x2_produkB", lowBound=0, cat="Continuous")
# Fungsi tujuan
model += 40 * x1 + 30 * x2, "Total_Profit"
# Kendala
model += 2 * x1 + x2 <= 100, "Waktu_Mesin"
model += x1 + 2 * x2 <= 80, "Tenaga_Kerja"
model += x1 + x2 <= 60, "Bahan_Baku"
# Selesaikan
model.solve(PULP_CBC_CMD(msg=False))
print(f"Status: {LpStatus[model.status]}")
print(f"Produk A (x1): {value(x1):.1f} unit")
print(f"Produk B (x2): {value(x2):.1f} unit")
print(f"Total Profit : Rp {value(model.objective):,.0f} ribu")
```
```{python}
#| label: pulp-slack
# Cek slack (sisa kapasitas) di setiap kendala
print("\nSlack per kendala (0 = aktif/binding, >0 = ada sisa):")
for name, constraint in model.constraints.items():
print(f" {name:15s}: slack = {constraint.slack:.2f}")
```
**Perhatikan:** kendala `Waktu_Mesin` dan `Tenaga_Kerja` memiliki **slack = 0**—keduanya aktif. Ini berarti kedua sumber daya ini **sepenuhnya terpakai** dan **membatasi** profit. Kendala `Bahan_Baku` memiliki slack > 0—ada sisa bahan baku yang tidak terpakai.
## Aplikasi 1: Alokasi Anggaran Retensi
Sekarang kita beralih ke aplikasi yang lebih relevan: **alokasi anggaran retensi ke berbagai segmen pelanggan**. Ini adalah masalah yang langsung terhubung ke Bab 9 (prediksi churn) dan Bab 11 (segmentasi).
### Skenario
Kita memiliki **tiga segmen pelanggan** (dari K-means di Bab 11) dengan:
- Jumlah pelanggan per segmen.
- Probabilitas churn rata-rata.
- Nilai pelanggan (LTV).
- Biaya dan efektivitas intervensi.
Anggaran retensi: **Rp 50 juta**. Kapasitas tim retensi: **300 panggilan per bulan**.
**Tujuan:** memaksimalkan **expected revenue yang diselamatkan**.
```{python}
#| label: segment-data
# Data segmen (dalam praktik, dari Bab 11 atau model Bab 9)
segments = pd.DataFrame({
"segmen": ["Premium Baru", "Loyal Hemat", "Mainstream"],
"n_pelanggan": [800, 2400, 3800],
"p_churn": [0.55, 0.12, 0.28],
"ltv": [1_200_000, 600_000, 900_000],
"biaya_intervensi": [200_000, 80_000, 120_000],
"efektivitas": [0.40, 0.25, 0.30],
})
segments["expected_benefit_per_customer"] = (
segments["p_churn"] * segments["efektivitas"] * segments["ltv"]
)
segments["net_benefit_per_customer"] = (
segments["expected_benefit_per_customer"] - segments["biaya_intervensi"]
)
segments
```
### Formulasi LP
**Variabel keputusan:**
- $x_1$ = jumlah pelanggan segmen "Premium Baru" yang diintervensi
- $x_2$ = jumlah pelanggan segmen "Loyal Hemat" yang diintervensi
- $x_3$ = jumlah pelanggan segmen "Mainstream" yang diintervensi
**Fungsi tujuan:** maksimalkan total net benefit
$$\max \, Z = 1.520.000 \cdot x_1 + 168.000 \cdot x_2 + 636.000 \cdot x_3$$
(dihitung dari `net_benefit_per_customer`)
**Kendala:**
- Anggaran: $200.000 x_1 + 80.000 x_2 + 120.000 x_3 \leq 50.000.000$
- Kapasitas tim: $x_1 + x_2 + x_3 \leq 300$
- Batas segmen: $x_1 \leq 800$, $x_2 \leq 2400$, $x_3 \leq 3800$
- Non-negatif: $x_i \geq 0$
```{python}
#| label: retention-lp
# Parameter
BUDGET = 50_000_000
CAPACITY = 300
# Model
retention_model = LpProblem("alokasi_retensi", LpMaximize)
# Variabel keputusan: jumlah pelanggan diintervensi per segmen
x = LpVariable.dicts(
"intervensi",
segments["segmen"].tolist(),
lowBound=0,
cat="Continuous", # relaksasi linear
)
# Fungsi tujuan
retention_model += lpSum(
segments.loc[i, "net_benefit_per_customer"] * x[seg]
for i, seg in enumerate(segments["segmen"])
), "Total_Net_Benefit"
# Kendala anggaran
retention_model += lpSum(
segments.loc[i, "biaya_intervensi"] * x[seg]
for i, seg in enumerate(segments["segmen"])
) <= BUDGET, "Anggaran"
# Kendala kapasitas
retention_model += lpSum(x[seg] for seg in segments["segmen"]) <= CAPACITY, "Kapasitas_Tim"
# Kendala batas segmen
for i, seg in enumerate(segments["segmen"]):
retention_model += x[seg] <= segments.loc[i, "n_pelanggan"], f"Batas_{seg.replace(' ', '_')}"
# Selesaikan
retention_model.solve(PULP_CBC_CMD(msg=False))
print(f"Status: {LpStatus[retention_model.status]}")
print(f"Total Net Benefit: Rp {value(retention_model.objective):,.0f}")
print(f"\nAlokasi intervensi per segmen:")
for seg in segments["segmen"]:
n = value(x[seg])
budget_seg = n * segments.loc[segments["segmen"] == seg, "biaya_intervensi"].iloc[0]
print(f" {seg:15s}: {n:6.1f} pelanggan | biaya: Rp {budget_seg:,.0f}")
# Cek penggunaan anggaran
total_budget_used = sum(
value(x[seg]) * segments.loc[segments["segmen"] == seg, "biaya_intervensi"].iloc[0]
for seg in segments["segmen"]
)
print(f"\nAnggaran terpakai : Rp {total_budget_used:,.0f} / Rp {BUDGET:,.0f}")
print(f"Kapasitas terpakai: {sum(value(x[seg]) for seg in segments['segmen']):.0f} / {CAPACITY}")
```
::: {.callout-important}
**Interpretasi hasil.** LP akan memilih segmen dengan **net benefit per pelanggan tertinggi** terlebih dahulu, lalu mengisi sisa kapasitas dengan segmen berikutnya. Urutan prioritas ini ditentukan oleh **rasio** net benefit terhadap biaya (dan terhadap kapasitas).
Tetapi perhatikan: LP menggunakan **variabel kontinu**—solusinya bisa berupa "2,7 pelanggan di segmen A". Untuk keputusan nyata, kita perlu **membulatkan** solusi ini (atau menggunakan integer programming di Bab 15).
:::
### Analisis Sensitivitas: Shadow Price
Setiap kendala memiliki **shadow price** yang menunjukkan berapa **kenaikan fungsi tujuan** jika kendala dilonggarkan satu unit. Ini adalah wawasan yang sangat berharga untuk keputusan bisnis.
```{python}
#| label: shadow-prices
print("Shadow price (dual value) per kendala:")
print("(Berapa Rp tambahan net benefit jika kendala dilonggarkan 1 unit)")
print()
for name, constraint in retention_model.constraints.items():
print(f" {name:20s}: Rp {constraint.pi:>15,.0f}")
```
**Cara membaca shadow price:**
- Jika shadow price anggaran = **Rp 8.000**, itu berarti: *"Setiap tambahan Rp 1 anggaran akan menghasilkan Rp 8.000 tambahan net benefit."* Ini adalah **ROI marginal** dari anggaran retensi.
- Jika shadow price kapasitas = **Rp 500.000**, itu berarti: *"Setiap tambahan 1 slot kapasitas tim akan menghasilkan Rp 500.000 tambahan net benefit."* Ini memberi tahu kita **seberapa besar nilai menambah kapasitas tim**—misalnya dengan merekrut satu orang lagi.
::: {.callout-tip}
**Shadow price adalah bahasa manajemen.** Daripada mengatakan "model LP memilih 200 pelanggan segmen A", kita dapat mengatakan: *"Setiap tambahan Rp 1 juta anggaran retensi menghasilkan Rp 8 juta net benefit—jadi investasi tambahan sangat layak."* Ini adalah argumen yang dapat digunakan dalam rapat anggaran.
Shadow price juga **valid hanya dalam rentang tertentu** (allowable range). Jika perubahan kendala terlalu besar, solusi optimal bisa berubah dan shadow price tidak lagi berlaku. PuLP tidak secara langsung memberikan rentang ini—untuk analisis lengkap, gunakan solver seperti Gurobi atau CPLEX yang menyediakan **sensitivity report**.
:::
## Aplikasi 2: Optimasi Bauran Produksi Retail
Sekarang kita gunakan dataset retail untuk aplikasi kedua: **berapa banyak setiap produk yang harus diproduksi/dipesan untuk memaksimalkan profit, dengan kendala kapasitas gudang dan anggaran?**
```{python}
#| label: retail-production
from decanal.utils import load_retail
df_retail = load_retail()
df_retail["Date"] = pd.to_datetime(df_retail["Date"])
n_stores = df_retail["Store ID"].nunique()
n_days_per_month = 30
# Asumsi margin: COGS = 65% dari harga (retail tipikal 30-40% margin)
COGS_RATIO = 0.65
produk = (df_retail
.groupby("Product ID")
.agg(
rata_permintaan_per_toko=("Units Sold", "mean"),
harga=("Price", "mean"),
rata_diskon=("Discount", "mean"),
competitor=("Competitor Pricing", "mean"),
)
.reset_index())
# Revenue efektif setelah diskon
produk["revenue_per_unit"] = produk["harga"] * (1 - produk["rata_diskon"])
# COGS diasumsikan proporsi dari harga list (bukan dari competitor)
produk["biaya"] = produk["harga"] * COGS_RATIO
produk["profit_per_unit"] = produk["revenue_per_unit"] - produk["biaya"]
produk["permintaan_bulanan"] = (
produk["rata_permintaan_per_toko"] * n_days_per_month * n_stores
)
produk_top = produk.nlargest(5, "profit_per_unit").reset_index(drop=True)
print(f"Total permintaan bulanan (5 produk top): {produk_top['permintaan_bulanan'].sum():,.0f} unit")
print(f"Total biaya memenuhi semua permintaan : "
f"Rp {(produk_top['biaya'] * produk_top['permintaan_bulanan']).sum():,.0f}")
print()
produk_top[["Product ID", "harga", "profit_per_unit", "permintaan_bulanan"]].round(2)
```
### Formulasi
**Variabel keputusan:** $x_i$ = jumlah unit produk $i$ yang dipesan bulan ini.
**Fungsi tujuan:** maksimalkan profit total.
$$\max \, Z = \sum_i \text{profit}_i \cdot x_i$$
**Kendala:**
- Permintaan maksimum: $x_i \leq \text{permintaan\_bulanan}_i$
- Kapasitas gudang total: $\sum_i x_i \leq 10.000$ unit
- Anggaran pembelian: $\sum_i \text{biaya}_i \cdot x_i \leq 500.000.000$
```{python}
#| label: product-mix-lp
# Parameter: buat kendala aktif agar shadow price bermakna
KAPASITAS_GUDANG = 8_000
ANGGARAN = 150_000_000
mix_model = LpProblem("bauran_produk", LpMaximize)
x = LpVariable.dicts(
"produk",
produk_top["Product ID"].tolist(),
lowBound=0, cat="Continuous",
)
# Fungsi tujuan
mix_model += lpSum(
produk_top.loc[i, "profit_per_unit"] * x[pid]
for i, pid in enumerate(produk_top["Product ID"])
), "Total_Profit"
# Kendala
mix_model += lpSum(x[pid] for pid in produk_top["Product ID"]) <= KAPASITAS_GUDANG, "Kapasitas"
mix_model += lpSum(
produk_top.loc[i, "biaya"] * x[pid]
for i, pid in enumerate(produk_top["Product ID"])
) <= ANGGARAN, "Anggaran"
# Permintaan maksimum per produk
for i, pid in enumerate(produk_top["Product ID"]):
mix_model += x[pid] <= produk_top.loc[i, "permintaan_bulanan"], f"Permintaan_{pid}"
mix_model.solve(PULP_CBC_CMD(msg=False))
# --- Hasil ---
print(f"Status : {LpStatus[mix_model.status]}")
print(f"Total Profit : Rp {value(mix_model.objective):,.0f}")
print(f"\nAlokasi per produk:")
for pid in produk_top["Product ID"]:
print(f" {pid}: {value(x[pid]):8.0f} unit")
```
```{python}
#| label: product-slack
print("\nShadow price per kendala:")
print("(Nilai marginal: tambahan profit jika kendala dilonggarkan 1 unit)")
print()
for name, constraint in mix_model.constraints.items():
if name in ["Kapasitas", "Anggaran"]:
print(f" {name:12s}: slack={constraint.slack:>10,.0f} "
f"shadow price = Rp {constraint.pi:,.4f} per unit")
```
**Insight dari shadow price:**
- Jika shadow price kapasitas gudang positif, itu berarti **gudang adalah bottleneck**—setiap tambahan 1 unit kapasitas menghasilkan tambahan profit sebesar shadow price.
- Jika shadow price anggaran positif, itu berarti **anggaran pembelian adalah bottleneck**—setiap tambahan Rp 1 anggaran menghasilkan tambahan profit.
::: {.callout-note}
**Shadow price = 0 berarti kendala tidak aktif.** Jika shadow price suatu kendala
adalah nol, itu berarti kendala tersebut **tidak membatasi** solusi optimal—ada
slack (sisa kapasitas) yang tidak terpakai. Relaksasi kendala (menambah kapasitas)
tidak akan meningkatkan profit.
Sebaliknya, shadow price **positif** menunjukkan kendala yang **aktif (binding)**—
menambah satu unit sumber daya akan meningkatkan profit sebesar shadow price
tersebut. Inilah yang memberi tahu kita **di mana harus berinvestasi**.
:::
## Kapan LP Tidak Cukup?
LP adalah alat yang sangat kuat, tetapi memiliki batasan:
1. **Variabel Diskrit:**
Jika variabel keputusan harus **bilangan bulat** (mis. jumlah karyawan, jumlah truk), LP dengan variabel kontinu tidak valid. Kita membutuhkan **integer programming** (Bab 15).
2. **Hubungan Non-Linear:**
Jika hubungan antara variabel dan tujuan **tidak linear** (mis. ekonomi skala, diminishing returns), LP tidak valid. Kita membutuhkan **nonlinear programming**.
3. **Kendala Logika:**
Jika kendala bersifat **logika** ("jika A dipilih, B tidak boleh dipilih"), LP tidak dapat mengekspresikannya secara langsung. Kita membutuhkan **integer/binary programming** (Bab 15).
4. **Ketidakpastian:**
LP mengasumsikan parameter **diketahui dengan pasti**. Dalam praktik, parameter seperti probabilitas churn dan permintaan **tidak pasti**. Kita membutuhkan **stochastic programming** atau **simulasi** (Bab 17).
::: {.callout-important}
**LP adalah fondasi, bukan tujuan akhir.** Di Bagian III, kita akan memperluas LP ke:
- **Integer programming** (Bab 15): variabel diskrit dan keputusan logika.
- **Network optimization** (Bab 16): aliran barang/jasa dalam jaringan.
- **Simulasi keputusan** (Bab 17): LP di bawah ketidakpastian.
Setiap perluasan ini menggunakan LP sebagai **building block**—jadi pemahaman mendalam tentang LP akan membuat bab-bab selanjutnya jauh lebih mudah.
:::
## Latihan Terbimbing
**Latihan 14.1 — Formulasi LP.**
Sebuah perusahaan memproduksi tiga produk dengan profit per unit: A=Rp 50.000, B=Rp 30.000, C=Rp 20.000. Setiap produk membutuhkan waktu mesin (A=3 jam, B=2 jam, C=1 jam) dan tenaga kerja (A=2 jam, B=1 jam, C=2 jam). Tersedia 120 jam mesin dan 100 jam tenaga kerja. Formulasikan LP dan selesaikan dengan PuLP.
**Latihan 14.2 — Interpretasi shadow price.**
Untuk LP di Latihan 14.1, periksa shadow price dari kendala mesin dan tenaga kerja. Jika shadow price mesin = Rp 15.000/jam dan shadow price tenaga kerja = Rp 5.000/jam, apa implikasi manajerialnya? Sumber daya mana yang lebih layak ditambah?
**Latihan 14.3 — Analisis sensitivitas.**
Dalam model alokasi retensi, ubah anggaran dari Rp 50 juta menjadi Rp 75 juta dan Rp 100 juta. Plot total net benefit vs anggaran. Apakah hubungannya linear? Jika tidak, mengapa? (Petunjuk: perhatikan shadow price.)
**Latihan 14.4 — Infeasible problem.**
Ubah model alokasi retensi dengan menambahkan kendala bahwa **setiap segmen harus mendapat minimal 200 pelanggan**. Apakah model masih feasible? Jika tidak, apa yang harus diubah agar feasible?
**Latihan 14.5 — Perbandingan strategi.**
Bandingkan solusi LP dengan strategi naif (alokasi proporsional terhadap jumlah pelanggan). Berapa selisih total net benefit? Apakah selisihnya signifikan secara bisnis?
**Latihan 14.6 — LP dengan data Bab 9.**
Ambil probabilitas churn dari model klasifikasi Bab 9 (atau gunakan simulasi). Formulasikan LP untuk memilih 100 pelanggan yang akan diintervensi dengan anggaran Rp 20 juta, di mana setiap pelanggan memiliki probabilitas churn, LTV, dan biaya intervensi yang berbeda. Selesaikan dengan PuLP.
::: {.callout-note collapse="true"}
**Petunjuk Latihan 14.4.** Jika kendala tambahan membuat model **infeasible**, PuLP akan mengembalikan status `Infeasible`. Periksa total anggaran dan kapasitas: apakah cukup untuk memenuhi minimal 200 pelanggan di setiap segmen? Jika tidak, longgarkan salah satu kendala—misalnya dengan menambah anggaran atau kapasitas.
:::
## Kotak "Jembatan": Dari LP ke Keputusan Nyata
::: {.callout-important}
**Jembatan.** Bab ini memberi kita **alat pertama** untuk optimasi skala nyata:
1. **Formulasi LP** mengubah masalah bisnis menjadi model matematis yang dapat diselesaikan secara optimal—bukan hanya "coba-coba".
2. **Shadow price** memberi kita bahasa untuk berkomunikasi dengan manajemen: bukan hanya "ini solusi optimal", tetapi "setiap tambahan Rp 1 anggaran menghasilkan Rp 8 manfaat—jadi tambah anggaran".
3. **Analisis sensitivitas** menunjukkan **seberapa stabil** solusi terhadap perubahan parameter—informasi penting ketika data tidak pasti.
Tetapi LP memiliki **tiga batasan besar** yang akan kita atasi di bab-bab berikutnya:
- **Variabel diskrit** → integer programming (Bab 15).
- **Aliran jaringan** → network optimization (Bab 16).
- **Ketidakpastian** → simulasi keputusan (Bab 17).
**Bab 15—Integer & Nonlinear Programming**—akan memperluas LP ke keputusan **ya/tidak** dan **bilangan bulat**, yang sangat umum dalam bisnis: "Apakah kita membuka gudang di lokasi ini?", "Berapa truk yang harus dibeli?", "Pelanggan mana yang harus diintervensi?" (tidak bisa 2,7 pelanggan).
:::
::: {.callout-warning}
**Dua jebakan umum dalam LP:**
1. **Parameter yang tidak bermakna bisnis.** Menggunakan `Competitor Pricing` sebagai
"biaya" menghasilkan profit ~0 karena harga kita dan pesaing berdekatan. Selalu
pastikan parameter mencerminkan **struktur biaya yang sebenarnya**—bukan sekadar
kolom yang tersedia di dataset.
2. **Pembulatan yang menyembunyikan shadow price.** Format `,.0f` membulatkan
shadow price kecil (mis. 0,11) menjadi 0—padahal kendala aktif. Gunakan
`,.4f` atau tampilkan dalam satuan yang sesuai (mis. ribuan) agar nilai marginal
tetap terlihat.
:::
## Rangkuman
Dalam bab ini kita telah:
- **Memahami anatomi LP**: variabel keputusan, fungsi tujuan, dan kendala.
- **Menyelesaikan LP dua variabel secara grafis**, memahami bahwa solusi optimal selalu berada di titik sudut feasible region.
- **Mengimplementasikan LP dengan PuLP**—mendefinisikan model secara deklaratif dan menyelesaikannya dengan solver CBC.
- **Membedakan solusi optimal dari solusi layak**, dan mengenali kendala yang **aktif** (binding) vs **tidak aktif**.
- **Menafsirkan shadow price** sebagai nilai marginal dari satu unit tambahan sumber daya—wawasan penting untuk keputusan investasi.
- **Menerapkan LP pada dua kasus nyata**: alokasi anggaran retensi dan optimasi bauran produksi.
- **Menyadari batas LP**: variabel diskrit, hubungan non-linear, kendala logika, dan ketidakpastian—yang akan diatasi di bab-bab selanjutnya.
## Jembatan ke Bab 15
LP mengasumsikan variabel keputusan **dapat berupa pecahan**. Tetapi banyak keputusan bisnis bersifat **diskrit**:
- Kita tidak bisa mengintervensi 2,7 pelanggan—harus 2 atau 3.
- Kita tidak bisa membuka 1,5 gudang—harus 1 atau 2.
- Kita tidak bisa membeli 3,7 truk—harus 3 atau 4.
**Integer programming** memperluas LP untuk menangani variabel diskrit. **Binary programming** (kasus khusus integer dengan nilai 0/1) memungkinkan kita memodelkan **keputusan ya/tidak**—yang sangat umum dalam bisnis.
Selain itu, **nonlinear programming** memungkinkan kita memodelkan hubungan yang tidak linear—misalnya, diminishing returns pada anggaran iklan, atau economies of scale pada produksi.
Bab 15 akan memperkenalkan teknik-teknik ini dan menerapkannya pada keputusan **pemilihan pelanggan** (tidak bisa 2,7 pelanggan), **lokasi fasilitas** (buka/tutup gudang), dan **penjadwalan** (shift kerja).