1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
//! Population Stability Index (PSI).
use ;
use crateHistogram;
use crateResult;
/// Population Stability Index between a `reference` and `live` histogram.
///
/// PSI is the workhorse drift metric from credit-risk modeling:
///
/// ```text
/// PSI = Σ_bin (live_pct − ref_pct) · ln(live_pct / ref_pct)
/// ```
///
/// Both distributions are epsilon-smoothed (see
/// [`DEFAULT_EPSILON`](crate::metrics::DEFAULT_EPSILON)) first, so an empty bin
/// never yields `inf`/`NaN`.
///
/// # Threshold guidance
///
/// These commonly-cited bands are **convention, not mathematical fact** — treat
/// them as a starting point and calibrate to your own data:
///
/// | PSI | Interpretation |
/// |--------------|----------------------------|
/// | `< 0.10` | no significant change |
/// | `0.10–0.25` | moderate change |
/// | `> 0.25` | significant change |
///
/// # Errors
/// Returns [`DriftError::BinCountMismatch`](crate::DriftError::BinCountMismatch)
/// if the two histograms have different bin counts.
///
/// # Example
/// ```
/// use driftwatch::{psi, Histogram, BinDefinition};
///
/// let bins = BinDefinition::Continuous { edges: vec![0.0, 1.0, 2.0] };
/// let reference = Histogram::new(bins.clone(), vec![50.0, 50.0]).unwrap();
/// let live = Histogram::new(bins, vec![50.0, 50.0]).unwrap();
/// assert!(psi(&reference, &live).unwrap() < 1e-9); // identical → ~0
/// ```
/// [`psi`] with an explicit epsilon smoothing constant.
///
/// # Errors
/// Same as [`psi`], plus
/// [`DriftError::InvalidConfig`](crate::DriftError::InvalidConfig) if `epsilon`
/// is negative or non-finite.