pytyche.analysis.thompson_allocation

pytyche.analysis.thompson_allocation(posterior, segments, epsilon=0.02)[source]

Per-segment traffic split: each arm’s weight is the posterior probability that it is the segment’s best arm.

This is Thompson sampling at segment granularity. Each posterior draw casts one vote per segment: the draw’s treatment-vs-control contrasts are averaged over the segment’s members, and the vote goes to the treatment with the largest contrast — or to control, when no contrast is positive. An arm’s weight is the fraction of draws it wins. Where the posterior is confident, traffic concentrates on the winner; where arms are still close, traffic stays spread across the contenders, so the next round collects evidence exactly where the decision is open.

Parameters:
  • posterior (HurdleBCFResult | ContinuousBCFResult | BinaryBCFResult) – One of the three posterior result types, carrying observed data (raises otherwise).

  • segments (Sequence[DiscoveredSegment]) – Segments to allocate over — typically fit_policy_tree(...).segments. Only id and rule are consumed. Membership is each segment’s rule applied to the concatenated visitor rows (all of variants[0]’s rows, then variants[1]’s, and so on — the same row order every per-visitor sample array uses).

  • epsilon (float) – Safety-net exploration floor: arms whose win frequency falls below epsilon / K are raised to exactly epsilon / K and the rest rescaled to preserve sum-to-1, iterating until stable — so no arm’s traffic is starved to zero. Inert when every arm is already above the floor; 0.0 returns raw win frequencies verbatim. This is NOT the dial for how much traffic stays on control — that is min_control_weight / min_explore_weight on pt.sequential_experiment. In the canonical Control + Explore + Optimized cell structure this floor is mostly redundant; rarely worth overriding.

Return type:

dict[int, dict[str, float]]

Returns:

{segment.id: {variant_name: weight}} — inner dicts in variant order (control first), control included, summing to 1.

Raises:
  • ValueError – When posterior.observed is None; when epsilon is outside [0, 1] (the floor-clip is only well-defined on that range — epsilon > 1 would demand per-arm floors that cannot sum to 1); or when a segment’s rule matches zero visitors (an empty mean over members would silently produce NaN weights).

  • TypeError – When posterior is not an accepted result type.