Books signal non-availability by declining to quote; we read only the price #16

Closed
opened 2026-08-27 21:06:48 +02:00 by solvreven · 1 comment
Owner

Found 2026-08-27 from a user observation: Ollie Watkins, widely reported as
sold and expected to leave, was ranked 6th most likely scorer for GW2.

The mapping is correct (book "Ollie Watkins" -> FPL id 55) and the existing
moved-player guard cannot help: it keys on FPL's club, and FPL still lists him
at Aston Villa, status a, no news. Both we and the books are downstream of
that.

But the books did say something, by omission:

Aston Villa v Arsenal -- 5 books priced the fixture
  fanduel     40 players   Watkins: NOT QUOTED
  bovada      42 players   Watkins: quoted
  betrivers   28 players   Watkins: NOT QUOTED
  betmgm      33 players   Watkins: quoted
  draftkings  40 players   Watkins: NOT QUOTED

Three of five books that priced the match declined to quote him. We average the
two that did, label it n_books: 2, confidence: LOW, and rank him 6th.

This is not rare. Across the GW2 run, on fixtures with >=4 books:

total players on >=4-book fixtures:        302
quoted by under half the books:             66

  1/6  Dejan Kulusevski (long-term injured)   1/6  Marc Guiu
  1/6  Dan Burn                               1/6  Xavi Simons
  1/6  Sean Neave                             1/6  Matthew O'Riley
  2/5  Ollie Watkins

The repo already articulates the principle and applies it too narrowly.
_unpriced_but_ours says: "the omission is not a gap -- it is the book's
opinion. It quoted 14 Liverpool players and left Munoz out, which says they do
not expect him to feature." That reasoning is applied only to players NO book
quoted, never to one a MAJORITY declined.

Suggested fix. Compute quote_coverage = books_quoting / books_pricing_the _fixture and carry it per player. It is free -- the data is already banked in
every run. Use it to demote or flag thin-coverage rows, and surface it beside
n_books so a reader can see the difference between "two books agree" and "two
books agree and three refused".

Note this is a genuine availability signal that is INDEPENDENT of our
start_prob, which is derived from the 25/26 archive and cannot know about a
transfer either. Related: #9 (gameweek contamination in the same runs).

**Found 2026-08-27 from a user observation:** Ollie Watkins, widely reported as sold and expected to leave, was ranked 6th most likely scorer for GW2. The mapping is correct (book "Ollie Watkins" -> FPL id 55) and the existing moved-player guard cannot help: it keys on FPL's club, and FPL still lists him at Aston Villa, status `a`, no news. Both we and the books are downstream of that. **But the books did say something, by omission:** ``` Aston Villa v Arsenal -- 5 books priced the fixture fanduel 40 players Watkins: NOT QUOTED bovada 42 players Watkins: quoted betrivers 28 players Watkins: NOT QUOTED betmgm 33 players Watkins: quoted draftkings 40 players Watkins: NOT QUOTED ``` Three of five books that priced the match declined to quote him. We average the two that did, label it `n_books: 2, confidence: LOW`, and rank him 6th. **This is not rare.** Across the GW2 run, on fixtures with >=4 books: ``` total players on >=4-book fixtures: 302 quoted by under half the books: 66 1/6 Dejan Kulusevski (long-term injured) 1/6 Marc Guiu 1/6 Dan Burn 1/6 Xavi Simons 1/6 Sean Neave 1/6 Matthew O'Riley 2/5 Ollie Watkins ``` **The repo already articulates the principle and applies it too narrowly.** `_unpriced_but_ours` says: "the omission is not a gap -- it is the book's opinion. It quoted 14 Liverpool players and left Munoz out, which says they do not expect him to feature." That reasoning is applied only to players NO book quoted, never to one a MAJORITY declined. **Suggested fix.** Compute `quote_coverage = books_quoting / books_pricing_the _fixture` and carry it per player. It is free -- the data is already banked in every run. Use it to demote or flag thin-coverage rows, and surface it beside `n_books` so a reader can see the difference between "two books agree" and "two books agree and three refused". Note this is a genuine availability signal that is INDEPENDENT of our start_prob, which is derived from the 25/26 archive and cannot know about a transfer either. Related: #9 (gameweek contamination in the same runs).
Author
Owner

Implemented in 726c73b (2026-08-28). The suggested fix is in, and the
measured effect was slightly different from what this issue predicted — worth
recording, because the difference is the interesting part.

quote_coverage exists as n_books / n_books_fixture, computed in
props_load (n_books_fixture: len(f["books"])) and read by _will_play in
main.py at MIN_BOOK_SHARE = 0.80. A SHARE rather than a count, exactly as
this issue argued: every fixture in the GW2 run drew 5 or 6 books, so a raw
count conflates "two books like him" with "the fixture only drew two books".

Measured on run gw2_20260827T185145Z, all 422 priced players joined to GW1's
realised minutes:

quoted by <80% of the books covering his match   133   2% started   2.8 min
quoted by >=80%                                  289  65% started  57.6 min

0.80 rather than unanimity, chosen against pre-registered alternatives on the
same data: unanimity cuts 185 players and takes 11 GW1 starters with them,
against 2 at this threshold, and buys only two fewer dead rows in the top 20.

One thing this issue did not anticipate, and it matters. Applying the share
alone PROMOTED Osula to rank 19 — quoted by six of six books, with status=i,
chance_of_playing_next_round=0 and "Foot injury - Unknown return date" in
FPL's own feed. Full coverage is what a bookmaker leaves standing when it has
not repriced, so the availability signal in n_books is stale exactly where it
looks most confident.

So the fix is three witnesses, deliberately kept apart rather than blended:
our archive-derived minutes model, the book coverage share, and FPL's own
availability_by_gw (which was computed in absence.py and written to every
cached player since injuries were modelled, and which this surface had never
read). They catch different players — the model catches Rashford, the share
catches G.Jesus and Zirkzee, the flag catches Osula.

Live GW2 board, before and after:

before   8/20 did not start GW1   3 with start_prob < 0.30
after    6/20                     1

The six that remain are all status=a — available rotation risks, which is a
judgement about what the board is for rather than a data fault.

Closing. #9 (gameweek contamination in the same runs), referenced at the end of
this issue, was also fixed in the same series — this_round() now filters
banked prices on the FPL fixture pair rather than the run's name.

**Implemented in `726c73b` (2026-08-28).** The suggested fix is in, and the measured effect was slightly different from what this issue predicted — worth recording, because the difference is the interesting part. `quote_coverage` exists as `n_books / n_books_fixture`, computed in `props_load` (`n_books_fixture: len(f["books"])`) and read by `_will_play` in `main.py` at `MIN_BOOK_SHARE = 0.80`. A SHARE rather than a count, exactly as this issue argued: every fixture in the GW2 run drew 5 or 6 books, so a raw count conflates "two books like him" with "the fixture only drew two books". Measured on run `gw2_20260827T185145Z`, all 422 priced players joined to GW1's realised minutes: ``` quoted by <80% of the books covering his match 133 2% started 2.8 min quoted by >=80% 289 65% started 57.6 min ``` 0.80 rather than unanimity, chosen against pre-registered alternatives on the same data: unanimity cuts 185 players and takes 11 GW1 starters with them, against 2 at this threshold, and buys only two fewer dead rows in the top 20. **One thing this issue did not anticipate, and it matters.** Applying the share alone PROMOTED Osula to rank 19 — quoted by six of six books, with `status=i`, `chance_of_playing_next_round=0` and "Foot injury - Unknown return date" in FPL's own feed. Full coverage is what a bookmaker leaves standing when it has not repriced, so the availability signal in `n_books` is stale exactly where it looks most confident. So the fix is three witnesses, deliberately kept apart rather than blended: our archive-derived minutes model, the book coverage share, and FPL's own `availability_by_gw` (which was computed in `absence.py` and written to every cached player since injuries were modelled, and which this surface had never read). They catch different players — the model catches Rashford, the share catches G.Jesus and Zirkzee, the flag catches Osula. Live GW2 board, before and after: ``` before 8/20 did not start GW1 3 with start_prob < 0.30 after 6/20 1 ``` The six that remain are all `status=a` — available rotation risks, which is a judgement about what the board is for rather than a data fault. Closing. #9 (gameweek contamination in the same runs), referenced at the end of this issue, was also fixed in the same series — `this_round()` now filters banked prices on the FPL fixture pair rather than the run's name.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
solvreven/FPL#16
No description provided.