Filter and normalize
Remove low-quality cells and normalize an SDO table before clustering or differential expression.
After you load a table from a Spatial Data Object (SDO), check cell quality and normalize features before downstream analysis. The multiomics Python package runs both steps under ebsd.pp, with defaults chosen per modality.
Before you begin
- The multiomics Python package is installed.
- You have an in-memory AnnData table:
import elembio_spatialdata_analysis as ebsd
from elembio_spatialdata_analysis.load import Loader
loader = Loader("/path/to/RunID.zarr.zip")
adata = loader.load_tables(table="ThreePrimeUntargeted", lazy=False)
This page uses the 3′ transcriptome table (ThreePrimeUntargeted) as the example. The same calls apply to other tables. Pass the matching target_type string.
| Table | target_type | Filter | Normalize |
|---|---|---|---|
| 3′ transcriptome | "ThreePrimeUntargeted" | Yes | Yes |
| Imaging (CellPaint and protein IF) | "Imaging" | Yes | No single default. Scaling depends on your plate design. See Imaging tables. |
| OPS and specialized panels | "OPS" | Yes (cell area only) | Not applicable. OPS is an identity readout. |
Compute QC metrics
Compute per-cell QC metrics first when you want to plot distributions before you filter. The call writes adata.obsm["qc_metrics"] in place and does not drop cells.
ebsd.pp.compute_qc_metrics(adata, "ThreePrimeUntargeted")
ebsd.vis.plot_qc_per_well(adata, metric="total_counts")
Filter cells
To preview filtering without changing the loaded table, pass inplace=False and review the report:
filtered = ebsd.pp.filter_cells(adata, "ThreePrimeUntargeted", inplace=False)
ebsd.pp.generate_filter_report(filtered)
generate_filter_report returns a table of pass and fail counts per filter. It separates cells that fail only one filter from cells that fail several.
Default thresholds for the 3′ transcriptome table:
| Filter | Parameter | Default |
|---|---|---|
| Low counts, per batch | min_quant_count | Bottom 2.5% |
| High counts, per batch | max_quant_count | Top 1% |
| Small cells, per well | min_quant_area | Bottom 1% |
| Large cells, per well | max_quant_area | Top 1% |
| Mitochondrial fraction | max_pct_mt | 0.10 |
| Spatial artifacts | filter_spatial_artifacts | Off |
Quantile thresholds are fractions (for example, min_quant_count=0.05 drops the bottom 5%). Pass None to turn a filter off. Other modalities use different defaults. For example, OPS tables skip count filtering and filter on cell area only.
The defaults are a starting point, not a universal setting. Appropriate thresholds depend on cell type, assay, and run quality. Review the QC plots and the filter report for your run, then adjust thresholds before you filter in place.
When the thresholds look right, filter the working table in place:
ebsd.pp.filter_cells(adata, "ThreePrimeUntargeted")
To filter several tables from the same run together, use ebsd.pp.filter_modalities. It applies each modality's defaults and reports how each cell fares across tables.
Filter genes
For the 3′ transcriptome, remove genes detected in fewer than 10 cells in every well:
ebsd.pp.filter_genes(adata, "ThreePrimeUntargeted")
Other modalities are targeted panels, so filter_genes does nothing for them unless you pass min_cells_per_gene or min_counts_per_gene.
Normalize
ebsd.pp.normalize(adata, "ThreePrimeUntargeted")
The normalized, log-transformed matrix is written to adata.X, and the raw counts are kept in adata.layers["raw"]. Do not apply log1p again. The table is ready for Scanpy steps such as highly variable gene selection, PCA, and clustering.
Imaging tables
Imaging features are fluorescence intensities and morphology measurements, not counts, so ebsd.pp.normalize does not apply to them. Transform and scale them with a z-score instead, for example an arcsinh transform followed by a robust z-score. The reference group for the z-score depends on what you are comparing:
| What you are comparing | Reference group |
|---|---|
| Groups mixed within each well (for example, guides in a pooled screen) | Each well |
| Conditions that differ between wells, with shared control wells | Control wells |
| Conditions that differ between wells, with no shared control | All cells |
Scaling each well on its own removes real differences between conditions that differ between wells. The imaging-pipeline skill in the multiomics skills covers how to choose a reference, which features to exclude, and how to handle edge cases.
Next steps
- Clustering cells with Python
- Analyzing an optical pooled CRISPR screen
- Measuring a 3′ treatment response