Macrophage identity and functional state cannot always be captured by a single marker or a binary activation label. Lineage-associated features and functional programs can vary partly independently, and cells assigned to the same computational cluster may show heterogeneous expression patterns.
MacrophageFramework provides a multidimensional,
expression-based workflow that separates an initial macrophage-detection
step from the subsequent characterization of lineage-associated and
functional programs. The framework returns quantitative profiles and
comparison metrics rather than requiring each cluster to be assigned one
definitive biological label.
This vignette demonstrates the workflow using a small subset of a publicly available human breast cancer single-cell dataset (GEO accession GSE114725). The example illustrates the software workflow; it is not intended as a comprehensive reanalysis of the original study. The framework itself is not restricted to breast cancer.
The package includes a Seurat object containing 2,000 cells and 14,875 genes. The object includes expression data, cell metadata, cluster assignments, and a UMAP embedding.
demo_file <- system.file(
"extdata",
"GSE114725_demo.rds",
package = "MacrophageFramework"
)
if (!nzchar(demo_file) || !file.exists(demo_file)) {
stop("The demonstration Seurat object was not found in the installed package.")
}
demo_seurat <- readRDS(demo_file)
dim(demo_seurat)
#> [1] 14875 2000
colnames(demo_seurat[[]])
#> [1] "orig.ident" "nCount_RNA" "nFeature_RNA" "patient"
#> [5] "tissue" "replicate" "cluster" "cellid"
#> [9] "cell_key" "percent.mt" "RNA_snn_res.0.5" "seurat_clusters"
Seurat::Reductions(demo_seurat)
#> [1] "pca" "umap"
SeuratObject::Layers(demo_seurat[["RNA"]])
#> [1] "counts"The demonstration object is provided to make the vignette lightweight and reproducible. The original dataset can be retrieved from the Gene Expression Omnibus using accession GSE114725.
analyze_macrophage_profile() accepts either:
For a Seurat object, the function uses the active identities returned
by Seurat::Idents(object). Seurat objects may contain
several alternative clusterings in their metadata, so explicitly set the
intended clustering before running the framework. The cell names in the
identity vector are used to align cluster labels with the expression
matrix.
In this example, we explicitly select
seurat_clusters:
Seurat::Idents(demo_seurat) <- "seurat_clusters"
table(Seurat::Idents(demo_seurat))
#>
#> 0 1 2 3 4 5 6 7 8 9 10
#> 538 335 270 223 165 147 146 81 53 23 19The framework does not perform clustering or change the cluster assignments. The cluster is the primary unit for the summarized profiles, while cell-level values can be used to examine the distribution and heterogeneity of program scores within clusters.
The following call runs the analysis on the demonstration object.
layer = "counts" selects the RNA count layer, and
reduction = "umap" makes the existing UMAP coordinates
available to output routines that use an embedding. Setting
save_outputs = FALSE avoids writing files when building the
vignette; the results remain available in the returned R object.
demo_result <- analyze_macrophage_profile(
object = demo_seurat,
assay = "RNA",
layer = "counts",
reduction = "umap",
detect_macrophages = TRUE,
min_core_score = 0.1,
species = "Human",
save_outputs = FALSE
)
names(demo_result)
#> [1] "macrophage_detection" "profile" "similarity"
#> [4] "hierarchy" "scores" "detection"
#> [7] "coherence" "ontology" "annotations"
#> [10] "embedding" "analyzed_cells" "analyzed_clusters"
#> [13] "output_dir" "saved_files"
demo_result$analyzed_clusters
#> [1] "9" "3" "2" "8" "10" "5"
length(demo_result$analyzed_cells)
#> [1] 735When detect_macrophages = TRUE, the framework evaluates
a macrophage-associated core signature and uses the
min_core_score threshold to identify clusters for
downstream profiling. In this example, the threshold is set to
0.1. This is an operational parameter for screening
candidate clusters, not a universally validated biological cutoff;
results should be interpreted in the context of the dataset and
independently assessed where possible.
The detector output includes cluster-level metrics and a status indicating whether each cluster passed the inclusion criterion. The percentage of detectable cells and the core coverage/coherence metrics provide additional context, but they are not interchangeable with the core score.
detector_table <- demo_result$macrophage_detection$cluster_metrics
if (!is.null(detector_table)) {
knitr::kable(
detector_table[
order(as.numeric(as.character(detector_table$Cluster))),
c(
"Cluster",
"n_cells",
"macrophage_core_score",
"macrophage_core_coverage",
"macrophage_core_coherence",
"pct_detectable",
"Status"
)
],
digits = 3,
caption = "Cluster-level macrophage detector metrics for the demonstration dataset."
)
}| Cluster | n_cells | macrophage_core_score | macrophage_core_coverage | macrophage_core_coherence | pct_detectable | Status | |
|---|---|---|---|---|---|---|---|
| 8 | 0 | 538 | 0.007 | 0.003 | NA | 1.301 | excluded |
| 5 | 1 | 335 | 0.057 | 0.032 | 1.000 | 13.433 | excluded |
| 4 | 2 | 270 | 0.919 | 0.451 | 0.862 | 90.370 | included |
| 3 | 3 | 223 | 1.145 | 0.518 | 0.754 | 96.413 | included |
| 1 | 4 | 165 | 0.066 | 0.036 | 0.875 | 16.970 | excluded |
| 11 | 5 | 147 | 0.111 | 0.060 | 0.872 | 17.687 | included |
| 7 | 6 | 146 | 0.018 | 0.010 | NA | 4.795 | excluded |
| 6 | 7 | 81 | 0.050 | 0.027 | NA | 13.580 | excluded |
| 9 | 8 | 53 | 0.105 | 0.075 | 0.774 | 35.849 | included |
| 2 | 9 | 23 | 0.146 | 0.087 | 0.942 | 30.435 | included |
| 10 | 10 | 19 | 0.406 | 0.221 | 0.782 | 89.474 | included |
Only cells belonging to the included clusters are used for downstream
framework profiling. The full detector table is retained in
demo_result$macrophage_detection, allowing users to inspect
both included and excluded clusters.
If macrophage clusters have already been identified using independent
evidence, the detector can be bypassed by setting
detect_macrophages = FALSE and supplying their labels
through macrophage_clusters. The labels must match the
cluster identities in the object (or the clusters vector
when using a matrix).
manual_result <- analyze_macrophage_profile(
object = demo_seurat,
assay = "RNA",
layer = "counts",
reduction = "umap",
detect_macrophages = FALSE,
macrophage_clusters = c("2", "3", "5"),
species = "Human",
save_outputs = FALSE
)The labels in this example are illustrative of the demonstration
clustering. For a new dataset, replace them with cluster labels
justified by the user’s own analysis. In this mode,
macrophage_detection is NULL, because the
independent detector was not run.
After cluster selection, the framework summarizes expression across
the programs in the selected ontology. The default ontology is version
v0.4, which includes two lineage-associated programs
(Monocyte-associated and Resident-associated)
and five functional-state programs (Inflammatory,
Repair-associated, Lipid-associated,
ECM-remodeling, and IFN-responsive).
The profile is multidimensional: a cluster can show signals from several programs at once. Program scores should therefore be interpreted as quantitative expression-derived measures, not probabilities, mutually exclusive classes, or proof of cellular ontogeny.
The program score summarizes the expression signal associated with
the genes and weights defined for each program. The framework starts
from the supplied expression values, performs transcriptome-wide
library-size normalization with a scale factor of 10,000, applies
log1p, and calculates a weighted mean over the genes
belonging to each program.
dim(demo_result$profile$scores)
#> [1] 7 6
round(demo_result$profile$scores, 3)
#> 9 3 2 8 10 5
#> Monocyte-associated 1.680 0.281 0.633 0.047 0.290 0.192
#> Resident-associated 0.000 0.226 0.071 0.008 0.039 0.029
#> Inflammatory 0.102 0.117 0.363 0.100 0.000 0.025
#> Repair-associated 0.062 0.885 0.622 0.054 0.042 0.111
#> Lipid-associated 0.112 1.947 0.582 0.342 0.339 0.286
#> ECM-remodeling 0.557 2.607 0.752 0.632 0.813 0.310
#> IFN-responsive 0.134 0.828 0.490 0.268 0.396 0.221A higher score indicates a stronger expression-derived signal for that program under the framework’s scoring procedure. Scores are not probabilities and should not be interpreted as a direct estimate of the proportion of cells in a biological state.
Coverage summarizes the fraction of program genes detected, without applying the program weights. It complements intensity: two clusters can have similar scores but differ in how broadly the program genes are detected.
dim(demo_result$profile$coverage)
#> [1] 7 6
round(demo_result$profile$coverage, 3)
#> 9 3 2 8 10 5
#> Monocyte-associated 0.370 0.163 0.289 0.028 0.105 0.114
#> Resident-associated 0.000 0.154 0.055 0.005 0.026 0.019
#> Inflammatory 0.043 0.083 0.187 0.047 0.000 0.017
#> Repair-associated 0.022 0.466 0.304 0.038 0.026 0.061
#> Lipid-associated 0.053 0.668 0.265 0.193 0.175 0.169
#> ECM-remodeling 0.217 0.706 0.346 0.358 0.421 0.211
#> IFN-responsive 0.054 0.442 0.248 0.139 0.211 0.109Coherence summarizes pairwise agreement among observed genes within a
program, using the program gene weights. It is intended as a diagnostic
measure of the consistency of the observed expression pattern, not as an
independent validation of biological function. At least two observed
program genes are required; where this requirement is not met, the
coherence value is NA, not zero.
dim(demo_result$profile$coherence)
#> [1] 7 6
round(demo_result$profile$coherence, 3)
#> 9 3 2 8 10 5
#> Monocyte-associated 0.895 0.762 0.840 1.000 NA 1.000
#> Resident-associated NA 0.865 0.846 NA NA 1.000
#> Inflammatory 0.467 0.810 0.793 NA NA NA
#> Repair-associated NA 0.790 0.877 NA NA 0.914
#> Lipid-associated 1.000 0.782 0.834 0.853 0.853 1.000
#> ECM-remodeling 0.845 0.745 0.792 0.751 0.845 1.000
#> IFN-responsive 0.629 0.805 0.818 0.746 0.809 0.951The profile also contains coherence_IQR, which describes
the interquartile range of the cell-level coherence values for each
program and cluster:
round(demo_result$profile$coherence_IQR, 3)
#> 9 3 2 8 10 5
#> Monocyte-associated 0.118 0.254 0.203 0.000 NA 0.100
#> Resident-associated NA 0.238 0.181 NA NA 0.000
#> Inflammatory 0.000 0.000 0.184 NA NA NA
#> Repair-associated NA 0.238 0.196 NA NA 0.085
#> Lipid-associated 0.000 0.117 0.134 0.214 0.087 0.097
#> ECM-remodeling 0.083 0.173 0.194 0.107 0.034 0.152
#> IFN-responsive 0.000 0.125 0.150 0.190 0.081 0.156The similarity matrix compares clusters using cosine similarity between their program-intensity vectors. It describes resemblance between the multidimensional profiles; it does not establish that clusters are identical, should be merged, or share the same developmental origin.
dim(demo_result$similarity)
#> [1] 6 6
round(demo_result$similarity, 3)
#> 9 3 2 8 10 5
#> 9 1.000 0.373 0.659 0.375 0.574 0.604
#> 3 0.373 1.000 0.853 0.962 0.917 0.929
#> 2 0.659 0.853 1.000 0.811 0.837 0.937
#> 8 0.375 0.962 0.811 1.000 0.959 0.906
#> 10 0.574 0.917 0.837 0.959 1.000 0.936
#> 5 0.604 0.929 0.937 0.906 0.936 1.000The package includes precomputed figures from the demonstration workflow so that the vignette can show the visual outputs without requiring each plot to be regenerated during every build. These images are illustrative outputs bundled with the vignette; the code above separately runs the analysis and exposes its returned results.
The original clustering provides the computational context for macrophage detection and characterization.
The detector visualization summarizes which clusters were selected for downstream profiling. Detection is a computational screening step and should not be treated as a substitute for independent validation.
The framework represents selected clusters through combinations of lineage-associated and functional programs rather than forcing each cluster into a single activation category.
The matrix-based mode is useful when expression data and cluster
labels are already available outside a Seurat object. The matrix must
have genes as rows, cells as columns, gene names as row names, and cell
identifiers as column names. clusters must contain one
non-missing label per matrix column, in the same cell order.
The following example extracts the count matrix and matching identities from the demonstration object. It is not evaluated during the vignette build because it repeats the same analysis using the alternative input interface.
counts <- Seurat::GetAssayData(
demo_seurat,
assay = "RNA",
layer = "counts"
)
cluster_labels <- Seurat::Idents(demo_seurat)[colnames(counts)]
matrix_result <- analyze_macrophage_profile(
expression = counts,
clusters = cluster_labels,
detect_macrophages = TRUE,
min_core_score = 0.1,
species = "Human",
save_outputs = FALSE
)For non-Seurat inputs, the framework does not receive a dimensionality-reduction embedding through this interface. The matrix and cluster vector must be aligned correctly; the function checks that the number of cluster labels equals the number of cells and that gene and cell names are present.
The returned object is a list. Commonly used elements include:
macrophage_detection: detector metrics and
included/excluded clusters when detection is enabled; NULL
when detection is bypassed.profile: cluster-level program scores, coverage,
coherence, and coherence interquartile ranges.scores and coherence: cell-level program
score and coherence matrices.similarity: pairwise similarity between the profiled
cluster vectors.hierarchy: hierarchical relationships derived from
profile similarity.annotations: concise textual summaries generated from
the multidimensional profiles.embedding: embedding coordinates when requested and
available.analyzed_cells and analyzed_clusters:
cells and clusters retained for downstream profiling.ontology: the ontology used for the analysis.saved_files: paths returned by the output-saving
routine, when save_outputs = TRUE.The complete structure can be inspected with:
Interpret the quantitative profile as the primary result. Text annotations are secondary summaries and should not replace inspection of scores, coverage, coherence, cluster size, and the underlying biological context. The framework does not infer definitive developmental origin from lineage-associated expression programs; markers associated with recruitment, residency, or functional state may reflect overlapping biological processes.
The demonstration object and bundled figures are intended to keep the vignette lightweight. The example does not replace a complete analysis of GSE114725 or independent biological validation.
Results can depend on the expression layer supplied, the clustering used, the ontology version, species selection, and detector threshold. When applying the framework to a new dataset, users should document these choices, inspect detector metrics, and assess whether the included clusters are appropriate for the biological question. If clusters contain substantial internal heterogeneity, consider whether the original clustering resolution is suitable rather than interpreting a cluster-level summary as a uniform cellular state.
For a complete analysis of the original dataset, retrieve GSE114725, document the preprocessing and clustering decisions, and rerun the framework on the intended cell population.
sessionInfo()
#> R version 4.6.1 (2026-06-24)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Ubuntu 26.04.1 LTS
#>
#> Matrix products: default
#> BLAS: /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3
#> LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.32.so; LAPACK version 3.12.0
#>
#> locale:
#> [1] LC_CTYPE=en_US.UTF-8 LC_NUMERIC=C
#> [3] LC_TIME=en_US.UTF-8 LC_COLLATE=en_US.UTF-8
#> [5] LC_MONETARY=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8
#> [7] LC_PAPER=en_US.UTF-8 LC_NAME=C
#> [9] LC_ADDRESS=C LC_TELEPHONE=C
#> [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C
#>
#> time zone: Etc/UTC
#> tzcode source: system (glibc)
#>
#> attached base packages:
#> [1] stats graphics grDevices utils datasets methods base
#>
#> other attached packages:
#> [1] MacrophageFramework_0.99.0 rmarkdown_2.32
#>
#> loaded via a namespace (and not attached):
#> [1] deldir_2.0-4 pbapply_1.7-5 gridExtra_2.3.1
#> [4] rlang_1.3.0 magrittr_2.0.5 RcppAnnoy_0.0.23
#> [7] otel_0.2.0 spatstat.geom_3.8-3 matrixStats_1.5.0
#> [10] ggridges_0.5.7 compiler_4.6.1 png_0.1-9
#> [13] vctrs_0.7.3 reshape2_1.4.5 stringr_1.6.0
#> [16] pkgconfig_2.0.3 fastmap_1.2.0 promises_1.5.0
#> [19] purrr_1.2.2 xfun_0.62 cachem_1.1.0
#> [22] jsonlite_2.0.0 goftest_1.2-3 later_1.4.8
#> [25] spatstat.utils_3.2-5 irlba_2.4.1 parallel_4.6.1
#> [28] cluster_2.1.8.3 R6_2.6.1 ica_1.0-3
#> [31] spatstat.data_3.1-9 bslib_0.12.0 stringi_1.8.9
#> [34] RColorBrewer_1.1-3 reticulate_1.47.0 spatstat.univar_3.2-0
#> [37] parallelly_1.48.0 lmtest_0.9-40 jquerylib_0.1.4
#> [40] scattermore_1.2 Rcpp_1.1.2 knitr_1.52
#> [43] tensor_1.5.1 future.apply_1.21.0 zoo_1.9-1
#> [46] sctransform_0.4.3 httpuv_1.6.17 Matrix_1.7-6
#> [49] splines_4.6.1 igraph_2.3.4 tidyselect_1.2.1
#> [52] abind_1.4-8 yaml_2.3.12 spatstat.random_3.5-2
#> [55] codetools_0.2-20 miniUI_0.1.2 spatstat.explore_3.8-3
#> [58] listenv_1.1.0 lattice_0.23-1 tibble_3.3.1
#> [61] plyr_1.8.9 shiny_1.14.0 S7_0.2.2
#> [64] ROCR_1.0-12 evaluate_1.0.5 Rtsne_0.17
#> [67] future_1.80.0 fastDummies_1.7.6 survival_3.8-12
#> [70] polyclip_1.10-7 fitdistrplus_1.2-6 pillar_1.11.1
#> [73] Seurat_5.6.0 KernSmooth_2.23-27 plotly_4.12.1
#> [76] generics_0.1.4 RcppHNSW_0.7.0 sp_2.2-4
#> [79] ggplot2_4.0.3 scales_1.4.0 globals_0.20.0
#> [82] xtable_1.8-8 glue_1.8.1 maketools_1.3.2
#> [85] tools_4.6.1 sys_3.4.3 data.table_1.18.6.1
#> [88] RSpectra_0.16-2 RANN_2.6.3 buildtools_1.0.0
#> [91] dotCall64_1.2 cowplot_1.2.0 grid_4.6.1
#> [94] tidyr_1.3.2 nlme_3.1-171 patchwork_1.3.2
#> [97] cli_3.6.6 spatstat.sparse_3.2-0 spam_2.11-4
#> [100] viridisLite_0.4.3 dplyr_1.2.1 uwot_0.2.5
#> [103] gtable_0.3.6 sass_0.4.10 digest_0.6.39
#> [106] progressr_1.1.0 ggrepel_0.9.8 htmlwidgets_1.6.4
#> [109] SeuratObject_5.4.0 farver_2.1.2 htmltools_0.5.9
#> [112] lifecycle_1.0.5 httr_1.4.9 mime_0.13
#> [115] MASS_7.3-66