Skip to content

CopyscAT 分析:基于 scATAC-seq 的 CNV 推断与肿瘤识别

作者: SeekGene
时长: 22 分钟
字数: 5.2k 字
更新: 2026-02-28
阅读: 0 次
ATAC + RNA 双组学 分析指南 Notebooks 拷贝数变异分析

背景介绍

染色体拷贝数变异(CNV)是肿瘤基因组不稳定性的典型标志,表现为染色体片段的大规模扩增或缺失。CNV 通过改变癌基因(如 MYC)或抑癌基因(如 TP53)的拷贝数,引发基因剂量效应,驱动肿瘤发生、转移及治疗抵抗。在单细胞分辨率下解析 CNV 对揭示肿瘤异质性尤为关键:依托恶性细胞特异的 CNV 事件(如 7 号染色体扩增、10 号染色体缺失),可在复杂微环境中区分肿瘤细胞与非恶性细胞(如 T 细胞、成纤维细胞),进而解析亚克隆特异性表观调控网络及其与微环境的动态互作。

Copy-scAT 面向 scATAC-seq 数据的 CNV 推断,通过整合染色质开放信号与基因组结构特征,在单细胞水平构建高精度 CNV 推断模型,实现恶性与非恶性细胞的鉴别与亚克隆精细刻画。

核心原理

  • 局灶性 CNV 检测:
    • 利用 1 Mbp 分箱策略校正技术偏倚,识别局灶性扩增事件(如 EGFR 双微体、染色体外 DNA/ecDNA 区域)。
  • 大尺度 CNV 推断:
    • 基于归一化后的染色体臂水平信号强度,结合高斯混合模型聚类,判别分段性(如 EGFR 扩增)及全染色体臂(如 7 号染色体增益)的拷贝数变异。
  • 信号整合与校正:
    • 通过变点分析识别显著信号峰值,结合归一化覆盖矩阵,提升 CNV 检测的灵敏度与特异性。

参数设置

请先挂载将要分析的云平台相关流程数据,挂载方式请参照 jupyter 使用教程

具体参数填写:

  • rds:挂载的流程相关的 rds 文件,RDS 文件是标准的 Seurat 对象文件,数据已经过预处理和多个样本合并
  • meta:挂载的流程相关的 meta 文件,是流程相关 rds 文件对应的 meta.data ;
  • species:物种信息,仅支持人;
  • SAMplenames:样本名,注意,为了兼容 SeekArc 输出的 fragment 文件命名方式,样本名需要包含 A,如下所示的"joint14_A","joint22_A",不能只写成"joint14","joint22";
  • reduction:降维方式,在可视化环节,将每个细胞 CNV 情况映射到降维图上(umap/tsne/lsi/atacumap/atactsne/wnnumap/wnntsne),方便直观比较不同细胞类型 CNV 的差异;
  • celltype_col:meta.data 中细胞类型列名,选择要分析的细胞类型或者聚类对应的标签。假如想对注释好的细胞类型进行分析,则选择其对应的标签,例如 CellAnnotation;
  • group_col:meta.data 中细胞分组的列名,样本分组情况,便于热图可视化 CNV 时,细胞的分组展示
  • resolution_cluster:meta.data 中细胞聚类列名;
  • minimumSegments:最小片段数​​:细胞过滤阈值,默认保留片段数 ≥ 40 的细胞,推荐直接使用默认参数
  • methods:auto 和 reference 两种选择,reference 表示以正常细胞作为参考来计算 SNV,auto 则表示不需要正常细胞做参考;
  • Normal_celltype:如果上面 methods=”reference”,这里则选择具体用什么细胞用作参考,一般建议用免疫 B 细胞或 T 细胞等作为参考。
  • file_path:待分析样本的 fragment 文件,支持多个样本;
R
# Parameters
rds = "/PROJ2/FLOAT/advanced-analysis/test/2388-5504-6349-581745468808125050/input.rds"
meta = "/PROJ2/FLOAT/advanced-analysis/test/2388-5504-6349-581745468808125050/meta.txt"
#outdir = "/PROJ2/FLOAT/advanced-analysis/test/2388-5504-6349-581745468808125050/output/"
species = "human"
samplenames = c("joint14_A","joint22_A")#为了兼容seekARC输出的fragment文件命名方式,样本名需要包含A,如下所示的"joint14_A","joint22_A",不能只写成"joint14","joint22";
reduction = "wnnumap"
celltype_col = "CellAnnotation"
group_col = ""
resolution_cluster = "wknn_res.0.5_d30_l2_50"
minimumSegments = "40"
methods = "reference"
Normal_celltype = "T_cells"
file_path=c("/PROJ2/FLOAT/advanced-analysis/test/2388-5504-6345-581728456711101050/joint14_A_fragments.tsv.gz","/PROJ2/FLOAT/advanced-analysis/test/2388-5504-6345-581728456711101050/joint22_A_fragments.tsv.gz")

环境准备

R 包加载

请选择 common_r 这个环境进行该整合教程的学习

R
options(warn = -1)
suppressWarnings({
  suppressMessages({
      invisible(capture.output(library(CopyscAT)))
      library(GenomicRanges)
      library(Seurat, lib.loc = "/PROJ2/FLOAT/shumeng/apps/miniconda3/envs/Geneactivity/lib/R/library",quietly = TRUE)
      library(tidyverse)
      library(ggplot2)
      library(ComplexHeatmap)
      library(circlize)
      library(RColorBrewer)
      library(base64enc, quietly = TRUE)
      library(DT, quietly = TRUE)
      library(htmltools)
      library(dplyr)
      library(data.table)
      source("/PROJ2/FLOAT/advanced-analysis/Seek_module_dev/CopyscAT/Module/bin/cnv_caller_functions_packaged.R")
  })
})  
# 5. 根据物种加载基因组数据库
suppressMessages({
    if(species=="human"){
        library(BSgenome.Hsapiens.UCSC.hg38, quietly = TRUE)
    } else {
        message("请安装相应物种的数据库")
    }
})
options(warn = 0)
R
genomeFilepath="/PROJ2/FLOAT/advanced-analysis/Seek_module_dev/CopyscAT/Module/bin/"
dir.create("./result", recursive = TRUE, showWarnings = FALSE)
setOutputFile("./result","result")
resolution.cluster=resolution_cluster
R
if (species == "human") {
    genomeFile <- paste0(genomeFilepath, "hg38_chrom_sizes.tsv")
    cytobandFile <- paste0(genomeFilepath, "hg38_1e+06_cytoband_densities_granges.tsv")
    cpgFile = paste0(genomeFilepath, "hg38_1e+06_cpg_densities.tsv")
} else {
  stop(paste0("Error: Unsupported species '", species, "'. Supported species are human."))
}

Fragment 文件预处理

SeekArctools 或 cellranger arc/ATAC 生成的 fragments 文件包含所有条形码(barcode)的片段信息,其中仅有一部分为真实细胞。为节省计算资源并与后续细胞注释保持一致,进行 CNV 分析前需先对 fragments 进行过滤:仅保留通过质控并用于下游分析的“细胞白名单”条形码。建议依据已确定的细胞元数据/注释表(如 QC 合格且被纳入分析的 barcodes 列表)对子集化 fragments,从而避免将低质量条形码或非细胞背景噪声引入 CNV 推断。

Fragment 文件过滤

SeekArctools 或 cellranger arc/ATAC 生成的 fragments 文件包含所有条形码(barcode)的片段信息,其中仅有一部分为真实细胞。为节省计算资源并与后续细胞注释保持一致,进行 CNV 分析前需先对 fragments 进行过滤:仅保留通过质控并用于下游分析的“细胞白名单”条形码。建议依据已确定的细胞元数据/注释表(如 QC 合格且被纳入分析的 barcodes 列表)对子集化 fragments,从而避免将低质量条形码或非细胞背景噪声引入 CNV 推断。

R
meta <- read.table(meta,sep="\t",header=T)  #读取挂载的meta文件,该文件包含挂载项目数据所有样本的barcode信息
Barcodes <- meta$barcodes #提取全部Barcode

# 修改循环,为每个样本分别保存文件
for(sample in samplenames){
    file_path <- grep(pattern = sample, file_paths, value = TRUE) # 注意这里应该是 file_paths(复数)
    frag <- fread(file_path, header=F)
    
    # 修正这里的列名赋值语法错误
    colnames(frag) <- c("chr", "start", "end", "Barcode", "reads") # 使用 <- 赋值
    
    frag_filter <- frag[Barcode %in% Barcodes]
    
    # 为每个样本分别保存文件,命名格式:{sample}_filtered_fragments.tsv
    output_file <- paste0("./", sample, "_filtered_fragments.tsv")
    write.table(frag_filter, 
                file = output_file, 
                quote = FALSE, 
                row.names = FALSE, 
                col.names = TRUE, # 写入列名,因为每个文件都是独立的
                sep = "\t")
}

Fragment 文件 bin 处理

说明

CopyscAT 对 scATAC-seq 数据进行 CNV 分析之前,需要对过滤后发 fragment 进一步处理,将每条染色体分成 1000kb 长度的 bin,统计每个 bin 的转座酶切割事件,生成一个新的矩阵文件,行是细胞,列是染色体 bin 的编号,如 chr1_0 表示对第一天染色体将前 1000kb 碱基合并成一个 bin,“0”表示这个 bin 的起始碱基,值表示对应 bin 中该 barcode 中检测到的转座事件总数。如下所示:
Cell_id chr1_0 chr1_1000000 chr1_2000000 chr1_3000000 chr1_4000000
GTAACCACGACCGCTAG_1 321.0 6057.0 4540.0 1942.0 2437.0
AACACACCGACCAGCGT_1 368.0 998.0 490.0 0.0 0.0
ACGTAGTATGTGAGGTC_1 363.0 1123.0 831.0 56.0 0.0
AACACACTACATGATTG_1 400.0 2629.0 476.0 658.0 172.0
TTACGCCCGACCTGCGA_1 261.0 1960.0 2450.0 1916.0 0.0
CAGGTATGCTTGACTGT_1 429.0 670.0 286.0 1018.0 0.0
AGTCAACGCTACCGACC_1 2264.0 14874.0 17016.0 11043.0 4900.0
CTTGAGACGACCGTGGT_1 921.0 4590.0 4726.0 1870.0 1255.0
CAACCAATACATTTGCC_1 762.0 4810.0 6431.0 5360.0 827.0

处理方式

去 CopyscAT 官方 github 复制 process_fragment_file.py 脚本和 hg38_chrom_sizes.tsv 文件,链接: https://github.com/spcdot/CopyscAT/blob/master/process_fragment_file.pyhttps://github.com/spcdot/CopyscAT/blob/master/hg38_references/hg38_chrom_sizes.tsv

然后按要求设定参数,例如:

  • python ./process_fragment_file.py -i ./SAMplefilteredfragments.tsvo./{SAMple}_filtered_output_1000kb.tsv -b 1000000 -f 0 -g ./hg38_chrom_sizes.tsv

CNV 分析

基于前述背景与参数设定,我们开始执行 Copy-scAT 的 CNV 推断流程。该流程将 scATAC-seq 片段数据转换为基因组固定窗口的覆盖度矩阵,通过多层次的分析策略,在单细胞层面解析染色体拷贝数变异。

执行步骤概述

  • 数据输入:使用上部分处理结果文件${SAMple}_filtered_output_1000kb.tsv 作为输入,支持多个样本的批量处理。
  • 局灶性 CNV 检测:基于归一化覆盖矩阵中的显著信号峰值(如染色体外 DNA/ecDNA 区域),通过变点分析识别局灶性扩增事件。
  • 大尺度 CNV 推断:利用归一化后的染色体臂水平信号强度,结合高斯混合模型聚类,判别分段性(如 EGFR 扩增)及全染色体臂(如 7 号染色体增益)的拷贝数变异。
  • 结果整合:区分恶性与非恶性细胞群,输出单细胞 CNV 图谱及亚克隆表观特征关联分析。

关键参数说明

  • 分箱策略:采用 1 Mbp 分箱策略,平衡 CNV 分辨率与统计稳健性
  • 信号校正:通过归一化处理校正技术偏倚,提升 CNV 检测的准确性
  • 聚类分析:结合高斯混合模型,支持局灶性与大尺度 CNV 的联合检测
  • 质量控制:自动过滤低质量信号,确保结果的可靠性
R
suppressWarnings({
    suppressMessages({
        sc=readRDS(rds)
        meta=read.table(meta, sep = "\t", header = T)
        rownames(meta)=meta$barcodes
        sc=AddMetaData(sc, metadata=meta)
        sc@meta.data$Sample <- gsub("_arc$", "_A", sc@meta.data$Sample)
        sc@meta.data$num <- sub("^.*_(\\d+)$", "\\1", sc@meta.data$barcodes)
    })
})
R
if (length(samplenames) > 1) {
    inputdata <- list()
    # 根据 samplename 长度动态生成 cellSuffix
    suffixes <- paste0("_", seq_along(samplenames))  # 生成如 "_1", "_2", ..., "_n"
    # 初始化环境(需根据样本数动态调整 cellSuffix)
    initialiseEnvironment(
        genomeFile = genomeFile,
        cytobandFile = cytobandFile,
        cpgFile = cpgFile,
        binSize = 1e6,
        minFrags = 1e4,
        cellSuffix = suffixes,  # 动态传入后缀
        lowerTrim = 0.5,
        upperTrim = 0.8
        )
    for (i in seq_along(samplenames)) {
        inputdata[[i]] <- readInputTable(paste0(outdir, samplenames[[i]], "_output_1000kb.tsv"))
        inputdata[[i]]$Barcode=rownames(inputdata[[i]])
    }
    scData <- rbindlist(inputdata)
    scData <- as.data.frame(scData)
    rownames(scData)=scData$Barcode
    #setDT(scData, keep.rownames = TRUE) 
    scData$Barcode=NULL
} else {
    # 单个样本时直接使用 "_1"
    initialiseEnvironment(
        genomeFile = genomeFile,
        cytobandFile = cytobandFile,
        cpgFile = cpgFile,
        binSize = 1e6,
        minFrags = 1e4,
        cellSuffix = "_1",  # 单个样本固定后缀
        lowerTrim = 0.5,
        upperTrim = 0.8
    )
    scData <- readInputTable(paste0(outdir, samplenames, "_output_1000kb.tsv"))
    rownames(scData) <- paste0(rownames(scData), "_1")
    sc@meta.data$barcodes=paste0(sc@meta.data$barcodes,"_1")
}
R
#PART 1: INITIAL DATA NORMALIZATION
#step 1 normalize the matrix
suppressWarnings({
  suppressMessages({
      scData_k_norm <- normalizeMatrixN(scData,logNorm = FALSE,maxZero=2000,imputeZeros = FALSE,blacklistProp = 0.8,blacklistCutoff=125,dividingFactor=1,upperFilterQuantile = 0.95)
      #collapse into chromosome arm level
      summaryFunction<-cutAverage
      scData_collapse<-collapseChrom3N(scData_k_norm,summaryFunction=summaryFunction,binExpand = 1,minimumChromValue = 100,logTrans = FALSE,tssEnrich = 1,logBase=2,minCPG=300,powVal=0.73) 
        })
})
R
options(repr.plot.width = 12, repr.plot.height = 6)
suppressWarnings({
  suppressMessages({
      #apply additional filters
      scData_collapse<-filterCells(scData_collapse,minimumSegments = minimumSegments,minDensity =0.1)
      cells=rownames(sc@meta.data[which(sc@meta.data$barcodes %in% colnames(scData_collapse)),])
      sc_filter=subset(sc, cells=cells)
      #show unscaled chromosome list
      graphCNVDistribution(scData_collapse,outputSuffix = ("_violinsn2"))
      #compute centers
      median_iqr <- computeCenters(scData_collapse,summaryFunction=summaryFunction)
              })
})
R
options(warn = -1)
suppressWarnings({
  suppressMessages({
      nmf_results<-identifyNonNeoplastic(scData_collapse,methodHclust="ward.D",cutHeight = 0.4)
      #?identifyNonNeoplastic
      write.table(x=rownames_to_column(data.frame(nmf_results$cellAssigns),var="Barcode"),file=str_c(scCNVCaller$locPrefix,scCNVCaller$outPrefix,"_nmf_clusters.csv"),quote=FALSE,row.names = FALSE,sep=",")
      #print(paste("Normal cluster is: ",nmf_results$clusterNormal))
                })
}) 
options(warn = 0)
R
options(warn = -1)
suppressWarnings({
  suppressMessages({
      #PART 2: ASSESSMENT OF CHROMOSOME-LEVEL CNVs 
      if(methods == "auto"){
          #print("methods is auto")
          #OPTION 1: identify chromosome-level amplifications using all cells to generate 'normal' control
          #identify chromosome-level amplifications
          candidate_cnvs<-identifyCNVClusters(scData_collapse,median_iqr,useDummyCells = TRUE,propDummy=0.25,minMix=0.01,deltaMean = 0.03,deltaBIC2 = 0.25,bicMinimum = 0.1, subsetSize=600,fakeCellSD = 0.08, uncertaintyCutoff = 0.55,summaryFunction=summaryFunction,maxClust = 4,mergeCutoff = 3,IQRCutoff= 0.2,medianQuantileCutoff = 0.4)
          #cleanup step
          candidate_cnvs_clean<-clusterCNV(initialResultList = candidate_cnvs,medianIQR = candidate_cnvs[[3]],minDiff=1.5) #= 1.5)
          #final results and annotation
          final_cnv_list<-annotateCNV4(candidate_cnvs_clean, saveOutput=TRUE,outputSuffix = "clean_cnv",sdCNV = 0.5,filterResults=TRUE,filterRange=0.8)
      
      }else if(methods == "reference"){
          #print("methods is reference")
          Idents(sc_filter)=sc_filter@meta.data[[celltype_col]]
          table(Idents(sc_filter))
          Normal_sc=subset(sc_filter, idents=Normal_celltype)
          Normal_cells=Normal_sc@meta.data$barcodes
          median_iqr <- computeCenters(scData_collapse %>% dplyr::select(chrom,Normal_cells),summaryFunction=summaryFunction)
          #setting medianQuantileCutoff to -1 and feeding non-neoplastic barcodes in as normalCells can improve accuracy of CNV calls
          candidate_cnvs<-identifyCNVClusters(scData_collapse,median_iqr,useDummyCells = TRUE,propDummy=0.25,minMix=0.01,deltaMean = 0.03,deltaBIC2 = 0.25,bicMinimum = 0.1, subsetSize=800,fakeCellSD = 0.09, uncertaintyCutoff = 0.65,summaryFunction=summaryFunction,maxClust = 4,mergeCutoff = 3,IQRCutoff = 0.25,medianQuantileCutoff = -1,normalCells=Normal_cells) 
          candidate_cnvs_clean<-clusterCNV(initialResultList = candidate_cnvs,medianIQR = candidate_cnvs[[3]],minDiff=1.0) #= 1.5)          
          #to save this data you can use annotateCNV4 as per usual
          #final_cnv_list<-annotateCNV4(candidate_cnvs_clean, saveOutput=TRUE,outputSuffix = "clean_cnv",sdCNV = 0.6,filterResults=TRUE,filterRange=0.4)
          final_cnv_list<-annotateCNV4B(candidate_cnvs_clean, Normal_cells, saveOutput=TRUE,outputSuffix = "clean_cnv",sdCNV = 0.6,filterResults=TRUE,filterRange=0.4,minAlteredCellProp = 0.5)

      }
          })
}) 
options(warn = 0)

热图可视化

用热图可视化细胞拷贝数变异情况,copy number 值在 2 附近,表示正常的 2 倍体;copy number 在 1-2 之间,表示染色质存在缺失,越靠近 1,表示缺失越严重;copy number 在 2-3 之间表示染色质存在扩增, 越靠近 3 表示扩增越严重。热图左侧色带分别表示将细胞按照细胞类型和细胞聚类情况进行排列,每行表示一个细胞,每列表示一个染色体臂,热图的颜色表示 CNV 分数,颜色越红表示扩增越严重,颜色越蓝表示缺失越严重。

R
options(warn = -1)
ht_opt$message <- FALSE

# 1. 读取数据
cnv_data <- final_cnv_list[[3]]
cnv_matrix <- cnv_data %>%
    tibble::column_to_rownames("rowname") %>%
    as.matrix()

# 2. 提取元数据
if(nchar(group_col) > 0){
  if(nchar(celltype_col) > 0){
    metadata <- sc_filter@meta.data[,c("raw_Sample", celltype_col, "barcode", resolution.cluster, group_col)]
  } else {
    metadata <- sc_filter@meta.data[,c("raw_Sample", "CellAnnotation", "barcode", resolution.cluster, group_col)]
  }
} else {
  if(nchar(celltype_col) > 0){
    metadata <- sc_filter@meta.data[,c("raw_Sample", celltype_col, "barcode", resolution.cluster)]
  } else {
    metadata <- sc_filter@meta.data[,c("raw_Sample", "CellAnnotation", "barcode", resolution.cluster)]
  }
}

# 3. 动态生成颜色 - 修正所有colorRampPalette函数
# 用于Group的颜色
if(nchar(group_col) > 0) {
  n_groups <- length(unique(metadata[[group_col]]))
  group_colors <- colorRampPalette(brewer.pal(min(9, 3), "Set1"))(n_groups)
}

# 用于Cluster的颜色
n_clusters <- length(unique(metadata[[resolution.cluster]]))
clusters_colors <- colorRampPalette(brewer.pal(min(8, 3), "Set2"))(n_clusters)

# 用于Sample的颜色
n_samples <- length(unique(metadata$Sample))
sample_colors <- colorRampPalette(brewer.pal(min(12, 3), "Paired"))(n_samples)

# 用于Cell type的颜色
if(nchar(celltype_col) > 0) {
  n_celltypes <- length(unique(metadata[[celltype_col]]))
  cell_type_colors <- colorRampPalette(brewer.pal(min(12, 3), "Set3"))(n_celltypes)
} else {
  n_celltypes <- length(unique(metadata$CellAnnotation))
  cell_type_colors <- colorRampPalette(brewer.pal(min(12, 3), "Set3"))(n_celltypes)
}

# 4. 准备热图颜色映射
temp <- log(cnv_matrix + 0.01)
d <- parallelDist::parallelDist(temp)
hc_re <- hclust(d, method = "ward.D2")
color_thre = c(min(cnv_matrix), max(cnv_matrix))
c <- colorRampPalette(rev(RColorBrewer::brewer.pal(n = 7, name = "RdYlBu")))(100)
colormap <- circlize::colorRamp2(c(1:100) * (color_thre[2] - color_thre[1]) * 0.01 + color_thre[1], c)

# 5. 创建注释 - 隐藏细胞类型图例
if(nchar(group_col) > 0){
    if(nchar(celltype_col) > 0){
        ha = rowAnnotation(
            Sample = metadata$Sample,
            Group = metadata[[group_col]],
            Cell_type = metadata[[celltype_col]],
            Cluster = metadata[[resolution.cluster]],
            col = list(
                Group = setNames(group_colors, unique(metadata[[group_col]])),
                Cluster = setNames(clusters_colors, unique(metadata[[resolution.cluster]])),
                Sample = setNames(sample_colors, unique(metadata$Sample)),
                Cell_type = setNames(cell_type_colors, unique(metadata[[celltype_col]]))
            ),
            show_annotation_name = TRUE,
            show_legend = c(TRUE, TRUE, TRUE, TRUE)  # 隐藏Cell_type的图例
        )
    } else {
        ha = rowAnnotation(
            Sample = metadata$Sample,
            Group = metadata[[group_col]],
            Cell_type = metadata$CellAnnotation,
            Cluster = metadata[[resolution.cluster]],
            col = list(
                Group = setNames(group_colors, unique(metadata[[group_col]])),
                Cluster = setNames(clusters_colors, unique(metadata[[resolution.cluster]])),
                Sample = setNames(sample_colors, unique(metadata$Sample)),
                Cell_type = setNames(cell_type_colors, unique(metadata$CellAnnotation))
            ),
            show_annotation_name = TRUE,
            show_legend = c(TRUE, TRUE, TRUE, TRUE)  # 隐藏Cell_type的图例
        )
    }
} else {
    if(nchar(celltype_col) > 0){
        ha = rowAnnotation(
            Sample = metadata$Sample,
            Cell_type = metadata[[celltype_col]],
            Cluster = metadata[[resolution.cluster]],
            col = list(
                Cluster = setNames(clusters_colors, unique(metadata[[resolution.cluster]])),
                Sample = setNames(sample_colors, unique(metadata$Sample)),
                Cell_type = setNames(cell_type_colors, unique(metadata[[celltype_col]]))
            ),
            show_annotation_name = TRUE,
            show_legend = c(TRUE, TRUE, TRUE)  # 隐藏Cell_type的图例
        )
    } else {
        ha = rowAnnotation(
            Sample = metadata$Sample,
            Cell_type = metadata$CellAnnotation,
            Cluster = metadata[[resolution.cluster]],
            col = list(
                Cluster = setNames(clusters_colors, unique(metadata[[resolution.cluster]])),
                Sample = setNames(sample_colors, unique(metadata$Sample)),
                Cell_type = setNames(cell_type_colors, unique(metadata$CellAnnotation))
            ),
            show_annotation_name = TRUE,
            show_legend = c(TRUE, TRUE, TRUE)  # 隐藏Cell_type的图例
        )
    }
}

# 6. 绘制热图
ht <- Heatmap(
    cnv_matrix,
    name = "Copy Number",
    col = colormap,
    left_annotation = ha,
    show_row_names = FALSE,
    cluster_rows = TRUE,
    cluster_columns = FALSE,
    row_split = metadata[,2],
    use_raster = TRUE,
    row_title = NULL,
    column_names_rot = 90,
    show_row_dend = FALSE,
    border = FALSE,
    column_names_gp = gpar(fontsize = 8),
    heatmap_legend_param = list(
        title = "Copy number",
        at = c(1, 1.5, 2, 2.5, 3),
        legend_height = unit(4, "cm"),
        grid_width = unit(0.5, "cm")
    )
)
# 8. 保存图片
invisible(pdf("./result/cnv_heatmap_annotated.pdf", width = 15, height = 10))
invisible(draw(ht))
invisible(dev.off())
draw(ht)
options(warn = 0)

可视化每条染色体的 CNV score

对每个染色体区域进行 CNV 评分分级(<1.5 为缺失(1),1.5-2.5 为正常(2),>2.5 为扩增(3)),通过将每个细胞每条染色体上的 CNV 分数映射到 UMAP 图中,可以直观看出哪些细胞簇哪条染色体发生了明显的染色体拷贝数变异。

R
options(warn = -1)
suppressWarnings({
  suppressMessages({

    celltype_colors <- c(
        "1" = "#7A94F4",          # 蓝色
        "2" = "#E4EAEA", # 橙色
        "3" = "#F769A3"
    )
      library(RColorBrewer)
      library(scales)  
      base_colors <- c("#EDD496","#65D1BF","#7ADAEA","#F2AFAF","#EA8F8F",
                 "#C1A9D3","#F2DA9E","#D9EF78","#EF9ED9","#C9BCC6","#8BD198")

      generate_cols <- function(n) {
          if (n <= 0) return(character(0))
          if (n <= length(base_colors)) {
            base_colors[1:n]
          } else {
              colorRampPalette(base_colors)(n)
          }
      }
      # 创建目录保存结果
      output_dir <- file.path("./result", "cnv_plots")
      dir.create(output_dir, recursive = TRUE, showWarnings = FALSE)

      # 定义保存和编码图像的函数
      save_and_encode_plot <- function(plot, filename, width, height) {
      # 保存图片文件
      full_path_pdf <- paste0(filename, ".pdf")
      temp_png <- tempfile(fileext = ".png")
      
      # 保存PDF
      ggsave(full_path_pdf, plot, width = width, height = height)
      # 临时保存PNG用于base64编码
      ggsave(temp_png, plot, width = width, height = height, dpi = 300)
      
      # 将图片转换为base64
      png_data <- readBin(temp_png, "raw", file.info(temp_png)$size)
      base64_img <- base64encode(png_data)
      
      # 删除临时PNG文件
      unlink(temp_png)
      
      return(list(
        base64 = base64_img,
        pdf_path = full_path_pdf
      ))
    }
      # 生成细胞类型注释图颜色
      if (nchar(celltype_col) > 0) {
      unique_celltype <- sort(unique(sc_filter@meta.data[[celltype_col]]))
      clusters_cols <- generate_cols(length(unique_celltype))
      identity_colors <- setNames(clusters_cols, unique_celltype)
      celltype_umap <- DimPlot(sc_filter, 
                          group.by = celltype_col, 
                          cols = identity_colors,
                          label = TRUE,
                          reduction = reduction) + NoLegend()
      } else {
          unique_celltype <- sort(unique(sc_filter@meta.data[["CellAnnotation"]]))
          clusters_cols <- generate_cols(length(unique_celltype))
          identity_colors <- setNames(clusters_cols, unique_celltype)
          celltype_umap <- DimPlot(sc_filter,
                          group.by = "CellAnnotation",
                          cols = identity_colors,
                          label = TRUE,
                          reduction = reduction) + NoLegend()
      }
      # 生成聚类分群图颜色
      unique_clusters <- sort(unique(sc_filter@meta.data[[resolution.cluster]]))
      clusters_cols <- generate_cols(length(unique_clusters))
      identity_colors <- setNames(clusters_cols, unique_clusters)
      cluster_umap <- DimPlot(sc_filter,
                       group.by = resolution.cluster,
                       cols = identity_colors,
                       label = TRUE,
                       reduction = reduction) + NoLegend()
      plot_filename2 <- file.path(output_dir, paste0(resolution.cluster, "_umap"))
      plot_filename3 <- file.path(output_dir, paste0("celltype", "_umap"))
      result2 <- save_and_encode_plot(
          plot = cluster_umap,
          filename = plot_filename2,
          width = 5,
          height = 4
      )
      result3 <- save_and_encode_plot(
          plot = celltype_umap,
          filename = plot_filename3,
          width = 5,
          height = 4
      )
      # 创建数据框用于展示
      image_df <- data.frame(
        Chromosome = character(),
        CNV_UMAP = character(),
        cell_culster=character(),
        celltype_culster=character(),
        stringsAsFactors = FALSE
      )
      # 初始化存储列表
      char <- list()
      # 遍历每个染色体列
      for(i in colnames(cnv_matrix)) {
          # 处理单个染色
          char[[i]] <- cnv_data %>%
          dplyr::select(rowname, i) %>%
          mutate(
              copy_number = case_when(
              .data[[i]] < 1.5 ~ "1",
              .data[[i]] >= 1.5 & .data[[i]] <= 2.5 ~ "2",
              .data[[i]] > 2.5 ~ "3"
              )
          )
          colnames(char[[i]]) <- c("Barcodes", paste0(i,"_scores"), i)
          rownames(char[[i]]) <- char[[i]]$Barcodes
          char[[i]] <- char[[i]][,-1]
          sc_filter <- AddMetaData(sc_filter, metadata=char[[i]])
      
          # 准备绘图数据
          max.col=max(cnv_matrix)
          #p=FeaturePlot(sc_filter, features=paste0(i,"_scores"),min.cutoff = 0,max.cutoff = max.col,raster=T)
          p=DimPlot(sc_filter, group.by=i,cols=celltype_colors,reduction = reduction)
          # 保存图片并获取base64编码
          plot_filename <- file.path(output_dir, paste0(i, "_umap"))
          result <- save_and_encode_plot(
            plot = p,
            filename = plot_filename,
            width = 5,
            height = 4
          )

          # 添加到数据框
          img_tag <- sprintf(
            '<a href="%s" target="_blank">
              <img src="/placeholder.svg" width="400px"/>
            </a>',
            result$pdf_path,
            result$base64
          )
        
          img_tag2 <- sprintf(
            '<a href="%s" target="_blank">
              <img src="/placeholder.svg" width="400px"/>
            </a>',
            result2$pdf_path,
            result2$base64
          )
           img_tag3 <- sprintf(
            '<a href="%s" target="_blank">
              <img src="/placeholder.svg" width="400px"/>
            </a>',
            result3$pdf_path,
            result3$base64
          )
          image_df <- rbind(image_df, data.frame(
            Chromosome = i,
            CNV_UMAP = img_tag,
            cluster_UMAP = img_tag2,
            celltype_UMAP = img_tag3
          ))
      
          # 也保存普通PDF(与原代码相同)
          #ggsave(paste0(outdir, i, "_umap.pdf"), plot = p, width = 5, height = 4)
        }
    
         # 创建交互式表格展示结果
         # 6. 创建交互式表格
         cnv_table <- DT::datatable(
           image_df,
           escape = FALSE,
           options = list(
             pageLength = 1,
             autoWidth = TRUE,
             scrollX = TRUE,
             dom = 'Bfrtip',
             server = TRUE,       # 强制服务器端模式
             deferRender = TRUE,  # 延迟渲染
             scroller = TRUE,     # 滚动加载
             columnDefs = list(
                   # 仅针对前两列(索引0和1)
                   list(targets = 0, width = "4%"),
                   list(targets = 1, width = "32%"),
                   list(targets = 2, width = "32%"),
                   list(targets = 2, width = "32%")
               )
           ),
           rownames = FALSE,
           filter = 'top',
           caption = htmltools::tags$caption(
             style = 'caption-side: top; text-align: center; font-size: 1.2em; font-weight: bold;',
             '染色体区域CNV拷贝数UMAP可视化'
           )
         )
    
         # 显示表格
         cnv_table
        })
})
options(warn = 0)

结果文件

├── cnv_plots
│ ├── chr*_umap.pdf 分别展示每条染色体臂的 cnv 变异情况
└── cnv_heatmap_annotated.pdf 拷贝数变异热图展示

结果目录:目录下包括此项分析所涉及的所有图片以及表格等结果文件

文献案例解析

  • 《Single-cell multi-omics sequencing uncovers region-specific plasticity of glioblastoma for complementary therapeutic targeting》

  • 《Single-cell landscape of primary central nervous system diffuse large B-cell lymphoma》

    在这项研究中,作者用 inferCNV 对 scRNA-seq 做拷贝数变异分析,用 Copy-scAT 对 scATAC-seq 做拷贝数变异分析,用于区分肿瘤细胞和非肿瘤细胞

参考资料

[1] Nikolic A, Singhal D, Ellestad K, et al.Copy-scAT: Deconvoluting single-cell chromatin accessibility of genetic subclones in cancer[J].Science advances, 2021, 7(42):eabg6045.DOI: 10.1126/sciadv.abg6045.

附录

.txt :结果数据表格文件,文件以制表符(Tab)分隔。unix/Linux/Mac 用户使用 less 或 more 命令查看;windows 用户使用高级文本编辑器 Notepad++ 等查看,也可以用 Microsoft Excel 打开。

.pdf :结果图像文件,矢量图,可以放大和缩小而不失真,方便用户查看和编辑处理,可使用 Adobe Illustrator 进行图片编辑,用于文章发表等。

.rds : 包含基因活力 assay 的 Seurat 对象,需要在 R 环境中打开查看和用于进一步分析。

0 条评论·0 条回复