Back to WCA Statistics

Moving average

Recent

moving_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

α = 0.8
Decay factor
Each step back in time discounts by 0.8
~5
Effective window
Most recent 5 averages contribute ~2/3 of total weight
≥ 5
Min averages
Solvers with under 5 career averages are dropped
50
Rows per event
Top 50 per event

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.

sql
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

1
Sort the whole table chronologically
SQL pre-sorts the entire table by 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.
2
Iterate EMA per person
Start 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.
3
Bias correction
Early on, 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.
4
Filter < 5, sort, top 50
.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

EMA recurrence + bias correction
avgₜ = α · avgₜ₋₁ + (1 − α) · numₜ ; EMA = avg_n / (1 − αⁿ)
α = 0.8, n = number of averages the solver has posted. Iterate, then divide the final value by 1 − αⁿ to correct the warm-up underestimate.

Caveats & edges

Related stats & links