datarust
The missing scikit-learn experience for Rust. — a modular, dependency-free preprocessing and classical ML library built on a lightweight Matrix type.
📖 Read the documentation book →
let mut scaler = new;
let normalized = scaler.fit_transform?;
Features
| Category | Transformers |
|---|---|
| Scalers | StandardScaler, MinMaxScaler, RobustScaler, MaxAbsScaler, Normalizer (L1/L2/Max) |
| Discretizers | KBinsDiscretizer (Uniform / Quantile / KMeans), Binarizer |
| Distribution Transformers | QuantileTransformer (Uniform / Normal output), PowerTransformer (Yeo-Johnson / Box-Cox) |
| Encoders | LabelEncoder (+ handle_unknown), OneHotEncoder (+ CSR sparse output), OrdinalEncoder, TargetEncoder, FrequencyEncoder |
| Imputers | SimpleImputer (mean / median / most_frequent / constant), KnnImputer (uniform / distance) |
| Polynomial | PolynomialFeatures (degree, interaction_only, include_bias) |
| Selection | VarianceThreshold, SelectKBest (ANOVA F / Chi2 / Mutual Information) |
| Decomposition | PCA (with whiten, inverse_transform), TruncatedSVD (SVDComponents: Count/Variance/All) |
| Linear Models | LinearRegression (Cholesky & SVD), Ridge (L2), Lasso (L1, coordinate descent, sparse) |
| Classification | LogisticRegression (binary IRLS + multiclass softmax, Cholesky & SVD) |
| Clustering | KMeans (Lloyd's algorithm, k-means++ initialization, n_init restarts), silhouette_score |
| Metrics | Regression: MSE/RMSE, MAE, R², max_error, explained_variance. Classification: accuracy, precision, recall, F1, confusion_matrix (n×n), log_loss, ROC-AUC, average precision, Cohen's kappa, Matthews corrcoef (macro-averaged for multiclass) |
| Model Selection | train_test_split, KFold, StratifiedKFold, cross_val_score |
| Pipeline | Sequential + supervised Pipeline (serde-serializable), ColumnTransformer (numeric + categorical) |
| Feature Names | FeatureNames trait on all transformers for output column names |
| Serialization | JSON save/load via optional serde feature |
| Parallelism | Rayon-backed column operations via optional rayon feature |
| Sparse | CSR SparseMatrix type for memory-efficient one-hot output |
| Datasets | Iris, Breast Cancer, Wine, Diabetes (embedded const arrays, datasets feature) |
Default build has zero external dependencies. All linear algebra (eigenvalue decomposition, covariance) is implemented in pure Rust using the Jacobi algorithm.
Quick Start
use *;
use Matrix;
// Create a 4×2 matrix
let x = new?;
// Standardize: (x - mean) / std (population, ddof=0)
let mut scaler = new;
let standardized = scaler.fit_transform?;
// Scale to [0, 1]
let mut minmax = new;
let scaled = minmax.fit_transform?;
Installation
Add to your Cargo.toml:
[]
= "0.6"
Optional features
[]
= { = "0.6", = ["serde", "rayon"] }
serde— enables JSON serialization/deserialization of fitted transformers viadatarust::serialize::{save_json, load_json, to_json, from_json}.rayon— enables parallel column statistics and transforms for large datasets.matrixmultiply— enables a tuned pure-Rust GEMM (no system BLAS) for matrix products and covariance computation, speeding up PCA and TruncatedSVD on large dense inputs. The default build remains zero-external-dependency.
Numerical estimators and transformers reject NaN and infinite observations
before computation. SimpleImputer and KnnImputer keep accepting NaN as
the missing-value marker, but reject positive and negative infinity.
datasets— embeds classic toy datasets (Iris, Breast Cancer, Wine, Diabetes) asconstarrays for examples, tests, and onboarding. No file I/O or network access.
Core Concepts
Matrix
The fundamental data container is Matrix, a row-major dense matrix backed by a single contiguous Vec<f64> buffer with validation:
let m = new?;
assert_eq!;
assert_eq!;
assert_eq!;
Categorical data uses StrMatrix (Vec<Vec<String>>), and sparse output is available as SparseMatrix (CSR).
Transformer Trait
All numeric transformers implement the Transformer trait:
Supervised Estimator Traits
All supervised estimators implement the Predictor contract. Regressors also implement Regressor; classifiers implement Classifier, and probabilistic classifiers implement PredictProba:
If you implement datarust traits for your own type, first add the marker
impl Estimator for MyType {}. Custom supervised models then implement
Predictor before their Regressor or Classifier semantic trait.
Clusterer Trait
Unsupervised clustering estimators implement the Clusterer trait. Unlike Predictor, fit takes only X (no target y), and predict returns cluster indices as Vec<usize> rather than regression targets or class labels:
Implemented by KMeans. Custom clustering estimators follow the same
pattern: add the marker impl Estimator for MyClusterer {}, then implement
Clusterer.
Params Trait (hyperparameter introspection)
Estimators whose hyperparameters should be searchable (for future
GridSearchCV) implement the Params trait.
It exposes get_params / set_params over a type-safe ParamValue enum:
Implemented by KMeans (n_clusters, max_iter, tol, n_init) and
LogisticRegression (max_iter, tol, fit_intercept).
Not every estimator needs Params — only those whose hyperparameters should be
tunable by an automated search.
CategoricalTransformer Trait
Categorical encoders (OneHot, Ordinal, Frequency) implement the CategoricalTransformer trait (StrMatrix → Matrix):
OneHotEncoder and OrdinalEncoder provide real inverse_transform; FrequencyEncoder returns an error (non-injective).
TargetTransformer Trait
The TargetTransformer trait extends categorical encoding to supervised transformers that require target values during fit:
All target transformers (currently only TargetEncoder) support fit_transform with target values and a default inverse_transform that returns an error.
LabelTransformer Trait
The LabelTransformer trait maps 1-D string labels to integer indices (&[String] → &[usize]), used by LabelEncoder:
Errors
Operations return Result<T, DatarustError> with variants for NotFitted, InvalidInput, ShapeMismatch, EmptyInput, AllMissing, UnknownCategory, UnknownLabel, InvalidConfig, and Singular.
Architecture
See ARCHITECTURE.md for a deep dive into the crate's module layout, trait hierarchy, type erasure, design decisions, and error handling philosophy.
Key architectural highlights:
| Layer | Description |
|---|---|
| Matrix types | Matrix (f64), StrMatrix (String), SparseMatrix (CSR) — all with validation |
| Core traits | Estimator, Transformer, Predictor, Regressor, Classifier, PredictProba, categorical traits |
| Type erasure | TransformerKind, CategoricalTransformerKind, TargetTransformerKind — enable heterogeneous Pipeline and ColumnTransformer |
| Features | serde (JSON save/load), rayon (parallel iterators) — both optional, zero deps by default |
API Reference
Scalers
StandardScaler
Standardize features by removing the mean and scaling to unit variance.
Uses population standard deviation (ddof = 0), matching sklearn.
use StandardScaler;
let mut s = new
.with_mean
.with_std;
let out = s.fit_transform?;
// out[i][j] = (x[i][j] - mean[j]) / std[j]
MinMaxScaler
Scale each feature to a given range (default [0, 1]).
use MinMaxScaler;
let mut s = new
.feature_range;
let out = s.fit_transform?;
// out[i][j] = (x[i][j] - min[j]) / (max[j] - min[j]) * range + lo
RobustScaler
Scale using median and IQR (outlier-resistant).
use RobustScaler;
let mut s = new;
let out = s.fit_transform?;
// out[i][j] = (x[i][j] - median[j]) / (q75[j] - q25[j])
MaxAbsScaler
Scale by dividing by the maximum absolute value per feature. Preserves sparsity structure.
use MaxAbsScaler;
let mut s = new;
let out = s.fit_transform?;
// out[i][j] = x[i][j] / max(abs(col_j))
Normalizer
Normalize samples individually to unit norm (row-wise).
use ;
let mut n = new; // L1, L2, or Max
let out = n.fit_transform?;
// row := row / norm(row)
Binarizer
Binarize features (thresholding at a given value).
use Binarizer;
let mut b = new.threshold;
let out = b.fit_transform?;
// out[i][j] = 1.0 if x[i][j] > 0.5 else 0.0
KBinsDiscretizer
Bin continuous data into intervals.
use ;
let mut kb = new?
.strategy // Uniform, Quantile, or KMeans
.encode; // Ordinal or OneHotDense
let out = kb.fit_transform?;
QuantileTransformer
Transform features using quantile information to follow a uniform or normal distribution. Robust to outliers.
use ;
let mut qt = new?
.output_distribution; // or Uniform
let out = qt.fit_transform?;
PowerTransformer
Apply a power transform (Yeo-Johnson or Box-Cox) to make data more Gaussian-like. Lambda is estimated via MLE with golden-section search.
use ;
let mut pt = new
.method // or BoxCox (requires positive data)
.standardize; // zero-mean, unit-variance after transform
let out = pt.fit_transform?;
Encoders
LabelEncoder
Encode string labels as integer values 0..n_classes-1 (sorted lexicographically).
use ;
let mut encoder = new;
encoder.fit?;
let encoded = encoder.transform?;
// encoded = [1, 2]
// Handle unknown labels gracefully (returns usize::MAX):
let mut encoder = new
.handle_unknown;
encoder.fit?;
let out = encoder.transform?;
// out = [0, usize::MAX, 1]
OneHotEncoder
Encode categorical features as a one-hot numeric matrix.
use ;
use StrMatrix;
let s = from_column?;
let mut ohe = new
.drop
.handle_unknown;
let dense = ohe.fit_transform?; // Matrix
let sparse = ohe.fit_transform_sparse?; // SparseMatrix (CSR)
// Inverse transform reconstructs categories from one-hot codes
let decoded = ohe.inverse_transform?;
assert_eq!;
// Sparse inverse via conversion
let decoded_sparse = ohe.inverse_transform_sparse?;
The CSR SparseMatrix output stores only the 1.0 positions, saving significant memory for high-cardinality columns. transform_sparse and inverse_transform are both parallelized under the rayon feature.
OrdinalEncoder
Encode categorical features as integer codes with optional user-defined ordering.
use ;
// Auto: sorted lexicographically
let mut enc = new;
let out = enc.fit_transform?;
// Manual: custom order (categories per column)
let mut enc = new;
let out = enc.fit_transform?;
// Handle unknown categories with -1.0 sentinel
let mut enc = new
.handle_unknown;
enc.fit?;
let out = enc.transform?;
// Unknown category → -1.0; inverse_transform → empty string ""
TargetEncoder
Replace categories with the smoothed mean of the target variable. Implements TargetTransformer (requires y during fit).
use TargetEncoder;
let mut te = new; // smoothing factor
te.fit?;
let out = te.transform?;
Controlled via UnknownTarget: GlobalMean (default), NaN, or Error for unseen categories.
FrequencyEncoder
Replace categories with their frequency (count or proportion). Implements CategoricalTransformer with configurable unknown handling.
use ;
// Raw counts
let mut fe = new;
let out = fe.fit_transform?;
// Normalized proportions with error on unknown categories
let mut fe = new
.handle_unknown;
let out = fe.fit_transform?;
Imputers
SimpleImputer
Impute missing values (f64::NAN) using a column-wise strategy.
use ;
let mut imp = new; // Median, MostFrequent, or Constant(val)
let out = imp.fit_transform?;
KnnImputer
Impute missing values using k-Nearest Neighbors. Distance is computed over co-observed features only.
use ;
let mut knn = new; // or Distance
let out = knn.fit_transform?;
Polynomial Features
use PolynomialFeatures;
let mut poly = new // degree
.include_bias // include intercept column
.interaction_only; // only cross-terms
let out = poly.fit_transform?;
Selection
VarianceThreshold
Remove features with variance below a threshold.
use VarianceThreshold;
let mut vt = new?;
let out = vt.fit_transform?;
SelectKBest
Keep the k highest-scoring features according to a univariate statistical test.
use ;
let mut skb = new?; // Chi2 or MutualInformation
skb.fit_with_labels?;
let out = skb.transform?;
Decomposition
PCA
Principal Component Analysis via Jacobi eigenvalue decomposition.
use ;
let mut pca = PCAnew // Count(2) or All
.whiten;
let projected = pca.fit_transform?;
// Components: pca.components()
// Explained variance: pca.explained_variance_ratio()
// Reconstruct: pca.inverse_transform(&projected)?
TruncatedSVD
Dimensionality reduction via truncated SVD (suitable for sparse or TF-IDF data).
Does not center the data. Supports flexible component selection via SVDComponents.
use ;
// By exact count:
let mut svd = new.unwrap;
let out = svd.fit_transform?;
// By variance threshold (keeps enough components to explain 95% variance):
let mut svd = new.unwrap;
let out = svd.fit_transform?;
// Keep all components:
let mut svd = new.unwrap;
let out = svd.fit_transform?;
Linear Models
LinearRegression
Ordinary least-squares regression — the crate's first predict-capable estimator. Estimates y ≈ Xβ + b by minimising the residual sum of squares. Mirrors sklearn.linear_model.LinearRegression.
Two solvers are available:
- Cholesky (default) — solves
XᵀX β = Xᵀyvia a pure-Rust Cholesky decomposition. Fast and dependency-free; requiresXto have full column rank. - SVD — eigen-decomposition-based pseudo-inverse. Numerically stable for rank-deficient / collinear inputs, at higher cost.
use ;
use Predictor;
let mut model = new
.with_fit_intercept // default true
.with_solver; // or LinearSolver::Svd
model.fit?;
let pred = model.predict?;
// Fitted parameters
model.coef; // &[f64] — coefficients β
model.intercept; // f64 — intercept b
model.n_features_in; // usize
// R² of the prediction (mirrors estimator.score in sklearn)
let r2 = model.score?;
Ridge
L2-regularized regression. Minimises ‖Xβ − y‖² + α‖β‖². Mirrors sklearn.linear_model.Ridge.
The α penalty shrinks coefficients toward zero (reducing variance at the cost of bias) and guarantees the system matrix XᵀX + αI is positive-definite — so Ridge succeeds on rank-deficient / collinear inputs where LinearRegression would fail.
use ;
use Predictor;
let mut model = new
.with_alpha // regularization strength
.with_solver; // or RidgeSolver::Svd
model.fit?;
let pred = model.predict?;
alpha must be finite and non-negative.
Lasso
L1-regularized regression. Minimises (1/(2n))‖Xβ − y‖² + α‖β‖₁. Mirrors sklearn.linear_model.Lasso.
The L1 penalty drives irrelevant coefficients to exactly zero, producing a sparse model that performs implicit feature selection — the key difference from Ridge. Solved by coordinate descent with soft-thresholding.
use Lasso;
use Predictor;
let mut model = new
.with_alpha // larger alpha → more sparsity
.with_max_iter // default 1000
.with_tol; // convergence tolerance
model.fit?;
let pred = model.predict?;
model.coef; // some entries may be exactly 0.0 (sparsity)
model.n_iter; // iterations actually run
For Lasso and logistic regression, max_iter must be positive and tol must
be finite and non-negative. Invalid solver configurations return
InvalidConfig before optimization begins.
LogisticRegression
Logistic regression for binary and multiclass classification. Mirrors sklearn.linear_model.LogisticRegression.
- Binary targets are fit via IRLS (Iteratively Reweighted Least Squares / Newton-Raphson on the logistic loss).
- Multiclass targets are fit via multinomial (softmax) logistic regression with Newton-Raphson on the cross-entropy loss. The last class is the reference.
- Labels can be any non-negative integers (for example
{2, 5, 9}); predictions retain those original values.
fit auto-detects binary vs multiclass and dispatches accordingly.
use ;
use Predictor;
let mut model = new
.with_solver // or LogisticSolver::Svd
.with_max_iter // default 100
.with_tol; // convergence tolerance
// Binary: arbitrary non-negative integer labels are accepted
model.fit?;
let classes = model.predict?; // Vec<f64> of class labels
let probabilities = model.predict_proba?; // binary: (n,2), multiclass: (n,k)
// Probability column i corresponds to model.classes()[i].
let selected_probability = model.predict_proba_for_class?;
// Multiclass: y can be {2, 5, 9}
model.fit?;
model.classes; // &[2.0, 5.0, 9.0] — sorted unique original labels
model.coef; // &[Vec<f64>] — one row per class (k-1 for multiclass)
model.intercept; // &[f64] — one per class
predict_positive_proba is binary-only and returns the probability of the second sorted class. Use predict_proba_for_class when the label should be explicit, or predict_proba for the full matrix.
Clustering
KMeans
k-means clustering via Lloyd's algorithm with k-means++ initialization, mirroring sklearn.cluster.KMeans. Minimizes within-cluster sum of squares; n_init restarts are run and the lowest-inertia result is kept.
use ;
use Clusterer;
use Matrix;
let x = new?;
let mut km = new
.with_n_clusters
.with_init // or Random
.with_n_init // restarts, keep best inertia
.with_max_iter
.with_tol
.with_random_state; // deterministic
let labels = km.fit_predict?; // Vec<usize>, one cluster index 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; // usize, Lloyd's iterations of best run
let new_labels = km.predict?; // assign new points to nearest centroid
Builder methods: with_n_clusters (default 8), with_init (default KMeansPlusPlus), with_max_iter (300), with_tol (1e-4), with_n_init (10), with_random_state (deterministic seed). Serde-serializable under the serde feature.
Solver configuration is validated before fitting: cluster, iteration, and
restart counts must be positive, while tol must be finite and non-negative.
Metrics
Regression metrics mirroring sklearn.metrics. Each takes y_true and y_pred as &[f64].
use *;
let mse = mean_squared_error?; // squared=true → MSE
let rmse = mean_squared_error?; // squared=false → RMSE
let mae = mean_absolute_error?;
let r2 = r2_score?;
let me = max_error?;
let ev = explained_variance_score?;
Classification metrics accept arbitrary non-negative integer labels and compact
them internally. The compatibility precision/recall/F1 helpers use macro averaging;
the *_with variants support binary, macro, weighted, and micro averaging:
use *;
let acc = accuracy_score?;
let prec = precision_score?; // macro-average for multiclass
let rec = recall_score?;
let f1 = f1_score?;
let cm = confusion_matrix?; // compact Vec<Vec<usize>>, n×n
let labeled = confusion_matrix_labeled?; // includes labels
let weighted_f1 = f1_score_with?;
let per_class = classification_report?;
let ll = log_loss?; // {0, 1}; P(label = 1)
// Ranking metrics for {0, 1}; scores refer to label 1:
let auc = roc_auc_score?; // ROC-AUC (Mann–Whitney U)
let ap = average_precision_score?; // average precision
// For another binary label space, name the positive class explicitly:
let ll_custom = log_loss_with_positive_label?;
let auc_custom = roc_auc_score_with_positive_label?;
let ap_custom = average_precision_score_with_positive_label?;
// Agreement & correlation metrics:
let kap = cohen_kappa_score?; // chance-corrected agreement
let mcc = matthews_corrcoef?; // MCC (binary + multiclass)
Clustering evaluation (no ground truth):
use silhouette_score;
let s = silhouette_score?; // f64 in [-1, 1], higher is better
Cluster IDs are compacted internally, so gapped values—including
usize::MAX—do not determine allocation size. The metric requires at least two
clusters and fewer clusters than samples; singleton-cluster samples contribute
zero, matching scikit-learn.
Model Selection
Train/test splitting and cross-validation, mirroring sklearn.model_selection.
train_test_split
use ;
// Quick split with defaults (25% test, shuffled):
let = train_test_split?;
// Or configure via the builder:
let = new
.with_test_size
.with_shuffle
.with_random_state
.split?;
At least two samples are required. A fractional test_size is rounded up, so
test_size = 0.25 selects two test rows from a five-row dataset.
KFold and StratifiedKFold
use ;
// K-fold: each sample is in the test set exactly once.
let cv = new.with_n_splits.with_shuffle.with_random_state;
for in cv.split?
// Stratified: approximately preserves class balance in each fold (pass y).
// Labels may be binary or multiclass non-negative integers, e.g. {2, 5, 9}.
let scv = new.with_n_splits;
for in scv.split?
cross_val_score
Evaluate any Predictor + Clone estimator with a user-supplied scorer:
use ;
use LinearRegression;
use r2_score;
let cv = new.with_n_splits;
let scores = cross_val_score?;
// scores.len() == 5; one R² per fold.
For classification, pass accuracy_score from metrics::classification instead.
Datasets
Classic toy datasets compiled as const arrays — no file I/O, no network. Enable with the datasets feature.
use datasets;
// Iris: 150 samples, 4 features, 3 classes
let 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", ...]
// Breast Cancer: 569 samples, 30 features, binary
let cancer = load;
// Wine: 178 samples, 13 features, 3 classes
let wine = load;
// Diabetes: 442 samples, 10 features, regression target
let diabetes = load;
Each loader returns a Dataset struct with features() → Matrix, targets() → &[f64], feature_names() → &[&str], target_names() → &[&str].
Pipeline
Chain multiple transformers sequentially. Fits and transforms each step on the output of the previous one. Serializable under the serde feature.
use Pipeline;
use TransformerKind;
let mut pipe = new
.push
.push
.push;
let out = pipe.fit_transform?;
// Inspect step names
assert_eq!;
All 17 transformer types are available as TransformerKind variants, enabling type-safe heterogeneous pipelines. For model training, attach a final estimator with with_estimator; supervised feature selectors receive y only from the training data:
use LogisticRegression;
use Pipeline;
use ;
use Predictor;
use TransformerKind;
let mut model = new
.push
.with_estimator;
model.fit?;
let classes = model.predict?;
ColumnTransformer
Apply different transformers to different columns of a mixed numeric/categorical dataset. Returns a combined numeric matrix or an Output preserving the numeric/categorical split.
use ;
use OneHotEncoder;
use StandardScaler;
use TransformerKind;
let table = new?;
let mut ct = new
.remainder // retain unselected columns
.add_numeric
.add_categorical;
let out = ct.fit_transform?;
// Preserve the numeric/categorical split
let output: Output = ct.fit_transform_to_table?;
// output.numeric → Matrix, output.categorical → StrMatrix
// Target specs require fit_with_target
let mut ct = new
.add_target;
ct.fit_with_target?; // fit() would error — use fit_with_target()
// Feature names compose from all sub-transformers
let names = ct.feature_names_out;
assert_eq!;
Output
The Output struct returned by transform_to_table preserves numeric and categorical columns in separate matrices. Validates row-count consistency at construction:
let output = new?;
assert_eq!;
Feature Names
All output-producing transformers implement the FeatureNames trait:
let scaler = new;
// (assuming fitted)
let names = scaler.feature_names_out;
assert_eq!;
let names = scaler.feature_names_out;
assert_eq!;
Pipeline chains names through all steps; OneHotEncoder appends _category suffixes; PCA/TruncatedSVD generate pca0/svd0 names; VarianceThreshold and SelectKBest filter names by the selected mask; ColumnTransformer composes names from all sub-transformers.
Inverse Transform
Several transformers support reversing the transformation via inverse_transform, returning an approximation of the original input:
| Transformer | Trait | Notes |
|---|---|---|
| StandardScaler | Transformer |
x = z * std + mean |
| MinMaxScaler | Transformer |
x = z * (max - min) + min |
| RobustScaler | Transformer |
x = z * iqr + median |
| MaxAbsScaler | Transformer |
x = z * max_abs |
| PowerTransformer | Transformer |
x = inverse_power(z), un-standardizes first |
| PCA | Transformer |
via components_ matrix multiply |
| TruncatedSVD | Transformer |
via components_ matrix multiply |
| OneHotEncoder | CategoricalTransformer |
Matrix → StrMatrix (dense + sparse via inverse_transform_sparse) |
| OrdinalEncoder | CategoricalTransformer |
-1.0 sentinel → empty string |
| LabelEncoder | LabelTransformer |
usize::MAX sentinel → empty string |
let mut s = new;
let transformed = s.fit_transform?;
let reconstructed = s.inverse_transform?;
// reconstructed ≈ x (within floating-point precision)
// Categorical inverse_transform via trait
let mut ohe = new;
let encoded = ohe.fit_transform?;
let decoded: StrMatrix = ohe.inverse_transform?;
// Label inverse via LabelTransformer
let mut le = new;
let indices = le.fit_transform?;
let back: = le.inverse_transform?;
Transformers that do not support inverse return an error (e.g. Binarizer, Normalizer, FrequencyEncoder, TargetEncoder).
FunctionTransformer
Wrap arbitrary functions as a Transformer, mirroring sklearn.preprocessing.FunctionTransformer.
use FunctionTransformer;
let mut ft = new;
let out = ft.fit_transform?;
// out[i][j] = x[i][j] * 2
An inverse function can be set via .with_inverse(func). At deserialization (serde feature), function pointers are skipped — call set_func() to restore.
Pipeline Ergonomics
Pipeline provides runtime access to individual steps without consuming or destructuring the pipeline:
| Method | Description |
|---|---|
get_step(name) |
Borrow a step by name |
get_step_mut(name) |
Mutably borrow a step by name |
step(index) |
Borrow a step and its name by index |
step_mut(index) |
Mutably borrow a step and its name by index |
remove_step(index) |
Remove and return a step |
insert_step(index, name, t) |
Insert a step at a position |
set_step(name, t) |
Replace a step by name |
let mut pipe = new
.push
.push;
// Replace the scaler
pipe.set_step;
// Access the PCA step's explained variance
if let PCA = pipe.get_step.unwrap
Matrix Slicing
Matrix supports column and row slicing with bounds checking:
let m = new?;
let cols = m.select_columns?; // columns 0 and 2
assert_eq!;
assert_eq!;
let rows = m.select_rows?; // only row 1
assert_eq!;
1-D Statistics
The stats module also has single-slice (1-D) counterparts of its column statistics, so a flat &[f64] doesn't need to be wrapped in a Vec<Vec<f64>> matrix:
use stats;
let x = ;
mean; // 2.5
sum; // 10.0
min; // 1.0
max; // 4.0
variance; // ~1.667 (sample, ddof=1)
std; // ~1.118 (population)
median; // Some(2.0) — sorts a copy
mode; // Some(2.0) — ties → smallest
mean/variance/std return NaN on an empty slice or when ddof >= n (numpy parity); median/mode return None on empty input.
Covariance & Correlation
The stats module provides matrix-level statistical operations:
use ;
let data = new?;
let cov = covariance_matrix; // ddof=0 (population)
let corr = correlation_matrix; // Pearson (ddof=1)
PCA also exposes noise_variance() — the average eigenvalue of discarded components, matching sklearn's PCA.noise_variance_.
Serialization
Enable the serde feature for JSON save/load of fitted transformers.
= { = "0.6", = ["serde"] }
use ;
use StandardScaler;
// String round-trip
let mut scaler = new;
scaler.fit?;
let json = to_json?;
let restored: StandardScaler = from_json?;
// File round-trip
save_json?;
let reloaded: StandardScaler = load_json?;
All leaf transformers, Pipeline (via TransformerKind), and ColumnTransformer are serializable.
Loaded fitted state is checked before transform or prediction. A syntactically
valid but internally inconsistent JSON document returns DatarustError rather
than causing an indexing panic.
Parallelism
Enable the rayon feature for parallel column operations on large datasets.
= { = "0.6", = ["rayon"] }
When enabled, the following use parallel iterators:
- Statistics:
column_mean,column_variance,column_min,column_max,column_median,column_mode,column_quantile(columnar) plus 1-Dmean,sum,min,max,variance,std,median,mode - Scalers: StandardScaler, MinMaxScaler, MaxAbsScaler, RobustScaler, Normalizer
- Encoders: OneHotEncoder (dense + sparse transform, inverse_transform), OrdinalEncoder (transform), FrequencyEncoder (transform), TargetEncoder (transform)
- Imputation: KNN Imputer distance computation
Feature Comparison: datarust vs sklearn
| Transformer | datarust | sklearn |
|---|---|---|
| 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) | ✓ |
| LabelEncoder | ✓ (handle_unknown: Error/Ignore) | ✓ |
| OrdinalEncoder | ✓ (auto + manual) | ✓ |
| OneHotEncoder | ✓ (drop, handle_unknown, sparse CSR) | ✓ |
| TargetEncoder | ✓ (smoothed mean, UnknownTarget: GlobalMean/NaN/Error) | ✓ |
| FrequencyEncoder | ✓ (count/proportion, UnknownFrequency: Zero/Error) | — |
| SimpleImputer | ✓ (mean/median/most_frequent/constant) | ✓ |
| KNN Imputer | ✓ (uniform/distance) | ✓ |
| PolynomialFeatures | ✓ (degree, interaction_only, bias) | ✓ |
| VarianceThreshold | ✓ | ✓ |
| SelectKBest | ✓ (F-classif / Chi2 / Mutual Info) | ✓ |
| PCA | ✓ (Jacobi EV + power-iteration deflation + randomized SVD, count/variance/all, whiten, PCASolver) |
✓ |
| TruncatedSVD | ✓ (SVDComponents: Count/Variance/All) | ✓ |
| KMeans | ✓ (Lloyd's algorithm, k-means++ init, n_init restarts, serde) | ✓ |
| Pipeline | ✓ (TransformerKind, serde) | ✓ |
| ColumnTransformer | ✓ (Numeric + Categorical + Target specs, Output table, duplicate detection, remainder passthrough) | ✓ |
| FunctionTransformer | ✓ (optional inverse, closure-based) | ✓ |
| FeatureNames | ✓ (trait, all transformers, short-input padding) | ✓ |
| inverse_transform | ✓ (scalers, PowerTransformer, PCA, SVD, OneHotEncoder, OrdinalEncoder, LabelEncoder) | ✓ |
| Pipeline Ergonomics | ✓ (get_step, step, set_step, insert, remove) | — |
| Matrix Slicing | ✓ (select_columns, select_rows) | — |
| Covariance / Correlation | ✓ (ddof-configurable) | — |
| ROC-AUC / PR-AUC | ✓ (roc_auc_score, average_precision_score) | ✓ |
| Cohen's Kappa / Matthews Corrcoef | ✓ (binary + multiclass) | ✓ |
| Multiclass Confusion Matrix | ✓ (n×n Vec, macro-averaged P/R/F1) | ✓ |
| Silhouette Score | ✓ (cluster::metrics) | ✓ |
| Params Trait (hyperparameter introspection) | ✓ (get_params / set_params) | — |
| Embedded Toy Datasets | ✓ (Iris, Cancer, Wine, Diabetes) | ✓ |
| JSON Serialization | ✓ (serde feature) | — (joblib) |
| Sparse Output | ✓ (CSR via SparseMatrix) | ✓ |
| Parallelism | ✓ (rayon feature) | — (joblib) |
Comparison with the Rust ML ecosystem
datarust is a preprocessing-first classical ML library. Its direct peers in the Rust ecosystem are smartcore (a single-crate algorithm library) and linfa (a modular framework of per-algorithm crates). Deep-learning stacks (candle, burn, tch-rs) target a different problem and are out of scope here.
The table below compares what is verified present as of 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 is stale.
Preprocessing & Encoders
| Component | datarust | smartcore | linfa |
|---|---|---|---|
| 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
| Component | datarust | smartcore | linfa |
|---|---|---|---|
| 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
| Feature | datarust | smartcore | linfa |
|---|---|---|---|
| Pipeline | ✓ | ? | ? |
| ColumnTransformer | ✓ | ? | ✗ |
| train_test_split | ✓ | ✓ | ? |
| KFold / StratifiedKFold | ✓ | ✓ | ? |
| cross_val_score | ✓ | ✓ | ? |
| Regression + Classification metrics | ✓ | ✓ | ✓ |
| JSON model serialization | ✓ (serde) | ? | ? |
| Zero external deps by default | ✓ | ✗ (ndarray + BLAS) | ✗ (ndarray + BLAS) |
| WASM-friendly (no native BLAS) | ✓ | ? | ? |
| Distribution model | single crate | single crate | per-algorithm crates |
Where each library shines
- datarust — the deepest sklearn-style preprocessing coverage in Rust
(18 transformers/encoders/imputers/selectors vs ≤5 elsewhere), a type-safe
Pipeline+ColumnTransformer, KMeans clustering, and a zero-dependency default build that compiles to WASM/embedded with no BLAS or LAPACK. Trade-off: only four linear models and KMeans — no SVM, trees, or DBSCAN yet (see the Roadmap). - smartcore — the broadest single-crate algorithm zoo (SVM, RandomForest,
DecisionTree, KMeans, DBSCAN, KNN, NaiveBayes…) with model selection and
metrics. Trade-off: thin preprocessing (StandardScaler + OneHotEncoder only)
and a mandatory
ndarray+ BLAS dependency. - linfa — a modular ecosystem of per-algorithm crates, strong on algorithms and unique in offering text vectorizers (Count/TF-IDF). Trade-off: categorical encoders, imputers, and feature selection are sparse or undocumented; Ridge/Lasso only exist through ElasticNet.
Complementary, not exclusive. datarust's preprocessing pipeline can feed features into a linfa or smartcore estimator, and vice-versa — pick the best tool for each stage of your workflow.
Performance: datarust 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. The
benchmark harness lives in examples/bench_compare_rust.rs and
benches/compare_sklearn.py — re-run them on your own hardware.
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.
Two datarust columns are shown: the default (zero-dependency) build, and the
build with the rayon feature enabled (parallel column/row processing). PCA additionally
benefits from the matrixmultiply feature, shown in the notes below the table.
| Workload | Size (rows × cols) | datarust default (ms) | datarust +rayon (ms) | sklearn (ms) | best ratio |
|---|---|---|---|---|---|
| StandardScaler | 1 000 × 10 | 0.031 | 0.030 | 0.270 | 8.9× |
| StandardScaler | 10 000 × 100 | 1.69 | 1.69 | 2.39 | 1.4× |
| StandardScaler | 50 000 × 200 | 14.2 | 10.4 | 21.5 | 2.1× |
| MinMaxScaler | 1 000 × 10 | 0.033 | 0.035 | 0.199 | 5.9× |
| MinMaxScaler | 10 000 × 100 | 1.75 | 1.88 | 1.32 | 0.8× |
| MinMaxScaler | 50 000 × 200 | 17.7 | 13.4 | 11.4 | 0.8× |
| RobustScaler | 1 000 × 10 | 0.11 | 0.18 | 0.722 | 6.3× |
| RobustScaler | 10 000 × 100 | 6.05 | 1.90 | 21.4 | 11× |
| RobustScaler | 50 000 × 200 | 68.7 | 14.7 | 193.8 | 13× |
| PCA (k = min(10, cols/2)) | 1 000 × 10 | 0.11 | 0.12 | 0.220 | 2.0× |
| PCA | 10 000 × 100 | 14.0 | 14.1 | 1.35 | 0.10× |
| PCA | 50 000 × 200 | 206 | 205 | 12.0 | 0.06× |
| Pipeline (Standard→MinMax→Robust) | 1 000 × 10 | 0.16 | 0.27 | 0.921 | 5.7× |
| Pipeline | 10 000 × 100 | 9.57 | 5.04 | 28.0 | 5.6× |
| Pipeline | 50 000 × 200 | 101.5 | 39.9 | 227.5 | 5.7× |
| OneHotEncoder (string) | 1 000 × 5 | 0.21 | 0.40 | 0.780 | 3.8× |
| OneHotEncoder | 10 000 × 10 | 4.25 | 3.57 | 10.2 | 2.9× |
| OneHotEncoder | 50 000 × 20 | 54.7 | 45.0 | 179.8 | 4.0× |
| ColumnTransformer (num + cat) | 1 000 × 5 | 0.029 | 0.031 | 4.41 | 153× |
| ColumnTransformer | 10 000 × 10 | 0.31 | 0.31 | 77.9 | 255× |
| ColumnTransformer | 50 000 × 20 | 2.10 | 2.11 | 796.7 | 380× |
| LinearRegression (fit+predict) | 1 000 × 10 | 0.12 | 0.12 | 0.314 | 2.6× |
| LinearRegression | 10 000 × 100 | 15.1 | 15.1 | 15.1 | 1.0× |
| LinearRegression | 50 000 × 200 | 263 | 264 | 118 | 0.45× |
PCA with the matrixmultiply feature. The default and rayon builds compute the
covariance Xcᵀ Xc with a scalar loop; enabling the optional matrixmultiply feature
dispatches the covariance and the transform/inverse matmuls to a tuned pure-Rust GEMM
(no system BLAS), and a power-iteration + deflation path (eigh_topk) replaces the full
Jacobi sweep when n_components is small. On 50 000 × 200 this cuts PCA from
206 ms → 111 ms (1.9× faster), and on 10 000 × 100 from 14.0 ms → 9.6 ms (1.5×).
PCA remains slower than scikit-learn (which uses LAPACK's full SVD) — see "Where
scikit-learn wins" below — but the gap narrowed from ~17× to ~9×.
Randomized SVD (opt-in). PCA::solver(PCASolver::Randomized) selects the
Halko–Martinsson–Tropp randomized SVD, which is O(n·p·(k+oversample)) instead of
O(p³·sweeps) and is the fast path for tall-and-wide, low-rank data (this is what
sklearn's svd_solver='randomized' does). It is currently opt-in while an oversampling
edge case is being verified; Auto (the default) uses the exact eigensolver paths.
LinearRegression with the matrixmultiply feature. fit forms the normal-equation
matrices XᵀX (p×p) and Xᵀy (p) via Matrix::matmul, then solves them with a pure-Rust
Cholesky decomposition. Enabling matrixmultiply dispatches the matmul to a tuned GEMM,
cutting fit+predict from 263 ms → 91 ms at 50 000 × 200 (2.9× faster) and
15.1 ms → 5.5 ms at 10 000 × 100 (2.7× faster). With the feature on, datarust's
fit+predict at 50 000 × 200 (91 ms) edges out scikit-learn's 118 ms — the first row
where datarust leads on a matmul-heavy workload. Reproduce with
cargo run --release --features matrixmultiply --example bench_compare_rust
and python3 benches/compare_sklearn.py.
Scalar vs matrixmultiply kernels
The kernel-level table below isolates what the optional matrixmultiply feature buys
inside the numeric hot paths — medians from the criterion suite
(cargo bench -p datarust --bench benchmarks -- --warm-up-time 0.5 --measurement-time 1.5 --sample-size 10),
single-threaded release build on the same Apple M5 Pro hardware as above. +GEMM is the
same build with features = ["matrixmultiply"]; Speedup is scalar / +GEMM. Each time
carries its own unit (ms / µs / ns).
| Benchmark | Size | scalar | +GEMM | Speedup |
|---|---|---|---|---|
matrix_matmul |
100 × 100 | 103 µs | 37.0 µs | 2.8× |
matrix_matmul |
50 × 50 | 14.3 µs | 5.32 µs | 2.7× |
matrix_matmul |
10 × 10 | 272 ns | 151 ns | 1.8× |
correlation_matrix_flat (Pearson) |
10 000 × 100 | 5.97 ms | 3.94 ms | 1.5× |
correlation_matrix_flat |
10 000 × 50 | 1.96 ms | 1.22 ms | 1.6× |
correlation_matrix_flat |
100 000 × 20 | 5.33 ms | 2.84 ms | 1.9× |
linear_regression fit |
100 000 × 100 | 147 ms | 53.2 ms | 2.7× |
linear_regression fit |
10 000 × 50 | 4.35 ms | 1.69 ms | 2.6× |
linear_regression fit |
1 000 × 10 | 72.4 µs | 26.3 µs | 2.7× |
linear_regression predict |
any | ≈ | ≈ | ~1.0× |
truncated_svd |
1000 × 50 → 10 comps | 1.76 ms | 886 µs | 2.0× |
truncated_svd |
500 × 30 → 5 comps | 355 µs | 175 µs | 2.0× |
pca |
200 × 20 → 5 comps | 108 µs | 91.5 µs | 1.2× |
pca |
50 × 10 → 3 comps | 21.0 µs | 19.3 µs | 1.1× |
Where the GEMM pays off. The classic matmul kernels win most: Matrix::matmul
1.8–2.8×, LinearRegression fit 2.6–2.7×, TruncatedSVD ~2.0×. Predict
paths are memory-bound matvecs and barely move (~1.0×). Pearson no longer needs GEMM for
wide tables — the lower-triangle scalar covariance puts the kernel at 6.0 ms for
10 000 × 100, with GEMM adding ~1.5× on top; the sibling datarust-profile crate shows
the same effect: wide profile_matrix (10 000 × 100) is 30.2 ms scalar vs 28.6 ms
+GEMM, because Pearson is no longer its dominant cost. PCA barely moves at these small
sizes (GEMM overhead dominates a 20 × 20 covariance) and shines on larger matrices — see
the 50 000 × 200 PCA note above.
Criterion microbenchmark coverage
The criterion suite (cargo bench -p datarust --bench benchmarks) covers the whole
API surface. The medians below are from the default (zero-dependency) build with
--warm-up-time 0.5 --measurement-time 1.5 --sample-size 10 on the same Apple M5 Pro
and document the operations added most recently. Each time carries its own unit.
| Benchmark | Size | median |
|---|---|---|
label_encoder fit_transform |
10 000 × 10 | 558 µs |
label_encoder fit_transform |
100 000 × 10 | 5.67 ms |
label_encoder fit_transform |
100 000 × 10 000 classes | 9.17 ms |
stats_nested mean_var |
10 000 × 100 | 220 µs |
stats_nested quantiles |
10 000 × 100 | 7.90 ms |
stats_nested mode_column |
10 000 × 100 | 11.1 ms |
stats_nested mean_var |
100 000 × 20 | 705 µs |
stats_nested quantiles |
100 000 × 20 | 16.2 ms |
stats_nested mode_column |
100 000 × 20 | 27.7 ms |
train_test_split |
10 000 × 50 | 116 µs |
train_test_split |
100 000 × 20 | 784 µs |
matrix_ops from_flat |
10 000 × 50 | 49.0 µs |
matrix_ops from_flat |
100 000 × 20 | 216 µs |
stats_nested mirrors the flat-storage kernels over Vec<Vec<f64>> inputs;
mode_column shares mode's sort-then-scan, so a 100 000 × 20 mostly-distinct table
modes in 27.7 ms. matrix_ops/from_flat (49 µs at 10 000 × 50) shows why flat
construction is the fast path over nested from_nested (~289 µs).
The next pass added eight groups covering the production-time paths that
fit_transform-only benchmarks hid: transform/inverse_transform on fitted
transformers, predict_proba, fitted-encoder transforms, sparse one-hot, the
remaining classification metrics, the splitters, and string-column gathering.
| Benchmark | Size | median |
|---|---|---|
metrics_more confusion_matrix |
100 000 | 1.25 ms |
metrics_more precision/recall/F1/kappa/MCC |
100 000 | ~1.24 ms |
metrics_more log_loss |
100 000 | 714 µs |
metrics_more average_precision_score |
100 000 | 1.70 ms |
logistic_predict_proba predict_proba_binary |
50 000 × 100 | 6.14 ms |
logistic_predict_proba predict_proba_multiclass |
10 000 × 50 | 715 µs |
encoder_transform onehot_transform |
50 000 × 20 | 14.3 ms |
encoder_transform ordinal/frequency/target transform |
50 000 × 20 | ~9.3 ms |
polynomial_transform transform |
10 000 × 5, d3 | 876 µs |
scaler_transform standard/minmax/robust transform |
100 000 × 20 | ~1.72 ms |
scaler_transform standard/minmax/robust inverse |
100 000 × 20 | ~1.45 ms |
scaler_transform quantile_transform |
100 000 × 20 | 14.6 ms |
onehot_sparse transform_sparse |
50 000 × 20 | 18.6 ms |
onehot_sparse fit_transform_sparse |
50 000 × 20 | 32.9 ms |
model_selection kfold_5_shuffled |
100 000 | 210 µs |
model_selection stratified_kfold_5 |
100 000 | 1.86 ms |
strmatrix_column column_clone |
100 000 × 20 | 72.0 ms |
strmatrix_column column_refs |
100 000 × 20 | 8.12 ms |
Three rows are the current bottlenecks. QuantileTransformer.transform is
still the per-value outlier — ~7.3 ns per value (14.6 ms at 100 000 × 20,
down from 23.4 ms) against ~0.85 ns for the linear scalers: each value maps
into one of 512 pre-partitioned value spans over the 1 000-point reference and
interpolates inside that span's handful of entries — O(1) plus a few compares
instead of a full log₂(1000) binary search, with bit-identical output. The
remaining cost is the per-value interpolation itself (the nested-Vec transpose
round-trips are gone; the transform now streams the flat buffer row-major).
StrMatrix::column (clone) is ~8× slower than the borrowing column_refs
(72.0 ms vs 8.12 ms at 100 000 × 20) — all four categorical encoders used to
pay this clone tax in their fit paths, and switching them to column_refs made
fit_transform 60–83% faster (ordinal/frequency fit at 10 000 × 20: ~21 ms
→ ~3.6–3.9 ms, with transform unchanged). StratifiedKFold.split used to
rebuild each fold's train set with a per-fold HashSet scan of every sample; a
reusable boolean mask (mark → scan → reset) cut it from 6.31 ms to 1.86 ms
at 100 000 rows (−71%), leaving the stratification setup itself as the
remaining cost. onehot_sparse
transform_sparse was 2× slower than the dense transform because it
round-tripped through per-row triplet Vecs and SparseMatrix::from_triplets'
per-row re-sort; it now builds the CSR arrays directly — 18.6 ms vs 14.3 ms
dense at 50 000 × 20 (down from 27.6 ms), and fit_transform_sparse dropped
from 78.3 ms to 32.9 ms.
For the sibling datarust-profile crate, the criterion suite adds
cramers_v_high_cardinality, point_biserial, profile_str_high_cardinality, and
serde-gated report_json groups — the mdBook performance page lists their medians.
Reading the results
Where datarust wins decisively:
- Mixed numeric + categorical composition.
ColumnTransformeris 153–380× faster than scikit-learn's on large inputs. This is the headline result and reflects the cost of sklearn's per-column Python dispatch, dtype coercion, andColumnTransformer's object-array marshalling on mixed-type inputs. - String / categorical encoding.
OneHotEncoderis ~2.9–4.0× faster because datarust operates on a nativeStrMatrixdirectly — no Python object-array overhead, no GIL. - Numeric scalers with
rayon. Once the data is large enough to amortise thread spawn,StandardScaler/RobustScaler/Pipelineall beat sklearn by 2.1–13× at 50 000 × 200. The single-pass Welford statistics and contiguous flat storage close the gap that numpy's vectorised kernels used to dominate. - Small data and startup latency. At 1 000 × 10, datarust is faster on every
workload — up to 8.9× on
StandardScalerand 153× onColumnTransformer(the rayon path now falls back to the scalar loop below 4 096 rows, avoiding thread-pool overhead on tiny inputs). There is no Python interpreter to spin up and no joblib/numpy import cost — relevant for embedded, batch-on-many-small-files, or request-scoped inference paths.
Where scikit-learn still wins:
- PCA on tall-and-wide data (without the
matrixmultiplyfeature). sklearn'sPCAis still faster when comparing default builds (0.06× at 50 000 × 200). It calls into LAPACK's full SVD via a shared-library BLAS; datarust implements the covariance eigendecomposition with a from-scratch Jacobi sweep. With thematrixmultiplyfeature the gap narrows from ~17× to ~9×, andPCA::solver(PCASolver::Randomized)(randomized SVD, the same algorithm sklearn'ssvd_solver='randomized'uses) closes it further for low-rank inputs. For PCA on large dense matrices as the hot path, sklearn remains the fastest option today. - MinMaxScaler at medium width. At 10 000 × 100 the two are roughly tied (0.8×); numpy's contiguous buffer and autovectorisation win narrowly on this particular shape. At both smaller and larger sizes datarust leads.
The honest one-line summary: for the workloads Rust ML pipelines typically care about
— heterogeneous ColumnTransformer composition, categorical encoding, numeric scaling on
medium-to-large data, and latency-sensitive preprocessing — datarust is now the faster
choice; the remaining gap is dense eigendecomposition (PCA/SVD) at scale, where a
dedicated BLAS/LAPACK backend still wins.
How the speedups were achieved (0.3.0)
Layered optimisations, each measurable:
- Single-pass fused statistics.
StandardScaler/MinMaxScalerpreviously made 2–3 full passes over the data (mean, then variance which re-read for mean, then the variance sweep). A Welford accumulator now computes mean+variance in one row-major pass; min+max are fused similarly;RobustScalersorts each column once instead of three times. - Contiguous flat storage.
Matrixis now a singleVec<f64>(+ rows, cols) instead ofVec<Vec<f64>>(one heap allocation per row). This unlocks stride-1 cache lines and auto-vectorisation across every numeric loop — the dominant win on large dense inputs. - Optional tuned GEMM. The
matrixmultiplyfeature (off by default, preserving the zero-dependency build) routesMatrix::matmul, centered-covariance, and PCA/SVD transforms through a micro-optimised pure-Rust kernel. - Flat Jacobi eigensolver + power-iteration deflation. The eigensolver behind PCA
and TruncatedSVD now operates on a single contiguous buffer (better cache locality)
and, when
n_componentsis small, a power-iteration + deflation path computes only the top-keigenpairs inO(k·p²·iters)instead of the fullO(p³·sweeps)sweep. - Adaptive parallelism threshold. Scaler
transformpaths now use the scalar loop below 4 096 rows and therayonparallel path above it — fixing a regression whererayon's thread-pool overhead made small-data transforms slower than the default build. - Randomized SVD (opt-in).
PCA::solver(PCASolver::Randomized)selects a Halko–Martinsson–Tropp randomized SVD — the same family of algorithm sklearn uses for itssvd_solver='randomized'. It isO(n·p·(k+oversample))and is the fast path for tall-and-wide, low-rank inputs.
See the [0.3.0] entry in CHANGELOG.md for the per-workload before/after numbers.
Non-performance advantages over the Python stack
Beyond raw throughput, datarust provides properties scikit-learn cannot offer:
- Zero external dependencies by default — no numpy/BLAS/LAPACK/scipy install, no
shared-library ABI concerns.
cargo add datarustand you have a working preprocessor. - No Python runtime, no GIL — embeddable in any Rust binary, WASM, or service.
- Compile-time type safety — categorical (
StrMatrix) vs numeric (Matrix) inputs are enforced by the type system, not discovered at runtime. - Single static binary — deployable preprocessing with no environment drift.
- Typed
Result<T, DatarustError>— no exceptions during inference; the public API is panic-free. - JSON serde round-trips — fitted transformers serialize to portable JSON, not joblib's Python-specific pickle.
Complete Examples
Preprocessing workflow with Pipeline + ColumnTransformer
use ;
use OneHotEncoder;
use Pipeline;
use StandardScaler;
use TransformerKind;
use Matrix;
// Numeric features: age, salary, bonus
let numeric = new?;
// Categorical features: city, department
let categorical = from_strings?;
let table = new?;
// Mixed-type transformation
let mut ct = new
.remainder
.add_numeric
.add_categorical;
let transformed = ct.fit_transform?;
// Feature names
let names = ct.feature_names_out;
assert_eq!;
PCA for dimensionality reduction
use ;
let x = new?;
// Keep 2 components
let mut pca = PCAnew;
let projected = pca.fit_transform?;
assert_eq!;
// Reconstruct (approximate)
let reconstructed = pca.inverse_transform?;
// Explained variance
let ratio: = pca.explained_variance_ratio.to_vec;
println!;
Missing value imputation
use ;
let mut x = new?;
// Mean imputation
let mut imp = new;
let filled = imp.fit_transform?;
// KNN imputation (5 neighbors, uniform weighting)
let mut knn = new;
let imputed = knn.fit_transform?;
Sparse one-hot encoding
use OneHotEncoder;
use SparseMatrix;
let s = from_column?;
let mut ohe = new;
let sp: SparseMatrix = ohe.fit_transform_sparse?;
assert_eq!; // 4 ones, rest zeros
assert_eq!;
Serialization of a fitted Pipeline
use Pipeline;
use ;
use TransformerKind;
use StandardScaler;
let mut pipe = new
.push;
pipe.fit?;
// Save to disk
save_json?;
// Load and reuse
let loaded: Pipeline = load_json?;
let out = loaded.transform?;
End-to-end: TargetEncoder + ColumnTransformer
use ;
use ;
use StandardScaler;
use TransformerKind;
let numeric = new?;
let categorical = from_strings?;
let targets = vec!;
let table = new?;
let mut ct = new
.add_numeric
.add_categorical
.add_target;
ct.fit_with_target?;
// Transform with all three spec types
let out = ct.transform?;
println!;
Feature selection + PCA + Pipeline
use ;
use ;
use StandardScaler;
use Pipeline;
use TransformerKind;
let mut pipe = new
.push
.push
.push;
pipe.fit_transform_with_labels?;
// 2-dimensional output from 50+ feature input
assert_eq!;
Inverse transform with error propagation
use ;
use ;
let x = new?;
// Forward: StandardScaler → MinMaxScaler → PCA
let mut scaler = new;
let scaled = scaler.fit_transform?;
let mut mm = new;
let normalized = mm.fit_transform?;
// Inverse: PCA → MinMaxScaler → StandardScaler
let mut pca = PCAnew;
let projected = pca.fit_transform?;
let pca_back = pca.inverse_transform?;
let mm_back = mm.inverse_transform?;
let reconstructed = scaler.inverse_transform?;
for i in 0..x.nrows
Custom transformer with FunctionTransformer
use FunctionTransformer;
let mut ft = new
.with_inverse;
let x = new?;
let log_x = ft.fit_transform?;
let back = ft.inverse_transform?;
// back ≈ x
Pipeline ergonomics: step inspection and replacement
use ;
use Pipeline;
use ;
use TransformerKind;
let mut pipe = new
.push
.push;
// Inspect step names
for name in pipe.names
// Replace the scaler with a robust alternative
pipe.set_step;
// Mutably access the PCA step to change parameters
if let PCA = pipe.get_step_mut.unwrap
// Remove and insert steps dynamically
pipe.remove_step;
pipe.insert_step;
Sparse inverse transform with OneHotEncoder
use OneHotEncoder;
use StrMatrix;
let s = from_column?;
let mut ohe = new;
// Two round-trip paths: dense → StrMatrix and sparse → StrMatrix
let dense = ohe.fit_transform?;
let from_dense = ohe.inverse_transform?;
let sparse = ohe.transform_sparse?;
let from_sparse = ohe.inverse_transform_sparse?;
for i in 0..s.nrows
QuantileTransformer with NaN rejection
use ;
use Matrix;
let x = new?;
let mut qt = new?;
let result = qt.fit_transform;
assert!; // NaN input is rejected with InvalidInput
Pipeline with feature names
use Pipeline;
use StandardScaler;
use VarianceThreshold;
use PCA;
use PCAComponents;
use TransformerKind;
use FeatureNames;
let mut pipe = new
.push
.push
.push;
pipe.fit?;
// Feature names propagate through the entire pipeline
let input_names = &;
let names = pipe.feature_names_out;
// e.g. ["pca0", "pca1", "pca2"] — depends on variance threshold + PCA
println!;
KMeans clustering
use KMeans;
use Clusterer;
use Matrix;
// Three visually distinct point clouds.
let x = new?;
let mut km = new
.with_n_clusters
.with_random_state;
let labels = km.fit_predict?; // [0,0,0,0, 1,1,1,1, 2,2,2,2] (up to permutation)
let centers = km.cluster_centers; // ≈ [[0.05,0.05], [10.05,10.05], [20.05,20.05]]
let inertia = km.inertia; // ≈ 0.04 (tight clusters)
// Assign new points to their nearest learned centroid.
let test = new?;
let predicted = km.predict?; // e.g. [0, 2]
Real World Examples
We also provide standard real-world examples in the examples/ directory:
- Iris (
cargo run --example iris --features datasets): Logistic regression on the classic Iris dataset. - Wine (
cargo run --example wine --features datasets): Logistic regression on the Wine quality dataset. - MNIST (
cargo run --example mnist): Digits classification on a synthetic MNIST-like dataset. - Housing (
cargo run --example housing): Linear regression on a synthetic California-housing-like dataset. - Titanic (
cargo run --example titanic): Survival classification using a synthetic Titanic-like dataset withColumnTransformer(scaling and one-hot encoding). - Spam (
cargo run --example spam): Email spam classification using synthetic TF-IDF text features.
Roadmap
datarust is working toward a complete scikit-learn-style ML toolkit for Rust,
with every algorithm implemented in pure Rust and zero external dependencies
by default. The path from the current preprocessing-first v0.6.5 to a
v1.0 stability release is tracked in ROADMAP.md and the
book's roadmap page.
Guiding principles (non-negotiable):
- Zero dependencies by default — every algorithm is pure Rust, no BLAS/LAPACK.
- CPU-first — GPU and deep learning are served by candle and burn; datarust owns classical ML on CPU.
- scikit-learn API parity (
fit/transform/predict) with type-safe Rust improvements where they help. - No panics — public APIs return
Result;missing_docsis enforced in CI.
Release track (summary — see ROADMAP.md for full detail
and checkboxes):
| Version | Theme | Headline deliverables |
|---|---|---|
| v0.6 ✅ shipped | Core ML foundations | Clusterer trait + KMeans, multiclass LogisticRegression (softmax), ROC-AUC / PR-AUC, Cohen's kappa, Matthews corrcoef, silhouette score, Params trait |
| v0.7 | Tree-based learning | DecisionTree, RandomForest, ExtraTrees, Bagging, feature importances |
| v0.8 | Model selection & text | GridSearchCV, CountVectorizer / TfidfVectorizer, sparse-matrix arithmetic, KNeighbors, Naive Bayes |
| v0.9 | Depth & breadth | GradientBoosting / AdaBoost, SVC (SMO), DBSCAN, ElasticNet, NMF, embedded datasets, CSV reader |
| v1.0 | Stability | API freeze, legacy-API cleanup, ARCHITECTURE.md refresh, full public-API audit, SemVer commitment |
Explicitly out of scope: GPU compute, distributed training, deep learning
(CNN/RNN/Transformer), pickle/joblib compatibility, SHAP/LIME. See
ROADMAP.md for the rationale.
Under consideration (post-1.0): f32 generics, TSNE/manifold learning,
HistGradientBoosting, ONNX export/import, NumPy .npy interop, and a
minimal MLPClassifier.