Skip to content

Commit 672dd78

Browse files
committed
tests etc from other branch
1 parent bcdc001 commit 672dd78

File tree

3 files changed

+133
-11
lines changed

3 files changed

+133
-11
lines changed

src/partial_out.jl

+45-11
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,52 @@ function partial_out(
6565
fes, ids, ids_fes, formula = parse_fixedeffect(df, formula)
6666
has_fes = !isempty(fes)
6767

68-
6968
nobs = sum(esample)
7069
(nobs > 0) || throw("sample is empty")
70+
71+
if has_fes
72+
# in case some FixedEffect does not have interaction, remove the intercept
73+
if any(isa(fe.interaction, UnitWeights) for fe in fes)
74+
formula = FormulaTerm(formula.lhs, tuple(ConstantTerm(0), (t for t in eachterm(formula.rhs) if t!= ConstantTerm(1))...))
75+
has_fes_intercept = true
76+
end
77+
end
78+
79+
# Extract Y
80+
vars = unique(StatsModels.termvars(formula))
81+
subdf = Tables.columntable(disallowmissing!(df[esample, vars]))
82+
formula_y = FormulaTerm(ConstantTerm(0), (ConstantTerm(0), eachterm(formula.lhs)...))
83+
formula_y_schema = apply_schema(formula_y, schema(formula_y, subdf, contrasts), StatisticalModel)
84+
Y = modelmatrix(formula_y_schema, subdf)
85+
86+
# Extract X
87+
formula_x = FormulaTerm(ConstantTerm(0), formula.rhs)
88+
formula_x_schema = apply_schema(formula_x, schema(formula_x, subdf, contrasts), StatisticalModel)
89+
X = modelmatrix(formula_x_schema, subdf)
90+
91+
# added in PR #109 to handle cases where formula terms introduce missings
92+
# to be removed when fixed in StatsModels
93+
esample2 = trues(size(Y, 1))
94+
for c in eachcol(Y)
95+
esample2 .&= .!ismissing.(c)
96+
end
97+
if size(X, 2) > 0 # X can have zero rows if all regressors are fixed effects
98+
for c in eachcol(X)
99+
esample2 .&= .!ismissing.(c)
100+
end
101+
end
102+
103+
if any(!, esample2)
104+
esample = esample2
105+
Y = Y[esample,:]
106+
X = X[esample,:]
107+
nobs = sum(esample)
108+
end
109+
110+
# Disallow missings
111+
Y = convert(Matrix{Float64}, Y)
112+
X = convert(Matrix{Float64}, X)
113+
71114
# Compute weights
72115
if has_weights
73116
weights = Weights(convert(Vector{Float64}, view(df, esample, weights)))
@@ -85,14 +128,8 @@ function partial_out(
85128
fes = FixedEffect[fe[esample] for fe in fes]
86129
feM = AbstractFixedEffectSolver{double_precision ? Float64 : Float32}(fes, weights, Val{method})
87130
end
88-
131+
89132
# Compute residualized Y
90-
vars = unique(StatsModels.termvars(formula))
91-
subdf = Tables.columntable(disallowmissing!(df[esample, vars]))
92-
formula_y = FormulaTerm(ConstantTerm(0), (ConstantTerm(0), eachterm(formula.lhs)...))
93-
formula_y_schema = apply_schema(formula_y, schema(formula_y, subdf, contrasts), StatisticalModel)
94-
Y = convert(Matrix{Float64}, modelmatrix(formula_y_schema, subdf))
95-
96133
ynames = coefnames(formula_y_schema)[2]
97134
if !isa(ynames, Vector)
98135
ynames = [ynames]
@@ -107,9 +144,6 @@ function partial_out(
107144
end
108145

109146
# Compute residualized X
110-
formula_x = FormulaTerm(ConstantTerm(0), formula.rhs)
111-
formula_x_schema = apply_schema(formula_x, schema(formula_x, subdf, contrasts), StatisticalModel)
112-
X = convert(Matrix{Float64}, modelmatrix(formula_x_schema, subdf))
113147
if has_fes
114148
X, b, c = solve_residuals!(X, feM; maxiter = maxiter, tol = tol, progress_bar = false)
115149
append!(iterations, b)

test/Project.toml

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
66
FixedEffects = "c8885935-8500-56a7-9867-7708b20db0eb"
77
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
88
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
9+
StatsModels = "3eaba693-59b7-5ba5-a881-562e759f1c8d"
910
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
1011

1112
[compat]

test/fit.jl

+87
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,93 @@ df.Price_zero = copy(df.Price)
228228
df.Price_zero[1] = 0.0
229229
m = @formula Sales ~ log(Price_zero)
230230
@test_throws "Some observations for the regressor are infinite" reg(df, m)
231+
232+
# function that introduces missing
233+
using StatsModels: lag
234+
df.id1 = df.State
235+
df.y = df.Sales
236+
df.x1 = df.Price
237+
df.z1 = df.Pimin
238+
df.w = df.Pop
239+
df.x1_lagged = lag(df.x1)
240+
df.z1_lagged = lag(df.z1)
241+
df.y_lagged = lag(df.y)
242+
243+
244+
df.x1_m1 = Array{Union{Float64,Missing}}(copy(df.x1))
245+
df.x1_m1[end-20:end] .= missing
246+
247+
df.x1_m2 = Array{Union{Float64,Missing}}(copy(df.x1))
248+
df.x1_m2[5:10:end] .= missing
249+
250+
df.x1_lagged = lag(df.x1)
251+
df.z1_lagged = lag(df.z1)
252+
253+
254+
function test_lags(m0, m1, descr)
255+
@testset "$descr" begin
256+
x0 = reg(df, m0, Vcov.cluster(:id1), weights=:w)
257+
x1 = reg(df, m1, Vcov.cluster(:id1), weights=:w)
258+
259+
@test x0.residuals == x1.residuals
260+
@test x0.coef == x1.coef
261+
@test x0.nobs == x1.nobs
262+
@test x0.vcov == x1.vcov
263+
end
264+
end
265+
266+
function test_lags_broken(m0, m1, descr)
267+
@testset "$descr" begin
268+
x0 = reg(df, m0, Vcov.cluster(:id1), weights=:w)
269+
@test_throws ArgumentError reg(df, m1, Vcov.cluster(:id1), weights=:w)
270+
271+
#@test_ x0.coef != x1.coef
272+
#@test_ x0.vcov != x1.vcov
273+
end
274+
end
275+
276+
# NOTE: This is a "dumb" lag function,
277+
# it doesn't take into account time and group indices!
278+
@testset "missings from @formula" begin
279+
m0 = @formula y ~ x1_lagged + fe(id1)
280+
m1 = @formula y ~ lag(x1) + fe(id1)
281+
test_lags(m0, m1, "ols: _ ~ lag")
282+
283+
m0 = @formula y_lagged ~ x1 + fe(id1)
284+
m1 = @formula lag(y) ~ x1 + fe(id1)
285+
test_lags(m0, m1, "ols: lag ~ _")
286+
287+
m0 = @formula y ~ (x1_lagged ~ z1_lagged) + fe(id1)
288+
m1 = @formula y ~ (lag(x1) ~ lag(z1)) + fe(id1)
289+
test_lags(m0, m1, "iv: _ ~ (lag ~ lag)")
290+
291+
m0 = @formula y ~ (x1_lagged ~ z1) + fe(id1)
292+
m1 = @formula y ~ (lag(x1) ~ z1) + fe(id1)
293+
test_lags(m0, m1, "iv: _ ~ (lag ~ _)")
294+
295+
m0 = @formula y ~ (x1 ~ z1_lagged) + fe(id1)
296+
m1 = @formula y ~ (x1 ~ lag(z1)) + fe(id1)
297+
test_lags(m0, m1, "iv: _ ~ (_ ~ lag)")
298+
299+
# NOTE: The case where the df contains missings and the formula contains missings cannot be handled yet. The case with :x1_m1 would actually work, but the case with :x1_m2 would not. This because the missings in x1_m1 and x1_m2 are removed BEFORE the the lag is applied.
300+
301+
m0 = @formula y_lagged ~ x1_m1 + fe(id1)
302+
m1 = @formula lag(y) ~ x1_m1 + fe(id1)
303+
test_lags_broken(m0, m1, "ols: lag ~ _, with missings")
304+
305+
m0 = @formula y_lagged ~ x1_m2 + fe(id1)
306+
m1 = @formula lag(y) ~ x1_m2 + fe(id1)
307+
test_lags_broken(m0, m1, "ols: lag ~ _, with missings")
308+
309+
m0 = @formula y ~ (x1_m1 ~ z1_lagged) + fe(id1)
310+
m1 = @formula y ~ (x1_m1 ~ lag(z1)) + fe(id1)
311+
test_lags_broken(m0, m1, "iv: _ ~ (_ ~ lag), with missings")
312+
313+
m0 = @formula y ~ (x1_m2 ~ z1_lagged) + fe(id1)
314+
m1 = @formula y ~ (x1_m2 ~ lag(z1)) + fe(id1)
315+
test_lags_broken(m0, m1, "iv: _ ~ (_ ~ lag), with missings")
316+
end
317+
231318
##############################################################################
232319
##
233320
## collinearity

0 commit comments

Comments
 (0)