taxcalc 4.2.1__py3-none-any.whl → 4.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. taxcalc/__init__.py +1 -1
  2. taxcalc/assumptions/ASSUMPTIONS.md +53 -0
  3. taxcalc/assumptions/README.md +17 -0
  4. taxcalc/assumptions/economic_assumptions_template.json +77 -0
  5. taxcalc/calcfunctions.py +7 -4
  6. taxcalc/data.py +10 -5
  7. taxcalc/policy.py +1 -1
  8. taxcalc/policy_current_law.json +4649 -288
  9. taxcalc/records.py +20 -15
  10. taxcalc/reforms/2017_law.json +125 -0
  11. taxcalc/reforms/2017_law.out.csv +10 -0
  12. taxcalc/reforms/ARPA.json +78 -0
  13. taxcalc/reforms/ARPA.out.csv +10 -0
  14. taxcalc/reforms/BrownKhanna.json +23 -0
  15. taxcalc/reforms/BrownKhanna.out.csv +10 -0
  16. taxcalc/reforms/CARES.json +40 -0
  17. taxcalc/reforms/CARES.out.csv +10 -0
  18. taxcalc/reforms/ConsolidatedAppropriationsAct2021.json +15 -0
  19. taxcalc/reforms/ConsolidatedAppropriationsAct2021.out.csv +10 -0
  20. taxcalc/reforms/Larson2019.json +36 -0
  21. taxcalc/reforms/Larson2019.out.csv +10 -0
  22. taxcalc/reforms/README.md +22 -0
  23. taxcalc/reforms/REFORMS.md +92 -0
  24. taxcalc/reforms/Renacci.json +61 -0
  25. taxcalc/reforms/Renacci.out.csv +10 -0
  26. taxcalc/reforms/SandersDeFazio.json +15 -0
  27. taxcalc/reforms/SandersDeFazio.out.csv +10 -0
  28. taxcalc/reforms/TCJA.json +160 -0
  29. taxcalc/reforms/TCJA.md +48 -0
  30. taxcalc/reforms/TCJA.out.csv +10 -0
  31. taxcalc/reforms/Trump2016.json +71 -0
  32. taxcalc/reforms/Trump2016.out.csv +10 -0
  33. taxcalc/reforms/Trump2017.json +51 -0
  34. taxcalc/reforms/Trump2017.out.csv +10 -0
  35. taxcalc/reforms/archive/Clinton2016.json +56 -0
  36. taxcalc/reforms/archive/RyanBrady.json +104 -0
  37. taxcalc/reforms/archive/TCJA_House.json +144 -0
  38. taxcalc/reforms/archive/TCJA_House_Amended.json +152 -0
  39. taxcalc/reforms/archive/TCJA_Reconciliation.json +187 -0
  40. taxcalc/reforms/archive/TCJA_Senate.json +116 -0
  41. taxcalc/reforms/archive/TCJA_Senate_111417.json +169 -0
  42. taxcalc/reforms/archive/TCJA_Senate_120117.json +174 -0
  43. taxcalc/reforms/cases.csv +10 -0
  44. taxcalc/reforms/clp.out.csv +10 -0
  45. taxcalc/reforms/ext.json +59 -0
  46. taxcalc/reforms/growfactors_ext.csv +65 -0
  47. taxcalc/reforms/ptaxes0.json +37 -0
  48. taxcalc/reforms/ptaxes0.out.csv +10 -0
  49. taxcalc/reforms/ptaxes1.json +21 -0
  50. taxcalc/reforms/ptaxes1.out.csv +10 -0
  51. taxcalc/reforms/ptaxes2.json +18 -0
  52. taxcalc/reforms/ptaxes2.out.csv +10 -0
  53. taxcalc/reforms/ptaxes3.json +28 -0
  54. taxcalc/reforms/ptaxes3.out.csv +10 -0
  55. taxcalc/taxcalcio.py +44 -22
  56. taxcalc/tests/benefits_expect.csv +169 -0
  57. taxcalc/tests/cmpi_cps_expect.txt +132 -0
  58. taxcalc/tests/cmpi_puf_expect.txt +132 -0
  59. taxcalc/tests/conftest.py +143 -0
  60. taxcalc/tests/cpscsv_agg_expect.csv +26 -0
  61. taxcalc/tests/puf_var_correl_coeffs_2016.csv +80 -0
  62. taxcalc/tests/puf_var_wght_means_by_year.csv +80 -0
  63. taxcalc/tests/pufcsv_agg_expect.csv +26 -0
  64. taxcalc/tests/pufcsv_mtr_expect.txt +63 -0
  65. taxcalc/tests/reforms.json +649 -0
  66. taxcalc/tests/reforms_expect.csv +65 -0
  67. taxcalc/tests/test_4package.py +67 -0
  68. taxcalc/tests/test_benefits.py +86 -0
  69. taxcalc/tests/test_calcfunctions.py +871 -0
  70. taxcalc/tests/test_calculator.py +1021 -0
  71. taxcalc/tests/test_compare.py +336 -0
  72. taxcalc/tests/test_compatible_data.py +338 -0
  73. taxcalc/tests/test_consumption.py +144 -0
  74. taxcalc/tests/test_cpscsv.py +163 -0
  75. taxcalc/tests/test_data.py +133 -0
  76. taxcalc/tests/test_decorators.py +332 -0
  77. taxcalc/tests/test_growdiff.py +102 -0
  78. taxcalc/tests/test_growfactors.py +94 -0
  79. taxcalc/tests/test_parameters.py +617 -0
  80. taxcalc/tests/test_policy.py +1557 -0
  81. taxcalc/tests/test_puf_var_stats.py +194 -0
  82. taxcalc/tests/test_pufcsv.py +385 -0
  83. taxcalc/tests/test_records.py +234 -0
  84. taxcalc/tests/test_reforms.py +386 -0
  85. taxcalc/tests/test_responses.py +41 -0
  86. taxcalc/tests/test_taxcalcio.py +755 -0
  87. taxcalc/tests/test_utils.py +792 -0
  88. taxcalc/validation/CSV_INPUT_VARS.md +29 -0
  89. taxcalc/validation/CSV_OUTPUT_VARS.md +63 -0
  90. taxcalc/validation/README.md +68 -0
  91. taxcalc/validation/taxsim35/Differences_Explained.md +54 -0
  92. taxcalc/validation/taxsim35/README.md +139 -0
  93. taxcalc/validation/taxsim35/expected_differences/a17-taxdiffs-expect.csv +25 -0
  94. taxcalc/validation/taxsim35/expected_differences/a18-taxdiffs-expect.csv +25 -0
  95. taxcalc/validation/taxsim35/expected_differences/a19-taxdiffs-expect.csv +25 -0
  96. taxcalc/validation/taxsim35/expected_differences/a20-taxdiffs-expect.csv +25 -0
  97. taxcalc/validation/taxsim35/expected_differences/a21-taxdiffs-expect.csv +25 -0
  98. taxcalc/validation/taxsim35/expected_differences/b17-taxdiffs-expect.csv +25 -0
  99. taxcalc/validation/taxsim35/expected_differences/b18-taxdiffs-expect.csv +25 -0
  100. taxcalc/validation/taxsim35/expected_differences/b19-taxdiffs-expect.csv +25 -0
  101. taxcalc/validation/taxsim35/expected_differences/b20-taxdiffs-expect.csv +25 -0
  102. taxcalc/validation/taxsim35/expected_differences/b21-taxdiffs-expect.csv +25 -0
  103. taxcalc/validation/taxsim35/expected_differences/c17-taxdiffs-expect.csv +25 -0
  104. taxcalc/validation/taxsim35/expected_differences/c18-taxdiffs-expect.csv +25 -0
  105. taxcalc/validation/taxsim35/expected_differences/c19-taxdiffs-expect.csv +25 -0
  106. taxcalc/validation/taxsim35/input_setup.py +67 -0
  107. taxcalc/validation/taxsim35/main_comparison.py +183 -0
  108. taxcalc/validation/taxsim35/prepare_taxcalc_input.py +161 -0
  109. taxcalc/validation/taxsim35/process_taxcalc_output.py +140 -0
  110. taxcalc/validation/taxsim35/taxsim_emulation.json +49 -0
  111. taxcalc/validation/taxsim35/taxsim_input.py +321 -0
  112. taxcalc/validation/taxsim35/tc_sims.py +98 -0
  113. taxcalc/validation/taxsim35/tests_35.py +80 -0
  114. taxcalc/validation/tests_35.sh +13 -0
  115. {taxcalc-4.2.1.dist-info → taxcalc-4.3.0.dist-info}/METADATA +3 -4
  116. taxcalc-4.3.0.dist-info/RECORD +139 -0
  117. {taxcalc-4.2.1.dist-info → taxcalc-4.3.0.dist-info}/WHEEL +1 -1
  118. taxcalc/tmd_growfactors.csv +0 -55
  119. taxcalc/tmd_weights.csv.gz +0 -0
  120. taxcalc-4.2.1.dist-info/RECORD +0 -34
  121. {taxcalc-4.2.1.dist-info → taxcalc-4.3.0.dist-info}/LICENSE +0 -0
  122. {taxcalc-4.2.1.dist-info → taxcalc-4.3.0.dist-info}/entry_points.txt +0 -0
  123. {taxcalc-4.2.1.dist-info → taxcalc-4.3.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,336 @@
1
+ """
2
+ Compares Tax-Calculator PUF and CPS results with historical information.
3
+ """
4
+ # CODING-STYLE CHECKS:
5
+ # pycodestyle test_compare.py
6
+ # pylint --disable=locally-disabled test_compare.py
7
+
8
+ import os
9
+ import pytest
10
+ import numpy as np
11
+ # pylint: disable=import-error,pointless-string-statement
12
+ from taxcalc import Policy, Records, Calculator
13
+ from taxcalc import add_income_table_row_variable, SOI_AGI_BINS
14
+
15
+
16
+ """
17
+ 2015 IRS-SOI amounts by AGI category are from "Table 3.3 All Returns: Tax
18
+ Liability, Tax Credits, and Tax Payments by Size of Adjusted Gross Income,
19
+ Tax Year 2015" which is available as a spreadsheet at this URL:
20
+ <https://www.irs.gov/statistics/soi-tax-stats-individual-
21
+ statistical-tables-by-size-of-adjusted-gross-income>
22
+ The IRS-SOI amounts are from 19 rows in the spreadsheet numbered from
23
+ 11 (AGI under one dollar) through 29 (AGI $10M or more).
24
+ Dollar IRS-SOI amounts are expressed in billions of dollars and rounded
25
+ to the nearest one-tenth of a million dollars.
26
+ """
27
+ ITAX = {
28
+ '0:EITC': {
29
+ # Full earned income credit
30
+ # in 2015 using the IRS-SOI information described above.
31
+ # EITC is column (37) in the Table 3.3 spreadsheet,
32
+ # which is the sum of columns (47), (75) and (97).
33
+ 'SOI': [0.2104,
34
+ 1.1843,
35
+ 7.1562,
36
+ 16.5927,
37
+ 15.8799,
38
+ 11.1025,
39
+ 7.5150,
40
+ 7.4528,
41
+ 1.3936,
42
+ 0.0375,
43
+ 0.0000,
44
+ 0.0000,
45
+ 0.0000,
46
+ 0.0000,
47
+ 0.0000,
48
+ 0.0000,
49
+ 0.0000,
50
+ 0.0000,
51
+ 0.0000],
52
+ 'TC': ['eitc']
53
+ },
54
+
55
+ '1:FCTC': {
56
+ # Full (basic and additional, refundable and nonrefundable) child tax
57
+ # credit in 2015 using the IRS-SOI information described above.
58
+ # FCTC is sum of columns (13) and (39) in the Table 3.3 spreadsheet.
59
+ 'SOI': [0.1301,
60
+ 0.0793,
61
+ 1.4740,
62
+ 4.2580,
63
+ 5.2104,
64
+ 4.6582,
65
+ 4.3166,
66
+ 7.2320,
67
+ 5.2848,
68
+ 9.4151,
69
+ 6.4075,
70
+ 5.2222,
71
+ 0.0018,
72
+ 0.0000,
73
+ 0.0000,
74
+ 0.0000,
75
+ 0.0000,
76
+ 0.0000,
77
+ 0.0000],
78
+ 'TC': ['c07220', # FCTC that is nonrefundable
79
+ 'c11070'] # FCTC that isrefundable
80
+ },
81
+
82
+ '2:NIIT': {
83
+ # Net investment income tax
84
+ # in 2015 using the IRS-SOI information described above.
85
+ # NIIT is column (53) in the Table 3.3 spreadsheet.
86
+ # NIIT is included in Tax-Calculator individual income tax liability.
87
+ 'SOI': [0.0004,
88
+ 0.0000,
89
+ 0.0000,
90
+ 0.0000,
91
+ 0.0000,
92
+ 0.0000,
93
+ 0.0001,
94
+ 0.0000,
95
+ 0.0000,
96
+ 0.0014,
97
+ 0.0005,
98
+ 0.0213,
99
+ 2.6397,
100
+ 3.1356,
101
+ 1.6715,
102
+ 1.0775,
103
+ 3.1267,
104
+ 2.0949,
105
+ 8.2730],
106
+ 'TC': ['niit']
107
+ },
108
+
109
+ '3:ITAX': {
110
+ # Total income tax liability
111
+ # in 2015 using the IRS-SOI information described above.
112
+ # ITAX is column (55) in the Table 3.3 spreadsheet,
113
+ # which includes NIIT and is after all credits.
114
+ 'SOI': [0.2425,
115
+ 0.0409,
116
+ 0.3680,
117
+ 1.3813,
118
+ 3.5238,
119
+ 6.1911,
120
+ 8.7526,
121
+ 25.1677,
122
+ 32.5302,
123
+ 99.7918,
124
+ 105.9015,
125
+ 316.3496,
126
+ 299.8322,
127
+ 154.3888,
128
+ 66.3236,
129
+ 39.6716,
130
+ 101.4885,
131
+ 56.3344,
132
+ 139.6113],
133
+ 'TC': ['iitax']
134
+ },
135
+
136
+ '4:SETAX': {
137
+ # Self-employment tax
138
+ # in 2015 using the IRS-SOI information described above.
139
+ # SETAX is column (59) in the Table 3.3 spreadsheet,
140
+ # which is not part of ITAX but is part of total payroll taxes.
141
+ 'SOI': [0.6557,
142
+ 0.5554,
143
+ 1.8956,
144
+ 3.5143,
145
+ 2.8228,
146
+ 1.9959,
147
+ 1.8020,
148
+ 3.3598,
149
+ 2.8199,
150
+ 5.9579,
151
+ 5.2751,
152
+ 12.1488,
153
+ 9.6864,
154
+ 3.4864,
155
+ 1.1938,
156
+ 0.6432,
157
+ 1.2527,
158
+ 0.4698,
159
+ 0.6383],
160
+ 'TC': ['setax']
161
+ },
162
+
163
+ '5:AMTAX': {
164
+ # Additional Medicare tax
165
+ # in 2015 using the IRS-SOI information described above.
166
+ # AMTAX is column (71) in the Table 3.3 spreadsheet,
167
+ # which is not part of ITAX but is part of total payroll taxes.
168
+ 'SOI': [0.0225,
169
+ 0.0003,
170
+ 0.0000,
171
+ 0.0002,
172
+ 0.0002,
173
+ 0.0004,
174
+ 0.0002,
175
+ 0.0041,
176
+ 0.0071,
177
+ 0.0057,
178
+ 0.0026,
179
+ 0.0372,
180
+ 1.8356,
181
+ 2.0214,
182
+ 0.8602,
183
+ 0.4898,
184
+ 1.1730,
185
+ 0.5805,
186
+ 0.9787],
187
+ 'TC': ['ptax_amc']
188
+ }
189
+ }
190
+
191
+
192
+ def comparison(cname, calc, cmpdata, ofile):
193
+ """
194
+ Write comparison results for cname to ofile.
195
+ """
196
+ # pylint: disable=too-many-locals
197
+ # generate compare table for cvarname
198
+ vardf = calc.dataframe(['s006', 'c00100']) # weight and AGI
199
+ # add compare variable to vardf
200
+ cvar = np.zeros(calc.array_len)
201
+ for var in cmpdata[cname]['TC']:
202
+ cvar += calc.array(var)
203
+ vardf['cvar'] = cvar
204
+ # construct AGI table
205
+ vardf = add_income_table_row_variable(vardf, 'c00100', SOI_AGI_BINS)
206
+ gbydf = vardf.groupby('table_row', as_index=False)
207
+ # write AGI table with ALL row at bottom to ofile
208
+ ofile.write('TABLE for {}\n'.format(cname.split(':')[1]))
209
+ results = '{:23s}\t{:8.3f}\t{:8.3f}\t{:+6.1f}\n'
210
+ colhead = '{:23s}\t{:>8s}\t{:>8s}\t{:>6s}\n'
211
+ ofile.write(colhead.format('AGI category', 'T-C', 'SOI', '%diff'))
212
+ txc_tot = 0.
213
+ soi_tot = 0.
214
+ idx = 0
215
+ for grp_interval, grp in gbydf:
216
+ txc = (grp['cvar'] * grp['s006']).sum() * 1e-9
217
+ soi = cmpdata[cname]['SOI'][idx]
218
+ txc_tot += txc
219
+ soi_tot += soi
220
+ if soi > 0:
221
+ pct_diff = 100. * ((txc / soi) - 1.)
222
+ else:
223
+ pct_diff = np.nan
224
+ glabel = '[{:.8g}, {:.8g})'.format(grp_interval.left,
225
+ grp_interval.right)
226
+ ofile.write(results.format(glabel, txc, soi, pct_diff))
227
+ idx += 1
228
+ pct_diff = 100. * ((txc_tot / soi_tot) - 1.)
229
+ ofile.write(results.format('ALL', txc_tot, soi_tot, pct_diff))
230
+
231
+
232
+ def nonsmall_diffs(linelist1, linelist2, small=0.0):
233
+ """
234
+ Return True if line lists differ significantly; otherwise return False.
235
+ Significant numerical difference means one or more numbers differ (between
236
+ linelist1 and linelist2) by more than the specified small amount.
237
+ """
238
+ # embedded function used only in nonsmall_diffs function
239
+ def isfloat(value):
240
+ """
241
+ Return True if value can be cast to float; otherwise return False.
242
+ """
243
+ try:
244
+ float(value)
245
+ return True
246
+ except ValueError:
247
+ return False
248
+ # begin nonsmall_diffs logic
249
+ assert isinstance(linelist1, list)
250
+ assert isinstance(linelist2, list)
251
+ if len(linelist1) != len(linelist2):
252
+ return True
253
+ assert 0.0 <= small <= 1.0
254
+ epsilon = 1e-6
255
+ smallamt = small + epsilon
256
+ for line1, line2 in zip(linelist1, linelist2):
257
+ if line1 == line2:
258
+ continue
259
+ else:
260
+ tokens1 = line1.replace(',', '').split()
261
+ tokens2 = line2.replace(',', '').split()
262
+ for tok1, tok2 in zip(tokens1, tokens2):
263
+ tok1_isfloat = isfloat(tok1)
264
+ tok2_isfloat = isfloat(tok2)
265
+ if tok1_isfloat and tok2_isfloat:
266
+ if abs(float(tok1) - float(tok2)) <= smallamt:
267
+ continue
268
+ else:
269
+ return True
270
+ elif not tok1_isfloat and not tok2_isfloat:
271
+ if tok1 == tok2:
272
+ continue
273
+ else:
274
+ return True
275
+ else:
276
+ return True
277
+ return False
278
+
279
+
280
+ def differences(afilename, efilename):
281
+ """
282
+ Check for differences between results in afilename and efilename files.
283
+ """
284
+ with open(afilename, 'r') as afile:
285
+ actres = afile.read()
286
+ with open(efilename, 'r') as efile:
287
+ expres = efile.read()
288
+ diffs = nonsmall_diffs(actres.splitlines(True),
289
+ expres.splitlines(True), 0.0)
290
+ if diffs:
291
+ afname = os.path.basename(afilename)
292
+ efname = os.path.basename(efilename)
293
+ msg = 'COMPARE RESULTS DIFFER\n'
294
+ msg += '-------------------------------------------------\n'
295
+ msg += '--- NEW RESULTS IN {} FILE ---\n'
296
+ msg += '--- if new OK, copy {} to ---\n'
297
+ msg += '--- {} ---\n'
298
+ msg += '--- and rerun test. ---\n'
299
+ msg += '-------------------------------------------------\n'
300
+ raise ValueError(msg.format(afname, afname, efname))
301
+ os.remove(afilename)
302
+
303
+
304
+ @pytest.mark.pre_release
305
+ @pytest.mark.requires_pufcsv
306
+ @pytest.mark.parametrize('using_puf', [True, False])
307
+ def test_itax_compare(tests_path, using_puf, puf_fullsample, cps_fullsample):
308
+ """
309
+ Conduct income tax comparisons using ITAX data.
310
+ """
311
+ using_puf_adjust_ratios = True
312
+ # generate 2015 estimates by AGI category using Tax-Calculator
313
+ if using_puf:
314
+ if using_puf_adjust_ratios:
315
+ recs = Records(data=puf_fullsample)
316
+ else:
317
+ recs = Records(data=puf_fullsample, adjust_ratios=None)
318
+ else:
319
+ recs = Records.cps_constructor(data=cps_fullsample)
320
+ calc = Calculator(policy=Policy(), records=recs, verbose=False)
321
+ calc.advance_to_year(2015)
322
+ calc.calc_all()
323
+ # open actual output file
324
+ if using_puf:
325
+ afilename = os.path.join(tests_path, 'cmpi_puf_actual.txt')
326
+ else:
327
+ afilename = os.path.join(tests_path, 'cmpi_cps_actual.txt')
328
+ afile = open(afilename, 'w')
329
+ # write compare results to afile
330
+ for cname in sorted(ITAX.keys()):
331
+ comparison(cname, calc, ITAX, afile)
332
+ # close actual output file
333
+ afile.close()
334
+ # check for differences between actual and expect output files
335
+ efilename = afilename.replace('actual', 'expect')
336
+ differences(afilename, efilename)
@@ -0,0 +1,338 @@
1
+ """
2
+ Tests of the compatible_data fields in the policy_current_law.json file.
3
+
4
+ In order to tap into the parallelization capabilities of py.test, this module
5
+ leans heavily on py.tests's `parametrization` method. Once you do so, the
6
+ plug-in pytest-xdist is able to run all parametrized functions in parallel
7
+ """
8
+ # CODING-STYLE CHECKS:
9
+ # pycodestyle test_compatible_data.py
10
+ # pylint --disable=locally-disabled test_compatible_data.py
11
+
12
+ import copy
13
+ import pytest
14
+ import numpy as np
15
+ from taxcalc import Policy, Records, Calculator # pylint: disable=import-error
16
+
17
+
18
+ @pytest.fixture(scope='module', name='allparams')
19
+ def fixture_allparams():
20
+ """
21
+ Return metadata for current-law policy parameters.
22
+ """
23
+ clp = Policy()
24
+ return clp.metadata()
25
+
26
+
27
+ def test_compatible_data_presence(allparams):
28
+ """
29
+ Test that every parameter in the policy_current_law.json file
30
+ has a compatible_data field that is a dictionary.
31
+ """
32
+ compatible_data_keys_set = set(['puf', 'cps'])
33
+
34
+ # Nested function used only in test_compatible_data_presence
35
+ def valid_compatible_data(compatible_data):
36
+ """
37
+ Return True if compatible_data is a valid dictionary;
38
+ otherwise return False
39
+ """
40
+ if not isinstance(compatible_data, dict):
41
+ return False
42
+ if set(compatible_data.keys()) != compatible_data_keys_set:
43
+ return False
44
+ for key in compatible_data:
45
+ boolean = (compatible_data[key] is True or
46
+ compatible_data[key] is False)
47
+ if not boolean:
48
+ return False
49
+ return True
50
+
51
+ # Main logic of test_compatible_data_presence function
52
+ problem_pnames = list()
53
+ for pname in allparams:
54
+ if 'compatible_data' in allparams[pname]:
55
+ compatible_data = allparams[pname]['compatible_data']
56
+ else:
57
+ compatible_data = None
58
+ if not valid_compatible_data(compatible_data):
59
+ problem_pnames.append(pname)
60
+ if problem_pnames:
61
+ msg = '{} has no or invalid compatible_data field'
62
+ for pname in problem_pnames:
63
+ print(msg.format(pname))
64
+ assert 'list of problem_pnames' == 'empty list'
65
+
66
+
67
+ XX_YEAR = 2019
68
+ TEST_YEAR = 2020
69
+
70
+
71
+ @pytest.fixture(scope='module', name='reform_xx')
72
+ def fixture_reform_xx():
73
+ """
74
+ Fixture for reform dictionary where reform starts before TEST_YEAR.
75
+
76
+ The provisions in the baseline reform, designated in _reform_xx,
77
+ are chosen to activate parameters that are inactive under current law.
78
+ For example a phaseout rate for a new credit is inactive if the credit's
79
+ amount is set to zero under current law. In order to activate the phaseout
80
+ rate, the credit amount should be set above zero. The provisions interact
81
+ with each other: you may acidentally deactivate one parameter
82
+ by introducing a provision to activate another. If you find that a pair of
83
+ parameters are impossible test jointly, add one to the local variable
84
+ `exempt_from_testing` in `test_compatible_data()` as a last resort.
85
+ """
86
+ assert XX_YEAR < TEST_YEAR
87
+
88
+ # Set baseline to activate parameters that are inactive under current law.
89
+ _reform_xx = {
90
+ XX_YEAR: {
91
+ 'FST_AGI_trt': 0.5,
92
+ 'CTC_new_rt': 0.5,
93
+ 'CTC_new_c': 5000,
94
+ 'CTC_new_prt': 0.1,
95
+ 'CTC_new_refund_limited': True,
96
+ 'CTC_new_refund_limit_payroll_rt': 1,
97
+ 'ACTC_ChildNum': 1,
98
+ 'ID_BenefitSurtax_trt': 0.1,
99
+ 'ID_BenefitSurtax_crt': 0.1,
100
+ 'UBI_u18': 1000,
101
+ 'UBI_1820': 1000,
102
+ 'UBI_21': 1000,
103
+ 'PT_brk7': [1000000, 1000000, 1000000, 1000000, 1000000],
104
+ 'II_credit_prt': 0.1,
105
+ 'II_credit': [100, 100, 100, 100, 100],
106
+ 'CG_brk3': [1000000, 1000000, 1000000, 1000000, 1000000],
107
+ 'ALD_Dependents_Child_c': 1000,
108
+ 'II_credit_nr': [1000, 1000, 1000, 1000, 1000],
109
+ 'II_credit_nr_prt': 0.1,
110
+ 'AMT_CG_brk3': [500000, 500000, 500000, 500000, 500000],
111
+ 'AGI_surtax_thd': [1000000, 1000000, 1000000, 1000000, 1000000],
112
+ 'AGI_surtax_trt': 0.5,
113
+ 'ID_AmountCap_rt': 0.9,
114
+ 'II_brk7': [1000000, 1000000, 1000000, 1000000, 1000000],
115
+ 'ID_BenefitCap_rt': 0.3,
116
+ 'PT_rt7': 0.35,
117
+ 'II_em': 1000,
118
+ 'ID_Casualty_hc': 0.1,
119
+ 'ID_Miscellaneous_hc': 0.1,
120
+ 'ID_prt': 0.03,
121
+ 'ID_crt': 0.8,
122
+ 'CR_Charity_rt': 0.4,
123
+ 'CR_Charity_f': [5000, 5000, 5000, 5000, 5000],
124
+ 'CR_Charity_frt': 0.5,
125
+ 'CR_SchR_hc': 0.5
126
+ }
127
+ }
128
+ return _reform_xx
129
+
130
+
131
+ @pytest.fixture(scope='module', name='sorted_param_names')
132
+ def fixture_sorted_param_names(allparams):
133
+ """
134
+ Fixture for storing a sorted parameter list
135
+ """
136
+ return sorted(list(allparams.keys()))
137
+
138
+
139
+ NPARAMS = 219 # hard-code NPARAMS to len(allparams)
140
+ BATCHSIZE = 10
141
+ BATCHES = int(np.floor(NPARAMS / BATCHSIZE)) + 1
142
+
143
+
144
+ @pytest.fixture(scope='module', name='allparams_batch',
145
+ params=[i for i in range(0, BATCHES)])
146
+ def fixture_allparams_batch(request, allparams, sorted_param_names):
147
+ """
148
+ Fixture for grouping Tax-Calculator parameters
149
+
150
+ Experiments indicated that there is some overhead when you run
151
+ `test_compatible_data` on each parameter individually. Suppose it takes X
152
+ amount of time to set up the test data for `test_compatible_data` and Y
153
+ amount of time to run `test_compatible_data` on each parameter wihtout
154
+ parallelization. Then, if there is no overhead from parallelization, you
155
+ would expect it to take Y + (X / NUMBER_WORKERS) to run these tests in
156
+ parallel. Note that setup data is only created once if you set the
157
+ fixture scope to 'module'. However, experiments indicated that there was
158
+ so much overhead that the tests weren't that much faster in parallel than
159
+ if they were run sequentially.
160
+
161
+ I found that running the parameters in batches decreased the amount of
162
+ overhead. Further, there was an optimal batch size that I found through
163
+ trial and error. On my local machine, this was something like 10
164
+ parameters. Others may find a different optimal batch size on their
165
+ machines. Further, if the number of parameters changes, the optimal
166
+ batch size could change, too.
167
+
168
+ Math for partitioning the parameters:
169
+
170
+ Suppose we have N parameters and choose batch size n. Then, we have
171
+ B batches where B equals floor(N / n) + 1.
172
+
173
+ Case 1: N % n = 0
174
+ Then we have:
175
+ idx_min = {i * b, i = 0, 1, 2, 3, ..., B - 1} and
176
+ idx_max = {min((i + 1) * b, N), i = 0, 1, 2, 3, ..., B - 1}
177
+
178
+ So, if i equals 0, the batch contains the first b - 1 parameters.
179
+ Then, if i equals B, then idx_min is n * (B - 1) = N and idx_max is N and
180
+ thus, the last batch is empty.
181
+
182
+ Case 2: N % n = r > 0
183
+ Then, everything is the same as case 1, except for the final batch.
184
+ In the final batch, idx_min = b * (B - 1) = b * floor(N / n) < N, and
185
+ idx_max is N. So, we our final batch size is
186
+ idx_max - idx_min = N - b * B = r.
187
+
188
+ returns: dictionary of size, BATCHSIZE, or for the final batch,
189
+ either an empty dictionary or dictionary of size NPARAMS mod BATCHSIZE
190
+ """
191
+ idx = request.param
192
+ idx_start = idx * BATCHSIZE
193
+ idx_end = min((idx + 1) * BATCHSIZE, NPARAMS)
194
+ pnames = sorted_param_names[idx_start: idx_end]
195
+ return {pname: allparams[pname] for pname in pnames}
196
+
197
+
198
+ @pytest.fixture(scope='module', name='tc_objs',
199
+ params=[True, False])
200
+ def fixture_tc_objs(request, reform_xx, puf_subsample, cps_subsample):
201
+ """
202
+ Fixture for creating Tax-Calculator objects that use the PUF and
203
+ use the CPS (called only twice: once for PUF and once for CPS)
204
+ """
205
+ puftest = request.param
206
+ p_xx = Policy()
207
+ p_xx.implement_reform(reform_xx, raise_errors=False)
208
+ if puftest:
209
+ rec_xx = Records(data=puf_subsample)
210
+ else:
211
+ rec_xx = Records.cps_constructor(data=cps_subsample)
212
+ c_xx = Calculator(policy=p_xx, records=rec_xx)
213
+ c_xx.advance_to_year(TEST_YEAR)
214
+ c_xx.calc_all()
215
+ return rec_xx, c_xx, puftest
216
+
217
+
218
+ @pytest.mark.skip
219
+ @pytest.mark.pre_release
220
+ @pytest.mark.compatible_data
221
+ @pytest.mark.requires_pufcsv
222
+ def test_compatible_data(cps_subsample, puf_subsample,
223
+ allparams, reform_xx,
224
+ tc_objs, allparams_batch):
225
+ """
226
+ Test that the compatible_data attribute in policy_current_law.json
227
+ is accurate by implementing the min and max values of each parameter
228
+ as reforms and ensuring that revenue differs from baseline when for
229
+ at least one of these reforms when using datasets marked compatible
230
+ and does not differ when using datasets marked as incompatible.
231
+ """
232
+ # pylint: disable=too-many-arguments,too-many-locals
233
+ # pylint: disable=too-many-statements,too-many-branches
234
+
235
+ # Check NPARAMS value
236
+ assert NPARAMS == len(allparams)
237
+
238
+ # Get taxcalc objects from tc_objs fixture
239
+ rec_xx, c_xx, puftest = tc_objs
240
+
241
+ # These parameters are exempt because they are not active under
242
+ # current law and activating them would deactivate other parameters,
243
+ # or if it is difficult to devise a test for them.
244
+ exempt_from_testing = [
245
+ 'CG_ec', 'CG_reinvest_ec_rt',
246
+ 'II_prt', 'ID_prt', 'ID_crt',
247
+ 'CR_SchR_hc', 'ACTC_ChildNum'
248
+ ]
249
+
250
+ # Loop through the parameters in allparams_batch
251
+ errmsg = 'ERROR: {} {}\n'
252
+ errors = ''
253
+ for pname in allparams_batch:
254
+ param = allparams_batch[pname]
255
+ max_listed = param['valid_values']['max']
256
+ # handle links to other params or self
257
+ if isinstance(max_listed, str):
258
+ if isinstance(allparams[max_listed]['value'][0], list):
259
+ max_val = allparams[max_listed]['value'][0]
260
+ else:
261
+ max_val = float(allparams[max_listed]['value'][0])
262
+ else:
263
+ if isinstance(param['value'][0], list):
264
+ max_val = [max_listed] * len(param['value'][0])
265
+ else:
266
+ max_val = max_listed
267
+ min_listed = param['valid_values']['min']
268
+ if isinstance(min_listed, str):
269
+ if isinstance(allparams[min_listed]['value'][0], list):
270
+ min_val = allparams[min_listed]['value'][0]
271
+ else:
272
+ min_val = float(allparams[min_listed]['value'][0])
273
+ else:
274
+ if isinstance(param['value'][0], list):
275
+ min_val = [min_listed] * len(param['value'][0])
276
+ else:
277
+ min_val = min_listed
278
+ # create reform dictionaries
279
+ max_reform = copy.deepcopy(reform_xx)
280
+ min_reform = copy.deepcopy(reform_xx)
281
+ max_reform[XX_YEAR][str(pname)] = [max_val]
282
+ min_reform[XX_YEAR][str(pname)] = [min_val]
283
+ # assess whether max reform changes results
284
+ if puftest:
285
+ rec_yy = Records(data=puf_subsample)
286
+ else:
287
+ rec_yy = Records.cps_constructor(data=cps_subsample)
288
+ p_yy = Policy()
289
+ p_yy.implement_reform(max_reform, raise_errors=False)
290
+ c_yy = Calculator(policy=p_yy, records=rec_yy, verbose=False)
291
+ c_yy.advance_to_year(TEST_YEAR)
292
+ c_yy.calc_all()
293
+ if pname.startswith('BEN') and pname.endswith('_repeal'):
294
+ max_reform_change = (
295
+ c_yy.weighted_total('benefit_cost_total') -
296
+ c_xx.weighted_total('benefit_cost_total')
297
+ )
298
+ else:
299
+ max_reform_change = (
300
+ c_yy.weighted_total('combined') -
301
+ c_xx.weighted_total('combined')
302
+ )
303
+ min_reform_change = 0
304
+ # assess whether min reform changes results, if max reform did not
305
+ if max_reform_change == 0:
306
+ p_yy = Policy()
307
+ p_yy.implement_reform(min_reform, raise_errors=False)
308
+ c_yy = Calculator(policy=p_yy, records=rec_xx)
309
+ c_yy.advance_to_year(TEST_YEAR)
310
+ c_yy.calc_all()
311
+ if pname.startswith('BEN') and pname.endswith('_repeal'):
312
+ min_reform_change = (
313
+ c_yy.weighted_total('benefit_cost_total') -
314
+ c_xx.weighted_total('benefit_cost_total')
315
+ )
316
+ else:
317
+ min_reform_change = (
318
+ c_yy.weighted_total('combined') -
319
+ c_xx.weighted_total('combined')
320
+ )
321
+ if min_reform_change == 0 and pname not in exempt_from_testing:
322
+ if puftest:
323
+ if param['compatible_data']['puf'] is True:
324
+ errors += errmsg.format(pname, 'is not True for puf')
325
+ else:
326
+ if param['compatible_data']['cps'] is True:
327
+ errors += errmsg.format(pname, 'is not True for cps')
328
+ if max_reform_change != 0 or min_reform_change != 0:
329
+ if puftest:
330
+ if param['compatible_data']['puf'] is False:
331
+ errors += errmsg.format(pname, 'is not False for puf')
332
+ else:
333
+ if param['compatible_data']['cps'] is False:
334
+ errors += errmsg.format(pname, 'is not False for cps')
335
+ # test failure if any errors
336
+ if errors:
337
+ print(errors)
338
+ assert 'compatible_data' == 'invalid'