-
Notifications
You must be signed in to change notification settings - Fork 114
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add F test for comparing two models #182
Conversation
Thanks. I think the function should return a table similar to |
src/ftest.jl
Outdated
@@ -0,0 +1,75 @@ | |||
type FTestResult | |||
mod1::RegressionModel |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should probably not store the original models, that can take a lot of space and can be tracked separately if needed.
src/ftest.jl
Outdated
end | ||
end | ||
|
||
export ftest |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exports should go to StatsBase.jl.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure what you mean here -- I can't export ftest until I've defined it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure you can.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bindings in export
statements aren't resolved immediately. As an example,
julia> baremodule Test
export A # has no definition
end
Test
julia> using Test # doesn't error even though A is exported and undefined
julia> Test.A
ERROR: UndefVarError: A not defined
Julia doesn't care until you try to call it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, you're right. It appears that I made a typo when I tried it the first time
src/ftest.jl
Outdated
end | ||
|
||
function ftest(mod1::RegressionModel, mod2::RegressionModel) | ||
SSR1 = deviance(mod1.model.rr) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should call deviance
on mod1
to be fully generic.
src/ftest.jl
Outdated
SSR1 = deviance(mod1.model.rr) | ||
SSR2 = deviance(mod2.model.rr) | ||
|
||
nparams1 = length(mod1.model.pp.beta0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use dof(mod1)
.
src/ftest.jl
Outdated
results::Array{FTestResult, 1} | ||
end | ||
|
||
function ftest(mod1::RegressionModel, mod2::RegressionModel) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The F test is only valid for linear regressions, so the signature should be stricter.
src/ftest.jl
Outdated
|
||
function ftest(mods::RegressionModel...) | ||
nmodels = length(mods) | ||
results = Array{FTestResult, 1}((nmodels^2)-nmodels) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What R's anova
and Stata's nestreg
do is to compare each model to the previous one. Computing all possible comparisons quickly gets very messy.
It would also be nice to check that the models were fitted on the same data, at least by checking that the response vectors are equal (and maybe even that all common variables are equal). |
This would be great; however, I am not sure how to get the common variables of the models. I am pretty new to the internals of GLM.jl, so please forgive my ignorance. |
src/GLM.jl
Outdated
@@ -53,7 +53,8 @@ module GLM | |||
nobs, # total number of observations | |||
predict, # make predictions | |||
updateμ!, # update the response type from the linear predictor | |||
wrkresp # working response | |||
wrkresp, # working response | |||
ftest # compare two models with an F test |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please indent with spaces instead of tabs
src/ftest.jl
Outdated
MSR1s[i] = result.MSR1 | ||
MSR2s[i] = result.MSR2 | ||
fstats[i] = result.fstat | ||
pvals[i] = result.pval |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same for these. Indents should be 4 spaces each.
src/ftest.jl
Outdated
end | ||
|
||
return MultiFTestResult(results) | ||
return CoefTable([SSR1s, SSR2s, df1s, df2s, MSR1s, MSR2s, fstats, pvals], ["Model 1 SSR", "Model 2 SSR", "Model 1 df", "Model 2 df", "Model 1 MSR", "Model 2 MSR", "F statistic", "p-value"], ["Model $(i-1):$i" for i in 2:nmodels]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be good to break this long line into multiple lines for readability.
Also, switching to LinPredModel has caused my tests to fail, with an error message about having no method matching
EDIT: Got it. and it was simple, too |
src/ftest.jl
Outdated
pvals[i] = result.pval | ||
end | ||
|
||
return CoefTable([SSR1s, SSR2s, df1s, df2s, MSR1s, MSR2s, fstats, pvals], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry if I wasn't clear, I didn't mean to (ab)use CoefTable
for this, but to take inspiration from it to define the MultiFTestResult
type (could be called FTestTable
BTW). That type would have the same fields as FTestResult
, but those would be vectors rather than scalars (one column for each info).
I'm not even sure we need FTestResult
for the special case of a two-model comparison. Opinions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't an opinion, but rather an explanation. I defined FTestResult purely so that I could define show() for it and have the result nicely printed.
Is there some more standard way to do this?
EDIT: @nalimilan I finally see your point: define just FTestTable, and for comparing two models, return an FTestTable with only one test in it. Correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not totally "standard", but CoefTable
(in StatsBase) is a very similar case (lightweight structure to be used where R would use a data frame, which is overkill in Julia).
Unfortunately, you can't get the names of the coefficients at this level, they are only available for What can be done in GLM.jl is to check that the fields of the |
Well, this morning with a fresh perspective, I tackled the submodel test. Tell me what you think of my solution. |
Where? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, this looks quite good. Would be nice to check whether any allocation happens when calling ftest
(though that can be fixed later).
We also need to solve the naming issue.
src/ftest.jl
Outdated
_diff{N, T}(t::NTuple{N, T})::NTuple{N, T} = ntuple(i->t[i+1]-t[i], N-1) | ||
|
||
import Base: ./ | ||
./{N, T1, T2}(t1::NTuple{N, T1}, t2::NTuple{N, T2}) = ntuple(i->t1[i]/t2[i], N) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately you can't do that as that will affect other packages. You can just use ntuple
directly, or define a custom function.
src/ftest.jl
Outdated
pval::Tuple{Vararg{PValue}} | ||
end | ||
|
||
#function FTestResult{N}(SSR1::Array{Float64, 1}, fstati |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove this comment (and others), and fix indentation below.
src/ftest.jl
Outdated
|
||
nc = 10 | ||
nr = N | ||
outrows = Array{String, 2}(nr+1, nc) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Matrix{String}
is slightly nicer to read.
src/ftest.jl
Outdated
cur_cell_len = length(cur_cell) | ||
|
||
print(io, cur_cell) | ||
print(io, RepString(" ", max_colwidths[c]-cur_cell_len+1)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use " "^(max_colwidths[c]-cur_cell_len+1)
as RepString
is likely to be deprecated at some point.
src/ftest.jl
Outdated
|
||
outrows[1, :] = ["", "Res. DOF", "DOF", "ΔDOF", "SSR", | ||
"ΔSSR", "R²", "ΔR²", "F*", "p(>F)"] | ||
outrows[2, :] = ["Model 1", @sprintf("%.4f", ftr.dof_resid[1]), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use a more regular organization, it's very hard to read as-is. Same below.
Also, DOF and number of parameters should be printed as integers, not floats.
test/runtests.jl
Outdated
@test sprint(show, ftest(mod, nullmod)) == | ||
""" | ||
Res. DOF DOF ΔDOF SSR ΔSSR R² ΔR² F* p(>F) | ||
Model 1 10.0000 3.0000 0.1283 0.9603 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a big deal, but I wonder whether it would be possible to force @printf
to align number on the decimal separator (see e.g. the R² column).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe, but I'd have to play with it a good bit to figure out how.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, that was just an idea. I know CoefTable
has the same issue.
src/ftest.jl
Outdated
@@ -0,0 +1,155 @@ | |||
type FTestResult{N} | |||
ssr::NTuple{N, Float64} | |||
nparams::NTuple{N, Int} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably better call this dof
, as it's the result of calling dof(model)
? Also params
is going to have a slightly different meanings from coef
(JuliaStats/StatsBase.jl#274).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, with the next round we should be ready if @andreasnoack validates and we agree on a name.
src/ftest.jl
Outdated
|
||
_diffn{N, T}(t::NTuple{N, T})::NTuple{N, T} = ntuple(i->t[i]-t[i+1], N-1) | ||
|
||
_diff{N, T}(t::NTuple{N, T})::NTuple{N, T} = ntuple(i->t[i+1]-t[i], N-1) | ||
|
||
import Base: ./ | ||
./{N, T1, T2}(t1::NTuple{N, T1}, t2::NTuple{N, T2}) = ntuple(i->t1[i]/t2[i], N) | ||
dividetuple{N, T1, T2}(t1::NTuple{N, T1}, t2::NTuple{N, T2}) = ntuple(i->t1[i]/t2[i], N) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
T1
and T2
are not needed. Maybe call this dividetuples
?
src/ftest.jl
Outdated
outrows[2, :] = ["Model 1", @sprintf("%.4f", ftr.dof_resid[1]), | ||
@sprintf("%.4f", ftr.nparams[1]), " ", @sprintf("%.4f", ftr.ssr[1]), | ||
" ", @sprintf("%.4f", ftr.r2[1]), " ", " ", " "] | ||
outrows[1, :] = [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd group these a bit more, e.g. on two or three lines.
test/runtests.jl
Outdated
Model 1 10.0000 3.0000 0.1283 0.9603 | ||
Model 2 11.0000 2.0000 -1.0000 3.2292 -3.1008 -0.0000 0.9603 241.6234 <1e-7 | ||
Res. DOF DOF ΔDOF SSR ΔSSR R² ΔR² F* p(>F) | ||
Model 1 10 3 0.1283 0.9603 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you align integers on the right? That makes it easier to read by columns since digits of the same rank are aligned. (BTW, if you do the same for floats, they will be automatically aligned on the decimal separator since their are fixed to four digits after it.)
src/ftest.jl
Outdated
"p(>F)" | ||
] | ||
|
||
outrows[2, :] = [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here too you can put an item on the first line, and the closing bracket with the last element. Same below.
src/ftest.jl
Outdated
"R²", "ΔR²", "F*", "p(>F)"] | ||
|
||
outrows[2, :] = ["Model 1", @sprintf("%.0d", ftr.dof_resid[1]), | ||
@sprintf("%.0d", ftr.dof[1]), " ", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be aligned with "
(as it's part of the array, so inside [
).
src/ftest.jl
Outdated
nmodels = length(mods) | ||
for i in 2:nmodels | ||
issubmodel(mods[i], mods[i-1]) || | ||
throw(ArgumentError("F test $i is only valid if model $i is nested in model $i-1")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be $(i-1)
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, good catch
src/ftest.jl
Outdated
print(io, "\n") | ||
end | ||
end | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since it looks like we'll need another round of fixes, it would be nice to remove these two empty lines in your next (and hopefully last) commit.
src/ftest.jl
Outdated
Model 1 10 3 0.1283 0.9603 | ||
Model 2 11 2 -1 3.2292 -3.1008 -0.0000 0.9603 241.6234 <1e-7 | ||
``` | ||
""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd format it like this:
"""
ftest(mod::LinearModel...)
For each sequential pair of linear predictors in `mod`, perform an F-test to determine if
the first one fits significantly better than the next.
A table is returned containing residual degrees of freedom (DOF), degrees of freedom,
difference in DOF from the preceding model, sum of squared residuals (SSR), difference in
SSR from the preceding model, R², difference in R² from the preceding model, and F-statistic
and p-value for the comparison between the two models.
!!! note
This function can be used to perform an ANOVA by testing the relative fit of two models
to the data
# Examples
Suppose we want to compare the effects of two or more treatments on some result. Because
this is an ANOVA, our null hypothesis is that `Result~1` fits the data as well as
`Result~Treatment`.
```jldoctest
julia> dat = DataFrame(Treatment=[1, 1, 1, 2, 2, 2, 1, 1, 1, 2, 2, 2.],
Result=[1.1, 1.2, 1, 2.2, 1.9, 2, .9, 1, 1, 2.2, 2, 2]);
julia> mod = lm(@formula(Result~Treatment), dat);
julia> nullmod = lm(@formula(Result~1), dat);
julia> ft = ftest(mod.model, nullmod.model)
Res. DOF DOF ΔDOF SSR ΔSSR R² ΔR² F* p(>F)
Model 1 10 3 0.1283 0.9603
Model 2 11 2 -1 3.2292 -3.1008 -0.0000 0.9603 241.6234 <1e-7
"""
There are a few modifications in here, too lazy to enumerate them rather than just copy-pasting and editing :P
src/ftest.jl
Outdated
return true | ||
end | ||
|
||
_diffn{N, T}(t::NTuple{N, T})::NTuple{N, T} = ntuple(i->t[i]-t[i+1], N-1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
::NTuple{N, T}
is actually wrong here, since the result has N-1
elements. Just drop the type assertion (same below).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And BTW, that's the cause of the tests failure.
There's also a double space.
Note that I resolved conflicts with master directly via GitHub, so you'll have either to pull from the branch, or to rebase locally against master and then force-push. |
OK -- fingers crossed... |
I have no idea what happened. I ran the tests before submitting, but now runtests.jl is all messed up. |
Now travis says it's failing on nightly Julia. What now? |
Don't worry about the nightlies, the failure was already there. I'll let @ararslan merge if he's OK with the new version. |
Thanks @LewisHein! Sorry if the process was a bit long. If you want to implement the likelihood ratio test at some point, now that we have settled the API, that would be great! |
Hi @LewisHein, @nalimilan and @ararslan, Thank you for all your work on the GLM package ! We would need the However, if I'm correct, right now v0.7.0 does not contain the changes you made here. Are you planing on making this feature available in a release soon ? That would be great for us, so that we could require this new version, and use your function. Sorry if this is not the right place to ask, or if the answer is already somewhere else. If so, I'd be grateful for you to point me in the right direction. Thank you ! |
Sure. I've just tagged a new release, see JuliaLang/METADATA.jl#11275. |
Thank you, that was fast ! |
Here is a rough first draft of what I'm thinking for the F-test for model comparison. If you all think I am on the right track, I'll write up some docs. The addition to the test suite tests a single ANOVA run against the p-value from R's implementation. This is for addressing #181