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

Correct R² formulas and add Efron's R² #400

Merged
merged 7 commits into from
Oct 3, 2018
Merged

Correct R² formulas and add Efron's R² #400

merged 7 commits into from
Oct 3, 2018

Conversation

lbittarello
Copy link
Contributor

@lbittarello lbittarello commented Jul 22, 2018

This PR fixed the R² formulas, which were previously based on the deviance instead of the likelihood function. It also adds Efron's R².
The following variants were benchmarked against Stata's fitstat: McFadden, Cox and Snell, Nagelkerke and Efron.
Cox and Snell's and Efron's R² should match the classical R² for linear models.

Closes #396.

This PR fixed the R² formulas, which were previously based on the deviance instead of the likelihood function. It also adds Efron's R².
The following variants were benchmarked against Stata's fitstat: McFadden, Cox and Snell, Nagelkerke and Efron.
Cox and Snell's and Efron's R² should match the classical R² for linear models.
@codecov
Copy link

codecov bot commented Jul 22, 2018

Codecov Report

Merging #400 into master will decrease coverage by 0.07%.
The diff coverage is n/a.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #400      +/-   ##
==========================================
- Coverage   97.97%   97.89%   -0.08%     
==========================================
  Files          17       16       -1     
  Lines        1282     1281       -1     
==========================================
- Hits         1256     1254       -2     
- Misses         26       27       +1
Impacted Files Coverage Δ
src/statmodels.jl 97.77% <ø> (ø) ⬆️
src/scalarstats.jl 96.66% <0%> (-0.72%) ⬇️
src/weights.jl 99.29% <0%> (-0.03%) ⬇️
src/hist.jl 97.18% <0%> (-0.02%) ⬇️
src/moments.jl 100% <0%> (ø) ⬆️
src/cov.jl 100% <0%> (ø) ⬆️
src/common.jl

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update cc9ad6e...7b40de8. Read the comment docs.

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. Sorry for the delay, I had missed the PR.

@@ -197,7 +197,7 @@ Coefficient of determination (R-squared).
For a linear model, the R² is defined as ``ESS/TSS``, with ``ESS`` the explained sum of squares
and ``TSS`` the total sum of squares.
"""
r2(obj::StatisticalModel) = mss(obj) / deviance(obj)
r2(obj::StatisticalModel) = mss(obj) / sum(abs2, response(obj) .- meanresponse(obj))
Copy link
Member

@nalimilan nalimilan Sep 2, 2018

Choose a reason for hiding this comment

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

Better do μ = meanresponse(obj); sum(x -> abs2(x - μ), response(obj)) to avoid allocating a copy. Maybe turn this into a long-form function for readability.

@Nosferican Is this formula OK for your package?

EDIT: actually I think we'd better deprecate this function and leave packages to implement it for linear models. That will give a clearer error message when called on a nonlinear model (instead of an error about mss not being defined).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

avoid allocating a copy

I thought that sum didn't allocate transform scalars into vectors with the dot syntax.

we'd better deprecate this function and leave packages to implement it for linear models

Do you mean removing the function and leaving a warning or leaving the function and adding a warning?

Copy link
Contributor

Choose a reason for hiding this comment

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

Consider this script,

using BenchmarkTools
a(x, y) = sum(abs2, x - y)
b(x, y) = sum(abs2, x .- y)
c(x, y) = sum(xy -> abs2(xy[1] - xy[2]), zip(x, y))
d(x, y) = sum(abs2(a - b) for (a, b) ∈ zip(x, y))
using Random: seed!
seed!(0)
const x = rand(1000);
const y = rand(1000);
@btime a($x, $y)
@btime b($x, $y)
@btime c($x, $y)
@btime d($x, $y)
  703.416 ns (1 allocation: 7.94 KiB)
  659.205 ns (1 allocation: 7.94 KiB)
  823.250 ns (1 allocation: 32 bytes)
  969.684 ns (2 allocations: 48 bytes)

It seems to be trivially faster, but it certainly allocates as opposed to c or d.

If the matches Stata, it will match mine. Some models will still need to define the method for some panel estimators, but this seems good.

If the statsmodels functions are being moved to StatsModels it might be best to make the decision there (better to trim there, than to have lost useful code then),

Copy link
Member

Choose a reason for hiding this comment

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

@lbittarello:

I thought that sum didn't allocate transform scalars into vectors with the dot syntax.

As @Nosferican shows, it allocates, and it will probably stay that way in the future. Maybe another syntax will be introduced to do that.

Do you mean removing the function and leaving a warning or leaving the function and adding a warning?

I mean print a warning using Base.depwarn(..., :r2), saying that packages should define their custom method. Then we'll turn the warning into an error just like for other function stubs in the file.

@Nosferican:

If the R² matches Stata, it will match mine. Some models will still need to define the method for some panel estimators, but this seems good.

OK, cool.

If the statsmodels functions are being moved to StatsModels it might be best to make the decision there (better to trim there, than to have lost useful code then),

It's not clear yet what will happen and when, and given how small this function is that's really not an issue.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've added a deprecation warning. Let me know if it's okay.

src/statmodels.jl Show resolved Hide resolved
y = response(obj)
ŷ = fitted(obj)
μ = meanresponse(obj)
1 - sum(abs2, y .- ŷ) / sum(abs2, y .- μ)
Copy link
Member

Choose a reason for hiding this comment

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

Same remark as above about avoiding allocations.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

check

- `:MacFadden` (a.k.a. likelihood ratio index), defined as ``1 - \\log (L)/\\log (L_0)``;
- `:CoxSnell`, defined as ``1 - (L_0/L)^{2/n}``;
- `:Nagelkerke`, defined as ``(1 - (L_0/L)^{2/n})/(1 - L_0^{2/n})``;
- `:Efron`, defined as ``1 - \\sum_i (y_i - \\hat{y})^2 / \\sum_i (y_i - \\bar{y})^2``.
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 \\har{y}_i.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

check

1 - exp(2/nobs(obj) * (ll0 - ll))
elseif variant == :Nagelkerke
(1 - exp(2/nobs(obj) * (ll0 - ll)))/(1 - exp(2/nobs(obj) * ll0))
# The following variants were benchmarked against Stata's fitstat:
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 rather put this mention in the tests next to the expected values. The explanation about linear models can go to the docstring.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Tests in GLM.jl?

Copy link
Contributor

Choose a reason for hiding this comment

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

Some tests here are run using GLM which uses this package... Might be best test these eventually without another package for it.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, in GLM.jl (since the check against Stata applies to GLMs AFAICT).

It's not ideal to have this split in two packages, but testing things directly here implies writing pseudo-model types. That's not so hard, but it will take some work.

@@ -259,10 +271,9 @@ In this formula, ``L`` is the likelihood of the model, ``L0`` that of the null m
of the model (as returned by [`dof`](@ref)).
"""
function adjr2(obj::StatisticalModel, variant::Symbol)
ll = -deviance(obj)/2
ll0 = -nulldeviance(obj)/2
ll = likelihood(obj)
Copy link
Member

Choose a reason for hiding this comment

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

AFAICT you want loglikelihood here. The fact it wasn't caught by testing is a bit worrying: isn't this covered by GLM's tests?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry. You're right. I hadn't tested the adjusted R².

Copy link
Member

Choose a reason for hiding this comment

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

OK. Do you confirm it works know?

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, sir!

src/statmodels.jl Show resolved Hide resolved
@nalimilan nalimilan changed the title Closes #396 Correct R² formulas and add Efron's R² Sep 2, 2018
y = response(obj)
ŷ = fitted(obj)
μ = meanresponse(obj)
1 - sum(abs2, y .- ŷ) / sum(x -> abs2(x - μ), response(obj))
Copy link
Member

Choose a reason for hiding this comment

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

sum(abs2, y .- ŷ) also needs to be changed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

check

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.

Looks good, thanks!

"Packages should define their own methods.", :r2)

μ = meanresponse(obj)
mss(obj) / sum(x -> abs2(x - μ), response(obj))
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't these take into accounts weights since weights would have been handled in mss and deviance?

Copy link
Member

Choose a reason for hiding this comment

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

Good catch. That also applies to the Efron R². For the method above, since it's deprecated, we'd better keep using deviance for now. For the Efron R², I guess we need to multiply observations by their weights, but will it work for all kinds of models?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Precisely: packages can handle weights through mss(obj) and loglikelihood(obj). There's no need to provide for weights here.

Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure for ProbabilityWeights, but should work fine for the other weights supported if not mistaken. The tests should cover the weight cases for sure.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Efron's R² does need special treatment, since it uses the sum of residuals.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you know the formula for weighted Efron R²? I guess we just need to multiply both the numerator and the denominator?

I think so. How are we going to implement it? We can retrieve weights via weights, but how do we check if weights were defined in the first place?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think the default is just a one array. Might be nice to follow FillArrays.jl for it to implement the UnitWeight or something.

Copy link
Member

Choose a reason for hiding this comment

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

Let's call weights and assume it returns a valid AbstractVector. Then it would make sense to define UnitWeights in this package, to complement other kinds of weights (#135).

Copy link
Member

Choose a reason for hiding this comment

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

Bump. Can you drop the Efron R² for now so that we can merge the PR? We can discuss improvements later.

Copy link
Member

Choose a reason for hiding this comment

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

Since #515 we have UnitWeights, so we can require models to return this when they are unweighted.

@nalimilan nalimilan merged commit 1ccb352 into JuliaStats:master Oct 3, 2018
@nalimilan
Copy link
Member

Thanks! Can you file a PR against GLM to update R² tests?

@lbittarello
Copy link
Contributor Author

As far as I can tell, the only tests involve linear models. This PR shouldn't affect them.

@nalimilan
Copy link
Member

OK. Then it would be nice to add tests to ensure results remain correct in the future. ;-)

@nalimilan nalimilan mentioned this pull request Jan 13, 2019
nalimilan added a commit that referenced this pull request Oct 19, 2019
Since #400 we use the log-likelihood.
nalimilan added a commit that referenced this pull request Dec 4, 2019
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.

Methods for StatsModel
3 participants