]> code.communitydata.science - stats_class_2020.git/blob - r_tutorials/w05a-R_tutorial.rmd
f079d7e766bfaf96b85a258035ac3540b2c78289
[stats_class_2020.git] / r_tutorials / w05a-R_tutorial.rmd
1 ---
2 title: "Week 5 R tutorial (supplement)"
3 subtitle: "Statistics and statistical programming  \nNorthwestern University  \nMTS 525"
4 author: "Aaron Shaw"
5 date: "October 13, 3030"
6 output:
7   html_document:
8     toc: yes
9     number_sections: true
10     toc_depth: 3
11     toc_float:
12       collapsed: false
13       smooth_scroll: true
14     theme: readable
15   pdf_document:
16     toc: yes
17     toc_depth: '3'
18 ---
19
20 ```{r setup, include=FALSE}
21 library(formatR)
22 knitr::opts_chunk$set(echo = TRUE, tidy='styler', message = FALSE)
23 ```
24
25 # Getting started (more better plots)
26 This is a supplement to the Week 5 R tutorial focused on elaborating some examples of time series plots and more polished plots using [`ggplot2`](https://ggplot2.tidyverse.org). I'll work with some data on state-level COVID-19 in the United States published by *The New York Times* (*NYT*). You can access the data as well as details about the sources, measurement, and related available datasets via [the *NYT* github repository](https://github.com/nytimes/covid-19-data). 
27
28 To start, I'll load up the `tidyverse` library and also attach the `lubridate` package, which can help to handle dates and times. Then I'll import the "raw csv" of my dataset from the web, and take a look at it:
29
30 ```{r}
31 library(tidyverse)
32 library(lubridate)
33
34 data_url <- url("https://raw.githubusercontent.com/nytimes/covid-19-data/master/us-states.csv")
35
36 d <- read_csv(data_url)
37
38 d
39 ```
40
41 For the sake of my examples, I'm planning to work with the `date`, `state`, `cases`, and `deaths` variables. Notice that by using the `read_csv()` function to import the data, R already recognizes the `date` column as dates. Also notice that the column names for cases and deaths don't reflect the fact that both variables are *cumulative* counts. Also also, notice that it looks like I need to convert the state variable to a factor. I'll start there and then get a quick sense of how much data I have for each state with a univariate table.
42 ```{r}
43 d$state <- factor(d$state)
44 table(d$state)
45 ```
46
47 Two things to point out here: (1) not all of our "states" are technically states (e.g., Puerto Rico, District of Columbia, Virgin Islands, Northern Mariana Islands, Guam). I prefer to think of this as the *NYT* data scientist team quietly reminding us that the United States maintains a number of colonial properties without formal political representation! The second thing (2) is that not all states have the same number of observations/rows. You can probably figure out exactly why this might be the case from the documentation of the data sources and or from thinking more carefully about the context (e.g., some states had cases much earlier in 2020 than others). Anyhow, just some things to be aware of as we move forward with our analysis.
48
49 # Plotting a univariate time series
50
51 A univariate time series is just a fancy term for a plot of a single variable for which you have repeated observations collected over time. I recommend using [`geom_path()`](https://ggplot2.tidyverse.org/reference/geom_path.html) (that's a hyperlink to the documentation) to create univariate time series plots. Specifically, I'll call `geom_line()`, which is a specialized (masked) version of `geom_path()` that connects observations in order according to the values of variable that is mapped to the x-axis. By convention, a univariate time series maps dates to the x-axis, so this will just plot a line connecting the values of my y-values over time.
52
53 For a univariate example, let's build a plot of weekly case counts in Illinois.
54
55 I can start by just plotting the cumulative cases for all of the states and work towards the specific plot we want from there:
56
57 ```{r}
58 ggplot(data=d, aes(date, cases)) + geom_line()
59 ```
60
61 Notice that ggplot handles the `date` variable quite well by default! It recognizes the units of time and generates axis labels in terms of months. Also notice that ggplot handles the axis labels for the `cases` variable...less well. I don't know about you, but my brain doesn't parse scientific notation quickly/easily. Finally, the fact that this figure incorporates all the state-level observations as cumulative counts means that there is just a huge clutter of points/lines in this figure. It's impossible to really figure out what's going on, much less learn anything other than the cumulative number of cases within states appears to have increased over time (thanks for nothing, ggplot).
62
63 ## Tidying some timeseries data
64
65 Okay, let's get to work cleaning this up. At this point, my next steps are to (1) restrict the data to the Illinois cases; (2) reorganize the *cumulative* daily case counts into weekly counts; and (3) plot it again with better axis labels and a nice title. 
66
67 I can restrict the data to Illinois in a few ways. Since I'm using ggplot, I'll work with Tidyverse "pipes" (`%>%`) and "verbs" (in this case, `filter`):
68 ```{r}
69 d %>%
70   filter(state == "Illinois") %>%
71   ggplot(aes(date, cases)) + geom_line()
72 ```
73
74 That's already much less cluttered and much clearer. It also looks plausibly accurate (it's always good to sanity check your data visualizations as you go—weird anomalies in a graph are usually a good indicator of something weird happening in the underlying code and/or data. 
75
76 Now onwards to converting my cumulative case counts into weekly case counts. When I wrote this tutorial, the first way I thought to do this involved making calls to the Tidyverse  `mutate`, `group_by`, and `summarize` verbs. After a little trial and error, I got it to work with the following code (which I'll walk through in detail below):
77 ```{r}
78 il_weekly_cases <- d %>%  
79   filter(state == "Illinois") %>%  
80   mutate(
81     diff_cases = c(cases[1], diff(cases, lag = 1)),
82     weekdate = cut(date, "week")) %>%
83   group_by(weekdate) %>%
84   summarize(new_cases = sum(diff_cases, na.rm = T),)
85
86 il_weekly_cases
87 ```
88 There's quite a lot happening there so let's go through it verb-by-verb.
89
90 First, I `filter` my cases to restrict the set to Illinois data. Then I use `mutate` to create a `diff_cases` variable that disaggregates the cumulative values of `cases` (read the documentation for `diff` to learn more about this one). Differenced values alone wouldn't produce the correct number of items (try running `length(1:10)` and compare that with `length(diff(1:10, 1))` to see what I mean), so I store the first value of my `cases` variable and then append the differenced values after that (Note that this assumes and takes advantage of the fact that the data is sorted by date. I could add a call to `arrange(-desc())` before doing my mutation to ensure the correct ordering, but won't bother with that for now). Within the same call to mutate I also create a new variable `weekdate` that collapses the dates into weeks (see the documentation for `cut.Date`) and stores the resulting strings as factors (e.g., a factor where the levels correspond to a series of Mondays: "2020-01-20", "2020-01-27"...). Hopefully, so far so good?
91
92 Next, I use `group_by` to aggregate everything by my `weekdate` factor values. This is essentially creating conditional groupings of the data that I can then summarize in my next command.
93
94 Finally I use `summarize` to reshape my data and collapse everything into weekly counts of new cases (notice that I use `sum` inside the `summarize` call to add up the case counts within the grouping variable). The result is a brand new two-column tibble consisting of weekdates and weekly counts of new cases. Excellent!
95
96 Okay, let's see about plotting this now:
97 ```{r}
98 il_weekly_cases %>%
99   ggplot(aes(weekdate, new_cases)) + geom_line()
100 ```
101
102 Hmm. looks like I have a problem here. My first guess is that there's something funny going on with my `weekdate` variable because it looks very different on the x-axis. Let's troubleshoot:
103 ```{r}
104 class(il_weekly_cases$weekdate)
105 ```
106
107 Whoops. Indeed, I need to convert that `weekdate` variable back into an object of class "date" so that it will work with ggplot. There are a number of ways I could do this, but I'll just make a new variable by first coercing `weekdate` to a character vector and then coercing that into a date using `as.Date` (and remember that it is sometimes easier to read these "nested" commands from the inside-out).
108 ```{r}
109 il_weekly_cases$date = as.Date(as.character((il_weekly_cases$weekdate)))
110 il_weekly_cases
111 ```
112 That ought to work for plotting now:
113 ```{r}
114 plot1 <- il_weekly_cases %>%
115   ggplot(aes(date, new_cases)) + geom_line()
116
117 plot1
118 ```
119
120 Much better! Notice that the final week of the data appears to fall off a cliff. That's just an artifact of the way that the *NYT* has published the data for part of the most recent week. Once it updates, the case count probably won't tumble like that (yikes).
121
122 ## Working on ggplot axis labels, titles, and scales
123 Now we can style the plot. As I mentioned briefly in class `ggplot2` treats labels, titles, and scales as "layers" within it's "grammar of graphics" (that sound you hear is me rolling my eyes as I type those scare-quotes). For the purposes of our example here I'm going to use `scale_date` to work with the x-axis, `scale_continuous` to work with the y-axis, and `labs` to clean up the title and axis labels. Each of those have documentation and should appear on the `ggplot2` cheatsheet available via RStudio/Tidyverse.
124
125 To start, let's see whether there might be any way I want to improve the x-axis labels. The ggplot defaults for my `date` variable are pretty good already, but maybe I want to incorporate a label ("break") for each month as well as a more granular grid in the background ("minor_breaks") that shows the weeks? Also, I like the date labels along the axis as abbreviations of the month names, so I'll keep that with a call to `date_labels`. Here's what all of that looks like:
126
127 ```{r}
128 plot2 <- plot1 + scale_x_date(date_labels = "%b", date_breaks= "1 month", date_minor_breaks = "1 week")
129 plot2
130 ```
131
132 The ggplot documentation for [`scale_date`](https://ggplot2.tidyverse.org/reference/scale_date.html) can give you some other examples and ideas. Also, notice how I appended the `scale_date` layer to my existing plot and stored it as a new object? This can make it easier to work iteratively on a single plot, adding new layers as I go without losing existing material along the way. 
133
134 Now I can fix up the y-axis labels a bit using a call to the `labels` argument after I load the `scales` package (why doesn't ggplot support this kind of labeling itself? I have no clue).
135 ```{r}
136 library(scales)
137 plot3 <- plot2 + scale_y_continuous(label=comma)
138 plot3
139 ```
140
141 Nearly done. All that's left is a title and better axis names. I'll do that with yet another layer call to `labs`. The arguments here are pretty intuitive.
142 ```{r}
143 plot4 <- plot3 + labs(x="Week (in 2020)", y="New cases", title="COVID-19 cases in Illinois")
144 plot4
145 ```
146
147 Last, but not least, I mentioned in our class session that ggplot also has "themes" that can be useful for styling plots. One I have used for publications is the "light" theme. Here I apply that theme as...yet another layer:
148 ```{r}
149 plot4 + theme_light()
150 ```
151
152 That's looking much better than when we started! If you wanted to export it as a standalone file (e.g., .png, .pdf, or whatever), I recommend looking at the documentation for the `ggsave()` function, which is available via ggplot2. Base R also has a `save()` function that you can work with, although it can be a bit more complicated to get comfortable with.
153
154 # Multivariate and multidimensional time series plots
155
156 Okay, that's a lovely univariate time series plot. Now let's make this more sophisticated and interesting by incorporating more data, more dimensions, and more variables. In order to do that, I want to start with a little detour into data structures. Try to stay with me—this turns out to be super important for working more efficiently with tools like ggplot as well as learning to manage more complex statistical analysis strategies (that we won't really cover in the course, but so be it).  
157
158 ## Long versus wide data (and why long data is often helpful)
159
160 So now you want to plot a multivariate time series (e.g., the same plot for more than one state and/or for more than one measure). As always, you have a number of options, but the most effective way to achieve this with ggplot involves learning to work with "longer" data.
161
162 Thus far, we have worked mostly with "wide" format data where (nearly) every row corresponds to a single unit/observation and every column corresponds to a distinct variable (for which we usually have no more than one value attributed to any unit/observation). This often results in wider format data that is great for many things. However, it turns out that longer format data can be super helpful for a number of purposes. Producing richer, multidimensional ggplot visualizations is one of them.
163
164 Consider the format of my tidied dataframe that I used for plotting:
165 ```{r}
166 il_weekly_cases
167 ```
168 This dataframe is in a pretty "long" format. Each row is a week and each column is a variable unique to that week (okay, I could consolidate my `weekdate` and `date` columns into just one, but that's not really the point here. The idea is that there's minimal redundant information in the rows and in the columns).
169
170 Our original dataframe was also pretty "long":
171 ```{r}
172 d
173 ```
174 Here we have multiple observations per state (I think I would say the units or rows correspond to "state-dates" or something like that). It's not as "long" as possible, though, because we also have multiple columns corresponding to the two variables of interest: `cases` and `deaths`. 
175
176 For the purposes of producing a multi-state and multivariate set of plots, the most important thing I want to do is consolidate my dataset into a format where I have the following columns: `date` (collapsed into weeks), `state`, `variable` (which will either have a value of `new cases` or `new deaths`), and a column for `value` that will hold the corresponding state-week count for the variable in each row. If that doesn't make sense, don't worry, we'll get there soon enough.
177
178 Doing this involves a different approach to tidying up my data. I'll start by dropping the step where I filtered by `state=="Illinois"` and replacing it with a `group_by` step before I create my `weekdate` variable. I'm also going to go ahead and drop the `date` and `fips` variables because they're just getting in my way.
179 ```{r}
180 weekly <- d %>%
181   group_by(state) %>%
182   mutate(
183     weekdate = cut(date, "week"),
184   ) %>% select(state, cases, deaths, weekdate)
185 weekly
186 ```
187
188 Now I've got multiple observations for each state-week spread across multiple rows (because my rows were structured around a more granular measure of time). My next move is to collapse these into a single observation for each state-week. Remember that my `cases` and `deaths` variables are still cumulative counts, so as I do this aggregation by week I will only need to store the maximum value for each state-week in order to calculate the number of new cases per state-week.
189 ```{r}
190 tidy_weekly <- weekly %>% 
191   group_by(state, weekdate) %>%
192   summarize(
193     cum_cases = max(cases, na.rm=T),
194     cum_deaths = max(deaths, na.rm=T)
195     )
196 ```
197
198 Notice that the call to `group_by` groups by multiple variables. The order here matters! If I reversed it to read `group_by(weekdate, state)` the results would be very different. With the correct ordering, I have things bundled up into state-week sub-groups and then I move on to calculate the maximum value of cumulative cases within each bundle. 
199
200 Next, I can fix up my `weekdate` variable again so that it is a Date object.
201 ```{r}
202 tidy_weekly$weekdate <- as.Date(as.character(tidy_weekly$weekdate))
203 ```
204
205 This will allow me to do some sorting within my state-week bundles to ensure things are in the proper order before I convert my weekly cumulative case count into weekly new case counts.
206 ```{r}
207 tidy_weekly <- tidy_weekly %>% 
208   group_by(state) %>%
209   arrange(-desc(weekdate)) %>%
210   mutate(
211     new_cases = c(cum_cases[1], diff(cum_cases, lag=1)),
212     new_deaths = c(cum_deaths[1], diff(cum_deaths, lag=1)),
213   ) 
214   
215 tidy_weekly
216 ```
217
218 We're much closer to our goal now! 
219
220 I can go ahead and drop the cumulative cases and deaths columns with a call to `select` in my next step. Then the big next (and nearly final) step is to "pivot" the data to organize the `new_cases` and `new_deaths` measures in the way I described above. To manage this, I'll use the `pivot_longer()` function (part of the `tidyr` package from the tidyverse):
221 ```{r}
222 long_weekly <- tidy_weekly %>%
223   select(state, weekdate, new_cases, new_deaths) %>%
224   pivot_longer(
225     cols = starts_with("new"),
226     names_to = "variable",
227     values_to = "value"
228   )
229
230 long_weekly
231 ```
232
233 Can you see what that did? I now have two rows of data for every state-week. One row contains a value for `new_cases` and one contains a value for `new_deaths`. Both of those variables have been "pivoted" into a single `variable` column and their corresponding values recorded in another new column. Note that this makes our dataframe a little longer even though it does not technically reduce the "width" of this particular dataset (because we've taken two columns and pivoted them to create...two different columns). However, consider that we could accommodate as many additional numerical variables and values as we might like in this manner and you can start to see how this pivoting step could result in much longer data (the length becomes a function of the number of units in your dataset and the variables you include in your pivoting step).
234
235 Before we move forward I'm also going to clean up the values of `variable`. This turns out to be helpful later on when we're plotting, but makes more sense to implement here before I start creating any plot layers.
236 ```{r}
237 long_weekly <- long_weekly %>%
238   mutate(
239     variable = recode(variable, new_cases = "new cases", new_deaths = "new deaths")
240   )
241
242 ```
243
244 Okay, prepared with my `long_weekly` tibble, I'm now ready to generate some more interesting and multidimensional plots. Let's start with the same univariate time series of new cases we made for Illinois before so we can see how to replicate that figure with this new data structure:
245 ```{r}
246 long_weekly %>% filter(
247   state == "Illinois" & variable == "new cases"
248 )  %>% ggplot(aes(weekdate, value)) + geom_line()
249
250 ```
251
252 With our "longer" data format, we can plot Illinois cases against deaths from the same tibble by incorporating a `color=variable` argument    :
253 ```{r}
254 long_weekly %>% filter(state == "Illinois") %>% 
255   ggplot(aes(weekdate, value, color=variable)) + geom_line()
256 ```
257
258 Unfortunately, that plot isn't so great because the death counts are dwarfed by the case counts (thank goodness!).
259
260 Now let's compare Illinois case counts against some the neighboring states in the upper midwest:
261 ```{r}
262 upper_midwest = c("Illinois", "Michigan", "Wisconsin", "Iowa", "Minnesota")
263
264 long_weekly %>% 
265   filter(state %in% upper_midwest & variable == "new cases") %>% 
266   ggplot(aes(weekdate, value, color=state)) + geom_line()
267 ```
268
269 Notice that I use the `%in%` operator to filter for the values of the `state` vector that are "in" the `upper_midwest` vector (see `help(%in%)` for more).
270
271 Also notice that we now have ourselves a multivariate time series!
272
273 So now how about finding some way to also incorporate those death counts? If I just add them to this same plot we'll run into the same issue we did with the Illinois data because the death counts look tiny plotted on the same scale as the case counts. A good solution in such a situation is to create a second plot for weekly deaths that we can display together with this weekly cases plot that uses a differently scaled y-axis. The ggplot way to do this involves another type of layer called "facets." Here's an example that creates a faceted "grid" (noy much of a grid since there are only two variables or categories we're using to do the faceting) of weekly case counts and deaths for the same five states.
274 ```{r}
275 midwest_plot <- long_weekly %>% 
276   filter(state %in% upper_midwest) %>% 
277   ggplot(aes(weekdate, value, color=state)) + geom_line() + facet_grid(rows=vars(variable), scales="free_y")
278
279 midwest_plot
280 ```
281
282 Nice! Now we can clean up some of the other elements we worked on with the original plot (axes, title, etc.). I'll bake that into a single chunk below.
283
284 ```{r}
285 midwest_plot + scale_x_date(date_labels = "%b", date_breaks= "1 month", date_minor_breaks = "1 week") + scale_y_continuous(label=comma) + labs(x="Week (in 2020)", y="", title="COVID-19 cases in the Upper Midwest") + theme_light()
286 ```
287
288 That's it! Mission accomplished. We've got ourselves a nice concise visualization of weekly COVID-19 cases and deaths across five upper midwest states over nearly 8 months of the pandemic. 

Community Data Science Collective || Want to submit a patch?