Joke¶
The Jester joke-ratings dataset is encoded as a contextual bandit with
10 actions (joke IDs), 100-dimensional random-projection contexts, and
a rating outcome normalized to [0, 5.0].
Data¶
The code assumes that you have created a KPE_DATA_DIR env variable which points to the directory you are storing the RCT_joke_data.csv` (n = 48 445).
The RCT_joke_data.csv is a preprocessed RCT form of the Jester dataset [https://goldberg.berkeley.edu/jester-data/]. See details here: https://datadryad.org/dataset/doi:10.5061/dryad.bg79cnpp7
Prerequisites¶
Install the package from GitHub; all learner backends (scikit-learn
for lasso, the per-arm linear policy) ship as dependencies — see
Getting started for the full learner/backend table.
This vignette also requires RCT_joke_data.csv and the KPE_DATA_DIR
environment variable pointing to the directory that contains it (see Data below)
This dataset can be accessed by cloning the repository and looking in the "data" directory.
git clone https://github.com/StanfordAI4HI/kpe-py.git
import os
import numpy as np
import pandas as pd
# Processed Jester arrays (columns X0..X99, A, Y), exported once from
# RCT_joke_data.pkl to a CSV
df = pd.read_csv(os.path.join(os.environ["KPE_DATA_DIR"], "RCT_joke_data.csv"))
X = df[[f"X{i}" for i in range(100)]].to_numpy(dtype=float)
A = df["A"].to_numpy(dtype=int)
Y = df["Y"].to_numpy(dtype=float)
n = X.shape[0]
unique, counts = np.unique(A, return_counts=True)
p = counts / n
pa_x = np.array([dict(zip(unique, p))[a] for a in A], dtype=float)
Run¶
from kpe import kpe
result = kpe(
X=X, A=A, Y=Y, propensity=pa_x,
n_shuffles=100,
n_folds=6,
contextual_policy="linear",
best_arm="best_arm_erm",
reward_model="lasso",
n_jobs=5,
random_state=12314,
)
print(result.summary())
The research code uses a custom learner named joker_lasso_flat; this
package implements a per-arm linear regression (contextual_policy
= "linear") combined with a Lasso reward model (reward_model
= "lasso") as the closest in-package equivalent. The joker_*
learners are not exported by this package.
Results¶
Running the above call on the authoritative pickle under the default
leave-one-fold-out cross-fitting (n_nuisance_folds = n_folds - 1, so
5/6 of the data fits every nuisance) yields:
| quantity | value |
|---|---|
psi |
0.1676 |
std_error |
0.0224 |
| 95% confidence interval | [0.124, 0.211] |
| z-statistic | 7.50 |
| one-sided p-value | 3.4e-14 |
policy_stability |
0.274 |
For reference, this two-fold
shared-nuisance scheme used here
slightly improves over the original results report in Li and Brunskill (Science, 2026).
The sign of psi and the reject-at-0.05 decision are unchanged.
Multi-arm learner choice¶
The Joke dataset has 10 actions. Among the built-in contextual-policy strings:
"linear"fits a separate OLS regression per arm and returnsargmaxpredictions; supports any number of actions."policy_tree"(alias"dr_econml_2") wrapseconml.policy.DRPolicyTree; supports any number of actions."policy_forest"(alias"dr_econml") wrapseconml.policy.DRPolicyForest; supports any number of actions.
Notes¶
- Joke ratings are normalized to the range shown above prior to this
call;
psiis on that normalized outcome scale.