Two models on this site rank invoices by who will pay late and when. A ranking is not a day's work. This engine turns it into one: one line per customer, promises on top, a cool-down so nobody is called twice in three days, and a capacity of four collectors × eighteen contacts split so each carries the same rupees. Then it runs the list for ten mornings and races it against the two lists every collections desk actually uses — biggest first and oldest first. It is running below, in your browser, on 900 open invoices.
A collections desk has a fixed number of conversations in it. Four people, eighteen contacts each, and a book of 900 open invoices belonging to 120 customers: the maths says most of the book is not getting called today, so the whole game is which seventy-two conversations. "Biggest first" calls the same large customer five times about five invoices. "Oldest first" spends the morning on the debts that have already stopped moving. In case 05 and case 08, the models produced a ranking — P(slip) per invoice, a payment date per invoice — and the ranking was the easy half. The Chase List is the other half: RANK made into SEAL of the Leak Ledger. It bundles, it remembers who was called on Friday, it puts a broken promise above a big score, and it splits the day so the senior collector's rupees are within 15% of everyone else's.
The engine is deliberately boring: it does not re-train the model, it does not guess who is in a good mood, and it does not let a collector pick. The score is one multiplication, the bundle is a sum, the cool-down is a number of days, and the split is greedy — the next line goes to whoever is carrying the least. Each rule is a sentence a collections manager can read and disagree with, which is what makes the list something the desk will actually follow: when a customer is on it, the line says why.
| STEP | RULE | WHAT IT DOES |
|---|---|---|
| P(slip) | Clipped logistic of 0.9 × prior late rate + 0.015 × days past due − 0.5, clipped to 0.02–0.98 | The stand-in for the who-pays-late model. In life this column comes from the model; the list does not care which. |
| Score | Amount × P(slip) for every overdue invoice. Not-yet-due invoices enter only if due within 3 days and above the courtesy threshold (₹1,00,000) | Rupees at risk, not rupees. A ₹5 lakh invoice with a 10% slip risk ranks below a ₹1 lakh invoice at 70%. |
| a · Bundle | One line per customer: all their eligible invoices, score = Σ, P(slip) = ₹-weighted mean | One conversation covers the account. "Biggest first" makes five calls to the same customer. |
| b · Cool-down | Skip customers contacted in the last 3 days — unless a promise fell due | A customer called on Friday is not called on Monday. Nagging is not chasing. |
| c · Promises | A promise due today or overdue puts the customer at the top of the list, above any score | Promise follow-ups are the cheapest cash on the desk; a promise nobody follows up teaches the customer that promises are free. |
| d · Capacity | 4 collectors × 18 contacts. Priority accounts go to the senior first; then each line goes to the collector with the lowest running ₹ who still has a slot (greedy, first minimum on ties). Check: every collector's ₹ within 15% of the mean | The list is cut where the day ends, not where hope does. The balance check is printed, not enforced — the greedy split normally passes it. |
| e · Roll-over | Whatever does not fit today is on tomorrow's list, one day older, with a higher P(slip) and so a higher score | Nothing falls off. A line that keeps rolling is a capacity problem, and the roll-over count says so. |
| Outcome | Each contact pays within 5 days with p = 0.35 + 0.4 × (1 − P(slip)); of the rest, 30% promise to pay in 5 days; the remainder is nothing. One draw per contact, in assignment order; the landing day (1–5) is a second draw only when it pays | The synthetic customer. Printed, so the comparison is fair: all three lists face the same customers and the same draws in the same order. |
| Tie-out | 900 = paid + open; today's lines = assigned + rolled; contacts = Σ collectors | If any breaks, the list is not published. |
# the split, in this order — the sentence a collections manager can disagree with (engine.py) for e in lines: # priority accounts to the senior first if e["priority"] and remaining[0] > 0: e["coll"] = 0; remaining[0] -= 1; running[0] += e["value"]; assigned.append(e) else: pool.append(e) for e in pool: # then greedy: the collector with the lowest running ₹ k = -1 for j in range(N_COLL): if remaining[j] > 0 and (k < 0 or running[j] < running[k]): k = j if k < 0: e["coll"] = None; continue # no slot left: tomorrow, one day older e["coll"] = k; remaining[k] -= 1; running[k] += e["value"]; assigned.append(e)
Run ten mornings to see what the list collects.
Two things to try. Set the cool-down to zero and watch the contacts climb while the cash barely moves — the same customers get called again before their money has had time to land. Then drop capacity to ten per collector: the roll-over count stops being zero after day 1 and the list starts carrying an ageing tail, which is the moment a desk is understaffed by arithmetic rather than by feel. The two naive lists are always run alongside on the same customers and the same draws; the slider "list shown below" only changes which one the panel describes.
| WHEN | WHAT HAPPENS | WHO SEES IT |
|---|---|---|
| 07:30 daily | Pull open invoices, yesterday's contact log and open promises; take P(slip) from the model's nightly score. Refuse to build if the ledger is older than 26 hours or the model score is missing for more than 2% of invoices. | A failed build pages the owner; yesterday's list stays up, marked stale. A missing score never becomes a silent zero. |
| 07:32 | Score, bundle, cool-down, promises on top, cut at capacity, split by running ₹. Tie out: lines = assigned + rolled; contacts = Σ collectors. Any gap → no publish. | The collections manager. A tie-out failure is the engine finding a bug in itself. |
| 07:35 | Publish four lists, one per collector: customer, invoices, ₹, P(slip), why it is here, "after 10:00" where the customer asked. Promise follow-ups first. | Each collector sees only their list. The senior sees the priority accounts at the top of theirs. |
| 18:00 | Outcomes logged: paid, promised (with a date), nothing. Tomorrow's cool-down and promise queue are built from this log, not from memory. | Nobody has to remember who they called. The log is the memory. |
| Weekly | Three numbers on one chart: cash landed against the two naive lists on the same book, contacts per ₹ lakh, lines rolled over per day. Capacity is discussed only when the roll-over chart stops being flat. | Leadership — the WATCH stage of the Leak Ledger. |
A model is not a plan; a list is. The ranking on this site is the easy half. The hard half is seventy-two conversations a day, chosen so that one call covers an account, a promise is never left to lapse, and nobody is rung on Monday about the thing they were rung about on Friday. Print the rules, cut the list where the day ends, and the same four people collect more with fewer calls — which is the only kind of collections improvement that survives the collector who has a bad week.
A ranking and no list? A chase-list engine is a two-to-three-week SEAL: your book, your collectors, your capacity — built every morning from the model's score, with a tie-out that refuses to publish a list that does not add up.
Start with a TRACE → engine.py README results.json The case it came from →