Ни для кого не секрет, что при работе с данными в области машинного обучения (и не только) значительная часть времени тратится на изучение набора данных, очистку данных, проектирование функций, и только сравнительно небольшая часть усилий идет на на создание, оценку и настройку наших моделей.

Такое распределение времени вполне разумно, поскольку точность и надежность наших результатов напрямую зависит от качества и удобства нашего набора данных.

Но иногда из-за нехватки времени или чистой лени специалисты по данным игнорируют эти важные шаги, надеясь, что алгоритмы интеллектуальной классификации или регрессии сами вычислят данные. даже бесполезная модель и, как следствие, ненадежный результат.

В этой статье я приведу пример одной из этих неприятных ситуаций (парадокс точности) и покажу вам, как работать с несбалансированным набором данных, чтобы поддерживать качество модели.

Я использовал набор данных из задачи Прогнозирование перекрестных продаж медицинского страхования на Kaggle. Короче говоря, наша задача состоит в том, чтобы построить модель, позволяющую предсказать, будут ли страхователи медицинского страхования в прошлом году также заинтересованы в страховании транспортных средств, предоставляемом компанией.

Не стесняйтесь получить доступ к моему исходному коду для этого проекта на моем Github. В README.txt вы найдете краткое описание всех файлов.

Часть 1: Парадокс точности и несбалансированный набор данных

Для начала давайте импортируем все необходимые пакеты и загрузим наши данные.

import pandas as pd
from sklearn.model_selection import train_test_split
from matplotlib import pyplot as plt
import seaborn as sns
# models
from sklearn.linear_model import LinearRegression,LogisticRegression, RidgeClassifier, SGDClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.naive_bayes import GaussianNB
from sklearn.tree import DecisionTreeClassifier
from sklearn import metrics

#loading data
data = pd.read_csv("6.Train.csv")

Теперь посмотрим на наши данные:

Кажется, мы можем отказаться от функции «id». Нам также необходимо преобразовать столбцы «Пол», «Возраст автомобиля» и «Повреждение автомобиля» в числовые данные. Давайте внесем эти изменения и снова посмотрим на данные:

#feature engineering
data = data.drop("id", axis=1)
data["Genders"] = data.Gender.apply(lambda x: 0  if x == "Male" else 1)
data =  data.drop("Gender",axis=1)
data.replace({"< 1 Year":0, "1-2 Year":1, "> 2 Years":2}, inplace=True)
data.Vehicle_Damage.replace({"Yes": 1, "No":0}, inplace=True)
data.drop_duplicates(inplace=True)

Что ж, это выглядит лучше, не так ли?

Поскольку нам слишком лень смотреть на наш набор данных глубже, мы переходим к подготовке наборов данных для обучения и проверки:

features = data.drop("Response",axis=1)
targets = data["Response"]

#splitting data
features_train, features_val, targets_train, targets_val = train_test_split(features, targets, test_size=0.2, random_state=12)

Наконец, мы можем построить нашу первую классификационную модель. Для начала попробуем логистическую регрессию и проверим ее оценку точности:

#building and evaluating model
LogReg = LogisticRegression()
LogReg.fit(features_train, targets_train)
acc_log_reg_train = round(LogReg.score(features_train, targets_train) * 100, 2)
acc_log_reg_val = round(LogReg.score(features_val, targets_val) * 100, 2)
print("LogReg accuracy score train " + str(acc_log_reg_train))
print("LogReg accuracy score val " + str(acc_log_reg_val))

Вау! 88%. Неплохой результат, учитывая, что мы почти не улучшили наш исходный набор данных. Кажется, что наша модель близка к идеальной. Но давайте воспользуемся еще одной продвинутой моделью оценки метрики под названием «оценка f1» (если ваша первая ассоциация связана с гонками, вам, вероятно, потребуется больше узнать об этой метрике, прежде чем продолжить), где 0 - наихудший результат, а 1 - лучший:

guesses = LogReg.predict(features_val)
f1_score = metrics.f1_score(targets_val, guesses)
print("LogReg f1 " + str(f1_score))

Но как? Такая высокая точность и такой ужасный счет на f1. Давайте сравним наши прогнозы с истинными, используя .value_counts:

Это выглядит действительно безумно: наша модель практически не распознает 1 класс (т.е. потенциальных покупателей). Спроси почему? Думаю, вы уже догадались, в чем проблема.

Попробуйте еще раз взглянуть на исходный набор данных, а именно на наш целевой столбец («Ответ»).

Итак, мы видим, что набор данных, представленный на Kaggle, содержит только 14% положительных наблюдений (кто на самом деле купил страховку). Остальное - отрицательные наблюдения. Итак, мы имеем дело с несбалансированным набором данных.

Следовательно, наша модель не может хорошо идентифицировать положительный класс. В обучающей выборке просто недостаточно наблюдений, чтобы делать выводы.

И поскольку большинство наблюдений в наборе для обучения и проверки отрицательны, а наша модель почти всегда предсказывает отрицательный класс, оценка точности этой модели высока. Однако предсказательная сила модели близка к нулю.

Поздравляем! Мы только что попали в ловушку «парадокса точности».

С другой стороны, у меня для вас тоже есть хорошие новости. Есть несколько способов решить проблему, и это не займет у вас много времени.

В этой статье я расскажу о 2 простых методах работы с несбалансированными наборами данных: недостаточная выборка и передискретизация.

Часть 2: Недостаточная выборка

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

Мы уже знаем, что наш доминирующий класс - «0» - 334 155 наблюдений; второстепенный класс - «1» - 46 685 наблюдений. Отличная разница. Теперь нам нужно сократить количество наблюдений второстепенных классов, и не обязательно до 46685, соотношение классов может быть 60/40.

Я немного поигрался с числами и обнаружил, что 63 000 - оптимальное количество наблюдений отрицательного класса, которое необходимо сохранить в нашем наборе данных. Теперь давайте сделаем недостаточную выборку и проверим, что у нас есть в модифицированном наборе данных:

#undersampling
positive_data = data[data["Response"] == 1]
negative_data = data[data["Response"] == 0]
short_negative_data = negative_data.iloc[:63000,]
prepared_data = pd.concat([positive_data, short_negative_data])
print(prepared_data.Response.value_counts())
sns.countplot(prepared_data.Response)
plt.show()

Итак, теперь соотношение классов почти 43/57, а это значит, что наш набор данных достаточно сбалансирован. Мы можем перейти к разделению нашего общего набора данных на наборы для обучения и проверки и, наконец, построить нашу модель:

features = prepared_data.drop("Response",axis=1)
target = prepared_data["Response"]
#train_test_split
features_train, features_val, targets_train, targets_val = train_test_split(features, target, test_size=0.2, random_state=12)
#building and evaluating models
LogReg = LogisticRegression()
LogReg.fit(features_train, targets_train)
acc_log_reg_train = round(LogReg.score(features_train, targets_train) * 100, 2)
acc_log_reg_val = round(LogReg.score(features_val, targets_val) * 100, 2)
print("LogReg accuracy score train " + str(acc_log_reg_train))
print("LogReg accuracy score val " + str(acc_log_reg_val))
guesses = LogReg.predict(features_val)
f1_score = metrics.f1_score(targets_val, guesses)
print("LogReg f1 " + str(f1_score))

Таким образом, оценка точности обновленной модели не так высока, как была раньше, но оценка f1 явно говорит о том, что эта модель намного лучше и может использоваться для прогнозов.

Оценочные метрики не очень высокие, потому что мы сократили общее количество наблюдений. Однако вы можете попробовать разработать дополнительные функции, чтобы улучшить результаты.

Часть 3: передискретизация

Передискретизация - это увеличение количества примеров второстепенных классов с целью сбалансировать набор данных. Это можно сделать по-разному, но на этот раз я буду использовать алгоритм SMOTE (Synthetic Minority Oversampling Technique). Нажмите сюда, чтобы узнать больше. Эта стратегия основана на идее создания ряда искусственных примеров, которые были бы похожи на примеры из класса меньшинства, но в то же время не дублировали бы их. Вы можете найти этот алгоритм в пакете imblearn на Python.

Я выбрал этот способ, потому что для балансировки набора данных потребуется всего 2 строки вашего кода. Подписывайтесь на меня:

features = data.drop("Response",axis=1)
target = data["Response"]

#balancing the data
oversampler = SMOTE(random_state=2)
new_features, new_targets = oversampler.fit_sample(features,target)

Теперь мы можем проверить, сбалансирован ли наш набор данных:

sns.countplot(new_targets)
plt.show()

Да, наш набор данных уже сбалансирован. В то же время у нас есть много наблюдений. Давайте построим несколько разных моделей и оценим их:

LogReg = LogisticRegression()
LogReg.fit(features_train, targets_train)
acc_log_reg_train = round(LogReg.score(features_train, targets_train) * 100, 2)
acc_log_reg_val = round(LogReg.score(features_val, targets_val) * 100, 2)
print("LogReg accuracy train " + str(acc_log_reg_train))
print("LogReg accuracy val " + str(acc_log_reg_val))
guesses = LogReg.predict(features_val)
f1_score = metrics.f1_score(targets_val, guesses)
print("LogReg f1 " + str(f1_score))


gaussian = GaussianNB()
gaussian.fit(features_train, targets_train)
acc_gaussian_train = round(gaussian.score(features_train, targets_train) * 100, 2)
acc_gaussian_val = round(gaussian.score(features_val, targets_val) * 100, 2)
print("NB accuracy train " + str(acc_gaussian_train))
print("NB accuracy val " + str(acc_gaussian_val))
guesses1 = gaussian.predict(features_val)
f1_score = metrics.f1_score(targets_val, guesses1)
print("NB f1 " + str(f1_score))

decision_tree = DecisionTreeClassifier()
decision_tree.fit(features_train, targets_train)
acc_decision_tree_train = round(decision_tree.score(features_train, targets_train) * 100, 2)
acc_decision_tree_val = round(decision_tree.score(features_val, targets_val) * 100, 2)
print("decision tree accuracy train" + str(acc_decision_tree_train))
print("decision tree accuracy val" + str(acc_decision_tree_val))
guesses2 = decision_tree.predict(features_val)
f1_scoreDT = metrics.f1_score(targets_val, guesses2)
print("decision tree " + str(f1_scoreDT))

KNC = KNeighborsClassifier(n_neighbors=2)
KNC.fit(features_train, targets_train)
acc_KNC_train = round(KNC.score(features_train, targets_train) * 100, 2)
acc_KNC_val = round(KNC.score(features_val, targets_val) * 100, 2)
print("K Neighbor accuracy train" + str(acc_KNC_train))
print("K Neighbor accuracy val" + str(acc_KNC_val))
guesses3 = KNC.predict(features_val)
f1_scoreKNC = metrics.f1_score(targets_val, guesses3)
print("K Neighbor f1 " + str(f1_scoreKNC))

Что ж, думаю, это победа! Результаты моделей после передискретизации очень хорошие. Просто посмотрите на дерево решений и оценки классификатора K-Neighbor!

Надеюсь, эта статья была для вас полезной. Не стесняйтесь комментировать эту статью.