Thoughts on Teaching R and Yet Another Tidyverse Intro
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.
Image credit to R Memes for Statistical Fiends
Considering this is a blog post, I’m going to get all bloggy here before jumping into the code.
Context
I recently had the opportunity to teach some R coding to colleagues and classmates in a series of workshops. Some had already dabbled in R or other programming languages, but it was the first time that the majority of participants had written a single line of code.
A few things happened in the week following the last session that I didn’t expect.
First, I saw a bit of R code written on a campus whiteboard that had nothing to do with me, but was straight out of the workshop. It may have come from some of my data-centric colleagues who use R, so I didn’t think too much of it.
Then, I overheard a conversation involving R from those in a program that doesn’t require any data-related coursework. Many folks are familiar with the name as the school’s primary data analysis course uses the
{Rcmdr} GUI for statistical analysis, but these students would not have necessarily taken the course. I wondered if there was a connection.
Finally, a student who didn’t even attend came to my office hours asking for resources. Why exactly? Some of his work colleagues attended. It turns out that they are now trying to incorporate some R-powered analysis in their work and he doesn’t want to miss out.
The workshop consisted of 3 consecutive Fridays lasting 90 minutes each. That’s only a total of 4.5 hours.
That’s relatively tiny amount of time.
Wait. Scratch that.
That’s a negligible amount of time.
… but it was enough to convince some participants and non-participants that they should take advantage of the power that a bit of data-centric coding can offer.
Reflection
I taught a similar 90 minute workshop last spring using R, but focused on base R and a few data types. 10 minutes in, I’m trying to explain the difference between a
data.frame and a
matrix and the person asking the question says something along the lines of “I guess I’m kinda dumb. Don’t worry about it.”
For context, these were international policy graduate students. While some have completed a bit of quantitative coursework, most don’t have a hardcore math or science background and programming is seen as something akin to wizardry. However, they hold domain expertise in some rather important subjects. These include WMD nonproliferation, international development, economic diplomacy, conflict studies, and environmental policy. Nearly half of the participants were international students and everyone is proficient in at least a second natural language. Most have already tackled big, complicated problems in their careers and the others are on their way to doing so following graduation. In a nutshell, they’re not dumb. The way I was teaching was dumb. They knew that they’re supposed to want to learn new skills, but they didn’t know why. Focusing on the “basics” didn’t show them anything immediately useful. It didn’t show them the why.
After the workshop, I never heard anyone mention R outside my circle of fellow data folks.
Since that time, I started using R more. Like, a lot more. I have found a way to use R in nearly everything I’ve done since May 2017. As a policy student myself, that has not always been very straightforward and I was still avoiding the strange “tidy” code I’d encounter on Stack Overflow and elsewhere. I realized the error of my ways when I came across Julia Silge and David Robinson’s Text Mining with R. It was like discovering that you’re still in the stone age while most people are off partying on spaceships.
In preparation for this workshop series, I found a lot of inspiration in Michael Levy’s presentation on teaching R, which itself echoes principles preached by other
tidyverse advocates.
A huge takeaway: live coding works.
Writing code in real time shows every single step we make from opening the IDE, to reshaping the data, to debugging inevitable errors, to rendering a final report.
Within a few short weeks of learning to code, it might be surprising how many tiny steps become automatic and taken for granted. Tack on a couple more months and newcomers will think you’re speaking in an entirely different language because you’re explaining something requiring context they simply haven’t yet encountered. Add a few years and… yeesh.
Something that frustrated me when I first started is that code explanations often seem to be written in such a way that dismisses how difficult establishing the basics can be. I’m half-convinced that, for some folks, the trauma was so great that they have simply blocked it from memory. Code is intimidating enough, but if an instructor doesn’t make a conscious effort to empathize, students will question their ability to learn. The goal is empowerment, not intimidation.
Live coding enforces a maximum speed in moving through exercises, which not only gives students more time to digest what you’re doing. It also provides more opportunities for them to ask questions on details you might find trivial, but only because you already suffered through them.
I also think that the benefits of live coding extend to the instructor as well. I found myself answering questions that framed things in ways that I had not even considered, but were exactly how multiple participants saw the task. Additionally, I have a better sense of which concepts need to be covered in more detail, as they weren’t necessarily as intuitive for others as they were for me. On the flip-side, concepts with which I remember struggling may not be difficult at all for others to understand.
… and now that we got the bloggyness of a blog post out of the way…
Here is the workflow I used for the first session. The goal was to introduce the primary
{dplyr} verbs, functions that accomplish tasks necessary in nearly every project. Between each section is an exercise using
{ggplot}.
tidyverse::tidyverse_logo() ## * __ _ __ . o * . ## / /_(_)__/ /_ ___ _____ _______ ___ ## / __/ / _ / // / |/ / -_) __(_- -_) ## \__/_/\_,_/\_, /|___/\__/_/ /___/\__/ ## * . /___/ o . *
# install.packages("tidyverse") library(tidyverse) # install.packages("gapminder") library(gapminder) # loads the gapminder data set ## just to prettify printed tables when knitting # install.packages("kableExtra") library(knitr) library(kableExtra)
Workflow
Resources Up Front
Data Carpentry
Plotting
Our Data
In the following exercises,
gm.data.frame will be used to demonstrate actions that use
{base} R methods for
data.frame operations while
gm_df will be used to to demonstrate
{tidyverse} methods for
tibble operations.
gm.data.frame <- as.data.frame(gapminder) gm_df <- gapminder
tibble
class(gm.data.frame) ## [1] "data.frame" class(gm_df) ## [1] "tbl_df" "tbl" "data.frame"
tibbles are opinionated
data.frames that keep everything that is helpful about
data.frames, changes some of their quirks, and adds methods that makes them even more useful.
Printing
gm.data.frame dumps the whole data set to the console, typically requiring
head() to limit the output.
Printing
head(gm.data.frame) ## country continent year lifeExp pop gdpPercap ##
Printing
gm_df provides the dimensions, data type of each column, and only prints the first 10 rows.
gm_df ## # A tibble: 1,704. ## 7 Afghanistan Asia 1982 39.9 12881816 978. ## 8 Afghanistan Asia 1987 40.8 13867957 852. ## 9 Afghanistan Asia 1992 41.7 16317921 649. ## 10 Afghanistan Asia 1997 41.8 22227415 635. ## # ... with 1,694 more rows
%>%
The pipe (
%>%) is used to chain operations together. Underneath the hood, it’s taking the value on the left-hand side of
%>% and using it as the first argument of the function on the right-hand side of
%>%.
For example, these 2 lines are doing the exact same thing.
head(gm_df) ## #. gm_df %>%.
For simple operations involving 1 function,
%>% is only (arguably) beneficial in that it improves readability as the flow of operations go from left to right.
%>% become truly useful when you need to perform multiple operations in succession, which is the vast majority of data carpentry.
As an arbitrary example, let’s say that we want to select the
head() (first 6 rows) of
gm.data.frame and convert it to a
tibble.
Without
%>%, we can do this in a few ways.
- Use intermediate variables.
- get
gm.data.frame’s
head()and assign it to
no_pipe_1
- convert
no_pipe_1to a
tibblewith
as_tibble()and assign it to
no_pipe_2
no_pipe_1 <- head(gm.data.frame) no_pipe_2 <- as_tibble(no_pipe_1) no_pipe_2 ## #.
- Nest
gm.data.frameinside of
head(), which is itself nested inside of
as_tibble().
as_tibble(head(gm.data.frame)) ## #.
With
%>%, we can chain these actions together in the order in which they occur, which is also the way we read English.
- Here, we do the same thing by:
- taking
gm_df
- piping it to
head()(keeping the top 6 rows)
- piping it to
as_tibble()(converting it to a
tibbledata frame)
gm_df %>% head() %>% as_tibble() ## #.
In practice, it’s usually best to place each of the functions on a separate line as it facilitates debugging and further improves readability.
gm_df %>% as_tibble() %>%.
From here on, you’ll notice
prettify(). This is only being used to print tables in a clean format when the document is
knit()ted.
I’m choosing to include it here as I often find myself reading similar pages where I come across a really effective way to format some output. I understand why the author chooses to set
echo=FALSE, but it can be nice to see the underlying code without having to hunt through their GitHub.
data.frames will print a default maximum of
3 rows while
tibbles will print a default maximum of
10 rows.
prettify <- function(df, n = NULL, cols_changed = NULL, rows_changed = NULL){ if(is.null(n)) n <- ifelse(is.tibble(df), 10, 3) pretty_df <- df %>% head(n) %>% kable(format = "html") %>% kable_styling(bootstrap_options = c("striped", "bordered", "condensed", "hover", "responsive"), full_width = FALSE) if(!is.null(cols_changed)){ pretty_df <- pretty_df %>% column_spec(cols_changed, bold = T, color = "black", background = "#C8FAE3") } if(!is.null(rows_changed)){ pretty_df <- pretty_df %>% row_spec(rows_changed, bold = T, color = "black", background = "#C8FAE3") } return(pretty_df) } gm.data.frame %>% prettify()
gm_df %>% prettify()
Sample Data
You’ll also see a toy data set for the introductory examples that start each section.
sample_countries <- c("Tunisia", "Nicaragua", "Singapore", "Hungary", "New Zealand", "Nigeria", "Brazil", "Sri Lanka", "Ireland", "Australia") sample_df <- gm_df %>% filter(year == 2007, country %in% sample_countries) sample_df %>% prettify()
“Tidy” Data
If you’re unsure of what “Tidy” data is actually describing and want to learn more, you can read Hadley Wickham’s article here. Otherwise, these graphics are likely the most concise explanation you’ll find.
With
tibbles,
%>%, and the concept of tidy data covered, let’s take a dive.
{dplyr}
{dplyr} provides a grammar of data manipulation and a set of verb functions that solve most common data carpentry challenges in a consistent fashion.
glimpse()
filter()
arrange()
mutate()
summarize()
group_by()
Taking a
glimpse()
In addition to the
summary(),
dim()ensions, and
str()ucture functions that can be used to inspect data, you can now use
{dplyr}’s
glimpse().
summary(gm.data.frame) ## country continent year lifeExp ## Afghanistan: 12 Africa :624 Min. :1952 Min. :23.60 ## Albania : 12 Americas:300 1st Qu.:1966 1st Qu.:48.20 ## Algeria : 12 Asia :396 Median :1980 Median :60.71 ## Angola : 12 Europe :360 Mean :1980 Mean :59.47 ## Argentina : 12 Oceania : 24 3rd Qu.:1993 3rd Qu.:70.85 ## Australia : 12 Max. :2007 Max. :82.60 ## (Other) :1632 ## pop gdpPercap ## Min. :6.001e+04 Min. : 241.2 ## 1st Qu.:2.794e+06 1st Qu.: 1202.1 ## Median :7.024e+06 Median : 3531.8 ## Mean :2.960e+07 Mean : 7215.3 ## 3rd Qu.:1.959e+07 3rd Qu.: 9325.5 ## Max. :1.319e+09 Max. :113523.1 ## dim(gm.data.frame) ## [1] 1704 6 str(gm.data.frame) ## 'data.frame': 1704 obs. of 6 variables: ## $ country : Factor w/ 142 levels "Afghanistan",..: 1 1 1 1 1 1 1 1 1 1 ... ## $ continent: Factor w/ 5 levels "Africa","Americas",..: 3 3 3 3 3 3 3 3 3 3 ... ## $ year : int 1952 1957 1962 1967 1972 1977 1982 1987 1992 1997 ... ## $ lifeExp : num 28.8 30.3 32 34 36.1 ... ## $ pop : int 8425333 9240934 10267083 11537966 13079460 14880372 12881816 13867957 16317921 22227415 ... ## $ gdpPercap: num 779 821 853 836 740 ... glimpse(gm_df) ## Observations: 1,704 ## Variables: 6 ## $ country
Afghanistan, Afghanistan, Afghanistan, Afghanistan, ... ## $ continent Asia, Asia, Asia, Asia, Asia, Asia, Asia, Asia, Asia... ## $ year 1952, 1957, 1962, 1967, 1972, 1977, 1982, 1987, 1992... ## $ lifeExp 28.801, 30.332, 31.997, 34.020, 36.088, 38.438, 39.8... ## $ pop 8425333, 9240934, 10267083, 11537966, 13079460, 1488... ## $ gdpPercap 779.4453, 820.8530, 853.1007, 836.1971, 739.9811, 78...
select() columns
Quick Example
Initial Data
sample_df %>% prettify()
End Data
sample_df %>% select(country, pop) %>% prettify()
The
select() family is used to choose columns to keep. You can use bare (unquoted) names.
select()columns by specific names.
- select only
gm_df’s
countryand
popcolumns
gm_df %>% select(country, year, pop) %>% # select columns by specific names prettify()
select()a range of columns by name
gm_df’s
continentcolumn and all columns from
lifeExpto
gdpPercap
gm_df %>% select(continent, lifeExp:gdpPercap) %>% # select columns name range prettify()
- de
select()a column with
-
select()all of
gm_df’s columns except
lifeExp
gm_df %>% select(-lifeExp) %>% # deselect column by name prettify()
- de
select()a range of columns by name
select()all of
gm_df’s columns except those between
lifeExpand
gdpPercap
gm_df %>% select(-c(lifeExp:gdpPercap)) %>% # deselect column by name range prettify()
select()column by index
gm_df’s
4th column
gm_df %>% select(4) %>% # select column by index prettify()
- de
select()a column by index
select()all of
gm_df’s columns except for the
4th column
gm_df %>% select(-4) %>% # deselect column by index prettify()
- de
select()a range of columns by index
select()all of
gm_df’s columns except those between the
3rd and
5th columns
gm_df %>% select(-c(3:5)) %>% # deselect columns by index range prettify()
ggplot() Exercise 1
{ggplot2} is monster of a package used for data visualization that follows The Grammar of Graphics.
{ggplot2} takes R’s powerful graphics capabilities and makes them more accessible by taking care of many plotting tasks that are often tedious, while still allowing for lower-level customization.
- Basic Setup
ggplot(your data
, aes(x =x values
, y =y values
)) +
geom_boxplot() the type of plot geometry desired
Steps
- Using
gm_df, select the
lifeExpcolumn
- Pipe (
%>%) the result to
ggplot()
- Select the plot’s
aes()thetic values
lifeExpfor the
xvalues
- a histogram’s
yare counts of its
xvalues, so we don’t provide them here
- Add
geom_histogram()as the geometry of the plot
gm_df %>% # data frame: Data select(lifeExp) %>% # columns to keep: Data ggplot(aes(x = lifeExp)) + # x values: Aesthetics geom_histogram() # histogram: Geometries
Figure 1: Figure 1
filter() Rows
Quick Example
Initial Data
sample_df %>% select(country, lifeExp) %>% prettify()
End Data
sample_df %>% select(country, lifeExp) %>% filter(lifeExp > 75) %>% prettify(cols_changed = 2)
Use
filter() to select rows using logic. Rows where a logical expression returns
TRUE are kept and others are dropped.
filter()rows where
numeric()values are greater or lesser than another value
filter()
gm_dfto only keep rows where
gdpPercap < 500
gm_df %>% filter(gdpPercap < 500) %>% prettify(cols_changed = 6)
filter()rows using multiple logical expressions where all must be
TRUE
filter()
gm_dfto only keep rows where
year > 1990and
lifeExp < 40
,and
&are evaluated identically in
filter()
gm_df %>% filter(year > 1990, lifeExp < 40) %>% prettify(cols_changed = 3:4)
filter()rows using multiple logical expressions where one must be
TRUE
filter()
gm_dfto only keep rows where
pop < 10000or
gdpPercap > 100000
|means or
gm_df %>% filter(pop < 10000 | gdpPercap > 100000) %>% prettify(cols_changed = 5:6)
filter()rows using a string
filter()
gm_dfto only keep rows where
yearis
1999and
continentis
"Europe"
==means is equal to
gm_df %>% filter(year == 1997 & continent == "Europe") %>% prettify(cols_changed = 2:3)
ggplot() Exercise 2
Steps
- Using
gm_df, select the
continent,
country, and
gdpPercapcolumns
filter()the rows to only keep those where
continent == "Oceania"
- Pipe (
%>%) the result to
ggplot()
- Select the plot’s
aes()thetic values
countryfor the
xvalues
gdpPercapfor the
yvalues
- Add
geom_boxplot()as the geometry of the plot
gm_df %>% # data frame: Data select(continent, country, gdpPercap) %>% # columns to keep: Data filter(continent == "Oceania") %>% # rows to keep: Data ggplot(aes(x = country, y = gdpPercap)) + # x and y values: Aesthetics geom_boxplot() # box plot: Geometries
mutate() Columns
Quick Example
Initial Data
sample_df %>% select(country, pop) %>% prettify()
End Data
sample_df %>% select(country, pop) %>% mutate(pop_in_thousands = pop / 1000) %>% prettify(cols_changed = 3)
Use
mutate() to manipulate column values and create new columns.
In order to
mutate() a column, use the name of the column you are manipulating and set its value using
=.
Here’s a silly example:
- Add a new column to
gm_df
mutate()
gm_dfto create a column named
planetand set its value to
"Earth"
gm_df %>% mutate(planet = "Earth") %>% prettify(cols_changed = 7)
Since we have
gdpPercap and
pop, we can calculate the values for a
total_GDP column.
mutate()
gm_dfto set the results of a calculation on each row to a new column
- multiply
pop * gdpPercapand assign the result to
total_GDPinside
mutate()
gm_df %>% mutate(total_GDP = pop * gdpPercap) %>% prettify(cols_changed = 7)
Typically,
mutate() is used to perform operations across columns in each individual row. You can also use summary functions to perform operations on individual columns (acting as vectors) that result in a vector that can be assigned to a column.
Makes sense, right??
Let’s calculate the z-score of each
gdpPercap value for a specific year.
\[ z = \frac {x_i -\mu_x} {\sigma_x}\]
- \(x\) =
gdpPercap
- \(\mu_x\) = the mean of \(x\) =
mean(gdpPercap)
\(\sigma_x\) = the standard deviation of x =
sd(gdpPercap)
- Use a summary function to perform a a calculation involving summary statistics of a column
- subtract
mean(gdpPercap)from
gdpPercap
- divide the result by
sd(gdpPercap)
- set the results as the values of a new column called
gdp_per_cap_z_score
gm_df %>% filter(year == 1977) %>% mutate(gdp_per_cap_z_score = (gdpPercap - mean(gdpPercap)) / sd(gdpPercap)) %>% prettify(cols_changed = 7)
Here are other functions that can be used similarly:
ggplot() Exercise 3
Steps
- Using
gm_df,
country,
year, and
gdpPercap
filter()the rows to keep only those where
countryis
"Korea, Rep.",
"Korea, Dem. Rep.",
"Japan", or
"China"
- Pipe the result to
ggplot()
- Select the plot’s
aes()thetic values
yearfor the
xvalues
gdpPercapfor the
yvalues
countryfor the
colorvalues
- Add
geom_line()as the geometry of the plot
- Add a
titleto the plot with
labs()
gm_df %>% filter(country %in% c("Korea, Rep.", "Korea, Dem. Rep.", "Japan", "China")) %>% mutate(total_GDP = pop * gdpPercap) %>% ggplot(aes(x = year, y = gdpPercap, color = country)) + geom_line() + labs(title = "GDP Over Time")
arrange() Rows
Quick Example
Initial Data
sample_df %>% select(country, gdpPercap) %>% prettify()
End Data
sample_df %>% select(country, gdpPercap)%>% arrange(gdpPercap) %>% prettify(cols_changed = 2)
Use
arrange() to sort rows.
arrange()by ascending number (smallest to largest)
arrange()
gm_df’s
popcolumn so that smallest populations are on top
gm_df %>% arrange(pop) %>% prettify(cols_changed = 5)
arrange()by
desc()number (largest to smallest)
arrange()the
lifeExpcolumn so that largest values are on top
gm_df %>% arrange(desc(lifeExp)) %>% prettify(cols_changed = 4)
arrange()alphabetically
filter()
gm_dfto keep only those rows where
year == 2007and
continent == "Americas"
arrange()the
countrycolumn alphabetically
gm_df %>% filter(year == 2007, continent == "Americas") %>% arrange(country) %>% prettify(cols_changed = 2:3)
group_by() for Grouped Data
Quick Example
Initial Data
sample_df %>% select(country, continent, pop) %>% prettify()
End Data
sample_df %>% select(country, continent, pop) %>% group_by(continent) %>% mutate(pop_by_continent = sum(pop)) %>% ungroup() %>% arrange(pop_by_continent) %>% prettify(cols_changed = 4)
group_by() allows us to group rows together based on column values.
Let’s say we wanted to compute summary values for each country for all years.
- Calculate the
mean_gdp_per_capof each
countrywith
group_by()
- take
gm_dfand
group_by()
countryto group rows of the same country together
- use
mean()to calculate the
mean_gdp_per_cap
ungroup()the rows
- a habit you want
- keep only those rows with
distinct()combinations of
countryand
mean_gdp_per_cap
distinct()’s default is to only keep columns used as arguments
gm_df %>% group_by(country) %>% mutate(mean_gdp_per_cap = median(gdpPercap)) %>% ungroup() %>% distinct(country, mean_gdp_per_cap) %>% prettify(cols_changed = 2)
ggplot() Exercise 4
Steps
- Using
gm_df,
group_by()the
continentand
year
mutate()to add a column called
mean_gdpfor the average GDP of each continent
ungroup()the data, because this is a habit that will save you headaches later
- Keep only
distinct()combinations of
continent,
year, and
mean_gdp
- Pipe the result to
ggplot()
- Select the plot’s
aes()thetic values
yearfor the
xvalues
mean_gdpfor the
yvalues
continentfor the
colorvalues
- Add
geom_line()as the geometry of the plot
- Add a
titleand a
caption(for the source of the data) to the plot with
labs()
gm_df %>% group_by(year, continent) %>% mutate(mean_gdp = mean(gdpPercap)) %>% ungroup() %>% distinct(continent, year, mean_gdp) %>% ggplot(aes(x = year, y = mean_gdp, color = continent)) + geom_line() + labs(title = "Mean GDPs by Continent Over Time", caption = "Source: Free material from")
summarize()
Quick Example
Initial Data
sample_df %>% select(country, continent, lifeExp, pop) %>% prettify()
sample_df %>% select(country, continent, lifeExp, pop) %>% group_by(continent) %>% summarise(max_pop = max(pop), mean_life_exp = mean(lifeExp)) %>% prettify(cols_changed = 2:3)
Now that we know how to use
group_by(), we can
summarize() data by group. This can be done using all of the summary functions seen earlier.
- Calculate some summary statistics for each continent.
- take
gm_dfand
group_by()
continent
- using
summarize()or
summarise(), calculate:
countwith
n()
mean_popwith
mean()
max_gdp_per_capwith
max()
gm_df %>% group_by(continent) %>% summarise(count = n(), mean_pop = mean(pop), max_gdp_per_cap = max(gdpPercap)) %>% prettify(cols_changed = 2:4)
ggplot() Exercise 5
Steps
- Using
gm_df,
filter()the data to remove rows where
continentis not
"Oceania"
group_by()
continentand
year
summarize()the groups by calculating them
mean()of
pop
ungroup()the data, because this is a habit that will save you headaches later
- Pipe the results to
ggplot()
- Select the plot’s
aes()thetics
yearfor the
xvalues
mean_popfor the
yvalues
continentfor the
colorvalues
- Add
geom_line()for the first geometry
- Add
geom_point()for the second geometry
- Change the theme by adding
theme_minimal()
- Using
facet_wrap(), split the plot into panels for each
continent
~is used as a
formulato select the facet variable
- Add a
titleand a
captionwith
labs()
gm_df %>% filter(continent != "Oceania") %>% group_by(continent, year) %>% summarise(mean_pop = mean(pop)) %>% ungroup() %>% ggplot(aes(x = year, y = mean_pop, color = continent)) + geom_line() + geom_point() + theme_minimal() + facet_wrap(~ continent) + labs(title = "Mean Continent Populations over Time", caption = "Source: Free material from")
النهاية
sessionInfo() ## R version 3.5.1 (2018-07-02) ## Platform: x86_64-w64-mingw32/x64 (64-bit) ## Running under: Windows 10 x64 (build 17134) ## ##] bindrcpp_0.2.2 kableExtra_0.9.0 knitr_1.20.8 ## [4] gapminder_0.3.0 forcats_0.3.0 stringr_1.3.1 ## [7] dplyr_0.7.6 purrr_0.2.5 readr_1.1.1 ## [10] tidyr_0.8.1 tibble_1.4.2.9004 ggplot2_3.0.0.9000 ## [13] tidyverse_1.2.1.9000 ## ## loaded via a namespace (and not attached): ## [1] tidyselect_0.2.4 xfun_0.3 reshape2_1.4.3 ## [4] haven_1.1.2 lattice_0.20-35 colorspace_1.3-2 ## [7] viridisLite_0.3.0 htmltools_0.3.6 yaml_2.1.19 ## [10] utf8_1.1.4 rlang_0.2.1 pillar_1.3.0.9000 ## [13] withr_2.1.2 foreign_0.8-70 glue_1.2.0 ## [16] modelr_0.1.2 readxl_1.1.0 bindr_0.1.1 ## [19] plyr_1.8.4 munsell_0.5.0 blogdown_0.7.1 ## [22] gtable_0.2.0 cellranger_1.1.0 rvest_0.3.2 ## [25] codetools_0.2-15 psych_1.8.4 evaluate_0.10.1 ## [28] labeling_0.3 parallel_3.5.1 fansi_0.2.3 ## [31] highr_0.7 broom_0.4.5 Rcpp_0.12.17 ## [34] scales_0.5.0.9000 jsonlite_1.5 mnormt_1.5-5 ## [37] hms_0.4.2 digest_0.6.15 stringi_1.2.3 ## [40] bookdown_0.7 grid_3.5.1 cli_1.0.0 ## [43] tools_3.5.1 magrittr_1.5 lazyeval_0.2.1 ## [46] crayon_1.3.4 pkgconfig_2.0.1 xml2_1.2.0 ## [49] lubridate_1.7.4 assertthat_0.2.0 rmarkdown_1.10.7 ## [52] httr_1.3.1 rstudioapi_0.7 htmldeps_0.1.0 ## [55] R6_2.2.2 nlme_3.1-137 compiler_3.