Авторы материала: Юрий Кашницкий, программист-исследователь Mail.Ru Group, и Мария Сумарокова, старший эксперт по аналитике VimpelCom. Материал распространяется на условиях лицензии Creative Commons CC BY-NC-SA 4.0. Можно использовать в любых целях (редактировать, поправлять и брать за основу), кроме коммерческих, но с обязательным упоминанием автора материала
В задании Вам предлагается разобраться с тем, как работает дерево решений, на игрушечном примере, затем обучить и настроить деревья и (при желании) случайный лес в задаче классификации на данных Adult репозитория UCI. Ответьте на все вопросы в этой тетрадке и заполните ответы в гугл-форме.
Подключаем необходимые библиотеки
%matplotlib inline
from matplotlib import pyplot as plt
plt.rcParams['figure.figsize'] = (10, 8)
import collections
import numpy as np
import pandas as pd
import seaborn as sns
from sklearn import preprocessing
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score
from sklearn.model_selection import GridSearchCV
from sklearn.preprocessing import LabelEncoder
from sklearn.tree import DecisionTreeClassifier, export_graphviz
Цель – "на пальцах", с помощью игрушечной задачи классификации разобраться в том, как работают деревья решений. Само по себе дерево решений – довольно слабый алгоритм, но основанные на нем алгоритмы случайного леса и градиентного бустинга - пожалуй, лучшее, что есть на сегодняшний день (в задачах, где можно обойтись без нейронных сетей). Поэтому разобраться в том, как работает дерево решений, полезно.
Рассмотрим игрушечную задачу бинарной классификации: поедет ли с Вами девушка из бара? Это будет зависеть от Вашей внешности и красноречия, крепости предлагаемых напитков и, как это ни меркантильно, от количества потраченных в баре денег.
# Создание датафрейма с dummy variables
def create_df(dic, feature_list):
out = pd.DataFrame(dic)
out = pd.concat([out, pd.get_dummies(out[feature_list])], axis = 1)
out.drop(feature_list, axis = 1, inplace = True)
return out
# Некоторые значения признаков есть в тесте, но нет в трейне и наоборот
def intersect_features(train, test):
common_feat = list( set(train.keys()) & set(test.keys()))
return train[common_feat], test[common_feat]
features = ['Внешность', 'Алкоголь_в_напитке',
'Уровень_красноречия', 'Потраченные_деньги']
Обучающая выборка
df_train = {}
df_train['Внешность'] = ['приятная', 'приятная', 'приятная', 'отталкивающая',
'отталкивающая', 'отталкивающая', 'приятная']
df_train['Алкоголь_в_напитке'] = ['да', 'да', 'нет', 'нет', 'да', 'да', 'да']
df_train['Уровень_красноречия'] = ['высокий', 'низкий', 'средний', 'средний', 'низкий',
'высокий', 'средний']
df_train['Потраченные_деньги'] = ['много', 'мало', 'много', 'мало', 'много',
'много', 'много']
df_train['Поедет'] = LabelEncoder().fit_transform(['+', '-', '+', '-', '-', '+', '+'])
df_train = create_df(df_train, features)
df_train
Тестовая выборка
df_test = {}
df_test['Внешность'] = ['приятная', 'приятная', 'отталкивающая']
df_test['Алкоголь_в_напитке'] = ['нет', 'да', 'да']
df_test['Уровень_красноречия'] = ['средний', 'высокий', 'средний']
df_test['Потраченные_деньги'] = ['много', 'мало', 'много']
df_test = create_df(df_test, features)
df_test
# Некоторые значения признаков есть в тесте, но нет в трейне и наоборот
y = df_train['Поедет']
df_train, df_test = intersect_features(train=df_train, test=df_test)
df_train
df_test
Постройте от руки (или в графическом редакторе) дерево решений для этого набора данных. Дополнительно (для желающих) – можете сделать отрисовку дерева и написать код для построения всего дерева.
Вопрос 1. Какова энтропия начальной системы ($S_0$)? Под состояниями системы понимаем значения признака "Поедет" – 0 или 1 (то есть всего 2 состояния).
Вопрос 2. Рассмотрим разбиение обучающей выборки по признаку "Внешность_приятная". Какова энтропия $S_1$ левой группы, тех, у кого внешность приятная, и правой группы – $S_2$? Каков прирост информации при данном разбиении (IG)?
# Ваш код здесь
Постройте с помощью sklearn
дерево решений, обучив его на обучающей выборке. Глубину можно не ограничивать.
# Ваш код здесь
Дополнительно: отобразите дерево с помощью graphviz. Можно использовать pydot
или, например, онлайн-сервис dot2png.
# Ваш код здесь
Примерчик для проверки: 9 синих шариков и 11 желтых. Пусть шарик находится в состоянии "1", если он синий и "0" – если он желтый.
balls = [1 for i in range(9)] + [0 for i in range(11)]
Дальше пусть шарики разбиваются на 2 группы
# две группы
balls_left = [1 for i in range(8)] + [0 for i in range(5)] # 8 синих и 5 желтых
balls_right = [1 for i in range(1)] + [0 for i in range(6)] # 1 синий и 6 желтых
Реализуйте функцию для расчета энтропии Шеннона.
def entropy(a_list):
# Ваш код здесь
pass
Проверка
print(entropy(balls)) # 9 синих и 11 желтых
print(entropy(balls_left)) # 8 синих и 5 желтых
print(entropy(balls_right)) # 1 синий и 6 желтых
print(entropy([1,2,3,4,5,6])) # энтропия игральной кости с несмещенным центром тяжести
Вопрос 3. Чему равна энтропия состояния, заданного списком balls_left
?
Вопрос 4. Чему равна энтропия игральной кости с несмещенным центром тяжести?
# расчет прироста информации
def information_gain(root, left, right):
''' root - изначальный набор данных, left и right два разбиения изначального набора'''
# Ваш код здесь
pass
Вопрос 5. Каков прирост информации при разделении выборки на balls_left
и balls_right
?
# Ваш код здесь
def best_feature_to_split(X, y):
''' Выводит прирост информации при разбиении по каждому признаку'''
# Ваш код здесь
pass
Опционально:
best_feature_to_split
Описание набора:
Набор данных UCI Adult (качать не надо, все есть в репозитории): классификация людей с помощью демографических данных для прогнозирования, зарабатывает ли человек более $ 50 000 в год.
Описание признаков:
Age – возраст, количественный признак
Workclass – тип работодателя, количественный признак
fnlwgt – итоговый вес обьекта, количественный признак
Education – уровень образования, качественный признак
Education_Num – количество лет обучения, количественный признак
Martial_Status – семейное положение, категориальный признак
Occupation – профессия, категориальный признак
Relationship – тип семейных отношений, категориальный признак
Race – раса, категориальный признак
Sex – пол, качественный признак
Capital_Gain – прирост капитала, количественный признак
Capital_Loss – потери капитала, количественный признак
Hours_per_week – количество часов работы в неделю, количественный признак
Country – страна, категориальный признак
Целевая переменная: Target – уровень заработка, категориальный (бинарный) признак
Считываем обучающую и тестовую выборки.
data_train = pd.read_csv('../../data/adult_train.csv', sep=';')
data_train.tail()
data_test = pd.read_csv('../../data/adult_test.csv', sep=';')
data_test.tail()
# необходимо убрать строки с неправильными метками в тестовой выборке
data_test = data_test[(data_test['Target'] == ' >50K.')
| (data_test['Target']==' <=50K.')]
# перекодируем target в числовое поле
data_train.at[data_train['Target'] == ' <=50K', 'Target'] = 0
data_train.at[data_train['Target'] == ' >50K', 'Target'] = 1
data_test.at[data_test['Target'] == ' <=50K.', 'Target'] = 0
data_test.at[data_test['Target'] == ' >50K.', 'Target'] = 1
Первичный анализ данных.
data_test.describe(include='all').T
data_train['Target'].value_counts()
fig = plt.figure(figsize=(25, 15))
cols = 5
rows = np.ceil(float(data_train.shape[1]) / cols)
for i, column in enumerate(data_train.columns):
ax = fig.add_subplot(rows, cols, i + 1)
ax.set_title(column)
if data_train.dtypes[column] == np.object:
data_train[column].value_counts().plot(kind="bar", axes=ax)
else:
data_train[column].hist(axes=ax)
plt.xticks(rotation="vertical")
plt.subplots_adjust(hspace=0.7, wspace=0.2)
Проверяем типы данных
data_train.dtypes
data_test.dtypes
Выяснилось, что в тесте возраст отнесен к типу object, необходимо это исправить.
data_test['Age'] = data_test['Age'].astype(int)
Также приведем показатели типа float в int для соответствия train и test выборок.
data_test['fnlwgt'] = data_test['fnlwgt'].astype(int)
data_test['Education_Num'] = data_test['Education_Num'].astype(int)
data_test['Capital_Gain'] = data_test['Capital_Gain'].astype(int)
data_test['Capital_Loss'] = data_test['Capital_Loss'].astype(int)
data_test['Hours_per_week'] = data_test['Hours_per_week'].astype(int)
Заполним пропуски в количественных полях медианными значениями, а в категориальных – наиболее часто встречающимся значением
# выделим в выборках категориальные и числовые поля
categorical_columns_train = [c for c in data_train.columns
if data_train[c].dtype.name == 'object']
numerical_columns_train = [c for c in data_train.columns
if data_train[c].dtype.name != 'object']
categorical_columns_test = [c for c in data_test.columns
if data_test[c].dtype.name == 'object']
numerical_columns_test = [c for c in data_test.columns
if data_test[c].dtype.name != 'object']
print('categorical_columns_test:', categorical_columns_test)
print('categorical_columns_train:', categorical_columns_train)
print('numerical_columns_test:', numerical_columns_test)
print('numerical_columns_train:', numerical_columns_train)
# заполним пропуски
for c in categorical_columns_train:
data_train[c] = data_train[c].fillna(data_train[c].mode())
for c in categorical_columns_test:
data_test[c] = data_test[c].fillna(data_train[c].mode())
for c in numerical_columns_train:
data_train[c] = data_train[c].fillna(data_train[c].median())
for c in numerical_columns_test:
data_test[c] = data_test[c].fillna(data_train[c].median())
Кодируем категориальные признаки 'Workclass', 'Education', 'Martial_Status', 'Occupation', 'Relationship', 'Race', 'Sex', 'Country'. Это можно сделать с помощью метода pandas get_dummies
.
data_train = pd.concat([data_train, pd.get_dummies(data_train['Workclass'],
prefix="Workclass"),
pd.get_dummies(data_train['Education'], prefix="Education"),
pd.get_dummies(data_train['Martial_Status'], prefix="Martial_Status"),
pd.get_dummies(data_train['Occupation'], prefix="Occupation"),
pd.get_dummies(data_train['Relationship'], prefix="Relationship"),
pd.get_dummies(data_train['Race'], prefix="Race"),
pd.get_dummies(data_train['Sex'], prefix="Sex"),
pd.get_dummies(data_train['Country'], prefix="Country")],
axis=1)
data_test = pd.concat([data_test, pd.get_dummies(data_test['Workclass'], prefix="Workclass"),
pd.get_dummies(data_test['Education'], prefix="Education"),
pd.get_dummies(data_test['Martial_Status'], prefix="Martial_Status"),
pd.get_dummies(data_test['Occupation'], prefix="Occupation"),
pd.get_dummies(data_test['Relationship'], prefix="Relationship"),
pd.get_dummies(data_test['Race'], prefix="Race"),
pd.get_dummies(data_test['Sex'], prefix="Sex"),
pd.get_dummies(data_test['Country'], prefix="Country")],
axis=1)
data_train.drop(['Workclass', 'Education', 'Martial_Status',
'Occupation', 'Relationship', 'Race', 'Sex', 'Country'],
axis=1, inplace=True)
data_test.drop(['Workclass', 'Education', 'Martial_Status', 'Occupation',
'Relationship', 'Race', 'Sex', 'Country'],
axis=1, inplace=True)
data_test.describe(include='all').T
set(data_train.columns) - set(data_test.columns)
data_train.shape, data_test.shape
В тестовой выборке не оказалось Голландии. Заведем необходимый признак из нулей.
data_test['Country_ Holand-Netherlands'] = np.zeros([data_test.shape[0], 1])
set(data_train.columns) - set(data_test.columns)
data_train.head(2)
data_test.head(2)
X_train=data_train.drop(['Target'], axis=1)
y_train = data_train['Target']
X_test=data_test.drop(['Target'], axis=1)
y_test = data_test['Target']
Обучите на имеющейся выборке дерево решений (DecisionTreeClassifier
) максимальной глубины 3 и получите качество на тесте. Используйте параметр random_state
= 17 для воспроизводимости результатов.
tree = # Ваш код здесь
tree.fit # Ваш код здесь
Сделайте с помощью полученной модели прогноз для тестовой выборки.
tree_predictions = tree.predict # Ваш код здесь
accuracy_score # Ваш код здесь
Вопрос 6. Какова доля правильных ответов дерева решений на тестовой выборке при максимальной глубине дерева = 3 и random_state = 17?
Обучите на имеющейся выборке дерево решений (DecisionTreeClassifier
, опять random_state
= 17 ). Максимальную глубину настройте на кросс-валидации с помощью GridSearchCV
. Проведите 5-кратную кросс-валидацию
tree_params = {'max_depth': range(2,11)}
locally_best_tree = GridSearchCV # Ваш код здесь
locally_best_tree.fit # Ваш код здесь
print("Best params:", locally_best_tree.best_params_)
print("Best cross validaton score", locally_best_tree.best_score_)
Обучите на имеющейся выборке дерево решений максимальной глубины 9 (это лучшее значение max_depth
в моем случае) и оцените долю правильных ответов на тесте. Используйте параметр random_state = 17 для воспроизводимости результатов.
tuned_tree = # Ваш код здесь
tuned_tree.fit # Ваш код здесь
tuned_tree_predictions = tuned_tree.predict # Ваш код здесь
accuracy_score # Ваш код здесь
Вопрос 7. Какова доля правильных ответов дерева решений на тестовой выборке при максимальной глубине дерева = 9 и random_state = 17?
Немного забежим вперед и попробуем в нашей задаче случайный лес. Пока можно его себе представлять, как куча деревьев решений, обученных на немного разных подвыборках исходной обучающей выборки, причем эта куча деревьев обычно работает существенно лучше, чем отдельные деревья.
Обучите на имеющейся выборке случайный лес (RandomForestClassifier
), число деревьев сделайте равным ста, а random_state
= 17.
rf = # Ваш код здесь
rf.fit # Ваш код здесь
Сделайте с помощью полученной модели прогноз для тестовой выборки.
forest_predictions = rf.predict # Ваш код здесь
accuracy_score # Ваш код здесь
Обучите на имеющейся выборке случайный лес (RandomForestClassifier
). Максимальную глубину и максимальное число признаков для каждого дерева настройте с помощью GridSearchCV.
forest_params = {'max_depth': range(10, 21),
'max_features': range(5, 105, 10)}
locally_best_forest = GridSearchCV # Ваш код здесь
locally_best_forest.fit # Ваш код здесь
print("Best params:", locally_best_forest.best_params_)
print("Best cross validaton score", locally_best_forest.best_score_)
Сделайте с помощью полученной модели прогноз для тестовой выборки.
tuned_forest_predictions = locally_best_forest.predict # Ваш код здесь
accuracy_score # Ваш код здесь