Skip to content

Eliminate redundant dataset deep-copies in multi-year extension (11x faster)#9000

Open
anth-volk wants to merge 4 commits into
mainfrom
perf/shallow-dataset-extension
Open

Eliminate redundant dataset deep-copies in multi-year extension (11x faster)#9000
anth-volk wants to merge 4 commits into
mainfrom
perf/shallow-dataset-extension

Conversation

@anth-volk

@anth-volk anth-volk commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Fixes #9001

Summary

Microsimulation() setup profiling showed ~half of init time was spent deep-copying the full dataset ~23 times during multi-year extension: extend_single_year_dataset deep-copies all six entity DataFrames once per projected year (2024–2035), then _apply_uprating deep-copies the entire multi-year set again. Uprated columns are overwritten immediately after being copied; carried-forward columns never change at all.

Under pandas copy-on-write (always on in pandas ≥ 3), year frames are now shallow copies of the base year: carried-forward columns share base-year buffers, and uprating's whole-column assignments materialize only the columns they replace. _apply_uprating's redundant defensive copy is skipped on the extension path (external callers keep the copying default).

Measured results (full Populace 2024 dataset, 12 years)

Metric Before After
Median extension wall-time (3 runs) 20.95s 1.90s (11×)
Per-column checksums (3,876 across all years/tables) bit-identical
End-to-end Microsimulation() init ~30s ~8s

Equivalence was verified with pd.util.hash_pandas_object checksums of every column of every entity table for every extended year, identical before/after.

Impact on state-level runs

Each Microsimulation() build sheds ~19s of redundant copying (init ~27s → ~8s). A reform analysis constructing baseline + reform simulations saves ~40s — roughly 13–33% of a 2–5 minute state-level run.

Compatibility

  • pandas pin: pandas>=3.0 on Python ≥ 3.11; Python 3.9/3.10 (retained for Census, Support Python 3.9 and 3.10 #8035) keep pandas>=2.0 via environment markers — mirroring policyengine-uk's tables marker pattern. uv lock resolution is unchanged (2.3.3 / 3.0.0).
  • On pandas 2.x the extension falls back to deep copies (today's exact behavior); shallow copies without CoW alias mutably — verified empirically that in-place ops (*=, .loc) write through on 2.x, which is why the fast path is version-gated.
  • Core-side aliasing is safe: holder._to_array casts float64→float32 at set_input, materializing fresh arrays, and pandas 3 hands out read-only .values views.

Tests

  • New copy-semantics tests for copy(deep=...) on both dataset classes.
  • Regression tests pin: exact equivalence with a deep-copy reference path, caller-dataset isolation, and the buffer sharing itself (so deep copies can't silently return), plus _apply_uprating's default defensive copy.

Commits

  1. Marker-based pandas pin (no resolution change)
  2. deep parameter on dataset copy methods (pure refactor, default unchanged)
  3. Shallow extension + regression tests

🤖 Generated with Claude Code

anth-volk and others added 3 commits July 11, 2026 00:20
pandas 3's always-on copy-on-write enables the shallow-copy dataset
extension fast path added in the following commits. Python 3.9/3.10
(retained for Census, #8035) stay on pandas 2.x via environment
markers, mirroring policyengine-uk's tables pin pattern.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
USSingleYearDataset.copy and USMultiYearDataset.copy accept
deep=False to produce shallow copies whose entity DataFrames share
column buffers under pandas copy-on-write. Default remains deep=True,
so existing callers are unchanged. Shallow callers must use
whole-column assignment only; on pandas 2.x shallow copies alias
mutably, which is why the fast path is gated on pandas 3 downstream.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
extend_single_year_dataset deep-copied all six entity DataFrames once
per projected year (2024-2035), and _apply_uprating deep-copied the
entire multi-year set again — 23 full dataset copies, most of which
were immediately overwritten by uprating or never modified at all.

Year frames are now shallow copies of the base year when pandas
copy-on-write is available (pandas >= 3): carried-forward columns
share base-year buffers, and uprating's whole-column assignments
materialize only the columns they replace. _apply_uprating gains a
copy flag so the extension path can skip its defensive copy; external
callers keep the copying default. On pandas 2.x (Python 3.9/3.10)
the extension falls back to deep copies, preserving today's behavior.

Measured on the full Populace 2024 dataset (12 years): median
extension time 20.95s -> 1.90s (11x), with all 3,876 per-column
checksums across all years bit-identical to the deep-copy path.

Regression tests pin equivalence with a deep-copy reference, caller
isolation, and the buffer sharing itself (so deep copies can't
silently return).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@anth-volk anth-volk marked this pull request as draft July 10, 2026 22:28
…g naming

- Update extend_single_year_dataset and _apply_single_year_uprating
  docstrings to describe the shallow-copy fast path instead of the
  removed deep-copy mechanism.
- Import _PANDAS_COW in test_dataset_copy.py instead of redefining it.
- Test _apply_uprating(copy=False) in-place semantics, and add a
  forced-fallback test proving the pandas 2.x deep-copy path produces
  output identical to the fast path (the only CI coverage of the path
  Python 3.9/3.10 installs use).
- Merge the two changelog fragments into one named after the branch
  per the changelog.d/<branch-name>.<type>.md convention.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@anth-volk anth-volk requested a review from PavelMakarchuk July 10, 2026 23:02
@anth-volk anth-volk marked this pull request as ready for review July 10, 2026 23:02
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.

Dataset extension deep-copies the full dataset ~23 times, adding ~20s to every Microsimulation build

1 participant