Treatment effects with marginaleffects

Predictions, slopes and comparisons in a feeding experiment

Gavin Simpson

Aarhus University

A pig feeding experiment

In a simulated experiment, researchers randomly assign control, A or B to pens within indoor and outdoor housing. Each pen holds one pig. Researchers record starting weight and four-week gain, both in kg.

  • What gain does the model predict for an indoor pig starting at 30 kg?
  • How much would switching from control to diet A change predicted gain?
  • How quickly does predicted gain change near a starting weight of 30 kg?
library("dplyr")
library("marginaleffects")

Three questions for a model

Question One result per row Average across rows Plot
What outcome does the model predict? predictions() avg_predictions() plot_predictions()
How much do two settings differ? comparisons() avg_comparisons() plot_comparisons()
How fast does the outcome change here? slopes() avg_slopes() plot_slopes()

The rows describe the pigs or settings where we ask each question.

Simulated pen records

pens <- tidyr::crossing(
  diet = factor(c("control", "A", "B"), levels = c("control", "A", "B")),
  housing = factor(c("indoor", "outdoor"), levels = c("indoor", "outdoor")),
  start_kg = seq(20, 40, by = 5),
  replicate = 1:8
) |>
  filter(housing == "indoor" | replicate <= 2)

The experiment includes eight indoor pens and two outdoor pens per combination of diet and starting weight.

Simulating four-week gain

set.seed(148)
pens <- pens |>
  mutate(
    gain_kg = 8 + 0.20 * (start_kg - 30) - 0.018 * (start_kg - 30)^2 +
      1.2 * (diet == "A") + 1.8 * (diet == "B") +
      2.0 * (diet == "A" & housing == "outdoor") -
      0.8 * (diet == "B" & housing == "outdoor") +
      rnorm(n(), sd = 2.4)
  )

Expected gain follows a curve with starting weight. Diet and housing shift the curve. Each pen has its own random noise.

Model for four-week gain

fit <- lm(
  gain_kg ~ diet * housing + start_kg + I((start_kg - 30)^2),
  data = pens
)

diet * housing lets diet differences vary with housing. The squared term lets the relationship with starting weight bend. The model treats the separate pens as independent.

A grid of pig profiles

datagrid() crosses the supplied values. With its default settings, it fills in omitted numeric predictors at their means and omitted categorical predictors at their most common levels.

grid <- datagrid(
  model = fit,
  diet = distinct(pens, diet)$diet,
  start_kg = 30
)
grid
  rowid housing    diet start_kg
1     1  indoor control       30
2     2  indoor       A       30
3     3  indoor       B       30

The diet column keeps its fitted factor levels. Each row describes a 30 kg pig housed indoors.

Predictions of weight gain

predictions(fit, newdata = grid, type = "response")
    diet start_kg Estimate Std. Error    z Pr(>|z|)     S 2.5 % 97.5 %
 control       30     7.87      0.445 17.7   <0.001 230.5  7.00   8.74
 A             30     8.94      0.445 20.1   <0.001 295.9  8.06   9.81
 B             30     9.63      0.445 21.7   <0.001 343.2  8.76  10.51

Type: response

type = "response" gives kg of predicted gain. Each interval describes uncertainty about the mean gain for pigs in that setting. Individual pigs vary around that mean.

Switching from control to A

diet_A <- c("control", "A")
comparisons(
  fit,
  newdata = datagrid(start_kg = 30),
  variables = list(diet = diet_A),
  type = "response"
)
 start_kg Estimate Std. Error    z Pr(>|z|)   S  2.5 % 97.5 %
       30     1.06      0.535 1.99    0.047 4.4 0.0143   2.11

Term: diet
Type: response
Comparison: A - control

R predicts gain for A and for control at the same starting weight and housing, then subtracts A − control. The printed contrast gives the subtraction order.

The slope at two weights

slopes(
  fit,
  newdata = datagrid(start_kg = c(25, 35)),
  variables = "start_kg",
  type = "response"
)
 start_kg Estimate Std. Error      z Pr(>|z|)    S  2.5 % 97.5 %
       25   0.3097     0.0543  5.704   <0.001 26.4  0.203 0.4161
       35  -0.0178     0.0543 -0.327    0.743  0.4 -0.124 0.0886

Term: start_kg
Type: response
Comparison: dY/dX

The slope is measured in kg of predicted gain per kg of starting weight. The two slopes can differ because the fitted curve bends.

Predicted gain by diet

Predicted gain. condition creates a grid over the named predictors. Other predictors use typical values unless you specify them.

plot_predictions(
  fit,
  condition = c(
    "start_kg", "diet"
  ),
  type = "response"
) +
  ggplot2::labs(
    x = "Starting weight (kg)",
    y = "Predicted gain (kg)"
  )

Three curved lines show expected gain against starting weight for the three diets, with uncertainty ribbons.

Diet effects by housing

Here by averages the A − control differences separately over the observed pens in each housing group.

plot_comparisons(
  fit,
  variables = list(
    diet = diet_A
  ),
  by = "housing",
  type = "response"
) +
  ggplot2::labs(
    y = "A − control (kg)"
  )

A minus control estimates and confidence intervals for indoor and outdoor pens.

The slope of predicted gain

Each point on the line gives the local slope at that starting weight. A zero slope means the fitted gain curve is flat there.

plot_slopes(
  fit,
  variables = "start_kg",
  condition = "start_kg",
  type = "response"
) +
  ggplot2::labs(
    x = "Starting weight (kg)",
    y = "Slope (kg gain per kg)"
  )

The local slope falls as starting weight rises, with a pointwise confidence ribbon.

Average effect across pens

For each observed pen, predict gain on A and on control at the same starting weight and housing. Subtract within each pen, then average the differences.

avg_comparisons(
  fit,
  variables = list(diet = diet_A),
  type = "response"
)
 Estimate Std. Error    z Pr(>|z|)    S 2.5 % 97.5 %
     1.73      0.479 3.62   <0.001 11.7 0.796   2.67

Term: diet
Type: response
Comparison: A - control

With newdata omitted, each pen counts equally. Indoor pens account for 80% of the average.

A balanced housing average

avg_comparisons(
  fit,
  newdata = "balanced",
  variables = list(diet = diet_A),
  type = "response"
)
 Estimate Std. Error    z Pr(>|z|)    S 2.5 % 97.5 %
     2.74      0.598 4.58   <0.001 17.7  1.57   3.91

Term: diet
Type: response
Comparison: A - control

The balanced grid crosses all diet and housing levels at mean starting weight. Each housing type contributes half of the average.

A counterfactual grid

A counterfactual grid copies the observed pens once per specified diet. Each copy keeps the original housing and starting weights.

all_diets <- datagrid(
  model = fit,
  diet = distinct(pens, diet)$diet,
  grid_type = "counterfactual"
)
count(all_diets, diet)
     diet   n
1 control 150
2       A 150
3       B 150

One predicted mean per diet

avg_predictions(
  fit,
  newdata = all_diets,
  by = "diet",
  type = "response"
)
    diet Estimate Std. Error    z Pr(>|z|)     S 2.5 % 97.5 %
 control     6.94      0.339 20.5   <0.001 307.5  6.27   7.60
 A           8.67      0.339 25.6   <0.001 478.2  8.01   9.33
 B           8.48      0.339 25.0   <0.001 457.5  7.82   9.14

Type: response

by = "diet" averages each set of 150 predictions separately. Subtracting the control mean from the A mean gives the observed-pen average difference.

Comparing three diets

avg_comparisons(
  fit,
  variables = list(diet = "pairwise"),
  type = "response"
)
    Contrast Estimate Std. Error     z Pr(>|z|)    S  2.5 % 97.5 %
 A - control    1.734      0.479  3.62  < 0.001 11.7  0.796  2.673
 B - A         -0.191      0.479 -0.40  0.68923  0.5 -1.130  0.747
 B - control    1.543      0.479  3.22  0.00127  9.6  0.605  2.481

Term: diet
Type: response

"pairwise" requests all three pairs. "reference" compares each other diet with the first level, control. A vector such as c("control", "A") requests just one pair.

A test against zero

avg_comparisons(
  fit,
  variables = list(diet = diet_A),
  type = "response",
  hypothesis = 0
)
 Estimate Std. Error    z Pr(>|z|)    S 2.5 % 97.5 %
     1.73      0.479 3.62   <0.001 11.7 0.796   2.67

Term: diet
Type: response
Comparison: A - control

The null says A − control is 0 kg. The Wald statistic is the distance from zero in standard errors (1.73 / 0.479 ≈ 3.62). The two-sided p-value uses the normal distribution.

A test against 1 kg

Suppose the researchers use 1 kg as a practical benchmark for A − control.

avg_comparisons(
  fit,
  variables = list(diet = diet_A),
  type = "response",
  hypothesis = 1
)
 Estimate Std. Error    z Pr(>|z|)   S 2.5 % 97.5 %
     1.73      0.479 1.53    0.125 3.0 0.796   2.67

Term: diet
Type: response
Comparison: A - control

The estimate and confidence interval still describe A − control. The test statistic now measures the distance from 1 kg in standard errors.

One effect per housing type

avg_comparisons(
  fit,
  variables = list(diet = diet_A),
  by = "housing",
  type = "response"
)
 housing Estimate Std. Error    z Pr(>|z|)    S  2.5 % 97.5 %
 indoor      1.06      0.535 1.99    0.047  4.4 0.0143   2.11
 outdoor     4.42      1.071 4.13   <0.001 14.7 2.3207   6.52

Term: diet
Type: response
Comparison: A - control

by gives one average difference per housing group. The first row is indoor and the second is outdoor. Each p-value tests that group’s difference against zero.

A test of how effects differ

avg_comparisons(
  fit,
  variables = list(diet = diet_A),
  by = "housing",
  type = "response",
  hypothesis = "b2 - b1 = 0"
)
 Hypothesis Estimate Std. Error   z Pr(>|z|)   S 2.5 % 97.5 %
    b2-b1=0     3.36        1.2 2.8  0.00505 7.6  1.01    5.7

Type: response

The test subtracts the indoor diet effect from the outdoor diet effect. The standard error uses the covariance between the two fitted contrasts.

A joint test for the diet factor

vs_control <- avg_comparisons(
  fit,
  variables = list(diet = "reference"),
  type = "response"
)
hypotheses(vs_control, joint = TRUE)
Joint hypothesis test:
 A - control = 0
 B - control = 0
 
    F Pr(>|F|) Df 1 Df 2
 7.89   <0.001    2  142

Type: response

One F test asks whether both average differences from control are zero.

Reporting a diet comparison

Averaging paired predictions equally over the observed pens, we estimated that diet A increased four-week weight gain by 1.73 kg compared with control (95% CI 0.80 to 2.67 kg, two-sided p < 0.001).

For a mixed model, say how you handled the group effects. With an lme4 fit, re.form = NA sets group deviations to zero. In a nonlinear model this can differ from averaging predictions over the distribution of group effects.

Package references