This brief tutorial accompanies the UNM Stats R Package Development “R-Hack-A-Pack”. It is intended to run through the initial steps to creating the package structure, writing documented functions, handling the package description and namespace, including data, writing package vignettes, and creating attractive documentation.
An R package is a collection of documented functions and data that can be shared as a unit.
Packages typically fall into one of three categories:
Most of you may be developing personal packages at part of this workshop. It’s a good way to keep a collection of operations you do regularly, or do rarely but are tired of searching for the code each time. As you find or develop functions useful to you, you can continue to add to your package like a toolbox.
While the new usethis package has functions to automate many package development steps, we will be using RStudio to perform these steps within the GUI.
In this example I’ll create a personal package called EBEmisc to include a set of miscellaneous functions I use regularly. Frank Harrell’s Miscellaneous Hmisc package is famous so following his example, I’m going to create one with a similar name. Please choose a good descriptive name for your real package.
I recommend creating a directory to contain your package directory as well as other materials associated with the package. My other materials usually includes a test directory and a directory to hold reference material.
./PkgDevel
/EBEmisc
/test
/ref
I’ll start by creating the PkgDevel directory, and my test and ref subdirectories.
RStudio’s short tutorial is worth reading.
Make sure devtools is installed.
install.packages("devtools")
PkgDevel directory
Create a git repositoryIt will create a directory with your package name and populate it with the basic files you need for a package.
RStudio is now opened with the EBEmisc project in the upper right and the hello.R example function.
The hello.R has some documentation with keyboard shortcuts for building the package.
Go to the Build menu and select Configure Build Tools.
Select Generate documentation with Roxygen and check all the boxes. You may want to deselect Vignettes later if you have examples that take a long time to run.
Before we write any code, let’s make sure our package structure builds and loads. From the Build menu, select More > Clean and Rebuild.
Look through the log, the first line is the R command that was run (documentation). When that was completed, the INSTALL command was run. When the build was complete, in the console the R session restarted and the package was loaded.
devtools::document(roclets=c('rd', 'collate', 'namespace', 'vignette'))
Rcmd.exe INSTALL --preclean --no-multiarch --with-keep.source EBEmisc
library(EBEmisc)
Run the hello() function to see that it worked.
hello()
## Hello, world!
Look at the help for the hello() function. There isn’t much there.
?hello
Menu: Code > Insert Roxygen Skeleton (or Ctrl-Alt-Shift-R)
This adds special commented lines with an apostrophe above the function. By including documentation here, Roxygen will build the function documentation and also update the pesky NAMESPACE file correctly.
#' Title
#'
#' @return
#' @export
#'
#' @examples
hello <- function() {
print("Hello, world!")
}
I’m going to edit this function and add some documentation.
#' Prints 'Hello, world!' with "x".
#'
#' \code{hello} returns two strings pasted together
#'
#' @param x A string
#' @param sep A string separator passed to \code{paste()}
#'
#' @return NULL, side effect is printing string.
#' @export
#'
#' @examples
#' hello()
#' hello("Additional string.")
hello <- function(x = NULL, sep = " ") {
message(trimws(paste("Hello, world!", as.character(x), sep = sep), which = "right"))
invisible(NULL)
}
I’m going to rebuild the package and test the new function. From the Build menu, select More > Clean and Rebuild, or Ctrl-Shift-B.
Note: I received a couple warnings because two files that Roxygen wants to create were not originally created by Roxygen. I’m going to go delete these two files and build again. Typically this won’t be an issue because you won’t be editing hello.R with existing documentation, you’ll be creating your own new functions. This example is intended to get you in the habit of reading and responding to warning and error messages.
Warning: The existing 'hello.Rd' file was not generated by roxygen2, and will not be overwritten.
Warning: The existing 'NAMESPACE' file was not generated by roxygen2, and will not be overwritten.
I then deleted
./man/hello.Rd
./NAMESPACE
I rebuild with Ctrl-Shift-B. Now the documentation is updated and I get the output I expect by running the two examples in the documentation.
?hello
hello()
## Hello, world!
hello("Additional string.")
## Hello, world! Additional string.
We’re on our way!
This is the metadata for the package. I’ve edited the template RStudio started with and here’s mine.
Package: EBEmisc
Type: Package
Title: Erik Erhardt's miscellaneous functions
Version: 0.0.0.9000
Authors@R: person("Erik", "Erhardt", email = "erik@statacumen.com",
role = c("aut", "cre"))
URL: http://StatAcumen.com
Description: Contains many functions useful for data analysis,
high-level graphics, utility operations, and other functions.
License: GPL (>= 3)
Depends: R (>= 3.5.0)
Encoding: UTF-8
LazyData: true
RoxygenNote: 6.0.1
You’ll need to update the DESCRIPTION file in order to avoid warnings in the next step.
We can check the package to see whether it’s up to the standards for upload to CRAN. From the Build menu, select More > Check Package, or Ctrl-Shift-E. This runs a ton of checks and can take a long time if there are lots of examples to run in the function documentation. It’s worth reading through all these checks once to have a sense of what CRAN thinks is important.
Perfection!
R CMD check results
0 errors | 0 warnings | 0 notes
R CMD check succeeded
For installation, this single compressed file includes all package code, documenation, and data.
From the Build menu, select More > Build Source Package.
* building 'EBEmisc_0.0.0.9000.tar.gz'
Source package written to C: ... /PkgDevel
Note that the *.Rcheck folder includes details about the previous package check.
The data and data-raw folders are discussed below.
You may wish to give an overview of your entire package with more detail than your DESCRIPTION file’s Description.
The help file will be called EBEmisc-package, so I create a file called EBEmisc-package.R and provide a description that includes sections. You can list functions in here, describe methods, or anything else that you think will be important for your users to know.
#' EBEmisc: Erik Erhardt's miscellaneous functions
#'
#' The EBEmisc package provides functions that Erik uses for important things.
#'
#' @section Teaching functions:
#' The teaching functions include \code{t_dist_pval}, etc.
#'
#' @section Plotting functions:
#' The plotting functions include ..., etc.
#'
#' @docType package
#' @name EBEmisc
NULL
Read more about documentation.
This zzz.R function can provide a startup message for the package and set some package settings.
## Startup functions ------------------------------------
#' .onAttach start message
#'
#' @param libname defunct
#' @param pkgname defunct
#'
#' @return invisible()
.onAttach <- function(libname, pkgname) {
start_message <- c( "EBEmisc: Erik Erhardt's miscellaneous functions\n\n"
, " by Dr. Erik Barry Erhardt <erik@StatAcumen.com>\n\n"
)
packageStartupMessage(start_message)
invisible()
}
#' .onLoad getOption package settings
#'
#' @param libname defunct
#' @param pkgname defunct
#'
#' @return
#'
#' @examples
#' getOption("EBEmisc.name")
.onLoad <- function(libname, pkgname) {
op <- options()
op.EBEmisc <- list(
#EBEmisc.path = "~/R-dev",
EBEmisc.install.args = "",
EBEmisc.name = "Erik Barry Erhardt",
EBEmisc.desc.author = "Erik Erhardt <erik@StatAcumen.com> [aut, cre]",
EBEmisc.desc.license = "GPL-3",
EBEmisc.desc.suggests = NULL,
EBEmisc.desc = list()
)
toset <- !(names(op.EBEmisc) %in% names(op))
if (any(toset)) options(op.EBEmisc[toset])
invisible()
}
Knowing how to manage the DESCRIPTION and NAMESPACE for importing certain operators can be challenging. The usethis package provides solutions to lots of issues. One that I struggled with was using the %>% infix operator from the magrittr package and popularized in the tidyverse. If I want to use this in my functions within my package, then running the command
usethis::use_pipe()
will create a R/utils-pipe.R function with the appropriate imports and update the DESCRIPTION file to include
Imports:
magrittr
Since there are many package details to keep straight, it’s wonderful to have these utility functions to simplify tedious steps.
Writing robust code is tedious and time consuming. If you want to help your users use your code correctly and avoid errors, or worse passing objects as input that seem to return results but produce erroneous inference, then there are a few things you can do. First, document your code very well. Second, produce warnings and errors when appropriate.
Be verbose – write more than you think you need to about each argument to any functions you make available to the user and try to really explain things using simpler words and concepts that people who do not have a huge amount of programming experience can understand. I have found this to be a real frustration when learning other languages and people on StackOverflow have answered my question but in words I do not understand.
Give very simple working examples – you should seek to give toy examples using your code that cover all relevant use cases, yet are simple enough that it is apparent what is happening. Comment this code as well!
Give very clear names to variables – this will help your users keep track of what data is supposed to go where.
You’ll help your users by including conditional statements when issues could arise, such as input of the wrong class or results that can’t be correct.
# stopping on error
if(class(dat) != "factor"){
stop("dat input must be a factor variable.")
}
## Error: dat input must be a factor variable.
# warning for unusual situation
# in this case, we expect the result to be nonnegative
if(result < 0){
warning("The result was unexpectedly less than zero, check your input for the cause.")
}
## Warning message:
## The result was unexpectedly less than zero, check your input for the cause.
For my Advanced Data Analysis 1 class I wrote a function t.dist.pval to plot the p-value tail area for a one-sample t-test. Below is my documented function where I replaced old-style period variable.names with underscore variable_names. I create an empty t_dist_pval.R file and copy this function in there. Then I insert the Roxygen Skeleton with Ctrl-Alt-Shift-R and add a title and examples. Finally, I build the package, check the function documentation, and run the examples to check the results.
#' Function to plot t-distribution with shaded p-value
#'
#' @param t_summary is the output object from the \code{stats::t.test} function.
#'
#' @return NULL invisibly
#' @export
#'
#' @examples
#' x <- rnorm(30)
#' t_dist_pval(t.test(x, mu = 0.2))
#' t_dist_pval(t.test(x, mu = 0.2, alternative = "less"))
#' t_dist_pval(t.test(x, mu = 0.2, alternative = "greater"))
t_dist_pval <- function(t_summary) {
par(mfrow=c(1,1))
lim_extreme <- max(4, abs(t_summary$statistic) + 0.5)
lim_lower <- -lim_extreme
lim_upper <- lim_extreme
x_curve <- seq(lim_lower, lim_upper, length=200)
y_curve <- dt(x_curve, df = t_summary$parameter)
plot(x_curve, y_curve, type = "n"
, ylab = paste("t-dist( df =", signif(t_summary$parameter, 3), ")")
, xlab = paste("t-stat =", signif(t_summary$statistic, 5)
, ", Shaded area is p-value =", signif(t_summary$p.value, 5)))
if ((t_summary$alternative == "less")
| (t_summary$alternative == "two.sided")) {
x_pval_l <- seq(lim_lower, -abs(t_summary$statistic), length=200)
y_pval_l <- dt(x_pval_l, df = t_summary$parameter)
polygon(c(lim_lower, x_pval_l, -abs(t_summary$statistic))
, c(0, y_pval_l, 0), col="gray")
}
if ((t_summary$alternative == "greater")
| (t_summary$alternative == "two.sided")) {
x_pval_u <- seq(abs(t_summary$statistic), lim_upper, length=200)
y_pval_u <- dt(x_pval_u, df = t_summary$parameter)
polygon(c(abs(t_summary$statistic), x_pval_u, lim_upper)
, c(0, y_pval_u, 0), col="gray")
}
points(x_curve, y_curve, type = "l", lwd = 2, col = "black")
invisible(NULL)
}
Build the package, check the help, and run the examples.
?t_dist_pval
x <- rnorm(30)
t_dist_pval(t.test(x, mu = 0.2))
t_dist_pval(t.test(x, mu = 0.2, alternative = "less"))
t_dist_pval(t.test(x, mu = 0.2, alternative = "greater"))
For a package I often have some raw data that I want to include, but it needs some formatting for use. First create a folder to keep your raw data, this directory is data-raw.
devtools::use_data_raw()
## * Creating `data-raw`.
## * Adding `data-raw` to `.Rbuildignore`.
## Next:
## * Add data creation scripts in data-raw
## * Use devtools::use_data() to add data to package
Now you can write a script to process the data and create the formatted object you want to include as data available in your package.
As a trivial example, let’s create a little data frame and a list and include those in the package.
dat1 <-
dplyr::tibble(
x = sample(1000)
, y = sample(letters, size = 1000, replace = TRUE)
)
dat2 <- list(a = 1:5, b = LETTERS[10:20], c = NA)
# save data in ./data directory
devtools::use_data(dat1, dat2)
## Saving dat1, dat2 as dat1.rda, dat2.rda to C: ... \PkgDevel\EBEmisc/data
When we rebuild the package, the two datasets dat1 and dat2 are now available as objects.
Read more about data.
A vignette is a worked example to help introduce your package’s functionality to your users. Start by creating a template vignette file (EBEmisc_teach.Rmd) in a new ./vignette directory.
devtools::use_vignette("EBEmisc_teach")
## * Adding `inst/doc` to ./.gitignore
Rebuilding the package will compile the vignette (since we checked that option earlier). Have a look at the html file it produces then edit it to demonstrate the use of your functions.
pdgdown for attractive documentationTo build a very attractive set of webpages with your package documentation, use pkgdown. This produces documentation in a new ./docs folder. You can upload the contents of this folder to a website.
pkgdown::build_site()
See the pkgdown documentation for EBEmisc.
There is a lot we haven’t covered.
I recommend reading Hadley Wickham’s R packages all the way through and flesh out your package with what you learn in there.