pertpy 0.11.5__py3-none-any.whl → 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pertpy/__init__.py CHANGED
@@ -2,10 +2,11 @@
2
2
 
3
3
  __author__ = "Lukas Heumos"
4
4
  __email__ = "lukas.heumos@posteo.net"
5
- __version__ = "0.11.5"
5
+ __version__ = "1.0.0"
6
6
 
7
7
  import warnings
8
8
 
9
+ from anndata._core.aligned_df import ImplicitModificationWarning
9
10
  from matplotlib import MatplotlibDeprecationWarning
10
11
  from numba import NumbaDeprecationWarning
11
12
 
@@ -13,6 +14,8 @@ warnings.filterwarnings("ignore", category=NumbaDeprecationWarning)
13
14
  warnings.filterwarnings("ignore", category=MatplotlibDeprecationWarning)
14
15
  warnings.filterwarnings("ignore", category=SyntaxWarning)
15
16
  warnings.filterwarnings("ignore", category=UserWarning, module="scvi._settings")
17
+ warnings.filterwarnings("ignore", message="Environment variable.*redefined by R")
18
+ warnings.filterwarnings("ignore", message="Transforming to str index.", category=ImplicitModificationWarning)
16
19
 
17
20
  import mudata
18
21
 
pertpy/tools/_milo.py CHANGED
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import random
4
4
  import re
5
+ from importlib.util import find_spec
5
6
  from typing import TYPE_CHECKING, Literal
6
7
 
7
8
  import matplotlib.pyplot as plt
@@ -29,18 +30,6 @@ from sklearn.metrics.pairwise import euclidean_distances
29
30
  class Milo:
30
31
  """Python implementation of Milo."""
31
32
 
32
- def __init__(self):
33
- try:
34
- from rpy2.robjects import conversion, numpy2ri, pandas2ri
35
- from rpy2.robjects.packages import STAP, PackageNotInstalledError, importr
36
- except ModuleNotFoundError:
37
- raise ImportError("milo requires rpy2 to be installed.") from None
38
-
39
- try:
40
- importr("edgeR")
41
- except ImportError as e:
42
- raise ImportError("milo requires a valid R installation with edger installed:\n") from e
43
-
44
33
  def load(
45
34
  self,
46
35
  input: AnnData,
@@ -266,7 +255,7 @@ class Milo:
266
255
  subset_samples: list[str] | None = None,
267
256
  add_intercept: bool = True,
268
257
  feature_key: str | None = "rna",
269
- solver: Literal["edger", "batchglm"] = "edger",
258
+ solver: Literal["edger", "pydeseq2"] = "edger",
270
259
  ):
271
260
  """Performs differential abundance testing on neighbourhoods using QLF test implementation as implemented in edgeR.
272
261
 
@@ -279,7 +268,9 @@ class Milo:
279
268
  subset_samples: subset of samples (obs in `milo_mdata['milo']`) to use for the test.
280
269
  add_intercept: whether to include an intercept in the model. If False, this is equivalent to adding + 0 in the design formula. When model_contrasts is specified, this is set to False by default.
281
270
  feature_key: If input data is MuData, specify key to cell-level AnnData object.
282
- solver: The solver to fit the model to. One of "edger" (requires R, rpy2 and edgeR to be installed) or "batchglm"
271
+ solver: The solver to fit the model to.
272
+ The "edger" solver requires R, rpy2 and edgeR to be installed and is the closest to the R implementation.
273
+ The "pydeseq2" requires pydeseq2 to be installed. It is still very comparable to the "edger" solver but might be a bit slower.
283
274
 
284
275
  Returns:
285
276
  None, modifies `milo_mdata['milo']` in place, adding the results of the DA test to `.var`:
@@ -298,7 +289,6 @@ class Milo:
298
289
  >>> milo.make_nhoods(mdata["rna"])
299
290
  >>> mdata = milo.count_nhoods(mdata, sample_col="orig.ident")
300
291
  >>> milo.da_nhoods(mdata, design="~label")
301
-
302
292
  """
303
293
  try:
304
294
  sample_adata = mdata["milo"]
@@ -425,13 +415,65 @@ class Milo:
425
415
  res = pd.DataFrame(res)
426
416
  # The columns of res looks like e.g. table.A, table.B, so remove the prefix
427
417
  res.columns = [col.replace("table.", "") for col in res.columns]
428
- # Save outputs
418
+ elif solver == "pydeseq2":
419
+ if find_spec("pydeseq2") is None:
420
+ raise ImportError("pydeseq2 is required but not installed. Install with: pip install pydeseq2")
421
+
422
+ from pydeseq2.dds import DeseqDataSet
423
+ from pydeseq2.ds import DeseqStats
424
+
425
+ counts_filtered = count_mat[np.ix_(keep_nhoods, keep_smp)]
426
+ design_df_filtered = design_df.iloc[keep_smp].copy()
427
+
428
+ design_df_filtered = design_df_filtered.astype(
429
+ dict.fromkeys(design_df_filtered.select_dtypes(exclude=["number"]).columns, "category")
430
+ )
431
+
432
+ design_clean = design if design.startswith("~") else f"~{design}"
433
+
434
+ dds = DeseqDataSet(
435
+ counts=pd.DataFrame(counts_filtered.T, index=design_df_filtered.index),
436
+ metadata=design_df_filtered,
437
+ design=design_clean,
438
+ refit_cooks=True,
439
+ )
440
+
441
+ dds.deseq2()
442
+
443
+ if model_contrasts is not None and "-" in model_contrasts:
444
+ if "(" in model_contrasts or "+" in model_contrasts.split("-")[1]:
445
+ raise ValueError(
446
+ f"Complex contrasts like '{model_contrasts}' are not supported by pydeseq2. "
447
+ "Use simple pairwise contrasts (e.g., 'GroupA-GroupB') or switch to solver='edger'."
448
+ )
449
+
450
+ parts = model_contrasts.split("-")
451
+ factor_name = design_clean.replace("~", "").split("+")[-1].strip()
452
+ group1 = parts[0].replace(factor_name, "").strip()
453
+ group2 = parts[1].replace(factor_name, "").strip()
454
+ stat_res = DeseqStats(dds, contrast=[factor_name, group1, group2])
455
+ else:
456
+ factor_name = design_clean.replace("~", "").split("+")[-1].strip()
457
+ if not isinstance(design_df_filtered[factor_name], pd.CategoricalDtype):
458
+ design_df_filtered[factor_name] = design_df_filtered[factor_name].astype("category")
459
+ categories = design_df_filtered[factor_name].cat.categories
460
+ stat_res = DeseqStats(dds, contrast=[factor_name, categories[-1], categories[0]])
461
+
462
+ stat_res.summary()
463
+ res = stat_res.results_df
464
+
465
+ res = res.rename(
466
+ columns={"baseMean": "logCPM", "log2FoldChange": "logFC", "pvalue": "PValue", "padj": "FDR"}
467
+ )
468
+
469
+ res = res[["logCPM", "logFC", "PValue", "FDR"]]
470
+
429
471
  res.index = sample_adata.var_names[keep_nhoods] # type: ignore
430
472
  if any(col in sample_adata.var.columns for col in res.columns):
431
473
  sample_adata.var = sample_adata.var.drop(res.columns, axis=1)
432
474
  sample_adata.var = pd.concat([sample_adata.var, res], axis=1)
433
- # Run Graph spatial FDR correction
434
- self._graph_spatial_fdr(sample_adata, neighbors_key=adata.uns["nhood_neighbors_key"])
475
+
476
+ self._graph_spatial_fdr(sample_adata)
435
477
 
436
478
  def annotate_nhoods(
437
479
  self,
@@ -674,6 +716,17 @@ class Milo:
674
716
  self,
675
717
  ):
676
718
  """Set up rpy2 to run edgeR."""
719
+ try:
720
+ from rpy2.robjects import conversion, numpy2ri, pandas2ri
721
+ from rpy2.robjects.packages import STAP, PackageNotInstalledError, importr
722
+ except ModuleNotFoundError:
723
+ raise ImportError("milo requires rpy2 to be installed.") from None
724
+
725
+ try:
726
+ importr("edgeR")
727
+ except ImportError as e:
728
+ raise ImportError("milo requires a valid R installation with edger installed.") from e
729
+
677
730
  from rpy2.robjects.packages import importr
678
731
 
679
732
  edgeR = self._try_import_bioc_library("edgeR")
@@ -685,26 +738,27 @@ class Milo:
685
738
 
686
739
  def _try_import_bioc_library(
687
740
  self,
688
- name: str,
741
+ r_package: str,
689
742
  ):
690
743
  """Import R packages.
691
744
 
692
745
  Args:
693
- name (str): R packages name
746
+ r_package: R packages name
694
747
  """
695
748
  from rpy2.robjects.packages import PackageNotInstalledError, importr
696
749
 
697
750
  try:
698
- _r_lib = importr(name)
751
+ _r_lib = importr(r_package)
699
752
  return _r_lib
700
753
  except PackageNotInstalledError:
701
- logger.error(f"Install Bioconductor library `{name!r}` first as `BiocManager::install({name!r}).`")
754
+ logger.error(
755
+ f"Install Bioconductor library `{r_package!r}` first as `BiocManager::install({r_package!r}).`"
756
+ )
702
757
  raise
703
758
 
704
759
  def _graph_spatial_fdr(
705
760
  self,
706
761
  sample_adata: AnnData,
707
- neighbors_key: str | None = None,
708
762
  ):
709
763
  """FDR correction weighted on inverse of connectivity of neighbourhoods.
710
764
 
@@ -712,7 +766,6 @@ class Milo:
712
766
 
713
767
  Args:
714
768
  sample_adata: Sample-level AnnData.
715
- neighbors_key: The key in `adata.obsp` to use as KNN graph.
716
769
  """
717
770
  # use 1/connectivity as the weighting for the weighted BH adjustment from Cydar
718
771
  w = 1 / sample_adata.var["kth_distance"]
@@ -80,7 +80,7 @@ class PerturbationSpace:
80
80
  group_masks = (
81
81
  [(adata.obs[group_col] == sample) for sample in adata.obs[group_col].unique()]
82
82
  if group_col
83
- else [[True] * adata.n_obs]
83
+ else [np.array([True] * adata.n_obs)]
84
84
  )
85
85
 
86
86
  if layer_key:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pertpy
3
- Version: 0.11.5
3
+ Version: 1.0.0
4
4
  Summary: Perturbation Analysis in the scverse ecosystem.
5
5
  Project-URL: Documentation, https://pertpy.readthedocs.io
6
6
  Project-URL: Source, https://github.com/scverse/pertpy
@@ -155,7 +155,13 @@ pip install 'pertpy[tcoda]'
155
155
 
156
156
  ### milo
157
157
 
158
- milo further requires edger, statmod, and rpy2 to be installed:
158
+ milo requires either the "de" extra for the "pydeseq2" solver:
159
+
160
+ ```console
161
+ pip install 'pertpy[de]'
162
+ ```
163
+
164
+ or, edger, statmod, and rpy2 for the "edger" solver:
159
165
 
160
166
  ```R
161
167
  BiocManager::install("edgeR")
@@ -1,4 +1,4 @@
1
- pertpy/__init__.py,sha256=KIxMlqyHlppcGM5Uc2HpTwCEtGFavXRPW50dM5dFB7U,716
1
+ pertpy/__init__.py,sha256=cZHJ7PIOhtLkxJMlHbJ2rzei5xhLB4vg0c8AaIShfzc,972
2
2
  pertpy/_doc.py,sha256=j5TMNC-DA9yIMqIIUNpjpcVgWfRqyBBfvbRjnCM_OLs,427
3
3
  pertpy/_types.py,sha256=IcHCojCUqx8CapibNkcYf2TUqjBFP2ujeELvn_IBSBQ,154
4
4
  pertpy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -21,7 +21,7 @@ pertpy/tools/_augur.py,sha256=tc1YKyc0BwzrEGgctsfyy7DsTNKxyvy7ZvWraTWCc1A,55262
21
21
  pertpy/tools/_cinemaot.py,sha256=54-rS0AEj31dMe7iU4kEmLoAunq3jNuhsBE3IEp9hrI,38071
22
22
  pertpy/tools/_dialogue.py,sha256=mygIZm5i_bnEE37TTQtr1efl_KJq-ejzeL3V1Bmr7Pg,52354
23
23
  pertpy/tools/_enrichment.py,sha256=55mwotLH9DXQOhl85MCkxXu-MX0RysLyrPheJysAnF0,21369
24
- pertpy/tools/_milo.py,sha256=zIYG0aP8B39_eiNgpZONhTKmDvcRwCzOLo5FMOTMUms,45530
24
+ pertpy/tools/_milo.py,sha256=9yoB9gkBNujqYDTKOlH2v3wiWhs5PdCuB8RgZ3xVI0Y,48049
25
25
  pertpy/tools/_mixscape.py,sha256=HfrpBeRlxHXaOpZkF2FmX7dg35kUB1rL0_-n2aSi2_0,57905
26
26
  pertpy/tools/decoupler_LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
27
27
  pertpy/tools/transferlearning_MMD_LICENSE,sha256=MUvDA-o_j9htRpI8fStVdCRuyLdPkQUuIH0a_EIc57w,1069
@@ -45,14 +45,14 @@ pertpy/tools/_perturbation_space/_clustering.py,sha256=pNx_SpPkZfCbgF7vzHWqAaiiH
45
45
  pertpy/tools/_perturbation_space/_comparison.py,sha256=-NzCPRT-IlhJ9hOz7NQLSk0riIzr2C0yZvX6zm3kon4,4291
46
46
  pertpy/tools/_perturbation_space/_discriminator_classifiers.py,sha256=a53-YmUwDHQBCT7ZWe_RH7PZsGXvoSHmJaQyL0CBJng,23383
47
47
  pertpy/tools/_perturbation_space/_metrics.py,sha256=y8-baP8WRdB1iDgvP3uuQxSCDxA2lcxvEHHM2C_vWHY,3248
48
- pertpy/tools/_perturbation_space/_perturbation_space.py,sha256=8RxVUkVEPZj5YZ-C-NP5zO4aYYVD04PzlsYuaIG-wjY,19447
48
+ pertpy/tools/_perturbation_space/_perturbation_space.py,sha256=Vyh15wWw9dcu2YUWhziQd2mA9-4IY8EC5dzkBT9HaIo,19457
49
49
  pertpy/tools/_perturbation_space/_simple.py,sha256=AJlHRaEP-vViBeMDvvMtUnXMuIKqZVc7wggnjsHMfMw,12721
50
50
  pertpy/tools/_scgen/__init__.py,sha256=uERFlFyF88TH0uLiwmsUGEfHfLVCiZMFuk8gO5f7164,45
51
51
  pertpy/tools/_scgen/_base_components.py,sha256=Qq8myRUm43q9XBrZ9gBggfa2cSV2wbz_KYoLgH7iF1A,3009
52
52
  pertpy/tools/_scgen/_scgen.py,sha256=AQNGsDe-9HEqli3oq7UBDg68ofLCoXm-R_jnLFQ-rlc,30856
53
53
  pertpy/tools/_scgen/_scgenvae.py,sha256=bPk4v7EdJc7ROdLuDitHiX_Pvwa7Flw2qHRUwBvjLJY,3889
54
54
  pertpy/tools/_scgen/_utils.py,sha256=qz5QUn_Bvk2NGyYVzp3jgjWTFOMt1YyHwUo6HWtoThY,2871
55
- pertpy-0.11.5.dist-info/METADATA,sha256=YEYgYTHkjmyWyboRL3RhBaSxOw86O5vr0wpXdvaLTGk,8827
56
- pertpy-0.11.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
57
- pertpy-0.11.5.dist-info/licenses/LICENSE,sha256=XuiT2hxeRInhquEIBKMZ5M21n5syhDQ4XbABoposIAg,1100
58
- pertpy-0.11.5.dist-info/RECORD,,
55
+ pertpy-1.0.0.dist-info/METADATA,sha256=PnK9O-MyIPzSy5DNOqMN7G6zcxZ2ZTJnMFB5cEr5XJQ,8920
56
+ pertpy-1.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
57
+ pertpy-1.0.0.dist-info/licenses/LICENSE,sha256=XuiT2hxeRInhquEIBKMZ5M21n5syhDQ4XbABoposIAg,1100
58
+ pertpy-1.0.0.dist-info/RECORD,,