# PGA Round 2 — simulate & compare to actual

**CourtEdge PGA DK Lineup Lab** (`/pga-dk-lineup-lab.html`) has a green card at the **top of the page** (first block under the site header): **Round 2 — simulate & compare to actual**. Use it Friday during Round 2, then again tonight with real DK scores.

## 5-step flow (Round 2 today → compare tonight)

1. **Open the lab** — `/pga-dk-lineup-lab.html` (or your deploy URL). Scroll to the green **Round sim** card (or `#round-sim`).
2. **Load slate** — **Schwab classic** for full-field R2, or **Showdown R2 CPT** for the 50-golfer post-cut pool. Wait until **Players** fills in.
3. **Play one round** — **Play one round** ranks everyone in a **single** simulated Round 2 draw (leaderboard top 15). Click again for a new draw. Pick **Golfer A / B** → **H2H win %** for ~2,000 sim head-to-head on that round.
4. **Run Round 2 simulation** — **Sim Round 2 field** (800 sims default). Table columns: **Golfer · Expected · Sim μ · Actual · Error (actual − μ) · Wind wave**. Sort with **By sim μ** or **By error** after you have actuals (adds **#** rank by |error|).
5. **After Round 2 ends** — paste or upload DK round points:
   - Per player: `"r2_pts": 52.5` or `"round_pts": { "2": 52.5 }`
   - Slate map: `"round_results": { "Scottie Scheffler": { "2": 52.5 } }`
   - Round-keyed map: `"round_results": { "r2": { "Scottie Scheffler": 52.5 } }`
   Click **Upload JSON** or paste into the box, then **Apply round results**. Re-run simulation if you want fresh sim μ; **Error** and accuracy metrics update from applied actuals either way.
6. **Compare tonight** — read the **Accuracy summary** box (MAE, bias, RMSE, within-1-stroke %, top-10 vs field mean). Sort **By error** for biggest misses. **Export round-sim-accuracy.csv** for a spreadsheet audit.

Rounds 3–4 (post-cut) live under **Rounds 3–4 (post-cut)** in the same card — same JSON pattern with `r3_pts` / `r4_pts`. R3/R4 sim pools are **made-cut only**, capped at `cut_line` (default **75**).

## Round 4 accuracy (Sunday — tomorrow’s workflow)

**Before final round:** load **Showdown — Round 4** slate (or Bay Hill backtest fixture), click **Sim Round 4** (800+ sims).

**After DK posts R4 showdown points:**

```json
{
  "round_results": { "Scottie Scheffler": { "4": 48.2 } },
  "players": [{ "name": "Scottie Scheffler", "r4_pts": 48.2 }]
}
```

Apply in the lab or CLI:

```bash
node scripts/sim-pga-round-accuracy.mjs --round 4 --slate data/pga-dk-slate-showdown-r4-cpt.json --sims 1200
node scripts/sim-pga-round-accuracy.mjs --round 4 --slate data/pga-dk-slate-showdown-r4-cpt.json --apply-actuals actuals-r4.json --csv data/pga-r4-accuracy.csv
```

**Bay Hill offline backtest** (fixture — not this week’s live tour stop):

```bash
node scripts/prep-pga-r4-sim.mjs --fixture bay-hill --sim
```

Uses `data/pga-dk-slate-fixture-bay-hill-r4-sim.json` (38 made-cut golfers, windy Bay Hill course profile).

**Live this weekend (May 24 R4):** CJ Cup Byron Nelson @ TPC Craig Ranch — paste DK R4 showdown export into `data/pga-dk-slate-showdown-weekend.json`, then `node scripts/prep-pga-r4-sim.mjs`.

For the lineup builder, preset **Showdown — Round 3** loads `data/pga-dk-slate-showdown-r3-cpt.json` (~72 made-cut golfers after filter); rebuild with `node scripts/build-pga-dk-showdown-r3-slate.mjs` when the weekend slate updates.

## Measuring accuracy nightly

After each round when DK points are final:

| Step | Action |
|------|--------|
| 1 | Run round sim (R2 full field; R3/R4 post-cut) with your current slate + projections |
| 2 | Apply actuals JSON (`r2_pts`, `round_results`, etc.) — no re-sim required for MAE |
| 3 | Note **MAE** (typical target &lt; 8–10 DK pts for a single round), **bias** (positive = model low), **RMSE** |
| 4 | Check **within 1 stroke** % — share of golfers within ±1 pt of sim μ |
| 5 | **Top-10 vs field mean** — how often your top sim-μ names beat a naive field-average predictor |
| 6 | Export **round-sim-accuracy.csv** — per-golfer rank by \|error\|, archive by tournament/week |

Last-run summary metrics are stored in **sessionStorage** (`pga_dk_round_sim_last`) for quick reload in the same browser tab.

**Bias sign:** Error column is **actual − sim μ**. Positive bias in the summary means the model undershot actuals on average.

## JSON example (minimal)

```json
{
  "round_results": {
    "Scottie Scheffler": { "2": 52.5 },
    "Rory McIlroy": { "2": 44.1 }
  },
  "players": [
    { "name": "Scottie Scheffler", "r2_pts": 52.5 }
  ]
}
```

Alternate shapes the engine accepts: `round_results.r2` name map, `round_results["2"]`, per-player `round_results: { "r2": 52.5 }`, `actual_r2`, `r2_actual`.

## How expected & sim μ are built

- Per-round override: `r2_proj`, `round_2_projection`, or `round_projections: { "2": 18.2 }`
- Else classic splits full `projection` using slate `early_round_share` (default 0.5): R1+R2 get half of proj (¼ each), R3+R4 get the other half (¼ each)
- Sim draw: floor–ceiling scaled to that round’s expected share (course/wind enrich as full-event sim)

Engine: `js/pga-dk-engine.js` → `monteCarloRoundPool`, `simulateRoundPlayout` (one draw + optional H2H), `readRoundActual`, `expectedRoundPts`, `computeRoundAccuracy`.

**Play-out vs field sim:** **Play one round** = one score per golfer, sorted leaderboard (good for “who wins this round today”). **Sim Round 2 field** = thousands of draws **per golfer** → sim μ / percentiles (good for projection accuracy). Neither models correlated course conditions across the field like SaberSim.

## Wind wave (GPP lineups)

On windy slates (`wind_mph` ≥ 12), the round table **Wind wave** column shows `calm` vs `wind` from `wind_exposure`, `wind_tier`, and `wave` / `tee_wave`. **Rules engine** → **Prefer no-wind wave (GPP)** stacks calm-wave lineups in **Build GPP set**.

## Testing locally

```bash
node scripts/test-pga-dk-optimizer.mjs
```

## vs SaberSim / full-field sim

- **No** correlated field, ownership, or dup simulation
- **No** automatic live leaderboard pull
- Single-round **independent** draws per golfer — sanity-check round projections vs outcomes, not MME portfolio EV
