Mathew K Analytics

Lesson 15 · Python for Banking and Finance

How to Build Core Banking Views with Python for Financial Data Management

We will learn how to create core banking views using Python and pandas. This problem is important because banks need to aggregate data for regulatory…

⬇ 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

Core Banking Views: Building Foundational Analytics and Summaries#

  • We will learn how to create core banking views using Python and pandas.
  • This problem is important because banks need to aggregate data for regulatory reporting, customer insights, and risk management.
  • By working through business-driven examples, you will learn how to assemble transaction, customer, and account data into meaningful analytics.
  • We will build a simple but powerful foundation for real-world banking data analysis.
  • You will gain skill at using Python to create data aggregations, summaries, and tables essential for everyday banking operations.
import pandas as pd
import numpy as np
import warnings
warnings.filterwarnings('ignore')
np.random.seed(42)

Understanding Core Bank Data: Transactions, Customers, Accounts#

  • Transactions are records of customer payments or deposits and contain dates, types, and channels.
  • Customers have attributes like region and segment, and are linked to their accounts.
  • Accounts track balances and account types like Savings or Cheque, with unique identifiers.
  • Beginners often miss that real banking data is always relational: all tables are linked by IDs.
  • Data quality is critical: duplicated, missing, or mismatched IDs cause broken joins and bad analytics.
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  
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 = 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      Savings 2015-03-02
print(df['channel'].value_counts())
channel
ATM       266
POS       250
Branch    248
Online    236
Name: count, dtype: int64
print(df.groupby('transaction_type')['amount'].mean())
transaction_type
Credit    150.538052
Debit     152.880161
Name: amount, dtype: float64
df['amount'].hist(bins=30)
<Axes: >
No description has been provided for this image
daily_totals = df.groupby(df['date'].dt.date)['amount'].sum()
print(daily_totals.head())
date
2024-01-01    3771.76
2024-01-02    3993.85
2024-01-03    3777.92
2024-01-04    3946.69
2024-01-05    3489.53
Name: amount, dtype: float64
channel_crosstab = pd.crosstab(df['channel'], df['transaction_type'])
print(channel_crosstab)
transaction_type  Credit  Debit
channel                        
ATM                  129    137
Branch               125    123
Online               113    123
POS                  136    114
merged = df.merge(customers, on='customer_id', how='left')
channel_by_seg = merged.groupby(['segment', 'channel']).size().unstack()
print(channel_by_seg)
channel   ATM  Branch  Online  POS
segment                           
Business   74      59      57   60
Retail    192     189     179  190
avg_amount_region = merged.groupby('region')['amount'].mean()
print(avg_amount_region)
region
Metro       152.279400
Regional    151.162727
Name: amount, dtype: float64
pivot = merged.pivot_table(index='region', columns='transaction_type', values='amount', aggfunc='sum')
print(pivot)
transaction_type    Credit     Debit
region                              
Metro             36565.37  36985.58
Regional          39155.27  38995.86
accounts_per_region = accounts.merge(customers, on='customer_id').groupby('region')['account_id'].count()
print(accounts_per_region)
region
Metro       100
Regional    100
Name: account_id, dtype: int64
activity_per_customer = df.groupby('customer_id').size()
top10_customers = activity_per_customer.sort_values(ascending=False).head(10)
print(top10_customers)
customer_id
CUST_0190    13
CUST_0099    13
CUST_0113    11
CUST_0161    11
CUST_0147    10
CUST_0144    10
CUST_0145    10
CUST_0090    10
CUST_0028     9
CUST_0111     9
dtype: int64

Handling Data Quality Issues in Banking Views#

  • Missing or duplicated IDs cause broken joins.
  • Negative or zero amounts can signal data capture errors.
  • Always check for unexpected nulls or out-of-bounds values after merging.
  • Print samples early and often to catch mistakes before deeper analysis.
print(df[df['amount'] <= 0].head())
     transaction_id customer_id  amount transaction_type channel  \
94               95   CUST_0024   -4.38           Credit  Online   
165             166   CUST_0170  -64.09           Credit  Online   
231             232   CUST_0068   -4.69           Credit     ATM   
297             298   CUST_0161  -12.75           Credit     ATM   
304             305   CUST_0166   -1.76            Debit  Branch   

                   date  
94  2024-01-04 22:00:00  
165 2024-01-07 21:00:00  
231 2024-01-10 15:00:00  
297 2024-01-13 09:00:00  
304 2024-01-13 16:00:00  
print(merged[merged['customer_id'].isnull()].head())
Empty DataFrame
Columns: [transaction_id, customer_id, amount, transaction_type, channel, date, segment, region]
Index: []

Best Practices in Building Bank Data Views#

  • Always start by profiling data for nulls and range errors.
  • Use explicit joins and always check output shapes.
  • Verify all ID relationships before business analysis.
  • Use meaningful column names so business partners understand your work.
  • Save clean, intermediate tables as checkpoints.
merged.to_csv('customer_transactions.csv', index=False)
print('Saved cleaned customer transaction dataset.')
Saved cleaned customer transaction dataset.
# End-to-end Example: Calculate total monthly debit value by customer segment
df['month'] = df['date'].dt.to_period('M')
enriched = df.merge(customers, on='customer_id', how='left')
monthly_debit = enriched[enriched['transaction_type'] == 'Debit'].groupby(['month', 'segment'])['amount'].sum().unstack()
print(monthly_debit.head())
segment  Business    Retail
month                      
2024-01  15220.63  40703.22
2024-02   4019.28  16038.31
# Final Practice: Calculate share of business customers by region with at least one credit transaction
business_credits = merged[(merged['segment'] == 'Business') & (merged['transaction_type'] == 'Credit')]
business_region_counts = business_credits.groupby('region')['customer_id'].nunique()
all_business_counts = customers[customers['segment'] == 'Business'].groupby('region')['customer_id'].nunique()
share = (business_region_counts / all_business_counts * 100).round(2)
print(share)
region
Regional    90.0
Name: customer_id, dtype: float64
 

Found this useful?

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