Следующая тема: ИАД. Валидация результатов
Вернуться в раздел: Исследовательский анализ данных
Вернуться в оглавление: Я.Практикум
Введение
Когда у вас два набора данных, будьте готовы к тому, что связь придётся искать не для двух, а для четырёх, шести, восьми наборов — параметров может быть много. Для таких поисков вам понадобится совместное распределение множества величин.
Чему вы научитесь:
- Строить диаграмму рассеяния;
- Считать коэффициент корреляции Пирсона;
- Находить совместное распределение для множества величин
Вам предстоит:
Построить две матрицы диаграмм рассеяния: для исходных и отфильтрованных данных.
Сколько времени это займет:
4 урока по 15 минут
Постановка задачи:
Убедитесь в том, что избавление от аномалий не повлияло на природу распределения исходных данных.
Диаграмма рассеяния
Часто нужно понимать, как разные значения зависят друг от друга. Возьмём, к примеру, данные о росте height (пер. «рост») и весе weight (пер. «вес») людей.
height weight
0 167.089607 51.253398
1 181.648633 61.910639
2 176.272800 69.413002
3 173.270164 64.563337
4 172.181037 65.453165
Изучим числовые характеристики:
hw.describe()
height weight
count 7634.000000 7634.000000
mean 172.640603 57.688374
std 4.875440 5.274638
min 153.956131 37.687478
25% 169.297852 54.155792
50% 172.641400 57.746296
75% 175.939310 61.205201
max 190.888112 76.604422
...и гистограммы:

Гистограммы похожи на графики нормального распределения. Отдельно отметим, что имеем дело с данными о взрослых людях: из числовых характеристик видно, что людей ниже 150 см в выборке нет.
Чтобы проследить за изменениями цен на бензин во времени, нужно изучить график, где по одной оси идёт стоимость за литр, а по другой — месяцы. Чтобы найти связь между ростом и весом, их тоже нужно отметить на одном графике, а не на отдельных гистограммах. Зададим рост и вес на осях X и Y соответственно:
hw.plot(x='height', y='weight')

Мохнатое нечто. Совсем не то, что вы ожидали увидеть. Исходные данные не были отсортированы. Да и рост каждого следующего человека никак не связан с ростом предыдущего. Избавимся от мешанины прыгающих значений, соединённых линиями. Отсортируем данные:
hw.sort_values('height').plot(x='height', y='weight')

Мохнатое нечто стремится. Линии по-прежнему путают. График, где значения соединяются линиями, хорош, если иллюстрирует непрерывную связь. Например, на графике почасового изменения температуры уместно соединить точки замеров в 8 часов утра и в 9. А вот люди в наблюдениях никак не связаны друг с другом. Гораздо лучше обозначить отдельные сочетания роста и веса точками. Это возможно на особом типе графиков — scatter (пер. «разброс, рассеивание»). Передадим значение scatter параметру kind метода plot():
hw.plot(x='height', y='weight', kind='scatter')

Такой график уже лучше отражает действительность. Есть основная масса точек с наиболее частыми сочетаниями веса и роста. При этом с увеличением роста увеличивается и вес. Но это лишь в среднем. Можно найти уникальные примеры людей с большим весом и малым ростом. Однако есть и совсем пустые зоны: например, люди ростом более 180 см не весят меньше 50 кг.
На графике можно увидеть взаимосвязь между двумя величинами. А также понять, какие данные типичны, а какие — аномальны.
Так, если нам скажут, что есть человек с ростом 190 см и весом 50 кг, мы ответим, что либо неправильно измеряли, либо это очень редкий человек. А про индивида ростом 170 см и весом более 70 кг известно, что его вес превышает типичное для его роста значение.
Задача
Постройте график по данным из station_stat_full, где для каждой АЗС будет отдельная точка: по горизонтальной оси — число заездов на АЗС, по вертикальной — медианное время заправки. Добавьте линии сетки.
import pandas as pd
data = pd.read_csv('/datasets/visits.csv', sep='\t')
# фильтруем слишком быстрые и медленные заезды и АЗС
data['too_fast'] = data['time_spent'] < 60
data['too_slow'] = data['time_spent'] > 1000
too_fast_stat = data.pivot_table(index='id', values='too_fast')
good_ids = too_fast_stat.query('too_fast < 0.5')
good_data = data.query('id in @good_ids.index')
good_data = good_data.query('60 <= time_spent <= 1000')
# считаем данные по отдельным АЗС и по сетям
station_stat = data.pivot_table(index='id', values='time_spent', aggfunc='median')
good_stations_stat = good_data.pivot_table(index='id', values='time_spent', aggfunc='median')
stat = data.pivot_table(index='name', values='time_spent')
good_stat = good_data.pivot_table(index='name', values='time_spent', aggfunc='median')
stat['good_time_spent'] = good_stat['time_spent']
id_name = good_data.pivot_table(index='id', values='name', aggfunc=['first', 'count'])
id_name.columns = ['name', 'count']
station_stat_full = id_name.join(good_stations_stat)
station_stat_full.plot(kind='scatter', x='count', y='time_spent', grid=True)
Результат
АЗС с малым числом заездов имеют значительный разброс медианного времени заправки (от 120 до 500). Остальные обычно укладываются в границы 200–350 секунд. Это наводит на две идеи: АЗС с большим числом заездов — крупные, процессы на них отлажены, вот и время одинаковое. Где мало заездов, там искажения данных более вероятны.
Корреляция

Очевидный недостаток этого графика в том, что посередине — огромное количество точек, слившихся в единую массу. В облаке значений не разглядеть области более высокой плотности. Есть два способа сделать график нагляднее.
Способ 1
Сделаем точки полупрозрачными, задав параметр alpha. Попробуем подобрать его оптимальное значение:
hw.plot(x='height', y='weight', kind='scatter', alpha=0.3)

С таким значением alpha стало несильно лучше. Уменьшим alpha в 10 раз:
hw.plot(x='height', y='weight', kind='scatter', alpha=0.03)

Достаточно наглядно. Что, если поиграть с прозрачностью ещё?
hw.plot(x='height', y='weight', kind='scatter', alpha=0.003)

Способ 2
Когда точек много и каждая в отдельности не интересна, данные отображают особым способом. График делят на ячейки; пересчитывают точки в каждой ячейке. Затем ячейки заливают цветом: чем больше точек — тем цвет гуще.
Такой тип графика задают через параметр kind, ему присваивают значение hexbin (от англ. hexagonal binning plot — «график, поделённый на шестиугольные области»).
Число ячеек по горизонтальной оси задают параметром gridsize (от англ. size of a grid — «размер сетки»), аналогом bins для hist().
Языки программирования придуманы людьми, поэтому неидеальны. Бывает, при столкновении с багами приходится ставить «костыли». Здесь это параметр sharex=False. Если значение True, то пропадёт подпись оси Х, а без sharex график выйдет неказистым — это «костыльный» обход бага библиотеки pandas.
hw.plot(x='height', y='weight', kind='hexbin', gridsize=20, figsize=(8, 6), sharex=False, grid=True)

Какие красивые соты получились! Отчётливо видно, что в центре графика не однородное пятно, а определённая структура данных. В самой «густой» зоне люди с ростом от 170 до 175 см и весом от 55 до 60 кг.
Смысл этого графика, как и у гистограммы, — отображение частотности. Но на гистограмме показана только одна величина, а здесь две: нельзя сделать гистограмму одновременно и по росту, и по весу. Повышенная частота определённых сочетаний указывает на закономерность. Часто цель анализа данных в том и состоит, чтобы показать связь двух величин.
Например, как цена квартиры зависит от её площади? Влияет ли на стоимость жилья его удалённость от центра? А тихие соседи? Или цена квартиры связана с годом строительства дома?

Скорее всего, каждый из этих факторов по-своему влияет на конечную стоимость квартиры. Узнать, как именно и насколько сильно, — это аналитическая задача. Её решают, изучая совместное распределение величин.
Взаимосвязь двух или нескольких величин называется «корреляция». График (да и жизненный опыт) позволяет утверждать, что рост и вес взаимосвязаны, или коррелируют. Очевидно не только наличие связи, но и прямая зависимость: чем больше рост, тем больше вес. В таком случае говорят, что корреляция положительная. А для Наполеона с его небольшим ростом высокие амбиции — пример отрицательной корреляции, так как уменьшение одной величины связано с увеличением другой.
То, что корреляция есть, понятно. Но как оценить численно, насколько тесна взаимосвязь? Для этого находят коэффициент корреляции Пирсона. Он помогает определить наличие линейной связи между величинами и принимает значения от -1 до 1.
Если с ростом первой величины растёт вторая, то коэффициент корреляции Пирсона — положительный. Если при изменении одной величины другая остаётся прежней, то коэффициент равен 0.
Если рост одной величины связан с уменьшением другой, коэффициент отрицательный.
Чем ближе коэффициент корреляции Пирсона к крайним значениям 1 или -1, тем теснее линейная взаимосвязь. Если значение близко к нулю, значит, линейная связь слабая либо отсутствует вовсе. Бывает, что коэффициент нулевой не потому, что связи между значениями нет, а из-за того, что у неё более сложный, не линейный характер. Потому-то коэффициент корреляции такую связь не берёт.
0.5196907833692264
0.5196907833692264
Корреляция в 0.52 говорит о наличии связи, однако не слишком сильной. Выходит, увеличение роста сопровождается прибавкой в весе, но так бывает не всегда.
Справедливо ли обратное: увеличивается ли рост при наборе веса? Насколько нам известно из жизненного опыта, нет. Хотя корреляция иллюстрирует зависимость между величинами, она не доказывает наличие причинно-следственной связи. Нельзя сказать, что, прибавляя в весе, мы становимся выше.
Итак, наличием корреляции причинно-следственную связь не доказать, но и не опровергнуть. А что, если надо анализировать не столь прозрачные данные?
import pandas as pd
data = pd.read_csv('/datasets/visits.csv', sep='\t')
# фильтруем слишком быстрые и медленные заезды и АЗС
data['too_fast'] = data['time_spent'] < 60
data['too_slow'] = data['time_spent'] > 1000
too_fast_stat = data.pivot_table(index='id', values='too_fast')
good_ids = too_fast_stat.query('too_fast < 0.5')
good_data = data.query('id in @good_ids.index')
good_data = good_data.query('60 <= time_spent <= 1000')
# считаем данные по отдельным АЗС и по сетям
station_stat = data.pivot_table(index='id', values='time_spent', aggfunc='median')
good_stations_stat = good_data.pivot_table(index='id', values='time_spent', aggfunc='median')
stat = data.pivot_table(index='name', values='time_spent')
good_stat = good_data.pivot_table(index='name', values='time_spent', aggfunc='median')
stat['good_time_spent'] = good_stat['time_spent']
id_name = good_data.pivot_table(index='id', values='name', aggfunc=['first', 'count'])
id_name.columns = ['name', 'count']
station_stat_full = id_name.join(good_stations_stat)
print(station_stat_full['count'].corr(station_stat_full['time_spent']))
Результат
0.12242632473597156
Штирлиц ещё никогда не был так близок к верному пониманию данных, а корреляция — так близка к нулю. Скорее всего, особой связи между числом заездов на АЗС и продолжительностью заправки нет. Либо она есть, но всё сложнее, чем кажется.
💡 Ещё одна мера зависимости двух переменных — это ковариация. Как её применяют и в чём сходство и различие с корреляцией, можно узнать на курсе «Математика для анализа данных».
Матрица диаграмм рассеяния
В прошлом уроке вы нашли корреляцию двух величин. Однако жизненные явления гораздо сложнее и могут зависеть от множества факторов. Например, интересно изучить не только рост и вес, но и то, как на них влияют возраст и пол.
К сожалению, нарисовать понятный график сразу для четырёх параметров нельзя. Однако можно построить диаграммы раcсеяния попарно для роста и веса, роста и возраста, веса и пола, веса и возраста — всего 16 вариантов. В pandas такую задачу решают не df.plot(), а специальным методом: pd.plotting.scatter_matrix(df) (от англ. scatter matrix — «матрица рассеяния»).
В наборе данных для каждого человека известен рост height, вес weight и возраст age. В столбце male (пер. «мужской») значением 1 обозначен мужской пол, 0 — женский. Построим матрицу рассеяния:

Красиво! Сетка 4х4. Каждая ячейка показывает взаимосвязь пары параметров. Изучим их подробнее:
- В верхней левой ячейке связь роста с ростом. Это не диаграмма рассеяния, а обычная гистограмма распределения по росту. Видно, что больше всего людей ростом 150–170 см.
- В ячейке ниже: связь роста и веса. Можно выделить две зоны: узкую полоску до 150 см и широкую — после. Выходит, до 150 см рост и вес увеличиваются равномерно, а после — идут вразнобой.
- Рост и возраст: до 150 см и 20 лет тесно связаны, затем зависимость перестаёт быть устойчивой. Дело в том, что в детстве и подростковом периоде мы растём примерно одинаково, а вот в 20 лет можем оказаться ростом и 150, и 180 см. Аналогично устроена связь «вес — возраст».
- Из диаграммы разброса в нижней левой ячейке можно сделать вывод, что рослых мужчин в среднем больше, чем рослых женщин. Так же обстоит и с весом (соседняя ячейка male-weight).
- Гистограмма пола male свидетельствует, что в выборке чуть больше женщин.
- Гистограмма возраста age сообщает, что больше всего информации собрано о детях младше 5 лет и совсем мало данных о людях старше 75.
- Гистограмма веса weight имеет два пика: правый характерен для веса взрослых, а левый иллюстрирует всплеск значений около 15–20 кг. Видимо, типичный вес дошкольника. Однако на гистограмме роста такого пика нет. Это может объясняться тем, что в возрасте пяти лет дети активно прибавляют в росте, но не в весе. Вот и получилась большая группа детей разного роста с весом около 20 кг: отсюда пик на гистограмме веса, без выдающихся соответствующих значений на гистограмме роста.
- По другую сторону диагонали зеркально расположились те же данные, только оси поменялись местами. Часто проще воспринимать графики, по которым взгляд скользит слева направо. Так, на графике height-age в верхней строке (более наглядно, чем на графике age-height) видно, что люди старше 60 ниже ростом. Обратите внимание, из этой корреляции не следует факт, что после 60 рост уменьшается. Мы не наблюдали одних и тех же людей год за годом, так что можем лишь отметить, что