Repository: AlexsLemonade/scpca-nf
Pull request #864: Add cell and read count reporting to change tracking notebook
Author: login: jashapiro
Created: 2025-05-02T20:30:56Z | Merged: 2025-05-06T16:32:37Z

Here I am adding the cell count change summaries and detail table to the change report. This is the first half of #853 (but does not fully close it)

There was a bit of error correction I had to do because we are using unboxed JSON for the metrics file, which seems to be causing a bit of a challenge with NA values. But maybe only sometimes? The issue is that we have a few NA values for miQC pass values, which are getting read in as strings. In my testing if we were to export without `auto_unbox`, this would go away. So I added that change for some simplification of reading the file in the future, but for now I had to deal with what we have so there is some added code to be sure the NA values are treated appropriately and the columns turned back into simple vectors where needed. I did pull that out to a separate function at least, which makes other parts a bit simpler anyway.

The remaining code is pretty straightforward as far as building tables goes, I think. The most challenging part was dealing with NA values: we do want to report if a value changed to/from NA, so there is a bit of code I had to add to every `filter()`step to make sure those are retained.  I also added some code in the DT template (adapted from https://github.com/rstudio/DT/issues/322) to make sure they show up as `NA` rather than a blank field in the table. 

I followed the same general plan of attack as before with dynamic text.

I don't know for sure that the plots I made are exactly what we will want to end up with, but it is a bit hard to tell with the number of changes that we have at the moment what those will really look like with real data. So I did not spend too much time on the formatting, but if you have suggestions there, do feel free to make them!




Diff:
diff --git a/bin/sce_metrics.R b/bin/sce_metrics.R
index 7c0b3f3ba..20c0ac51c 100755
--- a/bin/sce_metrics.R
+++ b/bin/sce_metrics.R
@@ -115,9 +115,17 @@ if (file.size(opt$filtered_sce) > 0) {
   metrics$filtered_total_spliced <- total_spliced(filtered_sce)
   metrics$filtered_expressed_genes <- sum(rowSums(counts(filtered_sce)) > 0)
   metrics$filtered_altexp_total <- altexp_totals(filtered_sce)
-  metrics$miqc_pass_count <- sum(filtered_sce$miQC_pass)
+  metrics$miqc_pass_count <- ifelse(
+    is.null(filtered_sce$miQC_pass),
+    NA_integer_,
+    sum(filtered_sce$miQC_pass)
+  )
   metrics$scpca_filter_count <- sum(filtered_sce$scpca_filter == "Keep")
-  metrics$adt_scpca_filter_count <- sum(filtered_sce$adt_scpca_filter == "Keep")
+  metrics$adt_scpca_filter_count <- ifelse(
+    is.null(filtered_sce$adt_scpca_filter),
+    NA_integer_,
+    sum(filtered_sce$adt_scpca_filter == "Keep")
+  )
 
   rm(filtered_sce)
 }
@@ -134,8 +142,16 @@ if (file.size(opt$processed_sce) > 0) {
   metrics$cluster_algorithm <- metadata(processed_sce)$cluster_algorithm
   # cluster counts as unnamed vector
   metrics$cluster_sizes <- as.vector(table(processed_sce$cluster))
-  metrics$singler_reference <- ifelse(is.null(metadata(processed_sce)$singler_reference), NA_character_, metadata(processed_sce)$singler_reference)
-  metrics$cellassign_reference <- ifelse(is.null(metadata(processed_sce)$cellassign_reference), NA_character_, metadata(processed_sce)$cellassign_reference)
+  metrics$singler_reference <- ifelse(
+    is.null(metadata(processed_sce)$singler_reference),
+    NA_character_,
+    metadata(processed_sce)$singler_reference
+  )
+  metrics$cellassign_reference <- ifelse(
+    is.null(metadata(processed_sce)$cellassign_reference),
+    NA_character_,
+    metadata(processed_sce)$cellassign_reference
+  )
   # convert celltype annotation counts to named lists
   metrics$singler_celltypes <- as.list(table(processed_sce$singler_celltype_ontology))
   metrics$cellassign_celltypes <- as.list(table(processed_sce$cellassign_celltype_annotation))
@@ -145,6 +161,5 @@ if (file.size(opt$processed_sce) > 0) {
 jsonlite::write_json(
   metrics,
   path = opt$metrics_json,
-  auto_unbox = TRUE,
   pretty = TRUE
 )
diff --git a/scripts/compare-metrics/compare-metrics-template.rmd b/scripts/compare-metrics/compare-metrics-template.rmd
index c0f4b8fb4..cdf8efef1 100644
--- a/scripts/compare-metrics/compare-metrics-template.rmd
+++ b/scripts/compare-metrics/compare-metrics-template.rmd
@@ -33,6 +33,22 @@ set.seed(1234)
 # Functions
 #| include: false
 
+
+read_metrics_files <- function(urls) {
+  # read a set of metrics files
+  # url: URL of the metrics files, each in json format
+  # returns: data frame (tibble) of combined metrics
+  purrr::map(urls, \(url) {
+    jsonlite::read_json(url, simplifyVector = TRUE) |>
+      # handle bare NAs, which may be quoted
+      purrr::modify_if(\(x){
+        length(x) == 1 && x == "NA"
+      }, ~NA)
+  }) |>
+    purrr::list_transpose(default = NA) |>
+    tibble::as_tibble()
+}
+
 report_table <- function(df, options = list(), ...) {
   # pass standard settings for report to DT::datatable
   # df: data frame to display
@@ -41,10 +57,21 @@ report_table <- function(df, options = list(), ...) {
   default_opts <- list(
     scroller = TRUE,
     deferRender = TRUE,
+    scrollX = TRUE,
     scrollY = 400,
     scrollCollapse = TRUE,
-    language = list(search = "Filter:")
+    language = list(search = "Filter:"),
+    columnDefs = list(list(
+      # render missing values as "NA" rather than blanks in the table
+      targets = "_all",
+      render = DT::JS(
+        "function(data, type, row, meta) {",
+        "return data === null ? 'NA' : data;",
+        "}"
+      )
+    ))
   )
+
   if (length(options) > 0) {
     # merge default options with user options
     opts <- modifyList(default_opts, options)
@@ -88,17 +115,8 @@ comp_metrics_paths <- s3fs::s3_dir_ls(
 ```{r, eval=!use_cache}
 #| include: false
 # read metrics files from S3
-ref_data <- purrr::map(ref_metrics_files, \(url) {
-  jsonlite::read_json(url, simplifyVector = TRUE)
-}) |>
-  purrr::list_transpose(default = NA) |>
-  tibble::as_tibble()
-
-comp_data <- purrr::map(comp_metrics_paths, \(url) {
-  jsonlite::read_json(url, simplifyVector = TRUE)
-}) |>
-  purrr::list_transpose(default = NA) |>
-  tibble::as_tibble()
+ref_data <- read_metrics_files(ref_metrics_files)
+comp_data <- read_metrics_files(comp_metrics_paths)
 
 # join the two data frames
 metrics_df <- dplyr::full_join(
@@ -254,38 +272,152 @@ report_table(
 
 ### Cell count changes {.tabset}
 
-For each sample, we will want to first compare:
+```{r}
+# calculate the difference between reference and comparison for selected fields
+
+cell_fields <- c(
+  "unfiltered_cells",
+  "filtered_cells",
+  "processed_cells",
+  "miqc_pass_count",
+  "scpca_filter_count",
+  "adt_scpca_filter_count"
+)
 
-- `unfiltered_cells`
-- `filtered_cells`
-- `processed_cells`
-- `miqc_pass_count`
-- `scpca_filter_count`
-- `adt_scpca_filter_count`
+cell_changes <- metrics_df |>
+  tidyr::drop_na(starts_with("mapped_reads")) |> # remove added/removed libraries
+  dplyr::select(
+    project_id,
+    sample_id,
+    library_id,
+    starts_with(cell_fields)
+  ) |>
+  tidyr::pivot_longer(
+    cols = starts_with(cell_fields),
+    names_pattern = "(.+)\\.(ref|comp)$",
+    names_to = c("field", ".value") # .value separates into `ref` and `comp` fields
+  ) |>
+  dplyr::filter(is.finite(comp) | is.finite(ref)) |> # keep where at least one is finite
+  dplyr::mutate(
+    change = comp - ref,
+    change_frac = change / ref
+  )
 
-If there are changes in any of those, we will report those, with prominent warnings for any large changes.
-What defines a large change?
-Perhaps start with a change > 0.5% of the total?
-Alternatively, if we see a large fraction of samples changing, we would want to note that.
-If there are any changes to the number of unfiltered cells, that should be highlighted, as we do not expect this to change between runs.
+changed_cell_counts <- cell_changes |>
+  dplyr::filter(is.na(change) | change != 0)
 
-#### Summary
-The main display item would be a summary table, in the first tab:
+has_cell_changes <- nrow(changed_cell_counts) > 0
+```
 
-| Metric | Number of samples changed | % of samples changed | Mean % change |
-| ----- | --------- | --------- | ---------------- |
-| `scpca_filter_count` | 100 | 20 | -10 |
+```{r}
+# summary text for cell count section
+if (!has_cell_changes) {
+  knitr::asis_output(
+    "There are no changes in cell counts between the reference and comparison runs."
+  )
+} else {
+  n_projects <- dplyr::n_distinct(changed_cell_counts$project_id)
+  n_libraries <- dplyr::n_distinct(changed_cell_counts$library_id)
 
-We will also include a plot with a histogram of the % changes for each metric where at least one sample changed.
+  glue::glue("
+    The tables below show changes in cell counts between the reference and comparison workflow runs at various stages of processing.<br>
+    There {ifelse(n_added == 1, 'is', 'are a total of')} **{n_libraries}** {ifelse(n_libraries == 1, 'library', 'libraries')} with cell count changes
+    from **{n_projects}** {ifelse(n_projects == 1, 'project', 'projects')}.
+  ") |>
+    knitr::asis_output()
+}
+```
 
+```{r}
+# warn if any unfiltered cell counts have changed
+unfiltered_cell_changes <- cell_changes |>
+  dplyr::filter(field == "unfiltered_cells", change != 0) |>
+  nrow()
+
+
+if (unfiltered_cell_changes > 0) {
+  knitr::asis_output(glue::glue("
+    <div class=\"alert alert-danger\">
+    **Warning**: Unfiltered cell counts have changed between the reference and comparison runs.
+    **{unfiltered_cell_changes}** {ifelse(unfiltered_cell_changes == 1, 'library is', 'libraries are')} affected.
+    </div>
+  "))
+}
+```
+
+```{r, eval=has_cell_changes}
+knitr::asis_output("#### Summary")
+```
 
-#### Detail
+```{r}
+# Summary table of cell count changes
+
+cell_summary <- cell_changes |>
+  dplyr::summarise(
+    .by = c("field"),
+    # count where at least one version has a value
+    n = dplyr::n(),
+    n_changed = sum(is.na(change) | change != 0),
+    library_change_frac = n_changed / n,
+    mean_change_frac = mean(abs(change_frac[which(change != 0)]))
+  )
+
+report_table(
+  cell_summary,
+  colnames = c("Metric", "Libraries", "Number of libraries changed", "% of libraries changed", "Mean % change")
+) |>
+  DT::formatPercentage(c("library_change_frac", "mean_change_frac"), 2)
+```
+
+
+```{r}
+change_fields <- cell_summary |>
+  dplyr::filter(n_changed > 0) |>
+  dplyr::pull(field)
+
+cell_plot_df <- cell_changes |>
+  dplyr::filter(field %in% change_fields, change != 0) |>
+  dplyr::mutate(
+    field = factor(field, levels = cell_fields),
+  )
+
+ggplot(cell_plot_df, aes(x = change_frac)) +
+  geom_histogram(bins = 20) +
+  facet_wrap(vars(field), ncol = 3) +
+  labs(
+    title = "Histogram of changes in cell counts",
+    x = "Change proportion",
+    y = "Number of samples"
+  ) +
+  theme_bw()
+```
 
-A detail table of changed samples/metrics might then look like the following:
+```{r, eval=has_cell_changes}
+knitr::asis_output("#### Detail")
+```
+
+```{r}
+# Detail table of cell count changes
+
+cell_detail <- changed_cell_counts |>
+  dplyr::select(
+    project_id,
+    sample_id,
+    library_id,
+    field,
+    ref,
+    comp,
+    change,
+    change_frac
+  )
+
+report_table(
+  cell_detail,
+  colnames = c("Project", "Sample", "Library", "Metric", "Reference", "Comparison", "Change", "% change")
+) |>
+  DT::formatPercentage("change_frac", 2)
+```
 
-| Sample | Metric | Reference Value | Comparison Value | Change | % change |
-| ------- | ---------------- | --------------- | ---------------- | ------ | -------- |
-| `SCPCS999999` | `unfiltered_cells` | 1000 | 1005 | 5 | 0.5 |
 
 
 ### Read count changes {.tabset}


Reviews:
user: login: allyhawkins
body: Thanks for attaching the rendered report! I had some questions and suggestions about the table that I think should be addressed. 

I think the histograms are good for now and we can always revisit them if we don't feel like they are actually helpful when looking at the real reports. This is what I had envisioned though when we talked about plots in the outline. 
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T16:06:27Z
user: login: jashapiro
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T20:04:58Z
user: login: jashapiro
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T20:06:49Z
user: login: jashapiro
body: Thanks for your comments Ally!  I think I got them all, but let me know if there was anything else I missed. 

The one extra change was to make the `adt_scpca_filter_count` `NA` if there is not an ADT table. (Apparently `sum(NULL == "foo")` yields `0`, which is not what I guess I expected). Seeing the effects of this will require a rerun of the workflow, but it should mean that the library counts in the summary will not all be the same (and yes, they should be library counts).

I think I fixed the scrolling bug too: the updated file is attached.

[compare-metrics.html.zip](https://github.com/user-attachments/files/20049201/compare-metrics.html.zip)
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T21:22:16Z
user: login: allyhawkins
body: This looks good, but I still think there's something off with the mean % change. I think it might be how you're using `DT::formatPercentage`. 
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T15:33:45Z
user: login: jashapiro
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T15:47:16Z
user: login: allyhawkins
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T15:49:02Z
user: login: jashapiro
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T15:54:59Z
user: login: jashapiro
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T15:56:46Z
user: login: jashapiro
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T16:08:18Z
user: login: allyhawkins
body: LGTM!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-06T16:12:40Z

Review comments:
diff_hunk: @@ -254,38 +271,152 @@ report_table(
 
 ### Cell count changes {.tabset}
 
-For each sample, we will want to first compare:
+```{r}
+# calculate the difference between reference and comparison for selected fields
+
+cell_fields <- c(
+  "unfiltered_cells",
+  "filtered_cells",
+  "processed_cells",
+  "miqc_pass_count",
+  "scpca_filter_count",
+  "adt_scpca_filter_count"
+)
 
-- `unfiltered_cells`
-- `filtered_cells`
-- `processed_cells`
-- `miqc_pass_count`
-- `scpca_filter_count`
-- `adt_scpca_filter_count`
+cell_changes <- metrics_df |>
+  tidyr::drop_na(starts_with("mapped_reads")) |> # remove added/removed libraries
+  dplyr::select(
+    project_id,
+    sample_id,
+    library_id,
+    starts_with(cell_fields)
+  ) |>
+  tidyr::pivot_longer(
+    cols = starts_with(cell_fields),
+    names_pattern = "(.+)\\.(ref|comp)$",
+    names_to = c("field", ".value") # .value separates into `ref` and `comp` fields
+  ) |>
+  dplyr::filter(is.finite(comp) | is.finite(ref)) |> # keep where at least one is finite
+  dplyr::mutate(
+    change = comp - ref,
+    change_frac = change / ref
+  )
 
-If there are changes in any of those, we will report those, with prominent warnings for any large changes.
-What defines a large change?
-Perhaps start with a change > 0.5% of the total?
-Alternatively, if we see a large fraction of samples changing, we would want to note that.
-If there are any changes to the number of unfiltered cells, that should be highlighted, as we do not expect this to change between runs.
+changed_cell_counts <- cell_changes |>
+  dplyr::filter(is.na(change) | change != 0)
 
-#### Summary
-The main display item would be a summary table, in the first tab:
+has_cell_changes <- nrow(changed_cell_counts) > 0
+```
 
-| Metric | Number of samples changed | % of samples changed | Mean % change |
-| ----- | --------- | --------- | ---------------- |
-| `scpca_filter_count` | 100 | 20 | -10 |
+```{r}
+# summary text for cell count section
+if (!has_cell_changes) {
+  knitr::asis_output(
+    "There are no changes in cell counts between the reference and comparison runs."
+  )
+} else {
+  n_projects <- dplyr::n_distinct(changed_cell_counts$project_id)
+  n_libraries <- dplyr::n_distinct(changed_cell_counts$library_id)
 
-We will also include a plot with a histogram of the % changes for each metric where at least one sample changed.
+  glue::glue("
+    The tables below show changes in cell counts changes between the reference and comparison workflow runs at various stages of processing.<br>
path: scripts/compare-metrics/compare-metrics-template.rmd
user: login: allyhawkins
body: ```suggestion
    The tables below show changes in cell counts between the reference and comparison workflow runs at various stages of processing.<br>
```
created_at: 2025-05-05T15:48:20Z
author_association: MEMBER
diff_hunk: @@ -254,38 +271,152 @@ report_table(
 
 ### Cell count changes {.tabset}
 
-For each sample, we will want to first compare:
+```{r}
+# calculate the difference between reference and comparison for selected fields
+
+cell_fields <- c(
+  "unfiltered_cells",
+  "filtered_cells",
+  "processed_cells",
+  "miqc_pass_count",
+  "scpca_filter_count",
+  "adt_scpca_filter_count"
+)
 
-- `unfiltered_cells`
-- `filtered_cells`
-- `processed_cells`
-- `miqc_pass_count`
-- `scpca_filter_count`
-- `adt_scpca_filter_count`
+cell_changes <- metrics_df |>
+  tidyr::drop_na(starts_with("mapped_reads")) |> # remove added/removed libraries
+  dplyr::select(
+    project_id,
+    sample_id,
+    library_id,
+    starts_with(cell_fields)
+  ) |>
+  tidyr::pivot_longer(
+    cols = starts_with(cell_fields),
+    names_pattern = "(.+)\\.(ref|comp)$",
+    names_to = c("field", ".value") # .value separates into `ref` and `comp` fields
+  ) |>
+  dplyr::filter(is.finite(comp) | is.finite(ref)) |> # keep where at least one is finite
+  dplyr::mutate(
+    change = comp - ref,
+    change_frac = change / ref
+  )
 
-If there are changes in any of those, we will report those, with prominent warnings for any large changes.
-What defines a large change?
-Perhaps start with a change > 0.5% of the total?
-Alternatively, if we see a large fraction of samples changing, we would want to note that.
-If there are any changes to the number of unfiltered cells, that should be highlighted, as we do not expect this to change between runs.
+changed_cell_counts <- cell_changes |>
+  dplyr::filter(is.na(change) | change != 0)
 
-#### Summary
-The main display item would be a summary table, in the first tab:
+has_cell_changes <- nrow(changed_cell_counts) > 0
+```
 
-| Metric | Number of samples changed | % of samples changed | Mean % change |
-| ----- | --------- | --------- | ---------------- |
-| `scpca_filter_count` | 100 | 20 | -10 |
+```{r}
+# summary text for cell count section
+if (!has_cell_changes) {
+  knitr::asis_output(
+    "There are no changes in cell counts between the reference and comparison runs."
+  )
+} else {
+  n_projects <- dplyr::n_distinct(changed_cell_counts$project_id)
+  n_libraries <- dplyr::n_distinct(changed_cell_counts$library_id)
 
-We will also include a plot with a histogram of the % changes for each metric where at least one sample changed.
+  glue::glue("
+    The tables below show changes in cell counts changes between the reference and comparison workflow runs at various stages of processing.<br>
+    There {ifelse(n_added == 1, 'is', 'are a total of')} **{n_libraries}** {ifelse(n_libraries == 1, 'library', 'libraries')} with cell count changes
+    from **{n_projects}** {ifelse(n_projects == 1, 'project', 'projects')}.
+  ") |>
+    knitr::asis_output()
+}
+```
 
+```{r}
+# warn if any unfiltered cell counts have changed
+unfiltered_cell_changes <- cell_changes |>
+  dplyr::filter(field == "unfiltered_cells") |>
+  dplyr::summarize(n_changed = sum(change != 0)) |>
+  dplyr::pull(n_changed)
+
+
+if (unfiltered_cell_changes > 0) {
+  knitr::asis_output(c(
+    "<div class=\"alert alert-danger\">",
+    "**Warning**: Unfiltered cell counts have changed between the reference and comparison runs.",
path: scripts/compare-metrics/compare-metrics-template.rmd
user: login: allyhawkins
body: Should we say how many libraries and projects this affects? I think it might be helpful to know if it's one specifically or if a bunch of them changed. You can also get that information in the table, so not totally necessary to add, but could be helpful. 
created_at: 2025-05-05T15:50:01Z
author_association: MEMBER
diff_hunk: @@ -254,38 +271,152 @@ report_table(
 
 ### Cell count changes {.tabset}
 
-For each sample, we will want to first compare:
+```{r}
+# calculate the difference between reference and comparison for selected fields
+
+cell_fields <- c(
+  "unfiltered_cells",
+  "filtered_cells",
+  "processed_cells",
+  "miqc_pass_count",
+  "scpca_filter_count",
+  "adt_scpca_filter_count"
+)
 
-- `unfiltered_cells`
-- `filtered_cells`
-- `processed_cells`
-- `miqc_pass_count`
-- `scpca_filter_count`
-- `adt_scpca_filter_count`
+cell_changes <- metrics_df |>
+  tidyr::drop_na(starts_with("mapped_reads")) |> # remove added/removed libraries
+  dplyr::select(
+    project_id,
+    sample_id,
+    library_id,
+    starts_with(cell_fields)
+  ) |>
+  tidyr::pivot_longer(
+    cols = starts_with(cell_fields),
+    names_pattern = "(.+)\\.(ref|comp)$",
+    names_to = c("field", ".value") # .value separates into `ref` and `comp` fields
+  ) |>
+  dplyr::filter(is.finite(comp) | is.finite(ref)) |> # keep where at least one is finite
+  dplyr::mutate(
+    change = comp - ref,
+    change_frac = change / ref
+  )
 
-If there are changes in any of those, we will report those, with prominent warnings for any large changes.
-What defines a large change?
-Perhaps start with a change > 0.5% of the total?
-Alternatively, if we see a large fraction of samples changing, we would want to note that.
-If there are any changes to the number of unfiltered cells, that should be highlighted, as we do not expect this to change between runs.
+changed_cell_counts <- cell_changes |>
+  dplyr::filter(is.na(change) | change != 0)
 
-#### Summary
-The main display item would be a summary table, in the first tab:
+has_cell_changes <- nrow(changed_cell_counts) > 0
+```
 
-| Metric | Number of samples changed | % of samples changed | Mean % change |
-| ----- | --------- | --------- | ---------------- |
-| `scpca_filter_count` | 100 | 20 | -10 |
+```{r}
+# summary text for cell count section
+if (!has_cell_changes) {
+  knitr::asis_output(
+    "There are no changes in cell counts between the reference and comparison runs."
+  )
+} else {
+  n_projects <- dplyr::n_distinct(changed_cell_counts$project_id)
+  n_libraries <- dplyr::n_distinct(changed_cell_counts$library_id)
 
-We will also include a plot with a histogram of the % changes for each metric where at least one sample changed.
+  glue::glue("
+    The tables below show changes in cell counts changes between the reference and comparison workflow runs at various stages of processing.<br>
+    There {ifelse(n_added == 1, 'is', 'are a total of')} **{n_libraries}** {ifelse(n_libraries == 1, 'library', 'libraries')} with cell count changes
+    from **{n_projects}** {ifelse(n_projects == 1, 'project', 'projects')}.
+  ") |>
+    knitr::asis_output()
+}
+```
 
+```{r}
+# warn if any unfiltered cell counts have changed
+unfiltered_cell_changes <- cell_changes |>
+  dplyr::filter(field == "unfiltered_cells") |>
+  dplyr::summarize(n_changed = sum(change != 0)) |>
+  dplyr::pull(n_changed)
+
+
+if (unfiltered_cell_changes > 0) {
+  knitr::asis_output(c(
+    "<div class=\"alert alert-danger\">",
+    "**Warning**: Unfiltered cell counts have changed between the reference and comparison runs.",
+    "</div>"
+  ))
+}
+```
+
+```{r, eval=has_cell_changes}
+knitr::asis_output("#### Summary")
+```
 
-#### Detail
+```{r}
+# Summary table of cell count changes
path: scripts/compare-metrics/compare-metrics-template.rmd
user: login: allyhawkins
body: In this table, the `Samples` column is the total number of samples where that metric was present? While `Number of samples changed` is the total number of samples where there is a change in that metric? 

I noticed that the `miqc_pass_count` metric is the only one where we see less samples than the other metrics. It looks like there are 3 samples that are changed, but only one of them is because a different filtering method was used and so the `miqc_pass` column is NA. We should double check something weird isn't going on there? 

A side note, we see a 0% change there, but that's because the detail shows two libraries that have the same amount of change, just in opposing directions. The third library is because we are comparing NA to a number. Maybe we want the summary table to show the average absolute change so we capture changes in both directions? 
created_at: 2025-05-05T15:59:52Z
author_association: MEMBER
diff_hunk: @@ -254,38 +271,152 @@ report_table(
 
 ### Cell count changes {.tabset}
 
-For each sample, we will want to first compare:
+```{r}
+# calculate the difference between reference and comparison for selected fields
+
+cell_fields <- c(
+  "unfiltered_cells",
+  "filtered_cells",
+  "processed_cells",
+  "miqc_pass_count",
+  "scpca_filter_count",
+  "adt_scpca_filter_count"
+)
 
-- `unfiltered_cells`
-- `filtered_cells`
-- `processed_cells`
-- `miqc_pass_count`
-- `scpca_filter_count`
-- `adt_scpca_filter_count`
+cell_changes <- metrics_df |>
+  tidyr::drop_na(starts_with("mapped_reads")) |> # remove added/removed libraries
+  dplyr::select(
+    project_id,
+    sample_id,
+    library_id,
+    starts_with(cell_fields)
+  ) |>
+  tidyr::pivot_longer(
+    cols = starts_with(cell_fields),
+    names_pattern = "(.+)\\.(ref|comp)$",
+    names_to = c("field", ".value") # .value separates into `ref` and `comp` fields
+  ) |>
+  dplyr::filter(is.finite(comp) | is.finite(ref)) |> # keep where at least one is finite
+  dplyr::mutate(
+    change = comp - ref,
+    change_frac = change / ref
+  )
 
-If there are changes in any of those, we will report those, with prominent warnings for any large changes.
-What defines a large change?
-Perhaps start with a change > 0.5% of the total?
-Alternatively, if we see a large fraction of samples changing, we would want to note that.
-If there are any changes to the number of unfiltered cells, that should be highlighted, as we do not expect this to change between runs.
+changed_cell_counts <- cell_changes |>
+  dplyr::filter(is.na(change) | change != 0)
 
-#### Summary
-The main display item would be a summary table, in the first tab:
+has_cell_changes <- nrow(changed_cell_counts) > 0
+```
 
-| Metric | Number of samples changed | % of samples changed | Mean % change |
-| ----- | --------- | --------- | ---------------- |
-| `scpca_filter_count` | 100 | 20 | -10 |
+```{r}
+# summary text for cell count section
+if (!has_cell_changes) {
+  knitr::asis_output(
+    "There are no changes in cell counts between the reference and comparison runs."
+  )
+} else {
+  n_projects <- dplyr::n_distinct(changed_cell_counts$project_id)
+  n_libraries <- dplyr::n_distinct(changed_cell_counts$library_id)
 
-We will also include a plot with a histogram of the % changes for each metric where at least one sample changed.
+  glue::glue("
+    The tables below show changes in cell counts changes between the reference and comparison workflow runs at various stages of processing.<br>
+    There {ifelse(n_added == 1, 'is', 'are a total of')} **{n_libraries}** {ifelse(n_libraries == 1, 'library', 'libraries')} with cell count changes
+    from **{n_projects}** {ifelse(n_projects == 1, 'project', 'projects')}.
+  ") |>
+    knitr::asis_output()
+}
+```
 
+```{r}
+# warn if any unfiltered cell counts have changed
+unfiltered_cell_changes <- cell_changes |>
+  dplyr::filter(field == "unfiltered_cells") |>
+  dplyr::summarize(n_changed = sum(change != 0)) |>
+  dplyr::pull(n_changed)
+
+
+if (unfiltered_cell_changes > 0) {
+  knitr::asis_output(c(
+    "<div class=\"alert alert-danger\">",
+    "**Warning**: Unfiltered cell counts have changed between the reference and comparison runs.",
+    "</div>"
+  ))
+}
+```
+
+```{r, eval=has_cell_changes}
+knitr::asis_output("#### Summary")
+```
 
-#### Detail
+```{r}
+# Summary table of cell count changes
+
+cell_summary <- cell_changes |>
+  dplyr::summarise(
+    .by = c("field"),
+    # count where at least one version has a value
+    n = dplyr::n(),
+    n_changed = sum(is.na(change) | change != 0),
+    sample_change_frac = n_changed / n,
+    mean_change_frac = mean(change_frac, na.rm = TRUE)
+  )
+
+report_table(
+  cell_summary,
+  colnames = c("Metric", "Samples", "Number of samples changed", "% of samples changed", "Mean % change")
path: scripts/compare-metrics/compare-metrics-template.rmd
user: login: allyhawkins
body: Do we want to count libraries or samples here? In the processing changes table we count libraries, and I think that's what we want to do here. 
created_at: 2025-05-05T16:00:35Z
author_association: MEMBER
diff_hunk: @@ -254,38 +271,152 @@ report_table(
 
 ### Cell count changes {.tabset}
 
-For each sample, we will want to first compare:
+```{r}
+# calculate the difference between reference and comparison for selected fields
+
+cell_fields <- c(
+  "unfiltered_cells",
+  "filtered_cells",
+  "processed_cells",
+  "miqc_pass_count",
+  "scpca_filter_count",
+  "adt_scpca_filter_count"
+)
 
-- `unfiltered_cells`
-- `filtered_cells`
-- `processed_cells`
-- `miqc_pass_count`
-- `scpca_filter_count`
-- `adt_scpca_filter_count`
+cell_changes <- metrics_df |>
+  tidyr::drop_na(starts_with("mapped_reads")) |> # remove added/removed libraries
+  dplyr::select(
+    project_id,
+    sample_id,
+    library_id,
+    starts_with(cell_fields)
+  ) |>
+  tidyr::pivot_longer(
+    cols = starts_with(cell_fields),
+    names_pattern = "(.+)\\.(ref|comp)$",
+    names_to = c("field", ".value") # .value separates into `ref` and `comp` fields
+  ) |>
+  dplyr::filter(is.finite(comp) | is.finite(ref)) |> # keep where at least one is finite
+  dplyr::mutate(
+    change = comp - ref,
+    change_frac = change / ref
+  )
 
-If there are changes in any of those, we will report those, with prominent warnings for any large changes.
-What defines a large change?
-Perhaps start with a change > 0.5% of the total?
-Alternatively, if we see a large fraction of samples changing, we would want to note that.
-If there are any changes to the number of unfiltered cells, that should be highlighted, as we do not expect this to change between runs.
+changed_cell_counts <- cell_changes |>
+  dplyr::filter(is.na(change) | change != 0)
 
-#### Summary
-The main display item would be a summary table, in the first tab:
+has_cell_changes <- nrow(changed_cell_counts) > 0
+```
 
-| Metric | Number of samples changed | % of samples changed | Mean % change |
-| ----- | --------- | --------- | ---------------- |
-| `scpca_filter_count` | 100 | 20 | -10 |
+```{r}
+# summary text for cell count section
+if (!has_cell_changes) {
+  knitr::asis_output(
+    "There are no changes in cell counts between the reference and comparison runs."
+  )
+} else {
+  n_projects <- dplyr::n_distinct(changed_cell_counts$project_id)
+  n_libraries <- dplyr::n_distinct(changed_cell_counts$library_id)
 
-We will also include a plot with a histogram of the % changes for each metric where at least one sample changed.
+  glue::glue("
+    The tables below show changes in cell counts changes between the reference and comparison workflow runs at various stages of processing.<br>
+    There {ifelse(n_added == 1, 'is', 'are a total of')} **{n_libraries}** {ifelse(n_libraries == 1, 'library', 'libraries')} with cell count changes
+    from **{n_projects}** {ifelse(n_projects == 1, 'project', 'projects')}.
+  ") |>
+    knitr::asis_output()
+}
+```
 
+```{r}
+# warn if any unfiltered cell counts have changed
+unfiltered_cell_changes <- cell_changes |>
+  dplyr::filter(field == "unfiltered_cells") |>
+  dplyr::summarize(n_changed = sum(change != 0)) |>
+  dplyr::pull(n_changed)
+
+
+if (unfiltered_cell_changes > 0) {
+  knitr::asis_output(c(
+    "<div class=\"alert alert-danger\">",
+    "**Warning**: Unfiltered cell counts have changed between the reference and comparison runs.",
+    "</div>"
+  ))
+}
+```
+
+```{r, eval=has_cell_changes}
+knitr::asis_output("#### Summary")
+```
 
-#### Detail
+```{r}
+# Summary table of cell count changes
+
+cell_summary <- cell_changes |>
+  dplyr::summarise(
+    .by = c("field"),
+    # count where at least one version has a value
+    n = dplyr::n(),
+    n_changed = sum(is.na(change) | change != 0),
+    sample_change_frac = n_changed / n,
+    mean_change_frac = mean(change_frac, na.rm = TRUE)
+  )
+
+report_table(
+  cell_summary,
+  colnames = c("Metric", "Samples", "Number of samples changed", "% of samples changed", "Mean % change")
+) |>
+  DT::formatPercentage(c(4, 5), 2)
+```
+
+
+```{r}
+change_fields <- cell_summary |>
+  dplyr::filter(n_changed > 0) |>
+  dplyr::pull(field)
+
+cell_plot_df <- cell_changes |>
+  dplyr::filter(field %in% change_fields, change != 0) |>
+  dplyr::mutate(
+    field = factor(field, levels = cell_fields),
+  )
+
+ggplot(cell_plot_df, aes(x = change_frac)) +
+  geom_histogram(bins = 20) +
+  facet_wrap(vars(field), ncol = 3) +
+  labs(
+    title = "Histogram of changes in cell counts",
+    x = "Change proportion",
+    y = "Number of samples"
+  ) +
+  theme_bw()
+```
 
-A detail table of changed samples/metrics might then look like the following:
+```{r, eval=has_cell_changes}
+knitr::asis_output("#### Detail")
+```
+
+```{r}
+# Detail table of cell count changes
+
+cell_detail <- changed_cell_counts |>
+  dplyr::select(
+    project_id,
+    sample_id,
+    library_id,
+    field,
+    ref,
+    comp,
+    change,
+    change_frac
+  )
+
+report_table(
+  cell_detail,
+  colnames = c("Project", "Sample", "Library", "Metric", "Reference", "Comparison", "Change", "% change")
path: scripts/compare-metrics/compare-metrics-template.rmd
user: login: allyhawkins
body: I noticed that I have a weird `c` in the upper right hand corner of the `Detail` table when I view it? Scrolling left/right doesn't move it and I can click on it and the table will sort. I think it's an error with printing the `% change` column? 
<img width="899" alt="Screenshot 2025-05-05 at 11 03 25 AM" src="https://github.com/user-attachments/assets/fc54c15d-e40a-46b9-99e4-9a67e924e943" />



created_at: 2025-05-05T16:03:31Z
author_association: MEMBER
diff_hunk: @@ -254,38 +271,152 @@ report_table(
 
 ### Cell count changes {.tabset}
 
-For each sample, we will want to first compare:
+```{r}
+# calculate the difference between reference and comparison for selected fields
+
+cell_fields <- c(
+  "unfiltered_cells",
+  "filtered_cells",
+  "processed_cells",
+  "miqc_pass_count",
+  "scpca_filter_count",
+  "adt_scpca_filter_count"
+)
 
-- `unfiltered_cells`
-- `filtered_cells`
-- `processed_cells`
-- `miqc_pass_count`
-- `scpca_filter_count`
-- `adt_scpca_filter_count`
+cell_changes <- metrics_df |>
+  tidyr::drop_na(starts_with("mapped_reads")) |> # remove added/removed libraries
+  dplyr::select(
+    project_id,
+    sample_id,
+    library_id,
+    starts_with(cell_fields)
+  ) |>
+  tidyr::pivot_longer(
+    cols = starts_with(cell_fields),
+    names_pattern = "(.+)\\.(ref|comp)$",
+    names_to = c("field", ".value") # .value separates into `ref` and `comp` fields
+  ) |>
+  dplyr::filter(is.finite(comp) | is.finite(ref)) |> # keep where at least one is finite
+  dplyr::mutate(
+    change = comp - ref,
+    change_frac = change / ref
+  )
 
-If there are changes in any of those, we will report those, with prominent warnings for any large changes.
-What defines a large change?
-Perhaps start with a change > 0.5% of the total?
-Alternatively, if we see a large fraction of samples changing, we would want to note that.
-If there are any changes to the number of unfiltered cells, that should be highlighted, as we do not expect this to change between runs.
+changed_cell_counts <- cell_changes |>
+  dplyr::filter(is.na(change) | change != 0)
 
-#### Summary
-The main display item would be a summary table, in the first tab:
+has_cell_changes <- nrow(changed_cell_counts) > 0
+```
 
-| Metric | Number of samples changed | % of samples changed | Mean % change |
-| ----- | --------- | --------- | ---------------- |
-| `scpca_filter_count` | 100 | 20 | -10 |
+```{r}
+# summary text for cell count section
+if (!has_cell_changes) {
+  knitr::asis_output(
+    "There are no changes in cell counts between the reference and comparison runs."
+  )
+} else {
+  n_projects <- dplyr::n_distinct(changed_cell_counts$project_id)
+  n_libraries <- dplyr::n_distinct(changed_cell_counts$library_id)
 
-We will also include a plot with a histogram of the % changes for each metric where at least one sample changed.
+  glue::glue("
+    The tables below show changes in cell counts changes between the reference and comparison workflow runs at various stages of processing.<br>
+    There {ifelse(n_added == 1, 'is', 'are a total of')} **{n_libraries}** {ifelse(n_libraries == 1, 'library', 'libraries')} with cell count changes
+    from **{n_projects}** {ifelse(n_projects == 1, 'project', 'projects')}.
+  ") |>
+    knitr::asis_output()
+}
+```
 
+```{r}
+# warn if any unfiltered cell counts have changed
+unfiltered_cell_changes <- cell_changes |>
+  dplyr::filter(field == "unfiltered_cells") |>
+  dplyr::summarize(n_changed = sum(change != 0)) |>
+  dplyr::pull(n_changed)
+
+
+if (unfiltered_cell_changes > 0) {
+  knitr::asis_output(c(
+    "<div class=\"alert alert-danger\">",
+    "**Warning**: Unfiltered cell counts have changed between the reference and comparison runs.",
+    "</div>"
+  ))
+}
+```
+
+```{r, eval=has_cell_changes}
+knitr::asis_output("#### Summary")
+```
 
-#### Detail
+```{r}
+# Summary table of cell count changes
+
+cell_summary <- cell_changes |>
+  dplyr::summarise(
+    .by = c("field"),
+    # count where at least one version has a value
+    n = dplyr::n(),
+    n_changed = sum(is.na(change) | change != 0),
+    sample_change_frac = n_changed / n,
+    mean_change_frac = mean(change_frac, na.rm = TRUE)
+  )
+
+report_table(
+  cell_summary,
+  colnames = c("Metric", "Samples", "Number of samples changed", "% of samples changed", "Mean % change")
+) |>
+  DT::formatPercentage(c(4, 5), 2)
+```
+
+
+```{r}
+change_fields <- cell_summary |>
+  dplyr::filter(n_changed > 0) |>
+  dplyr::pull(field)
+
+cell_plot_df <- cell_changes |>
+  dplyr::filter(field %in% change_fields, change != 0) |>
+  dplyr::mutate(
+    field = factor(field, levels = cell_fields),
+  )
+
+ggplot(cell_plot_df, aes(x = change_frac)) +
+  geom_histogram(bins = 20) +
+  facet_wrap(vars(field), ncol = 3) +
+  labs(
+    title = "Histogram of changes in cell counts",
+    x = "Change proportion",
path: scripts/compare-metrics/compare-metrics-template.rmd
user: login: allyhawkins
body: I feel like this should be `Change percentage` and not proportion? 
created_at: 2025-05-05T16:04:37Z
author_association: MEMBER
diff_hunk: @@ -254,38 +271,152 @@ report_table(
 
 ### Cell count changes {.tabset}
 
-For each sample, we will want to first compare:
+```{r}
+# calculate the difference between reference and comparison for selected fields
+
+cell_fields <- c(
+  "unfiltered_cells",
+  "filtered_cells",
+  "processed_cells",
+  "miqc_pass_count",
+  "scpca_filter_count",
+  "adt_scpca_filter_count"
+)
 
-- `unfiltered_cells`
-- `filtered_cells`
-- `processed_cells`
-- `miqc_pass_count`
-- `scpca_filter_count`
-- `adt_scpca_filter_count`
+cell_changes <- metrics_df |>
+  tidyr::drop_na(starts_with("mapped_reads")) |> # remove added/removed libraries
+  dplyr::select(
+    project_id,
+    sample_id,
+    library_id,
+    starts_with(cell_fields)
+  ) |>
+  tidyr::pivot_longer(
+    cols = starts_with(cell_fields),
+    names_pattern = "(.+)\\.(ref|comp)$",
+    names_to = c("field", ".value") # .value separates into `ref` and `comp` fields
+  ) |>
+  dplyr::filter(is.finite(comp) | is.finite(ref)) |> # keep where at least one is finite
+  dplyr::mutate(
+    change = comp - ref,
+    change_frac = change / ref
+  )
 
-If there are changes in any of those, we will report those, with prominent warnings for any large changes.
-What defines a large change?
-Perhaps start with a change > 0.5% of the total?
-Alternatively, if we see a large fraction of samples changing, we would want to note that.
-If there are any changes to the number of unfiltered cells, that should be highlighted, as we do not expect this to change between runs.
+changed_cell_counts <- cell_changes |>
+  dplyr::filter(is.na(change) | change != 0)
 
-#### Summary
-The main display item would be a summary table, in the first tab:
+has_cell_changes <- nrow(changed_cell_counts) > 0
+```
 
-| Metric | Number of samples changed | % of samples changed | Mean % change |
-| ----- | --------- | --------- | ---------------- |
-| `scpca_filter_count` | 100 | 20 | -10 |
+```{r}
+# summary text for cell count section
+if (!has_cell_changes) {
+  knitr::asis_output(
+    "There are no changes in cell counts between the reference and comparison runs."
+  )
+} else {
+  n_projects <- dplyr::n_distinct(changed_cell_counts$project_id)
+  n_libraries <- dplyr::n_distinct(changed_cell_counts$library_id)
 
-We will also include a plot with a histogram of the % changes for each metric where at least one sample changed.
+  glue::glue("
+    The tables below show changes in cell counts changes between the reference and comparison workflow runs at various stages of processing.<br>
+    There {ifelse(n_added == 1, 'is', 'are a total of')} **{n_libraries}** {ifelse(n_libraries == 1, 'library', 'libraries')} with cell count changes
+    from **{n_projects}** {ifelse(n_projects == 1, 'project', 'projects')}.
+  ") |>
+    knitr::asis_output()
+}
+```
 
+```{r}
+# warn if any unfiltered cell counts have changed
+unfiltered_cell_changes <- cell_changes |>
+  dplyr::filter(field == "unfiltered_cells") |>
+  dplyr::summarize(n_changed = sum(change != 0)) |>
+  dplyr::pull(n_changed)
+
+
+if (unfiltered_cell_changes > 0) {
+  knitr::asis_output(c(
+    "<div class=\"alert alert-danger\">",
+    "**Warning**: Unfiltered cell counts have changed between the reference and comparison runs.",
+    "</div>"
+  ))
+}
+```
+
+```{r, eval=has_cell_changes}
+knitr::asis_output("#### Summary")
+```
 
-#### Detail
+```{r}
+# Summary table of cell count changes
path: scripts/compare-metrics/compare-metrics-template.rmd
user: login: jashapiro
body: Thank you for catching the 0% error! I'll make this and average of absolute changes

> In this table, the Samples column is the total number of samples where that metric was present? While Number of samples changed is the total number of samples where there is a change in that metric?

That is correct. I want to have both for the cases where a metric might not exist (e.g., miQC pass)

I think the changes we are seeing are correct: I swapped 2 sets of samples; one from project 01 and one from project 03, but the 01 sample I also removed one of the pair. I should probably make a few more select changes in the test data, but I think it is working as expected. 
created_at: 2025-05-05T20:04:58Z
author_association: MEMBER
diff_hunk: @@ -254,38 +271,152 @@ report_table(
 
 ### Cell count changes {.tabset}
 
-For each sample, we will want to first compare:
+```{r}
+# calculate the difference between reference and comparison for selected fields
+
+cell_fields <- c(
+  "unfiltered_cells",
+  "filtered_cells",
+  "processed_cells",
+  "miqc_pass_count",
+  "scpca_filter_count",
+  "adt_scpca_filter_count"
+)
 
-- `unfiltered_cells`
-- `filtered_cells`
-- `processed_cells`
-- `miqc_pass_count`
-- `scpca_filter_count`
-- `adt_scpca_filter_count`
+cell_changes <- metrics_df |>
+  tidyr::drop_na(starts_with("mapped_reads")) |> # remove added/removed libraries
+  dplyr::select(
+    project_id,
+    sample_id,
+    library_id,
+    starts_with(cell_fields)
+  ) |>
+  tidyr::pivot_longer(
+    cols = starts_with(cell_fields),
+    names_pattern = "(.+)\\.(ref|comp)$",
+    names_to = c("field", ".value") # .value separates into `ref` and `comp` fields
+  ) |>
+  dplyr::filter(is.finite(comp) | is.finite(ref)) |> # keep where at least one is finite
+  dplyr::mutate(
+    change = comp - ref,
+    change_frac = change / ref
+  )
 
-If there are changes in any of those, we will report those, with prominent warnings for any large changes.
-What defines a large change?
-Perhaps start with a change > 0.5% of the total?
-Alternatively, if we see a large fraction of samples changing, we would want to note that.
-If there are any changes to the number of unfiltered cells, that should be highlighted, as we do not expect this to change between runs.
+changed_cell_counts <- cell_changes |>
+  dplyr::filter(is.na(change) | change != 0)
 
-#### Summary
-The main display item would be a summary table, in the first tab:
+has_cell_changes <- nrow(changed_cell_counts) > 0
+```
 
-| Metric | Number of samples changed | % of samples changed | Mean % change |
-| ----- | --------- | --------- | ---------------- |
-| `scpca_filter_count` | 100 | 20 | -10 |
+```{r}
+# summary text for cell count section
+if (!has_cell_changes) {
+  knitr::asis_output(
+    "There are no changes in cell counts between the reference and comparison runs."
+  )
+} else {
+  n_projects <- dplyr::n_distinct(changed_cell_counts$project_id)
+  n_libraries <- dplyr::n_distinct(changed_cell_counts$library_id)
 
-We will also include a plot with a histogram of the % changes for each metric where at least one sample changed.
+  glue::glue("
+    The tables below show changes in cell counts changes between the reference and comparison workflow runs at various stages of processing.<br>
+    There {ifelse(n_added == 1, 'is', 'are a total of')} **{n_libraries}** {ifelse(n_libraries == 1, 'library', 'libraries')} with cell count changes
+    from **{n_projects}** {ifelse(n_projects == 1, 'project', 'projects')}.
+  ") |>
+    knitr::asis_output()
+}
+```
 
+```{r}
+# warn if any unfiltered cell counts have changed
+unfiltered_cell_changes <- cell_changes |>
+  dplyr::filter(field == "unfiltered_cells") |>
+  dplyr::summarize(n_changed = sum(change != 0)) |>
+  dplyr::pull(n_changed)
+
+
+if (unfiltered_cell_changes > 0) {
+  knitr::asis_output(c(
+    "<div class=\"alert alert-danger\">",
+    "**Warning**: Unfiltered cell counts have changed between the reference and comparison runs.",
+    "</div>"
+  ))
+}
+```
+
+```{r, eval=has_cell_changes}
+knitr::asis_output("#### Summary")
+```
 
-#### Detail
+```{r}
+# Summary table of cell count changes
+
+cell_summary <- cell_changes |>
+  dplyr::summarise(
+    .by = c("field"),
+    # count where at least one version has a value
+    n = dplyr::n(),
+    n_changed = sum(is.na(change) | change != 0),
+    sample_change_frac = n_changed / n,
+    mean_change_frac = mean(change_frac, na.rm = TRUE)
+  )
+
+report_table(
+  cell_summary,
+  colnames = c("Metric", "Samples", "Number of samples changed", "% of samples changed", "Mean % change")
path: scripts/compare-metrics/compare-metrics-template.rmd
user: login: jashapiro
body: This is counting libraries; it is mislabeled.
created_at: 2025-05-05T20:06:49Z
author_association: MEMBER
diff_hunk: @@ -254,38 +271,152 @@ report_table(
 
 ### Cell count changes {.tabset}
 
-For each sample, we will want to first compare:
+```{r}
+# calculate the difference between reference and comparison for selected fields
+
+cell_fields <- c(
+  "unfiltered_cells",
+  "filtered_cells",
+  "processed_cells",
+  "miqc_pass_count",
+  "scpca_filter_count",
+  "adt_scpca_filter_count"
+)
 
-- `unfiltered_cells`
-- `filtered_cells`
-- `processed_cells`
-- `miqc_pass_count`
-- `scpca_filter_count`
-- `adt_scpca_filter_count`
+cell_changes <- metrics_df |>
+  tidyr::drop_na(starts_with("mapped_reads")) |> # remove added/removed libraries
+  dplyr::select(
+    project_id,
+    sample_id,
+    library_id,
+    starts_with(cell_fields)
+  ) |>
+  tidyr::pivot_longer(
+    cols = starts_with(cell_fields),
+    names_pattern = "(.+)\\.(ref|comp)$",
+    names_to = c("field", ".value") # .value separates into `ref` and `comp` fields
+  ) |>
+  dplyr::filter(is.finite(comp) | is.finite(ref)) |> # keep where at least one is finite
+  dplyr::mutate(
+    change = comp - ref,
+    change_frac = change / ref
+  )
 
-If there are changes in any of those, we will report those, with prominent warnings for any large changes.
-What defines a large change?
-Perhaps start with a change > 0.5% of the total?
-Alternatively, if we see a large fraction of samples changing, we would want to note that.
-If there are any changes to the number of unfiltered cells, that should be highlighted, as we do not expect this to change between runs.
+changed_cell_counts <- cell_changes |>
+  dplyr::filter(is.na(change) | change != 0)
 
-#### Summary
-The main display item would be a summary table, in the first tab:
+has_cell_changes <- nrow(changed_cell_counts) > 0
+```
 
-| Metric | Number of samples changed | % of samples changed | Mean % change |
-| ----- | --------- | --------- | ---------------- |
-| `scpca_filter_count` | 100 | 20 | -10 |
+```{r}
+# summary text for cell count section
+if (!has_cell_changes) {
+  knitr::asis_output(
+    "There are no changes in cell counts between the reference and comparison runs."
+  )
+} else {
+  n_projects <- dplyr::n_distinct(changed_cell_counts$project_id)
+  n_libraries <- dplyr::n_distinct(changed_cell_counts$library_id)
 
-We will also include a plot with a histogram of the % changes for each metric where at least one sample changed.
+  glue::glue("
+    The tables below show changes in cell counts changes between the reference and comparison workflow runs at various stages of processing.<br>
+    There {ifelse(n_added == 1, 'is', 'are a total of')} **{n_libraries}** {ifelse(n_libraries == 1, 'library', 'libraries')} with cell count changes
+    from **{n_projects}** {ifelse(n_projects == 1, 'project', 'projects')}.
+  ") |>
+    knitr::asis_output()
+}
+```
 
+```{r}
+# warn if any unfiltered cell counts have changed
+unfiltered_cell_changes <- cell_changes |>
+  dplyr::filter(field == "unfiltered_cells") |>
+  dplyr::summarize(n_changed = sum(change != 0)) |>
+  dplyr::pull(n_changed)
+
+
+if (unfiltered_cell_changes > 0) {
+  knitr::asis_output(c(
+    "<div class=\"alert alert-danger\">",
+    "**Warning**: Unfiltered cell counts have changed between the reference and comparison runs.",
+    "</div>"
+  ))
+}
+```
+
+```{r, eval=has_cell_changes}
+knitr::asis_output("#### Summary")
+```
 
-#### Detail
+```{r}
+# Summary table of cell count changes
+
+cell_summary <- cell_changes |>
+  dplyr::summarise(
+    .by = c("field"),
+    # count where at least one version has a value
+    n = dplyr::n(),
+    n_changed = sum(is.na(change) | change != 0),
+    sample_change_frac = n_changed / n,
+    mean_change_frac = mean(change_frac, na.rm = TRUE)
+  )
+
+report_table(
+  cell_summary,
+  colnames = c("Metric", "Samples", "Number of samples changed", "% of samples changed", "Mean % change")
+) |>
+  DT::formatPercentage(c(4, 5), 2)
+```
+
+
+```{r}
+change_fields <- cell_summary |>
+  dplyr::filter(n_changed > 0) |>
+  dplyr::pull(field)
+
+cell_plot_df <- cell_changes |>
+  dplyr::filter(field %in% change_fields, change != 0) |>
+  dplyr::mutate(
+    field = factor(field, levels = cell_fields),
+  )
+
+ggplot(cell_plot_df, aes(x = change_frac)) +
+  geom_histogram(bins = 20) +
+  facet_wrap(vars(field), ncol = 3) +
+  labs(
+    title = "Histogram of changes in cell counts",
+    x = "Change proportion",
+    y = "Number of samples"
+  ) +
+  theme_bw()
+```
 
-A detail table of changed samples/metrics might then look like the following:
+```{r, eval=has_cell_changes}
+knitr::asis_output("#### Detail")
+```
+
+```{r}
+# Detail table of cell count changes
+
+cell_detail <- changed_cell_counts |>
+  dplyr::select(
+    project_id,
+    sample_id,
+    library_id,
+    field,
+    ref,
+    comp,
+    change,
+    change_frac
+  )
+
+report_table(
+  cell_detail,
+  colnames = c("Project", "Sample", "Library", "Metric", "Reference", "Comparison", "Change", "% change")
path: scripts/compare-metrics/compare-metrics-template.rmd
user: login: jashapiro
body: Yeah, I think you are right. It's annoying that the titles don't scroll along with the body. I think I fixed this in the latest version
created_at: 2025-05-05T20:13:25Z
author_association: MEMBER
diff_hunk: @@ -254,38 +271,152 @@ report_table(
 
 ### Cell count changes {.tabset}
 
-For each sample, we will want to first compare:
+```{r}
+# calculate the difference between reference and comparison for selected fields
+
+cell_fields <- c(
+  "unfiltered_cells",
+  "filtered_cells",
+  "processed_cells",
+  "miqc_pass_count",
+  "scpca_filter_count",
+  "adt_scpca_filter_count"
+)
 
-- `unfiltered_cells`
-- `filtered_cells`
-- `processed_cells`
-- `miqc_pass_count`
-- `scpca_filter_count`
-- `adt_scpca_filter_count`
+cell_changes <- metrics_df |>
+  tidyr::drop_na(starts_with("mapped_reads")) |> # remove added/removed libraries
+  dplyr::select(
+    project_id,
+    sample_id,
+    library_id,
+    starts_with(cell_fields)
+  ) |>
+  tidyr::pivot_longer(
+    cols = starts_with(cell_fields),
+    names_pattern = "(.+)\\.(ref|comp)$",
+    names_to = c("field", ".value") # .value separates into `ref` and `comp` fields
+  ) |>
+  dplyr::filter(is.finite(comp) | is.finite(ref)) |> # keep where at least one is finite
+  dplyr::mutate(
+    change = comp - ref,
+    change_frac = change / ref
+  )
 
-If there are changes in any of those, we will report those, with prominent warnings for any large changes.
-What defines a large change?
-Perhaps start with a change > 0.5% of the total?
-Alternatively, if we see a large fraction of samples changing, we would want to note that.
-If there are any changes to the number of unfiltered cells, that should be highlighted, as we do not expect this to change between runs.
+changed_cell_counts <- cell_changes |>
+  dplyr::filter(is.na(change) | change != 0)
 
-#### Summary
-The main display item would be a summary table, in the first tab:
+has_cell_changes <- nrow(changed_cell_counts) > 0
+```
 
-| Metric | Number of samples changed | % of samples changed | Mean % change |
-| ----- | --------- | --------- | ---------------- |
-| `scpca_filter_count` | 100 | 20 | -10 |
+```{r}
+# summary text for cell count section
+if (!has_cell_changes) {
+  knitr::asis_output(
+    "There are no changes in cell counts between the reference and comparison runs."
+  )
+} else {
+  n_projects <- dplyr::n_distinct(changed_cell_counts$project_id)
+  n_libraries <- dplyr::n_distinct(changed_cell_counts$library_id)
 
-We will also include a plot with a histogram of the % changes for each metric where at least one sample changed.
+  glue::glue("
+    The tables below show changes in cell counts changes between the reference and comparison workflow runs at various stages of processing.<br>
+    There {ifelse(n_added == 1, 'is', 'are a total of')} **{n_libraries}** {ifelse(n_libraries == 1, 'library', 'libraries')} with cell count changes
+    from **{n_projects}** {ifelse(n_projects == 1, 'project', 'projects')}.
+  ") |>
+    knitr::asis_output()
+}
+```
 
+```{r}
+# warn if any unfiltered cell counts have changed
+unfiltered_cell_changes <- cell_changes |>
+  dplyr::filter(field == "unfiltered_cells") |>
+  dplyr::summarize(n_changed = sum(change != 0)) |>
+  dplyr::pull(n_changed)
+
+
+if (unfiltered_cell_changes > 0) {
+  knitr::asis_output(c(
+    "<div class=\"alert alert-danger\">",
+    "**Warning**: Unfiltered cell counts have changed between the reference and comparison runs.",
+    "</div>"
+  ))
+}
+```
+
+```{r, eval=has_cell_changes}
+knitr::asis_output("#### Summary")
+```
 
-#### Detail
+```{r}
+# Summary table of cell count changes
+
+cell_summary <- cell_changes |>
+  dplyr::summarise(
+    .by = c("field"),
+    # count where at least one version has a value
+    n = dplyr::n(),
+    n_changed = sum(is.na(change) | change != 0),
+    sample_change_frac = n_changed / n,
+    mean_change_frac = mean(change_frac, na.rm = TRUE)
+  )
+
+report_table(
+  cell_summary,
+  colnames = c("Metric", "Samples", "Number of samples changed", "% of samples changed", "Mean % change")
+) |>
+  DT::formatPercentage(c(4, 5), 2)
+```
+
+
+```{r}
+change_fields <- cell_summary |>
+  dplyr::filter(n_changed > 0) |>
+  dplyr::pull(field)
+
+cell_plot_df <- cell_changes |>
+  dplyr::filter(field %in% change_fields, change != 0) |>
+  dplyr::mutate(
+    field = factor(field, levels = cell_fields),
+  )
+
+ggplot(cell_plot_df, aes(x = change_frac)) +
+  geom_histogram(bins = 20) +
+  facet_wrap(vars(field), ncol = 3) +
+  labs(
+    title = "Histogram of changes in cell counts",
+    x = "Change proportion",
path: scripts/compare-metrics/compare-metrics-template.rmd
user: login: jashapiro
body: In this case it is proportion; I could convert it to percentage or count though... This is one where I'm not sure I know what the best choice is. 
created_at: 2025-05-05T20:14:11Z
author_association: MEMBER
diff_hunk: @@ -254,38 +272,152 @@ report_table(
 
 ### Cell count changes {.tabset}
 
-For each sample, we will want to first compare:
+```{r}
+# calculate the difference between reference and comparison for selected fields
+
+cell_fields <- c(
+  "unfiltered_cells",
+  "filtered_cells",
+  "processed_cells",
+  "miqc_pass_count",
+  "scpca_filter_count",
+  "adt_scpca_filter_count"
+)
 
-- `unfiltered_cells`
-- `filtered_cells`
-- `processed_cells`
-- `miqc_pass_count`
-- `scpca_filter_count`
-- `adt_scpca_filter_count`
+cell_changes <- metrics_df |>
+  tidyr::drop_na(starts_with("mapped_reads")) |> # remove added/removed libraries
+  dplyr::select(
+    project_id,
+    sample_id,
+    library_id,
+    starts_with(cell_fields)
+  ) |>
+  tidyr::pivot_longer(
+    cols = starts_with(cell_fields),
+    names_pattern = "(.+)\\.(ref|comp)$",
+    names_to = c("field", ".value") # .value separates into `ref` and `comp` fields
+  ) |>
+  dplyr::filter(is.finite(comp) | is.finite(ref)) |> # keep where at least one is finite
+  dplyr::mutate(
+    change = comp - ref,
+    change_frac = change / ref
+  )
 
-If there are changes in any of those, we will report those, with prominent warnings for any large changes.
-What defines a large change?
-Perhaps start with a change > 0.5% of the total?
-Alternatively, if we see a large fraction of samples changing, we would want to note that.
-If there are any changes to the number of unfiltered cells, that should be highlighted, as we do not expect this to change between runs.
+changed_cell_counts <- cell_changes |>
+  dplyr::filter(is.na(change) | change != 0)
 
-#### Summary
-The main display item would be a summary table, in the first tab:
+has_cell_changes <- nrow(changed_cell_counts) > 0
+```
 
-| Metric | Number of samples changed | % of samples changed | Mean % change |
-| ----- | --------- | --------- | ---------------- |
-| `scpca_filter_count` | 100 | 20 | -10 |
+```{r}
+# summary text for cell count section
+if (!has_cell_changes) {
+  knitr::asis_output(
+    "There are no changes in cell counts between the reference and comparison runs."
+  )
+} else {
+  n_projects <- dplyr::n_distinct(changed_cell_counts$project_id)
+  n_libraries <- dplyr::n_distinct(changed_cell_counts$library_id)
 
-We will also include a plot with a histogram of the % changes for each metric where at least one sample changed.
+  glue::glue("
+    The tables below show changes in cell counts between the reference and comparison workflow runs at various stages of processing.<br>
+    There {ifelse(n_added == 1, 'is', 'are a total of')} **{n_libraries}** {ifelse(n_libraries == 1, 'library', 'libraries')} with cell count changes
+    from **{n_projects}** {ifelse(n_projects == 1, 'project', 'projects')}.
+  ") |>
+    knitr::asis_output()
+}
+```
 
+```{r}
+# warn if any unfiltered cell counts have changed
+unfiltered_cell_changes <- cell_changes |>
+  dplyr::filter(field == "unfiltered_cells", change != 0) |>
+  nrow()
+
+
+if (unfiltered_cell_changes > 0) {
+  knitr::asis_output(glue::glue("
+    <div class=\"alert alert-danger\">
+    **Warning**: Unfiltered cell counts have changed between the reference and comparison runs.
+    **{unfiltered_cell_changes}** {ifelse(unfiltered_cell_changes == 1, 'library is', 'libraries are')} affected.
+    </div>
+  "))
+}
+```
+
+```{r, eval=has_cell_changes}
+knitr::asis_output("#### Summary")
+```
 
-#### Detail
+```{r}
+# Summary table of cell count changes
+
+cell_summary <- cell_changes |>
+  dplyr::summarise(
+    .by = c("field"),
+    # count where at least one version has a value
+    n = dplyr::n(),
+    n_changed = sum(is.na(change) | change != 0),
+    sample_change_frac = n_changed / n,
path: scripts/compare-metrics/compare-metrics-template.rmd
user: login: allyhawkins
body: ```suggestion
    library_change_frac = n_changed / n,
```
Since we are talking libraries and not samples
created_at: 2025-05-06T15:21:42Z
author_association: MEMBER
diff_hunk: @@ -254,38 +272,152 @@ report_table(
 
 ### Cell count changes {.tabset}
 
-For each sample, we will want to first compare:
+```{r}
+# calculate the difference between reference and comparison for selected fields
+
+cell_fields <- c(
+  "unfiltered_cells",
+  "filtered_cells",
+  "processed_cells",
+  "miqc_pass_count",
+  "scpca_filter_count",
+  "adt_scpca_filter_count"
+)
 
-- `unfiltered_cells`
-- `filtered_cells`
-- `processed_cells`
-- `miqc_pass_count`
-- `scpca_filter_count`
-- `adt_scpca_filter_count`
+cell_changes <- metrics_df |>
+  tidyr::drop_na(starts_with("mapped_reads")) |> # remove added/removed libraries
+  dplyr::select(
+    project_id,
+    sample_id,
+    library_id,
+    starts_with(cell_fields)
+  ) |>
+  tidyr::pivot_longer(
+    cols = starts_with(cell_fields),
+    names_pattern = "(.+)\\.(ref|comp)$",
+    names_to = c("field", ".value") # .value separates into `ref` and `comp` fields
+  ) |>
+  dplyr::filter(is.finite(comp) | is.finite(ref)) |> # keep where at least one is finite
+  dplyr::mutate(
+    change = comp - ref,
+    change_frac = change / ref
+  )
 
-If there are changes in any of those, we will report those, with prominent warnings for any large changes.
-What defines a large change?
-Perhaps start with a change > 0.5% of the total?
-Alternatively, if we see a large fraction of samples changing, we would want to note that.
-If there are any changes to the number of unfiltered cells, that should be highlighted, as we do not expect this to change between runs.
+changed_cell_counts <- cell_changes |>
+  dplyr::filter(is.na(change) | change != 0)
 
-#### Summary
-The main display item would be a summary table, in the first tab:
+has_cell_changes <- nrow(changed_cell_counts) > 0
+```
 
-| Metric | Number of samples changed | % of samples changed | Mean % change |
-| ----- | --------- | --------- | ---------------- |
-| `scpca_filter_count` | 100 | 20 | -10 |
+```{r}
+# summary text for cell count section
+if (!has_cell_changes) {
+  knitr::asis_output(
+    "There are no changes in cell counts between the reference and comparison runs."
+  )
+} else {
+  n_projects <- dplyr::n_distinct(changed_cell_counts$project_id)
+  n_libraries <- dplyr::n_distinct(changed_cell_counts$library_id)
 
-We will also include a plot with a histogram of the % changes for each metric where at least one sample changed.
+  glue::glue("
+    The tables below show changes in cell counts between the reference and comparison workflow runs at various stages of processing.<br>
+    There {ifelse(n_added == 1, 'is', 'are a total of')} **{n_libraries}** {ifelse(n_libraries == 1, 'library', 'libraries')} with cell count changes
+    from **{n_projects}** {ifelse(n_projects == 1, 'project', 'projects')}.
+  ") |>
+    knitr::asis_output()
+}
+```
 
+```{r}
+# warn if any unfiltered cell counts have changed
+unfiltered_cell_changes <- cell_changes |>
+  dplyr::filter(field == "unfiltered_cells", change != 0) |>
+  nrow()
+
+
+if (unfiltered_cell_changes > 0) {
+  knitr::asis_output(glue::glue("
+    <div class=\"alert alert-danger\">
+    **Warning**: Unfiltered cell counts have changed between the reference and comparison runs.
+    **{unfiltered_cell_changes}** {ifelse(unfiltered_cell_changes == 1, 'library is', 'libraries are')} affected.
+    </div>
+  "))
+}
+```
+
+```{r, eval=has_cell_changes}
+knitr::asis_output("#### Summary")
+```
 
-#### Detail
+```{r}
+# Summary table of cell count changes
+
+cell_summary <- cell_changes |>
+  dplyr::summarise(
+    .by = c("field"),
+    # count where at least one version has a value
+    n = dplyr::n(),
+    n_changed = sum(is.na(change) | change != 0),
+    sample_change_frac = n_changed / n,
+    mean_change_frac = mean(abs(change_frac), na.rm = TRUE)
path: scripts/compare-metrics/compare-metrics-template.rmd
user: login: allyhawkins
body: Something still seems off to me... Now the miQC pass count mean percentage is 0.01%. If we're removing NA and we have two libraries with 1.58% and -1.56% change then this should be 1.57% right? 
created_at: 2025-05-06T15:23:30Z
author_association: MEMBER
diff_hunk: @@ -254,38 +272,152 @@ report_table(
 
 ### Cell count changes {.tabset}
 
-For each sample, we will want to first compare:
+```{r}
+# calculate the difference between reference and comparison for selected fields
+
+cell_fields <- c(
+  "unfiltered_cells",
+  "filtered_cells",
+  "processed_cells",
+  "miqc_pass_count",
+  "scpca_filter_count",
+  "adt_scpca_filter_count"
+)
 
-- `unfiltered_cells`
-- `filtered_cells`
-- `processed_cells`
-- `miqc_pass_count`
-- `scpca_filter_count`
-- `adt_scpca_filter_count`
+cell_changes <- metrics_df |>
+  tidyr::drop_na(starts_with("mapped_reads")) |> # remove added/removed libraries
+  dplyr::select(
+    project_id,
+    sample_id,
+    library_id,
+    starts_with(cell_fields)
+  ) |>
+  tidyr::pivot_longer(
+    cols = starts_with(cell_fields),
+    names_pattern = "(.+)\\.(ref|comp)$",
+    names_to = c("field", ".value") # .value separates into `ref` and `comp` fields
+  ) |>
+  dplyr::filter(is.finite(comp) | is.finite(ref)) |> # keep where at least one is finite
+  dplyr::mutate(
+    change = comp - ref,
+    change_frac = change / ref
+  )
 
-If there are changes in any of those, we will report those, with prominent warnings for any large changes.
-What defines a large change?
-Perhaps start with a change > 0.5% of the total?
-Alternatively, if we see a large fraction of samples changing, we would want to note that.
-If there are any changes to the number of unfiltered cells, that should be highlighted, as we do not expect this to change between runs.
+changed_cell_counts <- cell_changes |>
+  dplyr::filter(is.na(change) | change != 0)
 
-#### Summary
-The main display item would be a summary table, in the first tab:
+has_cell_changes <- nrow(changed_cell_counts) > 0
+```
 
-| Metric | Number of samples changed | % of samples changed | Mean % change |
-| ----- | --------- | --------- | ---------------- |
-| `scpca_filter_count` | 100 | 20 | -10 |
+```{r}
+# summary text for cell count section
+if (!has_cell_changes) {
+  knitr::asis_output(
+    "There are no changes in cell counts between the reference and comparison runs."
+  )
+} else {
+  n_projects <- dplyr::n_distinct(changed_cell_counts$project_id)
+  n_libraries <- dplyr::n_distinct(changed_cell_counts$library_id)
 
-We will also include a plot with a histogram of the % changes for each metric where at least one sample changed.
+  glue::glue("
+    The tables below show changes in cell counts between the reference and comparison workflow runs at various stages of processing.<br>
+    There {ifelse(n_added == 1, 'is', 'are a total of')} **{n_libraries}** {ifelse(n_libraries == 1, 'library', 'libraries')} with cell count changes
+    from **{n_projects}** {ifelse(n_projects == 1, 'project', 'projects')}.
+  ") |>
+    knitr::asis_output()
+}
+```
 
+```{r}
+# warn if any unfiltered cell counts have changed
+unfiltered_cell_changes <- cell_changes |>
+  dplyr::filter(field == "unfiltered_cells", change != 0) |>
+  nrow()
+
+
+if (unfiltered_cell_changes > 0) {
+  knitr::asis_output(glue::glue("
+    <div class=\"alert alert-danger\">
+    **Warning**: Unfiltered cell counts have changed between the reference and comparison runs.
+    **{unfiltered_cell_changes}** {ifelse(unfiltered_cell_changes == 1, 'library is', 'libraries are')} affected.
+    </div>
+  "))
+}
+```
+
+```{r, eval=has_cell_changes}
+knitr::asis_output("#### Summary")
+```
 
-#### Detail
+```{r}
+# Summary table of cell count changes
+
+cell_summary <- cell_changes |>
+  dplyr::summarise(
+    .by = c("field"),
+    # count where at least one version has a value
+    n = dplyr::n(),
+    n_changed = sum(is.na(change) | change != 0),
+    sample_change_frac = n_changed / n,
+    mean_change_frac = mean(abs(change_frac), na.rm = TRUE)
+  )
+
+report_table(
+  cell_summary,
+  colnames = c("Metric", "Libraries", "Number of libraries changed", "% of libraries changed", "Mean % change")
+) |>
+  DT::formatPercentage(c(4, 5), 2)
path: scripts/compare-metrics/compare-metrics-template.rmd
user: login: allyhawkins
body: I think this is not working and that might be why you are off on the mean %? 
What if you do this: 
```suggestion
  DT::formatPercentage(c("% of libraries changed", "Mean % change"), 2)
```
I don't know why using the indices wouldn't work, but the examples show using the column names 🤷‍♀️ 
https://rstudio.github.io/DT/functions.html

It looks like there's another example that uses two columns with: 
`DT::formatPercentage(4:5, 2)`
created_at: 2025-05-06T15:32:44Z
author_association: MEMBER
diff_hunk: @@ -254,38 +272,152 @@ report_table(
 
 ### Cell count changes {.tabset}
 
-For each sample, we will want to first compare:
+```{r}
+# calculate the difference between reference and comparison for selected fields
+
+cell_fields <- c(
+  "unfiltered_cells",
+  "filtered_cells",
+  "processed_cells",
+  "miqc_pass_count",
+  "scpca_filter_count",
+  "adt_scpca_filter_count"
+)
 
-- `unfiltered_cells`
-- `filtered_cells`
-- `processed_cells`
-- `miqc_pass_count`
-- `scpca_filter_count`
-- `adt_scpca_filter_count`
+cell_changes <- metrics_df |>
+  tidyr::drop_na(starts_with("mapped_reads")) |> # remove added/removed libraries
+  dplyr::select(
+    project_id,
+    sample_id,
+    library_id,
+    starts_with(cell_fields)
+  ) |>
+  tidyr::pivot_longer(
+    cols = starts_with(cell_fields),
+    names_pattern = "(.+)\\.(ref|comp)$",
+    names_to = c("field", ".value") # .value separates into `ref` and `comp` fields
+  ) |>
+  dplyr::filter(is.finite(comp) | is.finite(ref)) |> # keep where at least one is finite
+  dplyr::mutate(
+    change = comp - ref,
+    change_frac = change / ref
+  )
 
-If there are changes in any of those, we will report those, with prominent warnings for any large changes.
-What defines a large change?
-Perhaps start with a change > 0.5% of the total?
-Alternatively, if we see a large fraction of samples changing, we would want to note that.
-If there are any changes to the number of unfiltered cells, that should be highlighted, as we do not expect this to change between runs.
+changed_cell_counts <- cell_changes |>
+  dplyr::filter(is.na(change) | change != 0)
 
-#### Summary
-The main display item would be a summary table, in the first tab:
+has_cell_changes <- nrow(changed_cell_counts) > 0
+```
 
-| Metric | Number of samples changed | % of samples changed | Mean % change |
-| ----- | --------- | --------- | ---------------- |
-| `scpca_filter_count` | 100 | 20 | -10 |
+```{r}
+# summary text for cell count section
+if (!has_cell_changes) {
+  knitr::asis_output(
+    "There are no changes in cell counts between the reference and comparison runs."
+  )
+} else {
+  n_projects <- dplyr::n_distinct(changed_cell_counts$project_id)
+  n_libraries <- dplyr::n_distinct(changed_cell_counts$library_id)
 
-We will also include a plot with a histogram of the % changes for each metric where at least one sample changed.
+  glue::glue("
+    The tables below show changes in cell counts between the reference and comparison workflow runs at various stages of processing.<br>
+    There {ifelse(n_added == 1, 'is', 'are a total of')} **{n_libraries}** {ifelse(n_libraries == 1, 'library', 'libraries')} with cell count changes
+    from **{n_projects}** {ifelse(n_projects == 1, 'project', 'projects')}.
+  ") |>
+    knitr::asis_output()
+}
+```
 
+```{r}
+# warn if any unfiltered cell counts have changed
+unfiltered_cell_changes <- cell_changes |>
+  dplyr::filter(field == "unfiltered_cells", change != 0) |>
+  nrow()
+
+
+if (unfiltered_cell_changes > 0) {
+  knitr::asis_output(glue::glue("
+    <div class=\"alert alert-danger\">
+    **Warning**: Unfiltered cell counts have changed between the reference and comparison runs.
+    **{unfiltered_cell_changes}** {ifelse(unfiltered_cell_changes == 1, 'library is', 'libraries are')} affected.
+    </div>
+  "))
+}
+```
+
+```{r, eval=has_cell_changes}
+knitr::asis_output("#### Summary")
+```
 
-#### Detail
+```{r}
+# Summary table of cell count changes
+
+cell_summary <- cell_changes |>
+  dplyr::summarise(
+    .by = c("field"),
+    # count where at least one version has a value
+    n = dplyr::n(),
+    n_changed = sum(is.na(change) | change != 0),
+    sample_change_frac = n_changed / n,
+    mean_change_frac = mean(abs(change_frac), na.rm = TRUE)
path: scripts/compare-metrics/compare-metrics-template.rmd
user: login: jashapiro
body: I think the issue here is the calculation includes all of the 0 changes. So the mean percent change is low. I could make it the mean given not 0 change, which probably makes more sense with expectations.
created_at: 2025-05-06T15:47:16Z
author_association: MEMBER
diff_hunk: @@ -254,38 +272,152 @@ report_table(
 
 ### Cell count changes {.tabset}
 
-For each sample, we will want to first compare:
+```{r}
+# calculate the difference between reference and comparison for selected fields
+
+cell_fields <- c(
+  "unfiltered_cells",
+  "filtered_cells",
+  "processed_cells",
+  "miqc_pass_count",
+  "scpca_filter_count",
+  "adt_scpca_filter_count"
+)
 
-- `unfiltered_cells`
-- `filtered_cells`
-- `processed_cells`
-- `miqc_pass_count`
-- `scpca_filter_count`
-- `adt_scpca_filter_count`
+cell_changes <- metrics_df |>
+  tidyr::drop_na(starts_with("mapped_reads")) |> # remove added/removed libraries
+  dplyr::select(
+    project_id,
+    sample_id,
+    library_id,
+    starts_with(cell_fields)
+  ) |>
+  tidyr::pivot_longer(
+    cols = starts_with(cell_fields),
+    names_pattern = "(.+)\\.(ref|comp)$",
+    names_to = c("field", ".value") # .value separates into `ref` and `comp` fields
+  ) |>
+  dplyr::filter(is.finite(comp) | is.finite(ref)) |> # keep where at least one is finite
+  dplyr::mutate(
+    change = comp - ref,
+    change_frac = change / ref
+  )
 
-If there are changes in any of those, we will report those, with prominent warnings for any large changes.
-What defines a large change?
-Perhaps start with a change > 0.5% of the total?
-Alternatively, if we see a large fraction of samples changing, we would want to note that.
-If there are any changes to the number of unfiltered cells, that should be highlighted, as we do not expect this to change between runs.
+changed_cell_counts <- cell_changes |>
+  dplyr::filter(is.na(change) | change != 0)
 
-#### Summary
-The main display item would be a summary table, in the first tab:
+has_cell_changes <- nrow(changed_cell_counts) > 0
+```
 
-| Metric | Number of samples changed | % of samples changed | Mean % change |
-| ----- | --------- | --------- | ---------------- |
-| `scpca_filter_count` | 100 | 20 | -10 |
+```{r}
+# summary text for cell count section
+if (!has_cell_changes) {
+  knitr::asis_output(
+    "There are no changes in cell counts between the reference and comparison runs."
+  )
+} else {
+  n_projects <- dplyr::n_distinct(changed_cell_counts$project_id)
+  n_libraries <- dplyr::n_distinct(changed_cell_counts$library_id)
 
-We will also include a plot with a histogram of the % changes for each metric where at least one sample changed.
+  glue::glue("
+    The tables below show changes in cell counts between the reference and comparison workflow runs at various stages of processing.<br>
+    There {ifelse(n_added == 1, 'is', 'are a total of')} **{n_libraries}** {ifelse(n_libraries == 1, 'library', 'libraries')} with cell count changes
+    from **{n_projects}** {ifelse(n_projects == 1, 'project', 'projects')}.
+  ") |>
+    knitr::asis_output()
+}
+```
 
+```{r}
+# warn if any unfiltered cell counts have changed
+unfiltered_cell_changes <- cell_changes |>
+  dplyr::filter(field == "unfiltered_cells", change != 0) |>
+  nrow()
+
+
+if (unfiltered_cell_changes > 0) {
+  knitr::asis_output(glue::glue("
+    <div class=\"alert alert-danger\">
+    **Warning**: Unfiltered cell counts have changed between the reference and comparison runs.
+    **{unfiltered_cell_changes}** {ifelse(unfiltered_cell_changes == 1, 'library is', 'libraries are')} affected.
+    </div>
+  "))
+}
+```
+
+```{r, eval=has_cell_changes}
+knitr::asis_output("#### Summary")
+```
 
-#### Detail
+```{r}
+# Summary table of cell count changes
+
+cell_summary <- cell_changes |>
+  dplyr::summarise(
+    .by = c("field"),
+    # count where at least one version has a value
+    n = dplyr::n(),
+    n_changed = sum(is.na(change) | change != 0),
+    sample_change_frac = n_changed / n,
+    mean_change_frac = mean(abs(change_frac), na.rm = TRUE)
path: scripts/compare-metrics/compare-metrics-template.rmd
user: login: allyhawkins
body: > I think the issue here is the calculation includes all of the 0 changes. So the mean percent change is low. I could make it the mean given not 0 change, which probably makes more sense with expectations.

Ah! That definitely makes sense why the number is so low. But yea I think it would make sense to report the mean change of the samples that actually have a change. Otherwise I think it's misleading to show that the change is really small. 
created_at: 2025-05-06T15:49:02Z
author_association: MEMBER
diff_hunk: @@ -254,38 +272,152 @@ report_table(
 
 ### Cell count changes {.tabset}
 
-For each sample, we will want to first compare:
+```{r}
+# calculate the difference between reference and comparison for selected fields
+
+cell_fields <- c(
+  "unfiltered_cells",
+  "filtered_cells",
+  "processed_cells",
+  "miqc_pass_count",
+  "scpca_filter_count",
+  "adt_scpca_filter_count"
+)
 
-- `unfiltered_cells`
-- `filtered_cells`
-- `processed_cells`
-- `miqc_pass_count`
-- `scpca_filter_count`
-- `adt_scpca_filter_count`
+cell_changes <- metrics_df |>
+  tidyr::drop_na(starts_with("mapped_reads")) |> # remove added/removed libraries
+  dplyr::select(
+    project_id,
+    sample_id,
+    library_id,
+    starts_with(cell_fields)
+  ) |>
+  tidyr::pivot_longer(
+    cols = starts_with(cell_fields),
+    names_pattern = "(.+)\\.(ref|comp)$",
+    names_to = c("field", ".value") # .value separates into `ref` and `comp` fields
+  ) |>
+  dplyr::filter(is.finite(comp) | is.finite(ref)) |> # keep where at least one is finite
+  dplyr::mutate(
+    change = comp - ref,
+    change_frac = change / ref
+  )
 
-If there are changes in any of those, we will report those, with prominent warnings for any large changes.
-What defines a large change?
-Perhaps start with a change > 0.5% of the total?
-Alternatively, if we see a large fraction of samples changing, we would want to note that.
-If there are any changes to the number of unfiltered cells, that should be highlighted, as we do not expect this to change between runs.
+changed_cell_counts <- cell_changes |>
+  dplyr::filter(is.na(change) | change != 0)
 
-#### Summary
-The main display item would be a summary table, in the first tab:
+has_cell_changes <- nrow(changed_cell_counts) > 0
+```
 
-| Metric | Number of samples changed | % of samples changed | Mean % change |
-| ----- | --------- | --------- | ---------------- |
-| `scpca_filter_count` | 100 | 20 | -10 |
+```{r}
+# summary text for cell count section
+if (!has_cell_changes) {
+  knitr::asis_output(
+    "There are no changes in cell counts between the reference and comparison runs."
+  )
+} else {
+  n_projects <- dplyr::n_distinct(changed_cell_counts$project_id)
+  n_libraries <- dplyr::n_distinct(changed_cell_counts$library_id)
 
-We will also include a plot with a histogram of the % changes for each metric where at least one sample changed.
+  glue::glue("
+    The tables below show changes in cell counts between the reference and comparison workflow runs at various stages of processing.<br>
+    There {ifelse(n_added == 1, 'is', 'are a total of')} **{n_libraries}** {ifelse(n_libraries == 1, 'library', 'libraries')} with cell count changes
+    from **{n_projects}** {ifelse(n_projects == 1, 'project', 'projects')}.
+  ") |>
+    knitr::asis_output()
+}
+```
 
+```{r}
+# warn if any unfiltered cell counts have changed
+unfiltered_cell_changes <- cell_changes |>
+  dplyr::filter(field == "unfiltered_cells", change != 0) |>
+  nrow()
+
+
+if (unfiltered_cell_changes > 0) {
+  knitr::asis_output(glue::glue("
+    <div class=\"alert alert-danger\">
+    **Warning**: Unfiltered cell counts have changed between the reference and comparison runs.
+    **{unfiltered_cell_changes}** {ifelse(unfiltered_cell_changes == 1, 'library is', 'libraries are')} affected.
+    </div>
+  "))
+}
+```
+
+```{r, eval=has_cell_changes}
+knitr::asis_output("#### Summary")
+```
 
-#### Detail
+```{r}
+# Summary table of cell count changes
+
+cell_summary <- cell_changes |>
+  dplyr::summarise(
+    .by = c("field"),
+    # count where at least one version has a value
+    n = dplyr::n(),
+    n_changed = sum(is.na(change) | change != 0),
+    sample_change_frac = n_changed / n,
+    mean_change_frac = mean(abs(change_frac), na.rm = TRUE)
path: scripts/compare-metrics/compare-metrics-template.rmd
user: login: jashapiro
body: Updated in https://github.com/AlexsLemonade/scpca-nf/pull/864/commits/a614d0d997f4b44d2647c2afca456281c363bd71. I didn't upload a new version of the file, but the percentages jumped up as expected.
created_at: 2025-05-06T15:54:59Z
author_association: MEMBER
diff_hunk: @@ -254,38 +272,152 @@ report_table(
 
 ### Cell count changes {.tabset}
 
-For each sample, we will want to first compare:
+```{r}
+# calculate the difference between reference and comparison for selected fields
+
+cell_fields <- c(
+  "unfiltered_cells",
+  "filtered_cells",
+  "processed_cells",
+  "miqc_pass_count",
+  "scpca_filter_count",
+  "adt_scpca_filter_count"
+)
 
-- `unfiltered_cells`
-- `filtered_cells`
-- `processed_cells`
-- `miqc_pass_count`
-- `scpca_filter_count`
-- `adt_scpca_filter_count`
+cell_changes <- metrics_df |>
+  tidyr::drop_na(starts_with("mapped_reads")) |> # remove added/removed libraries
+  dplyr::select(
+    project_id,
+    sample_id,
+    library_id,
+    starts_with(cell_fields)
+  ) |>
+  tidyr::pivot_longer(
+    cols = starts_with(cell_fields),
+    names_pattern = "(.+)\\.(ref|comp)$",
+    names_to = c("field", ".value") # .value separates into `ref` and `comp` fields
+  ) |>
+  dplyr::filter(is.finite(comp) | is.finite(ref)) |> # keep where at least one is finite
+  dplyr::mutate(
+    change = comp - ref,
+    change_frac = change / ref
+  )
 
-If there are changes in any of those, we will report those, with prominent warnings for any large changes.
-What defines a large change?
-Perhaps start with a change > 0.5% of the total?
-Alternatively, if we see a large fraction of samples changing, we would want to note that.
-If there are any changes to the number of unfiltered cells, that should be highlighted, as we do not expect this to change between runs.
+changed_cell_counts <- cell_changes |>
+  dplyr::filter(is.na(change) | change != 0)
 
-#### Summary
-The main display item would be a summary table, in the first tab:
+has_cell_changes <- nrow(changed_cell_counts) > 0
+```
 
-| Metric | Number of samples changed | % of samples changed | Mean % change |
-| ----- | --------- | --------- | ---------------- |
-| `scpca_filter_count` | 100 | 20 | -10 |
+```{r}
+# summary text for cell count section
+if (!has_cell_changes) {
+  knitr::asis_output(
+    "There are no changes in cell counts between the reference and comparison runs."
+  )
+} else {
+  n_projects <- dplyr::n_distinct(changed_cell_counts$project_id)
+  n_libraries <- dplyr::n_distinct(changed_cell_counts$library_id)
 
-We will also include a plot with a histogram of the % changes for each metric where at least one sample changed.
+  glue::glue("
+    The tables below show changes in cell counts between the reference and comparison workflow runs at various stages of processing.<br>
+    There {ifelse(n_added == 1, 'is', 'are a total of')} **{n_libraries}** {ifelse(n_libraries == 1, 'library', 'libraries')} with cell count changes
+    from **{n_projects}** {ifelse(n_projects == 1, 'project', 'projects')}.
+  ") |>
+    knitr::asis_output()
+}
+```
 
+```{r}
+# warn if any unfiltered cell counts have changed
+unfiltered_cell_changes <- cell_changes |>
+  dplyr::filter(field == "unfiltered_cells", change != 0) |>
+  nrow()
+
+
+if (unfiltered_cell_changes > 0) {
+  knitr::asis_output(glue::glue("
+    <div class=\"alert alert-danger\">
+    **Warning**: Unfiltered cell counts have changed between the reference and comparison runs.
+    **{unfiltered_cell_changes}** {ifelse(unfiltered_cell_changes == 1, 'library is', 'libraries are')} affected.
+    </div>
+  "))
+}
+```
+
+```{r, eval=has_cell_changes}
+knitr::asis_output("#### Summary")
+```
 
-#### Detail
+```{r}
+# Summary table of cell count changes
+
+cell_summary <- cell_changes |>
+  dplyr::summarise(
+    .by = c("field"),
+    # count where at least one version has a value
+    n = dplyr::n(),
+    n_changed = sum(is.na(change) | change != 0),
+    sample_change_frac = n_changed / n,
+    mean_change_frac = mean(abs(change_frac), na.rm = TRUE)
+  )
+
+report_table(
+  cell_summary,
+  colnames = c("Metric", "Libraries", "Number of libraries changed", "% of libraries changed", "Mean % change")
+) |>
+  DT::formatPercentage(c(4, 5), 2)
path: scripts/compare-metrics/compare-metrics-template.rmd
user: login: jashapiro
body: This is working as expected, but either way works, and the names are a bit more clear; I just didn't want to type them out... I'll make this change throughout for consistency.
created_at: 2025-05-06T15:56:46Z
author_association: MEMBER
diff_hunk: @@ -254,38 +272,152 @@ report_table(
 
 ### Cell count changes {.tabset}
 
-For each sample, we will want to first compare:
+```{r}
+# calculate the difference between reference and comparison for selected fields
+
+cell_fields <- c(
+  "unfiltered_cells",
+  "filtered_cells",
+  "processed_cells",
+  "miqc_pass_count",
+  "scpca_filter_count",
+  "adt_scpca_filter_count"
+)
 
-- `unfiltered_cells`
-- `filtered_cells`
-- `processed_cells`
-- `miqc_pass_count`
-- `scpca_filter_count`
-- `adt_scpca_filter_count`
+cell_changes <- metrics_df |>
+  tidyr::drop_na(starts_with("mapped_reads")) |> # remove added/removed libraries
+  dplyr::select(
+    project_id,
+    sample_id,
+    library_id,
+    starts_with(cell_fields)
+  ) |>
+  tidyr::pivot_longer(
+    cols = starts_with(cell_fields),
+    names_pattern = "(.+)\\.(ref|comp)$",
+    names_to = c("field", ".value") # .value separates into `ref` and `comp` fields
+  ) |>
+  dplyr::filter(is.finite(comp) | is.finite(ref)) |> # keep where at least one is finite
+  dplyr::mutate(
+    change = comp - ref,
+    change_frac = change / ref
+  )
 
-If there are changes in any of those, we will report those, with prominent warnings for any large changes.
-What defines a large change?
-Perhaps start with a change > 0.5% of the total?
-Alternatively, if we see a large fraction of samples changing, we would want to note that.
-If there are any changes to the number of unfiltered cells, that should be highlighted, as we do not expect this to change between runs.
+changed_cell_counts <- cell_changes |>
+  dplyr::filter(is.na(change) | change != 0)
 
-#### Summary
-The main display item would be a summary table, in the first tab:
+has_cell_changes <- nrow(changed_cell_counts) > 0
+```
 
-| Metric | Number of samples changed | % of samples changed | Mean % change |
-| ----- | --------- | --------- | ---------------- |
-| `scpca_filter_count` | 100 | 20 | -10 |
+```{r}
+# summary text for cell count section
+if (!has_cell_changes) {
+  knitr::asis_output(
+    "There are no changes in cell counts between the reference and comparison runs."
+  )
+} else {
+  n_projects <- dplyr::n_distinct(changed_cell_counts$project_id)
+  n_libraries <- dplyr::n_distinct(changed_cell_counts$library_id)
 
-We will also include a plot with a histogram of the % changes for each metric where at least one sample changed.
+  glue::glue("
+    The tables below show changes in cell counts between the reference and comparison workflow runs at various stages of processing.<br>
+    There {ifelse(n_added == 1, 'is', 'are a total of')} **{n_libraries}** {ifelse(n_libraries == 1, 'library', 'libraries')} with cell count changes
+    from **{n_projects}** {ifelse(n_projects == 1, 'project', 'projects')}.
+  ") |>
+    knitr::asis_output()
+}
+```
 
+```{r}
+# warn if any unfiltered cell counts have changed
+unfiltered_cell_changes <- cell_changes |>
+  dplyr::filter(field == "unfiltered_cells", change != 0) |>
+  nrow()
+
+
+if (unfiltered_cell_changes > 0) {
+  knitr::asis_output(glue::glue("
+    <div class=\"alert alert-danger\">
+    **Warning**: Unfiltered cell counts have changed between the reference and comparison runs.
+    **{unfiltered_cell_changes}** {ifelse(unfiltered_cell_changes == 1, 'library is', 'libraries are')} affected.
+    </div>
+  "))
+}
+```
+
+```{r, eval=has_cell_changes}
+knitr::asis_output("#### Summary")
+```
 
-#### Detail
+```{r}
+# Summary table of cell count changes
+
+cell_summary <- cell_changes |>
+  dplyr::summarise(
+    .by = c("field"),
+    # count where at least one version has a value
+    n = dplyr::n(),
+    n_changed = sum(is.na(change) | change != 0),
+    sample_change_frac = n_changed / n,
+    mean_change_frac = mean(abs(change_frac), na.rm = TRUE)
+  )
+
+report_table(
+  cell_summary,
+  colnames = c("Metric", "Libraries", "Number of libraries changed", "% of libraries changed", "Mean % change")
+) |>
+  DT::formatPercentage(c(4, 5), 2)
path: scripts/compare-metrics/compare-metrics-template.rmd
user: login: jashapiro
body: turns out it has to be the original column names, not the display names. Done in  [0516e21](https://github.com/AlexsLemonade/scpca-nf/pull/864/commits/0516e210f858cd10da4c03993c5940215be2e31d)
created_at: 2025-05-06T16:08:18Z
author_association: MEMBER

PR comments:
user: login: allyhawkins
created_at: 2025-05-05T15:35:24Z
body: @jashapiro do you by any chance have a rendered example report? If so, could you share it to make review easier? 
author_association: MEMBER
user: login: jashapiro
created_at: 2025-05-05T15:42:35Z
body: > @jashapiro do you by any chance have a rendered example report? If so, could you share it to make review easier?

Yes, sorry I had meant to include that! [compare-metrics.html.zip](https://github.com/user-attachments/files/20039617/compare-metrics.html.zip)

author_association: MEMBER