Moving average
Recentmoving_average computes an exponentially-weighted moving average (EMA) per (person, event) pair: walk through every official ao5 average that solver has posted, in date order, weighting new values by 1 − α and decaying old ones by α. Here α = 0.8, so the most recent ~5 averages dominate.
Unlike official ao5 / ao12 (a single-comp window of consecutive solves), this glides over an entire career, measuring "how strong has this solver been lately at this event". People with fewer than 5 career ao5 averages are excluded (not enough signal).
By the numbers
Data source
results join persons (sub_id = 1), competitions, round_types, ordered globally by start_date then round_type.rank (career timeline). Filters to average > 0 and drops BLD events (333bf / 333mbf / 333mbo / 444bf / 555bf, which are ranked by single, not average). Sibling stat average_of_x runs as pure-SQL sliding windows; here EMA + bias correction is done in TS.
SELECT
person.name, person.wca_id, event_id, average
FROM results result
JOIN persons person ON person.wca_id = person_id AND person.sub_id = 1
JOIN competitions competition ON competition.id = competition_id
JOIN round_types round_type ON round_type.id = round_type_id
WHERE average > 0
AND event_id NOT IN ('333bf','333mbf','333mbo','444bf','555bf')
ORDER BY competition.start_date, round_type.rank;
-- TS: per (person, event) accumulate
-- avg = avg * α + (1 - α) * new_value (α = 0.8)
-- bias-correct: corrected = avg / (1 - αⁿ)Algorithm / pipeline
start_date, round_type.rank. The TS side just needs to bucket by (person, event) — no re-sorting. round_type.rank ensures qualification → semifinal → final ordering within a comp.avg = 0. For each new num: avg = avg * α + (1 − α) * num. With α = 0.8, old values decay to 80% per step and the new one contributes 20% — but accumulated over time, the most recent 5 values contribute ~2/3 of total weight.avg is dragged down by its 0 start (the decay term has not "filled in" yet). Dividing by 1 − αⁿ (n = number of samples seen) cancels that — same trick as Adam optimizer's 1 − β₁ᵗ. Rounded to a centisecond long-int..filter(([, avgs]) => avgs.length >= 5) drops solvers with fewer than 5 averages. Sort ascending, take top 50. One section per event (BLD events absent as a group).Key formulae
1 − αⁿ to correct the warm-up underestimate.Caveats & edges
- BLD events (
333bf / 333mbf / 333mbo / 444bf / 555bf) are excluded as a group — they're ranked by single, so EMA-on-averages is meaningless. - Solvers with fewer than 5 averages are dropped. So a newcomer flying through 5 sub-7 ao5's still needs more career data before they appear.
- EMA has very short memory for a single bad blowup (it fades within a few averages). Recent decliners drop off this board faster than they would from ao12 / ao50.
- Unlike
average_of_x(a hard window of N consecutive official attempts), EMA is a soft window — there is no cliff cutoff.