feat(provenance): record per-year denominator metadata

Updates transformations\$per_capita with the new denominator_source string,
popyear_range, and pop_source_counts. .attach_per_capita stashes
popyear_range on the result; .verb_spendrev strips the helper attr after
provenance is built.
This commit is contained in:
2026-04-29 19:00:16 -04:00
parent b7ebb4cd88
commit a2ced368f5
3 changed files with 44 additions and 2 deletions
+19 -1
View File
@@ -61,9 +61,27 @@
per_capita = list( per_capita = list(
applied = isTRUE(per_capita), applied = isTRUE(per_capita),
denominator_source = if (isTRUE(per_capita)) { denominator_source = if (isTRUE(per_capita)) {
"ACS 2018-2022 B01003_001 (population_acs from canonical_fips_xwalk)" "Census F-33 population (per-year, from long.population)"
} else { } else {
NA_character_ NA_character_
},
popyear_range = if (isTRUE(per_capita)) {
attr(result, ".popyear_range") %||% integer(0)
} else {
integer(0)
},
pop_source_counts = if (isTRUE(per_capita)) {
ps <- result[["pop_source"]]
if (is.null(ps) || length(ps) == 0L) {
list(census_f33 = 0L, unavailable = 0L)
} else {
list(
census_f33 = sum(ps == "census_f33", na.rm = TRUE),
unavailable = sum(ps == "unavailable", na.rm = TRUE)
)
}
} else {
NULL
} }
), ),
inflation = list( inflation = list(
+10 -1
View File
@@ -79,6 +79,7 @@ cog_spending <- function(govid, years, category = NULL,
prov$scope$govids_found <- scope$found prov$scope$govids_found <- scope$found
prov$scope$govids_missing <- scope$missing prov$scope$govids_missing <- scope$missing
attr(result, "provenance") <- prov attr(result, "provenance") <- prov
attr(result, ".popyear_range") <- NULL
result result
} }
@@ -147,11 +148,12 @@ cog_spending <- function(govid, years, category = NULL,
if (nrow(result) == 0L) { if (nrow(result) == 0L) {
result$amt_per_capita_nominal <- numeric(0) result$amt_per_capita_nominal <- numeric(0)
result$pop_source <- character(0) result$pop_source <- character(0)
attr(result, ".popyear_range") <- integer(0)
return(result) return(result)
} }
years_lit <- paste(unique(as.integer(result$year)), collapse = ",") years_lit <- paste(unique(as.integer(result$year)), collapse = ",")
sql <- sprintf( sql <- sprintf(
"SELECT canonical_govid, year, population "SELECT canonical_govid, year, population, popyear
FROM gov_population_yearly FROM gov_population_yearly
WHERE canonical_govid IN (%s) WHERE canonical_govid IN (%s)
AND year IN (%s)", AND year IN (%s)",
@@ -163,7 +165,14 @@ cog_spending <- function(govid, years, category = NULL,
result$amt_per_capita_nominal <- result$amt_nominal / result$population result$amt_per_capita_nominal <- result$amt_nominal / result$population
result$pop_source <- ifelse(is.na(result$population), result$pop_source <- ifelse(is.na(result$population),
"unavailable", "census_f33") "unavailable", "census_f33")
py <- result$popyear[!is.na(result$popyear)]
attr(result, ".popyear_range") <- if (length(py) > 0L) {
as.integer(c(min(py), max(py)))
} else {
integer(0)
}
result$population <- NULL result$population <- NULL
result$popyear <- NULL
result result
} }
+15
View File
@@ -173,3 +173,18 @@ test_that("aggregate fallback + unavailable pop produce concatenated notes", {
expect_equal(notes[3], expect_equal(notes[3],
"Aggregate fallback applied; see cog_explain(); No population denominator available for this gov type") "Aggregate fallback applied; see cog_explain(); No population denominator available for this gov type")
}) })
test_that("provenance records per-year denominator metadata", {
skip_if_no_corpus()
with_fixture_corpus({
r <- cog_spending("101006006", years = 2019:2020,
category = "Police", per_capita = TRUE)
pc <- attr(r, "provenance")$transformations$per_capita
expect_true(pc$applied)
expect_match(pc$denominator_source, "Census F-33", fixed = FALSE)
expect_match(pc$denominator_source, "per-year", fixed = TRUE)
expect_equal(pc$pop_source_counts$census_f33, nrow(r))
expect_equal(pc$pop_source_counts$unavailable, 0L)
expect_equal(length(pc$popyear_range), 2L)
})
})