#!/usr/bin/env python
# coding: utf-8
#
#
# ## Открытый курс по машинному обучению. Сессия № 3
#
# ### Автор материала: Демо Владимир Олегович
# ## Индивидуальный проект по анализу данных:
# ## Прогноз успешного выступления на соревнованиях по Пауэрлифтингу
# In[ ]:
## подключаемые библиотеки
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
from sklearn import preprocessing
# ### 1. Описание набора данных и признаков (2 балла)
#
# Описан процесс сбора данных (если применимо), есть подробное описание решаемой задачи, в чем ее ценность, дано описание целевого и прочих признаков;
# __Процесс сбора данных:__
#
# Данные для анализа взяты с Каггл:
# https://www.kaggle.com/open-powerlifting/powerlifting-database
#
# Скачать их можно по ссылке:
# https://www.kaggle.com/open-powerlifting/powerlifting-database/downloads/powerlifting-database.zip/1
#
# __Подробное описание решаемой задачи:__
#
# Пауэрли́фтинг (англ. powerlifting; power — «сила, мощь» + lifting — «поднятие») или силовое троеборье — силовой вид спорта, суть которого заключается в преодолении сопротивления максимально тяжёлого для спортсмена веса.
#
# Пауэрлифтинг также называют силовым троеборьем. Связано это с тем, что в качестве соревновательных дисциплин в него входят три упражнения (приседания со штангой на спине (точнее на верхней части лопаток), жим штанги лежа на горизонтальной скамье и тяга штанги), которые в сумме определяют квалификацию спортсмена.
#
# На основе данных сайта "Openpowerlifting.org" постараемся дать ответ на следующие вопросы:
# 1. Влияет ли категория, в которой выступает пауэрлифтер на результаты соревнований?
# 2. Как экипировка влияет на достижение результата?
# 3. Можно ли спрогнозировать победу чемпионата на основе первых 2 испытаний (присед и жим)?
#
# __Ценность задачи:__
# У некоторых начинающих спортсменов результаты по отдельным дисциплинам в среднем лучше, чем по другим. Следовательно, необходимо понять, наксколько велик шанс спортсмена достигнуть призового места.
#
# __Описание целевого и прочих признаков:__
#
# Целевой признак:
# - Place - место в соревнованиях
#
# Прочие признаки:
# - MeetID - ID проведенного мероприятия;
# - Name - имя спортсмена (Имя Фамилия на англ. языке);
# - Sex - пол спортсмена (male - мужской /female - женский);
# - Equipment - экипировка (Multi-ply - многослойная, Raw - без экипировки, Single-ply - однослойная, Straps - бинты на кисти рук, Straps - бинты на колени ног);
# - Age - возраст спортсмена в полных годах;
# - Division - дивизион в котором выступает спортсмен (возрастная, проверка на допинг и пр.);
# - BodyweightKg - вес спортсмена (кг);
# - WeightClassKg - весовая категория спортсмена (его вес меньше или равен данной категории, кг);
# - BestSquatKg - лучший результат в дисциплине "Присед со штангой" (кг);
# - BestBenchKg - лучший результат в дисциплине "Жим штанги лежа" (кг);
# - BestDeadliftKg - лучший результат в дисциплине "Становая тяга штанги" (кг);
# - TotalKg - сумма трех испытаний (кг);
# - Wilks - коэффициент Вилкса, который позволяет на соревнованиях определить абсолютного чемпионата вне зависимости от весовой категории.
# In[ ]:
op = pd.read_csv('../../data/openpowerlifting.csv')
op.shape
# ### 2. Первичный анализ признаков
#
# Исследованы признаки, их взаимодействия, влияние на целевой признак. Исследовано распределение целевого признака (в случае задачи регрессии проведены стат-тесты на нормальность и скошенность (skewness) распределения). Если необходимо, объясняется, почему и как можно преобразовать целевой признак. Изучены выбросы и пропуски в данных;
# In[ ]:
op.head()
# In[ ]:
print(op.info())
# ### 7. Предобработка данных (4 балла)
#
# Проведена предобработка данных для конкретной модели. При необходимости есть и описано масштабирование признаков, заполнение пропусков, замены строк на числа, OheHotEncoding, обработка выбросов, отбор признаков с описанием используемых для этого методов. Корректно сделано разбиение данных на обучающую и отложенную части;
# Проверим данные на наличие пропусков. Определим долю пропусков от общего числа данных
# In[ ]:
op.isnull().sum() / op.shape[0]
# Столцы Squat4Kg, Bench4Kg, Deadlift4Kg в большинстве отсутствуют (>99%) и age в 62% случаев, следовательно данные признаки удаляем из dataset
# In[ ]:
op=op.drop(["Squat4Kg", "Bench4Kg", "Deadlift4Kg", "Age"], axis=1)
# In[ ]:
op.describe()
# Также имеем в столбцах BestSquatKg, BestBenchKg, BestDeadliftKg отрицательные значения, что по физической природе является выбросом. Спишем это на ошибки ввода, следовательно данные параметры необходимо взять по модулю
# In[ ]:
op['BestSquatKg']=np.abs(op['BestSquatKg'])
op['BestBenchKg']=np.abs(op['BestBenchKg'])
op['BestDeadliftKg']=np.abs(op['BestDeadliftKg'])
# Заменим все NaN в столбцах BestSquatKg, BestBenchKg, BestDeadliftKg, TotalKg, Wilks на "нули" (спортсмену не удалось выполнить начальный вес в состязаниях, поэтому у него суммарный вес и Вилкс тоже "ноль"
# In[ ]:
op['BestSquatKg']=op['BestSquatKg'].fillna(0)
op['BestBenchKg']=op['BestBenchKg'].fillna(0)
op['BestDeadliftKg']=op['BestDeadliftKg'].fillna(0)
op['TotalKg']=op['TotalKg'].fillna(0)
op['Wilks']=op['Wilks'].fillna(0)
# Проверим также на корректность итоговую сумму трех дисциплин. Создадим отдельную колонку, где покажем процент ошибки ввода данных. Затем удалим строчки, где ошибка больше 1%
# In[ ]:
op['TotalKg_check']=(op['TotalKg']-(op['BestSquatKg']+op['BestBenchKg']+op['BestDeadliftKg']))/op['TotalKg']
op=op.drop(op[np.abs(op['TotalKg_check'])>0.01].index)
op=op.drop(['TotalKg_check'], axis=1)
op.describe()
# Заметим, что еще остались значения Nan в признаке "BodyweightKg", следовательно удалим все строчки, где данный признак не указан, чтобы количество элементов в каждом признаке было одинаковым.
# In[ ]:
op=op.drop(op[op["BodyweightKg"].isnull()].index)
op.describe()
# Получили "очищенные данные", которые демонстрируют статистику по итоговым места в соревнованиях из 370219 строчек
# In[ ]:
op.groupby('Place')
place_stat=op['Place'].value_counts()
place_stat[0:10]
# In[ ]:
op['Place'].hist(bins=25)
# "Место" в соревнованиях определим как целевой признак. Чтобы в дальнейшем можно было провести прогноз, необходимо выполнить еще несколько подготовительных пунктов:
# - создание новых признаков
# - перевести текстовые значения в числовые для возможностей анализа и прогноза
# ### 9. Создание новых признаков и описание этого процесса (4 балла)
#
# Созданы новые признаки. Дано обоснование: логическое (например, у птиц температура тела на несколько градусов выше человеческой, значит вирус ХХХ не выживет в такой среде), физическое (например, радуга означает, что источник света расположен сзади; расчет величины по физическому закону с использованием данных признаков) или другое (скажем, признак построен после визуализации данных). Обоснование разумно описано. Полезность новых признаков подтверждена статистически или с помощью соответствующей модели;
# Создаем новый признак: "Число участий в соревнований" - 'MeetCount'. Каждый спортсмен будет иметь данные по сумме соревнований, что в дальнейшем позволяет понять насколько он опытный.
# In[ ]:
op2=op.drop(['Sex','Equipment','Division','BodyweightKg','WeightClassKg','BestSquatKg','BestBenchKg','BestDeadliftKg','TotalKg','Wilks','Place'], axis=1)
op3=op2.groupby('Name').agg(np.count_nonzero)
op4=op3.rename(columns={'MeetID': 'MeetCount'})
op4['Name']=op4.index
op = op.merge(op4, 'left', on='Name')
op.head()
# В дальнейшем признак 'Name' нам не понадобится, поэтому его удаляем.
# In[ ]:
op=op.drop(['Name'], axis=1)
# Переводим категориальные признаки в числовые
# In[ ]:
op['Division'] = pd.factorize(op.Division)[0]
op['Equipment'] = pd.factorize(op.Equipment)[0]
op['Sex'] = pd.factorize(op.Sex)[0]
op['WeightClassKg'] = pd.factorize(op.Place)[0]
op['Place'] = pd.factorize(op.Place)[0]
# In[ ]:
op.head()
# Признак "place" имеет признак: "место" в числовом значении и "дисквалификация" в буквенном. Нас не интересует не 1 место, следовательно "place" который не равен 1 (после факторризации 0) будет равен 0, а остальное 1
# In[ ]:
op['Place'][op['Place']==0]=0
op['Place'][op['Place']!=0]=1
# In[ ]:
op.head()
# ### 3. Первичный визуальный анализ данных (4 балла)
#
# Построены визуализации (распределения признаков, матрица корреляций и т.д.), описана связь с анализом данным (п. 2). Присутствуют выводы;
# In[ ]:
sns.heatmap(op.corr())
# In[ ]:
corr = op.corr()
corr
# ### 4. Инсайты, найденные зависимости (4 балла)
#
# Найдены и выдвинуты предположения о природе различных корреляций/пропусков/закономерностей и выбросов, найденных в предыдущих пунктах. Есть пояснение, почему они важны для решаемой задачи;
# Насколько видим из матрицы корреляции, место ('Place' - целевой признак) коррелирует лучше всего с WeightClassKg (весовая категория, в которой выступает спортсмен). Чем выше категория, тем меньше вероятность победы.
#
# С экипировкой никакой корреляции нет.
#
# Для прогнозы победы на основе двух первых испытаний (присед и жим) необходимо удалить данные по становой тяге, суммарный взятый вес и Вилкс, так как они являются результатом соревнований и напрямую прогнозируют победу.
# In[ ]:
op=op.drop(['BestDeadliftKg', 'TotalKg','Wilks'], axis=1)
op.head()
# ### 5. Выбор метрики (3 балла)
#
# Есть разумное обоснование выбора метрики качества модели. Описаны моменты, влияющие на выбор метрики качества (решаемая задача, цель решения, количество классов, дисбаланс классов, прочее);
# In[ ]:
target = op['Place']
train = op.drop(['Place'], axis=1)
# In[ ]:
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import GridSearchCV, StratifiedKFold
from sklearn.cross_validation import cross_val_score
# In[ ]:
dt = DecisionTreeClassifier(random_state=17, class_weight='balanced')
# In[ ]:
max_depth_values = range(1, 10)
max_features_values = range(1, 10)
tree_params = {'max_depth': max_depth_values,
'max_features': max_features_values}
# In[ ]:
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=17)
from sklearn.metrics import accuracy_score
X = train
y = target
# In[ ]:
grid=GridSearchCV(dt,tree_params,cv=skf, n_jobs=-1,scoring='roc_auc',verbose=1)
# In[ ]:
grid.fit(X, y)
# In[ ]:
from sklearn.ensemble import RandomForestClassifier
# In[ ]:
dt2 = RandomForestClassifier(n_jobs=1, random_state=17)
# In[ ]:
max_depth_values = range(1, 10)
max_features_values = range(1, 10)
tree_params = {'max_depth': max_depth_values,
'max_features': max_features_values}
# In[ ]:
grid2=GridSearchCV(dt2,tree_params,cv=skf, n_jobs=-1,scoring='roc_auc',verbose=1)
# In[ ]:
grid2.fit(X, y)
# ### 8. Кросс-валидация и настройка гиперпараметров модели (4 балла)
#
# Кросс-валидация выполнена технически верно, нет утечек данных. Разумно выбрано количество фолдов и разбиение (Random/Stratified или иное), зафиксирован seed. Присутствует объяснение. Объяснены гиперпараметры модели и способ их выбора. Выбор основан на некотором исследовании гипрепараметров модели для данной задачи;
# In[ ]:
clf_best_score = grid.best_score_
clf_best_params = grid.best_params_
clf_best = grid.best_estimator_
mean_validation_scores = []
print("Лучший результат в дереве решений", clf_best_score,
"лучшие параметры", clf_best_params)
# In[ ]:
clf_best_score2 = grid2.best_score_
clf_best_params2 = grid2.best_params_
clf_best2 = grid2.best_estimator_
print("Лучший результат в случайном лесе", clf_best_score2,
"лучшие параметры", clf_best_params2)
# ### 6. Выбор модели (3 балла)
#
# Произведен выбор модели. Описан процесс выбора и связь с решаемой задачей;
# Как видно, "случайный лес" и "дерево решений" дают точный прогноз (100%) победы спортсмена
# ### 11. Прогноз для тестовой или отложенной выборке (2 балла)
#
# Указаны результаты на тестовой выборке или LB score. Результаты на тестовой выборке сравнимы с результатами на кросс-валидации. Если тестовая выборка создавалась автором проекта, то механизм создания должен быть непредвзят и объяснен (применен разумный механизм выборки, в простейшем случае – рандомизация);
# Выполним прогноз при помощи "случайного леса" с лучшими параметрами на тренировочной и отложенной выборке
# In[ ]:
from sklearn.model_selection import train_test_split
X_train, X_valid, y_train, y_valid = train_test_split(train,target,test_size=0.3, random_state=17)
# In[ ]:
dt3 = RandomForestClassifier(n_jobs=1, max_depth=2, max_features=6,random_state=17)
dt3.fit(X_train, y_train)
# In[ ]:
dt3_pred = dt3.predict(X_valid)
accuracy_score(y_valid, dt3_pred)
# In[ ]:
dt4 = DecisionTreeClassifier(class_weight='balanced',max_depth=2, max_features=5,random_state=17)
dt4.fit(X_train, y_train)
# In[ ]:
dt4_pred = dt4.predict(X_valid)
accuracy_score(y_valid, dt4_pred)
# В итоге, "дерево решений" отлично справляется с прогнозом победы на соревнованиях по Пауэрлифтингу по результатам отложенной выборки (100%), в отличие от "случайного леса", где прогноз хуже (99.7%)
# ### 12. Выводы (2 балла)
#
# Описана ценность решения, возможности применения, дальнейшие пути развития и улучшения решения;
# Прогноз победы на соревнованиях по Пауэрлифтингу имеет низкую ценность, так как на такого рода соревнованиях отсутствует тотализатор. При этом на основе первичных данных о спортсмене, его опыт, экипировка, весовая категория и результаты по первым двум испытаниям есть возможность спрогнозировать победу со 100% вероятностью.