The standard advice on Credit Scoring Models with Python has drifted toward “throw more features at the XGBoost model and let the gradient boosting sort it out.” That costs you interpretability, and it leaves technical debt sitting in production for someone else to pay off.
In my 14+ years of building backend services and custom integrations, the projects that fell over were the ones where variable relationships stayed a black box. If you start training before you know how the features interact, what you have built is a liability. Feature selection has to rest on statistics you ran yourself rather than an automated “feature importance” chart.
Handle the basics first, outliers and missing values included. I covered that in my guide to Credit Scoring EDA. With clean data in hand, you can start measuring relationships.
Why relationships matter in credit scoring models
Two things are worth measuring here: predictive power and dimensionality. First, does the variable actually separate “default” from “non-default”? Second, is it repeating what another variable already told you? Two features carrying the same information is a refactor waiting to happen.
1. Continuous variables against a binary target
With a continuous feature like person_income and a binary target, the mean tells you very little. I have seen tidy-looking medians hide variance wide enough to make the whole risk assessment unstable. Use a non-parametric test instead, such as the Kruskal-Wallis H-test.
Kruskal-Wallis tests whether the population medians of the groups are equal. A p-value under 0.05 means the variable probably has discriminative power. In Python:
from scipy.stats import kruskal
import pandas as pd
def bbioon_check_predictive_power(df, continuous_var, target):
# Drop NAs to avoid SciPy silent failures
groups = [group[continuous_var].dropna().values for _, group in df.groupby(target)]
if len(groups) < 2:
return None
stat, p_value = kruskal(*groups)
return p_value
# Usage
p_val = bbioon_check_predictive_power(train_data, 'person_income', 'def')
print(f"P-value: {p_val}")
The official SciPy kruskal documentation has the implementation details.
2. Categorical variables and Cramer’s V
For person_home_ownership (categorical) against default (binary), a plain Chi-square test is too sensitive to sample size to tell you much. Cramer’s V gives you the intensity of the relationship as a number between 0 and 1. Above 0.1 counts as a low association, above 0.3 as moderate.
High values between two explanatory variables are the ones to watch. Anything over 0.5 there points at redundancy, and most Cramer’s V guidelines treat that as a very strong association worth investigating.
Spearman or Pearson for multicollinearity
This is where junior devs trip most often. They call df.corr(), which defaults to Pearson, and Pearson only sees linear relationships. In credit scoring the relationships are usually monotonic without being linear, so Spearman rank correlation is the one to reach for. It holds up against outliers and assumes nothing about normality.
On one project, loan_amnt and loan_percent_income came in above 85% correlated. Keeping both made the model behave erratically on edge cases, so we refactored the pipeline to drop the less predictive one. This Spearman Correlation Guide goes further into the method.
# The Robust Approach
corr_matrix = df[continuous_vars].corr(method='spearman')
# Highlight redundancies above 60%
redundant_pairs = corr_matrix[corr_matrix > 0.6].stack().reset_index()
redundant_pairs = redundant_pairs[redundant_pairs['level_0'] != redundant_pairs['level_1']]
If these calculations crawl, profile the script before you optimize anything. I have written about Python Profiling with Py-Spy, which is what I use for exactly this.
If this credit scoring work is eating your dev hours, I can take it off your hands. I have been working with WordPress, Python integrations and complex data pipelines since the 4.x days.
The short version
Kruskal-Wallis for predictive power, Cramer’s V for categorical strength, Spearman for redundancy checks. Run those before you train anything and you know what each feature is doing, which is the difference between a scoring model you can defend and one you end up apologizing for.