def interaction_matrix( adata: AnnData, cluster_key: str, connectivity_key: Optional[str] = None, normalized: bool = False, copy: bool = False, ) -> Optional[np.ndarray]: """ Compute interaction matrix for clusters. Parameters ---------- %(adata)s %(cluster_key)s %(conn_key)s normalized If `True`, each row is normalized to sum to 1. %(copy)s Returns ------- If ``copy = True``, returns the interaction matrix. Otherwise, modifies the ``adata`` with the following key: - :attr:`anndata.AnnData.uns` ``['{cluster_key}_interactions']`` - the interaction matrix. """ connectivity_key = Key.obsp.spatial_conn(connectivity_key) _assert_categorical_obs(adata, cluster_key) _assert_connectivity_key(adata, connectivity_key) graph = nx.from_scipy_sparse_matrix(adata.obsp[connectivity_key]) cluster = { i: { cluster_key: x } for i, x in enumerate(adata.obs[cluster_key]) } nx.set_node_attributes(graph, cluster) int_mat = np.asarray( nx.attr_matrix(graph, node_attr=cluster_key, normalized=normalized, rc_order=adata.obs[cluster_key].cat.categories)) if copy: return int_mat _save_data(adata, attr="uns", key=Key.uns.interaction_matrix(cluster_key), data=int_mat)
def centrality_scores( adata: AnnData, cluster_key: str, score: Optional[Union[str, Iterable[str]]] = None, connectivity_key: Optional[str] = None, copy: bool = False, n_jobs: Optional[int] = None, backend: str = "loky", show_progress_bar: bool = False, ) -> Optional[pd.DataFrame]: """ Compute centrality scores per cluster or cell type. Inspired by usage in Gene Regulatory Networks (GRNs) in :cite:`celloracle`. Parameters ---------- %(adata)s %(cluster_key)s score Centrality measures as described in :class:`networkx.algorithms.centrality` :cite:`networkx`. If `None`, use all the options below. Valid options are: - `{c.CLOSENESS.s!r}` - measure of how close the group is to other nodes. - `{c.CLUSTERING.s!r}` - measure of the degree to which nodes cluster together. - `{c.DEGREE.s!r}` - fraction of non-group members connected to group members. %(conn_key)s %(copy)s %(parallelize)s Returns ------- If ``copy = True``, returns a :class:`pandas.DataFrame`. Otherwise, modifies the ``adata`` with the following key: - :attr:`anndata.AnnData.uns` ``['{{cluster_key}}_centrality_scores']`` - the centrality scores, as mentioned above. """ connectivity_key = Key.obsp.spatial_conn(connectivity_key) _assert_categorical_obs(adata, cluster_key) _assert_connectivity_key(adata, connectivity_key) if isinstance(score, (str, Centrality)): centrality = [score] elif score is None: centrality = [c.s for c in Centrality] centralities = [Centrality(c) for c in centrality] graph = nx.from_scipy_sparse_matrix(adata.obsp[connectivity_key]) cat = adata.obs[cluster_key].cat.categories.values clusters = adata.obs[cluster_key].values fun_dict = {} for c in centralities: if c == Centrality.CLOSENESS: fun_dict[c.s] = partial( nx.algorithms.centrality.group_closeness_centrality, graph) elif c == Centrality.DEGREE: fun_dict[c.s] = partial( nx.algorithms.centrality.group_degree_centrality, graph) elif c == Centrality.CLUSTERING: fun_dict[c.s] = partial(nx.algorithms.cluster.average_clustering, graph) else: raise NotImplementedError( f"Centrality `{c}` is not yet implemented.") n_jobs = _get_n_cores(n_jobs) start = logg.info( f"Calculating centralities `{centralities}` using `{n_jobs}` core(s)") res_list = [] for k, v in fun_dict.items(): df = parallelize( _centrality_scores_helper, collection=cat, extractor=pd.concat, n_jobs=n_jobs, backend=backend, show_progress_bar=show_progress_bar, )(clusters=clusters, fun=v, method=k) res_list.append(df) df = pd.concat(res_list, axis=1) if copy: return df _save_data(adata, attr="uns", key=Key.uns.centrality_scores(cluster_key), data=df, time=start)
def nhood_enrichment( adata: AnnData, cluster_key: str, connectivity_key: Optional[str] = None, n_perms: int = 1000, numba_parallel: bool = False, seed: Optional[int] = None, copy: bool = False, n_jobs: Optional[int] = None, backend: str = "loky", show_progress_bar: bool = True, ) -> Optional[Tuple[np.ndarray, np.ndarray]]: """ Compute neighborhood enrichment by permutation test. Parameters ---------- %(adata)s %(cluster_key)s %(conn_key)s %(n_perms)s %(numba_parallel)s %(seed)s %(copy)s %(parallelize)s Returns ------- If ``copy = True``, returns a :class:`tuple` with the z-score and the enrichment count. Otherwise, modifies the ``adata`` with the following keys: - :attr:`anndata.AnnData.uns` ``['{cluster_key}_nhood_enrichment']['zscore']`` - the enrichment z-score. - :attr:`anndata.AnnData.uns` ``['{cluster_key}_nhood_enrichment']['count']`` - the enrichment count. """ connectivity_key = Key.obsp.spatial_conn(connectivity_key) _assert_categorical_obs(adata, cluster_key) _assert_connectivity_key(adata, connectivity_key) _assert_positive(n_perms, name="n_perms") adj = adata.obsp[connectivity_key] original_clust = adata.obs[cluster_key] clust_map = { v: i for i, v in enumerate(original_clust.cat.categories.values) } # map categories int_clust = np.array([clust_map[c] for c in original_clust], dtype=ndt) indices, indptr = (adj.indices.astype(ndt), adj.indptr.astype(ndt)) n_cls = len(clust_map) _test = _create_function(n_cls, parallel=numba_parallel) count = _test(indices, indptr, int_clust) n_jobs = _get_n_cores(n_jobs) start = logg.info( f"Calculating neighborhood enrichment using `{n_jobs}` core(s)") perms = parallelize( _nhood_enrichment_helper, collection=np.arange(n_perms), extractor=np.vstack, n_jobs=n_jobs, backend=backend, show_progress_bar=show_progress_bar, )(callback=_test, indices=indices, indptr=indptr, int_clust=int_clust, n_cls=n_cls, seed=seed) zscore = (count - perms.mean(axis=0)) / perms.std(axis=0) if copy: return zscore, count _save_data( adata, attr="uns", key=Key.uns.nhood_enrichment(cluster_key), data={ "zscore": zscore, "count": count }, time=start, )
def interaction_matrix( adata: AnnData, cluster_key: str, connectivity_key: Optional[str] = None, normalized: bool = False, copy: bool = False, weights: bool = False, ) -> Optional[np.ndarray]: """ Compute interaction matrix for clusters. Parameters ---------- %(adata)s %(cluster_key)s %(conn_key)s normalized If `True`, each row is normalized to sum to 1. %(copy)s weights Whether to use edge weights or binarize. Returns ------- If ``copy = True``, returns the interaction matrix. Otherwise, modifies the ``adata`` with the following key: - :attr:`anndata.AnnData.uns` ``['{cluster_key}_interactions']`` - the interaction matrix. """ connectivity_key = Key.obsp.spatial_conn(connectivity_key) _assert_categorical_obs(adata, cluster_key) _assert_connectivity_key(adata, connectivity_key) cats = adata.obs[cluster_key] mask = ~pd.isnull(cats).values cats = cats.loc[mask] if not len(cats): raise RuntimeError( f"After removing NaNs in `adata.obs[{cluster_key!r}]`, none remain." ) g = adata.obsp[connectivity_key] g = g[mask, :][:, mask] n_cats = len(cats.cat.categories) if weights: g_data = g.data else: g_data = np.broadcast_to(1, shape=len(g.data)) if pd.api.types.is_bool_dtype(g.dtype) or pd.api.types.is_integer_dtype( g.dtype): dtype = np.intp else: dtype = np.float_ output = np.zeros((n_cats, n_cats), dtype=dtype) _interaction_matrix(g_data, g.indices, g.indptr, cats.cat.codes.to_numpy(), output) if normalized: output = output / output.sum(axis=1).reshape((-1, 1)) if copy: return output _save_data(adata, attr="uns", key=Key.uns.interaction_matrix(cluster_key), data=output)
def moran( adata: AnnData, connectivity_key: str = Key.obsp.spatial_conn(), genes: Optional[Union[str, Sequence[str]]] = None, transformation: Literal["r", "B", "D", "U", "V"] = "r", n_perms: int = 1000, corr_method: Optional[str] = "fdr_bh", layer: Optional[str] = None, seed: Optional[int] = None, copy: bool = False, n_jobs: Optional[int] = None, backend: str = "loky", show_progress_bar: bool = True, ) -> Optional[pd.DataFrame]: """ Calculate Moran’s I Global Autocorrelation Statistic. Parameters ---------- %(adata)s %(conn_key)s genes List of gene names, as stored in :attr:`anndata.AnnData.var_names`, used to compute Moran's I statistics :cite:`pysal`. If `None`, it's computed :attr:`anndata.AnnData.var` ``['highly_variable']``, if present. Otherwise, it's computed for all genes. transformation Transformation to be used, as reported in :class:`esda.Moran`. Default is `"r"`, row-standardized. %(n_perms)s %(corr_method)s layer Layer in :attr:`anndata.AnnData.layers` to use. If `None`, use :attr:`anndata.AnnData.X`. %(seed)s %(copy)s %(parallelize)s Returns ------- If ``copy = True``, returns a :class:`pandas.DataFrame` with the following keys: - `'I'` - Moran's I statistic. - `'pval_sim'` - p-value based on permutations. - `'VI_sim'` - variance of `'I'` from permutations. - `'pval_sim_{{corr_method}}'` - the corrected p-values if ``corr_method != None`` . Otherwise, modifies the ``adata`` with the following key: - :attr:`anndata.AnnData.uns` ``['moranI']`` - the above mentioned dataframe. """ if esda is None or libpysal is None: raise ImportError( "Please install `esda` and `libpysal` as `pip install esda libpysal`." ) _assert_positive(n_perms, name="n_perms") _assert_connectivity_key(adata, connectivity_key) if genes is None: if "highly_variable" in adata.var.columns: genes = adata[:, adata.var.highly_variable.values].var_names.values else: genes = adata.var_names.values genes = _assert_non_empty_sequence(genes, name="genes") n_jobs = _get_n_cores(n_jobs) start = logg.info( f"Calculating for `{len(genes)}` genes using `{n_jobs}` core(s)") w = _set_weight_class(adata, key=connectivity_key) # init weights df = parallelize( _moran_helper, collection=genes, extractor=pd.concat, use_ixs=True, n_jobs=n_jobs, backend=backend, show_progress_bar=show_progress_bar, )(adata=adata, weights=w, transformation=transformation, permutations=n_perms, layer=layer, seed=seed) if corr_method is not None: _, pvals_adj, _, _ = multipletests(df["pval_sim"].values, alpha=0.05, method=corr_method) df[f"pval_sim_{corr_method}"] = pvals_adj df.sort_values(by="I", ascending=False, inplace=True) if copy: logg.info("Finish", time=start) return df _save_data(adata, attr="uns", key="moranI", data=df, time=start)
def spatial_autocorr( adata: AnnData, connectivity_key: str = Key.obsp.spatial_conn(), genes: Optional[Union[str, Sequence[str]]] = None, mode: Literal[ "moran", "geary"] = SpatialAutocorr.MORAN.s, # type: ignore[assignment] transformation: bool = True, n_perms: Optional[int] = None, two_tailed: bool = False, corr_method: Optional[str] = "fdr_bh", layer: Optional[str] = None, seed: Optional[int] = None, use_raw: bool = False, copy: bool = False, n_jobs: Optional[int] = None, backend: str = "loky", show_progress_bar: bool = True, ) -> Optional[pd.DataFrame]: """ Calculate Global Autocorrelation Statistic (Moran’s I or Geary's C). See :cite:`pysal` for reference. Parameters ---------- %(adata)s %(conn_key)s genes List of gene names, as stored in :attr:`anndata.AnnData.var_names`, used to compute global spatial autocorrelation statistic. If `None`, it's computed :attr:`anndata.AnnData.var` ``['highly_variable']``, if present. Otherwise, it's computed for all genes. mode Mode of score calculation: - `{sp.MORAN.s!r}` - `Moran's I autocorrelation <https://en.wikipedia.org/wiki/Moran%27s_I>`_. - `{sp.GEARY.s!r}` - `Geary's C autocorrelation <https://en.wikipedia.org/wiki/Geary%27s_C>`_. transformation If `True`, weights in :attr:`anndata.AnnData.obsp` ``['{key}']`` are row-normalized, advised for analytic p-value calculation. %(n_perms)s If `None`, only p-values under normality assumption are computed. two_tailed If `True`, p-values are two-tailed, otherwise they are one-tailed. %(corr_method)s layer Layer in :attr:`anndata.AnnData.layers` to use. If `None`, use :attr:`anndata.AnnData.X`. %(seed)s %(copy)s %(parallelize)s Returns ------- If ``copy = True``, returns a :class:`pandas.DataFrame` with the following keys: - `'I' or 'C'` - Moran's I or Geary's C statistic. - `'pval_norm'` - p-value under normality assumption. - `'var_norm'` - variance of `'score'` under normality assumption. - `'{{p_val}}_{{corr_method}}'` - the corrected p-values if ``corr_method != None`` . If ``n_perms != None`` is not None, additionally returns the following columns: - `'pval_z_sim'` - p-value based on standard normal approximation from permutations. - `'pval_sim'` - p-value based on permutations. - `'var_sim'` - variance of `'score'` from permutations. Otherwise, modifies the ``adata`` with the following key: - :attr:`anndata.AnnData.uns` ``['moranI']`` - the above mentioned dataframe, if ``mode = {sp.MORAN.s!r}``. - :attr:`anndata.AnnData.uns` ``['gearyC']`` - the above mentioned dataframe, if ``mode = {sp.GEARY.s!r}``. """ _assert_connectivity_key(adata, connectivity_key) if genes is None: if "highly_variable" in adata.var.columns: genes = adata[:, adata.var.highly_variable.values].var_names.values else: genes = adata.var_names.values genes = _assert_non_empty_sequence(genes, name="genes") mode = SpatialAutocorr(mode) # type: ignore[assignment] if TYPE_CHECKING: assert isinstance(mode, SpatialAutocorr) params = { "mode": mode.s, "transformation": transformation, "two_tailed": two_tailed } if mode == SpatialAutocorr.MORAN: params["func"] = _morans_i params["stat"] = "I" params["expected"] = -1.0 / (adata.shape[0] - 1) # expected score params["ascending"] = False elif mode == SpatialAutocorr.GEARY: params["func"] = _gearys_c params["stat"] = "C" params["expected"] = 1.0 params["ascending"] = True else: raise NotImplementedError(f"Mode `{mode}` is not yet implemented.") n_jobs = _get_n_cores(n_jobs) vals = _get_obs_rep(adata[:, genes], use_raw=use_raw, layer=layer).T g = adata.obsp[connectivity_key].copy() # row-normalize if transformation: normalize(g, norm="l1", axis=1, copy=False) score = params["func"](g, vals) start = logg.info( f"Calculating {mode}'s statistic for `{n_perms}` permutations using `{n_jobs}` core(s)" ) if n_perms is not None: _assert_positive(n_perms, name="n_perms") perms = np.arange(n_perms) score_perms = parallelize( _score_helper, collection=perms, extractor=np.concatenate, use_ixs=True, n_jobs=n_jobs, backend=backend, show_progress_bar=show_progress_bar, )(mode=mode, g=g, vals=vals, seed=seed) else: score_perms = None with np.errstate(divide="ignore"): pval_results = _p_value_calc(score, score_perms, g, params) results = {params["stat"]: score} results.update(pval_results) df = pd.DataFrame(results, index=genes) if corr_method is not None: for pv in filter(lambda x: "pval" in x, df.columns): _, pvals_adj, _, _ = multipletests(df[pv].values, alpha=0.05, method=corr_method) df[f"{pv}_{corr_method}"] = pvals_adj df.sort_values(by=params["stat"], ascending=params["ascending"], inplace=True) if copy: logg.info("Finish", time=start) return df _save_data(adata, attr="uns", key=params["mode"] + params["stat"], data=df, time=start)