Skip to content

Commit 6a1cd22

Browse files
authored
[ENH] add --normalize-betas option (#299)
* refactor _calc_beta_maps to allow for tstats output * test tstat/norm_beta functionality * propagate norm_betas through the workflow and add tests
1 parent 245575d commit 6a1cd22

File tree

8 files changed

+153
-54
lines changed

8 files changed

+153
-54
lines changed

docs/workflows.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Participant Workflow
2323
hrf_model='glover',
2424
high_pass=0.008,
2525
name='subtest',
26+
norm_betas=False,
2627
output_dir='.',
2728
preproc_img_list=[''],
2829
selected_confounds=[''],
@@ -54,6 +55,7 @@ Least Squares- Separate (LSS)
5455
fir_delays=None,
5556
hrf_model='glover',
5657
high_pass=0.008,
58+
norm_betas=False,
5759
smoothing_kernel=0.0,
5860
signal_scaling=0,
5961
selected_confounds=[''])
@@ -76,6 +78,7 @@ Finite BOLD Response- Separate (FS)
7678
fir_delays=[0, 1, 2, 3, 4],
7779
hrf_model='fir',
7880
high_pass=0.008,
81+
norm_betas=False,
7982
smoothing_kernel=0.0,
8083
signal_scaling=0,
8184
selected_confounds=[''])
@@ -99,6 +102,7 @@ Least Squares- All (LSA)
99102
fir_delays=None,
100103
hrf_model='glover',
101104
high_pass=0.008,
105+
norm_betas=False,
102106
smoothing_kernel=0.0,
103107
signal_scaling=0,
104108
selected_confounds=[''])

src/nibetaseries/cli/run.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@ def get_parser():
106106
help='do not scale every voxel with respect to time meaning'
107107
' beta estimates will be in the same units as the bold'
108108
' signal')
109+
proc_opts.add_argument('--normalize-betas', action='store_true', default=False,
110+
help='beta estimates will be divided by the square root '
111+
'of their variance')
109112

110113
# Image Selection options
111114
bids_opts = parser.add_argument_group('Options for selecting images')
@@ -260,6 +263,7 @@ def main():
260263
fir_delays=opts.fir_delays,
261264
hrf_model=opts.hrf_model,
262265
high_pass=opts.high_pass,
266+
norm_betas=opts.normalize_betas,
263267
output_dir=output_dir,
264268
run_label=opts.run_label,
265269
signal_scaling=signal_scaling,

src/nibetaseries/cli/tests/test_run.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,18 @@ def test_conditional_arguments(monkeypatch):
3636
get_parser().parse_args(no_img)
3737

3838

39-
@pytest.mark.parametrize("use_atlas,estimator,fir_delays,hrf_model,part_label,use_signal_scaling",
40-
[(True, 'lsa', None, 'spm', '01', True),
41-
(False, 'lss', None, 'spm', 'sub-01', False),
42-
(True, 'lss', [0, 1, 2, 3, 4], 'fir', None, False)])
39+
@pytest.mark.parametrize(
40+
"use_atlas,estimator,fir_delays,hrf_model,part_label,use_signal_scaling,norm_betas",
41+
[
42+
(True, 'lsa', None, 'spm', '01', True, True),
43+
(False, 'lss', None, 'spm', 'sub-01', False, False),
44+
(True, 'lss', [0, 1, 2, 3, 4], 'fir', None, False, True)
45+
])
4346
def test_nibs(
4447
bids_dir, deriv_dir, sub_fmriprep, sub_metadata, bold_file, preproc_file,
4548
sub_events, confounds_file, brainmask_file, atlas_file, atlas_lut,
4649
estimator, fir_delays, hrf_model, monkeypatch, part_label, use_atlas,
47-
use_signal_scaling):
50+
use_signal_scaling, norm_betas):
4851
import sys
4952
bids_dir = str(bids_dir)
5053
out_dir = os.path.join(bids_dir, 'derivatives')
@@ -65,6 +68,8 @@ def test_nibs(
6568
out_dir,
6669
"participant",
6770
"-c", ".*derivative.*"]
71+
if norm_betas:
72+
args.extend(['--normalize-betas'])
6873
if use_signal_scaling:
6974
args.extend(["--no-signal-scaling"])
7075
if use_atlas:

src/nibetaseries/interfaces/nistats.py

Lines changed: 74 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class LSSBetaSeriesInputSpec(BaseInterfaceInputSpec):
3939
fir_delays = traits.Either(None, traits.List(traits.Int),
4040
desc="FIR delays (in scans)",
4141
default=None, usedefault=True)
42+
return_tstat = traits.Bool(desc="use the T-statistic instead of the raw beta estimates")
4243

4344

4445
class LSSBetaSeriesOutputSpec(TraitedSpec):
@@ -99,15 +100,20 @@ def _run_interface(self, runtime):
99100
delay_ttype = trial_type+'_delay_{}'.format(delay)
100101
new_delay_ttype = delay_ttype.replace('_delay_{}'.format(delay),
101102
'Delay{}Vol'.format(delay))
102-
beta_map = model.compute_contrast(
103-
delay_ttype, output_type='effect_size')
103+
beta_map = _calc_beta_map(model,
104+
delay_ttype,
105+
self.inputs.hrf_model,
106+
self.inputs.return_tstat)
104107
if new_delay_ttype in beta_maps:
105108
beta_maps[new_delay_ttype].append(beta_map)
106109
else:
107110
beta_maps[new_delay_ttype] = [beta_map]
108111
else:
109112
# calculate the beta map
110-
beta_map = _calc_beta_map(model, trial_type, self.inputs.hrf_model)
113+
beta_map = _calc_beta_map(model,
114+
trial_type,
115+
self.inputs.hrf_model,
116+
self.inputs.return_tstat)
111117
design_matrix_collector[trial_idx] = model.design_matrices_[0]
112118
# assign beta map to appropriate list
113119
if trial_type in beta_maps:
@@ -168,6 +174,7 @@ class LSABetaSeriesInputSpec(BaseInterfaceInputSpec):
168174
smoothing_kernel = traits.Either(None, traits.Float(),
169175
desc="full wide half max smoothing kernel")
170176
high_pass = traits.Float(0.0078125, desc="the high pass filter (Hz)")
177+
return_tstat = traits.Bool(desc="use the T-statistic instead of the raw beta estimates")
171178

172179

173180
class LSABetaSeriesOutputSpec(TraitedSpec):
@@ -219,7 +226,10 @@ def _run_interface(self, runtime):
219226
t_type = lsa_df.loc[i_trial, 'original_trial_type']
220227

221228
# calculate the beta map
222-
beta_map = _calc_beta_map(model, t_name, self.inputs.hrf_model)
229+
beta_map = _calc_beta_map(model,
230+
t_name,
231+
self.inputs.hrf_model,
232+
self.inputs.return_tstat)
223233

224234
# assign beta map to appropriate list
225235
if t_type in beta_maps:
@@ -352,7 +362,7 @@ def _select_confounds(confounds_file, selected_confounds):
352362
return desired_confounds
353363

354364

355-
def _calc_beta_map(model, trial_type, hrf_model):
365+
def _calc_beta_map(model, trial_type, hrf_model, tstat):
356366
"""
357367
Calculates the beta estimates for every voxel from
358368
a nistats model
@@ -361,6 +371,12 @@ def _calc_beta_map(model, trial_type, hrf_model):
361371
----------
362372
model : nistats.first_level_model.FirstLevelModel
363373
a fit model of the first level results
374+
trial_type : str
375+
the trial to create the beta estimate
376+
hrf_model : str
377+
the hemondynamic response function used to fit the model
378+
tstat : bool
379+
return the t-statistic for the betas instead of the raw estimates
364380
365381
Returns
366382
-------
@@ -369,32 +385,68 @@ def _calc_beta_map(model, trial_type, hrf_model):
369385
"""
370386
import numpy as np
371387

388+
# make it so we do not divide by zero
389+
TINY = 1e-50
390+
raw_beta_map = _estimate_map(model, trial_type, hrf_model, 'effect_size')
391+
if tstat:
392+
var_map = _estimate_map(model, trial_type, hrf_model, 'effect_variance')
393+
tstat_array = raw_beta_map.get_fdata() / np.sqrt(np.maximum(var_map.get_fdata(), TINY))
394+
return nib.Nifti2Image(tstat_array, raw_beta_map.affine, raw_beta_map.header)
395+
else:
396+
return raw_beta_map
397+
398+
399+
def _estimate_map(model, trial_type, hrf_model, output_type):
400+
"""
401+
Calculates model output for every voxel from
402+
a nistats model
403+
404+
Parameters
405+
----------
406+
model : nistats.first_level_model.FirstLevelModel
407+
a fit model of the first level results
408+
trial_type : str
409+
the trial to create the beta estimate
410+
hrf_model : str
411+
the hemondynamic response function used to fit the model
412+
output_type : str
413+
Type of the output map.
414+
Can be ‘z_score’, ‘stat’, ‘p_value’, ‘effect_size’, or ‘effect_variance’
415+
416+
Returns
417+
-------
418+
map_img : nibabel.nifti2.Nifti2Image
419+
nifti image containing voxelwise output_type estimates
420+
"""
421+
import numpy as np
422+
372423
# calculate the beta map
373-
beta_map_list = []
374-
beta_map_base = model.compute_contrast(trial_type, output_type='effect_size')
375-
beta_map_list.append(beta_map_base.get_fdata())
376-
beta_sign = np.where(beta_map_list[0] < 0, -1, 1)
424+
map_list = []
425+
map_base = model.compute_contrast(trial_type, output_type=output_type)
426+
map_list.append(map_base.get_fdata())
427+
sign = np.where(map_list[0] < 0, -1, 1)
377428
if 'derivative' in hrf_model:
378429
td_contrast = '_'.join([trial_type, 'derivative'])
379-
beta_map_list.append(
430+
map_list.append(
380431
model.compute_contrast(
381-
td_contrast, output_type='effect_size').get_fdata())
432+
td_contrast, output_type=output_type).get_fdata())
382433
if 'dispersion' in hrf_model:
383434
dd_contrast = '_'.join([trial_type, 'dispersion'])
384-
beta_map_list.append(
435+
map_list.append(
385436
model.compute_contrast(
386-
dd_contrast, output_type='effect_size').get_fdata())
437+
dd_contrast, output_type=output_type).get_fdata())
387438

388-
if len(beta_map_list) == 1:
389-
beta_map = beta_map_base
439+
if len(map_list) == 1:
440+
map_img = map_base
390441
else:
391-
beta_map_array = beta_sign * \
442+
map_array = sign * \
392443
np.sqrt(
393444
np.sum(
394-
np.array([np.power(c, 2) for c in beta_map_list]), axis=0))
395-
beta_map = nib.Nifti2Image(
396-
beta_map_array,
397-
beta_map_base.affine,
398-
beta_map_base.header)
445+
np.array([np.power(c, 2) for c in map_list]), axis=0))
446+
447+
map_img = nib.Nifti2Image(
448+
map_array,
449+
map_base.affine,
450+
map_base.header)
399451

400-
return beta_map
452+
return map_img

src/nibetaseries/interfaces/tests/test_nistats.py

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,16 @@
1717

1818

1919
@pytest.mark.parametrize(
20-
"use_nibabel,hrf_model",
20+
"use_nibabel,hrf_model,return_tstat",
2121
[
22-
(True, 'spm'),
23-
(False, 'spm + derivative'),
24-
(False, 'spm + derivative + dispersion')
22+
(True, 'spm', True),
23+
(False, 'spm + derivative', False),
24+
(False, 'spm + derivative + dispersion', True)
2525
]
2626
)
2727
def test_lss_beta_series(sub_metadata, preproc_file, sub_events,
2828
confounds_file, brainmask_file, use_nibabel,
29-
hrf_model):
29+
hrf_model, return_tstat):
3030
"""Test lss interface with nibabel nifti images
3131
"""
3232
if use_nibabel:
@@ -48,6 +48,7 @@ def test_lss_beta_series(sub_metadata, preproc_file, sub_events,
4848
selected_confounds=selected_confounds,
4949
signal_scaling=0,
5050
hrf_model=hrf_model,
51+
return_tstat=return_tstat,
5152
smoothing_kernel=None,
5253
high_pass=0.008)
5354
res = beta_series.run()
@@ -102,6 +103,7 @@ def test_fs_beta_series(sub_metadata, preproc_file, sub_events,
102103
signal_scaling=0,
103104
hrf_model=hrf_model,
104105
fir_delays=fir_delays,
106+
return_tstat=False,
105107
smoothing_kernel=None,
106108
high_pass=0.008)
107109
res = beta_series.run()
@@ -131,9 +133,17 @@ def test_fs_beta_series(sub_metadata, preproc_file, sub_events,
131133
os.remove(res.outputs.residual)
132134

133135

134-
@pytest.mark.parametrize("use_nibabel", [(True), (False)])
136+
@pytest.mark.parametrize(
137+
"use_nibabel,hrf_model,return_tstat",
138+
[
139+
(True, 'spm', True),
140+
(False, 'spm + derivative', False),
141+
(False, 'spm + derivative + dispersion', True)
142+
]
143+
)
135144
def test_lsa_beta_series(sub_metadata, preproc_file, sub_events,
136-
confounds_file, brainmask_file, use_nibabel):
145+
confounds_file, brainmask_file, use_nibabel,
146+
hrf_model, return_tstat):
137147
if use_nibabel:
138148
bold_file = nib.load(str(preproc_file))
139149
mask_file = nib.load(str(brainmask_file))
@@ -142,7 +152,7 @@ def test_lsa_beta_series(sub_metadata, preproc_file, sub_events,
142152
mask_file = str(brainmask_file)
143153

144154
selected_confounds = ['white_matter', 'csf']
145-
hrf_model = 'spm'
155+
146156
with open(str(sub_metadata), 'r') as md:
147157
bold_metadata = json.load(md)
148158

@@ -154,6 +164,7 @@ def test_lsa_beta_series(sub_metadata, preproc_file, sub_events,
154164
signal_scaling=0,
155165
selected_confounds=selected_confounds,
156166
hrf_model=hrf_model,
167+
return_tstat=return_tstat,
157168
smoothing_kernel=None,
158169
high_pass=0.008)
159170
res = beta_series.run()
@@ -289,15 +300,16 @@ def test_select_confounds(confounds_file, selected_confounds, nan_confounds,
289300

290301

291302
@pytest.mark.parametrize(
292-
"hrf_model",
303+
"hrf_model,return_tstat",
293304
[
294-
("glover"),
295-
("glover + derivative"),
296-
("glover + derivative + dispersion"),
305+
("glover", True),
306+
("glover + derivative", False),
307+
("glover + derivative + dispersion", True),
297308
],
298309
)
299310
def test_calc_beta_map(sub_metadata, preproc_file, sub_events,
300-
confounds_file, brainmask_file, hrf_model):
311+
confounds_file, brainmask_file, hrf_model,
312+
return_tstat):
301313

302314
model = first_level_model.FirstLevelModel(
303315
t_r=2,
@@ -319,6 +331,6 @@ def test_calc_beta_map(sub_metadata, preproc_file, sub_events,
319331
i_trial = lsa_df.index[0]
320332
t_name = lsa_df.loc[i_trial, 'trial_type']
321333

322-
beta_map = _calc_beta_map(model, t_name, hrf_model)
334+
beta_map = _calc_beta_map(model, t_name, hrf_model, return_tstat)
323335

324336
assert beta_map.shape == nib.load(str(brainmask_file)).shape

0 commit comments

Comments
 (0)