import arviz as az
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import pymc as pm

import causalpy as cp

%load_ext autoreload
%autoreload 2
OMP: Info #276: omp_set_nested routine deprecated, please use omp_set_max_active_levels instead.

Variable Selection Priors and Instrumental Variable Designs#

When building causal inference models, we often face a dilemma: we want to control for confounders to get unbiased causal estimates, but we’re not always certain which variables are the true confounders. Include too few, and we risk omitted variable bias. Include too many, and we introduce noise that inflates our uncertainty or, worse, creates multicollinearity that destabilizes our estimates.

Traditional approaches force us to make hard choices upfront—which variables to include, which to exclude. This in ideal cases should be driven by theory. But what if we could let the data help us make these decisions while still maintaining the principled probabilistic framework of Bayesian inference? This is where variable selection priors come in. Let’s first simulate some data with some natural confounding structure.

def inv_logit(z):
    """Compute the inverse logit (sigmoid) of z."""
    return 1 / (1 + np.exp(-z))


def simulate_data(n=2500, alpha_true=3.0, rho=0.6, cate_estimation=False):
    # Exclusion restrictions:
    # X[0], X[1] affect both Y and T (confounders)
    # X[2], X[3] affect ONLY T (instruments for T)
    # X[4] affects ONLY Y (predictor of Y only)

    betaY = np.array(
        [0.5, -0.3, 0.0, 0.0, 0.4, 0, 0, 0, 0, 0, 0, 0, 0, 0]
    )  # X[2], X[3] excluded
    betaD = np.array(
        [0.7, 0.1, -0.4, 0.3, 0.0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
    )  # X[4] excluded
    p = len(betaY)

    # noise variances and correlation
    sigma_U = 3.0
    sigma_V = 3.0

    # design matrix (n × p) with mean-zero columns
    X = np.random.normal(size=(n, p))
    X = (X - X.mean(axis=0)) / X.std(axis=0)

    mean = [0, 0]
    cov = [[sigma_U**2, rho * sigma_U * sigma_V], [rho * sigma_U * sigma_V, sigma_V**2]]
    errors = np.random.multivariate_normal(mean, cov, size=n)
    U = errors[:, 0]  # error in outcome equation
    V = errors[:, 1]  #

    # continuous treatment
    T_cont = X @ betaD + V

    # latent variable for binary treatment
    T_latent = X @ betaD + V
    T_bin = np.random.binomial(n=1, p=inv_logit(T_latent), size=n)

    alpha_individual = 3.0 + 2.5 * X[:, 0]

    # outcomes
    Y_cont = alpha_true * T_cont + X @ betaY + U
    if cate_estimation:
        Y_bin = alpha_individual * T_bin + X @ betaY + U
    else:
        Y_bin = alpha_true * T_bin + X @ betaY + U

    # combine into DataFrame
    data = pd.DataFrame(
        {
            "Y_cont": Y_cont,
            "Y_bin": Y_bin,
            "T_cont": T_cont,
            "T_bin": T_bin,
        }
    )
    data["alpha"] = alpha_true + alpha_individual
    for j in range(p):
        data[f"feature_{j}"] = X[:, j]
    data["Y_cont_scaled"] = (data["Y_cont"] - data["Y_cont"].mean()) / data[
        "Y_cont"
    ].std(ddof=1)
    data["Y_bin_scaled"] = (data["Y_bin"] - data["Y_bin"].mean()) / data["Y_bin"].std(
        ddof=1
    )
    data["T_cont_scaled"] = (data["T_cont"] - data["T_cont"].mean()) / data[
        "T_cont"
    ].std(ddof=1)
    data["T_bin_scaled"] = (data["T_bin"] - data["T_bin"].mean()) / data["T_bin"].std(
        ddof=1
    )
    return data


data = simulate_data()
instruments_data = data.copy()
features = [col for col in data.columns if "feature" in col]
formula = "Y_cont ~ T_cont + " + " + ".join(features)
instruments_formula = "T_cont ~ 1 + " + " + ".join(features)
data
Y_cont Y_bin T_cont T_bin alpha feature_0 feature_1 feature_2 feature_3 feature_4 ... feature_8 feature_9 feature_10 feature_11 feature_12 feature_13 Y_cont_scaled Y_bin_scaled T_cont_scaled T_bin_scaled
0 3.169837 -0.170346 1.113394 0 9.236102 1.294441 0.418241 0.536286 -0.615573 -1.173784 ... 0.559393 1.111766 -0.216069 0.451496 -0.863189 0.319180 0.268475 -0.438362 0.347809 -1.022452
1 10.479049 6.662990 2.272020 1 3.787487 -0.885005 0.315013 0.810138 1.137214 0.203685 ... -0.017179 0.410818 0.670074 1.954944 0.888255 -0.506518 0.916637 1.321011 0.726014 0.977650
2 7.307821 4.118982 2.062946 1 3.311157 -1.075537 1.058536 1.908944 1.122914 0.611691 ... 0.050641 1.245489 -1.070642 -0.060250 -1.857638 0.806913 0.635421 0.666008 0.657767 0.977650
3 9.781360 0.649647 4.043904 1 8.423450 0.969380 0.698199 0.314419 1.446987 -2.729092 ... -1.052429 1.143079 -1.757701 -1.167276 -0.164620 1.687004 0.854768 -0.227239 1.304402 0.977650
4 5.739283 6.812030 0.642417 1 6.613922 0.245569 -0.338491 0.814305 -0.859798 0.334968 ... -0.547622 0.778724 0.678956 1.715229 -0.439130 -0.238847 0.496327 1.359385 0.194070 0.977650
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
2495 -5.099912 -0.870746 -1.409722 0 6.565630 0.226252 1.589653 0.056005 -0.386026 -0.462251 ... 0.987633 0.246870 -0.202917 0.178579 0.763186 0.527462 -0.464865 -0.618693 -0.475800 -1.022452
2496 -32.742858 -7.337551 -8.468435 0 4.760520 -0.495792 0.546002 0.209072 0.666614 0.400847 ... -0.798775 -0.616483 0.431552 1.238957 0.957759 -0.583051 -2.916171 -2.283696 -2.779944 -1.022452
2497 6.759804 1.912040 1.615921 0 10.445934 1.778373 0.097808 -0.807658 0.380358 -0.455391 ... 0.668040 1.471963 0.573966 -0.288768 -0.861025 0.372657 0.586824 0.097788 0.511847 -1.022452
2498 -11.249395 -1.938808 -3.103529 0 5.715321 -0.113872 0.747480 1.635159 -1.136585 -0.007239 ... -2.404202 -2.074570 0.022878 1.345018 0.705361 0.414329 -1.010186 -0.893686 -1.028702 -1.022452
2499 21.658258 7.675401 5.660952 1 5.741192 -0.103523 -1.083828 0.896827 0.146243 1.363973 ... -1.310958 -0.004224 0.206620 0.012787 -1.777783 0.340176 1.907981 1.581676 1.832247 0.977650

2500 rows × 23 columns

CausalPy’s Variable Selection module provides a way to encode our uncertainty about variable relevance directly into the prior distribution. Rather than choosing which predictors to include, we specify priors that allow coefficients to be shrunk toward zero (or exactly zero) when the data doesn’t support their inclusion. The key insight is that variable selection becomes part of the inference problem rather than a preprocessing step. The module offers two fundamentally different approaches to variable selection, each reflecting a different belief about how sparsity manifests in the world.

The Spike-and-Slab: Discrete Choices#

The spike-and-slab prior embodies a binary worldview: each variable either matters or it doesn’t. Mathematically, we express this as:

\[ \beta_{j} = \gamma_{j} \cdot \beta_{j_\text{raw}}\]

such that

\[ \gamma_{j} \in \{0, 1\}\]

So we have the “spike”—the coefficient is exactly zero. When \(\gamma_{j} = 1\), we have the “slab” i.e. the coefficient takes on a value from the raw distribution. This approach appeals to our intuition about many real-world scenarios. Consider a propensity score model predicting whether someone receives a treatment. Some demographic variables might genuinely have no relationship with treatment assignment, while others are strongly predictive. The spike-and-slab says: let’s let each variable clearly declare itself as relevant or irrelevant.

The Regularised Horseshoe: Gentle Moderation#

The horseshoe prior takes a different philosophical stance. Instead of discrete selection, it says: effects exist on a continuum from negligible to substantial, and we should shrink them proportionally to their signal strength. Small effects get heavily shrunk (possibly to near-zero), while large effects escape shrinkage almost entirely.

\[ \beta_{j} = \tau \cdot \lambda_{j} \cdot \beta_{j\text{raw}}\]

where \(\tau\) is a global shrinkage parameter shared across all coefficients, and \(\lambda_{j}\) is local or specific to each coefficient and regularised so as to ensure finite variance.

Hyperparameters for Variable Selection Priors#

You can control the behaviour of the variable selection priors through some of the hyperparameters available. For the spike and slab prior, the most important hyperparamers are temperature, pi_alpha, and pi_beta.

Because our sampler doesn’t like discrete variables, we’re approximating a bernoulli outcome in our sampling to define the spike and slab. The approximation is governed by the temperature parameter. The default value of 0.1 works well in most cases, creating indicators that cluster near 0 or 1 without causing sampling difficulties.

The selection probability parameters pi_alpha and pi_beta encode your prior belief about sparsity. With both set to 2 (the default), you’re placing a Beta(2,2) prior on π, the overall proportion of selected variables. This is symmetric around 0.5 but slightly concentrated there—you’re saying “I don’t know how many variables are relevant, but probably not all of them and probably not none of them.”

fig, axs = plt.subplots(1, 3, figsize=(20, 6))
axs = axs.flatten()
axs[0].hist(pm.draw(pm.Beta.dist(2, 2), 1000), ec="black", color="slateblue")
axs[1].hist(pm.draw(pm.Beta.dist(2, 5), 1000), ec="black", color="slateblue")
axs[2].hist(pm.draw(pm.Beta.dist(5, 2), 1000), ec="black", color="slateblue");
../_images/c7ffc015f8bd67c43df82c4a41cb46541e55dcbbd4c8faaf4a83831bbcf37637.png

We’ll now fit two models and estimate the implied treatment effect.

sample_kwargs = {
    "draws": 1000,
    "tune": 2000,
    "chains": 4,
    "cores": 4,
    "target_accept": 0.95,
    "progressbar": True,
    "random_seed": 42,
    "mp_ctx": "spawn",
}

# =========================================================================
# Model 1: Normal priors (no selection)
# =========================================================================
print("\n" + "-" * 80)
print("Model 1: Normal Priors (No Variable Selection)")
print("-" * 80)

result_normal = cp.InstrumentalVariable(
    instruments_data=instruments_data,
    data=data,
    instruments_formula=instruments_formula,
    formula=formula,
    model=cp.pymc_models.InstrumentalVariableRegression(sample_kwargs=sample_kwargs),
    vs_prior_type=None,  # No variable selection
)

# =========================================================================
# Model 2: Spike-and-Slab priors
# =========================================================================
print("\n" + "-" * 80)
print("Model 2: Spike-and-Slab Priors")
print("-" * 80)

result_spike_slab = cp.InstrumentalVariable(
    instruments_data=instruments_data,
    data=data,
    instruments_formula=instruments_formula,
    formula=formula,
    model=cp.pymc_models.InstrumentalVariableRegression(sample_kwargs=sample_kwargs),
    vs_prior_type="spike_and_slab",
    vs_hyperparams={
        "pi_alpha": 2,
        "pi_beta": 2,
        "slab_sigma": 2,
    },
)
--------------------------------------------------------------------------------
Model 1: Normal Priors (No Variable Selection)
--------------------------------------------------------------------------------
/Users/nathanielforde/mambaforge/envs/CausalPy/lib/python3.13/site-packages/causalpy/experiments/instrumental_variable.py:187: UserWarning: Warning. The treatment variable is not Binary.
                This is not necessarily a problem but it violates
                the assumption of a simple IV experiment.
                The coefficients should be interpreted appropriately.
  warnings.warn(
Initializing NUTS using jitter+adapt_diag...
Multiprocess sampling (4 chains in 4 jobs)
NUTS: [beta_t, beta_z, chol_cov]
OMP: Info #276: omp_set_nested routine deprecated, please use omp_set_max_active_levels instead.
OMP: Info #276: omp_set_nested routine deprecated, please use omp_set_max_active_levels instead.
OMP: Info #276: omp_set_nested routine deprecated, please use omp_set_max_active_levels instead.
OMP: Info #276: omp_set_nested routine deprecated, please use omp_set_max_active_levels instead.
/Users/nathanielforde/mambaforge/envs/CausalPy/lib/python3.13/site-packages/pytensor/compile/function/types.py:1039: RuntimeWarning: invalid value encountered in accumulate
  outputs = vm() if output_subset is None else vm(output_subset=output_subset)
/Users/nathanielforde/mambaforge/envs/CausalPy/lib/python3.13/site-packages/pytensor/compile/function/types.py:1039: RuntimeWarning: invalid value encountered in accumulate
  outputs = vm() if output_subset is None else vm(output_subset=output_subset)
/Users/nathanielforde/mambaforge/envs/CausalPy/lib/python3.13/site-packages/pytensor/compile/function/types.py:1039: RuntimeWarning: invalid value encountered in accumulate
  outputs = vm() if output_subset is None else vm(output_subset=output_subset)
/Users/nathanielforde/mambaforge/envs/CausalPy/lib/python3.13/site-packages/pytensor/compile/function/types.py:1039: RuntimeWarning: invalid value encountered in accumulate
  outputs = vm() if output_subset is None else vm(output_subset=output_subset)

Sampling 4 chains for 2_000 tune and 1_000 draw iterations (8_000 + 4_000 draws total) took 133 seconds.
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/nathanielforde/mambaforge/envs/CausalPy/lib/python3.13/site-packages/causalpy/experiments/instrumental_variable.py:187: UserWarning: Warning. The treatment variable is not Binary.
                This is not necessarily a problem but it violates
                the assumption of a simple IV experiment.
                The coefficients should be interpreted appropriately.
  warnings.warn(
/Users/nathanielforde/mambaforge/envs/CausalPy/lib/python3.13/site-packages/causalpy/pymc_models.py:699: UserWarning: Variable selection priors specified. The 'mus' and 'sigmas' in the priors dict will be ignored for beta coefficients. Only 'eta' and 'lkj_sd' will be used.
  warnings.warn(
--------------------------------------------------------------------------------
Model 2: Spike-and-Slab Priors
--------------------------------------------------------------------------------
Initializing NUTS using jitter+adapt_diag...
Multiprocess sampling (4 chains in 4 jobs)
NUTS: [pi_beta_t, beta_t_raw, gamma_beta_t_u, pi_beta_z, beta_z_raw, gamma_beta_z_u, chol_cov]
OMP: Info #276: omp_set_nested routine deprecated, please use omp_set_max_active_levels instead.
OMP: Info #276: omp_set_nested routine deprecated, please use omp_set_max_active_levels instead.
OMP: Info #276: omp_set_nested routine deprecated, please use omp_set_max_active_levels instead.
OMP: Info #276: omp_set_nested routine deprecated, please use omp_set_max_active_levels instead.
/Users/nathanielforde/mambaforge/envs/CausalPy/lib/python3.13/site-packages/pytensor/compile/function/types.py:1039: RuntimeWarning: invalid value encountered in accumulate
  outputs = vm() if output_subset is None else vm(output_subset=output_subset)
/Users/nathanielforde/mambaforge/envs/CausalPy/lib/python3.13/site-packages/pytensor/compile/function/types.py:1039: RuntimeWarning: invalid value encountered in accumulate
  outputs = vm() if output_subset is None else vm(output_subset=output_subset)
/Users/nathanielforde/mambaforge/envs/CausalPy/lib/python3.13/site-packages/pytensor/compile/function/types.py:1039: RuntimeWarning: invalid value encountered in accumulate
  outputs = vm() if output_subset is None else vm(output_subset=output_subset)
/Users/nathanielforde/mambaforge/envs/CausalPy/lib/python3.13/site-packages/pytensor/compile/function/types.py:1039: RuntimeWarning: invalid value encountered in accumulate
  outputs = vm() if output_subset is None else vm(output_subset=output_subset)

Sampling 4 chains for 2_000 tune and 1_000 draw iterations (8_000 + 4_000 draws total) took 551 seconds.
There were 167 divergences after tuning. Increase `target_accept` or reparameterize.
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

The models have quite a distinct structure. Compare the normal IV model with non variable selection priors.

pm.model_to_graphviz(result_normal.model)
../_images/c74b6a590cb21a15b488ce6a5cf81e95a6b4f77bd50f1fd6ad26f300a1c2a83f.svg

Now compare the structure of the spike and slab model.

pm.model_to_graphviz(result_spike_slab.model)
../_images/6ad9d7beba4749daea3525ac14c5bbe99a9d7227959b702db704c71e3fb024b3.svg

Despite seeing some divergences in our spike and slab model, most other sampler health metrics seem healthy

az.plot_energy(result_spike_slab.idata, figsize=(20, 6));
../_images/c9c8a6d5c7195043af048e58fd854320ad59cff95c9d796f30422d1dba7ac509.png

And since we know the true data generating conditions we can also assess the derived posterior treatment estimates.

fig, ax = plt.subplots(figsize=(20, 6))
az.plot_posterior(
    result_normal.idata,
    var_names=["beta_z"],
    coords={"covariates": ["T_cont"]},
    ax=ax,
    label="Normal",
)
az.plot_posterior(
    result_spike_slab.idata,
    var_names=["beta_z"],
    coords={"covariates": ["T_cont"]},
    ax=ax,
    color="green",
    label="spike and slab",
)
ax.axvline(3, color="black", linestyle="--", label="True value");
../_images/def28d3c2ac1cca61c4fb562c1dfaa656941a1599671937d234df329063db4bc.png

This plot suggests that the spike and slab prior was better able to ignore noise in the process and zero in on the true effect. This will not always work but it is a sensible practice to at least sensitivity check difference between the estimates under different prior settings. We can observe how aggressively the spike and slab prior worked to cull unwanted variables from each model by comparing the values on the coefficients across each model

axs = az.plot_forest(
    [result_spike_slab.idata, result_normal.idata],
    var_names=["beta_z"],
    combined=True,
    model_names=["Spike and Slab", "Normal"],
    r_hat=True,
)
axs[0].set_title("Parameter Comparison Outcome Model \n Baseline v Spike and Slab");
../_images/3e3415ed14d6191739f0e90e087ef8946a11bef27937596e95162f979b725d59.png

The Treatment Model#

axs = az.plot_forest(
    [result_spike_slab.idata, result_normal.idata],
    var_names=["beta_t"],
    combined=True,
    model_names=["Spike and Slab", "Normal"],
    r_hat=True,
)

axs[0].set_title("Parameter Comparison Treatment Model \n Baseline v Spike and Slab");
../_images/3023f5838d3b28648d86ac0a9f44b04c8a008f49fb6c3c609a6f8cb033ca9074.png
summary = result_spike_slab.model.vs_prior_outcome.get_inclusion_probabilities(
    result_spike_slab.idata, "beta_z"
)
summary
prob selected gamma_mean
0 0.01750 False 0.020546
1 1.00000 True 0.991285
2 0.64100 True 0.659357
3 0.58200 True 0.616580
4 0.17625 False 0.192949
5 0.06075 False 0.068297
6 0.66600 True 0.702671
7 0.01000 False 0.012679
8 0.01300 False 0.016342
9 0.00975 False 0.012392
10 0.01700 False 0.019508
11 0.01625 False 0.021217
12 0.02000 False 0.024469
13 0.01825 False 0.023474
14 0.02700 False 0.031636
15 0.01650 False 0.020863

Horseshoe#

# =========================================================================
# Model 2: Horseshoe priors
# =========================================================================
print("\n" + "-" * 80)
print("Model 3: Horseshoe Priors")
print("-" * 80)

result_horseshoe = cp.InstrumentalVariable(
    instruments_data=instruments_data,
    data=data,
    instruments_formula=instruments_formula,
    formula=formula,
    model=cp.pymc_models.InstrumentalVariableRegression(sample_kwargs=sample_kwargs),
    vs_prior_type="horseshoe",
)
--------------------------------------------------------------------------------
Model 3: Horseshoe Priors
--------------------------------------------------------------------------------
/Users/nathanielforde/mambaforge/envs/CausalPy/lib/python3.13/site-packages/causalpy/experiments/instrumental_variable.py:187: UserWarning: Warning. The treatment variable is not Binary.
                This is not necessarily a problem but it violates
                the assumption of a simple IV experiment.
                The coefficients should be interpreted appropriately.
  warnings.warn(
/Users/nathanielforde/mambaforge/envs/CausalPy/lib/python3.13/site-packages/causalpy/pymc_models.py:699: UserWarning: Variable selection priors specified. The 'mus' and 'sigmas' in the priors dict will be ignored for beta coefficients. Only 'eta' and 'lkj_sd' will be used.
  warnings.warn(
Initializing NUTS using jitter+adapt_diag...
Multiprocess sampling (4 chains in 4 jobs)
NUTS: [tau_beta_t, lambda_beta_t, c2_beta_t, beta_t_raw, tau_beta_z, lambda_beta_z, c2_beta_z, beta_z_raw, chol_cov]
OMP: Info #276: omp_set_nested routine deprecated, please use omp_set_max_active_levels instead.
OMP: Info #276: omp_set_nested routine deprecated, please use omp_set_max_active_levels instead.
OMP: Info #276: omp_set_nested routine deprecated, please use omp_set_max_active_levels instead.
OMP: Info #276: omp_set_nested routine deprecated, please use omp_set_max_active_levels instead.
/Users/nathanielforde/mambaforge/envs/CausalPy/lib/python3.13/site-packages/pytensor/compile/function/types.py:1039: RuntimeWarning: invalid value encountered in accumulate
  outputs = vm() if output_subset is None else vm(output_subset=output_subset)
/Users/nathanielforde/mambaforge/envs/CausalPy/lib/python3.13/site-packages/pytensor/compile/function/types.py:1039: RuntimeWarning: invalid value encountered in accumulate
  outputs = vm() if output_subset is None else vm(output_subset=output_subset)
/Users/nathanielforde/mambaforge/envs/CausalPy/lib/python3.13/site-packages/pytensor/compile/function/types.py:1039: RuntimeWarning: invalid value encountered in accumulate
  outputs = vm() if output_subset is None else vm(output_subset=output_subset)
/Users/nathanielforde/mambaforge/envs/CausalPy/lib/python3.13/site-packages/pytensor/compile/function/types.py:1039: RuntimeWarning: invalid value encountered in accumulate
  outputs = vm() if output_subset is None else vm(output_subset=output_subset)

Sampling 4 chains for 2_000 tune and 1_000 draw iterations (8_000 + 4_000 draws total) took 644 seconds.
There were 11 divergences after tuning. Increase `target_accept` or reparameterize.
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
summary = result_horseshoe.model.vs_prior_outcome.get_shrinkage_factors(
    result_horseshoe.idata, "beta_z"
)
summary
[autoreload of cutils_ext failed: Traceback (most recent call last):
  File "/Users/nathanielforde/mambaforge/envs/CausalPy/lib/python3.13/site-packages/IPython/extensions/autoreload.py", line 325, in check
    superreload(m, reload, self.old_objects)
    ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/nathanielforde/mambaforge/envs/CausalPy/lib/python3.13/site-packages/IPython/extensions/autoreload.py", line 580, in superreload
    module = reload(module)
  File "/Users/nathanielforde/mambaforge/envs/CausalPy/lib/python3.13/importlib/__init__.py", line 128, in reload
    raise ModuleNotFoundError(f"spec not found for the module {name!r}", name=name)
ModuleNotFoundError: spec not found for the module 'cutils_ext'
]
shrinkage_factor lambda_tilde tau
0 0.016681 1.222294 0.038199
1 0.847573 62.105904 0.038199
2 0.164625 12.062918 0.038199
3 0.136282 9.986066 0.038199
4 0.090871 6.658563 0.038199
5 0.030253 2.216801 0.038199
6 0.199524 14.620131 0.038199
7 0.015415 1.129522 0.038199
8 0.015330 1.123328 0.038199
9 0.015992 1.171778 0.038199
10 0.015676 1.148635 0.038199
11 0.020536 1.504805 0.038199
12 0.021961 1.609202 0.038199
13 0.020631 1.511758 0.038199
14 0.021207 1.553953 0.038199
15 0.019708 1.444109 0.038199
fig, ax = plt.subplots(figsize=(20, 6))
az.plot_posterior(
    result_normal.idata, var_names=["beta_z"], coords={"covariates": ["T_cont"]}, ax=ax
)
az.plot_posterior(
    result_horseshoe.idata,
    var_names=["beta_z"],
    coords={"covariates": ["T_cont"]},
    ax=ax,
)
ax.axvline(3, color="black", linestyle="--")
<matplotlib.lines.Line2D at 0x2a238b390>
../_images/a8bfa81a1da725bec3092d8f401eae63c549b7273d72ba4810862e9041b19fad.png