import arviz as az
import numpy as np
import pandas as pd
import pymc as pm
rng = np.random.default_rng(6420)
8. Prediction*#
We previously covered Bayesian prediction in Unit 4, Lesson 13. Recall that the core of Bayesian prediction is the posterior predictive distribution
Think of this as a “weighted average” of the likelihood for new data, with “weights” prescribed by for \(\pi(\theta \vert x_{1} , \cdots , x_{n})\). This is a departure from the “plug-in predictions” you may have encountered in a frequentist course. The result is a distribution of predictions. It should be clear by now that analytical methods are often intractable, and that is no different for the posterior predictive distribution. Fortunately, pymc has some tools that make this easier.
Taste of Cheese*#
Adapted from Unit 6: cheese.odc.
The link in the original .odc file is dead. I downloaded the data from here and have a copy here.
Problem Statement#
As cheddar cheese matures, a variety of chemical processes take place. The taste of matured cheese is related to the concentration of several chemicals in the final product. In a study of cheddar cheese from the Latrobe Valley of Victoria, Australia, samples of cheese were analyzed for their chemical composition and were subjected to taste tests. Overall taste scores were obtained by combining the scores from several tasters.
Can the score be predicted well by the predictors: Acetic, H2S, and Lactic?
data = pd.read_csv("../data/cheese.csv", index_col=0)
X = data[["Acetic", "H2S", "Lactic"]].to_numpy()
# add intercept column to X
X_aug = np.concatenate((np.ones((X.shape[0], 1)), X), axis=1)
y = data["taste"].to_numpy()
# out of sample data for our later predictions
new_obs = np.array([[1.0, 5.0, 7.1, 1.5]])
data.head(5)
| taste | Acetic | H2S | Lactic | |
|---|---|---|---|---|
| 1 | 12.3 | 4.543 | 3.135 | 0.86 |
| 2 | 20.9 | 5.159 | 5.043 | 1.53 |
| 3 | 39.0 | 5.366 | 5.438 | 1.57 |
| 4 | 47.9 | 5.759 | 7.496 | 1.81 |
| 5 | 5.6 | 4.663 | 3.807 | 0.99 |
There are multiple ways to get predictions on out-of-sample data in PyMC. One way is to set up a shared variable using pm.Data in the original model, then using pm.set_data to change to the new observations before calling pm.sample_posterior_predictive. If you’re doing this, it’s useful to tie the shape of the likelihood to the number of rows in your X data. Note the shape=X_data.shape[0] line in the likelihood.
Another way is to do what we call “BUGS-style” posterior predictives. This is just setting up the posterior predictive within the model context, as labeled with a comment below.
with pm.Model() as m:
# associate data with model
X_data = pm.Data("X", X_aug)
y_data = pm.Data("y", y)
# priors
beta = pm.Normal("beta", mu=0, sigma=1000, shape=X.shape[1] + 1)
tau = pm.Gamma("tau", alpha=0.001, beta=0.001)
sigma = pm.Deterministic("sigma", 1 / pm.math.sqrt(tau))
mu = pm.math.dot(X_data, beta)
# likelihood
pm.Normal(
"taste_score",
mu=mu,
sigma=sigma,
observed=y_data,
shape=X_data.shape[0],
)
oos_mean_response = pm.Deterministic(
"oos_mean_response", pm.math.dot(new_obs, beta)
)
pm.Normal(
"oos_prediction", oos_mean_response, sigma=sigma
) # BUGS-style post. pred.
# start sampling
trace = pm.sample(5000, target_accept=0.95, random_seed=rng)
az.summary(trace, ci_prob=0.95)
| mean | sd | eti95_lb | eti95_ub | ess_bulk | ess_tail | r_hat | mcse_mean | mcse_sd | |
|---|---|---|---|---|---|---|---|---|---|
| beta[0] | -29.4 | 20.5 | -71 | 11 | 4347 | 5445 | 1.00 | 0.31 | 0.23 |
| beta[1] | 0.5 | 4.6 | -8.6 | 9.7 | 4226 | 5232 | 1.00 | 0.071 | 0.054 |
| beta[2] | 3.9 | 1.3 | 1.3 | 6.5 | 7713 | 8903 | 1.00 | 0.015 | 0.012 |
| beta[3] | 19.6 | 8.9 | 1.7 | 37 | 7314 | 8326 | 1.00 | 0.1 | 0.079 |
| oos_prediction[0] | 29.9 | 11.4 | 7.4 | 52 | 15036 | 12078 | 1.00 | 0.093 | 0.069 |
| tau | 0.00978 | 0.00271 | 0.0052 | 0.016 | 14779 | 12317 | 1.00 | 2.2e-05 | 1.7e-05 |
| sigma | 10.41 | 1.51 | 8 | 14 | 14779 | 12317 | 1.00 | 0.013 | 0.011 |
| oos_mean_response[0] | 30 | 3.76 | 22 | 37 | 5632 | 7331 | 1.00 | 0.05 | 0.038 |
# single prediction on out-of-sample data (new_obs defined above)
pm.set_data({"X": new_obs}, model=m)
ppc = pm.sample_posterior_predictive(trace, model=m, predictions=True)
az.summary(ppc.predictions, group="predictions", ci_prob=0.95)
| mean | sd | eti95_lb | eti95_ub | ess_bulk | ess_tail | r_hat | mcse_mean | mcse_sd | |
|---|---|---|---|---|---|---|---|---|---|
| taste_score[0] | 29.9 | 11.1 | 8.2 | 52 | 15777 | 17502 | 1.00 | 0.089 | 0.066 |
%load_ext watermark
%watermark -n -u -v -iv -p pytensor
Last updated: Sun, 18 Oct 2026
Python implementation: CPython
Python version : 3.14.7
IPython version : 9.16.1
pytensor: 3.3.0
arviz : 1.3.0
numpy : 2.4.6
pandas: 3.0.5
pymc : 6.3.1