diff --git a/changelog.d/fix-cbo-agi-2016-2018-soi.fixed.md b/changelog.d/fix-cbo-agi-2016-2018-soi.fixed.md new file mode 100644 index 00000000000..ccdb108eae9 --- /dev/null +++ b/changelog.d/fix-cbo-agi-2016-2018-soi.fixed.md @@ -0,0 +1 @@ +Fill total AGI in calibration.gov.cbo.income_by_source.adjusted_gross_income for 2015-2018 with IRS Statistics of Income actuals. The 2015 value was tax-year 2013 AGI excluding dependent filers and was held flat through 2018, so dollar inputs on the default uprating series (those with no series of their own, such as charitable donations) dated 2018 or earlier came out 13% to 29% too high when uprated to 2019 or later, and up to 12% too low when uprated to a year within 2016-2018. Medicare IRMAA's two-year lookback deflates income by the same per-capita index, so a household entered for a single benefit year from 2016 to 2020 now gets that year's income times 1.007, 0.942, 0.890, 0.932 or 0.929, instead of 1.009, 1.016, 1.014, 0.765 or 0.721. diff --git a/policyengine_us/parameters/calibration/gov/cbo/income_by_source.yaml b/policyengine_us/parameters/calibration/gov/cbo/income_by_source.yaml index 74ae47aba21..67939c85410 100644 --- a/policyengine_us/parameters/calibration/gov/cbo/income_by_source.yaml +++ b/policyengine_us/parameters/calibration/gov/cbo/income_by_source.yaml @@ -1,4 +1,4 @@ -description: Historical data and projections of detailed adjusted gross income (AGI) by source (2019-2036) +description: Historical data and projections of detailed adjusted gross income (AGI) by source from CBO (2019-2036), with IRS Statistics of Income actuals for total AGI in 2015-2018 metadata: propagate_metadata_to_children: true unit: currency-USD @@ -19,12 +19,33 @@ adjusted_gross_income: metadata: label: Total AGI values: + # IRS Statistics of Income actuals, all returns: adjusted gross income + # less deficit. 2015-01-01: - value: 9_033_840_000_000 + value: 10_210_310_102_000 metadata: reference: - - title: Tax Foundation - href: https://taxfoundation.org/data/all/federal/summary-latest-federal-income-tax-data-2015-update/ + - title: IRS SOI Tax Stats - Publication 1304 Table 1.1, tax year 2015, adjusted gross income less deficit + href: https://www.irs.gov/pub/irs-soi/15in11si.xls + 2016-01-01: + value: 10_225_938_479_000 + metadata: + reference: + - title: IRS SOI Tax Stats - Publication 1304 Table 1.1, tax year 2016, adjusted gross income less deficit + href: https://www.irs.gov/pub/irs-soi/16in11si.xls + 2017-01-01: + value: 11_009_900_155_000 + metadata: + reference: + - title: IRS SOI Tax Stats - Publication 1304 Table 1.1, tax year 2017, adjusted gross income less deficit + href: https://www.irs.gov/pub/irs-soi/17in11si.xls + 2018-01-01: + value: 11_643_439_106_000 + metadata: + reference: + - title: IRS SOI Tax Stats - Publication 1304 Table 1.1, tax year 2018, adjusted gross income less deficit + href: https://www.irs.gov/pub/irs-soi/18in11si.xls + # CBO from 2019. 2019-01-01: 11_962_000_000_000 2020-01-01: 12_650_000_000_000 2021-01-01: 14_791_339_000_000 diff --git a/policyengine_us/tests/core/test_uprating_series.py b/policyengine_us/tests/core/test_uprating_series.py index 8fc14e84fb2..4cf30aa13b8 100644 --- a/policyengine_us/tests/core/test_uprating_series.py +++ b/policyengine_us/tests/core/test_uprating_series.py @@ -150,6 +150,26 @@ def test_series_is_defined_across_the_projection_window(series): assert _value(series, year) not in (None, 0) +def test_default_series_has_a_value_for_every_year(): + """A year with no value holds the previous one, so the per-capita index + drifts down with population and then jumps when values resume. CBO AGI + once held a tax-year 2013 total for 2015 and had nothing for 2016 to + 2018. Inputs on the default series dated 2018 came out 29% too high by + 2019, and Medicare IRMAA's lookback, which deflates income by the same + index, put a household entered for 2019 alone at 0.765 of its income + instead of 0.932. With the right 2015 total, the gap alone gives 14% and + 0.864.""" + total = get_parameter(system.parameters, _authored_series(DEFAULT)) + years = sorted( + { + int(value.instant_str[:4]) + for value in total.values_list + if value.instant_str.endswith("-01-01") + } + ) + assert years == list(range(years[0], years[-1] + 1)) + + @pytest.mark.parametrize("year", (2025, 2030, 2035, 2036)) def test_unemployment_compensation_grows_with_cbo_unemployment_outlays(year): soi = "calibration.gov.irs.soi.unemployment_compensation" diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/medicare_irmaa_magi_two_years_prior.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/medicare_irmaa_magi_two_years_prior.yaml index bcda1dddd8a..51a80ec99ef 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/medicare_irmaa_magi_two_years_prior.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/medicare_irmaa_magi_two_years_prior.yaml @@ -2,11 +2,17 @@ # holds no input for that year or any earlier one, the MAGI of the earliest # year with inputs (no earlier than 2015, the first modeled year) stands in, # deflated to the year two years prior by -# calibration.gov.cbo.income_by_source.adjusted_gross_income_per_capita (CBO +# calibration.gov.cbo.income_by_source.adjusted_gross_income_per_capita (total # AGI divided by population). That index starts in 2015 and is held at its -# 2015 value before then: -# 2015: 9,033,840,000,000 / 326,078,000 = 27,704.54 -# 2016: 9,033,840,000,000 / 328,964,000 = 27,461.49 +# 2015 value before then. Total AGI is IRS SOI Publication 1304 Table 1.1, all +# returns, adjusted gross income less deficit, for 2015 to 2018 +# (https://www.irs.gov/pub/irs-soi/15in11si.xls to 18in11si.xls), and CBO's +# series from 2019: +# 2015: 10,210,310,102,000 / 326,078,000 = 31,312.48 +# 2016: 10,225,938,479,000 / 328,964,000 = 31,085.28 +# 2017: 11,009,900,155,000 / 331,211,000 = 33,241.35 +# 2018: 11,643,439,106,000 / 333,525,000 = 34,910.24 +# 2019: 11,962,000,000,000 / 335,329,000 = 35,672.43 # 2022: 14,821,800,000,000 / 339,654,000 = 43,637.94 # 2023: 15,347,400,000,000 / 342,835,000 = 44,766.14 # 2024: 16,685,900,000,000 / 346,588,000 = 48,143.33 @@ -19,7 +25,7 @@ age: 66 employment_income: 500_000 output: - # 500,000 x 27,704.54 (2013, held at 2015) / 27,704.54 (2015). + # 500,000 x 31,312.48 (2013, held at 2015) / 31,312.48 (2015). medicare_irmaa_magi_two_years_prior: 500_000 - name: Case 2, 2016 benefit year with only 2016 inputs uses 2016 income. @@ -29,8 +35,8 @@ age: 66 employment_income: 500_000 output: - # 500,000 x 27,704.54 (2014, held at 2015) / 27,461.49 (2016). - medicare_irmaa_magi_two_years_prior: 504_425.34 + # 500,000 x 31,312.48 (2014, held at 2015) / 31,085.28 (2016). + medicare_irmaa_magi_two_years_prior: 503_654.40 - name: Case 3, 2015 benefit year counts 2013 income that is provided. period: 2015 @@ -87,10 +93,38 @@ is_medicare_eligible: true medicare_enrolled: true output: - # 100,000 x 27,704.54 / 27,461.49, under the first threshold of 103,001. - medicare_irmaa_magi_two_years_prior: 100_885.07 + # 100,000 x 31,312.48 / 31,085.28, under the first threshold of 103,001. + medicare_irmaa_magi_two_years_prior: 100_730.88 income_adjusted_part_d_premium_surcharge: 0 +- name: Case 8, 2018 benefit year with only 2018 inputs deflates 2018 income to 2016. + period: 2018 + # MAGI is stored as float32, so allow for its rounding. + absolute_error_margin: 0.1 + input: + age: 66 + employment_income: 500_000 + output: + # 500,000 x 31,085.28 (2016) / 34,910.24 (2018). Both years are IRS SOI + # totals. When total AGI had no 2016-2018 values, its 2015 value (tax-year + # 2013 AGI excluding dependent filers) was held, so the index fell with + # population (27,461.49 in 2016, 27,085.95 in 2018) and this came to + # 506,932.37. + medicare_irmaa_magi_two_years_prior: 445_217.19 + +- name: Case 9, 2019 benefit year with only 2019 inputs deflates 2019 income to 2017. + period: 2019 + # MAGI is stored as float32, so allow for its rounding. + absolute_error_margin: 0.1 + input: + age: 66 + employment_income: 500_000 + output: + # 500,000 x 33,241.35 (2017, IRS SOI) / 35,672.43 (2019, CBO). When total + # AGI for 2017 held the old 2015 value, the index was 27,275.18 and this + # came to 382,300.59. + medicare_irmaa_magi_two_years_prior: 465_924.93 + - name: A 2025 household with only 2024 inputs has IRMAA set on deflated 2024 income. period: 2025 absolute_error_margin: 0.01 diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/senior_household_2015_2016_integration.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/senior_household_2015_2016_integration.yaml index c940dcd51f4..3f957d71f35 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/senior_household_2015_2016_integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/senior_household_2015_2016_integration.yaml @@ -32,7 +32,7 @@ members: [person] state_code: TX output: - # 10,000 x 27,704.54 (2013, held at 2015) / 27,704.54 (2015). + # 10,000 x 31,312.48 (2013, held at 2015) / 31,312.48 (2015). medicare_irmaa_magi_two_years_prior: 10_000 income_tax: 0 ssi: 4_306 @@ -52,8 +52,9 @@ members: [person] state_code: TX output: - # 10,000 x 27,704.54 (2014, held at 2015) / 27,461.49 (2016). - medicare_irmaa_magi_two_years_prior: 10_088.51 + # 10,000 x 31,312.48 (2014, held at 2015) / 31,085.28 (2016), from IRS + # SOI total AGI over population (see medicare_irmaa_magi_two_years_prior.yaml). + medicare_irmaa_magi_two_years_prior: 10_073.09 income_tax: 0 ssi: 4_306 snap: 192 diff --git a/policyengine_us/tests/policy/baseline/household/expense/charitable/charitable_cash_donations.yaml b/policyengine_us/tests/policy/baseline/household/expense/charitable/charitable_cash_donations.yaml new file mode 100644 index 00000000000..5a967a8396c --- /dev/null +++ b/policyengine_us/tests/policy/baseline/household/expense/charitable/charitable_cash_donations.yaml @@ -0,0 +1,34 @@ +# charitable_cash_donations has no uprating series of its own, so it follows the +# default, calibration.gov.cbo.income_by_source.adjusted_gross_income_per_capita +# (total AGI divided by population). An input dated in one year and read in a +# later year is multiplied by the ratio of that index between the two years. +# Total AGI is IRS SOI Publication 1304 Table 1.1, all returns, adjusted gross +# income less deficit, for 2015 to 2018, and CBO's series from 2019: +# 2015: 10,210,310,102,000 / 326,078,000 = 31,312.48 +# (https://www.irs.gov/pub/irs-soi/15in11si.xls) +# 2018: 11,643,439,106,000 / 333,525,000 = 34,910.24 +# (https://www.irs.gov/pub/irs-soi/18in11si.xls) +# 2019: 11,962,000,000,000 / 335,329,000 = 35,672.43 + +- name: A 2018 donation read in 2019 grows with per-capita AGI across the SOI-to-CBO splice. + period: 2019 + absolute_error_margin: 0.01 + input: + charitable_cash_donations: + 2018: 10_000 + output: + # 10,000 x 35,672.43 / 34,910.24. When total AGI for 2018 held the old 2015 + # value, the index was 27,085.95 and this came to 13,170.09. + charitable_cash_donations: 10_218.33 + +- name: A 2015 donation read in 2018 grows with per-capita AGI within the SOI years. + period: 2018 + absolute_error_margin: 0.01 + input: + charitable_cash_donations: + 2015: 10_000 + output: + # 10,000 x 34,910.24 / 31,312.48. When total AGI held the old 2015 value + # through 2018, the index was 27,704.54 in 2015 and 27,085.95 in 2018, and + # this came to 9,776.72. + charitable_cash_donations: 11_148.99 diff --git a/policyengine_us/variables/gov/hhs/medicare/eligibility/medicare_irmaa_magi_two_years_prior.py b/policyengine_us/variables/gov/hhs/medicare/eligibility/medicare_irmaa_magi_two_years_prior.py index 16fc9d61c1d..e3ac8b03010 100644 --- a/policyengine_us/variables/gov/hhs/medicare/eligibility/medicare_irmaa_magi_two_years_prior.py +++ b/policyengine_us/variables/gov/hhs/medicare/eligibility/medicare_irmaa_magi_two_years_prior.py @@ -7,10 +7,10 @@ TAX_UNIT_ADDITIONS, ) -# Per-capita CBO AGI: the series that extends a single-year dataset's dollar -# inputs to later years when they have no series of their own -# (DEFAULT_MICRODATA_UPRATING, divided by population). Deflating by it runs -# that projection backward. +# Per-capita total AGI (IRS SOI for 2015-2018, CBO from 2019): the series that +# extends a single-year dataset's dollar inputs to later years when they have +# no series of their own (DEFAULT_MICRODATA_UPRATING, divided by population). +# Deflating by it runs that projection backward. LOOKBACK_DEFLATOR = ( "calibration.gov.cbo.income_by_source.adjusted_gross_income_per_capita" ) @@ -59,12 +59,12 @@ class medicare_irmaa_magi_two_years_prior(Variable): "gov.simulation.medicare_irmaa_uses_current_year_income but " "deflated. Provide this value, or inputs for the year two years " "prior, to hold it fixed. " - "CBO AGI has no values for 2016 to 2018 and is held at its 2015 " - "value, so the index drifts down through 2018 and then rises 31% " - "from 2017 to 2019 and 39% from 2018 to 2020: a household entered " - "for 2016, 2017 or 2018 alone gets slightly more than that year's " - "income, and one entered for 2019 or 2020 alone gets that year's " - "income times 0.765 or 0.721. " + "Total AGI in that index is IRS Statistics of Income actuals for " + "2015 to 2018 and CBO's series from 2019. The index begins in 2015 " + "and is held at its 2015 value before then, so a household entered " + "for 2016 alone gets that year's income times the index for 2015 " + "over the index for 2016, slightly more than that income because " + "per-capita AGI fell from 2015 to 2016. " "When inputs reach back to 2013 or 2014 and the year two years prior " "is one of those years, its income cannot be computed: only income " "provided for it counts, and the rest is zero."