В поиске интересного и простого ДатаСета я набрёл этого красавца.


Об этом красавце


В нём есть данные о росте и весе 10 000 мужчин и женщин. Никакого описания. Ничего «лишнего». Только рост, вес и метка пола. Эта таинственная простота мне понравилась.


Что ж, начнём!


Что мне было интересно?


  • В каком диапазоне вес и рост у большинства мужчин и женщин?
  • Какие они — «средний» мужчина и «средняя» женщина?
  • Сможет ли простенькая модель машинного обучения «KNN» по этим данным угадать вес по росту?

Погнали!


logo


Первый взгляд


Для начала подгрузим нужные модули


# Для работы с табличными данными
import pandas as pd

# Для моих любимых графиков
import matplotlib.pyplot as plt
%matplotlib inline

# Модель машинного обучения «К ближайших соседей»
from sklearn.neighbors import KNeighborsRegressor
# Для разбивки данных на тренировочный и тестовый наборы 
from sklearn.model_selection import train_test_split

Когда библиотеки встали ровно — пришло время загрузить сам ДатаСет и посмотреть на первые 10 элементов. Это нужно, чтобы наше нутро было спокойно, что мы всё загрузили правильно.


Кстати, не пугайтесь, что рост и вес отличаются от привычных нам. Это из-за другой системы измерений: дюймы и фунты, вместо сантиметров и килограмм.


data = pd.read_csv('weight-height.csv')
data.head(10)

Gender Height Weight
0 Male 74 242
1 Male 69 162
2 Male 74 213
3 Male 72 220
4 Male 70 206
5 Male 68 152
6 Male 69 184
7 Male 68 168
8 Male 67 176
9 Male 63 156

Хорошо! Мы видим, что первые десять записей — «мужчины». Мы видим их рост (height) и вес (weight). Данные подгрузились хорошо.


Теперь можно посмотреть на количество строк в наборе.


data.shape
>> (10000, 3)

Десять тысяч строк / записей. И у каждой по три параметра. То, что нужно!


Пришло время исправить систему измерений. Теперь тут сантиметры и килограммы.


data['Height'] *= 2.54
data['Weight'] /= 2.205

# И проверим результат
data.head(10)

Gender Height Weight
0 Male 188 110
1 Male 175 74
2 Male 188 96
3 Male 182 100
4 Male 177 94
5 Male 171 69
6 Male 175 83
7 Male 174 76
8 Male 170 80
9 Male 161 71

Вот теперь стало привычнее. И первая же запись нам говорит о мужчине с ростом ~190см и весом ~110кг. Большой человек. Назовём его Боб.


Но как понять: это много или мало по сравнению с остальными? Возможно ли, что мы все плюс-минус Бобы? Это немного позже.


А сейчас узнаем, насколько симметрично в этом наборе данных сочетаются два гендера?


data['Gender'].value_counts()

>> Male      5000
   Female    5000
   Name: Gender, dtype: int64

Идеально поровну. И это хорошо, ведь если бы было: 9999 мужчин и 1 женщина, то не осталось бы смысла делать вид, что этот ДатаСет одинакого хорошо раскрывает оба пола. В нашем случае — всё ок!


Разделяй и изучай!


Сейчас интуиция подсказывает, что будет правильно разделить два пола и исследовать отдельно. Ведь в жизни мы часто видим, что мужчины и женщины имеют плюс-минус разные рост и вес


# Мужчины
data_male = data[data['Gender'] == 'Male'].copy()

# Женщины
data_female = data[data['Gender'] == 'Female'].copy()

Давайте взглянем на небольшую описательную статистику, которую нам предлагает модуль pandas.


Мужчины:


data_male.describe()

Height Weight
count 5000 5000
mean 175 85
std 7 9
min 148 51
25% 171 79
50% 175 85
75% 180 91
max 201 122

Женщины:


data_female.describe()

Height Weight
count 5000 5000
mean 162 62
std 7 9
min 138 29
25% 157 56
50% 162 62
75% 167 67
max 186 92

Небольшой ликбез по инфе выше

Простым языком:


Описательная статистика — это набор чисел / характеристик для описания. Пожалуй, это самый простой для понимания вид статистики.


Представьте, что вы описываете параметры мяча. Он может быть:


  • большой / маленький
  • гладкий / шершавый
  • синий / красный
  • прыгучий / и не очень.

С сильным упрощением можно сказать, что этим и занимается описательная статистика. Но делает это не с мячиками, а с данными.


А вот параметры из таблицы выше:


  • count — Количество экземпляров.
  • mean — Среднее или сумма всех значений, делённая на их количество.
  • std — Стандартное отклонение или корень из дисперсии. Показывает разброс величин относительно среднего.
  • min — Минимальное значение или минимум.
  • 25% — Первый квартиль. Показывает значение, меньше которого находится 25% записей.
  • 50% — Второй квартиль или медиана. Показывает значение, выше и ниже которого одинаковое количество записей.
  • 75% — Третий квартиль. По анологии с первым квартилем, но ниже 75% записей.
  • max — Максимальное значение или максимум.

Среднее значение очень чувствительно к выбросам! Если четыре человека получают зарплату 10 000 ?, а пятый — 460 000 ?. То среднее будет — 100 000 ?. А медиана останется прежней — 10 000 ?.


Это не значит, что среднее — это плохой показатель. К нему нужно относиться внимательнее.


Кстати, с медианой тоже есть загвоздка.


Если количество измерений нечётное. То медиана — это значение посередине, если поставить данные «по росту».


А если чётное, то медиана — это среднее между двумя «самыми центральными».


Если в наборе данных только целые числа, а медиана получилась дробной — не удивляйтесь. Скорее всего количество измерений чётно.


Пример:


Сын принёс отметки со школы. Было пять уроков, он получил: 1, 5, 3, 2, 4
Пять оценок > нечётное количество
Сроим по росту: 1, 2, 3, 4, 5
Берём центральное — 3
Медианная оценка — 3


На следующий день сын принёс со школы новые оценки: 4, 2, 3, 5
Четыре оценки > нечётное количество
Строим по росту: 2, 3, 4, 5
Берём центральные: 3, 4
Находим их среднее: 3.5
Медиана — 3.5


Вывод: Молодец сына :)


Видим, что у мужчин среднее и медиана: 175см и 85кг. А у женщин: 162см и 62кг. Это говорит нам, что сильных выбросов нет. Либо они симметричны в обе стороны от медианы. Что бывает очень редко.


Но у обоих полов есть небольшие отклонения среднего от медианы. Но они несущественны и их видно только на сотых долях. Идём дальше!


Гистограма


Это график, который строит значения от минимума до максимума в порядке роста, и показывает количество отдельных экземпляров.


fig, axes = plt.subplots(2,2, figsize=(20,10))
plt.subplots_adjust(wspace=0, hspace=0)

axes[0,0].hist(data_male['Height'], 
               label='Male Height', 
               bins=100, 
               color='red')

axes[0,1].hist(data_male['Weight'], 
               label='Male Weight', 
               bins=100, 
               color='red', 
               alpha=0.4)

axes[1,0].hist(data_female['Height'], 
               label='Female Height', 
               bins=100, 
               color='blue')

axes[1,1].hist(data_female['Weight'], 
               label='Female Weight', 
               bins=100, 
               color='blue', 
               alpha=0.4)

axes[0,0].legend(loc=2, 
                 fontsize=20)

axes[0,1].legend(loc=2, 
                 fontsize=20)

axes[1,0].legend(loc=2, 
                 fontsize=20)

axes[1,1].legend(loc=2, 
                 fontsize=20)

plt.savefig('plt_histogram.png')
plt.show()

hist


Данные распределяются колоколообразно. Очень похоже на нормальное распределение.


Помимо статистических тестов на нормальность распределения есть визуальный тест. Если распределение по виду и логике похоже на нормальное — можно считать с долей допущений, что мы имеем дело именно с ним.


Можно было бы сделать статистический тест на нормальность и определить p-value, но не умею это выходит за рамки статьи.


Учимся работать ручками


Pandas за нас может посчитать многое. Но нужно хотя бы раз посчитать некоторые статистики самому. Сейчас покажу, как скалькулировать стандартное отклонение.


Сделаем это на примере мужчин и характеристике — рост.


Среднее


Формула:


$M = \frac{1}{N} \sum\limits_{i=1}^N n_i$


, где


  • М — среднее значение
  • N — количество экземпляров
  • ni — отдельный экземпляр

Код:


mean = data_male['Height'].mean()
print('mean:\t{:.2f}'.format(mean))

>> mean:    175.33

Средний рост — 175см


Квадрат отклонения от среднего


$d_i = (n_i - M)^2 $


, где


  • di — отдельное отклонение
  • ni — отдельный экземпляр
  • M — среднее

Код:


data_male['Height_d'] = (data_male['Height'] - mean) ** 2
data_male['Height_d'].head(10)

>> 0    149.927893
   1      0.385495
   2    166.739089
   3     47.193692
   4      4.721246
   5     20.288347
   6      0.375539
   7      2.964214
   8     25.997623
   9    200.149603
   Name: Height_d, dtype: float64

Дисперсия


Формула:


$D = \frac{1}{N} \sum\limits_{i=1}^N d_i$


, где


  • D — значение дисперсии
  • di — отдельное отклонение
  • N — количество экземпляров

Код:


disp = data_male['Height_d'].mean()
print('disp:\t{:.2f}'.format(disp))

>> disp:    52.89

Дисперсия — 53


Стандартное отклонение


Формула:


$std = \sqrt{D}$


, где


  • std — значение стандартного отклонения
  • D — значение дисперсии

Код:


std = disp ** 0.5
print('std:\t{:.2f}'.format(std))

>> std: 7.27

Стандартное отклонение — 7


Доверительные интервалы


Сейчас мы узнаем, в каких диапазонах роста и веса находятся 68%, 95% и 99.7% мужчин и женщин.


Это не так сложно — нужно прибавлять и отнимать стандартное отклонение от среднего. Выглядит это так:


  • 68% — плюс-минус одно стандартное отклонение
  • 95% — плюс-минус два стандартных отклонения
  • 99.7% — плюс-минус три стандартных отклонения

Напишем вспомогательную функцию, которая будет считать это:


def get_stats(series, title='noname'):
    # выводим название характеристики
    print('= {} =\n'.format(title.upper()))

    # получаем описательную статистику от pandas
    descr = series.describe()

    # выводим среднее
    mean = descr['mean']
    print('= Mean:\t{:.0f}'.format(mean))

    # выводим стандартное отклонение
    std = descr['std']
    print('= Std:\t{:.0f}'.format(std))

    # разделитель для красоты
    print('\n= = = =\n')

    # считаем интвервалы
    ## 68%
    devi_1 = [mean - std, mean + std]
    ## 95%
    devi_2 = [mean - 2 * std, mean + 2 * std]
    ## 99.7%
    devi_3 = [mean - 3 * std, mean + 3 * std]

    # выводим результат
    print('= 68% is from\t\t{:.0f} to {:.0f}'.format(devi_1[0], devi_1[1]))
    print('= 95% is from\t\t{:.0f} to {:.0f}'.format(devi_2[0], devi_2[1]))
    print('= 99.7% is from\t\t{:.0f} to {:.0f}'.format(devi_3[0], devi_3[1]))

Ну и применяем её к данным:


Мужчины | Рост


get_stats(data_male['Height'], title='Male Height')

>> 
= MALE HEIGHT =

= Mean: 175
= Std:  7

= = = =

= 68% is from       168 to 183
= 95% is from       161 to 190
= 99.7% is from     154 to 197

Мужчины | Вес


get_stats(data_male['Height'], title='Male Height')

>> 
= MALE WEIGHT =

= Mean: 85
= Std:  9

= = = =

= 68% is from       76 to 94
= 95% is from       67 to 103
= 99.7% is from     58 to 112

Женщины | Рост


get_stats(data_male['Height'], title='Male Height')

>> 
= FEMALE HEIGHT =

= Mean: 162
= Std:  7

= = = =

= 68% is from       155 to 169
= 95% is from       148 to 176
= 99.7% is from     141 to 182

Женщины | Вес


get_stats(data_male['Height'], title='Male Height')

>> 
= FEMALE WEIGHT =

= Mean: 62
= Std:  9

= = = =

= 68% is from       53 to 70
= 95% is from       44 to 79
= 99.7% is from     36 to 87

Отсюда выводы:


  • Большинство мужчин: 154см–197см и 58кг–112кг.
  • Большинство женщин: 141см–182см и 36кг–87кг.

Теперь осталось только применить машинное обучение к этому набору и попробовать предстазать вес по росту.


Ближайшие соседи


Алгоритм «К ближайших соседей» прост. Он существует для задач классификаций — отличить котика от собачки — и для задач регрессии — угадать вес по росту. Это то, что нам нужно!


Для регрессии он использует такой алгоритм:


  • Запоминает все точки данных
  • При появлении новой точки — ищет К её ближайших соседей (число К задаёт пользователь)
  • Усредняет результат
  • Выдаёт ответ

Для начала нужно разделить набор данных на обучающую и тестовую части и опробовать алгоритм


Экспериментируем на мужчинах


X_train, X_test, y_train, y_test = train_test_split(data_male['Height'], data_male['Weight'])

Разделили, настало время пробовать.


# Три соседа
knr3 = KNeighborsRegressor(n_neighbors=3)
knr3.fit(X_train.values.reshape(-1,1), y_train.values.reshape(-1,1))
knr3.score(X_train.values.reshape(-1,1), y_train.values.reshape(-1,1))

>> 0.8298400793623182

# Пять соседей
knr5 = KNeighborsRegressor(n_neighbors=5)
knr5.fit(X_train.values.reshape(-1,1), y_train.values.reshape(-1,1))
knr5.score(X_train.values.reshape(-1,1), y_train.values.reshape(-1,1))

>> 0.7958051642678619

# Семь соседей
knr7 = KNeighborsRegressor(n_neighbors=7)
knr7.fit(X_train.values.reshape(-1,1), y_train.values.reshape(-1,1))
knr7.score(X_train.values.reshape(-1,1), y_train.values.reshape(-1,1))

>> 0.7769249318420969

Не будем далеко ходить и остановимся на трёх соседях. Но вопрос: сможет ли такая модель угадать мой вес?


knr3.predict([[180]])[0, 0]

>> 88.67596236265881

88кг — это очень близко. В эту секунду мой вес — 89.8кг


График предсказаний для мужчин


Время построить мою любимую часть науки — графики.


array_male = []

# доверительный интервал 99.7%
xaxis = range(154, 198)

for h in xaxis:
    ans = knr3.predict([[h]])
    array_male.append(ans[0, 0])

plt.figure(figsize=(20,10))
plt.plot(xaxis, array_male, 'r-', linewidth=4)
plt.title('Male heght-weight dependence', fontsize=30)
plt.xlabel('Height', fontsize=30)
plt.ylabel('Weight', fontsize=30)
plt.grid()
plt.savefig('plt_knn_male.png')
plt.show()

male_plot


Модель и график предсказаний для женщин


X_train, X_test, y_train, y_test = train_test_split(data_female['Height'], data_female['Weight'])

knr3 = KNeighborsRegressor(n_neighbors=3)
knr3.fit(X_train.values.reshape(-1,1), y_train.values.reshape(-1,1))
knr3.score(X_train.values.reshape(-1,1), y_train.values.reshape(-1,1))

>> 0.8135681584074799

array_female = []

# доверительный интервал 99.7%
xaxis = range(141, 183)

for h in xaxis:
    ans = knr3.predict([[h]])
    array_female.append(ans[0, 0])

plt.figure(figsize=(20,10))
plt.plot(xaxis, array_female, 'b-', linewidth=4)
plt.title('Female heght-weight dependence', fontsize=30)
plt.xlabel('Height', fontsize=30)
plt.ylabel('Weight', fontsize=30)
plt.grid()
plt.savefig('plt_knn_female.png')
plt.show()

female_plot


Ну и конечно интересно, как выглядят эти графики вместе:


# объединение интервалов мужчин и женщин
xaxis = range(154, 183)

plt.figure(figsize=(20,10))
plt.plot(xaxis, array_male[:-15], 'r-', linewidth=4)
plt.plot(xaxis, array_female[13:], 'b-', linewidth=4)
plt.title('Together heght-weight dependence', fontsize=30)
plt.xlabel('Height', fontsize=30)
plt.ylabel('Weight', fontsize=30)
plt.grid()
plt.savefig('plt_knn_together.png')
plt.show()

together_plot


Ответы на вопросы


— В каком диапазоне вес и рост у большинства мужчин и женщин?


99.7% мужчин: от 154см до 197см и от 58кг до 112кг.
А 99.7% женщин: от 141см до 182см и от 36кг до 87кг.


— Какие они — «средний» мужчина и «средняя» женщина?


Средний мужчина — 175см и 85кг.
А средняя женщина — 162см и 62кг.


— Сможет ли простенькая модель машинного обучения «KNN» по этим данным угадать вес по росту?


Да, модель предсказала 88кг, а у меня 89.8кг.


Все, что сделал, собрал тут


Минусы статьи


  • Нет описания ДатаСета. Вероятно, возраст и другие факторы у людей различались. Поэтому на веру принимать его нельзя, а ради эксперимента — пожалуйста.
  • По-хорошему — нужно было сделать тест на нормальность распределения

Эпилог


Ставь лайк, если попал в 99.7% интервал

Комментарии (24)


  1. ABATAPA
    07.09.2019 19:02


    Что-то у Вас не сходится… Какие-то толстые карлики или дети…


    1. beardnessandy Автор
      07.09.2019 19:04

      Это в дюймах и фунтах. Я там дальше перевёл в см и кг :)


      1. ABATAPA
        07.09.2019 19:09

        Да, там дальше есть про это. Просмотрел, т. к. дальше уже в метрической системе. Но выглядит всё равно напрягающе. :)
        Ох уж эти «не такие как все»…


        1. beardnessandy Автор
          07.09.2019 19:11

          Ага) Жиза)


  1. Tyusha
    08.09.2019 20:55

    Я вот только не поняла, зачем для вычисления простейшей корреляции «вес-рост» использовать машинное обучение.


    1. RealSaniok
      08.09.2019 22:33
      +1

      В наше время без этого никак


      1. 0xd34df00d
        08.09.2019 04:27

        А промежуточные результаты экспериментов надо складывать в git блокчейн.


      1. beardnessandy Автор
        08.09.2019 13:29

        В наше время без этого никак


        Точн :D

        Я вот только не поняла, зачем для вычисления простейшей корреляции «вес-рост» использовать машинное обучение.


        Ради эскперимента а-ля «Посмотреть, что будет»


  1. Samouvazhektra
    08.09.2019 21:14

    Что-то никак не могу въехать в логику попытки предсказывать вес по росту на таких данных, так как вес помимо роста, зависит от множества других факторов, и по сути результат будет просто усредненный по выборке, для данного пола и роста. С таким же успехом можно курс доллара по прогнозу погоды предсказывать.
    Если от нефиг делать — то интереснее было бы рассмотреть корреляцию с ИМТ


    1. beardnessandy Автор
      08.09.2019 13:30

      Да, данных нехватает. Об этом написал в начале
      Я согласен, что эта модель не идеальная от слова совсем
      Тут я решил показать результат и провести опрос: «Получится ли угадать по этим данным хоть немного?»


  1. Griboks
    08.09.2019 22:56
    +1

    Обожаю смотреть на таблицы с оверхреноллионом знаков после запятой и рандомные гистограммы вместо красивых графиков.

    Интересный у вас подход к выбору модели и её обучения: ну… к-средних где-то там используется для регрессии, поэтому используем его… с какими-то рандомными параметрами обучения.

    И не забываем про гениальные результаты: много рандомных точек (без доверительных окрестностей!!!) на плоскости, зачем-то соединённых линиями (чтобы хоть какой-то график был). И 1/3 угаданных весов.

    Ради интереса конечно круто, но я бы улучшил это всё 1)ядерной оценкой с какими-нибудь 2)усами или интервалами, 3)обосновал выбор модели или хотя бы построил несколько моделей, 4)построил прямую/дугу регрессии с доверительными областями вместо двух рандомных ломанных и 5) убрал бы бесполезные первые 10 строк датасета.


    1. beardnessandy Автор
      08.09.2019 13:31

      Согласен с вами. Статью можно сделать в разу лучше по всем пяти пунктам
      С знаками после запятой разобрались. Они больше не виднеются на горизонте :)
      Спасибо!


  1. alex6999
    08.09.2019 23:36

    Средняя женщина какая-то тяжеловатая выходит


    1. beardnessandy Автор
      08.09.2019 13:33

      Да, я тоже заметил :)
      Хотя в жизни, по субъективной оценке — это не так)

      Не стоит доверять этому анализу на 146%
      Он очень общий и далеко не самый точный
      Просто ДатаСет показался мне интересным, решил поделиться


  1. vanxant
    08.09.2019 01:56

    Распределение больше похоже на логистическое (ну так на глаз). Посчитайте критерий Колмогорова, заодно научитесь:)


    1. beardnessandy Автор
      08.09.2019 13:33

      Сохранил в заметку) Научусь) Спасибо :)


  1. Izaron
    08.09.2019 10:02

    То, что описано в посте — не таинственное машинное обучение, а самая что ни на есть мужицкая Математическая Статистика из четвёртого семестра тех. вуза ;)


    Непонятно, зачем в самом конце статьи начинать тыкать пальцем в небо и что-то подгонять с k=1,3,5,..., когда можно использовать какой-нибудь крутой алгоритм аппроксимации функции (из курса Численных Методов третьего семестра) с потенциально наименьшей ошибкой. А если предположить, что средние вес и рост на нашей отрезке зависят линейно, то использовать линейную регрессию, приводя функцию к прямой.


    Датасет наверное либо старый на несколько десятилетий, либо про подростков — часто вижу вокруг себя людей ростом 185-190, но по графику, мои наблюдения крайне маловероятны.


    1. WhiteBlackGoose
      08.09.2019 10:54

      Четвертого? Это? Но это же просто линейная регрессия первой степени. Ее можно по линейке построить!


      1. beardnessandy Автор
        08.09.2019 13:35

        Да, вы оба правы)
        Можно было бы сделать анализ в разы глубже
        Мне интереснее было посмотреть на результат работы KNN, как, на мой взгляд, самой простой модели МО


  1. maxzhurkin
    08.09.2019 12:44

    Ставь лайк, если попал в 99.7% интервал
    а автор хитрец!
    Почему не 100%?


    1. beardnessandy Автор
      08.09.2019 13:34

      Ну это уже лайковымогание)
      А так шанс не попасть присутствует)


      1. maxzhurkin
        08.09.2019 13:37

        Это и так лайковыпрашивание в «лучших» традициях «одноклассников»


        1. beardnessandy Автор
          08.09.2019 13:39

          Ну это стёб был) Конечно норм чувак не будет на такое вестись)


  1. Matshishkapeu
    08.09.2019 17:39

    У вас криво построена панель из 4 распределений по росту и весу. Ось показана только для двух нижних, но у двух верних они другие. В противном случае медиана по росту у мужчин и женщин одинакова в районе 160 см а вес у медианного мужчины что-то около 60 кг и он легче медианной женщины. В целом по итогам вся модель укладывается в формулу для реферата по физкультуре в восьмом классе: вес[кг]=рост[см] -100 (110 для женщин), но тогда вместо машинлернинга достаточно однострочника в матплотлиб чтоб построить один график.