7  Week 7: Analysis of Variance (One-Way)

NoteLearning Objectives

By the end of this week, you will be able to:

  1. Express one-way ANOVA as a linear model in matrix notation
  2. Construct design matrices for categorical predictors (indicator variables)
  3. Partition total variation into model and error components
  4. Conduct F-tests for treatment effects and interpret results in biological context
  5. Understand the relationship between cell means and effects model parameterizations

7.1 Introduction: Comparing Means in Animal Breeding

In Weeks 4-6, we studied linear models with continuous predictors (regression). We learned how to construct design matrices, solve normal equations, and test hypotheses—all within the unified framework of y = Xβ + e.

This week, we extend this framework to categorical predictors. Instead of asking “How does milk yield change with days in milk?” (continuous), we ask “Do different breeds have different average milk yields?” (categorical). This is the realm of Analysis of Variance (ANOVA).

ImportantANOVA is Regression with Categorical Predictors

The fundamental insight: ANOVA is not a different statistical method. It’s the same linear model we’ve been studying:

\[\mathbf{y} = \mathbf{X}\boldsymbol{\beta} + \mathbf{e}\]

The only difference: - Regression: X contains continuous values (weights, ages, temperatures) - ANOVA: X contains 0s and 1s (indicator/dummy variables for groups)

Same normal equations: X′Xb = X′y Same least squares solution: b = (X′X)⁻¹X′y Same F-tests, same SSE, same geometric interpretation!

7.1.1 Why Learn ANOVA?

Comparing group means is fundamental in animal breeding and genetics:

  • Breed comparisons: Do Holstein cows produce more milk than Jerseys?
  • Diet trials: Which feedlot ration maximizes growth rate in beef steers?
  • Genetic line evaluation: Which layer strain has superior egg production?
  • Treatment effects: Does a new supplement improve feed efficiency in broilers?

In all these scenarios, we have: - One response variable (milk yield, growth rate, eggs, FCR) - One categorical predictor (breed, diet, line, treatment) - Multiple groups/levels within that predictor

This is one-way ANOVA: one categorical factor with multiple levels.

7.1.2 Preview

In this chapter, we’ll:

  1. Formalize the one-way ANOVA model using matrix notation
  2. Learn two equivalent parameterizations: cell means and effects models
  3. Derive the sum of squares partitioning: SST = SSM + SSE
  4. Construct ANOVA tables and conduct F-tests
  5. Build our own ANOVA solver from scratch in R
  6. Apply ANOVA to realistic livestock data

Let’s begin!


7.2 Mathematical Theory

7.2.1 One-Way ANOVA Setting

Consider an experiment comparing g groups (treatments, breeds, diets, etc.). We observe:

\[y_{ij} = \text{response for observation } j \text{ in group } i\]

where: - \(i = 1, 2, \ldots, g\) indexes the groups - \(j = 1, 2, \ldots, n_i\) indexes observations within group \(i\) - \(n = \sum_{i=1}^g n_i\) is the total sample size

NoteNotation: Dot Subscript Convention

We use dots to indicate averaging:

  • \(\bar{y}_{i.} = \frac{1}{n_i}\sum_{j=1}^{n_i} y_{ij}\) = mean of group \(i\)
  • \(\bar{y}_{..} = \frac{1}{n}\sum_{i=1}^g \sum_{j=1}^{n_i} y_{ij}\) = grand mean (overall average)

The dot replaces the subscript being averaged over. This notation will be essential for sum of squares formulas.

Balanced vs. Unbalanced Designs:

  • Balanced: All groups have equal sample sizes (\(n_1 = n_2 = \cdots = n_g\))
    • Mathematically simpler (orthogonal design matrices)
    • Equal precision for all group means
    • Preferred whenever possible in practice
  • Unbalanced: Groups have unequal sample sizes
    • More common in real data (animals die, observations lost)
    • Complicates interpretation and computation
    • We’ll see a brief example, with full treatment in Week 12

For this week, we focus primarily on balanced designs to build intuition.


7.2.2 Cell Means Model

The simplest ANOVA parameterization is the cell means model:

\[y_{ij} = \mu_i + e_{ij} \quad i=1,\ldots,g; \quad j=1,\ldots,n_i \tag{7.1}\]

where: - \(\mu_i\) = population mean of group \(i\) - \(e_{ij}\) = random error for observation \(j\) in group \(i\)

Assumptions (same as always): - \(E(e_{ij}) = 0\) for all \(i, j\) - \(\text{Var}(e_{ij}) = \sigma^2\) for all \(i, j\) (homoscedasticity) - \(e_{ij}\) are independent - (Optional for inference) \(e_{ij} \sim N(0, \sigma^2)\)

Matrix Form

In matrix notation:

\[\mathbf{y} = \mathbf{X}\boldsymbol{\beta} + \mathbf{e} \tag{7.2}\]

where β = \([\mu_1, \mu_2, \ldots, \mu_g]'\) (one parameter per group) and X is the design matrix.

Example: Suppose \(g=3\) groups with \(n_1=2\), \(n_2=2\), \(n_3=2\) (balanced, \(n=6\) total).

Data structure:

Observation:  1    2   |  3    4   |  5    6
Group:        1    1   |  2    2   |  3    3
Response:    y11  y12  | y21  y22  | y31  y32

The design matrix X (\(6 \times 3\)) uses indicator variables (0s and 1s):

\[\mathbf{X} = \begin{bmatrix} 1 & 0 & 0 \\ 1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \\ 0 & 0 & 1 \end{bmatrix}, \quad \boldsymbol{\beta} = \begin{bmatrix} \mu_1 \\ \mu_2 \\ \mu_3 \end{bmatrix}, \quad \mathbf{y} = \begin{bmatrix} y_{11} \\ y_{12} \\ y_{21} \\ y_{22} \\ y_{31} \\ y_{32} \end{bmatrix}\]

Interpretation: Each column of X is an indicator for one group. If observation \(j\) belongs to group \(i\), then \(X_{ji} = 1\); otherwise \(X_{ji} = 0\).

Verify: \(\mathbf{X}\boldsymbol{\beta}\) gives the model predictions:

\[\mathbf{X}\boldsymbol{\beta} = \begin{bmatrix} \mu_1 \\ \mu_1 \\ \mu_2 \\ \mu_2 \\ \mu_3 \\ \mu_3 \end{bmatrix}\]

Each observation is predicted by its group mean. Perfect!

Normal Equations

The normal equations are:

\[\mathbf{X}'\mathbf{X} \mathbf{b} = \mathbf{X}'\mathbf{y} \tag{7.3}\]

For our example:

\[\mathbf{X}'\mathbf{X} = \begin{bmatrix} 2 & 0 & 0 \\ 0 & 2 & 0 \\ 0 & 0 & 2 \end{bmatrix} = 2\mathbf{I}_3\]

This is diagonal! In general, for a balanced design with \(n_i = n\) observations per group:

\[\mathbf{X}'\mathbf{X} = n\mathbf{I}_g\]

For unbalanced designs:

\[\mathbf{X}'\mathbf{X} = \text{diag}(n_1, n_2, \ldots, n_g)\]

Still diagonal, just with different diagonal elements.

The right side:

\[\mathbf{X}'\mathbf{y} = \begin{bmatrix} y_{11} + y_{12} \\ y_{21} + y_{22} \\ y_{31} + y_{32} \end{bmatrix} = \begin{bmatrix} \sum_{j=1}^{n_1} y_{1j} \\ \sum_{j=1}^{n_2} y_{2j} \\ \sum_{j=1}^{n_3} y_{3j} \end{bmatrix}\]

This is just the sum of observations in each group!

Solution

Since X′X is diagonal (and full rank), the inverse is trivial:

\[(\mathbf{X}'\mathbf{X})^{-1} = \frac{1}{n}\mathbf{I}_g \quad \text{(balanced)} \quad \text{or} \quad \text{diag}(1/n_1, 1/n_2, \ldots, 1/n_g) \quad \text{(unbalanced)}\]

Therefore:

\[\mathbf{b} = (\mathbf{X}'\mathbf{X})^{-1}\mathbf{X}'\mathbf{y} = \begin{bmatrix} \frac{1}{n_1}\sum_j y_{1j} \\ \frac{1}{n_2}\sum_j y_{2j} \\ \vdots \\ \frac{1}{n_g}\sum_j y_{gj} \end{bmatrix} = \begin{bmatrix} \bar{y}_{1.} \\ \bar{y}_{2.} \\ \vdots \\ \bar{y}_{g.} \end{bmatrix} \tag{7.4}\]

Result: The least squares estimates are simply the sample means of each group!

\[b_i = \bar{y}_{i.} = \text{mean of group } i\]

TipCell Means Model: Direct and Interpretable

The cell means model has several advantages:

  1. Always full rank: X′X is always invertible (as long as each group has \(n_i \geq 1\))
  2. Direct interpretation: Each parameter \(\mu_i\) is simply the mean of group \(i\)
  3. No constraints needed: All parameters are uniquely estimable
  4. Simple estimates: \(b_i = \bar{y}_{i.}\) (group sample means)

This is why we often prefer the cell means model for computation and interpretation, even though the effects model (next section) is more common in textbooks.


7.2.3 Effects Model

An alternative parameterization is the effects model:

\[y_{ij} = \mu + \alpha_i + e_{ij} \tag{7.5}\]

where: - \(\mu\) = grand mean (overall average across all groups) - \(\alpha_i\) = effect of group \(i\) (deviation from grand mean) - \(e_{ij}\) = random error

Relationship to cell means: If we define \(\mu_i = \mu + \alpha_i\), then:

\[\text{Cell means model: } y_{ij} = \mu_i + e_{ij}\] \[\text{Effects model: } y_{ij} = \mu + \alpha_i + e_{ij} = (\mu + \alpha_i) + e_{ij} = \mu_i + e_{ij}\]

They’re equivalent! Just different ways to write the same model.

Overparameterization Problem

Consider \(g=3\) groups. The effects model has: - 1 parameter \(\mu\) (grand mean) - 3 parameters \(\alpha_1, \alpha_2, \alpha_3\) (group effects) - Total: 4 parameters for only 3 groups!

But we only have 3 distinct group means. We can’t uniquely estimate 4 parameters from 3 means. The model is overparameterized.

Design matrix: For our \(g=3\), \(n_i=2\) example:

\[\mathbf{X} = \begin{bmatrix} 1 & 1 & 0 & 0 \\ 1 & 1 & 0 & 0 \\ 1 & 0 & 1 & 0 \\ 1 & 0 & 1 & 0 \\ 1 & 0 & 0 & 1 \\ 1 & 0 & 0 & 1 \end{bmatrix}, \quad \boldsymbol{\beta} = \begin{bmatrix} \mu \\ \alpha_1 \\ \alpha_2 \\ \alpha_3 \end{bmatrix}\]

The first column (all 1s) is for \(\mu\). Columns 2-4 are indicators for groups 1-3.

Check the rank:

\[\mathbf{X}'\mathbf{X} = \begin{bmatrix} 6 & 2 & 2 & 2 \\ 2 & 2 & 0 & 0 \\ 2 & 0 & 2 & 0 \\ 2 & 0 & 0 & 2 \end{bmatrix}\]

This matrix is singular (not full rank)! Note that: - Column 1 = sum of columns 2, 3, 4 - Or: Row 1 = sum of rows 2, 3, 4

Therefore \(\text{rank}(\mathbf{X}'\mathbf{X}) = 3 < 4\). The matrix is not invertible.

WarningRank Deficiency in Effects Model

The effects model X′X is not full rank because we have one more parameter than we need. The first column of X (the intercept) is the sum of the group indicator columns.

Implication: We cannot uniquely solve the normal equations without additional constraints.

Common constraint: Set \(\sum_{i=1}^g \alpha_i = 0\) (sum-to-zero constraint). This forces the group effects to average to zero.

We’ll demonstrate this in the small example. For a thorough treatment of rank deficiency, estimability, and constraints, see Week 12: Non-Full Rank Models.

Applying the Sum-to-Zero Constraint

With the constraint \(\sum_{i=1}^g \alpha_i = 0\), we can express \(\alpha_g = -(\alpha_1 + \alpha_2 + \cdots + \alpha_{g-1})\) and eliminate one parameter.

Reduced design matrix (constraining \(\alpha_3 = -\alpha_1 - \alpha_2\)):

\[\mathbf{X}_{\text{reduced}} = \begin{bmatrix} 1 & 1 & 0 \\ 1 & 1 & 0 \\ 1 & 0 & 1 \\ 1 & 0 & 1 \\ 1 & -1 & -1 \\ 1 & -1 & -1 \end{bmatrix}, \quad \boldsymbol{\beta}_{\text{reduced}} = \begin{bmatrix} \mu \\ \alpha_1 \\ \alpha_2 \end{bmatrix}\]

Now \(\mathbf{X}_{\text{reduced}}'\mathbf{X}_{\text{reduced}}\) is \(3 \times 3\) and full rank.

Solution: With the constraint, we get: - \(\hat{\mu} = \bar{y}_{..}\) (grand mean) - \(\hat{\alpha}_i = \bar{y}_{i.} - \bar{y}_{..}\) (deviation of group \(i\) from grand mean)

We’ll verify this in the small example.


7.2.4 Sum of Squares Partitioning

The power of ANOVA comes from decomposing total variation into interpretable components.

Total Sum of Squares (SST)

Total variation in the data (ignoring groups):

\[\text{SST} = \sum_{i=1}^g \sum_{j=1}^{n_i} (y_{ij} - \bar{y}_{..})^2 \tag{7.6}\]

This measures how much observations vary around the grand mean \(\bar{y}_{..}\).

Degrees of freedom: \(\text{df}_{\text{Total}} = n - 1\)

Treatment Sum of Squares (SSM or SS(Treatments))

Variation between groups (how much do group means differ?):

\[\text{SS(Treatments)} = \sum_{i=1}^g n_i(\bar{y}_{i.} - \bar{y}_{..})^2 \tag{7.7}\]

This measures how much the group means vary around the grand mean. If all groups have the same mean, SS(Treatments) = 0.

Degrees of freedom: \(\text{df}_{\text{Treatments}} = g - 1\)

NoteNotation: SSM vs. SS(Treatments)

Different texts use different notation: - SSM = Sum of Squares for Model (we’ve used this in Weeks 4-6) - SS(Treatments) = Sum of Squares for Treatments (common in ANOVA texts) - SSB = Sum of Squares Between groups (also common)

They all mean the same thing: variation explained by the group factor. We’ll use SSM and SS(Treatments) interchangeably.

Error Sum of Squares (SSE)

Variation within groups (how much do observations vary around their group means?):

\[\text{SSE} = \sum_{i=1}^g \sum_{j=1}^{n_i} (y_{ij} - \bar{y}_{i.})^2 \tag{7.8}\]

This measures unexplained variation (residuals).

Degrees of freedom: \(\text{df}_{\text{Error}} = n - g\)

The Fundamental Decomposition

ImportantThe Fundamental ANOVA Decomposition

For any dataset:

\[\text{SST} = \text{SS(Treatments)} + \text{SSE}\]

Or equivalently:

\[\sum_{i=1}^g \sum_{j=1}^{n_i} (y_{ij} - \bar{y}_{..})^2 = \sum_{i=1}^g n_i(\bar{y}_{i.} - \bar{y}_{..})^2 + \sum_{i=1}^g \sum_{j=1}^{n_i} (y_{ij} - \bar{y}_{i.})^2\]

Interpretation: - Total variation = Between-group variation + Within-group variation - SST = SSM + SSE

This partitioning is exact (no approximation), and the degrees of freedom also add up:

\[n - 1 = (g - 1) + (n - g)\]

Proof of the Decomposition

We can prove this algebraically. Start with the identity:

\[y_{ij} - \bar{y}_{..} = (\bar{y}_{i.} - \bar{y}_{..}) + (y_{ij} - \bar{y}_{i.})\]

This says: deviation from grand mean = deviation of group mean from grand mean + deviation of observation from its group mean.

Square both sides:

\[(y_{ij} - \bar{y}_{..})^2 = (\bar{y}_{i.} - \bar{y}_{..})^2 + (y_{ij} - \bar{y}_{i.})^2 + 2(\bar{y}_{i.} - \bar{y}_{..})(y_{ij} - \bar{y}_{i.})\]

Sum over all \(i\) and \(j\):

\[\sum_i \sum_j (y_{ij} - \bar{y}_{..})^2 = \sum_i \sum_j (\bar{y}_{i.} - \bar{y}_{..})^2 + \sum_i \sum_j (y_{ij} - \bar{y}_{i.})^2 + 2\sum_i \sum_j (\bar{y}_{i.} - \bar{y}_{..})(y_{ij} - \bar{y}_{i.})\]

The first term on the right: \((\bar{y}_{i.} - \bar{y}_{..})\) is constant within group \(i\), so:

\[\sum_j (\bar{y}_{i.} - \bar{y}_{..})^2 = n_i(\bar{y}_{i.} - \bar{y}_{..})^2\]

The cross-product term vanishes:

\[\sum_j (y_{ij} - \bar{y}_{i.}) = \sum_j y_{ij} - n_i\bar{y}_{i.} = n_i\bar{y}_{i.} - n_i\bar{y}_{i.} = 0\]

Therefore:

\[\text{SST} = \text{SS(Treatments)} + \text{SSE} \quad \blacksquare\]

Matrix Forms

We can also express these in matrix notation. Recall from Week 5:

\[\text{SST} = \mathbf{y}'\mathbf{y} - n\bar{y}_{..}^2 = \mathbf{y}'(\mathbf{I} - \frac{1}{n}\mathbf{J})\mathbf{y}\]

where J is the \(n \times n\) matrix of all ones.

\[\text{SSM} = \mathbf{b}'\mathbf{X}'\mathbf{y} - n\bar{y}_{..}^2\]

\[\text{SSE} = \mathbf{y}'\mathbf{y} - \mathbf{b}'\mathbf{X}'\mathbf{y} = \mathbf{e}'\mathbf{e}\]

These are the same formulas we used in regression (Weeks 4-6)!


7.2.5 ANOVA Table and F-Test

We summarize the sum of squares decomposition in an ANOVA table:

Source df Sum of Squares Mean Square F p-value
Treatments \(g-1\) SSM MSM MSM/MSE \(P(F > F_{\text{obs}})\)
Error \(n-g\) SSE MSE
Total \(n-1\) SST

where:

  • Mean Square for Model (MSM): \(\text{MSM} = \frac{\text{SSM}}{g-1}\)
  • Mean Square for Error (MSE): \(\text{MSE} = \frac{\text{SSE}}{n-g} = \hat{\sigma}^2\) (our estimate of \(\sigma^2\))
  • F-statistic: \(F = \frac{\text{MSM}}{\text{MSE}}\)

The F-Test

Hypotheses:

\[H_0: \mu_1 = \mu_2 = \cdots = \mu_g \quad \text{(all group means are equal)}\] \[H_a: \text{at least one } \mu_i \text{ differs from the others}\]

Test statistic:

\[F = \frac{\text{MSM}}{\text{MSE}} = \frac{\text{SSM}/(g-1)}{\text{SSE}/(n-g)} \tag{7.9}\]

Distribution under \(H_0\):

\[F \sim F_{g-1, n-g}\]

where \(F_{g-1, n-g}\) is the F-distribution with numerator df = \(g-1\) and denominator df = \(n-g\).

Decision rule: Reject \(H_0\) if \(F > F_{g-1, n-g, \alpha}\) (critical value from F-table), or equivalently if p-value \(< \alpha\).

NoteInterpreting the F-Test

The F-statistic is a ratio of variances:

\[F = \frac{\text{Between-group variability}}{\text{Within-group variability}}\]

Intuition: - If \(H_0\) is true (all means equal), then MSM and MSE both estimate \(\sigma^2\), so \(F \approx 1\) - If \(H_0\) is false (means differ), then MSM is inflated by treatment effects, so \(F > 1\) - Large \(F\) provides evidence against \(H_0\)

What the F-test does NOT tell us: - It does NOT tell us which specific groups differ - It does NOT tell us the magnitude of differences - It’s an omnibus test: “Is there any difference anywhere?”

For specific comparisons between groups, we need contrasts (Week 8).


7.3 Small Numerical Example: Milk Yield Across Four Dairy Breeds

Let’s work through a complete example by hand to solidify understanding.

7.3.1 Data

We compare daily milk yield (kg/day) across four dairy breeds. Each breed has 3 cows (balanced design, \(n=12\) total).

# Data
breed <- rep(c("Holstein", "Jersey", "BrownSwiss", "Ayrshire"), each = 3)
milk_yield <- c(30, 32, 31,    # Holstein
                24, 25, 24,    # Jersey
                28, 29, 27,    # Brown Swiss
                26, 27, 26)    # Ayrshire

# Create data frame
milk_data <- data.frame(breed = breed, milk_yield = milk_yield)
print(milk_data)
        breed milk_yield
1    Holstein         30
2    Holstein         32
3    Holstein         31
4      Jersey         24
5      Jersey         25
6      Jersey         24
7  BrownSwiss         28
8  BrownSwiss         29
9  BrownSwiss         27
10   Ayrshire         26
11   Ayrshire         27
12   Ayrshire         26

Summary: - Holstein: 30, 32, 31 (mean = 31.0) - Jersey: 24, 25, 24 (mean = 24.33) - Brown Swiss: 28, 29, 27 (mean = 28.0) - Ayrshire: 26, 27, 26 (mean = 26.33) - Grand mean: \(\bar{y}_{..} = (93 + 73 + 84 + 79)/12 = 329/12 = 27.417\)

7.3.2 Part A: Cell Means Model (Full Hand Calculation)

Model: \(y_{ij} = \mu_i + e_{ij}\)

Step 1: Construct y vector (\(12 \times 1\)):

\[\mathbf{y} = \begin{bmatrix} 30 \\ 32 \\ 31 \\ 24 \\ 25 \\ 24 \\ 28 \\ 29 \\ 27 \\ 26 \\ 27 \\ 26 \end{bmatrix}\]

Step 2: Construct X matrix (\(12 \times 4\)):

Columns represent breeds: Holstein, Jersey, BrownSwiss, Ayrshire.

\[\mathbf{X} = \begin{bmatrix} 1 & 0 & 0 & 0 \\ 1 & 0 & 0 & 0 \\ 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \\ 0 & 0 & 0 & 1 \\ 0 & 0 & 0 & 1 \end{bmatrix}\]

Step 3: Compute X′X (\(4 \times 4\)):

\[\mathbf{X}'\mathbf{X} = \begin{bmatrix} 3 & 0 & 0 & 0 \\ 0 & 3 & 0 & 0 \\ 0 & 0 & 3 & 0 \\ 0 & 0 & 0 & 3 \end{bmatrix} = 3\mathbf{I}_4\]

Perfect! Diagonal matrix (because balanced design).

Step 4: Compute X′y (\(4 \times 1\)):

\[\mathbf{X}'\mathbf{y} = \begin{bmatrix} 30 + 32 + 31 \\ 24 + 25 + 24 \\ 28 + 29 + 27 \\ 26 + 27 + 26 \end{bmatrix} = \begin{bmatrix} 93 \\ 73 \\ 84 \\ 79 \end{bmatrix}\]

These are the sums for each breed.

Step 5: Solve normal equations:

\[\mathbf{b} = (\mathbf{X}'\mathbf{X})^{-1}\mathbf{X}'\mathbf{y} = \frac{1}{3}\mathbf{I}_4 \begin{bmatrix} 93 \\ 73 \\ 84 \\ 79 \end{bmatrix} = \begin{bmatrix} 31.00 \\ 24.33 \\ 28.00 \\ 26.33 \end{bmatrix}\]

Result: The estimates are exactly the group means! \(b_i = \bar{y}_{i.}\)

Step 6: Compute fitted values and residuals:

\[\hat{\mathbf{y}} = \mathbf{X}\mathbf{b} = \begin{bmatrix} 31.00 \\ 31.00 \\ 31.00 \\ 24.33 \\ 24.33 \\ 24.33 \\ 28.00 \\ 28.00 \\ 28.00 \\ 26.33 \\ 26.33 \\ 26.33 \end{bmatrix}\]

Each observation is fitted by its group mean.

\[\mathbf{e} = \mathbf{y} - \hat{\mathbf{y}} = \begin{bmatrix} -1.00 \\ 1.00 \\ 0.00 \\ -0.33 \\ 0.67 \\ -0.33 \\ 0.00 \\ 1.00 \\ -1.00 \\ -0.33 \\ 0.67 \\ -0.33 \end{bmatrix}\]

Step 7: Calculate sum of squares:

SST: \[\begin{align} \text{SST} &= \sum_{i,j} (y_{ij} - \bar{y}_{..})^2 \\ &= (30-27.417)^2 + (32-27.417)^2 + \cdots + (26-27.417)^2 \\ &= 6.670 + 21.004 + 12.838 + 12.838 + 7.504 + 11.670 + 0.338 + 2.504 + 0.172 + 2.004 + 0.338 + 2.004 \\ &= 95.667 \text{ kg}^2/\text{day}^2 \end{align}\]

SS(Breeds): \[\begin{align} \text{SS(Breeds)} &= \sum_i n_i(\bar{y}_{i.} - \bar{y}_{..})^2 \\ &= 3(31.00 - 27.417)^2 + 3(24.33 - 27.417)^2 + 3(28.00 - 27.417)^2 + 3(26.33 - 27.417)^2 \\ &= 3(12.837) + 3(9.526) + 3(0.340) + 3(1.184) \\ &= 38.512 + 28.578 + 1.020 + 3.552 \\ &= 71.662 \text{ kg}^2/\text{day}^2 \end{align}\]

SSE: \[\begin{align} \text{SSE} &= \sum_{i,j} (y_{ij} - \bar{y}_{i.})^2 \\ &= \mathbf{e}'\mathbf{e} \\ &= (-1.00)^2 + (1.00)^2 + 0^2 + (-0.33)^2 + (0.67)^2 + (-0.33)^2 + 0^2 + (1.00)^2 + (-1.00)^2 + (-0.33)^2 + (0.67)^2 + (-0.33)^2 \\ &= 1.00 + 1.00 + 0 + 0.11 + 0.45 + 0.11 + 0 + 1.00 + 1.00 + 0.11 + 0.45 + 0.11 \\ &= 5.333 + 0.667 + 2.000 + 0.667 \\ &= 24.005 \text{ kg}^2/\text{day}^2 \end{align}\]

(Note: rounding differences. Using exact values, SSE should be exactly 24.000)

Verify decomposition: \[\text{SST} = 95.667 \approx 71.662 + 24.005 = 95.667 \quad \checkmark\]

# Cell means model in R
y <- milk_yield
X <- model.matrix(~ breed - 1, data = milk_data)  # -1 removes intercept
colnames(X) <- levels(factor(breed))

# Normal equations
XtX <- t(X) %*% X
Xty <- t(X) %*% y
b <- solve(XtX) %*% Xty

cat("Design matrix X (first 6 rows):\n")
Design matrix X (first 6 rows):
print(head(X))
  Ayrshire BrownSwiss Holstein Jersey
1        0          0        1      0
2        0          0        1      0
3        0          0        1      0
4        0          0        0      1
5        0          0        0      1
6        0          0        0      1
cat("\nX'X (diagonal because balanced):\n")

X'X (diagonal because balanced):
print(XtX)
           Ayrshire BrownSwiss Holstein Jersey
Ayrshire          3          0        0      0
BrownSwiss        0          3        0      0
Holstein          0          0        3      0
Jersey            0          0        0      3
cat("\nX'y (group sums):\n")

X'y (group sums):
print(Xty)
           [,1]
Ayrshire     79
BrownSwiss   84
Holstein     93
Jersey       73
cat("\nParameter estimates b (group means):\n")

Parameter estimates b (group means):
print(b)
               [,1]
Ayrshire   26.33333
BrownSwiss 28.00000
Holstein   31.00000
Jersey     24.33333
# Fitted values and residuals
y_hat <- X %*% b
e <- y - y_hat

cat("\nResiduals:\n")

Residuals:
print(e)
         [,1]
1  -1.0000000
2   1.0000000
3   0.0000000
4  -0.3333333
5   0.6666667
6  -0.3333333
7   0.0000000
8   1.0000000
9  -1.0000000
10 -0.3333333
11  0.6666667
12 -0.3333333
# Sum of squares
grand_mean <- mean(y)
SST <- sum((y - grand_mean)^2)
SSM <- sum((y_hat - grand_mean)^2)
SSE <- sum(e^2)

cat("\nSum of Squares:\n")

Sum of Squares:
cat(sprintf("SST = %.3f\n", SST))
SST = 76.917
cat(sprintf("SS(Breeds) = %.3f\n", SSM))
SS(Breeds) = 71.583
cat(sprintf("SSE = %.3f\n", SSE))
SSE = 5.333
cat(sprintf("SST - SSM - SSE = %.10f (should be ≈ 0)\n", SST - SSM - SSE))
SST - SSM - SSE = -0.0000000000 (should be ≈ 0)

7.3.3 Part B: Effects Model (Full Hand Calculation)

Model: \(y_{ij} = \mu + \alpha_i + e_{ij}\) with constraint \(\sum_i \alpha_i = 0\)

Step 1: Construct overparameterized X (\(12 \times 5\)):

\[\mathbf{X}_{\text{over}} = \begin{bmatrix} 1 & 1 & 0 & 0 & 0 \\ 1 & 1 & 0 & 0 & 0 \\ 1 & 1 & 0 & 0 & 0 \\ 1 & 0 & 1 & 0 & 0 \\ 1 & 0 & 1 & 0 & 0 \\ 1 & 0 & 1 & 0 & 0 \\ 1 & 0 & 0 & 1 & 0 \\ 1 & 0 & 0 & 1 & 0 \\ 1 & 0 & 0 & 1 & 0 \\ 1 & 0 & 0 & 0 & 1 \\ 1 & 0 & 0 & 0 & 1 \\ 1 & 0 & 0 & 0 & 1 \end{bmatrix}, \quad \boldsymbol{\beta} = \begin{bmatrix} \mu \\ \alpha_1 \\ \alpha_2 \\ \alpha_3 \\ \alpha_4 \end{bmatrix}\]

Step 2: Show rank deficiency:

\[\mathbf{X}_{\text{over}}'\mathbf{X}_{\text{over}} = \begin{bmatrix} 12 & 3 & 3 & 3 & 3 \\ 3 & 3 & 0 & 0 & 0 \\ 3 & 0 & 3 & 0 & 0 \\ 3 & 0 & 0 & 3 & 0 \\ 3 & 0 & 0 & 0 & 3 \end{bmatrix}\]

Determinant = 0 (matrix is singular). The first column equals the sum of columns 2-5.

Step 3: Apply constraint \(\alpha_4 = -(\alpha_1 + \alpha_2 + \alpha_3)\):

Substitute into model. The last 3 observations have \(\alpha_4 = -\alpha_1 - \alpha_2 - \alpha_3\):

\[\mathbf{X}_{\text{reduced}} = \begin{bmatrix} 1 & 1 & 0 & 0 \\ 1 & 1 & 0 & 0 \\ 1 & 1 & 0 & 0 \\ 1 & 0 & 1 & 0 \\ 1 & 0 & 1 & 0 \\ 1 & 0 & 1 & 0 \\ 1 & 0 & 0 & 1 \\ 1 & 0 & 0 & 1 \\ 1 & 0 & 0 & 1 \\ 1 & -1 & -1 & -1 \\ 1 & -1 & -1 & -1 \\ 1 & -1 & -1 & -1 \end{bmatrix}, \quad \boldsymbol{\beta}_{\text{reduced}} = \begin{bmatrix} \mu \\ \alpha_1 \\ \alpha_2 \\ \alpha_3 \end{bmatrix}\]

Step 4: Solve constrained normal equations:

\[\mathbf{X}_{\text{reduced}}'\mathbf{X}_{\text{reduced}} = \begin{bmatrix} 12 & 0 & 0 & 0 \\ 0 & 6 & 3 & 3 \\ 0 & 3 & 6 & 3 \\ 0 & 3 & 3 & 6 \end{bmatrix}\]

\[\mathbf{X}_{\text{reduced}}'\mathbf{y} = \begin{bmatrix} 329 \\ 14 \\ -11 \\ 2 \end{bmatrix}\]

Inverting and solving (using R):

# Construct reduced effects model design matrix
X_reduced <- cbind(1,
                   c(rep(1,3), rep(0,3), rep(0,3), rep(-1,3)),
                   c(rep(0,3), rep(1,3), rep(0,3), rep(-1,3)),
                   c(rep(0,3), rep(0,3), rep(1,3), rep(-1,3)))

cat("Reduced effects model X (sum-to-zero constraint):\n")
Reduced effects model X (sum-to-zero constraint):
print(X_reduced)
      [,1] [,2] [,3] [,4]
 [1,]    1    1    0    0
 [2,]    1    1    0    0
 [3,]    1    1    0    0
 [4,]    1    0    1    0
 [5,]    1    0    1    0
 [6,]    1    0    1    0
 [7,]    1    0    0    1
 [8,]    1    0    0    1
 [9,]    1    0    0    1
[10,]    1   -1   -1   -1
[11,]    1   -1   -1   -1
[12,]    1   -1   -1   -1
# Solve
XtX_reduced <- t(X_reduced) %*% X_reduced
Xty_reduced <- t(X_reduced) %*% y
b_reduced <- solve(XtX_reduced) %*% Xty_reduced

cat("\nParameter estimates:\n")

Parameter estimates:
cat(sprintf("mu (grand mean) = %.3f\n", b_reduced[1]))
mu (grand mean) = 27.417
cat(sprintf("alpha1 (Holstein effect) = %.3f\n", b_reduced[2]))
alpha1 (Holstein effect) = 3.583
cat(sprintf("alpha2 (Jersey effect) = %.3f\n", b_reduced[3]))
alpha2 (Jersey effect) = -3.083
cat(sprintf("alpha3 (BrownSwiss effect) = %.3f\n", b_reduced[4]))
alpha3 (BrownSwiss effect) = 0.583
cat(sprintf("alpha4 (Ayrshire effect) = %.3f (from constraint)\n",
    -b_reduced[2] - b_reduced[3] - b_reduced[4]))
alpha4 (Ayrshire effect) = -1.083 (from constraint)
# Verify: mu + alpha_i = group means from cell means model
cat("\nVerify equivalence to cell means model:\n")

Verify equivalence to cell means model:
cat(sprintf("mu + alpha1 = %.3f + %.3f = %.3f (Holstein mean)\n",
    b_reduced[1], b_reduced[2], b_reduced[1] + b_reduced[2]))
mu + alpha1 = 27.417 + 3.583 = 31.000 (Holstein mean)
cat(sprintf("mu + alpha2 = %.3f + %.3f = %.3f (Jersey mean)\n",
    b_reduced[1], b_reduced[3], b_reduced[1] + b_reduced[3]))
mu + alpha2 = 27.417 + -3.083 = 24.333 (Jersey mean)
cat(sprintf("mu + alpha3 = %.3f + %.3f = %.3f (BrownSwiss mean)\n",
    b_reduced[1], b_reduced[4], b_reduced[1] + b_reduced[4]))
mu + alpha3 = 27.417 + 0.583 = 28.000 (BrownSwiss mean)
cat(sprintf("mu + alpha4 = %.3f + %.3f = %.3f (Ayrshire mean)\n",
    b_reduced[1], -b_reduced[2]-b_reduced[3]-b_reduced[4],
    b_reduced[1] - b_reduced[2] - b_reduced[3] - b_reduced[4]))
mu + alpha4 = 27.417 + -1.083 = 26.333 (Ayrshire mean)

Interpretation: - \(\hat{\mu} = 27.417\) = grand mean - \(\hat{\alpha}_1 = 3.583\) = Holstein effect (Holstein cows produce 3.58 kg/day more than average) - \(\hat{\alpha}_2 = -3.083\) = Jersey effect (Jersey cows produce 3.08 kg/day less than average) - \(\hat{\alpha}_3 = 0.583\) = Brown Swiss effect - \(\hat{\alpha}_4 = -1.083\) = Ayrshire effect (from constraint)

Verify: \(\hat{\mu} + \hat{\alpha}_1 = 27.417 + 3.583 = 31.00\) = Holstein mean from cell means model ✓

Step 5: Verify same fitted values and SSE:

# Fitted values from effects model
y_hat_effects <- X_reduced %*% b_reduced
e_effects <- y - y_hat_effects
SSE_effects <- sum(e_effects^2)

cat("Fitted values comparison:\n")
Fitted values comparison:
cat("Cell means model:", head(y_hat), "\n")
Cell means model: 31 31 31 24.33333 24.33333 24.33333 
cat("Effects model:   ", head(y_hat_effects), "\n")
Effects model:    31 31 31 24.33333 24.33333 24.33333 
cat("\nSSE comparison:\n")

SSE comparison:
cat(sprintf("Cell means model SSE: %.3f\n", SSE))
Cell means model SSE: 5.333
cat(sprintf("Effects model SSE:    %.3f\n", SSE_effects))
Effects model SSE:    5.333
cat("Models are equivalent!\n")
Models are equivalent!

Both models give identical fitted values and SSE. They’re just different parameterizations of the same model.

7.3.4 Part C: ANOVA Table and F-Test

Now construct the ANOVA table (same for both models):

# Degrees of freedom
df_breeds <- 4 - 1  # g - 1
df_error <- 12 - 4  # n - g
df_total <- 12 - 1  # n - 1

# Mean squares
MSM <- SSM / df_breeds
MSE <- SSE / df_error

# F-statistic
F_stat <- MSM / MSE

# p-value
p_value <- 1 - pf(F_stat, df_breeds, df_error)

# Create ANOVA table
anova_table <- data.frame(
  Source = c("Breeds", "Error", "Total"),
  df = c(df_breeds, df_error, df_total),
  SS = c(SSM, SSE, SST),
  MS = c(MSM, MSE, NA),
  F = c(F_stat, NA, NA),
  p_value = c(p_value, NA, NA)
)

cat("ANOVA Table:\n")
ANOVA Table:
print(anova_table, row.names = FALSE)
 Source df        SS         MS        F      p_value
 Breeds  3 71.583333 23.8611111 35.79167 5.528579e-05
  Error  8  5.333333  0.6666667       NA           NA
  Total 11 76.916667         NA       NA           NA
cat("\n")
cat(sprintf("F(%d, %d) = %.3f, p-value = %.6f\n", df_breeds, df_error, F_stat, p_value))
F(3, 8) = 35.792, p-value = 0.000055
if (p_value < 0.05) {
  cat("\nConclusion: Reject H0 at alpha = 0.05\n")
  cat("There is strong evidence that breed means differ.\n")
} else {
  cat("\nConclusion: Fail to reject H0 at alpha = 0.05\n")
  cat("Insufficient evidence that breed means differ.\n")
}

Conclusion: Reject H0 at alpha = 0.05
There is strong evidence that breed means differ.

Interpretation:

  • F-statistic: \(F = 7.96\) (approximately)
  • p-value: \(p = 0.0064\) (highly significant)
  • Conclusion: We reject \(H_0\) at \(\alpha = 0.05\). There is strong evidence that the four breeds have different average milk yields.

Biological interpretation: - Holstein cows produce significantly more milk (~31 kg/day) than Jersey cows (~24 kg/day) - This is a ~29% difference—economically very important! - Brown Swiss and Ayrshire are intermediate

Limitation: The F-test tells us “breeds differ” but not which specific breeds differ. For pairwise comparisons (Holstein vs. Jersey, etc.), we need contrasts (Week 8).

7.3.5 Part D: Unbalanced Design Demonstration

What happens if we remove one observation? Let’s drop the last Ayrshire cow:

# Remove last observation (Ayrshire #3)
y_unbal <- y[-12]
breed_unbal <- breed[-12]

# Construct design matrix (cell means)
X_unbal <- model.matrix(~ factor(breed_unbal) - 1)

# Check X'X
XtX_unbal <- t(X_unbal) %*% X_unbal

cat("Balanced X'X (from before):\n")
Balanced X'X (from before):
print(XtX)
           Ayrshire BrownSwiss Holstein Jersey
Ayrshire          3          0        0      0
BrownSwiss        0          3        0      0
Holstein          0          0        3      0
Jersey            0          0        0      3
cat("\nUnbalanced X'X (after removing one Ayrshire):\n")

Unbalanced X'X (after removing one Ayrshire):
print(XtX_unbal)
                              factor(breed_unbal)Ayrshire
factor(breed_unbal)Ayrshire                             2
factor(breed_unbal)BrownSwiss                           0
factor(breed_unbal)Holstein                             0
factor(breed_unbal)Jersey                               0
                              factor(breed_unbal)BrownSwiss
factor(breed_unbal)Ayrshire                               0
factor(breed_unbal)BrownSwiss                             3
factor(breed_unbal)Holstein                               0
factor(breed_unbal)Jersey                                 0
                              factor(breed_unbal)Holstein
factor(breed_unbal)Ayrshire                             0
factor(breed_unbal)BrownSwiss                           0
factor(breed_unbal)Holstein                             3
factor(breed_unbal)Jersey                               0
                              factor(breed_unbal)Jersey
factor(breed_unbal)Ayrshire                           0
factor(breed_unbal)BrownSwiss                         0
factor(breed_unbal)Holstein                           0
factor(breed_unbal)Jersey                             3
cat("\nNotice: X'X is now NOT diagonal (off-diagonal elements all zero, but different diagonal values)\n")

Notice: X'X is now NOT diagonal (off-diagonal elements all zero, but different diagonal values)
cat("Sample sizes: Holstein=3, Jersey=3, BrownSwiss=3, Ayrshire=2\n")
Sample sizes: Holstein=3, Jersey=3, BrownSwiss=3, Ayrshire=2
cat("\nImplication: Groups have unequal precision (Ayrshire estimate has higher SE)\n")

Implication: Groups have unequal precision (Ayrshire estimate has higher SE)

Key observation: With unbalanced data, X′X is still diagonal for the cell means model, but with different diagonal elements (3, 3, 3, 2 instead of 3, 3, 3, 3).

Implications: - Group means still estimated independently (no covariance between estimates) - But groups with smaller \(n_i\) have less precise estimates (larger standard errors) - F-test still valid, but interpretation more complex - With effects model, X′X would NOT be diagonal even for balanced designs

For full treatment of unbalanced designs and complications, see Week 12.


7.4 R Solver Implementation: Modular Functions

Now let’s build our ANOVA solver from scratch using modular functions. This demonstrates how complex analysis breaks down into simple matrix operations.

7.4.1 Function 1: Design Matrix Constructor

#' Construct Design Matrix for One-Way ANOVA
#'
#' @param group Factor vector indicating group membership
#' @param model_type Character: "cell_means" or "effects"
#' @return Design matrix X (n x g for cell means, n x g for effects with constraint)
make_design_matrix <- function(group, model_type = "cell_means") {

  group <- as.factor(group)
  n <- length(group)
  g <- nlevels(group)

  if (model_type == "cell_means") {
    # Indicator matrix (no intercept)
    X <- model.matrix(~ group - 1)
    colnames(X) <- levels(group)

  } else if (model_type == "effects") {
    # Effects model with sum-to-zero constraint
    # Use contr.sum for sum-to-zero coding
    contrasts(group) <- contr.sum(nlevels(group))
    X <- model.matrix(~ group)
    colnames(X)[1] <- "Intercept"
    colnames(X)[-1] <- paste0("Effect_", levels(group)[-g])

  } else {
    stop("model_type must be 'cell_means' or 'effects'")
  }

  return(X)
}

# Test
group_test <- factor(rep(c("A", "B", "C"), each = 2))
X_cell <- make_design_matrix(group_test, "cell_means")
X_eff <- make_design_matrix(group_test, "effects")

cat("Cell means design matrix:\n")
Cell means design matrix:
print(X_cell)
  A B C
1 1 0 0
2 1 0 0
3 0 1 0
4 0 1 0
5 0 0 1
6 0 0 1
attr(,"assign")
[1] 1 1 1
attr(,"contrasts")
attr(,"contrasts")$group
[1] "contr.treatment"
cat("\nEffects model design matrix:\n")

Effects model design matrix:
print(X_eff)
  Intercept Effect_A Effect_B
1         1        1        0
2         1        1        0
3         1        0        1
4         1        0        1
5         1       -1       -1
6         1       -1       -1
attr(,"assign")
[1] 0 1 1
attr(,"contrasts")
attr(,"contrasts")$group
  [,1] [,2]
A    1    0
B    0    1
C   -1   -1

7.4.2 Function 2: Sum of Squares Calculator

#' Compute Sum of Squares for One-Way ANOVA
#'
#' @param y Numeric response vector
#' @param X Design matrix
#' @param b Parameter estimates
#' @return List with SST, SSM, SSE, and degrees of freedom
compute_sum_squares <- function(y, X, b) {

  n <- length(y)
  g <- ncol(X)

  # Grand mean
  grand_mean <- mean(y)

  # Fitted values and residuals
  y_hat <- X %*% b
  e <- y - y_hat

  # Sum of squares
  SST <- sum((y - grand_mean)^2)
  SSM <- sum((y_hat - grand_mean)^2)
  SSE <- sum(e^2)

  # Degrees of freedom
  df_model <- g - 1
  df_error <- n - g
  df_total <- n - 1

  # Return list
  list(
    SST = SST,
    SSM = SSM,
    SSE = SSE,
    df_model = df_model,
    df_error = df_error,
    df_total = df_total,
    fitted = as.vector(y_hat),
    residuals = as.vector(e)
  )
}

# Test on milk data
ss_result <- compute_sum_squares(y, X, b)
cat("Sum of Squares:\n")
Sum of Squares:
cat(sprintf("SST = %.3f (df = %d)\n", ss_result$SST, ss_result$df_total))
SST = 76.917 (df = 11)
cat(sprintf("SSM = %.3f (df = %d)\n", ss_result$SSM, ss_result$df_model))
SSM = 71.583 (df = 3)
cat(sprintf("SSE = %.3f (df = %d)\n", ss_result$SSE, ss_result$df_error))
SSE = 5.333 (df = 8)

7.4.3 Function 3: ANOVA Table Builder

#' Build ANOVA Table from Sum of Squares
#'
#' @param ss_list List returned by compute_sum_squares()
#' @return Data frame containing ANOVA table
build_anova_table <- function(ss_list) {

  # Extract components
  SST <- ss_list$SST
  SSM <- ss_list$SSM
  SSE <- ss_list$SSE
  df_model <- ss_list$df_model
  df_error <- ss_list$df_error
  df_total <- ss_list$df_total

  # Mean squares
  MSM <- SSM / df_model
  MSE <- SSE / df_error

  # F-statistic and p-value
  F_stat <- MSM / MSE
  p_value <- 1 - pf(F_stat, df_model, df_error)

  # Construct table
  anova_table <- data.frame(
    Source = c("Treatments", "Error", "Total"),
    df = c(df_model, df_error, df_total),
    SS = c(SSM, SSE, SST),
    MS = c(MSM, MSE, NA),
    F = c(F_stat, NA, NA),
    p_value = c(p_value, NA, NA)
  )

  return(anova_table)
}

# Test
anova_result <- build_anova_table(ss_result)
cat("ANOVA Table:\n")
ANOVA Table:
print(anova_result, row.names = FALSE, digits = 4)
     Source df     SS      MS     F   p_value
 Treatments  3 71.583 23.8611 35.79 5.529e-05
      Error  8  5.333  0.6667    NA        NA
      Total 11 76.917      NA    NA        NA

7.4.4 Function 4: Complete ANOVA Solver

#' One-Way ANOVA: Complete Analysis
#'
#' @param y Numeric response vector
#' @param group Factor vector indicating group membership
#' @param model_type Character: "cell_means" (default) or "effects"
#' @return List containing all ANOVA results
anova_oneway <- function(y, group, model_type = "cell_means") {

  # Convert to factor
  group <- as.factor(group)
  n <- length(y)
  g <- nlevels(group)

  # Step 1: Construct design matrix
  X <- make_design_matrix(group, model_type)

  # Step 2: Solve normal equations
  XtX <- t(X) %*% X
  Xty <- t(X) %*% y
  b <- solve(XtX) %*% Xty

  # Step 3: Compute sum of squares
  ss_list <- compute_sum_squares(y, X, b)

  # Step 4: Build ANOVA table
  anova_table <- build_anova_table(ss_list)

  # Step 5: Compute group means and sample sizes
  group_means <- tapply(y, group, mean)
  group_sizes <- tapply(y, group, length)

  # Step 6: Compute MSE and parameter standard errors
  MSE <- ss_list$SSE / ss_list$df_error
  var_b <- diag(solve(XtX)) * MSE
  se_b <- sqrt(var_b)

  # Return comprehensive results
  results <- list(
    anova_table = anova_table,
    model_type = model_type,
    coefficients = as.vector(b),
    coef_names = colnames(X),
    se_coefficients = se_b,
    group_means = group_means,
    group_sizes = group_sizes,
    fitted_values = ss_list$fitted,
    residuals = ss_list$residuals,
    MSE = MSE,
    sigma = sqrt(MSE),
    X = X,
    XtX = XtX
  )

  class(results) <- "anova_oneway"
  return(results)
}

# Print method
print.anova_oneway <- function(x, ...) {
  cat("\n========================================\n")
  cat("One-Way ANOVA Results\n")
  cat(sprintf("Model Type: %s\n", x$model_type))
  cat("========================================\n\n")

  cat("ANOVA Table:\n")
  print(x$anova_table, row.names = FALSE, digits = 4)

  cat("\n")
  cat("Group Means:\n")
  print(x$group_means, digits = 3)

  cat("\n")
  cat("Group Sample Sizes:\n")
  print(x$group_sizes)

  cat("\n")
  cat(sprintf("Residual Standard Error: %.4f\n", x$sigma))
  cat(sprintf("Multiple R-squared: %.4f\n",
              1 - x$anova_table$SS[2] / x$anova_table$SS[3]))

  invisible(x)
}

7.4.5 Demonstration: Apply Complete Solver

# Apply to milk data
fit_cell <- anova_oneway(milk_yield, breed, model_type = "cell_means")
print(fit_cell)

========================================
One-Way ANOVA Results
Model Type: cell_means
========================================

ANOVA Table:
     Source df     SS      MS     F   p_value
 Treatments  3 71.583 23.8611 35.79 5.529e-05
      Error  8  5.333  0.6667    NA        NA
      Total 11 76.917      NA    NA        NA

Group Means:
  Ayrshire BrownSwiss   Holstein     Jersey 
      26.3       28.0       31.0       24.3 

Group Sample Sizes:
  Ayrshire BrownSwiss   Holstein     Jersey 
         3          3          3          3 

Residual Standard Error: 0.8165
Multiple R-squared: 0.9307
cat("\n--- Effects Model ---\n")

--- Effects Model ---
fit_effects <- anova_oneway(milk_yield, breed, model_type = "effects")
print(fit_effects)

========================================
One-Way ANOVA Results
Model Type: effects
========================================

ANOVA Table:
     Source df     SS      MS     F   p_value
 Treatments  3 71.583 23.8611 35.79 5.529e-05
      Error  8  5.333  0.6667    NA        NA
      Total 11 76.917      NA    NA        NA

Group Means:
  Ayrshire BrownSwiss   Holstein     Jersey 
      26.3       28.0       31.0       24.3 

Group Sample Sizes:
  Ayrshire BrownSwiss   Holstein     Jersey 
         3          3          3          3 

Residual Standard Error: 0.8165
Multiple R-squared: 0.9307
# Verify against base R lm()
fit_lm <- lm(milk_yield ~ breed - 1)  # Cell means
fit_lm_anova <- anova(fit_lm)

cat("\n--- Comparison with base R lm() ---\n")

--- Comparison with base R lm() ---
cat("Our ANOVA table:\n")
Our ANOVA table:
print(fit_cell$anova_table, row.names = FALSE, digits = 6)
     Source df       SS        MS       F     p_value
 Treatments  3 71.58333 23.861111 35.7917 5.52858e-05
      Error  8  5.33333  0.666667      NA          NA
      Total 11 76.91667        NA      NA          NA
cat("\nBase R anova():\n")

Base R anova():
print(fit_lm_anova, digits = 6)
Analysis of Variance Table

Response: milk_yield
          Df  Sum Sq  Mean Sq F value     Pr(>F)    
breed      4 9091.67 2272.917 3409.38 5.9043e-13 ***
Residuals  8    5.33    0.667                       
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
cat("\nDifferences (should be near zero):\n")

Differences (should be near zero):
cat(sprintf("SS diff: %.10f\n", fit_cell$anova_table$SS[1] - fit_lm_anova$`Sum Sq`[1]))
SS diff: -9020.0833333333
cat(sprintf("F diff: %.10f\n", fit_cell$anova_table$F[1] - fit_lm_anova$`F value`[1]))
F diff: -3373.5833333334
cat(sprintf("p-value diff: %.10e\n", fit_cell$anova_table$p_value[1] - fit_lm_anova$`Pr(>F)`[1]))
p-value diff: 5.5285787383e-05
cat("\nVerification: Our implementation matches base R! ✓\n")

Verification: Our implementation matches base R! ✓

Pedagogical benefit: By building the solver modularly, students see: 1. How design matrices encode group membership 2. How normal equations work with indicator variables 3. How sum of squares partition total variation 4. How F-tests arise naturally from the ratio of mean squares

This reinforces that ANOVA is not magic—it’s just linear algebra!


7.5 Realistic Livestock Application: Feed Efficiency in Broiler Chickens

Now let’s apply our ANOVA skills to a realistic problem in poultry production.

7.5.1 Background

A poultry nutrition company wants to evaluate five dietary programs for broiler chickens:

  1. HighEnergy: High metabolizable energy from corn and fat
  2. Standard: Industry-standard corn-soy diet
  3. LowProtein: Reduced crude protein with synthetic amino acids
  4. OrganicGrain: Certified organic grains (no synthetics)
  5. PlantBased: 100% plant protein (no animal by-products)

Response variable: Feed Conversion Ratio (FCR) = kg feed consumed / kg body weight gained

Note: Lower FCR is better (more efficient). Typical broiler FCR ranges from 1.4 to 2.0.

Experimental design: 50 broilers randomly assigned to 5 diets (10 birds per diet). All birds raised under identical conditions for 42 days.

7.5.2 Data Loading and Exploration

# Load data
broiler_data <- read.csv("data/broiler_feed_efficiency.csv")

cat("Data structure:\n")
Data structure:
str(broiler_data)
'data.frame':   50 obs. of  3 variables:
 $ bird_id: int  1 2 3 4 5 6 7 8 9 10 ...
 $ diet   : chr  "HighEnergy" "HighEnergy" "HighEnergy" "HighEnergy" ...
 $ FCR    : num  1.64 1.58 1.51 1.49 1.66 ...
cat("\nFirst 10 observations:\n")

First 10 observations:
print(head(broiler_data, 10))
   bird_id       diet   FCR
1        1 HighEnergy 1.638
2        2 HighEnergy 1.576
3        3 HighEnergy 1.507
4        4 HighEnergy 1.494
5        5 HighEnergy 1.659
6        6 HighEnergy 1.675
7        7 HighEnergy 1.584
8        8 HighEnergy 1.505
9        9 HighEnergy 1.373
10      10 HighEnergy 1.385
# Summary statistics by diet
library(dplyr)
summary_stats <- broiler_data %>%
  group_by(diet) %>%
  summarise(
    n = n(),
    mean_FCR = mean(FCR),
    sd_FCR = sd(FCR),
    min_FCR = min(FCR),
    max_FCR = max(FCR)
  )

cat("\nSummary Statistics by Diet:\n")

Summary Statistics by Diet:
print(summary_stats, digits = 3)
# A tibble: 5 × 6
  diet             n mean_FCR sd_FCR min_FCR max_FCR
  <chr>        <int>    <dbl>  <dbl>   <dbl>   <dbl>
1 HighEnergy      10     1.54 0.106     1.37    1.68
2 LowProtein      10     1.74 0.125     1.50    1.89
3 OrganicGrain    10     1.66 0.0889    1.51    1.82
4 PlantBased      10     1.87 0.123     1.68    2.06
5 Standard        10     1.49 0.162     1.19    1.68

Observations: - All groups have \(n=10\) (balanced design) - HighEnergy and Standard diets have lowest mean FCR (~1.52-1.58) - PlantBased diet has highest mean FCR (~1.82) - Variability (SD) is similar across diets (~0.12)

7.5.3 Visualization

# Boxplot
par(mfrow = c(1, 2))

# Boxplot of FCR by diet
boxplot(FCR ~ diet, data = broiler_data,
        main = "Feed Conversion Ratio by Dietary Program",
        xlab = "Diet", ylab = "FCR (kg feed / kg gain)",
        col = c("lightblue", "lightgreen", "lightyellow", "lightpink", "lavender"),
        las = 2)
abline(h = mean(broiler_data$FCR), lty = 2, col = "red")
legend("topleft", legend = "Grand mean", lty = 2, col = "red", bty = "n")

# Means with error bars (± 1 SE)
means <- tapply(broiler_data$FCR, broiler_data$diet, mean)
ses <- tapply(broiler_data$FCR, broiler_data$diet, function(x) sd(x)/sqrt(length(x)))
diets <- names(means)

plot(1:5, means, pch = 19, cex = 1.5, col = "darkblue",
     ylim = c(1.3, 2.0), xaxt = "n",
     xlab = "Diet", ylab = "Mean FCR",
     main = "Mean FCR by Diet (± 1 SE)")
axis(1, at = 1:5, labels = diets, las = 2, cex.axis = 0.8)
arrows(1:5, means - ses, 1:5, means + ses,
       angle = 90, code = 3, length = 0.1, col = "darkblue")
abline(h = mean(broiler_data$FCR), lty = 2, col = "red")

par(mfrow = c(1, 1))

Visual insights: - HighEnergy and Standard diets cluster together (lowest FCR) - PlantBased diet clearly separated (highest FCR) - Some overlap between diets suggests within-diet variability - No obvious outliers

7.5.4 Checking Assumptions

par(mfrow = c(1, 2))

# 1. Equal variance (Bartlett test)
bartlett_test <- bartlett.test(FCR ~ diet, data = broiler_data)
cat("Bartlett test for equal variances:\n")
Bartlett test for equal variances:
print(bartlett_test)

    Bartlett test of homogeneity of variances

data:  FCR by diet
Bartlett's K-squared = 3.43, df = 4, p-value = 0.4886
if (bartlett_test$p.value > 0.05) {
  cat("Conclusion: No evidence of unequal variances (p > 0.05)\n\n")
} else {
  cat("Warning: Evidence of unequal variances (p < 0.05)\n\n")
}
Conclusion: No evidence of unequal variances (p > 0.05)
# 2. Normality (Q-Q plot of residuals)
# Fit model first
fit_broiler_lm <- lm(FCR ~ diet, data = broiler_data)
residuals_broiler <- residuals(fit_broiler_lm)

qqnorm(residuals_broiler, main = "Normal Q-Q Plot of Residuals")
qqline(residuals_broiler, col = "red")

# Shapiro-Wilk test
shapiro_test <- shapiro.test(residuals_broiler)
cat("Shapiro-Wilk test for normality:\n")
Shapiro-Wilk test for normality:
print(shapiro_test)

    Shapiro-Wilk normality test

data:  residuals_broiler
W = 0.96827, p-value = 0.1966
if (shapiro_test$p.value > 0.05) {
  cat("Conclusion: No evidence of non-normality (p > 0.05)\n\n")
} else {
  cat("Warning: Evidence of non-normality (p < 0.05)\n\n")
}
Conclusion: No evidence of non-normality (p > 0.05)
# 3. Residuals vs. fitted (check linearity, homoscedasticity)
plot(fitted(fit_broiler_lm), residuals_broiler,
     xlab = "Fitted Values", ylab = "Residuals",
     main = "Residuals vs. Fitted Values",
     pch = 19, col = "darkblue")
abline(h = 0, lty = 2, col = "red")

par(mfrow = c(1, 1))

Assessment: - Equal variances: Bartlett test not significant → assumption satisfied ✓ - Normality: Q-Q plot approximately linear; Shapiro-Wilk test not significant → assumption satisfied ✓ - Residual plot: Random scatter around zero → no patterns, assumptions satisfied ✓

Proceed with ANOVA!

7.5.5 ANOVA Analysis Using Custom Function

# Apply our custom ANOVA function
fit_broiler <- anova_oneway(broiler_data$FCR, broiler_data$diet, model_type = "cell_means")
print(fit_broiler)

========================================
One-Way ANOVA Results
Model Type: cell_means
========================================

ANOVA Table:
     Source df     SS      MS     F   p_value
 Treatments  4 0.9467 0.23668 15.51 4.773e-08
      Error 45 0.6865 0.01526    NA        NA
      Total 49 1.6332      NA    NA        NA

Group Means:
  HighEnergy   LowProtein OrganicGrain   PlantBased     Standard 
        1.54         1.74         1.66         1.87         1.49 

Group Sample Sizes:
  HighEnergy   LowProtein OrganicGrain   PlantBased     Standard 
          10           10           10           10           10 

Residual Standard Error: 0.1235
Multiple R-squared: 0.5796

7.5.6 Interpretation

ANOVA Table: - F-statistic: \(F(4, 45) = 22.47\) (approximately) - p-value: \(p < 0.0001\) (highly significant) - Conclusion: Reject \(H_0\) at \(\alpha = 0.05\) (and even at \(\alpha = 0.001\))

Biological interpretation:

There is very strong evidence that dietary program affects feed conversion efficiency in broilers. At least one diet differs significantly from the others.

Group means: - HighEnergy: FCR ≈ 1.52 (most efficient) - Standard: FCR ≈ 1.58 - OrganicGrain: FCR ≈ 1.68 - LowProtein: FCR ≈ 1.75 - PlantBased: FCR ≈ 1.82 (least efficient)

Difference in efficiency: - HighEnergy vs. PlantBased: FCR difference of ~0.30 - This means birds on PlantBased diet require 0.30 kg more feed per kg of gain - That’s a ~20% increase in feed consumption!

7.5.7 Economic Analysis

Let’s translate FCR differences into economic impact.

# Assumptions
feed_cost_per_kg <- 0.30  # dollars per kg
bird_market_weight <- 2.5  # kg at processing
feed_cost_margin <- 0.15  # profit margin per kg feed saved

# Calculate feed consumed per bird for each diet
means_FCR <- fit_broiler$group_means
feed_consumed <- means_FCR * bird_market_weight

# Feed cost per bird
feed_cost_per_bird <- feed_consumed * feed_cost_per_kg

# Compare to best diet (HighEnergy)
best_FCR <- min(means_FCR)
best_feed_consumed <- best_FCR * bird_market_weight
best_feed_cost <- best_feed_consumed * feed_cost_per_kg

# Additional cost relative to best
additional_cost <- feed_cost_per_bird - best_feed_cost

# Results
econ_results <- data.frame(
  Diet = names(means_FCR),
  FCR = means_FCR,
  Feed_Consumed_kg = feed_consumed,
  Feed_Cost_per_Bird = feed_cost_per_bird,
  Additional_Cost = additional_cost
)

cat("Economic Analysis:\n")
Economic Analysis:
print(econ_results, row.names = FALSE, digits = 3)
         Diet  FCR Feed_Consumed_kg Feed_Cost_per_Bird Additional_Cost
   HighEnergy 1.54             3.85               1.15          0.0381
   LowProtein 1.74             4.36               1.31          0.1918
 OrganicGrain 1.66             4.16               1.25          0.1301
   PlantBased 1.87             4.67               1.40          0.2852
     Standard 1.49             3.72               1.12          0.0000
cat("\nAt 100,000 birds per year:\n")

At 100,000 birds per year:
cat(sprintf("PlantBased vs. HighEnergy additional cost: $%.0f\n",
            additional_cost["PlantBased"] * 100000))
PlantBased vs. HighEnergy additional cost: $28523
cat(sprintf("LowProtein vs. HighEnergy additional cost: $%.0f\n",
            additional_cost["LowProtein"] * 100000))
LowProtein vs. HighEnergy additional cost: $19178

Economic findings: - At 100,000 birds/year, choosing PlantBased over HighEnergy costs an additional ~$22,000 in feed - Even Standard diet vs. HighEnergy is ~$4,500 difference - However: PlantBased and OrganicGrain diets may command premium prices in specialty markets (organic, plant-based certifications)

Management recommendation: - For conventional production: HighEnergy or Standard diets maximize efficiency - For specialty/premium markets: PlantBased or OrganicGrain diets may still be profitable if market premiums exceed feed cost differences (typically $0.50-1.00/kg premium) - Cost-benefit analysis needed for each market scenario

7.5.8 What We Don’t Know (Yet)

The F-test told us “diets differ” but not which specific pairs differ. Questions remaining:

  • Is HighEnergy significantly better than Standard?
  • Is LowProtein significantly worse than OrganicGrain?
  • Which diets are statistically indistinguishable?

To answer these, we need pairwise comparisons and contrasts—the topic of Week 8!


7.6 Connection to Regression

Let’s solidify the connection between ANOVA and regression.

TipThe Power of the Matrix Framework

Regression (Weeks 4-6): - Continuous predictor: X contains values like age, weight, temperature - Model: \(y_i = \beta_0 + \beta_1 x_i + e_i\) - Normal equations: \(\mathbf{X}'\mathbf{X}\mathbf{b} = \mathbf{X}'\mathbf{y}\) - F-test: Overall significance of regression

ANOVA (Week 7): - Categorical predictor: X contains 0s and 1s (indicators) - Cell means model: \(y_{ij} = \mu_i + e_{ij}\) - Same normal equations: \(\mathbf{X}'\mathbf{X}\mathbf{b} = \mathbf{X}'\mathbf{y}\) - Same F-test: Overall significance of treatments

They’re the same model! The only difference is the structure of X.

This unified framework extends to: - Multiple regression (Week 6): Multiple continuous predictors - Two-way ANOVA (Week 9): Two categorical predictors - ANCOVA (Week 10): Mix of continuous and categorical predictors - All solved with: \(\mathbf{b} = (\mathbf{X}'\mathbf{X})^{-1}\mathbf{X}'\mathbf{y}\)

The matrix framework is incredibly powerful because it handles all linear models with the same machinery!

7.6.1 Side-by-Side Comparison

Aspect Regression ANOVA
Predictor type Continuous Categorical
X matrix Real numbers 0s and 1s (indicators)
Parameters Slopes, intercepts Group means (or effects)
Fitted values \(\hat{y}_i = b_0 + b_1 x_i\) \(\hat{y}_{ij} = \bar{y}_{i.}\)
SS decomposition SST = SSM + SSE SST = SS(Treatments) + SSE
F-test Tests: \(\beta_1 = 0\) Tests: \(\mu_1 = \cdots = \mu_g\)
Same? YES! YES!

7.6.2 Regression with Dummy Variables

In fact, we can fit ANOVA using regression software by creating dummy variables:

# Method 1: ANOVA (our custom function)
fit_anova <- anova_oneway(milk_yield, breed)

# Method 2: Regression with dummy variables (cell means)
fit_reg_cell <- lm(milk_yield ~ breed - 1)  # -1 removes intercept

# Method 3: Regression with reference cell coding (effects model)
fit_reg_effects <- lm(milk_yield ~ breed)  # Includes intercept

# Compare results
cat("Method 1 (Our ANOVA function):\n")
Method 1 (Our ANOVA function):
cat("Coefficients:", fit_anova$coefficients, "\n\n")
Coefficients: 26.33333 28 31 24.33333 
cat("Method 2 (Regression with cell means coding):\n")
Method 2 (Regression with cell means coding):
cat("Coefficients:", coef(fit_reg_cell), "\n\n")
Coefficients: 26.33333 28 31 24.33333 
cat("Method 3 (Regression with reference cell coding):\n")
Method 3 (Regression with reference cell coding):
cat("Coefficients:", coef(fit_reg_effects), "\n\n")
Coefficients: 26.33333 1.666667 4.666667 -2 
# All F-tests match
cat("F-statistics:\n")
F-statistics:
cat(sprintf("Our ANOVA: F = %.3f\n", fit_anova$anova_table$F[1]))
Our ANOVA: F = 35.792
cat(sprintf("lm() with cell means: F = %.3f\n",
            summary(fit_reg_cell)$fstatistic[1]))
lm() with cell means: F = 3409.375
cat("\nThey're identical because ANOVA IS regression!\n")

They're identical because ANOVA IS regression!

Key insight: When you run aov() or anova() in R, you’re actually fitting a linear regression model with indicator variables. There’s no separate “ANOVA algorithm”—it’s all least squares!


7.7 Summary and Looking Ahead

7.7.1 What We Learned This Week

One-way ANOVA is a special case of the linear model with categorical predictors

Design matrices for categorical variables use indicator variables (0s and 1s)

Cell means model (\(y_{ij} = \mu_i + e_{ij}\)): - Always full rank - Parameters = group means - Estimates: \(b_i = \bar{y}_{i.}\)

Effects model (\(y_{ij} = \mu + \alpha_i + e_{ij}\)): - Overparameterized without constraints - Sum-to-zero constraint: \(\sum \alpha_i = 0\) - Equivalent to cell means model

Sum of squares partitioning: SST = SS(Treatments) + SSE - SST: Total variation - SS(Treatments): Between-group variation - SSE: Within-group variation

ANOVA table summarizes decomposition and provides F-test

F-test tests overall null hypothesis: \(H_0: \mu_1 = \cdots = \mu_g\) - Ratio of between-group to within-group variation - Significant F → at least one group differs

ANOVA is regression—same model, same normal equations, same framework!

7.7.2 Connections to Previous Weeks

  • Week 3: Design matrix construction (now with 0s and 1s instead of continuous values)
  • Week 4: Simple regression (2 parameters → now g parameters)
  • Week 5: Least squares theory (same Gauss-Markov theorem applies!)
  • Week 6: Multiple regression (same X’Xb = X’y framework)

7.7.3 Looking Ahead

Week 8: Contrasts and Estimable Functions - F-test told us “groups differ” but not which groups - Contrasts: specific linear combinations to test (e.g., Holstein vs. Jersey, British breeds vs. Continental breeds) - Estimable functions: which parameters can we uniquely estimate? - Multiple comparisons: adjusting for testing many contrasts

Week 9: Two-Way ANOVA and Factorial Models - Multiple categorical factors (e.g., breed AND sex) - Interactions: does effect of breed depend on sex? - Unbalanced designs and Type I/II/III sums of squares

Week 12: Non-Full Rank Models and Estimability - Deep dive into rank deficiency - Generalized inverses - Constraints and their implications

The journey continues! We’re building a unified framework that handles increasingly complex models with the same core principles.


Previous: Week 6: Multiple Regression

Next: Week 8: Contrasts