Skip to content
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

Merged
merged 37 commits into from
Jun 28, 2017

Conversation

LewisHein
Copy link
Contributor

@LewisHein LewisHein commented May 17, 2017

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

@nalimilan
Copy link
Member

Thanks. I think the function should return a table similar to CoefTable rather than a list of objects when more than two models are passed. That would be more convenient for printing as well as to extract values.

src/ftest.jl Outdated
@@ -0,0 +1,75 @@
type FTestResult
mod1::RegressionModel
Copy link
Member

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
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure you can.

Copy link
Member

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.

Copy link
Contributor Author

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)
Copy link
Member

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)
Copy link
Member

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)
Copy link
Member

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)
Copy link
Member

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.

@nalimilan
Copy link
Member

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).

@LewisHein
Copy link
Contributor Author

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
Copy link
Member

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
Copy link
Member

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])
Copy link
Member

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.

@LewisHein
Copy link
Contributor Author

LewisHein commented May 17, 2017

Also, switching to LinPredModel has caused my tests to fail, with an error message about having no method matching ftest(::DataFrames.DataFrameRegressionModel{GLM.LinearModel{GLM.LmResp{Array{Float64,1}},GLM.DensePredQR{Float64}},Array{Float64,2}}, ::DataFrames.DataFrameRegressionModel{GLM.LinearModel{GLM.LmResp{Array{Float64,1}},GLM.DensePredQR{Float64}},Array{Float64,2}}).

How~~~~ ~~~~do~~~~ ~~~~I~~~~ ~~~~fix~~~~ ~~~~this?

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],
Copy link
Member

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?

Copy link
Contributor Author

@LewisHein LewisHein May 18, 2017

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?

Copy link
Member

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).

@nalimilan
Copy link
Member

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.

Unfortunately, you can't get the names of the coefficients at this level, they are only available for DataFrameRegressionModel objects (or other models with a model frame). We could add a specific method which would check this later in DataFrames/StatsModels, but that's beyond the scope of this PR.

What can be done in GLM.jl is to check that the fields of the LmResp object are equal, which should catch most problems.

@LewisHein
Copy link
Contributor Author

Well, this morning with a fresh perspective, I tackled the submodel test. Tell me what you think of my solution.

@LewisHein
Copy link
Contributor Author

LewisHein commented May 18, 2017

But can't you just do return false here?

Where?

Copy link
Member

@nalimilan nalimilan left a 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)
Copy link
Member

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
Copy link
Member

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)
Copy link
Member

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))
Copy link
Member

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]),
Copy link
Member

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
Copy link
Member

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).

Copy link
Contributor Author

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.

Copy link
Member

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}
Copy link
Member

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).

Copy link
Member

@nalimilan nalimilan left a 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)
Copy link
Member

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, :] = [
Copy link
Member

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
Copy link
Member

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, :] = [
Copy link
Member

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]), " ",
Copy link
Member

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"))
Copy link
Member

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)?

Copy link
Contributor Author

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

Copy link
Member

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
```
"""
Copy link
Member

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)
Copy link
Member

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).

Copy link
Member

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.

@nalimilan
Copy link
Member

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.

@LewisHein
Copy link
Contributor Author

OK -- fingers crossed...

@LewisHein
Copy link
Contributor Author

I have no idea what happened. I ran the tests before submitting, but now runtests.jl is all messed up.

@LewisHein
Copy link
Contributor Author

Now travis says it's failing on nightly Julia. What now?

@nalimilan
Copy link
Member

Don't worry about the nightlies, the failure was already there. I'll let @ararslan merge if he's OK with the new version.

@ararslan ararslan merged commit 5392a29 into JuliaStats:master Jun 28, 2017
@nalimilan
Copy link
Member

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!

@pbastide
Copy link
Contributor

Hi @LewisHein, @nalimilan and @ararslan,

Thank you for all your work on the GLM package !

We would need the ftest function in the PhyloNetwoks package, that already depends on GLM. Right now we use our own naive implementation of this function, but we would be happy to rely on your release in the future.

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 !

@nalimilan
Copy link
Member

nalimilan commented Sep 19, 2017

Sure. I've just tagged a new release, see JuliaLang/METADATA.jl#11275.

@pbastide
Copy link
Contributor

Thank you, that was fast !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants