Kenya-Risk-Dashboard / KPI_Dashboard.py
aadhavans's picture
Adjusting folder structure
be7bb59
import streamlit as st
import pandas as pd
import plotly.graph_objects as go
from streamlit_extras.metric_cards import style_metric_cards
import json
import warnings
warnings.filterwarnings("ignore")
# -----------------------------
# Page Configuration
# -----------------------------
st.set_page_config(
page_title="KPI Dashboard",
layout="wide",
initial_sidebar_state="expanded",
)
# -----------------------------
# Inject Custom CSS
# -----------------------------
with open("assets/styles.css", encoding="utf-8") as f:
st.markdown(f"<style>{f.read()}</style>", unsafe_allow_html=True)
# -----------------------------
# Styling Metric Cards
# -----------------------------
style_metric_cards(
background_color="#141212",
border_color="#8D1DB9",
border_size_px=1,
border_radius_px=10,
border_left_color="#8D1DB9",
box_shadow=True
)
st.markdown("""
<style>
/* increase the size of the value and the label in the metric cards */
[data-testid="stMetricLabel"] p {
font-size: 26px !important;
font-weight: 300 !important;
color: white !important;
}
[data-testid="stMetricValue"] {
font-size: 50px;
}
</style>
""", unsafe_allow_html=True)
# -----------------------------
# Header Section
# -----------------------------
col_title, col_logo = st.columns([4, 1])
with col_title:
st.markdown("""
<style>
.stImage > img {
width: 200px; /* Increase size */
float: right; /* Align to the right */
border-radius: 10px;
box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.2);
transition: transform 0.3s ease-in-out;
}
</style>
""", unsafe_allow_html=True)
st.image("assets/bank-logo.png", width=90)
with col_logo:
st.markdown("""
<style>
.stImage > img {
width: 200px; /* Increase size */
float: right; /* Align to the right */
border-radius: 10px;
box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.2);
transition: transform 0.3s ease-in-out;
}
</style>
""", unsafe_allow_html=True)
st.image("assets/ailabs_logo.png", width=250)
col_title, col_loan_line = st.columns([4, 1])
with col_title:
st.markdown(
"<h1 style='text-align: left; color: #ffffff;'>Early Detection Dashboard for Credit Risk</h1>",
unsafe_allow_html=True
)
with col_loan_line:
st.markdown("""
<style>
.stImage > img {
width: 200px; /* Increase size */
float: right; /* Align to the right */
border-radius: 10px;
box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.2);
transition: transform 0.3s ease-in-out;
}
</style>
""", unsafe_allow_html=True)
st.image("assets/loan_line.png", width=350)
st.subheader("Product Type: :violet[Personal Loans]")
st.markdown("---")
# -----------------------------
# Define Risk Score Bands
# -----------------------------
data = {
"Risk Level": ["High Risk", "Medium Risk", "Low Risk"],
"Score Range": ["0 - 350", "351 - 650", "651 - 999"],
"Description": [
"High probability of risk.",
"Moderate probability of risk.",
"Low probability of risk."
]
}
df_risk_bands = pd.DataFrame(data)
def highlight_risk(row):
color = ''
if row['Risk Level'] == 'High Risk':
color = 'background-color: #BD100D; color: white;'
elif row['Risk Level'] == 'Medium Risk':
color = 'background-color: #B88F12; color: white;'
elif row['Risk Level'] == 'Low Risk':
color = 'background-color: #3A800F; color: white;'
return [color]*len(row)
df_risk_bands = df_risk_bands.style.apply(highlight_risk, axis=1)
# -----------------------------
# Load Portfolio Data
# -----------------------------
def load_data():
# df = pd.read_csv("data/time_series_customer_loan_data_with_shap.csv")
df = pd.read_csv("data/kenya_personal_loan_data_with_shap.csv")
df["Top Contributors"] = df["Top Contributors"].apply(json.loads)
df['Date'] = pd.to_datetime(df['Date'])
# df['Loan Status'] = df['Delinquency Bucket'].apply(lambda x: 'Current' if x in [0, 1] else 'Delinquent') # defining the delinquency status
return df
df = load_data()
target_date = '2026-01-31'
last_month_df = df[df['Date'] == target_date] # filter the dataframe for the target_date
# -----------------------------
# Compute KPIs
# -----------------------------
# Portfolio Composition
total_number_of_accounts = df['Account ID'].nunique()
total_ending_balance = df["Outstanding Balance"].sum()
average_loan_size = df["Initial Loan Amount"].mean()
payments_sum_by_date = df.groupby('Date')['Monthly Payments'].sum().reset_index()
payments_sum_by_date = df.groupby('Date')['Monthly Payments'].sum().reset_index()
match = payments_sum_by_date[payments_sum_by_date['Date'] == target_date]
if not match.empty:
payments_within_the_month = match['Monthly Payments'].values[0]
else:
payments_within_the_month = 0
# Delinquency Risk Metrics
# total_number_of_dq_accounts = last_month_df[last_month_df['Loan Bucket'].isin([2,3,4,5,6,7,8])].shape[0]
total_number_of_dq_accounts = last_month_df[last_month_df['Delinquency Status'] == 'Delinquent'].shape[0]
average_risk_score = round(last_month_df["Risk Score"].mean(), 2)
risk_counts = last_month_df["Risk Category"].value_counts().to_dict()
## High risk metrics
high_risk_count = risk_counts.get("High Risk", 0)
high_risk_percentage = (high_risk_count / total_number_of_accounts) * 100 if total_number_of_accounts > 0 else 0
high_risk_percentage = round(high_risk_percentage, 1)
high_risk_average_score = round(last_month_df[last_month_df["Risk Category"] == "High Risk"]["Risk Score"].mean(), 2) if high_risk_count > 0 else 0
high_risk_average_loan_amount = round(last_month_df[last_month_df["Risk Category"] == "High Risk"]["Initial Loan Amount"].mean(), 2) if high_risk_count > 0 else 0
## Medium risk metrics
medium_risk_count = risk_counts.get("Medium Risk", 0)
medium_risk_percentage = (medium_risk_count / total_number_of_accounts) * 100 if total_number_of_accounts > 0 else 0
medium_risk_percentage = round(medium_risk_percentage, 1)
medium_risk_average_score = round(last_month_df[last_month_df["Risk Category"] == "Medium Risk"]["Risk Score"].mean(), 2) if medium_risk_count > 0 else 0
medium_risk_average_loan_amount = round(last_month_df[last_month_df["Risk Category"] == "Medium Risk"]["Initial Loan Amount"].mean(), 2) if medium_risk_count > 0 else 0
## Low risk metrics
low_risk_count = risk_counts.get("Low Risk", 0)
low_risk_percentage = (low_risk_count / total_number_of_accounts) * 100 if total_number_of_accounts > 0 else 0
low_risk_percentage = round(low_risk_percentage, 1)
low_risk_average_score = round(last_month_df[last_month_df["Risk Category"] == "Low Risk"]["Risk Score"].mean(), 2) if low_risk_count > 0 else 0
low_risk_average_loan_amount = round(last_month_df[last_month_df["Risk Category"] == "Low Risk"]["Initial Loan Amount"].mean(), 2) if low_risk_count > 0 else 0
## Pie chart for percentages of accounts in each loan bucket
loan_bucket_counts = last_month_df['Delinquency Bucket'].value_counts().sort_index()
## Pie chart for proportion percentage of outstanding balance by delinquency bucket
outstanding_balance_by_bucket = last_month_df.groupby('Delinquency Bucket')['Outstanding Balance'].sum().sort_index()
## Pie chart for account distribution by payment behaviour
payment_behaviour_counts = last_month_df['Payment Behaviour'].value_counts().sort_index()
## Pie chart for account distribution by collection strategy
collection_strategy_counts = last_month_df['Recommended Risk Action'].value_counts().sort_index()
## Risk Score Distribution by Deciles
last_month_df["Risk Decile"] = pd.qcut(last_month_df['Risk Score'], q=10, labels=[f'D{i+1}' for i in range(10)])
grouped_deciles = last_month_df.groupby(['Risk Decile', 'Delinquency Status']).size().reset_index(name='Count')
pivot_df_score_deciles = grouped_deciles.pivot(index='Risk Decile', columns='Delinquency Status', values='Count').fillna(0)
pivot_df_score_deciles = pivot_df_score_deciles.reindex([f'D{i+1}' for i in range(10)]) # Ensure consistent decile order
# -----------------------------
# KPI Summary Cards
# -----------------------------
st.header("Portfolio Composition- as of :violet[August 2025]")
kpi1, kpi2, kpi3, kpi4 = st.columns(4)
kpi1.metric(label="Total Number of Accounts", value=total_number_of_accounts, border=True)
kpi2.metric(label="Total Outstanding Balance", value=f"USD {total_ending_balance:,.2f}", border=True)
kpi3.metric(label="Average Loan Size", value=f"USD {average_loan_size:,.2f}", border=True)
kpi4.metric(label="Payments", value=f"USD {payments_within_the_month:,.2f}", border=True)
kpi5, kpi6, kpi7 = st.columns(3)
kpi5.metric(label="Average Loan Amount (High Risk Accounts)", value=f"USD {high_risk_average_loan_amount:,.2f}", border=True)
kpi6.metric(label="Average Loan Amount (Medium Risk Accounts)", value=f"USD {medium_risk_average_loan_amount:,.2f}", border=True)
kpi7.metric(label="Average Loan Amount (Low Risk Accounts)", value=f"USD {low_risk_average_loan_amount:,.2f}", border=True)
col_pred, col_mid, col_lim = st.columns([3, 1, 3])
# Displaying the metrics for the prediction month
with col_pred:
st.header("Forecasted Metrics for :violet[February 2026]")
st.metric(label="Total Number of Delinquent Accounts", value=total_number_of_dq_accounts, border=True)
st.metric(label="Average Risk Score (Portfolio Level)", value=558.57, border=True)
st.metric(label="High Risk Accounts (%)", value=f"{high_risk_percentage}%", border=True)
st.metric(label="Average Risk Score (High Risk Accounts)", value=high_risk_average_score, border=True)
st.metric(label="Medium Risk Accounts (%)", value=f"{medium_risk_percentage}%", border=True)
st.metric(label="Average Risk Score (Medium Risk Accounts)", value=medium_risk_average_score, border=True)
st.metric(label="Low Risk Accounts (%)", value=f"{low_risk_percentage}%", border=True)
st.metric(label="Average Risk Score (Low Risk Accounts)", value=low_risk_average_score, border=True)
# -----------------------------
# Account Distribution by Delinquency Bucket
# -----------------------------
green_shades = {
'0-30': '#92ff7b',
'30+ DPD': '#92ff7b'
}
red_shade_base = '#fe6262'
# Define colors based on label
colors = []
for label in loan_bucket_counts.index.astype(str):
if label in green_shades:
colors.append(green_shades[label])
else:
colors.append(red_shade_base)
bucket_wise_account_percentages = go.Figure(
data=[
go.Pie(
labels=loan_bucket_counts.index.astype(str),
values=loan_bucket_counts.values,
textinfo='label+percent',
textposition='inside',
marker=dict(colors=colors, line=dict(color='white', width=2)),
hovertemplate='<b>Delinquency Bucket:</b> %{label}<br><b>Customers:</b> %{value}<br><b>Share:</b> %{percent}',
sort=False
)
]
)
st.subheader("Accounts Distribution by Delinquency Bucket")
bucket_wise_account_percentages.update_layout(
template='plotly_white',
margin=dict(t=80, b=50, l=50, r=50)
)
st.plotly_chart(bucket_wise_account_percentages, use_container_width=True)
# -----------------------------
# Accounts Distribution by Collection Strategy
# -----------------------------
collection_strategy_colors = {
'No Action Required': '#92ff7b',
'Monitor': '#92ff7b',
'Payment reminder email': '#ff9966',
'Payment reminder call': '#ff9966',
'Debt Relief Plan': '#fe6262',
'Downgrade Account': '#fe6262'
}
colors = []
for label in collection_strategy_counts.index.astype(str):
colors.append(collection_strategy_colors.get(label, '#d3d3d3')) # default grey if not mapped
collection_strategy_percentages = go.Figure(
data=[
go.Pie(
labels=collection_strategy_counts.index.astype(str),
values=collection_strategy_counts.values,
textinfo='label+percent',
textposition='inside',
marker=dict(colors=colors, line=dict(color="white", width=2)),
hovertemplate='<b>Collection Strategy:</b> %{label}<br><b>Share:</b> %{percent}',
sort=False
)
]
)
st.subheader("Accounts Distribution by Collection Strategy")
collection_strategy_percentages.update_layout(
template='plotly_white',
margin=dict(t=80, b=50, l=10, r=100)
)
st.plotly_chart(collection_strategy_percentages, use_container_width=True)
# with col_mid:
# st.markdown(
# """
# <div style="display: flex; justify-content: center;">
# <div style="height: 225vh; border-left: 2px solid white;"></div>
# </div>
# """,
# unsafe_allow_html=True
# )
# Displaying the metrics for the last input month
with col_lim:
st.header("Metrics as of :violet[August 2025]")
st.metric(label="Total Number of Delinquent Accounts", value=34, border=True)
st.metric(label="Average Risk Score (Portfolio Level)", value=523.13, border=True)
st.metric(label="High Risk Accounts (%)", value=f"{34.0}%", border=True)
st.metric(label="Average Risk Score (High Risk Accounts)", value=146.90, border=True)
st.metric(label="Medium Risk Accounts (%)", value=f"{26.0}%", border=True)
st.metric(label="Average Risk Score (Medium Risk Accounts)", value=493.14, border=True)
st.metric(label="Low Risk Accounts (%)", value=f"{40.0}%", border=True)
st.metric(label="Average Risk Score (Low Risk Accounts)", value=843.54, border=True)
# -----------------------------
# Account Distribution by Proportion of Outstanding Balance
# -----------------------------
green_shades = {
'0-30': '#92ff7b',
'30+ DPD': '#92ff7b'
}
red_shade_base = '#fe6262'
# Define colors based on label
colors = []
for label in outstanding_balance_by_bucket.index.astype(str):
if label in green_shades:
colors.append(green_shades[label])
else:
colors.append(red_shade_base)
bucket_wise_outstanding_balance_percentages = go.Figure(
data=[
go.Pie(
labels=outstanding_balance_by_bucket.index.astype(str),
values=outstanding_balance_by_bucket.values,
textinfo='label+percent',
textposition='inside',
marker=dict(colors=colors, line=dict(color='white', width=2)),
hovertemplate='<b>Delinquency Bucket:</b> %{label}<br><b>Outstanding Balance:</b> %{value}<br><b>Share:</b> %{percent}',
sort=False
)
]
)
st.subheader("Accounts Distribution by Proportion of Ending Balance")
bucket_wise_outstanding_balance_percentages.update_layout(
template='plotly_white',
margin=dict(t=80, b=50, l=50, r=50)
)
st.plotly_chart(bucket_wise_outstanding_balance_percentages, use_container_width=True)
# -----------------------------
# Account Distribution by Payment Behaviour
# -----------------------------
payment_behaviour_colors = {
'On Time': '#92ff7b',
'Late Payer': '#ff9966',
'Irregular Payer': '#ff9966',
'Non Payer': '#fe6262'
}
# Assign colors based on the label
colors = []
for label in payment_behaviour_counts.index.astype(str):
colors.append(payment_behaviour_colors.get(label, '#d3d3d3')) # default grey if not mapped
bucket_wise_payment_behaviour_percentages = go.Figure(
data=[
go.Pie(
labels=payment_behaviour_counts.index.astype(str),
values=payment_behaviour_counts.values,
textinfo='label+percent',
textposition='inside',
marker=dict(colors=colors, line=dict(color="white", width=2)),
hovertemplate='<b>Payment Behaviour:</b> %{label}<br><b>Share:</b> %{percent}',
sort=False
)
]
)
st.markdown("### Accounts Distribution by Payment Behaviour")
bucket_wise_payment_behaviour_percentages.update_layout(
template='plotly_white',
margin=dict(t=80, b=50, l=50, r=50)
)
st.plotly_chart(bucket_wise_payment_behaviour_percentages, use_container_width=True)
# -----------------------------
# Account Distribution by Risk Score vs Loan Status (Decile Wise Snapshot)- as of :violet[February 2026]
# -----------------------------
colors = {
'Current': '#92ff7b',
'Delinquent': '#fe6262'
}
decile_score_distribution = go.Figure()
for status in ['Current', 'Delinquent']:
decile_score_distribution.add_trace(
go.Bar(
x=pivot_df_score_deciles.index,
y=pivot_df_score_deciles[status],
name=status,
marker_color=colors[status],
width=0.4,
)
)
decile_score_distribution.update_layout(
barmode='group',
title={
'text': 'Account Distribution by Risk Score vs Delinquency Status (Decile Wise Snapshot)- Forecasted for February 2026',
'x': 0.5,
'xanchor': 'center',
'font': dict(size=29, family=', sans-serif')
},
xaxis_title='Risk Score Decile',
yaxis_title='Number of Customers',
template='plotly_white',
legend=dict(
# title='Loan Status',
orientation='h',
yanchor='bottom',
y=-0.3,
xanchor='center',
x=0.5,
font=dict(size=12)
),
margin=dict(t=80, b=100, l=50, r=50)
)
st.plotly_chart(decile_score_distribution, use_container_width=True)