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.
@@ -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
@@ -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,6 @@
1
+ """
2
+ risk/stats/permutation
3
+ ~~~~~~~~~~~~~~~~~~~~~~
4
+ """
5
+
6
+ from .permutation import compute_permutation_test
@@ -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
- neighborhood_score = np.dot(neighborhoods_matrix, annotation_matrix)
26
- return neighborhood_score
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
- stdev = np.sqrt(variance)
53
- return stdev
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
- return z_scores
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
+ }