Как выполнять EDA, использовать конвейеры, поиск по сетке и простые классификаторы

Для моего первого заключительного проекта для Springboard Data Science Career Track я решил изучить набор данных банковского маркетинга из репозитория машинного обучения UCI и применить набор стандартных моделей классификации, чтобы изучить, как они работают, и научиться находить лучший набор параметров модели, используя поиск по сетке и конвейеры. Набор данных был относительно чистым, поэтому не требовалось слишком много очистки данных, что позволило мне сосредоточиться на исследовательском анализе данных (EDA) и подборе модели.

Я пройду стандартные этапы EDA и очистки данных, проведу некоторый статистический анализ, подгоню несколько моделей классификации и сравню их эффективность, так что это сообщение в блоге может служить относительно подробным руководством для тех, кто работает над своим первым проектом Data Science. или просто хотите увидеть сквозной мыслительный процесс простой задачи Data Science. Проект был выполнен на Python.

Шаг 1. Исследовательский анализ данных (EDA)

Данные для этого проекта получены из репозитория машинного обучения UCI и представляют собой результаты прямых маркетинговых кампаний (телефонных звонков) португальского банковского учреждения. Набор данных содержит 41 188 экземпляров и 20 характеристик (входных переменных), которые включают уровень образования респондентов, социальный статус, месяц и день, результаты предыдущей маркетинговой кампании и некоторые макроэкономические показатели. Переменная предиктора («y») указывает результат маркетинговой кампании - подписался ли респондент на депозит («да») или нет («нет»).

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

Давайте рассмотрим набор данных более подробно и посмотрим, есть ли какие-нибудь интересные закономерности. Я начну с загрузки всех библиотек, которые собираюсь использовать (желательно, чтобы они были в одном месте), и самого набора данных.

#Pandas for dataframes
import pandas as pd
#Changing default display option to display all columns
pd.set_option('display.max_columns', 21)

#Numpy for numerical computing
import numpy as np

#Matplotlib for visualization
import matplotlib.pyplot as plt

#Display plots in the notebook
%matplotlib inline 

#Seaborn for easier visualization
import seaborn as sns

#Stats package for statistical analysis
from scipy import stats

#Machine learning packages
from sklearn.feature_selection import RFECV
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.model_selection import train_test_split 
from sklearn.pipeline import make_pipeline, Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import GridSearchCV
from sklearn.metrics import roc_curve, roc_auc_score, auc, accuracy_score, confusion_matrix, classification_report
from sklearn.utils import resample
#Loading the data set
df = pd.read_csv('Data/Raw_data/bank-additional-full.csv', sep=';')

Давайте посмотрим на форму набора данных, количество и типы переменных, а также на общее распределение числовых переменных. Перед этим давайте отбросим дубликаты (если есть):

#Dropping the duplicates
df = df.drop_duplicates()
#Dataframe dimensions
df.shape

Мы получаем 41 176 неповторяющихся строк, что означает, что мы потеряли всего 12 наблюдений. Затем давайте посмотрим на типы данных:

У нас есть 21 переменная (10 числовых и 11 строковых) и 41 176 неповторяющихся строк для каждой переменной. Пропущенных значений действительно нет. Давайте преобразуем все переменные типа «объект» в категориальные переменные, чтобы они правильно хранились:

#Selecting categorical columns
categorical_columns = ['job', 'marital', 'education', 'default', 'housing', 'loan', 'contact', 'month', 'day_of_week', 'poutcome', 'y']
#Looping through the columns and changing type to 'category'
for column in categorical_columns:
    df[column] = df[column].astype('category')

Затем давайте посмотрим на первые и последние 10 строк данных, чтобы убедиться, что они согласованы и есть ли какие-либо проблемы:

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

Категориальные данные

Самый простой способ понять распределение категориальных переменных - это построить гистограммы. Я использую метод value_counts () для сортировки столбцов в порядке убывания.

#Bar plots of categorical features
for feature in df.dtypes[df.dtypes == 'category'].index:
    sns.countplot(y=feature, data=df, order = df[feature].value_counts().index)
    plt.show()

Данные не показывают необычной динамики. Есть несколько редких или похожих классов, которые можно объединить в одну категорию для упрощения моделей прогнозирования. Например, категории «предприниматель» и «самозанятый» переменной «работа», а также категории «пенсионеры» и «безработные» могут быть объединены в одну. Кроме того, категории «разведены» и «холост» переменной «семейное положение» могут быть объединены в одну «единую» категорию. Аналогичное объединение может быть выполнено для уровней базового образования переменной «образование».

Оценить точные пропорции категорий по диаграммам непросто, поэтому давайте рассмотрим уровни категориальных переменных как пропорции:

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

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

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

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

Между респондентами, имеющими жилищную ссуду, и теми, у кого ее нет, наблюдается почти равный разрыв.

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

С более чем 63% всех респондентов связались по мобильному телефону. Нет записей с неизвестным типом связи.

С большинством респондентов связывались в летние месяцы, при этом более 30% всех контактов приходилось на май. Месяц контакта может существенно повлиять на желание подписаться на депозит (например, многие люди могут получать бонусы к зарплате в конце календарного года, что может быть хорошим временем, чтобы связаться с ними по поводу депозита). Такой перекос усилий предыдущих кампаний в сторону лета может потенциально негативно повлиять на результаты будущих кампаний, особенно если летние месяцы окажутся отрицательными предикторами успеха кампании.

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

Более 86% респондентов никогда не участвовали в предыдущих маркетинговых кампаниях. Следовательно, может иметь смысл позже разделить респондентов на новых клиентов (со статусом «несуществующий») и существующих клиентов (статус «неудача» или «успех»), чтобы увидеть, различаются ли предикторы успеха кампании для этих двух групп. Существующие клиенты могут быть более склонны открывать депозит, чем те, кто никогда раньше не слышал об этом банке.

Только ~ 11% респондентов текущей кампании фактически подписались на депозит в результате кампании. Это делает наш набор данных сильно несбалансированным и требует применения специальных методов для его компенсации - модель, построенная на этом несбалансированном наборе данных без использования каких-либо подходов к балансировке, может быть правильной в 88% случаев, если в результате она просто предсказывает «нет» и полностью игнорирует положительные отзывы.

Числовые данные

Затем давайте посмотрим на распределения числовых переменных:

#Histogram grid
df.hist(figsize=(10,10), xrot=-45)
#Clear the text "residue"
plt.show()

В данных нет явных ошибок. У экономических индикаторов есть несколько выдающихся пиков, но мы не имеем никакого представления о том, что их вызвало.

Распределение количества дней с момента предыдущей кампании («pdays») смещено в сторону 1000, потому что для респондентов, с которыми никогда не связались, значение равно 999. Респонденты, с которыми никогда не связались до этого, искажают переменные «кампания» и «предыдущая». к нулю. Переменную «продолжительность» необходимо будет отбросить, прежде чем мы начнем строить прогнозную модель, потому что она сильно влияет на целевой результат (например, если длительность = 0, тогда y = «нет»). Тем не менее, продолжительность вызова до выполнения вызова неизвестна.

Шаг 2. Очистка данных и разработка функций

Чтобы подготовить набор данных для использования в прогнозной модели, мы удалим неизвестные значения, избыточные переменные и объединим разреженные классы в более общие категории. Очищенный набор данных будет сохранен во фрейме данных df_cleaned.

#Creating a copy of the original data frame
df_cleaned = df.copy()
#Dropping the unknown job level
df_cleaned = df_cleaned[df_cleaned.job != 'unknown']
#Dropping the unknown marital status
df_cleaned = df_cleaned[df_cleaned.marital != 'unknown']
#Dropping the unknown and illiterate education level
df_cleaned = df_cleaned[df_cleaned.education != 'unknown']
df_cleaned = df_cleaned[df_cleaned.education != 'illiterate']
#Deleting the 'default' column
del df_cleaned['default']
#Deleting the 'duration' column
del df_cleaned['duration']
#Dropping the unknown housing loan status
df_cleaned = df_cleaned[df_cleaned.housing != 'unknown']
#Dropping the unknown personal loan status
df_cleaned = df_cleaned[df_cleaned.loan != 'unknown']

Затем давайте объединим редкие категории. «Расширенные» категории могут быть несколько произвольными, но меньшее количество переменных упростит наши прогнозные модели и значительно снизит вычислительные усилия в дальнейшем.

#Combining entrepreneurs and self-employed into self-employed
df_cleaned.job.replace(['entrepreneur', 'self-employed'], 'self-employed', inplace=True)
#Combining administrative and management jobs into admin_management
df_cleaned.job.replace(['admin.', 'management'], 'administration_management', inplace=True)
#Combining blue-collar and tecnician jobs into blue-collar
df_cleaned.job.replace(['blue-collar', 'technician'], 'blue-collar', inplace=True)
#Combining retired and unemployed into no_active_income
df_cleaned.job.replace(['retired', 'unemployed'], 'no_active_income', inplace=True)
#Combining services and housemaid into services
df_cleaned.job.replace(['services', 'housemaid'], 'services', inplace=True)
#Combining single and divorced into single
df_cleaned.marital.replace(['single', 'divorced'], 'single', inplace=True)
#Combining basic school degrees
df_cleaned.education.replace(['basic.9y', 'basic.6y', 'basic.4y'], 'basic_school', inplace=True)

У нас есть 2 переменные, которые определяют, является ли респондент новым или существующим клиентом - «poutcome» (ранее с ним связались в рамках маркетинговой кампании) и «pdays» (дни с момента предыдущего маркетингового контакта). Для нового клиента poutcome будет иметь значение «несуществующий», а «pdays» будет иметь значение «999». Посмотрим, одинаковое ли количество респондентов на каждом из этих уровней:

Цифры значительно различаются, хотя по логике они должны быть одинаковыми. Давайте проверим другую переменную, «previous», которая содержит количество контактов с клиентами, выполненных до текущей кампании. Нулевое значение укажет на новых клиентов.

Количество новых клиентов в «предыдущем» в точности совпадает с количеством «несуществующих» респондентов в переменной «poutcome». Теперь давайте проверим, имеет ли переменная pday значение «999» для любых уровней переменной poutcome, кроме «nonexistent»:

Это как раз разница в количестве значений переменных «poutcome» и «pdays», поэтому похоже, что 3812 записей переменной «pdays» ошибочно помечены как «999». Теперь, чтобы решить, как поступить с этими 3812 записями, давайте проверим, отличаются ли распределения числовых переменных для подмножества «poutcome» = «failure» и «pdays» = «999» от распределений для подмножества «poutcome». »=« Отказ »&« pdays »! =« 999. » Мы выберем для этого теста переменные age, nr.employed и cons.price.idx.

#Filtering for the rows that have 'poutcome' = 'failure' and 'pdays' = '999'
fail_999 = df_cleaned.loc[( (df_cleaned['pdays'] == 999) & (df['poutcome'] == 'failure') )]
#Filtering for 'age' and 'nr.employed' columns only
fail_999 = fail_999.loc[:, ['age', 'nr.employed', 'cons.price.idx']]
#Filtering for the rows that have 'poutcome' = 'failure' and 'pdays' != '999'
fail_no999 = df_cleaned.loc[( (df_cleaned['pdays'] != 999) & (df['poutcome'] == 'failure') )]
#Filtering for 'age', 'nr.employed' and 'cons.price.idx' columns only
fail_no999 = fail_no999.loc[:, ['age', 'nr.employed', 'cons.price.idx']]
#Plotting histograms
fail_999.hist()
fail_no999.hist()
plt.show()

Переменная «возраст», кажется, примерно одинаково распределяется как для «999», так и «не-999» уровней «poutcome», но распределения «nr.employed» и «cons.price.idx» существенно различаются. Принимая это во внимание, мы рассмотрим «pday» значения «999» для строк, которые имеют «poutcome» как «отказ», чтобы быть пропущенными переменными. Поскольку мы не можем определить правильные значения для «pdays» и поскольку нет четкого способа вменения этих значений, мы будем рассматривать их как отсутствующие значения. Обозначим их так:

#Getting the positions of the mistakenly labeled 'pdays'
ind_999 = df_cleaned.loc[(df_cleaned['pdays'] == 999) & (df['poutcome'] != 'nonexistent')]['pdays'].index.values
#Assigning NaNs instead of '999'
df_cleaned.loc[ind_999, 'pdays'] = np.nan

Поскольку это ~ 10% данных, мы можем опустить недостающие значения, так как оставшихся данных будет достаточно для дальнейшего анализа:

#Dropping NAs from the dataset
df_cleaned = df_cleaned.dropna()
#Saving the cleaned dataset as a file
df_cleaned.to_csv('cleaned_data.csv')

Шаг 3. Статистический анализ

Во-первых, давайте посмотрим, как числовые переменные в наборе данных соотносятся друг с другом:

#Calculate correlations between numeric features
correlations = df_cleaned.corr()
#Make the figsize 7 x 6
plt.figure(figsize=(7,6))
#Plot heatmap of correlations
_ = sns.heatmap(correlations, cmap="Greens")

Экономические показатели сильно коррелируют между собой. Кроме того, переменные «previous» и «pdays» имеют сильную отрицательную корреляцию (как и ожидалось), поскольку наибольшее значение «pdays» (999) соответствует наименьшему значению «previous» (0).

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

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

  • ‘Emp.var.rate’ - ‘cons.price.idx’
  • ‘Emp.var.rate’ - ‘euribor3m’
  • ‘Emp.var.rate’ - ‘nr.employed’
  • ‘Cons.price.idx’ - ‘euribor3m’
  • ‘Cons.price.idx’ - ‘nr.employed’
  • ‘Euribor3m’ - ‘nr.employed’

Кроме того, мы проверим, независимы ли наши категориальные переменные друг от друга, используя критерий хи-квадрат с уровнем достоверности 95%. Нам нужно будет проверить следующие пары:

  • «Работа» - «супружеская жизнь»
  • «Работа» - «образование»
  • «Работа» - «жилье»
  • «Работа» - «ссуда»
  • «Работа» - «контакт»
  • «Супружеский» - «образование»
  • «Супружеский» - «жилье»
  • «Супружеский» - «заем»
  • «Супружеский» - «контакт»
  • «Образование» - «жилье»
  • «Образование» - «ссуда»
  • «Образование» - «контакт»
  • 'жилищный кредит'
  • «Жилье» - «контакт»
  • «Заем» - «контакт»

Наконец, мы проведем z-тест с двумя выборками, чтобы оценить, существенно ли различаются уровни наших категориальных переменных в их реакции на маркетинговую кампанию (переменная «y»). Для этого мы сгруппируем разные уровни каждой категориальной переменной в две подгруппы и вычислим z-оценку и p-значение. Нулевая гипотеза для теста будет заключаться в том, что переменная «y» для этих подгрупп одинакова.

Анализ довольно длинный, поэтому я не буду его здесь размещать. Подробности можно найти в этой записной книжке.

Выводы статистического анализа

1) Все корреляции между парами числовых переменных статистически значимы, так как p-значения очень близки к нулю. Однако для двух пар, а именно «cons.price.idx» - «euribor3m» и «cons.price.idx» - «nr.employed» - коэффициенты корреляции невысоки.

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

3) По результатам тестов хи-квадрат заключаем, что:

  • Уровень работы зависит от уровня образования, семейного положения и типа контакта и не зависит от личной / жилищной ссуды.
  • Семейное положение зависит от уровня образования, типа контакта и уровня работы и не зависит от личной / жилищной ссуды.
  • Уровень образования не зависит от личной / жилищной ссуды и зависит от типа контакта, уровня работы и семейного положения.
  • Жилищный кредит зависит от индивидуального кредита и типа контакта, а также не зависит от уровня работы, семейного положения и уровня образования.
  • Персональная ссуда зависит от жилищной ссуды и не зависит от типа контакта, уровня работы, семейного положения и уровня образования.

4) Основываясь на результатах z-теста с двумя выборками, мы делаем вывод, что успех кампании по депозитной подписке различается для разных уровней заработной платы, семейного положения, образования, способа связи, времени года и статус жилищного кредита, что означает, что некоторые комбинации этих уровней могут иметь большее влияние на вероятность успеха кампании. Успешность кампании одинакова для разных уровней статуса личного кредита и первой / второй половины недели, поэтому эти две переменные не кажутся значимыми предикторами успеха кампании. Посмотрим, дадут ли модели прогнозов такой же результат.

Шаг 4. Подбор прогнозных моделей

Во-первых, давайте преобразуем уровни категориальных переменных в фиктивные переменные, используя стандартную функцию из Pandas. Мы исключим один уровень фиктивной переменной для каждой категориальной переменной, чтобы избежать коллинеарности (drop_first = True).

#Substituting the string predictor variable values with numbers
df_cleaned.y.replace(['yes'], 1, inplace=True)
df_cleaned.y.replace(['no'], 0, inplace=True)
df_cleaned1 = pd.get_dummies(df_cleaned, drop_first=True)
df_cleaned1.head()

Самая простая и наиболее интерпретируемая модель для прогнозирования категориальной переменной «y» - это логистическая регрессия. Чтобы определить, насколько хорошо эта модель работает в нашем случае, давайте подберем различные типы модели логистической регрессии для определения лучших коэффициентов модели, используя GradientSearchCV и конвейеры. Поскольку у нас очень несбалансированные классы данных (только ~ 10% респондентов подписались на депозиты), мы будем использовать параметр сбалансированного веса класса (class_weight = ’сбалансированный’):

#Splitting the variables into predictor and target variables
X = df_cleaned1.drop('y', axis=1)
y = df_cleaned1.y
#Setting up pipelines with a StandardScaler function to normalize the variables
pipelines = {
    'l1' : make_pipeline(StandardScaler(), 
                         LogisticRegression(penalty='l1' , random_state=42, class_weight='balanced')),
    'l2' : make_pipeline(StandardScaler(), 
                         LogisticRegression(penalty='l2' , random_state=42, class_weight='balanced')),
    #Setting the penalty for simple Logistic Regression as L2 to minimize the fitting time
    'logreg' : make_pipeline(StandardScaler(), LogisticRegression(penalty='l2', random_state=42, class_weight='balanced'))
}
#Setting up a very large hyperparameter C for the non-penalized Logistic Regression (to cancel the regularization)
logreg_hyperparameters = {
    'logisticregression__C' : np.linspace(100000, 100001, 1),
    'logisticregression__fit_intercept' : [True, False]
}
#Setting up hyperparameters for the Logistic Regression with L1 penalty
l1_hyperparameters = {
    'logisticregression__C' : np.linspace(1e-3, 1e3, 10),
    'logisticregression__fit_intercept' : [True, False]
}
#Setting up hyperparameters for the Logistic Regression with L2 penalty
l2_hyperparameters = {
    'logisticregression__C' : np.linspace(1e-3, 1e3, 10),
    'logisticregression__fit_intercept' : [True, False]
}
#Creating the dictionary of hyperparameters
hyperparameters = {
    'logreg' : logreg_hyperparameters,
    'l1' : l1_hyperparameters,
    'l2' : l2_hyperparameters
}
#Splitting the data into train and test sets
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
#Creating an empty dictionary for fitted models
fitted_logreg_models = {}
# Looping through model pipelines, tuning each with GridSearchCV and saving it to fitted_logreg_models
for name, pipeline in pipelines.items():
    #Creating cross-validation object from pipeline and hyperparameters
    model = GridSearchCV(pipeline, hyperparameters[name], cv=10, n_jobs=-1)
    
    #Fitting the model on X_train, y_train
    model.fit(X_train, y_train)
    
    #Storing the model in fitted_logreg_models[name] 
    fitted_logreg_models[name] = model
    
    #Printing the status of the fitting
    print(name, 'has been fitted.')

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

#Creating an empty dictionary for predicted models
predicted_logreg_models = {}
#Predicting the response variables and displaying the prediction score
for name, model in fitted_logreg_models.items():
    y_pred = model.predict(X_test)
    predicted_logreg_models[name] = accuracy_score(y_test, y_pred)
print(predicted_logreg_models)

Модели дают следующие оценки:

  • L1: 0.83811276884324748
  • L2: 0.83811276884324748
  • Логрег: 0.8380158883937221

Все модели имеют высокие показатели точности как на обучающих, так и на тестовых наборах. Из 3 моделей L1-регуляризованная модель дает лучший результат по набору данных поезда, хотя оценка лишь незначительно отличается от L2-регуляризованной модели. В то же время, поскольку положительный отклик на маркетинговую кампанию (y = 1) составляет лишь ~ 11% от набора данных и делает его сильно несбалансированным, показатель точности не является хорошим показателем прогностической эффективности этих моделей. Следовательно, чтобы оценить конечную производительность модели, мы должны использовать более надежный подход (матрица неточностей, кривая ROC и т. Д.).

Давайте рассчитаем матрицу неточностей и отчет о классификации для модели логистической регрессии с наилучшим показателем точности. Поскольку оценки точности для L1- и L2-регуляризованных моделей практически одинаковы, мы будем использовать L2-модель, поскольку она требует гораздо меньше вычислений:

#Creating the confusion matrix
pd.crosstab(y_test, fitted_logreg_models['l2'].predict(X_test), rownames=['True'], colnames=['Predicted'], margins=True)

#Creating the classification report
print(classification_report(y_test, fitted_logreg_models['l2'].predict(X_test)))

Точность и запоминаемость модели для класса 0 (респонденты, которые не подписывались на депозит) довольно высоки, а для класса 1 (респонденты, которые подписались) - низка. Из всех респондентов, для которых наша лучшая модель логистической регрессии предсказывала класс 1, только 37% действительно относились к классу 1. А из всех фактических респондентов класса 1 наша модель правильно определила только 61%.

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

Давайте также посмотрим на кривую ROC:

#Obtaining the ROC score
roc_auc = roc_auc_score(y_test, fitted_logreg_models['l2'].predict(X_test))
#Obtaining false and true positives & thresholds
fpr, tpr, thresholds = roc_curve(y_test, fitted_logreg_models['l2'].predict_proba(X_test)[:,1])
#Plotting the curve
plt.plot(fpr, tpr, label='L2 Logistic Regression (area = %0.03f)' % roc_auc)
plt.plot([0, 1], [0, 1],'r--')
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
plt.xlabel('FPR')
plt.ylabel('TPR')
plt.title('ROC curve for Logistic regression')
plt.legend(loc="upper left")
plt.show()

Площадь под кривой составляет ~ 0,74, что существенно выше, чем область вероятности случайного угадывания (0,5). Учитывая результаты приведенного выше отчета о классификации, можно предположить, что наибольший вклад в площадь под кривой ROC дает правильно идентифицированный класс 0.

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

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

#Defining the model and parameters
final_logreg_model = LogisticRegression(C=111.11200000000001, penalty='l2', class_weight='balanced', random_state=42)
#Defining the parameters of the recursive feature elimination #selector. Step=1 means that the selector will remove
#one feature at a time
selector = RFECV(estimator=final_logreg_model, step=1, cv=10, scoring='roc_auc')
selector.fit(X, y)
print('The optimal number of features is {}'.format(selector.n_features_))
features = [f for f,s in zip(X.columns, selector.support_) if s]
print('The selected features are:')
print ('{}'.format(features))

Этот подход дает оптимальное количество функций, равное 3, и выбранные функции: «month_mar», «poutcome_nonexistent» и «poutcome_success». Давайте уточним нашу модель, включив только эти 3 функции, чтобы увидеть, как они меняют качество прогноза:

#Splitting the variables into predictor and target variables
X_optimized_logreg = df_cleaned1.loc[:, ['month_mar', 'poutcome_nonexistent', 'poutcome_success']]
#Creating a pipeline with the StandardScaler
pipeline = make_pipeline(StandardScaler(), 
                         final_logreg_model)
#Splitting the data into train and test sets
X_train1, X_test1, y_train1, y_test1 = train_test_split(X_optimized_logreg, y, test_size=0.3, random_state=42)
#Performing a Grid Search over 10 folds
model_optimized_logreg = GridSearchCV(pipeline, hyperparameters[name], cv=10, n_jobs=-1)
#Fitting the model
model_optimized_logreg.fit(X_train1, y_train1)
    
print('Model best score: ', model_optimized_logreg.best_score_)
print(classification_report(y_test1, model_optimized_logreg.predict(X_test1)))

Оценка точности для класса 1 подскочила с 0,37 (полная модель) до 0,6, но отзывчивость снизилась с 0,61 до 0,28. Общий балл F1 для класса 1 снизился с 0,46 до 0,38, так что эта модель даже хуже полной.

Интерпретация модели логистической регрессии L2

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

#The intercept of the final model
final_logreg_model.fit(X,y)
final_logreg_model.intercept_

Перехват - -0,02684812 и не дает существенного вклада в вероятность успеха кампании, поэтому его можно игнорировать.

#Creating a mapping of the values of the coefficients to names
coef_list = {}
for i in range(40):
    coef_list[list(X.columns)[i]] = np.asscalar(pd.DataFrame(final_logreg_model.coef_)[i].values)
#Sorting the resulting dictionary in descending order
sorted(coef_list.items(), key=lambda x: -x[1])

Код показывает следующие коэффициенты:

[('month_mar', 1.1259305549317145),
 ('euribor3m', 0.798444740301136),
 ('poutcome_success', 0.7856495132125266),
 ('cons.price.idx', 0.6371614398077847),
 ('month_dec', 0.3203258572861335),
 ('month_aug', 0.3055629907454928),
 ('job_student', 0.25298861980787574),
 ('job_no_active_income', 0.18466774418698276),
 ('month_jul', 0.14807099795724138),
 ('education_university.degree', 0.13008974786880506),
 ('day_of_week_wed', 0.08943229305551763),
 ('education_high.school', 0.07001464328469394),
 ('previous', 0.05067409145265186),
 ('education_professional.course', 0.026603686174141647),
 ('month_oct', 0.01599642509343227),
 ('marital_single', 0.011957440060841637),
 ('job_self-employed', 0.010111462370049456),
 ('poutcome_nonexistent', 0.005341666176947467),
 ('day_of_week_thu', 0.004358990665615383),
 ('loan_unknown', 0.0),
 ('marital_unknown', 0.0),
 ('housing_unknown', 0.0),
 ('pdays', -0.0005315624915834093),
 ('age', -0.0015103732047028588),
 ('loan_yes', -0.003343887922547138),
 ('nr.employed', -0.012083754512789206),
 ('cons.conf.idx', -0.018367065574214705),
 ('campaign', -0.03794255321241701),
 ('marital_married', -0.03880555935927075),
 ('housing_yes', -0.052375874205661734),
 ('day_of_week_tue', -0.05266599848078778),
 ('job_blue-collar', -0.05445222520941084),
 ('job_services', -0.10470417533235708),
 ('month_jun', -0.1250442317695784),
 ('day_of_week_mon', -0.17321132820505558),
 ('month_sep', -0.3148345237658016),
 ('contact_telephone', -0.5170542918071352),
 ('month_may', -0.6185322561115183),
 ('month_nov', -0.7304647596351376),
 ('emp.var.rate', -0.9710356051603397)]

Есть 3 характеристики, для которых коэффициенты точно равны нулю («marital_unknown», «ousing_unknown »,« заем_неизвестно »), поэтому мы проигнорируем эти характеристики в целях интерпретации, поскольку они не влияют на результат. Вот как интерпретировать приведенный выше список:

  1. Время года: март - самый эффективный месяц для кампании по депозитной подписке. Запуск кампании в марте увеличивает шансы на успех в 3 раза (e ^ 1,13). Запуск кампании в декабре, августе, июле и октябре также увеличивает шансы на успех, хотя и в значительно меньших масштабах. В то же время запуск кампании по депозитной подписке в ноябре, мае, сентябре или июне снижает шансы на успех, поскольку ноябрь и май являются худшими месяцами. Зависимость шансов на успех кампании от времени года, показанная моделью, согласуется с результатом двухвыборочного z-теста, который мы выполнили выше.
  2. День недели. Дни недели не сильно влияют на шансы успеха кампании в журнале. Самым удачным днем ​​кажется среда, что увеличивает шансы на успех примерно на 9%. В то же время звонок о депозите по понедельникам снижает шансы на успех на 15%. Зависимость шансов на успех кампании от дня недели, показанная моделью, не согласуется с результатом двухвыборочного z-теста, который мы выполнили выше, поэтому разница, показанная моделью, может не быть статистически значимый.
  3. Экономические показатели. Модель показывает, что ставка Euribor является одним из самых надежных предикторов успеха кампании по подписке на депозиты, что имеет смысл, поскольку более высокие ставки Euribor позволят банкам увеличить процентные ставки на вклады и сделать их более привлекательными для клиентов. Кроме того, индекс потребительских цен также кажется сильным предсказателем - увеличение индекса потребительских цен на один пункт увеличивает шансы на успех на 89%. В то же время снижение уровня вариативности занятости, по-видимому, оказывает существенное негативное влияние на шансы подписаться на депозит, что также имеет смысл - некоторые люди могут терять работу или беспокоиться о перспективах трудоустройства на сокращающемся рынке труда, Это означает, что им могут понадобиться деньги в их непосредственной досягаемости. Другие экономические показатели («cons.conf.idx», «кол-во занятых») не оказывают существенного влияния на шансы.
  4. Уровень образования. Как правило, уровень образования не оказывает большого влияния на шансы подписки на депозит. Среди трех уровней образования, которые были определены ранее, люди с университетским и средним образованием имеют самые высокие шансы подписаться на депозит. Зависимость шансов на успех кампании от уровня образования, показанная моделью, согласуется с результатом двухвыборочного z-теста, который мы выполнили выше.
  5. Демография и социально-экономический статус. Хотя семейное положение не является сильным предиктором готовности подписаться на депозит, одинокие люди, похоже, более склонны подписываться, в то время как брак, похоже, вызывает противоположный эффект. Возраст почти не влияет на шансы на депозитную подписку, а существующие личные и жилищные ссуды, похоже, оказывают негативное влияние на шансы подписки, что кажется разумным - если у человека есть ссуда, у этого человека, вероятно, будет меньше свободных денег и желания подписаться на депозит. Эти результаты соответствуют результатам двухвыборочного z-теста, приведенного выше, хотя личный заем не оказался статистически значимым предиктором, поэтому его влияние на результаты нашей модели может быть статистически незначимым.
  6. Уровень работы. Интересно, что люди без активного дохода и студенты, по-видимому, более всего готовы подписаться на депозит. В то же время работа в сфере обслуживания и рабочих, похоже, отрицательно сказывается на шансах на депозитную подписку. Предикторы модели, связанные с работой, соответствуют результатам двухвыборочного z-критерия.
  7. Участие в предыдущих маркетинговых кампаниях: предыдущая успешная маркетинговая кампания существенно увеличивает шансы на успех текущей кампании, которая ожидается. В то же время влияние отсутствия предыдущей маркетинговой кампании на депозитную подписку кажется очень небольшим - вопреки тому, что предлагалось с помощью исключения рекурсивной функции. Влияние количества дней с момента предыдущего контакта также кажется очень незначительным, но в этом случае причиной этого может быть переменный уровень «999», который обозначает несуществующий предыдущий контакт. Мы рассмотрим по отдельности существующих и новых (никогда ранее не контактировавших) клиентов, чтобы увидеть, отличаются ли прогнозы модели для этих подгрупп. Наконец, способ контакта, по-видимому, является сильным предиктором результата кампании - контакт по телефону существенно снижает шансы на успех кампании, поскольку, вероятно, отражает уровень доходов респондентов.

Тестирование других прогнозных моделей

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

#Setting up pipelines with a StandardScaler function to normalize the variables
pipelines = {
    'rf' : make_pipeline(StandardScaler(), 
                         RandomForestClassifier(random_state=42, class_weight='balanced')),
    'gb' : make_pipeline(StandardScaler(), 
                         GradientBoostingClassifier(random_state=42))
}
#Setting up the "rule of thumb" hyperparameters for the Random Forest
rf_hyperparameters = {
    'randomforestclassifier__n_estimators': [100, 200],
    'randomforestclassifier__max_features': ['auto', 'sqrt', 0.33]
}
#Setting up the "rule of thumb" hyperparameters for the Gradient Boost
gb_hyperparameters = {
    'gradientboostingclassifier__n_estimators': [100, 200],
    'gradientboostingclassifier__learning_rate': [0.05, 0.1, 0.2],
    'gradientboostingclassifier__max_depth': [1, 3, 5]
}
#Creating the dictionary of hyperparameters
hyperparameters = {
    'rf' : rf_hyperparameters,
    'gb' : gb_hyperparameters
}
#Creating an empty dictionary for fitted models
fitted_alternative_models = {}
# Looping through model pipelines, tuning each with GridSearchCV and saving it to fitted_logreg_models
for name, pipeline in pipelines.items():
    #Creating cross-validation object from pipeline and hyperparameters
    alt_model = GridSearchCV(pipeline, hyperparameters[name], cv=10, n_jobs=-1)
    
    #Fitting the model on X_train, y_train
    alt_model.fit(X_train, y_train)
    
    #Storing the model in fitted_logreg_models[name] 
    fitted_alternative_models[name] = alt_model
    
    #Printing the status of the fitting
    print(name, 'has been fitted.')
#Displaying the best_score_ for each fitted model
for name, model in fitted_alternative_models.items():
    print(name, model.best_score_ )

Модель случайного леса дает оценку точности 0,89755834233, а усиление градиента дает оценку 0,906361597874, что на данный момент является лучшим. Как упоминалось ранее, показатель точности не является хорошим предсказателем в этом случае, поскольку набор данных сильно несбалансирован, но он все же дает представление о производительности модели по сравнению с другими моделями. Давайте проверим эти две модели на тестовых данных:

#Creating an empty dictionary for predicted models
predicted_alternative_models = {}
#Predicting the response variables and displaying the prediction score
for name, model in fitted_alternative_models.items():
    y_pred = model.predict(X_test)
    predicted_alternative_models[name] = accuracy_score(y_test, y_pred)

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

#Creating the confusion matrix for Random Forest
pd.crosstab(y_test, fitted_alternative_models['rf'].predict(X_test), rownames=['True'], colnames=['Predicted'], margins=True)

#Creating the classification report for Random Forest
print(classification_report(y_test, fitted_alternative_models['rf'].predict(X_test)))

#Creating the confusion matrix for Gradient Boosting
pd.crosstab(y_test, fitted_alternative_models['gb'].predict(X_test), rownames=['True'], colnames=['Predicted'], margins=True)

#Creating the classification report for Gradient Boosting
print(classification_report(y_test, fitted_alternative_models['gb'].predict(X_test)))

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

Шаг 5. Устранение несбалансированных классов данных

Как упоминалось ранее, только ~ 11% респондентов кампании фактически подписались на депозит, что делает общий набор данных очень несбалансированным и что потенциально может привести к модели, предсказывающей только один класс (доминирующий). Давайте проверим, действительно ли модель предсказывает один класс:

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

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

Набор данных с повышенной дискретизацией

Давайте повысим дискретизацию данных с помощью функции resample из scikit-learn:

#Separating the imbalanced observations into 2 separate datasets
df_majority = df_cleaned1[df_cleaned1.y==0]
df_minority = df_cleaned1[df_cleaned1.y==1]
#Upsampling the minority class
df_minority_upsampled = resample(df_minority, replace=True, n_samples=30622, random_state=42)
#Concatenating two datasets
df_upsampled = pd.concat([df_majority, df_minority_upsampled])
#New class counts
df_upsampled.y.value_counts()

Повышающая дискретизация возвращает 30 622 экземпляра как класса 1, так и класса 0. Давайте подгоним модель под новый набор данных:

#Setting up the new features and the target variable
y_up = df_upsampled.y
X_up = df_upsampled.drop('y', axis=1)
#Defining the model once again
final_model = LogisticRegression(C=111.11200000000001, penalty='l2', class_weight='balanced', random_state=42)
#Defining the steps in order to include the scaler
steps = [('scaler', StandardScaler()),
          ('l2', final_model)]
#Defining the pipeline
pipeline = Pipeline(steps)
#Splitting the data into train and test sets
X_train_up, X_test_up, y_train_up, y_test_up = train_test_split(X_up, y_up, test_size=0.3, random_state=42)
#Fitting the model on X_train, y_train
scaled_model = pipeline.fit(X_train_up, y_train_up)
#Predicting the y
y_pred_up = scaled_model.predict(X_test_up)
#Calculating the accuracy score
print(accuracy_score(y_test_up, y_pred_up))

Оценка точности для этой модели - 0,737291825405. Давайте создадим матрицу путаницы и отчет о классификации:

#Creating the confusion matrix for upsampled model
pd.crosstab(y_test_up, y_pred_up, rownames=['True'], colnames=['Predicted'], margins=True)

#Creating the classification report for upsampled model
print(classification_report(y_test_up, y_pred_up))

Модель с повышенной дискретизацией дала лучший результат F1 для класса 1, а точность увеличилась до 0,83, что очень хорошо. Давайте теперь попробуем субдискретизацию.

Набор данных с пониженной дискретизацией

#Downsampling the majority class
df_majority_downsampled = resample(df_majority, replace=True, n_samples=3782, random_state=42)
#Concatenating two datasets
df_downsampled = pd.concat([df_minority, df_majority_downsampled])
#New class counts
df_downsampled.y.value_counts()

Пониженная дискретизация дает 3782 экземпляра как класса 1, так и класса 0. Давайте подберем модель:

#Setting up the new features and the target variable
y_down = df_downsampled.y
X_down = df_downsampled.drop('y', axis=1)
#Defining the model once again
final_model = LogisticRegression(C=111.11200000000001, penalty='l2', class_weight='balanced', random_state=42)
#Defining the steps in order to include the scaler
steps = [('scaler', StandardScaler()),
          ('l2', final_model)]
#Defining the pipeline
pipeline = Pipeline(steps)
#Splitting the data into train and test sets
X_train_down, X_test_down, y_train_down, y_test_down = train_test_split(X_down, y_down, test_size=0.3, random_state=42)
#Fitting the model on X_train, y_train
scaled_model = pipeline.fit(X_train_down, y_train_down)
#Predicting the y
y_pred_down = pipeline.predict(X_test_down)
#Calculating the accuracy score
print(accuracy_score(y_test_down, y_pred_down))

Оценка точности для этой модели - 0,730837004405. Давайте создадим матрицу путаницы и отчет о классификации:

#Creating the confusion matrix for downsampled model
pd.crosstab(y_test_down, y_pred_down, rownames=['True'], colnames=['Predicted'], margins=True)

#Creating the classification report for downsampled model
print(classification_report(y_test_down, y_pred_down))

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

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

Давайте посмотрим, сможем ли мы получить какое-либо представление о распределении числовых переменных для респондентов, которые были ошибочно классифицированы как 0 или 1. Мы сделаем это отдельно для обучающей и тестовой подмножеств.

#Fitting the model to training subset
final_logreg_model.fit(X_train, y_train)
#Predicting y for the training subset
y_train_pred = final_logreg_model.predict(X_train)
#Creating a mask for the respondents with wrongly predicted y=0
mispredicted_0_train_mask = (y_train == 1) & (y_train_pred == 0)
X_train_mispredicted_0 = X_train[mispredicted_0_train_mask]
#Summarizing the distribution of the variables
X_train_mispredicted_0.describe()

#Creating a mask for the respondents with wrongly predicted y=1
mispredicted_1_train_mask = (y_train == 0) & (y_train_pred == 1)
X_train_mispredicted_1 = X_train[mispredicted_1_train_mask]
#Summarizing the distribution of the variables
X_train_mispredicted_1.describe()

#Calculating the differences between the distributions for mispredicted y=1 and y=0
pd.DataFrame(X_train_mispredicted_1.describe()) - pd.DataFrame(X_train_mispredicted_0.describe())

Количество респондентов, ошибочно предсказанных моделью L2 как y = 1, почти в 2 раза больше, чем количество респондентов, ошибочно предсказанных как y = 0. То есть модель имеет тенденцию неверно предсказывать положительный ответ на маркетинговую кампанию почти в два раза чаще, чем отрицательный. Это неверное предсказание y = 1 имеет тенденцию немного смещаться в сторону старшего возраста и более существенно в сторону более низкого уровня изменчивости занятости, индексов потребительских цен и уверенности, ставки Euribor и количества занятых. Разница в средних для неверно предсказанных значений y = 1 и y = 0 для других числовых переменных несущественна.

Давайте также посмотрим на ошибочные прогнозы в тестовой подгруппе:

#Predicting the values on the test subset
X_test = X_test.drop('actual', axis=1)
y_test_pred = final_logreg_model.predict(X_test)
#Creating a mask for the respondents with wrongly predicted y=0
mispredicted_0_test_mask = (y_test == 1) & (y_test_pred == 0)
X_test_mispredicted_0 = X_test[mispredicted_0_test_mask]
#Summarizing the distribution of the variables
X_test_mispredicted_0.describe()

#Creating a mask for the respondents with wrongly predicted y=1
mispredicted_1_test_mask = (y_test == 0) & (y_test_pred == 1)
X_test_mispredicted_1 = X_test[mispredicted_1_test_mask]
#Summarizing the distribution of the variables
X_test_mispredicted_1.describe()

#Calculating the differences between the distributions for mispredicted y=1 and y=0
pd.DataFrame(X_test_mispredicted_1.describe()) - pd.DataFrame(X_test_mispredicted_0.describe())

Число респондентов, ошибочно предсказанных моделью как y = 1, опять же более чем в 2 раза выше, чем количество респондентов, ошибочно предсказанных как y = 0. Разница в средних между двумя неверно предсказанными классами очень близка к разнице в подмножестве поездов.

Шаг 7. Тестирование модели на новых и существующих клиентах.

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

Давайте разделим набор данных на новых и существующих клиентов, используя переменную pdays, и подберем модель. Для клиентов, с которыми ранее не связывались, значение переменной будет 999.

#Generating the prediction-ready dataset
new_customers_cleaned = pd.get_dummies(new_customers, drop_first=True)
#Setting up the new features and the target variable
y_new = new_customers_cleaned.y
X_new = new_customers_cleaned.drop('y', axis=1)
#Defining the model once again
final_model = LogisticRegression(C=111.11200000000001, penalty='l2', class_weight='balanced', random_state=42)
#Defining the steps in order to include the scaler
steps = [('scaler', StandardScaler()),
          ('l2', final_model)]
#Defining the pipeline
pipeline = Pipeline(steps)
#Splitting the data into train and test sets
X_train_new, X_test_new, y_train_new, y_test_new = train_test_split(X_new, y_new, test_size=0.3, random_state=42)
#Fitting the model on X_train, y_train
scaled_model = pipeline.fit(X_train_new, y_train_new)
#Predicting the y
y_pred_new = pipeline.predict(X_test_new)
#Calculating the accuracy score
print(accuracy_score(y_test_new, y_pred_new))

Оценка точности для этой модели составляет 0,799636803874. Давайте создадим матрицу путаницы и отчет о классификации:

#Creating the confusion matrix for new customers subset
pd.crosstab(y_test_new, y_pred_new, rownames=['True'], colnames=['Predicted'], margins=True)

#Creating the classification report
print(classification_report(y_test_new, y_pred_new))

Подбор модели к подмножеству новых клиентов дает очень низкий балл F1, поэтому не похоже, что он хорошо работает с этим подмножеством. Давайте посмотрим на существующее подмножество заказчиков:

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

#Generating the prediction-ready dataset
ex_customers_cleaned = pd.get_dummies(ex_customers, drop_first=True)
#Setting up the new features and the target variable
y_ex = ex_customers_cleaned.y
X_ex = ex_customers_cleaned.drop('y', axis=1)
#Defining the model once again
final_model = LogisticRegression(C=111.11200000000001, penalty='l2', class_weight='balanced', random_state=42)
#Defining the steps in order to include the scaler
steps = [('scaler', StandardScaler()),
          ('gb', final_model)]
#Defining the pipeline
pipeline = Pipeline(steps)
#Splitting the data into train and test sets
X_train_ex, X_test_ex, y_train_ex, y_test_ex = train_test_split(X_ex, y_ex, test_size=0.3, random_state=42)
#Fitting the model on X_train, y_train
scaled_model = pipeline.fit(X_train_ex, y_train_ex)
#Predicting the y
y_pred_ex = pipeline.predict(X_test_ex)
#Calculating the accuracy score
print(accuracy_score(y_test_ex, y_pred_ex))

Оценка точности для этой модели составляет 0,653658536585. Давайте создадим матрицу путаницы и отчет о классификации:

#Creating the confusion matrix for existing customers subset
pd.crosstab(y_test_ex, y_pred_ex, rownames=['True'], colnames=['Predicted'], margins=True)

#Creating the classification report
print(classification_report(y_test_ex, y_pred_ex))

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

Заключение

Модель логистической регрессии с регуляризацией L2 дала лучший результат F1 среди всех протестированных моделей, особенно в наборе данных с повышенной / пониженной дискретизацией. Для подмножества новых клиентов модель дала очень плохой F1-балл, что означает, что она не является хорошим предиктором результатов кампании для новых клиентов. Хотя подмножество существующих клиентов очень мало, высокий показатель F1 предполагает, что модель может быть хорошим предиктором успеха кампании по депозитной подписке для существующих клиентов.

Записную книжку ipynb с кодом для этого анализа можно найти здесь.