Spaces:
Sleeping
Sleeping
| 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) |