Mathew K Analytics

Lesson 20 · Python for Banking and Finance

Fundamentals of Credit Risk Data Analysis in Banking Using Python

In this lesson, we learn how banks use data to make decisions about lending money. You will see how real-world credit risk data is structured. We will build…

⬇ Download notebookOpen in Colab ↗

What you'll learn

Data

No separate download needed — the notebook creates or downloads everything it uses.

📓 Full notebook

Download .ipynb

Introduction to Credit Risk Data#

  • In this lesson, we learn how banks use data to make decisions about lending money.
  • You will see how real-world credit risk data is structured.
  • We will build and examine datasets to understand credit risk.
  • Credit risk analysis helps banks avoid business losses.
  • You will practice with synthetic and public credit datasets.
  • By the end, you will understand how basic data analysis supports smarter banking.
import pandas as pd
import numpy as np
import warnings
warnings.filterwarnings('ignore')

Core Data Concepts: What is Credit Risk Data?#

  • Credit risk data helps banks assess customers who may not repay loans.
  • Data may include transactions, account details, and loan information.
  • Understanding how to join tables and handle missing data is important.
  • Beginners often assume data is always clean or correctly labeled.
  • Data can be messy, with missing values, duplicate rows, or unclear columns.
# Let us create a small synthetic banking transactions table to practice.
np.random.seed(42)  # Set seed for reproducibility
n_transactions = 1000
n_customers = 200
df = pd.DataFrame({
    'transaction_id': range(1, n_transactions + 1),
    'customer_id': np.random.choice([f'CUST_{i:04d}' for i in range(1, n_customers + 1)], n_transactions),
    'amount': np.round(np.random.normal(150, 60, n_transactions), 2),
    'transaction_type': np.random.choice(['Debit', 'Credit'], n_transactions),
    'channel': np.random.choice(['ATM', 'Online', 'Branch', 'POS'], n_transactions),
    'date': pd.date_range(start='2024-01-01', periods=n_transactions, freq='h')
})
print(df.shape)
print(df.head(3))
(1000, 6)
   transaction_id customer_id  amount transaction_type channel  \
0               1   CUST_0103  238.77           Credit     ATM   
1               2   CUST_0180  269.17           Credit     POS   
2               3   CUST_0093   58.62           Credit  Online   

                 date  
0 2024-01-01 00:00:00  
1 2024-01-01 01:00:00  
2 2024-01-01 02:00:00  
# We also need a table for customer details.
customer_ids = [f'CUST_{i:04d}' for i in range(1, 201)]
customers = pd.DataFrame({
    'customer_id': customer_ids,
    'segment': ['Retail'] * 150 + ['Business'] * 50,
    'region': ['Metro'] * 100 + ['Regional'] * 100
})
print(customers.shape)
print(customers.head(3))
(200, 3)
  customer_id segment region
0   CUST_0001  Retail  Metro
1   CUST_0002  Retail  Metro
2   CUST_0003  Retail  Metro
# Accounts are tied to both customers and their products.
np.random.seed(42)
accounts = pd.DataFrame({
    'account_id': [f'ACC_{i:05d}' for i in range(1, 201)],
    'customer_id': customer_ids,
    'account_type': np.random.choice(['Savings', 'Cheque', 'Credit'], size=200),
    'open_date': pd.date_range(start='2015-01-01', periods=200, freq='30D')
})
print(accounts.shape)
print(accounts.head(3))
(200, 4)
  account_id customer_id account_type  open_date
0  ACC_00001   CUST_0001       Credit 2015-01-01
1  ACC_00002   CUST_0002      Savings 2015-01-31
2  ACC_00003   CUST_0003       Credit 2015-03-02
# Now let us load a well-known public credit risk dataset.
url = 'https://archive.ics.uci.edu/ml/machine-learning-databases/statlog/german/german.data'
cols = [f'feature_{i}' for i in range(1, 25)] + ['credit_status']
german = pd.read_csv(url, sep=' ', names=cols)
german['default_flag'] = (german['credit_status'] == 2).astype(int)
print(german.shape)
print(german[['credit_status', 'default_flag']].head(3))
(1000, 26)
   credit_status  default_flag
0            NaN             0
1            NaN             0
2            NaN             0
# Beginner Example 1: Count how many transactions are Debits vs Credits.
print(df['transaction_type'].value_counts())
transaction_type
Credit    503
Debit     497
Name: count, dtype: int64
# Beginner Example 2: Find average transaction amount.
print(df['amount'].mean())
151.70208000000002
# Beginner Example 3: What regions do customers belong to?
print(customers['region'].value_counts())
region
Metro       100
Regional    100
Name: count, dtype: int64
# Intermediate Example 1: Join transactions to customer detail by customer_id.
merged = pd.merge(df, customers, on='customer_id', how='left')
print(merged[['transaction_id', 'customer_id', 'segment', 'region']].head(3))
   transaction_id customer_id   segment    region
0               1   CUST_0103    Retail  Regional
1               2   CUST_0180  Business  Regional
2               3   CUST_0093    Retail     Metro
# Intermediate Example 2: Calculate total debit amount by customer.
debits = df[df['transaction_type'] == 'Debit']
totals = debits.groupby('customer_id')['amount'].sum().reset_index()
print(totals.head(3))
  customer_id  amount
0   CUST_0001  611.67
1   CUST_0002  193.86
2   CUST_0003  783.31
# Intermediate Example 3: Look for customers who have both debit and credit transactions.
has_debit = df[df['transaction_type']=='Debit']['customer_id'].unique()
has_credit = df[df['transaction_type']=='Credit']['customer_id'].unique()
both = np.intersect1d(has_debit, has_credit)
print('Number of customers with both types:', len(both))
Number of customers with both types: 172
# Advanced Example 1: Flag transactions above a certain amount as 'large'.
threshold = 300
df['is_large'] = df['amount'] > threshold
print(df[['transaction_id', 'amount', 'is_large']].head(3))
   transaction_id  amount  is_large
0               1  238.77     False
1               2  269.17     False
2               3   58.62     False
# Advanced Example 2: Estimate risk by region using the German Credit dataset.
risk_by_region = german.groupby('credit_status')['default_flag'].mean()
print(risk_by_region)
Series([], Name: default_flag, dtype: float64)
# Advanced Example 3: Join all three tables for a holistic view.
df_accounts = pd.merge(df, accounts, on='customer_id', how='left')
result = pd.merge(df_accounts, customers, on='customer_id', how='left')
print(result[['transaction_id','customer_id','account_id','segment','region','amount']].head(3))
   transaction_id customer_id account_id   segment    region  amount
0               1   CUST_0103  ACC_00103    Retail  Regional  238.77
1               2   CUST_0180  ACC_00180  Business  Regional  269.17
2               3   CUST_0093  ACC_00093    Retail     Metro   58.62
# Error Handling Example: What happens if we try to join on the wrong column?
try:
    wrong_merge = pd.merge(df, accounts, left_on='transaction_id', right_on='account_id', how='left')
    print(wrong_merge.head(3))
except Exception as e:
    print('Error:', e)
Error: You are trying to merge on int64 and object columns for key 'transaction_id'. If you wish to proceed you should use pd.concat
# Debugging Example: Check for missing values after merges.
merged = pd.merge(df, customers, on='customer_id', how='left')
missing = merged['segment'].isnull().sum()
print('Rows with missing segment after merge:', missing)
Rows with missing segment after merge: 0
# Best Practice: Always set random seed for reproducibility.
np.random.seed(42)
# Common Pattern: Use groupby to summarize banking data easily.
summary = df.groupby('transaction_type')['amount'].agg(['count','mean','sum'])
print(summary)
                  count        mean       sum
transaction_type                             
Credit              503  150.538052  75720.64
Debit               497  152.880161  75981.44
# End-to-End Mini Problem: Find customers whose total transaction amount is above average and are in Metro region.
totals = df.groupby('customer_id')['amount'].sum().reset_index()
avg_total = totals['amount'].mean()
high_spenders = totals[totals['amount'] > avg_total]
final = pd.merge(high_spenders, customers, on='customer_id', how='left')
metro_high = final[final['region'] == 'Metro']
print('High spenders (Metro region):')
print(metro_high[['customer_id', 'amount', 'region']].head())
High spenders (Metro region):
  customer_id   amount region
0   CUST_0001  1083.16  Metro
1   CUST_0002   919.98  Metro
2   CUST_0003  1172.13  Metro
3   CUST_0008  1534.89  Metro
4   CUST_0012  1397.30  Metro

Want more? Subscribe to our YouTube channel for practical Python banking tutorials!#

  • YouTube is a great place to watch more detailed code walkthroughs and banking cases.
  • Hit the Subscribe button to keep learning!

Found this useful?

All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.