scanpy · log1p · multi-layer normalization · false-positive warning
Why scanpy Warns "adata.X seems to be already log-transformed" Even When You Never Touched X
Status as of writing: open, reproduced against scanpy main, 2026-09. Reported in scanpy#4285. Not a data-loss bug — a misleading warning that can hide a real one.
If you work with spliced/unspliced layers (RNA velocity workflows) or any multi-layer object, you've probably run something like this and been confused by the output:
sc.pp.log1p(adata)
sc.pp.log1p(adata, layer="spliced")
sc.pp.log1p(adata, layer="unspliced")
# WARNING: adata.X seems to be already log-transformed.
# WARNING: adata.X seems to be already log-transformed.
The last two calls target layer="spliced" and layer="unspliced" — they don't touch .X at all. Yet you get the "X seems to be already log-transformed" warning twice.
The actual check
Confirmed by reading scanpy's _simple.py directly:
@log1p.register(AnnData)
def log1p_anndata(adata, *, base=None, copy=False, chunked=False,
chunk_size=None, layer=None, obsm=None):
if "log1p" in adata.uns:
logg.warning("adata.X seems to be already log-transformed.")
...
The check is if "log1p" in adata.uns — a single flag that gets set the first time you log1p anything on the object, and is then checked unconditionally on every subsequent call regardless of the layer argument. It never looks at the actual target layer, only at whether .X was ever log1p'd before, on any call.
Why this is more than cosmetic
Two real costs, neither of which is "the code breaks":
- Alert fatigue. If you log1p three layers per sample across dozens of samples in a loop, you get a wall of identical-looking warnings that are individually meaningless — which trains you to stop reading them. The one time this warning is actually telling you something real (you really did call log1p on
.Xtwice — the double-normalization class of bug we've written about before), it's buried in noise. - It's a global flag, not a per-layer one. There's no way from the warning text alone to tell which of your calls it's actually about.
What to do about it now
Since the fix isn't merged yet, the practical workaround is to not rely on the built-in warning to catch double-log1p at all — check explicitly instead:
# Before log1p-ing a layer, check its own max value directly —
# don't rely on adata.uns['log1p'] or the warning text.
import numpy as np
def already_logged(matrix, threshold=20):
m = matrix.max() if hasattr(matrix, "max") else matrix.toarray().max()
return m < threshold # raw UMI counts are almost always well above this
for layer in ["spliced", "unspliced"]:
if not already_logged(adata.layers[layer]):
sc.pp.log1p(adata, layer=layer)
This is the same lesson as our earlier double-normalization post, applied one level up: don't trust a global "has this been transformed" signal on an object that may hold several independently-processed matrices. Check the specific array you're about to touch.
Verified directly against scanpy's main branch source, 2026-09. Source: scanpy#4285.
More preprocessing landmines
The double-normalization bug that hit our own notebooks
Pack 1 documents 7 real API-drift bugs found while re-executing all 16 notebooks — exact errors, root causes, fixes.
Get Pack 1 for $19 →