From a2ced368f5c61958e80a4fd729b3d37fb3fde015 Mon Sep 17 00:00:00 2001 From: Jared Knowles Date: Wed, 29 Apr 2026 19:00:16 -0400 Subject: [PATCH] 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. --- R/provenance.R | 20 +++++++++++++++++++- R/spending.R | 11 ++++++++++- tests/testthat/test-spending.R | 15 +++++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/R/provenance.R b/R/provenance.R index 590d56d..72dd42e 100644 --- a/R/provenance.R +++ b/R/provenance.R @@ -61,9 +61,27 @@ per_capita = list( applied = 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 { 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( diff --git a/R/spending.R b/R/spending.R index 5e0c7eb..78ccd5b 100644 --- a/R/spending.R +++ b/R/spending.R @@ -79,6 +79,7 @@ cog_spending <- function(govid, years, category = NULL, prov$scope$govids_found <- scope$found prov$scope$govids_missing <- scope$missing attr(result, "provenance") <- prov + attr(result, ".popyear_range") <- NULL result } @@ -147,11 +148,12 @@ cog_spending <- function(govid, years, category = NULL, if (nrow(result) == 0L) { result$amt_per_capita_nominal <- numeric(0) result$pop_source <- character(0) + attr(result, ".popyear_range") <- integer(0) return(result) } years_lit <- paste(unique(as.integer(result$year)), collapse = ",") sql <- sprintf( - "SELECT canonical_govid, year, population + "SELECT canonical_govid, year, population, popyear FROM gov_population_yearly WHERE canonical_govid 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$pop_source <- ifelse(is.na(result$population), "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$popyear <- NULL result } diff --git a/tests/testthat/test-spending.R b/tests/testthat/test-spending.R index 8c70a33..6f69faf 100644 --- a/tests/testthat/test-spending.R +++ b/tests/testthat/test-spending.R @@ -173,3 +173,18 @@ test_that("aggregate fallback + unavailable pop produce concatenated notes", { expect_equal(notes[3], "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) + }) +})