Skip to content
This repository was archived by the owner on Mar 11, 2022. It is now read-only.

Commit 7e6651c

Browse files
authored
Add ParseFEterms, GroupFEterms and MakeFEs (#10)
1 parent 5e470d1 commit 7e6651c

File tree

9 files changed

+268
-104
lines changed

9 files changed

+268
-104
lines changed

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Vcov = "ec2bfdc2-55df-4fc9-b9ae-4958c2cf2486"
1919
[compat]
2020
DataAPI = "1.5"
2121
DataFrames = "1"
22-
DiffinDiffsBase = "0.3.2"
22+
DiffinDiffsBase = "0.3.3"
2323
FixedEffectModels = "1.5"
2424
FixedEffects = "2"
2525
Reexport = "0.2, 1"

src/InteractionWeightedDIDs.jl

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module InteractionWeightedDIDs
33
using Base: Callable
44
using DataAPI: refarray
55
using FixedEffectModels: FixedEffectTerm, Combination,
6-
fe, _parse_fixedeffect, invsym!, isnested, nunique
6+
fe, fesymbol, _multiply, invsym!, isnested, nunique
77
using FixedEffects
88
using LinearAlgebra: Cholesky, Factorization, Symmetric, cholesky!, diag
99
using Reexport
@@ -26,6 +26,9 @@ export Vcov,
2626
fe
2727

2828
export CheckVcov,
29+
ParseFEterms,
30+
GroupFEterms,
31+
MakeFEs,
2932
CheckFEs,
3033
MakeFESolver,
3134
MakeYXCols,

src/did.jl

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
Estimation procedure for regression-based difference-in-differences.
55
"""
66
const RegressionBasedDID = DiffinDiffsEstimator{:RegressionBasedDID,
7-
Tuple{CheckData, GroupTerms, CheckVcov, CheckVars, CheckFEs, MakeWeights, MakeFESolver,
7+
Tuple{CheckData, GroupTreatintterms, GroupXterms, CheckVcov, CheckVars, GroupSample,
8+
ParseFEterms, GroupFEterms, MakeFEs, CheckFEs, MakeWeights, MakeFESolver,
89
MakeYXCols, MakeTreatCols, SolveLeastSquares, EstVcov, SolveLeastSquaresWeights}}
910

1011
const Reg = RegressionBasedDID
@@ -64,7 +65,7 @@ struct RegressionBasedDIDResult{TR<:AbstractTreatment, CohortInteracted} <: DIDR
6465
xterms::Vector{AbstractTerm}
6566
contrasts::Union{Dict{Symbol, Any}, Nothing}
6667
weightname::Union{Symbol, Nothing}
67-
fenames::Vector{Symbol}
68+
fenames::Vector{String}
6869
nfeiterations::Union{Int, Nothing}
6970
feconverged::Union{Bool, Nothing}
7071
nfesingledropped::Int

src/procedures.jl

Lines changed: 125 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -26,73 +26,173 @@ exclude rows that are invalid for variance-covariance estimator.
2626
"""
2727
const CheckVcov = StatsStep{:CheckVcov, typeof(checkvcov!), true}
2828

29+
# Get esample and aux directly from CheckData
2930
required(::CheckVcov) = (:data, :esample, :aux)
3031
default(::CheckVcov) = (vce=Vcov.robust(),)
3132
copyargs(::CheckVcov) = (2,)
3233

34+
"""
35+
parsefeterms!(xterms)
36+
37+
Extract any `FixedEffectTerm` or interaction of `FixedEffectTerm` from `xterms`
38+
and determine whether any intercept term should be omitted.
39+
See also [`ParseFEterms`](@ref).
40+
"""
41+
function parsefeterms!(xterms::TermSet)
42+
feterms = Set{FETerm}()
43+
has_fe_intercept = false
44+
for t in xterms
45+
result = _parsefeterm(t)
46+
if result !== nothing
47+
push!(feterms, result)
48+
delete!(xterms, t)
49+
end
50+
end
51+
if !isempty(feterms)
52+
if any(t->isempty(t[2]), feterms)
53+
has_fe_intercept = true
54+
for t in xterms
55+
t isa Union{ConstantTerm,InterceptTerm} && delete!(xterms, t)
56+
end
57+
push!(xterms, InterceptTerm{false}())
58+
end
59+
end
60+
return (xterms=xterms, feterms=feterms, has_fe_intercept=has_fe_intercept)
61+
end
62+
63+
const ParseFEterms = StatsStep{:ParseFEterms, typeof(parsefeterms!), true}
64+
65+
required(::ParseFEterms) = (:xterms,)
66+
67+
"""
68+
groupfeterms(feterms)
69+
70+
Return the argument without change for allowing later comparisons based on object-id.
71+
See also [`GroupFEterms`](@ref).
72+
"""
73+
groupfeterms(feterms::Set{FETerm}) = (feterms=feterms,)
74+
75+
"""
76+
GroupFEterms <: StatsStep
77+
78+
Call [`InteractionWeightedDIDs.groupfeterms`](@ref)
79+
to obtain one of the instances of `feterms`
80+
that have been grouped by equality (`hash`)
81+
for allowing later comparisons based on object-id.
82+
83+
This step is only useful when working with [`@specset`](@ref) and [`proceed`](@ref).
84+
"""
85+
const GroupFEterms = StatsStep{:GroupFEterms, typeof(groupfeterms), false}
86+
87+
required(::GroupFEterms) = (:feterms,)
88+
89+
"""
90+
makefes(args...)
91+
92+
Construct `FixedEffect`s from `data` (the full sample).
93+
See also [`MakeFEs`](@ref).
94+
"""
95+
function makefes(data, allfeterms::Vector{FETerm})
96+
# Must use Dict instead of IdDict since the same feterm can be in multiple feterms
97+
allfes = Dict{FETerm,FixedEffect}()
98+
for t in allfeterms
99+
haskey(allfes, t) && continue
100+
if isempty(t[2])
101+
allfes[t] = FixedEffect((getcolumn(data, n) for n in t[1])...)
102+
else
103+
allfes[t] = FixedEffect((getcolumn(data, n) for n in t[1])...;
104+
interaction=_multiply(data, t[2]))
105+
end
106+
end
107+
return (allfes=allfes,)
108+
end
109+
110+
"""
111+
MakeFEs <: StatsStep
112+
113+
Call [`InteractionWeightedDIDs.makefes`](@ref)
114+
to construct `FixedEffect`s from `data` (the full sample).
115+
"""
116+
const MakeFEs = StatsStep{:MakeFEs, typeof(makefes), false}
117+
118+
required(::MakeFEs) = (:data,)
119+
combinedargs(::MakeFEs, allntargs) = (FETerm[t for nt in allntargs for t in nt.feterms],)
120+
33121
"""
34122
checkfes!(args...)
35123
36-
Extract any `FixedEffectTerm` from `xterms`,
37-
drop singleton observations for any fixed effect
38-
and determine whether intercept term should be omitted.
124+
Drop any singleton observation from fixed effects over the relevant subsample.
39125
See also [`CheckFEs`](@ref).
40126
"""
41-
function checkfes!(data, esample::BitVector, xterms::TermSet, drop_singletons::Bool)
42-
fes, fenames, has_fe_intercept = parse_fixedeffect!(data, xterms)
127+
function checkfes!(feterms::Set{FETerm}, allfes::Dict{FETerm,FixedEffect},
128+
esample::BitVector, drop_singletons::Bool)
43129
nsingle = 0
44-
if !isempty(fes)
130+
nfe = length(feterms)
131+
if nfe > 0
132+
fes = Vector{FixedEffect}(undef, nfe)
133+
fenames = Vector{String}(undef, nfe)
134+
# Loop together to ensure the orders are the same
135+
for (i, t) in enumerate(feterms)
136+
fes[i] = allfes[t]
137+
fenames[i] = getfename(t)
138+
end
139+
# Determine the unique order based on names
140+
order = sortperm(fenames)
141+
fes = fes[order]
142+
fenames = fenames[order]
143+
45144
if drop_singletons
46145
for fe in fes
47146
nsingle += drop_singletons!(esample, fe)
48147
end
49148
end
149+
sum(esample) == 0 && error("no nonmissing data")
150+
151+
for i in 1:nfe
152+
fes[i] = fes[i][esample]
153+
end
154+
return (esample=esample, fes=fes, fenames=fenames, nsingle=nsingle)
155+
else
156+
return (esample=esample, fes=FixedEffect[], fenames=String[], nsingle=0)
50157
end
51-
sum(esample) == 0 && error("no nonmissing data")
52-
return (xterms=xterms, esample=esample, fes=fes, fenames=fenames,
53-
has_fe_intercept=has_fe_intercept, nsingle=nsingle)
54158
end
55159

56160
"""
57161
CheckFEs <: StatsStep
58162
59163
Call [`InteractionWeightedDIDs.checkfes!`](@ref)
60-
to extract any `FixedEffectTerm` from `xterms`
61-
and drop singleton observations for any fixed effect.
164+
to drop any singleton observation from fixed effects over the relevant subsample.
62165
"""
63-
const CheckFEs = StatsStep{:CheckFixedEffects, typeof(checkfes!), true}
166+
const CheckFEs = StatsStep{:CheckFEs, typeof(checkfes!), true}
64167

65-
required(::CheckFEs) = (:data, :esample, :xterms)
168+
required(::CheckFEs) = (:feterms, :allfes, :esample)
66169
default(::CheckFEs) = (drop_singletons=true,)
67-
copyargs(::CheckFEs) = (2,3)
170+
copyargs(::CheckFEs) = (3,)
68171

69172
"""
70-
makefesolver!(args...)
173+
makefesolver(args...)
71174
72175
Construct `FixedEffects.AbstractFixedEffectSolver`.
73176
See also [`MakeFESolver`](@ref).
74177
"""
75-
function makefesolver!(fes::Vector{FixedEffect}, weights::AbstractWeights,
76-
esample::BitVector, nfethreads::Int)
178+
function makefesolver(fes::Vector{FixedEffect}, weights::AbstractWeights, nfethreads::Int)
77179
if !isempty(fes)
78-
fes = FixedEffect[fe[esample] for fe in fes]
79180
feM = AbstractFixedEffectSolver{Float64}(fes, weights, Val{:cpu}, nfethreads)
80-
return (feM=feM, fes=fes)
181+
return (feM=feM,)
81182
else
82-
return (feM=nothing, fes=fes)
183+
return (feM=nothing,)
83184
end
84185
end
85186

86187
"""
87188
MakeFESolver <: StatsStep
88189
89-
Call [`InteractionWeightedDIDs.makefesolver!`](@ref) to construct the fixed effect solver.
190+
Call [`InteractionWeightedDIDs.makefesolver`](@ref) to construct the fixed effect solver.
90191
"""
91-
const MakeFESolver = StatsStep{:MakeFESolver, typeof(makefesolver!), true}
192+
const MakeFESolver = StatsStep{:MakeFESolver, typeof(makefesolver), true}
92193

93-
required(::MakeFESolver) = (:fes, :weights, :esample)
194+
required(::MakeFESolver) = (:fes, :weights)
94195
default(::MakeFESolver) = (nfethreads=Threads.nthreads(),)
95-
copyargs(::MakeFESolver) = (1,)
96196

97197
function _makeyxcols!(yxterms::Dict, yxcols::Dict, yxschema, data, t::AbstractTerm)
98198
ct = apply_schema(t, yxschema, StatisticalModel)
@@ -291,6 +391,7 @@ transformed(::MakeTreatCols, @nospecialize(nt::NamedTuple)) = (nt.tr.time,)
291391
# Obtain the relative time periods excluded by all tr
292392
# and the treatment groups excluded by all pr in allntargs
293393
function combinedargs(::MakeTreatCols, allntargs)
394+
# exc cannot be IdDict for comparing different types of one
294395
exc = Dict{Int,Int}()
295396
notreat = IdDict{ValidTimeType,Int}()
296397
for nt in allntargs

src/utils.jl

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,27 @@
1-
function parse_fixedeffect!(data, ts::TermSet)
2-
fes = FixedEffect[]
3-
ids = Symbol[]
4-
has_fe_intercept = false
5-
for term in ts
6-
result = _parse_fixedeffect(data, term)
7-
if result !== nothing
8-
push!(fes, result[1])
9-
push!(ids, result[2])
10-
delete!(ts, term)
11-
end
1+
# A vector of fixed effects paired with a vector of interactions (empty if not interacted)
2+
const FETerm = Pair{Vector{Symbol},Vector{Symbol}}
3+
4+
# Parse fixed effects from a generic term
5+
function _parsefeterm(@nospecialize(t::AbstractTerm))
6+
if has_fe(t)
7+
s = fesymbol(t)
8+
return [s]=>Symbol[]
129
end
13-
order = sortperm(ids)
14-
fes .= fes[order]
15-
ids .= ids[order]
10+
end
11+
12+
# Parse fixed effects from an InteractionTerm
13+
function _parsefeterm(@nospecialize(t::InteractionTerm))
14+
fes = (x for x in t.terms if has_fe(x))
15+
interactions = (x for x in t.terms if !has_fe(x))
1616
if !isempty(fes)
17-
if any(fe->fe.interaction isa UnitWeights, fes)
18-
has_fe_intercept = true
19-
for t in ts
20-
t isa Union{ConstantTerm,InterceptTerm} && delete!(ts, t)
21-
end
22-
push!(ts, InterceptTerm{false}())
23-
end
17+
fes = sort!([fesymbol(x) for x in fes])
18+
feints = sort!([Symbol(x) for x in interactions])
19+
return fes=>feints
2420
end
25-
return fes, ids, has_fe_intercept
2621
end
2722

23+
getfename(feterm::FETerm) = join(vcat("fe_".*string.(feterm[1]), feterm[2]), "&")
24+
2825
# Count the number of singletons dropped
2926
function drop_singletons!(esample, fe::FixedEffect)
3027
cache = zeros(Int, fe.n)

test/did.jl

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -209,19 +209,25 @@ end
209209
@testset "@specset" begin
210210
hrs = exampledata("hrs")
211211
# The first two specs are identical hence no repetition of steps should occur
212-
# The third spec should only share the first three steps with the others
213-
r = @specset [verbose] begin
214-
@did(Reg, dynamic(:wave, -1), notyettreated(11), data=hrs,
215-
yterm=term(:oop_spend), treatname=:wave_hosp, treatintterms=(),
212+
# The third spec should share all the steps until SolveLeastSquares
213+
# The fourth and fifth specs should not add tasks for MakeFEs and MakeYXCols
214+
# The sixth spec should not add any task for MakeFEs
215+
r = @specset [verbose] data=hrs yterm=term(:oop_spend) treatname=:wave_hosp begin
216+
@did(Reg, dynamic(:wave, -1), notyettreated(11),
216217
xterms=(fe(:wave)+fe(:hhidpn)))
217-
@did(Reg, dynamic(:wave, -1), notyettreated(11), data=hrs,
218-
yterm=term(:oop_spend), treatname=:wave_hosp, treatintterms=[],
218+
@did(Reg, dynamic(:wave, -1), notyettreated(11),
219219
xterms=[fe(:hhidpn), fe(:wave)])
220-
@did(Reg, dynamic(:wave, -1), nevertreated(11), data=hrs,
221-
yterm=term(:oop_spend), treatname=:wave_hosp, treatintterms=(),
220+
@did(Reg, dynamic(:wave, -2:-1), notyettreated(11),
221+
xterms=[fe(:hhidpn), fe(:wave)])
222+
@did(Reg, dynamic(:wave, -1), notyettreated(11),
223+
xterms=[term(:male), fe(:hhidpn), fe(:wave)])
224+
@did(Reg, dynamic(:wave, -1), notyettreated(11),
225+
treatintterms=TermSet(:male), xterms=[fe(:hhidpn), fe(:wave)])
226+
@did(Reg, dynamic(:wave, -1), nevertreated(11),
222227
xterms=(fe(:wave)+fe(:hhidpn)))
223228
end
224-
@test r[1] == didspec(Reg, dynamic(:wave, -1), notyettreated(11), data=hrs,
229+
# Results might differ due to yxterms that include terms from other specs
230+
@test r[4] == didspec(Reg, dynamic(:wave, -1), notyettreated(11), data=hrs,
225231
yterm=term(:oop_spend), treatname=:wave_hosp, treatintterms=(),
226-
xterms=TermSet(fe(:wave), fe(:hhidpn)))()
232+
xterms=TermSet(term(:male), fe(:wave), fe(:hhidpn)))()
227233
end

0 commit comments

Comments
 (0)