如何优雅的阅读Seurat包

20211109(周二)
1.今天准备学习scanpy单细胞分析包,走在路上想,我用seurat和scanpy的意义在哪里,多学一个软件吗?
学习软件后面的代码逻辑 远远大于学会软件,后面抽时间一定要把seurat和scanpy的主要函数功能和代码底层逻辑弄清楚,
而不是盲目跑程序,不要只是工具包的使用者;
2.回来问XXX,学习的方法是什么?他说学习没有捷径而言,只有量变到质变的转化,你才能深入思考;

前面是之前的随记。于是就决定把seurat源码学习一下,尽量不要只做调“黑匣子”的工具人。


阅读seurat包的源码并非易事,seurat包是面向对象的编程思维,代码中有大量的S3,S4基于泛型函数(generic function)的调用。对于习惯面向过程编程的初学者,不易阅读和理解。代码中除了调用SeuratObject,sctransform等R包外,seurat引用了Rcpp调用C++函数,提高运算性能,不少函数还支持并行运算。就像做英语阅读理解一样,一半靠读,一半靠猜,猜不透就实操。读到后面发现底层都是数理统计和统计建模等知识,要不断的查资料,一点点消化,很难读通透,优雅也只是个愿景。
也不得不佩服开发seurat包的大师们功底深厚!在读的过程中,一些经验也可以慢慢记录和积攒,这份记录后面可能会修改,前面理解的不到位。
seurat的github地址:https://github.com/satijalab/seurat

1. 如何方便的查阅源码

我们一般去github网站seurat储存库查阅源码,比如想查看FindMarkers函数的代码,在github搜索框中输入“FindMarkers”,选择仅在此储存库中搜索即可。

image.png

另外一个更方便的途径是在chrome浏览器安装SourceGraph插件
通过单击github网站的seurat储存库主页上的 “Sourcegraph” 按钮或查看文件时,可以跳转到 "Sourcegraph"页面。使用Sourcegraph更好地搜索和浏览GitHub上的代码。
image.png

image.png

代码浏览界面的左侧是代码目录结构,就跟一般的IDE工程视图一样,你可以很轻松的在各个文件夹中查看文件,不用像在github那样来回前进后退。
鼠标单击相应的函数,出现的选项框可以选择跳转到定义,方便检索函数的调用关系。
image.png

2. R语言与面向对象编程

我们先对面向对象编程有个粗略的认识。
每种编程语言都有面向对象和面向过程的编程方式,非R语言独创。编程语言也是相互学习相互借鉴,尽量弥补自身的不足,R语言运行慢就调用C++。R语言虽然被认为是一种函数式语言, 但同样支持面向对象编程。
其实无论是面向对象还是面向过程,都是我们在编程时解决问题的一种思维方式。

面向过程(Procedure Oriented,简称PO):当解决一个问题的时候,我们一般会把所需要的步骤列出来,然后通过计算机中的函数把这些步骤逐一实现,这种过程化的叙事思维,就是面向过程思想。我们会把复用的代码提炼出来,写成公共函数,但没有类的概念。
面向对象(Object Oriented,简称OO):面向对象编程中有两个重要的术语“类”和“对象”。类是对某个事物的概括定义,可以看做是一个蓝图。对象则是对某个事物的具体实现,可以看做是依照蓝图建立起的房子。当解决一个问题的时候,面向对象会把事物抽象成对象的概念,就是说这个问题里面有哪些对象,然后给对象赋一些属性和方法,然后让每个对象去执行自己的方法,问题得到解决。
面向对象可以基于对象的共性做抽象,程序的特点是模块化和封装,继承、多态性的特性,可以设计出低耦合的系统,显著的改善了软件系统的可维护性,代码易维护、易复用、易扩展。很多大型软件开发选型上,大多会使用面向对象语言编程。

一个面向对象系统的核心是其实现的类 (class) 和方法(method)

  • 类定义了对象共同拥有的某些属性及其与其他类的关系
  • 如果一个对象属于某个类,则称该对象是这个类的实例(instance)
  • 方法是一种与特定类的对象关联的函数

R语言的面向对象跟其他Java,php等语言不是很相似,它是基于泛型函数(generic function)的,而不是基于类层次结构。
目前R中最热门的OOP系统主要有四种:S3,S4,R6, Reference Class。

问题:什么是泛型函数?
R语言是一种函数式语言,假如需要编写一个concat函数;
如果我们的数据是数组,我们就编写一个array_concat函数;
如果我们的数据是列表,我们就编写一个list_concat函数;
如果需求改变,需要写一个统一的函数,对数组和列表都能连接,于是有了concat函数。代码如下:

array_concat <- function(x,y) { 
  array(c(x, y))
}

list_concat <- function(x,y) {
  list(c(unlist(x), unlist(y)))
}

concat <- function(x, y) {
  if (is.array(x) && is.array(y)){
    return(array_concat(x, y))
  } else if (is.list(x) && is.list(y)) {
    return(list_concat(x, y))
  } else {
    stop("not supported type")
  }
}

但是,我们的需求一直在变化,如果新增不同的输入数据类型,上面的函数有得重写;每添加一种新的数据类型,concat函数都要修改,甚至重写,不便于代码的维护,可读性也很差,代码量很大,也不便于管理。于是,就有了开发者想开发一个“可扩展”的函数,在不改变原有代码的情况下,对函数能添加新的数据类型和属性。

R语言就有了泛型函数(Generic Function)的出现。
泛型函数是一个函数族,其中的每个函数都有相似的功能,实现对不同对象使用不同的方法。与一般函数相比,泛型函数多了一个判断对象类并传到指定函数的过程。例如concat.array处理array对象,而concat.list处理list对象。

3. 基于S3的面向对象编程

S3是目前最容易使用的方式,特别是处理R包时,使代码更易于理解和组织。

  • S3实现了一种基于泛型函数的面向对象方式
  • 泛型函数是一种特殊的函数, 其根据传入对象的类型决定调用哪个具体的方法

S3函数的创建
S3对象组成:generic(generic FUN)+method(generic.class FUN)
通常用UseMethod()函数定义一个泛型函数的名称,通过传入参数的class属性,来确定对应的方法调用。

定义一个get_n_elements的泛型函数

  • 用UseMethod()定义get_n_elements泛型函数
  • 用get_n_elements.xxx的语法格式定义get_n_elements对象的行为
  • get_n_elements.default是默认行为
get_n_elements <- function(x,...)
{
    UseMethod("get_n_elements")
}

method(generic.class)函数,创建示例:

# Create a data.frame method for get_n_elements
get_n_elements.data.frame <- function(x, ...)
{
    nrow(x) * ncol(x) # or prod(dim(x))
}
 
# Create a default method for get_n_elements
# 在使用UseMethod调用时,先在methods中寻找对应class,如果都没有找到,则会调用default方法。
get_n_elements.default <- function(x,...)
{
    length(unlist(x))
}

4.seurat包的S3泛型函数

切换到seurat包,我们可以看到大量的S3泛型函数。

seurat的NAMESPACE文件可以看到大量的S3泛型函数及方法,也可以通过R语句查看。

image.png

# List Methods for S3 Generic Functions or Classes
methods(class="Seurat")

# S3 class
.S3methods(class="Seurat")

# 用pryr包中ftype()函数,检查FindMarkers类型
pryr::ftype(FindMarkers)
# [1] "s3"      "generic"

methods(Seurat::FindMarkers)
# [1] FindMarkers.Assay*    FindMarkers.default*  FindMarkers.DimReduc* FindMarkers.Seurat*  

pryr::is_s3_generic("FindMarkers")
# [1] TRUE

# getS3method(f, class) 显示泛型函数对特定对象方法的实现
getS3method("FindMarkers", "Seurat")

例如FindMarkers函数就是一个S3泛型函数;

# NAMESPACE
S3method(FindMarkers,Assay)
S3method(FindMarkers,DimReduc)
S3method(FindMarkers,Seurat)
S3method(FindMarkers,default)

FindMarkers <- function(object, ...) {
  UseMethod(generic = 'FindMarkers', object = object)
}

# differential_expression.R
FindMarkers.default <- function(){...}
FindMarkers.Assay <- function(){...}
FindMarkers.DimReduc <- function(){...}
FindMarkers.Seurat <- function(){...}

案例:我们有1个seurat对象seurat_obj,查找cluster0和cluster1的差异基因。

DefaultAssay(seurat_obj) <- "RNA" 
DEG_wilcox <- FindMarkers(seurat_obj, ident.1 = 0, ident.2= 1,  test.use = "wilcox", min.pct = 0.5, logfc.threshold = 0.5, only.pos = T) 
head(DEG_wilcox) 
#           p_val avg_log2FC pct.1 pct.2 p_val_adj 
# RBP7         0   1.698551 0.726 0.004         0 
# PGD          0   1.670035 0.823 0.046         0 
# AGTRAP       0   1.488062 0.833 0.136         0 
# TNFRSF1B     0   1.696910 0.858 0.119         0 
# EFHD2        0   2.097743 0.917 0.079         0 
# CDA          0   1.577383 0.760 0.001         0 
dim(DEG_wilcox) 
# [1] 789   5

class(seurat_obj)
# [1] "Seurat"
# attr(,"package")
# [1] "SeuratObject"

问题:我们该如何一层层去看FindMarkers代码?
说明:FindMarkers的具体解析会在《seurat-FindAllMarkers()源码解析》说明,我们只摘出重要的代码:
step1:通过判断class(seurat_obj),我们知道seurat_obj是seurat对象,先调用FindMarkers.Seurat()函数,将FindMarkers的参数传至FindMarkers.Seurat。

object <- seurat_obj 
ident.1 = 0 
ident.2= 1 
test.use = "wilcox" 
min.pct = 0.5 
logfc.threshold = 0.5 
only.pos = T

step2:FindMarkers.Seurat()代码中提取归一化的基因表达矩阵seurat_obj[['RNA']]@data,赋值给data.use,基因表达矩阵大小为36601 *8824 ,36601个基因,8824个细胞。计算一些具体参数后,继续传递给FindMarkers函数;
我们判断class(data.use),data.use为Assay,我们调用FindMarkers.Assay(),我们将前面获得的参数继续传递给FindMarkers.Assay。

# select which data to use 
assay <- assay %||% DefaultAssay(object = object) 
assay <- "RNA" 
data.use <- object[[assay]] 
dim(data.use)  
# [1] 36601  8824

...
  de.results <- FindMarkers( 
    object = data.use, 
    slot = slot, 
    cells.1 = cells$cells.1, 
    cells.2 = cells$cells.2, 
    features = features, 
    logfc.threshold = logfc.threshold, 
    test.use = test.use, 
    min.pct = min.pct, 
    min.diff.pct = min.diff.pct, 
    verbose = verbose, 
    only.pos = only.pos, 
    max.cells.per.ident = max.cells.per.ident, 
    random.seed = random.seed, 
    latent.vars = latent.vars, 
    min.cells.feature = min.cells.feature, 
    min.cells.group = min.cells.group, 
    pseudocount.use = pseudocount.use, 
    mean.fxn = mean.fxn, 
    base = base, 
    fc.name = fc.name, 
    densify = densify, 
    ... 
  ) 
  return(de.results)

class(data.use)
# [1] "Assay" 
# attr(,"package") 
# [1] "SeuratObject"

step3:我们就这样一层层查看传入对象的类型,决定调用哪个具体的方法,最后我们进入到FindMarkers.default()这个函数,完成对FindMarkers函数源码的解析。
我们熟知S3泛型函数的一些基本功能,就能顺藤摸瓜,一步步去解析seurat包的S3函数,也让我们的理解变得更清晰。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,588评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,456评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,146评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,387评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,481评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,510评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,522评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,296评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,745评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,039评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,202评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,901评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,538评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,165评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,415评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,081评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,085评论 2 352

推荐阅读更多精彩内容