Все курсы > Анализ и обработка данных > Занятие 4 (часть 1)
На прошлом занятии мы изучали различные классификации данных, задачи EDA, а также познакомились с основными библиотеками для создания визуализаций. Сегодня мы свяжем эти концепции в практической работе по анализу датасета «Титаник» и датасета Tips.
Откроем ноутбук к этому занятию⧉
В первую очередь подготовим датасеты.
Подготовка данных
Датасет «Титаник»
Скачаем обучающий датасет Титаник, подгрузим его в сессионное хранилище Google Colab и импортируем в ноутбук.
1 2 |
# для импорта используем функцию read_csv() titanic = pd.read_csv('/content/train.csv') |
Как мы уже знаем, посмотреть на первые или последние несколько (по умолчанию, пять) значений можно с помощью методов .head() и .tail() соответственно.
1 2 |
# посмотрим на первые три записи titanic.head(3) |

Иногда для получения более объективного представления о данных удобно использовать метод .sample(), который по умолчанию выдает одно случайное наблюдение.
1 2 |
# выведем пять случайных строк titanic.sample(5) |

Метод .info() для каждого столбца выводит количество непустных (not-null) значений и тип данных. Кроме того, этот метод считает количество столбцов каждого типа и общий объем памяти, занимаемый датасетом.
1 |
titanic.info() |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<class 'pandas.core.frame.DataFrame'> RangeIndex: 891 entries, 0 to 890 Data columns (total 12 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 PassengerId 891 non-null int64 1 Survived 891 non-null int64 2 Pclass 891 non-null int64 3 Name 891 non-null object 4 Sex 891 non-null object 5 Age 714 non-null float64 6 SibSp 891 non-null int64 7 Parch 891 non-null int64 8 Ticket 891 non-null object 9 Fare 891 non-null float64 10 Cabin 204 non-null object 11 Embarked 889 non-null object dtypes: float64(2), int64(5), object(5) memory usage: 83.7+ KB |
Конечно, посмотреть количество пропусков удобнее, например, с помощью последовательного применения методов .isnull() и .sum().
1 2 3 |
# метод .isnull() выдает логический массив, где пропуски обозначены как True # метод .sum() по умолчанию суммирует эти True или единицы по столбцам (axis = 0) titanic.isnull().sum() |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
PassengerId 0 Survived 0 Pclass 0 Name 0 Sex 0 Age 177 SibSp 0 Parch 0 Ticket 0 Fare 0 Cabin 687 Embarked 2 dtype: int64 |
Теперь выполним несложную предобработку данных.
1 2 3 4 5 6 7 8 |
# в частности, избавимся от столбца Cabin titanic.drop(labels = 'Cabin', axis = 1, inplace = True) # заполним пропуски в столбце Age медианным значением titanic.Age.fillna(titanic.Age.median(), inplace = True) # два пропущенных значения в столбце Embarked заполним портом Southhampton titanic.Embarked.fillna('S', inplace = True) # проверим результат (найдем общее количество пропусков сначала по столбцам, затем по строкам) titanic.isnull().sum().sum() |
1 |
Более сложные методы обработки данных мы рассмотрим в третьем и четвертом разделах курса.
Датасет Tips
Кроме того, импортируем хранящийся в библиотеке Seaborn датасет Tips. В нем содержатся 244 записи о чаевых, которые официант ресторана получал на протяжении нескольких месяцев.
1 2 3 |
# для импорта воспользуемся функцией load_dataset() с параметром 'tips' tips = sns.load_dataset('tips') tips.head(3) |

Вновь воспользуемся методом .info().
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<class 'pandas.core.frame.DataFrame'> RangeIndex: 244 entries, 0 to 243 Data columns (total 7 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 total_bill 244 non-null float64 1 tip 244 non-null float64 2 sex 244 non-null category 3 smoker 244 non-null category 4 day 244 non-null category 5 time 244 non-null category 6 size 244 non-null int64 dtypes: category(4), float64(2), int64(1) memory usage: 7.4 KB |
Пропущенных значений в этом датасете нет.
1 |
tips.isnull().sum() |
1 2 3 4 5 6 7 8 |
total_bill 0 tip 0 sex 0 smoker 0 day 0 time 0 size 0 dtype: int64 |
Теперь, когда данные подгружены, перейдем к их описанию, нахождению различий и выявлению взаимосвязей.
Описание данных

Категориальные переменные
Методы .unique() и .value_counts()
Применение этих методов аналогично использованию метода библиотеки Numpy np.unique() с параметром return_counts = True. Применим его.
1 |
np.unique(titanic.Survived, return_counts = True) |
1 |
(array([0, 1]), array([549, 342])) |
Теперь воспользуемся методами библиотеки Pandas.
1 2 |
# первый метод возращает только уникальные значения titanic.Survived.unique() |
1 |
array([0, 1]) |
1 2 |
# второй - уникальные значения и их частоту titanic.Survived.value_counts() |
1 2 3 |
0 549 1 342 Name: Survived, dtype: int64 |
При этом для нахождения относительной частоты делить на общее количество строк не нужно. Достаточно указать параметр normalize = True.
1 |
titanic.Survived.value_counts(normalize = True) |
1 2 3 |
0 0.616162 1 0.383838 Name: Survived, dtype: float64 |
Долю «единичек» при наличии двух классов, обозначенных как 0 и 1, можно посчитать и так.
1 |
titanic.Survived.mean().round(2) |
1 |
0.38 |
df.describe()
Исследование качественных переменных удобно начать с метода .describe(). Его применение к категориальным столбцам выдаст:
- общее количество значений (count)
- количество уникальных значений (unique)
- наиболее часто встречающееся значение (top)
- и количество таких значений (freq)
Применим метод .describe() к столбцам Sex и Embarked.
1 |
titanic[['Sex', 'Embarked']].describe() |

Перейдем к графическим методам.
countplot и barplot
Рассмотрим два по сути однаковых графика countplot и barplot: и тот, и другой считают количество значений в каждой из категорий. С точки зрения Питона, различие заключается в том, что в случае countplot, мы считаем количество наблюдений в каждой из категорий в процессе построения графика, а в случае barplot эти метрики уже должны быть посчитаны. Рассмотрим на примерах.
Проще всего countplot и barplot построить с помощью библиотеки Seaborn.
1 2 |
# функция countplot() сама посчитает количество наблюдений в каждой из категорий sns.countplot(x = 'Survived', data = titanic); |

1 2 3 |
# для функции barplot() количество наблюдений можно посчитать # с помощью метода .value_counts() sns.barplot(x = titanic.Survived, y = titanic.Survived.value_counts()); |

1 2 |
# относительное количество наблюдений удобно вывести с параметром normalize = True sns.barplot(x = titanic.Survived, y = titanic.Survived.value_counts(normalize = True)); |

В библиотеке Matplotlib мы можем построить только barplot (функция bar()). Отдельного инструмента для построения countplot в ней нет. Количество наблюдений мы можем найти с помощью метода .value_counts().
1 2 3 4 5 6 7 8 9 10 |
# первым параметром (по оси x) передадим уникальные значения, # вторым параметром - количество наблюдений plt.bar(titanic.Survived.unique(), titanic.Survived.value_counts(), # кроме того, явно пропишем значения оси x # (в противном случае будет указана просто числовая шкала) tick_label = ['0', '1']) plt.xlabel('Survived') plt.ylabel('Count'); |

Горизонтальную столбчатую диаграмму (horizontal barplot) можно построить с помощью функции barh().
1 2 3 4 5 6 |
plt.barh(titanic.Survived.unique(), titanic.Survived.value_counts(), tick_label = ['0', '1']) plt.xlabel('Count') plt.ylabel('Survived'); |

Снова воспользуемся параметром normalize = True метода .value_counts() для нахождения относительной частоты каждой категории признака.
1 2 3 4 5 6 |
plt.bar(titanic.Survived.unique(), titanic.Survived.value_counts(normalize = True), tick_label = ['0', '1']) plt.xlabel('Survived') plt.ylabel('Count'); |

Для того чтобы построить такой график в библиотеке Pandas, вначале необходимо сгруппировать данные по столбцу Survived, затем выбрать один столбец (например, PassengerId), посчитать количество наблюдений в каждой группе через метод .count() и наконец построить столбчатую диаграмму с помощью метода .plot.bar().
1 2 3 |
# параметр rot = 0 ставит деления шкалы по оси x вертикально titanic.groupby('Survived')['PassengerId'].count().plot.bar(rot = 0) plt.ylabel('count'); |

Код можно упростить, если сначала выбрать желаемый признак (столбец), затем воспользоваться методом .value_counts() и наконец применить метод .plot.bar().
1 2 3 |
titanic.Survived.value_counts().plot.bar(rot = 0) plt.xlabel('Survived') plt.ylabel('count'); |

Мы продолжим изучать столбчатые диаграммы, когда перейдем к находению различий между двумя категориальными признаками.
Количественные данные
df.describe()
Если применить метод .describe() к количественным данным, то результат будет отличаться от рассмотренных выше категориальных признаков.
1 2 |
# применим метод .describe() к количественным признакам tips[['total_bill', 'tip']].describe().round(2) |

Как мы видим метод выдает:
- count — количество наблюдений
- mean — среднее арифметическое
- std или standard deviation — среднее квадратическое отклонение
- min и max — минимальное и максимальное значения, а также
- 25%, 50% и 75% — первый, второй (он же медиана) и третий квартили
Здесь будет полезно сделать небольшое отступление и ближе познакомиться с новыми для нас способами оценки данных.
Среднее арифметическое и СКО
К настоящему моменту мы уже знаем, как находить среднее арифметическое и среднее квадратическое отклонение. Проблема же этих метрик, как мы уже говорили, заключается в том, что они сильно подвержены выбросам.
Квантили и робастная статистика
В этом смысле медиана дает более надежную оценку среднего при наличии выбросов.
Статистические методы и алгоритмы, устойчивые к выбросам и менее зависимые от предположений (assumptions) модели, еще называют робастными (robust statistics).
Напомню, что медианой называется число, которое находится в середине упорядоченного от меньшего к большему набора чисел. В случае нечетного количества чисел, мы просто берем то значение, которое находится посередине.

Если количество чисел четное — два срединных значения складываются и делятся на два.

Медиану можно также определить как значение, которое наши данные (или случайная величина) не превышают с вероятностью 50 процентов (отсюда знак % в выводе метода .describe()).
Аналогично можно найти, например, значение, которое величина не будет превышать с вероятностью 25 или 75 процентов. Такие значения будут называться первым и третьим квартилями (quartile, от латинского — quarta, «четверть»), потому что они делят распределение на четыре части. По-английски первый, второй и третий квартили принято обозначать как Q1, Q2 и Q3.

Кроме этого, можно найти децили (deciles, делят распределение на десять частей). Наконец, если мы хотим найти конкретное значение, то будем искать квантиль (quantile). Если вероятность выражена в процентах, то квантиль принято называть процентилем или перцентилем (percentile).
Вывести конкретный процентиль в методе .describe() можно с помощью параметра percentiles.
1 2 |
# выведем второй и четвертый дециль, а также 99-й процентиль tips[['total_bill', 'tip']].describe(percentiles = [0.2, 0.4, 0.99]).round(2) |

Закрепим полученные знания, проанализировав приведенные выше метрики.
- Медианное значение обоих признаков чуть ниже среднего арифметического
- 40 процентов чаевых были ниже 2,48 доллара
- 99 процентов чеков были ниже 48,23 доллара
Рассмотрим еще одну очень полезную меру разброса.
Межквартильный размах
Межквартильный размах (interquartile range) — робастная (устойчивая к выбросам) альтернатива среднему квадратическому отклонению. Рассчитывается как разница между третьим (Q3) и первым (Q1) квартилями.

Зачастую количественные данные удобнее анализировать с помощью графиков. Для этого есть три основных инструмента: гистограмма, график плотности и boxplot.
Гистограмма
С гистограммой мы уже знакомы.
Напомню, что для построения гистограммы мы делим наши данные на интервалы (bins) и считаем, сколько наблюдений попало в каждый из них.
Построим несколько графиков с использованием рассматриваемых нами библиотек.
1 2 |
# гистограмма распределения размера чека с помощью библиотеки Matplotlib plt.hist(tips.total_bill, bins = 10); |

1 2 |
# такую же гистограмму можно построить с помощью Pandas tips.total_bill.plot.hist(bins = 10); |

1 2 3 |
# в библиотеке Seaborn мы указываем источник данных, что будет на оси x и количество интервалов # параметр kde = True добавляет кривую плотности распределения sns.histplot(data = tips, x = 'total_bill', bins = 10, kde = True); |

1 2 3 |
# функция displot() - еще один способ построить гистограмму в Seaborn # для этого используется параметр по умолчанию kind = 'hist' sns.displot(data = tips, x = 'total_bill', kind = 'hist', bins = 10); |

Обратите внимание, что функция называется именно displot(), а не distplot()⧉, которая объявлена устаревшей и не рекомендуемой к использованию (deprecated).
1 2 3 |
# Plotly, как уже было сказано, позволяет построить интерактивную гистограмму # параметр text_auto = True выводит количество наблюдений в каждом интервале px.histogram(tips, x = 'total_bill', nbins = 10, text_auto = True) |

Что можно сказать после изучения этих графиков? Распределение скошено вправо (skewed right или positively skewed), т.е. в нем есть несколько чеков на достаточно большую сумму, которые и создают правый «хвост».
Хотя на графике эта особенность распределения более очевидна, к такому же выводу мы могли прийти проанализировав разницу между средним арифметическим и медианой.
Когда медиана меньше среднего арифметического, мы наблюдаем скошенное вправо распределение.
В целом соотношение скошенности распределения со средним арифметическом, медианой и модой приведено на графике ниже.

График плотности
С графиком плотности (density plot) мы столкнулись при изучении нормального распределения и модуля random.
Напомню, график плотности позволяет визуализировать непрерывное случайное распределение.
Построим такой график с помощью библиотеки Seaborn.
1 2 3 4 |
# для этого используем функцию displot(), которой передадим датафрейм tips, # какой признак вывести по оси x, тип графика kind = 'kde' # а также заполним график цветом через fill = True sns.displot(tips, x = 'total_bill', kind = 'kde', fill = True); |

Добавлю, для справки, что в Seaborn значение параметра kde расшифровывается как kernel density estimation (ядерная оценка плотности), непараметрический способ оценки плотности случайной величины.
boxplot
После знакомства с квантилями и робастной статистикой понимание графика box plot или как его еще называют box-and-wisker plot (ящик с усами) не вызовет сложностей.

В первую очередь замечу, что boxplot строится на проранжированных по возрастанию данных. Теперь обратим внимание на сам «ящик» (box):
- его левый край отражает первый квартиль (Q1) или 25-тый процентиль (25%)
- вертикальная полоса посередине — медиана, второй квартиль (Q2) или 50-тый процентиль (50%)
- правый край, соответственно, третий квартиль (Q3) или 75-тый процентиль (75%)
- ширина ящика равна межквартильному размаху (IQR)
Усы (whiskers), то есть линии с ромбами на концах, отражают разброс данных за пределами IQR и рассчитываются как функция от этого значения. Данные, которые находятся за пределами этого диапазона, считаются выбросами (outliers).
Давайте построим этот график с помощью Seaborn.
1 2 3 |
# для этого функции boxplot() достаточно передать параметр x # с данными необходимого столбца sns.boxplot(x = tips.total_bill); |

Проверим расчет межквартильного размаха и усов. Вновь посмотрим на результат метода .describe().

Первый и третий квартили (13,35, 24,13), а также медиана (17,80) соответствуют графику. Рассчитаем IQR.
$$ \text{IQR} = \text{Q3}-\text{Q1} = 24,13-13,35 = 10,78 $$
Теперь определим длину «усов».
$$ \text{left} = \text{Q1}-1,5 \times \text{IQR} = 13,35-1,5 \times 10,78 = -2,82 $$
$$ \text{right} = \text{Q3}+1,5 \times \text{IQR} = 24,13+1,5 \times 10,78 = 40,3 $$
Как мы видим, значения на графике совпадают с нашими расчетами. Аналогично мы можем воспользовать библиотекой Plotly.
1 2 3 |
# если передать нужный нам столбец в параметр x, # то мы получим горизонтальный boxplot px.box(tips, x = 'total_bill') |

1 2 |
# если в y, то вертикальный px.box(tips, y = 'total_bill') |

Также приведу код для библиотек Matplotlib и Pandas.
1 2 |
# boxplot в Matplotlib plt.boxplot(tips.total_bill); |
1 2 |
# boxplot в Pandas tips.total_bill.plot.box(); |
Гистограмма и boxplot
Гистограмма, с одной стороны, и boxplot, с другой, имеют свои достоинства и недостатки. В частности,
- Гистограмма хорошо выявляет полимодальность (то есть несколько мод, «горбиков» в данных), при этом она сильно зависит от выбранного количества интервалов и не показывает выбросы.
- boxplot наоборот, показывает выбросы, но не справляется с полимодальностью.
Поэтому часто бывает удобно сразу построить оба графика распределения данных. Первый подобный график мы построили при изучении нормального распределения. Теперь у нас больше знаний, и мы лучше поймем суть каждого из них.
Вначале построим совмещенный график с помощью двух библиотек: Matplotlib и Seaborn. Первую мы будем использовать для создания подграфиков (рассмотрены ниже) и подписей, вторую — для самих визуализаций.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# создадим два подграфика ax_box и ax_hist # кроме того, укажем, что нам нужны: fig, (ax_box, ax_hist) = plt.subplots(2, # две строки в сетке подграфиков, sharex = True, # единая шкала по оси x и gridspec_kw = {'height_ratios': (.15, .85)}) # пропорция 15/85 по высоте # затем создадим графики, указав через параметр ax, в какой подграфик поместить каждый из них sns.boxplot(x = tips['total_bill'], ax = ax_box) sns.histplot(x = tips['total_bill'], ax = ax_hist, bins = 10, kde = True) # добавим подписи к каждому из графиков через метод .set() ax_box.set(xlabel = '') # пустые кавычки удаляют подпись (!) ax_hist.set(xlabel = 'total_bill') ax_hist.set(ylabel = 'count') # выведем результат plt.show() |

В Plotly такой график можно построить с меньшим количеством кода.
1 2 3 4 5 |
# воспользуемся функцией histogram() px.histogram(tips, # передав ей датафрейм, x = 'total_bill', # конкретный столбец для построения данных, nbins = 10, # количество интервалов в гистограмме marginal = 'box') # и тип дополнительного графика |

Прежде чем перейти к нахождению различий между признаками, еще раз приведу график плотности и boxplot нормального распределения c показателями среднего арифметического, СКО ($\sigma$, сигма), медианы и остальных квартилей, межквартильного размаха (IQR) и других метрик.

После изучения материалов этого занятия, думаю, он стал более понятен.
Перейдем ко второй части занятия.