Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions changelog_entry.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
- bump: minor
changes:
changed:
- title: Optimize DC married separate-combined deduction allocation
description: DC married couples filing separately on the same return can now benefit from optimal deduction allocation to the higher-earning spouse, reducing combined tax liability per DC Schedule S Calculation J Line F.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
- name: dc_deduction_indiv unit test 1
- name: dc_deduction_indiv unit test 1 - optimal allocation to higher earner
absolute_error_margin: 0.01
period: 2021
input:
Expand All @@ -24,4 +24,113 @@
members: [person1, person2, person3]
state_code: DC
output:
dc_deduction_indiv: [15_000, 5_000, 0]
# All deductions allocated to higher earner (person1) per DC Schedule S Line F
dc_deduction_indiv: [20_000, 0, 0]

- name: dc_deduction_indiv unit test 2 - spouse is higher earner
absolute_error_margin: 0.01
period: 2021
input:
people:
person1:
is_tax_unit_head: true
dc_agi: 50_000
person2:
is_tax_unit_spouse: true
dc_agi: 150_000
tax_units:
tax_unit:
members: [person1, person2]
tax_unit_itemizes: false
dc_deduction_joint: 25_000
spm_units:
spm_unit:
members: [person1, person2]
households:
household:
members: [person1, person2]
state_code: DC
output:
# All deductions allocated to higher earner (person2/spouse)
dc_deduction_indiv: [0, 25_000]

- name: dc_deduction_indiv unit test 3 - equal AGI splits equally
absolute_error_margin: 0.01
period: 2021
input:
people:
person1:
is_tax_unit_head: true
dc_agi: 100_000
person2:
is_tax_unit_spouse: true
dc_agi: 100_000
tax_units:
tax_unit:
members: [person1, person2]
tax_unit_itemizes: false
dc_deduction_joint: 30_000
spm_units:
spm_unit:
members: [person1, person2]
households:
household:
members: [person1, person2]
state_code: DC
output:
# Equal AGI: income gap is 0, so all deductions split equally
dc_deduction_indiv: [15_000, 15_000]

- name: dc_deduction_indiv unit test 4 - partial equalization
absolute_error_margin: 0.01
period: 2021
input:
people:
person1:
is_tax_unit_head: true
dc_agi: 120_000
person2:
is_tax_unit_spouse: true
dc_agi: 100_000
tax_units:
tax_unit:
members: [person1, person2]
tax_unit_itemizes: false
dc_deduction_joint: 30_000
spm_units:
spm_unit:
members: [person1, person2]
households:
household:
members: [person1, person2]
state_code: DC
output:
# Income gap = 20k, deduction = 30k
# Capped at gap: 20k to head, remaining (30k-20k)/2 = 5k each
# Head: 20k + 5k = 25k, Spouse: 5k
dc_deduction_indiv: [25_000, 5_000]

- name: dc_deduction_indiv unit test 5 - single filer
absolute_error_margin: 0.01
period: 2021
input:
people:
person1:
is_tax_unit_head: true
dc_agi: 80_000
tax_units:
tax_unit:
members: [person1]
tax_unit_itemizes: false
dc_deduction_joint: 15_000
spm_units:
spm_unit:
members: [person1]
households:
household:
members: [person1]
state_code: DC
output:
# Single filer: income gap = 80k, deduction = 15k < gap
# All deduction goes to head
dc_deduction_indiv: [15_000]
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
- name: DC married separate-combined - optimal deduction allocation
period: 2024
absolute_error_margin: 1
input:
people:
person1:
age: 74
employment_income: 32_665
taxable_interest_income: 2_865
taxable_pension_income: 6_300
social_security: 19_103
person2:
age: 74
employment_income: 83_570
taxable_interest_income: 2_865
taxable_pension_income: 6_299
social_security: 19_102
tax_units:
tax_unit:
members: [person1, person2]
spm_units:
spm_unit:
members: [person1, person2]
households:
household:
members: [person1, person2]
state_fips: 11 # DC
output:
# With optimal deduction allocation to higher earner (person2)
# All $32,300 standard deduction goes to the spouse with higher AGI
dc_deduction_indiv: [0, 32_300]
# Files separately when separate filing reduces combined tax
dc_files_separately: true
# Tax is approximately $5,856 (lower than TAXSIM's $6,098 due to
# differences in social security taxation calculations)
dc_income_tax: 5_856
Original file line number Diff line number Diff line change
Expand Up @@ -150,4 +150,6 @@
members: [person1, person2]
state_fips: 11 # DC
output: # as expected from above two test results
cliff_gap: [486.5, 470.47]
# Second value updated after dc_deduction_indiv optimization
# (PR #7297 allocates all deductions to higher earner)
cliff_gap: [486.5, 461.5]
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,27 @@ class dc_deduction_indiv(Variable):
unit = USD
definition_period = YEAR
reference = (
"https://otr.cfo.dc.gov/sites/default/files/dc/sites/otr/publication/attachments/52926_D-40_12.21.21_Final_Rev011122.pdf#page=44"
"https://otr.cfo.dc.gov/sites/default/files/dc/sites/otr/publication/attachments/2022_D-40_Booklet_Final_blk_01_23_23_Ordc.pdf#page=44"
"https://otr.cfo.dc.gov/sites/default/files/dc/sites/otr/publication/attachments/52926_D-40_12.21.21_Final_Rev011122.pdf#page=44",
"https://otr.cfo.dc.gov/sites/default/files/dc/sites/otr/publication/attachments/2022_D-40_Booklet_Final_blk_01_23_23_Ordc.pdf#page=44",
)
defined_for = StateCode.DC

def formula(person, period, parameters):
tax_unit_deduction = person.tax_unit("dc_deduction_joint", period)
# The above references say the following:
# "You may allocate this [tax-unit deduction] amount as you wish."
# Here we allocate in proportion to head and spouse DC AGI
# Per DC Schedule S, Calculation J, Line F:
# "You may allocate this amount as you wish."
# Optimal strategy: equalize taxable income across spouses
# to minimize combined tax under progressive rates.
person_agi = person("dc_agi", period)
tax_unit_agi = person.tax_unit.sum(person_agi)
share = np.zeros_like(tax_unit_agi)
mask = tax_unit_agi > 0
share[mask] = person_agi[mask] / tax_unit_agi[mask]
return share * tax_unit_deduction
head = person("is_tax_unit_head", period)
spouse = person("is_tax_unit_spouse", period)
head_agi = person.tax_unit.sum(person_agi * head)
spouse_agi = person.tax_unit.sum(person_agi * spouse)
income_difference = np.abs(head_agi - spouse_agi)
# Give up to the income gap to the higher earner
capped = min_(income_difference, tax_unit_deduction)
higher_earner = where(head_agi >= spouse_agi, head, spouse)
# Split the remainder equally
remaining = (tax_unit_deduction - capped) / 2
head_or_spouse = head | spouse
return capped * higher_earner + remaining * head_or_spouse