Skip to content

Commit 4845f6b

Browse files
paldaydmbates
andauthored
Improved handling of empty and rank-deficient fixed effects (#253)
* rankUpdate! for diagonal A * add warnings for rank-deficient FE * fix ordering of FE names in coeftable (undo pivot) * full FeMat support for empty FE * fix coefnames and add fixefnames for accessing cnames and respecting pivots * coefnames and fixefnames for GLMM Co-authored-by: Douglas Bates <[email protected]>
1 parent 2fe8b19 commit 4845f6b

File tree

8 files changed

+100
-10
lines changed

8 files changed

+100
-10
lines changed

src/MixedModels.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ export @formula,
7575
fit!,
7676
fitted,
7777
fixef,
78+
fixefnames,
7879
fulldummy,
7980
fnames,
8081
GHnorm,

src/femat.jl

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,24 @@ end
2020

2121
function FeMat(X::AbstractMatrix, cnms)
2222
T = eltype(X)
23-
ch = statscholesky(Symmetric(X'X))
24-
pivot = ch.piv
25-
Xp = all(pivot .== 1:size(X, 2)) ? X : X[:, ch.piv]
26-
FeMat{T,typeof(X)}(Xp, Xp, pivot, ch.rank, cnms[pivot])
23+
if size(X,2) > 0
24+
ch = statscholesky(Symmetric(X'X))
25+
pivot = ch.piv
26+
rank = ch.rank
27+
Xp = all(pivot .== 1:size(X, 2)) ? X : X[:, ch.piv]
28+
if rank < length(pivot)
29+
@warn "fixed-effects matrix is rank deficient"
30+
end
31+
else
32+
# although it doesn't take long for an empty matrix,
33+
# we can still skip the Cholesky step, which gets the rank
34+
# wrong anyway
35+
pivot = Int[]
36+
Xp = X
37+
rank = 0
38+
end
39+
# ch.rank is wrong for empty FE
40+
FeMat{T,typeof(X)}(Xp, Xp, pivot, rank, cnms[pivot])
2741
end
2842

2943
function reweight!(A::FeMat{T}, sqrtwts::Vector{T}) where {T}

src/generalizedlinearmixedmodel.jl

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

56+
StatsBase.coefnames(m::GeneralizedLinearMixedModel) = fixefnames(m, false)
57+
58+
5659
"""
5760
deviance(m::GeneralizedLinearMixedModel{T}, nAGQ=1)::T where {T}
5861
@@ -271,6 +274,10 @@ function fixef(m::GeneralizedLinearMixedModel{T}, permuted = true) where {T}
271274
invpermute!(v, piv)
272275
end
273276

277+
function fixefnames(m::GeneralizedLinearMixedModel{T}, permuted = true) where {T}
278+
fixefnames(m.LMM, permuted)
279+
end
280+
274281
GeneralizedLinearMixedModel(
275282
f::FormulaTerm,
276283
tbl,

src/linalg/rankUpdate.jl

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,20 @@ function rankUpdate!(
122122
C.diag .= β .* C.diag .+ α .* abs2.(A.diag)
123123
C
124124
end
125+
126+
127+
function rankUpdate!(
128+
C::HermOrSym{T,Matrix{T}},
129+
A::Diagonal{T},
130+
α::Number = true,
131+
β::Number = true,
132+
) where {T}
133+
m, n = size(A)
134+
m == size(C, 2) || throw(DimensionMismatch(""))
135+
adiag = A.diag
136+
cdata = C.data
137+
for (i, a) in zip(diagind(cdata), adiag)
138+
cdata[i] = β * cdata[i] + α * abs2(a)
139+
end
140+
C
141+
end

src/linearmixedmodel.jl

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -161,19 +161,21 @@ fit(
161161

162162
StatsBase.coef(m::MixedModel) = fixef(m, false)
163163

164-
βs(m::LinearMixedModel) = NamedTuple{(Symbol.(first(m.feterms).cnames)...,)}((fixef(m)...,))
164+
βs(m::LinearMixedModel) = NamedTuple{(Symbol.(fixefnames(m))...,)}((fixef(m)...,))
165165

166-
StatsBase.coefnames(m::LinearMixedModel) = first(m.feterms).cnames
166+
StatsBase.coefnames(m::LinearMixedModel) = fixefnames(m, false)
167167

168168
function StatsBase.coeftable(m::MixedModel)
169169
co = coef(m)
170170
se = stderror(m)
171171
z = co ./ se
172172
pvalue = ccdf.(Chisq(1), abs2.(z))
173+
names = coefnames(m)
174+
173175
CoefTable(
174176
hcat(co, se, z, pvalue),
175177
["Estimate", "Std.Error", "z value", "P(>|z|)"],
176-
first(m.feterms).cnames,
178+
names,
177179
4,
178180
)
179181
end
@@ -380,6 +382,28 @@ function fixef(m::LinearMixedModel{T}, permuted = true) where {T}
380382
val
381383
end
382384

385+
"""
386+
fixefnames(m::MixedModel, permuted=true)
387+
388+
Return the associated term names for fixed-effects parameter vector estimate of `m`.
389+
390+
If `permuted` is `true` the vector elements are permuted according to
391+
`first(m.feterms).piv` and truncated to the rank of that term.
392+
"""
393+
function fixefnames(m::LinearMixedModel{T}, permuted = true) where {T}
394+
Xtrm = first(m.feterms)
395+
val = copy(Xtrm.cnames)
396+
if !permuted
397+
piv = Xtrm.piv
398+
p = length(piv)
399+
invpermute!(val, piv)
400+
else
401+
val = val[1:Xtrm.rank]
402+
end
403+
404+
val
405+
end
406+
383407
"""
384408
fnames(m::MixedModel)
385409

test/linalg.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ end
4646
d2 = Diagonal(fill(2., 2))
4747
@test Diagonal(fill(5.,2)) == MixedModels.rankUpdate!(Diagonal(ones(2)), d2, 1.)
4848
@test Diagonal(fill(-3.,2)) == MixedModels.rankUpdate!(Diagonal(ones(2)), d2, -1.)
49+
50+
# when converting straight from diagonal to symmetric, the type is different
51+
@test Diagonal(fill(5.,2)) == MixedModels.rankUpdate!(Symmetric(Matrix(1. * I(2)), :L), d2)
4952
end
5053

5154
@testset "lmulλ!" begin

test/matrixterm.jl

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,12 @@ using LinearAlgebra, MixedModels, Random, Test
1515
wts = rand(MersenneTwister(123454321), 30)
1616
MixedModels.reweight!(trm, wts)
1717
@test mul!(prd, trm', trm)[ipiv[1], ipiv[1]] sum(abs2, wts)
18+
19+
# empty fixed effects
20+
trm = MixedModels.FeMat(ones(10,0), String[])
21+
@test size(trm) == (10, 0)
22+
@test length(trm) == 0
23+
@test size(trm') == (0, 10)
24+
@test eltype(trm) == Float64
25+
@test trm.rank == 0
1826
end

test/pls.jl

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -340,11 +340,27 @@ end
340340
end
341341

342342
@testset "Rank deficient" begin
343-
Random.seed!(0)
344-
x = rand(100)
345-
data = columntable((x = x, x2 = 1.5 .* x, y = rand(100), z = repeat('A':'T', 5)))
343+
rng = MersenneTwister(0)
344+
x = rand(rng, 100)
345+
data = columntable((x = x, x2 = 1.5 .* x, y = rand(rng, 100), z = repeat('A':'T', 5)))
346346
model = fit!(LinearMixedModel(@formula(y ~ x + x2 + (1|z)), data))
347347
@test length(fixef(model)) == 2
348348
@test rank(model) == 2
349349
@test length(coef(model)) == 3
350+
ct = coeftable(model)
351+
@test ct.rownms == ["(Intercept)", "x", "x2"]
352+
@test fixefnames(model, true) == ["(Intercept)", "x"]
353+
@test fixefnames(model, false) == ["(Intercept)", "x", "x2"]
354+
@test last(coef(model)) == -0.0
355+
356+
# check preserving of name ordering in coeftable and placement of
357+
# pivoted-out element
358+
fill!(data.x, 0)
359+
model2 = fit!(LinearMixedModel(@formula(y ~ x + x2 + (1|z)), data))
360+
ct = coeftable(model2)
361+
@test ct.rownms == ["(Intercept)", "x", "x2"]
362+
@test fixefnames(model2, true) == ["(Intercept)", "x2"]
363+
@test fixefnames(model2, false) == ["(Intercept)", "x", "x2"]
364+
@test coef(model2)[2] == -0.0
365+
@test last(fixef(model)) (last(fixef(model2)) * 1.5)
350366
end

0 commit comments

Comments
 (0)