diff --git a/src/lib/derby/data.ts b/src/lib/derby/data.ts new file mode 100644 index 0000000..9a4fc68 --- /dev/null +++ b/src/lib/derby/data.ts @@ -0,0 +1,57 @@ +// Derby R 2026 · Duindigt · zondag 20 september · 2600 m autostart +// Bronnen: NDR deelnemerslijst, ZEturf koerskaart (records over 2600 m), +// Geny uitslagen Derby Selectie R (heat 1) en Merrie Derby R (heat 2), 4 september 2026. + +export type Result = number | 'D'; + +export type Horse = { + n: number; + name: string; + driver: string; + trainer: string; + sex: 'M' | 'H' | 'R'; + /** laatste vijf uitslagen, nieuwste eerst; D = gediskwalificeerd */ + form: Result[]; + /** beste kilometertijd over 2600 m in seconden, null = onbekend */ + record: number | null; + /** Derbyproef 4 september (2000 m autostart) */ + trial: { heat: 1 | 2; pos: number; time: number; odds: number }; + note?: string; +}; + +export const race = { + name: 'Derby R 2026', + date: 'zo 20 sep · 17:40', + track: 'Duindigt', + distance: 2600, + start: 'Autostart', + purse: 74000, + surface: 'Sintelbaan · goed', + sources: [ + { + label: 'NDR deelnemerslijst', + href: 'https://ndr.nl/wp-content/uploads/2026/09/Ingeschreven-paarden-in-de-Derby-R-Duindigt-zondag-20-september.pdf' + }, + { + label: 'ZEturf koerskaart', + href: 'https://www.zeturf.be/nl/course-du-jour/2026-09-20/R14C6-duindigt-derby-r' + }, + { label: 'Proef heat 1', href: 'https://www.geny.com/course/1681375/arrivee-rapports' }, + { label: 'Proef heat 2', href: 'https://www.geny.com/course/1681376/arrivee-rapports' } + ] +}; + +const t = (m: number, s: number) => m * 60 + s; + +export const horses: Horse[] = [ + { n: 1, name: 'Rihane', driver: 'Micha Brouwer', trainer: 'Danny Brouwer', sex: 'M', form: [2, 3, 9, 7, 4], record: t(1, 13.7), trial: { heat: 2, pos: 2, time: t(1, 17.7), odds: 3.0 } }, + { n: 2, name: 'Rose Starlake', driver: 'Jeffrey Mieras', trainer: 'Dion Tesselaar', sex: 'M', form: [1, 2, 2, 1, 'D'], record: t(1, 13.4), trial: { heat: 2, pos: 1, time: t(1, 17.3), odds: 3.5 } }, + { n: 3, name: 'Rising Sun Boko', driver: 'Danny Brouwer', trainer: 'Danny Brouwer', sex: 'H', form: [2, 3, 1, 4, 1], record: t(1, 14.2), trial: { heat: 1, pos: 2, time: t(1, 13.8), odds: 23.1 } }, + { n: 4, name: 'Roy Schermer', driver: 'Robin Bakker', trainer: 'Paul Hagoort', sex: 'R', form: [3, 2, 'D', 2, 1], record: t(1, 11.8), trial: { heat: 1, pos: 3, time: t(1, 14.5), odds: 7.8 } }, + { n: 5, name: 'Rolleman Flevo', driver: 'Dion Tesselaar', trainer: 'Dion Tesselaar', sex: 'H', form: [1, 1, 1, 1, 1], record: null, trial: { heat: 1, pos: 1, time: t(1, 13.8), odds: 1.3 }, note: '13 uit 13' }, + { n: 6, name: 'Royal Express', driver: 'Jim Veldman', trainer: 'Paul Hagoort', sex: 'M', form: [3, 1, 6, 1, 3], record: t(1, 12.4), trial: { heat: 2, pos: 3, time: t(1, 17.8), odds: 1.8 } }, + { n: 7, name: 'Ram Jam', driver: 'Michel Rothengatter', trainer: 'Michel Rothengatter', sex: 'R', form: [4, 1, 6, 1, 1], record: t(1, 11.5), trial: { heat: 1, pos: 4, time: t(1, 14.6), odds: 8.8 } }, + { n: 8, name: 'Rex Beuckenswijk', driver: 'Jaap van Rijn', trainer: 'Jeroen Engwerda', sex: 'R', form: [6, 4, 2, 1, 1], record: null, trial: { heat: 1, pos: 6, time: t(1, 15.2), odds: 34.9 } }, + { n: 9, name: 'Ragna L Kite', driver: 'Rob de Vlieger', trainer: 'Rob de Vlieger', sex: 'M', form: [4, 2, 3, 1, 3], record: t(1, 14.8), trial: { heat: 2, pos: 4, time: t(1, 17.8), odds: 6.8 } }, + { n: 10, name: 'Ritchy Diamond', driver: 'Patrick de Haan', trainer: 'Michel Rothengatter', sex: 'R', form: [5, 3, 1, 'D', 2], record: null, trial: { heat: 1, pos: 5, time: t(1, 14.7), odds: 20.9 } } +]; diff --git a/src/lib/derby/model.test.ts b/src/lib/derby/model.test.ts new file mode 100644 index 0000000..279d936 --- /dev/null +++ b/src/lib/derby/model.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from 'vitest'; +import { horses } from './data'; +import { betValue, defaultWeights, placings, predict } from './model'; + +describe('derby model', () => { + it('produces a probability distribution', () => { + const preds = predict(horses, defaultWeights); + const sum = preds.reduce((a, r) => a + r.p, 0); + expect(sum).toBeCloseTo(1, 9); + expect(preds[0].horse.n).toBe(5); + }); + + it('renormalises after scratching', () => { + const preds = predict(horses.filter((h) => h.n !== 5), defaultWeights); + expect(preds.reduce((a, r) => a + r.p, 0)).toBeCloseTo(1, 9); + expect(preds.find((r) => r.horse.n === 5)).toBeUndefined(); + }); + + it('place probabilities are consistent', () => { + const preds = predict(horses, defaultWeights); + const { top, exacta, koppel, trio } = placings(preds, defaultWeights.lambda); + for (const key of ['win', 'place2', 'place3'] as const) { + expect(top.reduce((a, t) => a + t[key], 0)).toBeCloseTo(1, 9); + } + expect(exacta.reduce((a, e) => a + e.p, 0)).toBeCloseTo(1, 9); + expect(koppel.reduce((a, e) => a + e.p, 0)).toBeCloseTo(1, 9); + expect(trio.reduce((a, e) => a + e.p, 0)).toBeCloseTo(1, 9); + expect(koppel.length).toBe(45); + expect(trio.length).toBe(720); + }); + + it('kelly is zero without edge', () => { + expect(betValue(0.2, 4).kelly).toBe(0); + expect(betValue(0.5, 3).edge).toBeCloseTo(0.5); + expect(betValue(0.5, 3).kelly).toBeCloseTo(0.25); + }); +}); diff --git a/src/lib/derby/model.ts b/src/lib/derby/model.ts new file mode 100644 index 0000000..b3ba615 --- /dev/null +++ b/src/lib/derby/model.ts @@ -0,0 +1,184 @@ +import type { Horse, Result } from './data'; + +export type Weights = { + form: number; + trial: number; + record: number; + draw: number; + market: number; + risk: number; + /** halveringstijd van de vormweging, in koersen */ + halfLife: number; + /** seconden die de (langzamere) merrieproef wordt bestraft t.o.v. heat 1 */ + heatGap: number; + /** softmax-temperatuur: >1 vlakker, <1 scherper */ + temperature: number; + /** Stern-demping voor plaatskansen: lagere posities gebruiken p^lambda */ + lambda: number; +}; + +export const defaultWeights: Weights = { + form: 1, + trial: 1, + record: 0.6, + draw: 0.4, + market: 0.6, + risk: 1, + halfLife: 2, + heatGap: 0.8, + temperature: 1, + lambda: 0.8 +}; + +export const presets: Record> = { + Standaard: {}, + Proef: { form: 0.5, trial: 1.6, record: 0.3, draw: 0.4, market: 0 }, + Vorm: { form: 1.6, trial: 0.5, record: 0.5, draw: 0.3, market: 0 }, + Markt: { form: 0.3, trial: 0.3, record: 0.3, draw: 0.2, market: 1.6 } +}; + +export const featureKeys = ['form', 'trial', 'record', 'draw', 'market'] as const; +export type FeatureKey = (typeof featureKeys)[number]; + +/** logit-schaal: z-scores × gewichten worden hiermee vermenigvuldigd */ +const SCALE = 0.55; + +const POSITION_POINTS: Record = { 1: 1, 2: 0.75, 3: 0.6, 4: 0.45, 5: 0.35, 6: 0.25 }; +const positionPoints = (r: Result) => (r === 'D' ? 0 : (POSITION_POINTS[r] ?? 0.1)); + +/** binnenste startnummers zijn goud, 9 en 10 staan in de tweede rij */ +const DRAW_SCORE = [0, 0.6, 0.7, 0.6, 0.45, 0.4, 0.25, 0.1, 0, -0.2, -0.3]; + +export type Features = { + form: number; + trial: number; + record: number; + draw: number; + market: number; + pFail: number; + recordEstimated: boolean; +}; + +export function features(h: Horse, all: Horse[], w: Weights): Features { + const decay = h.form.map((_, i) => Math.pow(0.5, i / w.halfLife)); + const form = + h.form.reduce((sum: number, r, i) => sum + decay[i] * positionPoints(r), 0) / + decay.reduce((a, b) => a + b, 0); + + const heatWinner = Math.min(...all.filter((o) => o.trial.heat === h.trial.heat).map((o) => o.trial.time)); + const trial = -(h.trial.time - heatWinner) - (h.trial.heat === 2 ? w.heatGap : 0); + + const known = all.filter((o) => o.record !== null).map((o) => o.record as number); + const recordEstimated = h.record === null; + const record = -(h.record ?? known.reduce((a, b) => a + b, 0) / known.length); + + const disq = h.form.filter((r) => r === 'D').length; + const poor = h.form.filter((r) => r !== 'D' && r >= 7).length; + const pFail = Math.min(0.5, 0.03 + 0.1 * disq + 0.03 * poor); + + return { form, trial, record, draw: DRAW_SCORE[h.n], market: -Math.log(h.trial.odds), pFail, recordEstimated }; +} + +function zscores(values: number[]): number[] { + const mean = values.reduce((a, b) => a + b, 0) / values.length; + const sd = Math.sqrt(values.reduce((s, v) => s + (v - mean) ** 2, 0) / values.length); + return values.map((v) => (sd === 0 ? 0 : (v - mean) / sd)); +} + +export type Prediction = { + horse: Horse; + features: Features; + /** bijdrage per feature aan de logit (gewicht × z × schaal) */ + contributions: Record; + riskPenalty: number; + logit: number; + p: number; +}; + +export function predict(runners: Horse[], w: Weights): Prediction[] { + if (runners.length === 0) return []; + const feats = runners.map((h) => features(h, runners, w)); + const z = Object.fromEntries(featureKeys.map((k) => [k, zscores(feats.map((f) => f[k]))])) as Record; + + const rows = runners.map((horse, i) => { + const contributions = Object.fromEntries( + featureKeys.map((k) => [k, w[k] * z[k][i] * SCALE]) + ) as Record; + const riskPenalty = w.risk * Math.log(1 - feats[i].pFail); + const logit = Object.values(contributions).reduce((a, b) => a + b, 0) + riskPenalty; + return { horse, features: feats[i], contributions, riskPenalty, logit, p: 0 }; + }); + + const max = Math.max(...rows.map((r) => r.logit)); + const exp = rows.map((r) => Math.exp((r.logit - max) / w.temperature)); + const total = exp.reduce((a, b) => a + b, 0); + rows.forEach((r, i) => (r.p = exp[i] / total)); + return rows.sort((a, b) => b.p - a.p); +} + +export type Placings = { + /** kans op 1e, 2e, 3e per paard (index = positie in `preds`) */ + top: { win: number; place2: number; place3: number }[]; + exacta: { a: number; b: number; p: number }[]; + koppel: { a: number; b: number; p: number }[]; + trio: { a: number; b: number; c: number; p: number }[]; +}; + +/** + * Exacte Plackett–Luce-uitwerking over alle geordende drietallen, met + * Stern-demping: voor de 2e en 3e plaats wordt p^lambda gebruikt, wat de + * neiging van pure PL corrigeert om de favoriet te vaak tweede te laten worden. + */ +export function placings(preds: Prediction[], lambda: number): Placings { + const n = preds.length; + const p1 = preds.map((r) => r.p); + const p2 = p1.map((p) => Math.pow(p, lambda)); + const s2 = p2.reduce((a, b) => a + b, 0); + + const top = preds.map(() => ({ win: 0, place2: 0, place3: 0 })); + const exacta: Placings['exacta'] = []; + const trio: Placings['trio'] = []; + + for (let a = 0; a < n; a++) { + top[a].win = p1[a]; + for (let b = 0; b < n; b++) { + if (b === a) continue; + const pab = p1[a] * (p2[b] / (s2 - p2[a])); + exacta.push({ a, b, p: pab }); + top[b].place2 += pab; + for (let c = 0; c < n; c++) { + if (c === a || c === b) continue; + const pabc = pab * (p2[c] / (s2 - p2[a] - p2[b])); + trio.push({ a, b, c, p: pabc }); + top[c].place3 += pabc; + } + } + } + + const koppelMap = new Map(); + for (const e of exacta) { + const [a, b] = e.a < e.b ? [e.a, e.b] : [e.b, e.a]; + const key = `${a}-${b}`; + const cur = koppelMap.get(key) ?? { a, b, p: 0 }; + cur.p += e.p; + koppelMap.set(key, cur); + } + + const desc = (xs: T[]) => xs.sort((x, y) => y.p - x.p); + return { top, exacta: desc(exacta), koppel: desc([...koppelMap.values()]), trio: desc(trio) }; +} + +/** verwachte waarde per euro en Kelly-fractie voor een winweddenschap */ +export function betValue(p: number, odds: number) { + const edge = p * odds - 1; + const kelly = odds > 1 ? Math.max(0, edge / (odds - 1)) : 0; + return { edge, kelly }; +} + +export const fairOdds = (p: number) => (p > 0 ? 1 / p : Infinity); + +export function formatTime(seconds: number) { + const m = Math.floor(seconds / 60); + const s = seconds - m * 60; + return `${m}'${s.toFixed(1).padStart(4, '0').replace('.', '"')}`; +} diff --git a/src/lib/derby/practice.ts b/src/lib/derby/practice.ts new file mode 100644 index 0000000..8ec9bdb --- /dev/null +++ b/src/lib/derby/practice.ts @@ -0,0 +1,44 @@ +// Oefenrondes: verzonnen toto-noteringen om de pagina te leren lezen vóór de echte koers. + +export type Scenario = { + key: string; + title: string; + story: string; + lesson: string; + scratched: number[]; + /** win-noteringen per startnummer; ontbreekt = wordt uit de modelkans afgeleid */ + odds?: Record; + /** plaatsnoteringen per startnummer */ + place?: Record; +}; + +export const scenarios: Scenario[] = [ + { + key: 'overladen', + title: 'De favoriet wordt overladen', + story: 'Iedereen op de baan zet op Rolleman Flevo. Zijn notering zakt naar 1,20 en de rest wordt duur.', + lesson: + 'Rolleman winnend is rood: bij 1,20 moet hij meer dan 83% van de tijd winnen om quitte te spelen, en zo zeker is zelfs een ongeslagen paard niet. De waarde zit bij plaatsweddenschappen op paarden die de toto vergeet.', + scratched: [], + odds: { 5: 1.2, 3: 16, 2: 11, 4: 28, 7: 22, 6: 14, 1: 35, 9: 70, 10: 90, 8: 120 }, + place: { 3: 3.2, 2: 2.6, 7: 4.5, 4: 5 } + }, + { + key: 'eens', + title: 'De toto is het eens met het model', + story: 'De noteringen liggen precies in lijn met de kansen van het model, met de gebruikelijke marge van 20% erop.', + lesson: + 'Alles is rood. Als de kansen kloppen, wint de marge van de toto altijd. Niet wedden is dan de juiste zet — en dat is een prima uitkomst van een middag kijken.', + scratched: [] + }, + { + key: 'nietstarter', + title: 'Rolleman Flevo start niet', + story: 'Vlak voor de start wordt nummer 5 teruggetrokken. De toto moet zich in een paar minuten herstellen.', + lesson: + 'Zonder de favoriet worden de kansen herverdeeld en worden Rising Sun Boko en Rose Starlake de favorieten. Vink een niet-starter altijd meteen uit, anders klopt geen enkel getal meer.', + scratched: [5], + odds: { 3: 3.5, 2: 3.2, 4: 7, 7: 6, 6: 5, 1: 12, 9: 25, 10: 30, 8: 40 }, + place: { 3: 1.6, 2: 1.5, 7: 2.2 } + } +]; diff --git a/src/routes/projects/derby/+page.svelte b/src/routes/projects/derby/+page.svelte new file mode 100644 index 0000000..0598eb6 --- /dev/null +++ b/src/routes/projects/derby/+page.svelte @@ -0,0 +1,662 @@ + + + + Derby Lens — Duindigt 2026 + + + +
+
+ + + {#if scenario} +
+ Oefenmodus · verzonnen noteringen — {scenario.title} + +
+ {/if} + +
+
+
Kansmodel · 6 signalen
+

DERBY
R 2026

+
+
{race.distance.toLocaleString('nl')} m{race.start}
+
€{(race.purse / 1000).toFixed(0)}KPrijzengeld
+
{runners.length}Starters
+
GoedSintelbaan
+
+
+ {#if favourite} +
+
Model favoriet · nr {favourite.horse.n}
+
{favourite.horse.name}
+
+ {pct(favourite.p)} + + winkans · fair notering {oddsFmt(fairOdds(favourite.p))}
+ bij de eerste drie: {pct(pl.top[0].win + pl.top[0].place2 + pl.top[0].place3, 0)} + {#if favourite.horse.note}
{favourite.horse.note} gewonnen{/if} +
+
+
+ {/if} +
+ +
+
+
Zo gebruik je dit op de dag zelf
+

Vier stappen, twintig minuten voor 17:40

+

De toto houdt ongeveer 20% van elke euro in. Structureel winnen lukt bijna niemand, ook dit model niet. Wat deze pagina wél doet: de weddenschappen die zéker verliezen eruit halen, en de rest klein en netjes doseren. Tijdens de koers zelf hoef je niets te doen — die duurt ruim drie minuten.

+
+
    + {#each steps as st, i (st.title)} +
  1. + {st.done ? '✓' : i + 1} +
    + {st.title} +

    {st.detail}

    + {#if i === 0} + + {:else} + {st.status} + {/if} +
    +
  2. + {/each} +
+
+
Oefenen
+

Laad verzonnen noteringen en kijk wat er gebeurt. Wis daarna alles voordat je de echte invult.

+
+ {#each scenarios as sc (sc.key)} + + {/each} + +
+ {#if scenario} +
+

Situatie. {scenario.story}

+

Wat je moet zien. {scenario.lesson}

+ Naar de lijst ↓ +
+ {/if} +
+
+ +
+ + +
+
+
{runners.length} starters{#if scenario} · oefening{/if}

Winkans

+
+ {#if help}

Kans dat elk paard wint volgens het model, hoogste bovenaan. Fair is de notering waarbij een weddenschap precies quitte speelt. Vul bij Notering de echte toto-notering in: staat er dan een groene Waarde, dan schat het model dat paard hoger in dan de toto. Vink een paard uit als het niet start.
Onder de naam: rijder en de laatste vijf uitslagen, nieuwste eerst. D = gediskwalificeerd.

{/if} +
+
+ #Paard + KansFairNoteringWaarde +
+ {#each preds as r, i (r.horse.n)} + {@const o = odds[r.horse.n]} + {@const v = o && o > 1 ? betValue(r.p, o) : null} +
+ toggleScratch(r.horse)} /> +
{r.horse.n}
+
+
{r.horse.name}
+
{r.horse.driver} · {r.horse.form.join('-')}
+
+
+
{pct(r.p)}
+
{oddsFmt(fairOdds(r.p))}
+ +
0} class:neg={v && v.edge <= 0}>{v ? signed(v.edge * 100, 0) + '%' : ''}
+
+ {/each} + {#each horses.filter((h) => scratched.includes(h.n)) as h (h.n)} +
+ toggleScratch(h)} /> +
{h.n}
+
{h.name}
niet-starter
+
+ {/each} +
+
+
+ +
+
+
Wedvormen naast winnen · {blending ? 'model + toto gemengd' : 'volgens het model'}
+

Plaats & combinaties

+ {#if help}

Bij een plaatsweddenschap win je als het paard bij de eerste drie eindigt — kleinere uitbetaling, grotere kans. Vul bij Toto de plaatsnotering in om te zien of er waarde zit. Koppel: je kiest de eerste twee, volgorde maakt niet uit. Trio: de eerste drie in de juiste volgorde. Fair = notering waarbij je quitte speelt.

{/if} + + + + {#each preds as r, i (r.horse.n)} + {@const t = plFinal.top[i]} + {@const place = placeP(i)} + {@const po = placeOdds[r.horse.n]} + {@const v = po && po > 1 ? betValue(place, po) : null} + + + + + + + + + + + + {/each} + +
#Paard1e2e3ePlaatsFairTotoWaarde
{r.horse.n}{r.horse.name}{pct(t.win, 0)}{pct(t.place2, 0)}{pct(t.place3, 0)}{pct(place, 0)}{oddsFmt(fairOdds(place))} 0} class:neg={v && v.edge <= 0}>{v ? signed(v.edge * 100, 0) + '%' : ''}
+
+
+
Koppel · 1–2 elke volgorde
+ {#each plFinal.koppel.slice(0, 6) as k (`${k.a}-${k.b}`)} +
{name(k.a)} + {name(k.b)}{pct(k.p)}{oddsFmt(fairOdds(k.p))}
+ {/each} +
+
+
Trio · 1–2–3 juiste volgorde
+ {#each plFinal.trio.slice(0, 6) as t (`${t.a}-${t.b}-${t.c}`)} +
{preds[t.a].horse.n} · {preds[t.b].horse.n} · {preds[t.c].horse.n}{pct(t.p)}{oddsFmt(fairOdds(t.p))}
+ {/each} +
+
+
+ +
+
Model vs. toto
+

Waarde & inzet

+ {#if help}

De toto is de mening van alle wedders samen en zit er meestal minder naast dan één model. Daarom wordt de eindkans gemengd: hoe hoger het vertrouwen in de toto, hoe sterker het model het oneens moet zijn voordat een paard waarde toont. Waarde is wat je per ingezette euro verwacht te winnen: +19% betekent €1,19 terug per €1. Inzet volgt uit Kelly: de wiskundig ideale inzet als deel van je bankroll, met een kwart als veilige fractie.

{/if} +
+ + +
+
+ + +
+ {#if bets.length === 0} +

Vul noteringen in bij de paarden (winnend hierboven, plaats hiernaast). Waarde = kans × notering − 1.

+ {:else} +
+
{toto.entered}/{preds.length}noteringen
+
{toto.complete ? signed((toto.overround - 1) * 100, 0) + '%' : '—'}toto-marge
+
{goodBets.length}met waarde
+
+ + + + {#each bets as b (`${b.type}-${b.r.horse.n}`)} + + + + + + + + + + {/each} + +
#PaardTypeTotoEindkansWaardeInzet
{b.r.horse.n}{b.r.horse.name}{b.type}{oddsFmt(b.odds)}{pct(b.p)} 0} class:neg={b.edge <= 0}>{signed(b.edge * 100, 0)}%{b.kelly > 0 ? euro(b.stake) : '—'}
+ {#if goodBets.length === 0} +

Geen enkele weddenschap heeft positieve waarde bij deze noteringen. Dat is een prima uitkomst: niet wedden is dan de beste zet.

+ {/if} + {/if} +
+
+ +
+
Per signaal · groen = pluspunt · oranje = minpunt
+

Waarom deze kansen

+ {#if help}

Elk paard wordt per signaal vergeleken met de rest van het veld: beter dan gemiddeld is groen, slechter is oranje. Het grote getal is hoeveel het signaal bijdraagt aan de score (na weging), het kleine getal eronder is de ruwe waarde. De scores bij elkaar geven de kans.

{/if} +
+ + + + + {#each featureKeys as k (k)}{/each} + + + + + {#each preds as r (r.horse.n)} + {@const f = r.features} + {@const h = r.horse} + + + + + + + + + + + + + {/each} + +
#Paard{featureLabel[k]}UitvalScoreKans
{h.n}{h.name}{sexLabel[h.sex]} · tr. {h.trainer}{signed(r.contributions.form)}{f.form.toFixed(2)} van 1{signed(r.contributions.trial)}{trialLabel(h)}{signed(r.contributions.record)}{h.record ? formatTime(h.record) : 'onbekend'}{signed(r.contributions.draw)}nr {h.n}{h.n > 8 ? ' · 2e rij' : ''}{signed(r.contributions.market)}notering {h.trial.odds.toFixed(1)}{signed(r.riskPenalty)}{pct(f.pFail, 0)} kans op D{signed(r.logit)}{pct(r.p)}
+
+
+ +
+ Begrippen voor wie nooit naar de draf kijkt +
+
    +
  • Draverij — de paarden trekken een karretje (sulky) met de rijder erop en moeten in draf blijven. Gaat een paard in galop, dan wordt het gediskwalificeerd: de D in de uitslagen.
  • +
  • Autostart — de paarden starten achter een rijdende auto met uitklapbare hekken, op volgorde van startnummer: 1 aan de binnenkant, 1 t/m 8 op de eerste rij, 9 en 10 op de tweede rij.
  • +
  • Kilometertijd — gemiddelde tijd per 1000 m, de standaardmaat voor snelheid. 1'13"8 is 1 minuut 13,8 seconden per km. Lager is sneller; één seconde is ruim tien meter.
  • +
  • Derbyproef — de kwalificatiekoers van 4 september over 2000 m, in twee heats. Heat 1 (open) werd gewonnen door Rolleman Flevo; heat 2 (alleen merries) door Rose Starlake en was 3,5 s/km trager.
  • +
  • Derby — de belangrijkste koers van het jaar voor driejarigen. Alle tien starters zijn drie jaar en lopen 2600 m; hun namen beginnen met een R omdat het geboortejaar 2023 de letter R kreeg.
  • +
  • Notering — de uitbetaling per ingezette euro als het paard wint: bij 2,50 krijg je €2,50 terug. Hoe lager, hoe meer de toto in het paard gelooft.
  • +
  • Merrie / hengst / ruin — vrouwelijk, mannelijk, gecastreerd. Merries hadden in de proef een eigen heat.
  • +
  • Rijder / trainer — wie op de sulky zit tijdens de koers, en wie het paard dagelijks traint. Tesselaar doet bij Rolleman Flevo allebei.
  • +
+
+
+ +
+ Hoe het model rekent +
+

logit = 0.55 · Σ gewicht × z(signaal) + risicostraf · ln(1 − P(uitval))  ·  kans = softmax(logit / T)

+
    +
  • Elk signaal wordt een z-score over het veld (hoeveel standaardafwijkingen boven of onder het gemiddelde), zodat seconden, punten en noteringen vergelijkbaar zijn.
  • +
  • Uitslagen — 1e = 1, 2e = 0,75, 3e = 0,6, 4e = 0,45, 5e = 0,35, 6e = 0,25, verder 0,1, D = 0; gewogen met halveringstijd.
  • +
  • Proef — minus de achterstand in seconden op de heatwinnaar, minus de merriehandicap voor heat 2.
  • +
  • Snelste tijd — minus de record-kilometertijd over 2600 m; onbekend = veldgemiddelde.
  • +
  • Startpositie — vaste curve: 2 en 3 het best, dan 1, aflopend naar 8; 9 en 10 negatief.
  • +
  • Wedmarkt — −ln(notering bij de proef).
  • +
  • Uitval — 3% + 10% per D + 3% per plek ≥ 7 in de laatste vijf; telt mee als ln(1 − kans).
  • +
  • Plaats & combinaties — Plackett–Luce, exact uitgerekend over alle 720 mogelijke top-3's; voor 2e en 3e plaats met pλ (Stern-correctie).
  • +
  • Eindkans — log-lineaire menging: ln p = (1 − v) · ln pmodel + v · ln ptoto, daarna genormaliseerd; ptoto = 1/notering gedeeld door de som (zo valt de toto-marge weg). Waarde en inzet gebruiken de eindkans.
  • +
  • De startpositie-curve en de merriehandicap zijn aannames, geen gemeten waarden. Daarom zijn ze instelbaar.
  • +
+ +
+
+ +
DERBY LENS · {race.name}SvelteKit
+
+
+ + diff --git a/static/derby-favicon.svg b/static/derby-favicon.svg new file mode 100644 index 0000000..35161e7 --- /dev/null +++ b/static/derby-favicon.svg @@ -0,0 +1,5 @@ + + + + +