Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

datarust

Scikit-Learn Preprocessing in Rust — a modular, dependency-free machine-learning preprocessing and modeling library built on a lightweight Matrix type.

crates.io docs.rs CI License: MIT

#![allow(unused)]
fn main() {
use datarust::scaler::StandardScaler;
use datarust::traits::Transformer;
use datarust::Matrix;

let x = Matrix::new(vec![
    vec![1.0, 10.0],
    vec![2.0, 20.0],
    vec![3.0, 30.0],
    vec![4.0, 40.0],
])?;

// Standardize: (x - mean) / std (population, ddof=0)
let mut scaler = StandardScaler::new();
let standardized = scaler.fit_transform(&x)?;
}

Default build has zero external dependencies. All linear algebra (eigenvalue decomposition, covariance, Cholesky factorization, coordinate descent) is implemented in pure Rust — no system BLAS/LAPACK, no Python runtime, no GIL.


What’s included

CategoryComponents
ScalersStandardScaler, MinMaxScaler, RobustScaler, MaxAbsScaler, Normalizer (L1/L2/Max), Binarizer
DiscretizersKBinsDiscretizer (Uniform / Quantile / KMeans), Binarizer
Distribution TransformersQuantileTransformer (Uniform / Normal output), PowerTransformer (Yeo-Johnson / Box-Cox)
EncodersLabelEncoder, OneHotEncoder (+ CSR sparse output), OrdinalEncoder, TargetEncoder, FrequencyEncoder
ImputersSimpleImputer (mean / median / most_frequent / constant), KnnImputer (uniform / distance)
PolynomialPolynomialFeatures (degree, interaction_only, include_bias)
SelectionVarianceThreshold, SelectKBest (ANOVA F / Chi2 / Mutual Information)
DecompositionPCA (whiten, inverse_transform, randomized SVD), TruncatedSVD
Linear ModelsLinearRegression (Cholesky & SVD), Ridge (L2), Lasso (L1, coordinate descent, sparse)
ClassificationLogisticRegression (binary IRLS + multiclass softmax, Cholesky & SVD)
MetricsRegression: MSE/RMSE, MAE, R², max_error, explained_variance. Classification: accuracy, explicit precision/recall/F1 averaging, labeled confusion matrices, log_loss
Model Selectiontrain_test_split, KFold, StratifiedKFold, cross_val_score
PipelineSequential Pipeline (serde-serializable), ColumnTransformer (numeric + categorical)
Feature NamesFeatureNames trait on all transformers for output column names
SerializationJSON save/load via optional serde feature
ParallelismRayon-backed column operations via optional rayon feature
SparseCSR SparseMatrix type for memory-efficient one-hot output

Why datarust?

Zero dependencies by default

cargo add datarust pulls in nothing. No BLAS, no LAPACK, no numpy, no Python runtime. The default build has a completely empty dependency tree — every algorithm (Jacobi eigendecomposition, Cholesky factorization, one-sided Jacobi SVD, coordinate descent, IRLS) is pure Rust. Opt in to serde, rayon, or a tuned pure-Rust GEMM via feature flags when you need them.

Rust-native type safety

sklearn’s single duck-typed fit/transform API becomes four separate traits enforced by the type system:

You cannot accidentally pass categorical strings to a numeric scaler — the compiler catches it.

Panic-free public API

Every fallible public method returns Result<T, DatarustError>. No hidden panics on bad input, no unwrap() in the API surface.

JSON serialization (not pickle)

Fitted models serialize to plain JSON via the serde feature — human-readable, language-agnostic, and diff-friendly. No binary pickle blobs.

Measured speedups vs scikit-learn

On heterogeneous ColumnTransformer composition, categorical encoding, and numeric scaling, datarust is 1.5–620× faster than scikit-learn. See the Performance page for the full benchmark tables and methodology.


A taste: end-to-end pipeline

#![allow(unused)]
fn main() {
use datarust::compose::{ColumnTransformer, Table};
use datarust::encoder::OneHotEncoder;
use datarust::categorical_kind::CategoricalTransformerKind;
use datarust::scaler::StandardScaler;
use datarust::transformer_kind::TransformerKind;
use datarust::Matrix;

// Mixed numeric + categorical table
let numeric = Matrix::new(vec![vec![1.0, 2.0], vec![3.0, 4.0]])?;
let table = Table::new(numeric, /* categorical StrMatrix */ /* ... */ /* ) */;

// Build a column transformer: scale numeric, one-hot encode categorical
let mut ct = ColumnTransformer::new()
    .add_numeric("num", vec![0, 1], TransformerKind::StandardScaler(StandardScaler::new()));
    // .add_categorical("cat", vec![2, 3], CategoricalTransformerKind::OneHotEncoder(OneHotEncoder::new()));

let out = ct.fit_transform_to_table(&table)?;
// out.numeric and out.categorical hold the transformed columns
}

Next steps

Quick Start

Get from cargo add to a fitted, transformed pipeline in five minutes.

1. Add the dependency

[dependencies]
datarust = "0.6"

Or with the most useful feature flags:

[dependencies]
datarust = { version = "0.6", features = ["serde", "rayon"] }

See Installation for what each feature does.

2. Create a matrix

All numeric data flows through Matrix — a row-major dense matrix backed by a single contiguous Vec<f64>:

#![allow(unused)]
fn main() {
use datarust::Matrix;

let x = Matrix::new(vec![
    vec![1.0, 10.0],
    vec![2.0, 20.0],
    vec![3.0, 30.0],
    vec![4.0, 40.0],
])?;
}

3. Fit and transform

Every numeric transformer implements the Transformer trait: call fit to learn parameters, then transform to apply them.

#![allow(unused)]
fn main() {
use datarust::scaler::StandardScaler;
use datarust::traits::Transformer;

let mut scaler = StandardScaler::new();
let standardized = scaler.fit_transform(&x)?;  // fit + transform in one call

// mean=0, variance=1 per column
}

4. Train a model

Regression and classification models implement Predictor: call fit(X, y) then predict(X).

#![allow(unused)]
fn main() {
use datarust::linear_model::LinearRegression;
use datarust::traits::Predictor;

let y = vec![3.0, 5.0, 7.0, 9.0]; // y = 2x + 1 (for feature 0)
let features = x.select_columns(&[0])?; // pick one feature
let mut model = LinearRegression::new();
model.fit(&features, &y)?;
let pred = model.predict(&features)?;
}

5. Evaluate

#![allow(unused)]
fn main() {
use datarust::metrics::regression::r2_score;

let r2 = r2_score(&y, &pred)?;
println!("R² = {r2:.4}"); // ≈ 1.0 for a clean linear signal
}

6. Split and cross-validate

#![allow(unused)]
fn main() {
use datarust::model_selection::{train_test_split, cross_val_score, KFold};
use datarust::metrics::regression::r2_score;

let (x_tr, x_te, y_tr, y_te) = train_test_split(&x, &y)?;

let cv = KFold::new().with_n_splits(3);
let scores = cross_val_score(&LinearRegression::new(), &x, &y, &cv, r2_score)?;
// scores.len() == 3, one R² per fold
}

Where to go next

Installation

Cargo

[dependencies]
datarust = "0.6"

The default build has zero external dependenciescargo add datarust pulls in nothing but the standard library.

Optional features

All features are opt-in and independent. Enable the ones you need:

[dependencies]
datarust = { version = "0.6", features = ["serde", "rayon", "matrixmultiply"] }

serde

Enables JSON serialization of fitted transformers via datarust::serialize::{save_json, load_json, to_json, from_json}. Models serialize to human-readable JSON (not pickle).

#![allow(unused)]
fn main() {
use datarust::serialize::{save_json, load_json};
use datarust::scaler::StandardScaler;
use datarust::traits::Transformer;

let mut scaler = StandardScaler::new();
scaler.fit(&x)?;

// Save
save_json(&scaler, "model.json")?;

// Load into a fresh instance
let restored: StandardScaler = load_json("model.json")?;
let out = restored.transform(&x)?;
}

rayon

Enables parallel column/row operations for large datasets. Speeds up scaler transforms, encoders, and the KNN imputer on inputs above ~4 000 rows.

matrixmultiply

Enables a tuned pure-Rust GEMM (via the matrixmultiply crate, no system BLAS) for Matrix::matmul and centered-covariance computation. Significantly speeds up PCA, TruncatedSVD, and the linear models on large dense inputs. The default build remains zero-external-dependency; opt in with this feature.

datasets

Embeds four classic toy datasets (Iris, Breast Cancer, Wine, Diabetes) as const arrays for examples, tests, and onboarding. No file I/O, no network access — the data is compiled into the binary. See the Datasets guide.

Rust version

datarust targets Rust 1.70+ (the rust-version field in Cargo.toml). It is tested against stable and the MSRV in CI.

Platform support

CI runs on Ubuntu and macOS (both x86-64 and arm64). WASM should work for the default build (no system calls, no threading without rayon), though it is not formally tested.

Core Concepts

Understanding seven concepts unlocks the entire crate.

1. Matrix — the data container

All numeric data flows through Matrix: a row-major dense matrix backed by a single contiguous Vec<f64> buffer. The flat layout keeps every numeric hot loop cache-friendly and auto-vectorizable.

#![allow(unused)]
fn main() {
use datarust::Matrix;

let m = Matrix::new(vec![
    vec![1.0, 2.0, 3.0],
    vec![4.0, 5.0, 6.0],
])?;
assert_eq!(m.nrows(), 2);
assert_eq!(m.ncols(), 3);
assert_eq!(m.get(0, 1), 2.0);
}

Companion types:

  • StrMatrix — categorical string input for encoders.
  • SparseMatrix — CSR format for memory-efficient one-hot output.

2. The Transformer trait

All numeric transformers implement Transformer:

#![allow(unused)]
fn main() {
pub trait Transformer {
    fn name(&self) -> &'static str;
    fn fit(&mut self, x: &Matrix) -> Result<()>;
    fn transform(&self, x: &Matrix) -> Result<Matrix>;
    fn fit_transform(&mut self, x: &Matrix) -> Result<Matrix>; // default: fit + transform
    fn inverse_transform(&self, _x: &Matrix) -> Result<Matrix>; // optional
    fn is_fitted(&self) -> bool;
}
}
  • fit learns parameters from training data (takes &mut self).
  • transform applies the learned transformation (takes &self).
  • fit_transform is a convenience that calls both.
  • inverse_transform reverses the transformation where supported.

3. Supervised estimator traits

Regression and classification estimators implement Predictor:

#![allow(unused)]
fn main() {
pub trait Predictor: Estimator {
    fn fit(&mut self, x: &Matrix, y: &[f64]) -> Result<()>;
    fn predict(&self, x: &Matrix) -> Result<Vec<f64>>;
    fn fit_predict(&mut self, x: &Matrix, y: &[f64]) -> Result<Vec<f64>>; // default
    fn is_fitted(&self) -> bool;
}
}
  • LinearRegression, Ridge, Lassopredict returns continuous predictions.
  • LogisticRegression — implements Classifier; predict returns the original hard labels used during fitting. predict_proba returns one column per class in classes() order.

4. The Clusterer trait (unsupervised)

Clustering estimators implement Clusterer — the unsupervised counterpart to Predictor. fit takes only X (no target y), and predict returns cluster indices as Vec<usize> rather than regression targets or class labels:

#![allow(unused)]
fn main() {
pub trait Clusterer: Estimator {
    fn fit(&mut self, x: &Matrix) -> Result<()>;
    fn predict(&self, x: &Matrix) -> Result<Vec<usize>>;
    fn fit_predict(&mut self, x: &Matrix) -> Result<Vec<usize>>; // default
    fn fit_transform(&mut self, x: &Matrix) -> Result<Matrix>;   // default: one-hot labels
    fn n_clusters(&self) -> usize;
    fn is_fitted(&self) -> bool;
}
}
  • KMeansfit_predict returns one cluster index per row; cluster_centers() exposes the learned centroids and inertia() the minimized within-cluster sum of squares.

5. The Params trait (hyperparameter introspection)

Estimators whose hyperparameters should be searchable implement the opt-in Params trait:

#![allow(unused)]
fn main() {
pub trait Params {
    fn get_params(&self) -> Vec<(&'static str, ParamValue)>;
    fn set_params(&mut self, name: &str, value: ParamValue) -> Result<()>;
}
}

KMeans (n_clusters, max_iter, tol, n_init) and LogisticRegression (max_iter, tol, fit_intercept) implement it. Not every estimator needs Params — only those whose hyperparameters should be tuned by an automated search (the foundation for future GridSearchCV).

6. The categorical traits

Categorical data is kept separate at the type level:

This separation means the compiler prevents you from accidentally passing strings to a numeric scaler.

7. Error handling

Every fallible public method returns Result<T, DatarustError>. No hidden panics on bad input. The error variants are ML-domain-specific:

#![allow(unused)]
fn main() {
pub enum DatarustError {
    NotFitted(String),
    InvalidInput(String),
    ShapeMismatch { expected: String, actual: String },
    EmptyInput(String),
    InvalidConfig(String),
    Singular(String),       // e.g. rank-deficient matrix in Cholesky
    UnknownCategory(String),
    // ...
}
}

Typical usage:

#![allow(unused)]
fn main() {
match scaler.transform(&x) {
    Ok(out) => { /* use out */ }
    Err(DatarustError::NotFitted(name)) => eprintln!("call fit() first on {name}"),
    Err(DatarustError::ShapeMismatch { expected, actual }) => eprintln!("{expected} vs {actual}"),
    Err(e) => eprintln!("error: {e}"),
}
}

Scalers

Feature scaling and distribution-shaping transformers. All implement Transformer (Matrix → Matrix) and live in datarust::scaler.

Scaler inputs and floating-point configuration values must be finite. NaN and infinity return DatarustError before fitted state is changed.

StandardScaler

Standardize by removing the mean and scaling to unit variance. Population standard deviation (ddof = 0), matching sklearn.

#![allow(unused)]
fn main() {
use datarust::scaler::StandardScaler;
use datarust::traits::Transformer;

let mut s = StandardScaler::new()
    .with_mean(true)   // default: center
    .with_std(true);   // default: scale to unit variance
let out = s.fit_transform(&x)?;
}

MinMaxScaler

Scale each feature to a given range (default [0, 1]).

#![allow(unused)]
fn main() {
use datarust::scaler::MinMaxScaler;

let mut s = MinMaxScaler::new().feature_range(-1.0, 1.0);
let out = s.fit_transform(&x)?;
}

RobustScaler

Outlier-robust scaling using the median and interquartile range.

#![allow(unused)]
fn main() {
use datarust::scaler::RobustScaler;

let mut s = RobustScaler::new()
    .with_centering(true)
    .with_scaling(true);
let out = s.fit_transform(&x)?;
}

MaxAbsScaler

Scale by dividing by the maximum absolute value per feature. Preserves sparsity.

Normalizer

Row-wise normalization (not column-wise). Each sample is scaled to unit norm.

#![allow(unused)]
fn main() {
use datarust::scaler::{Normalizer, Norm};

let mut n = Normalizer::new().norm(Norm::L2); // or Norm::L1, Norm::Max
}

Binarizer

Threshold features to 0/1.

#![allow(unused)]
fn main() {
use datarust::scaler::Binarizer;

let mut b = Binarizer::new().threshold(0.5);
}

KBinsDiscretizer

Continuous-to-discrete bin discretization.

#![allow(unused)]
fn main() {
use datarust::scaler::{KBinsDiscretizer, BinStrategy, KBinsEncode};

let mut k = KBinsDiscretizer::new()
    .strategy(BinStrategy::Quantile) // or Uniform, KMeans
    .encode(KBinsEncode::Ordinal)    // or OneHotDense
    .n_bins(5);
}

QuantileTransformer

Transform features to follow a uniform or normal distribution. Robust to outliers.

#![allow(unused)]
fn main() {
use datarust::scaler::{QuantileTransformer, OutputDistribution};

let mut q = QuantileTransformer::new()
    .output(OutputDistribution::Normal); // or Uniform
}

PowerTransformer

Gaussianize features via Yeo-Johnson or Box-Cox, with automatic lambda estimation.

#![allow(unused)]
fn main() {
use datarust::scaler::{PowerTransformer, PowerMethod};

let mut p = PowerTransformer::new().method(PowerMethod::YeoJohnson); // or BoxCox
}

When to use which?

ScenarioRecommended scaler
Normal-ish data, no outliersStandardScaler
Bounded range needed (e.g. neural nets)MinMaxScaler
Data with outliersRobustScaler
Sparse dataMaxAbsScaler (preserves zeros)
Non-Gaussian → Gaussian neededPowerTransformer or QuantileTransformer
Sample-level normalizationNormalizer

Encoders

Categorical encoding transformers. Live in datarust::encoder.

LabelEncoder

1-D string ↔ int mapping for target labels. Implements LabelTransformer.

#![allow(unused)]
fn main() {
use datarust::encoder::LabelEncoder;
use datarust::traits::LabelTransformer;

let labels = vec!["cat".to_string(), "dog".to_string(), "cat".to_string()];
let mut le = LabelEncoder::new();
let codes = le.fit_transform(&labels)?;   // [0, 1, 0]
let back = le.inverse_transform(&codes)?; // ["cat", "dog", "cat"]
}

OneHotEncoder

Encode categorical features as a one-hot numeric matrix. Supports dense and CSR sparse output.

#![allow(unused)]
fn main() {
use datarust::encoder::{OneHotEncoder, HandleUnknown};

let mut ohe = OneHotEncoder::new()
    .handle_unknown(HandleUnknown::Ignore) // or Error
    .sparse_output(true);                  // CSR SparseMatrix output
let out = ohe.fit_transform(&str_matrix)?;
}

The CSR SparseMatrix output stores only the 1.0 positions, saving significant memory for high-cardinality columns.

OrdinalEncoder

Encode categorical features as integer codes with optional user-defined ordering.

#![allow(unused)]
fn main() {
use datarust::encoder::{OrdinalEncoder, OrdinalCategories};

// Auto-detect categories from data:
let mut oe = OrdinalEncoder::new();

// Or specify explicit ordering (e.g. "low" < "medium" < "high"):
let mut oe = OrdinalEncoder::new()
    .categories(OrdinalCategories::Manual(vec![vec!["low".into(), "medium".into(), "high".into()]]));
}

TargetEncoder

Supervised encoder: replaces categories with the smoothed mean of the target. Useful for high-cardinality features. Implements TargetTransformer — requires y during fit.

#![allow(unused)]
fn main() {
use datarust::encoder::{TargetEncoder, UnknownTarget};

let mut te = TargetEncoder::new()
    .smoothing(1.0)
    .handle_unknown(UnknownTarget::GlobalMean); // or NaN, Error
let out = te.fit_transform(&str_matrix, &y)?;
}

FrequencyEncoder

Replace categories with their count or proportion in the training data.

#![allow(unused)]
fn main() {
use datarust::encoder::{FrequencyEncoder, UnknownFrequency};

let mut fe = FrequencyEncoder::new()
    .proportion(true)                          // proportion vs raw count
    .handle_unknown(UnknownFrequency::Zero);   // or Error
}

Choosing an encoder

SituationEncoder
Target labels (1-D)LabelEncoder
Low-cardinality featuresOneHotEncoder
High-cardinality featuresTargetEncoder
Ordinal relationshipsOrdinalEncoder (with manual ordering)
Count-based signalFrequencyEncoder

Imputers

Missing-value completion. Live in datarust::imputer. Both implement Transformer.

Missing values are represented as f64::NAN in the Matrix.

SimpleImputer

Fill missing values with a per-column statistic or a constant.

#![allow(unused)]
fn main() {
use datarust::imputer::{SimpleImputer, ImputeStrategy};

// Mean (default), median, most_frequent, or constant
let mut imp = SimpleImputer::new()
    .strategy(ImputeStrategy::Median); // or Mean, MostFrequent, Constant(0.0)
let out = imp.fit_transform(&x_with_nans)?;
}

Strategies:

  • Mean — column mean (default)
  • Median — column median, robust to outliers
  • MostFrequent — most common value
  • Constant(v) — a fixed fill value

KnnImputer

Impute using k-Nearest Neighbors. Distance is computed over co-observed features only (features where neither sample is missing).

#![allow(unused)]
fn main() {
use datarust::imputer::{KnnImputer, KnnWeights};

let mut imp = KnnImputer::new()
    .n_neighbors(5)
    .weights(KnnWeights::Distance); // or Uniform
let out = imp.fit_transform(&x_with_nans)?;
}

KnnImputer is more accurate than SimpleImputer when features are correlated, but slower — it computes pairwise distances over all samples.

When to use which?

ScenarioImputer
Quick baselineSimpleImputer (mean or median)
Categorical dataSimpleImputer (most_frequent or constant)
Correlated features, accuracy mattersKnnImputer

Decomposition

Dimensionality reduction. Live in datarust::decomposition. Both implement Transformer.

PCA

Principal Component Analysis via Jacobi eigenvalue decomposition of the covariance matrix.

#![allow(unused)]
fn main() {
use datarust::decomposition::{PCA, PCAComponents};

// Select components by count, variance ratio, or keep all:
let mut pca = PCA::new(PCAComponents::Variance(0.95)) // keep 95% of variance
    .whiten(true);                                    // optional: whiten components
let projected = pca.fit_transform(&x)?;

// After fit:
pca.components();                // [[f64; n_features]; n_components]
pca.explained_variance_ratio();  // how much variance each component captures
pca.noise_variance();            // estimated noise variance
let reconstructed = pca.inverse_transform(&projected)?; // approximate recovery
}

Component selection

  • PCAComponents::Count(k) — request k components, capped at min(n_samples, n_features).
  • PCAComponents::Variance(0.95) — keep enough to explain 95% of variance.
  • PCAComponents::All — keep all components.

Solver

#![allow(unused)]
fn main() {
use datarust::decomposition::PCASolver;

let mut pca = PCA::new(PCAComponents::Count(10))
    .solver(PCASolver::Randomized); // Halko–Martinsson–Tropp randomized SVD
}
  • Auto (default) — exact eigensolver.
  • Full — full Jacobi eigendecomposition.
  • Randomized — randomized SVD, much faster for tall-and-wide, low-rank data.

Performance tip: enabling the matrixmultiply feature speeds up PCA significantly on large dense inputs by dispatching covariance and transform matmuls to a tuned pure-Rust GEMM.

TruncatedSVD

Dimensionality reduction via truncated SVD. Does not center the data, making it suitable for sparse or TF-IDF inputs.

#![allow(unused)]
fn main() {
use datarust::decomposition::{TruncatedSVD, SVDComponents};

// By count, variance threshold, or all:
let mut svd = TruncatedSVD::new(5).unwrap();                 // 5 components
let mut svd = TruncatedSVD::new(0.95).unwrap();              // 95% variance
let mut svd = TruncatedSVD::new(SVDComponents::All).unwrap();
let out = svd.fit_transform(&x)?;
}

PCA vs TruncatedSVD

PCATruncatedSVD
Centers dataYesNo
Sparse inputNoYes
Use caseDense, find directions of max varianceSparse/TF-IDF, latent semantic analysis

Linear Models

Regression and classification estimators. Live in datarust::linear_model. All implement Predictor; regression models also implement Regressor and classifiers implement Classifier.

LinearRegression

Ordinary least squares. Estimates y ≈ Xβ + b by minimising ‖Xβ − y‖².

#![allow(unused)]
fn main() {
use datarust::linear_model::{LinearRegression, LinearSolver};
use datarust::traits::Predictor;

let mut model = LinearRegression::new()
    .with_fit_intercept(true)             // default
    .with_solver(LinearSolver::Cholesky); // default; or LinearSolver::Svd

model.fit(&x, &y)?;
let pred = model.predict(&new_x)?;

model.coef();          // coefficients β
model.intercept();     // intercept b
model.n_features_in(); // feature count
let r2 = model.score(&x, &y)?;  // R²
}

Solvers:

  • Cholesky (default) — solves XᵀX β = Xᵀy via pure-Rust Cholesky. Fast; requires full column rank.
  • Svd — eigendecomposition pseudo-inverse. Stable for rank-deficient / collinear inputs.

Ridge

L2-regularized regression. Minimises ‖Xβ − y‖² + α‖β‖². The penalty guarantees the system matrix is positive-definite — Ridge succeeds on collinear inputs where LinearRegression fails.

#![allow(unused)]
fn main() {
use datarust::linear_model::{Ridge, RidgeSolver};

let mut model = Ridge::new()
    .with_alpha(1.0)                      // regularization strength (default 1.0)
    .with_solver(RidgeSolver::Cholesky);  // or Svd
model.fit(&x, &y)?;
}

Larger alpha → more shrinkage (coefficients shrink toward zero). alpha must be finite and non-negative.

Lasso

L1-regularized regression. Minimises (1/(2n))‖Xβ − y‖² + α‖β‖₁. Solved by coordinate descent with soft-thresholding.

The L1 penalty drives irrelevant coefficients to exactly zero, producing a sparse model that performs implicit feature selection.

#![allow(unused)]
fn main() {
use datarust::linear_model::Lasso;

let mut model = Lasso::new()
    .with_alpha(0.1)          // larger alpha → more sparsity
    .with_max_iter(1000)      // default 1000
    .with_tol(1e-4);          // convergence tolerance
model.fit(&x, &y)?;

model.coef();   // some entries may be exactly 0.0 (sparsity)
model.n_iter(); // iterations actually run
}

For iterative solvers, max_iter must be positive and tol must be finite and non-negative. Invalid configurations return InvalidConfig before the optimization starts.

LogisticRegression

Binary classification via IRLS and multiclass classification via multinomial softmax regression. Labels can be any non-negative integers; the model compacts them internally and maps predictions back to their original values.

#![allow(unused)]
fn main() {
use datarust::linear_model::{LogisticRegression, LogisticSolver};
use datarust::traits::Predictor;

let mut model = LogisticRegression::new()
    .with_solver(LogisticSolver::Cholesky) // or Svd
    .with_max_iter(100)                    // default 100
    .with_tol(1e-4);

model.fit(&x, &y)?;               // for example, labels can be 2.0 / 5.0 / 9.0
let classes = model.predict(&x)?; // returns the original labels
let probabilities = model.predict_proba(&x)?;
// Probability column i always corresponds to model.classes()[i].
let class_five_probability = model.predict_proba_for_class(&x, 5.0)?;
let acc = model.score(&x, &y)?; // mean accuracy
}

LogisticRegression applies the same iteration and tolerance validation as Lasso. Invalid values supplied through Params::set_params are rejected without changing the estimator.

Choosing a model

GoalModel
Simple baseline regressionLinearRegression
Collinear features, regularizationRidge (L2)
Feature selection via sparsityLasso (L1)
Binary or multiclass classificationLogisticRegression

Clustering

Unsupervised clustering estimators. Live in datarust::cluster. All implement the Clusterer trait — the unsupervised counterpart to Predictor. fit takes only X (no target y); predict returns cluster indices as Vec<usize>.

KMeans

k-means clustering via Lloyd’s algorithm with k-means++ initialization. Partitions n observations into k clusters by minimizing within-cluster sum of squares (inertia). Each iteration assigns every point to its nearest centroid, then recomputes centroids as the mean of their members.

#![allow(unused)]
fn main() {
use datarust::cluster::{KMeans, KMeansInit};
use datarust::traits::Clusterer;

let mut km = KMeans::new()
    .with_n_clusters(3)                         // default 8
    .with_init(KMeansInit::KMeansPlusPlus)      // default; or KMeansInit::Random
    .with_n_init(10)                            // restarts, keep best (default 10)
    .with_max_iter(300)                         // default 300
    .with_tol(1e-4)                             // default 1e-4
    .with_random_state(42);                     // deterministic seed

let labels = km.fit_predict(&x)?;               // Vec<usize>, one cluster per row
let centers = km.cluster_centers();             // &[Vec<f64>], one centroid per cluster
let inertia = km.inertia();                     // f64, sum of squared distances
let iters = km.n_iter();                        // Lloyd's iterations of best run

let new_labels = km.predict(&new_x)?;           // assign new points to nearest centroid
}

n_clusters, n_init, and max_iter must be positive. tol must be finite and non-negative. Invalid values return InvalidConfig before clustering; the same checks apply immediately when values are supplied through Params.

Initialization strategies (KMeansInit):

  • KMeansPlusPlus (default) — first centroid is uniform random; each subsequent centroid is sampled with probability proportional to its squared distance from the nearest already-chosen centroid. Produces well-spread initial centroids and is the scikit-learn default.
  • Random — choose n_clusters distinct points uniformly at random from the data.

How it works:

  1. Initialize k centroids using the chosen strategy.
  2. Assignment step — each point is assigned to its nearest centroid (lowest squared Euclidean distance).
  3. Update step — each centroid is recomputed as the mean of its members. Empty clusters keep their previous centroid.
  4. Repeat until centroid movement drops below tol or max_iter is reached.
  5. Steps 1–4 run n_init times with different seeds; the lowest-inertia result is kept.

Reproducibility: with_random_state(seed) makes results fully deterministic — the same seed yields identical labels, centroids, and inertia. KMeans uses the crate’s internal xorshift64 PRNG, preserving the zero-dependency ethos (no rand crate).

Serialization: KMeans derives Serialize/Deserialize under the serde feature — fitted centroids, labels, and inertia round-trip through JSON, and the restored model serves predictions without refitting.

Choosing a clustering algorithm

GoalEstimator
Spherical, equally-sized blobsKMeans
Arbitrary-shape clusters(DBSCAN — planned, see Roadmap)
Hierarchical structure(AgglomerativeClustering — planned)

Evaluating clusters

Without ground-truth labels, the silhouette score assesses clustering quality from the data alone:

#![allow(unused)]
fn main() {
use datarust::cluster::metrics::silhouette_score;

let s = silhouette_score(&x, &labels)?;  // f64 in [-1, 1]
}

For each sample, the silhouette coefficient is (b − a) / max(a, b) where a is the mean intra-cluster distance and b is the mean distance to the nearest other cluster. Values near 1 indicate well-separated clusters; near 0 indicate overlapping clusters. The inertia() (within-cluster sum of squares, lower is better) is also useful for comparing runs with different n_clusters or random_state values.

Metrics

Model evaluation. Mirrors sklearn.metrics. Live in datarust::metrics.

Regression metrics

In datarust::metrics::regression. Each takes y_true and y_pred as &[f64].

#![allow(unused)]
fn main() {
use datarust::metrics::regression::*;

let mse  = mean_squared_error(&y_true, &y_pred, true)?;   // squared=true → MSE
let rmse = mean_squared_error(&y_true, &y_pred, false)?;  // squared=false → RMSE
let mae  = mean_absolute_error(&y_true, &y_pred)?;
let r2   = r2_score(&y_true, &y_pred)?;
let me   = max_error(&y_true, &y_pred)?;
let ev   = explained_variance_score(&y_true, &y_pred)?;
}
MetricRangeBestNotes
MSE[0, ∞)0Mean of squared errors
RMSE[0, ∞)0Root MSE (same units as y)
MAE[0, ∞)0Mean of absolute errors
(-∞, 1]11.0 = perfect; 0.0 = predicting the mean
max_error[0, ∞)0Worst single prediction
explained_variance(-∞, 1]1Variance of residuals explained

Classification metrics

In datarust::metrics::classification. Hard-label metrics accept arbitrary non-negative integer labels such as {2, 5, 9} and compact them internally. Fractional, negative, or non-finite labels are rejected. The compatibility precision, recall, and F1 functions use macro averaging; the *_with variants make the averaging strategy explicit.

#![allow(unused)]
fn main() {
use datarust::metrics::classification::*;

// Hard-label metrics (work for binary and multiclass):
let acc  = accuracy_score(&y_true, &y_pred)?;
let prec = precision_score(&y_true, &y_pred)?;     // macro-avg for multiclass
let rec  = recall_score(&y_true, &y_pred)?;
let f1   = f1_score(&y_true, &y_pred)?;
let cm   = confusion_matrix(&y_true, &y_pred)?;    // compact Vec<Vec<usize>>, n×n
let labeled = confusion_matrix_labeled(&y_true, &y_pred)?;
// labeled.labels[i] identifies row and column i

let weighted_f1 = f1_score_with(&y_true, &y_pred, Average::Weighted)?;
let positive_f1 = f1_score_with(
    &y_true,
    &y_pred,
    Average::Binary { positive_label: 5.0 },
)?;
let per_class = classification_report(&y_true, &y_pred)?;
let ll   = log_loss(&y_true, &y_proba, 1e-15)?;     // {0, 1}; P(label = 1)

// Ranking metrics for {0, 1}; scores refer to label 1:
let auc  = roc_auc_score(&y_true, &y_score)?;        // ROC-AUC (Mann–Whitney U)
let ap   = average_precision_score(&y_true, &y_score)?; // average precision

// For another binary label space, name the positive class explicitly:
let ll_custom = log_loss_with_positive_label(&y_true, &y_proba, 5.0, 1e-15)?;
let auc_custom = roc_auc_score_with_positive_label(&y_true, &y_score, 5.0)?;
let ap_custom = average_precision_score_with_positive_label(&y_true, &y_score, 5.0)?;

// Agreement & correlation (binary + multiclass):
let kap  = cohen_kappa_score(&y_true, &y_pred)?;     // chance-corrected agreement
let mcc  = matthews_corrcoef(&y_true, &y_pred)?;     // Matthews correlation
}
MetricRangeBestBinaryMulticlassNotes
accuracy[0, 1]1Fraction correctly classified
precision[0, 1]1Binary, macro, weighted, or micro averaging
recall[0, 1]1Binary, macro, weighted, or micro averaging
F1[0, 1]1Binary, macro, weighted, or micro averaging
confusion_matrixn×ndiagonal✓ (2×2)✓ (n×n)Compact counts; labeled variant retains row/column labels
log_loss[0, ∞)0Validated probabilities; explicit positive-label variant available
roc_auc_score[0, 1]1Finite ranking scores; explicit positive-label variant available
average_precision_score[0, 1]1Ties grouped by threshold; explicit positive-label variant available
cohen_kappa_score[-1, 1]1Chance-corrected agreement
matthews_corrcoef[-1, 1]1Balanced; robust to imbalance

precision_score_with_options, recall_score_with_options, and f1_score_with_options additionally accept ZeroDivision::Zero (the default) or ZeroDivision::Error for undefined class metrics.

Clustering metrics

In datarust::cluster::metrics. Evaluate clustering quality without ground-truth labels.

#![allow(unused)]
fn main() {
use datarust::cluster::metrics::silhouette_score;

let s = silhouette_score(&x, &labels)?;  // [-1, 1], higher is better
}

Cluster IDs are compacted internally rather than used as vector indices. At least two clusters and fewer clusters than samples are required, and a sample in a singleton cluster receives coefficient zero.

MetricRangeBestNotes
silhouette_score[-1, 1]1Compact labels; singleton coefficient 0; (b−a)/max(a,b) averaged over samples

Estimator .score() shorthand

Every estimator has a built-in score method:

  • Regression models (LinearRegression, Ridge, Lasso) → R².
  • LogisticRegression → accuracy.
#![allow(unused)]
fn main() {
let r2 = ridge.score(&x, &y)?;       // R²
let acc = logistic.score(&x, &y)?;   // accuracy
}

Model Selection

Train/test splitting and cross-validation. Mirrors sklearn.model_selection. Live in datarust::model_selection.

train_test_split

Split X and y into train and test subsets.

#![allow(unused)]
fn main() {
use datarust::model_selection::{train_test_split, TrainTestSplit};

// Quick split with defaults (25% test, shuffled):
let (x_tr, x_te, y_tr, y_te) = train_test_split(&x, &y)?;

// Or configure via the builder:
let (x_tr, x_te, y_tr, y_te) = TrainTestSplit::new()
    .with_test_size(0.2)        // fraction in (0, 1)
    .with_shuffle(true)
    .with_random_state(42)      // deterministic seed
    .split(&x, &y)?;
}

The split requires at least two samples. Fractional test sizes round up, so a 25% test share uses two rows when the dataset contains five rows.

KFold

K-fold cross-validation. Each sample serves as the test set exactly once.

#![allow(unused)]
fn main() {
use datarust::model_selection::KFold;

let cv = KFold::new()
    .with_n_splits(5)
    .with_shuffle(true)
    .with_random_state(42);

for (train_idx, test_idx) in cv.split(n_samples)? {
    let x_train = x.select_rows(&train_idx)?;
    let x_test = x.select_rows(&test_idx)?;
    // ...
}
}

StratifiedKFold

Approximately preserves class balance across folds. It accepts two or more non-negative integer-valued classes, including gapped labels such as {2, 5, 9}. Essential for imbalanced classification.

#![allow(unused)]
fn main() {
use datarust::model_selection::StratifiedKFold;

let cv = StratifiedKFold::new().with_n_splits(5);

for (train_idx, test_idx) in cv.split(&y)? {  // note: takes y, not n_samples
    // each fold has roughly the same class ratio as the full dataset
}
}

cross_val_score

Evaluate any Predictor + Clone estimator by K-fold cross-validation with a user-supplied scorer.

#![allow(unused)]
fn main() {
use datarust::model_selection::{cross_val_score, KFold};
use datarust::linear_model::LinearRegression;
use datarust::metrics::regression::r2_score;

let cv = KFold::new().with_n_splits(5);
let scores = cross_val_score(&LinearRegression::new(), &x, &y, &cv, r2_score)?;
// scores.len() == 5, one R² per fold
}

For classification, pass accuracy_score instead:

#![allow(unused)]
fn main() {
use datarust::linear_model::LogisticRegression;
use datarust::metrics::classification::accuracy_score;

let scores = cross_val_score(&LogisticRegression::new(), &x, &y, &cv, accuracy_score)?;
}

The scorer is any closure Fn(&[f64], &[f64]) -> Result<f64>, so you can pass custom metrics too.

Compose: Pipeline & ColumnTransformer

Chain transformers and dispatch different columns to different transformers. Live in datarust::pipeline and datarust::compose.

Pipeline

Chain multiple transformers sequentially. Each step fits on the output of the previous one.

#![allow(unused)]
fn main() {
use datarust::pipeline::Pipeline;
use datarust::scaler::{StandardScaler, MinMaxScaler, RobustScaler};
use datarust::transformer_kind::TransformerKind;

let mut pipe = Pipeline::new()
    .push("standard", TransformerKind::StandardScaler(StandardScaler::new()))
    .push("minmax",   TransformerKind::MinMaxScaler(MinMaxScaler::new()))
    .push("robust",   TransformerKind::RobustScaler(RobustScaler::new()));

let out = pipe.fit_transform(&x)?;
}

Pipelines are serializable under the serde feature — fit, save to JSON, load and transform in a different process. Before use, leaf transformers validate the dimensions and required fields of loaded fitted state, so inconsistent JSON returns an error instead of panicking.

Supervised pipeline

Attach a final estimator with with_estimator to fit preprocessing and a model together. During fit(X, y), supervised selectors such as SelectKBest receive the training targets before the final estimator is fitted.

#![allow(unused)]
fn main() {
use datarust::linear_model::LogisticRegression;
use datarust::pipeline::Pipeline;
use datarust::selection::{ScoreFunc, SelectKBest};
use datarust::traits::Predictor;
use datarust::transformer_kind::TransformerKind;

let mut model = Pipeline::new()
    .push("select", TransformerKind::SelectKBest(SelectKBest::new(ScoreFunc::FClassif, 5)?))
    .with_estimator(LogisticRegression::new());
model.fit(&x, &y)?;
let labels = model.predict(&x)?;
}

Runtime step inspection

#![allow(unused)]
fn main() {
pipe.names();           // ["standard", "minmax", "robust"]
pipe.get_step("minmax");  // Option<&TransformerKind>
pipe.set_step("robust", TransformerKind::StandardScaler(StandardScaler::new()));
pipe.insert_step(0, "impute", /* ... */);
pipe.remove_step("standard");
}

ColumnTransformer

Dispatch different columns to different transformers — the workhorse for mixed numeric + categorical data.

#![allow(unused)]
fn main() {
use datarust::compose::{ColumnTransformer, Remainder, Table};
use datarust::encoder::OneHotEncoder;
use datarust::scaler::StandardScaler;
use datarust::categorical_kind::CategoricalTransformerKind;
use datarust::transformer_kind::TransformerKind;

let mut ct = ColumnTransformer::new()
    .add_numeric(
        "nums",
        vec![0, 1, 2],           // column indices
        TransformerKind::StandardScaler(StandardScaler::new()),
    )
    .add_categorical(
        "cats",
        vec![3, 4],
        CategoricalTransformerKind::OneHotEncoder(OneHotEncoder::new()),
    )
    .remainder(Remainder::Passthrough); // keep un-specified columns

// Table bundles numeric (Matrix) + categorical (StrMatrix) of equal row count
let out = ct.fit_transform_to_table(&table)?;
// out.numeric    — the scaled + one-hot-encoded numeric block
// out.categorical — the transformed categorical block
}

Supervised encoders

For TargetEncoder, use fit_with_target / fit_transform_with_target:

#![allow(unused)]
fn main() {
ct.fit_transform_with_target(&table, &y)?;
}

When to use what

NeedUse
Sequential transforms on one matrixPipeline
Preprocessing + final supervised estimatorPipeline::with_estimator / SupervisedPipeline
Different columns → different transformersColumnTransformer
Mixed numeric + categorical in one callColumnTransformer + Table
Serializable fitted pipelinePipeline or ColumnTransformer + serde

Datasets

Classic toy datasets for examples, tests, and onboarding. Live in datarust::datasets. Enable with the datasets feature:

[dependencies]
datarust = { version = "*", features = ["datasets"] }

The data is compiled into the binary as const arrays — no file I/O, no network access, no external dependencies. Each loader returns a Dataset struct with features(), targets(), feature_names(), and target_names().

Available datasets

DatasetSamplesFeaturesClassesTask
Iris15043 (50 each)Classification
Breast Cancer569302 (357 / 212)Binary classification
Wine178133Multiclass classification
Diabetes44210Regression

Usage

#![allow(unused)]
fn main() {
use datarust::datasets;

let iris = datasets::iris::load();

let x = iris.features();           // Matrix 150×4
let y = iris.targets();            // &[f64], values {0, 1, 2}
let names = iris.feature_names();  // &["sepal_length", "sepal_width", ...]
let classes = iris.target_names(); // &["setosa", "versicolor", "virginica"]

assert_eq!(iris.n_samples(), 150);
assert_eq!(iris.n_features(), 4);
assert_eq!(iris.n_classes(), 3);
}

Feed directly into a model:

#![allow(unused)]
fn main() {
use datarust::datasets::iris;
use datarust::linear_model::LogisticRegression;
use datarust::traits::Predictor;

let data = iris::load();
let x = data.features();
let y = data.targets().to_vec();

let mut model = LogisticRegression::new().with_max_iter(200);
model.fit(&x, &y)?;
let accuracy = model.score(&x, &y)?;
println!("Iris accuracy: {:.1}%", accuracy * 100.0);
}

Choosing a dataset

GoalDataset
Quick multiclass classification demoIris — small, fast, well-separated
Binary classification benchmarkBreast Cancer — 30 features, imbalanced
Multiclass with more featuresWine — 13 chemical features
Regression baselineDiabetes — continuous target, 10 features

Performance vs scikit-learn

The numbers below are measured, not estimated. The same deterministic synthetic dataset (xorshift64, seed 42, values in [-100, 100)) is fed to both libraries, and the median fit_transform time over 15 runs (after one warmup) is reported.

Test setup: Apple M5 Pro (18 cores, arm64), Rust 1.96.0 (release), Python 3.9.6, scikit-learn 1.6.1, numpy 2.0.2, scipy 1.13.1. Times are in milliseconds. The Ratio column is sklearn_ms / datarust_ms — values > 1 mean datarust is faster.

Benchmark table

WorkloadSize (rows × cols)datarust default (ms)datarust +rayon (ms)sklearn (ms)best ratio
StandardScaler1 000 × 100.0230.0160.28017.5×
StandardScaler10 000 × 1001.241.202.462.0×
StandardScaler50 000 × 2008.24.722.44.8×
MinMaxScaler1 000 × 100.0250.0140.20214.4×
MinMaxScaler10 000 × 1001.701.471.320.9×
MinMaxScaler50 000 × 20010.87.511.61.5×
RobustScaler1 000 × 100.170.130.7685.8×
RobustScaler10 000 × 10011.22.0921.410×
RobustScaler50 000 × 20012314.0193.513.8×
PCA (k = min(10, cols/2))1 000 × 100.180.100.2262.2×
PCA10 000 × 10045411.390.03×
PCA50 000 × 20083881912.20.01×
Pipeline (Standard→MinMax→Robust)1 000 × 100.200.211.024.9×
Pipeline10 000 × 10013.24.125.26.1×
Pipeline50 000 × 20014426.7229.68.6×
OneHotEncoder (string)1 000 × 50.380.550.8001.5×
OneHotEncoder10 000 × 107.46.89.91.5×
OneHotEncoder50 000 × 2089802052.6×
ColumnTransformer (num + cat)1 000 × 50.0260.0264.6179×
ColumnTransformer10 000 × 100.230.2479.8347×
ColumnTransformer50 000 × 201.311.32812.8620×
LinearRegression (fit+predict)1 000 × 100.160.16
LinearRegression10 000 × 10014.414.4
LinearRegression50 000 × 200258258

Feature flags make a difference

matrixmultiply. Enabling this feature dispatches covariance and matmuls to a tuned pure-Rust GEMM (no system BLAS). On 50 000 × 200, PCA drops from 838 ms → 104 ms (8× faster), and LinearRegression from 258 ms → 84 ms (3× faster).

rayon. Parallel column/row processing. RobustScaler at 50 000 × 200 drops from 123 ms (default) to 14 ms (8.8× faster with rayon).

Where datarust wins

  • Mixed numeric + categorical composition. ColumnTransformer is 179–620× faster than scikit-learn’s on large inputs. This is the headline result — it reflects the cost of sklearn’s per-column Python dispatch, dtype coercion, and object-array marshalling.
  • String / categorical encoding. OneHotEncoder is ~1.5–2.6× faster because datarust operates on a native StrMatrix directly — no Python object-array overhead, no GIL.
  • Numeric scalers with rayon. StandardScaler/RobustScaler/Pipeline beat sklearn by 4.8–13.8× at 50 000 × 200.
  • Small data and startup latency. At 1 000 × 10, datarust is faster on every workload — up to 17.5× on StandardScaler. No Python interpreter to spin up, no numpy import cost.

Where scikit-learn still wins

  • PCA on tall-and-wide data (without the matrixmultiply feature). sklearn calls into LAPACK’s full SVD via shared-library BLAS; datarust uses a from-scratch Jacobi sweep. With matrixmultiply the gap narrows from 85× to ~8×, and PCASolver::Randomized closes it further for low-rank inputs.

Reproduce the benchmarks

The harness lives in examples/bench_compare_rust.rs (Rust side) and benches/compare_sklearn.py (Python side). Run on your own hardware:

# Rust (all feature combos)
cargo run --release --features matrixmultiply --example bench_compare_rust 15

# Python (requires numpy, scikit-learn)
python3 benches/compare_sklearn.py 15

Feature Comparison: datarust vs sklearn

A side-by-side view of what’s implemented. = supported, = no equivalent.

Preprocessing

Transformerdatarustsklearn
StandardScaler✓ (ddof=0)✓ (ddof=0)
MinMaxScaler✓ (custom range)
RobustScaler✓ (centering + scaling)
MaxAbsScaler
Normalizer (L1/L2/Max)
Binarizer
KBinsDiscretizer✓ (Uniform/Quantile/KMeans, Ordinal/OneHotDense)
QuantileTransformer✓ (Uniform/Normal output)
PowerTransformer✓ (Yeo-Johnson/Box-Cox + MLE lambda)

Encoders

Transformerdatarustsklearn
LabelEncoder✓ (handle_unknown: Error/Ignore)
OrdinalEncoder✓ (auto + manual)
OneHotEncoder✓ (drop, handle_unknown, sparse CSR)
TargetEncoder✓ (smoothed mean, UnknownTarget)
FrequencyEncoder✓ (count/proportion)

Imputation & Selection

Componentdatarustsklearn
SimpleImputer✓ (mean/median/most_frequent/constant)
KNN Imputer✓ (uniform/distance)
PolynomialFeatures✓ (degree, interaction_only, bias)
VarianceThreshold
SelectKBest✓ (F-classif / Chi2 / Mutual Info)

Models & Decomposition

Componentdatarustsklearn
LinearRegression✓ (Cholesky & SVD)
Ridge✓ (L2, Cholesky & SVD)
Lasso✓ (L1, coordinate descent)
LogisticRegression✓ (IRLS, Cholesky & SVD)
PCA✓ (Jacobi + randomized SVD, whiten, inverse)
TruncatedSVD✓ (Count/Variance/All)

Model Selection & Metrics

Componentdatarustsklearn
train_test_split
KFold / StratifiedKFold
cross_val_score
Regression metrics (MSE, R², …)✓ (5 metrics)
Classification metrics (accuracy, F1, ROC-AUC, …)✓ (10 metrics)
Clustering metrics (silhouette)
Hyperparameter introspection (Params trait)

Composition & Infrastructure

Featuredatarustsklearn
Pipeline✓ (TransformerKind, serde)
ColumnTransformer✓ (numeric + categorical + target)
FunctionTransformer✓ (optional inverse, closure-based)
FeatureNames✓ (trait, all transformers)
inverse_transform✓ (scalers, PCA, SVD, encoders)
Pipeline Ergonomics✓ (get/set/insert/remove step)
Matrix Slicing✓ (select_columns, select_rows)
Covariance / Correlation✓ (ddof-configurable)
JSON Serialization✓ (serde feature)— (joblib/pickle)
Sparse Output✓ (CSR via SparseMatrix)
Parallelism✓ (rayon feature)— (joblib)

Where datarust goes beyond sklearn

  • FrequencyEncoder — count/proportion encoding not in sklearn.
  • Pipeline runtime editingget_step, set_step, insert_step, remove_step for live pipeline surgery.
  • JSON serialization — human-readable, language-agnostic fitted-model persistence (vs sklearn’s binary joblib/pickle).
  • Type-safe categorical/numeric separation — the compiler prevents mixing StrMatrix and Matrix.

Rust ecosystem comparison

datarust is a preprocessing-first classical ML library. Within the Rust ecosystem its direct peers are smartcore and linfa. Deep-learning frameworks (candle, burn, tch-rs) solve a different problem and are out of scope here; rusty-machine has been archived and is omitted.

Design philosophies

The three libraries were built around different priorities, which shows up in every design decision:

  • datarust optimizes for preprocessing depth and zero-dependency deployability. All linear algebra is pure Rust (Jacobi eigensolver, Cholesky, coordinate descent), so the default build has no external C libraries — it links cleanly into WASM, embedded targets, and CLI tools. The trade-off is algorithm breadth: only the four linear models (Linear / Ridge / Lasso / Logistic regression) and KMeans clustering are implemented so far.
  • smartcore optimizes for single-crate algorithm breadth. One dependency gives you SVM, RandomForest, DecisionTree, KMeans, DBSCAN, KNN, NaiveBayes, and more, plus model selection and metrics. The trade-off is preprocessing: only StandardScaler and OneHotEncoder are provided, and the crate depends on ndarray plus a BLAS backend.
  • linfa optimizes for modularity. Each algorithm family lives in its own crate (linfa-svm, linfa-trees, linfa-clustering, linfa-ensemble, …) behind a shared linfa-core trait surface, so you only compile what you use. linfa-preprocessing additionally offers scalers and text vectorizers (Count / TF-IDF). The trade-off: categorical encoders, imputers, and feature selectors are sparse or undocumented, and Ridge/Lasso exist only through linfa-elasticnet’s l1_ratio parameter.

Feature matrix

Verified against the July 2026 releases: smartcore 0.5.3, linfa 0.8.1. Legend: present, confirmed absent, ? not clearly documented at the time of writing — please open an issue or PR if a cell goes stale.

Preprocessing & Encoders

Componentdatarustsmartcorelinfa
StandardScaler
MinMaxScaler
RobustScaler?
MaxAbsScaler
Normalizer (L1/L2/Max)
KBinsDiscretizer?
QuantileTransformer?
PowerTransformer?
OneHotEncoder?
OrdinalEncoder?
LabelEncoder?
TargetEncoder
FrequencyEncoder
SimpleImputer??
KNN Imputer??
PolynomialFeatures??
VarianceThreshold??
SelectKBest??
Text vectorizers (Count/TF-IDF)

Models & Decomposition

Componentdatarustsmartcorelinfa
LinearRegression
Ridge (dedicated)✗ (via ElasticNet l1_ratio=0)
Lasso (dedicated)✗ (via ElasticNet l1_ratio=1)
LogisticRegression
PCA
TruncatedSVD
SVM
RandomForest / DecisionTree
KMeans✓ (k-means++ init)
DBSCAN

Infrastructure

Featuredatarustsmartcorelinfa
Pipeline??
ColumnTransformer?
train_test_split?
KFold / StratifiedKFold?
cross_val_score?
Regression + Classification metrics✓ (15 metrics + silhouette)
JSON model serialization✓ (serde)??
Zero external deps by default✗ (ndarray + BLAS)✗ (ndarray + BLAS)
WASM-friendly (no native BLAS)??
Distribution modelsingle cratesingle crateper-algorithm crates

When to choose which

Your priorityReach for
Rich preprocessing (scalers, encoders, imputers, selection)datarust
WASM / embedded / single-binary deployment, no BLASdatarust
SVM, trees, DBSCAN, or the widest single-crate model zoosmartcore
Modular compile-what-you-use algorithms, or text vectorizerslinfa
Deep learning (transformers, CNNs, autograd)candle / burn / tch-rs

Complementary use

These libraries are not mutually exclusive. Because all three expose plain Vec/matrix in/out interfaces, you can mix them in a single pipeline: use datarust for preprocessing (where its coverage is unique), then hand the transformed features to a smartcore or linfa estimator for an algorithm datarust doesn’t implement yet.

Examples

Runnable examples from the examples/ directory. Each can be run with cargo run --example <name>.

Basic preprocessing

cargo run --example basic_preprocessing

Standardize, scale, and encode a small mixed-type dataset. Demonstrates StandardScaler, MinMaxScaler, OneHotEncoder, and the Transformer trait.

Pipeline workflow

cargo run --example pipeline_workflow

Build a 3-step pipeline (StandardScaler → MinMaxScaler → RobustScaler), fit and transform, then inspect individual steps via the runtime ergonomics API.

Target encoding

cargo run --example target_encoding

Use TargetEncoder with smoothed mean encoding on a high-cardinality categorical feature, including the TargetTransformer trait and fit_with_target.

Regression workflow

cargo run --example regression_workflow

End-to-end regression on synthetic house-price data: generate data, train_test_split, fit a StandardScaler on train only (data-leakage prevention), train a Ridge, score test R², then run 5-fold cross_val_score. Shows the full preprocess → train → evaluate → cross-validate loop on a single realistic dataset.

Classification workflow

cargo run --example classification_workflow

Customer-churn binary classification with mixed numeric + categorical features. A ColumnTransformer applies StandardScaler to numeric columns and OneHotEncoder to the categorical column, feeding a LogisticRegression. Reports the full metric suite (accuracy, precision, recall, f1, confusion_matrix, log_loss), compares decision thresholds (0.3 / 0.5 / 0.7) to show the precision–recall trade-off, and finishes with stratified 5-fold cross-validation driven manually (StratifiedKFold::split, since cross_val_score only accepts KFold).

Regularization comparison

cargo run --example regularization_comparison

Compares Ridge (L2) and Lasso (L1) across multiple alpha values on an 8-feature dataset where only 3 features carry signal, one feature is collinear, and the rest are pure noise. Ridge shrinks all coefficients but zeros none; Lasso’s soft-thresholding drives the noise features to exactly zero (automatic feature selection). The collinear feature would make LinearRegression singular — both regularized solvers handle it.

Model persistence

cargo run --example model_persistence --features serde

Production-style model persistence. Trains a SupervisedPipeline<Ridge> (StandardScaler → PCA → Ridge), writes it to a JSON file with save_json, reloads it with load_json, and confirms the restored model is still is_fitted() and produces bit-identical predictions without refitting. Requires the serde feature.

KMeans clustering

cargo run --example kmeans_clustering

Unsupervised clustering on synthetic 2-D point data. Generates three well-separated blobs, fits a KMeans with k-means++ initialization, inspects the learned centroids against the true blob centers, predicts cluster assignments for new points, compares k-means++ vs random initialization by inertia, and (with --features serde) round-trips the fitted model through JSON. Demonstrates the Clusterer trait.

Benchmark comparison

cargo run --release --example bench_compare_rust

The deterministic benchmark harness that produces the numbers on the Performance page. Uses a xorshift64 PRNG (seed 42) so Rust and Python generate identical data. Pass a repetition count as an argument:

cargo run --release --features matrixmultiply --example bench_compare_rust 15

Using embedded datasets

The datasets feature provides classic toy datasets (Iris, Breast Cancer, Wine, Diabetes) as const arrays — no file I/O needed. See the Datasets guide for the full API.

#![allow(unused)]
fn main() {
use datarust::datasets::iris;
use datarust::linear_model::LogisticRegression;
use datarust::traits::Predictor;

let data = iris::load();
let x = data.features();
let y = data.targets().to_vec();

let mut model = LogisticRegression::new().with_max_iter(200);
model.fit(&x, &y)?;
println!("Iris accuracy: {:.1}%", model.score(&x, &y)? * 100.0);
}

Architecture

How datarust is organized and the design decisions behind it.

Design philosophy

  1. Zero external dependencies by default. cargo add datarust pulls in nothing. All linear algebra — Jacobi eigendecomposition, one-sided Jacobi SVD, Cholesky factorization, coordinate descent, IRLS — is pure Rust. Feature flags (serde, rayon, matrixmultiply) opt in to extras.

  2. sklearn-inspired but Rust-native. The familiar fit/transform/predict API, but using Result (panic-free), builder-pattern config, and enum-based parameters (Norm::L2, ImputeStrategy::Median) instead of strings.

  3. Type-safe modality separation. Four traits enforce that numeric, categorical, target, and label data go to the right transformer. The compiler catches misuse.

  4. Composability via type-erased enums. TransformerKind, CategoricalTransformerKind, and TargetTransformerKind allow heterogeneous pipelines that are still serializable.

Module layout

src/
├── lib.rs                 # Crate root: re-exports + module declarations
├── error.rs               # DatarustError enum + Result alias
├── traits.rs              # Estimator, Predictor, Regressor, Classifier, transformers, FeatureNames
├── matrix.rs              # Matrix (f64 flat), StrMatrix, SparseMatrix (CSR)
├── stats.rs               # Column statistics, covariance/correlation
├── pipeline.rs            # Sequential and supervised Pipeline
├── transformer_kind.rs    # TransformerKind enum (type erasure)
├── categorical_kind.rs    # CategoricalTransformerKind enum
├── target_kind.rs         # TargetTransformerKind enum
├── function_transformer.rs
├── polynomial.rs          # PolynomialFeatures
├── serialize.rs           # JSON save/load (serde feature)
├── datasets/              # Iris, Breast Cancer, Wine, Diabetes (datasets feature)
├── linalg/
│   └── cholesky.rs        # Cholesky decomposition + SPD solver
├── scaler/                # 9 transformers (standard, minmax, robust, ...)
├── encoder/               # 5 encoders (onehot, ordinal, label, target, frequency)
├── imputer/               # SimpleImputer, KnnImputer
├── selection/             # VarianceThreshold, SelectKBest
├── decomposition/         # PCA, TruncatedSVD, Jacobi, randomized_svd
├── linear_model/          # LinearRegression, Ridge, Lasso, LogisticRegression
├── metrics/
│   ├── regression.rs      # MSE, MAE, R², max_error, explained_variance
│   └── classification.rs  # accuracy, precision, recall, F1, confusion_matrix, log_loss
├── model_selection/
│   ├── split.rs           # train_test_split
│   ├── kfold.rs           # KFold, StratifiedKFold
│   ├── cross_val.rs       # cross_val_score
│   └── rng.rs             # shared xorshift64 PRNG
└── compose/
    ├── column_transformer.rs
    └── output.rs

Trait hierarchy

TraitData flowImplementors
TransformerMatrix → Matrixall scalers, PCA, TruncatedSVD, PolynomialFeatures, VarianceThreshold, SelectKBest, imputers, FunctionTransformer
Predictorfit(X, y) + predict(X) → Vec<f64>all linear models and supervised pipelines
Regressorcontinuous-prediction semanticsLinearRegression, Ridge, Lasso
Classifierclass-label prediction semanticsLogisticRegression
CategoricalTransformerStrMatrix → MatrixOneHotEncoder, OrdinalEncoder, FrequencyEncoder
TargetTransformerfit(StrMatrix, y)TargetEncoder
LabelTransformer&[String] ↔ Vec<usize>LabelEncoder
FeatureNamesoutput column namesevery output-producing transformer

Solver infrastructure

Three distinct solver families, all pure-Rust, in linalg/:

  1. Cholesky (linalg::cholesky) — symmetric positive-definite system solver. Used by LinearRegression, Ridge, and LogisticRegression (per IRLS iteration).
  2. Coordinate descent (Lasso) — soft-thresholding iteration for L1-regularized problems.
  3. IRLS (LogisticRegression) — Newton-Raphson on the logistic loss, solving a weighted least-squares system each iteration.

All three are backed by the shared Matrix::matmul for forming Gram matrices, which dispatches to a tuned GEMM under the matrixmultiply feature.

Error handling

Hand-rolled DatarustError enum (no anyhow/thiserror, consistent with the zero-dependency ethos). Every fallible public API returns Result<T, DatarustError>. The variant set is ML-domain-specific (NotFitted, UnknownCategory, Singular, etc.), more informative than a generic error blob.

Roadmap

The canonical, detailed roadmap lives in ROADMAP.md at the repository root. This page gives a higher-level tour of why each phase is ordered the way it is, and what it unlocks for users.

The destination

datarust aims to be the scikit-learn of Rust for classical, CPU-bound machine learning: the library you reach for when you need preprocessing, trees, clustering, SVM, cross-validation, and text features — without pulling in BLAS, a Python runtime, or a GPU stack.

The principles that shape every decision:

  • Zero dependencies by default. Every algorithm ships as pure Rust.
  • CPU-first. GPU and deep learning are served by candle and burn.
  • scikit-learn API parity where Rust allows it; type-safe improvements where Rust enables them.
  • No panics — public APIs return Result; missing_docs is enforced.

Where we are (v0.6.5)

The preprocessing, linear-model, and clustering foundations are solid:

  • 18 transformers/encoders/imputers/selectors — the deepest preprocessing coverage of any Rust ML library.
  • 4 linear models (Linear / Ridge / Lasso / Logistic regression) with binary IRLS and multiclass softmax support.
  • KMeans clustering (Lloyd’s algorithm, k-means++ initialization) + silhouette score.
  • 15 metrics: regression (MSE, MAE, R², …), classification (accuracy, precision, recall, F1, ROC-AUC, PR-AUC, kappa, Matthews, …), clustering (silhouette).
  • Pipeline, ColumnTransformer, cross-validation, Params trait.
  • Zero external dependencies by default; serde / rayon / matrixmultiply are opt-in.

What is conspicuously absent: trees and ensembles, hyperparameter search, text features, and SVM. The roadmap below addresses each, in order of impact.

The release track

v0.7 — Tree-based learning

Why this comes first. Trees and ensembles are the single most requested feature and the backbone of tabular ML. A shared, seedable RNG is a prerequisite for bootstrap sampling in ensembles — the current private xorshift64 needs to be promoted first.

Highlights: DecisionTree (CART), RandomForest, ExtraTrees, a Bagging meta-estimator, and feature_importances_ output. A new src/tree/ and src/ensemble/ module pair.

v0.8 — Model selection & text

Why this comes second. Once the algorithm catalog is broader, the next bottleneck is tuning and NLP. GridSearchCV builds directly on the Params trait (get_params/set_params) shipped in v0.6. Text vectorizers depend on sparse matrix arithmetic, which today does not exist — so this phase widens SparseMatrix from read-only storage into a real linear-algebra type.

Highlights: GridSearchCV/RandomizedSearchCV, validation_curve/ learning_curve, CountVectorizer/TfidfVectorizer, KNeighbors, and Naive Bayes — the last three together forming a complete document-classification stack.

v0.9 — Depth & breadth

Why this is late. These are high-value but lower-leverage than the foundational phases. Many users will be productive with v0.7–0.8 alone.

Highlights: GradientBoosting/AdaBoost/Voting/Stacking, LinearSVC/SVC (SMO solver, pure Rust), DBSCAN/AgglomerativeClustering, ElasticNet/ SGDClassifier, NMF/FactorAnalysis, an embedded datasets module, and a csv feature flag for data loading.

v1.0 — Stability

Why v1.0 is mostly cleanup. By this point the API surface has been exercised across many estimators and is ready to be frozen. v1.0 removes legacy #[doc(hidden)] APIs, refreshes ARCHITECTURE.md, runs a full public-API audit, and publishes the SemVer stability commitment.

What is deliberately out of scope

The roadmap is as much about what datarust will not become:

  • GPU compute and deep learning — served by candle and burn.
  • Distributed training — a networking project, not a classical-ML one.
  • pickle/joblib compatibility — technically impossible across languages.
  • SHAP/LIME — permutation importance (planned) covers the common need.

A few items are under consideration for post-1.0 but not committed: f32 generics, manifold learning (TSNE), HistGradientBoosting, ONNX export/import, NumPy .npy interop, and a minimal MLPClassifier. See ROADMAP.md for the full list and rationale.

Contributing

The checkboxes in ROADMAP.md double as a contribution guide. Pick an unchecked item, open an issue to align on API shape, and land it with doc comments and sklearn-parity tests. Progress is recorded in CHANGELOG.md under each release.

Changelog

All notable changes to datarust are documented in the project’s CHANGELOG.md on GitHub.

This page provides a summary. For the full, detailed changelog (including internal refactors and performance tables), see the canonical source.

Unreleased

0.6.5

  • Restored enforceable Rust 1.70 compatibility by pinning MSRV-compatible Rayon and serde_json dependency lines.
  • Hardened deserialized categorical encoder state against mismatched category lists, index maps, and output widths.
  • Rejected fractional ordinal codes during inverse transformation.
  • Completed fitted-state checks for KNN/Simple imputers, MaxAbsScaler, and QuantileTransformer.
  • Aligned PCA solver and component-count documentation with the implemented behavior.

0.6.4

  • Silhouette scoring compacts arbitrary cluster IDs, handles singleton clusters correctly, and rejects invalid one-cluster-per-sample input.
  • Numerical estimators, decompositions, scalers, selectors, and regression metrics reject non-finite observations; imputers retain NaN missing-value support while rejecting infinity.
  • Non-finite scaler, selector, encoder, and SVD configuration values fail before fitting.
  • Inconsistent fitted state loaded from JSON returns a recoverable error instead of panicking during transform or prediction.
  • CI now builds and validates the docs/blog site and denies rustdoc warnings explicitly.

0.6.3

  • Ridge, Lasso, logistic regression, and KMeans now reject non-finite or otherwise invalid solver hyperparameters before optimization.
  • Params::set_params validation no longer mutates KMeans or logistic regression when a candidate is invalid.
  • CI now executes the intended OS × feature matrix, including datasets and an all-features build, and verifies the crates.io package.
  • The published crate excludes the blog, book, website, npm, and Cloudflare deployment sources, reducing the compressed archive from about 3.0 MiB to 289 KiB.

0.6.2

  • Safe dense access now checks bounds in release builds, while dense allocations reject dimension overflow.
  • Raw and deserialized CSR matrices validate pointer ranges and sorted column indices; duplicate triplets are summed.
  • Train/test and cross-validation reject undersized or mismatched inputs, and fractional test sizes now round up consistently.
  • StratifiedKFold supports validated, non-contiguous multiclass labels.
  • Binary log loss, ROC-AUC, and average precision validate labels and numeric inputs. New *_with_positive_label functions support label spaces such as {2, 5}, and average-precision ties are threshold-grouped.

0.6.1

  • Classification labels are now compacted safely, so metrics and LogisticRegression support non-contiguous labels such as {2, 5, 9} while preserving original predictions and probability-column mappings.
  • Precision, recall, and F1 now expose explicit binary, macro, weighted, and micro averaging, configurable zero-division handling, and per-class reports.
  • The optional datasets feature adds embedded Iris, Breast Cancer, Wine, and Diabetes datasets with no runtime file or network dependency.

0.5.0

  • Estimator hierarchyEstimator, Predictor, Classifier, and PredictProba now define common supervised flows. Regressor is reserved for regression semantics.
  • SupervisedPipelinePipeline::with_estimator combines preprocessing, target-aware feature selection, and a final predictor in a cloneable, serde-serializable pipeline.
  • BREAKING: LogisticRegression classification semanticspredict returns hard labels; predict_proba returns a two-column probability matrix; predict_positive_proba exposes the positive-class vector.
  • BREAKING: Predictor imports — import Predictor to call shared fit/predict methods on supervised models.
  • BREAKING: Custom trait implementors — custom transformers and encoders must implement Estimator; custom regressors must implement Estimator and Predictor before Regressor.

0.4.0

Added

  • model_selection moduletrain_test_split, KFold, StratifiedKFold, cross_val_score. Shared deterministic xorshift64 PRNG, now used by both model_selection and decomposition::randomized_svd.
  • LogisticRegression — binary classification via IRLS (Cholesky/SVD). The crate’s first classifier.
  • metrics::classificationaccuracy_score, precision_score, recall_score, f1_score, confusion_matrix, log_loss.
  • Ridge — L2-regularized regression (Cholesky/SVD). Succeeds on collinear inputs.
  • Lasso — L1-regularized regression via coordinate descent. Produces sparse models (feature selection).
  • Regressor trait — supervised counterpart of Transformer (fit(X, y) + predict).
  • linalg::cholesky module — shared SPD solver foundation.
  • LinearRegression — OLS regression, the crate’s first estimator.
  • metrics::regression — MSE/RMSE, MAE, R², max_error, explained_variance.
  • PCASolver enum (Auto / Full / Randomized) on PCA.
  • jacobi::eigh_topk_flat — power-iteration + deflation for top-k eigenpairs.
  • Flat-storage Jacobi eigensolver, flat matmul helpers, flat covariance.

Performance

  • PCA 50 000 × 200 dropped from ~320 ms to ~104 ms with matrixmultiply.
  • LinearRegression fit at 50 000 × 200: 258 ms → 84 ms with matrixmultiply.
  • Scaler transform rayon threshold: scalar loop below 4 096 rows, parallel above.

0.3.0

  • matrixmultiply feature: optional tuned pure-Rust GEMM (no system BLAS).
  • BREAKING: Matrix internal storage switched from Vec<Vec<f64>> to a single contiguous Vec<f64>. ~13× on RobustScaler, ~5× on StandardScaler at 50 000 × 200.
  • Flat-storage scalers, fused Welford statistics, NaN validation fused into transform loops.

Earlier versions

See the full changelog on GitHub for 0.1.x and 0.2.x history.

API Reference (docs.rs)

The complete API reference for datarust is hosted on docs.rs — the standard home for Rust crate documentation.

→ Open the full API reference on docs.rs

What’s there

docs.rs builds the rustdoc documentation with all features enabled (serde, rayon, matrixmultiply, datasets), so you see the complete API surface including:

  • Every pub struct, pub enum, pub trait, and pub fn with doc comments.
  • Type signatures, trait implementations, and method docs.
  • Inline code examples from doc comments (runnable via cargo test --doc).
  • Cross-links between types and modules.
Traitdocs.rs link
Transformerdatarust::Transformer
Regressordatarust::Regressor
Predictordatarust::Predictor
Classifierdatarust::Classifier
PredictProbadatarust::PredictProba
Clustererdatarust::Clusterer
Paramsdatarust::Params
CategoricalTransformerdatarust::CategoricalTransformer
FeatureNamesdatarust::FeatureNames

Building locally

You can generate the same docs offline:

cargo doc --all-features --no-deps --open

The --open flag opens the generated HTML in your browser.