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.

python
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".

python
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)
text
bucket
Current    400000
1-30       250000
31-60       80000
61-90      120000
90+         60000
Name: outstanding, dtype: int64

Step 2: calculate PAR

A small function keeps the logic in one place and works for any day count.

python
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%}")
text
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

Useful analytics insights, tools and resources.

No noise. An email now and then, and you can unsubscribe in one click.

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