exprmat 0.1.3__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 (67) hide show
  1. exprmat-0.1.3/PKG-INFO +36 -0
  2. exprmat-0.1.3/exprmat/__init__.py +5 -0
  3. exprmat-0.1.3/exprmat/ansi.py +168 -0
  4. exprmat-0.1.3/exprmat/clustering/__init__.py +2 -0
  5. exprmat-0.1.3/exprmat/clustering/leiden.py +95 -0
  6. exprmat-0.1.3/exprmat/clustering/spectral.py +10 -0
  7. exprmat-0.1.3/exprmat/clustering/utils.py +61 -0
  8. exprmat-0.1.3/exprmat/cnv/__init__.py +3 -0
  9. exprmat-0.1.3/exprmat/cnv/cnvscore.py +167 -0
  10. exprmat-0.1.3/exprmat/cnv/infercnv.py +463 -0
  11. exprmat-0.1.3/exprmat/configuration.py +43 -0
  12. exprmat-0.1.3/exprmat/data/__init__.py +5 -0
  13. exprmat-0.1.3/exprmat/data/delim.py +89 -0
  14. exprmat-0.1.3/exprmat/data/finders.py +176 -0
  15. exprmat-0.1.3/exprmat/data/gff.py +162 -0
  16. exprmat-0.1.3/exprmat/data/hsa/genome.tsv.gz +0 -0
  17. exprmat-0.1.3/exprmat/data/mmu/genome.tsv.gz +0 -0
  18. exprmat-0.1.3/exprmat/data/private.py +70 -0
  19. exprmat-0.1.3/exprmat/data/ranges.py +164 -0
  20. exprmat-0.1.3/exprmat/descriptive/__init__.py +0 -0
  21. exprmat-0.1.3/exprmat/descriptive/de.py +43 -0
  22. exprmat-0.1.3/exprmat/descriptive/kde.py +119 -0
  23. exprmat-0.1.3/exprmat/plotting/__init__.py +6 -0
  24. exprmat-0.1.3/exprmat/plotting/cnv.py +156 -0
  25. exprmat-0.1.3/exprmat/plotting/de.py +300 -0
  26. exprmat-0.1.3/exprmat/plotting/expression.py +68 -0
  27. exprmat-0.1.3/exprmat/plotting/palettes.py +1653 -0
  28. exprmat-0.1.3/exprmat/preprocessing/__init__.py +165 -0
  29. exprmat-0.1.3/exprmat/preprocessing/doublets/__init__.py +0 -0
  30. exprmat-0.1.3/exprmat/preprocessing/doublets/composite.py +330 -0
  31. exprmat-0.1.3/exprmat/preprocessing/doublets/scrublet.py +428 -0
  32. exprmat-0.1.3/exprmat/preprocessing/filter.py +593 -0
  33. exprmat-0.1.3/exprmat/preprocessing/integrate.py +309 -0
  34. exprmat-0.1.3/exprmat/preprocessing/linear.py +79 -0
  35. exprmat-0.1.3/exprmat/preprocessing/normal.py +13 -0
  36. exprmat-0.1.3/exprmat/preprocessing/plot.py +60 -0
  37. exprmat-0.1.3/exprmat/preprocessing/qc.py +227 -0
  38. exprmat-0.1.3/exprmat/preprocessing/sctransform/__init__.py +0 -0
  39. exprmat-0.1.3/exprmat/preprocessing/sctransform/estimate.py +85 -0
  40. exprmat-0.1.3/exprmat/preprocessing/sctransform/ksmooth.py +338 -0
  41. exprmat-0.1.3/exprmat/preprocessing/sctransform/residual.py +25 -0
  42. exprmat-0.1.3/exprmat/preprocessing/sctransform/transform.py +106 -0
  43. exprmat-0.1.3/exprmat/preprocessing/sparse.py +38 -0
  44. exprmat-0.1.3/exprmat/preprocessing/uniform.py +23 -0
  45. exprmat-0.1.3/exprmat/reader/__init__.py +2 -0
  46. exprmat-0.1.3/exprmat/reader/experiment.py +1203 -0
  47. exprmat-0.1.3/exprmat/reader/matcher.py +334 -0
  48. exprmat-0.1.3/exprmat/reader/metadata.py +241 -0
  49. exprmat-0.1.3/exprmat/reduction/__init__.py +133 -0
  50. exprmat-0.1.3/exprmat/reduction/forceatlas.py +41 -0
  51. exprmat-0.1.3/exprmat/reduction/mde.py +55 -0
  52. exprmat-0.1.3/exprmat/reduction/nn.py +465 -0
  53. exprmat-0.1.3/exprmat/reduction/pca.py +54 -0
  54. exprmat-0.1.3/exprmat/reduction/plot.py +713 -0
  55. exprmat-0.1.3/exprmat/reduction/pumap.py +593 -0
  56. exprmat-0.1.3/exprmat/reduction/tsne.py +12 -0
  57. exprmat-0.1.3/exprmat/reduction/umap.py +55 -0
  58. exprmat-0.1.3/exprmat/utils.py +155 -0
  59. exprmat-0.1.3/exprmat.egg-info/.ipynb_checkpoints/requires-checkpoint.txt +22 -0
  60. exprmat-0.1.3/exprmat.egg-info/PKG-INFO +36 -0
  61. exprmat-0.1.3/exprmat.egg-info/SOURCES.txt +65 -0
  62. exprmat-0.1.3/exprmat.egg-info/dependency_links.txt +1 -0
  63. exprmat-0.1.3/exprmat.egg-info/not-zip-safe +1 -0
  64. exprmat-0.1.3/exprmat.egg-info/requires.txt +22 -0
  65. exprmat-0.1.3/exprmat.egg-info/top_level.txt +1 -0
  66. exprmat-0.1.3/setup.cfg +4 -0
  67. exprmat-0.1.3/setup.py +42 -0
exprmat-0.1.3/PKG-INFO ADDED
@@ -0,0 +1,36 @@
1
+ Metadata-Version: 2.4
2
+ Name: exprmat
3
+ Version: 0.1.3
4
+ Summary: routines to process expression matrices
5
+ Home-page: https://github.com/yang-z-03/exprmat
6
+ Author: Zheng Yang
7
+ Author-email: xornent@outlook.com
8
+ License: GPLv3
9
+ Requires-Dist: umap-learn>=0.5.0
10
+ Requires-Dist: scvi-tools>=1.3.0
11
+ Requires-Dist: scanpy
12
+ Requires-Dist: anndata
13
+ Requires-Dist: mudata
14
+ Requires-Dist: pandas
15
+ Requires-Dist: numpy
16
+ Requires-Dist: matplotlib
17
+ Requires-Dist: seaborn
18
+ Requires-Dist: torch>=2.0
19
+ Requires-Dist: annoy
20
+ Requires-Dist: scikit-image
21
+ Requires-Dist: scikit-misc
22
+ Requires-Dist: igraph
23
+ Requires-Dist: bbknn
24
+ Requires-Dist: scanorama
25
+ Requires-Dist: harmonypy
26
+ Requires-Dist: pymde
27
+ Requires-Dist: datashader
28
+ Requires-Dist: fa2_modified
29
+ Requires-Dist: pynndescent
30
+ Requires-Dist: gseapy
31
+ Dynamic: author
32
+ Dynamic: author-email
33
+ Dynamic: home-page
34
+ Dynamic: license
35
+ Dynamic: requires-dist
36
+ Dynamic: summary
@@ -0,0 +1,5 @@
1
+
2
+ import mudata as mu
3
+ import anndata as ad
4
+
5
+ mu.set_options(pull_on_update = False)
@@ -0,0 +1,168 @@
1
+
2
+ # ./shared/ansi.py:
3
+ # provides basic ansi console coloring and cursor manipulation support.
4
+ # considering adding later another alternative implementation of the same
5
+ # interface for terminal/emulators that do not support ansi.
6
+ #
7
+ # license: gplv3. <https://www.gnu.org/licenses>
8
+ # contact: yang-z <xornent at outlook dot com>
9
+
10
+ import re
11
+ import os
12
+ import sys
13
+
14
+ # foregrounds -----------------------------------------------------------------
15
+
16
+ def fore_black() -> None:
17
+ print('\033[30m', end = '')
18
+
19
+ def fore_red() -> None:
20
+ print('\033[31m', end = '')
21
+
22
+ def fore_green() -> None:
23
+ print('\033[32m', end = '')
24
+
25
+ def fore_yellow() -> None:
26
+ print('\033[33m', end = '')
27
+
28
+ def fore_blue() -> None:
29
+ print('\033[34m', end = '')
30
+
31
+ def fore_purple() -> None:
32
+ print('\033[35m', end = '')
33
+
34
+ def fore_cyan() -> None:
35
+ print('\033[36m', end = '')
36
+
37
+ def fore_white() -> None:
38
+ print('\033[37m', end = '')
39
+
40
+ def fore_gray() -> None:
41
+ print('\033[8m', end = '')
42
+
43
+ # reset ansi controls ---------------------------------------------------------
44
+
45
+ def ansi_reset() -> None:
46
+ print('\033[0m', end = '')
47
+
48
+ # backgrounds -----------------------------------------------------------------
49
+
50
+ def back_black() -> None:
51
+ print('\033[40m', end = '')
52
+
53
+ def back_red() -> None:
54
+ print('\033[41m', end = '')
55
+
56
+ def back_green() -> None:
57
+ print('\033[42m', end = '')
58
+
59
+ def back_yellow() -> None:
60
+ print('\033[43m', end = '')
61
+
62
+ def back_blue() -> None:
63
+ print('\033[44m', end = '')
64
+
65
+ def back_purple() -> None:
66
+ print('\033[45m', end = '')
67
+
68
+ def back_cyan() -> None:
69
+ print('\033[46m', end = '')
70
+
71
+ def back_white() -> None:
72
+ print('\033[47m', end = '')
73
+
74
+ # cursor operations
75
+
76
+ def ansi_move_cursor(line: int, col: int) -> None:
77
+ if line == 0: pass
78
+ elif line > 0: print('\033[{0}B'.format(str(line)), end = '', flush = True) # moves down
79
+ elif line < 0: print('\033[{0}A'.format(str(-line)), end = '', flush = True) # moves up
80
+
81
+ if col == 0: pass
82
+ elif col > 0: print('\033[{0}C'.format(str(col)), end = '', flush = True) # moves right
83
+ elif col < 0: print('\033[{0}D'.format(str(-col)), end = '', flush = True) # moves left
84
+
85
+ def fill_blank(blanks: int) -> None:
86
+ print(' ' * blanks, end = '', flush = True)
87
+
88
+ def common_length(string: str, limit: int) -> str:
89
+
90
+ cn_re = '[\u201c-\u201d\u3001-\u3011\uff08-\uff1f\u4e00-\u9fa5]'
91
+ cn = re.findall(cn_re, string)
92
+ cn_length = len(cn)
93
+
94
+ if limit <= 5:
95
+ x_str = string[-limit:]
96
+ cn = len(re.findall(cn_re, x_str))
97
+ while cn + len(x_str) > limit:
98
+ x_str = x_str[1:]
99
+ cn = len(re.findall(cn_re, x_str))
100
+ return x_str
101
+
102
+ elif len(string) + cn_length > limit:
103
+ x_str = string[-(limit - 4):]
104
+ cn = len(re.findall(cn_re, x_str))
105
+ while cn + len(x_str) > limit - 4:
106
+ x_str = x_str[1:]
107
+ cn = len(re.findall(cn_re, x_str))
108
+ return '... ' + x_str
109
+
110
+ else: return ('{:<' + str(limit - cn_length) + '}').format(string)
111
+
112
+ def fill_blank(blanks: int, string: str) -> None:
113
+ print(common_length(string, blanks), end = '', flush = True)
114
+
115
+ def line_start() -> None:
116
+ print('\r', end = '')
117
+
118
+ def print_message(color, title, path, overwrite = True):
119
+ if overwrite:
120
+ print('\r{0}[{1}]\033[0m'.format(color, title), common_length(path, 70), end = '')
121
+ return '{0}[{1}]\033[0m'.format(color, title) + ' ' + common_length(path, 70)
122
+ else:
123
+ print('{0}[{1}]\033[0m'.format(color, title), common_length(path, 70))
124
+ return '{0}[{1}]\033[0m'.format(color, title) + ' ' + common_length(path, 70)
125
+
126
+ def format_file_size(size):
127
+ for suffix in ['B', 'KiB', 'MiB', 'GiB', 'TiB']:
128
+ if size < 1024.0 or suffix == 'TiB': break
129
+ size /= 1024.0
130
+ return f"{size:.2f} {suffix}"
131
+
132
+ # prompt messages -------------------------------------------------------------
133
+
134
+ def error(text: str) -> None:
135
+ fore_red()
136
+ print('[error]', end = ' ')
137
+ ansi_reset()
138
+ print(text)
139
+ raise Exception()
140
+
141
+ def warning(text: str) -> None:
142
+ fore_yellow()
143
+ print('[!]', end = ' ')
144
+ ansi_reset()
145
+ print(text)
146
+ ansi_reset()
147
+
148
+ def info(text: str) -> None:
149
+ fore_cyan()
150
+ print('[i]', end = ' ')
151
+ ansi_reset()
152
+ print(text)
153
+ ansi_reset()
154
+
155
+ def clear():
156
+ # for windows
157
+ if os.name == 'nt':
158
+ os.system('cls')
159
+
160
+ # for mac and linux(here, os.name is 'posix')
161
+ else: os.system('clear')
162
+
163
+ def red(x): return '\033[31m' + x + '\033[0m'
164
+ def green(x): return '\033[32m' + x + '\033[0m'
165
+ def yellow(x): return '\033[33m' + x + '\033[0m'
166
+ def blue(x): return '\033[34m' + x + '\033[0m'
167
+ def purple(x): return '\033[35m' + x + '\033[0m'
168
+ def cyan(x): return '\033[36m' + x + '\033[0m'
@@ -0,0 +1,2 @@
1
+
2
+ from exprmat.clustering.leiden import leiden as run_leiden
@@ -0,0 +1,95 @@
1
+
2
+ import anndata as ad
3
+ import scanpy as sc
4
+
5
+
6
+ def leiden(
7
+ adata: ad.AnnData,
8
+ resolution: float = 1,
9
+ *,
10
+ restrict_to = None,
11
+ random_state = 0,
12
+ key_added: str = 'leiden',
13
+ adjacency = None,
14
+ directed: bool | None = None,
15
+ use_weights: bool = True,
16
+ n_iterations: int = 2,
17
+ partition_type = None,
18
+ neighbors_key: str | None = None,
19
+ obsp: str | None = None,
20
+ flavor = 'igraph',
21
+ ** clustering_args,
22
+ ) -> ad.AnnData | None:
23
+ '''
24
+ Cluster cells using the Leiden algorithm, an improved version of the Louvain algorithm.
25
+
26
+ Parameters
27
+ ----------
28
+ adata
29
+ The annotated data matrix.
30
+ resolution
31
+ A parameter value controlling the coarseness of the clustering. Higher values lead
32
+ to more clusters. Set to `None` if overriding `partition_type` to one that doesn't
33
+ accept a `resolution_parameter`.
34
+
35
+ random_state
36
+ Change the initialization of the optimization.
37
+
38
+ restrict_to
39
+ Restrict the clustering to the categories within the key for sample annotation,
40
+ tuple needs to contain `(obs_key, list_of_categories)`.
41
+
42
+ key_added
43
+ `adata.obs` key under which to add the cluster labels.
44
+
45
+ adjacency
46
+ Sparse adjacency matrix of the graph, defaults to neighbors connectivities.
47
+
48
+ directed
49
+ Whether to treat the graph as directed or undirected. `igrah` implementation
50
+ does not support a directed graph, you will need to drop back to `leidenalg`
51
+ method for directed neighbor graphs
52
+
53
+ use_weights
54
+ If `True`, edge weights from the graph are used in the computation.
55
+ (placing more emphasis on stronger edges).
56
+
57
+ n_iterations
58
+ How many iterations of the Leiden clustering algorithm to perform. Positive values
59
+ above 2 define the total number of iterations to perform, -1 has the algorithm run
60
+ until it reaches its optimal clustering. 2 is faster and the default for
61
+ underlying packages.
62
+
63
+ partition_type
64
+ Type of partition to use. Defaults to `leidenalg.RBConfigurationVertexPartition`.
65
+ For the available options, consult the documentation for `leidenalg.find_partition`.
66
+
67
+ neighbors_key
68
+ Use neighbors connectivities as adjacency. If not specified, leiden looks at
69
+ .obsp['connectivities'] for connectivities (default storage place for pp.neighbors).
70
+ If specified, leiden looks at .obsp[.uns[neighbors_key]['connectivities_key']] for
71
+ connectivities.
72
+
73
+ obsp
74
+ Use .obsp[obsp] as adjacency. You can't specify both `obsp` and `neighbors_key`
75
+ at the same time.
76
+
77
+ copy
78
+ Whether to copy `adata` or modify it inplace.
79
+
80
+ flavor
81
+ Which package's implementation to use.
82
+
83
+ clustering_args
84
+ Any further arguments to pass to :func:`~leidenalg.find_partition` (which in turn
85
+ passes arguments to the `partition_type`) or `igraph.Graph.community_leiden` from `igraph`.
86
+ '''
87
+
88
+ return sc.tl.leiden(
89
+ adata, resolution = resolution,
90
+ restrict_to = restrict_to, random_state = random_state,
91
+ key_added = key_added, adjacency = adjacency, directed = directed,
92
+ use_weights = use_weights, n_iterations = n_iterations,
93
+ partition_type = partition_type, neighbors_key = neighbors_key,
94
+ copy = False, flavor = flavor, **clustering_args
95
+ )
@@ -0,0 +1,10 @@
1
+
2
+ def get_spectral_clusters(A, k):
3
+
4
+ from sklearn.cluster import SpectralClustering
5
+ spec = SpectralClustering(
6
+ n_clusters = k, random_state = 0,
7
+ affinity = 'precomputed', assign_labels = 'discretize'
8
+ )
9
+
10
+ return spec.fit_predict(A)
@@ -0,0 +1,61 @@
1
+
2
+ import anndata as ad
3
+ import numpy as np
4
+ import pandas as pd
5
+ from sklearn.utils import check_random_state
6
+ import random
7
+
8
+
9
+ class igraph_rng:
10
+
11
+ def __init__(self, random_state: int | np.random.RandomState = 0) -> None:
12
+ self._rng = check_random_state(random_state)
13
+
14
+ def __getattr__(self, attr: str):
15
+ return getattr(self._rng, "normal" if attr == "gauss" else attr)
16
+
17
+
18
+ def set_igraph_random_state(random_state):
19
+
20
+ import igraph
21
+ rng = igraph_rng(random_state)
22
+ try: igraph.set_random_number_generator(rng); yield None
23
+ finally: igraph.set_random_number_generator(random)
24
+
25
+
26
+ def rename_groups(
27
+ adata: ad.AnnData,
28
+ restrict_key: str,
29
+ *,
30
+ key_added: str | None,
31
+ restrict_categories,
32
+ restrict_indices,
33
+ groups
34
+ ):
35
+
36
+ key_added = f"{restrict_key}_R" if key_added is None else key_added
37
+ all_groups = adata.obs[restrict_key].astype("U")
38
+ prefix = "-".join(restrict_categories) + ","
39
+ new_groups = [prefix + g for g in groups.astype("U")]
40
+ all_groups.iloc[restrict_indices] = new_groups
41
+ return all_groups
42
+
43
+
44
+ def restrict_adjacency(
45
+ adata: ad.AnnData,
46
+ restrict_key: str,
47
+ *,
48
+ restrict_categories,
49
+ adjacency
50
+ ):
51
+ if not isinstance(restrict_categories[0], str):
52
+ msg = "You need to use strings to label categories, e.g. '1' instead of 1."
53
+ raise ValueError(msg)
54
+ for c in restrict_categories:
55
+ if c not in adata.obs[restrict_key].cat.categories:
56
+ msg = f"{c!r} is not a valid category for {restrict_key!r}"
57
+ raise ValueError(msg)
58
+ restrict_indices = adata.obs[restrict_key].isin(restrict_categories).values
59
+ adjacency = adjacency[restrict_indices, :]
60
+ adjacency = adjacency[:, restrict_indices]
61
+ return adjacency, restrict_indices
@@ -0,0 +1,3 @@
1
+
2
+ from exprmat.cnv.infercnv import infercnv as run_infercnv
3
+ from exprmat.cnv.cnvscore import cnv_score, ithcna, ithgex
@@ -0,0 +1,167 @@
1
+ import warnings
2
+ from collections.abc import Mapping
3
+ from typing import Any
4
+
5
+ import numpy as np
6
+ import scipy.sparse as sp
7
+ from anndata import AnnData
8
+
9
+ from exprmat.utils import choose_representation as choose_rep
10
+ from exprmat.ansi import warning, info, error
11
+
12
+
13
+ def cnv_score(
14
+ adata: AnnData,
15
+ groupby: str = "leiden.cnv",
16
+ *,
17
+ use_rep: str = "cnv",
18
+ key_added: str = "score.cnv"
19
+ ) -> Mapping[Any, np.number] | None:
20
+ """
21
+ Assign each CNV cluster a CNV score.
22
+
23
+ Clusters with a high score are likely affected by copy number abberations.
24
+ Based on this score, cells can be divided into tumor / normal cells. Ths score
25
+ is currently simply defined as the mean of the absolute values of the
26
+ CNV scores in each cluster.
27
+
28
+ Parameters
29
+ ----------
30
+ adata
31
+ Annotated data matrix
32
+
33
+ groupby
34
+ Key under which the clustering is stored in adata.obs. Usually the result of
35
+ CNV leiden clustering, but could also be other grouping information, e.g.
36
+ sample or patient information.
37
+
38
+ use_rep
39
+ Key under which the result of ``infercnv`` stored in adata.
40
+
41
+ key_added
42
+ Key under which the score will be stored in `adata.obs`.
43
+ """
44
+
45
+ if groupby not in adata.obs.columns:
46
+ error(f"`{groupby}` not found in `adata.obs`.")
47
+
48
+ cluster_score = {
49
+ cluster: np.mean(np.abs(adata.obsm[f"{use_rep}"][adata.obs[groupby].values == cluster, :]))
50
+ for cluster in adata.obs[groupby].unique()
51
+ }
52
+
53
+ score_array = np.array([cluster_score[c] for c in adata.obs[groupby]])
54
+ adata.obs[key_added] = score_array
55
+
56
+
57
+ def ithgex(
58
+ adata: AnnData,
59
+ groupby: str,
60
+ *,
61
+ use_raw: bool | None = None,
62
+ layer: str | None = None,
63
+ key_added: str = "score.ithgex",
64
+ ) -> Mapping[str, float] | None:
65
+ """
66
+ Compute the ITHGEX diversity score based on gene expression. A high score indicates a
67
+ high diversity of gene expression profiles of cells within a group.
68
+
69
+ The score is defined as follows:
70
+
71
+ Intratumoral heterogeneity scores based on CNAs and gene expressions
72
+ The calculations of intratumoral heterogeneity scores were inspired by a
73
+ previous study and modified as follows. First, to calculate ITHCNA, we used
74
+ the relative expression value matrix generated by inferCNV and calculated the
75
+ pairwise cell-cell distances using Pearson's correlation coefficients for each
76
+ patient. ITHCNA was defined as interquartile range (IQR) of the distribution for
77
+ all malignant cell pairs' Pearson's correlation coefficients. **Similarly, we also
78
+ used gene expression profiles of cancer cells of each patient to construct the
79
+ distribution of the intratumoral distances. ITHGEX was assigned as the IQR of the
80
+ distribution.**
81
+
82
+ Parameters
83
+ ----------
84
+ adata
85
+ Annotated data matrix
86
+
87
+ groupby
88
+ Calculate diversity for each group defined in this category.
89
+ """
90
+
91
+ groups = adata.obs[groupby].unique()
92
+ ithgex = {}
93
+
94
+ for group in groups:
95
+ tmp_adata = adata[adata.obs[groupby] == group, :]
96
+ X = choose_rep(tmp_adata, use_raw, layer)
97
+ if sp.issparse(X): X = X.todense()
98
+ if X.shape[0] <= 1: continue
99
+
100
+ pcorr = np.corrcoef(X, rowvar = True)
101
+ assert pcorr.shape == (
102
+ tmp_adata.shape[0],
103
+ tmp_adata.shape[0],
104
+ ), f"pcorr is a cell by cell matrix {tmp_adata.shape[0]} {pcorr.shape}"
105
+
106
+ q75, q25 = np.percentile(pcorr, [75, 25])
107
+ ithgex[group] = q75 - q25
108
+
109
+ ithgex_obs = np.empty(adata.shape[0])
110
+ for group in groups:
111
+ ithgex_obs[adata.obs[groupby] == group] = ithgex[group]
112
+ adata.obs[key_added] = ithgex_obs
113
+
114
+
115
+ def ithcna(
116
+ adata: AnnData,
117
+ groupby: str,
118
+ *,
119
+ use_rep: str = "cnv",
120
+ key_added: str = "score.ithcna"
121
+ ) -> Mapping[str, float] | None:
122
+ """
123
+ Compute the ITHCNA diversity score based on copy number variation.
124
+ A high score indicates a high diversity of CNV profiles of cells within a group.
125
+
126
+ The score is defined as follows:
127
+
128
+ Intratumoral heterogeneity scores based on CNAs and gene expressions
129
+ The calculations of intratumoral heterogeneity scores were inspired by a
130
+ previous study and modified as follows. First, to calculate ITHCNA, we used
131
+ the relative expression value matrix generated by inferCNV and calculated the
132
+ pairwise cell-cell distances using Pearson's correlation coefficients for each
133
+ patient. ITHCNA was defined as interquartile range (IQR) of the distribution for
134
+ all malignant cell pairs' Pearson's correlation coefficients.
135
+
136
+ Parameters
137
+ ----------
138
+ adata
139
+ Annotated data matrix
140
+
141
+ groupby
142
+ Calculate diversity for each group defined in this category.
143
+ """
144
+
145
+ groups = adata.obs[groupby].unique()
146
+ ithcna = {}
147
+
148
+ for group in groups:
149
+ tmp_adata = adata[adata.obs[groupby] == group, :]
150
+ X = tmp_adata.obsm[use_rep]
151
+ if sp.issparse(X): X = X.todense()
152
+ if X.shape[0] <= 1: continue
153
+
154
+ pcorr = np.corrcoef(X, rowvar=True)
155
+ assert pcorr.shape == (
156
+ tmp_adata.shape[0],
157
+ tmp_adata.shape[0],
158
+ ), "pcorr is a cell by cell matrix"
159
+
160
+ q75, q25 = np.percentile(pcorr, [75, 25])
161
+ ithcna[group] = q75 - q25
162
+
163
+
164
+ ithcna_obs = np.empty(adata.shape[0])
165
+ for group in groups:
166
+ ithcna_obs[adata.obs[groupby] == group] = ithcna[group]
167
+ adata.obs[key_added] = ithcna_obs