|
| 1 | +""" |
| 2 | +Custom Metrics |
| 3 | +============== |
| 4 | +
|
| 5 | +This example covers defining and using custom metrics. |
| 6 | +""" |
| 7 | + |
| 8 | +# Import spectral model object |
| 9 | +from specparam import SpectralModel |
| 10 | + |
| 11 | +# Import Metric object for defining custom metrics |
| 12 | +from specparam.metrics.metric import Metric |
| 13 | + |
| 14 | +# Import function to simulate power spectra |
| 15 | +from specparam.sim import sim_power_spectrum |
| 16 | + |
| 17 | +################################################################################################### |
| 18 | +# Defining Custom Metrics |
| 19 | +# ----------------------- |
| 20 | +# |
| 21 | +# As covered in the tutorials, the specparam module has a set of predefined metrics, wherein |
| 22 | +# `metrics` refer to measures that are computed post model fitting to evaluate properties |
| 23 | +# of the model fit, typically as it relates to the original data. |
| 24 | +# |
| 25 | +# In this tutorial, we will explore how you can also define your own custom metrics. |
| 26 | +# |
| 27 | +# To do so, we will start by simulating an example power spectrum to use for this example. |
| 28 | +# |
| 29 | + |
| 30 | +################################################################################################### |
| 31 | + |
| 32 | +# Define simulation parameters |
| 33 | +ap_params = [50, 2] |
| 34 | +gauss_params = [10, 0.5, 2, 20, 0.3, 4] |
| 35 | +nlv = 0.05 |
| 36 | + |
| 37 | +# Simulate an example power spectrum |
| 38 | +freqs, powers = sim_power_spectrum([3, 50], {'fixed' : ap_params}, {'gaussian' : gauss_params}, |
| 39 | + nlv, freq_res=0.25) |
| 40 | + |
| 41 | +################################################################################################### |
| 42 | +# Defining Custom Measure Function |
| 43 | +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 44 | +# |
| 45 | +# To define a custom measure, we first need to define a function that computes our metric. |
| 46 | +# |
| 47 | +# By convention, a custom measure functions must have at least two input arguments, with the |
| 48 | +# first two arguments being the original data (power spectrum) and the model (modeled spectrum). |
| 49 | +# |
| 50 | +# Within the function, the measure of interest should be defined, such that it returns |
| 51 | +# the result of the metric, which should be a float. |
| 52 | +# |
| 53 | +# For our first example, we will define a simple error metric that computes the total |
| 54 | +# error of the model (sum of the absolute deviations). |
| 55 | +# |
| 56 | + |
| 57 | +################################################################################################### |
| 58 | + |
| 59 | +# Import any functionality needed for the metric |
| 60 | +import numpy as np |
| 61 | + |
| 62 | +# Define a function that computes a custom metric |
| 63 | +def compute_total_error(power_spectrum, modeled_spectrum): |
| 64 | + """Compute the total (summed) error between the data and the model.""" |
| 65 | + |
| 66 | + total_err = np.sum(np.abs(power_spectrum - modeled_spectrum)) |
| 67 | + |
| 68 | + return total_err |
| 69 | + |
| 70 | +################################################################################################### |
| 71 | +# The `Metric` Class |
| 72 | +# ~~~~~~~~~~~~~~~~~~ |
| 73 | +# |
| 74 | +# In order to use the custom metric, more information is needed. To collect this additional |
| 75 | +# information the :class:`~specparam.metrics.metric` object is used to define a metric. |
| 76 | +# |
| 77 | +# The Metric object requires the following information: |
| 78 | +# - `category`: a description of what kind of metric it is |
| 79 | +# - `measure`: a label for the specific measure that is defined |
| 80 | +# - `description`: a description of the custom metric |
| 81 | +# - `func`: the callable that compute the metric |
| 82 | +# |
| 83 | + |
| 84 | +################################################################################################### |
| 85 | + |
| 86 | +# Define Metric for the total error |
| 87 | +total_error_metric = Metric( |
| 88 | + category='error', |
| 89 | + measure='total_error', |
| 90 | + description='Total absolute error.', |
| 91 | + func=compute_total_error, |
| 92 | +) |
| 93 | + |
| 94 | +################################################################################################### |
| 95 | + |
| 96 | +# Initialize a spectral model, passing in our custom metric definition |
| 97 | +fm = SpectralModel(min_peak_height=0.25, metrics=[total_error_metric]) |
| 98 | + |
| 99 | +# Fit the model and print a report |
| 100 | +fm.report(freqs, powers) |
| 101 | + |
| 102 | +################################################################################################### |
| 103 | +# |
| 104 | +# Note that in the above report, the metrics section now includes the result of our custom metric! |
| 105 | +# |
| 106 | + |
| 107 | +################################################################################################### |
| 108 | +# Defining Metrics with Dictionaries |
| 109 | +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 110 | +# |
| 111 | +# In the above, we directly used the `Metric` object to define our custom metric. |
| 112 | +# |
| 113 | +# If you prefer, you can also collect the relevant information needed to define a metric into |
| 114 | +# a dictionary, and pass this into the model object instead. |
| 115 | +# |
| 116 | + |
| 117 | +################################################################################################### |
| 118 | + |
| 119 | +# Define the information for a custom metric, in a dictionary |
| 120 | +custom_metric_dict = { |
| 121 | + 'category' : 'error', |
| 122 | + 'measure' : 'total_error', |
| 123 | + 'description' : 'Total absolute error.', |
| 124 | + 'func' : compute_total_error, |
| 125 | +} |
| 126 | + |
| 127 | +################################################################################################### |
| 128 | + |
| 129 | +# Initialize a model object, passing in the custom metric, defined as a dictionary |
| 130 | +fm = SpectralModel(min_peak_height=0.25, metrics=[custom_metric_dict]) |
| 131 | + |
| 132 | +################################################################################################### |
| 133 | +# |
| 134 | +# When using custom metrics, you can also access the results using the |
| 135 | +# :meth:`~specparam.SpectralModel.get_metrics` by using the measure name, |
| 136 | +# the same as when accessing default / built in metrics. |
| 137 | +# |
| 138 | + |
| 139 | +################################################################################################### |
| 140 | + |
| 141 | +# Fit the model to the data |
| 142 | +fm.fit(freqs, powers) |
| 143 | + |
| 144 | +# Access the custom metric result with get_metrics |
| 145 | +fm.get_metrics('total_error') |
| 146 | + |
| 147 | +################################################################################################### |
| 148 | +# |
| 149 | +# Above, we initialized our model object by specifying to use only our new custom metric. |
| 150 | +# |
| 151 | +# Note that you can also initialize the model object with a list of multiple metrics, |
| 152 | +# including a mixture of pre-defined and/or custom defined metrics. |
| 153 | +# |
| 154 | + |
| 155 | +################################################################################################### |
| 156 | + |
| 157 | +# Initialize a spectral model, passing in multiple metrics (both internal and custom) |
| 158 | +fm = SpectralModel(min_peak_height=0.25, metrics=[total_error_metric, 'gof_rsquared']) |
| 159 | + |
| 160 | +# Fit the model and print a report |
| 161 | +fm.report(freqs, powers) |
| 162 | + |
| 163 | +################################################################################################### |
| 164 | +# |
| 165 | +# In the above report, we now see multiple metrics have been applied to the model, including |
| 166 | +# our new / custom metric as well as the built-in metrics which we specified. |
| 167 | +# |
| 168 | + |
| 169 | +################################################################################################### |
| 170 | +# Custom Metrics with Additional Arguments |
| 171 | +# ---------------------------------------- |
| 172 | +# |
| 173 | +# In some cases, you may want to define a metric that requires additional information than just |
| 174 | +# the data and model to compute the measure of interest (e.g., the custom metric function has |
| 175 | +# more than two arguments). |
| 176 | +# |
| 177 | +# For an example of this, we will define an example custom metric that computes the error |
| 178 | +# of a specific frequency range, therefore requiring information about the frequency definition |
| 179 | +# of the data. |
| 180 | +# |
| 181 | +# We start, as above, by defining a function that computes our metric, starting with the same |
| 182 | +# two arguments (data and model) and then adding additional arguments as needed. |
| 183 | +# |
| 184 | + |
| 185 | +################################################################################################### |
| 186 | + |
| 187 | +from specparam.utils.spectral import trim_spectrum |
| 188 | + |
| 189 | +def compute_lowfreq_error(power_spectrum, modeled_spectrum, freqs): |
| 190 | + """Compute the mean absolute error in the low frequency range.""" |
| 191 | + |
| 192 | + low_freq_range = [1, 8] |
| 193 | + _, power_spectrum_low = trim_spectrum(freqs, power_spectrum, low_freq_range) |
| 194 | + _, modeled_spectrum_low = trim_spectrum(freqs, modeled_spectrum, low_freq_range) |
| 195 | + |
| 196 | + low_err = np.abs(power_spectrum_low - modeled_spectrum_low).mean() |
| 197 | + |
| 198 | + return low_err |
| 199 | + |
| 200 | +################################################################################################### |
| 201 | +# |
| 202 | +# In the above error function, we need access to the frequency definition, as well as the |
| 203 | +# data and model. Now we need to make sure that when this function is called to compute |
| 204 | +# the metric, this additional information is made available to the function. |
| 205 | +# |
| 206 | +# To provide access to additional attributes, we need to use the optional `kwargs` argument |
| 207 | +# when defining our Metric to define how to access the additional information. |
| 208 | +# |
| 209 | +# To use kwargs, define a dictionary where each key is the string name of the additional input |
| 210 | +# to the measure function, and each value is a lambda function that accepts as input the model |
| 211 | +# data & results objects, and then uses these to access the information that is needed. |
| 212 | +# |
| 213 | +# For our low frequency error measure, this looks like: |
| 214 | +# |
| 215 | +# `kwargs={'freqs' : lambda data, results: data.freqs}` |
| 216 | +# |
| 217 | +# Internally, if `kwargs` is defined, the lambda function is called for each entry, |
| 218 | +# passing in the Model.data and Model.results objects, which then accesses the specified |
| 219 | +# attributes based on the implementation of the lambda function. |
| 220 | +# |
| 221 | +# Note that this means all additional inputs to the function need to be information that |
| 222 | +# can be accessed and/or computed based on what is available in the data and results |
| 223 | +# objects that are part of the model object |
| 224 | +# |
| 225 | + |
| 226 | +################################################################################################### |
| 227 | + |
| 228 | +# Define Metric for the low frequency error, defining `kwargs` |
| 229 | +lowfreq_error = Metric( |
| 230 | + category='error', |
| 231 | + measure='low_freq_mae', |
| 232 | + description='Mean absolute error of the low frequency range.', |
| 233 | + func=compute_lowfreq_error, |
| 234 | + kwargs={'freqs' : lambda data, results: data.freqs}, |
| 235 | +) |
| 236 | + |
| 237 | +################################################################################################### |
| 238 | +# |
| 239 | +# Now our custom metric is defined, and we can use it with a model object the same as before! |
| 240 | +# |
| 241 | + |
| 242 | +################################################################################################### |
| 243 | + |
| 244 | +# Initialize a spectral model, passing in custom metric with additional arguments |
| 245 | +fm = SpectralModel(metrics=[lowfreq_error]) |
| 246 | + |
| 247 | +# Fit the model and print a report |
| 248 | +fm.report(freqs, powers) |
| 249 | + |
| 250 | +################################################################################################### |
| 251 | +# |
| 252 | +# In the above, our new custom metric was now computed for our model fit! |
| 253 | +# |
| 254 | + |
| 255 | +################################################################################################### |
| 256 | +# A Final Example |
| 257 | +# --------------- |
| 258 | +# |
| 259 | +# For one last example, lets make a more complex metric, which requires multiple additional |
| 260 | +# pieces of information that need to be accessed from the model object. |
| 261 | +# |
| 262 | +# In this example, we will define and use a custom metric that defines an error metric |
| 263 | +# that is proportional to the model degrees of freedom. |
| 264 | +# |
| 265 | + |
| 266 | +################################################################################################### |
| 267 | + |
| 268 | +# Define a function to compute our custom error metric |
| 269 | +def custom_measure(power_spectrum, modeled_spectrum, freqs, n_params): |
| 270 | + """Compute a custom error metric of error proportional to model degrees of freedom.""" |
| 271 | + |
| 272 | + # Compute degrees of freedom (# data points - # parameters) |
| 273 | + df_error = len(freqs) - n_params |
| 274 | + |
| 275 | + # Compute the total error of the model fit |
| 276 | + err_total = compute_total_error(power_spectrum, modeled_spectrum) |
| 277 | + |
| 278 | + # Compute the error proportional to model degrees of freedom |
| 279 | + err_per_df = err_total / df_error |
| 280 | + |
| 281 | + return err_per_df |
| 282 | + |
| 283 | +################################################################################################### |
| 284 | +# |
| 285 | +# Now that we have defined the function, we define a Metric object, as before. |
| 286 | +# |
| 287 | +# Note that in defining the 'category' for our custom metric, we need not use the existing |
| 288 | +# categories from the built in metrics, and can instead define our own custom category. |
| 289 | +# |
| 290 | + |
| 291 | +################################################################################################### |
| 292 | + |
| 293 | +# Define Metric for the low frequency error |
| 294 | +custom_measure = Metric( |
| 295 | + category='custom', |
| 296 | + measure='err-by-df', |
| 297 | + description='Error proportionate to the degrees of freedom of the model.', |
| 298 | + func=custom_measure, |
| 299 | + kwargs={'freqs' : lambda data, results: data.freqs, |
| 300 | + 'n_params' : lambda data, results : results.n_params}, |
| 301 | +) |
| 302 | + |
| 303 | +################################################################################################### |
| 304 | + |
| 305 | +# Initialize a spectral model, passing in our new custom measure |
| 306 | +fm = SpectralModel(metrics=[custom_measure]) |
| 307 | + |
| 308 | +# Fit the model and print a report |
| 309 | +fm.report(freqs, powers) |
| 310 | + |
| 311 | +################################################################################################### |
| 312 | +# |
| 313 | +# We can again use `get_metrics` to access the metric results - note that in this case |
| 314 | +# we need to match the category name that we used in defining our metric. |
| 315 | +# |
| 316 | + |
| 317 | +################################################################################################### |
| 318 | + |
| 319 | +# Access the custom metric result |
| 320 | +fm.get_metrics('custom_err-by-df') |
| 321 | + |
| 322 | +################################################################################################### |
| 323 | +# |
| 324 | +# That covers how to define custom metrics! |
| 325 | + |
| 326 | +################################################################################################### |
| 327 | +# Adding New Metrics to the Module |
| 328 | +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 329 | +# |
| 330 | +# As a final note, if you look into the set of 'built-in' metrics that are available within |
| 331 | +# the module, you will see that these are defined in the exact way as done here - the only |
| 332 | +# difference is that they are defined within the module and therefore can be accessed via |
| 333 | +# their name, as a shortcut, rather than the user having to pass in their own full definitions. |
| 334 | +# |
| 335 | +# This also means that if you have a custom metric that you think would be of interest to |
| 336 | +# other specparam users, once the Metric object is defined it is quite easy to add this |
| 337 | +# to the module as a new default option. If you would be interested in suggesting a metric |
| 338 | +# be added to the module, feel free to open an issue and/or pull request. |
| 339 | +# |
0 commit comments