Skip to content

Step 2: the value-added block for 2017-2024, all five rows (#538) - #735

Open
WesIngwersen wants to merge 42 commits into
nowcastfrom
step2_va_timeseries
Open

Step 2: the value-added block for 2017-2024, all five rows (#538)#735
WesIngwersen wants to merge 42 commits into
nowcastfrom
step2_va_timeseries

Conversation

@WesIngwersen

Copy link
Copy Markdown
Member

Step 2 in full: the Use table's five value-added rows, built for 2017-2024.

Supersedes #691 and #693, both of which are ancestors of this branch.

The five rows are five different claims

Reading the block as "value added, nowcast" overstates most of it. What each row is worth:

row what it is how it moves
V00100 an estimate QCEW payroll growth inside 69 NIPA industry groups, rescaled to each group's published control. Graded -10.0% against frozen shares on the observed 2012→2017 holdout
T00OTOP a level plus two lookups T30500 per year; housing rescaled to T70405 B1031C, farm to T70305 B1017C. 43.3% of the row observed, the rest keeping 2017's within-block shape
V00300 a seed, only eight-line NIPA assembly per year, shares frozen at 2017. Frozen-share drift reaches 12.51% by 2022 — acceptable only because T18 makes this the residual the balance overwrites
T00TOP a conversion, and a seed level split + named routings, r = 0.947 at 27.9%
T00SUB a conversion, and not a seed code identity + two routings; reproduces the published 2017 row to machine precision

The first three are VABAS at basic prices and are estimated from NIPA. The last two are the wedge to VAPRO at producer prices and are converted from money that already exists in the Supply table's TOP/MDTY/SUB columns — so their levels carry no modelling content at all and only the industry split is estimated.

The tax rows: the obvious operator is wrong, and it was measured first

tax_axis_conversion scored the benchmark market-share matrix against the published 2017 rows before anything was built: r = 0.202 at 114.6% error on T00TOP. It is not noisy, it is pointed at the wrong stage of the chain — a tax on a product is remitted by whoever sells it, so market shares send the whole petroleum tax from wholesalers to refineries and the whole motor-vehicle tax from dealers to assemblers. 55.7% of the published row sits in wholesale and retail.

T00TOP uses the producer/trade level split top_by_level already computes for Step 4c:

operator corr |error|
market share on all TOP + MDTY 0.204 114.6%
+ level split, trade-level by trade output 0.743 41.9%
+ motor fuel routed to 424700 by name 0.946 29.9%
+ government columns zeroed, renormalised 0.947 27.9%

Two of the three legs are exact rather than estimated: duties are a lookup onto 4200ID, and the ten government columns are zero by an accounting rule the market-share leg was violating by 10,513. Still a seed — the residual is 20 named trade industries Step 5 moves under soft targets.

T00SUB turned out not to be a seed at all. A subsidy is paid to an operator, so it stays on its own code. Market shares score 0.676; code identity alone scores 0.569 — worse, and that is the finding, because identity's entire residual is two pairs of cells rather than a smear:

industry identity published what it is
S00203 0 19,471 public housing authorities
531HST 35,778 16,307 the same money, on the commodity it makes
S00102 0 6,339 federal insurance enterprises
5241XX 6,339 0 the same money, on the commodity it makes

Routing those two by name closes 2017 to shape agreement of 7e-17. What is frozen is one number and it is named: S00203's 54.4% of the housing line, which NIPA T31300 does not split.

Traps found and now asserted

  • ⚠️ The routings must not fire in 2020-21, where sub_decomposition replaces the other type with BEA's PPP allocation. Routing 5241XX then would put pandemic support on a federal enterprise. Conversely PPP is already industry-shaped — BEA publishes it by industry — so identity is at its best in those years, not its worst.
  • ⚠️ S00102 is over-subsidised from 2022 (36.6bn against 6.3bn in 2017). The cause is upstream: NIPA's other line still carries pandemic-era programmes, and the same 36.6bn sits on commodity 5241XX with or without this conversion. Documented as the other line's residue, not as a measurement.
  • ⚠️ T00SUB's 1 $M level gap is BEA's rounding — NIPA's 59,875 against the workbook's 59,876 — spread proportionally across all twelve subsidised cells. check tests shape and level separately, because scoring raw levels reports that rounding as error and could hide a real one behind it.
  • ⚠️ The summary SUT is a stale grader for exactly 2019-2022. Its own VAPRO sits 0.09-1.21% below current-vintage UVA205-A in those four years and matches to the dollar in 2017/2018/2023/2024. So V00300's apparent 2.64% error in 2022 is the workbook, not the assembly.
  • ⚠️ NIPA line-number selection is only safe because the lines do not move. Line: [37, 17] is a property of the table, not the series. other_taxes_allocation.check() now re-derives it from SeriesRegister, which selects by code, so it grades the selection.
  • ⚠️ Rows may be negative and must stay so. S00201 state and local passenger transit carries a V00300 of -36,919 in 2017 and is negative in every year; it is the only industry that is.

Diagnostics

use_va_detail_sut now grades five rows instead of three, with the published T00SUB flipped to the balance's sign convention (BEA publishes it positive and subtracts it; comparing unflipped reports every subsidised industry as a 200% error). 2017:

  • grand total 19,612,102 against 19,612,111 — $9M on $19.6T
  • all 5 row totals within tolerance
  • 48 of 402 column totals partial, and every one is T00TOP inside the trade block — the documented state of the seed, not a regression

Also in here

Two constants in test_nowcast_product_taxes moved when the Trade FBSs were rebuilt on the NAICS-2022 goods Crosswalk (#734). TOP's residual now rides a purchaser-price base carrying MCIF, so 2024's tobacco gap went -26,153 → -26,282 and its share 7.7% → 7.3%. 2020's two numbers did not move at all, which is the check that the shift is the Crosswalk and not the method.

Gates

ruff check . clean; mypy bedrock clean apart from the four pre-existing Windows-resource errors in settings.py. bedrock/transform/iot + bedrock/transform/eeio — 472 passed, 1 skipped, 1 xfailed; the new test_nowcast_va_taxes is 70 of those.

uv run python -m bedrock.transform.iot.nowcast_va_taxes --check
uv run python -m bedrock.analysis.nowcasting.tax_axis_conversion --check
uv run python -m bedrock.analysis.nowcasting.value_added_timeseries --check
uv run python -m bedrock.transform.nipa.othertax_lookups --check
uv run python -m bedrock.transform.nipa.compensation_movement --check

Still open

🤖 Generated with Claude Code

WesIngwersen and others added 30 commits August 22, 2026 15:16
Adds 24 tables to BEA_NIPA.yaml, all annual and complete for 2012-2024, at
roughly 950 extra FBA rows per year: the controls (1.10, 1.1.5, 1.3.5, 1.14),
compensation (6.2D, 6.3D, 6.10D, 6.11D, 3.10.5, 7.18) and gross operating
surplus (6.12D-6.22D, 7.5, 7.7, 7.9, 3.8, 7.3.5, 7.4.5, 7.11).

T00TOP and T00SUB are deliberately absent. Their commodity-axis counterparts
shipped in Step 4d (#690), and their industry split is an output of Step 5's
balance rather than an input to it, so sourcing them here would estimate the
same money twice on two axes.

Three findings, all pinned by the new test rather than left in prose:

- Take the *paid* line, not the table's root. 6.2D and 6.3D each state their
  total twice - line 1 received by residents, line 2 paid by domestic
  industries and government - and value added wants line 2. On it, wages plus
  both supplements close to 0 against compensation and land on the Use SUT's
  V00100 within 3. Reading line 1 is what left the ~10,600 the compensation
  plan carried as an open item; it is the rest-of-world adjustment A4187C,
  stated in 6.2D's own lines 97-99.

- 6.11D is three panels under one code, and only the first is by industry:
  lines 1-20 industry, 22-36 type of fund, 37-45 benefits paid - a different
  concept at 2,370,770. Its industry grain is 17, not the 36 the plan claimed,
  and selecting the whole table double-counts. T61600D, T71100 and T11400 also
  restate a code; select by line in all four.

- Table 3.8 gives the government-enterprise surplus an industry axis it was
  thought not to have, federal and state-and-local summing to 1.10's A108RC
  exactly.

The config half of the test needs no data and guards the substring hazard in
bea_nipa_parse, which selects a table's series with str.contains: two declared
ids where one contains the other would contaminate silently.

NIPA_final_dom_uses_2017 rebuilds bit-identical - all 17 columns, same row
count - so the added rows reach no existing method.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ache

The plan's Phases 1-5 were written as a pure-Python pipeline. With Step 2
settled as three FBS methods, most of it survives as configuration rather than
code: BEA_Detail_Use_SUT already melts the Use SUT through VAPRO, so the 2017
benchmark V00100 by industry is an attribution source today, and Phase 1 is a
selection_fields clause rather than an extraction.

The one step with no FBS primitive is the anchor-and-move itself - multiplication
does not preserve the group total and there is no renormalise step, which is
what the old Phase 3.4 needed. Recorded as the design seam: Phases 1-2 build a
cached FBS_outside_flowsa source holding the moved shares, Phase 3 is a single
proportional attribution against it, and the exact rescale then holds by
construction instead of being a follow-up step.

QCEW is cached locally as per-year FBA parquets because generating it is slow
(~9M rows a year, county grain). Dropping one into extract/output_data lets
getFlowByActivity find it through the ordinary local path with no code change -
esupy matches on name and ignores the v2.0.4 tag. Verified for 2017. Four things
measured off that cache rather than assumed:

- QCEW national NAICS-6 payroll is 93.8% of NIPA wages paid. That number is what
  settles "allocator, never control", and the missing 6.2% concentrates in the
  sectors Phase 4 already carves out.
- 1,027 of the 1,048 NAICS-6 codes in the BEA detail crosswalk are present, so
  the gap is a bounded list of 21 rather than a structural hole.
- Ownership is on the flow, not a separate axis. It still does not separate
  government enterprises from general government, which is the distinction the
  SUT needs - so government stays on NIPA, and the plan's claim is narrowed to
  what is actually true.
- The cache is 2017-2023, so the nowcast's 2024 has no QCEW and needs a stated
  fallback. Better found here than in Phase 3.

Employment_common.yaml cannot be reused as-is: every _bls_selection_fields_*
block hardcodes Class: Employment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
QCEW covers 93.8% of NIPA wages paid. The plan treated the other 6.2% as an
unexplained residual to spread pro rata. NIPA states it.

7.18 closes it exactly: BLS published 7,968,336 + misreporting 106,273 + not
UI-covered 399,801 + timing 0 = 8,474,410, wages received. And the gap is
already non-uniform before any industry table is opened - government's
uncovered rate is 11.3% of government wages against private's 3.5%.

6.4D is the table that turns the assumption into a measurement: QCEW publishes
employment on the same axis, so QCEW/NIPA is a coverage ratio per industry.
Measured on 2017 private employment it is 97-100% almost everywhere and
collapses in exactly two places - educational services 77.1%, other services
except government 63.0%, where religious and grantmaking organisations are
largely UI-exempt. So QCEW growth is trustworthy broadly and the exceptions are
a named short list, which is a better method than a flat haircut.

Construction at 97.1% is a warning rather than a reassurance: the count agrees
because both sources count the same workers, and the trade-versus-structure-type
disagreement that actually matters is invisible to a coverage ratio.

Three lookups located: RfHhInstComp W151RC is 18,684, the SUT's 814000 to the
dollar - the sector QCEW covers worst is the one NIPA hands over directly, and
the plan had described this line without finding it. U32500 splits general
government compensation into wages and supplements, which 3.10.5 only totals.
6.6D wages per FTE is the plausibility check Phase 5 lacked: a share can be
positive, sum to one, and still imply an implausible wage.

Adding those tables exposed two defects in bea_nipa_parse:

- Scale and unit were assumed, not read. A flat `* 1000000` with Class and Unit
  hardcoded to Money/USD is right only while every declared table is in millions
  of dollars. 6.4D/6.5D are thousands of persons and 6.6D is a ratio, so 6.4D's
  150,654 thousand employees would have been published as 1.5e11 USD. Both now
  come from each series' own MetricName/DefaultScale, with an explicit map that
  raises on an unrecognised metric rather than defaulting to dollars. Every
  dollar table BEA publishes has DefaultScale -6, so all 1,812 dollar rows are
  bit-identical across the change. 1.14's three chained-dollar lines move to
  Class Other, so a Class: Money selection cannot add real dollars to nominal.

- Table ids were matched by substring. Asking for U70205 also returned U70205S's
  44 physical-quantity and price-index series under their own id. Nothing
  downstream selected them so it never showed, and it was the metric guard above
  that surfaced it. Matched exactly now.

NIPA_final_dom_uses_2017 still rebuilds bit-identical, and 2012/2019/2020/2024
extract cleanly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Corrects an overstatement in the previous commit. T00TOP/T00SUB were described
as "not sourced here ... building them would estimate the same money twice".
That was wrong. They are already built by commodity in Step 4d, so the industry
row is a transformation of money that exists, not a second estimate - and the
plan's own coordination note said exactly that: "build once, derive the other
through the Make/Supply structure".

So the open question was never whether to derive it, but whether the derivation
works. Now measured for 2017, where the published answer exists.

It does not. Using the benchmark market-share matrix from the Supply table,
D[c,i] = V[c,i] / T007[c]:

  T00TOP (TOP + MDTY)   correlation 0.202   |error| 114.6% of the row
  T00TOP (TOP only)     correlation 0.211   |error| 109.5%
  T00SUB                correlation 0.676   |error|  79.8%

The reason is structural rather than noise. 55.7% of the published T00TOP row
sits in wholesale and retail industries, because a tax on a product is remitted
by whoever sells it while market shares place it with whoever makes it. The
pairs are stark: petroleum wholesalers 424700 are 88,362 published against 13
estimated while refineries 324110 are 397 against 92,893; motor vehicle dealers
441000 are 45,947 against 2,301 while assemblers 336111+336112 are 26 against
24,141. The whole tax moves one stage up the chain.

This is measured in the benchmark year, against the table the mix comes from,
so it is not the "2017 ratios drift" objection the 2026-08-17 decision was
argued from - it is stronger. A conversion that fails in 2017 cannot be rescued
by being applied nearer to 2017.

T00SUB fails differently and more usefully: its residual is two named structures,
not a smear. S00203 other state and local enterprises is 19,471 published against
1,964 (transit operating subsidies) and S00102 is 6,339 against 102 - subsidies
paid to an operator rather than attaching to a product, which no product-side
operator can place. T30800 already carries them.

One piece converts exactly: published T00TOP on 4200ID is 38,513 against a Supply
MDTY of 38,507, so customs duties are a lookup rather than an allocation - 5.1%
of the row, free, every year.

The decision stands: the industry distribution stays free for Step 5 under
economy-wide soft targets. But free is not unseeded, and the better operator is
the margin structure rather than the Make matrix - a commodity's wholesale and
retail margins say which trade industries handle it, which is the point-of-sale
signal the tax follows. Those margins are built, so that operator is testable
the same way. Left as the open follow-up.

Findings carried as a --check flag rather than unit tests, per the convention
for analysis modules.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ADME

Three passages still read as though T00TOP/T00SUB were simply out of Step 2's
scope, which was the overstatement the previous commit corrected in the Step 2
section but not elsewhere:

- the framework status table said "Not sourced", where the truth is that they
  are built by commodity and the industry row is a conversion Step 5 solves;
- the board-provenance note said they "left Step 2's scope entirely";
- the data-gap section already said "Step 2 still supplies a seed" and was
  right all along - it now also carries what the seed can and cannot contain,
  since the market-share operator was measured and fails at r=0.20.

Adds tax_axis_conversion.py to the analysis README index.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ersion

Only within wholesale, and only for one code.

The previous commit left "ride the margin structure instead of the Make matrix"
as an untested proposal. Testing it turned up something better: Step 4c already
computes the operator this needs. top_by_level splits TOP per commodity into
producer-level (325,829) and trade-level (391,096) from an identity with nothing
modelled in it - excise sits in Producers' Value, sales tax inside the margin
columns. That is exactly the producer-versus-seller distinction market shares get
wrong, and its trade-level total lands within +2.2% of published wholesale-plus-
retail T00TOP.

  market share on all TOP + MDTY                corr 0.204   |err| 114.6%
  + level split, trade-level by trade output    corr 0.743   |err|  41.9%
  + motor fuel routed to 424700 by name         corr 0.946   |err|  29.9%

On the question itself, the answer differs by block:

- Non-trade industries are 44.3% of the row and need no matrix at all. Once the
  producer-level portion is separated, plain market shares give corr 0.987.
- Within retail, no. Output shares - which for a trade industry are very nearly
  margin shares - give 0.744. Retail product tax is general sales tax:
  broad-based, HHI 0.137, an effective 7.3 of 9 industries carrying it.
- Within wholesale, yes, and output shares are worse than useless: corr -0.192.
  424700 petroleum wholesalers takes 51.3% of wholesale product tax on 3.4% of
  wholesale output, because wholesale tax is motor fuel excise rather than a
  broad-based tax. HHI 0.321, an effective 3.1 of 10.

But wholesale does not need a commodity-by-trade-industry matrix either. It needs
one named routing: NAMED_TAX_LINES already carries motor fuel as 324110 and
trade_level_share already says that tax is 99.8% trade-level, so sending it to
424700 takes wholesale from -0.192 to 0.973 and the whole row to 0.946. With
petroleum pulled out by name the other nine wholesale industries score 0.825 on
output shares - they behave like retail.

So the general commodity-by-trade-industry margin matrix that the PRO:PUR
producer-price work will eventually need is not required here. Still a seed and
not a target: 29.9% absolute error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Construction was the right last block to check and the wrong one to worry about.
It is block-shaped like the trade industries and could have been a second
petroleum; it is the opposite. Commodity TOP is 1,907 against a published T00TOP
of 1,857 and plain market shares give corr 1.000, |error| 31 = 1.7%.

The reason is the Make matrix. BEA defines the construction industries by type
of structure, so the block is 94.5% in-block and 100.0% diagonal - there is no
producer-versus-seller distinction when whoever builds the structure sells it.
top_by_level agrees: 100% of construction TOP is producer-level, so the routings
that rescue wholesale are inert here and all three operators give one number.
MDTY and SUB are zero on both axes, so there is no duties or subsidy question in
the block at all. Tax sits on 3 of 12 codes, none a NAMED_TAX_LINES entry.

The residual 1.7% is a leak, and market shares get its direction right and its
size about half: 5.5% of construction output is own-account or secondary work
(531HST 20,279, state and local government 19,991), and published moves 50 off
the block where market shares move 30.4.

That answers the larger question of whether the remaining sectors each need this
treatment. They do not - the error is 20 industries, not 402:

  top 5 industries carry 35.1% of the error, top 20 carry 80.3%
  17 of those 20 are wholesale or retail

  wholesale   22.8% of the row   41.9% of the error
  retail      27.8%              33.7%
  non-trade   44.3%              24.4%
  4200ID       5.1%               0.0%

What is left is the within-trade allocation already characterised plus five
named non-trade structures: 721000 accommodation (-6,592), 517210 wireless
(-4,310), 221100 electric power (-3,934), and government enterprises handed tax
they do not carry (S00202 +3,439 against a published zero, GSLGE +1,698, both of
which belong to the Step 7 reallocation). So build against the seed and repair
the named twenty later - Step 5 balances these cells under soft targets anyway,
and seed accuracy below the block level is not what the build is waiting on.

Adds construction_codes, construction_scores and error_concentration, wires both
into the report, and adds five assertions to --check: construction stays usable,
its error stays under 10%, its Make block stays diagonal, it stays MDTY/SUB-free,
and the top-20 error share stays above 60% - that last one fails if the residual
ever spreads and a broader sweep becomes necessary after all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
BEA books zero taxes on production to all ten government industry codes -
T00OTOP and T00TOP are both zero on every one of them in 2017, the single
exception being 538 of T00TOP on S00203. The columns are real (V00100 and VABAS
are populated), so the zero is an accounting rule and not a gap: a tax levied by
government and remitted by a government producer nets out.

The market-share leg was violating that rule by 10,513 against a published 538,
because government genuinely does produce taxed commodities and market shares
hand it a share of the tax on them:

  S00202  S&L electric utilities        3,439  against a published 0
  S00203  other S&L enterprises         2,428  against 538
  GSLGE   S&L educational services      1,698  against 0
  GSLGO   S&L other services              940  against 0
  S00101  federal electric utilities      854  against 0

market_share_matrix gains an exclusion that drops those columns and
renormalises each commodity over the producers that remain, so the tax stays
with its own commodity rather than being deleted or smeared economy-wide. That
is the fourth operator in the progression:

  + motor fuel routed to 424700 by name        corr 0.946   |err| 29.9%
  + government columns zeroed, renormalised    corr 0.948   |err| 27.9%

It moves 15,692 of error, and only two thirds of that is the excess itself. The
rest lands where it belongs:

  221100 electric power    |err| 3,934 -> 443
  721000 accommodation     |err| 6,592 -> 5,088
  622000 hospitals         |err|   763 -> 296

Read 221100 against S00202: the previous operator was under-attributing private
electric power by almost exactly the amount it had misplaced onto the government
utility, so the money was misplaced in a recoverable direction. The non-trade
block goes from corr 0.987 / 24.4% of the error to 0.992 / 18.8%, and no
government code appears in the top-20 error ranking any more.

Only the producer-level leg needs the exclusion - the trade-level leg lands on
wholesale and retail, where no government code sits, and duties land on 4200ID.
No commodity is stranded: no commodity produced entirely by government carries
any product tax in 2017, so the fallback that keeps such a row unrenormalised
never fires, and the seed total is unchanged to the dollar.

This one was worth fixing before the build rather than after. The other
residuals are misallocations Step 5's balance can pull back; these columns are
redistributed into private industries by the Step 7 government-enterprise
reallocation, so a wrong seed here propagates into work that would have to be
unpicked.

Adds government_industries and government_scores, and four assertions to
--check: the published rule still holds, the seed puts nothing there, the
exclusion still reduces the row error, and renormalisation still moves money
between industries without creating or destroying it. _frames is now cached -
the scoring functions call it a dozen times over and --check had grown to over
two minutes; it is 9s now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Step 2's third row was scoped as "no NIPA table has an industry axis for
this, accept a cruder method". The first half is true and the second does
not follow: measured, T00OTOP is the best-behaved of the three rows.

T30500 puts 88.1% of it in recurrent taxes on immovable property, so it is
a property tax and corporate income tax is not in it at all - that lands in
V00300. Which means the intuitive allocator is the wrong one. Scored against
the published 2017 detail row, industry output gives correlation 0.590 and an
absolute error of 92.3% of the row, missing 531HSO alone by 150,567; the
effective rate runs 0.49% at p10 to 15.20% on 531HST.

What works instead is that the row is concentrated - three real-estate codes
carry 46.3% - and BEA publishes the big cells. T70405 B1031C is the
531HSO+531HST pair to the dollar and T70305 B1017C the ten farm codes within
3, and neither is a 2017 coincidence: against the summary SUT they hold in
six and seven of the eight years 2017-2024. The remainder rides frozen 2017
shares, graded out of sample at 1.9% composition drift against a 40.5% level
move. The summary SUT is evidence here and never an input, which is the role
Step 5's Decision 3 reserves it for.

Also lands the two pieces of Step 2 plumbing every one of the three methods
needs: the three BEA_2017_Code identity crosswalk rows (Phase 0), and
assign_use_row_from_clean_parameter - the transpose of the final-demand
helper, since value added is a Use row rather than a Use column.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
NIPA_VA_othertax_2017 is the first of Step 2's three methods and the one
that proves the seam the other two need: the BEA_2017_Code identity
crosswalk rows, Sector_Crosswalk_BEA_NIPA_VA.csv, the melted
BEA_Detail_Use_SUT as an attribution source, and the Use-row transpose.

One activity set. The T30500 control (LA000365 state and local other taxes
on production, plus LA000237, the federal remainder after taxes on product)
attributed proportionally across the 392 non-government industries on the
2017 benchmark T00OTOP row, then transposed onto SectorProducedBy. Replaying
2017 gives correlation 1.0000 against the published row, 389 industries of
389, zero on government, and the control's own 9 of rounding. Five tests pin
it; NIPA_final_dom_uses_2017 rebuilds bit-identical across the crosswalk
change.

The housing and farm lookups are NOT in it, and that is the finding rather
than an omission. They cannot be their own activity sets: NIPA states no
"other taxes on production excluding housing and farm" line, so a third
set's control would still be the whole 608,533 and the three would sum to
872,044. Folding them into the weight vector instead needs an
FBS_outside_flowsa attribution source, and that path does not work -
get_flowby_from_config builds a FlowBySector for that data_format while
attribute_flows_to_sectors then calls map_to_sectors, which only
FlowByActivity defines. All four existing uses of the hatch in the repo are
top-level sources.

That matters well beyond this row: the plan commits V00100's anchor-and-move
build to the same hatch, so it has to be cleared before Step 2's largest
row. Finding it on the 1.8% row rather than the 30.9% one is the argument
for having built T00OTOP first. Here the cost of going without is bounded
and measured - 1.92% against 1.68% of row error in 2024, less than the
control's own 2.9% vintage error in 2021 - and lookup_improvement()
reproduces it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
All three of Step 2's value-added rows now generate, and the
use_va_detail_sut diagnostic is switched on: every cell of the 3 x 402
block matches, worst 2.6 million on a 178 billion cell, which is BEA's own
rounding. derive_initial_value_added stacks the three methods.

V00100 runs on the 69 T60200D industry groups as separate controls rather
than one national total, because those 69 leaves partition the 71 BEA
summary industries exactly - no gaps, no overlaps - and 63 of them equal a
summary industry's published compensation to the dollar. Verified by value
rather than by name. So the frozen 2017 shares only have to hold within a
summary industry, which is a far smaller claim than a national spread.
Correlation 1.000000 against the published row, 400 industries of 400.

That build reverses the plan's headline decision, and on measurement.
Splitting wages from supplements was meant to avoid imposing one industry's
supplement rate on another. It cannot: T60300D matches T60200D at 69 groups
but T61000D and T61100D publish only 16, so the split imposes a coarse
group's rate on industries whose own rates differ and misplaces 99,025 -
0.95% of the row - against a T60200D-only method that is exact. NIPA
publishing both halves by industry does not help when it publishes them at
coarser grain than the total.

V00300 is eight controls across five tables on one industry distribution,
and stays plain deliberately. Four of its components have industry tables on
mutually incompatible partitions (20 / 21 / 12-plus-financial / 63 groups)
and three have no axis at all, so a component build imposes four coarse
partitions at once - the same mistake, four times over. It keeps S00201 at
its published -36,919; nothing here may assume positivity.

The best next source for Step 2 is a value added by industry extractor.
V00300 = VABAS - V00100 - T00OTOP, and two of those three are now built at
detail, so BEA's GDP-by-Industry accounts would give V00300 by 71 industry
groups as a residual - better than assembling eight incompatible component
tables, and ahead of Fixed Assets because it serves the 41.6% row rather
than one 40% component of it. The summary Use SUT carries the same thing and
Decision 3 holds it in the test set, so it cannot stand in.

2018-2024 are not written: every method holds its within-group shares at
2017, which is complete for 2017 and not for later years. Compensation needs
the moved-share source, still blocked on the FBS_outside_flowsa gap.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
BEA_GDPbyIndustry reads table TVA113, Components of Value Added by Industry,
out of BEA's GDP-by-Industry release archive: value added at producer prices
plus compensation, taxes on production and imports less subsidies, and gross
operating surplus, for 100 industries, annually. No API key - BEA's
GDPbyIndustry API dataset needs a UserID and this archive does not, so it
follows BEA_NIPA's pattern of caching one archive under extract/input_data
with a GCS fallback. 2012-2024 declared, 1997-2024 available. The root
identity holds exactly in every year checked: compensation + taxes +
surplus = value added.

This was asked for because V00300 has no NIPA table of its own and its eight
component tables publish on mutually incompatible industry partitions.
TVA113 does state gross operating surplus by industry directly - so it looked
like the one source that would give V00300 a real industry axis.

It is not, and the extractor is what proves it. All 71 BEA summary
industries' V003 match a TVA113 surplus row to the dollar, and all 71 V001
match compensation the same way. BEA's industry accounts and the SUTs are the
same estimates published twice, so this is the summary Use SUT by another
door - and Step 5's Decision 3 holds the summary SUT out of the target set
precisely so it can grade the build. Two tests pin that, so the finding
cannot quietly stop being true.

NIPA_VA_surplus is therefore left unchanged. Wiring it in is a two-line swap
of the attribution source; what it buys is a near-exact V00300 seed at
summary grain, and what it costs is summary V003's ability to grade that
seed. That is a testing-strategy judgement rather than a data-availability
one, so it is documented and left open rather than taken.

The extractor is worth having regardless: it makes the series reachable as a
versioned FBA rather than a private workbook loader, and it is the natural
loader for the test set itself. It also corrects the recommendation the
previous commit made - "the highest-value missing source" was the wrong
framing, since it was never missing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Step 2's section had a target and no picture. It has both now:
use_va_detail_sut_2017.png, rendered in the same style as the Step 1 and
Step 4 images, plus the counts, coverage and accuracy tables the other two
sections carry. Every section in the diagnostic now has a candidate; none
carries candidate=None.

The block is 100% coverage and 100% accuracy, and the report says at both
the summary table and the section that this is NOT comparable to the other
two rows. All three methods take their within-group distribution from the
2017 benchmark, which is the reference, so a 2017 run tests the plumbing and
not the estimate - solid green is the floor, not an achievement. What it does
certify is that the orientation transpose, the identity crosswalk rows, the
69-way compensation control set and the eight-line V00300 assembly carry mass
end to end without losing or misrouting it: the grand total is off by
0.000016%, three million on eighteen trillion.

The picture earned its place on the first run. Reading the 17 absent cells
showed T00OTOP is absent on eleven government codes, not the canonical ten:
the prefix rule (S00, G) misses the US Postal Service, 491000, a federal
government enterprise whose BEA code is shaped like an industry's. It was
landing on zero only because the benchmark weight happened to be zero, not
because the rule excluded it. Now named in write_value_added_crosswalk.py.
No 2017 number moves - which is why a total would never have found it.

Also shortens the section note so the plot header stays one line, matching
the other two images.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
BEA_GDPbyIndustry and BEA_Detail_GrossOutput_IO read sibling zips from the
same BEA release page, and GrossOutput.xlsx is duplicated between them -
gdp.py gets it from GCS as a manually extracted workbook pinned to 2025Q2,
BEA_GDPbyIndustry fetches the summary archive straight from BEA. Filed as
#694; this just leaves a pointer at both ends so the next reader does not
re-derive the relationship.

Records the two things that make it more than a file move: the 2025Q2 pin is
deliberate and the archives are currently different vintages (June 2026
against September 2025), so dropping it would silently move gross output
under electricity disaggregation and derived_gross_industry_output.

Also records the negative result, since it is the reason V00300 stays at
summary grain: UGdpByInd's ValueAdded.xlsx has no counterpart to TVA113.
UVA201-UVA210 are indexes, real value added and UVA205 value added totals -
there is no components table, so compensation, taxes and gross operating
surplus are published at summary grain only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pytest names a test module by walking up from it until it reaches a
directory without an __init__.py. Both bedrock/transform/nipa/__tests__
and bedrock/utils/taxonomy/mappings/__tests__ carried an __init__.py
while their parent directories did not, so both claimed the top-level
module name __tests__. Whichever pytest collected first won, and the
other failed to import:

  ModuleNotFoundError: No module named
  '__tests__.test_bea_ceda_v7__cornerstone_commodity'

Removing the empty NIPA __init__.py puts those tests on the convention
the majority of __tests__ directories here already follow, giving them
the bare module names test_nipa_va_othertax and test_nipa_va_rows, both
unique across the repo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Step 3 freezes every industry's input structure at 2017 and carries it on a
price index. For manufacturing the part of that column the annual surveys
cannot refresh is the materials bill: AIES publishes all materials, parts and
supplies as one cell, 82.5% of the column, so only 8.3% of manufacturing's
intermediate is commodity-mappable annually (#564). The commodity breakout is
quinquennial Economic Census, and 2022 is a second observation of it sitting
close to the middle of the 2018-2025 span. #564 called that a consolation
prize. Measured, it is the main prize.

Census_EC_MatFuel pulls ecnmatfuel for both vintages -- 4,624 rows in 2017 and
4,399 in 2022, every industry at NAICS-6 and every material an 8-digit code.
The orientation is the opposite of Census_EC_PxI on purpose: PxI asks what an
industry sells, this asks what it buys, so the industry goes in
ActivityConsumedBy and the material in ActivityProducedBy, which is the Use
table's own orientation.

materials_structure.py answers the two questions that decide whether the source
is worth having. Coverage: 66.2% of the 2017 materials bill and 69.1% of 2022
is placeable on a BEA commodity, against 8.3% annually -- 52.9% and 54.0%
resolving 1:1 by NAICS prefix, the rest onto a BEA group that needs a
within-group split on 2017 Use shares. A third is residual buckets Census could
not place, and that is the ceiling. Movement: the mix moved 0.153 between the
two censuses against 0.173 for the whole Use column over 2012-2017, with 264 of
345 industries moving more than 10 points. So the largest and least-observed
part of the manufacturing column moves as fast as the rest of it, and freezing
2017 out to 2025 discards a reallocation this source can see.

Two traps are documented rather than worked around. 00772000 "Total Materials"
is the industry total and the named codes sum to it exactly, so summing the FBA
unfiltered doubles the table; it is kept because it is the control a
suppression recovery subtracts published children from, the same role NAICS 00
plays for PxI. And the vintages sit on different NAICS bases, sharing 345
industries and 291 materials carrying 90% of each year's cost -- 336411
aircraft reallocating 59% of its materials bill is almost certainly a code
reassignment, not economics, and is flagged as suspect.

Not built yet, and named in the plan: suppression recovery against the
00772000 control, the group-tier within-group split, the vintage code diff, and
the interpolation itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ted (#698)

Census withholds 412 of 4,624 cells in 2017 and 330 of 4,399 in 2022. They are
not zero: they sit inside each industry's published 00772000 total, and leaving
them there biases a materials mix toward whatever happens to be publishable,
which is systematically the large materials.

The control turns out to be exact, and that is measured rather than assumed --
for every industry with nothing withheld the named materials sum to 00772000 to
within 0.1%, 238 of 238 in 2017 and 247 of 247 in 2022, with fuels carrying
their own exact control in 00772002. After recovery all 406 and 386
industry-by-kind controls close to within 0.1% and no negative cell is created;
the one negative in the output is published Census data.

The prior is chosen by holdout rather than by argument. An economy-wide prior
was tried first and is visibly wrong: it hands an idiosyncratic industry the
economy's shopping list, and put $8.6bn of motor vehicle seating into aircraft
manufacturing while cutting its aircraft engines from $15.5bn to $0.5bn.
Masking published cells and recovering them scores every peer-prefix length;
NAICS-3 wins on average, at WAPE 0.602 and 0.718 against economy-wide's 0.640
and 1.033. Cross-vintage priors are deliberately excluded even though 2017 is
the best predictor of a withheld 2022 cell, because filling 2022 from 2017
biases the movement measurement toward zero -- a recovery must not manufacture
the answer the analysis is testing.

That WAPE is the finding that matters, and it corrects the last commit. The
mass a recovery places is exact, so all 0.6-0.7 of that error is allocation
across materials within the column -- which is exactly what a mix score
measures. Restricting to the 193 industries with nothing withheld in either
year, the materials mix moved 0.1330, not the 0.153 reported before, and the
count of columns over 0.25 collapses from 66 to 17. Most of the extremes were
the fill, not the economy. 336411 aircraft at 0.592 was the loudest of them and
chasing it is what found the defect: its 2022 column is mostly withheld.

The argument survives, weaker and better supported: materials mix moves 0.133
over five years against 0.173 for the entire Use column over 2012-2017, with
133 of 193 clean industries moving more than 10 points. So the largest and
least-observed part of the manufacturing column moves substantially -- somewhat
less than the column as a whole, not more, which is what the previous commit
claimed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…lation (#698)

The last three #698 items are built. Two dissolved a problem the plan expected
to fight; the third overturned the interpolation form the plan had already
chosen. materials_structure.py becomes inputs_structure.py, because what these
sources reach is no longer just materials.

The group-tier split is settled on evidence rather than on which prior sounds
more principled. A group cell is divided over the BEA commodities its NAICS
could be, on the purchasing industry's own 2017 Use row; scored by demoting
every direct cell one prefix and comparing against the commodity Census
actually named, the column prior puts 72.0% and 72.9% of the money on the right
commodity against 46.9% and 49.5% for an economy-wide one. Accuracy falls off
with group breadth -- 79.8% at 2-4 commodities down to 51.9% at 10-29 -- but
73% of group-tier dollars sit in groups of nine or fewer. The bare 33 prefix,
136 commodities, should be read as barely better than residual. The split lifts
the placeable bill from $2,097B to $2,681B and the commodities reached from 137
to 204.

The vintage code diff turns out not to be a diff. The plan expected to lose 10%
of each year's cost to the 2017-2022 revision and it loses none: the material
axis shares 289 of 289 and 290 MATFUEL codes, and every off-frame dollar on the
industry axis is NAICS 2022 merging pairs of 2017 codes. Connected components
of the year concordance put 100% of both vintages on one 365-industry basis
with no split assumption. This does not rescue 336411 -- aircraft was on the
shared frame all along, so its score is the suppression fill and the plan's
guess about why it mattered was wrong.

Linear interpolation was called "the obvious first form". It is obvious and it
is wrong, and seeing that meant stopping treating 2018-2025 as unobserved.
Manufacturing's materials bill is published every year the census misses, so
Census_ASM_Expenses and Census_AIES_Expenses now pull it -- ASM through 2021,
AIES for 2023. A straight line overstates 2020 by 28.8% because it cannot bend
around a pandemic, and past 2022 it gets the sign wrong: the bill fell 6.8%
into 2023 and the line says it rose 5.0%. That is the span the nowcast leans on
hardest. Scope has to be matched or a definition reads as a growth rate --
against CSTMTOT the census-to-ASM step is a median 1.181, on materials-plus-
fuels 1.063, a year of inflation.

Those extractors also buy more than a control. ASM and AIES publish electricity,
contract work, resales and 9-12 named purchased services at NAICS-6, 11.7% of
the manufacturing column, each mapping onto a BEA service commodity -- 91.0% of
the column reachable in total, and a separate cheaper task with no suppression
recovery or group split to do. AIES's expense block is manufacturing-only,
though: sectors 21, 22, 23 and 51-81 publish nothing at any NAICS level, so it
confirms #564 on the service drifters rather than overturning it, and it does
not cover the mining that the census does.

The headline moves down again, and this time because the frame was wrong. 0.133
is a MATFUEL-code score and the 0.173 it was compared against is a BEA detail
commodity one. On the same frame the clean subsample gives 0.0941, so the
materials block moves roughly half the column's rate rather than "somewhat
less". Aggregating 289 materials onto ~200 commodities nets off within-commodity
substitution and the split holds 2017 structure fixed inside each group; both
are properties of the seed, not corrections to 0.133.

Finally, BEA's 2022 and 2023 tables are still annual-survey updates carried over
the 2017 benchmark, so differencing a census-seeded block against them is not a
check. That cuts both ways, and the second way is the argument for this work:
it is information BEA has not yet incorporated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…, #664)

S3b was scoped as the cheap half of Step 3 -- ten named cells, no suppression
recovery, no group split. It was cheap, but not for the stated reason: the work
turned out to be a scope measurement rather than a mapping exercise, and two of
the claims it rested on were wrong.

The first is the coverage. 11.7% and 91.0% were survey-side dollars, and two of
the largest entries are not purchases of a commodity at all -- resales are goods
bought and sold on untransformed, which the Use table handles through trade
margins, and contract work is manufacturing services whose commodity is the
buyer's own industry rather than any fixed row. With the survey's own residual
they are reached as expense but cannot be placed. The honest figures are 6.4%
seedable and 85.8% reachable, and the seedable block is $228.6B over ten cells.

The second is that these cells could be read as levels. They cannot. Census_EC_
Expenses is new and pulls ecnbasic for 2017 and 2022, which publishes the cells
under the same variable names ASM uses -- so census and survey form one panel
with no crosswalk, and the splice is continuous: 2017 electricity $47.5B against
ASM's $51.0B in 2018, repair $52.7B against $55.2B. That 2017 observation is the
year the benchmark Use table is built on, and against it the survey and BEA
disagree about what the same cells contain by factors of 0.40 to 8.01. The
disagreements are structural, not noise: expensed software and computers are
operating expense to Census and mostly investment to BEA, repair is one Census
question against four BEA rows carrying parts BEA books elsewhere, and
professional services runs the other way, one question against BEA's legal,
accounting, engineering, consulting and R&D rows together.

So the seed moves BEA's cell rather than replacing it -- Use2017 times
survey(t)/survey(2017) -- which cancels every one of those because a constant
scope factor divides out, and preserves BEA's own level and its own split across
the commodities of a multi-row kind. 2017 reproduces the benchmark exactly,
which is the check that the form is right. The block carries the same pandemic
signature the materials bill does, -1.7% in 2020, and a frozen 2017 understates
it by 23.8% by 2023. AIES publishes no telephony and no expensed software --
both variables exist in the 2023 table and both are zero in all 883 rows -- so
those two are held at the 2022 census and marked rather than seeded as a
collapse. 2024 and 2025 raise rather than quietly extrapolating.

Chasing scrap through the same sources then found a defect in the materials
placement. Every MATFUEL scrap code begins 33, which is not a NAICS that maps to
any single commodity, so the prefix walk was filing purchased metal scrap into
the bare 33 group of 136 commodities and smearing it across most of
manufacturing. That group was the weak end of the group tier, flagged in the
last commit as barely better than residual -- and it turns out to have been
entirely scrap. BEA carries S00401 for exactly this concept, $49.1B into
manufacturing in 2017, and Census's "excluding home scrap" is the same thing:
bought in rather than generated on site. The five codes now map straight onto
S00401 ahead of the prefix walk, the 30+ band collapses from $32.1B/$54.9B to
$2.5B/$0.6B, and the clean commodity mix score moves 0.0941 to 0.0949. The
direct+group frame reaches 200 commodities rather than 204, and the four lost
were reached only through the smear.

Scrap is metal and only metal in this source -- no wastepaper, no cullet, no
plastic regrind, no textile rags. Census reproduces BEA's scrap concentration
independently and more finely, separating secondary aluminium at 0.628 from
secondary nonferrous at 0.529, and iron and steel mills move 0.328 to 0.440
between the vintages. The fuller picture, including the output side where ecnpxi
does carry paper and plastics as wholesale recyclable sales, is written up in
cornerstone-data/methods#59.

Census_EC_Inventories is extraction only, for #664. ecnbasic carries all three
stages of fabrication plus the totals, beginning and end of year, at NAICS-6 for
both vintages -- which is the industry x stage cell BEA does not publish
anywhere, and the level Hill's rules operate at. Verified additive: the stages
sum to the published total at a median ratio of exactly 1.0000. These are stock
levels and differencing them imports the holding gains CIPI excludes, so they
carry the stage shares and U50705BU1 keeps the level. The ASM annual equivalent
is deliberately not pulled yet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The plan carried 2024 and 2025 as years the estimate had to reach and could not
source, which framed an unobservable extrapolation as a risk this work owned.
It does not. AIES 2024 still returns 204 No Content, ASM ends at 2021 and the
census is quinquennial, so the observed panel runs 2017-2023 and the estimate
stops there. Extending it to 2024 is #707, Phase 2 work tied to producing a 2024
table, and 2025 is out of scope entirely.

So the open question in S3 is now only which interpolation form to fit, scored
on observed years, rather than which form to fit plus how far to extrapolate it
past the data. S3b needs no extrapolation at all: its span is fully covered, and
nonmaterial_seed() already raises for later years rather than inventing one.
unobserved_years() keeps reporting 2024 and 2025 because that is still the fact
a caller wants to assert on -- what changed is that they are outside the span
rather than gaps inside it.

Left alone deliberately: the references to BEA's published summary panel running
to 2024, gross output extracted for 2017-2024, the theta fitted on 2022-2024,
and FIWS covering to 2025. Those are statements about which data exists, not
about how far this estimate reaches, and the column-scaling argument in
particular is quantified on a 2024 seed and would lose its point without them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#705 asked, per drifting column, what could source it. Every one of them
already had a source - BEA named one for each at the 2017 benchmark - so the
question was whether a later vintage exists on the same basis and beats
holding BEA's answer. Four no, one marginally yes.

Census_SAS_Expenses splices SAS Table 5 across the two vintages that carry it,
sas-17 (2013-2017) and sas-22 (2020-2022). #564 recorded Table 5 as
"2020-2022 only"; that is the latest workbook's display window, not the
series. 63 industries at 2- to 4-digit NAICS, ~19 mappable items. The two
vintages sit on different Economic Census benchmarks, so every row carries the
benchmark it was built on in Description.

service_expense_seed.py indexes BEA's 2017 531ORE column on it and scores
against BEA's current summary Use: +4.4 / +3.8 / +4.5% at 2020-2022, positive
at every endpoint on a test biased against the seed, but at the inflation
carry's bar rather than over it. --reachable says why: 18.07pp of ORE's
movement sits on rows no survey item names against 13.51pp that a seed can
touch, and the largest single mover has no counterpart question at all.

Two corrections to earlier work in this branch:

- An exploratory cut of this score reported +18.9 to +24.7% by applying each
  item's index to whole summary rows. Temporary staff maps to 561300, $8.6B of
  the column; at summary it multiplied all of 561, $97.8B, mostly 561700
  services to buildings. A coarse commodity mapping inflates a result rather
  than blurring it. Build at detail, aggregate to score.
- _load_usa_summary_sut pins the workbook by year, so --drift's series changes
  basis at 2023. The same 2022 read from both vintages differs by a
  dollar-weighted 0.0557 against a measured drift of 0.0986, and by 0.0976 for
  ORE alone. Near-misses elsewhere on the page are inside that noise. The seed
  scores on one vintage; fixing the shared diagnostic is follow-on work.

Also records the negative results: the trade Business Expenses Supplement
exists for 2017 and 2022 on one benchmark but loses every item of 4A0 to
suppression; construction's ecnbasic pair is clean but 51% of the column is
one undifferentiated materials cell; and ecnpurmode, ecnpurelec and ecnpurgas
publish concepts BEA has no cell for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ives (#705)

intermediate_structure_drift read summary Use through
io_2017._load_usa_summary_sut, which pins the workbook by year: 2017-2022 from
the 2017-2022 release, 2023-2024 from the 1997-2024 one. That is right for FBA
consumers, whose published values must not move under BEA's revisions, and wrong
for a module that differences years against each other -- it put a vintage seam
between 2022 and 2023 in the middle of --drift's series.

Read every summary year from the current workbook instead, via this module's own
summary_use / summary_intermediate(year, workbook). io_2017's year-pinning is
left alone. service_expense_seed.summary_intermediate_current, which had made a
local copy of exactly this fix, is deleted in favour of the shared function; its
scores are unchanged, confirming the two were equivalent.

Add --revision, the same year read from both vintages. The revision table in
intermediate_estimation_plan.md was not reproducible by any committed code,
which broke that page's norm; the flag reproduces it exactly.

The question this was for: does #705's top-drifter ranking survive one basis?
It does, bit-for-bit. The seam sits between 2022 and 2023, but 2017 and 2018 are
identical across the two vintages (one cell, $3M) and 2024 was only ever read
from the current workbook, so the 2017-against-2024 comparison never crossed it.
ORE is still 0.141 and the candidate list -- ORE, GSLG/GFGD, 42, 5412OP, 81 --
needs no revisiting. The feared shift, that GFGD and 521CI revise by more than
ORE does, could not bite: the revision only starts at 2019.

What did move is the middle of the series, understated by 0.005-0.011:
2019 0.0452 -> 0.0504, 2020 0.0743 -> 0.0833, 2021 0.0711 -> 0.0823,
2022 0.0838 -> 0.0859. 2018, 2023 and 2024 are unchanged. The 2022 ranking does
not survive -- four columns in, four out of the top ten, ORE 0.084 -> 0.158 --
but nothing on the page ranks at 2022.

Two numbers corrected rather than restated. The plan doc and the seed docstring
both quoted a 2022 drift of 0.0986 that no code reproduces on either basis;
replaced with the measured 0.0859. The seed docstring said the gain was 4.4% at
2022 where the doc and the code both say 4.5%.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Margins.2 specified the margin rate as (TRADE + TRANS) / T013 -- margins over
*basic* value. BEA gross output is valued at producers' prices (confirmed), so
the price ratio #497 carries already contains the product-tax layer and the rate
must be taken over producer value: T014 / (T013 + T015). One factor, not two.

Also record the valuation chain the section had been using implicitly. T014 is
the margins alone, not a running subtotal: T016 = T013 + T014 + T015, verified
to $1M at detail, and independently stated in margins_estimation_plan.md.

Restated on the right denominator. The level error from using basic is a median
3.3% and lands hardest on the commodities this section quotes: 315AL apparel
1.372 not 1.793 (+31%), 324 petroleum 0.246 not 0.289, 313TT textiles 0.779 not
0.858, 311FT 0.489 not 0.533. The panel is 26 commodities with a rate above 1%,
not 36; median absolute change 3.6pp and p90 12.7pp, not 2.8 and 12.1.

Most of the level error divides out of a ratio, so the correction to the carry
factor itself is second-order -- median 0.35pp across the 26 receiving
commodities above $20B, p90 1.4pp, at most 2.1pp among named ones. Worth having,
not decisive; the section now says so rather than implying the fix is large.

New guard, which the section needed and did not have. For margin *suppliers*
T014 is large and negative -- the margin is allocated away from the trade or
transport commodity onto the goods it carries, which is why the columns net to
zero -- so mu is -0.94 for 42 wholesale, -0.99 for 486 pipeline, -0.88 for 482
rail. 1 + mu is then 0.06, 0.01 and 0.12 and the factor is a ratio of two
near-zero numbers. Set it to 1 wherever mu <= 0. Nearly free: those rows carry
almost no dollars in the purchaser-priced intermediate block, for the same
reason their mu is negative.

Finally, name the experiment. A missing deflator term and substitution under
relative-price dispersion produce the same symptom -- a low theta on the summary
panel against 1.00 on the detail one -- so theta must be fitted with and without
this factor. If adding it pulls summary theta toward 1 the gap was the deflator;
if not, the substitution reading stands.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…egative

Two results, and the second was not the one being looked for.

Add the margin leg of the purchaser deflator -- summary_supply,
summary_margin_rate and summary_margin_factor -- on the denominator fixed in
4fd1122: mu_c = T014_c / (T013_c + T015_c), margins over producer value,
because BEA gross output is at producers' prices. Margin suppliers are held at
1.0, since their T014 is large and negative (42 is -0.94, 486 is -0.99) and
1 + mu is then a near-zero denominator.

Add --theta, which fits the exponent with and without that leg. It was a
genuine question: Margins.2 hypothesised that the summary panel's low theta was
a missing-deflator artefact rather than substitution, and the two readings
predict the same symptom.

The hypothesis is wrong. Theta is unmoved in six years of seven and moves away
from 1.00 in the seventh; the score differs by under 0.001 either way. Not a
null test -- 26 of 73 commodities move, up to 10%, on the largest goods rows
(325 chemicals x1.049 on a $705B row, 3361MV x1.047). It fails *because* of
that: 22 of the 26 have a factor above 1, 17 of those lost intermediate share,
and the touched set lost 3.26pp of the block. Both legs inflate nominal goods
shares during real substitution away from goods. Keep the term as the correct
deflator, worth a median 0.35pp on the carry factor; drop the claim that it
explains theta.

Then the larger result. THETA_GRID started at 0.0, which censored the panel:
2023 and 2024 both pinned to the floor and were read as "the carry contributes
nothing". They fit -0.25 and -0.50. The frozen structure scores better when
commodity shares are moved *against* their own price movement, so #497's
theta = 1 is not merely too strong for the target years, it is the wrong sign.
The grid now runs -1.0 to 1.5 and carries a warning about the floor. Detail
2012->2017 still fits 1.00 -- its optimum was interior, so nothing measured
there moves -- and the +1.00 at 2020 to -0.50 at 2024 span is monotone in the
price regime.

Consequence for S1: theta must be a parameter, and its default for the recent
span is negative rather than 0 or 1. A build that hardcodes 1 applies a
correction pointing away from the answer on the block's largest rows.

Guard added with the negative exponents: a zero ratio would now raise a fit to
infinity rather than harmlessly to zero. No year has one today.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…704)

S1 and S0b of intermediate_estimation_plan.md. `nowcast_intermediate.py` is
#497 as scoped: seed the published 2017 detail Use SUT interior (402x402,
purchaser, before redefinitions), carry each column's shares on the commodity
price ratio at theta, renormalise, and scale the column to
`GO_producer - VAPRO_seed`. `nowcast.derive_initial_U_intermediate` is the entry
point. Runs 2017-2024, bounded by `BEA_Detail_GrossOutput_IO_<year>` rather than
by the price index.

S0b came with it rather than ahead of it: `use_intermediate_detail_sut` went
into sections.py runnable, not `candidate=None`.

The 2017 section run passes on every cell - 44,281 populated cells, 366 row
totals and 400 column totals inside tolerance, 100% coverage and accuracy. That
is the plumbing, not the movement: at 2017 every carry factor is 1.0.

The 2017 rescale is not the identity, and the residual is BEA's own rounding.
Published T005 is one rounded number; the interior sums 402 separately rounded
cells to a different one - $350M on $14.9T, at most $13M on a column. A small
column wears that as a large fraction, so `atol` carries those cells rather than
`rtol`: 334610 is $482M of intermediates and is rescaled 1.05%, the largest
relative error, against a largest absolute error of $6.0M on a $19.2B cell.
`reproduction_check` reports both, because either alone reads as the wrong kind
of error.

The seven published negative cells survive in all eight years and are not
clipped. The two structurally empty columns - 4200ID and 814000 - stay empty,
and a control that puts real dollars on one raises rather than dropping them.
So does a seed column whose nonzero cells cancel: an empty column has no
structure to normalise, a cancelling one has structure that cannot be written as
shares of its own total, and collapsing both to all-zero would lose that.

theta is an argument, defaulting to #497's 1.0. It fits negative at 2023 and
2024; choosing it stays #699.

The column control is well levelled and badly allocated. Step 2 is unbuilt, so
`vapro_seed` freezes 2017's VA share of gross output, which collapses the
control to `GO(t) x T005(2017)/GO(2017)` - gross-output movement and nothing
else. Scored against the published summary T005 (`--control`): within 2.3%
economy-wide in every year 2018-2024, but weighted MAE by industry runs 2.5% to
8.0% between 2018 and 2022, with GSLG 18.3% low at 2022.

Two corrections to claims this branch had been repeating.

GSLG topping that list is the column *level* and is not #578. #578 is the
commodity *mix* inside the G* columns, sourced from govslocalfin's function x
object split and sequenced at S5 behind a go/no-go. Separately, NIPA T31005
matches the published government T005 at $0/$0/$2M in 2017 and is referenced in
no source file - so the worst cell of the control table is the one with an exact
annual source already identified and never wired.

And "Step 5 imposes both margins hard, so Step 3 estimates a shape not levels"
holds only once Step 2 exists. Checked in nowcast_targets.py: T1 is hard and
real for 2017-2024 but pins the column's sum, not the split. T4 and T6 are both
soft and still PLACEHOLDER, and `va_row_targets` does `del year` and reads its
values off `published_2017_panel`. T5 - T00OTOP and V00300 - is deliberately not
imposed, entering as seed only so the income side stays out-of-sample evidence;
V00300 is $7.873T. The balance cannot re-derive VABAS, and there is no VA seed
for any year but 2017, so it cannot run on 2024 at all until Step 2 lands. The
shape is insulated from this - it is renormalised before the control is applied,
so a wrong control rescales a column without moving a share inside it - but the
level is not, and the module and plan doc now say so.

Tests are structural and run on a toy panel: the arithmetic (`carry_shares`,
`apply_column_control`) is split from the data wiring, so they need neither GCS
nor the gross-output parquet. The year-by-year numbers are CLI flags on the
drift diagnostic (`--seed`, `--control`) rather than tests, so everything quoted
above is reproducible from committed code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…control

BEA publishes intermediate inputs (UII205-A) and value added (UVA205-A)
annually 1997-2024 on its 191-row "underlying" industry frame, in two
workbooks sitting beside the GrossOutput.xlsx this repo already reads.
Nothing read either one. This extracts both, allocates them to the 402
detail industries, and replaces Step 3's frozen-2017 VAPRO seed.

The 191->402 mapping is derived, not hand-written. UGO205-A and UGO305-A
order industries the same way, so the 138 leaves of the 205-A hierarchy
partition the 414 rows of 305-A into contiguous runs; the runs are closed
by matching gross output in all 28 years. All 138 leaves match, exactly
414 of 414 detail rows are consumed, and the 402 codes cover the model
schema once each. derive_underlying_line_mapping reproduces the
checked-in constant exactly, and --mapping on the new diagnostic is how
to re-check it when the BEA vintage moves.

Value added is allocated and intermediate inputs are taken as the
residual GO - VAPRO. That is what makes GO = T005 + VAPRO hold per
industry, which is the form T1 imposes; allocating both independently
broke it by up to $15.2B a cell at 2024. It costs nothing against BEA:

  - VAPRO summed back to the 138 lines reproduces UVA205-A exactly
    (0.0 on 3,864 cells); T005 reproduces UII205-A to $9M, which is
    BEA's own GO=II+VA rounding.
  - At 2017 the derived columns reproduce the published detail Use SUT
    margins to $0.89M (VAPRO) and $1.48M (T005) per industry.
  - Economy-wide VA matches published GDP to at most $9M on $29T in
    every year 1997-2024.
  - GO - T005 - VAPRO at detail is 2.9e-11.

It also removes the suppression problem. BEA suppresses intermediate
inputs in every year on lines 83 (Customs duties) and 176 (Private
households); both are single-industry lines whose published 2017 T005 is
zero, and the residual recovers that zero rather than needing a special
case.

Step 3's column control is now GO - VAPRO with both sides observed.
Aggregated to summary and scored against the published summary T005 it
is within 0.00007% economy-wide and 0.00023% weighted MAE by industry in
every year 2018-2024, worst summary industry 0.003%. The superseded
frozen-ratio seed scored 0.2-2.3% and 2.5-8.0%, with GSLG 18.3% low at
2022. That is a consistency check and not an independent validation --
UII205-A and the summary Use SUT's T005 are the same BEA estimate
published two ways -- so what it establishes is that the allocation adds
back correctly and the control now *is* BEA's T005. vapro_seed becomes
vapro; the now-dead _row helper and published_gross_output import go.

Signs are carried, not clipped. S00201 has a published 2017 VAPRO of
-$10,069M and stays negative in all 28 years. The residual T005 goes
negative in 11 cells, all in 5191A0 and all in 2002-2015, so the
2017-2024 nowcast span is clear; the other residual direction was worse
(13 spurious negatives across three industries, reaching -$13,117M).

This is not Step 2. VAPRO is the column total; Step 2 owes the split
across the five value-added rows, and T4/T6 are still soft placeholders.
What changes is that there is now a VA level for every year rather than
for 2017 alone, so the balance can run on 2024. It also arrives off the
P1-gated path -- a straight Excel read on the shape of the working
UGO305-A loader, so map_fbs_sectors_to_model_schema never enters it.

Repo-wide: black, ruff, mypy clean (bar the four known Windows-only
settings.py errors); 779 passed, 2 skipped, 1 xfailed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
allocate_underlying_to_detail is public and takes an arbitrary mapping.
A code appearing under two lines would be indexed twice by .loc[children]
and written twice into the output, so the group totals would silently
stop adding up rather than raising. Same for a duplicated line in
group_values, where .loc[line] returns a frame instead of a row.

The checked-in mapping has neither -- 402 codes, all distinct -- so this
guards the function, not the current data.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
WesIngwersen and others added 12 commits August 26, 2026 13:26
…ation

The plan for the value-added time series was 2017 detail shares carried on QCEW
wage growth, renormalised inside T60200D's 69 groups and rescaled to a NIPA
control. Two sources landed after it was written and they change the estimand,
not just an input: UVA205-A gives VAPRO as a column total per detail industry
annually (#712), and TVA113 splits that same VAPRO three ways at 71 summary
industries annually (#538).

value_added_timeseries.py measures the four things that decision turns on.

They nest. The yaml warns the two release archives are different vintages, so
this was checked: rolled to summary, the detail VAPRO panel and TVA113 agree to
$9M in the worst of 923 industry-years. Both margins of a 3 x n block are
therefore observed, and Step 2 estimates a cross-structure rather than a level --
the same reframing Step 3 got from #497. Rolling up needs the INDUSTRY summary
map; the commodity one drops 331314, S00101, S00201 and S00202 and leaves state
and local government enterprises 15% short in every year.

Most of it is already determined. 20 of the 71 summary industries have one
detail child (19.4% of VA, no allocator at all), and 74 of the 138 underlying
leaf lines ARE a single detail industry (63.9% of VA, BEA's own annual number).
Only 26.9% sits in the 17 groups where a within-group allocator earns its keep.

The column control is worth 0.45% of VA in 2018 rising to 2.32% -- $678bn -- in
2024, graded on BEA's own leaf lines so no allocation model enters the
measurement; 3.75% of the $18.1T where the movement is measurable at all, 81%
of it in ten groups led by other retail at 11.7%. A lower bound, and it matters
past its size because Step 5 imposes GO = T005 + VAPRO hard, so value-added
error lands in the intermediate block.

T00TOP's industry axis turns out to be published. #536's rejection of the
market-share conversion (r=0.202) stands, but its conclusion does not: TVA113's
V00200 is T00OTOP + T00TOP - T00SUB by industry, max $1M across all 71, so with
T00OTOP built the product-tax row is an annual summary residual with no operator.

QCEW is still needed for the ~80% of compensation in multi-child groups, but it
no longer carries the level, and it can now be graded on the 2012->2017 detail
holdout instead of BEA's own later years -- which needs QCEW 2012, undeclared
until #728.

Left open for review: whether TVA113 becomes an input, spending summary
V001/V003 as a test. Two arguments that the trade is smaller than #538 thought
are recorded in the plan.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both sides declared a 402 x 402 interior section since the branch point --
step3_core the Use interior (Step 3), nowcast the Supply interior (Step 4a) --
so sections.py carried the whole conflict. Both are kept, and the docstring
moves from "the one 402 x 402 section" to two of them: the sparsity caveat
belongs to Supply alone, and the Use interior gets the note that it has no
candidate until Step 5's RAS runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Carries nowcast's Step 4a work through the stack. No conflicts of its own --
sections.py and plan.md merged cleanly once step3_core had taken the five-section
resolution.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TVA113 is NOT consumed as an input. The row controls stay NIPA's, so summary
V001/V002/V003 keep their ability to grade the build. That withdraws two claims
the first pass made: T00TOP does NOT get a build route from TVA113's V00200
identity (#536's conclusion stands unchanged, the split stays free for Step 5),
and T60200D's 69 groups are NOT superseded. What the identity buys instead is a
grader -- it says where the product-tax money should have landed, so Step 5's
answer can be scored rather than trusted.

The VAPRO column control is a separate decision and it is kept. UVA205-A is the
sibling of UGO305-A, which is already T1.

The slack goes to V00300, never to T005. #710 checked that T1 pins the column's
sum and not the split, and that T5 is deliberately unimposed -- so today
income-side error lands in the intermediate column total, which is the scale of
a column of A, and propagates through L into every N. Pinning VAPRO makes
T005 = GO - VAPRO, both observed, and moves the slack into value added.

residual_headroom() prices that, and states the counterargument rather than
burying it: in percentage terms T005 is the MORE forgiving absorber, because it
is the bigger number -- a 1% compensation error is a median 0.50% of an
industry's T005 against 1.55% of its V00300. It loses because V00300 is
terminal. Nothing reads gross operating surplus: not A, not L, not any emission
factor. A 7% error in a number nothing reads beats a 1.4% error in a column
scale that multiplies through the Leontief inverse.

22 industries cannot absorb much and need a sign guard rather than trust: a 1%
compensation error moves their surplus more than 10%, worst 336414 at 121% on
an $81M surplus under $9.8B of compensation. Only one industry has a published
negative V00300 (S00201, -36,919), so a residual manufacturing new negatives is
visibly wrong and cheap to detect.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e carve-out

The test the compensation plan could never run. Two things unblocked it:
SUPPLY-USE_2026-08-24.zip carries V00100 at BEA detail for 2007/2012/2017 on one
2017 code basis (#704), and QCEW 2012 now exists (#728). Scored the way #704
requires -- on the observed benchmark span, never against BEA's carried-forward
2018-2024 -- as the share of a group's compensation dollars on the wrong detail
industry, with the group total given to every candidate so this measures shape
and not level.

                   misplaced $M   % scored   vs frozen
  frozen                 487,348      5.84         --
  qcew                   517,328      6.20      +6.2%
  qcew_covered           462,044      5.54      -5.2%
  qcew_resolvable        438,534      5.26     -10.0%

Applied everywhere QCEW makes the block WORSE. Applied where the concordance can
resolve it, it is a clear go. The damage is essentially one group: GSLG goes
from 4,748 misplaced to 71,694, on its own more than twice the net degradation.

The carve-out is derived, not fitted. The crosswalk puts 47 NAICS codes under
more than one BEA detail industry -- 23 construction, 24 government -- reaching
18 detail industries in exactly five summary groups: 23, GFE, GFGN, GSLE, GSLG.
Decidable before any score is computed, and it is the carve-out Phase 4 argued
for structurally: BEA splits construction by type of structure and NAICS by
trade, so a plumbing contractor's payroll belongs to no single BEA construction
industry.

The coverage ratio is REJECTED as the selector. Trusting QCEW where payroll
covers >=75% of compensation scores -5.2%, but the floor is non-monotonic:
-9.7% at 0.70, -5.2% at 0.75, -3.5% at 0.80, +0.0% at 0.90. Between 0.70 and
0.75 it drops the five groups where QCEW helps most. Coverage and predictive
value are different quantities.

Two vintage traps had to be cleared and each faked a verdict. QCEW 2012 is on
NAICS 2012 against a NAICS 2017 crosswalk -- 28 codes in 2012 not 2017, 20 the
other way -- which put 541700 at 20.9x growth and made raw QCEW score +92%
before the filter. And the ambiguous concordance would not have raised; it would
have produced an even split and called it an answer, exactly as #536 warned.
Neither is visible in a total.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
T1 pins the Use column's sum; nothing pinned the split. T4 is soft and
aggregated to summary groups, T6 is two economy-wide scalars, and T5 is
unimposed by design -- so every dollar of value-added estimation error
landed in the intermediate column total, which is the scale of a column
of the technology matrix and multiplies through L into every downstream
result.

T18 pins VAPRO from UVA205-A, the sibling of the UGO305-A already behind
T1, allocated to 402 detail. With both, T005 is determined per industry
and the slack moves inside value added, where V00300 is free -- gross
operating surplus, which BEA largely computes as a residual and which
appears in no A, no L and no emission factor.

Measured, the error it takes off the intermediate block is 2.75% of T005
in 2018 rising to 9.24% ($1.98tn) in 2024, worst industry GSLGE at 23.5%
of its own T005. 2017 reads 31, which is what says the measurement is
not scoring its own anchor.

T1 and T18 cannot disagree: T1 reads the extracted UGO305-A parquet and
T18's series is derived against load_go_detail(), and the two gross
output vectors are identical to the dollar for every industry in every
year 2017-2024.

A column margin restricted to five rows partitions the column, and
gras_balance takes one column vector -- so T18 is imposed as a closer
after each Use pass, the way T4 is: +d on V00300 and -d across that
column's free sign-flexible commodity cells, leaving the column sum and
therefore T1 untouched. All of it on V00300 deliberately; a proportional
scale would move V00100 and undo the T4 closer that just ran. On the
2017 replay T18 closes to 0.005 and T1 holds at 0.15.

T5's absence is now load-bearing rather than tolerated: imposing it would
push the slack back into T005 and undo this. Said so where someone would
go to add it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The plan's design was "2017 detail share x QCEW growth, renormalised
in-parent, then the NIPA control", blocked on FBS_outside_flowsa not
working as an attribution source. It is bypassed rather than cleared: a
weight vector that is a rescaling of an FBA already in the method needs
no new source, so a clean_fba socket on the existing BEA_Detail_Use_SUT
attribution source scales the 2017 benchmark weights by QCEW growth
before sector mapping. The blocker is diagnosed to two lines on #731,
where T00OTOP's housing and farm lookups still want it.

All eight years build. 2017 still reproduces the published benchmark row
to $1M, which is the check that wiring the movement in did not disturb
the identity case; 206 industries move in 2018 and $66.8bn changes hands
against frozen shares, 0.61% of the row.

Pointing proportional straight at the QCEW FBA would have been the
trivial build, and it is rejected on measurement: QCEW level shares score
+21.9% worse than frozen at their best and +270.9% raw, against -10.0%
for the movement form. QCEW's share of an industry's compensation is not
that industry's share of compensation, but its change carries signal.

Four corrections, each of which changes the answer, and none of which is
visible in a total:

- The concordance cannot resolve construction or government. 47 NAICS
  codes sit under more than one BEA detail industry and reach exactly
  five summary groups. Applied everywhere QCEW is +6.2%; carved out it is
  -10.0%. Derived from the crosswalk, not fitted.
- QCEW changes NAICS vintage at data year 2022, not 2023. Unbridged it
  costs 31 detail industries their coverage and doubles unmapped payroll
  from 10.4% to 20.7%. The vintage is now detected from the concordance
  and the two years are paired through it, so composition is identical
  across the ratio by construction.
- 482000 rail is outside QCEW's universe -- railroad employees fall under
  the Railroad Retirement Board, not state UI -- so QCEW sees 0.11% of
  its compensation and rail "grows" 4.45x by 2024. A 1% coverage guard
  catches it. The holdout is neutral on that guard (-10.016%, identical),
  because the 2012->2017 span does not contain the failure; it is kept on
  an argument about the source rather than the score, and tuning it
  upward makes the block worse.
- A published zero is a suppression. QCEW reports exactly 0.0 for both
  NAICS under 334610 in 2021 with normal payroll either side; read as an
  observation it zeroes the weight and deletes the industry. One
  occurrence in seven years, fatal each time, so --check sweeps all of
  them.

Also generates the BEA_NIPA FBAs for 2018-2023, which existed without
T60200D and so produced no activity sets at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…kups

V00100 shipped on QCEW movement last commit; T00OTOP and V00300 were still
2017-only, so derive_initial_value_added raised for every other year. Both
now have per-year files and the guard is lifted: the block builds for
2017-2024, VABAS 18.92tn to 28.29tn.

The three rows are three different claims and the docstring now says so
rather than calling the block "value added, nowcast":

- T00OTOP is a level plus two lookups. The T30500 control is read per year
  (+40.5%), and 43.3% of the row is no longer a frozen share.
- V00300 is a seed and only a seed. Level from the eight-line assembly per
  year (+57.4%), shares frozen at 2017 where drift reaches 12.51% by 2022 -
  six times T00OTOP's. That is acceptable only because T18 makes it the
  residual the balance overwrites, and TVA113 must not "fix" it: that is the
  grader.

T00OTOP's housing and farm lookups are in, and they should have been in the
first time. T70405 B1031C is the 531HSO+531HST pair and T70305 B1017C is the
ten farm codes, both exact, both published annually, together 43.3% of the
row. They were left out because folding a computed weight vector in was read
as needing an FBS_outside_flowsa attribution source, which does not work.
That hatch is still broken and was never on the path: a weight vector that is
a rescaling of an FBA already in the method is a clean_fba socket, which is
how V00100 carries QCEW. Both rows were blocked on a hatch neither needed.
Worth 1.92% to 1.68% of row error in 2024, 0.12-0.39pp across the span -
real, bounded, and smaller than the control's own 2.9% vintage error in 2021.

Two things measured on the way that are not about these rows:

- The summary SUT is a stale grader for exactly 2019-2022. Its own VAPRO
  total sits 0.09-1.21% below current-vintage UVA205-A in those four years
  and matches to the dollar in 2017, 2018, 2023 and 2024. So V00300's
  apparent 2.64% assembly error in 2022 is the workbook being behind, not the
  assembly being wrong - and V00300 shows it at twice VAPRO's rate because it
  is the row a NIPA revision lands in.
- Selecting NIPA controls by line number is only safe because the lines do
  not move. Verified for all eight years and now asserted under --check:
  T30500 line 37 is LA000365 and line 17 is LA000237 in every one, and the
  eight V00300 lines are equally stable. A restructuring would point these at
  different series and no total would look wrong.

Also withdraws stale text in five places saying the T00TOP/T00SUB industry
split is "an output of Step 5's balance, not an input to it".
tax_axis_conversion.py takes that apart: rejecting the Make matrix (r = 0.202)
is not the same as having no operator, and Step 4c's producer/trade level
split plus a few named routings reaches r = 0.948. The split has no industry
target and is still seeded, so it is an input to the balance as well as an
output. The seed itself is not built here - it reads commodity output and
margins, both mid-update.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…l, and the NAICS-2022 goods Crosswalk

# Conflicts:
#	bedrock/analysis/nowcasting/README.md
#	bedrock/analysis/nowcasting/progress_report.md
#	bedrock/analysis/nowcasting/sections.py
#	bedrock/transform/eeio/nowcast.py
#	bedrock/transform/iot/nowcast_intermediate.py
…ded block

The last two value-added rows, and the only two that are not estimated: they
are the same money as the Supply table's TOP/MDTY/SUB columns, carried on the
industry axis. So `nowcast_va_taxes` converts rather than re-estimates, and its
whole content is the operator. Both rows are stacked into
`derive_initial_value_added`, which now returns all five rows for 2017-2024.

The two rows needed two different operators, and only one of them is a seed.

T00TOP - the level split, at r = 0.947 and 27.9% error. Market shares fail this
badly (r = 0.202) and not by being noisy: a tax on a product is remitted by
whoever sells it, so they send the whole petroleum tax from wholesalers to
refineries. `top_by_level` already draws the producer-versus-seller line Step 4c
needed for its own reasons. Two of the three legs are exact rather than
estimated - duties are a lookup onto 4200ID, and the ten government columns are
zero by an accounting rule, which the market-share leg was violating by 10,513.
Still a seed; the residual is 20 named trade industries Step 5 moves.

T00SUB - not a seed. A subsidy is paid to an operator, so it stays on its own
code, and code identity's entire residual is two pairs of cells rather than a
smear: S00203 public housing authorities and S00102 federal insurance
enterprises, both government enterprises that produce a subsidised commodity.
Routing those two by name closes 2017 to shape agreement of 7e-17. What is
frozen is one number and it is named - S00203's 54.4% of the housing line, which
NIPA T31300 does not split.

Three things found while building it, all now asserted:

- The routings must not fire in 2020-21, where `sub_decomposition` replaces the
  `other` type with PPP. Routing 5241XX then would put pandemic support on a
  federal enterprise. PPP is already industry-shaped, so identity is at its best
  in those years rather than its worst.
- S00102 is over-subsidised from 2022 (36.6bn against 6.3bn in 2017). The cause
  is upstream: NIPA's `other` line still carries pandemic-era programmes, and
  the same 36.6bn sits on commodity 5241XX with or without this conversion.
  Documented as the `other` line's residue, not as a measurement.
- The T00SUB level carries a 1 $M gap - NIPA's 59,875 against the workbook's
  59,876 - spread proportionally across all twelve subsidised cells. `check`
  tests shape and level separately, because scoring raw levels reports BEA's own
  rounding as error and could hide a real one behind it.

`use_va_detail_sut` now grades five rows instead of three, with the published
T00SUB flipped to the balance's sign convention. The 48 partial column totals
are all T00TOP inside the trade block, which is the documented state of the
seed.

Also updates two constants in test_nowcast_product_taxes that moved when the
Trade FBSs were rebuilt on the NAICS-2022 goods Crosswalk (#734): TOP's residual
now rides a purchaser-price base carrying MCIF, so 2024's tobacco gap went
-26,153 to -26,282 and its share 7.7% to 7.3%. 2020's two numbers did not move
at all, which is the check that the shift is the Crosswalk and not the method.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
WesIngwersen added a commit that referenced this pull request Aug 27, 2026
Docs only. The plan and progress report predated #733/#734 and were describing
a Supply bridge that no longer exists.

Supply column table, corrected against a live coverage run rather than from
memory: eight of the twelve columns now run 2017-2024, and only TRADE, TRANS
and therefore T014/T016 stop at 2023. MCIF, MADJ and MDTY were still listed as
"2017 candidate" and have been annual since #730. TRANS was "2017-2022" and now
reaches 2023 from AIES.

TOP's residual is no longer frozen, which the plan asserted in two places. The
purchaser-price base did not have to wait for Step 5 as the plan predicted - it
is assembled from the bridge's own components, and is not circular because TOP
sits in T015 and is excluded from its own base. Recorded with what it buys
(S00402's 174,312 base, the 0.939-1.008 rescale) and what it costs: TOP now
depends on margin coverage, so 2024 holds 2023's shares.

Step 1: F04000 was described as all-zero for 2018-2024 and has been annual since
#730 (2,211bn to 3,139bn). F03000 really is still 2017-only, so the two are now
stated separately instead of together.

Numbers re-measured rather than edited by hand. Steps 1 and 4 re-run, and the
embedded images regenerated with them: supply bridge coverage 99.0% to 99.2%,
accuracy 61.8% to 62.1%, grand total error 0.092% to 0.048%. That improvement is
not Step 4a - it is the Trade FBSs rebuilt on the NAICS-2022 goods Crosswalk,
which moved MCIF (misses 27 to 22) and carried through T013 and T016. Per-column
table updated with it. Step 3 is unchanged and was not re-run.

⚠️ The 2017 trade scorecard moved in two directions at once and the README now
says so rather than reporting the half that flatters it: the export national
error roughly halved, +12.20% to +6.16%, while Pearson fell 0.96 to 0.92,
top-20 Jaccard 0.67 to 0.60 and n_miss rose 39 to 43. The extra mass lands
closer to the published total but spread over more Detail codes. The prior row
is kept for comparison.

⚠️ The stored per-commodity trade baseline was deliberately NOT refreshed.
score_2017_trade_detail flags a handful of expected status changes against it;
--update-baseline is a judgement call and has not been made.

Step 2's plan entries are not touched here - they belong to #735.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant