1use crate::aes::Aesthetic;
12use crate::data::{DataFrame, Value};
13use crate::scale::ScaleSet;
14
15use super::distribution as d;
16use super::Stat;
17
18fn quantile_type7(sorted: &[f64], p: f64) -> f64 {
20 let n = sorted.len();
21 if n == 0 {
22 return 0.0;
23 }
24 if n == 1 {
25 return sorted[0];
26 }
27 let h = (n - 1) as f64 * p;
28 let lo = h.floor() as usize;
29 let hi = (lo + 1).min(n - 1);
30 let frac = h - lo as f64;
31 sorted[lo] + frac * (sorted[hi] - sorted[lo])
32}
33
34pub fn ppoints(n: usize) -> Vec<f64> {
36 let a = if n <= 10 { 3.0 / 8.0 } else { 0.5 };
37 (0..n)
38 .map(|i| (i as f64 + 1.0 - a) / (n as f64 + 1.0 - 2.0 * a))
39 .collect()
40}
41
42#[derive(Clone, Copy, Debug, PartialEq)]
45#[non_exhaustive]
46pub enum QQDistribution {
47 Normal { mean: f64, sd: f64 },
49 StudentT { df: f64 },
51 Exponential { rate: f64 },
53 HalfNormal { sd: f64 },
56}
57
58impl Default for QQDistribution {
59 fn default() -> Self {
60 QQDistribution::normal()
61 }
62}
63
64impl QQDistribution {
65 pub fn normal() -> Self {
67 QQDistribution::Normal { mean: 0.0, sd: 1.0 }
68 }
69 pub fn t(df: f64) -> Self {
71 QQDistribution::StudentT { df }
72 }
73 pub fn exponential() -> Self {
75 QQDistribution::Exponential { rate: 1.0 }
76 }
77 pub fn half_normal() -> Self {
79 QQDistribution::HalfNormal { sd: 1.0 }
80 }
81
82 pub fn quantile(&self, p: f64) -> f64 {
84 match *self {
85 QQDistribution::Normal { mean, sd } if sd > 0.0 => mean + sd * d::qnorm(p),
86 QQDistribution::StudentT { df } if df > 0.0 => d::qt(p, df),
87 QQDistribution::Exponential { rate } if rate > 0.0 => {
88 if !(0.0..=1.0).contains(&p) {
89 f64::NAN
90 } else {
91 -(-p).ln_1p() / rate
92 }
93 }
94 QQDistribution::HalfNormal { sd } if sd > 0.0 => {
95 if !(0.0..=1.0).contains(&p) {
96 f64::NAN
97 } else {
98 sd * d::qnorm((1.0 + p) / 2.0)
99 }
100 }
101 _ => f64::NAN,
102 }
103 }
104
105 pub fn density(&self, x: f64) -> f64 {
107 match *self {
108 QQDistribution::Normal { mean, sd } if sd > 0.0 => d::dnorm((x - mean) / sd) / sd,
109 QQDistribution::StudentT { df } if df > 0.0 => d::dt(x, df),
110 QQDistribution::Exponential { rate } if rate > 0.0 => {
111 if x < 0.0 {
112 0.0
113 } else {
114 rate * (-rate * x).exp()
115 }
116 }
117 QQDistribution::HalfNormal { sd } if sd > 0.0 => {
118 if x < 0.0 {
119 0.0
120 } else {
121 2.0 * d::dnorm(x / sd) / sd
122 }
123 }
124 _ => f64::NAN,
125 }
126 }
127
128 fn label(&self) -> &'static str {
129 match self {
130 QQDistribution::Normal { .. } => "norm",
131 QQDistribution::StudentT { .. } => "t",
132 QQDistribution::Exponential { .. } => "exp",
133 QQDistribution::HalfNormal { .. } => "halfnorm",
134 }
135 }
136}
137
138fn sorted_sample(data: &DataFrame) -> Vec<f64> {
140 let mut v: Vec<f64> = data
141 .column("y")
142 .map(|c| {
143 c.iter()
144 .filter_map(|v| v.as_f64())
145 .filter(|v| v.is_finite())
146 .collect()
147 })
148 .unwrap_or_default();
149 v.sort_by(|a, b| a.total_cmp(b));
150 v
151}
152
153fn carry_groups(data: &DataFrame, out: &mut DataFrame, n: usize) {
155 for col_name in ["color", "fill", "group", "linetype"] {
156 if let Some(first) = data.column(col_name).and_then(|c| c.first()) {
157 out.add_column(col_name.to_string(), vec![first.clone(); n]);
158 }
159 }
160}
161
162fn floats(v: impl IntoIterator<Item = f64>) -> Vec<Value> {
163 v.into_iter().map(Value::Float).collect()
164}
165
166fn qq_line_coef(sorted: &[f64], dist: &QQDistribution, line_p: (f64, f64)) -> (f64, f64) {
169 let (x1, x2) = (dist.quantile(line_p.0), dist.quantile(line_p.1));
170 let (y1, y2) = (
171 quantile_type7(sorted, line_p.0),
172 quantile_type7(sorted, line_p.1),
173 );
174 let slope = (y2 - y1) / (x2 - x1);
175 (slope, y1 - slope * x1)
176}
177
178#[derive(Clone, Debug, Default)]
181pub struct StatQQDist {
182 pub distribution: QQDistribution,
183}
184
185impl StatQQDist {
186 pub fn new(distribution: QQDistribution) -> Self {
187 StatQQDist { distribution }
188 }
189}
190
191impl Stat for StatQQDist {
192 fn compute_group(&self, data: &DataFrame, _scales: &ScaleSet) -> DataFrame {
193 let values = sorted_sample(data);
194 if values.is_empty() {
195 return DataFrame::new();
196 }
197 let n = values.len();
198 let theo: Vec<f64> = ppoints(n)
199 .into_iter()
200 .map(|p| self.distribution.quantile(p))
201 .collect();
202 let mut result = DataFrame::new();
203 result.add_column("x".to_string(), floats(theo.iter().copied()));
204 result.add_column("y".to_string(), floats(values.iter().copied()));
205 result.add_column("theoretical".to_string(), floats(theo));
206 result.add_column("sample".to_string(), floats(values));
207 carry_groups(data, &mut result, n);
208 result
209 }
210
211 fn required_aes(&self) -> Vec<Aesthetic> {
212 vec![Aesthetic::Y]
213 }
214
215 fn name(&self) -> &str {
216 "qq"
217 }
218}
219
220#[derive(Clone, Debug)]
224pub struct StatQQLineDist {
225 pub distribution: QQDistribution,
226 pub line_p: (f64, f64),
229}
230
231impl Default for StatQQLineDist {
232 fn default() -> Self {
233 StatQQLineDist {
234 distribution: QQDistribution::default(),
235 line_p: (0.25, 0.75),
236 }
237 }
238}
239
240impl StatQQLineDist {
241 pub fn new(distribution: QQDistribution) -> Self {
242 StatQQLineDist {
243 distribution,
244 ..Default::default()
245 }
246 }
247}
248
249impl Stat for StatQQLineDist {
250 fn compute_group(&self, data: &DataFrame, _scales: &ScaleSet) -> DataFrame {
251 let values = sorted_sample(data);
252 let n = values.len();
253 if n < 2 {
254 return DataFrame::new();
255 }
256 let (slope, intercept) = qq_line_coef(&values, &self.distribution, self.line_p);
257 let pp = ppoints(n);
258 let x_min = self.distribution.quantile(pp[0]);
259 let x_max = self.distribution.quantile(pp[n - 1]);
260 let mut result = DataFrame::new();
261 result.add_column("x".to_string(), floats([x_min, x_max]));
262 result.add_column(
263 "y".to_string(),
264 floats([intercept + slope * x_min, intercept + slope * x_max]),
265 );
266 result.add_column("slope".to_string(), floats([slope, slope]));
267 result.add_column("intercept".to_string(), floats([intercept, intercept]));
268 carry_groups(data, &mut result, 2);
269 result
270 }
271
272 fn required_aes(&self) -> Vec<Aesthetic> {
273 vec![Aesthetic::Y]
274 }
275
276 fn name(&self) -> &str {
277 "qq_line"
278 }
279}
280
281#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
283pub enum QQBandType {
284 #[default]
286 Pointwise,
287 Ks,
291}
292
293#[derive(Clone, Debug)]
297pub struct StatQQBand {
298 pub distribution: QQDistribution,
299 pub band: QQBandType,
300 pub level: f64,
302 pub line_p: (f64, f64),
304}
305
306impl Default for StatQQBand {
307 fn default() -> Self {
308 StatQQBand {
309 distribution: QQDistribution::default(),
310 band: QQBandType::Pointwise,
311 level: 0.95,
312 line_p: (0.25, 0.75),
313 }
314 }
315}
316
317impl StatQQBand {
318 pub fn new(distribution: QQDistribution) -> Self {
319 StatQQBand {
320 distribution,
321 ..Default::default()
322 }
323 }
324 pub fn band(mut self, band: QQBandType) -> Self {
326 self.band = band;
327 self
328 }
329 pub fn level(mut self, level: f64) -> Self {
331 self.level = level;
332 self
333 }
334}
335
336impl Stat for StatQQBand {
337 fn compute_group(&self, data: &DataFrame, _scales: &ScaleSet) -> DataFrame {
338 let values = sorted_sample(data);
339 let n = values.len();
340 if n < 2 || !(self.level > 0.0 && self.level < 1.0) {
341 return DataFrame::new();
342 }
343 let dist = &self.distribution;
344 let (slope, intercept) = qq_line_coef(&values, dist, self.line_p);
345 let probs = ppoints(n);
346 let nf = n as f64;
347 let mut xs = Vec::with_capacity(n);
348 let mut fit = Vec::with_capacity(n);
349 let mut lo = Vec::with_capacity(n);
350 let mut hi = Vec::with_capacity(n);
351 let z = d::qnorm(1.0 - (1.0 - self.level) / 2.0);
352 let eps = ((2.0 / (1.0 - self.level)).ln() / (2.0 * nf)).sqrt();
353 for &p in &probs {
354 let q = dist.quantile(p);
355 let fitted = intercept + slope * q;
356 let (l, u) = match self.band {
357 QQBandType::Pointwise => {
358 let se = slope / dist.density(q) * (p * (1.0 - p) / nf).sqrt();
359 (fitted - z * se, fitted + z * se)
360 }
361 QQBandType::Ks => {
362 let lp = (p - eps).max(0.0);
363 let up = (p + eps).min(1.0);
364 (
365 intercept + slope * dist.quantile(lp),
366 intercept + slope * dist.quantile(up),
367 )
368 }
369 };
370 let (l, u) = if l <= u { (l, u) } else { (u, l) };
372 xs.push(q);
373 fit.push(fitted);
374 lo.push(l);
375 hi.push(u);
376 }
377 let mut result = DataFrame::new();
378 result.add_column("x".to_string(), floats(xs));
379 result.add_column("y".to_string(), floats(fit));
380 result.add_column("ymin".to_string(), floats(lo));
381 result.add_column("ymax".to_string(), floats(hi));
382 carry_groups(data, &mut result, n);
383 result
384 }
385
386 fn required_aes(&self) -> Vec<Aesthetic> {
387 vec![Aesthetic::Y]
388 }
389
390 fn name(&self) -> &str {
391 match self.band {
392 QQBandType::Pointwise => "qq_band",
393 QQBandType::Ks => "qq_band_ks",
394 }
395 }
396}
397
398pub struct StatQQ;
402
403impl Stat for StatQQ {
404 fn compute_group(&self, data: &DataFrame, scales: &ScaleSet) -> DataFrame {
405 StatQQDist::default().compute_group(data, scales)
406 }
407
408 fn required_aes(&self) -> Vec<Aesthetic> {
409 vec![Aesthetic::Y]
410 }
411
412 fn name(&self) -> &str {
413 "qq"
414 }
415}
416
417pub struct StatQQLine;
420
421impl Stat for StatQQLine {
422 fn compute_group(&self, data: &DataFrame, scales: &ScaleSet) -> DataFrame {
423 StatQQLineDist::default().compute_group(data, scales)
424 }
425
426 fn required_aes(&self) -> Vec<Aesthetic> {
427 vec![Aesthetic::Y]
428 }
429
430 fn name(&self) -> &str {
431 "qq_line"
432 }
433}
434
435impl std::fmt::Display for QQDistribution {
436 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
437 f.write_str(self.label())
438 }
439}
440
441#[cfg(test)]
442mod tests {
443 use super::*;
444
445 fn sample(vals: &[f64]) -> DataFrame {
446 let mut data = DataFrame::new();
447 data.add_column("y".to_string(), floats(vals.iter().copied()));
448 data
449 }
450
451 #[test]
452 fn test_stat_qq() {
453 let data = sample(&(0..100).map(|i| i as f64).collect::<Vec<_>>());
454 let result = StatQQ.compute_group(&data, &ScaleSet::new());
455 assert_eq!(result.nrows(), 100);
456 let x = result.column("x").unwrap();
457 let y = result.column("y").unwrap();
458 for i in 1..y.len() {
459 assert!(y[i].as_f64().unwrap() >= y[i - 1].as_f64().unwrap());
460 assert!(x[i].as_f64().unwrap() >= x[i - 1].as_f64().unwrap());
461 }
462 }
463
464 #[test]
465 fn test_stat_qq_line() {
466 let data = sample(&(0..100).map(|i| i as f64).collect::<Vec<_>>());
467 let result = StatQQLine.compute_group(&data, &ScaleSet::new());
468 assert_eq!(result.nrows(), 2);
469 }
470
471 #[test]
472 fn quantiles_of_each_distribution() {
473 assert!((QQDistribution::exponential().quantile(0.5) - 2f64.ln()).abs() < 1e-15);
474 let hn = QQDistribution::half_normal();
475 assert!((hn.quantile(0.5) - d::qnorm(0.75)).abs() < 1e-15);
476 assert_eq!(hn.quantile(0.0), 0.0);
477 assert!(QQDistribution::t(0.0).quantile(0.3).is_nan());
478 let n = QQDistribution::Normal { mean: 2.0, sd: 3.0 };
479 assert!((n.quantile(0.975) - (2.0 + 3.0 * 1.959_963_984_540_054)).abs() < 1e-12);
480 }
481
482 #[test]
483 fn band_contains_line_and_ks_reaches_infinity() {
484 let vals: Vec<f64> = (1..=30).map(|i| (i as f64 * 0.37).sin() * 2.0).collect();
485 let data = sample(&vals);
486 let pw = StatQQBand::default().compute_group(&data, &ScaleSet::new());
487 let (lo, mid, hi) = (
488 pw.column("ymin").unwrap(),
489 pw.column("y").unwrap(),
490 pw.column("ymax").unwrap(),
491 );
492 for i in 0..pw.nrows() {
493 let (l, m, h) = (
494 lo[i].as_f64().unwrap(),
495 mid[i].as_f64().unwrap(),
496 hi[i].as_f64().unwrap(),
497 );
498 assert!(l < m && m < h);
499 }
500 let ks = StatQQBand::default()
501 .band(QQBandType::Ks)
502 .compute_group(&data, &ScaleSet::new());
503 let lo = ks.column("ymin").unwrap();
504 assert_eq!(lo[0].as_f64(), Some(f64::NEG_INFINITY));
505 }
506
507 #[test]
508 fn non_finite_and_tiny_samples() {
509 let data = sample(&[f64::NAN, 1.0, f64::INFINITY]);
510 assert_eq!(
511 StatQQDist::default()
512 .compute_group(&data, &ScaleSet::new())
513 .nrows(),
514 1
515 );
516 assert_eq!(
517 StatQQBand::default()
518 .compute_group(&data, &ScaleSet::new())
519 .nrows(),
520 0
521 );
522 }
523}