def build(self, spec, reset=True): ''' Compile the PyMC3 model from an abstract model specification. Args: spec (Model): A bambi Model instance containing the abstract specification of the model to compile. reset (bool): if True (default), resets the PyMC3BackEnd instance before compiling. ''' if reset: self.reset() with self.model: self.mu = 0. for t in spec.terms.values(): data = t.data label = t.name dist_name = t.prior.name dist_args = t.prior.args # Effects w/ hyperparameters (i.e., random effects) if isinstance(data, dict): for level, level_data in data.items(): n_cols = level_data.shape[1] mu_label = 'u_%s_%s' % (label, level) u = self._build_dist(mu_label, dist_name, shape=n_cols, **dist_args) self.mu += pm.dot(level_data, u)[:, None] else: prefix = 'u_' if t.random else 'b_' n_cols = data.shape[1] coef = self._build_dist(prefix + label, dist_name, shape=n_cols, **dist_args) self.mu += pm.dot(data, coef)[:, None] y = spec.y.data y_prior = spec.family.prior link_f = spec.family.link if not callable(link_f): link_f = self.links[link_f] y_prior.args[spec.family.parent] = link_f(self.mu) y_prior.args['observed'] = y y_like = self._build_dist(spec.y.name, y_prior.name, **y_prior.args) self.spec = spec
def build_model(self): wells = pm.get_data_file('pymc3.examples', 'data/wells.dat') data = pd.read_csv(wells, delimiter=u' ', index_col=u'id', dtype={u'switch': np.int8}) data.dist /= 100 data.educ /= 4 col = data.columns P = data[col[1:]] P -= P.mean() P['1'] = 1 with pm.Model() as model: effects = pm.Normal('effects', mu=0, tau=100. ** -2, shape=len(P.columns)) p = pm.sigmoid(pm.dot(np.array(P), effects)) pm.Bernoulli('s', p, observed=np.array(data.switch)) return model
def _setup_y(self, y_data, ar, by_run): from theano import shared from theano import tensor as T ''' Sets up y to be a theano shared variable. ''' if 'y' not in self.shared_params: self.shared_params['y'] = shared(y_data) with self.model: n_vols = self.dataset.n_vols n_runs = int(len(y_data) / n_vols) for i in range(1, ar+1): _pad = shared(np.zeros((i,))) _trunc = self.shared_params['y'][:-i] y_shifted = T.concatenate((_pad, _trunc)) weights = np.r_[np.zeros(i), np.ones(n_vols-i)] # Model an AR term for each run or use just one for all runs if by_run: smoother = pm.Cauchy('AR(%d)' % i, alpha=0, beta=1, shape=n_runs) weights = np.outer(weights, np.eye(n_runs)) weights = np.reshape(weights, (n_vols*n_runs, n_runs), order='F') _ar = pm.dot(weights, smoother) * y_shifted else: smoother = pm.Cauchy('AR(%d)' % i, alpha=0, beta=1) weights = np.tile(weights, n_runs) _ar = shared(weights) * y_shifted * smoother self.mu += _ar sigma = pm.HalfCauchy('sigma_y_obs', beta=10) y_obs = pm.Normal('Y_obs', mu=self.mu, sd=sigma, observed=self.shared_params['y']) else: self.shared_params['y'].set_value(y_data)
def simulate(num_subs, num_stims, A_mean, B_mean, sub_A_sd, sub_B_sd, stim_A_sd, stim_B_sd, resid_sd, ar=None, block_size=None): # build stimulus list stims = np.random.normal(size=num_stims//2, loc=1, scale=stim_A_sd/A_mean).tolist() + \ np.random.normal(size=num_stims//2, loc=1, scale=stim_B_sd/B_mean).tolist() stims = pd.DataFrame({'stim':range(num_stims), 'condition':np.repeat([0,1], num_stims//2), 'effect':np.array(stims)}) # now build design matrix from stimulus list if block_size is None: # build event-related design data = pd.concat([build_seq(sub_num=i, stims=stims, sub_A_sd=sub_A_sd, sub_B_sd=sub_B_sd) for i in range(num_subs)]) else: # build blocked design data = pd.concat([build_seq_block(sub_num=i, stims=stims, sub_A_sd=sub_A_sd, sub_B_sd=sub_B_sd, block_size=block_size) for i in range(num_subs)]) # add response variable and difference predictor if ar is None: # build y WITHOUT AR(2) errors data['y'] = (A_mean + data['sub_A'])*data.iloc[:,:(num_stims//2)].sum(axis=1).values + \ (B_mean + data['sub_B'])*data.iloc[:,(num_stims//2):num_stims].sum(axis=1).values + \ np.random.normal(size=len(data.index), scale=resid_sd) else: # build y WITH AR(2) errors data['y'] = np.empty(len(data.index)) data['y_t-1'] = np.zeros(len(data.index)) data['y_t-2'] = np.zeros(len(data.index)) for t in range(len(pd.unique(data['time']))): data.loc[t,'y'] = pd.DataFrame( (A_mean + data.loc[t,'sub_A'])*data.loc[t, range(num_stims//2)].sum(axis=1).values + \ (B_mean + data.loc[t,'sub_B'])*data.loc[t, range(num_stims//2, num_stims)].sum(axis=1).values + \ np.random.normal(size=len(data.loc[t].index), scale=resid_sd)).values if t==1: data.loc[t,'y'] = pd.DataFrame(data.loc[t,'y'].values + ar[0]*data.loc[t-1,'y'].values).values data.loc[t,'y_t-1'] = pd.DataFrame(data.loc[t-1,'y']).values if t>1: data.loc[t,'y'] = pd.DataFrame(data.loc[t,'y'].values + ar[0]*data.loc[t-1,'y'].values + ar[1]*data.loc[t-2,'y'].values).values data.loc[t,'y_t-1'] = pd.DataFrame(data.loc[t-1,'y']).values data.loc[t,'y_t-2'] = pd.DataFrame(data.loc[t-2,'y']).values # remove random stimulus effects from regressors before fitting model data.iloc[:, :num_stims] = data.iloc[:, :num_stims] / stims['effect'].tolist() # build design DataFrame # create num_subs * num_stims DataFrame # where each cell is when that stim was presented for that sub # note that this depends on there being no repeated stimulus presentations gb = data.groupby('sub_num') pres = pd.DataFrame([[next(i-1 for i, val in enumerate(df.iloc[:,stim]) if abs(val) > .0001) for stim in range(num_stims)] for sub_num, df in gb]) # build the design DataFrame from pres design = pd.concat([pd.DataFrame({'onset':pres.iloc[sub,:].sort_values(), 'run_onset':pres.iloc[sub,:].sort_values(), 'stimulus':pres.iloc[sub,:].sort_values().index, 'subject':sub, 'duration':1, 'amplitude':1, 'run':1, 'index':range(pres.shape[1])}) for sub in range(num_subs)]) design['condition'] = stims['condition'][design['stimulus']] # build activation DataFrame activation = pd.DataFrame({'y':data['y'].values, 'vol':data['time'], 'run':1, 'subject':data['sub_num']}) # build Dataset object dataset = nipymc.data.Dataset(design=design, activation=activation, TR=1) #################################### ############ FIT MODELS ############ #################################### # SPM model def get_diff(df): X = pd.concat([df.iloc[:,:num_stims//2].sum(axis=1), df.iloc[:,num_stims//2:num_stims].sum(axis=1), df['y_t-1'], df['y_t-2']], axis=1) beta = pd.stats.api.ols(y=df['y'], x=X, intercept=False).beta return pd.Series(beta[1] - beta[0]).append(beta) sub_diffs = data.groupby('sub_num').apply(get_diff) # fit model with FIXED stim effects (LS-All model) with pm.Model(): # Fixed effects b = pm.Normal('fixstim_b', mu=0, sd=10, shape=num_stims) if ar is not None: ar1 = pm.Cauchy('fixstim_AR1', alpha=0, beta=1) ar2 = pm.Cauchy('fixstim_AR2', alpha=0, beta=1) # random x1 & x2 slopes for participants sigma_sub_A = pm.HalfCauchy('fixstim_sigma_sub_A', beta=10) sigma_sub_B = pm.HalfCauchy('fixstim_sigma_sub_B', beta=10) u0 = pm.Normal('u0_sub_A_log', mu=0., sd=sigma_sub_A, shape=data['sub_num'].nunique()) u1 = pm.Normal('u1_sub_B_log', mu=0., sd=sigma_sub_B, shape=data['sub_num'].nunique()) # now write the mean model mu = u0[data['sub_num'].values]*data.iloc[:,:(num_stims//2)].sum(axis=1).values + \ u1[data['sub_num'].values]*data.iloc[:,(num_stims//2):num_stims].sum(axis=1).values + \ pm.dot(data.iloc[:, :num_stims].values, b) if ar is not None: mu += ar1*data['y_t-1'].values + ar2*data['y_t-2'].values # define the condition contrast cond_diff = Deterministic('fixstim_cond_diff', T.mean(b[num_stims//2:]) - T.mean(b[:num_stims//2])) # model for the observed values Y_obs = pm.Normal('Y_obs', mu=mu, sd=pm.HalfCauchy('fixstim_sigma', beta=10), observed=data['y'].values) # run the sampler step = pm.NUTS() print('fitting fixstim model...') trace0 = pm.sample(SAMPLES, step=step, progressbar=False) # fit model WITHOUT random stim effects with pm.Model(): # Fixed effects b1 = pm.Normal('nostim_b_A', mu=0, sd=10) b2 = pm.Normal('nostim_b_B', mu=0, sd=10) if ar is not None: ar1 = pm.Cauchy('nostim_AR1', alpha=0, beta=1) ar2 = pm.Cauchy('nostim_AR2', alpha=0, beta=1) # random x1 & x2 slopes for participants sigma_sub_A = pm.HalfCauchy('nostim_sigma_sub_A', beta=10) sigma_sub_B = pm.HalfCauchy('nostim_sigma_sub_B', beta=10) u0 = pm.Normal('u0_sub_A_log', mu=0., sd=sigma_sub_A, shape=data['sub_num'].nunique()) u1 = pm.Normal('u1_sub_B_log', mu=0., sd=sigma_sub_B, shape=data['sub_num'].nunique()) # now write the mean model mu = (b1 + u0[data['sub_num'].values])*data.iloc[:,:(num_stims//2)].sum(axis=1).values + \ (b2 + u1[data['sub_num'].values])*data.iloc[:,(num_stims//2):num_stims].sum(axis=1).values if ar is not None: mu += ar1*data['y_t-1'].values + ar2*data['y_t-2'].values # define the condition contrast cond_diff = Deterministic('nostim_cond_diff', b2 - b1) # model for the observed values Y_obs = pm.Normal('Y_obs', mu=mu, sd=pm.HalfCauchy('nostim_sigma', beta=10), observed=data['y'].values) # run the sampler step = pm.NUTS() print('fitting nostim model...') trace1 = pm.sample(SAMPLES, step=step, progressbar=False) # fit model with separate dists + variances with pm.Model(): # Fixed effects b1 = pm.Normal('randstim_b_A', mu=0, sd=10) b2 = pm.Normal('randstim_b_B', mu=0, sd=10) if ar is not None: ar1 = pm.Cauchy('randstim_AR1', alpha=0, beta=1) ar2 = pm.Cauchy('randstim_AR2', alpha=0, beta=1) # random x1 & x2 slopes for participants sigma_sub_A = pm.HalfCauchy('randstim_sigma_sub_A', beta=10) sigma_sub_B = pm.HalfCauchy('randstim_sigma_sub_B', beta=10) u0 = pm.Normal('u0_sub_A_log', mu=0., sd=sigma_sub_A, shape=data['sub_num'].nunique()) u1 = pm.Normal('u1_sub_B_log', mu=0., sd=sigma_sub_B, shape=data['sub_num'].nunique()) # random stim intercepts sigma_stim_A = pm.HalfCauchy('randstim_sigma_stim_A', beta=10) u2 = pm.Normal('randstim_stim_A', mu=0., sd=sigma_stim_A, shape=num_stims//2) sigma_stim_B = pm.HalfCauchy('randstim_sigma_stim_B', beta=10) u3 = pm.Normal('randstim_stim_B', mu=0., sd=sigma_stim_B, shape=num_stims//2) # now write the mean model mu = (b1 + u0[data['sub_num'].values])*data.iloc[:,:(num_stims//2)].sum(axis=1).values + \ (b2 + u1[data['sub_num'].values])*data.iloc[:,(num_stims//2):num_stims].sum(axis=1).values + \ pm.dot(data.iloc[:, :num_stims//2].values, u2) + pm.dot(data.iloc[:, (num_stims//2):num_stims].values, u3) if ar is not None: mu += ar1*data['y_t-1'].values + ar2*data['y_t-2'].values # define the condition contrast cond_diff = Deterministic('randstim_cond_diff', b2 - b1) # model for the observed values Y_obs = pm.Normal('Y_obs', mu=mu, sd=pm.HalfCauchy('randstim_sigma', beta=10), observed=data['y'].values) # run the sampler step = pm.NUTS() print('fitting 2dist2var model...') trace2 = pm.sample(SAMPLES, step=step, progressbar=False) # fit FIX_STIM model using pymcwrap mod3 = nipymc.model.BayesianModel(dataset) mod3.add_term('subject', label='nipymc_fixstim_subject', split_by='condition', categorical=True, random=True) mod3.add_term('stimulus', label='nipymc_fixstim_stimulus', categorical=True) mod3.groupA = [mod3.level_map['nipymc_fixstim_stimulus'][i] for i in range(num_stims//2)] mod3.groupB = [mod3.level_map['nipymc_fixstim_stimulus'][i] for i in range(num_stims//2, num_stims)] mod3.add_deterministic('nipymc_fixstim_cond_diff', "T.mean(self.dists['b_nipymc_fixstim_stimulus'][self.groupB]) - T.mean(self.dists['b_nipymc_fixstim_stimulus'][self.groupA])") mod3.set_y('y', scale=None, detrend=False, ar=0 if ar is None else 2) print('fitting nipymc_fixstim model...') mod3_fitted = mod3.run(samples=SAMPLES, verbose=False, find_map=False) # fit NO_STIM model using pymcwrap mod4 = nipymc.model.BayesianModel(dataset) mod4.add_term('condition', label='nipymc_nostim_condition', categorical=True, scale=False) mod4.add_term('subject', label='nipymc_nostim_subject', split_by='condition', categorical=True, random=True) groupA = str(mod4.level_map['nipymc_nostim_condition'][0]) groupB = str(mod4.level_map['nipymc_nostim_condition'][1]) mod4.add_deterministic('nipymc_nostim_cond_diff', "self.dists['b_nipymc_nostim_condition']["+groupB+"] - self.dists['b_nipymc_nostim_condition']["+groupA+"]") mod4.set_y('y', scale=None, detrend=False, ar=0 if ar is None else 2) print('fitting nipymc_nostim model...') mod4_fitted = mod4.run(samples=SAMPLES, verbose=False, find_map=False) # fit 2dist2var model using pymcwrap mod5 = nipymc.model.BayesianModel(dataset) mod5.add_term('condition', label='nipymc_randstim_condition', categorical=True, scale=False) mod5.add_term('stimulus', label='nipymc_randstim_stimulus', split_by='condition', categorical=True, random=True) mod5.add_term('subject', label='nipymc_randstim_subject', split_by='condition', categorical=True, random=True) groupA = str(mod5.level_map['nipymc_randstim_condition'][0]) groupB = str(mod5.level_map['nipymc_randstim_condition'][1]) mod5.add_deterministic('nipymc_randstim_cond_diff', "self.dists['b_nipymc_randstim_condition']["+groupB+"] - self.dists['b_nipymc_randstim_condition']["+groupA+"]") mod5.set_y('y', scale=None, detrend=False, ar=0 if ar is None else 2) print('fitting nipymc_randstim model...') mod5_fitted = mod5.run(samples=SAMPLES, verbose=False, find_map=False) # # save PNG of traceplot # plt.figure() # pm.traceplot(trace2[BURN:]) # plt.savefig('pymc3_randstim.png') # plt.close() # plt.figure() # pm.traceplot(mod5_fitted.trace[BURN:]) # plt.savefig('nipymc_randstim.png') # plt.close() ###################################### ########## SAVE RESULTS ############## ###################################### # return parameter estimates print('computing and returning parameter estimates...') # lists of traces and names of their model parameters traces = [trace0, # fixstim trace1, # nostim trace2, # randstim mod3_fitted.trace, # nipymc_fixstim mod4_fitted.trace, # nipymc_nostim mod5_fitted.trace] # nipymc_randstim parlists = [[x for x in trace.varnames if 'log' not in x and 'u_' not in x] for trace in traces] # get posterior mean and SDs as lists of lists means = [[trace[param][BURN:].mean() for param in parlist] for trace, parlist in zip(traces, parlists)] SDs = [[trace[param][BURN:].std() for param in parlist] for trace, parlist in zip(traces, parlists)] # print list of summary statistics stats = sum([['posterior_mean']*len(x) + ['posterior_SD']*len(x) for x in parlists], []) print(stats) print(len(stats)) # print parameter names in the order in which they are saved parlists = [2*parlist for parlist in parlists] extra_params = [] params = [param for parlist in parlists for param in parlist] + extra_params print(params) # add SPM model results ans = [summary for model in zip(means, SDs) for summary in model] ans = [sub_diffs.mean(0).tolist(), (sub_diffs.std(0)/(len(sub_diffs.index)**.5)).tolist()] + ans params = ['SPM_cond_diff','SPM_A_mean','SPM_B_mean','SPM_AR1','SPM_AR2']*2 + params stats = ['posterior_mean']*5 + ['posterior_SD']*5 + stats # add test statistics for all models # grab all posterior means nums = [np.array(x) for x in ans][::2] # grab all posterior SDs denoms = [np.array(x) for x in ans][1::2] # divide them zs = [n/d for n,d in zip(nums,denoms)] zs = sum([x.tolist() for x in zs], []) # keep only the test statistics related to cond_diff labels = [params[i] for i in [j for j,x in enumerate(stats) if x=='posterior_mean']] zs = [(z,l) for z,l in zip(zs,labels) if 'cond_diff' in l] # add them to the results ans = [[x[0] for x in zs]] + ans params = [x[1] for x in zs] + params stats = ['test_statistic']*7 + stats # return the parameter values # for first instance only, also return param names and etc. if int(instance)==0: ans = [ans, params, stats] return ans
def add_term(self, variable, label=None, categorical=False, random=False, split_by=None, yoke_random_mean=False, estimate_random_mean=False, dist='Normal', scale=None, trend=None, orthogonalize=None, convolution=None, conv_kws=None, sigma_kws=None, withhold=False, plot=False, **kwargs): ''' Args: variable (str): name of the variable in the Dataset that contains the predictor data for the term, or a list of variable names. label (str): short name/label of the term; will be used as the name passed to PyMC. If None, the variable name is used. categorical (bool): if False, treat the input data as continuous; if True, treats input as categorical, and assigns discrete levels to different columns in the predictor matrix random (bool): if False, model as fixed effect; if True, model as random effect split_by (str): optional name of another variable on which to split the target variable. A separate hyperparameter will be included for each level in the split_by variable. E.g., if variable = 'stimulus' and split_by = 'category', the model will include one parameter for each individual stimulus, plus C additional hyperparameters for the stimulus variances (one per category). yoke_random_mean (bool): estimate_random_mean (bool): If False (default), set mean of random effect distribution to 0. If True, estimate mean parameters for each level of split_by (in which case the corresponding fixed effect parameter should be omitted, for identifiability reasons). If split_by=None, this is equivalent to estimating a fixed intercept term. Note that models parameterized in this way are often less numerically stable than the default parameterization. dist (str, Distribution): the PyMC3 distribution to use for the prior. Can be either a string (must be the name of a class in pymc3.distributions), or an uninitialized Distribution object. scale (str, bool): if 'before', scaling will be applied before convolving with the HRF. If 'after', scaling will be applied to the convolved regressor. True is treated like 'before'. If None (default), no scaling is done. trend (int): if variable is 'subject' or 'run', passing an int here will result in addition of an Nth-order polynomial trend instead of the expected intercept. E.g., when variable = 'run' and trend = 1, a linear trend will be added for each run. orthogonalize (list): list of variables to orthogonalize the target variable with respect to. For now, this only works for categorical covariates. E.g., if variable = 'condition' and orthogonalize = ['stimulus_category'], each level of condition will be residualized on all (binarized) levels of stimulus condition. convolution (str): the name of the convolution function to apply to the input data; must be a valid function in convolutions.py. If None, the default convolution function set at class initialization is used. If 'none' is passed, no convolution at all is applied. conv_kws (dict): optional dictionary of additional keyword arguments to pass onto the selected convolution function. sigma_kws (dict): optional dictionary of keyword arguments specifying the parameters of the Distribution to use as the sigma for a random variable. Defaults to HalfCauchy with beta=10. Ignored unless random=True. withhold (bool): if True, the PyMC distribution(s) will be created but not added to the prediction equation. This is useful when, e.g., yoking the mean of one distribution to the estimated value of another distribution, without including the same quantity twice. plot (bool): if True, plots the resulting design matrix component. kwargs: optional keyword arguments passed onto the selected PyMC3 Distribution. ''' if label is None: label = '_'.join(listify(variable)) # Load design matrix for requested variable dm = self._get_variable_data(variable, categorical, label=label, trend=trend) n_cols = dm.shape[1] # Handle random effects with nesting/crossing. Basically this splits the design # matrix into a separate matrix for each level of split_by, stacked into 3D array if split_by is not None: split_dm = self._get_variable_data(split_by, True) dm = np.einsum('ab,ac->abc', dm, split_dm) # Orthogonalization # TODO: generalize this to handle any combination of settings; right # now it will only work properly when both the target variable and the # covariates are categorical fixed effects. if orthogonalize is not None: dm = self._orthogonalize(dm, orthogonalize) # Scaling and HRF: apply over last dimension # if there is no split_by, add a dummy 3rd dimension so code below works in general if dm.ndim == 2: dm = dm[..., None] if plot and plot != 'convolved': self.plot_design_matrix(dm, variable, split_by) for i in range(dm.shape[-1]): if scale and scale != 'after': dm[..., i] = standardize(dm[..., i]) # Convolve with HRF if variable not in ['intercept'] and convolution is not 'none': if convolution is None: convolution = self.convolution elif not hasattr(convolution, 'shape'): convolution = get_convolution(convolution, conv_kws) # Convolve each run separately n_vols = self.dataset.n_vols n_runs = int(len(dm) / n_vols) for r in range(n_runs): start, end = r * n_vols, (r * n_vols) + n_vols _convolved = self._convolve(dm[start:end, :, i], convolution) dm[start:end, :, i] = _convolved # np.squeeze(_convolved) if scale == 'after': dm[..., i] = standardize(dm[..., i]) if plot and plot == 'convolved': self.plot_design_matrix(dm, variable, split_by) # remove the dummy 3rd dimension if it was added prior to scaling/convolution if dm.shape[-1] == 1: dm = dm.reshape(dm.shape[:2]) with self.model: # Random effects if random: # User can pass sigma specification in sigma_kws. # If not provided, default to HalfCauchy with beta = 10. if sigma_kws is None: sigma_kws = {'dist': 'HalfCauchy', 'beta': 1} if split_by is None: sigma = self._build_dist('sigma_' + label, **sigma_kws) if estimate_random_mean: mu = self._build_dist('b_' + label, dist) else: mu = 0. u = self._build_dist('u_' + label, dist, mu=mu, sd=sigma, shape=n_cols, **kwargs) self.mu += pm.dot(dm, u) else: # id_map is essentially a crosstab except each cell is either 0 or 1 id_map = self._get_membership_graph(variable, split_by) for i in range(id_map.shape[1]): # select just the factor levels that appear with the # current level of split_by group_items = id_map.iloc[:, i].astype(bool) selected = dm[:, group_items.values, i] # add the level effects to the model name = '%s_%s' % (label, id_map.columns[i]) sigma = self._build_dist('sigma_' + name, **sigma_kws) if yoke_random_mean: mu = self.dists['b_' + split_by][i] elif estimate_random_mean: mu = self._build_dist('b_' + name, dist) else: mu = 0. name, size = 'u_' + name, selected.shape[1] u = self._build_dist(name, dist, mu=mu, sd=sigma, shape=size, **kwargs) self.mu += pm.dot(selected, u) # Update the level map levels = group_items[group_items].index.tolist() self.level_map[name] = OrderedDict( zip(levels, list(range(size)))) # Fixed effects else: b = self._build_dist('b_' + label, dist, shape=dm.shape[-1], **kwargs) if split_by is not None: dm = np.squeeze(dm) if not withhold: self.mu += pm.dot(dm, b)
y = np.sum(x * beta_true[1:].T, axis=1) + beta_true[0] + norm.rvs(0, sd_true, nData) # Select which predictors to include includeOnly = range(0, n_predictors) # default is to include all #x = x.iloc[includeOnly] predictorNames = x.columns n_predictors = len(predictorNames) # THE MODEL with pm.Model() as model: # define the priors beta0 = pm.Normal('beta0', mu=0, tau=1.0E-12) beta1 = pm.Normal('beta1', mu= 0, tau=1.0E-12, shape=n_predictors) tau = pm.Gamma('tau', 0.01, 0.01) mu = beta0 + pm.dot(beta1, x.values.T) # define the likelihood yl = pm.Normal('yl', mu=mu, tau=tau, observed=y) # Generate a MCMC chain start = pm.find_MAP() step1 = pm.NUTS([beta1]) step2 = pm.Metropolis([beta0, tau]) trace = pm.sample(10000, [step1, step2], start, progressbar=False) # EXAMINE THE RESULTS burnin = 5000 thin = 1 # Print summary for each trace #pm.summary(trace[burnin::thin]) #pm.summary(trace)
def add_term(self, variable, label=None, categorical=False, random=False, split_by=None, yoke_random_mean=False, estimate_random_mean=False, dist='Normal', scale=None, trend=None, orthogonalize=None, convolution=None, conv_kws=None, sigma_kws=None, withhold=False, plot=False, **kwargs): ''' Args: variable (str): name of the variable in the Dataset that contains the predictor data for the term, or a list of variable names. label (str): short name/label of the term; will be used as the name passed to PyMC. If None, the variable name is used. categorical (bool): if False, treat the input data as continuous; if True, treats input as categorical, and assigns discrete levels to different columns in the predictor matrix random (bool): if False, model as fixed effect; if True, model as random effect split_by (str): optional name of another variable on which to split the target variable. A separate hyperparameter will be included for each level in the split_by variable. E.g., if variable = 'stimulus' and split_by = 'category', the model will include one parameter for each individual stimulus, plus C additional hyperparameters for the stimulus variances (one per category). yoke_random_mean (bool): estimate_random_mean (bool): If False (default), set mean of random effect distribution to 0. If True, estimate mean parameters for each level of split_by (in which case the corresponding fixed effect parameter should be omitted, for identifiability reasons). If split_by=None, this is equivalent to estimating a fixed intercept term. Note that models parameterized in this way are often less numerically stable than the default parameterization. dist (str, Distribution): the PyMC3 distribution to use for the prior. Can be either a string (must be the name of a class in pymc3.distributions), or an uninitialized Distribution object. scale (str, bool): if 'before', scaling will be applied before convolving with the HRF. If 'after', scaling will be applied to the convolved regressor. True is treated like 'before'. If None (default), no scaling is done. trend (int): if variable is 'subject' or 'run', passing an int here will result in addition of an Nth-order polynomial trend instead of the expected intercept. E.g., when variable = 'run' and trend = 1, a linear trend will be added for each run. orthogonalize (list): list of variables to orthogonalize the target variable with respect to. For now, this only works for categorical covariates. E.g., if variable = 'condition' and orthogonalize = ['stimulus_category'], each level of condition will be residualized on all (binarized) levels of stimulus condition. convolution (str): the name of the convolution function to apply to the input data; must be a valid function in convolutions.py. If None, the default convolution function set at class initialization is used. If 'none' is passed, no convolution at all is applied. conv_kws (dict): optional dictionary of additional keyword arguments to pass onto the selected convolution function. sigma_kws (dict): optional dictionary of keyword arguments specifying the parameters of the Distribution to use as the sigma for a random variable. Defaults to HalfCauchy with beta=10. Ignored unless random=True. withhold (bool): if True, the PyMC distribution(s) will be created but not added to the prediction equation. This is useful when, e.g., yoking the mean of one distribution to the estimated value of another distribution, without including the same quantity twice. plot (bool): if True, plots the resulting design matrix component. kwargs: optional keyword arguments passed onto the selected PyMC3 Distribution. ''' if label is None: label = '_'.join(listify(variable)) # Load design matrix for requested variable dm = self._get_variable_data(variable, categorical, label=label, trend=trend) n_cols = dm.shape[1] # Handle random effects with nesting/crossing. Basically this splits the design # matrix into a separate matrix for each level of split_by, stacked into 3D array if split_by is not None: split_dm = self._get_variable_data(split_by, True) dm = np.einsum('ab,ac->abc', dm, split_dm) # Orthogonalization # TODO: generalize this to handle any combination of settings; right # now it will only work properly when both the target variable and the # covariates are categorical fixed effects. if orthogonalize is not None: dm = self._orthogonalize(dm, orthogonalize) # Scaling and HRF: apply over last dimension # if there is no split_by, add a dummy 3rd dimension so code below works in general if dm.ndim == 2: dm = dm[..., None] for i in range(dm.shape[-1]): if scale and scale != 'after': dm[..., i] = standardize(dm[..., i]) if plot: self.plot_design_matrix(dm, variable, split_by) # Convolve with HRF if variable not in ['subject', 'run', 'intercept'] and convolution is not 'none': if convolution is None: convolution = self.convolution elif not hasattr(convolution, 'shape'): convolution = get_convolution(convolution, conv_kws) _convolved = self._convolve(dm[..., i], convolution) dm[..., i] = _convolved # np.squeeze(_convolved) if scale == 'after': dm[..., i] = standardize(dm[..., i]) # remove the dummy 3rd dimension if it was added prior to scaling/convolution if dm.shape[-1] == 1: dm = dm.reshape(dm.shape[:2]) with self.model: # Random effects if random: # User can pass sigma specification in sigma_kws. # If not provided, default to HalfCauchy with beta = 10. if sigma_kws is None: sigma_kws = {'dist': 'HalfCauchy', 'beta': 10} if split_by is None: sigma = self._build_dist('sigma_' + label, **sigma_kws) if estimate_random_mean: mu = self._build_dist('b_' + label, dist) else: mu = 0. u = self._build_dist('u_' + label, dist, mu=mu, sd=sigma, shape=n_cols, **kwargs) self.mu += pm.dot(dm, u) else: # id_map is essentially a crosstab except each cell is either 0 or 1 id_map = self._get_membership_graph(variable, split_by) for i in range(id_map.shape[1]): # select just the factor levels that appear with the # current level of split_by group_items = id_map.iloc[:, i].astype(bool) selected = dm[:, group_items.values, i] # add the level effects to the model name = '%s_%s' % (label, id_map.columns[i]) sigma = self._build_dist('sigma_' + name, **sigma_kws) if yoke_random_mean: mu = self.dists['b_' + split_by][i] elif estimate_random_mean: mu = self._build_dist('b_' + name, dist) else: mu = 0. name, size = 'u_' + name, selected.shape[1] u = self._build_dist(name, dist, mu=mu, sd=sigma, shape=size, **kwargs) self.mu += pm.dot(selected, u) # Update the level map levels = group_items[group_items].index.tolist() self.level_map[name] = OrderedDict(zip(levels, list(range(size)))) # Fixed effects else: b = self._build_dist('b_' + label, dist, shape=dm.shape[-1], **kwargs) if split_by is not None: dm = np.squeeze(dm) if not withhold: self.mu += pm.dot(dm, b)
# random stimulus model model = pm.Model() with model: # Intercept mu = pm.Normal('intercept', 0, 10) # mu = 0 # Categorical fixed effects betas = {} # cat_fe = ['Valence', 'RACE'] cat_fe = ['Valence'] for cfe in cat_fe: dummies = pd.get_dummies(X[cfe], drop_first=True).values _b = pm.Normal('b_%s' % cfe, 0, 10, shape=dummies.shape[1]) mu += pm.dot(dummies, _b) betas[cfe] = _b # Continuous fixed effects # cont_fe = ['AGE', 'YRS_SCH', 'SEX'] cont_fe = [] for cfe in cont_fe: _b = pm.Normal('b_%s' % cfe, 0, 10) mu += _b * X[cfe].values betas[cfe] = _b # # Contrast between conditions # b_valence = betas['Valence'] # c = pm.Deterministic('valence_contrast', b_valence[0] - b_valence[1]) # Random effects
def simulate(num_subs, num_stims, A_mean, B_mean, sub_A_sd, sub_B_sd, stim_A_sd, stim_B_sd, resid_sd, ar=None, block_size=None): # build stimulus list stims = np.random.normal(size=num_stims//2, loc=1, scale=stim_A_sd/A_mean).tolist() + \ np.random.normal(size=num_stims//2, loc=1, scale=stim_B_sd/B_mean).tolist() stims = pd.DataFrame({ 'stim': range(num_stims), 'condition': np.repeat([0, 1], num_stims // 2), 'effect': np.array(stims) }) # now build design matrix from stimulus list if block_size is None: # build event-related design data = pd.concat([ build_seq(sub_num=i, stims=stims, sub_A_sd=sub_A_sd, sub_B_sd=sub_B_sd) for i in range(num_subs) ]) else: # build blocked design data = pd.concat([ build_seq_block(sub_num=i, stims=stims, sub_A_sd=sub_A_sd, sub_B_sd=sub_B_sd, block_size=block_size) for i in range(num_subs) ]) # add response variable and difference predictor if ar is None: # build y WITHOUT AR(2) errors data['y'] = (A_mean + data['sub_A'])*data.iloc[:,:(num_stims//2)].sum(axis=1).values + \ (B_mean + data['sub_B'])*data.iloc[:,(num_stims//2):num_stims].sum(axis=1).values + \ np.random.normal(size=len(data.index), scale=resid_sd) else: # build y WITH AR(2) errors data['y'] = np.empty(len(data.index)) data['y_t-1'] = np.zeros(len(data.index)) data['y_t-2'] = np.zeros(len(data.index)) for t in range(len(pd.unique(data['time']))): data.loc[t,'y'] = pd.DataFrame( (A_mean + data.loc[t,'sub_A'])*data.loc[t, range(num_stims//2)].sum(axis=1).values + \ (B_mean + data.loc[t,'sub_B'])*data.loc[t, range(num_stims//2, num_stims)].sum(axis=1).values + \ np.random.normal(size=len(data.loc[t].index), scale=resid_sd)).values if t == 1: data.loc[t, 'y'] = pd.DataFrame( data.loc[t, 'y'].values + ar[0] * data.loc[t - 1, 'y'].values).values data.loc[t, 'y_t-1'] = pd.DataFrame(data.loc[t - 1, 'y']).values if t > 1: data.loc[t, 'y'] = pd.DataFrame( data.loc[t, 'y'].values + ar[0] * data.loc[t - 1, 'y'].values + ar[1] * data.loc[t - 2, 'y'].values).values data.loc[t, 'y_t-1'] = pd.DataFrame(data.loc[t - 1, 'y']).values data.loc[t, 'y_t-2'] = pd.DataFrame(data.loc[t - 2, 'y']).values # remove random stimulus effects from regressors before fitting model data.iloc[:, : num_stims] = data.iloc[:, :num_stims] / stims['effect'].tolist() # build design DataFrame # create num_subs * num_stims DataFrame # where each cell is when that stim was presented for that sub # note that this depends on there being no repeated stimulus presentations gb = data.groupby('sub_num') pres = pd.DataFrame([[ next(i - 1 for i, val in enumerate(df.iloc[:, stim]) if abs(val) > .0001) for stim in range(num_stims) ] for sub_num, df in gb]) # build the design DataFrame from pres design = pd.concat([ pd.DataFrame({ 'onset': pres.iloc[sub, :].sort_values(), 'run_onset': pres.iloc[sub, :].sort_values(), 'stimulus': pres.iloc[sub, :].sort_values().index, 'subject': sub, 'duration': 1, 'amplitude': 1, 'run': 1, 'index': range(pres.shape[1]) }) for sub in range(num_subs) ]) design['condition'] = stims['condition'][design['stimulus']] # build activation DataFrame activation = pd.DataFrame({ 'y': data['y'].values, 'vol': data['time'], 'run': 1, 'subject': data['sub_num'] }) # build Dataset object dataset = nipymc.data.Dataset(design=design, activation=activation, TR=1) #################################### ############ FIT MODELS ############ #################################### # SPM model def get_diff(df): X = pd.concat([ df.iloc[:, :num_stims // 2].sum(axis=1), df.iloc[:, num_stims // 2:num_stims].sum(axis=1), df['y_t-1'], df['y_t-2'] ], axis=1) beta = pd.stats.api.ols(y=df['y'], x=X, intercept=False).beta return pd.Series(beta[1] - beta[0]).append(beta) sub_diffs = data.groupby('sub_num').apply(get_diff) # fit model with FIXED stim effects (LS-All model) with pm.Model(): # Fixed effects b = pm.Normal('fixstim_b', mu=0, sd=10, shape=num_stims) if ar is not None: ar1 = pm.Cauchy('fixstim_AR1', alpha=0, beta=1) ar2 = pm.Cauchy('fixstim_AR2', alpha=0, beta=1) # random x1 & x2 slopes for participants sigma_sub_A = pm.HalfCauchy('fixstim_sigma_sub_A', beta=10) sigma_sub_B = pm.HalfCauchy('fixstim_sigma_sub_B', beta=10) u0 = pm.Normal('u0_sub_A_log', mu=0., sd=sigma_sub_A, shape=data['sub_num'].nunique()) u1 = pm.Normal('u1_sub_B_log', mu=0., sd=sigma_sub_B, shape=data['sub_num'].nunique()) # now write the mean model mu = u0[data['sub_num'].values]*data.iloc[:,:(num_stims//2)].sum(axis=1).values + \ u1[data['sub_num'].values]*data.iloc[:,(num_stims//2):num_stims].sum(axis=1).values + \ pm.dot(data.iloc[:, :num_stims].values, b) if ar is not None: mu += ar1 * data['y_t-1'].values + ar2 * data['y_t-2'].values # define the condition contrast cond_diff = Deterministic( 'fixstim_cond_diff', T.mean(b[num_stims // 2:]) - T.mean(b[:num_stims // 2])) # model for the observed values Y_obs = pm.Normal('Y_obs', mu=mu, sd=pm.HalfCauchy('fixstim_sigma', beta=10), observed=data['y'].values) # run the sampler step = pm.NUTS() print('fitting fixstim model...') trace0 = pm.sample(SAMPLES, step=step, progressbar=False) # fit model WITHOUT random stim effects with pm.Model(): # Fixed effects b1 = pm.Normal('nostim_b_A', mu=0, sd=10) b2 = pm.Normal('nostim_b_B', mu=0, sd=10) if ar is not None: ar1 = pm.Cauchy('nostim_AR1', alpha=0, beta=1) ar2 = pm.Cauchy('nostim_AR2', alpha=0, beta=1) # random x1 & x2 slopes for participants sigma_sub_A = pm.HalfCauchy('nostim_sigma_sub_A', beta=10) sigma_sub_B = pm.HalfCauchy('nostim_sigma_sub_B', beta=10) u0 = pm.Normal('u0_sub_A_log', mu=0., sd=sigma_sub_A, shape=data['sub_num'].nunique()) u1 = pm.Normal('u1_sub_B_log', mu=0., sd=sigma_sub_B, shape=data['sub_num'].nunique()) # now write the mean model mu = (b1 + u0[data['sub_num'].values])*data.iloc[:,:(num_stims//2)].sum(axis=1).values + \ (b2 + u1[data['sub_num'].values])*data.iloc[:,(num_stims//2):num_stims].sum(axis=1).values if ar is not None: mu += ar1 * data['y_t-1'].values + ar2 * data['y_t-2'].values # define the condition contrast cond_diff = Deterministic('nostim_cond_diff', b2 - b1) # model for the observed values Y_obs = pm.Normal('Y_obs', mu=mu, sd=pm.HalfCauchy('nostim_sigma', beta=10), observed=data['y'].values) # run the sampler step = pm.NUTS() print('fitting nostim model...') trace1 = pm.sample(SAMPLES, step=step, progressbar=False) # fit model with separate dists + variances with pm.Model(): # Fixed effects b1 = pm.Normal('randstim_b_A', mu=0, sd=10) b2 = pm.Normal('randstim_b_B', mu=0, sd=10) if ar is not None: ar1 = pm.Cauchy('randstim_AR1', alpha=0, beta=1) ar2 = pm.Cauchy('randstim_AR2', alpha=0, beta=1) # random x1 & x2 slopes for participants sigma_sub_A = pm.HalfCauchy('randstim_sigma_sub_A', beta=10) sigma_sub_B = pm.HalfCauchy('randstim_sigma_sub_B', beta=10) u0 = pm.Normal('u0_sub_A_log', mu=0., sd=sigma_sub_A, shape=data['sub_num'].nunique()) u1 = pm.Normal('u1_sub_B_log', mu=0., sd=sigma_sub_B, shape=data['sub_num'].nunique()) # random stim intercepts sigma_stim_A = pm.HalfCauchy('randstim_sigma_stim_A', beta=10) u2 = pm.Normal('randstim_stim_A', mu=0., sd=sigma_stim_A, shape=num_stims // 2) sigma_stim_B = pm.HalfCauchy('randstim_sigma_stim_B', beta=10) u3 = pm.Normal('randstim_stim_B', mu=0., sd=sigma_stim_B, shape=num_stims // 2) # now write the mean model mu = (b1 + u0[data['sub_num'].values])*data.iloc[:,:(num_stims//2)].sum(axis=1).values + \ (b2 + u1[data['sub_num'].values])*data.iloc[:,(num_stims//2):num_stims].sum(axis=1).values + \ pm.dot(data.iloc[:, :num_stims//2].values, u2) + pm.dot(data.iloc[:, (num_stims//2):num_stims].values, u3) if ar is not None: mu += ar1 * data['y_t-1'].values + ar2 * data['y_t-2'].values # define the condition contrast cond_diff = Deterministic('randstim_cond_diff', b2 - b1) # model for the observed values Y_obs = pm.Normal('Y_obs', mu=mu, sd=pm.HalfCauchy('randstim_sigma', beta=10), observed=data['y'].values) # run the sampler step = pm.NUTS() print('fitting 2dist2var model...') trace2 = pm.sample(SAMPLES, step=step, progressbar=False) # fit FIX_STIM model using pymcwrap mod3 = nipymc.model.BayesianModel(dataset) mod3.add_term('subject', label='nipymc_fixstim_subject', split_by='condition', categorical=True, random=True) mod3.add_term('stimulus', label='nipymc_fixstim_stimulus', categorical=True) mod3.groupA = [ mod3.level_map['nipymc_fixstim_stimulus'][i] for i in range(num_stims // 2) ] mod3.groupB = [ mod3.level_map['nipymc_fixstim_stimulus'][i] for i in range(num_stims // 2, num_stims) ] mod3.add_deterministic( 'nipymc_fixstim_cond_diff', "T.mean(self.dists['b_nipymc_fixstim_stimulus'][self.groupB]) - T.mean(self.dists['b_nipymc_fixstim_stimulus'][self.groupA])" ) mod3.set_y('y', scale=None, detrend=False, ar=0 if ar is None else 2) print('fitting nipymc_fixstim model...') mod3_fitted = mod3.run(samples=SAMPLES, verbose=False, find_map=False) # fit NO_STIM model using pymcwrap mod4 = nipymc.model.BayesianModel(dataset) mod4.add_term('condition', label='nipymc_nostim_condition', categorical=True, scale=False) mod4.add_term('subject', label='nipymc_nostim_subject', split_by='condition', categorical=True, random=True) groupA = str(mod4.level_map['nipymc_nostim_condition'][0]) groupB = str(mod4.level_map['nipymc_nostim_condition'][1]) mod4.add_deterministic( 'nipymc_nostim_cond_diff', "self.dists['b_nipymc_nostim_condition'][" + groupB + "] - self.dists['b_nipymc_nostim_condition'][" + groupA + "]") mod4.set_y('y', scale=None, detrend=False, ar=0 if ar is None else 2) print('fitting nipymc_nostim model...') mod4_fitted = mod4.run(samples=SAMPLES, verbose=False, find_map=False) # fit 2dist2var model using pymcwrap mod5 = nipymc.model.BayesianModel(dataset) mod5.add_term('condition', label='nipymc_randstim_condition', categorical=True, scale=False) mod5.add_term('stimulus', label='nipymc_randstim_stimulus', split_by='condition', categorical=True, random=True) mod5.add_term('subject', label='nipymc_randstim_subject', split_by='condition', categorical=True, random=True) groupA = str(mod5.level_map['nipymc_randstim_condition'][0]) groupB = str(mod5.level_map['nipymc_randstim_condition'][1]) mod5.add_deterministic( 'nipymc_randstim_cond_diff', "self.dists['b_nipymc_randstim_condition'][" + groupB + "] - self.dists['b_nipymc_randstim_condition'][" + groupA + "]") mod5.set_y('y', scale=None, detrend=False, ar=0 if ar is None else 2) print('fitting nipymc_randstim model...') mod5_fitted = mod5.run(samples=SAMPLES, verbose=False, find_map=False) # # save PNG of traceplot # plt.figure() # pm.traceplot(trace2[BURN:]) # plt.savefig('pymc3_randstim.png') # plt.close() # plt.figure() # pm.traceplot(mod5_fitted.trace[BURN:]) # plt.savefig('nipymc_randstim.png') # plt.close() ###################################### ########## SAVE RESULTS ############## ###################################### # return parameter estimates print('computing and returning parameter estimates...') # lists of traces and names of their model parameters traces = [ trace0, # fixstim trace1, # nostim trace2, # randstim mod3_fitted.trace, # nipymc_fixstim mod4_fitted.trace, # nipymc_nostim mod5_fitted.trace ] # nipymc_randstim parlists = [[ x for x in trace.varnames if 'log' not in x and 'u_' not in x ] for trace in traces] # get posterior mean and SDs as lists of lists means = [[trace[param][BURN:].mean() for param in parlist] for trace, parlist in zip(traces, parlists)] SDs = [[trace[param][BURN:].std() for param in parlist] for trace, parlist in zip(traces, parlists)] # print list of summary statistics stats = sum([['posterior_mean'] * len(x) + ['posterior_SD'] * len(x) for x in parlists], []) print(stats) print(len(stats)) # print parameter names in the order in which they are saved parlists = [2 * parlist for parlist in parlists] extra_params = [] params = [param for parlist in parlists for param in parlist] + extra_params print(params) # add SPM model results ans = [summary for model in zip(means, SDs) for summary in model] ans = [ sub_diffs.mean(0).tolist(), (sub_diffs.std(0) / (len(sub_diffs.index)**.5)).tolist() ] + ans params = [ 'SPM_cond_diff', 'SPM_A_mean', 'SPM_B_mean', 'SPM_AR1', 'SPM_AR2' ] * 2 + params stats = ['posterior_mean'] * 5 + ['posterior_SD'] * 5 + stats # add test statistics for all models # grab all posterior means nums = [np.array(x) for x in ans][::2] # grab all posterior SDs denoms = [np.array(x) for x in ans][1::2] # divide them zs = [n / d for n, d in zip(nums, denoms)] zs = sum([x.tolist() for x in zs], []) # keep only the test statistics related to cond_diff labels = [ params[i] for i in [j for j, x in enumerate(stats) if x == 'posterior_mean'] ] zs = [(z, l) for z, l in zip(zs, labels) if 'cond_diff' in l] # add them to the results ans = [[x[0] for x in zs]] + ans params = [x[1] for x in zs] + params stats = ['test_statistic'] * 7 + stats # return the parameter values # for first instance only, also return param names and etc. if int(instance) == 0: ans = [ans, params, stats] return ans