Skip to content

Commit ebb5e2d

Browse files
committed
add metrics example
1 parent 7f11931 commit ebb5e2d

File tree

1 file changed

+339
-0
lines changed

1 file changed

+339
-0
lines changed
Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
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

Comments
 (0)