diff --git a/docs/source/learn/core_notebooks/vector_variables.ipynb b/docs/source/learn/core_notebooks/vector_variables.ipynb new file mode 100644 index 0000000000..894b5dd4e0 --- /dev/null +++ b/docs/source/learn/core_notebooks/vector_variables.ipynb @@ -0,0 +1,995 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "777d3f58", + "metadata": {}, + "source": [ + "# Working with vector valued variables for multiple groups in PyMC\n", + "\n", + "This notebook is written as a response to a recurring question on GitHub and Discourse: \n", + "*“How do I handle multiple groups of data in PyMC and slice vector random variables correctly?”*\n", + "\n", + "The user’s example had several groups of observations and a vector of `mu` and `sigma` values, one for each group. \n", + "The confusing part was how to turn the group labels into something PyMC can index cleanly, and how to connect those indices to vector valued priors.\n", + "\n", + "To make the idea clear, I built a very small example with two levels:\n", + "\n", + "- a category (like Beverage or Snack)\n", + "- a family inside each category\n", + "\n", + "The goal here isn’t to create a big statistical model,\n", + "it’s just to show the exact pattern for:\n", + "- factorizing group labels \n", + "- building the mapping between levels \n", + "- creating vector RVs \n", + "- and slicing them correctly inside the likelihood \n", + "\n", + "Once you see this simple version working, the structure becomes easy to reuse in real models.\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "5269058d", + "metadata": {}, + "source": [ + "## 1. Setup\n", + "\n", + "Before jumping into the modeling part, we just import the usual tools: NumPy, pandas, ArviZ, and PyMC. \n", + "I also set a random seed so the results don’t wiggle around every time this notebook runs.\n", + "\n", + "One small thing I like doing , and it helps a lot later is defining named coordinates for the different group levels we’ll work with. \n", + "These labels make ArviZ’s output much easier to read because the plots will show actual names instead of axis numbers.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "e8f207b4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "PyMC version: 5.26.1\n" + ] + } + ], + "source": [ + "# Importing the basic libraries we need\n", + "# Nothing special here, just the usual stack for PyMC work\n", + "import arviz as az\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import pandas as pd\n", + "\n", + "import pymc as pm\n", + "\n", + "rng = np.random.default_rng(42)\n", + "az.style.use(\"arviz-darkgrid\")\n", + "\n", + "print(\"PyMC version:\", pm.__version__)" + ] + }, + { + "cell_type": "markdown", + "id": "113aa86d", + "metadata": {}, + "source": [ + "## 2. A small dataset to illustrate the idea\n", + "\n", + "The original GitHub issue used five groups of synthetic data. \n", + "To keep things intuitive here, I’m using a slightly more “real world sounding” example: categories and families.\n", + "\n", + "The dataset has three columns:\n", + "\n", + "- `category` \n", + "- `family` (which lives inside a category) \n", + "- `sales` (just some numeric values we’ll fit a model to)\n", + "\n", + "The exact numbers don’t matter what matters is that each row belongs to one family, and each family belongs to one category. \n", + "That structure is exactly the situation the user in the issue was dealing with.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "fdbe45c3", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
categoryfamilysales
0BeverageTea15.609434
1BeverageMilk7.920032
2BeverageSoft Drinks21.500902
3SnackChips8.881129
4SnackNuts1.097930
\n", + "
" + ], + "text/plain": [ + " category family sales\n", + "0 Beverage Tea 15.609434\n", + "1 Beverage Milk 7.920032\n", + "2 Beverage Soft Drinks 21.500902\n", + "3 Snack Chips 8.881129\n", + "4 Snack Nuts 1.097930" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Tiny example dataset\n", + "data = pd.DataFrame(\n", + " {\n", + " \"category\": [\"Beverage\", \"Beverage\", \"Beverage\", \"Snack\", \"Snack\"],\n", + " \"family\": [\"Tea\", \"Milk\", \"Soft Drinks\", \"Chips\", \"Nuts\"],\n", + " }\n", + ")\n", + "\n", + "# Pretend we observed some sales numbers (generated from a simple ground truth)\n", + "true_sales = {\n", + " \"Tea\": 15.0,\n", + " \"Milk\": 10.0,\n", + " \"Soft Drinks\": 20.0,\n", + " \"Chips\": 7.0,\n", + " \"Nuts\": 5.0,\n", + "}\n", + "data[\"sales\"] = [true_sales[f] + rng.normal(0, 2.0) for f in data[\"family\"]]\n", + "\n", + "data" + ] + }, + { + "cell_type": "markdown", + "id": "af402ace", + "metadata": {}, + "source": [ + "## 3. Turning text labels into indices and making the mapping\n", + "\n", + "The thing that usually trips people up is how to connect the observed labels to vector-valued priors.\n", + "\n", + "PyMC can only index arrays with integers, so the first step is simply:\n", + "- convert the category names into integer codes\n", + "- convert the family names into integer codes\n", + "\n", + "Once we have those, we make a small array that maps each family to its category. \n", + "For example: if the 0th family belongs to the 1st category, the mapping array contains a 1 at position 0.\n", + "\n", + "This mapping array is the heart of the whole trick. \n", + "It tells PyMC how the lower level “inherits” from the upper level.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bd665a51", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Category labels: ['Beverage', 'Snack']\n", + "Family labels: ['Tea', 'Milk', 'Soft Drinks', 'Chips', 'Nuts']\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
categoryfamilysalescat_codefam_code
0BeverageTea15.60943400
1BeverageMilk7.92003201
2BeverageSoft Drinks21.50090202
3SnackChips8.88112913
4SnackNuts1.09793014
\n", + "
" + ], + "text/plain": [ + " category family sales cat_code fam_code\n", + "0 Beverage Tea 15.609434 0 0\n", + "1 Beverage Milk 7.920032 0 1\n", + "2 Beverage Soft Drinks 21.500902 0 2\n", + "3 Snack Chips 8.881129 1 3\n", + "4 Snack Nuts 1.097930 1 4" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "family_to_category mapping (family index → category index): [0 0 0 1 1]\n", + "Length (should equal number of families): 5 vs 5\n" + ] + } + ], + "source": [ + "# Factorize categories and families into integer codes\n", + "cat_codes, cat_labels = pd.factorize(data[\"category\"])\n", + "fam_codes, fam_labels = pd.factorize(data[\"family\"])\n", + "\n", + "data[\"cat_code\"] = cat_codes\n", + "data[\"fam_code\"] = fam_codes\n", + "\n", + "print(\"Category labels:\", list(cat_labels))\n", + "print(\"Family labels:\", list(fam_labels))\n", + "display(data)\n", + "\n", + "# Build mapping\n", + "edges = data[[\"fam_code\", \"cat_code\"]].drop_duplicates().sort_values(\"fam_code\")\n", + "\n", + "family_to_category = edges[\"cat_code\"].to_numpy().astype(\"int64\")\n", + "\n", + "print(\"\\nfamily_to_category mapping (family index → category index):\", family_to_category)\n", + "print(\"Length (should equal number of families):\", len(family_to_category), \"vs\", len(fam_labels))" + ] + }, + { + "cell_type": "markdown", + "id": "b6b39354", + "metadata": {}, + "source": [ + "## 4. Building the PyMC model\n", + "\n", + "Now that the indices and mapping are ready, the model becomes fairly straightforward.\n", + "\n", + "We set up:\n", + "- one global intercept\n", + "- one category effect per category\n", + "- one family effect per family\n", + "\n", + "The important part is that the family effect is centered on the category effect using the mapping array we created earlier. \n", + "That’s exactly the pattern the user in the GitHub issue needed but wasn’t sure how to set up.\n", + "\n", + "For each observation, the expected value is:\n", + "\n", + "global_mu \n", + "+ the effect for its category \n", + "+ the effect for its family\n", + "\n", + "Once the indexing is correct, PyMC takes it from there.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "c7b47307", + "metadata": {}, + "outputs": [ + { + "data": { + "text/latex": [ + "$$\n", + " \\begin{array}{rcl}\n", + " \\text{global\\_mu} &\\sim & \\operatorname{Normal}(10,~10)\\\\\\text{sigma\\_cat} &\\sim & \\operatorname{HalfNormal}(0,~5)\\\\\\text{category\\_effect} &\\sim & \\operatorname{Normal}(\\text{global\\_mu},~\\text{sigma\\_cat})\\\\\\text{sigma\\_fam} &\\sim & \\operatorname{HalfNormal}(0,~3)\\\\\\text{family\\_effect} &\\sim & \\operatorname{Normal}(f(\\text{category\\_effect}),~\\text{sigma\\_fam})\\\\\\text{sigma\\_obs} &\\sim & \\operatorname{HalfNormal}(0,~2)\\\\\\text{sales} &\\sim & \\operatorname{Normal}(f(\\text{family\\_effect}),~\\text{sigma\\_obs})\n", + " \\end{array}\n", + " $$" + ], + "text/plain": [ + " global_mu ~ Normal(10, 10)\n", + " sigma_cat ~ HalfNormal(0, 5)\n", + "category_effect ~ Normal(global_mu, sigma_cat)\n", + " sigma_fam ~ HalfNormal(0, 3)\n", + " family_effect ~ Normal(f(category_effect), sigma_fam)\n", + " sigma_obs ~ HalfNormal(0, 2)\n", + " sales ~ Normal(f(family_effect), sigma_obs)" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "coords = {\n", + " \"category\": cat_labels,\n", + " \"family\": fam_labels,\n", + " \"obs\": np.arange(len(data)),\n", + "}\n", + "\n", + "with pm.Model(coords=coords) as model:\n", + " # Global mean\n", + " global_mu = pm.Normal(\"global_mu\", mu=10.0, sigma=10.0)\n", + "\n", + " # Level 0: category level vector variable\n", + " sigma_cat = pm.HalfNormal(\"sigma_cat\", sigma=5.0)\n", + " category_effect = pm.Normal(\n", + " \"category_effect\",\n", + " mu=global_mu,\n", + " sigma=sigma_cat,\n", + " dims=\"category\",\n", + " )\n", + "\n", + " # Level 1: family level vector variable, centered on its category\n", + " sigma_fam = pm.HalfNormal(\"sigma_fam\", sigma=3.0)\n", + " family_effect = pm.Normal(\n", + " \"family_effect\",\n", + " mu=category_effect[family_to_category],\n", + " sigma=sigma_fam,\n", + " dims=\"family\",\n", + " )\n", + "\n", + " # Observation model: each row uses its family's effect\n", + " sigma_obs = pm.HalfNormal(\"sigma_obs\", sigma=2.0)\n", + " mu = family_effect[fam_codes]\n", + "\n", + " sales = pm.Normal(\n", + " \"sales\",\n", + " mu=mu,\n", + " sigma=sigma_obs,\n", + " observed=data[\"sales\"].values,\n", + " dims=\"obs\",\n", + " )\n", + "\n", + "model" + ] + }, + { + "cell_type": "markdown", + "id": "2a52eb49", + "metadata": {}, + "source": [ + "## 5. Sampling\n", + "\n", + "Here we let PyMC run `pm.sample()` to draw posterior samples. \n", + "Because the dataset is tiny, this finishes quickly.\n", + "\n", + "In real modeling work, I’d look at the diagnostics (R-hat, effective sample size, divergences). \n", + "But since this notebook is mainly about *how* to wire up the hierarchical structure, I’m keeping this part simple.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "b8044151", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Initializing NUTS using jitter+adapt_diag...\n", + "Multiprocess sampling (4 chains in 4 jobs)\n", + "NUTS: [global_mu, sigma_cat, category_effect, sigma_fam, family_effect, sigma_obs]\n", + "Sampling 4 chains for 1_000 tune and 1_000 draw iterations (4_000 + 4_000 draws total) took 47 seconds.\n", + "There were 132 divergences after tuning. Increase `target_accept` or reparameterize.\n", + "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\n", + "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\n" + ] + } + ], + "source": [ + "%%capture sampling_output\n", + "\n", + "with model:\n", + " idata = pm.sample(\n", + " draws=1000,\n", + " tune=1000,\n", + " target_accept=0.95,\n", + " chains=4,\n", + " random_seed=42,\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "edfaacb3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sampling finished\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
meansdhdi_3%hdi_97%mcse_meanmcse_sdess_bulkess_tailr_hat
global_mu10.3883.9482.74317.8290.1280.116957.01060.01.00
category_effect[Beverage]13.1003.0437.16718.5090.1140.087708.0992.01.00
category_effect[Snack]7.6663.4511.30314.1430.1400.086607.0967.01.00
family_effect[Tea]14.8672.3559.92819.0030.1000.112680.0444.01.01
family_effect[Milk]9.5682.7925.35615.7090.1220.086594.01149.01.00
family_effect[Soft Drinks]18.8113.40612.02823.6870.2100.140307.0561.01.00
family_effect[Chips]8.4742.4053.57613.2310.0860.103834.0797.01.00
family_effect[Nuts]3.0943.050-1.5999.5500.1570.117442.0652.01.00
\n", + "
" + ], + "text/plain": [ + " mean sd hdi_3% hdi_97% mcse_mean \\\n", + "global_mu 10.388 3.948 2.743 17.829 0.128 \n", + "category_effect[Beverage] 13.100 3.043 7.167 18.509 0.114 \n", + "category_effect[Snack] 7.666 3.451 1.303 14.143 0.140 \n", + "family_effect[Tea] 14.867 2.355 9.928 19.003 0.100 \n", + "family_effect[Milk] 9.568 2.792 5.356 15.709 0.122 \n", + "family_effect[Soft Drinks] 18.811 3.406 12.028 23.687 0.210 \n", + "family_effect[Chips] 8.474 2.405 3.576 13.231 0.086 \n", + "family_effect[Nuts] 3.094 3.050 -1.599 9.550 0.157 \n", + "\n", + " mcse_sd ess_bulk ess_tail r_hat \n", + "global_mu 0.116 957.0 1060.0 1.00 \n", + "category_effect[Beverage] 0.087 708.0 992.0 1.00 \n", + "category_effect[Snack] 0.086 607.0 967.0 1.00 \n", + "family_effect[Tea] 0.112 680.0 444.0 1.01 \n", + "family_effect[Milk] 0.086 594.0 1149.0 1.00 \n", + "family_effect[Soft Drinks] 0.140 307.0 561.0 1.00 \n", + "family_effect[Chips] 0.103 834.0 797.0 1.00 \n", + "family_effect[Nuts] 0.117 442.0 652.0 1.00 " + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "print(\"Sampling finished\")\n", + "\n", + "az.summary(idata, var_names=[\"global_mu\", \"category_effect\", \"family_effect\"])" + ] + }, + { + "cell_type": "markdown", + "id": "3e783202", + "metadata": {}, + "source": [ + "## 6. Inspecting the results\n", + "\n", + "Now we look at the fitted parameters. \n", + "You should see something like:\n", + "\n", + "- category effects (one per category)\n", + "- family effects (one per family), roughly centered on their category’s effect\n", + "\n", + "Thanks to the named dimensions we defined earlier, ArviZ will label everything clearly in the plots. \n", + "That was one of the frustrations mentioned in the GitHub issue things got confusing fast without readable labels.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "11ef7bcb", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
meansdhdi_3%hdi_97%mcse_meanmcse_sdess_bulkess_tailr_hat
category_effect[Beverage]13.2333.0677.35118.9240.1170.101688.0830.01.0
category_effect[Snack]7.7313.5011.32314.2540.1370.086648.0718.01.0
family_effect[Tea]14.8142.4369.55018.9120.0930.112609.0909.01.0
family_effect[Milk]9.7312.8974.77515.3630.1680.152363.0477.01.0
family_effect[Soft Drinks]18.8283.39712.11523.9490.2340.128254.0681.01.0
family_effect[Chips]8.5052.3683.78312.9980.0900.107746.0600.01.0
family_effect[Nuts]3.2433.056-1.17010.0380.2010.147309.0437.01.0
\n", + "
" + ], + "text/plain": [ + " mean sd hdi_3% hdi_97% mcse_mean \\\n", + "category_effect[Beverage] 13.233 3.067 7.351 18.924 0.117 \n", + "category_effect[Snack] 7.731 3.501 1.323 14.254 0.137 \n", + "family_effect[Tea] 14.814 2.436 9.550 18.912 0.093 \n", + "family_effect[Milk] 9.731 2.897 4.775 15.363 0.168 \n", + "family_effect[Soft Drinks] 18.828 3.397 12.115 23.949 0.234 \n", + "family_effect[Chips] 8.505 2.368 3.783 12.998 0.090 \n", + "family_effect[Nuts] 3.243 3.056 -1.170 10.038 0.201 \n", + "\n", + " mcse_sd ess_bulk ess_tail r_hat \n", + "category_effect[Beverage] 0.101 688.0 830.0 1.0 \n", + "category_effect[Snack] 0.086 648.0 718.0 1.0 \n", + "family_effect[Tea] 0.112 609.0 909.0 1.0 \n", + "family_effect[Milk] 0.152 363.0 477.0 1.0 \n", + "family_effect[Soft Drinks] 0.128 254.0 681.0 1.0 \n", + "family_effect[Chips] 0.107 746.0 600.0 1.0 \n", + "family_effect[Nuts] 0.147 309.0 437.0 1.0 " + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "az.summary(idata, var_names=[\"category_effect\", \"family_effect\"])" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "47795582", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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8ojFS3Jysl1wwQXu33v27HEhcsWKFfbUU0+5rzPPPNNuv/12K1WqVEzP++abb7owmqhhTNavX08YA4A8QBhDvtuxY4eNHDnSypUrZ88//7zVrVs33f27d++2RYsW5fp1fv31V2vRooXFU0pKil199dUZbl+9erXddNNNNm3aNLf9o0ePjul5DznkkDhuJQAkhjVrQ7Z+vVn16vr/p++tSVyEMeS7b775xrZv3+6GMCODmJQsWdJatWqV4XZVop599llXVVu7dq17nHrBLr74YmvTpk3a4wYNGmSvvPKK+7dCXTC8qEpbcHvksGO/fv2ihqzs0nOpMnbaaafZq6++6vq/mjRp4u5buHCh9erVy72Ghl8ffPBBW7p0qetpU4gLfr9ly5b2zDPPZHgf7777rqvy6b2rX6tq1aquAnfllVe6ode9+fnnn+2iiy6yn376ye677z7XQycLFiywJ554wlW7Nm3a5Kp+qiSefvrp1r179xzvCwB+bdwU8n4INm8J2ZixZkuW/u+25kdutuuuTbVKFZNy9JwpyTn7vYKAMIZ8V6lSJfddwSI1NTVbgSIUCrlJAG+//bbVrl3bzj//fPvrr7/srbfesssvv9yGDBniAk8wNFq9enUXevQ9GO5U75p+Vp+a9O7dO+35FYRyq3LlynbuuefaQw895IYdgzAWUG/chAkT7KijjrKzzz7bhaTs9r4pVCpEKaQqnD3wwAOuAre3iRHffvutC6uqRiosHnnkke72uXPnWt++fV0PnULxvvvua3/++aetXLnShUnCGAq7HTv8B5a80rmL//em/62XK2c2/PYka9rYbOlys9Fj91ivPmapqTnbvjmzzJsyZfI2CBLGkO/U8K4mffVaKRApLDVt2tSFrOLFi0f9nZkzZ7ogptD05JNPpvVkXXHFFa4nTYFFYaVGjRoujOkrCGPhFS/dHlTHclMJy0wwLLp8+fIM93388cc2atQoO+uss2J6Tu0nBaT99tvP/ayKWMeOHV0V7aqrrsq0P03hT4FL96uqFl6F1HCqAu7kyZMzTDpQHxxQ2J3UyX9gKcxSU81uGJBkbdv8G2LattEf1WZDh4UK5DGbNzdvwxhLWyDfJSUl2fjx4+3www93FZ/BgwfbKaec4qo2ffr0ccOQ//zzT7rfCQLUDTfckC587L///u53VCV67bXXzLcgMEULNBpS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gmFKxEtRHc4q7IrqcU1hmBIAACCx0TMGAADgEWEMAADAI8IYAACAR4QxAAAAjwhjAAAAHhHGAAAAPCKMAQAAeEQYAwAA8IgwBgAA4BFhDAAAwCPCGAAAgEeEMQAAAI8IYwAAAB4RxgAAADwijAEAAHhEGAMAAPCIMAYAAOARYQwAAMAjwhgAAIBHhDEAAACPkkKhUMjnBgAAABRlVMYAAAA8IowBAAB4RBgDAADwiDAGAADgEWEMAADAI8IYAACARyV8vjgAxGrZsmX2wAMP2JIlS2z37t126KGHWu/eva1z587szAQ3c+ZM++yzz+zLL7+0r7/+2h2/O++807p16xb18du2bXPH+u2337bffvvN9t13X+vQoYNdffXVVr58+XzffmS0YcMGmzVrln344Yf23Xff2e+//26VKlWyI444wi655BJr2rRpht/huGbEOmMACoyFCxfaxRdfbCVLlrRTTz3VKlSo4E7U69ats/79+1vfvn19byKy0LZtW1u/fr2lpKRY2bJl3b8zC2N//fWXnXfeebZy5Upr1aqVNWzY0FatWmUfffSRNWjQwKZMmeKeA36NGTPGHn/8catZs6a1aNHCqlSpYt9//7298847pmVMx44da6ecckra4zmumdCirwCQ6Hbv3h1q37596LDDDgutWLEi7fatW7eGTj311FDDhg1Da9as8bqNyNrHH38cWrdunfv3hAkTQnXr1g1NmzYt6mPHjx/v7r/nnnui3q7v8G/27NmhxYsXZ7hdtzVq1CjUsmXL0M6dO9Nu57hGR88YgAJhwYIF9sMPP9hpp53mqiQBDVddeeWVtmfPHps+fbrXbUTWjj32WKtevfped5MqKi+//LKrfF111VXp7rv88svdMNjUqVPd4+CXho2bN2+e4XbddtRRR9mmTZts9erV7jaOa+YIYwAKhEWLFrnvrVu3znCfhrHCH4OCbe3atfbrr7+6vqPIocjSpUu7E716lTQchsRVokSJdN85rpkjjAEoEPQ/cqlVq1aG+1QpUR8SJ+fCITiOtWvXjnp/8BngeCeun376yebPn+8mXdStW9fdxnHNHGEMQIGgGViipv1oNFy5devWfN4q5IXgOGY2YzK4neOdmDRL9sYbb7Rdu3bZwIEDrXjx4u52jmvmCGMAACAuUlNT7eabb7bFixfb2WefbV26dGHPZgNhDECBsLdqiCpnmVXNULAExzGohsZaJYUfatC/5ZZb7NVXX7XTTz/dhg0blu5+jmvmCGMACoSgfyhan9DmzZtt48aNUfvJUPAExzHoE4wUfAY43olXEZs2bZqb8XzXXXdZsWLpIwbHNXOEMQAFghaUlHnz5mW47+OPP3bfW7Zsme/bhbwJ3vvtt599/vnnbpHQcDt37rRPP/3U3U8YS5wgNmTIELe0jBZ4veeee9L6xMJxXDNHGANQIBxzzDFWo0YNe/31192q7OFDVg8//LCbPt+1a1ev24j4SEpKsu7du7sg9tBDD6W7b8KECa4Sqvv1OCROEDv55JNt9OjRUYOYcFwzx+WQABSohV91vTtdDklDIeojCy6HdN1119kVV1zhexORBS3kqmtTiq5NuWLFCreWWFDhat++vfuKdtmcRo0aucsh6RqIXA4pcejaoQ8++KBbD65Xr15pa4qF0zHVMROOa3SEMQAF7kLh999/f4YLhathGIlt0KBB9sorr2R6f79+/dxFwAOarKET/ezZs90FqKtWrWodO3Z0j6N5v2AcU4m8/ijHNSPCGAAAgEf0jAEAAHhEGAMAAPCIMAYAAOARYQwAAMAjwhgAAIBHhDEAAACPCGMAAAAeEcYAAAA8IowBAAB4RBgDAADwiDAGAADgEWEMAADA/Pk/E4rea076bQYAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "az.plot_forest(\n", + " idata,\n", + " var_names=[\"category_effect\", \"family_effect\"],\n", + " combined=True,\n", + ")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "e3c83a47", + "metadata": {}, + "source": [ + "## 7. Takeaways\n", + "\n", + "Once you walk through this pattern once, it becomes much easier to set up similar models:\n", + "\n", + "- factorize the labels into integer indices \n", + "- build one mapping array from the lower level to the upper level \n", + "- index the upper level parameters using that mapping \n", + "- use those indexed values to center the lower level parameters\n", + "\n", + "This is all the original GitHub post was struggling with on how to slice vector RVs cleanly.\n", + "\n", + "Once the mapping is in place, the rest of the model looks like any other hierarchical setup.\n" + ] + }, + { + "cell_type": "markdown", + "id": "1be8ea9b", + "metadata": {}, + "source": [ + "## Optional: Extending this pattern to more than two levels\n", + "\n", + "This example uses two levels (category and family), but the same idea works for any number of levels. \n", + "The key ingredients stay the same:\n", + "\n", + "1. factorize each level of labels into integer codes \n", + "2. build a mapping array from each level to the one above it \n", + "3. use that mapping to index the parent vector inside the next level’s prior\n", + "\n", + "For example, if you had three levels:\n", + "\n", + "- level_0\n", + "- level_1\n", + "- level_2\n", + "\n", + "You would create the following:\n", + "\n", + "- level_1_to_level_0\n", + "- level_2_to_level_1\n", + "\n", + "Then you would define priors like this:\n", + "\n", + "- level_0_effect\n", + "- level_1_effect, centered on level_0_effect[level_1_to_level_0]\n", + "- level_2_effect, centered on level_1_effect[level_2_to_level_1]\n", + "\n", + "Each additional level only requires one more factorized index and one more mapping array. \n", + "Nothing else about the PyMC model needs to change.\n", + "\n", + "This is what people sometimes call a telescoping hierarchy: every group is centered on the group above it, and the indexing arrays connect the levels together.\n" + ] + }, + { + "cell_type": "markdown", + "id": "39c4c0af", + "metadata": {}, + "source": [ + "## Watermark" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "33e5147c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Last updated: Tue Nov 25 2025\n", + "\n", + "Python implementation: CPython\n", + "Python version : 3.11.14\n", + "IPython version : 9.7.0\n", + "\n", + "matplotlib: 3.10.7\n", + "pymc : 5.26.1+28.g4ad7fa8f8\n", + "arviz : 0.22.0\n", + "pandas : 2.3.3\n", + "numpy : 2.3.5\n", + "\n", + "Watermark: 2.5.0\n", + "\n" + ] + } + ], + "source": [ + "%load_ext watermark\n", + "%watermark -n -u -v -iv -w" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "pymc-dev", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.14" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}