Choosing an Algorithm
All algorithms share identical per-arm state. Switching is a single field at campaign creation.
| Algorithm | Value | How it explores | When to use |
|---|---|---|---|
| LinUCB | "linucb" (default) |
Deterministic UCB bonus: θ·x + α·√(x·A⁻¹·x) |
Predictable, tunable. Sweep alpha offline to calibrate exploration. |
| Linear Thompson Sampling | "thompson_sampling" |
Samples θ̃ ~ N(θ, α²·A⁻¹), scores by θ̃·x | Natural Bayesian exploration. alpha=1.0 is the principled default. Concurrent users automatically diversify arm coverage. |
| NeuralLinUCB | {"neural_lin_ucb": {…}} |
Identical UCB formula to LinUCB, but applied to a learned embedding h(x; W) instead of raw context. MLP retrained in batch on its own cadence once retrain_every rewards accumulate. |
Non-linear or high-dimensional contexts where LinUCB has plateaued. Requires the neural feature flag at compile time. |
| NeuralThompsonSampling | {"neural_thompson_sampling": {…}} |
Same MLP embedding and retrain path as NeuralLinUCB, but draws w ~ N(θ, σ²A⁻¹) and scores by w·h(x;W) instead of a UCB bound. | Same non-linear/high-dimensional use case as NeuralLinUCB. Better long-run convergence, higher early regret. Requires the neural feature flag. |
| Progressive (Tournament) | {"progressive": {"base": "linucb", "challenger": "neural_lin_ucb"}} |
Runs both models in parallel. Uses the better-performing model for 90% of traffic. | The recommended default. Handles the cold-start with Linear and upgrades to Neural automatically when data justifies it. |
# Progressive — the internal tournament handled by BanditDB
db.create_campaign("auto_agent", ["model_a", "model_b"], feature_dim=1536,
algorithm={"progressive": {"base": "linucb", "challenger": "neural_lin_ucb"}})
Normalise your context vectors. Every algorithm here scores
exploration with a term proportional to the context's magnitude, and the regret
bounds behind LinUCB assume ‖x‖ ≤ 1. Feed raw features on
mixed scales — age next to income next to a 0/1 flag — and the model still
converges, only much more slowly, with nothing in the output to indicate why.
On the UCB shuttle benchmark, unit-L2 scaling alone moved cumulative regret from
2,026 to 709. The Python SDK ships normalize_context() for this.
Retraining runs on a background cadence
(BANDITDB_RETRAIN_POLL_SECS), not as part of checkpointing. Before
2.0.0 the two were coupled, so a retrain held up the checkpoint behind it.
Predictions read an immutable snapshot of the network weights and never wait on
a retrain in progress.
The algorithm field is stored in both the WAL and checkpoint files. Old WAL records and checkpoints without an algorithm field recover as "linucb" automatically.