R/tbl-metric.R

Defines functions prune_dimensions distinct.tbl_metric transmute.tbl_metric rename.tbl_metric anti_join.tbl_metric semi_join.tbl_metric right_join.tbl_metric left_join.tbl_metric inner_join.tbl_metric summarise.tbl_metric group_by.tbl_metric mutate.tbl_metric arrange.tbl_metric select.tbl_metric filter.tbl_metric reclass.tbl_metric reclass as_tibble.tbl_metric as_tbl_metric condense_metric check_metric print.tbl_metric

Documented in anti_join.tbl_metric arrange.tbl_metric as_tibble.tbl_metric check_metric condense_metric distinct.tbl_metric filter.tbl_metric group_by.tbl_metric inner_join.tbl_metric left_join.tbl_metric mutate.tbl_metric print.tbl_metric reclass rename.tbl_metric right_join.tbl_metric select.tbl_metric semi_join.tbl_metric summarise.tbl_metric transmute.tbl_metric

# A tbl_metric is an S3 class built around a tbl_df, which generally contains a period,
# a date, some number of dimensions, and a value. It supports almost all dplyr operations.

#' S3 operators for metrics, including printing and coercing to a data frame
#'
#' @param x A tbl_metric
#' @param ... Extra arguments, not used.
#'
#' @name metric-methods
#'
#' @import dplyr
#'
#' @export
print.tbl_metric <- function(x, ...) {
  periods <- unique(x$period)
  m <- attr(x, "metadata")

  header <- paste0(
    "# Metric: ", m$title, " (", m$metric_full, ")\n",
    "# Dimensions: ", paste(var_names_dimensions(x), collapse = ", "), "\n"
  )

  if (!all(is.na(x$date))) {
    header <- paste0(header, "# Dates: ", min(x$date, na.rm = TRUE), " to ", max(x$date, na.rm = TRUE), "\n")
  }
  header <- paste0(header, "# Periods: ", paste(periods, collapse = ", "), "\n")
  if (!is.null(m$updated_at)) {
    header <- paste0(header, "# Updated At: ", m$updated_at, "\n")
  }

  cat(pillar::style_subtle(header))

  NextMethod()
}

#' Perform sanity checks on a metric object
#'
#' This function previously worked on metric data tables, but it now works on metric objects
#' (which contain all the metadata, documentation, and everything needed to plot).
#'
#' @param metric A metric table, as found in the data field of a metric object
#'
#' @export
check_metric <- function(metric) {
  assertthat::assert_that(inherits(metric, "tbl_metric"),
    msg = "Not a 'tbl_metric' object (check_metric parses tbl_metric objects)"
  )

  # Need metric_full to print error messages
  metadata <- attr(metric, "metadata")
  assertthat::assert_that("metric_full" %in% names(metadata),
    msg = "Missing metric_full field in metric object"
  )

  context_name <- metadata$metric_full

  # check the rest
  expected_names <- c(
    "metric", "title", "description", "category",
    "subcategory", "owner"
  )

  for (n in expected_names) {
    assertthat::assert_that(n %in% names(metadata),
      msg = glue::glue("Missing { n } field ({ context_name })")
    )
  }

  ## check the data
  assertthat::assert_that(inherits(metric, "tbl_df"),
    msg = glue::glue("Metric data should be a tbl_df ({ context_name })")
  )
  assertthat::assert_that(nrow(metric) > 0,
    msg = glue::glue("Metric data should have at least one row ({ context_name })")
  )

  fields <- colnames(metric)
  fields_numeric <- metric %>%
    select_if(is.numeric) %>%
    colnames()
  fields_dimensions <- var_names_dimensions(metric)

  assertthat::assert_that(
    "date" %in% fields,
    msg = glue::glue("A metric table should have a field named date ({ context_name })")
  )
  assertthat::assert_that(
    "period" %in% fields,
    msg = glue::glue("A metric table should have a field named period ({ context_name })")
  )
  assertthat::assert_that(
    length(fields_numeric) >= 1,
    msg = glue::glue("A metric table should have at least one numeric field  ({ context_name })")
  )


  d <- metadata$dimensions

  # check dimension documentation
  for (dn in names(d)) {
    assertthat::assert_that("title" %in% names(d[[dn]]),
      msg = glue::glue("Missing title in dimension { dn } ({ context_name })")
    )
    assertthat::assert_that("description" %in% names(d[[dn]]),
      msg = glue::glue("Missing title in dimension { dn } ({ context_name })")
    )
  }
}

#' Condense a metric_tbl object to remove cases with multiple non-All dimensions
#'
#' This reduces the size of a metrics table, by limiting the number of dimensions
#' that can be anything besides All at the same time. If there is a `min_dimensions`
#' field in the metric metadata, it never condenses beyond that (this is useful for some
#' that need multiple dimensions to be interpretable)
#'
#' @param metric A `tbl_metric` object
#' @param max_dimensions The number of (non-All) dimensions that each row
#' can have
#'
#' @export
condense_metric <- function(metric, max_dimensions = 2) {
  min_dimensions <- attr(metric, "metadata")$min_dimensions
  if (!is.null(min_dimensions)) {
    max_dimensions <- max(min_dimensions, max_dimensions)
  }

  dims <- var_names_dimensions(metric)
  dimensions <- as.matrix(metric[, dims])
  num_not_all <- rowSums(dimensions != "All")

  ret <- metric[num_not_all <= max_dimensions, ]

  # If it's a tbl_metric, keep it that way
  class(ret) <- class(metric)
  attr(ret, "metadata") <- attr(metric, "metadata")
  ret
}


### S3 methods

as_tbl_metric <- function(x) {
  class(x) <- c("tbl_metric", class(x))
  x
}

#' Metric dplyr S3 methods
#'
#' @param .data A `tbl_metric` object
#' @param x For as_data_frame, the
#' @param ... Arguments passed on to the appropriate dplyr metric
#'
#' @importFrom dplyr as_tibble anti_join arrange filter group_by inner_join
#' left_join mutate rename right_join select semi_join summarise
#' transmute
#'
#' @name metric-s3
#' @export
as_tibble.tbl_metric <- function(x, ...) {
  class(x) <- class(x)[class(x) != "tbl_metric"]
  x
}

#' Copy class and attributes from the original version of an object to a modified version.
#'
#' Copied over from https://github.com/tidyverse/dplyr/issues/719
#' @export
#' @param x The original object, which has a class/attributes to copy
#' @param result The modified object, which is / might be missing the
#'   class/attributes.
#'
#' @return `result`, now with class/attributes restored.
reclass <- function(x, result) {
  UseMethod("reclass")
}

#' @export
reclass.tbl_metric <- function(x, result) {
  class(result) <- unique(c(class(x)[[1]], class(result)))
  attr(result, class(x)[[1]]) <- attr(x, class(x)[[1]])
  attr(result, "metadata") <- attr(x, "metadata")
  result
}

#' @export
reclass.tbl_metric_group <- reclass.tbl_metric


#' @importFrom dplyr filter
#' @export
dplyr::filter

#' @rdname metric-s3
#' @export
filter.tbl_metric <- function(.data, ...) {
  reclass(.data, NextMethod())
}

#' @rdname metric-s3
#' @export
select.tbl_metric <- function(.data, ...) {
  reclass(.data, NextMethod())
}

#' @rdname metric-s3
#' @export
arrange.tbl_metric <- function(.data, ...) {
  reclass(.data, NextMethod())
}

#' @rdname metric-s3
#' @export
mutate.tbl_metric <- function(.data, ...) {
  reclass(.data, NextMethod())
}

#' @rdname metric-s3
#' @export
group_by.tbl_metric <- function(.data, ...) {
  reclass(.data, NextMethod())
}

#' @rdname metric-s3
#' @export
summarise.tbl_metric <- function(.data, ...) {
  reclass(.data, NextMethod())
}

#' @rdname metric-s3
#' @export
inner_join.tbl_metric <- function(.data, ...) {
  reclass(.data, NextMethod())
}

#' @rdname metric-s3
#' @export
left_join.tbl_metric <- function(.data, ...) {
  reclass(.data, NextMethod())
}

#' @rdname metric-s3
#' @export
right_join.tbl_metric <- function(.data, ...) {
  reclass(.data, NextMethod())
}

#' @rdname metric-s3
#' @export
semi_join.tbl_metric <- function(.data, ...) {
  reclass(.data, NextMethod())
}

#' @rdname metric-s3
#' @export
anti_join.tbl_metric <- function(.data, ...) {
  reclass(.data, NextMethod())
}

#' @rdname metric-s3
#' @export
rename.tbl_metric <- function(.data, ...) {
  reclass(.data, NextMethod())
}

#' @rdname metric-s3
#' @export
transmute.tbl_metric <- function(.data, ...) {
  reclass(.data, NextMethod())
}


#' @rdname metric-s3
#' @export
distinct.tbl_metric <- function(.data, ...) {
  .data <- tibble::as_data_frame(.data)
  NextMethod()
}

# utilities
prune_dimensions <- function(metric) {
  metadata <- attr(metric, "metadata")
  names_dimensions <- intersect(
    names(metadata$dimensions),
    colnames(metric)
  )
  metadata$dimensions <- metadata$dimensions[names_dimensions]
  attr(metric, "metadata") <- metadata
  return(metric)
}
datacamp/tidymetrics documentation built on March 21, 2021, 3:28 a.m.