Skip to content

Commit 1192d98

Browse files
authored
Merge pull request #165 from PolicyEngine/nikhilwoodruff/issue163
Target itemized deduction tax expenditures
2 parents d99ba7d + b279d5a commit 1192d98

File tree

4 files changed

+106
-46
lines changed

4 files changed

+106
-46
lines changed

policyengine_us_data/datasets/cps/cps.py

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -242,29 +242,6 @@ def add_personal_variables(cps: h5py.File, person: DataFrame) -> None:
242242
]
243243
cps["is_disabled"] = (person[DISABILITY_FLAGS] == 1).any(axis=1)
244244

245-
def _assign_some_newborns_to_pregnancy(
246-
age: pd.Series, person: pd.DataFrame
247-
) -> pd.Series:
248-
"""Takes an array of ages, returns the new age array with the given percentage of newborns assigned a negative age (in pregnancy)."""
249-
age = np.where(
250-
person.A_AGE == 0,
251-
np.where(
252-
np.random.randint(
253-
0, 2, len(person)
254-
), # Random number of 0 or 1
255-
# If 1 is flipped, select a random number between -0.75 and 0
256-
# This will represent the pregnany month
257-
# At -0.75 the pregnancy month is 0 and at -0.0001 the pregnancy month is 9
258-
np.random.uniform(-0.75, 0, len(person)),
259-
# If 0 is flipped, the child is a newborn at the age of 0 to 1
260-
np.random.uniform(0, 1, len(person)),
261-
),
262-
person.A_AGE,
263-
)
264-
return age
265-
266-
cps["age"] = _assign_some_newborns_to_pregnancy(cps["age"], person)
267-
268245
def children_per_parent(col: str) -> pd.DataFrame:
269246
"""Calculate number of children in the household using parental
270247
pointers.

policyengine_us_data/tests/test_datasets/test_enhanced_cps.py

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -36,21 +36,46 @@ def test_ecps_has_mortgage_interest():
3636
sim = Microsimulation(dataset=EnhancedCPS_2024)
3737

3838
assert sim.calculate("deductible_mortgage_interest").sum() > 1
39-
assert sim.calculate("deductible_interest_expense").sum() > 1
4039

4140

42-
def test_newborns_and_pregnancies():
43-
from policyengine_us_data.datasets.cps import EnhancedCPS_2024
41+
def test_ecps_replicates_jct_tax_expenditures():
4442
from policyengine_us import Microsimulation
43+
from policyengine_core.reforms import Reform
44+
from policyengine_us_data.datasets import EnhancedCPS_2024
45+
46+
# JCT tax expenditure targets
47+
EXPENDITURE_TARGETS = {
48+
"salt_deduction": 21.247e9,
49+
"medical_expense_deduction": 11.4e9,
50+
"charitable_deduction": 65.301e9,
51+
"interest_deduction": 24.8e9,
52+
}
4553

46-
sim = Microsimulation(dataset=EnhancedCPS_2024)
47-
48-
# Test for unborn children (age < 0)
49-
unborn = sim.calculate("age") < 0
50-
unborn_count = unborn.sum()
51-
assert unborn_count > 0
52-
53-
# Test for newborns (0 <= age < 1)
54-
newborns = (sim.calculate("age") >= 0) & (sim.calculate("age") < 1)
55-
newborn_count = newborns.sum()
56-
assert newborn_count > 0
54+
baseline = Microsimulation(dataset=EnhancedCPS_2024)
55+
income_tax_b = baseline.calculate(
56+
"income_tax", period=2024, map_to="household"
57+
)
58+
59+
for deduction, target in EXPENDITURE_TARGETS.items():
60+
# Create reform that neutralizes the deduction
61+
class RepealDeduction(Reform):
62+
def apply(self):
63+
self.neutralize_variable(deduction)
64+
65+
# Run reform simulation
66+
reformed = Microsimulation(
67+
reform=RepealDeduction, dataset=EnhancedCPS_2024
68+
)
69+
income_tax_r = reformed.calculate(
70+
"income_tax", period=2024, map_to="household"
71+
)
72+
73+
# Calculate tax expenditure
74+
tax_expenditure = (income_tax_r - income_tax_b).sum()
75+
pct_error = abs((tax_expenditure - target) / target)
76+
TOLERANCE = 0.15
77+
78+
print(
79+
f"{deduction} tax expenditure {tax_expenditure/1e9:.1f}bn differs from target {target/1e9:.1f}bn by {pct_error:.2%}"
80+
)
81+
assert pct_error < TOLERANCE, deduction

policyengine_us_data/utils/loss.py

Lines changed: 66 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from .soi import pe_to_soi, get_soi
33
import numpy as np
44
from policyengine_us_data.storage import STORAGE_FOLDER
5+
from policyengine_core.reforms import Reform
56

67

78
def fmt(x):
@@ -132,6 +133,7 @@ def build_loss_matrix(dataset: type, time_period):
132133
from policyengine_us import Microsimulation
133134

134135
sim = Microsimulation(dataset=dataset)
136+
sim.default_calculation_period = time_period
135137
hh_id = sim.calculate("household_id", map_to="person")
136138
tax_unit_hh_id = sim.map_result(
137139
hh_id, "person", "tax_unit", how="value_from_first_person"
@@ -252,7 +254,7 @@ def build_loss_matrix(dataset: type, time_period):
252254
"alimony_income": 13e9,
253255
"alimony_expense": 13e9,
254256
# Rough estimate, not CPS derived
255-
"real_estate_taxes": 400e9, # Rough estimate between 350bn and 600bn total property tax collections
257+
"real_estate_taxes": 500e9, # Rough estimate between 350bn and 600bn total property tax collections
256258
"rent": 735e9, # ACS total uprated by CPI
257259
}
258260

@@ -340,18 +342,22 @@ def build_loss_matrix(dataset: type, time_period):
340342
)
341343
targets_array.append(row["population_under_5"])
342344

343-
# Population by number of newborns and pregancies
344-
345345
age = sim.calculate("age").values
346346
infants = (age >= 0) & (age < 1)
347347
label = "census/infants"
348348
loss_matrix[label] = sim.map_result(infants, "person", "household")
349-
targets_array.append(3_491_679)
349+
# Total number of infants in the 1 Year ACS
350+
INFANTS_2023 = 3_491_679
351+
INFANTS_2022 = 3_437_933
352+
# Assume infant population grows at the same rate from 2023.
353+
infants_2024 = INFANTS_2023 * (INFANTS_2023 / INFANTS_2022)
354+
targets_array.append(infants_2024)
355+
356+
# SALT tax expenditure targeting
350357

351-
pregnancies = (age >= -0.75) & (age < 0)
352-
label = "census/pregnancies"
353-
loss_matrix[label] = sim.map_result(pregnancies, "person", "household")
354-
targets_array.append(2_618_759)
358+
_add_tax_expenditure_targets(
359+
dataset, time_period, sim, loss_matrix, targets_array
360+
)
355361

356362
if any(loss_matrix.isna().sum() > 0):
357363
raise ValueError("Some targets are missing from the loss matrix")
@@ -360,3 +366,55 @@ def build_loss_matrix(dataset: type, time_period):
360366
raise ValueError("Some targets are missing from the targets array")
361367

362368
return loss_matrix, np.array(targets_array)
369+
370+
371+
def _add_tax_expenditure_targets(
372+
dataset,
373+
time_period,
374+
baseline_simulation,
375+
loss_matrix: pd.DataFrame,
376+
targets_array: list,
377+
):
378+
from policyengine_us import Microsimulation
379+
380+
income_tax_b = baseline_simulation.calculate(
381+
"income_tax", map_to="household"
382+
).values
383+
384+
# Dictionary of itemized deductions and their target values
385+
# (in billions for 2024, per the 2024 JCT Tax Expenditures report)
386+
# https://www.jct.gov/publications/2024/jcx-48-24/
387+
ITEMIZED_DEDUCTIONS = {
388+
"salt_deduction": 21.247e9,
389+
"medical_expense_deduction": 11.4e9,
390+
"charitable_deduction": 65.301e9,
391+
"interest_deduction": 24.8e9,
392+
}
393+
394+
def make_repeal_class(deduction_var):
395+
# Create a custom Reform subclass that neutralizes the given deduction.
396+
class RepealDeduction(Reform):
397+
def apply(self):
398+
self.neutralize_variable(deduction_var)
399+
400+
return RepealDeduction
401+
402+
for deduction, target in ITEMIZED_DEDUCTIONS.items():
403+
# Generate the custom repeal class for the current deduction.
404+
RepealDeduction = make_repeal_class(deduction)
405+
406+
# Run the microsimulation using the repeal reform.
407+
simulation = Microsimulation(dataset=dataset, reform=RepealDeduction)
408+
simulation.default_calculation_period = time_period
409+
410+
# Calculate the baseline and reform income tax values.
411+
income_tax_r = simulation.calculate(
412+
"income_tax", map_to="household"
413+
).values
414+
415+
# Compute the tax expenditure (TE) values.
416+
te_values = income_tax_r - income_tax_b
417+
418+
# Record the TE difference and the corresponding target value.
419+
loss_matrix[f"jct/{deduction}_expenditure"] = te_values
420+
targets_array.append(target)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ authors = [
1717
license = {file = "LICENSE"}
1818
requires-python = ">=3.10, <3.13.0"
1919
dependencies = [
20-
"policyengine_us",
20+
"policyengine_us>=1.197.0",
2121
"policyengine_core>=3.14.1",
2222
"requests",
2323
"tqdm",

0 commit comments

Comments
 (0)