--- title: "Data Science for Economists" subtitle: "Lecture 8: Regression analysis in R" author: name: Grant R. McDermott affiliation: University of Oregon | [EC 607](https://github.com/uo-ec607/lectures) # date: Lecture 6 #"`r format(Sys.time(), '%d %B %Y')`" output: html_document: theme: journal highlight: haddock # code_folding: show toc: yes toc_depth: 4 toc_float: yes keep_md: false keep_tex: false ## Change to true if want keep intermediate .tex file css: css/preamble.css ## For multi-col environments pdf_document: latex_engine: xelatex toc: true dev: cairo_pdf # fig_width: 7 ## Optional: Set default PDF figure width # fig_height: 6 ## Optional: Set default PDF figure height includes: in_header: tex/preamble.tex ## For multi-col environments extra_dependencies: ["float", "booktabs", "longtable"] pandoc_args: --template=tex/mytemplate.tex ## For affiliation field. See: https://bit.ly/2T191uZ always_allow_html: true urlcolor: blue mainfont: cochineal sansfont: Fira Sans monofont: Fira Code ## Although, see: https://tex.stackexchange.com/q/294362 ## Automatically knit to both formats: knit: (function(inputFile, encoding) { rmarkdown::render(inputFile, encoding = encoding, output_format = 'all') }) --- ```{r setup, include=FALSE} knitr::opts_chunk$set(echo = TRUE, cache = TRUE, dpi=300) ``` Today's lecture is about the bread-and-butter tool of applied econometrics and data science: regression analysis. My goal is to give you a whirlwind tour of the key functions and packages. I'm going to assume that you already know all of the necessary theoretical background on causal inference, asymptotics, etc. This lecture will *not* cover any of theoretical concepts or seek to justify a particular statistical model. Indeed, most of the models that we're going to run today are pretty silly. We also won't be able to cover some important topics. For example, I'll only provide the briefest example of a Bayesian regression model and I won't touch times series analysis at all. (Although, I will provide links for further reading at the bottom of this document.) These disclaimers aside, let's proceed... ## Software requirements ### R packages It's important to note that "base" R already provides all of the tools we need for basic regression analysis. However, we'll be using several additional packages today, because they will make our lives easier and offer increased power for some more sophisticated analyses. - New: **fixest**, **estimatr**, **ivreg**, **sandwich**, **lmtest**, **mfx**, **margins**, **broom**, **modelsummary**, **vtable** - Already used: **tidyverse**, **hrbrthemes**, **listviewer** A convenient way to install (if necessary) and load everything is by running the below code chunk. ```{r libs_print, cache=FALSE, eval=FALSE} ## Load and install the packages that we'll be using today if (!require("pacman")) install.packages("pacman") pacman::p_load(mfx, tidyverse, hrbrthemes, estimatr, ivreg, fixest, sandwich, lmtest, margins, vtable, broom, modelsummary) ## Make sure we have at least version 0.6.0 of ivreg if (numeric_version(packageVersion("ivreg")) < numeric_version("0.6.0")) install.packages("ivreg") ## My preferred ggplot2 plotting theme (optional) theme_set(hrbrthemes::theme_ipsum()) ``` ```{r libs, cache=FALSE, message=FALSE, echo=FALSE} ## Load and install the packages that we'll be using today if (!require("pacman")) install.packages("pacman") pacman::p_load(mfx, tidyverse, hrbrthemes, estimatr, ivreg, fixest, sandwich, lmtest, margins, vtable, broom, modelsummary, kableExtra) ## My preferred ggplot2 plotting theme (optional) theme_set(hrbrthemes::theme_ipsum()) ``` While we've already loaded all of the required packages for today, I'll try to be as explicit about where a particular function is coming from, whenever I use it below. Something else that I want to mention up front is that we'll mostly be working with the `starwars` data frame that we've already seen from previous lectures. Here's a quick reminder of what it looks like to refresh your memory. ```{r starwars} starwars ``` ## Regression basics ### The `lm()` function R's workhorse command for running regression models is the built-in `lm()` function. The "**lm**" stands for "**l**inear **m**odels" and the syntax is very intuitive. ```r lm(y ~ x1 + x2 + x3 + ..., data = df) ``` You'll note that the `lm()` call includes a reference to the data source (in this case, a hypothetical data frame called `df`). We [covered this](https://raw.githack.com/uo-ec607/lectures/master/04-rlang/04-rlang.html#global_env) in our earlier lecture on R language basics and object-orientated programming, but the reason is that many objects (e.g. data frames) can exist in your R environment at the same time. So we need to be specific about where our regression variables are coming from --- even if `df` is the only data frame in our global environment at the time. Let's run a simple bivariate regression of mass on height using our dataset of starwars characters. ```{r ols1} ols1 = lm(mass ~ height, data = starwars) ols1 ``` The resulting object is pretty terse, but that's only because it buries most of its valuable information --- of which there is a lot --- within its internal list structure. If you're in RStudio, you can inspect this structure by typing `View(ols1)` or simply clicking on the "ols1" object in your environment pane. Doing so will prompt an interactive panel to pop up for you to play around with. That approach won't work for this knitted R Markdown document, however, so I'll use the `listviewer::jsonedit()` function that we saw in the previous lecture instead. ```{r ols1_str, message=F, out.width="100%", out.height="10%"} # View(ols1) ## Run this instead if you're in a live session listviewer::jsonedit(ols1, mode="view") ## Better for R Markdown ``` As we can see, this `ols1` object has a bunch of important slots... containing everything from the regression coefficients, to vectors of the residuals and fitted (i.e. predicted) values, to the rank of the design matrix, to the input data, etc. etc. To summarise the key pieces of information, we can use the --- *wait for it* --- generic `summary()` function. This will look pretty similar to the default regression output from Stata that many of you will be used to. ```{r ols1_summ} summary(ols1) ``` We can then dig down further by extracting a summary of the regression coefficients: ```{r ols1_coefs} summary(ols1)$coefficients ``` ### Get "tidy" regression coefficients with the `broom` package While it's easy to extract regression coefficients via the `summary()` function, in practice I always use the **broom** package ([link ](https://broom.tidyverse.org/)) to do so. **broom** has a bunch of neat features to convert regression (and other statistical) objects into "tidy" data frames. This is especially useful because regression output is so often used as an input to something else, e.g. a plot of coefficients or marginal effects. Here, I'll use `broom::tidy(..., conf.int = TRUE)` to coerce the `ols1` regression object into a tidy data frame of coefficient values and key statistics. ```{r ols1_tidy} # library(broom) ## Already loaded tidy(ols1, conf.int = TRUE) ``` Again, I could now pipe this tidied coefficients data frame to a **ggplot2** call, using saying `geom_pointrange()` to plot the error bars. Feel free to practice doing this yourself now, but we'll get to some explicit examples further below. **broom** has a couple of other useful functions too. For example, `broom::glance()` summarises the model "meta" data (R2, AIC, etc.) in a data frame. ```{r ols1_glance} glance(ols1) ``` By the way, if you're wondering how to export regression results to other formats (e.g. LaTeX tables), don't worry: We'll [get to that](#regression-tables) at the end of the lecture. ### Regressing on subsetted data Our simple model isn't particularly good; the R2 is only `r I(round(glance(ols1)$r.squared, 3))`. Different species and homeworlds aside, we may have an extreme outlier in our midst... ```{r jabba, message=FALSE, echo=FALSE, warning=FALSE} starwars %>% ggplot(aes(x=height, y=mass)) + geom_point(alpha=0.5) + geom_point( data = starwars %>% filter(mass==max(mass, na.rm=T)), col="red" ) + geom_curve( x = 212, y = 1150, xend = 180, yend = max(starwars$mass, na.rm=T), curvature = 0.25, col = 'red', hjust = 1, arrow = arrow(length = unit(0.03, "npc")) ) + annotate("text", x = 212, y = 1100, label = "Jabba!", col = 'red') + labs( title = "Spot the outlier", caption = "Remember: Always plot your data..." ) ``` Maybe we should exclude Jabba from our regression? You can do this in two ways: 1) Create a new data frame and then regress, or 2) Subset the original data frame directly in the `lm()` call. #### 1) Create a new data frame Recall that we can keep multiple objects in memory in R. So we can easily create a new data frame that excludes Jabba using, say, **dplyr** ([lecture](https://raw.githack.com/uo-ec607/lectures/master/05-tidyverse/05-tidyverse.html)) or **data.table** ([lecture](https://raw.githack.com/uo-ec607/lectures/master/05-datatable/05-datatable.html)). For these lecture notes, I'll stick with **dplyr** commands since that's where our starwars dataset is coming from. But it would be trivial to switch to **data.table** if you prefer. ```{r ols2} starwars2 = starwars %>% filter(name != "Jabba Desilijic Tiure") # filter(!(grepl("Jabba", name))) ## Regular expressions also work ols2 = lm(mass ~ height, data = starwars2) summary(ols2) ``` #### 2) Subset directly in the `lm()` call Running a regression directly on a subsetted data frame is equally easy. ```{r ols2a} ols2a = lm(mass ~ height, data = starwars %>% filter(!(grepl("Jabba", name)))) summary(ols2a) ``` The overall model fit is much improved by the exclusion of this outlier, with R2 increasing to `r I(round(glance(ols2)$r.squared, 3))`. Still, we should be cautious about throwing out data. Another approach is to handle or account for outliers with statistical methods. Which provides a nice segue to nonstandard errors. ## Nonstandard errors Dealing with statistical irregularities (heteroskedasticity, clustering, etc.) is a fact of life for empirical researchers. However, it says something about the economics profession that a random stranger could walk uninvited into a live seminar and ask, "How did you cluster your standard errors?", and it would likely draw approving nods from audience members. The good news is that there are *lots* of ways to get nonstandard errors in R. For many years, these have been based on the excellent **sandwich** package ([link](http://sandwich.r-forge.r-project.org/articles/sandwich.html)). However, here I'll demonstrate using the **estimatr** package ([link](https://declaredesign.org/r/estimatr/articles/getting-started.html)), which is both fast and provides convenient aliases for the standard regression functions. Some examples follow below. ### Robust standard errors You can obtain heteroskedasticity-consistent (HC) "robust" standard errors using `estimatr::lm_robust()`. Let's illustrate by implementing a robust version of the `ols1` regression that we ran earlier. Note that **estimatr** models automatically print in pleasing tidied/summary format, although you can certainly pipe them to `tidy()` too. ```{r ols1_robust} # library(estimatr) ## Already loaded ols1_robust = lm_robust(mass ~ height, data = starwars) # tidy(ols1_robust, conf.int = TRUE) ## Could tidy too ols1_robust ``` The package defaults to using Eicker-Huber-White robust standard errors, commonly referred to as "HC2" standard errors. You can easily specify alternate methods using the `se_type = ` argument.^[See the [package documentation](https://declaredesign.org/r/estimatr/articles/mathematical-notes.html#lm_robust-notes) for a full list of options.] For example, you can specify Stata robust standard errors if you want to replicate code or results from that language. (See [here](https://declaredesign.org/r/estimatr/articles/stata-wls-hat.html) for more details on why this isn't the default and why Stata's robust standard errors differ from those in R and Python.) ```{r ols1_robust_stata} lm_robust(mass ~ height, data = starwars, se_type = "stata") ``` **estimatr** also supports robust instrumental variable (IV) regression. However, I'm going to hold off discussing these until we get to the [IV section](#instrumental-variables) below. #### Aside on HAC (Newey-West) standard errors On thing I want to flag is that **estimatr** does not yet offer support for HAC (i.e. heteroskedasticity *and* autocorrelation consistent) standard errors *a la* [Newey-West](https://en.wikipedia.org/wiki/Newey%E2%80%93West_estimator). I've submitted a [feature request](https://github.com/DeclareDesign/estimatr/issues/272) on GitHub --- vote up if you would like to see it added sooner! --- but you can still obtain these pretty easily using the aforementioned **sandwich** package. For example, we can use `sandwich::NeweyWest()` on our existing `ols1` object to obtain HAC SEs for it. ```{r ols1_hac_ses} # library(sandwich) ## Already loaded # NeweyWest(ols1) ## Print the HAC VCOV sqrt(diag(NeweyWest(ols1))) ## Print the HAC SEs ``` If you plan to use HAC SEs for inference, then I recommend converting the model object with `lmtest::coeftest()`. This function builds on **sandwich** and provides a convenient way to do [on-the-fly](https://grantmcdermott.com/better-way-adjust-SEs/) hypothesis testing with your model, swapping out a wide variety of alternate variance-covariance (VCOV) matrices. These alternate VCOV matrices could extended way beyond HAC --- including HC, clustered, bootstrapped, etc. --- but here's how it would work for the present case: ```{r ols1_hac} # library(lmtest) ## Already loaded ols1_hac = lmtest::coeftest(ols1, vcov = NeweyWest) ols1_hac ``` Note that its easy to convert `coeftest()`-adjusted models to tidied **broom** objects too. ```{r ols1_hac_tidy} tidy(ols1_hac, conf.int = TRUE) ``` ### Clustered standard errors Clustered standard errors is an issue that most commonly affects panel data. As such, I'm going to hold off discussing clustering until we get to the [panel data section](#high-dimensional-fes-and-multiway-clustering) below. But here's a quick example of clustering with `estimatr::lm_robust()` just to illustrate: ```{r ols1_robust_clustered, warning = FALSE} lm_robust(mass ~ height, data = starwars, clusters = homeworld) ``` ## Dummy variables and interaction terms For the next few sections, it will prove convenient to demonstrate using a subsample of the starwars data that comprises only the human characters. Let's quickly create this new dataset before continuing. ```{r humans} humans = starwars %>% filter(species=="Human") %>% select(where(Negate(is.list))) ## Drop list columns (optional) humans ``` ### Dummy variables as *factors* Dummy variables are a core component of many regression models. However, these can be a pain to create in some statistical languages, since you first have to tabulate a whole new matrix of binary variables and then append it to the original data frame. In contrast, R has a very convenient framework for creating and evaluating dummy variables in a regression: Simply specify the variable of interest as a [factor](https://r4ds.had.co.nz/factors.html).^[Factors are variables that have distinct qualitative levels, e.g. "male", "female", "hermaphrodite", etc.] Here's an example where we explicitly tell R that "gender" is a factor. Since I don't plan on reusing this model, I'm just going to print the results to screen rather than saving it to my global environment. ```{r ols_dv} summary(lm(mass ~ height + as.factor(gender), data = humans)) ``` Okay, I should tell you that I'm actually making things more complicated than they need to be with the heavy-handed emphasis on factors. R is "friendly" and tries to help whenever it thinks you have misspecified a function or variable. While this is something to be [aware of](https://rawgit.com/grantmcdermott/R-intro/master/rIntro.html#r_tries_to_guess_what_you_meant), normally It Just Works^TM^. A case in point is that we don't actually *need* to specify a string (i.e. character) variable as a factor in a regression. R will automatically do this for you regardless, since it's the only sensible way to include string variables in a regression. ```{r ols_dv2} ## Use the non-factored version of "gender" instead; R knows it must be ordered ## for it to be included as a regression variable summary(lm(mass ~ height + gender, data = humans)) ``` ### Interaction effects Like dummy variables, R provides a convenient syntax for specifying interaction terms directly in the regression model without having to create them manually beforehand.^[Although there are very good reasons that you might want to modify your parent variables before doing so (e.g. centering them). As it happens, I'm [on record](https://twitter.com/grant_mcdermott/status/903691491414917122) as stating that interaction effects are most widely misunderstood and misapplied concept in econometrics. However, that's a topic for another day.] You can use any of the following expansion operators: - `x1:x2` "crosses" the variables (equivalent to including only the x1 × x2 interaction term) - `x1/x2` "nests" the second variable within the first (equivalent to `x1 + x1:x2`; more on this [later](#nestedmarg)) - `x1*x2` includes all parent and interaction terms (equivalent to `x1 + x2 + x1:x2`) As a rule of thumb, if [not always](#nestedmarg), it is generally advisable to include all of the parent terms alongside their interactions. This makes the `*` option a good default. For example, we might wonder whether the relationship between a person's body mass and their height is modulated by their gender. That is, we want to run a regression of the form, $$Mass = \beta_0 + \beta_1 D_{Male} + \beta_2 Height + \beta_3 D_{Male} \times Height$$ To implement this in R, we simply run the following, ```{r ols_ie} ols_ie = lm(mass ~ gender * height, data = humans) summary(ols_ie) ``` ## Panel models ### Fixed effects with the **fixest** package The simplest (and least efficient) way to include fixed effects in a regression model is, of course, to use dummy variables. However, it isn't very efficient or scalable. What's the point learning all that stuff about the [Frisch-Waugh-Lovell](https://en.wikipedia.org/wiki/Frisch%E2%80%93Waugh%E2%80%93Lovell_theorem), within-group transformations, etc. etc. if we can't use them in our software routines? Again, there are several options to choose from here. For example, many of you are probably familiar with the excellent **lfe** package ([link](https://cran.r-project.org/web/packages/lfe/index.html)), which offers near-identical functionality to the popular Stata library, **reghdfe** ([link](http://scorreia.com/software/reghdfe/)). However, for fixed effects models in R, I am going to advocate that you look no further than the **fixest** package ([link](https://lrberge.github.io/fixest)). **fixest** is relatively new on the scene and has quickly become one of my absolute favourite packages. It has an *boatload* of functionality built in to it: support for nonlinear models, high-dimensional fixed effects, multiway clustering, multi-model estimation, LaTeX tables, etc, etc. It is also insanely fast... as in, up to [orders of magnitude](https://lrberge.github.io/fixest/#benchmarking) faster than **lfe** or **reghdfe**. I won't be able to cover all of **fixest**'s features in depth here --- see the [introductory vignette](https://lrberge.github.io/fixest/articles/fixest_walkthrough.html) for a thorough walkthrough --- but I hope to least give you a sense of why I am so enthusiastic about it. Let's start off with a simple example before moving on to something slightly more demanding. #### Simple FE model The package's main function is `fixest::feols()`, which is used for estimating linear fixed effects models. The syntax is such that you first specify the regression model as per normal, and then list the fixed effect(s) after a `|`. An example may help to illustrate. Let's say that we again want to run our simple regression of mass on height, but this time control for species-level fixed effects.^[Since we specify "species" in the fixed effects slot below, `feols()` will automatically coerce it to a factor variable even though we didn't explicitly tell it to.] ```{r ols_fe, message=FALSE} # library(fixest) ## Already loaded ols_fe = feols(mass ~ height | species, data = starwars) ## Fixed effect(s) go after the "|" ols_fe ``` Note that the resulting model object has automatically clustered the standard errors by the fixed effect variable (i.e. species). We'll explore some more options for adjusting standard errors in **fixest** objects shortly. But to preview things, you can specify the standard errors you want at estimation time... or you can adjust the standard errors for any existing model via `summary.fixest()`. For example, here are two equivalent ways to specify vanilla (iid) standard errors: :::::: {.columns} ::: {.column width="48%" data-latex="{0.48\textwidth}"} Specify SEs at estimation time. ```{r ols_fe_standard_et, message=FALSE} feols(mass ~ height | species, data = starwars, se = 'standard') ``` ::: ::: {.column width="4%" data-latex="{0.04\textwidth}"} \ ::: ::: {.column width="48%" data-latex="{0.48\textwidth}"} Adjust SEs of an existing model (`ols_fe`) on the fly. ```{r ols_fe_standard_otf} summary(ols_fe, se = 'standard') ``` ::: :::::: \ Before continuing, let's quickly save a "tidied" data frame of the coefficients for later use. I'll use iid standard errors again, if only to show you that the `broom::tidy()` method for `fixest` objects also accepts an `se` argument. This basically just provides another convenient way for you to adjust standard errors for your models on the fly. ```{r coefs_fe} # coefs_fe = tidy(summary(ols_fe, se = 'standard'), conf.int = TRUE) ## same as below coefs_fe = tidy(ols_fe, se = 'standard', conf.int = TRUE) ``` #### High dimensional FEs and multiway clustering As I already mentioned above, **fixest** supports (arbitrarily) high-dimensional fixed effects and (up to fourway) multiway clustering. To see this in action, let's add "homeworld" as an additional fixed effect to the model. ```{r ols_hdfe, message = FALSE} ## We now have two fixed effects: species and homeworld ols_hdfe = feols(mass ~ height | species + homeworld, data = starwars) ols_hdfe ``` Easy enough, but the standard errors of the above model are automatically clustered by species, i.e. the first fixed effect variable. Let's go a step further and cluster by both "species" and "homeworld". ^[I make no claims to this is a particularly good or sensible clustering strategy, but just go with it.] **fixest** provides several ways for us to do this --- via the `se` or `cluster` arguments --- and, again, you can specify your clustering strategy at estimation time, or adjust the standard errors of an existing model on-the-fly. I'll (re)assign the model to the same `ols_hdfe` object, but you could, of course, create a new object if you so wished. ```{r ols_hdfe_twoway} ## Cluster by both species and homeworld ## These next few lines all do the same thing. Pick your favourite! ## Specify desired SEs at estimation time... # ols_hdfe = feols(mass ~ height | species + homeworld, se = 'twoway', data = starwars) # ols_hdfe = feols(mass ~ height | species + homeworld, cluster = c('species', 'homeworld'), data = starwars) # ols_hdfe = feols(mass ~ height | species + homeworld, cluster = ~ species + homeworld, data = starwars) # ##... or, adjust the SEs of an existing model on the fly # ols_hdfe = summary(ols_hdfe, se = 'twoway') # ols_hdfe = summary(ols_hdfe, cluster = c('species', 'homeworld')) ols_hdfe = summary(ols_hdfe, cluster = ~ species + homeworld) ## I'll go with this one ols_hdfe ``` #### Comparing our model coefficients We'll get to [model presentation](https://raw.githack.com/uo-ec607/lectures/master/08-regression/08-regression.html#Presentation) at the very end of the lecture. For now, I want to quickly flag that **fixest** provides some really nice, built-in functions for comparing models. For example, you can get regression tables with [`fixest::etable()`](https://lrberge.github.io/fixest/articles/exporting_tables.html). ```{r etable} etable(ols_fe, ols_hdfe) ``` Similarly, the [`fixest::coefplot()`](https://lrberge.github.io/fixest/reference/coefplot.html) function for plotting estimation results: ```{r coefplot} coefplot(list(ols_fe, ols_hdfe)) ## Add legend (optional) legend("bottomleft", col = 1:2, lwd = 1, pch = c(20, 17), legend = c("FE and no clustering", "HDFE and twoway clustering")) ``` `coefplot()` is especially useful for tracing the evolution of treatment effects over time, as in a difference-in-differences setup (see [Examples](https://lrberge.github.io/fixest/reference/coefplot.html#examples)). However, I realise some people may find it a bit off-putting that it produces base R plots, rather than a **ggplot2** object. We'll get to an automated **ggplot2** coefficient plot solution further below with `modelsummary::modelplot()`. Nevertheless, let me close this out this section by demonstrating the relative ease with which you can do this "manually". Consider the below example, which leverages the fact that we have saved (or can save) regression models as data frames with `broom::tidy()`. As I suggested earlier, this makes it simple to construct our own bespoke coefficient plots. ```{r fe_mods_compared} # library(ggplot2) ## Already loaded ## First get tidied output of the ols_hdfe object coefs_hdfe = tidy(ols_hdfe, conf.int = TRUE) bind_rows( coefs_fe %>% mutate(reg = "Model 1\nFE and no clustering"), coefs_hdfe %>% mutate(reg = "Model 2\nHDFE and twoway clustering") ) %>% ggplot(aes(x=reg, y=estimate, ymin=conf.low, ymax=conf.high)) + geom_pointrange() + labs(Title = "Marginal effect of height on mass") + geom_hline(yintercept = 0, col = "orange") + ylim(-0.5, NA) + ## Added a bit more bottom space to emphasize the zero line labs( title = "'Effect' of height on mass", caption = "Data: Characters from the Star Wars universe" ) + theme(axis.title.x = element_blank()) ``` FWIW, we'd normally expect our standard errors to blow up with clustering. Here that effect appears to be outweighed by the increased precision brought on by additional fixed effects. Still, I wouldn't put too much thought into it. Our clustering choice doesn't make much sense and I really just trying to demonstrate the package syntax. #### Aside on standard errors We've now seen the various options that **fixest** has for specifying different standard error structures. In short, you invoke either of the `se` or `cluster` arguments. Moreover, you can choose to do so either at estimation time, or by adjusting the standard errors for an existing model post-estimation (e.g. with `summary.fixest(mod, cluster = ...)`). There are two additional points that I want to draw your attention to. First, if you're coming from another statistical language, adjusting the standard errors post-estimation (rather than always at estimation time) may seem slightly odd. But this behaviour is actually extremely powerful, because it allows us to analyse the effect of different error structures *on-the-fly* without having to rerun the entire model again. **fixest** is already the fastest game in town, but just think about the implied time savings for really large models.^[To be clear, adjusting the standard errors via, say, `summary.fixest()` completes instantaneously.] I'm a huge fan of the flexibility, safety, and speed that on-the-fly standard error adjustment offers us. I even wrote a whole [blog post](https://grantmcdermott.com/better-way-adjust-SEs/) about it if you'd like to read more. Second, reconciling standard errors across different software is a much more complicated process than you may realise. There are a number of unresolved theoretical issues to consider --- especially when it comes to multiway clustering --- and package maintainers have to make a number of arbitrary decisions about the best way to account for these. See [here](https://github.com/sgaure/lfe/issues/1#issuecomment-530643808) for a detailed discussion. Luckily, Laurent (the **fixest** package author) has taken the time to write out a [detailed vignette](https://lrberge.github.io/fixest/articles/standard_errors.html) about how to replicate standard errors from other methods and software packages.^[If you want a deep dive into the theory with even more simulations, then [this paper](http://sandwich.r-forge.r-project.org/articles/jss_2020.html) by the authors of the **sandwich** paper is another excellent resource.] ### Random and mixed effects Fixed effects models are more common than random or mixed effects models in economics (in my experience, anyway). I'd also advocate for [Bayesian hierachical models](http://www.stat.columbia.edu/~gelman/arm/) if we're going down the whole random effects path. However, it's still good to know that R has you covered for random effects models through the **plm** ([link](https://cran.r-project.org/web/packages/plm/)) and **nlme** ([link](https://cran.r-project.org/web/packages/nlme/index.html)) packages.^[As I mentioned above, **plm** also handles fixed effects (and pooling) models. However, I prefer **fixest** and **lfe** for the reasons already discussed.] I won't go into detail , but click on those links if you would like to see some examples. ## Instrumental variables As you would have guessed by now, there are a number of ways to run instrumental variable (IV) regressions in R. I'll walk through three different options using the `ivreg::ivreg()`, `estimatr::iv_robust()`, and `fixest::feols()` functions, respectively. These are all going to follow a similar syntax, where the IV first-stage regression is specified in a multi-part formula (i.e. where formula parts are separated by one or more pipes, **`|`**). However, there are also some subtle and important differences, which is why I want to go through each of them. After that, I'll let you decide which of the three options is your favourite. The dataset that we'll be using for this section describes cigarette demand for the 48 continental US states in 1995, and is taken from the **ivreg** package. Here's a quick a peek: ```{r cigaretteDemand} data("CigaretteDemand", package = "ivreg") head(CigaretteDemand) ``` Now, assume that we are interested in regressing the number of cigarettes packs consumed per capita on their average price and people's real incomes. The problem is that the price is endogenous, because it is simultaneously determined by demand and supply. So we need to instrument for it using cigarette sales tax. That is, we want to run the following two-stage IV regression. $$Price_i = \pi_0 + \pi_1 SalesTax_i + v_i \hspace{1cm} \text{(First stage)}$$ $$Packs_i = \beta_0 + \beta_2\widehat{Price_i} + \beta_1 RealIncome_i + u_i \hspace{1cm} \text{(Second stage)}$$ ### Option 1: `ivreg::ivreg()` I'll start with `ivreg()` from the **ivreg** package ([link](https://john-d-fox.github.io/ivreg/index.html)).^[Some of you may wondering, but **ivreg** is a dedicated IV-focused package that splits off (and updates) functionality that used to be bundled with the **AER** package.] The `ivreg()` function supports several syntax options for specifying the IV component. I'm going to use the syntax that I find most natural, namely a formula with a three-part RHS: `y ~ ex | en | in`. Implementing our two-stage regression from above may help to illustrate. ```{r iv} # library(ivreg) ## Already loaded ## Run the IV regression. Note the three-part formula RHS. iv = ivreg( log(packs) ~ ## LHS: Dependent variable log(rincome) | ## 1st part RHS: Exogenous variable(s) log(rprice) | ## 2nd part RHS: Endogenous variable(s) salestax, ## 3rd part RHS: Instruments data = CigaretteDemand ) summary(iv) ``` **ivreg** has lot of functionality bundled into it, including cool diagnostic tools and full integration with **sandwich** and co. for swapping in different standard errors on the fly. See the [introductory vignette](https://john-d-fox.github.io/ivreg/articles/ivreg.html) for more. The only other thing I want to mention briefly is that you may see a number `ivreg()` tutorials using an alternative formula representation. (Remember me saying that the package allows different formula syntax, right?) Specifically, you'll probably see examples that use an older two-part RHS formula like: `y ~ ex + en | ex + in`. Note that here we are writing the `ex` variables on both sides of the `|` separator. The equivalent for our cigarette example would be as follows. Run this yourself to confirm the same output as above. ```{r iv2, eval=FALSE} ## Alternative two-part formula RHS (which I like less but YMMV) iv2 = ivreg( log(packs) ~ ## LHS: Dependent var log(rincome) + log(rprice) | ## 1st part RHS: Exogenous vars + endogenous vars log(rincome) + salestax, ## 2nd part RHS: Exogenous vars (again) + Instruments data = CigaretteDemand ) summary(iv2) ``` This two-part syntax is closely linked to the manual implementation of IV, since it requires explicitly stating *all* of your exogenous variables (including instruments) in one slot. However, it requires duplicate typing of the exogenous variables and I personally find it less intuitive to write.^[Note that we didn't specify the endogenous variable (i.e. `log(rprice)`) directly. Rather, we told R what the *exogenous* variables were. It then figured out which variables were endogenous and needed to be instrumented in the first-stage.] But different strokes for different folks. ### Option 2: `estimatr::iv_robust()` Our second IV option comes from the **estimatr** package that we saw earlier. This will default to using HC2 robust standard errors although, as before, we could specify other options if we so wished (including clustering). Currently, the function doesn't accept the three-part RHS formula. But the two-part version works exactly the same as it did for `ivreg()`. All we need to do is change the function call to `estimatr::iv_robust()`. ```{r, iv_robust} # library(estimatr) ## Already loaded ## Run the IV regression with robust SEs. Note the two-part formula RHS. iv_reg_robust = iv_robust( ## Only need to change the function call. Everything else stays the same. log(packs) ~ log(rincome) + log(rprice) | log(rincome) + salestax, data = CigaretteDemand ) summary(iv_reg_robust, diagnostics = TRUE) ``` ### Option 3: `fixest::feols()` Finally, we get back to the `fixest::feols()` function that we've already seen above. Truth be told, this is the IV option that I use most often in my own work. In part, this statement reflects the fact that I work mostly with panel data and will invariably be using **fixest** anyway. But I also happen to like its IV syntax a lot. The key thing is to specify the IV first-stage as a separate formula in the _final_ slot of the model call.^[This closely resembles [Stata's approach](https://www.stata.com/manuals13/rivregress.pdf) to writing out the IV first-stage, where you specify the endogenous variable(s) and the instruments together in a slot.] For example, if we had `fe` fixed effects, then the model call would be `y ~ ex | fe | en ~ in`. Since we don't have any fixed effects in our current cigarette demand example, the first-stage will come directly after the exogenous variables: ```{r iv_feols} # library(fixest) ## Already loaded iv_feols = feols( log(packs) ~ log(rincome) | ## y ~ ex log(rprice) ~ salestax, ## en ~ in (IV first-stage; must be the final slot) data = CigaretteDemand ) # summary(iv_feols, stage = 1) ## Show the 1st stage in detail iv_feols ``` Again, I emphasise that the IV first-stage must always come last in the `feols()` model call. Just to be pedantic --- but also to demonstrate how easy **fixest**'s IV functionality extends to panel settings --- here's a final `feols()` example. This time, I'll use a panel version of the same US cigarette demand data that includes entries from both 1985 and 1995. The dataset originally comes from the **AER** package, but we can download it from the web as follows. Note that I'm going to modify some variables to make it better comparable to our previous examples. ```{r cigs_panel} ## Get the data url = 'https://vincentarelbundock.github.io/Rdatasets/csv/AER/CigarettesSW.csv' cigs_panel = read.csv(url, row.names = 1) %>% mutate( rprice = price/cpi, rincome = income/population/cpi ) head(cigs_panel) ``` Let's run a panel IV now, where we'll explicitly account for year and state fixed effects. ```{r iv_feols_panel} iv_feols_panel = feols( log(packs) ~ log(rincome) | year + state | ## Now include FEs slot log(rprice) ~ taxs, ## IV first-stage still comes last data = cigs_panel ) # summary(iv_feols_panel, stage = 1) ## Show the 1st stage in detail iv_feols_panel ``` Good news, our coefficients are around the same magnitude. But the increased precision of the panel model has yielded gains in statistical significance. ## Other models ### Generalised linear models (logit, etc.) To run a generalised linear model (GLM), we use the in-built `glm()` function and simply assign an appropriate [family](https://stat.ethz.ch/R-manual/R-devel/library/stats/html/family.html) (which describes the error distribution and corresponding link function). For example, here's a simple logit model. ```{r logit, warning = FALSE} glm_logit = glm(am ~ cyl + hp + wt, data = mtcars, family = binomial) summary(glm_logit) ``` Alternatively, you may recall me saying earlier that **fixest** supports nonlinear models. So you could (in this case, without fixed-effects) also estimate: ```{r logit_fixest, warning = FALSE} feglm(am ~ cyl + hp + wt, data = mtcars, family = binomial) ``` Remember that the estimates above simply reflect the naive coefficient values, which enter multiplicatively via the link function. We'll get a dedicated section on extracting [marginal effects](#marginal-effects) from non-linear models in a moment. But I do want to quickly flag the **mfx** package ([link](https://cran.r-project.org/web/packages/mfx/vignettes/mfxarticle.pdf)), which provides convenient aliases for obtaining marginal effects from a variety of GLMs. For example, ```{r mfx_logit} # library(mfx) ## Already loaded ## Be careful: mfx loads the MASS package, which produces a namespace conflict ## with dplyr for select(). You probably want to be explicit about which one you ## want, e.g. `select = dplyr::select` ## Get marginal effects for the above logit model # logitmfx(am ~ cyl + hp + wt, atmean = TRUE, data = mtcars) ## Can also estimate directly logitmfx(glm_logit, atmean = TRUE, data = mtcars) ``` ### Bayesian regression We could spend a whole course on Bayesian models. The very, very short version is that R offers outstanding support for Bayesian models and data analysis. You will find convenient interfaces to all of the major MCMC and Bayesian software engines: [Stan](https://mc-stan.org/users/interfaces/rstan), [JAGS](http://mcmc-jags.sourceforge.net/), TensorFlow (via [Greta](https://greta-stats.org/)), etc. Here follows a *super* simple example using the **rstanarm** package ([link](http://mc-stan.org/rstanarm/)). Note that we did not install this package with the others above, as it can take fairly long and involve some minor troubleshooting.^[FWIW, on my machine (running Arch Linux) I had to install `stan` (and thus `rstanarm`) by running R through the shell. For some reason, RStudio kept closing midway through the installation process.] ```{r bayes_reg, message=F, warning=F, results="hide"} # install.packages("rstanarm") ## Run this first if you want to try yourself library(rstanarm) bayes_reg = stan_glm( mass ~ gender * height, data = humans, family = gaussian(), prior = cauchy(), prior_intercept = cauchy() ) ``` ```{r bayes_reg_summ, dependson=bayes_reg} summary(bayes_reg) ``` ### Even more models Of course, there are simply too many other models and other estimation procedures to cover in this lecture. A lot of these other models that you might be thinking of come bundled with the base R installation. But just to highlight a few, mostly new packages that I like a lot for specific estimation procedures: - Difference-in-differences (with variable timing, etc.): **did** ([link](https://github.com/bcallaway11/did)) and **DRDID** ([link](https://pedrohcgs.github.io/DRDID/)) - Synthetic control: **tidysynth** ([link](https://github.com/edunford/tidysynth)), **gsynth** ([link](https://yiqingxu.org/software/gsynth/gsynth_examples.html)) and **scul** ([link](https://hollina.github.io/scul/)) - Count data (hurdle models, etc.): **pscl** ([link](https://cran.r-project.org/web/packages/pscl/vignettes/countreg.pdf)) - Lasso: **biglasso** ([link](https://github.com/YaohuiZeng/biglasso)) - Causal forests: **grf** ([link](https://grf-labs.github.io/grf/)) - etc. Finally, just a reminder to take a look at the [Further Resources](#further-resources) links at the bottom of this document to get a sense of where to go for full-length econometrics courses and textbooks. ## Marginal effects Calculating marginal effects in a linear regression model like OLS is perfectly straightforward... just look at the coefficient values. But that quickly goes out the window when you have interaction terms or non-linear models like probit, logit, etc. Luckily, there are various ways to obtain these from R models. For example, we already saw the **mfx** package above for obtaining marginal effects from GLM models. I want to briefly focus on two of my favourite methods for obtaining marginal effects across different model classes: 1) The **margins** package and 2) a shortcut that works particularly well for models with interaction terms. ### The **margins** package The **margins** package ([link](https://cran.r-project.org/web/packages/margins)), which is modeled on its namesake in Stata, is great for obtaining marginal effects across an entire range of models.^[I do, however, want to flag that it does [not yet support](https://github.com/leeper/margins/issues/128) **fixest** (or **lfe**) models. But there are [workarounds](https://github.com/leeper/margins/issues/128#issuecomment-636372023) in the meantime.] You can read more in the package [vignette](https://cran.r-project.org/web/packages/margins/vignettes/Introduction.html), but here's a very simple example to illustrate. Consider our interaction effects regression [from earlier](#interaction-effects), where we were interested in how people's mass varied by height and gender. To get the average marginal effect (AME) of these dependent variables, we can just use the `margins::margins()` function. ```{r margins0, dependson=ols_ie} # library(margins) ## Already loaded ols_ie_marg = margins(ols_ie) ``` Like a normal regression object, we can get a nice print-out display of the above object by summarising or tidying it. ```{r margins1, dependson=ols_ie} # summary(ols_ie_marg) ## Same effect tidy(ols_ie_marg, conf.int = TRUE) ``` If we want to compare marginal effects at specific values --- e.g. how the AME of height on mass differs across genders --- then that's easily done too. ```{r margins2, dependson=ols_ie} ols_ie %>% margins( variables = "height", ## The main variable we're interested in at = list(gender = c("masculine", "feminine")) ## How the main variable is modulated by at specific values of a second variable ) %>% tidy(conf.int = TRUE) ## Tidy it (optional) ``` If you're the type of person who prefers visualizations (like me), then you should consider `margins::cplot()`, which is the package's in-built method for constructing *conditional* effect plots. ```{r margins3, dependson=ols_ie, dependson=humans} cplot(ols_ie, x = "gender", dx = "height", what = "effect", data = humans) ``` In this case,it doesn't make much sense to read a lot into the larger standard errors on the female group; that's being driven by a very small sub-sample size. Finally, you can also use `cplot()` to plot the predicted values of your outcome variable (here: "mass"), conditional on one of your dependent variables. For example: ```{r margins4, dependson=ols_ie, dependson=humans} par(mfrow=c(1, 2)) ## Just to plot these next two (base) figures side-by-side cplot(ols_ie, x = "gender", what = "prediction", data = humans) cplot(ols_ie, x = "height", what = "prediction", data = humans) par(mfrow=c(1, 1)) ## Reset plot defaults ``` Note that `cplot()` uses the base R plotting method. If you'd prefer **ggplot2** equivalents, take a look at the **marginsplot** package ([link](https://github.com/vincentarelbundock/marginsplot)). Finally, I also want to draw your attention to the **emmeans** package ([link](https://cran.r-project.org/web/packages/emmeans/index.html)), which provides very similar functionality to **margins**. I'm not as familiar with it myself, but I know that it has many fans. ### Special case: `/` shortcut for interaction terms {#nestedmarg} I'll keep this one brief, but I wanted to mention one of my favourite R shortcuts: Obtaining the full marginal effects for interaction terms by using the `/` expansion operator. I've [tweeted](https://twitter.com/grant_mcdermott/status/1202084676439085056?s=20) about this and even wrote an [whole blog post](https://grantmcdermott.com/2019/12/16/interaction-effects/) about it too (which you should totally read). But the very short version is that you can switch out the normal `f1 * x2` interaction terms syntax for `f1 / x2` and it automatically returns the full marginal effects. (The formal way to describe it is that the model variables have been "nested".) Here's a super simple example, using the same interaction effects model from before. ```{r nested, dependson=humans} # ols_ie = lm(mass ~ gender * height, data = humans) ## Original model ols_ie_marg2 = lm(mass ~ gender / height, data = humans) tidy(ols_ie_marg2, conf.int = TRUE) ``` Note that the marginal effects on the two gender × height interactions (i.e. `r round(ols_ie_marg2$coefficients[['genderfeminine:height']], 3)` and `r round(ols_ie_marg2$coefficients[['gendermasculine:height']], 3)`) are the same as we got with the `margins::margins()` function [above](#the-margins-package). Where this approach really shines is when you are estimating interaction terms in large models. The **margins** package relies on a numerical delta method which can be very computationally intensive, whereas using `/` adds no additional overhead beyond calculating the model itself. Still, that's about as much as say it here. Read my aforementioned [blog post](https://grantmcdermott.com/2019/12/16/interaction-effects/) if you'd like to learn more. ## Presentation ### Tables #### Regression tables There are loads of [different options](https://hughjonesd.github.io/huxtable/design-principles.html) here. We've already seen the excellent `etable()` function from **fixest** above.^[Note that `etable()` is limited to fixest models only.] However, my own personal favourite tool or creating and exporting regression tables is the **modelsummary** package ([link](https://vincentarelbundock.github.io/modelsummary)). It is extremely flexible and handles all manner of models and output formats. **modelsummary** also supports automated coefficient plots and data summary tables, which I'll get back to in a moment. The [documentation](https://vincentarelbundock.github.io/modelsummary/articles/modelsummary.html) is outstanding and you should read it, but here is a bare-boned example just to demonstrate. ```{r msum, warning=FALSE} # library(modelsummary) ## Already loaded ## Note: msummary() is an alias for modelsummary() msummary(list(ols1, ols_ie, ols_fe, ols_hdfe)) ```
One nice thing about **modelsummary** is that it plays very well with R Markdown and will automatically coerce your tables to the format that matches your document output: HTML, LaTeX/PDF, RTF, etc. Of course, you can also [specify the output type](https://vincentarelbundock.github.io/modelsummary/#saving-and-viewing-output-formats) if you aren't using R Markdown and want to export a table for later use. Finally, you can even specify special table formats like *threepartable* for LaTeX and, provided that you have called the necessary packages in your preamble, it will render correctly (see example [here](https://github.com/grantmcdermott/lecturenotes). #### Summary tables A variety of summary tables --- balance, correlation, etc. --- can be produced by the companion set of `modelsummary::datasummary*()` functions. Again, you should read the [documentation](https://vincentarelbundock.github.io/modelsummary/articles/datasummary.html) to see all of the options. But here's an example of a very simple balance table using a subset of our "humans" data frame. ```{r datsum} datasummary_balance(~ gender, data = humans %>% select(height:mass, birth_year, eye_color, gender)) ```
Another package that I like a lot in this regard is **vtable** ([link](https://nickch-k.github.io/vtable)). Not only can it be used to construct descriptive labels like you'd find in Stata's "Variables" pane, but it is also very good at producing the type of "out of the box" summary tables that economists like. For example, here's the equivalent version of the above balance table. ```{r st1} # library(vtable) ## Already loaded ## An additional argument just for formatting across different output types of ## this .Rmd document otype = ifelse(knitr::is_latex_output(), 'return', 'kable') ## st() is an alias for sumtable() st(humans %>% select(height:mass, birth_year, eye_color, gender), group = 'gender', out = otype) ```
Lastly, Stata users in particular might like the `qsu()` and `descr()` functions from the lightning-fast **collapse** package ([link](https://sebkrantz.github.io/collapse)). ### Figures #### Coefficient plots We've already worked through an example of how to extract and compare model coefficients [here](#comparing-our-model-coefficients). I use this "manual" approach to visualizing coefficient estimates all the time. However, our focus on **modelsummary** in the preceding section provides a nice segue to another one of the package's features: [`modelplot()`](https://vincentarelbundock.github.io/modelsummary/articles/modelplot.html). Consider the following, which shows both the degree to which `modelplot()` automates everything and the fact that it readily accepts regular **ggplot2** syntax. ```{r modplot, warning=FALSE} # library(modelsummary) ## Already loaded mods = list('FE, no clustering' = summary(ols_fe, se = 'standard'), # Don't cluster SEs 'HDFE, twoway clustering' = ols_hdfe) modelplot(mods) + ## You can further modify with normal ggplot2 commands... coord_flip() + labs( title = "'Effect' of height on mass", subtitle = "Comparing fixed effect models" ) ``` Or, here's another example where we compare the (partial) Masculine × Height coefficient from our earlier interaction model, with the (full) marginal effect that we obtained later on. ```{r modplot2} ie_mods = list('Partial effect' = ols_ie, 'Marginal effect' = ols_ie_marg2) modelplot(ie_mods, coef_map = c("gendermasculine:height" = "Masculine × Height")) + coord_flip() + labs( title = "'Effect' of height on mass", subtitle = "Comparing partial vs marginal effects" ) ``` #### Prediction and model validation The easiest way to visually inspect model performance (i.e. validation and prediction) is with **ggplot2**. In particular, you should already be familiar with `geom_smooth()` from our earlier lectures, which allows you to feed a model type directly in the plot call. For instance, using our `starwars2` data frame that excludes that slimy outlier, Jabba the Hutt: ```{r smooth, warning=FALSE} ggplot(starwars2, aes(x = height, y = mass)) + geom_point(alpha = 0.7) + geom_smooth(method = "lm") ## See ?geom_smooth for other methods/options ``` Now, I should say that `geom_smooth()` isn't particularly helpful when you've already constructed a (potentially complicated) model outside of the plot call. Similarly, it's not useful when you want to use a model for making predictions on a *new* dataset (e.g. evaluating out-of-sample fit). The good news is that the generic `predict()` function in base R has you covered. For example, let's say that we want to re-estimate our simple bivariate regression of mass on height from earlier.^[I'm sticking to a bivariate regression model for these examples because we're going to be evaluating a 2D plot below.] This time, however, we'll estimate our model on a training dataset that only consists of the first 30 characters ranked by height. Here's how you would do it. ```{r predict} ## Estimate a model on a training sample of the data (shortest 30 characters) ols1_train = lm(mass ~ height, data = starwars %>% filter(rank(height) <=30)) ## Use our model to predict the mass for all starwars characters (excl. Jabba). ## Note that I'm including a 95% prediction interval. See ?predict.lm for other ## intervals and options. predict(ols1_train, newdata = starwars2, interval = "prediction") %>% head(5) ## Just print the first few rows ``` Hopefully, you can already see how the above data frame could easily be combined with the original data in a **ggplot2** call. (I encourage you to try it yourself before continuing.) At the same time, it is perhaps a minor annoyance to have to combine the original and predicted datasets before plotting. If this describes your thinking, then there's even more good news because the **broom** package does more than tidy statistical models. It also ships the `augment()` function, which provides a convenient way to append model predictions to your dataset. Note that `augment()` accepts exactly the same arguments as `predict()`, although the appended variable names are slightly different.^[Specifically, we' re adding ".fitted", ".resid", ".lower", and ".upper" columns to our data frame. The convention adopted by `augment()` is to always prefix added variables with a "." to avoid overwriting existing variables.] ```{r augment1} ## Alternative to predict(): Use augment() to add .fitted and .resid, as well as ## .conf.low and .conf.high prediction interval variables to the data. starwars2 = augment(ols1_train, newdata = starwars2, interval = "prediction") ## Show the new variables (all have a "." prefix) starwars2 %>% select(contains("."), everything()) %>% head() ``` We can now see how well our model --- again, only estimated on the shortest 30 characters --- performs against all of the data. ```{r predict_plot, warning=FALSE} starwars2 %>% ggplot(aes(x = height, y = mass, col = rank(height)<=30, fill = rank(height)<=30)) + geom_point(alpha = 0.7) + geom_line(aes(y = .fitted)) + geom_ribbon(aes(ymin = .lower, ymax = .upper), alpha = 0.3, col = NA) + scale_color_discrete(name = "Training sample?", aesthetics = c("colour", "fill")) + labs( title = "Predicting mass from height", caption = "Line of best fit, with shaded regions denoting 95% prediction interval." ) ``` ## Further resources - [Ed Rubin](https://twitter.com/edrubin) has outstanding [teaching notes](http://edrub.in/teaching.html) for econometrics with R on his website. This includes both [undergrad-](https://github.com/edrubin/EC421S19) and [graduate-](https://github.com/edrubin/EC525S19)level courses. Seriously, check them out. - Several introductory texts are freely available, including [*Introduction to Econometrics with R*](https://www.econometrics-with-r.org/) (Christoph Hanck *et al.*), [*Using R for Introductory Econometrics*](http://www.urfie.net/) (Florian Heiss), and [*Modern Dive*](https://moderndive.com/) (Chester Ismay and Albert Kim). - [Tyler Ransom](https://twitter.com/tyleransom) has a nice [cheat sheet](https://github.com/tyleransom/EconometricsLabs/blob/master/tidyRcheatsheet.pdf) for common regression tasks and specifications. - [Itamar Caspi](https://twitter.com/itamarcaspi) has written a neat unofficial appendix to this lecture, [*recipes for Dummies*](https://itamarcaspi.rbind.io/post/recipes-for-dummies/). The title might be a little inscrutable if you haven't heard of the `recipes` package before, but basically it handles "tidy" data preprocessing, which is an especially important topic for machine learning methods. We'll get to that later in course, but check out Itamar's post for a good introduction. - I promised to provide some links to time series analysis. The good news is that R's support for time series is very, very good. The [Time Series Analysis](https://cran.r-project.org/web/views/TimeSeries.html) task view on CRAN offers an excellent overview of available packages and their functionality. - Lastly, for more on visualizing regression output, I highly encourage you to look over Chapter 6 of Kieran Healy's [*Data Visualization: A Practical Guide*](https://socviz.co/modeling.html). Not only will learn how to produce beautiful and effective model visualizations, but you'll also pick up a variety of technical tips.