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 CompositeEstimator with RecordEstimator if your observations are dictionaries.

  • Replace GaussianEstimator with 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.