Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@

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

## MAJOR CHANGES

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

## MINOR CHANGES

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

## BUGFIXES

Expand Down
84 changes: 84 additions & 0 deletions src/metrics/morans_i/config.vsh.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
__merge__: ../../api/comp_metric.yaml

name: morans_i

info:
metrics:
- name: morans_i
label: Moran's I
summary: "Measures the spatial autocorrelation of the inferred pseudotime across the spatial coordinates of the cells/spots."
description: |
Computes Moran's I statistic for the inferred pseudotime, using a k-nearest-neighbour
graph built on the spatial coordinates (`obsm['X_spatial']`) of the cells/spots.

Moran's I quantifies how similar the pseudotime values of spatially neighbouring
cells/spots are. Values close to 1 indicate a spatially smooth trajectory in which
neighbouring cells receive similar pseudotime values, values around 0 indicate a
spatially random assignment, and negative values indicate that neighbouring cells
receive dissimilar pseudotime values.

Note that this metric evaluates the spatial coherence of the prediction only and does
not compare it against the ground-truth pseudotime; a spatially smooth but incorrect
ordering can still score highly.

references:
doi:
- 10.2307/2332142
bibtex: |
@article{Moran_1950,
author = {Moran, P. A. P.},
title = {Notes on Continuous Stochastic Phenomena},
journal = {Biometrika},
volume = {37},
number = {1/2},
pages = {17--23},
year = {1950},
doi = {10.2307/2332142}
}

links:

documentation: https://scanpy.readthedocs.io/en/stable/api/generated/scanpy.metrics.morans_i.html

repository: https://github.com/scverse/scanpy
min: -1
max: 1
maximize: true

arguments:
- name: "--n_neighbors"
type: "integer"
default: 6
description: Number of spatial neighbours used to build the k-NN graph.

resources:
- type: python_script
path: script.py


engines:
- type: docker
image: python:3.11-slim
setup:
- type: apt
packages:
- procps
- git
- type: python
packages:
- anndata~=0.10.9
- scanpy~=1.10.4
- numpy~=2.4.6
- pandas~=3.0.5
- scipy~=1.17.1
- pyyaml~=6.0.3
- requests~=2.34.2
- jsonschema~=4.26.0
github:
- "openproblems-bio/core#subdirectory=packages/python/openproblems"

runners:
- type: executable
- type: nextflow
directives:
label: [midtime,midmem,midcpu]
78 changes: 78 additions & 0 deletions src/metrics/morans_i/script.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import anndata as ad
import numpy as np
import pandas as pd
import scanpy as sc
from scanpy.metrics import morans_i

## VIASH START
# Note: this section is auto-generated by viash at runtime. To edit it, make changes
# in config.vsh.yaml and then run `viash config inject config.vsh.yaml`.
par = {
'input_solution': 'resources_test/task_spatial_trajectory_inference/dlpfc_151673/solution.h5ad',
'input_prediction': 'resources_test/task_spatial_trajectory_inference/dlpfc_151673/prediction.h5ad',
'output': 'output.h5ad',
'n_neighbors': 6,
}
meta = {
'name': 'morans_i'
}
## VIASH END


def calc_morans_i(adata, pt_col, coords_key, n_neighbors=6):
"""Spatial autocorrelation of pseudotime via Moran's I."""
try:
sc.pp.neighbors(adata, use_rep=coords_key, n_neighbors=n_neighbors, key_added="spatial_neighbors")
pt = pd.to_numeric(adata.obs[pt_col], errors="coerce").values

return float(morans_i(adata.obsp["spatial_neighbors_connectivities"], pt))
except Exception as e:
print(f"Moran's I skipped for {pt_col}: {e}")
return np.nan

# read input data
print('Reading input files', flush=True)
input_solution = ad.read_h5ad(par['input_solution'])
input_prediction = ad.read_h5ad(par['input_prediction'])

assert (input_prediction.obs_names == input_solution.obs_names).all(), "obs_names not the same in prediction and solution inputs"

# inferred pseudotime and spatial coordinates
INFERRED_COL = "pseudotime_inferred"
COORDS_KEY = "X_spatial"

# spatial coordinates in the solution, the pseudotime in the prediction
adata = ad.AnnData(
obs=pd.DataFrame(
{INFERRED_COL: input_prediction.obs[INFERRED_COL].values},
index=input_solution.obs_names,
),
obsm={COORDS_KEY: np.asarray(input_solution.obsm[COORDS_KEY])},
)

# generate results
print('Compute metrics', flush=True)
# metric_ids and metric_values can have length > 1
# but should be of equal length

score = calc_morans_i(adata, INFERRED_COL, COORDS_KEY, n_neighbors=par['n_neighbors'])

uns_metric_ids = [ 'morans_i' ]
uns_metric_values = [ score ]

# Write output data to file
print("Write output AnnData to file...", flush=True)

output = ad.AnnData(
obs=pd.DataFrame(index=pd.Index(np.array([], dtype=str))),
var=pd.DataFrame(index=pd.Index(np.array([], dtype=str))),
uns={
'dataset_id': input_solution.uns.get('dataset_id', 'unknown'),
'normalization_id': input_solution.uns.get('normalization_id', 'unknown'),
'method_id': input_prediction.uns.get('method_id', 'unknown'),
'metric_ids': uns_metric_ids,
'metric_values': uns_metric_values,
}
)

output.write_h5ad(par['output'], compression='gzip')
Loading