From 3a4ff53654c75e4559b7d55374e8f00c0261b392 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 8 Sep 2026 15:22:24 -0400 Subject: [PATCH] Pin reviewed SPM correction and verify published threshold values --- changelog.d/spm-release-pin.changed.md | 1 + .../spm_unit/test_spm_unit_spm_threshold.py | 26 +++- pyproject.toml | 5 +- uv.lock | 115 ++++++------------ 4 files changed, 68 insertions(+), 79 deletions(-) create mode 100644 changelog.d/spm-release-pin.changed.md diff --git a/changelog.d/spm-release-pin.changed.md b/changelog.d/spm-release-pin.changed.md new file mode 100644 index 00000000000..233465d5a01 --- /dev/null +++ b/changelog.d/spm-release-pin.changed.md @@ -0,0 +1 @@ +Pin the reviewed SPM Calculator 0.5.0 development commit for reproducible threshold corrections, including the revised 2019–2024 series and published 2025 continuation. Preserve legacy housing-share constants and CPI-U extrapolation. Replace the Git pin with exactly 0.5.0 only during coordinated package promotion. diff --git a/policyengine_us/tests/policy/baseline/household/income/spm_unit/test_spm_unit_spm_threshold.py b/policyengine_us/tests/policy/baseline/household/income/spm_unit/test_spm_unit_spm_threshold.py index 012c7b647f8..0e36f31185b 100644 --- a/policyengine_us/tests/policy/baseline/household/income/spm_unit/test_spm_unit_spm_threshold.py +++ b/policyengine_us/tests/policy/baseline/household/income/spm_unit/test_spm_unit_spm_threshold.py @@ -1,14 +1,14 @@ """Regression tests for the spm_unit_spm_threshold formula. Verifies that: -1. Historical published years (2015-2024) match the Census Bureau's +1. Historical published years (2005-2025) match the Census Bureau's published Betson reference thresholds exactly (via spm-calculator). 2. Post-published years uprate via PolicyEngine's ``gov.bls.cpi.cpi_u`` parameter. 3. Composition and tenure changes between periods flow through to the threshold while applying the unit-specific geographic adjustment. 4. The Betson three-parameter equivalence scale is applied (a 2A2C - reference family at the renter national base equals 39430 in 2024). + reference family at the corrected renter national base equals 39219.893902 in 2024). """ import numpy as np @@ -56,7 +56,7 @@ def test_reference_threshold_matches_census_for_published_years(): def test_reference_threshold_uprates_with_cpi_u_past_latest_published(): - """Post-2024 reference threshold must equal the latest published + """Post-published reference threshold must equal the latest published value scaled by PolicyEngine's CPI-U ratio.""" cpi_u = _cpi_u() latest = LATEST_PUBLISHED_SPM_THRESHOLD_YEAR @@ -259,3 +259,23 @@ def test_prior_threshold_does_not_imply_geographic_adjustment(): got = float(sim.calculate("spm_unit_spm_threshold", YEAR)[0]) expected = current_base * equiv assert got == pytest.approx(expected, rel=1e-5) + + +@pytest.mark.parametrize( + "year,tenure,expected", + [ + (2024, SPMUnitTenureType.OWNER_WITH_MORTGAGE, 39230.994457), + (2024, SPMUnitTenureType.OWNER_WITHOUT_MORTGAGE, 32878.594848), + (2024, SPMUnitTenureType.RENTER, 39219.893902), + (2025, SPMUnitTenureType.OWNER_WITH_MORTGAGE, 41322.707394), + (2025, SPMUnitTenureType.OWNER_WITHOUT_MORTGAGE, 34325.997720), + (2025, SPMUnitTenureType.RENTER, 41700.555713), + ], +) +def test_reviewed_bls_correction_and_2025_continuation(year, tenure, expected): + # Literal workbook values, independent of the installed package's table. + # 2024 revised segment: https://www.bls.gov/pir/spm/spm_thresholds_2024_correction.htm + # 2025 continuation: https://www.bls.gov/pir/spmhome.htm + assert LATEST_PUBLISHED_SPM_THRESHOLD_YEAR == 2025 + actual = _reference_threshold_array(np.array([tenure]), year, _cpi_u())[0] + assert actual == pytest.approx(expected, abs=0.000001, rel=0) diff --git a/pyproject.toml b/pyproject.toml index 8c4794a7415..10bafb0f2b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ dependencies = [ # extension and is available on every supported Python version. "pandas>=3.0", "policyengine-core>=3.30.1", - "spm-calculator>=0.2.0", + "spm-calculator @ git+https://github.com/PolicyEngine/spm-calculator@9e6ae4798458771231613b994df2345dd1685214", "tables>=3.9", "tqdm>=4.67.1", ] @@ -47,6 +47,9 @@ dev = [ "towncrier>=24.8.0", ] +[tool.hatch.metadata] +allow-direct-references = true + [tool.hatch.build.targets.wheel] packages = ["policyengine_us"] diff --git a/uv.lock b/uv.lock index 627244a0371..d57e8890093 100644 --- a/uv.lock +++ b/uv.lock @@ -1663,7 +1663,7 @@ name = "pexpect" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ptyprocess" }, + { name = "ptyprocess", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } wheels = [ @@ -1730,14 +1730,13 @@ wheels = [ [[package]] name = "policyengine-us" -version = "1.800.0" +version = "1.824.3" source = { editable = "." } dependencies = [ { name = "microdf-python" }, { name = "pandas" }, { name = "policyengine-core" }, - { name = "spm-calculator", version = "0.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, - { name = "spm-calculator", version = "0.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "spm-calculator" }, { name = "tables" }, { name = "tqdm" }, ] @@ -1764,7 +1763,7 @@ requires-dist = [ { name = "policyengine-core", specifier = ">=3.30.1" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.9.0" }, { name = "setuptools", marker = "extra == 'dev'", specifier = ">=80.9.0" }, - { name = "spm-calculator", specifier = ">=0.2.0" }, + { name = "spm-calculator", git = "https://github.com/PolicyEngine/spm-calculator?rev=9e6ae4798458771231613b994df2345dd1685214" }, { name = "tables", specifier = ">=3.9" }, { name = "towncrier", marker = "extra == 'dev'", specifier = ">=24.8.0" }, { name = "tqdm", specifier = ">=4.67.1" }, @@ -2316,23 +2315,23 @@ resolution-markers = [ "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "alabaster" }, - { name = "babel" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "docutils" }, - { name = "imagesize" }, - { name = "jinja2" }, - { name = "packaging" }, - { name = "pygments" }, - { name = "requests" }, - { name = "roman-numerals" }, - { name = "snowballstemmer" }, - { name = "sphinxcontrib-applehelp" }, - { name = "sphinxcontrib-devhelp" }, - { name = "sphinxcontrib-htmlhelp" }, - { name = "sphinxcontrib-jsmath" }, - { name = "sphinxcontrib-qthelp" }, - { name = "sphinxcontrib-serializinghtml" }, + { name = "alabaster", marker = "python_full_version < '3.12'" }, + { name = "babel", marker = "python_full_version < '3.12'" }, + { name = "colorama", marker = "python_full_version < '3.12' and sys_platform == 'win32'" }, + { name = "docutils", marker = "python_full_version < '3.12'" }, + { name = "imagesize", marker = "python_full_version < '3.12'" }, + { name = "jinja2", marker = "python_full_version < '3.12'" }, + { name = "packaging", marker = "python_full_version < '3.12'" }, + { name = "pygments", marker = "python_full_version < '3.12'" }, + { name = "requests", marker = "python_full_version < '3.12'" }, + { name = "roman-numerals", marker = "python_full_version < '3.12'" }, + { name = "snowballstemmer", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/50/a8c6ccc36d5eacdfd7913ddccd15a9cee03ecafc5ee2bc40e1f168d85022/sphinx-9.0.4.tar.gz", hash = "sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3", size = 8710502, upload-time = "2025-12-04T07:45:27.343Z" } wheels = [ @@ -2352,23 +2351,23 @@ resolution-markers = [ "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "alabaster" }, - { name = "babel" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "docutils" }, - { name = "imagesize" }, - { name = "jinja2" }, - { name = "packaging" }, - { name = "pygments" }, - { name = "requests" }, - { name = "roman-numerals" }, - { name = "snowballstemmer" }, - { name = "sphinxcontrib-applehelp" }, - { name = "sphinxcontrib-devhelp" }, - { name = "sphinxcontrib-htmlhelp" }, - { name = "sphinxcontrib-jsmath" }, - { name = "sphinxcontrib-qthelp" }, - { name = "sphinxcontrib-serializinghtml" }, + { name = "alabaster", marker = "python_full_version >= '3.12'" }, + { name = "babel", marker = "python_full_version >= '3.12'" }, + { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, + { name = "docutils", marker = "python_full_version >= '3.12'" }, + { name = "imagesize", marker = "python_full_version >= '3.12'" }, + { name = "jinja2", marker = "python_full_version >= '3.12'" }, + { name = "packaging", marker = "python_full_version >= '3.12'" }, + { name = "pygments", marker = "python_full_version >= '3.12'" }, + { name = "requests", marker = "python_full_version >= '3.12'" }, + { name = "roman-numerals", marker = "python_full_version >= '3.12'" }, + { name = "snowballstemmer", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } wheels = [ @@ -2444,38 +2443,8 @@ wheels = [ [[package]] name = "spm-calculator" -version = "0.2.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", -] -dependencies = [ - { name = "census" }, - { name = "numpy" }, - { name = "openpyxl" }, - { name = "pandas" }, - { name = "requests" }, - { name = "us" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5e/10/74712a053ec0df47a38103541bb682c1465da5b5447790b623d88cffa482/spm_calculator-0.2.0.tar.gz", hash = "sha256:b8e37548a7556a8e22ffe4eb2a15c59e9167414df1f26a0b7ff6678d11749281", size = 59591, upload-time = "2026-04-16T22:21:36.768Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/22/69efed64ce1d061e0e6a0aeac9f75439332bfd083b59606e97798f9e043e/spm_calculator-0.2.0-py3-none-any.whl", hash = "sha256:bda985bfa9fcc278b7a8bf65e1905e6d000b85b731447d3fc78f23b623dc4f0e", size = 52147, upload-time = "2026-04-16T22:21:35.831Z" }, -] - -[[package]] -name = "spm-calculator" -version = "0.3.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", - "python_full_version < '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version < '3.12' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", -] +version = "0.5.0" +source = { git = "https://github.com/PolicyEngine/spm-calculator?rev=9e6ae4798458771231613b994df2345dd1685214#9e6ae4798458771231613b994df2345dd1685214" } dependencies = [ { name = "census" }, { name = "numpy" }, @@ -2484,10 +2453,6 @@ dependencies = [ { name = "requests" }, { name = "us" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/54/3b/b805c7e3e18c5b5c00f61b60112f9690d084c910e2481bc020f35390d8fd/spm_calculator-0.3.1.tar.gz", hash = "sha256:41f2f4d00d8c03422a7d57b800052e7760b88e463a5884802f83ed58d35c18c1", size = 75945, upload-time = "2026-04-17T19:52:39.707Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8e/1b/29f705f8a96fc7f55f2c07dfcddbbae78efdc6f174d25d4a0560fc3f5cf9/spm_calculator-0.3.1-py3-none-any.whl", hash = "sha256:52c57ecc5a240ec941b0f2b0d93bc4fa437ef6250e233baed8e11916fa9c1150", size = 57826, upload-time = "2026-04-17T19:52:38.444Z" }, -] [[package]] name = "stack-data"