Lecturer: Michael Lydeamore
Department of Econometrics and Business Statistics
Aim
Why
pkgdown make your work installable and discoverableThis is a taste of ETC4500/5450 (Advanced R Programming). We will learn the bare minimum required to get to documentation.
Documentation is vital
pokemonvgc packageLast 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
will set up the directory structure we need for a package.
- R/
- data/
- data-raw/
- man/
- vignettes/
- DESCRIPTION
- NAMESPACE
- README.md
Tip
Different forms of communication live in different parts of 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.
Our package will include two user-facing data objects:
pokemon_elo: Elo histories derived from the match historyrecent_champions: the champion lookup used last weekUser-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.
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"
)Keeping this lookup as data separates championship status from the rating calculation.
This is the 47 frequently observed players plus the four recent champions represented in the archive.
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.
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.
devtools lets us load the package without installing it first:
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!
Three files play different communication roles:
NAMESPACE: machine-generated instructions about exported functions and importsDESCRIPTION: structured metadata about the package and its dependenciesREADME: the introduction people see before they install the packageHadley gives a good template for a 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:
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.
A licence tells other people what they may do with your code.
Tip
For Assignment 4, include a licence file rather than leaving the generated placeholder in DESCRIPTION.
Inevitably, you’ll want to use a function from another package. Again there’s a usethis function for that too:
For now, reference exported functions using ::. After introducing roxygen2, we will import selected names where that makes package code easier to read.
For example:
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:
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)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.
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 skeletonrecent_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?
#' @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.
| 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.
#' 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
#' 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 thresholdTip
Examples should be runnable and help users interpret the result.
.Rd fileJust 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!
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.
#' 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.
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 are “how-to” guides to packages. Perhaps they document a specific workflow, or how to solve a specific problem.
Check out the dplyr vignettes
| 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.
Write for a reader who may never have used your package before.
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.
This creates a Quarto vignette in vignettes/ and adds the required package metadata.
Documentation that renders is not enough: check the package as a complete product.
Important
Fix every error and warning. Read every note and decide whether it needs action.
Once the package is in a public GitHub repository, another user can install it with:
They should not need your source files, working directory, or instructions sent separately.
Tip
Put a copyable installation command in the README.
pkgdownpkgdown combines your package metadata and documentation into a website.
This configures a GitHub Action that rebuilds and publishes the site when the package changes.
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.
min_events argument controls.README → ?recent_players → Elo-plot vignette → README installation instructions
Summary
document() and check() before sharing the package.pkgdown makes its documentation accessible.What happens when you type print(object) 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.
We can prepend our own class while preserving the object’s existing classes.
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:
All of our communication principles apply to print methods:
Often this will need multiple rounds of feedback (like all writing).
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.

ETC5523 Week 8