Public Welfare understated 32-38% in legacy years with no coverage-gap signpost #9
Closed
opened 2026-07-25 19:45:24 -04:00 by jared
·
3 comments
No Branch/Tag Specified
main
ci/mirror-canonical-tags
chore/release-47-badges-mirror-pr
docs/readme-perf-remeasure-56
feat/pagination-search-balances-57
feat/duckdb-threads-60
feat/cohort-predicates-58
fix/windows-backslash-paths
ci/mirror-to-github
ci/github-actions-matrix
feat/public-release-0.3.0
chore/fixture-sb203
ci/apt-https
fix/pushdown-pagination
feat/all-categories-37
fix/partial-coverage-signposting-9
fix/schema-v7
fix/cog-categories-balance-subtype
feat/cog-balances-25
feat/revenue-concepts-12
feat/expenditure-concepts-11
feat/coverage-disclosure-13
feat/complete-argument-18
fix/kodor-batch-14-15-16
fix/all-scoped-series-breaks-19
fix/regen-fixture-corpus-18
test/walkthrough-findings
feat/expenditure-concept
fix/3-url-trailing-slash
feat/phase-r3-signposting
fix/fixture-option-b-aggregates
feat/phase-r2-harmonization
feat/phase-r1-forward
feat/cog-gov-search-basket-mode
v0.4.0
Labels
Clear labels
kodor
kodor/feature-proposal
kodor/fix
kodor/needs-review
kodor/triaged
madison-walkthrough
severity/high
severity/low
severity/medium
south-guide
verdict/defect
verdict/definitional
kodor
kodor/feature-proposal
kodor/fix
kodor/needs-review
kodor/triaged
Kodor should process this issue
Kodor has written a feature proposal
Kodor should implement a fix (assigned to Kodor)
Kodor's work or failure needs Jared's review
Kodor has already triaged this issue (skip)
Surfaced while building the client-facing Southern API guide
needs
human
Cannot move without a person -- a decision, a check an agent cannot make, something outside the repo
origin
client
Came from a client ask
origin
obligation
Created by a change elsewhere
origin
review
Came from human review
origin
roborev
Promoted from a roborev finding
type
chore
Maintenance with no behaviour change
type
debt
Owed work -- docs, tests, cleanup a change obligated
type
decision
Needs a decision before work can proceed
type
defect
Something is wrong
type
feature
New capability
ws
api
Query verbs and results
ws
corpus
Corpus, mirror, provenance
ws
docs
Vignettes and guides
Assign a task to kodor
Kodor thinks this needs a feature.
Kodor should fix this
Kodor thinks the user is ready to review this.
Kodor is done with this issue.
No labels
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: Civilytics/uscogdata#9
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Found while scoping issue #6; filed separately so the expenditure-concept work stays purely additive.
The defect.
cog_spending(category = "Public Welfare")is understated 32-38% in every legacy year (<=FY2011), and no signpost fires.E67/E68(welfare cash assistance) areis_aggregate = TRUEin the wide era — the same deliberate class asE05, since the wide files publish those families only as aggregates.spending_longfiltersAND NOT is_aggregate, so they are dropped.National measurement (publish_cache, pipeline_commit 05b1fff; $1,000s):
Why this is worse than the Corrections case. Corrections behaves correctly:
E05/F05/G05are also aggregate-flagged, the category returns no rows at all for legacy years, the coverage-gap machinery fires and names the fix —— and the recipe then returns a continuous 2005->2019 series. That is the system working as designed.
Public Welfare is the failure mode:
E74/E75/E77/E79still return rows, so there is no row-absence gap to detect. Verified:attr(r, "provenance")$suggestionsislist()— empty — even thoughwelfare_cash_e67_wideandwelfare_cash_e68_wideexist and would fill it. The user gets a plausible number that is a third too low, silently.Root cause. The signposting trigger is row-absence; the hazard here is component-absence inside a category that still has rows.
Suggested fix. Fire a suggestion when any recipe's components overlap the requested category and are aggregate-suppressed in the requested years.
Reproduce:
Minor, same area:
aggregate_fallbackincog_spending()output is vestigial.spending_longpre-filtersNOT is_aggregate, sobool_and(is_aggregate)is structurally alwaysFALSE(verified 10/10 rows) and the "Aggregate fallback applied; see cog_explain()" note in.notes_column()can never fire. Safe to remove alongside.Cross-reference from the Madison walkthrough audit (
cog_explorer/docs/walkthroughs/FINDINGS.md, 2026-07-28), which hit an adjacent Public Welfare problem and filed it incensus_of_governments_finance_pipeline#61rather than here — recording the relationship so whoever picks either one reads both.This issue is the ≤FY2011 window:
E67/E68areis_aggregate = TRUEin the wide era,spending_longfilters them out, and the category still returns rows so no coverage-gap suggestion fires. Understatement 32-38%.Finding F-007 is a different window and a different mechanism: for Madison,
category == "Public Welfare"has no rows at all — not $0 rows, no rows — for FY2012 through FY2021, then reappears in FY2022 ($17,965,000) and FY2023 ($48,996,000) under item codeE79alone, against a pre-2012 all-time peak across any subtype of $3.9M.The FY2022 four-code-to-one-code narrowing is already explained corpus-side:
cog_explain()on that query surfacesSB171("FY2022 absorbs E74+E75 and J67+J68 per 2022 State Govt Finances Technical Documentation pp.4-5"), which is the coverage machinery working exactly as designed and is why F-007 is rateddefinitional. What is not catalogued is the FY2012–FY2021 absence itself, so that is the part tracked in pipeline#61.Two consequences worth noting for the work in this issue:
E67/E68for ≤FY2011 (this issue) and the FY2012–FY2021 row absence plus the FY2022SB171pivot (pipeline#61).SB171's own advice is to compare FY2022 againstE79+E74+E75+J67+J68for pre-2022 years, which is the same bundle this issue's recipe work touches.Also related:
census_of_governments_finance_pipeline#58(J-prefix crosswalk gap) namesJ67/J68as the modern successors in this same family.No change requested here — filed as context only.
Re-measured against the freshly published corpus (
pipeline_commit aadb46b, 2026-07-31). The defect is real and unchanged in dollars, but the headline severity in the title is wrong by roughly 5x — it should read ~5-10%, not 32-38%.Keeping this open, with a corrected scope.
What changed, and what did not
The dropped dollars are exactly as filed —
E67/E68are stillis_aggregate = TRUEfor 1967-2011 ($624.6B + $126.0B lifetime) andspending_longstill filtersNOT is_aggregate, so they are still absent:The numerator matches the original measurement to the dollar. The denominator is what moved. The original compared against
E79+F79+G79alone; since then the #58/#60 crosswalk batch mappedE74/E75/E77andF77/G77into Public Welfare, and uscogdata#11 admitted theJassistance codes (J67/J68, 2012+). Public Welfare is now a 15-code category, so the same missing dollars are a much smaller share of a much larger base.The root cause is narrower than the issue implies
E67/E68are not merely filtered out — they are not insummary_categoriesat all, so they belong to no category. That matters for the fix: simply mapping them would not work, becausespending_long'sNOT is_aggregatewould still drop them. Under the crosswalk-membership architecture uscogdata#11 introduced, an aggregate-flagged code needs the recipe path, not a category row.The real complaint still stands
The valuable part of this issue is unchanged and still true: no signpost fires.
E74/E75/E77/E79return rows, so there is no row-absence gap for.build_suggestions()to detect, andwelfare_cash_e67_wide/welfare_cash_e68_wideare never surfaced even though they would fill it. The Corrections case works only because that category returns zero rows in legacy years.So the fix is in the suggestion machinery, not the crosswalk: teach it to fire on partial coverage (a category whose recipe-reachable codes carry dollars the result does not), not just on empty years. That is a real design question — a naive version would fire on every category in every legacy year — and it is the thing worth scoping next.
Suggested retitle
Public Welfare understated ~5-10% in legacy years (E67/E68 aggregates) with no coverage-gap signpostFixed in PR #32 (merged
2fc9e758). The trigger was the problem, not the crosswalk..build_suggestions()already identifiedwelfare_cash_e67_wideandwelfare_cash_e68_wideas candidates —J67/J68are Public Welfare members, so the candidate query matched. It then threw them away at thegap_yearsearly return, because E74/E79 returned rows and there was no empty year to detect.A recipe now also qualifies when a component code carries dollars the verb's own long view structurally excludes — aggregate-published, or absent from
summary_categories. Reachability is decided by anti-joining the real view rather than restating its WHERE clause, which is sound because no recipe component is ever renamed by harmonization (now asserted as a test; a reviewer separately verified the converse direction too — nothing harmonizes onto a recipe component).Each suggestion carries
trigger,suppressed_amount,suppressed_years,suppressed_codes.cog_revenue()inherits the fix through the shared verb path: Alaska FY2011Miscellaneous Revenuereported $943,842,000 while dropping $1,899,995,000 of aggregate-publishedU4-rents and royalties — an omission larger than the reported figure.On the "would fire on every category in every legacy year" worry — measured, it does not. Candidates stay gated on the 24-recipe catalog, so it only fires where an actionable recipe exists.
higher_ed_e18_wideandgeneral_gov_e89_widenever fire, because E18/E89 are ordinary classified leaves even pre-2012.Three things the review caught that are worth recording
1. The first cut fabricated dollar claims across flow families. Candidates were filtered for M/L but never for flow family, so a component belonging to the other verb was always absent from this verb's view and got reported as suppressed —
cog_revenue(category = "Corrections")claimed "$3,631,945,000 excluded (E04, E05)" for dollars fully reachable viacog_spending(). Fixed by scoping the measurement to the calling verb's ownflow_prefixes. The deeper fix (scoping the candidate query bycategory_type, which would also stop the mis-scopedempty_yearfire) is uscogdata#34.2. The published schema described a different quantity than the code computes.
suppressed_amountsaid "what the result excludes"; it measures what the long view excludes. Those differ three ways — it under-reports to zero (a component present in the view under a different category contributes 0), it over-reports (item 1), and it can be negative where Census publishes negative amounts.provenance-v1.jsonnow says what the code actually does.3. The anti-join defeated year-partition pruning. The
NOT EXISTSside got no file filter and scanned every partition — on a 1967–2023 corpus read over HTTP. Restating the year/govid literals inside the subquery fixed it. A follow-on attempt to also skip the query entirely on healthy paths was measured, found net-negative (2.7% skip rate, 0% on the batch shape it targeted), and reverted; the real optimization is uscogdata#35.The "minor" sub-item is withdrawn, not implemented
aggregate_fallbackis no longer vestigial..build_verb_sql()usesbool_or(is_aggregate)(R/spending.R), andig_longdeliberately keeps aggregate rows, so the flag is live for the intergovernmental leg —test-expenditure-concept.Rasserts aggregate-sourced IG dollars report TRUE. Removing it would break that disclosure.Reproduction, now signposted
Suite 796 → 843. Also live on the API (cog-api#33).
Follow-ups: uscogdata#33 (decompose
.build_suggestions()), #34 (category_typescoping), #35 (batch-aware suppression skip).jared referenced this issue2026-08-23 16:14:53 -04:00