## NBA Optimization

### 1. Goal

Given a set of feasible HCP × channel actions for a period, we want to select which actions to execute so that we:

- Maximise total expected impact
- Stay within budget and capacity constraints
- Respect contact and consent rules

This is formulated as a binary optimization problem (0–1 integer programming).

### 2. Inputs to the Optimizer

After ML scoring and business pre-filters, the optimizer receives a candidate actions table with one row per potential action:

| Field | Source |
|---|---|
| `hcp_id`, `channel` | HCP Master + Action Catalog |
| `engagement_probability` | ML model output |
| `avg_rx_8w` | KPI / Rx Metrics |
| `channel_uplift` | Engagement Action Catalog |
| `channel_cpa` | Engagement Action Catalog |
| `expected_impact` | Computed: p_engage × avg_rx_8w × channel_uplift |

**Pre-filtering (before optimization)**

Before the optimizer runs, candidates are filtered out if they violate hard rules:
- HCP opt-out or channel not permitted for this HCP
- Too soon since last contact on that channel (`min_days_between_*`)
- Engagement probability below minimum quality threshold (< 0.02)

The optimizer only sees feasible, meaningful candidates.

### 3. Business Constraints

#### 3.1 Separate Budgets for Field and Media

- **Field budget** (`budget_field_per_period`): covers rep_visit, phone_call
- **Media budget** (`budget_media_per_period`): covers webinar, digital_ad, email

Each bucket has its own cost constraint reflecting how budgets are managed in practice.

#### 3.2 Per-HCP Contact Cap

`max_contacts_per_hcp_per_period = 1` - at most one action per HCP per cycle.

#### 3.3 Per-Channel Volume Caps

- `max_rep_visits_per_period` - limited by field force headcount
- `max_phone_calls_per_period` - inside sales capacity
- `max_emails_per_period` - to avoid spam and respect frequency rules
- `max_webinars_per_period` - available event slots
- `max_digital_ads_per_period` - programmatic activation cap
- `min_digital_ads_per_period` - floor to maintain brand presence

All constraint values are configured as Dataiku project variables - see the [Project Variables](article:27) article.


### 4. Optimization Formulation

**Objective - maximise total expected impact:**

```
max Σ x(i,c) · expected_impact(i,c)
```

**Subject to:**

- Field budget: `Σ x(i,c) · cpa(i,c) ≤ budget_field_per_period` for c ∈ {rep_visit, phone_call}
- Media budget: `Σ x(i,c) · cpa(i,c) ≤ budget_media_per_period` for c ∈ {webinar, digital_ad, email}
- One action per HCP: `Σ_c x(i,c) ≤ 1` for all i
- Channel caps: `Σ_i x(i,c) ≤ max_c` for each channel c
- Digital floor: `Σ_i x(i,digital_ad) ≥ min_digital_ads_per_period`

---

### 5. Solver

**Tool:** PuLP (Python library) + CBC solver in Dataiku

- Time limit: 300 seconds
- Optimality gap: 1% (accepts solution within 1% of true optimum)
- Typical solve time: under 5 seconds for 2,500 candidate actions


### 6. Output

**NBA recommendation table** - one row per selected action:

- `hcp_id` · `hcp_name` · `specialty` · `territory`
- `nba_recommendation` (selected channel)
- `engagement_probability` · `engagement_tier`
- `expected_impact` · `expected_lift_pct`
- `priority_tier` · `impact_tier`
- `channel_cpa`

**Solver metadata** - aggregated summary per channel:

- Total actions selected per channel
- Total expected impact
- Total field cost and media cost vs budget caps
- Average engagement probability and average expected lift

Timing emerges naturally — the optimizer re-runs every week with fresh scores. HCPs not selected this cycle are reconsidered the next.