mixle.stats.combinator.backoff module¶
Backoff: reserve a small share of mass for outcomes a fitted support cannot represent.
A distribution fitted on observed data can only score what it observed. A bounded family is the sharp
case – IntegerCategoricalDistribution fitted on a training split gives -inf to any integer
outside (or inside a hole of) that split’s support, and one such row drives a whole held-out mean
log-density to -inf.
BackoffDistribution is the composable answer: a two-component mixture of a base (the sharp
fitted model) and a fallback whose support covers the outcomes base cannot reach, with the
mixing weight pinned small rather than freely estimated:
log p(x) = logaddexp( log(1 - w) + log p_base(x), log w + log p_fallback(x) )
The pin is the whole point, and it is what separates this from MixtureEstimator over the same two
components. Free EM on a categorical-plus-count pair is model selection, not smoothing: on 400 Poisson
draws it settles near [0.46, 0.54] and the fallback swallows the detail the base was there to
capture. max_escape_weight bounds that, so the base keeps explaining the data it can explain and
the fallback only covers the tail.
Two things the caller owns:
The fallback sets the tail. It must have support wherever
basedoes not. APoissonDistributionfallback scores a far-out integer at roughly-74because its tail decays factorially;NegativeBinomialDistributionor a broad bounded-uniform is usually the better choice. Whatever you pass is what the tail behaves like.This needs a fallback family to exist. That holds for integers. It does not hold for arbitrary string labels – there is no natural distribution over unseen strings – so
CategoricalDistribution.default_valueremains the right tool there, and this combinator is not a replacement for it.IntegerCategoricalDistributionhas no such knob – it was removed in MXR-080-1839 because a constant out-of-support mass over an unbounded integer support diverges – which is exactly why integers get this combinator instead.A mixture of factors is a factor, not a law. Several mixle families deliberately admit unnormalized weight vectors –
CategoricalDistributionand its integer counterpart both document that – and this class cannot detect it: there is no general “are you normalized?” query to ask a child. SoBackoffDistributioninherits its children’s status rather than conferring one, and wrapping a factor does not make it exactly generative however proper the mixing weights are (MXR-080-1844).sampler()in particular is only meaningful when both children are true laws. Pass normalized components if you need the result to be one.
Usage (MXR-080-1854):
from mixle.stats import BackoffDistribution, BackoffEstimator
from mixle.stats import IntegerCategoricalEstimator, PoissonDistribution, PoissonEstimator
# score-time: a sharp support plus a fallback that covers what it never saw
scorer = BackoffDistribution(sharp_fit, PoissonDistribution(16.0), escape_weight=0.01)
scorer.log_density(84) # finite even though sharp_fit alone gives -inf
# fit-time: escape_weight is a FLOOR, max_escape_weight a ceiling
est = BackoffEstimator(
IntegerCategoricalEstimator(min_val=0, max_val=99),
PoissonEstimator(),
escape_weight=0.01, # never estimated below this; zero is absorbing
max_escape_weight=0.05, # above this it becomes model selection, not smoothing
)
Set max_escape_weight equal to escape_weight to freeze the weight, or to None to let EM
choose freely – which is model selection between the two components and usually not what you want.
- class BackoffDistribution(base, fallback, escape_weight=DEFAULT_ESCAPE_WEIGHT, name=None, keys=None)[source]
Bases:
SequenceEncodableProbabilityDistributionA sharp
basemixed with a broadfallbackunder a pinned escape weight.- Parameters:
- compute_declaration()[source]
Declare the backoff’s support as the support its two children agree on.
A backoff mixes a sharp
basewith a broadfallbackover the SAME sample space, so when both children declare the same support the mixture has it too. Declaring nothing made the composite unusable wherever a caller has to prove what a distribution ranges over:SequenceDistributionrefuses a length model that cannot “prove support on non-negative integers”, andmixle.utils.automaticbuilds exactly this shape – a backoff length model over sequence records – so the automatic path produced a model the library then rejected.Children that disagree (or do not declare) leave the support unstated, which is the honest answer rather than a guess.
- density_semantics()[source]
Join both children’s semantics – a backoff is only exact when everything it mixes is.
The base-class default returns
EXACT, which is a claim about the whole mixture. A backoff over a child explicitly markedLIKELIHOOD_FACTOR(a categorical used as an unnormalized factor, say) inherits that child’s missing normalizer: the escape-weighted sum of an exact law and an unnormalized factor is itself unnormalized. ReportingEXACTthere tells a caller a density integrates to one when it does not.
- log_density(x)[source]
Return the mixture log-density at
x.Finite whenever either component can score
x, which is the reason this class exists: the base may return-inffor an unobserved outcome and the total stays finite.
- component_log_densities(x)[source]
Return the two weighted component log-density vectors for encoded data
x.
- seq_log_density(x)[source]
Vectorized mixture log-density over sequence-encoded observations.
- sampler(seed=None)[source]
Return a sampler that draws the component first, then the value.
- Parameters:
seed (int | None)
- Return type:
BackoffSampler
- estimator(pseudo_count=None)[source]
Return an estimator for this backoff structure, reusing the children’s own estimators.
- Parameters:
pseudo_count (float | None)
- Return type:
BackoffEstimator
- dist_to_encoder()[source]
Return an encoder that encodes each observation for both components.
- Return type:
BackoffDataEncoder
- class BackoffSampler(dist, seed=None)[source]
Bases:
DistributionSamplerSampler for
BackoffDistribution.- Parameters:
dist (BackoffDistribution)
seed (int | None)
- sample(size=None, *, batched=True)[source]
Draw
sizeobservations (or one whensizeis None).sizemust be an exact non-negative integer. Bareint(size)silently accepted every near-miss as a request:2.7became two draws,Truebecame one, and-3became an empty result that reads like a successful zero-row sample. Each of those is a caller mistake producing a differently-sized dataset than the one asked for.
- class BackoffAccumulator(base_accumulator, fallback_accumulator, keys=None)[source]
Bases:
SequenceEncodableStatisticAccumulatorAccumulates child statistics weighted by EM responsibilities, plus the escape counts.
- Parameters:
base_accumulator (SequenceEncodableStatisticAccumulator)
fallback_accumulator (SequenceEncodableStatisticAccumulator)
keys (str | None)
- update(x, weight, estimate)[source]
Accumulate one raw observation.
- seq_update(x, weights, estimate)[source]
Accumulate a sequence-encoded batch.
- initialize(x, weight, rng)[source]
Cold start: give both components the full observation, and no escape evidence yet.
The escape weight starts at the estimator’s pin rather than being inferred from an initialization that has no responsibilities to work with.
- Parameters:
x (Any)
weight (float)
rng (RandomState | None)
- Return type:
None
- seq_initialize(x, weights, rng)[source]
Vectorized
initialize().- Parameters:
weights (ndarray)
rng (RandomState | None)
- Return type:
None
- combine(suff_stat)[source]
Merge another accumulator’s value into this one.
- from_value(x)[source]
Replace this accumulator’s state with
x.
- scale(c)[source]
Scale every accumulated statistic by
c.- Parameters:
c (float)
- Return type:
BackoffAccumulator
- key_merge(stats_dict)[source]
Merge keyed statistics, delegating to both children.
- key_replace(stats_dict)[source]
Adopt keyed statistics, delegating to both children.
- acc_to_encoder()[source]
Return the encoder matching this accumulator’s children.
- Return type:
BackoffDataEncoder
- class BackoffAccumulatorFactory(base_factory, fallback_factory, keys=None)[source]
Bases:
StatisticAccumulatorFactoryFactory for
BackoffAccumulator.- Parameters:
base_factory (StatisticAccumulatorFactory)
fallback_factory (StatisticAccumulatorFactory)
keys (str | None)
- make()[source]
Return a fresh backoff accumulator.
- Return type:
BackoffAccumulator
- class BackoffEstimator(base_estimator, fallback_estimator, escape_weight=DEFAULT_ESCAPE_WEIGHT, max_escape_weight=DEFAULT_MAX_ESCAPE_WEIGHT, name=None, keys=None)[source]
Bases:
ParameterEstimatorEstimator for
BackoffDistributionwith a bounded escape weight.- Parameters:
- accumulator_factory()[source]
Return the matching accumulator factory.
- Return type:
BackoffAccumulatorFactory
- class BackoffDataEncoder(base_encoder, fallback_encoder)[source]
Bases:
DataSequenceEncoderEncodes each observation for both components.
- Parameters:
base_encoder (DataSequenceEncoder)
fallback_encoder (DataSequenceEncoder)
- seq_encode(x)[source]
Encode
xonce per component.Both components see every observation – unlike the zero-inflated/hurdle encoders, there is no mask here, because a backoff does not partition the data. Which component explains a row is a posterior quantity settled at fit time, not a property of the row.
The row count travels with the encoding so scoring can check its answer against the input it was asked about. Requiring only that the two children AGREE was not enough: both can drop the same row and return a plausible shorter vector, which then silently re-aligns every score with the wrong observation (MXR-080-1843).