Data ScienceIndustry analytics playbooks

Credit card roll rates: build the delinquency transition matrix

PK
Pankit Kumar
Sr. Data Scientist at Parexel (a Goldman Sachs–backed company) · 27 September 2026 · 6 min read
In this article (7 sections)

Related programme: the Data Science course (6 months; ₹1,30,000 (inclusive of GST) · EMI from ₹5,000/month).

The short answer

A credit card roll rate is the percentage of accounts (or outstanding balance) in one days-past-due bucket that move to the next worse bucket one month later. You get it by pairing every card's bucket this month with its bucket next month, counting the pairs in a transition matrix, and dividing each row by its total. Multiplying the forward roll rates from Current to 90+ gives a rough share of today's current book that will reach 90+ if this month's behaviour persists.

What the roll rate tells you that a delinquency rate hides

A card portfolio's 30+ delinquency rate can stay flat while the book gets worse: fewer customers slip into 1-30, but more of those who slip keep sliding. Roll rates separate the two. They answer the collections question ("of the customers in 31-60 today, how many will I lose to 61-90 unless someone calls?") and the risk question ("how much current balance flows toward charge-off each month?").

The monthly snapshot you need

One row per card per statement month is enough:

ColumnMeaning
card_idaccount key, stable across months
monthstatement or cycle month
bucketCurrent, 1-30, 31-60, 61-90, 90+, Closed
balancetotal outstanding at cycle date, ₹
min_due_unpaidamount of minimum due still unpaid (used to derive the bucket)

Buckets on a card are usually driven by the unpaid minimum amount due, not the total balance. A customer who pays the minimum is Current even while revolving ₹1,00,000. Confirm how your bank derives the bucket before you compute anything; if the file mixes cycle-date and calendar-month-end snapshots, the transitions are meaningless.

Generate a synthetic book and build the matrix

The data below is synthetic: 3,000 cards, six months, generated from assumed transition probabilities so you can see whether the estimate recovers them.

python
import random
import pandas as pd

# Synthetic monthly delinquency snapshots for 3,000 cards (not real portfolio data)
rng = random.Random(42)
BUCKETS = ["Current", "1-30", "31-60", "61-90", "90+", "Closed"]
# assumed monthly transition probabilities used only to generate the data
P = {
    "Current": [0.93, 0.05, 0.00, 0.00, 0.00, 0.02],
    "1-30":    [0.55, 0.20, 0.25, 0.00, 0.00, 0.00],
    "31-60":   [0.20, 0.10, 0.15, 0.55, 0.00, 0.00],
    "61-90":   [0.08, 0.02, 0.05, 0.15, 0.70, 0.00],
    "90+":     [0.02, 0.00, 0.00, 0.00, 0.98, 0.00],
    "Closed":  [0.00, 0.00, 0.00, 0.00, 0.00, 1.00],
}
months = pd.period_range("2026-01", "2026-06", freq="M")
rows = []
for card in range(3000):
    state = "Current"
    balance = rng.randint(5_000, 1_50_000)
    for m in months:
        rows.append((card, str(m), state, balance if state != "Closed" else 0))
        state = rng.choices(BUCKETS, weights=P[state])[0]
        if state not in ("Current", "Closed"):
            balance = round(balance * 1.04)   # interest and late fees accrue
snap = pd.DataFrame(rows, columns=["card_id", "month", "bucket", "balance"])

Now pair each month with the next and normalise each row:

python
snap = snap.sort_values(["card_id", "month"])
snap["next_bucket"] = snap.groupby("card_id")["bucket"].shift(-1)
pairs = snap.dropna(subset=["next_bucket"])
pairs = pairs[pairs["bucket"] != "Closed"]      # closed cards are not at risk

counts = pd.crosstab(pairs["bucket"], pairs["next_bucket"])
matrix = counts.div(counts.sum(axis=1), axis=0).reindex(index=BUCKETS[:5], columns=BUCKETS)
print((matrix * 100).round(1))
print(counts.sum(axis=1))

The printed matrix (row = this month, column = next month, %):

From \ ToCurrent1-3031-6061-9090+Closed
Current93.25.00.00.00.01.8
1-3058.120.221.70.00.00.0
31-6022.911.010.156.00.00.0
61-907.12.44.816.769.00.0
90+0.00.00.00.0100.00.0

Reading the rows like a collections manager

  • The diagonal-right cell is the roll rate. 1-30 to 31-60 is 21.7%; 31-60 to 61-90 is 56.0%; 61-90 to 90+ is 69.0%. Severity compounds: once a customer misses two cycles, the odds of recovery fall sharply.
  • Left of the diagonal is cure. 58.1% of 1-30 accounts are Current next month. That is where early-bucket calling earns its cost.
  • Row sizes matter. The row totals printed underneath show 642 pairs in 1-30 but only 42 in 61-90. A 69% rate on 42 accounts can move ten points on a handful of customers; show the count next to every rate.
  • 90+ shows 100% stay. The generator allowed a 2% cure from 90+, but with only 16 pairs none happened. Small late buckets routinely produce "impossible" zeros and ones.

Count versus balance, and the chain

python
order = ["Current", "1-30", "31-60", "61-90", "90+"]
def roll_forward(df, weight=None):
    out = {}
    for a, b in zip(order[:-1], order[1:]):
        base = df[df["bucket"] == a]
        moved = base[base["next_bucket"] == b]
        if weight:
            out[f"{a} -> {b}"] = moved[weight].sum() / base[weight].sum()
        else:
            out[f"{a} -> {b}"] = len(moved) / len(base)
    return pd.Series(out)

rolls = pd.DataFrame({"by_count": roll_forward(pairs),
                      "by_balance": roll_forward(pairs, "balance")})
print((rolls * 100).round(1))
chain = rolls["by_count"].prod()
print(f"Current -> 90+ within four months: {chain:.4%}")
TransitionBy count %By balance %
Current → 1-305.05.0
1-30 → 31-6021.721.2
31-60 → 61-9056.056.2
61-90 → 90+69.066.9

The product of the four count-based rates is about 0.42%: under steady behaviour, roughly 42 of every 10,000 cards that are current today are 90+ four months later. That is a flow estimate, not a forecast; it assumes this month's rates hold for four months and ignores cures that come back and roll again.

In this synthetic book the balance column tracks the count column. In a real portfolio, a gap is a finding. If the balance roll rate from 61-90 is well above the count rate, high-limit customers are the ones sliding, and your loss estimate should use the balance version.

Where card roll-rate reports go wrong

  1. 1New cards filled in as Current. A card issued this month has no prior bucket. If a left join fills the missing value with "Current", every new card becomes a Current → Current pair and the Current → 1-30 rate is understated.
  2. 2Written-off cards disappearing. If charged-off accounts vanish from the snapshot, 90+ looks like it cures. Keep them as an absorbing state.
  3. 3Skipping buckets. Cycle-date shifts can make a card jump from 1-30 to 61-90. Show those jumps rather than forcing them into the diagonal; they usually point to a data problem.
  4. 4Mixing vintages. A burst of new sourcing lowers the Current → 1-30 rate for months because new customers rarely default immediately. Split the matrix by months-on-book when sourcing changes.

One slide for the risk committee

Put the forward roll rates for the last six months as lines, one per transition, with the bucket sizes as a small table underneath. Add one sentence per line that moved: which transition, how many points, and which segment drove it (sourcing channel, limit band, city tier). Risk committees act on the transition that changed, not on the matrix.

Transition matrices like this lead naturally into Markov-chain loss models covered in the Data Science programme. The bucket logic itself is the same DPD dating problem described in NPA ageing with SMA buckets.

Frequently asked questions

What is a roll rate in credit cards?

A roll rate is the share of accounts, or balances, in one delinquency bucket this month that move to the next worse bucket by next month. For example, the 31-60 to 61-90 roll rate is the fraction of cards 31-60 days past due in June that are 61-90 days past due in July.

Should roll rates be calculated on count or balance?

Calculate both. Count-based rates tell collections how many customers to call; balance-based rates drive loss and provisioning estimates. When the two diverge, larger balances are behaving differently from the typical account.

PK
Pankit Kumar
Lead Instructor, NeuraPath Academy

Pankit Kumar has 10 years in Data Science & AI, building and shipping production systems in regulated pharma and clinical environments. He is a freelance trainer at Boston Institute of Analytics, AnalytixLabs and Scaler, and has taught this material to thousands of working professionals.

This article is part of our Data Science programme — 6 months. From data foundations to machine learning, deep learning and deployment.

Explore Data Science
Counselling is free · no obligation

Not sure which programme fits?

Tell us your background and we will map it to the right entry point — including saying so when a cheaper programme is the better fit. A counsellor replies within one working day.