LMM usage example

Below we will fit a linear mixed model using the Ruby gem mixed_models, and demostrate many inference and prediction methods available for objects of class LMM.

Table of Contents*

  • Example Data

  • Linear Mixed Model

  • Some model attributes

  • Fitted values and residuals

  • Fixed effects hypotheses tests and confidence intervals

  • Predictions and prediction intervals

* linking the titles to the respective sections messes up the display in nbviewer for some reason; would appreciate any hint on how to make it work...

Example data

The data set, which is simulated, contains two numeric variables Age and Aggression, and two categorical variables Location and Species. These data are available for 100 (human and alien) individuals.

We will fit the model with the method LMM#from_formula, which mimics the behaviour of the function lmer from the R package lme4.

The data is supplied to LMM#from_formula as a Daru::DataFrame (from the excellent Ruby gem daru). We load the data, and display the first 10 lines with:


In [1]:
require 'daru'
alien_species = Daru::DataFrame.from_csv '../examples/data/alien_species.csv'
alien_species.head


Out[1]:
Daru::DataFrame:70189648942520 rows: 10 cols: 4
AgeAggressionLocationSpecies
0204.95877.54242028595AsylumDalek
139.88852.528392188206OodSphereWeepingAngel
2107.34388.791416909388AsylumHuman
3210.01170.010124622982OodSphereOod
4270.221078.31219494376OodSphereDalek
5157.65164.924992952256OodSphereOod
6136.15865.838374677443OodSphereWeepingAngel
7241.311052.36035549029EarthDalek
886.84-8.57251993382567AsylumOod
9206.71070.71900405899OodSphereDalek

Linear mixed model

We model the Aggression level of an individual as a linear function of the Age (Aggression decreases with Age), with a different constant added for each Species (i.e. each species has a different base level of aggression). Moreover, we assume that there is a random fluctuation in Aggression due to the Location that an individual is at. Additionally, there is a random fluctuation in how Age affects Aggression at each different Location.

Thus, the Aggression level of an individual of Species $spcs$ who is at the Location $lctn$ can be expressed as: $$Aggression = \beta_{0} + \gamma_{spcs} + Age \cdot \beta_{1} + b_{lctn,0} + Age \cdot b_{lctn,1} + \epsilon,$$ where $\epsilon$ is a random residual, and the random vector $(b_{lctn,0}, b_{lctn,1})^T$ follows a multivariate normal distribution (the same distribution but different realizations of the random vector for each Location). That is, we have a linear mixed model with fixed effects $\beta_{0}, \beta_{1}, \gamma_{Dalek}, \gamma_{Ood}, \dots$, and random effects $b_{Asylum,0}, b_{Asylum,1}, b_{Earth,0},\dots$.

We fit this model in mixed_models using a syntax familiar from the R package lme4, and display the estimated fixed and random effects coefficients:


In [2]:
require 'mixed_models'
model_fit = LMM.from_formula(formula: "Aggression ~ Age + Species + (Age | Location)", 
                             data: alien_species)
puts "Fixed effects:"
puts model_fit.fix_ef
puts "Random effects:"
puts model_fit.ran_ef


Fixed effects:
{:intercept=>1016.2867207696772, :Age=>-0.06531615343467667, :Species_lvl_Human=>-499.693695290209, :Species_lvl_Ood=>-899.5693213535769, :Species_lvl_WeepingAngel=>-199.58895804200762}
Random effects:
{:intercept_Asylum=>-116.68080682806675, :Age_Asylum=>-0.03353391213062394, :intercept_Earth=>83.86571630094429, :Age_Earth=>-0.13613996644462312, :intercept_OodSphere=>32.81508992422805, :Age_OodSphere=>0.16967387859838903}

Some model attributes

Apart from the fixed and random effects coefficients (seen above), we can access many attributes of the fitted model. Among others:

  • fix_ef_names and ran_ef_names are Arrays of names of the fixed and random effects.

  • reml is an indicator whether the profiled REML criterion or the profiled deviance function was optimized by the model fitting algorithm.

  • formula returns the R-like formula used to fit the model as a String.

  • model_data, optimization_result and dev_fun store the various model matrices in an LMMData object, the results of the utilized optimization algorithm, and the corresponding objective function as a Proc.

  • sigma2 is the residual variance (unless weights was specified in the model fit).

  • sigma_mat is the covariance matrix of the multivariate normal random effects vector.

We can look at some of these parameters for our example model:


In [3]:
puts "REML criterion used: \t#{model_fit.reml}"
puts "Residual variance: \t#{model_fit.sigma2}"
puts "Formula: \t" + model_fit.formula
puts "Variance of the intercept due to 'location' (i.e. variance of b0): \t#{model_fit.sigma_mat[0,0]}"
puts "Variance of the effect of 'age' due to 'location' (i.e. variance of b1): \t#{model_fit.sigma_mat[1,1]}"
puts "Covariance of b0 and b1: \t#{model_fit.sigma_mat[0,1]}"


REML criterion used: 	true
Residual variance: 	0.9496833447256808
Formula: 	Aggression ~ Age + Species + (Age | Location)
Variance of the intercept due to 'location' (i.e. variance of b0): 	10870.932406181153
Variance of the effect of 'age' due to 'location' (i.e. variance of b1): 	0.024233390356817052
Covariance of b0 and b1: 	-0.9716403033290782

Some further convenience methods are:

  • sigma returns the square root of sigma2.

  • theta returns the optimal solution of the minimization of the deviance function or the REML criterion (whichever was used to fit the model).

  • deviance returns the value of the deviance function or the REML criterion at the optimal solution.


In [4]:
puts "Residual standard deviation: \t#{model_fit.sigma}"
puts "REML criterion: \t#{model_fit.deviance}"


Residual standard deviation: 	0.9745169802141371
REML criterion: 	333.715539101513

Fitted values and residual

There are methods to get the fitted values of the response variable, with or without inclusion of random effects, as well as the model residuals.


In [5]:
puts "Fitted values at the population level:"
model_fit.fitted(with_ran_ef: false)


Fitted values at the population level:
Out[5]:
[1002.9001751232403, 814.0929545286947, 509.58198956979004, 103.00035403328388, 998.6369897885589, 106.42030782712357, 807.8049684375385, 1000.5252797843553, 111.04534465183303, 1002.7858718547295, 1008.0797460906101, 498.79959896079356, 100.90435866956511, 111.03358774421474, 102.37593160644838, 812.6239942379489, 97.21856813124623, 800.9297901270043, 510.0032787594437, 502.69505435163774, 503.7329280297147, 999.8864878037642, 497.2875300087809, 1003.3325680589778, 804.1328942914408, 811.9113950039764, 99.91285946042672, 803.9356395080681, 998.4671677896288, 99.50267401685687, 500.71140277182656, 1014.3853675431938, 112.41241174322079, 1010.3031079535265, 114.52212349916078, 813.8107887458568, 811.7526767511301, 498.69901208450415, 1005.0836941325615, 110.67826786953015, 501.0020596546109, 810.6305452351226, 511.31809292808373, 1000.7623774213233, 508.8465296821156, 1008.3730156195318, 1011.249539016795, 102.45953628284474, 1010.9490847109954, 1007.0346876356552, 506.2286582524538, 502.1503176319925, 813.0589998198238, 499.7525616394055, 812.9329396436949, 498.2568217257515, 514.1586924409578, 499.7636653854894, 510.7733562084385, 798.2159039517935, 511.41476083516704, 506.48077860471153, 1012.176375234033, 1013.9856326841735, 101.35504012826436, 1013.1593833432249, 1006.1091577414859, 106.8226553322811, 1002.77542127018, 806.3314360160521, 1002.5860044252195, 514.9699190666165, 1007.2214918344785, 806.2550161165334, 502.05822185564966, 1011.9458092124087, 804.6221122806667, 997.8786692471823, 802.2648523032092, 1011.170506471139, 806.8657221511478, 1011.1763849249481, 798.768478609851, 1009.0999844072597, 806.0166121564969, 102.78481072694944, 99.66400491584056, 506.1835901065838, 998.8949885946258, 499.43577829524736, 814.6050331716226, 1012.1182438574762, 109.602510822461, 501.6904919118124, 98.02326314156153, 106.71945580985437, 99.98274774460185, 114.88985344299806, 798.1950027826945, 106.620828418168]

In [6]:
puts "Model residuals:"
model_fit.residuals


Model residuals:
Out[6]:
[-1.8041727180520866, -1.1462465432205136, -0.5102357042341055, -1.4385305789776055, 1.010839756116411, -1.0594917601316638, 2.1172177445057514, 0.8212947077422541, -0.024972828168568384, 0.046451573745571295, -0.25215128087575067, 0.5337610637693615, -0.530629854101111, -0.874628273213915, -0.5173146938876982, -1.0767035113831298, -0.37352823345418074, 0.43428801048560217, -0.008686417241960953, 0.9633162152579189, -1.8591988675071889, -0.06035456182212329, -0.8747549066539477, 2.156663624529642, -0.12014582672793495, 1.5012618544112684, 0.5916114379899646, -1.4808531218287726, -0.16478112799109113, 2.0314537895965294, 0.010773844424193157, 0.8539551421492888, -1.2568149547066971, 0.07403493882111434, -0.11003692957027056, 1.0271335859753208, 1.289389380032162, -0.1484680679627104, -0.4342951733153768, 1.7157974749034395, 1.0173666823257577, -0.6984875406659512, 0.16845508494225214, -1.4232161610595995, -0.14893911781359748, 0.1337897922234106, -0.2102464993108697, 0.4063849522508747, -0.40370493998511847, -1.508647121178683, 0.22131784542773403, -0.33435674401818005, -1.335498834106943, 1.1064057146313644, 0.34729798131309053, 0.21318943374672017, -0.4491433732106884, -0.3144066453173764, -0.8024013676738377, -1.138768647945426, -0.2702669888674336, 0.32795235324385885, -0.07521784973914691, 1.83548484319806, 0.5871444360181357, -0.3036822597837272, -0.8505153742191851, -0.5012451945714247, -0.8060105059507805, 0.5330080478595391, 1.4260343762143748, -1.050545171033491, -0.6140910982553578, -0.2504994695145797, 0.6581363886531335, 0.47527809077325855, 0.7314233385789066, -0.7084492435407128, -1.227234868766459, -0.7943531062594502, 0.24601716633878823, 0.45095282087265787, 0.24100078165179184, 0.5882243348614793, -0.045955678732184424, 2.3011995430929026, -0.8055504416392125, 1.2857127416018557, 0.9605154956996103, -0.05181584374128079, -0.041085223578647856, -0.41963047564354383, 1.2953557251600536, 0.31683184725352476, -1.6086909528895035, 0.3949956845657887, -1.2512383164572398, -0.1762592429558083, 0.09344137531593333, 1.204989211145545]

We can assess the goodness of the model fit (to some extent) by plotting the residuals agains the fitted values, and checking for unexpected patterns. We use the gem gnuplotrb for plotting.


In [7]:
require 'gnuplotrb'
include GnuplotRB

x, y = model_fit.fitted, model_fit.residuals
fitted_vs_residuals = Plot.new([[x,y], with: 'points', pointtype: 6, notitle: true],
                               xlabel: 'Fitted', ylabel: 'Residuals')


Out[7]:
Gnuplot Produced by GNUPLOT 5.0 patchlevel rc2 -2 -1.5 -1 -0.5 0 0.5 1 1.5 2 2.5 -200 0 200 400 600 800 1000 1200 Residuals Fitted gnuplot_plot_1

We see that the residuals look more or less like noise, which is good.

We can further analyze the validity of the linear mixed model somewhat, by checking if the residuals appear to be approximately normally distributed.


In [8]:
bin_width = (y.max - y.min)/10.0
bins = (y.min..y.max).step(bin_width).to_a
rel_freq = Array.new(bins.length-1){0.0}
y.each do |r|
  0.upto(bins.length-2) do |i|
    if r >= bins[i] && r < bins[i+1] then
      rel_freq[i] += 1.0/y.length
    end
  end
end
bins_center = bins[0...-1].map { |b| b + bin_width/2.0 }
  
residuals_hist = Plot.new([[bins_center, rel_freq], with: 'boxes', notitle: true],
                           style: 'fill solid 0.5')


Out[8]:
Gnuplot Produced by GNUPLOT 5.0 patchlevel rc2 0.02 0.04 0.06 0.08 0.1 0.12 0.14 0.16 0.18 0.2 -2 -1.5 -1 -0.5 0 0.5 1 1.5 2 2.5 gnuplot_plot_1

The histogram does not appear to be too different from a bell shaped curve, although it might be slightly skewed to the right.

We can further explore the validity of the normality assumption by looking at the Q-Q plot of the residuals.


In [9]:
require 'distribution'

observed = model_fit.residuals.sort
n = observed.length
theoretical = (1..n).to_a.map { |t| Distribution::Normal.p_value(t.to_f/n.to_f) * model_fit.sigma}
qq_plot = Plot.new([[theoretical, observed], with: 'points', pointtype: 6, notitle: true],
                   ['x', with: 'lines', notitle: true],
                   xlabel: 'Normal theoretical quantiles', ylabel: 'Observed quantiles',
                   title: 'Q-Q plot of the residuals')


Out[9]:
Gnuplot Produced by GNUPLOT 5.0 patchlevel rc2 -2.5 -2 -1.5 -1 -0.5 0 0.5 1 1.5 2 2.5 -2.5 -2 -1.5 -1 -0.5 0 0.5 1 1.5 2 2.5 Observed quantiles Normal theoretical quantiles Q-Q plot of the residuals gnuplot_plot_1 gnuplot_plot_2

The straight line in the above plot is simply the diagonal. We see that the observed quantiles aggree with the theoretical values fairly well, as expected from a "good" model.

Fixed effects hypotheses tests and confidence intervals

Often statistical models are used in order to determine which of the predictor variables have a significant relationship with the response variable. LMM has a number of methods to aid with this kind of statistical inference.

Variances and covariances of the fixed effects coefficient estimates

The covariance matrix of the fixed effects estimates is returned by LMM#fix_ef_cov_mat, and the standard deviations of the fixed effects coefficients are returned by LMM#fix_ef_sd. Methods for hypotheses tests and confidence intervals can be based on these values.


In [10]:
model_fit.fix_ef_sd


Out[10]:
{:intercept=>60.19727495932258, :Age=>0.08988486367253856, :Species_lvl_Human=>0.2682523406941927, :Species_lvl_Ood=>0.2814470814004366, :Species_lvl_WeepingAngel=>0.2757835779525997}

Wald tests and confidence intervals

The Wald Z test statistics for the fixed effects coefficients can be computed with:


In [11]:
model_fit.fix_ef_z


Out[11]:
{:intercept=>16.882603431075875, :Age=>-0.7266646548258817, :Species_lvl_Human=>-1862.7747813759402, :Species_lvl_Ood=>-3196.2289922406044, :Species_lvl_WeepingAngel=>-723.7158917283754}

Based on the above Wald Z test statistics, we can carry out hypotheses tests for each fixed effects terms $\beta_{i}$ or $\gamma_{species}$, testing the null $H_{0} : \beta_{i} = 0$ against the alternative $H_{a} : \beta_{i} \neq 0$, or respectively the null $H_{0} : \gamma_{species} = 0$ against the alternative $H_{a} : \gamma_{species} \neq 0$.

The corresponding (approximate) p-values are obtained with:


In [12]:
model_fit.fix_ef_p(method: :wald)


Out[12]:
{:intercept=>0.0, :Age=>0.4674314106158888, :Species_lvl_Human=>0.0, :Species_lvl_Ood=>0.0, :Species_lvl_WeepingAngel=>0.0}

We see that the aggression level of each species is significantly different from the base level (which is the species Dalek in this model), while statistically we don't have enough evidence to conclude that the age of an individual is a good predictor of his/her/its aggression level.

We can use the Wald method for confidence intervals as well. For example 90% confidence intervals for each fixed effects coefficient estimate can be computed as follows.


In [13]:
conf_int = model_fit.fix_ef_conf_int(level: 0.9, method: :wald)


Out[13]:
{:intercept=>[917.2710135369496, 1115.302428002405], :Age=>[-0.2131635992213468, 0.08253129235199347], :Species_lvl_Human=>[-500.13493113101106, -499.25245944940696], :Species_lvl_Ood=>[-900.0322606117458, -899.1063820954081], :Species_lvl_WeepingAngel=>[-200.04258166587766, -199.13533441813757]}

For greated visual clarity we can put the coefficient estimates and the confidence intervals into a Daru::DataFrame:


In [14]:
df = Daru::DataFrame.rows(conf_int.values, order: [:lower90, :upper90], index: model_fit.fix_ef_names)
df[:coef] = model_fit.fix_ef.values
df


Out[14]:
Daru::DataFrame:70189652060000 rows: 5 cols: 3
lower90upper90coef
intercept917.27101353694961115.3024280024051016.2867207696772
Age-0.21316359922134680.08253129235199347-0.06531615343467667
Species_lvl_Human-500.13493113101106-499.25245944940696-499.693695290209
Species_lvl_Ood-900.0322606117458-899.1063820954081-899.5693213535769
Species_lvl_WeepingAngel-200.04258166587766-199.13533441813757-199.58895804200762

Predictions and prediction intervals

One might also fit a statistical model in order to predict future observations based on new data input.

We consider the following new data set containing age, geographic location and species for ten individuals.


In [15]:
newdata = Daru::DataFrame.from_csv '../examples/data/alien_species_newdata.csv'


Out[15]:
Daru::DataFrame:70189651729120 rows: 10 cols: 3
AgeLocationSpecies
0209OodSphereDalek
190EarthOod
2173AsylumOod
3153AsylumHuman
4255OodSphereWeepingAngel
5256AsylumWeepingAngel
637EarthDalek
7146EarthWeepingAngel
8127AsylumWeepingAngel
941AsylumOod

Point estimates

Based on the fitted linear mixed model we can predict the aggression levels for the inidividuals, where we can specify whether the random effects estimates should be included in the calculations or not.


In [16]:
puts "Predictions of aggression levels on a new data set:"
pred =  model_fit.predict(newdata: newdata, with_ran_ef: true)


Predictions of aggression levels on a new data set:
Out[16]:
[1070.9125752531213, 182.45206492790766, -17.064468754763425, 384.78815861991046, 876.1240725686444, 674.711339114886, 1092.6985606350875, 871.150885526236, 687.4629975728096, -4.0162601001437395]

Now we can add the computed predictions to the data set, in order to see better which of the individuals are likely to be particularly dangerous.


In [17]:
newdata = Daru::DataFrame.from_csv '../examples/data/alien_species_newdata.csv'
newdata[:Predicted_Agression] = pred
newdata


Out[17]:
Daru::DataFrame:70189650369680 rows: 10 cols: 4
AgeLocationSpeciesPredicted_Agression
0209OodSphereDalek1070.9125752531213
190EarthOod182.45206492790766
2173AsylumOod-17.064468754763425
3153AsylumHuman384.78815861991046
4255OodSphereWeepingAngel876.1240725686444
5256AsylumWeepingAngel674.711339114886
637EarthDalek1092.6985606350875
7146EarthWeepingAngel871.150885526236
8127AsylumWeepingAngel687.4629975728096
941AsylumOod-4.0162601001437395

Interval estimates

Since the estimated fixed and random effects coefficients most likely are not exactly the true values, we probably should look at interval estimates of the predictions, rather than the point estimates computed above.

Two types of such interval estimates are currently available in LMM. On the one hand, a confidence interval is an interval estimate of the mean value of the response for given covariates (i.e. a population parameter); on the other hand, a prediction interval is an interval estimate of a future observation (for further explanation of this distinction see for example https://stat.ethz.ch/education/semesters/ss2010/seminar/06_Handout.pdf).


In [18]:
puts "88% confidence intervals for the predictions:"
ci = model_fit.predict_with_intervals(newdata: newdata, level: 0.88, type: :confidence)
Daru::DataFrame.new(ci, order: [:pred, :lower88, :upper88])


88% confidence intervals for the predictions:
Out[18]:
Daru::DataFrame:70189649231640 rows: 10 cols: 3
predlower88upper88
01002.6356447018298906.27547368063051098.9958157230292
1110.8389456069794517.153931194406766204.52396001955213
2105.4177048719012610.164688001453413200.67072174234912
3506.59965400396266411.8519192433839601.3473887645414
4800.0421436018271701.9091175621678898.1751696414864
5799.9768274483924701.8009453651559898.152709531629
61013.8700230925942920.4439313837141107.2961148014745
7807.1616043262068712.5717592728961901.7514493795175
8808.4026112414656714.1916401880408902.6135822948904
9114.039437125278620.614034935160902207.4648393153963

In [19]:
puts "88% prediction intervals for the predictions:"
pi = model_fit.predict_with_intervals(newdata: newdata, level: 0.88, type: :prediction)
Daru::DataFrame.new(pi, order: [:pred, :lower88, :upper88])


88% prediction intervals for the predictions:
Out[19]:
Daru::DataFrame:70189645192360 rows: 10 cols: 3
predlower88upper88
01002.6356447018298809.91005021063671195.361239193023
1110.83894560697945-76.53615878138834298.21404999534724
2105.41770487190126-85.09352857986573295.92893832366826
3506.59965400396266317.0988996180352696.1004083898902
4800.0421436018271603.7713981525592996.312889051095
5799.9768274483924603.6203777718086996.3332771249762
61013.8700230925942827.01272329759831200.7273228875902
7807.1616043262068617.9767304767107996.3464781757028
8808.4026112414656619.975479314019996.8297431689122
9114.0394371252786-72.81614465010145300.89501890065867