Skip to article frontmatterSkip to article content

Theory Walkthrough: From One Household to Society

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, Dict

Part 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:

  1. Direct Effect: Each household makes suboptimal labor choices due to misperceiving their MTR
  2. Fiscal Externality: Suboptimal labor choices reduce total tax revenue
  3. Redistribution Effect: Lower revenue means smaller demogrant for everyone
  4. 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:

  1. Use 56,839 real household records from the Enhanced CPS microdata
  2. Apply household weights to represent ~131.7 million U.S. households
  3. Use actual marginal tax rates from PolicyEngine calculations (including all federal, state, and local taxes plus benefit phase-outs)
  4. Model realistic uncertainty based on survey evidence (Gideon 2017, Rees-Jones & Taubinsky 2020)
  5. 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:

  1. Individual level: How tax uncertainty causes suboptimal labor supply decisions
  2. Society level: How individual mistakes aggregate to reduce total tax revenue and redistribution
  3. 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.