Skip to content

Commit 870a9ba

Browse files
authored
Merge pull request #120 from settylab/main
Refactor run_diffusion_maps
2 parents 8fba608 + ba4c10b commit 870a9ba

2 files changed

Lines changed: 118 additions & 61 deletions

File tree

README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,12 +107,11 @@ ____
107107

108108
Release Notes
109109
-------------
110-
### Version 1.3.2
110+
### Version 1.3.1
111111
* implemented `palantir.plot.plot_stats` to plot arbitray cell-wise statistics as x-/y-positions.
112112
* reduce memory usgae of `palantir.presults.compute_gene_trends`
113-
114-
### Version 1.3.1
115113
* removed seaborn dependency
114+
* refactor `run_diffusion_maps` to split out `compute_kernel` and `diffusion_maps_from_kernel`
116115

117116
### Version 1.3.0
118117

src/palantir/utils.py

Lines changed: 116 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,113 @@ def run_density_evaluation(
293293
return log_density
294294

295295

296+
def compute_kernel(
297+
data: Union[pd.DataFrame, sc.AnnData],
298+
knn: int = 30,
299+
alpha: float = 0,
300+
pca_key: str = "X_pca",
301+
kernel_key: str = "DM_Kernel",
302+
) -> csr_matrix:
303+
"""
304+
Compute the adaptive anisotropic diffusion kernel.
305+
306+
Parameters
307+
----------
308+
data : Union[pd.DataFrame, sc.AnnData]
309+
Data points (rows) in a feature space (columns) for pd.DataFrame.
310+
For sc.AnnData, it uses the .X attribute.
311+
knn : int
312+
Number of nearest neighbors for adaptive kernel calculation. Default is 30.
313+
alpha : float
314+
Normalization parameter for the diffusion operator. Default is 0.
315+
pca_key : str, optional
316+
Key to retrieve PCA projections from data if it is a sc.AnnData object. Default is 'X_pca'.
317+
kernel_key : str, optional
318+
Key to store the kernel in obsp of data if it is a sc.AnnData object. Default is 'DM_Kernel'.
319+
320+
Returns
321+
-------
322+
csr_matrix
323+
Computed kernel matrix.
324+
"""
325+
326+
# If the input is sc.AnnData, convert it to a DataFrame
327+
if isinstance(data, sc.AnnData):
328+
data_df = pd.DataFrame(data.obsm[pca_key], index=data.obs_names)
329+
else:
330+
data_df = data
331+
332+
N = data_df.shape[0]
333+
temp = sc.AnnData(data_df.values)
334+
sc.pp.neighbors(temp, n_pcs=0, n_neighbors=knn)
335+
kNN = temp.obsp["distances"]
336+
337+
adaptive_k = int(np.floor(knn / 3))
338+
adaptive_std = np.zeros(N)
339+
for i in np.arange(N):
340+
adaptive_std[i] = np.sort(kNN.data[kNN.indptr[i] : kNN.indptr[i + 1]])[
341+
adaptive_k - 1
342+
]
343+
344+
x, y, dists = find(kNN)
345+
dists /= adaptive_std[x]
346+
W = csr_matrix((np.exp(-dists), (x, y)), shape=[N, N])
347+
348+
kernel = W + W.T
349+
350+
if alpha > 0:
351+
D = np.ravel(kernel.sum(axis=1))
352+
D[D != 0] = D[D != 0] ** (-alpha)
353+
mat = csr_matrix((D, (range(N), range(N))), shape=[N, N])
354+
kernel = mat.dot(kernel).dot(mat)
355+
356+
if isinstance(data, sc.AnnData):
357+
data.obsp[kernel_key] = kernel
358+
359+
return kernel
360+
361+
362+
def diffusion_maps_from_kernel(
363+
kernel: csr_matrix, n_components: int = 10, seed: Union[int, None] = 0
364+
):
365+
"""
366+
Compute the diffusion map given a kernel matrix.
367+
368+
Parameters
369+
----------
370+
kernel : csr_matrix
371+
Precomputed kernel matrix.
372+
n_components : int
373+
Number of diffusion components to compute. Default is 10.
374+
seed : Union[int, None]
375+
Seed for random initialization. Default is 0.
376+
377+
Returns
378+
-------
379+
dict
380+
T-matrix (T), Diffusion components (EigenVectors) and corresponding eigenvalues (EigenValues).
381+
"""
382+
N = kernel.shape[0]
383+
D = np.ravel(kernel.sum(axis=1))
384+
D[D != 0] = 1 / D[D != 0]
385+
T = csr_matrix((D, (range(N), range(N))), shape=[N, N]).dot(kernel)
386+
387+
np.random.seed(seed)
388+
v0 = np.random.rand(min(T.shape))
389+
D, V = eigs(T, n_components, tol=1e-4, maxiter=1000, v0=v0)
390+
391+
D = np.real(D)
392+
V = np.real(V)
393+
inds = np.argsort(D)[::-1]
394+
D = D[inds]
395+
V = V[:, inds]
396+
397+
for i in range(V.shape[1]):
398+
V[:, i] = V[:, i] / np.linalg.norm(V[:, i])
399+
400+
return {"T": T, "EigenVectors": pd.DataFrame(V), "EigenValues": pd.Series(D)}
401+
402+
296403
def run_diffusion_maps(
297404
data: Union[pd.DataFrame, sc.AnnData],
298405
n_components: int = 10,
@@ -347,70 +454,21 @@ def run_diffusion_maps(
347454
else:
348455
data_df = data
349456

350-
if not isinstance(data_df, pd.DataFrame):
351-
raise ValueError("'data_df' should be a pd.DataFrame or a sc.AnnData instance")
457+
if not isinstance(data_df, pd.DataFrame) and not issparse(data_df):
458+
raise ValueError("'data_df' should be a pd.DataFrame or sc.AnnData")
352459

353-
# Determine the kernel
354-
N = data_df.shape[0]
355460
if not issparse(data_df):
356-
print("Determing nearest neighbor graph...")
357-
temp = sc.AnnData(data_df.values)
358-
sc.pp.neighbors(temp, n_pcs=0, n_neighbors=knn)
359-
kNN = temp.obsp["distances"]
360-
361-
# Adaptive k
362-
adaptive_k = int(np.floor(knn / 3))
363-
adaptive_std = np.zeros(N)
364-
365-
for i in np.arange(len(adaptive_std)):
366-
adaptive_std[i] = np.sort(kNN.data[kNN.indptr[i] : kNN.indptr[i + 1]])[
367-
adaptive_k - 1
368-
]
369-
370-
# Kernel
371-
x, y, dists = find(kNN)
372-
373-
# X, y specific stds
374-
dists = dists / adaptive_std[x]
375-
W = csr_matrix((np.exp(-dists), (x, y)), shape=[N, N])
376-
377-
# Diffusion components
378-
kernel = W + W.T
461+
kernel = compute_kernel(data_df, knn, alpha)
379462
else:
463+
warn(
464+
"'data' is a sparse matrix and will be interpreted as kernel. "
465+
"To avoid this warning compute diffusion maps from a precompued kernel using "
466+
"palantir.utils.diffusion_maps_from_kernel()."
467+
)
380468
kernel = data_df
381469

382-
# Markov
383-
D = np.ravel(kernel.sum(axis=1))
384-
385-
if alpha > 0:
386-
# L_alpha
387-
D[D != 0] = D[D != 0] ** (-alpha)
388-
mat = csr_matrix((D, (range(N), range(N))), shape=[N, N])
389-
kernel = mat.dot(kernel).dot(mat)
390-
D = np.ravel(kernel.sum(axis=1))
391-
392-
D[D != 0] = 1 / D[D != 0]
393-
T = csr_matrix((D, (range(N), range(N))), shape=[N, N]).dot(kernel)
394-
# Eigen value dcomposition
395-
np.random.seed(seed)
396-
v0 = np.random.rand(min(T.shape))
397-
D, V = eigs(T, n_components, tol=1e-4, maxiter=1000, v0=v0)
398-
D = np.real(D)
399-
V = np.real(V)
400-
inds = np.argsort(D)[::-1]
401-
D = D[inds]
402-
V = V[:, inds]
403-
404-
# Normalize
405-
for i in range(V.shape[1]):
406-
V[:, i] = V[:, i] / np.linalg.norm(V[:, i])
470+
res = diffusion_maps_from_kernel(kernel, n_components, seed)
407471

408-
# Create are results dictionary
409-
res = {"T": T, "EigenVectors": V, "EigenValues": D}
410-
res["EigenVectors"] = pd.DataFrame(res["EigenVectors"])
411-
if not issparse(data_df):
412-
res["EigenVectors"].index = data_df.index
413-
res["EigenValues"] = pd.Series(res["EigenValues"])
414472
res["kernel"] = kernel
415473

416474
if isinstance(data, sc.AnnData):

0 commit comments

Comments
 (0)