diff --git a/.travis.yml b/.travis.yml index 076ece6c2..fd3ef2a76 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ os: - linux - osx julia: - - 1.2 - 1.3 codecov: true notifications: @@ -15,7 +14,7 @@ jobs: include: - stage: "Documentation" os: linux - julia: 1.2 + julia: 1.3 script: - julia --project=docs/ -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()' diff --git a/Project.toml b/Project.toml index 7fc31c21c..37561be09 100644 --- a/Project.toml +++ b/Project.toml @@ -39,7 +39,7 @@ StatsFuns = "0.8, 0.9" StatsModels = "0.6" Tables = "0.2" TypedTables = "1" -julia = "1.2" +julia = "1.3" [extras] DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" diff --git a/README.md b/README.md index c3ea675d6..fafcc6619 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ |**Documentation**|**Citation**|**Build Status**|**Code Coverage**| |:-:|:-:|:-:|:-:| -|[![][docs-stable-img]][docs-stable-url] [![][docs-latest-img]][docs-latest-url] | [![][doi-img]][doi-url] | [![][travis-img]][travis-url] [![][appveyor-img]][appveyor-url] | [![][coveralls-img]][coveralls-url] [![][codecov-img]][codecov-url]| +|[![][docs-stable-img]][docs-stable-url] [![][docs-latest-img]][docs-latest-url] | [![][doi-img]][doi-url] | [![][travis-img]][travis-url] [![][appveyor-img]][appveyor-url] | [![][codecov-img]][codecov-url]| [doi-img]: https://zenodo.org/badge/9106942.svg [doi-url]: https://zenodo.org/badge/latestdoi/9106942 @@ -19,9 +19,6 @@ [appveyor-img]: https://ci.appveyor.com/api/projects/status/github/JuliaStats/MixedModels.jl?svg=true [appveyor-url]: https://ci.appveyor.com/project/JuliaStats/mixedmodels-jl -[coveralls-img]: https://coveralls.io/repos/github/JuliaStats/MixedModels.jl/badge.svg?branch=master -[coveralls-url]: https://coveralls.io/github/JuliaStats/MixedModels.jl?branch=master - [codecov-img]: https://codecov.io/github/JuliaStats/MixedModels.jl/badge.svg?branch=master [codecov-url]: https://codecov.io/github/JuliaStats/MixedModels.jl?branch=master diff --git a/appveyor.yml b/appveyor.yml index b369981a7..a8c6e3ef7 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,6 +1,6 @@ environment: matrix: - - julia_version: 1.2 + - julia_version: 1.3 - julia_version: nightly platform: diff --git a/src/MixedModels.jl b/src/MixedModels.jl index 757662498..f2f40e246 100644 --- a/src/MixedModels.jl +++ b/src/MixedModels.jl @@ -116,7 +116,6 @@ include("randomeffectsterm.jl") include("linearmixedmodel.jl") include("gausshermite.jl") include("generalizedlinearmixedmodel.jl") -include("mixed.jl") include("linalg/statschol.jl") include("linalg/cholUnblocked.jl") include("linalg/rankUpdate.jl") diff --git a/src/arraytypes.jl b/src/arraytypes.jl index 1c487de41..a8d3e94ea 100644 --- a/src/arraytypes.jl +++ b/src/arraytypes.jl @@ -1,21 +1,21 @@ -using StaticArrays, SparseArrays, LinearAlgebra - """ UniformBlockDiagonal{T} Homogeneous block diagonal matrices. `k` diagonal blocks each of size `m×m` """ struct UniformBlockDiagonal{T} <: AbstractMatrix{T} - data::Array{T, 3} + data::Array{T,3} facevec::Vector{SubArray{T,2,Array{T,3}}} end function UniformBlockDiagonal(dat::Array{T,3}) where {T} - UniformBlockDiagonal(dat, - SubArray{T,2,Array{T,3}}[view(dat,:,:,i) for i in 1:size(dat, 3)]) + UniformBlockDiagonal( + dat, + SubArray{T,2,Array{T,3}}[view(dat, :, :, i) for i = 1:size(dat, 3)], + ) end -function Base.copyto!(dest::UniformBlockDiagonal{T}, src::UniformBlockDiagonal{T}) where{T} +function Base.copyto!(dest::UniformBlockDiagonal{T}, src::UniformBlockDiagonal{T}) where {T} sdat = src.data ddat = dest.data size(ddat) == size(sdat) || throw(DimensionMismatch("")) @@ -28,13 +28,13 @@ function Base.copyto!(dest::Matrix{T}, src::UniformBlockDiagonal{T}) where {T} fill!(dest, zero(T)) sdat = src.data m, n, l = size(sdat) - for k in 1:l + for k = 1:l ioffset = (k - 1) * m joffset = (k - 1) * n - for j in 1:n + for j = 1:n jind = joffset + j - for i in 1:m - dest[ioffset + i, jind] = sdat[i,j,k] + for i = 1:m + dest[ioffset+i, jind] = sdat[i, j, k] end end end @@ -45,7 +45,7 @@ function Base.getindex(A::UniformBlockDiagonal{T}, i::Int, j::Int) where {T} Ad = A.data m, n, l = size(Ad) (0 < i ≤ l * m && 0 < j ≤ l * n) || - throw(IndexError("attempt to access $(l*m) × $(l*n) array at index [$i, $j]")) + throw(IndexError("attempt to access $(l*m) × $(l*n) array at index [$i, $j]")) iblk, ioffset = divrem(i - 1, m) jblk, joffset = divrem(j - 1, n) iblk == jblk ? Ad[ioffset+1, joffset+1, iblk+1] : zero(T) @@ -54,7 +54,7 @@ end function LinearAlgebra.Matrix(A::UniformBlockDiagonal{T}) where {T} Ad = A.data m, n, l = size(Ad) - mat = zeros(T, (m*l, n*l)) + mat = zeros(T, (m * l, n * l)) @inbounds for k = 0:(l-1) kp1 = k + 1 km = k * m @@ -62,7 +62,7 @@ function LinearAlgebra.Matrix(A::UniformBlockDiagonal{T}) where {T} for j = 1:n knpj = kn + j for i = 1:m - mat[km + i, knpj] = Ad[i, j, kp1] + mat[km+i, knpj] = Ad[i, j, kp1] end end end @@ -81,7 +81,10 @@ A `SparseMatrixCSC` whose nonzeros form blocks of rows or columns or both. # Members * `cscmat`: `SparseMatrixCSC{Tv, Int32}` representation for general calculations -* `blkpattern`: `SparseMatrixCSC{Bool,Int32}` pattern of blocks of size (S,P) +* `nzasmat`: nonzeros of `cscmat` as a dense matrix +* `colblkptr`: pattern of blocks of columns + +The only time these are created are as products of `ReMat`s. """ mutable struct BlockedSparse{T,S,P} <: AbstractMatrix{T} cscmat::SparseMatrixCSC{T,Int32} @@ -102,7 +105,8 @@ SparseArrays.sparse(A::BlockedSparse) = A.cscmat SparseArrays.nnz(A::BlockedSparse) = nnz(A.cscmat) function Base.copyto!(L::BlockedSparse{T}, A::SparseMatrixCSC{T}) where {T} - size(L) == size(A) && nnz(L) == nnz(A) || throw(DimensionMismatch("size(L) ≠ size(A) or nnz(L) ≠ nnz(A")) + size(L) == size(A) && nnz(L) == nnz(A) || + throw(DimensionMismatch("size(L) ≠ size(A) or nnz(L) ≠ nnz(A")) copyto!(nonzeros(L.cscmat), nonzeros(A)) L end diff --git a/src/femat.jl b/src/femat.jl index 1a36cf637..e0c1d8aec 100644 --- a/src/femat.jl +++ b/src/femat.jl @@ -55,7 +55,7 @@ Base.size(A::FeMat, i) = size(A.wtx, i) Base.copyto!(A::FeMat{T}, src::AbstractVecOrMat{T}) where {T} = copyto!(A.x, src) *(adjA::Adjoint{T,<:FeMat{T}}, B::FeMat{T}) where {T} = - fullrankwtx(adjA.parent)'fullrankwtx(B) + fullrankwtx(adjA.parent)' * fullrankwtx(B) LinearAlgebra.mul!(R::StridedVecOrMat{T}, A::FeMat{T}, B::StridedVecOrMat{T}) where {T} = mul!(R, A.x, B) diff --git a/src/gausshermite.jl b/src/gausshermite.jl index 191e2f431..c51497485 100644 --- a/src/gausshermite.jl +++ b/src/gausshermite.jl @@ -1,5 +1,3 @@ -using StaticArrays, LinearAlgebra - """ GaussHermiteQuadrature @@ -28,8 +26,8 @@ gn5 = GHnorm(5) sum(@. abs2(σ*gn5.z + μ)*gn5.w) # E[X^2] where X ∼ N(μ, σ) ``` -For evaluation of the log-likelihood of a GLMM the integral to evaluate for each level of the grouping -factor is approximately Gaussian shaped. +For evaluation of the log-likelihood of a GLMM the integral to evaluate for each level of +the grouping factor is approximately Gaussian shaped. """ GaussHermiteQuadrature """ @@ -40,18 +38,20 @@ A struct with 2 SVector{K,Float64} members - `wt`: Gauss-Hermite weights normalized to sum to unity """ struct GaussHermiteNormalized{K} - z::SVector{K, Float64} + z::SVector{K,Float64} w::SVector{K,Float64} end function GaussHermiteNormalized(k::Integer) ev = eigen(SymTridiagonal(zeros(k), sqrt.(1:k-1))) - w = abs2.(ev.vectors[1,:]) + w = abs2.(ev.vectors[1, :]) GaussHermiteNormalized( SVector{k}((ev.values .- reverse(ev.values)) ./ 2), - SVector{k}(LinearAlgebra.normalize((w .+ reverse(w)) ./ 2, 1))) + SVector{k}(LinearAlgebra.normalize((w .+ reverse(w)) ./ 2, 1)), + ) end -Base.iterate(g::GaussHermiteNormalized{K}, i=1) where {K} = (K < i ? nothing : ((z = g.z[i], w = g.w[i]), i + 1)) +Base.iterate(g::GaussHermiteNormalized{K}, i = 1) where {K} = + (K < i ? nothing : ((z = g.z[i], w = g.w[i]), i + 1)) Base.length(g::GaussHermiteNormalized{K}) where {K} = K @@ -61,10 +61,13 @@ Base.length(g::GaussHermiteNormalized{K}) where {K} = K Memoized values of `GHnorm`{@ref} stored as a `Dict{Int,GaussHermiteNormalized}` """ const GHnormd = Dict{Int,GaussHermiteNormalized}( - 1 => GaussHermiteNormalized(SVector{1}(0.),SVector{1}(1.)), - 2 => GaussHermiteNormalized(SVector{2}(-1.0,1.0),SVector{2}(0.5,0.5)), - 3 => GaussHermiteNormalized(SVector{3}(-sqrt(3),0.,sqrt(3)),SVector{3}(1/6,2/3,1/6)) - ) + 1 => GaussHermiteNormalized(SVector{1}(0.0), SVector{1}(1.0)), + 2 => GaussHermiteNormalized(SVector{2}(-1.0, 1.0), SVector{2}(0.5, 0.5)), + 3 => GaussHermiteNormalized( + SVector{3}(-sqrt(3), 0.0, sqrt(3)), + SVector{3}(1 / 6, 2 / 3, 1 / 6), + ), +) """ GHnorm(k::Int) @@ -74,7 +77,8 @@ Return the (unique) GaussHermiteNormalized{k} object. The function values are stored (memoized) when first evaluated. Subsequent evaluations for the same `k` have very low overhead. """ -GHnorm(k::Int) = get!(GHnormd, k) do - GaussHermiteNormalized(k) -end +GHnorm(k::Int) = + get!(GHnormd, k) do + GaussHermiteNormalized(k) + end GHnorm(k) = GHnorm(Int(k)) diff --git a/src/generalizedlinearmixedmodel.jl b/src/generalizedlinearmixedmodel.jl index b50dbdbbe..adb11e57b 100644 --- a/src/generalizedlinearmixedmodel.jl +++ b/src/generalizedlinearmixedmodel.jl @@ -36,7 +36,7 @@ In addition to the fieldnames, the following names are also accessible through t - `y`: response vector """ -struct GeneralizedLinearMixedModel{T <: AbstractFloat} <: MixedModel{T} +struct GeneralizedLinearMixedModel{T<:AbstractFloat} <: MixedModel{T} LMM::LinearMixedModel{T} β::Vector{T} β₀::Vector{T} @@ -63,14 +63,14 @@ If the distribution `D` does not have a scale parameter the Laplace approximatio is the squared length of the conditional modes, `u`, plus the determinant of `Λ'Z'WZΛ + I`, plus the sum of the squared deviance residuals. """ -function StatsBase.deviance(m::GeneralizedLinearMixedModel{T}, nAGQ=1) where {T} +function StatsBase.deviance(m::GeneralizedLinearMixedModel{T}, nAGQ = 1) where {T} nAGQ == 1 && return T(sum(m.resp.devresid) + logdet(m) + sum(u -> sum(abs2, u), m.u)) u = vec(first(m.u)) u₀ = vec(first(m.u₀)) copyto!(u₀, u) ra = RaggedArray(m.resp.devresid, first(m.LMM.reterms).refs) devc0 = sum!(map!(abs2, m.devc0, u), ra) # the deviance components at z = 0 - sd = map!(inv, m.sd, m.LMM.L[Block(1,1)].diag) + sd = map!(inv, m.sd, m.LMM.L[Block(1, 1)].diag) mult = fill!(m.mult, 0) devc = m.devc for (z, w) in GHnorm(nAGQ) @@ -81,7 +81,7 @@ function StatsBase.deviance(m::GeneralizedLinearMixedModel{T}, nAGQ=1) where {T} @. u = u₀ + z * sd updateη!(m) sum!(map!(abs2, devc, u), ra) - @. mult += exp((abs2(z) + devc0 - devc)/2)*w + @. mult += exp((abs2(z) + devc0 - devc) / 2) * w end end end @@ -98,14 +98,14 @@ deviance!(m::GeneralizedLinearMixedModel, nAGQ=1) Update `m.η`, `m.μ`, etc., install the working response and working weights in `m.LMM`, update `m.LMM.A` and `m.LMM.R`, then evaluate the [`deviance`](@ref). """ -function deviance!(m::GeneralizedLinearMixedModel, nAGQ=1) +function deviance!(m::GeneralizedLinearMixedModel, nAGQ = 1) updateη!(m) GLM.wrkresp!(m.LMM.y, m.resp) reweight!(m.LMM, m.resp.wrkwt) deviance(m, nAGQ) end -GLM.dispersion(m::GeneralizedLinearMixedModel, sqr::Bool=false) = +GLM.dispersion(m::GeneralizedLinearMixedModel, sqr::Bool = false) = dispersion(m.resp, dof_residual(m), sqr) GLM.dispersion_parameter(m::GeneralizedLinearMixedModel) = dispersion_parameter(m.resp.d) @@ -114,36 +114,87 @@ function StatsBase.dof(m::GeneralizedLinearMixedModel) length(m.β) + length(m.θ) + GLM.dispersion_parameter(m.resp.d) end -fit(::Type{GeneralizedLinearMixedModel}, f::FormulaTerm, tbl, - d::Distribution = Normal(), l::Link = canonicallink(d); +fit( + ::Type{GeneralizedLinearMixedModel}, + f::FormulaTerm, + tbl, + d::Distribution = Normal(), + l::Link = canonicallink(d); wts = [], contrasts = Dict{Symbol,Any}(), offset = [], verbose::Bool = false, fast::Bool = false, - nAGQ::Integer = 1) = - fit(GeneralizedLinearMixedModel, f, columntable(tbl), d, l, + nAGQ::Integer = 1, +) = fit( + GeneralizedLinearMixedModel, + f, + columntable(tbl), + d, + l, + wts = wts, + offset = offset, + contrasts = contrasts, + verbose = verbose, + fast = fast, + nAGQ = nAGQ, +) + +fit( + ::Type{GeneralizedLinearMixedModel}, + f::FormulaTerm, + tbl::Tables.ColumnTable, + d::Distribution, + l::Link = canonicallink(d); + wts = [], + contrasts = Dict{Symbol,Any}(), + offset = [], + verbose::Bool = false, + fast::Bool = false, + nAGQ::Integer = 1, +) = fit!( + GeneralizedLinearMixedModel( + f, + tbl, + d, + l, wts = wts, offset = offset, contrasts = contrasts, - verbose = verbose, - fast = fast, - nAGQ = nAGQ) -fit(::Type{GeneralizedLinearMixedModel}, f::FormulaTerm, tbl::Tables.ColumnTable, - d::Distribution, l::Link = canonicallink(d); + ), + verbose = verbose, + fast = fast, + nAGQ = nAGQ, +) + + +fit( + ::Type{MixedModel}, + f::FormulaTerm, + tbl, + d::Distribution, + l::Link = canonicallink(d); wts = [], contrasts = Dict{Symbol,Any}(), offset = [], verbose::Bool = false, + REML::Bool = false, fast::Bool = false, - nAGQ::Integer = 1) = - fit!(GeneralizedLinearMixedModel(f, tbl, d, l, - wts = wts, - offset = offset, - contrasts = contrasts), - verbose = verbose, - fast = fast, - nAGQ = nAGQ) + nAGQ::Integer = 1, +) = fit( + GeneralizedLinearMixedModel, + f, + tbl, + d, + l, + wts = wts, + contrasts = contrasts, + offset = offset, + verbose = verbose, + fast = fast, + nAGQ = nAGQ, +) + """ fit!(m::GeneralizedLinearMixedModel[, verbose = false, fast = false, nAGQ = 1]) @@ -153,19 +204,19 @@ When `fast` is `true` a potentially much faster but slightly less accurate algor which `pirls!` optimizes both the random effects and the fixed-effects parameters, is used. """ -function fit!(m::GeneralizedLinearMixedModel{T}; - verbose::Bool = false, - fast::Bool = false, - nAGQ::Integer = 1) where {T} +function fit!( + m::GeneralizedLinearMixedModel{T}; + verbose::Bool = false, + fast::Bool = false, + nAGQ::Integer = 1, +) where {T} β = m.β lm = m.LMM optsum = lm.optsum if !fast - fit!(m, verbose=verbose, fast=true, nAGQ=nAGQ) optsum.lowerbd = vcat(fill!(similar(β), T(-Inf)), optsum.lowerbd) optsum.initial = vcat(β, m.θ) optsum.final = copy(optsum.initial) - optsum.initial_step = vcat(stderror(m) ./ 3, min.(T(0.05), m.θ ./ 4)) end setpar! = fast ? setθ! : setβθ! feval = 0 @@ -211,7 +262,7 @@ end StatsBase.fitted(m::GeneralizedLinearMixedModel) = m.resp.mu -function fixef(m::GeneralizedLinearMixedModel{T}, permuted=true) where {T} +function fixef(m::GeneralizedLinearMixedModel{T}, permuted = true) where {T} permuted && return m.β Xtrm = first(m.LMM.feterms) piv = Xtrm.piv @@ -219,43 +270,64 @@ function fixef(m::GeneralizedLinearMixedModel{T}, permuted=true) where {T} copyto!(view(v, 1:Xtrm.rank), m.β) invpermute!(v, piv) end -GeneralizedLinearMixedModel(f::FormulaTerm, tbl, - d::Distribution, - l::Link = canonicallink(d); - wts = [], - offset = [], - contrasts = Dict{Symbol,Any}()) = - GeneralizedLinearMixedModel(f, Tables.columntable(tbl), - d, l; - wts = [], - offset = [], - contrasts = Dict{Symbol,Any}()) -GeneralizedLinearMixedModel(f::FormulaTerm, tbl::Tables.ColumnTable, - d::Normal, - l::IdentityLink; - wts = [], - offset = [], - contrasts = Dict{Symbol,Any}()) = - throw(ArgumentError("use LinearMixedModel for Normal distribution with IdentityLink")) -function GeneralizedLinearMixedModel(f::FormulaTerm, tbl::Tables.ColumnTable, - d::Distribution, - l::Link = canonicallink(d); - wts = [], - offset = [], - contrasts = Dict{Symbol,Any}()) + +GeneralizedLinearMixedModel( + f::FormulaTerm, + tbl, + d::Distribution, + l::Link = canonicallink(d); + wts = [], + offset = [], + contrasts = Dict{Symbol,Any}(), +) = GeneralizedLinearMixedModel( + f, + Tables.columntable(tbl), + d, + l; + wts = [], + offset = [], + contrasts = Dict{Symbol,Any}(), +) + +GeneralizedLinearMixedModel( + f::FormulaTerm, + tbl::Tables.ColumnTable, + d::Normal, + l::IdentityLink; + wts = [], + offset = [], + contrasts = Dict{Symbol,Any}(), +) = throw(ArgumentError("use LinearMixedModel for Normal distribution with IdentityLink")) + +function GeneralizedLinearMixedModel( + f::FormulaTerm, + tbl::Tables.ColumnTable, + d::Distribution, + l::Link = canonicallink(d); + wts = [], + offset = [], + contrasts = Dict{Symbol,Any}(), +) if isa(d, Binomial) && isempty(wts) d = Bernoulli() end (isa(d, Normal) && isa(l, IdentityLink)) && - throw(ArgumentError("use LinearMixedModel for Normal distribution with IdentityLink")) + throw(ArgumentError("use LinearMixedModel for Normal distribution with IdentityLink")) LMM = LinearMixedModel(f, tbl, contrasts = contrasts; wts = wts) y = copy(LMM.y) # the sqrtwts field must be the correct length and type but we don't know those # until after the model is constructed if wt is empty. Because a LinearMixedModel # type is immutable, another one must be created. if isempty(wts) - LMM = LinearMixedModel(LMM.formula, LMM.reterms, LMM.feterms, fill!(similar(y), 1), - LMM.A, LMM.L, LMM.optsum) + LMM = LinearMixedModel( + LMM.formula, + LMM.reterms, + LMM.feterms, + fill!(similar(y), 1), + LMM.A, + LMM.L, + LMM.optsum, + ) end updateL!(LMM) # fit a glm to the fixed-effects only - awkward syntax is to by-pass a test @@ -266,9 +338,22 @@ function GeneralizedLinearMixedModel(f::FormulaTerm, tbl::Tables.ColumnTable, # it is empty unless there is a single random-effects term vv = length(u) == 1 ? vec(first(u)) : similar(y, 0) - res = GeneralizedLinearMixedModel(LMM, β, copy(β), LMM.θ, copy.(u), u, - zero.(u), gl.rr, similar(y), oftype(y, wts), similar(vv), - similar(vv), similar(vv), similar(vv)) + res = GeneralizedLinearMixedModel( + LMM, + β, + copy(β), + LMM.θ, + copy.(u), + u, + zero.(u), + gl.rr, + similar(y), + oftype(y, wts), + similar(vv), + similar(vv), + similar(vv), + similar(vv), + ) deviance!(res, 1) res end @@ -303,16 +388,30 @@ function StatsBase.loglikelihood(m::GeneralizedLinearMixedModel{T}) where {T} accum += logpdf(D(μ), y) end end - accum - (mapreduce(u -> sum(abs2, u), + , m.u) + logdet(m)) / 2 + accum - (mapreduce(u -> sum(abs2, u), +, m.u) + logdet(m)) / 2 end StatsBase.nobs(m::GeneralizedLinearMixedModel) = length(m.η) StatsBase.predict(m::GeneralizedLinearMixedModel) = fitted(m) -Base.propertynames(m::GeneralizedLinearMixedModel, private=false) = - (:A, :L, :theta, :beta, :coef, :λ, :lambda, :σ, :sigma, :X, :y, :lowerbd, :σρs, :σs, - fieldnames(typeof(m))...) +Base.propertynames(m::GeneralizedLinearMixedModel, private = false) = ( + :A, + :L, + :theta, + :beta, + :coef, + :λ, + :lambda, + :σ, + :sigma, + :X, + :y, + :lowerbd, + :σρs, + :σs, + fieldnames(typeof(m))..., +) """ pirls!(m::GeneralizedLinearMixedModel) @@ -325,7 +424,12 @@ optimized and `β` is held fixed. Passing `verbose = true` provides verbose output of the iterations. """ -function pirls!(m::GeneralizedLinearMixedModel{T}, varyβ=false, verbose=false; maxiter::Integer=10) where {T} +function pirls!( + m::GeneralizedLinearMixedModel{T}, + varyβ = false, + verbose = false; + maxiter::Integer = 10, +) where {T} u₀ = m.u₀ u = m.u β = m.β @@ -344,11 +448,11 @@ function pirls!(m::GeneralizedLinearMixedModel{T}, varyβ=false, verbose=false; end println() end - for iter in 1:maxiter - varyβ && ldiv!(adjoint(feL(m)), copyto!(β, lm.L.blocks[end, end - 1])) + for iter = 1:maxiter + varyβ && ldiv!(adjoint(feL(m)), copyto!(β, lm.L.blocks[end, end-1])) ranef!(u, m.LMM, β, true) # solve for new values of u obj = deviance!(m) # update GLM vecs and evaluate Laplace approx - verbose && println(lpad(iter,4), ": ", obj) + verbose && println(lpad(iter, 4), ": ", obj) nhalf = 0 while obj > obj₀ nhalf += 1 @@ -384,7 +488,7 @@ Set the parameter vector, `:βθ`, of `m` to `v`. """ function setβθ!(m::GeneralizedLinearMixedModel, v) setβ!(m, v) - setθ!(m, view(v, (length(m.β) + 1) : length(v))) + setθ!(m, view(v, (length(m.β)+1):length(v))) end function setβ!(m::GeneralizedLinearMixedModel, v) @@ -420,9 +524,9 @@ function Base.show(io::IO, m::GeneralizedLinearMixedModel) println(io, " Link: ", GLM.Link(m.resp), "\n") println(io, string(" Deviance: ", @sprintf("%.4f", deviance(m, nAGQ))), "\n") - show(io,VarCorr(m)) + show(io, VarCorr(m)) - print(io," Number of obs: $(length(m.y)); levels of grouping factors: ") + print(io, " Number of obs: $(length(m.y)); levels of grouping factors: ") join(io, nlevs.(m.reterms), ", ") println(io) @@ -460,7 +564,7 @@ for f in ( :(StatsBase.vcov), :σs, :σρs, - ) +) @eval begin $f(m::GeneralizedLinearMixedModel) = $f(m.LMM) end diff --git a/src/linalg.jl b/src/linalg.jl index d4053b755..564dfc493 100644 --- a/src/linalg.jl +++ b/src/linalg.jl @@ -1,23 +1,12 @@ -function mulαβ!(C::Matrix{T}, A::Matrix{T}, adjB::Adjoint{T,<:Matrix{T}}, - α=true, β=false) where {T<:BlasFloat} - BLAS.gemm!('N', 'C', T(α), A, adjB.parent, T(β), C) -end - -function mulαβ!(C::Matrix{T}, A::Matrix{T}, adjB::Diagonal{T,S}, - α=true, β=false) where {T<:BlasFloat,S} - # adapted from LinearAlgebra/src/diagonal.jl in 1.3 - C .= (A .* permutedims(adjB.diag)) .* α .+ C .* β -end - -function mulαβ!(C::SparseMatrixCSC{T}, A::SparseMatrixCSC{T}, adjB::Diagonal{T,S}, - α=true, β=false) where {T<:BlasFloat,S} - # adapted from LinearAlgebra/src/diagonal.jl in 1.3 - C .= (A .* permutedims(adjB.diag)) .* α .+ C .* β -end - -function mulαβ!(C::Matrix{T}, A::SparseMatrixCSC{T}, adjB::Adjoint{T,<:SparseMatrixCSC{T}}, - α=true, β=false) where T <: Number - B = adjB.parent +function LinearAlgebra.mul!( + C::Matrix{T}, + blkA::BlockedSparse{T}, + adjB::Adjoint{T,<:BlockedSparse{T}}, + α::Number, + β::Number, +) where {T} + A = blkA.cscmat + B = adjB.parent.cscmat B.m == size(C, 2) && A.m == size(C, 1) && A.n == B.n || throw(DimensionMismatch("")) anz = nonzeros(A) arv = rowvals(A) @@ -36,134 +25,78 @@ function mulαβ!(C::Matrix{T}, A::SparseMatrixCSC{T}, adjB::Adjoint{T,<:SparseM C end -mulαβ!(C::Matrix{T}, A::BlockedSparse{T}, adjB::Adjoint{T,<:BlockedSparse{T}}, α=true, - β=false) where {T} = mulαβ!(C, A.cscmat, adjB.parent.cscmat', α, β) - -function mulαβ!(C::Matrix{T}, A::SparseMatrixCSC{T}, adjB::Adjoint{T,Matrix{T}}, - α=true, β=false) where {T} - B = adjB.parent - A.n == size(B, 2) || throw(DimensionMismatch()) - A.m == size(C, 1) || throw(DimensionMismatch()) - size(B, 1) == size(C, 2) || throw(DimensionMismatch()) - nzv = A.nzval - rv = A.rowval - if β != 1 - β != 0 ? rmul!(C, β) : fill!(C, zero(eltype(C))) - end - for k = 1:size(C, 2) - @inbounds for col = 1:A.n - αxj = α*B[k,col] - for j = nzrange(A, col) - C[rv[j], k] += nzv[j]*αxj - end - end - end - C -end - -mulαβ!(C::Matrix{T}, A::BlockedSparse{T}, adjB::Adjoint{T,<:Matrix{T}}, α=true, β=false) where {T} = - mulαβ!(C, A.cscmat, adjB, α, β) - -function mulαβ!(C::SparseMatrixCSC{T}, A::SparseMatrixCSC{T}, adjB::Adjoint{T,<:SparseMatrixCSC{T}}, - α=true, β=false) where {T} - B = adjB.parent - C.m == A.m && C.n == B.m && A.n == B.n || throw(DimensionMismatch("")) - Anz = nonzeros(A) - Bnz = nonzeros(B) - Cnz = nonzeros(C) - isone(β) || rmul!(Cnz, β) - Arv = rowvals(A) - Brv = rowvals(B) - Crv = rowvals(C) - for j in 1:A.n - for K in nzrange(B, j) - k = Brv[K] - alphabjk = α * Bnz[K] - colkfirstr = Int(C.colptr[k]) - colklastr = Int(C.colptr[k + 1] - 1) - for I in nzrange(A, j) - i = Arv[I] - searchk = searchsortedfirst(Crv, i, colkfirstr, colklastr, Base.Order.Forward) - if searchk <= colklastr && Crv[searchk] == i - Cnz[searchk] += alphabjk * Anz[I] - else - throw(ArgumentError("C does not have the nonzero pattern of A*B'")) - end - end - end - end - C -end - -mulαβ!(C::BlockedSparse{T}, A::BlockedSparse{T}, adjB::Adjoint{T,<:BlockedSparse{T}}, α=true, β=false) where {T} = - mulαβ!(C.cscmat, A.cscmat, adjB.parent.cscmat', α, β) - -function mulαβ!(C::StridedVecOrMat{T}, A::StridedVecOrMat{T}, adjB::Adjoint{T,<:SparseMatrixCSC{T}}, - α=true, β=false) where T - B = adjB.parent - m, n = size(A) - p, q = size(B) - r, s = size(C) - r == m && s == p && n == q || throw(DimensionMismatch("")) - isone(β) || rmul!(C, β) - nz = nonzeros(B) - rv = rowvals(B) - @inbounds for j in 1:q, k in nzrange(B, j) - rvk = rv[k] - anzk = α * nz[k] - for jj in 1:r - C[jj, rvk] += A[jj, j] * anzk - end - end - C -end - -mulαβ!(C::StridedVecOrMat{T}, A::StridedVecOrMat{T}, adjB::Adjoint{T,<:BlockedSparse{T}}, - α=true, β=false) where {T} = mulαβ!(C, A, adjB.parent.cscmat', α, β) - -mulαβ!(C::StridedVector{T}, adjA::Adjoint{T,<:StridedMatrix{T}}, B::StridedVector{T}, - α=true, β=false) where {T<:BlasFloat} = BLAS.gemv!('C', T(α), adjA.parent, B, T(β), C) - -mulαβ!(C::StridedVector{T}, adjA::Adjoint{T,<:SparseMatrixCSC{T}}, B::StridedVector{T}, - α=true, β=false) where {T} = mul!(C, adjA, B, T(α), T(β)) - -mulαβ!(C::StridedVector{T}, adjA::Adjoint{T,<:BlockedSparse{T}}, B::StridedVector{T}, - α=true, β=false) where {T} = mulαβ!(C, adjA.parent.cscmat', B, α, β) - -function LinearAlgebra.ldiv!(adjA::Adjoint{T,<:LowerTriangular{T,UniformBlockDiagonal{T}}}, - B::StridedVector{T}) where {T} +LinearAlgebra.mul!( + C::Matrix{T}, + A::BlockedSparse{T}, + adjB::Adjoint{T,<:Matrix{T}}, + α::Number, + β::Number, +) where {T} = mul!(C, A.cscmat, adjB, α, β) + +LinearAlgebra.mul!( + C::BlockedSparse{T}, + A::BlockedSparse{T}, + adjB::Adjoint{T,<:BlockedSparse{T}}, + α::Number, + β::Number, +) where {T} = mul!(C.cscmat, A.cscmat, adjB.parent.cscmat', α, β) + +LinearAlgebra.mul!( + C::StridedVecOrMat{T}, + A::StridedVecOrMat{T}, + adjB::Adjoint{T,<:BlockedSparse{T}}, + α::Number, + β::Number, +) where {T} = mul!(C, A, adjB.parent.cscmat', α, β) + +LinearAlgebra.mul!( + C::StridedVector{T}, + adjA::Adjoint{T,<:BlockedSparse{T}}, + B::StridedVector{T}, + α::Number, + β::Number, +) where {T} = mul!(C, adjA.parent.cscmat', B, α, β) + +function LinearAlgebra.ldiv!( + adjA::Adjoint{T,<:LowerTriangular{T,UniformBlockDiagonal{T}}}, + B::StridedVector{T}, +) where {T} A = adjA.parent length(B) == size(A, 2) || throw(DimensionMismatch("")) m, n, k = size(A.data.data) fv = A.data.facevec bb = reshape(B, (n, k)) - for j in 1:k + for j = 1:k ldiv!(adjoint(LowerTriangular(fv[j])), view(bb, :, j)) end B end -function LinearAlgebra.rdiv!(A::Matrix{T}, - adjB::Adjoint{T,<:LowerTriangular{T,UniformBlockDiagonal{T}}}) where T +function LinearAlgebra.rdiv!( + A::Matrix{T}, + adjB::Adjoint{T,<:LowerTriangular{T,UniformBlockDiagonal{T}}}, +) where {T} Bd = adjB.parent.data m, n, k = size(Bd.data) size(A, 2) == size(Bd, 1) && m == n || throw(DimensionMismatch("")) inds = 1:m for (i, f) in enumerate(Bd.facevec) - BLAS.trsm!('R', 'L', 'T', 'N', one(T), f, view(A, :, inds .+ m * (i-1))) + BLAS.trsm!('R', 'L', 'T', 'N', one(T), f, view(A, :, inds .+ m * (i - 1))) end A end -function LinearAlgebra.rdiv!(A::BlockedSparse{T,S,P}, - B::Adjoint{T,<:LowerTriangular{T,UniformBlockDiagonal{T}}}) where {T,S,P} +function LinearAlgebra.rdiv!( + A::BlockedSparse{T,S,P}, + B::Adjoint{T,<:LowerTriangular{T,UniformBlockDiagonal{T}}}, +) where {T,S,P} Bpd = B.parent.data - j,k,l = size(Bpd.data) + j, k, l = size(Bpd.data) cbpt = A.colblkptr nzv = A.cscmat.nzval P == j == k && length(cbpt) == (l + 1) || throw(DimensionMismatch("")) - for (j,f) in enumerate(Bpd.facevec) - rdiv!(reshape(view(nzv, cbpt[j]:(cbpt[j + 1] - 1)), :, P), adjoint(LowerTriangular(f))) + for (j, f) in enumerate(Bpd.facevec) + rdiv!(reshape(view(nzv, cbpt[j]:(cbpt[j+1]-1)), :, P), adjoint(LowerTriangular(f))) end A end diff --git a/src/linalg/logdet.jl b/src/linalg/logdet.jl index 508b3574e..e1451103f 100644 --- a/src/linalg/logdet.jl +++ b/src/linalg/logdet.jl @@ -12,15 +12,15 @@ function LD(d::UniformBlockDiagonal{T}) where {T} m, n, k = size(dat) m == n || throw(ArgumentError("Blocks of d must be square")) s = log(one(T)) - @inbounds for j in 1:k, i in 1:m - s += log(dat[i,i,j]) + @inbounds for j = 1:k, i = 1:m + s += log(dat[i, i, j]) end s end function LD(d::DenseMatrix{T}) where {T} s = log(one(T)) - for i in 1:LinearAlgebra.checksquare(d) + for i = 1:LinearAlgebra.checksquare(d) s += log(d[i, i]) end s @@ -39,7 +39,7 @@ function LinearAlgebra.logdet(m::LinearMixedModel{T}) where {T} s = log(one(T)) L = m.L nre = length(m.reterms) - @inbounds for i in 1:nre + @inbounds for i = 1:nre s += LD(L[Block(i, i)]) end if m.optsum.REML diff --git a/src/linalg/rankUpdate.jl b/src/linalg/rankUpdate.jl index f5779f113..e734ed7c6 100644 --- a/src/linalg/rankUpdate.jl +++ b/src/linalg/rankUpdate.jl @@ -11,19 +11,31 @@ The order of the arguments """ function rankUpdate! end -function rankUpdate!(C::HermOrSym{T,S}, a::StridedVector{T}, - α=true) where {T<:BlasReal,S<:StridedMatrix} +function rankUpdate!( + C::HermOrSym{T,S}, + a::StridedVector{T}, + α = true, +) where {T<:BlasReal,S<:StridedMatrix} BLAS.syr!(C.uplo, T(α), a, C.data) C ## to ensure that the return value is HermOrSym end -function rankUpdate!(C::HermOrSym{T,S}, A::StridedMatrix{T}, - α=true, β=true) where {T<:BlasReal,S<:StridedMatrix} +function rankUpdate!( + C::HermOrSym{T,S}, + A::StridedMatrix{T}, + α = true, + β = true, +) where {T<:BlasReal,S<:StridedMatrix} BLAS.syrk!(C.uplo, 'N', T(α), A, T(β), C.data) C end -function rankUpdate!(C::HermOrSym{T,Matrix{T}}, A::SparseMatrixCSC{T}, α=true, β=true) where {T} +function rankUpdate!( + C::HermOrSym{T,Matrix{T}}, + A::SparseMatrixCSC{T}, + α = true, + β = true, +) where {T} m, n = size(A) m == size(C, 2) || throw(DimensionMismatch("")) C.uplo == 'L' || throw(ArgumentError("C.uplo must be 'L'")) @@ -31,13 +43,13 @@ function rankUpdate!(C::HermOrSym{T,Matrix{T}}, A::SparseMatrixCSC{T}, α=true, isone(β) || rmul!(LowerTriangular(Cd), β) rv = rowvals(A) nz = nonzeros(A) - @inbounds for jj in 1:n + @inbounds for jj = 1:n rangejj = nzrange(A, jj) lenrngjj = length(rangejj) for (k, j) in enumerate(rangejj) anzj = α * nz[j] rvj = rv[j] - for i in k:lenrngjj + for i = k:lenrngjj kk = rangejj[i] Cd[rv[kk], rvj] += nz[kk] * anzj end @@ -46,16 +58,22 @@ function rankUpdate!(C::HermOrSym{T,Matrix{T}}, A::SparseMatrixCSC{T}, α=true, C end -rankUpdate!(C::HermOrSym, A::BlockedSparse, α=true, β=true) = rankUpdate!(C, sparse(A), α, β) +rankUpdate!(C::HermOrSym, A::BlockedSparse, α = true, β = true) = + rankUpdate!(C, sparse(A), α, β) -function rankUpdate!(C::Diagonal{T}, A::SparseMatrixCSC{T}, α=true, β=true) where {T <: Number} +function rankUpdate!( + C::Diagonal{T}, + A::SparseMatrixCSC{T}, + α = true, + β = true, +) where {T<:Number} m, n = size(A) dd = C.diag length(dd) == m || throw(DimensionMismatch("")) isone(β) || rmul!(dd, β) nz = nonzeros(A) rv = rowvals(A) - @inbounds for j in 1:n + @inbounds for j = 1:n nzr = nzrange(A, j) if !isempty(nzr) length(nzr) == 1 || throw(ArgumentError("A*A' has off-diagonal elements")) @@ -66,32 +84,40 @@ function rankUpdate!(C::Diagonal{T}, A::SparseMatrixCSC{T}, α=true, β=true) wh C end -rankUpdate!(C::Diagonal{T}, A::BlockedSparse{T}, α=true, β=true) where {T <: Number} = rankUpdate!(C, sparse(A), α, β) +rankUpdate!(C::Diagonal{T}, A::BlockedSparse{T}, α = true, β = true) where {T<:Number} = + rankUpdate!(C, sparse(A), α, β) -function rankUpdate!(C::HermOrSym{T,UniformBlockDiagonal{T}}, A::BlockedSparse{T,S}, - α=true) where {T,S} +function rankUpdate!( + C::HermOrSym{T,UniformBlockDiagonal{T}}, + A::BlockedSparse{T,S}, + α = true, +) where {T,S} Ac = A.cscmat cp = Ac.colptr - all(diff(cp) .== S) || - throw(ArgumentError("Each column of A must contain exactly S nonzeros")) - j,k,l = size(C.data.data) + all(diff(cp) .== S) || throw(ArgumentError("Each column of A must contain exactly S nonzeros")) + j, k, l = size(C.data.data) S == j == k && div(Ac.m, S) == l || - throw(DimensionMismatch("div(A.cscmat.m, S) ≠ length(C.data.facevec)")) + throw(DimensionMismatch("div(A.cscmat.m, S) ≠ length(C.data.facevec)")) nz = Ac.nzval rv = Ac.rowval Cdf = C.data.facevec - for j in 1:Ac.n + for j = 1:Ac.n nzr = nzrange(Ac, j) BLAS.syr!('L', α, view(nz, nzr), Cdf[div(rv[last(nzr)], S)]) end C end -rankUpdate!(C::HermOrSym{T,Matrix{T}}, A::BlockedSparse{T}, α=true) where {T} = rankUpdate!(C, A.cscmat, α) +rankUpdate!(C::HermOrSym{T,Matrix{T}}, A::BlockedSparse{T}, α = true) where {T} = + rankUpdate!(C, A.cscmat, α) -function rankUpdate!(C::Diagonal{T,S}, A::Diagonal{T,S}, α::Number=true, β::Number=true) where {T, S} - length(C.diag) == length(A.diag) || - throw(DimensionMismatch("length(C.diag) ≠ length(A.diag)")) +function rankUpdate!( + C::Diagonal{T,S}, + A::Diagonal{T,S}, + α::Number = true, + β::Number = true, +) where {T,S} + length(C.diag) == length(A.diag) || throw(DimensionMismatch("length(C.diag) ≠ length(A.diag)")) C.diag .= β .* C.diag .+ α .* abs2.(A.diag) C diff --git a/src/linalg/statschol.jl b/src/linalg/statschol.jl index f6c209f78..168c63923 100644 --- a/src/linalg/statschol.jl +++ b/src/linalg/statschol.jl @@ -6,28 +6,27 @@ retains the original order unless singularity is detected. Columns that are (computationally) linearly dependent on columns to their left are moved to the right hand side in a left circular shift. """ -function statscholesky(xtx::Symmetric{T}, tol::Real=-1) where{T<:AbstractFloat} +function statscholesky(xtx::Symmetric{T}, tol::Real = -1) where {T<:AbstractFloat} n = size(xtx, 2) - chpiv = cholesky(xtx, Val(true), tol=T(-1), check=false) + chpiv = cholesky(xtx, Val(true), tol = T(-1), check = false) chunp = cholesky(xtx, check = false) r = chpiv.rank piv = [1:n;] - if r < n + if r < n nleft = n while r < nleft k = chunp.info if k < nleft piv = piv[[1:k-1; k+1:n; k]] - chunp = cholesky!(Symmetric(xtx[piv,piv]), check=false) + chunp = cholesky!(Symmetric(xtx[piv, piv]), check = false) end nleft -= 1 end end - for j in (r+1):n # an MKL <-> OpenBLAS difference - for i in (r+1):j - chunp.factors[i,j] = zero(T) + for j = (r+1):n # an MKL <-> OpenBLAS difference + for i = (r+1):j + chunp.factors[i, j] = zero(T) end end CholeskyPivoted(chunp.factors, chunp.uplo, piv, r, tol, chpiv.info) end - diff --git a/src/linearmixedmodel.jl b/src/linearmixedmodel.jl index 17e6f18a7..93216b7c0 100644 --- a/src/linearmixedmodel.jl +++ b/src/linearmixedmodel.jl @@ -25,7 +25,7 @@ Linear mixed-effects model representation * `X`: the fixed-effects model matrix * `y`: the response vector """ -struct LinearMixedModel{T <: AbstractFloat} <: MixedModel{T} +struct LinearMixedModel{T<:AbstractFloat} <: MixedModel{T} formula::FormulaTerm reterms::Vector{ReMat{T}} feterms::Vector{FeMat{T}} @@ -34,15 +34,19 @@ struct LinearMixedModel{T <: AbstractFloat} <: MixedModel{T} L::BlockMatrix{T} optsum::OptSummary{T} end -LinearMixedModel(f::FormulaTerm, tbl; - contrasts = Dict{Symbol,Any}(), - wts = []) = - LinearMixedModel(f::FormulaTerm, Tables.columntable(tbl), - contrasts = contrasts, - wts = wts) -function LinearMixedModel(f::FormulaTerm, tbl::Tables.ColumnTable; - contrasts = Dict{Symbol,Any}(), - wts = []) +LinearMixedModel(f::FormulaTerm, tbl; contrasts = Dict{Symbol,Any}(), wts = []) = + LinearMixedModel( + f::FormulaTerm, + Tables.columntable(tbl), + contrasts = contrasts, + wts = wts, + ) +function LinearMixedModel( + f::FormulaTerm, + tbl::Tables.ColumnTable; + contrasts = Dict{Symbol,Any}(), + wts = [], +) # TODO: perform missing_omit() after apply_schema() when improved # missing support is in a StatsModels release tbl, _ = StatsModels.missing_omit(tbl, f) @@ -56,7 +60,7 @@ function LinearMixedModel(f::FormulaTerm, tbl::Tables.ColumnTable; reterms = ReMat{T}[] feterms = FeMat{T}[] - for (i,x) in enumerate(Xs) + for (i, x) in enumerate(Xs) if isa(x, ReMat{T}) push!(reterms, x) else @@ -71,7 +75,7 @@ function LinearMixedModel(f::FormulaTerm, tbl::Tables.ColumnTable; reterms = amalgamate(reterms) end - sort!(reterms, by=nranef, rev=true) + sort!(reterms, by = nranef, rev = true) # create A and L terms = vcat(reterms, feterms) @@ -79,18 +83,18 @@ function LinearMixedModel(f::FormulaTerm, tbl::Tables.ColumnTable; sz = append!(size.(reterms, 2), rank.(feterms)) A = BlockArray(undef_blocks, AbstractMatrix{T}, sz, sz) L = BlockArray(undef_blocks, AbstractMatrix{T}, sz, sz) - for j in 1:k - for i in j:k - Lij = L[Block(i,j)] = densify(terms[i]'terms[j]) - A[Block(i,j)] = deepcopy(isa(Lij, BlockedSparse) ? Lij.cscmat : Lij) + for j = 1:k + for i = j:k + Lij = L[Block(i, j)] = densify(terms[i]' * terms[j]) + A[Block(i, j)] = deepcopy(isa(Lij, BlockedSparse) ? Lij.cscmat : Lij) end end - for i in 2:length(reterms) # check for fill-in due to non-nested grouping factors + for i = 2:length(reterms) # check for fill-in due to non-nested grouping factors ci = reterms[i] - for j in 1:(i - 1) + for j = 1:(i-1) cj = reterms[j] if !isnested(cj, ci) - for l in i:k + for l = i:k L[Block(l, i)] = Matrix(L[Block(l, i)]) end break @@ -103,29 +107,83 @@ function LinearMixedModel(f::FormulaTerm, tbl::Tables.ColumnTable; fill!(optsum.xtol_abs, 1.0e-10) LinearMixedModel(form, reterms, feterms, sqrt.(convert(Vector{T}, wts)), A, L, optsum) end -fit(::Type{LinearMixedModel}, f::FormulaTerm, tbl; + +fit( + ::Type{LinearMixedModel}, + f::FormulaTerm, + tbl; wts = [], contrasts = Dict{Symbol,Any}(), verbose::Bool = false, - REML::Bool = false) = - fit(LinearMixedModel, f, Tables.columntable(tbl), - wts = wts, contrasts = contrasts, verbose = verbose, REML = REML) -fit(::Type{LinearMixedModel}, + REML::Bool = false, +) = fit( + LinearMixedModel, + f, + Tables.columntable(tbl), + wts = wts, + contrasts = contrasts, + verbose = verbose, + REML = REML, +) + +fit( + ::Type{LinearMixedModel}, f::FormulaTerm, tbl::Tables.ColumnTable; - wts = wts, contrasts = contrasts, verbose = verbose, REML = REML) = - fit!(LinearMixedModel(f, tbl, - contrasts = contrasts, - wts = wts), - verbose = verbose, - REML = REML) + wts = wts, + contrasts = contrasts, + verbose = verbose, + REML = REML, +) = fit!( + LinearMixedModel(f, tbl, contrasts = contrasts, wts = wts), + verbose = verbose, + REML = REML, +) + +fit( + ::Type{MixedModel}, + f::FormulaTerm, + tbl; + wts = [], + contrasts = Dict{Symbol,Any}(), + verbose::Bool = false, + REML::Bool = false, +) = fit( + LinearMixedModel, + f, + tbl, + wts = wts, + contrasts = contrasts, + verbose = verbose, + REML = REML, +) + +fit( + ::Type{MixedModel}, + f::FormulaTerm, + tbl, + d::Normal, + l::IdentityLink; + wts = [], + contrasts = Dict{Symbol,Any}(), + verbose::Bool = false, + REML::Bool = false, + offset = [], + fast::Bool = false, + nAGQ::Integer = 1, +) = fit( + LinearMixedModel, + f, + tbl, + wts = wts, + contrasts = contrasts, + verbose = verbose, + REML = REML, +) StatsBase.coef(m::MixedModel) = fixef(m, false) -function βs(m::LinearMixedModel) - fetrm = first(m.feterms) - (; (k => v for (k,v) in zip(Symbol.(fetrm.cnames), fixef(m)))...) -end +βs(m::LinearMixedModel) = NamedTuple{(Symbol.(first(m.feterms).cnames)...,)}((fixef(m)...,)) StatsBase.coefnames(m::LinearMixedModel) = first(m.feterms).cnames @@ -134,8 +192,12 @@ function StatsBase.coeftable(m::MixedModel) se = stderror(m) z = co ./ se pvalue = ccdf.(Chisq(1), abs2.(z)) - CoefTable(hcat(co, se, z, pvalue), ["Estimate", "Std.Error", "z value", "P(>|z|)"], - first(m.feterms).cnames, 4) + CoefTable( + hcat(co, se, z, pvalue), + ["Estimate", "Std.Error", "z value", "P(>|z|)"], + first(m.feterms).cnames, + 4, + ) end """ @@ -167,12 +229,12 @@ function condVar(m::LinearMixedModel{T}) where {T} retrms = m.reterms t1 = first(retrms) L11 = m.L[Block(1, 1)] - if !isone(length(retrms)) || !isa(L11, Diagonal{T, Vector{T}}) - throw(ArgumentError("code for vector-valued r.e. or more than one term not yet written")) + if !isone(length(retrms)) || !isa(L11, Diagonal{T,Vector{T}}) + throw(ArgumentError("code for multiple or vector-valued r.e. not yet written")) end ll = first(t1.λ) Ld = L11.diag - Array{T, 3}[reshape(abs2.(ll ./ Ld) .* varest(m), (1, 1, length(Ld)))] + Array{T,3}[reshape(abs2.(ll ./ Ld) .* varest(m), (1, 1, length(Ld)))] end """ @@ -184,16 +246,26 @@ Describe the types and sizes of the blocks in the lower triangle of `m.A` and `m function describeblocks(io::IO, m::LinearMixedModel) A = m.A L = m.L - for i in 1:BlockArrays.nblocks(A, 2), j in 1:i - println(io, i, ",", j, ": ", typeof(A[Block(i, j)]), " ", - BlockArrays.blocksize(A, (i, j)), " ", typeof(L[Block(i, j)])) + for i = 1:BlockArrays.nblocks(A, 2), j = 1:i + println( + io, + i, + ",", + j, + ": ", + typeof(A[Block(i, j)]), + " ", + BlockArrays.blocksize(A, (i, j)), + " ", + typeof(L[Block(i, j)]), + ) end end describeblocks(m::MixedModel) = describeblocks(stdout, m) StatsBase.deviance(m::MixedModel) = objective(m) -GLM.dispersion(m::LinearMixedModel, sqr::Bool=false) = sqr ? varest(m) : sdest(m) +GLM.dispersion(m::LinearMixedModel, sqr::Bool = false) = sqr ? varest(m) : sdest(m) GLM.dispersion_parameter(m::LinearMixedModel) = true @@ -210,7 +282,7 @@ end Return the lower Cholesky factor for the fixed-effects parameters, as an `LowerTriangular` `p × p` matrix. """ -feL(m::LinearMixedModel) = LowerTriangular(m.L.blocks[end - 1, end - 1]) +feL(m::LinearMixedModel) = LowerTriangular(m.L.blocks[end-1, end-1]) """ fit!(m::LinearMixedModel[; verbose::Bool=false, REML::Bool=false]) @@ -218,7 +290,7 @@ feL(m::LinearMixedModel) = LowerTriangular(m.L.blocks[end - 1, end - 1]) Optimize the objective of a `LinearMixedModel`. When `verbose` is `true` the values of the objective and the parameters are printed on stdout at each function evaluation. """ -function fit!(m::LinearMixedModel{T}; verbose::Bool=false, REML::Bool=false) where {T} +function fit!(m::LinearMixedModel{T}; verbose::Bool = false, REML::Bool = false) where {T} optsum = m.optsum opt = Opt(optsum) feval = 0 @@ -228,7 +300,7 @@ function fit!(m::LinearMixedModel{T}; verbose::Bool=false, REML::Bool=false) whe feval += 1 val = objective(updateL!(setθ!(m, x))) feval == 1 && (optsum.finitial = val) - verbose && println("f_", feval, ": ", round(val, digits=5), " ", x) + verbose && println("f_", feval, ": ", round(val, digits = 5), " ", x) val end NLopt.min_objective!(opt, obj) @@ -263,8 +335,7 @@ end function fitted!(v::AbstractArray{T}, m::LinearMixedModel{T}) where {T} ## FIXME: Create and use `effects(m) -> β, b` w/o calculating β twice - vv = vec(v) - mul!(vv, first(m.feterms), fixef(m)) + vv = mul!(vec(v), first(m.feterms), fixef(m)) for (rt, bb) in zip(m.reterms, ranef(m)) unscaledre!(vv, rt, bb) end @@ -279,7 +350,7 @@ StatsBase.fitted(m::LinearMixedModel{T}) where {T} = fitted!(Vector{T}(undef, no Overwrite `v` with the pivoted and, possibly, truncated fixed-effects coefficients of model `m` """ fixef!(v::AbstractVector{T}, m::LinearMixedModel{T}) where {T} = - ldiv!(feL(m)', copyto!(v, m.L.blocks[end, end - 1])) + ldiv!(feL(m)', copyto!(v, m.L.blocks[end, end-1])) """ fixef(m::MixedModel, permuted=true) @@ -287,9 +358,9 @@ fixef!(v::AbstractVector{T}, m::LinearMixedModel{T}) where {T} = Return the fixed-effects parameter vector estimate of `m`. If `permuted` is `true` the vector elements are permuted according to -`m.trms[end - 1].piv` and truncated to the rank of that term. +`first(m.feterms).piv` and truncated to the rank of that term. """ -function fixef(m::LinearMixedModel{T}, permuted=true) where {T} +function fixef(m::LinearMixedModel{T}, permuted = true) where {T} val = ldiv!(feL(m)', vec(copy(m.L.blocks[end, end-1]))) if !permuted Xtrm = first(m.feterms) @@ -308,7 +379,7 @@ end Return the names of the grouping factors for the random-effects terms. """ -fnames(m::MixedModel) = ((Symbol(tr.trm.sym) for tr in m.reterms)...,) +fnames(m::MixedModel) = ((tr.trm.sym for tr in m.reterms)...,) """ getθ(m::LinearMixedModel) @@ -370,7 +441,7 @@ end Test whether the model `m` is singular if the parameter vector is `θ`. """ -issingular(m::LinearMixedModel, θ::AbstractVector=m.θ) = any(isapprox.(m.optsum.lowerbd, θ)) +issingular(m::LinearMixedModel, θ=m.θ) = any(isapprox.(lowerbd(m), θ)) function StatsBase.loglikelihood(m::LinearMixedModel) if m.optsum.REML @@ -397,7 +468,10 @@ function likelihoodratiotest(m::LinearMixedModel...) dofdiffs = diff(dofs) devdiffs = .-(diff(devs)) pvals = ccdf.(Chisq.(dofdiffs), devdiffs) - (models=(dof=dofs, deviance=devs), tests=(dofdiff=dofdiffs, deviancediff=devdiffs, p_values=pvals)) + ( + models = (dof = dofs, deviance = devs), + tests = (dofdiff = dofdiffs, deviancediff = devdiffs, p_values = pvals), + ) end function StatsBase.modelmatrix(m::LinearMixedModel) @@ -420,15 +494,40 @@ Return negative twice the log-likelihood of model `m` """ function objective(m::LinearMixedModel) wts = m.sqrtwts - logdet(m) + dof_residual(m)*(1 + log2π + log(varest(m))) - (isempty(wts) ? 0 : 2sum(log, wts)) + logdet(m) + dof_residual(m) * (1 + log2π + log(varest(m))) - + (isempty(wts) ? 0 : 2 * sum(log, wts)) end StatsBase.predict(m::LinearMixedModel) = fitted(m) -Base.propertynames(m::LinearMixedModel, private=false) = - (:formula, :sqrtwts, :A, :L, :optsum, :θ, :theta, :β, :beta, :λ, :lambda, :stderror, - :σ, :sigma, :σs, :sigmas, :b, :u, :lowerbd, :X, :y, :rePCA, :reterms, :feterms, - :objective, :pvalues) +Base.propertynames(m::LinearMixedModel, private = false) = ( + :formula, + :sqrtwts, + :A, + :L, + :optsum, + :θ, + :theta, + :β, + :beta, + :λ, + :lambda, + :stderror, + :σ, + :sigma, + :σs, + :sigmas, + :b, + :u, + :lowerbd, + :X, + :y, + :rePCA, + :reterms, + :feterms, + :objective, + :pvalues, +) """ pwrss(m::LinearMixedModel) @@ -445,19 +544,29 @@ Overwrite `v` with the conditional modes of the random effects for `m`. If `uscale` is `true` the random effects are on the spherical (i.e. `u`) scale, otherwise on the original scale """ -function ranef!(v::Vector, m::LinearMixedModel{T}, β::AbstractArray{T}, uscale::Bool) where {T} +function ranef!( + v::Vector, + m::LinearMixedModel{T}, + β::AbstractArray{T}, + uscale::Bool, +) where {T} (k = length(v)) == length(m.reterms) || throw(DimensionMismatch("")) L = m.L - for j in 1:k - mulαβ!(vec(copyto!(v[j], L[Block(BlockArrays.nblocks(L, 2), j)])), - L[Block(k + 1, j)]', β, -one(T), one(T)) + for j = 1:k + mul!( + vec(copyto!(v[j], L[Block(BlockArrays.nblocks(L, 2), j)])), + L[Block(k + 1, j)]', + β, + -one(T), + one(T), + ) end - for i in k: -1 :1 + for i = k:-1:1 Lii = L[Block(i, i)] vi = vec(v[i]) ldiv!(adjoint(isa(Lii, Diagonal) ? Lii : LowerTriangular(Lii)), vi) - for j in 1:(i - 1) - mulαβ!(vec(v[j]), L[Block(i, j)]', vi, -one(T), one(T)) + for j = 1:(i-1) + mul!(vec(v[j]), L[Block(i, j)]', vi, -one(T), one(T)) end end if !uscale @@ -471,7 +580,7 @@ end ranef!(v::Vector, m::LinearMixedModel, uscale::Bool) = ranef!(v, m, fixef(m), uscale) """ - ranef(m::LinearMixedModel; uscale=false) #, named=true) + ranef(m::LinearMixedModel; uscale=false, named=true) Return, as a `Vector{Vector{T}}` (`Vector{NamedVector{T}}` if `named=true`), the conditional modes of the random effects in model `m`. @@ -479,7 +588,7 @@ the conditional modes of the random effects in model `m`. If `uscale` is `true` the random effects are on the spherical (i.e. `u`) scale, otherwise on the original scale. """ -function ranef(m::LinearMixedModel{T}; uscale=false, named=false) where {T} +function ranef(m::LinearMixedModel{T}; uscale = false, named = false) where {T} v = [Matrix{T}(undef, size(t.z, 1), nlevs(t)) for t in m.reterms] ranef!(v, m, uscale) named || return v @@ -497,7 +606,7 @@ function rePCA(m::LinearMixedModel{T}) where {T} re = m.reterms nt = length(re) tup = ntuple(i -> normalized_variance_cumsum(re[i].λ), nt) - NamedTuple{ntuple(i -> re[i].trm.sym, nt), typeof(tup)}(tup) + NamedTuple{ntuple(i -> re[i].trm.sym, nt),typeof(tup)}(tup) end """ @@ -553,7 +662,7 @@ Return the estimate of σ, the standard deviation of the per-observation noise. sdest(m::LinearMixedModel) = √varest(m) """ - setθ!{T}(m::LinearMixedModel{T}, v::Vector{T}) + setθ!(m::LinearMixedModel, v) Install `v` as the θ parameters in `m`. """ @@ -567,7 +676,8 @@ function setθ!(m::LinearMixedModel, v) m end -Base.setproperty!(m::LinearMixedModel, s::Symbol, y) = s == :θ ? setθ!(m, y) : setfield!(m, s, y) +Base.setproperty!(m::LinearMixedModel, s::Symbol, y) = + s == :θ ? setθ!(m, y) : setfield!(m, s, y) function Base.show(io::IO, m::LinearMixedModel) if m.optsum.feval < 0 @@ -582,7 +692,7 @@ function Base.show(io::IO, m::LinearMixedModel) if REML println(io, " REML criterion at convergence: ", oo) else - nums = showoff([-oo/ 2, oo, aic(m), bic(m)]) + nums = showoff([-oo / 2, oo, aic(m), bic(m)]) fieldwd = max(maximum(textwidth.(nums)) + 1, 11) for label in [" logLik", "-2 logLik", "AIC", "BIC"] print(io, rpad(lpad(label, (fieldwd + textwidth(label)) >> 1), fieldwd)) @@ -593,13 +703,13 @@ function Base.show(io::IO, m::LinearMixedModel) end println(io) - show(io,VarCorr(m)) + show(io, VarCorr(m)) - print(io," Number of obs: $n; levels of grouping factors: ") + print(io, " Number of obs: $n; levels of grouping factors: ") join(io, nlevs.(m.reterms), ", ") println(io) - println(io,"\n Fixed-effects parameters:") - show(io,coeftable(m)) + println(io, "\n Fixed-effects parameters:") + show(io, coeftable(m)) end function σs(m::LinearMixedModel) @@ -634,7 +744,7 @@ Return the estimated standard deviations of the random effects as a `Vector{Vect function Statistics.std(m::LinearMixedModel) rl = rowlengths.(m.reterms) s = sdest(m) - isfinite(s) ? rmul!(push!(rl, [1.]), s) : rl + isfinite(s) ? rmul!(push!(rl, [1.0]), s) : rl end """ @@ -648,8 +758,8 @@ function updateA!(m::LinearMixedModel) terms = vcat(m.reterms, m.feterms) k = length(terms) A = m.A - for j in 1:k - for i in j:k + for j = 1:k + for i = j:k mul!(A[Block(i, j)], terms[i]', terms[j]) end end @@ -667,32 +777,32 @@ function updateL!(m::LinearMixedModel{T}) where {T} A = m.A L = m.L k = BlockArrays.nblocks(A, 2) - for j in 1:k # copy lower triangle of A to L - for i in j:BlockArrays.nblocks(A, 1) + for j = 1:k # copy lower triangle of A to L + for i = j:BlockArrays.nblocks(A, 1) copyto!(L[Block(i, j)], A[Block(i, j)]) end end for (j, cj) in enumerate(m.reterms) # pre- and post-multiply by Λ, add I to diagonal scaleinflate!(L[Block(j, j)], cj) - for i in (j+1):k # postmultiply column by Λ + for i = (j+1):k # postmultiply column by Λ rmulΛ!(L[Block(i, j)], cj) end - for jj in 1:(j-1) # premultiply row by Λ' + for jj = 1:(j-1) # premultiply row by Λ' lmulΛ!(cj', L[Block(j, jj)]) end end - for j in 1:k # blocked Cholesky + for j = 1:k # blocked Cholesky Ljj = L[Block(j, j)] LjjH = isa(Ljj, Diagonal) ? Ljj : Hermitian(Ljj, :L) - for jj in 1:(j - 1) + for jj = 1:(j-1) rankUpdate!(LjjH, L[Block(j, jj)], -one(T)) end cholUnblocked!(Ljj, Val{:L}) LjjT = isa(Ljj, Diagonal) ? Ljj : LowerTriangular(Ljj) - for i in (j + 1):k + for i = (j+1):k Lij = L[Block(i, j)] - for jj in 1:(j - 1) - mulαβ!(Lij, L[Block(i, jj)], L[Block(j, jj)]', -one(T), one(T)) + for jj = 1:(j-1) + mul!(Lij, L[Block(i, jj)], L[Block(j, jj)]', -one(T), one(T)) end rdiv!(Lij, LjjT') end @@ -717,9 +827,9 @@ function StatsBase.vcov(m::LinearMixedModel{T}) where {T} if p == Xtrm.rank permvcov[iperm, iperm] else - covmat = fill(zero(T)/zero(T), (p, p)) - for j in 1:r, i in 1:r - covmat[i,j] = permvcov[i, j] + covmat = fill(zero(T) / zero(T), (p, p)) + for j = 1:r, i = 1:r + covmat[i, j] = permvcov[i, j] end covmat[iperm, iperm] end diff --git a/src/mixed.jl b/src/mixed.jl deleted file mode 100644 index 3f8e95730..000000000 --- a/src/mixed.jl +++ /dev/null @@ -1,35 +0,0 @@ -# LinearMixedModel -fit(::Type{MixedModel}, f::FormulaTerm, tbl; - wts = [], - contrasts = Dict{Symbol,Any}(), - verbose::Bool = false, - REML::Bool = false) = - fit(LinearMixedModel, - f, tbl, wts = wts, contrasts = contrasts, - verbose = verbose, REML = REML) -# GeneralizedLinearMixedModel -fit(::Type{MixedModel}, f::FormulaTerm, tbl, - d::Distribution, l::Link = canonicallink(d); - wts = [], - contrasts = Dict{Symbol,Any}(), - offset = [], - verbose::Bool = false, - REML::Bool = false, - fast::Bool = false, - nAGQ::Integer = 1) = - fit(GeneralizedLinearMixedModel, - f, tbl, d, l, wts = wts, contrasts = contrasts, offset = offset, - verbose = verbose, fast = fast, nAGQ = nAGQ) -# LinearMixedModel -fit(::Type{MixedModel}, f::FormulaTerm, tbl, - d::Normal, l::IdentityLink; - wts = [], - contrasts = Dict{Symbol,Any}(), - verbose::Bool = false, - REML::Bool = false, - offset = [], - fast::Bool = false, - nAGQ::Integer = 1) = - fit(LinearMixedModel, f, tbl, - wts = wts, contrasts = contrasts, - verbose = verbose, REML = REML) diff --git a/src/optsummary.jl b/src/optsummary.jl index 8f19c4b80..beebbcb2a 100644 --- a/src/optsummary.jl +++ b/src/optsummary.jl @@ -23,7 +23,7 @@ Summary of an `NLopt` optimization The latter field doesn't really belong here but it has to be in a mutable struct in case it is changed. """ -mutable struct OptSummary{T <: AbstractFloat} +mutable struct OptSummary{T<:AbstractFloat} initial::Vector{T} lowerbd::Vector{T} finitial::T @@ -38,14 +38,36 @@ mutable struct OptSummary{T <: AbstractFloat} feval::Int optimizer::Symbol returnvalue::Symbol - nAGQ::Integer # doesn't really belong here but I needed some place to store it + nAGQ::Integer # don't really belong here but I needed a place to store them REML::Bool end -function OptSummary(initial::Vector{T}, lowerbd::Vector{T}, - optimizer::Symbol; ftol_rel::T=zero(T), ftol_abs::T=zero(T), xtol_rel::T=zero(T), - initial_step::Vector{T}=T[]) where T <: AbstractFloat - OptSummary(initial, lowerbd, T(Inf), ftol_rel, ftol_abs, xtol_rel, zero(initial), - initial_step, -1, copy(initial), T(Inf), -1, optimizer, :FAILURE, 1, false) +function OptSummary( + initial::Vector{T}, + lowerbd::Vector{T}, + optimizer::Symbol; + ftol_rel::T = zero(T), + ftol_abs::T = zero(T), + xtol_rel::T = zero(T), + initial_step::Vector{T} = T[], +) where {T<:AbstractFloat} + OptSummary( + initial, + lowerbd, + T(Inf), + ftol_rel, + ftol_abs, + xtol_rel, + zero(initial), + initial_step, + -1, + copy(initial), + T(Inf), + -1, + optimizer, + :FAILURE, + 1, + false, + ) end function Base.show(io::IO, s::OptSummary) @@ -74,7 +96,7 @@ function NLopt.Opt(optsum::OptSummary) NLopt.ftol_rel!(opt, optsum.ftol_rel) # relative criterion on objective NLopt.ftol_abs!(opt, optsum.ftol_abs) # absolute criterion on objective NLopt.xtol_rel!(opt, optsum.xtol_rel) # relative criterion on parameter values - if length(optsum.xtol_abs) == length(lb) # not true for the second optimization in GLMM + if length(optsum.xtol_abs) == length(lb) # not true for fast=false optimization in GLMM NLopt.xtol_abs!(opt, optsum.xtol_abs) # absolute criterion on parameter values end NLopt.lower_bounds!(opt, lb) diff --git a/src/randomeffectsterm.jl b/src/randomeffectsterm.jl index 1a1f2ddab..ddc596370 100644 --- a/src/randomeffectsterm.jl +++ b/src/randomeffectsterm.jl @@ -1,9 +1,15 @@ struct RandomEffectsTerm <: AbstractTerm lhs::StatsModels.TermOrTerms rhs::StatsModels.TermOrTerms - function RandomEffectsTerm(lhs,rhs) + function RandomEffectsTerm(lhs, rhs) if isempty(intersect(StatsModels.termvars(lhs), StatsModels.termvars(rhs))) - if !isa(rhs, Union{CategoricalTerm,InteractionTerm{<:NTuple{N,CategoricalTerm} where N}}) + if !isa( + rhs, + Union{ + CategoricalTerm, + InteractionTerm{<:NTuple{N,CategoricalTerm} where {N}}, + }, + ) throw(ArgumentError("blocking variables (those behind |) must be Categorical ($(rhs) is not)")) end new(lhs, rhs) @@ -13,18 +19,19 @@ struct RandomEffectsTerm <: AbstractTerm end end -function StatsModels.apply_schema(t::FunctionTerm{typeof(/)}, - sch::StatsModels.FullRank, - Mod::Type{<:MixedModel}) - length(t.args_parsed) == 2 || - throw(ArgumentError("malformed nesting term: $t " * - "(Exactly two arguments are supported)")) - +function StatsModels.apply_schema( + t::FunctionTerm{typeof(/)}, + sch::StatsModels.FullRank, + Mod::Type{<:MixedModel}, +) + if length(t.args_parsed) ≠ 2 + throw(ArgumentError("malformed nesting term: $t (Exactly two arguments required")) + end first, second = apply_schema.(t.args_parsed, Ref(sch.schema), Mod) return first + first & second end -RandomEffectsTerm(lhs, rhs::NTuple{2, AbstractTerm}) = +RandomEffectsTerm(lhs, rhs::NTuple{2,AbstractTerm}) = (RandomEffectsTerm(lhs, rhs[1]), RandomEffectsTerm(lhs, rhs[2])) Base.show(io::IO, t::RandomEffectsTerm) = print(io, "($(t.lhs) | $(t.rhs))") @@ -34,9 +41,11 @@ function StatsModels.termvars(t::RandomEffectsTerm) vcat(StatsModels.termvars(t.lhs), StatsModels.termvars(t.rhs)) end -function StatsModels.apply_schema(t::FunctionTerm{typeof(|)}, - schema::StatsModels.FullRank, - Mod::Type{<:MixedModel}) +function StatsModels.apply_schema( + t::FunctionTerm{typeof(|)}, + schema::StatsModels.FullRank, + Mod::Type{<:MixedModel}, +) schema = StatsModels.FullRank(schema.schema) lhs, rhs = t.args_parsed if !StatsModels.hasintercept(lhs) && !StatsModels.omitsintercept(lhs) @@ -55,15 +64,23 @@ function StatsModels.modelcols(t::RandomEffectsTerm, d::NamedTuple) grp = t.rhs m = reshape(1:abs2(S), (S, S)) inds = sizehint!(Int[], (S * (S + 1)) >> 1) - for j in 1:S, i in j:S - push!(inds, m[i,j]) + for j = 1:S, i = j:S + push!(inds, m[i, j]) end refs, levels = _ranef_refs(grp, d) ReMat{T,S}( - grp, refs, levels, isa(cnames, String) ? [cnames] : collect(cnames), - z, z, LowerTriangular(Matrix{T}(I, S, S)), inds, - adjA(refs, z), Matrix{T}(undef, (S, length(levels)))) + grp, + refs, + levels, + isa(cnames, String) ? [cnames] : collect(cnames), + z, + z, + LowerTriangular(Matrix{T}(I, S, S)), + inds, + adjA(refs, z), + Matrix{T}(undef, (S, length(levels))), + ) end @@ -74,11 +91,13 @@ function _ranef_refs(grp::CategoricalTerm, d::NamedTuple) refs, grp.contrasts.levels end -function _ranef_refs(grp::InteractionTerm{<:NTuple{N,CategoricalTerm}}, - d::NamedTuple) where {N} +function _ranef_refs( + grp::InteractionTerm{<:NTuple{N,CategoricalTerm}}, + d::NamedTuple, +) where {N} combos = zip(getproperty.(Ref(d), [g.sym for g in grp.terms])...) uniques = unique(combos) - invindex = Dict(x => i for (i,x) in enumerate(uniques)) + invindex = Dict(x => i for (i, x) in enumerate(uniques)) refs = convert(Vector{Int32}, getindex.(Ref(invindex), combos)) refs, uniques end @@ -90,17 +109,21 @@ fulldummy(t::AbstractTerm) = "coding (only CategoricalTerms)")) function fulldummy(t::CategoricalTerm) - new_contrasts = StatsModels.ContrastsMatrix(StatsModels.FullDummyCoding(), - t.contrasts.levels) + new_contrasts = StatsModels.ContrastsMatrix( + StatsModels.FullDummyCoding(), + t.contrasts.levels, + ) t = CategoricalTerm(t.sym, new_contrasts) end fulldummy(x) = throw(ArgumentError("fulldummy isn't supported outside of a MixedModel formula")) -function StatsModels.apply_schema(t::FunctionTerm{typeof(fulldummy)}, - sch::StatsModels.FullRank, - Mod::Type{<:MixedModel}) +function StatsModels.apply_schema( + t::FunctionTerm{typeof(fulldummy)}, + sch::StatsModels.FullRank, + Mod::Type{<:MixedModel}, +) fulldummy(apply_schema.(t.args_parsed, Ref(sch), Mod)...) end @@ -118,12 +141,12 @@ Remove correlations between random effects in `term`. """ zerocorr(x) = ZeroCorr(x) -function StatsModels.apply_schema(t::FunctionTerm{typeof(zerocorr)}, - sch::StatsModels.FullRank, - Mod::Type{<:MixedModel}) +function StatsModels.apply_schema( + t::FunctionTerm{typeof(zerocorr)}, + sch::StatsModels.FullRank, + Mod::Type{<:MixedModel}, +) ZeroCorr(apply_schema(t.args_parsed..., sch, Mod)) end -StatsModels.modelcols(t::ZeroCorr, d::NamedTuple) = - zerocorr!(modelcols(t.term, d)) - +StatsModels.modelcols(t::ZeroCorr, d::NamedTuple) = zerocorr!(modelcols(t.term, d)) diff --git a/src/remat.jl b/src/remat.jl index 56614bf0d..869f0718b 100644 --- a/src/remat.jl +++ b/src/remat.jl @@ -233,11 +233,8 @@ end *(adjA::Adjoint{T,<:FeMat{T}}, B::ReMat{T}) where {T} = mul!(Matrix{T}(undef, rank(adjA.parent), size(B, 2)), adjA, B) -LinearAlgebra.mul!(C::AbstractMatrix{T}, adjA::Adjoint{T,<:FeMat{T}}, - B::ReMat{T}) where {T} = mulαβ!(C, adjA, B) - -function mulαβ!(C::Matrix{T}, adjA::Adjoint{T,<:FeMat{T}}, B::ReMat{T,1}, - α=true, β=false) where {T} +function LinearAlgebra.mul!(C::Matrix{T}, adjA::Adjoint{T,<:FeMat{T}}, B::ReMat{T,1}, + α::Number, β::Number) where {T} A = adjA.parent Awt = A.wtx n, p = size(Awt) @@ -255,8 +252,8 @@ function mulαβ!(C::Matrix{T}, adjA::Adjoint{T,<:FeMat{T}}, B::ReMat{T,1}, C end -function mulαβ!(C::Matrix{T}, adjA::Adjoint{T,<:FeMat{T}}, - B::ReMat{T,S}, α=true, β=false) where {T,S} +function LinearAlgebra.mul!(C::Matrix{T}, adjA::Adjoint{T,<:FeMat{T}}, B::ReMat{T,S}, + α::Number, β::Number) where {T,S} A = adjA.parent Awt = A.wtx r = rank(A) @@ -374,8 +371,7 @@ function *(adjA::Adjoint{T,<:ReMat{T,S}}, B::ReMat{T,P}) where {T,S,P} return Matrix(cscmat) end - BlockedSparse{T,S,P}(cscmat, reshape(cscmat.nzval, S, :), - cscmat.colptr[1:P:(cscmat.n + 1)]) + BlockedSparse{T,S,P}(cscmat, reshape(cscmat.nzval, S, :), cscmat.colptr[1:P:(cscmat.n + 1)]) end function reweight!(A::ReMat, sqrtwts::Vector) diff --git a/src/simulate.jl b/src/simulate.jl index 5a48cbbb3..c4fe811b9 100644 --- a/src/simulate.jl +++ b/src/simulate.jl @@ -47,18 +47,23 @@ The default random number generator is `Random.GLOBAL_RNG`. `β`, `σ`, and `θ` are the values of `m`'s parameters for simulating the responses. """ -function parametricbootstrap(rng::AbstractRNG, nsamp::Integer, m::LinearMixedModel{T}; - β = m.β, σ = m.σ, θ = m.θ) where {T} +function parametricbootstrap( + rng::AbstractRNG, + nsamp::Integer, + m::LinearMixedModel{T}; + β = m.β, + σ = m.σ, + θ = m.θ, +) where {T} y₀ = copy(response(m)) # to restore original state of m θscr = copy(θ) βscr = copy(β) k = length(θ) bnms = Symbol.(subscriptednames("β", length(β))) vnms = (:objective, :σ, bnms..., :θ) - value = (;(nm => nm == :θ ? SVector{k,T}[] : Vector{T}(undef, nsamp) - for nm in vnms)...) + value = (; (nm => nm == :θ ? SVector{k,T}[] : Vector{T}(undef, nsamp) for nm in vnms)...) try - @showprogress 1 for i in 1:nsamp + @showprogress 1 for i = 1:nsamp refit!(simulate!(rng, m, β = β, σ = σ, θ = θ)) value.objective[i] = objective(m) value.σ[i] = sdest(m) @@ -66,7 +71,7 @@ function parametricbootstrap(rng::AbstractRNG, nsamp::Integer, m::LinearMixedMod for (j, bnm) in enumerate(bnms) getproperty(value, bnm)[i] = βscr[j] end - push!(value.θ, SVector{k, T}(getθ!(θscr, m))) + push!(value.θ, SVector{k,T}(getθ!(θscr, m))) end finally refit!(m, y₀) @@ -74,12 +79,6 @@ function parametricbootstrap(rng::AbstractRNG, nsamp::Integer, m::LinearMixedMod MixedModelBootstrap(deepcopy(m), value) end -function mktable(nsamp, p, k, T) - nms = (:objective, :σ, Symbol.(subscriptednames("β", p))..., :θ) - (; (nm => nm == :θ ? Vector{SVector{k,T}}(undef, nsamp) : Vector{T}(undef, nsamp) for nm in nms)...) -end - - function parametricbootstrap(nsamp::Integer, m::LinearMixedModel, β = m.β, σ = m.σ, θ = m.θ) parametricbootstrap(Random.GLOBAL_RNG, nsamp, m, β = β, σ = σ, θ = θ) end @@ -93,7 +92,7 @@ function byreterm(bsamp::MixedModelBootstrap{T}, f::Function) where {T} oldθ = getθ(m) # keep a copy to restore later retrms = m.reterms value = [typeof(v)[] for v in f.(retrms, m.σ)] - for (σ,θ) in zip(bsamp.bstr.σ, bsamp.bstr.θ) + for (σ, θ) in zip(bsamp.bstr.σ, bsamp.bstr.θ) setθ!(m, θ) for (i, v) in enumerate(f.(retrms, σ)) push!(value[i], v) @@ -117,7 +116,7 @@ function shortestCovInt(v, level = 0.95) 0 < level < 1 || throw(ArgumentError("level = $level should be in (0,1)")) vv = issorted(v) ? v : sort(v) ilen = Int(ceil(n * level)) # the length of the interval in indices - len, i = findmin([vv[i + ilen - 1] - vv[i] for i in 1:(n + 1 - ilen)]) + len, i = findmin([vv[i+ilen-1] - vv[i] for i = 1:(n+1-ilen)]) vv[[i, i + ilen - 1]] end @@ -127,7 +126,13 @@ end Overwrite the response (i.e. `m.trms[end]`) with a simulated response vector from model `m`. """ -function simulate!(rng::AbstractRNG, m::LinearMixedModel{T}; β = coef(m), σ=m.σ, θ=T[]) where {T} +function simulate!( + rng::AbstractRNG, + m::LinearMixedModel{T}; + β = coef(m), + σ = m.σ, + θ = T[], +) where {T} isempty(θ) || setθ!(m, θ) y = randn!(rng, response(m)) # initialize y to standard normal for trm in m.reterms # add the unscaled random effects @@ -138,8 +143,8 @@ function simulate!(rng::AbstractRNG, m::LinearMixedModel{T}; β = coef(m), σ=m. m end -function simulate!(m::LinearMixedModel{T}; β=m.β, σ=m.σ, θ=T[]) where {T} - simulate!(Random.GLOBAL_RNG, m, β=β, σ=σ, θ=θ) +function simulate!(m::LinearMixedModel{T}; β = m.β, σ = m.σ, θ = T[]) where {T} + simulate!(Random.GLOBAL_RNG, m, β = β, σ = σ, θ = θ) end """ @@ -170,7 +175,7 @@ function unscaledre!(y::AbstractVector{T}, A::ReMat{T,S}, b::AbstractMatrix{T}) l = nlevs(A) length(y) == n && size(b) == (k, l) || throw(DimensionMismatch("")) @inbounds for (i, ii) in enumerate(A.refs) - for j in 1:k + for j = 1:k y[i] += Z[j, i] * b[j, ii] end end diff --git a/src/utilities.jl b/src/utilities.jl index c197d60cf..937fc3957 100644 --- a/src/utilities.jl +++ b/src/utilities.jl @@ -14,7 +14,7 @@ function densify(A::SparseMatrixCSC, threshold::Real = 0.25) # the diagonal is always dense (otherwise rank deficit) # so make sure it's stored as such Diagonal(Vector(diag(A))) - elseif nnz(A)/(m * n) ≤ threshold + elseif nnz(A) / (m * n) ≤ threshold A else Array(A) @@ -22,8 +22,9 @@ function densify(A::SparseMatrixCSC, threshold::Real = 0.25) end densify(A::AbstractMatrix, threshold::Real = 0.3) = A -densify(A::SparseVector, threshold::Real = 0.3) = Vector(A) -densify(A::Diagonal{T, SparseVector}, threshold::Real = 0.3) where T = Diagonal(Vector(A.diag)) +densify(A::SparseVector, threshold::Real = 0.3) = Vector(A) +densify(A::Diagonal{T,SparseVector}, threshold::Real = 0.3) where {T} = + Diagonal(Vector(A.diag)) """ RaggedArray{T,I} @@ -40,7 +41,8 @@ struct RaggedArray{T,I} vals::Vector{T} inds::Vector{I} end -function Base.sum!(s::AbstractVector{T}, a::RaggedArray{T}) where T + +function Base.sum!(s::AbstractVector{T}, a::RaggedArray{T}) where {T} for (v, i) in zip(a.vals, a.inds) s[i] += v end @@ -82,17 +84,6 @@ Return a `Vector{String}` of `nm` with subscripts from `₁` to `len` """ function subscriptednames(nm, len) nd = ndigits(len) - nd == 1 ? - [string(nm, '₀' + j) for j in 1:len] : - [string(nm, lpad(string(j), nd, '0')) for j in 1:len] -end -#= -updatedevresid!(r::GLM.GlmResp, η::AbstractVector) = updateμ!(r, η) - -fastlogitdevres(η, y) = 2log1p(exp(iszero(y) ? η : -η)) - -function updatedevresid!(r::GLM.GlmResp{V,<:Bernoulli,LogitLink}, η::V) where V<:AbstractVector{<:AbstractFloat} - map!(fastlogitdevres, r.devresid, η, r.y) - r + nd == 1 ? [string(nm, '₀' + j) for j = 1:len] : + [string(nm, lpad(string(j), nd, '0')) for j = 1:len] end -=# diff --git a/src/varcorr.jl b/src/varcorr.jl index 07cd6f398..7a919ba40 100644 --- a/src/varcorr.jl +++ b/src/varcorr.jl @@ -1,11 +1,11 @@ """ -VarCorr + VarCorr Information from the fitted random-effects variance-covariance matrices. # Members * `σρ`: a `NamedTuple` of `NamedTuple`s as returned from `σρs` -* `s`: the estimate of the scale parameter in the distribution of the conditional dist'n of Y +* `s`: the estimate of the per-observation dispersion parameter The main purpose of defining this type is to isolate the logic in the show method. """ @@ -37,21 +37,21 @@ function Base.show(io::IO, vc::VarCorr) write(io, cpad("Column", cnmwd)) write(io, cpad("Variance", varwd)) write(io, cpad("Std.Dev.", stdwd)) - iszero(nρ) || write(io," Corr.") + iszero(nρ) || write(io, " Corr.") println(io) ind = 1 - for (i,v) in enumerate(values(vc.σρ)) + for (i, v) in enumerate(values(vc.σρ)) write(io, rpad(nmvec[i], nmwd)) firstrow = true k = length(v.σ) # number of columns in grp factor k ρ = v.ρ ρind = 0 - for j in 1:k + for j = 1:k !firstrow && write(io, " "^nmwd) write(io, rpad(cnmvec[ind], cnmwd)) write(io, lpad(showvarvec[ind], varwd)) write(io, lpad(showσvec[ind], stdwd)) - for l in 1:(j - 1) + for l = 1:(j-1) ρind += 1 ρval = ρ[ρind] ρval === -0.0 ? write(io, " . ") : @printf(io, "%6.2f", ρval) diff --git a/test/linalg.jl b/test/linalg.jl index 259859a60..3d7390e39 100644 --- a/test/linalg.jl +++ b/test/linalg.jl @@ -1,7 +1,6 @@ using DataFrames, LinearAlgebra, MixedModels, Random, SparseArrays, StatsModels, Test -using MixedModels: mulαβ! -@testset "mulαβ!" begin +@testset "mul!" begin for (m, p, n, q, k) in ( (10, 0.7, 5, 0.3, 15), (100, 0.01, 100, 0.01, 20), @@ -15,12 +14,12 @@ using MixedModels: mulαβ! ab = a * b arbt = Array(b') aab = Array(a) * Array(b) - @test aab ≈ mulαβ!(c, a, bs', true, true) - @test aab ≈ mulαβ!(c, a, bs') - @test aab ≈ mulαβ!(c, a, arbt') - @test aab ≈ mulαβ!(c, a, arbt') - @test aab ≈ mulαβ!(fill!(c, 0.0), a, arbt', true, true) - @test maximum(abs, mulαβ!(c, a, arbt', -1.0, true)) ≤ sqrt(eps()) + @test aab ≈ mul!(c, a, bs', true, true) + @test aab ≈ mul!(c, a, bs') + @test aab ≈ mul!(c, a, arbt') + @test aab ≈ mul!(c, a, arbt') + @test aab ≈ mul!(fill!(c, 0.0), a, arbt', true, true) + @test maximum(abs, mul!(c, a, arbt', -1.0, true)) ≤ sqrt(eps()) @test maximum(abs.(ab - aab)) < 100*eps() @test a*bs' == ab @test as'*b == ab @@ -42,6 +41,13 @@ end @test loglikelihood(fit!(lmm1)) ≈ -578.9080978272708 end +@testset "rankupdate!" begin + @test ones(2, 2) == MixedModels.rankUpdate!(Hermitian(zeros(2, 2)), ones(2)) + d2 = Diagonal(fill(2., 2)) + @test Diagonal(fill(5.,2)) == MixedModels.rankUpdate!(Diagonal(ones(2)), d2, 1.) + @test Diagonal(fill(-3.,2)) == MixedModels.rankUpdate!(Diagonal(ones(2)), d2, -1.) +end + @testset "lmulλ!" begin gendata(n::Int, ng::Int) = gendata(MersenneTwister(42), n, ng) diff --git a/test/pirls.jl b/test/pirls.jl index e157d1093..763b52a96 100644 --- a/test/pirls.jl +++ b/test/pirls.jl @@ -1,5 +1,4 @@ using DataFrames, LinearAlgebra, MixedModels, RData, Test - if !@isdefined(dat) || !isa(dat, Dict{Symbol, DataFrame}) const dat = Dict(Symbol(k) => v for (k, v) in load(joinpath(dirname(pathof(MixedModels)), "..", "test", "dat.rda"))) @@ -22,20 +21,14 @@ end @test nobs(gm0) == 1934 fit!(gm0, fast=true, nAGQ=7) @test isapprox(deviance(gm0), 2360.9838, atol=0.001) - fit!(gm0, nAGQ=7) - @test isapprox(deviance(gm0), 2360.8760, atol=0.001) - @test gm0.β == gm0.beta - @test gm0.θ == gm0.theta - @test isnan(gm0.σ) - @test length(gm0.y) == size(gm0.X, 1) + gm1 = fit(MixedModel, contraform, contra, Bernoulli(), nAGQ=7) + @test isapprox(deviance(gm1), 2360.8760, atol=0.001) + @test gm1.β == gm1.beta + @test gm1.θ == gm1.theta + @test isnan(gm1.σ) + @test length(gm1.y) == size(gm1.X, 1) @test :θ in propertynames(gm0) - gm0.β = gm0.beta - @test gm0.β == gm0.beta - gm0.θ = gm0.theta - @test gm0.θ == gm0.theta gm0.βθ = vcat(gm0.β, gm0.theta) - @test gm0.β == gm0.beta - @test gm0.θ == gm0.theta # the next three values are not well defined in the optimization #@test isapprox(logdet(gm1), 75.7217, atol=0.1) #@test isapprox(sum(abs2, gm1.u[1]), 48.4747, atol=0.1) @@ -46,12 +39,12 @@ end @testset "cbpp" begin cbpp = dat[:cbpp] cbpp[!, :prop] = cbpp[!, :i] ./ cbpp[!, :s] - gm2 = fit(MixedModel, @formula(prop ~ 1 + p + (1|h)), cbpp, Binomial(), wts = cbpp[!,:s]) - @test isapprox(deviance(gm2,true), 100.09585619324639, atol=0.0001) - @test isapprox(sum(abs2, gm2.u[1]), 9.723175126731014, atol=0.0001) - @test isapprox(logdet(gm2), 16.90099, atol=0.0001) - @test isapprox(sum(gm2.resp.devresid), 73.47179193718736, atol=0.001) - @test isapprox(loglikelihood(gm2), -92.02628186555876, atol=0.001) + gm2 = fit(MixedModel, @formula(prop ~ 1 + p + (1|h)), cbpp, Binomial(), wts=cbpp[!,:s]) + @test isapprox(deviance(gm2,true), 100.09585619892968, atol=0.0001) + @test isapprox(sum(abs2, gm2.u[1]), 9.723054788538546, atol=0.0001) + @test isapprox(logdet(gm2), 16.90105378801136, atol=0.0001) + @test isapprox(sum(gm2.resp.devresid), 73.47174762237978, atol=0.001) + @test isapprox(loglikelihood(gm2), -92.02628186840045, atol=0.001) @test isnan(sdest(gm2)) @test varest(gm2) == 1 end @@ -63,15 +56,14 @@ end @test lowerbd(gm3) == vcat(fill(-Inf, 6), zeros(2)) @test fitted(gm3) == predict(gm3) # these two values are not well defined at the optimum - @test sum(x -> sum(abs2, x), gm3.u) ≈ 273.31563469936697 rtol=1e-3 - @test sum(gm3.resp.devresid) ≈ 7156.558983084621 rtol=1e-4 + @test isapprox(sum(x -> sum(abs2, x), gm3.u), 273.29266717430795, rtol=1e-3) + @test sum(gm3.resp.devresid) ≈ 7156.547357801238 rtol=1e-4 end @testset "grouseticks" begin gm4 = fit(MixedModel, @formula(t ~ 1 + y + ch + (1|i) + (1|b) + (1|l)), dat[:grouseticks], Poisson(), fast=true) # fails in pirls! with fast=false @test isapprox(deviance(gm4), 851.4046, atol=0.001) - @test lowerbd(gm4) == vcat(zeros(3)) # these two values are not well defined at the optimum #@test isapprox(sum(x -> sum(abs2, x), gm4.u), 196.8695297987013, atol=0.1) #@test isapprox(sum(gm4.resp.devresid), 220.92685781326136, atol=0.1) diff --git a/test/pls.jl b/test/pls.jl index 4e625942c..8be9ac38d 100644 --- a/test/pls.jl +++ b/test/pls.jl @@ -312,9 +312,16 @@ end @test deviance(fm) ≈ 339.0218639362958 atol=0.001 simulate!(fm, θ = fm.θ) @test_throws DimensionMismatch refit!(fm, zeros(29)) - bsamp = parametricbootstrap(MersenneTwister(1234321), 10, fm) - @test length(bsamp.objective) == 10 + bsamp = parametricbootstrap(MersenneTwister(1234321), 100, fm) + @test isa(propertynames(bsamp), Vector{Symbol}) + @test length(bsamp.objective) == 100 @test keys(bsamp.bstr) == (:objective, :σ, :β₁, :θ) + @test length(bsamp.objective) == 100 + @test isa(bsamp.model, LinearMixedModel) + @test isa(bsamp.σs, NamedTuple) + @test isa(bsamp.σρs, NamedTuple) + @test length(bsamp.σs) == 1 + @test shortestCovInt(bsamp.σ) ≈ [48.2551828768727, 81.85810781858969] rtol = 1.e-4 end @testset "Rank deficient" begin