CellMentor is a novel supervised cell type aware non-negative matrix factorization (NMF) method designed for enhanced cell type resolution in single-cell RNA sequencing analysis. By integrating cell type annotations into the NMF framework, CellMentor enables improved cell type separation, clustering, and annotation by leveraging latent patterns from reference datasets. Features
CellMentor package requires only a standard computer with enough RAM to support the in-memory operations.This package is supported for macOS, Linux and Windows. The package has been tested on the following systems:
CellMentor relies on several R packages, all of which are specified in the package DESCRIPTION file under the Imports field. These dependencies are installed automatically when you install CellMentor.
You can install CellMentor from BioConductor:
if (!require("BiocManager", quietly = TRUE)) {
install.packages("BiocManager")
}
BiocManager::install("CellMentor")
You can install CellMentor directly from GitHub using devtools:
if (!require("devtools")) install.packages("devtools")
devtools::install_github("petrenkokate/CellMentor", dependencies = TRUE)
Installation time: ~5 minutes on a standard desktop computer
(may vary depending on the number of dependencies to install).
Demo runtime: ~8–10 minutes on a standard desktop computer
(tested on macOS Sequoia 15.6.1 with R ≥ 4.3, 16 GB RAM).
Set random seed for reproducibility
set.seed(100)
Load required packages
library(CellMentor)
library(Matrix)
library(ggplot2)
library(dplyr)
library(Seurat)
library(scRNAseq)
# Loading reference dataset (Baron)
baron <- hBaronDataset()
reference_matrix <- baron$data
reference_celltypes <- baron$celltypes
# Loading query dataset (Muraro)
muraro <- muraro_dataset()
query_matrix <- muraro$data
query_celltypes <- muraro$celltypes # This would be unknown in a real application
# We keep it here for evaluation
# Function to create balanced subsets
create_subset <- function(matrix, celltypes, cells_per_type = 30) {
# Get unique cell types
unique_types <- unique(celltypes)
# Select cells for each type
selected_cells <- c()
for (cell_type in unique_types) {
# Get cells of this type
type_cells <- names(celltypes)[celltypes == cell_type]
# If fewer cells than requested, take all of them
n_to_select <- min(cells_per_type, length(type_cells))
# Randomly select cells
selected <- sample(type_cells, n_to_select)
selected_cells <- c(selected_cells, selected)
}
# Return subset
list(
matrix = matrix[, selected_cells],
celltypes = celltypes[selected_cells]
)
}
# Create balanced subsets with 30 cells per type
baron_subset <- create_subset(reference_matrix, reference_celltypes, 30)
muraro_subset <- create_subset(query_matrix, query_celltypes, 30)
# Update variable names for clarity
reference_matrix <- baron_subset$matrix
reference_celltypes <- baron_subset$celltypes
query_matrix <- muraro_subset$matrix
query_celltypes <- muraro_subset$celltypes # This would be unknown in a real application
# We keep it here for evaluation
csfnmf_obj <- CreateCSFNMFobject(
ref_matrix = reference_matrix,
ref_celltype = reference_celltypes,
data_matrix = query_matrix,
norm = TRUE,
most.variable = TRUE,
scale = TRUE,
scale_by = "cells",
verbose = TRUE,
num_cores = 1
)
Expected Output:
[11:45:56] Starting CSFNMF object creation
[11:45:59] Validating inputs
[11:45:59] Creating CSFNMF object
[11:45:59] Converting matrices to sparse format
[11:46:00] Setting up annotations
[11:46:00] Cleaning matrices
[11:46:00] Removed 3319 empty rows from reference matrix
[11:46:00] Removed 496 empty rows from query matrix
[11:46:00] Finding common genes
[11:46:00] Found 14904 common genes
[11:46:00] Normalizing data
[11:46:04] Selecting variable genes
[11:46:05] Selected 999 variable genes
[11:46:05] Scaling data by cells
[11:46:06] Encoding cell types
[11:46:06] Reordering data
[11:46:06] Validating final object
[11:46:06] CSFNMF object creation complete
The parameter selection process involves optimizing multiple hyperparameters to achieve the best cell type separation. Note that the function is computationally intensive and because of it we test only limited ranges of parameters.
These are parameter settings that work well across most datasets without extensive tuning.
# Find optimal parameters
optimal_params <- CellMentor(
csfnmf_obj,
alpha_range = c(1, 5), # Limited alpha range
beta_range = c(1, 5), # Limited beta range
gamma_range = c(0.1), # use only one gamma for speed
delta_range = c(1), # use only one delta for speed
num_cores = 5,
verbose = TRUE
)
# Get best model
best_model <- optimal_params$best_model
K_VALUE <- best_model@parameters$rank
Expected Output:
[11:46:11] Creating training object
[11:46:11] Determining optimal rank
[11:47:02] Optimal rank determined: 80
[11:47:02] Starting parameter grid search
[11:47:02] Testing configuration 1/4
[11:49:09] Testing configuration 2/4
[11:51:20] Testing configuration 3/4
[11:53:25] Testing configuration 4/4
[11:55:40] Training final model with best parameters on full dataset
[11:55:40] Initializing W and H matrices
[11:55:40] Calculating helper matrices
[11:55:40] Calculating alpha
[11:55:40] Calculating H constants
[11:55:47] Updating W and H matrices
Check best params
print(optimal_params$best_params)
Expected Output:
$k
[1] 80
$init_method
[1] "regulated"
$alpha
[1] 5
$beta
[1] 5
$gamma
[1] 0.1
$delta
[1] 1
# Project query data onto the learned space
h_project <- project_data(
W = best_model@W, # Learned gene weights
X = best_model@matrices@data, # Query data matrix
num_cores = 5,
verbose = TRUE
)
rownames(query_matrix) <- make.unique(rownames(query_matrix))
seu_muraro <- CreateSeuratObject(counts = query_matrix)
seu_muraro$celltype <- query_celltypes
# Add CellMentor dimensionality reduction to Seurat object
seu_muraro$CellMentor <- CreateDimReducObject(
embeddings = t(as.matrix(h_project)),
key = "CellMentor_",
assay = DefaultAssay(seu_muraro),
loadings = as.matrix(best_model@W)
)
# Visualization
seu_muraro <- RunUMAP(seu_muraro, reduction = 'CellMentor', dims= 1:K_VALUE)
DimPlot(seu_muraro, group.by = 'celltype')
This quick guide shows how to create a CSFNMF object from your data. After creating the object, follow the same steps as in the demo (parameter search with CellMentor(), projection with project_data(), optional Seurat integration).
ref_counts)ncol(ref_counts) (ref_celltypes), names must match colnames(ref_counts)
qry_counts), with overlapping gene IDsTip: rows = genes, columns = cells. Use sparse matrices (
Matrix::dgCMatrix) for speed/memory.
library(Matrix)
library(CellMentor)
# 1) Build CSFNMF object
csfnmf_obj <- CreateCSFNMFobject(
ref_matrix = ref_counts,
ref_celltype = ref_celltypes, # names(ref_celltypes) == colnames(ref_counts)
data_matrix = qry_counts,
norm = TRUE,
most.variable = TRUE,
scale = TRUE,
scale_by = "cells",
num_cores = 1,
verbose = TRUE
)
Proceed exactly as in the Demo section:
optimal <- CellMentor(csfnmf_obj, ...)
best_model <- optimal$best_model
h_project <- project_data(W = best_model@W, X = best_model@matrices@data)
All scripts used to generate the analyses and figures reported in the manuscript are openly available at petrenkokate/CellMentor_paper. The repository contains:
If you use CellMentor in your work, please cite:
CellMentor: Cell-Type Aware Dimensionality Reduction for Single-cell RNA-Sequencing Data
Or Hevdeli†, Ekaterina Petrenko†, Dvir Aran
bioRxiv 2025.06.17.660094
doi: https://doi.org/10.1101/2025.06.17.660094
† These authors contributed equally to this work.
This project is covered under the Apache 2.0 License.