单细胞数据分析--多数据整合harmony聚类

复制代码
######################################################################
# 简化版:多数据集单细胞预处理 + Harmony整合
# 用法:整个脚本分块跑(在RStudio里一段一段执行),
#       遇到 NULL 的地方,先看窗口里的图/数字,自己填完再往下跑。
######################################################################

library(Seurat)
library(dplyr)
library(tidyr)
library(harmony)
library(bluster)     # 算轮廓系数用,比cluster::silhouette快
library(scDblFinder)
library(babelgene)   # 小鼠转人ortholog
library(patchwork)

setwd("E:/OMV/宿主互作/single_cell/数据挖掘/多数据集整合")
set.seed(1)

## ========== 1. 读入7个样本 ==========

sample_info <- data.frame(
  dataset   = c("GSE190262","GSE190262","GSE220594","GSE220594",
                "GSE252663","GSE252663","GSE252663"),
  dir       = c("GSE190262_GSM5718727_ctrl","GSE190262_GSM5718728_KP",
                "GSE220594_GSM6807161_ctrl","GSE220594_GSM6807162_KP",
                "GSE252663_GSM8004971_KP","GSE252663_GSM8004972_KP",
                "GSE252663_GSM8004973_ctrl"),
  label     = c("GSE190262_Sham","GSE190262_hvKp","GSE220594_Sham","GSE220594_hvKp",
                "GSE252663_hvKp_1","GSE252663_hvKp_2","GSE252663_Sham"),
  condition = c("Sham","hvKp","Sham","hvKp","hvKp","hvKp","Sham")
)

seurat_list <- list()
for (i in 1:nrow(sample_info)) {
  counts <- Read10X(sample_info$dir[i])
  obj <- CreateSeuratObject(counts, project = sample_info$label[i],
                             min.cells = 3, min.features = 200)
  obj$dataset   <- sample_info$dataset[i]
  obj$sample    <- sample_info$label[i]
  obj$condition <- sample_info$condition[i]
  seurat_list[[i]] <- obj
}
lung <- merge(seurat_list[[1]], y = seurat_list[-1], add.cell.ids = sample_info$label)

## ========== 2. QC:先看图/看分位数,自己定阈值 ==========

lung[["percent.mt"]] <- PercentageFeatureSet(lung, pattern = "^mt-")
lung[["percent.hb"]] <- PercentageFeatureSet(
  lung, features = grep("^Hb[ab]-", rownames(lung), value = TRUE)
)

VlnPlot(lung, features = c("nFeature_RNA", "nCount_RNA", "percent.mt", "percent.hb"),
        group.by = "sample", pt.size = 0, ncol = 4)

quantile(lung$nFeature_RNA, seq(0.01, 0.10, 0.01))
quantile(lung$percent.mt,   seq(0.90, 1.00, 0.01))
quantile(lung$percent.hb,   seq(0.90, 1.00, 0.01))

# 看完上面的图和分位数,把下面4个值改成你自己的判断,再往下跑
qc_min_features   <- 200
qc_max_features   <- 6000
qc_max_percent_mt <- 20
qc_max_percent_hb <- 10

lung <- subset(lung, subset = nFeature_RNA > qc_min_features & nFeature_RNA < qc_max_features &
                  percent.mt < qc_max_percent_mt & percent.hb < qc_max_percent_hb)

## ========== 3. 每个样本单独去doublet ==========

lung_list <- SplitObject(lung, split.by = "sample")
lung_list <- lapply(lung_list, function(o) {
  sce <- as.SingleCellExperiment(o)
  sce <- scDblFinder(sce)
  o$scDblFinder.class <- sce$scDblFinder.class
  subset(o, subset = scDblFinder.class == "singlet")
})
lung <- merge(lung_list[[1]], y = lung_list[-1])
lung <- JoinLayers(lung)

## ========== 4.(可选)去解离/应激拉丝细胞 ==========
# 需要自己下载解离基因集,放到 markers/coregene_df-FALSE-v3.csv
# 没有这个文件就跳过这一段,不影响后面流程

lung <- NormalizeData(lung)

dis_file <- "markers/coregene_df-FALSE-v3.csv"
if (file.exists(dis_file)) {
  dis_genes <- read.csv(dis_file) %>%
    arrange(PValue) %>% slice(1:200) %>% pull(gene_symbol) %>% unique()
  dis_genes <- intersect(dis_genes, rownames(lung))

  lung <- AddModuleScore(lung, features = list(dis_genes), name = "dissociation_score")
  FeaturePlot(lung, features = "dissociation_score1")

  # 看完分布,自己定剔除比例,默认剔除最高1%,想改就改这个数
  dissociation_cutoff_pct <- 0.99
  lung <- subset(lung, subset = dissociation_score1 <
                    quantile(lung$dissociation_score1, dissociation_cutoff_pct))
}

## ========== 5. Harmony整合(用dataset+sample两个批次变量) ==========

lung[["RNA"]] <- split(lung[["RNA"]], f = lung$dataset)
lung <- lung %>%
  NormalizeData() %>%
  FindVariableFeatures(nfeatures = 2000) %>%
  ScaleData() %>%
  RunPCA(npcs = 40)
###############harmony整合#########
lung <- IntegrateLayers(
  lung, method = HarmonyIntegration, orig.reduction = "pca",
  new.reduction = "harmony", group.by.vars = c("dataset", "sample")
)
lung <- JoinLayers(lung)

##### 6. 选dims:几个候选值一起画出来,自己挑###########
## 法1:---- 累积方差贡献量化 ----
pct  <- lung[["pca"]]@stdev^2 / sum(lung[["pca"]]@stdev^2) * 100
cumu <- cumsum(pct)
co1 <- which(cumu > 90 & pct < 5)[1]
co2 <- sort(
  which((pct[1:(length(pct)-1)] - pct[2:length(pct)]) > 0.1),
  decreasing = TRUE
)[1] + 1
cat("co1 =", co1, " co2 =", co2, "\n")
pcs_suggested <- min(co1, co2, na.rm = TRUE)
cat("量化法建议的PC数:", pcs_suggested, "\n")

## 法2---- ElbowPlot看肘部 ----
ElbowPlot(lung, ndims = 40)

##法3:绘图
dims_try <- c(10,15,pcs_suggested,20,30,33,40)   # 把量化法给的值也加进候选里

dims_plots <- lapply(dims_try, function(d) {
  lung <- RunUMAP(lung, reduction = "harmony", dims = 1:d, reduction.name = paste0("umap", d))
  DimPlot(lung, reduction = paste0("umap", d), group.by = "dataset") +
    ggtitle(paste0("dims=1:", d)) + NoLegend()
})
pdf("UMAP_compare_dims.pdf", width = 15, height = 10)
print(wrap_plots(dims_plots, ncol = 3))   # 6张图,3列2行
dev.off()

# 看完图,自己填最终用的dims
use_dims <- 20

lung <- RunUMAP(lung, reduction = "harmony", dims = 1:use_dims, reduction.name = "umap", n.neighbors = 40, min.dist =0.1, spread = 0.3)
lung <- FindNeighbors(lung, reduction = "harmony", dims = 1:use_dims)

##### 7. 选resolution:轮廓系数 + 多分辨率UMAP ==========
####平均轮廓系数 mean silhouette####
res_try <- seq(0.1, 1, 0.1)   # 想改区间自己改

sil_score <- sapply(res_try, function(r) {
  lung <- FindClusters(lung, resolution = r)
  mean(approxSilhouette(Embeddings(lung, "harmony")[, 1:use_dims], lung$seurat_clusters)$width)
})
plot(res_try, sil_score, type = "b", xlab = "resolution", ylab = "mean silhouette")

res_plots <- lapply(res_try, function(r) {
  lung <- FindClusters(lung, resolution = r)
  DimPlot(lung, reduction = "umap", label = TRUE) +
    ggtitle(paste0("res=", r)) + NoLegend()
})
pdf("UMAP_compare_resolutions.pdf", width = 20, height = 12)
print(wrap_plots(res_plots, ncol = 4))
dev.off()

# 结合轮廓系数曲线 + UMAP + 后面的marker表达,自己定最终resolution
use_resolution <- 0.1
lung <- FindClusters(lung, resolution = use_resolution)

p_cluster <- DimPlot(lung, reduction = "umap", label = TRUE) + NoLegend() + ggtitle("Cluster")
p_dataset <- DimPlot(lung, reduction = "umap", group.by = "dataset") + ggtitle("Dataset")
p_condition <- DimPlot(lung, reduction = "umap", group.by = "condition") + ggtitle("Condition")
pdf("UMAP_final.pdf", width = 18, height = 6)
print(p_cluster | p_dataset | p_condition)
dev.off()

## ========== 8. 每个数据集单独看一次UMAP(按sample/condition着色) ==========

for (ds in unique(lung$dataset)) {
  print(
    DimPlot(subset(lung, dataset == ds), reduction = "umap", group.by = "sample") +
      ggtitle(ds)
  )
}

saveRDS(lung, "lung_integrated_before_annotation.rds")
readRDS("lung_integrated_before_annotation.rds")
## ========== 9. marker查看 + 人工注释 ==========

markers <- list(
  T_cell = c("Cd3e","Cd3d","Cd4","Cd8a","Trac"),
  NK = c("Ncr1","Klrb1c","Gzmb","Prf1"),
  B = c("Cd19","Cd79a","Ms4a1","Cd79b"),
  Plasma = c("Jchain","Mzb1","Xbp1","Sdc1"),
  Neutrophil = c("S100a8","S100a9","Retnlg","Ly6g","Cxcr2"),
  Monocyte_Macrophage = c("Lyz2","Cd68","Itgam","Csf1r","Adgre1"),
  Alveolar_Mac = c("Marco","Siglecf","Pparg","Ear2"),
  MoDerived_Mac = c("Lyz2","Ccr2","Fcgr1","Msr1"),
  DC = c("Itgax","Flt3","Xcr1","Clec9a","Ccr7"),
  Endothelial = c("Pecam1","Cldn5","Kdr","Vwf"),
  Epithelial = c("Epcam","Krt8","Krt18"),
  AT1 = c("Ager","Pdpn","Hopx"),
  AT2 = c("Sftpc","Sftpa1","Sftpb","Lamp3"),
  Ciliated = c("Foxj1","Tppp3","Pifo"),
  Club = c("Scgb1a1","Scgb3a2"),
  Fibroblast = c("Col1a1","Col1a2","Pdgfra","Dcn"),
  SmoothMuscle = c("Acta2","Tagln","Myh11"),
  RBC = c("Hba-a1","Hba-a2","Hbb-bs","Hbb-bt","Alas2"),
  Proliferating = c("Mki67","Top2a","Stmn1","Cdk1","Ccnb2")
)
marker_features <- intersect(unique(unlist(markers)), rownames(lung))
DotPlot(lung, features = marker_features) + RotatedAxis()

all_markers <- FindAllMarkers(lung, only.pos = TRUE, min.pct = 0.25, logfc.threshold = 0.25)
write.csv(all_markers, "cluster_markers.csv", row.names = FALSE)

top_markers <- all_markers %>% group_by(cluster) %>%
  arrange(desc(avg_log2FC), .by_group = TRUE) %>% slice_head(n = 15) %>%
  summarise(top_markers = paste(gene, collapse = ";"), .groups = "drop")
write.csv(top_markers, "cluster_top15_markers.csv", row.names = FALSE)

annotation_file <- "cluster_to_celltype.csv"
if (!file.exists(annotation_file)) {
  clusters <- sort(levels(Idents(lung)))
  write.csv(
    data.frame(cluster = clusters, celltype = paste0("Cluster_", clusters),
               top_markers = top_markers$top_markers[match(clusters, top_markers$cluster)]),
    annotation_file, row.names = FALSE
  )
  stop("已生成 ", annotation_file, ",根据 cluster_markers.csv / DotPlot 手动填好celltype列后重新运行。")
}

cluster_map <- read.csv(annotation_file, colClasses = "character")
lung$celltype <- cluster_map$celltype[match(as.character(Idents(lung)), cluster_map$cluster)]
Idents(lung) <- "celltype"

DimPlot(lung, reduction = "umap", group.by = "celltype", label = TRUE, repel = TRUE) + NoLegend()

for (ds in unique(lung$dataset)) {
  print(
    DimPlot(subset(lung, dataset == ds), reduction = "umap", group.by = "celltype",
            label = TRUE, repel = TRUE) + NoLegend() + ggtitle(ds)
  )
}

saveRDS(lung, "lung_annotated.rds")

## ========== 10. 剔除 artifact celltype,导出宿主表达矩阵(用于 MicrobioLink) ==========

library(Seurat)
library(Matrix)
library(dplyr)
library(tidyr)

## 输出目录要和 05_preflight_check.py 默认路径一致
dir.create("inputs", showWarnings = FALSE, recursive = TRUE)

## 1. 去掉 artifact / 低质量细胞类型
artifact_pattern <- "Doublet|LowQuality|RBC|Uncertain|Cycl|Proliferat"

keep_celltypes <- grep(
  artifact_pattern,
  unique(lung$celltype),
  ignore.case = TRUE,
  invert = TRUE,
  value = TRUE
)

lung_host <- subset(lung, subset = celltype %in% keep_celltypes)

lung_host$celltype <- droplevels(factor(lung_host$celltype))
lung_host$condition <- as.character(lung_host$condition)

## 检查 condition 名称是否正确
print(table(lung_host$condition))

if (!all(c("hvKp", "Sham") %in% unique(lung_host$condition))) {
  stop("condition 中必须包含 hvKp 和 Sham,请检查 lung_host$condition 的命名。")
}

lung_host$ct_cond <- paste(lung_host$celltype, lung_host$condition, sep = "_")

groups <- sort(unique(lung_host$ct_cond))

## 2. 提取 counts 和 data
counts <- GetAssayData(lung_host, layer = "counts")
data_norm <- GetAssayData(lung_host, layer = "data")

## 3. 计算每个 celltype_condition 中的表达比例 pct_expr
pct_mat <- sapply(groups, function(g) {
  cells_g <- colnames(lung_host)[lung_host$ct_cond == g]
  Matrix::rowMeans(counts[, cells_g, drop = FALSE] > 0)
})

pct_mat <- as.data.frame(pct_mat)
colnames(pct_mat) <- groups
pct_mat$symbol <- rownames(pct_mat)

## 4. 生成 0/1 表达矩阵:pct >= 0.10 认为表达
expressed <- pct_mat
expressed[groups] <- (expressed[groups] >= 0.10) * 1

## 只对至少在一个 group 中表达的 mouse gene 做同源转换
expressed_symbols <- expressed$symbol[rowSums(expressed[groups]) > 0]

ortho <- orthologs(
  genes = expressed_symbols,
  species = "mouse",
  human = FALSE
)

ortho <- ortho %>%
  select(symbol, human_symbol) %>%
  distinct() %>%
  filter(!is.na(symbol), !is.na(human_symbol))

## 5. 导出 MicrobioLink 需要的 human 0/1 表达矩阵
host_human <- expressed %>%
  inner_join(ortho, by = "symbol") %>%
  select(-symbol) %>%
  group_by(human_symbol) %>%
  summarise(across(all_of(groups), max), .groups = "drop")

write.csv(
  host_human,
  "inputs/host_expressed_human.csv",
  row.names = FALSE
)

## 6. 计算每个 celltype_condition 中的平均表达 avg_expr
avg_mat <- sapply(groups, function(g) {
  cells_g <- colnames(lung_host)[lung_host$ct_cond == g]
  Matrix::rowMeans(data_norm[, cells_g, drop = FALSE])
})

avg_mat <- as.data.frame(avg_mat)
colnames(avg_mat) <- groups
avg_mat$symbol <- rownames(avg_mat)

## 7. avg_expr 转成长表,并映射到 human_symbol
avg_long <- avg_mat %>%
  pivot_longer(
    cols = all_of(groups),
    names_to = "group",
    values_to = "avg_expr"
  ) %>%
  inner_join(ortho, by = "symbol") %>%
  mutate(
    condition = ifelse(grepl("_hvKp$", group), "hvKp", "Sham"),
    celltype = sub("_(hvKp|Sham)$", "", group)
  ) %>%
  group_by(human_symbol, celltype, condition) %>%
  summarise(avg_expr = max(avg_expr), .groups = "drop")

## 8. pct_expr 也转成长表,并映射到 human_symbol
pct_long <- pct_mat %>%
  pivot_longer(
    cols = all_of(groups),
    names_to = "group",
    values_to = "pct_expr"
  ) %>%
  inner_join(ortho, by = "symbol") %>%
  mutate(
    condition = ifelse(grepl("_hvKp$", group), "hvKp", "Sham"),
    celltype = sub("_(hvKp|Sham)$", "", group)
  ) %>%
  group_by(human_symbol, celltype, condition) %>%
  summarise(pct_expr = max(pct_expr), .groups = "drop")

## 9. 合并 avg_expr 和 pct_expr
expr_pair_long <- avg_long %>%
  full_join(
    pct_long,
    by = c("human_symbol", "celltype", "condition")
  ) %>%
  mutate(
    avg_expr = ifelse(is.na(avg_expr), 0, avg_expr),
    pct_expr = ifelse(is.na(pct_expr), 0, pct_expr)
  )

## 10. 转成 hvKp vs Sham 宽表,生成 preflight 需要的标准列名
expr_pair <- expr_pair_long %>%
  pivot_wider(
    id_cols = c(human_symbol, celltype),
    names_from = condition,
    values_from = c(avg_expr, pct_expr),
    values_fill = 0
  )

## 防止某些 celltype 里缺少 Sham 或 hvKp 时列不存在
required_cols <- c(
  "avg_expr_hvKp",
  "avg_expr_Sham",
  "pct_expr_hvKp",
  "pct_expr_Sham"
)

for (cc in required_cols) {
  if (!cc %in% colnames(expr_pair)) {
    expr_pair[[cc]] <- 0
  }
}

expr_pair <- expr_pair %>%
  mutate(
    delta_pct_hvKp_vs_Sham = pct_expr_hvKp - pct_expr_Sham,
    log2FC_expr_hvKp_vs_Sham = log2(
      (expm1(avg_expr_hvKp) + 1e-6) /
        (expm1(avg_expr_Sham) + 1e-6)
    )
  ) %>%
  select(
    human_symbol,
    celltype,
    avg_expr_hvKp,
    avg_expr_Sham,
    pct_expr_hvKp,
    pct_expr_Sham,
    delta_pct_hvKp_vs_Sham,
    log2FC_expr_hvKp_vs_Sham
  )

write.csv(
  expr_pair,
  "inputs/host_expression_hvKp_vs_Sham.csv",
  row.names = FALSE
)



## 12. 简单检查输出
cat("\n[OK] 已输出:inputs/host_expressed_human.csv\n")
cat("[OK] 已输出:inputs/host_expression_hvKp_vs_Sham.csv\n")


cat("host_expressed_human.csv 列名:\n")
print(colnames(host_human))

cat("\nhost_expression_hvKp_vs_Sham.csv 列名:\n")
print(colnames(expr_pair))

cat("\ncelltype 数量:\n")
print(length(unique(expr_pair$celltype)))

cat("\ncelltype 列表:\n")
print(sort(unique(expr_pair$celltype)))






## ========== 11. 读取已有marker表(跳过FindAllMarkers) ==========
## ========== 11. 用cluster_markers.csv + cluster_to_celltype.csv 生成celltype top marker ==========

library(Seurat)
library(dplyr)
library(tidyr)
library(ggplot2)
library(pheatmap)

cluster_markers <- read.csv("cluster_markers.csv", stringsAsFactors = FALSE)
cluster_map     <- read.csv("cluster_to_celltype.csv", colClasses = "character")

cluster_markers <- cluster_markers %>%
  mutate(celltype = cluster_map$celltype[match(as.character(cluster), cluster_map$cluster)])

celltype_markers <- cluster_markers %>%
  group_by(celltype, gene) %>%
  slice_max(avg_log2FC, n = 1, with_ties = FALSE) %>%
  ungroup()
write.csv(celltype_markers, "celltype_markers_merged.csv", row.names = FALSE)

n_top <- 4
top_genes_df <- celltype_markers %>%
  group_by(celltype) %>%
  arrange(desc(avg_log2FC), .by_group = TRUE) %>%
  slice_head(n = n_top) %>%
  ungroup()
write.csv(top_genes_df, "celltype_top_markers.csv", row.names = FALSE)

celltype_order <- c("T_cell","NK","B_cell",
                    "Interstitial_Mac","Alveolar_Mac","Neutrophil","Megakaryocyte_Platelet",
                    "Endothelial","Fibroblast","SmoothMuscle","Mesothelial",
                    "AT1","AT2","Ciliated")
celltype_order <- intersect(celltype_order, unique(lung$celltype))

## ---- 关键修正1:先把celltype不在celltype_order里的细胞剔除掉,避免NA identity ----
lung_plot <- subset(lung, subset = celltype %in% celltype_order)
lung_plot$celltype <- droplevels(factor(lung_plot$celltype, levels = celltype_order))
Idents(lung_plot) <- "celltype"

table(lung_plot$celltype)   # 跑完看一眼,确认没有NA、每类都有细胞数

top_genes_df$celltype <- factor(top_genes_df$celltype, levels = celltype_order)
top_genes_df <- top_genes_df %>% arrange(celltype)
gene_order <- unique(top_genes_df$gene)
gene_order <- intersect(gene_order, rownames(lung_plot))   # 防止基因名对不上报错

## ========== 12. 气泡图 DotPlot ==========

p_dot <- DotPlot(lung_plot, features = gene_order, group.by = "celltype") +
  RotatedAxis() +
  scale_color_gradient2(low = "blue", mid = "white", high = "red", midpoint = 0) +
  labs(x = NULL, y = NULL)
ggsave("DotPlot_top_markers.pdf", p_dot,
       width = 0.25 * length(gene_order) + 3,
       height = 0.3 * length(celltype_order) + 2)

## ========== 13. 热图:celltype平均表达 ==========

avg_exp <- AverageExpression(lung_plot, features = gene_order, group.by = "celltype",
                             assay = "RNA", layer = "data")$RNA

## ---- 关键修正2:AverageExpression会把identity名里的"_"换成"-",这里手动对应回来 ----
celltype_order_dash <- gsub("_", "-", celltype_order)
avg_exp <- avg_exp[, celltype_order_dash, drop = FALSE]
colnames(avg_exp) <- celltype_order   # 换回原来带下划线的名字,方便画图好看

avg_exp_scaled <- t(scale(t(avg_exp)))

pdf("Heatmap_top_markers_avgexpr.pdf", width = 5, height = 0.25 * length(gene_order) + 2)
pheatmap(avg_exp_scaled,
         cluster_rows = FALSE, cluster_cols = FALSE,
         color = colorRampPalette(c("blue","white","red"))(100),
         border_color = NA, fontsize_row = 8)
dev.off()

# 单细胞水平热图(downsample避免文件过大)
lung_sub <- subset(lung_plot, downsample = 300)
p_heat_sc <- DoHeatmap(lung_sub, features = gene_order, group.by = "celltype", size = 3) +
  scale_fill_gradientn(colors = c("blue","white","red"))
ggsave("DoHeatmap_top_markers_singlecell.pdf", p_heat_sc,
       width = 10, height = 0.25 * length(gene_order) + 3)

## ========== 14. 细胞比例堆叠图(hvKp vs Sham) ==========
## ========== 14. 细胞比例堆叠图(改进版:加百分比标签 + 重命名分组 + 换配色) ==========

library(RColorBrewer)
library(scales)

# ---- 统一配色:按celltype_order固定顺序上色,保证三张图配色/图例顺序完全一致 ----
n_types <- length(celltype_order)
pal <- colorRampPalette(brewer.pal(12, "Paired"))(n_types)
names(pal) <- celltype_order

# ---- 重命名条件:Sham -> Control, hvKp -> KP,并让Control排在左边 ----
recode_condition <- function(x) {
  factor(dplyr::recode(x, "Sham" = "Control", "hvKp" = "KP"),
         levels = c("Control", "KP"))
}

# 只显示占比较大的标签,避免小碎片文字堆在一起看不清(阈值可自己调)
label_threshold <- 0.03

## ---- 14.1 每个样本单独一根柱子 ----
prop_sample <- lung_plot@meta.data %>%
  dplyr::count(sample, dataset, condition, celltype) %>%
  dplyr::group_by(sample) %>%
  dplyr::mutate(fraction = n / sum(n)) %>%
  dplyr::ungroup() %>%
  dplyr::mutate(celltype  = factor(celltype, levels = celltype_order),
                condition = recode_condition(condition))

p_prop_sample <- ggplot(prop_sample, aes(x = sample, y = fraction, fill = celltype)) +
  geom_bar(stat = "identity", position = "fill", color = "white", linewidth = 0.2) +
  geom_text(data = subset(prop_sample, fraction >= label_threshold),
            aes(label = percent(fraction, accuracy = 1)),
            position = position_fill(vjust = 0.5), size = 2.3, color = "black") +
  facet_grid(~ dataset, scales = "free_x", space = "free_x") +
  scale_fill_manual(values = pal) +
  scale_y_continuous(labels = percent) +
  theme_bw(base_size = 11) +
  theme(axis.text.x = element_text(angle = 45, hjust = 1),
        panel.grid = element_blank()) +
  labs(x = NULL, y = "Fraction of cells", fill = "Cell type")
ggsave("Proportion_stackedbar_bysample.pdf", p_prop_sample, width = 11, height = 6)

## ---- 14.2 pool全部细胞按condition(仅展示用) ----
prop_condition_pooled <- lung_plot@meta.data %>%
  dplyr::count(condition, celltype) %>%
  dplyr::group_by(condition) %>%
  dplyr::mutate(fraction = n / sum(n)) %>%
  dplyr::ungroup() %>%
  dplyr::mutate(celltype  = factor(celltype, levels = celltype_order),
                condition = recode_condition(condition))

p_prop_condition <- ggplot(prop_condition_pooled, aes(x = condition, y = fraction, fill = celltype)) +
  geom_bar(stat = "identity", position = "fill", color = "white", linewidth = 0.3) +
  geom_text(data = subset(prop_condition_pooled, fraction >= label_threshold),
            aes(label = percent(fraction, accuracy = 1)),
            position = position_fill(vjust = 0.5), size = 3, color = "black") +
  scale_fill_manual(values = pal) +
  scale_y_continuous(labels = percent) +
  theme_bw(base_size = 12) +
  theme(panel.grid = element_blank()) +
  labs(x = NULL, y = "Fraction of cells", fill = "Cell type",
       title = "Pooled across datasets, exploratory (Control n=1)")
ggsave("Proportion_stackedbar_bycondition_pooled.pdf", p_prop_condition, width = 6.5, height = 6)

## ---- 14.3 先按样本算比例再取mean(推荐主图)----
prop_summary <- prop_sample %>%
  dplyr::group_by(condition, celltype) %>%
  dplyr::summarise(mean_fraction = mean(fraction), sd_fraction = sd(fraction),
                   n_sample = dplyr::n(), .groups = "drop")
write.csv(prop_summary, "Proportion_summary_by_condition.csv", row.names = FALSE)

p_prop_summary <- ggplot(prop_summary, aes(x = condition, y = mean_fraction, fill = celltype)) +
  geom_bar(stat = "identity", position = "fill", color = "white", linewidth = 0.3) +
  geom_text(data = subset(prop_summary, mean_fraction >= label_threshold),
            aes(label = percent(mean_fraction, accuracy = 1)),
            position = position_fill(vjust = 0.5), size = 3, color = "black") +
  scale_fill_manual(values = pal) +
  scale_y_continuous(labels = percent) +
  theme_bw(base_size = 12) +
  theme(panel.grid = element_blank()) +
  labs(x = NULL, y = "Mean fraction (per-sample averaged)", fill = "Cell type")
ggsave("Proportion_stackedbar_bycondition_samplemean.pdf", p_prop_summary, width = 6.5, height = 6)

伪bulk差异表达分析

复制代码
######################################################################
# 02 多数据集整合后差异分析 → 每个细胞类型导出 DEG
#
# 输入:
#   lung_annotated_multi_dataset.rds
#
# 输出:
#   DEG_multi_dataset/{celltype}_DEG.csv
#   DEG_multi_dataset/celltype_deg_summary_multi_dataset.csv
#   DEG_multi_dataset/DEG_multi_dataset_pseudobulk_skip_log.csv
#
# 设计:
#   hvKp = 4 个样本,Sham = 3 个样本
#   pseudobulk 按 sample 聚合,每个细胞类型单独做 DESeq2。
#
# 关键原则:
#   1) 差异分析使用 RNA counts,不使用 integrated.cca。
#   2) 分组名固定为 Sham / hvKp。
#   3) 多数据集分析优先使用 design = ~ dataset + condition;
#      如果某个细胞类型内模型不满秩,则自动退回 ~ condition。
#   4) 输出列名固定为 human_symbol, avg_log2FC, p_val_adj,方便对接 TieDIE。
######################################################################

options(stringsAsFactors = FALSE)

base_dir <- "E:/OMV/宿主互作/single_cell/数据挖掘/多数据集整合harmony"
setwd(base_dir)

suppressPackageStartupMessages({
  library(Seurat)
  library(dplyr)
  library(Matrix)
  library(babelgene)
  library(DESeq2)
})

######################################################################
# 0) 工具函数
######################################################################

sanitize_label <- function(x) {
  x <- as.character(x)
  x <- gsub("[^A-Za-z0-9_]+", "_", x)
  x <- gsub("_+", "_", x)
  x <- gsub("^_|_$", "", x)
  x
}

get_counts <- function(obj, assay = "RNA") {
  tryCatch(
    GetAssayData(obj, assay = assay, layer = "counts"),
    error = function(e) GetAssayData(obj, assay = assay, slot = "counts")
  )
}

empty_human_deg <- function() {
  data.frame(
    human_symbol = character(),
    avg_log2FC = numeric(),
    p_val_adj = numeric(),
    stringsAsFactors = FALSE
  )
}

write_empty_deg <- function(outdir, CT, reason) {
  write.csv(
    empty_human_deg(),
    file.path(outdir, sprintf("%s_DEG.csv", CT)),
    row.names = FALSE
  )
  data.frame(
    celltype = CT,
    status = "skipped",
    reason = reason,
    n_genes = 0,
    stringsAsFactors = FALSE
  )
}

format_deg_output <- function(res_df, mouse_col = "mouse_symbol", fc_col, p_col) {
  res_df <- res_df[!is.na(res_df[[mouse_col]]), ]

  if (nrow(res_df) == 0) {
    return(empty_human_deg())
  }

  ortho <- orthologs(genes = unique(res_df[[mouse_col]]), species = "mouse", human = FALSE)

  if (is.null(ortho) || nrow(ortho) == 0) {
    return(empty_human_deg())
  }

  out <- merge(
    res_df,
    ortho[, c("symbol", "human_symbol")],
    by.x = mouse_col,
    by.y = "symbol"
  )

  if (nrow(out) == 0) {
    return(empty_human_deg())
  }

  out <- out[, c("human_symbol", fc_col, p_col)]
  colnames(out) <- c("human_symbol", "avg_log2FC", "p_val_adj")

  out <- out[!is.na(out$human_symbol), ]
  out$avg_log2FC <- as.numeric(out$avg_log2FC)
  out$p_val_adj <- as.numeric(out$p_val_adj)

  out <- out %>%
    group_by(human_symbol) %>%
    summarise(
      avg_log2FC = avg_log2FC[which.max(abs(avg_log2FC))],
      p_val_adj = suppressWarnings(min(p_val_adj, na.rm = TRUE)),
      .groups = "drop"
    ) %>%
    mutate(
      p_val_adj = ifelse(is.infinite(p_val_adj), NA, p_val_adj)
    ) %>%
    select(human_symbol, avg_log2FC, p_val_adj)

  out
}

######################################################################
# 1) 读入对象
######################################################################

rds_file <- "lung_annotated.rds"

if (!file.exists(rds_file)) {
  stop(
    "找不到 ", rds_file,
    "\n请先运行 01 脚本,并完成 cluster_to_celltype_multi_dataset.csv 的人工细胞注释。"
  )
}

lung <- readRDS(rds_file)

if (!"celltype" %in% colnames(lung@meta.data)) {
  stop("对象中没有 celltype 列,请先完成 01 脚本的细胞注释。")
}

lung$celltype <- sanitize_label(lung$celltype)
lung$condition <- factor(as.character(lung$condition), levels = c("Sham", "hvKp"))
lung$dataset <- factor(as.character(lung$dataset))
lung$sample <- as.character(lung$sample)

if (!all(na.omit(unique(lung$condition)) %in% c("Sham", "hvKp"))) {
  stop("condition 必须只能是 Sham 或 hvKp。")
}



bad_celltype_pattern <- "Cycl|Proliferat|Doublet|LowQuality|Low_quality|RBC|Erythrocyte|Hemoglobin|Uncertain"
keep_cells <- colnames(lung)[!grepl(bad_celltype_pattern, as.character(lung$celltype), ignore.case = TRUE)]


lung <- subset(lung, cells = keep_cells)
lung$celltype <- droplevels(factor(lung$celltype))
Idents(lung) <- "celltype"

outdir <- file.path(base_dir, "DEG_multi_dataset")
dir.create(outdir, showWarnings = FALSE, recursive = TRUE)

cat("\n用于 DEG 的样本分布:\n")
print(table(lung$dataset, lung$condition))
cat("\n用于 DEG 的细胞类型 × 条件细胞数:\n")
print(table(lung$celltype, lung$condition))

write.csv(as.data.frame(table(lung$dataset, lung$condition)),
          file.path(outdir, "DEG_input_sample_distribution.csv"),
          row.names = FALSE)

write.csv(as.data.frame(table(lung$celltype, lung$condition)),
          file.path(outdir, "DEG_input_celltype_condition_cell_number.csv"),
          row.names = FALSE)

celltypes <- sort(unique(as.character(lung$celltype)))
cat("\n将处理这些细胞类型:\n")
print(celltypes)

#################pseudobulk + DESeq2###################################

run_pseudobulk <- function(lung,
                           celltypes,
                           outdir,
                           min_cells_per_celltype = 30,
                           min_cells_per_sample = 10,
                           min_samples_per_condition = 2) {
  counts_all <- get_counts(lung, assay = "RNA")
  meta <- lung@meta.data

  skip_log <- list()
  summary_log <- list()

  for (CT in celltypes) {
    cat("\n==== [pseudobulk] ", CT, " ====\n", sep = "")

    cells_ct <- rownames(meta)[as.character(meta$celltype) == CT]
    if (length(cells_ct) < min_cells_per_celltype) {
      reason <- paste0("该细胞类型总细胞数 < ", min_cells_per_celltype)
      cat("  跳过:", reason, "\n")
      skip_log[[CT]] <- write_empty_deg(outdir, CT, reason)
      next
    }

    meta_ct <- meta[cells_ct, , drop = FALSE]

    sample_cell_n <- table(meta_ct$sample)
    samp_ok <- names(sample_cell_n)[sample_cell_n >= min_cells_per_sample]
    meta_ct <- meta_ct[meta_ct$sample %in% samp_ok, , drop = FALSE]

    if (nrow(meta_ct) == 0) {
      reason <- paste0("没有 sample 在该细胞类型中达到 ", min_cells_per_sample, " 个细胞")
      cat("  跳过:", reason, "\n")
      skip_log[[CT]] <- write_empty_deg(outdir, CT, reason)
      next
    }

    sample_meta <- meta_ct %>%
      select(sample, condition, dataset) %>%
      distinct(sample, .keep_all = TRUE) %>%
      arrange(sample)

    sample_meta$condition <- factor(as.character(sample_meta$condition), levels = c("Sham", "hvKp"))
    sample_meta$dataset <- droplevels(factor(as.character(sample_meta$dataset)))

    cond_n <- table(sample_meta$condition)
    cat("  pseudobulk 样本数:", paste(names(cond_n), cond_n, collapse = ", "), "\n")

    if (length(cond_n) < 2 || any(cond_n < min_samples_per_condition)) {
      reason <- paste0(
        "该细胞类型中每组 pseudobulk 样本数不足 ",
        min_samples_per_condition,
        ";当前为 ",
        paste(names(cond_n), cond_n, collapse = ", ")
      )
      cat("  跳过:", reason, "\n")
      skip_log[[CT]] <- write_empty_deg(outdir, CT, reason)
      next
    }

    samples <- sample_meta$sample

    pb <- sapply(samples, function(s) {
      cs <- rownames(meta_ct)[meta_ct$sample == s]
      Matrix::rowSums(counts_all[, cs, drop = FALSE])
    })

    pb <- as.matrix(pb)
    mode(pb) <- "integer"
    colnames(pb) <- samples

    col_data <- as.data.frame(sample_meta)
    rownames(col_data) <- col_data$sample
    col_data <- col_data[colnames(pb), , drop = FALSE]

    # 低表达基因过滤:至少 2 个 pseudobulk 样本中 count >= 10
    keep_genes <- rowSums(pb >= 10) >= 2
    pb <- pb[keep_genes, , drop = FALSE]

    if (nrow(pb) < 10) {
      reason <- "过滤后可用于 DESeq2 的基因数 < 10"
      cat("  跳过:", reason, "\n")
      skip_log[[CT]] <- write_empty_deg(outdir, CT, reason)
      next
    }

    # 优先用 dataset + condition;如果某细胞类型内模型不满秩,则退回 condition。
    design_formula <- ~ dataset + condition

    if (nlevels(col_data$dataset) < 2) {
      design_formula <- ~ condition
    } else {
      mm <- model.matrix(design_formula, data = col_data)
      if (qr(mm)$rank < ncol(mm)) {
        cat("  dataset + condition 模型不满秩,退回 ~ condition\n")
        design_formula <- ~ condition
      }
    }

    dds <- tryCatch(
      DESeqDataSetFromMatrix(
        countData = round(pb),
        colData = col_data,
        design = design_formula
      ),
      error = function(e) {
        cat("  DESeqDataSetFromMatrix 失败:", conditionMessage(e), "\n")
        NULL
      }
    )

    if (is.null(dds)) {
      reason <- "DESeqDataSetFromMatrix 失败"
      skip_log[[CT]] <- write_empty_deg(outdir, CT, reason)
      next
    }

    dds <- tryCatch(
      DESeq(dds, sfType = "poscounts", quiet = TRUE),
      error = function(e) {
        cat("  DESeq 失败:", conditionMessage(e), "\n")
        NULL
      }
    )

    if (is.null(dds)) {
      reason <- "DESeq 失败"
      skip_log[[CT]] <- write_empty_deg(outdir, CT, reason)
      next
    }

    res <- tryCatch(
      as.data.frame(results(dds, contrast = c("condition", "hvKp", "Sham"))),
      error = function(e) {
        cat("  results 提取失败:", conditionMessage(e), "\n")
        NULL
      }
    )

    if (is.null(res) || nrow(res) == 0) {
      reason <- "results 为空"
      skip_log[[CT]] <- write_empty_deg(outdir, CT, reason)
      next
    }

    res$mouse_symbol <- rownames(res)
    res <- res[!is.na(res$padj), , drop = FALSE]

    if (nrow(res) == 0) {
      reason <- "所有基因 padj 均为 NA"
      cat("  跳过:", reason, "\n")
      skip_log[[CT]] <- write_empty_deg(outdir, CT, reason)
      next
    }

    out <- format_deg_output(
      res_df = res,
      mouse_col = "mouse_symbol",
      fc_col = "log2FoldChange",
      p_col = "padj"
    )

    stopifnot(identical(colnames(out), c("human_symbol", "avg_log2FC", "p_val_adj")))

    write.csv(out, file.path(outdir, sprintf("%s_DEG.csv", CT)), row.names = FALSE)

    summary_log[[CT]] <- data.frame(
      celltype = CT,
      status = "success",
      design = paste(deparse(design_formula), collapse = ""),
      n_cells = nrow(meta_ct),
      n_pseudobulk_samples = ncol(pb),
      n_sham_samples = as.integer(cond_n["Sham"]),
      n_hvkp_samples = as.integer(cond_n["hvKp"]),
      n_genes_tested_mouse = nrow(res),
      n_genes_output_human = nrow(out),
      stringsAsFactors = FALSE
    )

    cat(sprintf("  导出 %s_DEG.csv:%d human genes\n", CT, nrow(out)))
  }

  skip_df <- if (length(skip_log) > 0) {
    bind_rows(skip_log)
  } else {
    data.frame(
      celltype = character(),
      status = character(),
      reason = character(),
      n_genes = integer(),
      stringsAsFactors = FALSE
    )
  }

  summary_df <- if (length(summary_log) > 0) {
    bind_rows(summary_log)
  } else {
    data.frame()
  }

  write.csv(skip_df,
            file.path(outdir, "DEG_multi_dataset_pseudobulk_skip_log.csv"),
            row.names = FALSE)

  write.csv(summary_df,
            file.path(outdir, "celltype_deg_summary_multi_dataset.csv"),
            row.names = FALSE)

  invisible(list(summary = summary_df, skipped = skip_df))
}

######################################################################


######################################################################
# 4) 运行
######################################################################

res_pb <- run_pseudobulk(
  lung = lung,
  celltypes = celltypes,
  outdir = outdir,
  min_cells_per_celltype = 30,
  min_cells_per_sample = 10,
  min_samples_per_condition = 2
)


cat("\n全部完成!DEG 文件输出目录:\n")
cat(outdir, "\n")
cat("\n成功细胞类型:\n")
print(res_pb$summary)
cat("\n跳过细胞类型:\n")
print(res_pb$skipped)
相关推荐
Mr数据杨1 小时前
莫斯科公寓价格预测实战 从 Kaggle 房价回归到可落地估值流程
人工智能·数据分析·kaggle竞赛
KKKlucifer1 小时前
多模态数据精准识别:智能分类分级引擎在运营商场景的技术突破
人工智能·分类·数据挖掘
小白说大模型1 小时前
Hermes 全配置指南:从裸版到 AI Agent 天花板
大数据·人工智能·学习·算法·机器学习·数据挖掘
在所不辞兄1 小时前
【神经网络干货】可穿戴电子皮肤正在从“单点传感”走向“多通道、多模态、连续监测”
人工智能·神经网络·机器学习·数据挖掘
Highcharts.js1 小时前
Highcharts Orbit 管理器:让企业级可视化与数据分析实现平台化
信息可视化·数据挖掘·数据分析·可视化工具·highcharts·图表工具·管理器
FOORIR 客流统计1 小时前
智能客流系统未来趋势:3D视觉、AI识别与数据分析的融合架构
人工智能·3d·数据分析
Raas10013 小时前
MAI Gateway(魔芋企业级AI网关)对比分析:AI网关和API网关区别?企业级能力差距一览
大数据·人工智能·数据挖掘·mai gateway·企业级产品
SelectDB17 小时前
Apache Doris 5.0 年度版本前瞻(一):构建统一的多模湖仓实时分析平台
大数据·数据库·数据分析
橙时数据 FineInsight18 小时前
AI 能回答数据问题,但能解释业务原因吗?
大数据·人工智能·数据分析·指标平台·fineinsight