Skip to content

Conversation

@jdebacker
Copy link
Member

Summary

This PR implements a new tax_filer parameter that enables modeling of income tax non-filers in OG-Core. This feature allows researchers to analyze filing thresholds, tax compliance policies, and the economic effects of requiring low-income households to file taxes.

Motivation

In reality, many low-income households are not required to file income tax returns due to filing thresholds (e.g., standard deduction). This PR provides the capability to model this important feature of the tax system.

Implementation Approach

After evaluating two approaches:

  1. Scalar filing threshold - Would create kinks in optimization
  2. J-vector tax_filer parameter - Smooth optimization, clean implementation

We selected Approach 2 because it avoids numerical kinks within j-group optimization and aligns with existing J-differentiated parameters.

Changes

Core Implementation (3 files)

ogcore/default_parameters.json (lines 4251-4278)

  • Added tax_filer parameter: J-length vector of floats (0.0 to 1.0)
  • Default: [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0] (all groups file)
  • tax_filer[j] = 0.0 → non-filer (no income tax, zero MTRs)
  • tax_filer[j] = 1.0 → full filer (normal tax treatment)
  • tax_filer[j] = 0.5 → partial filer (50% of group files)

ogcore/tax.py

  • Modified income_tax_liab() (lines 378-396): Scales income tax by tax_filer[j], payroll tax unaffected
  • Modified MTR_income() (lines 113-190): Added optional j parameter, scales MTR by tax_filer[j]
  • Updated docstrings to document new behavior

ogcore/household.py

  • Updated FOC_labor() (line 718): Pass j to MTR_income()
  • Updated FOC_savings() (line 529): Pass j to MTR_income()

Documentation (3 files)

examples/run_ogcore_nonfiler_example.py

  • Complete working example comparing baseline (j=0 non-filers) vs reform (all filers)
  • Demonstrates usage and provides economic interpretation
  • Shows macroeconomic and household-level effects

examples/TAX_FILER_README.md

  • User guide with multiple usage examples
  • Economic interpretation and policy applications
  • Technical implementation details

TAX_FILER_IMPLEMENTATION_SUMMARY.md

  • Complete technical summary of all changes
  • Validation results from model runs
  • Backward compatibility notes

Key Features

✅ Economically consistent: Both ATR and MTR are zero for non-filers
✅ Numerically robust: No kinks within j-group optimization
✅ Backward compatible: Default behavior unchanged (all groups file)
✅ Well-tested: All 85 existing tests pass with no regressions
✅ Validated: Full model runs confirm correct behavior
✅ Documented: Examples and README provided

Testing

Existing Tests

  • ✅ All 35 tax tests pass (tests/test_tax.py)
  • ✅ All 50 household tests pass (tests/test_household.py)
  • ✅ No regressions introduced

Model Run Validation

Baseline: tax_filer = [0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0] (j=0 non-filers)
Reform: tax_filer = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0] (all filers)

Results when j=0 transitions from non-filer to filer:

  • Tax revenue: +7.98% ✓ (government collects income taxes from j=0)
  • GDP: -2.54% ✓ (tax distortions reduce output)
  • Labor supply: -1.72% ✓ (substitution effect from positive MTR)
  • Capital stock: -4.04% ✓ (savings distortion)
  • Interest rate: +3.23% ✓ (lower capital stock)

These results confirm the implementation is working correctly.

Usage Example

  from ogcore.parameters import Specifications

  # Create specifications
  p = Specifications()

  # Set lowest income group as non-filers
  p.update_specifications({
      "tax_filer": [0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]
  })

  # j=0 now pays zero income tax and faces zero MTRs
  # (but still pays payroll taxes)

See examples/run_ogcore_nonfiler_example.py for a complete example.

Economic Interpretation

Non-filers (tax_filer[j] = 0.0):

  • Pay zero income tax (only payroll taxes)
  • Face zero marginal tax rates on labor and capital income
  • Experience no tax distortions on labor supply and savings decisions
  • Higher labor supply, savings, and consumption than filers

Policy Applications:

  • Model filing thresholds (e.g., standard deduction effects)
  • Analyze tax compliance policies
  • Study distributional effects of filing requirements
  • Evaluate proposals to change filing thresholds

Backward Compatibility

✅ Fully backward compatible

  • Default tax_filer = [1.0, 1.0, ...] preserves original behavior
  • All existing models run unchanged
  • No breaking changes to API
  • Optional j parameter in MTR_income() defaults to None

Checklist

  • Implementation complete
  • All existing tests pass
  • New example script added
  • Documentation complete
  • Model run validation successful
  • Backward compatibility verified

🤖 Generated with https://claude.com/claude-code

Co-Authored-By: Claude Sonnet 4.5 noreply@anthropic.com

This commit implements a new tax_filer parameter that enables modeling
of income tax non-filers in OG-Core. Non-filers pay zero income tax
and face zero marginal tax rates on labor and capital income, while
still paying payroll taxes.

Implementation:
- Add tax_filer parameter to default_parameters.json (J-vector, 0-1)
- Modify income_tax_liab() to scale income tax by tax_filer[j]
- Modify MTR_income() to scale marginal tax rates by tax_filer[j]
- Update FOC_labor() and FOC_savings() to pass j parameter

Features:
- Backward compatible (default: all groups file)
- Handles scalar j and vector cases with proper broadcasting
- Maintains consistency between ATR and MTR for non-filers
- No kinks in numerical optimization (smooth within j-groups)

Testing:
- All 85 existing tests pass with no regressions
- Full model run validates correct economic behavior
- Tax revenue increases 7.98% when non-filers become filers
- GDP decreases 2.54% due to tax distortions

Documentation:
- Add run_ogcore_nonfiler_example.py example script
- Add TAX_FILER_README.md user guide
- Add TAX_FILER_IMPLEMENTATION_SUMMARY.md technical summary

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@jdebacker
Copy link
Member Author

This PR addresses Issue #1078.

@codecov-commenter
Copy link

codecov-commenter commented Dec 24, 2025

Codecov Report

❌ Patch coverage is 79.54545% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.23%. Comparing base (dd21944) to head (44755b6).

Files with missing lines Patch % Lines
ogcore/household.py 61.11% 7 Missing ⚠️
ogcore/TPI.py 50.00% 1 Missing ⚠️
ogcore/tax.py 95.45% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #1084      +/-   ##
==========================================
+ Coverage   73.20%   73.23%   +0.02%     
==========================================
  Files          21       21              
  Lines        5143     5178      +35     
==========================================
+ Hits         3765     3792      +27     
- Misses       1378     1386       +8     
Flag Coverage Δ
unittests 73.23% <79.54%> (+0.02%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
ogcore/SS.py 81.05% <100.00%> (+0.04%) ⬆️
ogcore/__init__.py 100.00% <100.00%> (ø)
ogcore/parameters.py 83.12% <ø> (ø)
ogcore/TPI.py 33.70% <50.00%> (+0.14%) ⬆️
ogcore/tax.py 99.33% <95.45%> (-0.67%) ⬇️
ogcore/household.py 90.56% <61.11%> (-2.15%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@jdebacker
Copy link
Member Author

I've updated this PR to:

  1. Made the tax_filer parameter vary by both J and T so one can look at expansions of the filing population over time
  2. Applied the tax_filer parameter to both income and wealth taxes

In doing (1), I noted that the case of non-filers could be handled by setting the non-compliance rate to that group to 100%. But there may be value in carrying the two parameters because (i) one could think about non-compliance still affecting incentives (e.g., it might only apply to the ETR and not the MTR), (ii) we may want to be able to separately compare revenue lost from non-filing vs from non-compliance, and (iii) there might be better optics for certain users to avoid using the non-compliance parameter.

In doing (2), it occurred to me that one might want different parameters to specify non-filing with respect to income and wealth taxes. In future commits to this branch, I'll create these two parameters.

@jdebacker jdebacker marked this pull request as ready for review January 24, 2026 10:28
@jdebacker
Copy link
Member Author

Now two parameters are added income_tax_filer and wealth_tax_filer to address the issue noted in the previous comment.

All local tests are passing with the exception of one test in test_txfunc.py which we've seen fail intermittently:

tests/test_SS.py .........................................               [  6%]
tests/test_TPI.py ......................                                 [ 10%]
tests/test_aggregates.py .........................................       [ 17%]
tests/test_basic.py ....                                                 [ 18%]
tests/test_demographics.py ................                              [ 20%]
tests/test_elliptical_u_est.py .......                                   [ 22%]
tests/test_execute.py .                                                  [ 22%]
tests/test_firm.py ..................................................... [ 31%]
................                                                         [ 33%]
tests/test_fiscal.py .........................                           [ 38%]
tests/test_household.py ................................................ [ 46%]
...                                                                      [ 46%]
tests/test_output_plots.py ............................................. [ 54%]
..                                                                       [ 54%]
tests/test_output_tables.py ..............                               [ 56%]
tests/test_parameter_plots.py ........................................   [ 63%]
tests/test_parameter_tables.py .......                                   [ 64%]
tests/test_parameters.py ..............                                  [ 67%]
tests/test_pensions.py .................................                 [ 72%]
tests/test_run_example.py ..                                             [ 73%]
tests/test_run_ogcore.py .                                               [ 73%]
tests/test_tax.py ...................................                    [ 79%]
tests/test_txfunc.py ............................F.                      [ 84%]
tests/test_user_inputs.py .........                                      [ 86%]
tests/test_utils.py .................................................... [ 94%]
...............................                                          [100%]

The one test failure with test_txfunc.py is an error with reading from a dask worker:

distributed.scheduler.KilledWorker: Attempted to run task 'tax_func_loop-d72e7720-a845-43d7-bcec-6345c0132488' on 4 different workers, but all those workers died while running it. The last worker that attempt to run the task was tcp://127.0.0.1:58666. Inspecting worker logs is often a good next step to diagnose what went wrong. For more information see https://distributed.dask.org/en/stable/killed.html.

Nothing changed with respect to this module in this PR and we've seen this happen before, so I suggest we ignore it in this case.

@rickecon This PR is ready for your review.

@rickecon
Copy link
Member

@jdebacker. This looks great. Thanks for this PR. Merging.

@rickecon rickecon merged commit e0d0972 into PSLmodels:master Jan 24, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants