datarust
Scikit-Learn Preprocessing in Rust — a modular, dependency-free data preprocessing 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 solver, Cholesky & SVD) |
| Metrics | Regression: MSE/RMSE, MAE, R², max_error, explained_variance. Classification: accuracy, precision, recall, F1, confusion_matrix, log_loss |
| Model Selection | train_test_split, KFold, StratifiedKFold, cross_val_score |
| Pipeline | Sequential 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 |
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.3"
Optional features
[]
= { = "0.3", = ["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.
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:
Regressor Trait
Regression estimators (currently LinearRegression) implement the Regressor trait — the supervised counterpart of Transformer, with predict instead of transform:
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 | Transformer, CategoricalTransformer, TargetTransformer, LabelTransformer, FeatureNames |
| 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 Regressor;
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 Regressor;
let mut model = new
.with_alpha // regularization strength
.with_solver; // or RidgeSolver::Svd
model.fit?;
let pred = model.predict?;
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 Regressor;
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
LogisticRegression
Binary classification via IRLS (Iteratively Reweighted Least Squares). Mirrors sklearn.linear_model.LogisticRegression.
Estimates P(y = 1 | x) = σ(x·β + b) by maximising the log-likelihood via Newton-Raphson. Each iteration solves a weighted least-squares system using the shared Cholesky (default) or SVD solver. Targets must be 0.0 or 1.0.
use ;
use Regressor;
let mut model = new
.with_solver // or LogisticSolver::Svd
.with_max_iter // default 100
.with_tol; // convergence tolerance
model.fit?; // y must be 0.0 / 1.0
let probs = model.predict?; // Vec<f64> of P(y=1|x) in [0,1]
let classes = model.predict_class?; // Vec<f64> of 0.0 / 1.0 (threshold 0.5)
let acc = model.score?; // mean accuracy (f64)
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 for binary labels (0.0 / 1.0):
use *;
let acc = accuracy_score?;
let prec = precision_score?;
let rec = recall_score?;
let f1 = f1_score?;
let cm = confusion_matrix?; // [[tn, fp], [fn, tp]]
let ll = log_loss?; // cross-entropy
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?;
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: preserves class balance in each fold (pass y).
let scv = new.with_n_splits;
for in scv.split?
cross_val_score
Evaluate any Regressor + 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.
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.
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!;
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.3", = ["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.
Parallelism
Enable the rayon feature for parallel column operations on large datasets.
= { = "0.3", = ["rayon"] }
When enabled, the following use parallel iterators:
- Statistics:
column_mean,column_variance,column_min,column_max,column_median,column_mode,column_quantile - 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) | ✓ |
| 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) | — |
| JSON Serialization | ✓ (serde feature) | — (joblib) |
| Sparse Output | ✓ (CSR via SparseMatrix) | ✓ |
| Parallelism | ✓ (rayon feature) | — (joblib) |
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.023 | 0.016 | 0.280 | 17.5× |
| StandardScaler | 10 000 × 100 | 1.24 | 1.20 | 2.46 | 2.0× |
| StandardScaler | 50 000 × 200 | 8.2 | 4.7 | 22.4 | 4.8× |
| MinMaxScaler | 1 000 × 10 | 0.025 | 0.014 | 0.202 | 14.4× |
| MinMaxScaler | 10 000 × 100 | 1.70 | 1.47 | 1.32 | 0.9× |
| MinMaxScaler | 50 000 × 200 | 10.8 | 7.5 | 11.6 | 1.5× |
| RobustScaler | 1 000 × 10 | 0.17 | 0.13 | 0.768 | 5.8× |
| RobustScaler | 10 000 × 100 | 11.2 | 2.09 | 21.4 | 10× |
| RobustScaler | 50 000 × 200 | 123 | 14.0 | 193.5 | 13.8× |
| PCA (k = min(10, cols/2)) | 1 000 × 10 | 0.18 | 0.10 | 0.226 | 2.2× |
| PCA | 10 000 × 100 | 45 | 41 | 1.39 | 0.03× |
| PCA | 50 000 × 200 | 838 | 819 | 12.2 | 0.01× |
| Pipeline (Standard→MinMax→Robust) | 1 000 × 10 | 0.20 | 0.21 | 1.02 | 4.9× |
| Pipeline | 10 000 × 100 | 13.2 | 4.1 | 25.2 | 6.1× |
| Pipeline | 50 000 × 200 | 144 | 26.7 | 229.6 | 8.6× |
| OneHotEncoder (string) | 1 000 × 5 | 0.38 | 0.55 | 0.800 | 1.5× |
| OneHotEncoder | 10 000 × 10 | 7.4 | 6.8 | 9.9 | 1.5× |
| OneHotEncoder | 50 000 × 20 | 89 | 80 | 205 | 2.6× |
| ColumnTransformer (num + cat) | 1 000 × 5 | 0.026 | 0.026 | 4.6 | 179× |
| ColumnTransformer | 10 000 × 10 | 0.23 | 0.24 | 79.8 | 347× |
| ColumnTransformer | 50 000 × 20 | 1.31 | 1.32 | 812.8 | 620× |
| LinearRegression (fit+predict) | 1 000 × 10 | 0.16 | 0.16 | — | — |
| LinearRegression | 10 000 × 100 | 14.4 | 14.4 | — | — |
| LinearRegression | 50 000 × 200 | 258 | 258 | — | — |
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
838 ms → 104 ms (8× faster), and on 10 000 × 100 from 45 ms → 9.3 ms (4.8×). PCA
remains slower than scikit-learn (which uses LAPACK's full SVD) — see "Where scikit-learn
wins" below — but the gap narrowed from 85× to ~8×.
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 from 258 ms → 84 ms at 50 000 × 200 (3× faster) and 14.4 ms → 5.0 ms
at 10 000 × 100 (2.9× faster). sklearn timing is not shown here because the Python
comparison harness requires numpy/scipy in the environment; the Rust harness
(cargo run --release --features matrixmultiply --example bench_compare_rust) is
reproducible on any machine.
Reading the results
Where datarust wins decisively:
- Mixed numeric + categorical composition.
ColumnTransformeris 160–590× 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 ~1.5–2.6× 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 4.8–13.8× 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 17.5× on
StandardScaler(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.01× 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 85× to ~8×, 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.9×); 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!;