File: aca_utils/ACA_utils.py
#!/usr/bin/python3
"""
=====================================================================================
Compute ACA (a.k.a. "obamacare") health insurance premium tax credit
from income, number people, actual and benchmark plan premiums, IRS
povertylevel%=>contribution% mappings, and HHS poverty levels.
Main entry point:
credit = ACApremiumTaxCredit(income, numpeople, actualpremium, benchmarkpremium)
netpremium = actualpremium - credit
Used to create functions in Excel for future year projections -- either as
a prototype for translation to VBA, or usable directly with a plug-in like
PyXLL or DataNitro. Requires inflation of some parematers in this role, not
shown here. (Personally, the ACA law today means a 50% premiums increase with
no offsetting tax credit, but later years may offset, and possibly eclipse,
some of this cost increase.)
Sources and resources:
An online credit calculator -- http://www.wahbexchange.org/;
IRS docs -- http://www.gpo.gov/fdsys/pkg/FR-2012-05-23/pdf/2012-12421.pdf;
IRS docs -- http://www.gpo.gov/fdsys/pkg/FR-2013-05-03/pdf/2013-10463.pdf;
HHS poverty bases -- http://aspe.hhs.gov/poverty/13poverty.cfm#guidelines;
An overview -- http://www.cbpp.org/files/QA-on-Premium-Credits.pdf;
An example -- http://consumersunion.org/wp-content/uploads/2013/05/Tax_Credit_Worksheet_2014.pdf
Note: you should not take the results of this code as gospel (and should
not use them for your taxes!); these are ballpark calculations only
which may differ slightly from IRS final procedures, and are used for
only rough estimation purposes in spreadsheet yearly projections.
=====================================================================================
"""
trace = print # or: lambda *args: None
#####################################################################################
# poverty percent => pay percent (per IRS)
#####################################################################################
#
# Per IRS, for mapping income to insuree maximum premium contribution:
# (Income/poverty)% low..high => MaxContributionIncome% low..high
#
IRSpovertyToContribRanges = [
[(0, 133), (2.0, 2.0) ], # povlow%..povhigh%, paylow%..payhigh%
[(133, 150), (3.0, 4.0) ], # should this be 2.0..4.0?: not in IRS doc
[(150, 200), (4.0, 6.3) ],
[(200, 250), (6.3, 8.05)],
[(250, 300), (8.05, 9.5) ],
[(300, 400), (9.5, 9.5)]] # inclusive at 400, but not for 400.0001
def mapRanges(povpct, povlow, povhigh, paylow, payhigh): # 135, (133..150), (3.0..4.0)
"""
Evenly map povpct in [povlow..povhigh] to [paylow..payhigh].
All aruments and return value are scaled percentages (*100).
See # comments to the right for an example's expected calcs.
This scheme may or may not match the final IRS technique.
"""
povrange = povhigh - povlow # 17 = 150 - 133
payrange = payhigh - paylow # 1.0 = 4.0 - 3.0
povincr = povpct - povlow # 2 = 135 - 133
pctincr = povincr / povrange # pct = 2 / 17
paypct = paylow + (pctincr * payrange) # 3.0 + (pct * 1.0)
return round(paypct, 2) # per IRS: round to nearest 100th
def test_mapRanges():
for [(povlow, povhigh), (paylow, payhigh)] in IRSpovertyToContribRanges:
for povpct in range(povlow, povhigh):
paypct = mapRanges(povpct, povlow, povhigh, paylow, payhigh)
print(povpct, '=>', paypct)
print('-' * 40)
assert mapRanges(210, 200, 250, 6.3, 8.05) == 6.65 # per IRS doc example
assert mapRanges(135, 133, 150, 3.0, 4.0) == 3.12 # original dev example
assert mapRanges(150, 150, 200, 4.0, 6.3) == 4.0
assert mapRanges(200, 150, 200, 4.0, 6.3) == 6.3
assert mapRanges(300, 300, 400, 9.5, 9.5) == 9.5
assert mapRanges(400, 300, 400, 9.5, 9.5) == 9.5
#####################################################################################
# income => poverty percent (per HHS)
#####################################################################################
def povertyPercent(income, numpeople):
"""
Result is a percent * 100.
Calculate poverty level base from income and size of household.
This can change per year, and may or may not reflect inflation.
"""
HHSpovertyLevels = {
1: 11490, # or index a list[numpeople-1]
2: 15510, # 15510 = 62040 / 4
3: 19530, # 11490 = 45960 / 4
4: 23550,
5: 27570,
6: 31590, # VB: must inflate levels?
7: 35610,
8: 39630}
if numpeople in HHSpovertyLevels:
povlevel = HHSpovertyLevels[numpeople]
else:
povlevel = HHSpovertyLevels[8] + (4020 * (numpeople - 8))
return (income / povlevel) * 100
testincomes = (100000,
62039, 62040, 62041, # +$1 income = $0 credit threshhold for 2 ppl!
45959, 45960, 45961, # +$1 income = $0 credit thresshold for 1 ppl!
22980, 52988, # see asserts ahead
40000, 31021, 31020, 20000, 10000)
def test_povertyPercent():
print('=' * 40)
for numpeople in (2, 1):
for income in testincomes:
print(numpeople, income, '=>', povertyPercent(income, numpeople))
print('=' * 40)
#####################################################################################
# income => taxcredit (combine tools)
#####################################################################################
def applyContribRanges(povpct):
"""
Calculate max premium contribution % from poverty %.
Result and inputs are both scaled percents (* 100).
"""
lenofranges = len(IRSpovertyToContribRanges)
countranges = enumerate(IRSpovertyToContribRanges)
for row, [(povlow, povhigh), (paylow, payhigh)] in countranges:
lastrow = (row+1 == lenofranges)
if lastrow:
inrange = (povlow <= povpct <= povhigh) # VB: x <= y and y <= z
else:
inrange = (povlow <= povpct < povhigh)
if inrange:
paypct = mapRanges(povpct, povlow, povhigh, paylow, payhigh)
trace('(%.2f => %.2f)' % (povpct, paypct))
return paypct
return 100 # > high end of ranges: no tax credit offset, pays premium in full
def insureePremiumContribution(income, numpeople):
"""
Calculate insuree's maximum premium contribution $ from
income, poverty levels, and contribution percent ranges.
"""
povertyPct = povertyPercent(income, numpeople)
contribPct = applyContribRanges(povertyPct)
contribAmt = income * (contribPct / 100)
trace('[%s, %s => %.2f, %.2f, %.2f]' %
(income, numpeople, contribPct, contribAmt, contribAmt/12))
return contribAmt
def ACApremiumTaxCredit(income, numpeople, actualpremium, benchmarksilverpremium):
"""
============================================================================
MAIN ENTRY POINT: calculate the premium credit, for Excel formulas.
All values here are give as yearly/annual amounts, not monthly.
Caveats:
--does not handle Medicaid cutoff at 100/133% of poverty line.
--does nothing about uneven monthly amounts or prepayments.
--really computed for -prior- year from payments and old plans.
--does nothing for cost-sharing subsidies for out-of-pockets.
--may differ slightly from IRS due to rounding errors.
--some aspects very per location but are ignored or givens here.
Not taxcredit = max(0, (yourpremium - contribution(income, numpeople)):
Raw tax credit is difference between the benchmark silver plan's
premium for area and the insurees's maximum contribution calculated
from income and family size (not insuree premium - contribution).
This credit can then be applied to actual premiums regardless of plan
(and may hence decrease or increase actual insuree premium contribution),
but is capped at the total actual plan cost paid. Thus, calculating tax
credits and plan net cost requires income details, plus two plan premiums
for your area per year: insuree's actual, and "benchmark" silver plan,
in addition to each year's expected poverty line data.
There are 6 permutations of the ap, bp, and yc premium and contibution
variables, only 2 of which (plus their equality cases) are truly valid:
[ap >= bp >= c] (ex:gold>silver) and [bp >= ap >= c] (ex:bronze<silver).
============================================================================
Per final? IRS docs...
§ 1.36B–3 Computing the premium assistance credit amount.
a) In general. A taxpayer’s premium assistance credit amount for a taxable
year is the sum of the premium assistance amounts determined under
paragraph (d) of this section for all coverage months for individuals in the
taxpayer’s family.
...
(d) ***Premium assistance amount.
The premium assistance amount for a coverage month is the lesser of—
(1) The premiums for the month for one or more qualified health plans in
which a taxpayer or a member of the taxpayer’s family enrolls; or
(2) The excess of the adjusted monthly premium for the applicable
benchmark plan over 1/12 of the product of a taxpayer’s household
income and the applicable percentage for the taxable year.
...
(f) Applicable benchmark plan—(1) In general. Except as otherwise provided
in this paragraph (f), the applicable benchmark plan for each coverage
month is the second lowest cost silver plan ... offered through the Exchange
for the rating area where the taxpayer resides ...
============================================================================
"""
contributionbase = insureePremiumContribution(income, numpeople)
benchmarkexcess = max(0, benchmarksilverpremium - contributionbase)
taxcredit = min(actualpremium, benchmarkexcess)
netpremium = actualpremium - taxcredit # VB: must inflate premiums
return taxcredit # VB: netpremium not returned
def test_ACApremiumTaxCredit():
"""
Actual insuree and benchmark annual premiums will be taken
from spreadsheet table cells in Excel (and possibly inflated
for future years): hardcode/estimate here.
"""
examplepremiums = (431, 816)
for numpeople in (2, 1):
benchprem = examplepremiums[numpeople-1] * 12 # 2nd lowest silver for area
actualprem = benchprem - (100 * 12) # bronze est: $100/mo < silver
print('-' * 79)
for income in testincomes:
print('ppl=%d, inc=%d' % (numpeople, income))
yrtaxcredit = ACApremiumTaxCredit(income, numpeople, actualprem, benchprem)
yrnetpremium = actualprem - yrtaxcredit
print('** [Month: %d (prem) = %.2f (tax) + %.2f (you)] [Year: %d=%d+%d]\n' %
(actualprem/12, yrtaxcredit/12, yrnetpremium/12,
actualprem, yrtaxcredit, yrnetpremium))
global trace
trace = lambda *args: None
assert round(ACApremiumTaxCredit(22980,1,5000,5000), 2) == 3552.26 # actual = benchmark, 200% pov
assert round(ACApremiumTaxCredit(22980,1,4500,5000), 2) == 3552.26 # actual < benchmark
assert round(ACApremiumTaxCredit(22980,1,3500,5000), 2) == 3500.00
assert round(ACApremiumTaxCredit(22980,1,1000,5000), 2) == 1000.00
assert round(ACApremiumTaxCredit(22980,1,6000,5000), 2) == 3552.26 # actual > benchmark
assert round(ACApremiumTaxCredit(22980,1,10000,5000),2) == 3552.26
assert round(ACApremiumTaxCredit(22980,1,1448,5000), 2) == 1448.00 # actual near contribution
assert round(ACApremiumTaxCredit(22980,1,1447,5000), 2) == 1447.00
assert round(ACApremiumTaxCredit(22980,1,5,5000), 2) == 5.00 # unlikely but true
assert round(ACApremiumTaxCredit(52988,4,15000,15000), 2) == 11195.46 # 225%: 15000=11195+3804
assert round(ACApremiumTaxCredit(62039,2,8592,9792), 2) == 3898.30 # 399.99%: 8592=3898+4693 (716,816)
assert round(ACApremiumTaxCredit(62040,2,8592,9792), 2) == 3898.20 # 400.00%: 8592=3898+4693 (716,816)
assert round(ACApremiumTaxCredit(62041,2,8592,9792), 2) == 0 # MASSIVE $4k DROPOFF FOR $1 INCOME!
assert round(ACApremiumTaxCredit(94200,4,12000,13200), 2) == 4251.00 # 400%: 12k=4251+7749 (354/645)
assert round(ACApremiumTaxCredit(94199,4,12000,13200), 2) == 4251.09 # 400%: ditto
assert round(ACApremiumTaxCredit(94201,4,12000,13200), 2) == 0 # MASSIVE DROPOFF FOR 4: (0/1000)
trace = print
def test_interactive():
"""
Test main function with interactively entered parameters.
"""
print('*' * 80)
while True:
try:
reply = input('[income,people,actprem,benchprem]? ')
if not reply: break
income, numpeople, actprem, benchprem = [int(x) for x in reply.split(',')]
yrtaxcredit = ACApremiumTaxCredit(income, numpeople, actprem, benchprem)
yrnetpremium = actprem - yrtaxcredit
print('Taxcredit => %.2f, Netpremium => %.2f [Monthly credit/premium: %.2f/%.2f]\n' %
(yrtaxcredit, yrnetpremium,
yrtaxcredit / 12, yrnetpremium / 12))
except EOFError:
break
#####################################################################################
# main: self test
#####################################################################################
if __name__ == '__main__':
test_mapRanges()
test_povertyPercent() # comment-out to disable (see also 'trace' setting)
test_ACApremiumTaxCredit()
test_interactive()