risk-network 0.0.4b2__py3-none-any.whl → 0.0.5b0__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.
- risk/__init__.py +2 -5
- risk/annotations/annotations.py +1 -1
- risk/neighborhoods/neighborhoods.py +5 -1
- risk/network/geometry.py +2 -2
- risk/network/io.py +45 -30
- risk/network/plot.py +55 -10
- risk/risk.py +171 -19
- risk/stats/__init__.py +4 -1
- risk/stats/fisher_exact.py +132 -0
- risk/stats/hypergeom.py +131 -0
- risk/stats/permutation/__init__.py +6 -0
- risk/stats/permutation/permutation.py +212 -0
- risk/stats/{permutation.py → permutation/test_functions.py} +12 -39
- risk/stats/stats.py +1 -212
- {risk_network-0.0.4b2.dist-info → risk_network-0.0.5b0.dist-info}/METADATA +2 -2
- risk_network-0.0.5b0.dist-info/RECORD +30 -0
- {risk_network-0.0.4b2.dist-info → risk_network-0.0.5b0.dist-info}/WHEEL +1 -1
- risk_network-0.0.4b2.dist-info/RECORD +0 -26
- {risk_network-0.0.4b2.dist-info → risk_network-0.0.5b0.dist-info}/LICENSE +0 -0
- {risk_network-0.0.4b2.dist-info → risk_network-0.0.5b0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,132 @@
|
|
1
|
+
"""
|
2
|
+
risk/stats/fisher_exact
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~~~
|
4
|
+
"""
|
5
|
+
|
6
|
+
from multiprocessing import get_context, Manager
|
7
|
+
from tqdm import tqdm
|
8
|
+
from typing import Any, Dict
|
9
|
+
|
10
|
+
import numpy as np
|
11
|
+
from scipy.stats import fisher_exact
|
12
|
+
|
13
|
+
|
14
|
+
def compute_fisher_exact_test(
|
15
|
+
neighborhoods: np.ndarray,
|
16
|
+
annotations: np.ndarray,
|
17
|
+
max_workers: int = 4,
|
18
|
+
) -> Dict[str, Any]:
|
19
|
+
"""Compute Fisher's exact test for enrichment and depletion in neighborhoods.
|
20
|
+
|
21
|
+
Args:
|
22
|
+
neighborhoods (np.ndarray): Binary matrix representing neighborhoods.
|
23
|
+
annotations (np.ndarray): Binary matrix representing annotations.
|
24
|
+
max_workers (int, optional): Number of workers for multiprocessing. Defaults to 4.
|
25
|
+
|
26
|
+
Returns:
|
27
|
+
dict: Dictionary containing depletion and enrichment p-values.
|
28
|
+
"""
|
29
|
+
# Ensure that the matrices are binary (boolean) and free of NaN values
|
30
|
+
neighborhoods = neighborhoods.astype(bool) # Convert to boolean
|
31
|
+
annotations = annotations.astype(bool) # Convert to boolean
|
32
|
+
|
33
|
+
# Initialize the process of calculating p-values using multiprocessing
|
34
|
+
ctx = get_context("spawn")
|
35
|
+
manager = Manager()
|
36
|
+
progress_counter = manager.Value("i", 0)
|
37
|
+
total_tasks = neighborhoods.shape[1] * annotations.shape[1]
|
38
|
+
|
39
|
+
# Calculate the workload per worker
|
40
|
+
chunk_size = total_tasks // max_workers
|
41
|
+
remainder = total_tasks % max_workers
|
42
|
+
|
43
|
+
# Execute the Fisher's exact test using multiprocessing
|
44
|
+
with ctx.Pool(max_workers) as pool:
|
45
|
+
with tqdm(total=total_tasks, desc="Total progress", position=0) as progress:
|
46
|
+
params_list = []
|
47
|
+
start_idx = 0
|
48
|
+
for i in range(max_workers):
|
49
|
+
end_idx = start_idx + chunk_size + (1 if i < remainder else 0)
|
50
|
+
params_list.append(
|
51
|
+
(neighborhoods, annotations, start_idx, end_idx, progress_counter)
|
52
|
+
)
|
53
|
+
start_idx = end_idx
|
54
|
+
|
55
|
+
# Start the Fisher's exact test process in parallel
|
56
|
+
results = pool.starmap_async(_fisher_exact_process_subset, params_list, chunksize=1)
|
57
|
+
|
58
|
+
# Update progress bar based on progress_counter
|
59
|
+
while not results.ready():
|
60
|
+
progress.update(progress_counter.value - progress.n)
|
61
|
+
results.wait(0.05) # Wait for 50ms
|
62
|
+
# Ensure progress bar reaches 100%
|
63
|
+
progress.update(total_tasks - progress.n)
|
64
|
+
|
65
|
+
# Accumulate results from each worker
|
66
|
+
depletion_pvals, enrichment_pvals = [], []
|
67
|
+
for dp, ep in results.get():
|
68
|
+
depletion_pvals.extend(dp)
|
69
|
+
enrichment_pvals.extend(ep)
|
70
|
+
|
71
|
+
# Reshape the results back into arrays with the appropriate dimensions
|
72
|
+
depletion_pvals = np.array(depletion_pvals).reshape(
|
73
|
+
neighborhoods.shape[1], annotations.shape[1]
|
74
|
+
)
|
75
|
+
enrichment_pvals = np.array(enrichment_pvals).reshape(
|
76
|
+
neighborhoods.shape[1], annotations.shape[1]
|
77
|
+
)
|
78
|
+
|
79
|
+
return {
|
80
|
+
"depletion_pvals": depletion_pvals,
|
81
|
+
"enrichment_pvals": enrichment_pvals,
|
82
|
+
}
|
83
|
+
|
84
|
+
|
85
|
+
def _fisher_exact_process_subset(
|
86
|
+
neighborhoods: np.ndarray,
|
87
|
+
annotations: np.ndarray,
|
88
|
+
start_idx: int,
|
89
|
+
end_idx: int,
|
90
|
+
progress_counter,
|
91
|
+
) -> tuple:
|
92
|
+
"""Process a subset of neighborhoods using Fisher's exact test.
|
93
|
+
|
94
|
+
Args:
|
95
|
+
neighborhoods (np.ndarray): The full neighborhood matrix.
|
96
|
+
annotations (np.ndarray): The annotation matrix.
|
97
|
+
start_idx (int): Starting index of the neighborhood-annotation pairs to process.
|
98
|
+
end_idx (int): Ending index of the neighborhood-annotation pairs to process.
|
99
|
+
progress_counter: Shared counter for tracking progress.
|
100
|
+
|
101
|
+
Returns:
|
102
|
+
tuple: Local p-values for depletion and enrichment.
|
103
|
+
"""
|
104
|
+
# Initialize lists to store p-values for depletion and enrichment
|
105
|
+
depletion_pvals = []
|
106
|
+
enrichment_pvals = []
|
107
|
+
# Process the subset of tasks assigned to this worker
|
108
|
+
for idx in range(start_idx, end_idx):
|
109
|
+
i = idx // annotations.shape[1] # Neighborhood index
|
110
|
+
j = idx % annotations.shape[1] # Annotation index
|
111
|
+
|
112
|
+
neighborhood = neighborhoods[:, i]
|
113
|
+
annotation = annotations[:, j]
|
114
|
+
|
115
|
+
# Calculate the contingency table values
|
116
|
+
TP = np.sum(neighborhood & annotation)
|
117
|
+
FP = np.sum(neighborhood & ~annotation)
|
118
|
+
FN = np.sum(~neighborhood & annotation)
|
119
|
+
TN = np.sum(~neighborhood & ~annotation)
|
120
|
+
table = np.array([[TP, FP], [FN, TN]])
|
121
|
+
|
122
|
+
# Perform Fisher's exact test for depletion (alternative='less')
|
123
|
+
_, p_value_depletion = fisher_exact(table, alternative="less")
|
124
|
+
depletion_pvals.append(p_value_depletion)
|
125
|
+
# Perform Fisher's exact test for enrichment (alternative='greater')
|
126
|
+
_, p_value_enrichment = fisher_exact(table, alternative="greater")
|
127
|
+
enrichment_pvals.append(p_value_enrichment)
|
128
|
+
|
129
|
+
# Update the shared progress counter
|
130
|
+
progress_counter.value += 1
|
131
|
+
|
132
|
+
return depletion_pvals, enrichment_pvals
|
risk/stats/hypergeom.py
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
"""
|
2
|
+
risk/stats/hypergeom
|
3
|
+
~~~~~~~~~~~~~~~~~~~~
|
4
|
+
"""
|
5
|
+
|
6
|
+
from multiprocessing import get_context, Manager
|
7
|
+
from tqdm import tqdm
|
8
|
+
from typing import Any, Dict
|
9
|
+
|
10
|
+
import numpy as np
|
11
|
+
from scipy.stats import hypergeom
|
12
|
+
|
13
|
+
|
14
|
+
def compute_hypergeom_test(
|
15
|
+
neighborhoods: np.ndarray,
|
16
|
+
annotations: np.ndarray,
|
17
|
+
max_workers: int = 4,
|
18
|
+
) -> Dict[str, Any]:
|
19
|
+
"""Compute hypergeometric test for enrichment and depletion in neighborhoods.
|
20
|
+
|
21
|
+
Args:
|
22
|
+
neighborhoods (np.ndarray): Binary matrix representing neighborhoods.
|
23
|
+
annotations (np.ndarray): Binary matrix representing annotations.
|
24
|
+
max_workers (int, optional): Number of workers for multiprocessing. Defaults to 4.
|
25
|
+
|
26
|
+
Returns:
|
27
|
+
dict: Dictionary containing depletion and enrichment p-values.
|
28
|
+
"""
|
29
|
+
# Ensure that the matrices are binary (boolean) and free of NaN values
|
30
|
+
neighborhoods = neighborhoods.astype(bool) # Convert to boolean
|
31
|
+
annotations = annotations.astype(bool) # Convert to boolean
|
32
|
+
|
33
|
+
# Initialize the process of calculating p-values using multiprocessing
|
34
|
+
ctx = get_context("spawn")
|
35
|
+
manager = Manager()
|
36
|
+
progress_counter = manager.Value("i", 0)
|
37
|
+
total_tasks = neighborhoods.shape[1] * annotations.shape[1]
|
38
|
+
|
39
|
+
# Calculate the workload per worker
|
40
|
+
chunk_size = total_tasks // max_workers
|
41
|
+
remainder = total_tasks % max_workers
|
42
|
+
|
43
|
+
# Execute the hypergeometric test using multiprocessing
|
44
|
+
with ctx.Pool(max_workers) as pool:
|
45
|
+
with tqdm(total=total_tasks, desc="Total progress", position=0) as progress:
|
46
|
+
params_list = []
|
47
|
+
start_idx = 0
|
48
|
+
for i in range(max_workers):
|
49
|
+
end_idx = start_idx + chunk_size + (1 if i < remainder else 0)
|
50
|
+
params_list.append(
|
51
|
+
(neighborhoods, annotations, start_idx, end_idx, progress_counter)
|
52
|
+
)
|
53
|
+
start_idx = end_idx
|
54
|
+
|
55
|
+
# Start the hypergeometric test process in parallel
|
56
|
+
results = pool.starmap_async(_hypergeom_process_subset, params_list, chunksize=1)
|
57
|
+
|
58
|
+
# Update progress bar based on progress_counter
|
59
|
+
while not results.ready():
|
60
|
+
progress.update(progress_counter.value - progress.n)
|
61
|
+
results.wait(0.05) # Wait for 50ms
|
62
|
+
# Ensure progress bar reaches 100%
|
63
|
+
progress.update(total_tasks - progress.n)
|
64
|
+
|
65
|
+
# Accumulate results from each worker
|
66
|
+
depletion_pvals, enrichment_pvals = [], []
|
67
|
+
for dp, ep in results.get():
|
68
|
+
depletion_pvals.extend(dp)
|
69
|
+
enrichment_pvals.extend(ep)
|
70
|
+
|
71
|
+
# Reshape the results back into arrays with the appropriate dimensions
|
72
|
+
depletion_pvals = np.array(depletion_pvals).reshape(
|
73
|
+
neighborhoods.shape[1], annotations.shape[1]
|
74
|
+
)
|
75
|
+
enrichment_pvals = np.array(enrichment_pvals).reshape(
|
76
|
+
neighborhoods.shape[1], annotations.shape[1]
|
77
|
+
)
|
78
|
+
|
79
|
+
return {
|
80
|
+
"depletion_pvals": depletion_pvals,
|
81
|
+
"enrichment_pvals": enrichment_pvals,
|
82
|
+
}
|
83
|
+
|
84
|
+
|
85
|
+
def _hypergeom_process_subset(
|
86
|
+
neighborhoods: np.ndarray,
|
87
|
+
annotations: np.ndarray,
|
88
|
+
start_idx: int,
|
89
|
+
end_idx: int,
|
90
|
+
progress_counter,
|
91
|
+
) -> tuple:
|
92
|
+
"""Process a subset of neighborhoods using the hypergeometric test.
|
93
|
+
|
94
|
+
Args:
|
95
|
+
neighborhoods (np.ndarray): The full neighborhood matrix.
|
96
|
+
annotations (np.ndarray): The annotation matrix.
|
97
|
+
start_idx (int): Starting index of the neighborhood-annotation pairs to process.
|
98
|
+
end_idx (int): Ending index of the neighborhood-annotation pairs to process.
|
99
|
+
progress_counter: Shared counter for tracking progress.
|
100
|
+
|
101
|
+
Returns:
|
102
|
+
tuple: Local p-values for depletion and enrichment.
|
103
|
+
"""
|
104
|
+
# Initialize lists to store p-values for depletion and enrichment
|
105
|
+
depletion_pvals = []
|
106
|
+
enrichment_pvals = []
|
107
|
+
# Process the subset of tasks assigned to this worker
|
108
|
+
for idx in range(start_idx, end_idx):
|
109
|
+
i = idx // annotations.shape[1] # Neighborhood index
|
110
|
+
j = idx % annotations.shape[1] # Annotation index
|
111
|
+
|
112
|
+
neighborhood = neighborhoods[:, i]
|
113
|
+
annotation = annotations[:, j]
|
114
|
+
|
115
|
+
# Calculate the required values for the hypergeometric test
|
116
|
+
M = annotations.shape[0] # Total number of items (population size)
|
117
|
+
n = np.sum(annotation) # Total number of successes in population
|
118
|
+
N = np.sum(neighborhood) # Total number of draws (sample size)
|
119
|
+
k = np.sum(neighborhood & annotation) # Number of successes in sample
|
120
|
+
|
121
|
+
# Perform hypergeometric test for depletion
|
122
|
+
p_value_depletion = hypergeom.cdf(k, M, n, N)
|
123
|
+
depletion_pvals.append(p_value_depletion)
|
124
|
+
# Perform hypergeometric test for enrichment
|
125
|
+
p_value_enrichment = hypergeom.sf(k - 1, M, n, N)
|
126
|
+
enrichment_pvals.append(p_value_enrichment)
|
127
|
+
|
128
|
+
# Update the shared progress counter
|
129
|
+
progress_counter.value += 1
|
130
|
+
|
131
|
+
return depletion_pvals, enrichment_pvals
|
@@ -0,0 +1,212 @@
|
|
1
|
+
"""
|
2
|
+
risk/stats/permutation/permutation
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
4
|
+
"""
|
5
|
+
|
6
|
+
from multiprocessing import get_context, Manager
|
7
|
+
from tqdm import tqdm
|
8
|
+
from typing import Any, Callable, Dict
|
9
|
+
|
10
|
+
import numpy as np
|
11
|
+
from threadpoolctl import threadpool_limits
|
12
|
+
|
13
|
+
from risk.stats.permutation.test_functions import DISPATCH_TEST_FUNCTIONS
|
14
|
+
|
15
|
+
|
16
|
+
def compute_permutation_test(
|
17
|
+
neighborhoods: np.ndarray,
|
18
|
+
annotations: np.ndarray,
|
19
|
+
score_metric: str = "sum",
|
20
|
+
null_distribution: str = "network",
|
21
|
+
num_permutations: int = 1000,
|
22
|
+
random_seed: int = 888,
|
23
|
+
max_workers: int = 1,
|
24
|
+
) -> Dict[str, Any]:
|
25
|
+
"""Compute permutation test for enrichment and depletion in neighborhoods.
|
26
|
+
|
27
|
+
Args:
|
28
|
+
neighborhoods (np.ndarray): Binary matrix representing neighborhoods.
|
29
|
+
annotations (np.ndarray): Binary matrix representing annotations.
|
30
|
+
score_metric (str, optional): Metric to use for scoring ('sum', 'mean', etc.). Defaults to "sum".
|
31
|
+
null_distribution (str, optional): Type of null distribution ('network' or other). Defaults to "network".
|
32
|
+
num_permutations (int, optional): Number of permutations to run. Defaults to 1000.
|
33
|
+
random_seed (int, optional): Seed for random number generation. Defaults to 888.
|
34
|
+
max_workers (int, optional): Number of workers for multiprocessing. Defaults to 1.
|
35
|
+
|
36
|
+
Returns:
|
37
|
+
dict: Dictionary containing depletion and enrichment p-values.
|
38
|
+
"""
|
39
|
+
# Ensure that the matrices are in the correct format and free of NaN values
|
40
|
+
neighborhoods = neighborhoods.astype(np.float32)
|
41
|
+
annotations = annotations.astype(np.float32)
|
42
|
+
# Retrieve the appropriate neighborhood score function based on the metric
|
43
|
+
neighborhood_score_func = DISPATCH_TEST_FUNCTIONS[score_metric]
|
44
|
+
|
45
|
+
# Run the permutation test to calculate depletion and enrichment counts
|
46
|
+
counts_depletion, counts_enrichment = _run_permutation_test(
|
47
|
+
neighborhoods=neighborhoods,
|
48
|
+
annotations=annotations,
|
49
|
+
neighborhood_score_func=neighborhood_score_func,
|
50
|
+
null_distribution=null_distribution,
|
51
|
+
num_permutations=num_permutations,
|
52
|
+
random_seed=random_seed,
|
53
|
+
max_workers=max_workers,
|
54
|
+
)
|
55
|
+
# Compute p-values for depletion and enrichment
|
56
|
+
# If counts are 0, set p-value to 1/num_permutations to avoid zero p-values
|
57
|
+
depletion_pvals = np.maximum(counts_depletion, 1) / num_permutations
|
58
|
+
enrichment_pvals = np.maximum(counts_enrichment, 1) / num_permutations
|
59
|
+
|
60
|
+
return {
|
61
|
+
"depletion_pvals": depletion_pvals,
|
62
|
+
"enrichment_pvals": enrichment_pvals,
|
63
|
+
}
|
64
|
+
|
65
|
+
|
66
|
+
def _run_permutation_test(
|
67
|
+
neighborhoods: np.ndarray,
|
68
|
+
annotations: np.ndarray,
|
69
|
+
neighborhood_score_func: Callable,
|
70
|
+
null_distribution: str = "network",
|
71
|
+
num_permutations: int = 1000,
|
72
|
+
random_seed: int = 888,
|
73
|
+
max_workers: int = 4,
|
74
|
+
) -> tuple:
|
75
|
+
"""Run a permutation test to calculate enrichment and depletion counts.
|
76
|
+
|
77
|
+
Args:
|
78
|
+
neighborhoods (np.ndarray): The neighborhood matrix.
|
79
|
+
annotations (np.ndarray): The annotation matrix.
|
80
|
+
neighborhood_score_func (Callable): Function to calculate neighborhood scores.
|
81
|
+
null_distribution (str, optional): Type of null distribution. Defaults to "network".
|
82
|
+
num_permutations (int, optional): Number of permutations. Defaults to 1000.
|
83
|
+
random_seed (int, optional): Seed for random number generation. Defaults to 888.
|
84
|
+
max_workers (int, optional): Number of workers for multiprocessing. Defaults to 4.
|
85
|
+
|
86
|
+
Returns:
|
87
|
+
tuple: Depletion and enrichment counts.
|
88
|
+
"""
|
89
|
+
# Initialize the RNG for reproducibility
|
90
|
+
rng = np.random.default_rng(seed=random_seed)
|
91
|
+
# Determine the indices to use based on the null distribution type
|
92
|
+
if null_distribution == "network":
|
93
|
+
idxs = range(annotations.shape[0])
|
94
|
+
else:
|
95
|
+
idxs = np.nonzero(np.sum(~np.isnan(annotations), axis=1))[0]
|
96
|
+
|
97
|
+
# Replace NaNs with zeros in the annotations matrix
|
98
|
+
annotations[np.isnan(annotations)] = 0
|
99
|
+
annotation_matrix_obsv = annotations[idxs]
|
100
|
+
neighborhoods_matrix_obsv = neighborhoods.T[idxs].T
|
101
|
+
# Calculate observed neighborhood scores
|
102
|
+
with np.errstate(invalid="ignore", divide="ignore"):
|
103
|
+
observed_neighborhood_scores = neighborhood_score_func(
|
104
|
+
neighborhoods_matrix_obsv, annotation_matrix_obsv
|
105
|
+
)
|
106
|
+
|
107
|
+
# Initialize count matrices for depletion and enrichment
|
108
|
+
counts_depletion = np.zeros(observed_neighborhood_scores.shape)
|
109
|
+
counts_enrichment = np.zeros(observed_neighborhood_scores.shape)
|
110
|
+
|
111
|
+
# Determine the number of permutations to run in each worker process
|
112
|
+
subset_size = num_permutations // max_workers
|
113
|
+
remainder = num_permutations % max_workers
|
114
|
+
|
115
|
+
# Use the spawn context for creating a new multiprocessing pool
|
116
|
+
ctx = get_context("spawn")
|
117
|
+
manager = Manager()
|
118
|
+
progress_counter = manager.Value("i", 0)
|
119
|
+
total_progress = num_permutations
|
120
|
+
|
121
|
+
# Execute the permutation test using multiprocessing
|
122
|
+
with ctx.Pool(max_workers) as pool:
|
123
|
+
with tqdm(total=total_progress, desc="Total progress", position=0) as progress:
|
124
|
+
# Prepare parameters for multiprocessing
|
125
|
+
params_list = [
|
126
|
+
(
|
127
|
+
annotations,
|
128
|
+
np.array(idxs),
|
129
|
+
neighborhoods_matrix_obsv,
|
130
|
+
observed_neighborhood_scores,
|
131
|
+
neighborhood_score_func,
|
132
|
+
subset_size + (1 if i < remainder else 0),
|
133
|
+
progress_counter,
|
134
|
+
rng, # Pass the RNG to each process
|
135
|
+
)
|
136
|
+
for i in range(max_workers)
|
137
|
+
]
|
138
|
+
|
139
|
+
# Start the permutation process in parallel
|
140
|
+
results = pool.starmap_async(_permutation_process_subset, params_list, chunksize=1)
|
141
|
+
|
142
|
+
# Update progress bar based on progress_counter
|
143
|
+
# NOTE: Waiting for results to be ready while updating progress bar gives a big improvement
|
144
|
+
# in performance, especially for large number of permutations and workers
|
145
|
+
while not results.ready():
|
146
|
+
progress.update(progress_counter.value - progress.n)
|
147
|
+
results.wait(0.05) # Wait for 50ms
|
148
|
+
# Ensure progress bar reaches 100%
|
149
|
+
progress.update(total_progress - progress.n)
|
150
|
+
|
151
|
+
# Accumulate results from each worker
|
152
|
+
for local_counts_depletion, local_counts_enrichment in results.get():
|
153
|
+
counts_depletion = np.add(counts_depletion, local_counts_depletion)
|
154
|
+
counts_enrichment = np.add(counts_enrichment, local_counts_enrichment)
|
155
|
+
|
156
|
+
return counts_depletion, counts_enrichment
|
157
|
+
|
158
|
+
|
159
|
+
def _permutation_process_subset(
|
160
|
+
annotation_matrix: np.ndarray,
|
161
|
+
idxs: np.ndarray,
|
162
|
+
neighborhoods_matrix_obsv: np.ndarray,
|
163
|
+
observed_neighborhood_scores: np.ndarray,
|
164
|
+
neighborhood_score_func: Callable,
|
165
|
+
subset_size: int,
|
166
|
+
progress_counter,
|
167
|
+
rng: np.random.Generator,
|
168
|
+
) -> tuple:
|
169
|
+
"""Process a subset of permutations for the permutation test.
|
170
|
+
|
171
|
+
Args:
|
172
|
+
annotation_matrix (np.ndarray): The annotation matrix.
|
173
|
+
idxs (np.ndarray): Indices of valid rows in the matrix.
|
174
|
+
neighborhoods_matrix_obsv (np.ndarray): Observed neighborhoods matrix.
|
175
|
+
observed_neighborhood_scores (np.ndarray): Observed neighborhood scores.
|
176
|
+
neighborhood_score_func (Callable): Function to calculate neighborhood scores.
|
177
|
+
subset_size (int): Number of permutations to run in this subset.
|
178
|
+
progress_counter: Shared counter for tracking progress.
|
179
|
+
rng (np.random.Generator): Random number generator object.
|
180
|
+
|
181
|
+
Returns:
|
182
|
+
tuple: Local counts of depletion and enrichment.
|
183
|
+
"""
|
184
|
+
# Initialize local count matrices for this worker
|
185
|
+
local_counts_depletion = np.zeros(observed_neighborhood_scores.shape)
|
186
|
+
local_counts_enrichment = np.zeros(observed_neighborhood_scores.shape)
|
187
|
+
# NOTE: Limit the number of threads used by NumPy's BLAS implementation to 1.
|
188
|
+
# This can help prevent oversubscription of CPU resources during multiprocessing,
|
189
|
+
# ensuring that each process doesn't use more than one CPU core.
|
190
|
+
with threadpool_limits(limits=1, user_api="blas"):
|
191
|
+
for _ in range(subset_size):
|
192
|
+
# Permute the annotation matrix using the RNG
|
193
|
+
annotation_matrix_permut = annotation_matrix[rng.permutation(idxs)]
|
194
|
+
# Calculate permuted neighborhood scores
|
195
|
+
with np.errstate(invalid="ignore", divide="ignore"):
|
196
|
+
permuted_neighborhood_scores = neighborhood_score_func(
|
197
|
+
neighborhoods_matrix_obsv, annotation_matrix_permut
|
198
|
+
)
|
199
|
+
|
200
|
+
# Update local depletion and enrichment counts based on permuted scores
|
201
|
+
local_counts_depletion = np.add(
|
202
|
+
local_counts_depletion, permuted_neighborhood_scores <= observed_neighborhood_scores
|
203
|
+
)
|
204
|
+
local_counts_enrichment = np.add(
|
205
|
+
local_counts_enrichment,
|
206
|
+
permuted_neighborhood_scores >= observed_neighborhood_scores,
|
207
|
+
)
|
208
|
+
|
209
|
+
# Update the shared progress counter
|
210
|
+
progress_counter.value += 1
|
211
|
+
|
212
|
+
return local_counts_depletion, local_counts_enrichment
|
@@ -1,12 +1,13 @@
|
|
1
1
|
"""
|
2
|
-
risk/stats/permutation
|
3
|
-
|
2
|
+
risk/stats/permutation/test_function
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
4
4
|
"""
|
5
5
|
|
6
6
|
import numpy as np
|
7
7
|
|
8
8
|
# Note: Cython optimizations provided minimal performance benefits.
|
9
9
|
# The final version with Cython is archived in the `cython_permutation` branch.
|
10
|
+
# DISPATCH_TEST_FUNCTIONS can be found at the end of the file.
|
10
11
|
|
11
12
|
|
12
13
|
def compute_neighborhood_score_by_sum(
|
@@ -22,8 +23,8 @@ def compute_neighborhood_score_by_sum(
|
|
22
23
|
np.ndarray: Sum of attribute values for each neighborhood.
|
23
24
|
"""
|
24
25
|
# Calculate the neighborhood score as the dot product of neighborhoods and annotations
|
25
|
-
|
26
|
-
return
|
26
|
+
neighborhood_sum = np.dot(neighborhoods_matrix, annotation_matrix)
|
27
|
+
return neighborhood_sum
|
27
28
|
|
28
29
|
|
29
30
|
def compute_neighborhood_score_by_stdev(
|
@@ -49,40 +50,12 @@ def compute_neighborhood_score_by_stdev(
|
|
49
50
|
# Calculate variance as EXX - M^2
|
50
51
|
variance = EXX - M**2
|
51
52
|
# Compute the standard deviation as the square root of the variance
|
52
|
-
|
53
|
-
return
|
54
|
-
|
55
|
-
|
56
|
-
def compute_neighborhood_score_by_z_score(
|
57
|
-
neighborhoods_matrix: np.ndarray, annotation_matrix: np.ndarray
|
58
|
-
) -> np.ndarray:
|
59
|
-
"""Compute Z-scores for neighborhood scores.
|
60
|
-
|
61
|
-
Args:
|
62
|
-
neighborhoods_matrix (np.ndarray): Binary matrix representing neighborhoods.
|
63
|
-
annotation_matrix (np.ndarray): Matrix representing annotation values.
|
53
|
+
neighborhood_stdev = np.sqrt(variance)
|
54
|
+
return neighborhood_stdev
|
64
55
|
|
65
|
-
Returns:
|
66
|
-
np.ndarray: Z-scores for each neighborhood.
|
67
|
-
"""
|
68
|
-
# Calculate the neighborhood score as the dot product of neighborhoods and annotations
|
69
|
-
neighborhood_score = np.dot(neighborhoods_matrix, annotation_matrix)
|
70
|
-
# Calculate the number of elements in each neighborhood
|
71
|
-
N = np.dot(
|
72
|
-
neighborhoods_matrix, np.ones(annotation_matrix.shape[1], dtype=annotation_matrix.dtype)
|
73
|
-
)
|
74
|
-
# Compute the mean of the neighborhood scores
|
75
|
-
M = neighborhood_score / N
|
76
|
-
# Compute the mean of squares (EXX)
|
77
|
-
EXX = np.dot(neighborhoods_matrix, annotation_matrix**2) / N
|
78
|
-
# Calculate the standard deviation for each neighborhood
|
79
|
-
variance = EXX - M**2
|
80
|
-
std = np.sqrt(variance)
|
81
|
-
# Calculate Z-scores, handling cases where std is 0 or N is less than 3
|
82
|
-
with np.errstate(divide="ignore", invalid="ignore"):
|
83
|
-
z_scores = M / std
|
84
|
-
z_scores[(std == 0) | (N < 3)] = (
|
85
|
-
np.nan
|
86
|
-
) # Handle division by zero and apply minimum threshold
|
87
56
|
|
88
|
-
|
57
|
+
# Dictionary to dispatch statistical test functions based on the score metric
|
58
|
+
DISPATCH_TEST_FUNCTIONS = {
|
59
|
+
"sum": compute_neighborhood_score_by_sum,
|
60
|
+
"stdev": compute_neighborhood_score_by_stdev,
|
61
|
+
}
|