import pymc as pm
import numpy as np
import arviz as az
import pytensor.tensor as pt
rng = np.random.default_rng(6420)
17. Multinomial regression*#
Adapted from Unit 7: NHANESmulti.odc
Problem statement#
The National Health and Nutrition Examination Survey (NHANES) is a program of studies designed to assess the health and nutritional status of adults and children in the United States. The survey is unique in that it combines interviews and physical examinations.
Assume that \(N\) subjects select a choice from \(K\) categories. The \(i\text{-th}\) subject is characterized by 3 covariates x[i, 1], x[i, 2], and x[i, 3]. Given the covariates, model the probability of a subject selecting the category \(k\), where \(k \in \{1,...,K\}\).
# data
# fmt: off
y = np.array([[1, 0, 0, 0, 0],
[0, 1, 0, 0, 0],
[1, 0, 0, 0, 0],
[0, 0, 1, 0, 0],
[0, 1, 0, 0, 0],
[0, 0, 1, 0, 0],
[0, 0, 0, 1, 0],
[0, 0, 0, 0, 1],
[0, 0, 0, 0, 1],
[0, 0, 0, 1, 0]])
X = np.array([[2, 4, 9],
[1, 5, 10],
[1, 6, 14],
[2, 4, 21],
[2, 4, 22],
[2, 6, 30],
[3, 3, 33],
[3, 2, 36],
[3, 1, 40],
[4, 1, 44]])
# fmt: on
# N = 10, P = 4 (intercept + 3 predictors)
X_aug = np.concatenate((np.ones((X.shape[0], 1)), X), axis=1)
N, P = X_aug.shape
K = y.shape[1]
The Multinomial distribution has two parameters, \(n\) and \(p\). See the PyMC Documentation for the Multinomial distribution class
Argument |
What it means |
In this model |
|---|---|---|
|
Total number of independent trials per replicate. A Multinomial with \(n = 1\) is a one‑hot vector. Must equal the sum (per-row) of the corresponding observed values. |
Each survey respondent makes exactly one choice, so |
|
Vector (or matrix) of category probabilities. Must sum to 1 along the last axis. |
We generate |
|
Either a one‑hot matrix (\(n = 1\)) or a count matrix (if \(n > 1\)). Shape must match |
For this example every row of |
with pm.Model() as m:
y_data = pm.Data("y", y)
X_data = pm.Data("X", X_aug)
_beta = pm.Normal("_beta", mu=0, tau=0.1, shape=(P, K - 1))
beta = pt.concatenate([pt.zeros((P, 1)), _beta], axis=1)
eta = pm.math.dot(X_data, beta)
p = pm.Deterministic(
"p", pm.math.softmax(eta, axis=1)
) # saving for probability prediction as well
pm.Multinomial("likelihood", n=1, p=p, observed=y_data, shape=X_data.shape)
trace = pm.sample(10000, random_seed=rng)
NUTS[nutpie]: [_beta]
X_new = np.array([1, 3, 3, 30]).reshape((1, 4))
with m:
pm.set_data({"X": X_new})
ppc = pm.sample_posterior_predictive(
trace, var_names=["p", "likelihood"], predictions=True
)
Sampling: [likelihood]
Notice the more useful credible intervals on the probabilities (p) than with the predictions for y/likelihood.
az.summary(ppc, group="predictions")
| mean | sd | eti89_lb | eti89_ub | ess_bulk | ess_tail | r_hat | mcse_mean | mcse_sd | |
|---|---|---|---|---|---|---|---|---|---|
| p[0, 0] | 0.0079 | 0.042 | 1.4e-10 | 0.026 | 11486 | 13223 | 1.00 | 0.00023 | 0.0013 |
| p[0, 1] | 0.039 | 0.108 | 5e-07 | 0.22 | 35026 | 30613 | 1.00 | 0.00055 | 0.0013 |
| p[0, 2] | 0.118 | 0.19 | 5.5e-05 | 0.55 | 36996 | 32827 | 1.00 | 0.00097 | 0.0012 |
| p[0, 3] | 0.682 | 0.288 | 0.12 | 0.99 | 35569 | 33227 | 1.00 | 0.0015 | 0.0009 |
| p[0, 4] | 0.154 | 0.21 | 0.00093 | 0.63 | 32743 | 30727 | 1.00 | 0.0012 | 0.0012 |
| likelihood[0, 0] | 0.0081 | 0.09 | 0 | 0 | 38805 | 38805 | 1.00 | 0.00046 | 0.0025 |
| likelihood[0, 1] | 0.038 | 0.192 | 0 | 0 | 39811 | 39811 | 1.00 | 0.00096 | 0.0023 |
| likelihood[0, 2] | 0.118 | 0.323 | 0 | 1 | 39191 | 39191 | 1.00 | 0.0016 | 0.0019 |
| likelihood[0, 3] | 0.682 | 0.466 | 0 | 1 | 38573 | 38573 | 1.00 | 0.0024 | 0.00093 |
| likelihood[0, 4] | 0.153 | 0.36 | 0 | 1 | 38030 | 38030 | 1.00 | 0.0018 | 0.0018 |
%load_ext watermark
%watermark -n -u -v -iv
Last updated: Sun, 18 Oct 2026
Python implementation: CPython
Python version : 3.14.7
IPython version : 9.16.1
arviz : 1.3.0
numpy : 2.4.6
pymc : 6.3.1
pytensor: 3.3.0