Skip to content

Move congressional districts to the 120th Congress #1526

Open

Description

@DTrim99

Part of #48. This tracks moving PolicyEngine's congressional-district data, maps and labels from the 119th to the 120th Congress, which is seated on January 3, 2027.

What changes

  • Nine states use new congressional maps for the 120th Congress: Alabama, California, Florida, Louisiana, North Carolina, Ohio, Tennessee, Texas and Utah, 173 of the 435 seats.
  • Missouri keeps its 2022 map. It used that map in 2026, and the November 2026 referendum on its new map failed.
  • Virginia keeps its previous map, because its new one was struck down.
  • Each district ID follows its state's new numbering: in the 120th data, TX-35 is the area Texas's new map numbers 35. Seat counts come from the 2020 apportionment, so the list of 436 IDs (AL-01 … WY-01, plus DC) doesn't change.
  • In the nine states, an ID now names a different area, and different households. For example, 40% of the new TX-35's 2020 population was in the old TX-28, and only 10% was in the old TX-35.

Today every layer assumes 119th districts, and nothing records which Congress a result used:

  • Households: congressional_district_geoid is drawn from 119th PUMA overlaps (congressional_district_vintage.py, the block and PUMA ladders).
  • Targets: district targets use 119th codes.
  • Map: app-v2 draws cb_2024_us_cd119_20m, and that file is copied into ui-kit and about 19 dashboards.

Decisions

  • Swap rather than run both. New releases use 120th districts in congressional_district_geoid. Every district result carries a congress field. A result without one draws on the 119th map, so saved reports keep theirs.
  • Ship when the checks below pass, aiming for mid-December, labeled "120th Congress districts (from January 3, 2027)".
  • Label each district with its member's name and party, or "Not yet called" until the race is called. After January 3, take members from unitedstates/congress-legislators.
  • Only the app and future projects move to the 120th. Existing dashboards keep the 119th districts they were built with, unless their owner chooses to update one.

Sources

Work

Phase 1: shared 120th geography

  • Check the registry's 120th_congress plan (Add the US block -> congressional-district plan registry (117th-120th) #1041) against Census's October 7 national file, block by block. Also check that the 41 unchanged states, DC and Missouri keep identical assignments.
  • Build county, tract and PUMA → 120th district population shares, and a 119th → 120th overlap table.
  • Build the 120th map file: the nine redrawn states from TIGER/Line 2026, clipped to the shoreline and simplified, and every other state unchanged. Keep the current DISTRICT_ID keys and add CONGRESS and CD120FP. It will live in policyengine-ui-kit, versioned by Congress.
  • Check the hex layout positions inside the nine states.
  • Build the labels table: district name, member-elect, party and status, with a source and date for each entry.

Phase 2: households on 120th districts (microcosm)

Phase 3: model, API and app

  • policyengine.py: pin the release, record the district plan in the bundle, and add congress to the district impact output.
  • policyengine-sim-api: add congress to the output contract.
  • policyengine-api: put the district plan in the metadata. The cache refreshes with the bundle version.
  • policyengine-app-v2: choose the map by congress, show member labels in tooltips, and add the 120th note.

Phase 4: dashboards (no migration)

  • Existing dashboards stay on the 119th districts they were built with, unless their owner changes them.
    • None of the 21 dashboards with district maps calls the API for district results; they use precomputed or pinned data, so none shifts on its own.
    • A dashboard whose pins are moved to the 120th release should switch its map then, using ui-kit's congress={120}.
  • New projects use ui-kit's 120th maps and members (congress={120}) instead of copying map files.

Checks before release

  • District populations from block sums equal the CD120 Summary File.
  • The map has 436 features matching the API's district list.
  • District weights sum to the state totals.
  • For a fixed reform, districts in unchanged states give the same results before and after.
  • On a preview, a saved report draws the 119th map and a new report draws the 120th.

Activity

  1. DTrim99 commented on Nov 5, 2026

    @DTrim99
    CollaboratorAuthor

    Phase 1, step 1: the Census 120th file against the 119th

    I compared Census's current national 120th block equivalency file with the 119th one, block by block.

    • Current file: cd120.zip (last modified 2026-10-07, SHA-256 bc9a4fc5…8cc8a5).
    • 119th file: cd119.zip, SHA-256 1433feb5…294bd6, which matches the registry's pin.
    Blocks in each file 8,174,955, with the same IDs and no duplicates
    States with any block in a different district AL 23,529 · CA 133,426 · FL 99,385 · LA 40,716 · NC 18,826 · OH 47,697 · TN 63,380 · TX 192,131 · UT 47,874
    Missouri 0 blocks changed: Census's regenerated file now encodes its 2022 map
    Every other state and DC identical assignments
    ZZ (unassigned water) CT 81, IL 72 and NH 3 in both files
    Districts (state, district pairs) 437 in both: 435, DC and PR

    So 120th districts equal 119th districts everywhere except those nine states. Targets and shares for the other 41 states and DC carry over unchanged.

    A stale pin in the registry

    cd_plan_registry (#1041) pins the August 31 cd120.zip (f3074ac5…). Census replaced the file at that URL on October 7, so a rebuild with tools/build_us_cd_plan_registry_artifact.py would now fail its SHA check.

    • Suggested fix: re-pin to the October 7 file. Its Missouri rows are the 2022 map, so the known_deviations["29"] override should become a no-op check, or be removed with a note.
    • Missouri is settled: the November 2026 referendum on the 2025 map failed, so the 2022 map stands for the 120th Congress.

    Two questions for @MaxGhenis

    1. Route for Phase 2.

      I'd go with (a), unless US household location v1: one census block per household, every geography derived #1047 is close to landing.

    2. Who rebuilds the registry. OK for me to open a PR that re-pins it and rebuilds it in CI? The build downloads about 4 GB of Census block files, which is more than I can run locally.

  2. DTrim99 commented on Nov 5, 2026

    @DTrim99
    CollaboratorAuthor

    Phase 1: shared geography is up for review

    PolicyEngine/policyengine-ui-kit#41 adds 120th Congress boundaries and hexes beside the 119th ones, chosen with a congress option that defaults to 119. Its data-sources/congressional-districts/ folder (not packaged) has:

    • every source with its SHA-256;
    • the build scripts;
    • tables built from 2020 block populations:
      • district keys in each format we use, including policyengine-us's congressional_district_geoid;
      • district populations;
      • county → district and tract → district shares;
      • 119th → 120th overlaps;
      • a per-state change summary.

    Checks:

    • Block populations sum to 331,449,281 people in 5,769,942 populated blocks, which matches the registry's block ladder.
    • Boundaries. Tracts wholly inside one district should have their internal point inside that district's shape. On the 120th file, 1.02% of those points in the redrawn states land in another district, and 0.94% elsewhere. The 119th file the app uses today gives 2.51% and 1.97% on the same test.
    • Alabama comes out at 2 of 1,382 tracts, so TIGER/Line 2026 carries its June map.
    • The 120th GEOID prefix is 5002000US, confirmed from TIGER/Line's GEOIDFQ.

    Checklist:

    • Census 120th file checked against the 119th, block by block (comment above).
    • County, tract and 119th → 120th tables.
    • 120th boundary file.
    • Hex layout: The Downballot's hexmap v3.2 (CC BY 4.0) moves CA, FL, NC, OH, TN, TX and UT, and leaves AL, LA and MO as they were.
    • Labels table (member-elect, party, status): next, as races are called.
    • Registry re-pin to the October 7 file: waiting on the question above.
  3. DTrim99 commented on Nov 5, 2026

    @DTrim99
    CollaboratorAuthor

    Phase 2 prep: district targets on 120th boundaries

    The build calibrates to ACS 2024 1-year S0101 (18 age bands) and S2201 (households, receiving SNAP, not receiving) for each 119th district, from Ledger's acs_s0101_district_2024 and acs_s2201_district_2024. Census won't publish ACS on 120th boundaries until at least the 2026 1-year (around September 2027).

    • Unchanged districts. Their assignments are identical to the 119th (comment above), so 263 districts in 41 states, plus DC, can keep their values exactly.
    • The nine redrawn states. There are two ways to re-express each 119th district's 1-year value on the new districts. I tested both:
      • Crosswalk. Split the value by 2020 population overlap: people for the age bands, occupied housing units for households. This is how the SOI targets already move from the 117th to the 119th.
      • Block-group method. Split it by where the characteristic is, using ACS 2020–2024 5-year block-group estimates (B01001 age by sex, B22010 SNAP). Each block group is divided among districts by its blocks' 2020 people or occupied units.

    Test 1: the block-group method against ground truth. I built every 119th district's 5-year value from block groups and compared it with Census's published 5-year value for that district.

    Redrawn states (173 districts) Median error 95th percentile Max
    Ages 0–4 0.19% 0.80% 1.3%
    Ages 35–39 0.18% 0.76% 1.2%
    Ages 85+ 0.26% 1.5% 4.6%
    Households 0.06% 0.26% 0.6%
    Households receiving SNAP 0.18% 0.85% 1.8%

    Elsewhere, two states are off for coding reasons, and neither redrew:

    • Connecticut: ACS block groups use the 2022 planning-region county codes, so they don't match 2020 blocks.
    • New York: 39 block groups (0.37% of the state) are missing.

    In the nine redrawn states, every 2020 block's block group is in the ACS file.

    Test 2: the crosswalk against the block-group estimate on 120th districts in the nine states. These are 5-year values, so both start from the same data.

    Median gap 95th percentile Max
    Ages 0–4 1.7% 10.7% 24%
    Ages 85+ 2.8% 17% 46%
    Households 0.3% 2.0% 3.2%
    Households receiving SNAP 4.7% 27% 64%

    The crosswalk misplaces age and SNAP targets by several percent at the median and by tens of percent in some districts. The block-group method's own error is about 0.2%.

    Result. I've produced 1-year 2024 targets for all 436 districts on 120th boundaries:

    • the 263 unchanged districts keep their 1-year values;
    • each redrawn 119th district's 1-year value is split across its 120th pieces in proportion to the block-group estimate.

    State totals are preserved exactly. Codes use the 5002000US prefix (TIGER/Line 2026 GEOIDFQ). @MaxGhenis, where should these live: a derived Ledger package beside the 119th ones, or a microcosm build input? I'll open the PR wherever you prefer. The script and output CSVs are ready.

    Proposal: how a result records its Congress

    The app needs to know which boundaries a district result was computed on, so that saved 119th reports keep their map after the switch. A suggested chain:

    1. microcosm writes the plan as a first-class field in the US release manifest. Today it appears only inside reviewed_limitations[].layer_vintages.congressional_district ("119th_congress"). The pool H5 also carries the populace_congressional_district_vintage_target root attribute.
    2. policyengine.py certification copies it into the bundle, for example data_releases.us.congressional_district_plan.
    3. CongressionalDistrictImpact reports congress (119 or 120) from the bundle entry for the dataset it ran on.
    4. policyengine-sim-api adds an optional congress to CongressionalDistrictImpactOutput.
    5. The app draws congress ?? 119.

    All five steps are backward compatible, and they can land before the data switch. Results would say 119 until the 120th release is pinned. Does that match how you'd want the vintage carried, or should policyengine.py read it from the dataset itself?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions