# Next Best Action (NBA) Recommendation

Learn from past user interactions to recommend the next best action — channel, send time, or offer — for every customer, every time.

## Overview and Use Cases

The NBA workflow from Treasure AI's machine learning platform (AI Signal) looks at how your users have responded to past marketing actions and learns which action is most likely to work for each user the next time around. Instead of sending the same email, offer, or channel to everyone, NBA matches each user's context (who they are, what device they use, how they've behaved) to the action they're most likely to engage with. The result plugs into Treasure AI Master Segments and Journey orchestration so marketers and CRM teams can personalize at scale without writing policy rules by hand.

Under the hood, NBA is a **contextual bandit** — a model family built for explore/exploit decisions. It reads historical interaction logs, evaluates many candidate policies offline, and picks the one most likely to outperform random or rules-based targeting. You don't need to hand-label "good" or "bad" actions; the reward signal (a click, an open, a purchase) is all NBA needs to improve.

Common use cases:

* **Next best channel** — decide whether to reach each user via email, push, SMS, or paid media
* **Send time optimization** — pick the time of day or day of week most likely to earn a response
* **Next best offer** — select the best coupon, deal, or product recommendation from a candidate set
* **Content personalization** — pick the subject line, hero banner, or creative variant per user
* **Feature input for downstream models** — use NBA recommendations as signals for CLV, churn, or lifecycle models


Who benefits most: Lifecycle marketers, CRM managers, and personalization teams who already run A/B tests but want a learned, per-user policy rather than a one-size-fits-all winner.

## Model Configuration

NBA AI Signals ships as three functions that run on the Treasure AI ML Batch API:

* `nba_tune` — automatically searches models, preprocessing, and evaluation settings to find the best configuration for your dataset
* `nba_train` — trains a chosen policy on the full dataset and saves it to model storage
* `nba_predict` — scores new users with a trained policy and writes the recommended best action


Most users run `nba_tune` once to identify a good configuration, then schedule `nba_train` and `nba_predict` on a recurring cadence.

### Training Inputs

NBA expects a **user-action interaction table** — one row per event, capturing who the user was, what action they saw, whether they engaged, and the context at the time.

| **Field**  | **Type**  | **Required**  | **Description**  |
|  --- | --- | --- | --- |
| `timestamp` | LONG | Yes | Unix timestamp of when the interaction took place. |
| `user_id` | VARCHAR | Yes | Unique identifier for the user. Column name is customizable via `user_column`. |
| `action` | VARCHAR | Yes | The action the user was shown (e.g., `email`, `paid_search`, a coupon code, or a numeric index). |
| `reward` | INT | Yes | Outcome of the interaction. Typically `1` for a positive outcome (click, open, purchase) and `0` otherwise. |
| `feature_1 … feature_N` | DOUBLE | Yes | Columns describing the user's context — age, device, tenure, one-hot encoded profile fields, or a latent vector. Any number of feature columns is supported. |
| `pscore` | DOUBLE | No | The true propensity — the probability the logging policy chose this action for this user. If you don't have this, NBA will estimate it for you. |
| `position` | INT | No | Position the action was shown in (e.g., slot 1 of an email grid). Reserved for future multi-action support. |


Data quality requirements
* Feature columns must be numeric. Encode categorical variables (one-hot, target encoding, or embeddings) before passing them in.
* Missing values are handled by the configured imputer — see the `impute_type` parameter.


### Training Outputs

The three functions produce different outputs. The most important for end users is the prediction output.

#### `nba_predict` output

One row per user, containing the recommended action with the highest predicted reward.

| **Field**  | **Type**  | **Description**  |
|  --- | --- | --- |
| `time` | LONG | TD time the prediction was written. |
| `user_id` | VARCHAR | User identifier carried forward from the input. |
| `predictions` | ARRAY<STRING> | Ordered list of recommended actions. First element is the top recommendation. Currently returns a single recommendation; support for multiple predictions per user is planned. |


**Example prediction row:**

| **time**  | **user_id**  | **predictions**  |
|  --- | --- | --- |
| 1758004803 | 23492371 | ["15"] |


Activate this table directly in a journey, or join it to your customer table as attributes for use in Master Segment rules.

#### `nba_tune` output (OCV mode)

A diagnostic table for data scientists. One row per trial across three tuning phases (preprocessing, OPE estimator selection, policy selection) plus a random baseline for comparison. Key columns include `phase`, `phase_trial`, `is_best`, `overall_score`, `estimated_policy_value`, `ci_lower`, `ci_upper`, and `lift_vs_random_pct`.

Use this table to decide which policy is worth training at full scale. A good sign: the best policy's `ci_lower` is above the random baseline's `estimated_policy_value`, and `lift_vs_random_pct` is positive.

#### `nba_train` output

A small metadata table confirming training completed. The trained policy itself is saved to managed model storage (S3) under the `model_name` you provided, and is retrieved by `nba_predict` automatically.

### Parameters

NBA exposes parameters across the three functions. The most common ones are listed below.

#### Common parameters (all functions)

| **Parameter**  | **Type**  | **Default**  | **Required**  | **Description**  |
|  --- | --- | --- | --- | --- |
| `input_table` | string | — | Yes | Source table in `dbname.table_name` format. |
| `output_table` | string | — | Yes | Destination table for function output. |
| `user_id` | string | `user_id` | No | Column holding the user identifier. |
| `action_column` | string | `action` | No | Column holding the action taken. |
| `reward_column` | string | `reward` | No | Column holding the reward. |
| `timestamp_column` | string | `timestamp` | No | Event time column. |
| `propensity_column` | string | — | No | Column with true propensity scores. If omitted, NBA estimates them. |
| `exclude_columns` | string | — | No | Pipe-delimited patterns to drop from features (e.g., `col_a|*_raw|temp_*`). |


#### `nba_train` parameters

| **Parameter**  | **Type**  | **Default**  | **Description**  |
|  --- | --- | --- | --- |
| `model_type` | string | — | Policy family. One of `lin_ucb`, `lin_ts`, `lin_eps_greedy`, `ipw_learner`. See the Model Types section. |
| `model_name` | string | — | Unique name used to save and retrieve the trained model. Unique per TD account. |
| `n_predictions` | int | `1` | Number of recommended actions per user at predict time. Currently fixed at `1`; support for multiple predictions is planned. |
| `epsilon` | float | `0.1` | Exploration rate for online models. Higher = more random exploration. Ignored by `ipw_learner`. |
| `impute_type` | string | `knn` | Missing-value strategy: `knn`, `hybrid`, `median`, `most_frequent`, `mean`, `drop`, `none`. |
| `scaler_type` | string | `minmax` | Feature scaling: `minmax`, `standard`, `robust`, `maxabs`, `none`. |
| `base_classifier` | string | `random_forest` | Classifier inside `ipw_learner`: `random_forest` or `logistic_regression`. |
| `propensity_type` | string | `logistic` | Propensity score estimation method: `uniform`, `logistic`, `true_propensity`. |
| `tune_propensity` | boolean | `true` | Whether to tune propensity model hyperparameters. |
| `max_ope_samples` | int | `200000` | Maximum number of samples to use for off-policy evaluation (OPE). Prevents out-of-memory errors on large datasets by stratified subsampling that guarantees action coverage. |
| `max_training_samples_per_model` | object | See description | Maximum training samples per model type. `ipw_learner` defaults to 1,000,000; `lin_ucb`, `lin_eps_greedy`, and `lin_ts` default to 5,000,000. |
| `tuning_results_table` | string | — | Table containing tuning results to use for training hyperparameters. If provided, best hyperparameters from tuning will be used instead of defaults. Format: `database.table` (e.g., `mydb.nba_tuning_results`). |
| `tuning_run_id` | string | — | Run ID of the tuning run to use. If not provided but `tuning_results_table` is set, the most recent tuning run will be used. |


#### `nba_tune` parameters

| **Parameter**  | **Type**  | **Default**  | **Description**  |
|  --- | --- | --- | --- |
| `hyperparam_tune_sample_ratio` | float | `0.01` | Fraction of data to use for tuning. Increase for small datasets. |
| `ocv_sigma_coef` | float | `0.0` | Conservativeness of policy selection. Higher values favor safer policies. |
| `ocv_phase1_trials` | int | `20` | Optuna trials for OPE estimator tuning. |
| `ocv_phase2_trials` | int | `50` | Optuna trials for policy tuning. |
| `search_space` | object | — | Override parts of the default hyperparameter search space. |
| `ocv_ess_config` | object | — | ESS (Effective Sample Size) filtering configuration for OCV Phase 2. Controls per-estimator safety factors and minimum ESS threshold. |
| `max_ope_samples` | int | `200000` | Maximum number of samples to use for off-policy evaluation (OPE). Prevents out-of-memory errors on large datasets by stratified subsampling that guarantees action coverage. |


**Tuning notes:**

* Start with `nba_tune` which will tune using OCV. OCV is paper-backed, gives more reliable policy selection, and is what the team recommends for new deployments.
* `epsilon` controls exploration on online models. `0.1` is a reasonable default; `0.0` is pure exploitation (risky), `1.0` is pure random.
* `impute_type: knn` is a safe default but slow on very large tables. Try `median` if runtime matters.
* Set `exclude_columns` to drop ID-like columns, raw timestamps, or leaky features before training.


## Model Types

NBA supports four policy types across two categories. `nba_tune` will select the best for your data, but understanding them helps interpret results.

Offline
### Inverse Propensity Weighting Learner (`ipw_learner`)

Reweights historical rows by the inverse of how likely the original system was to show each action, then trains a supervised classifier on the reweighted data. Lightweight and fast. Works well with abundant logged data. Sensitive to propensity accuracy — if estimated propensities are noisy, results can be biased.

Online (simulated)
### Linear Upper Confidence Bound (`lin_ucb`)

Maintains a linear reward model per action and picks actions using upper confidence bounds. Strong theoretical guarantees, adaptive exploration. Assumes reward is approximately linear in features.

### Linear Thompson Sampling (`lin_ts`)

Bayesian linear regression with posterior sampling to pick actions. Excellent empirical performance, natural uncertainty-driven exploration.

### Linear Epsilon-Greedy (`lin_eps_greedy`)

Linear reward model plus epsilon-greedy exploration — exploit the best action with probability `(1 - epsilon)`, pick randomly otherwise. Simple and predictable. Exploration is uninformed, so less efficient than UCB or TS.

## Example Workflow Code

NBA AI Signals runs via the Treasure AI ML Batch API, called from Treasure Workflow. The workflow below shows the full tune → train → predict lifecycle.

```yaml
# nba_signal_workflow.dig
# Treasure Workflow: NBA AI Signals (PrecisionML)
# Runs tuning, then trains the selected model, then predicts.

# Step 1: Tune — automatic search for best model and configuration
+tune:
  http>: https://ml-batch-api.treasuredata.com/v1/runs/
  method: POST
  headers:
    - authorization: ${secret:td.apikey}
    - X-TD-ML-SESSION-ID: ${session_id}
    - X-TD-ML-ATTEMPT-ID: ${attempt_id}
  store_content: true
  content:
    input_table: your_database.user_interactions
    output_table: your_database.nba_tune_results
    solution_name: nba_tune
    solution_arguments:
      action_column: "item_id"
      reward_column: "click"
      timestamp_column: "timestamp"

+tune_status:
  http>: https://ml-batch-api.treasuredata.com/v1/runs/${JSON.parse(http.last_content)['id']}/status
  method: GET
  headers:
    - authorization: ${secret:td.apikey}

# Step 2: Train — fit the selected model on the full dataset
+train:
  http>: https://ml-batch-api.treasuredata.com/v1/runs/
  method: POST
  headers:
    - authorization: ${secret:td.apikey}
    - X-TD-ML-SESSION-ID: ${session_id}
    - X-TD-ML-ATTEMPT-ID: ${attempt_id}
  store_content: true
  content:
    input_table: your_database.user_interactions
    output_table: your_database.nba_train_results
    solution_name: nba_train
    solution_arguments:
      model_name: "nba_retail_v1"      # Unique per TD account
      model_type: "lin_eps_greedy"     # Chosen from tune results
      action_column: "item_id"
      reward_column: "click"
      timestamp_column: "timestamp"
      n_predictions: 1                 # Currently supports 1 action per user
      epsilon: 0.1
      impute_type: "median"
      scaler_type: "standard"

+train_status:
  http>: https://ml-batch-api.treasuredata.com/v1/runs/${JSON.parse(http.last_content)['id']}/status
  method: GET
  headers:
    - authorization: ${secret:td.apikey}

# Step 3: Predict — score new users with the trained model
+predict:
  http>: https://ml-batch-api.treasuredata.com/v1/runs/
  method: POST
  headers:
    - authorization: ${secret:td.apikey}
    - X-TD-ML-SESSION-ID: ${session_id}
    - X-TD-ML-ATTEMPT-ID: ${attempt_id}
  store_content: true
  content:
    input_table: your_database.users_to_score
    output_table: your_database.nba_predictions
    solution_name: nba_predict
    solution_arguments:
      model_name: "nba_retail_v1"
      user_column: "user_id"
      timestamp_column: "timestamp"

+predict_status:
  http>: https://ml-batch-api.treasuredata.com/v1/runs/${JSON.parse(http.last_content)['id']}/status
  method: GET
  headers:
    - authorization: ${secret:td.apikey}

+done:
  echo>: "NBA pipeline complete. Predictions in your_database.nba_predictions"
```

**What this workflow does:**

* **Step 1 — Tune:** Runs OCV-based hyperparameter search over preprocessing, OPE estimators, and policies. Writes a diagnostic table. Inspect it to pick the best policy.
* **Step 2 — Train:** Fits the chosen policy on the full dataset and saves it to managed model storage under `model_name`.
* **Step 3 — Predict:** Loads the trained model, scores new users, and writes the recommended action per user.


In production, most teams schedule Step 1 monthly (or on major data changes) and Steps 2–3 daily.

Security
Store your TD API key as a Workflow secret named `td.apikey`. Never hardcode API keys in a workflow definition or commit them to version control.

To create and schedule this workflow, see [Getting Started with Treasure Workflow](/products/customer-data-platform/data-workbench/workflows/getting-started-with-treasure-workflow).

## Reading Tuning Results

After `nba_tune` finishes, this SQL pulls the best policy's lift vs. random:

```sql
SELECT
    phase,
    estimated_policy_value,
    ci_lower,
    ci_upper,
    lift_vs_random_pct
FROM your_database.nba_tune_results
WHERE run_id = '<your_run_id>'
  AND (phase = 'baseline_random'
       OR (phase = 'phase2_policy' AND is_best = 'true'));
```

**How to interpret:**

* If the best policy's `ci_lower` is above the random baseline's `estimated_policy_value`, the policy is confidently better than random. Proceed to training.
* If `lift_vs_random_pct` is not positive, or the confidence interval overlaps random, the model may not help. Consider more data, better features, or a different reward definition before deploying.


## Scalability

NBA is designed for batch workloads on the Treasure AI ML Batch API. Training scales with the number of interactions; prediction scales with the number of users.

* **Training:** Runtime depends on policy type, feature count, and dataset size. `lin_ucb` and `lin_eps_greedy` are the fastest; `lin_ts` is slower due to posterior sampling. `ipw_learner` speed depends on the base classifier.
* **Prediction:** The trained policy is frozen at predict time, so predictions parallelize cleanly. Split the input table across parallel ML Batch API calls to reduce wall-clock time for large user bases.
* **Tuning:** `nba_tune` runs many trials. Use `hyperparam_tune_sample_ratio` to subsample large datasets during tuning, then train the winning policy on the full data.


Detailed benchmarks are being published — reach out to your Treasure AI account team if you need sizing guidance for datasets over 100M interactions.

### Controlling Training Data Size

Training bandit models on very large datasets can run out of memory. Models like `ipw_learner` are especially memory-hungry — they fit a propensity classifier alongside the reward model, and both build large intermediate arrays (action distributions, estimated rewards) that grow directly with the number of training rows. Without a cap, datasets in the millions of rows can easily trigger out-of-memory (OOM) errors inside the training container.

To prevent that, the pipeline automatically subsamples before training: if your dataset exceeds the limit set for the chosen model type, it's downsampled using stratified sampling — which preserves the distribution across all actions rather than dropping some entirely. The defaults are intentionally conservative: `ipw_learner` caps at 1M rows (reflecting its heavier memory footprint), while the linear models (`lin_ucb`, `lin_ts`, `lin_eps_greedy`) cap at 5M rows.

You can override these limits per model type with a `max_training_samples_per_model` object:

```yaml
max_training_samples_per_model:
  lin_ucb: 5000000
  lin_ts: 5000000
  lin_eps_greedy: 5000000
  ipw_learner: 1000000
```

Raising a limit gives the model more data to learn from, which can improve policy quality — but weigh two trade-offs first:

* **Longer training times** — more samples means more computation for model fitting, cross-validation, and OPE.
* **Higher memory usage** — exceeding the container's available memory will crash the job with an OOM error. Be especially cautious raising the limit for `ipw_learner`, given its heavier footprint.


### Reusing Tuned Hyperparameters

We strongly recommend training with tuning results rather than defaults. During tuning, `nba_tune` runs an extensive hyperparameter search across preprocessing (imputation, scaling), propensity models, reward models, and OPE estimators to find the best configuration for your specific dataset. Training with those tuned hyperparameters — instead of generic defaults — typically produces a meaningfully better policy, with higher estimated value and better generalization to new data.

This is also why tuning and training are separate steps: it lets you run the expensive search once on a representative sample, then reuse the validated hyperparameters to train multiple models without re-tuning each time. Skip this step and training falls back to generic defaults that may not fit your data distribution, action space, or reward pattern well.

Point `tuning_results_table` at the output table from an `nba_tune` run (format: `database.table`, e.g. `mydb.nba_tuning_results`) to have `nba_train` pick up the winning configuration automatically. If you don't also set `tuning_run_id`, the most recent tuning run in that table is used.

### Effective Sample Size (ESS) Configuration

During tuning, `nba_tune` doesn't just score each trial — it also checks whether that score rests on enough effective data to be trustworthy. That check is **ESS (Effective Sample Size)**: a measure of how many of your historical rows actually count toward evaluating a candidate policy, once OPE reweighting is applied.

Here's why that check is necessary. OPE estimators like IPW and DR reweight each logged row by how likely the *new* policy would have taken the same action the *old* (logging) policy actually took. If the new policy behaves quite differently from the old one, only a handful of rows end up with meaningful weight — the rest barely count. So you can have 100,000 logged rows where the real signal behind an "estimated policy value" is closer to a few hundred independent observations. The trial's score can look strong while resting on a small, shaky foundation. ESS is the number that exposes that gap.

`ocv_ess_config` controls how strictly Phase 2 (policy selection) enforces this check. Any trial with ESS below the configured threshold is dropped during the search, instead of being allowed to win on a score that can't be trusted.

**Structure:**

| **Field**  | **Description**  |
|  --- | --- |
| `enabled` | Global toggle for ESS filtering. Default `true`. |
| `min_ess_threshold` | Minimum absolute ESS threshold applied across estimators unless overridden. Default `10.0`. |
| `ipw` / `snipw` / `dr` / `dros` / `dm` | Per-estimator override blocks. Each accepts its own `enabled`, `safety_factor`, and `min_ess_threshold`. |


Within each block, `safety_factor` scales up a dynamically calculated threshold (based on action coverage and data distribution) — the higher the value, the more trials get rejected. The defaults differ by estimator because each tolerates distribution shift differently: `ipw`: 2.0, `dr`: 1.5, `dros`: 1.5, `snipw`: 1.0. `dm` is disabled by default, since the Direct Method doesn't reweight by propensity and ESS doesn't apply to it the same way.

**Example — tighten filtering for IPW and DR:**

```yaml
ocv_ess_config:
  enabled: true
  min_ess_threshold: 20.0   # raise the global floor
  ipw:
    safety_factor: 3.0      # more conservative for IPW
  dr:
    safety_factor: 2.0      # more conservative for DR
```

**Example — give SNIPW a lower bar, since it can work reliably with fewer samples:**

```yaml
ocv_ess_config:
  enabled: true
  snipw:
    safety_factor: 1.0
    min_ess_threshold: 5.0
```

**Reading the result:** every trial's calculated ESS is written to the `nba_tune` output table as `phase2_ess` (see `nba_tune` output (OCV mode)). Check it alongside `estimated_policy_value` and `lift_vs_random_pct` — a big lift number paired with a low `phase2_ess` is a reason to double-check before shipping, not a green light.

**When to adjust this:** leave `ocv_ess_config` at its defaults in most cases. It's worth revisiting if:

* **Trials keep getting rejected** and tuning isn't converging on a policy — this often means thin action coverage. Try lowering `min_ess_threshold`, or a specific estimator's `safety_factor`, rather than turning filtering off entirely.
* **You want extra confidence before a production rollout** — raise `safety_factor` (or `min_ess_threshold`) so only the most robust trials survive.


## Limitations and Known Issues

* **Reward design is your responsibility — and it's critical.** NBA can only learn what you tell it to optimize. A reward that's too sparse (e.g., only purchases) will make the model learn slowly; a reward that's too dense but weakly correlated with business value (e.g., any page view) will mislead it. Start with a clear, binary business outcome and iterate.
* **Works best with good action coverage.** If a candidate action only appears a handful of times in your logs, NBA can't reliably evaluate whether a new policy that chooses it would do well. Aim for at least a few hundred interactions per action, ideally more.
* **Sparse-reward datasets are harder.** When positive rewards are very rare (< 1% of rows), both propensity and reward models become less reliable, which in turn weakens the off-policy evaluation. More data and stronger features help.
* **Does not currently handle cold-start actions.** Adding a brand-new action with no history requires retraining from scratch. Hybrid approaches with shared weights across actions are on the roadmap.
* **Batch only.** NBA runs as scheduled batch jobs, not real-time inference. Sub-daily cadence is supported via frequent workflow runs, but true millisecond serving is out of scope.
* **Offline evaluation has inherent uncertainty.** Off-policy evaluation corrects for the gap between the logging policy and the candidate policy, but the correction is only as good as the propensity and reward models underneath it. Always A/B test a new policy against the incumbent before full rollout.
* **Negative rewards require a different learner.** The current online models assume non-negative rewards. Use cases that need true negative penalties (e.g., unsubscribes) need a different policy class — contact your Treasure AI team if this applies.


## Glossary

| **Term**  | **Definition**  |
|  --- | --- |
| Contextual bandit | A model that picks an action for each user (the "context") and learns from the reward that follows — a lightweight form of reinforcement learning. |
| Action | One of the options the system can choose (e.g., a specific email subject, a coupon, a send time). |
| Context | The user's features at decision time — age, device, tenure, behavioral signals, anything that helps predict which action will work. |
| Reward | The measured outcome of an action (e.g., `1` for a click, `0` for no click). The model learns to maximize this. |
| Policy | A learned mapping from context to action. NBA's job is to find a policy that performs better than your current one. |
| Logging policy | The rule that picked actions in your historical data (random assignment, an old heuristic, etc.). NBA learns from interactions logged under this policy. |
| Propensity (pscore) | The probability the logging policy chose a given action for a given user. Needed for unbiased evaluation. If unknown, NBA estimates it. |
| Off-Policy Evaluation (OPE) | Estimating how well a new policy would perform, using data collected under a different policy — without actually deploying it. |
| OCV (OPE Cross-Validation) | A cross-validation method for OPE that produces more stable and conservative policy estimates. Enabled by default in `nba_tune`. |
| IPW (Inverse Propensity Weighting) | An OPE technique that reweights logged rows by `1 / propensity` to correct for selection bias. Unbiased but can have high variance. |
| Direct Method (DM) | An OPE technique that uses a reward model to predict outcomes under a new policy. Low variance but can be biased if the reward model is off. |
| Doubly Robust (DR) | An OPE technique that combines IPW and DM. Unbiased if either the propensity model or the reward model is well-calibrated. |
| ESS (Effective Sample Size) | A measure of how much of your logged data actually contributes to evaluating a new policy. Low ESS means high variance and unreliable estimates. |
| Coverage | The assumption that every action a new policy might pick has been taken often enough in the logged data to evaluate it reliably. |
| Lift vs random | How much better the selected policy is expected to perform compared to random action assignment. |
| PrecisionML | The production ML infrastructure that powers NBA AI Signals, shared across the AI Signals suite. |


## FAQs

| **Question**  | **Answer**  |
|  --- | --- |
| What kind of data do I need to get started? | A log of past user-action interactions with a clear reward signal. Each row should describe: who the user was (`user_id`), what they were shown (`action`), what happened (`reward`, typically `0` or `1`), when (`timestamp`), and numeric features describing the user at that moment. Categorical features need to be encoded before training. |
| How do I pick the right reward? | Pick the business outcome you'd celebrate if it went up — purchases, clicks, bookings, completed signups. Avoid rewards that are noisy or weakly correlated with value. If your true goal is rare (e.g., large purchases), consider a layered reward: high for purchases, lower for adds-to-cart, zero for nothing. Get this right first; the best model can't rescue a bad reward. |
| What's the difference between `nba_tune` and `nba_train`? | `nba_tune` searches over many configurations and tells you which policy, preprocessing, and hyperparameters look best for your data. `nba_train` takes one chosen configuration and fits it on the full dataset, saving a reusable model. Most teams run tuning monthly and training daily. |
| Do I need to provide propensity scores? | No. If your `pscore` column is missing, NBA trains a propensity model to estimate them for you. However, we highly recommend including `pscore`. True propensities are better when available (e.g., if you logged them during a randomized test), but estimated propensities work in most real-world setups. |
| How do I know if my trained policy is any good? | Check the `nba_tune` output: the best policy's lower confidence bound (`ci_lower`) should be above the random baseline's estimated value, and `lift_vs_random_pct` should be comfortably positive. Before full rollout, A/B test the new policy against your current approach. Off-policy evaluation is a strong guide, not a substitute for live measurement. |
| Can NBA recommend more than one action per user? | Not yet — `n_predictions` is currently fixed at `1`, returning the single best action for each user. Support for multiple recommendations per user is planned for a future release. |
| How often should I retrain? | Retrain on a cadence that matches how fast your user behavior changes. Daily works for fast-moving e-commerce; weekly or monthly is fine for slower categories. Re-run `nba_tune` less often — usually when you add new actions, change the feature set, or observe policy performance drift. |
| What happens when I add a new action that wasn't in training data? | The current model can't evaluate or recommend actions it hasn't seen. You'll need to retrain with interactions that include the new action. Support for cold-start actions via shared-weight hybrid models is on the roadmap. |