Python
How to calculate PAR30, PAR60 and PAR90 in pandas
A step-by-step walkthrough of bucketing loans by days past due and measuring portfolio at risk in Python, using a small made-up loan book.
By Adekunle Okunade2 min read
Sample article. Created to demonstrate the layout. The data in it is invented.
Portfolio at risk, or PAR, answers one question: what share of the loan book is overdue by more than a given number of days? It is one of the first numbers a lender checks, and it is easy to calculate once the data is shaped correctly.
This tutorial uses a tiny, invented loan book so every number can be checked by hand.
What PAR measures
PAR is expressed with a day count. PAR30 is the outstanding balance of loans that are more than 30 days past due, divided by the total outstanding balance. PAR60 and PAR90 work the same way with 60 and 90 days.
Note that PAR uses the whole outstanding balance of an overdue loan, not only the missed instalment. That is what makes it a measure of exposure.
The data
Each row is one loan, with its outstanding balance and how many days it is past due.
import pandas as pd
loans = pd.DataFrame({
"loan_id": [1, 2, 3, 4, 5, 6],
"outstanding": [100_000, 250_000, 80_000, 120_000, 60_000, 300_000],
"days_past_due": [0, 12, 35, 65, 95, 0],
})Step 1: put each loan in a bucket
pd.cut assigns each loan to a delinquency bucket. The first bin starts at -1 so that loans with zero days past due land in "Current".
bins = [-1, 0, 30, 60, 90, float("inf")]
labels = ["Current", "1-30", "31-60", "61-90", "90+"]
loans["bucket"] = pd.cut(loans["days_past_due"], bins=bins, labels=labels)
by_bucket = loans.groupby("bucket", observed=True)["outstanding"].sum()
print(by_bucket)bucket
Current 400000
1-30 250000
31-60 80000
61-90 120000
90+ 60000
Name: outstanding, dtype: int64Step 2: calculate PAR
A small function keeps the logic in one place and works for any day count.
total = loans["outstanding"].sum()
def par(days):
"""Share of the book that is more than `days` days past due."""
return loans.loc[loans["days_past_due"] > days, "outstanding"].sum() / total
for d in (30, 60, 90):
print(f"PAR{d}: {par(d):.1%}")PAR30: 28.6%
PAR60: 19.8%
PAR90: 6.6%You can verify PAR30 by hand: the loans over 30 days are 80,000 + 120,000 + 60,000 = 260,000, and 260,000 divided by the 910,000 total is 28.6%.
Common mistakes
- Using the missed instalment instead of the full balance. That gives a smaller number that is not PAR.
- Off-by-one buckets. "Past due more than 30 days" means
> 30, so a loan at exactly 30 days belongs in the 1-30 bucket. - Mixing currencies or dates. Calculate PAR on one snapshot date, in one currency.
Next steps
Once PAR is calculated per snapshot, track it over time and by segment. A rising PAR30 with a flat PAR90 usually means new delinquency is entering faster than it is rolling forward, which is where roll rate analysis helps.
This is a sample article created to demonstrate the layout. The data is invented.
- pandas
- credit risk
- PAR
- tutorial
Related articles
SQL2 min readSample
Group loans into delinquency buckets with SQL CASE WHEN
One query that buckets loans by days past due and shows each bucket's share of the book, with a note on what changes between database engines.
- SQL
- credit risk
- tutorial
- PAR
SQL5 min read
Build a roll-rate table in SQL
A delinquency report tells you where loans are today. A roll rate tells you where they are heading. Here is how to build one with a SQL window function, with output you can check by hand.
- SQL
- credit risk
- roll rates
- window functions
- tutorial