Migrating to 0.8.2¶
0.8.2 is a patch release of 0.8.1 with the same public surface. It repairs the findings the ten
adversarial reviews of the 0.8.1 candidate deferred (release-checklists/0.8.2-followups.md,
decision D-0213), and the blocking defects a second ten-pass review found on the 0.8.2 candidate
itself (release-checklists/0.8.2-reviews/, decision D-0217). No documented public API was
renamed or removed; what was added is disclosure and control on existing objects (for example
numerical_repairs(), surrogate_repairs, filtered= and tie_tolerance; the CHANGELOG lists
each), plus internal helper names that are not part of the documented API. Every 0.8.1 pickle, JSON model
and checkpoint loads, with one visible difference: the eight families that now serialize their
constructor parameters (Dirichlet, DirichletProcessMixture, MultivariateStudentT,
ProbabilisticPCA, HierarchicalMixture, JointMixture, GaussianCopula, RVineCopula) have a
different content hash than on 0.8.1. A model_hash recorded for one of them on 0.8.1 – in a
provenance header, a deploy manifest or a registry record – will not match the same model on 0.8.2,
and Model.load of a 0.8.1 deploy artifact for one of them emits its content-hash integrity note.
That note is expected for these eight families, not a sign of tampering; the model itself is
unchanged. What changed otherwise is that a number of inputs 0.8.1 accepted and mis-scored in
silence now raise. Code that never sent those inputs is unaffected; code that did was getting a
wrong answer, and the error names what to change. The full list, by surface:
Univariate estimators refuse observations outside their support¶
ExponentialEstimator, PoissonEstimator, GeometricEstimator, UniformEstimator and
LogSeriesEstimator used to drop a negative, NaN, infinite, or fractional observation from the
sufficient statistics and fit the rest. They now raise ValueError naming the family and its
support. Clean the column (x[np.isfinite(x)], or fix the sign error) before fitting; a
zero-weight row is still exempt. WeibullEstimator and the other clamped estimators report their
shape ceiling, scale floor and rate floor through numerical_repairs() instead of silently.
Scoring is the other half, and it moved the opposite way. Every support-limited family’s encoder now
admits a finite value outside the support and scores it -inf, as the scalar log_density already did, so a
mixture or HMM can encode one batch against every component and the component that owns a row takes
it. On 0.8.1 the encoders of GeneralizedPareto, Nakagami, Rician, Tweedie, Pareto,
NegativeBinomial, BetaBinomial and Bernoulli (among others) refused such a row outright, so a
Mixture[Gaussian, Nakagami] could not even be encoded on data the Gaussian owns. Code that relied on
that encode-time error to catch bad data will now get it at fit time instead, from the accumulator, and
a mixture in which NO component supports a row names how many rows score -inf. Two refusals stay at
encode time on purpose: NaN (and, for most continuous families, an infinity), which marks missing or
malformed evidence rather than an observation, and a non-integer value passed to a count family (Poisson, Geometric, LogSeries, NegativeBinomial, BetaBinomial,
Binomial, Bernoulli, Skellam), which has no support off the integers. An out-of-range integer, such as
a negative count, is admitted and scored -inf.
quantile(q) refuses a q outside [0, 1] on every family¶
Most families used to answer an out-of-domain q: many returned NaN (scipy’s ppf convention),
BernoulliDistribution and BetaBinomialDistribution returned a plausible support point (1.0 for
q = 1.5, 10 for q = 1.1), and HalfNormalDistribution returned a negative number for
q = -0.1. Every family that defines quantile – thirty-one of them – now raises ValueError
naming the family, which is what four discrete families already did. Clamp the level, or fix the
computation that produced it; q = 0 and q = 1 are still answered, with the support’s own bounds.
optimize() refuses inputs it used to fit meaninglessly¶
optimize(enc_data=...)with a zero-row batch, andoptimize(data)on an empty sequence, raise instead of returning an unfitted model marked converged.optimize(delta=0)raises: the test isgain < delta, so zero could never stop. Passdelta=Noneto remove the convergence stop. That is not a guaranteed iteration count: a rejected update (a non-improving or non-finite step) still ends the run, with a note saying how many of themax_itsiterations ran, andfit_provenance().iterationsrecords the count.rng=andprev_estimate=raiseTypeErrorby name when given the wrong type. (So doespotentials=on the probabilistic-programming sampler fits, which is where that argument lives;optimizehas nopotentialsparameter.)A one-shot iterator with
estimator=Noneis materialized and fitted (200 rows fit 200 rows) instead of fitting an emptyIgnoredDistribution.print_iter=withoutout=prints tosys.stdoutinstead of doing nothing. Its default is nowNonerather than1, so a call that does not mention it stays quiet exactly as before;print_iter=0still asks for no periodic lines. Only an explicit value starts printing.The cap note for a run that went downhill under best-seen selection says so, and names the caller’s line; library-internal capped calls no longer warn from inside
distill.A bare
strorbytesraises instead of being fitted as a categorical over its individual characters:optimize('hello world hello')modelled nine characters. Pass a sequence of observations (['hello', 'world', 'hello']).A
datetime64ortimedelta64array raises, naming the dtype and suggestingdata.astype('int64'). On 0.8.1 adatetime64array was fitted as an ignored categorical over its timestamps, and atimedelta64array crashed inside the profiler.The same front door – those refusals, the refusals of a masked array, a
numpy.matrixand a 0-dimensional array, and the normalization of a one-shot iterator, a structured array, a mapping of columns and a DataFrame into rows – now applies to every verb that reads data:Model().fit,Model().evaluate,propose,best_of, thevdata=validation set,learn_structure,learn_bayesian_network,fit_with_provenance,Monitor,Service.score,detect_driftandscore_drift. Several of these read a DataFrame or a mapping as its column NAMES on 0.8.1 –fit_with_provenance(df)fitted a categorical over two labels and recordedn_records=2,detect_drift(model, reference, df)scored the labels, andMonitor.update(df)retrained on them – so results from those calls on a table change, and are now the onesoptimizegives. A DataFrame handed to a record model arrives as rows keyed the way its encoder reads them; a mapping of columns or a structured array handed to a record model is refused on every verb, where the drift verbs used to report DRIFT on two identical batches of it.learn_mixture_structureandmixture_structure_healthread a DataFrame or a mapping as rows too, and the structure learners refuse a sequence of scalars or of dict rows, which they cannot read as fields.
Latent models¶
MixtureDistributionand the HMM families refuse a component that is a likelihood factor rather than a generative law (aSequenceDistributionwith no length model). Passlen_estimator=/len_dist=so every component models everything it emits.An
(n, 1)column array into a univariate estimator or a lookback HMM raises naming the shape andnumpy.ravel;df['x']gives the one-dimensional form.HiddenMarkovModelDistribution.seq_posteriorreturns the documented smoothing marginals on a base install and withterminal_states, where it returnedNone.LookbackHiddenMarkovModelDistribution.seq_posteriorreturns different numbers. It ran the forward-only kernel, so it returned the filteredP(state_t | observations up to t)under the smoothed name; it now returns the smoothing marginalsP(state_t | the whole sequence), like every otherseq_posteriorin the package. The two agree at the last position and differ everywhere else (0.16 on a four-step chain). Passfiltered=Truefor the old values – the flag and its default matchHiddenMarkovModelDistribution.seq_posterior.A terminal-state HMM’s
latent_posterior,viterbi,seq_viterbi,mode(),sample()andposterior_predictivereturn different answers. They answered for a model without the full restriction – including Viterbi paths that visit a terminal state mid-sequence, and paths and samples that END in a non-terminal state, both of which the model gives probability zero. A length-1 sequence is now assigned a terminal state. Models withoutterminal_statesare unaffected.LookbackHiddenMarkovModelDistribution.seq_posteriorraisesNotImplementedErrorwhen the model setsterminal_states, where it silently ran the unrestricted recursion. Atlag=0the same emissions can go toHiddenMarkovModelDistribution, which runs the restricted one.LDAEstimatorreturns the last valid alpha iterate with a warning where it raisedLDAConvergenceErrorout ofoptimize(); the strict solver still raises.Variance floors applied inside mixtures and HMMs are disclosed in
fit_provenance().repairs.The hidden-Markov families start from a symmetry-broken initialization when their emissions are Gaussian or vector-valued. Gamma, Weibull, Beta, Poisson and categorical emissions decline that start and stay bit-identical to 0.8.1, including the slow initialization plateau where they had one: a categorical-emission HMM needs the same iteration budget it did.
Mixture fits warn when a component ends with fewer effective rows than parameters. The note can also fire from an intermediate EM iterate or an initialization subsample and describe a starvation the returned model does not have; check
MixtureDistribution.component_row_masson the returned model before acting on it.
Structure learning, proposals and lifecycle¶
learn_bayesian_networkand the dependency screen raise on an empty corpus or ragged records.propose()raises when the holdout would leave fewer records than a fit needs (holdout=or more rows), and names multimodal fields with the$['key']spelling.Registry, checkpoint and deploy paths refuse a non-finite header value, a corrupt version file, and an invalid
trust_codeflag before touching data.Service(keep=...)requires a positive integer, andRegistry.checkpointer(every=...)likewise, withresume=required to be a bool. Akeeporeveryof0or a negative number asked for a buffer or a cadence that cannot exist.Monitor(...)validates its thresholds at construction, with the same rulesdetect_driftapplies:loglik_shift_thresholdmust be<= 0(it is how far the mean log-likelihood may FALL, so a positive one flags drift on identical data) andpsi_thresholdmust be non-negative. Both used to construct cleanly and raise from everycheck()andupdate()afterwards, naming a constructor argument from a method that does not take one.
Seeds derived from a prompt¶
mixle.task derives a per-prompt seed from a base seed. A prompt whose value has no canonical
encoding (a custom class, say) now seeds from its repr, which is the fallback the API documented
all along; it used to seed from the literal string "<base_seed>:None", so every such prompt in
a run shared one seed. Draws recorded on 0.8.1 for those prompts will not reproduce – they were
never per-prompt draws. The same applies to mixle models and posteriors used as prompts: on 0.8.1
they had no canonical encoding and fell into that shared seed; on 0.8.2 they seed from their
canonical key, so their draws differ from 0.8.1’s. Prompts with a canonical encoding on both
versions (str, bytes, numbers, None, and containers of those) are unaffected and seed exactly as
before.
Bayesian optimization discloses a repaired surrogate¶
BayesOptResult gained numerical_repairs() (and the surrogate_repairs field behind it), which
reports any covariance ridging the Gaussian-process surrogate needed during the run. It is () when
none was needed, so reading it is always safe. Nothing else about the result changed, and the field
is keyword-only with a default, so existing construction of a BayesOptResult is unaffected.
Compute kernels¶
NumbaKernelFactory.build raises KernelCapabilityDeclinedError when numba is not installed
(the [numba] extra) instead of running an interpreted kernel under the numba name. Ask for
GeneratedNumbaKernelFactory to fall back to the generic kernel, or install the extra.
Probabilistic programs and surrogates¶
.fit()on a program whose parameters are all constants warns instead of returningresult=NoneSILENTLY – the return is unchanged, and the warning is the repair; an unfitted vectorfree()raises the same message a scalar one does.TransformDistribution.mean()/.variance()are exact for an affine transform and raiseNotImplementedErrorfor a transform with no closed form, instead of returning a sampled number labelled exact.RandomVariable.mean/.varstill answer for any transform: they catch that refusal and fall back to sampling, which is deliberate and is what the regression test pins.The GP surrogate escalates its jitter and reports it; a matrix no jitter can rescue raises a named error instead of a raw torch
_LinAlgError.A sampler that accepted none of its proposals is refused rather than returned as a fit.
A sampler or Laplace fit (
how='mcmc'|'nuts'|'hmc'|'ensemble'|'laplace') is refused when the model has parameters that NO prior and no data pin: morefreeparameters without a prior than there are numbers in the data.how='map'is not guarded and still fits such a model, returning the degenerate point estimate – forNormal(free, free)on one observation, a scale at its floor. Parameters carrying a proper prior are not counted – a one-observation Bayesian update is fitted as before – and a row of a vector model counts as the scalars it carries, not as one observation. Previously such a model wandered to scales that overflowed double precision and ended in anOverflowErrorraised from inside the lowering table. The guard sits in the numerical-target builder, so the defaulthow='auto'route (which resolves to closed-form EM for an all-free model) andhow='vi'do not pass through it and still fit.how='ensemble'returns different posteriors at its default budget, and takes longer. Itsburndefault is nowNone, resolved tomax(1500, 300 * d)sweeps for ad-parameter model; the old fixed 500 did not contract the walkers’ deliberately overdispersed start, and on ordinary 5- and 18-parameter models the returned posteriors were several posterior standard deviations off and several times too wide, with no warning. The walkers also start differently: around the log-target’s maximum over the location parameters, instead of every location at one shared data mean, which left a model whose location parameters sit far apart 141 posterior sds off at any burn-in.how='sample'dispatches to the ensemble for small models, so its answers change the same way. An explicitburn=is still honoured as given, so a call that passes a short one keeps that behaviour: runchains=2or more and read the R-hat when the budget is not the default.predict()on a posterior restored from a pickle raises instead of silently sampling the plug-in (posterior-mean) model. That now includes the conjugate route, the default for a conjugate model: on 0.8.1 a conjugate fit could not be pickled at all.