feat: name the intergovernmental counterpart in firing recipe suggestions
Closes uscogdata #6 item 4. Only extends suggestions that already fire -- a concept hint on every healthy call would be noise.
This commit is contained in:
+64
-3
@@ -98,19 +98,80 @@
|
|||||||
hint = sprintf("re-run with recipe = '%s'", rid)
|
hint = sprintf("re-run with recipe = '%s'", rid)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
suggestions
|
.attach_ig_counterparts(con, suggestions)
|
||||||
|
}
|
||||||
|
|
||||||
|
#' Attach `ig_recipe_id` to each suggestion: the intergovernmental-expenditure
|
||||||
|
#' recipe (an M-to-local or L-to-state recipe) whose component codes cover
|
||||||
|
#' exactly the same set of function suffixes as the firing recipe's own
|
||||||
|
#' components, e.g. `corrections_combined`'s {E04, E05} -> suffixes {"04",
|
||||||
|
#' "05"} matches `corrections_ig_local_combined`'s {M04, M05} -> the same
|
||||||
|
#' {"04", "05"}. `NULL` when no such recipe exists, which also covers the
|
||||||
|
#' case where the firing recipe already IS the IG recipe (self-matches are
|
||||||
|
#' excluded, so an IG recipe never names itself as its own counterpart).
|
||||||
|
#'
|
||||||
|
#' Matching is deliberately an exact set match, not "any suffix in common":
|
||||||
|
#' the two-digit suffix only means the same "function" across recipes that
|
||||||
|
#' share the underlying Census functional-classification scheme (E/F/G/L/M
|
||||||
|
#' all use "04"/"05" for corrections). M/L "combined other" codes (47/89/
|
||||||
|
#' 91-94) reuse digits for an unrelated catch-all construct, so e.g.
|
||||||
|
#' `general_gov_e89_wide`'s {E85, E89} -> {"85", "89"} must NOT match
|
||||||
|
#' `ige_local_m89_wide`'s {"89", "91", "92", "93"} on the shared "89" alone --
|
||||||
|
#' verified against the fixture's full `harmonization_recipes` catalog (see
|
||||||
|
#' task-6-report.md): only the corrections family (E/F/G/M, suffixes 04/05)
|
||||||
|
#' has an exact-set match in this corpus.
|
||||||
|
#' @noRd
|
||||||
|
.attach_ig_counterparts <- function(con, suggestions) {
|
||||||
|
if (length(suggestions) == 0L) return(suggestions)
|
||||||
|
|
||||||
|
comp <- DBI::dbGetQuery(con,
|
||||||
|
"SELECT recipe_id, component_code FROM harmonization_recipes")
|
||||||
|
comp$suffix <- substr(comp$component_code, 2L, nchar(comp$component_code))
|
||||||
|
suffix_sets <- lapply(split(comp$suffix, comp$recipe_id), function(x) sort(unique(x)))
|
||||||
|
|
||||||
|
ig_recipe_ids <- unique(
|
||||||
|
comp$recipe_id[substr(comp$component_code, 1L, 1L) %in% c("M", "L")]
|
||||||
|
)
|
||||||
|
|
||||||
|
find_counterpart <- function(rid) {
|
||||||
|
own <- suffix_sets[[rid]]
|
||||||
|
if (is.null(own)) return(NULL)
|
||||||
|
for (cand in ig_recipe_ids) {
|
||||||
|
if (identical(cand, rid)) next
|
||||||
|
if (setequal(suffix_sets[[cand]], own)) return(cand)
|
||||||
|
}
|
||||||
|
NULL
|
||||||
|
}
|
||||||
|
|
||||||
|
lapply(suggestions, function(s) {
|
||||||
|
# `s$ig_recipe_id <- NULL` would DELETE the element rather than set it
|
||||||
|
# (standard R list-assignment gotcha), leaving no-match entries missing
|
||||||
|
# the key entirely instead of carrying it as NULL. Single-bracket
|
||||||
|
# assignment with a wrapped list preserves a NULL-valued element so the
|
||||||
|
# field is always present, per the brief's "NULL when there is none".
|
||||||
|
s["ig_recipe_id"] <- list(find_counterpart(s$recipe_id))
|
||||||
|
s
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#' Emit the single cli::cli_inform() message summarizing all suggestions
|
#' Emit the single cli::cli_inform() message summarizing all suggestions
|
||||||
#' for a verb call (the brief's "one message", not one per suggestion).
|
#' for a verb call (the brief's "one message", not one per suggestion).
|
||||||
#' Bullet text is pre-formatted plain text (no cli/glue `{}` markup) since
|
#' Bullet text is pre-formatted plain text (no cli/glue `{}` markup) since
|
||||||
#' recipe ids/labels are untrusted-ish data values, not literal call-site
|
#' recipe ids/labels are untrusted-ish data values, not literal call-site
|
||||||
#' expressions.
|
#' expressions. When a suggestion has an `ig_recipe_id`, one indented
|
||||||
|
#' continuation line is appended naming the intergovernmental counterpart
|
||||||
|
#' recipe (embedded `\n` renders as a hanging-indent continuation of the
|
||||||
|
#' same bullet under cli, not a new bullet).
|
||||||
#' @noRd
|
#' @noRd
|
||||||
.inform_suggestions <- function(suggestions) {
|
.inform_suggestions <- function(suggestions) {
|
||||||
bullets <- vapply(suggestions, function(s) {
|
bullets <- vapply(suggestions, function(s) {
|
||||||
sprintf("%s (%d-%d): %s", s$recipe_id,
|
bullet <- sprintf("%s (%d-%d): %s", s$recipe_id,
|
||||||
s$available_years[1], s$available_years[2], s$hint)
|
s$available_years[1], s$available_years[2], s$hint)
|
||||||
|
if (!is.null(s$ig_recipe_id)) {
|
||||||
|
bullet <- paste0(bullet, sprintf(
|
||||||
|
"\n intergovernmental counterpart: recipe = '%s'", s$ig_recipe_id))
|
||||||
|
}
|
||||||
|
bullet
|
||||||
}, character(1))
|
}, character(1))
|
||||||
cli::cli_inform(c(
|
cli::cli_inform(c(
|
||||||
i = "Coverage gap detected for the requested years; a harmonization recipe may fill it:",
|
i = "Coverage gap detected for the requested years; a harmonization recipe may fill it:",
|
||||||
|
|||||||
@@ -264,3 +264,22 @@ test_that("the provenance schema documents expenditure_concept", {
|
|||||||
)
|
)
|
||||||
expect_true("expenditure_concept" %in% names(sch$properties))
|
expect_true("expenditure_concept" %in% names(sch$properties))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test_that("a firing suggestion names the intergovernmental counterpart recipe", {
|
||||||
|
# Corrections has no legacy leaf rows, so the coverage-gap suggestion fires;
|
||||||
|
# corrections_ig_local_combined is its IG counterpart.
|
||||||
|
r <- suppressMessages(
|
||||||
|
cog_spending("010000226085", years = c(2005, 2011), category = "Corrections")
|
||||||
|
)
|
||||||
|
sugg <- attr(r, "provenance")$suggestions
|
||||||
|
expect_gt(length(sugg), 0L)
|
||||||
|
ids <- vapply(sugg, function(s) s$recipe_id %||% "", character(1))
|
||||||
|
expect_true("corrections_combined" %in% ids)
|
||||||
|
ig <- unlist(lapply(sugg, function(s) s$ig_recipe_id))
|
||||||
|
expect_true("corrections_ig_local_combined" %in% ig)
|
||||||
|
})
|
||||||
|
|
||||||
|
test_that("no suggestion fires for a healthy query", {
|
||||||
|
r <- cog_spending("010000226085", years = 2019, category = "Police")
|
||||||
|
expect_length(attr(r, "provenance")$suggestions, 0L)
|
||||||
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user