Здесь вы найдете: очистку данных, выбор функций, байесовскую оптимизацию, классификацию и проверку модели.

Проблема

Группа Santander - это глобальная банковская группа, возглавляемая Banco Santander S.A., крупнейшим банком в зоне евро. Он берет свое начало в Сантандере, Кантабрия, Испания. Как и в каждом банке, у них есть программа удержания, которую следует применять к неудовлетворенным клиентам.

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

Программа удержания стоит 10 долларов на каждого клиента, а эффективное приложение (для действительно неудовлетворенных клиентов) возвращает прибыль в размере 100 долларов. В задаче классификации могут быть следующие сценарии:

  1. Ложноположительный результат (FP): классифицируйте клиента как НЕ УДОВЛЕТВОРЕННЫЙ, но он УДОВЛЕТВОРЕН. Стоимость: 10 долларов, заработок: 0 долларов;
  2. Ложноотрицательный (FN): классифицируйте покупателя как УДОВЛЕТВОРЕННЫЙ, но он НЕ УДОВЛЕТВОРЕН. Стоимость: 0 долларов, заработок: 0 долларов;
  3. Истинно-положительный (TP): классифицируйте клиента как НЕ УДОВЛЕТВОРЕННЫЙ, и он НЕ УДОВЛЕТВОРЕН. Стоимость: 10 долларов, заработок: 100 долларов;
  4. Истинно-отрицательный (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()

Глядя на выходные данные ячеек выше, мы можем сказать, что:

  1. Все столбцы уже в числовом формате. Это означает, что нам не нужно кодировать для преобразования любого типа переменной в числовую переменную;
  2. Поскольку это анонимный набор данных, мы не знаем, существуют ли категориальные переменные. Таким образом, для решения этой проблемы нет необходимости кодировать.
  3. Наконец, 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% положительных результатов. Это необходимо учитывать в двух ситуациях:

  1. Разделить данные на обучение и тестирование;
  2. Для выбора гиперпараметров, таких как «вес_класса», с помощью случайного леса.

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 столбцов), и мы не знаем, что представляет каждая функция и как они могут повлиять на модель, для этого требуется выбор функций по трем причинам:

  1. Знать, какие функции обеспечивают наиболее релевантную предсказательную силу модели;
  2. Избегайте использования функций, которые могут снизить производительность модели;
  3. Сведите к минимуму вычислительные затраты, используя минимальное количество функций, обеспечивающих наилучшую производительность модели.

По этой причине мы постараемся ответить на следующие вопросы:

  • Есть ли какие-либо постоянные и / или полуконстантные функции, которые можно удалить?
  • Есть ли повторяющиеся функции?
  • Имеет ли смысл выполнять дополнительную фильтрацию, чтобы охватить меньшую группу функций?

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 ссылки