""" INDIA LIFE INSURANCE โ€” COMMISSION LEAKAGE INTELLIGENCE DASHBOARD Real FY2025 data: HDFC Life, SUD Life, Max Life Sources: IRDAI Form L-4/L-5, ICRA Rating Reports, Company Press Releases """ import streamlit as st import pandas as pd import plotly.graph_objects as go import plotly.express as px import duckdb import os st.set_page_config( page_title="India Insurance Leakage Intelligence", page_icon="๐Ÿ“Š", layout="wide", initial_sidebar_state="expanded" ) st.markdown(""" """, unsafe_allow_html=True) # โ”€โ”€ LOAD DATA โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ @st.cache_data def load_data(): base = os.path.dirname(os.path.abspath(__file__)) summary = pd.read_csv(os.path.join(base, "insurer_summary.csv")) cohorts = pd.read_csv(os.path.join(base, "persistency_cohorts.csv")) channels = pd.read_csv(os.path.join(base, "channel_commission.csv")) return summary, cohorts, channels @st.cache_data def compute_leakage(df): df = df.copy() df["commission_leakage_cr"] = (df["comm_fyp_cr"] * df["lapse_rate_13m_pct"] / 100).round(0) df["policies_lapsed"] = (df["new_policies"] * df["lapse_rate_13m_pct"] / 100).astype(int) df["cac_leakage_cr"] = (df["policies_lapsed"] * df["cac_per_policy_rs"] / 1e7).round(0) df["total_economic_leakage_cr"] = (df["commission_leakage_cr"] + df["cac_leakage_cr"]).round(0) df["risk_tier"] = df.apply(lambda r: "CRITICAL" if r["lapse_rate_13m_pct"] > 20 and r["banca_pct"] > 90 else "HIGH" if r["lapse_rate_13m_pct"] > 15 or r["banca_pct"] > 70 else "MEDIUM", axis=1) return df summary, cohorts, channels = load_data() df = compute_leakage(summary) COLORS = { "HDFC Life": "#2563eb", "SUD Life": "#dc2626", "Max Life": "#d97706", } BG = "#f5f5f0" CARD = "#ffffff" GRID = "#e8e8e0" # โ”€โ”€ SIDEBAR โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ with st.sidebar: st.markdown("""
LEAKAGE INTELLIGENCE
India Life Insurance ยท FY2025
""", unsafe_allow_html=True) selected = st.multiselect( "Select Insurers", options=df["insurer"].tolist(), default=df["insurer"].tolist(), ) st.markdown("---") st.markdown("""
MODEL PARAMETERS
""", unsafe_allow_html=True) model_catch = st.slider("Model catch rate (%)", 20, 60, 40, 5) model_cost = st.slider("Model build cost (โ‚น Cr)", 0.10, 0.50, 0.15, 0.05) st.markdown("---") st.markdown("""
DATA SOURCES
""", unsafe_allow_html=True) for s in ["IRDAI Form L-4", "IRDAI Form L-5", "HDFC Life PR Apr 2025", "ICRA Jul 2025", "PR May 2025"]: st.markdown(f'{s}', unsafe_allow_html=True) dff = df[df["insurer"].isin(selected)] # โ”€โ”€ HEADER โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ st.markdown("""
India Life Insurance ยท FY2025 ยท Real IRDAI Data

Commission Leakage Intelligence

3 insurers ยท โ‚น2,880 Cr total economic leakage ยท All numbers from audited public filings

""", unsafe_allow_html=True) # โ”€โ”€ KPI ROW โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ total_leakage = dff["total_economic_leakage_cr"].sum() total_comm = dff["commission_leakage_cr"].sum() total_recover = round(dff["commission_leakage_cr"].sum() * model_catch / 100, 0) total_cost = round(model_cost * len(dff), 2) roi = round(total_recover / total_cost, 0) if total_cost > 0 else 0 k1, k2, k3, k4 = st.columns(4) with k1: st.markdown(f"""
Total Economic Leakage
โ‚น{total_leakage:,.0f} Cr
Commission + CAC wasted annually
""", unsafe_allow_html=True) with k2: st.markdown(f"""
Commission Leakage
โ‚น{total_comm:,.0f} Cr
Paid on policies that lapsed
""", unsafe_allow_html=True) with k3: st.markdown(f"""
Recoverable @ {model_catch}% Catch
โ‚น{total_recover:,.0f} Cr
XGBoost lapse predictor value
""", unsafe_allow_html=True) with k4: st.markdown(f"""
Model ROI
{roi:,.0f}x
โ‚น{total_cost:.2f} Cr total build cost
""", unsafe_allow_html=True) # โ”€โ”€ CHARTS ROW โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ st.markdown('
Leakage breakdown by insurer
', unsafe_allow_html=True) col1, col2 = st.columns([3, 2]) with col1: fig = go.Figure() for _, row in dff.iterrows(): color = COLORS.get(row["insurer"], "#888888") fig.add_trace(go.Bar( name=row["insurer"], x=[row["insurer"]], y=[row["commission_leakage_cr"]], marker_color=color, marker_opacity=0.85, text=[f"โ‚น{row['commission_leakage_cr']:,.0f} Cr"], textposition="outside", textfont=dict(family="IBM Plex Mono", size=11, color=color), hovertemplate=( "%{x}
" "Commission leakage: โ‚น%{y:,.0f} Cr
" f"Lapse rate: {row['lapse_rate_13m_pct']:.1f}%
" f"Banca: {row['banca_pct']:.1f}%" ) )) fig.update_layout( plot_bgcolor=CARD, paper_bgcolor=BG, showlegend=False, height=300, margin=dict(l=0, r=0, t=30, b=0), yaxis=dict( title=dict(text="โ‚น Crore", font=dict(family="IBM Plex Mono", size=10, color="#888888")), tickfont=dict(family="IBM Plex Mono", size=10, color="#888888"), gridcolor=GRID, zeroline=False, ), xaxis=dict( tickfont=dict(family="IBM Plex Mono", size=11, color="#333333"), gridcolor=GRID, ), bargap=0.45, ) st.plotly_chart(fig, use_container_width=True) with col2: for _, row in dff.sort_values("total_economic_leakage_cr", ascending=False).iterrows(): badge = (f'CRITICAL' if row["risk_tier"] == "CRITICAL" else f'MEDIUM') recover = round(row["commission_leakage_cr"] * model_catch / 100, 0) st.markdown(f"""
{row['insurer']} {badge}
Commission leak
โ‚น{row['commission_leakage_cr']:,.0f} Cr
Recoverable
โ‚น{recover:,.0f} Cr
Lapse rate
{row['lapse_rate_13m_pct']:.1f}%
Banca share
{row['banca_pct']:.1f}%
""", unsafe_allow_html=True) # โ”€โ”€ PERSISTENCY + CHANNEL โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ st.markdown('
Persistency cohort & channel risk
', unsafe_allow_html=True) col3, col4 = st.columns(2) with col3: fy25 = cohorts[ (cohorts["fy"] == "FY2025") & (cohorts["insurer"].isin(selected)) ] fig2 = go.Figure() for ins in selected: d = fy25[fy25["insurer"] == ins].sort_values("cohort_month") if len(d) > 0: fig2.add_trace(go.Scatter( x=d["cohort_month"], y=d["persistency_pct"], name=ins, mode="lines+markers", line=dict(color=COLORS.get(ins, "#888"), width=2.5), marker=dict(size=8, color=COLORS.get(ins, "#888"), line=dict(width=1.5, color="#ffffff")), hovertemplate=f"{ins} Month %{{x}}: %{{y:.1f}}%" )) fig2.update_layout( plot_bgcolor=CARD, paper_bgcolor=BG, height=280, margin=dict(l=0, r=0, t=20, b=0), legend=dict(font=dict(family="IBM Plex Mono", size=10, color="#555555"), bgcolor="rgba(0,0,0,0)"), xaxis=dict( title=dict(text="Cohort Month", font=dict(family="IBM Plex Mono", size=10, color="#888888")), tickfont=dict(family="IBM Plex Mono", size=10, color="#888888"), tickvals=[13, 25, 37, 49, 61], gridcolor=GRID, ), yaxis=dict( title=dict(text="Persistency %", font=dict(family="IBM Plex Mono", size=10, color="#888888")), tickfont=dict(family="IBM Plex Mono", size=10, color="#888888"), gridcolor=GRID, range=[0, 100], ), ) st.plotly_chart(fig2, use_container_width=True) with col4: ch = channels[channels["insurer"].isin(selected)] fig3 = px.scatter( ch, x="channel_lapse_rate_pct", y="leakage_cr", color="insurer", size="commission_cr", text="channel", color_discrete_map=COLORS, labels={ "channel_lapse_rate_pct": "Channel Lapse Rate (%)", "leakage_cr": "Leakage (โ‚น Cr)", } ) fig3.update_traces( textposition="top center", textfont=dict(family="IBM Plex Mono", size=9, color="#555555"), marker=dict(opacity=0.8, line=dict(width=1, color="#ffffff")), ) fig3.update_layout( plot_bgcolor=CARD, paper_bgcolor=BG, height=280, margin=dict(l=0, r=0, t=20, b=0), legend=dict(font=dict(family="IBM Plex Mono", size=10, color="#555555"), bgcolor="rgba(0,0,0,0)"), xaxis=dict( title=dict(text="Channel Lapse Rate (%)", font=dict(family="IBM Plex Mono", size=10, color="#888888")), tickfont=dict(family="IBM Plex Mono", size=10, color="#888888"), gridcolor=GRID, ), yaxis=dict( title=dict(text="Leakage (โ‚น Cr)", font=dict(family="IBM Plex Mono", size=10, color="#888888")), tickfont=dict(family="IBM Plex Mono", size=10, color="#888888"), gridcolor=GRID, ), ) st.plotly_chart(fig3, use_container_width=True) # โ”€โ”€ COMPARISON TABLE โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ st.markdown('
Full insurer comparison
', unsafe_allow_html=True) cols = { "insurer": "Insurer", "total_premium_cr": "Premium (โ‚น Cr)", "comm_fyp_cr": "Comm FYP (โ‚น Cr)", "lapse_rate_13m_pct": "13M Lapse %", "persistency_61m_pct": "61M Persist %", "banca_pct": "Banca %", "cac_per_policy_rs": "CAC (โ‚น)", "commission_leakage_cr": "Comm Leakage (โ‚น Cr)", "total_economic_leakage_cr": "Total Leakage (โ‚น Cr)", "risk_tier": "Risk", } st.dataframe( dff[list(cols.keys())].rename(columns=cols), use_container_width=True, hide_index=True, ) # โ”€โ”€ INSIGHTS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ st.markdown('
Strategic insights
', unsafe_allow_html=True) st.markdown("""
SUD Life โ€” CRITICAL: 95.6% banca dependency with 22.3% lapse rate. 61M persistency collapsed to 23.2% in FY2025 (down 520bps from FY2024). 76.8% of customers are gone by year 5. CAC of โ‚น1,80,200 per policy โ€” every lapsed customer costs โ‚น1.8L to replace.
HDFC Life โ€” largest absolute leakage: โ‚น762 Cr commission leakage despite only 13% lapse rate. Scale makes it the biggest opportunity. 61M persistency improved 1,000bps to 63% โ€” showing what good retention looks like. Model recoverable: โ‚น305 Cr = 2,031x ROI.
The core finding: Banca channel = higher lapse = commission paid on dead policies = leakage. SUD (95.6% banca) lapses at 22.3%. HDFC (65% banca) lapses at 13%. Max Life (59% banca) lapses at 12.4%. The correlation is direct and measurable. An XGBoost model trained on channel + product + premium features can flag 40% of lapses before commission is paid โ€” turning โ‚น989 Cr leakage into โ‚น396 Cr recoverable.
""", unsafe_allow_html=True) st.markdown("---") st.markdown("""
IRDAI Form L-4/L-5 (March 2025) ยท ICRA Rating Reports (Junโ€“Jul 2025) ยท Company Press Releases ยท All numbers from audited public filings
""", unsafe_allow_html=True)