学习资料来源:
- anndata主页:https://anndata-tutorials.readthedocs.io/en/latest/index.html
- 官网:https://anndata-tutorials.readthedocs.io/en/latest/getting-started.html【注意教程有两个版本,这里是latest版本的学习笔记】
本篇教程来自Adam in 2021 and Alex in 2017.的博客内容
在本教程中,我们将介绍中心对象AnnData(“Annotated Data”)的基本属性
AnnData是专门为类似矩阵的数据设计的。这意味着我们有观测,每一个观测都可以表示为-维向量,其中每个维度对应一个变量或特征。这个矩阵的行和列都是特殊的,因为它们都有索引。
例如,在scRNA-seq数据中,每一行对应一个带有barcode的细胞,每一列对应一个带有基因id的基因。此外,对于每个细胞和每个基因,我们可能有额外的元数据,比如(1)每个细胞的供体信息,或(2)每个基因的替代基因符号。最后,我们可能有其他非结构化元数据,比如用于绘图的调色板。不需要深入了解每一个基于python的数据结构,我们认为直到今天还没有其他的替代方案存在:
- 处理稀疏
- 处理非结构化数据
- 处理观察级和特性级元数据
- 是友好的
首先,加载包
anndata的版本为0.8.0
import numpy as np
import pandas as pd
import anndata as ad
from scipy.sparse import csr_matrix
print(ad.__version__)
0.8.0
初始化AnnData对象
让我们从构建一个基本的AnnData对象开始,该对象带有一些稀疏的counts信息,可能表示基因表达counts。
counts = csr_matrix(np.random.poisson(1, size=(100, 2000)), dtype=np.float32)
adata = ad.AnnData(counts)
adata
AnnData object with n_obs × n_vars = 100 × 2000
我们可以看到,AnnData提供了一个数据汇总统计的表示,我们传递的初始数据可以通过使用数据的稀疏矩阵来访问adata.X
adata.X
<100x2000 sparse matrix of type '<class 'numpy.float32'>'
with 126555 stored elements in Compressed Sparse Row format>
现在,我们使用.obs_names和.var_names为obs和var轴提供索引。
adata.obs_names = [f"Cell_{i:d}" for i in range(adata.n_obs)]
adata.var_names = [f"Gene_{i:d}" for i in range(adata.n_vars)]
print(adata.obs_names[:10])
Index(['Cell_0', 'Cell_1', 'Cell_2', 'Cell_3', 'Cell_4', 'Cell_5', 'Cell_6',
'Cell_7', 'Cell_8', 'Cell_9'],
dtype='object')
提取子集
这些索引值可用于AnnData的子集,AnnData提供了AnnData对象的视图。我们可以想象,这对将AnnData子集用于特定的细胞类型或感兴趣的基因模块非常有用。设置AnnData子集的规则与Pandas DataFrame非常相似。您可以在obs/var_names、布尔masks或cell index integers使用值。
adata[["Cell_1", "Cell_10"], ["Gene_5", "Gene_1900"]]
View of AnnData object with n_obs × n_vars = 2 × 2
添加metadata信息
Observation/Variable level
所以我们有了对象的核心,现在我们想在观察和变量级别添加元数据。这对AnnData来说非常简单,都是数据。adata.obs和adata.var是Pandas DataFrames
ct = np.random.choice(["B", "T", "Monocyte"], size=(adata.n_obs,))
# Categoricals are preferred for efficiency
adata.obs["cell_type"] = pd.Categorical(ct)
adata.obs
cell_type
Cell_0 T
Cell_1 Monocyte
Cell_2 Monocyte
Cell_3 T
Cell_4 B
... ...
Cell_95 Monocyte
Cell_96 Monocyte
Cell_97 T
Cell_98 B
Cell_99 T
[100 rows x 1 columns]
我们现在还可以看到,AnnData表示已经更新
adata
AnnData object with n_obs × n_vars = 100 × 2000
obs: 'cell_type'
使用metadata提取子集
我们还可以使用这些随机生成的细胞类型对AnnData进行子集化:
bdata = adata[adata.obs.cell_type == "B"]
bdata
View of AnnData object with n_obs × n_vars = 42 × 2000
obs: 'cell_type'
观察/变量级矩阵
我们也可能在任何级别上都有元数据,它有许多维度,比如数据的UMAP embedding。对于这种类型的元数据,AnnData有.obsm/.varm属性。我们使用 keys来识别插入的不同矩阵。.obsm/.varm的限制是.obsm矩阵的长度必须等于.n_obs个数,.varm矩阵的长度必须等于.n_vars个数。它们各自有不同的维数。
让我们从一个随机生成的矩阵开始,我们可以将其解释为我们想要存储的数据的UMAP嵌入,以及一些随机的基因级metadata:
adata.obsm["X_umap"] = np.random.normal(0, 1, size=(adata.n_obs, 2))
adata.varm["gene_stuff"] = np.random.normal(0, 1, size=(adata.n_vars, 5))
adata.obsm
看到AnnData再次更新:
adata
AnnData object with n_obs × n_vars = 100 × 2000
obs: 'cell_type'
obsm: 'X_umap'
varm: 'gene_stuff'
关于.obsm/.varm的Notes:
- “类似数组”的metadata可以来自Pandas DataFrame、 scipy sparse matrix或numpy dense array
- 当使用 Scanpy 时,它们的值(列)不容易绘制,而是从.obs很容易绘制,例如 UMAP 图
非结构化metadata
AnnData有.uns,它允许任何非结构化元数据。这可以是任何东西,比如包含一些在数据分析中有用的一般信息的列表或字典。
adata.uns["random"] = [1, 2, 3]
adata.uns
Layers
最后,原始核心数据可能有不同的形式,可能是normalized的,也可能不是。这些数据可以存储在AnnData的不同层中。例如,让我们对原始数据进行log转换,并将其存储在一个layer中
adata.layers["log_transformed"] = np.log1p(adata.X)
adata
AnnData object with n_obs × n_vars = 100 × 2000
obs: 'cell_type'
uns: 'random'
obsm: 'X_umap'
varm: 'gene_stuff'
layers: 'log_transformed'
转换成DataFrames
我们还可以要求AnnData从其中一个层返回一个DataFrame
adata.to_df(layer="log_transformed")
结果图:我们看到.obs_names/.var_names用于创建这个Pandas对象
保存结果
AnnData自带基于hdf5的持久文件格式:h5ad。如果包含少量类别的字符串列还不是类别,AnnData将自动转换为类别categoricals。
outdir = '/Pub/Users/project/scanpy/Anndata/'
adata.write(outdir + 'my_results.h5ad', compression="gzip")
总结介绍
AnnData已经成为Python中单细胞分析的标准,这是有充分理由的——它使用起来很简单,并且通过基于键的存储便利了更可复制的分析。它甚至变得更容易转换为流行的基于r的单细胞分析格式。
现在继续往下阅读,以更好地理解“views”, 磁盘上备份和其他细节。
Views and copies
为了好玩,我们来看看另一个元数据用例。想象一下,这些观察结果来自一项多年研究的仪器,该研究的样本来自不同地点的不同受试者。我们通常会以某种格式获取信息,然后将其存储在DataFrame中:
obs_meta = pd.DataFrame({
'time_yr': np.random.choice([0, 2, 4, 8], adata.n_obs),
'subject_id': np.random.choice(['subject 1', 'subject 2', 'subject 4', 'subject 8'], adata.n_obs),
'instrument_type': np.random.choice(['type a', 'type b'], adata.n_obs),
'site': np.random.choice(['site x', 'site y'], adata.n_obs),
},
index=adata.obs.index, # these are the same IDs of observations as above!
)
结果图:
这就是我们将读出数据与元数据连接起来的方式。当然,下面对X的调用的第一个参数也可以只是一个数据帧。
现在我们又有了一个单独的数据容器来跟踪所有内容:
adata = ad.AnnData(adata.X, obs=obs_meta, var=adata.var)
访问两个变量的前5行:
# This is a view!
adata[:5, ['Gene_1', 'Gene_3']]
# If we want an AnnData that holds the data in memory, let’s call .copy()
adata_subset = adata[:5, ['Gene_1', 'Gene_3']].copy()
对于view,我们可以查看一列的前三个元素
print(adata[:3, 'Gene_1'].X.toarray().tolist())
adata[:3, 'Gene_1'].X = [0, 0, 0]
print(adata[:3, 'Gene_1'].X.toarray().tolist())
如果您试图访问AnnData的view的部分,内容将自动复制,并生成一个数据存储对象。
adata_subset = adata[:3, ['Gene_1', 'Gene_2']]
adata_subset
adata_subset.obs['foo'] = range(3)
adata_subset
现在adata_subset存储实际数据,不再只是对数据的引用。
显然,您可以使用所有的pandas对序列或布尔索引进行切片。
adata[adata.obs.time_yr.isin([2, 4])].obs.head()
结果图如下:
大数据部分读取
如果一个.h5ad非常大,你可以使用备份模式将其部分读入内存:
adata = ad.read('my_results.h5ad', backed='r')
adata.isbacked
如果你这样做,你需要记住AnnData对象有一个打开的连接到用于读取的文件:
adata.filename
因为我们是在只读模式下使用它,所以不会损坏任何东西。为了继续本教程,我们仍然需要显式地关闭它:
adata.file.close()
和往常一样,应该使用with语句来避免打开的文件悬空(即将推出的特性)。
操作磁盘上的对象是可能的,但对于稀疏数据来说是实验性的。因此,我们在本教程中不讨论它。
下次见~