Exploring Age-Related Immune–Vascular Changes in the Brain Using Single-Cell RNA-seq

Introduction

The brain, once thought to be immune privileged, is now recognized as a dynamic immune environment with a diverse array of resident and infiltrating immune cells. Microglia, the brain’s resident macrophages, are central to maintaining homeostasis, while border-associated immune populations—including perivascular macrophages, meningeal dendritic cells, and infiltrating T cells—play critical roles in surveillance and neuroimmune interactions.

Dataset Overview

For my final project, I analyzed single-cell RNA-seq data generated by our lab from young and aged mouse brains. The dataset includes ~20,000 cells per age group, derived from whole-brain single-cell suspensions sorted for live cells and sequenced using the 10x Genomics platform.After sequencing, the data underwent standard quality control (QC) and preprocessing using the Seurat package in R. This included filtering low-quality cells, normalizing gene expression, identifying highly variable genes, and clustering cell populations.This analysis focuses on endothelial cells, macrophages , and T cells— some of the key populations at the brain’s immune–vascular interface. By comparing age-related changes in the expression of adhesion molecules (e.g., ICAM1, VCAM1) and inflammatory mediators (e.g., CXCL10), the project aims to uncover how aging alters immune signaling and vascular function.

#read data file

library(readxl)
DEG_data <- read_excel("DEG_All_Condition_All_CellType 1.xlsx", sheet = 1)
library(tidyverse)
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.1.4     ✔ readr     2.1.5
✔ forcats   1.0.0     ✔ stringr   1.5.1
✔ ggplot2   3.5.2     ✔ tibble    3.3.0
✔ lubridate 1.9.4     ✔ tidyr     1.3.1
✔ purrr     1.1.0     
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(dplyr)
library(ggplot2)
#head
head (DEG_data)
# A tibble: 6 × 9
  gene_symbol     p_val avg_log2FC pct.1 pct.2 p_val_adj Cluster ident.1 ident.2
  <chr>           <dbl>      <dbl> <dbl> <dbl>     <dbl> <chr>   <chr>   <chr>  
1 Gm42418             0      -22.2 0     1             0 Endoth… G2_old  G1_you…
2 ENSMUSG0000012…     0       18.7 0.977 0             0 Endoth… G2_old  G1_you…
3 AY036118            0      -19.2 0     0.962         0 Endoth… G2_old  G1_you…
4 Ptprg               0      -19.1 0     0.957         0 Endoth… G2_old  G1_you…
5 Qk                  0      -17.0 0     0.857         0 Endoth… G2_old  G1_you…
6 Col4a3bp            0      -17.0 0     0.819         0 Endoth… G2_old  G1_you…
# Define the cell types you want to keep
cell_types <- c("Endothelial_cells", "Macrophages", "T_memory_cells")

# Filter the DEG dataset to keep only these 2 cell types
filtered_deg <- DEG_data %>%
  filter(Cluster %in% cell_types)

# Check how many gene comparisons exist for each selected cell type
table(filtered_deg$Cluster)

Endothelial_cells       Macrophages    T_memory_cells 
            64413             16803              6767 
young_vs_aged <- filtered_deg %>%
  filter(ident.1 == "G2_old", ident.2 == "G1_young")
table(young_vs_aged$Cluster)

Endothelial_cells       Macrophages    T_memory_cells 
            10813               477               524 
# Step 1: Broader inflammation/immune gene panel
inflammatory_genes <- c(
  "Il1b", "Il6", "Tnf", "Ifng", 
  "Ccl2", "Ccl3", "Ccl4", "Cxcl9", "Cxcl10",
  "Cd74", "Cd80", "Cd86", "Cd274", "Pdcd1lg2",
  "H2-Ab1", "H2-Aa", "H2-Eb1", "Tap1", "Tapbp",
  "Icam1", "Vcam1", "Sele", "Sell", 
  "Lyz2", "Nos2", "Ptprc"
)

# Step 2: Filter for the genes and relevant cell types
cell_types_focus <- c("Endothelial_cells", "Macrophages", "T_memory_cells")

inflamm_subset <- DEG_data %>%
  filter(Cluster %in% cell_types_focus,
         gene_symbol %in% inflammatory_genes,
         ident.1 == "G2_old", ident.2 == "G1_young")  # ensure aged vs young only

# Step 3: Plot as ggplot heatmap
ggplot(inflamm_subset, aes(x = Cluster, y = gene_symbol, fill = avg_log2FC)) +
  geom_tile(color = "white") +
  scale_fill_gradient2(low = "blue", mid = "white", high = "red", midpoint = 0,
                       name = "log2FC") +
  labs(title = "Log2 Fold Change of Broad Inflammatory Genes (Aged vs. Young)",
       x = "Cell Type", y = "Gene") +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1),
        panel.grid = element_blank())

Gene Function Summary

These genes were selected for their roles in inflammation, immune regulation, or endothelial interactions relevant to brain aging:

Gene Function
Vcam1 Endothelial adhesion molecule involved in leukocyte binding and transmigration
Tnf Master pro-inflammatory cytokine activating NF-κB and other immune pathways
Tapbp Tapasin; involved in MHC class I antigen processing and presentation
Sell L-selectin; mediates immune cell migration to lymphoid and inflamed tissues
Sele E-selectin; recruits neutrophils and monocytes to sites of inflammation
Ptprc CD45; essential for T and B cell activation and signaling
Nos2 iNOS; produces nitric oxide in macrophages for microbial defense
Lyz2 Antimicrobial enzyme produced by macrophages and neutrophils
Icam1 Endothelial adhesion molecule involved in immune-endothelial interactions
Cd274 PD-L1; immune checkpoint molecule that suppresses T cell activation
Ccl4 Chemokine that recruits monocytes, T cells, and NK cells to inflamed tissue
# Filter for endothelial cells aged vs young
endo_deg <- DEG_data %>%
  filter(Cluster == "Endothelial_cells",
         ident.1 == "G2_old", ident.2 == "G1_young") %>%
  mutate(log10_pval = -log10(p_val_adj),
         regulation = case_when(
           p_val_adj < 0.05 & avg_log2FC > 1 ~ "Upregulated",
           p_val_adj < 0.05 & avg_log2FC < -1 ~ "Downregulated",
           TRUE ~ "Not significant"
         ))

# Plot
ggplot(endo_deg, aes(x = avg_log2FC, y = log10_pval)) +
  geom_point(aes(color = regulation), alpha = 0.7, size = 2) +
  scale_color_manual(values = c(
    "Upregulated" = "firebrick2",
    "Downregulated" = "dodgerblue3",
    "Not significant" = "grey70"
  )) +
  geom_vline(xintercept = c(-1, 1), linetype = "dashed", color = "gray50") +
  geom_hline(yintercept = -log10(0.05), linetype = "dashed", color = "gray50") +
  labs(title = "Volcano Plot: Endothelial Cells (Aged vs Young)",
       x = "log2 Fold Change",
       y = "-log10 Adjusted p-value",
       color = "Regulation") +
  theme_minimal(base_size = 13) +
  theme(legend.position = "top")

library(dplyr)
library(tidyr)
library(ggplot2)
library(forcats)

cell_types_focus <- c("Endothelial_cells", "Macrophages", "T_memory_cells")

sig_deg <- DEG_data %>%
  filter(Cluster %in% cell_types_focus,
         ident.1 == "G2_old", ident.2 == "G1_young",
         p_val_adj < 0.05)

top_up   <- sig_deg %>%
  group_by(Cluster) %>%
  slice_max(order_by = avg_log2FC, n = 10, with_ties = FALSE) %>%
  mutate(direction = "Up (old vs young)")

top_down <- sig_deg %>%
  group_by(Cluster) %>%
  slice_min(order_by = avg_log2FC, n = 10, with_ties = FALSE) %>%
  mutate(direction = "Down (old vs young)")

top10_both <- bind_rows(top_up, top_down) %>%
  arrange(Cluster, desc(avg_log2FC))

top10_both
# A tibble: 60 × 10
# Groups:   Cluster [3]
   gene_symbol    p_val avg_log2FC pct.1 pct.2 p_val_adj Cluster ident.1 ident.2
   <chr>          <dbl>      <dbl> <dbl> <dbl>     <dbl> <chr>   <chr>   <chr>  
 1 ENSMUSG000001…     0       18.7 0.977     0         0 Endoth… G2_old  G1_you…
 2 Qki                0       16.3 0.761     0         0 Endoth… G2_old  G1_you…
 3 Cert1              0       16.2 0.715     0         0 Endoth… G2_old  G1_you…
 4 Garre1             0       15.3 0.545     0         0 Endoth… G2_old  G1_you…
 5 Nherf2             0       15.0 0.468     0         0 Endoth… G2_old  G1_you…
 6 Septin7            0       14.7 0.422     0         0 Endoth… G2_old  G1_you…
 7 Atp10d             0       14.2 0.323     0         0 Endoth… G2_old  G1_you…
 8 6820431F20Rik      0       14.0 0.302     0         0 Endoth… G2_old  G1_you…
 9 Bltp1              0       13.9 0.282     0         0 Endoth… G2_old  G1_you…
10 Dennd2b            0       13.8 0.244     0         0 Endoth… G2_old  G1_you…
# ℹ 50 more rows
# ℹ 1 more variable: direction <chr>
library(knitr)

top10_both %>%
  select(Cluster, gene_symbol, avg_log2FC, p_val_adj, direction) %>%
  mutate(p_val_adj = signif(p_val_adj, 3)) %>%
  kable(format = "html", digits = 2, 
        caption = "Top 10 Up & Down Genes per Cell Type (Aged vs Young)")
Top 10 Up & Down Genes per Cell Type (Aged vs Young)
Cluster gene_symbol avg_log2FC p_val_adj direction
Endothelial_cells ENSMUSG00000121513 18.68 0 Up (old vs young)
Endothelial_cells Qki 16.30 0 Up (old vs young)
Endothelial_cells Cert1 16.23 0 Up (old vs young)
Endothelial_cells Garre1 15.30 0 Up (old vs young)
Endothelial_cells Nherf2 15.01 0 Up (old vs young)
Endothelial_cells Septin7 14.71 0 Up (old vs young)
Endothelial_cells Atp10d 14.24 0 Up (old vs young)
Endothelial_cells 6820431F20Rik 13.97 0 Up (old vs young)
Endothelial_cells Bltp1 13.89 0 Up (old vs young)
Endothelial_cells Dennd2b 13.81 0 Up (old vs young)
Endothelial_cells Sept11 -14.39 0 Down (old vs young)
Endothelial_cells 4932438A13Rik -14.50 0 Down (old vs young)
Endothelial_cells Sept7 -15.17 0 Down (old vs young)
Endothelial_cells Slc9a3r2 -15.65 0 Down (old vs young)
Endothelial_cells 4931406P16Rik -15.98 0 Down (old vs young)
Endothelial_cells Col4a3bp -16.95 0 Down (old vs young)
Endothelial_cells Qk -16.96 0 Down (old vs young)
Endothelial_cells Ptprg -19.14 0 Down (old vs young)
Endothelial_cells AY036118 -19.19 0 Down (old vs young)
Endothelial_cells Gm42418 -22.23 0 Down (old vs young)
Macrophages Qki 12.17 0 Up (old vs young)
Macrophages Cyrib 11.80 0 Up (old vs young)
Macrophages ENSMUSG00000095041 10.93 0 Up (old vs young)
Macrophages Marchf1 10.77 0 Up (old vs young)
Macrophages Niban1 10.68 0 Up (old vs young)
Macrophages Lilrb4b 10.62 0 Up (old vs young)
Macrophages Gm56663 10.59 0 Up (old vs young)
Macrophages H2az1 10.12 0 Up (old vs young)
Macrophages Clec7a 9.97 0 Up (old vs young)
Macrophages Vis1 9.88 0 Up (old vs young)
Macrophages H2afz -10.43 0 Down (old vs young)
Macrophages March1 -10.80 0 Down (old vs young)
Macrophages Fam129a -11.06 0 Down (old vs young)
Macrophages Lilr4b -11.19 0 Down (old vs young)
Macrophages AC149090.1 -11.21 0 Down (old vs young)
Macrophages Gm26917 -11.27 0 Down (old vs young)
Macrophages Fam49b -12.36 0 Down (old vs young)
Macrophages Qk -12.63 0 Down (old vs young)
Macrophages AY036118 -14.05 0 Down (old vs young)
Macrophages Gm42418 -18.06 0 Down (old vs young)
T_memory_cells Cyrib 9.26 0 Up (old vs young)
T_memory_cells Bltp1 8.60 0 Up (old vs young)
T_memory_cells Abtb3 8.59 0 Up (old vs young)
T_memory_cells Niban1 8.38 0 Up (old vs young)
T_memory_cells Mideas 8.33 0 Up (old vs young)
T_memory_cells Qki 8.29 0 Up (old vs young)
T_memory_cells Gm56906 8.26 0 Up (old vs young)
T_memory_cells ENSMUSG00000095041 8.18 0 Up (old vs young)
T_memory_cells Septin7 8.08 0 Up (old vs young)
T_memory_cells H2az1 7.96 0 Up (old vs young)
T_memory_cells Fam129a -11.70 0 Down (old vs young)
T_memory_cells H2afz -11.91 0 Down (old vs young)
T_memory_cells AC149090.1 -11.94 0 Down (old vs young)
T_memory_cells 4932438A13Rik -11.94 0 Down (old vs young)
T_memory_cells Sept11 -11.96 0 Down (old vs young)
T_memory_cells Btbd11 -12.03 0 Down (old vs young)
T_memory_cells Gm26917 -12.97 0 Down (old vs young)
T_memory_cells Fam49b -13.06 0 Down (old vs young)
T_memory_cells AY036118 -14.40 0 Down (old vs young)
T_memory_cells Gm42418 -17.92 0 Down (old vs young)
ggplot(top10_both,
       aes(x = fct_reorder(gene_symbol, avg_log2FC),
           y = avg_log2FC,
           color = direction)) +
  geom_point(size = 3) +
  geom_segment(aes(xend = gene_symbol, y = 0, yend = avg_log2FC),
               linewidth = 0.6) +
  facet_wrap(~ Cluster, scales = "free_y") +
  coord_flip() +
  scale_color_manual(values = c("Up (old vs young)" = "firebrick2",
                                "Down (old vs young)" = "dodgerblue3")) +
  labs(title = "Top 10 Up & Down DEGs per Cell Type (Aged vs Young)",
       x = "Gene", y = "log2FC (old vs young)", color = "") +
  theme_minimal(base_size = 12) +
  theme(legend.position = "top")