cog_spending(), cog_revenue() and cog_balances() gain optional state/type
arguments. Both default to NULL, so every existing govid-based call is
unchanged.
The verbs took a cohort only as a govid vector, which .sql_lit_chr()
rendered into a quoted IN list and .verb_spendrev() embedded into 5-8
separate statements per call: the scope check, the main aggregate, the
per-capita join, the harmonization block, and the suggestion and
suppression queries. For type = "city" that list is 301,589 characters,
parsed and planned from scratch every time it appears.
Passing state/type instead expresses the cohort as a subquery against
canonical_fips_xwalk, so its size never enters the SQL string at all.
Measured on the production corpus, same FY2022 aggregate over the
20,106-government city cohort, DUCKDB_THREADS=2, median of 5:
IN (20,106 literals) -- 0.3.0 432 ms
join against a temp cohort table 132 ms
predicate on canonical_fips_xwalk 102 ms
no cohort filter at all (the floor) 105 ms
The predicate reaches the no-filter floor: the cohort restriction is
now free. End to end through cog_spending(category = "Police"),
1080 ms -> 271 ms, 3.99x -- larger than the single-query saving,
because the repetition across statements is what actually cost.
Design decisions, both made explicitly rather than left implicit:
- govid AND state/type INTERSECT. "These ids, narrowed to that
state/type" is a real query, and an error here could never be
relaxed later without breaking callers.
- A predicate cohort has no id list to report, so
provenance$scope$govids_found/govids_missing stay empty and a new
scope$cohort block carries state, type and n_governments. Resolving
the ids just to report them would put 20,000 govids in every
fleet-scale response body -- the cost this change removes. A
govid-named cohort's provenance is untouched.
state/type are coerced with .coerce_state_to_fips()/.coerce_type(), the
same helpers cog_gov_search() uses. That is load-bearing: the argument
is a postal abbreviation ("WI") while fips_state holds a FIPS code
("55"), and a predicate on the raw parameter matches nothing and returns
an empty result indistinguishable from "reported nothing". cog-api hit
exactly this trap optimizing the same path.
.attach_per_capita() now keys its population lookup on the govids present
in the result rather than the requested cohort. Those are the only ones
its LEFT JOIN can match, so the output is identical -- but it needs no id
list, and on a paginated call it looks up one page instead of the fleet.
Fixes uscogdata#58.
150 lines
6.0 KiB
R
150 lines
6.0 KiB
R
# R/complete.R
|
|
#
|
|
# `complete = TRUE` on the money verbs. Fills the requested grid so that a
|
|
# cell the corpus does not carry still appears, labelled with WHY it is
|
|
# missing.
|
|
#
|
|
# The corpus stopped storing the wide era's explicit zeros
|
|
# (cog_pipeline#64, series break SB194), which made absence ambiguous:
|
|
#
|
|
# <= FY2011 dense_source absent => Census published $0 (census_zero)
|
|
# >= FY2012 sparse_source absent => not reported, unknown (not_reported)
|
|
#
|
|
# Before sparsification a wide-era query whose cells were all $0 came back as
|
|
# explicit $0 rows; afterwards it came back empty, with nothing to say which
|
|
# of the two meanings applied. This restores that -- and improves on it,
|
|
# because the pre-sparsification corpus could not distinguish the two either.
|
|
#
|
|
# `census_zero` fills carry `amt_nominal = 0`; `not_reported` fills carry NA.
|
|
# That difference is the entire point: writing 0 into a modern absence would
|
|
# invent data, which is the error the representation contract exists to stop.
|
|
|
|
#' @noRd
|
|
.abort_complete_unsupported <- function(reason, alternative) {
|
|
cli::cli_abort(c(
|
|
"{.code complete = TRUE} is not supported for this query.",
|
|
x = reason,
|
|
i = alternative
|
|
), class = "uscogdata_complete_unsupported")
|
|
}
|
|
|
|
#' @noRd
|
|
.require_representation <- function(con, manifest) {
|
|
needed <- c("representation.parquet", "code_set.parquet")
|
|
missing <- needed[!vapply(needed, function(f) .corpus_has_table(manifest, f),
|
|
logical(1))]
|
|
if (length(missing) == 0L) return(invisible(TRUE))
|
|
cli::cli_abort(c(
|
|
"This corpus does not publish the representation contract.",
|
|
x = "Missing: {.file {missing}}.",
|
|
i = "{.code complete = TRUE} needs those tables to know whether an absent cell means Census published $0 or means the government did not report.",
|
|
i = "They ship with corpora published from 2026-07-29 onward; re-point {.envvar USCOGDATA_URL} at a current corpus, or omit {.code complete}."
|
|
), class = "uscogdata_representation_unavailable")
|
|
}
|
|
|
|
#' The cells a government-year COULD carry: every code in force for that
|
|
#' government's own type, mapped through `summary_categories`, restricted to
|
|
#' the calling verb's crosswalk subtype scope (the same subtype-membership
|
|
#' classification the verb SQL itself uses -- e.g. the `primary` concept's
|
|
#' operations/capital/assistance) and (when given) its category filter.
|
|
#'
|
|
#' Scoped by `govs_type` deliberately. Filling against the union of all types
|
|
#' would invent cells that the government can never report -- a county row for
|
|
#' "state IG transfer to school districts" -- and those inventions would then
|
|
#' be indistinguishable from real census zeros.
|
|
#'
|
|
#' `NOT cs.is_aggregate` mirrors `spending_long` / `revenue_long`, which drop
|
|
#' aggregate rows. Without it the grid would offer cells the verb structurally
|
|
#' never returns, so every one of them would fill as a phantom $0.
|
|
#' @noRd
|
|
.completion_grid_sql <- function(subtype_col, cohort, years, category,
|
|
subtype_scope) {
|
|
category_pred <- if (is.null(category)) {
|
|
""
|
|
} else {
|
|
sprintf("AND c.category IN (%s)", .sql_lit_chr(category))
|
|
}
|
|
sprintf(
|
|
"SELECT DISTINCT
|
|
cs.year,
|
|
x.canonical_govid,
|
|
x.gov_name,
|
|
c.%1$s AS subtype_value,
|
|
c.category,
|
|
r.absence_means
|
|
FROM code_set cs
|
|
JOIN canonical_fips_xwalk x ON x.govs_type = cs.type
|
|
JOIN summary_categories c ON c.item_code = cs.item_code
|
|
JOIN representation r ON r.year = cs.year
|
|
WHERE %2$s
|
|
AND cs.year IN (%3$s)
|
|
AND NOT cs.is_aggregate
|
|
AND c.category IS NOT NULL
|
|
AND c.%1$s IN (%4$s)
|
|
%5$s",
|
|
subtype_col, .cohort_sql(cohort, "x.canonical_govid"),
|
|
paste(as.integer(years), collapse = ","),
|
|
.sql_lit_chr(subtype_scope), category_pred
|
|
)
|
|
}
|
|
|
|
#' Fill `result` out to the full grid, stamping `value_source` on every row.
|
|
#'
|
|
#' Returns the completed tibble with a `.completion` attribute carrying the
|
|
#' provenance block. Reported rows are passed through untouched -- filling
|
|
#' must never alter or drop what the corpus actually published.
|
|
#' @noRd
|
|
.complete_result <- function(result, con, subtype_col, cohort, years, category,
|
|
subtype_scope) {
|
|
grid <- tibble::as_tibble(DBI::dbGetQuery(
|
|
con, .completion_grid_sql(subtype_col, cohort, years, category, subtype_scope)
|
|
))
|
|
|
|
result$value_source <- rep("reported", nrow(result))
|
|
if (nrow(grid) == 0L) {
|
|
attr(result, ".completion") <- list(
|
|
applied = TRUE, rows_filled = 0L, absence_means = list()
|
|
)
|
|
return(result)
|
|
}
|
|
|
|
names(grid)[names(grid) == "subtype_value"] <- subtype_col
|
|
key <- function(d) {
|
|
paste(d$year, d$canonical_govid, d[[subtype_col]], d$category, sep = "\r")
|
|
}
|
|
missing <- grid[!key(grid) %in% key(result), , drop = FALSE]
|
|
|
|
if (nrow(missing) > 0L) {
|
|
filled <- tibble::tibble(
|
|
year = as.integer(missing$year),
|
|
canonical_govid = as.character(missing$canonical_govid),
|
|
gov_name = as.character(missing$gov_name),
|
|
category = as.character(missing$category),
|
|
# census_zero is a value Census published; not_reported is unknown and
|
|
# must stay NA. Collapsing the two to 0 is the defect, not the fill.
|
|
amt_nominal = ifelse(missing$absence_means == "census_zero",
|
|
0, NA_real_),
|
|
codes_included = NA_character_,
|
|
aggregate_fallback = NA,
|
|
value_source = as.character(missing$absence_means)
|
|
)
|
|
filled[[subtype_col]] <- as.character(missing[[subtype_col]])
|
|
if ("notes" %in% names(result)) filled$notes <- NA_character_
|
|
|
|
result <- dplyr::bind_rows(result, filled)
|
|
result <- result[order(result$year, result$canonical_govid,
|
|
result[[subtype_col]], result$category), ,
|
|
drop = FALSE]
|
|
}
|
|
|
|
rules <- unique(grid[, c("year", "absence_means")])
|
|
attr(result, ".completion") <- list(
|
|
applied = TRUE,
|
|
rows_filled = nrow(missing),
|
|
absence_means = stats::setNames(
|
|
as.list(as.character(rules$absence_means)), as.character(rules$year)
|
|
)
|
|
)
|
|
result
|
|
}
|