← Retour à l'épreuve

Kazakhstan FAIO 2025 · Correction IOAI Training

Problem 1: Stats 101

Cette page donne toute la méthode. Lisez-la une fois que vous avez cherché.

Métrique
exact answer
Évaluée sur
written answers
Notre score
1.5 1 1, verified
Niveau attendu
the exact line (10 points)

Niveau attendu atteint

Nous avons résolu l'épreuve nous-mêmes : téléchargé les données, mené les expériences et mesuré le score ci-dessous. L'énoncé et les données appartiennent à leurs organisateurs. Cette correction est rédigée en anglais.

The problem in brief

Ten students each say how many pets they own, giving the list 2,0,1,3,2,1,0,4,1,12, 0, 1, 3, 2, 1, 0, 4, 1, 1. You must summarise this list with three classic "typical value" statistics: the arithmetic mean, the mode and the median. It is the warm-up task of the Fizmat AI Olympiad 2025 (Kazakhstan) online selection, so the computation itself is short. The interesting part is understanding why there are three different answers to "what is a typical value?", and why machine learning cares about all three.

How you are scored

The answer is a single line with three numbers separated by spaces: mean, then mode, then median. A fully correct line earns 10 points; this is an exact-answer task, there is no partial metric.

Our answer, checked three independent ways in Python (exact fractions, the statistics module and NumPy):

1.5 1 1\boxed{\texttt{1.5 1 1}}

First look at the data

Before computing anything, draw the data. Each dot below is one student.

Dot plot of the ten answers: two students have 0 pets, four have 1, two have 2, one has 3, one has 4. The mean 1.5 sits to the right of the median and mode, both equal to 1.

Three things jump out:

  • The values pile up on the left (0, 1, 2) and a thin tail stretches to the right (3 and 4). Such a distribution is called right-skewed.
  • The tallest column is at 1: that will be the mode.
  • The tail pulls the mean to the right of the bulk of the data, so we should expect mean >> median. A picture lets you predict the order of the answers before doing any arithmetic, which is an excellent sanity check.

The frequency table makes everything else easy:

pets vv01234total
students with vv pets2421110

Question by question

1. The mean

The mean xˉ\bar{x} is the sum of the values divided by how many there are:

xˉ=1n∑i=1nxi=2+0+1+3+2+1+0+4+1+110=1510=1.5.\bar{x} = \frac{1}{n}\sum_{i=1}^{n} x_i = \frac{2+0+1+3+2+1+0+4+1+1}{10} = \frac{15}{10} = 1.5 .

With the frequency table the sum is quicker: 0⋅2+1⋅4+2⋅2+3⋅1+4⋅1=150\cdot 2 + 1\cdot 4 + 2\cdot 2 + 3\cdot 1 + 4\cdot 1 = 15.

A mean of 1.5 pets is not a value any student can have. That is normal: the mean is a balance point, not an observed value. If you put the ten dots on a weightless ruler, it balances exactly at 1.5, because the deviations on each side cancel:

∑i(xi−xˉ)=15−10×1.5=0.\sum_i (x_i - \bar{x}) = 15 - 10 \times 1.5 = 0 .

Classic trap: the two students with zero pets still count. Dividing by the 8 pet owners gives 15/8=1.87515/8 = 1.875, which answers a different question ("how many pets does a pet owner have on average?").

Answer: 1.5.

2. The mode

The mode is the value that appears most often. From the table, 1 appears four times and no other value appears more than twice, so the mode is unique.

Answer: 1. Note that the mode is the value 1, not its frequency 4: confusing the two is the most common slip on this question.

(A list can have several modes when there is a tie for the top frequency, for example 1,1,2,21,1,2,2. Here there is no tie, so the answer is unambiguous.)

3. The median

First sort the list, the step people most often forget:

0, 0, 1, 1, 1, 1⏟5th and 6th, 2, 2, 3, 4.0,\ 0,\ 1,\ 1,\ \underbrace{1,\ 1}_{\text{5th and 6th}},\ 2,\ 2,\ 3,\ 4 .

With n=10n = 10 (even) there is no single middle element, so the median is the average of the two central ones, positions n/2=5n/2 = 5 and n/2+1=6n/2 + 1 = 6:

median=x(5)+x(6)2=1+12=1.\text{median} = \frac{x_{(5)} + x_{(6)}}{2} = \frac{1 + 1}{2} = 1 .

(For odd nn it would simply be the element at position (n+1)/2(n+1)/2.)

Answer: 1.

A subtle point worth noticing: the statement's informal definition says "50 % of the values are smaller and 50 % are larger". With ties this cannot hold literally: here only 2 values are below 1, 4 are equal to 1 and 4 are above. The precise version is: at least half of the values are ≤\le the median and at least half are ≥\ge it. Check: 6 of 10 values are ≤1\le 1, 8 of 10 are ≥1\ge 1.

The key idea

Why do statisticians and ML practitioners keep three different "centres"? Because each one is the best single guess for a different way of measuring error. Imagine you must predict every student's number of pets with one constant cc. How good cc is depends on how you are penalised:

loss for one studenttotal loss L(c)L(c)best constant cc
squared error (xi−c)2(x_i - c)^2∑i(xi−c)2\sum_i (x_i - c)^2the mean
absolute error ∣xi−c∣\lvert x_i - c \rvert∑i∣xi−c∣\sum_i \lvert x_i - c \rvertthe median
0-1 error: 1 if c≠xic \ne x_i, else 0n−#{i:xi=c}n - \#\{i : x_i = c\}the mode

The three total losses as functions of the constant guess c, computed on the ten answers. The squared error is a parabola with its lowest point at 1.5; the absolute error is a broken line with a corner at 1; the 0-1 error bars are lowest at 1.

Here is why each line of the table is true.

Squared error gives the mean. L(c)=∑i(xi−c)2L(c) = \sum_i (x_i - c)^2 is a parabola in cc. Setting its derivative to zero,

L′(c)=−2∑i(xi−c)=0  ⟺  c=1n∑ixi=xˉ.L'(c) = -2\sum_i (x_i - c) = 0 \iff c = \frac{1}{n}\sum_i x_i = \bar{x}.

On our data the minimum value is L(1.5)=14.5L(1.5) = 14.5.

Absolute error gives the median. L(c)=∑i∣xi−c∣L(c) = \sum_i \lvert x_i - c\rvert is a broken line. Each point to the left of cc adds +1+1 to the slope, each point to the right adds −1-1, so

slope of L at c=#{xi<c}−#{xi>c}.\text{slope of } L \text{ at } c = \#\{x_i < c\} - \#\{x_i > c\}.

Just below c=1c = 1 the slope is 2−8=−62 - 8 = -6 (going down), just above it is 6−4=+26 - 4 = +2 (going up). The function turns around exactly at 1: the median, where the points on each side "balance in number" rather than in distance. Minimum value: L(1)=9L(1) = 9.

0-1 error gives the mode. You are wrong for every student whose value differs from cc, so you want the cc shared by the most students: 1 (wrong 6 times, against 8 or 9 times for any other choice).

This explains the examples in the statement. Training a regressor with MSE pushes it toward conditional means; training with MAE pushes it toward conditional medians; a classifier that maximises accuracy, or an ensemble deciding by majority vote, outputs the mode of its votes.

It also explains robustness. Suppose the student with 4 pets had typed 40 by mistake. The squared loss punishes that single point enormously, so the mean jumps from 1.5 to 5.1. The absolute loss only counts which side each point is on, so the median stays at 1, and so does the mode. That is why salaries and house prices are usually reported with medians, and why MAE is preferred over MSE when the data contain outliers.

The full solution

The whole task fits in a few lines; the point of solution.py is to check each answer independently rather than trust one computation:

from fractions import Fraction
from collections import Counter
import statistics, numpy as np

pets = [2, 0, 1, 3, 2, 1, 0, 4, 1, 1]
mean = Fraction(sum(pets), len(pets))             # exact: 3/2
counts = Counter(pets)                            # {1: 4, 0: 2, 2: 2, 3: 1, 4: 1}
modes = [v for v, k in counts.items() if k == max(counts.values())]
s = sorted(pets); n = len(s)
median = (s[n//2 - 1] + s[n//2]) / 2              # n even: average the middle two

assert mean == statistics.mean(pets) == np.mean(pets) == 1.5
assert modes == statistics.multimode(pets) == [1]
assert median == statistics.median(pets) == np.median(pets) == 1
print(f"{float(mean):g} {modes[0]} {median:g}")   # 1.5 1 1

The script also verifies the key idea by brute force: it evaluates the squared and absolute losses on a grid of 50 001 constants between −0.5-0.5 and 4.54.5 and finds their minima at 1.51.5 and 1.01.0, and computes the 0-1 loss for each integer value (8, 6, 8, 9, 9 errors for c=0,…,4c = 0, \dots, 4).

Final line to submit:

1.5 1 1

Results

approachmeanmodemedianverdict
correct computation (hand, statistics, NumPy agree)1.511correct, 10 points
divide by pet owners only (forget the zeros)1.87511wrong mean
report the frequency of the mode instead of the mode1.541wrong mode
take the median of the unsorted list (5th and 6th entries: 2 and 1)1.511.5wrong median
take only the 5th sorted value, or only the 6th1.511right here by luck (both are 1), wrong in general
robustness check: the "4" replaced by an outlier "40"5.111mean moves a lot, median and mode do not

What to remember

  • Look before you compute. A dot plot or frequency table predicts the shape (here right-skewed, so mean >> median) and catches arithmetic slips.
  • Mean, median and mode are the best constant predictions under squared, absolute and 0-1 loss respectively. Choosing a loss function is choosing which "centre" your model aims for.
  • The mean is sensitive to outliers, the median and mode are not. Prefer the median (and MAE) when a few extreme values could dominate.
  • Mechanics matter: sort before taking a median, average the two middle values when nn is even, and keep zero values in the count.
  • Verify a hand answer with an independent computation (here exact fractions plus two libraries): it costs seconds and is the habit that saves points in every later, harder problem.
Code complet (solution.py)
"""FAIO 2025 qualification, Problem 1 "Stats 101": mean, mode, median.

Computes the three answers in several independent ways (hand formula, the
standard library, NumPy), checks the "loss minimiser" view of each statistic
by brute force, and draws the figures used in the correction.
Run:  python solution.py
"""
from collections import Counter
from fractions import Fraction
import statistics

import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt

pets = [2, 0, 1, 3, 2, 1, 0, 4, 1, 1]
n = len(pets)

# ---------------------------------------------------------------- 1. mean
mean_exact = Fraction(sum(pets), n)                  # exact rational: 15/10
assert mean_exact == Fraction(3, 2)
assert statistics.mean(pets) == np.mean(pets) == 1.5

# ---------------------------------------------------------------- 2. mode
counts = Counter(pets)                               # value -> frequency
top = max(counts.values())
modes = sorted(v for v, c in counts.items() if c == top)
assert modes == [1] and top == 4                     # unique mode, seen 4 times
assert statistics.multimode(pets) == [1]

# -------------------------------------------------------------- 3. median
s = sorted(pets)                                     # 0 0 1 1 1 1 2 2 3 4
# n is even, so the median is the average of the two middle values
median = (s[n // 2 - 1] + s[n // 2]) / 2
assert median == statistics.median(pets) == np.median(pets) == 1.0
below = sum(x < median for x in pets); above = sum(x > median for x in pets)
print(f"values below the median: {below}, above: {above}, equal: {n-below-above}")

print("frequencies:", dict(sorted(counts.items())))
print("sorted:", s)
answer = f"{float(mean_exact):g} {modes[0]} {median:g}"
print("ANSWER:", answer)                              # 1.5 1 1

# ------------------------- 4. each statistic minimises a loss (brute force)
c = np.linspace(-0.5, 4.5, 50001)                     # candidate constants
x = np.array(pets)[:, None]
sq = ((x - c) ** 2).sum(0)                           # squared error  -> mean
ab = np.abs(x - c).sum(0)                            # absolute error -> median
ints = np.arange(0, 5)
zero_one = np.array([(np.array(pets) != k).sum() for k in ints])  # 0-1 loss -> mode
print("min squared loss:", sq.min().round(3), " min absolute loss:", ab.min().round(3))
print("argmin squared :", round(c[sq.argmin()], 4))
print("argmin absolute:", round(c[ab.argmin()], 4), " (set of minimisers:",
      round(c[np.isclose(ab, ab.min())].min(), 4), "to",
      round(c[np.isclose(ab, ab.min())].max(), 4), ")")
print("0-1 loss per value:", dict(zip(ints.tolist(), zero_one.tolist())))
assert abs(c[sq.argmin()] - 1.5) < 1e-3
assert abs(c[ab.argmin()] - 1.0) < 1e-3
assert ints[zero_one.argmin()] == 1

# ----------------------------------------- 5. robustness: one outlier
out = [40 if v == 4 else v for v in pets]       # the "4 pets" student now says 40
print(f"with an outlier: mean {np.mean(out):.1f}, median {np.median(out):g}, "
      f"mode {statistics.multimode(out)}")

# ----------------------------------------------------------------- figures
plt.rcParams.update({"font.size": 13, "axes.spines.top": False,
                     "axes.spines.right": False})
C_MEAN, C_MED, C_MODE = "#d1495b", "#2e86ab", "#3a7d44"

# Figure 1: dot plot of the 10 answers with the three statistics
fig, ax = plt.subplots(figsize=(8.5, 4.2), dpi=150)
for v, k in counts.items():
    ax.scatter([v] * k, np.arange(1, k + 1), s=420, color="#8d99ae",
               edgecolor="white", zorder=3)
ax.axvline(1.5, color=C_MEAN, lw=2.5, label="mean = 1.5 (balance point)")
ax.axvline(1.0, color=C_MED, lw=2.5, ls="--", label="median = 1 (middle of the sorted list)")
ax.scatter([1], [top + 0.85], marker="v", s=260, color=C_MODE, zorder=4,
           label="mode = 1 (tallest column)")
ax.set_xticks(range(5)); ax.set_yticks(range(0, 6))
ax.set_ylim(0, 5.6); ax.set_xlim(-0.6, 4.6)
ax.set_xlabel("number of pets"); ax.set_ylabel("number of students")
ax.set_title("The 10 answers: one dot per student")
ax.legend(loc="upper right", frameon=False, fontsize=11)
fig.tight_layout(); fig.savefig("figures/dotplot.png", facecolor="white")

# Figure 2: the three losses as a function of the single guess c
fig, axs = plt.subplots(1, 3, figsize=(13, 4.3), dpi=150)
axs[0].plot(c, sq, color=C_MEAN, lw=2.5)
axs[0].axvline(1.5, color=C_MEAN, ls=":"); axs[0].set_title("squared error\nminimum at the mean, 1.5", fontsize=13)
axs[0].set_ylabel("total loss over the 10 students")
axs[1].plot(c, ab, color=C_MED, lw=2.5)
axs[1].axvline(1.0, color=C_MED, ls=":"); axs[1].set_title("absolute error\nminimum at the median, 1", fontsize=13)
axs[2].bar(ints, zero_one, color=[C_MODE if k == 1 else "#c9d1d9" for k in ints])
axs[2].set_title("0-1 error (count of wrong guesses)\nminimum at the mode, 1", fontsize=13)
for a in axs:
    a.set_xlabel("constant guess c")
fig.tight_layout(); fig.savefig("figures/losses.png", facecolor="white")
print("figures written")