use millwright::prelude::*;
#[track_caller]
fn close(actual: f64, golden: f64) {
assert!(
(actual - golden).abs() < 1e-9,
"golden drift: expected {golden}, got {actual} (Δ {:.3e})",
(actual - golden).abs()
);
}
#[track_caller]
fn close_vec(actual: &[f64], golden: &[f64]) {
assert_eq!(
actual.len(),
golden.len(),
"length drift: {actual:?} vs {golden:?}"
);
for (a, g) in actual.iter().zip(golden) {
close(*a, *g);
}
}
#[test]
fn golden_standard_scaler() {
let f = Frame::from_rows(vec![vec![1.0], vec![2.0], vec![3.0]], vec!["x".into()]).unwrap();
let out = StandardScaler::new().fit_transform(&f).unwrap();
close_vec(
&out.column(0),
&[-1.224744871391589, 0.0, 1.224744871391589],
);
}
#[test]
fn golden_minmax_scaler() {
let f = Frame::from_rows(
vec![vec![10.0, 100.0], vec![20.0, 300.0], vec![30.0, 500.0]],
vec!["a".into(), "b".into()],
)
.unwrap();
let out = MinMaxScaler::new().fit_transform(&f).unwrap();
close_vec(&out.column(0), &[0.0, 0.5, 1.0]);
close_vec(&out.column(1), &[0.0, 0.5, 1.0]);
}
#[test]
fn golden_simple_imputer() {
let nan = f64::NAN;
let f = Frame::from_rows(
vec![
vec![1.0, 1.0],
vec![nan, 2.0],
vec![3.0, nan],
vec![5.0, 4.0],
],
vec!["a".into(), "b".into()],
)
.unwrap();
let mean = SimpleImputer::mean().fit_transform(&f).unwrap();
close(mean.get(1, 0), 3.0);
let median = SimpleImputer::median().fit_transform(&f).unwrap();
close(median.get(2, 1), 2.0);
}
#[test]
fn golden_one_hot_encoder() {
let f = Frame::from_rows(
vec![
vec![0.0, 5.0],
vec![2.0, 6.0],
vec![1.0, 7.0],
vec![0.0, 8.0],
],
vec!["cat".into(), "num".into()],
)
.unwrap();
let out = OneHotEncoder::columns(["cat"]).fit_transform(&f).unwrap();
assert_eq!(
out.columns(),
&[
"cat=0".to_string(),
"cat=1".into(),
"cat=2".into(),
"num".into()
],
);
close_vec(out.row(0), &[1.0, 0.0, 0.0, 5.0]); close_vec(out.row(1), &[0.0, 0.0, 1.0, 6.0]); close_vec(out.row(2), &[0.0, 1.0, 0.0, 7.0]); }
#[cfg(feature = "smartcore-backend")]
#[test]
fn golden_linear_regression_plane() {
let rows = vec![
vec![0.0, 0.0],
vec![1.0, 0.0],
vec![0.0, 1.0],
vec![1.0, 1.0],
vec![2.0, 3.0],
];
let y: Vec<f64> = rows.iter().map(|r| 3.0 + 2.0 * r[0] - r[1]).collect();
let train = Dataset::new(
Frame::from_rows(rows, vec!["x1".into(), "x2".into()]).unwrap(),
y,
)
.unwrap();
let mut lr = LinearRegression::new();
lr.fit(&train).unwrap();
let probe = Frame::from_rows(
vec![vec![4.0, 2.0], vec![-1.0, 5.0]],
vec!["x1".into(), "x2".into()],
)
.unwrap();
close_vec(&lr.predict(&probe).unwrap(), &[9.0, -4.0]);
}
#[test]
fn golden_regression_report() {
let t = [1.5, 2.5, 3.5, 4.5];
let p = [1.0, 2.0, 4.0, 5.0];
let r = Report::new(&t, &p);
assert_eq!(r.task(), Task::Regression);
close(r.get("mae").unwrap(), 0.5);
close(r.get("mse").unwrap(), 0.25);
close(r.get("rmse").unwrap(), 0.5);
close(r.get("r2").unwrap(), 0.8);
}
#[test]
fn golden_classification_report() {
let t = [0.0, 0.0, 1.0, 1.0];
let p = [0.0, 1.0, 0.0, 1.0];
let r = Report::new(&t, &p);
assert_eq!(r.task(), Task::Classification);
close(r.get("accuracy").unwrap(), 0.5);
close(r.get("precision").unwrap(), 0.5);
close(r.get("recall").unwrap(), 0.5);
close(r.get("f1").unwrap(), 0.5);
}
#[cfg(feature = "smartcore-backend")]
fn two_clusters() -> (Dataset, Frame) {
let cols = vec!["a".to_string(), "b".to_string()];
let features = Frame::from_rows(
vec![
vec![0.0, 0.1],
vec![0.4, 0.2],
vec![0.2, 0.5],
vec![9.0, 9.1],
vec![9.4, 8.7],
vec![8.8, 9.5],
],
cols.clone(),
)
.unwrap();
let train = Dataset::new(features, vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0]).unwrap();
let probe = Frame::from_rows(vec![vec![0.3, 0.2], vec![9.1, 9.0]], cols).unwrap();
(train, probe)
}
#[cfg(feature = "smartcore-backend")]
#[test]
fn golden_random_forest_labels() {
let (train, probe) = two_clusters();
let mut rf = RandomForest::new().n_trees(50);
rf.fit(&train).unwrap();
close_vec(&rf.predict(&probe).unwrap(), &[0.0, 1.0]);
}
#[cfg(feature = "smartcore-backend")]
#[test]
fn golden_pipeline_labels() {
let (train, probe) = two_clusters();
let mut pipe = Pipeline::new()
.step("impute", SimpleImputer::median())
.step("scale", StandardScaler::new())
.estimator("rf", RandomForest::new().n_trees(50));
pipe.fit(&train).unwrap();
close_vec(&pipe.predict(&probe).unwrap(), &[0.0, 1.0]);
}
#[cfg(all(feature = "ensemble", feature = "smartcore-backend"))]
#[test]
fn golden_soft_vote_labels() {
let (train, probe) = two_clusters();
let mut vote = Voting::soft()
.add("rf_shallow", RandomForest::new().max_depth(2))
.add("rf_deep", RandomForest::new().max_depth(8));
vote.fit(&train).unwrap();
close_vec(&vote.predict(&probe).unwrap(), &[0.0, 1.0]);
}
#[cfg(feature = "eda")]
#[test]
fn golden_table_and_profile_from_csv() {
let path = std::env::temp_dir().join("millwright_golden_eda.csv");
std::fs::write(&path, "age,city,label\n20,ny,0\n30,sf,1\n,ny,0\n40,la,1\n").unwrap();
let table = Table::from_csv(&path).unwrap();
assert_eq!(table.shape(), (4, 3));
assert_eq!(table.null_count("age").unwrap(), 1);
let profile = Profile::of_with_target(&table, "label").unwrap();
let o = profile.overview();
assert_eq!((o.nrows, o.ncols), (4, 3));
assert_eq!(o.n_numeric, 2); assert_eq!(o.n_categorical, 1); assert_eq!(o.missing_cells, 1);
let ds = table.into_dataset("label").unwrap();
assert_eq!(ds.features().shape(), (4, 2));
assert_eq!(ds.target(), &[0.0, 1.0, 0.0, 1.0]);
assert!(ds.features().get(2, 0).is_nan());
assert_eq!(
profile.suggest_pipeline().step_names(),
vec!["impute", "encode", "scale"],
);
let _ = std::fs::remove_file(&path);
}
#[cfg(feature = "onnx")]
#[test]
fn golden_onnx_linear_regression_roundtrip() {
use millwright::onnx::{ExportOnnx, InferenceModel};
let rows = vec![vec![0.0], vec![1.0], vec![2.0], vec![3.0]];
let y: Vec<f64> = rows.iter().map(|r| 2.0 * r[0] + 1.0).collect();
let train = Dataset::new(Frame::from_rows(rows, vec!["x".into()]).unwrap(), y).unwrap();
let mut lr = LinearRegression::new();
lr.fit(&train).unwrap();
let probe = Frame::from_rows(vec![vec![4.0], vec![10.0]], vec!["x".into()]).unwrap();
let native = lr.predict(&probe).unwrap();
close_vec(&native, &[9.0, 21.0]);
let path = std::env::temp_dir().join("millwright_golden_lr.onnx");
lr.export_onnx(&path).unwrap();
let model = InferenceModel::load(&path).unwrap();
let onnx_out = model.predict(&probe).unwrap();
close_vec(&onnx_out, &native);
let _ = std::fs::remove_file(&path);
}