This chapter builds intuition for how tax uncertainty affects welfare, starting with a single household and building up to societal effects with revenue recycling.
import numpy as np
import pandas as pd
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from dataclasses import dataclass
from typing import Tuple, DictPart 1: One Household Example¶
Consider Sarah, a single parent earning $30/hour who faces uncertainty about her marginal tax rate due to EITC phase-outs and state tax interactions.
@dataclass
class Household:
"""Represents a household with Cobb-Douglas preferences"""
name: str
wage: float # $/hour
alpha: float = 0.5 # leisure preference
beta: float = 0.5 # consumption preference
total_hours: float = 24 * 365 / 7 # hours per week
def utility(self, leisure: float, consumption: float) -> float:
"""Cobb-Douglas utility function"""
return (leisure ** self.alpha) * (consumption ** self.beta)
def optimal_labor(self, tax_rate: float, transfer: float = 0) -> float:
"""Optimal labor supply given tax rate and transfers"""
# From FOC of utility maximization
net_wage = self.wage * (1 - tax_rate)
if net_wage <= 0:
return 0
# Optimal leisure from Cobb-Douglas FOC
optimal_leisure = (self.alpha * (net_wage * self.total_hours + transfer)) / ((self.alpha + self.beta) * net_wage)
optimal_leisure = min(optimal_leisure, self.total_hours) # Can't have negative labor
return self.total_hours - optimal_leisure
def consumption(self, labor: float, tax_rate: float, transfer: float = 0) -> float:
"""Calculate consumption given labor choice"""
gross_income = self.wage * labor
tax_paid = gross_income * tax_rate
return gross_income - tax_paid + transfer
# Create Sarah's household
sarah = Household(name="Sarah", wage=30)
print(f"Household: {sarah.name}")
print(f"Hourly wage: ${sarah.wage}")
print(f"Weekly time endowment: {sarah.total_hours:.0f} hours")
print(f"Preferences: α={sarah.alpha} (leisure), β={sarah.beta} (consumption)")Case 1: Perfect Information¶
First, let’s see Sarah’s optimal choice when she knows her exact marginal tax rate.
# Sarah's actual marginal tax rate (federal + state + EITC phase-out)
actual_mtr = 0.35
# Calculate optimal choice with perfect information
optimal_labor_perfect = sarah.optimal_labor(actual_mtr)
optimal_consumption_perfect = sarah.consumption(optimal_labor_perfect, actual_mtr)
optimal_leisure_perfect = sarah.total_hours - optimal_labor_perfect
utility_perfect = sarah.utility(optimal_leisure_perfect, optimal_consumption_perfect)
print("With Perfect Information:")
print(f" Actual MTR: {actual_mtr:.1%}")
print(f" Optimal labor: {optimal_labor_perfect:.1f} hours/week")
print(f" Optimal leisure: {optimal_leisure_perfect:.1f} hours/week")
print(f" Weekly consumption: ${optimal_consumption_perfect:,.2f}")
print(f" Utility level: {utility_perfect:,.1f}")Case 2: Tax Uncertainty¶
Now suppose Sarah is uncertain about her marginal tax rate. She knows it’s somewhere between 25% and 45%, but the tax code is too complex to determine exactly.
# Sarah perceives her MTR could be anywhere in this range
perceived_mtr_low = 0.25
perceived_mtr_high = 0.45
perceived_mtr_mean = (perceived_mtr_low + perceived_mtr_high) / 2
# She optimizes based on expected (mean) tax rate
labor_with_uncertainty = sarah.optimal_labor(perceived_mtr_mean)
# But her actual consumption depends on the true tax rate
actual_consumption = sarah.consumption(labor_with_uncertainty, actual_mtr)
actual_leisure = sarah.total_hours - labor_with_uncertainty
utility_uncertain = sarah.utility(actual_leisure, actual_consumption)
print("With Tax Uncertainty:")
print(f" Perceived MTR range: {perceived_mtr_low:.1%} - {perceived_mtr_high:.1%}")
print(f" Expected MTR (used for decision): {perceived_mtr_mean:.1%}")
print(f" Actual MTR (determines outcome): {actual_mtr:.1%}")
print(f" Labor choice: {labor_with_uncertainty:.1f} hours/week")
print(f" Actual consumption: ${actual_consumption:,.2f}")
print(f" Utility level: {utility_uncertain:,.1f}")
# Calculate welfare loss
welfare_loss = utility_perfect - utility_uncertain
welfare_loss_percent = (welfare_loss / utility_perfect) * 100
print(f"\nWelfare Loss from Uncertainty:")
print(f" Absolute: {welfare_loss:.2f} utils")
print(f" Percentage: {welfare_loss_percent:.2f}% of optimal utility")Visualizing the Welfare Loss¶
Let’s visualize how utility varies with labor choice and how uncertainty leads to suboptimal decisions.
# Calculate utility for different labor choices
labor_range = np.linspace(0, sarah.total_hours, 100)
utilities_actual = []
utilities_perceived = []
for labor in labor_range:
# Utility with actual tax rate
consumption_actual = sarah.consumption(labor, actual_mtr)
leisure = sarah.total_hours - labor
utilities_actual.append(sarah.utility(leisure, consumption_actual))
# Utility as perceived (using expected tax rate)
consumption_perceived = sarah.consumption(labor, perceived_mtr_mean)
utilities_perceived.append(sarah.utility(leisure, consumption_perceived))
# Create plot
fig = go.Figure()
# Actual utility curve
fig.add_trace(go.Scatter(
x=labor_range, y=utilities_actual,
mode='lines', name='Actual Utility',
line=dict(color='blue', width=2)
))
# Perceived utility curve
fig.add_trace(go.Scatter(
x=labor_range, y=utilities_perceived,
mode='lines', name='Perceived Utility',
line=dict(color='red', width=2, dash='dash')
))
# Mark optimal points
fig.add_trace(go.Scatter(
x=[optimal_labor_perfect], y=[utility_perfect],
mode='markers', name='Optimal (perfect info)',
marker=dict(color='green', size=10, symbol='star')
))
fig.add_trace(go.Scatter(
x=[labor_with_uncertainty], y=[utility_uncertain],
mode='markers', name='Choice (uncertainty)',
marker=dict(color='red', size=10, symbol='x')
))
# Add welfare loss annotation
fig.add_annotation(
x=labor_with_uncertainty, y=utility_uncertain,
text=f"Welfare loss: {welfare_loss_percent:.1f}%",
showarrow=True, arrowhead=2,
ax=40, ay=-40
)
fig.update_layout(
title="Sarah's Labor Choice Under Tax Uncertainty",
xaxis_title="Labor Hours per Week",
yaxis_title="Utility",
template='plotly_white',
height=400
)
fig.show()Part 2: Two-Household Society with Revenue Recycling¶
Now let’s add a second household and see how tax uncertainty affects society when tax revenue is redistributed as a demogrant (universal basic income).
# Create two households with different wages
sarah = Household(name="Sarah (Middle income)", wage=30)
john = Household(name="John (High income)", wage=60)
# Their actual marginal tax rates
sarah_mtr = 0.35 # Higher due to benefit phase-outs
john_mtr = 0.30 # Lower despite higher income (no phase-outs)
# Uncertainty ranges (both face ±10 percentage points)
uncertainty_range = 0.10
def calculate_society_welfare(households, mtrs, uncertainty=False):
"""Calculate total welfare with revenue recycling through demogrant"""
total_revenue = 0
labor_choices = []
# Step 1: Each household makes labor choice
for household, actual_mtr in zip(households, mtrs):
if uncertainty:
# Under uncertainty, use expected MTR for decision
# But could be wrong by ±uncertainty_range
perceived_mtr = actual_mtr # They guess correctly on average
# But might be off - simulate being off by half the range
decision_mtr = actual_mtr - uncertainty_range/2 # Systematic underestimation
else:
decision_mtr = actual_mtr
labor = household.optimal_labor(decision_mtr, transfer=0) # Initial choice without knowing transfer
labor_choices.append(labor)
# Calculate tax revenue using actual MTR
revenue = household.wage * labor * actual_mtr
total_revenue += revenue
# Step 2: Redistribute revenue as demogrant
demogrant = total_revenue / len(households)
# Step 3: Calculate utilities with demogrant
utilities = []
for household, labor, actual_mtr in zip(households, labor_choices, mtrs):
consumption = household.consumption(labor, actual_mtr, demogrant)
leisure = household.total_hours - labor
utility = household.utility(leisure, consumption)
utilities.append(utility)
return {
'total_revenue': total_revenue,
'demogrant': demogrant,
'labor_choices': labor_choices,
'utilities': utilities,
'total_welfare': sum(utilities)
}
# Calculate with perfect information
perfect_info = calculate_society_welfare([sarah, john], [sarah_mtr, john_mtr], uncertainty=False)
# Calculate with uncertainty
uncertain = calculate_society_welfare([sarah, john], [sarah_mtr, john_mtr], uncertainty=True)
print("Two-Household Society Results:\n")
print("With Perfect Information:")
print(f" Sarah's labor: {perfect_info['labor_choices'][0]:.1f} hours")
print(f" John's labor: {perfect_info['labor_choices'][1]:.1f} hours")
print(f" Total tax revenue: ${perfect_info['total_revenue']:,.2f}")
print(f" Demogrant per household: ${perfect_info['demogrant']:,.2f}")
print(f" Sarah's utility: {perfect_info['utilities'][0]:,.1f}")
print(f" John's utility: {perfect_info['utilities'][1]:,.1f}")
print(f" Total social welfare: {perfect_info['total_welfare']:,.1f}")
print("\nWith Tax Uncertainty:")
print(f" Sarah's labor: {uncertain['labor_choices'][0]:.1f} hours")
print(f" John's labor: {uncertain['labor_choices'][1]:.1f} hours")
print(f" Total tax revenue: ${uncertain['total_revenue']:,.2f}")
print(f" Demogrant per household: ${uncertain['demogrant']:,.2f}")
print(f" Sarah's utility: {uncertain['utilities'][0]:,.1f}")
print(f" John's utility: {uncertain['utilities'][1]:,.1f}")
print(f" Total social welfare: {uncertain['total_welfare']:,.1f}")
# Calculate welfare losses
welfare_loss_total = perfect_info['total_welfare'] - uncertain['total_welfare']
welfare_loss_percent = (welfare_loss_total / perfect_info['total_welfare']) * 100
revenue_loss = perfect_info['total_revenue'] - uncertain['total_revenue']
revenue_loss_percent = (revenue_loss / perfect_info['total_revenue']) * 100
print("\nEffects of Tax Uncertainty:")
print(f" Social welfare loss: {welfare_loss_percent:.2f}%")
print(f" Tax revenue loss: {revenue_loss_percent:.2f}%")
print(f" Demogrant reduction: ${perfect_info['demogrant'] - uncertain['demogrant']:,.2f}")Key Insights from Two-Household Example¶
The two-household example reveals several important mechanisms:
- Direct Effect: Each household makes suboptimal labor choices due to misperceiving their MTR
- Fiscal Externality: Suboptimal labor choices reduce total tax revenue
- Redistribution Effect: Lower revenue means smaller demogrant for everyone
- Heterogeneous Impact: The welfare loss depends on both the uncertainty and the tax rate level
# Visualize the distribution of welfare losses
fig = make_subplots(
rows=1, cols=2,
subplot_titles=('Individual Welfare Effects', 'Revenue and Redistribution'),
specs=[[{'type': 'bar'}, {'type': 'bar'}]]
)
# Individual utilities
households_names = ['Sarah', 'John']
fig.add_trace(
go.Bar(name='Perfect Info', x=households_names,
y=perfect_info['utilities'], marker_color='blue'),
row=1, col=1
)
fig.add_trace(
go.Bar(name='Uncertainty', x=households_names,
y=uncertain['utilities'], marker_color='red'),
row=1, col=1
)
# Revenue effects
revenue_categories = ['Tax Revenue', 'Demogrant']
fig.add_trace(
go.Bar(name='Perfect Info', x=revenue_categories,
y=[perfect_info['total_revenue'], perfect_info['demogrant']],
marker_color='blue'),
row=1, col=2
)
fig.add_trace(
go.Bar(name='Uncertainty', x=revenue_categories,
y=[uncertain['total_revenue'], uncertain['demogrant']],
marker_color='red'),
row=1, col=2
)
fig.update_yaxes(title_text="Utility", row=1, col=1)
fig.update_yaxes(title_text="Dollars", row=1, col=2)
fig.update_layout(
title="Two-Household Society: Effects of Tax Uncertainty",
template='plotly_white',
height=400,
showlegend=True
)
fig.show()Part 3: Scaling Up - Preview of Full Analysis¶
These simple examples illustrate the key mechanisms. The full analysis with PolicyEngine ECPS data will:
- Use 56,839 real household records from the Enhanced CPS microdata
- Apply household weights to represent ~131.7 million U.S. households
- Use actual marginal tax rates from PolicyEngine calculations (including all federal, state, and local taxes plus benefit phase-outs)
- Model realistic uncertainty based on survey evidence (Gideon 2017, Rees-Jones & Taubinsky 2020)
- Analyze distributional impacts across income deciles, family types, and states
The next chapter implements this full analysis.
# Preview of what PolicyEngine data provides
print("PolicyEngine Enhanced CPS Microdata Features:")
print("\n✓ Variables available:")
print(" - marginal_tax_rate: Computed MTR for each household")
print(" - employment_income: Wages and salaries")
print(" - household_weight: For population-level estimates")
print(" - state_code: State of residence")
print(" - filing_status: Tax filing status")
print(" - household_size: Number of people")
print(" - spm_unit_net_income: After-tax income")
print("\n✓ Sample size: 56,839 household records (all with positive weights)")
print("✓ Represents: ~131.7 million U.S. households when weighted")
print("✓ MTR calculation includes:")
print(" - Federal income tax with all brackets")
print(" - State and local income taxes")
print(" - Payroll taxes (Social Security, Medicare)")
print(" - EITC phase-ins and phase-outs")
print(" - Child Tax Credit effects")
print(" - SNAP, TANF, and other benefit reductions")
print(" - ACA premium tax credit cliffs")Summary¶
This walkthrough demonstrates:
- Individual level: How tax uncertainty causes suboptimal labor supply decisions
- Society level: How individual mistakes aggregate to reduce total tax revenue and redistribution
- Mechanisms: Both direct utility losses and fiscal externalities matter
The framework is now ready to be applied to real data from PolicyEngine’s Enhanced CPS.