Skip to main content

u_analytics/
distribution.rs

1//! Distribution analysis.
2//!
3//! Empirical distribution functions, histogram binning, QQ-plot data,
4//! distribution fitting/testing, and kernel density estimation.
5//!
6//! # Examples
7//!
8//! ```
9//! use u_analytics::distribution::{ecdf, histogram_bins, BinMethod};
10//!
11//! let data = [1.0, 2.0, 3.0, 4.0, 5.0];
12//! let (values, probs) = ecdf(&data).unwrap();
13//! assert_eq!(values.len(), 5);
14//! assert!((probs[4] - 1.0).abs() < 1e-10);
15//!
16//! let bins = histogram_bins(&data, BinMethod::Sturges).unwrap();
17//! assert!(bins.n_bins >= 2);
18//! ```
19
20use u_numflow::special;
21use u_numflow::stats;
22
23/// A point on the empirical CDF.
24///
25/// Computes the ECDF: F_n(x) = (number of observations ≤ x) / n.
26///
27/// # Returns
28///
29/// Tuple of (sorted unique values, cumulative probabilities). Returns
30/// `None` if the data is empty or contains non-finite values.
31///
32/// # Examples
33///
34/// ```
35/// use u_analytics::distribution::ecdf;
36///
37/// let data = [3.0, 1.0, 2.0, 1.0, 4.0];
38/// let (vals, probs) = ecdf(&data).unwrap();
39/// assert_eq!(vals, vec![1.0, 2.0, 3.0, 4.0]);
40/// assert!((probs[0] - 0.4).abs() < 1e-10); // 2 values ≤ 1.0
41/// assert!((probs[3] - 1.0).abs() < 1e-10);
42/// ```
43pub fn ecdf(data: &[f64]) -> Option<(Vec<f64>, Vec<f64>)> {
44    if data.is_empty() || data.iter().any(|v| !v.is_finite()) {
45        return None;
46    }
47
48    let n = data.len() as f64;
49    let mut sorted: Vec<f64> = data.to_vec();
50    sorted.sort_by(|a, b| a.partial_cmp(b).expect("finite values"));
51
52    let mut values = Vec::new();
53    let mut probs = Vec::new();
54
55    let mut i = 0;
56    while i < sorted.len() {
57        let val = sorted[i];
58        // Count all occurrences of this value
59        let mut j = i;
60        while j < sorted.len() && (sorted[j] - val).abs() < 1e-300 {
61            j += 1;
62        }
63        values.push(val);
64        probs.push(j as f64 / n);
65        i = j;
66    }
67
68    Some((values, probs))
69}
70
71// ---------------------------------------------------------------------------
72// Histogram binning
73// ---------------------------------------------------------------------------
74
75/// Method for computing optimal number of histogram bins.
76#[derive(Debug, Clone, Copy)]
77pub enum BinMethod {
78    /// Sturges' rule: k = ⌈log₂(n)⌉ + 1. Best for near-normal data.
79    Sturges,
80    /// Scott's rule: h = 3.49·σ·n^(-1/3). Width-based.
81    Scott,
82    /// Freedman-Diaconis rule: h = 2·IQR·n^(-1/3). Robust to outliers.
83    FreedmanDiaconis,
84    /// Explicit bin count chosen by the caller. Must be ≥ 1.
85    Fixed(usize),
86}
87
88/// Result of histogram bin computation.
89#[derive(Debug, Clone)]
90pub struct HistogramBins {
91    /// Number of bins.
92    pub n_bins: usize,
93    /// Bin width.
94    pub bin_width: f64,
95    /// Bin edges (length = n_bins + 1).
96    pub edges: Vec<f64>,
97    /// Bin counts.
98    pub counts: Vec<usize>,
99}
100
101/// Computes optimal histogram bins using the specified method.
102///
103/// # Returns
104///
105/// `None` if fewer than 2 data points, non-finite values, or zero range.
106///
107/// # Examples
108///
109/// ```
110/// use u_analytics::distribution::{histogram_bins, BinMethod};
111///
112/// let data = [1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0];
113/// let result = histogram_bins(&data, BinMethod::Sturges).unwrap();
114/// assert!(result.n_bins >= 3);
115/// assert_eq!(result.edges.len(), result.n_bins + 1);
116/// assert_eq!(result.counts.len(), result.n_bins);
117/// ```
118pub fn histogram_bins(data: &[f64], method: BinMethod) -> Option<HistogramBins> {
119    let n = data.len();
120    if n < 2 || data.iter().any(|v| !v.is_finite()) {
121        return None;
122    }
123
124    let min_val = data.iter().cloned().reduce(f64::min)?;
125    let max_val = data.iter().cloned().reduce(f64::max)?;
126    let range = max_val - min_val;
127
128    if range < 1e-300 {
129        return None; // all same value
130    }
131
132    let nf = n as f64;
133
134    let n_bins = match method {
135        BinMethod::Sturges => {
136            let k = (nf.log2()).ceil() as usize + 1;
137            k.max(2)
138        }
139        BinMethod::Scott => {
140            let sd = stats::std_dev(data)?;
141            if sd < 1e-300 {
142                return None;
143            }
144            let h = 3.49 * sd * nf.powf(-1.0 / 3.0);
145            ((range / h).ceil() as usize).max(2)
146        }
147        BinMethod::FreedmanDiaconis => {
148            let q1 = stats::quantile(data, 0.25)?;
149            let q3 = stats::quantile(data, 0.75)?;
150            let iqr = q3 - q1;
151            if iqr < 1e-300 {
152                // Fall back to Sturges if IQR is zero
153                let k = (nf.log2()).ceil() as usize + 1;
154                k.max(2)
155            } else {
156                let h = 2.0 * iqr * nf.powf(-1.0 / 3.0);
157                ((range / h).ceil() as usize).max(2)
158            }
159        }
160        // Caller-specified count is honored exactly (no automatic floor);
161        // zero bins is invalid input.
162        BinMethod::Fixed(0) => return None,
163        BinMethod::Fixed(k) => k,
164    };
165
166    let bin_width = range / n_bins as f64;
167
168    // Compute edges
169    let mut edges = Vec::with_capacity(n_bins + 1);
170    for i in 0..=n_bins {
171        edges.push(min_val + i as f64 * bin_width);
172    }
173
174    // Count data in each bin
175    let mut counts = vec![0_usize; n_bins];
176    for &x in data {
177        let bin = ((x - min_val) / bin_width).floor() as usize;
178        let bin = bin.min(n_bins - 1); // last point goes in last bin
179        counts[bin] += 1;
180    }
181
182    Some(HistogramBins {
183        n_bins,
184        bin_width,
185        edges,
186        counts,
187    })
188}
189
190// ---------------------------------------------------------------------------
191// QQ-plot
192// ---------------------------------------------------------------------------
193
194/// Generates QQ-plot data (theoretical quantiles vs sample quantiles).
195///
196/// Uses the standard normal distribution as the theoretical distribution.
197/// Sample quantiles are the sorted data. Theoretical quantiles are computed
198/// using the plotting position (i - 0.5) / n.
199///
200/// # Returns
201///
202/// Tuple of (theoretical_quantiles, sample_quantiles). Returns `None` if
203/// fewer than 3 data points.
204///
205/// # Examples
206///
207/// ```
208/// use u_analytics::distribution::qq_plot_normal;
209///
210/// let data = [-1.5, -0.5, 0.0, 0.5, 1.5];
211/// let (theoretical, sample) = qq_plot_normal(&data).unwrap();
212/// assert_eq!(theoretical.len(), 5);
213/// assert_eq!(sample.len(), 5);
214/// // Sample and theoretical should be roughly aligned for normal data
215/// ```
216pub fn qq_plot_normal(data: &[f64]) -> Option<(Vec<f64>, Vec<f64>)> {
217    let n = data.len();
218    if n < 3 || data.iter().any(|v| !v.is_finite()) {
219        return None;
220    }
221
222    let mut sample: Vec<f64> = data.to_vec();
223    sample.sort_by(|a, b| a.partial_cmp(b).expect("finite values"));
224
225    let nf = n as f64;
226    let theoretical: Vec<f64> = (0..n)
227        .map(|i| {
228            let p = (i as f64 + 0.5) / nf;
229            special::inverse_normal_cdf(p)
230        })
231        .collect();
232
233    Some((theoretical, sample))
234}
235
236// ---------------------------------------------------------------------------
237// Kolmogorov-Smirnov test
238// ---------------------------------------------------------------------------
239
240/// Kolmogorov-Smirnov one-sample test against the standard normal distribution.
241///
242/// # Algorithm
243///
244/// D = max|F_n(x) - Φ(x)| where F_n is the ECDF and Φ is the CDF of N(0,1).
245/// P-value approximation: Kolmogorov distribution (Marsaglia et al., 2003).
246///
247/// # Returns
248///
249/// `None` if fewer than 5 observations or non-finite values.
250///
251/// # Examples
252///
253/// ```
254/// use u_analytics::distribution::ks_test_normal;
255///
256/// // Data from approximately N(0,1)
257/// let data = [-1.2, -0.8, -0.3, 0.1, 0.5, 0.7, 1.1, 1.4];
258/// let (d_stat, p_value) = ks_test_normal(&data).unwrap();
259/// assert!(p_value > 0.05); // cannot reject normality
260/// ```
261pub fn ks_test_normal(data: &[f64]) -> Option<(f64, f64)> {
262    let n = data.len();
263    if n < 5 || data.iter().any(|v| !v.is_finite()) {
264        return None;
265    }
266
267    let mean = stats::mean(data)?;
268    let sd = stats::std_dev(data)?;
269    if sd < 1e-300 {
270        return None;
271    }
272
273    let mut sorted: Vec<f64> = data.to_vec();
274    sorted.sort_by(|a, b| a.partial_cmp(b).expect("finite values"));
275
276    let nf = n as f64;
277    let mut d_stat = 0.0_f64;
278
279    for (i, &x) in sorted.iter().enumerate() {
280        let z = (x - mean) / sd;
281        let cdf = special::standard_normal_cdf(z);
282        let ecdf_above = (i + 1) as f64 / nf;
283        let ecdf_below = i as f64 / nf;
284        d_stat = d_stat.max((ecdf_above - cdf).abs());
285        d_stat = d_stat.max((ecdf_below - cdf).abs());
286    }
287
288    // P-value approximation (Kolmogorov distribution, simplified)
289    // For large n: P(D > x) ≈ 2·Σ (-1)^(k-1) exp(-2k²n·x²)
290    let lambda = (nf.sqrt() + 0.12 + 0.11 / nf.sqrt()) * d_stat;
291    let mut p_value = 0.0;
292    for k in 1..=100 {
293        let kf = k as f64;
294        let sign = if k % 2 == 1 { 1.0 } else { -1.0 };
295        let term = sign * (-2.0 * kf * kf * lambda * lambda).exp();
296        p_value += term;
297        if term.abs() < 1e-15 {
298            break;
299        }
300    }
301    p_value = (2.0 * p_value).clamp(0.0, 1.0);
302
303    Some((d_stat, p_value))
304}
305
306// ---------------------------------------------------------------------------
307// Kernel Density Estimation
308// ---------------------------------------------------------------------------
309
310/// Bandwidth selection method for kernel density estimation.
311#[derive(Debug, Clone, Copy)]
312pub enum BandwidthMethod {
313    /// Silverman's rule of thumb: h = 0.9 * min(σ, IQR/1.34) * n^(-1/5).
314    /// Robust to outliers and multimodal distributions.
315    ///
316    /// Reference: Silverman (1986), "Density Estimation for Statistics and
317    /// Data Analysis"
318    Silverman,
319    /// Scott's rule: h = 1.06 * σ * n^(-1/5).
320    /// Slightly smoother, assumes approximately normal data.
321    ///
322    /// Reference: Scott (1992), "Multivariate Density Estimation"
323    Scott,
324    /// Manual bandwidth specification.
325    Manual(f64),
326}
327
328/// Result of kernel density estimation.
329#[derive(Debug, Clone)]
330pub struct KdeResult {
331    /// Evaluation points (x-axis).
332    pub x: Vec<f64>,
333    /// Density estimates at each evaluation point (y-axis).
334    pub density: Vec<f64>,
335    /// Bandwidth used.
336    pub bandwidth: f64,
337}
338
339/// Gaussian kernel density estimation.
340///
341/// # Algorithm
342///
343/// f̂(x) = (1/nh) Σᵢ K((x - xᵢ)/h)
344///
345/// where K(u) = (1/√(2π)) exp(-u²/2) is the Gaussian kernel and
346/// h is the bandwidth (smoothing parameter).
347///
348/// The evaluation grid extends 3h beyond the data range to capture tail
349/// contributions from boundary points (3σ covers 99.7% of a Gaussian).
350///
351/// Reference: Silverman (1986), "Density Estimation for Statistics and
352/// Data Analysis"
353///
354/// # Parameters
355///
356/// - `data`: Sample observations (must have ≥ 2 finite values)
357/// - `method`: Bandwidth selection method
358/// - `n_points`: Number of evaluation grid points (typical: 256–1024)
359///
360/// # Returns
361///
362/// `None` if fewer than 2 data points, fewer than 2 grid points,
363/// non-finite values, or zero variance (for automatic bandwidth methods).
364///
365/// # Examples
366///
367/// ```
368/// use u_analytics::distribution::{kde, BandwidthMethod};
369///
370/// let data = [1.0, 1.1, 1.2, 2.0, 2.1, 2.2, 5.0];
371/// let result = kde(&data, BandwidthMethod::Silverman, 512).unwrap();
372/// assert_eq!(result.x.len(), 512);
373/// assert_eq!(result.density.len(), 512);
374/// assert!(result.bandwidth > 0.0);
375/// // Density should integrate approximately to 1
376/// let dx = result.x[1] - result.x[0];
377/// let integral: f64 = result.density.iter().sum::<f64>() * dx;
378/// assert!((integral - 1.0).abs() < 0.05);
379/// ```
380pub fn kde(data: &[f64], method: BandwidthMethod, n_points: usize) -> Option<KdeResult> {
381    let n = data.len();
382    if n < 2 || n_points < 2 || data.iter().any(|v| !v.is_finite()) {
383        return None;
384    }
385
386    let bandwidth = kde_bandwidth(data, method)?;
387
388    // Evaluation grid: extend 3*bandwidth beyond data range
389    let min_val = data.iter().cloned().reduce(f64::min)?;
390    let max_val = data.iter().cloned().reduce(f64::max)?;
391    let x_min = min_val - 3.0 * bandwidth;
392    let x_max = max_val + 3.0 * bandwidth;
393    let step = (x_max - x_min) / (n_points - 1) as f64;
394
395    let x: Vec<f64> = (0..n_points).map(|i| x_min + i as f64 * step).collect();
396
397    // Evaluate density at each grid point
398    let inv_h = 1.0 / bandwidth;
399    let inv_nh = inv_h / n as f64;
400    let inv_sqrt_2pi = 1.0 / (2.0 * std::f64::consts::PI).sqrt();
401
402    let density: Vec<f64> = x
403        .iter()
404        .map(|&xi| {
405            let sum: f64 = data
406                .iter()
407                .map(|&xj| {
408                    let u = (xi - xj) * inv_h;
409                    inv_sqrt_2pi * (-0.5 * u * u).exp()
410                })
411                .sum();
412            sum * inv_nh
413        })
414        .collect();
415
416    Some(KdeResult {
417        x,
418        density,
419        bandwidth,
420    })
421}
422
423/// Evaluates the kernel density estimate at a single point.
424///
425/// Useful for point-wise evaluation without generating a full grid.
426///
427/// # Returns
428///
429/// `None` if data is empty, contains non-finite values, or bandwidth
430/// is invalid.
431pub fn kde_evaluate(data: &[f64], bandwidth: f64, x: f64) -> Option<f64> {
432    let n = data.len();
433    if n == 0 || bandwidth <= 0.0 || !bandwidth.is_finite() || !x.is_finite() {
434        return None;
435    }
436    if data.iter().any(|v| !v.is_finite()) {
437        return None;
438    }
439
440    let inv_h = 1.0 / bandwidth;
441    let inv_nh = inv_h / n as f64;
442    let inv_sqrt_2pi = 1.0 / (2.0 * std::f64::consts::PI).sqrt();
443
444    let sum: f64 = data
445        .iter()
446        .map(|&xj| {
447            let u = (x - xj) * inv_h;
448            inv_sqrt_2pi * (-0.5 * u * u).exp()
449        })
450        .sum();
451
452    Some(sum * inv_nh)
453}
454
455/// Computes the bandwidth for KDE using the specified method.
456///
457/// Useful when you want to inspect the bandwidth before running full KDE.
458///
459/// # Returns
460///
461/// `None` if fewer than 2 data points, non-finite values, or zero
462/// variance (for automatic methods).
463pub fn kde_bandwidth(data: &[f64], method: BandwidthMethod) -> Option<f64> {
464    let n = data.len();
465    if n < 2 || data.iter().any(|v| !v.is_finite()) {
466        return None;
467    }
468
469    match method {
470        BandwidthMethod::Silverman => {
471            let sd = stats::std_dev(data)?;
472            if sd < 1e-300 {
473                return None;
474            }
475            let q1 = stats::quantile(data, 0.25)?;
476            let q3 = stats::quantile(data, 0.75)?;
477            let iqr = q3 - q1;
478            let spread = if iqr > 1e-300 { sd.min(iqr / 1.34) } else { sd };
479            Some(0.9 * spread * (n as f64).powf(-0.2))
480        }
481        BandwidthMethod::Scott => {
482            let sd = stats::std_dev(data)?;
483            if sd < 1e-300 {
484                return None;
485            }
486            Some(1.06 * sd * (n as f64).powf(-0.2))
487        }
488        BandwidthMethod::Manual(h) => {
489            if h <= 0.0 || !h.is_finite() {
490                None
491            } else {
492                Some(h)
493            }
494        }
495    }
496}
497
498// ---------------------------------------------------------------------------
499// MLE Distribution Fitting
500// ---------------------------------------------------------------------------
501
502/// Result of a distribution fit via maximum likelihood estimation.
503#[derive(Debug, Clone)]
504pub struct FitResult {
505    /// Name of the fitted distribution (e.g., "Normal", "Exponential").
506    pub distribution: String,
507    /// Estimated parameters (name, value) pairs.
508    pub parameters: Vec<(String, f64)>,
509    /// Log-likelihood at MLE parameters.
510    pub log_likelihood: f64,
511    /// Akaike Information Criterion: -2·ℓ + 2k.
512    pub aic: f64,
513    /// Bayesian Information Criterion: -2·ℓ + k·ln(n).
514    pub bic: f64,
515    /// Number of estimated parameters (k).
516    pub n_params: usize,
517}
518
519/// Fits a Normal distribution N(μ, σ²) via MLE.
520///
521/// # Estimators
522///
523/// - μ̂ = x̄ (sample mean)
524/// - σ̂ = √((1/n) Σ(xᵢ - x̄)²) (biased MLE, not sample std dev)
525///
526/// # Returns
527///
528/// `None` if fewer than 2 data points, non-finite values, or zero variance.
529///
530/// # Examples
531///
532/// ```
533/// use u_analytics::distribution::fit_normal;
534///
535/// let data = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
536/// let fit = fit_normal(&data).unwrap();
537/// assert_eq!(fit.distribution, "Normal");
538/// assert!((fit.parameters[0].1 - 5.0).abs() < 1e-10); // μ̂ = 5.0
539/// assert!(fit.parameters[1].1 > 0.0); // σ̂ > 0
540/// ```
541pub fn fit_normal(data: &[f64]) -> Option<FitResult> {
542    let n = data.len();
543    if n < 2 || data.iter().any(|v| !v.is_finite()) {
544        return None;
545    }
546
547    let nf = n as f64;
548    let mu = stats::mean(data)?;
549
550    // Biased MLE variance (denominator n, not n-1)
551    let sum_sq: f64 = data.iter().map(|&x| (x - mu).powi(2)).sum();
552    let sigma_sq = sum_sq / nf;
553    if sigma_sq < 1e-300 {
554        return None;
555    }
556    let sigma = sigma_sq.sqrt();
557
558    // Log-likelihood: -n/2·ln(2π) - n·ln(σ) - n/2
559    let log_lik = -nf / 2.0 * (2.0 * std::f64::consts::PI).ln() - nf * sigma.ln() - nf / 2.0;
560
561    let k = 2;
562    let aic = -2.0 * log_lik + 2.0 * k as f64;
563    let bic = -2.0 * log_lik + k as f64 * nf.ln();
564
565    Some(FitResult {
566        distribution: "Normal".to_string(),
567        parameters: vec![("mu".to_string(), mu), ("sigma".to_string(), sigma)],
568        log_likelihood: log_lik,
569        aic,
570        bic,
571        n_params: k,
572    })
573}
574
575/// Fits an Exponential distribution Exp(λ) via MLE.
576///
577/// # Estimators
578///
579/// - λ̂ = 1/x̄ (rate parameter)
580///
581/// # Returns
582///
583/// `None` if fewer than 2 data points, non-positive values, or non-finite values.
584///
585/// # Examples
586///
587/// ```
588/// use u_analytics::distribution::fit_exponential;
589///
590/// let data = [0.5, 1.2, 0.8, 2.1, 0.3, 1.5, 0.9, 1.8];
591/// let fit = fit_exponential(&data).unwrap();
592/// assert_eq!(fit.distribution, "Exponential");
593/// assert!(fit.parameters[0].1 > 0.0); // λ̂ > 0
594/// ```
595pub fn fit_exponential(data: &[f64]) -> Option<FitResult> {
596    let n = data.len();
597    if n < 2 || data.iter().any(|v| !v.is_finite()) {
598        return None;
599    }
600    // Exponential requires x > 0
601    if data.iter().any(|&v| v <= 0.0) {
602        return None;
603    }
604
605    let nf = n as f64;
606    let mean = stats::mean(data)?;
607    if mean < 1e-300 {
608        return None;
609    }
610
611    let lambda = 1.0 / mean;
612
613    // Log-likelihood: n·ln(λ) - λ·n·x̄ = n·ln(λ) - n
614    let log_lik = nf * lambda.ln() - nf;
615
616    let k = 1;
617    let aic = -2.0 * log_lik + 2.0 * k as f64;
618    let bic = -2.0 * log_lik + k as f64 * nf.ln();
619
620    Some(FitResult {
621        distribution: "Exponential".to_string(),
622        parameters: vec![("lambda".to_string(), lambda)],
623        log_likelihood: log_lik,
624        aic,
625        bic,
626        n_params: k,
627    })
628}
629
630/// Digamma function ψ(x) = d/dx ln(Γ(x)).
631///
632/// Uses the asymptotic expansion for x ≥ 8, with recurrence for smaller x.
633/// Reference: Abramowitz & Stegun (1972), formula 6.3.18.
634fn digamma(x: f64) -> f64 {
635    if x <= 0.0 {
636        return f64::NAN;
637    }
638
639    // Recurrence: ψ(x) = ψ(x+1) - 1/x
640    let mut val = 0.0;
641    let mut x = x;
642    while x < 8.0 {
643        val -= 1.0 / x;
644        x += 1.0;
645    }
646
647    // Asymptotic expansion for x ≥ 8
648    // ψ(x) ≈ ln(x) - 1/(2x) - 1/(12x²) + 1/(120x⁴) - 1/(252x⁶) + ...
649    let inv_x = 1.0 / x;
650    let inv_x2 = inv_x * inv_x;
651    val += x.ln()
652        - 0.5 * inv_x
653        - inv_x2 * (1.0 / 12.0 - inv_x2 * (1.0 / 120.0 - inv_x2 * (1.0 / 252.0)));
654
655    val
656}
657
658/// Trigamma function ψ'(x) = d²/dx² ln(Γ(x)).
659///
660/// Asymptotic expansion for x ≥ 8, with recurrence for smaller x.
661fn trigamma(x: f64) -> f64 {
662    if x <= 0.0 {
663        return f64::NAN;
664    }
665
666    // Recurrence: ψ'(x) = ψ'(x+1) + 1/x²
667    let mut val = 0.0;
668    let mut x = x;
669    while x < 8.0 {
670        val += 1.0 / (x * x);
671        x += 1.0;
672    }
673
674    // Asymptotic expansion for x ≥ 8
675    // ψ'(x) ≈ 1/x + 1/(2x²) + 1/(6x³) - 1/(30x⁵) + 1/(42x⁷) - ...
676    let inv_x = 1.0 / x;
677    let inv_x2 = inv_x * inv_x;
678    val += inv_x
679        + 0.5 * inv_x2
680        + inv_x2 * inv_x * (1.0 / 6.0 - inv_x2 * (1.0 / 30.0 - inv_x2 * (1.0 / 42.0)));
681
682    val
683}
684
685/// Fits a Gamma distribution Gamma(α, β) via MLE.
686///
687/// # Algorithm
688///
689/// Rate parameter β̂ = α̂/x̄ (closed-form once α is known).
690/// Shape parameter α̂ solved via Newton-Raphson on:
691/// ln(α) - ψ(α) = ln(x̄) - (1/n)Σln(xᵢ)
692///
693/// Initial guess from method of moments: α₀ = x̄²/s².
694///
695/// Reference: Minka (2002), "Estimating a Gamma distribution"
696///
697/// # Returns
698///
699/// `None` if fewer than 2 data points, non-positive values, non-finite values,
700/// or Newton-Raphson fails to converge.
701///
702/// # Examples
703///
704/// ```
705/// use u_analytics::distribution::fit_gamma;
706///
707/// let data = [2.1, 3.5, 1.8, 4.2, 2.9, 3.1, 2.5, 3.8, 1.5, 2.7];
708/// let fit = fit_gamma(&data).unwrap();
709/// assert_eq!(fit.distribution, "Gamma");
710/// assert!(fit.parameters[0].1 > 0.0); // α̂ > 0
711/// assert!(fit.parameters[1].1 > 0.0); // β̂ > 0
712/// ```
713pub fn fit_gamma(data: &[f64]) -> Option<FitResult> {
714    let n = data.len();
715    if n < 2 || data.iter().any(|v| !v.is_finite()) {
716        return None;
717    }
718    // Gamma requires x > 0
719    if data.iter().any(|&v| v <= 0.0) {
720        return None;
721    }
722
723    let nf = n as f64;
724    let mean = stats::mean(data)?;
725    if mean < 1e-300 {
726        return None;
727    }
728
729    // Biased variance (denominator n)
730    let var: f64 = data.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / nf;
731    if var < 1e-300 {
732        return None;
733    }
734
735    let sum_log: f64 = data.iter().map(|&x| x.ln()).sum();
736    let mean_log = sum_log / nf;
737    let target = mean.ln() - mean_log; // ln(x̄) - (1/n)Σln(xᵢ)
738
739    // Method of moments initial guess
740    let mut alpha = mean * mean / var;
741    if alpha < 1e-10 {
742        alpha = 0.5;
743    }
744
745    // Newton-Raphson: solve ln(α) - ψ(α) = target
746    for _ in 0..200 {
747        let f = alpha.ln() - digamma(alpha) - target;
748        let f_prime = 1.0 / alpha - trigamma(alpha);
749        if f_prime.abs() < 1e-30 {
750            break;
751        }
752
753        let delta = f / f_prime;
754        alpha -= delta;
755
756        // Keep alpha positive
757        if alpha <= 0.0 {
758            alpha = 1e-10;
759        }
760
761        if delta.abs() < 1e-10 {
762            break;
763        }
764    }
765
766    if !alpha.is_finite() || alpha <= 0.0 {
767        return None;
768    }
769
770    let beta = alpha / mean; // rate parameter
771
772    // Log-likelihood: n·α·ln(β) - n·ln(Γ(α)) + (α-1)·Σln(xᵢ) - β·Σxᵢ
773    let sum_x: f64 = data.iter().sum();
774    let log_lik = nf * alpha * beta.ln() - nf * special::ln_gamma(alpha) + (alpha - 1.0) * sum_log
775        - beta * sum_x;
776
777    let k = 2;
778    let aic = -2.0 * log_lik + 2.0 * k as f64;
779    let bic = -2.0 * log_lik + k as f64 * nf.ln();
780
781    Some(FitResult {
782        distribution: "Gamma".to_string(),
783        parameters: vec![("alpha".to_string(), alpha), ("beta".to_string(), beta)],
784        log_likelihood: log_lik,
785        aic,
786        bic,
787        n_params: k,
788    })
789}
790
791/// Fits a LogNormal distribution via MLE.
792///
793/// # Estimators
794///
795/// If Y = ln(X), then Y ~ N(μ, σ²):
796/// - μ̂ = (1/n) Σ ln(xᵢ)
797/// - σ̂ = √((1/n) Σ (ln(xᵢ) - μ̂)²)
798///
799/// # Returns
800///
801/// `None` if fewer than 2 data points, non-positive values, or non-finite values.
802///
803/// # Examples
804///
805/// ```
806/// use u_analytics::distribution::fit_lognormal;
807///
808/// let data = [1.2, 2.5, 1.8, 3.1, 0.9, 2.0, 1.5, 4.0];
809/// let fit = fit_lognormal(&data).unwrap();
810/// assert_eq!(fit.distribution, "LogNormal");
811/// assert!(fit.parameters[0].1.is_finite()); // μ̂
812/// assert!(fit.parameters[1].1 > 0.0); // σ̂ > 0
813/// ```
814pub fn fit_lognormal(data: &[f64]) -> Option<FitResult> {
815    let n = data.len();
816    if n < 2 || data.iter().any(|v| !v.is_finite()) {
817        return None;
818    }
819    if data.iter().any(|&v| v <= 0.0) {
820        return None;
821    }
822
823    let nf = n as f64;
824    let log_data: Vec<f64> = data.iter().map(|&x| x.ln()).collect();
825
826    let mu = log_data.iter().sum::<f64>() / nf;
827    let sigma_sq = log_data.iter().map(|&y| (y - mu).powi(2)).sum::<f64>() / nf;
828    if sigma_sq < 1e-300 {
829        return None;
830    }
831    let sigma = sigma_sq.sqrt();
832
833    // Log-likelihood: -n/2·ln(2π) - n·ln(σ) - (1/2σ²)Σ(ln(xᵢ)-μ)² - Σln(xᵢ)
834    let sum_log: f64 = log_data.iter().sum();
835    let log_lik =
836        -nf / 2.0 * (2.0 * std::f64::consts::PI).ln() - nf * sigma.ln() - nf / 2.0 - sum_log;
837
838    let k = 2;
839    let aic = -2.0 * log_lik + 2.0 * k as f64;
840    let bic = -2.0 * log_lik + k as f64 * nf.ln();
841
842    Some(FitResult {
843        distribution: "LogNormal".to_string(),
844        parameters: vec![("mu".to_string(), mu), ("sigma".to_string(), sigma)],
845        log_likelihood: log_lik,
846        aic,
847        bic,
848        n_params: k,
849    })
850}
851
852/// Fits a Poisson distribution via MLE.
853///
854/// # Estimators
855///
856/// λ̂ = x̄ (sample mean). Data are counts: non-negative integers
857/// (represented as f64 for API consistency).
858///
859/// # Returns
860///
861/// `None` if fewer than 2 data points, a value that is not a non-negative
862/// integer, all zeros, or non-finite values. (A non-integer has no Poisson
863/// probability; `ln Γ(x + 1)` would extend the formula past where it means one.)
864///
865/// # Examples
866///
867/// ```
868/// use u_analytics::distribution::fit_poisson;
869///
870/// let data = [2.0, 3.0, 1.0, 4.0, 2.0, 3.0, 1.0, 2.0];
871/// let fit = fit_poisson(&data).unwrap();
872/// assert_eq!(fit.distribution, "Poisson");
873/// assert!((fit.parameters[0].1 - 2.25).abs() < 1e-10); // λ̂ = mean
874/// ```
875pub fn fit_poisson(data: &[f64]) -> Option<FitResult> {
876    let n = data.len();
877    if n < 2 || data.iter().any(|v| !v.is_finite()) {
878        return None;
879    }
880    if data.iter().any(|&v| v < 0.0 || v.fract() != 0.0) {
881        return None;
882    }
883
884    let nf = n as f64;
885    let lambda = stats::mean(data)?;
886    if lambda < 1e-300 {
887        return None; // All zeros → degenerate
888    }
889
890    // Log-likelihood: Σ(xᵢ·ln(λ) - λ - ln(xᵢ!))
891    // = n·x̄·ln(λ) - n·λ - Σln(xᵢ!)
892    let sum_log_fact: f64 = data.iter().map(|&x| special::ln_gamma(x + 1.0)).sum();
893    let log_lik = nf * lambda * lambda.ln() - nf * lambda - sum_log_fact;
894
895    let k = 1;
896    let aic = -2.0 * log_lik + 2.0 * k as f64;
897    let bic = -2.0 * log_lik + k as f64 * nf.ln();
898
899    Some(FitResult {
900        distribution: "Poisson".to_string(),
901        parameters: vec![("lambda".to_string(), lambda)],
902        log_likelihood: log_lik,
903        aic,
904        bic,
905        n_params: k,
906    })
907}
908
909/// Fits a Beta distribution Beta(α, β) via MLE.
910///
911/// # Algorithm
912///
913/// Newton-Raphson on the system:
914/// - ψ(α) - ψ(α + β) = (1/n) Σ ln(xᵢ)
915/// - ψ(β) - ψ(α + β) = (1/n) Σ ln(1 - xᵢ)
916///
917/// Initial guess from method of moments:
918/// - α₀ = x̄ · ((x̄(1-x̄)/s²) - 1)
919/// - β₀ = (1-x̄) · ((x̄(1-x̄)/s²) - 1)
920///
921/// Data must be in (0, 1).
922///
923/// # Returns
924///
925/// `None` if fewer than 2 data points, values outside (0,1), or convergence failure.
926///
927/// # Examples
928///
929/// ```
930/// use u_analytics::distribution::fit_beta;
931///
932/// let data = [0.2, 0.35, 0.5, 0.15, 0.45, 0.3, 0.6, 0.25, 0.4, 0.55];
933/// let fit = fit_beta(&data).unwrap();
934/// assert_eq!(fit.distribution, "Beta");
935/// assert!(fit.parameters[0].1 > 0.0); // α̂ > 0
936/// assert!(fit.parameters[1].1 > 0.0); // β̂ > 0
937/// ```
938pub fn fit_beta(data: &[f64]) -> Option<FitResult> {
939    let n = data.len();
940    if n < 2 || data.iter().any(|v| !v.is_finite()) {
941        return None;
942    }
943    // Beta requires 0 < x < 1
944    if data.iter().any(|&v| v <= 0.0 || v >= 1.0) {
945        return None;
946    }
947
948    let nf = n as f64;
949    let mean = stats::mean(data)?;
950    let var = data.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / nf;
951    if var < 1e-300 {
952        return None;
953    }
954
955    let sum_log_x: f64 = data.iter().map(|&x| x.ln()).sum();
956    let sum_log_1mx: f64 = data.iter().map(|&x| (1.0 - x).ln()).sum();
957    let mean_log_x = sum_log_x / nf;
958    let mean_log_1mx = sum_log_1mx / nf;
959
960    // Method of moments initial guess
961    let factor = mean * (1.0 - mean) / var - 1.0;
962    if factor <= 0.0 {
963        return None;
964    }
965    let mut alpha = mean * factor;
966    let mut beta = (1.0 - mean) * factor;
967
968    if alpha < 0.01 {
969        alpha = 0.5;
970    }
971    if beta < 0.01 {
972        beta = 0.5;
973    }
974
975    // Newton-Raphson (joint update)
976    for _ in 0..200 {
977        let psi_ab = digamma(alpha + beta);
978        let tri_ab = trigamma(alpha + beta);
979
980        let f1 = digamma(alpha) - psi_ab - mean_log_x;
981        let f2 = digamma(beta) - psi_ab - mean_log_1mx;
982
983        let j11 = trigamma(alpha) - tri_ab;
984        let j12 = -tri_ab;
985        let j21 = -tri_ab;
986        let j22 = trigamma(beta) - tri_ab;
987
988        let det = j11 * j22 - j12 * j21;
989        if det.abs() < 1e-30 {
990            break;
991        }
992
993        let da = (j22 * f1 - j12 * f2) / det;
994        let db = (j11 * f2 - j21 * f1) / det;
995
996        alpha -= da;
997        beta -= db;
998
999        if alpha <= 0.0 {
1000            alpha = 1e-10;
1001        }
1002        if beta <= 0.0 {
1003            beta = 1e-10;
1004        }
1005
1006        if da.abs() < 1e-10 && db.abs() < 1e-10 {
1007            break;
1008        }
1009    }
1010
1011    if !alpha.is_finite() || !beta.is_finite() || alpha <= 0.0 || beta <= 0.0 {
1012        return None;
1013    }
1014
1015    // Log-likelihood: n·[ln(Γ(α+β)) - ln(Γ(α)) - ln(Γ(β))]
1016    //                + (α-1)·Σln(xᵢ) + (β-1)·Σln(1-xᵢ)
1017    let log_lik = nf
1018        * (special::ln_gamma(alpha + beta) - special::ln_gamma(alpha) - special::ln_gamma(beta))
1019        + (alpha - 1.0) * sum_log_x
1020        + (beta - 1.0) * sum_log_1mx;
1021
1022    let k = 2;
1023    let aic = -2.0 * log_lik + 2.0 * k as f64;
1024    let bic = -2.0 * log_lik + k as f64 * nf.ln();
1025
1026    Some(FitResult {
1027        distribution: "Beta".to_string(),
1028        parameters: vec![("alpha".to_string(), alpha), ("beta".to_string(), beta)],
1029        log_likelihood: log_lik,
1030        aic,
1031        bic,
1032        n_params: k,
1033    })
1034}
1035
1036/// Fits a two-parameter Weibull distribution by maximum likelihood
1037/// ([`crate::weibull::weibull_mle`]), as a [`FitResult`] for comparison with
1038/// the other families.
1039///
1040/// # Returns
1041///
1042/// `None` if fewer than 2 data points, a value that is not strictly positive
1043/// and finite, or the fit does not converge.
1044///
1045/// # Examples
1046///
1047/// ```
1048/// use u_analytics::distribution::fit_weibull;
1049///
1050/// let data = [12.0, 25.0, 31.0, 44.0, 52.0, 60.0, 71.0, 85.0];
1051/// let fit = fit_weibull(&data).unwrap();
1052/// assert_eq!(fit.distribution, "Weibull");
1053/// assert_eq!(fit.parameters[0].0, "shape");
1054/// ```
1055pub fn fit_weibull(data: &[f64]) -> Option<FitResult> {
1056    if data.len() < 2 || data.iter().any(|&v| !(v.is_finite() && v > 0.0)) {
1057        return None;
1058    }
1059    let r = crate::weibull::weibull_mle(data)?;
1060    let nf = data.len() as f64;
1061    let k = 2;
1062    Some(FitResult {
1063        distribution: "Weibull".to_string(),
1064        parameters: vec![
1065            ("shape".to_string(), r.shape),
1066            ("scale".to_string(), r.scale),
1067        ],
1068        log_likelihood: r.log_likelihood,
1069        aic: -2.0 * r.log_likelihood + 2.0 * k as f64,
1070        bic: -2.0 * r.log_likelihood + k as f64 * nf.ln(),
1071        n_params: k,
1072    })
1073}
1074
1075/// Fits the continuous families that apply to the data and returns them sorted
1076/// by AIC (best first).
1077///
1078/// Tries Normal, Exponential, Gamma, LogNormal, Weibull and Beta; each is kept
1079/// only if it fits (the positive families need data > 0, Beta needs data in
1080/// (0, 1)).
1081///
1082/// Poisson is not a candidate: its likelihood is a probability mass, the
1083/// others' a density, and AIC compares likelihoods only on the same footing --
1084/// a density's value changes with the unit of measurement, a mass does not.
1085/// Fit count data with [`fit_poisson`] directly.
1086///
1087/// # Examples
1088///
1089/// ```
1090/// use u_analytics::distribution::fit_best;
1091///
1092/// let data = [2.1, 3.5, 1.8, 4.2, 2.9, 3.1, 2.5, 3.8, 1.5, 2.7];
1093/// let fits = fit_best(&data);
1094/// assert!(!fits.is_empty());
1095/// // Results sorted by AIC (lowest = best fit)
1096/// if fits.len() >= 2 {
1097///     assert!(fits[0].aic <= fits[1].aic);
1098/// }
1099/// ```
1100pub fn fit_best(data: &[f64]) -> Vec<FitResult> {
1101    let mut results = Vec::new();
1102
1103    if let Some(r) = fit_normal(data) {
1104        results.push(r);
1105    }
1106    if let Some(r) = fit_exponential(data) {
1107        results.push(r);
1108    }
1109    if let Some(r) = fit_gamma(data) {
1110        results.push(r);
1111    }
1112    if let Some(r) = fit_lognormal(data) {
1113        results.push(r);
1114    }
1115    if let Some(r) = fit_weibull(data) {
1116        results.push(r);
1117    }
1118    if let Some(r) = fit_beta(data) {
1119        results.push(r);
1120    }
1121
1122    results.sort_by(|a, b| {
1123        a.aic
1124            .partial_cmp(&b.aic)
1125            .unwrap_or(std::cmp::Ordering::Equal)
1126    });
1127    results
1128}
1129
1130#[cfg(test)]
1131mod tests {
1132    use super::*;
1133
1134    // -----------------------------------------------------------------------
1135    // ECDF
1136    // -----------------------------------------------------------------------
1137
1138    #[test]
1139    fn ecdf_basic() {
1140        let (vals, probs) = ecdf(&[3.0, 1.0, 2.0]).expect("should compute");
1141        assert_eq!(vals, vec![1.0, 2.0, 3.0]);
1142        assert!((probs[0] - 1.0 / 3.0).abs() < 1e-10);
1143        assert!((probs[1] - 2.0 / 3.0).abs() < 1e-10);
1144        assert!((probs[2] - 1.0).abs() < 1e-10);
1145    }
1146
1147    #[test]
1148    fn ecdf_with_ties() {
1149        let (vals, probs) = ecdf(&[1.0, 2.0, 2.0, 3.0]).expect("should compute");
1150        assert_eq!(vals, vec![1.0, 2.0, 3.0]);
1151        assert!((probs[0] - 0.25).abs() < 1e-10);
1152        assert!((probs[1] - 0.75).abs() < 1e-10); // 3 out of 4 ≤ 2.0
1153        assert!((probs[2] - 1.0).abs() < 1e-10);
1154    }
1155
1156    #[test]
1157    fn ecdf_single() {
1158        let (vals, probs) = ecdf(&[5.0]).expect("should compute");
1159        assert_eq!(vals, vec![5.0]);
1160        assert!((probs[0] - 1.0).abs() < 1e-10);
1161    }
1162
1163    #[test]
1164    fn ecdf_empty() {
1165        assert!(ecdf(&[]).is_none());
1166    }
1167
1168    #[test]
1169    fn ecdf_nan() {
1170        assert!(ecdf(&[1.0, f64::NAN, 3.0]).is_none());
1171    }
1172
1173    // -----------------------------------------------------------------------
1174    // Histogram bins
1175    // -----------------------------------------------------------------------
1176
1177    #[test]
1178    fn hist_sturges_basic() {
1179        let data: Vec<f64> = (0..100).map(|i| i as f64).collect();
1180        let r = histogram_bins(&data, BinMethod::Sturges).expect("should compute");
1181        assert!(r.n_bins >= 5);
1182        assert_eq!(r.edges.len(), r.n_bins + 1);
1183        assert_eq!(r.counts.len(), r.n_bins);
1184        let total: usize = r.counts.iter().sum();
1185        assert_eq!(total, 100);
1186    }
1187
1188    #[test]
1189    fn hist_scott() {
1190        let data: Vec<f64> = (0..100).map(|i| i as f64).collect();
1191        let r = histogram_bins(&data, BinMethod::Scott).expect("should compute");
1192        assert!(r.n_bins >= 3);
1193        let total: usize = r.counts.iter().sum();
1194        assert_eq!(total, 100);
1195    }
1196
1197    #[test]
1198    fn hist_fd() {
1199        let data: Vec<f64> = (0..100).map(|i| i as f64).collect();
1200        let r = histogram_bins(&data, BinMethod::FreedmanDiaconis).expect("should compute");
1201        assert!(r.n_bins >= 3);
1202        let total: usize = r.counts.iter().sum();
1203        assert_eq!(total, 100);
1204    }
1205
1206    #[test]
1207    fn hist_edge_cases() {
1208        assert!(histogram_bins(&[1.0], BinMethod::Sturges).is_none()); // < 2
1209        assert!(histogram_bins(&[5.0, 5.0, 5.0], BinMethod::Sturges).is_none());
1210        // zero range
1211    }
1212
1213    #[test]
1214    fn hist_fixed_exact_count() {
1215        let data: Vec<f64> = (0..100).map(|i| i as f64).collect();
1216        let r = histogram_bins(&data, BinMethod::Fixed(5)).expect("should compute");
1217        assert_eq!(r.n_bins, 5);
1218        assert_eq!(r.edges.len(), 6);
1219        assert_eq!(r.counts.len(), 5);
1220        let total: usize = r.counts.iter().sum();
1221        assert_eq!(total, 100);
1222    }
1223
1224    #[test]
1225    fn hist_fixed_single_bin() {
1226        let data = [1.0, 2.0, 3.0];
1227        let r = histogram_bins(&data, BinMethod::Fixed(1)).expect("should compute");
1228        assert_eq!(r.n_bins, 1);
1229        assert_eq!(r.counts, vec![3]);
1230    }
1231
1232    #[test]
1233    fn hist_fixed_zero_is_invalid() {
1234        let data = [1.0, 2.0, 3.0];
1235        assert!(histogram_bins(&data, BinMethod::Fixed(0)).is_none());
1236    }
1237
1238    #[test]
1239    fn hist_fixed_shares_input_guards() {
1240        assert!(histogram_bins(&[1.0], BinMethod::Fixed(5)).is_none()); // < 2 points
1241        assert!(histogram_bins(&[5.0, 5.0, 5.0], BinMethod::Fixed(5)).is_none());
1242        // zero range
1243    }
1244
1245    // -----------------------------------------------------------------------
1246    // QQ-plot
1247    // -----------------------------------------------------------------------
1248
1249    #[test]
1250    fn qq_basic() {
1251        let data = [-1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5];
1252        let (theo, samp) = qq_plot_normal(&data).expect("should compute");
1253        assert_eq!(theo.len(), 7);
1254        assert_eq!(samp.len(), 7);
1255        // Sorted sample
1256        assert!((samp[0] + 1.5).abs() < 1e-10);
1257        assert!((samp[6] - 1.5).abs() < 1e-10);
1258        // Theoretical: median should be near 0
1259        assert!(theo[3].abs() < 0.1);
1260    }
1261
1262    #[test]
1263    fn qq_edge_cases() {
1264        assert!(qq_plot_normal(&[1.0, 2.0]).is_none()); // < 3
1265    }
1266
1267    // -----------------------------------------------------------------------
1268    // KS test
1269    // -----------------------------------------------------------------------
1270
1271    #[test]
1272    fn ks_normal_data() {
1273        // Approximately normal data
1274        let data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 2.0, 2.5];
1275        let (d, p) = ks_test_normal(&data).expect("should compute");
1276        assert!(d > 0.0 && d < 1.0, "D = {d}");
1277        assert!(p > 0.05, "p = {p}");
1278    }
1279
1280    #[test]
1281    fn ks_non_normal_data() {
1282        // Clearly non-normal (uniform 0-100)
1283        let data: Vec<f64> = (0..50).map(|i| i as f64 * 2.0).collect();
1284        let (d, _p) = ks_test_normal(&data).expect("should compute");
1285        assert!(d > 0.0);
1286    }
1287
1288    #[test]
1289    fn ks_edge_cases() {
1290        assert!(ks_test_normal(&[1.0, 2.0, 3.0, 4.0]).is_none()); // < 5
1291        assert!(ks_test_normal(&[5.0, 5.0, 5.0, 5.0, 5.0]).is_none()); // zero var
1292    }
1293
1294    // -----------------------------------------------------------------------
1295    // KDE
1296    // -----------------------------------------------------------------------
1297
1298    #[test]
1299    fn kde_silverman_basic() {
1300        let data: Vec<f64> = (0..100).map(|i| i as f64).collect();
1301        let r = kde(&data, BandwidthMethod::Silverman, 256).expect("should compute");
1302        assert_eq!(r.x.len(), 256);
1303        assert_eq!(r.density.len(), 256);
1304        assert!(r.bandwidth > 0.0);
1305        // All densities non-negative
1306        assert!(r.density.iter().all(|&d| d >= 0.0));
1307        // Integral ≈ 1
1308        let dx = r.x[1] - r.x[0];
1309        let integral: f64 = r.density.iter().sum::<f64>() * dx;
1310        assert!(
1311            (integral - 1.0).abs() < 0.05,
1312            "integral = {integral}, expected ≈ 1.0"
1313        );
1314    }
1315
1316    #[test]
1317    fn kde_scott_basic() {
1318        let data: Vec<f64> = (0..100).map(|i| i as f64).collect();
1319        let r = kde(&data, BandwidthMethod::Scott, 256).expect("should compute");
1320        assert!(r.bandwidth > 0.0);
1321        let dx = r.x[1] - r.x[0];
1322        let integral: f64 = r.density.iter().sum::<f64>() * dx;
1323        assert!(
1324            (integral - 1.0).abs() < 0.05,
1325            "integral = {integral}, expected ≈ 1.0"
1326        );
1327    }
1328
1329    #[test]
1330    fn kde_manual_bandwidth() {
1331        let data = [1.0, 2.0, 3.0, 4.0, 5.0];
1332        let r = kde(&data, BandwidthMethod::Manual(0.5), 128).expect("should compute");
1333        assert!((r.bandwidth - 0.5).abs() < 1e-10);
1334        assert_eq!(r.x.len(), 128);
1335    }
1336
1337    #[test]
1338    fn kde_edge_cases() {
1339        // Too few points
1340        assert!(kde(&[1.0], BandwidthMethod::Silverman, 256).is_none());
1341        // Empty
1342        assert!(kde(&[], BandwidthMethod::Silverman, 256).is_none());
1343        // NaN
1344        assert!(kde(&[1.0, f64::NAN], BandwidthMethod::Silverman, 256).is_none());
1345        // Constant data (zero variance)
1346        assert!(kde(&[5.0, 5.0, 5.0], BandwidthMethod::Silverman, 256).is_none());
1347        // Too few grid points
1348        assert!(kde(&[1.0, 2.0, 3.0], BandwidthMethod::Manual(1.0), 1).is_none());
1349        // Invalid manual bandwidth
1350        assert!(kde(&[1.0, 2.0, 3.0], BandwidthMethod::Manual(0.0), 256).is_none());
1351        assert!(kde(&[1.0, 2.0, 3.0], BandwidthMethod::Manual(-1.0), 256).is_none());
1352    }
1353
1354    #[test]
1355    fn kde_bimodal() {
1356        // Two clusters: around 0 and around 10
1357        let mut data = Vec::new();
1358        for i in 0..50 {
1359            data.push(i as f64 * 0.1); // 0.0 to 4.9
1360        }
1361        for i in 0..50 {
1362            data.push(10.0 + i as f64 * 0.1); // 10.0 to 14.9
1363        }
1364        let r = kde(&data, BandwidthMethod::Silverman, 512).expect("should compute");
1365        // Should have higher density near the two clusters
1366        // Find density at x ≈ 2.5 (in cluster 1) and x ≈ 7.5 (between clusters)
1367        let idx_cluster = r.x.iter().position(|&x| x >= 2.5).expect("grid point");
1368        let idx_valley = r.x.iter().position(|&x| x >= 7.5).expect("grid point");
1369        assert!(
1370            r.density[idx_cluster] > r.density[idx_valley],
1371            "cluster density ({}) should exceed valley density ({})",
1372            r.density[idx_cluster],
1373            r.density[idx_valley]
1374        );
1375    }
1376
1377    #[test]
1378    fn kde_evaluate_single() {
1379        let data = [0.0, 1.0, 2.0, 3.0, 4.0];
1380        let h = 1.0;
1381        let d = kde_evaluate(&data, h, 2.0).expect("should compute");
1382        assert!(d > 0.0);
1383        // At the center of the data, density should be highest
1384        let d_edge = kde_evaluate(&data, h, -5.0).expect("should compute");
1385        assert!(d > d_edge, "center density ({d}) > edge density ({d_edge})");
1386    }
1387
1388    #[test]
1389    fn kde_evaluate_edge_cases() {
1390        assert!(kde_evaluate(&[], 1.0, 0.0).is_none());
1391        assert!(kde_evaluate(&[1.0], 0.0, 0.0).is_none());
1392        assert!(kde_evaluate(&[1.0], -1.0, 0.0).is_none());
1393        assert!(kde_evaluate(&[1.0], 1.0, f64::NAN).is_none());
1394        assert!(kde_evaluate(&[f64::NAN], 1.0, 0.0).is_none());
1395    }
1396
1397    #[test]
1398    fn kde_bandwidth_methods() {
1399        let data: Vec<f64> = (0..50).map(|i| i as f64).collect();
1400        let h_silv = kde_bandwidth(&data, BandwidthMethod::Silverman).expect("silverman");
1401        let h_scott = kde_bandwidth(&data, BandwidthMethod::Scott).expect("scott");
1402        let h_manual = kde_bandwidth(&data, BandwidthMethod::Manual(2.5)).expect("manual");
1403        assert!(h_silv > 0.0);
1404        assert!(h_scott > 0.0);
1405        assert!((h_manual - 2.5).abs() < 1e-10);
1406        // Scott produces slightly larger bandwidth than Silverman for uniform data
1407        // (1.06σ vs 0.9*min(σ, IQR/1.34))
1408        assert!(h_scott > h_silv);
1409    }
1410
1411    #[test]
1412    fn kde_bandwidth_edge_cases() {
1413        assert!(kde_bandwidth(&[1.0], BandwidthMethod::Silverman).is_none());
1414        assert!(kde_bandwidth(&[5.0, 5.0], BandwidthMethod::Silverman).is_none());
1415        assert!(kde_bandwidth(&[1.0, 2.0], BandwidthMethod::Manual(0.0)).is_none());
1416    }
1417
1418    #[test]
1419    fn kde_grid_extends_beyond_data() {
1420        let data = [10.0, 20.0, 30.0];
1421        let r = kde(&data, BandwidthMethod::Manual(2.0), 64).expect("should compute");
1422        // Grid should extend 3*h = 6 beyond data range [10, 30]
1423        assert!(r.x[0] < 10.0 - 2.0, "grid starts at {}", r.x[0]);
1424        assert!(
1425            *r.x.last().expect("non-empty") > 30.0 + 2.0,
1426            "grid ends at {}",
1427            r.x.last().expect("non-empty")
1428        );
1429    }
1430
1431    // -----------------------------------------------------------------------
1432    // Distribution Fitting
1433    // -----------------------------------------------------------------------
1434
1435    #[test]
1436    fn fit_normal_basic() {
1437        let data = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
1438        let r = fit_normal(&data).expect("should compute");
1439        assert_eq!(r.distribution, "Normal");
1440        assert_eq!(r.n_params, 2);
1441        assert!((r.parameters[0].1 - 5.0).abs() < 1e-10, "μ̂ = 5.0");
1442        // Biased MLE σ̂ = √(Σ(xᵢ-x̄)²/n) = √(32/8) = 2.0
1443        // (xᵢ-5)² = 9,1,1,1,0,0,4,16 → sum = 32
1444        let expected_sigma = (32.0_f64 / 8.0).sqrt();
1445        assert!(
1446            (r.parameters[1].1 - expected_sigma).abs() < 1e-10,
1447            "σ̂ = {}, expected {}",
1448            r.parameters[1].1,
1449            expected_sigma
1450        );
1451        assert!(r.log_likelihood.is_finite());
1452        assert!(r.aic.is_finite());
1453        assert!(r.bic.is_finite());
1454    }
1455
1456    #[test]
1457    fn fit_normal_edge_cases() {
1458        assert!(fit_normal(&[1.0]).is_none()); // < 2
1459        assert!(fit_normal(&[]).is_none());
1460        assert!(fit_normal(&[1.0, f64::NAN]).is_none());
1461        assert!(fit_normal(&[5.0, 5.0, 5.0]).is_none()); // zero variance
1462    }
1463
1464    #[test]
1465    fn fit_exponential_basic() {
1466        let data = [0.5, 1.2, 0.8, 2.1, 0.3, 1.5, 0.9, 1.8];
1467        let r = fit_exponential(&data).expect("should compute");
1468        assert_eq!(r.distribution, "Exponential");
1469        assert_eq!(r.n_params, 1);
1470        let mean: f64 = data.iter().sum::<f64>() / data.len() as f64;
1471        let expected_lambda = 1.0 / mean;
1472        assert!(
1473            (r.parameters[0].1 - expected_lambda).abs() < 1e-10,
1474            "λ̂ = {}, expected {}",
1475            r.parameters[0].1,
1476            expected_lambda
1477        );
1478    }
1479
1480    #[test]
1481    fn fit_exponential_edge_cases() {
1482        assert!(fit_exponential(&[1.0]).is_none()); // < 2
1483        assert!(fit_exponential(&[1.0, -1.0]).is_none()); // negative
1484        assert!(fit_exponential(&[0.0, 1.0]).is_none()); // zero value
1485        assert!(fit_exponential(&[1.0, f64::NAN]).is_none());
1486    }
1487
1488    #[test]
1489    fn fit_gamma_basic() {
1490        // Generate Gamma-like data (known α ≈ 4, β ≈ 2, mean = α/β = 2)
1491        let data = [
1492            1.5, 2.1, 1.8, 2.5, 1.9, 2.3, 2.0, 1.7, 2.4, 2.2, 1.6, 2.6, 1.4, 2.8, 2.1, 1.9, 2.3,
1493            2.0, 1.8, 2.5,
1494        ];
1495        let r = fit_gamma(&data).expect("should compute");
1496        assert_eq!(r.distribution, "Gamma");
1497        assert_eq!(r.n_params, 2);
1498        assert!(r.parameters[0].1 > 0.0, "α̂ > 0");
1499        assert!(r.parameters[1].1 > 0.0, "β̂ > 0");
1500        assert!(r.log_likelihood.is_finite());
1501    }
1502
1503    #[test]
1504    fn fit_gamma_edge_cases() {
1505        assert!(fit_gamma(&[1.0]).is_none()); // < 2
1506        assert!(fit_gamma(&[1.0, -1.0]).is_none()); // negative
1507        assert!(fit_gamma(&[0.0, 1.0]).is_none()); // zero
1508        assert!(fit_gamma(&[1.0, f64::NAN]).is_none());
1509        assert!(fit_gamma(&[5.0, 5.0, 5.0]).is_none()); // zero variance
1510    }
1511
1512    #[test]
1513    fn fit_gamma_mean_rate_relationship() {
1514        let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
1515        let r = fit_gamma(&data).expect("should compute");
1516        let alpha = r.parameters[0].1;
1517        let beta = r.parameters[1].1;
1518        let mean: f64 = data.iter().sum::<f64>() / data.len() as f64;
1519        // β̂ = α̂/x̄, so α̂/β̂ ≈ x̄
1520        assert!(
1521            (alpha / beta - mean).abs() < 1e-6,
1522            "α/β = {}, mean = {}",
1523            alpha / beta,
1524            mean
1525        );
1526    }
1527
1528    #[test]
1529    fn fit_best_sorted_by_aic() {
1530        let data = [2.1, 3.5, 1.8, 4.2, 2.9, 3.1, 2.5, 3.8, 1.5, 2.7];
1531        let fits = fit_best(&data);
1532        assert!(!fits.is_empty());
1533        // Should include Normal and Gamma (and Exponential since all > 0)
1534        assert!(fits.len() >= 2, "got {} fits", fits.len());
1535        // Verify AIC ordering
1536        for w in fits.windows(2) {
1537            assert!(
1538                w[0].aic <= w[1].aic,
1539                "AIC not sorted: {} > {}",
1540                w[0].aic,
1541                w[1].aic
1542            );
1543        }
1544    }
1545
1546    #[test]
1547    fn fit_best_mixed_sign_data() {
1548        // Negative values → only Normal should succeed
1549        let data = [-2.0, -1.0, 0.5, 1.0, 2.0, 3.0, 1.5, -0.5];
1550        let fits = fit_best(&data);
1551        assert_eq!(fits.len(), 1, "only Normal should fit");
1552        assert_eq!(fits[0].distribution, "Normal");
1553    }
1554
1555    #[test]
1556    fn digamma_known_values() {
1557        // ψ(1) = -γ ≈ -0.5772156649
1558        assert!((digamma(1.0) + 0.5772156649).abs() < 1e-8);
1559        // ψ(2) = 1 - γ ≈ 0.4227843351
1560        assert!((digamma(2.0) - 0.4227843351).abs() < 1e-8);
1561        // ψ(0.5) = -γ - 2·ln(2) ≈ -1.9635100260
1562        assert!((digamma(0.5) + 1.9635100260).abs() < 1e-7);
1563    }
1564
1565    #[test]
1566    fn trigamma_known_values() {
1567        // ψ'(1) = π²/6 ≈ 1.6449340668
1568        assert!((trigamma(1.0) - std::f64::consts::PI.powi(2) / 6.0).abs() < 1e-7);
1569        // ψ'(2) = π²/6 - 1 ≈ 0.6449340668
1570        assert!((trigamma(2.0) - (std::f64::consts::PI.powi(2) / 6.0 - 1.0)).abs() < 1e-7);
1571    }
1572
1573    // -----------------------------------------------------------------------
1574    // LogNormal fitting
1575    // -----------------------------------------------------------------------
1576
1577    #[test]
1578    fn fit_lognormal_basic() {
1579        let data = [1.2, 2.5, 1.8, 3.1, 0.9, 2.0, 1.5, 4.0];
1580        let r = fit_lognormal(&data).expect("should compute");
1581        assert_eq!(r.distribution, "LogNormal");
1582        assert_eq!(r.n_params, 2);
1583        assert!(r.parameters[1].1 > 0.0); // σ̂ > 0
1584        assert!(r.log_likelihood.is_finite());
1585    }
1586
1587    #[test]
1588    fn fit_lognormal_params_match_log_transform() {
1589        let data = [1.0, 2.0, 3.0, 4.0, 5.0];
1590        let r = fit_lognormal(&data).expect("should compute");
1591        let mu = r.parameters[0].1;
1592        let sigma = r.parameters[1].1;
1593        // μ̂ should be mean of ln(data)
1594        let log_data: Vec<f64> = data.iter().map(|&x| x.ln()).collect();
1595        let expected_mu: f64 = log_data.iter().sum::<f64>() / 5.0;
1596        assert!(
1597            (mu - expected_mu).abs() < 1e-10,
1598            "μ = {}, expected {}",
1599            mu,
1600            expected_mu
1601        );
1602        assert!(sigma > 0.0);
1603    }
1604
1605    #[test]
1606    fn fit_lognormal_edge_cases() {
1607        assert!(fit_lognormal(&[1.0]).is_none()); // < 2
1608        assert!(fit_lognormal(&[1.0, -1.0]).is_none()); // negative
1609        assert!(fit_lognormal(&[0.0, 1.0]).is_none()); // zero
1610    }
1611
1612    // -----------------------------------------------------------------------
1613    // Poisson fitting
1614    // -----------------------------------------------------------------------
1615
1616    #[test]
1617    fn fit_poisson_basic() {
1618        let data = [2.0, 3.0, 1.0, 4.0, 2.0, 3.0, 1.0, 2.0];
1619        let r = fit_poisson(&data).expect("should compute");
1620        assert_eq!(r.distribution, "Poisson");
1621        assert_eq!(r.n_params, 1);
1622        let mean: f64 = data.iter().sum::<f64>() / data.len() as f64;
1623        assert!(
1624            (r.parameters[0].1 - mean).abs() < 1e-10,
1625            "λ̂ = {}, expected {}",
1626            r.parameters[0].1,
1627            mean
1628        );
1629    }
1630
1631    #[test]
1632    fn fit_poisson_edge_cases() {
1633        assert!(fit_poisson(&[1.0]).is_none()); // < 2
1634        assert!(fit_poisson(&[1.0, -1.0]).is_none()); // negative
1635        assert!(fit_poisson(&[0.0, 0.0]).is_none()); // all zeros
1636        assert!(fit_poisson(&[1.0, 2.5]).is_none()); // not a count
1637    }
1638
1639    // -----------------------------------------------------------------------
1640    // Beta fitting
1641    // -----------------------------------------------------------------------
1642
1643    #[test]
1644    fn fit_beta_basic() {
1645        let data = [0.2, 0.35, 0.5, 0.15, 0.45, 0.3, 0.6, 0.25, 0.4, 0.55];
1646        let r = fit_beta(&data).expect("should compute");
1647        assert_eq!(r.distribution, "Beta");
1648        assert_eq!(r.n_params, 2);
1649        assert!(r.parameters[0].1 > 0.0, "α̂ > 0");
1650        assert!(r.parameters[1].1 > 0.0, "β̂ > 0");
1651        assert!(r.log_likelihood.is_finite());
1652    }
1653
1654    #[test]
1655    fn fit_beta_symmetric() {
1656        // Symmetric data around 0.5 → α ≈ β
1657        let data = [0.3, 0.4, 0.5, 0.6, 0.7, 0.35, 0.45, 0.55, 0.65, 0.5];
1658        let r = fit_beta(&data).expect("should compute");
1659        let alpha = r.parameters[0].1;
1660        let beta = r.parameters[1].1;
1661        // α and β should be roughly equal for symmetric data
1662        assert!(
1663            (alpha - beta).abs() / alpha < 0.3,
1664            "α = {alpha}, β = {beta}, should be roughly equal"
1665        );
1666    }
1667
1668    #[test]
1669    fn fit_beta_edge_cases() {
1670        assert!(fit_beta(&[0.5]).is_none()); // < 2
1671        assert!(fit_beta(&[0.5, 1.0]).is_none()); // value = 1.0 (boundary)
1672        assert!(fit_beta(&[0.0, 0.5]).is_none()); // value = 0.0 (boundary)
1673        assert!(fit_beta(&[0.5, 1.5]).is_none()); // value > 1
1674        assert!(fit_beta(&[0.5, -0.5]).is_none()); // value < 0
1675    }
1676
1677    // -----------------------------------------------------------------------
1678    // fit_best (updated)
1679    // -----------------------------------------------------------------------
1680
1681    #[test]
1682    fn fit_best_compares_the_continuous_families_that_apply() {
1683        let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
1684        let fits = fit_best(&data);
1685        let names: Vec<&str> = fits.iter().map(|f| f.distribution.as_str()).collect();
1686        // All > 0 and above 1: every positive family, not Beta.
1687        for family in ["Normal", "Exponential", "Gamma", "LogNormal", "Weibull"] {
1688            assert!(
1689                names.contains(&family),
1690                "should include {family}: {names:?}"
1691            );
1692        }
1693        assert!(!names.contains(&"Beta"), "{names:?}");
1694        // Counts are still data a density is fitted to; a mass is not ranked
1695        // against densities.
1696        assert!(!names.contains(&"Poisson"), "{names:?}");
1697
1698        let proportions = [0.2, 0.35, 0.5, 0.15, 0.45, 0.3, 0.6, 0.25, 0.4, 0.55];
1699        let names: Vec<String> = fit_best(&proportions)
1700            .into_iter()
1701            .map(|f| f.distribution)
1702            .collect();
1703        assert!(names.iter().any(|n| n == "Beta"), "{names:?}");
1704    }
1705
1706    /// The ranking a mass and a density cannot share: rescaling continuous
1707    /// data moves every density's log-likelihood by n·ln(scale) and leaves the
1708    /// order among densities alone -- so the winner must not depend on units.
1709    #[test]
1710    fn fit_best_ranking_does_not_depend_on_the_unit() {
1711        let data = [2.0, 3.0, 1.0, 4.0, 2.0, 3.0, 5.0, 2.0, 3.0, 4.0];
1712        let in_thousandths: Vec<f64> = data.iter().map(|v| v * 1000.0).collect();
1713        let order = |d: &[f64]| -> Vec<String> {
1714            fit_best(d).into_iter().map(|f| f.distribution).collect()
1715        };
1716        assert_eq!(order(&data), order(&in_thousandths));
1717    }
1718
1719    #[test]
1720    fn fit_weibull_matches_the_weibull_mle() {
1721        let data = [12.0, 25.0, 31.0, 44.0, 52.0, 60.0, 71.0, 85.0];
1722        let fit = fit_weibull(&data).expect("positive data");
1723        let mle = crate::weibull::weibull_mle(&data).expect("same data");
1724        assert_eq!(fit.parameters[0], ("shape".to_string(), mle.shape));
1725        assert_eq!(fit.parameters[1], ("scale".to_string(), mle.scale));
1726        assert!((fit.aic - (-2.0 * mle.log_likelihood + 4.0)).abs() < 1e-12);
1727        assert!(fit_weibull(&[1.0, 0.0, 2.0]).is_none());
1728    }
1729}
1730
1731#[cfg(test)]
1732mod proptests {
1733    use super::*;
1734    use proptest::prelude::*;
1735
1736    proptest! {
1737        #[test]
1738        fn ecdf_last_is_one(
1739            data in proptest::collection::vec(-1e3_f64..1e3, 1..=50)
1740        ) {
1741            if let Some((_, probs)) = ecdf(&data) {
1742                let last = *probs.last().expect("non-empty");
1743                prop_assert!((last - 1.0).abs() < 1e-10, "last prob = {last}");
1744            }
1745        }
1746
1747        #[test]
1748        fn ecdf_monotonic(
1749            data in proptest::collection::vec(-1e3_f64..1e3, 2..=50)
1750        ) {
1751            if let Some((_, probs)) = ecdf(&data) {
1752                for w in probs.windows(2) {
1753                    prop_assert!(w[1] >= w[0] - 1e-15, "{} < {}", w[1], w[0]);
1754                }
1755            }
1756        }
1757
1758        #[test]
1759        fn hist_counts_sum_to_n(
1760            data in proptest::collection::vec(-1e3_f64..1e3, 5..=100)
1761        ) {
1762            if let Some(r) = histogram_bins(&data, BinMethod::Sturges) {
1763                let total: usize = r.counts.iter().sum();
1764                prop_assert_eq!(total, data.len());
1765            }
1766        }
1767
1768        #[test]
1769        fn kde_density_non_negative(
1770            data in proptest::collection::vec(-100.0_f64..100.0, 5..=50)
1771        ) {
1772            if let Some(r) = kde(&data, BandwidthMethod::Silverman, 128) {
1773                for &d in &r.density {
1774                    prop_assert!(d >= 0.0, "negative density: {d}");
1775                }
1776            }
1777        }
1778
1779        #[test]
1780        fn kde_integral_approx_one(
1781            data in proptest::collection::vec(-100.0_f64..100.0, 10..=80)
1782        ) {
1783            if let Some(r) = kde(&data, BandwidthMethod::Silverman, 512) {
1784                let dx = r.x[1] - r.x[0];
1785                let integral: f64 = r.density.iter().sum::<f64>() * dx;
1786                prop_assert!(
1787                    (integral - 1.0).abs() < 0.1,
1788                    "integral = {integral}, expected ≈ 1.0"
1789                );
1790            }
1791        }
1792
1793        #[test]
1794        fn fit_normal_aic_finite(
1795            data in proptest::collection::vec(-100.0_f64..100.0, 5..=50)
1796        ) {
1797            if let Some(r) = fit_normal(&data) {
1798                prop_assert!(r.aic.is_finite(), "AIC = {}", r.aic);
1799                prop_assert!(r.bic.is_finite(), "BIC = {}", r.bic);
1800                prop_assert!(r.log_likelihood.is_finite(), "LL = {}", r.log_likelihood);
1801            }
1802        }
1803
1804        #[test]
1805        fn fit_gamma_alpha_positive(
1806            data in proptest::collection::vec(0.1_f64..100.0, 5..=50)
1807        ) {
1808            if let Some(r) = fit_gamma(&data) {
1809                let alpha = r.parameters[0].1;
1810                let beta = r.parameters[1].1;
1811                prop_assert!(alpha > 0.0, "α = {alpha}");
1812                prop_assert!(beta > 0.0, "β = {beta}");
1813                prop_assert!(r.aic.is_finite(), "AIC = {}", r.aic);
1814            }
1815        }
1816    }
1817}