ECL Model in Python: IFRS 9 / Ind AS 109

How to Build an ECL Model in Python: Worked Example

Quick answer: To build an ECL model in Python, assign each loan to Stage 1, 2 or 3, estimate PD, LGD and EAD by period, discount expected losses at the effective interest rate, and weight the results across economic scenarios. Stage 1 uses 12-month ECL; Stages 2 and 3 use lifetime ECL. This applies to IFRS 9 and Ind AS 109.

Most guides to expected credit loss stop at the formula. This one builds the whole calculation in Python on a synthetic loan portfolio: staging, 12-month and lifetime PD, LGD, EAD, discounting at the effective interest rate (EIR) and scenario weighting. Run the code blocks in order and you can reproduce the results table at the end.

It is written for credit risk analysts, provisioning teams and model validators who know basic pandas and want to see how the pieces of an ECL model in Python fit together. The method follows the impairment approach common to IFRS 9 and Ind AS 109.

Important: all data here is synthetic and this is a teaching model. It is not a production or regulatory model. The section on limitations explains what a real model adds.

How ECL works under IFRS 9 and Ind AS 109

Expected credit loss (ECL) is the probability-weighted, discounted estimate of the cash shortfalls you expect on a financial asset. The impairment model applies to financial assets at amortised cost, debt instruments at fair value through other comprehensive income, lease receivables, loan commitments and financial guarantee contracts. For each future period, the building block is:

ECL = marginal PD × LGD × EAD × discount factor

You sum this across the relevant horizon. The standards add three requirements (paragraph 5.5.17 in both): the estimate must be unbiased and probability-weighted, reflect the time value of money, and use reasonable and supportable information about past events, current conditions and forecasts of future conditions.

Impairment follows a three-stage approach:

StageTriggerECL horizon
Stage 1No significant increase in credit risk (SICR) since initial recognition, or the low credit risk exemption is applied12-month ECL
Stage 2SICR, but not credit-impairedLifetime ECL
Stage 3Credit-impaired (Appendix A events, usually aligned with your default definition)Lifetime ECL, with PD treated as 100%

Two points trip up newcomers.

First, 12-month ECL is not the cash shortfall you expect in the next year. It is the part of lifetime ECL that results from defaults possible within the next 12 months.

Second, the standards do not give one fixed definition of default. You define it consistently with your internal credit risk management, and the standard carries a rebuttable presumption that default does not occur later than 90 days past due. Similarly, there is a rebuttable presumption that credit risk has increased significantly once payments are more than 30 days past due. Our code uses both as policy choices.

IFRS 9 vs Ind AS 109: regulatory scope

IFRS 9 is the IASB’s international standard. Ind AS 109 is its counterpart under India’s converged accounting standards. The staging, SICR, 12-month versus lifetime and probability-weighting concepts used below apply to both.

What differs is who applies the standard and what regulators add on top. For NBFCs covered by Rule 4 of the Companies (Indian Accounting Standards) Rules, 2015, RBI’s current NBFC Financial Statements Directions, 2025 (updated 1 July 2026) set out regulatory guidance on Ind AS implementation, carried forward from its 13 March 2020 circular. These points matter for ECL modelling:

  • They hold impairment allowances as required by Ind AS 109 and, in parallel, compute provisions under RBI’s IRACP norms. A comparison of the two is disclosed in the notes to the financial statements.
  • Where the Ind AS 109 allowance is lower than the IRACP provision, the difference is appropriated from post-tax profit to a separate Impairment Reserve, which does not count towards regulatory capital. RBI labels this the prudential floor for ECL and says the requirement may be reviewed.

RBI also restates the 30-days-past-due rebuttable presumption for SICR and adds a limit: an NBFC shall not defer recognising a significant increase in credit risk for any exposure overdue beyond 60 days. It recommends that the accounting definition of default be guided by the regulatory one, and it points to the Basel Committee’s December 2015 guidance on credit risk and accounting for expected credit losses as a reference.

For banks, regulatory provisioning follows RBI’s prudential norms, and that framework has been evolving. Check RBI’s latest directions before using any model like this for regulatory provisioning. This article does not cover bank provisioning rules.

Step 1: Create a synthetic loan portfolio

We generate 1,000 loans with a fixed random seed so the results are reproducible. pd_orig is the 12-month PD at origination and pd_now is the current 12-month PD. In practice both come from your PD model; here they are simulated.

import numpy as np
import pandas as pd

# 1. SYNTHETIC portfolio (illustrative only: no real bank data)
rng = np.random.default_rng(42)
n = 1000
loans = pd.DataFrame({
‘loan_id’: np.arange(1, n + 1),
‘ead’: rng.integers(2, 50, n) * 100_000.0,   # outstanding exposure, INR
‘eir’: rng.uniform(0.09, 0.16, n),           # effective interest rate
‘term_yrs’: rng.integers(1, 6, n),           # remaining life, years
‘secured’: rng.random(n) < 0.55,
‘pd_orig’: rng.uniform(0.005, 0.03, n),      # 12-month PD at origination
})
loans[‘pd_now’] = np.clip(loans[‘pd_orig’] * rng.lognormal(0.2, 0.7, n), 0.001, 0.60)
loans[‘dpd’] = np.where(rng.random(n) < 0.12, rng.integers(1, 140, n), 0)

Step 2: Assign stages with SICR rules

Staging decides how long a horizon each loan gets, so it is often the biggest driver of the final number. Our rules are deliberately simple:

  • Stage 3: more than 90 days past due.
  • Stage 2: more than 30 days past due, or current PD at least double the origination PD and at least 0.5 percentage points higher.
  • Stage 1: everything else.

The relative-plus-absolute test stops tiny PD moves on very low-risk loans from triggering Stage 2. The standard looks at the change in the risk of default over the expected life. Using 12-month PD as a proxy is allowed only where it does not mislead (paragraphs B5.5.13 and B5.5.14), so loans whose risk is not evenly spread over time need a lifetime comparison. These thresholds are illustrative. A real policy calibrates and documents them, and adds qualitative indicators such as watchlist status and forbearance, which we omit.

# 2. Staging (thresholds are illustrative policy choices, not a standard)
def assign_stage(r):
if r.dpd > 90:
return 3
sicr = (r.dpd > 30) or (r.pd_now >= 2 * r.pd_orig and r.pd_now – r.pd_orig >= 0.005)
return 2 if sicr else 1

loans[‘stage’] = loans.apply(assign_stage, axis=1)

Step 3: Build the PD term structure (12-month vs lifetime)

Lifetime ECL needs a PD for every year of the loan, not just the next 12 months. We take the simplest route: assume a constant annual hazard rate derived from the 12-month PD.

  • Hazard: h = −ln(1 − PD12m)
  • Survival to year t: S(t) = e^(−h·t)
  • Marginal PD in year t: S(t−1) − S(t)

Stage 1 loans use year 1 only. Stage 2 loans use every year to maturity. Production models replace the constant hazard with survival models, migration matrices or vintage curves, and make PDs point-in-time by linking them to macroeconomic variables.

Step 4: Add LGD, EAD and EIR discounting

The function in the next step turns one scenario’s PD and LGD assumptions into loan-level ECL. It makes four simplifying assumptions, all stated in the code comments:

  • LGD: 25% for secured and 45% for unsecured loans, scaled by a scenario multiplier.
  • EAD: straight-line amortisation, so exposure falls evenly to zero at maturity. Year t uses that year’s opening balance and defaults are assumed to fall at year-end, which is slightly conservative.
  • Discounting: year-end cash flows discounted at each loan’s EIR.
  • Stage 3: PD is 100%, so ECL is LGD × EAD, with no discounting of recoveries.

Step 5: Weight the scenarios

The standards require a probability-weighted outcome, so we run the full calculation under three scenarios and weight the loan-level results.

ScenarioProbability weightPD multiplierLGD multiplier
Base50%1.001.00
Upside20%0.800.90
Downside30%1.501.15

These weights and multipliers are placeholders. Stage allocation is also held fixed across scenarios here; an institution may instead assess SICR on a scenario-weighted basis. Real models derive them from macroeconomic forecasts and satellite models that link GDP, unemployment or interest rates to PD and LGD.

# 3. Scenarios: name -> (probability weight, PD multiplier, LGD multiplier)
scen = {‘base’: (0.50, 1.00, 1.00),
‘upside’: (0.20, 0.80, 0.90),
‘downside’: (0.30, 1.50, 1.15)}

MAX_T = 5

def scenario_ecl(df, pd_mult, lgd_mult):
pd12 = np.clip(df[‘pd_now’] * pd_mult, 0, 0.99)
hazard = -np.log(1 – pd12)                     # constant annual hazard (simplification)
lgd = np.clip(np.where(df[‘secured’], 0.25, 0.45) * lgd_mult, 0, 1)
ecl = np.zeros(len(df))
for t in range(1, MAX_T + 1):
marg_pd = np.exp(-hazard * (t – 1)) – np.exp(-hazard * t)   # default in year t
ead_t = df[‘ead’] * np.clip((df[‘term_yrs’] – t + 1) / df[‘term_yrs’], 0, 1)
disc = (1 + df[‘eir’]) ** -t                                # discount at EIR
in_horizon = np.where(df[‘stage’] == 1, t <= 1, t <= df[‘term_yrs’])
ecl += np.where(in_horizon, marg_pd * lgd * ead_t * disc, 0.0)
return np.where(df[‘stage’] == 3, lgd * df[‘ead’], ecl)         # Stage 3: PD = 100%

# 4. Probability-weighted ECL
for name, (w, pm, lm) in scen.items():
loans[f’ecl_{name}’] = scenario_ecl(loans, pm, lm)
loans[‘ecl_weighted’] = sum(w * loans[f’ecl_{k}’] for k, (w, _, _) in scen.items())

# 5. Results (INR crore)
cr = 1e7
out = loans.groupby(‘stage’).agg(loans=(‘loan_id’, ‘count’), ead=(‘ead’, ‘sum’),
ecl_base=(‘ecl_base’, ‘sum’),
ecl_weighted=(‘ecl_weighted’, ‘sum’))
out[‘coverage_pct’] = 100 * out[‘ecl_weighted’] / out[‘ead’]
print((out[[‘ead’, ‘ecl_base’, ‘ecl_weighted’]] / cr).round(2).join(out[[‘loans’, ‘coverage_pct’]].round(2)))
print({k: round(float(loans[f’ecl_{k}’].sum() / cr), 2) for k in scen}, round(float(loans[‘ecl_weighted’].sum() / cr), 2))

Results of the ECL model in Python

Running the script with seed 42 gives these figures, in INR crore:

StageLoansEADECL (base)ECL (weighted)Coverage
Stage 1702177.890.951.100.62%
Stage 225365.251.832.103.22%
Stage 34513.354.614.7335.41%
Total1,000256.497.397.933.09%

Total ECL by scenario is 7.39 (base), 6.16 (upside) and 10.01 (downside). The weighted figure of 7.93 sits above the base case. The reason is asymmetry: the downside scenario adds about 2.6 crore to base ECL, while the upside removes only about 1.2 crore. Here, the base case alone would understate the provision. Losses respond non-linearly to PD and LGD, which is one reason the standard asks for an unbiased, probability-weighted amount (paragraph 5.5.17(a)).

The stage mix, with about a quarter of loans in Stage 2, comes from our synthetic assumptions. The data also generates days past due and PD independently, which real portfolios do not. Do not read it as a benchmark.

Stage 3 dominates in this run: 45 loans make up about 60% of weighted ECL. In this synthetic portfolio, LGD and staging assumptions move the total more than Stage 1 detail does.

What this model leaves out

A production ECL model differs from this example in at least these ways:

  • PD: statistically developed models, point-in-time calibration and a proper term structure instead of a constant hazard.
  • LGD: recovery-cash-flow models, cure rates, collateral haircuts and discounting of recoveries.
  • EAD: contractual amortisation schedules, prepayments and credit conversion factors for undrawn limits.
  • Staging: qualitative SICR triggers, probation and cure periods, and consistency with your default definition.
  • Scenarios: macroeconomic forecasts governed by an approved process, with documented overlays.
  • Governance: independent validation, documentation, controls over data and model change.
  • Other scope: purchased or originated credit-impaired assets, the simplified approach for trade receivables, off-balance-sheet commitments, and interest on Stage 3 assets (calculated on the net carrying amount) follow separate rules.

Validation checklist

Before you trust any ECL output, test it:

  • Reconcile loan counts and EAD to the general ledger.
  • Check stage migration against prior periods and explain large movements.
  • Run sensitivity tests on SICR thresholds, LGD and scenario weights.
  • Backtest PD and LGD against observed defaults and recoveries.
  • Compare weighted ECL with each single scenario, and confirm the direction makes sense.
  • Have someone other than the developer re-perform a sample of loans by hand.

FAQ

What is the ECL formula in IFRS 9? For each period, ECL is marginal PD × LGD × EAD × discount factor, summed over the relevant horizon and weighted across scenarios. The discount rate is the effective interest rate, or an approximation of it.

What is the difference between 12-month and lifetime ECL? 12-month ECL counts losses from defaults possible in the next 12 months. Lifetime ECL counts losses from defaults over the full remaining life. Stage 1 uses the first; Stages 2 and 3 use the second.

IFRS 9 vs Ind AS 109: what is the difference? The core ECL method (three stages, SICR, 12-month or lifetime ECL, probability weighting) is the same in both. The practical differences lie in who must apply the standard and in the regulatory overlay. For NBFCs applying Ind AS, RBI requires a parallel IRACP provision and an Impairment Reserve where the Ind AS allowance is lower.

How do you discount ECL? Discount each period’s expected shortfall back to the reporting date at the loan’s effective interest rate, as the code above does with (1 + eir) ** -t.

Can I use this model for regulatory provisioning? No. It is a teaching model on synthetic data. A bank or NBFC needs a governed, validated model that follows its accounting policy and applicable regulation.

Which Python libraries do I need? This example uses only numpy and pandas. For real PD models you would typically add statsmodels or scikit-learn, and a survival-analysis package for term structures.

Learn to build and validate ECL models

A small script shows the mechanics. Production ECL work needs PD, LGD and EAD development, scenario design and validation done properly. If you want structured, instructor-led training on this, see DexLab’s Advanced Certificate in IFRS 9 Modeling


.

October 31, 2026 11:21 am Published by

, , , , , , , , , , , , , ,

Comments are closed here.

...

Call us to know more

×