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

pokemonvgc package

Last week we rescued a plot of Pokémon Video Game Championship Elo ratings. We will turn that work into a small package so that someone else can derive the ratings, use the data, and repeat the analysis.

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("pokemonvgc")

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.

For pokemonvgc, we will export recent_players(). It reduces rating histories to the most recent observation for each player with enough recorded events.

Tip

Add @export to functions that users should call directly.

Data for the package

Our package will include two user-facing data objects:

  • pokemon_elo: Elo histories derived from the match history
  • recent_champions: the champion lookup used last week

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

The match history is a raw input to the build process, not a dataset shipped to package users.

The source pipeline downloads and caches VGC History’s tournament catalogue and per-event match files, then validates and normalises them.

usethis::use_data_raw("pokemon-elo")

Build the Elo histories

The generated script records how cached match data become package data objects:

source("data-raw/calculate-elo.R")

# Created by the cached VGC History download and validation pipeline
match_history <- readRDS("data-raw/vgc-match-history.rds")
players <- readRDS("data-raw/vgc-players.rds")

ratings <- calculate_elo(match_history) |>
  tibble::as_tibble() |>
  dplyr::group_by(player_id, date) |>
  dplyr::summarise(
    rating = round(dplyr::last(rating), 1),
    .groups = "drop"
  )

Include recent champions

recent_champions <- tibble::tribble(
  ~year, ~player_id,          ~player_name,
  2022,  "eduardo-cunha",    "Eduardo Cunha",
  2023,  "shohei-kimura",    "Shohei Kimura",
  2024,  "luca-ceribelli",   "Luca Ceribelli",
  2025,  "giovanni-cischke", "Giovanni Cischke",
  2026,  "takuma-yamazaki",  "Takuma Yamazaki"
)

Keeping this lookup as data separates championship status from the rating calculation.

Reproduce the workshop snapshot

frequent_players <- ratings |>
  dplyr::count(player_id) |>
  dplyr::filter(n > 35) |>
  dplyr::pull(player_id)

selected_players <- union(frequent_players, recent_champions$player_id)

This is the 47 frequently observed players plus the four recent champions represented in the archive.

Finish and save the package data

pokemon_elo <- ratings |>
  dplyr::filter(player_id %in% selected_players) |>
  dplyr::left_join(
    dplyr::select(players, player_id, player_name),
    by = "player_id"
  ) |>
  dplyr::select(player_id, player_name, date, rating)

usethis::use_data(
  pokemon_elo, recent_champions,
  overwrite = TRUE
)

This reproduces the 51 players and 2,043 observations used last week. The 2026 champion remains in the lookup but has no rating history in this snapshot.

Which code belongs where?

Tip

Keep downloading, caching, one-off cleaning, and the Elo calculation in data-raw/. These steps build the package data but are not part of its user-facing interface.

Note

Put recent_players() in R/recent-players.R and export it: selecting the latest eligible players is a task package users will repeat.

Load the package while developing

devtools lets us load the package without installing it first:

devtools::load_all()
pokemon_elo

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.

README

For pokemonvgc, the opening should tell readers that it contains VGC match data, derived Elo histories, and recent champion metadata. Then show the shortest useful example:

library(pokemonvgc)

recent_players(pokemon_elo) |>
  dplyr::slice_max(rating, n = 3)

DESCRIPTION

DESCRIPTION tells tools and users what the package is:

Package: pokemonvgc
Title: Explore Competitive Pokemon VGC Results
Version: 0.0.0.9000
Authors@R: 
    person("First", "Last", role = c("aut", "cre"))
Description: Provides competitive Pokemon VGC match data, derived Elo
    rating histories, recent champion metadata, and tools for identifying
    each eligible player's latest rating.
License: MIT + file LICENSE
Imports:
    dplyr

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("dplyr")

For now, reference exported functions using ::. After introducing roxygen2, we will import selected names where that makes package code easier to read.

For example:

recent_players <- function(ratings, min_events = 15) {
  ratings |>
    dplyr::group_by(player_id, player_name) |>
    dplyr::mutate(events = dplyr::n()) |>
    dplyr::slice_max(date, n = 1, with_ties = FALSE) |>
    dplyr::ungroup() |>
    dplyr::filter(events >= min_events)
}

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:

pokemon_elo |>
  dplyr::group_by(player_id, player_name) |>
  dplyr::mutate(events = dplyr::n()) |>
  dplyr::slice_max(date, n = 1, with_ties = FALSE) |>
  dplyr::ungroup() |>
  dplyr::filter(events >= 15) |>
  dplyr::slice_max(rating, n = 3, with_ties = FALSE)

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:

pokemon_elo |>
  dplyr::group_by(player_id, player_name) |>
  # Count events before keeping only each player's latest rating
  dplyr::mutate(events = dplyr::n()) |>
  dplyr::slice_max(date, n = 1, with_ties = FALSE) |>
  dplyr::ungroup() |>
  # Match the workshop definition of an established current player
  dplyr::filter(events >= 15) |>
  dplyr::slice_max(rating, n = 3, with_ties = FALSE)

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

recent_players <- function(ratings, min_events = 15) {
  ratings |>
    dplyr::group_by(player_id, player_name) |>
    dplyr::mutate(events = dplyr::n()) |>
    dplyr::slice_max(date, n = 1, with_ties = FALSE) |>
    dplyr::ungroup() |>
    dplyr::filter(events >= min_events)
}

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

Import only the functions you use

#' @importFrom dplyr filter group_by mutate n slice_max ungroup
recent_players <- function(ratings, min_events = 15) {
  ratings |>
    group_by(player_id, player_name) |>
    mutate(events = n()) |>
    slice_max(date, n = 1, with_ties = FALSE) |>
    ungroup() |>
    filter(events >= min_events)
}

@importFrom lets package code use those selected names without repeating dplyr::.

Tip

Prefer selective @importFrom declarations to importing an entire package with @import dplyr.

Dependency, import, generation

Instruction Role
usethis::use_package("dplyr") Records dplyr in DESCRIPTION
@importFrom dplyr ... Declares names used inside package code
devtools::document() Writes importFrom(...) entries to NAMESPACE

Important

Do not edit NAMESPACE by hand. If an imported name is ambiguous, keep the explicit package::function() form.

Describe purpose, inputs, and output

#' Find each eligible player's most recent rating
#' 
#' Keeps the latest rating for every player with at least `min_events`
#' observations in the supplied rating histories.
#'
#' @param ratings A data frame containing `player_id`, `player_name`,
#'   `date`, and `rating`.
#' @param min_events Minimum number of observations a player must have.
#' @return A data frame with one row per eligible player, including their
#'   most recent date and rating and their total number of events.
#' @importFrom dplyr filter group_by mutate n slice_max ungroup
#' @export
recent_players <- function(ratings, min_events = 15) {
  # ...
}

Examples teach use and interpretation

#' @examples
#' recent_players(pokemon_elo)
#' # Each established player's latest available rating
#'
#' recent_players(pokemon_elo, min_events = 10) |>
#'   dplyr::slice_max(rating, n = 3)
#' # The current top three under a lower eligibility threshold

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,

#' Pokemon Video Game Championship Elo rating histories
#' 
#' @format A data frame with 2,043 rows and 4 columns:
#' \describe{
#'   \item{player_id}{Stable player identifier}
#'   \item{player_name}{Player display name}
#'   \item{date}{Date of the rated event}
#'   \item{rating}{Estimated Elo rating after the event}
#' }
#' @details Elo is estimated from the available match archive and is not
#'   an official ranking. Coverage varies across events.
#' @source Match data compiled by VGC History,
#'   <https://vgchistory.com/data>
"pokemon_elo"

Every user-facing data object needs its own documentation block. Raw inputs kept only in data-raw/, such as match_history, are documented for package developers rather than exposed through the package help system.

Document the champion lookup

#' Recent Pokemon VGC World Champions
#'
#' @format A data frame with 5 rows and 3 columns:
#' \describe{
#'   \item{year}{Championship year}
#'   \item{player_id}{Stable player identifier}
#'   \item{player_name}{Player display name}
#' }
#' @source VGC History, <https://vgchistory.com/>
"recent_champions"

Tip

Metadata can include a player who has no rows in the rating snapshot. Document that distinction rather than silently dropping them.

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

For pokemonvgc, the Week 7 task becomes a guide: Rescue a Pokémon Elo plot.

Tip

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

Vignettes: How to

usethis::use_vignette("rescue-an-elo-plot.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("MikeLydeamore/pokemonvgc")

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 min_events argument controls.
  3. You want to reproduce the multi-step Elo plot from last week.
  4. You want to install the package from GitHub.

README → ?recent_players → Elo-plot 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: 0x55af58ee70e0>
<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.