Skip to content

Commit 5399717

Browse files
committed
Fix: improve run_pca and fix Python 3.9 compatibility
- Extract _slice_pca to module level for testability - Fix run_pca to properly preserve existing X_pca when using custom pca_key - Capture results before modifying obsm to avoid accessing deleted keys - Add future annotations import for Python 3.9 compatibility in tests - Update release notes with PR #176 optimizations
1 parent 11f3753 commit 5399717

3 files changed

Lines changed: 47 additions & 21 deletions

File tree

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,14 @@ Release Notes
7676
### Next Release
7777
* Avoid warning about coloring in `plot_trajectories`
7878
* Fix: forward `eigvec_key` to fallback method in `early_cell()`
79+
* Performance optimizations (PR #176):
80+
- Faster max-min waypoint sampling: O(N·K²) → O(N·K)
81+
- PCA slicing optimization to avoid redundant recomputation
82+
- Sparse LU solver for absorption probabilities
83+
- Vectorized perspective matrix and Dijkstra shortest paths
84+
- Vectorized Markov chain construction and terminal state detection
85+
- Optional sklearn backend for diffusion kernel (`kernel_backend` parameter)
86+
* Fix: `run_pca` now correctly preserves existing `X_pca` when using custom `pca_key`
7987

8088
### Version 1.4.2
8189
* Fix "lightgray" error in `plot_trend`

src/palantir/utils.py

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,29 @@ class CellNotFoundException(Exception):
2626
pass
2727

2828

29+
def _slice_pca(ad: AnnData, n_comps: int) -> None:
30+
"""Slice PCA results to fewer components without recomputing.
31+
32+
Parameters
33+
----------
34+
ad : AnnData
35+
AnnData object with PCA results in obsm["X_pca"], uns["pca"], and varm["PCs"].
36+
n_comps : int
37+
Number of components to keep.
38+
"""
39+
if "X_pca" in ad.obsm:
40+
ad.obsm["X_pca"] = ad.obsm["X_pca"][:, :n_comps]
41+
if "pca" in ad.uns:
42+
for key in ("variance", "variance_ratio"):
43+
if key in ad.uns["pca"]:
44+
ad.uns["pca"][key] = np.asarray(ad.uns["pca"][key])[:n_comps]
45+
params = ad.uns["pca"].get("params")
46+
if isinstance(params, dict) and "n_comps" in params:
47+
params["n_comps"] = n_comps
48+
if "PCs" in ad.varm:
49+
ad.varm["PCs"] = ad.varm["PCs"][:, :n_comps]
50+
51+
2952
def run_pca(
3053
data: Union[pd.DataFrame, AnnData],
3154
n_components: int = 300,
@@ -53,25 +76,14 @@ def run_pca(
5376
Tuple of PCA projections of the data and the explained variance.
5477
If AnnData is passed as data, the results are also written to the input object and None is returned.
5578
"""
56-
def _slice_pca(ad: AnnData, n_comps: int) -> None:
57-
if "X_pca" in ad.obsm:
58-
ad.obsm["X_pca"] = ad.obsm["X_pca"][:, :n_comps]
59-
if "pca" in ad.uns:
60-
for key in ("variance", "variance_ratio"):
61-
if key in ad.uns["pca"]:
62-
ad.uns["pca"][key] = np.asarray(ad.uns["pca"][key])[:n_comps]
63-
params = ad.uns["pca"].get("params")
64-
if isinstance(params, dict) and "n_comps" in params:
65-
params["n_comps"] = n_comps
66-
if "PCs" in ad.varm:
67-
ad.varm["PCs"] = ad.varm["PCs"][:, :n_comps]
68-
6979
if isinstance(data, pd.DataFrame):
7080
ad = AnnData(data.values)
81+
old_pca = None
7182
else:
7283
ad = data
73-
if pca_key != "X_pca":
74-
old_pca = ad.obsm.get("X_pca", None)
84+
# Preserve existing X_pca if user wants results in a different key
85+
if pca_key != "X_pca" and "X_pca" in ad.obsm:
86+
old_pca = ad.obsm["X_pca"].copy()
7587
else:
7688
old_pca = None
7789

@@ -91,15 +103,19 @@ def _slice_pca(ad: AnnData, n_comps: int) -> None:
91103
elif n_comps > l_n_comps:
92104
sc.pp.pca(ad, n_comps=n_comps, mask_var="highly_variable", zero_center=False)
93105

94-
if isinstance(data, AnnData):
106+
# Capture results before potentially modifying obsm
107+
pca_projections = pd.DataFrame(ad.obsm["X_pca"], index=ad.obs_names)
108+
variance_ratio = ad.uns["pca"]["variance_ratio"]
109+
110+
if isinstance(data, AnnData) and pca_key != "X_pca":
95111
data.obsm[pca_key] = ad.obsm["X_pca"]
96-
if pca_key != "X_pca":
97-
del data.obsm["X_pca"]
98-
elif old_pca is not None:
112+
# Restore original X_pca if it existed, otherwise remove the temporary one
113+
if old_pca is not None:
99114
data.obsm["X_pca"] = old_pca
115+
else:
116+
del data.obsm["X_pca"]
100117

101-
pca_projections = pd.DataFrame(ad.obsm[pca_key], index=ad.obs_names)
102-
return pca_projections, ad.uns["pca"]["variance_ratio"]
118+
return pca_projections, variance_ratio
103119

104120

105121
def run_low_density_variability(

tests/test_core_equivalence.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
import numpy as np
24
import pandas as pd
35
from scipy.sparse import csr_matrix, csgraph, find

0 commit comments

Comments
 (0)