use datarust::{Matrix, StrMatrix};
use datarust_profile::quality::checks::run_checks;
use datarust_profile::{
profile_matrix, profile_str_matrix, profile_table, ColumnType, Severity, Thresholds,
};
fn names(parts: &[&str]) -> Vec<String> {
parts.iter().map(|s| s.to_string()).collect()
}
#[test]
fn numeric_profile_counts_and_quantiles() {
let m = Matrix::from_rows(vec![
vec![1.0, 10.0],
vec![2.0, 20.0],
vec![3.0, 30.0],
vec![4.0, 40.0],
vec![5.0, f64::NAN], ])
.unwrap();
let p = profile_matrix(&m, Some(&names(&["a", "b"]))).unwrap();
assert_eq!(p.n_rows, 5);
assert_eq!(p.n_columns, 2);
assert_eq!(p.columns.len(), 2);
assert_eq!(p.columns[0].name, "a");
assert_eq!(p.columns[1].name, "b");
assert_eq!(p.columns[0].missing_count, 0);
assert_eq!(p.columns[1].missing_count, 1);
let n0 = p.columns[0].numeric.as_ref().unwrap();
assert!((n0.mean - 3.0).abs() < 1e-9);
assert!((n0.five.min - 1.0).abs() < 1e-9);
assert!((n0.five.max - 5.0).abs() < 1e-9);
assert!((n0.five.median - 3.0).abs() < 1e-9);
}
#[test]
fn default_column_names_when_none() {
let m = Matrix::from_rows(vec![vec![1.0, 2.0], vec![3.0, 4.0]]).unwrap();
let p = profile_matrix(&m, None).unwrap();
assert_eq!(p.columns[0].name, "x0");
assert_eq!(p.columns[1].name, "x1");
}
#[test]
fn duplicate_numeric_rows_detected() {
let m = Matrix::from_rows(vec![
vec![1.0, 1.0],
vec![1.0, 1.0], vec![2.0, 2.0],
])
.unwrap();
let p = profile_matrix(&m, None).unwrap();
assert_eq!(p.duplicate_rows, 1);
assert!((p.duplicate_fraction - 1.0 / 3.0).abs() < 1e-9);
}
#[test]
fn str_matrix_infers_numeric_and_categorical() {
let s = StrMatrix::from_strings(vec![vec!["1", "red"], vec!["2", "blue"], vec!["3", "red"]])
.unwrap();
let p = profile_str_matrix(&s, Some(&names(&["num", "color"]))).unwrap();
assert_eq!(p.columns[0].column_type, ColumnType::Numeric);
assert_eq!(p.columns[1].column_type, ColumnType::Categorical);
let cat = p.columns[1].categorical.as_ref().unwrap();
assert_eq!(cat.unique, 2);
assert_eq!(cat.top, "red");
assert_eq!(cat.freq, 2);
}
#[test]
fn mixed_table_profiles_both_blocks() {
let numeric = Matrix::from_rows(vec![vec![10.0], vec![20.0], vec![30.0]]).unwrap();
let categorical = StrMatrix::from_strings(vec![vec!["a"], vec!["b"], vec!["a"]]).unwrap();
let p = profile_table(Some(&numeric), Some(&categorical), &names(&["v", "k"])).unwrap();
assert_eq!(p.n_columns, 2);
assert_eq!(p.columns[0].column_type, ColumnType::Numeric);
assert_eq!(p.columns[1].column_type, ColumnType::Categorical);
}
#[test]
fn empty_matrix_rejected_at_construction() {
assert!(Matrix::new(vec![]).is_err());
assert!(Matrix::new(vec![vec![]]).is_err());
}
#[test]
fn quality_flags_high_missing_and_constant() {
let m = Matrix::from_rows(vec![
vec![5.0, f64::NAN],
vec![5.0, f64::NAN],
vec![5.0, f64::NAN],
vec![5.0, 1.0],
vec![5.0, 2.0],
])
.unwrap();
let p = profile_matrix(&m, Some(&names(&["c", "m"]))).unwrap();
let issues = run_checks(&p, &Thresholds::default());
assert!(issues.iter().any(|i| {
i.column.as_deref() == Some("c") && i.kind == datarust_profile::QualityKind::ConstantColumn
}));
assert!(issues.iter().any(|i| {
i.column.as_deref() == Some("m")
&& i.kind == datarust_profile::QualityKind::HighMissing
&& i.severity == Severity::Warning
}));
}
#[test]
fn html_card_layout_replaces_table() {
let m = Matrix::from_rows(vec![vec![1.0, f64::NAN], vec![1.0, 2.0]]).unwrap();
let p = profile_matrix(&m, Some(&names(&["a", "b"]))).unwrap();
let html = datarust_profile::report::to_html(&p);
assert!(html.contains("col-grid"));
assert!(html.contains("col-card"));
assert!(html.contains("chart"));
assert!(html.contains("bar"));
assert!(!html.contains("<table"));
assert!(html.contains("a"));
assert!(html.contains("numeric"));
}
#[cfg(feature = "serde")]
#[test]
fn json_report_round_trips() {
use datarust_profile::report::{to_json, JsonReport};
let m = Matrix::from_rows(vec![vec![1.0, 10.0], vec![2.0, 20.0]]).unwrap();
let p = profile_matrix(&m, Some(&names(&["a", "b"]))).unwrap();
let json = to_json(&JsonReport::from_profile(&p)).unwrap();
assert!(json.contains("\"n_rows\""));
assert!(json.contains("\"a\""));
assert!(json.contains("\"quality\""));
}
#[test]
fn numeric_profile_includes_distribution_fields() {
let m = Matrix::from_rows(vec![vec![1.0], vec![2.0], vec![3.0], vec![4.0], vec![5.0]]).unwrap();
let p = profile_matrix(&m, Some(&names(&["x"]))).unwrap();
let n = p.columns[0].numeric.as_ref().unwrap();
assert!(n.skewness.abs() < 1e-9, "skewness {}", n.skewness);
assert_eq!(n.histogram.nbins(), 4);
assert_eq!(n.histogram.counts.iter().sum::<usize>(), 5);
assert!((n.five.min - 1.0).abs() < 1e-9);
assert!((n.five.max - 5.0).abs() < 1e-9);
}
#[test]
fn outlier_detection_via_iqr_flags_extreme_values() {
let m = Matrix::from_rows(vec![
vec![1.0],
vec![2.0],
vec![3.0],
vec![4.0],
vec![100.0],
])
.unwrap();
let p = profile_matrix(&m, Some(&names(&["x"]))).unwrap();
let n = p.columns[0].numeric.as_ref().unwrap();
assert!(
n.outlier_count >= 1,
"expected outliers, got {}",
n.outlier_count
);
let issues = run_checks(&p, &Thresholds::default());
assert!(issues.iter().any(|i| {
i.column.as_deref() == Some("x") && i.kind == datarust_profile::QualityKind::Outliers
}));
}
#[test]
fn categorical_imbalance_detected_when_top_dominates() {
let mut rows: Vec<Vec<String>> = (0..19).map(|_| vec!["a".to_string()]).collect();
rows.push(vec!["b".to_string()]);
let s = StrMatrix::from_strings(rows).unwrap();
let p = profile_str_matrix(&s, Some(&names(&["k"]))).unwrap();
let c = p.columns[0].categorical.as_ref().unwrap();
assert!((c.imbalance_ratio - 0.95).abs() < 1e-9);
let issues = run_checks(&p, &Thresholds::default());
assert!(issues.iter().any(|i| {
i.column.as_deref() == Some("k") && i.kind == datarust_profile::QualityKind::Imbalance
}));
}
#[test]
fn flat_path_matches_independent_stats() {
let m = Matrix::from_rows(vec![
vec![1.0, 10.0, 100.0],
vec![2.0, 20.0, f64::NAN],
vec![3.0, 30.0, 300.0],
vec![4.0, 40.0, 400.0],
vec![5.0, 50.0, 500.0],
])
.unwrap();
let flat = profile_matrix(&m, Some(&names(&["a", "b", "c"]))).unwrap();
for j in 0..m.ncols() {
let col = m.col(j);
let present: Vec<f64> = col.iter().copied().filter(|v| v.is_finite()).collect();
let n_flat = flat.columns[j].numeric.as_ref().unwrap();
let mean = datarust::stats::mean(&present);
let std = datarust::stats::std(&present, 1);
assert!((n_flat.mean - mean).abs() < 1e-9, "col {j} mean mismatch");
assert!((n_flat.std - std).abs() < 1e-9, "col {j} std mismatch");
assert!(n_flat.skewness.is_finite(), "col {j} skewness not finite");
assert!(n_flat.kurtosis.is_finite(), "col {j} kurtosis not finite");
assert!(
n_flat.outlier_count <= present.len(),
"col {j} outlier overflow"
);
}
}
#[test]
fn categorical_stats_carry_top_values_list() {
let s = StrMatrix::from_strings(vec![vec!["a"], vec!["a"], vec!["b"], vec!["c"], vec!["d"]])
.unwrap();
let p = profile_str_matrix(&s, Some(&names(&["k"]))).unwrap();
let c = p.columns[0].categorical.as_ref().unwrap();
assert_eq!(c.unique, 4);
assert_eq!(c.top_values.first().unwrap().0, "a");
assert_eq!(c.top_values.first().unwrap().1, 2);
}