*! bbandits, version 2, 24.01.2026 *! Authors: Jan Kemper, Davud Rostam-Afschar *** Import python functions * Import python functions used for the bbandits package. * All function are in the file bbandits_functions.py which has to be in the ado folder (likely under subfolder "py") python: from bbandits_functions import * keys = ['Beta_OLS', 'Beta_BOLS_aggregated', 'Z-value', 'P-value', 'CI_lower_bound_95', 'CI_upper_bound_95', 'Treatment_arm_n', 'Reference_arm_n'] end program bbandits_update version 17 * input reward , chosen_arm, batch, [options] syntax varlist [, Thompson Greedy SAE Clipping(real 0.05) Epsilon(real 0.1) Seed(integer 1234) EXcel(string) ACtive_arms(numlist) BAtch_sae(int 3)] *** Exit the program if certain arguments are not given: if "`thompson'" == "" & "`greedy'" == "" & "`sae'" == "" { display as error "No Algorithm provided. Exiting early." exit // <-- stops the program here } *** If chosen_arm or reward has no missing values -- nothing can be updated, stop the program // Count missing values quietly count if missing(`1') // If no missing values, warn and exit if r(N) == 0 { di as error "Error: Variable `1' has no missing values. The program stops here. Missing values for to be updated units in the reward variable are required to determine which parts of the data are going to get updated. Look at the examples in the help file for the required data structure." exit 198 } quietly count if missing(`2') // If no missing values, warn and exit if r(N) == 0 { di as error "Error: Variable `2' has no missing values. The program stops here. Missing values for to be updated units in the 'chosen arm' variable are required to determine which parts of the data are going to get updated. Look at the examples in the help file for the required data structure." exit 198 } *** Parse input -> so that chosen_arm is from 0-k in the python function capture drop label_chosen_arm capture drop chosen_arm_py * create a tempvar for chosen_arm tempvar chosen_arm_py capture confirm numeric variable `2' if !_rc { * Numeric input → preserve numeric ordering qui egen `chosen_arm_py' = group(`2'), label * label variable for exports qui tostring `2', gen(label_chosen_arm) force } else { * String input → group by string labels display as error "Error: The assigned arm variable (input 2) is required to be numeric. It is recommended to number the arms from 0 up to arm k." exit // <-- stops the program here } qui replace `chosen_arm_py' = `chosen_arm_py' - 1 capture confirm variable chosen_arm if !_rc { // The variable exists, do something drop chosen_arm // Not an elegant solution to drop the existing chosen_arm, better to work with tempvars qui gen chosen_arm = `chosen_arm_py' } else { // The variable does not exist, do something else or nothing qui gen chosen_arm = `chosen_arm_py' } *** Exit program if chosen arm is given but not sufficient rewards * Count missing values qui count if missing(chosen_arm) local n_missing_arm = r(N) qui count if missing(`1') local n_missing_reward = r(N) * Check if missing rewards exceed missing arms if `n_missing_reward' > `n_missing_arm' { display as error "ERROR: More chosen arm data points (`n_missing_arm') than assigned arm data points (`n_missing_reward'). Both have to match. It is likely that the bbandits_update function was run twice or that there are missing values. Ensure that these dimension match, then run the command again. The program exits now." exit 198 } *** Parse 3rd input variable * problem: Data.get requires variable name as string but string stata parser * returns "var," if "," is placed without space in the syntax "syntax varlist," instead of "syntax varlist ," * solution: replace "," with empty space local var3 = subinstr("`3'", ",", "", .) dis "`var3'" * run inference command in python python: df = pd.DataFrame({"reward": stata_to_numpy(Data.get(var="`1'")), "chosen_arm": Data.get(var="chosen_arm"),"batch": stata_to_numpy(Data.get(var="`var3'")), "label_chosen_arm":Data.get(var="label_chosen_arm") }) python: df.loc[df["chosen_arm"] > 100000000000, "chosen_arm"] = np.nan python: df["chosen_arm"] = df["chosen_arm"].astype("Int64") # nullable integer * correct nan values python: df.loc[df['reward'] == -2147483648 , 'reward'] = np.nan # Because nans are wrongly read in python: pre = thompson_updating_preprocessing(df) python: t1 = pre["df"] python: next_batch = pre["next_batch"] *** Chose algorithm *** if "`sae'" != ""{ display("Sequential Arm Elimination (SAE) Algorithm") * Integrate check that active arms are given if "`active_arms'" == "" { display as error "WARNING: You must specify the option active_arms() which requires a numlist of the arms that are still active according to selected elimination arm algorithm." exit 198 } * list of active arms python: list_active_arms = "`active_arms'" python: list_active_arms = list_active_arms.split() python: list_active_arms = np.array(list_active_arms, dtype = int) python: list_active_arms_len = len(list_active_arms) * return value to stata and stop program if only one active arm is left python: Scalar.setValue('e(active_arms_len)', list_active_arms_len) if `e(active_arms_len)' == 1 { di "Number of active arms: " `e(active_arms_len)' display as error "WARNING: Only one remaining arm active. The program will stop here. The remaining arm can be played for the rest of the sample." exit } python: arms = df["chosen_arm"].nunique() python: res = esfandiari_update(df = df,arms = arms, active_arms = list_active_arms, batch = `batch_sae') python: final = res["df"] python: print(f"The active arms are: {res['active_arms']}") *** Now return the results into eresults python: t = pd.DataFrame({"active_arms": res["active_arms"]}) * merge labels to numeric chosen_arm values * clear estimation results ereturn clear python: Matrix.store("e(active_arms)", t['active_arms']) * also send it via a macro python: active_arms_stata = " ".join(str(x) for x in res["active_arms"]) python: Macro.setGlobal('active_arms_macro', active_arms_stata) } if "`thompson'" != "" { display("Thompson sampling algorithm arm probabilities:") python: probabilities, exact_values = thompson_updating(t1["chosen_arm"].astype(int), t1["reward"], t1["batch"], pre["batch_size"], clipping_rate = `clipping') * Display and store results python: arm_values = pre["arm_values"] python: Matrix.store("e(probabilities)", probabilities) python: Matrix.store("e(arm_labels)", arm_values) python: arm_values_str = [str(i) for i in arm_values] python: Matrix.setRowNames("e(probabilities)", arm_values_str) python: final = update_randomization(df, next_batch, probabilities, arm_values) } else if "`greedy'" != "" { display("Epsilon Greedy algorithm arm probabilities:") python: shares, chosen_arms, arm_values = epsilon_greedy_updating(t1["chosen_arm"].astype(int), t1["reward"], t1["batch"], pre["batch_size"], epsilon = `epsilon') python: Matrix.store("e(probabilities)", shares) python: Matrix.store("e(arm_labels)", arm_values) python: final = update_shuffling(df, next_batch, chosen_arms) } * for greedy and thompsons * Merge python : unique_pairs = df[['chosen_arm', 'label_chosen_arm']].drop_duplicates() # Merge the original DataFrame with unique_pairs to replace chosen_arm with chosen_arm_label * Only keep unique pairs, sometimes missing is "." or "" - drop both python: unique_pairs = unique_pairs[unique_pairs["label_chosen_arm"] != "."] python: unique_pairs = unique_pairs[unique_pairs["label_chosen_arm"] != ""] python: final = final.drop(columns='label_chosen_arm') # Drop chonsen_arm_label so that the new label can be merged python: final = final.merge(unique_pairs, on='chosen_arm', how='left') python: final["label_chosen_arm"] = final["label_chosen_arm"].astype(str) *** Initialize variables in stata capture drop chosen_arm capture drop reward capture drop batch capture drop chosen_arm_numeric capture drop label_chosen_arm // could also be a tempvar qui gen reward = . qui gen chosen_arm = "" qui gen batch = . qui gen chosen_arm_numeric = . python: chosen_arm_numeric = final["chosen_arm"].astype("float").to_numpy() # because pandas NA values not allowed python: Data.store("reward", None, final['reward'], None) python: Data.store("chosen_arm", None, final['label_chosen_arm'], None) python: Data.store("batch", None, final['batch'], None) python: Data.store("chosen_arm_numeric", None, chosen_arm_numeric, None) * fix "nan" should be come . qui replace chosen_arm = "" if chosen_arm == "nan" if "`thompson'" != "" | "`greedy'" != "" { * display("Present results") * Get scalar value python: current_batch = next_batch-1 python: Scalar.setValue('e(current_batch)', current_batch) * format returns *** Update * Display results nicely: tempname value name mat `value' = e(probabilities) mat `name' = e(arm_labels) local rows = rowsof(`name') * Display the header display "Arm Label Numeric Probability" forval i = 1/`rows' { local name1 = `name'[`i', 1] local value1 = `value'[`i', 1] display "Arm `name1' = `value1'" } } * If `excel' option is provided, proceed with exporting to Excel if "`excel'" != "" { capture drop `chosen_arm_py' // tempvar should not be displayed in excel file local path "`excel'" export excel using "`path'", firstrow(variables) replace } end