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"", 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(""" """, unsafe_allow_html=True) # ----------------------------- # Header Section # ----------------------------- col_title, col_logo = st.columns([4, 1]) with col_title: st.markdown(""" """, unsafe_allow_html=True) st.image("assets/bank-logo.png", width=90) with col_logo: st.markdown(""" """, 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( "

Early Detection Dashboard for Credit Risk

", unsafe_allow_html=True ) with col_loan_line: st.markdown(""" """, 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='Delinquency Bucket: %{label}
Customers: %{value}
Share: %{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='Collection Strategy: %{label}
Share: %{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( # """ #
#
#
# """, # 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='Delinquency Bucket: %{label}
Outstanding Balance: %{value}
Share: %{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='Payment Behaviour: %{label}
Share: %{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)