def test_mle_fit_psycho(self): expected = { 'weibull50': (np.array([0.0034045, 3.9029162, .1119576]), -334.1149693046583), 'weibull': (np.array([0.00316341, 1.72552866, 0.1032307]), -261.235178611311), 'erf_psycho': (np.array([-9.78747259, 10., 0.15967605]), -193.0509031440323), 'erf_psycho_2gammas': (np.array([-11.45463779, 9.9999999, 0.24117732, 0.0270835]), -147.02380025592902) } for model in self.test_data.keys(): pars, L = psy.mle_fit_psycho(self.test_data[model], P_model=model, nfits=10) expected_pars, expected_L = expected[model] self.assertTrue(np.allclose(expected_pars, pars, atol=1e-3), f'unexpected pars for {model}') self.assertTrue(np.isclose(expected_L, L, atol=1e-3), f'unexpected likelihood for {model}') # Test on of the models with function pars params = { 'parmin': np.array([-5., 10., 0.]), 'parmax': np.array([5., 15., .1]), 'parstart': np.array([0., 11., 0.1]), 'nfits': 5 } model = 'erf_psycho' pars, L = psy.mle_fit_psycho(self.test_data[model], P_model=model, **params) expected = [-5, 15, 0.1] self.assertTrue(np.allclose(expected, pars, rtol=.01), f'unexpected pars for {model}') self.assertTrue(np.isclose(-195.55603, L, atol=1e-5), f'unexpected likelihood for {model}')
def compute_psychometric(trials, signed_contrast=None, block=None, plotting=False): """ Compute psychometric fit parameters for trials object :param trials: trials object that must contain contrastLeft, contrastRight and probabilityLeft :type trials: dict :param signed_contrast: array of signed contrasts in percent, where -ve values are on the left :type signed_contrast: np.array :param block: biased block can be either 0.2 or 0.8 :type block: float :return: array of psychometric fit parameters - bias, threshold, lapse high, lapse low """ if signed_contrast is None: signed_contrast = get_signed_contrast(trials) if block is None: block_idx = np.full(trials.probabilityLeft.shape, True, dtype=bool) else: block_idx = trials.probabilityLeft == block if not np.any(block_idx): return np.nan * np.zeros(4) prob_choose_right, contrasts, n_contrasts = compute_performance( trials, signed_contrast=signed_contrast, block=block, prob_right=True) if plotting: psych, _ = psy.mle_fit_psycho(np.vstack( [contrasts, n_contrasts, prob_choose_right]), P_model='erf_psycho_2gammas', parstart=np.array([0., 40., 0.1, 0.1]), parmin=np.array([-50., 10., 0., 0.]), parmax=np.array([50., 50., 0.2, 0.2]), nfits=10) else: psych, _ = psy.mle_fit_psycho( np.vstack([contrasts, n_contrasts, prob_choose_right]), P_model='erf_psycho_2gammas', parstart=np.array([np.mean(contrasts), 20., 0.05, 0.05]), parmin=np.array([np.min(contrasts), 0., 0., 0.]), parmax=np.array([np.max(contrasts), 100., 1, 1])) return psych
def fit_psychfunc(df): choicedat = df.groupby('signed_contrast').agg({ 'choice': 'count', 'choice2': 'mean' }).reset_index() if len(choicedat) >= 4: # need some minimum number of unique x-values pars, L = psy.mle_fit_psycho( choicedat.values.transpose(), P_model='erf_psycho_2gammas', parstart=np.array([0, 20., 0.05, 0.05]), parmin=np.array([choicedat['signed_contrast'].min(), 5, 0., 0.]), parmax=np.array([choicedat['signed_contrast'].max(), 40., 1, 1])) else: pars = [np.nan, np.nan, np.nan, np.nan] df2 = { 'bias': pars[0], 'threshold': pars[1], 'lapselow': pars[2], 'lapsehigh': pars[3] } df2 = pd.DataFrame(df2, index=[0]) df2['ntrials'] = df['choice'].count() return df2
def compute_psychometric(trials, signed_contrast=None, block=None): """ Compute psychometric fit parameters for trials object :param trials: trials object that must contain contrastLeft, contrastRight and probabilityLeft :type trials: dict :param signed_contrast: array of signed contrasts in percent, where -ve values are on the left :type signed_contrast: np.array :param block: biased block can be either 0.2 or 0.8 :type block: float :return: array of psychometric fit parameters - bias, threshold, lapse high, lapse low """ if signed_contrast is None: signed_contrast = get_signed_contrast(trials) if block is None: block_idx = np.full(trials.probabilityLeft.shape, True, dtype=bool) else: block_idx = trials.probabilityLeft == block contrasts, n_contrasts = np.unique(signed_contrast[block_idx], return_counts=True) rightward = trials.choice == -1 # Calculate the proportion rightward for each contrast type prob_choose_right = np.vectorize(lambda x: np.mean(rightward[(x == signed_contrast) & block_idx]))(contrasts) psych, _ = psy.mle_fit_psycho( np.vstack([contrasts, n_contrasts, prob_choose_right]), P_model='erf_psycho_2gammas', parstart=np.array([np.mean(contrasts), 20., 0.05, 0.05]), parmin=np.array([np.min(contrasts), 0., 0., 0.]), parmax=np.array([np.max(contrasts), 100., 1, 1])) return psych
def plot_psychometric(x, y, subj, **kwargs): # summary stats - average psychfunc over observers df = pd.DataFrame({ 'signed_contrast': x, 'choice': y, 'choice2': y, 'subject_nickname': subj }) df2 = df.groupby(['signed_contrast', 'subject_nickname']).agg({ 'choice2': 'count', 'choice': 'mean' }).reset_index() df2.rename(columns={ "choice2": "ntrials", "choice": "fraction" }, inplace=True) df2 = df2.groupby(['signed_contrast']).mean().reset_index() df2 = df2[['signed_contrast', 'ntrials', 'fraction']] # only 'break' the x-axis and remove 50% contrast when 0% is present # print(df2.signed_contrast.unique()) if 0. in df2.signed_contrast.values: brokenXaxis = True else: brokenXaxis = False # fit psychfunc pars, L = psy.mle_fit_psycho( df2.transpose().values, # extract the data from the df P_model='erf_psycho_2gammas', parstart=np.array([0, 20., 0.05, 0.05]), parmin=np.array([df2['signed_contrast'].min(), 5, 0., 0.]), parmax=np.array([df2['signed_contrast'].max(), 40., 1, 1])) if brokenXaxis: # plot psychfunc g = sns.lineplot(np.arange(-27, 27), psy.erf_psycho_2gammas(pars, np.arange(-27, 27)), **kwargs) # plot psychfunc: -100, +100 sns.lineplot(np.arange(-36, -31), psy.erf_psycho_2gammas(pars, np.arange(-103, -98)), **kwargs) sns.lineplot(np.arange(31, 36), psy.erf_psycho_2gammas(pars, np.arange(98, 103)), **kwargs) # if there are any points at -50, 50 left, remove those if 50 in df.signed_contrast.values or -50 in df.signed_contrast.values: df.drop(df[(df['signed_contrast'] == -50.) | (df['signed_contrast'] == 50)].index, inplace=True) # now break the x-axis df['signed_contrast'] = df['signed_contrast'].replace(-100, -35) df['signed_contrast'] = df['signed_contrast'].replace(100, 35) else: # plot psychfunc g = sns.lineplot(np.arange(-103, 103), psy.erf_psycho_2gammas(pars, np.arange(-103, 103)), **kwargs) df3 = df.groupby(['signed_contrast', 'subject_nickname']).agg({ 'choice2': 'count', 'choice': 'mean' }).reset_index() # plot datapoints with errorbars on top if df['subject_nickname'].nunique() > 1: # put the kwargs into a merged dict, so that overriding does not cause an error sns.lineplot( df3['signed_contrast'], df3['choice'], **{ **{ 'err_style': "bars", 'linewidth': 0, 'linestyle': 'None', 'mew': 0.5, 'marker': 'o', 'ci': 68 }, **kwargs }) if brokenXaxis: g.set_xticks([-35, -25, -12.5, 0, 12.5, 25, 35]) g.set_xticklabels(['-100', '-25', '-12.5', '0', '12.5', '25', '100'], size='small', rotation=60) g.set_xlim([-40, 40]) break_xaxis(y=-0.004) else: g.set_xticks([-100, -50, 0, 50, 100]) g.set_xticklabels(['-100', '-50', '0', '50', '100'], size='small', rotation=60) g.set_xlim([-110, 110]) g.set_ylim([0, 1.02]) g.set_yticks([0, 0.25, 0.5, 0.75, 1]) g.set_yticklabels(['0', '25', '50', '75', '100'])