File size: 9,666 Bytes
7f0977b
 
 
 
 
 
 
 
 
 
 
 
7592386
7f0977b
7592386
7f0977b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
import streamlit as st

from sklearn.metrics import classification_report, roc_curve

import numpy as np

import plotly.express as px

import pandas as pd

from numpy import argmax

from  visualization.metrics import streamlit_2columns_metrics_df, streamlit_2columns_metrics_pct_df

from  visualization.graphs_threshold import acceptance_rate_driven_threshold_graph


def model_probability_values_df(model, X):
    return pd.DataFrame(model.predict_proba(X)[:, 1], columns=["PROB_DEFAULT"])


def find_best_threshold_J_statistic(y, clf_prediction_prob_df):
    fpr, tpr, thresholds = roc_curve(y, clf_prediction_prob_df)
    # get the best threshold
    # Youden’s J statistic tpr-fpr
    # Argmax to get the index in
    # thresholds
    return thresholds[argmax(tpr - fpr)]

# Function that makes dataframe with probability of default, predicted default status based on threshold
# and actual default status


def classification_report_per_threshold(
    threshold_list, threshold_default_status_list, y_test
):
    target_names = ["Non-Default", "Default"]
    classification_report_list = []
    for threshold_default_status in threshold_default_status_list:
        thresh_classification_report = classification_report(
            y_test,
            threshold_default_status,
            target_names=target_names,
            output_dict=True,
            zero_division=0,
        )
        classification_report_list.append(thresh_classification_report)
    # Return threshold classification report dict
    return dict(zip(threshold_list, classification_report_list))


def thresh_classification_report_recall_accuracy(
    thresh_classification_report_dict,
):
    thresh_def_recalls_list = []
    thresh_nondef_recalls_list = []
    thresh_accs_list = []
    for x in [*thresh_classification_report_dict]:
        thresh_def_recall = thresh_classification_report_dict[x]["Default"][
            "recall"
        ]
        thresh_def_recalls_list.append(thresh_def_recall)
        thresh_nondef_recall = thresh_classification_report_dict[x][
            "Non-Default"
        ]["recall"]
        thresh_nondef_recalls_list.append(thresh_nondef_recall)
        thresh_accs = thresh_classification_report_dict[x]["accuracy"]
        thresh_accs_list.append(thresh_accs)
    return [
        thresh_def_recalls_list,
        thresh_nondef_recalls_list,
        thresh_accs_list,
    ]


def apply_threshold_to_probability_values(probability_values, threshold):
    return (
        probability_values["PROB_DEFAULT"]
        .apply(lambda x: 1 if x > threshold else 0)
        .rename("PREDICT_DEFAULT_STATUS")
    )


@st.cache(suppress_st_warning=True)
def find_best_threshold_J_statistic(y, clf_prediction_prob_df):
    fpr, tpr, thresholds = roc_curve(y, clf_prediction_prob_df)
    # get the best threshold
    J = tpr - fpr  # Youden’s J statistic
    ix = argmax(J)
    return thresholds[ix]


def default_status_per_threshold(threshold_list, prob_default):
    threshold_default_status_list = []
    for threshold in threshold_list:
        threshold_default_status = prob_default.apply(
            lambda x: 1 if x > threshold else 0
        )
        threshold_default_status_list.append(threshold_default_status)
    return threshold_default_status_list


def threshold_and_predictions(clf_xgbt_model, split_dataset, threshold):

    clf_prediction_prob_df_gbt = model_probability_values_df(
        clf_xgbt_model,
        split_dataset.X_test,
    )
    clf_thresh_predicted_default_status = (
        apply_threshold_to_probability_values(
            clf_prediction_prob_df_gbt,
            threshold,
        )
    )

    streamlit_2columns_metrics_df(
        "# of Predicted Defaults",
        "# of Predicted Non-Default",
        clf_thresh_predicted_default_status,
    )

    streamlit_2columns_metrics_pct_df(
        "% of Loans Predicted to Default",
        "% of Loans Predicted not to Default",
        clf_thresh_predicted_default_status,
    )

    return clf_thresh_predicted_default_status


def user_defined_probability_threshold(model_name_short, clf_xgbt_model, split_dataset):
    st.subheader("Classification Probability Threshold - User Defined")

    user_defined_threshold = st.slider(
        label="Default Probability Threshold:",
        min_value=0.0,
        max_value=1.0,
        value=0.8,
        key=f"threshold_{model_name_short}_default",
    )

    clf_thresh_predicted_default_status = threshold_and_predictions(
        clf_xgbt_model, split_dataset, user_defined_threshold)

    return clf_thresh_predicted_default_status, user_defined_threshold


def J_statistic_driven_probability_threshold(clf_prediction_prob_df_gbt, clf_xgbt_model, split_dataset):
    st.subheader("J Statistic Driven Classification Probability Threshold")

    J_statistic_best_threshold = find_best_threshold_J_statistic(
        split_dataset.y_test, clf_prediction_prob_df_gbt
    )
    st.metric(
        label="Youden's J statistic calculated best threshold",
        value=J_statistic_best_threshold,
    )

    clf_thresh_predicted_default_status = threshold_and_predictions(
        clf_xgbt_model, split_dataset, J_statistic_best_threshold)

    return clf_thresh_predicted_default_status, J_statistic_best_threshold


def create_tradeoff_graph(df):
    fig2 = px.line(
        data_frame=df,
        y=["Default Recall", "Non Default Recall", "Accuracy"],
        x="Threshold",
    )

    fig2.update_layout(
        title="Recall and Accuracy score Trade-off with Probability Threshold",
        xaxis_title="Probability Threshold",
        yaxis_title="Score",
    )
    fig2.update_yaxes(range=[0.0, 1.0])

    st.plotly_chart(fig2)


def tradeoff_threshold(clf_prediction_prob_df_gbt, split_dataset):
    st.subheader(
        "Recall and Accuracy Tradeoff with given Probability Threshold"
    )

    threshold_list = np.arange(
        0, 1, 0.025).round(decimals=3).tolist()

    threshold_default_status_list = default_status_per_threshold(
        threshold_list, clf_prediction_prob_df_gbt["PROB_DEFAULT"]
    )
    thresh_classification_report_dict = (
        classification_report_per_threshold(
            threshold_list,
            threshold_default_status_list,
            split_dataset.y_test,
        )
    )

    (
        thresh_def_recalls_list,
        thresh_nondef_recalls_list,
        thresh_accs_list,
    ) = thresh_classification_report_recall_accuracy(
        thresh_classification_report_dict
    )

    namelist = [
        "Default Recall",
        "Non Default Recall",
        "Accuracy",
        "Threshold",
    ]

    df = pd.DataFrame(
        [
            thresh_def_recalls_list,
            thresh_nondef_recalls_list,
            thresh_accs_list,
            threshold_list,
        ],
        index=namelist,
    )

    df = df.T

    create_tradeoff_graph(df)


def select_probability_threshold(model_name_short,
                                 user_defined_threshold,
                                 clf_thresh_predicted_default_status_user_gbt,
                                 J_statistic_best_threshold,
                                 clf_thresh_predicted_default_status_Jstatistic_gbt,
                                 acc_rate_thresh_gbt,
                                 clf_thresh_predicted_default_status_acceptance_gbt):
    st.subheader("Selected Probability Threshold")

    options = [
        "User Defined",
        "J Statistic Driven",
        "Acceptance Rate Driven",
    ]
    prob_thresh_option = st.radio(
        label="Selected Probability Threshold",
        options=options,
        key=f"{model_name_short}_radio_thresh",
    )

    if prob_thresh_option == "User Defined":
        prob_thresh_selected_gbt = user_defined_threshold
        predicted_default_status_gbt = (
            clf_thresh_predicted_default_status_user_gbt
        )
    elif prob_thresh_option == "J Statistic Driven":
        prob_thresh_selected_gbt = J_statistic_best_threshold
        predicted_default_status_gbt = (
            clf_thresh_predicted_default_status_Jstatistic_gbt
        )
    else:
        prob_thresh_selected_gbt = acc_rate_thresh_gbt
        predicted_default_status_gbt = (
            clf_thresh_predicted_default_status_acceptance_gbt
        )

    st.write(
        f"Selected probability threshold is {prob_thresh_selected_gbt}"
    )

    return prob_thresh_selected_gbt, predicted_default_status_gbt


def acceptance_rate_driven_threshold(model_name_short, clf_prediction_prob_df_gbt):
    st.subheader("Acceptance Rate Driven Probability Threshold")
    # Steps
    # Set acceptance rate
    # Get default status per threshold
    # Get classification report per threshold
    # Get recall, nondef recall, and accuracy per threshold

    acceptance_rate = (
        st.slider(
            label="% of loans accepted (acceptance rate):",
            min_value=0,
            max_value=100,
            value=85,
            key=f"acceptance_rate_{model_name_short}",
            format="%f%%",
        )
        / 100
    )

    acc_rate_thresh_gbt = np.quantile(
        clf_prediction_prob_df_gbt["PROB_DEFAULT"], acceptance_rate
    )

    st.write(
        f"An acceptance rate of {acceptance_rate} results in probability threshold of {acc_rate_thresh_gbt}"
    )

    acceptance_rate_driven_threshold_graph(
        clf_prediction_prob_df_gbt, acc_rate_thresh_gbt)

    clf_thresh_predicted_default_status_acceptance_gbt = apply_threshold_to_probability_values(
        clf_prediction_prob_df_gbt,
        acc_rate_thresh_gbt,
    )

    return acc_rate_thresh_gbt, clf_thresh_predicted_default_status_acceptance_gbt