3. ANOVA tables and post-hoc comparisons

Note

ANOVAs and post-hoc tests are only available for Lmer models estimated using the factors argument of model.fit() and rely on implementations in R

In the previous tutorial where we looked at categorical predictors, behind the scenes pymer4 was using the factor functionality in R. This means the output of model.fit() looks a lot like summary() in R applied to a model with categorical predictors. But what if we want to compute an F-test across all levels of our categorical predictor?

pymer4 makes this easy to do, and makes it easy to ensure Type III sums of squares infereces are valid. It also makes it easy to follow up omnibus tests with post-hoc pairwise comparisons.

ANOVA tables and orthogonal contrasts

Because ANOVA is just regression, pymer4 can estimate ANOVA tables with F-results using the .anova() method on a fitted model. This will compute a Type-III SS table given the coding scheme provided when the model was initially fit. Based on the distribution of data across factor levels and the specific coding-scheme used, this may produce invalid Type-III SS computations. For this reason the .anova() method has a force-orthogonal=True argument that will reparameterize and refit the model using orthogonal polynomial contrasts prior to computing an ANOVA table.

Here we first estimate a mode with dummy-coded categories and suppress the summary output of .fit(). Then we use .anova() to examine the F-test results.

# import basic libraries and sample data
import os
import pandas as pd
from pymer4.utils import get_resource_path
from pymer4.models import Lmer

# IV3 is a categorical predictors with 3 levels in the sample data
df = pd.read_csv(os.path.join(get_resource_path(), "sample_data.csv"))

# # We're going to fit a multi-level regression using the
# categorical predictor (IV3) which has 3 levels
model = Lmer("DV ~ IV3 + (1|Group)", data=df)

# Using dummy-coding; suppress summary output
model.fit(factors={"IV3": ["1.0", "0.5", "1.5"]}, summarize=False)

# Get ANOVA table
print(model.anova())
SS Type III Analysis of Variance Table with Satterthwaite approximated degrees of freedom:
(NOTE: Using original model contrasts, orthogonality not guaranteed)
              SS           MS  NumDF     DenomDF    F-stat     P-val Sig
IV3  2359.778135  1179.889067      2  515.000001  5.296284  0.005287  **

Type III SS inferences will only be valid if data are fully balanced across levels or if contrasts between levels are orthogonally coded and sum to 0. Below we tell pymer4 to respecify our contrasts to ensure this before estimating the ANOVA. pymer4 also saves the last set of contrasts used priory to forcing orthogonality.

Because the sample data is balanced across factor levels and there are not interaction terms, in this case orthogonal contrast coding doesn’t change the results.

# Get ANOVA table, but this time force orthogonality
# for valid SS III inferences
# In this case the data are balanced so nothing changes
print(model.anova(force_orthogonal=True))
SS Type III Analysis of Variance Table with Satterthwaite approximated degrees of freedom:
(NOTE: Model refit with orthogonal polynomial contrasts)
              SS           MS  NumDF     DenomDF    F-stat     P-val Sig
IV3  2359.778135  1179.889067      2  515.000001  5.296284  0.005287  **
# Checkout current contrast scheme (for first contrast)
# Notice how it's simply a linear contrast across levels
print(model.factors)
{'IV3': ['0.5', '1.0', '1.5']}
# Checkout previous contrast scheme
# which was a treatment contrast with 1.0
# as the reference level
print(model.factors_prev_)
{'IV3': ['1.0', '0.5', '1.5']}

Marginal estimates and post-hoc comparisons

pymer4 leverages the emmeans package in order to compute marginal estimates (“cell means” in ANOVA lingo) and pair-wise comparisons of models that contain categorical terms and/or interactions. This can be performed by using the .post_hoc() method on fitted models. Let’s see an example:

First we’ll quickly create a second categorical IV to demo with and estimate a 3x3 ANOVA to get main effects and the interaction.

# Fix the random number generator
# for reproducibility
import numpy as np

np.random.seed(10)

# Create a new categorical variable with 3 levels
df = df.assign(IV4=np.random.choice(["1", "2", "3"], size=df.shape[0]))

# Estimate model with orthogonal polynomial contrasts
model = Lmer("DV ~ IV4*IV3 + (1|Group)", data=df)
model.fit(
    factors={"IV4": ["1", "2", "3"], "IV3": ["1.0", "0.5", "1.5"]},
    ordered=True,
    summarize=False,
)
# Get ANOVA table
# We can ignore the note in the output because
# we manually specified polynomial contrasts
print(model.anova())
SS Type III Analysis of Variance Table with Satterthwaite approximated degrees of freedom:
(NOTE: Using original model contrasts, orthogonality not guaranteed)
                  SS           MS  NumDF     DenomDF    F-stat     P-val Sig
IV4       449.771051   224.885525      2  510.897775  1.006943  0.366058
IV3      2486.124318  1243.062159      2  508.993080  5.565910  0.004063  **
IV4:IV3   553.852530   138.463132      4  511.073624  0.619980  0.648444

Example 1

Compare each level of IV3 to each other level of IV3, within each level of IV4. Use default Tukey HSD p-values.

# Compute post-hoc tests
marginal_estimates, comparisons = model.post_hoc(
    marginal_vars="IV3", grouping_vars="IV4"
)

# "Cell" means of the ANOVA
print(marginal_estimates)
P-values adjusted by tukey method for family of 3 estimates
   IV3 IV4  Estimate  2.5_ci  97.5_ci     SE      DF
1  1.0   1    42.554  33.778   51.330  4.398  68.140
2  0.5   1    45.455  36.644   54.266  4.417  69.299
3  1.5   1    40.904  32.196   49.612  4.361  65.943
4  1.0   2    42.092  33.301   50.882  4.406  68.609
5  0.5   2    41.495  32.829   50.161  4.339  64.626
6  1.5   2    38.786  29.961   47.612  4.425  69.746
7  1.0   3    43.424  34.741   52.107  4.348  65.149
8  0.5   3    46.008  37.261   54.755  4.383  67.208
9  1.5   3    38.119  29.384   46.854  4.376  66.801
# Pairwise comparisons
print(comparisons)
          Contrast IV4  Estimate  2.5_ci  ...       DF  T-stat  P-val  Sig
1  IV31.0 - IV30.5   1    -2.901  -9.523  ...  510.016  -1.030  0.558
2  IV31.0 - IV31.5   1     1.650  -4.750  ...  510.137   0.606  0.817
3  IV30.5 - IV31.5   1     4.552  -1.951  ...  510.267   1.645  0.228
4  IV31.0 - IV30.5   2     0.596  -5.749  ...  510.249   0.221  0.973
5  IV31.0 - IV31.5   2     3.305  -3.387  ...  510.883   1.161  0.477
6  IV30.5 - IV31.5   2     2.709  -3.749  ...  510.732   0.986  0.586
7  IV31.0 - IV30.5   3    -2.584  -8.893  ...  510.213  -0.963  0.601
8  IV31.0 - IV31.5   3     5.305  -1.006  ...  510.710   1.976  0.119
9  IV30.5 - IV31.5   3     7.889   1.437  ...  510.663   2.874  0.012    *

[9 rows x 10 columns]

Example 2

Compare each unique IV3,IV4 “cell mean” to every other IV3,IV4 “cell mean” and used FDR correction for multiple comparisons:

# Compute post-hoc tests
marginal_estimates, comparisons = model.post_hoc(
    marginal_vars=["IV3", "IV4"], p_adjust="fdr"
)

# Pairwise comparisons
print(comparisons)
P-values adjusted by fdr method for 36 comparisons
                     Contrast  Estimate  2.5_ci  ...  T-stat  P-val  Sig
1   IV31.0 IV41 - IV30.5 IV41    -2.901 -11.957  ...  -1.030  0.535
2   IV31.0 IV41 - IV31.5 IV41     1.650  -7.102  ...   0.606  0.726
3   IV31.0 IV41 - IV31.0 IV42     0.463  -8.657  ...   0.163  0.871
4   IV31.0 IV41 - IV30.5 IV42     1.059  -7.649  ...   0.391  0.835
5   IV31.0 IV41 - IV31.5 IV42     3.768  -5.364  ...   1.326  0.473
6   IV31.0 IV41 - IV31.0 IV43    -0.870  -9.659  ...  -0.318  0.869
7   IV31.0 IV41 - IV30.5 IV43    -3.454 -12.306  ...  -1.254  0.473
8   IV31.0 IV41 - IV31.5 IV43     4.435  -4.426  ...   1.609  0.390
9   IV30.5 IV41 - IV31.5 IV41     4.552  -4.341  ...   1.645  0.390
10  IV30.5 IV41 - IV31.0 IV42     3.364  -5.732  ...   1.189  0.493
11  IV30.5 IV41 - IV30.5 IV42     3.960  -4.883  ...   1.440  0.446
12  IV30.5 IV41 - IV31.5 IV42     6.669  -2.568  ...   2.321  0.186
13  IV30.5 IV41 - IV31.0 IV43     2.031  -6.796  ...   0.740  0.637
14  IV30.5 IV41 - IV30.5 IV43    -0.552  -9.603  ...  -0.196  0.869
15  IV30.5 IV41 - IV31.5 IV43     7.336  -1.568  ...   2.648  0.118
16  IV31.5 IV41 - IV31.0 IV42    -1.188 -10.044  ...  -0.431  0.827
17  IV31.5 IV41 - IV30.5 IV42    -0.591  -9.041  ...  -0.225  0.869
18  IV31.5 IV41 - IV31.5 IV42     2.117  -6.937  ...   0.752  0.637
19  IV31.5 IV41 - IV31.0 IV43    -2.520 -11.037  ...  -0.951  0.535
20  IV31.5 IV41 - IV30.5 IV43    -5.104 -13.818  ...  -1.883  0.362
21  IV31.5 IV41 - IV31.5 IV43     2.785  -5.986  ...   1.021  0.535
22  IV31.0 IV42 - IV30.5 IV42     0.596  -8.082  ...   0.221  0.869
23  IV31.0 IV42 - IV31.5 IV42     3.305  -5.848  ...   1.161  0.493
24  IV31.0 IV42 - IV31.0 IV43    -1.333 -10.235  ...  -0.481  0.811
25  IV31.0 IV42 - IV30.5 IV43    -3.916 -12.888  ...  -1.403  0.446
26  IV31.0 IV42 - IV31.5 IV43     3.972  -4.883  ...   1.442  0.446
27  IV30.5 IV42 - IV31.5 IV42     2.709  -6.123  ...   0.986  0.535
28  IV30.5 IV42 - IV31.0 IV43    -1.929 -10.318  ...  -0.739  0.637
29  IV30.5 IV42 - IV30.5 IV43    -4.513 -13.207  ...  -1.669  0.390
30  IV30.5 IV42 - IV31.5 IV43     3.376  -5.172  ...   1.270  0.473
31  IV31.5 IV42 - IV31.0 IV43    -4.638 -13.457  ...  -1.691  0.390
32  IV31.5 IV42 - IV30.5 IV43    -7.222 -16.183  ...  -2.590  0.118
33  IV31.5 IV42 - IV31.5 IV43     0.667  -8.475  ...   0.235  0.869
34  IV31.0 IV43 - IV30.5 IV43    -2.584 -11.212  ...  -0.963  0.535
35  IV31.0 IV43 - IV31.5 IV43     5.305  -3.325  ...   1.976  0.351
36  IV30.5 IV43 - IV31.5 IV43     7.889  -0.935  ...   2.874  0.118

[36 rows x 9 columns]

Example 3

For this example we’ll estimate a more complicated ANOVA with 1 continuous IV and 2 categorical IVs with 3 levels each. This is the same model as before but with IV2 thrown into the mix. Now, pairwise comparisons reflect changes in the slope of the continuous IV (IV2) between levels of the categorical IVs (IV3 and IV4).

First let’s get the ANOVA table

model = Lmer("DV ~ IV2*IV3*IV4 + (1|Group)", data=df)
# Only need to polynomial contrasts for IV3 and IV4
# because IV2 is continuous
model.fit(
    factors={"IV4": ["1", "2", "3"], "IV3": ["1.0", "0.5", "1.5"]},
    ordered=True,
    summarize=False,
)

# Get ANOVA table
print(model.anova())
SS Type III Analysis of Variance Table with Satterthwaite approximated degrees of freedom:
(NOTE: Using original model contrasts, orthogonality not guaranteed)
                       SS            MS  NumDF  ...      F-stat         P-val  Sig
IV2          46010.245471  46010.245471      1  ...  306.765451  1.220547e-54  ***
IV3            726.318000    363.159000      2  ...    2.421301  8.984551e-02    .
IV4            143.379932     71.689966      2  ...    0.477981  6.203159e-01
IV2:IV3        613.455876    306.727938      2  ...    2.045056  1.304528e-01
IV2:IV4          4.914900      2.457450      2  ...    0.016385  9.837494e-01
IV3:IV4         92.225327     23.056332      4  ...    0.153724  9.612985e-01
IV2:IV3:IV4    368.085569     92.021392      4  ...    0.613537  6.530638e-01

[7 rows x 7 columns]

Now we can compute the pairwise difference in slopes

# Compute post-hoc tests with bonferroni correction
marginal_estimates, comparisons = model.post_hoc(
    marginal_vars="IV2", grouping_vars=["IV3", "IV4"], p_adjust="bonf"
)

# Pairwise comparisons
print(comparisons)
P-values adjusted by bonf method for 3 comparisons
          Contrast IV4  Estimate  2.5_ci  ...       DF  T-stat  P-val  Sig
1  IV31.0 - IV30.5   1    -0.053  -0.254  ...  502.345  -0.638  1.000
2  IV31.0 - IV31.5   1    -0.131  -0.313  ...  502.494  -1.734  0.250
3  IV30.5 - IV31.5   1    -0.078  -0.278  ...  502.821  -0.933  1.000
4  IV31.0 - IV30.5   2    -0.038  -0.210  ...  501.096  -0.526  1.000
5  IV31.0 - IV31.5   2     0.002  -0.184  ...  502.745   0.031  1.000
6  IV30.5 - IV31.5   2     0.040  -0.142  ...  502.836   0.530  1.000
7  IV31.0 - IV30.5   3    -0.134  -0.329  ...  502.956  -1.646  0.301
8  IV31.0 - IV31.5   3    -0.110  -0.302  ...  502.109  -1.368  0.516
9  IV30.5 - IV31.5   3     0.024  -0.166  ...  502.538   0.304  1.000

[9 rows x 10 columns]

Gallery generated by Sphinx-Gallery