ETC5523: Communicating with Data

R Packages and documentation

Lecturer: Michael Lydeamore

Department of Econometrics and Business Statistics



Aim

  • Document your functions
  • Document data and longer workflows
  • Check that your package works
  • Share your package and its documentation

Why

  • Reference documentation answers specific questions
  • READMEs and vignettes help new users get started
  • Package checks catch problems before users encounter them
  • GitHub and pkgdown make your work installable and discoverable

Let’s make a package

This is a taste of ETC4500/5450 (Advanced R Programming). We will learn the bare minimum required to get to documentation.

Communicating about your R package

  • What is the goal of the package?
  • What does your function(s) do?
  • How do we use it?
  • Why should we use it?
  • Where do we find and install it?

Documentation is vital

ausbirthplace package

It’s never been easier to make a package. The usethis package can do all of the heavy lifting for us

Tip

This means we can easily make packages that might only be just “for us”

This package is for teaching demo only

usethis::create_package("ausbirthplace")

will set up the directory structure we need for a package.

Package directory structure

- R/
- data/
- data-raw/
- man/
- vignettes/
- DESCRIPTION
- NAMESPACE
- README.md

Tip

Different forms of communication live in different parts of the package.

Functions for the package

Functions in the R/ folder become part of the package.

Tip

Add @export to functions that users should call directly.

Data for the package

Your package could load in some data (like palmerpenguins::penguins for example).

User-facing data objects go in data; the scripts that produce them go in data-raw.

usethis::use_data_raw("census-birthplace")

The generated script should finish by saving the object into the package:

usethis::use_data(censusdata, overwrite = TRUE)

Load the package while developing

devtools lets us load the package without installing it first:

devtools::load_all()
censusdata

You can also use the default hotkey Ctrl+Shift+L to load.

You should reload often - try to avoid manually sourcing files unless you have to!

NAMESPACE, DESCRIPTION, README

Three files play different communication roles:

  • NAMESPACE: machine-generated instructions about exported functions and imports
  • DESCRIPTION: structured metadata about the package and its dependencies
  • README: the introduction people see before they install the package

README

Hadley gives a good template for a README:

  1. A paragraph describing the purpose of the package
  2. An example that shows how to use the package to solve a simple problem
  3. Installation instructions that you can copy/paste straight into R
  4. An overview of the main components of the package.

We can set up the README using

usethis::use_readme_rmd()

and then edit to suit your package.

DESCRIPTION

DESCRIPTION tells tools and users what the package is:

Package: mypackage
Title: What the Package Does (One Line, Title Case)
Version: 0.0.0.9000
Authors@R: 
    person("First", "Last", role = c("aut", "cre"))
Description: What the package does (one paragraph).
License: MIT + file LICENSE
Imports: ggplot2

Important

Replace every placeholder; a valid package needs a meaningful title and description.

Choose a licence

A licence tells other people what they may do with your code.

usethis::use_mit_license("Your Name")

Tip

For Assignment 4, include a licence file rather than leaving the generated placeholder in DESCRIPTION.

Using other packages

Inevitably, you’ll want to use a function from another package. Again there’s a usethis function for that too:

usethis::use_package("ggplot2")

You still have to reference exported functions using ::, or import specific functions with @importFrom.

For example:

plot_census_data <- function() {
  censusdata |>
    ggplot2::ggplot(ggplot2::aes(x = count, y = birth)) +
    ggplot2::geom_col() +
    ggplot2::labs(x = "Number of residents", y = "Country of birth")
}

Code commenting

Comments in code are communication. There’s no need to re-state very clear code, but if you are doing anything even mildly complicated, chances are you won’t be able to understand the code quickly.

Example:

palmerpenguins::penguins |>
  dplyr::select(species, bill_depth_mm, bill_length_mm, flipper_length_mm, island) |>
  tidyr::pivot_longer(!c(species, island)) |>
  dplyr::group_by(species, island) |>
  dplyr::summarise(means = mean(value, na.rm = TRUE)) |>
  tidyr::pivot_wider(names_from = species, values_from = means)

Code commenting

Comments in code are communication. There’s no need to re-state very clear code, but if you are doing anything even mildly complicated, chances are you won’t be able to understand the code quickly.

Example:

palmerpenguins::penguins |>
  dplyr::select(species, bill_depth_mm, bill_length_mm, flipper_length_mm, island) |>
  # Reshape the measurements so they can be summarised together
  tidyr::pivot_longer(!c(species, island)) |>
  dplyr::group_by(species, island) |>
  dplyr::summarise(means = mean(value, na.rm = TRUE)) |>
  # Present species as columns for easier comparison
  tidyr::pivot_wider(names_from = species, values_from = means)

Code commenting

Useful comments explain intent, assumptions, or a surprising decision.

Avoid narrating code that is already clear. As with all communication, conciseness and clarity are key.

Function documentation

When we don’t know how to use a function, we look up the function documentation by typing ?group_by.

But where does this documentation come from?

R gives a standard way of documenting packages: you write .Rd files in the man/ directory. These files use a custom syntax, which is very similar to LaTeX.

This separates code from comments, making it easy to forget to update a function’s documentation if you change the function. roxygen2 puts the documentation with your code, and generates .Rd files.

roxygen2 skeleton

#' Function title
#' 
#' Function description
#' 
#' @param param_name Parameter description
#' @return What does the function return?
#' @examples
#' R code for examples goes here
myfunction <- function(param_name) {
  print(param_name)
}

A function worth documenting

percent_change <- function(old, new, digits = 1) {
  if (any(old == 0)) {
    stop("old cannot contain zero")
  }
  round((new - old) / old * 100, digits = digits)
}

Before reading the source, what would a user need to know?

Describe purpose, inputs, and output

#' Calculate percentage change
#' 
#' Calculates change from an old value to a new value, relative
#' to the old value. Positive results indicate an increase.
#'
#' @param old Numeric vector of baseline values; must not contain zero.
#' @param new Numeric vector of new values.
#' @param digits Number of decimal places used to round the result.
#' @return A numeric vector giving percentage change relative to `old`.
#' @export
percent_change <- function(old, new, digits = 1) {
  # ...
}

Examples teach use and interpretation

#' @examples
#' percent_change(80, 100)
#' # A 25% increase
#'
#' percent_change(c(80, 50), c(100, 45), digits = 0)
#' # A 25% increase followed by a 10% decrease

Tip

Examples should be runnable and help users interpret the result.

Generating the .Rd file

Just writing roxygen2 doesn’t generate the man files. To get these, we have to run

devtools::document()

If you are in RStudio, you can press ctrl+shift+d

Important

For a function to be accessible by installing your package, you need to include the @export tag!

Data documentation

Just like functions, we have to document our data. You can put this anywhere (after all, there is no function that generates our data), but the convention is in R/data.R.

For our data,

#' Number of Australian residents by country of birth from the 2016 and 2021 census
#' 
#' @format A data frame with 105 rows and 4 columns:
#' \describe{
#'   \item{birth}{Country of birth}
#'   \item{count}{Number of residents from that birth country}
#'   \item{percentage}{Percentage of residents from that birth country}
#'   \item{census}{Year of census (either 2016 or 2021)}
#' }
#' @source Australian Bureau of Statistics, Census data,
#'   <https://www.abs.gov.au/census/find-census-data>
"censusdata"

Vignettes

Documentation is great to look at something specific (like a function).

What if you’ve just installed a package and would like to know how to use it?

Enter the Vignette

Vignettes

Vignettes are “how-to” guides to packages. Perhaps they document a specific workflow, or how to solve a specific problem.

Check out the dplyr vignettes

Match the document to the user’s question

User’s question Best starting point
Why might I use this package? README
How do I complete this workflow? Vignette
What does this function or argument do? Reference documentation

Tip

Good packages provide routes for both new and returning users.

Writing vignettes

Write for a reader who may never have used your package before.

  • Begin with the reader’s goal, not the package structure
  • Ask someone to follow the vignette without your help

Tip

If a workflow is difficult to explain, the package may also be difficult to use.

Vignettes: How to

usethis::use_vignette("getting-started.qmd")

This creates a Quarto vignette in vignettes/ and adds the required package metadata.

From package to audience

Check the whole package

Documentation that renders is not enough: check the package as a complete product.

devtools::document()
devtools::check()

Important

Fix every error and warning. Read every note and decide whether it needs action.

Make the package installable

Once the package is in a public GitHub repository, another user can install it with:

remotes::install_github("username/packagename")

They should not need your source files, working directory, or instructions sent separately.

Tip

Put a copyable installation command in the README.

Publish the documentation with pkgdown

pkgdown combines your package metadata and documentation into a website.

usethis::use_pkgdown_github_pages()

This configures a GitHub Action that rebuilds and publishes the site when the package changes.

One package, several routes for users

DESCRIPTION + README + man/ + vignettes/

checked package + installable GitHub repository + pkgdown website

Tip

Keep the source documents in the package; regenerate the outputs when the source changes.

Which document would you open?

  1. You have never heard of the package and want to know why it exists.
  2. You need to check what the digits argument accepts.
  3. You want to complete a multi-step analysis.
  4. You want to install the package from GitHub.

README → reference page → vignette → README installation instructions

Week 8 lesson

Summary

  • Packages communicate through code, metadata, reference pages, examples, and longer guides.
  • Write from the user’s task: explain purpose, inputs, outputs, restrictions, and interpretation.
  • Run document() and check() before sharing the package.
  • GitHub makes the package installable; pkgdown makes its documentation accessible.

Appendix: Custom print methods

Extension: Custom print method (not assessable)

What happens when you type print(object) in R?

  • print is a function
print
function (x, ...) 
UseMethod("print")
<bytecode: 0x564d5f541410>
<environment: namespace:base>

Methods and OOP in R

print() is an S3 generic.

We can’t cover S3 precisely here (come back in Advanced R Programming for that), but we can think of it as:

a different print function is called depending on what object looks like.

Which method is chosen is based on the class of object.

Methods and OOP in R

fit <- lm(bill_length_mm ~ bill_depth_mm, data = palmerpenguins::penguins)
class(fit)
[1] "lm"
head(methods("print"))
[1] "print.acf"               "print.activeConcordance"
[3] "print.AES"               "print.anova"            
[5] "print.aov"               "print.aovlist"          

Methods and OOP in R

We can prepend our own class while preserving the object’s existing classes.

class(fit) <- c("myclass", class(fit))

print.myclass <- function(x, ...) {
  cat("A fitted model with", length(coef(x)), "coefficients\n")
  invisible(x)
}

print(fit)
A fitted model with 2 coefficients

Custom print methods

The print method selects what a user needs to see; it does not change the underlying object.

print methods are communication too. Compare the user-facing summary with the internal component names:

print(fit)

Call:
lm(formula = bill_length_mm ~ bill_depth_mm, data = palmerpenguins::penguins)

Coefficients:
  (Intercept)  bill_depth_mm  
      55.0674        -0.6498  
head(names(unclass(fit)), 6)
[1] "coefficients"  "residuals"     "effects"       "rank"         
[5] "fitted.values" "assign"       

Writing print methods

All of our communication principles apply to print methods:

  • Important information first
  • Unnecessary information hidden
  • Conciseness
  • Clarity

Often this will need multiple rounds of feedback (like all writing).

Including print methods in packages

To include a custom print method in a package, document it with @export. roxygen2 will usually generate the required S3 registration in NAMESPACE.

This is out of scope for the unit, but you can read more in the roxygen2 S3 documentation.