Skip to content
All insights AI for credit & lending operations

Generating explainable adverse-action reasons at scale

Every decline owes a specific, accurate reason. Here is how we generate ECOA-compliant adverse-action reasons from complex models, reviewably.

Financial services professionals working through an AI initiative

Every declined application owes the applicant the specific principal reasons behind the decision, and under ECOA and Regulation B those reasons have to reflect what the model actually did. The reliable way to produce them at scale is a deterministic pipeline: compute per-decision feature contributions, rank them against a reference population, map the top contributions to an approved reason-code taxonomy, and store the chain as an audit record.

The hard part is not producing a sentence. Any model can produce a sentence. The hard part is producing a reason that is true for this applicant, at this score, on the model version that ran, and being able to prove it a year later when a regulator or the applicant’s lawyer asks how the number was reached.

Where the reason has to come from

A reason code is a claim about causation inside a specific decision. If your underwriting model is a gradient-boosted ensemble or a neural net, there is no coefficient to read off. You attribute.

We compute per-decision contributions with a method whose behavior we can defend: SHAP for tree ensembles, or integrated gradients for networks, both against a fixed reference population that represents approved applicants. The reference choice matters more than teams expect. A reason is always relative to a baseline, and “why were you declined relative to the average approved applicant” is a different question from “why were you declined relative to all applicants.” Pick one, write it down, and keep it stable across model versions so reasons stay comparable quarter to quarter.

Two failure modes show up immediately:

  • The top contribution by magnitude is a feature the applicant cannot act on, or one that is a proxy the reason taxonomy does not permit surfacing. You need a filtering and ranking layer between raw attributions and the reason the applicant sees.
  • The attribution is unstable. Small perturbations to inputs reorder the top features, which means two near-identical applicants get different stated reasons. We test for this directly by perturbing inputs within measurement noise and checking that the top-k reason set is stable. If it is not, the reason is not defensible and the mapping needs to collapse those features into a coarser, stable code.

The mapping layer, and why it is deterministic

Between attribution and disclosure sits a mapping from model features to a fixed reason-code taxonomy. This layer is deterministic code with tests, not a model, and that is a deliberate choice.

Each internal feature, or group of features, maps to one approved reason code. Debt-to-income ratio, revolving utilization, and the count of recent inquiries each land on a code the compliance function has reviewed and signed off. When several raw features express the same underlying reason, they collapse to one code so the applicant does not receive three variations of “too much existing debt.” The pipeline then ranks the codes by summed contribution and returns the top principal reasons, typically up to four.

Determinism here earns its keep under examination. Given the stored inputs and the pinned model version, the exact reason set recomputes byte-for-byte, with no sampling temperature and no drift between two runs of the same case. A compliance analyst can read the mapping table and the ranking rule and know precisely what any applicant would be told, without wading into model internals. And the stored record links the score, the model and pipeline versions, the raw contributions, the mapped codes, and the final rendered text, so lineage runs unbroken from a feature value to the sentence in the notice.

If you want natural-language phrasing beyond the fixed code text, a language model can render the approved code into fluent prose. But it renders a reason that has already been fixed. It does not choose the reason. The moment a generative model decides which reason to give, you have reintroduced the exact problem the mapping layer exists to remove: a confident sentence whose factual basis you cannot reconstruct.

Point-in-time correctness and the audit record

The reason has to describe the decision that was actually made, which means it is bound to a moment. Models get retrained. Feature pipelines change definitions. The reference population shifts. If you recompute a reason next quarter against the current model, you may generate a reason that never applied to the applicant, because the model that declined them no longer exists in production.

So we compute reasons at decision time, against the model version and feature values that produced the score, and store them. The record we keep for each decision includes:

  • The scored inputs as they were at decision time, with their point-in-time values, so no later restatement of a data source silently changes the answer.
  • The model and mapping-code versions, pinned by hash.
  • The raw per-feature contributions and the reference baseline used.
  • The ranked, mapped reason codes and the final rendered text sent to the applicant.

This record is what makes the pipeline auditable rather than merely functional. It also feeds monitoring. Reason distributions drift, and that drift is a signal. If the share of declines citing recent-inquiry counts jumps between two model versions, either the population changed or the model started leaning on that feature harder, and both are worth a look before someone else notices for you. Tracking the reason mix over time is cheaper than discovering at quarter-end that a retrain quietly changed why a whole segment gets declined.

The test that a reason-generation system is working is boring and specific: pull any past decline, recompute from the stored record, and get exactly the reasons the applicant received. If you cannot do that for every decision, you do not have adverse-action reasons at scale. You have plausible sentences, and under ECOA those are a liability, not a feature.

FAQ

Can we use SHAP values directly as adverse-action reasons?

Not without a mapping layer. SHAP tells you which features pushed a score down, but a raw feature name is not a consumer-readable reason, and the top SHAP feature is not always the top actionable reason. You need a deterministic step that ranks contributions and maps them to an approved reason-code taxonomy.

How many reason codes does ECOA require on a decline?

Regulation B does not fix a number, but the specific principal reasons for the adverse action must be disclosed. In practice most lenders return up to four, and the CFPB has been explicit that generic or checklist-style reasons are not sufficient when the actual basis is more specific.

Do reasons need to be generated at decision time or can they be produced later?

Produce them at decision time and store them. Recomputing a reason weeks later against a retrained model or a changed feature pipeline risks a reason that no longer matches the score that actually declined the applicant.

Working on something similar?

Tell us about your data and the workflow around it, and we will give you a straight read.

Book a 30-min intro call