In R, we do things using functions, as soon as we use it beyond a calculator.
Modularity: break a complex problem into small, manageable pieces. During development you focus on one function at a time instead of the whole project.
Scope management: functions provide local scope. Objects created inside a function stay inside it; only inputs and outputs touch the global environment.
Reusability, the DRY principle: “Don’t Repeat Yourself”. When something changes, you edit one definition instead of the same code in ten places.
Calling functions
We have already seen and used a multitude of functions in R.
Some come pre-packaged with base R (e.g. mean())
Others come from external packages (e.g. mvtnorm::rmvnorm())
Regardless of where they come from, functions in R all adopt the same basic syntax:
function_name(ARGUMENTS)
Writing your own functions
Most of the time we rely on functions that other people have written for us. But you can, and should, write your own too, with the generic function() function.1
function(ARGUMENTS) { OPERATIONSreturn(VALUE)}
1 Yes, it is a function that lets you write functions. Very meta.
Name your functions
Writing anonymous functions like the above is possible and reasonably common. But we typically write functions because we want to reuse code, and for that it makes sense to name them.1
Short functions need neither the curly brackets nor an explicit return object, so you can write them on a single line:
my_short_func =function(ARGUMENTS) OPERATION
1 Remember: “In R, everything is an object and everything has a name.”
Name your functions (cont.)
Tip
Try to give your functions short, pithy names that are informative to both you and anyone else reading your code. This is harder than it sounds, but will pay off down the road.
Two things we still owe you, and which the next few slides deliver:
when the curly brackets and the explicit return() actually matter;
what happens to the objects created inside the brackets.
Simple example: addition
# Built-in functionsum(c(3, 4))
## [1] 7
# User-defined functionadd_func <-function(x, y) { total <- x + y ## Create an intermediary object (that will be returned)return(total) ## The value(s) or object(s) that we want returned.}add_func(3, 4)
## [1] 7
Return explicitly
Caution
R’s default behaviour is to automatically return the final object that you created within the function. However, this won’t always be the case. Get into the habit of assigning the return object(s) explicitly.
An explicit return() is also how we hand back more than one object:
add_func <-function(x, y) { total <- x + yreturn(list(sum = total, first_element = x, second_element = y))}add_func(3, 4)
None of the intermediate objects created inside the functions above (total, etc.) made their way into our global environment. Confirm this in the “Environment” pane of RStudio.
R has a set of lexical scoping rules governing where it stores and evaluates the values of different objects.
Functions operate in a quasi-sandboxed environment.
They do not return or use objects in the global environment unless forced to, e.g. by a return() command.
A function only looks to outside environments, e.g. a level “up”, if it does not see the object named within itself.
Example: OLS estimation
Consider a simple linear regression model with a constant term and one regressor:
y_i = \beta_0 + \beta_1 x_i + \varepsilon_i
y_i: the dependent variable
x_i: the independent variable
\beta_0: the intercept, \beta_1: the slope coefficient
To conduct OLS estimation in R, we literally translate this mathematical expression into code.
Step 1: simulate the data
We need data Y and X to run OLS, so we simulate an artificial dataset.
# simulate dataset.seed(111) # can be removed to allow the result to change# set the parametersn <-100b0 <-matrix(1, nrow =2)# generate the datae <-rnorm(n)X <-cbind(1, rnorm(n))Y <- X %*% b0 + e
Step 1: simulate the data (cont.)
# Snapshot of X (first 10 rows):print(head(X, 10))
Jim Hester’s lookup package does the legwork for you
Packages
R’s package ecosystem
A base R installation is relatively compact. R’s true power lies in its extensive ecosystem of add-on packages, most of them hosted on CRAN, the Comprehensive R Archive Network.
A common practice in the statistical community: researchers publish an R package alongside the academic paper.
New statistical methods become immediately accessible to practitioners.
A virtuous cycle: researchers gain visibility, users get cutting-edge tools within months, sometimes weeks, of publication.
Install and load packages
A package is installed by install.packages("package_name") and invoked by library(package_name).
To install ggplot2, either:
Console: enter install.packages("ggplot2").
RStudio: click the “Packages” tab in the bottom-right pane, then “Install”, and search for the package.
Install and load packages (cont.)
Loading packages
Once installed, load a package into your R session with library().
library(ggplot2)
Note
Notice that you do not need quotes around the package name any more.
Reason: R now recognizes the package as a defined object with a given name. (“Everything in R is an object and everything has a name.”)
Shortcuts and namespaces
Tip
A convenient way to combine installation and loading is the pacman package’sp_load() function. pacman::p_load(ggplot2) first checks whether it needs to install the package, then loads it. Clever.
We can also run a function from an installed package without loading it, using PACKAGE::package_function():
stats::sd(1:10)
## [1] 3.02765
Packages beyond CRAN
Many authors distribute packages through GitHub or their personal websites, especially for cutting-edge research or development versions.
Install them with devtools or remotes.
Follow the instructions on the project’s repository, e.g. RDHonest.
if (!requireNamespace("remotes")) {install.packages("remotes")}remotes::install_github("kolesarm/RDHonest")
Keep your R session’s working directory at the package root.
Iterate with devtools::load_all() for fast feedback.
Run devtools::check() often to catch issues early.
Step 1: create a package skeleton
usethis::create_package("~/dev/eco4370") # choose your path# RStudio will open the new project; from there:usethis::use_git() # initialize Git repousethis::use_mit_license("Your Name") # add a license
usethis::use_package("forcats") # adds to Imports# call explicitly as forcats::fct_unify(...),# unless you import specific functions with @importFrom in roxygen
Best practices
Prefer pkg::fun() in code.
Avoid library() inside package code.
Keep Imports: tidy and alphabetical in DESCRIPTION.
Step 5: project hygiene and style
Group related functions together. Avoid one giant file, and avoid one file per tiny function.
Avoid global side-effects: no setwd(), options(), par(), Sys.setenv(), set.seed() in package code.
Never use source() or library() inside R/ package code.
Style regularly:
styler::style_pkg()
Step 6: README, GitHub, and visibility
usethis::use_readme_rmd()devtools::build_readme()usethis::use_github() # creates remote, sets origin, and optionally pushes
This makes your package discoverable and easier to collaborate on.
Step 7: vignettes
Vignette = long-form documentation.
usethis::use_vignette("eco4370-intro")# Knit within the vignette to preview; pkg build will render it
Step 7: data
User-facing datasets go in data/ via use_data(), documented in R/data.R. Never @export datasets.
Internal data for functions: use_data(..., internal = TRUE) writes R/sysdata.rda, no help file needed.
Raw sources and the scripts that create data: data-raw/, added to .Rbuildignore.
#' Small Car Dataset#'#' A tiny excerpt of mtcars for examples.#'#' @format A data frame with 8 rows and 4 variables.#' @source Base R `mtcars`"cars_small"
Step 8: check, install, build
devtools::check() # CRAN-style QAdevtools::install() # local installdevtools::build() # source tarball; build(binary = TRUE) for binary on your OS
Know the package states:
source → bundled → binary → installed → in-memory
Worked mini-example: eco4370
A compact, end-to-end demo that you can reproduce.
# file: R/data.R#' Small Car Dataset#'#' A tiny excerpt of mtcars for examples.#'#' @format A data frame with 8 rows and 4 variables.#' @source Base R `mtcars`"cars_small"
For internal helper data: usethis::use_data(obj, internal = TRUE) writes R/sysdata.rda, no help file.
5) Vignette
usethis::use_vignette("getting-started")
Edit vignettes/getting-started.Rmd:
# markdown---title:"Getting Started with eco4370"output: rmarkdown::html_vignettevignette:> %\VignetteIndexEntry{Getting Started} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8}---
5) Vignette: basic usage
add(10, 5)movavg(1:10, k =3)cars_small
Preview by knitting. When building the package, vignettes are rendered automatically.
6) README and GitHub, 7) final checks
usethis::use_readme_rmd()devtools::build_readme()usethis::use_github() # requires a GitHub account + token set up
A modern approach to developing R packages is through literate programming.
The litr package, by Jacob Bien, lets you write a complete R package inside a single R Markdown document.
The package is created when you knit the Rmd file.
For larger packages, you can write a bookdown that defines the package.
Interested? The videos show how it works in a few minutes.
Control Flow
Controlling the flow
Now that we have a good sense of the basic function syntax, it is time to learn control flow: controlling the order, or “flow”, of the statements and operations that our functions evaluate.
This is common to all programming languages.
if is used for choice.
for and while are used for loops.
if
The basic syntax for an if statement:
if (CONDITION) { OPERATION}
square_root <-function(x) {if (x <0) {message("x is negative")return(NA) }return(sqrt(x))}square_root(9)
## [1] 3
square_root(-9)
## x is negative
## [1] NA
if ... else
We can extend this with an else statement to handle alternative conditions:
To guard against this behaviour, and to add some other optimisations, both the tidyverse (through dplyr) and data.table offer their own versions of ifelse.
As you may have guessed, it is certainly possible to write nested ifelse() statements:
ifelse(CONDITION1, DO IF TRUE, ifelse(CONDITION2, DO IF TRUE, ifelse(...)))
or
if (CONDITION1) { DO IF TRUE} elseif (CONDITION2) { DO IF TRUE} else { DO IF FALSE}
However, these nested statements quickly become difficult to read and troubleshoot.
case when
A better solution was originally developed in SQL with the CASE WHEN statement. Both dplyr (case_when()) and data.table (fcase()) provide implementations in R.
x <-1:10## dplyr::case_when()case_when( x <=3~"small", x <=7~"medium",TRUE~"big"## Default value. Could also write `x > 7 ~ "big"` here.)
When we use loops, we should minimize the tasks within the loop.
Pre-specify the dimension and allocate memory for variables outside the loop.
In cases where we want to “grow” an object via a for loop, we first have to create an empty (or NULL) object, and R re-allocates memory at every iteration.
Let us measure how much this actually costs.
Example: empirical coverage of a confidence interval
CI <-function(x) {# construct confidence interval# x is a vector of random variables n <-length(x) mu <-mean(x) sig <-sd(x) upper <- mu +1.96/sqrt(n) * sig lower <- mu -1.96/sqrt(n) * sigreturn(list(lower = lower, upper = upper))}Rep <-100000sample_size <-1000mu <-2
Option 1: append a new outcome after each loop
pts0 <-Sys.time() # check timefor (i in1:Rep) { x <-rpois(sample_size, mu) bounds <-CI(x) out_i <- ((bounds$lower <= mu) & (mu <= bounds$upper))if (i ==1) { out <- out_i } else { out <-c(out, out_i) }}mean(out)
## [1] 0.95023
cat("Takes", Sys.time() - pts0, "seconds\n")
## Takes 7.476734 seconds
Option 2: initialize the result vector
# Initialize the result vectorout <-rep(0, Rep)pts0 <-Sys.time() # check timefor (i in1:Rep) { x <-rpois(sample_size, mu) bounds <-CI(x) out[i] <- ((bounds$lower <= mu) & (mu <= bounds$upper))}mean(out)
## [1] 0.94893
cat("Takes", Sys.time() - pts0, "seconds\n")
## Takes 2.480385 seconds
Identical answer, and the empirical coverage sits close to the nominal 95%. Pre-allocating is the cheaper route.
Vectorization
Before we write any loops, we should ask ourselves: “Do I need to iterate at all?”
R is vectorized. Often you can apply a function to every element of a vector at once, rather than one at a time.
In R and other high-level languages, for loops can be slow. When you loop through many elements and perform an expensive operation on each one, your code may take a long time to execute.
Many operations typically implemented with for-loops can be rewritten using vectorized operations or matrix computations.
Whenever possible, leverage R’s built-in vectorization capabilities for better performance.
lpm <-function(n) {# set the parameters b0 <-matrix(c(-1, 1), nrow =2)# generate the data from a Probit model e <-rnorm(n) X <-cbind(1, rnorm(n)) Y <-as.numeric(X %*% b0 + e >=0)# note that in this regression bhat does not converge to b0# because the model is mis-specified# OLS estimation bhat <-solve(t(X) %*% X, t(X) %*% Y) e_hat <- Y - X %*% bhatreturn(list(X = X, e_hat =as.vector(e_hat)))}
Set up
# Set upn <-50Rep <-1000data.Xe <-lpm(n)X <- data.Xe$Xe_hat <- data.Xe$e_hat
Functional programming (FP) is arguably the most important thing you can take away from today’s lecture. Hadley Wickham puts the core idea like this in Advanced R:
R, at its heart, is a functional programming (FP) language. […] R has what’s known as first class functions.
You can do anything with a function that you can do with a vector:
assign it to a variable,
store it in a list,
pass it as an argument to another function,
create it inside a function,
return it as the result of a function.
Hadley explains it better
Why not for loops?
Summary:for loops emphasise the objects we are working with (say, a vector of numbers) rather than the operations we want to apply to them (get the mean, or the median, or whatever).
This is inefficient: it requires us to write out the for loop by hand rather than getting an R function to create the loop for us.
As a corollary, for loops pollute our global environment with counting variables. Look at your “Environment” pane: i is sitting there, equal to the last value of its loop.
Creating those auxiliary variables is almost certainly not an intended outcome, and they can cause errors when we inadvertently refer to a similarly-named variable elsewhere in our script. So we best remove them as soon as we are finished.
rm(i)
Why not for loops? (cont.)
Another annoyance arrived in cases where we want to “grow” an object as we iterate over it. To do that with a for loop, we had to create an empty object first.
FP lets us avoid the explicit loop construct and its associated downsides. In practice, there are two ways to implement FP in R:
No i in your global environment. Because of R’s lexical scoping rules, any object created and invoked by a function is evaluated in a sandbox outside your global environment.
The syntax barely changed when switching from for() to lapply(). The essential structure is the same: first the iteration list (1:10), then the desired function or operation (LETTERS[i]).
The returned object is a list.lapply() accepts vectors, data frames and lists as arguments, but always returns a list, with one element per iteration of the loop. (So now you know where the “l” in “lapply” comes from.)
Getting output that is not a list
Several options exist.1 The one I use most commonly is to bind the list elements into a single data frame with dplyr::bind_rows() or data.table::rbindlist().
lapply(1:10, function(i) { df <-tibble(num = i, let = LETTERS[i])return(df)}) %>%bind_rows()
## # A tibble: 10 × 2
## num let
## <int> <chr>
## 1 1 A
## 2 2 B
## 3 3 C
## 4 4 D
## 5 5 E
## 6 6 F
## 7 7 G
## 8 8 H
## 9 9 I
## 10 10 J
1 E.g. pipe the output to unlist() if you want a vector, or use sapply(), which is covered next.
Why lapply() anyway?
The default list-return behaviour may not sound ideal at first, but I use lapply() more frequently than any of the other apply family members.
My functions normally return multiple objects of different type, which makes a list the only sensible format;
or they return a single data frame, which is where dplyr::bind_rows() and data.table::rbindlist() come in.
The *apply family: sapply()
sapply() stands for “simplify apply”. It is essentially a wrapper around lapply that tries to return simplified output that matches the input type. Feed it a vector and it will try to return a vector.
sapply(1:10, function(i) LETTERS[i])
## [1] "A" "B" "C" "D" "E" "F" "G" "H" "I" "J"
The *apply family: apply()
apply() applies functions over array margins: the rows or columns of matrices and data frames.
# Create a simple matrixmat <-matrix(1:12, nrow =3, ncol =4)print(mat)
For a concise overview of the different *apply() functions, see this blog post by Neil Saunders.
2) The purrr package
purrr’s map() is the tidyverse’s lapply(). Same syntax, same list output:
map(1:10, function(i) { ## only need to swap `lapply` for `map` df <-tibble(num = i, let = LETTERS[i])return(df)})
## [[1]]
## # A tibble: 1 × 2
## num let
## <int> <chr>
## 1 1 A
##
## [[2]]
## # A tibble: 1 × 2
## num let
## <int> <chr>
## 1 2 B
##
## [[3]]
## # A tibble: 1 × 2
## num let
## <int> <chr>
## 1 3 C
##
## [[4]]
## # A tibble: 1 × 2
## num let
## <int> <chr>
## 1 4 D
##
## [[5]]
## # A tibble: 1 × 2
## num let
## <int> <chr>
## 1 5 E
##
## [[6]]
## # A tibble: 1 × 2
## num let
## <int> <chr>
## 1 6 F
##
## [[7]]
## # A tibble: 1 × 2
## num let
## <int> <chr>
## 1 7 G
##
## [[8]]
## # A tibble: 1 × 2
## num let
## <int> <chr>
## 1 8 H
##
## [[9]]
## # A tibble: 1 × 2
## num let
## <int> <chr>
## 1 9 I
##
## [[10]]
## # A tibble: 1 × 2
## num let
## <int> <chr>
## 1 10 J
map_df(): pick your output type
map() comes with its own variants for returning objects of a desired type. purrr::map_df() returns a data frame:
map_df(1:10, function(i) { ## don't need bind_rows with `map_df` df <-tibble(num = i, let = LETTERS[i])return(df)})
## # A tibble: 10 × 2
## num let
## <int> <chr>
## 1 1 A
## 2 2 B
## 3 3 C
## 4 4 D
## 5 5 E
## 6 6 F
## 7 7 G
## 8 8 H
## 9 9 I
## 10 10 J
More efficient, i.e. less typing, than the lapply() version: no extra row-binding step at the end. Jenny Bryan’s purrr tutorial goes much deeper.
Create and iterate over named functions
We can split the function and the iteration (and binding) into separate steps. This is generally a good idea, since you typically create named functions with the goal of reusing them.
## Create a named functionnum_to_alpha <-function(i) { df <-tibble(num = i, let = LETTERS[i])return(df) }
Create and iterate over named functions (cont.)
Now we can easily iterate over our function using different input values.
lapply(1:10, num_to_alpha) %>%bind_rows()
## # A tibble: 10 × 2
## num let
## <int> <chr>
## 1 1 A
## 2 2 B
## 3 3 C
## 4 4 D
## 5 5 E
## 6 6 F
## 7 7 G
## 8 8 H
## 9 9 I
## 10 10 J
map_df(c(1, 5, 26, 3), num_to_alpha)
## # A tibble: 4 × 2
## num let
## <dbl> <chr>
## 1 1 A
## 2 5 E
## 3 26 Z
## 4 3 C
Iterate over multiple inputs
Thus far we have fed our functions a single input when iterating. What if we want to iterate over multiple inputs? Consider a function that takes x and y, combines them in a data frame, and uses them to create a third variable z.
## Create a named functionmulti_func <-function(x, y) { df <-tibble(x = x, y = y) %>%mutate(z = (x + y) /sqrt(x))return(df) }
Another silly function that we could easily improve upon with standard vectorised tools. The goal is to demonstrate programming principles with simple examples that carry over to complicated cases where vectorisation is not possible.
Test it before iterating
multi_func(1, 6)
## # A tibble: 1 × 3
## x y z
## <dbl> <dbl> <dbl>
## 1 1 6 7
Great, it works. Two basic approaches let us iterate over various levels of both x and y:
Use base::mapply() or purrr::pmap().
Use a data frame of input combinations.
1) mapply() and pmap()
Both base R, through mapply(), and purrr, through pmap(), handle multiple inputs. The latter is easier to work with, since its syntax is nearly identical to the single-input case.
## Note that the inputs are now moved to the *end* of the call.## Also, mapply() is based on sapply(), so we also have to tell it not to## simplify if we want to keep the list structure.mapply( multi_func,x =1:5, ## Our "x" vector inputy =6:10, ## Our "y" vector inputSIMPLIFY =FALSE## Tell it not to simplify to keep the list structure) %>%bind_rows()
## # A tibble: 5 × 3
## x y z
## <int> <int> <dbl>
## 1 1 6 7
## 2 2 7 6.36
## 3 3 8 6.35
## 4 4 9 6.5
## 5 5 10 6.71
1) mapply() and pmap() (cont.)
Second, purrr::pmap():
## Note that the inputs are combined in a list.pmap_df(list(x =1:5, y =6:10), multi_func)
## # A tibble: 5 × 3
## x y z
## <int> <int> <dbl>
## 1 1 6 7
## 2 2 7 6.36
## 3 3 8 6.35
## 4 4 9 6.5
## 5 5 10 6.71
2) Using a data frame of input combinations
Both approaches above work perfectly well, but in practice I prefer to “cheat” by feeding a multi-input function a single data frame that specifies the necessary combination of variables by row. Why?
Length safety. No accidentally feeding separate inputs of different lengths. Try the above with an x input of 1:10, everything else unchanged: pmap() at least fails with a helpful message, but mapply() completes with totally misaligned columns. A rectangular data frame forces equal lengths a priori.
All combinations. I often need to run a function over all possible combinations of inputs, so it is convenient to use that data frame directly.1
Simplicity. One input is simpler and cleaner. This matters most with complicated functions that nest heavily and/or parallelize.
1base::expand.grid(), and the equivalent tidyr::expand_grid() and data.table::CJ(), automatically generate a data frame of all combinations.
2) Using a data frame of input combinations (cont.)
parent_func <-## Main function: Takes a single data frame as an inputfunction(input_df) { df <-## Nested iteration functionmap_df(1:nrow(input_df), ## i.e. Iterate (map) over each row of the input data framefunction(n) {## Extract the `x` and `y` values from row "n" of the data frame x <- input_df$x[n] y <- input_df$y[n]## Use the extracted values df <-multi_func(x, y)return(df) } )return(df) }
Three conceptual steps
Create a new function parent_func(), which takes a single input: a data frame containing x and y columns (and potentially other columns too).
This input data frame is passed to a second, nested function, which iterates over the rows of the data frame.
During each iteration, the x and y values for that row are passed to our original multi_func(), which returns a data frame containing the desired output.
Test it with two input data frames
## Case 1: Iterate over x=1:5 and y=6:10input_df1 <-tibble(x =1:5, y =6:10)parent_func(input_df1)
## # A tibble: 5 × 3
## x y z
## <int> <int> <dbl>
## 1 1 6 7
## 2 2 7 6.36
## 3 3 8 6.35
## 4 4 9 6.5
## 5 5 10 6.71
## Case 2: Iterate over *all possible## combinations* of x=1:5 and y=6:10input_df2 <-expand.grid(x =1:5, y =6:10)parent_func(input_df2)
tell R to run the iteration in parallel, i.e. plan(multisession);
slightly amend the lapply() call, i.e. future_lapply().
And for the purrr crowd
Prefer the purrr::map() family and feeling left out? Don’t worry: the furrr package has you covered.
Once again, the syntax for these parallel functions is very little changed from their serial versions.
Tell R you want things in parallel with plan(multisession), then amend the map call to future_map_dfr(). The extra “r” tells future to concatenate the data frames from each iteration by rows.
R has elegant built-in statistical functions. Four prefixes go ahead of the name of a probability distribution:
p: probability, i.e. the CDF
d: density for a continuous random variable, or mass for a discrete one
q: quantile
r: random variable generator
Distribution names include norm (normal), chisq (\chi^2), t (t), weibull (Weibull), cauchy (Cauchy), binomial (binomial), pois (Poisson), to name a few.
Example: sampling error
Plot the density of \chi^2(3) over an equally spaced grid x_axis = seq(0.01, 15, by = 0.01).
Generate 1000 observations from \chi^2(3) and plot their histogram.
Note how little changes from lm(): same formula interface, plus a family argument.
Logit: results
summary(glm_fit)
##
## Call:
## glm(formula = participation ~ age + education + youngkids + oldkids +
## foreign, family = "binomial", data = SwissLabor)
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) 2.085961 0.540535 3.859 0.000114 ***
## age -0.527066 0.089670 -5.878 4.16e-09 ***
## education -0.001298 0.027502 -0.047 0.962371
## youngkids -1.326957 0.177893 -7.459 8.70e-14 ***
## oldkids -0.072517 0.071878 -1.009 0.313024
## foreignyes 1.353614 0.198598 6.816 9.37e-12 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 1203.2 on 871 degrees of freedom
## Residual deviance: 1069.9 on 866 degrees of freedom
## AIC: 1081.9
##
## Number of Fisher Scoring iterations: 4
We will revisit the regression models in much more detail in the later lectures.
R Markdown and Quarto
R Markdown
A document format that combines the power of R programming with the simplicity of Markdown syntax. Code, results, and narrative text coexist seamlessly.
R Markdown provides an authoring framework for data science. You can use a single R Markdown file to both
save and execute code
generate high quality reports that can be shared with an audience
Key features of R Markdown
Reproducible research: your analysis and results are automatically updated when you change your code or data.
Multiple output formats: HTML, PDF, Word documents, presentations and more, all from the same source.
Code integration: embed R code chunks that execute and display results inline with your text.
Version control friendly: the plain text format works well with Git and other version control systems.
Basic structure
An R Markdown document consists of three main components:
YAML header: metadata and output options, enclosed by ---
Markdown text: regular text formatted with Markdown syntax
Code chunks: R code blocks enclosed by triple backticks with {r}
This deck, and the long-form note it mirrors, are themselves such documents.
Code chunks
Code chunks are the heart of R Markdown. They allow you to:
execute R code and display results;
control output with chunk options, e.g. echo=FALSE, eval=FALSE;