Linear Mixed Model demo

Contents

This demonstration was prepared by Bogdan Petre and Tor Wager

It includes code to perform a mixed-effects analysis on a dataset in both Matlab and R. The published version runs the Matlab code.

To reproduce the analysis, you must have the dataset, which is in the CANlab_help_examples repository on Github, on your Matlab path. it is in the subfolder canlab_mixed_effects_matlab_demo1

About the dataset

The example uses a subset of data from an ongoing study, provided for model demonstration purposes only. Do not reuse or reshare this dataset for other purposes. It was collected by Tor Wager's Cognitive and Affective Neuroscience lab at the University of Colorado.

behav_dat = readtable('study_behav_dat_sid_14_164.csv');

behav_dat(1:5, :)

% we remove this subject who only has placebo but no contorl data
% glmfit_multilevel isn't designed to handle this
%behav_dat(behav_dat.sid == 113,:) = [];
ans =

  5×11 table

     Yint       Yunp       RTint      RTunp     stimLvl    sid    heat    placebo    run    trial     vif  
    _______    _______    _______    _______    _______    ___    ____    _______    ___    _____    ______

    0.91016          1     3.4159     4.4826     -0.5       6      1       -0.5       1     -1.5     2.0965
    0.59766     0.4668     3.6492     2.5496      0.5       6      1       -0.5       1     -0.5     1.7078
          0          0    0.81655    0.68308     -0.5       6      1       -0.5       1      0.5     1.7062
          0          0    0.46561    0.46575     -0.5       6      0       -0.5       1     -0.5     1.4626
    0.82031    0.55664      3.466     3.3659      0.5       6      0       -0.5       1      0.5     1.5805

*Variables*

% Yint - pain intensity rating
% Yunp - pain unpleasantness rating.
% RTint - reaction time for intensity ratings
% RTunp - reaction time for unpleasantness ratings
% stimLvl - low, medium or high. 46.5C, 47C and 47.5C, possibly different
%   for earliest subjects, but we think all with different temps are excluded
% sid - subject id label. Corresponds to PGsXXX labels used internally by
%   the lab
% heat - 1 if heat trial, 0 for pressure trials
% placebo - 0.5 for stimulation of placebo treated sight, -0.5
%   otherwise
% run - run number(1,2,3 or 4). Runs 1 and 4 are ctrl. 2 and 3 are
%   placebo.
% trial - trial number within run. Centered.
% vif - variance inflation factor for use with fMRI data. can ignore this.

*Preparation*

% this data was prepared with various preprocessing steps:
% any response times <= 0.02s were removed (we had sticky button problems
%   and based on temporal distribution of responses it seems that anything
%   < 0.02 is likely to be due to this rather than a deliberate response)
% any response times >= 5.01 seconds. These are timeouts without a click
%   and it's ambiguous whether the cursor location recorded corresponds to
%   the subjects rating or not.
% subjects with sid 1-13 were removed due to changes to the paradigm at
%   this stage of the study. Subject 6 retained because this subject was
%   processed out of order and actually was run on the newer paradigm.
% subject 23 and 84 are removed because too few of their responses passed
%   the timing criteria above

Using fitlme to create a LinearMixedModel object in Matlab

The fitlme code below does the following:

% - test placebo effect on heat trials
% - Control for stimulus level and test for interaction effects.
% - Model random subject (sid) intercepts and slopes.
% - Intercept is modeled implicitly both at the group and subject level.
% That is, you do not need to enter 'Yint ~ 1 + stimLvl*placebo ...'
% in the model in order for it to include an intercept term, because fitlme
% adds it automatically.
% - Use REML estimator instead of the default ML.


heat_dat = behav_dat(behav_dat.heat == 1,:); % Use heat trials only

m = fitlme(heat_dat, 'Yint ~ stimLvl*placebo + (stimLvl*placebo | sid)',...
    'FitMethod','REML');

Other notes:

% - Adding stimLvl*placebo automatically adds stimLvl, placebo, and
% stimLvl*placebo effects. You do not need to enter
% 'Yint ~ 1 + stimLvl + placebo + stimLvl*placebo ...'
% 'Yint ~ stimLvl*placebo ...' does the same thing.
%
% - Adding (stimLvl*placebo | sid) adds random effects for the intercept
% (1), added automatically if another random effect is added), stimLvl,
% placebo, and stimLvl*placebo effects.

*Sample output* Here is a sample of the output.

% - The P-values for Fixed effects coefficients do not generalize across
% levels of the random factors. The df and P-values use the overall number
% of observations (1704) minus the number of fixed-effect parameters (4 =
% 1 (intercept) + 1 (stimLvl) + 1 (placebo) + 1 (stimLvl*placebo).
% This is not correct for an inference across levels of the random effect
% of subject (sid).
% Thus, we need to use the anova() function below to get the correct
% inferences.

% Fixed effects coefficients (95% CIs):
%     Name                        Estimate     SE           tStat      DF      pValue        Lower        Upper
%     '(Intercept)'                 0.16402     0.011235     14.598    1700    1.4571e-45      0.14198      0.18606
%     'stimLvl'                    0.073984    0.0091767     8.0622    1700    1.3994e-15     0.055985     0.091983
%     'placebo'                -0.054471    0.0086796    -6.2757    1700    4.4069e-10    -0.071495    -0.037447
%     'stimLvl:placebo'        0.0065812     0.014501    0.45385    1700       0.64999     -0.02186     0.035022
%
% Random effects covariance parameters (95% CIs):
% Group: sid (121 Levels)
%     Name1                       Name2                       Type          Estimate      Lower        Upper
%     '(Intercept)'               '(Intercept)'               'std'            0.11924      0.10469     0.13581
%     'stimLvl'                   '(Intercept)'               'corr'            0.7489      0.71099     0.78247

inspect model coefficients (ignore p-values)

m.Coefficients
ans = 


    FIXED EFFECT COEFFICIENTS: DFMETHOD = 'RESIDUAL', ALPHA = 0.05

    Name                       Estimate     SE           tStat      DF  
    {'(Intercept)'    }          0.16402     0.011235     14.598    1700
    {'stimLvl'        }         0.073984    0.0091767     8.0622    1700
    {'placebo'        }        -0.054471    0.0086796    -6.2757    1700
    {'stimLvl:placebo'}        0.0065812     0.014501    0.45385    1700


    pValue        Lower        Upper    
    1.4571e-45      0.14198      0.18606
    1.3994e-15     0.055985     0.091983
    4.4069e-10    -0.071495    -0.037447
       0.64999     -0.02186     0.035022

get satterthwaite corrected p-values LinearMixedModel.anova Perform hypothesis tests on fixed effect terms. STATS = anova(LME) tests the significance of each fixed effect term in the linear mixed effects model LME and returns a table array STATS. If DummyVarCoding is used in fitlme call, anova will perform Type III hypothesis tests across DummyVar (not shown here).

anova(m,'dfmethod','satterthwaite')
ans = 


    ANOVA MARGINAL TESTS: DFMETHOD = 'SATTERTHWAITE'

    Term                       FStat      DF1    DF2       pValue    
    {'(Intercept)'    }         213.11    1      120.31    2.1189e-28
    {'stimLvl'        }         64.999    1      127.14     4.762e-13
    {'placebo'        }         39.384    1      121.44    5.5834e-09
    {'stimLvl:placebo'}        0.20598    1      255.48       0.65032

Satterthwaite corrected p-values can also be obtained using LinearMixedModel/coefTest, which can be useful for certain circumstances where anova doesn't return the specific contrast of interest by default. One circumstance where this arises is when you have DummyVarCoding in your fitlme invocation, but it's also useful for performing post-hoc comparisons.

The following implementation tests stimLvl = 0, which should provide the same results as above, while the other should post-hoc test whether placebo + stimLvl = 0, which conceptually amounts to testing if placebo effects are larger than a 1C decrease in stimulus intensity. The test is somewhat contrived, but mostly selected to illustrate how to use coefTest.

[Pa, Fa, DF1a, DF2a] = coefTest(m,[0,1,0,0],0,'dfmethod','satterthwaite');

[Pb, Fb, DF1b, DF2b] = coefTest(m,[0,1,1,0],0,'dfmethod','satterthwaite');

t = array2table([Pa,Fa,DF1a,DF2a; Pb, Fb, DF1b, DF2b],'VariableNames',...
    {'pValue','F','DF1','DF2'},'RowNames',{'stimLvl','placebo = (-1)*stimLvl'})
t =

  2×4 table

                               pValue        F       DF1     DF2  
                              _________    ______    ___    ______

    stimLvl                   4.762e-13    64.999     1     127.14
    placebo = (-1)*stimLvl      0.12765    2.3542     1     116.79

*Satterthwaite correction*

% Using the satterthwaite option for df estimates the error degrees of
% freedom based on the rank
% The formula was developed by the statistician Franklin E. Satterthwaite and a derivation of the
% result is given in Satterthwaite's article in Psychometrika (vol. 6, no. 5, October 1941).
% This site is useful for deriving the df in the two-sample case: https://secure-media.collegeboard.org/apc/ap05_stats_allwood_fin4prod.pdf
% Code for Satterthwaite df calculation for mulitple regression can be found in
% fit_gls.m (Martin Lindquist), and is included here for the
% no-autocorrelation (AR(p), p==0) case:

% A = W;   % for ar p = 0 case; weights
% R = (eye(T) - X * invxvx * X' * iV);        % Residual inducing matrix
%
% Wd = R * A * inv(iV) * A';                  % inv(iV) = Covariance matrix
% df = (trace(Wd).^2)./trace(Wd*Wd);       % Satterthwaite approximation for degrees of freedom

*Model properties* You can query the model object (m) to get its properties. e.g.,

help LinearMixedModel
m.Rsquared
m.VariableInfo                          % all variables in input dataset and which are in model
m.Variables(1:5, :)                     % all variables in input dataset
X = designMatrix(m); X(1:5, :)          % Fixed-effects design matrix
Xg = designMatrix(m, 'Random'); whos('Xg')          % Random-effects design matrix


% This option,  'DummyVarCoding', 'effects', effects-codes, which we
% prefer:
m = fitlme(heat_dat,'Yint ~ stimLvl*placebo + (stimLvl*placebo | sid)',...
    'FitMethod','REML', 'DummyVarCoding', 'effects');
% help anova says:
%     To obtain tests for the Type III hypotheses, set the 'DummyVarCoding'
%     to 'effects' when fitting the model.
 LinearMixedModel Fitted linear mixed effects model.
    LME = FITLME(...) fits a linear mixed effects model to data. The fitted
    model LME is a LinearMixedModel modeling a response variable as a
    linear function of fixed effect predictors and random effect
    predictors.
 
    LinearMixedModel methods:
        coefCI - Coefficient confidence intervals
        coefTest - Linear hypothesis test on coefficients
        predict - Compute predicted values given predictor values
        random - Generate random response values given predictor values
        plotResiduals - Plot of residuals
        designMatrix - Fixed and random effects design matrices
        fixedEffects - Stats on fixed effects
        randomEffects - Stats on random effects
        covarianceParameters - Stats on covariance parameters
        fitted - Fitted response
        residuals - Various types of residuals
        response - Response used to fit the model
        compare - Compare fitted models
        anova - Marginal tests for fixed effect terms           
        disp - Display a fitted model                                                                                                                 
 
    LinearMixedModel properties:
        FitMethod - Method used for fitting (either 'ML' or 'REML')
        MSE - Mean squared error (estimate of residual variance)
        Formula - Representation of the model used in this fit
        LogLikelihood - Log of likelihood function at coefficient estimates
        DFE - Degrees of freedom for residuals
        SSE - Error sum of squares
        SST - Total sum of squares
        SSR - Regression sum of squares
        CoefficientCovariance - Covariance matrix for coefficient estimates
        CoefficientNames - Coefficient names
        NumCoefficients - Number of coefficients
        NumEstimatedCoefficients - Number of estimated coefficients
        Coefficients - Coefficients and related statistics
        Rsquared - R-squared and adjusted R-squared
        ModelCriterion - AIC and other model criteria
        VariableInfo - Information about variables used in the fit
        ObservationInfo - Information about observations used in the fit
        Variables - Table of variables used in fit
        NumVariables - Number of variables used in fit
        VariableNames - Names of variables used in fit
        NumPredictors - Number of predictors
        PredictorNames - Names of predictors
        ResponseName - Name of response
        NumObservations - Number of observations in the fit
        ObservationNames - Names of observations in the fit
 
    See also FITLME, FITLMEMATRIX, LinearModel, GeneralizedLinearModel, NonLinearModel.


ans = 

  struct with fields:

    Ordinary: 0.5735
    Adjusted: 0.5728


ans =

  11×4 table

                 Class          Range        InModel    IsCategorical
               __________    ____________    _______    _____________

    Yint       {'double'}    {1×2 double}     false         false    
    Yunp       {'double'}    {1×2 double}     false         false    
    RTint      {'double'}    {1×2 double}     false         false    
    RTunp      {'double'}    {1×2 double}     false         false    
    stimLvl    {'double'}    {1×2 double}     true          false    
    sid        {'double'}    {1×2 double}     true          false    
    heat       {'double'}    {1×2 double}     false         false    
    placebo    {'double'}    {1×2 double}     true          false    
    run        {'double'}    {1×2 double}     false         false    
    trial      {'double'}    {1×2 double}     false         false    
    vif        {'double'}    {1×2 double}     false         false    


ans =

  5×11 table

     Yint       Yunp       RTint      RTunp     stimLvl    sid    heat    placebo    run    trial     vif  
    _______    _______    _______    _______    _______    ___    ____    _______    ___    _____    ______

    0.91016          1     3.4159     4.4826     -0.5       6      1       -0.5       1     -1.5     2.0965
    0.59766     0.4668     3.6492     2.5496      0.5       6      1       -0.5       1     -0.5     1.7078
          0          0    0.81655    0.68308     -0.5       6      1       -0.5       1      0.5     1.7062
    0.67578    0.74805     2.0656     4.0154      0.5       6      1        0.5       2     -0.5     1.8016
    0.24609    0.13672     1.3997     1.6329     -0.5       6      1        0.5       2      0.5     1.7161


ans =

    1.0000   -0.5000   -0.5000    0.2500
    1.0000    0.5000   -0.5000   -0.2500
    1.0000   -0.5000   -0.5000    0.2500
    1.0000    0.5000    0.5000    0.2500
    1.0000   -0.5000    0.5000   -0.2500

  Name         Size             Bytes  Class     Attributes

  Xg        1704x484            98472  double    sparse    

Simple two-level mixed effects using CANlab glmfit_multilevel

Mixed-effects models differ in their assumptions and implementation details. glmfit_multilevel is a fast and simple option for running a two-level mixed effects model with participant as a random effect. It implements a random-intercept, random-slope model across 2nd-level units (e.g., participants). it fits regressions for individual 2nd-level units (e.g., participants), and then (optionally) uses a precision-weighted least squares approach to model group effects. It thus treats participants as a random effect. This is appropriate when 2nd-level units are participants and 1st-level units are observations (e.g., trials) within participants. glmfit_multilevel was designed with this use case in mind.

glmfit_multilevel requires enough 1st-level units to fit a separate model for each 2nd-level unit (participant). If this is not the case, other models (igls.m, LMER, etc.) are preferred.

Degrees of freedom: glmfit_multilevel is conservative in the sense that the degrees of freedom in the group statistical test is always based on the number of subjects - 2nd-level parameters. The df is never higher than the sample size, which you would have with mixed effects models that estimate the df from the data. This causes problems in many other packages, particularly when there are many 1st-level observations and they are uncorrelated, resulting in large and undesirable estimated df.

% update appropriately
addpath(genpath(['/home/' getenv('USER') '/.matlab/canlab/CanlabCore']));

% provides glmfit_multilevel() dependency RB_empirical_bayes_params()
addpath(genpath(['/home/' getenv('USER') '/.matlab/canlab/MediationToolbox']));
Warning: Function vecnorm has the same name as a MATLAB builtin. We suggest you
rename the function to avoid a potential name conflict. 
Warning: Function vecnorm has the same name as a MATLAB builtin. We suggest you
rename the function to avoid a potential name conflict. 

Prepare the dataset** glmfit_multilevel takes cell array input, with one cell per subject Y contains the response for each subject, and X the within-subject design matrix.

First, we need to turn the dataset into cell arrays:

u = unique(heat_dat.sid);
[Y, X] = deal(cell(1, length(u)));

Y_name = 'Yint';
X_var_names = {'stimLvl' 'placebo'};

Y_var = heat_dat.(Y_name);

X_var = zeros(size(Y_var, 1), length(X_var_names));

for j = 1:length(X_var_names)
    X_var(:, j) = heat_dat.(X_var_names{j});
end

for i = 1:length(u)

    wh = heat_dat.sid == u(i);  % indicator for this subject

    Y{i} = Y_var(wh);

    for j = 1:length(X_var_names)

        X{i}(:, j) = X_var(wh, j);

    end

end

% X2 is the 2nd-level (between-person) design matrix.
% we don't have any between-person predictors, so X2 is empty here:
X2 = [];

X2_names = {'2nd-level Intercept (average within-person effects)'};

*Between-person predictors* Note: if we had a between-person predictor, the intercept would be first, and names would be, e.g., X2_names = {'2nd-level Intercept (overall group effect)' '2nd-lev predictor: Group membership'}

*Within-person variation* glmfit_multilevel takes a summary statistics approach with precision-reweighting. it fits a model for each person individually. Therefore, each individual must have variance in Y and individual X.

Run the model

stats = glmfit_multilevel(Y, X, X2, 'names', X_var_names, 'beta_names', X2_names);

% The 'weighted' option uses Empirical Bayes weighting for a single-step
% precision reweighting. This is a simple version of what is acccomplished
% by fitlme in Matlab or LMER in R, or igls.m (Lindquist/Wager).

stats = glmfit_multilevel(Y, X, X2, 'names', X_var_names, 'beta_names', X2_names, 'weighted');

% The 'boot' option, with 'bootsamples', n, runs n boostrap samples at the 2nd level:
% Run with 1000 bootstrap samples initially, and more are added based on P-value tolerance to avoid instability with too-few samples

stats = glmfit_multilevel(Y, X, X2, 'names', X_var_names, 'beta_names', X2_names, 'weighted', 'boot');
Warning! Some participants have no variance in X column(s) and will be excluded
Participant numbers:80
Analysis description: Second Level of Multilevel Model  
GLS analysis

Observations: 120, Predictors:   1
Outcome names: Intercept	stimLvl	placebo	
Weighting option: unweighted
Inference option: t-test

Other Options:
	Plots: No
	Robust: no
	Save: No
	Output file: glmfit_general_output.txt
	Total time: 0.00 s

________________________________________

Second Level of Multilevel Model 
	Outcome variables:
	Intercept	stimLvl	placebo	
Adj. mean	0.16	0.07	-0.05	

2nd-level Intercept (average within-person effects)
	Intercept	stimLvl	placebo	
Coeff	0.16	0.07	-0.05	
STE	0.01	0.01	0.01	
t	14.53	8.23	-6.31	
Z	8.21	7.30	-5.85	
p	0.00000	0.00000	0.00000	

________________________________________
Warning! Some participants have no variance in X column(s) and will be excluded
Participant numbers:80
Analysis description: Second Level of Multilevel Model  
GLS analysis

Observations: 120, Predictors:   1
Outcome names: Intercept	stimLvl	placebo	
Weighting option: weighted
Inference option: t-test

Other Options:
	Plots: No
	Robust: no
	Save: No
	Output file: glmfit_general_output.txt
	R & B - type Empirical Bayes reweighting.
Total time: 0.01 s

________________________________________

Second Level of Multilevel Model 
	Outcome variables:
	Intercept	stimLvl	placebo	
Adj. mean	0.16	0.05	-0.05	

2nd-level Intercept (average within-person effects)
	Intercept	stimLvl	placebo	
Coeff	0.16	0.05	-0.05	
STE	0.01	0.01	0.01	
t	14.24	6.69	-5.52	
Z	8.21	6.11	-5.18	
p	0.00000	0.00000	0.00000	

________________________________________
Warning! Some participants have no variance in X column(s) and will be excluded
Participant numbers:80
Analysis description: Second Level of Multilevel Model  
GLS analysis

Observations: 120, Predictors:   1
Outcome names: Intercept	stimLvl	placebo	
Weighting option: weighted
Inference option: bootstrap

Other Options:
	Plots: No
	Robust: no
	Save: No
	Output file: glmfit_general_output.txt
	R & B - type Empirical Bayes reweighting.
 Min p-value is 0.000000. Needed: 5867 samples
 Bootstrapping 5867 samples... Bootstrap done in 0.01 s, bias correct in 0.00 s 
Total time: 0.25 s

________________________________________
Average p-value tolerance (average max alpha): 0.0060

Bootstrapped statistics. Bootstrap inference
	Outcome variables:
	Intercept	stimLvl	placebo	
Adj. mean	0.16	0.05	-0.05	

2nd-level Intercept (average within-person effects)
	Intercept	stimLvl	placebo	
Coeff	0.16	0.05	-0.05	
STE	0.01	0.01	0.01	
t	14.24	6.69	-5.52	
Z	3.73	3.77	-3.32	
p	0.00019	0.00016	0.00091	

________________________________________

Plotting data and effects

This is a crucial part of the analysis process!

with glmfit_multilevel, the 'plots' option generates some plots. you can also use functions including barplot_columns() to plot summary 2nd-level stats, and lineplot_multisubject() to plot individual and group within-person effects

% % stats = glmfit_multilevel(Y, X, X2, 'names', X_var_names, 'beta_names', X2_names, 'weighted', 'boot', 'plots');

effectcolors = {[.5 .5 .5] [.7 .4 .4] [.4 .4 .7]};

Plot the individual within-person effect scores and group summary statistics Note: P values and Cohen's d from the barplot_columns output may not match your model results in stats exactly, because it provides a simple t-test on each column. so you should prefer the stats from the mixed model output.

create_figure('2nd level stats');
barplot_columns(stats.first_level.beta', 'names', stats.inputOptions.names, 'colors', effectcolors, 'nofigure');
ylabel(Y_name);
title('Naive individual within-person scores');
drawnow, snapnow
Col   1: Intercept	Col   2: stimLvl	Col   3: placebo	
---------------------------------------------
Tests of column means against zero
---------------------------------------------
        Name         Mean_Value    Std_Error      T           P         Cohens_d
    _____________    __________    _________    ______    __________    ________

    {'Intercept'}      0.16386      0.011276    14.532    2.2204e-15      1.3266
    {'stimLvl'  }     0.074936     0.0091093    8.2263    2.7751e-13     0.75096
    {'placebo'  }    -0.054998     0.0087174    -6.309    4.9825e-09    -0.57593

If you are using subject scores on within-person effects for other purposes, e.g., to regress on brain variables, then using the Empirical Bayes estimates for these is a good idea!! They are likely to be more accurate representation of the individual participants' scores than the naive within-person effects.

create_figure('2nd level stats comparison', 1, 2);
subplot(1, 2, 1);
barplot_columns(stats.first_level.beta', 'names', stats.inputOptions.names, 'colors', effectcolors, 'nofigure');
ylabel(Y_name);
title('Naive individual within-person scores');

subplot(1, 2, 2);
barplot_columns(stats.Y_star, 'names', stats.inputOptions.names, 'colors', effectcolors, 'nofigure');
ylabel(Y_name);
title('Empirical Bayes individual scores');
drawnow, snapnow
Col   1: Intercept	Col   2: stimLvl	Col   3: placebo	
---------------------------------------------
Tests of column means against zero
---------------------------------------------
        Name         Mean_Value    Std_Error      T           P         Cohens_d
    _____________    __________    _________    ______    __________    ________

    {'Intercept'}      0.16386      0.011276    14.532    2.2204e-15      1.3266
    {'stimLvl'  }     0.074936     0.0091093    8.2263    2.7751e-13     0.75096
    {'placebo'  }    -0.054998     0.0087174    -6.309    4.9825e-09    -0.57593

Col   1: Intercept	Col   2: stimLvl	Col   3: placebo	
---------------------------------------------
Tests of column means against zero
---------------------------------------------
        Name         Mean_Value    Std_Error       T           P         Cohens_d
    _____________    __________    _________    _______    __________    ________

    {'Intercept'}      0.15939      0.010343      15.41    2.2204e-15     1.4067 
    {'stimLvl'  }     0.067031     0.0058821     11.396    2.2204e-15     1.0403 
    {'placebo'  }     -0.05237     0.0062338    -8.4009    1.0938e-13    -0.7669 

With line_plot_multisubject, you can plot the within-person scores for one effect at a time. This does not control for other effects, and stats reported are simple summary stats, so you should prefer the stats from the mixed model output.

Get one column of X (Alternatively, you could reconstruct individual scores for marginal estimates for each level, or modify glmfit_multilevel to return this output!)

X_placebo = X;
for i = 1:length(X), X_placebo{i} = X_placebo{i}(:, 2); end

%

Create a plot, with each subject in a different color along a spectrum

create_figure('1st level effect');
line_plot_multisubject(X_placebo, Y);
set(gca, 'XTick', [-.5 .5], 'XLim', [-.6 .6], 'XTickLabel', {'Control' 'placebo'});
Warnings:

____________________________________________________________________________________________________________________________________________
X: some input cells have no varability:
80
X: input cells with low varability:
80
Y: input cells with low varability:
18 25 53 70 113 116
Excluded some subjects from individual fits:
80
____________________________________________________________________________________________________________________________________________

____________________________________________________________________________________________________________________________________________
Input data:
X scaling: Centered
Y scaling: No centering or z-scoring
                
Transformations:
No data transformations before plot
                                                                    
Correlations:                                                       
r = -0.15 across all observations, based on untransformed input data
                                                                
Stats on slopes after transformation, subject is random effect: 
Mean b = -0.06, t(119) = -6.62, p = 0.000000, num. missing:   1 
                                                                
Average within-person r = -0.21 +- 0.35 (std)
 
Between-person r (across subject means) = 0.05, p = 0.554204
____________________________________________________________________________________________________________________________________________

Create a plot with individuals that show positive effects in one color and negative effects in another color (Negative effects mean analgesia here)

indiv_scores = stats.Y_star(:, 3);
% or: indiv_scores = stats.first_level.beta(3, :)';

wh = indiv_scores > 0; % individuals with positive effects

poscolors = {[1 .4 .4]}; % custom_colors([.3 .3 .7], [.3 .5 1], sum(wh))';    % colors for those with pos effects
negcolors = {[.3 .3 1]}; % custom_colors([.7 .2 .2], [.7 .4 .4], sum(~wh))';    % colors for everyone else

colors = {};
colors(wh) = poscolors;
colors(~wh) = negcolors;

create_figure('1st level effect');
line_plot_multisubject(X_placebo, Y, 'colors', colors);
set(gca, 'XTick', [-.5 .5], 'XLim', [-.6 .6], 'XTickLabel', {'Control' 'placebo'});
Warnings:

____________________________________________________________________________________________________________________________________________
X: some input cells have no varability:
80
X: input cells with low varability:
80
Y: input cells with low varability:
18 25 53 70 113 116
Excluded some subjects from individual fits:
80
____________________________________________________________________________________________________________________________________________

____________________________________________________________________________________________________________________________________________
Input data:
X scaling: Centered
Y scaling: No centering or z-scoring
                
Transformations:
No data transformations before plot
                                                                    
Correlations:                                                       
r = -0.15 across all observations, based on untransformed input data
                                                                
Stats on slopes after transformation, subject is random effect: 
Mean b = -0.06, t(119) = -6.62, p = 0.000000, num. missing:   1 
                                                                
Average within-person r = -0.21 +- 0.35 (std)
 
Between-person r (across subject means) = 0.05, p = 0.554204
____________________________________________________________________________________________________________________________________________

Assessing and considering correlations between the individual effects

Individuals with high stimulus responses might also show large analgesia effects

Or, commonly, the individuals with higher baseline outcomes (here, pain) might show larger effects. This is likely if there are individual differences in the *scale* of the outcome, such that some individuals have compressed values on the outcome relative to others, causing a proportional compression of the treatment effects. This is very common with outcome measures including self-reported symptoms and reaction times. If so, it's ambiguous what the best individual difference scores for treatment effects ought to be. They could be the effect scores in raw units, or after adjusting for potential response scale differences.

In both these cases, the within-person effects will be correlated -- across treatment effects, in the former case, and with the intercept (average outcome value) in the latter. We can assess these empirically, using the EB estimates from our mixed-effects model:

plot_correlation_matrix(stats.Y_star, 'names', stats.inputOptions.names);
drawnow, snapnow

it's also a good idea to look at the scatterplots and marginal distributions (e.g., check for outliers)

figure; plotmatrix(stats.Y_star);
drawnow, snapnow

Here, we see that the intercept (average pain) is strongly correlated with the effect of stimLvl (intensity sensitivity) and also correlated with the effect of placebo (negatively, which is sensible as a negative effect means a larger analgesic effect). This suggests that there are individual differences in scale usage (compressed vs. expanded range) that influence the treatment effect scores when analyzed in raw units. Specifically, e.g., here about half the variation in stimulus intensity effects can be explained by overall pain report differences (r = 0.71)

This can add noise variability that masks effects (makes them non-significant) in the linear model, or when correlating with external variables (e.g., fMRI activity, another task). Or it can create artifactual positive correlations with other tasks (e.g., a different drug/treatment effect) if participants display the same scale compression/expansion across tasks. That is, we might observe positive correlations between placebo effects in pain and in a different task (allergy test) that could be caused by consistency in scale usage rather than consistency in the underlying treatment effects.

Some options for dealing with this include: 1. Include baseline scale (e.g., average outcome) as a 2nd-level covariate in the mixed-effects model. This makes the GLM a bi-linear model: Treatment effects vary linearly as a function of baseline pain.

2. Transform outcome values to be on the same scale (e.g., within-subject Z-scores or normalization of the outcome by the L2 norm)

3. Residualizing effects by removing the average outcome (individual intercepts) from each treatment effect

Here is what happens when we use Option 3. This removes the predicted effects of baseline pain from the treatment effect estimates.

We'll use the Empirical Bayes estimates because these are probably the best estimates of individual scores we have.

Note: the resid() function has a flag that preserves the overall mean response so we can remove the covariate without removing the average treatment effects.

avg_pain = stats.Y_star(:, 1);

% Get new placebo scores
resid_placebo = resid(avg_pain, stats.Y_star(:, 3), true);

resid_stimLvl = resid(avg_pain, stats.Y_star(:, 2), true);

figure; plotmatrix([avg_pain resid_stimLvl resid_placebo]);

plot_correlation_matrix([avg_pain resid_stimLvl resid_placebo], 'names', stats.inputOptions.names);
drawnow, snapnow

2nd-level predictors

Mixed models can include between-person (e.g., group) predictors as well. These could include sex/gender, counterbalancing order variables, and others.

You can include between-person and within-person predictors in any mixed effects model. They can interact with (i.e., moderate) the within-person effects, and/or they can have main effects on the response.

In glmfit_multilevel, these are entered in a second-level design matrix, X2. X2 will automatically include an intercept as the first effect. One block of output stats is included for the average within-person effects (the group intercept), with one block for each of the 2nd-level predictors.

e.g., here, we will include baseline pain as a 2nd-level predictor. The first stats block reports the within-person effects at the average baseline pain, and the second reports baseline pain effects on each within-person effect (the intercept is not of interest here)

Note: you must mean-center all 2nd-level predictors (except the intercept) for the interpretations above to hold. If you do not, your estimates of within-person effects will change and be potentially uninterpretable. Centering will be done automatically UNLESS you explicitly turn it off using the 'nocenter' option.

X2 = cellfun(@nanmean, Y)';

stats = glmfit_multilevel(Y, X, X2, 'names', X_var_names, 'beta_names', {'Average_pain'}, 'weighted', 'boot');
Warning! Some participants have no variance in X column(s) and will be excluded
Participant numbers:80
Analysis description: Second Level of Multilevel Model  
GLS analysis

Observations: 120, Predictors:   2
Outcome names: Intercept	stimLvl	placebo	
Weighting option: weighted
Inference option: bootstrap

Other Options:
	Plots: No
	Robust: no
	Save: No
	Output file: glmfit_general_output.txt
	R & B - type Empirical Bayes reweighting.
 Min p-value is 0.000000. Needed: 5867 samples
 Bootstrapping 5867 samples... Bootstrap done in 0.02 s, bias correct in 0.00 s 
Total time: 0.77 s

________________________________________
Average p-value tolerance (average max alpha): 0.0060

Bootstrapped statistics. Bootstrap inference
	Outcome variables:
	Intercept	stimLvl	placebo	
Adj. mean	0.00	1.01	0.01	0.36	-0.01	-0.26	

Within-person average effects
	Intercept	stimLvl	placebo	
Coeff	0.00	0.01	-0.01	
STE	0.00	0.01	0.01	
t	0.50	0.68	-1.03	
Z	0.82	1.03	-1.24	
p	0.40976	0.30380	0.21422	


Average_pain
	Intercept	stimLvl	placebo	
Coeff	1.01	0.36	-0.26	
STE	0.01	0.07	0.09	
t	123.11	5.84	-3.58	
Z	3.33	3.52	-3.39	
p	0.00086	0.00043	0.00070	

________________________________________