A longer note on why this tool exists, how the numbers are made, and what the backtests showed. For install and commands, see the repository README.
History
The project started as a personal tool for myself (Stanislav Litvinov, SK Niš, Olympic Bow, Seniori). Before a competition the only public picture of the field is a registration table plus scattered result pages. The first version was a single script for Indoor 18m tournament type: scrape who is entered, weight recent scores, simulate places, print three goals.
It grew into this package: indoor and outdoor rounds, club and mixed-team forecasts, HTTP caching, and a backtest against finished official SSS events. The model stayed deliberately small so a predicted 445 is something you can argue with, not a score from a trained net.
The problem
A Serbian tournament weekend answers three practical questions:
- What score is a realistic qualification for me?
- Roughly which place is that in this field?
- After the event: was the forecast honest, or did it peek at results it should not have seen?
A naive average of everything on an archer’s profile fails in several ways. Indoor 18m (max 600) and outdoor 70m (max 720) cannot be mixed. A walk-off of 161 or 322 is not a current level. Two no-shows change everyone’s place. Someone shooting the round for the first time has no history to average. Recency matters: last month should count more than last year.
Coaches and archers still do this in their heads, badly, the night before KV.
The solution
Given a registration URL, style, category, and round:
- Scrape who is entered.
- Load each archer’s results for the chosen seasons, only that round and category, dropping incomplete scores.
- Predict a qualification mean
μand spreadσ. - Simulate the field 2,500 times (plus a small elimination place shift from KV vs EL).
- Print a table: expected place (median and middle 50%), score, distance to the field median, confidence, and goals for
--me. - Optionally validate: pretend a finished event has not been shot yet, compare to actual KV, report MAE and bias.
Walk-offs, DNS, and archers with no history are shown but excluded from error metrics. They are not treated as 0.
Here is a live forecast of an outdoor Seniori field. Place(P50) is the typical finish; Place(P25–P75) is the ordinary spread of simulated days. Goals and the short verdict appear for --me.

How it works
Data
| Source | Use |
|---|---|
/tournaments/{id}/registration/all |
Field: name, club, style, category |
/archers/info/{id}/?sezona= |
Scores, dates, personal best |
/tournaments/{id}/archers |
Actual KV scores; KV vs EL place for the elimination shift |
/tournaments/?sezona= |
Official SSS list (Zvanični turniri SSS only) for --events backtests |
--round indoor18 keeps Indoor 18m (max 600). --round outdoor70 keeps Outdoor 70/50m (max 720). Scores below 45% of max (270 indoor / 324 outdoor) are treated as incomplete. In validation, a result is also incomplete if it falls 100 points (or 4σ) below the prediction — a walk-off that still cleared 45%.
Qualification score
Last 24 matching scores, exponential half-life of 120 days:
weight = 0.5 ** (days_ago / 120)
base = weighted average of scores
Then the named adjustments in config.py (ADJ):
| Weight | Default | Effect |
|---|---|---|
recent_delta |
0.45 | Pull toward the last score: + 0.45 × (last − base) |
pb_delta |
0.10 | Small lift toward personal best, only if PB is above base |
trend_points |
0.10 | Linear trend × 30 days |
age_penalty |
0.35 | 0.35 × \|age − 28\| (peak age 28) |
uncertainty |
1.0 | Penalty for a thin sample: 4 if n < 3, else 2 / √n |
cup |
0.0 | Reserved for a JRLT / ranking-cup prior; unused |
So:
μ = base
+ 0.45 × (last − base)
+ 0.10 × max(0, PB − base)
+ 0.10 × trend_30d
− age_penalty
− uncertainty_penalty
σ is the sample standard deviation, at least 3, and at least 12 when there are fewer than three scores (so “safe” can sit below a two-start worst score on purpose).
Goals for --me:
- Safe ≈ μ − 0.674σ (about the 25th percentile of your score)
- Realistic ≈ μ
- Stretch ≈ μ + 0.674σ
Place
Each Monte-Carlo iteration draws every archer’s qualification from N(μ, σ), ranks the field, then shifts place using elimination history:
place = qualification_place − 0.35 × mean(KV_place − EL_place)
A positive KV−EL (you climb in matches) improves predicted place. Eliminations never change the score prediction; they only move place.
Place(P50) is the median of those simulated places. Place(P25–P75) is the middle half of days. # is just sort order after that (ties broken by predicted score). Percentiles (Top 25% / 33% / 50%, bottom-25% risk) exist because fields are 12 people one week and 30 the next — absolute “top 8” would lie.
Club team forecast: sum of the top three same-category scores per club. Mixed team: best man + best woman from the same club (the counterpart category is fetched automatically).

Backtest
--validate on one URL, or --validate --events N on the last N finished official SSS events of that round:
- Cut history at the event date (no peeking).
- Compare predicted vs actual score and place.
- MAE = typical miss. Bias = mean (pred − actual); positive means the model is too high.
One event looks like this — predicted vs actual, with no-history and incomplete rounds left out of MAE:

Across a season, the summary is one line per official SSS event:

On official outdoor 70m, Seniori landed around MAE score 21, bias about −7, place 0.7. Seniorke were noisier (MAE score ~31, place ~1.3). Indoor place error is larger because fields are packed. Bias was not stable enough across men/women and indoor/outdoor to retune ADJ.
Who might use it
- Archers on the Serbian calendar who want a goal the night before KV, not a fantasy PB.
- Coaches looking at a registration list and asking who is actually in the mix.
- Anyone with a
serbianarchery.comregistration URL, matching style / category / round.
It is a forecasting aid. It does not know injuries, sight settings, or who will walk off. First outdoor (or a long break) is reported as no history, not as a score of 0.
Future ideas
- JRLT / ranking-cup as a weak prior (not wired).
- Calibrate
ADJonly if bias stays the same sign across indoor/outdoor and men/women. - Athlete floor so Safe never drops below a chosen minimum.
- Plots of place distributions; JSON output.
- Training rounds as a weak extra signal.
- More round formats if the calendar needs them.
- Uuser-friendly UI.