Claude
Skills
Sign in
Back

matchup-win-probability-sim

Included with Lifetime
$97 forever

Computes P(we win at least K of N categories) for a head-to-head categorical matchup via Monte-Carlo simulation or Poisson-binomial approximation. Domain-neutral — works for any fantasy sport with H2H Categories scoring (MLB, NBA, NHL) or any zero-sum per-category competition. Use when you need matchup_win_probability, per_cat_win_probability, expected_cats_won, or variance_estimate; or when user mentions "matchup win probability", "head to head simulation", "Monte Carlo matchup", "Poisson binomial matchup", "P win 6 of 10", "category matchup simulation", or "weekly win probability".

General

What this skill does

# Matchup Win Probability Simulator

## Table of Contents
- [Example](#example)
- [Workflow](#workflow)
- [Common Patterns](#common-patterns)
- [Guardrails](#guardrails)
- [Quick Reference](#quick-reference)

## Example

**Scenario**: Yahoo MLB 10-category H2H matchup, Week 8, threshold = 6 of 10. Categories: R, HR, RBI, SB, OBP (hitting); K, ERA, WHIP, QS, SV (pitching). ERA and WHIP are inverse (lower is better).

**Inputs** (remaining-week projections for both teams; mean = expected output, stddev = uncertainty):

| Cat | Our mean | Our stddev | Opp mean | Opp stddev | Inverse? |
|-----|----------|-----------|----------|-----------|----------|
| R | 42 | 8 | 38 | 7 | no |
| HR | 12 | 3.5 | 14 | 4 | no |
| RBI | 40 | 9 | 41 | 8 | no |
| SB | 6 | 2.5 | 4 | 2 | no |
| OBP | 0.335 | 0.015 | 0.328 | 0.014 | no |
| K | 55 | 10 | 50 | 9 | no |
| ERA | 3.85 | 0.45 | 4.10 | 0.50 | **yes** |
| WHIP | 1.22 | 0.08 | 1.28 | 0.09 | **yes** |
| QS | 4 | 1.5 | 3 | 1.4 | no |
| SV | 2 | 1.2 | 5 | 1.5 | no |

**Run both modes** with `random_seed=42`, `cat_win_threshold=6`, `n_simulations=10000`:

**Monte Carlo output**:
- `matchup_win_probability` = 0.612
- `expected_cats_won` = 6.18
- `variance_estimate` = 2.42 (variance of cats-won count)
- `per_cat_win_probability`: R 0.64, HR 0.35, RBI 0.47, SB 0.72, OBP 0.64, K 0.64, ERA 0.65, WHIP 0.68, QS 0.69, SV 0.08

**Poisson-binomial output** (for comparison, uses the same per-cat win probs as inputs to the PB recurrence):
- `matchup_win_probability` = 0.605
- `expected_cats_won` = 6.18 (exact — sum of per-cat probs)
- `variance_estimate` = 2.11 (variance of sum of independent Bernoullis)

Interpretation: we are a modest favorite (~61%). SV is a hard-punt cat (8% win). HR is contested but we lean losing. The six most defensible pushes are SB, QS, WHIP, ERA, R, K (and OBP). Downstream `mlb-lineup-optimizer` uses `win_prob = 0.612` to classify us as a favorite → damp variance.

## Workflow

Copy this checklist and track progress:

```
Matchup Win Probability Simulation Progress:
- [ ] Step 1: Validate inputs and cat_list coverage
- [ ] Step 2: Choose sim_mode (monte_carlo or poisson_binomial)
- [ ] Step 3: Apply inverse-cat handling
- [ ] Step 4: Run simulation with seeded RNG
- [ ] Step 5: Compute per-cat and overall win probabilities
- [ ] Step 6: Emit outputs with variance and optional sim_trace
```

**Step 1: Validate inputs**

Confirm every cat in `cat_list` has an entry in both `our_per_cat_projection` and `opp_per_cat_projection`, each with `{mean, stddev}`. Confirm `cat_win_threshold <= len(cat_list)`. Confirm `cat_inverse_list ⊆ cat_list`. See [resources/template.md](resources/template.md#input-schema).

- [ ] Every cat in `cat_list` has both sides' projections
- [ ] `stddev > 0` for all cats (zero stddev blocks correct simulation; use small floor if unknown)
- [ ] `cat_win_threshold` in `[1, len(cat_list)]`
- [ ] All `cat_inverse_list` entries are valid cat names
- [ ] If `sim_mode == "monte_carlo"`, `n_simulations >= 1000` (10k default; 100k for tight confidence)

**Step 2: Choose sim_mode**

Default to `monte_carlo` for operational decisions (full distribution of cats-won). Use `poisson_binomial` when you need a deterministic, sub-millisecond answer and are willing to assume per-cat independence. See [resources/methodology.md](resources/methodology.md#when-to-use-which-mode).

- [ ] `monte_carlo`: when you need `sim_trace` for audit, when distributions are non-normal, or when per-cat correlations are passed
- [ ] `poisson_binomial`: when you need a fast closed-form approximation, or when calling this skill inside an inner optimization loop

**Step 3: Apply inverse-cat handling**

For every cat in `cat_inverse_list`, flip the comparison: our team wins the cat when our draw is **less** than the opponent's draw (ERA 3.50 beats ERA 4.20). The cleanest implementation is to negate the margin `(our_draw - opp_draw) → (opp_draw - our_draw)` for inverse cats before counting the win. See [resources/methodology.md](resources/methodology.md#inverse-category-handling).

- [ ] Identify inverse cats from `cat_inverse_list`
- [ ] Negate margin (or flip comparison) per cat
- [ ] Verify the per-cat win prob for a known-inverse cat aligns with intuition

**Step 4: Run simulation**

With `random_seed` set, draw paired outcomes (one per team per cat per sim) from the configured distribution (normal default) and score each sim.

- [ ] Seed the RNG deterministically from `random_seed`
- [ ] For each of `n_simulations`:
  - Draw `our_draw[cat] ~ Normal(our_mean, our_stddev)` for every cat
  - Draw `opp_draw[cat] ~ Normal(opp_mean, opp_stddev)` for every cat
  - For each cat, compute margin (negated for inverse)
  - Count `cats_won = sum(margin > 0)` (tie-break rule: exact tie counts as 0.5 or 0; see guardrail #4)
  - Record `matchup_win = (cats_won >= cat_win_threshold)`

For Poisson-binomial mode instead: compute per-cat win prob `p_i = Φ(margin_mean_i / combined_stddev_i)` analytically (negate margin for inverse cats), then apply the PB recurrence to get `P(sum >= threshold)`.

**Step 5: Compute outputs**

- `matchup_win_probability` = mean of `matchup_win` across sims (MC) or closed-form PB result.
- `per_cat_win_probability[cat]` = mean of `margin > 0` per cat (MC) or `Φ(...)` per cat (PB).
- `expected_cats_won` = mean of `cats_won` (MC) or `Σ p_i` (PB).
- `variance_estimate` = variance of `cats_won` across sims (MC) or `Σ p_i(1-p_i)` (PB, since cats are modeled as independent Bernoullis).

See [resources/methodology.md](resources/methodology.md#variance-estimation) for derivation.

**Step 6: Emit outputs and audit trace**

Return the full output dict. If `return_sim_trace=true`, include the first 100 sims as `sim_trace` for caller-side audit (showing per-cat draws and the final cats_won vector).

- [ ] All outputs present: `matchup_win_probability`, `per_cat_win_probability`, `expected_cats_won`, `variance_estimate`
- [ ] Optional: `sim_trace` (first 100 sims only — keep payload small)
- [ ] Cite `random_seed` used so the caller can reproduce
- [ ] Validate using [resources/evaluators/rubric_matchup_win_probability_sim.json](resources/evaluators/rubric_matchup_win_probability_sim.json). Minimum standard: average score 3.5 or above.

## Common Patterns

**Pattern 1: MLB Yahoo 5x5 (10 cats)**
- **cat_list**: `[R, HR, RBI, SB, OBP, K, ERA, WHIP, QS, SV]`
- **cat_win_threshold**: 6
- **cat_inverse_list**: `[ERA, WHIP]`
- **Ratio cats needing volume weighting**: OBP (weight by PA), ERA (weight by IP), WHIP (weight by IP). Caller should pre-compute stddev that reflects volume — a half-week with 20 IP has larger ratio variance than a full week with 55 IP.
- **Typical runtime**: 10k sims < 200ms in plain Python

**Pattern 2: NBA 9-cat H2H**
- **cat_list**: `[PTS, REB, AST, STL, BLK, 3PM, FG%, FT%, TO]`
- **cat_win_threshold**: 5
- **cat_inverse_list**: `[TO]` (turnovers — lower is better)
- **Ratio cats**: FG%, FT% (volume-weight by FGA, FTA)
- **Special**: NBA has higher cat-to-cat correlation than MLB (team usage patterns link PTS-AST-3PM); pass correlation matrix if available

**Pattern 3: NHL 10-cat or similar**
- **cat_list** example: `[G, A, +/-, PIM, PPP, SOG, W, GAA, SV%, SO]`
- **cat_win_threshold**: 6
- **cat_inverse_list**: `[GAA]` (goals against average)
- **Ratio cats**: SV% (volume-weight by SA), GAA (volume-weight by games played)

**Pattern 4: Mid-week live matchup (partial week elapsed)**
- Caller should pass `{mean, stddev}` reflecting **remaining-week** output plus current running total. That is, `mean = running_total + expected_remaining`; `stddev` shrinks as less time remains.
- `cat_position` from `mlb-category-state-analyzer` feeds directly: locked-in cats should have near-zero stddev and a mean far outside the opponent's distribution.

## Guardrails

1. **Reproducibility requires a seed**. Without `random_seed`, two calls with identical inputs will return slightly different probabilities (Monte Carlo error). Fo

Related in General