flow_prefixes cannot classify item codes correctly: implement the three-concept expenditure model on the crosswalk spend_type #11
Closed
opened 2026-07-29 00:05:28 -04:00 by jared
·
2 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.
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#11
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.
Filed from the Madison walkthrough audit (
cog_explorer/docs/walkthroughs/FINDINGS.md, 2026-07-28).Root cause
cog_spending()andcog_revenue()classify item codes with a fixed first-letter allowlist —flow_prefixes = c("E","F","G")andc("T","A","U","B","C","D")respectively, wired through thespending_long/revenue_longSQL views (WHERE LEFT(item_code, 1) IN (...)). That architecture cannot be patched code-by-code, because at least one prefix carries both revenue and expenditure codes under a single letter:No single-letter allowlist routes those four correctly. The crosswalk's
spend_typecolumn, assigned per code rather than per prefix, does.Three findings are three consequences of that one architecture.
Findings resolved
Ymixes revenue and expenditure codes under one first letter, which is why a prefix-based filter cannot classify it correctly by construction — the architectural root causecog_spending(..., expenditure_concept = "direct")excludes interest on long-term debt from every total, ~7.1% below Census's own published "Direct Expenditure" for Madison FY2022Q12/Q18(state intergovernmental transfers to school districts) are missing from both verbs, sototal— the concept that is supposed to include intergovernmental transfers — is understated for state governments tooReproduction (verbatim from FINDINGS.md, verified against the live corpus)
F-018
F-012
F-017
Wisconsin state FY2022:
Q12+Q18= $8,279,354,000 absent fromtotal.Why it matters
Every "direct spending" total this package produces — for all 55 years, for every government in the corpus — is below Census's own published concept of the same name by exactly the excluded interest. For Madison FY2022 that is -7.1%. Unlike a units question, it is not reversible through the public interface: the
I/J/Yrows exist in the corpus but no argument or column returns them, so recovering the broader figure requires a code change here. And none of the mechanisms this same function already has for flagging correct-but-surprising results (aggregate_fallback,pop_source == "unavailable",provenance$expenditure_concept_direct_suppressed) fires. For state governments,total— the concept explicitly meant to include intergovernmental transfers — is missing $8.3B of them for a single state in a single year.The agreed design (settled 2026-07-28 by the project owner — not open for re-litigation)
Three named concepts:
total= primary + interest + intergovernmental transfersdirect= primary + interest — matching Census's published Direct Expenditure exactly (the concept today'sdirectfalls short of by the excluded interest)primary= direct minus debt service — the new defaultexpenditure_concept, replacing today'sdirectprimaryis not a Census term; it is the established fiscal-policy term for spending excluding interest (IMF/CBO/OECD "primary balance"/"primary spending"), chosen because oncedirectis redefined to include interest,primaryis precisely what today'sdirectalready computes.Implementation reclassifies item codes onto these three concepts using the crosswalk's
spend_typecolumn, not item-code first-letter prefixes — because F-018 shows a prefix-only scheme cannot be patched to handle every code correctly.Definition of done
expenditure_concept = c("primary", "direct", "total")withprimarythe default; classification driven byspend_type, andflow_prefixesretired as the classification mechanism.Q12/Q18(spend_type == "IG Transfer to School Districts") are insidetotal.Y05/Y06classify as expenditure andY01/Y02as revenue, from the samespend_type-driven mechanism — the concrete proof the prefix architecture is gone.Test goes green:
tests/testthat/test-expenditure-concepts.R→test_that("expenditure concepts classify on spend_type, not item-code prefix", ...). Against the bundled fixture it asserts, with amounts read from the raw corpus (never through the verb under test):primary(the default) = $623,347,000;direct= $651,051,000 = primary +I89(27,704 thousands);total= $36,191,455,000 = today's total ($29,226,534,000) +Q12(6,431,530) +Q18(533,391) thousands;codes_includedcontainsY05and its revenuecodes_includedcontainsY01, with neither verb claiming the other's codes.Remove the
skip()on line 1 of the test body to activate.(Madison FY2022's
I89 = 46,609thousands — the figure reconciled against Census's published Individual Unit File — is outside the fixture's year window (2011/2012/2019/2020), so the test asserts the same invariant on FY2020. The FY2022 expectation is recorded in the test file as a comment for whoever runs it against the full corpus.)Cross-references
cog-apimust expose the new vocabulary once this ships — finding F-026, tracked there.provenance.verb/provenance.callconfirm the API callscog_spending()directly rather than reimplementing the filter.census_of_governments_finance_pipeline#58(open) covers theJprefix — missing from both the category crosswalk and the flow prefixes. Same mechanism, different codes; that crosswalk work is a prerequisite forJlanding correctly in these concepts, since Census's Direct Expenditure isE + F + I + J + Y.summary_categoriescurrently has zero rows for prefixesI,Q,X, andY, so admitting them here without a category mapping would surface rows withcategory = NA.Blocked: the classification column this issue names does not exist in the corpus
Started implementation and stopped at a hard prerequisite. This issue's own final cross-reference predicted it; the situation is worse than that note implies, so recording the measurements before anyone else picks this up.
1.
spend_typeis not a corpus columnThe design says classification is "driven by the crosswalk's
spend_typecolumn". That column lives incog_explorer/data/item_code_xwalk.csv— a local analysis file in a directory with no git remote, not part ofuscogdataand not part of the published corpus.What the corpus actually publishes is
summary_categories, whose columns are:There is no
spend_type. The good news:category_type(expenditure|revenue) is already a per-code classification, so it is the right mechanism — it just needs rows.2.
summary_categorieshas zero rows for I, Q, X and YPublished corpus (290 rows, post-#65):
I,Q,X,Y: 0 rows. And40-spending_annotated.sqlusesLEFT JOIN summary_categories, so admitting those prefixes today yields rows withcategory = NULL— the outcome this issue explicitly rules out.3. The bundled fixture is stale on top of that
The fixture corpus every test in this package runs against has no
Jrows either — it predates the #65 crosswalk work already shipped:So even the crosswalk work that has already landed is not reflected in this package's tests. The fixture needs regenerating regardless of this issue.
The underlying rows are all present in the fixture's raw
long(I: 5 codes / 76,811 rows; Q: 3 / 6,614; X: 16 / 104,999; Y: 15 / 46,358) — only the category mapping is missing.4. The mapping is bigger and more interesting than 33 codes
The corpus carries codes the explorer crosswalk does not:
Q11,X04,X06,X09,X14,X35. More importantly, X and Y are not all flows. Sorting by what Census actually treats them as:X21/X30/X44are asset holdings (cash, federal securities, other securities);Y07is a balance in the US Treasury. They are neither revenue nor expenditure, andcategory_typetoday has only those two values. Admitting X/Y therefore needs a thirdcategory_type(or an explicit decision to leave the balance codes uncategorised and out of both verbs).Two more facts that bear on the design:
docs/STATUS.md: "109 discontinued codes, G/K→F collapse, trust-treatment break"), so deciding it here risks contradicting that work.What unblocks this
A chain across two repos, in order:
data/summary_categories.csvto cover I / Q / X / Y (~39 codes), withcategory_typeper code and a ruling on the balance codes.flow_prefixesallowlists ininst/sql/20-spending_long.sql/21-revenue_long.sqlwith a join onsummary_categories.category_type, and map the three concepts ontospend_subtype.Step 4 — the actual content of this issue — is small and well-specified. Steps 1–3 are the work, and step 1 carries decisions (the third
category_type, the X/Y trust treatment) that overlap Wave 5 and are not mine to settle.Nothing in the three-concept model itself is in question —
total/direct/primarywithprimaryas the new default is settled and is not what is blocking. Note also thatsubtype=assistancereturning 0 rows everywhere is a separate and now-shallower problem:Jis in the published crosswalk (3 rows) since #65, so that one only needs step 4'sJadmission, not the I/Q/X/Y work.Phase 1 (crosswalk) is done and published. The corpus at
pipeline_commit e64a046(published 2026-07-30) now carries every code this issue needs:Iexpenditure/spend_subtype = interestQexpenditure/intergovernmentalYflowexpenditure/insurance_benefitsYflowrevenue/revenue_subtype = insurance_trustX/Y/W/Zcategory_type = balance(pipeline#76)Prefix
Ynow spans all threecategory_types, which is F-018 made concrete in the crosswalk. Pipeline PRs #77 and #78. The fixture is regenerated at the same commit.Finding 1:
directmust include insurance benefits, not just interestThis issue's model says
direct = primary + interest. Validated against Census and that is incomplete. The 2006 manual §5.2.2.1:Social insurance trust is a sector (§5.3.4), not a character, so its benefit payments are direct expenditure. Reconciled numerically against Census's own published state aggregates (
20statetypepu.txt, level 2 — independent of our corpus), FY2020:totalreproduces Census's published sum to the dollar for both states, and the crosswalk covers 100% of their published expenditure codes — so "all expenditure except intergovernmental" is exactlyprimary + interest + insurance_benefits. Every one of the 8 mismatches is a retirement-holdings code (Z01,X01,X30,X40,X50,X71,X80), i.e. the documented "X family ends FY2016" gap, not a flow discrepancy.Omitting insurance benefits would understate California's Direct Expenditure by 10.9% — larger than the 7.1% error F-012 was filed for. Owner confirmed 2026-07-30.
This does not change this issue's fixture assertion: Madison is a city and carries no
Yrows, sodirect = primary + I89still holds there.Finding 2: DoD item 4's
Y01-in-default assertion conflicts with #12DoD item 4 and the test at line 73 require
Y01in the defaultcog_revenue()output. #12 requires the default to begeneral= $31,338,293,000 for WI FY2012, with insurance trust reachable only viarevenue_concept = "total".Measured on the regenerated fixture, WI FY2012:
Y01(unemployment contributions) andX01(retirement contributions) are both Insurance Trust Revenue. A default holding one but not the other matches no Census concept. Manual §4.3:Resolution (mirroring Census, per the owner): the default stays
general, and DoD item 4'sY01proof moves off the default call — assert it via the crosswalk / an explicit concept argument instead. The F-018 point is thatY01classifies as revenue whileY05classifies as expenditure from one mechanism; that is fully provable without requiringY01in the default result, and it is now visible directly insummary_categories.Still to do
Phase 2 (reader) only: rewrite
inst/sql/20-spending_long.sqloffLEFT(item_code,1) IN ('E','F','G')onto a join tosummary_categories, addexpenditure_concept = c("primary","direct","total")withprimarydefault, and delete theskip().