pytyche.experiment.recommendation

Recommendation engine: the per-round NextRoundPlan construction.

The engine is a thin composition over the shipped L2 primitives: the policy tree (already carrying the ε-clipped Thompson allocation_map) comes from posterior.fit_policy_tree, and graduation evidence from posterior.recommendation_summary — nothing here reimplements the allocation or decision rules (design.md §”Recommendation engine”).

Prose templates: every operator-facing phrase the engine emits lives in the “Prose templates” section below — deterministic, no LLM, no timestamps.

Functions

cold_start_plan(treatments, n_visitors)

The round-0 default plan: Control + Explore at 50/50.

detect_dropped_treatments(...[, epsilon, ...])

Treatments whose allocation starved below epsilon everywhere.

next_fit_num_trees_tau(posterior, ...)

The next round's num_trees_tau from this round's evidence.

recommend(analysis, *, history, treatments, ...)

Build the next round's NextRoundPlan from this round's analysis.

Classes

ExpectedLossRule([expected_loss_max, ...])

Default graduation rule: sustained compound-threshold evidence.

GraduationCandidate(treatment, segment, ...)

A (treatment, segment) pair that has met the graduation rule.

GraduationRule(*args, **kwargs)

Decision rule for whether a (treatment, segment) pair is a graduation candidate.

NextRoundPlan(n_visitors, cells, treatments, ...)

The recommendation engine's proposal for the next round.

class pytyche.experiment.recommendation.GraduationRule(*args, **kwargs)[source]

Bases: Protocol

Decision rule for whether a (treatment, segment) pair is a graduation candidate.

history is oldest-first and INCLUDES the round under evaluation as its LAST element — the engine appends a provisional Experiment for the current round before consulting the rule. Default implementation: ExpectedLossRule.

class pytyche.experiment.recommendation.ExpectedLossRule(expected_loss_max=0.005, p_positive_threshold=0.95, p_better_threshold=0.8, sustained_rounds=2)[source]

Bases: object

Default graduation rule: sustained compound-threshold evidence.

A (treatment, segment) pair is a candidate when its per-round decision evidence cleared all three thresholds — expected_loss_comparison < expected_loss_max AND probability_positive > p_positive_threshold AND probability_better > p_better_threshold — in the LAST sustained_rounds consecutive rounds of the history (this round and the sustained_rounds - 1 before it). The evidence is recomputed per round from each history entry’s stored posterior via experiment.posterior.recommendation_summary(treatment, segment=segment) — array math on stored draws, no extra state.

Graduation runs on RAW posterior probabilities in v0.2: the calibration artifact’s correction scope is intervals-only, so the probability and expected-loss inputs here come from raw posterior draws even when a calibration artifact is attached. When the calibration track’s p-curve extension lands, graduation becomes calibrated with no API change here.

The defaults are the canonical thresholds; calibrate to your domain.

Parameters:
  • expected_loss_max (float)

  • p_positive_threshold (float)

  • p_better_threshold (float)

  • sustained_rounds (int)

is_candidate(treatment, segment, history)[source]

Whether the thresholds held in the last sustained_rounds.

Parameters:
Return type:

bool

consecutive_held(treatment, segment, history)[source]

Count of trailing rounds whose evidence cleared the thresholds.

The engine records this on GraduationCandidate — a pair holding five consecutive rounds reports 5, not the rule’s configured minimum.

Parameters:
Return type:

int

class pytyche.experiment.recommendation.GraduationCandidate(treatment, segment, sustained_rounds, latest_recommendation)[source]

Bases: object

A (treatment, segment) pair that has met the graduation rule.

Surfaced as structured data only — the library never auto-graduates; the operator (or an automated workflow) decides whether to ship.

treatment

The treatment name.

segment

The discovered segment where graduation fired.

sustained_rounds

Count of consecutive rounds the rule has held.

latest_recommendation

The current round’s per-(treatment, segment) decision evidence.

Parameters:
class pytyche.experiment.recommendation.NextRoundPlan(n_visitors, cells, treatments, dropped_treatments, graduation_candidates, prose_summary, tree=None)[source]

Bases: object

The recommendation engine’s proposal for the next round.

The engine emits ONE proposed cell structure per round; the operator may accept, partially override, or fully replace before shipping.

n_visitors

schedule.next_round_size(rounds_completed); None when the schedule is exhausted (the final round’s plan has no next round to size).

cells

The recommended cell structure (weights sum to 1.0).

treatments

Treatments active going into the next round.

dropped_treatments

Treatments dropped between this round and the next; disjoint from treatments.

graduation_candidates

(treatment, segment) pairs that met the graduation rule. Candidates’ treatments stay active — no auto-graduation.

prose_summary

Multi-paragraph PM-readable rationale (template-formatted, deterministic).

tree

The round’s policy-tree fit, from which the Optimized cell was built — segments, stability scores, allocation map. None only on the round-0 cold-start plan, where no fit exists yet. Consumers needing the round’s tree (the truth comparison, next-fit num_trees_tau sizing) read it here rather than refitting.

Parameters:
  • n_visitors (int | None)

  • cells (list[Cell])

  • treatments (list[str])

  • dropped_treatments (list[str])

  • graduation_candidates (list[GraduationCandidate])

  • prose_summary (str)

  • tree (PolicyTreeResult | None)

pytyche.experiment.recommendation.cold_start_plan(treatments, n_visitors)[source]

The round-0 default plan: Control + Explore at 50/50.

No posterior exists yet to derive a tree from, so there is no Optimized cell — the Control cell measures the baseline cleanly and the Explore cell gives the first fit uniform-random signal.

Parameters:
  • treatments (list[str]) – The experiment’s full treatments universe (control first).

  • n_visitors (int | None) – The first round’s size per the schedule; None when the schedule offers no rounds.

Return type:

NextRoundPlan

Returns:

The default NextRoundPlan for round 0.

pytyche.experiment.recommendation.next_fit_num_trees_tau(posterior, tree_result, cumulative_n)[source]

The next round’s num_trees_tau from this round’s evidence.

compute_num_trees_tau at its defaults, with d_tau the unique split-feature count of the fitted policy tree (min 1 — a root-only tree still has one effective dimension) and sigma_tau the standard deviation of the posterior-mean CATEs.

Parameters:
  • posterior (HurdleBCFResult) – This round’s hurdle posterior.

  • tree_result (PolicyTreeResult) – This round’s policy-tree fit.

  • cumulative_n (int) – Total visitors across all rounds including the next one being sized.

Return type:

int

Returns:

Tree count for the next fit’s GPUBCFConfig.num_trees_tau.

pytyche.experiment.recommendation.detect_dropped_treatments(allocation_history, treatments, *, epsilon=0.02, sustained_rounds=2)[source]

Treatments whose allocation starved below epsilon everywhere.

A treatment is dropped when its allocation is below epsilon in EVERY segment for the last sustained_rounds consecutive rounds. A name absent from a segment’s weights counts as zero allocation.

Parameters:
  • allocation_history (Sequence[dict[int, dict[str, float]]]) – Per-round allocation maps ({leaf_id: {treatment_name: weight}}), oldest first.

  • treatments (list[str]) – Names to test; the result preserves this order.

  • epsilon (float) – The starvation threshold (the Thompson clip floor).

  • sustained_rounds (int) – Consecutive starved rounds required.

Return type:

list[str]

Returns:

Dropped names in treatments order; empty when the history is shorter than sustained_rounds.

pytyche.experiment.recommendation.recommend(analysis, *, history, treatments, schedule, min_control_weight=0.05, min_explore_weight=0.05, max_segment_depth=3, min_segment_share=0.1, graduation_rule=None, seed=0)[source]

Build the next round’s NextRoundPlan from this round’s analysis.

Composes the shipped L2 primitives: the policy tree and its ε-clipped Thompson allocation come from analysis.posterior.fit_policy_tree(...), graduation evidence from posterior.recommendation_summary(...). The proposed cells are the Control + Explore + Optimized triple at the floor weights, the graduation rule is consulted per (non-control treatment × segment) pair over [*history, <this round>], and treatments starved below the allocation floor for consecutive rounds are dropped from the active list.

Parameters:
  • analysis (AnalysisResult) – This round’s analysis; the posterior rides on analysis.posterior.

  • history (list[Experiment]) – Prior rounds’ experiments, oldest first (this round is NOT included; the engine appends its own provisional view).

  • treatments (list[str]) – The experiment’s full treatments universe (control first).

  • schedule (Schedule) – Sizes the next round via next_round_size(len(history) + 1).

  • min_control_weight (float) – Guaranteed Control-cell share.

  • min_explore_weight (float) – Guaranteed Explore-cell share.

  • max_segment_depth (int) – Policy-tree depth bound.

  • min_segment_share (float) – Minimum per-leaf population share.

  • graduation_rule (GraduationRule | None) – None uses ExpectedLossRule at its defaults.

  • seed (int) – Bootstrap seed for the policy tree’s stability scores.

Return type:

NextRoundPlan

Returns:

The assembled NextRoundPlan.

Raises:

ValueError – When min_control_weight + min_explore_weight >=     1.0, or when the posterior carries no observed data.