Elements of Data Science, copyright 2021 Allen B. Downey
License: Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
В этой главе исследуются отношения между переменными.
Мы будем визуализировать отношения с помощью диаграмм рассеяния (scatter plots), диаграмм размаха (box plots) и скрипичных диаграмм (violin plots),
И мы будем количественно определять отношения, используя корреляцию (correlation) и простую регрессию (simple regression).
Самый важный урок этой главы заключается в том, что вы всегда должны визуализировать взаимосвязь между переменными, прежде чем пытаться ее количественно оценить; в противном случае вас могут ввести в заблуждение.
from os.path import basename, exists
def download(url):
filename = basename(url)
if not exists(filename):
from urllib.request import urlretrieve
local, _ = urlretrieve(url, filename)
print('Downloaded ' + local)
download('https://github.com/AllenDowney/' +
'ElementsOfDataScience/raw/master/brfss.hdf5')
В качестве первого примера мы рассмотрим взаимосвязь между ростом и весом.
Мы будем использовать данные из Системы наблюдения за поведенческими факторами риска (BRFSS), которая находится в ведении Центров по контролю за заболеваниями по адресу https://www.cdc.gov/brfss.
В опросе приняли участие более 400 000 респондентов, но, чтобы произвести анализ, я выбрал случайную подвыборку из 100 000 человек.
import pandas as pd
brfss = pd.read_hdf('brfss.hdf5', 'brfss')
brfss.shape
(100000, 9)
Вот несколько строк:
brfss.head()
SEX | HTM4 | WTKG3 | INCOME2 | _LLCPWT | _AGEG5YR | _VEGESU1 | _HTMG10 | AGE | |
---|---|---|---|---|---|---|---|---|---|
96230 | 2.0 | 160.0 | 60.33 | 8.0 | 1398.525290 | 6.0 | 2.14 | 150.0 | 47.0 |
244920 | 2.0 | 163.0 | 58.97 | 5.0 | 84.057503 | 13.0 | 3.14 | 160.0 | 89.5 |
57312 | 2.0 | 163.0 | 72.57 | 8.0 | 390.248599 | 5.0 | 2.64 | 160.0 | 42.0 |
32573 | 2.0 | 165.0 | 74.84 | 1.0 | 11566.705300 | 3.0 | 1.46 | 160.0 | 32.0 |
355929 | 2.0 | 170.0 | 108.86 | 3.0 | 844.485450 | 3.0 | 1.81 | 160.0 | 32.0 |
BRFSS включает сотни переменных. Для примеров в этой главе я выбрал всего девять.
Мы начнем с HTM4
, который записывает рост каждого респондента в см, и WTKG3
, который записывает вес в кг.
height = brfss['HTM4']
weight = brfss['WTKG3']
Чтобы визуализировать взаимосвязь между этими переменными, мы построим диаграмму рассеяния (scatter plot).
Диаграммы рассеяния широко распространены и понятны, но их на удивление сложно правильно построить.
В качестве первой попытки мы будем использовать функцию plot
с аргументом o
, который строит круг для каждой точки.
import matplotlib.pyplot as plt
%matplotlib inline
plt.plot(height, weight, 'o')
plt.xlabel('Height in cm')
plt.ylabel('Weight in kg')
plt.title('Scatter plot of weight versus height');
Похоже, что высокие люди тяжелее, но в этом графике есть несколько моментов, которые затрудняют интерпретацию.
Первый из них - перекрытие (overplotted), то есть точки данных накладываются друг на друга, поэтому вы не можете сказать, где много точек, а где только одна.
Когда это происходит, результаты могут вводить в заблуждение.
Один из способов улучшить график - использовать прозрачность (transparency), что мы можем сделать с помощью ключевого аргумента alpha
. Чем ниже значение alpha
, тем прозрачнее каждая точка данных.
Вот как это выглядит с alpha=0.02
.
plt.plot(height, weight, 'o', alpha=0.02)
plt.xlabel('Height in cm')
plt.ylabel('Weight in kg')
plt.title('Scatter plot of weight versus height');
Уже лучше, но на графике так много точек данных, что диаграмма рассеяния все еще перекрывается. Следующим шагом будет уменьшение размеров маркеров.
При markersize=1
и низком значении alpha
диаграмма рассеяния будет менее насыщенной.
Вот как это выглядит.
plt.plot(height, weight, 'o', alpha=0.02, markersize=1)
plt.xlabel('Height in cm')
plt.ylabel('Weight in kg')
plt.title('Scatter plot of weight versus height');
Уже лучше, но теперь мы видим, что точки строятся отдельными столбцами. Это потому, что большая часть высоты была указана в дюймах и преобразована в сантиметры.
Мы можем разбить столбцы, добавив к значениям некоторый случайный шум; по сути, мы заполняем округленные значения.
Такое добавление случайного шума называется дрожанием (jittering).
Дрожание - это добавление случайного шума к данным для предотвращения перекрытия статистических графиков. Если непрерывное измерение округлено до некоторой удобной единицы, может произойти перекрытие. Это приводит к превращению непрерывной переменной в дискретную порядковую переменную. Например, возраст измеряется в годах, а масса тела - в фунтах или килограммах. Если вы построите диаграмму разброса веса в зависимости от возраста для достаточно большой выборки людей, там может быть много людей, записанных, скажем, с 29 годами и 70 кг, и, следовательно, в этой точке будет нанесено много маркеров (29, 70).
Чтобы уменьшить перекрытие, вы можете добавить к данным небольшой случайный шум. Размер шума часто выбирается равным ширине единицы измерения. Например, к значению 70 кг вы можете добавить количество u , где u - равномерная случайная величина в интервале [-0,5, 0,5]. Вы можете обосновать дрожание, предположив, что истинный вес человека весом 70 кг с равной вероятностью находится в любом месте интервала [69,5, 70,5].
Контекст данных важен при принятии решения о дрожании. Например, возраст обычно округляется в меньшую сторону: 29-летний человек может праздновать свой 29-й день рождения сегодня или, возможно, ему исполнится 30 завтра, но ей все равно 29 лет. Следовательно, вы можете изменить возраст, добавив величину v , где v - равномерная случайная величина в интервале [0,1]. (Мы игнорируем статистически значимый случай женщин, которым остается 29 лет в течение многих лет!)
Источник: Jittering to prevent overplotting in statistical graphics
Мы можем использовать NumPy для добавления шума из нормального распределения со средним 0 и стандартным отклонением 2.
import numpy as np
noise = np.random.normal(0, 2, size=len(brfss))
height_jitter = height + noise
Вот как выглядит график с дрожащими (jittered) высотами.
plt.plot(height_jitter, weight, 'o',
alpha=0.02, markersize=1)
plt.xlabel('Height in cm')
plt.ylabel('Weight in kg')
plt.title('Scatter plot of weight versus height');
Столбцы исчезли, но теперь мы видим, что есть строки, в которых люди округляют свой вес. Мы также можем исправить это с помощью дрожания веса.
noise = np.random.normal(0, 2, size=len(brfss))
weight_jitter = weight + noise
plt.plot(height_jitter, weight_jitter, 'o',
alpha=0.02, markersize=1)
plt.xlabel('Height in cm')
plt.ylabel('Weight in kg')
plt.title('Scatter plot of weight versus height');
Наконец, давайте увеличим масштаб области, где находится большинство точек данных.
Функции xlim
и ylim
устанавливают нижнюю и верхнюю границы для осей $x$ и $y$; в данном случае мы наносим рост от 140 до 200 сантиметров и вес до 160 килограмм.
Вот как это выглядит.
plt.plot(height_jitter, weight_jitter, 'o',
alpha=0.02, markersize=1)
plt.xlim([140, 200])
plt.ylim([0, 160])
plt.xlabel('Height in cm')
plt.ylabel('Weight in kg')
plt.title('Scatter plot of weight versus height');
Теперь у нас есть достоверная картина взаимосвязи между ростом и весом.
Ниже вы можете увидеть вводящий в заблуждение график, с которого мы начали, и более надежный, которым мы закончили. Они явно разные, и они предлагают разные истории о взаимосвязи между этими переменными.
# Set the figure size
plt.figure(figsize=(8, 3))
# Create subplots with 2 rows, 1 column, and start plot 1
plt.subplot(1, 2, 1)
plt.plot(height, weight, 'o')
plt.xlabel('Height in cm')
plt.ylabel('Weight in kg')
plt.title('Scatter plot of weight versus height')
# Adjust the layout so the two plots don't overlap
plt.tight_layout()
# Start plot 2
plt.subplot(1, 2, 2)
plt.plot(height_jitter, weight_jitter, 'o',
alpha=0.02, markersize=1)
plt.xlim([140, 200])
plt.ylim([0, 160])
plt.xlabel('Height in cm')
plt.ylabel('Weight in kg')
plt.title('Scatter plot of weight versus height')
plt.tight_layout()
Смысл этого примера в том, что для создания эффективного графика разброса требуются некоторые усилия.
Упражнение: Набирают ли люди вес с возрастом? Мы можем ответить на этот вопрос, визуализировав взаимосвязь между весом и возрастом.
Но прежде чем строить диаграмму рассеяния, рекомендуется визуализировать распределения по одной переменной за раз. Итак, давайте посмотрим на возрастное распределение.
Набор данных BRFSS включает столбец AGE
, который представляет возраст каждого респондента в годах. Чтобы защитить конфиденциальность респондентов, возраст округляется до пятилетних интервалов. AGE
содержит середину интервалов (bins).
Извлеките переменную 'AGE'
из фрейма данных brfss
и присвойте ее age
.
Постройте функцию вероятности (Probability mass function, PMF) для age
в виде гистограммы, используя Pmf
из empiricaldist
.
empiricaldist
- библиотека Python, представляющая эмпирические функции распределения.
try:
import empiricaldist
except ImportError:
!pip install empiricaldist
from empiricaldist import Pmf
# Решение здесь
Упражнение: Теперь давайте посмотрим на распределение веса.
Столбец, содержащий вес в килограммах, - это WTKG3
. Поскольку этот столбец содержит много уникальных значений, отображение его как функции вероятности (PMF) работает плохо.
Pmf.from_seq(weight).bar()
plt.xlabel('Weight in kg')
plt.ylabel('PMF')
plt.title('Distribution of weight');
Чтобы получить лучшее представление об этом распределении, попробуйте построить график функции распределения (Cumulative distribution function, CDF).
Вычислите функцию распределения (CDF) нормального распределения с тем же средним значением и стандартным отклонением и сравните его с распределением веса.
Подходит ли нормальное распределение для этих данных? А как насчет логарифмического преобразования весов?
# Решение здесь
# Решение здесь
# Решение здесь
Упражнение: Теперь давайте построим диаграмму разброса (scatter plot) для weight
и age
.
Отрегулируйте alpha
и markersize
, чтобы избежать наложения (overplotting). Используйте ylim
, чтобы ограничить ось y
от 0 до 200 килограммов.
# Решение здесь
Упражнение: В предыдущем упражнении возрасты указаны в столбцах, потому что они были округлены до 5-летних интервалов (bins). Если мы добавим дрожание (jitter), диаграмма рассеяния покажет взаимосвязь более четко.
age
со средним значением 0
и стандартным отклонением 2.5
.alpha
и markersize
.# Решение здесь
В предыдущем разделе мы использовали диаграммы разброса для визуализации взаимосвязей между переменными, а в упражнениях вы исследовали взаимосвязь между возрастом и весом. В этом разделе мы увидим другие способы визуализации этих отношений, в том числе диаграммы размаха и скрипичные диаграммы.
Я начну с диаграммы разброса веса в зависимости от возраста.
age = brfss['AGE']
noise = np.random.normal(0, 1.0, size=len(brfss))
age_jitter = age + noise
plt.plot(age_jitter, weight_jitter, 'o',
alpha=0.01, markersize=1)
plt.xlabel('Age in years')
plt.ylabel('Weight in kg')
plt.ylim([0, 200])
plt.title('Weight versus age');