连接#

使用 concat()AnnData 对象可以通过两种操作的组合进行合并:连接和合并。

  • 连接是指我们保留每个对象的所有子元素,并以有序的方式堆叠这些元素。

  • 合并是将一组集合合并为一个结果集合,该集合包含来自对象的元素。

注意

这个函数借鉴了pandasxarray中的类似函数。用于控制连接的参数借鉴了pandas.concat(),而合并策略则受到xarray.merge()compat参数的启发。

连接#

让我们从一个例子开始:

>>> import scanpy as sc, anndata as ad, numpy as np, pandas as pd
>>> from scipy import sparse
>>> from anndata import AnnData
>>> pbmc = sc.datasets.pbmc68k_reduced()
>>> pbmc
AnnData object with n_obs × n_vars = 700 × 765
    obs: 'bulk_labels', 'n_genes', 'percent_mito', 'n_counts', 'S_score', 'G2M_score', 'phase', 'louvain'
    var: 'n_counts', 'means', 'dispersions', 'dispersions_norm', 'highly_variable'
    uns: 'bulk_labels_colors', 'louvain', 'louvain_colors', 'neighbors', 'pca', 'rank_genes_groups'
    obsm: 'X_pca', 'X_umap'
    varm: 'PCs'
    obsp: 'distances', 'connectivities'

如果我们按观察的簇将这个对象拆分,然后堆叠这些子集,我们将获得相同的值——只是顺序不同。

>>> groups = pbmc.obs.groupby("louvain", observed=True).indices
>>> pbmc_concat = ad.concat([pbmc[inds] for inds in groups.values()], merge="same")
>>> assert np.array_equal(pbmc.X, pbmc_concat[pbmc.obs_names].X)
>>> pbmc_concat
AnnData object with n_obs × n_vars = 700 × 765
    obs: 'bulk_labels', 'n_genes', 'percent_mito', 'n_counts', 'S_score', 'G2M_score', 'phase', 'louvain'
    var: 'n_counts', 'means', 'dispersions', 'dispersions_norm', 'highly_variable'
    obsm: 'X_pca', 'X_umap'
    varm: 'PCs'

请注意,我们默认沿观察值进行连接,并且大多数与观察值对齐的元素也被连接了。 一个显著的例外是 obsp,它可以通过 pairwise 关键字参数重新启用。 这是因为结合用0填充的图形或距离矩阵并不是显而易见的特别有用,可能会让人感到困惑。

内连接和外连接#

当要连接的对象中的变量不完全相同时,您可以选择取这些变量的交集或并集。 这被称为进行"inner"(交集)或"outer"(并集)连接。 例如,给定两个具有不同变量的anndata对象:

>>> a = AnnData(sparse.eye(3, format="csr"), var=pd.DataFrame(index=list("abc")))
>>> b = AnnData(sparse.eye(2, format="csr"), var=pd.DataFrame(index=list("ba")))
>>> ad.concat([a, b], join="inner").X.toarray()
array([[1., 0.],
       [0., 1.],
       [0., 0.],
       [0., 1.],
       [1., 0.]])
>>> ad.concat([a, b], join="outer").X.toarray()
array([[1., 0., 0.],
       [0., 1., 0.],
       [0., 0., 1.],
       [0., 1., 0.],
       [1., 0., 0.]])

join参数用于任何同时具有(1)一个被连接的轴和(2)一个未被连接的轴的元素。
当沿着obs维度进行连接时,这意味着.Xobs.layers.obsm的元素将受到join选择的影响。

为了演示这一点,假设我们正在尝试将基于液滴的实验与空间实验相结合。 在构建联合的 anndata 对象时,我们仍然希望存储空间样本的坐标。

>>> coords = np.hstack([np.repeat(np.arange(10), 10), np.tile(np.arange(10), 10)]).T
>>> spatial = AnnData(
...     sparse.random(5000, 10000, format="csr"),
...     obsm={"coords": np.random.randn(5000, 2)}
... )
>>> droplet = AnnData(sparse.random(5000, 10000, format="csr"))
>>> combined = ad.concat([spatial, droplet], join="outer")
>>> sc.pl.embedding(combined, "coords")  

注释数据源 (label, keys, 和 index_unique)#

通常,您可能想知道哪些值来自于哪个对象。这可以通过 labelkeysindex_unique 关键字参数来实现。

例如,我们将展示如何通过传递一个Mapping的数据集名称到AnnData对象的concat来跟踪原始数据集:

>>> adatas = {
...     "a": ad.AnnData(
...         sparse.random(3, 50, format="csr", density=0.1),
...         obs=pd.DataFrame(index=[f"a-{i}" for i in range(3)])
...     ),
...     "b": ad.AnnData(
...         sparse.random(5, 50, format="csr", density=0.1),
...         obs=pd.DataFrame(index=[f"b-{i}" for i in range(5)])
...     ),
... }
>>> ad.concat(adatas, label="dataset").obs
    dataset
a-0       a
a-1       a
a-2       a
b-0       b
b-1       b
b-2       b
b-3       b
b-4       b

在这里,添加了一个分类列(名称由 label 指定)到结果中。作为传递 Mapping 的替代,您也可以通过 keys 参数指定数据集名称。

在某些情况下,您的对象可能在被连接的轴上共享名称。通过使用 index_unique 参数,可以通过附加相关键来使这些值唯一:

>>> adatas = {
...     "a": ad.AnnData(
...         sparse.random(3, 10, format="csr", density=0.1),
...         obs=pd.DataFrame(index=[f"cell-{i}" for i in range(3)])
...     ),
...     "b": ad.AnnData(
...         sparse.random(5, 10, format="csr", density=0.1),
...         obs=pd.DataFrame(index=[f"cell-{i}" for i in range(5)])
...     ),
... }
>>> ad.concat(adatas).obs  
Observation names are not unique. To make them unique, call `.obs_names_make_unique`.
Empty DataFrame
Columns: []
Index: [cell-0, cell-1, cell-2, cell-0, cell-1, cell-2, cell-3, cell-4]
>>> ad.concat(adatas, index_unique="_").obs
Empty DataFrame
Columns: []
Index: [cell-0_a, cell-1_a, cell-2_a, cell-0_b, cell-1_b, cell-2_b, cell-3_b, cell-4_b]

合并#

结合未对齐到连接轴的元素通过 merge 参数进行控制。 我们提供了一些策略来合并对齐到替代轴的元素:

  • None: 结果对象中没有与替代轴对齐的元素。

  • "same": 每个对象中相同的元素。

  • "unique": 只有一个可能值的元素。

  • "first": 每个位置看到的第一个元素。

  • "only": 只出现在其中一个对象中的元素。

我们将展示如何在替代轴上对齐元素,以及合并如何与 .uns 一起工作。首先,我们的示例案例:

>>> import scanpy as sc
>>> blobs = sc.datasets.blobs(n_variables=30, n_centers=5)
>>> sc.pp.pca(blobs)
>>> blobs
AnnData object with n_obs × n_vars = 640 × 30
    obs: 'blobs'
    uns: 'pca'
    obsm: 'X_pca'
    varm: 'PCs'

现在我们将通过类别 "blobs" 来拆分这个对象,并重新组合以说明不同的合并策略。

>>> adatas = []
>>> for group, idx in blobs.obs.groupby("blobs").indices.items():
...     sub_adata = blobs[idx].copy()
...     sub_adata.obsm["qc"], sub_adata.varm[f"{group}_qc"] = sc.pp.calculate_qc_metrics(
...         sub_adata, percent_top=(), inplace=False, log1p=False
...     )
...     adatas.append(sub_adata)
>>> adatas[0]
AnnData object with n_obs × n_vars = 128 × 30
    obs: 'blobs'
    uns: 'pca'
    obsm: 'X_pca', 'qc'
    varm: 'PCs', '0_qc'

adatas 现在是一个具有不重叠观察集合和共同变量集合的数据集列表。每个对象都计算了QC指标,观察-wise指标存储在"qc"下的.obsm中,变量-wise指标则为每个子集存储一个唯一的键。查看这如何影响连接:

>>> ad.concat(adatas)
AnnData object with n_obs × n_vars = 640 × 30
    obs: 'blobs'
    obsm: 'X_pca', 'qc'
>>> ad.concat(adatas, merge="same")
AnnData object with n_obs × n_vars = 640 × 30
    obs: 'blobs'
    obsm: 'X_pca', 'qc'
    varm: 'PCs'
>>> ad.concat(adatas, merge="unique")
AnnData object with n_obs × n_vars = 640 × 30
    obs: 'blobs'
    obsm: 'X_pca', 'qc'
    varm: 'PCs', '0_qc', '1_qc', '2_qc', '3_qc', '4_qc'

请注意,比较是在索引对齐后进行的。 也就是说,如果对象仅在替代轴上共享一部分索引,则仅在使用类似 "same" 的策略时,要求这些索引的值匹配。

>>> a = AnnData(
...     sparse.eye(3, format="csr"),
...     var=pd.DataFrame({"nums": [1, 2, 3]}, index=list("abc"))
... )
>>> b = AnnData(
...     sparse.eye(2, format="csr"),
...     var=pd.DataFrame({"nums": [2, 1]}, index=list("ba"))
... )
>>> ad.concat([a, b], merge="same").var
   nums
a     1
b     2

合并 .uns#

我们对合并 uns 使用相同的策略,正如我们对齐到一个轴的条目一样,但这些策略是递归应用的。 这有点抽象,所以我们将看一些例子。 这是我们的设置:

>>> from anndata import AnnData
>>> import numpy as np
>>> a = AnnData(np.zeros((10, 10)), uns={"a": 1, "b": 2, "c": {"c.a": 3, "c.b": 4}})
>>> b = AnnData(np.zeros((10, 10)), uns={"a": 1, "b": 3, "c": {"c.b": 4}})
>>> c = AnnData(np.zeros((10, 10)), uns={"a": 1, "b": 4, "c": {"c.a": 3, "c.b": 4, "c.c": 5}})

作为快速参考,这些是每种合并策略的结果。 下面将更深入地讨论这些内容:

uns_merge

结果

None

{}

"same"

{"a": 1, "c": {"c.b": 4}}

"unique"

{"a": 1, "c": {"c.a": 3, "c.b": 4, "c.c": 5}}

"only"

{"c": {"c.c": 5}}

"first"

{"a": 1, "b": 2, "c": {"c.a": 3, "c.b": 4, "c.c": 5}}

默认返回一个相当明显的结果:

>>> ad.concat([a, b, c]).uns == {}
True

但让我们更深入地看看其他内容。在这里,我们将输出数据包装在一个 dict 中,以简化返回值。

>>> dict(ad.concat([a, b, c], uns_merge="same").uns)
{'a': 1, 'c': {'c.b': 4}}

这里只有 uns["a"]uns["c"]["c.b"] 的值完全相同,所以只保留了它们。 uns["b"] 有多个值,且 uns["c"]["c.a"]uns["c"]["c.b"] 在每个 uns 中都没有出现。

一个需要注意的关键特征是比较意识到 uns 的嵌套结构,并将在任何深度应用。 这就是为什么 uns["c"]["c.b"] 被保留的原因。

以这种方式合并 uns 在被连接的对象之间存在一些共享数据时是非常有用的。比如,如果每个对象都经过相同的管道并使用相同的参数,则所使用的参数仍将存在于结果对象中。

现在让我们看看 unique 的行为:

>>> dict(ad.concat([a, b, c], uns_merge="unique").uns)
{'a': 1, 'c': {'c.a': 3, 'c.b': 4, 'c.c': 5}}

这里的结果是来自"same"的一个超集合。注意,在生成的映射中的每个位置只有一个可能的值。也就是说,尽管uns["c"]["c.c"]只出现了一次,但没有其他替代值存在。

当对象都通过同一管道运行但包含每个对象的特定元数据时,这可能很有用。 这方面的一个例子是空间数据集,其中图像存储在 uns

>>> dict(ad.concat([a, b, c], uns_merge="only").uns)
{'c': {'c.c': 5}}

uns["c"]["c.c"] 是唯一保留的值,因为它是唯一在一个 uns 中指定的值。

>>> dict(ad.concat([a, b, c], uns_merge="first").uns)
{'a': 1, 'b': 2, 'c': {'c.a': 3, 'c.b': 4, 'c.c': 5}}

在这种情况下,结果包含所有起始字典的键的并集。 值来自第一个在此键上具有值的对象。