# When will this invoice be paid? — a payment-date model with a cash forecast

A machine-learning project by Simran Jaiswal on real B2B receivables: predict, for every open
invoice, **how many days after its due date it will clear** (with an honest band), flag the ones
likely to slip past a week, and roll the answers up into a **weekly cash forecast** — the WATCH stage
of the Leak Ledger, as a model.

Live case, with an in-browser scorer and the forecast: https://simranjaiswal.in/work/invoice-payment-date

## Data

The HighRadius B2B invoice dataset (public, anonymised): 50,000 invoices from one company's
receivables, posted December 2018 – May 2020 — posting, baseline and due dates, payment-terms code,
amount (USD / CAD), business unit, a masked customer, and the clearing date for the invoices that
were paid. After dropping 1,161 exact-duplicate rows: 39,158 cleared invoices and 9,681 still open
(the order book, $323.1M).

Hygiene worth knowing: customer names are masked into 4,197 variants ("WAL-MAR corp", "WAL-MAR
trust"…) for 1,099 customer numbers — use the number; the file mixes ISO and YYYYMMDD date encodings;
`area_business` is entirely empty; one customer accounts for 43% of invoices and pays four days
early. A widely mirrored "new" copy of this dataset has scrambled due dates (some in the year 2157) —
this project uses the original.

## Pipeline (`train.py`)

1. **Features known on the posting day** — amount, terms (due − baseline), business unit, due
   day-of-week / day-of-month / month, and the customer's history built **only from invoices
   cleared before that day**: count, median / mean / share late, share > 7 days late, last and
   last-three lateness, spread, days since last clearing, invoices and amount still open.
   No lookahead: an invoice never sees an outcome that had not happened when it was posted.
2. **Time-based split** — train on invoices posted before 2019-12-01 (32,395), test on December 2019
   – February 2020 (6,763). Random splits flatter payment models; time splits don't.
3. **Models** — three gradient-boosting regressors for days-late (median, 10th and 90th
   percentiles, 220 trees, depth 3) and a classifier for "more than 7 days late".
   Baseline: the customer's own historic median lateness — the number a good collector already has.
4. **Evaluation** — MAE against three baselines, by history depth and amount band; p10–p90 coverage;
   classifier AUC / PR-AUC / calibration; residual distribution.
5. **The WATCH** — score the open book; for each invoice draw days-late from a piecewise-linear
   quantile function through (p10, median, p90); 400 Monte-Carlo runs give a weekly cash schedule with
   bands, against the due-date schedule; plus a predicted ageing and a chase list ranked by
   amount × P(> 7 days late).
6. **Export** — the four models to `model.json`; the page reproduces sklearn to < 1e-6 and explains
   each prediction with path contributions in days.

## Results (6,763 held-out invoices, Dec 2019 – Feb 2020)

| predictor | MAE, days |
|---|---|
| "everyone pays on the due date" | 4.41 |
| terms-code median | 3.72 |
| customer's own median (the collector's number) | 3.02 |
| **gradient boosting, median** | **2.82** |

"> 7 days late" (5.9% of the period): AUC **0.870** (0.804 for the customer's prior share alone),
PR-AUC 0.607, Brier 0.034; top decile catches a 38% late rate against 5.9%.
p10–p90 band: median width 5.1 days, **73% coverage against an 80% target** — the bands are a little
narrow, a known weakness of quantile boosting on a spiky target; widen by the measured factor before use.

What carries the signal: share of prior invoices late (38% of split gain), due day-of-week (17%),
prior median lateness (15%). Terms are the structural tell — net-0 documents clear a median 39 days
late (90% past a week); net-15, the bulk, a median 0.

Open book: $323.1M; expected to clear more than 7 days late: **$11.3M across ~382 invoices** (3.5%);
at the aggregate the cash schedule barely slips, so the forecast's value is the tail and the bands.

## Reproduce

```bash
python -m venv .venv && ./.venv/bin/pip install -r requirements.txt
# put the original HighRadius file at data/h2h_invoices.csv, then:
./.venv/bin/python train.py
```

## Limitations, stated plainly

- One company, fifteen months, mostly net-15 terms and one dominant customer. The method transfers; the model does not.
- The customer's own history is most of the signal; the model's gain over it is real but modest (7% on MAE, 7 AUC points on the slip flag). Its real value is calibrated bands and the forecast.
- The Monte-Carlo forecast treats invoices as independent; a customer who slips slips on every invoice at once, so real bands are wider than these.
- Amounts mix USD and CAD without conversion (CAD is 8% of invoices).

## Files

- `train.py` — the whole pipeline, one file, deterministic (`SEED = 42`)
- `model.json` — four exported models (median, p10, p90, late-7 classifier)
- `evaluation.json` — results, hygiene, tables, curves, the forecast, ageing, chase list, presets, a 1,000-invoice reference sample
- `requirements.txt`

MIT for the code. The dataset is © HighRadius; it circulates publicly from their training programme.
