Eight numbers. Seven of them agree and one does not, which is what real data looks like.
values = [24, 25, 26, 26, 27, 28, 29, 300] sum(values) / len(values) # -> 60.625 the mean # nobody in this list is anywhere near 60 ordered = sorted(values) (ordered[3] + ordered[4]) / 2 # -> 26.5 the median # Eight values, so the middles are at 3 and 4 — len // 2 is 4, and the # other one is the index below it. len(values) // 2 # -> 4
- The mean moves with every value; the median moves with the order
One number a hundred times too large drags the mean a long way and the median barely at all — which is why the median is the honest summary of anything with a long tail: salaries, response times, house prices. The mean is right when you want the total shared out, and that is a different question from 'what is typical'.
- An even-length median is not one of the values
There is no middle element, so the answer is the average of the two that straddle the middle — 26.5 in a list of integers. That surprises people who expect the median to be a value from the data, and it is the case most hand-written medians get wrong, because the odd-length version works and looks finished.
- sorted(), not .sort()
The median needs the values in order and the caller did not ask for their list to be reordered. sorted() builds a new list; .sort() rearranges theirs — and the second one is particularly nasty here, because it leaves a function that computes a correct median and quietly destroys the order of whatever it was measuring.
Refuse the empty list first, so everything after it can divide by the count without worrying. The mean is one line; the median needs the values in order and then depends on whether there is a middle one.
ordered = sorted(values) — a new list, not values.sort(). The middle index is len(values) // 2. For an odd length that index *is* the median; for an even length the median is the mean of that value and the one before it.
middle = count // 2 works for both cases: odd lengths want ordered[middle], even lengths want (ordered[middle - 1] + ordered[middle]) / 2. Test the parity with count % 2 rather than trying to find one expression that covers both.
def summary(values): count = len(values) if count == ____: raise ____('no values to summarise') ordered = ____(values) middle = count // ____ if count % 2: median = ordered[____] else: median = (ordered[middle - 1] + ordered[middle]) / ____ return { 'count': count, 'mean': sum(values) / count, 'median': median, }
def summary(values):
count = len(values)
if count == 0:
raise ValueError("no values to summarise")
# sorted(), so the caller's list keeps its own order.
ordered = sorted(values)
middle = count // 2
if count % 2:
median = ordered[middle]
else:
median = (ordered[middle - 1] + ordered[middle]) / 2
return {
"count": count,
"mean": sum(values) / count,
"median": median,
}
For an odd length it lands exactly on the middle element; for an even length it lands on the upper of the two, so the lower is one index back. That is why no separate arithmetic is needed per case — and why `middle - 1` rather than `middle + 1` is the right neighbour, which is the easiest thing here to get backwards.
Without it, sum([]) / 0 raises ZeroDivisionError — technically a refusal, and one that says nothing about values or averages. Worse, ordered[middle] on an empty list raises IndexError, so which exception you get depends on which line happens to run first. Refusing up front makes the error deterministic and readable.
An odd count gives 1, which is truthy, so `if count % 2:` reads as 'if there is a middle one'. Writing == 1 is equally correct and a little louder; writing `!= 0` is the same thing again. What to avoid is `if count % 2 == 1` for negative counts — impossible here, and a real trap in code where the number can be negative, because Python’s modulo returns a non-negative result.
The statistics module has mean, median, median_low, median_high and fmean, all tested and all handling the empty case with a StatisticsError. Use them. Write this once to know what median_low and median_high are *for* — they are the two answers this exercise averages, and sometimes you want an actual data point rather than a number between two of them. For anything numeric and large, numpy.median is dramatically faster; and if you are reaching for the mean of a long-tailed distribution at all, the question to ask is whether you wanted a percentile.
- Add the mode, and decide what to return when two values tie for the most common — there is no single right answer, which is why statistics.mode and multimode both exist.
- Add a trimmed mean that drops the top and bottom 10% before averaging, and check it against the outlier case.
- Return the median without sorting the whole list, using the quickselect idea, and measure whether it is actually faster at the sizes you care about.