Bucketwise Join waitlist

Learn / Data guide

Derived fields: what to calculate before any credit-risk analysis

Most analyses need fields that no table stores. DPD, months on book, bad flag and roll flag are calculated, and each lender calculates them a little differently. This guide lists the common ones with a plain definition, a SQL pattern and the choice you must make first. Patterns use the table and column names from the data guide. Swap in your own names, and write your choices down once so every analysis uses the same ones.

Rules before you calculate anything

Delinquency fields

FieldWhat it meansSQL pattern (PostgreSQL)Decide first
DPDDays since the oldest unpaid instalment fell due, on the as-of dateGREATEST(0, DATE '2026-09-30' - MIN(due_dt) FILTER (WHERE cum_due > paid)) where cum_due is the running total of due_amt per loan and paid is total successful payments to the as-of dateAllocation rule (oldest due first is usual), grace days, which payment statuses count, how reversals are netted
BucketThe DPD band the loan sits inCASE WHEN dpd <= 0 THEN 'Current' WHEN dpd <= 30 THEN '1-30' WHEN dpd <= 60 THEN '31-60' WHEN dpd <= 90 THEN '61-90' ELSE '91+' ENDBoundary days. 91+ means more than 90
Overdue amountWhat is due up to the as-of date and not yet paidGREATEST(0, SUM(due_amt) FILTER (WHERE due_dt <= DATE '2026-09-30') - COALESCE(paid, 0))Principal only, or principal plus interest and charges
Unpaid instalmentsHow many instalments are still openCOUNT(*) FILTER (WHERE cum_due > paid) over the schedule rows due by the as-of dateHow a part payment is treated
Worst DPD in a windowThe highest DPD a loan reached in a periodMAX(dpd) FILTER (WHERE mob BETWEEN 0 AND 6)Window length, and whether it counts month-end values or any day

Time fields

FieldWhat it meansSQL pattern (PostgreSQL)Decide first
Disbursal month (vintage)The month the loan was disbursed, used to group cohortsDATE_TRUNC('month', disb_dt)Disbursal date or sanction date
Months on book (MOB)How many months have passed since disbursal(EXTRACT(YEAR FROM snap_dt) - EXTRACT(YEAR FROM disb_dt)) * 12 + EXTRACT(MONTH FROM snap_dt) - EXTRACT(MONTH FROM disb_dt)Whether the disbursal month is MOB 0 or MOB 1
Maturity dateThe month the last instalment is duedisb_dt + (tenure_m || ' months')::intervalContract tenure, or the last due date in the schedule
Age of loan in daysDays since disbursalDATE '2026-09-30' - disb_dtUsed for early-default windows

Outcome flags

FieldWhat it meansSQL pattern (PostgreSQL)Decide first
Bad flagWhether the loan reached your default definition inside a fixed windowMAX(CASE WHEN dpd > 90 AND mob <= 12 THEN 1 ELSE 0 END) per loan over its monthly snapshotsThe DPD line, the window, and what to do with loans too young to have a full window
First-payment defaultThe first instalment was not paid in timeCASE WHEN paid_by_first_due_plus_n_days < first_due_amt THEN 1 ELSE 0 END, where paid_by_first_due_plus_n_days sums payments up to the first due date plus N daysN days, and the amount that counts as paid
Early defaultA bad outcome in the first few monthsMAX(CASE WHEN dpd > 30 AND mob <= 3 THEN 1 ELSE 0 END)Both the DPD line and the number of months
Active flagWhether the loan is still live on the as-of dateCASE WHEN status = 'Active' AND pos_amt > 0 THEN 1 ELSE 0 ENDTreatment of written-off and restructured loans
Prepayment flagThe loan closed before its scheduled maturityCASE WHEN closure_dt < maturity_dt THEN 1 ELSE 0 ENDA tolerance for loans closed within a few days of maturity

Movement fields

FieldWhat it meansSQL pattern (PostgreSQL)Decide first
Previous bucketThe bucket at the last month-endLAG(bucket) OVER (PARTITION BY loan_id ORDER BY month_end)Check the previous row really is the previous month. A gap makes LAG point to the wrong month
Next bucketThe bucket at the next month-endLEAD(bucket) OVER (PARTITION BY loan_id ORDER BY month_end)Same check. Also what to do with loans that close in between
Roll flagWorse, same or better than last monthCASE WHEN next_idx > idx THEN 'Roll forward' WHEN next_idx < idx THEN 'Cure' ELSE 'Stay' END, where idx numbers the buckets 0 to 4Count or balance, and which loans are in the base
Collected in monthMoney actually received in the monthSUM(pay_amt) FILTER (WHERE txn_status = 'SUCCESS' AND pay_dt BETWEEN month_start AND month_end)Whether reversals are netted and whether failed presentations are excluded

Application and bureau fields

FieldWhat it meansSQL pattern (PostgreSQL)Decide first
Bureau score at applicationThe latest bureau score before the application dateSELECT DISTINCT ON (app_id) app_id, bureau_score FROM bureau_pull WHERE pull_dt <= app_dt ORDER BY app_id, pull_dt DESCMaximum age of a pull you accept, and how to treat the no-history value
FOIRFixed obligations as a share of income(existing_emi + proposed_emi) / NULLIF(monthly_income, 0)Which obligations count, and declared or verified income
Score bandBureau score grouped into bandsCASE WHEN score = -1 THEN 'No history' WHEN score < 650 THEN '<650' WHEN score < 700 THEN '650-699' WHEN score < 750 THEN '700-749' ELSE '750+' ENDBand edges. Use the ones your policy already uses
Ticket and tenure bandsLoan size and tenure grouped into bandsCASE WHEN sanction_amt < 50000 THEN '<50k' WHEN sanction_amt < 200000 THEN '50k-2L' ELSE '2L+' ENDBand edges, and sanctioned or disbursed amount
UtilisationShare of a limit that is in usebalance / NULLIF(credit_limit, 0)Limit at the month-end, or the original limit

What we checked

We ran these patterns on a loan data set with known answers. DPD, bucket and overdue amount matched the known answer on every loan whose schedule was complete. A base table built by reducing each source to one row per key came out with exactly one row per loan, while a direct join of schedule to payments produced about 140 rows per loan.

The roll flag worked as a pattern, but built from stored month-end snapshots it differed from the known answer in 3 of 25 cells, because a few stored month-end values were wrong. If the result matters, recalculate the bucket from payments for both months instead of trusting the stored value.

The bad flag, first-payment default and the band fields depend on your own definitions, so there is no single right answer to test against. Write your definition down and keep it fixed.

Where to go next

The Analysis base table tool writes the query that builds one table with all of these fields, using your table names. Run its five checks before you start the analysis.

Try it on your own tables

The free tools give you the prompt, the checks and how to present the result. You enter table names only.

See the free tools

Frequently asked questions

Why do I have to calculate these fields myself?

Source systems store facts: what was due, what was paid, when. Fields like DPD, months on book and the bad flag are interpretations, so each lender defines them a little differently and rarely stores them.

Why does joining schedule to payments inflate my numbers?

Each instalment pairs with every payment on the loan, so one loan becomes many rows. Total each source to one row per loan first, then join.

Can I copy these patterns as they are?

Use them as a starting point. Replace the names with yours, and make the choices in the last column before you run anything.

Keep reading