Receivables Finance, Payment, & Supply Chain Risk Scoring
A multi-tier payment and fulfillment scoring framework built for invoice finance: replacing bureau data with transactional signals, then extending to a MARS-based system handling 75+ variables with non-linear interactions and peer-relative normalization.
Why bureau data fails SMB invoice finance
The credit bureau industry was built around a different problem. When you apply bureau data to SMB invoice finance at global scale, four structural failures emerge that no amount of data cleaning fixes.
The data is old. Financial statements submitted to bureaus are often 12–24 months out of date by the time they’re incorporated. For an SMB, two years is multiple business cycles. During the funded portfolio, at least one company scored near the top of a major bureau’s probability-of-payment scale while simultaneously committing fraud because the behavior hadn’t yet propagated into the bureau’s data pipeline.
It rewards reputation over behavior. Bureau scores for SMBs are heavily influenced by years in business, number of trade lines, and credit utilization patterns. These correlate with size, not payment reliability. A 20-year-old regional distributor scores well because it has history, not because it pays on time.
The correlation to actual payment is weak. Empirical testing against the funded portfolio found bureau probability-of-payment scores performing only marginally better than random on the actual outcome being underwritten — whether a specific company would pay a specific invoice on time. Transactional signals derived directly from a company’s own accounting system outperformed commercial bureau products significantly.
Phase 1: Transactional scoring
The Phase 1 model replaced bureau-only scoring with behavioral signals from the counterparty’s own accounting and banking data, specifically days-beyond-term patterns across invoice history. Replacing binary classification with a weighted DBT formula improved predictiveness 80%+. That scoring ran in the Crowdz Citi proof of concept.
USPTO publication US-20220076330-A1 lists Kevin Hopkins as inventor, assignee Agora Intelligence. I built the non-linear weighted-DBT scoring used in that Citi work. An amended filing to put that formula on the application and add me as co-inventor was planned. Crowdz closed before it was submitted. The application later went abandoned.
The conceptual design is described here.
MARS architecture
After Crowdz closed I wrote a Multivariate Adaptive Regression Splines version meant to handle non-linear payment behavior and a wider variable set. Crowdz had already shut down. There was no portfolio data left to train or validate on. The Python file is a reconstructed pipeline on synthetic data so the architecture is inspectable. It is not a production model and it is not the filed, nor planned-to-file, Crowdz scoring.
Six-tier scoring architecture
| Tier 1 — Invoice Level | Company as Buyer — payment timing probability per invoice [0–200] |
| Tier 2 — PO Level | Company as Seller — fulfillment timing and accuracy per PO [0–200] |
| Tier 3 — Buyer Profile | Dollar-weighted aggregate of all invoice-level scores |
| Tier 4 — Seller Profile | Dollar-weighted aggregate of all PO-level scores |
| Tier 5 — Company Overall | Strict average of Buyer and Seller profiles |
| Tier 6 — Normalized | Overall score re-expressed within geo, industry, and size cohorts |
Score scale and outcome definition
The MARS model predicts a continuous score from 0 to 200, defined as follows:
| Score | Meaning | Context |
|---|---|---|
| 200 | Perfect early payment/fulfillment | Paid or fulfilled as early as contractually possible |
| 100 | Exactly on time | Paid or fulfilled on the due date — DBT = 0 |
| 50 | Materially late | Approximately 90 days beyond term |
| 0 | Non-payment / non-fulfillment | 180+ days beyond term or complete failure to pay/deliver |
The same scale applies to both payment scoring (Company as Buyer, predicting invoice payment timing) and fulfillment scoring (Company as Seller, predicting PO fulfillment timing and accuracy). This symmetry is intentional — it allows direct comparison across entity roles and simplifies composite construction.
Hypothesis from Crowdz, specified for MARS to test
In the Crowdz funded book, companies that self-identified an ESG focus in their own materials tracked with later payment and higher non-payment than companies that did not use that language. I did not isolate why. Bureau files do not carry this label, so it never showed up in the bureau-only scores.
Two mechanisms were on the table, untested. One: smaller, mission-led firms where ESG language tagged thin cash and weak pay discipline. Two: large buyers who used the same language in filings and still stretched small suppliers 60 to 150 days as a terms tactic. Either way the flag would be a proxy, not a moral score.
MARS was specified to test that. A linear model treats ESG as a small plus or a small minus. A hinge model can ask whether the association shows up only past a threshold, and whether it survives after DBT, size, industry, and geography are in the model. Crowdz closed before that test ran. There was no book left to train on.
If the book had stayed live, the review rule would have possibly been: ESG self-ID is a reason to look, not a reason to decline. The pay history still decides, but having a non-payment flag that can indicate helps streamline pre-check due-diligence and offerings.
Peer-relative normalization
The Company Overall Score is computed as the strict average of the Buyer Profile and Seller Profile scores. This global score is then recalculated within three normalization dimensions:
- Geographic normalization: Score percentile within companies in the same country or region — a score of 75 means something different in a high-trust payment environment versus a market where 45-day delays are structural
- Industry normalization: Score percentile within NAICS/SIC industry cohort — construction and manufacturing have different payment norms than software and professional services
- Size normalization: Score percentile within revenue band or employee count tier — micro-enterprises are not comparable to mid-market companies on absolute financial ratios
The global score is the primary output. The normalized dimensions provide interpretive context — they answer “how does this company compare to its peers” without overriding the behavioral signal. A company with a global score of 65 that ranks in the 90th percentile of its geography and industry is a meaningfully different risk than one with the same score at the 30th percentile.
What the model is not for
The normalization layers and the 75+ variable set are primarily useful for investors, competitors, and companies themselves seeking to understand their position in the market. The core predictive signal (payment probability and fulfillment probability) is simpler. MARS confirms this empirically: the payment behavior variables (DBT, delinquency history, prior transaction performance) consistently dominate the financial ratio variables in terms of predictive contribution. A company’s cash conversion cycle tells you something; how it actually paid its last 20 invoices tells you significantly more.
Implementation
A reconstructed Python implementation is available as a companion file. It includes: full variable preprocessing pipeline with missingness flag generation; PaymentScoreModel and FulfillmentScoreModel classes using py-earth’s MARS implementation; dollar-weighted invoice-to-company-profile aggregation; peer-relative normalization across geo, industry, and size cohorts; feature importance extraction; and a synthetic data example illustrating the full pipeline end-to-end.
Missingness is encoded as a binary feature throughout as absent data is semantically different from a score of zero and should not be imputed to neutral.
The scoring model
This is the core of the MARS implementation — the model class itself, plus the function that converts raw payment timing into the 0–200 score scale described above.
def invoice_score_from_timing(days_beyond_term: float, paid: bool) -> float:
"""
Convert observed payment timing to [0, 200] score.
Score interpretation:
200 = paid as early as contractually possible
100 = paid exactly on due date (DBT = 0)
0 = 180+ days late or no payment
Early payment (negative DBT) scores above 100.
The exact early-payment ceiling and late-payment floor logic
reflects the design intent without reproducing patented specifics.
Args:
days_beyond_term: actual DBT observed (negative = early)
paid: whether invoice was ever paid
Returns:
float score in [0, 200]
"""
if not paid:
return SCORE_FLOOR
if days_beyond_term <= 0:
early_bonus = min(abs(days_beyond_term), EARLY_PAYMENT_CAP_DAYS) / EARLY_PAYMENT_CAP_DAYS * 100
return clip_score(SCORE_ONTIME + early_bonus)
else:
late_penalty = min(days_beyond_term, LATE_PAYMENT_FLOOR_DAYS) / LATE_PAYMENT_FLOOR_DAYS * 100
return clip_score(SCORE_ONTIME - late_penalty)
class PaymentScoreModel:
"""
MARS model for predicting invoice-level payment likelihood.
Predicts a continuous score [0, 200] representing payment timing quality.
Uses py-earth's Earth implementation of MARS, which automatically:
- Identifies non-linear relationships via hinge functions
- Discovers interaction terms between variables
- Prunes non-contributory terms via GCV (generalized cross-validation)
Why MARS over logistic regression or tree methods:
- Payment behavior has threshold effects (5 days late is not 30 days late,
but 120 days late is approximately 150 days late in terms of risk signal)
- Industry and geography create structural breaks that must be
discovered from data, not pre-specified
- MARS hinge functions are interpretable and auditable — important
for regulatory and underwriter review
- Unlike black-box methods, MARS can explain why a score changed
"""
def __init__(self, max_degree: int = 2, max_terms: int = 50,
min_samples_leaf: int = 10):
"""
Args:
max_degree: maximum interaction degree between variables.
2 = pairwise interactions (recommended starting point)
max_terms: maximum basis functions before pruning.
Higher = more complex model, higher overfitting risk.
min_samples_leaf: minimum observations per MARS leaf node.
Higher = more conservative, less overfitting.
"""
self.max_degree = max_degree
self.max_terms = max_terms
self.min_samples_leaf = min_samples_leaf
self.model = None
self.scaler = StandardScaler()
self.feature_names = None
self.is_fitted = False
def fit(self, X: pd.DataFrame, y: pd.Series) -> 'PaymentScoreModel':
"""
Fit MARS model.
Args:
X: feature matrix, preprocessed (missingness flags added,
categoricals encoded, no NaN)
y: target scores [0, 200]
"""
self.feature_names = list(X.columns)
X_scaled = self.scaler.fit_transform(X)
self.model = Earth(
max_degree=self.max_degree,
max_terms=self.max_terms,
min_samples_leaf=self.min_samples_leaf,
allow_linear=True,
enable_pruning=True,
)
# ...fit continues in full source fileDownload the full source file (mars_payment_scoring.py, ~800 lines)