Analytics · Fintech · Python

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 derived from the counterparty’s own accounting data — specifically Days Beyond Term (DBT) patterns across their invoice history. This approach produced the insight that became the patent filing: replacing binary classification with a weighted DBT formula improved predictive accuracy 80%+.

The conceptual design and findings are described here.*

MARS architecture

After working on a patent-pending model* for a fintech firm, there were significant areas that could be improved. I made some iterative improvement to the model that resulted in an amended filing in 2023. However, the fintech ran out of money and shut down. Meanwhile, I had an idea for an entirely new model and while the framework was fresh in my mind, I put this model together using a Multivariate Adaptive Regression Splines (MARS) implementation handling 75+ variables with non-linear interactions. MARS was chosen specifically because payment behavior is not linear — the difference between 5 days late and 30 days late is meaningful; the difference between 120 days late and 150 days late is not. Standard logistic regression requires pre-specifying which interactions matter and assumes linear relationships. Both assumptions fail for SMB credit risk.

MARS discovers breakpoints from the data, fitting piecewise linear hinge functions and automatically identifying which interactions are predictive. The resulting model is more interpretable than black-box alternatives — fitted hinge functions can be inspected, explained to underwriters, and audited by regulators.

*Patent note: Specific formula implementation details are omitted per USPTO filing (provisional 2020, nonprovisional 2021, updated 2023 — US-20220076330-A1).

Six-tier scoring architecture

Tier 1 — Invoice LevelCompany as Buyer — payment timing probability per invoice [0–200]
Tier 2 — PO LevelCompany as Seller — fulfillment timing and accuracy per PO [0–200]
Tier 3 — Buyer ProfileDollar-weighted aggregate of all invoice-level scores
Tier 4 — Seller ProfileDollar-weighted aggregate of all PO-level scores
Tier 5 — Company OverallStrict average of Buyer and Seller profiles
Tier 6 — NormalizedOverall 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:

ScoreMeaningContext
200Perfect early payment/fulfillmentPaid or fulfilled as early as contractually possible
100Exactly on timePaid or fulfilled on the due date — DBT = 0
50Materially lateApproximately 90 days beyond term
0Non-payment / non-fulfillment180+ 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.

Notable finding: ESG self-identification

One of the more striking MARS findings: companies that self-identified an ESG focus showed systematically worse payment behavior (later payments, higher rates of non-payment) than companies that did not. The ESG flag became a negative predictor in the model.

The likely mechanism: ESG self-identification in this dataset correlated either with early-stage, mission-driven companies that were undercapitalized, had loose financial discipline, and prioritized stakeholder optics over operational rigor. The ESG signal was proxying for cash flow problems and management priorities misaligned with creditor obligations; or signified large industry leaders using ESG as a way to bolster large shareholder enthusiasm and typically showed signs of lateness as a bully-tactic with smaller companies, using their industry leading position as a “you’re lucky to have our business at all” position to intentionally delay payments to often 2-5 months beyond term.

This is exactly the kind of finding that bureau models cannot surface (they do not have this variable or the data to link to it) and that MARS catches because it looks for non-linear threshold effects rather than assuming every variable contributes linearly and positively. An ESG company isn’t slightly worse; it’s categorically different.

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 file

Download the full source file (mars_payment_scoring.py, ~800 lines)