Сегодня мы рассмотрим проект классификации, а именно построение модели кредитного скоринга, предназначенной для классификации заявок на получение кредита.
Набор данных, который я буду использовать, доступен на Kaggle, его можно найти здесь: https://www.kaggle.com/c/home-credit-default-risk/data. Этот набор данных представляет собой богатую коллекцию, которая включает в себя несколько таблиц, предлагающих многочисленные возможности для разработки признаков.
Первые шаги этого проекта включают загрузку необходимых библиотек, набора данных и проведение начальной проверки.
import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns # Suppress warnings import warnings warnings.filterwarnings('ignore') # Display all columns in output pd.set_option('display.max_columns', None) # Set plot style and define some custom colours plt.style.use('ggplot') viva_magenta = '#BE3455' winery ='#7E212A' classic_blue ='#0f4c81' # Read in the datasets application_test = pd.read_csv("application_test.csv") application_train = pd.read_csv("application_train.csv") bureau = pd.read_csv("bureau.csv") print(application_train.head()) print(application_test.head())
После проверки первых пяти строк наборов данных application_train и application_test стало очевидно, что единственная разница между ними заключается в том, что application_train содержит столбец TARGET, который является двоичной переменной. Основываясь на этом наблюдении, предполагается, что столбец «ЦЕЛЬ» может быть нашей меткой класса. При дальнейшем изучении документации это подозрение подтверждается.
Далее я решил объединить наборы данных application_train и application_test, поскольку некоторые шаги по очистке данных могут быть применимы к обоим наборам данных.
# Append test and train data, and reset index df = application_train.append(application_test).reset_index(drop=True)
После долгих размышлений я решил добавить в наш набор данных информацию из предыдущих кредитных заявок. Хотя я не претендую на звание банковского эксперта (я скорее эксперт по забавным видео с котиками), я решил, что это может быть ценным дополнением к нашему анализу.
previous_application = pd.read_csv("previous_application.csv", usecols=['SK_ID_PREV', 'SK_ID_CURR', 'AMT_ANNUITY', 'AMT_APPLICATION', 'CODE_REJECT_REASON', 'SELLERPLACE_AREA']) # Count previous applications per client CNT_PREV_APPLICATION = previous_application.groupby('SK_ID_CURR').apply(len).to_frame() CNT_PREV_APPLICATION.columns=['CNT_PREV_APPLICATION'] CNT_PREV_APPLICATION.reset_index(inplace=True) previous_application = previous_application.merge(CNT_PREV_APPLICATION,on='SK_ID_CURR') # Merge previous application information with main dataframe df = df.merge(previous_application.drop_duplicates(subset='SK_ID_CURR'), on='SK_ID_CURR', suffixes=('_CURR', '_PREV'), how='left').copy()
В дополнение к предыдущим заявкам на получение кредита я также решил включить Bureau.csv. набор данных в наш анализ. Доступны дополнительные наборы данных, которые потенциально могут предоставить ценную информацию.
# Merge bureau.csv with main dataframe bureau.drop(columns=['SK_ID_BUREAU'],inplace=True) df = df.merge(bureau.drop_duplicates(subset='SK_ID_CURR'), on='SK_ID_CURR', suffixes=('_CURR', '_BUREAU'), how='left').copy()
Теперь давайте посмотрим, какой материал у нас есть.
df.shape
356255, 143
# Types of features and counts df.dtypes.value_counts()
float64 83
int64 40
объект 20
Кажется, что наш объединенный набор данных содержит в общей сложности 143 переменных, из которых 83 — с плавающей запятой, 40 — целочисленные и 20 — категориальные переменные. Нам нужно будет очистить и предварительно обработать данные, прежде чем мы сможем начать построение нашей модели кредитного скоринга.
Давайте посмотрим на наши целевые классы.
# Bar plot displaying target class counts. plt.figure(figsize=(6, 4)) ax = application_train['TARGET'].value_counts().plot( kind='bar', color=classic_blue, linewidth=0.7, edgecolor='white') pcnt = round(application_train['TARGET'].value_counts( )/len(application_train['TARGET']), 2) for i in range(len(pcnt)): plt.text(x=i, y=application_train['TARGET'].value_counts()[i]/2, s=(f"{pcnt[i]*100},%"), size=10, color='white', weight='bold', ha='center', va='center') ax.set_xlabel('Target Classes', fontsize=10) ax.set_ylabel('Count', fontsize=10) ax.set_title('Class Count in Train Set', fontsize=12) plt.show()
О, похоже, у нас есть серьезная проблема дисбаланса классов с нашей целевой переменной. Если мы не обратимся к этому должным образом, это может привести к предвзятой модели классификации, которая отдает предпочтение классу большинства.
Давайте взглянем на корреляцию между нашей целевой переменной и функциями в нашем наборе данных.
# plot the correlations of features with target target_corr = df.corr()['TARGET'].sort_values(ascending=False) target_corr.drop(index='TARGET', inplace=True) pos = target_corr.loc[target_corr.values > 0] neg = target_corr.loc[target_corr.values <= 0] fig, ax = plt.subplots(1, 1, figsize=(12, 6)) sns.barplot(x=pos.index, y=pos.values, color=winery, width=0.7, linewidth=0.7, edgecolor='white', ax=ax) sns.barplot(x=neg.index, y=neg.values, color=classic_blue, width=0.7, linewidth=0.7, edgecolor='white', ax=ax,) ax.set_xticklabels(ax.get_xticklabels(), rotation=90, ha='right', size=8) plt.suptitle('Variable Correlation with Target', fontsize=12) plt.tight_layout() plt.show()
Здесь нет сильной корреляции.
После обширного исследовательского анализа я получил хорошее представление о нашем наборе данных и обнаружил некоторые интригующие закономерности и взаимосвязи. Хотя я не буду вдаваться здесь во все детали, чтобы сосредоточиться на нашем проекте кредитного скоринга, я призываю вас самостоятельно изучить этот богатый и сложный набор данных. Это действительно захватывающе, что мы можем узнать из данных!
Ниже приведены шаги, предпринятые для очистки нашего набора данных. Я также добавил некоторые функции, которые, по моему мнению, могут быть полезны.
def cleanse_df_application(df): # Replace error value 'XNA' in gender column df['CODE_GENDER'].replace({'XNA': np.nan}, inplace=True) #Transform boolean categorical features into binery variables df['EMERGENCYSTATE_MODE'].replace({'nan':np.nan},inplace=True) df['EMPLOYEMENT_ANOMALY'] = (df['DAYS_EMPLOYED'] == 365243).astype(int) df['DAYS_EMPLOYED'].replace({365243: np.nan}, inplace=True) # Replace region ratings that are not 1/2/3 df['REGION_RATING_CLIENT_W_CITY'].mask(~df['REGION_RATING_CLIENT_W_CITY'].isin([1,2,3]),np.nan,inplace=True) df['REGION_RATING_CLIENT'].mask(~df['REGION_RATING_CLIENT'].isin([1,2,3]),np.nan,inplace=True) # add features df['DAYS_EMPLOYED_PERC'] = df['DAYS_EMPLOYED'] / df['DAYS_BIRTH'] # Percentage of employed years df['INCOME_CREDIT_PERC'] = df['AMT_INCOME_TOTAL'] / df['AMT_CREDIT'] # Debt to incom ratio df['INCOME_PER_PERSON'] = df['AMT_INCOME_TOTAL'] / df['CNT_FAM_MEMBERS'] # Income per person df['ANNUITY_INCOME_PERC'] = df['AMT_ANNUITY_CURR'] / df['AMT_INCOME_TOTAL'] df['PAYMENT_RATE'] = df['AMT_ANNUITY_CURR'] / df['AMT_CREDIT'] return df df = cleanse_df_application(df)
Теперь мы переходим к следующим важным шагам в этом проекте: предварительной обработке данных, настройке гиперпараметров и построению нашей модели классификации. Засучим рукава!
# seperate our train and test set train_set=df[~df['TARGET'].isna()] test_set = df[df['TARGET'].isna()]
Хотя 148 признаков могут показаться устрашающим объемом данных, наши шаги предварительной обработки могут на самом деле привести к тому, что это число увеличится в геометрической прогрессии. Крайне важно выбрать наиболее подходящие функции, прежде чем двигаться дальше. Для этой задачи я использую SelectKBest.
from sklearn.compose import make_column_transformer from sklearn.preprocessing import OneHotEncoder from sklearn.impute import SimpleImputer from sklearn.preprocessing import StandardScaler from sklearn.feature_selection import SelectKBest,f_classif # define our X and y for this task X_selection=train_set.drop(columns='TARGET') y_selection=train_set['TARGET'] # SelectKBest takes in numerical data only. We need to transform our data first # categorical variables are first one hot encoded col_transform = make_column_transformer((OneHotEncoder(sparse=False, handle_unknown="ignore"), X_selection.select_dtypes('object').columns,), remainder="passthrough") X_ohe = col_transform.fit_transform(X_selection) # missing values need to be imputed as well imputer=SimpleImputer(missing_values=np.nan,strategy='median',add_indicator=True).fit(X_ohe) X_imputed = imputer.transform(X_ohe) # scaling data X_scaled = StandardScaler().fit_transform(X_imputed) # apply SelectKBest selector=SelectKBest(score_func=f_classif).fit(X_scaled,y_selection) X_selectkbest=selector.transform(X_scaled) # compare results print('Number of features of original X: ', X_selection.shape[1]) print('Number of features after one-hot-encode: ', X_ohe.shape[1]) print('Number of features after imputation: ', X_imputed.shape[1]) print('Number of features after SelectKBest: ', X_selectkbest.shape[1]) Number of features of original X: 148 Number of features after one-hot-encode: 306 Number of features after imputation: 389 Number of features after SelectKBest: 10
Как видно из приведенного выше результата, после горячего кодирования и вменения количество наших признаков увеличилось до 389. Однако SelectKBest определил из них только 10 наиболее релевантных признаков.
Но что это за особенности?
# Extract feature names so we can actually select them. features=col_transform.get_feature_names_out().tolist() diff=len(imputer.indicator_.features_) for i in range(diff): features.append(i) mask = selector.get_support() k_best_features = np.array(features)[mask] best_features = [feat.split('__')[1] for feat in k_best_features] print(best_features)
"NAME_INCOME_TYPE_Working", "DAYS_BIRTH", "DAYS_EMPLOYED", "REGION_RATING_CLIENT", "REGION_RATING_CLIENT_W_CITY", "EXT_SOURCE_1", "EXT_SOURCE_2", "EXT_SOURCE_3", "DAYS_CREDIT", "DAYS_EMPLOYED_PERC"
Теперь у нас есть наши лучшие черты. Прежде чем двигаться дальше, давайте просто рассмотрим корреляции между выбранными нами функциями.
# A heatmap to visualize correlations between selected features plt.figure(figsize=(10,8)) ax = sns.heatmap(pd.DataFrame(X_selectkbest,columns=best_features).corr(), annot=True, xticklabels=True) ax.xaxis.tick_top() ax.set_xticklabels(ax.get_xticklabels(), rotation=90) plt.title('Correlation Between SelectKBest Features', fontsize=12) plt.show()
Существует два набора тесно связанных функций: DAYS_EMPLOYED_PERC/DAYS_EMPLOYED и REGION_RATING_CLIENT/REGION_RATING_CLIENT_W_CITY.
После дальнейшего исключения ниже приведены наши окончательные выбранные функции.
# Define a list of features selected. features_selected = [ 'NAME_INCOME_TYPE', 'DAYS_BIRTH', 'DAYS_EMPLOYED_PERC', 'REGION_RATING_CLIENT', 'DAYS_CREDIT', 'EXT_SOURCE_1', 'EXT_SOURCE_2', 'EXT_SOURCE_3' ] # Define X and y X=train_set[features_selected] y=train_set['TARGET']
Чтобы решить проблему дисбаланса классов, которую мы видели ранее, я буду использовать SMOTE для передискретизации класса меньшинства. Такой подход помогает избежать потери информации, которая может произойти при недостаточной выборке. Для этой задачи хорошо зарекомендовала себя библиотека Imbalanced Learn.
Одним из важных факторов, который следует учитывать, прежде чем приступать к обучению наших моделей, является стоимость, связанная с ложными срабатываниями (FP) и ложными отрицательными результатами (FN). В этом случае, предполагая, что FN в 10 раз дороже, чем FP, нам необходимо соответствующим образом скорректировать нашу оценку. Чтобы решить эту проблему, я создал собственную оценку под названием «error_cost_score», используя функцию make_scorer из библиотеки sklearn.
from sklearn.metrics import confusion_matrix, make_scorer def error_cost(y, y_pred): cm = confusion_matrix(y, y_pred) TN, FP, FN, TP = cm.flatten() cost = FP + 10 * FN return cost error_cost_score=make_scorer(error_cost, greater_is_better=False)
Чтобы сделать предварительную обработку более эффективной, я использую конвейер. Для настройки гиперпараметров я использую GridSearchCV и создал функцию поиска по сетке, чтобы упростить тестирование нескольких моделей. Кроме того, для отслеживания производительности модели я включил MLflow.
from sklearn.impute import SimpleImputer from sklearn.model_selection import GridSearchCV,StratifiedKFold from imblearn.over_sampling import SMOTE from imblearn.pipeline import Pipeline # A function to preprocess and grid search estimater with pipeline def classifier(model, params): # A pipeline aith preprocessing included (one hot encode, impute, SMOTE & scaler) pipe = Pipeline(steps=[ ['one hot encode', make_column_transformer( (OneHotEncoder(sparse=False, handle_unknown="ignore"), ['NAME_INCOME_TYPE'],), remainder="passthrough") ], ['impute', SimpleImputer(missing_values=np.nan, strategy='median', add_indicator=True)], ['smote', SMOTE(random_state=42)], ['scaler', StandardScaler()], ['classifier', model] ] ) stratified_kfold = StratifiedKFold( n_splits=5, shuffle=True, random_state=42) scores = {'auc': "roc_auc_ovr_weighted", 'cost_score': error_cost_score} clf = GridSearchCV(estimator=pipe, param_grid=params, scoring=scores, refit='cost_score', cv=stratified_kfold, n_jobs=2) return clf
Модели-кандидаты, которые я выбираю, — это логистическая регрессия, случайный лес и lightGBM.
Логистическая регрессия
# activate mlflow auto tracking import mlflow mlflow.set_tracking_uri("http://127.0.0.1:5000/") experiment_id = mlflow.create_experiment("credit_scoring") experiment = mlflow.get_experiment(experiment_id) mlflow.sklearn.autolog(log_models=True) # logistic regression with mlflow.start_run(experiment_id=experiment.experiment_id, run_name='Weighted Logistic Regression') as run: from sklearn.linear_model import LogisticRegression param_logistic_regression = {'classifier__penalty': ['l2', 'elasticnet'], 'classifier__C': [1, 10], 'classifier__solver': ['lbfgs', 'newton-cholesky'] } clf_logistic = classifier(model=LogisticRegression(class_weight='balanced', random_state=42, max_iter=200), params=param_logistic_regression) clf_logistic.fit(X_train, y_train)
Случайный лес.
with mlflow.start_run(experiment_id=experiment.experiment_id, run_name='Random Forest') as run: from sklearn.ensemble import RandomForestClassifier param_forest = { 'classifier__n_estimators': [50, 100,150], 'classifier__max_depth': [10, 15], 'classifier__min_samples_leaf': [100, 200, 500], 'classifier__max_samples': [0.5, 0.8] } clf_forest = classifier(model=RandomForestClassifier(class_weight='balanced', random_state=42), params=param_forest) clf_forest.fit(X_train, y_train)
СветGBM
with mlflow.start_run(experiment_id=experiment.experiment_id, run_name='LightGBM') as run: from lightgbm import LGBMClassifier param_lgbm = {'classifier__max_depth': [10,30, 60], 'classifier__num_leaves': [20, 50], 'classifier__learning_rate': [0.001, 0.01, 0.1], } clf_lgbm = classifier(model=LGBMClassifier(objective='binary', class_weight='balanced', random_state=42), params=param_lgbm) clf_lgbm.fit(X_train, y_train)
Вуаля!
Теперь давайте сравним результаты.
model_dict = { 'Logistic Regression': clf_logistic, 'Random Forest': clf_forest, 'LightGBM': clf_lgbm } result = {'Model': [], 'Error cost score': [], 'AUC score (mean_test_score)': [], 'Mean fit time': [], 'Best params': []} for model_name, model in model_dict.items(): result['Model'].append(model_name) result['Error cost score'].append(round(model.best_score_, 4)) result['AUC score (mean_test_score)'].append( round(max(model.cv_results_['mean_test_auc']), 4)) result['Mean fit time'].append( np.around(np.average(model.cv_results_['mean_fit_time']), 2)) result['Best params'].append(model.best_params_) pd.DataFrame(result).sort_values(by='Error cost score', ascending=False)
Таблица результатов:
Одна модель, кажется, работает особенно хорошо с точки зрения стоимости ошибки: логистическая регрессия. Тем не менее, мы должны более внимательно посмотреть на результаты. Использование матрицы путаницы может обеспечить четкий и прямой обзор производительности моделей. О, пока я этим занимаюсь, я бы хотел также засечь время предсказания.
import time y_preds = [] predict_times = [] for model in list(model_dict.values()): start_time = time.time() preds = model.predict(X_test) end_time = time.time() y_preds.append(preds) predict_times.append(round(end_time - start_time,2)) print(predict_times) [0.07, 1.09, 0.14]
Из трех логистическая регрессия имеет самое быстрое время прогнозирования, а случайный лес — самое медленное.
К матрице путаницы.
# a function to plot confusion matrix heatmap def confusion_matrix_heatmap(y,y_pred,ax): cm = confusion_matrix(y, y_pred) TN, FP, FN, TP = cm.flatten() strings = np.asarray(['TN', 'FP', 'FN', 'TP']) labels = np.asarray(["{0}\n{1}".format(string, value) for string, value in zip(strings.flatten(), cm.flatten())]) annot = labels.reshape(cm.shape[0], cm.shape[1]) sns.heatmap(cm, annot=annot, fmt='', cmap="Blues", ax=ax) ax.set_xlabel('Predicted', fontsize=10) ax.set_ylabel('Actual', fontsize=10) return ax # plot three confusion matrices for our three models fig, axs = plt.subplots(1, 3, figsize=(12, 4)) for i, ax in enumerate(axs.flatten()): confusion_matrix_heatmap(y_test, y_preds[i], ax) ax.set_title(f"{list(model_dict.keys())[i]}, predict time: {predict_times[i]}", fontsize=10) plt.suptitle('Confusion Matrix', fontsize=12) plt.tight_layout() plt.show()
Глядя на диагональные квадраты матриц путаницы, мы видим, что логистическая регрессия имеет наибольшее количество ложных срабатываний (хорошие приложения прогнозируются как плохие), но наименьшее количество ложных отрицательных результатов (плохие приложения прогнозируются как хорошие). С другой стороны, две другие модели были менее «консервативными» и давали больше ложноотрицательных результатов.
Чтобы выразить это в денежном выражении, давайте предположим, что истинный отрицательный результат приносит 1 доллар, ложный положительный результат стоит 1 доллар, ложный отрицательный результат стоит 10 долларов, а истинный положительный результат приносит 10 долларов. Основываясь на этих предположениях, мы можем создать матрицу затрат, которая поможет нам оценить модели.
# a function to draw a cost matrix def cost_matrix_heatmap(y, y_pred, ax): cm = confusion_matrix(y, y_pred) TN, FP, FN, TP = cm.flatten() costs = [TN, -FP, -10*FN, 10*TP] cost_total = FP+10*FN strings = np.asarray(['TN', 'FP', 'FN', 'TP']) labels = np.asarray(["{0}\n{1}".format(string, value) for string, value in zip(strings.flatten(), costs)]) annot = labels.reshape(cm.shape[0], cm.shape[1]) sns.heatmap(cm, annot=annot, fmt='', cmap="Blues", ax=ax) ax.set_xlabel('Predicted', fontsize=10) ax.set_ylabel('Actual', fontsize=10) return ax, cost_total # plot three cost matrices for three models fig, axs = plt.subplots(1, 3, figsize=(12, 4)) costs = [] for i, ax in enumerate(axs.flatten()): fig, cost = cost_matrix_heatmap(y_test, y_preds[i], ax) ax.set_title(f"{list(model_dict.keys())[i]}, Total Loss:{cost}", fontsize=10) costs.append(cost) plt.suptitle('Cost Matrix', fontsize=12) plt.tight_layout() plt.show()
Из приведенного выше графика совершенно ясно, что логистическая регрессия имеет самую низкую стоимость ошибки из трех моделей. Теперь меня интересует общий доход от каждой модели.
def get_total_revenue(model): y_pred = model.predict(X_test) cm = confusion_matrix(y_test, y_pred) TN, FP, FN, TP = cm.flatten() total_revenue = TN - FP - 10 * FN + 10 * TP return total_revenue revenues = [get_total_revenue(model) for model in model_dict.values()] sorted_models = [model for _, model in sorted(zip(revenues, model_dict.values()), reverse=True)] plt.figure(figsize=(7,5)) sns.barplot(x=list(model_dict.keys()), y=revenues, edgecolor='white', linewidth=0.8, order=[list(model_dict.keys())[list(model_dict.values()).index(model)] for model in sorted_models]) plt.xticks(rotation=45, ha='right') plt.title('Total Revenue by Model',fontsize=12) plt.show()
Логистическая регрессия кажется явным победителем, постоянно превосходящим другие модели по всем таблицам и показателям.
Что ж, давайте сохраним нашу выигрышную модель и перейдем к построению информационных панелей и развертыванию в облаке.
model=clf_logistic.best_estimator_ import pickle # save model for creating our dashboard app with open("model.pkl", "wb") as f: pickle.dump(model, f)
Чтобы создать удобную и понятную панель инструментов, я стремлюсь сделать ее простой и наглядной. Я планирую включить раскрывающийся список для выбора идентификатора клиента, который будет отображать соответствующий результат прогнозирования вместе с ключевыми решающими функциями.
Но чтобы это сработало, мне нужно сначала развернуть API моей модели. Для развертывания API моей модели я выбрал фреймворк FastAPI и приложение Dash и буду использовать Heroku в качестве платформы для хостинга.
Ядро моего кода FastAPI включает в себя загрузку предварительно обученной модели и создание прогнозов на основе входных функций с помощью функции «give_score». Кроме того, я внедрил пользовательский интерфейс Swagger для тестирования модели и управления ею без использования панели инструментов.
import pandas as pd import pickle from fastapi import FastAPI from fastapi.openapi.utils import get_openapi from client_features import ClientFeatures with open("trained_pipeline.pkl", "rb") as f: model = pickle.load(f) app = FastAPI() def custom_openapi(): if app.openapi_schema: return app.openapi_schema openapi_schema = get_openapi( title="Credit Default Prediction ", version="0.0.1", description="Classification with Logistic Regression. To try it out, click 'Try it out' under '/score' ", routes=app.routes, ) app.openapi_schema = openapi_schema return app.openapi_schema app.openapi = custom_openapi @app.get("/") async def root(): return {"message": "Hello World"} @app.post('/score') async def give_score(data:ClientFeatures): data_dict = data.dict() df = pd.DataFrame([data_dict]) score = model.predict_proba(df)[0][1] score = round(score, 2) if score < 0.4933: status = 'Accepted' else: status = 'Rejected' return {"risk_score": score, 'application_status': status}
Возможно, вы заметили в коде класс ClientFeatures. Это класс типа данных Pydantic, который помогает обеспечить правильное форматирование входных данных перед их передачей в модель. Он существует в отдельном файле в той же папке API.
from pydantic import BaseModel class ClientFeatures(BaseModel): NAME_INCOME_TYPE: str DAYS_CREDIT: float DAYS_BIRTH: int DAYS_EMPLOYED_PERC: float REGION_RATING_CLIENT: int EXT_SOURCE_1: float EXT_SOURCE_2: float EXT_SOURCE_3:
Чтобы настроить и запустить API на Heroku, вам понадобится несколько вещей: Procfile, runtime.txt, requirements.txt и, конечно же, ваша надежная обученная модель. Эти файлы позволяют Heroku узнать, в какой среде должен работать ваш API и какие библиотеки ему нужны, чтобы творить чудеса. Совет: используйте команду pip freeze, чтобы убедиться, что у вас есть точные версии всех ваших зависимостей, а затем укажите эти версии в файле requirements.txt. Для получения более подробной информации посетите мой милый репозиторий GitHub: https://github.com/jzpatrao/credit_classification.
Как только это будет сделано, вы можете продолжить создание приложения панели мониторинга. Я использую Даш.
from dash import Dash, dcc, html, Input, Output import dash_bootstrap_components as dbc import pandas as pd import numpy as np from pathlib import Path import plotly.graph_objs as go import requests # Get the base directory of the current file BASE_DIR = Path(__file__).resolve(strict=True).parent # Instantiate the dash app app = Dash(external_stylesheets=[dbc.themes.LUX]) server = app.server app.title = "Credit Scoring" # Load dataset and saved shap values from pickle df = pd.read_csv("application_test.csv") # Define options for a dropdown box that select a client id. client_options = [{'label': str(x), 'value': x} for x in df['SK_ID_CURR'].unique()] # Drop down box for client selection select_client = html.Div([ dcc.Dropdown( id="client_id", options=client_options, multi=False, value=100001 ) ]) # Define client profile in detail client_profile = html.Div([ dbc.Row([ html.Span("Income type: ", style={"font-weight": "bold"}), html.Div(title='Income type', id='Income type', children=[]) ]), dbc.Row([ html.Span("Days credit: ", style={"font-weight": "bold"}), html.Div(title='Days credit', id='Days credit', children=[]) ]), dbc.Row([ html.Span("Region rating: ", style={"font-weight": "bold"}), html.Div(title='Region rating client', id='Region rating client', children=[]) ]), dbc.Row([ html.Span("Age: ", style={"font-weight": "bold"}), html.Div(title='Days birth', id='Days birth', children=[]) ]), dbc.Row([ html.Span("Employment length: ", style={"font-weight": "bold"}), html.Div(title='Days employed percent', id='Days employed percent', children=[]) ]), dbc.Row([ html.Span("Ext source 1: ", style={"font-weight": "bold"}), html.Div(title='Ext source 1', id='Ext source 1', children=[]) ]), dbc.Row([ html.Span("Ext source 2: ", style={"font-weight": "bold"}), html.Div(title='Ext source 2', id='Ext source 2', children=[]) ]), dbc.Row([ html.Span("Ext source: ", style={"font-weight": "bold"}), html.Div(title='Ext source 3', id='Ext source 3', children=[]) ]) ]) # Define dashboard layout app.layout = dbc.Container([ html.H1("Credit Scoring", className='text-center mb-2'), # Page title dbc.Row([ dbc.Col(dbc.Card([ dbc.CardHeader("Select Client ID", style={"background-color": "#1A85FF", "color": "white", "font-weight": "bold"}), select_client, dbc.CardBody(dcc.Graph(id="gauge-chart")) ], className='mb-4', style={"height": "100%"}), width=5), # Card component with a dropdown box and a gauge chart dbc.Col(dbc.Card([ dbc.CardHeader("Client Profile", style={"background-color": "#1A85FF", "color": "white", "font-weight": "bold"}), client_profile ], className='mb-4', style={"height": "100%"}), width=5) ], className='text-center', align='stretch', justify='center'), # A card components displaying selected client features ], fluid=True,) # Define inputs in above layout with callbacks. @app.callback( [ Output(component_id='Income type', component_property='children'), Output(component_id='Days credit', component_property='children'), Output(component_id='Region rating client', component_property='children'), Output(component_id='Days birth', component_property='children'), Output(component_id='Days employed percent', component_property='children'), Output(component_id='Ext source 1', component_property='children'), Output(component_id='Ext source 2', component_property='children'), Output(component_id='Ext source 3', component_property='children') ], [Input(component_id='client_id', component_property='value')] ) def get_client_features(client_id): data = df[df['SK_ID_CURR'] == client_id] Output1 = html.Div(data['NAME_INCOME_TYPE']) Output2 = html.Div(data['DAYS_CREDIT']) Output3 = html.Div(data['REGION_RATING_CLIENT']) Output4 = html.Div(int(data['DAYS_BIRTH']/365)) Output5 = html.Div(round(data['DAYS_EMPLOYED_PERC'],2)) Output6 = html.Div(round(data['EXT_SOURCE_1'],2)) Output7 = html.Div(round(data['EXT_SOURCE_2'],2)) Output8 = html.Div(round(data['EXT_SOURCE_3'],2)) return Output1, Output2, Output3, Output4, Output5, Output6, Output7, Output8 @app.callback(Output("gauge-chart", "figure"), Input(component_id='client_id', component_property='value') ) # Define values for above callback outputs def get_model_prediction(client_id): data = df[df['SK_ID_CURR'] == client_id] url = "https://oc-project-7-fastapi.herokuapp.com/score" payload = { "NAME_INCOME_TYPE": data['NAME_INCOME_TYPE'].item(), "DAYS_CREDIT": data['DAYS_CREDIT'].item(), "DAYS_BIRTH": data['DAYS_BIRTH'].item(), "DAYS_EMPLOYED_PERC": data['DAYS_EMPLOYED_PERC'].item(), "REGION_RATING_CLIENT": data['REGION_RATING_CLIENT'].item(), "EXT_SOURCE_1": data['EXT_SOURCE_1'].item(), "EXT_SOURCE_2": data['EXT_SOURCE_2'].item(), "EXT_SOURCE_3": data['EXT_SOURCE_3'].item() } response = requests.post(url, json=payload) data_from_api = response.json() risk_score = data_from_api["risk_score"] status = data_from_api["application_status"] fig_gauge = go.Figure(go.Indicator(domain={'x': [0, 1], 'y': [0, 1]}, value=np.around(risk_score, 2), mode="gauge+number", title={'text': f"{status}", 'font': {'size': 24}}, gauge={'axis': {'range': [0, 1], 'tickwidth': 2, 'tickcolor': "grey"}, 'bar': {'color': "black"},'bordercolor': "black", 'steps': [{'range': [0, 0.4933], 'color': "#1A85FF"}, #using colorblind friendly colors {'range': [0.4933, 1], 'color': "#D41159"}], 'threshold': {'line': {'color': "black", 'width': 1}, 'thickness': 1, 'value': 0.4933}})) return fig_gauge if __name__ == '__main__': app.run_server(debug=True)
При создании приложения Dash вы не должны напортачить с обратными вызовами. Это функции, которые соединяют ввод и вывод вашего приложения, и они должны правильно соответствовать вашему макету. Таким образом, при написании ваших обратных вызовов и любых связанных с ними функций важно правильно продумать детали. Я очень рекомендую очаровательный канал данных на Youtube.
Еще одна вещь, на которую следует обратить внимание, — это URL-адрес, который приложение вызывает для получения прогноза. Это будет API, который мы развернули ранее. Это достигается в этих строках кода выше:
# the url of our deployed model api url = "https://oc-project-7-fastapi.herokuapp.com/score" # filtered data (client featres) to send to api payload = { "NAME_INCOME_TYPE": data['NAME_INCOME_TYPE'].item(), "DAYS_CREDIT": data['DAYS_CREDIT'].item(), "DAYS_BIRTH": data['DAYS_BIRTH'].item(), "DAYS_EMPLOYED_PERC": data['DAYS_EMPLOYED_PERC'].item(), "REGION_RATING_CLIENT": data['REGION_RATING_CLIENT'].item(), "EXT_SOURCE_1": data['EXT_SOURCE_1'].item(), "EXT_SOURCE_2": data['EXT_SOURCE_2'].item(), "EXT_SOURCE_3": data['EXT_SOURCE_3'].item() } # get the response from api (give_score function) response = requests.post(url, json=payload) # format the response in a json dictionary data_from_api = response.json() risk_score = data_from_api["risk_score"] status = data_from_api["application_status"]
Остальная часть приложения похожа на тот, что был в моем API ранее, в том смысле, что ему нужны Procfile, runtime.txt и requirement.txt для работа. Не забудьте также добавить сюда test_set.csv.
После развертывания в Heroku панель инструментов теперь доступна и доступна через веб-браузер. Вот краткий обзор того, как это выглядит:
Видеть? Простой и понятный.
Вы можете поиграть с ним здесь: https://credit-scoring-dashboard-qq.herokuapp.com/
На этом путешествие по этому проекту заканчивается. Всегда есть возможности для дальнейшего совершенствования, и в будущем я могу вернуться к этому проекту, чтобы изучить больше функций разработки и настройки гиперпараметров. Таким образом, этот проект демонстрирует, как разработать и развернуть модель машинного обучения с оптимизированным конвейером и панелью мониторинга для практического варианта использования. В любом случае, спасибо, что присоединились ко мне в этом путешествии и удачной обработки данных!