Skip to main content

rich_data/
stats.rs

1//! Column statistics (#261): count, nulls, distinct values, min, max, mean,
2//! median, quantiles and the most common values, per column.
3//!
4//! A column is numeric when every non-null cell is a number, or text that
5//! parses as one ([`infer::parse_float`](crate::infer::parse_float)); its
6//! min and max are numeric, and it gets a mean, a median and quantiles
7//! (linear interpolation between closest ranks, as NumPy's default). Any
8//! other column's min and max compare the cells' text. Nulls are
9//! [`Value::Null`] and empty text; run [`Inference::apply`] first for other
10//! null tokens.
11//!
12//! [`Stats`] renders as a table, a row per column; [`Stats::headers`] gives
13//! a short summary to show under each column heading instead.
14//!
15//! [`Inference::apply`]: crate::infer::Inference::apply
16//!
17//! ```
18//! use rich_data::stats::Stats;
19//! use rich_data::{Rows, Value};
20//!
21//! let mut rows = Rows::new(["service", "p99"]);
22//! for (service, p99) in [("web", 120), ("api", 35), ("web", 80), ("db", 5)] {
23//!     rows.push([service.into(), Value::Int(p99)]);
24//! }
25//! let stats = Stats::of(&rows);
26//! let p99 = &stats.columns()[1];
27//! assert_eq!((p99.count, p99.nulls, p99.distinct), (4, 0, 4));
28//! assert_eq!(p99.mean, Some(60.0));
29//! assert_eq!(p99.median, Some(57.5));
30//! let service = &stats.columns()[0];
31//! assert_eq!(service.top[0], ("web".to_string(), 2));
32//! assert_eq!(service.min.as_deref(), Some("api"));
33//! ```
34
35use std::collections::HashMap;
36
37use rich::{Console, ConsoleOptions, Justify, Renderable, Segment, Table, Text};
38
39use crate::infer::{parse_float, parse_int};
40use crate::{Rows, Value};
41
42/// What [`Stats`] computes beyond the counts.
43#[derive(Clone, Debug, PartialEq)]
44pub struct StatsOptions {
45    /// The quantiles to compute, each in `0.0..=1.0` (default the quartiles
46    /// 0.25 and 0.75; the median is always computed).
47    pub quantiles: Vec<f64>,
48    /// How many of the most common values to keep (default 3).
49    pub top: usize,
50}
51
52impl Default for StatsOptions {
53    fn default() -> Self {
54        StatsOptions {
55            quantiles: vec![0.25, 0.75],
56            top: 3,
57        }
58    }
59}
60
61/// One column's statistics.
62#[derive(Clone, Debug, PartialEq)]
63pub struct ColumnStats {
64    /// The column name.
65    pub name: String,
66    /// Non-null cells.
67    pub count: usize,
68    /// Null or empty cells.
69    pub nulls: usize,
70    /// Distinct non-null values (by their text).
71    pub distinct: usize,
72    /// Whether every non-null cell is a number.
73    pub numeric: bool,
74    /// The smallest value, as text (numerically for a numeric column).
75    pub min: Option<String>,
76    /// The largest value, as text.
77    pub max: Option<String>,
78    /// The mean, for a numeric column.
79    pub mean: Option<f64>,
80    /// The median, for a numeric column.
81    pub median: Option<f64>,
82    /// Each requested quantile and its value, for a numeric column.
83    pub quantiles: Vec<(f64, f64)>,
84    /// The most common values and their counts, most common first (ties in
85    /// text order).
86    pub top: Vec<(String, usize)>,
87}
88
89impl ColumnStats {
90    /// A one-line summary: `n=4 · min 5 · max 120 · mean 60`, or for text
91    /// `n=4 · 3 distinct · top web (2)`, with `· 1 null` when there are any.
92    pub fn summary(&self) -> String {
93        let mut parts = vec![format!("n={}", self.count)];
94        if self.nulls > 0 {
95            parts.push(format!("{} null", self.nulls));
96        }
97        if self.numeric {
98            if let (Some(min), Some(max)) = (&self.min, &self.max) {
99                parts.push(format!("min {min}"));
100                parts.push(format!("max {max}"));
101            }
102            if let Some(mean) = self.mean {
103                parts.push(format!("mean {}", number(mean)));
104            }
105        } else {
106            parts.push(format!("{} distinct", self.distinct));
107            if let Some((value, count)) = self.top.first() {
108                parts.push(format!("top {value} ({count})"));
109            }
110        }
111        parts.join(" · ")
112    }
113}
114
115/// Statistics for every column. See the [module docs](self).
116#[derive(Clone, Debug, PartialEq)]
117pub struct Stats {
118    columns: Vec<ColumnStats>,
119    quantiles: Vec<f64>,
120}
121
122impl Stats {
123    /// Statistics for `rows` with the default options.
124    pub fn of(rows: &Rows) -> Self {
125        Stats::with_options(rows, &StatsOptions::default())
126    }
127
128    /// Statistics for `rows`.
129    pub fn with_options(rows: &Rows, options: &StatsOptions) -> Self {
130        let quantiles: Vec<f64> = options
131            .quantiles
132            .iter()
133            .copied()
134            .filter(|q| (0.0..=1.0).contains(q))
135            .collect();
136        let columns = rows
137            .columns()
138            .iter()
139            .enumerate()
140            .map(|(index, name)| column(name, rows.column(index), &quantiles, options.top))
141            .collect();
142        Stats { columns, quantiles }
143    }
144
145    /// One entry per column, in order.
146    pub fn columns(&self) -> &[ColumnStats] {
147        &self.columns
148    }
149
150    /// Each column's name with its [`summary`](ColumnStats::summary) on a
151    /// second line, to use as table headings.
152    pub fn headers(&self) -> Vec<String> {
153        self.columns
154            .iter()
155            .map(|c| format!("{}\n{}", c.name, c.summary()))
156            .collect()
157    }
158
159    /// The table this renders as: a row per column with count, nulls,
160    /// distinct, min, max, mean, median, the quantiles and the top values.
161    pub fn to_table(&self) -> Table {
162        let mut table = Table::new();
163        table.add_column("column");
164        for heading in ["count", "nulls", "distinct", "min", "max", "mean", "median"] {
165            table.add_column_justify(heading, Justify::Right);
166        }
167        for q in &self.quantiles {
168            table.add_column_justify(format!("p{}", number(q * 100.0)), Justify::Right);
169        }
170        table.add_column("top");
171        for c in &self.columns {
172            let optional = |v: Option<f64>| v.map(number).unwrap_or_default();
173            let mut cells = vec![
174                c.name.clone(),
175                c.count.to_string(),
176                c.nulls.to_string(),
177                c.distinct.to_string(),
178                c.min.clone().unwrap_or_default(),
179                c.max.clone().unwrap_or_default(),
180                optional(c.mean),
181                optional(c.median),
182            ];
183            for q in &self.quantiles {
184                let value = c.quantiles.iter().find(|(at, _)| at == q).map(|(_, v)| *v);
185                cells.push(optional(value));
186            }
187            let top: Vec<String> = c.top.iter().map(|(v, n)| format!("{v} ({n})")).collect();
188            cells.push(top.join(", "));
189            table.add_row_text(cells.into_iter().map(Text::new).collect());
190        }
191        table
192    }
193}
194
195impl Renderable for Stats {
196    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
197        self.to_table().rich_render(console, options)
198    }
199
200    fn measure(&self, console: &Console, options: &ConsoleOptions) -> rich::measure::Measurement {
201        self.to_table().measure(console, options)
202    }
203}
204
205fn column<'a>(
206    name: &str,
207    values: impl Iterator<Item = &'a Value>,
208    quantiles: &[f64],
209    top: usize,
210) -> ColumnStats {
211    let mut nulls = 0;
212    let mut counts: HashMap<String, usize> = HashMap::new();
213    let mut numbers: Vec<f64> = Vec::new();
214    // Every value as an exact integer while they all are one, so integers
215    // past 2^53 keep their order and their printed min and max.
216    let mut integers: Option<Vec<i64>> = Some(Vec::new());
217    let mut numeric = true;
218    for value in values {
219        if value.is_empty() {
220            nulls += 1;
221            continue;
222        }
223        let text = value.plain();
224        if numeric {
225            let integer = match value {
226                Value::Int(n) => Some(*n),
227                Value::Float(_) => None,
228                _ => parse_int(text.trim()),
229            };
230            match (integer, integers.as_mut()) {
231                (Some(n), Some(exact)) => exact.push(n),
232                _ => integers = None,
233            }
234            match value.as_f64().or_else(|| parse_float(text.trim())) {
235                Some(n) if !n.is_nan() => numbers.push(n),
236                _ => numeric = false,
237            }
238        }
239        *counts.entry(text).or_insert(0) += 1;
240    }
241    let count: usize = counts.values().sum();
242    let mut ranked: Vec<(String, usize)> = counts.into_iter().collect();
243    ranked.sort_by(|(a, m), (b, n)| n.cmp(m).then_with(|| a.cmp(b)));
244    let distinct = ranked.len();
245    let numeric = numeric && count > 0;
246    let (min, max, mean, median, quantile_values) = if numeric && integers.is_some() {
247        let mut exact = integers.unwrap_or_default();
248        exact.sort_unstable();
249        let sum: i128 = exact.iter().map(|&n| i128::from(n)).sum();
250        (
251            exact.first().map(i64::to_string),
252            exact.last().map(i64::to_string),
253            Some(sum as f64 / exact.len() as f64),
254            Some(integer_quantile(&exact, 0.5)),
255            quantiles
256                .iter()
257                .map(|&q| (q, integer_quantile(&exact, q)))
258                .collect(),
259        )
260    } else if numeric {
261        numbers.sort_by(f64::total_cmp);
262        let n = numbers.len() as f64;
263        let mut mean = numbers.iter().sum::<f64>() / n;
264        if !mean.is_finite() && numbers.iter().all(|x| x.is_finite()) {
265            // The sum overflowed; scaled first, it cannot.
266            mean = numbers.iter().map(|x| x / n).sum();
267        }
268        (
269            numbers.first().map(|n| number(*n)),
270            numbers.last().map(|n| number(*n)),
271            Some(mean),
272            Some(quantile(&numbers, 0.5)),
273            quantiles
274                .iter()
275                .map(|&q| (q, quantile(&numbers, q)))
276                .collect(),
277        )
278    } else {
279        let min = ranked.iter().map(|(v, _)| v).min().cloned();
280        let max = ranked.iter().map(|(v, _)| v).max().cloned();
281        (min, max, None, None, Vec::new())
282    };
283    ranked.truncate(top);
284    ColumnStats {
285        name: name.to_string(),
286        count,
287        nulls,
288        distinct,
289        numeric,
290        min,
291        max,
292        mean,
293        median,
294        quantiles: quantile_values,
295        top: ranked,
296    }
297}
298
299/// The `q` quantile of sorted, non-empty `values`, interpolating linearly
300/// between the closest ranks.
301fn quantile(values: &[f64], q: f64) -> f64 {
302    let position = q * (values.len() - 1) as f64;
303    let below = position.floor() as usize;
304    let above = position.ceil() as usize;
305    if values[below] == values[above] {
306        // An exact rank, or equal neighbours: no interpolation, so an
307        // infinite value is itself rather than `inf - inf`'s NaN.
308        return values[below];
309    }
310    let (low, high, t) = (values[below], values[above], position - below as f64);
311    let gap = high - low;
312    if gap.is_infinite() && low.is_finite() && high.is_finite() {
313        // The gap overflows (-1e308 to 1e308); the weighted sum cannot.
314        return low * (1.0 - t) + high * t;
315    }
316    low + gap * t
317}
318
319/// [`quantile`] over sorted, non-empty integers: the interpolation runs on
320/// the exact neighbours, and only the result becomes a float.
321fn integer_quantile(values: &[i64], q: f64) -> f64 {
322    let position = q * (values.len() - 1) as f64;
323    let below = position.floor() as usize;
324    let above = position.ceil() as usize;
325    let gap = i128::from(values[above]) - i128::from(values[below]);
326    values[below] as f64 + gap as f64 * (position - below as f64)
327}
328
329/// A number for display: whole numbers without a point, others to at most
330/// four decimals, and very small ones in scientific notation.
331pub fn number(n: f64) -> String {
332    if !n.is_finite() {
333        return n.to_string();
334    }
335    if n.fract() == 0.0 && n.abs() < 1e15 {
336        return format!("{n:.0}");
337    }
338    let fixed = format!("{n:.4}");
339    let fixed = fixed.trim_end_matches('0').trim_end_matches('.');
340    if fixed == "0" || fixed == "-0" {
341        format!("{n:.3e}")
342    } else {
343        fixed.to_string()
344    }
345}
346
347#[cfg(test)]
348mod tests {
349    use super::*;
350
351    #[test]
352    fn integers_past_two_to_the_53_keep_their_min_and_max() {
353        let values = [
354            Value::Int(i64::MAX),
355            Value::Int(i64::MAX - 1),
356            Value::Str("9007199254740993".into()),
357        ];
358        let stats = column("big", values.iter(), &[0.25], 3);
359        assert!(stats.numeric);
360        assert_eq!(stats.min.as_deref(), Some("9007199254740993"));
361        assert_eq!(stats.max.as_deref(), Some("9223372036854775807"));
362        let only = [Value::Int(i64::MAX)];
363        let stats = column("max", only.iter(), &[], 3);
364        assert_eq!(stats.min.as_deref(), Some("9223372036854775807"));
365        assert_eq!(stats.max.as_deref(), Some("9223372036854775807"));
366        assert_eq!(integer_quantile(&[1, 2, 3, 4], 0.5), 2.5);
367    }
368
369    #[test]
370    fn numbers_print_short() {
371        assert_eq!(number(3.0), "3");
372        assert_eq!(number(-2.5), "-2.5");
373        assert_eq!(number(1.0 / 3.0), "0.3333");
374        assert_eq!(number(0.000012), "1.200e-5");
375        assert_eq!(number(1e20), "100000000000000000000");
376    }
377
378    #[test]
379    fn quantiles_interpolate() {
380        let values = [1.0, 2.0, 3.0, 4.0];
381        assert_eq!(quantile(&values, 0.0), 1.0);
382        assert_eq!(quantile(&values, 0.25), 1.75);
383        assert_eq!(quantile(&values, 0.5), 2.5);
384        assert_eq!(quantile(&values, 1.0), 4.0);
385    }
386
387    /// An infinite value (text such as `1e999` parses as one) at an exact
388    /// rank is that value, not `inf - inf`'s NaN; and finite values whose
389    /// sum overflows still have a finite mean.
390    #[test]
391    fn infinities_and_huge_values_keep_their_statistics() {
392        let mut rows = Rows::new(["inf", "huge"]);
393        for (a, b) in [("1", "1e308"), ("1e999", "1e308"), ("1e999", "1e300")] {
394            rows.push([a.into(), b.into()]);
395        }
396        let stats = Stats::of(&rows);
397        let inf = &stats.columns()[0];
398        assert_eq!(inf.median, Some(f64::INFINITY));
399        assert_eq!(inf.quantiles[1], (0.75, f64::INFINITY));
400        assert_eq!(inf.mean, Some(f64::INFINITY));
401        let huge = &stats.columns()[1];
402        let mean = huge.mean.unwrap();
403        let expected = 2.0 / 3.0 * 1e308 + 1e300 / 3.0;
404        assert!((mean / expected - 1.0).abs() < 1e-12, "{mean}");
405        assert_eq!(huge.median, Some(1e308));
406    }
407
408    /// Interpolating between finite neighbours whose gap overflows still
409    /// gives a finite quantile: the median of -1e308 and 1e308 is 0, not inf.
410    #[test]
411    fn quantiles_between_far_apart_values_stay_finite() {
412        let values = [-1e308, 1e308];
413        assert_eq!(quantile(&values, 0.5), 0.0);
414        assert_eq!(quantile(&values, 0.25), -5e307);
415        assert_eq!(quantile(&values, 0.75), 5e307);
416        assert_eq!(quantile(&[1.0, 2.0], 0.5), 1.5);
417    }
418
419    #[test]
420    fn numbers_in_text_count_and_nulls_are_skipped() {
421        let mut rows = Rows::new(["n", "s"]);
422        rows.push(["10".into(), "b".into()]);
423        rows.push(["".into(), Value::Null]);
424        rows.push([Value::Float(2.5), "a".into()]);
425        rows.push(["-1".into(), "b".into()]);
426        let stats = Stats::of(&rows);
427        let n = &stats.columns()[0];
428        assert!(n.numeric);
429        assert_eq!((n.count, n.nulls, n.distinct), (3, 1, 3));
430        assert_eq!(
431            (n.min.as_deref(), n.max.as_deref()),
432            (Some("-1"), Some("10"))
433        );
434        assert_eq!(n.quantiles, [(0.25, 0.75), (0.75, 6.25)]);
435        assert_eq!(n.summary(), "n=3 · 1 null · min -1 · max 10 · mean 3.8333");
436        let s = &stats.columns()[1];
437        assert!(!s.numeric);
438        assert_eq!(s.top, [("b".to_string(), 2), ("a".to_string(), 1)]);
439        assert_eq!(s.summary(), "n=3 · 1 null · 2 distinct · top b (2)");
440        assert_eq!(
441            stats.headers()[1],
442            "s\nn=3 · 1 null · 2 distinct · top b (2)"
443        );
444    }
445
446    #[test]
447    fn the_table_has_a_row_per_column() {
448        let mut rows = Rows::new(["n"]);
449        rows.push([Value::Int(1)]);
450        rows.push([Value::Int(3)]);
451        let out = Console::builder()
452            .width(100)
453            .build()
454            .render_export(&Stats::of(&rows));
455        let lines: Vec<&str> = out.lines().collect();
456        assert_eq!(
457            lines[1],
458            "┃ column ┃ count ┃ nulls ┃ distinct ┃ min ┃ max ┃ mean ┃ median ┃ p25 ┃ p75 ┃ top          ┃"
459        );
460        assert_eq!(
461            lines[3],
462            "│ n      │     2 │     0 │        2 │   1 │   3 │    2 │      2 │ 1.5 │ 2.5 │ 1 (1), 3 (1) │"
463        );
464    }
465}