pertpy 1.0.0__tar.gz → 1.0.1__tar.gz

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.
Files changed (198) hide show
  1. {pertpy-1.0.0 → pertpy-1.0.1}/.github/release-drafter.yml +2 -2
  2. pertpy-1.0.1/.github/workflows/test_tutorials.yml +94 -0
  3. {pertpy-1.0.0 → pertpy-1.0.1}/.pre-commit-config.yaml +4 -4
  4. {pertpy-1.0.0 → pertpy-1.0.1}/PKG-INFO +3 -3
  5. pertpy-1.0.1/biome.jsonc +34 -0
  6. {pertpy-1.0.0 → pertpy-1.0.1}/docs/api/tools_index.md +0 -2
  7. {pertpy-1.0.0 → pertpy-1.0.1}/docs/changelog.md +12 -0
  8. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/__init__.py +1 -1
  9. pertpy-1.0.1/pertpy/data/_dataloader.py +117 -0
  10. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/tools/__init__.py +18 -27
  11. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/tools/_coda/_base_coda.py +10 -4
  12. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/tools/_coda/_sccoda.py +42 -0
  13. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/tools/_dialogue.py +3 -3
  14. pertpy-1.0.1/pertpy/tools/_differential_gene_expression/__init__.py +60 -0
  15. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/tools/_differential_gene_expression/_base.py +2 -1
  16. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/tools/_differential_gene_expression/_edger.py +9 -12
  17. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/tools/_differential_gene_expression/_pydeseq2.py +0 -2
  18. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/tools/_distances/_distance_tests.py +2 -2
  19. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/tools/_distances/_distances.py +33 -8
  20. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/tools/_milo.py +3 -1
  21. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/tools/_perturbation_space/_discriminator_classifiers.py +16 -25
  22. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/tools/_perturbation_space/_simple.py +8 -0
  23. {pertpy-1.0.0 → pertpy-1.0.1}/pyproject.toml +6 -4
  24. {pertpy-1.0.0 → pertpy-1.0.1}/tests/tools/_differential_gene_expression/conftest.py +14 -1
  25. {pertpy-1.0.0 → pertpy-1.0.1}/tests/tools/_differential_gene_expression/test_base.py +5 -0
  26. {pertpy-1.0.0 → pertpy-1.0.1}/tests/tools/_differential_gene_expression/test_compare_groups.py +6 -0
  27. {pertpy-1.0.0 → pertpy-1.0.1}/tests/tools/_differential_gene_expression/test_edger.py +8 -1
  28. {pertpy-1.0.0 → pertpy-1.0.1}/tests/tools/_differential_gene_expression/test_input_checks.py +8 -1
  29. {pertpy-1.0.0 → pertpy-1.0.1}/tests/tools/_differential_gene_expression/test_pydeseq2.py +9 -1
  30. {pertpy-1.0.0 → pertpy-1.0.1}/tests/tools/_differential_gene_expression/test_simple_tests.py +6 -0
  31. pertpy-1.0.1/tests/tools/_differential_gene_expression/test_statsmodels.py +24 -0
  32. {pertpy-1.0.0 → pertpy-1.0.1}/tests/tools/_distances/test_distances.py +2 -2
  33. {pertpy-1.0.0 → pertpy-1.0.1}/tests/tools/test_milo.py +7 -7
  34. pertpy-1.0.0/biome.jsonc +0 -18
  35. pertpy-1.0.0/pertpy/data/_dataloader.py +0 -114
  36. pertpy-1.0.0/pertpy/tools/_differential_gene_expression/__init__.py +0 -19
  37. pertpy-1.0.0/tests/tools/_differential_gene_expression/test_statsmodels.py +0 -30
  38. {pertpy-1.0.0 → pertpy-1.0.1}/.editorconfig +0 -0
  39. {pertpy-1.0.0 → pertpy-1.0.1}/.gitattributes +0 -0
  40. {pertpy-1.0.0 → pertpy-1.0.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  41. {pertpy-1.0.0 → pertpy-1.0.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  42. {pertpy-1.0.0 → pertpy-1.0.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  43. {pertpy-1.0.0 → pertpy-1.0.1}/.github/labels.yml +0 -0
  44. {pertpy-1.0.0 → pertpy-1.0.1}/.github/pull_request_template.md +0 -0
  45. {pertpy-1.0.0 → pertpy-1.0.1}/.github/workflows/build.yml +0 -0
  46. {pertpy-1.0.0 → pertpy-1.0.1}/.github/workflows/labeler.yml +0 -0
  47. {pertpy-1.0.0 → pertpy-1.0.1}/.github/workflows/release.yml +0 -0
  48. {pertpy-1.0.0 → pertpy-1.0.1}/.github/workflows/release_drafter.yml +0 -0
  49. {pertpy-1.0.0 → pertpy-1.0.1}/.github/workflows/test.yml +0 -0
  50. {pertpy-1.0.0 → pertpy-1.0.1}/.gitignore +0 -0
  51. {pertpy-1.0.0 → pertpy-1.0.1}/.gitmodules +0 -0
  52. {pertpy-1.0.0 → pertpy-1.0.1}/.readthedocs.yml +0 -0
  53. {pertpy-1.0.0 → pertpy-1.0.1}/LICENSE +0 -0
  54. {pertpy-1.0.0 → pertpy-1.0.1}/README.md +0 -0
  55. {pertpy-1.0.0 → pertpy-1.0.1}/codecov.yml +0 -0
  56. {pertpy-1.0.0 → pertpy-1.0.1}/docs/Makefile +0 -0
  57. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_ext/edit_on_github.py +0 -0
  58. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_ext/typed_returns.py +0 -0
  59. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/SCVI_LICENSE +0 -0
  60. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/css/overwrite.css +0 -0
  61. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/css/sphinx_gallery.css +0 -0
  62. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/docstring_previews/augur_dp_scatter.png +0 -0
  63. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/docstring_previews/augur_important_features.png +0 -0
  64. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/docstring_previews/augur_lollipop.png +0 -0
  65. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/docstring_previews/augur_scatterplot.png +0 -0
  66. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/docstring_previews/de_fold_change.png +0 -0
  67. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/docstring_previews/de_multicomparison_fc.png +0 -0
  68. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/docstring_previews/de_paired_expression.png +0 -0
  69. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/docstring_previews/de_volcano.png +0 -0
  70. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/docstring_previews/dialogue_pairplot.png +0 -0
  71. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/docstring_previews/dialogue_violin.png +0 -0
  72. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/docstring_previews/enrichment_dotplot.png +0 -0
  73. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/docstring_previews/enrichment_gsea.png +0 -0
  74. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/docstring_previews/milo_da_beeswarm.png +0 -0
  75. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/docstring_previews/milo_nhood.png +0 -0
  76. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/docstring_previews/milo_nhood_graph.png +0 -0
  77. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/docstring_previews/mixscape_barplot.png +0 -0
  78. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/docstring_previews/mixscape_heatmap.png +0 -0
  79. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/docstring_previews/mixscape_lda.png +0 -0
  80. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/docstring_previews/mixscape_perturbscore.png +0 -0
  81. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/docstring_previews/mixscape_violin.png +0 -0
  82. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/docstring_previews/pseudobulk_samples.png +0 -0
  83. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/docstring_previews/sccoda_boxplots.png +0 -0
  84. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/docstring_previews/sccoda_effects_barplot.png +0 -0
  85. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/docstring_previews/sccoda_rel_abundance_dispersion_plot.png +0 -0
  86. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/docstring_previews/sccoda_stacked_barplot.png +0 -0
  87. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/docstring_previews/scgen_reg_mean.png +0 -0
  88. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/docstring_previews/tasccoda_draw_effects.png +0 -0
  89. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/docstring_previews/tasccoda_draw_tree.png +0 -0
  90. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/docstring_previews/tasccoda_effects_umap.png +0 -0
  91. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/icons/code-24px.svg +0 -0
  92. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/icons/computer-24px.svg +0 -0
  93. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/icons/library_books-24px.svg +0 -0
  94. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/icons/play_circle_outline-24px.svg +0 -0
  95. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/pertpy_logo.png +0 -0
  96. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/pertpy_logo.svg +0 -0
  97. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/placeholder.png +0 -0
  98. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/tutorials/augur.png +0 -0
  99. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/tutorials/cinemaot.png +0 -0
  100. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/tutorials/dge.png +0 -0
  101. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/tutorials/dialogue.png +0 -0
  102. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/tutorials/distances.png +0 -0
  103. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/tutorials/distances_tests.png +0 -0
  104. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/tutorials/enrichment.png +0 -0
  105. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/tutorials/guide_rna_assignment.png +0 -0
  106. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/tutorials/mcfarland.png +0 -0
  107. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/tutorials/metadata.png +0 -0
  108. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/tutorials/milo.png +0 -0
  109. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/tutorials/mixscape.png +0 -0
  110. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/tutorials/norman.png +0 -0
  111. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/tutorials/ontology.png +0 -0
  112. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/tutorials/perturbation_space.png +0 -0
  113. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/tutorials/placeholder.png +0 -0
  114. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/tutorials/sccoda.png +0 -0
  115. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/tutorials/sccoda_extended.png +0 -0
  116. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/tutorials/scgen_perturbation_prediction.png +0 -0
  117. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/tutorials/tasccoda.png +0 -0
  118. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_static/tutorials/zhang.png +0 -0
  119. {pertpy-1.0.0 → pertpy-1.0.1}/docs/_templates/autosummary/class.rst +0 -0
  120. {pertpy-1.0.0 → pertpy-1.0.1}/docs/about/background.md +0 -0
  121. {pertpy-1.0.0 → pertpy-1.0.1}/docs/about/cite.md +0 -0
  122. {pertpy-1.0.0 → pertpy-1.0.1}/docs/api/datasets_index.md +0 -0
  123. {pertpy-1.0.0 → pertpy-1.0.1}/docs/api/metadata_index.md +0 -0
  124. {pertpy-1.0.0 → pertpy-1.0.1}/docs/api/preprocessing_index.md +0 -0
  125. {pertpy-1.0.0 → pertpy-1.0.1}/docs/api.md +0 -0
  126. {pertpy-1.0.0 → pertpy-1.0.1}/docs/conf.py +0 -0
  127. {pertpy-1.0.0 → pertpy-1.0.1}/docs/contributing.md +0 -0
  128. {pertpy-1.0.0 → pertpy-1.0.1}/docs/index.md +0 -0
  129. {pertpy-1.0.0 → pertpy-1.0.1}/docs/installation.md +0 -0
  130. {pertpy-1.0.0 → pertpy-1.0.1}/docs/make.bat +0 -0
  131. {pertpy-1.0.0 → pertpy-1.0.1}/docs/references.bib +0 -0
  132. {pertpy-1.0.0 → pertpy-1.0.1}/docs/references.md +0 -0
  133. {pertpy-1.0.0 → pertpy-1.0.1}/docs/tutorials/metadata.md +0 -0
  134. {pertpy-1.0.0 → pertpy-1.0.1}/docs/tutorials/preprocessing.md +0 -0
  135. {pertpy-1.0.0 → pertpy-1.0.1}/docs/tutorials/tools.md +0 -0
  136. {pertpy-1.0.0 → pertpy-1.0.1}/docs/tutorials.md +0 -0
  137. {pertpy-1.0.0 → pertpy-1.0.1}/docs/usecases.md +0 -0
  138. {pertpy-1.0.0 → pertpy-1.0.1}/docs/utils.py +0 -0
  139. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/_doc.py +0 -0
  140. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/_types.py +0 -0
  141. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/data/__init__.py +0 -0
  142. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/data/_datasets.py +0 -0
  143. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/metadata/__init__.py +0 -0
  144. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/metadata/_cell_line.py +0 -0
  145. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/metadata/_compound.py +0 -0
  146. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/metadata/_drug.py +0 -0
  147. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/metadata/_look_up.py +0 -0
  148. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/metadata/_metadata.py +0 -0
  149. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/metadata/_moa.py +0 -0
  150. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/plot/__init__.py +0 -0
  151. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/preprocessing/__init__.py +0 -0
  152. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/preprocessing/_guide_rna.py +0 -0
  153. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/preprocessing/_guide_rna_mixture.py +0 -0
  154. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/py.typed +0 -0
  155. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/tools/_augur.py +0 -0
  156. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/tools/_cinemaot.py +0 -0
  157. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/tools/_coda/__init__.py +0 -0
  158. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/tools/_coda/_tasccoda.py +0 -0
  159. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/tools/_differential_gene_expression/_checks.py +0 -0
  160. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/tools/_differential_gene_expression/_dge_comparison.py +0 -0
  161. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/tools/_differential_gene_expression/_simple_tests.py +0 -0
  162. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/tools/_differential_gene_expression/_statsmodels.py +0 -0
  163. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/tools/_distances/__init__.py +0 -0
  164. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/tools/_enrichment.py +0 -0
  165. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/tools/_mixscape.py +0 -0
  166. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/tools/_perturbation_space/__init__.py +0 -0
  167. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/tools/_perturbation_space/_clustering.py +0 -0
  168. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/tools/_perturbation_space/_comparison.py +0 -0
  169. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/tools/_perturbation_space/_metrics.py +0 -0
  170. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/tools/_perturbation_space/_perturbation_space.py +0 -0
  171. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/tools/_scgen/__init__.py +0 -0
  172. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/tools/_scgen/_base_components.py +0 -0
  173. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/tools/_scgen/_scgen.py +0 -0
  174. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/tools/_scgen/_scgenvae.py +0 -0
  175. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/tools/_scgen/_utils.py +0 -0
  176. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/tools/decoupler_LICENSE +0 -0
  177. {pertpy-1.0.0 → pertpy-1.0.1}/pertpy/tools/transferlearning_MMD_LICENSE +0 -0
  178. {pertpy-1.0.0 → pertpy-1.0.1}/tests/conftest.py +0 -0
  179. {pertpy-1.0.0 → pertpy-1.0.1}/tests/metadata/test_cell_line.py +0 -0
  180. {pertpy-1.0.0 → pertpy-1.0.1}/tests/metadata/test_compound.py +0 -0
  181. {pertpy-1.0.0 → pertpy-1.0.1}/tests/metadata/test_drug.py +0 -0
  182. {pertpy-1.0.0 → pertpy-1.0.1}/tests/metadata/test_moa.py +0 -0
  183. {pertpy-1.0.0 → pertpy-1.0.1}/tests/preprocessing/test_grna_assignment.py +0 -0
  184. {pertpy-1.0.0 → pertpy-1.0.1}/tests/tools/_coda/test_sccoda.py +0 -0
  185. {pertpy-1.0.0 → pertpy-1.0.1}/tests/tools/_coda/test_tasccoda.py +0 -0
  186. {pertpy-1.0.0 → pertpy-1.0.1}/tests/tools/_differential_gene_expression/__init__.py +0 -0
  187. {pertpy-1.0.0 → pertpy-1.0.1}/tests/tools/_differential_gene_expression/test_dge.py +0 -0
  188. {pertpy-1.0.0 → pertpy-1.0.1}/tests/tools/_distances/test_distance_tests.py +0 -0
  189. {pertpy-1.0.0 → pertpy-1.0.1}/tests/tools/_perturbation_space/test_comparison.py +0 -0
  190. {pertpy-1.0.0 → pertpy-1.0.1}/tests/tools/_perturbation_space/test_discriminator_classifiers.py +0 -0
  191. {pertpy-1.0.0 → pertpy-1.0.1}/tests/tools/_perturbation_space/test_simple_cluster_space.py +0 -0
  192. {pertpy-1.0.0 → pertpy-1.0.1}/tests/tools/_perturbation_space/test_simple_perturbation_space.py +0 -0
  193. {pertpy-1.0.0 → pertpy-1.0.1}/tests/tools/test_augur.py +0 -0
  194. {pertpy-1.0.0 → pertpy-1.0.1}/tests/tools/test_cinemaot.py +0 -0
  195. {pertpy-1.0.0 → pertpy-1.0.1}/tests/tools/test_dialogue.py +0 -0
  196. {pertpy-1.0.0 → pertpy-1.0.1}/tests/tools/test_enrichment.py +0 -0
  197. {pertpy-1.0.0 → pertpy-1.0.1}/tests/tools/test_mixscape.py +0 -0
  198. {pertpy-1.0.0 → pertpy-1.0.1}/tests/tools/test_scgen.py +0 -0
@@ -1,5 +1,5 @@
1
- name-template: "1.0.0 🌈"
2
- tag-template: 1.0.0
1
+ name-template: "1.0.1 🌈"
2
+ tag-template: 1.0.1
3
3
  exclude-labels:
4
4
  - "skip-changelog"
5
5
 
@@ -0,0 +1,94 @@
1
+ name: Test tutorials
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ concurrency:
10
+ group: ${{ github.workflow }}-${{ github.ref }}
11
+ cancel-in-progress: true
12
+
13
+ jobs:
14
+ test:
15
+ runs-on: ubuntu-latest
16
+ defaults:
17
+ run:
18
+ # to fail on error in multiline statements (-e), in pipes (-o pipefail), and on unset variables (-u).
19
+ shell: bash -euo pipefail {0}
20
+
21
+ env:
22
+ NOTEBOOK_PATH: docs/tutorials/notebooks
23
+
24
+ strategy:
25
+ fail-fast: false
26
+ matrix:
27
+ notebook: [
28
+ "augur.ipynb",
29
+ "cinemaot.ipynb",
30
+ "differential_gene_expression.ipynb",
31
+ "distances.ipynb",
32
+ "distance_tests.ipynb",
33
+ "enrichment.ipynb",
34
+ "guide_rna_assignment.ipynb",
35
+ "milo.ipynb",
36
+ "mixscape.ipynb",
37
+ "perturbation_space.ipynb",
38
+ "sccoda.ipynb",
39
+ # "dialogue.ipynb", takes too long
40
+ # "tasccoda.ipynb", a pain to get running because of the required QT dependency. The QT action leads to a dead kernel
41
+ # also no use cases yet
42
+ ]
43
+
44
+ steps:
45
+ - uses: actions/checkout@v4
46
+ with:
47
+ filter: blob:none
48
+ fetch-depth: 0
49
+ submodules: "true"
50
+
51
+ - name: Cache .pertpy_cache
52
+ uses: actions/cache@v4
53
+ with:
54
+ path: cache
55
+ key: ${{ runner.os }}-pertpy-cache-${{ hashFiles('pertpy/metadata/**') }}
56
+ restore-keys: |
57
+ ${{ runner.os }}-pertpy-cache
58
+
59
+ - name: Set up Python
60
+ uses: actions/setup-python@v5
61
+ with:
62
+ python-version: "3.13"
63
+ - name: Install R
64
+ uses: r-lib/actions/setup-r@v2
65
+ with:
66
+ r-version: "4.4.3"
67
+
68
+ - name: Cache R packages
69
+ id: r-cache
70
+ uses: actions/cache@v3
71
+ with:
72
+ path: ${{ env.R_LIBS_USER }}
73
+ key: ${{ runner.os }}-r-${{ hashFiles('**/pertpy/tools/_milo.py') }}
74
+ restore-keys: ${{ runner.os }}-r-
75
+
76
+ - name: Install R dependencies
77
+ if: steps.r-cache.outputs.cache-hit != 'true'
78
+ run: |
79
+ mkdir -p ${{ env.R_LIBS_USER }}
80
+ Rscript --vanilla -e "install.packages(c('BiocManager', 'statmod'), repos='https://cran.r-project.org'); BiocManager::install('edgeR', lib='${{ env.R_LIBS_USER }}')"
81
+
82
+ - name: Install uv
83
+ uses: astral-sh/setup-uv@v6
84
+ with:
85
+ enable-cache: true
86
+ cache-dependency-glob: pyproject.toml
87
+ - name: Install dependencies
88
+ run: |
89
+ uv pip install --system rpy2 decoupler muon
90
+ uv pip install --system ${{ matrix.pip-flags }} ".[dev,test,tcoda,de]"
91
+ uv pip install --system nbconvert ipykernel
92
+
93
+ - name: Run ${{ matrix.notebook }} Notebook
94
+ run: jupyter nbconvert --to notebook --execute ${{ env.NOTEBOOK_PATH }}/${{ matrix.notebook }}
@@ -7,13 +7,13 @@ default_stages:
7
7
  minimum_pre_commit_version: 2.16.0
8
8
  repos:
9
9
  - repo: https://github.com/biomejs/pre-commit
10
- rev: v1.9.4
10
+ rev: v2.1.1
11
11
  hooks:
12
12
  - id: biome-format
13
13
  - repo: https://github.com/astral-sh/ruff-pre-commit
14
- rev: v0.11.9
14
+ rev: v0.12.3
15
15
  hooks:
16
- - id: ruff
16
+ - id: ruff-check
17
17
  args: [--fix, --exit-non-zero-on-fix, --unsafe-fixes]
18
18
  - id: ruff-format
19
19
  - repo: https://github.com/pre-commit/pre-commit-hooks
@@ -33,7 +33,7 @@ repos:
33
33
  - id: no-commit-to-branch
34
34
  args: ["--branch=main"]
35
35
  - repo: https://github.com/pre-commit/mirrors-mypy
36
- rev: v1.15.0
36
+ rev: v1.16.1
37
37
  hooks:
38
38
  - id: mypy
39
39
  args: [--no-strict-optional, --ignore-missing-imports]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pertpy
3
- Version: 1.0.0
3
+ Version: 1.0.1
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
@@ -49,7 +49,7 @@ Requires-Python: <3.14,>=3.11
49
49
  Requires-Dist: adjusttext
50
50
  Requires-Dist: arviz
51
51
  Requires-Dist: blitzgsea
52
- Requires-Dist: fast-array-utils
52
+ Requires-Dist: fast-array-utils[accel,sparse]
53
53
  Requires-Dist: lamin-utils
54
54
  Requires-Dist: mudata
55
55
  Requires-Dist: openpyxl
@@ -93,7 +93,7 @@ Requires-Dist: sphinxext-opengraph; extra == 'doc'
93
93
  Provides-Extra: tcoda
94
94
  Requires-Dist: ete4; extra == 'tcoda'
95
95
  Requires-Dist: pyqt6; extra == 'tcoda'
96
- Requires-Dist: toytree; extra == 'tcoda'
96
+ Requires-Dist: toytree>=3.0; extra == 'tcoda'
97
97
  Provides-Extra: test
98
98
  Requires-Dist: coverage; extra == 'test'
99
99
  Requires-Dist: leidenalg; extra == 'test'
@@ -0,0 +1,34 @@
1
+ {
2
+ "$schema": "https://biomejs.dev/schemas/2.0.5/schema.json",
3
+ "formatter": { "useEditorconfig": true },
4
+ "overrides": [
5
+ {
6
+ "includes": [".vscode/**/*.json", "**/*.jsonc", "**/asv.conf.json"],
7
+ "json": {
8
+ "formatter": {
9
+ "trailingCommas": "all",
10
+ },
11
+ "parser": {
12
+ "allowComments": true,
13
+ "allowTrailingCommas": true,
14
+ },
15
+ },
16
+ },
17
+ ],
18
+ "linter": {
19
+ "rules": {
20
+ "style": {
21
+ "noParameterAssign": "error",
22
+ "useAsConstAssertion": "error",
23
+ "useDefaultParameterLast": "error",
24
+ "useEnumInitializers": "error",
25
+ "useSelfClosingElements": "error",
26
+ "useSingleVarDeclarator": "error",
27
+ "noUnusedTemplateLiteral": "error",
28
+ "useNumberNamespace": "error",
29
+ "noInferrableTypes": "error",
30
+ "noUselessElse": "error",
31
+ },
32
+ },
33
+ },
34
+ }
@@ -423,8 +423,6 @@ ps_adata = ps.compute(
423
423
  target_col="gene_target",
424
424
  groups_col="gene_target",
425
425
  mode="mean",
426
- min_cells=0,
427
- min_counts=0,
428
426
  )
429
427
  ```
430
428
 
@@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## v1.0.1
9
+
10
+
11
+ ### 🚀 Features
12
+
13
+ * Add support for gamma parameter in MMD distance ([#825](https://github.com/scverse/pertpy/pull/825)) @Zethson
14
+ * Run notebooks in CI ([#815](https://github.com/scverse/pertpy/pull/815)) @Zethson
15
+
16
+ ## 🐛 Bug Fixes
17
+
18
+ * Fix milo output writing ([#821](https://github.com/scverse/pertpy/pull/821)) @Zethson
19
+
8
20
  ## v1.0.0
9
21
 
10
22
  ### 🚀 Features
@@ -2,7 +2,7 @@
2
2
 
3
3
  __author__ = "Lukas Heumos"
4
4
  __email__ = "lukas.heumos@posteo.net"
5
- __version__ = "1.0.0"
5
+ __version__ = "1.0.1"
6
6
 
7
7
  import warnings
8
8
 
@@ -0,0 +1,117 @@
1
+ import shutil
2
+ import tempfile
3
+ import time
4
+ from pathlib import Path
5
+ from random import choice
6
+ from string import ascii_lowercase
7
+ from zipfile import ZipFile
8
+
9
+ import requests
10
+ from filelock import FileLock
11
+ from lamin_utils import logger
12
+ from requests.exceptions import RequestException
13
+ from rich.progress import Progress
14
+
15
+
16
+ def _download( # pragma: no cover
17
+ url: str,
18
+ output_file_name: str | None = None,
19
+ output_path: str | Path | None = None,
20
+ block_size: int = 1024,
21
+ overwrite: bool = False,
22
+ is_zip: bool = False,
23
+ timeout: int = 30,
24
+ max_retries: int = 3,
25
+ retry_delay: int = 5,
26
+ ) -> Path:
27
+ """Downloads a dataset irrespective of the format.
28
+
29
+ Args:
30
+ url: URL to download
31
+ output_file_name: Name of the downloaded file
32
+ output_path: Path to download/extract the files to.
33
+ block_size: Block size for downloads in bytes.
34
+ overwrite: Whether to overwrite existing files.
35
+ is_zip: Whether the downloaded file needs to be unzipped.
36
+ timeout: Request timeout in seconds.
37
+ max_retries: Maximum number of retry attempts.
38
+ retry_delay: Delay between retries in seconds.
39
+ """
40
+ if output_file_name is None:
41
+ letters = ascii_lowercase
42
+ output_file_name = f"pertpy_tmp_{''.join(choice(letters) for _ in range(10))}"
43
+
44
+ if output_path is None:
45
+ output_path = tempfile.gettempdir()
46
+
47
+ download_to_path = Path(output_path) / output_file_name
48
+
49
+ Path(output_path).mkdir(parents=True, exist_ok=True)
50
+ lock_path = Path(output_path) / f"{output_file_name}.lock"
51
+
52
+ try:
53
+ with FileLock(lock_path, timeout=300):
54
+ if Path(download_to_path).exists() and not overwrite:
55
+ logger.warning(f"File {download_to_path} already exists!")
56
+ return download_to_path
57
+
58
+ temp_file_name = Path(f"{download_to_path}.part")
59
+
60
+ retry_count = 0
61
+ while retry_count <= max_retries:
62
+ try:
63
+ head_response = requests.head(url, timeout=timeout)
64
+ head_response.raise_for_status()
65
+ content_length = int(head_response.headers.get("content-length", 0))
66
+
67
+ free_space = shutil.disk_usage(output_path).free
68
+ if content_length > free_space:
69
+ raise OSError(
70
+ f"Insufficient disk space. Need {content_length} bytes, but only {free_space} available."
71
+ )
72
+
73
+ response = requests.get(url, stream=True)
74
+ response.raise_for_status()
75
+ total = int(response.headers.get("content-length", 0))
76
+
77
+ with Progress(refresh_per_second=5) as progress:
78
+ task = progress.add_task("[red]Downloading...", total=total)
79
+ with Path(temp_file_name).open("wb") as file:
80
+ for data in response.iter_content(block_size):
81
+ file.write(data)
82
+ progress.update(task, advance=len(data))
83
+ progress.update(task, completed=total, refresh=True)
84
+
85
+ Path(temp_file_name).replace(download_to_path)
86
+
87
+ if is_zip:
88
+ with ZipFile(download_to_path, "r") as zip_obj:
89
+ zip_obj.extractall(path=output_path)
90
+ return Path(output_path)
91
+
92
+ return download_to_path
93
+ except (OSError, RequestException) as e:
94
+ retry_count += 1
95
+ if retry_count <= max_retries:
96
+ logger.warning(
97
+ f"Download attempt {retry_count}/{max_retries} failed: {str(e)}. Retrying in {retry_delay} seconds..."
98
+ )
99
+ time.sleep(retry_delay)
100
+ else:
101
+ logger.error(f"Download failed after {max_retries} attempts: {str(e)}")
102
+ if Path(temp_file_name).exists():
103
+ Path(temp_file_name).unlink(missing_ok=True)
104
+ raise
105
+
106
+ except Exception as e:
107
+ logger.error(f"Download failed: {str(e)}")
108
+ if Path(temp_file_name).exists():
109
+ Path(temp_file_name).unlink(missing_ok=True)
110
+ raise
111
+ finally:
112
+ if Path(temp_file_name).exists():
113
+ Path(temp_file_name).unlink(missing_ok=True)
114
+ finally:
115
+ lock_path.unlink(missing_ok=True)
116
+
117
+ return Path(download_to_path)
@@ -1,24 +1,5 @@
1
1
  from importlib import import_module
2
2
 
3
-
4
- def lazy_import(module_path: str, class_name: str, extras: list[str]):
5
- try:
6
- for extra in extras:
7
- import_module(extra)
8
- module = import_module(module_path)
9
- return getattr(module, class_name)
10
- except ImportError:
11
-
12
- class Placeholder:
13
- def __init__(self, *args, **kwargs):
14
- raise ImportError(
15
- f"Extra dependencies required: {', '.join(extras)}. "
16
- f"Please install with: pip install {' '.join(extras)}"
17
- )
18
-
19
- return Placeholder
20
-
21
-
22
3
  from pertpy.tools._augur import Augur
23
4
  from pertpy.tools._cinemaot import Cinemaot
24
5
  from pertpy.tools._coda._sccoda import Sccoda
@@ -42,15 +23,25 @@ from pertpy.tools._perturbation_space._simple import (
42
23
  )
43
24
  from pertpy.tools._scgen import Scgen
44
25
 
45
- CODA_EXTRAS = ["toytree", "ete4"] # also "pyqt6" but it cannot be imported
46
- Tasccoda = lazy_import("pertpy.tools._coda._tasccoda", "Tasccoda", CODA_EXTRAS)
47
26
 
48
- DE_EXTRAS = ["formulaic", "pydeseq2"]
49
- EdgeR = lazy_import("pertpy.tools._differential_gene_expression", "EdgeR", DE_EXTRAS) # edgeR will be imported via rpy2
50
- PyDESeq2 = lazy_import("pertpy.tools._differential_gene_expression", "PyDESeq2", DE_EXTRAS)
51
- Statsmodels = lazy_import("pertpy.tools._differential_gene_expression", "Statsmodels", DE_EXTRAS + ["statsmodels"])
52
- TTest = lazy_import("pertpy.tools._differential_gene_expression", "TTest", DE_EXTRAS)
53
- WilcoxonTest = lazy_import("pertpy.tools._differential_gene_expression", "WilcoxonTest", DE_EXTRAS)
27
+ def __getattr__(name: str):
28
+ if name == "Tasccoda":
29
+ try:
30
+ for extra in ["toytree", "ete4"]:
31
+ import_module(extra)
32
+ module = import_module("pertpy.tools._coda._tasccoda")
33
+ return module.Tasccoda
34
+ except ImportError:
35
+ raise ImportError(
36
+ "Extra dependencies required: toytree, ete4. Please install with: pip install toytree ete4"
37
+ ) from None
38
+
39
+ elif name in ["EdgeR", "PyDESeq2", "Statsmodels", "TTest", "WilcoxonTest"]:
40
+ module = import_module("pertpy.tools._differential_gene_expression")
41
+ return getattr(module, name)
42
+
43
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
44
+
54
45
 
55
46
  __all__ = [
56
47
  "Augur",
@@ -1181,7 +1181,7 @@ class CompositionalModel2(ABC):
1181
1181
  r,
1182
1182
  bars,
1183
1183
  bottom=cum_bars,
1184
- color=palette(n % palette.N),
1184
+ color=palette(n % palette.N), # type: ignore
1185
1185
  width=barwidth,
1186
1186
  label=type_names[n],
1187
1187
  linewidth=0,
@@ -1377,6 +1377,7 @@ class CompositionalModel2(ABC):
1377
1377
  plot_df.columns = covariate_names
1378
1378
  plot_df = pd.melt(plot_df, ignore_index=False, var_name="Covariate")
1379
1379
 
1380
+ plot_df.index.name = "Cell Type"
1380
1381
  plot_df = plot_df.reset_index()
1381
1382
 
1382
1383
  if len(covariate_names_zero) != 0 and plot_facets and plot_zero_covariate and not plot_zero_cell_type:
@@ -1472,6 +1473,7 @@ class CompositionalModel2(ABC):
1472
1473
  if return_fig and not plot_facets:
1473
1474
  return plt.gcf()
1474
1475
  plt.show()
1476
+
1475
1477
  return None
1476
1478
 
1477
1479
  @_doc_params(common_plot_args=doc_common_plot_args)
@@ -1823,6 +1825,7 @@ class CompositionalModel2(ABC):
1823
1825
  if return_fig:
1824
1826
  return plt.gcf()
1825
1827
  plt.show()
1828
+
1826
1829
  return None
1827
1830
 
1828
1831
  @_doc_params(common_plot_args=doc_common_plot_args)
@@ -1881,7 +1884,7 @@ class CompositionalModel2(ABC):
1881
1884
  from ete4.treeview import CircleFace, NodeStyle, TextFace, TreeStyle, faces
1882
1885
  except ImportError:
1883
1886
  raise ImportError(
1884
- "To use tasccoda please install additional dependencies with `pip install pertpy[coda]`"
1887
+ "To use tasccoda please install additional dependencies: `pip install pertpy[coda]`"
1885
1888
  ) from None
1886
1889
 
1887
1890
  if isinstance(data, MuData):
@@ -1902,8 +1905,8 @@ class CompositionalModel2(ABC):
1902
1905
  tree.render(save, tree_style=tree_style, units=units, w=figsize[0], h=figsize[1], dpi=dpi) # type: ignore
1903
1906
  if return_fig:
1904
1907
  return tree, tree_style
1908
+
1905
1909
  return tree.render("%%inline", tree_style=tree_style, units=units, w=figsize[0], h=figsize[1], dpi=dpi) # type: ignore
1906
- return None
1907
1910
 
1908
1911
  @_doc_params(common_plot_args=doc_common_plot_args)
1909
1912
  def plot_draw_effects( # pragma: no cover # noqa: D417
@@ -1969,7 +1972,7 @@ class CompositionalModel2(ABC):
1969
1972
  from ete4.treeview import CircleFace, NodeStyle, TextFace, TreeStyle, faces
1970
1973
  except ImportError:
1971
1974
  raise ImportError(
1972
- "To use tasccoda please install additional dependencies as `pip install pertpy[coda]`"
1975
+ "To use tasccoda please install additional dependencies: `pip install pertpy[coda]`"
1973
1976
  ) from None
1974
1977
 
1975
1978
  if isinstance(data, MuData):
@@ -2207,6 +2210,7 @@ class CompositionalModel2(ABC):
2207
2210
  if return_fig:
2208
2211
  return fig
2209
2212
  plt.show()
2213
+
2210
2214
  return None
2211
2215
 
2212
2216
 
@@ -2325,6 +2329,7 @@ def df2newick(df: pd.DataFrame, levels: list[str], inner_label: bool = True) ->
2325
2329
  strs = [traverse(df_tax, a, 0, inner_label) for a in alevel]
2326
2330
 
2327
2331
  newick = f"({','.join(strs)});"
2332
+
2328
2333
  return newick
2329
2334
 
2330
2335
 
@@ -2562,6 +2567,7 @@ def from_scanpy(
2562
2567
  covariate_obs = list(set(covariate_obs or []) | set(sample_identifier))
2563
2568
 
2564
2569
  if isinstance(sample_identifier, list):
2570
+ adata.obs = adata.obs.copy()
2565
2571
  adata.obs["scCODA_sample_id"] = adata.obs[sample_identifier].agg("-".join, axis=1)
2566
2572
  sample_identifier = "scCODA_sample_id"
2567
2573
 
@@ -409,6 +409,48 @@ class Sccoda(CompositionalModel2):
409
409
  import arviz as az
410
410
 
411
411
  # Create arviz object
412
+ if use_posterior_predictive:
413
+ posterior_predictive = Predictive(self.model, self.mcmc.get_samples())(
414
+ rng_key,
415
+ counts=None,
416
+ covariates=numpyro_covariates,
417
+ n_total=numpyro_n_total,
418
+ ref_index=ref_index,
419
+ sample_adata=sample_adata,
420
+ )
421
+ # Remove problematic posterior predictive arrays with wrong dimensions
422
+ if posterior_predictive and "counts" in posterior_predictive:
423
+ counts_shape = posterior_predictive["counts"].shape
424
+ expected_dims = 2 # ['sample', 'cell_type']
425
+ if len(counts_shape) != expected_dims:
426
+ posterior_predictive = {k: v for k, v in posterior_predictive.items() if k != "counts"}
427
+ logger.warning(
428
+ f"Removed 'counts' from posterior_predictive due to dimension mismatch: got {len(counts_shape)}D, expected {expected_dims}D"
429
+ )
430
+ else:
431
+ posterior_predictive = None
432
+
433
+ if num_prior_samples > 0:
434
+ prior = Predictive(self.model, num_samples=num_prior_samples)(
435
+ rng_key,
436
+ counts=None,
437
+ covariates=numpyro_covariates,
438
+ n_total=numpyro_n_total,
439
+ ref_index=ref_index,
440
+ sample_adata=sample_adata,
441
+ )
442
+ # Remove problematic prior arrays with wrong dimensions
443
+ if prior and "counts" in prior:
444
+ counts_shape = prior["counts"].shape
445
+ expected_dims = 2 # ['sample', 'cell_type']
446
+ if len(counts_shape) != expected_dims:
447
+ prior = {k: v for k, v in prior.items() if k != "counts"}
448
+ logger.warning(
449
+ f"Removed 'counts' from prior due to dimension mismatch: got {len(counts_shape)}D, expected {expected_dims}D"
450
+ )
451
+ else:
452
+ prior = None
453
+
412
454
  arviz_data = az.from_numpyro(
413
455
  self.mcmc, prior=prior, posterior_predictive=posterior_predictive, dims=dims, coords=coords
414
456
  )
@@ -882,9 +882,9 @@ class Dialogue:
882
882
  if len(conditions_compare) != 2:
883
883
  raise ValueError("Please specify conditions to compare or supply an object with only 2 conditions")
884
884
 
885
- pvals = pd.DataFrame(1, adata.obs[celltype_label].unique(), ["mcp_" + str(n) for n in range(n_mcps)])
886
- tstats = pd.DataFrame(1, adata.obs[celltype_label].unique(), ["mcp_" + str(n) for n in range(n_mcps)])
887
- pvals_adj = pd.DataFrame(1, adata.obs[celltype_label].unique(), ["mcp_" + str(n) for n in range(n_mcps)])
885
+ pvals = pd.DataFrame(1.0, adata.obs[celltype_label].unique(), ["mcp_" + str(n) for n in range(n_mcps)])
886
+ tstats = pd.DataFrame(1.0, adata.obs[celltype_label].unique(), ["mcp_" + str(n) for n in range(n_mcps)])
887
+ pvals_adj = pd.DataFrame(1.0, adata.obs[celltype_label].unique(), ["mcp_" + str(n) for n in range(n_mcps)])
888
888
 
889
889
  response = adata.obs.groupby(sample_label)[condition_label].agg(pd.Series.mode)
890
890
  for celltype in adata.obs[celltype_label].unique():
@@ -0,0 +1,60 @@
1
+ import contextlib
2
+ from importlib import import_module
3
+ from importlib.util import find_spec
4
+
5
+ from ._base import LinearModelBase, MethodBase
6
+ from ._dge_comparison import DGEEVAL
7
+ from ._edger import EdgeR
8
+ from ._simple_tests import SimpleComparisonBase, TTest, WilcoxonTest
9
+
10
+
11
+ def __getattr__(name: str):
12
+ deps = {
13
+ "PyDESeq2": ["pydeseq2", "formulaic_contrasts", "formulaic"],
14
+ "EdgeR": ["rpy2", "formulaic_contrasts", "formulaic"],
15
+ "Statsmodels": ["formulaic_contrasts", "formulaic"],
16
+ }
17
+
18
+ if name in deps:
19
+ for dep in deps[name]:
20
+ if find_spec(dep) is None:
21
+ raise ImportError(f"{dep} is required but not installed")
22
+
23
+ module_map = {
24
+ "PyDESeq2": "pertpy.tools._differential_gene_expression._pydeseq2",
25
+ "EdgeR": "pertpy.tools._differential_gene_expression._edger",
26
+ "Statsmodels": "pertpy.tools._differential_gene_expression._statsmodels",
27
+ }
28
+
29
+ module = import_module(module_map[name])
30
+ return getattr(module, name)
31
+
32
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
33
+
34
+
35
+ def _get_available_methods():
36
+ methods = [WilcoxonTest, TTest]
37
+ from importlib.util import find_spec
38
+
39
+ for name in ["Statsmodels", "PyDESeq2", "EdgeR"]:
40
+ with contextlib.suppress(ImportError):
41
+ methods.append(__getattr__(name))
42
+
43
+ return methods
44
+
45
+
46
+ AVAILABLE_METHODS = _get_available_methods()
47
+
48
+
49
+ AVAILABLE_METHODS = _get_available_methods()
50
+
51
+ __all__ = [
52
+ "MethodBase",
53
+ "LinearModelBase",
54
+ "EdgeR",
55
+ "PyDESeq2",
56
+ "Statsmodels",
57
+ "SimpleComparisonBase",
58
+ "WilcoxonTest",
59
+ "TTest",
60
+ ]
@@ -12,7 +12,6 @@ import matplotlib.pyplot as plt
12
12
  import numpy as np
13
13
  import pandas as pd
14
14
  import seaborn as sns
15
- from formulaic_contrasts import FormulaicContrasts
16
15
  from lamin_utils import logger
17
16
  from matplotlib.pyplot import Figure
18
17
  from matplotlib.ticker import MaxNLocator
@@ -881,6 +880,8 @@ class LinearModelBase(MethodBase):
881
880
  super().__init__(adata, mask=mask, layer=layer)
882
881
  self._check_counts()
883
882
 
883
+ from formulaic_contrasts import FormulaicContrasts
884
+
884
885
  self.formulaic_contrasts = None
885
886
  if isinstance(design, str):
886
887
  self.formulaic_contrasts = FormulaicContrasts(adata.obs, design)