*! version 17.0 27jun2024

bbandits_update Update a multi-armed bandit experiment

Syntax

bbandits_update varlist [, thompson greedy sae clipping(real 0.05) epsilon(real 0.1) active_arms(numlist) batch_sae(int) excel("path")]

Description

bbandits_update updates the data structure for running a multi-armed bandit experiment based on the provided reward, chosen arm, and batch variables.
The chosen arm variable is required to be numeric. It is recommended to number the arms with integers starting from 0 up to arm k.
The command implements re-encoding of chosen arm values, performing the specified updating algorithm (Thompson Sampling, Epsilon-Greedy or Sequential Arm Elimination), and preparing the data for the next batch.

The program performs the following steps:

1. Re-encodes the chosen arm variable to be zero-indexed.

2. Preprocesses data for the specified updating algorithm (Bernoulli Thompson Sampling, Epsilon-Greedy or Sequential Arm Elimination).

3. Applies the algorithm within a Python function (Python installation necessary for this program).

4. Stores the updated variables back into the Stata dataset.

5. Optionally exports the dataset to an Excel file if the excel option is specified.

The underlying algorithms are implemented in Python; therefore, a Python installation is necessary. The clipping rate or epsilon rate
has to be sufficiently high so that each arm is at least played once, otherwise for the respective batch the BOLS estimate is not defined.

Options

thompson specifies Bernoulli Thompson sampling algorithm. The Bernoulli Thompson Sampling algorithm only accepts 1 (successes) and 0 (failures) as rewards.

greedy specifies the Epsilon-Greedy algorithm.

sae specifies the Sequential Arm Elimination (SAE) algorithm which is an implementation of the algorithm presented in Esfandiari et al. (2021, p. 7343).
For this option the units have to be randomly ordered because the algorithm assigns the arms blockwise per batch and does not randomly reshuffle.

clipping(real) specifies the clipping rate for the Bernoulli Thompson algorithm. The default value is 0.05.

epsilon(real) specifies the epsilon rate for the Epsilon-Greedy algorithm. The default value is 0.1.

active_arms(numlist) specifies the number of active arms according to the sequential elimination algorithm (sae) formatted as a numlist (e.g. 0 2 3 7).
It is a required input for the sae algorithm. In the initial stage all available arms are active. The numeric index saved in the column "chosen_arm_numeric" should be used
to specify the respective arms.

batch_sae(int) requires the total number of batches in the successive arm elimination setting.

excel("path") indicates that the updated data is saved as an Excel file under the specified path. The saved file can be used to impute the newly observed rewards.
The replace option is activated, hence it overwrites excel files with the same name in the same directory.

Requirements

The underlying calculations are computed in Python. Therefore, Python and the respective Python packages have to be installed. The required Python packages are scikit-learn, numpy,
pandas, scipy, statsmodels, and sfi.
At least Stata 16 is required.

Examples

Update the multi-armed bandit experiment using the Thompson Sampling algorithm:

bbandits_update reward chosen_arm batch, thompson

Update the multi-armed bandit experiment using the Epsilon-Greedy algorithm with specified epsilon and seed:

bbandits_update reward chosen_arm batch, greedy epsilon(0.2)

Update according to the Sequential Arm Elimination algorithm. It saves a numlist of the active arms in a macro so that it can be used for further analysis:

bbandits_update reward chosen_arm batch, sae active_arms(0 1 2) batch_sae(5)

Update and export the dataset to an Excel file:

bbandits_update reward chosen_arm batch, greedy excel("path")

Example 2: Simulated Bernoulli Thompson experiment to get started

Clear the dataset and create the required data structure. A dataset in which each row represents a unit to be treated. A complete list of planned treated units is recommended:

clear

set obs 1000

Create an ID variable:

gen ID = ""

Populate the ID variable with values "school_1", "school_2", ..., "school_1000". Hence there are 1000 schools which are going to be treated:

forval i = 1/1000 {

qui replace ID = "school_" + string(`i') if _n == `i'

}

Initialize the bandit experiment

bbandits_initialize, batches(10) arms(3) exploration_phase(2)

Assign treatment and observe rewards during the exploration phase

generate rand = runiform()

replace reward = .

forval i = 1/2 {

replace reward = 0 if batch == `i'

replace reward = 1 if rand < 0.4 & batch == `i' & chosen_arm_numeric == 2

replace reward = 1 if rand < 0.5 & batch == `i' & chosen_arm_numeric == 1

replace reward = 1 if rand < 0.6 & batch == `i' & chosen_arm_numeric == 0

}

Update assignments using Thompson Sampling:

bbandits_update reward chosen_arm_numeric batch, thompson clipping(0.2)

Run subsequent batches. Capture is necessary because in last round of the loop, nothing can be updated anymore.

forval i = 3/10 {

display `i'

replace reward = 0 if batch == `i'

replace reward = 1 if rand < 0.4 & batch == `i' & chosen_arm_numeric == 2

replace reward = 1 if rand < 0.5 & batch == `i' & chosen_arm_numeric == 1

replace reward = 1 if rand < 0.6 & batch == `i' & chosen_arm_numeric == 0

capture bbandits_update reward chosen_arm_numeric batch, thompson clipping(0.2)

}

Inspect final assignments:

bbandits reward chosen_arm_numeric batch

Example 3: Simulated Sequential Arm Elimination experiment

Clear the dataset and create 1,000 observations:

clear

set obs 1000

Create an ID variable:

gen ID = ""

Populate the ID variable with values "school_1", "school_2", ..., "school_1000":

forval i = 1/1000 {

qui replace ID = "school_" + string(`i') if _n == `i'

}

Initialize the bandit experiment using the Sequential Arm Elimination algorithm:

bbandits_initialize, batches(5) arms(3) sae

Display the active arms macro which can be used in the subsequent batches of the bbandits_update command. Alternatively, they can be manually specified:

di "$active_arms_macro"

Generate random rewards:

generate rand = runiform()

replace reward = .

Assign rewards in the first batch:

forval i = 1/1 {

replace reward = 0 if batch == `i'

replace reward = 1 if rand < 0.4 & batch == `i' & chosen_arm == 1

replace reward = 1 if rand < 0.5 & batch == `i' & chosen_arm == 2

replace reward = 1.5 if rand < 0.8 & batch == `i' & chosen_arm == 3

}

Update assignments using Sequential Arm Elimination:

bbandits_update reward chosen_arm_numeric batch, sae active_arms("$active_arms_macro") batch_sae(5)

Assign rewards in the second batch:

forval i = 2/2 {

replace reward = 0 if batch == `i'

replace reward = 1 if rand < 0.4 & batch == `i' & chosen_arm_numeric == 0

replace reward = 1 if rand < 0.5 & batch == `i' & chosen_arm_numeric == 1

replace reward = 1.5 if rand < 0.8 & batch == `i' & chosen_arm_numeric == 2

}

Display updated active arms and update assignments:

bbandits_update reward chosen_arm_numeric batch, sae active_arms("$active_arms_macro") batch_sae(5)

Literature

Esfandiari, H., Karbasi, A., Mehrabian, A., & Mirrokni, V. (2021). "Regret Bounds for Batched Bandits." Proceedings of the AAAI Conference on Artificial Intelligence, 35(8), 7340–7348.

Kemper, J., & Rostam-Afschar, D. (2026). "Earning While Learning: How to Run Batched Bandit Experiments." Stata Journal.

Disclaimer

This software is provided "as is" without warranty of any kind, either expressed or implied. The entire risk as to the quality and performance of the program is with you.

Should the program prove defective, you assume the cost of all necessary servicing, repair, or correction.

In no event will the copyright holders or their employers, or any other party who may modify and/or redistribute this software, be liable to you for damages, including any general, special, incidental,
or consequential damages arising out of the use or inability to use the program.

Authors: Jan Kemper, Davud Rostam-Afschar