Overview

This script assesses IL-17 pathway biology in B-ALL leukaemia cells from the CNS and BM compartments. The CNS immune environment is enriched for Vγ6Vδ4 γδ T cells, which are a major source of IL-17 in barrier tissues, raising the question of whether leukaemia cells are IL-17 responsive and whether this responsiveness is tissue-specific or population-specific.

The full IL-17 receptor family (Il17ra–Il17re) and ligand family (Il17a–Il17f, Il25) are examined alongside canonical downstream transcriptional targets. Results are stratified by tissue (BM vs CNS) and propagating population (LSK_IL7R vs LK_CLP). This script depends on scored objects produced by 05_population_scoring_cns_analysis.Rmd.


Setup

library(Seurat)
## Loading required package: SeuratObject
## Loading required package: sp
## 
## Attaching package: 'SeuratObject'
## The following objects are masked from 'package:base':
## 
##     intersect, t
library(ggplot2)
library(patchwork)
library(dplyr)
## 
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union
library(knitr)
library(qs2)
## qs2 0.1.7
library(viridis)
## Loading required package: viridisLite
library(tidyr)
library(tibble)
library(scales)
## 
## Attaching package: 'scales'
## The following object is masked from 'package:viridis':
## 
##     viridis_pal
tissue_cols <- c("BM" = "steelblue", "CNS" = "firebrick")
pop_cols    <- c("LSK_IL7R"     = "#1a3a6b",
                 "LK_CLP"       = "#8b0000",
                 "Intermediate" = "grey75")

bm_res_col  <- "originalexp_snn_res.0.3"
cns_res_col <- "originalexp_snn_res.0.3"

ambient_blacklist <- c("Slc1a2",  "Sparcl1", "Apod",  "Cpe",
                       "Mobp",    "S100b",   "Atp1a2","Ndrg2")
# Load scored leukaemia objects from script 10
# These already contain: population, LSK_IL7R_BM1, CNS_sig1, BM_sig1,
# bulk_CNS_sig1, bulk_BM_sig1, Phase, S.Score, G2M.Score
leuk_BM        <- qs_read("/exports/eddie/scratch/aduguid3/harmony_clustering/05_leuk_BM_scored.qs")
leuk_CNS_clean <- qs_read("/exports/eddie/scratch/aduguid3/harmony_clustering/05_leuk_CNS_clean_scored.qs")
leuk_all       <- qs_read("/exports/eddie/scratch/aduguid3/harmony_clustering/05_leuk_all_scored.qs")

stopifnot("leuk_BM not found"        = exists("leuk_BM"))
stopifnot("leuk_CNS_clean not found" = exists("leuk_CNS_clean"))
stopifnot("leuk_all not found"       = exists("leuk_all"))

# Load clean pseudobulk DE results from script 04
res_clean_df <- read.csv("/exports/eddie/scratch/aduguid3/Rmarkdown/04_DE_CNSvsBM_clean.csv")
stopifnot("res_clean_df not found" = exists("res_clean_df"))

# Barcode prefix vectors for leuk_all metadata transfers
bm_cells  <- colnames(leuk_all)[grepl("^BM_",  colnames(leuk_all))]
cns_cells <- colnames(leuk_all)[grepl("^CNS_", colnames(leuk_all))]
bm_orig   <- sub("^BM_",  "", bm_cells)
cns_orig  <- sub("^CNS_", "", cns_cells)

cat("BM  cells:", ncol(leuk_BM),        "\n")
## BM  cells: 44606
cat("CNS cells:", ncol(leuk_CNS_clean), "\n")
## CNS cells: 47017
cat("All cells:", ncol(leuk_all),       "\n")
## All cells: 91623
cat("DE genes loaded:", nrow(res_clean_df), "\n")
## DE genes loaded: 16195
# Confirm required metadata columns are present in scored objects
# transplant_source should already be present from script 10
required_cols <- c("population", "LSK_IL7R_BM1", "Tissue", "transplant_source")

cat("BM object — required columns present:\n")
## BM object — required columns present:
for (col in required_cols) {
  cat(" ", col, ":", col %in% colnames(leuk_BM@meta.data), "\n")
}
##   population : TRUE 
##   LSK_IL7R_BM1 : TRUE 
##   Tissue : TRUE 
##   transplant_source : TRUE
cat("\nCNS object — required columns present:\n")
## 
## CNS object — required columns present:
for (col in required_cols) {
  cat(" ", col, ":", col %in% colnames(leuk_CNS_clean@meta.data), "\n")
}
##   population : TRUE 
##   LSK_IL7R_BM1 : TRUE 
##   Tissue : TRUE 
##   transplant_source : TRUE
# If transplant_source is missing, derive from Mouse_ID
if (!"transplant_source" %in% colnames(leuk_BM@meta.data)) {
  cat("\ntransplant_source missing — deriving from Mouse_ID\n")
  source_map <- c("1838161" = "AD4_EM3",  "1838162" = "AD4_EM3",
                  "1838163" = "AD2.2_EM4","1838171" = "AD2.2_EM4",
                  "1838279" = "AD5_EM4",  "1838280" = "AD5_EM4")
  leuk_BM$transplant_source        <- unname(source_map[as.character(leuk_BM$Mouse_ID)])
  leuk_CNS_clean$transplant_source <- unname(source_map[as.character(leuk_CNS_clean$Mouse_ID)])
  cat("transplant_source assigned\n")
}

cat("\nBM population distribution:\n")
## 
## BM population distribution:
print(table(leuk_BM$population))
## 
## Intermediate       LK_CLP     LSK_IL7R 
##         5133        29528         9945
cat("\nCNS population distribution:\n")
## 
## CNS population distribution:
print(table(leuk_CNS_clean$population))
## 
## Intermediate       LK_CLP     LSK_IL7R 
##         5063        31012        10942

Part 1 — IL-17 Gene Sets

Receptor Family

The IL-17 receptor family has five members with distinct ligand specificities. The canonical IL-17A/F receptor is the Il17ra/Il17rc heterodimer. Il17ra also pairs with Il17rb to form the receptor for IL-17B and IL-17E (Il25 in mouse). All five receptor variants are assessed to determine which are expressed in leukaemia cells and whether the canonical or non-canonical receptor complex is present.

Ligand Family

All six IL-17 family ligands are assessed. Note that IL-17E is annotated as Il25 in mouse rather than Il17e — Il17e is absent from the mouse genome annotation and searching for it will return no results. Il25 signals through the Il17ra/Il17rb heterodimer and is typically produced by epithelial and stromal cells rather than immune cells.

Downstream Targets

Canonical IL-17 downstream targets include chemokines driving neutrophil recruitment (Cxcl1, Cxcl2, Cxcl5), pro-inflammatory cytokines (Il6, Tnf, Il1b), and matrix remodelling genes (Mmp3, Mmp13). The intracellular adaptor Act1 is encoded by Traf3ip2 in mouse — the human gene symbol Act1 is not used in the mouse annotation. Defb4 (defensin beta 4) is not expressed in haematopoietic cells and is excluded. Il17raf is poorly annotated in mouse and excluded.

# IL-17 receptor family — all five variants
il17_receptors <- c(
  "Il17ra",   # main subunit — pairs with Il17rc for IL-17A/F
              #              — pairs with Il17rb for IL-17B/Il25
  "Il17rb",   # receptor for IL-17B and Il25 (IL-17E)
  "Il17rc",   # co-receptor with Il17ra for IL-17A/F
  "Il17rd",   # Sef — modulates MAPK/FGFR signalling
  "Il17re"    # receptor for IL-17C
)

# IL-17 ligand family
# Note: Il17e = Il25 in mouse — Il17e is not a valid mouse gene symbol
il17_ligands <- c(
  "Il17a",   # canonical — produced by γδ T cells, Th17
  "Il17b",   # ligand for Il17ra/Il17rb heterodimer
  "Il17c",   # ligand for Il17re
  "Il17d",   # receptor not well defined
  "Il17f",   # pairs with Il17a — signals via Il17ra/Il17rc
  "Il25"     # Il17e in mouse — ligand for Il17ra/Il17rb
)

# Canonical IL-17 downstream transcriptional targets
# Act1 = Traf3ip2 in mouse (intracellular adaptor linking Il17ra to NF-κB/MAPK)
# Defb4 excluded — not expressed in haematopoietic cells
# Il17raf excluded — poorly annotated in mouse
il17_targets <- c(
  # Chemokines — neutrophil recruitment
  "Cxcl1",    "Cxcl2",  "Cxcl5",  "Ccl2",  "Ccl7",  "Ccl20",
  # Cytokines
  "Il6",      "Tnf",    "Il1b",   "Csf3",  "Csf2",
  # Antimicrobial / alarmins
  "S100a8",   "S100a9", "Lcn2",
  # Matrix remodelling
  "Mmp3",     "Mmp13",
  # Signalling mediators
  "Traf6",    "Traf3ip2",  # Traf3ip2 = Act1 in mouse
  "Nfkb1",    "Nfkb2",  "Ikbkb",
  # Negative regulator
  "Socs3"
)

# Helper — check gene availability and report missing
check_genes <- function(genes, obj, label) {
  found   <- genes[genes %in% rownames(obj)]
  missing <- genes[!genes %in% rownames(obj)]
  cat(label, "—", length(found), "of", length(genes), "genes found\n")
  if (length(missing) > 0)
    cat("  Missing:", paste(missing, collapse = ", "), "\n")
  invisible(found)
}

cat("=== IL-17 Receptors ===\n")
## === IL-17 Receptors ===
il17r_bm  <- check_genes(il17_receptors, leuk_BM,        "BM")
## BM — 5 of 5 genes found
il17r_cns <- check_genes(il17_receptors, leuk_CNS_clean, "CNS")
## CNS — 5 of 5 genes found
cat("\n=== IL-17 Ligands ===\n")
## 
## === IL-17 Ligands ===
il17l_bm  <- check_genes(il17_ligands, leuk_BM,        "BM")
## BM — 6 of 6 genes found
il17l_cns <- check_genes(il17_ligands, leuk_CNS_clean, "CNS")
## CNS — 6 of 6 genes found
cat("\n=== IL-17 Target Genes ===\n")
## 
## === IL-17 Target Genes ===
il17t_bm  <- check_genes(il17_targets, leuk_BM,        "BM")
## BM — 22 of 22 genes found
il17t_cns <- check_genes(il17_targets, leuk_CNS_clean, "CNS")
## CNS — 22 of 22 genes found

Part 2 — Receptor and Ligand Expression in Leukaemia Cells

Detection Rate and Mean Expression

# Compute detection rate and mean expression for receptors and ligands
expr_summary <- function(genes, label) {
  lapply(genes, function(gene) {
    bm_expr  <- if (gene %in% rownames(leuk_BM)) {
      GetAssayData(leuk_BM, layer = "counts")[gene, ]
    } else rep(0, ncol(leuk_BM))

    cns_expr <- if (gene %in% rownames(leuk_CNS_clean)) {
      GetAssayData(leuk_CNS_clean, layer = "counts")[gene, ]
    } else rep(0, ncol(leuk_CNS_clean))

    data.frame(
      gene          = gene,
      type          = label,
      BM_pct_expr   = round(mean(bm_expr  > 0) * 100, 2),
      BM_mean_expr  = round(mean(bm_expr),             4),
      CNS_pct_expr  = round(mean(cns_expr > 0) * 100, 2),
      CNS_mean_expr = round(mean(cns_expr),            4)
    )
  }) %>% bind_rows()
}

receptor_summary <- expr_summary(il17_receptors, "Receptor")
ligand_summary   <- expr_summary(il17_ligands,   "Ligand")

kable(receptor_summary,
      caption = "IL-17 receptor family — detection rate and mean expression in BM and CNS leukaemia")
IL-17 receptor family — detection rate and mean expression in BM and CNS leukaemia
gene type BM_pct_expr BM_mean_expr CNS_pct_expr CNS_mean_expr
Il17ra Receptor 47.76 0.7898 44.77 0.7205
Il17rb Receptor 47.34 0.8070 47.31 0.8109
Il17rc Receptor 0.10 0.0011 0.08 0.0009
Il17rd Receptor 0.26 0.0028 0.31 0.0035
Il17re Receptor 0.05 0.0005 0.04 0.0004
kable(ligand_summary,
      caption = "IL-17 ligand family — detection rate and mean expression in BM and CNS leukaemia")
IL-17 ligand family — detection rate and mean expression in BM and CNS leukaemia
gene type BM_pct_expr BM_mean_expr CNS_pct_expr CNS_mean_expr
Il17a Ligand 0.00 0.0002 0.00 0e+00
Il17b Ligand 0.00 0.0000 0.00 0e+00
Il17c Ligand 0.07 0.0007 0.06 6e-04
Il17d Ligand 0.10 0.0010 0.07 8e-04
Il17f Ligand 0.12 0.0013 0.09 9e-04
Il25 Ligand 0.00 0.0000 0.00 0e+00

Key Findings — Receptor Profile

# Summarise receptor findings
cat("=== Receptor expression summary ===\n\n")
## === Receptor expression summary ===
cat("Il17ra: BM", receptor_summary$BM_pct_expr[receptor_summary$gene == "Il17ra"],
    "% / CNS", receptor_summary$CNS_pct_expr[receptor_summary$gene == "Il17ra"], "%\n")
## Il17ra: BM 47.76 % / CNS 44.77 %
cat("Il17rb: BM", receptor_summary$BM_pct_expr[receptor_summary$gene == "Il17rb"],
    "% / CNS", receptor_summary$CNS_pct_expr[receptor_summary$gene == "Il17rb"], "%\n")
## Il17rb: BM 47.34 % / CNS 47.31 %
cat("Il17rc: BM", receptor_summary$BM_pct_expr[receptor_summary$gene == "Il17rc"],
    "% / CNS", receptor_summary$CNS_pct_expr[receptor_summary$gene == "Il17rc"], "%\n\n")
## Il17rc: BM 0.1 % / CNS 0.08 %
cat("Interpretation:\n")
## Interpretation:
cat("Il17ra and Il17rb are co-expressed at ~47% in both tissues.\n")
## Il17ra and Il17rb are co-expressed at ~47% in both tissues.
cat("Il17rc is essentially absent (<0.1%) in both tissues.\n")
## Il17rc is essentially absent (<0.1%) in both tissues.
cat("Without Il17rc, the canonical Il17ra/Il17rc receptor for IL-17A/F\n")
## Without Il17rc, the canonical Il17ra/Il17rc receptor for IL-17A/F
cat("cannot be formed. Leukaemia cells are poised to respond to IL-17B\n")
## cannot be formed. Leukaemia cells are poised to respond to IL-17B
cat("or Il25 via the non-canonical Il17ra/Il17rb heterodimer.\n\n")
## or Il25 via the non-canonical Il17ra/Il17rb heterodimer.
cat("Ligand expression is negligible in leukaemia cells (<0.1% detection)\n")
## Ligand expression is negligible in leukaemia cells (<0.1% detection)
cat("confirming leukaemia cells are IL-17 responsive but not IL-17 producing.\n")
## confirming leukaemia cells are IL-17 responsive but not IL-17 producing.

IL-17 Receptor — Dot Plot Across Clusters

il17r_detected <- il17_receptors[il17_receptors %in%
                                   c(rownames(leuk_BM), rownames(leuk_CNS_clean))]

p_bm_dot <- DotPlot(leuk_BM,
                     features  = il17r_detected,
                     group.by  = bm_res_col,
                     cols      = c("lightgrey", "#1a3a6b"),
                     dot.scale = 8) +
  RotatedAxis() +
  ggtitle("BM — IL-17 receptor expression by cluster") +
  theme(axis.text.x = element_text(size = 9))

p_cns_dot <- DotPlot(leuk_CNS_clean,
                      features  = il17r_detected,
                      group.by  = cns_res_col,
                      cols      = c("lightgrey", "#8b0000"),
                      dot.scale = 8) +
  RotatedAxis() +
  ggtitle("CNS — IL-17 receptor expression by cluster") +
  theme(axis.text.x = element_text(size = 9))

p_bm_dot / p_cns_dot
IL-17 receptor expression across clusters — BM and CNS

IL-17 receptor expression across clusters — BM and CNS

IL-17 Receptor — Population Comparison

p_bm_pop <- DotPlot(leuk_BM,
                     features  = il17r_detected,
                     group.by  = "population",
                     cols      = c("lightgrey", "#1a3a6b"),
                     dot.scale = 8) +
  RotatedAxis() +
  ggtitle("BM — IL-17 receptors by population")
## Warning: Scaling data with a low number of groups may produce misleading
## results
p_cns_pop <- DotPlot(leuk_CNS_clean,
                      features  = il17r_detected,
                      group.by  = "population",
                      cols      = c("lightgrey", "#8b0000"),
                      dot.scale = 8) +
  RotatedAxis() +
  ggtitle("CNS — IL-17 receptors by population")
## Warning: Scaling data with a low number of groups may produce misleading
## results
p_bm_pop + p_cns_pop
IL-17 receptor expression by population — BM and CNS

IL-17 receptor expression by population — BM and CNS

IL-17 Receptor — Harmony UMAP

# Only plot the two expressed receptors
il17r_expressed <- c("Il17ra", "Il17rb")

plots <- lapply(il17r_expressed, function(gene) {
  FeaturePlot(leuk_all,
               features   = gene,
               cols       = c("lightgrey", "firebrick"),
               order      = TRUE,
               min.cutoff = "q10",
               split.by   = "Tissue") +
    theme(legend.position = "right")
})

wrap_plots(plots, ncol = 1)
IL-17 receptor expression on Harmony-integrated UMAP

IL-17 receptor expression on Harmony-integrated UMAP


Part 3 — IL-17 Pathway Activity Score

Downstream IL-17 pathway targets are scored onto leukaemia cells using AddModuleScore. Even in the absence of Il17rc, pathway activity may be detectable if leukaemia cells are responding to IL-17B or Il25 via Il17ra/Il17rb. Score ranges are assessed by tissue and propagating population.

leuk_BM <- AddModuleScore(leuk_BM,
                           features = list(il17t_bm),
                           name     = "IL17_pathway",
                           seed     = 42)

leuk_CNS_clean <- AddModuleScore(leuk_CNS_clean,
                                  features = list(il17t_cns),
                                  name     = "IL17_pathway",
                                  seed     = 42)

cat("IL-17 pathway score ranges:\n")
## IL-17 pathway score ranges:
cat("  BM  cells:", round(range(leuk_BM$IL17_pathway1),        3), "\n")
##   BM  cells: -0.085 0.251
cat("  CNS cells:", round(range(leuk_CNS_clean$IL17_pathway1), 3), "\n")
##   CNS cells: -0.091 0.259
# Transfer to leuk_all for UMAP
il17_vec <- c(
  setNames(leuk_BM$IL17_pathway1[match(bm_orig, colnames(leuk_BM))], bm_cells),
  setNames(leuk_CNS_clean$IL17_pathway1[match(cns_orig, colnames(leuk_CNS_clean))], cns_cells)
)
leuk_all <- AddMetaData(leuk_all, metadata = il17_vec, col.name = "IL17_pathway1")

leuk_all_bm  <- subset(leuk_all, subset = Tissue == "BM")
leuk_all_cns <- subset(leuk_all, subset = Tissue == "CNS")

Pathway Score — BM vs CNS

wt_il17 <- wilcox.test(leuk_BM$IL17_pathway1, leuk_CNS_clean$IL17_pathway1)

bind_rows(
  leuk_BM@meta.data        %>% select(Tissue, IL17_pathway1),
  leuk_CNS_clean@meta.data %>% select(Tissue, IL17_pathway1)
) %>%
  ggplot(aes(x = Tissue, y = IL17_pathway1, fill = Tissue)) +
  geom_violin(trim = FALSE, alpha = 0.8) +
  geom_boxplot(width = 0.1, fill = "white", outlier.size = 0.2) +
  scale_fill_manual(values = tissue_cols) +
  geom_hline(yintercept = 0, linetype = "dashed", colour = "grey40") +
  theme_classic(base_size = 12) +
  labs(y     = "IL-17 pathway score",
       title = paste0("IL-17 pathway activity — BM vs CNS\n",
                      "Wilcoxon p = ", signif(wt_il17$p.value, 3))) +
  theme(legend.position = "none")
IL-17 pathway activity score — BM vs CNS

IL-17 pathway activity score — BM vs CNS

Pathway Score — By Cluster

v_bm <- VlnPlot(leuk_BM,
                 features  = "IL17_pathway1",
                 group.by  = bm_res_col,
                 pt.size   = 0,
                 cols      = viridis(length(unique(
                               leuk_BM@meta.data[[bm_res_col]])))) +
  ggtitle("BM — IL-17 pathway score by cluster") +
  geom_hline(yintercept = 0, linetype = "dashed", colour = "grey40") +
  theme(legend.position = "none",
        axis.text.x = element_text(angle = 45, hjust = 1))

v_cns <- VlnPlot(leuk_CNS_clean,
                  features  = "IL17_pathway1",
                  group.by  = cns_res_col,
                  pt.size   = 0,
                  cols      = viridis(length(unique(
                                leuk_CNS_clean@meta.data[[cns_res_col]])))) +
  ggtitle("CNS — IL-17 pathway score by cluster") +
  geom_hline(yintercept = 0, linetype = "dashed", colour = "grey40") +
  theme(legend.position = "none",
        axis.text.x = element_text(angle = 45, hjust = 1))

v_bm / v_cns
IL-17 pathway score by cluster — BM and CNS

IL-17 pathway score by cluster — BM and CNS

Pathway Score — By Population

bind_rows(
  leuk_BM@meta.data        %>% select(Tissue, population, IL17_pathway1),
  leuk_CNS_clean@meta.data %>% select(Tissue, population, IL17_pathway1)
) %>%
  filter(population != "Intermediate") %>%
  ggplot(aes(x = population, y = IL17_pathway1, fill = population)) +
  geom_violin(trim = FALSE, alpha = 0.8) +
  geom_boxplot(width = 0.1, fill = "white", outlier.size = 0.2) +
  scale_fill_manual(values = pop_cols) +
  facet_wrap(~Tissue) +
  geom_hline(yintercept = 0, linetype = "dashed", colour = "grey40") +
  theme_classic(base_size = 12) +
  labs(y     = "IL-17 pathway score",
       title = "IL-17 pathway activity by population and tissue") +
  theme(legend.position = "none",
        axis.text.x = element_text(angle = 45, hjust = 1))
IL-17 pathway score by population and tissue

IL-17 pathway score by population and tissue

lapply(c("BM", "CNS"), function(tiss) {
  dat <- if (tiss == "BM") leuk_BM@meta.data else leuk_CNS_clean@meta.data
  lsk <- dat %>% filter(population == "LSK_IL7R") %>% pull(IL17_pathway1)
  clp <- dat %>% filter(population == "LK_CLP")   %>% pull(IL17_pathway1)
  data.frame(
    Tissue        = tiss,
    mean_LSK_IL7R = round(mean(lsk), 4),
    mean_LK_CLP   = round(mean(clp), 4),
    wilcox_p      = signif(wilcox.test(lsk, clp)$p.value, 3)
  )
}) %>%
  bind_rows() %>%
  kable(caption = "IL-17 pathway score — LSK_IL7R vs LK_CLP by tissue")
IL-17 pathway score — LSK_IL7R vs LK_CLP by tissue
Tissue mean_LSK_IL7R mean_LK_CLP wilcox_p
BM 0.0004 0.0120 0
CNS -0.0023 0.0069 0

Pathway Score — Harmony UMAP

p_il17_bm <- FeaturePlot(leuk_all_bm,
                          features   = "IL17_pathway1",
                          cols       = c("lightgrey", "firebrick"),
                          order      = TRUE,
                          min.cutoff = "q10") +
  ggtitle("BM — IL-17 pathway score")

p_il17_cns <- FeaturePlot(leuk_all_cns,
                           features   = "IL17_pathway1",
                           cols       = c("lightgrey", "firebrick"),
                           order      = TRUE,
                           min.cutoff = "q10") +
  ggtitle("CNS — IL-17 pathway score")

p_il17_bm + p_il17_cns
IL-17 pathway activity on Harmony-integrated UMAP

IL-17 pathway activity on Harmony-integrated UMAP


Part 4 — IL-17 Pathway Genes in CNS vs BM DE

Leukaemia cells were found to express Il17ra and Il17rb but not Il17rc, and produce no IL-17 ligands. IL-17 pathway activity scores were uniform across tissues and clusters with no tissue-specific enrichment. As a final check, all IL-17 receptor, ligand and target genes are cross-referenced against the pseudobulk CNS vs BM DE results to determine whether any are significantly differentially expressed at the tissue level.

il17_all_genes <- c(il17_receptors, il17_ligands, il17_targets)

il17_de <- res_clean_df %>%
  filter(gene %in% il17_all_genes,
         !is.na(padj)) %>%
  mutate(
    gene_type = case_when(
      gene %in% il17_receptors ~ "Receptor",
      gene %in% il17_ligands   ~ "Ligand",
      TRUE                     ~ "Target"
    ),
    direction = case_when(
      padj < 0.05 & log2FoldChange > 0 ~ "CNS-enriched",
      padj < 0.05 & log2FoldChange < 0 ~ "BM-enriched",
      TRUE                              ~ "NS"
    )
  ) %>%
  arrange(padj) %>%
  select(gene, gene_type, log2FoldChange, baseMean, padj, direction)

cat("IL-17 pathway genes in CNS vs BM DE:", nrow(il17_de), "\n")
## IL-17 pathway genes in CNS vs BM DE: 15
cat("Significant (padj < 0.05):",
    sum(il17_de$padj < 0.05, na.rm = TRUE), "\n")
## Significant (padj < 0.05): 0
cat("CNS-enriched:", sum(il17_de$direction == "CNS-enriched"), "\n")
## CNS-enriched: 0
cat("BM-enriched: ", sum(il17_de$direction == "BM-enriched"),  "\n")
## BM-enriched:  0
kable(il17_de %>% mutate(across(where(is.numeric), ~round(., 4))),
      caption = "IL-17 pathway genes in CNS vs BM pseudobulk DE — leukaemia cells")
IL-17 pathway genes in CNS vs BM pseudobulk DE — leukaemia cells
gene gene_type log2FoldChange baseMean padj direction
Traf3ip2 Target 0.1663 4970.6501 0.1878 NS
Tnf Target 0.1819 788.6269 0.4461 NS
Il17ra Receptor -0.0870 5543.6867 0.6334 NS
S100a9 Target -1.5716 8.0922 0.7075 NS
S100a8 Target -1.7112 7.6889 0.7192 NS
Il17rd Receptor 0.3729 23.3149 0.7855 NS
Mmp13 Target 0.1731 205.5965 0.9064 NS
Il17f Ligand -0.2999 7.8364 0.9270 NS
Traf6 Target -0.0220 5123.2998 0.9436 NS
Il17rb Receptor 0.0498 5935.3512 0.9474 NS
Nfkb2 Target 0.0565 595.5345 0.9576 NS
Il17rc Receptor -0.1938 6.9059 0.9669 NS
Nfkb1 Target -0.0239 18426.9172 0.9761 NS
Socs3 Target 0.0275 512.6285 0.9874 NS
Ikbkb Target 0.0045 7301.4304 0.9913 NS

Summary

IL-17 pathway analysis in B-ALL leukaemia cells revealed the following:

Receptor profile: Leukaemia cells express the non-canonical IL-17 receptor components Il17ra (~47% detection, mean ~0.79) and Il17rb (~47% detection, mean ~0.81) in both BM and CNS compartments. Il17rc, required for the canonical IL-17A/F receptor heterodimer, is essentially absent (<0.1% detection). This profile indicates leukaemia cells are poised to respond to IL-17B or Il25 via the Il17ra/Il17rb heterodimer rather than to canonical IL-17A produced by γδ T cells.

Ligand profile: All six IL-17 family ligands are essentially absent from leukaemia cells (<0.1% detection), confirming leukaemia cells are IL-17 responsive but not IL-17 producing. Any IL-17 pathway activity must arise from extrinsic ligand produced by other cells in the niche.

Population specificity: Il17ra and Il17rb expression is enriched in LSK_IL7R cells relative to LK_CLP in both tissues, suggesting that if non-canonical IL-17 signalling occurs it would preferentially affect the LSK_IL7R propagating population.

Tissue specificity: Receptor expression, pathway activity scores and pseudobulk DE all show no significant tissue difference — IL-17 responsiveness via the Il17ra/Il17rb axis is a cell-intrinsic property shared across BM and CNS compartments rather than a CNS-specific adaptation.

Open question: The identity of the Il17b/Il25 ligand source in the niche remains to be determined. This will be addressed in the immune cell reintegration and CellChat analyses (scripts 11–12), where Il17b and Il25 expression across immune cell types will be assessed.


Session Information

sessionInfo()
## R version 4.4.1 (2024-06-14)
## Platform: x86_64-pc-linux-gnu
## Running under: Rocky Linux 9.5 (Blue Onyx)
## 
## Matrix products: default
## BLAS/LAPACK: /opt/intel/oneapi/mkl/2024.0/lib/libmkl_gf_lp64.so.2;  LAPACK version 3.10.1
## 
## locale:
##  [1] LC_CTYPE=en_GB.UTF-8       LC_NUMERIC=C              
##  [3] LC_TIME=en_GB.UTF-8        LC_COLLATE=en_GB.UTF-8    
##  [5] LC_MONETARY=en_GB.UTF-8    LC_MESSAGES=en_GB.UTF-8   
##  [7] LC_PAPER=en_GB.UTF-8       LC_NAME=C                 
##  [9] LC_ADDRESS=C               LC_TELEPHONE=C            
## [11] LC_MEASUREMENT=en_GB.UTF-8 LC_IDENTIFICATION=C       
## 
## time zone: Europe/London
## tzcode source: system (glibc)
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
##  [1] scales_1.4.0       tibble_3.3.1       tidyr_1.3.1        viridis_0.6.5     
##  [5] viridisLite_0.4.2  qs2_0.1.7          knitr_1.51         dplyr_1.1.4       
##  [9] patchwork_1.3.2    ggplot2_4.0.2      Seurat_5.4.0       SeuratObject_5.3.0
## [13] sp_2.1-4          
## 
## loaded via a namespace (and not attached):
##   [1] deldir_2.0-4           pbapply_1.7-4          gridExtra_2.3         
##   [4] rlang_1.1.7            magrittr_2.0.3         RcppAnnoy_0.0.23      
##   [7] otel_0.2.0             spatstat.geom_3.7-0    matrixStats_1.5.0     
##  [10] ggridges_0.5.6         compiler_4.4.1         png_0.1-8             
##  [13] vctrs_0.6.5            reshape2_1.4.4         stringr_1.5.1         
##  [16] pkgconfig_2.0.3        fastmap_1.2.0          labeling_0.4.3        
##  [19] utf8_1.2.4             promises_1.5.0         rmarkdown_2.30        
##  [22] ggbeeswarm_0.7.3       purrr_1.0.2            xfun_0.56             
##  [25] cachem_1.1.0           jsonlite_2.0.0         goftest_1.2-3         
##  [28] later_1.4.6            spatstat.utils_3.2-1   irlba_2.3.7           
##  [31] parallel_4.4.1         cluster_2.1.6          R6_2.6.1              
##  [34] ica_1.0-3              spatstat.data_3.1-9    bslib_0.7.0           
##  [37] stringi_1.8.4          RColorBrewer_1.1-3     reticulate_1.45.0     
##  [40] spatstat.univar_3.1-6  parallelly_1.37.1      lmtest_0.9-40         
##  [43] jquerylib_0.1.4        scattermore_1.2        Rcpp_1.0.12           
##  [46] tensor_1.5.1           future.apply_1.11.2    zoo_1.8-15            
##  [49] sctransform_0.4.3      httpuv_1.6.16          Matrix_1.7-0          
##  [52] splines_4.4.1          igraph_2.2.2           tidyselect_1.2.1      
##  [55] abind_1.4-5            rstudioapi_0.16.0      dichromat_2.0-0.1     
##  [58] yaml_2.3.12            stringfish_0.18.0      spatstat.random_3.4-4 
##  [61] codetools_0.2-20       miniUI_0.1.2           spatstat.explore_3.7-0
##  [64] listenv_0.9.1          lattice_0.22-6         plyr_1.8.9            
##  [67] withr_3.0.2            shiny_1.12.1           S7_0.2.1              
##  [70] ROCR_1.0-12            ggrastr_1.0.2          evaluate_1.0.5        
##  [73] Rtsne_0.17             future_1.33.2          fastDummies_1.7.5     
##  [76] survival_3.6-4         RcppParallel_5.1.8     polyclip_1.10-7       
##  [79] fitdistrplus_1.2-6     pillar_1.9.0           KernSmooth_2.23-24    
##  [82] plotly_4.12.0          generics_0.1.3         RcppHNSW_0.6.0        
##  [85] globals_0.16.3         xtable_1.8-4           glue_1.8.0            
##  [88] lazyeval_0.2.2         tools_4.4.1            data.table_1.15.4     
##  [91] RSpectra_0.16-2        RANN_2.6.2             dotCall64_1.2         
##  [94] cowplot_1.2.0          grid_4.4.1             nlme_3.1-164          
##  [97] beeswarm_0.4.0         vipor_0.4.7            cli_3.6.5             
## [100] spatstat.sparse_3.1-0  spam_2.11-3            fansi_1.0.6           
## [103] uwot_0.2.4             gtable_0.3.6           sass_0.4.9            
## [106] digest_0.6.35          progressr_0.18.0       ggrepel_0.9.6         
## [109] htmlwidgets_1.6.4      farver_2.1.2           htmltools_0.5.8.1     
## [112] lifecycle_1.0.5        httr_1.4.7             mime_0.12             
## [115] MASS_7.3-60.2