annotate_empirical_fdr
Annotates a within-state DEG table with an empirical FDR for the low-expression control-sampling artifact.
annotate_empirical_fdr(
deg_tbl,
abundances = NULL,
unaffected_cell_types = NULL,
sig_thresh = 0.05,
n_abund_bins = 4L,
expr_breaks = c(-Inf, -5, -4, -3.5, -3, -2.5, -2, -1, Inf),
lower_envelope_frac = 1 / 3
)
Arguments
-
deg_tbl
data.frame / tibble
Within-state DEG results. Must containcell_group,log_mean_expression,perturb_to_ctrl_p_value(the ashr local false sign rate used as the significance quantity), andperturb_to_ctrl_shrunken_lfc. -
abundances
named numeric vector or data.frame
Optional per-cell-type abundance used to stratify the null. Either a named numeric vector (cell_group-> total cells) or a data.frame with columnscell_groupandabund. IfNULL, a depth proxy is derived frommean_log_sf(mean cells per pseudobulk), which must then be present indeg_tbl. Supplying real cell counts is more accurate and recommended. -
unaffected_cell_types
character vector
Optionalcell_groupnames known to have no real perturbation effect (e.g. abundance "A0" and no fitness/identity phenotype), used as the self-null training set. IfNULL, a lower-envelope fallback is used: within each abundance bin the cell types in the bottom tertile of overall call rate are treated as unaffected. -
sig_thresh
numeric
Significance threshold defining a call. Default0.05. -
n_abund_bins
integer
Number of abundance strata. Default4. -
expr_breaks
numeric vector
Breakpoints used to binlog_mean_expression. -
lower_envelope_frac
numeric
Fraction of each abundance bin's cell types treated as the self-null whenunaffected_cell_typesisNULL. Default1/3.
Value
deg_tbl with three added columns joined per (cell_group, expression-bin, direction): empirical_null_rate (percent of admitted genes called in the matched unaffected cell types), observed_rate (percent called in this cell type), and empirical_fdr (min(1, empirical_null_rate / observed_rate)). Rows in strata with no unaffected estimate receive empirical_fdr = 0 (i.e. not flagged), the conservative-for-discovery default.
Details
In control-heavy perturbation designs the deep control arm detects lowly expressed genes that the shallow perturbation arm misses, producing a large excess of low-expression, "on-in-control" (down) DEG calls that are sampling artifacts rather than biology. This function estimates, per (cell_group x expression-bin x direction), the rate of calls attributable to that artifact and returns the input rows annotated with the null/observed rates and the resulting FDR.
The null is read directly off the perturbation's own unaffected cell types (perturbation biology only adds calls, so cell types with no real effect are self-null), matched to each cell type by abundance so that shallow cell types are compared against shallow ones. No permutation, model fitting, or re-run is required, so existing DEG tables can be decorated in place.
Estimation is direction-specific because the artifact is directional: in control-heavy designs it inflates down (on-in-control) calls, while up calls at high expression are largely genuine. Consumers can threshold empirical_fdr (e.g. drop calls above some cutoff) to suppress the artifact while retaining real signal.
Pipeline-computed DEG tables (as in the Perturbation DEGs page) are typically already annotated with empirical_p/empirical_fdr via a more elaborate, arm-size-conditioned model — see annotate_empirical_fdr_model(). annotate_empirical_fdr() is the simpler, self-contained version of the same idea for decorating your own DEG tables without training a model first.
Examples
genes_within_cell_state <- annotate_empirical_fdr(genes_within_cell_state)
genes_within_cell_state %>% dplyr::filter(empirical_fdr < 0.1)