Skip to content

Commit 006c93a

Browse files
authored
Merge pull request #296 from fooof-tools/getmodel
[ENH] - Add getter functions for data & model components
2 parents 312fed7 + f6b35f7 commit 006c93a

File tree

5 files changed

+239
-22
lines changed

5 files changed

+239
-22
lines changed

examples/models/plot_data_components.py

+112-22
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@
2929
fm.fit(freqs, powers)
3030

3131
###################################################################################################
32-
# Data Components
33-
# ~~~~~~~~~~~~~~~
32+
# Data & Model Components
33+
# -----------------------
3434
#
3535
# The model fit process includes procedures for isolating aperiodic and periodic components in
3636
# the data, fitting each of these components separately, and then combining the model components
@@ -39,8 +39,13 @@
3939
# In doing this process, the model fit procedure computes and stores isolated data components,
4040
# which are available in the model.
4141
#
42-
# Before diving into the isolated data components, let's check the data (`power_spectrum`)
43-
# and full model fit of a model object (`fooofed_spectrum`).
42+
43+
###################################################################################################
44+
# Full Data & Model Components
45+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
46+
#
47+
# Before diving into the isolated data components, let's check 'full' components, including
48+
# the data (`power_spectrum`) and full model fit of a model object (`fooofed_spectrum`).
4449
#
4550

4651
###################################################################################################
@@ -53,25 +58,39 @@
5358
# Plot the power spectrum model from the object
5459
plot_spectra(fm.freqs, fm.fooofed_spectrum_, color='red')
5560

61+
###################################################################################################
62+
# Isolated Components
63+
# -------------------
64+
#
65+
# As well as the 'full' data & model components above, the model fitting procedure includes
66+
# steps that result in isolated periodic and aperiodic components, in both the
67+
# data and model. These isolated components are stored internally in the model.
68+
#
69+
# To access these components, we can use the following `getter` methods:
70+
#
71+
# - :meth:`~fooof.FOOOF.get_data`: allows for accessing data components
72+
# - :meth:`~fooof.FOOOF.get_model`: allows for accessing model components
73+
#
74+
5675
###################################################################################################
5776
# Aperiodic Component
5877
# ~~~~~~~~~~~~~~~~~~~
5978
#
6079
# To fit the aperiodic component, the model fit procedure includes a peak removal process.
6180
#
62-
# The resulting 'peak-removed' data component is stored in the model object, in the
63-
# `_spectrum_peak_rm` attribute.
81+
# The resulting 'peak-removed' data component is stored in the model object, as well as the
82+
# isolated aperiodic component model fit.
6483
#
6584

6685
###################################################################################################
6786

6887
# Plot the peak removed spectrum data component
69-
plot_spectra(fm.freqs, fm._spectrum_peak_rm, color='black')
88+
plot_spectra(fm.freqs, fm.get_data('aperiodic'), color='black')
7089

7190
###################################################################################################
7291

7392
# Plot the peak removed spectrum, with the model aperiodic fit
74-
plot_spectra(fm.freqs, [fm._spectrum_peak_rm, fm._ap_fit],
93+
plot_spectra(fm.freqs, [fm.get_data('aperiodic'), fm.get_model('aperiodic')],
7594
colors=['black', 'blue'], linestyle=['-', '--'])
7695

7796
###################################################################################################
@@ -81,19 +100,20 @@
81100
# To fit the periodic component, the model fit procedure removes the fit peaks from the power
82101
# spectrum.
83102
#
84-
# The resulting 'flattened' data component is stored in the model object, in the
85-
# `_spectrum_flat` attribute.
103+
# The resulting 'flattened' data component is stored in the model object, as well as the
104+
# isolated periodic component model fit.
86105
#
87106

88107
###################################################################################################
89108

90109
# Plot the flattened spectrum data component
91-
plot_spectra(fm.freqs, fm._spectrum_flat, color='black')
110+
plot_spectra(fm.freqs, fm.get_data('peak'), color='black')
92111

93112
###################################################################################################
94113

95114
# Plot the flattened spectrum data with the model peak fit
96-
plot_spectra(fm.freqs, [fm._spectrum_flat, fm._peak_fit], colors=['black', 'green'])
115+
plot_spectra(fm.freqs, [fm.get_data('peak'), fm.get_model('peak')],
116+
colors=['black', 'green'], linestyle=['-', '--'])
97117

98118
###################################################################################################
99119
# Full Model Fit
@@ -106,18 +126,88 @@
106126
###################################################################################################
107127

108128
# Plot the full model fit, as the combination of the aperiodic and peak model components
109-
plot_spectra(fm.freqs, [fm._ap_fit + fm._peak_fit], color='red')
129+
plot_spectra(fm.freqs, [fm.get_model('aperiodic') + fm.get_model('peak')], color='red')
110130

111131
###################################################################################################
112-
# Notes on Analyzing Data Components
113-
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
132+
# Linear vs Log Spacing
133+
# ---------------------
114134
#
115135
# The above shows data components as they are available on the model object, and used in
116-
# the fitting process. Some analyses may aim to use these isolated components to compute
117-
# certain measures of interest on the data. Note that these data components are stored in
118-
# 'private' attributes (indicated by a leading underscore), meaning in normal function they
119-
# are not expected to be accessed by the user, but as we've seen above they can still be accessed.
120-
# However, analyses derived from these isolated data components is not currently officially
121-
# supported by the module, and so users who wish to do so should consider the benefits and
122-
# limitations of any such analyses.
136+
# the fitting process - notable, in log10 spacing.
137+
#
138+
# Some analyses may aim to use these isolated components to compute certain measures of
139+
# interest on the data. However, when doing so, one may often want the linear power
140+
# representations of these components.
141+
#
142+
# Both the `get_data` and `get_model` methods accept a 'space' argument, whereby the user
143+
# can specify whether the return the components in log10 or linear spacing.
144+
145+
146+
147+
###################################################################################################
148+
# Aperiodic Components in Linear Space
149+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
150+
#
151+
# First we can examine the aperiodic data & model components, in linear space.
152+
#
153+
154+
###################################################################################################
155+
156+
# Plot the peak removed spectrum, with the model aperiodic fit
157+
plot_spectra(fm.freqs, [fm.get_data('aperiodic', 'linear'), fm.get_model('aperiodic', 'linear')],
158+
colors=['black', 'blue'], linestyle=['-', '--'])
159+
160+
###################################################################################################
161+
# Peak Component in Linear Space
162+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
163+
#
164+
# Next, we can examine the peak data & model components, in linear space.
165+
#
166+
167+
###################################################################################################
168+
169+
# Plot the flattened spectrum data with the model peak fit
170+
plot_spectra(fm.freqs, [fm.get_data('peak', 'linear'), fm.get_model('peak', 'linear')],
171+
colors=['black', 'green'], linestyle=['-', '--'])
172+
173+
###################################################################################################
174+
# Linear Space Additive Model
175+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~
176+
#
177+
# Note that specifying 'linear' does not simply unlog the data components to return them
178+
# in linear space, but instead defines the space of the additive data definition such that
179+
# `power_spectrum = aperiodic_component + peak_component` (for data and/or model).
180+
#
181+
# We can see this by plotting the linear space data (or model) with the corresponding
182+
# aperiodic and periodic components summed together. Note that if you simply unlog
183+
# the components and sum them, they does not add up to reflecting the full data / model.
184+
#
185+
186+
###################################################################################################
187+
188+
# Plot the linear data, showing the combination of peak + aperiodic matches the full data
189+
plot_spectra(fm.freqs,
190+
[fm.get_data('full', 'linear'),
191+
fm.get_data('aperiodic', 'linear') + fm.get_data('peak', 'linear')],
192+
linestyle=['-', 'dashed'], colors=['black', 'red'], alpha=[0.3, 0.75])
193+
194+
###################################################################################################
195+
196+
# Plot the linear model, showing the combination of peak + aperiodic matches the full model
197+
plot_spectra(fm.freqs,
198+
[fm.get_model('full', 'linear'),
199+
fm.get_model('aperiodic', 'linear') + fm.get_model('peak', 'linear')],
200+
linestyle=['-', 'dashed'], colors=['black', 'red'], alpha=[0.3, 0.75])
201+
202+
###################################################################################################
203+
# Notes on Analyzing Data & Model Components
204+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
205+
#
206+
# The functionality here allows for accessing the model components in log space (as used by
207+
# the model for fitting), as well as recomputing in linear space.
208+
#
209+
# If you are aiming to analyze these components, it is important to consider which version of
210+
# the data you should analyze for the question at hand, as there are key differences to the
211+
# different representations. Users who wish to do so post-hoc analyses of these data and model
212+
# components should consider the benefits and limitations the different representations.
123213
#

fooof/core/utils.py

+19
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,25 @@
88
###################################################################################################
99
###################################################################################################
1010

11+
def unlog(arr, base=10):
12+
"""Helper function to unlog an array.
13+
14+
Parameters
15+
----------
16+
arr : ndarray
17+
Array.
18+
base : float
19+
Base of the log to undo.
20+
21+
Returns
22+
-------
23+
ndarray
24+
Unlogged array.
25+
"""
26+
27+
return np.power(base, arr)
28+
29+
1130
def group_three(vec):
1231
"""Group an array of values into threes.
1332

fooof/objs/fit.py

+90
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
from numpy.linalg import LinAlgError
6464
from scipy.optimize import curve_fit
6565

66+
from fooof.core.utils import unlog
6667
from fooof.core.items import OBJ_DESC
6768
from fooof.core.info import get_indices
6869
from fooof.core.io import save_fm, load_json
@@ -596,6 +597,95 @@ def get_meta_data(self):
596597
for key in OBJ_DESC['meta_data']})
597598

598599

600+
def get_data(self, component='full', space='log'):
601+
"""Get a data component.
602+
603+
Parameters
604+
----------
605+
component : {'full', 'aperiodic', 'peak'}
606+
Which data component to return.
607+
'full' - full power spectrum
608+
'aperiodic' - isolated aperiodic data component
609+
'peak' - isolated peak data component
610+
space : {'log', 'linear'}
611+
Which space to return the data component in.
612+
'log' - returns in log10 space.
613+
'linear' - returns in linear space.
614+
615+
Returns
616+
-------
617+
output : 1d array
618+
Specified data component, in specified spacing.
619+
620+
Notes
621+
-----
622+
The 'space' parameter doesn't just define the spacing of the data component
623+
values, but rather defines the space of the additive data definition such that
624+
`power_spectrum = aperiodic_component + peak_component`.
625+
With space set as 'log', this combination holds in log space.
626+
With space set as 'linear', this combination holds in linear space.
627+
"""
628+
629+
assert space in ['linear', 'log'], "Input for 'space' invalid."
630+
631+
if component == 'full':
632+
output = self.power_spectrum if space == 'log' else unlog(self.power_spectrum)
633+
elif component == 'aperiodic':
634+
output = self._spectrum_peak_rm if space == 'log' else \
635+
unlog(self.power_spectrum) / unlog(self._peak_fit)
636+
elif component == 'peak':
637+
output = self._spectrum_flat if space == 'log' else \
638+
unlog(self.power_spectrum) - unlog(self._ap_fit)
639+
else:
640+
raise ValueError('Input for component invalid.')
641+
642+
return output
643+
644+
645+
def get_model(self, component='full', space='log'):
646+
"""Get a model component.
647+
648+
Parameters
649+
----------
650+
component : {'full', 'aperiodic', 'peak'}
651+
Which model component to return.
652+
'full' - full model
653+
'aperiodic' - isolated aperiodic model component
654+
'peak' - isolated peak model component
655+
space : {'log', 'linear'}
656+
Which space to return the model component in.
657+
'log' - returns in log10 space.
658+
'linear' - returns in linear space.
659+
660+
Returns
661+
-------
662+
output : 1d array
663+
Specified model component, in specified spacing.
664+
665+
Notes
666+
-----
667+
The 'space' parameter doesn't just define the spacing of the model component
668+
values, but rather defines the space of the additive model such that
669+
`model = aperiodic_component + peak_component`.
670+
With space set as 'log', this combination holds in log space.
671+
With space set as 'linear', this combination holds in linear space.
672+
"""
673+
674+
assert space in ['linear', 'log'], "Input for 'space' invalid."
675+
676+
if component == 'full':
677+
output = self.fooofed_spectrum_ if space == 'log' else unlog(self.fooofed_spectrum_)
678+
elif component == 'aperiodic':
679+
output = self._ap_fit if space == 'log' else unlog(self._ap_fit)
680+
elif component == 'peak':
681+
output = self._peak_fit if space == 'log' else \
682+
unlog(self.fooofed_spectrum_) - unlog(self._ap_fit)
683+
else:
684+
raise ValueError('Input for component invalid.')
685+
686+
return output
687+
688+
599689
def get_params(self, name, col=None):
600690
"""Return model fit parameters for specified feature(s).
601691

fooof/tests/core/test_utils.py

+7
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@
1212
###################################################################################################
1313
###################################################################################################
1414

15+
def test_unlog():
16+
17+
orig = np.array([1, 2, 3, 4])
18+
logged = np.log10(orig)
19+
unlogged = unlog(logged)
20+
assert np.array_equal(orig, unlogged)
21+
1522
def test_group_three():
1623

1724
dat = [0, 1, 2, 3, 4, 5]

fooof/tests/objs/test_fit.py

+11
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,17 @@ def test_obj_gets(tfm):
311311
results = tfm.get_results()
312312
assert isinstance(results, FOOOFResults)
313313

314+
def test_get_components(tfm):
315+
316+
# Make sure test object has been fit
317+
tfm.fit()
318+
319+
# Test get data & model components
320+
for comp in ['full', 'aperiodic', 'peak']:
321+
for space in ['log', 'linear']:
322+
assert isinstance(tfm.get_data(comp, space), np.ndarray)
323+
assert isinstance(tfm.get_model(comp, space), np.ndarray)
324+
314325
def test_get_params(tfm):
315326
"""Test the get_params method."""
316327

0 commit comments

Comments
 (0)