Skip to content

Commit 4e72e76

Browse files
Implement single-model ftest (#424)
1 parent fa8c6e3 commit 4e72e76

File tree

5 files changed

+124
-6
lines changed

5 files changed

+124
-6
lines changed

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "GLM"
22
uuid = "38e38edf-8417-5370-95a0-9cbb8c7f171a"
3-
version = "1.5.1"
3+
version = "1.6.0"
44

55
[deps]
66
Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"

src/GLM.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@ module GLM
1515
fitted, fit, model_response, response, modelmatrix, r2, r², adjr2, adjr², PValue
1616
import StatsFuns: xlogy
1717
import SpecialFunctions: erfc, erfcinv, digamma, trigamma
18+
import StatsModels: hasintercept
1819
export coef, coeftable, confint, deviance, nulldeviance, dof, dof_residual,
1920
loglikelihood, nullloglikelihood, nobs, stderror, vcov, residuals, predict,
2021
fitted, fit, fit!, model_response, response, modelmatrix, r2, r², adjr2, adjr²,
21-
cooksdistance
22+
cooksdistance, hasintercept
2223

2324
export
2425
# types

src/ftest.jl

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
struct SingleFTestResult
2+
nobs::Int
3+
dof::Int
4+
fstat::Float64
5+
pval::Float64
6+
end
7+
18
mutable struct FTestResult{N}
29
nobs::Int
310
ssr::NTuple{N, Float64}
@@ -28,6 +35,37 @@ _diffn(t::NTuple{N, T}) where {N, T} = ntuple(i->t[i]-t[i+1], N-1)
2835

2936
_diff(t::NTuple{N, T}) where {N, T} = ntuple(i->t[i+1]-t[i], N-1)
3037

38+
"""
39+
ftest(mod::LinearModel)
40+
41+
Perform an F-test to determine whether model `mod` fits significantly better
42+
than the null model (i.e. which includes only the intercept).
43+
44+
```jldoctest
45+
julia> dat = DataFrame(Result=[1.1, 1.2, 1, 2.2, 1.9, 2, 0.9, 1, 1, 2.2, 2, 2],
46+
Treatment=[1, 1, 1, 2, 2, 2, 1, 1, 1, 2, 2, 2]);
47+
48+
julia> model = lm(@formula(Result ~ 1 + Treatment), dat);
49+
50+
julia> ftest(model.model)
51+
F-test against the null model:
52+
F-statistic: 241.62 on 12 observations and 1 degrees of freedom, p-value: <1e-07
53+
```
54+
"""
55+
function ftest(mod::LinearModel)
56+
hasintercept(mod) || throw(ArgumentError("ftest only works for models with an intercept"))
57+
58+
rss = deviance(mod)
59+
tss = nulldeviance(mod)
60+
61+
n = Int(nobs(mod))
62+
p = dof(mod) - 2 # -2 for intercept and dispersion parameter
63+
fstat = ((tss - rss) / rss) * ((n - p - 1) / p)
64+
fdist = FDist(p, dof_residual(mod))
65+
66+
SingleFTestResult(n, p, promote(fstat, ccdf(fdist, abs(fstat)))...)
67+
end
68+
3169
"""
3270
ftest(mod::LinearModel...; atol::Real=0.0)
3371
@@ -85,9 +123,6 @@ F-test: 3 models fitted on 12 observations
85123
```
86124
"""
87125
function ftest(mods::LinearModel...; atol::Real=0.0)
88-
if length(mods) < 2
89-
Base.depwarn("Passing less than two models to ftest is deprecated", :ftest)
90-
end
91126
if !all(==(nobs(mods[1])), nobs.(mods))
92127
throw(ArgumentError("F test is only valid for models fitted on the same data, " *
93128
"but number of observations differ"))
@@ -127,6 +162,12 @@ function ftest(mods::LinearModel...; atol::Real=0.0)
127162
return FTestResult(Int(nobs(mods[1])), SSR, df, r2.(mods), fstat, pval)
128163
end
129164

165+
function show(io::IO, ftr::SingleFTestResult)
166+
print(io, "F-test against the null model:\nF-statistic: ", StatsBase.TestStat(ftr.fstat), " ")
167+
print(io, "on ", ftr.nobs, " observations and ", ftr.dof, " degrees of freedom, ")
168+
print(io, "p-value: ", PValue(ftr.pval))
169+
end
170+
130171
function show(io::IO, ftr::FTestResult{N}) where N
131172
Δdof = _diff(ftr.dof)
132173
Δssr = _diff(ftr.ssr)

src/linpred.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,3 +255,5 @@ coef(x::LinPred) = x.beta0
255255
coef(obj::LinPredModel) = coef(obj.pp)
256256

257257
dof_residual(obj::LinPredModel) = nobs(obj) - dof(obj) + 1
258+
259+
hasintercept(m::LinPredModel) = any(i -> all(==(1), view(m.pp.X , :, i)), 1:size(m.pp.X, 2))

test/runtests.jl

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -643,7 +643,62 @@ end
643643
@test preds_delta.upper .- preds_delta.lower 2 .* 1.96 .* R_glm_se atol=1e-3
644644
@test_throws ArgumentError predict(gm, newX, interval=:confidence, interval_method=:undefined_method)
645645
@test_throws ArgumentError predict(gm, newX, interval=:undefined)
646-
end
646+
end
647+
648+
@testset "F test comparing to null model" begin
649+
d = DataFrame(Treatment=[1, 1, 1, 2, 2, 2, 1, 1, 1, 2, 2, 2.],
650+
Result=[1.1, 1.2, 1, 2.2, 1.9, 2, .9, 1, 1, 2.2, 2, 2],
651+
Other=categorical([1, 1, 2, 1, 2, 1, 3, 1, 1, 2, 2, 1]))
652+
mod = lm(@formula(Result~Treatment), d).model
653+
othermod = lm(@formula(Result~Other), d).model
654+
nullmod = lm(@formula(Result~1), d).model
655+
bothmod = lm(@formula(Result~Other+Treatment), d).model
656+
nointerceptmod = lm(reshape(d.Treatment, :, 1), d.Result)
657+
658+
ft1 = ftest(mod)
659+
ft1base = ftest(nullmod, mod)
660+
@test ft1.nobs == ft1base.nobs
661+
@test ft1.dof dof(mod) - dof(nullmod)
662+
@test ft1.fstat ft1base.fstat[2]
663+
@test ft1.pval ft1base.pval[2]
664+
if VERSION >= v"1.6.0"
665+
@test sprint(show, ft1) == """
666+
F-test against the null model:
667+
F-statistic: 241.62 on 12 observations and 1 degrees of freedom, p-value: <1e-07"""
668+
else
669+
@test sprint(show, ft1) == """
670+
F-test against the null model:
671+
F-statistic: 241.62 on 12 observations and 1 degrees of freedom, p-value: <1e-7"""
672+
end
673+
674+
ft2 = ftest(othermod)
675+
ft2base = ftest(nullmod, othermod)
676+
@test ft2.nobs == ft2base.nobs
677+
@test ft2.dof dof(othermod) - dof(nullmod)
678+
@test ft2.fstat ft2base.fstat[2]
679+
@test ft2.pval ft2base.pval[2]
680+
@test sprint(show, ft2) == """
681+
F-test against the null model:
682+
F-statistic: 1.12 on 12 observations and 2 degrees of freedom, p-value: 0.3690"""
683+
684+
ft3 = ftest(bothmod)
685+
ft3base = ftest(nullmod, bothmod)
686+
@test ft3.nobs == ft3base.nobs
687+
@test ft3.dof dof(bothmod) - dof(nullmod)
688+
@test ft3.fstat ft3base.fstat[2]
689+
@test ft3.pval ft3base.pval[2]
690+
if VERSION >= v"1.6.0"
691+
@test sprint(show, ft3) == """
692+
F-test against the null model:
693+
F-statistic: 81.97 on 12 observations and 3 degrees of freedom, p-value: <1e-05"""
694+
else
695+
@test sprint(show, ft3) == """
696+
F-test against the null model:
697+
F-statistic: 81.97 on 12 observations and 3 degrees of freedom, p-value: <1e-5"""
698+
end
699+
700+
@test_throws ArgumentError ftest(nointerceptmod)
701+
end
647702

648703
@testset "F test for model comparison" begin
649704
d = DataFrame(Treatment=[1, 1, 1, 2, 2, 2, 1, 1, 1, 2, 2, 2.],
@@ -895,3 +950,22 @@ end
895950
@test hash(NegativeBinomialLink(0.3)) == hash(NegativeBinomialLink(0.3))
896951
@test hash(NegativeBinomialLink(0.31)) != hash(NegativeBinomialLink(0.3))
897952
end
953+
954+
@testset "hasintercept" begin
955+
d = DataFrame(Treatment=[1, 1, 1, 2, 2, 2, 1, 1, 1, 2, 2, 2.],
956+
Result=[1.1, 1.2, 1, 2.2, 1.9, 2, .9, 1, 1, 2.2, 2, 2],
957+
Other=categorical([1, 1, 2, 1, 2, 1, 3, 1, 1, 2, 2, 1]))
958+
959+
mod = lm(@formula(Result~Treatment), d).model
960+
@test hasintercept(mod)
961+
962+
nullmod = lm(@formula(Result~1), d).model
963+
@test hasintercept(nullmod)
964+
965+
nointerceptmod = lm(reshape(d.Treatment, :, 1), d.Result)
966+
@test !hasintercept(nointerceptmod)
967+
968+
rng = StableRNG(1234321)
969+
secondcolinterceptmod = glm([randn(rng, 5) ones(5)], ones(5), Binomial(), LogitLink())
970+
@test hasintercept(secondcolinterceptmod)
971+
end

0 commit comments

Comments
 (0)