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.
- benchmarks/__init__.py +0 -0
- benchmarks/bench_cbma.py +57 -0
- nimare/__init__.py +45 -0
- nimare/_version.py +21 -0
- nimare/annotate/__init__.py +21 -0
- nimare/annotate/cogat.py +213 -0
- nimare/annotate/gclda.py +924 -0
- nimare/annotate/lda.py +147 -0
- nimare/annotate/text.py +75 -0
- nimare/annotate/utils.py +87 -0
- nimare/base.py +217 -0
- nimare/cli.py +124 -0
- nimare/correct.py +462 -0
- nimare/dataset.py +685 -0
- nimare/decode/__init__.py +33 -0
- nimare/decode/base.py +115 -0
- nimare/decode/continuous.py +462 -0
- nimare/decode/discrete.py +753 -0
- nimare/decode/encode.py +110 -0
- nimare/decode/utils.py +44 -0
- nimare/diagnostics.py +510 -0
- nimare/estimator.py +139 -0
- nimare/extract/__init__.py +19 -0
- nimare/extract/extract.py +466 -0
- nimare/extract/utils.py +295 -0
- nimare/generate.py +331 -0
- nimare/io.py +635 -0
- nimare/meta/__init__.py +39 -0
- nimare/meta/cbma/__init__.py +6 -0
- nimare/meta/cbma/ale.py +951 -0
- nimare/meta/cbma/base.py +947 -0
- nimare/meta/cbma/mkda.py +1361 -0
- nimare/meta/cbmr.py +970 -0
- nimare/meta/ibma.py +1683 -0
- nimare/meta/kernel.py +501 -0
- nimare/meta/models.py +1199 -0
- nimare/meta/utils.py +494 -0
- nimare/nimads.py +492 -0
- nimare/reports/__init__.py +24 -0
- nimare/reports/base.py +664 -0
- nimare/reports/default.yml +123 -0
- nimare/reports/figures.py +651 -0
- nimare/reports/report.tpl +160 -0
- nimare/resources/__init__.py +1 -0
- nimare/resources/atlases/Harvard-Oxford-LICENSE +93 -0
- nimare/resources/atlases/HarvardOxford-cort-maxprob-thr25-2mm.nii.gz +0 -0
- nimare/resources/database_file_manifest.json +142 -0
- nimare/resources/english_spellings.csv +1738 -0
- nimare/resources/filenames.json +32 -0
- nimare/resources/neurosynth_laird_studies.json +58773 -0
- nimare/resources/neurosynth_stoplist.txt +396 -0
- nimare/resources/nidm_pain_dset.json +1349 -0
- nimare/resources/references.bib +541 -0
- nimare/resources/semantic_knowledge_children.txt +325 -0
- nimare/resources/semantic_relatedness_children.txt +249 -0
- nimare/resources/templates/MNI152_2x2x2_brainmask.nii.gz +0 -0
- nimare/resources/templates/tpl-MNI152NLin6Asym_res-01_T1w.nii.gz +0 -0
- nimare/resources/templates/tpl-MNI152NLin6Asym_res-01_desc-brain_mask.nii.gz +0 -0
- nimare/resources/templates/tpl-MNI152NLin6Asym_res-02_T1w.nii.gz +0 -0
- nimare/resources/templates/tpl-MNI152NLin6Asym_res-02_desc-brain_mask.nii.gz +0 -0
- nimare/results.py +225 -0
- nimare/stats.py +276 -0
- nimare/tests/__init__.py +1 -0
- nimare/tests/conftest.py +229 -0
- nimare/tests/data/amygdala_roi.nii.gz +0 -0
- nimare/tests/data/data-neurosynth_version-7_coordinates.tsv.gz +0 -0
- nimare/tests/data/data-neurosynth_version-7_metadata.tsv.gz +0 -0
- nimare/tests/data/data-neurosynth_version-7_vocab-terms_source-abstract_type-tfidf_features.npz +0 -0
- nimare/tests/data/data-neurosynth_version-7_vocab-terms_vocabulary.txt +100 -0
- nimare/tests/data/neurosynth_dset.json +2868 -0
- nimare/tests/data/neurosynth_laird_studies.json +58773 -0
- nimare/tests/data/nidm_pain_dset.json +1349 -0
- nimare/tests/data/nimads_annotation.json +1 -0
- nimare/tests/data/nimads_studyset.json +1 -0
- nimare/tests/data/test_baseline.txt +2 -0
- nimare/tests/data/test_pain_dataset.json +1278 -0
- nimare/tests/data/test_pain_dataset_multiple_contrasts.json +1242 -0
- nimare/tests/data/test_sleuth_file.txt +18 -0
- nimare/tests/data/test_sleuth_file2.txt +10 -0
- nimare/tests/data/test_sleuth_file3.txt +5 -0
- nimare/tests/data/test_sleuth_file4.txt +5 -0
- nimare/tests/data/test_sleuth_file5.txt +5 -0
- nimare/tests/test_annotate_cogat.py +32 -0
- nimare/tests/test_annotate_gclda.py +86 -0
- nimare/tests/test_annotate_lda.py +27 -0
- nimare/tests/test_dataset.py +99 -0
- nimare/tests/test_decode_continuous.py +132 -0
- nimare/tests/test_decode_discrete.py +92 -0
- nimare/tests/test_diagnostics.py +168 -0
- nimare/tests/test_estimator_performance.py +385 -0
- nimare/tests/test_extract.py +46 -0
- nimare/tests/test_generate.py +247 -0
- nimare/tests/test_io.py +240 -0
- nimare/tests/test_meta_ale.py +298 -0
- nimare/tests/test_meta_cbmr.py +295 -0
- nimare/tests/test_meta_ibma.py +240 -0
- nimare/tests/test_meta_kernel.py +209 -0
- nimare/tests/test_meta_mkda.py +234 -0
- nimare/tests/test_nimads.py +21 -0
- nimare/tests/test_reports.py +110 -0
- nimare/tests/test_stats.py +101 -0
- nimare/tests/test_transforms.py +272 -0
- nimare/tests/test_utils.py +200 -0
- nimare/tests/test_workflows.py +221 -0
- nimare/tests/utils.py +126 -0
- nimare/transforms.py +907 -0
- nimare/utils.py +1367 -0
- nimare/workflows/__init__.py +14 -0
- nimare/workflows/base.py +189 -0
- nimare/workflows/cbma.py +165 -0
- nimare/workflows/ibma.py +108 -0
- nimare/workflows/macm.py +77 -0
- nimare/workflows/misc.py +65 -0
- nimare-0.4.2rc4.dist-info/LICENSE +21 -0
- nimare-0.4.2rc4.dist-info/METADATA +124 -0
- nimare-0.4.2rc4.dist-info/RECORD +119 -0
- nimare-0.4.2rc4.dist-info/WHEEL +5 -0
- nimare-0.4.2rc4.dist-info/entry_points.txt +2 -0
- nimare-0.4.2rc4.dist-info/top_level.txt +2 -0
nimare/transforms.py
ADDED
@@ -0,0 +1,907 @@
|
|
1
|
+
"""Miscellaneous spatial and statistical transforms."""
|
2
|
+
|
3
|
+
import copy
|
4
|
+
import logging
|
5
|
+
import os.path as op
|
6
|
+
import warnings
|
7
|
+
|
8
|
+
import nibabel as nib
|
9
|
+
import numpy as np
|
10
|
+
import pandas as pd
|
11
|
+
from nilearn.reporting import get_clusters_table
|
12
|
+
from scipy import stats
|
13
|
+
|
14
|
+
from nimare.base import NiMAREBase
|
15
|
+
from nimare.utils import _dict_to_coordinates, _dict_to_df, _listify, get_masker
|
16
|
+
|
17
|
+
LGR = logging.getLogger(__name__)
|
18
|
+
|
19
|
+
|
20
|
+
class ImageTransformer(NiMAREBase):
|
21
|
+
"""A class to create new images from existing ones within a Dataset.
|
22
|
+
|
23
|
+
This class is a light wrapper around :func:`~nimare.transforms.transform_images`.
|
24
|
+
|
25
|
+
.. versionadded:: 0.0.9
|
26
|
+
|
27
|
+
Parameters
|
28
|
+
----------
|
29
|
+
target : {'z', 'p', 'beta', 'varcope'} or list
|
30
|
+
Target image type. Multiple target types may be specified as a list.
|
31
|
+
overwrite : :obj:`bool`, optional
|
32
|
+
Whether to overwrite existing files or not. Default is False.
|
33
|
+
|
34
|
+
See Also
|
35
|
+
--------
|
36
|
+
nimare.transforms.transform_images : The function called by this class.
|
37
|
+
"""
|
38
|
+
|
39
|
+
def __init__(self, target, overwrite=False):
|
40
|
+
self.target = _listify(target)
|
41
|
+
self.overwrite = overwrite
|
42
|
+
|
43
|
+
def transform(self, dataset):
|
44
|
+
"""Generate images of the target type from other image types in a Dataset.
|
45
|
+
|
46
|
+
Parameters
|
47
|
+
----------
|
48
|
+
dataset : :obj:`~nimare.dataset.Dataset`
|
49
|
+
A Dataset containing images and relevant metadata.
|
50
|
+
|
51
|
+
Returns
|
52
|
+
-------
|
53
|
+
new_dataset : :obj:`~nimare.dataset.Dataset`
|
54
|
+
A copy of the input Dataset, with new images added to its images attribute.
|
55
|
+
"""
|
56
|
+
# Using attribute check instead of type check to allow fake Datasets for testing.
|
57
|
+
if not hasattr(dataset, "slice"):
|
58
|
+
raise ValueError(
|
59
|
+
f"Argument 'dataset' must be a valid Dataset object, not a {type(dataset)}."
|
60
|
+
)
|
61
|
+
|
62
|
+
new_dataset = dataset.copy()
|
63
|
+
temp_images = dataset.images
|
64
|
+
|
65
|
+
for target_type in self.target:
|
66
|
+
temp_images = transform_images(
|
67
|
+
temp_images,
|
68
|
+
target=target_type,
|
69
|
+
masker=dataset.masker,
|
70
|
+
metadata_df=dataset.metadata,
|
71
|
+
out_dir=dataset.basepath,
|
72
|
+
overwrite=self.overwrite,
|
73
|
+
)
|
74
|
+
new_dataset.images = temp_images
|
75
|
+
return new_dataset
|
76
|
+
|
77
|
+
|
78
|
+
def transform_images(images_df, target, masker, metadata_df=None, out_dir=None, overwrite=False):
|
79
|
+
"""Generate images of a given type from other image types and write out to files.
|
80
|
+
|
81
|
+
.. versionchanged:: 0.0.9
|
82
|
+
|
83
|
+
* [ENH] Add overwrite option to transform_images
|
84
|
+
|
85
|
+
.. versionadded:: 0.0.4
|
86
|
+
|
87
|
+
Parameters
|
88
|
+
----------
|
89
|
+
images_df : :class:`pandas.DataFrame`
|
90
|
+
DataFrame with paths to images for studies in Dataset.
|
91
|
+
target : {'z', 'p', 'beta', 'varcope'}
|
92
|
+
Target data type.
|
93
|
+
masker : :class:`~nilearn.input_data.NiftiMasker` or similar
|
94
|
+
Masker used to define orientation and resolution of images.
|
95
|
+
Specific voxels defined in mask will not be used, and a new masker
|
96
|
+
with _all_ voxels in acquisition matrix selected will be created.
|
97
|
+
metadata_df : :class:`pandas.DataFrame` or :obj:`None`, optional
|
98
|
+
DataFrame with metadata. Rows in this DataFrame must match those in
|
99
|
+
``images_df``, including the ``'id'`` column.
|
100
|
+
out_dir : :obj:`str` or :obj:`None`, optional
|
101
|
+
Path to output directory. If None, use folder containing first image
|
102
|
+
for each study in ``images_df``.
|
103
|
+
overwrite : :obj:`bool`, optional
|
104
|
+
Whether to overwrite existing files or not. Default is False.
|
105
|
+
|
106
|
+
Returns
|
107
|
+
-------
|
108
|
+
images_df : :class:`pandas.DataFrame`
|
109
|
+
DataFrame with paths to new images added.
|
110
|
+
"""
|
111
|
+
new_images_df = images_df.copy() # Work on a copy of the images_df
|
112
|
+
|
113
|
+
valid_targets = {"t", "z", "p", "beta", "varcope"}
|
114
|
+
if target not in valid_targets:
|
115
|
+
raise ValueError(
|
116
|
+
f"Target type {target} not supported. Must be one of: {', '.join(valid_targets)}"
|
117
|
+
)
|
118
|
+
|
119
|
+
mask_img = masker.mask_img
|
120
|
+
new_mask = np.ones(mask_img.shape, int)
|
121
|
+
new_mask = nib.Nifti1Image(new_mask, mask_img.affine, header=mask_img.header)
|
122
|
+
new_masker = get_masker(new_mask)
|
123
|
+
res = masker.mask_img.header.get_zooms()
|
124
|
+
res = "x".join([str(r) for r in res])
|
125
|
+
if target not in images_df.columns:
|
126
|
+
target_ids = images_df["id"].values
|
127
|
+
else:
|
128
|
+
target_ids = images_df.loc[images_df[target].isnull(), "id"]
|
129
|
+
|
130
|
+
for id_ in target_ids:
|
131
|
+
row = images_df.loc[images_df["id"] == id_].iloc[0]
|
132
|
+
|
133
|
+
# Determine output filename, if file can be generated
|
134
|
+
if out_dir is None:
|
135
|
+
options = [r for r in row.values if isinstance(r, str) and op.isfile(r)]
|
136
|
+
id_out_dir = op.dirname(options[0])
|
137
|
+
else:
|
138
|
+
id_out_dir = out_dir
|
139
|
+
new_file = op.join(id_out_dir, f"{id_}_{res}_{target}.nii.gz")
|
140
|
+
|
141
|
+
# Grab columns with actual values
|
142
|
+
available_data = row[~row.isnull()].to_dict()
|
143
|
+
if metadata_df is not None:
|
144
|
+
metadata_row = metadata_df.loc[metadata_df["id"] == id_].iloc[0]
|
145
|
+
metadata = metadata_row[~metadata_row.isnull()].to_dict()
|
146
|
+
for k, v in metadata.items():
|
147
|
+
if k not in available_data.keys():
|
148
|
+
available_data[k] = v
|
149
|
+
|
150
|
+
# Get converted data
|
151
|
+
img = resolve_transforms(target, available_data, new_masker)
|
152
|
+
if img is not None:
|
153
|
+
if overwrite or not op.isfile(new_file):
|
154
|
+
img.to_filename(new_file)
|
155
|
+
else:
|
156
|
+
LGR.debug("Image already exists. Not overwriting.")
|
157
|
+
|
158
|
+
new_images_df.loc[new_images_df["id"] == id_, target] = new_file
|
159
|
+
else:
|
160
|
+
new_images_df.loc[new_images_df["id"] == id_, target] = None
|
161
|
+
return new_images_df
|
162
|
+
|
163
|
+
|
164
|
+
def resolve_transforms(target, available_data, masker):
|
165
|
+
"""Determine and apply the appropriate transforms to a target image type from available data.
|
166
|
+
|
167
|
+
.. versionchanged:: 0.0.8
|
168
|
+
|
169
|
+
* [FIX] Remove unnecessary dimensions from output image object *img_like*. \
|
170
|
+
Now, the image object only has 3 dimensions.
|
171
|
+
|
172
|
+
.. versionadded:: 0.0.4
|
173
|
+
|
174
|
+
Parameters
|
175
|
+
----------
|
176
|
+
target : {'z', 'p', 't', 'beta', 'varcope'}
|
177
|
+
Target image type.
|
178
|
+
available_data : dict
|
179
|
+
Dictionary mapping data types to their values. Images in the dictionary
|
180
|
+
are paths to files.
|
181
|
+
masker : nilearn Masker
|
182
|
+
Masker used to convert images to arrays and back. Preferably, this mask
|
183
|
+
should cover the full acquisition matrix (rather than an ROI), given
|
184
|
+
that the calculated images will be saved and used for the full Dataset.
|
185
|
+
|
186
|
+
Returns
|
187
|
+
-------
|
188
|
+
img_like or None
|
189
|
+
Image object with the desired data type, if it can be generated.
|
190
|
+
Otherwise, None.
|
191
|
+
"""
|
192
|
+
if target in available_data.keys():
|
193
|
+
LGR.warning(f"Target '{target}' already available.")
|
194
|
+
return available_data[target]
|
195
|
+
|
196
|
+
if target == "z":
|
197
|
+
if ("t" in available_data.keys()) and ("sample_sizes" in available_data.keys()):
|
198
|
+
dof = sample_sizes_to_dof(available_data["sample_sizes"])
|
199
|
+
t = masker.transform(available_data["t"])
|
200
|
+
z = t_to_z(t, dof)
|
201
|
+
elif "p" in available_data.keys():
|
202
|
+
p = masker.transform(available_data["p"])
|
203
|
+
z = p_to_z(p)
|
204
|
+
else:
|
205
|
+
return None
|
206
|
+
z = masker.inverse_transform(z.squeeze())
|
207
|
+
return z
|
208
|
+
elif target == "t":
|
209
|
+
# will return none given no transform/target exists
|
210
|
+
temp = resolve_transforms("z", available_data, masker)
|
211
|
+
if temp is not None:
|
212
|
+
available_data["z"] = temp
|
213
|
+
|
214
|
+
if ("z" in available_data.keys()) and ("sample_sizes" in available_data.keys()):
|
215
|
+
dof = sample_sizes_to_dof(available_data["sample_sizes"])
|
216
|
+
z = masker.transform(available_data["z"])
|
217
|
+
t = z_to_t(z, dof)
|
218
|
+
t = masker.inverse_transform(t.squeeze())
|
219
|
+
return t
|
220
|
+
else:
|
221
|
+
return None
|
222
|
+
elif target == "beta":
|
223
|
+
if "t" not in available_data.keys():
|
224
|
+
# will return none given no transform/target exists
|
225
|
+
temp = resolve_transforms("t", available_data, masker)
|
226
|
+
if temp is not None:
|
227
|
+
available_data["t"] = temp
|
228
|
+
|
229
|
+
if "varcope" not in available_data.keys():
|
230
|
+
temp = resolve_transforms("varcope", available_data, masker)
|
231
|
+
if temp is not None:
|
232
|
+
available_data["varcope"] = temp
|
233
|
+
|
234
|
+
if ("t" in available_data.keys()) and ("varcope" in available_data.keys()):
|
235
|
+
t = masker.transform(available_data["t"])
|
236
|
+
varcope = masker.transform(available_data["varcope"])
|
237
|
+
beta = t_and_varcope_to_beta(t, varcope)
|
238
|
+
beta = masker.inverse_transform(beta.squeeze())
|
239
|
+
return beta
|
240
|
+
else:
|
241
|
+
return None
|
242
|
+
elif target == "varcope":
|
243
|
+
if "se" in available_data.keys():
|
244
|
+
se = masker.transform(available_data["se"])
|
245
|
+
varcope = se_to_varcope(se)
|
246
|
+
elif ("samplevar_dataset" in available_data.keys()) and (
|
247
|
+
"sample_sizes" in available_data.keys()
|
248
|
+
):
|
249
|
+
sample_size = sample_sizes_to_sample_size(available_data["sample_sizes"])
|
250
|
+
samplevar_dataset = masker.transform(available_data["samplevar_dataset"])
|
251
|
+
varcope = samplevar_dataset_to_varcope(samplevar_dataset, sample_size)
|
252
|
+
elif ("sd" in available_data.keys()) and ("sample_sizes" in available_data.keys()):
|
253
|
+
sample_size = sample_sizes_to_sample_size(available_data["sample_sizes"])
|
254
|
+
sd = masker.transform(available_data["sd"])
|
255
|
+
varcope = sd_to_varcope(sd, sample_size)
|
256
|
+
varcope = masker.inverse_transform(varcope)
|
257
|
+
elif ("t" in available_data.keys()) and ("beta" in available_data.keys()):
|
258
|
+
t = masker.transform(available_data["t"])
|
259
|
+
beta = masker.transform(available_data["beta"])
|
260
|
+
varcope = t_and_beta_to_varcope(t, beta)
|
261
|
+
else:
|
262
|
+
return None
|
263
|
+
varcope = masker.inverse_transform(varcope.squeeze())
|
264
|
+
return varcope
|
265
|
+
elif target == "p":
|
266
|
+
if ("t" in available_data.keys()) and ("sample_sizes" in available_data.keys()):
|
267
|
+
dof = sample_sizes_to_dof(available_data["sample_sizes"])
|
268
|
+
t = masker.transform(available_data["t"])
|
269
|
+
z = t_to_z(t, dof)
|
270
|
+
p = z_to_p(z)
|
271
|
+
elif "z" in available_data.keys():
|
272
|
+
z = masker.transform(available_data["z"])
|
273
|
+
p = z_to_p(z)
|
274
|
+
else:
|
275
|
+
return None
|
276
|
+
p = masker.inverse_transform(p.squeeze())
|
277
|
+
return p
|
278
|
+
else:
|
279
|
+
return None
|
280
|
+
|
281
|
+
|
282
|
+
class ImagesToCoordinates(NiMAREBase):
|
283
|
+
"""Transformer from images to coordinates.
|
284
|
+
|
285
|
+
.. versionadded:: 0.0.8
|
286
|
+
|
287
|
+
Parameters
|
288
|
+
----------
|
289
|
+
merge_strategy : {"fill", "replace", "demolish"}, optional
|
290
|
+
Strategy for how to incorporate the generated coordinates with possible pre-existing
|
291
|
+
coordinates. The available options are
|
292
|
+
|
293
|
+
================ =========================================================================
|
294
|
+
"fill" (default) Only add coordinates to study contrasts that do not have coordinates.
|
295
|
+
If a study contrast has both image and coordinate data, the original
|
296
|
+
coordinate data will be kept.
|
297
|
+
"replace" Replace existing coordinates with coordinates generated by this function.
|
298
|
+
If a study contrast only has coordinate data and no images or if the
|
299
|
+
statistical threshold is too high for nimare to detect any peaks the
|
300
|
+
original coordinates will be kept.
|
301
|
+
"demolish" Only keep generated coordinates and discard any study contrasts with
|
302
|
+
coordinate data, but no images.
|
303
|
+
================ =========================================================================
|
304
|
+
|
305
|
+
cluster_threshold : :obj:`int` or `None`, optional
|
306
|
+
Cluster size threshold, in voxels. Default=None.
|
307
|
+
remove_subpeaks : :obj:`bool`, optional
|
308
|
+
If True, removes subpeaks from the cluster results. Default=False.
|
309
|
+
two_sided : :obj:`bool`, optional
|
310
|
+
Whether to employ two-sided thresholding or to evaluate positive values only.
|
311
|
+
Default=False.
|
312
|
+
min_distance : :obj:`float`, optional
|
313
|
+
Minimum distance between subpeaks in mm. Default=8mm.
|
314
|
+
z_threshold : :obj:`float`
|
315
|
+
Cluster forming z-scale threshold. Default=3.1.
|
316
|
+
|
317
|
+
Notes
|
318
|
+
-----
|
319
|
+
The raw Z and/or P maps are not corrected for multiple comparisons. Uncorrected z-values and/or
|
320
|
+
p-values are used for thresholding.
|
321
|
+
"""
|
322
|
+
|
323
|
+
def __init__(
|
324
|
+
self,
|
325
|
+
merge_strategy="fill",
|
326
|
+
cluster_threshold=None,
|
327
|
+
remove_subpeaks=False,
|
328
|
+
two_sided=False,
|
329
|
+
min_distance=8.0,
|
330
|
+
z_threshold=3.1,
|
331
|
+
):
|
332
|
+
self.merge_strategy = merge_strategy
|
333
|
+
self.cluster_threshold = cluster_threshold
|
334
|
+
self.remove_subpeaks = remove_subpeaks
|
335
|
+
self.min_distance = min_distance
|
336
|
+
self.two_sided = two_sided
|
337
|
+
self.z_threshold = z_threshold
|
338
|
+
|
339
|
+
def transform(self, dataset):
|
340
|
+
"""Create coordinate peaks from statistical images.
|
341
|
+
|
342
|
+
Parameters
|
343
|
+
----------
|
344
|
+
dataset : :obj:`~nimare.dataset.Dataset`
|
345
|
+
Dataset with z maps and/or p maps
|
346
|
+
that can be converted to coordinates.
|
347
|
+
|
348
|
+
Returns
|
349
|
+
-------
|
350
|
+
dataset : :obj:`~nimare.dataset.Dataset`
|
351
|
+
Dataset with coordinates generated from
|
352
|
+
images and metadata indicating origin
|
353
|
+
of coordinates ('original' or 'nimare').
|
354
|
+
"""
|
355
|
+
# relevant variables from dataset
|
356
|
+
space = dataset.space
|
357
|
+
masker = dataset.masker
|
358
|
+
images_df = dataset.images
|
359
|
+
metadata = dataset.metadata.copy()
|
360
|
+
|
361
|
+
# conform space specification
|
362
|
+
if "mni" in space.lower() or "ale" in space.lower():
|
363
|
+
coordinate_space = "MNI"
|
364
|
+
elif "tal" in space.lower():
|
365
|
+
coordinate_space = "TAL"
|
366
|
+
else:
|
367
|
+
coordinate_space = None
|
368
|
+
|
369
|
+
coordinates_dict = {}
|
370
|
+
for _, row in images_df.iterrows():
|
371
|
+
if row["id"] in list(dataset.coordinates["id"]) and self.merge_strategy == "fill":
|
372
|
+
continue
|
373
|
+
|
374
|
+
if row.get("z"):
|
375
|
+
clusters = get_clusters_table(
|
376
|
+
nib.funcs.squeeze_image(nib.load(row.get("z"))),
|
377
|
+
self.z_threshold,
|
378
|
+
self.cluster_threshold,
|
379
|
+
self.two_sided,
|
380
|
+
self.min_distance,
|
381
|
+
)
|
382
|
+
elif row.get("p"):
|
383
|
+
LGR.info(
|
384
|
+
f"No Z map for {row['id']}, using p map "
|
385
|
+
"(p-values will be treated as positive z-values)"
|
386
|
+
)
|
387
|
+
if self.two_sided:
|
388
|
+
LGR.warning(f"Cannot use two_sided threshold using a p map for {row['id']}")
|
389
|
+
|
390
|
+
p_threshold = 1 - z_to_p(self.z_threshold)
|
391
|
+
nimg = nib.funcs.squeeze_image(nib.load(row.get("p")))
|
392
|
+
inv_nimg = nib.Nifti1Image(1 - nimg.get_fdata(), nimg.affine, nimg.header)
|
393
|
+
clusters = get_clusters_table(
|
394
|
+
inv_nimg,
|
395
|
+
p_threshold,
|
396
|
+
self.cluster_threshold,
|
397
|
+
self.min_distance,
|
398
|
+
)
|
399
|
+
# Peak stat p-values are reported as 1 - p in get_clusters_table
|
400
|
+
clusters["Peak Stat"] = p_to_z(1 - clusters["Peak Stat"])
|
401
|
+
else:
|
402
|
+
LGR.warning(f"No Z or p map for {row['id']}, skipping...")
|
403
|
+
continue
|
404
|
+
|
405
|
+
# skip entry if no clusters are found
|
406
|
+
if clusters.empty:
|
407
|
+
LGR.warning(
|
408
|
+
f"No clusters were found for {row['id']} at a threshold of {self.z_threshold}"
|
409
|
+
)
|
410
|
+
continue
|
411
|
+
|
412
|
+
if self.remove_subpeaks:
|
413
|
+
# subpeaks are identified as 1a, 1b, etc
|
414
|
+
# while peaks are kept as 1, 2, 3, etc,
|
415
|
+
# so removing all non-int rows will
|
416
|
+
# keep main peaks while removing subpeaks
|
417
|
+
clusters = clusters[clusters["Cluster ID"].apply(lambda x: isinstance(x, int))]
|
418
|
+
|
419
|
+
coordinates_dict[row["study_id"]] = {
|
420
|
+
"contrasts": {
|
421
|
+
row["contrast_id"]: {
|
422
|
+
"coords": {
|
423
|
+
"space": coordinate_space,
|
424
|
+
"x": list(clusters["X"]),
|
425
|
+
"y": list(clusters["Y"]),
|
426
|
+
"z": list(clusters["Z"]),
|
427
|
+
"z_stat": list(clusters["Peak Stat"]),
|
428
|
+
},
|
429
|
+
"metadata": {"coordinate_source": "nimare"},
|
430
|
+
}
|
431
|
+
}
|
432
|
+
}
|
433
|
+
|
434
|
+
# only the generated coordinates ('demolish')
|
435
|
+
coordinates_df = _dict_to_coordinates(coordinates_dict, masker, space)
|
436
|
+
meta_df = _dict_to_df(
|
437
|
+
pd.DataFrame(dataset._ids),
|
438
|
+
coordinates_dict,
|
439
|
+
"metadata",
|
440
|
+
)
|
441
|
+
|
442
|
+
if "coordinate_source" in meta_df.columns:
|
443
|
+
metadata["coordinate_source"] = meta_df["coordinate_source"]
|
444
|
+
else:
|
445
|
+
# nimare did not overwrite any coordinates
|
446
|
+
metadata["coordinate_source"] = ["original"] * metadata.shape[0]
|
447
|
+
|
448
|
+
if self.merge_strategy != "demolish":
|
449
|
+
original_idxs = ~dataset.coordinates["id"].isin(coordinates_df["id"])
|
450
|
+
old_coordinates_df = dataset.coordinates[original_idxs]
|
451
|
+
coordinates_df = pd.concat([coordinates_df, old_coordinates_df], ignore_index=True)
|
452
|
+
|
453
|
+
# specify original coordinates
|
454
|
+
original_ids = set(old_coordinates_df["id"])
|
455
|
+
metadata.loc[metadata["id"].isin(original_ids), "coordinate_source"] = "original"
|
456
|
+
|
457
|
+
if "z_stat" in coordinates_df.columns:
|
458
|
+
# ensure z_stat is treated as float
|
459
|
+
coordinates_df["z_stat"] = coordinates_df["z_stat"].astype(float)
|
460
|
+
|
461
|
+
# Raise warning if coordinates dataset contains both positive and negative z_stats
|
462
|
+
if ((coordinates_df["z_stat"].values >= 0).any()) and (
|
463
|
+
(coordinates_df["z_stat"].values < 0).any()
|
464
|
+
):
|
465
|
+
warnings.warn(
|
466
|
+
"Coordinates dataset contains both positive and negative z_stats. "
|
467
|
+
"The algorithms currently implemented in NiMARE are designed for "
|
468
|
+
"one-sided tests. This might lead to unexpected results."
|
469
|
+
)
|
470
|
+
|
471
|
+
new_dataset = copy.deepcopy(dataset)
|
472
|
+
new_dataset.coordinates = coordinates_df
|
473
|
+
new_dataset.metadata = metadata
|
474
|
+
|
475
|
+
return new_dataset
|
476
|
+
|
477
|
+
|
478
|
+
class StandardizeField(NiMAREBase):
|
479
|
+
"""Standardize metadata fields."""
|
480
|
+
|
481
|
+
def __init__(self, fields):
|
482
|
+
self.fields = fields # the fields to be standardized
|
483
|
+
|
484
|
+
def transform(self, dataset):
|
485
|
+
"""Standardize metadata fields."""
|
486
|
+
# update a copy of the dataset
|
487
|
+
dataset = dataset.copy()
|
488
|
+
|
489
|
+
categorical_metadata, numerical_metadata = [], []
|
490
|
+
for metadata_name in self.fields:
|
491
|
+
if np.array_equal(
|
492
|
+
dataset.annotations[metadata_name], dataset.annotations[metadata_name].astype(str)
|
493
|
+
):
|
494
|
+
categorical_metadata.append(metadata_name)
|
495
|
+
elif np.array_equal(
|
496
|
+
dataset.annotations[metadata_name],
|
497
|
+
dataset.annotations[metadata_name].astype(float),
|
498
|
+
):
|
499
|
+
numerical_metadata.append(metadata_name)
|
500
|
+
if len(categorical_metadata) > 0:
|
501
|
+
LGR.warning(f"Categorical metadata {categorical_metadata} can't be standardized.")
|
502
|
+
if len(numerical_metadata) == 0:
|
503
|
+
raise ValueError("No numerical metadata found.")
|
504
|
+
|
505
|
+
moderators = dataset.annotations[numerical_metadata]
|
506
|
+
standardize_moderators = moderators - np.mean(moderators, axis=0)
|
507
|
+
standardize_moderators /= np.std(standardize_moderators, axis=0)
|
508
|
+
if isinstance(self.fields, str):
|
509
|
+
column_name = "standardized_" + self.fields
|
510
|
+
elif isinstance(self.fields, list):
|
511
|
+
column_name = ["standardized_" + moderator for moderator in numerical_metadata]
|
512
|
+
dataset.annotations[column_name] = standardize_moderators
|
513
|
+
|
514
|
+
return dataset
|
515
|
+
|
516
|
+
|
517
|
+
def sample_sizes_to_dof(sample_sizes):
|
518
|
+
"""Calculate degrees of freedom from a list of sample sizes using a simple heuristic.
|
519
|
+
|
520
|
+
.. versionadded:: 0.0.4
|
521
|
+
|
522
|
+
Parameters
|
523
|
+
----------
|
524
|
+
sample_sizes : array_like
|
525
|
+
A list of sample sizes for different groups in the study.
|
526
|
+
|
527
|
+
Returns
|
528
|
+
-------
|
529
|
+
dof : int
|
530
|
+
An estimate of degrees of freedom. Number of participants minus number
|
531
|
+
of groups.
|
532
|
+
"""
|
533
|
+
dof = np.sum(sample_sizes) - len(sample_sizes)
|
534
|
+
return dof
|
535
|
+
|
536
|
+
|
537
|
+
def sample_sizes_to_sample_size(sample_sizes):
|
538
|
+
"""Calculate appropriate sample size from a list of sample sizes using a simple heuristic.
|
539
|
+
|
540
|
+
.. versionadded:: 0.0.4
|
541
|
+
|
542
|
+
Parameters
|
543
|
+
----------
|
544
|
+
sample_sizes : array_like
|
545
|
+
A list of sample sizes for different groups in the study.
|
546
|
+
|
547
|
+
Returns
|
548
|
+
-------
|
549
|
+
sample_size : int
|
550
|
+
Total (sum) sample size.
|
551
|
+
"""
|
552
|
+
sample_size = np.sum(sample_sizes)
|
553
|
+
return sample_size
|
554
|
+
|
555
|
+
|
556
|
+
def sd_to_varcope(sd, sample_size):
|
557
|
+
"""Convert standard deviation to sampling variance.
|
558
|
+
|
559
|
+
.. versionadded:: 0.0.3
|
560
|
+
|
561
|
+
Parameters
|
562
|
+
----------
|
563
|
+
sd : array_like
|
564
|
+
Standard deviation of the sample
|
565
|
+
sample_size : int
|
566
|
+
Sample size
|
567
|
+
|
568
|
+
Returns
|
569
|
+
-------
|
570
|
+
varcope : array_like
|
571
|
+
Sampling variance of the parameter
|
572
|
+
"""
|
573
|
+
se = sd / np.sqrt(sample_size)
|
574
|
+
varcope = se_to_varcope(se)
|
575
|
+
return varcope
|
576
|
+
|
577
|
+
|
578
|
+
def se_to_varcope(se):
|
579
|
+
"""Convert standard error values to sampling variance.
|
580
|
+
|
581
|
+
.. versionadded:: 0.0.3
|
582
|
+
|
583
|
+
Parameters
|
584
|
+
----------
|
585
|
+
se : array_like
|
586
|
+
Standard error of the sample parameter
|
587
|
+
|
588
|
+
Returns
|
589
|
+
-------
|
590
|
+
varcope : array_like
|
591
|
+
Sampling variance of the parameter
|
592
|
+
|
593
|
+
Notes
|
594
|
+
-----
|
595
|
+
Sampling variance is standard error squared.
|
596
|
+
"""
|
597
|
+
varcope = se**2
|
598
|
+
return varcope
|
599
|
+
|
600
|
+
|
601
|
+
def samplevar_dataset_to_varcope(samplevar_dataset, sample_size):
|
602
|
+
"""Convert "sample variance of the dataset" to "sampling variance".
|
603
|
+
|
604
|
+
.. versionadded:: 0.0.3
|
605
|
+
|
606
|
+
Parameters
|
607
|
+
----------
|
608
|
+
samplevar_dataset : array_like
|
609
|
+
Sample variance of the dataset (i.e., variance of the individual observations in a single
|
610
|
+
sample). Can be calculated with ``np.var``.
|
611
|
+
sample_size : int
|
612
|
+
Sample size
|
613
|
+
|
614
|
+
Returns
|
615
|
+
-------
|
616
|
+
varcope : array_like
|
617
|
+
Sampling variance of the parameter (i.e., variance of sampling distribution for the
|
618
|
+
parameter).
|
619
|
+
|
620
|
+
Notes
|
621
|
+
-----
|
622
|
+
Sampling variance is sample variance divided by sample size.
|
623
|
+
"""
|
624
|
+
varcope = samplevar_dataset / sample_size
|
625
|
+
return varcope
|
626
|
+
|
627
|
+
|
628
|
+
def t_and_varcope_to_beta(t, varcope):
|
629
|
+
"""Convert t-statistic to parameter estimate using sampling variance.
|
630
|
+
|
631
|
+
.. versionadded:: 0.0.3
|
632
|
+
|
633
|
+
Parameters
|
634
|
+
----------
|
635
|
+
t : array_like
|
636
|
+
T-statistics of the parameter
|
637
|
+
varcope : array_like
|
638
|
+
Sampling variance of the parameter
|
639
|
+
|
640
|
+
Returns
|
641
|
+
-------
|
642
|
+
beta : array_like
|
643
|
+
Parameter estimates
|
644
|
+
"""
|
645
|
+
beta = t * np.sqrt(varcope)
|
646
|
+
return beta
|
647
|
+
|
648
|
+
|
649
|
+
def t_and_beta_to_varcope(t, beta):
|
650
|
+
"""Convert t-statistic to sampling variance using parameter estimate.
|
651
|
+
|
652
|
+
.. versionadded:: 0.0.4
|
653
|
+
|
654
|
+
Parameters
|
655
|
+
----------
|
656
|
+
t : array_like
|
657
|
+
T-statistics of the parameter
|
658
|
+
beta : array_like
|
659
|
+
Parameter estimates
|
660
|
+
|
661
|
+
Returns
|
662
|
+
-------
|
663
|
+
varcope : array_like
|
664
|
+
Sampling variance of the parameter
|
665
|
+
"""
|
666
|
+
varcope = (beta / t) ** 2
|
667
|
+
return varcope
|
668
|
+
|
669
|
+
|
670
|
+
def z_to_p(z, tail="two"):
|
671
|
+
"""Convert z-values to p-values.
|
672
|
+
|
673
|
+
.. versionadded:: 0.0.8
|
674
|
+
|
675
|
+
Parameters
|
676
|
+
----------
|
677
|
+
z : array_like
|
678
|
+
Z-statistics
|
679
|
+
tail : {'one', 'two'}, optional
|
680
|
+
Whether p-values come from one-tailed or two-tailed test. Default is
|
681
|
+
'two'.
|
682
|
+
|
683
|
+
Returns
|
684
|
+
-------
|
685
|
+
p : array_like
|
686
|
+
P-values
|
687
|
+
"""
|
688
|
+
z = np.array(z)
|
689
|
+
if tail == "two":
|
690
|
+
p = stats.norm.sf(abs(z)) * 2
|
691
|
+
elif tail == "one":
|
692
|
+
p = stats.norm.sf(z)
|
693
|
+
else:
|
694
|
+
raise ValueError('Argument "tail" must be one of ["one", "two"]')
|
695
|
+
|
696
|
+
if p.shape == ():
|
697
|
+
p = p[()]
|
698
|
+
return p
|
699
|
+
|
700
|
+
|
701
|
+
def p_to_z(p, tail="two"):
|
702
|
+
"""Convert p-values to (unsigned) z-values.
|
703
|
+
|
704
|
+
.. versionadded:: 0.0.3
|
705
|
+
|
706
|
+
Parameters
|
707
|
+
----------
|
708
|
+
p : array_like
|
709
|
+
P-values
|
710
|
+
tail : {'one', 'two'}, optional
|
711
|
+
Whether p-values come from one-tailed or two-tailed test. Default is
|
712
|
+
'two'.
|
713
|
+
|
714
|
+
Returns
|
715
|
+
-------
|
716
|
+
z : array_like
|
717
|
+
Z-statistics (unsigned)
|
718
|
+
"""
|
719
|
+
p = np.array(p)
|
720
|
+
if tail == "two":
|
721
|
+
z = stats.norm.isf(p / 2)
|
722
|
+
elif tail == "one":
|
723
|
+
z = stats.norm.isf(p)
|
724
|
+
z = np.array(z)
|
725
|
+
z[z < 0] = 0
|
726
|
+
else:
|
727
|
+
raise ValueError('Argument "tail" must be one of ["one", "two"]')
|
728
|
+
|
729
|
+
if z.shape == ():
|
730
|
+
z = z[()]
|
731
|
+
return z
|
732
|
+
|
733
|
+
|
734
|
+
def t_to_z(t_values, dof):
|
735
|
+
"""Convert t-statistics to z-statistics.
|
736
|
+
|
737
|
+
.. versionadded:: 0.0.3
|
738
|
+
|
739
|
+
An implementation of :footcite:t:`hughett2008accurate` from Vanessa Sochat's TtoZ package
|
740
|
+
:footcite:p:`sochat2015ttoz`.
|
741
|
+
|
742
|
+
Parameters
|
743
|
+
----------
|
744
|
+
t_values : array_like
|
745
|
+
T-statistics
|
746
|
+
dof : int
|
747
|
+
Degrees of freedom
|
748
|
+
|
749
|
+
Returns
|
750
|
+
-------
|
751
|
+
z_values : array_like
|
752
|
+
Z-statistics
|
753
|
+
|
754
|
+
License
|
755
|
+
-------
|
756
|
+
The MIT License (MIT)
|
757
|
+
Copyright (c) 2015 Vanessa Sochat
|
758
|
+
|
759
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
760
|
+
and associated documentation files (the "Software"), to deal in the Software without
|
761
|
+
restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
762
|
+
distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
763
|
+
Software is furnished to do so, subject to the following conditions:
|
764
|
+
|
765
|
+
The above copyright notice and this permission notice shall be included in all copies or
|
766
|
+
substantial portions of the Software.
|
767
|
+
|
768
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
769
|
+
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
770
|
+
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
771
|
+
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
772
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
773
|
+
SOFTWARE.
|
774
|
+
|
775
|
+
References
|
776
|
+
----------
|
777
|
+
.. footbibliography::
|
778
|
+
"""
|
779
|
+
# Select just the nonzero voxels
|
780
|
+
nonzero = t_values[t_values != 0]
|
781
|
+
|
782
|
+
# We will store our results here
|
783
|
+
z_values_nonzero = np.zeros(len(nonzero))
|
784
|
+
|
785
|
+
# Select values less than or == 0, and greater than zero
|
786
|
+
c = np.zeros(len(nonzero))
|
787
|
+
k1 = nonzero <= c
|
788
|
+
k2 = nonzero > c
|
789
|
+
|
790
|
+
# Subset the data into two sets
|
791
|
+
t1 = nonzero[k1]
|
792
|
+
t2 = nonzero[k2]
|
793
|
+
|
794
|
+
# Calculate p values for <=0
|
795
|
+
p_values_t1 = stats.t.cdf(t1, df=dof)
|
796
|
+
p_values_t1[p_values_t1 < np.finfo(p_values_t1.dtype).eps] = np.finfo(p_values_t1.dtype).eps
|
797
|
+
z_values_t1 = stats.norm.ppf(p_values_t1)
|
798
|
+
|
799
|
+
# Calculate p values for > 0
|
800
|
+
p_values_t2 = stats.t.cdf(-t2, df=dof)
|
801
|
+
p_values_t2[p_values_t2 < np.finfo(p_values_t2.dtype).eps] = np.finfo(p_values_t2.dtype).eps
|
802
|
+
z_values_t2 = -stats.norm.ppf(p_values_t2)
|
803
|
+
z_values_nonzero[k1] = z_values_t1
|
804
|
+
z_values_nonzero[k2] = z_values_t2
|
805
|
+
|
806
|
+
z_values = np.zeros(t_values.shape)
|
807
|
+
z_values[t_values != 0] = z_values_nonzero
|
808
|
+
return z_values
|
809
|
+
|
810
|
+
|
811
|
+
def z_to_t(z_values, dof):
|
812
|
+
"""Convert z-statistics to t-statistics.
|
813
|
+
|
814
|
+
.. versionadded:: 0.0.3
|
815
|
+
|
816
|
+
An inversion of the t_to_z implementation of :footcite:t:`hughett2008accurate` from
|
817
|
+
Vanessa Sochat's TtoZ package :footcite:p:`sochat2015ttoz`.
|
818
|
+
|
819
|
+
Parameters
|
820
|
+
----------
|
821
|
+
z_values : array_like
|
822
|
+
Z-statistics
|
823
|
+
dof : int
|
824
|
+
Degrees of freedom
|
825
|
+
|
826
|
+
Returns
|
827
|
+
-------
|
828
|
+
t_values : array_like
|
829
|
+
T-statistics
|
830
|
+
|
831
|
+
References
|
832
|
+
----------
|
833
|
+
.. footbibliography::
|
834
|
+
"""
|
835
|
+
# Select just the nonzero voxels
|
836
|
+
nonzero = z_values[z_values != 0]
|
837
|
+
|
838
|
+
# We will store our results here
|
839
|
+
t_values_nonzero = np.zeros(len(nonzero))
|
840
|
+
|
841
|
+
# Select values less than or == 0, and greater than zero
|
842
|
+
c = np.zeros(len(nonzero))
|
843
|
+
k1 = nonzero <= c
|
844
|
+
k2 = nonzero > c
|
845
|
+
|
846
|
+
# Subset the data into two sets
|
847
|
+
z1 = nonzero[k1]
|
848
|
+
z2 = nonzero[k2]
|
849
|
+
|
850
|
+
# Calculate p values for <=0
|
851
|
+
p_values_z1 = stats.norm.cdf(z1)
|
852
|
+
t_values_z1 = stats.t.ppf(p_values_z1, df=dof)
|
853
|
+
|
854
|
+
# Calculate p values for > 0
|
855
|
+
p_values_z2 = stats.norm.cdf(-z2)
|
856
|
+
t_values_z2 = -stats.t.ppf(p_values_z2, df=dof)
|
857
|
+
t_values_nonzero[k1] = t_values_z1
|
858
|
+
t_values_nonzero[k2] = t_values_z2
|
859
|
+
|
860
|
+
t_values = np.zeros(z_values.shape)
|
861
|
+
t_values[z_values != 0] = t_values_nonzero
|
862
|
+
return t_values
|
863
|
+
|
864
|
+
|
865
|
+
def t_to_d(t_values, sample_sizes):
|
866
|
+
"""Convert t-statistics to Cohen's d.
|
867
|
+
|
868
|
+
Parameters
|
869
|
+
----------
|
870
|
+
t_values : array_like
|
871
|
+
T-statistics
|
872
|
+
sample_sizes : array_like
|
873
|
+
Sample sizes
|
874
|
+
|
875
|
+
Returns
|
876
|
+
-------
|
877
|
+
d_values : array_like
|
878
|
+
Cohen's d
|
879
|
+
"""
|
880
|
+
d_values = t_values / np.sqrt(sample_sizes)
|
881
|
+
return d_values
|
882
|
+
|
883
|
+
|
884
|
+
def d_to_g(d, N, return_variance=False):
|
885
|
+
"""Convert Cohen's d to Hedges' g.
|
886
|
+
|
887
|
+
Parameters
|
888
|
+
----------
|
889
|
+
d : array_like
|
890
|
+
Cohen's d
|
891
|
+
N : array_like
|
892
|
+
Sample sizes
|
893
|
+
return_variance : bool, optional
|
894
|
+
Whether to return the variance of Hedges' g. Default is False.
|
895
|
+
|
896
|
+
Returns
|
897
|
+
-------
|
898
|
+
g_values : array_like
|
899
|
+
Hedges' g
|
900
|
+
"""
|
901
|
+
# Calculate bias correction h(N)
|
902
|
+
h = 1 - (3 / (4 * (N - 1) - 1))
|
903
|
+
|
904
|
+
if return_variance:
|
905
|
+
return d * h, ((N - 1) * (1 + N * d**2) * (h**2) / (N * (N - 3))) - d**2
|
906
|
+
|
907
|
+
return d * h
|