Regression on "p_outlier"¶
This small tutorial illustrates how we can define a regression directly on the p_outlier parameter.
The way we define the regression follows the basic pattern we establish in the include argument.
Load Modules¶
In [1]:
Copied!
import arviz as az
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import hssm
import arviz as az
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import hssm
Simulate some data¶
In [2]:
Copied!
# synth data
n_participants = 10
conditions = ["switch", "noswitch", "someswitch"]
n_samples_per_condition = 250
v_by_participant = np.random.normal(loc=0.0, scale=0.5, size=n_participants)
v_displacement_by_condition = {"switch": 1.0, "noswitch": 0.0, "someswitch": -1.0}
a_true = 1.0
z_true = 0.5
t_true = 0.5
dfs = []
for participant in range(n_participants):
for condition in conditions:
tmp_df = hssm.simulate_data(
model="ddm",
theta=dict(
v=v_by_participant[participant]
+ v_displacement_by_condition[condition],
a=a_true,
z=z_true,
t=t_true,
),
size=n_samples_per_condition,
)
tmp_df["true_v"] = (
v_by_participant[participant] + v_displacement_by_condition[condition]
)
tmp_df["true_a"] = a_true
tmp_df["true_z"] = z_true
tmp_df["true_t"] = t_true
tmp_df["participant_id"] = str(participant)
tmp_df["trialtype"] = condition
dfs.append(tmp_df)
data_df = pd.concat(dfs).reset_index(drop=True)
# synth data
n_participants = 10
conditions = ["switch", "noswitch", "someswitch"]
n_samples_per_condition = 250
v_by_participant = np.random.normal(loc=0.0, scale=0.5, size=n_participants)
v_displacement_by_condition = {"switch": 1.0, "noswitch": 0.0, "someswitch": -1.0}
a_true = 1.0
z_true = 0.5
t_true = 0.5
dfs = []
for participant in range(n_participants):
for condition in conditions:
tmp_df = hssm.simulate_data(
model="ddm",
theta=dict(
v=v_by_participant[participant]
+ v_displacement_by_condition[condition],
a=a_true,
z=z_true,
t=t_true,
),
size=n_samples_per_condition,
)
tmp_df["true_v"] = (
v_by_participant[participant] + v_displacement_by_condition[condition]
)
tmp_df["true_a"] = a_true
tmp_df["true_z"] = z_true
tmp_df["true_t"] = t_true
tmp_df["participant_id"] = str(participant)
tmp_df["trialtype"] = condition
dfs.append(tmp_df)
data_df = pd.concat(dfs).reset_index(drop=True)
Inject Uniform noise into rts to simulate outliers¶
In [3]:
Copied!
# Inject noise
p_outlier_noise = np.random.uniform(0, 0.15, size=n_participants)
for i in range(n_participants):
# Get indices of trials to inject noise
p_outlier_indices = np.random.choice(
data_df[data_df["participant_id"] == str(i)].index,
size=int(
p_outlier_noise[i] * len(data_df[data_df["participant_id"] == str(i)])
),
replace=False,
)
# Inject noise
data_df.loc[p_outlier_indices, "rt"] = np.random.uniform(
low=0.0, high=20.0, size=data_df.loc[p_outlier_indices, "rt"].shape
)
# Inject noise
p_outlier_noise = np.random.uniform(0, 0.15, size=n_participants)
for i in range(n_participants):
# Get indices of trials to inject noise
p_outlier_indices = np.random.choice(
data_df[data_df["participant_id"] == str(i)].index,
size=int(
p_outlier_noise[i] * len(data_df[data_df["participant_id"] == str(i)])
),
replace=False,
)
# Inject noise
data_df.loc[p_outlier_indices, "rt"] = np.random.uniform(
low=0.0, high=20.0, size=data_df.loc[p_outlier_indices, "rt"].shape
)
Define and sample from HSSM model¶
In [4]:
Copied!
test_model_1_C = hssm.HSSM(
data_df,
model="ddm",
loglik_kind="approx_differentiable",
include=[{"name": "v", "formula": "v ~ 0 + (1 + C(trialtype) |participant_id)"}],
p_outlier={
"formula": "p_outlier ~ 1 + (1|C(participant_id))",
"prior": {"Intercept": {"name": "Normal", "mu": 0, "sigma": 1}},
"link": "logit",
},
)
test_model_1_C = hssm.HSSM(
data_df,
model="ddm",
loglik_kind="approx_differentiable",
include=[{"name": "v", "formula": "v ~ 0 + (1 + C(trialtype) |participant_id)"}],
p_outlier={
"formula": "p_outlier ~ 1 + (1|C(participant_id))",
"prior": {"Intercept": {"name": "Normal", "mu": 0, "sigma": 1}},
"link": "logit",
},
)
No common intercept. Bounds for parameter v is not applied due to a current limitation of Bambi. This will change in the future. Model initialized successfully.
In [5]:
Copied!
test_model_1_C.sample(
# type of sampler to choose, 'numpyro', 'blackjax' or default pymc nuts
sampler="numpyro",
cores=2, # how many cores to use
chains=2, # how many chains to run
draws=500, # number of draws from the markov chain
tune=500, # number of burn-in samples
idata_kwargs=dict(log_likelihood=True), # return log likelihood
)
test_model_1_C.sample(
# type of sampler to choose, 'numpyro', 'blackjax' or default pymc nuts
sampler="numpyro",
cores=2, # how many cores to use
chains=2, # how many chains to run
draws=500, # number of draws from the markov chain
tune=500, # number of burn-in samples
idata_kwargs=dict(log_likelihood=True), # return log likelihood
)
Using default initvals.
/Users/yxu150/HSSM/.venv/lib/python3.13/site-packages/pytensor/gradient.py:1327: FutureWarning: LANLogpOp should implement `pullback` instead of `L_op`/`grad`. Direct `L_op`/`grad` implementations are deprecated and will stop being called in a future version. input_grads = node.op.pullback(inputs, node.outputs, new_output_grads) /Users/yxu150/HSSM/.venv/lib/python3.13/site-packages/bambi/backend/pymc.py:224: UserWarning: `init='adapt_diag'` is ignored by `nuts_sampler='numpyro'`; the external sampler uses its own initialization. idata = pm.sample( NUTS[numpyro]: [z, a, t, v_1|participant_id_mu, v_1|participant_id_sigma, v_1|participant_id_offset, v_C(trialtype)|participant_id_mu, v_C(trialtype)|participant_id_sigma, v_C(trialtype)|participant_id_offset, p_outlier_Intercept, p_outlier_1|C(participant_id)_sigma, p_outlier_1|C(participant_id)_offset] /Users/yxu150/HSSM/.venv/lib/python3.13/site-packages/pymc/sampling/jax.py:463: UserWarning: There are not enough devices to run parallel chains: expected 2 but got 1. Chains will be drawn sequentially. If you are running MCMC in CPU, consider using `numpyro.set_host_device_count(2)` at the beginning of your program. You can double-check how many devices are available in your system using `jax.local_device_count()`. pmap_numpyro = MCMC( sample: 100%|██████████████████████████| 1000/1000 [03:51<00:00, 4.32it/s, 31 steps of size 1.07e-01. acc. prob=0.92] sample: 100%|██████████████████████████| 1000/1000 [03:38<00:00, 4.57it/s, 31 steps of size 1.21e-01. acc. prob=0.90] We recommend running at least 4 chains for robust computation of convergence diagnostics The rhat statistic is larger than 1.01 for some parameters. This indicates problems during sampling. See https://arxiv.org/abs/1903.08008 for details The effective sample size per chain is smaller than 100 for some parameters. A higher number is needed for reliable rhat and ess computation. See https://arxiv.org/abs/1903.08008 for details /Users/yxu150/HSSM/.venv/lib/python3.13/site-packages/pytensor/link/numba/dispatch/basic.py:214: UserWarning: Numba will use object mode to run LANLogpOp's perform method. Set `pytensor.config.compiler_verbose = True` to see more details. warnings.warn(
Out[5]:
<xarray.DataTree>
Group: /
├── Group: /posterior
│ Dimensions: (chain: 2, draw: 500,
│ C(participant_id)__factor_dim: 10,
│ v_C(trialtype)|participant_id__expr_dim: 2,
│ v_C(trialtype)|participant_id__factor_dim: 10,
│ v_1|participant_id__factor_dim: 10)
│ Coordinates:
│ * chain (chain) int64 16B 0 1
│ * draw (draw) int64 4kB 0 1 ... 498 499
│ * C(participant_id)__factor_dim (C(participant_id)__factor_dim) <U1 40B ...
│ * v_C(trialtype)|participant_id__expr_dim (v_C(trialtype)|participant_id__expr_dim) <U10 80B ...
│ * v_C(trialtype)|participant_id__factor_dim (v_C(trialtype)|participant_id__factor_dim) <U1 40B ...
│ * v_1|participant_id__factor_dim (v_1|participant_id__factor_dim) <U1 40B ...
│ Data variables: (12/15)
│ p_outlier_1|C(participant_id)_offset (chain, draw, C(participant_id)__factor_dim) float64 80kB ...
│ a (chain, draw) float64 8kB ...
│ v_C(trialtype)|participant_id (chain, draw, v_C(trialtype)|participant_id__expr_dim, v_C(trialtype)|participant_id__factor_dim) float64 160kB ...
│ t (chain, draw) float64 8kB ...
│ v_1|participant_id_offset (chain, draw, v_1|participant_id__factor_dim) float64 80kB ...
│ v_1|participant_id_sigma (chain, draw) float64 8kB ...
│ ... ...
│ z (chain, draw) float64 8kB ...
│ v_1|participant_id (chain, draw, v_1|participant_id__factor_dim) float64 80kB ...
│ p_outlier_1|C(participant_id) (chain, draw, C(participant_id)__factor_dim) float64 80kB ...
│ v_1|participant_id_mu (chain, draw) float64 8kB ...
│ v_C(trialtype)|participant_id_mu (chain, draw, v_C(trialtype)|participant_id__expr_dim) float64 16kB ...
│ v_C(trialtype)|participant_id_offset (chain, draw, v_C(trialtype)|participant_id__expr_dim, v_C(trialtype)|participant_id__factor_dim) float64 160kB ...
│ Attributes:
│ created_at: 2026-06-30T13:28:26.411645+00:00
│ creation_library: ArviZ
│ creation_library_version: 1.1.0
│ creation_library_language: Python
│ sample_dims: ['chain', 'draw']
│ inference_library: numpyro
│ inference_library_version: 0.21.0
│ sampling_time: 451.86005
│ tuning_steps: 500
│ modeling_interface: bambi
│ modeling_interface_version: 0.18.0
├── Group: /sample_stats
│ Dimensions: (chain: 2, draw: 500)
│ Coordinates:
│ * chain (chain) int64 16B 0 1
│ * draw (draw) int64 4kB 0 1 2 3 4 5 6 ... 494 495 496 497 498 499
│ Data variables:
│ acceptance_rate (chain, draw) float64 8kB ...
│ step_size (chain, draw) float64 8kB ...
│ diverging (chain, draw) bool 1kB ...
│ energy (chain, draw) float64 8kB ...
│ n_steps (chain, draw) int64 8kB ...
│ tree_depth (chain, draw) int64 8kB 5 5 6 5 5 5 5 5 ... 5 5 6 5 5 6 4 5
│ lp (chain, draw) float64 8kB ...
│ Attributes:
│ created_at: 2026-06-30T13:28:26.418274+00:00
│ creation_library: ArviZ
│ creation_library_version: 1.1.0
│ creation_library_language: Python
│ sample_dims: ['chain', 'draw']
│ modeling_interface: bambi
│ modeling_interface_version: 0.18.0
├── Group: /observed_data
│ Dimensions: (__obs__: 7500, rt,response_extra_dim_0: 2)
│ Coordinates:
│ * __obs__ (__obs__) int64 60kB 0 1 2 3 ... 7497 7498 7499
│ * rt,response_extra_dim_0 (rt,response_extra_dim_0) int64 16B 0 1
│ Data variables:
│ rt,response (__obs__, rt,response_extra_dim_0) float64 120kB ...
│ Attributes:
│ created_at: 2026-06-30T13:28:26.418750+00:00
│ creation_library: ArviZ
│ creation_library_version: 1.1.0
│ creation_library_language: Python
│ sample_dims: []
│ modeling_interface: bambi
│ modeling_interface_version: 0.18.0
├── Group: /constant_data
│ Attributes:
│ created_at: 2026-06-30T13:28:26.418805+00:00
│ creation_library: ArviZ
│ creation_library_version: 1.1.0
│ creation_library_language: Python
│ sample_dims: []
│ modeling_interface: bambi
│ modeling_interface_version: 0.18.0
└── Group: /log_likelihood
Dimensions: (chain: 2, draw: 500, __obs__: 7500)
Coordinates:
* chain (chain) int64 16B 0 1
* draw (draw) int64 4kB 0 1 2 3 4 5 6 ... 493 494 495 496 497 498 499
* __obs__ (__obs__) int64 60kB 0 1 2 3 4 5 ... 7495 7496 7497 7498 7499
Data variables:
rt,response (chain, draw, __obs__) float64 60MB -0.2702 ... -0.07273
Attributes:
modeling_interface: bambi
modeling_interface_version: 0.18.0In [6]:
Copied!
# Add tiral-wise parameters to idata (since we define p_outlier as a regression)
test_model_1_C.add_likelihood_parameters_to_datatree(inplace=True)
# Add tiral-wise parameters to idata (since we define p_outlier as a regression)
test_model_1_C.add_likelihood_parameters_to_datatree(inplace=True)
Out[6]:
<xarray.DataTree>
Group: /
├── Group: /posterior
│ Dimensions: (chain: 2, draw: 500,
│ C(participant_id)__factor_dim: 10,
│ v_C(trialtype)|participant_id__expr_dim: 2,
│ v_C(trialtype)|participant_id__factor_dim: 10,
│ v_1|participant_id__factor_dim: 10,
│ __obs__: 7500)
│ Coordinates:
│ * chain (chain) int64 16B 0 1
│ * draw (draw) int64 4kB 0 1 ... 498 499
│ * C(participant_id)__factor_dim (C(participant_id)__factor_dim) <U1 40B ...
│ * v_C(trialtype)|participant_id__expr_dim (v_C(trialtype)|participant_id__expr_dim) <U10 80B ...
│ * v_C(trialtype)|participant_id__factor_dim (v_C(trialtype)|participant_id__factor_dim) <U1 40B ...
│ * v_1|participant_id__factor_dim (v_1|participant_id__factor_dim) <U1 40B ...
│ * __obs__ (__obs__) int64 60kB 0 1 ... 7499
│ Data variables: (12/17)
│ p_outlier_1|C(participant_id)_offset (chain, draw, C(participant_id)__factor_dim) float64 80kB ...
│ a (chain, draw) float64 8kB ...
│ v_C(trialtype)|participant_id (chain, draw, v_C(trialtype)|participant_id__expr_dim, v_C(trialtype)|participant_id__factor_dim) float64 160kB ...
│ t (chain, draw) float64 8kB ...
│ v_1|participant_id_offset (chain, draw, v_1|participant_id__factor_dim) float64 80kB ...
│ v_1|participant_id_sigma (chain, draw) float64 8kB ...
│ ... ...
│ p_outlier_1|C(participant_id) (chain, draw, C(participant_id)__factor_dim) float64 80kB ...
│ v_1|participant_id_mu (chain, draw) float64 8kB ...
│ v_C(trialtype)|participant_id_mu (chain, draw, v_C(trialtype)|participant_id__expr_dim) float64 16kB ...
│ v_C(trialtype)|participant_id_offset (chain, draw, v_C(trialtype)|participant_id__expr_dim, v_C(trialtype)|participant_id__factor_dim) float64 160kB ...
│ v (chain, draw, __obs__) float64 60MB ...
│ p_outlier (chain, draw, __obs__) float64 60MB ...
│ Attributes:
│ created_at: 2026-06-30T13:28:26.411645+00:00
│ creation_library: ArviZ
│ creation_library_version: 1.1.0
│ creation_library_language: Python
│ sample_dims: ['chain', 'draw']
│ inference_library: numpyro
│ inference_library_version: 0.21.0
│ sampling_time: 451.86005
│ tuning_steps: 500
│ modeling_interface: bambi
│ modeling_interface_version: 0.18.0
├── Group: /sample_stats
│ Dimensions: (chain: 2, draw: 500)
│ Coordinates:
│ * chain (chain) int64 16B 0 1
│ * draw (draw) int64 4kB 0 1 2 3 4 5 6 ... 494 495 496 497 498 499
│ Data variables:
│ acceptance_rate (chain, draw) float64 8kB ...
│ step_size (chain, draw) float64 8kB ...
│ diverging (chain, draw) bool 1kB ...
│ energy (chain, draw) float64 8kB ...
│ n_steps (chain, draw) int64 8kB ...
│ tree_depth (chain, draw) int64 8kB 5 5 6 5 5 5 5 5 ... 5 5 6 5 5 6 4 5
│ lp (chain, draw) float64 8kB ...
│ Attributes:
│ created_at: 2026-06-30T13:28:26.418274+00:00
│ creation_library: ArviZ
│ creation_library_version: 1.1.0
│ creation_library_language: Python
│ sample_dims: ['chain', 'draw']
│ modeling_interface: bambi
│ modeling_interface_version: 0.18.0
├── Group: /observed_data
│ Dimensions: (__obs__: 7500, rt,response_extra_dim_0: 2)
│ Coordinates:
│ * __obs__ (__obs__) int64 60kB 0 1 2 3 ... 7497 7498 7499
│ * rt,response_extra_dim_0 (rt,response_extra_dim_0) int64 16B 0 1
│ Data variables:
│ rt,response (__obs__, rt,response_extra_dim_0) float64 120kB ...
│ Attributes:
│ created_at: 2026-06-30T13:28:26.418750+00:00
│ creation_library: ArviZ
│ creation_library_version: 1.1.0
│ creation_library_language: Python
│ sample_dims: []
│ modeling_interface: bambi
│ modeling_interface_version: 0.18.0
├── Group: /constant_data
│ Attributes:
│ created_at: 2026-06-30T13:28:26.418805+00:00
│ creation_library: ArviZ
│ creation_library_version: 1.1.0
│ creation_library_language: Python
│ sample_dims: []
│ modeling_interface: bambi
│ modeling_interface_version: 0.18.0
└── Group: /log_likelihood
Dimensions: (chain: 2, draw: 500, __obs__: 7500)
Coordinates:
* chain (chain) int64 16B 0 1
* draw (draw) int64 4kB 0 1 2 3 4 5 6 ... 493 494 495 496 497 498 499
* __obs__ (__obs__) int64 60kB 0 1 2 3 4 5 ... 7495 7496 7497 7498 7499
Data variables:
rt,response (chain, draw, __obs__) float64 60MB -0.2702 ... -0.07273
Attributes:
modeling_interface: bambi
modeling_interface_version: 0.18.0Plot Results¶
In [7]:
Copied!
# Define output dataframe for plotting
data_df["p_outlier"] = test_model_1_C.traces.posterior.p_outlier.mean(
dim=["chain", "draw"]
)
data_df[["hdi_higher", "hdi_lower"]] = (
az.hdi(test_model_1_C.traces.posterior.p_outlier, prob=0.95)
.to_dataframe()
.reset_index()
.pivot(index="__obs__", columns="ci_bound", values="p_outlier")
)
# Define output dataframe for plotting
data_df["p_outlier"] = test_model_1_C.traces.posterior.p_outlier.mean(
dim=["chain", "draw"]
)
data_df[["hdi_higher", "hdi_lower"]] = (
az.hdi(test_model_1_C.traces.posterior.p_outlier, prob=0.95)
.to_dataframe()
.reset_index()
.pivot(index="__obs__", columns="ci_bound", values="p_outlier")
)
In [8]:
Copied!
grouped_df = data_df.groupby("participant_id")[
["p_outlier", "hdi_higher", "hdi_lower"]
].mean()
grouped_df["p_outlier_gt"] = p_outlier_noise
grouped_df = data_df.groupby("participant_id")[
["p_outlier", "hdi_higher", "hdi_lower"]
].mean()
grouped_df["p_outlier_gt"] = p_outlier_noise
In [9]:
Copied!
grouped_df
grouped_df
Out[9]:
| p_outlier | hdi_higher | hdi_lower | p_outlier_gt | |
|---|---|---|---|---|
| participant_id | ||||
| 0 | 0.067372 | 0.049270 | 0.085554 | 0.060338 |
| 1 | 0.097499 | 0.074902 | 0.121336 | 0.081809 |
| 2 | 0.129767 | 0.105165 | 0.157269 | 0.114840 |
| 3 | 0.065435 | 0.046709 | 0.084455 | 0.057307 |
| 4 | 0.078988 | 0.057767 | 0.099082 | 0.062660 |
| 5 | 0.064713 | 0.046092 | 0.083147 | 0.053531 |
| 6 | 0.122499 | 0.096230 | 0.148552 | 0.101626 |
| 7 | 0.068811 | 0.047504 | 0.088975 | 0.046738 |
| 8 | 0.069161 | 0.052255 | 0.090714 | 0.054330 |
| 9 | 0.098343 | 0.074172 | 0.123144 | 0.080630 |
In [10]:
Copied!
# Create forest plot
plt.figure(figsize=(10, 6))
# Plot HDI bands for each participant
for idx, row in grouped_df.iterrows():
plt.plot([row["hdi_lower"], row["hdi_higher"]], [idx, idx], "b-", linewidth=2)
plt.plot(row["p_outlier"], idx, "b|") # Plot mean point
# Plot ground truth values as red crosses
plt.plot(
grouped_df["p_outlier_gt"],
grouped_df.index,
"rx",
label="Ground Truth",
markersize=10,
)
plt.xlabel("p_outlier")
plt.ylabel("participant_id")
plt.title("Forest Plot of p_outlier Estimates with 95% HDI")
plt.legend()
plt.grid(True, alpha=0.3)
# Create forest plot
plt.figure(figsize=(10, 6))
# Plot HDI bands for each participant
for idx, row in grouped_df.iterrows():
plt.plot([row["hdi_lower"], row["hdi_higher"]], [idx, idx], "b-", linewidth=2)
plt.plot(row["p_outlier"], idx, "b|") # Plot mean point
# Plot ground truth values as red crosses
plt.plot(
grouped_df["p_outlier_gt"],
grouped_df.index,
"rx",
label="Ground Truth",
markersize=10,
)
plt.xlabel("p_outlier")
plt.ylabel("participant_id")
plt.title("Forest Plot of p_outlier Estimates with 95% HDI")
plt.legend()
plt.grid(True, alpha=0.3)
In [ ]:
Copied!