showing distributions

Data cleaning

Below we will make use of the command as.numeric() to force one pesky column that should be numbers to be interpreted as numbers. We have used this data before, but will briefly review and recreate some of that work, and then extend it. We’ll read the data, filter out a few problematic entries, and ensure the variable types are best.

dat <- read.delim("./../data/cards.csv", header = TRUE, sep = ',')
summary(dat)
      X                 Card              Time1               Time2      
 Length:119         Length:119         Length:119         Min.   :0.440  
 Class :character   Class :character   Class :character   1st Qu.:0.980  
 Mode  :character   Mode  :character   Mode  :character   Median :1.360  
                                                          Mean   :1.718  
                                                          3rd Qu.:2.310  
                                                          Max.   :7.650  
                                                          NA's   :30     
table(dat$Card)

    A  B  C  D  E 
 8 23 23 22 21 22 
dat <- dat[dat$Card %in% c("A", "B", "C", "D", "E"), ]
table(dat$Card)

 A  B  C  D  E 
23 23 22 21 22 
dat$Card <- factor(dat$Card, levels = c("A", "B", "C", "D", "E"), ordered = TRUE)
summary(dat)
      X             Card      Time1               Time2      
 Length:111         A:23   Length:111         Min.   :0.440  
 Class :character   B:23   Class :character   1st Qu.:0.980  
 Mode  :character   C:22   Mode  :character   Median :1.360  
                    D:21                      Mean   :1.718  
                    E:22                      3rd Qu.:2.310  
                                              Max.   :7.650  
                                              NA's   :22     
dat$Time1 <- as.numeric(dat$Time1)
Warning: NAs introduced by coercion
head(dat)
     X Card Time1 Time2
1 1990    A    NA    NA
2 1990    B    NA    NA
3 1990    C  3.26    NA
4 1990    D  3.25    NA
5 1990    E    NA    NA
6 KL05    A  1.28  0.71

Making plots

We will get started, as we have before, with a simple boxplot shown in Figure 1.

par(mar = c(4.1, 5.1, 1.1, 1.1))
boxplot(Time1 ~ Card, dat, ylim = c(0, 25), las = 1, xlab = "", ylab = "")
mtext("Reaction time", side = 3, at = 0, font = 2)
mtext("Card", side = 1, line = 2, font = 2)
Reaction times for cards A, B, and C are relatively small and not that variable. Reaction time and variability are increased for cards D and E.
Figure 1: A boxplot showing the variability in reaction times by card type.

There is often confusion about the annotation of boxplots – “is the line the mean or the median?” The solid line in the box represents the median value for that group of measurements. People often want the annotation to represent the mean, or talk about it as if they do. I suspect this is because most people making box plots would rather be making bar plots and spread on a bar plot is often indicated by error bars (of some sort) around the mean. By computing the “5-number summary”, we can plot horizontal lines at those numbers. Notice the coincidence with important features of the boxplot.

tmp <- summary(dat[dat$Card == "E", "Time1"])
names(tmp)
[1] "Min."    "1st Qu." "Median"  "Mean"    "3rd Qu." "Max."    "NA's"   
#min(dat[dat$Card == "E", "Time1"], na.rm = TRUE) ## this was just to confirm the minimum value 
boxplot(Time1 ~ Card, dat, ylim = c(0, 25), subset = Card == "E", las = 1)
abline(h = tmp, col = "gray")
abline(h = tmp['Median'], col = "red")
A boxplot used diagnostically by adding annotations derived from the 5-number statistical summary of the data in the boxplot. Horizontal lines are shown at locations coinciding with major feature of the box plot.
Figure 2

A criticism of box plots is the absence of most of the raw data (excluding outliers) and of more-detailed information about the distribution. Below we will create a few histograms and empirical density plots. Rather than attack all quantities at once, for now we will look at the distribution of a single value or “level” of the categorical variable or “factor” in the Card column.

Histograms

par(mfrow = c(2, 2), mar = c(4.1, 4.1, 1.1, 1.1))
hist(dat[dat$Card == "E", "Time1"], main = "default", xlab = "Reaction time")
hist(dat[dat$Card == "E", "Time1"], breaks = 2, main = "breaks = 2", xlab = "Reaction time")
hist(dat[dat$Card == "E", "Time1"], breaks = 3, main = "breaks = 3", xlab = "Reaction time")
hist(dat[dat$Card == "E", "Time1"], breaks = c(0, 10, 20, 30), freq = TRUE, main = "breaks = c(0, 10, 20, 30)", xlab = "Reaction time")
A demonstration of the role of one optional argument to the histogram command. The option (breaks) is flexible, but may not always behave as desired.
Figure 3: Sample histograms generated with default settings and modified with the breaks option.

Layering the histograms for additional values of Card onto any one of the panels of Figure 3 gets messy. For just two values, it’s possible to use shading or hashing to emphasize and account for overlaps. Beyond that, it gets more difficult. In order to compare distributions more easily, it helps to represent the distribution by a curve or an empirical density function.

plot(density(dat[dat$Card == "E", "Time1"], from = 0, na.rm = TRUE))
The plot of a curve that represents the empirical distribution of values for the variable Time1.
Figure 4: Sample empirical density function with the from option set to ensure negative values of the variable are excluded.

Let’s add layers that represent the computed densities for other card choices. The results shown in Figure 5 border on what is sometimes called a “spaghetti plot”. We definitely would not want to add additional curves to this - 5 to 6 seems to be about the upper limit on what we can generally pay attention to.

plot(density(dat[dat$Card == "A", "Time1"], from = 0, na.rm = TRUE))
for(card in c("B", "C", "D", "E")){
  lines(density(dat[dat$Card == card, "Time1"], from = 0, na.rm = TRUE))
}
The plot of a curve that represents the empirical distribution of values for the variable Time1.
Figure 5: Sample empirical density function with the from option set to ensure negative values of the variable are excluded.

To have any chance interpreting this we need colors to know which curve is which card and a legend or other labeling. Labeling the curves directly would be nice, but each is so distinct, there doesn’t seem to be a nice way to “compute” locations for direct labeling. We will build a legend, the result of which is shown in Figure 6.

cols <- hcl.colors(5, palette = "viridis")
cards <- c("B", "C", "D", "E")
plot(density(dat[dat$Card == "A", "Time1"], from = 0, na.rm = TRUE), xlim = c(0, 25), col = cols[1])
for(card in 1:4){
  lines(density(dat[dat$Card == cards[card], "Time1"], from = 0, na.rm = TRUE), col = cols[card + 1])
}
legend("topright", c("A", "B", "C", "D", "E"), col = cols, lty = 1)
The plot of a curve that represents the empirical distribution of values for the variable Time1 for different card types.
Figure 6: Empirical density functions of reaction time distribution for each card type.

We could have done that a bit more efficiently, but I didn’t want to keep changing things. Revisiting this myself, i probably would define cards <- c("A", "B", ...) and instead of dat$Card == "A", I would use ... == cards[1]. With this I could start the for() loop at 2 and not have to index by card + 1, using just card instead.

warpbreaks data

#?warpbreaks
par(mfrow = c(1, 2), mar = c(4.1, 5.1, 1.1, 1.1))
boxplot(breaks ~ tension, warpbreaks, las = 1, ylim = c(0, 80))
boxplot(breaks ~ wool, warpbreaks, las = 1, ylim = c(0, 80))
Side-by-side box plots showing the number of breaks in a spool of wool by tension (low, medium high) and wool (type A or type B).
Figure 7: Side-by-side box plots of the warpbreaks data.

It could be intersting to explore the effect of tension on each wool type, or the reverse.

par(mfrow = c(1, 2), mar = c(4.1, 5.1, 1.1, 1.1))
boxplot(breaks ~ wool*tension, warpbreaks, col = c("red", "blue"))
boxplot(breaks ~ tension*wool, warpbreaks, col = c("red", "blue", "gray"))
Box plots where values are grouped by types within each tension or by tensions within each type. Color has been used to group similar values of type or tension.
Figure 8: Interaction between wool and tension and the interaction between tension and wool. There is a better way to say this, but quite honestly I’m tired.

We can get some diagnostic information out of a box plot; using plot = FALSE will “compute” the box plot and return information about its construction which will be useful for some of our annotation.

bxp <- boxplot(breaks ~ wool, warpbreaks, plot = FALSE)
bxp
$stats
     [,1] [,2]
[1,] 10.0   13
[2,] 19.5   18
[3,] 26.0   24
[4,] 36.0   29
[5,] 54.0   44

$n
[1] 27 27

$conf
         [,1]     [,2]
[1,] 20.98283 20.65522
[2,] 31.01717 27.34478

$out
[1] 70 67

$group
[1] 1 1

$names
[1] "A" "B"

Looking at the contents of bxp gives a little insight into the construction of the graph. This information helps in the construction of Figure 9. Here the goal is to show raw data, somewhat like a “beeswarm” plot.

boxplot(breaks ~ wool, warpbreaks)
points(jitter(rep(1, 27), amount = 0.1), warpbreaks[warpbreaks$wool == "A", "breaks"], pch = 19, col = "darkgray")
points(jitter(rep(2, 27), amount = 0.1), warpbreaks[warpbreaks$wool == "B", "breaks"], pch = 19, col = "lightgray")
The boxplot for breaks by wool type, but with raw data shown roughly centered vertically in the box.
Figure 9: The boxplot for breaks by wool type, but with raw data shown.

With just one clever change we can incorporate information about the level of tension. Shown in Figure 10, we define three colors from the viridis palette. Including alpha = 0.5 “softens” the color a bit by introducing transparency.Colors are selected by the numeric equivalent of the factor level. To understand this better, run warpbreaks[warpbreaks$wool == "A", "tension"])], then slowly work your way out with as.numeric(...) and then cols[...], as below.

boxplot(breaks ~ wool, warpbreaks)
cols <- hcl.colors(3, alpha = 0.5)
points(jitter(rep(1, 27), amount = 0.1), warpbreaks[warpbreaks$wool == "A", "breaks"], pch = 19, col = cols[as.numeric(warpbreaks[warpbreaks$wool == "A", "tension"])])
points(jitter(rep(2, 27), amount = 0.1), warpbreaks[warpbreaks$wool == "B", "breaks"], pch = 19, col = cols[as.numeric(warpbreaks[warpbreaks$wool == "B", "tension"])])
legend("topright", levels(warpbreaks$tension), col = cols, pch = 19)
The boxplot for breaks by wool type, but with raw data shown roughly centered vertically in the box.
Figure 10: The boxplot for breaks by wool type, but with raw data shown.