nimare 0.4.2rc4__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.
Files changed (119) hide show
  1. benchmarks/__init__.py +0 -0
  2. benchmarks/bench_cbma.py +57 -0
  3. nimare/__init__.py +45 -0
  4. nimare/_version.py +21 -0
  5. nimare/annotate/__init__.py +21 -0
  6. nimare/annotate/cogat.py +213 -0
  7. nimare/annotate/gclda.py +924 -0
  8. nimare/annotate/lda.py +147 -0
  9. nimare/annotate/text.py +75 -0
  10. nimare/annotate/utils.py +87 -0
  11. nimare/base.py +217 -0
  12. nimare/cli.py +124 -0
  13. nimare/correct.py +462 -0
  14. nimare/dataset.py +685 -0
  15. nimare/decode/__init__.py +33 -0
  16. nimare/decode/base.py +115 -0
  17. nimare/decode/continuous.py +462 -0
  18. nimare/decode/discrete.py +753 -0
  19. nimare/decode/encode.py +110 -0
  20. nimare/decode/utils.py +44 -0
  21. nimare/diagnostics.py +510 -0
  22. nimare/estimator.py +139 -0
  23. nimare/extract/__init__.py +19 -0
  24. nimare/extract/extract.py +466 -0
  25. nimare/extract/utils.py +295 -0
  26. nimare/generate.py +331 -0
  27. nimare/io.py +635 -0
  28. nimare/meta/__init__.py +39 -0
  29. nimare/meta/cbma/__init__.py +6 -0
  30. nimare/meta/cbma/ale.py +951 -0
  31. nimare/meta/cbma/base.py +947 -0
  32. nimare/meta/cbma/mkda.py +1361 -0
  33. nimare/meta/cbmr.py +970 -0
  34. nimare/meta/ibma.py +1683 -0
  35. nimare/meta/kernel.py +501 -0
  36. nimare/meta/models.py +1199 -0
  37. nimare/meta/utils.py +494 -0
  38. nimare/nimads.py +492 -0
  39. nimare/reports/__init__.py +24 -0
  40. nimare/reports/base.py +664 -0
  41. nimare/reports/default.yml +123 -0
  42. nimare/reports/figures.py +651 -0
  43. nimare/reports/report.tpl +160 -0
  44. nimare/resources/__init__.py +1 -0
  45. nimare/resources/atlases/Harvard-Oxford-LICENSE +93 -0
  46. nimare/resources/atlases/HarvardOxford-cort-maxprob-thr25-2mm.nii.gz +0 -0
  47. nimare/resources/database_file_manifest.json +142 -0
  48. nimare/resources/english_spellings.csv +1738 -0
  49. nimare/resources/filenames.json +32 -0
  50. nimare/resources/neurosynth_laird_studies.json +58773 -0
  51. nimare/resources/neurosynth_stoplist.txt +396 -0
  52. nimare/resources/nidm_pain_dset.json +1349 -0
  53. nimare/resources/references.bib +541 -0
  54. nimare/resources/semantic_knowledge_children.txt +325 -0
  55. nimare/resources/semantic_relatedness_children.txt +249 -0
  56. nimare/resources/templates/MNI152_2x2x2_brainmask.nii.gz +0 -0
  57. nimare/resources/templates/tpl-MNI152NLin6Asym_res-01_T1w.nii.gz +0 -0
  58. nimare/resources/templates/tpl-MNI152NLin6Asym_res-01_desc-brain_mask.nii.gz +0 -0
  59. nimare/resources/templates/tpl-MNI152NLin6Asym_res-02_T1w.nii.gz +0 -0
  60. nimare/resources/templates/tpl-MNI152NLin6Asym_res-02_desc-brain_mask.nii.gz +0 -0
  61. nimare/results.py +225 -0
  62. nimare/stats.py +276 -0
  63. nimare/tests/__init__.py +1 -0
  64. nimare/tests/conftest.py +229 -0
  65. nimare/tests/data/amygdala_roi.nii.gz +0 -0
  66. nimare/tests/data/data-neurosynth_version-7_coordinates.tsv.gz +0 -0
  67. nimare/tests/data/data-neurosynth_version-7_metadata.tsv.gz +0 -0
  68. nimare/tests/data/data-neurosynth_version-7_vocab-terms_source-abstract_type-tfidf_features.npz +0 -0
  69. nimare/tests/data/data-neurosynth_version-7_vocab-terms_vocabulary.txt +100 -0
  70. nimare/tests/data/neurosynth_dset.json +2868 -0
  71. nimare/tests/data/neurosynth_laird_studies.json +58773 -0
  72. nimare/tests/data/nidm_pain_dset.json +1349 -0
  73. nimare/tests/data/nimads_annotation.json +1 -0
  74. nimare/tests/data/nimads_studyset.json +1 -0
  75. nimare/tests/data/test_baseline.txt +2 -0
  76. nimare/tests/data/test_pain_dataset.json +1278 -0
  77. nimare/tests/data/test_pain_dataset_multiple_contrasts.json +1242 -0
  78. nimare/tests/data/test_sleuth_file.txt +18 -0
  79. nimare/tests/data/test_sleuth_file2.txt +10 -0
  80. nimare/tests/data/test_sleuth_file3.txt +5 -0
  81. nimare/tests/data/test_sleuth_file4.txt +5 -0
  82. nimare/tests/data/test_sleuth_file5.txt +5 -0
  83. nimare/tests/test_annotate_cogat.py +32 -0
  84. nimare/tests/test_annotate_gclda.py +86 -0
  85. nimare/tests/test_annotate_lda.py +27 -0
  86. nimare/tests/test_dataset.py +99 -0
  87. nimare/tests/test_decode_continuous.py +132 -0
  88. nimare/tests/test_decode_discrete.py +92 -0
  89. nimare/tests/test_diagnostics.py +168 -0
  90. nimare/tests/test_estimator_performance.py +385 -0
  91. nimare/tests/test_extract.py +46 -0
  92. nimare/tests/test_generate.py +247 -0
  93. nimare/tests/test_io.py +240 -0
  94. nimare/tests/test_meta_ale.py +298 -0
  95. nimare/tests/test_meta_cbmr.py +295 -0
  96. nimare/tests/test_meta_ibma.py +240 -0
  97. nimare/tests/test_meta_kernel.py +209 -0
  98. nimare/tests/test_meta_mkda.py +234 -0
  99. nimare/tests/test_nimads.py +21 -0
  100. nimare/tests/test_reports.py +110 -0
  101. nimare/tests/test_stats.py +101 -0
  102. nimare/tests/test_transforms.py +272 -0
  103. nimare/tests/test_utils.py +200 -0
  104. nimare/tests/test_workflows.py +221 -0
  105. nimare/tests/utils.py +126 -0
  106. nimare/transforms.py +907 -0
  107. nimare/utils.py +1367 -0
  108. nimare/workflows/__init__.py +14 -0
  109. nimare/workflows/base.py +189 -0
  110. nimare/workflows/cbma.py +165 -0
  111. nimare/workflows/ibma.py +108 -0
  112. nimare/workflows/macm.py +77 -0
  113. nimare/workflows/misc.py +65 -0
  114. nimare-0.4.2rc4.dist-info/LICENSE +21 -0
  115. nimare-0.4.2rc4.dist-info/METADATA +124 -0
  116. nimare-0.4.2rc4.dist-info/RECORD +119 -0
  117. nimare-0.4.2rc4.dist-info/WHEEL +5 -0
  118. nimare-0.4.2rc4.dist-info/entry_points.txt +2 -0
  119. nimare-0.4.2rc4.dist-info/top_level.txt +2 -0
@@ -0,0 +1,1361 @@
1
+ """CBMA methods from the multilevel kernel density analysis (MKDA) family."""
2
+
3
+ import logging
4
+
5
+ import nibabel as nib
6
+ import numpy as np
7
+ import sparse
8
+ from joblib import Memory, Parallel, delayed
9
+ from pymare.stats import fdr
10
+ from scipy import ndimage
11
+ from scipy.stats import chi2
12
+ from tqdm.auto import tqdm
13
+
14
+ from nimare import _version
15
+ from nimare.meta.cbma.base import CBMAEstimator, PairwiseCBMAEstimator
16
+ from nimare.meta.kernel import KDAKernel, MKDAKernel
17
+ from nimare.meta.utils import _calculate_cluster_measures
18
+ from nimare.stats import null_to_p, one_way, two_way
19
+ from nimare.transforms import p_to_z
20
+ from nimare.utils import _check_ncores, vox2mm
21
+
22
+ LGR = logging.getLogger(__name__)
23
+ __version__ = _version.get_versions()["version"]
24
+
25
+
26
+ class MKDADensity(CBMAEstimator):
27
+ r"""Multilevel kernel density analysis- Density analysis.
28
+
29
+ The MKDA density method was originally introduced in :footcite:t:`wager2007meta`.
30
+
31
+ .. versionchanged:: 0.2.1
32
+
33
+ - New parameters: ``memory`` and ``memory_level`` for memory caching.
34
+
35
+ .. versionchanged:: 0.0.12
36
+
37
+ - Use a 4D sparse array for modeled activation maps.
38
+
39
+ Parameters
40
+ ----------
41
+ kernel_transformer : :obj:`~nimare.meta.kernel.KernelTransformer`, optional
42
+ Kernel with which to convolve coordinates from dataset. Default is
43
+ :class:`~nimare.meta.kernel.MKDAKernel`.
44
+ null_method : {"approximate", "montecarlo"}, optional
45
+ Method by which to determine uncorrected p-values. The available options are
46
+
47
+ ======================= =================================================================
48
+ "approximate" (default) Build a histogram of summary-statistic values and their
49
+ expected frequencies under the assumption of random spatial
50
+ associated between studies, via a weighted convolution.
51
+
52
+ This method is much faster, but slightly less accurate.
53
+ "montecarlo" Perform a large number of permutations, in which the coordinates
54
+ in the studies are randomly drawn from the Estimator's brain mask
55
+ and the full set of resulting summary-statistic values are
56
+ incorporated into a null distribution (stored as a histogram for
57
+ memory reasons).
58
+
59
+ This method is must slower, and is only slightly more accurate.
60
+ ======================= =================================================================
61
+
62
+ n_iters : int, default=5000
63
+ Number of iterations to use to define the null distribution.
64
+ This is only used if ``null_method=="montecarlo"``.
65
+ Default is 5000.
66
+ memory : instance of :class:`joblib.Memory`, :obj:`str`, or :class:`pathlib.Path`
67
+ Used to cache the output of a function. By default, no caching is done.
68
+ If a :obj:`str` is given, it is the path to the caching directory.
69
+ memory_level : :obj:`int`, default=0
70
+ Rough estimator of the amount of memory used by caching.
71
+ Higher value means more memory for caching. Zero means no caching.
72
+ n_cores : :obj:`int`, optional
73
+ Number of cores to use for parallelization.
74
+ This is only used if ``null_method=="montecarlo"``.
75
+ If <=0, defaults to using all available cores.
76
+ Default is 1.
77
+ **kwargs
78
+ Keyword arguments. Arguments for the kernel_transformer can be assigned
79
+ here, with the prefix '\kernel__' in the variable name.
80
+
81
+ Attributes
82
+ ----------
83
+ masker : :class:`~nilearn.input_data.NiftiMasker` or similar
84
+ Masker object.
85
+ inputs_ : :obj:`dict`
86
+ Inputs to the Estimator. For CBMA estimators, there is only one key: coordinates.
87
+ This is an edited version of the dataset's coordinates DataFrame.
88
+ null_distributions_ : :obj:`dict` of :class:`numpy.ndarray`
89
+ Null distributions for the uncorrected summary-statistic-to-p-value conversion and any
90
+ multiple-comparisons correction methods.
91
+ Entries are added to this attribute if and when the corresponding method is applied.
92
+
93
+ If ``null_method == "approximate"``:
94
+
95
+ - ``histogram_means``: Array of mean value per experiment.
96
+ - ``histogram_bins``: Array of bin centers for the null distribution histogram,
97
+ ranging from zero to the maximum possible summary statistic value for the Dataset.
98
+ - ``histweights_corr-none_method-approximate``: Array of weights for the null
99
+ distribution histogram, with one value for each bin in ``histogram_bins``.
100
+
101
+ If ``null_method == "montecarlo"``:
102
+
103
+ - ``histogram_bins``: Array of bin centers for the null distribution histogram,
104
+ ranging from zero to the maximum possible summary statistic value for the Dataset.
105
+ - ``histweights_corr-none_method-montecarlo``: Array of weights for the null
106
+ distribution histogram, with one value for each bin in ``histogram_bins``.
107
+ These values are derived from the full set of summary statistics from each
108
+ iteration of the Monte Carlo procedure.
109
+ - ``histweights_level-voxel_corr-fwe_method-montecarlo``: Array of weights for the
110
+ voxel-level FWE-correction null distribution, with one value for each bin in
111
+ ``histogram_bins``. These values are derived from the maximum summary statistic
112
+ from each iteration of the Monte Carlo procedure.
113
+
114
+ If :meth:`correct_fwe_montecarlo` is applied:
115
+
116
+ - ``values_level-voxel_corr-fwe_method-montecarlo``: The maximum summary statistic
117
+ value from each Monte Carlo iteration. An array of shape (n_iters,).
118
+ - ``values_desc-size_level-cluster_corr-fwe_method-montecarlo``: The maximum cluster
119
+ size from each Monte Carlo iteration. An array of shape (n_iters,).
120
+ - ``values_desc-mass_level-cluster_corr-fwe_method-montecarlo``: The maximum cluster
121
+ mass from each Monte Carlo iteration. An array of shape (n_iters,).
122
+
123
+ Notes
124
+ -----
125
+ The MKDA density algorithm is also implemented in MATLAB at
126
+ https://github.com/canlab/Canlab_MKDA_MetaAnalysis.
127
+
128
+ Available correction methods: :func:`MKDADensity.correct_fwe_montecarlo`
129
+
130
+ References
131
+ ----------
132
+ .. footbibliography::
133
+ """
134
+
135
+ def __init__(
136
+ self,
137
+ kernel_transformer=MKDAKernel,
138
+ null_method="approximate",
139
+ n_iters=5000,
140
+ memory=Memory(location=None, verbose=0),
141
+ memory_level=0,
142
+ n_cores=1,
143
+ **kwargs,
144
+ ):
145
+ if not (isinstance(kernel_transformer, MKDAKernel) or kernel_transformer == MKDAKernel):
146
+ LGR.warning(
147
+ f"The KernelTransformer being used ({kernel_transformer}) is not optimized "
148
+ f"for the {type(self).__name__} algorithm. "
149
+ "Expect suboptimal performance and beware bugs."
150
+ )
151
+
152
+ # Add kernel transformer attribute and process keyword arguments
153
+ super().__init__(
154
+ kernel_transformer=kernel_transformer,
155
+ memory=memory,
156
+ memory_level=memory_level,
157
+ **kwargs,
158
+ )
159
+ self.null_method = null_method
160
+ self.n_iters = None if null_method == "approximate" else n_iters or 5000
161
+ self.n_cores = _check_ncores(n_cores)
162
+ self.dataset = None
163
+
164
+ def _generate_description(self):
165
+ """Generate a description of the fitted Estimator.
166
+
167
+ Returns
168
+ -------
169
+ str
170
+ Description of the Estimator.
171
+ """
172
+ if self.null_method == "montecarlo":
173
+ null_method_str = (
174
+ "a Monte Carlo-based null distribution, in which dataset coordinates were "
175
+ "randomly drawn from the analysis mask and the full set of ALE values were "
176
+ f"retained, using {self.n_iters} iterations"
177
+ )
178
+ else:
179
+ null_method_str = "an approximate null distribution"
180
+
181
+ description = (
182
+ "A multilevel kernel density (MKDA) meta-analysis \\citep{wager2007meta} was "
183
+ "performed was performed with NiMARE "
184
+ f"{__version__} "
185
+ "(RRID:SCR_017398; \\citealt{Salo2023}), using a(n) "
186
+ f"{self.kernel_transformer.__class__.__name__.replace('Kernel', '')} kernel. "
187
+ f"{self.kernel_transformer._generate_description()} "
188
+ f"Summary statistics (OF values) were converted to p-values using {null_method_str}. "
189
+ f"The input dataset included {self.inputs_['coordinates'].shape[0]} foci from "
190
+ f"{len(self.inputs_['id'])} experiments."
191
+ )
192
+ return description
193
+
194
+ def _compute_weights(self, ma_values):
195
+ """Determine experiment-wise weights per the conventional MKDA approach."""
196
+ # TODO: Incorporate sample-size and inference metadata extraction and
197
+ # merging into df.
198
+ # This will need to be distinct from the kernel_transformer-based kind
199
+ # done in CBMAEstimator._preprocess_input
200
+ ids_df = self.inputs_["coordinates"].groupby("id").first()
201
+
202
+ n_exp = len(ids_df)
203
+
204
+ # Default to unit weighting for missing inference or sample size
205
+ if "inference" not in ids_df.columns:
206
+ ids_df["inference"] = "rfx"
207
+ if "sample_size" not in ids_df.columns:
208
+ ids_df["sample_size"] = 1.0
209
+
210
+ n = ids_df["sample_size"].astype(float).values
211
+ inf = ids_df["inference"].map({"ffx": 0.75, "rfx": 1.0}).values
212
+
213
+ weight_vec = n_exp * ((np.sqrt(n) * inf) / np.sum(np.sqrt(n) * inf))
214
+ weight_vec = weight_vec[:, None]
215
+
216
+ assert weight_vec.shape[0] == ma_values.shape[0]
217
+ return weight_vec
218
+
219
+ def _compute_summarystat_est(self, ma_values):
220
+ ma_values = ma_values.reshape((ma_values.shape[0], -1))
221
+ stat_values = ma_values.T.dot(self.weight_vec_)
222
+
223
+ if isinstance(ma_values, sparse._coo.core.COO):
224
+ # NOTE: This may not work correctly with a non-NiftiMasker.
225
+ mask_data = self.masker.mask_img.get_fdata().astype(bool)
226
+
227
+ stat_values = stat_values[mask_data.reshape(-1)].ravel()
228
+ # This is used by _compute_null_approximate
229
+ self.__n_mask_voxels = stat_values.shape[0]
230
+ else:
231
+ # np.array type is used by _compute_null_reduced_montecarlo
232
+ stat_values = stat_values.ravel()
233
+
234
+ return stat_values
235
+
236
+ def _determine_histogram_bins(self, ma_maps):
237
+ """Determine histogram bins for null distribution methods.
238
+
239
+ Parameters
240
+ ----------
241
+ ma_maps : :obj:`sparse._coo.core.COO`
242
+ MA maps.
243
+ The ma_maps can be a 4d sparse array of MA maps,
244
+
245
+ Notes
246
+ -----
247
+ This method adds two entries to the null_distributions_ dict attribute: "histogram_bins",
248
+ and "histogram_means" only if ``null_method == "approximate"``.
249
+ """
250
+ if not isinstance(ma_maps, sparse._coo.core.COO):
251
+ raise ValueError(f"Unsupported data type '{type(ma_maps)}'")
252
+
253
+ n_exp = ma_maps.shape[0]
254
+ prop_active = np.zeros(n_exp)
255
+ data = ma_maps.data
256
+ coords = ma_maps.coords
257
+ for exp_idx in range(n_exp):
258
+ # The first column of coords is the fourth dimension of the dense array
259
+ study_ma_values = data[coords[0, :] == exp_idx]
260
+
261
+ n_nonzero_voxels = study_ma_values.shape[0]
262
+ n_zero_voxels = self.__n_mask_voxels - n_nonzero_voxels
263
+
264
+ prop_active[exp_idx] = np.mean(np.hstack([study_ma_values, np.zeros(n_zero_voxels)]))
265
+
266
+ self.null_distributions_["histogram_bins"] = np.arange(len(prop_active) + 1, step=1)
267
+
268
+ if self.null_method.startswith("approximate"):
269
+ # To speed things up in _compute_null_approximate, we save the means too,
270
+ self.null_distributions_["histogram_means"] = prop_active
271
+
272
+ def _compute_null_approximate(self, ma_maps):
273
+ """Compute uncorrected null distribution using approximate solution.
274
+
275
+ Parameters
276
+ ----------
277
+ ma_maps
278
+ Modeled activation maps. Unused for this estimator.
279
+
280
+ Notes
281
+ -----
282
+ This method adds one entry to the null_distributions_ dict attribute: "histogram_weights".
283
+ """
284
+ assert "histogram_means" in self.null_distributions_.keys()
285
+
286
+ # MKDA maps are binary, so we only have k + 1 bins in the final
287
+ # histogram, where k is the number of studies. We can analytically
288
+ # compute the null distribution by convolution.
289
+ # prop_active contains the mean value per experiment
290
+ prop_active = self.null_distributions_["histogram_means"]
291
+
292
+ ss_hist = 1.0
293
+ for exp_prop in prop_active:
294
+ ss_hist = np.convolve(ss_hist, [1 - exp_prop, exp_prop])
295
+
296
+ self.null_distributions_["histweights_corr-none_method-approximate"] = ss_hist
297
+
298
+
299
+ class MKDAChi2(PairwiseCBMAEstimator):
300
+ r"""Multilevel kernel density analysis- Chi-square analysis.
301
+
302
+ The MKDA chi-square method was originally introduced in :footcite:t:`wager2007meta`.
303
+
304
+ .. versionchanged:: 0.2.1
305
+
306
+ - Make `prior` parameter default to None, which controls if posterior probabilities
307
+ pFgA, pAgF_prior and pFgA_prior are calculated. This is useful because probability
308
+ maps are difficult to interpret and for speeding up the algorithm.
309
+ - Rename ``consistency`` to ``uniformity`` and ``specificity`` to ``association`` to match
310
+ Neurosynth's terminology
311
+ - New parameters: ``memory`` and ``memory_level`` for memory caching.
312
+
313
+ .. versionchanged:: 0.0.12
314
+
315
+ - Use a 4D sparse array for modeled activation maps.
316
+
317
+ .. versionchanged:: 0.0.8
318
+
319
+ * [REF] Use saved MA maps, when available.
320
+
321
+ Parameters
322
+ ----------
323
+ kernel_transformer : :obj:`~nimare.meta.kernel.KernelTransformer`, optional
324
+ Kernel with which to convolve coordinates from dataset. Default is
325
+ :class:`~nimare.meta.kernel.MKDAKernel`.
326
+ prior : float, default=0.5
327
+ Uniform prior probability of each feature being active in a map in
328
+ the absence of evidence from the map. Default: 0.5
329
+ memory : instance of :class:`joblib.Memory`, :obj:`str`, or :class:`pathlib.Path`
330
+ Used to cache the output of a function. By default, no caching is done.
331
+ If a :obj:`str` is given, it is the path to the caching directory.
332
+ memory_level : :obj:`int`, default=0
333
+ Rough estimator of the amount of memory used by caching.
334
+ Higher value means more memory for caching. Zero means no caching.
335
+ **kwargs
336
+ Keyword arguments. Arguments for the kernel_transformer can be assigned
337
+ here, with the prefix '\kernel__' in the variable name.
338
+
339
+ Attributes
340
+ ----------
341
+ masker : :class:`~nilearn.input_data.NiftiMasker` or similar
342
+ Masker object.
343
+ inputs_ : :obj:`dict`
344
+ Inputs to the Estimator. For CBMA estimators, there is only one key: coordinates.
345
+ This is an edited version of the dataset's coordinates DataFrame.
346
+ null_distributions_ : :obj:`dict` of :class:`numpy.ndarray`
347
+ Null distributions for any multiple-comparisons correction methods.
348
+
349
+ .. important::
350
+ MKDAChi2 does not retain uncorrected summary-statistic-to-p null distributions,
351
+ since the summary statistic in this case is the chi-squared value, which has an
352
+ established null distribution.
353
+
354
+ Entries are added to this attribute if and when the corresponding method is applied.
355
+
356
+ If :meth:`correct_fwe_montecarlo` is applied:
357
+
358
+ - ``values_desc-pAgF_level-voxel_corr-fwe_method-montecarlo``: The maximum
359
+ chi-squared value from the p(A|F) one-way chi-squared test from each Monte Carlo
360
+ iteration. An array of shape (n_iters,).
361
+ - ``values_desc-pAgFsize_level-cluster_corr-fwe_method-montecarlo``: The maximum
362
+ cluster size value from the p(A|F) one-way chi-squared test from each Monte Carlo
363
+ iteration. An array of shape (n_iters,).
364
+ - ``values_desc-pAgFmass_level-cluster_corr-fwe_method-montecarlo``: The maximum
365
+ cluster mass value from the p(A|F) one-way chi-squared test from each Monte Carlo
366
+ iteration. An array of shape (n_iters,).
367
+ - ``values_desc-pFgA_level-voxel_corr-fwe_method-montecarlo``: The maximum
368
+ chi-squared value from the p(F|A) two-way chi-squared test from each Monte Carlo
369
+ iteration. An array of shape (n_iters,).
370
+ - ``values_desc-pFgAsize_level-cluster_corr-fwe_method-montecarlo``: The maximum
371
+ cluster size value from the p(F|A) two-way chi-squared test from each Monte Carlo
372
+ iteration. An array of shape (n_iters,).
373
+ - ``values_desc-pFgAmass_level-cluster_corr-fwe_method-montecarlo``: The maximum
374
+ cluster mass value from the p(F|A) two-way chi-squared test from each Monte Carlo
375
+ iteration. An array of shape (n_iters,).
376
+
377
+ Notes
378
+ -----
379
+ The MKDA Chi-square algorithm was originally implemented as part of the Neurosynth Python
380
+ library (https://github.com/neurosynth/neurosynth).
381
+
382
+ Available correction methods: :meth:`MKDAChi2.correct_fwe_montecarlo`,
383
+ :meth:`MKDAChi2.correct_fdr_indep`.
384
+
385
+ References
386
+ ----------
387
+ .. footbibliography::
388
+ """
389
+
390
+ def __init__(
391
+ self,
392
+ kernel_transformer=MKDAKernel,
393
+ prior=0.5,
394
+ memory=Memory(location=None, verbose=0),
395
+ memory_level=0,
396
+ **kwargs,
397
+ ):
398
+ if not (isinstance(kernel_transformer, MKDAKernel) or kernel_transformer == MKDAKernel):
399
+ LGR.warning(
400
+ f"The KernelTransformer being used ({kernel_transformer}) is not optimized "
401
+ f"for the {type(self).__name__} algorithm. "
402
+ "Expect suboptimal performance and beware bugs."
403
+ )
404
+
405
+ # Add kernel transformer attribute and process keyword arguments
406
+ super().__init__(
407
+ kernel_transformer=kernel_transformer,
408
+ memory=memory,
409
+ memory_level=memory_level,
410
+ **kwargs,
411
+ )
412
+
413
+ self.prior = prior
414
+
415
+ def _generate_description(self):
416
+ description = (
417
+ "A multilevel kernel density chi-squared analysis \\citep{wager2007meta} was "
418
+ "performed according to the same procedure as implemented in Neurosynth with NiMARE "
419
+ f"{__version__} "
420
+ "(RRID:SCR_017398; \\citealt{Salo2023}), "
421
+ f"using a(n) {self.kernel_transformer.__class__.__name__.replace('Kernel', '')} "
422
+ "kernel. "
423
+ f"{self.kernel_transformer._generate_description()} "
424
+ "This analysis calculated several measures. "
425
+ "The first dataset was evaluated for uniformity of activation via a one-way "
426
+ "chi-square test. "
427
+ f"The first input dataset included {self.inputs_['coordinates1'].shape[0]} foci from "
428
+ f"{len(self.inputs_['id1'])} experiments. "
429
+ f"The second input dataset included {self.inputs_['coordinates2'].shape[0]} foci from "
430
+ f"{len(self.inputs_['id2'])} experiments."
431
+ )
432
+
433
+ return description
434
+
435
+ def _fit(self, dataset1, dataset2):
436
+ self.dataset1 = dataset1
437
+ self.dataset2 = dataset2
438
+ self.masker = self.masker or dataset1.masker
439
+ self.null_distributions_ = {}
440
+
441
+ # Generate MA maps and calculate count variables for first dataset
442
+ n_selected_active_voxels = self._collect_ma_maps(
443
+ maps_key="ma_maps1",
444
+ coords_key="coordinates1",
445
+ return_type="summary_array",
446
+ )
447
+
448
+ n_selected = self.dataset1.coordinates["id"].unique().shape[0]
449
+
450
+ # Generate MA maps and calculate count variables for second dataset
451
+ n_unselected_active_voxels = self._collect_ma_maps(
452
+ maps_key="ma_maps2",
453
+ coords_key="coordinates2",
454
+ return_type="summary_array",
455
+ )
456
+ n_unselected = self.dataset2.coordinates["id"].unique().shape[0]
457
+
458
+ n_mappables = n_selected + n_unselected
459
+
460
+ # Nomenclature for variables below: p = probability,
461
+ # F = feature present, g = given, U = unselected, A = activation.
462
+ # So, e.g., pAgF = p(A|F) = probability of activation
463
+ # in a voxel if we know that the feature is present in a study.
464
+ pF = n_selected / n_mappables
465
+ pA = np.array(
466
+ (n_selected_active_voxels + n_unselected_active_voxels) / n_mappables
467
+ ).squeeze()
468
+
469
+ del n_mappables
470
+
471
+ pAgF = n_selected_active_voxels / n_selected
472
+ pAgU = n_unselected_active_voxels / n_unselected
473
+ pFgA = pAgF * pF / pA
474
+
475
+ del pF
476
+
477
+ if self.prior:
478
+ # Recompute conditionals with uniform prior
479
+ pAgF_prior = self.prior * pAgF + (1 - self.prior) * pAgU
480
+ pFgA_prior = pAgF * self.prior / pAgF_prior
481
+
482
+ # One-way chi-square test for uniformity of activation
483
+ pAgF_chi2_vals = one_way(np.squeeze(n_selected_active_voxels), n_selected)
484
+ pAgF_p_vals = chi2.sf(pAgF_chi2_vals, 1)
485
+ pAgF_sign = np.sign(n_selected_active_voxels - np.mean(n_selected_active_voxels))
486
+ pAgF_z = p_to_z(pAgF_p_vals, tail="two") * pAgF_sign
487
+
488
+ del pAgF_sign
489
+
490
+ # Two-way chi-square for association of activation
491
+ cells = np.squeeze(
492
+ np.array(
493
+ [
494
+ [n_selected_active_voxels, n_unselected_active_voxels],
495
+ [
496
+ n_selected - n_selected_active_voxels,
497
+ n_unselected - n_unselected_active_voxels,
498
+ ],
499
+ ]
500
+ ).T
501
+ )
502
+
503
+ del n_selected, n_unselected
504
+
505
+ pFgA_chi2_vals = two_way(cells)
506
+
507
+ del n_selected_active_voxels, n_unselected_active_voxels
508
+
509
+ eps = np.spacing(1)
510
+ pFgA_p_vals = chi2.sf(pFgA_chi2_vals, 1)
511
+ pFgA_p_vals[pFgA_p_vals < eps] = eps
512
+ pFgA_sign = np.sign(pAgF - pAgU).ravel()
513
+ pFgA_z = p_to_z(pFgA_p_vals, tail="two") * pFgA_sign
514
+
515
+ del pFgA_sign, pAgU
516
+
517
+ maps = {
518
+ "z_desc-uniformity": pAgF_z,
519
+ "z_desc-association": pFgA_z,
520
+ "chi2_desc-uniformity": pAgF_chi2_vals,
521
+ "chi2_desc-association": pFgA_chi2_vals,
522
+ "p_desc-uniformity": pAgF_p_vals,
523
+ "p_desc-association": pFgA_p_vals,
524
+ "prob_desc-A": pA,
525
+ "prob_desc-AgF": pAgF,
526
+ "prob_desc-FgA": pFgA,
527
+ }
528
+
529
+ if self.prior:
530
+ maps["prob_desc-AgF_prior"] = pAgF_prior
531
+ maps["prob_desc-FgA_prior"] = pFgA_prior
532
+
533
+ description = self._generate_description()
534
+ return maps, {}, description
535
+
536
+ def _run_fwe_permutation(self, iter_xyz1, iter_xyz2, iter_df1, iter_df2, conn, voxel_thresh):
537
+ """Run a single permutation of the Monte Carlo FWE correction procedure.
538
+
539
+ Parameters
540
+ ----------
541
+ iter_xyz1, iter_xyz2 : :obj:`numpy.ndarray`
542
+ Random coordinates for the permutation.
543
+ iter_df1, iter_df2 : :obj:`pandas.DataFrame`
544
+ DataFrames with as many rows as there are coordinates in each of the two datasets,
545
+ to be filled in with random coordinates for the permutation.
546
+ conn : :obj:`numpy.ndarray` of shape (3, 3, 3)
547
+ Connectivity matrix for defining clusters.
548
+ voxel_thresh : :obj:`float`
549
+ Uncorrected summary-statistic thresholded for defining clusters.
550
+
551
+ Returns
552
+ -------
553
+ pAgF_max_chi2_value : :obj:`float`
554
+ Forward inference maximum chi-squared value, for voxel-level FWE correction.
555
+ pAgF_max_size : :obj:`float`
556
+ Forward inference maximum cluster size, for cluster-level FWE correction.
557
+ pAgF_max_mass : :obj:`float`
558
+ Forward inference maximum cluster mass, for cluster-level FWE correction.
559
+ pFgA_max_chi2_value : :obj:`float`
560
+ Reverse inference maximum chi-squared value, for voxel-level FWE correction.
561
+ pFgA_max_size : :obj:`float`
562
+ Reverse inference maximum cluster size, for cluster-level FWE correction.
563
+ pFgA_max_mass : :obj:`float`
564
+ Reverse inference maximum cluster mass, for cluster-level FWE correction.
565
+ """
566
+ # Not sure if joblib will automatically use a copy of the object, but I'll make a copy to
567
+ # be safe.
568
+ iter_df1 = iter_df1.copy()
569
+ iter_df2 = iter_df2.copy()
570
+
571
+ iter_xyz1 = np.squeeze(iter_xyz1)
572
+ iter_xyz2 = np.squeeze(iter_xyz2)
573
+ iter_df1[["x", "y", "z"]] = iter_xyz1
574
+ iter_df2[["x", "y", "z"]] = iter_xyz2
575
+
576
+ # Generate MA maps and calculate count variables for first dataset
577
+ n_selected_active_voxels = self.kernel_transformer.transform(
578
+ iter_df1, self.masker, return_type="summary_array"
579
+ )
580
+
581
+ n_selected = self.dataset1.coordinates["id"].unique().shape[0]
582
+
583
+ # Generate MA maps and calculate count variables for second dataset
584
+ n_unselected_active_voxels = self.kernel_transformer.transform(
585
+ iter_df2, self.masker, return_type="summary_array"
586
+ )
587
+ n_unselected = self.dataset2.coordinates["id"].unique().shape[0]
588
+
589
+ # Currently unused conditional probabilities
590
+ # pAgF = n_selected_active_voxels / n_selected
591
+ # pAgU = n_unselected_active_voxels / n_unselected
592
+
593
+ # One-way chi-square test for uniformity of activation
594
+ pAgF_chi2_vals = one_way(np.squeeze(n_selected_active_voxels), n_selected)
595
+
596
+ # Voxel-level inference
597
+ pAgF_max_chi2_value = np.max(np.abs(pAgF_chi2_vals))
598
+
599
+ # Cluster-level inference
600
+ pAgF_chi2_map = self.masker.inverse_transform(pAgF_chi2_vals).get_fdata()
601
+ pAgF_max_size, pAgF_max_mass = _calculate_cluster_measures(
602
+ pAgF_chi2_map, voxel_thresh, conn, tail="two"
603
+ )
604
+
605
+ # Two-way chi-square for association of activation
606
+ cells = np.squeeze(
607
+ np.array(
608
+ [
609
+ [n_selected_active_voxels, n_unselected_active_voxels],
610
+ [
611
+ n_selected - n_selected_active_voxels,
612
+ n_unselected - n_unselected_active_voxels,
613
+ ],
614
+ ]
615
+ ).T
616
+ )
617
+ pFgA_chi2_vals = two_way(cells)
618
+
619
+ # Voxel-level inference
620
+ pFgA_max_chi2_value = np.max(np.abs(pFgA_chi2_vals))
621
+
622
+ # Cluster-level inference
623
+ pFgA_chi2_map = self.masker.inverse_transform(pFgA_chi2_vals).get_fdata()
624
+ pFgA_max_size, pFgA_max_mass = _calculate_cluster_measures(
625
+ pFgA_chi2_map, voxel_thresh, conn, tail="two"
626
+ )
627
+
628
+ return (
629
+ pAgF_max_chi2_value,
630
+ pAgF_max_size,
631
+ pAgF_max_mass,
632
+ pFgA_max_chi2_value,
633
+ pFgA_max_size,
634
+ pFgA_max_mass,
635
+ )
636
+
637
+ def _apply_correction(self, stat_values, voxel_thresh, vfwe_null, csfwe_null, cmfwe_null):
638
+ """Apply different kinds of FWE correction to statistical value matrix.
639
+
640
+ .. versionchanged:: 0.0.13
641
+
642
+ Change cluster neighborhood from faces+edges to faces, to match Nilearn.
643
+
644
+ Parameters
645
+ ----------
646
+ stat_values : :obj:`numpy.ndarray`
647
+ 1D array of summary-statistic values.
648
+ voxel_thresh : :obj:`float`
649
+ Summary statistic threshold for defining clusters.
650
+ vfwe_null, csfwe_null, cmfwe_null : :obj:`numpy.ndarray`
651
+ Null distributions for FWE correction.
652
+
653
+ Returns
654
+ -------
655
+ p_vfwe_values, p_csfwe_values, p_cmfwe_values : :obj:`numpy.ndarray`
656
+ 1D arrays of FWE-corrected p-values.
657
+ """
658
+ eps = np.spacing(1)
659
+
660
+ # Define connectivity matrix for cluster labeling
661
+ conn = ndimage.generate_binary_structure(rank=3, connectivity=1)
662
+
663
+ # Voxel-level FWE
664
+ p_vfwe_values = null_to_p(np.abs(stat_values), vfwe_null, tail="upper")
665
+
666
+ # Crop p-values of 0 or 1 to nearest values that won't evaluate to 0 or 1.
667
+ # Prevents inf z-values.
668
+ p_vfwe_values[p_vfwe_values < eps] = eps
669
+ p_vfwe_values[p_vfwe_values > (1.0 - eps)] = 1.0 - eps
670
+
671
+ # Cluster-level FWE
672
+ # Extract the summary statistics in voxel-wise (3D) form, threshold, and cluster-label
673
+ stat_map_thresh = self.masker.inverse_transform(stat_values).get_fdata()
674
+
675
+ stat_map_thresh[np.abs(stat_map_thresh) <= voxel_thresh] = 0
676
+
677
+ # Label positive and negative clusters separately
678
+ labeled_matrix = np.empty(stat_map_thresh.shape, int)
679
+ labeled_matrix, _ = ndimage.label(stat_map_thresh > 0, conn)
680
+ n_positive_clusters = np.max(labeled_matrix)
681
+ temp_labeled_matrix, _ = ndimage.label(stat_map_thresh < 0, conn)
682
+ temp_labeled_matrix[temp_labeled_matrix > 0] += n_positive_clusters
683
+ labeled_matrix = labeled_matrix + temp_labeled_matrix
684
+ del temp_labeled_matrix
685
+
686
+ cluster_labels, idx, cluster_sizes = np.unique(
687
+ labeled_matrix,
688
+ return_inverse=True,
689
+ return_counts=True,
690
+ )
691
+ assert cluster_labels[0] == 0
692
+
693
+ # Cluster mass-based inference
694
+ cluster_masses = np.zeros(cluster_labels.shape)
695
+ for i_val in cluster_labels:
696
+ if i_val == 0:
697
+ cluster_masses[i_val] = 0
698
+
699
+ cluster_mass = np.sum(np.abs(stat_map_thresh[labeled_matrix == i_val]) - voxel_thresh)
700
+ cluster_masses[i_val] = cluster_mass
701
+
702
+ p_cmfwe_vals = null_to_p(cluster_masses, cmfwe_null, tail="upper")
703
+ p_cmfwe_map = p_cmfwe_vals[np.reshape(idx, labeled_matrix.shape)]
704
+
705
+ p_cmfwe_values = np.squeeze(
706
+ self.masker.transform(nib.Nifti1Image(p_cmfwe_map, self.masker.mask_img.affine))
707
+ )
708
+
709
+ # Cluster size-based inference
710
+ cluster_sizes[0] = 0 # replace background's "cluster size" with zeros
711
+ p_csfwe_vals = null_to_p(cluster_sizes, csfwe_null, tail="upper")
712
+ p_csfwe_map = p_csfwe_vals[np.reshape(idx, labeled_matrix.shape)]
713
+ p_csfwe_values = np.squeeze(
714
+ self.masker.transform(nib.Nifti1Image(p_csfwe_map, self.masker.mask_img.affine))
715
+ )
716
+
717
+ return p_vfwe_values, p_csfwe_values, p_cmfwe_values
718
+
719
+ def correct_fwe_montecarlo(self, result, voxel_thresh=0.001, n_iters=1000, n_cores=1):
720
+ """Perform FWE correction using the max-value permutation method.
721
+
722
+ Only call this method from within a Corrector.
723
+
724
+ .. versionchanged:: 0.0.13
725
+
726
+ Change cluster neighborhood from faces+edges to faces, to match Nilearn.
727
+
728
+ .. versionchanged:: 0.0.12
729
+
730
+ Include cluster level-corrected results in Monte Carlo null method.
731
+
732
+ Parameters
733
+ ----------
734
+ result : :obj:`~nimare.results.MetaResult`
735
+ Result object from a KDA meta-analysis.
736
+ voxel_thresh : :obj:`float`, default=0.001
737
+ Voxel-level threshold. Default is 0.001.
738
+ n_iters : :obj:`int`, default=1000
739
+ Number of iterations to build the vFWE null distribution.
740
+ Default is 1000.
741
+ n_cores : :obj:`int`, default=1
742
+ Number of cores to use for parallelization.
743
+ If <=0, defaults to using all available cores. Default is 1.
744
+
745
+ Returns
746
+ -------
747
+ maps : :obj:`dict`
748
+ Dictionary of 1D arrays corresponding to masked maps generated by
749
+ the correction procedure. The following arrays are generated by
750
+ this method:
751
+
752
+ - ``p_desc-uniformity_level-voxel``: Voxel-level FWE-corrected p-values from the
753
+ uniformity/forward inference analysis.
754
+ - ``z_desc-uniformity_level-voxel``: Voxel-level FWE-corrected z-values from the
755
+ uniformity/forward inference analysis.
756
+ - ``logp_desc-uniformity_level-voxel``: Voxel-level FWE-corrected -log10 p-values
757
+ from the uniformity/forward inference analysis.
758
+ - ``p_desc-uniformityMass_level-cluster``: Cluster-level FWE-corrected p-values
759
+ from the uniformity/forward inference analysis, using cluster mass.
760
+ - ``z_desc-uniformityMass_level-cluster``: Cluster-level FWE-corrected z-values
761
+ from the uniformity/forward inference analysis, using cluster mass.
762
+ - ``logp_desc-uniformityMass_level-cluster``: Cluster-level FWE-corrected -log10
763
+ p-values from the uniformity/forward inference analysis, using cluster mass.
764
+ - ``p_desc-uniformitySize_level-cluster``: Cluster-level FWE-corrected p-values
765
+ from the uniformity/forward inference analysis, using cluster size.
766
+ - ``z_desc-uniformitySize_level-cluster``: Cluster-level FWE-corrected z-values
767
+ from the uniformity/forward inference analysis, using cluster size.
768
+ - ``logp_desc-uniformitySize_level-cluster``: Cluster-level FWE-corrected -log10
769
+ p-values from the uniformity/forward inference analysis, using cluster size.
770
+ - ``p_desc-association_level-voxel``: Voxel-level FWE-corrected p-values from the
771
+ association/reverse inference analysis.
772
+ - ``z_desc-association_level-voxel``: Voxel-level FWE-corrected z-values from the
773
+ association/reverse inference analysis.
774
+ - ``logp_desc-association_level-voxel``: Voxel-level FWE-corrected -log10 p-values
775
+ from the association/reverse inference analysis.
776
+ - ``p_desc-associationMass_level-cluster``: Cluster-level FWE-corrected p-values
777
+ from the association/reverse inference analysis, using cluster mass.
778
+ - ``z_desc-associationMass_level-cluster``: Cluster-level FWE-corrected z-values
779
+ from the association/reverse inference analysis, using cluster mass.
780
+ - ``logp_desc-associationMass_level-cluster``: Cluster-level FWE-corrected -log10
781
+ p-values from the association/reverse inference analysis, using cluster mass.
782
+ - ``p_desc-associationSize_level-cluster``: Cluster-level FWE-corrected p-values
783
+ from the association/reverse inference analysis, using cluster size.
784
+ - ``z_desc-associationSize_level-cluster``: Cluster-level FWE-corrected z-values
785
+ from the association/reverse inference analysis, using cluster size.
786
+ - ``logp_desc-associationSize_level-cluster``: Cluster-level FWE-corrected -log10
787
+ p-values from the association/reverse inference analysis, using cluster size.
788
+
789
+ Notes
790
+ -----
791
+ This method adds six new keys to the ``null_distributions_`` attribute:
792
+
793
+ - ``values_desc-pAgF_level-voxel_corr-fwe_method-montecarlo``: The maximum
794
+ chi-squared value from the p(A|F) one-way chi-squared test from each Monte Carlo
795
+ iteration. An array of shape (n_iters,).
796
+ - ``values_desc-pAgFsize_level-cluster_corr-fwe_method-montecarlo``: The maximum
797
+ cluster size value from the p(A|F) one-way chi-squared test from each Monte Carlo
798
+ iteration. An array of shape (n_iters,).
799
+ - ``values_desc-pAgFmass_level-cluster_corr-fwe_method-montecarlo``: The maximum
800
+ cluster mass value from the p(A|F) one-way chi-squared test from each Monte Carlo
801
+ iteration. An array of shape (n_iters,).
802
+ - ``values_desc-pFgA_level-voxel_corr-fwe_method-montecarlo``: The maximum
803
+ chi-squared value from the p(F|A) two-way chi-squared test from each Monte Carlo
804
+ iteration. An array of shape (n_iters,).
805
+ - ``values_desc-pFgAsize_level-cluster_corr-fwe_method-montecarlo``: The maximum
806
+ cluster size value from the p(F|A) two-way chi-squared test from each Monte Carlo
807
+ iteration. An array of shape (n_iters,).
808
+ - ``values_desc-pFgAmass_level-cluster_corr-fwe_method-montecarlo``: The maximum
809
+ cluster mass value from the p(F|A) two-way chi-squared test from each Monte Carlo
810
+ iteration. An array of shape (n_iters,).
811
+
812
+ See Also
813
+ --------
814
+ nimare.correct.FWECorrector : The Corrector from which to call this method.
815
+
816
+ Examples
817
+ --------
818
+ >>> meta = MKDAChi2()
819
+ >>> result = meta.fit(dset)
820
+ >>> corrector = FWECorrector(method='montecarlo', n_iters=5, n_cores=1)
821
+ >>> cresult = corrector.transform(result)
822
+ """
823
+ null_xyz = vox2mm(
824
+ np.vstack(np.where(self.masker.mask_img.get_fdata())).T,
825
+ self.masker.mask_img.affine,
826
+ )
827
+ pAgF_chi2_vals = result.get_map("chi2_desc-uniformity", return_type="array")
828
+ pFgA_chi2_vals = result.get_map("chi2_desc-association", return_type="array")
829
+ pAgF_z_vals = result.get_map("z_desc-uniformity", return_type="array")
830
+ pFgA_z_vals = result.get_map("z_desc-association", return_type="array")
831
+ pAgF_sign = np.sign(pAgF_z_vals)
832
+ pFgA_sign = np.sign(pFgA_z_vals)
833
+
834
+ n_cores = _check_ncores(n_cores)
835
+
836
+ iter_df1 = self.inputs_["coordinates1"]
837
+ iter_df2 = self.inputs_["coordinates2"]
838
+ rand_idx1 = np.random.choice(null_xyz.shape[0], size=(iter_df1.shape[0], n_iters))
839
+ rand_xyz1 = null_xyz[rand_idx1, :]
840
+ iter_xyzs1 = np.split(rand_xyz1, rand_xyz1.shape[1], axis=1)
841
+ rand_idx2 = np.random.choice(null_xyz.shape[0], size=(iter_df2.shape[0], n_iters))
842
+ rand_xyz2 = null_xyz[rand_idx2, :]
843
+ iter_xyzs2 = np.split(rand_xyz2, rand_xyz2.shape[1], axis=1)
844
+ eps = np.spacing(1)
845
+
846
+ # Identify summary statistic corresponding to intensity threshold
847
+ ss_thresh = chi2.isf(voxel_thresh, 1)
848
+
849
+ # Define connectivity matrix for cluster labeling
850
+ conn = ndimage.generate_binary_structure(rank=3, connectivity=1)
851
+
852
+ perm_results = [
853
+ r
854
+ for r in tqdm(
855
+ Parallel(return_as="generator", n_jobs=n_cores)(
856
+ delayed(self._run_fwe_permutation)(
857
+ iter_xyz1=iter_xyzs1[i_iter],
858
+ iter_xyz2=iter_xyzs2[i_iter],
859
+ iter_df1=iter_df1,
860
+ iter_df2=iter_df2,
861
+ conn=conn,
862
+ voxel_thresh=ss_thresh,
863
+ )
864
+ for i_iter in range(n_iters)
865
+ ),
866
+ total=n_iters,
867
+ )
868
+ ]
869
+
870
+ del rand_idx1, rand_xyz1, iter_xyzs1
871
+ del rand_idx2, rand_xyz2, iter_xyzs2
872
+
873
+ (
874
+ pAgF_vfwe_null,
875
+ pAgF_csfwe_null,
876
+ pAgF_cmfwe_null,
877
+ pFgA_vfwe_null,
878
+ pFgA_csfwe_null,
879
+ pFgA_cmfwe_null,
880
+ ) = zip(*perm_results)
881
+
882
+ del perm_results
883
+
884
+ # pAgF_FWE
885
+ pAgF_p_vfwe_vals, pAgF_p_csfwe_vals, pAgF_p_cmfwe_vals = self._apply_correction(
886
+ pAgF_chi2_vals,
887
+ ss_thresh,
888
+ vfwe_null=pAgF_vfwe_null,
889
+ csfwe_null=pAgF_csfwe_null,
890
+ cmfwe_null=pAgF_cmfwe_null,
891
+ )
892
+
893
+ self.null_distributions_["values_desc-pAgF_level-voxel_corr-fwe_method-montecarlo"] = (
894
+ pAgF_vfwe_null
895
+ )
896
+ self.null_distributions_[
897
+ "values_desc-pAgFsize_level-cluster_corr-fwe_method-montecarlo"
898
+ ] = pAgF_csfwe_null
899
+ self.null_distributions_[
900
+ "values_desc-pAgFmass_level-cluster_corr-fwe_method-montecarlo"
901
+ ] = pAgF_cmfwe_null
902
+
903
+ del pAgF_vfwe_null, pAgF_csfwe_null, pAgF_cmfwe_null
904
+
905
+ # pFgA_FWE
906
+ pFgA_p_vfwe_vals, pFgA_p_csfwe_vals, pFgA_p_cmfwe_vals = self._apply_correction(
907
+ pFgA_chi2_vals,
908
+ ss_thresh,
909
+ vfwe_null=pFgA_vfwe_null,
910
+ csfwe_null=pFgA_csfwe_null,
911
+ cmfwe_null=pFgA_cmfwe_null,
912
+ )
913
+
914
+ self.null_distributions_["values_desc-pFgA_level-voxel_corr-fwe_method-montecarlo"] = (
915
+ pFgA_vfwe_null
916
+ )
917
+ self.null_distributions_[
918
+ "values_desc-pFgAsize_level-cluster_corr-fwe_method-montecarlo"
919
+ ] = pFgA_csfwe_null
920
+ self.null_distributions_[
921
+ "values_desc-pFgAmass_level-cluster_corr-fwe_method-montecarlo"
922
+ ] = pFgA_cmfwe_null
923
+
924
+ del pFgA_vfwe_null, pFgA_csfwe_null, pFgA_cmfwe_null
925
+
926
+ # Convert p-values
927
+ # pAgF
928
+ pAgF_z_vfwe_vals = p_to_z(pAgF_p_vfwe_vals, tail="two") * pAgF_sign
929
+ pAgF_logp_vfwe_vals = -np.log10(pAgF_p_vfwe_vals)
930
+ pAgF_logp_vfwe_vals[np.isinf(pAgF_logp_vfwe_vals)] = -np.log10(eps)
931
+ pAgF_z_cmfwe_vals = p_to_z(pAgF_p_cmfwe_vals, tail="two") * pAgF_sign
932
+ pAgF_logp_cmfwe_vals = -np.log10(pAgF_p_cmfwe_vals)
933
+ pAgF_logp_cmfwe_vals[np.isinf(pAgF_logp_cmfwe_vals)] = -np.log10(eps)
934
+ pAgF_z_csfwe_vals = p_to_z(pAgF_p_csfwe_vals, tail="two") * pAgF_sign
935
+ pAgF_logp_csfwe_vals = -np.log10(pAgF_p_csfwe_vals)
936
+ pAgF_logp_csfwe_vals[np.isinf(pAgF_logp_csfwe_vals)] = -np.log10(eps)
937
+
938
+ # pFgA
939
+ pFgA_z_vfwe_vals = p_to_z(pFgA_p_vfwe_vals, tail="two") * pFgA_sign
940
+ pFgA_logp_vfwe_vals = -np.log10(pFgA_p_vfwe_vals)
941
+ pFgA_logp_vfwe_vals[np.isinf(pFgA_logp_vfwe_vals)] = -np.log10(eps)
942
+ pFgA_z_cmfwe_vals = p_to_z(pFgA_p_cmfwe_vals, tail="two") * pFgA_sign
943
+ pFgA_logp_cmfwe_vals = -np.log10(pFgA_p_cmfwe_vals)
944
+ pFgA_logp_cmfwe_vals[np.isinf(pFgA_logp_cmfwe_vals)] = -np.log10(eps)
945
+ pFgA_z_csfwe_vals = p_to_z(pFgA_p_csfwe_vals, tail="two") * pFgA_sign
946
+ pFgA_logp_csfwe_vals = -np.log10(pFgA_p_csfwe_vals)
947
+ pFgA_logp_csfwe_vals[np.isinf(pFgA_logp_csfwe_vals)] = -np.log10(eps)
948
+
949
+ maps = {
950
+ # uniformity analysis
951
+ "p_desc-uniformity_level-voxel": pAgF_p_vfwe_vals,
952
+ "z_desc-uniformity_level-voxel": pAgF_z_vfwe_vals,
953
+ "logp_desc-uniformity_level-voxel": pAgF_logp_vfwe_vals,
954
+ "p_desc-uniformityMass_level-cluster": pAgF_p_cmfwe_vals,
955
+ "z_desc-uniformityMass_level-cluster": pAgF_z_cmfwe_vals,
956
+ "logp_desc-uniformityMass_level-cluster": pAgF_logp_cmfwe_vals,
957
+ "p_desc-uniformitySize_level-cluster": pAgF_p_csfwe_vals,
958
+ "z_desc-uniformitySize_level-cluster": pAgF_z_csfwe_vals,
959
+ "logp_desc-uniformitySize_level-cluster": pAgF_logp_csfwe_vals,
960
+ # association analysis
961
+ "p_desc-association_level-voxel": pFgA_p_vfwe_vals,
962
+ "z_desc-association_level-voxel": pFgA_z_vfwe_vals,
963
+ "logp_desc-association_level-voxel": pFgA_logp_vfwe_vals,
964
+ "p_desc-associationMass_level-cluster": pFgA_p_cmfwe_vals,
965
+ "z_desc-associationMass_level-cluster": pFgA_z_cmfwe_vals,
966
+ "logp_desc-associationMass_level-cluster": pFgA_logp_cmfwe_vals,
967
+ "p_desc-associationSize_level-cluster": pFgA_p_csfwe_vals,
968
+ "z_desc-associationSize_level-cluster": pFgA_z_csfwe_vals,
969
+ "logp_desc-associationSize_level-cluster": pFgA_logp_csfwe_vals,
970
+ }
971
+
972
+ description = ""
973
+
974
+ return maps, {}, description
975
+
976
+ def correct_fdr_indep(self, result, alpha=0.05):
977
+ """Perform FDR correction using the Benjamini-Hochberg method.
978
+
979
+ Only call this method from within a Corrector.
980
+
981
+ .. versionchanged:: 0.0.12
982
+
983
+ Renamed from ``correct_fdr_bh`` to ``correct_fdr_indep``.
984
+
985
+ Parameters
986
+ ----------
987
+ result : :obj:`~nimare.results.MetaResult`
988
+ Result object from a KDA meta-analysis.
989
+ alpha : :obj:`float`, optional
990
+ Alpha. Default is 0.05.
991
+
992
+ Returns
993
+ -------
994
+ maps : :obj:`dict`
995
+ Dictionary of 1D arrays corresponding to masked maps generated by
996
+ the correction procedure. The following arrays are generated by
997
+ this method: 'z_desc-uniformity_level-voxel' and 'z_desc-association_level-voxel'.
998
+
999
+ See Also
1000
+ --------
1001
+ nimare.correct.FDRCorrector : The Corrector from which to call this method.
1002
+
1003
+ Examples
1004
+ --------
1005
+ >>> meta = MKDAChi2()
1006
+ >>> result = meta.fit(dset)
1007
+ >>> corrector = FDRCorrector(method='indep', alpha=0.05)
1008
+ >>> cresult = corrector.transform(result)
1009
+ """
1010
+ pAgF_p_vals = result.get_map("p_desc-uniformity", return_type="array")
1011
+ pFgA_p_vals = result.get_map("p_desc-association", return_type="array")
1012
+ pAgF_z_vals = result.get_map("z_desc-uniformity", return_type="array")
1013
+ pFgA_z_vals = result.get_map("z_desc-association", return_type="array")
1014
+ pAgF_sign = np.sign(pAgF_z_vals)
1015
+ pFgA_sign = np.sign(pFgA_z_vals)
1016
+ pAgF_p_FDR = fdr(pAgF_p_vals, q=alpha, method="bh")
1017
+ pAgF_z_FDR = p_to_z(pAgF_p_FDR, tail="two") * pAgF_sign
1018
+
1019
+ pFgA_p_FDR = fdr(pFgA_p_vals, q=alpha, method="bh")
1020
+ pFgA_z_FDR = p_to_z(pFgA_p_FDR, tail="two") * pFgA_sign
1021
+
1022
+ maps = {
1023
+ "z_desc-uniformity_level-voxel": pAgF_z_FDR,
1024
+ "z_desc-association_level-voxel": pFgA_z_FDR,
1025
+ }
1026
+
1027
+ description = ""
1028
+
1029
+ return maps, {}, description
1030
+
1031
+
1032
+ class KDA(CBMAEstimator):
1033
+ r"""Kernel density analysis.
1034
+
1035
+ .. versionchanged:: 0.2.1
1036
+
1037
+ - New parameters: ``memory`` and ``memory_level`` for memory caching.
1038
+
1039
+ .. versionchanged:: 0.0.12
1040
+
1041
+ - Use a 4D sparse array for modeled activation maps.
1042
+
1043
+ Parameters
1044
+ ----------
1045
+ kernel_transformer : :obj:`~nimare.meta.kernel.KernelTransformer`, optional
1046
+ Kernel with which to convolve coordinates from dataset. Default is
1047
+ :class:`~nimare.meta.kernel.KDAKernel`.
1048
+ null_method : {"approximate", "montecarlo"}, optional
1049
+ Method by which to determine uncorrected p-values. The available options are
1050
+
1051
+ ======================= =================================================================
1052
+ "approximate" (default) Build a histogram of summary-statistic values and their
1053
+ expected frequencies under the assumption of random spatial
1054
+ associated between studies, via a weighted convolution.
1055
+
1056
+ This method is much faster, but slightly less accurate.
1057
+ "montecarlo" Perform a large number of permutations, in which the coordinates
1058
+ in the studies are randomly drawn from the Estimator's brain mask
1059
+ and the full set of resulting summary-statistic values are
1060
+ incorporated into a null distribution (stored as a histogram for
1061
+ memory reasons).
1062
+
1063
+ This method is must slower, and is only slightly more accurate.
1064
+ ======================= =================================================================
1065
+
1066
+ n_iters : int, default=5000
1067
+ Number of iterations to use to define the null distribution.
1068
+ This is only used if ``null_method=="montecarlo"``.
1069
+ Default is 5000.
1070
+ memory : instance of :class:`joblib.Memory`, :obj:`str`, or :class:`pathlib.Path`
1071
+ Used to cache the output of a function. By default, no caching is done.
1072
+ If a :obj:`str` is given, it is the path to the caching directory.
1073
+ memory_level : :obj:`int`, default=0
1074
+ Rough estimator of the amount of memory used by caching.
1075
+ Higher value means more memory for caching. Zero means no caching.
1076
+ n_cores : :obj:`int`, default=1
1077
+ Number of cores to use for parallelization.
1078
+ This is only used if ``null_method=="montecarlo"``.
1079
+ If <=0, defaults to using all available cores.
1080
+ Default is 1.
1081
+ **kwargs
1082
+ Keyword arguments. Arguments for the kernel_transformer can be assigned
1083
+ here, with the prefix '\kernel__' in the variable name.
1084
+
1085
+ Attributes
1086
+ ----------
1087
+ masker : :class:`~nilearn.input_data.NiftiMasker` or similar
1088
+ Masker object.
1089
+ inputs_ : :obj:`dict`
1090
+ Inputs to the Estimator. For CBMA estimators, there is only one key: coordinates.
1091
+ This is an edited version of the dataset's coordinates DataFrame.
1092
+ null_distributions_ : :obj:`dict` of :class:`numpy.ndarray`
1093
+ Null distributions for the uncorrected summary-statistic-to-p-value conversion and any
1094
+ multiple-comparisons correction methods.
1095
+ Entries are added to this attribute if and when the corresponding method is applied.
1096
+
1097
+ If ``null_method == "approximate"``:
1098
+
1099
+ - ``histogram_bins``: Array of bin centers for the null distribution histogram,
1100
+ ranging from zero to the maximum possible summary statistic value for the Dataset.
1101
+ - ``histweights_corr-none_method-approximate``: Array of weights for the null
1102
+ distribution histogram, with one value for each bin in ``histogram_bins``.
1103
+
1104
+ If ``null_method == "montecarlo"``:
1105
+
1106
+ - ``histogram_bins``: Array of bin centers for the null distribution histogram,
1107
+ ranging from zero to the maximum possible summary statistic value for the Dataset.
1108
+ - ``histweights_corr-none_method-montecarlo``: Array of weights for the null
1109
+ distribution histogram, with one value for each bin in ``histogram_bins``.
1110
+ These values are derived from the full set of summary statistics from each
1111
+ iteration of the Monte Carlo procedure.
1112
+ - ``histweights_level-voxel_corr-fwe_method-montecarlo``: Array of weights for the
1113
+ voxel-level FWE-correction null distribution, with one value for each bin in
1114
+ ``histogram_bins``. These values are derived from the maximum summary statistic
1115
+ from each iteration of the Monte Carlo procedure.
1116
+
1117
+ If :meth:`correct_fwe_montecarlo` is applied:
1118
+
1119
+ - ``values_level-voxel_corr-fwe_method-montecarlo``: The maximum summary statistic
1120
+ value from each Monte Carlo iteration. An array of shape (n_iters,).
1121
+ - ``values_desc-size_level-cluster_corr-fwe_method-montecarlo``: The maximum cluster
1122
+ size from each Monte Carlo iteration. An array of shape (n_iters,).
1123
+ - ``values_desc-mass_level-cluster_corr-fwe_method-montecarlo``: The maximum cluster
1124
+ mass from each Monte Carlo iteration. An array of shape (n_iters,).
1125
+
1126
+ Notes
1127
+ -----
1128
+ Kernel density analysis was first introduced in :footcite:t:`wager2003valence` and
1129
+ :footcite:t:`wager2004neuroimaging`.
1130
+
1131
+ Available correction methods: :func:`KDA.correct_fwe_montecarlo`
1132
+
1133
+ Warnings
1134
+ --------
1135
+ The KDA algorithm has been replaced in the literature with the MKDA algorithm.
1136
+ As such, this estimator should almost never be used, outside of systematic
1137
+ comparisons between algorithms.
1138
+
1139
+ References
1140
+ ----------
1141
+ .. footbibliography::
1142
+ """
1143
+
1144
+ def __init__(
1145
+ self,
1146
+ kernel_transformer=KDAKernel,
1147
+ null_method="approximate",
1148
+ n_iters=5000,
1149
+ memory=Memory(location=None, verbose=0),
1150
+ memory_level=0,
1151
+ n_cores=1,
1152
+ **kwargs,
1153
+ ):
1154
+ LGR.warning(
1155
+ "The KDA algorithm has been replaced in the literature with the MKDA algorithm. "
1156
+ "As such, this estimator should almost never be used, outside of systematic "
1157
+ "comparisons between algorithms."
1158
+ )
1159
+
1160
+ if not (isinstance(kernel_transformer, KDAKernel) or kernel_transformer == KDAKernel):
1161
+ LGR.warning(
1162
+ f"The KernelTransformer being used ({kernel_transformer}) is not optimized "
1163
+ f"for the {type(self).__name__} algorithm. "
1164
+ "Expect suboptimal performance and beware bugs."
1165
+ )
1166
+
1167
+ # Add kernel transformer attribute and process keyword arguments
1168
+ super().__init__(
1169
+ kernel_transformer=kernel_transformer,
1170
+ memory=memory,
1171
+ memory_level=memory_level,
1172
+ **kwargs,
1173
+ )
1174
+ self.null_method = null_method
1175
+ self.n_iters = None if null_method == "approximate" else n_iters or 5000
1176
+ self.n_cores = _check_ncores(n_cores)
1177
+ self.dataset = None
1178
+
1179
+ def _generate_description(self):
1180
+ """Generate a description of the fitted Estimator.
1181
+
1182
+ Returns
1183
+ -------
1184
+ str
1185
+ Description of the Estimator.
1186
+ """
1187
+ if self.null_method == "montecarlo":
1188
+ null_method_str = (
1189
+ "a Monte Carlo-based null distribution, in which dataset coordinates were "
1190
+ "randomly drawn from the analysis mask and the full set of ALE values were "
1191
+ f"retained, using {self.n_iters} iterations"
1192
+ )
1193
+ else:
1194
+ null_method_str = "an approximate null distribution"
1195
+
1196
+ description = (
1197
+ "A kernel density (KDA) meta-analysis \\citep{wager2007meta} was "
1198
+ "performed was performed with NiMARE "
1199
+ f"{__version__} "
1200
+ "(RRID:SCR_017398; \\citealt{Salo2023}), "
1201
+ f"using a(n) {self.kernel_transformer.__class__.__name__.replace('Kernel', '')} "
1202
+ "kernel. "
1203
+ f"{self.kernel_transformer._generate_description()} "
1204
+ f"Summary statistics (OF values) were converted to p-values using {null_method_str}. "
1205
+ f"The input dataset included {self.inputs_['coordinates'].shape[0]} foci from "
1206
+ f"{len(self.inputs_['id'])} experiments."
1207
+ )
1208
+ return description
1209
+
1210
+ def _compute_summarystat_est(self, ma_values):
1211
+ """Compute OF scores from data.
1212
+
1213
+ Parameters
1214
+ ----------
1215
+ ma_maps : :obj:`numpy.ndarray` or :obj:`sparse._coo.core.COO`
1216
+ MA maps.
1217
+ The ma_maps can be:
1218
+ (1) a 1d contrast-len or 2d contrast-by-voxel array of MA values,
1219
+ or (2) a 4d sparse array of MA maps,
1220
+
1221
+ Returns
1222
+ -------
1223
+ stat_values : 1d array
1224
+ OF values. One value per voxel.
1225
+ """
1226
+ # OF is just a sum of MA values.
1227
+ if isinstance(ma_values, sparse._coo.core.COO):
1228
+ # NOTE: This may not work correctly with a non-NiftiMasker.
1229
+ mask_data = self.masker.mask_img.get_fdata().astype(bool)
1230
+
1231
+ stat_values = ma_values.sum(axis=0)
1232
+
1233
+ stat_values = stat_values.todense().reshape(-1)
1234
+ stat_values = stat_values[mask_data.reshape(-1)]
1235
+
1236
+ # This is used by _compute_null_approximate
1237
+ self.__n_mask_voxels = stat_values.shape[0]
1238
+ else:
1239
+ # np.array type is used by _determine_histogram_bins to calculate max_poss_value
1240
+ stat_values = np.sum(ma_values, axis=0)
1241
+
1242
+ return stat_values
1243
+
1244
+ def _determine_histogram_bins(self, ma_maps):
1245
+ """Determine histogram bins for null distribution methods.
1246
+
1247
+ Parameters
1248
+ ----------
1249
+ ma_maps : :obj:`sparse._coo.core.COO`
1250
+ MA maps.
1251
+
1252
+ Notes
1253
+ -----
1254
+ This method adds one entry to the null_distributions_ dict attribute: "histogram_bins".
1255
+ """
1256
+ if not isinstance(ma_maps, sparse._coo.core.COO):
1257
+ raise ValueError(f"Unsupported data type '{type(ma_maps)}'")
1258
+
1259
+ # assumes that groupby results in same order as MA maps
1260
+ n_foci_per_study = self.inputs_["coordinates"].groupby("id").size().values
1261
+
1262
+ # Determine bins for null distribution histogram
1263
+ if hasattr(self.kernel_transformer, "value"):
1264
+ # Binary-sphere kernels (KDA & MKDA)
1265
+ # The maximum possible MA value for each study is the weighting factor (generally 1)
1266
+ # times the number of foci in the study.
1267
+ # We grab the weighting factor from the kernel transformer.
1268
+ step_size = self.kernel_transformer.value # typically 1
1269
+ max_ma_values = step_size * n_foci_per_study
1270
+ max_poss_value = self._compute_summarystat_est(max_ma_values)
1271
+ else:
1272
+ # Continuous-sphere kernels (ALE)
1273
+ LGR.info(
1274
+ "A non-binary kernel has been detected. Parameters for the null distribution "
1275
+ "will be guesstimated."
1276
+ )
1277
+
1278
+ N_BINS = 100000
1279
+ # The maximum possible MA value is the max value from each MA map,
1280
+ # unlike the case with a summation-based kernel.
1281
+ # Need to convert to dense because np.ceil is too slow with sparse
1282
+ max_ma_values = ma_maps.max(axis=[1, 2, 3]).todense()
1283
+
1284
+ # round up based on resolution
1285
+ # hardcoding 1000 here because figuring out what to round to was difficult.
1286
+ max_ma_values = np.ceil(max_ma_values * 1000) / 1000
1287
+ max_poss_value = self._compute_summarystat(max_ma_values)
1288
+
1289
+ # create bin centers
1290
+ hist_bins = np.linspace(0, max_poss_value, N_BINS - 1)
1291
+ step_size = hist_bins[1] - hist_bins[0]
1292
+
1293
+ # Weighting is not supported yet, so I'm going to build my bins around the min MA value.
1294
+ # The histogram bins are bin *centers*, not edges.
1295
+ hist_bins = np.arange(0, max_poss_value + (step_size * 1.5), step_size)
1296
+ self.null_distributions_["histogram_bins"] = hist_bins
1297
+
1298
+ def _compute_null_approximate(self, ma_maps):
1299
+ """Compute uncorrected null distribution using approximate solution.
1300
+
1301
+ Parameters
1302
+ ----------
1303
+ ma_maps : :obj:`sparse._coo.core.COO`
1304
+ MA maps.
1305
+
1306
+ Notes
1307
+ -----
1308
+ This method adds two entries to the null_distributions_ dict attribute:
1309
+ "histogram_bins" and "histogram_weights".
1310
+ """
1311
+ if not isinstance(ma_maps, sparse._coo.core.COO):
1312
+ raise ValueError(f"Unsupported data type '{type(ma_maps)}'")
1313
+
1314
+ # Derive bin edges from histogram bin centers for numpy histogram function
1315
+ bin_centers = self.null_distributions_["histogram_bins"]
1316
+ step_size = bin_centers[1] - bin_centers[0]
1317
+ inv_step_size = 1 / step_size
1318
+ bin_edges = bin_centers - (step_size / 2)
1319
+ bin_edges = np.append(bin_centers, bin_centers[-1] + step_size)
1320
+
1321
+ n_exp = ma_maps.shape[0]
1322
+ n_bins = bin_centers.shape[0]
1323
+ ma_hists = np.zeros((n_exp, n_bins))
1324
+ data = ma_maps.data
1325
+ coords = ma_maps.coords
1326
+ for exp_idx in range(n_exp):
1327
+ # The first column of coords is the fourth dimension of the dense array
1328
+ study_ma_values = data[coords[0, :] == exp_idx]
1329
+
1330
+ n_nonzero_voxels = study_ma_values.shape[0]
1331
+ n_zero_voxels = self.__n_mask_voxels - n_nonzero_voxels
1332
+
1333
+ ma_hists[exp_idx, :] = np.histogram(study_ma_values, bins=bin_edges, density=False)[
1334
+ 0
1335
+ ].astype(float)
1336
+ ma_hists[exp_idx, 0] += n_zero_voxels
1337
+
1338
+ # Normalize MA histograms to get probabilities
1339
+ ma_hists /= ma_hists.sum(1)[:, None]
1340
+
1341
+ # Null distribution to convert summary statistics to p-values.
1342
+ stat_hist = ma_hists[0, :].copy()
1343
+
1344
+ for i_exp in range(1, ma_hists.shape[0]):
1345
+ exp_hist = ma_hists[i_exp, :]
1346
+
1347
+ # Find histogram bins with nonzero values for each histogram.
1348
+ stat_idx = np.where(stat_hist > 0)[0]
1349
+ exp_idx = np.where(exp_hist > 0)[0]
1350
+
1351
+ # Compute output MA values, stat_hist indices, and probabilities
1352
+ stat_scores = np.add.outer(bin_centers[exp_idx], bin_centers[stat_idx]).ravel()
1353
+ score_idx = np.floor(stat_scores * inv_step_size).astype(int)
1354
+ probabilities = np.outer(exp_hist[exp_idx], stat_hist[stat_idx]).ravel()
1355
+
1356
+ # Reset histogram and set probabilities. Use at() because there can
1357
+ # be redundant values in score_idx.
1358
+ stat_hist = np.zeros(stat_hist.shape)
1359
+ np.add.at(stat_hist, score_idx, probabilities)
1360
+
1361
+ self.null_distributions_["histweights_corr-none_method-approximate"] = stat_hist