--- title: "IS5 in R: Paired Samples and Blocks (Chapter 18)" author: "Nicholas Horton (nhorton@amherst.edu)" date: "2025-01-20" date-format: iso format: pdf toc: true editor: source --- ```{r} #| label: setup #| include: false library(mosaic) library(tidyverse) ``` ## Introduction and background This document is intended to help describe how to undertake analyses introduced as examples in the Fifth Edition of *Intro Stats* (2018) by De Veaux, Velleman, and Bock. This file as well as the associated Quarto reproducible analysis source file used to create it can be found at http://nhorton.people.amherst.edu/is5. This work leverages initiatives undertaken by Project MOSAIC (http://www.mosaic-web.org), an NSF-funded effort to improve the teaching of statistics, calculus, science and computing in the undergraduate curriculum. In particular, we utilize the `mosaic` package, which was written to simplify the use of R for introductory statistics courses. A short summary of the R needed to teach introductory statistics can be found in the mosaic package vignettes (https://cran.r-project.org/web/packages/mosaic). A paper describing the mosaic approach was published in the *R Journal*: https://journal.r-project.org/archive/2017/RJ-2017-024. We begin by loading packages that will be required for our analyses. ```{r} library(mosaic) library(tidyverse) library(tidyr) # for the pivot_longer() function ``` ## Chapter 18: Paired Samples and Blocks ```{r} #| message: false #| warning: false Dexterity <- read_csv("http://nhorton.people.amherst.edu/is5/data/Dexterity.csv") |> janitor::clean_names() ``` By default, the `read_csv()` function prints the variable names. These messages have been suppressed using the `message: false` code chunk option to save space and improve readability. Here we use the `clean_names()` function from the `janitor` package to sanitize the names of the columns (which would otherwise contain special characters or whitespace). ```{r} Dexterity |> select(age_months, dominant_6, non_dominant_2, gender) |> head(n = 7) ``` ### Section 18.1: Paired Data ```{r} #| warning: false # Figure 18.1 Dexterity |> select(dominant_6, non_dominant_2) |> rename(dominant = dominant_6, non_dominant = non_dominant_2) |> tidyr::pivot_longer( dominant:non_dominant, names_to = "hand_type", values_to = "speed" ) |> gf_boxplot(speed ~ hand_type, fill = ~ hand_type) |> gf_labs(x = "", y = "Speed (cyl/sec)") + ylim(0.2, 0.9) + guides(fill = FALSE) ``` The `pivot_longer()` function is used to reshape the data from wide to long format. This is a powerful and flexible function to convert datasets to the format needed to display or model them. #### Example 18.1: Identifying Paired Data We begin by creating the data set on page 586. ```{r} WorkWeek <- bind_rows( data.frame(name = "Jeff", fiveday = 2798, fourday = 2914), data.frame(name = "Betty", fiveday = 7724, fourday = 6112), data.frame(name = "Roger", fiveday = 7505, fourday = 6177), data.frame(name = "Tom", fiveday = 838, fourday = 1102), data.frame(name = "Aimee", fiveday = 4592, fourday = 3281), data.frame(name = "Greg", fiveday = 8107, fourday = 4997), data.frame(name = "Larry G.", fiveday = 1228, fourday = 1695), data.frame(name = "Tad", fiveday = 8718, fourday = 6606), data.frame(name = "Larry M.", fiveday = 1097, fourday = 1063), data.frame(name = "Leslie", fiveday = 8089, fourday = 6392), data.frame(name = "Lee", fiveday = 3807, fourday = 3362) ) WorkWeek ``` Looking at pairwise differences in `Dexterity`. ```{r} Dexterity |> select(dominant_6, non_dominant_2) |> mutate(difference = dominant_6 - non_dominant_2) |> head(n = 18) ``` ### Section 18.2: The Paired *t*-Test #### Example 18.2: Checking Assumptions and Conditions We can display the distribution of the differences (see page 588). ```{r} WorkWeek <- WorkWeek |> mutate(difference = fiveday - fourday) gf_histogram(~ difference, data = WorkWeek, binwidth = 2000, center = 1000) |> gf_labs(x = "Difference (mi)", y = "# of Workers") ``` #### Example 18.3: Doing a Paired *t*-Test It is straightforward to carry out the paired t-test. ```{r} t.test(~ difference, data = WorkWeek) ``` or do the same by "hand" within R: ```{r} nwork <- nrow(WorkWeek) nwork # number of pairs dwork <- mean(~ difference, data = WorkWeek) dwork # mean of differences swork <- sd(~ difference, data = WorkWeek) swork # SD of differences sework <- swork / (nwork^.5) sework # SE of differences twork <- (dwork - 0) / sework twork # t stat 2 * xpt(twork, df = nwork - 1, lower.tail = FALSE) ``` The `xpt()` function finds the p-value and plots it on a graph to visualize it. Here, the visualization shows a one-sided test, but in the book, it is two sided. ### Section 18.3: Confidence Intervals for Matched Pairs We begin by reading the data. ```{r} #| message: false Couples <- read_csv("http://nhorton.people.amherst.edu/is5/data/Couples.csv") |> filter(wAge != "*") |> mutate(wAge = as.numeric(wAge)) # table on page 592 Couples |> select(wAge, hAge) |> mutate(difference = hAge - wAge) |> head(n = 7) ``` #### Step-By-Step Example: A Paired *t*-Interval We replicate the example from page 593. ```{r} DexData <- Dexterity |> select(dominant_6, non_dominant_2) |> mutate(difference = dominant_6 - non_dominant_2) |> filter(dominant_6 < 1) # For some reason, the book has removed one observation where dominant_6 = 1, # but has kept the count of children at 93 instead of 92 gf_histogram(~ difference, data = DexData, binwidth = .05, center = .025) |> gf_labs(x = "Dominant-Non-dominant", y = "# Subjects") ``` Here we display the calculations using the `t.test()` function and then by hand. ```{r} df_stats(~ difference, data = DexData) t.test(~ difference, data = DexData) ``` ```{r} ndex <- nrow(DexData) + 1 # the book kept n at 93 for some reason ndex # number of pairs (children) ddex <- mean(~ difference, data = DexData) ddex # mean difference sdex <- sd(~ difference, data = DexData) sdex # standard deviation of the differences sedex <- sdex / (ndex^.5) sedex # standard error of the differences df <- ndex - 1 df # this matches the book, but it should be 91 tstats <- qt(p = c(.025, .975), df = df) tstats medex <- tstats * sedex medex # margin of error of the differences ddex + medex # Or, if you don't want to go through all those calculations: t.test(~ difference, data = DexData, df = df) ``` #### Effect Size #### Example 18.4: Looking at Effect Size with a Paired *t* Confidence Interval We can verify the calculations from the example. ```{r} tstats <- qt(p = c(.025, .975), df = nwork - 1) tstats me <- tstats * sework me # margin of error dwork + me # confidence interval ``` ### Section 18.4: Blocking #### What's Independent? #### Random Matters: A Bootstrapped Paired Data Confidence Interval and Hypothesis Test Our usual approach to bootstrapping works here. ```{r} set.seed(2345) num_sim <- 5000 # What does do() do? mean(~ difference, data = resample(DexData)) # One mean of a random resample mean(~ difference, data = resample(DexData)) # Another mean of a random resample do(2) * mean(~ difference, data = resample(DexData)) # Calculates two means # We need num_sim means DexBoots <- do(num_sim) * mean(~ difference, data = resample(DexData)) ``` For more information about `resample()`, refer to the resampling vignette: https://cran.r-project.org/web/packages/mosaic/vignettes/Resampling.html ```{r} #| warning: false qdata(~ mean, p = c(.025, .975), data = DexBoots) DexBoots <- DexBoots |> mutate(interval = ifelse( mean > 0.0245 & mean < 0.0783, "Within 95% Confidence", "Outside 95% Confidence" )) # Figure 18.4, page 597 gf_histogram( ~ mean, fill = ~interval, data = DexBoots, binwidth = 0.002, center = 0.001 ) |> gf_vline(xintercept = 0.0245) |> gf_vline(xintercept = 0.0783) |> gf_labs(x = "Differences of Bootstrapped Means", y = "# of Trials") + guides(fill = FALSE) # Figure 18.5 gf_histogram(~ (mean - ddex), data = DexBoots, binwidth = .002, center = .001) |> gf_vline(xintercept = ddex) |> gf_vline(xintercept = -ddex) |> gf_labs(x = "Differences of Bootstrapped Means - Actual Difference", y = "# of Trials") ``` ```{r} df_stats(~ (mean - ddex), data = DexBoots) ``` With `df_stats()`, we can see that our minimum is within the interval, but our maximum isn't. ```{r} DexBoots |> filter((mean - ddex) > ddex) ``` Like the book, there is one instance (out of 5,000), so we estimate the P-value as 1/5,000 (the book says 50,000, which is incorrect), or .0002.