use std::collections::HashMap;
use crate::infer;
use crate::types::ColumnType;
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct FiveNumber {
pub min: f64,
pub q1: f64,
pub median: f64,
pub q3: f64,
pub max: f64,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct Histogram {
pub edges: Vec<f64>,
pub counts: Vec<usize>,
}
impl Histogram {
pub fn nbins(&self) -> usize {
self.counts.len()
}
pub fn max_count(&self) -> usize {
self.counts.iter().copied().max().unwrap_or(0)
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct NumericStats {
pub mean: f64,
pub std: f64,
pub five: FiveNumber,
pub skewness: f64,
pub kurtosis: f64,
pub histogram: Histogram,
pub outlier_count: usize,
pub outlier_fraction: f64,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct CategoricalStats {
pub unique: usize,
pub top: String,
pub freq: usize,
pub imbalance_ratio: f64,
pub top_values: Vec<(String, usize)>,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct ColumnProfile {
pub name: String,
pub column_type: ColumnType,
pub count: usize,
pub missing_count: usize,
pub missing_fraction: f64,
pub numeric: Option<NumericStats>,
pub categorical: Option<CategoricalStats>,
}
const TOP_VALUES_CAP: usize = 8;
pub(crate) struct PrecomputedStats {
pub(crate) mean: f64,
pub(crate) std: f64,
pub(crate) five: FiveNumber,
}
impl ColumnProfile {
pub(crate) fn from_numeric(name: String, values: &[f64]) -> Self {
Self::from_numeric_with_stats(name, values, None)
}
pub(crate) fn from_numeric_with_stats(
name: String,
values: &[f64],
precomputed: Option<PrecomputedStats>,
) -> Self {
let count = values.len();
let present: Vec<f64> = values.iter().copied().filter(|v| v.is_finite()).collect();
let missing_count = count - present.len();
let missing_fraction = if count == 0 {
0.0
} else {
missing_count as f64 / count as f64
};
let numeric = if present.is_empty() {
None
} else {
let mut sorted = present.clone();
sorted.sort_by(|a, b| a.total_cmp(b));
let (mean, std, five) = match precomputed {
Some(p) => (p.mean, p.std, p.five),
None => {
let m = datarust::stats::mean(&present);
let s = datarust::stats::std(&present, 1);
let f = FiveNumber {
min: datarust::stats::quantile(&sorted, 0.0).unwrap_or(f64::NAN),
q1: datarust::stats::quantile(&sorted, 0.25).unwrap_or(f64::NAN),
median: datarust::stats::median_sorted(&sorted).unwrap_or(f64::NAN),
q3: datarust::stats::quantile(&sorted, 0.75).unwrap_or(f64::NAN),
max: datarust::stats::quantile(&sorted, 1.0).unwrap_or(f64::NAN),
};
(m, s, f)
}
};
let skew = super::distribution::skewness(&present, mean, std);
let kurt = super::distribution::kurtosis_excess(&present, mean, std);
let histogram = super::distribution::histogram(&sorted, five.min, five.max);
let (outlier_count, outlier_fraction) =
super::distribution::outlier_count(&sorted, five.q1, five.q3);
Some(NumericStats {
mean,
std,
five,
skewness: skew,
kurtosis: kurt,
histogram,
outlier_count,
outlier_fraction,
})
};
ColumnProfile {
name,
column_type: ColumnType::Numeric,
count,
missing_count,
missing_fraction,
numeric,
categorical: None,
}
}
pub(crate) fn from_strings(name: String, cells: &[String]) -> Self {
let count = cells.len();
let missing_count = cells.iter().filter(|c| infer::is_missing(c)).count();
let missing_fraction = if count == 0 {
0.0
} else {
missing_count as f64 / count as f64
};
let column_type = infer::infer_column(cells);
match column_type {
ColumnType::Numeric => {
let values = infer::parse_numeric_column(cells);
let mut p = Self::from_numeric(name, &values);
p.count = count;
p.missing_count = missing_count;
p.missing_fraction = missing_fraction;
p
}
ColumnType::Categorical => {
let categorical = compute_categorical(cells);
ColumnProfile {
name,
column_type,
count,
missing_count,
missing_fraction,
numeric: None,
categorical,
}
}
}
}
}
fn compute_categorical(cells: &[String]) -> Option<CategoricalStats> {
let mut counts: HashMap<&str, usize> = HashMap::new();
let mut order: Vec<&str> = Vec::new();
for cell in cells {
if infer::is_missing(cell) {
continue;
}
let trimmed = cell.trim();
match counts.get(trimmed) {
None => {
counts.insert(trimmed, 1);
order.push(trimmed);
}
Some(c) => *counts.get_mut(trimmed).unwrap() = c + 1,
}
}
if order.is_empty() {
return None;
}
let present_total: usize = order.iter().map(|k| counts[*k]).sum();
let mut entries: Vec<(&str, usize)> = order.iter().map(|k| (*k, counts[*k])).collect();
entries.sort_by_key(|&(_, c)| std::cmp::Reverse(c));
let top_values: Vec<(String, usize)> = entries
.into_iter()
.take(TOP_VALUES_CAP)
.map(|(k, c)| (k.to_string(), c))
.collect();
let (top, freq) = {
let first = top_values.first().expect("non-empty");
(first.0.clone(), first.1)
};
let imbalance_ratio = if present_total == 0 {
0.0
} else {
freq as f64 / present_total as f64
};
Some(CategoricalStats {
unique: order.len(),
top,
freq,
imbalance_ratio,
top_values,
})
}