Logistic regression coefficients

Fitting a logistic regression model is easy in R, but coefficient interpretation is non-trivial
code
analysis
Author

David Harper, CFA, FRM

Published

September 4, 2023

I wanted to shadow GARP’s logistic regression example, so I sampled from the same LendingClub database and performed a similar logistic regression. The key difference is practical: I’ll often re-sample from the data in order to get a result that lends itself to a good practice question. I’ve been writing practice questions for a long time, and there are many little details that go into this. For example, GARP’s logistic regression shows 10 independent variables, and I reduced that to seven merely because I don’t need to show all the variables to make the point.

After I seeded the result that appealed to me, I wrote the practice question below (the published question sans answer is here). After fiddling with the four choices, I’m happy with the final question. It’s an “EXCEPT FOR” question, which is what I often use when I’m trying to blanked the concept more comprehensively than a “TRUE” question. This is a bit more work because each distractor must be carefully written.

23.6.1. Darlene is a risk analyst who evaluate the creditworthiness of loan applicants at her financial institution. Her department is testing a new logistic regression model. If the model performs well in testing, it will be deployed to assist in the underwriting decision-making process. The training data is a sub-sample (n = 800) from the same LendingClub database used in reading. In the logistic regression, the dependent variable is a 0/1 for the terminal state of the loan being either zero (fully paid off) or one (deemed irrecoverable or defaulted). In the actual code, this dependent variable is labeled ‘outcome’.

The following are the features (aka, independent variables) as given by their textual labels: Amount, Term, Interest_rate, Installment, Employ_hist, Income, and Bankruptcies. In regard to units in the database, please note the following: Amount is thousands of dollars ($000s); Term is months; Interest_rate is effectively multiplied by one hundred such that 7 equates to 7% or 0.070; Installment is dollars; Employment_hist is years; Income is thousand of dollars ($000); and Bankruptcies is a whole number {0, 1, 2, …}.

The table below displays the logistic regression results:

<<See regression output below; table will paste here>>

In regard to this logistic regression, each of the following statements is true EXCEPT which is false?

  1. A single additional bankruptcy increases the expected odds of default by almost 58 percent
  2. If she requires significance at the 5% level or better, then two of the coefficients (in addition to the intercept) are significant
  3. Each +100 basis points increase in the interest rate (e.g., from 8.0% to 9.0%) implies an increase of about 14.0 basis points in the default probability
  4. If the cost of making a bad loan is high, she can decrease the threshold (i.e., set Z to a low value such as 0.05), but this will reject more good borrowers

Here is the code with some comments. The logistic model itself, a type of glm(), requires only a single line and the model is stored in logit_model_1 as a list object. Most of my code is re-coding the dataset, and then rendering the model’s coefficients with the awesome gt package. Posit’s Richard Iannone does an incredible job maintaining the package. If you think about it, generating tables are really important in data!

library(tidyverse) 
library(gt)
# library(labelled) Didn't use but helpful

# set.seed(xzy)
set.seed(374)

sample_size <- 800
lcfeatures <- read_csv("lcfeatures.csv") 
# Same LendingClub dataset used in FRM Chapter 15 (Logistic Regression Example)
# Located at https://www.kaggle.com/datasets/wordsforthewise/lending-club
# But lcfeatures is a random sample of 10,000 which is too large for my need
# So I just sample_n as random subset of the 10,000
lcfeatures <- lcfeatures |> sample_n(sample_size)

# recoding 
lcfeatures$emp_length_n <- gsub("< 1", "0", lcfeatures$emp_length)
lcfeatures$emp_length_n2 <- parse_number(lcfeatures$emp_length_n)
lcfeatures$term_n <- parse_number(lcfeatures$term)

lcfeatures$home_ownership_simpler <- recode(lcfeatures$home_ownership,
                                             "MORTGAGE" = "OWN",
                                             "ANY" = "RENT",
                                             "NONE" = "RENT")

lcfeatures$mortgage_simpler <- recode(lcfeatures$home_ownership,
                                       "OWN" = "NO",
                                       "ANY" = "NO",
                                       "NONE" = "NO",
                                       "RENT" = "NO",
                                       "MORTGAGE" = "YES")

lcfeatures$loan_status_coded <- recode(lcfeatures$loan_status,
                                        "Charged Off" = "Default",
                                        "Does not meet the credit policy. Status:Charged Off" = "Default",
                                        "Late (31-120 days)" = "Default",
                                        .default = "Paid")

lcfeatures$home_ownership_bern <- recode(lcfeatures$home_ownership_simpler,
                                          "RENT" = 0,
                                          "OWN" = 1)

lcfeatures$mortgage_bern <- recode(lcfeatures$mortgage_simpler,
                                          "NO" = 0,
                                          "YES" = 1)

lcfeatures$loan_status_bern <- recode(lcfeatures$loan_status_coded,
                                          "Paid" = 0,
                                          "Default" = 1)

lcfeatures$loan_amnt_000 <- lcfeatures$loan_amnt / 1000
lcfeatures$annual_inc_000 <- lcfeatures$annual_inc / 1000
lcfeatures$outcome <- lcfeatures$loan_status_bern

# This is logistic regression model
logit_model_1 <- glm(formula = outcome ~ loan_amnt_000 + term_n + int_rate + installment + 
        emp_length_n2 + annual_inc_000 + pub_rec_bankruptcies,
        family = binomial(link = "logit"), data = lcfeatures)

coef_table <- coef(summary(logit_model_1)) 
coef_tbl  <-  as_tibble(coef_table)
Coeff_labels <- c("(Intercept)", "Amount", "Term", "Interest_rate", "Installment", 
                 "Employment_hist", "Income","Bankruptcies")
coef_tbl <- cbind(Coeff_labels, coef_tbl)

# Using gt() to render a table
coef_tbl_gt <- coef_tbl %>% gt() |> 
    opt_table_font(stack = "humanist") |>
    fmt_number(columns = everything(),
               decimals = 3)
coef_tbl_gt
Coeff_labels Estimate Std. Error z value Pr(>|z|)
(Intercept) −2.329 0.841 −2.769 0.006
Amount 0.123 0.092 1.339 0.181
Term −0.041 0.027 −1.519 0.129
Interest_rate 0.140 0.034 4.108 0.000
Installment −0.003 0.003 −1.033 0.302
Employment_hist −0.032 0.031 −1.025 0.305
Income −0.003 0.003 −0.937 0.349
Bankruptcies 0.457 0.230 1.991 0.046

If we use predict() with type = “response”, then the logistic regression returns the vector of predicted probabilities (from zero to 100%). We can classify the Bernoulli prediction (0 = nondefault, 1 = default) as a function of our desired conservative/aggressive threshold. Below I show the number of rejections would increase as we lower the threshold.

predicted_probs <- predict(logit_model_1, lcfeatures, type = "response")
thresholds <- c(0.4, 0.3, 0.2, 0.1, 0.05, 0.010)
thresholds |> map_int(\(x) sum(ifelse(predicted_probs > x, 1, 0), na.rm = TRUE))
[1]   5  30  86 383 689 748

Inspired by this blog post on color coding the {gt} table, I added some color to highlight the significant coefficients (obviously not in the actual Q&A, just here!):

coef_tbl_gt |> 
    data_color(
        columns = 'Pr(>|z|)', 
        palette = c("#19F000","#E4FF00"),
        domain = c(0,0.05),
        na_color = "lightgrey"
    )
Coeff_labels Estimate Std. Error z value Pr(>|z|)
(Intercept) −2.329 0.841 −2.769 0.006
Amount 0.123 0.092 1.339 0.181
Term −0.041 0.027 −1.519 0.129
Interest_rate 0.140 0.034 4.108 0.000
Installment −0.003 0.003 −1.033 0.302
Employment_hist −0.032 0.031 −1.025 0.305
Income −0.003 0.003 −0.937 0.349
Bankruptcies 0.457 0.230 1.991 0.046