Skip to main content

stancounts logo

Recover raw counts from log1p-normalized single-cell expression matrices

Python 3.9+ License: MIT PyPI Platform


Why

Many single-cell tools and public datasets distribute only the log1p-normalized expression matrix, discarding the raw counts. Some downstream methods (e.g., RNA velocity, differential expression via negative-binomial models, scVI) require integer counts.

stancounts reverses the normalization — exactly, in one line — without needing to know the original target_sum.

How it works

Standard scRNA-seq normalization:

y = log1p(count / library_size × target_sum)

The key insight: in every cell, the minimum nonzero value after expm1 corresponds to a gene with count = 1 (near-universal in sparse scRNA-seq data). This gives us the per-cell scale factor, and division + rounding recovers exact integer counts.

scale_factor = min_nonzero(expm1(y))     # per cell
counts       = round(expm1(y) / scale_factor)

The algorithm is target_sum agnostic — it works with any normalization target (1e4, 1e6, median, etc.) and any per-cell size factor scheme (scanpy, scran, Seurat).

Installation

Python package

pip install git+https://github.com/chansigit/stancounts.git

Or from source:

git clone https://github.com/chansigit/stancounts.git
cd stancounts
pip install -e .

Claude Code plugin

If you use Claude Code, stancounts is also available as a plugin with a built-in skill that guides Claude through the detection and recovery workflow:

/plugins add chansigit/stancounts

Once installed, simply ask Claude to "recover counts" or "reverse log1p" and the skill activates automatically.

Quick start

import stancounts
import scanpy as sc

adata = sc.read_h5ad("my_dataset.h5ad")

# One-liner: recover counts into a new layer
stancounts.reverse_log1p_anndata(adata)
# → adata.layers["counts_recovered"]
# → adata.obs["stancounts_scale_factor"]
# → adata.obs["stancounts_library_size"]

Auto-detect normalization

det = stancounts.detect_normalization(adata.X)
print(det)
# {'is_log1p': True, 'base': 'e', 'scores': {'e': 1.0, '2': 0.32, '10': 0.31},
#  'is_integer': False, 'max_value': 7.26}

if det["is_log1p"]:
    stancounts.reverse_log1p_anndata(adata, base=det["base"])

Low-level API

result = stancounts.reverse_log1p(
    adata.X,          # sparse or dense matrix
    base="e",         # "e", "2", or "10"
    robust=True,      # handle rare min_count > 1 cells
)

counts        = result["counts"]          # recovered count matrix
scale_factors = result["scale_factors"]   # per-cell scale factors
library_sizes = result["library_sizes"]   # recovered library sizes
corrections   = result["corrections"]     # robust correction flags

Validation

Tested on 256,000+ real cells with ground-truth counts:

Dataset Cells Genes Recovery
PBMC3k 2,638 13,714 100%
Purified PBMC 105,753 19,512 100%
CITE-seq PBMC 147,582 20,729 100%

All three datasets: every element, every cell, exactly recovered.

Robustness

Scenario Result
target_sum = 100, 1e3, 1e4, 1e5, 1e6 100%
target_sum = median(library_size) 100%
scran-style arbitrary size factors 100%
log2(1+x) and log10(1+x) 100%
Cells with min_count > 1 (robust mode) 100%
Deeply sequenced cells (high counts) 100%

API reference

stancounts.reverse_log1p(X, *, base="e", robust=True)

Reverse log1p normalization to recover raw counts.

Parameters:

  • X — expression matrix (cells × genes), sparse or dense
  • base — log base: "e" (default), "2", or "10"
  • robust — detect and correct cells whose minimum nonzero count > 1

Returns dict with keys: counts, scale_factors, library_sizes, corrections

stancounts.reverse_log1p_anndata(adata, *, source="X", target_layer="counts_recovered", base="e", robust=True)

Convenience wrapper for AnnData objects. Stores recovered counts in adata.layers[target_layer] and scale factors in adata.obs.

stancounts.detect_normalization(X, *, n_sample=200, seed=0)

Auto-detect whether data is log1p-normalized and determine the log base.

Returns dict with keys: is_log1p, base, scores, is_integer, max_value

stancounts.is_log1p_normalized(X, **kwargs)

Convenience wrapper: returns True if data appears log1p-normalized.

When it won't work

  • Batch-corrected data (e.g., Harmony, scVI latent space) — the linear count→normalized relationship is broken
  • scTransform (Pearson residuals) — not a log1p(scaled_counts) transform
  • Aggregated/pseudobulk data — may still work if the aggregation preserves the structure
  • Fractional counts (e.g., from imputation) — no integer counts to recover

Dependencies

  • numpy >= 1.22
  • scipy >= 1.7
  • anndata >= 0.8 (optional, for AnnData integration)

License

MIT

Metadata

Release files for stancounts 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for stancounts 0.3.0
File Size Uploaded Provenance
stancounts-0.3.0.tar.gz 26.1 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for stancounts 0.3.0
File Interpreter ABI Platform
stancounts-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 37.8 kB

Release files / stancounts-0.3.0.tar.gz

Download URL stancounts-0.3.0.tar.gz
Size 26.1 kB
Tags Source
SHA-256 checksum
How to use checksums
ec5837544add81337524dc27aefb4d4609642c552ccef84a69ba3bfc558d4e2d
BLAKE2b-256 checksum
How to use checksums
66cf899fda8eb6ebfca4339b3bddc2cbd71a41e51999a27f9fe2e38e69054cde
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.1

Release files / stancounts-0.3.0-py3-none-any.whl

Download URL stancounts-0.3.0-py3-none-any.whl
Size 11.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d060b49f78b9163677fa5b11a1dcffbd2428f7268899e171ca365b80d0667e3a
BLAKE2b-256 checksum
How to use checksums
89f7ce42e5550d274deebb45b6d66ea1ae336e3da0166ee6d67e9f81ca50f484
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.1

Security for stancounts 0.3.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page