use std::collections::HashMap;
pub const COLUMN_STATS_DIM: usize = 27;
pub const COLUMN_STATS_NAMES: [&str; COLUMN_STATS_DIM] = [
"col_entropy", "frac_unique", "n_values", "frac_empty", "length_mean", "length_variance", "length_min", "length_max", "length_median", "length_skewness", "length_kurtosis", "length_sum", "frac_numeric_cells", "frac_alpha_cells", "frac_special_cells", "avg_digit_count", "std_digit_count", "avg_alpha_count", "std_alpha_count", "avg_special_count", "std_special_count", "avg_word_count", "std_word_count", "frac_starts_upper", "frac_all_upper", "frac_all_lower", "frac_mixed_case", ];
pub fn extract_column_stats(values: &[&str]) -> Option<[f32; COLUMN_STATS_DIM]> {
if values.is_empty() {
return None;
}
let mut f = [0.0f32; COLUMN_STATS_DIM];
let total = values.len();
let non_empty: Vec<&str> = values.iter().filter(|v| !v.is_empty()).copied().collect();
let n_non_empty = non_empty.len();
let n_empty = total - n_non_empty;
let mut freq: HashMap<&str, usize> = HashMap::new();
for &v in values {
*freq.entry(v).or_insert(0) += 1;
}
let n_f = total as f64;
let entropy: f64 = freq
.values()
.map(|&count| {
let p = count as f64 / n_f;
if p > 0.0 {
-p * p.ln()
} else {
0.0
}
})
.sum();
f[0] = entropy as f32; f[1] = freq.len() as f32 / total as f32; f[2] = (total as f64 + 1.0).ln() as f32; f[3] = n_empty as f32 / total as f32;
if n_non_empty == 0 {
return Some(f);
}
let n_ne_f = n_non_empty as f64;
let mut lengths: Vec<f64> = Vec::with_capacity(n_non_empty);
let mut digit_counts: Vec<f64> = Vec::with_capacity(n_non_empty);
let mut alpha_counts: Vec<f64> = Vec::with_capacity(n_non_empty);
let mut special_counts: Vec<f64> = Vec::with_capacity(n_non_empty);
let mut word_counts: Vec<f64> = Vec::with_capacity(n_non_empty);
let mut numeric_cells: usize = 0;
let mut alpha_cells: usize = 0;
let mut special_cells: usize = 0;
let mut starts_upper: usize = 0;
let mut all_upper: usize = 0;
let mut all_lower: usize = 0;
let mut mixed_case: usize = 0;
for &v in &non_empty {
let chars: Vec<char> = v.chars().collect();
lengths.push(chars.len() as f64);
let mut digits: u32 = 0;
let mut alphas: u32 = 0;
let mut specials: u32 = 0;
let mut uppers: u32 = 0;
let mut lowers: u32 = 0;
for &c in &chars {
if c.is_ascii_digit() {
digits += 1;
} else if c.is_alphabetic() {
alphas += 1;
if c.is_uppercase() {
uppers += 1;
} else if c.is_lowercase() {
lowers += 1;
}
} else if !c.is_whitespace() {
specials += 1;
}
}
digit_counts.push(digits as f64);
alpha_counts.push(alphas as f64);
special_counts.push(specials as f64);
word_counts.push(v.split_whitespace().count() as f64);
if digits > 0 {
numeric_cells += 1;
}
if alphas > 0 {
alpha_cells += 1;
}
if specials > 0 {
special_cells += 1;
}
if alphas > 0 {
if let Some(&first) = chars.iter().find(|c| c.is_alphabetic()) {
if first.is_uppercase() {
starts_upper += 1;
}
}
if lowers == 0 {
all_upper += 1;
} else if uppers == 0 {
all_lower += 1;
} else {
mixed_case += 1;
}
}
}
let len_mean = mean(&lengths);
let len_var = variance(&lengths, len_mean);
f[4] = len_mean as f32; f[5] = len_var as f32;
let len_min = lengths.iter().copied().fold(f64::INFINITY, f64::min);
let len_max = lengths.iter().copied().fold(f64::NEG_INFINITY, f64::max);
f[6] = len_min as f32; f[7] = len_max as f32;
f[8] = median(&mut lengths) as f32;
let len_std = len_var.sqrt();
f[9] = skewness(&lengths, len_mean, len_std) as f32; f[10] = kurtosis(&lengths, len_mean, len_std) as f32;
let len_sum: f64 = lengths.iter().sum();
f[11] = (len_sum + 1.0).ln() as f32;
f[12] = numeric_cells as f32 / n_ne_f as f32; f[13] = alpha_cells as f32 / n_ne_f as f32; f[14] = special_cells as f32 / n_ne_f as f32;
let digit_mean = mean(&digit_counts);
f[15] = digit_mean as f32; f[16] = std_dev(&digit_counts, digit_mean) as f32;
let alpha_mean = mean(&alpha_counts);
f[17] = alpha_mean as f32; f[18] = std_dev(&alpha_counts, alpha_mean) as f32;
let special_mean = mean(&special_counts);
f[19] = special_mean as f32; f[20] = std_dev(&special_counts, special_mean) as f32;
let word_mean = mean(&word_counts);
f[21] = word_mean as f32;
f[22] = std_dev(&word_counts, word_mean) as f32; f[23] = starts_upper as f32 / n_ne_f as f32; f[24] = all_upper as f32 / n_ne_f as f32; f[25] = all_lower as f32 / n_ne_f as f32; f[26] = mixed_case as f32 / n_ne_f as f32;
Some(f)
}
fn mean(values: &[f64]) -> f64 {
if values.is_empty() {
return 0.0;
}
values.iter().sum::<f64>() / values.len() as f64
}
fn variance(values: &[f64], mean: f64) -> f64 {
if values.is_empty() {
return 0.0;
}
values.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / values.len() as f64
}
fn std_dev(values: &[f64], mean: f64) -> f64 {
variance(values, mean).sqrt()
}
fn median(values: &mut [f64]) -> f64 {
if values.is_empty() {
return 0.0;
}
values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let n = values.len();
if n.is_multiple_of(2) {
(values[n / 2 - 1] + values[n / 2]) / 2.0
} else {
values[n / 2]
}
}
fn skewness(values: &[f64], mean: f64, std_dev: f64) -> f64 {
if values.len() < 2 || std_dev == 0.0 {
return 0.0;
}
let n = values.len() as f64;
let m3: f64 = values
.iter()
.map(|&x| ((x - mean) / std_dev).powi(3))
.sum::<f64>()
/ n;
m3
}
fn kurtosis(values: &[f64], mean: f64, std_dev: f64) -> f64 {
if values.len() < 2 || std_dev == 0.0 {
return 0.0;
}
let n = values.len() as f64;
let m4: f64 = values
.iter()
.map(|&x| ((x - mean) / std_dev).powi(4))
.sum::<f64>()
/ n;
m4 - 3.0 }
#[cfg(test)]
mod tests {
use super::*;
fn stat(features: &[f32; COLUMN_STATS_DIM], name: &str) -> f32 {
let idx = COLUMN_STATS_NAMES
.iter()
.position(|&n| n == name)
.unwrap_or_else(|| panic!("Unknown column stat: {}", name));
features[idx]
}
#[test]
fn test_column_stats_dim_matches_names() {
assert_eq!(COLUMN_STATS_NAMES.len(), COLUMN_STATS_DIM);
assert_eq!(COLUMN_STATS_DIM, 27);
let unique: std::collections::HashSet<&str> = COLUMN_STATS_NAMES.iter().copied().collect();
assert_eq!(unique.len(), COLUMN_STATS_DIM);
}
#[test]
fn test_known_column_entropy_and_frac_unique() {
let values: Vec<&str> = vec!["a", "b", "c", "a", "b", "c", "a", "b", "c", "a"];
let stats = extract_column_stats(&values).unwrap();
assert!((stat(&stats, "frac_unique") - 0.3).abs() < 0.001);
let entropy = stat(&stats, "col_entropy");
assert!(
(entropy - 1.0889).abs() < 0.01,
"Expected entropy ~1.0889, got {}",
entropy
);
let n_values = stat(&stats, "n_values");
assert!(
(n_values - 2.3979).abs() < 0.01,
"Expected n_values ~2.3979, got {}",
n_values
);
assert_eq!(stat(&stats, "frac_empty"), 0.0);
}
#[test]
fn test_all_identical_values() {
let values: Vec<&str> = vec!["same", "same", "same", "same", "same"];
let stats = extract_column_stats(&values).unwrap();
assert_eq!(stat(&stats, "col_entropy"), 0.0);
assert!((stat(&stats, "frac_unique") - 0.2).abs() < 0.001);
assert_eq!(stat(&stats, "length_variance"), 0.0);
assert_eq!(stat(&stats, "length_skewness"), 0.0);
assert_eq!(stat(&stats, "length_kurtosis"), 0.0);
assert_eq!(stat(&stats, "length_mean"), 4.0);
assert_eq!(stat(&stats, "length_min"), 4.0);
assert_eq!(stat(&stats, "length_max"), 4.0);
assert_eq!(stat(&stats, "length_median"), 4.0);
}
#[test]
fn test_single_value() {
let values: Vec<&str> = vec!["hello"];
let stats = extract_column_stats(&values).unwrap();
assert_eq!(stat(&stats, "col_entropy"), 0.0); assert_eq!(stat(&stats, "frac_unique"), 1.0); assert_eq!(stat(&stats, "length_variance"), 0.0);
assert_eq!(stat(&stats, "length_skewness"), 0.0);
assert_eq!(stat(&stats, "length_kurtosis"), 0.0);
}
#[test]
fn test_frac_numeric_cells() {
let values: Vec<&str> = vec!["abc", "123", "a1b"];
let stats = extract_column_stats(&values).unwrap();
assert!(
(stat(&stats, "frac_numeric_cells") - 2.0 / 3.0).abs() < 0.001,
"Expected frac_numeric_cells ~0.667, got {}",
stat(&stats, "frac_numeric_cells")
);
}
#[test]
fn test_length_stats_simple() {
let values: Vec<&str> = vec!["a", "ab", "abc", "abcd", "abcde"];
let stats = extract_column_stats(&values).unwrap();
assert!((stat(&stats, "length_mean") - 3.0).abs() < 0.001);
assert!((stat(&stats, "length_variance") - 2.0).abs() < 0.001);
assert_eq!(stat(&stats, "length_min"), 1.0);
assert_eq!(stat(&stats, "length_max"), 5.0);
assert_eq!(stat(&stats, "length_median"), 3.0);
assert!((stat(&stats, "length_sum") - (16.0_f32).ln()).abs() < 0.01);
}
#[test]
fn test_empty_values_returns_none() {
let values: Vec<&str> = vec![];
assert!(extract_column_stats(&values).is_none());
}
#[test]
fn test_all_empty_values() {
let values: Vec<&str> = vec!["", "", ""];
let stats = extract_column_stats(&values).unwrap();
assert_eq!(stat(&stats, "frac_empty"), 1.0);
assert_eq!(stat(&stats, "length_mean"), 0.0);
assert_eq!(stat(&stats, "frac_numeric_cells"), 0.0);
}
#[test]
fn test_mixed_empty_nonempty() {
let values: Vec<&str> = vec!["hello", "", "world", ""];
let stats = extract_column_stats(&values).unwrap();
assert!((stat(&stats, "frac_empty") - 0.5).abs() < 0.001);
assert_eq!(stat(&stats, "length_mean"), 5.0);
assert_eq!(stat(&stats, "length_variance"), 0.0);
}
#[test]
fn test_case_patterns() {
let values: Vec<&str> = vec!["Hello", "WORLD", "foo", "Bar", "BAZ", "qux"];
let stats = extract_column_stats(&values).unwrap();
assert!(
(stat(&stats, "frac_starts_upper") - 4.0 / 6.0).abs() < 0.001,
"Expected frac_starts_upper ~0.667, got {}",
stat(&stats, "frac_starts_upper")
);
assert!(
(stat(&stats, "frac_all_upper") - 2.0 / 6.0).abs() < 0.001,
"Expected frac_all_upper ~0.333, got {}",
stat(&stats, "frac_all_upper")
);
assert!(
(stat(&stats, "frac_all_lower") - 2.0 / 6.0).abs() < 0.001,
"Expected frac_all_lower ~0.333, got {}",
stat(&stats, "frac_all_lower")
);
assert!(
(stat(&stats, "frac_mixed_case") - 2.0 / 6.0).abs() < 0.001,
"Expected frac_mixed_case ~0.333, got {}",
stat(&stats, "frac_mixed_case")
);
}
#[test]
fn test_character_composition() {
let values: Vec<&str> = vec!["abc", "123", "a!b"];
let stats = extract_column_stats(&values).unwrap();
assert!((stat(&stats, "avg_digit_count") - 1.0).abs() < 0.001);
assert!(
(stat(&stats, "frac_alpha_cells") - 2.0 / 3.0).abs() < 0.001,
"Expected frac_alpha_cells ~0.667, got {}",
stat(&stats, "frac_alpha_cells")
);
assert!(
(stat(&stats, "frac_special_cells") - 1.0 / 3.0).abs() < 0.001,
"Expected frac_special_cells ~0.333, got {}",
stat(&stats, "frac_special_cells")
);
}
#[test]
fn test_word_counts() {
let values: Vec<&str> = vec!["one", "two words", "three word sentence"];
let stats = extract_column_stats(&values).unwrap();
assert!((stat(&stats, "avg_word_count") - 2.0).abs() < 0.001);
assert!(
(stat(&stats, "std_word_count") - 0.8165).abs() < 0.01,
"Expected std_word_count ~0.8165, got {}",
stat(&stats, "std_word_count")
);
}
#[test]
fn test_whitespace_values() {
let values: Vec<&str> = vec![" ", "hello"];
let stats = extract_column_stats(&values).unwrap();
assert_eq!(stat(&stats, "frac_empty"), 0.0); assert!((stat(&stats, "avg_word_count") - 0.5).abs() < 0.001);
}
#[test]
fn test_deterministic() {
let values: Vec<&str> = vec!["hello@world.com", "2024-01-15", "42", "New York"];
let s1 = extract_column_stats(&values).unwrap();
let s2 = extract_column_stats(&values).unwrap();
assert_eq!(s1, s2);
}
}