Здесь вы найдете: очистку данных, выбор функций, байесовскую оптимизацию, классификацию и проверку модели.
Проблема
Группа Santander - это глобальная банковская группа, возглавляемая Banco Santander S.A., крупнейшим банком в зоне евро. Он берет свое начало в Сантандере, Кантабрия, Испания. Как и в каждом банке, у них есть программа удержания, которую следует применять к неудовлетворенным клиентам.
Чтобы правильно использовать эту программу, нам необходимо разработать модель машинного обучения, чтобы классифицировать, доволен клиент или нет. Клиенты, отнесенные к категории неудовлетворенных, должны стать объектом программы удержания.
Программа удержания стоит 10 долларов на каждого клиента, а эффективное приложение (для действительно неудовлетворенных клиентов) возвращает прибыль в размере 100 долларов. В задаче классификации могут быть следующие сценарии:
- Ложноположительный результат (FP): классифицируйте клиента как НЕ УДОВЛЕТВОРЕННЫЙ, но он УДОВЛЕТВОРЕН. Стоимость: 10 долларов, заработок: 0 долларов;
- Ложноотрицательный (FN): классифицируйте покупателя как УДОВЛЕТВОРЕННЫЙ, но он НЕ УДОВЛЕТВОРЕН. Стоимость: 0 долларов, заработок: 0 долларов;
- Истинно-положительный (TP): классифицируйте клиента как НЕ УДОВЛЕТВОРЕННЫЙ, и он НЕ УДОВЛЕТВОРЕН. Стоимость: 10 долларов, заработок: 100 долларов;
- Истинно-отрицательный (TN): классифицируйте клиента как УДОВЛЕТВОРЕННЫЙ, и он УДОВЛЕТВОРЕН. Стоимость: 0 долларов, заработок: 0 долларов.
Таким образом, мы хотим минимизировать скорость FP и FN, а также максимизировать скорость TP. Для этого мы будем использовать метрическую AUC (площадь под кривой) кривой ROC (рабочая характеристика приемника), потому что она возвращает нам лучшую модель, а также лучший порог.
Вы можете проверить всю записную книжку с этим решением на моем Github.
Этот кейс был создан как часть приза за победу в конкурсе Santander Data Masters Competition. В этой статье я более подробно расскажу о самом соревновании, а также о сложных навыках, которые я изучил, и о мягких навыках, которые я использовал на своем пути к победе.
Пойдем.
1 Загрузка данных и пакетов
# Loading packages import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns import time %matplotlib inline # Loading the Train and Test datasets df_train = pd.read_csv("data/train.csv") df_test = pd.read_csv("data/test.csv")
Данные можно найти в этом старом Сантандерс Соревновании.
2 Базовый исследовательский анализ
На этом этапе давайте обратимся к следующим моментам:
- Являются ли данные в столбцах числовыми или их нужно кодировать?
- Можно ли использовать тестовый набор данных или он полезен только для соревнований Kaggle?
- Есть ли недостающие данные?
- Какова доля недовольных клиентов (1) в наборе данных df_train?
- Имеет ли смысл применять к данным метод выбора характеристик?
Прежде всего, давайте сделаем первый обзор набора данных и его функций.
# Checking the first 5 rows of df_train df_train.head()
# Checking the first 5 rows of df_test df_test.head()
# Checking the genearl infos of df_train and df_test df_train.info()
# Checking the genearl infos of df_test df_test.info()
Глядя на выходные данные ячеек выше, мы можем сказать, что:
- Все столбцы уже в числовом формате. Это означает, что нам не нужно кодировать для преобразования любого типа переменной в числовую переменную;
- Поскольку это анонимный набор данных, мы не знаем, существуют ли категориальные переменные. Таким образом, для решения этой проблемы нет необходимости кодировать.
- Наконец, df_train имеет 371 столбец, а df_test - 370 столбцов. Это происходит потому, что, поскольку он взят из наборов данных о соревнованиях, df_test не имеет столбца Target.
Еще один важный момент - проверить, есть ли какие-либо пропущенные значения в этих наборах данных. Давай проверим.
# Checking if is there any missing value in both train and test datasets df_train.isnull().sum().sum(), df_test.isnull().sum().sum()
Теперь мы можем сделать вывод, что ни в одном наборе данных нет недостающих данных.
Наконец, давайте посмотрим, какова доля неудовлетворенных клиентов (наша цель) в наборе данных df_train.
# Investigating the proportion of unsatisfied customers on df_train rate_insatisfied = df_train.TARGET.value_counts()[1] / df_train.TARGET.value_counts()[0] rate_insatisfied * 100
У нас есть крайне несбалансированный набор данных, примерно 4,12% положительных результатов. Это необходимо учитывать в двух ситуациях:
- Разделить данные на обучение и тестирование;
- Для выбора гиперпараметров, таких как «вес_класса», с помощью случайного леса.
3 Dataset Split (поезд - тест)
Поскольку метод train_test_split выполняет сегментацию случайным образом, даже с чрезвычайно несбалансированным набором данных, разделение должно происходить таким образом, чтобы и обучение, и тестирование имели одинаковую долю неудовлетворенных клиентов.
Однако, поскольку сложно гарантировать случайность Фактически, мы можем сделать стратифицированное разбиение на основе переменной TARGET, таким образом гарантируя, что пропорция будет точной в обоих наборах данных.
from sklearn.model_selection import train_test_split # Spliting the dataset on a proportion of 80% for train and 20% for test. X_train, X_test, y_train, y_test = train_test_split(df_train.drop('TARGET', axis = 1), df_train.TARGET, train_size = 0.8, stratify = df_train.TARGET, random_state = 42) # Checking the split X_train.shape, y_train.shape[0], X_test.shape, y_test.shape[0]
Мы успешно разделили тест на данные поезда (X_train, y_train) и данные теста (X_test, y_test).
4 Выбор функций
Поскольку наборы данных для обучения и тестирования относительно велики (около 76 тыс. Строк и 370 столбцов), и мы не знаем, что представляет каждая функция и как они могут повлиять на модель, для этого требуется выбор функций по трем причинам:
- Знать, какие функции обеспечивают наиболее релевантную предсказательную силу модели;
- Избегайте использования функций, которые могут снизить производительность модели;
- Сведите к минимуму вычислительные затраты, используя минимальное количество функций, обеспечивающих наилучшую производительность модели.
По этой причине мы постараемся ответить на следующие вопросы:
- Есть ли какие-либо постоянные и / или полуконстантные функции, которые можно удалить?
- Есть ли повторяющиеся функции?
- Имеет ли смысл выполнять дополнительную фильтрацию, чтобы охватить меньшую группу функций?
4.1 Удаление признаков с низкой дисперсией
Если дисперсия мала или близка к нулю, функция приблизительно постоянна и не улучшит производительность модели. В таком случае его следует удалить.
# Investigating if there are constant or semi-constat feature in X_train from sklearn.feature_selection import VarianceThreshold # Removing all features that have variance under 0.01 selector = VarianceThreshold(threshold = 0.01) selector.fit(X_train) mask_clean = selector.get_support() X_train = X_train[X_train.columns[mask_clean]]
Теперь проверим, сколько столбцов было удалено.
# Total of remaning features X_train.shape[1]
Благодаря этой фильтрации было удалено 104 функции. Таким образом, набор данных стал более компактным без потери предсказательной силы. Поскольку эти функции не добавляют информацию в модель машинного обучения, они влияют на ее способность классифицировать экземпляр. Сейчас у нас осталось 266 функций.
4.2 Удаление повторяющихся функций
По очевидной причине удаление повторяющихся функций улучшает производительность нашей модели за счет удаления избыточной информации, а также позволяет работать с более легким набором данных.
# Checking if there is any duplicated column remove = [] cols = X_train.columns for i in range(len(cols)-1): column = X_train[cols[i]].values for j in range(i+1,len(cols)): if np.array_equal(column, X_train[cols[j]].values): remove.append(cols[j]) # If yes, than they will be dropped here X_train.drop(remove, axis = 1, inplace=True)
А теперь проверим результат.
# Checking if any column was dropped X_train.shape
До проверки на наличие повторяющихся функций было 266 столбцов, а теперь их 251. Таким образом, было 15 повторяющихся функций.
4.3 Использование SelectKBest для выбора функций
Существует два типа методов оценки характеристик с помощью SelectKBest: f_classif (fc) и duplic_info_classif (mic). Первый работает лучше всего, когда функции и цель имеют более линейную связь. Второй более уместен, когда есть нелинейные отношения.
Поскольку набор данных анонимизирован, а количество объектов слишком велико для качественного исследования взаимосвязи объект-объект, будут протестированы оба метода, и будет выбран тот, который дает стабильную область с наивысшим значением AUC.
Для этого различные значения K будут протестированы с помощью класса SelectKBest, который будет использоваться для обучения модели XGBClassifier и будет оцениваться с использованием метрики AUC. Имея набор значений, будет создан график для fc и другой для mic.
Таким образом, с помощью визуального анализа можно выбрать наилучшее значение K, а также лучший метод оценки характеристик.
from sklearn.feature_selection import SelectKBest, f_classif, mutual_info_classif from sklearn.metrics import roc_auc_score as auc from sklearn.model_selection import cross_val_score import xgboost as xgb #Create an automated routine to test different K values in each of these methods K_vs_score_fc = [] #List to store AUC of each K with f_classif K_vs_score_mic = [] #List to store AUC of each K with mutual_info_classif start = time.time() for k in range(2, 247, 2): start = time.time() # Instantiating a KBest object for each of the metrics in order to obtain the K features with the highest value selector_fc = SelectKBest(score_func = f_classif, k = k) selector_mic = SelectKBest(score_func = mutual_info_classif, k = k) # Selecting K-features and modifying the dataset X_train_selected_fc = selector_fc.fit_transform(X_train, y_train) X_train_selected_mic = selector_mic.fit_transform(X_train, y_train) # Instantiating an XGBClassifier object clf = xgb.XGBClassifier(seed=42) # Using 10-CV to calculate AUC for each K value avoinding overfitting auc_fc = cross_val_score(clf, X_train_selected_fc, y_train, cv = 10, scoring = 'roc_auc') auc_mic = cross_val_score(clf, X_train_selected_mic, y_train, cv = 10, scoring = 'roc_auc') # Adding the average values obtained in the CV for further analysis. K_vs_score_fc.append(auc_fc.mean()) K_vs_score_mic.append(auc_mic.mean()) end = time.time() # Returning the metrics related to the tested K and the time spent on this iteration of the loop print("k = {} - auc_fc = {} - auc_mic = {} - Time = {}s".format(k, auc_fc.mean(), auc_mic.mean(), end-start)) print(time.time() - start) # Computing the total time spent
Приведенный выше код возвращает два списка с 123 К-баллами. Построив их для каждого значения K, мы можем выбрать лучшее значение K, а также лучшие характеристики.
На графиках выше видно, что наилучшие значения находятся между 0,80 и 0,83 AUC. Однако графики имеют диапазон от 0,70 до 0,83 из-за малых значений K. Чтобы лучше оценить значение K и метод, который будет выбран для следующих шагов, давайте построим визуализацию только для диапазона между 0,80 и 0,83.
# Ploting K_vs_score_fc e K_vs_score_mic (# of K-Best features vs AUC) import matplotlib.patches as patches # Figure setup fig, ax = plt.subplots(1, figsize = (20, 8)) plt.title('Score valeus for each K', fontsize=18) plt.ylabel('Score', fontsize = 16) plt.xlabel('Value of K', fontsize = 16) ax.spines['top'].set_visible(False) ax.spines['right'].set_visible(False) plt.xticks(fontsize = 12) plt.yticks(fontsize = 12) # Create the lines plt.plot(np.arange(2, 247, 2), K_vs_score_fc, color='blue', linewidth=2) plt.plot(np.arange(2, 247, 2), K_vs_score_mic, color='grey', linewidth=2, alpha = 0.5) ax.legend(labels = ['fc', 'mic'], fontsize=14, frameon=False, loc = 'upper left') ax.set_ylim(0.80, 0.825); # Create a Rectangle patch rect = patches.Rectangle((82, 0.817), 20, (0.823 - 0.817), linewidth=2, edgecolor='r', facecolor='none') # Add the patch to the Axes ax.add_patch(rect) plt.show()
Анализируя, какой метод обеспечивает лучший набор функций, которые следует использовать, мы обращаем внимание на два основных момента:
- Наименьшее количество функций, генерирующих наивысшее значение AUC;
- Область кривой, где K-особенности более стабильны, потому что, если K является просто пиком, это может внести некоторую нестабильность в модель.
Применяя эти условия к вышеприведенным графикам для fc и mic, мы заметили, что, хотя метод weak_info_classif дает несколько лучшие результаты, он не дает нам какой-либо стабильной области.
Поэтому мы выбрали для K значение 96, что стало бы промежуточной точкой в этом регионе.
Чтобы лучше понять выбранные функции, давайте визуализируем гистограмму для первых 30 функций, набравших наибольшее количество баллов.
# Ploting the score for the best 30 features feature_score = pd.Series(selector_fc.scores_, index = X_train.columns).sort_values(ascending = False) fig, ax = plt.subplots(figsize=(20, 12)) ax.barh(feature_score.index[0:30], feature_score[0:30]) plt.gca().invert_yaxis() ax.set_xlabel('K-Score', fontsize=18); ax.set_ylabel('Features', fontsize=18); ax.set_title('30 best features by its K-Score', fontsize = 20) plt.yticks(fontsize = 14) plt.xticks(fontsize = 14) ax.spines['top'].set_visible(False) ax.spines['right'].set_visible(False) ax.spines['left'].set_visible(False);
Теперь, когда у нас есть список функций, которые мы будем использовать для этой задачи, мы можем создать набор данных, в котором должны присутствовать только желаемые функции.
# Creating datasets where only the selected 96 features are included X_train_selected = X_train[selected_col] X_test_selected = X_test[selected_col]
Теперь, когда у нас есть хорошее понимание функций и хороший выбор тех, которые оказывают наибольшее влияние на модель, мы можем перейти к следующим шагам.
5 Байесовская оптимизация для XGBClassifier
Для этой задачи классификации мы собираемся использовать алгоритм классификатора XGBoost. Этот алгоритм известен своей высокой производительностью, надежностью и простотой понимания процесса обучения.
Итак, с помощью алгоритма интересным подходом является оптимизация его гиперпараметров таким образом, чтобы мы могли добиться максимальной производительности, которую он может нам предложить.
Для этой задачи мы будем использовать подход байесовской оптимизации. В некоторых статьях доказывается его более высокая эффективность по сравнению с поиском по сетке и производительность, аналогичная или даже лучше, чем случайный поиск. Еще одно преимущество заключается в том, что байесовская оптимизация позволяет нам одновременно оптимизировать несколько гиперпараметров.
# Function for hyperparamters tunning # Implementation learned on a lesson of Mario Filho (Kagle Grandmaster) for parametes optmization. # Link to the video: https://www.youtube.com/watch?v=WhnkeasZNHI from skopt import forest_minimize def tune_xgbc(params): """Function to be passed as scikit-optimize minimizer/maximizer input Parameters: Tuples with information about the range that the optimizer should use for that parameter, as well as the behavior that it should follow in that range. Returns: float: the metric that should be minimized. If the objective is maximization, then the negative of the desired metric must be returned. In this case, the negative AUC average generated by CV is returned. """ #Hyperparameters to be optimized print(params) learning_rate = params[0] n_estimators = params[1] max_depth = params[2] min_child_weight = params[3] gamma = params[4] subsample = params[5] colsample_bytree = params[6] #Model to be optimized mdl = xgb.XGBClassifier(learning_rate = learning_rate, . n_estimators = n_estimators, max_depth = max_depth, min_child_weight = min_child_weight, gamma = gamma, subsample = subsample, colsample_bytree = colsample_bytree, seed = 42) #Cross-Validation in order to avoid overfitting auc = cross_val_score(mdl, X_train_selected, y_train, cv = 10, scoring = 'roc_auc') print(auc.mean()) # as the function is minimization (forest_minimize), we need to use the negative of the desired metric (AUC) return -auc.mean()
Теперь выберем интервалы гиперпараметров и вызовем функцию.
# Creating a sample space in which the initial randomic search should be performed space = [(1e-3, 1e-1, 'log-uniform'), # learning rate (100, 2000), # n_estimators (1, 10), # max_depth (1, 6.), # min_child_weight (0, 0.5), # gamma (0.5, 1.), # subsample (0.5, 1.)] # colsample_bytree # Minimization using a random forest with 20 random samples and 50 iterations for Bayesian optimization. result = forest_minimize(tune_xgbc, space, random_state=42, n_random_starts=20, n_calls=50, verbose=1)
Теперь мы можем проверить лучшие гиперпараметры, найденные с помощью байесовской оптимизации.
# Hyperparameters optimized values hyperparameters = ['learning rate', 'n_estimators', 'max_depth', 'min_child_weight', 'gamma', 'subsample', 'colsample_bytree'] for i in range(0, len(result.x)): print('{}: {}'.format(hyperparameters[i], result.x[i]))
Чтобы завершить эту часть, мы также можем визуализировать сходимость оптимизации для параметров, которые приводят к наивысшему AUC.
На практике отрицательный результат AUC был минимизирован, то есть чем ниже значение, тем лучше выполнялись выбранные параметры.
Таким образом, можно заметить некоторые очень важные скачки наряду с итерациями байесовской оптимизации. Вблизи шестой итерации отрицательное значение AUC уже сигнализирует о том, что найденные гиперпараметры достигли стабильной области и оптимизированные значения для AUC.
Поэтому переходим к следующим шагам с найденными оптимальными значениями гиперпараметров.
6 Оценка модели
Теперь, когда известны наиболее важные функции и лучшие гиперпараметры модели, эту модель можно применить к тестовым данным для ее окончательной оценки. Опять же, здесь будет использоваться метрика AUC.
# Generating the model with the optimized hyperparameters clf_optimized = xgb.XGBClassifier(learning_rate = result.x[0], n_estimators = result.x[1], max_depth = result.x[2], min_child_weight = result.x[3], gamma = result.x[4], subsample = result.x[5], colsample_bytree = result.x[6], seed = 42) # Fitting the model to the X_train_selected dataset clf_optimized.fit(X_train_selected, y_train)
Теперь, когда наша модель настроена и обучена, давайте протестируем ее на X_test_select, части, которую мы выделили из данных обучения в части 3.
# Evaluating the performance of the model in the test data (which have not been used so far). y_predicted = clf_optimized.predict_proba(X_test_selected)[:,1] auc(y_test, y_predicted)
Итак, показатель AUC нашей модели составил 0,8477. Пока что неплохо!
Поскольку наборы данных взяты из старого конкурса Kaggle, мы можем проверить производительность модели на тестовых данных (df_test) и оценить ее на веб-сайте Kaggle. Таким образом, мы можем увидеть, какой показатель AUC оценивает наша модель для 75818 экземпляров, которых она никогда раньше не видела.
# making predctions on the test dataset (df_test), from Kaggle, with the selected features and optimized parameters y_predicted_df_test = clf_optimized.predict_proba(df_test[selected_col])[:, 1] # saving the result into a csv file to be uploaded into Kaggle late subimission # https://www.kaggle.com/c/santander-customer-satisfaction/submit sub = pd.Series(y_predicted_df_test, index = df_test['ID'], name = 'TARGET') sub.to_csv('data/df_test_predictions.csv')
Кажется, что наша модель также очень хорошо справилась с набором данных, которого она никогда раньше не видела. Это великолепно!
7 Анализ результатов
На протяжении всего процесса разработки модели были предприняты шаги по выбору функций и оптимизации для создания надежной модели, способной хорошо обобщать новые данные и максимизировать прибыль за счет правильной классификации клиентов за счет минимизации количества FP и максимизации количества TP.
Итак, используя показатель AUC, мы пришли к модели, которая:
- На тестовых данных, разделенных на шаге 3, оценка AUC составила 0,8477;
- По данным Kaggle, в 75818 новых случаях AUC составила 0,8305.
Таким образом, можно сделать вывод, что цель создания модели, которая максимизирует прибыль, была удовлетворительно достигнута.
Кроме того, мы можем проанализировать кривую ROC, чтобы лучше понять AUC, созданную моделью. Это дает лучшее понимание того, как может быть достигнута максимизация прибыли, и мы можем увидеть оптимальную точку для порога решения по классификации. Давайте построим это.
# Code base on this post: https://stackoverflow.com/questions/25009284/how-to-plot-roc-curve-in-python import sklearn.metrics as metrics # Calculate FPR and TPR for all thresholds fpr, tpr, threshold = metrics.roc_curve(y_test, y_predicted) roc_auc = metrics.auc(fpr, tpr) # Plotting the ROC curve import matplotlib.pyplot as plt fig, ax = plt.subplots(figsize = (20, 8)) plt.title('Receiver Operating Characteristic', fontsize=18) plt.plot(fpr, tpr, 'b', label = 'AUC = %0.4f' % roc_auc) ax.spines['top'].set_visible(False) ax.spines['right'].set_visible(False) plt.legend(loc = 'upper left', fontsize = 16) plt.plot([0, 1], [0, 1],'r--') plt.xlim([0, 1]) plt.ylim([0, 1]) plt.ylabel('True Positive Rate', fontsize = 16) plt.xlabel('False Positive Rate', fontsize = 16) plt.show()
Наконец, анализируя кривую ROC, мы можем выбрать порог, который максимизирует прибыль. В этом случае следует выбрать точку, где кривая AUC приближается (кратчайшее расстояние) к вершине оси y. Таким образом, можно сделать вывод, что необходимо выбрать порог, который дает FPR, равный 0,12, и TPR, равный приблизительно 0,65.
8 Следующие шаги
Для дальнейших итераций этого проекта с целью улучшения анализа и результатов я бы предложил 3 основных момента:
- Работайте над проектированием функций, создавая, если возможно, новые функции;
- Попробуйте разные алгоритмы машинного обучения и сравните их с XGBClassifier.
- Как сделал и предложил мне Кайо Мартинс (https://github.com/CaioMar/), хорошим улучшением было бы создание функции, которая вычисляет общую прибыль. Это возможно, если у нас есть значения TP и FP.
9 ссылки
- [1] Банерджи. Прашант, Всеобъемлющее руководство по выбору функций., Https://www.kaggle.com/prashant111/comprehensive-guide-on-feature-selection
- [2] Д. Бенягуев, Расширенное исследование возможностей. Https://www.kaggle.com/selfishgene/advanced-feature-exploration
- [3] M. Filho., A forma mais simples de selecionar as melhores varáveis usando Scikit-learn. Https://www.youtube.com/watch?v=Bcn5e7LYMhg&t=2027s
- [4] M. Filho., Como Remover Variáveis Irrelevantes de um Modelo de Machine Learning, https://www.youtube.com/watch?v=6-mKATDSQmk&t=1454s
- [5] M. Filho., Como Tunar Hiperparâmetros de Machine Learning Sem Perder Tempo, https://www.youtube.com/watch?v=WhnkeasZNHI
[6] Г. Капонетто., Случайный поиск vs Grid Search для оптимизации гиперпараметров, https://towardsdatascience.com/random-search-vs-grid-search-for-hyperparameter-optimization-345e1422899d - [7] А. ДЖЕЙН., Полное руководство по настройке параметров в XGBoost с кодами на Python, https://www.analyticsvidhya.com/blog/2016/03/complete-guide-parameter-tuning-xgboost-with-codes -python / [8] Как построить кривую ROC в Python, https://stackoverflow.com/questions/25009284/how-to-plot-roc-curve-in-python
- [9] F. Santana., Algoritmo K-means: Aprenda essa Técnica Essêncial através de Exemplos Passo a Passo com Python, https://minerandodados.com.br/algoritmo-k-means-python-passo-passo/
- [10] А. Жерон., Практическое машинное обучение с помощью Scikit-Learn, Keras и TensorFlow: концепции, инструменты и методы для создания интеллектуальных систем, Alta Books, Рио-де-Жанейро, 2019, 516 с.
- [11] W. McKinney., Python для анализа данных, Novatec Editora Ltda, Сан-Паулу, 2019, 613 с.