A multidimensional framework for macrophage profiling with MacrophageFramework

Introduction

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.

Demonstration dataset

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.

Input and clustering

analyze_macrophage_profile() accepts either:

  1. a Seurat object, from which expression data and cluster identities are extracted; or
  2. an expression matrix with genes in rows and cells in columns, accompanied by one cluster label per cell.

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  19

The 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.

Run MacrophageFramework

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] 735

Macrophage detection

When 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-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.

Manually specifying macrophage 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.

Multidimensional program profiles

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.

Program intensity (score)

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.221

A 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.

Program coverage

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.109

Program coherence

Coherence 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.951

The 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.156

Similarity between cluster profiles

The 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.000

Visual summaries

The 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.

Original computational clusters

The original clustering provides the computational context for macrophage detection and characterization.

Original computational clusters
Original computational clusters

Macrophage detection

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.

Macrophage detector
Macrophage detector

Multidimensional framework annotation

The framework represents selected clusters through combinations of lineage-associated and functional programs rather than forcing each cluster into a single activation category.

Framework annotation
Framework annotation

Program intensity

Program intensity
Program intensity

Program coverage

Program coverage
Program coverage

Program coherence

Program coherence
Program coherence

Similarity between cluster profiles

Profile similarity
Profile similarity

Alternative input: expression matrix and cluster assignments

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.

Outputs and interpretation

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:

str(demo_result, max.level = 1)

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.

Reproducibility and limitations

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.

Session information

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