Estimates and compares heterogeneous gene conditional-dependence networks from single-cell expression data. A latent Gaussian mixture and probit observation mechanism represent cellular heterogeneity and zero observations. Protein interaction information supplies edge-specific prior weights, while network propensity matching links cell clusters across conditions and guides adaptation. Staged inference freezes stable matching, disables feedback, and retains baseline-model samples for posterior group and directional differential edge selection. Functions assess balance, record criterion diagnostics, and visualize within-condition and differential networks.
Version 1.0.6 estimates heterogeneous gene conditional-dependence networks and compares matched clusters across two conditions.
if (!requireNamespace("remotes", quietly = TRUE)) {
install.packages("remotes")
}
remotes::install_github("taostat1/scNPM_R_package", upgrade = "never")
library(scNPM)
packageVersion("scNPM")
This installs the current default branch. To reproduce a particular analysis,
replace the repository argument with "taostat1/scNPM_R_package@<commit-sha>",
using the full commit identifier for that analysis.
Download the GitHub source ZIP, or use Code > Download ZIP on the repository page. Unzip it, then install the extracted package directory:
remotes::install_local("path/to/scNPM_R_package-main", upgrade = "never")
The GitHub ZIP contains source code; it is not an R Windows binary package. For a separately supplied package archive, use:
# Built source archive
install.packages("scNPM_1.0.6.tar.gz", repos = NULL, type = "source")
# Windows binary archive, if supplied for your R version
# install.packages("scNPM_1.0.6.zip", repos = NULL, type = "win.binary")
Source installation requires R >= 4.0 and a compatible C++17 toolchain;
on Windows, use the Rtools release matching your R version.
remotes installs missing R dependencies. For local archives installed with
repos = NULL, install their dependencies first (listed in DESCRIPTION).
Restart R before replacing a loaded installation.
The package includes a demonstration subset of 50 genes and 400 cells: 200 peripheral (control) and 200 macular (treatment) retinal cells. This example compares retinal regions; the treatment label does not mean that all macular cells are diseased. The subset illustrates the workflow and does not reproduce the full-data results in the paper.
The complete script is inst/examples/amd_workflow.R. After installation, locate it with:
system.file("examples", "amd_workflow.R", package = "scNPM")
library(scNPM)
data("top50_goal_gene_mt", package = "scNPM")
expression <- as.matrix(goal_gene_mt)
stopifnot(identical(as.character(cell$Barcode), colnames(expression)))
stopifnot(all(cell$PR_or_MR %in% c("No", "Yes")))
qc_names <- c(
"scater_qc.all.total_features_by_counts",
"scater_qc.all.total_counts",
"scater_qc.endogenous.pct_counts",
"scater_qc.feature_control_MT.pct_counts",
"scater_qc.all.pct_counts_in_top_50_features"
)
cell_t <- cbind(
treatment = as.integer(cell$PR_or_MR == "Yes"),
as.matrix(cell[, qc_names, drop = FALSE])
)
colnames(cell_t) <- c("treatment", "detected_genes", "total_counts",
"endogenous_pct", "mitochondrial_pct", "top50_pct")
stopifnot(nrow(cell_t) == ncol(expression), all(is.finite(cell_t)))
ppi <- system.file("extdata", "top50_string_interactions_short.tsv",
package = "scNPM")
stopifnot(nzchar(ppi))
cell_t has one row per cell, in exactly the expression-column order. Its first
column is the 0/1 group indicator; the other five columns are measured QC
covariates. Supply these columns on their original scale: the propensity step
standardizes them across the pooled treatment and control cells. The eight
network-reference covariates for K = 4 are computed internally.
The PPI file supplies gene pairs and combined_score. In this implementation,
a positive score marks a supplied interaction; score magnitudes are not used
as continuous prior weights. Gene names must match the expression row names.
Use protein_input = NULL to use the common low baseline prior for all pairs.
fit <- run_scNPM(
goal_gene_mt = expression, cell_t = cell_t, protein_input = ppi, K = 4,
num_iter = 5000, num_save = 2000,
inference_mode = "staged", adapt_min = 500, adapt_max = 2000,
freeze_patience = 20, final_burnin = 1000,
network_covariates = "all_reference", matching_method = "lexicographic",
differential_method = "bayes_fdr", fdr_level = 0.05,
fdr_scope = "frozen", fdr_rule = "delta_threshold",
group_fdr_level = 0.05, diagnostic_every = 10, ds_seed = 123
)
The example retains iterations 3001--5000. Matching must freeze by iteration 2000, leaving at least 1000 baseline iterations before retention. If the matching does not stabilize, the function reports an error; increase the adaptation and total iteration budgets together rather than disabling the stability check. Runtime depends on the data, iteration count and hardware.
fit$matching_result_df # Reported pairs and final-state SMDs
fit$matching_objective # Maximum and total SMD for that map
fit$sampling_schedule # Freeze and retained-sample schedule
fit$group_fdr$summary # Separate BFDR for each group network
fit$differential_fdr$status
fit$differential_fdr$threshold # Strict |delta_p| > threshold
fit$differential_fdr$estimated_error_bound
head(fit$differential_fdr$selected)
compute_qf_smd(fit) # Each network feature, by matched pair
fit$smd_covariate_results # QC and network-feature balance
A row of differential_fdr$selected is one unordered gene pair in one matched
cluster comparison. direction = "treatment" or "control" names the enriched
condition, not a causal direction between the genes. delta_p is
p_treatment_only - p_control_only; error_bound is the estimated error used
in selection. The selected-set mean, rather than every individual row, is
required to be at most 0.05. An empty selection is a valid outcome; the returned
mean of zero for an empty set does not indicate evidence for differential edges.
out_dir <- "scNPM_example_output"
dir.create(out_dir, recursive = TRUE, showWarnings = FALSE)
write_csv <- function(x, name, row_names = FALSE) {
write.csv(x, file.path(out_dir, name), row.names = row_names)
}
saveRDS(fit, file.path(out_dir, "fit.rds"))
write_csv(fit$matching_result_df, "matching.csv")
write_csv(compute_qf_smd(fit), "network_feature_smd.csv")
write_csv(fit$smd_covariate_results, "qc_covariate_smd.csv", row_names = TRUE)
write_csv(fit$group_fdr$summary, "group_bfdr_summary.csv")
write_csv(fit$group_fdr$table, "group_all.csv")
write_csv(fit$group_fdr$selected, "group_selected.csv")
set.seed(123)
for (network_name in names(fit$group_fdr$networks)) {
net <- fit$group_fdr$networks[[network_name]]
write_csv(net$mask, paste0("group_", network_name, "_mask.csv"), row_names = TRUE)
if (!any(net$mask)) next
p <- plot_group_network(net)
if (!is.null(p)) ggplot2::ggsave(
file.path(out_dir, paste0("group_", network_name, ".pdf")),
plot = p, width = 7, height = 5
)
}
write_csv(fit$differential_fdr$table, "differential_all.csv")
write_csv(fit$differential_fdr$selected, "differential_selected.csv")
write_csv(fit$differential_fdr$threshold_curve, "bfdr_threshold_curve.csv")
write_csv(fit$stage_history, "stage_history.csv")
write_csv(data.frame(iteration = seq_len(nrow(fit$matching_history)),
fit$matching_history), "matching_history.csv")
write_csv(fit$criterion_trace, "criterion_trace.csv")
maps <- fit$matching_history
mapping_changes <- if (nrow(maps) < 2L) 0L else
sum(rowSums(maps[-1, , drop = FALSE] !=
maps[-nrow(maps), , drop = FALSE]) > 0)
write_csv(data.frame(
runtime_minutes = as.numeric(fit$runtime, units = "mins"),
frozen_at = fit$sampling_schedule$frozen_at,
adaptation_iterations = sum(fit$stage_history$stage == "adaptation"),
mapping_changes = mapping_changes,
retained_draws = fit$sampling_schedule$retained_draws,
cutoff = fit$differential_fdr$threshold,
selected_edges = nrow(fit$differential_fdr$selected),
estimated_BFDR = fit$differential_fdr$estimated_error_bound,
selection_status = fit$differential_fdr$status
), "run_summary.csv")
trace <- fit$criterion_trace
if (sum(is.finite(trace$J_aug)) >= 2L) {
pdf(file.path(out_dir, "criterion_trace.pdf"), width = 7, height = 4)
plot(trace$iteration, trace$J_aug, type = "l",
xlab = "Iteration", ylab = "Diagnostic criterion")
abline(v = fit$sampling_schedule$frozen_at, lty = 2, col = "grey50")
dev.off()
}
set.seed(123) # Reproducible plotting layouts
for (pair_name in names(fit$edge_diff_networks)) {
net <- fit$edge_diff_networks[[pair_name]]
if (net$solid_network$count + net$dashed_network$count == 0L) next
p <- plot_diff_network(net$diff_matrix, fit$gene_names,
output_mode = "combined")
if (!is.null(p)) ggplot2::ggsave(
file.path(out_dir, paste0("differential_", pair_name, ".pdf")),
plot = p, width = 7, height = 5
)
}
writeLines(capture.output(sessionInfo()), file.path(out_dir, "sessionInfo.txt"))
For each matched pair, the script saves a combined differential network PDF
when selected edges exist. Solid edges denote treatment enrichment and dashed
edges denote control enrichment. The original diff_matrix carries the BFDR
selection masks; passing it directly preserves the selected set.
Group-network plots use their saved BFDR masks:
plot_group_network(fit$group_fdr$networks$treatment_1)
plot_group_network(fit$group_fdr$networks$control_1)
fit$group_fdr$summary
For each condition and subtype, the selection ranks the marginal Rao-Blackwell
slab probabilities and retains the largest complete tie-block prefix whose
mean 1 - probability is at most group_fdr_level (default 0.05). Inclusion
uses probability >= cutoff. Each of the eight networks for K = 4 has its own
BFDR calculation. Selection takes place on the full panel; use the saved mask
when extracting subnetworks or calculating gene-panel sensitivity.
edge_treat_est and edge_ctrl_est remain empirical allocation-frequency
arrays for compatibility. They are not the new BFDR masks. A raw matrix passed
to plot_group_network() without a saved mask raises an informative error.
The former descriptive plot remains available explicitly:
plot_group_network(fit$edge_treat_est[, , 1], fit$gene_names,
selection = "frequency", frequency_threshold = 0.5)
Existing staged 1.0.5 results that contain joint_edge_probabilities can be
updated without resampling:
# fit <- readRDS("previous_fit.rds")
fit$group_fdr <- select_group_edges(fit, fdr_level = 0.05)
plot_group_network(fit$group_fdr$networks$treatment_1)
The selector reconstructs the marginal means using p10 + p11 for treatment
and p01 + p11 for control, checks consistency across counterparts, and does
not substitute empirical allocation frequencies for those probabilities.
File in scNPM_example_output/ |
Contents |
|---|---|
fit.rds |
Complete result, including selection attributes and provenance; reload with readRDS(). |
matching.csv |
One-to-one cluster correspondence and final-state SMD. |
network_feature_smd.csv, qc_covariate_smd.csv |
Network-feature balance and the combined QC/network covariate balance matrix. |
group_bfdr_summary.csv, group_selected.csv, group_all.csv |
Per-network cutoffs and BFDR, selected edges and the full marginal edge table. |
group_treatment_*.pdf, group_control_*.pdf |
BFDR-selected group networks, when nonempty. |
group_treatment_*_mask.csv, group_control_*_mask.csv |
Saved symmetric selection masks, including empty networks. |
differential_all.csv |
All tested gene-pair/cluster items, their probabilities, contrasts, errors and selection flags. |
differential_selected.csv |
Only selected differential associations. |
bfdr_threshold_curve.csv |
Candidate cutoffs, edge counts, mean errors and the chosen cutoff. |
run_summary.csv |
Runtime, freeze iteration, adaptation count, mapping changes, selected cutoff and estimated BFDR. |
stage_history.csv, matching_history.csv, criterion_trace.csv |
Iteration-level stages and matching, plus recorded criterion components. |
criterion_trace.pdf |
Diagnostic criterion against iteration. |
differential_treat_*_ctrl_*.pdf |
Selected differential network for each nonempty matched comparison. |
sessionInfo.txt |
R session and dependency versions. |
The function returns a list in memory; these files are written by the example script. See the field reference below for parameter and network summaries.
run_scNPM() is quiet by default (verbose = FALSE). Use verbose = TRUE
for initialization, sampler progress and elapsed-time messages. All of these,
including C++ progress, use R message conditions and can also be suppressed
with suppressMessages(). Warnings and errors retain their normal behavior.
Runtime, dimensions, stages and diagnostic values are returned in the result.
freeze_patience consecutive identical
assignments, no earlier than adapt_min. Every cluster must be nonempty at
these checks. Stop with an error if this has not occurred by adapt_max.final_burnin iterations under this model before retaining the last
num_save draws. The frozen matching remains the reported correspondence.The final sampling target is the baseline model without counterpart feedback. Matching affects adaptation and the reported comparisons; it does not keep altering the final network prior. Re-run analyses when moving from 1.0.3.
The observation model is unchanged: positive values fix latent expression at log X; zero values have sampled latent expression. HMC uses fixed step sizes, fixed leapfrog counts and Metropolis acceptance correction.
For every cell in either condition, the default uses centered distances to all 2K current reference networks, ordered T1,...,TK,C1,...,CK:
$$q_{i,r}^{d;s}=(\theta_i^d-\mu_r^s)^\top\Omega_r^s(\theta_i^d-\mu_r^s),\quad s\in{T,C}.$$
K = 4 therefore uses eight network covariates. These and the optional QC
covariates enter the logistic propensity model. Each column is standardized
using its pooled mean and sample standard deviation over both conditions.
Constant columns become zero. propensity_ridge = 1e-6 stabilizes IRLS.
network_covariates = "legacy" requests the original assigned-network,
uncentered quadratic form. Inspect both propensity-score and feature SMDs.
goal_gene_mt is a finite, nonnegative gene-by-cell matrix on the original
expression scale, with unique gene names. Rows of cell_t follow the cell
columns: its first column codes control 0 and treatment 1; remaining numeric
columns are covariates. protein_input accepts a data frame, TSV text or a
local TSV file with node1, node2 and combined_score, or NULL.
The complete retinal example above constructs these inputs and runs the staged analysis.
Budgets satisfy adapt_min <= adapt_max and
adapt_max + final_burnin <= num_iter - num_save. Earlier freezing gives a
longer actual final burn-in; it does not move the retained window. Defaults
resolve from this available pre-retention budget; see ?run_scNPM. Stability
of the matching and completion of a burn-in budget are separate from MCMC
convergence. Examine multiple chains and the retained diagnostic traces.
For a treatment cluster k and an unordered gene pair, a treatment-direction claim means slab allocation in k and spike allocation in its control counterpart. A control-direction claim means the reverse. These are continuous spike-and-slab states, not exact zero precision entries or gene absence.
The package averages within-draw conditional products for all K x K pairs. It does not multiply marginal edge-frequency estimates. The more supported direction under the frozen pairing supplies the reported direction.
fdr_scope = "frozen" (default): use errors for the frozen counterpart.fdr_scope = "all_matchings": sensitivity analysis using the largest error
over all K control counterparts for the chosen direction.The default fdr_rule = "delta_threshold" computes delta_p = p10 - p01,
then scans every distinct strict cutoff abs(delta_p) > tau. Equal contrasts
are never split. For each whole threshold set, average its directional errors
1 - max(p10,p01) (or the all-counterpart bound). Choose the set with the most
edges among those whose average is at most fdr_level. These averages need not
be monotone in contrast order; the scan does not stop at the first failure.
fit$differential_fdr$threshold records tau; threshold_curve records every
candidate's size and mean error. Infinity selects no edges. The cutoff is the
next excluded score, or zero if all nonzero scores are included; the operator
is strictly >. The plot uses selection masks and does not apply +/-0.5 again.
fdr_rule = "posterior_error" retains the error-sorted prefix from 1.0.4.
There is one direction per unordered edge and treatment cluster, with all K
clusters sharing one budget. Below fdr_min_draws (default 200), return the
exploratory curve but no threshold discoveries. This minimum is not a mixing
guarantee. The batch MCSE is a diagnostic, not a confidence bound.
This is an estimated posterior expected directional FDP, conditional on the baseline model and label convention. A target of 0.05 is not a uniform frequentist 5% guarantee across fixed network configurations, donors, model misspecification or incomplete mixing. The supplied simulation report evaluates this distinction explicitly. Donor dependence is not modeled by adding a donor column to the propensity model.
conditional <- select_differential_edges(fit, scope = "frozen")
conservative <- select_differential_edges(fit, scope = "all_matchings")
criterion_trace records log_augmented_likelihood, network_prior_kernel,
matching_loss and their combination J_aug = likelihood + prior - loss.
The likelihood includes sampled missing latent expression and component labels;
it is not the integrated observed-data likelihood. The prior column is a
state-wise network factor, not the complete posterior density. SMDs are
recomputed against the frozen map after the switch. Undefined SMDs produce NA
loss/criterion values. Changes in stages also change the prior factor.
This trace describes interactions between modules. It does not define a joint optimization objective. Random draws can decrease any of these values; no updates are rejected merely because J_aug decreases. Global matching optimizes (maximum SMD, total SMD) for the current cost matrix, not a common objective across sampling iterations. Freezing preserves that map as costs change.
Use inference_mode = "coupled", differential_method = "frequency", matching_method = "greedy" for the
1.0.3 continuous-feedback algorithm. Its +/-0.5 selections are descriptive.
Posterior error selection rejects coupled-mode results. Old and new draws
must not be pooled. K=4 still has four clusters per condition, not eight.
The returned list contains the following commonly used results:
| Field | Contents |
|---|---|
K, G, N, gene_names |
Clusters per condition, gene count, cell count and gene order. |
runtime |
Sampler runtime as a difftime value in minutes; excludes later export and plotting. |
edge_treat_est, edge_ctrl_est |
G x G x K retained slab-allocation frequency arrays. |
mu_treat_est, mu_ctrl_est |
Retained component-mean summaries. |
invcov_treat_est, invcov_ctrl_est |
Retained precision-matrix summaries. |
theta_treat_est, theta_ctrl_est, theta_est |
Retained latent-expression summaries for cells. |
group_treat_est, group_ctrl_est, group_est |
Cell cluster assignments. |
matching_result_df, match_map, smd_results |
Reported correspondence and final-state propensity-score balance. |
smd_covariate_results, network_covariates |
Balance of all QC/network covariates and the network-reference values used in matching. |
edge_diff_networks |
One entry per matched pair, containing the contrast matrix, treatment/control edge lists and counts. |
provenance |
Version, seed, settings, timestamps, thread information and input fingerprints. |
Result |
Retained sampler output and trace summaries; not a copy of every state from every iteration. |
Staged inference and selection also return:
| Field | Contents |
|---|---|
sampling_schedule |
Freeze iteration, minimum/actual final burn-in and retained interval. |
stage_history |
Per-iteration phase and feedback status. |
matching_history |
Per-iteration map, fixed after freezing. |
criterion_trace |
Augmented criterion components at recorded iterations. |
joint_edge_probabilities |
p00, p10, p01, p11 for every cluster pair, empirical checks and batch means. |
group_fdr |
Marginal probabilities, per-network masks, summary, selected/all edge tables and threshold curves; NULL in coupled mode. |
differential_fdr |
Edge table, threshold, full threshold curve, selections, target, scope and status. |
smd_cost_matrix, frozen_smd_cost_matrix |
All pair costs at the final state and at the freezing iteration. |
matching_objective |
Final-state maximum/total SMD under the reported map; frozen flag. |
edge_diff_networks and plot_diff_network() use the selected differential
edges in Bayesian mode. Keep the scNPM_selection attribute on the returned
diff_matrix: the plotting function reads these masks directly. Plain matrices
without that attribute retain the descriptive +/-0.5 plotting behavior.
match_map and matching_result_df use the frozen map,
while SMDs refer to the final sampled state under that map. The main edge
summaries are retained allocation frequencies; the new joint probabilities
use Rao-Blackwellization. Raw draws remain in Result.
The baseline PPI inclusion weight is
ppi_prior_low + (ppi_prior_high - ppi_prior_low) * protein_adjacency, with
defaults 0.05 and 0.5. It is used at every edge update, including the final
phase. The SPD matrix constraint is part of the joint network prior.
For formulae, assumptions and the error-bound proof, read algorithm.md. This file is also installed in the package directory. Development repository: taostat1/scNPM_R_package. License: GPL >= 3.