Skip to main content

numeric_distribution/
numeric_distribution.rs

1//! Numeric distribution profiling: mean/std, five-number summary, skewness,
2//! kurtosis, histogram, and IQR outliers.
3//!
4//! Run with: `cargo run --example numeric_distribution -p datarust-profile`
5//!
6//! No features required — this reads the profile fields directly.
7
8use datarust::Matrix;
9use datarust_profile::profile_matrix;
10
11fn main() -> datarust_profile::Result<()> {
12    // A synthetic column with a visible right skew: most values cluster low,
13    // one extreme value pulls the tail.
14    let m = Matrix::from_rows(vec![
15        vec![10.0],
16        vec![12.0],
17        vec![11.0],
18        vec![13.0],
19        vec![10.0],
20        vec![14.0],
21        vec![12.0],
22        vec![11.0],
23        vec![150.0], // outlier — well above the IQR fence
24    ])?;
25
26    let p = profile_matrix(&m, Some(&["reaction_ms".into()]))?;
27    let col = &p.columns[0];
28    let n = col.numeric.as_ref().expect("numeric column");
29
30    println!(
31        "Column: {} ({} rows, {} missing)\n",
32        col.name, col.count, col.missing_count
33    );
34
35    // Central tendency + spread.
36    println!("  mean   = {:.2}", n.mean);
37    println!("  std    = {:.2}\n", n.std);
38
39    // Five-number summary (min / Q1 / median / Q3 / max).
40    println!("  five-number summary:");
41    println!("    min    = {:.1}", n.five.min);
42    println!("    Q1     = {:.1}", n.five.q1);
43    println!("    median = {:.1}", n.five.median);
44    println!("    Q3     = {:.1}", n.five.q3);
45    println!("    max    = {:.1}\n", n.five.max);
46
47    // Distributional shape (v0.2).
48    //   skew ≈ 0  → symmetric; large positive → right tail (our case).
49    //   excess kurtosis ≈ 0 → normal-like; positive → heavy-tailed.
50    println!("  shape:");
51    println!(
52        "    skewness  = {:+.3}  ({})",
53        n.skewness,
54        describe_skew(n.skewness)
55    );
56    println!(
57        "    kurtosis  = {:+.3}  ({})\n",
58        n.kurtosis,
59        describe_kurtosis(n.kurtosis)
60    );
61
62    // Histogram — equal-width bins, Sturges' rule for the count.
63    println!("  histogram ({} bins):", n.histogram.nbins());
64    let max_count = n.histogram.max_count().max(1);
65    for (i, &count) in n.histogram.counts.iter().enumerate() {
66        let lo = n.histogram.edges.get(i).copied().unwrap_or(f64::NAN);
67        let hi = n.histogram.edges.get(i + 1).copied().unwrap_or(f64::NAN);
68        let bar_len = (count as f64 / max_count as f64 * 40.0).round() as usize;
69        let bar: String = "█".repeat(bar_len);
70        println!("    [{:>7.1}, {:>7.1})  {:>3}  {}", lo, hi, count, bar);
71    }
72    println!();
73
74    // Outliers — values beyond the Tukey IQR fences.
75    println!("  outliers (IQR rule):");
76    println!(
77        "    {} found ({:.1}% of values)\n",
78        n.outlier_count,
79        n.outlier_fraction * 100.0
80    );
81
82    Ok(())
83}
84
85fn describe_skew(s: f64) -> &'static str {
86    if s > 0.5 {
87        "right-skewed (long upper tail)"
88    } else if s < -0.5 {
89        "left-skewed (long lower tail)"
90    } else {
91        "roughly symmetric"
92    }
93}
94
95fn describe_kurtosis(k: f64) -> &'static str {
96    if k > 1.0 {
97        "leptokurtic (heavy-tailed, peaked)"
98    } else if k < -1.0 {
99        "platykurtic (light-tailed, flat)"
100    } else {
101        "mesokurtic (near-normal tails)"
102    }
103}