Python: от данных до своей сводки Урок 38 из 56
Среднее, медиана, разброс: когда среднее врёт
Тридцать седьмой урок курса по Python и начало модуля про деньги. Десять стран, один год: среднее 20.48 %, медиана 13.88 % — и среднее выше, чем было у восьми стран из десяти. `mean`, `median`, `std`, четверти и правило, по которому меру выбирают до того, как увидят результат.
Зачем это нужно
Ряд из одиннадцати чисел в заголовок не поместится, и его сворачивают в одно. Вопрос не в том, сворачивать ли, а в том, что именно вы при этом теряете — и не потеряется ли вместе с этим правда.
«Средняя инфляция по региону — 20 %» звучит как описание региона. На настоящих числах 2022 года это описание ровно одной страны из десяти: у восьми инфляция была ниже среднего, а среднее туда затащила Турция с её семьюдесятью двумя процентами.
Этот урок — про три меры, которые отвечают на три разных вопроса, и про правило, какую из них ставить в заголовок.
Сразу целиком
Файл srednee.py. Два настоящих ряда: инфляция Казахстана по годам и инфляция десяти стран за 2022 год, оба — из данных Всемирного банка.
"""Урок 37: среднее, медиана, разброс — и когда среднее врёт.
Два ряда настоящих чисел: инфляция в Казахстане по годам и инфляция десяти
стран за 2022 год. Источник обоих — Всемирный банк, показатель FP.CPI.TOTL.ZG.
"""
import numpy as np
import pandas as pd
# Казахстан, годовая инфляция в процентах, одиннадцать лет.
kazakhstan = pd.Series(
[6.85, 6.68, 14.36, 7.44, 6.16, 5.33, 6.72, 8.04, 15.03, 14.53, 8.69],
index=[2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024],
name="инфляция",
)
# Тот же показатель, другой срез: один год и десять стран.
year_2022 = pd.Series(
{
"Казахстан": 15.03, "Россия": 13.74, "Узбекистан": 11.45,
"Кыргызстан": 13.92, "Турция": 72.31, "Грузия": 11.90,
"Армения": 8.64, "Азербайджан": 13.85, "Беларусь": 15.21,
"Молдова": 28.74,
},
name="инфляция 2022",
)
print("== Одиннадцать лет одной страны")
print(" среднее:", round(kazakhstan.mean(), 2))
print(" медиана:", round(kazakhstan.median(), 2))
print(" разброс:", round(kazakhstan.std(), 2))
print(" от", kazakhstan.min(), "до", kazakhstan.max(), "— размах", round(kazakhstan.max() - kazakhstan.min(), 2))
print()
print("== Один год, десять стран")
mean_2022 = year_2022.mean()
median_2022 = year_2022.median()
print(" среднее:", round(mean_2022, 2))
print(" медиана:", round(median_2022, 2))
print(" ниже среднего:", int((year_2022 < mean_2022).sum()), "из", len(year_2022))
print(" ниже медианы:", int((year_2022 < median_2022).sum()), "из", len(year_2022))
print()
print("== Что делает с ними одна страна")
without = year_2022.drop("Турция")
print(" с Турцией: среднее", round(mean_2022, 2), "| медиана", round(median_2022, 2))
print(" без Турции: среднее", round(without.mean(), 2), "| медиана", round(without.median(), 2))
# Сдвиг считаем по тем числам, которые читатель видит выше, а не по скрытым:
# иначе строка "сдвинулась на 0.04" не сойдётся с 13.88 − 13.85.
print(" среднее сдвинулось на", round(round(mean_2022, 2) - round(without.mean(), 2), 2))
print(" медиана сдвинулась на", round(round(median_2022, 2) - round(without.median(), 2), 2))
print()
print("== Разброс: два ответа на один вопрос")
quarters = year_2022.quantile([0.25, 0.5, 0.75])
print(" std:", round(year_2022.std(), 2))
print(" четверти:", round(quarters[0.25], 2), "|", round(quarters[0.5], 2), "|", round(quarters[0.75], 2))
print(" межквартильный размах:", round(quarters[0.75] - quarters[0.25], 2))
print()
print("== Одно слово, два числа")
print(" pandas .std():", round(kazakhstan.std(), 4), "— делит на n − 1")
print(" numpy .std():", round(float(np.std(kazakhstan)), 4), "— делит на n")
print(" pandas std(ddof=0):", round(kazakhstan.std(ddof=0), 4))
print()
print("== Одна строка вместо шести")
print(year_2022.describe().round(2).to_string())
Выводит:
== Одиннадцать лет одной страны
среднее: 9.08
медиана: 7.44
разброс: 3.69
от 5.33 до 15.03 — размах 9.7
== Один год, десять стран
среднее: 20.48
медиана: 13.88
ниже среднего: 8 из 10
ниже медианы: 5 из 10
== Что делает с ними одна страна
с Турцией: среднее 20.48 | медиана 13.88
без Турции: среднее 14.72 | медиана 13.85
среднее сдвинулось на 5.76
медиана сдвинулась на 0.03
== Разброс: два ответа на один вопрос
std: 18.97
четверти: 12.36 | 13.88 | 15.16
межквартильный размах: 2.81
== Одно слово, два числа
pandas .std(): 3.6851 — делит на n − 1
numpy .std(): 3.5136 — делит на n
pandas std(ddof=0): 3.5136
== Одна строка вместо шести
count 10.00
mean 20.48
std 18.97
min 8.64
25% 12.36
50% 13.88
75% 15.16
max 72.31
Разбор
Среднее отвечает на вопрос «сколько бы вышло у каждого, если поровну»
Среднее — это сумма, разделённая поровну. Оно единственное из трёх мер, у которого есть прямой смысл, когда важна сумма: общий счёт, общий расход, общий сбор налога. Если вопрос звучит «сколько всего и на сколько человек», ответ даёт среднее, и никакая другая мера его не заменит.
Цена за это — чувствительность. Одно большое значение тянет среднее к себе тем сильнее, чем оно больше и чем меньше наблюдений.
Медиана отвечает на вопрос «а как у середины»
Медиана — центральное значение упорядоченного ряда (или среднее двух центральных значений при чётном числе наблюдений). Крайнее значение можно сделать ещё больше, не изменив медиану, пока оно остаётся по ту же сторону от середины; изменение центральных значений медиану сдвинет.
В программе это видно измеренным. Уберите из десяти стран Турцию:
с Турцией: среднее 20.48 | медиана 13.88
без Турции: среднее 14.72 | медиана 13.85
Среднее сдвинулось на 5.76 процентного пункта, медиана — на 0.03. Одна страна из десяти переписала «средний» ответ почти на треть, а «серединный» не заметила.
Когда среднее врёт
Не всегда, и не само по себе: среднее врёт, когда его выдают за типичное значение, а ряд несимметричный. Проверка занимает одну строку:
ниже среднего: 8 из 10
ниже медианы: 5 из 10
Медиана делит упорядоченный ряд на две половины, но при совпадающих значениях строго ниже неё может оказаться не ровно половина наблюдений. Если под средним оказалось восемь из десяти, значит у ряда длинный хвост вверх, и «в среднем 20 %» описывает не регион, а его единственную крайность.
Такой же перекос виден и в одиннадцати годах Казахстана: среднее 9.08 против медианы 7.44 — три всплеска из одиннадцати лет подняли среднее на полтора с лишним пункта над серединой.
Разброс: одно число против четырёх
std — квадратный корень из среднего квадрата отклонений от среднего (с поправкой ddof, описанной ниже), а не среднее расстояние. Выброс раздувает его вместе со средним. У десяти стран std равен 18.97 при медиане 13.88 — разброс больше самой середины.
Четверти устроены иначе. Первая четверть — значение, ниже которого четверть ряда; третья — ниже которого три четверти; расстояние между ними называют межквартильным размахом:
четверти: 12.36 | 13.88 | 15.16
межквартильный размах: 2.81
Центральные 50 % стран умещаются в полосу шириной меньше трёх пунктов. Межквартильный размах устойчив к выбросам, а стандартное отклонение чувствительно к ним; обе меры описывают разброс, но отвечают на разные вопросы.
Размах (max - min) полезен другим: он называет границы, а не типичное расстояние. В заголовке он честен только вместе с числом наблюдений.
Одно слово, два числа
Пара строк, из-за которых цифры в отчётах расходятся у двух людей с одним рядом:
pandas .std(): 3.6851 — делит на n − 1
numpy .std(): 3.5136 — делит на n
Это не ошибка ни в одной из библиотек. Когда ряд — вся совокупность, делят на n; когда ряд — выборка, по которой судят о большем целом, делят на n − 1. pandas по умолчанию считает ваши данные выборкой, numpy — совокупностью.
Разница в третьем знаке кажется мелочью ровно до того дня, когда её увидит человек, считавший другой библиотекой. Поэтому в отчёте рядом с разбросом пишут, чем он посчитан, а в коде ставят ddof явно.
describe() — одна строка вместо шести
Series.describe() печатает количество, среднее, разброс, минимум, три четверти и максимум. Это первое, что делают с незнакомым рядом: восемь чисел показывают и центр, и разброс, и перекос сразу.
Для отчёта из него берут два-три числа. Для собственного взгляда на данные — читают все восемь.
Что ставить в заголовок
Правило, которое стоит записать в код, а не держать в голове:
- Вопрос про сумму — среднее. «Сколько всего собрали и сколько на каждого» отвечает только оно.
- Вопрос про типичное — сначала посмотрите на распределение, квантили и выбросы. Близость среднего и медианы полезна, но сама по себе не делает меры взаимозаменяемыми: выбор зависит от смысла вопроса и цены крайних значений.
- Рядом с любой мерой — число наблюдений: 20 % по десяти странам и 20 % по сотне — это разные утверждения.
- Если ряд несимметричный, одной меры мало: медиана плюс четверти говорят то, что среднее плюс
stdскрывают.
Карта урока
Скажите своими словами
Не подглядывая, ответьте вслух или на бумаге. Ответы — в конце урока.
- Почему медиана почти не сдвинулась, когда из ряда убрали Турцию, а среднее сдвинулось на 5.76?
- Что значит «ниже среднего оказалось восемь из десяти» и о чём это говорит применительно к ряду?
- Почему
.std()в pandas и в numpy дают разные числа для одного ряда?
Разминка
Три коротких шага перед заданием: предсказать, дописать, починить. Ответы — в конце урока, но сначала ответьте сами.
1. Предскажите. Что напечатает эта программа?
import pandas as pd
pay = pd.Series([120000, 130000, 140000, 150000, 900000])
print("среднее:", int(pay.mean()))
print("медиана:", int(pay.median()))
print("ниже среднего:", int((pay < pay.mean()).sum()), "из", len(pay))
2. Заполните пропуск. Вместо ... поставьте меру, которую не сдвинет один дорогой чек.
# пять чеков, один из них — холодильник
import pandas as pd
prices = pd.Series([320, 340, 350, 360, 4800])
measure = ...
print("мера, которую не сдвинул один дорогой чек:", measure)
3. Почините. Программа печатает два разных разброса для одного ряда и объявляет одно из чисел ошибкой.
# «одна из библиотек считает неправильно» — нет, они считают разное
import numpy as np
import pandas as pd
row = pd.Series([6.85, 6.68, 14.36, 7.44, 6.16])
print("pandas:", round(row.std(), 4))
print("numpy: ", round(float(np.std(row)), 4))
print("совпало:", round(row.std(), 10) == round(float(np.std(row)), 10))
Задание
Обязательное. Возьмите три ряда: одиннадцать лет Казахстана, спокойные пять лет из них (2017–2021) и десять стран за 2022 год. Для каждого напечатайте число наблюдений, среднее, медиану, разброс, границы и межквартильный размах — и меру, которая пойдёт в заголовок. Меру выбирает правило в коде: если среднее ушло от медианы больше чем на десятую часть медианы, в заголовок идёт медиана. В конце — describe() всех трёх рядов рядом.
Ожидаемый вывод:
== Казахстан, 2014–2024
наблюдений: 11
среднее: 9.08 | медиана: 7.44
разброс: 3.69 | от 5.33 до 15.03
межквартильный размах: 4.82
в заголовок: медиана 7.44 %
ниже среднего: 8 из 11
== Казахстан, 2017–2021
наблюдений: 5
среднее: 6.74 | медиана: 6.72
разброс: 1.06 | от 5.33 до 8.04
межквартильный размах: 1.28
в заголовок: среднее 6.74 %
ниже среднего: 3 из 5
== Десять стран, 2022
наблюдений: 10
среднее: 20.48 | медиана: 13.88
разброс: 18.97 | от 8.64 до 72.31
межквартильный размах: 2.81
в заголовок: медиана 13.88 %
ниже среднего: 8 из 10
== Почему не одна мера на всё
11 лет 5 спокойных 10 стран
count 11.00 5.00 10.00
mean 9.08 6.74 20.48
std 3.69 1.06 18.97
min 5.33 5.33 8.64
25% 6.70 6.16 12.36
50% 7.44 6.72 13.88
75% 11.52 7.44 15.16
max 15.03 8.04 72.31
Готово, когда: вывод совпадает построчно; меру выбирает функция, а не вы глазами; правило умеет сказать и «среднее» — на спокойных пяти годах оно так и говорит; число наблюдений печатается рядом с мерой, а не отдельно от неё.
На своих данных. Возьмите любой свой ряд — расходы по дням, время в пути, вес, счета за свет. Посчитайте среднее и медиану. Если они разошлись больше чем на десятую часть, найдите в ряду то значение, которое их разводит, и решите: это ошибка ввода, редкое событие или обычная жизнь. От ответа зависит, убирать его или оставить, — и это решение стоит записать рядом с числом.
По желанию.
- Посчитайте
stdсddof=0иddof=1на своём ряду и посмотрите, с какого числа наблюдений разница перестаёт быть заметной. - Постройте по своему ряду
quantile([0.1, 0.5, 0.9])и сравните полосу «восемь из десяти» с границамиmin–max. - Возьмите ряд из тридцатого урока с пропусками и проверьте, что
mean()считает по непустым значениям, а не по длине ряда.
Куда это встанет в проекте
Семнадцатый шаг, и с него начинается пятый модуль: сводка перестаёт показывать только последний год.
В sholu/esep.py появляется ozara — по каждой стране считает число наблюдений, среднее, медиану, разброс и межквартильный размах, а рядом ставит меру, выбранную тем же правилом, что и в задании. Страница отчёта получает второй блок — «за все годы», под таблицей последнего года.
Что правило сказало на настоящих данных сводки:
KAZ 5 лет среднее 11.54 медиана 11.39 разброс 3.22 → среднее
RUS 5 лет среднее 8.69 медиана 8.43 разброс 3.06 → среднее
UZB 5 лет среднее 10.14 медиана 9.96 разброс 1.04 → среднее
Всем трём странам оно выбрало среднее: их пятилетние ряды достаточно симметричны, медиана отходит от среднего на процент-другой. В этом и польза правила, записанного в код, — оно не подставляет «осторожную» медиану там, где данные её не требуют, и скажет обратное в тот день, когда в ряду появится всплеск вроде турецкого.
Долги. Пять точек — короткий ряд: медиана на пяти наблюдениях грубая, а разброс по пяти годам — оценка с широкой погрешностью, которую сводка нигде не называет. Это тот же разговор про число наблюдений рядом с мерой, и он вернётся в уроке о том, как читать официальную статистику.
Ответы
Показать ответы
На вопросы
- Потому что медиану определяет положение значения, а не его величина. Турция в любом случае стоит выше середины, а какое именно там число — 28 или 72 — середине безразлично. Среднее же складывает все значения, поэтому одно большое тянет его вверх тем сильнее, чем оно больше.
- Что ряд несимметричный: у него длинный хвост вверх. Под медианой всегда половина — это её определение; если под средним оказалось восемь из десяти, значит среднее ушло в сторону хвоста и типичным значением уже не является.
- Потому что они делят на разное: pandas по умолчанию считает ряд выборкой и делит на
n − 1, numpy считает его всей совокупностью и делит наn. Ни то ни другое не ошибка; ошибка — не сказать в отчёте, что именно посчитано, и поставитьddofпо умолчанию.
К разминке
- Одна зарплата в девятьсот тысяч утащила среднее выше, чем у четверых из пяти.
среднее: 288000
медиана: 140000
ниже среднего: 4 из 5
prices.median(). Медиана смотрит на положение, а не на величину: холодильник останется справа от середины, какой бы ни была его цена.
import pandas as pd
prices = pd.Series([320, 340, 350, 360, 4800])
measure = prices.median()
print("мера, которую не сдвинул один дорогой чек:", measure)
мера, которую не сдвинул один дорогой чек: 350.0
- Дело не в ошибке, а в
ddof: поставьте обеим библиотекам один и тот же делитель.
import numpy as np
import pandas as pd
row = pd.Series([6.85, 6.68, 14.36, 7.44, 6.16])
print("pandas:", round(row.std(ddof=0), 4))
print("numpy: ", round(float(np.std(row)), 4))
print("совпало:", round(row.std(ddof=0), 10) == round(float(np.std(row)), 10))
pandas: 3.0584
numpy: 3.0584
совпало: True
К заданию
Правило вынесено в функцию headline не ради красоты: пока оно в голове, каждый ряд получает ту меру, которая лучше выглядит. Записанное в код, оно отвечает одинаково на все три ряда — и на спокойных пяти годах честно говорит «среднее», хотя медиана была бы осторожнее.
Четверти в таблице describe() показывают то, чего не видно ни в среднем, ни в разбросе: у десяти стран половина ряда уместилась между 12.36 и 15.16, а std в 18.97 описывает расстояние до одного значения, а не до остальных девяти.
Источники
- Описательная статистика в pandas —
mean,median,std,quantile,describeи что они делают с пропусками. - numpy.std — параметр
ddofи почему по умолчанию он нулевой. - Инфляция потребительских цен, Всемирный банк — источник всех чисел этого урока.
Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом
Комментарии (0)
Войдите, чтобы оставить комментарий →
Пока нет комментариев. Будьте первым.