diff --git a/fooof/core/info.py b/fooof/core/info.py index 9cd9c3e4..f6303a8a 100644 --- a/fooof/core/info.py +++ b/fooof/core/info.py @@ -25,7 +25,7 @@ def get_description(): """ attributes = {'results' : ['aperiodic_params_', 'gaussian_params_', 'peak_params_', - 'r_squared_', 'error_'], + 'r_squared_', 'adj_r_squared_', 'error_'], 'settings' : ['peak_width_limits', 'max_n_peaks', 'min_peak_height', 'peak_threshold', 'aperiodic_mode'], diff --git a/fooof/core/strings.py b/fooof/core/strings.py index ad0e34c5..c2fc4c2b 100644 --- a/fooof/core/strings.py +++ b/fooof/core/strings.py @@ -312,6 +312,7 @@ def gen_results_fm_str(fm, concise=False): # Goodness if fit 'Goodness of fit metrics:', 'R^2 of model fit is {:5.4f}'.format(fm.r_squared_), + 'Adjusted R^2 of model fit is {:5.4f}'.format(fm.adj_r_squared_), 'Error of the fit is {:5.4f}'.format(fm.error_), '', @@ -351,6 +352,7 @@ def gen_results_fg_str(fg, concise=False): # Extract all the relevant data for printing n_peaks = len(fg.get_params('peak_params')) r2s = fg.get_params('r_squared') + adj_r2s = fg.get_params('adj_r_squared') errors = fg.get_params('error') exps = fg.get_params('aperiodic_params', 'exponent') kns = fg.get_params('aperiodic_params', 'knee') \ @@ -399,6 +401,8 @@ def gen_results_fg_str(fg, concise=False): 'Goodness of fit metrics:', ' R2s - Min: {:6.3f}, Max: {:6.3f}, Mean: {:5.3f}' .format(np.nanmin(r2s), np.nanmax(r2s), np.nanmean(r2s)), + 'Adj R2s - Min: {:6.3f}, Max: {:6.3f}, Mean: {:5.3f}' + .format(np.nanmin(adj_r2s), np.nanmax(adj_r2s), np.nanmean(adj_r2s)), 'Errors - Min: {:6.3f}, Max: {:6.3f}, Mean: {:5.3f}' .format(np.nanmin(errors), np.nanmax(errors), np.nanmean(errors)), '', diff --git a/fooof/data/data.py b/fooof/data/data.py index aef669f1..d5f05905 100644 --- a/fooof/data/data.py +++ b/fooof/data/data.py @@ -56,7 +56,8 @@ class FOOOFMetaData(namedtuple('FOOOFMetaData', ['freq_range', 'freq_res'])): class FOOOFResults(namedtuple('FOOOFResults', ['aperiodic_params', 'peak_params', - 'r_squared', 'error', 'gaussian_params'])): + 'r_squared', 'adj_r_squared', 'error', + 'gaussian_params'])): """Model results from parameterizing a power spectrum. Parameters @@ -68,6 +69,9 @@ class FOOOFResults(namedtuple('FOOOFResults', ['aperiodic_params', 'peak_params' Fitted parameter values for the peaks. Each row is a peak, as [CF, PW, BW]. r_squared : float R-squared of the fit between the full model fit and the input data. + adj_r_squared : float + R-squared of the fit between the full model fit and the input data, + adjusted for the number of parameters in the model. error : float Error of the full model fit. gaussian_params : 2d array diff --git a/fooof/objs/fit.py b/fooof/objs/fit.py index 827afa9e..e4aa9786 100644 --- a/fooof/objs/fit.py +++ b/fooof/objs/fit.py @@ -127,6 +127,9 @@ class FOOOF(): Each row is a gaussian, as [mean, height, standard deviation]. r_squared_ : float R-squared of the fit between the input power spectrum and the full model fit. + adj_r_squared_ : float + Adjusted R-squared of the fit between the input power spectrum and the full model fit, + adjusted for the number of parameters in the model. error_ : float Error of the full model fit. n_peaks_ : int @@ -281,6 +284,7 @@ def _reset_data_results(self, clear_freqs=False, clear_spectrum=False, clear_res self.gaussian_params_ = np.empty([0, 3]) self.peak_params_ = np.empty([0, 3]) self.r_squared_ = np.nan + self.adj_r_squared_ = np.nan self.error_ = np.nan self.fooofed_spectrum_ = None @@ -367,6 +371,7 @@ def add_results(self, fooof_result): self.gaussian_params_ = fooof_result.gaussian_params self.peak_params_ = fooof_result.peak_params self.r_squared_ = fooof_result.r_squared + self.adj_r_squared_ = fooof_result.adj_r_squared self.error_ = fooof_result.error self._check_loaded_results(fooof_result._asdict()) @@ -567,7 +572,7 @@ def get_params(self, name, col=None): Parameters ---------- - name : {'aperiodic_params', 'peak_params', 'gaussian_params', 'error', 'r_squared'} + name : {'aperiodic_params', 'peak_params', 'gaussian_params', 'error', 'r_squared', 'adj_r_squared'} Name of the data field to extract. col : {'CF', 'PW', 'BW', 'offset', 'knee', 'exponent'} or int, optional Column name / index to extract from selected data, if requested. @@ -1098,8 +1103,14 @@ def _drop_peak_overlap(self, guess): def _calc_r_squared(self): """Calculate the r-squared goodness of fit of the model, compared to the original data.""" + # compute r-squared r_val = np.corrcoef(self.power_spectrum, self.fooofed_spectrum_) self.r_squared_ = r_val[0][1] ** 2 + + # compute adjusted r-squared + n = len(self.power_spectrum) # number of data points + k = len(self.peak_params_) * 3 + len(self.aperiodic_params_) # number of parameters + self.adj_r_squared_ = 1 - (1 - self.r_squared_) * (n - 1) / (n - k - 1) def _calc_error(self, metric=None): diff --git a/fooof/objs/group.py b/fooof/objs/group.py index e26213fb..e17138e1 100644 --- a/fooof/objs/group.py +++ b/fooof/objs/group.py @@ -88,8 +88,8 @@ class FOOOFGroup(FOOOF): and the BW of the peak, is 2*std of the gaussian (as 'two sided' bandwidth). - The FOOOFGroup object inherits from the FOOOF object. As such it also has data attributes (`power_spectrum` & `fooofed_spectrum_`), and parameter attributes - (`aperiodic_params_`, `peak_params_`, `gaussian_params_`, `r_squared_`, `error_`) - which are defined in the context of individual model fits. These attributes are + (`aperiodic_params_`, `peak_params_`, `gaussian_params_`, `r_squared_`, `adj_r_squared_`, + `error_`) which are defined in the context of individual model fits. These attributes are used during the fitting process, but in the group context do not store results post-fitting. Rather, all model fit results are collected and stored into the `group_results` attribute. To access individual parameters of the fit, use @@ -334,7 +334,7 @@ def get_params(self, name, col=None): Parameters ---------- - name : {'aperiodic_params', 'peak_params', 'gaussian_params', 'error', 'r_squared'} + name : {'aperiodic_params', 'peak_params', 'gaussian_params', 'error', 'r_squared', 'adj_r_squared'} Name of the data field to extract across the group. col : {'CF', 'PW', 'BW', 'offset', 'knee', 'exponent'} or int, optional Column name / index to extract from selected data, if requested. diff --git a/fooof/objs/utils.py b/fooof/objs/utils.py index b7a6f052..a4d92718 100644 --- a/fooof/objs/utils.py +++ b/fooof/objs/utils.py @@ -98,10 +98,11 @@ def average_fg(fg, bands, avg_method='mean', regenerate=True): # Goodness of fit measures: extract & average r2 = avg_func(fg.get_params('r_squared')) + adj_r2 = avg_func(fg.get_params('adj_r_squared')) error = avg_func(fg.get_params('error')) # Collect all results together, to be added to FOOOF object - results = FOOOFResults(ap_params, peak_params, r2, error, gauss_params) + results = FOOOFResults(ap_params, peak_params, r2, adj_r2, error, gauss_params) # Create the new FOOOF object, with settings, data info & results fm = FOOOF() diff --git a/fooof/plts/fg.py b/fooof/plts/fg.py index d750e165..db894ec8 100644 --- a/fooof/plts/fg.py +++ b/fooof/plts/fg.py @@ -11,6 +11,7 @@ from fooof.plts.templates import plot_scatter_1, plot_scatter_2, plot_hist from fooof.plts.utils import savefig from fooof.plts.style import style_plot +from fooof.plts.utils import check_ax plt = safe_import('.pyplot', 'matplotlib') gridspec = safe_import('.gridspec', 'matplotlib') @@ -104,8 +105,17 @@ def plot_fg_gf(fg, ax=None, **plot_kwargs): Keyword arguments to pass into the ``style_plot``. """ - plot_scatter_2(fg.get_params('error'), 'Error', - fg.get_params('r_squared'), 'R^2', 'Goodness of Fit', ax=ax) + ax = check_ax(ax) + ax1 = ax.twinx() + + plot_scatter_1(fg.get_params('error'), 'Error', 'Goodness of Fit', color='#1f77b4', ax=ax) + plot_scatter_1(fg.get_params('r_squared'), 'R^2', x_val=1, color='#1f77b4', ax=ax1) + plot_scatter_1(fg.get_params('adj_r_squared'), x_val=2, color='#1f77b4', ax=ax1) + + ax.set(xlim=[-0.5, 2.5], + xticks=[0, 1, 2], + xticklabels=['Error', 'R^2', 'Adj. R^2']) + ax.tick_params(axis='x', labelsize=16) @savefig diff --git a/fooof/plts/templates.py b/fooof/plts/templates.py index 9b1e341b..d4ce83da 100644 --- a/fooof/plts/templates.py +++ b/fooof/plts/templates.py @@ -17,7 +17,7 @@ ################################################################################################### @check_dependency(plt, 'matplotlib') -def plot_scatter_1(data, label=None, title=None, x_val=0, ax=None): +def plot_scatter_1(data, label=None, title=None, x_val=0, color=None, ax=None): """Plot a scatter plot, with a single y-axis. Parameters @@ -30,6 +30,9 @@ def plot_scatter_1(data, label=None, title=None, x_val=0, ax=None): Title for the plot. x_val : int, optional, default: 0 Position along the x-axis to plot set of data. + color : color, optional, default: None + Color of data points plotted ('c' argument for pyplot.scatter). + None will use default color. ax : matplotlib.Axes, optional Figure axes upon which to plot. @@ -43,7 +46,11 @@ def plot_scatter_1(data, label=None, title=None, x_val=0, ax=None): # Create x-axis data, with small jitter for visualization purposes x_data = np.ones_like(data) * x_val + np.random.normal(0, 0.025, data.shape) - ax.scatter(x_data, data, s=36, alpha=set_alpha(len(data))) + # Plot the data + if color is None: + ax.scatter(x_data, data, s=36, alpha=set_alpha(len(data))) + else: + ax.scatter(x_data, data, s=36, alpha=set_alpha(len(data)), c=color) if label: ax.set_ylabel(label, fontsize=16) diff --git a/fooof/tests/data/test_data.py b/fooof/tests/data/test_data.py index 85a77cbb..a0e90215 100644 --- a/fooof/tests/data/test_data.py +++ b/fooof/tests/data/test_data.py @@ -29,7 +29,7 @@ def test_fooof_meta_data(): def test_fooof_results(): - results = FOOOFResults([1, 1], [10, 0.5, 1], 0.95, 0.05, [10, 0.5, 0.5]) + results = FOOOFResults([1, 1], [10, 0.5, 1], 0.95, 0.94, 0.05, [10, 0.5, 0.5]) assert results results_fields = OBJ_DESC['results'] diff --git a/fooof/tests/objs/test_fit.py b/fooof/tests/objs/test_fit.py index b568ceca..59cefa84 100644 --- a/fooof/tests/objs/test_fit.py +++ b/fooof/tests/objs/test_fit.py @@ -243,7 +243,7 @@ def test_add_data(): # Test that prior data does not get cleared, when requesting not to clear tfm._reset_data_results(True, True, True) - tfm.add_results(FOOOFResults([1, 1], [10, 0.5, 0.5], 0.95, 0.02, [10, 0.5, 0.25])) + tfm.add_results(FOOOFResults([1, 1], [10, 0.5, 0.5], 0.95, 0.94, 0.02, [10, 0.5, 0.25])) tfm.add_data(freqs, pows, clear_results=False) assert tfm.has_data assert tfm.has_model @@ -285,7 +285,7 @@ def test_add_results(): tfm = get_tfm() # Test adding results - fooof_results = FOOOFResults([1, 1], [10, 0.5, 0.5], 0.95, 0.02, [10, 0.5, 0.25]) + fooof_results = FOOOFResults([1, 1], [10, 0.5, 0.5], 0.95, 0.94, 0.02, [10, 0.5, 0.25]) tfm.add_results(fooof_results) assert tfm.has_model for setting in OBJ_DESC['results']: