|
6 | 6 | 2. Post-published years uprate via PolicyEngine's ``gov.bls.cpi.cpi_u`` |
7 | 7 | parameter. |
8 | 8 | 3. Composition and tenure changes between periods flow through to the |
9 | | - threshold while preserving the unit-specific geographic adjustment |
10 | | - implied by the prior-year stored threshold. |
| 9 | + threshold while applying the unit-specific geographic adjustment. |
11 | 10 | 4. The Betson three-parameter equivalence scale is applied (a 2A2C |
12 | 11 | reference family at the renter national base equals 39430 in 2024). |
13 | 12 | """ |
@@ -170,3 +169,130 @@ def test_formula_preserves_implied_geographic_adjustment(): |
170 | 169 | got = float(sim.calculate("spm_unit_spm_threshold", YEAR)[0]) |
171 | 170 | expected = current_base * equiv * GEOADJ |
172 | 171 | assert got == pytest.approx(expected, rel=1e-5) |
| 172 | + |
| 173 | + |
| 174 | +def test_formula_uses_current_geographic_adjustment_input(): |
| 175 | + """A dataset can provide the geographic adjustment directly without |
| 176 | + materializing an SPM threshold input.""" |
| 177 | + YEAR = 2023 |
| 178 | + GEOADJ = 1.25 |
| 179 | + |
| 180 | + cpi_u = _cpi_u() |
| 181 | + base = _reference_threshold_array( |
| 182 | + np.array([SPMUnitTenureType.RENTER]), |
| 183 | + YEAR, |
| 184 | + cpi_u, |
| 185 | + )[0] |
| 186 | + equiv = spm_equivalence_scale(2, 2) |
| 187 | + |
| 188 | + situation = { |
| 189 | + "people": { |
| 190 | + "a1": {"age": {YEAR: 40}}, |
| 191 | + "a2": {"age": {YEAR: 40}}, |
| 192 | + "k1": {"age": {YEAR: 5}}, |
| 193 | + "k2": {"age": {YEAR: 3}}, |
| 194 | + }, |
| 195 | + "spm_units": { |
| 196 | + "spm_unit": { |
| 197 | + "members": ["a1", "a2", "k1", "k2"], |
| 198 | + "spm_unit_geographic_adjustment": { |
| 199 | + YEAR: GEOADJ, |
| 200 | + }, |
| 201 | + "spm_unit_tenure_type": { |
| 202 | + YEAR: "RENTER", |
| 203 | + }, |
| 204 | + } |
| 205 | + }, |
| 206 | + } |
| 207 | + |
| 208 | + sim = Simulation(situation=situation) |
| 209 | + got = float(sim.calculate("spm_unit_spm_threshold", YEAR)[0]) |
| 210 | + expected = base * equiv * GEOADJ |
| 211 | + assert got == pytest.approx(expected, rel=1e-5) |
| 212 | + |
| 213 | + |
| 214 | +def test_geographic_adjustment_defaults_to_one_without_prior_threshold(): |
| 215 | + """With no direct or implied geographic adjustment, the threshold uses |
| 216 | + the national reference threshold.""" |
| 217 | + YEAR = 2024 |
| 218 | + |
| 219 | + cpi_u = _cpi_u() |
| 220 | + base = _reference_threshold_array( |
| 221 | + np.array([SPMUnitTenureType.RENTER]), |
| 222 | + YEAR, |
| 223 | + cpi_u, |
| 224 | + )[0] |
| 225 | + equiv = spm_equivalence_scale(2, 2) |
| 226 | + |
| 227 | + situation = { |
| 228 | + "people": { |
| 229 | + "a1": {"age": {YEAR: 40}}, |
| 230 | + "a2": {"age": {YEAR: 40}}, |
| 231 | + "k1": {"age": {YEAR: 5}}, |
| 232 | + "k2": {"age": {YEAR: 3}}, |
| 233 | + }, |
| 234 | + "spm_units": { |
| 235 | + "spm_unit": { |
| 236 | + "members": ["a1", "a2", "k1", "k2"], |
| 237 | + "spm_unit_tenure_type": { |
| 238 | + YEAR: "RENTER", |
| 239 | + }, |
| 240 | + } |
| 241 | + }, |
| 242 | + } |
| 243 | + |
| 244 | + sim = Simulation(situation=situation) |
| 245 | + got = float(sim.calculate("spm_unit_spm_threshold", YEAR)[0]) |
| 246 | + expected = base * equiv |
| 247 | + assert got == pytest.approx(expected, rel=1e-5) |
| 248 | + |
| 249 | + |
| 250 | +def test_current_geographic_adjustment_input_overrides_prior_implied_value(): |
| 251 | + """Current-period geographic adjustment inputs should take precedence |
| 252 | + over a prior-year threshold used for backward compatibility.""" |
| 253 | + YEAR = 2026 |
| 254 | + PRIOR = 2025 |
| 255 | + PRIOR_GEOADJ = 1.5 |
| 256 | + CURRENT_GEOADJ = 1.1 |
| 257 | + |
| 258 | + cpi_u = _cpi_u() |
| 259 | + prior_base = _reference_threshold_array( |
| 260 | + np.array([SPMUnitTenureType.RENTER]), |
| 261 | + PRIOR, |
| 262 | + cpi_u, |
| 263 | + )[0] |
| 264 | + current_base = _reference_threshold_array( |
| 265 | + np.array([SPMUnitTenureType.RENTER]), |
| 266 | + YEAR, |
| 267 | + cpi_u, |
| 268 | + )[0] |
| 269 | + equiv = spm_equivalence_scale(2, 2) |
| 270 | + |
| 271 | + situation = { |
| 272 | + "people": { |
| 273 | + "a1": {"age": {PRIOR: 40, YEAR: 41}}, |
| 274 | + "a2": {"age": {PRIOR: 40, YEAR: 41}}, |
| 275 | + "k1": {"age": {PRIOR: 5, YEAR: 6}}, |
| 276 | + "k2": {"age": {PRIOR: 3, YEAR: 4}}, |
| 277 | + }, |
| 278 | + "spm_units": { |
| 279 | + "spm_unit": { |
| 280 | + "members": ["a1", "a2", "k1", "k2"], |
| 281 | + "spm_unit_spm_threshold": { |
| 282 | + PRIOR: float(prior_base * equiv * PRIOR_GEOADJ), |
| 283 | + }, |
| 284 | + "spm_unit_geographic_adjustment": { |
| 285 | + YEAR: CURRENT_GEOADJ, |
| 286 | + }, |
| 287 | + "spm_unit_tenure_type": { |
| 288 | + PRIOR: "RENTER", |
| 289 | + YEAR: "RENTER", |
| 290 | + }, |
| 291 | + } |
| 292 | + }, |
| 293 | + } |
| 294 | + |
| 295 | + sim = Simulation(situation=situation) |
| 296 | + got = float(sim.calculate("spm_unit_spm_threshold", YEAR)[0]) |
| 297 | + expected = current_base * equiv * CURRENT_GEOADJ |
| 298 | + assert got == pytest.approx(expected, rel=1e-5) |
0 commit comments