Skip to content

Commit 2e431e4

Browse files
paldaydmbates
andauthored
LRT for GLMMs (#279)
* LRT for GLMMs and fix bug in safety check for LMM * pretty printing of LR test * doc update for named arg use_threads in replicate() Co-authored-by: Douglas Bates <[email protected]>
1 parent f7c2820 commit 2e431e4

File tree

6 files changed

+122
-18
lines changed

6 files changed

+122
-18
lines changed

src/generalizedlinearmixedmodel.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ end
9595

9696
StatsBase.deviance(m::GeneralizedLinearMixedModel) = deviance(m, m.optsum.nAGQ)
9797

98+
objective(m::GeneralizedLinearMixedModel) = deviance(m)
99+
98100
"""
99101
deviance!(m::GeneralizedLinearMixedModel, nAGQ=1)
100102

src/likelihoodratiotest.jl

Lines changed: 73 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,27 @@ Likeihood ratio test applied to a set of nested models.
5252
Note that nesting of the models is not checked. It is incumbent on the user to check this.
5353
"""
5454
function likelihoodratiotest(m::LinearMixedModel...)
55+
allequal(getproperty.(getproperty.(m,:optsum),:REML)) ||
56+
throw(ArgumentError("Models must all be fit with the same objective (i.e. alll ML or all REML)"))
5557
if any(getproperty.(getproperty.(m,:optsum),:REML))
56-
reduce(==,coefnames.(m)) ||
58+
allequal(coefnames.(m)) ||
5759
throw(ArgumentError("Likelihood-ratio tests for REML-fitted models are only valid when the fixed-effects specifications are identical"))
5860
end
61+
_likelihoodratiotest(m...)
62+
end
63+
64+
function likelihoodratiotest(m::GeneralizedLinearMixedModel...)
65+
# TODO: test that all models are fit with same fast/nAGQ option?
66+
glms = getproperty.(m,:resp);
67+
allequal(Distribution.(glms)) ||
68+
throw(ArgumentError("Models must be fit to the same distribution"))
69+
allequal(string.(Link.(glms))) ||
70+
throw(ArgumentError("Models must have the same link function"))
71+
72+
_likelihoodratiotest(m...)
73+
end
74+
75+
function _likelihoodratiotest(m::Vararg{T}) where T <: MixedModel
5976
m = collect(m) # change the tuple to an array
6077
dofs = dof.(m)
6178
formulas = String.(Symbol.(getproperty.(m,:formula)))
@@ -86,21 +103,62 @@ function Base.show(io::IO, lrt::LikelihoodRatioTest; digits=2)
86103
println(io, "Model Formulae")
87104

88105
for (i, f) in enumerate(lrt.formulas)
89-
println("$i: $f")
106+
println(io, "$i: $f")
90107
end
91-
cols = hcat(lrt.models.dof, lrt.models.deviance,
92-
_prepend_0(lrt.tests.deviancediff),
93-
_prepend_0(lrt.tests.dofdiff),
94-
_prepend_0(lrt.tests.pvalues))
95-
96-
ct = CoefTable(
97-
cols, # cols
98-
["model-dof", "deviance", "χ²", "χ²-dof", "P(>χ²)"], # colnms
99-
string.(1:length(lrt.formulas)), # rownms
100-
5, # pvalcol
101-
3 # teststatcol
102-
)
103-
show(io, ct)
108+
109+
# the following was adapted from StatsModels#162
110+
# from nalimilan
111+
Δdf = lrt.tests.dofdiff
112+
Δdev = lrt.tests.deviancediff
113+
114+
nc = 6
115+
nr = length(lrt.formulas)
116+
outrows = Matrix{String}(undef, nr+1, nc)
117+
118+
outrows[1, :] = ["",
119+
"model-dof",
120+
"deviance",
121+
"χ²",
122+
"χ²-dof",
123+
"P(>χ²)"] # colnms
124+
125+
126+
outrows[2, :] = ["[1]",
127+
@sprintf("%.0d", lrt.dof[1]),
128+
@sprintf("%.4f", lrt.deviance[1]),
129+
" "," ", " "]
130+
131+
for i in 2:nr
132+
outrows[i+1, :] = ["[$i]",
133+
@sprintf("%.0d", lrt.dof[i]),
134+
@sprintf("%.4f", lrt.deviance[i]),
135+
@sprintf("%.4f", Δdev[i-1]),
136+
@sprintf("%.0d", Δdf[i-1]),
137+
string(StatsBase.PValue(lrt.pvalues[i-1]))]
138+
end
139+
colwidths = length.(outrows)
140+
max_colwidths = [maximum(view(colwidths, :, i)) for i in 1:nc]
141+
totwidth = sum(max_colwidths) + 2*5
142+
143+
println(io, ''^totwidth)
144+
145+
for r in 1:nr+1
146+
for c in 1:nc
147+
cur_cell = outrows[r, c]
148+
cur_cell_len = length(cur_cell)
149+
150+
padding = " "^(max_colwidths[c]-cur_cell_len)
151+
if c > 1
152+
padding = " "*padding
153+
end
154+
155+
print(io, padding)
156+
print(io, cur_cell)
157+
end
158+
print(io, "\n")
159+
r == 1 && println(io, ''^totwidth)
160+
end
161+
print(io, ''^totwidth)
104162

105163
nothing
106164
end

src/utilities.jl

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,20 @@
1+
"""
2+
allequal(x::Array)
3+
allequal(x::Tuple)
4+
Return the equality of all elements of the array
5+
"""
6+
function allequal(x::Array; comparison=isequal)::Bool
7+
all(comparison.(first(x), x))
8+
end
9+
10+
function allequal(x::Tuple; comparison=isequal)::Bool
11+
all(comparison.(first(x), x))
12+
end
13+
14+
function allequal(x...; comparison=isequal)::Bool
15+
all(comparison.(first(x), x))
16+
end
17+
118
"""
219
average(a::T, b::T) where {T<:AbstractFloat}
320
@@ -88,7 +105,7 @@ function checkindprsk(k::Integer)
88105
end
89106

90107
"""
91-
replicate(f::Function, n::Integer, use_threads=false)
108+
replicate(f::Function, n::Integer; use_threads=false)
92109
93110
Return a vector of the values of `n` calls to `f()` - used in simulations where the value of `f` is stochastic.
94111

test/Project.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
BlockArrays = "8e7c35d0-a365-5155-bbbb-fb81a777f24e"
33
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
44
Feather = "becb17da-46f6-5d3c-ad1b-1c5fe96bc73c"
5+
GLM = "38e38edf-8417-5370-95a0-9cbb8c7f171a"
56
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
67
NamedArrays = "86f7a689-2022-50b4-a561-43c23ac3c673"
78
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"

test/likelihoodratiotest.jl

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using MixedModels, Test
2+
import GLM: ProbitLink
23

34
@testset "likelihoodratio test" begin
45
slp = MixedModels.dataset(:sleepstudy);
@@ -13,11 +14,35 @@ using MixedModels, Test
1314
@test sum(map(length,lrt.pvalues)) == 1
1415
@test sum(map(length,lrt.models)) == 4
1516
@test length(lrt.formulae) == 2
17+
show(IOBuffer(),lrt);
1618

19+
20+
# mix of REML and ML
1721
fm0 = fit(MixedModel,@formula(reaction ~ 1 + (1+days|subj)),slp, REML=true);
22+
@test_throws ArgumentError MixedModels.likelihoodratiotest(fm0,fm1)
23+
24+
# differing FE with REML
1825
fm1 = fit(MixedModel,@formula(reaction ~ 1 + days + (1+days|subj)),slp, REML=true);
1926
fm10 = fit(MixedModel,@formula(reaction ~ 1 + days + (1|subj)),slp, REML=true);
20-
2127
@test_throws ArgumentError MixedModels.likelihoodratiotest(fm0,fm1);
22-
lrt = MixedModels.likelihoodratiotest(fm1,fm10);
28+
29+
contra = MixedModels.dataset(:contra);
30+
gm0 = fit(MixedModel, @formula(use ~ 1+age+urban+livch+(1|urbdist)), contra, Bernoulli(), fast=true);
31+
gm1 = fit(MixedModel, @formula(use ~ 1+age+abs2(age)+urban+livch+(1|urbdist)), contra, Bernoulli(), fast=true);
32+
lrt = MixedModels.likelihoodratiotest(gm0,gm1);
33+
@test [deviance(gm0), deviance(gm1)] == lrt.deviance
34+
@test deviance(gm0) - deviance(gm1) == first(lrt.tests.deviancediff)
35+
@test first(lrt.tests.dofdiff) == 1
36+
@test sum(map(length,lrt.tests)) == 3
37+
@test sum(map(length,lrt.pvalues)) == 1
38+
@test sum(map(length,lrt.models)) == 4
39+
@test length(lrt.formulae) == 2
40+
41+
# mismatched links
42+
gm_probit = fit(MixedModel, @formula(use ~ 1+age+urban+livch+(1|urbdist)), contra, Bernoulli(), ProbitLink(), fast=true);
43+
@test_throws ArgumentError MixedModels.likelihoodratiotest(gm0,gm_probit)
44+
45+
# mismatched families
46+
gm_poisson = fit(MixedModel, @formula(use ~ 1+age+urban+livch+(1|urbdist)), contra, Poisson(), fast=true);
47+
@test_throws ArgumentError MixedModels.likelihoodratiotest(gm0,gm_poisson)
2348
end

test/runtests.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ include("pirls.jl")
1111
include("gausshermite.jl")
1212
include("fit.jl")
1313
include("missing.jl")
14+
include("likelihoodratiotest.jl")

0 commit comments

Comments
 (0)