regression_diagnostics/residuals/qq_plot_data.rs
1use ndarray::ArrayView1;
2use statrs::distribution::{ContinuousCDF, Normal};
3
4/// Theoretical-vs-sample quantile pairs for a normal QQ plot of `residuals`.
5///
6/// For sorted residuals, observation `i` (1-indexed) is paired with the standard
7/// normal quantile at plotting position `(i − 0.5) / n`. The returned pairs are
8/// `(theoretical_quantile, sample_quantile)`, sorted ascending by sample value —
9/// ready to scatter.
10///
11/// This crate produces the *data*, not the rendering. The natural pairing is with
12/// [`plotters-statistical`](https://crates.io/crates/plotters-statistical)'s
13/// scatter/reference-line primitives: a straight diagonal (`y = x`) is the
14/// "perfect normality" baseline, conceptually the same role its `RocCurve`
15/// diagonal plays. That is a documented cross-crate integration point, not a hard
16/// dependency — pass the pairs into any plotting backend you like.
17///
18/// Typically you feed in standardized or studentized residuals so the diagonal
19/// reference line has unit slope; raw residuals produce a line whose slope is the
20/// residual standard deviation.
21///
22/// # Example
23///
24/// ```
25/// use ndarray::array;
26/// use regression_diagnostics::residuals::qq_plot_data;
27///
28/// let r = array![-1.2, 0.3, -0.1, 0.9, 0.05];
29/// let pairs = qq_plot_data(r.view());
30/// assert_eq!(pairs.len(), 5);
31/// // Sample quantiles come out sorted ascending.
32/// assert!(pairs.windows(2).all(|w| w[0].1 <= w[1].1));
33/// ```
34pub fn qq_plot_data(residuals: ArrayView1<f64>) -> Vec<(f64, f64)> {
35 let n = residuals.len();
36 if n == 0 {
37 return Vec::new();
38 }
39 let mut sorted: Vec<f64> = residuals.iter().copied().collect();
40 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
41
42 let normal = Normal::new(0.0, 1.0).expect("standard normal is valid");
43 sorted
44 .into_iter()
45 .enumerate()
46 .map(|(idx, sample)| {
47 let pos = (idx as f64 + 0.5) / n as f64;
48 (normal.inverse_cdf(pos), sample)
49 })
50 .collect()
51}