Analyse My Data

Benchmark · BioView single-cell RNA-seq

BioView in the browser vs scanpy on real PBMC data

We ran the same single-cell pipeline — quality control, highly variable genes, scaling, PCA, nearest neighbours, clustering, UMAP and marker genes — in BioView (WebGPU and CPU paths, inside a browser tab) and in scanpy 1.10.3 (Python) on the same laptop, with matched parameters, and compared timings and results. Everything below is measured; nothing is extrapolated.

Summary

11×
faster than scanpy on 10x PBMC 68k (12.0 s vs 130.8 s, including marker genes), BioView GPU path
7×
faster than scanpy on the PBMC 100k panel (8.3 s vs 61.8 s)
r = 1.0000
correlation of PCA explained-variance ratios with scanpy on all three datasets
ARI 0.964
cluster agreement with scanpy Leiden on PBMC 100k (0.825 on 68k, 0.717 on 3k)
1,000,000
cells × 32,738 genes analysed end to end in 70 s on a 16 GB laptop (resampled data, see limits)

Results agree with scanpy as closely as two correct implementations can: the PCA spectra match, most cells land in the same clusters, and UMAP layout quality (trustworthiness) is within 0.004. Cluster agreement is not 1.0 for any pair of tools because clustering at resolution 0.8 is sensitive to small differences in the neighbour graph; this is also true between scanpy's own Leiden implementations.

Data used

DatasetFile and sourceSizeCells × genesCells after QCLicence / notes
PBMC 3k (real) pbmc3k_filtered_gene_bc_matrices.tar.gz from 10x Genomics, cf.10xgenomics.com/samples/cell-exp/1.1.0/pbmc3k/ (hg19, Matrix Market) 7.6 MB2,700 × 32,7382,699 10x Genomics public dataset (CC BY 4.0)
PBMC 68k (real) fresh_68k_pbmc_donor_a_filtered_gene_bc_matrices.tar.gz from 10x Genomics, cf.10xgenomics.com/samples/cell-exp/1.1.0/fresh_68k_pbmc_donor_a/ (hg19, Matrix Market) 124 MB68,579 × 32,73868,548 10x Genomics public dataset (CC BY 4.0); 37.3 million non-zero counts
PBMC 100k panel (simulated) scRNAseq_PBMC_100k_counts.csv + scRNAseq_PBMC_100k_cell_metadata.csv (dense CSV, cells as rows), with ground-truth columns true_cell_type, true_quality, doublet_components, batch and donor 109 MB + 9 MB100,000 × 50084,449 Simulated targeted panel with known labels; 9 true types including doublets; 15,323 cells removed by the 200-gene minimum because the panel has only 500 genes

Methods

Hardware and software

MachineApple M2 (8 CPU cores, 8-core GPU), 16 GB unified memory, macOS 26.6
BrowserChromium 152 with WebGPU (Metal), cross-origin isolated; GPU self-test 334 GFLOP/s (1024² matrix multiply)
BioViewAnalysis in the app's background worker, exactly as users run it; GPU path = WebGPU compute shaders with automatic per-stage CPU fallback; CPU path = the same pipeline in JavaScript
scanpyscanpy 1.10.3, anndata 0.10.9, numpy 1.26.4, scipy 1.13.1, scikit-learn 1.6.1, umap-learn 0.5.12, pynndescent 0.6.0, numba 0.60.0, leidenalg 0.12.0, python-igraph 1.0.0, scikit-misc 0.3.1 (needed for seurat_v3); Python 3.9, OpenBLAS 8 threads; numba warmed before timing

Pipeline and parameters (identical in both tools)

StepscanpyBioView
Quality controlcalculate_qc_metrics; keep ≥ 200 genes and ≤ 20 % mitochondrial countsmin genes 200, max mito 20 %
Gene filterfilter_genes(min_cells=3)min cells per gene 3
Variable geneshighly_variable_genes(flavor="seurat_v3", n_top_genes=2000) on raw countsSeurat v3 variance-stabilising method, 2,000 genes
Normalisenormalize_total(target_sum=1e4), log1ptarget sum 10,000, log1p
Scalescale(max_value=10) on variable genesz-score, clip at 10
PCA50 components, ARPACK (randomised also timed)50 components, exact Gram-matrix + Lanczos solver
Neighbourspp.neighbors(n_neighbors=15, n_pcs=50) (pynndescent above 4,096 cells)k = 15; exact tiled search on GPU up to 100k cells, IVF above; NN-descent on CPU
ClustersLeiden, resolution 0.8 (igraph and leidenalg)Leiden with refinement on the UMAP graph, resolution 0.8 (Louvain on SNN also reported)
UMAPtl.umap(min_dist=0.3)min_dist 0.3, same fuzzy-graph objective; PCA initialisation
Markersrank_genes_groups(method="wilcoxon", corr_method="benjamini-hochberg"), all genesWilcoxon rank-sum, Benjamini–Hochberg (stratified subsample above 50,000 cells)

How results were compared

Speed (seconds, lower is better)

PBMC 68k — 10x Genomics, 68,548 cells after QC

StageBioView GPUBioView CPUscanpy
Load (Matrix Market, 506 MB text)2.7–3.52.7–3.56.10
QC + gene filter + variable genes0.410.631.05
Normalise + scale0.150.351.20
PCA2.353.0011.34
Neighbours1.86 (exact)6.899.15
Clusters (Leiden)0.940.822.01 (igraph) · 212 (leidenalg)
UMAP0.7029.433.05
Marker genes (Wilcoxon, every cluster)2.842.9166.93
Paired total12.048.0130.8

PBMC 100k panel — 84,449 cells after QC

StageBioView GPUBioView CPUscanpy
Load (dense CSV 109 MB; scanpy read MTX)1.9–2.11.9–2.13.67
QC + gene filter + variable genes0.350.340.53
Scale0.180.730.30
PCA0.519.072.98
Neighbours2.58 (exact)10.638.92
Clusters (Leiden)0.900.962.10 (igraph) · 39.3 (leidenalg)
UMAP0.9035.7237.52
Marker genes1.121.155.81
Paired total8.360.461.8

PBMC 3k — 10x Genomics, 2,699 cells after QC

StageBioView GPUBioView CPUscanpy
Load0.20.20.32
PCA1.541.831.48
Neighbours0.030.210.19
UMAP0.182.942.79
Marker genes0.340.351.46
Paired total2.45.76.4

On small datasets "Auto" mode uses the CPU path (below 5,000 cells), where the difference is small.

Agreement with scanpy

MetricPBMC 3kPBMC 68kPBMC 100k
Variable genes, Jaccard (top 2,000)0.9970.982all 500 genes
PCA variance ratios, Pearson r (max abs. difference)1.0000 (1.2×10⁻⁴)1.0000 (4.9×10⁻⁴)1.0000 (1.5×10⁻⁶)
Clusters vs scanpy Leiden, ARI / NMI (GPU, Leiden)0.717 / 0.8780.825 / 0.8400.964 / 0.973
Clusters vs scanpy Leiden, ARI (default Louvain/SNN)0.9290.7610.875
Neighbour recall@15 in BioView's own PC space (GPU)1.0001.0001.000
Neighbour recall@15 vs scanpy's exact neighbours (GPU)0.8890.649*0.991
UMAP trustworthiness: BioView GPU / scanpy0.876 / 0.8750.891 / 0.8980.963 / 0.966
Marker genes, top-25 overlap (matched clusters)0.610.430.54

* On PBMC 68k, 2 % of the variable genes differ, so the two 50-dimensional PC spaces differ and neighbour lists cannot match exactly; within its own space BioView's GPU neighbours are exact. Marker overlap is limited by cluster boundaries differing slightly between tools.

Against known cell types (PBMC 100k panel)

MethodClustersARINMIMajority-vote ARIPurityTypes recovered
BioView GPU, Leiden210.3800.6950.9340.9499 / 9
BioView CPU, Leiden250.3320.6680.9350.9509 / 9
scanpy Leiden (leidenalg)230.3670.6860.9350.9519 / 9

Raw ARI is low for every tool because resolution 0.8 splits 9 types (with treatment, batch and donor structure) into 20–30 clusters. Mapping each cluster to its majority type gives ARI ≈ 0.93 for all methods.

Scaling to one million cells

Resampled data, not new biology. Cells were drawn with replacement from the real datasets and each count replaced by a Poisson draw (fixed seed). These runs test memory and speed only.

RunCells after QCMatrix storageTotal (s)
PBMC 100k → 500,000, CPU304,9530.77 GiB in memory170.6
PBMC 100k → 500,000, GPU304,9530.77 GiB in memory15.1
PBMC 68k → 1,000,000 × 32,738 genes, GPU, Leiden977,5221.49 GiB compact (398 M counts)70.4
Same, matrix kept on disk (browser storage)977,5220.11 GiB resident97.7

Machine load from other processes was high during these runs (load average 7–34), so they are pessimistic. At one million cells clustering is the slowest stage (17 s Leiden).

Statistics, plots and methods: reference checks

Besides the single-cell pipeline, every statistic and plot BioView draws is checked against a reference in an automated test (npm test in bioView/). All passed on 20 October 2026.

Test scriptReferenceChecks
check-stats.mjs — tests, post hoc comparisons, dose–response (4PL/5PL, EC50 with 95 % CI), flow cytometry mathsSciPy 1.13 / statsmodels 0.14; the ryegrass dose–response fit matches R drc LL.4275
check-methods.mjs — every study method in docs/METHODS.md, including normality, outliers, Deming, logistic and Prism nonlinear fits, Cox regression and number readingSciPy, statsmodels, lifelines 0.30, scikit-learn; R survival on aml; Hanley and McNeil 1982. Edge cases must give the right value or say why it cannot be computed832
check-plots.mjs — Kaplan–Meier and log-rank, ROC/AUC, bootstrap estimation intervals, hierarchical clustering, Q–Q, correlation matricesR's printed output (survival, pROC conventions) and hand-derived values256
check-matrix.mjs — every plot type with every compatible column and option, on the examples and on edge cases (missing values, text in number columns, n = 1 groups, zeros on log axes, 55 groups, 100,000 rows)Invariants: no NaN coordinates, every row plotted or counted as left out in the caption, points inside their axes, legends match series, brackets join the right groups, SVG and PDF export well formed15,102 builds · 85,949 checks
check-recipe.mjs — analysis recipe record, export, import and replayReplay must reproduce the recorded map bit for bit43
check-analysis.mjs, check-annotate.mjs, check-gpu.mjs — single-cell steps, CellTypist annotation, GPU supportExact maths; celltypist 1.7.1 on small synthetic models; GPU kernel parity runs in the browser135 named

One wrong-number bug was found this way and fixed: values such as "0.125" were read as 125 (a dot followed by three decimals was taken as a thousands separator). BioView now uses the same number reader as Data and Studio, with 13 regression cases.

Limits and honest caveats

Reproduce it

  1. Download the two 10x datasets listed above.
  2. Run scanpy with the parameters in the Methods table (the full script and package versions are in the project's bioView/docs/BENCHMARK-REFERENCE.md).
  3. Open BioView, load the same files, keep the default map settings (or choose Leiden in the map options), and run the map. Every setting used is recorded in BioView's Recipe and methods text.
  4. Compare outputs with bioView/scripts/compare-bench.mjs, which computes every metric on this page.

Full technical write-ups: bioView/docs/BENCHMARK.md (GPU vs CPU vs scanpy, kernel parity) and bioView/docs/GPU.md (architecture).