Skip to article frontmatterSkip to article content

Theoretical Framework

This section develops the theoretical model of labor supply under tax rate uncertainty. The framework builds on standard optimal taxation theory but incorporates the realistic feature that agents must make labor supply decisions before knowing the exact tax rate they will face.

import numpy as np
import pandas as pd
import plotly.graph_objects as go
import plotly.express as px
from plotly.subplots import make_subplots

# Skip package imports if not available
try:
    import sys
    sys.path.append('../..')
    from src.taxuncertainty.models.utility import CobbDouglasUtility, OptimalChoice
    from src.taxuncertainty.analysis.uncertainty import UncertaintyAnalysis
except ImportError:
    # Define mock classes for demonstration if package not installed
    class CobbDouglasUtility:
        def __init__(self, leisure_exponent, consumption_exponent):
            self.leisure_exponent = leisure_exponent
            self.consumption_exponent = consumption_exponent
            self.elasticity_of_substitution = 1.0
        
        def calculate(self, leisure, consumption):
            return (leisure ** self.leisure_exponent) * (consumption ** self.consumption_exponent)
    
    class OptimalChoice:
        def __init__(self, utility_function):
            self.utility_function = utility_function
        
        def optimal_leisure(self, wage, tax_rate, transfers, total_hours=24):
            a = self.utility_function.leisure_exponent
            b = self.utility_function.consumption_exponent
            net_wage = wage * (1 - tax_rate)
            if net_wage <= 0:
                return total_hours
            uncapped = a * (net_wage * total_hours + transfers) / (net_wage * (a + b))
            return min(uncapped, total_hours)
        
        def labor_supply(self, wage, tax_rate, transfers, total_hours=24):
            return total_hours - self.optimal_leisure(wage, tax_rate, transfers, total_hours)
        
        def consumption(self, wage, tax_rate, transfers, total_hours=24):
            labor = self.labor_supply(wage, tax_rate, transfers, total_hours)
            return wage * (1 - tax_rate) * labor + transfers
        
        def indirect_utility(self, wage, tax_rate, transfers, total_hours=24):
            leisure = self.optimal_leisure(wage, tax_rate, transfers, total_hours)
            consumption_level = self.consumption(wage, tax_rate, transfers, total_hours)
            return self.utility_function.calculate(leisure, consumption_level)
    
    class UncertaintyAnalysis:
        def __init__(self, utility_function):
            self.utility_function = utility_function
            self.choice_solver = OptimalChoice(utility_function)
        
        def expected_utility_with_certainty(self, wage, tax_rates, probabilities, transfers=0, total_hours=24):
            expected_utility = 0
            for tax_rate, prob in zip(tax_rates, probabilities):
                utility = self.choice_solver.indirect_utility(wage, tax_rate, transfers, total_hours)
                expected_utility += prob * utility
            return expected_utility
        
        def expected_utility_with_uncertainty(self, wage, tax_rates, probabilities, transfers=0, total_hours=24):
            expected_tax = np.sum([t * p for t, p in zip(tax_rates, probabilities)])
            labor_choice = self.choice_solver.labor_supply(wage, expected_tax, transfers, total_hours)
            leisure_choice = total_hours - labor_choice
            expected_utility = 0
            for tax_rate, prob in zip(tax_rates, probabilities):
                consumption = wage * (1 - tax_rate) * labor_choice + transfers
                utility = self.utility_function.calculate(leisure_choice, consumption)
                expected_utility += prob * utility
            return expected_utility
        
        def deadweight_loss_from_uncertainty(self, wage, tax_rate_mean, tax_rate_std, n_scenarios=5, transfers=0, total_hours=24):
            if tax_rate_std == 0:
                return 0.0, 0.0
            from scipy import stats
            tax_dist = stats.norm(loc=tax_rate_mean, scale=tax_rate_std)
            quantiles = np.linspace(0.1, 0.9, n_scenarios)
            tax_rates = [max(0, min(1, tax_dist.ppf(q))) for q in quantiles]
            probabilities = [1/n_scenarios] * n_scenarios
            u_certain = self.expected_utility_with_certainty(wage, tax_rates, probabilities, transfers, total_hours)
            u_uncertain = self.expected_utility_with_uncertainty(wage, tax_rates, probabilities, transfers, total_hours)
            dwl = u_certain - u_uncertain
            dwl_percent = (dwl / u_certain) * 100 if u_certain > 0 else 0
            return dwl, dwl_percent
        
        def social_welfare(self, tax_rate, wage_samples, redistribute=True):
            n_agents = len(wage_samples)
            if redistribute:
                total_revenue = 0
                for wage in wage_samples:
                    labor = self.choice_solver.labor_supply(wage, tax_rate, 0)
                    total_revenue += wage * tax_rate * labor
                transfers = total_revenue / n_agents
            else:
                transfers = 0
            total_welfare = 0
            for wage in wage_samples:
                utility = self.choice_solver.indirect_utility(wage, tax_rate, transfers)
                total_welfare += utility
            return total_welfare / n_agents
        
        def optimal_tax_rate(self, wage_samples, tax_rate_uncertainty=0, search_range=(0.1, 0.5), n_grid=50):
            from dataclasses import dataclass
            
            @dataclass
            class UncertaintyResults:
                expected_utility_certain: float
                expected_utility_uncertain: float
                deadweight_loss: float
                deadweight_loss_percent: float
                optimal_tax_certain: float
                optimal_tax_uncertain: float
                welfare_gain_from_information: float
            
            tax_grid = np.linspace(search_range[0], search_range[1], n_grid)
            welfare_certain = []
            for tax in tax_grid:
                w = self.social_welfare(tax, wage_samples, redistribute=True)
                welfare_certain.append(w)
            
            optimal_idx_certain = np.argmax(welfare_certain)
            optimal_tax_certain = tax_grid[optimal_idx_certain]
            max_welfare_certain = welfare_certain[optimal_idx_certain]
            
            if tax_rate_uncertainty > 0:
                welfare_uncertain = []
                for tax_mean in tax_grid:
                    total_welfare = 0
                    for wage in wage_samples:
                        tax_scenarios = [
                            max(0, min(1, tax_mean + tax_rate_uncertainty * z))
                            for z in [-1.5, -0.5, 0, 0.5, 1.5]
                        ]
                        probs = [0.1, 0.2, 0.4, 0.2, 0.1]
                        labor = self.choice_solver.labor_supply(wage, tax_mean, 0)
                        revenue = wage * tax_mean * labor
                        transfers = revenue / len(wage_samples)
                        u = self.expected_utility_with_uncertainty(wage, tax_scenarios, probs, transfers)
                        total_welfare += u
                    welfare_uncertain.append(total_welfare / len(wage_samples))
                
                optimal_idx_uncertain = np.argmax(welfare_uncertain)
                optimal_tax_uncertain = tax_grid[optimal_idx_uncertain]
                max_welfare_uncertain = welfare_uncertain[optimal_idx_uncertain]
            else:
                optimal_tax_uncertain = optimal_tax_certain
                max_welfare_uncertain = max_welfare_certain
            
            dwl = max_welfare_certain - max_welfare_uncertain
            dwl_percent = (dwl / max_welfare_certain) * 100 if max_welfare_certain > 0 else 0
            welfare_gain = dwl
            
            return UncertaintyResults(
                expected_utility_certain=max_welfare_certain,
                expected_utility_uncertain=max_welfare_uncertain,
                deadweight_loss=dwl,
                deadweight_loss_percent=dwl_percent,
                optimal_tax_certain=optimal_tax_certain,
                optimal_tax_uncertain=optimal_tax_uncertain,
                welfare_gain_from_information=welfare_gain
            )

# Set random seed for reproducibility
np.random.seed(42)

Model Setup

Consider an agent with Cobb-Douglas preferences over leisure LL and consumption CC:

U(L,C)=LαCβU(L, C) = L^\alpha C^\beta

where α\alpha and β\beta are positive parameters representing the preference weights for leisure and consumption respectively.

The agent faces a time constraint L+h=TL + h = T, where hh is hours worked and TT is total time available. The budget constraint is:

C=w(1τ)h+vC = w(1-\tau)h + v

where ww is the wage rate, τ\tau is the tax rate on labor income, and vv represents lump-sum transfers.

Optimal Choice Under Certainty

When the tax rate is known with certainty, the agent solves:

maxLU(L,w(1τ)(TL)+v)\max_{L} U(L, w(1-\tau)(T-L) + v)

The first-order condition yields the optimal leisure choice:

L=α(w(1τ)T+v)w(1τ)(α+β)L^* = \frac{\alpha(w(1-\tau)T + v)}{w(1-\tau)(\alpha + \beta)}

This formula shows that leisure depends on the after-tax wage w(1τ)w(1-\tau), transfers vv, and preference parameters.

# Demonstrate optimal choice under certainty
utility = CobbDouglasUtility(leisure_exponent=0.5, consumption_exponent=0.5)
choice = OptimalChoice(utility)

# Parameters
wage = 25  # $/hour
tax_rate = 0.3
transfers = 100  # lump sum
total_hours = 24

# Calculate optimal choice
leisure_opt = choice.optimal_leisure(wage, tax_rate, transfers, total_hours)
labor_opt = choice.labor_supply(wage, tax_rate, transfers, total_hours)
consumption_opt = choice.consumption(wage, tax_rate, transfers, total_hours)
utility_opt = choice.indirect_utility(wage, tax_rate, transfers, total_hours)

print(f"Optimal choices under certainty (τ = {tax_rate:.1%}):")
print(f"  Leisure: {leisure_opt:.2f} hours")
print(f"  Labor: {labor_opt:.2f} hours")
print(f"  Consumption: ${consumption_opt:.2f}")
print(f"  Utility: {utility_opt:.2f}")

Choice Under Tax Rate Uncertainty

Now suppose the agent must choose labor supply before the tax rate is revealed. The tax rate τ\tau follows a distribution with support on [τL,τH][\tau_L, \tau_H]. The timing is:

  1. Agent chooses labor supply hh based on expected tax rate
  2. Tax rate τ\tau is realized
  3. Consumption is determined as C=w(1τ)h+vC = w(1-\tau)h + v
  4. Utility is realized as U(Th,C)U(T-h, C)

The key insight is that the agent cannot adjust labor supply after the tax rate is revealed, leading to suboptimal choices ex-post.

# Demonstrate choice under uncertainty
analysis = UncertaintyAnalysis(utility)

# Define uncertain tax rates
tax_rates = [0.2, 0.3, 0.4]  # Possible tax rates
probabilities = [0.25, 0.5, 0.25]  # Probabilities
expected_tax = sum(t * p for t, p in zip(tax_rates, probabilities))

print(f"Tax rate distribution:")
for t, p in zip(tax_rates, probabilities):
    print(f"  τ = {t:.1%} with probability {p:.2f}")
print(f"Expected tax rate: {expected_tax:.1%}")

# Calculate utilities
u_certain = analysis.expected_utility_with_certainty(
    wage, tax_rates, probabilities, transfers, total_hours
)
u_uncertain = analysis.expected_utility_with_uncertainty(
    wage, tax_rates, probabilities, transfers, total_hours
)

dwl = u_certain - u_uncertain
dwl_percent = (dwl / u_certain) * 100

print(f"\nExpected utility with perfect information: {u_certain:.3f}")
print(f"Expected utility under uncertainty: {u_uncertain:.3f}")
print(f"Deadweight loss from uncertainty: {dwl:.3f} ({dwl_percent:.2f}% of utility)")

The Special Case of Cobb-Douglas Preferences

Cobb-Douglas preferences have a special property: the elasticity of substitution between leisure and consumption equals one. This means that when facing wage uncertainty (as opposed to tax uncertainty), income and substitution effects exactly cancel out.

However, tax rate uncertainty is different from wage uncertainty because:

  1. Taxes create a wedge between gross and net wages
  2. The timing of information revelation matters
  3. Transfers may depend on realized tax revenue

This distinction is crucial for understanding why tax uncertainty generates welfare losses even with Cobb-Douglas preferences.

# Visualize how uncertainty affects welfare across different mean tax rates
mean_tax_rates = np.linspace(0.1, 0.5, 20)
uncertainty_levels = [0, 0.05, 0.10, 0.15]

fig = go.Figure()

for uncertainty in uncertainty_levels:
    welfare_losses = []
    for mean_tax in mean_tax_rates:
        if uncertainty == 0:
            dwl, dwl_pct = 0, 0
        else:
            dwl, dwl_pct = analysis.deadweight_loss_from_uncertainty(
                wage, mean_tax, uncertainty, n_scenarios=5
            )
        welfare_losses.append(dwl_pct)
    
    fig.add_trace(go.Scatter(
        x=mean_tax_rates * 100,
        y=welfare_losses,
        mode='lines',
        name=f'σ = {uncertainty:.2f}',
        line=dict(width=2)
    ))

fig.update_layout(
    title='Deadweight Loss from Tax Rate Uncertainty',
    xaxis_title='Mean Tax Rate (%)',
    yaxis_title='Welfare Loss (% of Certain Utility)',
    hovermode='x unified',
    template='plotly_white'
)

fig.show()

Welfare Analysis with Heterogeneous Agents

In reality, agents differ in their wages and potentially in their preferences. Consider a population with wage distribution F(w)F(w). The social planner chooses a tax rate τ\tau to maximize utilitarian social welfare:

W(τ)=U(L(w,τ),C(w,τ))dF(w)W(\tau) = \int U(L^*(w,\tau), C^*(w,\tau)) dF(w)

When tax rates are uncertain, the planner must account for the additional welfare loss from uncertainty.

# Create heterogeneous population
n_agents = 1000
wage_distribution = np.random.lognormal(mean=3.0, sigma=0.6, size=n_agents)
wage_distribution = np.maximum(wage_distribution, 5)  # Minimum wage

# Visualize wage distribution
fig = px.histogram(
    x=wage_distribution,
    nbins=30,
    title='Wage Distribution in Population',
    labels={'x': 'Hourly Wage ($)', 'y': 'Count'},
    template='plotly_white'
)
fig.add_vline(x=np.median(wage_distribution), line_dash="dash", 
              annotation_text=f"Median: ${np.median(wage_distribution):.2f}")
fig.show()

print(f"Wage distribution statistics:")
print(f"  Mean: ${np.mean(wage_distribution):.2f}")
print(f"  Median: ${np.median(wage_distribution):.2f}")
print(f"  Std Dev: ${np.std(wage_distribution):.2f}")
print(f"  90/10 ratio: {np.percentile(wage_distribution, 90)/np.percentile(wage_distribution, 10):.2f}")

Optimal Taxation with and without Uncertainty

The social planner’s problem changes fundamentally when tax rates are uncertain. Without uncertainty, the planner chooses τ\tau to balance efficiency (minimizing deadweight loss) and equity (redistributing from high to low earners). With uncertainty, there’s an additional consideration: the welfare loss from agents’ inability to optimize perfectly.

This generally leads to lower optimal tax rates under uncertainty, as the marginal welfare cost of taxation is higher when agents cannot adjust optimally.

# Find optimal tax rates with and without uncertainty
results_certain = analysis.optimal_tax_rate(
    wage_distribution,
    tax_rate_uncertainty=0,
    search_range=(0.15, 0.45),
    n_grid=30
)

results_uncertain = analysis.optimal_tax_rate(
    wage_distribution,
    tax_rate_uncertainty=0.08,
    search_range=(0.15, 0.45),
    n_grid=30
)

print("Optimal Tax Analysis Results:")
print("\nWithout Uncertainty:")
print(f"  Optimal tax rate: {results_certain.optimal_tax_certain:.1%}")
print(f"  Social welfare: {results_certain.expected_utility_certain:.3f}")

print("\nWith Uncertainty (σ = 0.08):")
print(f"  Optimal tax rate: {results_uncertain.optimal_tax_uncertain:.1%}")
print(f"  Social welfare: {results_uncertain.expected_utility_uncertain:.3f}")
print(f"  Deadweight loss: {results_uncertain.deadweight_loss:.3f} ({results_uncertain.deadweight_loss_percent:.2f}%)")
print(f"\nValue of Information Provision: {results_uncertain.welfare_gain_from_information:.3f}")

Decomposing the Welfare Effects

The total welfare effect of tax uncertainty can be decomposed into several components:

  1. Direct effect: Agents make suboptimal labor supply choices
  2. Fiscal effect: Tax revenue differs from expected, affecting transfers
  3. Distribution effect: Uncertainty affects different income groups differently
  4. Dynamic effect: Uncertainty may change savings and investment (not modeled here)

The relative importance of these effects depends on the wage distribution, tax progressivity, and the nature of uncertainty.

# Analyze welfare effects by income quintile
quintile_boundaries = np.percentile(wage_distribution, [0, 20, 40, 60, 80, 100])
quintile_labels = ['Q1 (Bottom 20%)', 'Q2', 'Q3', 'Q4', 'Q5 (Top 20%)']

welfare_by_quintile = []

for i in range(5):
    mask = (wage_distribution >= quintile_boundaries[i]) & (wage_distribution < quintile_boundaries[i+1])
    quintile_wages = wage_distribution[mask]
    
    if len(quintile_wages) > 0:
        avg_wage = np.mean(quintile_wages)
        dwl, dwl_pct = analysis.deadweight_loss_from_uncertainty(
            avg_wage, tax_rate_mean=0.3, tax_rate_std=0.08
        )
        welfare_by_quintile.append({
            'Quintile': quintile_labels[i],
            'Avg Wage': avg_wage,
            'DWL (%)': dwl_pct
        })

df_quintiles = pd.DataFrame(welfare_by_quintile)

fig = px.bar(
    df_quintiles,
    x='Quintile',
    y='DWL (%)',
    title='Deadweight Loss from Tax Uncertainty by Income Quintile',
    template='plotly_white',
    text='DWL (%)'
)
fig.update_traces(texttemplate='%{text:.2f}%', textposition='outside')
fig.update_layout(yaxis_title='Welfare Loss (% of Certain Utility)')
fig.show()

Key Theoretical Results

The theoretical analysis yields several important results:

  1. Uncertainty creates deadweight loss: Even with Cobb-Douglas preferences where income and substitution effects offset for wage uncertainty, tax rate uncertainty generates welfare losses because agents cannot optimize ex-post.

  2. Losses increase with uncertainty: The welfare loss is approximately quadratic in the standard deviation of tax rates, meaning that small reductions in uncertainty can have meaningful welfare effects.

  3. Heterogeneous effects: Middle-income households typically face the largest welfare losses because they:

    • Have sufficient labor supply flexibility to be affected
    • Face more complex tax schedules with various phase-ins/outs
    • Cannot easily afford professional tax planning
  4. Optimal tax adjustment: A social planner accounting for uncertainty should set lower tax rates than suggested by standard models, as uncertainty amplifies the efficiency cost of taxation.

  5. Information value: Providing clear, advance information about tax changes is equivalent to a lump-sum transfer to all agents, representing a Pareto improvement.

These theoretical insights provide the foundation for the empirical analysis that follows, where we calibrate the model using actual U.S. tax data.