Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

Performance vs scikit-learn

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

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

Benchmark table

WorkloadSize (rows × cols)datarust default (ms)datarust +rayon (ms)sklearn (ms)best ratio
StandardScaler1 000 × 100.0310.0300.2708.9×
StandardScaler10 000 × 1001.691.692.391.4×
StandardScaler50 000 × 20014.210.421.52.1×
MinMaxScaler1 000 × 100.0330.0350.1995.9×
MinMaxScaler10 000 × 1001.751.881.320.8×
MinMaxScaler50 000 × 20017.713.411.40.8×
RobustScaler1 000 × 100.110.180.7226.3×
RobustScaler10 000 × 1006.051.9021.411×
RobustScaler50 000 × 20068.714.7193.813×
PCA (k = min(10, cols/2))1 000 × 100.110.120.2202.0×
PCA10 000 × 10014.014.11.350.10×
PCA50 000 × 20020620512.00.06×
Pipeline (Standard→MinMax→Robust)1 000 × 100.160.270.9215.7×
Pipeline10 000 × 1009.575.0428.05.6×
Pipeline50 000 × 200101.539.9227.55.7×
OneHotEncoder (string)1 000 × 50.210.400.7803.8×
OneHotEncoder10 000 × 104.253.5710.22.9×
OneHotEncoder50 000 × 2054.745.0179.84.0×
ColumnTransformer (num + cat)1 000 × 50.0290.0314.41153×
ColumnTransformer10 000 × 100.310.3177.9255×
ColumnTransformer50 000 × 202.102.11796.7380×
LinearRegression (fit+predict)1 000 × 100.120.120.3142.6×
LinearRegression10 000 × 10015.115.115.11.0×
LinearRegression50 000 × 2002632641180.45×

Feature flags make a difference

matrixmultiply. Enabling this feature dispatches covariance and matmuls to a tuned pure-Rust GEMM (no system BLAS). On 50 000 × 200, PCA drops from 206 ms → 111 ms (1.9× faster), and LinearRegression fit+predict from 263 ms → 91 ms (2.9× faster).

rayon. Parallel column/row processing. RobustScaler at 50 000 × 200 drops from 68.7 ms (default) to 14.7 ms (4.7× faster with rayon).

Scalar vs matrixmultiply kernels

The 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).

BenchmarkSizescalar+GEMMSpeedup
matrix_matmul100 × 100103 µs37.0 µs2.8×
matrix_matmul50 × 5014.3 µs5.32 µs2.7×
matrix_matmul10 × 10272 ns151 ns1.8×
correlation_matrix_flat (Pearson)10 000 × 1005.97 ms3.94 ms1.5×
correlation_matrix_flat10 000 × 501.96 ms1.22 ms1.6×
correlation_matrix_flat100 000 × 205.33 ms2.84 ms1.9×
linear_regression fit100 000 × 100147 ms53.2 ms2.7×
linear_regression fit10 000 × 504.35 ms1.69 ms2.6×
linear_regression fit1 000 × 1072.4 µs26.3 µs2.7×
linear_regression predictany~1.0×
truncated_svd1000 × 50 → 10 comps1.76 ms886 µs2.0×
truncated_svd500 × 30 → 5 comps355 µs175 µs2.0×
pca200 × 20 → 5 comps108 µs91.5 µs1.2×
pca50 × 10 → 3 comps21.0 µs19.3 µs1.1×

The GEMM pays off most on matmul-heavy kernels — Matrix::matmul 1.8–2.8×, LinearRegression fit 2.6–2.7×, TruncatedSVD ~2.0× — while 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); PCA barely moves at these small sizes and shines on larger matrices.

Where datarust wins

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

Where scikit-learn still wins

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

Criterion microbenchmark suite

Alongside the sklearn comparison, the criterion suite (cargo bench -p datarust --bench benchmarks and cargo bench -p datarust-profile --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.

Feature-build comparisons: Criterion identifies a benchmark by group and ID, not Cargo features. Use a distinct CARGO_TARGET_DIR for the default and matrixmultiply runs; otherwise its change output compares unlike builds and falsely reports the feature switch as a regression.

CARGO_TARGET_DIR=target/criterion-default cargo bench -p datarust --bench benchmarks
CARGO_TARGET_DIR=target/criterion-matrixmultiply cargo bench -p datarust \
  --bench benchmarks --features matrixmultiply

datarust

BenchmarkSizemedian
label_encoder fit_transform10 000 × 10558 µs
label_encoder fit_transform100 000 × 105.67 ms
label_encoder fit_transform100 000 × 10 000 classes9.17 ms
stats_nested mean_var10 000 × 100220 µs
stats_nested quantiles10 000 × 1007.90 ms
stats_nested mode_column10 000 × 10011.1 ms
stats_nested mean_var100 000 × 20705 µs
stats_nested quantiles100 000 × 2016.2 ms
stats_nested mode_column100 000 × 2027.7 ms
train_test_split10 000 × 50116 µs
train_test_split100 000 × 20784 µs
matrix_ops from_flat10 000 × 5049.0 µs
matrix_ops from_flat100 000 × 20216 µs

The stats_nested group 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 the nested from_nested (~289 µs for the same shape).

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:

BenchmarkSizemedian
metrics_more confusion_matrix100 0001.25 ms
metrics_more precision/recall/F1/kappa/MCC100 000~1.24 ms
metrics_more log_loss100 000714 µs
metrics_more average_precision_score100 0001.70 ms
logistic_predict_proba predict_proba_binary50 000 × 1006.14 ms
logistic_predict_proba predict_proba_multiclass10 000 × 50715 µs
encoder_transform onehot_transform50 000 × 2014.3 ms
encoder_transform ordinal/frequency/target transform50 000 × 20~9.3 ms
polynomial_transform transform10 000 × 5, d3876 µs
scaler_transform standard/minmax/robust transform100 000 × 20~1.72 ms
scaler_transform standard/minmax/robust inverse100 000 × 20~1.45 ms
scaler_transform quantile_transform100 000 × 2014.6 ms
onehot_sparse transform_sparse50 000 × 2018.6 ms
onehot_sparse fit_transform_sparse50 000 × 2032.9 ms
model_selection kfold_5_shuffled100 000210 µs
model_selection stratified_kfold_5100 0001.86 ms
strmatrix_column column_clone100 000 × 2072.0 ms
strmatrix_column column_refs100 000 × 208.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.

datarust-profile

BenchmarkSizemedian
cramers_v_high_cardinality10 000 × 10, 100 levels2.04 ms
cramers_v_high_cardinality10 000 × 10, 1 000 levels6.01 ms
point_biserial10 000 × 20 num + 20 bin12.0 ms
point_biserial50 000 × 10 num + 10 bin20.8 ms
profile_str_high_cardinality10 000 × 2082.6 ms
profile_str_high_cardinality50 000 × 10122 ms
report_json to_json10 000 × 2036.1 µs
report_json to_json100 000 × 2038.5 µs

The high-cardinality groups pin down the worst cases of wide-table profiling: near-unique identifier columns (10 000 × 20) take ~83 ms end-to-end — dominated by duplicate detection and per-column categorical tallies — and Cramér’s V over 1 000-level columns costs ~6 ms per 10-column table. report_json scales with the column count, not the row count (the profile is column-summarized), so 10 000 × 20 and 100 000 × 20 serialize in ~36–39 µs.

Reproduce the benchmarks

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

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

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