Python API¶
from citeqc import coupling, diagnostics, mapping, datasets, io
citeqc.coupling ¶
Protein ~ cognate-mRNA coupling.
The measurement answers: within a homogeneous group of cells, does a marker's antibody signal track its own transcript?
Three design constraints make the number meaningful, and all three are enforced here rather than left to the caller:
- Stratify by donor x cell type. Differences in cell-type composition move protein and mRNA together, so an unstratified correlation mostly measures composition and is strongly inflated.
- Use equal-sized pseudo-groups. A mean over cells is less noisy when more
cells are averaged, so groups of unequal size introduce a size-driven
gradient (see
diagnostics.size_confoundinganddiagnostics.equal_n_resample). - Regress out library depth in both modalities before correlating.
Absolute values depend on how finely cell types are split, so compare ranks
between datasets, not raw correlations (rank_percentile).
log_normalise ¶
log_normalise(counts, total=None, scale=10000.0)
log1p(counts per scale), the standard RNA normalisation.
measure ¶
measure(
adt,
rna,
meta,
pairs,
*,
strata=("donor", "celltype"),
group_size=10,
n_draw=10,
min_groups=25,
seed=0,
depth_cols=("nCount_ADT", "nCount_RNA"),
return_raw=False,
min_obs=10,
)
Measure protein~mRNA coupling for each antibody/gene pair.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
adt
|
DataFrame
|
Cells x antibodies, already normalised (see |
required |
rna
|
DataFrame
|
Cells x genes, already normalised (see |
required |
meta
|
DataFrame
|
Cells x metadata; must contain |
required |
pairs
|
dict
|
|
required |
strata
|
tuple of str
|
Metadata columns whose combinations define homogeneous groups. |
('donor', 'celltype')
|
group_size
|
int
|
Cells averaged into each pseudo-group. |
10
|
n_draw
|
int
|
Random partitions into pseudo-groups per stratum. |
10
|
min_groups
|
int
|
Pseudo-groups a stratum must yield to be used, so a stratum needs at
least |
25
|
seed
|
int
|
Seed for the random partitions. |
0
|
depth_cols
|
tuple of str
|
Columns of |
('nCount_ADT', 'nCount_RNA')
|
return_raw
|
bool
|
Also return every stratum x draw observation. Needed for anything that must respect the donor structure, such as a donor bootstrap, a per-cell-type breakdown, or weighting by variance. |
False
|
min_obs
|
int
|
Markers with fewer stratum x draw observations than this are left out of the summary. |
10
|
Returns:
| Name | Type | Description |
|---|---|---|
summary |
DataFrame
|
One row per marker: |
raw |
DataFrame
|
Only with |
Raises:
| Type | Description |
|---|---|
ValueError
|
If required columns are missing, no cells or pairs are shared, or no
marker reaches |
specificity ¶
specificity(
adt,
rna,
meta,
pairs,
*,
genes=None,
strata=("donor", "celltype"),
group_size=10,
n_draw=10,
min_groups=25,
seed=0,
depth_cols=("nCount_ADT", "nCount_RNA"),
max_gb=2.0,
)
Is a marker's coupling to its own transcript above the background of other genes?
A small correlation is hard to read on its own. This correlates every
antibody with every gene in genes, using the same stratification, pseudo-
grouping and depth adjustment as measure, and asks where the cognate gene
falls within that antibody's row.
The background is not a null: genes that truly co-vary with the antibody
(CD3E with CD8A in T cells) sit in it. Read the percentile as descriptive.
With the same seed, the cognate entries equal measure's medians.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
adt
|
DataFrame
|
Cells x antibodies, already normalised, as in |
required |
rna
|
DataFrame
|
Cells x genes, already normalised, as in |
required |
meta
|
DataFrame
|
As in |
required |
pairs
|
dict
|
As in |
required |
genes
|
list of str
|
Genes forming the background. Default: the cognate genes of |
None
|
strata
|
tuple of str
|
As in |
('donor', 'celltype')
|
group_size
|
int
|
As in |
10
|
n_draw
|
int
|
As in |
10
|
min_groups
|
int
|
As in |
25
|
seed
|
int
|
As in |
0
|
depth_cols
|
tuple of str
|
As in |
('nCount_ADT', 'nCount_RNA')
|
max_gb
|
float
|
Refuse to run if the stored observations would exceed this size. |
2.0
|
Returns:
| Name | Type | Description |
|---|---|---|
matrix |
DataFrame
|
Antibodies x genes, the median correlation. |
summary |
DataFrame
|
One row per antibody: |
rank_percentile ¶
rank_percentile(df, col='median_rho')
Within-dataset percentile rank, the unit to compare across datasets.
citeqc.diagnostics ¶
Five checks for artefacts that produce confident-looking false positives.
Each check is built around the same question: what would this analysis report if there were no signal at all? None of them needs a ground truth: they use negative controls that the data already contains.
panel_consistency antibody panel differs between donors/batches
control_check markers that cannot be expressed behave like real signal
size_confounding a per-group statistic tracks group size
pvalue_sanity the p-value distribution is not what a null looks like
saturation_check bounded values (PSI, fractions) are pinned at 0/1
control_check diagnoses; it does not explain. When it flags, test group size
(size_confounding, equal_n_resample) before reaching for an ambient-signal
correction: in the data this package was developed on, resampling to equal N
removed the gradient and regressing out ambient signal did not.
Finding
dataclass
¶
The result of one check.
Attributes:
| Name | Type | Description |
|---|---|---|
check |
str
|
Name of the check. |
passed |
bool
|
False when the check flags a problem. |
summary |
str
|
One line for humans; |
detail |
dict
|
The numbers behind the decision. Keys depend on the check. |
panel_consistency ¶
panel_consistency(adt, meta, by='donor', min_detect=0.5)
Is the same antibody panel measured in every donor/batch?
Heterogeneous panels are common in multi-site CITE-seq and silently turn
"marker absent" into "marker not measured". An antibody counts as measured
in a group when more than min_detect of its cells have a value, so a
marker that was not measured must be NaN, not 0. Returns the core panel (the
markers present everywhere), which is what cross-donor analysis may use.
control_check ¶
control_check(
stat,
control_markers,
signal_markers=None,
ratio_threshold=0.5,
)
Do markers that cannot be expressed behave like real signal?
stat is your own per-marker test statistic (Z, -log10 p, effect size...).
control_markers should be absent from the cells analysed: CD19/CD20 on
T cells, CD3 on monocytes. Choose markers that are truly absent: CD16, CD56
and CD161 occur on CD8 T-cell subsets and make poor controls there.
Flags when the controls reach a comparable magnitude to the real markers.
That means the statistic contains a marker-independent gradient. It does
not say where the gradient comes from. Group size is the first suspect
(size_confounding); ambient signal is the second, and a correction for it
should be judged on control markers held out of the correction.
size_confounding ¶
size_confounding(
values, sizes, method="spearman", skew_threshold=0.75
)
Does a per-group statistic track how many cells each group contains?
values is groups x features (clonotype pseudobulk, cluster means...);
sizes the number of cells per group, in the same row order as values
(a Series indexed by the same group labels is matched by label). Because a mean over more cells is
less noisy, low-expressed features drift upward with group size. The
signature is a pervasive one-directional association across features;
real biology does not move almost every feature the same way.
equal_n_resample ¶
equal_n_resample(cell_groups, n, n_draw=10, seed=0)
Yield n_draw dicts {group: array of exactly n cell ids}.
Groups with fewer than n cells are dropped; that exclusion is the price
of removing the size artefact, and is reported by the caller.
pvalue_sanity ¶
pvalue_sanity(
pvalues, alpha=(0.001, 0.01, 0.05), spike_bins=20
)
Is this p-value distribution consistent with a working test?
Three regimes matter. Enriched near zero means signal. Uniform means no signal. Depleted near zero means the test is broken, commonly because many features are constant or saturated and yield p close to 1, which also shows up as a spike somewhere in the interior.
saturation_check ¶
saturation_check(
values,
lo=0.02,
hi=0.98,
min_range=0.05,
max_saturated=0.25,
)
What fraction of bounded features (PSI, fractions) is pinned at 0 or 1?
Saturated features carry no information but do enter multiple-testing correction and flatten the p-value distribution. Returns the index of the features worth testing.
citeqc.mapping ¶
Antibody -> cognate gene lookup.
map_antibodies ¶
map_antibodies(antibodies, genes=None, extra=None)
Map antibody names to cognate gene symbols.
Tries the raw name, then a normalised form, then the normalised form with a trailing replicate index removed ('CD38-1' -> 'CD38').
Returns (pairs, unmapped) where pairs is {antibody: gene}. If genes is
given, pairs are restricted to genes actually present.
mechanism_of ¶
mechanism_of(antibody)
Documented post-transcriptional mechanism for an antibody, or ''.
citeqc.io ¶
Readers. CSV/TSV always, parquet with pyarrow, AnnData/MuData with the anndata extra.
read_matrix ¶
read_matrix(path, transpose=False)
Read a table whose first column is the row index (cell barcodes or group names).
Tab-separated for .tsv/.txt, comma-separated otherwise; gzip, bz2, xz,
zip and zstd compression are detected from the extension. .parquet files
need pyarrow (or fastparquet). Pass transpose=True if the file is
features x cells.
from_mudata ¶
from_mudata(
path,
adt_key="prot",
rna_key="rna",
strata=("donor", "celltype"),
)
Read (adt, rna, meta) from a .h5mu file. Requires the anndata extra.
adt and rna are the .X matrices of the two modalities as cells x
features DataFrames, and meta is the global .obs, which must contain
the strata columns. Cells missing from either modality are dropped from
all three tables.
If .obs has no nCount_ADT / nCount_RNA column, they are computed as
the per-cell sum of each modality's .X. That is only a library size if
.X holds raw counts: if your .X is already normalised, add the raw
totals to .obs before calling this.
citeqc.datasets ¶
A small synthetic CITE-seq dataset with a known answer, for trying citeqc offline.
Nothing here is real data. The counts are simulated, so that every antibody's behaviour is known by construction and you can check that citeqc recovers it. Use it to learn the workflow, not to draw biological conclusions.
Design (see DESIGN):
- 4 donors x 2 T-cell types (
CD4 T,CD8 T), 320-479 cells per stratum. - 15 antibodies and 25 genes, as raw UMI counts. The RNA table is a subset of
the transcriptome:
meta.nCount_RNAis the whole-library size, larger than the row sums ofrna. - Within each stratum, every gene has a per-cell latent state. Coupled antibodies are driven by the state of their cognate gene plus protein-specific noise; decoupled antibodies (CD45RA, CD152, CD154) vary independently of their transcript; the B-cell markers CD19 and CD20 are absent from T cells and carry only ambient antibody background, so they serve as negative controls; IgG1 is an isotype control with no cognate gene.
- Donor
D4was stained with a panel that lacks CD154 (NaN inadt), sodiagnostics.panel_consistencyhas something to find. BG01-BG12are background genes with no antibody, forcoupling.specificity.
synthetic ¶
synthetic(seed=0)
Simulate the example dataset.
The same seed gives the same tables on the same NumPy version.
Returns:
| Name | Type | Description |
|---|---|---|
adt |
DataFrame
|
Cells x 15 antibodies, raw counts (float, NaN where not measured). |
rna |
DataFrame
|
Cells x 25 genes, raw counts. |
meta |
DataFrame
|
Cells x [donor, celltype, nCount_ADT, nCount_RNA]. |
pseudoclonotypes ¶
pseudoclonotypes(
rna,
meta,
n_groups=120,
min_size=5,
max_size=300,
seed=0,
)
Groups of unequal size drawn from the same cells: a null for group-size effects.
Each group is a random sample of cells (sizes log-uniform between min_size
and max_size), so no gene truly depends on group size. The value per group
is a pseudobulk, log1p of the mean counts per 10,000 (meta.nCount_RNA
as library size). Any association with size that
diagnostics.size_confounding finds is therefore the artefact.
Returns:
| Name | Type | Description |
|---|---|---|
values |
DataFrame
|
Groups x genes. |
sizes |
Series
|
Cells per group. |
members |
dict
|
|
write_example ¶
write_example(directory, seed=0)
Write the example dataset as CSV files for the command line.
Files: adt.csv, rna.csv, meta.csv (cells in rows), groups.csv
(pseudo-clonotype pseudobulk plus an n_cells column, for
citeqc diagnose --groups) and design.csv (the ground truth).
Returns {name: path}.