feat: accept category = 'All Categories' on cog_spending/cog_revenue
Returns one summed row per (year, govid, subtype) across every category in the requested concept's subtype scope, so a caller never sums categories client-side and cannot sum the wrong scope. Combining it with other category names is an error rather than a silent partial sum.
This commit is contained in:
+10
@@ -8,6 +8,16 @@
|
||||
#' multiplies by 1000 and records the conversion in `provenance`).
|
||||
#'
|
||||
#' @inheritParams cog_spending
|
||||
#' @param category Character vector of category names (from
|
||||
#' `summary_categories.category`), or `NULL` for all categories broken out
|
||||
#' one row each. The reserved value `"All Categories"` instead returns a
|
||||
#' single summed row per `(year, canonical_govid, subtype)`, covering every
|
||||
#' category inside the requested concept's subtype scope. It cannot be
|
||||
#' combined with other category names, and it is not the same thing as
|
||||
#' `revenue_concept = "total"`: the concept chooses which subtypes are in
|
||||
#' scope, `"All Categories"` chooses whether rows inside that scope are
|
||||
#' broken out or summed. Combine with `subtype = "operations"` for an
|
||||
#' own-source revenue total.
|
||||
#' @param revenue_concept Which of Census's two published revenue concepts to
|
||||
#' return. Concepts are defined as sets of the crosswalk's `revenue_subtype`
|
||||
#' values -- never as item-code first letters, which cannot classify
|
||||
|
||||
+24
-3
@@ -71,7 +71,15 @@
|
||||
#' @param govid Character vector of `canonical_govid` values.
|
||||
#' @param years Integer vector of years.
|
||||
#' @param category Character vector of category names (from
|
||||
#' `summary_categories.category`), or `NULL` for all categories.
|
||||
#' `summary_categories.category`), or `NULL` for all categories broken out
|
||||
#' one row each. The reserved value `"All Categories"` instead returns a
|
||||
#' single summed row per `(year, canonical_govid, subtype)`, covering every
|
||||
#' category inside the requested concept's subtype scope. It cannot be
|
||||
#' combined with other category names, and it is not the same thing as
|
||||
#' `expenditure_concept = "total"`: the concept chooses which subtypes are in
|
||||
#' scope, `"All Categories"` chooses whether rows inside that scope are
|
||||
#' broken out or summed. Combine with `subtype = "operations"` for an
|
||||
#' operating-expenditure total.
|
||||
#' @param per_capita If `TRUE`, adds `amt_per_capita_nominal` (and
|
||||
#' `amt_per_capita_real` when `adjust_to_year` is set) using the per-year
|
||||
#' Census F-33 population from `gov_population_yearly`. Result also gains
|
||||
@@ -268,6 +276,17 @@ cog_spending <- function(govid, years, category = NULL,
|
||||
.validate_verb_inputs(govid, years, category, per_capita, adjust_to_year,
|
||||
recipe)
|
||||
|
||||
# Recognize the reserved pseudo-category. Detected after type validation so a
|
||||
# non-character `category` still fails with the ordinary type error.
|
||||
all_categories <- !is.null(category) && .ALL_CATEGORIES %in% category
|
||||
if (all_categories && length(category) > 1L) {
|
||||
cli::cli_abort(c(
|
||||
"{.val {(.ALL_CATEGORIES)}} cannot be combined with other categories.",
|
||||
"i" = "It already sums every category in the requested concept's scope.",
|
||||
"*" = "Ask for it alone, or list the specific categories you want."
|
||||
), class = "uscogdata_all_categories_not_combinable")
|
||||
}
|
||||
|
||||
if (!is.null(recipe) && identical(expenditure_concept, "total")) {
|
||||
cli::cli_abort(c(
|
||||
"`recipe` and `expenditure_concept = \"total\"` are mutually exclusive.",
|
||||
@@ -341,8 +360,10 @@ cog_spending <- function(govid, years, category = NULL,
|
||||
} else {
|
||||
NULL
|
||||
}
|
||||
sql <- .build_verb_sql(view, subtype_col, govid, years, category, ig_view,
|
||||
subtype_scope)
|
||||
sql <- .build_verb_sql(view, subtype_col, govid, years,
|
||||
if (all_categories) NULL else category,
|
||||
ig_view, subtype_scope,
|
||||
all_categories = all_categories)
|
||||
result <- tibble::as_tibble(DBI::dbGetQuery(con, sql))
|
||||
}
|
||||
|
||||
|
||||
+9
-1
@@ -22,7 +22,15 @@ cog_revenue(
|
||||
\item{years}{Integer vector of years.}
|
||||
|
||||
\item{category}{Character vector of category names (from
|
||||
`summary_categories.category`), or `NULL` for all categories.}
|
||||
`summary_categories.category`), or `NULL` for all categories broken out
|
||||
one row each. The reserved value `"All Categories"` instead returns a
|
||||
single summed row per `(year, canonical_govid, subtype)`, covering every
|
||||
category inside the requested concept's subtype scope. It cannot be
|
||||
combined with other category names, and it is not the same thing as
|
||||
`revenue_concept = "total"`: the concept chooses which subtypes are in
|
||||
scope, `"All Categories"` chooses whether rows inside that scope are
|
||||
broken out or summed. Combine with `subtype = "operations"` for an
|
||||
own-source revenue total.}
|
||||
|
||||
\item{per_capita}{If `TRUE`, adds `amt_per_capita_nominal` (and
|
||||
`amt_per_capita_real` when `adjust_to_year` is set) using the per-year
|
||||
|
||||
+9
-1
@@ -22,7 +22,15 @@ cog_spending(
|
||||
\item{years}{Integer vector of years.}
|
||||
|
||||
\item{category}{Character vector of category names (from
|
||||
`summary_categories.category`), or `NULL` for all categories.}
|
||||
`summary_categories.category`), or `NULL` for all categories broken out
|
||||
one row each. The reserved value `"All Categories"` instead returns a
|
||||
single summed row per `(year, canonical_govid, subtype)`, covering every
|
||||
category inside the requested concept's subtype scope. It cannot be
|
||||
combined with other category names, and it is not the same thing as
|
||||
`expenditure_concept = "total"`: the concept chooses which subtypes are in
|
||||
scope, `"All Categories"` chooses whether rows inside that scope are
|
||||
broken out or summed. Combine with `subtype = "operations"` for an
|
||||
operating-expenditure total.}
|
||||
|
||||
\item{per_capita}{If `TRUE`, adds `amt_per_capita_nominal` (and
|
||||
`amt_per_capita_real` when `adjust_to_year` is set) using the per-year
|
||||
|
||||
@@ -37,3 +37,62 @@ test_that(".build_verb_sql is unchanged when all_categories is FALSE", {
|
||||
test_that(".ALL_CATEGORIES is the exact reserved string", {
|
||||
expect_identical(uscogdata:::.ALL_CATEGORIES, "All Categories")
|
||||
})
|
||||
|
||||
test_that('cog_spending(category = "All Categories") sums to the per-category total', {
|
||||
gov <- "552025209777"
|
||||
by_cat <- cog_spending(gov, 2019L)
|
||||
total <- cog_spending(gov, 2019L, category = "All Categories")
|
||||
|
||||
expect_true(nrow(total) > 0L)
|
||||
expect_setequal(unique(total$category), "All Categories")
|
||||
# one row per subtype present in the by-category result
|
||||
expect_setequal(unique(total$spend_subtype), unique(by_cat$spend_subtype))
|
||||
expect_equal(nrow(total), length(unique(by_cat$spend_subtype)))
|
||||
|
||||
# the dollars agree, per subtype
|
||||
lhs <- tapply(by_cat$amt_nominal, by_cat$spend_subtype, sum)
|
||||
rhs <- tapply(total$amt_nominal, total$spend_subtype, sum)
|
||||
expect_equal(as.numeric(rhs[names(lhs)]), as.numeric(lhs), tolerance = 1e-8)
|
||||
})
|
||||
|
||||
test_that('"All Categories" respects expenditure_concept', {
|
||||
gov <- "552025209777"
|
||||
prim <- cog_spending(gov, 2019L, category = "All Categories",
|
||||
expenditure_concept = "primary")
|
||||
dir <- cog_spending(gov, 2019L, category = "All Categories",
|
||||
expenditure_concept = "direct")
|
||||
# direct = primary plus interest and insurance benefits, so it is never smaller
|
||||
expect_gte(sum(dir$amt_nominal), sum(prim$amt_nominal))
|
||||
})
|
||||
|
||||
test_that('"All Categories" works on revenue and respects revenue_concept', {
|
||||
gov <- "552025209777"
|
||||
gen <- cog_revenue(gov, 2019L, category = "All Categories",
|
||||
revenue_concept = "general")
|
||||
tot <- cog_revenue(gov, 2019L, category = "All Categories",
|
||||
revenue_concept = "total")
|
||||
expect_setequal(unique(gen$category), "All Categories")
|
||||
expect_gte(sum(tot$amt_nominal), sum(gen$amt_nominal))
|
||||
})
|
||||
|
||||
test_that('"All Categories" cannot be combined with another category', {
|
||||
expect_error(
|
||||
cog_spending("552025209777", 2019L, category = c("All Categories", "Police")),
|
||||
class = "uscogdata_all_categories_not_combinable"
|
||||
)
|
||||
})
|
||||
|
||||
test_that('"All Categories" is recorded in provenance', {
|
||||
r <- cog_spending("552025209777", 2019L, category = "All Categories")
|
||||
expect_identical(cog_explain(r, format = "list")$category, "All Categories")
|
||||
})
|
||||
|
||||
test_that('"All Categories" combines with subtype to give operating totals', {
|
||||
gov <- "552025209777"
|
||||
ops_by_cat <- cog_spending(gov, 2019L)
|
||||
ops_by_cat <- ops_by_cat[ops_by_cat$spend_subtype == "operations", ]
|
||||
ops_total <- cog_spending(gov, 2019L, category = "All Categories")
|
||||
ops_total <- ops_total[ops_total$spend_subtype == "operations", ]
|
||||
expect_equal(sum(ops_total$amt_nominal), sum(ops_by_cat$amt_nominal),
|
||||
tolerance = 1e-8)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user