# Sprint 3 — CWoLa trainer with tightened labels

**Source:** `cwola_export_20260415_windowed.json` (N = 2209)
**Generated:** by `scripts/pwpc/sprint3.py`

Trains the Stacked PLR classifier specified in STATISTICAL_FUSION.md §C3 (GradientBoostingClassifier over raw tier features + window correlations + cos(q, top_candidate)). Compares four label sets to isolate whether the re-query bucket label is noisy and whether tightening to same-intent reformulations raises the AUC gate.

### Gate (STATISTICAL_FUSION.md §C2 degenerate-bucket gate)

**AUC > 0.55** on held-out split — below this, `f_A ≈ f_B`, the bucket labels are mostly noise, and the trainer learns the background.

## Next-query cosine distribution on B-bucket

- Matched B rows: 2048 / 2048
- mean: 0.740   median: 0.793
- p10: 0.433   p90: 0.950

| threshold | retained B | % of loose B |
|---|---|---|
| > 0.3 | 1984 | 96.9% |
| > 0.4 | 1883 | 91.9% |
| > 0.5 | 1752 | 85.5% |
| > 0.6 | 1632 | 79.7% |
| > 0.7 | 1319 | 64.4% |
| > 0.8 | 1002 | 48.9% |

## Training results per label set

| label set | n_A | n_B | f_gap² | AUC mean | AUC std | Brier | gate |
|---|---|---|---|---|---|---|---|
| loose | 161 | 2048 | 0.7297 | 0.610 | 0.039 | 0.072 | PASS |
| t04 | 161 | 1883 | 0.7097 | 0.605 | 0.048 | 0.075 | PASS |
| t05 | 161 | 1752 | 0.6917 | 0.602 | 0.024 | 0.083 | PASS |
| t07 | 161 | 1319 | 0.6122 | 0.631 | 0.048 | 0.101 | PASS |

### Label-set descriptions

- **loose** — all B rows (no cosine filter)
- **t04** — B AND cos(q_t, q_{t+1}) > 0.4 (spec default)
- **t05** — B AND cos(q_t, q_{t+1}) > 0.5
- **t07** — B AND cos(q_t, q_{t+1}) > 0.7 (unambiguous reformulation)

## Top features by importance (full-fit, per label set)

### loose

| feature | importance |
|---|---|
| splade | 0.0595 |
| tag_exact__harmonic | 0.0586 |
| tag_prefix__harmonic | 0.0581 |
| fts5__tag_exact | 0.0450 |
| tag_exact__pki | 0.0433 |
| cos_q_c | 0.0430 |
| tag_prefix | 0.0426 |
| sema_boost | 0.0406 |
| harmonic | 0.0382 |
| lex_anchor__pki | 0.0360 |

### t04

| feature | importance |
|---|---|
| splade | 0.0643 |
| sema_boost | 0.0583 |
| tag_exact__harmonic | 0.0573 |
| cos_q_c | 0.0533 |
| tag_exact | 0.0515 |
| tag_exact__pki | 0.0462 |
| harmonic | 0.0440 |
| tag_prefix__harmonic | 0.0433 |
| lex_anchor | 0.0354 |
| fts5__tag_exact | 0.0323 |

### t05

| feature | importance |
|---|---|
| splade | 0.0647 |
| tag_exact | 0.0602 |
| cos_q_c | 0.0539 |
| tag_exact__harmonic | 0.0503 |
| sema_boost | 0.0458 |
| tag_prefix__harmonic | 0.0451 |
| harmonic | 0.0446 |
| fts5__tag_exact | 0.0443 |
| tag_exact__pki | 0.0441 |
| splade__pki | 0.0311 |

### t07

| feature | importance |
|---|---|
| lex_anchor | 0.0780 |
| splade | 0.0620 |
| harmonic | 0.0599 |
| cos_q_c | 0.0598 |
| tag_prefix | 0.0458 |
| tag_exact | 0.0422 |
| sema_boost__sr | 0.0396 |
| fts5__sema_boost | 0.0349 |
| sema_boost | 0.0344 |
| fts5__tag_exact | 0.0339 |

## Verdict

- **Best label set:** `t07` (AUC mean = 0.631 ± 0.048). Gate: **PASS**.
- The Stacked PLR classifier extracts above-chance signal on this label set. Ship the trained model as `fleet/helix/fusion_plr.py::StackedPLRFuser` per STATISTICAL_FUSION.md §C3; wire its `score()` behind a `plr.enabled` feature flag.

- **Class imbalance note:** n_A = 161 across all label sets (A is unaffected by the B-tightening filter). All tightening reduces n_B, which *improves* the balance but reduces absolute sample size. Stratified CV handles the imbalance; AUC is the right metric to report here (accuracy is misleading).

- **Convergence budget (Metodiev §3):** `N ~ 1/(f_A - f_B)²` per bucket for ε-close-to-optimal AUC. Current f_gap² values are in the table; the closer to 1, the less data you need. The tight label sets reduce contamination (raising `f_A - f_B`) but also reduce n — it's a real tradeoff.

— Generated by `scripts/pwpc/sprint3.py`
