Chapter 2 Prepare the data and do Exploratory Data Analysis
The data set used in the first several chapters here comes from an experiment described in the textbook by Hays (1994), section 10.16, pg 399. The study examined whether asynchronous presentation of video and audio recordings of speakers impaired memory of what was viewed/heard. It is a one-factor design, 3 levels. In the synchronous condition (control) audio and visual recordings were synchronized. In the two asynchronous conditions the audio was slightly ahead of the visual image of the speaker’s lips (fast) or slightly behind the visual image (slow). This independent variable is called “factora” in the data set. The dependent variable (called dv in the data set) is number of words recalled from a list of 50 heard by the participant from the presentation of the recording.
2.1 Import and prepare the data set
The data set is found in a .csv file and has equal sample sizes of ten per group. It is thus a “balanced” design, and a “completely randomized” design.
<- read.csv("data/hays1.csv",header=TRUE, stringsAsFactors = T)
hays # better yet, implement the file.choose function so that
# you don't have to change the default folder
# hays <- read.csv(file.choose(),header=TRUE, stringsAsFactors=T)
# note that the initial data frame sees the dv as
# integer rather than numeric, so....
$dv <- as.numeric(hays$dv)
hays# show the structure of the data frame
str(hays)
## 'data.frame': 30 obs. of 2 variables:
## $ factora: Factor w/ 3 levels "control","fast",..: 1 1 1 1 1 1 1 1 1 1 ...
## $ dv : num 27 28 33 19 25 29 36 30 26 21 ...
#hays
# I will use the attach function here to simplify our code even though
# current best practices in R recommend against using it.
# The downside of using attach is not encountered in this illustration
attach(hays)
2.2 Numerical Summaries
Basic description of the DV as a function of factora is accomplished the the ‘describeBy’ function from psych.
describeBy(dv, group=factora, type=2)
##
## Descriptive statistics by group
## group: control
## vars n mean sd median trimmed mad min max range skew kurtosis se
## X1 1 10 27.4 5.1 27.5 27.38 3.71 19 36 17 -0.04 -0.09 1.61
## ------------------------------------------------------------
## group: fast
## vars n mean sd median trimmed mad min max range skew kurtosis se
## X1 1 10 21.2 5.2 20.5 20.88 4.45 15 30 15 0.66 -0.65 1.65
## ------------------------------------------------------------
## group: slow
## vars n mean sd median trimmed mad min max range skew kurtosis se
## X1 1 10 21.8 2.53 22.5 22 2.22 17 25 8 -0.7 -0.29 0.8
2.3 Graphs of Data by Group
There are a great many ways to draw graphs to display data of the type found even in this simple 3-group design. R can produce them with varying degrees of complexity in the code. Several will be demonstrated here. Some of them are quick tactics to “get to know” your data. Others can be seen as close to publication quality.
2.3.1 Simple EDA Plots
First, we can quickly obtain a simple and very basic plot called a stripchart.
stripchart(dv~factora, method="stack")
An alternative to the base system stripchart shown above is a simple dot plot. Although there are many ways to accomplish this in R, the dotplot
function from the lattice package is simple to use.
# require(lattice)
with(hays, dotplot(dv ~ factora))
It is always useful to obtain boxplots of the DV for the all groups in a 1-way design. With base system graphics it is possible to display data from multiple conditions on the same plot.
boxplot(dv~factora, data=hays,col="lightgray",ylim=c(0,40),
main="1-Way treatment study Box Plot Illustration",
xlab="Treatment Group",
ylab="Mean Words Recalled")
What if we want to truncate the axis and show an axis break? (can’t do in ‘ggplot2’)? Let’s do it for this boxplot. The axis.break
function from the plotrix package accomplishes this.
#library(plotrix) # to obtain the axis.break function
boxplot(dv~factora, data=hays,col="lightgray",ylim=c(8,40),
main="1-Way treatment study Box Plot Illustration",
xlab="Treatment Group",
ylab="Mean Words Recalled - note axis truncation")
axis.break(2,8,style="slash")
One of Tufte’s axioms is “show the data.” Here, using the beeswarm package, we can draw a box plot with raw data points overlaid. Note that the beeswarm function could have been executed after either of the boxplot graphs drawn above as well. Here it produces the simplest/rudimentary boxplot.
boxplot(dv~factora)
beeswarm(dv~factora,add=T)
2.3.2 Frequency Histograms
In many situations it is helpful to visualize each group’s DV distribution with a frequency histogram. The data set used in this document has small sample size per group, so the frequency histogram is not likely to be useful beyond what can be seen with the simpler stripchart above. Nonetheless, it is useful to put code in place as a template for use in situations where sample size is larger. The approach taken here uses ‘ggplot2’ to create a plot that employs “small multiples”, where different panels are drawn for each group. It is also possible to create a layout of histograms with the ‘layout’ function for base system graphics, and that approach has already been demonstrated in prior R tutorials.
Here, I kept the ggplot fairly simple so as to be useful for quick EDA.
#library(ggplot2)
# first, the core xy plot specs
<- ggplot(hays, aes(x = dv))
hbase # now the base plot
<- hbase + geom_histogram(aes(y=..density..), # change y axis to density
hbase1 bins=8, colour="black", fill="white") +
geom_density()
# now add the specification that creates separate panels for eaach group
<- hbase1 + facet_grid(factora ~ .)
hbase2 hbase2
## Warning: The dot-dot notation
## (`..density..`) was deprecated
## in ggplot2 3.4.0.
## ℹ Please use
## `after_stat(density)` instead.
2.3.3 Bar Graphs with Error Bars
The applied sciences rely heavily on a type of bar plots of means, with std errors (or CI’s, or SD’s) displayed as “whiskers”. There is a bit of cottage industry on the web that is heavily critical of these types of graphs, focusing on the fact that information is lost when the data are presented this way. Just do a google search on “dynamite plots” and you will find many blog posts and textbook sections that argue for doing away with them.
Perhaps this is why they are not always simple to obtain in R. Judicious use of them as summary graphs for “ANOVA” types of analyses strikes me as OK. But if one finds that the data have issues of skewness, outliers, or heteroscedasticity, then “showing the raw data points” might be a better approach (c.f. Tufte) - adding raw data onto the bar graph as outlined in the “scientific graphing practices” part of the course.
Notwithstanding this criticism, I have generated some examples of how to draw these graphs in R. I still feel that commercial graphing software such as SigmaPlot or Origins would be a better choice for publication quality graphs of these types.
In the ‘psych’ package, the error.bars.by function gives
a good plot with 95% CI errors as the default.
Other % CI’s can be specified - read the help file: ?error.bars.by
colors of the bars are chosen simply to illustrate the capability.
Use of varying colors would probably not a good choice for either presentation or publication - refer to the BCD discussions of scientific graphing practices.
error.bars.by(hays$dv,hays$factora,bars=TRUE, ylim=c(0,35),
main="1-Way treatment study \n Means +/- 95%CI",
xlab="Treatment Group",
ylab="Mean Words Recalled",
labels=c("control","fast","slow"),
#colors=c("lemonchiffon","forestgreen", "springgreen4"))
colors=c("lightgray","lightgray", "lightgray"))
I found, in the asbio
package, a function to generate bar graphs +/- either std errors or CI’s. Here is a graph with means +/- std errors.
#require(asbio)
bplot(dv,factora,int="SE",
xlab="Treatment Group", ylab="Mean Number of Words",
print.summary=F)
Perhaps a nicer way is available from the from ‘sciplot’ package. default gives +/- 1 std error:
#require(sciplot)
#win.graph() # or quartz() or x11()
bargraph.CI(factora,dv,lc=TRUE, uc=TRUE,legend=T,
cex.leg=1,bty="n",col="gray75",
ylim=c(0,33),
ylab="Mean DV Score",main="Base 1-Way Design Illustration",
cex.names=1.25,cex.lab=1.25)
box()
#axis(4,labels=F)
Next, we can put an axis break on the Y axis to give a visual indicator of the scale break.
# use the axis.break function from plotrix to give the slashes
library(plotrix)
#win.graph() # or quartz() or x11()
bargraph.CI(factora,dv,lc=TRUE, uc=TRUE,legend=T,
cex.leg=1,bty="n",col="gray75",
ylim=c(6,33),
ylab="MEAN DV Score",main="Base 1-Way Design Illustration",
cex.names=1.25,cex.lab=1.25)
box()
#axis.break(4,7,style="slash")
axis.break(2,7,style="slash")
2.3.4 GGPLOT2 can draw boxplots or bar graphs with error bars
The ggplot2 package is a go-to package for visualizations in the data science universe. It is broadly capable and has a different style of programming than base system graphics. It builds on a “grammar of graphics” philosophy, coming from Wilkinson, that is well aligned with object oriented programming. In separate tutorials, basics of ggplot2 programming are taught.
Initally, we will build a boxplot and then a series of bar graphs. Many of the stylistic attributes of a ggplot graph are controlled by the “theme” function. A later section reviews a few alternatives, but for this graph, I prefer a minimal suite of style attributes. This is also a non-standard boxplot in that I added an indicator of the mean of each group since boxplots traditionally only display the median. The blue point indicates the mean. By traditional boxplot definitions, we see no outliers - remembering the relatively small sample sizes, this is not surprising.
<- ggplot(hays, aes(x=factora, y=dv)) +
pbox geom_boxplot() +
stat_summary(fun = mean, geom = "point",
shape = 16, size = 3.5, color = "blue") +
xlab("Treatment Group") +
ylab("Number of Words") +
ggtitle(" Boxplot of Number of Recalled Words\n Means are added with blue point") +
theme_minimal() + theme(text=element_text(size=12))
pbox
With the traditional bar graph for displaying group means, a decision has to be made about inclusion of error bars. The primary tradition is display of +/- one standard error of the mean, but this prompts some thought about just what the purpose of error bars on a graph is this type is. Some discussion of that takes place in the scientific graphing practices section of the course and a literature is found in the toolkit bibliography as well.
If we are going to plot means and add error bars, the approach requires some preliminary work to establish the means, std errors, sd’s, and CI’s to be used. By default the CI is a 95% range but the value can be changed.
The summarySE function from Rmisc produces a data frame that has the summary stats by group.
ggplot cannot work directly on the data frame to produce this type of plot. We need the summary statistics first.
# now use summarySE on our data
<- Rmisc::summarySE(hays, measurevar="dv", groupvars="factora",conf.interval=.95)
hays_summ str(hays_summ)
## 'data.frame': 3 obs. of 6 variables:
## $ factora: Factor w/ 3 levels "control","fast",..: 1 2 3
## $ N : num 10 10 10
## $ dv : num 27.4 21.2 21.8
## $ sd : num 5.1 5.2 2.53
## $ se : num 1.61 1.65 0.8
## $ ci : num 3.65 3.72 1.81
# rename the column that contains the mean to something more less confusing
colnames(hays_summ) <- c("factora", "N", "mean", "sd", "sem", "CI" )
# look at the product
kable(hays_summ)
factora | N | mean | sd | sem | CI |
---|---|---|---|---|---|
control | 10 | 27.4 | 5.103376 | 1.613829 | 3.650735 |
fast | 10 | 21.2 | 5.202564 | 1.645195 | 3.721690 |
slow | 10 | 21.8 | 2.529822 | 0.800000 | 1.809726 |
Initially, one should consider displaying the raw data points. This is the best way of having a sense of the dispersion of the DV within each group and follows Tufte’s axiom: “Show the Data”. The first code chunk defines the base bar graph and the succeeding one adds on another layer that contains the data points. Note that I changed the theme to one that removes the grid background found above in the boxplot.
<- ggplot(hays_summ, aes(x=factora, y=mean)) +
p1 geom_bar(position=position_dodge(), stat="identity", fill="gray") +
xlab("Treatment Group") +
ylab("Mean Number of Words") +
theme_classic() + theme(text=element_text(size=12))
#p1
<- p1 + geom_point(data=hays, aes(x=factora, y=dv))+
p2 ggtitle("The Effect of Visual and Auditory\n Stimulus Synchrony on Performance\n Means plus raw data points")
p2
Next, the same base plot is used for the bars, but standard errors of the mean are added, +/- one standard error.
# add on std error bars
#win.graph() # or quartz() or x11()
<- p1 + geom_errorbar(aes(ymin=mean-sem, ymax=mean+sem),
p3 width=.2, # Width of the error bars
position=position_dodge(.9)) +
ggtitle(" The Effect of Visual and Auditory\n Stimulus Synchrony on Performance\n Means +/- SEM")
p3
A more finished ggplot2 graph might look like this:
<- ggplot(hays_summ, aes(x = factora, y = mean)) +
p3base geom_bar(
position = position_dodge(),
stat = "identity",
fill = "gray",
colour = "black"
+
) geom_errorbar(aes(ymin = mean - sem, ymax = mean + sem),
width = .2,
# Width of the error bars
position = position_dodge(.9)) +
scale_y_continuous(breaks = 0:30 * 5) +
xlab("Treatment Group") +
ylab("Mean Number of Words") +
ggtitle("The Effect of Visual and Auditory\n Stimulus Synchrony on Performance\n Means +/- SEM")
+ theme_bw() p3base
The reader might have noticed a call in this last ‘ggplot2’ graph called theme_bw(). ‘ggplot2’ can handle many different themes available in add-on packages. I use one called ggthemes.
Many other themes are available from the ggthemes package. see [https://cran.r-project.org/web/packages/ggthemes/vignettes/ggthemes.html]
The reader might try any one of the following:
+ theme_tufte()
p3base + theme_grey()
p3base + theme_dark()
p3base + theme_economist()
p3base + theme_excel() # very ugly
p3base + theme_few() # from Stephen Few
p3base + theme_fivethirtyeight()
p3base + theme_gdocs()
p3base + theme_hc()
p3base + theme_solarized()
p3base + theme_stata()
p3base + theme_wsj()
p3base + theme_pander() p3base
Sometimes, error bars are chosen to summarize the dispersion of the DV rather than as a noise estimator of the sample mean. One option (in addition to showing the raw data) is to use standard deviations from each group to form the error bars. The error bars extend up and down one standard deviation.
# add on std error bars
<- p1 + geom_errorbar(aes(ymin=mean-sd, ymax=mean+sd),
p4 width=.2, # Width of the error bars
position=position_dodge(.9)) +
ggtitle(" The Effect of Visual and Auditory\n Stimulus Synchrony on Performance\n Means +/- SD")
p4
A recommended approach is to show Confidence Intervals rather than SEM’s (see the toolkit bibliography section on confidence intervals). This is easy enough to accomplish since the summarySE
function also computed the half width of a confidence interval so that it can be used in plotting the “error bars”.
<- p1 + geom_errorbar(aes(ymin=mean-CI, ymax=mean+CI),
p5 width=.2, # Width of the error bars
position=position_dodge(.9)) +
ggtitle(" The Effect of Visual and Auditory\n Stimulus Synchrony on Performance\n Means plus 95%CI")
p5
### Miscl: Dotplots (lattice), Violin plots
One useful plot that we have seen before combines the violinplot (showing kernel density curves) with either a boxplot or, in this case, a dotplot. The violinplot is best used when sample sizes are a bit larger than the n=10 in this hays data set. Nonetheless, the capability is illustrated. This figure is generated using ggplot2 techniques.
ggplot(hays, aes(factora, dv)) +
geom_violin() + geom_dotplot(binaxis='y', stackdir='center', dotsize=.5) +
theme_classic()
## Bin width defaults to 1/30 of
## the range of the data. Pick
## better value with `binwidth`.
2.3.5 Combining multiple ggplot
figures into one layout
The next plot is an illustration of combining multiple graphs into one layout. First, we will use the hays data set, for continuity. But it is not very useful to draw histograms or kernel density functions with such a low sample size, so a second illustration uses the “cereals” data set.
# Modeled after https://dmyee.files.wordpress.com/2016/03/advancedggplot.pdf
#Histogram of DV, by treatment condition
<-ggplot(data = hays, aes(x = dv, fill=factora)) +
p1geom_histogram(binwidth = .1) +
scale_colour_grey() + scale_fill_grey() +
xlab("Mean Number of Words") + ylab("Count") +
ggtitle("Histograms, by treatment group") +
theme_minimal() +
theme(plot.title = element_text(size=10, face = "bold", hjust = 1))
# Boxplots of DV, by treatment condition
<-ggplot(data = hays, aes(x = factora, y = dv, fill=factora)) +
p2geom_boxplot() +xlab("Treatment Group") + ylab("Number of Words") +
scale_colour_grey() + scale_fill_grey() +
ggtitle("Boxplots of DV by Treatment Group") +
theme_minimal() +
theme(plot.title = element_text(size=10, face = "bold", hjust = 1))
# Violin plots of DV, by treatment condition
<-ggplot(data = hays, aes(x = factora, y = dv, fill=factora)) +
p3geom_violin(alpha=.25, color="gray") +
geom_jitter(alpha=.5, aes(color=factora), position=position_jitter(width=0.3)) +
scale_colour_grey() + scale_fill_grey() +
coord_flip() +
xlab("Treatment Group") + ylab("Number of Words") +
ggtitle("Violin plots of DV by Treatment Group") +
theme_minimal() +theme(plot.title = element_text(size=10, face = "bold", hjust = 1))
# Creating a matrix that defines the layout
# (not all graphs need to take up the same space)
<- rbind(c(1,2),c(3,3))# Plotting the plots on a grid
lay grid.arrange(p1, p2, p3, ncol=2, layout_matrix=lay)
The second illustration uses the “cereals” data set that has been explored for previous course work with SPSS. Here, I chose the dependent variable to be the “Healthiness” rating of each cereal type and the categorical/grouping factor is shelf that the cereal was found on in the supermarket. This second illustration also introduces a way to control color for “fills”, by group, and uses a colorblind friendly palette of colors.
<- read.csv("data/cereal_cold_sugar_shelf.csv", stringsAsFactors=T) cereals
# Modeled after https://dmyee.files.wordpress.com/2016/03/advancedggplot.pdf
#Histograms of "Healthiness" Rating" by Shelf
#library("RColorBrewer")
<- c("#999999", "#E69F00", "#56B4E9", "#009E73",
cbPalette "#F0E442", "#0072B2", "#D55E00", "#CC79A7")
<-ggplot(data = cereals, aes(x = rating, fill=shelf)) +
p1geom_histogram(binwidth = .1) +
scale_fill_manual(values=cbPalette) +
#scale_fill_brewer(palette="Paired") +
#scale_colour_grey() + scale_fill_grey() +
xlab("Rating") + ylab("Count") +
ggtitle("Histograms, by Shelf") +
theme_minimal() +
theme(plot.title = element_text(size=10, face = "bold", hjust = 1))
# Boxplots of "Healthiness" Rating" by Shelf
<-ggplot(data = cereals, aes(x = shelf, y = rating, fill=shelf)) +
p2geom_boxplot() +xlab("Shelf") + ylab("Rating") +
scale_fill_manual(values=cbPalette) +
#scale_fill_brewer(palette="Paired") +
#scale_colour_grey() + scale_fill_grey() +
ggtitle("Boxplots of Rating by Shelf") +
theme_minimal() +
theme(plot.title = element_text(size=10, face = "bold", hjust = 1))
# Violin plots of "Healthiness" Rating" by Shelf
<-ggplot(data = cereals, aes(x = shelf, y = rating, fill=shelf)) +
p3geom_violin(alpha=.25, color="gray") +
geom_jitter(alpha=.5, aes(color=shelf), position=position_jitter(width=0.3)) +
scale_fill_manual(values=cbPalette) + scale_colour_manual(values=cbPalette) +
#scale_fill_brewer(palette="Paired") +
#scale_colour_grey() + scale_fill_grey() +
coord_flip() +
xlab("Shelf") + ylab("Rating") +
ggtitle("Violin plots of Rating by Shelf") +
theme_minimal() +theme(plot.title = element_text(size=10, face = "bold", hjust = 1))
# Creating a matrix that defines the layout
# (not all graphs need to take up the same space)
<- rbind(c(1,2),c(3,3))# Plotting the plots on a grid
lay grid.arrange(p1, p2, p3, ncol=2, layout_matrix=lay)
2.3.6 A Caution about Error Bars and Confidence Intervals for Visualizing Inference
There is a common myth that in group comparison studies such as this that non-overlap of 95% CIs between a pair of groups indicates significance with a traditional two-sample test and that overlap indicates non-significance. The reality is different and a discussion of this is found in other course materials and R tutorials. While the former may be true in most circumstances, the latter is not when using a per comparison alpha of .05.
The issue raises the important point of just what the goal of data display is. If to provide an indicator of dispersion, then raw data points or error bars representing standard deviations might be preferred. If an indicator of sampling variation related to the mean is desired, then error bars as SEM’s or CI’s would be preferred. If the goal is “inference by eye”, then some modification of the CI strategy might be appropriate and that literature has been referred to above. More sophisticated graphs can include information about inference as well. One example of this is an easily produced plot from the ggbetweenstats
function that is found in the ggstatsplot package. This graph may be too cluttered to be suitable for publication, but it is an example of how simple use of R functions can accomplish many things to facilitate exploratory data analysis and rudimentary inference.
set.seed(123)
<- ggbetweenstats(
p6 data = hays,
x = factora,
y = dv,
var.equal=T,
p.adjust.method="fdr",
#ggtheme="classical",
title = "Distribution of Words Recalled across Treatment Condition"
) p6