mixle.evolve.population module¶
The meta-search that learns which improvement operators help: a bandit + a diversity population.
The operator-choice problem is a non-stationary bandit: each step, pick an operator (arm), apply it through the propose-and-verify gate, observe the verified gate delta as reward (0 if the challenger was rejected), and update the arm’s value. Because the reward is the anti-regression verified delta rather than an in-sample fit score, the policy cannot be fooled by a challenger overfitting the data it was FIT on.
It can still be fooled by a subtler failure: adaptive reuse of the data it is VERIFIED on. A
Population run queries the SAME reusable validation set every single generation, and both
survivor selection and OperatorBandit adapt to what they saw there – exactly the setting
where a held-out set stops being a fair test the more times you look at it and act on what you saw
(the same reason a test set quietly becomes a training signal if you tune against it repeatedly). See
Population’s docstring (“Data roles”, MXR-080-0042) for the three roles this module keeps
separate – fit data, the reused validation signal (a heuristic, not evidence), and the genuine
one-shot final holdout (Population.evaluate_holdout()).
OperatorBandit– Thompson or UCB over a fixed operator pool. Reward is the verified delta from that reused validation signal (see above); cost is tracked for a report. Non-stationary: a forgetting factor decays stale arm statistics so the policy can follow a problem whose best operator changes over the run.Population– a diversity-preserving population of model structures evolved by the bandit: select operators, apply them to parents, gate the challengers, reward the bandit, and keep the verified-best plus a coarse capability-diversity quota.runreturns aSearchResult;championis the incumbent.
- class OperatorBandit(operators, *, policy='thompson', decay=0.97, prior_cost_aware=True, seed=0)[source]
Bases:
objectA non-stationary bandit over a fixed pool of
ImprovementOperator.select(k)returns thekhighest-value operators under the chosen policy;rewardfolds a verified delta + cost back into the chosen arm;reportis the “which operators help” artifact.- Policies:
'thompson'– Beta-Bernoulli Thompson sampling on the win indicator (reward > 0), scaled by the mean positive reward, so an operator that wins rarely but big and one that wins often but small are compared on expected verified delta.'ucb'– UCB1 on the mean reward with asqrt(2 ln N / n)exploration bonus.
Non-stationarity: each
rewardfirst multiplies every arm’s statistics bydecay(a forgetting factor in(0, 1]), so old evidence fades and the policy can track a shifting best operator.- Parameters:
- value(name)[source]
The current policy value of operator
name(a Thompson draw or the UCB index).
- select(k=1)[source]
Return the
koperators with the highest policy value (a fresh Thompson draw each call).
- reward(op_name, delta, cost)[source]
Fold a verified
delta(0 if the challenger was rejected) andcostinto the arm.Decays every arm first (non-stationarity), then updates the chosen arm. A
deltais clipped at 0 below – a rejected challenger is a zero reward, never negative, matching the anti-regression guarantee (we never punish an operator for a rejected proposal beyond not rewarding it).deltaandcostmust be finite (andcostnon-negative).max(nan, 0.0)returnsnan, so a single non-finite update used to poison the arm permanently: mean delta, mean cost, and every subsequent Thompson draw for it came backnan, and ananpolicy value sorts unpredictably against real ones. Policy state is not a place to absorb an invalid measurement.
- class Population(seeds, *, objective, operators=None, bandit=None, size=12, diversity_quota=2, holdout=0.25, seed=0)[source]
Bases:
objectA diversity-preserving population of model structures, evolved by the
OperatorBandit.seedsare fitted models (the starting structures). Eachstep()selects operators via the bandit, applies them to parents chosen by fitness, gates the challengers with the Phase-1 champion/challenger rule – pooling the whole generation’s p-values for a single Benjamini-Hochberg correction before the per-candidate gate, since a generation produces many challengers at once – rewards the bandit with the verified deltas, and keeps the verified-best plus a coarse capability-diversity quota.Data roles (MXR-080-0042). This class keeps three distinct data roles separate:
Fit data – what operators actually fit challengers on (
datainstep()/run(), or its auto-derived fit-split whenverify_datais omitted – see below).Reusable validation data (
verify_data) – read bystep()every single generation, to gate every challenger (challenger_beats_champion()) AND to score population fitness / drive survivor selection. When omitted,step()/run()derive a genuinely disjoint split fromdata(_auto_split()) rather than the old default of reusingdataitself verbatim – but disjointness from the fit data is only half the fix. This SAMEverify_datais then queried again, adaptively, by EVERY generation of arun(): survivor selection keeps whoever currently scores best against it, andOperatorBanditlearns from whichever operators currently win against it. The per-generation Benjamini-Hochberg pool (seestep()) only controls multiplicity WITHIN one generation’s simultaneous comparisons; it does nothing to correct for that same set being re-queried, and acted on, by every OTHER generation – the textbook way a held-out set stops being a fair test once you repeatedly tune against it. Treat everyVerdict/score this class computes against a reusedverify_dataas a NOISY HEURISTIC that steers the search, never as a promotion-grade statistical claim on its own.One-shot final holdout – data never passed to this population as
dataorverify_dataat any point, evaluated exactly ONCE viaevaluate_holdout()after every intendedstep()/run()call is done. This is the only comparison this class makes that carries real evidentiary weight – a caller that needs a defensible promotion decision (as opposed to search guidance) must use it rather than trustingchampionor aGenerationReportalone.
- Parameters:
seeds (Sequence[Any]) – the initial fitted models (at least one).
objective (Objective) – the
Objectiveto optimize (lower-is-better aware).operators (Sequence[ImprovementOperator] | None) – the proposal-move pool; defaults to the Phase-1 safe set.
bandit (OperatorBandit | None) – an
OperatorBanditoveroperators(built with the default policy if omitted).size (int) – the carrying capacity of the population.
diversity_quota (int) – how many of
sizeslots are reserved for capability-diverse members (the rest go to the fittest); the quota keeps the search from collapsing onto one structure too early.holdout (float) – fraction of
datareserved for the auto-derivedverify_datasplit (_auto_split()) whenever a caller omitsverify_data– irrelevant if the caller always passes an explicit, already-disjointverify_data.seed (int) – RNG seed for parent sampling, the bandit, and the auto-derived split.
- step(data, verify_data=None)[source]
Run one generation: select -> propose -> gate -> reward -> survivor selection.
- Parameters:
data (Any) – the pool operators FIT candidates from (data role 1, “fit data”, in the class docstring). When
verify_datais omitted,datais split (_auto_split()) and only the fit-half is actually fit on – the other half becomes the auto-derivedverify_databelow, so a challenger is never fit and scored on the same rows.verify_data (Any | None) – the held-out data used to score population fitness and to gate every challenger against its parent (
challenger_beats_champion()) – data role 2, “reusable validation data”, in the class docstring: read again every generation, and its repeated, adaptive use across arun()is a HEURISTIC search signal, not evidence on its own – seeevaluate_holdout()for the genuine one-shot check. Defaults to the verify-half of_auto_split()applied todatawhen omitted (neverdataitself – MXR-080-0042: the old default silently made verification equal training data). Pass an explicit, already-disjoint split here (e.g.search()does) to control the split yourself instead.
- Return type:
GenerationReport
A generation proposes many challengers at once, so the gate applies a genuine population-wide Benjamini-Hochberg correction: every candidate is first compared on its own RAW p-value (pass 1 below), the whole generation’s raw p-values are then pooled and corrected together in ONE
adjust_pvalues()call (pass 2), and only then is each candidate’s promotion finalized against its adjusted p-value (pass 3). Correcting each candidate’s single p-value in isolation – family size 1 – is the identity transform for every method inmultiple_testing, which is whychallenger_beats_champion()refuses to do that itself. This pooled correction operates WITHIN one generation only – it does not, and cannot, correct for the sameverify_databeing queried again by every OTHER generation of arun()(see the class docstring’s “Data roles” note andevaluate_holdout()).A scalar-only
self.objective(calibration_objective(),decision_regret_objective()) has no paired test to run at all – every candidate’sVerdictthen carriesp_value = nanby design (see that function’s scalar-only branch) – so pass 2 excludes non-finite p-values from the pooled family entirely rather than handing them toadjust_pvalues(which rejects non-finite input outright); such a candidate’s own unadjustedverdict.promotestands as its final promotion decision, since there is no p-value for a population-wide correction to act on – which, perpromote, is now ALWAYSFalsefor a scalar-only objective regardless offavored(no sampling-uncertainty estimate backs a bare scalar delta – seehas_statistical_evidence).
- run(data, verify_data=None, generations=5)[source]
Evolve for
generationssteps; return aSearchResult.- Parameters:
data (Any) – the pool operators fit candidates from each generation (data role 1, “fit data”, in the class docstring). When
verify_datais omitted,datais split ONCE, up front (_auto_split()), and only the fit-half is fit on for every generation of this run.verify_data (Any | None) – the held-out data used to score fitness and gate every challenger – data role 2, “reusable validation data” (see the class docstring’s “Data roles” note): the SAME split is reused for EVERY generation, and both survivor selection and
OperatorBanditadapt to it, which is exactly the sequential/adaptive reuse the class docstring warns is a heuristic search signal, not evidence. Defaults to the verify-half of_auto_split()applied todatawhen omitted (neverdataitself – MXR-080-0042). Pass an explicit, already-disjoint split for a caller-controlled gate (e.g.search()always does).generations (int) – number of
step()calls.
- Return type:
The returned
best_modelis the run incumbent, guaranteed no worse than the best seed on whatever ``verify_data`` this run used (anti-regression is relative to that reused validation signal, not an unconditional guarantee about unseen data – seeevaluate_holdout()for the one-shot check that actually earns that stronger claim).historyis one row per generation (proposals / verified / score).
- evaluate_holdout(holdout_data, *, reference=None, **verdict_kwargs)[source]
The one genuinely evidence-bearing check this class can make (data role 3, “one-shot final holdout”, in the class docstring) – call this EXACTLY ONCE, after every
step()/run()call you intend to make is already done, onholdout_datathat was never passed to this population asdataorverify_dataat any point.Every
Verdict/ scorestep()computes internally along the way is checked against the SAME reusedverify_data, generation after generation – a heuristic search signal, not a defensible promotion claim (see the class docstring’s “Data roles” note). This method is the genuine, single-look alternative: ONEchallenger_beats_champion()comparison betweenreference(defaults to this population’s incumbent BEFORE its firststep()/run()call – i.e. the best seed) andchampion(the current incumbent).Calling this more than once against the same
holdout_datareintroduces exactly the adaptive-reuse problem it exists to avoid – treatholdout_dataas spent after one call, the same as any other held-out test set.- Parameters:
holdout_data (Any) – fresh data this population has never scored anything on. Verifying that is the caller’s responsibility –
Populationhas no way to check it from the object alone.reference (Any | None) – the champion to compare
championagainst; defaults to the pre-run incumbent (see above). Pass the currently-deployed production model explicitly if that differs.verdict_kwargs (Any) – forwarded to
challenger_beats_champion()(e.g.alpha,min_effect,seed);nonnesteddefaults to whetherreferenceandchampionare different model types, but can be overridden.
- Returns:
The one-shot
Verdict;verdict.promoteis the genuine, evidence-backed promotion decision.- Return type:
Verdict
- property champion: Any
The current run incumbent (the best model seen, anti-regression guaranteed).