Skip to content

Commit 7de1daf

Browse files
authored
Metric: Moran's I (#6)
* added moran's I metric * morans I config and changelog * added package versions * changelog
1 parent 4bea293 commit 7de1daf

3 files changed

Lines changed: 164 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@
88

99
* Added `spaTrack` method (PR #4).
1010
* Added `Spearman's correlation` metric (PR #5).
11+
* Added `Moran's I` metric (PR #6).
1112

1213
## MAJOR CHANGES
1314

1415
* Updated `api` files and set the data processor (PR #1).
1516

1617
## MINOR CHANGES
1718

19+
* Added package versions to the moran's I config (PR #6).
1820

1921
## BUGFIXES
2022

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