Fitting Heterogeneous Records¶
This tutorial fits a mixture over records shaped like:
(category, real_value, variable_length_count_sequence)
That one observation shape contains three different supports. mixle handles
it by making the model a composition of three estimators.
The point of the example is not the particular families. The point is the shape rule: one observation is one Python value, and the estimator should have the same structure as that value.
1. Import the pieces¶
from mixle.inference import optimize
from mixle.stats import (
CategoricalEstimator,
CompositeEstimator,
GaussianEstimator,
MixtureEstimator,
PoissonEstimator,
SequenceEstimator,
)
2. Prepare data¶
The records below are intentionally small, but they show the shape:
data = [
("a", -0.4, [5, 7]),
("b", 4.9, [11, 9]),
("a", 0.2, [6, 5, 4]),
("b", 5.3, [10, 12, 11]),
("a", -1.1, [4, 6]),
("b", 4.5, [9, 10]),
("a", 0.7, [5, 5]),
("b", 5.1, [12, 8]),
]
3. Mirror the data shape with estimators¶
CompositeEstimator means “one observation is a tuple.” Its children are
matched position by position.
component = CompositeEstimator(
(
CategoricalEstimator(),
GaussianEstimator(),
SequenceEstimator(PoissonEstimator(), len_estimator=CategoricalEstimator()),
)
)
The third field is a sequence of counts. SequenceEstimator fits the element
distribution and, when supplied, a distribution over sequence length.
4. Add latent structure¶
Wrap two copies of the component in MixtureEstimator:
estimator = MixtureEstimator([component, component])
model = optimize(data, estimator, max_its=20, out=None)
The fitted object is a MixtureDistribution. Each component is a
CompositeDistribution with the same three-child structure.
Mixtures can have local optima. For a real analysis, run several random starts
with mixle.inference.best_of() or pass a validation set to the fitting
workflow before interpreting the components.
5. Query the fitted model¶
score = model.log_density(("a", 0.0, [5, 6]))
samples = model.sampler(seed=0).sample(3)
log_density returns one joint score for the whole record. Low probability
can come from the category, the real value, the count sequence, the sequence
length, or the mixture assignment implied by the record.
6. Inspect posterior responsibility¶
For latent models, inspect responsibilities before naming clusters.
responsibilities = model.posterior(data)
print(responsibilities[:3])
High responsibility for one component means the row is strongly associated with that latent type under the fitted model. Ambiguous rows are often more useful than the obvious ones when deciding whether the component structure is scientifically meaningful.
7. Use dictionaries when fields are named¶
Tuple position is compact, but production records usually have names. Use
RecordEstimator for dictionary-shaped observations:
from mixle.stats import RecordEstimator, field
named = RecordEstimator(
(
field("category", CategoricalEstimator()),
field("value", GaussianEstimator()),
field(
"counts",
SequenceEstimator(PoissonEstimator(), len_estimator=CategoricalEstimator()),
),
)
)
The same fitting route applies; only the observation shape changes.
What to change next¶
Replace
CompositeEstimatorwithRecordEstimatorif your observations are dictionaries.Replace
GaussianEstimatorwith another scalar family if the real-valued field has skew, tails, or bounded support.Use
mixle.inference.best_of()for more robust mixture fitting.Pass
backend="mp"or an engine when the data is large enough to justify parallel work.Use Capabilities And Contracts before relying on enumeration, conditioning, or exact posterior behavior.