CEP-IP with CMRF

cepip

Author: Kah Keng Wong ORCID

R package for CEP-IP (cell explanatory power with inflection point) and its extensions SKCS (Spearman-Kendall cell-level score) and CMRF (copula monotone recovery filter) for co-expression analysis in single-cell data.

Table of Contents
No. Section Descriptions
About the package
1 Overview Aims of the package and its functions
Getting started
2 Install Installation from GitHub
3 Quick Start A first run on simulated data
4 Worked Example Each step of the method on the SEA-AD example data
Outputs and reproduction
5 Output of cepip_save() Tables and plots written by the package
6 Reproducing the SEA-AD Analysis 698 published values checked
7 Reproducing the PCa Analysis 133 published values of the CEP-IP paper checked
Using this work
8 License and Data Terms MIT License and the terms of the example data
9 Citation Citations of the methods


1. Overview

cepip analyzes the co-expression of a gene of interest (GOI) with a set of partner genes in single-cell data. The partner genes are genes whose expression rises with the GOI across cells. Their mean in each cell is called the composite. In most datasets, only some of the cells follow this relationship closely. cepip identifies these cells, finds the GOI level at which the relationship changes, and relates the result to donor-level outcomes.

The package has three parts:

The downstream functions compute the share of each donor's cells in each group, the differential expression between groups, and the association of a donor-level measure with clinical outcomes.

The functions, in the order of the analysis:

Step Function Output
Partner screen cepip_screen() GOIs ranked by their number of dual-filter partner genes (Spearman rs and Kendall τb)
Composite cepip_composite() mean expression of the partner genes per cell
CEP-IP GAM cepip_fit() GAM-REML-PRSS fit (k by the lowest PRSS), deviance explained (DE)
EP and TREP cepip_ep() explanatory power per cell and the TREP cells (top round(n x DE) by EP)
Inflection point cepip_ip() IP from the residual GAM R(x), 95% CI, IPy, IPRS
SKCS cepip_skcs() per-cell Spearman and Kendall contributions and the SKCS-core cells
CMRF band cepip_band() spine, δbase and δadapt band in copula space (Stage 1)
CMRF gate cepip_cmrf() RSk-fCV gate (Stage 2) and the Module (OriRef plus Recovered cells)
Validation1 cepip_validate() GAM refit on the Module cells and label agreement
Groups per donor cepip_donor() share of each donor's cells in the Module Post-IP and other groups
DEGs cepip_deg() donor-paired differential expression between two groups
Donor outcomes cepip_associate() OLS of donor outcomes on a donor measure with covariates
All steps cepip() runs CEP-IP, and SKCS and CMRF when asked
Output cepip_save() numbered tables (one workbook) and plots in one folder

Each step has a plot function: plot_screen(), plot_prss(), plot_fit(), plot_residual(), plot_pitcher(), plot_copula(), plot_band(), plot_rskfcv(), plot_module(), plot_validate(), plot_donor(), plot_deg(), and plot_associate().

The full analysis of the SEA-AD preprint, with all scripts, the container, and the console logs, is in the repository CEP-IP_with_CMRF_in_SEA-AD.


2. Install

# install.packages("remotes")
# remotes::install_github("kahkengwong/cepip")
install.packages("cepip", repos = c('https://kahkengwong.r-universe.dev', 'https://cloud.r-project.org'))

The class tests of CMRF use the suggested packages clinfun, Hmisc, and energy. The screen uses pcaPP for a fast Kendall tau-b when it is installed.


3. Quick Start

library(cepip)
d <- cepip_toy                                  # 1,200 simulated cells, 12 donors
res <- cepip(d$x, d$y, donor = d$donor, id = d$id, cmrf = TRUE)
res
plot_fit(res)
plot_module(res$cmrf)
cepip_save(list(toy = res), file.path(tempdir(), "toy_output"))

[!NOTE] cepip(x, y) with the default settings is the published CEP-IP, except for one setting. Cells are labelled Pre-IP or Post-IP on the response axis (y < IPy). ip_axis = "x" gives the published split on the GOI axis (x < IPx).

[!IMPORTANT] The package does not transform the data. The published CEP-IP analyses used log2(value + 1) of Seurat log-normalized data, and the SEA-AD analysis used ln(CP10K + 1). The default thresholds (screen, DEGs, outcomes, and covariates) are those of the published analyses. They should be adjusted to each dataset and question.


4. Worked Example

This section runs cepip on the SEA-AD example data and explains the method two panels at a time. The data are the 13,331 PVM_3 cells of the neocortical regions (NeoC). The GOI is MT-CO3, and the composite is the mean of its eight mitochondrial partner genes. The run uses the settings of the paper and gives the published values.

pvm3 <- cepip_example("pvm3")
d    <- pvm3[pvm3$region_set == "NeoC", ]
mt8  <- c("MT-ATP6", "MT-CO2", "MT-ND3", "MT-ND4", "MT-CYB", "MT-ND1", "MT-ND2", "MT-CO1")
y    <- cepip_composite(d, mt8)
res  <- cepip(d[["MT-CO3"]], y, donor = d$donor, id = d$barcode, cmrf = TRUE, ip_k = 10,
              y_spine = d$composite_part8, workers = 4)   # y_spine: composite of the paper's data export

The code under each figure is condensed from the package source, and the file is named in its first line. The figures are drawn by assets/readme_figure.R. The six panels are also in one figure: assets/cepip_example_figure.png.

4.1 SKCS-core Cells and the Inflection Point (Panels A and B)

Panels A and B

Aims. Panel A shows the cells that follow the global monotonic relationship between the GOI and its partner genes. Panel B finds the GOI level at which the best-explained cells move from below the population curve to above it. This level is the inflection point (IP).

Methods.

$$S_i = \frac{4,(R_{x,i} - \bar{R})(R_{y,i} - \bar{R})}{(n-1)^2} \qquad\qquad K_i = \frac{C_i - D_i}{n-1}$$

# R/skcs.R (condensed)
Si_norm <- 4 * (rx - mean(rx)) * (ry - mean(ry)) / (n - 1)^2   # S_i above; rx, ry: ranks
Ki      <- cd / sqrt((n - 1 - tx) * (n - 1 - ty))               # K_i; tx, ty: ties of cell i (tau-b form)
is_core <- (Si_norm > 0.05) & (Ki > 0.5)

[!NOTE] Si in this README is the normalized score Si_norm of the package. With ties, Ki uses the tau-b denominator, as in the code above.

# R/ip.R (condensed)
rm <- mgcv::gam(residual ~ s(GOI, bs = "tp", k = 20) + s(donor, bs = "re"),
                data = rd, weights = w, method = "REML")   # rd: residuals of the top-EP cells
i  <- which(r_hat[-g] < 0 & r_hat[-1] > 0)[1]               # r_hat: R(x) on a grid of g points
ip <- x_seq[i] - r_hat[i] * (x_seq[i + 1] - x_seq[i]) / (r_hat[i + 1] - r_hat[i])

Results.

4.2 The CMRF Band and the RSk-fCV Gate (Panels C and D)

Panels C and D

Aims. The fixed Si and Ki gates leave out cells that still follow the relationship. Panel C builds a band around the curve of the SKCS-core cells and finds the other cells that fall inside it. Panel D tests these candidate cells and keeps those that do not make the monotonic curve worse.

Methods.

$$\delta_{adapt}(u) = \delta_{base}\left(1 + \kappa, e^{-(u - IP_u)^2 / (2,bw^2)}\right), \qquad \kappa = 0.5,\ bw = 0.1$$

# R/band.R (condensed)
u <- rank(x) / (n + 1); v <- rank(y) / (n + 1)                 # copula space
delta_base  <- weighted_quantile(abs(v[core] - g_u[core]), w_core, 0.95)
delta_adapt <- delta_base * (1 + kappa * exp(-(u - IP_u)^2 / (2 * bw^2)))
pass1 <- abs(v - g_u) <= delta_adapt                            # candidates: !core & pass1
# R/cmrf.R (condensed): one iteration of the gate
p   <- wilcox.test(rmse_B, rmse_A, paired = TRUE, alternative = "greater")$p.value
eff <- median(rmse_B - rmse_A) / median(rmse_A)                 # dRMSE
bv  <- IQR(rmse_A) / median(rmse_A)                             # background variability
if (p < 0.05 && eff > bv) active[worst] <- FALSE                # remove the candidate farthest from the curve

[!TIP] cepip_band() is fast. It can be run with several values of kappa and bw and checked with plot_band() before the slower gate of cepip_cmrf(). plot_rskfcv() shows the folds and the test RMSE of curves A and B.

Results.

4.3 The Final Module and the Pitcher Plot (Panels E and F)

Panels E and F

Aims. Panel E shows the final Module. Panel F places the cells on the two cell-level scores, Si and Ki, and shows the fixed gate that the Recovered cells failed.

Methods.

# R/cmrf.R (condensed)
cls <- ifelse(core & pass, "Original + Refined", ifelse(core, "Original only",
       ifelse(pass, "Recovered", "Excluded")))
in_module <- cls %in% c("Original + Refined", "Recovered")
u_SK <- rank(Si_norm) / (n + 1); v_SK <- rank(Ki) / (n + 1)    # pitcher plot coordinates

Results.


5. Output of cepip_save()

One folder per arm, with no subfolders. Files are numbered in pipeline order, and the region set (or other subset) is part of each name.

File Content
00_settings.txt, 00_log.txt settings, versions, summaries, and the console log
<arm>_cepip_tables.xlsx one sheet per table, from 01_screen to 14_associate
<arm>_cepip_cells.xlsx the cell-level tables, when they are too large for the first workbook
02_prss to 14_associate (.png and .pdf) the plots

6. Reproducing the SEA-AD Analysis

Rscript "$(Rscript -e 'cat(system.file("scripts", "reproduce_seaad.R", package = "cepip"))')" cepip_seaad 4
Value PVM_3 NeoC PVM_3 NonNeoC HKG8 NeoC HKG8 NonNeoC
Cells 13,331 11,132 1,432 1,079
IPx 2.4953 2.6572 1.5552 1.8130
IPRS 0.791 0.819 0.639 0.571
SKCS-core cells 8,407 6,941 250 199
OriRef / Recovered 8,103 / 1,351 6,646 / 1,089 239 / 287 191 / 240
Removed by the gate 643 619 65 22

[!NOTE] The DEG step runs only when the DEG matrices are available. When a data folder is given, it must hold pvm3_deg_NeoC.rds and pvm3_deg_NonNeoC.rds. Without them, the 56 DEG values are marked NOT RUN in reproduction_check.csv.


7. Reproducing the CEP-IP Analysis of the PCa Dataset

Rscript "$(Rscript -e 'cat(system.file("scripts", "reproduce_cmpb_pca.R", package = "cepip"))')" cepip_cmpb_pca 4 <folder with the .RData> <Supplementary Table 10 .xlsx>
Sample Cells k DE IPx IPRS
HYW_4701 985 6 0.3155 3.7963 0.696
HYW_4847 820 3 0.2865 2.1185 0.670
HYW_4880 337 6 0.5887 3.2237 0.769
HYW_4881 1,232 10 0.8346 3.2233 0.768
HYW_5386 156 4 0.3095 2.3856 0.631
HYW_5742 1,468 4 0.5106 3.3212 0.719
HYW_5755 857 3 0.5276 3.4270 0.691

[!TIP] The first run saves the GAM data of the seven samples as pca_trpm4_ribo_cells.csv in the output folder. Later runs with the same output folder read this file, so the 9.5 GB .RData is needed only once.


8. License and Data Terms

The package code is licensed under the MIT License.

The example data are derived from the Seattle Alzheimer's Disease Brain Cell Atlas (SEA-AD) of the Allen Institute for Brain Science. Their use is subject to the Allen Institute Terms of Use (noncommercial research use) and Citation Policy. Commercial use needs the written permission of the Allen Institute.

The example data and the example figures in this README are derived from the SEA-AD data (SEA-AD portal), described in:


9. Citation

If you use cepip, please cite:

K.K. Wong. CEP-IP: An explainable framework for cell subpopulation identification in single-cell transcriptomics. Computer Methods and Programs in Biomedicine, 282 (2026) 109372. https://doi.org/10.1016/j.cmpb.2026.109372

If you use SKCS or CMRF, please also cite:

K.K. Wong. CEP-IP with CMRF in SEA-AD Microglia: A Mitochondrial Co-expression Module Linked to Cognition. bioRxiv (2026). https://www.biorxiv.org/content/10.64898/2026.10.22.761284v1

BibTeX (click to expand)
@article{Wong2026CEPIP,
  author  = {Wong, Kah Keng},
  title   = {{CEP-IP}: An explainable framework for cell subpopulation identification in single-cell transcriptomics},
  journal = {Computer Methods and Programs in Biomedicine},
  volume  = {282},
  pages   = {109372},
  year    = {2026},
  doi     = {10.1016/j.cmpb.2026.109372}
}

@article{Wong2026SEAAD,
  author  = {Wong, Kah Keng},
  title   = {{CEP-IP} with {CMRF} in {SEA-AD} Microglia: A Mitochondrial Co-expression Module Linked to Cognition},
  journal = {bioRxiv},
  year    = {2026},
  doi     = {10.64898/2026.10.22.761284},
  url     = {https://www.biorxiv.org/content/10.64898/2026.10.22.761284v1}
}