Credit Card Dataset

Code
import warnings
import os

# 1. Suppress the "OpenMP" red text (The big block of text about libraries)
os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE"

# 2. Suppress "FutureWarning", "UserWarning", etc.
warnings.filterwarnings('ignore')

# 3. Suppress the specific ArviZ/PyMC widget warning
# (If it persists even after step 2)
import logging
logging.getLogger('pymc').setLevel(logging.ERROR)

Library Imports

Code
import pandas as pd
import seaborn as sb
import matplotlib.pyplot as plt
import statsmodels.api as sm
import numpy as np
from scipy.stats import chi2_contingency
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
import pymc as pm
import arviz as az

Reading CSV files and skimming their contents

Code
df_credit = pd.read_csv(r"C:\Users\desja\Dropbox\Backup of Jumper Laptop files continuous\Documents\Project Revamp Portfolio\Credit Card Data\Credit_card.csv")
df_label = pd.read_csv(r"C:\Users\desja\Dropbox\Backup of Jumper Laptop files continuous\Documents\Project Revamp Portfolio\Credit Card Data\Credit_card_label.csv")
Code
df_credit.head()
Ind_ID GENDER Car_Owner Propert_Owner CHILDREN Annual_income Type_Income EDUCATION Marital_status Housing_type Birthday_count Employed_days Mobile_phone Work_Phone Phone EMAIL_ID Type_Occupation Family_Members
0 5008827 M Y Y 0 180000.0 Pensioner Higher education Married House / apartment -18772.0 365243 1 0 0 0 NaN 2
1 5009744 F Y N 0 315000.0 Commercial associate Higher education Married House / apartment -13557.0 -586 1 1 1 0 NaN 2
2 5009746 F Y N 0 315000.0 Commercial associate Higher education Married House / apartment NaN -586 1 1 1 0 NaN 2
3 5009749 F Y N 0 NaN Commercial associate Higher education Married House / apartment -13557.0 -586 1 1 1 0 NaN 2
4 5009752 F Y N 0 315000.0 Commercial associate Higher education Married House / apartment -13557.0 -586 1 1 1 0 NaN 2
Code
df_label.head()
Ind_ID label
0 5008827 1
1 5009744 1
2 5009746 1
3 5009749 1
4 5009752 1
Code
df_label.tail()
Ind_ID label
1543 5028645 0
1544 5023655 0
1545 5115992 0
1546 5118219 0
1547 5053790 0
Code
(~df_label['label'].isin([0, 1])).any()
np.False_
So it is apparent that the label column consists of only 0 or 1
It is apparent from the original website where the data was downloaded that supposedly the intended meaning of the label “1” is a rejected application and the meaning of a label “0” is an approved application.

Systematically check the data for NaN and blank values

Code
df_credit.isna().sum()
Ind_ID               0
GENDER               7
Car_Owner            0
Propert_Owner        0
CHILDREN             0
Annual_income       23
Type_Income          0
EDUCATION            0
Marital_status       0
Housing_type         0
Birthday_count      22
Employed_days        0
Mobile_phone         0
Work_Phone           0
Phone                0
EMAIL_ID             0
Type_Occupation    488
Family_Members       0
dtype: int64
Code
df_label.isna().sum()
Ind_ID    0
label     0
dtype: int64

Clean the data

Some replacement for blank Type_Occupation values is needed. I will create a new category “Other” and replace these blank values
Code
df_credit['Type_Occupation'] = df_credit['Type_Occupation'].fillna('Other')
I’m going to drop the rows where the Annual_income is blank. This is a critical variable and since there is such a small fraction of the total dataset that has a blank value, I will simply drop it.
Code
df_credit.dropna(subset=["Annual_income"], axis = 0, inplace = True)
To simplify analysis, I’m going to simply drop the NaN rows for Gender and Birthday also. In this manner, I will not have any empty values in my dataset. The cost of doing this is low, since there are very few rows overall that I am dropping compared with the total amount of data (>1000 rows, only ~40-50 rows being dropped).
Code
df_credit.dropna(subset=["GENDER"], axis = 0, inplace = True)
Code
df_credit.dropna(subset=["Birthday_count"], axis = 0, inplace = True)
Code
df_credit.isna().sum()
Ind_ID             0
GENDER             0
Car_Owner          0
Propert_Owner      0
CHILDREN           0
Annual_income      0
Type_Income        0
EDUCATION          0
Marital_status     0
Housing_type       0
Birthday_count     0
Employed_days      0
Mobile_phone       0
Work_Phone         0
Phone              0
EMAIL_ID           0
Type_Occupation    0
Family_Members     0
dtype: int64

Save the cleaned data to the local hard disk

OK, so the dataset has now been cleaned without removing too much data. Now I will save the updated pandas dataframes as separate CSV files on my local hard drive for storage and retention of these updates to the dataset. Since no changes were made to the “Label” dataset (rejection/approval of the credit application) there is no need to save a separate file for it. It is already clean.
Code
df_credit.to_csv(r"C:\Users\desja\Dropbox\Backup of Jumper Laptop files continuous\Documents\Project Revamp Portfolio\Credit Card Data\Clean Data\credit.csv")

Look at the basic statistical summary for each column in the dataset

Code
df_credit.describe(include =  "all")
Ind_ID GENDER Car_Owner Propert_Owner CHILDREN Annual_income Type_Income EDUCATION Marital_status Housing_type Birthday_count Employed_days Mobile_phone Work_Phone Phone EMAIL_ID Type_Occupation Family_Members
count 1.496000e+03 1496 1496 1496 1496.000000 1.496000e+03 1496 1496 1496 1496 1496.000000 1496.000000 1496.0 1496.000000 1496.000000 1496.000000 1496 1496.000000
unique NaN 2 2 2 NaN NaN 4 5 5 6 NaN NaN NaN NaN NaN NaN 19 NaN
top NaN F N Y NaN NaN Working Secondary / secondary special Married House / apartment NaN NaN NaN NaN NaN NaN Other NaN
freq NaN 947 898 977 NaN NaN 769 998 1011 1331 NaN NaN NaN NaN NaN NaN 471 NaN
mean 5.079217e+06 NaN NaN NaN 0.415775 1.907750e+05 NaN NaN NaN NaN -16036.192513 59290.681818 1.0 0.205882 0.304813 0.094251 NaN 2.165107
std 4.168109e+04 NaN NaN NaN 0.780784 1.131384e+05 NaN NaN NaN NaN 4226.506557 137766.774169 0.0 0.404480 0.460482 0.292276 NaN 0.951752
min 5.008827e+06 NaN NaN NaN 0.000000 3.375000e+04 NaN NaN NaN NaN -24946.000000 -14887.000000 1.0 0.000000 0.000000 0.000000 NaN 1.000000
25% 5.045349e+06 NaN NaN NaN 0.000000 1.210500e+05 NaN NaN NaN NaN -19543.000000 -3229.250000 1.0 0.000000 0.000000 0.000000 NaN 2.000000
50% 5.079010e+06 NaN NaN NaN 0.000000 1.660500e+05 NaN NaN NaN NaN -15686.000000 -1575.500000 1.0 0.000000 0.000000 0.000000 NaN 2.000000
75% 5.115801e+06 NaN NaN NaN 1.000000 2.250000e+05 NaN NaN NaN NaN -12417.000000 -431.000000 1.0 0.000000 1.000000 0.000000 NaN 3.000000
max 5.150412e+06 NaN NaN NaN 14.000000 1.575000e+06 NaN NaN NaN NaN -7705.000000 365243.000000 1.0 1.000000 1.000000 1.000000 NaN 15.000000

Add features to the dataset (adjusted column values)

Since this is an Indian dataset, and I am an American, it is confusing for me to see the salaries which are likely in Indian Rupees. I am going to convert these salaries to US dollars in a separate column for my own mental clarity when looking at this dataset.
Code
df_credit['Annual_income_USD'] = df_credit['Annual_income'] / 90
Code
df_credit.head()
Ind_ID GENDER Car_Owner Propert_Owner CHILDREN Annual_income Type_Income EDUCATION Marital_status Housing_type Birthday_count Employed_days Mobile_phone Work_Phone Phone EMAIL_ID Type_Occupation Family_Members Annual_income_USD
0 5008827 M Y Y 0 180000.0 Pensioner Higher education Married House / apartment -18772.0 365243 1 0 0 0 Other 2 2000.0
1 5009744 F Y N 0 315000.0 Commercial associate Higher education Married House / apartment -13557.0 -586 1 1 1 0 Other 2 3500.0
4 5009752 F Y N 0 315000.0 Commercial associate Higher education Married House / apartment -13557.0 -586 1 1 1 0 Other 2 3500.0
6 5009754 F Y N 0 315000.0 Commercial associate Higher education Married House / apartment -13557.0 -586 1 1 1 0 Other 2 3500.0
7 5009894 F N N 0 180000.0 Pensioner Secondary / secondary special Married House / apartment -22134.0 365243 1 0 0 0 Other 2 2000.0
Convert Birthday_count column (given as a very large negative number) to age
Code
df_credit['Age'] = (df_credit['Birthday_count'] / -365.25).astype(int)
Convert employed days column to years employed
Code
# We use np.where(condition, value_if_true, value_if_false)
df_credit['Years_Employed'] = np.where(df_credit['Employed_days'] > 0, 0, df_credit['Employed_days'])
# We divide by -365.25. Since we already set the positive ones to 0, 
# 0 / -365.25 is still 0.
df_credit['Years_Employed'] = df_credit['Years_Employed'] / -365.25

Merge credit dataset with label dataset and group by approval/denial status

Code
merged_df = pd.merge(df_credit, df_label, on='Ind_ID')
Code
# This shows the average values for Approved (0) vs Denied (1)
summary = merged_df.groupby('label')[['Annual_income_USD', 'Age', 'Family_Members', 'Years_Employed']].mean()
print(summary)
       Annual_income_USD        Age  Family_Members  Years_Employed
label                                                              
0            2114.450037  43.240269        2.173653        6.317400
1            2163.750000  44.812500        2.093750        4.443001
See this is very unusual. For some reason the mean salary is higher in the rejected group. This seems to contradict common sense, which is that generally wealthier people are more often entrusted with credit lines because it’s less likely they will default or otherwise fail to pay back what they borrow. Let’s check the other measure of central tendency, the median, which is less sensitive to outliers and may reveal something about the nature of the distribution of wealth in this dataset.
Code
# This shows the median values for Approved (0) vs Denied (1)
summary = merged_df.groupby('label')[['Annual_income_USD', 'Age', 'Family_Members', 'Years_Employed']].median()
print(summary)
       Annual_income_USD   Age  Family_Members  Years_Employed
label                                                         
0                 1850.0  42.0             2.0        4.514716
1                 1750.0  45.0             2.0        2.524298
See this appears to make more sense. So in this case the wealth distribution for the denied is skewed right because some wealthy outliers are pushing the mean above the median and making it look like it is advantageous to have lower income in order to be approved for credit. Let’s see if we can confirm this suspicion by looking at the top incomes for both groups using SQL

Exploratory Analysis

First export the currently merged dataframe to a CSV file so I can create a database file with this information that I can then query from Jupyter Notebooks
Code
merged_df.to_csv('merged.csv', index=False)
Get SQL loaded
Code
from sqlalchemy import create_engine

# 1. Load the extension first
%reload_ext sql

# 2. Define your path
db_path = r'C:\Users\desja\Dropbox\Backup of Jumper Laptop files continuous\Documents\Project Revamp Portfolio\Credit Card Data\credit.db'

# 3. Create the engine (safest way to handle paths with spaces)
engine = create_engine(f"sqlite:///{db_path}")

# 4. Connect JupySQL to that engine
%sql engine
Explore the top earners in the rejected group
Code
%%sql 
SELECT Annual_Income_USD, Age
FROM merged
WHERE label = '1'
ORDER BY Annual_Income_USD DESC
Running query in 'sqlite:///C:\\Users\\desja\\Dropbox\\Backup of Jumper Laptop files continuous\\Documents\\Project Revamp Portfolio\\Credit Card Data\\credit.db'
Annual_income_USD Age
7500.0 42
6500.0 28
6500.0 28
6000.0 42
6000.0 54
5250.0 24
5000.0 49
5000.0 49
5000.0 49
5000.0 44
Truncated to displaylimit of 10.
Explore the top earners in the accepted group
Code
%%sql 
SELECT Annual_Income_USD, Age
FROM merged
WHERE label = '0'
ORDER BY Annual_Income_USD DESC
Running query in 'sqlite:///C:\\Users\\desja\\Dropbox\\Backup of Jumper Laptop files continuous\\Documents\\Project Revamp Portfolio\\Credit Card Data\\credit.db'
Annual_income_USD Age
17500.0 27
17500.0 27
10000.0 42
10000.0 27
10000.0 27
10000.0 46
9000.0 49
8750.0 47
7000.0 60
6800.0 34
Truncated to displaylimit of 10.
So at this point it still does not make sense to me how the accepted category has a lower average income than the rejected
Code
# Visualize the Distribution
plt.figure(figsize=(10,6))
sb.histplot(data=merged_df, x='Annual_income_USD', hue='label', bins=30, kde=True)
plt.title('Income Distribution by Label')
plt.show()

Code

# Check for Low-Income "Accepted" Applicants
# See if Label 0 has many people with very low income
print(merged_df[merged_df['label'] == 0]['Annual_income_USD'].describe())
count     1336.000000
mean      2114.450037
std       1253.458588
min        375.000000
25%       1350.000000
50%       1850.000000
75%       2500.000000
max      17500.000000
Name: Annual_income_USD, dtype: float64
Code
# Check for High-Income "Denied" Applicants
# See if Label 1 has many people with very high income
print(merged_df[merged_df['label'] == 1]['Annual_income_USD'].describe())
count     160.000000
mean     2163.750000
std      1290.212795
min       725.000000
25%      1250.000000
50%      1750.000000
75%      2500.000000
max      7500.000000
Name: Annual_income_USD, dtype: float64
Now that I have considered this carefully, it appears that a reasonable conclusion is that the vast majority of cases are approved for credit, and those that are rejected are mostly not rejected on the grounds that they have an income that is too low, since the accepted and rejected incomes are so similar, in two measures of central tendency, and it actually is evident that low-income earners are more often approved than high-income earners on average.
As we look back at the table in section “Merge credit dataset with label dataset and group by approval/denial status” it appears clear that the biggest difference between the approved and the rejected is not age (typically associated with maturity) and not income (typically associated with financial responsibility and good stewardship) but is years employed. This metric, which is more indicative of a person’s current financial stability, seems to be more important, based on this.
Code
# Visualize the Distribution
plt.figure(figsize=(10,6))
sb.histplot(data=merged_df, x='Years_Employed', hue='label', bins=30, kde=True)
plt.title('Years_Employed by Label')
plt.show()

This is interesting because it shows that indeed many people are accepted for credit even at 0 years of work experience but there is a general trend that is manifest in this graph that when people exceed 10 to 15 years of stable work it is very rare that they are ever rejected for credit. Let me confirm this with a SQL query, because it isn’t clear to me from the graph alone what exactly is happening in that range.
Code
%%sql
SELECT COUNT(*)
FROM merged
WHERE Years_Employed > 10 AND label = '1'
Running query in 'sqlite:///C:\\Users\\desja\\Dropbox\\Backup of Jumper Laptop files continuous\\Documents\\Project Revamp Portfolio\\Credit Card Data\\credit.db'
COUNT(*)
18
Code
merged_df['Years_Employed'] = merged_df['Years_Employed'].astype(float)
employed_long_term = merged_df[merged_df['Years_Employed'] > 10]
rejected_employed_long_term = employed_long_term[employed_long_term['label'] == 1]
percentage = (len(rejected_employed_long_term) / len(merged_df)) * 100
print(f"{percentage:.2f}% of applicants are rejected with greater than 10 years of consistent employment")
1.20% of applicants are rejected with greater than 10 years of consistent employment
This number is very low (1.2%), and so it confirms my observation that there is a general rule that stability is an important factor in credit decisions, up and above things like yearly income and age, which don’t appear to have the same impact.

Bayesian Probability Analysis

To get a better idea of how each variable affects the approval, I am going to try some Bayesian probability analysis to further understand this dataset. First I will create an approval column to clear up any confusion about label 0 being approved and label 1 being rejected.
Code
merged_df['is_approved'] = (merged_df['label'] == 0).astype(int)
Now I will set up my model for Years Employed, since we just reviewed that on a basic level.
Code
y_data = merged_df['is_approved'].values
x_data = merged_df['Years_Employed'].values

# Standardize x
x_mean = x_data.mean()
x_std = x_data.std()
x_scaled = (x_data - x_mean) / x_std

# The Model
with pm.Model() as approval_model:


    # --- A. THE PRIORS (What we think BEFORE seeing data) ---
    # We define variables 'alpha' and 'beta' as our knobs to turn.
    # We use a Normal distribution centered at 0 because we are skeptical.
    # We are saying: "Assume the base rate is average (0) and experience matters little (0),
    # unless the data proves otherwise."
    
    alpha = pm.Normal('alpha', mu=0, sigma=1)
    beta = pm.Normal('beta', mu=0, sigma=1)
    
    # --- B. THE MATH (Connecting the knobs to the data) ---
    # This is the Linear Equation: y = mx + b
    # 'mu' is the "Log-Odds" of approval. It can be any number (-100 to +100).
    mu = alpha + beta * x_scaled

    # This is the Link Function.
    # We wrap 'mu' in a Sigmoid function to force it to be a Probability (0 to 1).
    # pm.Deterministic just tells PyMC to save this variable so we can look at it later.
    
    p = pm.Deterministic('p', pm.math.sigmoid(mu))


    # --- C. THE LIKELIHOOD (Comparing to Reality) ---
    # This is where we tell PyMC:
    # "The observed data (y_data) follows a Bernoulli (Coin Flip) distribution."
    # "The chance of heads (p) is NOT fixed—it depends on the calculation above."
    
    observed = pm.Bernoulli('obs', p=p, observed=y_data)

    # --- D. THE INFERENCE (Pushing the Button) ---
    # PyMC now uses MCMC (Markov Chain Monte Carlo) to find the best values
    # for alpha and beta that make the data most likely.
    # 'idata_approval' is the "Result Container." It holds all the thousands of
    # simulation samples.
    
    idata_approval = pm.sample(1000, tune=1000, return_inferencedata=True)

print("Model finished! Now predicting Probability of APPROVAL.")
                                                                                                                   
  Progress                    Draws   Divergences   Step size   Grad evals   Sampling Speed   Elapsed   Remaining  
 ───────────────────────────────────────────────────────────────────────────────────────────────────────────────── 
  ━━━━━━━━━━━━━━━━━━━━━━━━━   2000    0             1.463       3            246.52 draws/s   0:00:08   0:00:00    
  ━━━━━━━━━━━━━━━━━━━━━━━━━   2000    0             1.454       3            158.54 draws/s   0:00:12   0:00:00    
                                                                                                                   

Model finished! Now predicting Probability of APPROVAL.
Ok so the model finished and stored the results in memory. Now I just need to visualize the results
Code
# 1. VISUALIZE THE PARAMETERS
# This checks: Did the model find a clear link? 
# If 'beta' is far from 0, then Experience matters.
az.plot_posterior(idata_approval, var_names=['alpha', 'beta'], ref_val=0)
plt.show()

# 2. PLOT THE PREDICTION CURVE
# We will ask the model to predict approval for 0 to 20 years of experience
years_range = np.linspace(0, 20, 100)

# Scale these new numbers exactly like we scaled the training data
x_mean = merged_df['Years_Employed'].mean()
x_std = merged_df['Years_Employed'].std()
years_scaled = (years_range - x_mean) / x_std

# Extract the learned Alpha and Beta values (the "Posteriors")
posterior_alpha = idata_approval.posterior['alpha'].values.mean()
posterior_beta = idata_approval.posterior['beta'].values.mean()

# Calculate the curve: Sigmoid(alpha + beta * x)
def sigmoid(x):
    return 1 / (1 + np.exp(-x))

predicted_probs = sigmoid(posterior_alpha + posterior_beta * years_scaled)

# Plot it!
plt.figure(figsize=(10, 6))
plt.plot(years_range, predicted_probs, color='blue', linewidth=3, label='Bayesian Model')
plt.scatter(merged_df['Years_Employed'], merged_df['is_approved'], 
            alpha=0.1, color='gray', label='Actual Data (0=Rej, 1=Appr)')

plt.title('Probability of Approval vs. Years Employed')
plt.xlabel('Years of Employment')
plt.ylabel('Probability of Approval (0% to 100%)')
plt.grid(True, alpha=0.3)
plt.legend()
plt.show()

So the beta curve is centered around 0.36 and the entire area of the curve is above x = 0, which means that there is 94% certainty that having more experience increases one’s chance of being approved for credit. (There is a default 94% high density interval in these python libraries, chosen perhaps to indicate that the exact percentage is arbitrary, but it is enough to show great confidence in the conclusion).
The alpha curve is the average person’s chance of getting approved, and since the logit is 2.2 this translates to approximately a 90% probability of being approved, which suggests a very generous bank that mostly approves anyone with an average income.
The blue curve shows that even beginner workers are often (greater than 80% of the time) approved. However the curve slopes upward significantly, approaching 100% by 20 years of consistent work history.
Now I want to test all of the variables to see their impact on approval. I’m going to use another Bayesian model to do this, but first I need to convert a lot of these letter inputs into numbers so the model doesn’t break.
Code
# 1. Convert Text to Numbers (Feature Engineering)
# We map "Y" to 1 and "N" to 0, etc.
merged_df['Gender_Code'] = merged_df['GENDER'].map({'M': 1, 'F': 0})
merged_df['Car_Owner_Code'] = merged_df['Car_Owner'].map({'Y': 1, 'N': 0})
merged_df['Prop_Owner_Code'] = merged_df['Propert_Owner'].map({'Y': 1, 'N': 0})

# 2. Select the variables to include
features = [
    'Annual_income_USD', 
    'Age', 
    'Family_Members', 
    'Years_Employed',
    'CHILDREN',           # Number of children
    'Gender_Code',        # 1=Male, 0=Female
    'Car_Owner_Code',     # 1=Owns Car
    'Prop_Owner_Code',    # 1=Owns Property
    'Mobile_phone',       # 1=Has Mobile
    'Work_Phone',         # 1=Has Work Phone
    'Phone',              # 1=Has Landline
    'EMAIL_ID'            # 1=Has Email
]

# 3. Create the Matrix (Drop missing rows to be safe)
data_clean = merged_df.dropna(subset=features + ['is_approved']).copy()
X_data = data_clean[features].values
y_data = data_clean['is_approved'].values

# 4. STANDARDIZE (Crucial!)
# If we don't do this, Income (10,000) will overpower Years Employed (9).
X_mean = X_data.mean(axis=0)
X_std = X_data.std(axis=0)

# Safety check: If a column is all the same (e.g., everyone has a mobile), 
# std will be 0. We set it to 1 to avoid dividing by zero.
X_std[X_std == 0] = 1 

X_scaled = (X_data - X_mean) / X_std

# Define the coordinates: "These are the names of my predictors"
coords = {"predictor": features}

print(f"Running model with named dimensions: {features}")

print(f"Ready to test {len(features)} variables on {len(data_clean)} applicants.")
Running model with named dimensions: ['Annual_income_USD', 'Age', 'Family_Members', 'Years_Employed', 'CHILDREN', 'Gender_Code', 'Car_Owner_Code', 'Prop_Owner_Code', 'Mobile_phone', 'Work_Phone', 'Phone', 'EMAIL_ID']
Ready to test 12 variables on 1496 applicants.
Code
# We pass 'coords' to the model so it knows the names
with pm.Model(coords=coords) as named_model:
    
    # Intercept
    alpha = pm.Normal('intercept', mu=0, sigma=1)
    
    # Slopes: instead of 'shape=', we use 'dims='
    # This tells PyMC: "Create one beta for every name in the 'predictor' list"
    betas = pm.Normal('betas', mu=0, sigma=1, dims='predictor')
    
    # The Math
    mu = alpha + pm.math.dot(X_scaled, betas)
    
    # Probability & Likelihood
    p = pm.Deterministic('p', pm.math.sigmoid(mu))
    observed = pm.Bernoulli('obs', p=p, observed=y_data)
    
    # Sampling
    idata_named = pm.sample(1000, tune=1000, return_inferencedata=True)

print("Model finished! Ready to plot.")
                                                                                                                   
  Progress                    Draws   Divergences   Step size   Grad evals   Sampling Speed   Elapsed   Remaining  
 ───────────────────────────────────────────────────────────────────────────────────────────────────────────────── 
  ━━━━━━━━━━━━━━━━━━━━━━━━━   2000    0             0.363       15           161.34 draws/s   0:00:12   0:00:00    
  ━━━━━━━━━━━━━━━━━━━━━━━━━   2000    0             0.361       7            75.48 draws/s    0:00:26   0:00:00    
                                                                                                                   

Model finished! Ready to plot.
Code
# 1. Generate the basic plot and CAPTURE the "axes" object
# We save the plot into a variable 'axes' instead of just showing it
plt.figure(figsize=(10, 12))
axes = az.plot_forest(idata_named, 
                      var_names=['betas'], 
                      combined=True, 
                      figsize=(10, 12),
                      textsize=14,
                      colors='navy') # Changed to 'navy' for better contrast

# 2. The Customization
# We grab the first (and only) chart from the axes array
ax = axes[0]

# We loop through every single line drawn on the chart
for line in ax.get_lines():
    width = line.get_linewidth()
    
    # logic: The "Thick Bar" (HDI) is the widest line.
    # The "Whiskers" and "ticks" are the thinner lines.
    
    if width > 1.5:  
        # CASE 1: The HDI (Signal) -> make it more distinctive
        line.set_linewidth(7)       # Much thicker (was ~2-3)
        line.set_alpha(1.0)         # Fully opaque
        line.set_capstyle('round')  # Rounded edges look nicer
        
    else:
        # CASE 2: The Whiskers (Noise) -> FADE THEM OUT
        line.set_linewidth(1)       # Keep thin
        line.set_color('gray')      # Turn them gray
        line.set_alpha(0.5)         # Make them semi-transparent

# 3. Add our standard decorations
plt.axvline(x=0, color='red', linestyle='--', linewidth=2, label="Meaningless (0)")
plt.title("Drivers of Credit Approval (High Contrast)", fontsize=18, pad=20)
plt.xlabel("Impact on Approval (Left=Bad, Right=Good)", fontsize=14)
plt.legend()
plt.grid(True, axis='x', alpha=0.3)

plt.show()
<Figure size 1000x1200 with 0 Axes>

From the Bayesian analysis here we can conclude that meaningful contributions to the decision making process (based on this dataset for this particular bank in India) are age (the older one gets the less likely they are to be approved for credit), Gender (females are more likely to get approved than males), car ownership (owning a car is a favorable factor in the decision to award credit), property ownership (this is borderline but appears to contribute positively to the decision), and the most significant, years employed, which positively contributes with increasing years employed.