|
| 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