Skip to content

Commit 6022ba0

Browse files
committed
added moran's I metric
1 parent f674afe commit 6022ba0

2 files changed

Lines changed: 175 additions & 0 deletions

File tree

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
__merge__: ../../api/comp_metric.yaml
2+
3+
name: morans_i
4+
5+
6+
7+
# Metadata for your component
8+
info:
9+
metrics:
10+
- name: morans_i
11+
label: Moran's I
12+
summary: "Measures the spatial autocorrelation of the inferred pseudotime across the spatial coordinates of the cells/spots."
13+
description: |
14+
Computes Moran's I statistic for the inferred pseudotime, using a k-nearest-neighbour
15+
graph built on the spatial coordinates (`obsm['X_spatial']`) of the cells/spots.
16+
17+
Moran's I quantifies how similar the pseudotime values of spatially neighbouring
18+
cells/spots are. Values close to 1 indicate a spatially smooth trajectory in which
19+
neighbouring cells receive similar pseudotime values, values around 0 indicate a
20+
spatially random assignment, and negative values indicate that neighbouring cells
21+
receive dissimilar pseudotime values.
22+
23+
Note that this metric evaluates the spatial coherence of the prediction only and does
24+
not compare it against the ground-truth pseudotime; a spatially smooth but incorrect
25+
ordering can still score highly.
26+
27+
references:
28+
doi:
29+
- 10.2307/2332142
30+
bibtex: |
31+
@article{Moran_1950,
32+
author = {Moran, P. A. P.},
33+
title = {Notes on Continuous Stochastic Phenomena},
34+
journal = {Biometrika},
35+
volume = {37},
36+
number = {1/2},
37+
pages = {17--23},
38+
year = {1950},
39+
doi = {10.2307/2332142}
40+
}
41+
42+
links:
43+
44+
documentation: https://scanpy.readthedocs.io/en/stable/api/generated/scanpy.metrics.morans_i.html
45+
46+
repository: https://github.com/scverse/scanpy
47+
# The minimum possible value for this metric (required)
48+
min: -1
49+
# The maximum possible value for this metric (required)
50+
max: 1
51+
# Whether a higher value represents a 'better' solution (required)
52+
maximize: true
53+
54+
# Component-specific parameters (optional)
55+
arguments:
56+
- name: "--n_neighbors"
57+
type: "integer"
58+
default: 6
59+
description: Number of spatial neighbours used to build the k-NN graph.
60+
61+
# Resources required to run the component
62+
resources:
63+
# The script of your component (required)
64+
- type: python_script
65+
path: script.py
66+
# Additional resources your script needs (optional)
67+
# - type: file
68+
# path: weights.pt
69+
70+
engines:
71+
# Specifications for the Docker image for this component.
72+
- type: docker
73+
image: python:3.11-slim
74+
setup:
75+
- type: apt
76+
packages:
77+
- procps
78+
- git
79+
- type: python
80+
packages:
81+
- anndata~=0.10.0
82+
- scanpy~=1.10.0
83+
- scipy
84+
- pandas
85+
- pyyaml
86+
- requests
87+
- jsonschema
88+
github:
89+
- "openproblems-bio/core#subdirectory=packages/python/openproblems"
90+
91+
runners:
92+
# This platform allows running the component natively
93+
- type: executable
94+
# Allows turning the component into a Nextflow module / pipeline.
95+
- type: nextflow
96+
directives:
97+
label: [midtime,midmem,midcpu]

src/metrics/morans_i/script.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import anndata as ad
2+
import numpy as np
3+
import pandas as pd
4+
import scanpy as sc
5+
from scanpy.metrics import morans_i
6+
7+
## VIASH START
8+
# Note: this section is auto-generated by viash at runtime. To edit it, make changes
9+
# in config.vsh.yaml and then run `viash config inject config.vsh.yaml`.
10+
par = {
11+
'input_solution': 'resources_test/task_spatial_trajectory_inference/dlpfc_151673/solution.h5ad',
12+
'input_prediction': 'resources_test/task_spatial_trajectory_inference/dlpfc_151673/prediction.h5ad',
13+
'output': 'output.h5ad',
14+
'n_neighbors': 6,
15+
}
16+
meta = {
17+
'name': 'morans_i'
18+
}
19+
## VIASH END
20+
21+
22+
def calc_morans_i(adata, pt_col, coords_key, n_neighbors=6):
23+
"""Spatial autocorrelation of pseudotime via Moran's I."""
24+
try:
25+
sc.pp.neighbors(adata, use_rep=coords_key, n_neighbors=n_neighbors, key_added="spatial_neighbors")
26+
pt = pd.to_numeric(adata.obs[pt_col], errors="coerce").values
27+
28+
return float(morans_i(adata.obsp["spatial_neighbors_connectivities"], pt))
29+
except Exception as e:
30+
print(f"Moran's I skipped for {pt_col}: {e}")
31+
return np.nan
32+
33+
# read input data
34+
print('Reading input files', flush=True)
35+
input_solution = ad.read_h5ad(par['input_solution'])
36+
input_prediction = ad.read_h5ad(par['input_prediction'])
37+
38+
assert (input_prediction.obs_names == input_solution.obs_names).all(), "obs_names not the same in prediction and solution inputs"
39+
40+
# inferred pseudotime and spatial coordinates
41+
INFERRED_COL = "pseudotime_inferred"
42+
COORDS_KEY = "X_spatial"
43+
44+
# spatial coordinates in the solution, the pseudotime in the prediction
45+
adata = ad.AnnData(
46+
obs=pd.DataFrame(
47+
{INFERRED_COL: input_prediction.obs[INFERRED_COL].values},
48+
index=input_solution.obs_names,
49+
),
50+
obsm={COORDS_KEY: np.asarray(input_solution.obsm[COORDS_KEY])},
51+
)
52+
53+
# generate results
54+
print('Compute metrics', flush=True)
55+
# metric_ids and metric_values can have length > 1
56+
# but should be of equal length
57+
58+
score = calc_morans_i(adata, INFERRED_COL, COORDS_KEY, n_neighbors=par['n_neighbors'])
59+
60+
uns_metric_ids = [ 'morans_i' ]
61+
uns_metric_values = [ score ]
62+
63+
# Write output data to file
64+
print("Write output AnnData to file...", flush=True)
65+
66+
output = ad.AnnData(
67+
obs=pd.DataFrame(index=pd.Index(np.array([], dtype=str))),
68+
var=pd.DataFrame(index=pd.Index(np.array([], dtype=str))),
69+
uns={
70+
'dataset_id': input_solution.uns.get('dataset_id', 'unknown'),
71+
'normalization_id': input_solution.uns.get('normalization_id', 'unknown'),
72+
'method_id': input_prediction.uns.get('method_id', 'unknown'),
73+
'metric_ids': uns_metric_ids,
74+
'metric_values': uns_metric_values,
75+
}
76+
)
77+
78+
output.write_h5ad(par['output'], compression='gzip')

0 commit comments

Comments
 (0)