Coach explainer
Why this works — a coach's explainer
Plain-English version of the model, with no math beyond what you'd write on a whiteboard. If something here doesn't square with how you think about the season, that's a flag worth raising.
The one-line idea
Start with where you finished. Adjust for who's leaving and who's coming back. Flag the cases where the data and the eye test disagree, and let the board talk those out.
That's the whole engine. Everything below is a defensible answer to how we adjust.
What this is and isn't
This is.
- A pre-season scheduling and ranking aid for Missouri girls' D1.
- An honest second opinion on "where will each program land next year?"
- A board packet that points to the few teams worth a real conversation (rebuilds, late surges, programs losing All-State talent).
This isn't.
- A replacement for Powerwise. Powerwise is the source of truth for in-season ratings.
- A box-score popularity contest. We do not re-rank the league by who scored the most goals.
- A black box. Every adjustment is named and shows up in a CSV the board can question.
What we use as truth
We trust results. We do not trust raw stats to rank teams.
- Last year's finish order (
finish_2026.tsv) is our anchor. That's where everyone did end up after the season. - Game results drive strength of schedule. Beating an elite team counts more; rolling a 33-2 against a rebuilding program counts less.
- Rosters and grad years drive who's leaving.
- STL Lacrosse Awards (All-Conference, All-State, All-American) drive who the coaches said were the best players.
We do not rank a team higher because their goals total was big. The only way box stats move you is relative to your opponents, and only when the opponent was actually a tough matchup.
Why "where did you finish?" isn't enough
If we just carry forward last year's finish, two things happen and both are unfair:
- Rebuilds get punished. A program that had a senior-heavy top-5 team last year and graduates 8 starters is still scheduled like a top-5 team. Their younger kids get dragged through a brutal schedule, lose interest, and the program slides further.
- Legacy programs get a free pass. A multi-year power that returns very little is still treated as elite, gets a softer schedule than it deserves, and beats up on rebuilds.
Both are bad for the league. The model exists to adjust off the prior finish — not to throw it out.
How we measure team strength (three buckets)
For each team, every game produces three 0–100 numbers. We then average across the season, weighted so close games against good opponents matter more than blowouts against weak ones.
| Bucket | What it captures | Coach intuition |
|---|---|---|
| Offense | Goals per possession (not per game) | Are we efficient when we have the ball? |
| Defense | Shots faced + save % and ground balls + interceptions + caused turnovers | Goalie play plus field pressure that ends possessions. |
| Possession | Draw controls | Do we win the draw and start with the ball? |
We don't count goals allowed twice (so a great goalie isn't double-credited and a leaky D isn't double-penalized). Each bucket is calibrated to the actual Missouri distribution, so the top of the league lands in the 80s/90s instead of everyone pegging at 100.
The composite score is 0.35×offense + 0.35×defense + 0.30×possession. That number is a diagnostic — a quick read on a team's profile. It's not how we rank the league.
How we measure player impact
A player's "impact" is their share of team production, weighted by who they did it against.
- A goal against the league #1 counts about 3× a goal against the league #30.
- A goal in a 1-goal game counts more than a goal in a 12-goal blowout (we use the same close/medium/blowout buckets that Powerwise does).
- Defense share splits like the team model: 55% of your defensive credit comes from goalie work (minutes-weighted saves/GA when we have them), 45% from field events (ground balls, interceptions, caused turnovers). A starter goalie is not 100% of team defense — defenders earn the field slice by their GB/INT/CTO.
- Goalies are credited by minutes when we have them, not just save totals — so a starter logging 80% of goalie minutes gets ~44% of total team defense credit (0.55 × 0.8), not the whole bar.
This gives us per-player scores like offense_share_sos = 0.21 ("she generated 21% of our scoring against the schedule we played"). We can sum those up for the seniors graduating to get the share of team power that walks off the field.
How we project next year
Two layers, deliberately conservative:
Layer 1 — replacement-level subtraction
When a senior leaves, the next kid up the depth chart isn't zero — they're roughly a typical bench player at that position. So we subtract:
loss = max(senior's share − replacement baseline at her position, 0)
The replacement baselines come from real non-senior shares pooled across two seasons (run_meta.json → replacement_baselines).
Layer 2 — context multipliers
Then we tug that loss up or down for things scouts already think about:
| Situation | Direction |
|---|---|
| Senior held ≥ 32% of team's scoring (concentration risk) | bigger loss |
| Strong returning juniors / sophomores (depth) | smaller loss |
| Starting goalie leaves and backup played < 22% of minutes | bigger defensive loss |
| Late-season underclass surge (last-third goal share) | smaller loss |
| Departing assist-and-goal partner (feeder pair) | bigger loss |
| Returning draw control share is high | smaller possession loss |
| Departing All-State / All-American honors | bigger loss |
| Returning All-State / All-American honors | smaller loss |
| Multi-year legacy program (small composite floor) | small protective bump |
The award piece is tuned: All-Conference 1st = 2 points, State HM = 4, State 1st = 8, All-American = 16 (= 2× State 1st). Coaches' selections are an independent talent signal we don't get from box scores alone.
How we turn that into a finish rank
This is the part the board votes on, so we're explicit about it.
projected_rank = prior_finish + (capped move based on roster turnover)
- The cap is ±4 slots in either direction. A 12th-place team can land 8th or 16th, but not 1st or 32nd from one year of turnover. That's deliberate. A program with a great recruiting class still has to win games to climb 10 spots.
- A "stability factor" (legacy, returning share, low-loss bonus, top-8 bonus) shrinks the move further for programs with a track record.
- We then resolve any ties so every team gets a unique slot.
There's a diagnostic column called composite_rank_diagnostic that re-sorts by box-score composite. We show it to the board, but it's not the vote line — re-sorting on box scores alone has more than 3× the rank error in our holdout test.
When the board should look harder
Every team in the recommendations CSV has a list of flags. If the flag list is empty, the model says "no surprises here, ship the proposed rank." If a team has flags, the board reads them aloud and decides:
| Flag | Plain English | What to look at |
|---|---|---|
large_move_vs_prior |
Model wants to move them ≥ 4 slots | Confirm the roster turnover is real |
low_returning_heavy_loss |
Lost a lot, returning is thin | Underclass video; replacement candidates |
high_returning_still_down |
Strong return but model still moves down | Is the assist/feeder structure intact? |
legacy_downgrade |
Long-term contender, model says down | Coach interview, off-season program signals |
award_talent_loss |
Multiple All-State / All-American departures | Real talent loss vs. quality returning class |
roster_composite_split |
Box-score composite disagrees with roster move | Diagnostic only — usually composite is wrong |
backtest_large_error |
(Backtest only) the model missed by ≥ 5 last year | Worth a future audit |
A "high priority" flag count of 1 doesn't mean the model is wrong; it means somebody at the table probably knows something the data doesn't, and it's worth asking before voting.
The board can also write program_notes and board_voted_rank into a CSV, and those overrides flow back into the final report.
How we know the model is doing something useful
We hold out a season we already know the answer to (2024-25 stats + 2025 grads → predict 2026 finish, where we know finish_2026). The numbers are in output/backtest_2024_25/backtest_report.md:
| Predictor | All teams | Top-20 finish |
|---|---|---|
| "Same as last year" (naive) | MAE 2.7 | MAE 2.5 |
| Roster-adjusted board line | MAE 3.1 | MAE 1.9 |
| Re-sorted by box-score composite | MAE 6.7 | MAE 4.9 |
Reading: in the top half of the league — the part scheduling and league balance care about — the board line is meaningfully better than naive, and the box-score composite is meaningfully worse. That's why the model exists, and why we don't trust composite re-sort.
For the bottom of the league, the model and "same as last year" are roughly tied. That's fine; movement at the bottom is mostly noise.
What we don't claim
- We can't predict championships. This is a regular-season finish projection, not a postseason model.
- We can't replace coach knowledge. Several flags exist precisely to force the conversation with someone who knows the program.
- We don't know about transfers we haven't seen. A new All-American who shows up in August won't be in the projection until games are played.
- The model is conservative. A ±4 slot cap and replacement-level subtraction make us slow to call a 25th-place team into the top 10 even if their video is shocking.
How to read a team's row in ranking_recommendations_*.csv
Take MICDS as a worked example (current 2027 forward outlook):
| Column | Value | What it tells you |
|---|---|---|
prior_finish_rank |
4 | Where they finished 2026 |
recommended_rank |
2 | First-pass roster model proposal |
recommended_rank_adjusted |
2 | After secondary regression — board vote line |
composite_rank_diagnostic |
1 | Box-score re-sort says #1 (diagnostic only) |
score_drop |
4.86 | Composite points lost to graduation |
returning_production_share |
0.774 | ~77% of weighted production stays |
legacy_prior |
1.00 | Top-tier multi-year program |
departing_awards_points |
13 | Several big honors graduated |
returning_awards_points |
21 | Strong honors returning |
flag_codes |
(mostly empty) | No required action |
final_rank |
2 | Board vote line (after manual overrides if any) |
So: high legacy, returning class is loaded with award winners, model carries them up one slot from prior finish. No flags, but composite_rank_diagnostic = 1 is a "hey, the box scores think they're #1" footnote — likely an MICDS top-5 group that the prior #1 (Burroughs) lost more from.
Process at a glance
1. Pull every game and stat for the season (already done)
2. Pull the awards from the STL site
3. Run the pipeline: python3 run_glax_analysis.py --season both
4. Look at output/ranking_board_report_2027.md ← this is the board packet
5. For each "Action required" flagged team, the board talks it out
6. Edit board_overrides_2027.csv with notes / votes
7. Re-run the pipeline; final_rank reflects the votes; ship the report
If a team's projection feels off, it almost always shows up in the flags or in secondary_adjustment_reasons. Open the CSV and the answer is in plain English.
What you should push back on
The model is meant to be argued with. Things worth challenging out loud:
- A flagged downgrade for a program you know recruited well in spring. That's exactly the kind of input the board override CSV is for.
- A blowout-risk score that doesn't match a known rivalry. Add an HFA / context note in
program_notes. - An award winner you know is actually transferring. Currently the model treats her as returning; flag manually.
The validation guide in docs/VALIDATION.md walks through every CSV column and the math behind each adjustment. If something still feels off after reading both, that's a finding worth fixing — open a ticket.