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 and consumption :
where and are positive parameters representing the preference weights for leisure and consumption respectively.
The agent faces a time constraint , where is hours worked and is total time available. The budget constraint is:
where is the wage rate, is the tax rate on labor income, and represents lump-sum transfers.
Optimal Choice Under Certainty¶
When the tax rate is known with certainty, the agent solves:
The first-order condition yields the optimal leisure choice:
This formula shows that leisure depends on the after-tax wage , transfers , 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 follows a distribution with support on . The timing is:
- Agent chooses labor supply based on expected tax rate
- Tax rate is realized
- Consumption is determined as
- Utility is realized as
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:
- Taxes create a wedge between gross and net wages
- The timing of information revelation matters
- 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 . The social planner chooses a tax rate to maximize utilitarian social welfare:
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 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:
- Direct effect: Agents make suboptimal labor supply choices
- Fiscal effect: Tax revenue differs from expected, affecting transfers
- Distribution effect: Uncertainty affects different income groups differently
- 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:
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.
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.
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
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.
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.