This experiment tested whether water temperature and paper towel brand affect water absorption, measured in grams absorbed per 6-sheet sample. The whole-plot factor was temperature with two levels (Cool 64°F and Hot 160°F), and the subplot factor was brand with three levels (Kirkland, Bounty, Viva). Bowls were the block and whole plot, with 3 bowls per temperature and one trial per brand inside each bowl, for 18 total observations.
At \(\alpha = 0.05\), we tested each model term from
\[ Y_{ijk} = \mu + \alpha_i + \beta_{ij} + \gamma_k + (\alpha\gamma)_{ik} + \varepsilon_{ijk} \]
with \(\alpha_i\) for temperature (whole-plot factor), \(\beta_{ij}\) for bowl nested in temperature, and \(\gamma_k\) for brand (subplot factor).
For temperature (\(\alpha_i\)):
\[ H_0: \mu_{\text{Cool}\cdot\cdot} = \mu_{\text{Hot}\cdot\cdot} \]
\[ H_a: \mu_{\text{Cool}\cdot\cdot} \ne \mu_{\text{Hot}\cdot\cdot} \]
This whole-plot test uses \(MS_{\text{Bowl(Temperature)}}\) as the denominator.
For brand (\(\gamma_k\)):
\[ H_0: \mu_{\cdot\text{Kirkland}} = \mu_{\cdot\text{Bounty}} = \mu_{\cdot\text{Viva}} \]
\[ H_a: \mu_{\cdot k} \text{ is not the same for all } k \]
For interaction (\((\alpha\gamma)_{ik}\)):
\[ H_0: (\alpha\gamma)_{ik} = 0 \text{ for all } i,k \]
\[ H_a: (\alpha\gamma)_{ik} \ne 0 \text{ for at least one } i,k \]
Both the brand and interaction tests use \(MS_E\) as the denominator.
This was a split-plot design with temperature as the whole-plot
factor and brand as the subplot factor. For each trial, absorption was
computed from bowl weight loss: Water_Absorbed_g is
negative in the raw file, so the analysis used
Absorbed_g = -Water_Absorbed_g.
DT::datatable(raw.tb,
colnames = c("Obs", "Bowl", "Temperature", "Trial order", "Brand", "Absorbed (g)"),
rownames = FALSE,
options = list(pageLength = 10, autoWidth = TRUE)
) |>
DT::formatRound(columns = "Absorbed_g", digits = 2)
Absorbed_g = -Water_Absorbed_g.The randomized part was brand order within each bowl. In the
experiment, the bowls were kept tied to the temperature groups, with
bowls C1 to C3 used for cool water and bowls
H1 to H3 used for hot water, so bowls were not
randomly reassigned between temperature groups. That could be an issue
if some bowls lose more or less water because of scale placement or
handling differences, since those bowl effects are partly confounded
with temperature. The split-plot model addresses that by using
bowl-within-temperature as the whole-plot error term, but a stronger
follow-up study would randomly assign bowls to temperature or rotate
temperature across bowls.
The most important extra variation here is bowl differences and small timing across runs. The split-plot model helps this by using bowl as the whole-plot error term for temperature, while brand and interaction use the subplot residual error.
kable(temp.tb, digits = 2, col.names = c("Temperature", "n", "Mean", "SD", "Min", "Max")) |>
kable_styling(full_width = TRUE)
| Temperature | n | Mean | SD | Min | Max |
|---|---|---|---|---|---|
| Cool (64°F) | 9 | 50.00 | 8.86 | 38 | 64 |
| Hot (160°F) | 9 | 46.33 | 14.29 | 24 | 76 |
kable(brand.tb, digits = 2, col.names = c("Brand", "n", "Mean", "SD", "Min", "Max")) |>
kable_styling(full_width = TRUE)
| Brand | n | Mean | SD | Min | Max |
|---|---|---|---|---|---|
| Kirkland | 6 | 37.50 | 7.50 | 24 | 45 |
| Bounty | 6 | 49.33 | 3.33 | 44 | 53 |
| Viva | 6 | 57.67 | 12.27 | 40 | 76 |
kable(int.tb, digits = 2, col.names = c("Temperature", "Brand", "n", "Mean", "SD")) |>
kable_styling(full_width = TRUE)
| Temperature | Brand | n | Mean | SD |
|---|---|---|---|---|
| Cool (64°F) | Kirkland | 3 | 40.33 | 2.52 |
| Cool (64°F) | Bounty | 3 | 50.67 | 2.52 |
| Cool (64°F) | Viva | 3 | 59.00 | 6.24 |
| Hot (160°F) | Kirkland | 3 | 34.67 | 10.50 |
| Hot (160°F) | Bounty | 3 | 48.00 | 4.00 |
| Hot (160°F) | Viva | 3 | 56.33 | 18.23 |
On average, cool water showed slightly higher absorption than hot water, but the difference was small. Brand means were more separated, with Viva highest, Bounty in the middle, and Kirkland lowest. The temperature-by-brand means looked fairly parallel, which hints at a weak interaction.
boxplot(Absorbed_g ~ Temperature, data = d.df,
main = "Absorbed Water grams by Temperature: Cool 64°F vs Hot 160°F",
xlab = "Water temperature", ylab = "Absorbed water (g)",
col = c("tan", "peru")
)
stripchart(Absorbed_g ~ Temperature, data = d.df, add = TRUE, vertical = TRUE,
method = "jitter", pch = 16, col = "gray35"
)
boxplot(Absorbed_g ~ Brand, data = d.df,
main = "Absorbed Water grams by Brand: Kirkland, Bounty, Viva",
xlab = "Paper towel brand", ylab = "Absorbed water (g)",
col = c("tan", "peru", "lightblue")
)
stripchart(Absorbed_g ~ Brand, data = d.df, add = TRUE, vertical = TRUE,
method = "jitter", pch = 16, col = "gray35"
)
interaction.plot(d.df$Brand, d.df$Temperature, d.df$Absorbed_g,
type = "b", pch = 19, lwd = 2,
col = c("blue", "red"),
xlab = "Paper towel brand", ylab = "Absorbed water (g)",
legend = FALSE,
main = "Temperature × Brand Interaction grams: Points = Trials, Lines = Means"
)
legend("top",
legend = levels(d.df$Temperature),
title = "",
horiz = TRUE,
lty = 1,
lwd = 2,
pch = 19,
col = c("blue", "red"),
bty = "n",
inset = 0.02
)
points(
as.numeric(d.df$Brand) + ifelse(d.df$Temperature == "Cool (64°F)", -0.06, 0.06),
d.df$Absorbed_g,
pch = 16,
col = ifelse(d.df$Temperature == "Cool (64°F)", "blue", "red")
)
The temperature plot shows overlap between cool and hot bowls. The brand plot shows clearer separation, especially Viva vs Kirkland. The interaction lines are close to parallel, so there is little visual evidence that temperature changes brand ranking.
Because this is a split-plot design, there are two relevant sets of residual checks. The temperature test uses bowl-within-temperature as its error term, so those assumptions should be checked at the bowl level. The brand and temperature-by-brand tests use the subplot residual error from the full split-plot model, so they need their own diagnostic set as well.
For the whole-plot temperature test, I checked residuals from the bowl-level model using each bowl’s mean absorption. The residual-vs-fitted plot does show some megaphone-like spread, but with only six bowl means that pattern is hard to judge confidently and could easily be driven by one or two points. The Q-Q plot is fairly straight for such a small sample, and the Shapiro-Wilk test on the bowl-level residuals gave \(p = 0.376\), so there is no strong normality warning for the whole-plot error term. Overall, the whole-plot assumptions should be treated cautiously because the batch size is so small.
op <- par(mfrow = c(1, 2))
plot(wp.fit, which = 1)
plot(wp.fit, which = 2)
par(op)
For the subplot tests, the residual-vs-fitted plot does not show a clear megaphone shape, so constant variance looks acceptable. The Q-Q plot is reasonably straight for this small sample, so normality looks acceptable. The residual-by-order plot does not show a strong run trend, so independence looks reasonable.
Because assumptions looked acceptable on the original scale, no log transform was used. The Shapiro-Wilk test on the subplot residuals gave \(p = 0.484\), which does not suggest a strong departure from normality.
op <- par(mfrow = c(1, 2))
plot(sp.fit, which = 1)
plot(sp.fit, which = 2)
par(op)
plot(resid.df$order, resid.df$resid,
pch = 16,
xlab = "Run order",
ylab = "Residual",
main = "Independence check: residuals by run order"
)
abline(h = 0, lty = 2)
Because this is a split-plot model, temperature is tested against bowl-within-temperature and both brand and interaction are tested against the residual subplot error.
# Code used for the split-plot ANOVA summary and corrected temperature test.
sp.fit <- aov(Absorbed_g ~ Temperature + Bowl + Brand + Temperature:Brand, data = d.df)
sp.sum <- summary(sp.fit)[[1]]
ms.temp <- sp.sum["Temperature", "Mean Sq"]
ms.bowl <- sp.sum["Bowl", "Mean Sq"]
F.temp <- ms.temp / ms.bowl
p.temp <- pf(F.temp, sp.sum["Temperature", "Df"], sp.sum["Bowl", "Df"], lower.tail = FALSE)
kable(anova.tb, digits = 4) |>
kable_styling(full_width = TRUE)
| Source | Df | Sum.Sq | Mean.Sq | F.value | p.value |
|---|---|---|---|---|---|
| Temperature | 1 | 60.5000 | 60.5000 | 0.6517 | 0.4648 |
| Bowl (within temperature) | 4 | 371.3333 | 92.8333 | NA | NA |
| Brand | 2 | 1232.3333 | 616.1667 | 7.5914 | 0.0142 |
| Temperature x Brand | 2 | 9.0000 | 4.5000 | 0.0554 | 0.9464 |
| Residual | 8 | 649.3333 | 81.1667 | NA | NA |
The interaction was not statistically significant, \(F(2, 8) = 0.055\), \(p = 0.946\), so it is reasonable to interpret the main effects. Temperature was not statistically significant using the correct whole-plot denominator, \(F(1, 4) = 0.652\), \(p = 0.465\). Brand was statistically significant, \(F(2, 8) = 7.591\), \(p = 0.014\), which means average absorption differed by brand. The bowl-within-temperature term also shows non-trivial whole-plot variation, which supports keeping bowl in the model as the correct error structure for testing temperature.
kable(tukey.tb, digits = 3, col.names = c("Comparison", "Mean difference", "Lower 95%", "Upper 95%", "Adjusted p-value")) |>
kable_styling(full_width = TRUE)
| Comparison | Mean difference | Lower 95% | Upper 95% | Adjusted p-value |
|---|---|---|---|---|
| Bounty-Kirkland | 11.833 | -3.030 | 26.696 | 0.118 |
| Viva-Kirkland | 20.167 | 5.304 | 35.030 | 0.012 |
| Viva-Bounty | 8.333 | -6.530 | 23.196 | 0.299 |
Since interaction was not significant and the brand omnibus test was significant, the Tukey follow-up is justified for brand means. The only statistically significant pair was Viva vs Kirkland (mean difference = 20.167 g, 95% CI [5.304, 35.030], adjusted \(p = 0.012\)), so Viva absorbed significantly more water on average than Kirkland in this dataset. Bounty vs Kirkland (adjusted \(p = 0.118\)) and Viva vs Bounty (adjusted \(p = 0.299\)) were not statistically significant because their confidence intervals include 0. Practically, this supports a clear gap between Viva and Kirkland, while Bounty sits in the middle without a clear separation from either brand at the family-wise 0.05 level.
This split-plot study found no clear temperature effect and no temperature-by-brand interaction, but it did find a brand effect on grams absorbed. In this dataset, Viva absorbed the most water on average, Kirkland absorbed the least, and Bounty was in between. A practical next step is to repeat with more bowls per temperature to sharpen whole-plot precision and confirm the same brand ranking.
After these results, I probably need to become a brand ambassador for Viva.