Пример #1
0
    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}')
Пример #2
0
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
Пример #4
0
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'])