Skip to content

Commit 8ae0d86

Browse files
committed
add example on component relationships
1 parent 1e0cbf7 commit 8ae0d86

File tree

1 file changed

+314
-0
lines changed

1 file changed

+314
-0
lines changed
Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
"""
2+
Component Combinations
3+
======================
4+
5+
Explore different approaches combining model components.
6+
"""
7+
8+
# sphinx_gallery_thumbnail_number = 3
9+
10+
import numpy as np
11+
import matplotlib.pyplot as plt
12+
13+
from specparam import SpectralModel
14+
15+
from specparam.plts import plot_spectra
16+
17+
from specparam.utils.array import unlog
18+
from specparam.utils.select import nearest_ind
19+
from specparam.utils.download import load_example_data
20+
21+
# Import function to directly compute peak heights
22+
from specparam.convert.params import compute_peak_height
23+
24+
# Import the default parameter conversions
25+
from specparam.modes.convert import DEFAULT_CONVERTERS
26+
27+
# sphinx_gallery_start_ignore
28+
from specparam.plts.utils import check_ax
29+
30+
def plot_peak_height(model, peak_ind, spacing, operation, ax=None):
31+
"""Annotat plot by drawing the computed peak height."""
32+
33+
# Get the frequency value of the data closest to the specified peak
34+
f_ind = nearest_ind(model.data.freqs,
35+
model.results.params.periodic.params[peak_ind, 0])
36+
37+
# Plot the power spectrum
38+
ax = check_ax(ax)
39+
title = 'Peak Height: {:s}_{:s}'.format(spacing[0:3], operation[0:3])
40+
plot_spectra(freqs, powers, log_powers=spacing=='log',
41+
color='black', title=title, ax=ax)
42+
43+
# Add dot marker at the peak frequency index, at the aperiodic component power value
44+
ax.plot([model.data.freqs[f_ind]],
45+
[model.results.model.get_component('aperiodic', spacing)[f_ind]],
46+
'.', ms=12, color='blue')
47+
48+
# Add dot marker at the peak frequency index, at the peak top (combined) power value
49+
ax.plot([model.data.freqs[f_ind]],
50+
[model.results.model.get_component('full', spacing)[f_ind]],
51+
'.', ms=12, color='red')
52+
53+
# Draw the line for the computed peak height, based on provided spacing and operation
54+
ax.plot([model.data.freqs[f_ind], model.data.freqs[f_ind]],
55+
[model.results.model.get_component('aperiodic', spacing)[f_ind],
56+
model.results.model.get_component('aperiodic', spacing)[f_ind] + \
57+
compute_peak_height(fm, peak_ind, spacing, operation)],
58+
color='green', lw=2)
59+
# sphinx_gallery_end_ignore
60+
61+
###################################################################################################
62+
# Introduction
63+
# ------------
64+
#
65+
# In general, the approach taken for doing spectral parameterization considers the power
66+
# spectrum to be a combination of multiple components. Notably, however, there is more than
67+
# one possible way to combine the components, for example, components could be added
68+
# together, or multiplied, etc.
69+
#
70+
# An additional complication is that the power values of power spectra are often examined
71+
# in log-power spacing. This is important as whether the implications of how the model
72+
# components are combined also depends on the spacing of the data. To explore this, we
73+
# will first start with some brief notes on logging, and then explore how this all
74+
# relates to model component combinations and related measures, such as peak heights.
75+
#
76+
77+
###################################################################################################
78+
79+
# Load example spectra - using real data here
80+
freqs = load_example_data('freqs.npy', folder='data')
81+
powers = load_example_data('spectrum.npy', folder='data')
82+
83+
# Define frequency range for model fitting
84+
freq_range = [2, 40]
85+
86+
###################################################################################################
87+
# Some Notes on Logging
88+
# ---------------------
89+
#
90+
# In order to explore the implications of how the different components are combined, we will first
91+
# briefly revisit some rules for how logs work in mathematics.
92+
#
93+
# Specifically, the relationship between adding & subtracting log values, and how this relates
94+
# to equivalent operations in linear space, whereby the rules are:
95+
#
96+
# - log(x) + log(y) = log(x * y)
97+
# - log(x) - log(y) = log(x / y)
98+
#
99+
# When working in log space, the addition or subtraction of two log spaced values is
100+
# equivalent to the log of the multiplication or division of those values.
101+
#
102+
# Relatedly, we could note some properties that don't hold in log space, such as:
103+
#
104+
# - log(a) + log(y) != log(x + y)
105+
# - log(a) - log(y) != log(x - y)
106+
#
107+
# Collectively, what this means is that the addition or subtraction of log values,
108+
# is not equivalent of doing addition of subtraction of the linear values.
109+
#
110+
111+
###################################################################################################
112+
113+
# Sum of log values is equivalent to the log of the product
114+
assert np.log10(1.5) + np.log10(1.5) == np.log10(1.5 * 1.5)
115+
116+
# Sum of log values is not equivalent to the log of sum
117+
assert np.log10(1.5) + np.log10(1.5) != np.log10(1.5 + 1.5)
118+
119+
###################################################################################################
120+
# So, why do we use logs?
121+
# ~~~~~~~~~~~~~~~~~~~~~~~
122+
#
123+
# Given this, it is perhaps worth a brief interlude to consider why we so often use log
124+
# transforms when examining power spectra. One reason is simply that power values are
125+
# extremely skewed, with huge differences in the measured power values between, for example,
126+
# low frequencies and high frequencies and/or between the peak of an oscillation peak and the
127+
# power values for surrounding frequencies.
128+
#
129+
# This is why for visualizations and/or statistical analyses, working in log space can be
130+
# useful and convenient. However, when doing so, it's important to keep in mind the implications
131+
# of doing so, since it can otherwise be easy to think about properties and transformations
132+
# in linear space, and end up with incorrect conclusions. For example, when adding or subtracting
133+
# from power spectra in log space and/or when comparing power values, such as between different
134+
# peaks, we need to remember the implications of log spacing.
135+
#
136+
137+
###################################################################################################
138+
139+
# Plot a power spectrum in both linear-linear and log-linear space
140+
_, axes = plt.subplots(1, 2, figsize=(12, 6))
141+
plot_spectra(freqs, powers, log_powers=False, label='Linear Power', ax=axes[0])
142+
plot_spectra(freqs, powers, log_powers=True, label='Log Power', ax=axes[1])
143+
144+
###################################################################################################
145+
#
146+
# In the above linear-linear power spectrum plot, we can see the skewed nature of the power
147+
# values, including the steepness of the decay of the 1/f-like nature of the spectrum, and
148+
# the degree to which peaks of power, such as the alpha peak here, can be many times higher
149+
# power than other frequencies.
150+
#
151+
152+
###################################################################################################
153+
# Model Component Combinations
154+
# ----------------------------
155+
#
156+
# Having explored typical representations of neural power spectra, and some notes on logging,
157+
# let's come back to the main topic of model component combinations.
158+
#
159+
# Broadly,when considering how the components relate to each other, in terms of how they are
160+
# combined to create the full model fit, we can start with considering two key aspects:
161+
#
162+
# - the operation, e.g. additive or multiplicative
163+
# - the spacing of the data, e.g. linear or log
164+
#
165+
# Notably, as seen above there is an interaction between these choices that needs to be considered.
166+
#
167+
168+
###################################################################################################
169+
170+
# Initialize and fit an example model
171+
fm = SpectralModel(verbose=False)
172+
fm.fit(freqs, powers, [2, 40])
173+
174+
# Plot the model fit, with peak annotations
175+
fm.plot(plot_peaks='dot')
176+
177+
###################################################################################################
178+
#
179+
# To compute different possible versions of the peak height, we can use the
180+
# :func:`~.compute_peak_height` function. Using this function, we can compute measures of
181+
# the peak height, specifying different data representations and difference measures.
182+
#
183+
184+
###################################################################################################
185+
186+
# Define which peak ind to compute height for
187+
peak_ind = 0
188+
189+
# Compute 4 different measures of the peak height
190+
peak_heights = {
191+
'log_sub' : compute_peak_height(fm, peak_ind, 'log', 'subtract'),
192+
'log_div' : compute_peak_height(fm, peak_ind, 'log', 'divide'),
193+
'lin_sub' : compute_peak_height(fm, peak_ind, 'linear', 'subtract'),
194+
'lin_div' : compute_peak_height(fm, peak_ind, 'linear', 'divide'),
195+
}
196+
197+
###################################################################################################
198+
199+
# Check computing difference / division measures
200+
print('log sub : {:+08.4f}'.format(peak_heights['log_sub']))
201+
print('log div : {:+08.4f}'.format(peak_heights['log_div']))
202+
print('lin sub : {:+08.4f}'.format(peak_heights['lin_sub']))
203+
print('lin div : {:+08.4f}'.format(peak_heights['lin_div']))
204+
205+
###################################################################################################
206+
#
207+
# As expected, we can see that the four differet combinations of spacing and operation can
208+
# lead to 4 different answers for the peak height.
209+
#
210+
# We can also go one step further, and examing (un)logging the results.
211+
#
212+
213+
###################################################################################################
214+
215+
# Check logging / unlogging measures: lines up with above
216+
print('Unlog log sub : {:+08.4f}'.format(unlog(peak_heights['log_sub'])))
217+
print('Log of lin div : {:+08.4f}'.format(np.log10(peak_heights['lin_div'])))
218+
219+
###################################################################################################
220+
221+
# Check logging / unlogging measures: does not line up with above
222+
print('Unlog log div : {:+08.4f}'.format(unlog(peak_heights['log_div'])))
223+
print('Log of lin sub : {:+08.4f}'.format(np.log10(peak_heights['lin_sub'])))
224+
225+
###################################################################################################
226+
#
227+
# As expected, unlogging the log-subtraction is equivalent to the linear division, and
228+
# (vice-versa) logging the linear division is equivalent to the log-subtraction.
229+
#
230+
# However, unlogging the log-division or logging the linear-subtraction do not lead to
231+
# answers that align with any of the previous measures.
232+
#
233+
# To summarize:
234+
#
235+
# - log / linear and difference / division all give difference values
236+
# - unlogging the log difference is the same as the linear division
237+
# - unlogging the log difference does NOT give the linear difference
238+
#
239+
# - logging the linear division is the same as the log difference
240+
# - logging the linear difference does NOT give the log difference
241+
#
242+
#
243+
# Note that this is all standard log operations, the point here is to evaluate these
244+
# different estimates in the context of spectral parameterization, so that we can next
245+
# discuss when to select and use these different estimates.
246+
#
247+
248+
###################################################################################################
249+
250+
# Visualize log vs linear peak height estimates
251+
_, axes = plt.subplots(1, 2, figsize=(12, 6))
252+
plot_peak_height(fm, peak_ind, 'linear', 'subtract', ax=axes[1])
253+
plot_peak_height(fm, peak_ind, 'log', 'subtract', ax=axes[0])
254+
255+
###################################################################################################
256+
# Additive vs. Multiplicative Component Combinations
257+
# --------------------------------------------------
258+
#
259+
# Given these different possible measures of the peak height, the natural next question
260+
# is which is 'correct' or 'best'.
261+
#
262+
# The short answer is that there is not a singular definitive answer. Depending on
263+
# one's goals and assumptions about the data, there may be better answers for particular
264+
# use cases. The different measures make different assumptions about the generative model
265+
# of the data under study. If we had a definitive model of the underlying generators of
266+
# the different data components, and a clear understanding of they related to each other,
267+
# then we could use that information to decide exactly how to proceed.
268+
#
269+
# However, for the case of neuro-electrophysiological recordings, there is not a definitively
270+
# established generative model for the data, and as such, no singular or definitive answer
271+
# to how best to model the data.
272+
#
273+
# For any individual project / analysis, one can choose the approach that best fits the
274+
# assumed generative model of the data. For example, if one wishes to examine the data
275+
# based on a linearly-additive model, then the linear-subtraction of components matches this,
276+
# whereas if one wants to specify a linearly multiplicative model (equivalent to subtraction
277+
# in log space, and the kind of model assumed by filtered noise processes), then the
278+
# linear-division approach the the way to go.
279+
#
280+
# Within specparam, you can specify the approach to take for converting parameters post
281+
# model fitting, which can be used to re-compute peak heights based on the desired model.
282+
# For more discussion of this, see other documentation sections on choosing and defining
283+
# parameter conversions.
284+
#
285+
286+
###################################################################################################
287+
288+
# Initialize model objects, specifying different peak height parameter conversions
289+
fm_log_sub = SpectralModel(converters={'periodic' : {'pw' : 'log_sub'}}, verbose=False)
290+
fm_lin_sub = SpectralModel(converters={'periodic' : {'pw' : 'lin_sub'}}, verbose=False)
291+
292+
# Fit the models to the data
293+
fm_log_sub.fit(freqs, powers, freq_range)
294+
fm_lin_sub.fit(freqs, powers, freq_range)
295+
296+
# Check the resulting parameters, with different peak height values
297+
print(fm_log_sub.results.get_params('periodic'))
298+
print(fm_lin_sub.results.get_params('periodic'))
299+
300+
###################################################################################################
301+
# Does it matter which form I choose?
302+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
303+
#
304+
# In the above, we have shown that choosing the peak height estimations does lead to different
305+
# computed values. However, in most analyses, it is not the absolute values or absolute
306+
# differences of these measures that is of interest, but their relative differences.
307+
#
308+
# Broadly speaking, a likely rule of thumb is that within the spectral parameterization
309+
# approach, switching the model combination definition is generally unlikely to change the
310+
# general pattern of things (in terms of which parameters change). However it could well
311+
# change effect size measures (and as such, potentially the results of significant tests),
312+
# such that it is plausible that the results of different model combination forms could
313+
# be at least somewhat different.
314+
#

0 commit comments

Comments
 (0)