> Status: `draft`
>
> Template class: SOURCE-BACKED WORKFLOW

## Purpose

Score one or more gene signatures with UCell while retaining signature
coverage, direction, and score construction in the notebook. The signature
file must contain signature, gene, and direction columns; direction is UP or
DOWN.

## Inputs and parameters

```{r}
library(Seurat)
library(UCell)
library(qs2)
library(readr)
library(dplyr)
library(ggplot2)

input_object <- "input/seurat_object.qs2"
signature_path <- "input/signatures.tsv"
output_object <- "output/seurat_object_ucell.qs2"
output_coverage <- "output/signature_coverage.tsv"
output_plot <- "output/ucell_scores.png"

assay_name <- "RNA"
min_genes_found <- 1
use_zscore_composite <- FALSE
```

## Analysis

```{r}
object <- qs2::qs_read(input_object)
DefaultAssay(object) <- assay_name
signatures <- read_tsv(signature_path, show_col_types = FALSE)

stopifnot(all(c("signature", "gene", "direction") %in% colnames(signatures)))
signatures <- signatures |>
  mutate(
    signature = as.character(signature),
    gene = as.character(gene),
    direction = toupper(as.character(direction))
  ) |>
  filter(direction %in% c("UP", "DOWN")) |>
  distinct(signature, gene, direction)

object_genes <- rownames(object[[assay_name]])
coverage <- signatures |>
  group_by(signature, direction) |>
  summarise(
    genes_requested = n(),
    genes_found = sum(gene %in% object_genes),
    fraction_found = genes_found / genes_requested,
    .groups = "drop"
  )
print(coverage)
stopifnot(all(coverage$genes_found >= min_genes_found))

feature_sets <- list()
for (signature_name in unique(signatures$signature)) {
  signature_rows <- signatures[signatures$signature == signature_name, ]
  for (direction_name in c("UP", "DOWN")) {
    genes <- signature_rows$gene[signature_rows$direction == direction_name]
    genes <- intersect(genes, object_genes)
    if (length(genes) > 0) {
      feature_sets[[paste(signature_name, direction_name, sep = "_")]] <- genes
    }
  }
}

object <- AddModuleScore_UCell(
  obj = object,
  features = feature_sets,
  name = NULL
)

for (signature_name in unique(signatures$signature)) {
  up_column <- paste(signature_name, "UP_UCell", sep = "_")
  down_column <- paste(signature_name, "DOWN_UCell", sep = "_")
  raw_score_column <- paste(signature_name, "score_raw", sep = "_")
  if (all(c(up_column, down_column) %in% colnames(object[[]]))) {
    up_values <- object[[up_column, drop = TRUE]]
    down_values <- object[[down_column, drop = TRUE]]
    object[[raw_score_column]] <- up_values - down_values
    if (use_zscore_composite) {
      z_score_column <- paste(signature_name, "score_z", sep = "_")
      object[[z_score_column]] <- as.numeric(scale(up_values)) -
        as.numeric(scale(down_values))
    }
  }
}
```

## Diagnostics

```{r}
dir.create(dirname(output_plot), recursive = TRUE, showWarnings = FALSE)
score_columns <- grep("(_UCell|_score_raw|_score_z)$", colnames(object[[]]), value = TRUE)
print(summary(object[[]][, score_columns, drop = FALSE]))
if (length(score_columns) > 0) {
  p <- VlnPlot(object, features = head(score_columns, 4), ncol = 2, pt.size = 0)
  ggsave(output_plot, p, width = 9, height = 6, dpi = 150)
}
```

## Outputs

```{r}
dir.create(dirname(output_object), recursive = TRUE, showWarnings = FALSE)
dir.create(dirname(output_coverage), recursive = TRUE, showWarnings = FALSE)
write_tsv(coverage, output_coverage)
qs2::qs_save(object, output_object)
```

## Method notes

The primary directional score is the raw UP-minus-DOWN difference and the UP
and DOWN component scores are always retained. Smoothing is excluded because
it changes the estimand.

::: {.callout-warning}
## Dataset-relative composite scores

The optional z-scored composite is relative to the cells in this dataset. It
is useful for within-cohort visualization, but it is not an absolute or
transferable score across cohorts with different cell compositions.
:::
