import numpy as np
import pandas as pd
import scipy
import matplotlib as mpl
import matplotlib.pyplot as plt
import seaborn as sns
import scipy.stats as stats
from statsmodels.stats.multitest import multipletests
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn import tree
Вам разрешается пользоваться любыми источниками информации (Python Docs, StackOverflow, etc.), кроме ваших коллег.
Если домашние задания будут значительно совпадать, то они могут быть аннулированы на усмотрение преподавателя.
Задание не должно совпадать с анализом, уже выложенным в Интернет.
Датасет надо выбрать на странице до 23:59:59 10 мая 2021
Проектное домашнее задание нужно отправить на почту python.msu@mail.ru до 23:59:59 22 мая 2021.
Тема письма должна быть "Фамилия Имя - Проект".
К письму нужно прикрепить эту Jupyter-тетрадь с названием "Family_Name_project.ipynb" и ваш файл с датасетом с тем же названием, как вы используете в коде. Файлы должны лежать в zip-архиве в папке "Family_Name". Пример:
Ivanov_Ivan.zip
|-- Ivanov_Ivan
|-- Ivanov_Ivan_project.ipynb
|-- titanic_train.csv
|-- titanic_test.csv # не нужен, если не используется в коде
Творческое задание на навыки работы с библиотекой Pandas.
Что необходимо сделать:
train
и test
, вам обычно нужна только часть train
. Данные должны удовлетворять следующим условиям:test
. В этой части не указан один или несколько признаков (обычно категориальных или числовых). Попробуйте предсказать значения этого пропущенного признака по другой части датасета (train
) с помощью математической модели. Помните, что выполнение этого задания потребует больших временных затрат! Библиотека моделей для обучения называется scikit-learn
. [много баллов :^)]Естественно, некоторые "обязательные" пункты из этого минимума, возможно, не будут иметь смысла для ваших данных, а другие могут, наоборот, быть очень познавательными. Сделайте все, что дает информацию о датасете, и если у вас получится хороший и внятный анализ, то задание будет зачтено. Дополнительные баллы будут ставиться за осмысленный анализ и демонстрацию хорошего владения модулями, (в т.ч. бонус за сторонние модули).
Не стремитесь подогнать ваш анализ под приведенные выше пункты. Можно не делать большинство из них, но вместо этого качественно и вдумчиво проанализировать ваши данные, сделать подходящие иллюстрации и описать наблюдаемые закономерности, проблемы датасета, гипотетические артефакты...
EDA без графиков не принимаются.
Если вы решили выполнять пункт с ML, можете обращаться к преподавателям за советом по поводу выбора модели для ваших данных.
Для начала, попробуем выяснить, что представляют из себя наши данные.
df = pd.read_csv("./penguins_size.csv", encoding = 'utf-8', sep = ',')
df = df.rename(columns={"species": "Species",
"island": "Island",
"culmen_length_mm": "Culmen length (mm)",
"culmen_depth_mm": "Culmen depth (mm)",
"flipper_length_mm": "Flipper length (mm)",
"body_mass_g": "Body mass (g)",
"sex": "Sex"})
df.head()
df.shape
Всего в датафрейме 343 записи и 7 колонок.
Посчитаем число строчек с NaN и значением NA в столбце "пол", и если их немного, удалим их.
print(df.isna().sum())
print('\n')
print(df.loc[df['Sex'] == '.'])
#Всего 10 строчек - можно удалить
#Всего одна строчка с точкой в столбце "пол"
df = df[~pd.isna(df).any(axis=1)]
df = df.loc[~df['Sex'].isin(['.'])]
df
print(df.isna().sum())
print('\n')
print(df.loc[df['Sex'] == '.'])
#Убедились, что ничего не осталось, исчезло всего 11 строчек
df.shape
df.info()
Почищенный датасет состоит из 333 строчек и 7 столбцов Столбцы:
df.describe()
Выведем некоторые отдельные параметры распределения массы пингвинов.
mn = df['Body mass (g)'].min()
mx = df['Body mass (g)'].max()
mean = df['Body mass (g)'].mean()
med = df['Body mass (g)'].median()
print(f'Минимальная масса тела - {round(mn)/1000} кг., максимальная - {round(mx)/1000} кг.;')
print(f'Средняя масса - {round(mean)/1000} кг., а медиана - {round(med)/1000}.кг.')
Посчитаем среднюю массу тела для каждого вида и пола с помощью группировки.
df.groupby(['Species','Sex']).agg({'Body mass (g)':'mean'})
Представим сводную таблицу средней массы тела в зависимости от пола и острова и визуализируем её в виде теплокарты. Видно, что более крупные особи живут на острове Biscoe.
plt.figure(figsize=(5,5))
sns.heatmap(df.pivot_table('Body mass (g)', index='Sex', columns='Island', aggfunc = 'mean'),
cmap='coolwarm_r', annot=True)
Информация о цветовой палитре (представлена в виде HEX и RGB-percent кода), которую будем использовать далее:
pal = ((0.97,0.15,0.52), (0.34,0.04,0.68), (0.28,0.58,0.94))
Из общих представлений о зоологии мы знаем, что высота и длина клюва могли бы послужить хорошими признаками для определения вида. Попробуем посмотреть на то, кластеризуются ли как-то пингвины по этим двум параметрам с помощью диаграммы рассеяния.
plt.rcParams['figure.figsize']=5,5
sns.displot(x='Culmen length (mm)', y='Culmen depth (mm)', hue='Species', kind="kde", data=df, palette=(pal))
Видим, что кластеризуются, хотя есть и пересечения.
Интересно, а есть ли корреляция между признаками? Построим матрицу корреляции и представим её в виде теплокарты.
plt.figure(figsize=(7,7))
correlation_matrix = df.corr()
sns.heatmap(correlation_matrix, annot=True, cmap="coolwarm_r", center=0, linewidths=0)
Видим хорошую положительную корреляцию между длиной крыла и массой тела.
Посмотрим, как распределён вес пингвинов одновременно между полом и видом. Визуально кажется, что от вида масса тела зависит больше, чем от пола. Хотя при попарном сравнении это может различаться. А ещё видно, что самцы в среднем крупнее самок.
plt.figure(figsize=(7,7))
sns.boxplot(x='Species', y='Body mass (g)', hue='Sex', data=df, palette=pal)
Мы хотим узнать, какова внутривидовая и межвидовая изменчивость у пингвинов на примере массы тела. Для этого применим тест ANOVA, чтобы сравнить средние двух и более групп.
Создадим разнообразные переменные-фильтры.
adelie_mass = df[df['Species'] == 'Adelie']['Body mass (g)']
chinstrap_mass = df[df['Species'] == 'Chinstrap']['Body mass (g)']
gentoo_mass = df[df['Species'] == 'Gentoo']['Body mass (g)']
#
male_mass = df[df['Sex'] == 'MALE']['Body mass (g)']
female_mass = df[df['Sex'] == 'FEMALE']['Body mass (g)']
fvalue, pvalue = stats.f_oneway(adelie_mass, chinstrap_mass, gentoo_mass)
print(f'f-value is {round(fvalue)}, p-value is {pvalue}')
Затем проведём поправку Холма на множественное тестирование.
p_val_1 = stats.f_oneway(adelie_mass, chinstrap_mass, gentoo_mass)[1]
a = multipletests(p_val_1, alpha=0.05, method='holm')
print(f'reject hypothesis - {bool(a[0])}, adjusted p-value is {float(a[1])}')
На примере видов разберём post hoc анализ сделаем ANOVA попарно для каждого пингвина и уже на трёх p-value сделаем поправку Холма.
fvalue1, pvalue1 = stats.f_oneway(adelie_mass, chinstrap_mass)
fvalu2, pvalue2 = stats.f_oneway(chinstrap_mass, gentoo_mass)
fvalue3, pvalue3 = stats.f_oneway(adelie_mass, gentoo_mass)
print(f'adj.p-value for Adelie-Chinstrap is {pvalue1},\nadj.p-value for Chinstrap-Gentoo is {pvalue2},\nadj.p-value for Adelie-Gentoo is {pvalue3}')
a = multipletests((pvalue1, pvalue2, pvalue3), alpha=0.05, method='holm')
print(f'reject hypothesis - {list(a[0])},\nadjusted p-value is {list(a[1])}')
Видим, что гипотеза H0 о том, что между тремя видами есть хотя бы одна пара, у которой вес не различается статистически значимо - отвергается. То есть в хотя бы одной паре видов различие есть. При попарном сравнении и множественной поправке, видим, что две пары видов различаются значимо (Chinstrap-Gentoo и Adelie-Gentoo), а вот пара Adelie-Chinstrap не различается значимо. Это соотносится с графиком.
a = sns.displot(x='Body mass (g)', hue='Species', kde=True, data=df, palette=(pal), alpha=0.3,
bins=40, element='step').add_legend()
a.fig.set_size_inches(10,5)
Проверим, есть ли статистически значимое различие между полами непараметрическим тестом Манна-Уитни и ANOVA.
fvalue, pvalue = stats.mannwhitneyu(male_mass, female_mass)
print(f'f-value is {round(fvalue)}, p-value is {pvalue}')
fvalue, pvalue = stats.f_oneway(male_mass, female_mass)
print(f'f-value is {round(fvalue)}, p-value is {pvalue}')
По результатам двух тестов отвергаем гипотезу о неразличимости полов и тут.
Попробуем разобраться с основами машинного обучения на примере двух задач: предсказание пола с помощью логистической регрессии и предсказание вида с помощью дерева решений. Логистическая регрессия подходит для определения бинарной категориальной переменной по численным значениям. А дерево решений подойдёт, когда признаков больше двух (три в нашем случае). Для обучения будем давать массу пингвина, длину и высоту клюва, а также длину крыльев.
#Перепишем категориальные признаки в числа
df_learn = df
df_learn['Species'] = np.where(df_learn['Species'] == 'Adelie', '1', df_learn['Species'])
df_learn['Species'] = np.where(df_learn['Species'] == 'Chinstrap', '2', df_learn['Species'])
df_learn['Species'] = np.where(df_learn['Species'] == 'Gentoo', '3', df_learn['Species'])
#
df_learn['Island'] = np.where(df_learn['Island'] == 'Torgersen', '1', df_learn['Island'])
df_learn['Island'] = np.where(df_learn['Island'] == 'Biscoe', '2', df_learn['Island'])
df_learn['Island'] = np.where(df_learn['Island'] == 'Dream', '3', df_learn['Island'])
#
df_learn['Sex'] = np.where(df_learn['Sex'] == 'MALE', '1', df_learn['Sex'])
df_learn['Sex'] = np.where(df_learn['Sex'] == 'FEMALE', '2', df_learn['Sex'])
#
df_learn.head(2)
Логистическая регрессия - аналог одного нейрона в нейросети, который настраивает веса для определения бинарного признака.
np.random.seed(999)
tr, tst = train_test_split(df_learn, shuffle=True)
x_tr = tr.drop(['Species', 'Island', 'Sex'], axis=1)
y_tr = tr['Sex']
x_tst = tst.drop(['Species', 'Island', 'Sex'], axis=1)
y_tst = tst['Sex']
x_tr.head(1)
model = LogisticRegression(solver='liblinear', random_state=0).fit(x_tr, y_tr)
print(f'Исследуются бинарные классы: {model.classes_}: 1 - самцы и 2 - самки')
print(f'Коэффициенты регрессии для 4 параметров: {model.coef_}')
print(f'Пересечение с осью OX: {model.intercept_}')
Если я правильно интерпретирую значения коэффициентов наклона регрессии, то, например, по весу пингвина, пол почти не предскажешь, а вот по высоте клюва можно неплохо предсказать.
model.score(x_tst, y_tst)
pred = np.array(model.predict(x_tst), dtype=int)
test = np.array(y_tst, dtype=int)
dif = pred - test
print(f'Предсказанный массив отличается от истинного на {np.count_nonzero(dif)} элементов из {len(dif)}.')
Мы получили модель, которая предсказывает пол пингвина в 85% случаев.
np.random.seed(999)
tr, tst = train_test_split(df_learn, shuffle=True)
x_tr = tr.drop(['Species', 'Island', 'Sex'], axis=1)
y_tr = tr['Species']
x_tst = tst.drop(['Species', 'Island', 'Sex'], axis=1)
y_tst = tst['Species']
model = tree.DecisionTreeClassifier().fit(x_tr, y_tr)
model.score(x_tst, y_tst)
pred = np.array(model.predict(x_tst), dtype=int)
test = np.array(y_tst, dtype=int)
dif = pred - test
print(f'Предсказанный массив отличается от истинного на {np.count_nonzero(dif)} элемента из {len(dif)}.')
А вот дерево решений предсказывает вид гораздо лучше, чем логистическая регрессия пол. Оно работает в 97% случаев.
В этой работе мы провели базовый Exploratory Data Analysis на примере датасета о пингвинах, посчитали некоторые статистические тесты, а также познакомились с основами машинного обучения.
В совокупности полученной информации, нельзя сделать вывод о том, по какому из параметров лучше всего производить определение вида. Больше всего мы проанализировали вес и поняли, что нельзя точно сказать, что от чего он больше зависит - от видовой принадлежности или от пола. Зато возможно, определять пингвинов лучше всего по длине и высоте клюва, но этот вопрос был исследован менее детально.