Introduction

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.

Paper towels used in the absorption experiment
Paper towels used in the absorption experiment

Methods

Hypotheses

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.

Design

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)

Procedure

  1. Set up six bowls total, with three bowls assigned to Cool (64°F) water and three bowls assigned to Hot (160°F) water, and fill each bowl to 300 grams of water.
Type of Equipment Used
Type of Equipment Used
  1. Cut paper towels into 4 in x 4 in samples using 6 sheets per trial sample.
  2. Use a ruler to mark 1 inch from the towel edge so dip depth stays consistent for every trial.
Marking the 1-inch dip depth on the towel
Marking the 1-inch dip depth on the towel
  1. Randomize the brand order within each bowl so brand is not confounded with run order. Excel was used for this with random function, sorting from smallest to largest
  2. For each trial, dip the towel 1 inch into the bowl for 10 seconds, then hold for a 5-second drip period.
  3. Record bowl weight before and after each trial, then compute absorption from bowl weight loss as Absorbed_g = -Water_Absorbed_g.

Randomization And Bias

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.

Variance And Limits

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.

Analysis

Data Summary

Numerical summaries

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.

Graphical summaries

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.

Assumptions

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.

Temperature test: bowl-level residuals

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)

Brand and interaction tests: subplot residuals

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)

ANOVA

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.

Follow-Up

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.

Conclusion

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.

Potential Viva brand ambassador moment
Potential Viva brand ambassador moment