Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
probml
GitHub Repository: probml/pyprobml
Path: blob/master/notebooks/book2/15/linreg_hierarchical_pymc3.ipynb
1193 views
Kernel: Python 3

Open In Colab

Hierarchical Bayesian Linear Regression in PyMC3

The text and code for this notebook are taken directly from this blog post by Thomas Wiecki and Danne Elbers. Original notebook.

Gelman et al.'s (2007) radon dataset is a classic for hierarchical modeling. In this dataset the amount of the radioactive gas radon has been measured among different households in all county's of several states. Radon gas is known to be the highest cause of lung cancer in non-smokers. It is believed to enter the house through the basement. Moreover, its concentration is thought to differ regionally due to different types of soil.

Here we'll investigate this difference and try to make predictions of radon levels in different countys and where in the house radon was measured. In this example we'll look at Minnesota, a state that contains 85 county's in which different measurements are taken, ranging from 2 till 80 measurements per county.

First, we'll load the data:

%matplotlib inline import matplotlib.pyplot as plt import numpy as np import pymc3 as pm import pandas as pd url = "https://github.com/twiecki/WhileMyMCMCGentlySamples/blob/master/content/downloads/notebooks/radon.csv?raw=true" data = pd.read_csv(url) county_names = data.county.unique() county_idx = data["county_code"].values
!pip install arviz import arviz
Collecting arviz Downloading https://files.pythonhosted.org/packages/a9/05/54183e9e57b0793eceb67361dbf4a7c4ed797ae36a04a3287791a564568c/arviz-0.10.0-py3-none-any.whl (1.5MB) |████████████████████████████████| 1.5MB 4.7MB/s Collecting netcdf4 Downloading https://files.pythonhosted.org/packages/09/39/3687b2ba762a709cd97e48dfaf3ae36a78ae603ec3d1487f767ad58a7b2e/netCDF4-1.5.4-cp36-cp36m-manylinux1_x86_64.whl (4.3MB) |████████████████████████████████| 4.3MB 40.0MB/s Requirement already satisfied: numpy>=1.12 in /usr/local/lib/python3.6/dist-packages (from arviz) (1.18.5) Requirement already satisfied: setuptools>=38.4 in /usr/local/lib/python3.6/dist-packages (from arviz) (50.3.2) Requirement already satisfied: packaging in /usr/local/lib/python3.6/dist-packages (from arviz) (20.4) Requirement already satisfied: matplotlib>=3.0 in /usr/local/lib/python3.6/dist-packages (from arviz) (3.2.2) Requirement already satisfied: pandas>=0.23 in /usr/local/lib/python3.6/dist-packages (from arviz) (1.1.4) Collecting xarray>=0.16.1 Downloading https://files.pythonhosted.org/packages/7a/cc/62ca520e349e63b05ce43c781757cbd3bea71d83ece96f2176763b57e8c2/xarray-0.16.1-py3-none-any.whl (720kB) |████████████████████████████████| 727kB 36.7MB/s Requirement already satisfied: scipy>=0.19 in /usr/local/lib/python3.6/dist-packages (from arviz) (1.4.1) Collecting cftime Downloading https://files.pythonhosted.org/packages/66/60/bad8525d2c046eb2964911bc412a85ba240b31c7b43f0c19377233992c6c/cftime-1.3.0-cp36-cp36m-manylinux1_x86_64.whl (295kB) |████████████████████████████████| 296kB 48.4MB/s Requirement already satisfied: six in /usr/local/lib/python3.6/dist-packages (from packaging->arviz) (1.15.0) Requirement already satisfied: pyparsing>=2.0.2 in /usr/local/lib/python3.6/dist-packages (from packaging->arviz) (2.4.7) Requirement already satisfied: kiwisolver>=1.0.1 in /usr/local/lib/python3.6/dist-packages (from matplotlib>=3.0->arviz) (1.3.1) Requirement already satisfied: cycler>=0.10 in /usr/local/lib/python3.6/dist-packages (from matplotlib>=3.0->arviz) (0.10.0) Requirement already satisfied: python-dateutil>=2.1 in /usr/local/lib/python3.6/dist-packages (from matplotlib>=3.0->arviz) (2.8.1) Requirement already satisfied: pytz>=2017.2 in /usr/local/lib/python3.6/dist-packages (from pandas>=0.23->arviz) (2018.9) Installing collected packages: cftime, netcdf4, xarray, arviz Found existing installation: xarray 0.15.1 Uninstalling xarray-0.15.1: Successfully uninstalled xarray-0.15.1 Successfully installed arviz-0.10.0 cftime-1.3.0 netcdf4-1.5.4 xarray-0.16.1

The relevant part of the data we will model looks as follows:

data[["county", "log_radon", "floor"]].head()

As you can see, we have multiple radon measurements (log-converted to be on the real line) in a county and whether the measurement has been taken in the basement (floor == 0) or on the first floor (floor == 1). Here we want to test the prediction that radon concentrations are higher in the basement.

The Models

Pooling of measurements

Now you might say: "That's easy! I'll just pool all my data and estimate one big regression to asses the influence of measurement across all counties". In math-speak that model would be:

radoni,c=α+βfloori,c+ϵradon_{i, c} = \alpha + \beta*\text{floor}_{i, c} + \epsilon

Where ii represents the measurement, cc the county and floor contains which floor the measurement was made. If you need a refresher on Linear Regressions in PyMC3, check out my previous blog post. Critically, we are only estimating one intercept and one slope for all measurements over all counties.

Separate regressions

But what if we are interested whether different counties actually have different relationships (slope) and different base-rates of radon (intercept)? Then you might say "OK then, I'll just estimate nn (number of counties) different regresseions -- one for each county". In math-speak that model would be:

radoni,c=αc+βcfloori,c+ϵcradon_{i, c} = \alpha_{c} + \beta_{c}*\text{floor}_{i, c} + \epsilon_c

Note that we added the subindex cc so we are estimating nn different α\alphas and β\betas -- one for each county.

This is the extreme opposite model, where above we assumed all counties are exactly the same, here we are saying that they share no similarities whatsoever which ultimately is also unsatisifying.

Hierarchical Regression: The best of both worlds

Fortunately there is a middle ground to both of these extreme views. Specifically, we may assume that while α\alphas and β\betas are different for each county, the coefficients all come from a common group distribution:

αcN(μα,σα2)\alpha_{c} \sim \mathcal{N}(\mu_{\alpha}, \sigma_{\alpha}^2)βcN(μβ,σβ2)\beta_{c} \sim \mathcal{N}(\mu_{\beta}, \sigma_{\beta}^2)

We thus assume the intercepts α\alpha and slopes β\beta to come from a normal distribution centered around their respective group mean μ\mu with a certain standard deviation σ2\sigma^2, the values (or rather posteriors) of which we also estimate. That's why this is called multilevel or hierarchical modeling.

How do we estimate such a complex model with all these parameters you might ask? Well, that's the beauty of Probabilistic Programming -- we just formulate the model we want and press our Inference Button(TM).

Note that the above is not a complete Bayesian model specification as we haven't defined priors or hyperpriors (i.e. priors for the group distribution, μ\mu and σ\sigma). These will be used in the model implementation below but only distract here.

Probabilistic Programming

Individual/non-hierarchical model

To really highlight the effect of the hierarchical linear regression we'll first estimate the non-hierarchical Bayesian model from above (separate regressions). For each county a new estimate of the parameters is initiated. As we have no prior information on what the intercept or regressions could be we are placing a Normal distribution centered around 0 with a wide standard-deviation. We'll assume the measurements are normally distributed with noise ϵ\epsilon on which we place a Half-Cauchy distribution.

# takes about 45 minutes indiv_traces = {} for county_name in county_names: # Select subset of data belonging to county c_data = data.loc[data.county == county_name] c_data = c_data.reset_index(drop=True) c_log_radon = c_data.log_radon c_floor_measure = c_data.floor.values with pm.Model() as individual_model: # Intercept prior a = pm.Normal("alpha", mu=0, sigma=1) # Slope prior b = pm.Normal("beta", mu=0, sigma=1) # Model error prior eps = pm.HalfCauchy("eps", beta=1) # Linear model radon_est = a + b * c_floor_measure # Data likelihood y_like = pm.Normal("y_like", mu=radon_est, sigma=eps, observed=c_log_radon) # Inference button (TM)! trace = pm.sample(progressbar=False) indiv_traces[county_name] = trace
Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] The acceptance probability does not match the target. It is 0.8787883116657818, but should be close to 0.8. Try to increase the number of tuning steps. Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] The number of effective samples is smaller than 25% for some parameters. Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] There were 2 divergences after tuning. Increase `target_accept` or reparameterize. Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] There were 5 divergences after tuning. Increase `target_accept` or reparameterize. There were 11 divergences after tuning. Increase `target_accept` or reparameterize. The estimated number of effective samples is smaller than 200 for some parameters. Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] The acceptance probability does not match the target. It is 0.8879638611360473, but should be close to 0.8. Try to increase the number of tuning steps. Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] The acceptance probability does not match the target. It is 0.888361176465224, but should be close to 0.8. Try to increase the number of tuning steps. The acceptance probability does not match the target. It is 0.8896695632402015, but should be close to 0.8. Try to increase the number of tuning steps. The estimated number of effective samples is smaller than 200 for some parameters. Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] There were 12 divergences after tuning. Increase `target_accept` or reparameterize. There were 34 divergences after tuning. Increase `target_accept` or reparameterize. The estimated number of effective samples is smaller than 200 for some parameters. Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] The number of effective samples is smaller than 25% for some parameters. Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] There were 5 divergences after tuning. Increase `target_accept` or reparameterize. There were 18 divergences after tuning. Increase `target_accept` or reparameterize. Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] There was 1 divergence after tuning. Increase `target_accept` or reparameterize. There was 1 divergence after tuning. Increase `target_accept` or reparameterize. The estimated number of effective samples is smaller than 200 for some parameters. Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] There were 23 divergences after tuning. Increase `target_accept` or reparameterize. There were 41 divergences after tuning. Increase `target_accept` or reparameterize. Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] The estimated number of effective samples is smaller than 200 for some parameters. Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] There were 2 divergences after tuning. Increase `target_accept` or reparameterize. The acceptance probability does not match the target. It is 0.895837530718079, but should be close to 0.8. Try to increase the number of tuning steps. There were 15 divergences after tuning. Increase `target_accept` or reparameterize. The number of effective samples is smaller than 25% for some parameters. Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] There were 11 divergences after tuning. Increase `target_accept` or reparameterize. There were 25 divergences after tuning. Increase `target_accept` or reparameterize. The number of effective samples is smaller than 25% for some parameters. Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] The number of effective samples is smaller than 25% for some parameters. Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] There were 2 divergences after tuning. Increase `target_accept` or reparameterize. There were 3 divergences after tuning. Increase `target_accept` or reparameterize. The estimated number of effective samples is smaller than 200 for some parameters. Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] There were 17 divergences after tuning. Increase `target_accept` or reparameterize. The acceptance probability does not match the target. It is 0.6656933653154277, but should be close to 0.8. Try to increase the number of tuning steps. There were 22 divergences after tuning. Increase `target_accept` or reparameterize. The acceptance probability does not match the target. It is 0.7158651396860832, but should be close to 0.8. Try to increase the number of tuning steps. The estimated number of effective samples is smaller than 200 for some parameters. Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] There were 27 divergences after tuning. Increase `target_accept` or reparameterize. The acceptance probability does not match the target. It is 0.711253697870715, but should be close to 0.8. Try to increase the number of tuning steps. There were 31 divergences after tuning. Increase `target_accept` or reparameterize. The estimated number of effective samples is smaller than 200 for some parameters. Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] The acceptance probability does not match the target. It is 0.88190038620472, but should be close to 0.8. Try to increase the number of tuning steps. Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] The acceptance probability does not match the target. It is 0.8842276342469811, but should be close to 0.8. Try to increase the number of tuning steps. Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] There was 1 divergence after tuning. Increase `target_accept` or reparameterize. There was 1 divergence after tuning. Increase `target_accept` or reparameterize. Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] The acceptance probability does not match the target. It is 0.8879799606890425, but should be close to 0.8. Try to increase the number of tuning steps. Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] There were 8 divergences after tuning. Increase `target_accept` or reparameterize. There were 18 divergences after tuning. Increase `target_accept` or reparameterize. The number of effective samples is smaller than 25% for some parameters. Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] There were 11 divergences after tuning. Increase `target_accept` or reparameterize. There were 25 divergences after tuning. Increase `target_accept` or reparameterize. Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha] There was 1 divergence after tuning. Increase `target_accept` or reparameterize. There were 35 divergences after tuning. Increase `target_accept` or reparameterize.

Hierarchical Model

Instead of initiating the parameters separatly, the hierarchical model initiates group parameters that consider the county's not as completely different but as having an underlying similarity. These distributions are subsequently used to influence the distribution of each county's α\alpha and β\beta.

with pm.Model() as hierarchical_model: # Hyperpriors mu_a = pm.Normal("mu_alpha", mu=0.0, sigma=1) sigma_a = pm.HalfCauchy("sigma_alpha", beta=1) mu_b = pm.Normal("mu_beta", mu=0.0, sigma=1) sigma_b = pm.HalfCauchy("sigma_beta", beta=1) # Intercept for each county, distributed around group mean mu_a a = pm.Normal("alpha", mu=mu_a, sigma=sigma_a, shape=len(data.county.unique())) # Intercept for each county, distributed around group mean mu_a b = pm.Normal("beta", mu=mu_b, sigma=sigma_b, shape=len(data.county.unique())) # Model error eps = pm.HalfCauchy("eps", beta=1) # Expected value radon_est = a[county_idx] + b[county_idx] * data.floor.values # Data likelihood y_like = pm.Normal("y_like", mu=radon_est, sigma=eps, observed=data.log_radon)
with hierarchical_model: hierarchical_trace = pm.sample()
Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Sequential sampling (2 chains in 1 job) NUTS: [eps, beta, alpha, sigma_beta, mu_beta, sigma_alpha, mu_alpha] 100%|██████████| 1000/1000 [00:06<00:00, 158.02it/s] 100%|██████████| 1000/1000 [00:05<00:00, 186.49it/s] There were 2 divergences after tuning. Increase `target_accept` or reparameterize. The acceptance probability does not match the target. It is 0.6974116479821089, but should be close to 0.8. Try to increase the number of tuning steps. There were 2 divergences after tuning. Increase `target_accept` or reparameterize. The estimated number of effective samples is smaller than 200 for some parameters.
pm.traceplot(hierarchical_trace);
Image in a Jupyter notebook
pm.traceplot(hierarchical_trace, var_names=["alpha", "beta"])
--------------------------------------------------------------------------- ImportError Traceback (most recent call last) <ipython-input-18-1a92d8337989> in <module>() ----> 1 pm.traceplot(hierarchical_trace, var_names=['alpha', 'beta']) /usr/local/lib/python3.6/dist-packages/pymc3/plots/__init__.py in wrapped(*args, **kwargs) 40 warnings.warn('Keyword argument `{old}` renamed to `{new}`, and will be removed in pymc3 3.8'.format(old=old, new=new)) 41 kwargs[new] = kwargs.pop(old) ---> 42 return func(*args, **kwargs) 43 return wrapped 44 /usr/local/lib/python3.6/dist-packages/pymc3/plots/__init__.py in __call__(self, *args, **kwargs) 20 def __call__(self, *args, **kwargs): 21 raise ImportError( ---> 22 "ArviZ is not installed. In order to use `{0.attr}`:\npip install arviz".format(self) 23 ) 24 ImportError: ArviZ is not installed. In order to use `plot_trace`: pip install arviz --------------------------------------------------------------------------- NOTE: If your import is failing due to a missing package, you can manually install dependencies using either !pip or !apt. To view examples of installing some common dependencies, click the "Open Examples" button below. ---------------------------------------------------------------------------

The marginal posteriors in the left column are highly informative. mu_a tells us the group mean (log) radon levels. mu_b tells us that the slope is significantly negative (no mass above zero), meaning that radon concentrations are higher in the basement than first floor. We can also see by looking at the marginals for a that there is quite some differences in radon levels between counties; the different widths are related to how much measurements we have per county, the more, the higher our confidence in that parameter estimate.

After writing this blog post I found out that the chains here (which look worse after I just re-ran them) are not properly converged, you can see that best for sigma_beta but also the warnings about "diverging samples" (which are also new in PyMC3). If you want to learn more about the problem and its solution, see my more recent blog post "Why hierarchical models are awesome, tricky, and Bayesian".

Posterior Predictive Check

The Root Mean Square Deviation

To find out which of the models works better we can calculate the Root Mean Square Deviaton (RMSD). This posterior predictive check revolves around recreating the data based on the parameters found at different moments in the chain. The recreated or predicted values are subsequently compared to the real data points, the model that predicts data points closer to the original data is considered the better one. Thus, the lower the RMSD the better.

When computing the RMSD (code not shown) we get the following result:

  • individual/non-hierarchical model: 0.13

  • hierarchical model: 0.08

As can be seen above the hierarchical model performs a lot better than the non-hierarchical model in predicting the radon values. Following this, we'll plot some examples of county's showing the true radon values, the hierarchial predictions and the non-hierarchical predictions.

selection = ["CASS", "CROW WING", "FREEBORN"] fig, axis = plt.subplots(1, 3, figsize=(12, 6), sharey=True, sharex=True) axis = axis.ravel() for i, c in enumerate(selection): c_data = data.loc[data.county == c] c_data = c_data.reset_index(drop=True) z = list(c_data["county_code"])[0] xvals = np.linspace(-0.2, 1.2) for a_val, b_val in zip(indiv_traces[c]["alpha"][::10], indiv_traces[c]["beta"][::10]): axis[i].plot(xvals, a_val + b_val * xvals, "b", alpha=0.05) axis[i].plot( xvals, indiv_traces[c]["alpha"][::10].mean() + indiv_traces[c]["beta"][::10].mean() * xvals, "b", alpha=1, lw=2.0, label="individual", ) for a_val, b_val in zip(hierarchical_trace["alpha"][::10][z], hierarchical_trace["beta"][::10][z]): axis[i].plot(xvals, a_val + b_val * xvals, "g", alpha=0.05) axis[i].plot( xvals, hierarchical_trace["alpha"][::10][z].mean() + hierarchical_trace["beta"][::10][z].mean() * xvals, "g", alpha=1, lw=2.0, label="hierarchical", ) axis[i].scatter( c_data.floor + np.random.randn(len(c_data)) * 0.01, c_data.log_radon, alpha=1, color="k", marker=".", s=80, label="original data", ) axis[i].set_xticks([0, 1]) axis[i].set_xticklabels(["basement", "first floor"]) axis[i].set_ylim(-1, 4) axis[i].set_title(c) if not i % 3: axis[i].legend() axis[i].set_ylabel("log radon level")
Image in a Jupyter notebook

In the above plot we have the data points in black of three selected counties. The thick lines represent the mean estimate of the regression line of the individual (blue) and hierarchical model (in green). The thinner lines are regression lines of individual samples from the posterior and give us a sense of how variable the estimates are.

When looking at the county 'CASS' we see that the non-hierarchical estimation has huge uncertainty about the radon levels of first floor measurements -- that's because we don't have any measurements in this county. The hierarchical model, however, is able to apply what it learned about the relationship between floor and radon-levels from other counties to CASS and make sensible predictions even in the absence of measurements.

We can also see how the hierarchical model produces more robust estimates in 'CROW WING' and 'FREEBORN'. In this regime of few data points the non-hierarchical model reacts more strongly to individual data points because that's all it has to go on.

Having the group-distribution constrain the coefficients we get meaningful estimates in all cases as we apply what we learn from the group to the individuals and vice-versa.

Shrinkage

Shrinkage describes the process by which our estimates are "pulled" towards the group-mean as a result of the common group distribution -- county-coefficients very far away from the group mean have very low probability under the normality assumption. In the non-hierachical model every county is allowed to differ completely from the others by just using each county's data, resulting in a model more prone to outliers (as shown above).

hier_a = hierarchical_trace["alpha"].mean(axis=0) hier_b = hierarchical_trace["beta"].mean(axis=0) indv_a = [indiv_traces[c]["alpha"].mean() for c in county_names] indv_b = [indiv_traces[c]["beta"].mean() for c in county_names]
fig = plt.figure(figsize=(10, 10)) ax = fig.add_subplot( 111, xlabel="Intercept", ylabel="Floor Measure", title="Hierarchical vs. Non-hierarchical Bayes", xlim=(0.25, 2), ylim=(-2, 1.5), ) ax.scatter(indv_a, indv_b, s=26, alpha=0.4, label="non-hierarchical") ax.scatter(hier_a, hier_b, c="red", s=26, alpha=0.4, label="hierarchical") for i in range(len(indv_b)): ax.arrow( indv_a[i], indv_b[i], hier_a[i] - indv_a[i], hier_b[i] - indv_b[i], fc="k", ec="k", length_includes_head=True, alpha=0.4, head_width=0.02, ) ax.legend();
Image in a Jupyter notebook

In the shrinkage plot above we show the coefficients of each county's non-hierarchical posterior mean (blue) and the hierarchical posterior mean (red). To show the effect of shrinkage on a single coefficient-pair (alpha and beta) we connect the blue and red points belonging to the same county by an arrow. Some non-hierarchical posteriors are so far out that we couldn't display them in this plot (it makes the axes to wide). Interestingly, all hierarchical posteriors of the floor-measure seem to be around -0.6 confirming out prediction that radon levels are higher in the basement than in the first floor. The differences in intercepts (which we take for type of soil) differs among countys indicating that meaningful regional differences exist in radon concentration. This information would have been difficult to find when just the non-hierarchial model had been used and estimates for individual counties would have been much more noisy.

Summary

In this post, co-authored by Danne Elbers, we showed how a multi-level hierarchical Bayesian model gives the best of both worlds when we have multiple sets of measurements we expect to have similarity. The naive approach either pools all data together and ignores the individual differences, or treats each set as completely separate leading to noisy estimates as shown above. By placing a group distribution on the individual sets we can learn about each set and the group simultaneously. Probabilistic Programming in PyMC then makes Bayesian estimation of this model trivial.

References