Skip to content
Last updated

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
timestampLONGYesUnix timestamp of when the interaction took place.
user_idVARCHARYesUnique identifier for the user. Column name is customizable via user_column.
actionVARCHARYesThe action the user was shown (e.g., email, paid_search, a coupon code, or a numeric index).
rewardINTYesOutcome of the interaction. Typically 1 for a positive outcome (click, open, purchase) and 0 otherwise.
feature_1 … feature_NDOUBLEYesColumns describing the user's context — age, device, tenure, one-hot encoded profile fields, or a latent vector. Any number of feature columns is supported.
pscoreDOUBLENoThe 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.
positionINTNoPosition 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
timeLONGTD time the prediction was written.
user_idVARCHARUser identifier carried forward from the input.
predictionsARRAY<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
175800480323492371["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_tablestringYesSource table in dbname.table_name format.
output_tablestringYesDestination table for function output.
user_idstringuser_idNoColumn holding the user identifier.
action_columnstringactionNoColumn holding the action taken.
reward_columnstringrewardNoColumn holding the reward.
timestamp_columnstringtimestampNoEvent time column.
propensity_columnstringNoColumn with true propensity scores. If omitted, NBA estimates them.
exclude_columnsstringNoPipe-delimited patterns to drop from features (e.g., col_a|*_raw|temp_*).

nba_train parameters

Parameter Type Default Description
model_typestringPolicy family. One of lin_ucb, lin_ts, lin_eps_greedy, ipw_learner. See the Model Types section.
model_namestringUnique name used to save and retrieve the trained model. Unique per TD account.
n_predictionsint1Number of recommended actions per user at predict time. Currently fixed at 1; support for multiple predictions is planned.
epsilonfloat0.1Exploration rate for online models. Higher = more random exploration. Ignored by ipw_learner.
impute_typestringknnMissing-value strategy: knn, hybrid, median, most_frequent, mean, drop, none.
scaler_typestringminmaxFeature scaling: minmax, standard, robust, maxabs, none.
base_classifierstringrandom_forestClassifier inside ipw_learner: random_forest or logistic_regression.
propensity_typestringlogisticPropensity score estimation method: uniform, logistic, true_propensity.
tune_propensitybooleantrueWhether to tune propensity model hyperparameters.
max_ope_samplesint200000Maximum 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_modelobjectSee descriptionMaximum 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_tablestringTable 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_idstringRun 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_ratiofloat0.01Fraction of data to use for tuning. Increase for small datasets.
ocv_sigma_coeffloat0.0Conservativeness of policy selection. Higher values favor safer policies.
ocv_phase1_trialsint20Optuna trials for OPE estimator tuning.
ocv_phase2_trialsint50Optuna trials for policy tuning.
search_spaceobjectOverride parts of the default hyperparameter search space.
ocv_ess_configobjectESS (Effective Sample Size) filtering configuration for OCV Phase 2. Controls per-estimator safety factors and minimum ESS threshold.
max_ope_samplesint200000Maximum 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.

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.

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.

# 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.

Reading Tuning Results

After nba_tune finishes, this SQL pulls the best policy's lift vs. 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'));

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:

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
enabledGlobal toggle for ESS filtering. Default true.
min_ess_thresholdMinimum absolute ESS threshold applied across estimators unless overridden. Default 10.0.
ipw / snipw / dr / dros / dmPer-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:

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:

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 banditA model that picks an action for each user (the "context") and learns from the reward that follows — a lightweight form of reinforcement learning.
ActionOne of the options the system can choose (e.g., a specific email subject, a coupon, a send time).
ContextThe user's features at decision time — age, device, tenure, behavioral signals, anything that helps predict which action will work.
RewardThe measured outcome of an action (e.g., 1 for a click, 0 for no click). The model learns to maximize this.
PolicyA learned mapping from context to action. NBA's job is to find a policy that performs better than your current one.
Logging policyThe 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.
CoverageThe assumption that every action a new policy might pick has been taken often enough in the logged data to evaluate it reliably.
Lift vs randomHow much better the selected policy is expected to perform compared to random action assignment.
PrecisionMLThe 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.