Case Study: German Credit Risk Classification
German Credit Data Set, serving as the foundation for a credit risk classification case study aligned with **CRISP-DM to
clearly define the problem, specify business objectives, and understand the cost structure.
Data Source and Goal
[Link]
The data was collected by a German bank and provided by Professor Dr. Hans Hofmann. It classifies loan applicants as
either Good Credit Risk or Bad Credit Risk.
Business Objective
The primary business objective is to minimize financial loss due to loan defaults by accurately identifying applicants
who are likely to default (Bad Credit Risk).
Target Variable (Business Outcome)
Business Term Data Value Count Proportion
Good Credit Risk (Loan Approved - Repaid) 1 700 70%
Bad Credit Risk (Loan Approved - Defaulted) 2 300 30%
Note: The dataset is inherently imbalanced, with a 70:30 ratio of good to bad credit risks.
Business Cost Matrix (Misclassification Penalties)
In credit risk, not all errors are equally costly. Approving a bad applicant (False Positive) is far more damaging than
rejecting a good applicant (False Negative). The original dataset requires the use of a Cost Matrix
Actual \ Predicted 1 (Predicted Good) (Approve Loan) 2 (Predicted Bad) (Reject Loan)
1 (Actual Good) (Profitable) 0 (Correct) 1 (Lost Opportunity - Cost: 1)
2 (Actual Bad) (Default Risk) 5 (Loan Default - Cost: 5) 0 (Correct)
Key Business Insight from Cost Matrix
It is 5 times worse to classify a customer as Good when they are Bad (i.e., approving a defaulter), than it
is to classify a customer as Bad when they are Good (i.e., mistakenly rejecting a profitable customer).
Therefore, the model must be optimized to minimize False Positives (misclassifying actual bad risk as
good).
In this business scenario, Recall for the "Bad" class is critically important. Missing a bad credit risk
(False Negative) is the most expensive mistake we can make.
Data Set Structure
Metric Detail
Total Instances 1000
Total Attributes 20
Attribute Mix 7 Numerical, 13 Categorical
Missing Values None ('No missing values')
Numerical Features (7 Attributes)
These attributes represent quantitative measures and require scaling (e.g., Standardization) during preprocessing to
prevent features with large magnitudes from dominating the model.
Attribute Description Units/Scale
A2 Duration in month Months
A5 Credit amount DM (Deutsche Mark)
A8 Installment rate in % of disposable income Percentage
A11 Present residence since Years
A13 Age in years Years
A16 Number of existing credits at this bank Count
A18 Number of people being liable to provide maintenance for Count
Categorical Features (13 Attributes)
These attributes represent symbolic categories and MUST be One-Hot Encoded (OHE) for use in an Artificial Neural
Network (ANN).
Attribute Description Example Categories
A1 Status of existing checking account A11 ($\lt$ 0 DM), A14 (no checking account)
A3 Credit history A30 (no/paid duly), A34 (critical account)
A4 Purpose A40 (car new), A43 (radio/TV), A49 (business)
A6 Savings account/bonds A61 ($\lt$ 100 DM), A65 (unknown/no savings)
A7 Present employment since ]A71 (unemployed), A75 ($\ge$ 7 years)
A9 Personal status and sex A92 (female), A93 (male: single)
A10 Other debtors / guarantors A101 (none), A103 (guarantor)
A12 Property A121 (real estate), A124 (unknown/no property)
A14 Other installment plans ]A141 (bank), A143 (none)
A15 Housing A151 (rent), A152 (own)
A17 Job A171 (unskilled/unemployed), A173 (skilled employee)
A19 Telephone A191 (none), A192 (yes)
A20 Foreign worker A201 (yes), A202 (no)
Classification Basics
Definition: A supervised learning task where the goal is to predict a discrete class label for a given input. It is like
teaching a child to sort fruits into baskets (Apples, Oranges, Bananas).
Types of Classification:
Binary Classification: Two possible outcomes.
Example: Loan vs. No Loan, Fraudulent vs. Legitimate, Good Credit Vs Bad Credit.
Output layer uses 1 neuron with sigmoid activation (outputs a probability between 0 and 1).
Multi-Class Classification: More than two classes; each sample belongs to exactly one class.
Example: Digit recognition (0-9), E-commerce, Customer service - product category classification.
Output layer uses n neurons (where n = number of classes) with softmax activation (outputs a
probability distribution across all classes).
Multi-Label Classification: More than two classes; each sample can belong to multiple classes simultaneously.
Example: Image tagging (a photo can contain a "beach," "sunset," and "dog") as in Google Lens model.
Output layer uses n neurons with sigmoid activation (each outputs an independent probability).
Classification in a Business Setting
Definition: A process that uses data to predict a categorical outcome that directly impacts business operations, risk,
and profitability.
In this case study output is the probability that the applicant is a "Good" credit risk.
Classification Performance Measures
The Confusion Matrix
A table used to describe the performance of a classification model on a set of test data for which the true values are
known.
For Binary Classification:
Actual \ Predicted Predicted Positive Predicted Negative
Actual Positive True Positive (TP) False Negative (FN)
Actual Negative False Positive (FP) True Negative (TN)
Key Terms:
True Positive (TP): You predicted positive, and it's true.
True Negative (TN): You predicted negative, and it's true.
False Positive (FP): You predicted positive, and it's false. (Type I Error)
False Negative (FN): You predicted negative, and it's false. (Type II Error)
Core Metrics: Formulas & Interpretation
Accuracy
The proportion of total predictions that were correct.
Use when your classes are well-balanced.
Formula: Accuracy = (TP + TN) / (TP + TN + FP + FN)
Precision
The proportion of positive predictions that were actually correct. "How precise are our positive predictions?"
Use when the cost of False Positives (FP) is high (e.g., spam detection, recommending unsafe videos).
Formula: Precision = TP / (TP + FP)
Recall (Sensitivity, True Positive Rate - TPR)
The proportion of actual positives that we correctly identified. "How many of the true positives did we catch?"
Use when the cost of False Negatives (FN) is high (e.g., disease screening, fraud detection).
Formula: Recall = TP / (TP + FN)
F1-Score
The harmonic mean of Precision and Recall. A single score that balances both concerns.
Use when you need a single metric to compare models and you have an imbalanced dataset.
Formula: F1-Score = 2 * (Precision * Recall) / (Precision + Recall)
Trade-off: Precision and Recall are often in a trade-off. Increasing one typically decreases the other.
For the case study
"Bad" as Positive (Risk Management View) is taken
Positive Class = "Bad" Credit (the risky ones we need to catch)
Negative Class = "Good" Credit (the safe ones)
Actual \ Predicted Predicted Bad (Reject) Predicted Good (Approve)
True Positive (TP) False Negative (FN)
Actual Bad (Positive) Correctly rejected a bad loan Wrongly approved a bad loan
Cost: 0 Cost: 5
False Positive (FP) True Negative (TN)
Actual Good (Negative) Wrongly rejected a good loan Correctly approved a good loan
Cost: 1 Cost: 0
**Why Recall is Critically Important:**
Recall (for "Bad" as Positive) = TP / (TP + FN)
Recall measures: "Of all the actual bad credit risks, what percentage did we correctly identify and reject?"
High Recall means we're catching most of the dangerous applicants
Low Recall means we're missing bad credit risks and incurring the $5 cost repeatedly
ROC Curve & AUC
ROC Curve (Receiver Operating Characteristic)
A plot that shows the trade-off between True Positive Rate (Recall) and False Positive Rate (FPR) at various
classification thresholds.
False Positive Rate (FPR): FPR = FP / (FP + TN)
The curve is created by varying the decision threshold from 0 to 1 and plotting TPR vs. FPR at each point.
A random classifier would lie along the diagonal line (dashed line).
AUC (Area Under the ROC Curve)
A single number that summarizes the ROC curve's performance.
Interpretation:
AUC = 1.0: Perfect classifier.
AUC = 0.5: No better than random guessing (the diagonal line).
AUC > 0.8: Generally considered a good model.
The probability that the model ranks a random positive example more highly than a random negative example. It
measures the model's ranking power, not its calibrated probability.
Other Important Measures
Specificity (True Negative Rate): TNR = TN / (TN + FP) = 1 - FPR
Log Loss (Cross-Entropy Loss): Measures the uncertainty of your probabilities by comparing them to the true
labels. A lower log loss is better. This is often the direct loss function used in training ANNs for classification.
For this case study
If we only maximize recall, we could create a disastrous business model:
Extreme Example: The "Reject Everyone" Model
# This model has perfect recall for the "Bad" class
def reject_everyone_model(X):
return [Link](len(X)) # Predict "Bad" for everyone
# Results:
# - Recall for Bad class = 1.0 (perfect! catches all bad customers)
# - Precision for Bad class = 0.05 (terrible! only 5% of rejections are correct)
# - Business outcome: BANKRUPTCY (we reject 95% of good customers)
A model can achieve high recall by being overly paranoid and rejecting too many good customers.
ROC-AUC as the "Ranking Quality" Metric
"What's the probability that a randomly chosen Bad customer gets a higher risk score than a randomly chosen
Good customer?"
AUC = 1.0: Perfect ranking - all Bad customers get higher risk scores than all Good customers
AUC = 0.8: Good ranking - 80% of the time, Bad customers get higher risk scores than Good customers
AUC = 0.5: Random guessing - no ranking ability
ROC-AUC tells us how well the model distinguishes between truly risky vs. safe customers, regardless of our
chosen threshold.
How Recall and ROC-AUC Work Together
Recall Answers: "Can we catch enough bad customers?" ROC-AUC Answers: "How well does our model separate
good from bad customers?"
The Ideal Scenario:
High ROC-AUC (>0.8): Model has strong inherent ability to distinguish risks
High Recall (>0.8): We've set our threshold to catch most bad customers
Sample Scenarios for better understanding
ROC-
Scenario Recall Business Interpretation
AUC
High Model has poor ranking ability; achieving high recall by being overly conservative and
Danger Low (0.6)
(0.9) rejecting many good customers
High Low Model can distinguish risks well, but we're using the wrong threshold and missing too
Ineffective
(0.85) (0.5) many bad customers
High High
Excellent Model has strong ranking ability AND we're catching most bad customers
(0.85) (0.9)
The Problem of Imbalanced Classes
Scenario: When one class significantly outnumbers the other(s).
Example: 99% "Not Fraud" and 1% "Fraud" transactions.
Why is it a problem?
A naive model that always predicts the majority class can achieve very high accuracy but is useless.
The model becomes biased towards the majority class and fails to learn the patterns of the minority class.
Handling Imbalanced Data
1. Resampling Techniques
Oversampling: Increase the number of minority class samples.
Method: Duplicate existing samples or use SMOTE (Synthetic Minority Over-sampling Technique) to create
synthetic samples.
Undersampling: Decrease the number of majority class samples.
Risk: You might lose important information from the majority class.
2. Algorithm-Level Techniques
Use Appropriate Metrics: Stop using Accuracy! Use Precision, Recall, F1-Score, and AUC instead.
Adjust Class Weights: Tell the model to "pay more attention" to the minority class.
In sklearn and [Link] , you can set the class_weight parameter to 'balanced' or a
custom dictionary.
3. Data-Level Techniques
Collect more data for the minority class, if possible.
4. Anomaly Detection
Frame the problem as anomaly detection, where the minority class is treated as an "anomaly."
Other Salient Points to consider for ANN (Deep Learning)
Classification
Data Preprocessing
Feature Scaling: Crucial for ANNs. Use Standardization ( StandardScaler ) or Normalization ( MinMaxScaler )
to ensure stable and fast convergence.
Categorical Encoding: For input features, use one-hot encoding or embedding layers.
Model Architecture & Training
Output Layer Activation:
Binary: sigmoid
Multi-Class: softmax
Loss Function:
Binary: binary_crossentropy
Multi-Class: categorical_crossentropy (one-hot encoded labels) or
sparse_categorical_crossentropy (integer labels)
Overfitting:
Use L1/L2 Regularization in your Dense layers.
Use Dropout layers to randomly turn off neurons during training.
Use Early Stopping to halt training when validation performance stops improving.
Threshold Tuning
The default threshold for classification is 0.5. You can adjust this threshold to favor Precision or Recall based on
your business problem.
Use the ROC curve or Precision-Recall curve to select an optimal threshold.
Evaluation Best Practices
Always use a stratified train-test split to preserve the class distribution in your splits.
Use K-Fold Cross-Validation (with stratification) for a more robust evaluation, especially on small datasets.
Businesss and Data Understanding
The business and Data Undestanding has been defined earlier.
In [1]: import pandas as pd
import numpy as np
import seaborn as sns
import [Link] as plt
from sklearn.model_selection import train_test_split
from [Link] import StandardScaler
from sklearn.linear_model import LogisticRegression
from [Link] import accuracy_score, confusion_matrix, classification_report, roc_curve,roc_auc_sco
In [2]: import tensorflow as tf
import keras
from [Link] .models import Sequential
from [Link] import Dense
from keras import Input
from [Link] import load_model
Data Preparation
In [3]: # Data Pre-processing
column_names = [f'A{i}' for i in range(1, 21)] + ['Risk']
# Load data from the provided content or URL
data = pd.read_csv('[Link]', sep=' ', header=None)
[Link] = column_names
X = [Link]('Risk', axis=1)
# Target: 0 (Good Risk), 1 (Bad Risk)
y = data['Risk'].apply(lambda x: 0 if x == 1 else 1)
# Identify feature types
categorical_cols = [f'A{i}' for i in [1, 3, 4, 6, 7, 9, 10, 12, 14, 15, 17, 19, 20]]
numerical_cols = [col for col in [Link] if col not in categorical_cols]
# Preprocessing: Convert all data to numerical format
X_cat = pd.get_dummies(X[categorical_cols], drop_first=True)
scaler = StandardScaler()
X_num_scaled = scaler.fit_transform(X[numerical_cols])
X_num_scaled = [Link](X_num_scaled, columns=numerical_cols)
X_processed = [Link]([X_num_scaled, X_cat], axis=1)
input_dim = X_processed.shape[1]
# Split the data
X_train, X_test, y_train, y_test = train_test_split(
X_processed, y, test_size=0.2, random_state=42, stratify=y
)
In [4]: len(X_train)
Out[4]: 800
In [5]: len(X_test)
Out[5]: 200
Modelling
Baseline Model - Logistic Regression
In [6]: # Initialize and Train Logistic Regression
log_model = LogisticRegression(solver='liblinear', random_state=42)
log_model.fit(X_train, y_train)
# Predict probabilities and calculate AUC
log_pred_proba = log_model.predict_proba(X_test)[:, 1]
log_auc = roc_auc_score(y_test, log_pred_proba)
log_accuracy = log_model.score(X_test, y_test)
print("2. Baseline Model Performance (Logistic Regression) ")
print(f"Logistic Regression Accuracy: {log_accuracy:.4f}")
print(f"Logistic Regression ROC-AUC: {log_auc:.4f}")
--- 2. Baseline Model Performance (Logistic Regression) ---
Logistic Regression Accuracy: 0.7850
Logistic Regression ROC-AUC: 0.8043
Artificial Neural Network Model
Defining the Neural Network Architecture : Sequential model
The sequential model is the simplest way to build a neural network using keras. The addition of layers one after the
other in sequence defines the neural network architecture.
In [7]: # Initialize the Sequential model
ann_model = Sequential(name="Simple_Credit_ANN")
ann_model.add([Link](shape=(input_dim,)))
# Layer 1: Input Layer (and 1st Hidden Layer)
ann_model.add(Dense(units=32, activation='relu', name = "Hidden_layer"))
# Layer 2: Output Layer
ann_model.add(Dense(units=1, activation='sigmoid', name = "Output_layer"))
print(" ANN Model Architecture ")
ann_model.summary()
--- ANN Model Architecture ---
Model: "Simple_Credit_ANN"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┓
┃ Layer (type) ┃ Output Shape ┃ Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━┩
│ Hidden_layer (Dense) │ (None, 32) │ 1,568 │
├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ Output_layer (Dense) │ (None, 1) │ 33 │
└──────────────────────────────────────┴─────────────────────────────┴─────────────────┘
Total params: 1,601 (6.25 KB)
Trainable params: 1,601 (6.25 KB)
Non-trainable params: 0 (0.00 B)
Observe the core building block of neural network model which is the "layer", a data-processing module. The data is
being "filtered" so as to pass on next data which is useful for further layers.
Precisely, layers extract representations out of the data fed into them -- hopefully representations that are more
meaningful for the problem at hand. So usually deep learning models consists of chaining together simple layers which
will implement a form of progressive "data distillation".
Two Dense layers, which are densely-connected (also called "fully-connected") neural layers. The second (and last)
layer is a 1 neuron "sigmoid" layer, which implies that it will return probability scores ( the score from zero to
probabilities being 1). Score will be the probability that the current risk is bad risk (1) or good risk(0).
Before training, few more things to be provided:
A loss function: is the way in which deep learning model measures the performance on its training data, and thus
getting feedback to tweak the learning in right direction.
An optimizer: this is the mechanism through which the deep learning model will update itself based on the data it
"sees" and its loss function.
Metrics to monitor during training and testing.
In this model we would use accuracy and AUC to montior first.
Compile and Fit the model
In [8]: # Compile the model
ann_model.compile(
optimizer='adam',
loss='binary_crossentropy',
metrics=['accuracy', [Link](name='auc')]
)
print("\n--- ANN Training (The Deep Learning Cycle) ---")
# Train the model
history = ann_model.fit(
X_train, y_train,
epochs=50,
batch_size=32,
validation_split=0.1, # Use 10% of training data for validation tracking - Net training samples = 800
verbose=1 # Set to 1 to show the epoch-by-epoch learning process
)
# Evaluate the model
ann_loss, ann_accuracy, ann_auc = ann_model.evaluate(X_test, y_test, verbose=0)
--- ANN Training (The Deep Learning Cycle) ---
Epoch 1/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 5s 52ms/step - accuracy: 0.6708 - auc: 0.5495 - loss: 0.6281 - val_accuracy: 0.6
875 - val_auc: 0.6422 - val_loss: 0.6000
Epoch 2/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 14ms/step - accuracy: 0.7028 - auc: 0.6230 - loss: 0.5890 - val_accuracy: 0.6
875 - val_auc: 0.7131 - val_loss: 0.5844
Epoch 3/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 1s 22ms/step - accuracy: 0.7028 - auc: 0.6941 - loss: 0.5657 - val_accuracy: 0.7
000 - val_auc: 0.7284 - val_loss: 0.5700
Epoch 4/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 15ms/step - accuracy: 0.7125 - auc: 0.7324 - loss: 0.5485 - val_accuracy: 0.7
125 - val_auc: 0.7429 - val_loss: 0.5550
Epoch 5/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 14ms/step - accuracy: 0.7194 - auc: 0.7604 - loss: 0.5321 - val_accuracy: 0.7
125 - val_auc: 0.7531 - val_loss: 0.5477
Epoch 6/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 14ms/step - accuracy: 0.7194 - auc: 0.7803 - loss: 0.5191 - val_accuracy: 0.7
125 - val_auc: 0.7589 - val_loss: 0.5413
Epoch 7/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 15ms/step - accuracy: 0.7403 - auc: 0.7913 - loss: 0.5076 - val_accuracy: 0.7
125 - val_auc: 0.7636 - val_loss: 0.5316
Epoch 8/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 12ms/step - accuracy: 0.7444 - auc: 0.7997 - loss: 0.4980 - val_accuracy: 0.7
375 - val_auc: 0.7676 - val_loss: 0.5273
Epoch 9/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 12ms/step - accuracy: 0.7431 - auc: 0.8085 - loss: 0.4893 - val_accuracy: 0.7
375 - val_auc: 0.7655 - val_loss: 0.5282
Epoch 10/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 11ms/step - accuracy: 0.7514 - auc: 0.8171 - loss: 0.4802 - val_accuracy: 0.7
500 - val_auc: 0.7640 - val_loss: 0.5235
Epoch 11/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 12ms/step - accuracy: 0.7583 - auc: 0.8215 - loss: 0.4736 - val_accuracy: 0.7
500 - val_auc: 0.7611 - val_loss: 0.5216
Epoch 12/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 13ms/step - accuracy: 0.7625 - auc: 0.8284 - loss: 0.4664 - val_accuracy: 0.7
625 - val_auc: 0.7593 - val_loss: 0.5253
Epoch 13/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 12ms/step - accuracy: 0.7694 - auc: 0.8335 - loss: 0.4605 - val_accuracy: 0.7
625 - val_auc: 0.7615 - val_loss: 0.5231
Epoch 14/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 14ms/step - accuracy: 0.7667 - auc: 0.8368 - loss: 0.4553 - val_accuracy: 0.7
750 - val_auc: 0.7604 - val_loss: 0.5201
Epoch 15/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 12ms/step - accuracy: 0.7708 - auc: 0.8414 - loss: 0.4499 - val_accuracy: 0.7
750 - val_auc: 0.7644 - val_loss: 0.5191
Epoch 16/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 12ms/step - accuracy: 0.7833 - auc: 0.8451 - loss: 0.4455 - val_accuracy: 0.7
750 - val_auc: 0.7582 - val_loss: 0.5217
Epoch 17/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 1s 24ms/step - accuracy: 0.7764 - auc: 0.8503 - loss: 0.4398 - val_accuracy: 0.7
625 - val_auc: 0.7604 - val_loss: 0.5212
Epoch 18/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 16ms/step - accuracy: 0.7819 - auc: 0.8537 - loss: 0.4358 - val_accuracy: 0.7
250 - val_auc: 0.7651 - val_loss: 0.5182
Epoch 19/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 1s 23ms/step - accuracy: 0.7944 - auc: 0.8574 - loss: 0.4309 - val_accuracy: 0.7
375 - val_auc: 0.7662 - val_loss: 0.5176
Epoch 20/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 15ms/step - accuracy: 0.7889 - auc: 0.8607 - loss: 0.4270 - val_accuracy: 0.7
375 - val_auc: 0.7647 - val_loss: 0.5245
Epoch 21/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 16ms/step - accuracy: 0.7861 - auc: 0.8623 - loss: 0.4235 - val_accuracy: 0.7
125 - val_auc: 0.7596 - val_loss: 0.5204
Epoch 22/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 16ms/step - accuracy: 0.7944 - auc: 0.8669 - loss: 0.4186 - val_accuracy: 0.7
125 - val_auc: 0.7636 - val_loss: 0.5233
Epoch 23/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 1s 20ms/step - accuracy: 0.8028 - auc: 0.8702 - loss: 0.4145 - val_accuracy: 0.7
000 - val_auc: 0.7662 - val_loss: 0.5218
Epoch 24/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 12ms/step - accuracy: 0.8125 - auc: 0.8730 - loss: 0.4106 - val_accuracy: 0.7
000 - val_auc: 0.7665 - val_loss: 0.5216
Epoch 25/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 15ms/step - accuracy: 0.8125 - auc: 0.8762 - loss: 0.4066 - val_accuracy: 0.7
000 - val_auc: 0.7665 - val_loss: 0.5253
Epoch 26/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 14ms/step - accuracy: 0.8083 - auc: 0.8789 - loss: 0.4034 - val_accuracy: 0.7
250 - val_auc: 0.7633 - val_loss: 0.5233
Epoch 27/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 12ms/step - accuracy: 0.8181 - auc: 0.8819 - loss: 0.3995 - val_accuracy: 0.7
250 - val_auc: 0.7669 - val_loss: 0.5216
Epoch 28/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 16ms/step - accuracy: 0.8194 - auc: 0.8836 - loss: 0.3963 - val_accuracy: 0.7
375 - val_auc: 0.7687 - val_loss: 0.5203
Epoch 29/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 17ms/step - accuracy: 0.8236 - auc: 0.8868 - loss: 0.3927 - val_accuracy: 0.7
250 - val_auc: 0.7658 - val_loss: 0.5226
Epoch 30/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - accuracy: 0.8319 - auc: 0.8903 - loss: 0.3886 - val_accuracy: 0.7
375 - val_auc: 0.7676 - val_loss: 0.5218
Epoch 31/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 16ms/step - accuracy: 0.8319 - auc: 0.8916 - loss: 0.3859 - val_accuracy: 0.7
375 - val_auc: 0.7673 - val_loss: 0.5216
Epoch 32/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 18ms/step - accuracy: 0.8292 - auc: 0.8942 - loss: 0.3820 - val_accuracy: 0.7
375 - val_auc: 0.7644 - val_loss: 0.5269
Epoch 33/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 1s 23ms/step - accuracy: 0.8347 - auc: 0.8956 - loss: 0.3805 - val_accuracy: 0.7
500 - val_auc: 0.7680 - val_loss: 0.5181
Epoch 34/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 15ms/step - accuracy: 0.8431 - auc: 0.8995 - loss: 0.3761 - val_accuracy: 0.7
375 - val_auc: 0.7687 - val_loss: 0.5220
Epoch 35/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 16ms/step - accuracy: 0.8389 - auc: 0.9007 - loss: 0.3727 - val_accuracy: 0.7
500 - val_auc: 0.7647 - val_loss: 0.5232
Epoch 36/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 1s 20ms/step - accuracy: 0.8486 - auc: 0.9020 - loss: 0.3699 - val_accuracy: 0.7
375 - val_auc: 0.7695 - val_loss: 0.5249
Epoch 37/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 16ms/step - accuracy: 0.8472 - auc: 0.9055 - loss: 0.3659 - val_accuracy: 0.7
500 - val_auc: 0.7695 - val_loss: 0.5232
Epoch 38/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 12ms/step - accuracy: 0.8514 - auc: 0.9080 - loss: 0.3624 - val_accuracy: 0.7
500 - val_auc: 0.7687 - val_loss: 0.5243
Epoch 39/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 13ms/step - accuracy: 0.8458 - auc: 0.9101 - loss: 0.3596 - val_accuracy: 0.7
375 - val_auc: 0.7698 - val_loss: 0.5263
Epoch 40/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 17ms/step - accuracy: 0.8472 - auc: 0.9111 - loss: 0.3568 - val_accuracy: 0.7
500 - val_auc: 0.7684 - val_loss: 0.5314
Epoch 41/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 12ms/step - accuracy: 0.8514 - auc: 0.9139 - loss: 0.3525 - val_accuracy: 0.7
500 - val_auc: 0.7698 - val_loss: 0.5282
Epoch 42/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 14ms/step - accuracy: 0.8597 - auc: 0.9154 - loss: 0.3503 - val_accuracy: 0.7
500 - val_auc: 0.7742 - val_loss: 0.5267
Epoch 43/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 14ms/step - accuracy: 0.8569 - auc: 0.9174 - loss: 0.3473 - val_accuracy: 0.7
500 - val_auc: 0.7702 - val_loss: 0.5304
Epoch 44/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - accuracy: 0.8569 - auc: 0.9204 - loss: 0.3439 - val_accuracy: 0.7
500 - val_auc: 0.7698 - val_loss: 0.5310
Epoch 45/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 12ms/step - accuracy: 0.8625 - auc: 0.9218 - loss: 0.3409 - val_accuracy: 0.7
250 - val_auc: 0.7651 - val_loss: 0.5341
Epoch 46/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 11ms/step - accuracy: 0.8639 - auc: 0.9233 - loss: 0.3381 - val_accuracy: 0.7
250 - val_auc: 0.7669 - val_loss: 0.5310
Epoch 47/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 11ms/step - accuracy: 0.8597 - auc: 0.9260 - loss: 0.3344 - val_accuracy: 0.7
500 - val_auc: 0.7684 - val_loss: 0.5340
Epoch 48/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step - accuracy: 0.8667 - auc: 0.9272 - loss: 0.3319 - val_accuracy: 0.7
250 - val_auc: 0.7695 - val_loss: 0.5328
Epoch 49/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 12ms/step - accuracy: 0.8708 - auc: 0.9293 - loss: 0.3289 - val_accuracy: 0.7
250 - val_auc: 0.7676 - val_loss: 0.5320
Epoch 50/50
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 9ms/step - accuracy: 0.8736 - auc: 0.9312 - loss: 0.3254 - val_accuracy: 0.72
50 - val_auc: 0.7662 - val_loss: 0.5390
# You can try to use below the RMSprop optimizer with learning rate
[Link](optimizer=[Link](1e-8),
loss='binary_crossentropy',
metrics=['accuracy', [Link](name='auc')])
steps_per_epoch = ceil(720/32) = 23. Batch_size default = 32, alternative parameter is steps_per_epoch in fit method.
Either batch_size or steps_per_epoch can be used
In [9]: # Results for comparison
comparison_results = [Link]({
'Model': ['Logistic Regression (Baseline)', 'Simple ANN (Deep Learning)'],
'Accuracy':[log_accuracy, ann_accuracy],
'ROC-AUC Score': [log_auc, ann_auc]
})
print("\n Model Comparison: DL vs. Traditional ML")
print(comparison_results)
--- Model Comparison: DL vs. Traditional ML ---
Model Accuracy ROC-AUC Score
0 Logistic Regression (Baseline) 0.785 0.804286
1 Simple ANN (Deep Learning) 0.780 0.804226
Logistic Regression performed better. In this case, the complexity of an ANN wasn't justified. More layers, More
units/neurons or Regularization would be needed to unlock the full potential of deep learning.
In [10]: from [Link] import EarlyStopping
In [11]: # Initialize the Sequential model
ann_deep_model = Sequential(name="Deeper_Credit_ANN")
ann_deep_model.add([Link]( shape = (input_dim,)))
# Layer 1: Hidden Layer (64 neurons)
ann_deep_model.add(Dense(units=64, activation='relu'))
# Layer 2: New Hidden Layer (32 neurons)
ann_deep_model.add(Dense(units=32, activation='relu'))
# Layer 3: Output Layer (1 neuron)
ann_deep_model.add(Dense(units=1, activation='sigmoid'))
# Compile the model
ann_deep_model.compile(
optimizer='adam',
loss='binary_crossentropy',
metrics=['accuracy', [Link](name='auc')]
)
# Display the new architecture
print(" Deeper ANN Architecture (3 Hidden Layers)")
ann_deep_model.summary()
--- Deeper ANN Architecture (3 Hidden Layers) ---
Model: "Deeper_Credit_ANN"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┓
┃ Layer (type) ┃ Output Shape ┃ Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━┩
│ dense (Dense) │ (None, 64) │ 3,136 │
├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ dense_1 (Dense) │ (None, 32) │ 2,080 │
├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ dense_2 (Dense) │ (None, 1) │ 33 │
└──────────────────────────────────────┴─────────────────────────────┴─────────────────┘
Total params: 5,249 (20.50 KB)
Trainable params: 5,249 (20.50 KB)
Non-trainable params: 0 (0.00 B)
In [12]: # Define the Early Stopping Callback Monitors 'val_loss' (validation loss) to minimize overfitting.
# patience=10: Waits for 10 epochs without improvement before stopping.
early_stopper_deep = EarlyStopping(
monitor='val_loss',
patience=10,
restore_best_weights=True,
verbose=1
)
# Train the deeper model
print("\n Training with Early Stopping (Max 100 Epochs)")
history_deep = ann_deep_model.fit(
X_train, y_train,
epochs=100,
batch_size=32,
validation_split=0.1,
callbacks=[early_stopper_deep], # <-- Apply the stopper here
verbose=0
)
print(f"Training Complete. Model halted after {len(history_deep.history['loss'])} epochs.")
# Evaluate the deeper model on the test set
deep_loss, deep_accuracy, deep_auc = ann_deep_model.evaluate(X_test, y_test, verbose=0)
--- Training with Early Stopping (Max 100 Epochs) ---
Epoch 21: early stopping
Restoring model weights from the end of the best epoch: 11.
Training Complete. Model halted after 21 epochs.
In [13]: comparison_data = {
'Model': [
'Logistic Regression',
'Simple ANN (2-Layer)',
'Deeper ANN + Early Stopping'
],
'Test Accuracy': [log_accuracy, ann_accuracy, deep_accuracy],
'ROC-AUC Score': [log_auc, ann_auc, deep_auc]
}
comparison_df = [Link](comparison_data)
# Re-sort to show best performance first (based on ROC-AUC)
comparison_df = comparison_df.sort_values(by='ROC-AUC Score', ascending=False)
print("\n Model Performance Comparison ")
print(comparison_df)
# Business Insight
best_model = comparison_df.iloc[0]['Model']
best_auc = comparison_df.iloc[0]['ROC-AUC Score']
print(f"\n The {best_model} achieved the highest ROC-AUC score of {best_auc:.4f}.")
--- Model Performance Comparison ---
Model Test Accuracy ROC-AUC Score
0 Logistic Regression 0.785 0.804286
1 Simple ANN (2-Layer) 0.780 0.804226
2 Deeper ANN + Early Stopping 0.735 0.775238
The Logistic Regression achieved the highest ROC-AUC score of 0.8043.
While the deeper network did not achieve the highest AUC, the results are comparable, demonstrating that for this
specific dataset, a minimal structure is often sufficient.
Overfitting/Underfitting diagnosis
In [14]: import [Link] as plt
import seaborn as sns
print(" Learning Curves for Deeper ANN ")
# A. Plot Training & Validation Loss
[Link](figsize=(12, 5))
[Link](1, 2, 1)
[Link](history_deep.history['loss'], label='Training Loss')
[Link](history_deep.history['val_loss'], label='Validation Loss')
[Link]('Model Loss: Training vs. Validation')
[Link]('Epoch')
[Link]('Loss (Binary Crossentropy)')
[Link]()
[Link](True)
# B. Plot Training & Validation AUC
[Link](1, 2, 2)
# Note: The metric name for AUC is 'auc' in the history object
[Link](history_deep.history['auc'], label='Training AUC')
[Link](history_deep.history['val_auc'], label='Validation AUC')
[Link]('Model AUC: Training vs. Validation')
[Link]('Epoch')
[Link]('AUC Score')
[Link]()
[Link](True)
plt.tight_layout()
[Link]()
# --- C. Final Status Check ---
# Get final scores from the restored best weights
best_val_loss = min(history_deep.history['val_loss'])
final_epochs = len(history_deep.history['loss'])
print(f"\nFinal Epochs Run: {final_epochs}")
print(f"Best Validation Loss: {best_val_loss:.4f}")
--- Learning Curves for Deeper ANN ---
Final Epochs Run: 21
Best Validation Loss: 0.5255
Overfitting/Underfitting diagnosis based on the plots:
The Training Loss continues to decrease while the Validation Loss stops decreasing and starts increasing rapidly after a
certain point. Similarly, the Training AUC continues to rise while the Validation AUC plateaus or starts to fall. The model
has started to "memorize" the noise and specifics of the training data. This is called overfitting. The divergence point is
where generalization ability peaked. The Early Stopping did not mitigated the worst effects of overfitting. This indicates
patience needs to be reduced.
Regularization approach to avoid overfitting.
In [15]: from [Link] import Dense, Dropout
In [16]: # Model Definition: ANN_regularized
# We use the Deeper ANN architecture but apply more aggressive regularization
# Initialize the Sequential model
ann_regularized = Sequential(name="ANN_regularized")
# Layer 1: Input layer
ann_regularized.add([Link]( shape = (input_dim,)))
# Layer 2: Hidden Layer (64 neurons)
ann_regularized.add(Dense(units=64, activation='relu'))
# Layer 3: Dropout (Increased Rate: 0.3)
ann_regularized.add(Dropout(0.3))
# Layer 4: Hidden Layer (32 neurons)
ann_regularized.add(Dense(units=32, activation='relu'))
# Layer 5: Dropout (Increased Rate: 0.3)
ann_regularized.add(Dropout(0.3))
# Layer 6: Output Layer (1 neuron)
ann_regularized.add(Dense(units=1, activation='sigmoid'))
# Compile the model
ann_regularized.compile(
optimizer='adam',
loss='binary_crossentropy',
metrics=['accuracy', [Link](name='auc')]
)
# Define the Early Stopping Callback with stricter patience
early_stopper = EarlyStopping(
monitor='val_loss',
patience=5, # <--- Reduced Patience
restore_best_weights=True,
verbose=1
)
# Training the Model
print("\n Training ANN_regularized ")
# Using 100 epochs, allowing Early Stopping to halt the training quickly
history_regularized = ann_regularized.fit(
X_train, y_train,
epochs=100,
batch_size=32,
validation_split=0.1,
callbacks=[early_stopper],
verbose=0
)
--- Training ANN_regularized ---
Epoch 25: early stopping
Restoring model weights from the end of the best epoch: 20.
In [17]: from [Link] import roc_auc_score
# Evaluate the new model
regularized_loss, regularized_accuracy, regularized_auc = ann_regularized.evaluate(X_test, y_test, verbose
print("\n Model Performance (Post-Regularization)")
comparison_data = {
'Metric': ['Test Accuracy', 'ROC-AUC Score', 'Epochs Run'],
'ANN_regularized': [
f"{regularized_accuracy:.4f}",
f"{regularized_auc:.4f}",
len(history_regularized.history['loss'])
]
}
comparison_df = [Link](comparison_data)
print(comparison_df)
--- Model Performance (Post-Regularization) ---
Metric ANN_regularized
0 Test Accuracy 0.7650
1 ROC-AUC Score 0.7860
2 Epochs Run 25
Result: Performance is comparable or slightly worse. The model stopped after fewer epochs. This confirms that the
previous training run was too long and highlights the effectiveness of Early Stopping in saving computation time.
Wider ANN (breadth) approach to increase the hypothesis search space.
In [18]: from [Link] import Recall
In [19]: model_wide = Sequential(name="ANN_Wide_Model")
model_wide.add([Link]( shape = (input_dim,)))
# Wider Hidden Layer (128 units)
# This layer provides high capacity to learn patterns.
model_wide.add(Dense(units=128, activation='relu'))
# Dropout Regularization (High Capacity requires strong control)
model_wide.add(Dropout(0.3))
# Hidden Layer (64 units)
model_wide.add(Dense(units=64, activation='relu'))
# Dropout Regularization
model_wide.add(Dropout(0.3))
# Output Layer
model_wide.add(Dense(units=1, activation='sigmoid'))
# Compile the model
model_wide.compile(
optimizer='adam',
loss='binary_crossentropy',
metrics=['accuracy', Recall(), [Link](name='auc')]
)
# Display the final, most complex architecture
print(" Wide ANN Architecture (Max Capacity) ")
model_wide.summary()
--- Wide ANN Architecture (Max Capacity) ---
Model: "ANN_Wide_Model"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┓
┃ Layer (type) ┃ Output Shape ┃ Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━┩
│ dense_6 (Dense) │ (None, 128) │ 6,272 │
├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ dropout_2 (Dropout) │ (None, 128) │ 0 │
├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ dense_7 (Dense) │ (None, 64) │ 8,256 │
├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ dropout_3 (Dropout) │ (None, 64) │ 0 │
├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ dense_8 (Dense) │ (None, 1) │ 65 │
└──────────────────────────────────────┴─────────────────────────────┴─────────────────┘
Total params: 14,593 (57.00 KB)
Trainable params: 14,593 (57.00 KB)
Non-trainable params: 0 (0.00 B)
In [20]: # Define the Early Stopping Callback (stricter control)
# Patience is set low to prevent the performance decay seen in earlier models.
early_stopper_final = EarlyStopping(
monitor='val_loss',
patience=5,
restore_best_weights=True,
verbose=0
)
# Training the Final Model
print("\n Training Wide ANN (Optimal Control) ")
history_final_wide = model_wide.fit(
X_train, y_train,
epochs=100,
batch_size=32,
validation_split=0.1,
callbacks=[early_stopper_final],
verbose=0
)
--- Training Wide ANN (Optimal Control) ---
In [21]: from [Link] import roc_auc_score
# Evaluate the final model
wide_loss, wide_accuracy, wide_recall, wide_auc = model_wide.evaluate(X_test, y_test, verbose=0)
# Final Comparison
# Assuming the best prior AUC was approx 0.7860 (from ANN_regularized)
print("\n--- Model Evaluation ---")
print(f" Model (Wide ANN) Test Accuracy: {wide_accuracy:.4f}")
print(f" Model (Wide ANN) ROC-AUC Score: {wide_auc:.4f}")
print(f" Model (Wide ANN) Recall Score: {wide_recall:.4f}")
--- Model Evaluation ---
Model (Wide ANN) Test Accuracy: 0.8050
Model (Wide ANN) ROC-AUC Score: 0.8020
Model (Wide ANN) Recall Score: 0.5500
Assessment: The wider network was not so necessary or slightly over-parameterized. The final performance is
comparable, proving that the simpler, less complex ANN may also be sufficient for the data complexity.
Cost-Sensitive Training of ANN Classifier for imbalance class
Goal is to increase Generalization & Statistical Accuracy with Minimizing Financial Cost (FN×5)
Method which we start with is Early Stopping + Dropout Class plus class weighting, train it using the Class Weights that
reflect the 5:1 cost matrix.
Helps the model pay more attention to underrepresented classes during training
In [22]: from [Link] import class_weight
from [Link] import confusion_matrix, roc_auc_score, recall_score
In [23]: # Define Cost-Sensitive Weights
# Target: y (0=Good, 1=Bad)
# For binary classification: Weight for class i = total_samples / (n_classes * count(class_i))
# For example Class 0 weight = 1000 / (2 * 800) = 0.625 , Class 1 weight = 1000 / (2 * 200) = 2.5
class_weights_array = class_weight.compute_class_weight(
'balanced',
classes=[Link](y_train),
y=y_train
)
class_weights = {i: class_weights_array[i] for i in range(2)}
# Apply Cost Matrix Factor: FN (Class 1 error) is 5x more costly than FP (Class 0 error)
cost_factor = 5
class_weights[1] = class_weights[1] * cost_factor # Weight of Bad Risk class (1) is scaled by 5
print(f"Calculated Cost-Sensitive Class Weights: {class_weights}")
Calculated Cost-Sensitive Class Weights: {0: np.float64(0.7142857142857143), 1: np.float64(8.33333333333333
4)}
In [24]: # Defining the same architecture as wide ANN one
model_final = Sequential(name="ANN_Final_Model")
model_final.add([Link]( shape = (input_dim,)))
# This layer provides high capacity to learn patterns.
model_final.add(Dense(units=128, activation='relu'))
# Dropout Regularization (High Capacity requires strong control)
model_final.add(Dropout(0.3))
# Hidden Layer (64 units)
model_final.add(Dense(units=64, activation='relu'))
# Dropout Regularization
model_final.add(Dropout(0.3))
# Output Layer
model_final.add(Dense(units=1, activation='sigmoid'))
# Compile the model
model_final.compile(
optimizer='adam',
loss='binary_crossentropy',
metrics=['accuracy', Recall(), [Link](name='auc')]
)
# Display the final, most complex architecture
print(" Wide ANN Architecture (Max Capacity) ")
model_final.summary()
--- Wide ANN Architecture (Max Capacity) ---
Model: "ANN_Final_Model"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┓
┃ Layer (type) ┃ Output Shape ┃ Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━┩
│ dense_9 (Dense) │ (None, 128) │ 6,272 │
├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ dropout_4 (Dropout) │ (None, 128) │ 0 │
├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ dense_10 (Dense) │ (None, 64) │ 8,256 │
├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ dropout_5 (Dropout) │ (None, 64) │ 0 │
├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ dense_11 (Dense) │ (None, 1) │ 65 │
└──────────────────────────────────────┴─────────────────────────────┴─────────────────┘
Total params: 14,593 (57.00 KB)
Trainable params: 14,593 (57.00 KB)
Non-trainable params: 0 (0.00 B)
In [25]: # Train Model with Cost-Sensitivity
early_stopper_cost = EarlyStopping(monitor='val_loss', patience=5, restore_best_weights=True, verbose=1)
print("\n Training COST-SENSITIVE Model ")
model_final.fit(
X_train, y_train,
epochs=100,
batch_size=32,
validation_split=0.1,
callbacks=[early_stopper_cost],
class_weight=class_weights, # <--- Final Optimization Technique
verbose=1
)
print("Training Complete: Cost-Sensitive Model Ready.")
--- Training COST-SENSITIVE Model ---
Epoch 1/100
23/23 ━━━━━━━━━━━━━━━━━━━━ 5s 50ms/step - accuracy: 0.3194 - auc: 0.5637 - loss: 1.4833 - recall_1: 0.9535
- val_accuracy: 0.3125 - val_auc: 0.5884 - val_loss: 1.3219 - val_recall_1: 1.0000
Epoch 2/100
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 12ms/step - accuracy: 0.2986 - auc: 0.6159 - loss: 1.3259 - recall_1: 1.0000
- val_accuracy: 0.3125 - val_auc: 0.6251 - val_loss: 1.1353 - val_recall_1: 1.0000
Epoch 3/100
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 13ms/step - accuracy: 0.3042 - auc: 0.6751 - loss: 1.2697 - recall_1: 1.0000
- val_accuracy: 0.3125 - val_auc: 0.6713 - val_loss: 1.1152 - val_recall_1: 1.0000
Epoch 4/100
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 13ms/step - accuracy: 0.3167 - auc: 0.7611 - loss: 1.1712 - recall_1: 1.0000
- val_accuracy: 0.3125 - val_auc: 0.6833 - val_loss: 1.0407 - val_recall_1: 1.0000
Epoch 5/100
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 15ms/step - accuracy: 0.3319 - auc: 0.7287 - loss: 1.2047 - recall_1: 0.9953
- val_accuracy: 0.3625 - val_auc: 0.7062 - val_loss: 1.0509 - val_recall_1: 1.0000
Epoch 6/100
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 14ms/step - accuracy: 0.3931 - auc: 0.7828 - loss: 1.1166 - recall_1: 0.9860
- val_accuracy: 0.4375 - val_auc: 0.7127 - val_loss: 0.9783 - val_recall_1: 0.9600
Epoch 7/100
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 14ms/step - accuracy: 0.4389 - auc: 0.7866 - loss: 1.0948 - recall_1: 1.0000
- val_accuracy: 0.4625 - val_auc: 0.7207 - val_loss: 0.9817 - val_recall_1: 0.9600
Epoch 8/100
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 16ms/step - accuracy: 0.4347 - auc: 0.8136 - loss: 1.0416 - recall_1: 0.9907
- val_accuracy: 0.4750 - val_auc: 0.7247 - val_loss: 0.9233 - val_recall_1: 0.8800
Epoch 9/100
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 15ms/step - accuracy: 0.5306 - auc: 0.8109 - loss: 1.0576 - recall_1: 0.9814
- val_accuracy: 0.4875 - val_auc: 0.7251 - val_loss: 0.9716 - val_recall_1: 0.8400
Epoch 10/100
23/23 ━━━━━━━━━━━━━━━━━━━━ 0s 16ms/step - accuracy: 0.4861 - auc: 0.8325 - loss: 0.9892 - recall_1: 0.9907
- val_accuracy: 0.5125 - val_auc: 0.7553 - val_loss: 0.9059 - val_recall_1: 0.8400
Epoch 11/100
23/23 ━━━━━━━━━━━━━━━━━━━━ 1s 22ms/step - accuracy: 0.5750 - auc: 0.8355 - loss: 0.9743 - recall_1: 0.9860
- val_accuracy: 0.5125 - val_auc: 0.7527 - val_loss: 0.9775 - val_recall_1: 0.8800
Epoch 12/100
23/23 ━━━━━━━━━━━━━━━━━━━━ 1s 28ms/step - accuracy: 0.5556 - auc: 0.8428 - loss: 0.9493 - recall_1: 0.9860
- val_accuracy: 0.5625 - val_auc: 0.7509 - val_loss: 0.8915 - val_recall_1: 0.8400
Epoch 13/100
23/23 ━━━━━━━━━━━━━━━━━━━━ 1s 20ms/step - accuracy: 0.6097 - auc: 0.8624 - loss: 0.8929 - recall_1: 0.9860
- val_accuracy: 0.6125 - val_auc: 0.7662 - val_loss: 0.8326 - val_recall_1: 0.8400
Epoch 14/100
23/23 ━━━━━━━━━━━━━━━━━━━━ 1s 22ms/step - accuracy: 0.6083 - auc: 0.8688 - loss: 0.8777 - recall_1: 0.9860
- val_accuracy: 0.5500 - val_auc: 0.7571 - val_loss: 0.9567 - val_recall_1: 0.8400
Epoch 15/100
23/23 ━━━━━━━━━━━━━━━━━━━━ 1s 22ms/step - accuracy: 0.6153 - auc: 0.8781 - loss: 0.8613 - recall_1: 0.9860
- val_accuracy: 0.5625 - val_auc: 0.7625 - val_loss: 0.9043 - val_recall_1: 0.8400
Epoch 16/100
23/23 ━━━━━━━━━━━━━━━━━━━━ 1s 20ms/step - accuracy: 0.6292 - auc: 0.8754 - loss: 0.8482 - recall_1: 0.9860
- val_accuracy: 0.6000 - val_auc: 0.7687 - val_loss: 0.8378 - val_recall_1: 0.8400
Epoch 17/100
23/23 ━━━━━━━━━━━━━━━━━━━━ 1s 24ms/step - accuracy: 0.6444 - auc: 0.8805 - loss: 0.8284 - recall_1: 0.9767
- val_accuracy: 0.5875 - val_auc: 0.7593 - val_loss: 0.8987 - val_recall_1: 0.8800
Epoch 18/100
23/23 ━━━━━━━━━━━━━━━━━━━━ 1s 20ms/step - accuracy: 0.6514 - auc: 0.8963 - loss: 0.7834 - recall_1: 0.9860
- val_accuracy: 0.5875 - val_auc: 0.7651 - val_loss: 0.8744 - val_recall_1: 0.8400
Epoch 18: early stopping
Restoring model weights from the end of the best epoch: 13.
Training Complete: Cost-Sensitive Model Ready.
In [26]: # Evaluation: Calculate Cost and Compare
# Generate predictions
P_pred_final = model_final.predict(X_test)
Y_pred_class_final = (P_pred_final > 0.5).astype(int)
# Calculate Cost Metrics
cm_final = confusion_matrix(y_test, Y_pred_class_final)
tn, fp, fn, tp = cm_final.ravel()
# Total Misclassification Cost = (FN * 5) + (FP * 1)
total_cost = (fn * 5) + (fp * 1)
final_recall = recall_score(y_test, Y_pred_class_final)
print("\n Final Business Performance Summary ")
print(f"Total Misclassification Cost: {total_cost} Units")
print(f"Recall (Bad Risk - FN Reduction): {final_recall:.4f}")
print("Cost Matrix (Final):")
cm_df_final = [Link](cm_final,
index=['Actual Good (0)', 'Actual Bad (1)'],
columns=['Predicted Good (0)', 'Predicted Bad (1)'])
print(cm_df_final)
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 25ms/step
--- Final Business Performance Summary ---
Total Misclassification Cost: 114 Units
Recall (Bad Risk - FN Reduction): 0.9000
Cost Matrix (Final):
Predicted Good (0) Predicted Bad (1)
Actual Good (0) 56 84
Actual Bad (1) 6 54
In [27]: cm_final
Out[27]: array([[56, 84],
[ 6, 54]])
In [28]: [Link](figsize=(8, 6))
[Link](cm_final, annot=True, fmt='d', cmap='Blues',
xticklabels=['Predicted Good (0)', 'Predicted Bad (1)'],
yticklabels=['Actual Good (0)', 'Actual Bad (1)'])
[Link]('Confusion Matrix: German Credit Risk ')
[Link]('True Class')
[Link]('Predicted Class')
[Link]()
Interpretation:
Top Left (TN): Correctly Identified Good Credit
Top Right (FP): False Positive (Type I Error) - Predicted Bad, but Actual Good. This is a LOST OPPORTUNITY
(Business Cost: 1).
Bottom Right (TP): Correctly Identified Bad Credit (Loan Rejected/No Loss)
Bottom Left (FN): False Negative (Type II Error) - Predicted Good, but Actual Bad. This is the CRITICAL ERROR
(Business Cost: 5 - Loan Default).
In [29]: print("\n Classification Report ")
# Using the target labels 0 (Good) and 1 (Bad)
print(classification_report(y_test, Y_pred_class_final, target_names=['Good Risk (0)', 'Bad Risk (1)']))
--- Classification Report ---
precision recall f1-score support
Good Risk (0) 0.90 0.40 0.55 140
Bad Risk (1) 0.39 0.90 0.55 60
accuracy 0.55 200
macro avg 0.65 0.65 0.55 200
weighted avg 0.75 0.55 0.55 200
Interpretation of Critical Metrics (for class 'Bad Risk (1)'):
1. Recall (Bad Risk): Measures the model's ability to find ALL positive samples.
How many of the ACTUAL Bad Risks did we correctly identify?
Maximize this to minimize the costliest error (False Negatives/Loan Defaults).
2. Precision (Bad Risk): Measures the accuracy of positive predictions.
Of all the applicants we PREDICTED as Bad Risk, how many were actually Bad?
Maximize this to avoid rejecting too many good customers (False Positives/Lost Opportunity).
In [30]: print("\n ROC-AUC and ROC Curve (Model Discriminatory Power) ")
# Calculate AUC
roc_auc = roc_auc_score(y_test, P_pred_final)
print(f"ROC AUC Score: {roc_auc:.4f}")
# Calculate ROC Curve points
fpr, tpr, thresholds = roc_curve(y_test, P_pred_final)
# Plot ROC Curve
[Link](figsize=(8, 6))
[Link](fpr, tpr, label=f'AUC = {roc_auc:.4f}')
[Link]([0, 1], [0, 1], 'r--')
[Link]([0.0, 1.0])
[Link]([0.0, 1.05])
[Link]('False Positive Rate (1 - Specificity)')
[Link]('True Positive Rate (Recall)')
[Link]('Receiver Operating Characteristic (ROC) Curve')
[Link](loc="lower right")
[Link]()
--- ROC-AUC and ROC Curve (Model Discriminatory Power) ---
ROC AUC Score: 0.8054
Interpretation:
AUC is the probability that the model ranks a randomly chosen positive case (Bad Risk) higher than a randomly
chosen negative case (Good Risk).
A value close to 1.0 indicates excellent discriminative power. A value of 0.5 is no better than random guessing.
Deployment
Save the model
In [31]: # Model Saving and Prediction - The modern, recommended way to save Keras models is the Keras v3 format (.
# which saves the architecture, weights, compilation info (loss/optimizer state) efficiently as a single a
print("\n Model Saving and Loading ")
model_filename = 'german_credit_ann.keras'
# Save the Model
model_final.save(model_filename)
print(f"Model saved successfully in .keras format as: {model_filename}")
--- Model Saving and Loading ---
Model saved successfully in .keras format as: german_credit_ann.keras
Load the model
In [32]: # Load the Model
reconstructed_model = load_model(model_filename)
print("Model loaded successfully.")
Model loaded successfully.
Predictions
In [33]: # Make Prediction with the loaded model (Let us make predictions on the first 5 test samples)
sample_predictions = reconstructed_model.predict(X_test.head(5))
sample_results = [Link]({
'Probability_Bad_Risk': sample_predictions.flatten(),
'Predicted_Class': (sample_predictions > 0.5).astype(int).flatten(),
'Actual_Class': y_test.head(5).values
})
print("\nSample Predictions on Loaded Model:")
print(sample_results)
1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 166ms/step
Sample Predictions on Loaded Model:
Probability_Bad_Risk Predicted_Class Actual_Class
0 0.761553 1 0
1 0.640403 1 0
2 0.896385 1 1
3 0.566097 1 0
4 0.736858 1 1