Все курсы > Вводный курс > Занятие 15
На прошлом занятии мы говорили про регрессию. Теперь давайте поговорим про задачи классификации. Отчасти мы уже коснулись этой темы в самом начале, когда разбирали принцип машинного обучения. В этот раз мы рассмотрим похожую задачу, но уже воспользуемся Питоном для ее решения.
По традиции вначале откроем ноутбук к этому занятию⧉
Этап 1. Загрузка данных — классификация опухолей

Возьмем данные из модуля datasets библиотеки Scikit-learn. Вначале загрузим их.
1 2 3 |
# импортируем данные и поместим их в переменную cancer from sklearn.datasets import load_breast_cancer cancer = load_breast_cancer() |
Этот датасет состоит из тех же частей, что и данные по недвижимости в Бостоне. Добавился только компонент target_names, в котором содержатся названия целевых классов. Давайте сразу посмотрим на описание этого датасета.
1 |
print(cancer.DESCR) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
Breast cancer wisconsin (diagnostic) dataset -------------------------------------------- **Data Set Characteristics:** :Number of Instances: 569 :Number of Attributes: 30 numeric, predictive attributes and the class :Attribute Information: - radius (mean of distances from center to points on the perimeter) - texture (standard deviation of gray-scale values) - perimeter - area - smoothness (local variation in radius lengths) - compactness (perimeter^2 / area - 1.0) - concavity (severity of concave portions of the contour) - concave points (number of concave portions of the contour) - symmetry - fractal dimension ("coastline approximation" - 1) The mean, standard error, and "worst" or largest (mean of the three largest values) of these features were computed for each image, resulting in 30 features. For instance, field 3 is Mean Radius, field 13 is Radius SE, field 23 is Worst Radius. - class: - WDBC-Malignant - WDBC-Benign |
Как мы видим здесь собраны данные по 569 образованиям, которые могут быть злокачественными (раком груди) либо доброкачественными.
- Для каждой из 10 базовых характеристик опухоли (таких как, радиус, текстура, периметр, площадь и т.д.) рассчитаны три значения (среднее арифметическое, СКО и среднее трёх наибольших значений). Таким образом, получается 30 параметров или признаков.
- Помимо этого, каждое образование классифицировано как злокачественное или доброкачественное.
Наша задача заключается в том, чтобы построить модель, которая, используя эти признаки (все или часть из них), сможет с высокой долей уверенности говорить о том, злокачественная перед нами опухоль или нет.
Преобразуем наши данные в формат датафрейма.
1 2 3 4 5 6 7 8 9 |
# cоздадим датафрейм # названия столбцов возьмем из cancer.feature_names cancer_df = pd.DataFrame(cancer.data, columns = cancer.feature_names) # добавим целевую переменную cancer_df['target'] = cancer.target # посмотрим на первые пять наблюдений cancer_df.head() |

Когда вы работаете над новым датасетом, очень важно идти от логики исследования и того результата, который хотите получить. Если вы знаете, что вы хотите получить, то всегда напишете код, который решит поставленную задачу.
Целевая переменная обозначается как ноль и один. Посмотрим, какой цифрой обозначается доброкачественная опухоль, а какой — злокачественная.
1 2 3 4 |
# расшифруем 0 и 1 в значениях целевой переменной # для этого посчитаем сколько раз встречается 0 и сколько раз встречается 1 unique, counts = np.unique(cancer.target, return_counts = True) unique, counts |
1 |
(array([0, 1]), array([212, 357])) |
Из описания мы знаем, что в датасете 212 злокачественных опухолей и 357 доброкачественных. Получается, что ноль означает злокачественное образование, а единица — доброкачественное.
Изучим тип переменных, с которыми нам предстоит работать.
1 2 |
# посмотрим на тип переменных, используя метод .info() cancer_df.info() |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
<class 'pandas.core.frame.DataFrame'> RangeIndex: 569 entries, 0 to 568 Data columns (total 31 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 mean radius 569 non-null float64 1 mean texture 569 non-null float64 2 mean perimeter 569 non-null float64 3 mean area 569 non-null float64 4 mean smoothness 569 non-null float64 5 mean compactness 569 non-null float64 6 mean concavity 569 non-null float64 7 mean concave points 569 non-null float64 8 mean symmetry 569 non-null float64 9 mean fractal dimension 569 non-null float64 10 radius error 569 non-null float64 11 texture error 569 non-null float64 12 perimeter error 569 non-null float64 13 area error 569 non-null float64 14 smoothness error 569 non-null float64 15 compactness error 569 non-null float64 16 concavity error 569 non-null float64 17 concave points error 569 non-null float64 18 symmetry error 569 non-null float64 19 fractal dimension error 569 non-null float64 20 worst radius 569 non-null float64 21 worst texture 569 non-null float64 22 worst perimeter 569 non-null float64 23 worst area 569 non-null float64 24 worst smoothness 569 non-null float64 25 worst compactness 569 non-null float64 26 worst concavity 569 non-null float64 27 worst concave points 569 non-null float64 28 worst symmetry 569 non-null float64 29 worst fractal dimension 569 non-null float64 30 target 569 non-null int64 dtypes: float64(30), int64(1) memory usage: 137.9 KB |
Все зависимые переменные — количественные. Целевая переменная — категориальная, однако что важно, обозначена числовым значением (0 и 1).
Посмотрим на основные статистические показатели (summary statistics):
1 2 |
# для этого воспользуемся методом .describe() и округлим значения cancer_df.describe().round(2) |

Этап 2. Предварительная обработка данных
Пропущенные значения
Вначале посмотрим на пропущенные значения. Если в наборе данных пропущен существенный процент значений, то мы не сможем построить корректную модель.
1 2 |
# воспользуемся функциями isnull() и sum() cancer_df.isnull().sum() |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
mean radius 0 mean texture 0 mean perimeter 0 mean area 0 mean smoothness 0 mean compactness 0 mean concavity 0 mean concave points 0 mean symmetry 0 mean fractal dimension 0 radius error 0 texture error 0 perimeter error 0 area error 0 smoothness error 0 compactness error 0 concavity error 0 concave points error 0 symmetry error 0 fractal dimension error 0 worst radius 0 worst texture 0 worst perimeter 0 worst area 0 worst smoothness 0 worst compactness 0 worst concavity 0 worst concave points 0 worst symmetry 0 worst fractal dimension 0 target 0 dtype: int64 |
Пропущенных значений нет. Мы также могли видеть это в результате метода .info(), сравнив диапазон значений с количеством значений каждой переменной.
Преобразование категориальных переменных
Как уже было сказано, наша целевая категориальная переменная записана числами. Если бы вместо них стояли, например, слова «Да»/ «Нет», или «Злокачественное»/ «Доброкачественное», нам бы пришлось превратить эти строковые значения в цифры. Делается это из-за того, что модель не умеет оперировать словами. Она просто не сможет подобрать веса.
Нормализация данных
Помимо пропущенных значений и строковых категорий при обучении модели может возникнуть еще одна сложность. Если масштаб различных признаков сильно различается, модель может придать больше значимости признаку с большим масштабом.
Например, мы видим, что mean texture имеет разброс от 9,71 до 39,28. При этом mean area находится в диапазоне от 143,50 до 2501,00. Значит ли это, что mean area важнее для нашей модели? Не обязательно. Однако в силу особенностей алгоритма модель может отдать предпочтение (больший вес) именно этому признаку.
Для того чтобы этого не произошло данные нужно нормализовать или привести к единому масштабу (normalization, feature scaling).
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# приведем все независимые переменные к единому масштабу # снова создадим датафрейм без целевой переменной cancer_df = pd.DataFrame(cancer.data, columns = cancer.feature_names) # импортируем необходимый класс из модуля preprocessing библиотеки sklearn from sklearn.preprocessing import StandardScaler # создадим объект этого класса scaler = StandardScaler() # приведем данные к единому масштабу scaled_data = scaler.fit_transform(cancer_df) |
В переменную scaled_data был записан массив Numpy. Его нужно вновь преобразовать в датафрейм.
1 2 3 4 5 6 7 8 |
# преобразуем scaled_data обратно в датафрейм cancer_df_scaled = pd.DataFrame(scaled_data, columns = cancer.feature_names) # вновь добавим целевую переменную cancer_df_scaled['target'] = cancer.target # посмотрим на результат (только два первых значения) cancer_df_scaled.head(2) |

1 2 |
# а также на основные статистическое показатели (масштаб должен быть другим!) cancer_df_scaled.describe().round(2) |

Как мы видим, масштаб переменных изменился. Теперь у них у всех одинаковое среднее арифметическое (ноль) и одинаковое СКО (единица).
Нормализацию данных лучше производить после того, как мы разобьем нашу выборку на обучающую и тестовую часть. Для простоты кода сегодня мы сделаем наоборот.
Этап 3. Исследовательский анализ данных
Приступим к исследовательскому анализу данных (Exploratory Data Analysis, EDA). Как уже было сказано на прошлом занятии, от нас требуется выявить взаимосвязь между зависимой и независимыми переменными.
В данном случае речь идет о взаимосвязи количественных и категориальной переменной. Вот какие варианты выявления такой взаимосвязи у нас есть:
- Мы можем количественно оценить насколько будут отличаться основные статистические показатели (например, среднее арифметическое) для каждой из переменных в зависимости от класса.
- Мы можем графически построить две гистограммы на одном графике, как мы уже делали раньше.
- В рамках более продвинутого анализа мы можем оценить статистическую значимость такого различия.
На сегодняшнем занятии займемся первым и вторым пунктами.
1 2 3 4 5 6 |
# сгруппируем данные по целевой переменной, рассчитаем среднее и перевернем (транспонируем) наш датафрейм # все это последовательно делается с помощью group_by, mean() и .T data = cancer_df_scaled.groupby('target').mean().T # выведем первые два значения, чтобы убедиться в верности результата data.head(2) |

Как мы видим, теперь в строках содаржатся наши признаки и соответствующие им средние значения, разбитые на две группы (среднее значение для злокачественной опухоли и среднее для доброкачественной). Теперь давайте посчитаем разницу и вычислим модуль. Так мы сможем сравнивать получившиеся значения между собой.
1 2 3 4 5 6 7 8 9 |
# вычтем одну колонку из другой и вычислим модуль # синтаксис может показаться сложным, пока не обращайте на это внимания data['diff'] = abs(data.iloc[:, 0] - data.iloc[:, 1]) # остается отсортировать наш датафрейм по столбцу разницы средних в нисходящем порядке data = data.sort_values(by = ['diff'], ascending = False) # и вывести те значения (пусть их будет 10), где разница наиболее существенная data.head(10) |

Эту разницу в средних значениях и в целом в распределении переменной мы можем также увидеть на совмещенных гистограммах. Давайте построим такие гистограммы для признака с наибольшей разницей средних worst concave points.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
# задаем количество интервалов bins = 17 # и размер графика plt.figure(figsize = (10,6)) # из датафрейма cancer_df_scaled выберем столбец 'worst concave points' # и только те строки, где target == 0 (злокачественная) plt.hist(cancer_df_scaled.loc[cancer_df_scaled['target'] == 0, 'worst concave points'], bins, alpha = 0.5, label = 'Злокачественная') # то же самое, но target == 1 (доброкачественная) plt.hist(cancer_df_scaled.loc[cancer_df_scaled['target'] == 1, 'worst concave points'], bins, alpha = 0.5, label = 'Доброкачественная') plt.legend(loc = 'upper right') # добавим подписи и размер шрифта plt.xlabel('worst concave points', fontsize = 16) plt.ylabel('Количество наблюдений', fontsize = 16) plt.title('Распределение worst concave points для двух типов опухолей', fontsize = 16) |

Как мы видим, распределение признака отличается для злокачественных и доброкачественных образований.
Этап 4. Отбор и выделение признаков
На основе проведенного EDA, давайте возьмем десять признаков в наибольшими отличиями среднего арифметического в зависимости от значения целевой переменной. Таким образом, в нашу модель войдут: worst concave points, worst perimeter, mean concave points, worst radius, mean perimeter, worst area, mean radius, mean area, mean concavity, worst concavity.
Давайте поместим наши признаки в переменную X, а классы в переменную y. Для этого возьмем названия признаков из индекса нашего вспомогательного датафрейма data, преобразуем их в список и сделаем срез по первым 10 значениям (и все это для того, чтобы не набирать названия столбцов вручную).
Результат запишем в переменную features.
1 2 |
features = list(data.index[:10]) print(features) |
1 |
['worst concave points', 'worst perimeter', 'mean concave points', 'worst radius', 'mean perimeter', 'worst area', 'mean radius', 'mean area', 'mean concavity', 'worst concavity'] |
Теперь отфильтруем исходный датафрейм по этим признакам. Переменная features и будет нашим фильтром.
1 |
X = cancer_df_scaled[features] |
В переменную y запишем классы.
1 |
y = cancer_df_scaled['target'] |
Разумеется задачи отбора и выделения признаков (feature selection and feature extraction) и конструирования признаков (feature engineering) намного шире и сложнее. Пока мы только начинаем знакомиться с этими инструментами.
Этап 5. Обучение и оценка качества модели
Разделение на обучающую и тестовую выборки
Прежде всего разобьем наши данные на обучающую и тестовую выборки.
1 2 3 4 5 6 7 8 |
# импортируем необходимый модуль from sklearn.model_selection import train_test_split # размер тестовой выборки составит 30% # также зададим точку отсчета для воспроизводимости результата X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.3, random_state = 42) |
Обучение модели и прогноз
Задача классификации решается с помощью множества различных алгоритмов. Сегодня мы будем использовать логистическую регрессию (logistic regression). Во многом принцип ее работы был описан еще на втором занятии, когда мы изучали принцип машинного обучения.
Обратите внимание, что хотя в названии присутствует слово «регрессия», модель логистической регрессии решает задачу классификации.
1 2 3 4 5 6 7 8 9 10 11 |
# импортируем логистическую регрессию из модуля linear_model библиотеки sklearn from sklearn.linear_model import LogisticRegression # создадим объект этого класса и запишем его в переменную model model = LogisticRegression() # обучим нашу модель model.fit(X_train, y_train) # выполним предсказание класса на тестовой выборке y_pred = model.predict(X_test) |
Оценка качества модели
Те метрики, которые мы использовали для оценки качества модели регрессии, здесь, разумеется, не подойдут. Нужны новые.
Вначале построим матрицу ошибок (confusion matrix). Она в целом покажет, сколько наблюдений были правильно и неправильно классифицированы как злокачественные, и сколько — как доброкачественные. Схематично это выглядит так:

Теперь построим такую матрицу на основе прогнозных значений нашей модели.
1 2 3 4 5 6 7 8 9 10 |
# построим матрицу ошибок from sklearn.metrics import confusion_matrix # передадим ей тестовые и прогнозные значения # поменяем порядок так, чтобы злокачественные опухоли были положительным классом model_matrix = confusion_matrix(y_test, y_pred, labels = [1,0]) # для удобства создадим датафрейм model_matrix_df = pd.DataFrame(model_matrix) model_matrix_df |

Мы сделали злокачественные опухоли положительным классом (единица), потому что обычно в медицине под положительным результатом подразумевается выявление заболевания.
1 2 3 4 5 |
# добавим подписи к столбцам и строкам через параметры columns и index # столбец - это прогноз, строка - фактическое значение # 0 - добр. образование, 1 - злок. образование (только в рамках матрицы ошибок!) model_matrix_df = pd.DataFrame(model_matrix, columns = ['Прогноз добр.', 'Прогноз злок.'], index = ['Факт добр.', 'Факт злок.']) model_matrix_df |

Как мы видим, модель допустила шесть ошибок:
- Две опухоли она классифицировала как доброкачественные, хотя на самом деле это не так
- Кроме того, четыре доброкачественные опухоли были помечены как злокачественные
- Остальные значения предсказаны верно
Доля правильно предсказанных значений называется accuracy. Чтобы ее посчитать, мы берем те значения, которые предсказаны верно (TP + TN) и делим на общее количество прогнозов.
$$ accuracy = \frac{TP + TN}{TP + TN + FP + FN} $$
Так как существует определенная путаница в терминологии на русском языке, здесь и далее для метрик моделей классификации мы будем использовать только английские термины.
1 2 |
# рассчитаем accuracy или долю правильных прогнозов round((61 + 104)/(61 + 104 + 2 + 4), 2) |
1 |
0.96 |
Мы также можем воспользоваться встроенной в sklearn метрикой.
1 2 3 4 |
from sklearn.metrics import accuracy_score model_accuracy = accuracy_score(y_test, y_pred) round(model_accuracy, 2) |
1 |
0.96 |
Итак, наша модель предсказывает верный результат в 96% случаев. Кажется, что это очень хорошо. Однако, есть одна проблема. С точки зрения медицины и здравого смысла мы больше всего боимся того, что модель не сможет распознать злокачественное образование, и нас прежде всего интересует минимизация ложноотрицательныго результата, когда пациенту с онкологией мы говорим, что не стоит волноваться. Показатель accuracy не дает этой информации.
Хотя сегодня мы не будем сильнее углубляться в эту тему, скажу, что в каждом конкретном случае нужно выбирать наиболее подходящую с точки зрения решаемой задачи метрику.
Подведем итог
Мы проделали большую работу.
- После загрузки данных мы создали датафрейм, изучили переменные, а также посмотрели на основные статистические показатели.
- Обработка данных предполагала изучение пропущенных значений, кодировку категориальных переменных и нормализацию данных.
- Исследовательcкий анализ данных позволил выявить взаимосвязь нескольких независимых переменных и целевых классов.
- В рамках отбора и выделения признаков мы взяли 10 наиболее значимых признаков.
- Затем мы разделили выборку на обучающую и тестовую, обучили модель логистической регрессии и сделали прогноз. При оценке результата мы столкнулись с тем, что в случае задачи классификации доля правильных прогнозов не всегда отражает качество модели.
Вопросы для закрепления
Какие три задачи нужно решить при предварительной обработке данных?
Посмотреть правильный ответ
Ответ: (1) обработать пропущенные значения, (2) убедиться, что категориальные данные обозначены числом и (3) нормализовать или масштабировать данные
Какая задача решается в рамках исследовательского анализа данных (EDA)?
Посмотреть правильный ответ
Ответ: главным образом, от нас требуется выявить взаимосвязь между признаками и зависимой переменной
Почему accuracy не всегда позволяет адекватно оценить качество модели?
Посмотреть правильный ответ
Ответ: в зависимости от типа задачи классификации, мы можем стремиться не только увеличить долю правильно предсказанных значений (accuracy), но и минимизировать, например, ложноположительные или ложноотрицательные прогнозы
Дополнительные упражнения⧉ вы найдете в конце ноутбука.
Теперь перейдем к алгоритму кластеризации.
Ответы на вопросы
Вопрос. Почему правильно проводить нормализацию данных после разделения на train и test?
Ответ. При разделении на обучающую (train) и тестовую (test) выборки самое главное — избежать их взаимного влияния друг на друга. В противном случае, мы не сможем объективно оценить качество модели.
Эту проблему также называют проблемой утечки данных (data leakage).
Если говорить про решение этой задачи с помощью библиотеки sklearn, то, например, у класса StandardScaler есть методы .fit(), .transform() и .fit_transform(). Метод .fit() рассчитывает среднее арифметическое и СКО для масштабирования данных, метод .transform() их применяет. Метод .fit_transform() сразу делает и то, и другое.
На практике, мы могли бы (1) применить метод .fit_transform() к обучающей выборке (train dataset). Это даст нам необходимые среднее арифметическое и СКО (метод .fit()), которые затем позволят масштабировать эти данные (метод .transform()). После этого (2) мы можем применить полученные с помощью .fit() параметры для масштабирования уже тестовых данных (test dataset).
В этом случае тестовые данные не повлияют на масштабирование обучающей выборки. Другими словами, не произойдет той самой утечки данных.
Приведу пример на Питоне. Предположим, вы уже поместили ваши данные в переменные X и y.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# тогда вначале делим данные на train и test X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.30, random_state = 42) # создаем объект класса StandardScaler() scaler = StandardScaler() # масштабируем обучающую выборку X_train_scaled = scaler.fit_transform(X_train) # обучаем модель на масштабированных train данных model = LogisticRegression().fit(X_train_scaled, y_train) # используем среднее арифметическое и СКО обучающей выборки для масштабирования тестовых данных X_test_scaled = scaler.transform(X_test) # делаем прогноз y_pred = model.predict(X_test_scaled) |
Вопрос. Запутался с терминологией. Встречал в других источниках, что приведение данных к одному масштабу, которое описывается в этой статье, называют стандартизацией или z-преобразованием. А нормализацией — когда данные масштабируются в диапазон от 0 до 1.
Есть ли чёткие определения у этих масштабирований в русском языке или, чтобы не путаться, лучше пользоваться оригинальными наименованиями типа StandardScaler, MinMaxScaler и другие?
Ответ. Я думал над этим вопросом. На самом деле есть некоторая путаница в самих источниках. Я руководствовался двумя статьями.
(1) Статья в Википедии⧉, которая, в свою очередь, ссылается на Оксфордский словарь, обозначает нормализацию как наиболее общий термин.
Нормализация же включает:
- стандартизацию (standartization) или как еще говорят нормализацию по z-оценке (z-score normalization), т.е. изменение данных таким образом, чтобы их среднее (mean) было равно нулю, а СКО (standard deviation) — единице
- min-max нормализацию, т.е. приведение данных к определенному диапазону; кстати, диапазон не обязательно должен быть от 0 до 1, на занятии по нейросетям мы, например, приводим данные к диапазону от −1 до 1 (общая формула приведена ниже)
- и другие методы
(2) Еще один общий термин — feature scaling⧉ (масштабирование признаков).
Именно эти два понятия (нормализация и масштабирование) я и использовал для первого знакомства с темой преобразования данных.
На практике, конечно, главное понимать, что делает конкретный инструмент и какого результата вы с его помощью хотите достичь.
И StandardScaler, разумеется, отвечает за стандартизацию.
Более подробно все эти методы мы рассмотрим на курсе по анализу и обработке данных.
А вот формула для min-max нормализации:
$$ x’ = a + \frac{x-min(x)(b-a)}{max(x)-min(x)} $$
где a и b — это желаемые минимальное и максимальное значения.
UPD: вероятно наиболее общим термином следует считать преобразование количественных переменных (numerical data transformation), которое включает в себя:
- масштабирование (scaling), т.е. стандартизацию (standartization) и приведение данных к определенному диапазону (min-max scaling)
- нелинейные преобразования (non-linear transformations)
- нормализацию (normalization), т.е. приведение отдельных наблюдений к единичной норме (scaling individual samples to have unit norm)