![](https://i-blog.csdnimg.cn/img_convert/4e4892546fd9f3e6d30ac6f513ad5839.png)
此处使用python版本的scRNA-seq处理工具scanpy,而不是R版本的seurat,因为seurat包安装繁杂
一,准备工作
1,python库的安装:
新建1个环境sc-python
python
mamba create -n sc-python -c conda-forge -y scanpy python-igraph leidenalg python=3.12
conda activate sc-python
![](https://i-blog.csdnimg.cn/img_convert/75358f8e1099b81874b08897d6025e8a.png)
二,AnnData数据结构理解
AnnData被设计用于矩阵样数据的处理,可以方便的获得矩阵行与列的索引。例如在scRNA-seq的数据中,每一行的数据对应为一个细胞表达的所有基因数据,每一列的数据对应为一个基因在所有细胞中的表达数据(这点与R中不同,Seurat对象的矩阵正好是AnnData的转置版)。不仅是表达矩阵,每个细胞(如样本来源、细胞类型等)及每个基因(如别名、基因ID等)还会有自己的注释信息。另外,考虑到单细胞矩阵的稀疏性、以及数据结构需要有用户友好型的特性,AnnData显得再合适不过。AnnData的基本结构可以看这个示意图:
![](https://i-blog.csdnimg.cn/img_convert/4c3c14be8a2bf6dacea228f7d08b202b.jpeg)
AnnData的数据结构是:行是样本(cell),列是gene
![](https://i-blog.csdnimg.cn/img_convert/58d04538f4a2fe8be5853be708ce632d.png)
![](https://i-blog.csdnimg.cn/img_convert/b60d284dd9cff381054dab4b31da0308.png)
1,构建模拟Anndata对象并查看
python
# 导包
import numpy as np
import pandas as pd
import anndata as ad #导入 anndata 库,并将其命名为 ad。anndata 是一个用于处理单细胞基因表达数据的库,提供了 AnnData 对象,用于存储和操作高维数据
from scipy.sparse import csr_matrix #从 scipy.sparse 模块中导入 csr_matrix 类。csr_matrix 是一种压缩稀疏行矩阵格式,用于高效存储和操作稀疏矩阵
print(ad.__version__)
![](https://i-blog.csdnimg.cn/img_convert/a1c9e3a6092a22756723468ce88eca6d.png)
python
# 模拟一个矩阵构建AnnData对象
counts = csr_matrix(np.random.poisson(1, size=(100, 2000)), dtype=np.float32) #将生成的随机矩阵转换为 scipy.sparse 模块中的压缩稀疏行矩阵(CSR 格式),并指定数据类型为 float32
temp_adata = ad.AnnData(counts)
print(f"这个AnnData对象包括{temp_adata.shape[0]}行,与{temp_adata.shape[1]}列") #使用 anndata 库的 AnnData 类创建一个 AnnData 对象,并将稀疏矩阵 counts 作为数据存储在其中
# 稀疏矩阵存放在temp_adata.X中
![](https://i-blog.csdnimg.cn/img_convert/9294dbb1d9fc6000f617fefc51c96113.png)
![](https://i-blog.csdnimg.cn/img_convert/ec5461e3790ece3752db0ab486034f28.png)
![](https://i-blog.csdnimg.cn/img_convert/226bb0732d371e79811466dd7d29e833.png)
![](https://i-blog.csdnimg.cn/img_convert/5f963f3a483ce12eb971569d0fb48a45.png)
![](https://i-blog.csdnimg.cn/img_convert/0d0231f3dcac49814e4a545a2d81c675.png)
![](https://i-blog.csdnimg.cn/img_convert/3defdff5f40e55d1695248bc294a3910.png)
主要的构建函数
![](https://i-blog.csdnimg.cn/img_convert/898fa1b50e5bbc97fb4e9b075cc0c1a2.png)
查看这里的counts,其实就是csr的稀疏矩阵格式,符合要求
对于构建生成的这个注释之后的数据对象,进行仔细分析查看:
数据对象
![](https://i-blog.csdnimg.cn/img_convert/118ae74364eeb64eb8880736caec8cc0.png)
数据类型
![](https://i-blog.csdnimg.cn/img_convert/30c68adfae8b7e9788462ba5c08ec4ea.png)
尺寸形状
![](https://i-blog.csdnimg.cn/img_convert/a66ca79c1e4be6b0d73ea8d242d5bbaa.png)
python
蓝色盒子:
该图标通常表示一个标准的类或对象。它表示对象具有相关的方法和属性,并可以通过其定义的函数或属性进行交互。这一视觉提示暗示对象内部存在一个明确定义的结构。
紫色盒子:
通常表示特定类型的对象或命名空间,特别是模块或包。这个符号暗示对象是一个集合,可能包含多个函数或其他子对象,为代码中的层级关系提供了视觉指示。
白色方框:
通常表示一个普通的函数或变量。该图标通常显示基本信息,例如函数签名或类型提示,表明这是一个简单的对象,没有特殊的内部结构。它为开发者提供了基础层次的信息。
扳手:
常用于表示可以对对象执行的一系列操作,如重构选项或编辑功能。此图标可能表明重命名、导航至对象定义或应用其他修改等操作,从而促进代码维护。
X存放的就是稀疏矩阵本身
![](https://i-blog.csdnimg.cn/img_convert/84694456ed81a4a2f51264defc8ea00c.png)
行
![](https://i-blog.csdnimg.cn/img_convert/17022721f7fd1ef89602117ab6c61326.png)
行数
![](https://i-blog.csdnimg.cn/img_convert/2af910c2d899e71271ab392b65304fe2.png)
行名
![](https://i-blog.csdnimg.cn/img_convert/de0fbc53dfbff96df47e9eca031533bc.png)
![](https://i-blog.csdnimg.cn/img_convert/31682aa0d41ca896c6940a0473339f1f.png)
列
![](https://i-blog.csdnimg.cn/img_convert/1fcd999f6019b9feb9ff61145a25b92e.png)
列数
![](https://i-blog.csdnimg.cn/img_convert/68ee580aa62d34b7fef77a079d97759b.png)
列名
![](https://i-blog.csdnimg.cn/img_convert/87f3c70c55c58f39ff0459102ea25d35.png)
后面都是空的slot
AnnData索引操作可以帮助我们取得对应的AnnData子集,这样我们就可以选取感兴趣的细胞或基因参与下游的计算,简化流程并节省计算资源。类似于pandas的DataFrame通过obs_names和var_names、布尔值、索引整数数字均可以完成对AnnData的取子集操作。(切片slice,bool/index)
我们的矩阵是刚才模拟出来的,可以用这样的方式添加上细胞名称与基因名称:
行使观测obs样本cell,列是变量var基因
python
# 生成细胞名称并传递给obs_names
temp_adata.obs_names = [f"Cell_{i:d}" for i in range(temp_adata.n_obs)]
# 生成基因名称并传递给var_names
temp_adata.var_names = [f"Gene_{i:d}" for i in range(temp_adata.n_vars)]
print(f"前五个细胞名称为:\n{temp_adata.obs_names[:5]}")
![](https://i-blog.csdnimg.cn/img_convert/08bcfc3675d637e406e930df0b3dfb81.png)
![](https://i-blog.csdnimg.cn/img_convert/7ce2f182acc95a5fa1b15253d48ba91c.png)
![](https://i-blog.csdnimg.cn/img_convert/07719fa83d87c4b65c2d4c7f46b50082.png)
修改之后
![](https://i-blog.csdnimg.cn/img_convert/cbf445896121fe8712b6d9c004cdab79.png)
就可以通过行列切片方式取出1个子集:bool或index
![](https://i-blog.csdnimg.cn/img_convert/4a529724eece2e8d486464a35a98d6b1.png)
python
# 那么子集就可以这么取:
temp_adata[["Cell_1", "Cell_10"], ["Gene_5", "Gene_1900"]]
# 这样就得到了一个包含"两个细胞"、"两个基因"的表达矩阵
![](https://i-blog.csdnimg.cn/img_convert/5ad12759a05ee3b8bc72bd7bb182ae73.png)
2,添加注释信息
其实前面已经观察到obs以及var都是一个slot对象,两个都是pandas.Dataframe对象,熟悉pandas语法即可
obs是行对象数据框,var是列对象数据框
![](https://i-blog.csdnimg.cn/img_convert/4fa697e09dc646179089a8b871e8518d.png)
adata.obs与adata.var均是DataFrame,那么就可以很容易的加上对应的注释,例如加上细胞类型的注释:
python
# 例如这里随机生成"B", "T", "Monocyte"三种细胞类型作为注释
ct = np.random.choice(["B", "T", "Monocyte"], size=(temp_adata.n_obs,))
print(f"ct的前10个元素为{ct[0:10]}")
# 加入adata.obs中
![](https://i-blog.csdnimg.cn/img_convert/3d7c618da8d5be74640741458d87e4c9.png)
生成的是一维数组
![](https://i-blog.csdnimg.cn/img_convert/469335f3adce0e4abe64ea8e381b2c98.png)
![](https://i-blog.csdnimg.cn/img_convert/f502bceb434dd66e08f6e83c96ba349f.png)
从提供的列表list(cell名称注释中)随机选取
![](https://i-blog.csdnimg.cn/img_convert/9e5038ce48eef3bf15af7d4353dc9ecf.png)
![](https://i-blog.csdnimg.cn/img_convert/5cf61f21d77f50d6e1e026742ef981cf.png)
然后这里的size提供int还是元组都可以
前面我们知道行obs是一个数据框,其实这个数据框没有列column,即var
![](https://i-blog.csdnimg.cn/img_convert/43dac7dca58a84a9c307c2741fc67725.png)
可以新增一列,也就是注释
python
temp_adata.obs["cell_type"] = pd.Categorical(ct) # Categorical的执行效率较高
# 可以看到adata.obs已经包含了我们刚刚添加的注释
temp_adata.obs
![](https://i-blog.csdnimg.cn/img_convert/aacdd6629e923af4d20cba6d5d9ba01f.png)
![](https://i-blog.csdnimg.cn/img_convert/f5a23f6b648db84e25443d4d31322cb0.png)
因为细胞cell很多,但是一般注释出来的类型很少,就固定那么几种,故cell类型其实就是分类变量,即R中的factor因子类型变量,其实就是相当于在R中做了一层factor()函数类型转换处理
![](https://i-blog.csdnimg.cn/img_convert/8dcd807f7de68506549f87a107ce3d61.png)
返回一维数组
![](https://i-blog.csdnimg.cn/img_convert/7059ac9b1929600b55e90caf196a87cb.png)
![](https://i-blog.csdnimg.cn/img_convert/e9fe78563dd5fd0a90df0dd545d0dbb5.png)
![](https://i-blog.csdnimg.cn/img_convert/c0780fef5e17a24604a75170bb36a948.png)
其实上面就相当于是给数据框添加列变量,因为行index已经提供,自己再补充填充列var即可
![](https://i-blog.csdnimg.cn/img_convert/ffd5851186dd54c10f23b8b8933d614e.png)
同样的,对行数据框做注释(添加新列)之后,也可以对列数据框做注释(添加新列)
同样处理,先生成对应的注释一维数组
python
# 例如这里随机生成"Pathway_A", "Pathway_B", "Pathway_C"三种通路名作为细胞的注释
my_path = np.random.choice(["Pathway_A", "Pathway_B", "Pathway_C"],
size=(temp_adata.n_vars,))
print(f"ct的前10个元素为{my_path[0:10]}")
![](https://i-blog.csdnimg.cn/img_convert/6e898e5255f9447e46392d891fb5786a.png)
同样是对列数据框添加列注释
python
temp_adata.var["my_pathway"] = pd.Categorical(my_path) # Categorical的执行效率较高
# 可以看到adata.var已经包含了我们刚刚添加的注释
temp_adata.var
![](https://i-blog.csdnimg.cn/img_convert/f3b1fc954b8afe965d7c64113b29cc00.png)
![](https://i-blog.csdnimg.cn/img_convert/dcf8fd638244ab4faee3c43a87abad3f.png)
行对象数据框obs的注释cell_type以及列对象数据框var的注释my_pathway;
然后这个时候可以进一步使用行以及列的注释来提取子集:
比如说对于行索引,行是cell,其属性(var)目前只有1列,即cell类型,那我就通过行对象的这一个属性来提取或者说是范围限制特定的行,
即 数据【数据行的条件,:】用于提取符合某些行条件的子集
其中**数据行的条件 **为 行对象数据框.列属性=某某条件
python
# 这时可以通过注释来选取数据的子集:
# 取出B cell的细胞数据
bcell_adata = temp_adata[temp_adata.obs.cell_type == "B",:]
# 取出仅包含Pathway_A的基因数据:
PA_adata = temp_adata[:,temp_adata.var.my_pathway == "Pathway_A"]
![](https://i-blog.csdnimg.cn/img_convert/80d96ef898930e410d78786aa90661d7.png)
在该单细胞的稀疏矩阵中,属于B类型的只有40个细胞,总共是100个细胞;
属于通路A的只有662个gene,总共是2000个gene;
当然,在实际分析过程中,往往会有非常复杂且大量的注释,比如说对于行对象,也就是cell,
可能会有时序信息,也就是发育等信息;
病患来源信息,以及亚型信息,以及组织采集位点信息等
python
# 构建一个略显"复杂"的注释DataFrame
obs_meta = pd.DataFrame({
'time_yr': np.random.choice([0, 2, 4, 8], temp_adata.n_obs),
'subject_id': np.random.choice(['subject 1', 'subject 2', 'subject 4', 'subject 8'], temp_adata.n_obs),
'instrument_type': np.random.choice(['type a', 'type b'], temp_adata.n_obs),
'site': np.random.choice(['site x', 'site y'], temp_adata.n_obs),
},
index=temp_adata.obs.index, # 需要与AnnData的observations相一致
)
obs_meta.head()
创新数据框的时候需要提供行索引temp_adata.obs.index
而这仅仅只是行的注释,即对cell的注释,实际过程中还会有对gene的注释
![](https://i-blog.csdnimg.cn/img_convert/ebdcf15c4543c491a65424ec90b26e29.png)
现在再回到我们这里的定义:
我们在定义这个注释数据框的时候可以提供很多对象以及注释信息
![](https://i-blog.csdnimg.cn/img_convert/1585243a992ee56e3ef75e3052621234.png)
比如说是提供稀疏矩阵,以及分别行数据框以及列数据框对象的注释信息
python
# 将注释添加到AnnData对象中:
new_adata = ad.AnnData(temp_adata.X, obs=obs_meta, var=temp_adata.var)
new_adata
# 可以看到多出很多注释内容
3,注释矩阵管理
刚才我们添加的cell_type以及my_pathway包含的均是单个维度的注释信息(因为我们都是使用np.random.choice随机生成的一维矩阵,都是一维的注释信息),
而有些数据是以多维度的形式参与单细胞的数据处理。例如UMAP与tsne的降维矩阵,这些多维度的信息可以添加至.obsm或.varm中。需要注意的是,矩阵的长度需要与.n_obs和.n_vars相对应。
![](https://i-blog.csdnimg.cn/img_convert/9fe95d5733a5eebb77cfe9b622024ea8.png)
从上面我们可以看出,一维的注释信息我们可以从obs以及var中直接新增一列,
如果是多维的数据,我们需要在obsm以及varm中添加(m指代的是multi维度);
然后同样的,我们将obs以及var当做一维的注释数据框对象,
obsm以及varm也可以直接用于当做多维的注释数据框对象,我们同样可以像新增列名一样去增加属性
例如这里我们模拟一个UMAP和gene_stuff的结果进行添加:
python
temp_adata.obsm["X_umap"] = np.random.normal(0, 1, size=(temp_adata.n_obs, 2))
temp_adata.varm["gene_stuff"] = np.random.normal(0, 1, size=(temp_adata.n_vars, 5))
temp_adata
# 可以看到X_umap与gene_stuff数据被添加
![](https://i-blog.csdnimg.cn/img_convert/75284132394aa07a33e203cc4f273f15.png)
其实就是行cell多了一个2维的注释信息,列gene多了一个5维的注释信息
![](https://i-blog.csdnimg.cn/img_convert/e41cd825d23a80f1f86be1a8c9cf8713.png)
![](https://i-blog.csdnimg.cn/img_convert/1e6b523c946306d6bb6aa7718e0083ac.png)
其实和前面一样的道理,一维的数据我们直接对行obs以及列var对象新增一列,增加一维数组即可;
多为的数据,我们暂时没法直接在obs以及var对象中添加,所以需要在其他的参数对象中添加
4,非结构性的注释信息
这部分Unstructure metaData可以存放在.uns中,并且对数据格式、内容均无任何要求,可以存放一些提示信息:
同样是参数uns+新增1列var
python
temp_adata.uns["random"] = [1, 2, 3]
temp_adata.uns
![](https://i-blog.csdnimg.cn/img_convert/36ae8f2a5de32abc1dfbc0398221deac.png)
5,Layers
原始的矩阵数据可能会经计算生成一些新的数据,这时我们就可以将这些过程或结果数据存放在不同的Layers中,例如数据处理过程中产生的标准化数据log_transformed;
其实我个人觉得可以理解为channel也就是通道,因为我们的表达矩阵需要进行归一化等数据处理,所以相当于是不同层的矩阵数据叠加在一起,当然是不同的channel;
然后行或者是列的注释数据其实一般是不会变的,因为矩阵只是值发生了映射变换,实际上并没有变形shape,所以我们的行或者是列的注释信息还是对应的
python
temp_adata.layers["log_transformed"] = np.log1p(temp_adata.X)
temp_adata
![](https://i-blog.csdnimg.cn/img_convert/0882b423a2474417f8e68f6043d22de3.png)
注意log1p是自然对数e,是自然natural
先在layer中定义1个新矩阵,即先定义1个layer,再将layer转换为数据框dataframe
python
temp_adata.to_df(layer="log_transformed")# Layer也可以直接转换为矩阵
![](https://i-blog.csdnimg.cn/img_convert/7f60ab10868d7e4a3444d37380fd1477.png)
![](https://i-blog.csdnimg.cn/img_convert/d953465504c013c5bf007c759369d583.png)
将layer转换为df
![](https://i-blog.csdnimg.cn/img_convert/d402e499e8f2b29b3975c86d90cbb547.png)
python
# 也可以输出为csv文件:当然前面转换为数据框之后可以直接导出为csv文件
temp_adata.to_df(layer="log_transformed").to_csv("log_transformed.csv")
![](https://i-blog.csdnimg.cn/img_convert/4ac8af2c862979177cd9d1fa6253a559.png)
![](https://i-blog.csdnimg.cn/img_convert/56fb0eefea09d9fb0ea7fe6b2f4485ec.png)
![](https://i-blog.csdnimg.cn/img_convert/e401f8036e5901608383982f80c19640.png)
6,AnnData存储与读取
AnnData可以很方便的以h5ad的形式保存在本地,这种格式的读写速度实际体验起来均比rds文件要快。
![](https://i-blog.csdnimg.cn/img_convert/47245502a096c0818e5329d3aad0171b.png)
python
#保存为h5ad格式
temp_adata.write('my_results.h5ad', compression="gzip")
实际上就是AnnData object对象的write属性而已
![](https://i-blog.csdnimg.cn/img_convert/eb42e7cdca88532cf933eda70b3ee200.png)
python
adata = ad.read('my_results.h5ad', backed='r')
adata.isbacked #此时my_results.h5ad处于open状态,即被占用的状态
FutureWarning: anndata.read
is deprecated, use anndata.read_h5ad
instead. ad.read
will be removed in mid 2024
根据警告信息,anndata.read 方法已被弃用,建议使用 anndata.read_h5ad 方法来读取 .h5ad 文件
python
# 从文件中读取 AnnData 对象,以只读模式打开
adata = ad.read_h5ad('my_results.h5ad', backed='r')
# 检查 AnnData 对象是否处于 backed 模式
print(adata.isbacked) # 输出: True
# 关闭文件,释放资源
adata.file.close()
print(adata.isbacked) # 输出: True
![](https://i-blog.csdnimg.cn/img_convert/e1ad94b0531a90d5673ad11fe2a28097.png)
python
adata = ad.read_h5ad('my_results.h5ad', backed='r')
adata.isbacked #此时my_results.h5ad处于open状态,即被占用的状态,输出为True
# 此时adata可以被正常使用,且多出路径变量
adata.filename
# 当完成分析时可以解除占用:
adata.file.close()
# 这其实就是scanpy在分析相同的数据比Seurat占用更少内存的秘诀
![](https://i-blog.csdnimg.cn/img_convert/60b083fd4f93d5c3e221eb4be574fa73.png)
7,AnnData的Views与copies
与pandas的DataFrame一样,AnnData也可以通过Views来创建一个对象的"影子",这样既可以避免占据新的内存,又可以修改原有的对象
python
# 在做View时候也可以使用切片:
temp_adata[:5, ['Gene_1', 'Gene_3']]
# 而copy的方式会占据额外的内存
adata_subset = temp_adata[:5, ['Gene_1', 'Gene_3']].copy()
adata_subset