mixle.inference.bayesian_network module

Heterogeneous Bayesian network learning – a directed graph over mixed-type fields with parametric edges.

This deepens mixle.inference.structure (a single-parent forest with quantile-binned continuous parents) into the real thing: a DAG where a field may have several parents, and continuous dependence is a parametric conditional, not a binning. A continuous child is a conditional-linear-Gaussian node – child ~ N(w . [continuous parents, one-hot(discrete parents)] + b, sigma^2) – so a real driven by two reals, or by a category and a real, is modeled exactly and cheaply (closed-form least squares). A discrete/count child with all-discrete parents conditions on their joint configuration (marginal backoff for unseen configs); with a continuous driver it becomes a GLM node (logistic / Poisson log-link / multinomial softmax), so category<->real dependence is representable in BOTH orientations and BIC picks the cheaper one.

learn_bayesian_network grows each node’s parent set greedily by description-length gain, up to max_parents, keeping the graph acyclic. The result scores, samples, and composes like any mixle distribution – the moat: automatic discovery and fitting of a heterogeneous graphical model across arbitrary families, which no mainstream tool does (Stan/PyMC: you write it; sklearn/pomegranate: independence; bnlearn/pgmpy: discrete-or-Gaussian).

class HeterogeneousBayesianNetwork(factors)[source]

Bases: FitProvenanceCarrier

A DAG joint over a heterogeneous record: log p(x) = sum_i log P(x_i | parents(i)) over fitted factors.

Carries the fitting receipt like every other public fitting result. This is what optimize(data) and fit(data) return by default, and it was the one such result that could not say how it had been fitted at all – fit_provenance() and numerical_repairs() did not exist on it (P03-F01), even though the sibling structure learner already carried both.

Parameters:

factors (Sequence[Any])

edges()[source]

Return DAG edges as (parent_field, child_field) pairs.

Return type:

list[tuple[int, int]]

describe(field_names=None)[source]

A real, structured explanation of what the fit found – every field’s per-factor report (_MarginalFactor.describe() and friends: fitted regression coefficients, GLM weights, the conditional table, or the marginal summary), split into edges (fields with >=1 parent) and roots (independent fields). Pass field_names (e.g. the tuple position -> column name map) to label everything by name instead of integer index. Every value is read off this network’s own fitted parameters – nothing here is a placeholder or a re-derived approximation.

Parameters:

field_names (Sequence[str] | None)

Return type:

dict[str, Any]

log_density(x)[source]

Evaluate the joint log density of one record.

The record’s width is checked against the network’s: a short record used to raise a bare IndexError: tuple index out of range and a long one was scored on its first fields with the extras ignored, so a five-tuple got a plausible number from a four-field net (P02-F07). A field the factors cannot score (a NaN in a Gaussian factor, say) makes the whole record impossible rather than NaN, which is what every other law in the package returns for evidence outside its support.

Parameters:

x (tuple)

Return type:

float

seq_log_density(encoded)[source]

Evaluate joint log density for encoded records.

A record the factors cannot score is impossible here for the same reason it is impossible in log_density(), and is reported the same way: -inf, not NaN. The scalar route already did this and the vectorized one did not, so the same NaN field read as an impossible record one way and poisoned the sum, mean or comparison of a whole batch the other (R02-F09). A condition disclosed differently by route is not disclosed.

Parameters:

encoded (Any)

Return type:

ndarray

dist_to_encoder()[source]

Return the encoder for record batches consumed by seq_log_density.

Return type:

Any

sampler(seed=None)[source]

Return a sampler for the fitted Bayesian network.

Parameters:

seed (int | None)

Return type:

Any

class MixtureOfBayesianNetworks(components, weights)[source]

Bases: object

A latent mixture whose components each carry their own heterogeneous DAG (regression edges and all).

The fullest form of the moat: it discovers the clustering and each cluster’s cross-field graphical model, so the slope of one field on another (or the whole dependency graph) can differ between clusters – which a single network cannot represent. log p(x) = logsumexp_k(log w_k + log p_k(x)) over HeterogeneousBayesianNetwork components. Fit by learn_mixture_bayesian_network().

Parameters:
  • components (Sequence[HeterogeneousBayesianNetwork])

  • weights (Sequence[float])

log_density(x)[source]

Evaluate the mixture joint log density of one record.

Parameters:

x (tuple)

Return type:

float

seq_log_density(encoded)[source]

Evaluate mixture joint log density for encoded records.

Parameters:

encoded (Any)

Return type:

ndarray

dist_to_encoder()[source]

Return the record encoder shared by all mixture components.

Return type:

Any

responsibilities(data)[source]

Return posterior component probabilities for each record.

Parameters:

data (Sequence[tuple])

Return type:

ndarray

sampler(seed=None)[source]

Return a sampler for the mixture of Bayesian networks.

Parameters:

seed (int | None)

Return type:

Any

property n_components: int

Return the number of mixture components.

learn_mixture_bayesian_network(data, n_components, *, restarts=3, max_iter=12, seed=0, max_parents=2, min_gain=0.0, max_its=30, em='hard')[source]

Fit a MixtureOfBayesianNetworks by EM: discover clusters and each cluster’s DAG.

em="hard" (default): each iteration re-learns a network per cluster on its assigned points and reassigns every record to its most-probable cluster, until assignments stabilize. em="soft": proper EM – every record contributes to EVERY cluster with its responsibility, each component’s structure search and factor fits are responsibility-weighted (learn_bayesian_network(weights=)), and convergence is on the observed-data log-likelihood; boundary points shape both clusters instead of whipsawing between them. Both run from the same restarts (k-means-seeded + random); best final log-likelihood wins. Starved clusters are re-seeded (hard) / responsibility-floored (soft).

Parameters:
Return type:

MixtureOfBayesianNetworks

bayesian_network_bic(model, data)[source]

BIC (lower is better) for a fitted network or mixture: -2 LL + n_params log n.

Parameters:
Return type:

float

select_mixture_components(data, k_values=(1, 2, 3, 4), *, em='hard', seed=0, **kwargs)[source]

Model selection over the number of clusters by BIC.

Fits learn_bayesian_network() for k=1 and learn_mixture_bayesian_network() for each larger k, scores each by bayesian_network_bic(), and returns (best_model, report) where report = {"k": chosen, "bic": {k: score, ...}}. BIC’s n_params log n penalty is what stops a mixture of per-cluster DAGs from always preferring more clusters.

Parameters:
Return type:

tuple[Any, dict[str, Any]]

learn_bayesian_network(data, *, max_parents=2, min_gain=0.0, max_its=30, weights=None, _routing=None)[source]

Discover a heterogeneous DAG for data and return the fitted network.

Each field greedily gains up to max_parents parents by BIC-penalized conditional likelihood, keeping the graph acyclic. Continuous children become conditional-linear-Gaussian factors (regression on continuous + one-hot discrete parents); discrete/count children condition on the joint config of their discrete parents, or become GLM nodes when a driver is continuous. With weights (soft-EM responsibilities), every factor fit and the BIC search itself are responsibility-weighted, with effective sample size sum(weights).

A field the conditional-linear-Gaussian machinery cannot read as a real number – a string column with more than 64 distinct values, or a numeric column carrying missing values – is modeled by the same automatic detector mixle.utils.automatic.get_estimator() uses (a categorical/ignored leaf, or an Optional-wrapped one) and takes no part in the edge search; every other field still gets the full search. Such a column used to be handed a Gaussian template and crash inside the Gaussian encoder. See _column_routing(), which makes that decision; _routing is a private hook letting a caller that fits many components on SLICES of one dataset decide it once on the whole dataset instead.

Parameters:
Return type:

HeterogeneousBayesianNetwork