From 1cbcb1c9b75807775994d41f34673b444bb2b2db Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Thu, 13 Nov 2025 14:33:36 +0000 Subject: [PATCH 1/4] update strings & descriptions --- specparam/plts/event.py | 2 +- specparam/plts/group.py | 8 ++++---- specparam/plts/time.py | 2 +- specparam/reports/save.py | 4 ++-- specparam/reports/strings.py | 20 ++++---------------- specparam/results/results.py | 2 +- specparam/tests/models/test_model.py | 4 ++-- specparam/tests/plts/test_group.py | 6 +++--- 8 files changed, 18 insertions(+), 30 deletions(-) diff --git a/specparam/plts/event.py b/specparam/plts/event.py index 3b99df68d..7a836405b 100644 --- a/specparam/plts/event.py +++ b/specparam/plts/event.py @@ -78,7 +78,7 @@ def plot_event_model(event, **plot_kwargs): color=PARAM_COLORS['presence'], ax=next(axes)) next(axes).axis('off') - # 03: goodness of fit + # 03: metrics for ind, glabel in enumerate(event.results.metrics.labels): plot_param_over_time_yshade(\ None, event.results.event_time_results[glabel], diff --git a/specparam/plts/group.py b/specparam/plts/group.py index a3e00bd08..61e2734e1 100644 --- a/specparam/plts/group.py +++ b/specparam/plts/group.py @@ -53,7 +53,7 @@ def plot_group_model(group, **plot_kwargs): # Goodness of fit plot ax1 = plt.subplot(gs[0, 1]) - plot_group_goodness(group, ax1, **scatter_kwargs, custom_styler=None) + plot_group_metrics(group, ax1, **scatter_kwargs, custom_styler=None) # Center frequencies plot ax2 = plt.subplot(gs[1, :]) @@ -88,8 +88,8 @@ def plot_group_aperiodic(group, ax=None, **plot_kwargs): @savefig @style_plot @check_dependency(plt, 'matplotlib') -def plot_group_goodness(group, ax=None, **plot_kwargs): - """Plot goodness of fit results, in a scatter plot. +def plot_group_metrics(group, ax=None, **plot_kwargs): + """Plot metrics results, in a scatter plot. Parameters ---------- @@ -111,7 +111,7 @@ def plot_group_goodness(group, ax=None, **plot_kwargs): group.results.metrics.flabels[err_ind], group.results.get_metrics(gof_label), group.results.metrics.flabels[gof_ind], - 'Fit Quality', ax=ax) + 'Metrics', ax=ax) @savefig diff --git a/specparam/plts/time.py b/specparam/plts/time.py index 6fa435d0f..5d0cf9f60 100644 --- a/specparam/plts/time.py +++ b/specparam/plts/time.py @@ -66,7 +66,7 @@ def plot_time_model(time, **plot_kwargs): colors=[PARAM_COLORS[plabel] for plabel in time.modes.periodic.params.labels], title='Periodic Parameters - ' + blabel, ax=next(axes)) - # 03: goodness of fit + # 03: metrics err_ind = find_first_ind(time.results.metrics.labels, 'error') gof_ind = find_first_ind(time.results.metrics.labels, 'gof') plot_params_over_time(None, \ diff --git a/specparam/reports/save.py b/specparam/reports/save.py index 41a919b7f..35e215469 100644 --- a/specparam/reports/save.py +++ b/specparam/reports/save.py @@ -3,7 +3,7 @@ from specparam.io.utils import create_file_path from specparam.modutils.dependencies import safe_import, check_dependency from specparam.plts.templates import plot_text -from specparam.plts.group import (plot_group_aperiodic, plot_group_goodness, +from specparam.plts.group import (plot_group_aperiodic, plot_group_metrics, plot_group_peak_frequencies) from specparam.reports.strings import (gen_settings_str, gen_model_results_str, gen_group_results_str, gen_time_results_str, @@ -99,7 +99,7 @@ def save_group_report(group, file_name, file_path=None, add_settings=True): # Goodness of fit plot ax2 = plt.subplot(grid[1, 1]) - plot_group_goodness(group, ax2, custom_styler=None) + plot_group_metrics(group, ax2, custom_styler=None) # Peak center frequencies plot ax3 = plt.subplot(grid[2, :]) diff --git a/specparam/reports/strings.py b/specparam/reports/strings.py index 08af86cf6..dae0fd97a 100644 --- a/specparam/reports/strings.py +++ b/specparam/reports/strings.py @@ -401,21 +401,18 @@ def gen_model_results_str(model, concise=False): _report_str_model(model), '', - # Aperiodic parameters 'Aperiodic Parameters (\'{}\' mode)'.format(model.modes.aperiodic.name), '(' + ', '.join(model.modes.aperiodic.params.labels) + ')', ', '.join(['{:2.4f}'] * \ len(model.results.params.aperiodic.params)).format(*model.results.params.aperiodic.params), '', - # Peak parameters 'Peak Parameters (\'{}\' mode) {} peaks found'.format(\ model.modes.periodic.name, model.results.n_peaks), *[peak_str.format(*op) for op in model.results.params.periodic.params], '', - # Metrics - 'Model fit quality metrics:', + 'Model metrics:', *['{:>18s} is {:1.4f} {:8s}'.format('{:s} ({:s})'.format(*key.split('_')), res, ' ') \ for key, res in model.results.metrics.results.items()], '', @@ -460,7 +457,6 @@ def gen_group_results_str(group, concise=False): _report_str_model(group), '', - # Aperiodic parameters 'Aperiodic Parameters (\'{}\' mode)'.format(group.modes.aperiodic.name), *[el for el in [\ '{:8s} - Min: {:6.2f}, Max: {:6.2f}, Mean: {:5.2f}'.format(label, \ @@ -468,13 +464,11 @@ def gen_group_results_str(group, concise=False): for label in group.modes.aperiodic.params.labels]], '', - # Peak Parameters 'Peak Parameters (\'{}\' mode) {} total peaks found'.format(\ group.modes.periodic.name, sum(group.results.n_peaks)), '', - # Metrics - 'Model fit quality metrics:', + 'Model metrics:', *['{:>18s} - Min: {:6.3f}, Max: {:6.3f}, Mean: {:5.3f}'.format(\ '{:s} ({:s})'.format(*label.split('_')), *compute_arr_desc(group.results.get_metrics(label))) \ @@ -525,7 +519,6 @@ def gen_time_results_str(time, concise=False): _report_str_model(time), '', - # Aperiodic parameters 'Aperiodic Parameters (\'{}\' mode)'.format(time.modes.aperiodic.name), *[el for el in [\ '{:8s} - Min: {:6.2f}, Max: {:6.2f}, Mean: {:5.2f}'.format(label, \ @@ -533,7 +526,6 @@ def gen_time_results_str(time, concise=False): for label in time.modes.aperiodic.params.labels]], '', - # Peak Parameters 'Peak Parameters (\'{}\' mode) - mean values across windows'.format(\ time.modes.periodic.name), *[peak_str.format(*[band_label] + \ @@ -543,8 +535,7 @@ def gen_time_results_str(time, concise=False): for band_label in time.results.bands.labels], '', - # Metrics - 'Model fit quality metrics (values across windows):', + 'Model metrics (values across windows):', *['{:>18s} - Min: {:6.3f}, Max: {:6.3f}, Mean: {:5.3f}'.format(\ '{:s} ({:s})'.format(*key.split('_')), *compute_arr_desc(time.results.time_results[key])) \ @@ -597,7 +588,6 @@ def gen_event_results_str(event, concise=False): _report_str_model(event), '', - # Aperiodic parameters 'Aperiodic Parameters (\'{}\' mode)'.format(event.modes.aperiodic.name), *[el for el in [\ '{:8s} - Min: {:6.2f}, Max: {:6.2f}, Mean: {:5.2f}'.format(label, \ @@ -605,7 +595,6 @@ def gen_event_results_str(event, concise=False): for label in event.modes.aperiodic.params.labels]], '', - # Peak Parameters 'Peak Parameters (\'{}\' mode) - mean values across windows'.format(\ event.modes.periodic.name), *[peak_str.format(*[band_label] + \ @@ -616,8 +605,7 @@ def gen_event_results_str(event, concise=False): for band_label in event.results.bands.labels], '', - # Metrics - 'Model fit quality metrics (values across events):', + 'Model metrics (values across events):', *['{:>18s} - Min: {:6.3f}, Max: {:6.3f}, Mean: {:5.3f}'.format(\ '{:s} ({:s})'.format(*key.split('_')), *compute_arr_desc(np.mean(event.results.event_time_results[key], 1))) \ diff --git a/specparam/results/results.py b/specparam/results/results.py index 8db41d088..1ab1aba1c 100644 --- a/specparam/results/results.py +++ b/specparam/results/results.py @@ -160,7 +160,7 @@ def add_results(self, results): def get_results(self): - """Return model fit parameters and goodness of fit metrics. + """Return model fit parameters and metrics. Returns ------- diff --git a/specparam/tests/models/test_model.py b/specparam/tests/models/test_model.py index ea0bb5ae4..82e39fd44 100644 --- a/specparam/tests/models/test_model.py +++ b/specparam/tests/models/test_model.py @@ -110,7 +110,7 @@ def test_fit_knee(): assert np.allclose(gauss, tfm.results.params.periodic.get_params('fit')[ii], [2.0, 0.5, 1.0]) def test_fit_default_metrics(): - """Test goodness of fit & error metrics, post model fitting.""" + """Test computing metrics, post model fitting.""" tfm = SpectralModel(verbose=False) @@ -118,7 +118,7 @@ def test_fit_default_metrics(): tfm.data.power_spectrum = np.array([1, 2, 3, 4, 5]) tfm.results.model.modeled_spectrum = np.array([1, 2, 5, 4, 5]) - # Check default goodness of fit and error measures + # Check default metrics tfm.results.metrics.compute_metrics(tfm.data, tfm.results) assert np.isclose(tfm.results.metrics.results['error_mae'], 0.4) assert np.isclose(tfm.results.metrics.results['gof_rsquared'], 0.75757575) diff --git a/specparam/tests/plts/test_group.py b/specparam/tests/plts/test_group.py index b62463835..514b6ed6f 100644 --- a/specparam/tests/plts/test_group.py +++ b/specparam/tests/plts/test_group.py @@ -31,10 +31,10 @@ def test_plot_group_aperiodic(tfg, skip_if_no_mpl): file_name='test_plot_group_aperiodic.png') @plot_test -def test_plot_group_goodness(tfg, skip_if_no_mpl): +def test_plot_group_metrics(tfg, skip_if_no_mpl): - plot_group_goodness(tfg, file_path=TEST_PLOTS_PATH, - file_name='test_plot_group_goodness.png') + plot_group_metrics(tfg, file_path=TEST_PLOTS_PATH, + file_name='test_plot_group_metrics.png') @plot_test def test_plot_group_peak_frequencies(tfg, skip_if_no_mpl): From a0c957e556a32919d2ed5c0212a2ecde10a8c381 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Thu, 13 Nov 2025 14:58:37 +0000 Subject: [PATCH 2/4] fix group report metrics for for variable # metrics --- specparam/plts/group.py | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/specparam/plts/group.py b/specparam/plts/group.py index 61e2734e1..f1b6a041d 100644 --- a/specparam/plts/group.py +++ b/specparam/plts/group.py @@ -79,10 +79,10 @@ def plot_group_aperiodic(group, ax=None, **plot_kwargs): if group.modes.aperiodic.name == 'knee': plot_scatter_2(group.results.get_params('aperiodic', 'exponent'), 'Exponent', group.results.get_params('aperiodic', 'knee'), 'Knee', - 'Aperiodic Fit', ax=ax) + 'Aperiodic Parameters', ax=ax) else: plot_scatter_1(group.results.get_params('aperiodic', 'exponent'), 'Exponent', - 'Aperiodic Fit', ax=ax) + 'Aperiodic Parameters', ax=ax) @savefig @@ -101,17 +101,26 @@ def plot_group_metrics(group, ax=None, **plot_kwargs): Additional plot related keyword arguments, with styling options managed by ``style_plot``. """ - # Get indices of metrics to plot - err_ind = find_first_ind(group.results.metrics.labels, 'error') - err_label = group.results.metrics.labels[err_ind] - gof_ind = find_first_ind(group.results.metrics.labels, 'gof') - gof_label = group.results.metrics.labels[gof_ind] + if len(group.results.metrics) == 0: + ax.set(xticks=[], yticks=[]) - plot_scatter_2(group.results.get_metrics(err_label), - group.results.metrics.flabels[err_ind], - group.results.get_metrics(gof_label), - group.results.metrics.flabels[gof_ind], - 'Metrics', ax=ax) + if len(group.results.metrics) == 1: + plot_scatter_1(group.results.get_metrics(group.results.metrics.labels[0]), + group.results.metrics.flabels[0], + 'Metrics', ax=ax) + + elif len(group.results.metrics) >= 2: + ind1 = 0 + ind2 = 1 + if 'error' in group.results.metrics.categories: + ind1 = find_first_ind(group.results.metrics.labels, 'error') + if 'gof' in group.results.metrics.categories: + ind2 = find_first_ind(group.results.metrics.labels, 'gof') + plot_scatter_2(group.results.get_metrics(group.results.metrics.labels[ind1]), + group.results.metrics.flabels[ind1], + group.results.get_metrics(group.results.metrics.labels[ind2]), + group.results.metrics.flabels[ind2], + 'Metrics', ax=ax) @savefig @@ -130,5 +139,5 @@ def plot_group_peak_frequencies(group, ax=None, **plot_kwargs): Additional plot related keyword arguments, with styling options managed by ``style_plot``. """ - plot_hist(group.results.get_params('peak', 0)[:, 0], 'Center Frequency', - 'Peaks - Center Frequencies', x_lims=group.data.freq_range, ax=ax) + plot_hist(group.results.get_params('peak', 'cf')[:, 0], 'Center Frequency', + 'Peak Parameters - Center Frequencies', x_lims=group.data.freq_range, ax=ax) From 7e414a0da0795ec59c911060fd8871ac59232c6f Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Thu, 13 Nov 2025 15:03:08 +0000 Subject: [PATCH 3/4] update group str report for no metrics --- specparam/reports/strings.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/specparam/reports/strings.py b/specparam/reports/strings.py index dae0fd97a..878ebe252 100644 --- a/specparam/reports/strings.py +++ b/specparam/reports/strings.py @@ -467,15 +467,21 @@ def gen_group_results_str(group, concise=False): 'Peak Parameters (\'{}\' mode) {} total peaks found'.format(\ group.modes.periodic.name, sum(group.results.n_peaks)), '', + ] - 'Model metrics:', - *['{:>18s} - Min: {:6.3f}, Max: {:6.3f}, Mean: {:5.3f}'.format(\ - '{:s} ({:s})'.format(*label.split('_')), - *compute_arr_desc(group.results.get_metrics(label))) \ - for label in group.results.metrics.labels], - '', + if len(group.results.metrics) > 0: + str_lst.extend([ + 'Model metrics:', + *['{:>18s} - Min: {:6.3f}, Max: {:6.3f}, Mean: {:5.3f}'.format(\ + '{:s} ({:s})'.format(*label.split('_')), + *compute_arr_desc(group.results.get_metrics(label))) \ + for label in group.results.metrics.labels], + '', + ]) + + str_lst.extend([ DIVIDER, - ] + ]) output = _format(str_lst, concise) From 12564c2912a9c8ca916aadfc751c4d4ebbea5041 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Thu, 13 Nov 2025 15:31:32 +0000 Subject: [PATCH 4/4] generalize gen_methods_txt_str --- specparam/reports/strings.py | 44 ++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/specparam/reports/strings.py b/specparam/reports/strings.py index 878ebe252..778caea65 100644 --- a/specparam/reports/strings.py +++ b/specparam/reports/strings.py @@ -323,7 +323,6 @@ def gen_methods_report_str(concise=False): return output -# TODO: UPDATE def gen_methods_text_str(model=None): """Generate a string representation of a template methods report. @@ -334,32 +333,43 @@ def gen_methods_text_str(model=None): If None, the text is returned as a template, without values. """ - template = ( + if model: + settings_names = list(model.algorithm.settings.values.keys()) + settings_values = list(model.algorithm.settings.values.values()) + else: + settings_names = [] + settings_values = [] + + template = [ "The periodic & aperiodic spectral parameterization algorithm (version {}) " "was used to parameterize neural power spectra. " "The model was fit with {} aperiodic mode and {} periodic mode. " "Settings for the algorithm were set as: " - "peak width limits : {}; " - "max number of peaks : {}; " - "minimum peak height : {}; " - "peak threshold : {}; ." + ] + + if settings_names: + settings_strs = [el + ' : {}, ' for el in settings_names] + settings_strs[-1] = settings_strs[-1][:-2] + '. ' + template.extend(settings_strs) + else: + template.extend('XX. ') + + template.extend([ "Power spectra were parameterized across the frequency range " "{} to {} Hz." - ) + ]) - if model: - freq_range = model.data.freq_range if model.data.has_data else ('XX', 'XX') + if model and model.data.has_data: + freq_range = model.data.freq_range else: freq_range = ('XX', 'XX') - methods_str = template.format(MODULE_VERSION, - model.modes.aperiodic.name if model else 'XX', - model.modes.periodic.name if model else 'XX', - model.algorithm.settings.peak_width_limits if model else 'XX', - model.algorithm.settings.max_n_peaks if model else 'XX', - model.algorithm.settings.min_peak_height if model else 'XX', - model.algorithm.settings.peak_threshold if model else 'XX', - *freq_range) + methods_str = ''.join(template).format(\ + MODULE_VERSION, + model.modes.aperiodic.name if model else 'XX', + model.modes.periodic.name if model else 'XX', + *settings_values, + *freq_range) return methods_str