Skip to content

Commit a2617ad

Browse files
authored
Add stderror! method, distinguish coef and fixef (#287)
Needs a documentation update but we will wait for #283 to land first.
1 parent 2e431e4 commit a2617ad

File tree

4 files changed

+114
-56
lines changed

4 files changed

+114
-56
lines changed

src/femat.jl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,11 @@ LinearAlgebra.mul!(R::StridedVecOrMat{T}, A::FeMat{T}, B::StridedVecOrMat{T}) wh
7777
LinearAlgebra.mul!(C, adjA::Adjoint{T,<:FeMat{T}}, B::FeMat{T}) where {T} =
7878
mul!(C, fullrankwtx(adjA.parent)', fullrankwtx(B))
7979

80+
"""
81+
isfullrank(A::FeMat)
82+
83+
Does `A` have full column rank?
84+
"""
85+
isfullrank(A::FeMat) = A.rank == length(A.piv)
86+
8087
(A::FeMat) = 0

src/generalizedlinearmixedmodel.jl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@ struct GeneralizedLinearMixedModel{T<:AbstractFloat} <: MixedModel{T}
5353
mult::Vector{T}
5454
end
5555

56-
StatsBase.coefnames(m::GeneralizedLinearMixedModel) = fixefnames(m, false)
56+
StatsBase.coefnames(m::GeneralizedLinearMixedModel) = coefnames(m.LMM)
57+
58+
StatsBase.coeftable(m::GeneralizedLinearMixedModel) = coeftable(m.LMM)
5759

5860

5961
"""

src/linearmixedmodel.jl

Lines changed: 89 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -159,15 +159,21 @@ fit(
159159
REML = REML,
160160
)
161161

162-
StatsBase.coef(m::MixedModel) = fixef(m, false)
162+
function StatsBase.coef(m::LinearMixedModel{T}) where {T}
163+
piv = fetrm(m).piv
164+
invpermute!(fixef!(similar(piv, T), m), piv)
165+
end
163166

164-
βs(m::LinearMixedModel) = NamedTuple{(Symbol.(fixefnames(m))...,)}((fixef(m)...,))
167+
βs(m::LinearMixedModel) = NamedTuple{(Symbol.(coefnames(m))...,)}(coef(m))
165168

166-
StatsBase.coefnames(m::LinearMixedModel) = fixefnames(m, false)
169+
function StatsBase.coefnames(m::LinearMixedModel)
170+
Xtrm = fetrm(m)
171+
invpermute!(copy(Xtrm.cnames), Xtrm.piv)
172+
end
167173

168-
function StatsBase.coeftable(m::MixedModel)
174+
function StatsBase.coeftable(m::LinearMixedModel)
169175
co = coef(m)
170-
se = stderror(m)
176+
se = stderror!(similar(co), m)
171177
z = co ./ se
172178
pvalue = ccdf.(Chisq(1), abs2.(z))
173179
names = coefnames(m)
@@ -285,13 +291,30 @@ function StatsBase.dof_residual(m::LinearMixedModel)::Int
285291
n - m.optsum.REML * p
286292
end
287293

294+
"""
295+
feind(m::MixedModel)
296+
297+
An internal utility to return the index in `m.allterms` of the fixed-effects term.
298+
"""
299+
feind(m::MixedModel) = findfirst(Base.Fix2(isa, FeMat), m.allterms)
300+
288301
"""
289302
feL(m::MixedModel)
290303
291304
Return the lower Cholesky factor for the fixed-effects parameters, as an `LowerTriangular`
292305
`p × p` matrix.
293306
"""
294-
feL(m::LinearMixedModel) = LowerTriangular(m.L.blocks[end-1, end-1])
307+
function feL(m::LinearMixedModel)
308+
k = feind(m)
309+
LowerTriangular(m.L.blocks[k, k])
310+
end
311+
312+
"""
313+
fetrm(m::LinearMixedModel)
314+
315+
Return the fixed-effects term from `m.allterms`
316+
"""
317+
fetrm(m) = m.allterms[feind(m)]
295318

296319
"""
297320
fit!(m::LinearMixedModel[; verbose::Bool=false, REML::Bool=false])
@@ -344,7 +367,8 @@ end
344367

345368
function fitted!(v::AbstractArray{T}, m::LinearMixedModel{T}) where {T}
346369
## FIXME: Create and use `effects(m) -> β, b` w/o calculating β twice
347-
vv = mul!(vec(v), first(m.feterms), fixef(m))
370+
Xtrm = fetrm(m)
371+
vv = mul!(vec(v), Xtrm, fixef!(similar(Xtrm.piv, T), m))
348372
for (rt, bb) in zip(m.reterms, ranef(m))
349373
unscaledre!(vv, rt, bb)
350374
end
@@ -356,53 +380,44 @@ StatsBase.fitted(m::LinearMixedModel{T}) where {T} = fitted!(Vector{T}(undef, no
356380
"""
357381
fixef!(v::Vector{T}, m::LinearMixedModel{T})
358382
359-
Overwrite `v` with the pivoted and, possibly, truncated fixed-effects coefficients of model `m`
360-
"""
361-
fixef!(v::AbstractVector{T}, m::LinearMixedModel{T}) where {T} =
362-
ldiv!(feL(m)', copyto!(v, m.L.blocks[end, end-1]))
383+
Overwrite `v` with the pivoted fixed-effects coefficients of model `m`
363384
385+
For full-rank models the length of `v` must be the rank of `X`. For rank-deficient models
386+
the length of `v` can be the rank of `X` or the number of columns of `X`. In the latter
387+
case the calculated coefficients are padded with -0.0 out to the number of columns.
364388
"""
365-
fixef(m::MixedModel, permuted=true)
366-
367-
Return the fixed-effects parameter vector estimate of `m`.
368-
369-
If `permuted` is `true` the vector elements are permuted according to
370-
`first(m.feterms).piv` and truncated to the rank of that term.
371-
"""
372-
function fixef(m::LinearMixedModel{T}, permuted = true) where {T}
373-
val = ldiv!(feL(m)', vec(copy(m.L.blocks[end, end-1])))
374-
if !permuted
375-
Xtrm = first(m.feterms)
376-
piv = Xtrm.piv
377-
p = length(piv)
378-
if Xtrm.rank < p
379-
val = copyto!(fill(-zero(T), p), val)
380-
end
381-
invpermute!(val, piv)
389+
function fixef!(v::AbstractVector{T}, m::LinearMixedModel{T}) where {T}
390+
Xtrm = fetrm(m)
391+
if isfullrank(Xtrm)
392+
ldiv!(feL(m)', copyto!(v, m.L.blocks[end, end-1]))
393+
else
394+
ldiv!(
395+
feL(m)',
396+
view(copyto!(fill!(v, -zero(T)), m.L.blocks[end, end-1]), 1:(Xtrm.rank)),
397+
)
382398
end
383-
val
399+
v
384400
end
385401

386402
"""
387-
fixefnames(m::MixedModel, permuted=true)
403+
fixef(m::MixedModel)
388404
389-
Return the associated term names for fixed-effects parameter vector estimate of `m`.
405+
Return the fixed-effects parameter vector estimate of `m`.
390406
391-
If `permuted` is `true` the vector elements are permuted according to
392-
`first(m.feterms).piv` and truncated to the rank of that term.
407+
In the rank-deficient case the truncated parameter vector, of length `rank(m)` is returned.
408+
This is unlike `coef` which always returns a vector whose length matches the number of
409+
columns in `X`.
393410
"""
394-
function fixefnames(m::LinearMixedModel{T}, permuted = true) where {T}
395-
Xtrm = first(m.feterms)
396-
val = copy(Xtrm.cnames)
397-
if !permuted
398-
piv = Xtrm.piv
399-
p = length(piv)
400-
invpermute!(val, piv)
401-
else
402-
val = val[1:Xtrm.rank]
403-
end
411+
fixef(m::LinearMixedModel{T}) where {T} = fixef!(Vector{T}(undef, fetrm(m).rank), m)
412+
413+
"""
414+
fixefnames(m::MixedModel)
404415
405-
val
416+
Return a (permuted and truncated in the rank-deficient case) vector of coefficient names.
417+
"""
418+
function fixefnames(m::LinearMixedModel{T}) where {T}
419+
Xtrm = fetrm(m)
420+
Xtrm.cnames[1:Xtrm.rank]
406421
end
407422

408423
"""
@@ -788,6 +803,33 @@ function Statistics.std(m::LinearMixedModel)
788803
isfinite(s) ? rmul!(push!(rl, [1.0]), s) : rl
789804
end
790805

806+
"""
807+
stderror!(v::AbstractVector, m::LinearMixedModel)
808+
809+
Overwrite `v` with the standard errors of the fixed-effects coefficients in `m`
810+
811+
The length of `v` should be the total number of coefficients (i.e. `length(coef(m))`).
812+
When the model matrix is rank-deficient the coefficients forced to `-0.0` have an
813+
undefined (i.e. `NaN`) standard error.
814+
"""
815+
function stderror!(v::AbstractVector{T}, m::LinearMixedModel{T}) where {T}
816+
L = feL(m)
817+
scr = Vector{T}(undef, size(L, 2))
818+
s = sdest(m)
819+
fill!(v, zero(T) / zero(T)) # initialize to appropriate NaN for rank-deficient case
820+
for i in eachindex(scr)
821+
fill!(scr, false)
822+
scr[i] = true
823+
v[i] = s * norm(ldiv!(L, scr))
824+
end
825+
invpermute!(v, fetrm(m).piv)
826+
v
827+
end
828+
829+
function StatsBase.stderror(m::LinearMixedModel{T}) where {T}
830+
stderror!(similar(fetrm(m).piv, T), m)
831+
end
832+
791833
"""
792834
updateA!(m::LinearMixedModel)
793835
@@ -857,14 +899,15 @@ end
857899
Returns the estimate of σ², the variance of the conditional distribution of Y given B.
858900
"""
859901
varest(m::LinearMixedModel) = pwrss(m) / dof_residual(m)
902+
860903
"""
861904
vcov(m::LinearMixedModel)
862905
863906
Returns the variance-covariance matrix of the fixed effects.
864907
If `corr=true`, then correlation of fixed effects is returned instead.
865908
"""
866909
function StatsBase.vcov(m::LinearMixedModel{T}; corr=false) where {T}
867-
Xtrm = first(m.feterms)
910+
Xtrm = fetrm(m)
868911
iperm = invperm(Xtrm.piv)
869912
p = length(iperm)
870913
r = Xtrm.rank
@@ -880,7 +923,7 @@ function StatsBase.vcov(m::LinearMixedModel{T}; corr=false) where {T}
880923
vv = covmat[iperm, iperm]
881924
end
882925

883-
corr ? StatsBase.cov2cor!(vv,stderror(m)) : vv
926+
corr ? StatsBase.cov2cor!(vv, stderror(m)) : vv
884927
end
885928

886929
"""

test/pls.jl

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ end
165165
@test length(MixedModels.PCA(fm1)) == 3
166166
end
167167

168-
fm2 = fit!(LinearMixedModel(@formula(y ~ 1 + service*dept + (1|s) + (1|d)), insteval))
168+
fm2 = fit(MixedModel, @formula(y ~ 1 + service*dept + (1|s) + (1|d)), insteval)
169169
@test objective(fm2) 237585.5534151694 atol=0.001
170170
@test size(fm2) == (73421, 28, 4100, 2)
171171
end
@@ -360,29 +360,35 @@ end
360360
end
361361

362362
@testset "Rank deficient" begin
363-
rng = MersenneTwister(0)
364-
x = rand(rng, 100)
363+
rng = MersenneTwister(0);
364+
x = rand(rng, 100);
365365
data = columntable((x = x, x2 = 1.5 .* x, y = rand(rng, 100), z = repeat('A':'T', 5)))
366-
model = fit!(LinearMixedModel(@formula(y ~ x + x2 + (1|z)), data))
366+
model = fit(MixedModel, @formula(y ~ x + x2 + (1|z)), data)
367367
@test length(fixef(model)) == 2
368368
@test rank(model) == 2
369369
@test length(coef(model)) == 3
370370
ct = coeftable(model)
371371
@test ct.rownms == ["(Intercept)", "x", "x2"]
372-
@test fixefnames(model, true) == ["(Intercept)", "x"]
373-
@test fixefnames(model, false) == ["(Intercept)", "x", "x2"]
372+
@test fixefnames(model) == ["(Intercept)", "x"]
373+
@test coefnames(model) == ["(Intercept)", "x", "x2"]
374374
@test last(coef(model)) == -0.0
375+
stde = MixedModels.stderror!(zeros(3), model)
376+
@test isnan(last(stde))
377+
375378

376379
# check preserving of name ordering in coeftable and placement of
377380
# pivoted-out element
378381
fill!(data.x, 0)
379-
model2 = fit!(LinearMixedModel(@formula(y ~ x + x2 + (1|z)), data))
382+
model2 = fit(MixedModel, @formula(y ~ x + x2 + (1|z)), data)
380383
ct = coeftable(model2)
381384
@test ct.rownms == ["(Intercept)", "x", "x2"]
382-
@test fixefnames(model2, true) == ["(Intercept)", "x2"]
383-
@test fixefnames(model2, false) == ["(Intercept)", "x", "x2"]
385+
@test fixefnames(model2) == ["(Intercept)", "x2"]
386+
@test coefnames(model2) == ["(Intercept)", "x", "x2"]
384387
@test coef(model2)[2] == -0.0
385388
@test last(fixef(model)) (last(fixef(model2)) * 1.5)
389+
stde2 = MixedModels.stderror!(zeros(3), model2)
390+
@test isnan(stde2[2])
391+
386392
end
387393

388394
@testset "coeftable" begin

0 commit comments

Comments
 (0)