Mathew K Analytics

Lesson 14 · Python for Banking and Finance

Comparing SQL and Pandas for Effective Financial Reporting and Data Analysis

Understand the differences between SQL and Pandas for reporting tasks Learn why fast and accurate reporting matters for banks and customers Explore hands-on…

⬇ Download notebookOpen in Colab ↗

📓 Full notebook

Download .ipynb

SQL vs Pandas for Reporting in Banking#

  • Understand the differences between SQL and Pandas for reporting tasks
  • Learn why fast and accurate reporting matters for banks and customers
  • Explore hands-on banking scenarios for both approaches
  • Build simple and complex reports with Python code
  • Know key strengths and pitfalls of each method
import pandas as pd
import numpy as np
import sqlite3
import warnings
warnings.filterwarnings('ignore')
np.random.seed(42)

Core Data Concepts for Reporting#

  • Transaction: Records the flow of money in or out for a customer
  • Customer: Holds customer details and attributes
  • Bank Account: Describes each customer's active account
  • Common relations: One customer has many transactions; one account belongs to one customer
  • Typical errors: Missing customer information, column mismatches, or date formatting mistakes
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

Beginner Example 1: Count Transactions per Customer (Pandas)#

  • Use Pandas to quickly count transactions for each customer.
  • This is a common starting point for any reporting pipeline.
txn_counts = df.groupby('customer_id').size().reset_index(name='txn_count')
print(txn_counts.head())
  customer_id  txn_count
0   CUST_0001          8
1   CUST_0002          5
2   CUST_0003          6
3   CUST_0004          4
4   CUST_0005          6
conn = sqlite3.connect(':memory:')
df.to_sql('transactions', conn, index=False)
sql_query = '''
SELECT customer_id, COUNT(*) AS txn_count
FROM transactions
GROUP BY customer_id
LIMIT 5
'''
sql_txn_counts = pd.read_sql_query(sql_query, conn)
print(sql_txn_counts)
  customer_id  txn_count
0   CUST_0001          8
1   CUST_0002          5
2   CUST_0003          6
3   CUST_0004          4
4   CUST_0005          6

Beginner Example 2: Filtering Transactions above $200#

  • Show customers with a transaction over $200 using Pandas.
  • This is a simple fraud risk or VIP query in banks.
large_txn_customers = df[df['amount'] > 200]['customer_id'].unique()
print('Unique customers with transactions > $200:', len(large_txn_customers))
Unique customers with transactions > $200: 126
sql_query = '''
SELECT COUNT(DISTINCT customer_id) AS unique_customers
FROM transactions
WHERE amount > 200
'''
sql_large_count = pd.read_sql_query(sql_query, conn)
print(sql_large_count)
   unique_customers
0               126

Beginner Example 3: Join Transactions with Customer Segment#

  • Combine transactions with customer data using Pandas.
  • This join lets you compare how business and retail groups behave.
df_merged = df.merge(customers, on='customer_id', how='left')
print(df_merged[['customer_id','segment','amount']].head(3))
  customer_id   segment  amount
0   CUST_0103    Retail  238.77
1   CUST_0180  Business  269.17
2   CUST_0093    Retail   58.62
customers.to_sql('customers', conn, index=False)
sql_query = '''
SELECT t.*, c.segment
FROM transactions t
LEFT JOIN customers c ON t.customer_id = c.customer_id
LIMIT 3
'''
sql_join_result = pd.read_sql_query(sql_query, conn)
print(sql_join_result[['customer_id','segment','amount']])
  customer_id   segment  amount
0   CUST_0103    Retail  238.77
1   CUST_0180  Business  269.17
2   CUST_0093    Retail   58.62

Intermediate Example 1: Monthly Channel Summary#

  • Find total transaction amounts for each channel and month using Pandas.
  • This answers a typical executive request: Which channels drive volume this year?
df['month'] = df['date'].dt.to_period('M')
chan_month = df.groupby(['month','channel'])['amount'].sum().reset_index()
print(chan_month.head())
     month channel    amount
0  2024-01     ATM  30960.24
1  2024-01  Branch  28278.66
2  2024-01  Online  27541.15
3  2024-01     POS  27713.45
4  2024-02     ATM  10547.34
sql_query = '''
SELECT strftime('%Y-%m', date) AS month, channel, SUM(amount) AS total_amount
FROM transactions
GROUP BY month, channel
LIMIT 5
'''
sql_chan_month = pd.read_sql_query(sql_query, conn)
print(sql_chan_month)
     month channel  total_amount
0  2024-01     ATM      30960.24
1  2024-01  Branch      28278.66
2  2024-01  Online      27541.15
3  2024-01     POS      27713.45
4  2024-02     ATM      10547.34

Intermediate Example 2: Average Debit Amount by Customer Segment#

  • Analyze debit transactions by customer segment with Pandas.
  • Shows banks which segments spend more with debit products.
df_merged = df.merge(customers, on='customer_id', how='left')
debit_averages = df_merged[df_merged['transaction_type']=='Debit'].groupby('segment')['amount'].mean().reset_index()
print(debit_averages)
    segment      amount
0  Business  149.146589
1    Retail  154.188940
sql_query = '''
SELECT c.segment, AVG(t.amount) AS avg_debit
FROM transactions t
LEFT JOIN customers c ON t.customer_id = c.customer_id
WHERE t.transaction_type = 'Debit'
GROUP BY c.segment
'''
sql_debits = pd.read_sql_query(sql_query, conn)
print(sql_debits)
    segment   avg_debit
0  Business  149.146589
1    Retail  154.188940

Intermediate Example 3: Find Inactive Customers#

  • Find customers with no transactions in the last month.
  • Banks use this report to target win-back campaigns.
latest_date = df['date'].max()
cutoff = latest_date - pd.Timedelta(days=30)
active_last_month = set(df[df['date'] > cutoff]['customer_id'])
inactive_customers = set(customers['customer_id']) - active_last_month
print('Number of inactive customers in last 30 days:', len(inactive_customers))
Number of inactive customers in last 30 days: 8
inactive_df = pd.DataFrame({'customer_id': list(inactive_customers)})
inactive_df.to_csv('inactive_customers.csv', index=False)
print('Example inactive customers:', inactive_df.head())
Example inactive customers:   customer_id
0   CUST_0031
1   CUST_0198
2   CUST_0064
3   CUST_0088
4   CUST_0139

Advanced Example 1: Pivoting Channel Usage by Segment#

  • Compare channel usage across business and retail with a Pandas pivot.
  • This builds a custom cross-tab report common in banking analytics.
pivot = df_merged.pivot_table(index='segment', columns='channel', values='transaction_id', aggfunc='count', fill_value=0)
print(pivot)
channel   ATM  Branch  Online  POS
segment                           
Business   74      59      57   60
Retail    192     189     179  190
sql_query = '''
SELECT c.segment, t.channel, COUNT(*) as txn_count
FROM transactions t
LEFT JOIN customers c ON t.customer_id = c.customer_id
GROUP BY c.segment, t.channel
'''
sql_cross = pd.read_sql_query(sql_query, conn)
print(sql_cross.pivot(index='segment', columns='channel', values='txn_count'))
channel   ATM  Branch  Online  POS
segment                           
Business   74      59      57   60
Retail    192     189     179  190

Advanced Example 2: Report with Complex Filtering and Subqueries#

  • Prepare a report: For customers with more than 5 debit transactions, what is their total credit volume?
  • These queries blend business rules and reporting in one statement.
debit_counts = df[df['transaction_type']=='Debit'].groupby('customer_id').size()
active_customers = debit_counts[debit_counts > 5].index
credit_volumes = df[(df['customer_id'].isin(active_customers)) & (df['transaction_type']=='Credit')]
result = credit_volumes.groupby('customer_id')['amount'].sum().reset_index()
print(result.head())
  customer_id   amount
0   CUST_0008   502.11
1   CUST_0028   597.22
2   CUST_0051   127.57
3   CUST_0058   389.48
4   CUST_0099  1081.76
sql_query = '''
SELECT t1.customer_id, SUM(t1.amount) AS credit_volume
FROM transactions t1
WHERE t1.transaction_type = 'Credit'
AND t1.customer_id IN (
  SELECT customer_id
  FROM transactions
  WHERE transaction_type = 'Debit'
  GROUP BY customer_id
  HAVING COUNT(*) > 5
)
GROUP BY t1.customer_id
LIMIT 5
'''
sql_credit_vols = pd.read_sql_query(sql_query, conn)
print(sql_credit_vols)
  customer_id  credit_volume
0   CUST_0008         502.11
1   CUST_0028         597.22
2   CUST_0051         127.57
3   CUST_0058         389.48
4   CUST_0099        1081.76

Error Handling: Common Data Mistakes in Reporting#

  • Mismatched date formats between systems
  • Misspelled column names: e.g. txn_type vs transaction_type
  • Accidentally including duplicate transactions
  • Forgetting to reset index after groupby
  • Not handling null or missing values in join keys
# Example mistake: Referencing a wrong column name in pandas
try:
    test = df.groupby('customerid').size()
except Exception as e:
    print('Error:', e)
Error: 'customerid'
# Example mistake: Date format mismatch in SQL
try:
    sql_query = '''
    SELECT * FROM transactions WHERE date > '2024/01/01' '''
    bad_dates = pd.read_sql_query(sql_query, conn)
except Exception as e:
    print('Error:', e)

Best Practices and Common Patterns#

  • Always look at sample rows with .head() before group or join
  • Name columns consistently across tables
  • Convert all dates to pandas datetime objects
  • Drop duplicates regularly for production pipelines
  • Prefer SQL for production reporting, Pandas for rapid prototyping
# Check for duplicates
duplicates = df.duplicated().sum()
print('Number of duplicated rows:', duplicates)
Number of duplicated rows: 0

End-to-End Example: Building a Daily Transaction Volume Report#

  • Join, group, filter, and export a typical daily transaction report for the business.
  • Data goes from raw input to a ready-to-share output file.
daily_vol = df_merged.groupby(['date', 'segment'])['amount'].sum().reset_index()
daily_vol.to_csv('daily_transaction_report.csv', index=False)
print('Report rows:', daily_vol.shape[0])
print(daily_vol.head(3))
Report rows: 1000
                 date   segment  amount
0 2024-01-01 00:00:00    Retail  238.77
1 2024-01-01 01:00:00  Business  269.17
2 2024-01-01 02:00:00    Retail   58.62

Want more guided Python banking lessons?#

  • Subscribe to our YouTube channel for deep dives and hands-on challenges!
  • Post your real-world banking projects in the comments below.

Found this useful?

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