1use 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#[derive(Clone, Debug, PartialEq)]
44pub struct StatsOptions {
45 pub quantiles: Vec<f64>,
48 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#[derive(Clone, Debug, PartialEq)]
63pub struct ColumnStats {
64 pub name: String,
66 pub count: usize,
68 pub nulls: usize,
70 pub distinct: usize,
72 pub numeric: bool,
74 pub min: Option<String>,
76 pub max: Option<String>,
78 pub mean: Option<f64>,
80 pub median: Option<f64>,
82 pub quantiles: Vec<(f64, f64)>,
84 pub top: Vec<(String, usize)>,
87}
88
89impl ColumnStats {
90 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#[derive(Clone, Debug, PartialEq)]
117pub struct Stats {
118 columns: Vec<ColumnStats>,
119 quantiles: Vec<f64>,
120}
121
122impl Stats {
123 pub fn of(rows: &Rows) -> Self {
125 Stats::with_options(rows, &StatsOptions::default())
126 }
127
128 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 pub fn columns(&self) -> &[ColumnStats] {
147 &self.columns
148 }
149
150 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 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 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 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
299fn 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 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 return low * (1.0 - t) + high * t;
315 }
316 low + gap * t
317}
318
319fn 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
329pub 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 #[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 #[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}