mixle.inference.structure module¶
Automatic dependency-structure learning for heterogeneous records – the tagline, taken literally.
CompositeDistribution models a record’s fields as independent (Naive-Bayes under a mixture). But real
heterogeneous data has cross-field dependence – a category shifts a real’s mean, a count’s rate tracks another
field – and modeling it is worth a great deal of likelihood (a blatant category->Gaussian link is ~1000 nats on
600 rows). No mainstream tool discovers that structure across arbitrary families: Stan/PyMC make you write it,
sklearn/pomegranate mixtures assume independence, bnlearn/pgmpy are discrete-or-Gaussian only.
This module closes the gap. Dependence is detected by modeling it: fit P(child) vs
P(child | parent) and compare description length (dependency_gain()). The winning edges are assembled
into a DependencyTreeDistribution – a directed forest over the record where each field is either a
marginal or a per-parent-value conditional (a real ConditionalDistribution
edge) – and learn_structure() picks the forest and fits it automatically. The result scores, samples, and
composes like any mixle distribution, but models the dependence a composite drops.
- dependency_gain(parent, child, child_estimator, *, max_its=30, penalty='bic', rng=None)[source]
Description-length gain (nats) of modeling
childconditioned on a discreteparentvs. independently.Fits the marginal
P(child)and the conditionalP(child | parent)(a child model per parent value) on the same data and returnsLL_cond - LL_marginalminus a complexity penalty for the extra parameters (BIC:0.5 * (levels - 1) * k * ln n). Positive means the dependence is worth modeling. This is a model-based dependency test – it works across any pair of families, unlike a same-type MI estimate.rngseeds the fits’ EM initializations (None= a fixed seed: deterministic by default; matters when the child family needs a randomized init, e.g. a mixture).
- class LinearGaussianEdge(a, b, sigma2)[source]
Bases:
objectA regression edge:
P(child | parent) = Normal(a + b*parent, sigma2).Where the current per-parent-bin conditional needs
bins * kparameters (and coarse bins) to model a smooth continuous dependence, this captures it with a single slopeb— far more statistically efficient and exact for a linear relationship. Slots intoDependencyTreeDistributionas a factor with an identity binner (the raw parent value drives the conditional).- log_density(x)[source]
Evaluate
log p(child | parent)for one parent-child pair.
- seq_log_density(encoded)[source]
Evaluate log densities for encoded parent-child pairs.
- fit_linear_gaussian_edge(pairs)[source]
OLS fit of a linear-Gaussian conditional
child ~ a + b*parent(closed form).
- regression_gain(parent, child, child_estimator, *, max_its=30, penalty='bic', rng=None)[source]
Description-length gain (nats) of a linear-Gaussian regression edge
child ~ a + b*parentover the child marginal. One extra parameter (the slope) vs. thebins * ka binned conditional spends — so for a real linear dependence this beats binning decisively. Returns-infwhen a regression is undefined.rngseeds the marginal fit’s EM initialization (None= a fixed seed: deterministic by default).
- class GLMEdge(family, beta, link, phi=1.0)[source]
Bases:
objectA generalized-linear regression edge: a count child’s rate
= exp(a + b*parent)(Poisson log-link), a binary child’s probability= logit^-1(a + b*parent)(logistic) — the heterogeneous generalization ofLinearGaussianEdge(McCullagh & Nelder 1989). One slope parameter, fit by IRLS viamixle.inference.glm.glm(). Models a count/binary child driven by a continuous parent far better than a coarse per-bin conditional.- log_density(x)[source]
Evaluate
log p(child | parent)for one GLM edge pair.
- seq_log_density(encoded)[source]
Evaluate log densities for encoded GLM edge pairs.
- fit_glm_edge(pairs, family)[source]
Fit a GLM conditional
child ~ g^-1(a + b*parent)(family’s canonical link) by IRLS.
- glm_gain(parent, child, child_estimator, family, *, max_its=30, penalty='bic', rng=None)[source]
Description-length gain (nats) of a GLM
familyregression edge over the child marginal (one extra slope parameter). Returns-infwhen the fit is undefined or non-finite.rngseeds the marginal fit’s EM initialization (None= a fixed seed: deterministic by default).
- class DependencyTreeDistribution(parents, factors, binners=None)[source]
Bases:
objectA directed-forest joint over a heterogeneous record: each field is a marginal or a conditional on its parent.
log_density(record) = sum_root log P(f_root) + sum_child log P(f_child | f_parent). The dependence aCompositeDistributionassumes away is modeled here as per-parent-value conditionals – while it still scores, samples, and composes like any mixle distribution.- Parameters:
parents (Sequence[int | None])
factors (Sequence[Any])
binners (Sequence[Any] | None)
- log_density(x)[source]
Evaluate the dependency-tree joint log density for one record.
- seq_log_density(encoded)[source]
Evaluate dependency-tree joint log density for encoded records.
- sampler(seed=None)[source]
Return a sampler for the dependency tree.
- class MixtureOfDependencyTrees(components, weights)[source]
Bases:
objectA latent mixture whose components each carry their own discovered dependency structure.
log p(x) = logsumexp_k ( log w_k + log p_k(x) )where eachp_kis aDependencyTreeDistribution. This is the deep form of the tagline: it discovers both the clustering and the within-cluster cross-field dependence – so the same category can map to different reals in different clusters, which neither a single dependency tree (one relationship) nor a mixture of independent composites (no within-cluster dependence) can represent. Fit bylearn_mixture_structure().- Parameters:
components (Sequence[DependencyTreeDistribution])
weights (Sequence[float])
- property w: ndarray
Mixture-convention alias for
weights– lets mixture-generic tooling (e.g.mixle.utils.hvis.model_fit_health/hvis_map) accept this model unchanged.
- property log_w: ndarray
Mixture-convention alias for
log_weights(seew).
- log_density(x)[source]
Evaluate mixture log density for one dependency-tree record.
- seq_log_density(encoded)[source]
Evaluate mixture log density for encoded dependency-tree records.
- responsibilities(data)[source]
Posterior
p(component | record)for each record – the E-step and a soft cluster assignment.
- sampler(seed=None)[source]
Return a sampler for the mixture of dependency trees.
- property n_components: int
Return the number of mixture components.
- learn_mixture_structure(data, n_components, *, restarts=3, max_iter=15, seed=0, min_gain=0.0, n_bins=4, max_its=30, field_estimators=None)[source]
Fit a
MixtureOfDependencyTreesby hard EM: discover clusters and each cluster’s dependency graph.Each iteration re-learns a dependency forest per cluster on its currently-assigned points (M-step), then reassigns every record to its most-probable cluster (E-step), until assignments stabilize. Runs
restartsrandom initializations and returns the highest-likelihood fit. Empty/tiny clusters are re-seeded so a component never collapses. Deterministic givenseed: the oneRandomStatedrives the k-means/random initializations AND every per-cluster fit’s EM init (vialearn_structure()’srng).field_estimatorspins each field’s family (forwarded tolearn_structure()) instead of re-running the automatic detector per cluster per iteration. Beyond the speedup, pinning matters for identifiability: the detector models a multimodal column with a Gaussian MIXTURE, which lets ONE cluster absorb what the caller intended as two – with per-field families pinned to unimodal models, regimes that differ in level must separate into different components to score well. random initializations and returns the highest-likelihood fit. Empty or very small clusters are re-seeded so a component never collapses.
- mixture_structure_health(mot, data, *, merged_sep_threshold=None)[source]
The identifiability receipt for a fitted
MixtureOfDependencyTrees.The trap it names: a flexible per-field family (a Gaussian-mixture conditional, a heavy-tailed catch-all marginal) lets ONE component absorb what the caller intended as SEVERAL regimes – and because the absorbed fit scores well, likelihood-level receipts look healthy. Measured concretely: on two planted regimes, a one-component fit matches the two-component fit’s likelihood, so nothing downstream of the density can see the difference.
The check that can: STRUCTURE-CONDITIONAL multimodality. For every continuous field of every component, group that component’s dominated points by the field’s own learned conditioning (parent level for a binned conditional, regression residuals for a linear edge, the whole fiber for a root marginal) – after the tree has explained what it can, each group must be unimodal. A group that still splits (deterministic 2-means separation over the same
2.65 + 6/sqrt(n)finite-sample threshold the hvis merged-regime detector is calibrated to, minority share >= 20%) is a regime split the component is hiding, whichever family absorbed it.Returns
{"components": [...], "diagnosis": [str, ...]}– emptydiagnosismeans no component hides multimodal structure. Per component,multimodal_fieldslists(field, where, separation)andmixture_factorslists factors the detector fitted as mixture families (supporting detail: where the absorbed structure went). The fix when it fires is more components or pinned unimodalfield_estimators(seelearn_mixture_structure()). Complementary tomixle.utils.hvis.model_fit_health, which audits density calibration and accepts this model directly.
- learn_structure(data, *, field_estimators=None, min_gain=0.0, max_levels=64, n_bins=4, max_its=30, rng=None)[source]
Discover the dependency forest for heterogeneous
dataand return the fitted joint model.Any field can be a parent: a discrete one conditions directly, a continuous one is quantile-binned into
n_binsconditioning levels (so a real can drive a count, a category, or another real). Scores every(parent -> child)pair bydependency_gain(), greedily builds a maximum-gain acyclic forest (each field at most one parent), and fits each factor. Falls back to independent marginals where no dependence clearsmin_gain– never worse than a composite, much better when structure exists. This is “automatic inference for composable models of heterogeneous data” made real.rngseeds every internal fit’s EM initialization;Noneresolves to a FIXED seed, so two calls on the same data return the same model. (Before this knob, fits whose detected family needs a randomized init – a Gaussian-mixture conditional, say – drew fresh OS entropy per call, so the learned model itself was nondeterministic.)