diff --git a/Project.toml b/Project.toml index 80bff94..39dd9ac 100644 --- a/Project.toml +++ b/Project.toml @@ -8,6 +8,7 @@ Combinatorics = "861a8166-3701-5b0c-9a16-15d98fcdc6aa" StatsModels = "3eaba693-59b7-5ba5-a881-562e759f1c8d" [compat] +Combinatorics = "1" StatsBase = "0.33" StatsModels = "0.6.7" julia = "1.6" diff --git a/src/nesting.jl b/src/nesting.jl index 8d36b67..fbf4290 100644 --- a/src/nesting.jl +++ b/src/nesting.jl @@ -4,6 +4,13 @@ function _isfulldummy(x::CategoricalTerm) return isa(x.contrasts, StatsModels.ContrastsMatrix{StatsModels.FullDummyCoding}) end +function _fulldummycheck(outer::InteractionTerm) + all(_isfulldummy, outer.terms[1:end-1]) || + throw(ArgumentError("Outer interactions in a nesting must consist only " * + " of categorical terms with FullDummyCoding, got $outer")) + return nothing +end + """ group / term @@ -14,19 +21,29 @@ function Base.:(/)(outer::CategoricalTerm, inner::AbstractTerm) return outer + fulldummy(outer) & inner end -function Base.:(/)(outer::TermTuple, inner::AbstractTerm) +function Base.:(/)(outer::CategoricalTerm, inner::TermTuple) + fd = fulldummy(outer) + return mapfoldl(x -> fd & x, +, inner; init=outer) +end + +function Base.:(/)(outer::TermTuple, inner::Union{AbstractTerm, TermTuple}) return outer[1:end-1] + last(outer) / inner end function Base.:(/)(outer::InteractionTerm, inner::AbstractTerm) # we should only get here via expansion where the interaction term, # but who knows what devious things users will try - all(_isfulldummy, outer.terms[1:end-1]) || - throw(ArgumentError("Outer interactions in a nesting must consist only " * - " of categorical terms with FullDummyCoding, got $outer")) + _fulldummycheck(outer) return outer + outer & inner end +function Base.:(/)(outer::InteractionTerm, inner::TermTuple) + # we should only get here via expansion where the interaction term, + # but who knows what devious things users will try + _fulldummycheck(outer) + return mapfoldl(x -> outer & x, +, inner; init=outer) +end + function Base.:(/)(outer::AbstractTerm, inner::AbstractTerm) throw(ArgumentError("nesting terms requires categorical grouping term, got $outer / $inner " * "Manually specify $outer as `CategoricalTerm` in hints/contrasts")) diff --git a/test/nesting.jl b/test/nesting.jl index dea2162..4d6d77c 100644 --- a/test/nesting.jl +++ b/test/nesting.jl @@ -31,6 +31,19 @@ end m = fit(DummyMod, @formula(y ~ 1 + a / x), dat) @test coefnames(m) == ["(Intercept)", "a: o", "a: u", "a: i & x", "a: o & x", "a: u & x"] + + m = fit(DummyMod, @formula(y ~ 0 + a / (b + x)), dat) + @test coefnames(m) == ["a: i", "a: o", "a: u", + "a: i & b: q", "a: o & b: q", "a: u & b: q", + "a: i & b: w", "a: o & b: w", "a: u & b: w", + "a: i & x", "a: o & x", "a: u & x"] + m = fit(DummyMod, @formula(y ~ 0 + a / (b * x)), dat) + @test coefnames(m) == ["a: i", "a: o", "a: u", + "a: i & b: q", "a: o & b: q", "a: u & b: q", + "a: i & b: w", "a: o & b: w", "a: u & b: w", + "a: i & x", "a: o & x", "a: u & x", + "a: i & b: q & x", "a: o & b: q & x", "a: u & b: q & x", + "a: i & b: w & x", "a: o & b: w & x", "a: u & b: w & x"] end @testset "multiple nesting levels" begin @@ -68,4 +81,19 @@ end "a: i & b: q & x", "a: o & b: q & x", "a: u & b: q & x", "a: i & b: w & x", "a: o & b: w & x", "a: u & b: w & x"] + + m = fit(DummyMod, @formula(y ~ 0 + a / b / (c * x)), dat) + @test coefnames(m) == ["a: i", "a: o", "a: u", + "a: i & b: q", "a: o & b: q", "a: u & b: q", + "a: i & b: w", "a: o & b: w", "a: u & b: w", + "a: i & b: q & c: f", "a: o & b: q & c: f", "a: u & b: q & c: f", + "a: i & b: w & c: f", "a: o & b: w & c: f", "a: u & b: w & c: f", + "a: i & b: q & c: s", "a: o & b: q & c: s", "a: u & b: q & c: s", + "a: i & b: w & c: s", "a: o & b: w & c: s", "a: u & b: w & c: s", + "a: i & b: q & x", "a: o & b: q & x", "a: u & b: q & x", + "a: i & b: w & x", "a: o & b: w & x", "a: u & b: w & x", + "a: i & b: q & c: f & x", "a: o & b: q & c: f & x", "a: u & b: q & c: f & x", + "a: i & b: w & c: f & x", "a: o & b: w & c: f & x", "a: u & b: w & c: f & x", + "a: i & b: q & c: s & x", "a: o & b: q & c: s & x", "a: u & b: q & c: s & x", + "a: i & b: w & c: s & x", "a: o & b: w & c: s & x", "a: u & b: w & c: s & x"] end