feat: cog_recipes + recipe= + signposting suggestions
Adds cog_recipes() to list the curated harmonization_recipes catalog (24 recipes / schema_version >= 5), and a recipe= argument on cog_spending()/ cog_revenue() that runs a recipe's generic multi-code join instead of the category view: SUM(amt * weight) across whichever component codes are present for a (year, canonical_govid), scoped by gov_type_scope. The join deliberately does not filter is_aggregate -- the wide era (<= 2011) exposes these split families (corrections 04+05, IG *89/*47, U4- rents, etc.) ONLY as aggregate rows, with leaf codes first appearing in 2012, so excluding aggregates would zero out the wide-era half of every recipe. This is safe by corpus construction: wide-era rows are aggregate-only, modern rows are leaf-only, and every component is year-scoped, so there is no double-counting. recipe= is mutually exclusive with category=; the result's subtype column reads "recipe" and category reads the recipe's label. Adds recipe-component-driven signposting: when a basis="harmonized" + category query comes back with zero rows in a requested year, and a harmonization recipe covering that category would actually produce rows for this government in that year (via the same join .run_recipe() uses), the recipe is surfaced in provenance$suggestions plus one cli::cli_inform() message. This is deliberately keyed off recipe components rather than harmonization_map's suggested_recipe_id column (which is empty on every live row -- the wide era's split families are NA-by-construction via aggregate exclusion, not an NA ruling to hang a suggestion off of). Also populates the previously-always-empty provenance$series_break_refs (schema v5 only: series_breaks_pq rows whose fin_code is among the observed codes and whose break_year falls in the requested span), and extends cog_explain() with Basis/Harmonization/Recipe/Suggestions/Series breaks sections.
This commit is contained in:
+64
@@ -51,6 +51,15 @@ cog_explain <- function(result, format = c("print", "list")) {
|
||||
cli::cli_text("Category: (all)")
|
||||
}
|
||||
|
||||
if (!is.null(prov$basis)) {
|
||||
note <- if (!is.null(prov$basis_note) && !is.na(prov$basis_note)) {
|
||||
sprintf(" (%s)", prov$basis_note)
|
||||
} else {
|
||||
""
|
||||
}
|
||||
cli::cli_text("Basis: {prov$basis}{note}")
|
||||
}
|
||||
|
||||
cli::cli_h2("Codes observed")
|
||||
codes <- prov$codes_summed$observed
|
||||
if (length(codes) == 0L) {
|
||||
@@ -66,6 +75,39 @@ cog_explain <- function(result, format = c("print", "list")) {
|
||||
)
|
||||
}
|
||||
|
||||
h <- prov$harmonization
|
||||
if (!is.null(h) && isTRUE(h$applied)) {
|
||||
cli::cli_h2("Harmonization")
|
||||
cli::cli_text(
|
||||
"Excluded {h$na_rows_excluded} row(s) with no harmonized_code (${format(h$na_amount_excluded, big.mark = ',')})"
|
||||
)
|
||||
}
|
||||
|
||||
rc <- prov$recipe
|
||||
if (!is.null(rc)) {
|
||||
cli::cli_h2("Recipe")
|
||||
cli::cli_text("{rc$recipe_id}: {rc$label}")
|
||||
comp_lines <- vapply(rc$components, function(x) {
|
||||
sprintf("%s (%s, %s-%s, weight=%s)", x$component_code, x$gov_type_scope,
|
||||
x$year_min, x$year_max, x$weight)
|
||||
}, character(1))
|
||||
cli::cli_ul(comp_lines)
|
||||
}
|
||||
|
||||
if (length(prov$suggestions) > 0L) {
|
||||
cli::cli_h2("Suggestions")
|
||||
sugg_lines <- vapply(prov$suggestions, function(s) {
|
||||
sprintf("%s -- %s (years %s-%s): %s", s$recipe_id, s$label,
|
||||
s$available_years[1], s$available_years[2], s$hint)
|
||||
}, character(1))
|
||||
cli::cli_ul(sugg_lines)
|
||||
}
|
||||
|
||||
if (length(prov$series_break_refs) > 0L) {
|
||||
cli::cli_h2("Series breaks")
|
||||
cli::cli_ul(.series_break_story_lines(prov$series_break_refs))
|
||||
}
|
||||
|
||||
cli::cli_h2("Transformations")
|
||||
uc <- prov$transformations$units_conversion
|
||||
if (isTRUE(uc$applied)) {
|
||||
@@ -109,6 +151,28 @@ cog_explain <- function(result, format = c("print", "list")) {
|
||||
invisible(NULL)
|
||||
}
|
||||
|
||||
# One "break-story" line per referenced break_id: "SB109 (2005): <join_advice>".
|
||||
# Re-queries series_breaks_pq for the detail (break_year, join_advice) that
|
||||
# provenance$series_break_refs deliberately doesn't carry (the schema keeps
|
||||
# that field to a plain id array). Falls back to bare ids if no session is
|
||||
# available (e.g. explaining a result after cog_close()) rather than
|
||||
# erroring cog_explain() over a cosmetic detail.
|
||||
#' @noRd
|
||||
.series_break_story_lines <- function(break_ids) {
|
||||
con <- tryCatch(.ensure_session(), error = function(e) NULL)
|
||||
if (is.null(con) || !DBI::dbIsValid(con)) return(break_ids)
|
||||
detail <- tryCatch(
|
||||
DBI::dbGetQuery(con, sprintf(
|
||||
"SELECT break_id, break_year, join_advice FROM series_breaks_pq
|
||||
WHERE break_id IN (%s) ORDER BY break_id",
|
||||
.sql_lit_chr(break_ids)
|
||||
)),
|
||||
error = function(e) NULL
|
||||
)
|
||||
if (is.null(detail) || nrow(detail) == 0L) return(break_ids)
|
||||
sprintf("%s (%s): %s", detail$break_id, detail$break_year, detail$join_advice)
|
||||
}
|
||||
|
||||
# Expand a 2-digit Census popyear (e.g. 19) to a 4-digit calendar year (2019).
|
||||
# F-33 metadata stores popyear as 2 digits; pivot at 70 to handle a future
|
||||
# corpus that ever spans pre-1970 vintages, though current scope is 2000+.
|
||||
|
||||
Reference in New Issue
Block a user