Credit Scorecard Validation: KS, Gini and ROC Explained with Python

Credit Scorecard Validation: KS, Gini and ROC Explained (with Python)

A scorecard can post a strong Gini on its development sample and still disappoint in production, which is why credit scorecard validation matters. Often the cause is one of three things: it ranks borrowers well but predicts the wrong default rates, the applicant population has drifted since it was built, or it was only ever tested on the data used to build it.

Credit scorecard validation is how you catch these problems before the portfolio does. In short, you test discrimination (KS, Gini, AUC), calibration, rank ordering and stability (PSI, CSI) on data the model has not seen. Because portfolios change, you also repeat the tests as the book ages.

This guide explains each metric from first principles, so you can read every number with confidence. It also works through all of them in Python on synthetic data.

What is credit scorecard validation?

Credit scorecard validation is the process of testing whether a scorecard does its job. Specifically, it checks that the scorecard ranks borrowers by risk, predicts default rates close to what actually happens, and stays stable as the applicant population changes. It happens at development, in independent review and through ongoing monitoring.

Validation is not a single test run once. Instead, it is a set of checks that answer different questions at different points in a scorecard’s life:

  • Development validation. The modeling team checks whether the scorecard still works on data it was not fitted on, using a holdout sample, an out-of-time sample, KS, Gini and a decile table.
  • Independent validation. A team separate from development reviews design, data and performance, so the bank can be sure the scorecard is sound and fit for its intended use. It also replicates results, checks calibration and reviews documentation.
  • Ongoing monitoring. The model owner or risk team tracks PSI, CSI, KS, Gini and bad rate by score band, because population or performance can move after launch.

How often each stage runs depends on the bank’s model risk policy, although the materiality of the scorecard also matters. For the governance and regulatory side, see our guide to credit risk validation methods. This article, however, focuses on the statistical tests specific to scorecards.

Discrimination vs calibration: what KS and Gini can’t tell you

Discrimination is how well a scorecard separates future defaulters from non-defaulters, whereas calibration is how closely its predicted default rates match observed ones. KS, Gini and AUC, however, measure discrimination only. They depend on the order of scores, so a scorecard can rank perfectly and still predict the wrong default rates.

Why a high Gini can still mislead

Here is why. Take any scorecard and double every predicted PD. The order of borrowers does not change, so AUC, Gini and KS stay exactly the same. However, the predicted default rates are now twice as high.

As a result, a scorecard can have an excellent Gini and still be badly wrong about levels. For example, a score band might predict a 2% default rate while 4% of its borrowers actually default. The ranking may be flawless and the Gini unchanged, yet anything built on that 2% is understated.

Where calibration matters

Calibration matters in three places:

  • Pricing and provisioning. If the scorecard’s output feeds a PD used for risk-based pricing or an expected credit loss calculation, the level of the estimate matters rather than just the order. See our worked example of an ECL model in Python under IFRS 9 / Ind AS 109.
  • Cut-off decisions. Approve/decline cut-offs are set using the bad rate expected at each score, so a shift in that bad rate changes what the same cut-off produces.
  • A stable Gini can hide a drift in default levels, especially when only discrimination is tracked.

Calibration, on the other hand, is checked by comparing the average predicted default rate with the observed rate in each score band. KS, Gini and AUC answer the ranking question, so credit scorecard validation should always report calibration alongside them. Our scorecard course covers model validation and performance metrics end to end.

ROC curve, AUC and Gini

The ROC curve plots the share of defaulters a scorecard catches (true positive rate) against the share of non-defaulters it wrongly flags (false positive rate) at every possible cut-off. AUC is the area under that curve. Gini = 2 × AUC − 1 rescales AUC, so 0 means no ranking power and 1 means perfect ranking. AUC and Gini are the most common discrimination measures in credit scorecard validation.

How to read a ROC curve

Treat “bad” (default) as the positive class, and flag every account below a chosen score cut-off as a predicted bad. At that cut-off:

  • True positive rate = bads flagged ÷ all bads
  • False positive rate = goods flagged ÷ all goods

For example, with 100 bads and 900 goods in a sample, a cut-off that flags 60 bads and 200 goods gives a true positive rate of 60% and a false positive rate of 22.2%. Moving the cut-off from the lowest score to the highest traces the curve.

A scorecard with no ranking power follows the diagonal, while a better one bows toward the top-left corner. AUC also has a plain-language meaning: it is the probability that a randomly chosen bad account scores as riskier than a randomly chosen good one, with ties counted as half. Therefore, an AUC of 0.75 means the bad account is ranked riskier in 75% of such pairs.

From AUC to Gini

The relationship is exact: Gini = 2 × AUC − 1. As a result, an AUC of 0.50 gives a Gini of 0, an AUC of 0.70 gives 0.40, an AUC of 0.75 gives 0.50, and an AUC of 1.00 gives 1.

Three practical points apply:

  • Check the scale. Gini is reported as 0 to 1 in some places and 0 to 100 in others, so confirm which one you are comparing.
  • Do not mix up the Gini measures. The credit Gini is not the income-inequality Gini coefficient, and it is not the Gini impurity used in decision trees. Some reports also call it the accuracy ratio.
  • Treat benchmarks as rules of thumb. Application scorecards often land at an AUC of 0.70–0.80, which is a Gini of 0.40–0.60, whereas behavioural scorecards usually score higher. What counts as good varies by product, data availability and market. Moreover, a very high figure, such as a Gini above 0.90, should trigger a check for target leakage before anyone celebrates.

KS statistic and the decile table

The KS statistic is the maximum vertical gap between the cumulative distribution of bad accounts and the cumulative distribution of good accounts when both are ordered by score. Therefore, a larger KS means the scorecard separates the two groups better. It runs from 0 to 1, or from 0 to 100 when expressed as a percentage. KS is the other core discrimination measure in credit scorecard validation.

Reading KS from a decile table

  1. Sort accounts from riskiest score to safest.
  2. Split them into ten equal-sized groups (deciles).
  3. Count goods and bads in each decile.
  4. Compute the cumulative percentage of all bads and of all goods up to each decile.
  5. Finally, take the largest difference between the two cumulative percentages. That is the KS.

The example below is illustrative, so none of its figures are real portfolio data: it uses 10,000 accounts, 500 bads (a 5.0% overall bad rate) and 9,500 goods, split into deciles of 1,000 accounts each. The chart shows the bad rate in each decile on the left and the cumulative curves on the right.

By decile 4, the scorecard has captured 80.4% of bads but only 37.9% of goods, so the largest gap is 42.5 points and KS ≈ 42.5%. By common retail rules of thumb, that is solid, although benchmarks differ by product and market.

The decile where the gap peaks also tells you something practical. Here, the riskiest 40% of accounts contain about 80% of the bads but only about 38% of the goods. That is a useful reference point for setting a cut-off. However, a business cut-off must also weigh approval rate, profitability and risk appetite, so the KS peak is not automatically the right place to cut.

Rank ordering and lift

A decile table also tests rank ordering: the bad rate should fall at every step from the riskiest decile to the safest. In the chart above it does, falling from 17.0% in decile 1 to 0.7% in decile 10. If the bad rate rises in a later decile, the scorecard is mis-ranking part of the population, even when KS looks healthy.

Lift shows how concentrated the risk is. For example, decile 1 has a 17.0% bad rate against 5.0% overall, which is a lift of 3.4×. Lift is simply a decile’s bad rate divided by the overall bad rate.

One limitation remains: KS looks at a single point on the curve, whereas Gini uses the whole curve. As a result, two scorecards can share a KS and still differ in shape, which is why the metrics are reported together.

KS vs Gini vs AUC vs PSI: which metric answers which question?

In credit scorecard validation, KS, Gini and AUC test ranking power on outcomes, so they tell you how well a scorecard separates goods from bads. PSI and CSI, on the other hand, test stability by showing whether the population being scored has changed. None of them tests calibration, so report predicted against observed default rates alongside them.

MetricWhat it measuresUse it whenMain limitation
KSLargest gap between cumulative bad and good distributionsYou want a quick read of separation or a region for a cut-offLooks at one point only; depends on binning; ignores calibration
AUCChance a random bad scores riskier than a random goodYou compare models on one number across all cut-offsAverages over cut-offs you may never use; ignores calibration
GiniAUC rescaled so 0 is random and 1 is perfectYou report ranking power in the usual industry shorthandSame limits as AUC; scale varies (0–1 or 0–100)
PSIShift in a distribution, usually the score, between two samplesYou monitor whether the scored population has moved; needs no outcomesDoes not show whether performance worsened; sensitive to binning
CSIThe same shift measure for each input variableYou need to find which variable drove a PSI moveSame as PSI; many variables to track

 

PSI and CSI: how to monitor scorecard stability

The Population Stability Index (PSI) compares the score distribution today with the distribution at development, while the Characteristic Stability Index (CSI) applies the same formula to each input variable. Generally, a PSI below 0.10 is read as stable, 0.10–0.25 as a shift worth investigating, and above 0.25 as a significant shift. Stability checks complete credit scorecard validation by showing whether the scored population still resembles the development population.

How to calculate PSI

PSI = Σ (Actual%ᵢ − Expected%ᵢ) × ln(Actual%ᵢ / Expected%ᵢ)

Here, Expected% is the share of the development population in bin i, whereas Actual% is the share of the current population in the same bin. To compute it:

  1. Fix the bin edges on the development sample, typically score deciles.
  2. Calculate the share of accounts in each bin for the development and current samples.
  3. Finally, apply the formula to each bin and sum the results.

The 0.10 and 0.25 cut-offs are conventions associated with Siddiqi’s work on credit scorecards, as summarised in a survey of R packages for credit scoring. They are rules of thumb rather than statistical tests.

How to use PSI and CSI in practice

Four points are worth keeping in mind:

  • PSI needs no outcomes. It can be computed as soon as new applications are scored, so it works as an early warning, whereas Gini and KS must wait until outcomes mature.
  • PSI says the population moved, not that performance fell. A population can shift while ranking stays intact, and it can also stay stable while the link between inputs and default changes.
  • Binning matters. Use the development-sample bin edges and handle empty bins with a small floor. Also remember that the number of bins changes the result.
  • Monitor inputs as well as the score. A stable score can hide offsetting shifts in inputs, and several small input shifts can compound into a material score shift. CSI therefore helps you find the cause. Many teams compute PSI monthly or quarterly, but your monitoring policy should set the cadence.

Split-sample, out-of-time and bootstrap validation

Split-sample validation tests the scorecard on a random holdout from the development window, while out-of-time validation tests it on a different period, which is closer to real use. Bootstrapping, in addition, resamples the data many times to put a confidence interval around Gini or KS. Strong credit scorecard validation therefore uses more than one.

Split-sample (holdout)

Fit the scorecard on one part of the data and test it on the rest, for example with a 70/30 split. This catches overfitting. However, both parts come from the same period, so a holdout cannot show whether the scorecard survives a change in the economy or the applicant mix.

Out-of-time (OOT)

An out-of-time sample contains observations from a period outside the development window, so it is the most realistic stability test. Because the bad definition needs time to mature, the accounts must have been observed long enough, and the right window depends on the product, the default definition and data volume.

Bootstrapping

Bootstrapping draws many samples with replacement and recomputes Gini or KS each time, so you can report a range instead of a single value. It is especially useful when bads are few. Moreover, it shows whether a gap between two samples, or between two scorecards, is real or just noise.

For this reason, judge results against intervals rather than single values, since a Gini that moves by a point or two may well be within sampling noise.

Worked example: validating a scorecard in Python (synthetic data)

This example of credit scorecard validation in Python builds a simple logistic scorecard on synthetic applicants and then runs the tests above on a holdout sample and an out-of-time sample. Because the data is simulated, no real bank data is used. Moreover, the out-of-time sample deliberately includes a drifted population, so you can see what each metric catches and what it misses.

Step 1: Simulate applicants and fit the scorecard

The full script simulates 40,000 development applicants and 15,000 out-of-time applicants. Specifically, the out-of-time group has lower average income, higher utilisation and a higher base default rate. Although it is simple, a logistic regression on three inputs (income, utilisation and past delinquencies) is fitted on 70% of the development data and scored on the rest.

Step 2: Define the validation metrics

Gini comes from AUC and KS comes from the ROC curve, while the decile table and PSI are built by hand.

import numpy as np

import pandas as pd

from sklearn.metrics import roc_auc_score, roc_curve

 

def gini(y, p):

return 2 * metrics.roc_auc_score(y, p) – 1

 

def ks(y, p):

fpr, tpr, _ = metrics.roc_curve(y, p)

return np.max(tpr – fpr)

 

def decile_table(y, p):

df = pd.DataFrame({“y”: y.values, “p”: p.values})

df[“decile”] = pd.qcut(df[“p”].rank(method=”first”, ascending=False),

10, labels=range(1, 11))

t = df.groupby(“decile”, observed=True).agg(

accounts=(“y”, “size”), bads=(“y”, “sum”), avg_pd=(“p”, “mean”))

goods = t[“accounts”] – t[“bads”]

rate = t[“bads”] / t[“accounts”]

cum_bad = t[“bads”].cumsum() / t[“bads”].sum()

cum_good = goods.cumsum() / goods.sum()

return t.assign(goods=goods, bad_rate=rate, cum_bad=cum_bad,

cum_good=cum_good, gap=cum_bad – cum_good)

 

def psi(expected, actual, bins=10):

edges = np.unique(np.quantile(expected, np.linspace(0, 1, bins + 1)))

edges[0], edges[-1] = -np.inf, np.inf

e = np.clip(np.histogram(expected, edges)[0] / len(expected), 1e-4, None)

a = np.clip(np.histogram(actual, edges)[0] / len(actual), 1e-4, None)

return float(np.sum((a – e) * np.log(a / e)))

 

Step 3: Run the tests

# train, test, oot and FEATURES come from Step 1 (see the full script)

 

for name, d in [(“Test”, test), (“OOT”, oot)]:

print(name, “Gini”, round(gini(d[“default”], d[“pd_hat”]), 3),

“KS”, round(ks(d[“default”], d[“pd_hat”]), 3))

print(decile_table(test[“default”], test[“pd_hat”]).round(3))

print(“Score PSI:”, round(psi(train[“pd_hat”], oot[“pd_hat”]), 3))

for f in FEATURES:

print(“CSI”, f, round(psi(train[f], oot[f]), 3))

 

Reading the results

Running the full script gives the results below, and they are specific to this simulation.

  • Discrimination held. Gini is 0.500 on train, 0.483 on holdout and 0.500 out-of-time, while KS is 37.3%, 36.3% and 37.4% respectively. The holdout bootstrap 95% interval for Gini is 0.450–0.518, so the small gap between train and holdout is consistent with sampling noise rather than material overfitting.
  • Calibration did not hold. On the out-of-time sample, the average predicted PD is 9.63%, but the observed bad rate is 12.43%. The scorecard under-predicts in every decile, even though ranking is intact. For example, the riskiest decile is predicted at 30.6% but defaults at 38.0%, whereas the safest is predicted at 1.5% and defaults at 1.6%.
  • Stability flagged the shift. Score PSI is 0.154, which falls in the “investigate” band. Income CSI is 0.085, utilisation CSI is 0.091 and delinquencies CSI is 0.000. Neither drifting input crosses 0.10 on its own, yet the score PSI does, because both shifts push risk in the same direction.
  • Decile KS is slightly lower than exact KS. The decile table gives 35.1% against an exact 36.3% on the holdout, because ten bins can miss the true peak. Therefore, use the exact calculation when you can.

What this means for the next step

The pattern matches the one described earlier: ranking is intact, but levels are off. A Gini-only review would therefore have passed this scorecard, whereas the calibration check would have flagged it. That points to recalibration, which the next section covers.

If you want to practise this workflow with instructor feedback, the Advanced Certificate in Credit Scorecard Modeling covers scorecard validation hands-on in Python.

Redevelop or recalibrate? A decision framework

Recalibrate when the scorecard still ranks risk well but its predicted default rates have drifted, because you only need to adjust the mapping from score to PD. Redevelop when ranking power has fallen, the data or definitions have changed, or the population has moved enough that the original variables and bins no longer fit.

Signals that point to each decision

  • Level drift with ranking intact. When Gini and KS stay close to development levels but observed default rates sit consistently above or below predictions across score bands, recalibrate by re-estimating the intercept or the score-to-PD mapping.
  • Ranking power lost. When Gini or KS fall materially below development levels, or bad rates stop falling steadily across score bands, redevelop instead by refitting, rebinning and revisiting variables.
  • Population shift. When score PSI exceeds 0.25 and CSI points to specific variables, investigate first, because the right action depends on whether ranking has fallen. Redevelop if it has, recalibrate if only levels moved, and consider segmenting.
  • Inputs or target changed. When new bureau data, a policy or product change, or a new bad definition arrives, redevelop in most cases, since the original bins and weights no longer describe the data.
  • Possibly transient. When PSI or performance moves in a single period, for example because of seasonality or a one-off event, keep monitoring rather than rebuilding on one data point.

Credit scorecard validation ends with this decision, and what counts as “materially” below is set by your model risk policy, usually through tolerance bands and escalation triggers, so there is no universal threshold. Moreover, recalibration cannot repair weak ranking. Any recalibrated or redeveloped scorecard also goes back through validation and governance approval.

Common credit scorecard validation mistakes

Most validation failures come down to six avoidable mistakes:

  • Treating development-sample Gini as proof. In-sample metrics are optimistic, so report holdout and out-of-time results.
  • Relying on a random holdout alone. It cannot reveal drift, so add an out-of-time sample.
  • Reporting discrimination without calibration. Instead, compare predicted and observed default rates by score band.
  • Watching only the score. Track CSI on the inputs as well, because offsetting or compounding shifts can hide in a score-level PSI.
  • Validating before outcomes mature. Until the performance window is complete, the observed bad rate is understated and the metrics are unreliable.
  • Celebrating a very high Gini or KS. Overly high values often signal target leakage, especially when an input already encodes the outcome.

Conclusion

Credit scorecard validation comes down to four questions. First, does the scorecard rank risk (KS, Gini, AUC, decile table)? Second, are its predicted default rates right (calibration)? Third, is the population it scores stable (PSI, CSI)? Finally, does it hold up on data it has not seen, including out-of-time data? Answer all four, repeat them as the book ages, and let the answers decide whether to keep, recalibrate or redevelop.

For the governance side, read our guide to credit risk validation methods. Likewise, if you are weighing scorecards against rating models for a portfolio, see credit rating model vs credit scoring.

To learn credit scorecard validation under instructor guidance, explore the Advanced Certificate in Credit Scorecard Modeling. The course is live and instructor-led, and it also uses hands-on work in Python. In addition, it covers KS, Gini, ROC curves, lift charts, performance monitoring and when to redevelop versus recalibrate.

Sources and further reading

  • PSI thresholds attributed to Siddiqi (2006), as summarised in “An Overview on the Landscape of R Packages for Credit Scoring”: org/pdf/2006.11835
  • MathWorks documentation for scorecard validation (KS, AUROC and accuracy ratio outputs): com

Frequently asked questions

How do you calculate Gini for a credit scorecard?

Gini = 2 × AUC − 1. To get it, score each account, treat default as the positive class, compute the area under the ROC curve (AUC), and rescale it. In Python, 2 * roc_auc_score(y, pd_hat) – 1 does it. However, check whether your report uses a 0-1 or 0-100 scale before comparing results.

What is a good KS for a credit scorecard?

There is no universal cut-off, but a KS above about 30% is commonly treated as acceptable for retail scorecards, and 40% or more as strong. Benchmarks vary by product, data and market, so compare against your own history. An unusually high KS, moreover, should prompt a check for target leakage.

What is a good Gini for a credit scorecard?

Application scorecards commonly show a Gini of roughly 0.40 to 0.60, which is equivalent to an AUC of 0.70 to 0.80. Behavioural scorecards often score higher because they use repayment history. Treat these as rules of thumb, and be wary of very high values, since they often signal leakage.

What PSI value is acceptable?

By convention, a PSI below 0.10 indicates a stable population, 0.10 to 0.25 suggests a shift worth investigating, and above 0.25 signals a significant shift. These are rules of thumb rather than statistical tests, and results depend on how the score is binned. Therefore, read PSI alongside performance, never alone.

Can a scorecard have a high Gini and still be wrong?

Yes. Gini measures ranking only, so a scorecard can order borrowers correctly while predicting default rates that are too low or too high. It can also look strong on development data but weaken out of time. For this reason, credit scorecard validation should check calibration by score band and use a later sample.

When should I redevelop rather than recalibrate a scorecard?

Redevelop when ranking power has fallen materially, rank ordering breaks, inputs or the default definition have changed, or the population has shifted so far that the original variables no longer fit. Recalibrate when ranking holds but predicted default rates are consistently off. Your model risk policy, however, sets the tolerances.

 

Learn to build and validate ECL models

A small script shows the mechanics. Production ECL work needs PD, LGD and EAD development, scenario design and validation done properly. If you want structured, instructor-led training on this, see DexLab’s Advanced Certificate in IFRS 9 Modeling


.

November 5, 2026 12:45 pm Published by

, , , , , , , , , , , , , ,

Comments are closed here.

...

Call us to know more

×