Skip to contents

Two effect-size summaries of how far the distribution of a numeric column has moved from a reference sample. Both samples are binned on the reference's quantiles and the two sets of bin shares are compared. Unlike a p-value they do not grow with the sample size, so they say how large a shift is rather than whether it is detectable. cpsdrft() reports both for every numeric column.

Usage

drftpsi(x, y, bins = 10L, eps = 1e-06)

Arguments

x, y

Numeric vectors, the reference and the new sample. Binned on the quantiles of x, so the reference defines the bins.

bins

Whole number, default 10: the number of quantile bins of the reference. Fewer bins are used when the reference has tied quantiles.

eps

Floor applied to empty bins in the PSI (default 1e-6), any value in (0, 1). An empty bin contributes \((p - eps)\log(p/eps)\) to the index, so the floor is part of the statistic: the default reports the raw index, and a larger floor (0.01, 0.05) smooths an empty bin toward that share, which dampens the PSI. Compare runs only at one eps.

Value

A named length-2 numeric: psi (non-negative, unbounded) and jsdiv (in nats, between 0 and \(\log 2 \approx 0.693\)). It has class c("rmbl_drftpsi", "rmbl_measure", "numeric"), so it prints as a box with its inputs (kept in attribute "lakner"); arithmetic, [, c() and as.numeric() give plain numbers.

Details

Binning: values are rounded to 12 significant digits (so a formatting difference between two releases does not move tied values across an edge), the bins + 1 quantiles of x are computed, duplicates are removed, and the interior quantiles become right-closed cut points with open ends at \(\pm\infty\). When the reference has fewer than three distinct quantiles (a constant or nearly constant column), every distinct quantile is a cut point instead. The bin shares \(p_i\) (reference) and \(q_i\) (new) are the proportions of each sample in each bin.

The population stability index is \(\sum_i (p_i - q_i)\log(p_i/q_i)\) – the symmetrised Kullback-Leibler divergence of the two discrete distributions. The conventional reading, from credit-risk monitoring where it originates, is that below 0.1 is stable, 0.1 to 0.25 warrants a look, and above 0.25 is a material shift.

The Jensen-Shannon divergence is \(\tfrac12 KL(p\|m) + \tfrac12 KL(q\|m)\) with \(m\) the mixture \((p+q)/2\). Unlike PSI it is bounded – by \(\log 2\) in nats – so it is comparable across columns with different numbers of bins, and it is finite even when a category is absent from one side.

Empty bins are floored at eps for the PSI only, since \(\log(0)\) would otherwise send it to infinity on a single missing category.

Missing values are dropped. Errors: non-numeric or infinite input, an empty sample, bins not a whole number from 2 to 1,000,000, eps not strictly between 0 and 1. The PSI bands quoted above are heuristics with no calibrated null distribution; on a few hundred rows binning noise alone can exceed 0.25 (see psiminn in cpsdrft()).

References

Wu D, Olson DL (2010). Enterprise risk management: coping with model risk in a large bank. Journal of the Operational Research Society 61(2), 179–190. doi:10.1057/jors.2008.144

Lin J (1991). Divergence measures based on the Shannon entropy. IEEE Transactions on Information Theory 37(1), 145–151. doi:10.1109/18.61115

See also

Examples

set.seed(2)
ref <- stats::rnorm(500)

# Same distribution: both indices near zero.
drftpsi(ref, stats::rnorm(500))
#> ╭── Population stability index ──────────────────────────────────────────╮
#> │   Samples  500 and 500 values                                          │
#> │   Bins     10, on the reference's quantiles                            │
#> │                                                                        │
#> │   statistic  value                                                     │
#> │   ---------  --------                                                  │
#> │   psi        0.02207                                                   │
#> │   jsdiv      0.002753                                                  │
#> │                                                                        │
#> │   PSI 0.02207: by the usual bands, stable.                             │
#> │   The JS divergence is 0.4% of its largest possible value.             │
#> │                                                                        │
#> │   PSI below 0.1 is read as stable, 0.1 to 0.25 as worth a look, above  │
#> │   0.25 as a material shift: heuristic bands with no null distribution, │
#> │   which binning noise alone can cross on a few hundred rows. The JS    │
#> │   divergence is bounded by log 2 (0.693).                              │
#> ╰────────────────────────────────────────────────────────────────────────╯

# A shift both indices register.
drftpsi(ref, stats::rnorm(500, mean = 1))
#> ╭── Population stability index ──────────────────────────────────────────╮
#> │   Samples  500 and 500 values                                          │
#> │   Bins     10, on the reference's quantiles                            │
#> │                                                                        │
#> │   statistic  value                                                     │
#> │   ---------  ------                                                    │
#> │   psi        0.9543                                                    │
#> │   jsdiv      0.1053                                                    │
#> │                                                                        │
#> │   PSI 0.9543: by the usual bands, a material shift.                    │
#> │   The JS divergence is 15.2% of its largest possible value.            │
#> │                                                                        │
#> │   PSI below 0.1 is read as stable, 0.1 to 0.25 as worth a look, above  │
#> │   0.25 as a material shift: heuristic bands with no null distribution, │
#> │   which binning noise alone can cross on a few hundred rows. The JS    │
#> │   divergence is bounded by log 2 (0.693).                              │
#> ╰────────────────────────────────────────────────────────────────────────╯

# A sample compared with itself has moved nowhere at all.
drftpsi(ref, ref)
#> ╭── Population stability index ──────────────────────────────────────────╮
#> │   Samples  500 and 500 values                                          │
#> │   Bins     10, on the reference's quantiles                            │
#> │                                                                        │
#> │   statistic  value                                                     │
#> │   ---------  -----                                                     │
#> │   psi        0                                                         │
#> │   jsdiv      0                                                         │
#> │                                                                        │
#> │   PSI 0: by the usual bands, stable.                                   │
#> │   The JS divergence is 0.0% of its largest possible value.             │
#> │                                                                        │
#> │   PSI below 0.1 is read as stable, 0.1 to 0.25 as worth a look, above  │
#> │   0.25 as a material shift: heuristic bands with no null distribution, │
#> │   which binning noise alone can cross on a few hundred rows. The JS    │
#> │   divergence is bounded by log 2 (0.693).                              │
#> ╰────────────────────────────────────────────────────────────────────────╯

# The Jensen-Shannon divergence is bounded by log(2), whatever the
# shift, which is what makes it comparable across columns.
drftpsi(c(1, 1, 1), c(9, 9, 9))[["jsdiv"]] <= log(2)
#> [1] TRUE

# a coarser binning, and a larger floor that damps an empty bin
drftpsi(ref, stats::rnorm(500, mean = 1), bins = 4)
#> ╭── Population stability index ──────────────────────────────────────────╮
#> │   Samples  500 and 500 values                                          │
#> │   Bins     4, on the reference's quantiles                             │
#> │                                                                        │
#> │   statistic  value                                                     │
#> │   ---------  -------                                                   │
#> │   psi        0.6932                                                    │
#> │   jsdiv      0.08235                                                   │
#> │                                                                        │
#> │   PSI 0.6932: by the usual bands, a material shift.                    │
#> │   The JS divergence is 11.9% of its largest possible value.            │
#> │                                                                        │
#> │   PSI below 0.1 is read as stable, 0.1 to 0.25 as worth a look, above  │
#> │   0.25 as a material shift: heuristic bands with no null distribution, │
#> │   which binning noise alone can cross on a few hundred rows. The JS    │
#> │   divergence is bounded by log 2 (0.693).                              │
#> ╰────────────────────────────────────────────────────────────────────────╯
drftpsi(ref, stats::rnorm(500, mean = 1), eps = 0.01)
#> ╭── Population stability index ──────────────────────────────────────────╮
#> │   Samples  500 and 500 values                                          │
#> │   Bins     10, on the reference's quantiles                            │
#> │                                                                        │
#> │   statistic  value                                                     │
#> │   ---------  ------                                                    │
#> │   psi        0.9233                                                    │
#> │   jsdiv      0.1062                                                    │
#> │                                                                        │
#> │   PSI 0.9233: by the usual bands, a material shift.                    │
#> │   The JS divergence is 15.3% of its largest possible value.            │
#> │                                                                        │
#> │   PSI below 0.1 is read as stable, 0.1 to 0.25 as worth a look, above  │
#> │   0.25 as a material shift: heuristic bands with no null distribution, │
#> │   which binning noise alone can cross on a few hundred rows. The JS    │
#> │   divergence is bounded by log 2 (0.693).                              │
#> ╰────────────────────────────────────────────────────────────────────────╯

try(drftpsi(ref, ref, bins = 1))
#> Error : `bins` must be one whole number of at least 2