pmcprg.pmc¶
Pairwise Markov Chain models (HMC-IN, HMC-IN2, HMC-DN, PMC-IN, PMC) loaded from TOML, with simulation, MPM classification, exact handling of missing observations, and unsupervised estimation (ICE, SEM, GICE). See the README for the TOML model format, the CLI and the GUI.
Model¶
PMCModel ¶
PMCModel(path: str | Path)
Container for a PMC/HMC model loaded from a TOML configuration file.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str or Path
|
Path to the |
required |
Attributes (read-only)
variant : Variant
K : int Number of hidden states.
N_default : int Default sequence length for simulation.
name : str Human-readable label from [model].
margin_structure : str "state" (K margins f_i) or "pair"
(K² margins f_ij) — see the module docstring.
missingness : None (ignorable, the default) or the non-ignorable
mechanism of the [missingness] table
(:mod:pmcprg.pmc.missingness).
transition_A
property
¶
transition_A: ndarray
K×K row-stochastic matrix A[i,j] = P(X_{n+1}=j | X_n=i).
missingness
property
¶
missingness
Missingness mechanism: None (ignorable) or a mechanism object.
None — the default, no [missingness] table or mechanism =
"ignorable": missing rows are integrated out of the observed-data
likelihood p(y_obs). Otherwise a
:class:~pmcprg.pmc.missingness.StateMissingness ("state") or
:class:~pmcprg.pmc.missingness.StateMarkovMissingness
("state-markov"), frozen: the mask m is then evidence on the
states and every inference function works with p(y_obs, m)
(:mod:pmcprg.pmc.missingness, :mod:pmcprg.pmc.gaps). Use
:meth:with_missingness for a model with another mechanism.
margin_structure
property
¶
margin_structure: str
"state" (K margins f_i) or "pair" (K² margins f_ij).
State margins are exactly the SR-PMCs whose hidden chain X is Markov (Proposition of DerrodePieczynski_CSDA2013 §2.1); pair margins give the general PMC of DerrodePieczynski_CSDA2013 Eqs. 12–14 (PMC and PMC-IN variants only). See the module docstring.
from_dict
classmethod
¶
from_dict(raw: dict, path: str | Path = 'memory') -> PMCModel
Build a PMCModel directly from a dict (useful for unit tests).
The dict is deep-copied so subsequent mutations of either raw or
model._raw do not affect the other.
with_missingness ¶
with_missingness(missingness) -> PMCModel
A copy of this model with another missingness mechanism.
missingness is None (ignorable), a mechanism object or a
[missingness] table (dict), validated against K. The copy is built
like the estimators' iterates — from_dict on a deep copy of
raw with the [missingness] table replaced (or removed) — so
every other parameter is unchanged and self is not modified.
sem_config ¶
sem_config() -> dict
Return the [sem] TOML section as a dict (empty if absent).
SEM (:func:pmcprg.pmc.sem.sem) merges this on top of [ice] so a
model file can override shared estimator keys (or set SEM-specific ones
like sem_seed / max_iter) in a dedicated [sem] block while
still inheriting the common [ice] settings.
margin_blocks ¶
margin_blocks() -> list[dict]
Return a deep copy of the [[margins]] blocks, in a stable order.
margin_structure == "state"— K blocks keyed byi({"i", "dist", "params"[, "candidates"]}), in K-format even when the source listed K² blocks collapsed by[model].margin_structure = "state";margin_structure == "pair"— K² blocks keyed byiandj({"i", "j", "dist", "params"[, "candidates"]}), sorted by (i, j).
margin ¶
margin(i: int, j: int | None = None) -> _MarginDist
Return the margin f_ij of the observation y_n given (x_n, x_{n+1}) = (i, j).
- State margins (
margin_structure == "state"): the density is f_i whatever the other state, sojis ignored and may be omitted —margin(i, j) is margin(i). - Pair margins (
margin_structure == "pair"):jis required.margin(i, j)is f_ij, the left margin of the pair (i, j) in DerrodePieczynski_CSDA2013 Eq. 12; the right margin of that pair (the density of y_{n+1}) ismargin(j, i)= f_ji.margin(i)raisesValueError: no density is attached to a state alone.
pdf ¶
pdf(i: int, j: int | None, y: float) -> float
f_ij(y) — PDF of margin(i, j) at y (f_i(y) for state margins).
cdf ¶
cdf(i: int, j: int | None, y: float) -> float
F_ij(y) — CDF of margin(i, j) at y (F_i(y) for state margins).
ppf ¶
ppf(i: int, j: int | None, q: float) -> float
F_ij^{-1}(q) — quantile of margin(i, j) (F_i^{-1} for state margins).
weight ¶
weight(i: int, j: int, y_n: float, y_n1: float) -> float
Un-normalised weight of the pair (x_n, x_{n+1}) = (i, j) given (y_n, y_{n+1}).
The margin of the left observation y_n is margin(i, j) = f_ij and
that of the right observation y_{n+1} is margin(j, i) = f_ji —
the index inversion of DerrodePieczynski_CSDA2013 Eq. 12. With state margins
(margin_structure == "state") both reduce to the per-state
densities f_i and f_j.
Formulas by variant
HMC-IN : A[i,j] · f_j(y_{n+1}) HMC-IN2 : A[i,j] · f_j(y_{n+1}) HMC-DN : A[i,j] · f_j(y_{n+1}) · c_ij(F_i(y_n), F_j(y_{n+1})) PMC-IN : p[i,j] · f_ij(y_n) · f_ji(y_{n+1}) PMC : p[i,j] · f_ij(y_n) · f_ji(y_{n+1}) · c_ij(F_ij(y_n), F_ji(y_{n+1}))
(HMC-* variants have state margins, so f_ji = f_j there.)
Relation to forward–backward
For HMC- the weight is the transition p(z_{n+1} | z_n) itself:
precompute_weights(model, Y)[0][n, i, j] = weight(i, j, Y[n], Y[n+1]).
For PMC- it is the summand of the pair density of
DerrodePieczynski_CSDA2013 Eq. 12,
p(x_n = i, x_{n+1} = j, y_n, y_{n+1}); the transition used by
forward–backward divides it by p(x_n = i, y_n) = Σ_k p[i,k] f_ik(y_n)
(DerrodePieczynski_CSDA2013 Eqs. 13–14):
W[n, i, j] = weight(i, j, y_n, y_{n+1}) / Σ_k p[i,k] · f_ik(y_n).
(precompute_weights clips the CDF values to [EPS, 1 − EPS]
before the copula density; weight does not.)
Why the margins may depend on the pair
Reversibility (DerrodePieczynski_CSDA2013 §2.2) only gives p(y_1 | x_1, x_2) = p(y_2 | x_2, x_1), whence f_ji for the right observation. It does not make f_ij independent of j: by the Proposition of DerrodePieczynski_CSDA2013 §2.1, f_ij = f_i holds exactly when the hidden chain X is Markov, in which case the PMC is an SR HMC-DN (the transition p(x_{n+1} = j | x_n = i, y_n) ∝ p_ij f_i(y_n) no longer depends on y_n).
save ¶
save(path: str | Path | None = None) -> Path
Write the model back to a TOML file. Returns the path written.
Variant ¶
Bases: Enum
The five PMC/HMC model variants.
Properties
uses_copula : True if Y_n depends on Y_{n-1} via a copula (HMC-DN, PMC). has_markov_prior : True if the prior is a transition matrix A (HMC-). False if the prior is a joint p[i,j] (PMC-). allows_pair_margins : True if the variant admits pair-indexed margins f_ij (PMC, PMC-IN). per_class_margin : True if the variant forces state-indexed margins f_i (HMC-*); the negation of allows_pair_margins.
allows_pair_margins
property
¶
allows_pair_margins: bool
True for PMC and PMC-IN, whose margins may be pair-indexed (f_ij).
For an HMC- variant X is a Markov chain, and by the
Proposition of DerrodePieczynski_CSDA2013 §2.1 an SR-PMC has a
Markov X iff p(y_2 | x_1, x_2) = p(y_2 | x_2): its margins are
state-indexed. Whether a given model uses* pair margins is
:attr:PMCModel.margin_structure.
per_class_margin
property
¶
per_class_margin: bool
True if the variant forces state-indexed margins f_i (HMC-*).
For PMC and PMC-IN the margins are state-indexed or pair-indexed
depending on the model (:attr:PMCModel.margin_structure), so this
is False. (Versions 0.5.0–0.8.x returned True for every variant,
wrongly attributing f_ij = f_i to reversibility; the Proposition of
DerrodePieczynski_CSDA2013 §2.1 ties it to X being Markov.)
Simulation¶
simulate ¶
simulate.py — Sequence generator for all 5 PMC/HMC variants.
Public API
simulate(model, N=None, seed=None) -> (X, Y) Generate a synthetic sequence (X_{1:N}, Y_{1:N}) from a PMCModel.
Algorithm
State margins (model.margin_structure == "state", f_ij = f_i) — the
hidden chain X is Markov (Proposition of DerrodePieczynski_CSDA2013 §2.1),
so for every variant:
1. Sample the latent chain X_0, X_1, ..., X_N (X_0 is a virtual "warm-up" state,
not returned). The chain is driven by the row-stochastic matrix A[i,j].
2. Sample observations Y_1, ..., Y_N according to the variant-specific conditional:
HMC-IN : Y_n | X_n = j ~ f_j
HMC-IN2 : Y_n | X_{n-1}=i, X_n=j ~ f_j
HMC-DN : Y_1 | X_0=i, X_1=j ~ f_j (marginal, no copula)
Y_n | X_{n-1}=i, X_n=j, Y_{n-1} ~ f_j(·) · c_{ij}(F_i(Y_{n-1}), F_j(·))
PMC-IN : identical to HMC-IN2 (different prior, same conditional)
PMC : identical to HMC-DN (different prior, same conditional)
Pair margins (model.margin_structure == "pair", general PMC, PMC and
PMC-IN only) — X is not Markov, and the pairs z_n = (x_n, y_n) are drawn
alternately, as in DerrodePieczynski_CSDA2013 §3.1 (Eqs. 13–14):
x_1 ~ π, π_i = Σ_j p[i,j]; y_1 | x_1 = i ~ Σ_j A[i,j] f_ij
x_{n+1} | x_n = i, y_n ∝ p[i, x_{n+1}] · f_{i,x_{n+1}}(y_n) (Eq. 13)
y_{n+1} | x_n = i, x_{n+1} = j, y_n ~ f_ji(·) · c_ij(F_ij(y_n), F_ji(·)) (Eq. 14)
PMC-IN drops the copula factor (y_{n+1} ~ f_ji). The mixture for y_1 is drawn through an auxiliary state j ~ A[x_1, ·], and y_{n+1} by conditional inversion of c_ij at u = F_ij(y_n), then F_ji^{-1}.
In both cases the right margin of the pair (i, j) is model.margin(j, i)
= f_ji (the index inversion of DerrodePieczynski_CSDA2013 Eq. 12), which is
f_j for state margins.
Returns:
| Name | Type | Description |
|---|---|---|
X |
np.ndarray, shape (N,), dtype int
|
Hidden state sequence X_{1:N} with values in {0, ..., K-1}. |
Y |
np.ndarray, shape (N,), dtype float
|
Observed sequence Y_{1:N}. |
simulate ¶
simulate(model: PMCModel, N: int | None = None, seed: int | None = None) -> tuple[np.ndarray, np.ndarray]
Generate a synthetic sequence from a PMCModel.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
PMCModel
|
The model to simulate from. |
required |
N
|
int
|
Sequence length. Defaults to |
None
|
seed
|
int
|
Seed for the random number generator (reproducibility). |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
X |
np.ndarray, shape (N,), dtype int
|
Hidden state sequence X_{1:N} ∈ {0, …, K-1}. |
Y |
ndarray
|
Observed sequence Y_{1:N}. Shape |
Inference¶
Forward-backward, MPM classification, and Forward-Filter Backward-Sample.
classify ¶
classify(model: PMCModel, Y: ndarray, *, gap_nodes: int | None = None) -> tuple[np.ndarray, np.ndarray, float]
Supervised MPM classification of the observation sequence Y.
Runs the full forward-backward-smooth-MPM pipeline.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
PMCModel — fully specified model (parameters known).
|
|
required |
Y
|
ndarray
|
|
required |
gap_nodes
|
quadrature nodes for the missing rows of the grid variants
|
|
None
|
Returns:
| Name | Type | Description |
|---|---|---|
X_hat |
np.ndarray, shape (N,) int — MPM class labels (every n,
|
missing rows included). |
gamma |
np.ndarray, shape (N, K) — posterior marginals P(x_n | y_obs).
|
|
log_lik |
float — log p(y_obs | model), the
|
observed-data log-likelihood (log p(y_{1:N}) without gaps);
log p(y_obs, m | model) when |
classify_image ¶
classify_image(model: PMCModel, img: ndarray) -> tuple[np.ndarray, np.ndarray, float]
Supervised MPM classification of a 2D image (mono- or multi-channel).
The image is linearised along the Generalized Hilbert ("gilbert") path,
classified with :func:classify, then re-folded into 2D maps.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
PMCModel — fully specified model (parameters known).
|
|
required |
img
|
ndarray
|
Shape |
required |
Returns:
| Name | Type | Description |
|---|---|---|
X_hat_2d |
np.ndarray, shape (H, W), int — MPM class-label map.
|
|
gamma_2d |
np.ndarray, shape (H, W, K) — per-pixel posterior marginals.
|
|
log_lik |
float — log p(image | model).
|
|
Notes
The class-label map is always 2D (segmentation is per-pixel, scalar)
regardless of the input dimensionality. Posterior marginals are over
states, so their last axis is always K — independent of model.d.
forward ¶
forward(model: PMCModel, Y: ndarray, W: ndarray | None = None, f_pdf: ndarray | None = None, *, gap_nodes: int | None = None) -> tuple[np.ndarray, float]
Normalized forward pass (Devijver 1985 / Baum-Welch).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
PMCModel
|
|
required |
Y
|
ndarray
|
|
required |
W
|
ndarray | None
|
|
None
|
f_pdf
|
pre-computed margin-PDF tensor (N, K, K). Computed with W if None.
|
|
None
|
gap_nodes
|
quadrature nodes for the missing rows of the grid variants
|
|
None
|
Returns:
| Name | Type | Description |
|---|---|---|
alpha_hat |
np.ndarray, shape (N, K) — normalized forward variables
|
|
log_lik |
float — log-likelihood log p(y_{1:N})
|
|
Underflow |
when a step's normaliser C is not finite or below
|
|
``MIN_POSITIVE`` — every weight reachable from α̂_n underflowed — the
|
|
|
pass is recomputed in log space from the model (see the module
|
|
|
docstring); ``W`` must therefore be ``precompute_weights(model, Y)``.
|
|
|
A step impossible even in log space raises
|
|
|
class:`IncompatibleObservationError`.
|
|
|
Missing rows (module docstring): the exact-shortcut variants run this
|
|
|
recursion on the marginalised ``precompute_weights``; the grid variants
|
|
|
run the augmented chain of :mod:`pmcprg.pmc.gaps` (``W`` must then be None),
|
|
|
``alpha_hat[n, i]`` being P(x_n = i | observations up to n) at every n and
|
|
|
``log_lik`` the observed-data log-likelihood.
|
|
|
Non-ignorable missingness (``model.missingness`` not None): ``alpha_hat``
|
|
|
and ``log_lik`` are those of p(y_obs, m) — the mask up to n is part of
|
|
|
the conditioning; the factors live in ``W`` and ``f_pdf``
|
|
|
(:func:`precompute_weights`), which must then come from the same model.
|
|
backward ¶
backward(model: PMCModel, Y: ndarray, W: ndarray | None = None, *, gap_nodes: int | None = None) -> np.ndarray
Normalized backward pass.
Each β̂n is rescaled by its own sum (Rabiner 1989 §V.A-type scaling),
not by the forward constants C{n+1} as in Devijver (1985) — harmless
because :func:smooth and :func:joint_posteriors renormalise
(audit K-13).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
PMCModel
|
|
required |
Y
|
ndarray
|
|
required |
W
|
ndarray | None
|
|
None
|
gap_nodes
|
quadrature nodes for the missing rows of the grid variants
|
|
None
|
Returns:
| Name | Type | Description |
|---|---|---|
beta_hat |
(ndarray, shape(N, K))
|
|
Underflow |
a step whose sum D is not finite or below ``MIN_POSITIVE``
|
|
makes the pass restart in log space from the model, as in
|
|
|
func:`forward` (the former floor zeroed β̂ for every earlier step).
|
|
|
Missing rows: exact-shortcut variants as in :func:`forward`. For the grid
|
|
|
variants (``W`` must be None) β̂ at a missing n depends on the forward
|
|
|
messages — ``β̂_n(i) ∝ Σ_g α̃_n(i, g) β̃_n(i, g) / Σ_g α̃_n(i, g)`` — so the
|
|
|
forward pass is run too; ``smooth(forward(...)[0], backward(...))`` is
|
|
|
then exact at every n (:mod:`pmcprg.pmc.gaps`).
|
|
|
Non-ignorable missingness: β̂ carries the factors of the later masks, as
|
|
|
``W`` does (:func:`precompute_weights`).
|
|
smooth ¶
smooth(alpha_hat: ndarray, beta_hat: ndarray) -> np.ndarray
Compute the posterior marginals γ_n(j) = P(X_n=j | y_{1:N}).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
alpha_hat
|
(N, K)
|
|
required |
beta_hat
|
ndarray
|
|
required |
Returns:
| Name | Type | Description |
|---|---|---|
gamma |
(N, K) — each row sums to 1.
|
|
joint_posteriors ¶
joint_posteriors(alpha_hat: ndarray, W: ndarray, beta_hat: ndarray) -> np.ndarray
Pairwise posteriors ξ_n(i, j) = P(X_n=i, X_{n+1}=j | Y), n = 0 … N-2.
The pairwise companion of :func:smooth (which returns the marginal
γ_n). Fully vectorised::
ξ_n(i, j) ∝ α̂_n(i) · W[n, i, j] · β̂_{n+1}(j)
with a per-step normalisation. A step whose un-normalised mass is zero —
its transition weights underflowed to 0.0 — falls back to the product
γ_n(i) · γ_{n+1}(j) of the smoothed marginals (:func:smooth), the
coupling that keeps both marginal-consistency invariants
γ_n(i) = Σ_j ξ_n(i, j) and γ_{n+1}(j) = Σ_i ξ_n(i, j) (audit N-11). The
former uniform 1/K² kept them only while γ itself was uniform, which was
the case when an underflowed step zeroed α̂ and β̂; since
:func:forward and :func:backward recover from underflow in log space,
γ around such a step is informative. With uniform γ the two coincide.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
alpha_hat
|
(N, K) — normalised forward variables (:func:`forward`).
|
|
required |
W
|
ndarray
|
|
required |
beta_hat
|
ndarray
|
|
required |
Returns:
| Name | Type | Description |
|---|---|---|
xi |
(N - 1, K, K)
|
|
mpm ¶
mpm(gamma: ndarray) -> np.ndarray
Marginal Posterior Mode (MPM) classification.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
gamma
|
(N, K) — posterior marginals (rows sum to 1).
|
|
required |
Returns:
| Name | Type | Description |
|---|---|---|
X_hat |
(N,) int — state sequence maximising the marginal posterior.
|
|
sample_posterior ¶
sample_posterior(model: PMCModel, Y: ndarray, rng: Generator, *, W: ndarray | None = None, f_pdf: ndarray | None = None, alpha_hat: ndarray | None = None, gap_nodes: int | None = None, return_y: bool = False) -> np.ndarray | tuple[np.ndarray, np.ndarray]
Draw one realisation of the state sequence X̃ ~ P(X | Y).
Implements Forward-Filter Backward-Sample (FFBS) — the standard
posterior sampler for state-space models (Carter & Kohn 1994;
Frühwirth-Schnatter 1994; and, for the discrete-state case used here,
Chib 1996). DerrodePieczynski_CSDA2013 Eq. 23 samples forward instead, along
p(x_{n+1} | x_n, y_{1:N}) — the same posterior law, drawn in the
other direction.
- Filter: compute (or accept pre-computed)
α̂_n(j) = P(X_n=j | Y_{1:n}). - Sample
X̃_{N-1} ~ α̂_{N-1}(the last filtered posterior is the smoothed posterior because there is no future observation). -
Backward-sample, for
n = N-2, …, 0::P(X_n=i | X̃{n+1}, Y{1:N}) ∝ α̂n(i) · W[n, i, X̃{n+1}]
Z = (X, Y) being Markov (DerrodePieczynski_CSDA2013 Eq. 4), X_n
and Y_{n+2:N} are independent given
Z_{n+1} = (X_{n+1}, Y_{n+1}), which makes this sampler exact for
pair margins (general PMC, X alone not Markov) as well as for state
margins.
Used by SEM (Stochastic EM) to obtain the hard pseudo-labels on which the M-step is then run.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
PMCModel
|
|
required |
Y
|
ndarray
|
|
required |
rng
|
Generator
|
|
required |
W
|
optional pre-computed tensors (skip re-computation).
|
|
None
|
f_pdf
|
optional pre-computed tensors (skip re-computation).
|
|
None
|
alpha_hat
|
optional pre-computed tensors (skip re-computation).
|
|
None
|
gap_nodes
|
quadrature nodes for the missing rows of the grid variants
|
|
None
|
return_y
|
bool
|
|
False
|
Returns:
| Name | Type | Description |
|---|---|---|
X_sample |
np.ndarray, shape (N,), dtype int — one draw from P(X | Y).
|
|
Y_sample |
(only with ``return_y``) copy of Y whose missing rows are drawn
|
from P(y_miss | X̃, y_obs) — jointly with X̃. |
Missing rows (non-finite rows of Y): the draw is from P(X | y_obs). The
|
|
|
exact-shortcut variants (HMC-IN, HMC-IN2, PMC-IN with state margins) run
|
|
|
the FFBS below on the marginalised weights, then draw each missing
|
|
|
y_n ~ f_{X̃_n}. The grid variants run FFBS on the augmented chain of
|
|
|
mod:`pmcprg.pmc.gaps` (``W``/``alpha_hat`` must be None); the missing y are
|
|
|
then drawn on its quadrature nodes.
|
|
|
Non-ignorable missingness (``model.missingness`` not None): the draw is
|
|
|
from P(X | y_obs, m) — the factors live in ``W`` (:func:`precompute_weights`)
|
|
|
and in the augmented chain; given X̃ the missing y are drawn as above
|
|
|
(y_miss does not depend on m given the states).
|
|
error_rate ¶
error_rate(X_true: ndarray, X_hat: ndarray) -> float
Classification error rate (fraction of misclassified labels), invariant under label permutation.
For unsupervised classifiers, predicted class indices are arbitrary. We
therefore find the optimal one-to-one relabeling π̂ of predicted labels
that minimises mean(X_true != π̂(X_hat)) and return that minimum.
Implementation: build the K×K confusion matrix C[k, ℓ] = #{n : X_true=k, X_hat=ℓ}
then solve the linear assignment problem (Kuhn-Munkres / Hungarian) on
-C to maximise total agreement.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X_true
|
np.ndarray, shape (N,) — ground-truth labels in {0, …, K-1}.
|
|
required |
X_hat
|
ndarray
|
|
required |
Returns:
| Type | Description |
|---|---|
float — minimum error rate over all label permutations, in [0, 1].
|
|
Missing observations: exact inference, imputation, forecasting¶
Missing values (NaN) are integrated out exactly rather than imputed
before inference.
gap_posterior ¶
gap_posterior(model: PMCModel, Y: ndarray, *, gap_nodes: int | None = None, xi: bool = True) -> GapPosterior
Exact (up to quadrature) posterior quantities of a NaN-bearing Y.
Works for every variant and for a Y without missing values too (then
identical to the functions of :mod:pmcprg.pmc.inference). The exact
shortcut variants use precompute_weights / forward / backward
/ smooth / joint_posteriors on the marginalised weights; the grid
variants run the augmented chain (module docstring). With a
non-ignorable model.missingness every quantity is given (y_obs, m),
the missingness factors included (module docstring, "Missingness
mechanisms").
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
PMCModel
|
|
required |
Y
|
ndarray
|
|
required |
gap_nodes
|
number G of quadrature nodes (default 64; grid variants only).
|
|
None
|
xi
|
bool
|
|
True
|
GapPosterior
dataclass
¶
GapPosterior(miss: ndarray, log_lik: float, alpha_hat: ndarray, beta_hat: ndarray, gamma: ndarray, xi: ndarray | None, method: str, grid: QuadratureGrid | None = None, node_post: ndarray | None = None, nodes: ndarray | None = None, quad_error: float | None = None)
Posterior quantities of a sequence with missing observations.
Attributes:
| Name | Type | Description |
|---|---|---|
miss |
(N,) bool — missing rows.
|
|
log_lik |
float — log p(y_obs), the observed-data log-likelihood
|
(log p(y_obs, m) with a non-ignorable |
alpha_hat |
(N, K) — P(x_n = i | observations up to n).
|
|
beta_hat |
(N, K) — normalised backward messages (module docstring for
|
their definition at a missing n). |
gamma |
(N, K) — P(x_n = i | y_obs).
|
|
xi |
(N-1, K, K) or None — P(x_n = i, x_{n+1} = j | y_obs).
|
|
method |
``"exact"`` (K-state shortcut) or ``"grid"``.
|
|
grid |
QuadratureGrid or None (exact shortcut) — the reference grid.
|
|
node_post |
(M, K, G) or None — P(x_n = i, y_n ∈ node g | y_obs) at the
|
M missing positions |
nodes |
(M, G) or None — the quadrature nodes y_g of each missing
|
position: the reference nodes, or those of its local grid (module docstring, "Local grids"). |
quad_error |
float or None — the quadrature diagnostic of
|
:func: |
impute ¶
impute(model: PMCModel, Y: ndarray, *, gap_nodes: int | None = DEFAULT_GAP_NODES, quantiles=DEFAULT_QUANTILES, n_samples: int = 0, rng=None) -> Imputation
Posterior law of every missing y_n given all the observed data.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
PMCModel
|
|
required |
Y
|
ndarray
|
|
required |
gap_nodes
|
quadrature nodes G for the grid variants (default 64).
|
|
DEFAULT_GAP_NODES
|
quantiles
|
levels in (0, 1) of the reported posterior quantiles.
|
|
DEFAULT_QUANTILES
|
n_samples
|
number S of joint FFBS draws of (x_{1:N}, y_miss) (default 0).
|
|
0
|
rng
|
|
None
|
Returns:
| Type | Description |
|---|---|
Imputation — see its docstring. A Y without missing rows gives empty
|
|
per-position arrays.
|
|
Imputation
dataclass
¶
Imputation(index: ndarray, mean: ndarray, sd: ndarray, quantiles: tuple, quantile_values: ndarray, gamma: ndarray, log_lik: float, Y_mean: ndarray, method: str, nodes: ndarray | None = None, density: ndarray | None = None, x_samples: ndarray | None = None, y_samples: ndarray | None = None, grid_nodes: ndarray | None = None, grid_mass: ndarray | None = None)
Posterior law of the missing observations given all the observed ones.
Attributes:
| Name | Type | Description |
|---|---|---|
index |
(M,) positions of the missing rows.
|
|
mean, sd |
(M,) — or (M, d) — posterior mean and standard deviation
|
of y_n given y_obs. |
quantiles |
the requested levels.
|
|
quantile_values |
(M, Q) — or (M, Q, d), per component — posterior quantiles.
|
|
gamma |
(M, K) — P(x_n = i | y_obs) at the missing positions.
|
|
log_lik |
float — log p(y_obs).
|
|
Y_mean |
Y with every missing row replaced by its posterior mean.
|
|
method |
``"exact"`` or ``"grid"``.
|
|
nodes |
(G,) or None — the reference nodes y_g (d = 1).
|
|
density |
(M, G) or None — posterior density of y_n at ``nodes``
|
(the exact mixture density for the shortcut variants;
mass / ω on the reference grid, the Nyström density on
a local grid, see :func: |
grid_nodes |
(M, G) or None — the quadrature nodes of each missing
|
position (grid variants: reference or local grid). |
grid_mass |
(M, G) or None — the posterior masses on ``grid_nodes``,
|
the discrete law whose moments are |
x_samples |
(S, N) int or None — FFBS draws of the whole state path.
|
|
y_samples |
(S, M) — or (S, M, d) — or None: the matching draws of the
|
missing observations (exact continuous draws for the
shortcut variants; drawn on the quadrature nodes — the
discrete law whose moments are |
forecast ¶
forecast(model: PMCModel, Y: ndarray, h: int, *, gap_nodes: int | None = DEFAULT_GAP_NODES, quantiles=DEFAULT_QUANTILES, check_nodes: bool | int = False) -> Forecast
Predictive laws of the next h observations, a trailing gap of length h.
Runs the forward filter on Y followed by h missing rows: the normalised filter at N + k is P(x_{N+k}, y_{N+k} | y_obs) (no backward message is needed for a trailing gap). Y may contain missing rows.
With a non-ignorable model.missingness the laws are given
(y_obs, m_{1:N}): the N rows of Y carry their missingness factors, the h
appended rows none — their mask is unknown, and summing p(m_n | m_{n-1},
x_n) over it gives 1.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
PMCModel
|
|
required |
Y
|
ndarray
|
|
required |
h
|
int
|
|
required |
gap_nodes
|
int | None
|
|
DEFAULT_GAP_NODES
|
quantiles
|
|
DEFAULT_QUANTILES
|
|
check_nodes
|
False (default), True, or an integer G_ref > G. Checks that
|
|
False
|
Returns:
| Type | Description |
|---|---|
Forecast — see its docstring.
|
|
Forecast
dataclass
¶
Forecast(h: int, state_probs: ndarray, mean: ndarray, sd: ndarray, quantiles: tuple, quantile_values: ndarray, log_lik: float, method: str, nodes: ndarray | None = None, density: ndarray | None = None, grid_nodes: ndarray | None = None, grid_mass: ndarray | None = None, node_check: NodeCheck | None = None)
h-step predictive law of (x_{N+k}, y_{N+k}) given the observed part of Y.
Attributes:
| Name | Type | Description |
|---|---|---|
h |
horizon.
|
|
state_probs |
(h, K) — P(x_{N+k} = j | y_obs), k = 1..h.
|
|
mean, sd |
(h,) — or (h, d) — predictive mean and standard deviation.
|
|
quantiles |
the requested levels.
|
|
quantile_values |
(h, Q) — or (h, Q, d) — predictive quantiles.
|
|
log_lik |
float — log p(y_obs) of the conditioning sequence.
|
|
method |
``"exact"`` or ``"grid"``.
|
|
nodes |
(G,) or None — the reference nodes y_g (d = 1).
|
|
density |
(h, G) or None — predictive density at ``nodes``.
|
|
grid_nodes |
(h, G) or None — the quadrature nodes of each horizon
|
(grid variants: reference or local grid). |
grid_mass |
(h, G) or None — the predictive masses on ``grid_nodes``.
|
|
node_check |
:class:`NodeCheck` or None — the convergence of these
|
laws in |
NodeCheck
dataclass
¶
NodeCheck(gap_nodes: int, gap_nodes_ref: int, mean_sd: float, tails: float, converged: bool)
Convergence of a forecast's predictive laws in gap_nodes.
The result of :func:forecast's check_nodes: the forecast at
gap_nodes = G against a second one at gap_nodes_ref = G_ref > G
(2G by default), at every horizon, with the definitions of the
forecasting study (report/forecasting/fc_common.py, quad_diff).
Attributes:
| Name | Type | Description |
|---|---|---|
gap_nodes |
int — G, the nodes of the returned forecast.
|
|
gap_nodes_ref |
int — G_ref, the nodes of the reference forecast.
|
|
mean_sd |
float — the largest |mean(G) − mean(G_ref)| and
|
|sd(G) − sd(G_ref)| over the horizons, in units of sd(G_ref) at the same horizon. |
tails |
float — the largest change of the 2.5 % and 97.5 %
|
quantiles of the node law over the horizons, in the same
unit. The node law is the discrete law on |
converged |
bool — mean_sd ≤ :data:`FORECAST_CHECK_TOL` (0.01) and
|
tails ≤ :data: |
The node-law quantiles are first order in the node spacing, and the |
|
|
returned ``quantile_values`` (the Legendre interpolant of the Nyström |
|
|
density) converge much faster |
``tails`` can flag a G whose returned
|
|
quantiles are converged (τ = 0.3, G = 16 |
tails 0.28, while the returned
|
|
2.5 % and 97.5 % quantiles are within 1.1e-3 sd of those at G = 256; at |
|
|
the default G = 64, tails 1.5e-3). It is the study's criterion |
the
|
|
study scores its interval forecasts on the node law. |
|
|
``mean_sd`` and ``tails`` are inf, and ``converged`` False, when a |
|
|
predictive sd collapses |
below 1e-3 of the sd of the observed values of
|
|
Y, at some horizon, at G or at G_ref. When Y has fewer than two distinct |
|
|
observed values, the sd of the reference law g_ref is used instead. A law |
|
|
that is genuinely that narrow is flagged too. The exact variants |
|
|
(``Forecast.method == "exact"``) get 0.0, 0.0 and True |
their laws do
|
|
not depend on ``gap_nodes``, and nothing is recomputed. |
|
Non-ignorable missingness mechanisms¶
The [missingness] block of a model's TOML file: the missingness mask
itself carries evidence on the hidden states.
StateMissingness
dataclass
¶
StateMissingness(rates: tuple[float, ...])
Mechanism "state": P(m_n = 1 | x_n = i) = π_i, independently given X.
Attributes:
| Name | Type | Description |
|---|---|---|
rates |
tuple of K floats in [0, 1] — π_i.
|
|
StateMarkovMissingness
dataclass
¶
StateMarkovMissingness(onset: tuple[float, ...], persistence: tuple[float, ...])
Mechanism "state-markov": a Markov mask whose transition depends on x_n.
Attributes:
| Name | Type | Description |
|---|---|---|
onset |
tuple of K floats in [0, 1] — a_i = P(m_n = 1 | m_{n-1} = 0, x_n = i).
|
|
persistence |
tuple of K floats in [0, 1] — b_i = P(m_n = 1 | m_{n-1} = 1, x_n = i).
|
|
The initial mask follows the stationary law of the mask chain under a |
|
|
constant state |
P(m_0 = 1 | x_0 = i) = s_i = a_i / (1 − b_i + a_i)
|
|
( |
attr:`stationary`). a_i = 0 with b_i = 1 is refused (s_i = 0/0).
|
|
stationary
property
¶
stationary: ndarray
(K,) s_i = a_i / (1 − b_i + a_i) — the law of m_0 given x_0 = i.
evidence ¶
evidence(miss) -> np.ndarray
(N, K) factors e_n(i) = p(m_n | m_{n-1}, x_n = i); e_0(i) = p(m_0 | x_0 = i).
log_evidence ¶
log_evidence(miss) -> np.ndarray
(N, K) log e_n(i) (−∞ where e_n(i) = 0), log(1 − ·) by log1p.
missingness_lr_test ¶
missingness_lr_test(model, Y, *, alternative: str = 'state', null: str = 'common', ice_cfg: dict | None = None, n_bootstrap: int = 0, seed: int = 0, progress_cb=None) -> MissingnessLRTest
Likelihood-ratio test of state-independent against state-dependent missingness.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
PMCModel
|
Starting point of the null fit (its own mechanism, if any, is only a
start: the null fit replaces it). Its |
required |
Y
|
array(N) or (N, d)
|
Observations; the rows with a non-finite value are the mask m. Y must have at least one missing and one observed row. |
required |
alternative
|
``"state"`` or ``"state-markov"`` — H1.
|
|
'state'
|
null
|
``"common"`` (a state-independent mechanism of the same kind,
|
default) or |
'common'
|
ice_cfg
|
dict
|
ICE settings of the fits, over the model's |
None
|
n_bootstrap
|
int
|
Parametric-bootstrap replicates B (0: asymptotic p-value only). Each costs two ICE fits. |
0
|
seed
|
int
|
Seed of the bootstrap (separate streams for the paths and the masks). |
0
|
progress_cb
|
callable
|
|
None
|
Returns:
| Type | Description |
|---|---|
class:`MissingnessLRTest` (see the module docstring for the statistic,
|
|
the null log-likelihood and the two p-values).
|
|
MissingnessLRTest
dataclass
¶
MissingnessLRTest(alternative: str, null: str, statistic: float, df: int, p_value: float, p_value_bootstrap: float, n_bootstrap: int, n_bootstrap_valid: int, bootstrap_statistics: tuple, log_lik_null: float, log_lik_alt: float, null_model: object, alt_model: object, null_params: dict, alt_params: dict, n_obs: int, n_missing: int, sup_log_lik_null: float, sup_log_lik_alt: float, profile_log_liks: dict, statistic_fits: float)
Result of :func:missingness_lr_test.
Fields
alternative : "state" or "state-markov" (H1).
null : "common" (state-independent) or "state" (H0).
statistic : LR = 2 (sup_log_lik_alt − sup_log_lik_null) ≥ 0
(module docstring, "The statistic").
df : degrees of freedom of the asymptotic χ².
p_value : asymptotic, χ²(df) survival function at statistic.
p_value_bootstrap : parametric-bootstrap p-value, NaN without bootstrap.
n_bootstrap : replicates requested (B).
n_bootstrap_valid : replicates that produced a statistic.
bootstrap_statistics : the replicate statistics LR_b (valid ones).
log_lik_null, log_lik_alt : log p(y_obs, m) of the two ICE fits.
null_model, alt_model : the fitted models (:class:~pmcprg.pmc.model.PMCModel),
each carrying its mechanism (missingness).
null_params, alt_params : the [missingness] tables of the two fitted
mechanisms.
n_obs, n_missing : N and M.
sup_log_lik_null, sup_log_lik_alt : the approximations of the two
suprema, max over the θ of both fits of the profile
log-likelihoods profile_log_liks.
profile_log_liks : {"null_at_null_theta": ℓ0(θ̂0), "null_at_alt_theta":
ℓ0(θ̂1), "alt_at_null_theta": ℓ1(θ̂0),
"alt_at_alt_theta": ℓ1(θ̂1)} — ℓ_h(θ) the
log-likelihood maximised over hypothesis h's
mechanism at θ.
statistic_fits : 2 (log_lik_alt − log_lik_null), the difference of the
two ICE fits alone — a diagnostic: it can be negative
where ICE is not EM (module docstring).
Erroneous observations: predictive PIT, flags, flag-and-mask estimation¶
One-step-ahead predictive PIT of every observed row (exact per variant,
missing rows integrated out), outlier flags with innovation gating and
multiplicity corrections, calibration checks, and a flag-and-mask ICE/SEM
that trims the flagged rows through the missing-data machinery. The
formulas, the multiplicity discussion and the design of the robust loop are
in the docstring of pmcprg/pmc/outliers.py; the simulation study is in
report/erroneous_data.
predictive_pit ¶
predictive_pit(model: PMCModel, Y, *, gap_nodes: int | None = None) -> PredictivePIT
One-step-ahead predictive PIT, p-values and normal scores of Y.
PIT_n = P(Y_n ≤ y_n | y_1:n−1) from the forward filter and the
conditional CDFs of the transition (module docstring, per variant);
missing rows before n are integrated out (exact shortcut, or the
quadrature grid of :mod:pmcprg.pmc.gaps), missing rows get NaN. Every
observed row enters the filter: to keep a flagged row out of the
prediction of the next ones, use :func:flag_outliers.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
PMCModel
|
|
required |
Y
|
|
required | |
gap_nodes
|
quadrature nodes G for the missing rows of the grid variants
|
|
None
|
Returns:
| Type | Description |
|---|---|
PredictivePIT
|
|
PredictivePIT
dataclass
¶
PredictivePIT(pit: ndarray, pvalue: ndarray, z: ndarray, log_pred: ndarray, log_lik: float, alpha_hat: ndarray, miss: ndarray, method: str)
One-step-ahead predictive PIT of a series (module docstring).
Attributes:
| Name | Type | Description |
|---|---|---|
pit |
(N,) PIT_n = P(Y_n ≤ y_n | past observed rows); NaN at a
|
missing row. |
pvalue |
(N,) two-sided p-value 2 min(PIT_n, 1 − PIT_n), capped at 1;
|
NaN at a missing row. |
z |
(N,) normal score Φ⁻¹(PIT_n), from the smaller tail (finite:
|
a tail probability is floored at the smallest normal float). |
log_pred |
(N,) log p(y_n | past), the log predictive density; NaN at a
|
missing row. |
log_lik |
float — Σ of the log normalisers of the pass: the
|
observed-data log-likelihood of the rows the filter used. |
alpha_hat |
(N, K) — P(x_n = i | rows up to n used by the filter).
|
|
miss |
(N,) bool — missing rows of Y (non-finite values).
|
|
method |
``"exact"`` (K-state filter) or ``"grid"`` (quadrature grid
|
for the missing rows). |
flag_outliers ¶
flag_outliers(model: PMCModel, Y, *, alpha: float = DEFAULT_ALPHA, sequential: bool = True, correction: str | None = None, gap_nodes: int | None = None) -> OutlierFlags
Flag the observed rows whose predictive p-value is too small.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
PMCModel
|
|
required |
Y
|
|
required | |
alpha
|
float
|
|
DEFAULT_ALPHA
|
sequential
|
gate a flagged row out of the filter of the rows after it
|
|
True
|
correction
|
``None`` (default), ``"bonferroni"`` or ``"bh"`` — module
|
|
None
|
gap_nodes
|
int | None
|
|
None
|
Returns:
| Type | Description |
|---|---|
OutlierFlags — ``flagged ⇔ pvalue < threshold`` at every observed row.
|
|
OutlierFlags
dataclass
¶
OutlierFlags(flagged: ndarray, pvalue: ndarray, pit: ndarray, z: ndarray, log_pred: ndarray, threshold: float, alpha: float, correction: str | None, sequential: bool, n_tests: int, log_lik: float, alpha_hat: ndarray, miss: ndarray, method: str)
Rows flagged by :func:flag_outliers and the statistics used.
Attributes:
| Name | Type | Description |
|---|---|---|
flagged |
(N,) bool — flagged rows (never a missing row).
|
|
pvalue |
(N,) two-sided p-values the decision used (with
|
|
pit, z |
(N,) the matching PITs and normal scores.
|
|
log_pred |
(N,) log predictive densities of the rows that entered the
|
filter; NaN at missing and flagged rows when |
threshold |
float — the per-row threshold: flagged ⇔ p < threshold.
|
|
alpha |
float — the requested level (per row, family-wise or FDR).
|
|
correction |
``None``, ``"bonferroni"`` or ``"bh"``.
|
|
sequential |
bool — whether flagged rows were gated out of the filter.
|
|
n_tests |
int — number of tested (observed) rows.
|
|
log_lik |
float — log-likelihood of the rows that entered the filter
|
(flagged rows integrated out when |
alpha_hat |
(N, K) — the filter P(x_n = i | rows up to n it used).
|
|
miss |
(N,) bool — missing rows of Y.
|
|
method |
``"exact"`` or ``"grid"``.
|
|
pit_checks ¶
pit_checks(pit, *, lags: int = 10, exclude=None) -> PitChecks
Uniformity and independence checks of a PIT sequence.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
pit
|
|
required | |
lags
|
int
|
|
10
|
exclude
|
optional (N,) bool mask of rows to leave out (e.g. the flagged
|
|
None
|
Returns:
| Type | Description |
|---|---|
PitChecks — KS test of U(0, 1) on the PITs; Ljung–Box on the normal
|
|
scores and on their squares, over the kept rows in order (the PITs of
|
|
the observed rows of a correct model are iid, whatever the missing rows
|
|
between them).
|
|
PitChecks
dataclass
¶
PitChecks(n: int, ks_stat: float, ks_pvalue: float, lb_stat: float, lb_pvalue: float, lb2_stat: float, lb2_pvalue: float, lags: int, z_mean: float, z_sd: float)
Calibration checks of a PIT sequence (:func:pit_checks).
Attributes:
| Name | Type | Description |
|---|---|---|
n |
number of PITs used (observed, not excluded).
|
|
ks_stat, ks_pvalue |
Kolmogorov–Smirnov test of U(0, 1) on the PITs.
|
|
lb_stat, lb_pvalue |
Ljung–Box Q on the normal scores, ``lags`` lags.
|
|
lb2_stat, lb2_pvalue |
Ljung–Box Q on the squared normal scores
|
(McLeod–Li). |
lags |
number of lags.
|
|
z_mean, z_sd |
mean and standard deviation of the normal scores (0 and 1
|
under the model). |
robust_estimate ¶
robust_estimate(model: PMCModel, Y, estim_cfg: dict | None = None, *, algorithm: str = 'ice', flag_cfg: dict | None = None, max_rounds: int = 10, initial_mask=None, progress_cb=None) -> RobustFit
Flag-and-mask estimation: fit, flag, set the flags to NaN, refit.
The design (masking as trimming, iteration against the masking effect, flags recomputed on the original Y, restarts from the initial model, breakdown, stopping rule) is in the module docstring, "Robust estimation".
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
PMCModel
|
|
required |
Y
|
|
required | |
estim_cfg
|
dict | None
|
|
None
|
algorithm
|
str
|
|
'ice'
|
flag_cfg
|
dict | None
|
|
None
|
max_rounds
|
int
|
|
10
|
initial_mask
|
optional (N,) bool — rows set to NaN for the first fit
|
|
None
|
progress_cb
|
|
None
|
Returns:
| Type | Description |
|---|---|
RobustFit
|
|
RobustFit
dataclass
¶
RobustFit(model: PMCModel, trace: object, mask: ndarray, flags: OutlierFlags, masks: list = list(), traces: list = list(), converged: bool = False)
Result of :func:robust_estimate.
Attributes:
| Name | Type | Description |
|---|---|---|
model |
PMCModel — the last fit.
|
|
trace |
IceTrace or SemTrace of the last fit.
|
|
mask |
(N,) bool — rows set to NaN for the last fit.
|
|
flags |
OutlierFlags — the flags of the last fit on the original Y
|
( |
masks |
list of (N,) bool — the mask of every fit, ``masks[0]`` the
|
|
traces |
list — the trace of every fit.
|
|
converged |
bool — the last flags equal the last mask.
|
|
ICE / SEM unsupervised estimation¶
Both estimators share the same M-step; ICE uses the soft forward-backward
posteriors and converges deterministically, SEM draws one
sample_posterior realisation per iteration. See
"ICE vs SEM"
in the README, and State labelling for how the
hidden-state indices are numbered and identified across a fit.
ice ¶
ice.py — Iterative Conditional Estimation (ICE) for unsupervised PMC/HMC fitting.
Public API
ice(model, Y, ice_cfg=None) -> (PMCModel, IceTrace)
Fit a PMCModel to the observation sequence Y using ICE.
Returns the fitted model and an :class:IceTrace capturing per-iteration
diagnostics (log-lik, τ, family, prior matrix, margin params; plus the
losing runs when multistart is enabled). Use trace.log_liks for the
plain log-likelihood history.
Algorithm (ICE for SR-PMC)
Given an initial model θ^(0) and observations Y = y_{1:N}:
Iterate until convergence:
E-step: Compute forward-backward quantities: α̂n(j), β̂_n(j) — normalized forward/backward variables γ_n(j) = P(X_n=j | Y) — marginal posteriors ξ_n(i,j) = P(X_n=i, X{n+1}=j | Y) — joint posteriors
M-step (Conditional estimation): 1. Prior p̂[i,j] = (1/N-1) Σ_n ξ_n(i,j)
2. For each pair (i,j) — copula selection + τ estimation:
a. Collect pseudo-observations (u_n, v_n) = (F_{ij}(y_n), F_{ji}(y_{n+1}))
weighted by ξ_n(i,j) for n = 1, …, N-1 (F_{ij} = F_i for state
margins). With ``copula_margins = "empirical"`` the F are the
posterior-weighted empirical margins of :func:`_empirical_margin_cdfs`
(a weighted rank pseudo-likelihood; the E-step keeps the model's F).
b. Select best copula family from 'candidates' by weighted log-likelihood.
c. Estimate τ_{ij} by weighted MLE on τ ∈ [τ_min, τ_max].
3. (Optional, ``fit_margins``) Margin re-estimation by weighted MLE, the
declared family kept (or selected among ``candidates`` — GICE):
* state margins f_i : y_n with weight γ_n(i);
* pair margins f_ij (general PMC, DerrodePieczynski_CSDA2013
Eqs. 12–14) — "dual view": y_n with weight ξ_n(i,j) and
y_{n+1} with weight ξ_n(j,i), since the right margin of the
pair (j,i) is f_ij. For a Gaussian margin,
μ̂_{ij} = Σ w y / Σ w and σ̂²_{ij} = Σ w (y − μ̂)² / Σ w
over that sample. An ICE-style estimator, not an exact EM
M-step (see ``_pair_margin_sample``).
Relation to the papers' ICE (DerrodePieczynski_CSDA2013 §4.2, DerrodePieczynski_SP2016 §3)
This is a responsibility-weighted variant of ICE, not the scheme of
Derrode & Pieczynski (2013, §4.2, Eqs. 21–24) and (2016, §3 steps (b)–(c),
Remark 3.1). There, only the prior p_ij is updated through the conditional
expectation of Eq. 22; the copula (and margin) parameters, whose conditional
expectation is not computable, are estimated by the complete-data estimator
applied to one posterior draw x^(q) ~ p(x | y, θ^q) — "replacing x_{1:N}
by x^q_{1:N} (L = 1)" — i.e. on the hard sub-samples y^{ij}(x^(q)). Here every
ξ_n(i,j) enters as a weight instead of a 0/1 membership, which makes the
M-step deterministic and EM-like (for known margins it is EM's M-step for
the copula parameters). :func:pmcprg.pmc.sem.sem is the hard-draw estimator
and therefore the closest thing in the package to the papers' ICE with
L = 1 — except that it also takes p_ij from the draw rather than from the
expectation. Neither reproduces the papers' scheme to the letter; the
reproduction report (report/csda2013_reproduction.tex, §Exp. 3) says
which one it ran. (Audit K-4.)
ICE configuration (TOML [ice] section or dict)
fit_margins : bool (default False) — re-estimate margin parameters.
max_iter : int (default 50) — maximum EM iterations.
tol : float (default 1e-4) — relative log-likelihood convergence threshold.
candidates : list[str] — SHORT_NAMEs of candidate copula families.
default: all 1-parameter available families except Product.
n_starts, multistart_seed, multistart_jitter, multistart_workers,
multistart_families — multistart (see :func:_parse_ice_cfg for the full list).
return_best_iterate : bool (default False) — return the iterate with the
highest log-likelihood instead of the last one.
missing_strategy, missing_draws, missing_seed, gap_nodes — missing
observations (NaN rows of Y): see :func:ice, section "Missing observations".
copula_margins : str (default "parametric") — "empirical" computes the
copula step's pseudo-observations from posterior-weighted
empirical margins instead of the model's F (AUDIT_COPULES
FR-7 a; see :func:_parse_ice_cfg and
:func:_empirical_margin_cdfs).
missingness : str (default "model") — the missingness mechanism: the
model's, held fixed; "ignorable"; or "state" /
"state-markov", estimated (see :func:ice, section
"Missingness mechanism").
References
ICE itself (the references DerrodePieczynski_SP2016 gives for the method):
- Pieczynski, W. (1992). Statistical image segmentation. Machine Graphics and Vision 1(1/2), 261–268.
- Delignon, Y., Marzouki, A. & Pieczynski, W. (1997). Estimation of generalized mixtures and its application in image segmentation. IEEE Trans. Image Processing 6(10), 1364–1375.
- Giordana, N. & Pieczynski, W. (1997). Estimation of generalized multisensor hidden Markov chains and unsupervised image segmentation. IEEE Trans. PAMI 19(5), 465–475.
The two papers this package reproduces:
- DerrodePieczynski_CSDA2013 — Derrode, S. & Pieczynski, W. (2013). Unsupervised data classification using pairwise Markov chains with automatic copulas selection. CSDA 63, 81–98.
- DerrodePieczynski_SP2016 — Derrode, S. & Pieczynski, W. (2016). Unsupervised classification using hidden Markov chain with unknown noise copulas and margins. Signal Processing 128, 8–17.
IceTrace
dataclass
¶
IceTrace(log_liks: list[float] = list(), tau_history: ndarray = (lambda: np.empty((0, 0, 0), dtype=float))(), family_history: list[list[list[str]]] = list(), p_history: ndarray = (lambda: np.empty((0, 0, 0), dtype=float))(), margin_history: list[list[dict]] = list(), multistart_runs: list[IceTrace] = list(), run_tag: str = '', candidates: list[str] = list(), best_iter: int = -1, returned_iter: int = -1, degenerate: list | None = None, missingness_history: list[dict | None] = list())
Per-iteration ICE history, captured for diagnostic visualisations.
Snapshot semantics
All time-indexed arrays/lists have length T = number of completed
E-steps. The snapshot at index t reflects the model state as the
log-likelihood log_liks[t] was computed — i.e. just before the
M-step that produces the model used for log_liks[t + 1]::
E-step ──→ M-step ──→ E-step ──→ …
▲ ▲
│ │
snapshot[t] snapshot[t+1]
log_liks[t] log_liks[t+1]
For T = 0 (no completed iteration), the array fields are empty arrays
of shape (0, K, K) — never None.
Fields
log_liks : list[float] — log-likelihood trace.
tau_history : np.ndarray shape (T, K, K), dtype float —
τ_K of the copula at pair (i, j) for each iteration.
NaN if the variant has no copula at that pair (e.g.
HMC-IN) or the candidate selection failed.
family_history : list[list[list[str]]] — same shape (T, K, K),
each entry is the SHORT_NAME of the selected family
(or "" if no copula).
p_history : np.ndarray shape (T, K, K) — joint prior
p[i,j] = P(X_n=i, X_{n+1}=j) (computed as
π·A for HMC variants).
margin_history : list[list[dict]] — at index t a list of margin
blocks {"i": ..., "j": ..., "dist": ..., "params": {...}}
in declaration order, mirroring model.margin_blocks().
multistart_runs: list[IceTrace] — when n_starts > 1, the traces
from the non-best runs (the chosen run is the trace
itself). Empty list when single-start.
run_tag : str — label of this run ("unperturbed",
"perturbed-3", …) when multistart was used.
candidates : list[str] — copula candidate SHORT_NAMEs that ICE
iterated over, captured for the family-ribbon legend.
best_iter : int — index of the highest log_liks value (first
one on ties, NaN ranked lowest); -1 when the trace
is empty. Set whatever return_best_iterate.
returned_iter : int — iterate q of the returned parameter set θ^q, so
that log_liks[returned_iter] is its log-likelihood
when returned_iter < len(log_liks). With
return_best_iterate it equals best_iter. Without
it, len(log_liks) - 1 when ICE stopped early
(convergence, patience) and len(log_liks) when
the run used all max_iter iterations: the model of
the last M-step was returned without being evaluated
(always the case for SEM). -1 when not recorded.
degenerate : list[DegenerateFinding] | None — degenerate states
of the returned model
(:func:pmcprg.pmc._estim_common.degenerate_states),
[] when none; None when not checked (the traces
of multistart_runs, whose models are not kept).
missingness_history : list[dict | None] — length T: at index
t the [missingness] table of the iterate
({"mechanism": "state", "rates": [...]} or
{"mechanism": "state-markov", "onset": [...],
"persistence": [...]}, :mod:pmcprg.pmc.missingness),
None when it is ignorable. Constant unless the
missingness config key estimates the mechanism.
IceResult
dataclass
¶
IceResult(initial_model: PMCModel, fitted_model: PMCModel, Y: ndarray, trace: IceTrace)
Bundle returned by GUI/diagnostics layers around an ICE run.
Carries everything the View-selector needs to draw the 12 ICE views
(the trace alone is not sufficient — view J needs the initial
model, view C and H need the observation sequence Y).
Fields
initial_model : :class:PMCModel — the model handed to ice().
fitted_model : :class:PMCModel — the best-run model returned by ICE.
Y : np.ndarray — observations ICE was fitted to.
trace : :class:IceTrace — per-iteration history.
ice ¶
ice(model: PMCModel, Y: ndarray, ice_cfg: dict | None = None, progress_cb=None) -> tuple[PMCModel, IceTrace]
Iterative Conditional Estimation (ICE) for unsupervised PMC/HMC fitting.
With n_starts > 1 (config), runs ICE several times from random
perturbations of model and keeps the run with the highest final
log-likelihood — this hardens the fit against multimodal log-likelihoods
(a real concern for archimedean copula mixtures and HMC variants with
many similar margins). The first start is always the unperturbed model.
Parameter jitter keeps the copula families of model; with
multistart_families = "random" or "sweep" the starts also change
them, which is what leaves a wrong-family basin (see
:func:_parse_ice_cfg).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
PMCModel
|
|
required |
Y
|
ndarray
|
|
required |
ice_cfg
|
dict | None
|
|
None
|
progress_cb
|
|
None
|
Returns:
| Name | Type | Description |
|---|---|---|
fitted_model |
PMCModel — model with updated parameters (the best run
|
when multistart is enabled; its best iterate
with |
trace |
IceTrace — captured per-iteration diagnostics. Use
|
|
Degenerate states
The returned model is checked by
:func:pmcprg.pmc._estim_common.degenerate_states (a state of
stationary weight below 0.5 %, a margin standard deviation below 1 % of
that of the observed data, a copula τ within 1e-3 of ±1): the findings
are stored in trace.degenerate and, when there are any, logged as one
WARNING line. On such a fit the likelihood may be unbounded — a variance
collapsing on a data atom gains nats without limit — and should not be
compared with others. Nothing in the estimate changes.
Missing observations
A row of Y with a non-finite value is missing (ignorable missingness,
MCAR/MAR — :mod:pmcprg.pmc.gaps). A Y without missing rows runs the
historical algorithm unchanged. Otherwise every E-step uses the
missing-data inference of :mod:pmcprg.pmc.gaps (exact K-state shortcut
for HMC-IN, HMC-IN2 and PMC-IN with state margins, quadrature grid of
gap_nodes nodes otherwise), trace.log_liks is the observed-data
log-likelihood log p(y_obs), and the M-step follows missing_strategy:
"available"(default; deterministic) — γ, ξ exact given y_obs at every n (gap_posterior); prior from ξ over all n; state margins on the observed y_n with weight γ_n(i); pair margins on the observed endpoints of their dual view (same ξ weights); copulas on the pairs whose two endpoints are observed, weight ξ_n(i, j). See :func:_m_step."impute"— multiple imputation:missing_drawscompletions (x, y_mis) ~ P(· | y_obs, θ^q) per iteration (seedmissing_seed), the complete-data ICE estimator on each completed series, parameter estimates averaged (DerrodePieczynski_CSDA2013 Eq. 24 with L =missing_draws), prior from the exact ξ. See :func:_m_step_impute.
init = "kmeans" clusters the observed rows only
(:func:_warmstart_from_kmeans); GICE margin selection runs on the
samples above (observed values, or each completed series). Why
"available" is the default (bias/RMSE against the missing rate) is
documented at :data:DEFAULT_MISSING_STRATEGY.
Missingness mechanism
The config key missingness (:data:MISSINGNESS_MODES, also in the
TOML [ice] table) says what ICE does with the law of the mask m
(:mod:pmcprg.pmc.missingness, section "Estimation"):
"model"(default) —model.missingnessis carried unchanged to every iterate and to the returned model, its parameters held fixed: ignorable when the model has none (the text above), otherwise every E-step is given (y_obs, m) —trace.log_liksis log p(y_obs, m), γ and ξ are P(· | y_obs, m), the completions of"impute"are drawn given (y_obs, m) and the complete-data E-step of each completed series keeps the observed mask. A Y without missing rows still has a mask (m = 0), which the E-step uses too. The M-step formulas are those above: the factors of p(m | x) do not involve the other parameters."ignorable"— any mechanism is dropped: every iterate and the returned model havemissingness is None."state"/"state-markov"— the mechanism of that kind is estimated with the other parameters. E-steps as for"model"; the M-step adds :func:pmcprg.pmc.missingness.estimate_mechanismon the exact γ given (y_obs, m) — for"impute"too, as the prior takes the exact ξ. Start: the model's mechanism of that kind if it has one, otherwise the state-independent MLE of the mask, whose first E-step is the ignorable one. Each M-step adds one pseudo-observation at the pooled rate (the boundary guard: a rate at 0 or 1 would be absorbing). The returned model carries the estimate. A Y without missing rows identifies no rate (MLE 0: the ignorable model): WARNING and"ignorable".
trace.missingness_history records the [missingness] table of
every iterate (None when ignorable), like the other parameters.
Testing a state-dependent mechanism against a state-independent one:
:func:pmcprg.pmc.missingness_lr.missingness_lr_test.
ice_image ¶
ice_image(model: PMCModel, img: ndarray, ice_cfg: dict | None = None, progress_cb=None) -> tuple[PMCModel, IceTrace]
ICE on a 2D image — linearises along the gilbert path then calls :func:ice.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
PMCModel
|
|
required |
img
|
ndarray
|
Shape |
required |
ice_cfg
|
dict | None
|
|
None
|
progress_cb
|
callable, optional — same as :func:`ice`.
|
|
None
|
Returns:
| Name | Type | Description |
|---|---|---|
fitted_model |
PMCModel
|
|
trace |
IceTrace
|
|
ice_image ¶
ice_image(model: PMCModel, img: ndarray, ice_cfg: dict | None = None, progress_cb=None) -> tuple[PMCModel, IceTrace]
ICE on a 2D image — linearises along the gilbert path then calls :func:ice.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
PMCModel
|
|
required |
img
|
ndarray
|
Shape |
required |
ice_cfg
|
dict | None
|
|
None
|
progress_cb
|
callable, optional — same as :func:`ice`.
|
|
None
|
Returns:
| Name | Type | Description |
|---|---|---|
fitted_model |
PMCModel
|
|
trace |
IceTrace
|
|
IceResult
dataclass
¶
IceResult(initial_model: PMCModel, fitted_model: PMCModel, Y: ndarray, trace: IceTrace)
Bundle returned by GUI/diagnostics layers around an ICE run.
Carries everything the View-selector needs to draw the 12 ICE views
(the trace alone is not sufficient — view J needs the initial
model, view C and H need the observation sequence Y).
Fields
initial_model : :class:PMCModel — the model handed to ice().
fitted_model : :class:PMCModel — the best-run model returned by ICE.
Y : np.ndarray — observations ICE was fitted to.
trace : :class:IceTrace — per-iteration history.
IceTrace
dataclass
¶
IceTrace(log_liks: list[float] = list(), tau_history: ndarray = (lambda: np.empty((0, 0, 0), dtype=float))(), family_history: list[list[list[str]]] = list(), p_history: ndarray = (lambda: np.empty((0, 0, 0), dtype=float))(), margin_history: list[list[dict]] = list(), multistart_runs: list[IceTrace] = list(), run_tag: str = '', candidates: list[str] = list(), best_iter: int = -1, returned_iter: int = -1, degenerate: list | None = None, missingness_history: list[dict | None] = list())
Per-iteration ICE history, captured for diagnostic visualisations.
Snapshot semantics
All time-indexed arrays/lists have length T = number of completed
E-steps. The snapshot at index t reflects the model state as the
log-likelihood log_liks[t] was computed — i.e. just before the
M-step that produces the model used for log_liks[t + 1]::
E-step ──→ M-step ──→ E-step ──→ …
▲ ▲
│ │
snapshot[t] snapshot[t+1]
log_liks[t] log_liks[t+1]
For T = 0 (no completed iteration), the array fields are empty arrays
of shape (0, K, K) — never None.
Fields
log_liks : list[float] — log-likelihood trace.
tau_history : np.ndarray shape (T, K, K), dtype float —
τ_K of the copula at pair (i, j) for each iteration.
NaN if the variant has no copula at that pair (e.g.
HMC-IN) or the candidate selection failed.
family_history : list[list[list[str]]] — same shape (T, K, K),
each entry is the SHORT_NAME of the selected family
(or "" if no copula).
p_history : np.ndarray shape (T, K, K) — joint prior
p[i,j] = P(X_n=i, X_{n+1}=j) (computed as
π·A for HMC variants).
margin_history : list[list[dict]] — at index t a list of margin
blocks {"i": ..., "j": ..., "dist": ..., "params": {...}}
in declaration order, mirroring model.margin_blocks().
multistart_runs: list[IceTrace] — when n_starts > 1, the traces
from the non-best runs (the chosen run is the trace
itself). Empty list when single-start.
run_tag : str — label of this run ("unperturbed",
"perturbed-3", …) when multistart was used.
candidates : list[str] — copula candidate SHORT_NAMEs that ICE
iterated over, captured for the family-ribbon legend.
best_iter : int — index of the highest log_liks value (first
one on ties, NaN ranked lowest); -1 when the trace
is empty. Set whatever return_best_iterate.
returned_iter : int — iterate q of the returned parameter set θ^q, so
that log_liks[returned_iter] is its log-likelihood
when returned_iter < len(log_liks). With
return_best_iterate it equals best_iter. Without
it, len(log_liks) - 1 when ICE stopped early
(convergence, patience) and len(log_liks) when
the run used all max_iter iterations: the model of
the last M-step was returned without being evaluated
(always the case for SEM). -1 when not recorded.
degenerate : list[DegenerateFinding] | None — degenerate states
of the returned model
(:func:pmcprg.pmc._estim_common.degenerate_states),
[] when none; None when not checked (the traces
of multistart_runs, whose models are not kept).
missingness_history : list[dict | None] — length T: at index
t the [missingness] table of the iterate
({"mechanism": "state", "rates": [...]} or
{"mechanism": "state-markov", "onset": [...],
"persistence": [...]}, :mod:pmcprg.pmc.missingness),
None when it is ignorable. Constant unless the
missingness config key estimates the mechanism.
sem ¶
sem.py — Stochastic EM (SEM) for unsupervised PMC/HMC fitting.
Public API
sem(model, Y, sem_cfg=None) -> (PMCModel, SemTrace)
Fit a :class:PMCModel to the observation sequence Y using SEM.
Returns the fitted model and a :class:SemTrace capturing per-iteration
diagnostics.
sem_image(model, img, sem_cfg=None) -> (PMCModel, SemTrace) Convenience wrapper that linearises a 2D image along the Generalized Hilbert ("gilbert") path and runs SEM on it.
Algorithm
Given an initial model θ⁽⁰⁾ and observations Y = y_{1:N}, SEM alternates:
E-step (filter)
Compute α̂n(j) = P(X_n=j | Y{1:n}) via :func:forward.
S-step (stochastic completion)
Draw one realisation X̃ ~ P(X | Y) via Forward-Filter Backward-Sample
(see :func:sample_posterior). Build hard one-hot posteriors
γ̃_n(i) = 1[X̃_n = i]
ξ̃_n(i, j) = 1[X̃_n = i, X̃_{n+1} = j]
M-step (supervised-style)
Update prior, margins (optional), copula τ from the hard posteriors —
delegated to :func:pmcprg.pmc._estim_common.m_step so the implementation
stays identical to ICE's M-step body. State margins f_i are fitted on
{y_n : X̃n = i}; pair margins f_ij (general PMC, DerrodePieczynski_CSDA2013
Eqs. 12–14) on the dual-view sub-sample {y_n : (X̃_n, X̃{n+1}) = (i, j)} ∪
{y_{n+1} : (X̃n, X̃{n+1}) = (j, i)}, and the copula c_ij on the
pseudo-observations (F_ij(y_n), F_ji(y_{n+1})) of the pairs drawn in
(i, j) — the model's F, or with copula_margins = "empirical" the
rescaled empirical CDFs of those drawn sub-samples. A pair that does not
occur in the draw keeps its margin (and copula) unchanged.
Where SEM differs from ICE
- Stochastic E-step. Each iteration draws a single X̃ ~ P(X|Y) instead of computing the soft posteriors ξ, γ. Successive iterations are random; the log-likelihood trace fluctuates rather than monotonically increasing.
- Convergence detection. The log-lik does NOT converge in the
deterministic sense. We therefore stop only at
max_iter(nopatience/tolearly-stop). For inference, prefer averaging the post burn-in estimates externally (the full trace is exposed). - Multistart and K-means warm-start are supported with the same
semantics as ICE — see :func:
pmcprg.pmc._estim_common.warmstart_from_kmeans.
Missing observations
A row of Y with a non-finite value is missing (ignorable missingness,
:mod:pmcprg.pmc.gaps); a Y without missing rows runs the algorithm
above unchanged. Otherwise SEM is exact data augmentation (Tanner & Wong
1987): the S-step draws the states and the missing values jointly,
(X̃, ỹ_mis) ~ P(x, y_mis | y_obs, θ^q), by forward-filter backward-sample on
the missing-data chain of :mod:pmcprg.pmc.gaps (the exact K-state shortcut
for HMC-IN, HMC-IN2 and PMC-IN with state margins — then ỹ_n ~ f_{X̃_n}; the
quadrature grid of gap_nodes nodes otherwise — then ỹ_n is the drawn
node). The M-step is the complete-data one on the completed series
(ỹ_mis filled in), GICE margin selection included; log_liks is the
observed-data log-likelihood log p(y_obs) of the forward pass. The k-means
warm start clusters the observed rows only.
Missingness mechanism
The config key missingness is ICE's (:func:pmcprg.pmc.ice.ice, section
"Missingness mechanism"; :mod:pmcprg.pmc.missingness, "Estimation"):
"model"(default) —model.missingnessis carried unchanged to every iterate and to the returned model, its parameters held fixed. With a mechanism the S-step draws from P(x, y_mis | y_obs, m, θ^q) andlog_liksis log p(y_obs, m) — the missingness factors are in the forward messages the draw comes from, for a complete Y (mask m = 0) too."ignorable"— any mechanism is dropped."state"/"state-markov"— estimated: the M-step adds ICE's formulas (:func:pmcprg.pmc.missingness.estimate_mechanism) on the one-hot drawn path γ̃, with the same start (the model's mechanism of that kind, or the state-independent MLE of the mask) and the same boundary guard. A Y without missing rows falls back to"ignorable"with a WARNING.
trace.missingness_history records the mechanism of every iterate.
Drawing ỹ_n on the quadrature nodes rather than from the continuous law is
an approximation of the grid variants; measured against a continuous
within-cell jitter and a 4× finer grid, it leaves the estimates unbiased
where the jitter is not: see :data:pmcprg.pmc.ice._GAP_DRAW.
Relation to the papers' ICE
SEM is the closest estimator in the package to the ICE of the papers with
L = 1 (DerrodePieczynski_CSDA2013 §4.2, DerrodePieczynski_SP2016 §3): both
estimate the copula and margin parameters on one posterior draw x^(q) ~
p(x | y, θ^q). It differs in taking the prior p_ij from the draw too, where
the papers use its conditional expectation.
The :mod:pmcprg.pmc.ice module docstring ("Relation to the papers' ICE")
sets the two side by side.
Reference
Celeux, G. & Diebolt, J. (1985). The SEM algorithm: a probabilistic teacher algorithm derived from the EM algorithm for the mixture problem. Computational Statistics Quarterly 2, 73–82.
Tanner, M. A. & Wong, W. H. (1987). The calculation of posterior distributions by data augmentation. Journal of the American Statistical Association 82(398), 528–540.
The awesomePMC
implementation reuses the shared M-step / warm-start helpers in
:mod:pmcprg.pmc._estim_common to avoid duplicating the update logic.
SemTrace
dataclass
¶
SemTrace(log_liks: list[float] = list(), tau_history: ndarray = (lambda: np.empty((0, 0, 0), dtype=float))(), family_history: list[list[list[str]]] = list(), p_history: ndarray = (lambda: np.empty((0, 0, 0), dtype=float))(), margin_history: list[list[dict]] = list(), multistart_runs: list[IceTrace] = list(), run_tag: str = '', candidates: list[str] = list(), best_iter: int = -1, returned_iter: int = -1, degenerate: list | None = None, missingness_history: list[dict | None] = list(), sampled_X_history: ndarray = (lambda: np.empty((0, 0), dtype=int))())
Bases: IceTrace
Per-iteration SEM history — :class:pmcprg.pmc.ice.IceTrace plus the
SEM-specific stochastic completions.
Inherits every :class:IceTrace field (log_liks, tau_history,
family_history, p_history, margin_history, multistart_runs,
run_tag, candidates, best_iter, returned_iter,
degenerate, missingness_history) and the n_iters /
__len__ helpers, so
the two share all GUI/plotting code. Adds:
sampled_X_history: (T, N) int — the FFBS draw X̃ used at each M-step (no counterpart in :class:IceTrace).
SemResult
dataclass
¶
SemResult(initial_model: PMCModel, fitted_model: PMCModel, Y: ndarray, trace: IceTrace)
Bases: IceResult
Bundle returned by GUI/diagnostics layers around an SEM run.
Identical structure to :class:pmcprg.pmc.ice.IceResult (initial_model,
fitted_model, Y, trace); kept as a distinct class so callers
can tell an SEM result from an ICE one by type.
sem ¶
sem(model: PMCModel, Y: ndarray, sem_cfg: dict | None = None, progress_cb=None) -> tuple[PMCModel, SemTrace]
Stochastic EM (SEM) for unsupervised PMC/HMC fitting.
With n_starts > 1 (config), runs SEM several times from random
perturbations of model and keeps the run with the highest final
log-likelihood — the same multistart strategy as
:func:pmcprg.pmc.ice.ice, multistart_families included.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
PMCModel
|
|
required |
Y
|
ndarray
|
|
required |
sem_cfg
|
dict | None
|
|
None
|
progress_cb
|
|
None
|
Returns:
| Name | Type | Description |
|---|---|---|
fitted_model |
PMCModel — the model at the last iteration (or the best
|
run when multistart is enabled). |
trace |
SemTrace — per-iteration history.
|
|
Best iterate
With return_best_iterate = True the model returned is the iterate θ^q
with the highest observed-data log-likelihood log_liks[q] (computed
with θ^q before the S- and M-steps that give θ^(q+1); after the last
M-step, θ^max_iter is evaluated once more, so the trace has
max_iter + 1 log-likelihoods and max_iter draws). This is a
heuristic, not an SEM estimator: SEM's iterates are a Markov chain whose
stationary spread is the Monte-Carlo noise of one draw, and the
likelihood-best iterate of that chain is not its mean — averaging the
post-burn-in iterates is the usual estimate. It picks the point of the
chain that fits best, and inherits its noise. trace.best_iter and
trace.returned_iter locate it; multistart ranks each start by the
log-likelihood of the model it returns.
Degenerate states
Checked on the returned model as for :func:pmcprg.pmc.ice.ice
(trace.degenerate, one WARNING line).
sem_image ¶
sem_image(model: PMCModel, img: ndarray, sem_cfg: dict | None = None, progress_cb=None) -> tuple[PMCModel, SemTrace]
SEM on a 2D image — linearises along the gilbert path then calls :func:sem.
Same API as :func:pmcprg.pmc.ice.ice_image.
sem_image ¶
sem_image(model: PMCModel, img: ndarray, sem_cfg: dict | None = None, progress_cb=None) -> tuple[PMCModel, SemTrace]
SEM on a 2D image — linearises along the gilbert path then calls :func:sem.
Same API as :func:pmcprg.pmc.ice.ice_image.
SemResult
dataclass
¶
SemResult(initial_model: PMCModel, fitted_model: PMCModel, Y: ndarray, trace: IceTrace)
Bases: IceResult
Bundle returned by GUI/diagnostics layers around an SEM run.
Identical structure to :class:pmcprg.pmc.ice.IceResult (initial_model,
fitted_model, Y, trace); kept as a distinct class so callers
can tell an SEM result from an ICE one by type.
SemTrace
dataclass
¶
SemTrace(log_liks: list[float] = list(), tau_history: ndarray = (lambda: np.empty((0, 0, 0), dtype=float))(), family_history: list[list[list[str]]] = list(), p_history: ndarray = (lambda: np.empty((0, 0, 0), dtype=float))(), margin_history: list[list[dict]] = list(), multistart_runs: list[IceTrace] = list(), run_tag: str = '', candidates: list[str] = list(), best_iter: int = -1, returned_iter: int = -1, degenerate: list | None = None, missingness_history: list[dict | None] = list(), sampled_X_history: ndarray = (lambda: np.empty((0, 0), dtype=int))())
Bases: IceTrace
Per-iteration SEM history — :class:pmcprg.pmc.ice.IceTrace plus the
SEM-specific stochastic completions.
Inherits every :class:IceTrace field (log_liks, tau_history,
family_history, p_history, margin_history, multistart_runs,
run_tag, candidates, best_iter, returned_iter,
degenerate, missingness_history) and the n_iters /
__len__ helpers, so
the two share all GUI/plotting code. Adds:
sampled_X_history: (T, N) int — the FFBS draw X̃ used at each M-step (no counterpart in :class:IceTrace).