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

#### reading the data set ####
data = pd.read_csv('case_study_data.csv')
data.head()

#### checking null values in any of the columns ####
print(mi.bar(data.iloc[:,0:11], figsize = (15,5)))

print(mi.bar(data.iloc[:,11:], figsize = (15,5)))

Один из столбцов (asset_class_cd) содержит только 20 % ненулевых значений. Поскольку это число намного ниже эмпирического правила 40%, мы не будем пытаться вменить значения. Если бы число было ближе к 40%, мы бы создали прогнозную модель, чтобы сначала предсказать значения assets_class_cd, а затем использовать прогнозируемые значения для запуска всей модели. query_purpose_code имеет около 3% нулевых значений. Кроме того, в assets_code есть нулевые значения. Мы проверим, что они распределены полностью случайным образом, и если да, мы можем удалить их напрямую, так как это не приведет к уменьшению большого количества точек данных.

### removing the column ###
mod_data = data.drop(["asset_class_cd"], axis = 1)
mod_data.head(10)

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

mi.matrix(mod_data)

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

mod_data = mod_data[~pd.isna(mod_data['inquiry_purpose_code'])]
mod_data = mod_data[~pd.isna(mod_data['asset_code'])]

Исследовательский анализ данных

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

mod_data['zipcode'] = list(("").join(list(i)[-5:]) for i in list(mod_data['address']))

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

## if the domain name is not one of gmail, yahoo or hotmail, it's marked as others ##
mod_data['domain'] = list(re.search(r"(.*)@(.*)\.", i).group(2) for i in list(mod_data['email']))
mod_data['domain'] = list(i if i in ["gmail", "yahoo", "hotmail"] else "other" for i in list(mod_data['domain']))

У нас есть дата рождения. По дате рождения мы можем рассчитать текущий возраст пользователя.

### converting the dob column in required column ###
mod_data['date_of_birth'] = list(pd.to_datetime(i, format = "%d/%m/%y") for i in list(mod_data['date_of_birth']))
current_date = date(2022, 1, 25)
mod_data['age'] = list(((current_date - datetime.date(i)).days)/365 for i in list(mod_data['date_of_birth']))
### in some case, years like 1956 are being changed to 2056 ###
### taking care of that issue ###
mod_data['age'] = list(i if i >= 0 else i+100 for i in list(mod_data['age']))

Мы рассмотрим краткое изложение набора данных

## description of the data frame ##
mod_data.describe()

sns.pairplot(mod_data)

Проверка того, все ли user_id уникальны

uni = mod_data.drop_duplicates(['user_id'], keep = "first")
len(list(uni.index)) == len(list(mod_data.index))

Проверка корреляционной матрицы

mod_data.corr()

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

mod_data.groupby(['approved'])['approved'].count()

sns.histplot(x = "age", hue = "approved", data = mod_data)

Из приведенного выше графика видно, что пользователи старшего возраста чаще получают одобрение.

ax = sns.factorplot(x='education_level', y='age', hue='approved', data=mod_data, kind='bar')
ax.set_xticklabels(rotation=65, horizontalalignment='right')

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

ax = sns.factorplot(x='hours_per_week', y='age', hue='approved', data=mod_data, kind='point')
ax.set_xticklabels(label = None, rotation=65, horizontalalignment='right')
ax.set(xticklabels=[])

ax = sns.factorplot(x='marital_status', y='age', hue='approved', data=mod_data, kind='point')
ax.set_xticklabels(label = None, rotation=65, horizontalalignment='right')

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

ax = sns.factorplot(x='capital_gain', y='capital_loss', hue='approved', data=mod_data, kind='point')
ax.set_xticklabels(label = None, rotation=65, horizontalalignment='right')
ax.set(xticklabels=[])

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

sns.boxplot(x = 'age', data = mod_data)

sns.boxplot(x = 'capital_gain', data = mod_data)

sns.boxplot(x = 'capital_loss', data = mod_data)

sns.boxplot(x = 'hours_per_week', data = mod_data)

sns.histplot(x = 'domain', data = mod_data)

sns.histplot(x = 'education_num', data = mod_data, hue = "approved")

ax = sns.histplot(x = 'education_level', data = mod_data, hue = "approved")
ax.set_xticklabels(mod_data['education_level'].unique(),rotation = 90)

ax = sns.factorplot(y='capital_gain', x='education_level', hue='approved', data=mod_data, kind='point')
ax.set_xticklabels(label = None, rotation=65, horizontalalignment='right')

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

Масштабирование функций

mod_original = mod_data.copy(deep = True)
dupl_mod_data = mod_data.copy(deep=True)
mod_data['age'] = (mod_data['age']-mod_data['age'].mean())/mod_data['age'].std()
mod_data['capital_gain'] = (mod_data['capital_gain']-mod_data['capital_gain'].mean())/mod_data['capital_gain'].std()
mod_data['capital_loss'] = (mod_data['capital_loss']-mod_data['capital_loss'].mean())/mod_data['capital_loss'].std()
mod_data['hours_per_week'] = (mod_data['hours_per_week']-mod_data['hours_per_week'].mean())/mod_data['hours_per_week'].std()
### changing Gender to 1 and 0 ###
mod_data['gender'] = list(1 if i == "Male" else 0 for i in list(mod_data['gender']))

Важность функции

Многие модели машинного обучения трудно интерпретировать, и трудно оценить важность каждого параметра в модели. Чтобы значительно упростить процесс интерпретации, мы воспользуемся библиотекой теории игр под названием Shapley Value Explanations. SHAP используется для присвоения предельного числа важности каждому параметру в зависимости от их важности. Я использую ту же модель в своем исследовании, чтобы выделить важные функции.

# creating dummy variables for all the string variables so that they can be used in evaluating SHAP values ##
workclass = pd.get_dummies(mod_data['workclass'], drop_first = True)
education_level = pd.get_dummies(mod_data['education_level'], drop_first = True)
marital_status = pd.get_dummies(mod_data['marital_status'], drop_first = True)
occupation = pd.get_dummies(mod_data['occupation'], drop_first = True)
relationship = pd.get_dummies(mod_data['relationship'], drop_first = True)
inquiry_purpose_code = pd.get_dummies(mod_data['inquiry_purpose_code'], drop_first = True)
institute_type = pd.get_dummies(mod_data['institute_type'], drop_first = True)
account_type = pd.get_dummies(mod_data['account_type'], drop_first = True)
asset_code = pd.get_dummies(mod_data['asset_code'], drop_first = True)
portfolio_type = pd.get_dummies(mod_data['portfolio_type'], drop_first = True)
domain = pd.get_dummies(mod_data['domain'], drop_first = True)
mod_data.drop(['workclass', 'education_level', 'marital_status', 'occupation', 'relationship','inquiry_purpose_code', 'institute_type', 'account_type', 'asset_code', 'portfolio_type', 'domain'], axis = 1, inplace = True)
mod_data = pd.concat([mod_data, workclass, education_level, occupation,relationship, inquiry_purpose_code, institute_type,
                account_type, asset_code, portfolio_type, domain], axis = 1)
part_data = mod_data
X = mod_data.loc[:, mod_data.columns !=  'approved']
X = X.loc[:, X.columns !=  'user_id']
X = X.loc[:, X.columns !=  'email']
X = X.loc[:, X.columns !=  'zipcode']
X = X.loc[:, X.columns !=  'date_of_birth']
X = X.loc[:, X.columns !=  'address']
Y = mod_data['approved']
X_train, X_test, y_train, y_test = train_test_split(X, Y, test_size=0.20)
masker = shap.maskers.Independent(data = X_test)
model = LogisticRegression(random_state=1, max_iter=1000).fit(X_train, y_train.values.ravel())
explainer = shap.LinearExplainer(model, masker=masker)
shap_values = explainer(X_test)
shap.plots.beeswarm(shap_values)

Из приведенной выше диаграммы видно, что отношения, номер_образования, семейное_статус, возраст, прирост капитала играют наиболее важную роль в принятии решения о том, какие функции включить. Мы пойдем оттуда и добавим переменные в виде пошаговой регрессии вперед, т. Е. Как только переменная будет добавлена ​​​​в модель, она не будет удалена.

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

formula = 'approved ~ age + education_num + capital_gain + C(inquiry_purpose_code) + C(relationship)  + C(occupation) + C(institute_type) + C(marital_status)'
fit = smf.mnlogit(formula=formula, data=dupl_mod_data).fit()
print(fit.summary())
print("AIC =", str(fit.aic))

AIC = 25571.174263297573

Теперь, когда у нас есть модель линейной регрессии, мы проверим наличие мультиколинеарности.

## calculating vif for quantitative variables ##
y, X = dmatrices('approved ~ age + education_num + capital_gain + capital_loss', data=dupl_mod_data, return_type='dataframe')
#calculate VIF for each explanatory variable
vif = pd.DataFrame()
vif['VIF'] = [variance_inflation_factor(X.values, i) for i in range(X.shape[1])]
vif['variable'] = X.columns
vif

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

Бизнес-проблема

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

## dividing the data
z_columns = ['approved','age', 'education_num','capital_gain', 'relationship', 'occupation', 'institute_type', 'inquiry_purpose_code', 'marital_status']
part_data = dupl_mod_data[z_columns]
X = part_data.loc[:,part_data.columns != 'approved']
Y = part_data['approved']
X_train, X_test, y_train, y_test = train_test_split(X, Y, test_size=0.20)
X_train, X_val, y_train, y_val = train_test_split(X_train, y_train, test_size = 0.2)
XY_train = pd.concat([X_train, y_train], axis=1, join='inner')
formula = 'approved ~ age + education_num + capital_gain + C(inquiry_purpose_code) + C(relationship)  + C(occupation) + C(institute_type) + C(marital_status)'
fit = smf.mnlogit(formula=formula, data=XY_train).fit()
fit.summary()
### using the validation set to find the optimized threshold value ####
true_negative = []
true_positive = []
threshold = []
expected = list(y_val)
predict = list(fit.predict(X_val)[1])
for i in list(np.linspace(0, 1, 100)):
    predicted = list(1 if j > i else 0 for j in predict)
    
    
    cm = confusion_matrix(expected, predicted)
TP = cm[0][0]
    FN = cm[0][1]
    FP = cm[1][0]
    TN = cm[1][1]
    #print(TP, FN, FP, TN)
    
    # Specificity or true negative rate
    TNR = TN/(TN+FP)
    TPR = TP/(TP+FN)
    #print(TPR, TNR)
    
    true_negative.append(TNR)
    true_positive.append(TPR)
    
    threshold.append(i)

При пороговом значении 0,2 мы видим, что получаем наилучшую производительность для TNR, и, следовательно, мы будем использовать это значение в нашем тестовом наборе.

expected = list(y_test)
predict = list(fit.predict(X_test)[1])
predicted = list(1 if j > 0.2 else 0 for j in predict)
    
cm = confusion_matrix(expected, predicted)
TP = cm[0][0]
FN = cm[0][1]
FP = cm[1][0]
TN = cm[1][1]
print(TP, FN, FP, TN)
    
    # Specificity or true negative rate
TNR = TN/(TN+FP)
TPR = TP/(TP+FN)
    #print(TPR, TNR)
    
print(TNR)
print(TPR)

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

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

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

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

Подпишитесь на меня в LinkedIn по адресу:



Другие мои статьи ищите по адресу: