What factors best predict the interest rate assigned to a personal loan?
Not all borrowers are offered the same interest rate — lenders price loans based on how risky they believe a borrower is. This project looks for the borrower- and loan-level characteristics that are most strongly associated with the interest rate a person actually receives.
The dataset used is loans_full_schema, distributed
through the OpenIntro Statistics R package. It contains 10,000 loans
made through the Lending Club platform and 55 variables describing each
loan and borrower — things like loan amount, term length, interest rate,
loan grade, the borrower’s income, debt load, homeownership status, and
dozens of credit-history details. Because the dataset only contains
loans that were actually issued (not rejected applications), conclusions
here describe approved loans only.
For this analysis we focus on the response variable
interest_rate and six candidate predictors, chosen because
they are known at the time before the loan is approved.
| Variable | Type | Description |
|---|---|---|
interest_rate (response) |
numeric | Interest rate assigned to the loan (%) |
annual_income |
numeric | Borrower’s stated annual income |
debt_to_income |
numeric | Debt-to-income ratio |
loan_amount |
numeric | Amount of the loan |
homeownership |
categorical | Rent / own / mortgage |
verified_income |
categorical | Whether income was verified by Lending Club |
term |
categorical | Loan length in months (36 or 60) |
Source: Diez, D., Barr, C., & Çetinkaya-Rundel,
M. OpenIntro Statistics data — loans_full_schema,
available at https://www.openintro.org/data/index.php?data=loans_full_schema,
originally sourced from Lending Club (https://www.lendingclub.com/info/statistics.action).
We start by inspecting the structure of the dataset, identifying variables that are stored as numbers but really behave as categories, cleaning our variables of interest, and visualizing their distributions before fitting any model.
loans <- read.csv("loans_full_schema.csv")
dim(loans) # 10,000 rows x 55 columns
## [1] 10000 55
table(sapply(loans, class)) # how many character / integer / numeric columns
##
## character integer numeric
## 13 30 12
var_character <- loans |>
select(where(is.character))
length(names(var_character))
## [1] 13
names(var_character)
## [1] "emp_title" "state"
## [3] "homeownership" "verified_income"
## [5] "verification_income_joint" "loan_purpose"
## [7] "application_type" "grade"
## [9] "sub_grade" "issue_month"
## [11] "loan_status" "initial_listing_status"
## [13] "disbursement_method"
var_integer <- loans |>
select(where(is.integer))
length(names(var_integer))
## [1] 30
names(var_integer)
## [1] "emp_length" "delinq_2y"
## [3] "months_since_last_delinq" "earliest_credit_line"
## [5] "inquiries_last_12m" "total_credit_lines"
## [7] "open_credit_lines" "total_credit_limit"
## [9] "total_credit_utilized" "num_collections_last_12m"
## [11] "num_historical_failed_to_pay" "months_since_90d_late"
## [13] "current_accounts_delinq" "total_collection_amount_ever"
## [15] "current_installment_accounts" "accounts_opened_24m"
## [17] "months_since_last_credit_inquiry" "num_satisfactory_accounts"
## [19] "num_accounts_120d_past_due" "num_accounts_30d_past_due"
## [21] "num_active_debit_accounts" "total_debit_limit"
## [23] "num_total_cc_accounts" "num_open_cc_accounts"
## [25] "num_cc_carrying_balance" "num_mort_accounts"
## [27] "tax_liens" "public_record_bankrupt"
## [29] "loan_amount" "term"
var_double <- loans |>
select(where(is.double))
length(names(var_double))
## [1] 12
names(var_double)
## [1] "annual_income" "debt_to_income"
## [3] "annual_income_joint" "debt_to_income_joint"
## [5] "account_never_delinq_percent" "interest_rate"
## [7] "installment" "balance"
## [9] "paid_total" "paid_principal"
## [11] "paid_interest" "paid_late_fees"
We check each integer variable to see if there are many integer values or a relatively few number of integer values in that column / variable. We need this information when looking for categorical variables. An integer column with only a handful of unique values is usually a category in disguise, not a continuous measurement.
unique_integers <- function(x) {
length(unique(x))
}
n_unique <- sapply(var_integer, unique_integers)
sort(n_unique)
## current_accounts_delinq num_accounts_120d_past_due
## 2 2
## num_accounts_30d_past_due term
## 2 2
## num_collections_last_12m public_record_bankrupt
## 4 4
## num_historical_failed_to_pay tax_liens
## 9 9
## emp_length delinq_2y
## 12 12
## num_mort_accounts num_active_debit_accounts
## 15 25
## inquiries_last_12m accounts_opened_24m
## 26 26
## months_since_last_credit_inquiry current_installment_accounts
## 26 30
## num_cc_carrying_balance num_open_cc_accounts
## 30 40
## open_credit_lines num_satisfactory_accounts
## 45 45
## earliest_credit_line num_total_cc_accounts
## 53 56
## total_credit_lines months_since_last_delinq
## 78 98
## months_since_90d_late loan_amount
## 107 612
## total_collection_amount_ever total_debit_limit
## 896 1222
## total_credit_limit total_credit_utilized
## 9119 9497
term stands out with only 2 unique values:
unique(loans$term)
## [1] 60 36
We see that the “Term” column contains integer types with only two unique values throughout the column. Upon further inspection (below), we find the two unique integers are 60 and 36. These refer to the length of term for the loan: 60 and 36 month terms. This does allow us to use Term as a categorical variable.
Those two values are 36 and 60 — loan lengths in months. Even though
term is stored as a number, it behaves like a category, so
we convert it, along with homeownership and
verified_income, which are also categorical, to factors
before modeling.
loans_with_factors <- loans |>
mutate(
term = factor(term),
homeownership = factor(homeownership),
verified_income = factor(verified_income)
)
# checking for "factor"
class(loans_with_factors$term)
## [1] "factor"
class(loans_with_factors$homeownership)
## [1] "factor"
class(loans_with_factors$verified_income)
## [1] "factor"
Now we focus on the seven variables we set out to analyze, to find out their influence on the multilinear model.
loans_seven_variables <- loans_with_factors |>
select("interest_rate", "annual_income", "debt_to_income", "loan_amount",
"homeownership", "verified_income", "term")
dim(loans_seven_variables) # 10,000 rows by 7 columns
## [1] 10000 7
head(loans_seven_variables)
## interest_rate annual_income debt_to_income loan_amount homeownership
## 1 14.07 90000 18.01 28000 MORTGAGE
## 2 12.61 40000 5.04 5000 RENT
## 3 17.09 40000 21.15 2000 RENT
## 4 6.72 30000 10.16 21600 RENT
## 5 14.07 35000 57.96 23000 RENT
## 6 6.72 34000 6.46 5000 OWN
## verified_income term
## 1 Verified 60
## 2 Not Verified 36
## 3 Source Verified 36
## 4 Not Verified 36
## 5 Verified 36
## 6 Not Verified 36
Next we check for NA values in our data frame.
sum(is.na(loans_seven_variables$interest_rate))
## [1] 0
sum(is.na(loans_seven_variables$annual_income))
## [1] 0
sum(is.na(loans_seven_variables$debt_to_income))
## [1] 24
sum(is.na(loans_seven_variables$loan_amount))
## [1] 0
sum(is.na(loans_seven_variables$homeownership))
## [1] 0
sum(is.na(loans_seven_variables$verified_income))
## [1] 0
sum(is.na(loans_seven_variables$term))
## [1] 0
debt_to_income is the only column with missing values
(24 rows). We drop those rows since they’re a small fraction of the
dataset.
loans_seven_variables_no_nas <- loans_seven_variables |>
drop_na()
ncol(loans_seven_variables_no_nas)
## [1] 7
nrow(loans_seven_variables_no_nas)
## [1] 9976
dim(loans_seven_variables_no_nas) # 9,976 rows x 7 columns
## [1] 9976 7
sum(is.na(loans_seven_variables_no_nas$debt_to_income)) # confirm 0 remaining NAs
## [1] 0
Rather than a formal skewness statistic, we compare each numeric variable’s mean to its median. If the mean sits notably above the median, the distribution is right-skewed.
loans_summary <- loans_seven_variables_no_nas |>
summarise(
annual_income_mean = mean(annual_income),
annual_income_median = median(annual_income),
debt_to_income_mean = mean(debt_to_income),
debt_to_income_median = median(debt_to_income),
loan_amount_mean = mean(loan_amount),
loan_amount_median = median(loan_amount)
)
loans_summary # row version
## annual_income_mean annual_income_median debt_to_income_mean
## 1 79412.74 65000 19.30819
## debt_to_income_median loan_amount_mean loan_amount_median
## 1 17.57 16357.53 14500
t(loans_summary) # column version — easier to read
## [,1]
## annual_income_mean 79412.73889
## annual_income_median 65000.00000
## debt_to_income_mean 19.30819
## debt_to_income_median 17.57000
## loan_amount_mean 16357.52807
## loan_amount_median 14500.00000
Results:
For annual income, the mean was 79,412.74 dollars and the median was 65,000.00 dollars. In this case, the mean is above the median by about 22% or 14,400 dollars. We suspect the distribution of annual income is right skewed. It could be that a few families with very high annual incomes may have pulled the mean to the right. We will try a transformation on this variable.
For loan amount, the mean was about 16,358 dollars and the median was about 14,500 dollars. In this case, the mean is above the median by about 1858 dollars or about 13% of the median.
For debt to income, the mean was 19.31 and the median was 17.57. In this case, the mean is above the median by about 10%.
The annual income is definitely skewed so we will continue with a transform on that variable.
To supplement the median mean comparisons for the two remaining numerical varaiables, we use histograms to see if we can discern a skewness.
ggplot(loans_seven_variables_no_nas, aes(x = annual_income)) +
geom_histogram(bins = 50, fill = "steelblue", color = "white") +
labs(title = "Distribution of Annual Income", x = "Annual Income", y = "Count") +
theme_minimal()
ggplot(loans_seven_variables_no_nas, aes(x = debt_to_income)) +
geom_histogram(bins = 50, fill = "red", color = "white") +
labs(title = "Distribution of Debt-to-Income Ratio", x = "Debt-to-Income Ratio", y = "Count") +
theme_minimal()
ggplot(loans_seven_variables_no_nas, aes(x = loan_amount)) +
geom_histogram(bins = 50, fill = "green", color = "white") +
labs(title = "Distribution of Loan Amount", x = "Loan Amount", y = "Count") +
theme_minimal()
Given the histograms, it appears that annual income and debt to income are both right skewed. We will see if a transformation will help us see associations in the multilinear model.
The distribution of loan amount seems to be mildly skewed. The counts on loan amounts at 20,000 dollars is almost 750 loans. There are 350 to 400 loans each at 25,000, 30,000, 35,000, and 40,000 dollars. That said, there are more loans at the in between amonts for loans less than 20,000 dollars. We feel that transforming loan amount is not needed.
It was suggested by Professor Ch that we try a log transformation. The log1p() = log (1+x)transformation handles data values of x =0 very well, returning output of log(0+1) = log(1) = 0 at x = 0. For log(x), x=0 is not in the domain of the log(). At best, R may return infinity.
Log transformation of skewed predictors is a widely used technique in regression analysis, we applied it here with our professor’s encouragement (see References).
We compared two transformations on debt_to_income —
log1p() and sqrt() — before deciding which to
use:
loans_transformed <- loans_seven_variables_no_nas |>
mutate(
log_annual_income = log1p(annual_income),
log_debt_to_income = log1p(debt_to_income),
sqrt_debt_to_income = sqrt(debt_to_income)
# loan_amount left untransformed — mild skew, clustering at round dollar amounts
)
names(loans_transformed)
## [1] "interest_rate" "annual_income" "debt_to_income"
## [4] "loan_amount" "homeownership" "verified_income"
## [7] "term" "log_annual_income" "log_debt_to_income"
## [10] "sqrt_debt_to_income"
We have the following histograms of the log transformed annual income data:
ggplot(loans_transformed, aes(x = annual_income)) +
geom_histogram(bins = 50, fill = "steelblue", color = "white") +
labs(title = "Annual Income — Before Transformation", x = "Annual Income", y = "Count") +
theme_minimal()
ggplot(loans_transformed, aes(x = log_annual_income)) +
geom_histogram(bins = 50, fill = "lightblue", color = "white") +
labs(title = "Annual Income — After log1p() Transformation", x = "log(1 + Annual Income)", y = "Count") +
theme_minimal()
The histogram of the log1p transform of annual_income has symmetry and is absent of right tail skewness. The histogram looks very normal. (Thank you Professor!)
We repeat the process for debt_to_income as well.
ggplot(loans_transformed, aes(x = debt_to_income)) +
geom_histogram(bins = 50, fill = "red", color = "white") +
labs(title = "Debt-to-Income — Before Transformation", x = "Debt-to-Income Ratio", y = "Count") +
theme_minimal()
ggplot(loans_transformed, aes(x = log_debt_to_income)) +
geom_histogram(bins = 50, fill = "pink", color = "white") +
labs(title = "Debt-to-Income — After log1p() Transformation", x = "log(1 + Debt-to-Income)", y = "Count") +
theme_minimal()
The histogram for the log transform of debt_to_income seems to be left skewed after transformation. For this reason, we try a different transform in an attempt to see a more normal distribution. The dataset is mutated with the addition on a column devoted to the square root of the debt_to_income variable. The histogram of the square root transform is below.
ggplot(loans_transformed, aes(x = sqrt_debt_to_income)) +
geom_histogram(bins = 50, fill = "orange", color = "white") +
labs(title = "Debt-to-Income — After sqrt() Transformation", x = "sqrt(Debt-to-Income)", y = "Count") +
theme_minimal()
Comparing the two debt_to_income transformations, the
square-root version produced a visibly more symmetric, bell-shaped
distribution than the log-transformed version, so we use
sqrt_debt_to_income (not log_debt_to_income)
going forward. Guidance is much appreciated here as well!
We will use the log1p transform on the annual_income variable and the square root transform on the debt_to_income variable. We leave the loan_amount variable as is with the light skew and clustering at certain dollar amounts equally spaced by 5000 dollars.
Final transformation decisions:
| Variable | Transform | Reason |
|---|---|---|
annual_income |
log1p() |
Severe right skew (mean ~22% above median); long right tail |
debt_to_income |
sqrt() |
Milder skew (~10% gap); sqrt() produced the more
symmetric result of the two transforms tested |
loan_amount |
none | Mild skew (~13% gap); clustering at round dollar amounts rather than a long tail |
Predictors: annual_income
(log-transformed), debt_to_income
(square-root–transformed), loan_amount (untransformed),
homeownership, verified_income, and
term. Response: interest_rate
(quantitative).
Because interest_rate is a continuous percentage rather
than a yes/no outcome, multiple linear regression — not logistic
regression — is the appropriate tool, regardless of the fact that
several of our predictors are categorical (the choice between linear and
logistic regression depends on the response variable, not the
predictors). We transform annual_income and
debt_to_income because both are right-skewed; regression
works best when relationships are approximately linear, and reducing
skew in the predictors helps that assumption hold.
loans_transformed |>
select(interest_rate, log_annual_income, sqrt_debt_to_income, loan_amount) |>
cor(use = "complete.obs")
## interest_rate log_annual_income sqrt_debt_to_income
## interest_rate 1.00000000 -0.1253211 0.17997097
## log_annual_income -0.12532114 1.0000000 -0.25417662
## sqrt_debt_to_income 0.17997097 -0.2541766 1.00000000
## loan_amount 0.06506083 0.4098242 0.04988062
## loan_amount
## interest_rate 0.06506083
## log_annual_income 0.40982417
## sqrt_debt_to_income 0.04988062
## loan_amount 1.00000000
Numerical Variable Correlations with interest_rate (the response):
log_annual_income: -0.125 — weak negative. Higher income is very mildly associated with lower interest rates.
sqrt_debt_to_income: 0.180 — weak positive, and the strongest of the three correlations with interest_rate. Higher debt-to-income is associated with higher interest rates — this makes intuitive sense: more debt relative to income signals more risk to a lender, so they charge more.
loan_amount: 0.065 — very weak positive, barely distinguishable from no relationship.
So sqrt_debt_to_income has the strongest individual relationship with interest_rate, though even that is a weak correlation overall (0.18 is far from strong).
None of these three predictors is strongly correlated with interest_rate on its own, which sets the expectation that no single variable will “explain” much of the variation by itself; the categorical variables (homeownership, verified_income, term) and the combination of everything together in the multiple regression may end up doing more of the work.
model <- lm(interest_rate ~ log_annual_income + sqrt_debt_to_income + loan_amount +
homeownership + verified_income + term,
data = loans_transformed)
summary(model)
##
## Call:
## lm(formula = interest_rate ~ log_annual_income + sqrt_debt_to_income +
## loan_amount + homeownership + verified_income + term, data = loans_transformed)
##
## Residuals:
## Min 1Q Median 3Q Max
## -14.1872 -3.2935 -0.7072 2.4822 19.8642
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 1.265e+01 1.025e+00 12.340 < 2e-16 ***
## log_annual_income -4.206e-01 8.867e-02 -4.744 2.13e-06 ***
## sqrt_debt_to_income 5.057e-01 3.389e-02 14.919 < 2e-16 ***
## loan_amount -4.301e-05 5.267e-06 -8.166 3.58e-16 ***
## homeownershipOWN 5.922e-01 1.383e-01 4.282 1.87e-05 ***
## homeownershipRENT 1.327e+00 9.976e-02 13.305 < 2e-16 ***
## verified_incomeSource Verified 1.375e+00 1.025e-01 13.412 < 2e-16 ***
## verified_incomeVerified 2.773e+00 1.231e-01 22.516 < 2e-16 ***
## term60 4.085e+00 1.044e-01 39.134 < 2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 4.411 on 9967 degrees of freedom
## Multiple R-squared: 0.2216, Adjusted R-squared: 0.221
## F-statistic: 354.7 on 8 and 9967 DF, p-value: < 2.2e-16
coef(model)
## (Intercept) log_annual_income
## 1.265281e+01 -4.205928e-01
## sqrt_debt_to_income loan_amount
## 5.056730e-01 -4.301053e-05
## homeownershipOWN homeownershipRENT
## 5.922131e-01 1.327309e+00
## verified_incomeSource Verified verified_incomeVerified
## 1.375151e+00 2.772607e+00
## term60
## 4.084868e+00
summary(model)$r.squared
## [1] 0.2216263
summary(model)$adj.r.squared
## [1] 0.2210015
summary(model)$coefficients
## Estimate Std. Error t value
## (Intercept) 1.265281e+01 1.025314e+00 12.340424
## log_annual_income -4.205928e-01 8.866703e-02 -4.743509
## sqrt_debt_to_income 5.056730e-01 3.389359e-02 14.919429
## loan_amount -4.301053e-05 5.267051e-06 -8.165961
## homeownershipOWN 5.922131e-01 1.382996e-01 4.282104
## homeownershipRENT 1.327309e+00 9.975836e-02 13.305244
## verified_incomeSource Verified 1.375151e+00 1.025318e-01 13.411949
## verified_incomeVerified 2.772607e+00 1.231415e-01 22.515610
## term60 4.084868e+00 1.043822e-01 39.133741
## Pr(>|t|)
## (Intercept) 9.832600e-35
## log_annual_income 2.129645e-06
## sqrt_debt_to_income 8.476038e-50
## loan_amount 3.575189e-16
## homeownershipOWN 1.868686e-05
## homeownershipRENT 4.734147e-40
## verified_incomeSource Verified 1.157989e-40
## verified_incomeVerified 1.528007e-109
## term60 9.777072e-312
Results: Report and Interpret slopes, intercept, and r-squared
Intercept (12.65):
The predicted interest rate when x = 0 log_annual_income = 0, sqrt_debt_to_income = 0, loan_amount = 0, homeownership = MORTGAGE, income = Not Verified, term = 36. In this model, x = 0 does not give good predictive information. For starters, this would be an application for a loan of $0.
Slope of Continuous predictors:
For every 1-unit increase in log_annual_income (i.e., a borrower’s log-transformed income), predicted interest rate decreases by 0.42 percentage points, holding all other variables constant (p = 2.13e-6, statistically significant at p < 0.001). Since this is a log1p()-transformed variable, doubling a borrower’s annual income is associated with roughly a 0.29 percentage-point decrease in interest rate [ (0.4206 x ln(2Income) = 0.4206 x (ln(2) + ln(income) = [0.4206 × ln(2)] + (0.4206)ln(income) ≈ 0.29 + 0.4206ln(income) ].
For every 1-unit increase in sqrt_debt_to_income (i.e., the square-root-transformed debt-to-income ratio), predicted interest rate increases by 0.51 percentage points, holding all other variables constant (p = 8.48e-50, statistically significant at p < 0.001). This marginal effect is larger for borrowers who already have low debt-to-income and smaller for those with high debt-to-income. Think of the square root function increasing quickly leaving the origin and then ascending slowly.
For every $1 increase in loan_amount, predicted interest rate decreases by 0.000043 percentage points, holding all other variables constant — equivalently, about 0.43 percentage points per $10,000 (p = 3.58e-16, statistically significant at p < 0.001).
Slopes of Categorical predictors:
For every 1-unit increase in homeownershipOWN (i.e., the borrower’s status changes from having a mortgage to owning outright), predicted interest rate increases by 0.59 percentage points, holding all other variables constant (p = 1.87e-5, statistically significant at p < 0.001).
For every 1-unit increase in homeownershipRENT (i.e., the borrower’s status changes from having a mortgage to renting), predicted interest rate increases by 1.33 percentage points, holding all other variables constant (p = 4.73e-40, statistically significant at p < 0.001).
For every 1-unit increase in verified_incomeSource Verified (i.e., the borrower’s status changes from unverified to source-verified), predicted interest rate increases by 1.38 percentage points, holding all other variables constant (p = 1.16e-40, statistically significant at p < 0.001).
For every 1-unit increase in verified_incomeVerified (i.e., the borrower’s status changes from unverified to fully verified), predicted interest rate increases by 2.77 percentage points, holding all other variables constant (p = 1.53e-109, statistically significant at p < 0.001).
For every 1-unit increase in term60 (i.e., the loan’s term changes from 36 months to 60 months), predicted interest rate increases by 4.08 percentage points, holding all other variables constant (p = 9.78e-312, statistically significant at p < 0.001) — the largest effect in the model.
Every predictor in the model is statistically significant at the p < 0.001 level. This indicates strong evidence that each is genuinely associated with interest rate rather than reflecting random sampling variation.
Adjusted R-squared (0.2210015) :
The model’s Adjusted R-squared was 0.2210, meaning approximately 22.1% of the variation in interest rate is explained by annual income, debt-to-income, loan amount, home ownership, income verification status, and loan term combined. While this leaves a good amount of variation unexplained, it is the beginning of a deeper exploration into the numerous variables that we could analyze. Also, the adjusted r squared and r-squared were pretty close. (r-squared: 0.2216263, adj r-squared: 0.2210015)
plot(model, which = 1) # Residuals vs. Fitted — homoscedasticity / linearity
Residual vs Fitted Plot - not supporting homoscedasticity
The plot shows a concentration of residuals between fitted values of about -3 up to 17 or 18. The residuals seem to cluster between -10 and +10 with a noticeable downward sloping fence starting at (-3, -3) and going through (15, -10) supporting the cluster. It appears as the lower half of a funnel phenomena. There may be other downward sloping lines of residuals in parallel to each other, stacked on top of one another. The parallel lines of residuals are closer to each other toward the bottom of the graph and then spread out a little as we go up the graph toward positive residuals. This is not the graph of residuals scattered throughout the plot. Although this is a residual plot that does not support the equal variance / homoscedasticity assumption, it is a sign of associations that we may have not yet found in the data.
Q - Q Plot - not supporting normally distributed residuals
plot(model, which = 2) # Q-Q plot — normality of residuals
The Q-Q plot definitely shows a curve that is not staying on the diagonal. The curve is above the diagonal at both the lower left end and the upper right end. This can occur when there are more residuals in the tails than would be present in a normal distribution. This strong s-shape occurs when the residuals are not normally distributed.
VIF - all values less than 5/ no visible multicollinearity
vif(model) # VIF > 5 (or 10) flags a multicollinearity problem
## GVIF Df GVIF^(1/(2*Df))
## log_annual_income 1.429836 1 1.195757
## sqrt_debt_to_income 1.143173 1 1.069193
## loan_amount 1.509198 1 1.228494
## homeownership 1.100536 2 1.024238
## verified_income 1.108727 2 1.026139
## term 1.179763 1 1.086169
VIF - all GVIF^(1/(2xDf) are less than 5
As one can see, we actually do not have a Variance Inflation Factor, VIF. We have a GVIF and a GVIF^(1/(2xDf)) values. It may be due to the nature of our categorical variables homeownership and verified_income, each of which have 3 levels within them. Homeownership has mortgage, own, and rent as levels. Verified_income has not verified, source verified, and verified as levels. From what we could read / skim quickly, R would use a VIF if there were two levels in each category. Three or more levels, R uses the Generalized VIF for users to make the same decision in the three level cases (Fox). Each of the GVIF^(1/(2xDf)) values are less than 5 as well.
rmse <- sqrt(mean(residuals(model)^2))
rmse
## [1] 4.409204
The RMSE for this model is approximately 4.41. Within the context of this model, that means, on average, this model’s predicted interest rate differs from the actual interest rate by about 4.41 percentage points.
sd(loans_transformed$interest_rate)
## [1] 4.997904
range(loans_transformed$interest_rate)
## [1] 5.31 30.94
mean(loans_transformed$interest_rate)
## [1] 12.42246
The sd is about 5.0. The range is about 5.3. The mean interest rate is about 12.422 percent. Since the RMSE (4.409204) is close to the standard deviation (4.998), our model is not producing an interest prediction that is much better than the average rate plus or minus 1 sd. The adjusted R-squared (0.22) also pointed to a model that can explain approximately 22% of the variability in interest rate is explained by the variables we chose.
Double checking levels:
levels(loans_transformed$homeownership)
## [1] "MORTGAGE" "OWN" "RENT"
levels(loans_transformed$verified_income)
## [1] "Not Verified" "Source Verified" "Verified"
levels(loans_transformed$term)
## [1] "36" "60"
new_applicant <- data.frame(
log_annual_income = log1p(60000),
sqrt_debt_to_income = sqrt(18),
loan_amount = 15000,
homeownership = factor("RENT", levels = levels(loans_transformed$homeownership)),
verified_income = factor("Not Verified", levels = levels(loans_transformed$verified_income)),
term = factor("36", levels = levels(loans_transformed$term))
)
predict(model, new_applicant)
## 1
## 10.85294
Given our multiple linear regression model, we created a hypothetical applicant with the following attributes:
Annual Income of 60,000 Debt to Income ration of 18 Loan amount of 15,000 Under homeownership we chose Renter. The income was Not Verified. The term of the loan was 3 years / 36 months.
The model retuned an interest rate of 10.9 %.
This rate of 10.9% is below the sample mean of 12.42%. We think it is due to the applicant not needing their income verified as well as the shorter loan term of 36 months. These two factors were shown to be associated with a lowered interest rate. On the other hand, we also chose the applicant to be a Renter. This would raise the applicants rate some. It appears that the income level of the applicant also helped lower the rate to below average. The model’s income contribution was approximately -4.63 [ -0.4(log(1+60,000))] to lowering the interest rate.
This project set out to answer: what factors best predict the interest rate assigned to a personal loan? Using a multiple linear regression on 9,976 Lending Club loans, we found that all six predictors — annual income, debt-to-income ratio, loan amount, homeownership status, income verification status, and loan term — were statistically significant (p < 0.001) predictors of interest rate. Higher income was associated with lower interest rates, while higher debt-to-income and larger loan terms were associated with higher rates. By far the strongest effect in the model was loan term: 60-month loans carried interest rates about 4.09 percentage points higher than 36-month loans, holding all other variables steady. This was a substantially larger effect than any other predictor.
One notably counterintuitive finding was income verification status: borrowers with verified income were predicted to have rates 1.38–2.77 points higher than unverified borrowers. We don’t think verification causes higher rates. We think verification more often for applications that already carry risk signals (e.g., non-standard or self-employment income), making verification status a way of communicating an underlying risk rather than a risk-reducing factor.
Overall, the model explained about 22.2% of the variation in interest rate (adjusted R-squared = 0.2216), with an average prediction error of about 4.41 percentage points (RMSE). This was not so great, especially relative to the response variable’s own standard deviation of about 5 points. This tells us that while borrower- and loan-level characteristics genuinely matter for interest rate pricing, the majority of the variation is driven by factors outside this analysis. There is more for us to learn from the data.
The residual diagnostics also revealed heteroscedasticity and non-normal residuals (a right-skewed pattern echoing the skew we originally saw in the raw predictors and in interest_rate itself), meaning the model’s confidence intervals and p-values should be interpreted with some caution despite the large sample size.
In future work we would try transforming interest_rate itself to address the residual non-normality and testing interaction effects (e.g., does the effect of debt-to-income differ for renters versus homeowners?) in multiple pairs throughout the 50 plus variables. Thank you so much!
Diez, D., Barr, C., & Çetinkaya-Rundel, M. OpenIntro
Statistics — loans_full_schema dataset. Retrieved from
https://www.openintro.org/data/index.php?data=loans_full_schema
Lending Club. Statistics (original data source). Retrieved from https://www.lendingclub.com/info/statistics.action
R Core Team. log1p function documentation. R: A
Language and Environment for Statistical Computing. Retrieved from
https://stat.ethz.ch/R-manual/R-devel/library/base/html/log.html
Tukey, J. W. (1977). Exploratory Data Analysis. Addison-Wesley. (Log and square-root transformation of right-skewed variables to improve linearity in regression models — a technique, used here with our professor’s encouragement. : )
Fox, J., Weisberg, S., & Price, B. (2024). vif: Variance
Inflation Factors [R package car documentation]. The
Comprehensive R Archive Network (CRAN). Retrieved from https://cran.r-project.org/web/packages/car/car.pdf