Skill Tracer
Skills › Scientific Agent Skills › pymc › workflows.md

workflows.md

154 lines · skills/pymc/references/workflows.md at 5f2bc03

Open on GitHub
1# PyMC Workflows and Scientific Checks
2
3Targets PyMC 6.3.2 / ArviZ 1.3.0. The complete regression workflow is in
4`standard_workflow.md`; runnable linear and hierarchical templates are under
5`assets/` at the skill root. The fragments here are illustrative building blocks
6requiring the displayed inputs. Missing-predictor assembly and log predictive
7scoring have small numerical tests; these are not missingness-model validation.
8

PyMC Workflows and Scientific Checks

Targets PyMC 6.3.2 / ArviZ 1.3.0. The complete regression workflow is in standard_workflow.md; runnable linear and hierarchical templates are under assets/ at the skill root. The fragments here are illustrative building blocks requiring the displayed inputs. Missing-predictor assembly and log predictive scoring have small numerical tests; these are not missingness-model validation.

Raw lines 1 to 8
9## Train-only scaling and units
11Estimate means/scales inside each training split, retain the values, and apply
12them unchanged to held-out or prediction data. Reject zero-scale columns and
13check design rank; standardization does not remove collinearity. Do not blindly
14standardize binary/categorical codes, exposure offsets, or scientifically fixed
15reference scales. Centering/scaling outcomes changes the meaning of prior scales
16and predicted quantities.
18For `x_scaled = (x - x_mean) / x_scale`, transform posterior coefficients as
19`beta_original = beta_scaled / x_scale` and
20`alpha_original = alpha_scaled - sum(beta_scaled * x_mean / x_scale)` when the
21outcome is unchanged. If the outcome is standardized too, multiply coefficients
22by its training scale and add its training mean to the intercept.

Train-only scaling and units

Estimate means/scales inside each training split, retain the values, and apply them unchanged to held-out or prediction data. Reject zero-scale columns and check design rank; standardization does not remove collinearity. Do not blindly standardize binary/categorical codes, exposure offsets, or scientifically fixed reference scales. Centering/scaling outcomes changes the meaning of prior scales and predicted quantities.

For x_scaled = (x - x_mean) / x_scale, transform posterior coefficients as beta_original = beta_scaled / x_scale and alpha_original = alpha_scaled - sum(beta_scaled * x_mean / x_scale) when the outcome is unchanged. If the outcome is standardized too, multiply coefficients by its training scale and add its training mean to the intercept.

Raw lines 9 to 23
24## Missing predictors
26`pm.Data` rejects NaN/masked inputs; it does not automatically create a predictor
27imputation model. Model the missing values explicitly and assemble them with
28PyTensor indexed assignment:
30```python
31import numpy as np
32import pymc as pm
33import pytensor.tensor as pt
35x = np.array([[1., np.nan], [2., 3.], [np.nan, 4.]])
36mask = np.isnan(x)
37with pm.Model() as model:
38 missing = pm.Normal("x_missing", 0, sigma=1, shape=int(mask.sum()))
39 x_complete = pt.set_subtensor(pt.as_tensor_variable(np.nan_to_num(x))[mask], missing)
40 # Use x_complete in the scientific likelihood.
41```
43A `switch(mask, missing_vector, full_array)` does not broadcast or place the
44missing values correctly. The independent Normal imputation above is only an
45illustration: predictors can need joint distributions and missing-not-at-random
46mechanisms. NaNs supplied directly to supported likelihood `observed` arrays
47can trigger automatic imputation; inspect resulting variables and likelihood
48factorization before LOO. Missing outcomes are not extra measured observations.

Missing predictors

pm.Data rejects NaN/masked inputs; it does not automatically create a predictor imputation model. Model the missing values explicitly and assemble them with PyTensor indexed assignment:

import numpy as np
import pymc as pm
import pytensor.tensor as pt

x = np.array([[1., np.nan], [2., 3.], [np.nan, 4.]])
mask = np.isnan(x)
with pm.Model() as model:
    missing = pm.Normal("x_missing", 0, sigma=1, shape=int(mask.sum()))
    x_complete = pt.set_subtensor(pt.as_tensor_variable(np.nan_to_num(x))[mask], missing)
    # Use x_complete in the scientific likelihood.

A switch(mask, missing_vector, full_array) does not broadcast or place the missing values correctly. The independent Normal imputation above is only an illustration: predictors can need joint distributions and missing-not-at-random mechanisms. NaNs supplied directly to supported likelihood observed arrays can trigger automatic imputation; inspect resulting variables and likelihood factorization before LOO. Missing outcomes are not extra measured observations.

Raw lines 24 to 49
50## Hierarchical and mixture identification
52Check that group IDs map to integer positions consistently at training and
53prediction time. Predictions for existing groups condition on those group
54posteriors. Predictions for new groups integrate over the population distribution
55and its uncertainty. Reuse each sampled new group effect across all rows of that
56group, and separately add observation noise. Independent new effects for every
57row accidentally model separate groups.
59In mixture models, exchanging component labels can leave the likelihood
60unchanged. Raw component means and R-hat can be misleading; use scientifically
61justified identification constraints or label-invariant predictive quantities.
62A good predictive fit can coexist with unresolved component labels or parameter
63combinations. Simulated recovery checks should cover the actual data design and
64noise scale, not only a favorable toy case.

Hierarchical and mixture identification

Check that group IDs map to integer positions consistently at training and prediction time. Predictions for existing groups condition on those group posteriors. Predictions for new groups integrate over the population distribution and its uncertainty. Reuse each sampled new group effect across all rows of that group, and separately add observation noise. Independent new effects for every row accidentally model separate groups.

In mixture models, exchanging component labels can leave the likelihood unchanged. Raw component means and R-hat can be misleading; use scientifically justified identification constraints or label-invariant predictive quantities. A good predictive fit can coexist with unresolved component labels or parameter combinations. Simulated recovery checks should cover the actual data design and noise scale, not only a favorable toy case.

Raw lines 50 to 65
66## Model comparison and held-out scoring
68`pm.compute_log_likelihood(idata, model=model)` adds pointwise contributions for
69observed RVs while the model contains the current data. It does not automatically
70turn a `Potential` into pointwise likelihood. Inspect dimensions and confirm one
71contribution per intended holdout unit. Time series, multivariate observations,
72imputation, and hierarchical groups need particular care.
74ArviZ 1 uses:
76```python
77loo = az.loo(idata, var_name="y_obs", pointwise=True)
78problem_units = (~np.isfinite(loo.pareto_k)) | (loo.pareto_k > loo.good_k)
79comparison = az.compare({"model_a": idata_a, "model_b": idata_b},
80 var_name="y_obs", round_to="none")
81```
83ELPD is a predictive score, not a Bayes factor. Stacking weights optimize a
84predictive combination, not posterior model probabilities. Compare models on the
85same observed values, ordering, density measure and predictive unit. If responses
86were transformed differently, include the log-Jacobian to return to a common
87scale. High Pareto-k requires investigation, exact refits/moment matching where
88supported, or suitable K-fold validation. WAIC is not a fallback cure.
90For K-fold, split by the independent scientific unit and fit preprocessing only
91on training data. `KFold(..., random_state=42)` uses `random_state`, not
92`random_seed`; group or temporal splitters are often needed instead of random
93rows. After a model is fitted and diagnosed, update **both** predictor and
94outcome data containers to the held-out fold and resize their shared coordinates.
95Compute held-out likelihood without overwriting the training likelihood group:
97```python
98with fitted_model:
99 pm.set_data({"x": x_test, "y_data": y_test},
100 coords={"obs_id": np.arange(len(y_test))})
101 heldout = pm.compute_log_likelihood(idata, extend_inferencedata=False,
102 progressbar=False)
103```
105The likelihood must have been defined with `observed=pm.Data("y_data", ...)`.
106This fragment is schematic and needs the corresponding training model. Restore
107training values/coordinates afterward. For independent held-out rows:
109```python
110from scipy.special import logsumexp
111log_lik = heldout["y_obs"].stack(sample=("chain", "draw"))
112log_predictive = logsumexp(log_lik.values, axis=-1) - np.log(log_lik.sizes["sample"])
113elpd = log_predictive.sum()
114```
116Integrate over posterior uncertainty using **log mean exp**, not the mean or sum
117of log likelihoods over draws. For a joint held-out group, first sum within-group
118log likelihood for each posterior draw, then integrate. New-group scoring must
119also integrate new group effects. A row-level score and a group-level score
120answer different questions. Retain per-unit paired differences and uncertainty.

Model comparison and held-out scoring

pm.compute_log_likelihood(idata, model=model) adds pointwise contributions for observed RVs while the model contains the current data. It does not automatically turn a Potential into pointwise likelihood. Inspect dimensions and confirm one contribution per intended holdout unit. Time series, multivariate observations, imputation, and hierarchical groups need particular care.

ArviZ 1 uses:

loo = az.loo(idata, var_name="y_obs", pointwise=True)
problem_units = (~np.isfinite(loo.pareto_k)) | (loo.pareto_k > loo.good_k)
comparison = az.compare({"model_a": idata_a, "model_b": idata_b},
                        var_name="y_obs", round_to="none")

ELPD is a predictive score, not a Bayes factor. Stacking weights optimize a predictive combination, not posterior model probabilities. Compare models on the same observed values, ordering, density measure and predictive unit. If responses were transformed differently, include the log-Jacobian to return to a common scale. High Pareto-k requires investigation, exact refits/moment matching where supported, or suitable K-fold validation. WAIC is not a fallback cure.

For K-fold, split by the independent scientific unit and fit preprocessing only on training data. KFold(..., random_state=42) uses random_state, not random_seed; group or temporal splitters are often needed instead of random rows. After a model is fitted and diagnosed, update both predictor and outcome data containers to the held-out fold and resize their shared coordinates. Compute held-out likelihood without overwriting the training likelihood group:

with fitted_model:
    pm.set_data({"x": x_test, "y_data": y_test},
                coords={"obs_id": np.arange(len(y_test))})
    heldout = pm.compute_log_likelihood(idata, extend_inferencedata=False,
                                        progressbar=False)

The likelihood must have been defined with observed=pm.Data("y_data", ...). This fragment is schematic and needs the corresponding training model. Restore training values/coordinates afterward. For independent held-out rows:

from scipy.special import logsumexp
log_lik = heldout["y_obs"].stack(sample=("chain", "draw"))
log_predictive = logsumexp(log_lik.values, axis=-1) - np.log(log_lik.sizes["sample"])
elpd = log_predictive.sum()

Integrate over posterior uncertainty using log mean exp, not the mean or sum of log likelihoods over draws. For a joint held-out group, first sum within-group log likelihood for each posterior draw, then integrate. New-group scoring must also integrate new group effects. A row-level score and a group-level score answer different questions. Retain per-unit paired differences and uncertainty.

Raw lines 66 to 121
122## Predictive combinations
124For a predictive mean, align model weights by name and sum weighted mean
125predictions. For intervals or full predictive draws, sample a model using its
126weight, then a full predictive draw from that model. Averaging unrelated draw
127pairs shrinks variance and can invent unsupported outcomes between modes. The
128bundled `model_averaging` implements the mixture and validates matching
129coordinates. Evaluate the ensemble on held-out data when possible.

Predictive combinations

For a predictive mean, align model weights by name and sum weighted mean predictions. For intervals or full predictive draws, sample a model using its weight, then a full predictive draw from that model. Averaging unrelated draw pairs shrinks variance and can invent unsupported outcomes between modes. The bundled model_averaging implements the mixture and validates matching coordinates. Evaluate the ensemble on held-out data when possible.

Raw lines 122 to 130
131## Saving and reporting
133```python
134idata.to_netcdf("posterior.nc")
135predictions.to_netcdf("predictions.nc")
136loaded = az.from_netcdf("posterior.nc")
137```
139DataTree files contain draws/data, not an executable model definition. Keep model
140source, dependency lockfile, priors/units, training preprocessing, data versions,
141seeds, sampler/backend configuration, diagnostics and failed checks beside them.
142Rebuild a model from source for predictions. Pickled model graphs are not a stable
143cross-version interchange format.
145Report uncertainty with the interval kind and probability (`ci_kind="hdi"`,
146`ci_prob=.95` for summaries; `prob=.95` for `az.hdi`). Do not label an ArviZ
147default equal-tail interval as an HDI. Posterior checks, model comparison,
148convergence diagnostics and external scientific validation answer distinct
149questions and should be reported separately.
151Sources: [log likelihood](https://www.pymc.io/projects/docs/en/stable/api/generated/pymc.stats.compute_log_likelihood.html),
152[LOO](https://python.arviz.org/projects/stats/en/stable/api/generated/arviz_stats.loo.html),
153[comparison](https://python.arviz.org/projects/stats/en/stable/api/generated/arviz_stats.compare.html),
154[summary](https://python.arviz.org/projects/stats/en/stable/api/generated/arviz_stats.summary.html).

Saving and reporting

idata.to_netcdf("posterior.nc")
predictions.to_netcdf("predictions.nc")
loaded = az.from_netcdf("posterior.nc")

DataTree files contain draws/data, not an executable model definition. Keep model source, dependency lockfile, priors/units, training preprocessing, data versions, seeds, sampler/backend configuration, diagnostics and failed checks beside them. Rebuild a model from source for predictions. Pickled model graphs are not a stable cross-version interchange format.

Report uncertainty with the interval kind and probability (ci_kind="hdi", ci_prob=.95 for summaries; prob=.95 for az.hdi). Do not label an ArviZ default equal-tail interval as an HDI. Posterior checks, model comparison, convergence diagnostics and external scientific validation answer distinct questions and should be reported separately.

Sources: log likelihood, LOO, comparison, summary.

Raw lines 131 to 154