Skip to content

Collect ACS margins of error - #173

Merged
duaneatat merged 4 commits into
db/backend-test-suitefrom
db/acs-moe-collection
Nov 2, 2026
Merged

duaneatat merged 4 commits into
db/backend-test-suitefrom
db/acs-moe-collection

Conversation

@duaneatat

@duaneatat duaneatat commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator

First of a stacked set adding Census margins of error (MOE), split by ETL step: collection, cleaning, then frontend. It's based on #171, so review that first. This one only changes what we collect into the RAW lake tables. The cleaners select explicit columns, so they ignore the new columns until the cleaning PR. It also stops storing Census placeholder codes as real values: tiny towns like Averill had a median household income of -666,666,666.

  • B-table scrapers (demographics, economics, education, housing) request whole tables with group(), which includes every margin and stays under the API's 50-variable limit. Rows gain Value_MOE and Percent_MOE, using the Census formulas for sums and proportions (see the docstrings in data_collection/base.py).
  • Negative B-table estimates are null, and a sum with a missing component is null instead of partial.
  • Data profile rows (DP02–DP05) keep their published margin as an MOE column. The profile variable list names only E and PE codes, so the M and PM codes were being dropped.
  • test_base.py imports from data_collection again, and the backend CI workflow now runs it.
  • I checked against the Census's own published profile margins for 2023 (eight measures, all towns): the median ratio is 1.0, and median household income matches exactly.

Closes #172

The B-table scrapers (demographics, economic, education, housing) kept
Census annotation codes such as -666666666 as real numbers and filled
missing components with 0 before summing. Tiny towns like Averill and
Glastenbury ended up with a median household income of -666,666,666,
and a sum could include a sentinel or skip a missing part.

Negative estimates are now missing, and a sum with any missing
component is null rather than partial. Also points test_base.py at
data_collection.base; it imported the deleted data_collection_OLD and
could not be collected.
The B-table scrapers fetched only estimates. They now request each
table with group(), which returns every estimate and margin of error and
stays under the API's 50-variable limit (B01001 alone needs 49 codes),
and each tidy row gains Value_MOE and Percent_MOE (percentage points).

Margins follow the Census approximations: root sum of squares for sums,
counting only the largest margin among zero estimates, and the
proportion formula for percents, with the ratio form when the term under
the root is negative or zero (a 100% share). -555555555 margins count as
zero; other negative codes are missing.
group(DPxx) already returns each estimate's margin (M) and each
percent's margin (PM), but the profile variables.json lists only E and
PE codes, so the tidy merge dropped them. Each tidy row now carries its
margin as MOE. They're a column rather than rows of their own so that
selectors on Measure (e.g. ILIKE 'Percent%') don't pick them up.

@iansargent iansargent left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This all looks great! Approving.

@duaneatat
duaneatat merged commit 3de8a18 into db/backend-test-suite Nov 2, 2026
4 checks passed
iansargent added a commit that referenced this pull request Nov 3, 2026
Collect ACS margins of error (redo of #173)
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.

2 participants