Learn from past user interactions to recommend the next best action, channel, send time, or offer, for every customer.
The Next Best Action (NBA) AI Signals solution looks at how your users responded to past marketing actions and learns which action is most likely to work for each user next time. Instead of sending the same email, offer, or channel to everyone, NBA matches each user's context to the action they are most likely to engage with. The output 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: email, push, SMS, or paid media
- Send time optimization: the time of day or day of week most likely to earn a response
- Next best offer: the best coupon, deal, or product recommendation from a candidate set
- Content personalization: subject line, hero banner, or creative variant per user
- Feature input for downstream models: NBA recommendations as signals for CLTV, 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 single one-size-fits-all winner.
| Question you're asking | Use |
|---|---|
| Who matters, based on past purchasing? | RFM AI Signals: descriptive segments, no modeling required |
| How likely is this customer to do X? | Propensity Scoring AI Signals: a calibrated probability per event |
| How much will this customer be worth? | CLTV AI Signals: a dollar forecast and percentile rank |
| Which product should I recommend next? | NBP AI Signals: a ranked list of products, services, or content |
| Which action should I take for this customer? | NBA AI Signals (you are here): a learned per-user policy across candidate actions |
NBA or NBP? NBP ranks items from a catalog by affinity: what is this person most likely to want. NBA learns a policy over a small set of marketing decisions you control, based on what has actually worked: which channel, which send time, which offer. NBP works from purchase and interaction history. NBA needs logged action and outcome pairs. The two compose well: let NBP choose the product, then let NBA choose how and when to pitch it.
Let's get you started with an example workflow. NBA runs as three functions on the Treasure AI ML Batch API:
nba_tunesearches models, preprocessing, and evaluation settings to find the best configuration for your data.nba_trainfits the chosen policy on the full dataset and saves it to model storage.nba_predictscores users with a trained policy and writes the recommended action.
Run tuning once to find a good configuration, then schedule training and prediction. Teams can tune monthly, or when they add new actions or change the feature set.
# Step 1: Tune, search for the 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_ocv: true
+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}Expected output: a diagnostic table in your_database.nba_tune_results, one row per trial across three tuning phases, plus a random baseline.
# Step 2: Train, fit the selected policy 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"
action_column: "item_id"
reward_column: "click"
timestamp_column: "timestamp"
tuning_results_table: your_database.nba_tune_results
+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}Expected output: a small metadata table confirming training completed. The trained policy is saved to managed model storage under model_name and is retrieved by nba_predict automatically using the model_name field. model_name must be unique per TD account.
By default, model_type comes from your tuning results. Passing tuning_results_table reuses the tuned hyperparameters instead of falling back to generic defaults, which is the recommended path.
# Step 3: Predict, score users with the trained policy
+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"Expected output: one row per user with the recommended action.
time user_id predictions
---------- --------- -------------
1758004803 23492371 ["15"]To create and schedule these workflows, see Getting Started with Treasure Workflow.
nba_tune evaluates preprocessing, estimation, and policy settings to determine the best configuration for your data. Pass the tuning result to nba_train via the tuning_results_table parameter to automatically apply the optimal model type and hyperparameters.
The input_table required for the tuning step is a user-action interaction table, structured with one row per event. This table should match the dataset you intend to use for the full training phase.
The neural_lin_ucb model type is not part of the tuning search space. It is configured and trained directly through nba_train.
| Field | Type | Required | Description |
|---|---|---|---|
timestamp | LONG | Yes | Unix timestamp of the interaction. |
user_id | VARCHAR | Yes | Unique user identifier. Column name is customizable. |
action | VARCHAR | Yes | The action the user was shown, for example email, paid_search, a coupon code, or a numeric index. |
reward | INT | Yes | Outcome of the interaction. Typically 1 for a click, open, or 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 is supported. |
pscore | DOUBLE | No | The true propensity, meaning the probability the logging policy chose this action for this user. NBA estimates it if omitted. |
position | INT | No | Position the action was shown in. Reserved for future multi-action support. |
Example rows:
timestamp user_id action reward age tenure_days device_mobile pscore
1757900000 23492371 email 1 34 412 1 0.25
1757901200 88104553 push 0 51 87 0 0.25
1757902450 23492371 sms 0 34 412 1 0.25
1757903900 41029887 paid_search 1 27 1203 1 0.25Data requirements:
- Feature columns must be numeric. Encode categorical variables using one-hot, target encoding, or embeddings before passing them in.
- Aim for at least a few hundred interactions per action. NBA cannot reliably evaluate an action that appears only a handful of times.
- Positive rewards below roughly 1% of rows weaken both the propensity and reward models. More data and stronger features help.
- Missing values are handled by the configured imputer. See
impute_typeinnba_train. - Use
exclude_columnsto drop ID-like columns, raw timestamps, and leaky features.
| 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, for example col_a|*_raw|temp_*. |
hyperparam_tune_sample_ratio | float | 0.01 | No | Fraction of data used for tuning. Increase for small datasets. |
ocv_sigma_coef | float | 0.0 | No | Conservativeness of policy selection. Higher values favor safer policies. |
ocv_phase1_trials | int | 20 | No | Optuna trials for OPE estimator tuning. |
ocv_phase2_trials | int | 50 | No | Optuna trials for policy tuning. |
search_space | object | — | No | Override parts of the default hyperparameter search space. |
ocv_ess_config | object | — | No | Effective Sample Size filtering for OCV Phase 2. See Effective Sample Size (ESS) Configuration. |
max_ope_samples | int | 200000 | No | Maximum samples used for off-policy evaluation. Prevents out-of-memory errors on large datasets through stratified subsampling that guarantees action coverage. |
Keep tune_ocv: true. OCV is paper-backed, gives more reliable policy selection, and is the recommended setting for new deployments.
A diagnostic table, one row per trial across three phases (preprocessing, OPE estimator selection, policy selection), plus a random baseline.
nba_train fits one chosen policy on the full dataset and saves it for reuse.
The input_table required for the training step is a user-action interaction table, structured with one row per event. This is the same table you used for model tuning.
| 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. |
model_type | string | — | Yes | Policy family: lin_ucb, lin_ts, lin_eps_greedy, ipw_learner, or neural_lin_ucb. See Model Types. |
model_name | string | — | Yes | Unique name used to save and retrieve the trained model. Unique per TD account. |
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, for example col_a|*_raw|temp_*. |
n_predictions | int | 1 | No | Number of recommended actions per user. Currently fixed at 1; multi-prediction support is planned. |
epsilon | float | 0.1 | No | Exploration rate for online models. Higher means more random exploration. Ignored by ipw_learner. |
impute_type | string | knn | No | Missing-value strategy: knn, hybrid, median, most_frequent, mean, drop, none. knn is safe but slow on very large tables; try median if runtime matters. |
scaler_type | string | minmax | No | Feature scaling: minmax, standard, robust, maxabs, none. |
base_classifier | string | random_forest | No | Classifier inside ipw_learner: random_forest or logistic_regression. |
propensity_type | string | logistic | No | Propensity estimation method: uniform, logistic, true_propensity. |
tune_propensity | boolean | true | No | Whether to tune propensity model hyperparameters. |
max_ope_samples | int | 200000 | No | Maximum samples used for off-policy evaluation. |
max_training_samples_per_model | object | ipw_learner and neural_lin_ucb: 1000000; lin_ucb, lin_ts, lin_eps_greedy: 5000000 | No | Maximum training samples per model type. See Controlling Training Data Size. |
arm_feature_columns | string | — | Conditional | Pipe-separated glob patterns identifying static per-action attribute columns. Required when per_arm=true; ignored with a warning by other model types. |
neural_lin_ucb_params | object | — | No | Hyperparameter block for neural_lin_ucb. See NeuralLinUCB. Ignored by other model types. |
tuning_results_table | string | — | No | Table of tuning results to source hyperparameters from, in database.table format. See Reuse Tuned Hyperparameters. |
tuning_run_id | string | — | No | Specific tuning run to use. Defaults to the most recent run in tuning_results_table. |
A small metadata table confirming training completed. The trained policy itself is saved to a managed model storage under model_name and retrieved by nba_predict automatically.
nba_predict loads a trained policy and scores users.
A table of user context vectors, one row per user. It carries the same feature columns used in training, but no action and no reward.
| Field | Type | Required | Description |
|---|---|---|---|
user_id | VARCHAR | Yes | Unique user identifier. Column name is customizable via user_column. |
feature_1 … feature_N | DOUBLE | Yes | The same context feature columns used at training time. |
pscore | DOUBLE | No | True propensity score of the action proposed by the production policy, if known. |
Example rows:
user_id pscore feature_1 feature_2 …
ba890-aced 0.1039 1.007 -9.1333
93adc-jicae 0.0981 0.093 -0.138action_column, reward_column, and propensity_column are not prediction parameters. The input table has no actions or rewards, and the trained policy is frozen at predict time.
| 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. |
model_name | string | — | Yes | Must match the name used at training time. Preprocessing settings from training, including scaling, imputation, and n_predictions, are inherited from the saved model. |
user_column | string | user_id | No | Column holding the user identifier. Note this parameter is named user_column here, while tune and train use user_id. |
timestamp_column | string | timestamp | No | Event time column. |
exclude_columns | string | — | No | Pipe-delimited patterns to drop from features, for example col_a|*_raw|temp_*. |
Using the predictions. The output table can be activated directly in a Journey, or joined to your customer table so recommendations become attributes available to Master Segment rules. A typical pattern is to join on user_id, expose the first element of predictions as a customer attribute, then branch a Journey on that attribute so each user receives the recommended channel or offer. Schedule prediction to match how quickly your context changes; daily is common for e-commerce.
| 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, top recommendation first. Currently one action per user. |
time user_id predictions
---------- --------- -------------
1758004803 23492371 ["15"]nba_tune evaluates the candidate set across the four tunable policy families. For most deployments, running tuning and adopting the winning configuration is the recommended path. However, teams may manually override the model type during training if a specific policy class is required. In these cases, we strongly suggest passing the tuning_results_table to inherit validated hyperparameters, while manually tuning model-specific parameters as necessary.
These sections describe the different model types we currently offer within NBA AI Signals, and also cover neural_lin_ucb, a model type that does not need tuning.
| Model type | Category | Best for | Main tradeoff |
|---|---|---|---|
ipw_learner | Offline | Abundant logged data | Sensitive to propensity accuracy |
lin_ucb | Online | Fast baseline with adaptive exploration | Assumes reward is roughly linear in features |
lin_ts | Online | Strong empirical performance | Slower, due to posterior sampling |
lin_eps_greedy | Online | Simplicity and predictability | Exploration is uninformed |
neural_lin_ucb | Online, hybrid | Non-linear structure, large action spaces | Slowest, outside tuning, needs volume |
Online models here are simulated: they are trained on logged data rather than interacting with live users.
Inverse Propensity Weighting 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, and it works well when logged data is abundant. It is sensitive to propensity accuracy: noisy estimated propensities produce biased results. Supplying a true pscore column helps most here.
| Parameter | Type | Default | Required | Description |
|---|---|---|---|---|
base_classifier | string | random_forest | No | Classifier trained on the reweighted data: random_forest or logistic_regression. |
rf_n_estimators | int | — | No | Number of trees. Applies when base_classifier is random_forest. Tuning searches [100, 500]. |
rf_max_depth | int | — | No | Maximum tree depth. Applies when base_classifier is random_forest. Tuning searches [4, 6, 8, 10]. |
rf_min_sample_split | int | — | No | Minimum samples to split. Applies when base_classifier is random_forest. Tuning searches [10, 20]. Note the singular spelling; the tuning output column is rf_min_samples_split. |
lr_C | float | — | No | Regularization strength. Applies when base_classifier is logistic_regression. Distinct from the tuning output's phase1_lr_c, which configures the OPE reward model. |
lr_max_iter | int | — | No | Iteration cap. Applies when base_classifier is logistic_regression. |
rf_min_samples_leaf can be tuned but has no direct train parameter, so it reaches training only through tuning_results_table.
solution_arguments:
model_name: "nba_retail_ipw_v1"
model_type: "ipw_learner"
action_column: "item_id"
reward_column: "click"
timestamp_column: "timestamp"
base_classifier: "random_forest"
propensity_type: "logistic"Linear Upper Confidence Bound. Maintains a linear reward model per action and picks actions using upper confidence bounds, so uncertainty drives exploration.
Strong theoretical guarantees and adaptive exploration. One of the two fastest options. It assumes reward is approximately linear in your features.
| Parameter | Type | Default | Required | Description |
|---|---|---|---|---|
epsilon | float | 0.1 | No | Exploration setting. The tuning search space samples [0.01, 0.1, 0.5, 1.0, 5.0]. |
solution_arguments:
model_name: "nba_retail_ucb_v1"
model_type: "lin_ucb"
action_column: "item_id"
reward_column: "click"
timestamp_column: "timestamp"Linear Thompson Sampling. Bayesian linear regression with posterior sampling to pick actions.
Excellent empirical performance and natural uncertainty-driven exploration. Slower than the other linear models because of the sampling step. lin_ts is a valid model_type but is not an option for the default tuning search.
| Parameter | Type | Default | Required | Description |
|---|---|---|---|---|
epsilon | float | 0.1 | No | Exploration setting for the online policies. |
solution_arguments:
model_name: "nba_retail_ts_v1"
model_type: "lin_ts"
action_column: "item_id"
reward_column: "click"
timestamp_column: "timestamp"Linear Epsilon-Greedy. A linear reward model plus epsilon-greedy exploration: take the best action with probability (1 - epsilon), pick randomly otherwise.
Simple, predictable, and fast. Exploration is uninformed, so it is less efficient than UCB or Thompson Sampling. epsilon at 0.1 is a reasonable default; 0.0 is pure exploitation and risky, 1.0 is pure random. Like lin_ts, this policy is a valid model_type but is not in the default tuning search space.
| Parameter | Type | Default | Required | Description |
|---|---|---|---|---|
epsilon | float | 0.1 | No | Probability of exploring instead of exploiting. 0.0 is pure exploitation and risky; 1.0 is pure random. This is the policy where epsilon carries its literal epsilon-greedy meaning. |
solution_arguments:
model_name: "nba_retail_eps_v1"
model_type: "lin_eps_greedy"
action_column: "item_id"
reward_column: "click"
timestamp_column: "timestamp"
epsilon: 0.1NeuralLinUCB. A hybrid: a learned encoder captures non-linear structure in the features, then a linear LinUCB head picks the action in that encoded space.
It is trained directly through nba_train and is not part of the nba_tune search. Reach for it when the linear models underfit and you suspect non-linear structure, or when you have a large action space with useful per-action attributes. Run it as a deliberate comparison against your tuned linear baseline. It is the slowest option and needs more data than the linear policies.
Global vs. per-arm mode, set by per_arm:
- Global (
per_arm: false, default) feeds only the user context to the encoder and keeps a separate LinUCB model per action. Right for a fixed, fairly small action set where every action has plenty of history. - Per-arm (
per_arm: true) concatenates static per-action attributes fromarm_feature_columnsto the context and uses one shared model to score every action. Because actions are described by features rather than opaque labels, this handles large or sparse action spaces better and can partially generalize to actions with thin history.
Warmup. The policy must accumulate roughly warmup_rounds * n_actions feedback rows to finish warmup and switch to LinUCB. On smaller datasets it may never get there. Set strict_warmup: true so training fails loudly instead of shipping a model that is still exploring randomly.
neural_lin_ucb_params
| Parameter | Type | Default | Description |
|---|---|---|---|
encoding_dim | int ≥ 1 | 32 | Dimension of the learned encoding feeding the linear UCB head. |
hidden_layer_sizes | list of ints | [64] | Encoder MLP hidden layer sizes, for example [64] or [128, 64]. |
per_arm | bool | false | Use the per-arm observation model, concatenating arm_feature_columns to the user context. |
learning_rate | float > 0 | 0.01 | Adam learning rate for encoder and reward-head training. |
weight_decay | float ≥ 0 | 0.0 | Adam weight decay (L2). |
train_batch_size | int ≥ 1 | 32 | Minibatch size for encoder and reward-head training. |
train_frequency | int ≥ 1 | 50 | How often, in warmup rounds, the encoder and reward head are trained. |
train_steps_per_update | int ≥ 1 | 32 | SGD updates performed each time the encoder is trained. |
max_buffer_size | int ≥ 1 | 100000 | Most-recent samples retained in the replay buffer. |
warmup_rounds | int ≥ 0 | 1000 | Feedback rounds spent in epsilon-greedy warmup before switching to LinUCB. |
strict_warmup | bool | false | When true, training fails if the policy never leaves warmup. |
epsilon_greedy | float in [0, 1] | 0.1 | Probability of random exploration during warmup. |
lambda_reg | float > 0 | 1.0 | Ridge regularization for the LinUCB covariance initialization. |
alpha | float ≥ 0 | 0.1 | UCB exploration coefficient multiplying the LinUCB confidence interval. |
solution_arguments:
model_name: "nba_retail_neural_v1"
model_type: "neural_lin_ucb"
action_column: "item_id"
reward_column: "click"
timestamp_column: "timestamp"
arm_feature_columns: "item_price|item_category_*"
neural_lin_ucb_params:
per_arm: true
encoding_dim: 32
hidden_layer_sizes: [128, 64]
warmup_rounds: 1000
strict_warmup: true
alpha: 0.1After nba_tune finishes, pull the best policy's lift against random:
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'));Pass condition: the best policy's ci_lower is above the random baseline's estimated_policy_value, and lift_vs_random_pct is comfortably positive. Proceed to training.
If it fails: if lift 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. Also check phase2_ess; a large lift paired with a low effective sample size is a reason to look closer, not a green light. See Effective Sample Size (ESS) Configuration.
Off-policy evaluation is a strong guide, not a substitute for live measurement. A/B test a new policy against the incumbent before full rollout.
Train with tuning results rather than defaults. nba_tune searches across preprocessing, propensity models, reward models, and OPE estimators to find the best configuration for your data. Training with those values typically produces a meaningfully better policy than generic defaults.
This is why tuning and training are separate steps. Run the expensive search once on a representative sample, then reuse the validated hyperparameters to train repeatedly without re-tuning.
Point tuning_results_table at the output table from an nba_tune run. If you do not also set tuning_run_id, the most recent run in that table is used. This applies to the four tunable policies; neural_lin_ucb takes its hyperparameters from neural_lin_ucb_params instead.
solution_arguments:
model_name: "nba_retail_v1"
model_type: "lin_eps_greedy"
action_column: "item_id"
reward_column: "click"
timestamp_column: "timestamp"
tuning_results_table: your_database.nba_tune_results
tuning_run_id: "20260815_103422"With tune_ocv: true, nba_tune also checks whether each trial's score rests on enough effective data to trust. ESS (Effective Sample Size) measures how many of your historical rows actually count toward evaluating a candidate policy once OPE reweighting is applied.
OPE estimators like IPW and DR reweight each logged row by how likely the new policy would have taken the action the logging policy actually took. If the new policy behaves quite differently, only a handful of rows carry meaningful weight. You can have 100,000 logged rows where the real signal behind an estimated policy value is closer to a few hundred independent observations. ESS exposes that gap.
ocv_ess_config controls how strictly Phase 2 enforces the check. Trials below the threshold are dropped during the search.
| Field | Description |
|---|---|
enabled | Global toggle for ESS filtering. Default true. |
min_ess_threshold | Minimum absolute ESS applied across estimators unless overridden. Default 10.0. |
ipw / snipw / dr / dros / dm | Per-estimator override blocks, each accepting its own enabled, safety_factor, and min_ess_threshold. |
Within each block, safety_factor scales a dynamically calculated threshold based on action coverage and data distribution. Higher values reject more trials. Defaults differ because each estimator 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 does not reweight by propensity.
Tighten filtering for IPW and DR:
ocv_ess_config:
enabled: true
min_ess_threshold: 20.0
ipw:
safety_factor: 3.0
dr:
safety_factor: 2.0Give SNIPW a lower bar, since it can work reliably with fewer samples:
ocv_ess_config:
enabled: true
snipw:
safety_factor: 1.0
min_ess_threshold: 5.0Every trial's ESS is written to the tune output as phase2_ess. Leave ocv_ess_config at its defaults in most cases. Revisit it if trials keep getting rejected and tuning is not converging, which usually means thin action coverage; lower min_ess_threshold or a specific safety_factor rather than disabling filtering. Raise safety_factor when you want extra confidence before a production rollout.
Training on very large datasets can exhaust memory. ipw_learner and neural_lin_ucb are the most demanding, since each fits an additional model (a propensity classifier, or an encoder network) alongside the reward model, building large intermediate arrays that grow with row count.
The pipeline subsamples automatically. If your dataset exceeds the limit for the chosen model type, it is downsampled using stratified sampling, preserving the distribution across all actions rather than dropping some entirely. Defaults are conservative: ipw_learner and neural_lin_ucb cap at 1M rows, the linear models at 5M.
Override per model type:
max_training_samples_per_model:
lin_ucb: 5000000
lin_ts: 5000000
lin_eps_greedy: 5000000
ipw_learner: 1000000
neural_lin_ucb: 1000000Raising a limit gives the model more data and can improve policy quality, at the cost of longer training times and higher memory usage. Exceeding container memory crashes the job with an out-of-memory error. Be especially careful raising limits for ipw_learner and neural_lin_ucb.
What data do I need? A log of past user-action interactions with a clear reward signal. Each row describes who the user was, what they were shown, what happened, when, and numeric features describing the user at that moment. Categorical features must be encoded before training.
How do I pick the right reward? Pick the business outcome you would 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, consider a layered reward: high for purchases, lower for adds-to-cart, zero for nothing. Get this right first; the best model cannot rescue a bad reward.
What is the difference between nba_tune and nba_train? Tuning searches many configurations and reports which policy, preprocessing, and hyperparameters look best for your data. Training fits one chosen configuration on the full dataset and saves a reusable model.
Does tuning use the same data as training? Yes. Both point at the same user-action interaction table with the same schema. You do not prepare a separate, smaller table for tuning: hyperparam_tune_sample_ratio subsamples inside the tune run, so by default tuning reads 1% of the rows while training uses all of them.
Do I need to provide propensity scores? No. NBA trains a propensity model to estimate them if pscore is missing. Including true propensities is still strongly recommended, especially if you logged them during a randomized test.
What if positive rewards are very rare? Below roughly 1% of rows, both the propensity and reward models become less reliable, which weakens off-policy evaluation. More data and stronger features help. Validate carefully before acting on results at scale.
What if an action has very little history? NBA cannot reliably evaluate whether a policy that chooses it would do well. Aim for at least a few hundred interactions per action. Per-arm neural_lin_ucb handles thin actions better than the alternatives, since it scores them from shared arm features.
Do my features have to be numeric? Yes. Encode categorical variables using one-hot, target encoding, or embeddings before training. Use exclude_columns to drop IDs, raw timestamps, and leaky features.
Which model type should I use? Run nba_tune and use what it selects. That is the right answer for most teams.
When is neural_lin_ucb worth it? When the linear models underperform and you suspect the relationship between features and response is not linear, or when you have a large action space with useful per-action attributes. It is trained directly through nba_train rather than selected by tuning, so run it as a deliberate comparison against your tuned baseline.
What is per-arm mode? Global mode keeps a separate LinUCB model per action and looks only at user context. Per-arm mode describes each action with its own features and shares one model across all actions. Use per-arm when you have many actions, sparse history per action, or meaningful action metadata.
How often should I retrain? Match the cadence to how fast your user behavior changes. Daily suits fast-moving e-commerce; weekly or monthly is fine for slower categories. Re-run tuning less often, usually when you add actions, change the feature set, or see performance drift. An example cadence could be tuning monthly, training weekly, and predicting daily.
How do I know my policy is any good? Check the tuning output: ci_lower above the random baseline's estimated value, and lift_vs_random_pct comfortably positive. Then A/B test against your current approach. 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, so live measurement stays necessary.
What happens when I add a new action? The model cannot evaluate or recommend actions it has not seen, so retrain with interactions that include the new one. Per-arm neural_lin_ucb is the closest thing to a workaround today, since it can score a thinly-observed action from its arm features. Full cold-start support via shared-weight hybrid models is on the roadmap.
How does runtime scale? Training scales with the number of interactions; prediction scales with the number of users. lin_ucb and lin_eps_greedy are fastest, lin_ts is slower due to posterior sampling, neural_lin_ucb is slowest. ipw_learner depends on its base classifier. The trained policy is frozen at predict time, so predictions parallelize cleanly; split the input table across parallel API calls for large user bases.
How large a dataset is supported? Training subsamples automatically above the per-model-type caps described in Controlling Training Data Size. Use hyperparam_tune_sample_ratio to subsample during tuning, then train the winning policy on full data. Detailed benchmarks are being published. Contact your Treasure AI account team for sizing guidance above 100M interactions.
Can NBA recommend more than one action per user? Not yet. n_predictions is fixed at 1, so predictions holds a single top action. Multi-action support, and the position input column that enables it, are planned.
Can NBA run in real time? No. NBA runs as scheduled batch jobs. Sub-daily cadence is supported through frequent workflow runs, but millisecond serving is out of scope.
Can I use negative rewards? Not with the current online models, which assume non-negative rewards. Use cases needing true negative penalties, such as unsubscribes, require a different policy class. Contact your Treasure AI team if this applies.
| 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, such as an email subject, a coupon, or 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, for example 1 for a click and 0 for no click. The model learns to maximize this. |
| Policy | A learned mapping from context to action. NBA's job is to find one that performs better than your current approach. |
| Logging policy | The rule that picked actions in your historical data, such as random assignment or an old heuristic. |
| Propensity (pscore) | The probability the logging policy chose a given action for a given user. Needed for unbiased evaluation. Estimated if unknown. |
| Off-Policy Evaluation (OPE) | Estimating how well a new policy would perform using data collected under a different policy, without deploying it. |
| ESS (Effective Sample Size) | How much of your logged data actually contributes to evaluating a new policy. Low ESS means unreliable estimates. |
| Lift vs random | How much better the selected policy is expected to perform than random action assignment. |