Germán Rodríguez
Generalized Linear Models Princeton University

2.7 Two-Way Analysis of Variance

We start by reading the data and recreating setting in groups.

> library(dplyr)
> fpe <- read.table("https://grodri.github.io/datasets/effort.dat") |>
+   mutate(setting_g = cut(setting, breaks=c(min(setting),70,80,max(setting)),
+   right=FALSE, include.lowest=TRUE, labels=c("Low","Medium","High"))) 

Let us now create a copy of program effort and group it into categories 0-4, 5-14 and 15+. We’ll call the variable effort_g for effort in groups.

> fpe <- mutate(fpe, effort_g = cut(effort, breaks=c(min(effort), 5, 15, max(effort)),
+   right=FALSE, include.lowest=TRUE, labels=c("Weak","Moderate","Strong")))

Here’s a table showing steeper declines in countries with strong programs, with a smaller difference between weak and moderate:

> group_by(fpe, effort_g) |> summarize(change = mean(change))
# A tibble: 3 × 2
  effort_g change
  <fct>     <dbl>
1 Weak       5   
2 Moderate   9.33
3 Strong    27.9 

An Additive Model

Let us fit a model treating both setting and effort as factor variables, with weak programs in low settings as the reference cell

> m2g <- lm(change ~ setting_g + effort_g, data = fpe)
> summary(m2g)

Call:
lm(formula = change ~ setting_g + effort_g, data = fpe)

Residuals:
    Min      1Q  Median      3Q     Max 
-7.5347 -4.5883  0.5617  1.7909 11.7845 

Coefficients:
                 Estimate Std. Error t value Pr(>|t|)    
(Intercept)         5.379      3.105   1.732 0.103723    
setting_gMedium    -1.681      3.855  -0.436 0.669035    
setting_gHigh       2.388      4.457   0.536 0.599989    
effort_gModerate    3.836      3.575   1.073 0.300138    
effort_gStrong     20.672      4.339   4.764 0.000251 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 6.188 on 15 degrees of freedom
Multiple R-squared:  0.7833,    Adjusted R-squared:  0.7255 
F-statistic: 13.55 on 4 and 15 DF,  p-value: 7.19e-05
> anova(m2g)
Analysis of Variance Table

Response: change
          Df  Sum Sq Mean Sq F value    Pr(>F)    
setting_g  2 1193.79  596.89  15.588 0.0002176 ***
effort_g   2  882.02  441.01  11.517 0.0009320 ***
Residuals 15  574.39   38.29                      
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Compare these estimates with the results in Table 2.15 and the anova with Table 2.16 in the lecture notes.

Countries with strong family planning programs show steeper declines than countries with weak programs at the same level of social setting, on average 21 percentage points more.

This statement is based on the assumption of additivity, namely that the difference in outcomes across categories of program effort is the same at every level of setting. We will test this assumption below.

As you can see the differences in fertility change by level of effort, among countries with the same level of setting are significant, with an F-ratio of 11.5 on 2 and 15 d.f.

Fitted Values

Let us reproduce Table 2.17 in the notes, showing fitted means by grouped setting and effort. I will use fitted() to generate fitted values, and then summarize() them by the two relevant factors

> library(tidyr)

Attaching package: 'tidyr'
The following objects are masked from 'package:Matrix':

    expand, pack, unpack
> fpe <- mutate(fpe, fitted = fitted(m2g)) 
> group_by(fpe, setting_g, effort_g) |> summarize(fitted = mean(fitted)) |>
+   pivot_wider(names_from=effort_g, values_from=fitted)
`summarise()` has grouped output by 'setting_g'. You can override using the `.groups` argument.
# A tibble: 3 × 4
# Groups:   setting_g [3]
  setting_g  Weak Moderate Strong
  <fct>     <dbl>    <dbl>  <dbl>
1 Low        5.38     9.22   NA  
2 Medium     3.70     7.53   24.4
3 High       7.77    11.6    28.4

Can you get the missing cell in the upper right corner? How about the margins, can you get those values?

A Two-Factor Interaction

Let us now consider a model with an interaction between social setting and program effort, so differences in fertility decline by effort will vary by setting.

In R we use a colon : to indicate an interaction term, and an asterisk * to include main effects and interactions, so A*B is equivalent to A + B + A:B.

> m2gi <- lm(change ~ setting_g*effort_g, data=fpe)
> summary(m2gi)

Call:
lm(formula = change ~ setting_g * effort_g, data = fpe)

Residuals:
    Min      1Q  Median      3Q     Max 
-7.3333 -3.0000  0.0000  0.9375 11.6667 

Coefficients: (1 not defined because of singularities)
                                 Estimate Std. Error t value Pr(>|t|)  
(Intercept)                        2.6667     3.5775   0.745   0.4704  
setting_gMedium                    3.3333     5.0594   0.659   0.5224  
setting_gHigh                      6.3333     7.1550   0.885   0.3935  
effort_gModerate                   8.5833     4.7326   1.814   0.0948 .
effort_gStrong                    19.3333     6.6929   2.889   0.0136 *
setting_gMedium:effort_gModerate -14.5833     8.5786  -1.700   0.1149  
setting_gHigh:effort_gModerate    -6.5833     9.9594  -0.661   0.5211  
setting_gMedium:effort_gStrong    -0.3333     9.7974  -0.034   0.9734  
setting_gHigh:effort_gStrong           NA         NA      NA       NA  
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 6.196 on 12 degrees of freedom
Multiple R-squared:  0.8261,    Adjusted R-squared:  0.7247 
F-statistic: 8.146 on 7 and 12 DF,  p-value: 0.0009208

Oops, our software dropped a term. Why? Because there are no countries with strong programs in low settings, so we have only eight groups, but are trying to represent their means using nine parameters, which is obviously one too many. Fortunately this doesn’t affect testing.

> anova(m2gi)
Analysis of Variance Table

Response: change
                   Df  Sum Sq Mean Sq F value    Pr(>F)    
setting_g           2 1193.79  596.89 15.5458 0.0004664 ***
effort_g            2  882.02  441.01 11.4859 0.0016322 ** 
setting_g:effort_g  3  113.64   37.88  0.9866 0.4317952    
Residuals          12  460.75   38.40                      
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

We have no evidence that the differences by effort vary with social setting, with an F just below 1 on 3 and 12 d.f.

This makes the issue of interpreting parameters moot, but it may be worth noting briefly the problems caused by the empty cell. As things stand, the coefficient of moderate effort compares moderate with weak at low setting, but the coefficient of strong effort compares strong with weak at high setting. (Table 2.20 in the notes may help see this point. When the term for high and strong is dropped, the only difference between weak and strong programs at high setting is the coefficient of strong.)

The parametrization I like best for this problem combines the main effects of effort with the interactions, so we obtain differences between strong and weak, and between moderate and weak programs, at each level of setting. This allows us to omit the difference between strong and weak programs at low setting, which is the one we can’t identify. Try the specification below

lm( change ~ setting_g + effort_g:setting_g, data=fpe)

Dummy Variables

You could have obtained the same results in this unit using dummy variables. For the record, this is how you might build the dummies and fit the models. For the additive model we need just four indicators, two for each factor. In R I make sure the dummies are numeric, not boolean, otherwise the coefficient would have names like effortStrongTRUE.

d <- as.numeric # for short
mutate(fpe, 
  settingMed = d(setting_g=="Medium"), settingHigh = d(setting_g=="High"),
  effortMod = d(effort_g=="Moderate"), effortStrong = d(effort_g=="Strong"))
lm(change ~ settingMed + settingHigh + effortMod + effortStrong, data=fpe)

We need a total of four dummies to represent the interactions, which can be computed simply as the product of the indicators for the main effects.

fpe <- mutate(fpe, 
  seMedMod = settingMed*effortMod, seMedStrong=settingMed*effortStrong,
  seHighMod = settingHigh*effortMod, seHighStrong = settingHigh*effortStrong)
lm(change settingMed + settingHigh + effortMod + effortStrong + 
  seMedMod + seMedStrong + seHighMod + seHighStrong, data = fpe)

Our software will again omit a variable, but you have more control on what to drop. Can you figure out which dummies you would need to show the effects of effort at each level of setting?

Updated fall 2022