Depending on how much you played around last week, you may have found that methods like QuadGK don’t play nicely with automatic differentiation. This is because QuadGK adjusts the number of nodes adaptively.
One solution is to use a fixed number of nodes and weights with FastGaussQuadrature. Here is a simple example.
usingFastGaussQuadrature, QuadGK, ForwardDiff, Distributions, Optim, Roots# a function to integrate using quadrature (default: 10 nodes)functionintegrateGL(f,a,b ; num_nodes =10) nodes, weights =gausslegendre( num_nodes ) ∫f =0.for k ineachindex(nodes) x = (a+b)/2+ (b - a)/2* nodes[k] ∫f += weights[k] *f(x)endreturn (b - a)/2* ∫fenddS(x ; F,β,δ) = (1-cdf(F,x)) / (1-β*(1-δ))res_wage_1(wres , b,λ,δ,β,F) = wres - b - β * λ *integrateGL(x->dS(x;F,β,δ),wres,quantile(F,0.9999))res_wage_2(wres , b,λ,δ,β,F) = wres - b - β * λ *quadgk(x->dS(x;F,β,δ),wres,Inf)[1]ForwardDiff.derivative(x->res_wage_1(x,0.,0.5,0.03,0.99,LogNormal(0,1)),1.) #ForwardDiff.derivative(x->res_wage_2(x,0.,0.5,0.03,0.99,LogNormal(0,1)),1.) <- you can try running this and you will see an error
7.224647846541125
Re-writing the model solution
Based on this, we’re going to re-write the model solution using this new integration routine. We will also use Roots to solve for the reservation wage in a way that will also play nicely with ForwardDiff.
res_wage(wres , b,λ,δ,β,F::Distribution) = wres - b - β * λ *integrateGL(x->dS(x;F,β,δ),wres,quantile(F,0.9999))pars = (;b =-5.,λ =0.45,δ =0.03,β =0.99,F =LogNormal(1,1))functionsolve_res_wage(b,λ,δ,β,F)returnfind_zero(x->res_wage(x,b,λ,δ,β,F),eltype(b)(4.)) #<- initial guess of $4/hourendsolve_res_wage(0.,0.4,0.03,0.995,LogNormal())# testing that we can take a derivative here:ForwardDiff.derivative(x->solve_res_wage(x,0.4,0.04,0.995,LogNormal()),0.)
0.45978046027514413
Cleaning the Data
None of this is really different from before, but we added a function that pulls a named tuple out for a specific demographic group. You might find that useful for your assignment.
usingCSV, DataFrames, DataFramesMeta, Statisticsdata = CSV.read("../data/cps_00019.csv",DataFrame)data =@chain data begin@transform:E =:EMPSTAT.<21@transform@byrow:wage =beginif:PAIDHOUR==0returnmissingelseif:PAIDHOUR==2if:HOURWAGE<99.99&&:HOURWAGE>0return:HOURWAGEelsereturnmissingendelseif:PAIDHOUR==1if:EARNWEEK>0&&:UHRSWORKT<997&&:UHRSWORKT>0return:EARNWEEK /:UHRSWORKTelsereturnmissingendendend@subset:MONTH.==1@select:AGE :SEX :RACE :EDUC :wage :E :DURUNEMP@transformbegin:bachelors =:EDUC.>=111:nonwhite =:RACE.!=100:female =:SEX.==2:DURUNEMP =round.(:DURUNEMP .*12/52)endend# the whole dataset in a named tuplewage_missing =ismissing.(data.wage)wage =coalesce.(data.wage,1.)N =length(data.AGE)X = [ones(N) data.bachelors data.female data.nonwhite]# create a named tuple with all variables to conveniently pass to the log-likelihood:d = (;logwage =log.(wage),wage_missing,E = data.E,tU = data.DURUNEMP, X) #<- you will need to add your demographics as well.functionget_data(data,C,F,R) data =@subset data :bachelors.==C :female.==F :nonwhite.==R wage_missing =ismissing.(data.wage) wage =coalesce.(data.wage,1.) N =length(data.AGE)# create a named tuple with all variables to conveniently pass to the log-likelihood:return d = (;logwage =log.(wage),wage_missing,E = data.E,tU = data.DURUNEMP) enddx =get_data(data,1,0,0) #<- data for white men with a college degree
Coding the log-likelihood with one set of parameters
In your assignment you will estimate the model for one set of demographic characteristics by directly estimating \((h,\delta,\mu,\sigma,w^*)\) and backing out \(b\) and \(\lambda\). Here is a log-likelihood function that you could use (feel free to write your own).
ϕ(x,μ,σ) =pdf(Normal(μ,σ),x)Φ(x,μ,σ) =cdf(Normal(μ,σ),x)# a function for the log-likelihood of observed wages (integrating out measurement error)functionlogwage_likelihood(logwage,F,σζ,wres)f(x) =pdf(F,x) / (1-cdf(F,wres)) *ϕ(logwage,log(x),σζ) ub =quantile(F,0.9999)returnintegrateGL(f,wres,ub)end# a function to get the log-likelihood of a single observationfunctionlog_likelihood(n,data,pars) (;h,δ,wres,F,σζ) = pars ll =0.if data.E[n] ll +=log(h) -log(h + δ)if !data.wage_missing[n] ll +=logwage_likelihood(data.logwage[n],F,σζ,wres)endelse ll +=log(δ) -log(h + δ) ll +=log(h) + data.tU[n] *log(1-h)endreturn llend# a function to map the vector x into parameterslogit(x) =exp(x) / (1+exp(x))logit_inv(x) =log(x/(1-x))functionupdate(pars,x) h =logit(x[1]) δ =logit(x[2]) μ = x[3] σ =exp(x[4]) wres =exp(x[5]) F =LogNormal(μ,σ) σζ =0.05return (;pars...,h,δ,μ,σ,wres,F,σζ)end# a function to iterate over all observationsfunctionlog_likelihood_obj(x,pars,data) pars =update(pars,x) ll =0.for n ineachindex(data.E) ll +=log_likelihood(n,data,pars)endreturn ll /length(data.E)end
log_likelihood_obj (generic function with 1 method)
With this log-likelihood we can pass straight to Optim.
What about standard errors? Some parameters (such as \(\delta\)) are transformations of the estimated vector. Here we need the delta method, which tells us that if \(\mathbb{V}(\hat{\theta}) = v\) and \(\beta = f(\theta)\) then \(\mathbb{V}(\hat{\beta}) = f'(\theta)^2v\):
One final note. You could automate this whole calculation by writing a function that returns all your transformed parameters of interest as a function of the estimated vector, then calling ForwardDiff:
usingLinearAlgebrafunctionfinal_ests(x_est,pars) pars =update(pars,x_est) (;h,δ,μ,σ,wres,F,β) = pars λ = h / (1-cdf(F,wres)) b = wres - β * λ *integrateGL(x->dS(x;F,β,δ),wres,quantile(F,0.9999))return [h , δ, μ, σ, wres, λ, b]enddF = ForwardDiff.jacobian(x->final_ests(x,pars),res.minimizer)var_est = dF * avar * dF'/ N #<- this is the variance of the estimates implied by the delta method# now a table with estimates and standard errors:p_str = ["h","δ","μ","σ","wres","λ","b"]DataFrame(par = p_str,est =final_ests(res.minimizer,pars),se =sqrt.(diag(var_est)))