From dc0cbc85b535cd9971092d08c0c11b915459c239 Mon Sep 17 00:00:00 2001 From: Adam Blake Date: Thu, 5 Oct 2023 22:33:09 -0700 Subject: [PATCH 1/9] perf: :recycle: reduce expensive lookups #CKR-79 --- NAMESPACE | 6 +- R/coursekata_attach.R | 30 +++------ R/coursekata_packages.R | 18 +++-- R/{theme_coursekata.R => theme.R} | 16 ++--- R/utils-pkg.R | 66 ++++++++----------- R/zzz.R | 23 ++----- README.Rmd | 2 +- man/coursekata_attach.Rd | 7 +- ...hments.Rd => coursekata_attach_message.Rd} | 8 +-- ...ata_themes.Rd => coursekata_load_theme.Rd} | 14 ++-- man/coursekata_palette.Rd | 2 +- man/coursekata_palette_provider.Rd | 2 +- ...t_themes.Rd => coursekata_unload_theme.Rd} | 12 ++-- man/m_predictor.Rd | 18 +++++ man/pkg_is_installed.Rd | 4 +- man/pkg_library_location.Rd | 4 +- man/pkg_version.Rd | 4 +- man/possibly_pkg_status.Rd | 4 +- man/scale_discrete_coursekata.Rd | 2 +- man/split_data.Rd | 19 ++++++ man/test_fit.Rd | 21 ++++++ man/theme_coursekata.Rd | 4 +- tests/testthat/helper-test_utilities.R | 19 +++--- tests/testthat/test-coursekata_attach.R | 14 ++-- tests/testthat/test-coursekata_packages.R | 12 +--- tests/testthat/test-gf_model.R | 16 ----- tests/testthat/test-utils-pkg.R | 4 +- 27 files changed, 184 insertions(+), 167 deletions(-) rename R/{theme_coursekata.R => theme.R} (95%) rename man/{coursekata_attachments.Rd => coursekata_attach_message.Rd} (74%) rename man/{load_coursekata_themes.Rd => coursekata_load_theme.Rd} (56%) rename man/{restore_default_themes.Rd => coursekata_unload_theme.Rd} (57%) create mode 100644 man/m_predictor.Rd create mode 100644 man/split_data.Rd create mode 100644 man/test_fit.Rd diff --git a/NAMESPACE b/NAMESPACE index 7477955..28e6126 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -7,23 +7,25 @@ export(b0) export(b1) export(coursekata_attach) export(coursekata_install) +export(coursekata_load_theme) export(coursekata_packages) export(coursekata_palette) export(coursekata_palette_provider) export(coursekata_repos) +export(coursekata_unload_theme) export(coursekata_update) export(f) export(fVal) export(gf_model) export(gf_model_old) -export(load_coursekata_themes) export(lower) export(middle) export(p) export(pre) -export(restore_default_themes) export(scale_discrete_coursekata) +export(split_data) export(tails) +export(test_fit) export(theme_coursekata) export(upper) import(rlang) diff --git a/R/coursekata_attach.R b/R/coursekata_attach.R index b2288b0..f8531da 100644 --- a/R/coursekata_attach.R +++ b/R/coursekata_attach.R @@ -1,31 +1,24 @@ #' Attach the CourseKata course packages -#' -#' @param startup Is this being run at start-up? -#' -#' @return An object with info about which course packages are installed and attached. +#' @return The packages that were attached. #' @export -#' #' @examples #' coursekata_attach() -coursekata_attach <- function(startup = FALSE) { +coursekata_attach <- function() { to_attach <- coursekata_detached() - if (length(to_attach)) { - suppressPackageStartupMessages(pkg_require(to_attach)) - } - - coursekata_attachments(startup) + suppressPackageStartupMessages(pkg_require(to_attach)) + invisible(to_attach) } #' Information about CourseKata packages. -#' -#' @param startup Is this being run at start-up? -#' +#' @param pkgs A character vector of packages being loaded. #' @return A coursekata_attachments object, also of class data.frame with a row for each course #' package and a column for each of the `package` name, `version`, and whether it is currently #' `attached`. #' @keywords internal -coursekata_attachments <- function(startup = FALSE) { +coursekata_attach_message <- function(pkgs) { + if (length(pkgs) == 0) return(NULL) + is_dark_theme <- rstudioapi::isAvailable() && rstudioapi::hasFun("getThemeInfo") && rstudioapi::getThemeInfo()$dark @@ -39,7 +32,7 @@ coursekata_attachments <- function(startup = FALSE) { cli::ansi_align(version, max(cli::ansi_nchar(version))) )) - msg <- paste( + paste( cli::rule( left = cli::style_bold("CourseKata packages"), right = cli::style_bold("coursekata ", utils::packageVersion("coursekata")) @@ -47,12 +40,9 @@ coursekata_attachments <- function(startup = FALSE) { to_cols(pkgs, 2), sep = "\n" ) - - if (startup) packageStartupMessage(msg) else message(msg) - - invisible(info) } + themes <- list( light = list( text = cli::col_black, diff --git a/R/coursekata_packages.R b/R/coursekata_packages.R index d76e35b..1b024e1 100644 --- a/R/coursekata_packages.R +++ b/R/coursekata_packages.R @@ -1,9 +1,10 @@ # reversed so that most important packages are loaded last (and mask earlier ones) -coursekata_pkg_list <- rev(c( +coursekata_pkgs <- rev(c( "supernova", "mosaic", "lsr", "Metrics", "fivethirtyeight", "fivethirtyeightdata", "Lock5withR", "dslabs" )) + #' List all CourseKata course packages #' #' @param check_remote_version Should the remote version number be checked? Requires internet, and @@ -16,12 +17,13 @@ coursekata_pkg_list <- rev(c( #' @examples #' coursekata_packages() coursekata_packages <- function(check_remote_version = FALSE) { - pkgs <- coursekata_pkg_list + pkgs <- coursekata_pkgs + statuses <- pak::pkg_status(pkgs) info <- data.frame( package = pkgs, - installed = pkg_is_installed(pkgs), + installed = pkg_is_installed(pkgs, statuses), attached = pkg_is_attached(pkgs), - version = pkg_version(pkgs), + version = pkg_version(pkgs, statuses), stringsAsFactors = FALSE ) @@ -35,20 +37,16 @@ coursekata_packages <- function(check_remote_version = FALSE) { #' List all currently attached CourseKata course packages -#' #' @return A character vector of the course packages that have been attached. #' @keywords internal coursekata_attached <- function() { - info <- coursekata_packages() - info$package[info$attached] + coursekata_pkgs[pkg_is_attached(coursekata_pkgs)] } #' List all currently NOT attached CourseKata course packages -#' #' @return A character vector of the course packages that are not attached. #' @keywords internal coursekata_detached <- function() { - info <- coursekata_packages() - info$package[!info$attached] + coursekata_pkgs[!pkg_is_attached(coursekata_pkgs)] } diff --git a/R/theme_coursekata.R b/R/theme.R similarity index 95% rename from R/theme_coursekata.R rename to R/theme.R index 86b815a..d2af6c5 100644 --- a/R/theme_coursekata.R +++ b/R/theme.R @@ -3,7 +3,7 @@ #' The `coursekata` package automatically loads this theme when the package is loaded. This is in #' addition to a number of other plot tweaks and option settings. To just restore the theme to the #' default, you can run `set_theme(theme_grey)`. If you want to restore all plot related settings -#' and/or prevent them when loading the package, see [`restore_default_themes`]. +#' and/or prevent them when loading the package, see [`coursekata_unload_theme`]. #' #' @return A gg theme object #' @export @@ -146,15 +146,15 @@ scale_discrete_coursekata <- function(...) { #' Utility function for loading all themes. #' #' This function is called at package start-up and should rarely be needed by the user. The -#' exception is when the user has called [`restore_default_themes()`] and wants to go back to the +#' exception is when the user has called [`coursekata_unload_theme()`] and wants to go back to the #' CourseKata look and feel. When run, this function sets the CourseKata color palettes #' [`coursekata_palette()`], sets the default theme to [`theme_coursekata()`], and tweaks some #' default settings for specific plots. To restore the original `ggplot2` settings, run -#' [`restore_default_themes()`]. +#' [`coursekata_unload_theme()`]. #' -#' @seealso coursekata_palette theme_coursekata scale_discrete_coursekata restore_default_themes +#' @seealso coursekata_palette theme_coursekata scale_discrete_coursekata coursekata_unload_theme #' @export -load_coursekata_themes <- function() { +coursekata_load_theme <- function() { ggplot2::update_geom_defaults("bar", ggplot2::aes( `colour` = "black", `fill` = coursekata_palette(1), @@ -227,11 +227,11 @@ load_coursekata_themes <- function() { #' #' This function will restore all of the tweaks to themes and plotting to the original `ggplot2` #' defaults. If you want to go back to the CourseKata look and feel, run -#' [`load_coursekata_themes()`]. +#' [`coursekata_load_theme()`]. #' -#' @seealso load_coursekata_themes +#' @seealso coursekata_load_theme #' @export -restore_default_themes <- function() { +coursekata_unload_theme <- function() { # find these values by creating a plot, storing it to a variable, and, e.g. # p$layers[[1]]$geom$default_aes ggplot2::update_geom_defaults("bar", ggplot2::aes( diff --git a/R/utils-pkg.R b/R/utils-pkg.R index 496916f..ed39726 100644 --- a/R/utils-pkg.R +++ b/R/utils-pkg.R @@ -1,7 +1,5 @@ #' Check if packages are attached -#' #' @param pkgs Character vector of the names of the packages to check. -#' #' @return Logical vector indicating whether the packages are attached or not. #' @keywords internal pkg_is_attached <- function(pkgs) { @@ -16,76 +14,75 @@ pkg_is_attached <- function(pkgs) { #' than a single Boolean value regarding the packages as a set). #' #' @param pkgs Character vector of the names of the packages to check. -#' +#' @param statuses The output of [`pak::pkg_status()`] (computed if not supplied). #' @return Logical vector indicating whether the packages are installed. #' @keywords internal -pkg_is_installed <- function(pkgs) { - pkgs %in% pak::pkg_status(pkgs)$package +pkg_is_installed <- function(pkgs, statuses = NULL) { + statuses <- if (is.null(statuses)) pak::pkg_status(pkgs) else statuses + pkgs %in% statuses$package } #' Determine which libraries packages were loaded from -#' #' @param pkgs A character vector of packages to check. -#' +#' @param statuses The output of [`pak::pkg_status()`] (computed if not supplied). #' @return A character vector of library directory paths the packages were loaded from, the default #' location if the package is not loaded but is installed, or NA if the package is not installed. #' @keywords internal -pkg_library_location <- function(pkgs) { - possibly_pkg_status(pkgs, "library") +pkg_library_location <- function(pkgs, statuses = NULL) { + possibly_pkg_status(pkgs, "library", statuses = statuses) } #' Get the local package version numbers. -#' #' @param pkgs A character vector of packages to look up. -#' +#' @param statuses The output of [`pak::pkg_status()`] (computed if not supplied). #' @return A character vector of the package versions. If the package is already loaded, this is #' pulled from the library the package was loaded from, else the default library location. #' @keywords internal -pkg_version <- function(pkgs) { - possibly_pkg_status(pkgs, "version") +pkg_version <- function(pkgs, statuses = NULL) { + possibly_pkg_status(pkgs, "version", statuses = statuses) } #' Attempt to get package information using `pak`, but fail with NA -#' #' @param pkgs A character vector of packages to look up. #' @param status_key The column to get from the [`pak::pkg_status()`] output. -#' +#' @param statuses The output of [`pak::pkg_status()`] (computed if not supplied). #' @return A character vector with each package's status information. #' @keywords internal -possibly_pkg_status <- function(pkgs, status_key) { - purrr::map_chr(pkgs, function(pkg) { - x <- pak::pkg_status(pkg)[[status_key]] - if (length(x)) x else NA_character_ - }) +possibly_pkg_status <- function(pkgs, status_key, statuses = NULL) { + statuses <- if (is.null(statuses)) pak::pkg_status(pkgs) else statuses + get_key <- function(pkg) { + if (pkg %in% statuses$package) { + statuses[statuses$package == pkg, status_key] + } else { + NA_character_ + } + } + vapply(pkgs, get_key, character(1)) } #' Get the remote package version numbers. -#' #' @param pkgs A character vector of packages to look up. -#' #' @return A character vector of the package versions, or NA if the package cannot be found. #' @keywords internal pkg_remote_version <- function(pkgs) { info <- utils::available.packages(repos = coursekata_repos()) get_remote_version <- purrr::possibly(function(pkg) info[pkg, ]["Version"], NA_character_) - purrr::map_chr(pkgs, get_remote_version) + vapply(pkgs, get_remote_version, character(1)) } #' Load the package, making sure to load from the correct location. Sometimes people have different #' versions of packages installed, so if the package is currently loaded, make sure to load from the #' place it was already loaded from. -#' #' @param pkgs A character vector of packages to load. #' @param do_not_ask Prevent asking the user to install missing packages (they are skipped). #' @param quietly Prevent package startup messages. -#' #' @keywords internal pkg_require <- function(pkgs, do_not_ask = FALSE) { - loader <- function(pkg) { + pkg_load <- function(pkg) { suppressPackageStartupMessages(suppressWarnings(require( pkg, lib.loc = pkg_library_location(pkg), @@ -95,23 +92,20 @@ pkg_require <- function(pkgs, do_not_ask = FALSE) { ))) } - purrr::map_lgl(pkgs, function(pkg) { - if (pkg_is_installed(pkg)) { - loader(pkg) - } else if (!do_not_ask && ask_to_install(pkg)) { + vapply(pkgs, function(pkg) { + loaded <- pkg_load(pkg) + if (!loaded && !do_not_ask && ask_to_install(pkg)) { pkg_install(pkg) - loader(pkg) + pkg_load(pkg) } else { - FALSE + loaded } - }) + }, logical(1), USE.NAMES = FALSE) } #' Ask the user if they want to install packages -#' #' @param pkgs A character vector of packages to ask about. -#' #' @return A logical indicating whether the user answered yes or no. #' @keywords internal ask_to_install <- function(pkgs) { @@ -132,10 +126,8 @@ ask_to_install <- function(pkgs) { #' Install packages using appropriate repositories. -#' #' @param pkgs A character vector of the packages to install. #' @param ... Arguments passed on to [`pak::pkg_install()`]. -#' #' @keywords internal pkg_install <- function(pkgs, ...) { is_538 <- pkgs %in% "fivethirtyeightdata" diff --git a/R/zzz.R b/R/zzz.R index 07b03b3..75e5a97 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -1,21 +1,12 @@ -#' @keywords internal -.onLoad <- function(...) { - # this is needed because of the way mosaic loads packages - # gets rid of "Registered S3 method overwritten by 'mosaic'" message - # TODO: this no longer works - suppressMessages(pkg_is_installed("mosaic")) -} - - #' @keywords internal .onAttach <- function(...) { - # prevents double message in devtools::test() - needed <- coursekata_pkg_list[!pkg_is_attached(coursekata_pkg_list)] - if (length(needed) == 0) { - return() + attached <- coursekata_attach() + coursekata_load_theme() + if (!is_loading_for_tests()) { + coursekata_attach_message(attached) } +} - crayon::num_colors(TRUE) - coursekata_attach(TRUE) - load_coursekata_themes() +is_loading_for_tests <- function() { + !interactive() && identical(Sys.getenv("DEVTOOLS_LOAD"), "tidyverse") } diff --git a/README.Rmd b/README.Rmd index a94a276..79789be 100644 --- a/README.Rmd +++ b/README.Rmd @@ -134,7 +134,7 @@ gf_histogram(~Thumb, data = Fingers, fill = ~ middle(Thumb, .80)) ### Toggling the Theme -The `ggplot2` theme is loaded by default but can be toggled on and off via `load_coursekata_themes()` and `restore_default_themes()`. The actual plot theme and scale components are also provided for advanced users as `theme_coursekata()` and `scale_discrete_coursekata()` (`viridis` is used for continuous color scales). +The `ggplot2` theme is loaded by default but can be toggled on and off via `coursekata_load_theme()` and `coursekata_unload_theme()`. The actual plot theme and scale components are also provided for advanced users as `theme_coursekata()` and `scale_discrete_coursekata()` (`viridis` is used for continuous color scales). # Contributing diff --git a/man/coursekata_attach.Rd b/man/coursekata_attach.Rd index 284359f..15a0680 100644 --- a/man/coursekata_attach.Rd +++ b/man/coursekata_attach.Rd @@ -4,13 +4,10 @@ \alias{coursekata_attach} \title{Attach the CourseKata course packages} \usage{ -coursekata_attach(startup = FALSE) -} -\arguments{ -\item{startup}{Is this being run at start-up?} +coursekata_attach() } \value{ -An object with info about which course packages are installed and attached. +The packages that were attached. } \description{ Attach the CourseKata course packages diff --git a/man/coursekata_attachments.Rd b/man/coursekata_attach_message.Rd similarity index 74% rename from man/coursekata_attachments.Rd rename to man/coursekata_attach_message.Rd index 06a1d9e..3c8e7bf 100644 --- a/man/coursekata_attachments.Rd +++ b/man/coursekata_attach_message.Rd @@ -1,13 +1,13 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/coursekata_attach.R -\name{coursekata_attachments} -\alias{coursekata_attachments} +\name{coursekata_attach_message} +\alias{coursekata_attach_message} \title{Information about CourseKata packages.} \usage{ -coursekata_attachments(startup = FALSE) +coursekata_attach_message(pkgs) } \arguments{ -\item{startup}{Is this being run at start-up?} +\item{pkgs}{A character vector of packages being loaded.} } \value{ A coursekata_attachments object, also of class data.frame with a row for each course diff --git a/man/load_coursekata_themes.Rd b/man/coursekata_load_theme.Rd similarity index 56% rename from man/load_coursekata_themes.Rd rename to man/coursekata_load_theme.Rd index d67b73d..ab2701f 100644 --- a/man/load_coursekata_themes.Rd +++ b/man/coursekata_load_theme.Rd @@ -1,19 +1,19 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/theme_coursekata.R -\name{load_coursekata_themes} -\alias{load_coursekata_themes} +% Please edit documentation in R/theme.R +\name{coursekata_load_theme} +\alias{coursekata_load_theme} \title{Utility function for loading all themes.} \usage{ -load_coursekata_themes() +coursekata_load_theme() } \description{ This function is called at package start-up and should rarely be needed by the user. The -exception is when the user has called \code{\link[=restore_default_themes]{restore_default_themes()}} and wants to go back to the +exception is when the user has called \code{\link[=coursekata_unload_theme]{coursekata_unload_theme()}} and wants to go back to the CourseKata look and feel. When run, this function sets the CourseKata color palettes \code{\link[=coursekata_palette]{coursekata_palette()}}, sets the default theme to \code{\link[=theme_coursekata]{theme_coursekata()}}, and tweaks some default settings for specific plots. To restore the original \code{ggplot2} settings, run -\code{\link[=restore_default_themes]{restore_default_themes()}}. +\code{\link[=coursekata_unload_theme]{coursekata_unload_theme()}}. } \seealso{ -coursekata_palette theme_coursekata scale_discrete_coursekata restore_default_themes +coursekata_palette theme_coursekata scale_discrete_coursekata coursekata_unload_theme } diff --git a/man/coursekata_palette.Rd b/man/coursekata_palette.Rd index cf18d67..17408a9 100644 --- a/man/coursekata_palette.Rd +++ b/man/coursekata_palette.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/theme_coursekata.R +% Please edit documentation in R/theme.R \name{coursekata_palette} \alias{coursekata_palette} \title{The color palettes used in our theme system} diff --git a/man/coursekata_palette_provider.Rd b/man/coursekata_palette_provider.Rd index 894a61e..593142b 100644 --- a/man/coursekata_palette_provider.Rd +++ b/man/coursekata_palette_provider.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/theme_coursekata.R +% Please edit documentation in R/theme.R \name{coursekata_palette_provider} \alias{coursekata_palette_provider} \title{Create a function that provides a colorblind palette.} diff --git a/man/restore_default_themes.Rd b/man/coursekata_unload_theme.Rd similarity index 57% rename from man/restore_default_themes.Rd rename to man/coursekata_unload_theme.Rd index f87a898..5fc74d8 100644 --- a/man/restore_default_themes.Rd +++ b/man/coursekata_unload_theme.Rd @@ -1,16 +1,16 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/theme_coursekata.R -\name{restore_default_themes} -\alias{restore_default_themes} +% Please edit documentation in R/theme.R +\name{coursekata_unload_theme} +\alias{coursekata_unload_theme} \title{Restore \code{ggplot2} default settings} \usage{ -restore_default_themes() +coursekata_unload_theme() } \description{ This function will restore all of the tweaks to themes and plotting to the original \code{ggplot2} defaults. If you want to go back to the CourseKata look and feel, run -\code{\link[=load_coursekata_themes]{load_coursekata_themes()}}. +\code{\link[=coursekata_load_theme]{coursekata_load_theme()}}. } \seealso{ -load_coursekata_themes +coursekata_load_theme } diff --git a/man/m_predictor.Rd b/man/m_predictor.Rd new file mode 100644 index 0000000..fec81cd --- /dev/null +++ b/man/m_predictor.Rd @@ -0,0 +1,18 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/test_fit.R +\name{m_predictor} +\alias{m_predictor} +\title{Get the predictor variable from a model.} +\usage{ +m_predictor(model) +} +\arguments{ +\item{model}{An \code{\link{lm}} model (or anything compatible with \code{\link[=formula]{formula()}}).} +} +\value{ +A string with the name of the predictor variable. +} +\description{ +Get the predictor variable from a model. +} +\keyword{internal} diff --git a/man/pkg_is_installed.Rd b/man/pkg_is_installed.Rd index 2285180..04fb03b 100644 --- a/man/pkg_is_installed.Rd +++ b/man/pkg_is_installed.Rd @@ -4,10 +4,12 @@ \alias{pkg_is_installed} \title{Check if packages are installed} \usage{ -pkg_is_installed(pkgs) +pkg_is_installed(pkgs, statuses = NULL) } \arguments{ \item{pkgs}{Character vector of the names of the packages to check.} + +\item{statuses}{The output of \code{\link[pak:pkg_status]{pak::pkg_status()}} (computed if not supplied).} } \value{ Logical vector indicating whether the packages are installed. diff --git a/man/pkg_library_location.Rd b/man/pkg_library_location.Rd index 532e71f..84b3859 100644 --- a/man/pkg_library_location.Rd +++ b/man/pkg_library_location.Rd @@ -4,10 +4,12 @@ \alias{pkg_library_location} \title{Determine which libraries packages were loaded from} \usage{ -pkg_library_location(pkgs) +pkg_library_location(pkgs, statuses = NULL) } \arguments{ \item{pkgs}{A character vector of packages to check.} + +\item{statuses}{The output of \code{\link[pak:pkg_status]{pak::pkg_status()}} (computed if not supplied).} } \value{ A character vector of library directory paths the packages were loaded from, the default diff --git a/man/pkg_version.Rd b/man/pkg_version.Rd index 412745e..27a318d 100644 --- a/man/pkg_version.Rd +++ b/man/pkg_version.Rd @@ -4,10 +4,12 @@ \alias{pkg_version} \title{Get the local package version numbers.} \usage{ -pkg_version(pkgs) +pkg_version(pkgs, statuses = NULL) } \arguments{ \item{pkgs}{A character vector of packages to look up.} + +\item{statuses}{The output of \code{\link[pak:pkg_status]{pak::pkg_status()}} (computed if not supplied).} } \value{ A character vector of the package versions. If the package is already loaded, this is diff --git a/man/possibly_pkg_status.Rd b/man/possibly_pkg_status.Rd index 886051a..a61f1ae 100644 --- a/man/possibly_pkg_status.Rd +++ b/man/possibly_pkg_status.Rd @@ -4,12 +4,14 @@ \alias{possibly_pkg_status} \title{Attempt to get package information using \code{pak}, but fail with NA} \usage{ -possibly_pkg_status(pkgs, status_key) +possibly_pkg_status(pkgs, status_key, statuses = NULL) } \arguments{ \item{pkgs}{A character vector of packages to look up.} \item{status_key}{The column to get from the \code{\link[pak:pkg_status]{pak::pkg_status()}} output.} + +\item{statuses}{The output of \code{\link[pak:pkg_status]{pak::pkg_status()}} (computed if not supplied).} } \value{ A character vector with each package's status information. diff --git a/man/scale_discrete_coursekata.Rd b/man/scale_discrete_coursekata.Rd index 9dbcdb3..725ce23 100644 --- a/man/scale_discrete_coursekata.Rd +++ b/man/scale_discrete_coursekata.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/theme_coursekata.R +% Please edit documentation in R/theme.R \name{scale_discrete_coursekata} \alias{scale_discrete_coursekata} \title{A discrete color scale constructor with colorblind-safe palettes.} diff --git a/man/split_data.Rd b/man/split_data.Rd new file mode 100644 index 0000000..cff6c9d --- /dev/null +++ b/man/split_data.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/test_fit.R +\name{split_data} +\alias{split_data} +\title{Split data into train and test sets.} +\usage{ +split_data(data, prop = 0.7) +} +\arguments{ +\item{data}{A data frame.} + +\item{prop}{The proportion of rows to assign to the training set.} +} +\value{ +A list with two data frames, \code{train} and \code{test}. +} +\description{ +Split data into train and test sets. +} diff --git a/man/test_fit.Rd b/man/test_fit.Rd new file mode 100644 index 0000000..d72cfe4 --- /dev/null +++ b/man/test_fit.Rd @@ -0,0 +1,21 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/test_fit.R +\name{test_fit} +\alias{test_fit} +\title{Test the fit of a model on a train and test set.} +\usage{ +test_fit(model, df_train, df_test) +} +\arguments{ +\item{model}{An \code{\link{lm}} model (or anything compatible with \code{\link[=formula]{formula()}}).} + +\item{df_train}{A data frame with the training data.} + +\item{df_test}{A data frame with the test data.} +} +\value{ +A data frame with the fit statistics. +} +\description{ +Test the fit of a model on a train and test set. +} diff --git a/man/theme_coursekata.Rd b/man/theme_coursekata.Rd index 6b15087..54ceb3c 100644 --- a/man/theme_coursekata.Rd +++ b/man/theme_coursekata.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/theme_coursekata.R +% Please edit documentation in R/theme.R \name{theme_coursekata} \alias{theme_coursekata} \title{A simple theme built on top of \code{\link[=theme_bw]{theme_bw()}}} @@ -13,7 +13,7 @@ A gg theme object The \code{coursekata} package automatically loads this theme when the package is loaded. This is in addition to a number of other plot tweaks and option settings. To just restore the theme to the default, you can run \code{set_theme(theme_grey)}. If you want to restore all plot related settings -and/or prevent them when loading the package, see \code{\link{restore_default_themes}}. +and/or prevent them when loading the package, see \code{\link{coursekata_unload_theme}}. } \examples{ gf_boxplot(Thumb ~ RaceEthnic, data = Fingers, fill = ~RaceEthnic) diff --git a/tests/testthat/helper-test_utilities.R b/tests/testthat/helper-test_utilities.R index 35806ca..e6f6e17 100644 --- a/tests/testthat/helper-test_utilities.R +++ b/tests/testthat/helper-test_utilities.R @@ -1,14 +1,15 @@ -detacher <- function(pkg) { - try(detach(paste0("package:", pkg), unload = TRUE, character.only = TRUE), silent = TRUE) +detacher <- function(pkgs) { + detach_ <- function(pkg) { + try(detach(paste0("package:", pkg), unload = TRUE, character.only = TRUE), silent = TRUE) + } + lapply(pkgs, detach_) } -attacher <- function(pkg) { - suppressMessages(library(pkg, character.only = TRUE)) -} - -load_before <- function(x) { - .GlobalEnv$.load_dev() - x +attacher <- function(pkgs) { + attach_ <- function(pkg) { + suppressMessages(library(pkg, character.only = TRUE)) + } + lapply(pkgs, attach_) } expect_doppelganger <- function(plot, name) { diff --git a/tests/testthat/test-coursekata_attach.R b/tests/testthat/test-coursekata_attach.R index d632c9c..6e678df 100644 --- a/tests/testthat/test-coursekata_attach.R +++ b/tests/testthat/test-coursekata_attach.R @@ -1,12 +1,16 @@ test_that("all course packages are listed with version and whether attached", { - attachments <- suppressMessages(coursekata_attach()) - expect_identical(attachments, coursekata_packages()) + pkgs <- "fivethirtyeight" # use this package because it is not imported + detacher(pkgs) + withr::defer(attacher(pkgs)) + + attachments <- coursekata_attach() + expect_identical(attachments, pkgs) }) test_that("a nicely formatted message is displayed when attaching the packages", { - info <- coursekata_packages() - purrr::walk(info$package, function(package) { - expect_message(coursekata_attach(), sprintf(".*[vx] %s +.*", package)) + msg <- coursekata_attach_message(coursekata_pkgs) + purrr::walk(coursekata_pkgs, function(package) { + expect_match(msg, sprintf(".*[vx] %s +.*", package)) }) }) diff --git a/tests/testthat/test-coursekata_packages.R b/tests/testthat/test-coursekata_packages.R index 0e4365d..774cc66 100644 --- a/tests/testthat/test-coursekata_packages.R +++ b/tests/testthat/test-coursekata_packages.R @@ -1,14 +1,8 @@ -pkgs <- rev(c( - "supernova", "mosaic", "lsr", "Metrics", - "fivethirtyeight", "fivethirtyeightdata", "Lock5withR", "dslabs" -)) - - test_that("all course packages are listed with version and whether attached", { packages <- suppressMessages(coursekata_packages()) - expect_identical(packages$package, pkgs) - expect_identical(packages$version, pkg_version(pkgs)) - expect_identical(packages$attached, pkg_is_attached(pkgs)) + expect_identical(packages$package, coursekata_pkgs) + expect_identical(packages$version, unname(pkg_version(coursekata_pkgs))) + expect_identical(packages$attached, unname(pkg_is_attached(coursekata_pkgs))) }) diff --git a/tests/testthat/test-gf_model.R b/tests/testthat/test-gf_model.R index a90f8bc..f74b1cb 100644 --- a/tests/testthat/test-gf_model.R +++ b/tests/testthat/test-gf_model.R @@ -419,22 +419,6 @@ test_that("it allows mapping new aesthetics", { # Alternate specification --------------------------------------------------------------------- -# TODO: empty tests -# test_that("it can handle data$var syntax", { -# # info <- gf_point(er$later_anxiety ~ er$condition, color = ~ er$condition) %>% -# # gf_model(lm(er$later_anxiety ~ er$condition)) -# # expect_doppelganger("cond. mod. with data$var syntax") -# }) - -# test_that("it allows modified variables as long as they match", { -# # maybe the problem with this is that we use model$model which doesn't have the -# # unaltered variables in it - -# # gf_point(later_anxiety ~ factor(base_anxiety), color = ~condition, data = er) %>% -# # gf_model(lm(later_anxiety ~ factor(base_anxiety), data = er)) %>% -# # load_before() -# }) - test_that("you can pass it a formula instead of an `lm()` object", { gf_point(later_anxiety ~ base_anxiety, color = ~condition, data = er) %>% gf_model(later_anxiety ~ condition) %>% diff --git a/tests/testthat/test-utils-pkg.R b/tests/testthat/test-utils-pkg.R index 61c2d39..280391b 100644 --- a/tests/testthat/test-utils-pkg.R +++ b/tests/testthat/test-utils-pkg.R @@ -13,10 +13,8 @@ test_that("it determines whether a package is attached or not", { test_that("it retrieves the library location for currently installed packages, or NA", { pkgs <- c("supernova", "lsr", "does_not_exist") - locations <- pkg_library_location(pkgs) - expect_vector(locations, character(), 3) - expect_identical(locations[3], NA_character_) + expect_identical(unname(locations[3]), NA_character_) expect_true(all(dir.exists(locations[1:2]))) }) From ffe12ed2f5dd0bd4314bb60977b2ce9e4f831d07 Mon Sep 17 00:00:00 2001 From: Adam Blake Date: Thu, 5 Oct 2023 23:15:06 -0700 Subject: [PATCH 2/9] perf: add quickstart option #CKR-79 --- R/utils-pkg.R | 2 +- R/zzz.R | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/R/utils-pkg.R b/R/utils-pkg.R index ed39726..a91bb5c 100644 --- a/R/utils-pkg.R +++ b/R/utils-pkg.R @@ -85,7 +85,7 @@ pkg_require <- function(pkgs, do_not_ask = FALSE) { pkg_load <- function(pkg) { suppressPackageStartupMessages(suppressWarnings(require( pkg, - lib.loc = pkg_library_location(pkg), + lib.loc = if (quickstart()) NULL else pkg_library_location(pkg), character.only = TRUE, warn.conflicts = FALSE, quietly = TRUE diff --git a/R/zzz.R b/R/zzz.R index 75e5a97..96c1be1 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -2,11 +2,12 @@ .onAttach <- function(...) { attached <- coursekata_attach() coursekata_load_theme() - if (!is_loading_for_tests()) { + if (!quickstart()) { coursekata_attach_message(attached) } } -is_loading_for_tests <- function() { - !interactive() && identical(Sys.getenv("DEVTOOLS_LOAD"), "tidyverse") +quickstart <- function() { + getOption("coursekata.quickstart", FALSE) || + !interactive() && identical(Sys.getenv("DEVTOOLS_LOAD"), "coursekata") } From a97607190f9ae4e5f40f69896332a1e08b13641c Mon Sep 17 00:00:00 2001 From: Adam Blake Date: Thu, 5 Oct 2023 23:41:20 -0700 Subject: [PATCH 3/9] test: reinstate gf_density related tests #CKR-72 --- .../gf-dens-empty-model-outcome-on-x.svg | 66 ++++++++ .../gf-density-empty-model-outcome-on-x.svg | 82 ++++++++++ ...am-cond-mod-outcome-on-x-pred-on-color.svg | 119 ++++++++++++++ ...am-cond-mod-outcome-on-x-pred-on-facet.svg | 150 ++++++++++++++++++ ...gf-dhistogram-empty-model-outcome-on-x.svg | 124 +++++++++++++++ tests/testthat/test-gf_model.R | 37 ++--- 6 files changed, 553 insertions(+), 25 deletions(-) create mode 100644 tests/testthat/_snaps/gf_model/gf-dens-empty-model-outcome-on-x.svg create mode 100644 tests/testthat/_snaps/gf_model/gf-density-empty-model-outcome-on-x.svg create mode 100644 tests/testthat/_snaps/gf_model/gf-dhistogram-cond-mod-outcome-on-x-pred-on-color.svg create mode 100644 tests/testthat/_snaps/gf_model/gf-dhistogram-cond-mod-outcome-on-x-pred-on-facet.svg create mode 100644 tests/testthat/_snaps/gf_model/gf-dhistogram-empty-model-outcome-on-x.svg diff --git a/tests/testthat/_snaps/gf_model/gf-dens-empty-model-outcome-on-x.svg b/tests/testthat/_snaps/gf_model/gf-dens-empty-model-outcome-on-x.svg new file mode 100644 index 0000000..573b360 --- /dev/null +++ b/tests/testthat/_snaps/gf_model/gf-dens-empty-model-outcome-on-x.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + +0.050 +0.075 +0.100 +0.125 + + + + + + + + + +0.0 +2.5 +5.0 +7.5 +10.0 +later_anxiety +density + +condition + + + + +Control +Dog +[gf_dens] Empty model, outcome on X + + diff --git a/tests/testthat/_snaps/gf_model/gf-density-empty-model-outcome-on-x.svg b/tests/testthat/_snaps/gf_model/gf-density-empty-model-outcome-on-x.svg new file mode 100644 index 0000000..d81d938 --- /dev/null +++ b/tests/testthat/_snaps/gf_model/gf-density-empty-model-outcome-on-x.svg @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0.000 +0.025 +0.050 +0.075 +0.100 +0.125 + + + + + + + + + + + +0.0 +2.5 +5.0 +7.5 +10.0 +later_anxiety +density + +condition + + + + +Control +Dog + +fill + + + + +Control +Dog +[gf_density] Empty model, outcome on X + + diff --git a/tests/testthat/_snaps/gf_model/gf-dhistogram-cond-mod-outcome-on-x-pred-on-color.svg b/tests/testthat/_snaps/gf_model/gf-dhistogram-cond-mod-outcome-on-x-pred-on-color.svg new file mode 100644 index 0000000..0b6cce7 --- /dev/null +++ b/tests/testthat/_snaps/gf_model/gf-dhistogram-cond-mod-outcome-on-x-pred-on-color.svg @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0.00 +0.25 +0.50 +0.75 +1.00 + + + + + + + + + + +0.0 +2.5 +5.0 +7.5 +10.0 +later_anxiety +density + +condition + + + + + + +Control +Dog +[gf_dhistogram] cond. mod., outcome on X, pred. on color + + diff --git a/tests/testthat/_snaps/gf_model/gf-dhistogram-cond-mod-outcome-on-x-pred-on-facet.svg b/tests/testthat/_snaps/gf_model/gf-dhistogram-cond-mod-outcome-on-x-pred-on-facet.svg new file mode 100644 index 0000000..6a066b5 --- /dev/null +++ b/tests/testthat/_snaps/gf_model/gf-dhistogram-cond-mod-outcome-on-x-pred-on-facet.svg @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Control + + + + + + + + + + +Dog + + + + + + + +0.0 +2.5 +5.0 +7.5 +10.0 + + + + + +0.0 +2.5 +5.0 +7.5 +10.0 +0.0 +0.2 +0.4 +0.6 + + + + +later_anxiety +density +[gf_dhistogram] cond. mod., outcome on X, pred. on facet + + diff --git a/tests/testthat/_snaps/gf_model/gf-dhistogram-empty-model-outcome-on-x.svg b/tests/testthat/_snaps/gf_model/gf-dhistogram-empty-model-outcome-on-x.svg new file mode 100644 index 0000000..33f24ab --- /dev/null +++ b/tests/testthat/_snaps/gf_model/gf-dhistogram-empty-model-outcome-on-x.svg @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0.0 +0.4 +0.8 +1.2 + + + + + + + + + +0.0 +2.5 +5.0 +7.5 +10.0 +later_anxiety +density + +condition + + + + +Control +Dog +[gf_dhistogram] Empty model, outcome on X + + diff --git a/tests/testthat/test-gf_model.R b/tests/testthat/test-gf_model.R index f74b1cb..220f9e9 100644 --- a/tests/testthat/test-gf_model.R +++ b/tests/testthat/test-gf_model.R @@ -39,12 +39,6 @@ test_that("it plots the empty model as a vertical line when outcome is on Y, one # but these stat funs are in ggstance, so idk... if you're here and you're worried, just # test by hand because it usually only doesn't work in testthat # bin_plots <- c("gf_histogramh", "gf_dhistogramh") - # purrr::walk(bin_plots, function(plot) { - # do.call(plot, append(plot_args, list(bins = 30))) %>% - # gf_model(lm(later_anxiety ~ NULL, data = er), color = "brown") %>% - # expect_doppelganger(snap_name(plot)) - # }) - other_plots <- c("gf_rugy") purrr::walk(other_plots, function(plot) { do.call(plot, plot_args) %>% @@ -76,18 +70,15 @@ test_that("it plots the empty model as a vertical line when outcome is on X, one plot_args <- list(gformula = ~later_anxiety, color = ~condition, data = er) - # skip `gf_dhistogram` - # https://github.com/ProjectMOSAIC/ggformula/issues/156 - bin_plots <- c("gf_histogram", "gf_freqpoly") + bin_plots <- c("gf_histogram", "gf_dhistogram", "gf_freqpoly") purrr::walk(bin_plots, function(plot) { do.call(plot, append(plot_args, list(bins = 30))) %>% gf_model(lm(later_anxiety ~ NULL, data = er), color = "brown") %>% expect_doppelganger(snap_name(plot)) }) - # skip "gf_dens2", "gf_density", "gf_dens" - # https://github.com/ProjectMOSAIC/ggformula/issues/156 - other_plots <- c("gf_rug", "gf_rugx") + # TODO: "gf_dens2": Can't find geom called "density_line" + other_plots <- c("gf_rug", "gf_rugx", "gf_density", "gf_dens") purrr::walk(other_plots, function(plot) { do.call(plot, plot_args) %>% gf_model(lm(later_anxiety ~ NULL, data = er), color = "brown") %>% @@ -99,11 +90,6 @@ test_that("it plots the empty model as a vertical line when outcome is on X, one gf_model(lm(later_anxiety ~ NULL, data = er)) %>% expect_doppelganger(snap_name("gf_boxplot")) - # TODO: can't get gf_boxploth working... - # gf_boxploth(~later_anxiety, data = er) %>% - # gf_model(lm(later_anxiety ~ NULL, data = er)) %>% - # expect_doppelganger(snap_name("gf_boxplot")) - gf_violin(1 ~ later_anxiety, data = er) %>% gf_model(lm(later_anxiety ~ NULL, data = er)) %>% expect_doppelganger(snap_name("gf_violin", " -- 2")) @@ -111,6 +97,11 @@ test_that("it plots the empty model as a vertical line when outcome is on X, one gf_violin(later_anxiety ~ 1, data = er) %>% gf_model(lm(later_anxiety ~ NULL, data = er)) %>% expect_doppelganger(snap_name("gf_violin horizontal")) + + # TODO: Can't find geom called "boxploth" + # gf_boxploth(~later_anxiety, data = er) %>% + # gf_model(lm(later_anxiety ~ NULL, data = er)) %>% + # expect_doppelganger(snap_name("gf_boxplot")) }) @@ -135,7 +126,7 @@ test_that("it plots 1 predictor (on axis, categorical) models as lines at means, glue("[{plot_name}] cond. mod., outcome on X{suffix}") } - # TODO: removed broken "gf_boxploth" + # TODO: Can't find geom called "boxploth" plot_args <- list(gformula = condition ~ later_anxiety, color = ~condition, data = er) plot_types <- c("gf_point") purrr::walk(plot_types, function(plot) { @@ -163,7 +154,7 @@ test_that("it plots 1 predictor (on aesthetic, cat.) models as lines at means, o }) # plot where one axis is calculated - # skip "gf_histogramh", "gf_dhistogramh" funs because their related stat funs can't be found + # TODO: skip "gf_histogramh", "gf_dhistogramh" funs because their related stat funs can't be found plot_args <- list(gformula = ~later_anxiety, color = ~condition, data = er) plot_types <- c("gf_rugy") purrr::walk(plot_types, function(plot) { @@ -188,10 +179,8 @@ test_that("it plots 1 predictor (on aesthetic, cat.) models as lines at means, o }) # plots where one axis is calculated - # skip `gf_dhistogram`: - # https://github.com/ProjectMOSAIC/ggformula/issues/156 plot_args <- list(gformula = ~later_anxiety, color = ~condition, data = er) - plot_types <- c("gf_histogram", "gf_rug", "gf_rugx") + plot_types <- c("gf_histogram", "gf_dhistogram", "gf_rug", "gf_rugx") purrr::walk(plot_types, function(plot) { do.call(plot, plot_args) %>% gf_model(lm(later_anxiety ~ condition, data = er)) %>% @@ -252,10 +241,8 @@ test_that("it plots 1 predictor (on facet, compact cat.) models as lines at mean }) # plots where one axis is calculated - # skip `gf_dhistogram`: - # https://github.com/ProjectMOSAIC/ggformula/issues/156 plot_args <- list(gformula = ~ later_anxiety | condition, data = er) - plot_types <- c("gf_histogram", "gf_rug", "gf_rugx") + plot_types <- c("gf_histogram", "gf_dhistogram", "gf_rug", "gf_rugx") purrr::walk(plot_types, function(plot) { do.call(plot, plot_args) %>% From f5900cb0f5774ecd09bc2a5c4a0f094067cd65d4 Mon Sep 17 00:00:00 2001 From: Adam Blake Date: Thu, 5 Oct 2023 23:53:46 -0700 Subject: [PATCH 4/9] docs: update NEWS for 0.13.0 --- NEWS.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/NEWS.md b/NEWS.md index 2054686..0880190 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,12 @@ # `coursekata` change log +## coursekata 0.13.0 + +- Add `coursekata.quickstart` option, which can reduce load times significantly. +- Reduce expensive lookups when attaching packages, further reducing load times. +- Re-introduce `gf_model` tests for density plots now that upstream is fixed. +- Add `test_fit()` simple model stats to help teachers evaluate student models. + ## coursekata 0.12.0 - Remove `sse()`, `ssm()`, `ssr()`, `SSE()`, `SSM()`, `SSR()` functions: they conflict with `Metrics` package. From cb39ee14524853534fb88d7c086a769f5310016d Mon Sep 17 00:00:00 2001 From: Adam Blake Date: Thu, 5 Oct 2023 23:54:07 -0700 Subject: [PATCH 5/9] chore: bump version 0.13.0 --- DESCRIPTION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index 816e18b..947daf8 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: coursekata Title: Packages and Functions for CourseKata Courses -Version: 0.12.0 +Version: 0.13.0 Date: 2023-10-05 Authors@R: c( person("Adam", "Blake", , "adamblake@g.ucla.edu", role = c("cre", "aut"), From 5ad7cd523aaa80d4936269571f86306d718b6a18 Mon Sep 17 00:00:00 2001 From: Adam Blake Date: Fri, 6 Oct 2023 02:55:25 -0700 Subject: [PATCH 6/9] test: account for possibly uninstalled fivethirtyeightdata --- R/coursekata_attach.R | 5 ++--- R/utils-pkg.R | 7 ++++--- man/coursekata_attach.Rd | 2 +- man/pkg_is_installed.Rd | 2 +- tests/testthat/test-coursekata_attach.R | 15 +++++++++++---- tests/testthat/test-utils-pkg.R | 2 +- 6 files changed, 20 insertions(+), 13 deletions(-) diff --git a/R/coursekata_attach.R b/R/coursekata_attach.R index f8531da..d5a3de4 100644 --- a/R/coursekata_attach.R +++ b/R/coursekata_attach.R @@ -1,12 +1,11 @@ #' Attach the CourseKata course packages -#' @return The packages that were attached. +#' @return A named logical vector indicating which packages were attached. #' @export #' @examples #' coursekata_attach() coursekata_attach <- function() { to_attach <- coursekata_detached() - suppressPackageStartupMessages(pkg_require(to_attach)) - invisible(to_attach) + invisible(suppressPackageStartupMessages(pkg_require(to_attach))) } diff --git a/R/utils-pkg.R b/R/utils-pkg.R index a91bb5c..1aca3a5 100644 --- a/R/utils-pkg.R +++ b/R/utils-pkg.R @@ -15,11 +15,12 @@ pkg_is_attached <- function(pkgs) { #' #' @param pkgs Character vector of the names of the packages to check. #' @param statuses The output of [`pak::pkg_status()`] (computed if not supplied). -#' @return Logical vector indicating whether the packages are installed. +#' @return Named logical vector indicating whether the packages are installed. #' @keywords internal pkg_is_installed <- function(pkgs, statuses = NULL) { statuses <- if (is.null(statuses)) pak::pkg_status(pkgs) else statuses - pkgs %in% statuses$package + checker <- function(pkg) pkg %in% statuses$package + vapply(pkgs, checker, logical(1)) } @@ -100,7 +101,7 @@ pkg_require <- function(pkgs, do_not_ask = FALSE) { } else { loaded } - }, logical(1), USE.NAMES = FALSE) + }, logical(1)) } diff --git a/man/coursekata_attach.Rd b/man/coursekata_attach.Rd index 15a0680..f7a6704 100644 --- a/man/coursekata_attach.Rd +++ b/man/coursekata_attach.Rd @@ -7,7 +7,7 @@ coursekata_attach() } \value{ -The packages that were attached. +A named logical vector indicating which packages were attached. } \description{ Attach the CourseKata course packages diff --git a/man/pkg_is_installed.Rd b/man/pkg_is_installed.Rd index 04fb03b..b88f53a 100644 --- a/man/pkg_is_installed.Rd +++ b/man/pkg_is_installed.Rd @@ -12,7 +12,7 @@ pkg_is_installed(pkgs, statuses = NULL) \item{statuses}{The output of \code{\link[pak:pkg_status]{pak::pkg_status()}} (computed if not supplied).} } \value{ -Logical vector indicating whether the packages are installed. +Named logical vector indicating whether the packages are installed. } \description{ Note: this function differs from \code{\link[rlang:is_installed]{rlang::is_installed()}} in two regards: it is quieter and will diff --git a/tests/testthat/test-coursekata_attach.R b/tests/testthat/test-coursekata_attach.R index 6e678df..07c6e3c 100644 --- a/tests/testthat/test-coursekata_attach.R +++ b/tests/testthat/test-coursekata_attach.R @@ -1,10 +1,17 @@ test_that("all course packages are listed with version and whether attached", { - pkgs <- "fivethirtyeight" # use this package because it is not imported - detacher(pkgs) - withr::defer(attacher(pkgs)) + # use these packages because they are not imported + # they need to be in the order they appear in the coursekata_pkgs object + pkgs <- c("fivethirtyeightdata", "fivethirtyeight") + # fivethirtyeightdata is not always installed, and if so, it won't get attached + # detach/re-attach only if it is installed + installed <- pkg_is_installed(pkgs) + detacher(pkgs[installed]) + withr::defer(attacher(pkgs[installed])) + # only the installed package will be attached attachments <- coursekata_attach() - expect_identical(attachments, pkgs) + names(installed) <- pkgs + expect_identical(attachments, installed) }) diff --git a/tests/testthat/test-utils-pkg.R b/tests/testthat/test-utils-pkg.R index 280391b..40737a8 100644 --- a/tests/testthat/test-utils-pkg.R +++ b/tests/testthat/test-utils-pkg.R @@ -29,7 +29,7 @@ test_that("it retrieves the package version for currently installed packages, or test_that("requiring a package is vectorized", { pkgs <- "fivethirtyeight" # use this package because it is not imported purrr::walk(pkgs, detacher) - expect_identical(pkg_require(pkgs), rep(TRUE, length(pkgs))) + expect_identical(pkg_require(pkgs), pkg_is_installed(pkgs)) }) From f587ef889e4104e97e2e81e56a90b4d689bb23ee Mon Sep 17 00:00:00 2001 From: Adam Blake Date: Fri, 6 Oct 2023 03:17:07 -0700 Subject: [PATCH 7/9] refactor: rename test_fit -> fit_stats; add tests #CKR-80 --- NAMESPACE | 3 ++- R/test_fit.R | 35 ++++++++----------------------- man/{test_fit.Rd => fit_stats.Rd} | 11 ++++++---- man/m_predictor.Rd | 18 ---------------- tests/testthat/test-test_fit.R | 30 ++++++++++++++++++++++++++ 5 files changed, 48 insertions(+), 49 deletions(-) rename man/{test_fit.Rd => fit_stats.Rd} (69%) delete mode 100644 man/m_predictor.Rd create mode 100644 tests/testthat/test-test_fit.R diff --git a/NAMESPACE b/NAMESPACE index 28e6126..045372e 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -16,6 +16,8 @@ export(coursekata_unload_theme) export(coursekata_update) export(f) export(fVal) +export(fit_stats) +export(fitstats) export(gf_model) export(gf_model_old) export(lower) @@ -25,7 +27,6 @@ export(pre) export(scale_discrete_coursekata) export(split_data) export(tails) -export(test_fit) export(theme_coursekata) export(upper) import(rlang) diff --git a/R/test_fit.R b/R/test_fit.R index 1b608a4..23741b2 100644 --- a/R/test_fit.R +++ b/R/test_fit.R @@ -1,5 +1,4 @@ #' Split data into train and test sets. -#' #' @param data A data frame. #' @param prop The proportion of rows to assign to the training set. #' @return A list with two data frames, `train` and `test`. @@ -11,30 +10,20 @@ split_data <- function(data, prop = .7) { } #' Test the fit of a model on a train and test set. -#' -#' @param model An [`lm`] model (or anything compatible with [`formula()`]). +#' @param model An [`lm`] model. #' @param df_train A data frame with the training data. #' @param df_test A data frame with the test data. #' @return A data frame with the fit statistics. #' @export -test_fit <- function(model, df_train, df_test) { +fit_stats <- function(model, df_train, df_test) { fit_train <- stats::update(model, data = df_train) fit_test <- stats::update(model, data = df_test) - predictor <- m_predictor(model) + predictor <- as.character(rlang::f_lhs(stats::formula(model))) tibble::tibble( - data = c( - 'train', - 'test' - ), - F = c( - coursekata::f(fit_train), - coursekata::f(fit_test) - ), - PRE = c( - coursekata::pre(fit_train), - coursekata::pre(fit_test) - ), + data = c("train", "test"), + F = c(f(fit_train), f(fit_test)), + PRE = c(pre(fit_train), pre(fit_test)), RMSE = c( Metrics::rmse(df_train[[predictor]], stats::predict(fit_train)), Metrics::rmse(df_test[[predictor]], stats::predict(fit_test)) @@ -42,12 +31,6 @@ test_fit <- function(model, df_train, df_test) { ) } -#' Get the predictor variable from a model. -#' -#' @param model An [`lm`] model (or anything compatible with [`formula()`]). -#' @return A string with the name of the predictor variable. -#' @keywords internal -m_predictor <- function(model) { - as.character(rlang::f_lhs(stats::formula(model))) -} - +#' @rdname fit_stats +#' @export +fitstats <- fit_stats diff --git a/man/test_fit.Rd b/man/fit_stats.Rd similarity index 69% rename from man/test_fit.Rd rename to man/fit_stats.Rd index d72cfe4..ef0a56d 100644 --- a/man/test_fit.Rd +++ b/man/fit_stats.Rd @@ -1,13 +1,16 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/test_fit.R -\name{test_fit} -\alias{test_fit} +\name{fit_stats} +\alias{fit_stats} +\alias{fitstats} \title{Test the fit of a model on a train and test set.} \usage{ -test_fit(model, df_train, df_test) +fit_stats(model, df_train, df_test) + +fitstats(model, df_train, df_test) } \arguments{ -\item{model}{An \code{\link{lm}} model (or anything compatible with \code{\link[=formula]{formula()}}).} +\item{model}{An \code{\link{lm}} model.} \item{df_train}{A data frame with the training data.} diff --git a/man/m_predictor.Rd b/man/m_predictor.Rd deleted file mode 100644 index fec81cd..0000000 --- a/man/m_predictor.Rd +++ /dev/null @@ -1,18 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/test_fit.R -\name{m_predictor} -\alias{m_predictor} -\title{Get the predictor variable from a model.} -\usage{ -m_predictor(model) -} -\arguments{ -\item{model}{An \code{\link{lm}} model (or anything compatible with \code{\link[=formula]{formula()}}).} -} -\value{ -A string with the name of the predictor variable. -} -\description{ -Get the predictor variable from a model. -} -\keyword{internal} diff --git a/tests/testthat/test-test_fit.R b/tests/testthat/test-test_fit.R new file mode 100644 index 0000000..65b6678 --- /dev/null +++ b/tests/testthat/test-test_fit.R @@ -0,0 +1,30 @@ +test_that("it splits data proportionately", { + data <- tibble::tibble(x = 1:100) + split <- split_data(data, .7) + expect_equal(nrow(split$train), 70) + expect_equal(nrow(split$test), 30) +}) + +test_that("it splits data randomly", { + data <- tibble::tibble(x = 1:1000) + split1 <- split_data(data, .7) + split2 <- split_data(data, .7) + expect_false(all(split1$train == split2$train)) + expect_false(all(split1$test == split2$test)) +}) + +test_that("it compares the fit of a model on train and test data", { + model <- lm(mpg ~ cyl, data = mtcars) + split <- split_data(mtcars, .7) + fit_train <- lm(model, data = split$train) + fit_test <- lm(model, data = split$test) + stats <- fit_stats(model, split$train, split$test) + + expect_equal(stats$data, c("train", "test")) + expect_equal(stats$F, c(f(fit_train), f(fit_test))) + expect_equal(stats$PRE, c(pre(fit_train), pre(fit_test))) + expect_equal(stats$RMSE, c( + rmse(split$train$mpg, predict(fit_train)), + rmse(split$test$mpg, predict(fit_test)) + )) +}) From 856c4df789273005d1aa60bd87ef88bda655595d Mon Sep 17 00:00:00 2001 From: Adam Blake Date: Fri, 6 Oct 2023 03:37:41 -0700 Subject: [PATCH 8/9] feat: extend color palettes using viridis #CKR-69 --- DESCRIPTION | 1 + R/theme.R | 15 +++++++-------- man/coursekata_palette_provider.Rd | 7 ++++++- tests/testthat/test-palette.R | 10 ++++++++++ 4 files changed, 24 insertions(+), 9 deletions(-) create mode 100644 tests/testthat/test-palette.R diff --git a/DESCRIPTION b/DESCRIPTION index 947daf8..7953f6a 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -39,6 +39,7 @@ Imports: rstudioapi (>= 0.13), supernova (>= 2.5.1), vctrs (>= 0.4.1), + viridisLite, yesno (>= 0.1.2) Suggests: fivethirtyeight (>= 0.6.2), diff --git a/R/theme.R b/R/theme.R index d2af6c5..bbe05d4 100644 --- a/R/theme.R +++ b/R/theme.R @@ -93,10 +93,12 @@ coursekata_palette <- function(indices = integer(0)) { } #' Create a function that provides a colorblind palette. -#' #' @return A function that accepts one argument `n`, which is the number of colors you want to use #' in the plot. This function is used by scales like `scale_color_discrete` to provide colorblind- -#' safe palettes. See [`scale_discrete_coursekata`] for more information. +#' safe palettes. Where possible, the function will use the hand-picked colors from +#' [`coursekata_palette()`], and when more colors are needed than are available, it will use the +#' [`viridisLite::viridis()`] palette. +#' @seealso scale_discrete_coursekata #' @export coursekata_palette_provider <- function() { unwrap <- function(x) { @@ -111,13 +113,10 @@ coursekata_palette_provider <- function() { provider <- function(n) { if (n > max_values) { - rlang::warn(paste( - glue::glue("This manual palette can handle a maximum of {max_values}."), - glue::glue("You are requesting {n}.") - )) + viridisLite::viridis(n) + } else { + palette[seq_len(n)] } - - palette[seq_len(n)] } structure(provider, max_n = max_values) diff --git a/man/coursekata_palette_provider.Rd b/man/coursekata_palette_provider.Rd index 593142b..7fccea3 100644 --- a/man/coursekata_palette_provider.Rd +++ b/man/coursekata_palette_provider.Rd @@ -9,8 +9,13 @@ coursekata_palette_provider() \value{ A function that accepts one argument \code{n}, which is the number of colors you want to use in the plot. This function is used by scales like \code{scale_color_discrete} to provide colorblind- -safe palettes. See \code{\link{scale_discrete_coursekata}} for more information. +safe palettes. Where possible, the function will use the hand-picked colors from +\code{\link[=coursekata_palette]{coursekata_palette()}}, and when more colors are needed than are available, it will use the +\code{\link[viridisLite:viridis]{viridisLite::viridis()}} palette. } \description{ Create a function that provides a colorblind palette. } +\seealso{ +scale_discrete_coursekata +} diff --git a/tests/testthat/test-palette.R b/tests/testthat/test-palette.R new file mode 100644 index 0000000..d115fbe --- /dev/null +++ b/tests/testthat/test-palette.R @@ -0,0 +1,10 @@ +test_that("it provides hand-picked colors where available", { + max_colors <- length(coursekata_palette()) + provider <- coursekata_palette_provider() + expect_equal(provider(max_colors), as.character(coursekata_palette())) +}) + +test_that("it uses viridisLite when more colors are needed", { + provider <- coursekata_palette_provider() + expect_equal(provider(20), viridisLite::viridis(20)) +}) From f40553bf383424ce4d0dae10abd16c673171c609 Mon Sep 17 00:00:00 2001 From: Adam Blake Date: Fri, 6 Oct 2023 03:56:56 -0700 Subject: [PATCH 9/9] docs: re-knit README --- README.md | 7 +------ man/figures/README-samp_dist_of_b1-1.png | Bin 19176 -> 18683 bytes man/figures/README-samp_dist_of_hp-1.png | Bin 18985 -> 19103 bytes 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/README.md b/README.md index c622f2c..2374ecd 100644 --- a/README.md +++ b/README.md @@ -54,11 +54,6 @@ package: ``` r library(coursekata) -#> ── CourseKata packages ──────────────────────────────────── coursekata 0.11.0 ── -#> ✔ dslabs 0.7.6 ✔ Metrics 0.1.4 -#> ✔ Lock5withR 1.2.2 ✔ lsr 0.5.2 -#> ✔ fivethirtyeightdata 0.1.0 ✔ mosaic 1.8.4.2 -#> ✔ fivethirtyeight 0.6.2 ✔ supernova 2.5.7 ``` - [supernova](https://github.com/UCLATALL/supernova), for @@ -179,7 +174,7 @@ gf_histogram(~Thumb, data = Fingers, fill = ~ middle(Thumb, .80)) ### Toggling the Theme The `ggplot2` theme is loaded by default but can be toggled on and off -via `load_coursekata_themes()` and `restore_default_themes()`. The +via `coursekata_load_theme()` and `coursekata_unload_theme()`. The actual plot theme and scale components are also provided for advanced users as `theme_coursekata()` and `scale_discrete_coursekata()` (`viridis` is used for continuous color scales). diff --git a/man/figures/README-samp_dist_of_b1-1.png b/man/figures/README-samp_dist_of_b1-1.png index 751aa65fcd8616d2c5f29100d80f06c3396e83ef..c44f445bdc00b6b1584a38506e80c3d7ec21423c 100644 GIT binary patch literal 18683 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