-
Notifications
You must be signed in to change notification settings - Fork 31
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
FunctionTerm is dead, long live FunctionTerm #183
Conversation
The major problem with this proposal, now that I've messed with it a bit, is that these kinds of methods really hit the compiler hard: # + concatenates terms
Base.:+(terms::AbstractTerm...) = (unique(reduce(+, terms))..., )
Base.:+(a::AbstractTerm) = a
Base.:+(a::AbstractTerm, b::AbstractTerm) = (a,b)
# associative rule for +
Base.:+(as::TupleTerm, b::AbstractTerm) = (as..., b)
Base.:+(a::AbstractTerm, bs::TupleTerm) = (a, bs...)
Base.:+(as::TupleTerm, bs::TupleTerm) = (as..., bs...) ( The reason is that a new method has to be compiled for every combination of number and types of terms being added together. This is related to #165 : all the type parameters in the term types create a lot of compiler overhead. I can think of a few ways around this.
Edit: I did try wrapping all these in a |
Now I'm really confused. I've actually gone through the exercise of replacing the tuple-based representation for |
|
Yeah, I'm afraid so. Makes me wonder how stuff like this is handled in Base... edit, it's easy enough to find out :) So I think defining that method is actually not necessary, except that we also want to call |
Specialisation around splatting is weird in general, and its not documented what it will do. But in anycase if you don't want specialiation just used the
or for an argument:
if |
I'm more and more concerned that just putting a few At this point, my plan is to do a bit of benchmarking to see whether formula creation gets appreciably WORSE with this PR, and if not, we can forget about trying to fix the performance in another PR and focus on whether these changes do justice to @oxinabox vision enough to merge. |
Okay I've done a bit of timing (using the script from y ~ 1 + x
0.000033 seconds (24 allocations: 1.453 KiB)
0.000032 seconds (24 allocations: 1.453 KiB)
y ~ a + b
0.008989 seconds (3.57 k allocations: 222.652 KiB)
0.000014 seconds (11 allocations: 720 bytes)
y ~ a + b + c
0.009176 seconds (3.58 k allocations: 223.059 KiB)
0.000017 seconds (12 allocations: 752 bytes)
y ~ a + b + c + d
0.011082 seconds (3.58 k allocations: 222.840 KiB)
0.000017 seconds (13 allocations: 784 bytes)
y ~ a + b + c + d + e
0.009841 seconds (3.58 k allocations: 223.012 KiB)
0.000023 seconds (15 allocations: 896 bytes)
y ~ a + b + c + d + e + f
0.010660 seconds (3.58 k allocations: 223.105 KiB)
0.000029 seconds (16 allocations: 928 bytes)
y ~ log(a)
0.065474 seconds (96.09 k allocations: 5.480 MiB)
0.029364 seconds (9.05 k allocations: 520.255 KiB)
y ~ log(a) + log(b)
0.941035 seconds (1.19 M allocations: 59.954 MiB, 10.87% gc time)
0.746309 seconds (1.02 M allocations: 51.098 MiB, 12.56% gc time)
y ~ log(a) + log(b) + log(c)
0.793438 seconds (1.43 M allocations: 72.068 MiB, 2.55% gc time)
0.647390 seconds (901.92 k allocations: 45.123 MiB, 1.24% gc time)
y ~ log(a) + log(b) + log(c) + log(d)
0.777564 seconds (1.01 M allocations: 50.078 MiB, 1.10% gc time)
0.809190 seconds (1.01 M allocations: 50.081 MiB, 1.07% gc time)
y ~ log(a) + log(b) + log(c) + log(d) + log(e)
0.914824 seconds (1.11 M allocations: 55.022 MiB, 1.15% gc time)
0.918323 seconds (1.11 M allocations: 55.028 MiB, 1.02% gc time)
y ~ log(a) + log(b) + log(c) + log(d) + log(e) + log(f)
1.036401 seconds (1.21 M allocations: 59.980 MiB, 1.71% gc time)
1.017207 seconds (1.21 M allocations: 59.978 MiB, 0.86% gc time)
y ~ exp(a)
0.027674 seconds (9.39 k allocations: 547.880 KiB)
0.030770 seconds (9.05 k allocations: 520.177 KiB)
y ~ exp(a) + exp(b)
0.778712 seconds (1.20 M allocations: 59.997 MiB, 2.29% gc time)
0.684971 seconds (1.02 M allocations: 51.120 MiB, 1.34% gc time)
y ~ exp(a) + exp(b) + exp(c)
0.742065 seconds (1.24 M allocations: 62.374 MiB, 1.24% gc time)
0.655097 seconds (901.92 k allocations: 45.129 MiB, 1.56% gc time)
y ~ exp(a) + exp(b) + exp(c) + exp(d)
0.773946 seconds (1.01 M allocations: 50.081 MiB, 1.32% gc time)
0.778134 seconds (1.01 M allocations: 50.084 MiB, 2.41% gc time)
y ~ exp(a) + exp(b) + exp(c) + exp(d) + exp(e)
0.891833 seconds (1.11 M allocations: 55.043 MiB, 1.04% gc time)
0.873386 seconds (1.11 M allocations: 55.022 MiB, 1.05% gc time)
y ~ exp(a) + exp(b) + exp(c) + exp(d) + exp(e) + exp(f)
0.982716 seconds (1.21 M allocations: 59.970 MiB, 1.11% gc time)
1.017105 seconds (1.21 M allocations: 59.977 MiB, 1.86% gc time) And on this PR (commit eaec084): y ~ 1 + x
0.000100 seconds (25 allocations: 1.406 KiB)
0.000093 seconds (25 allocations: 1.406 KiB)
y ~ a + b
0.007148 seconds (2.17 k allocations: 135.030 KiB)
0.000016 seconds (11 allocations: 768 bytes)
y ~ a + b + c
0.042739 seconds (53.78 k allocations: 3.048 MiB)
0.000026 seconds (23 allocations: 1.484 KiB)
y ~ a + b + c + d
0.036280 seconds (53.91 k allocations: 3.052 MiB)
0.000020 seconds (25 allocations: 1.547 KiB)
y ~ a + b + c + d + e
0.047632 seconds (54.05 k allocations: 3.059 MiB)
0.000022 seconds (28 allocations: 1.688 KiB)
y ~ a + b + c + d + e + f
0.039089 seconds (54.18 k allocations: 3.064 MiB)
0.000020 seconds (30 allocations: 1.750 KiB)
y ~ log(a)
0.027760 seconds (44.74 k allocations: 2.699 MiB)
0.000014 seconds (11 allocations: 480 bytes)
y ~ log(a) + log(b)
0.277548 seconds (332.92 k allocations: 17.035 MiB)
0.000028 seconds (27 allocations: 1.703 KiB)
y ~ log(a) + log(b) + log(c)
0.080824 seconds (121.33 k allocations: 6.716 MiB)
0.000031 seconds (42 allocations: 2.906 KiB)
y ~ log(a) + log(b) + log(c) + log(d)
0.113681 seconds (114.74 k allocations: 6.305 MiB, 10.12% gc time)
0.000034 seconds (49 allocations: 3.297 KiB)
y ~ log(a) + log(b) + log(c) + log(d) + log(e)
0.075690 seconds (118.20 k allocations: 6.460 MiB)
0.000034 seconds (57 allocations: 3.828 KiB)
y ~ log(a) + log(b) + log(c) + log(d) + log(e) + log(f)
0.075736 seconds (121.95 k allocations: 6.627 MiB)
0.000037 seconds (64 allocations: 4.219 KiB)
y ~ exp(a)
0.013628 seconds (6.69 k allocations: 411.996 KiB)
0.000013 seconds (11 allocations: 480 bytes)
y ~ exp(a) + exp(b)
0.251576 seconds (332.96 k allocations: 17.036 MiB)
0.000035 seconds (27 allocations: 1.703 KiB)
y ~ exp(a) + exp(b) + exp(c)
0.083678 seconds (121.34 k allocations: 6.716 MiB)
0.000037 seconds (42 allocations: 2.906 KiB)
y ~ exp(a) + exp(b) + exp(c) + exp(d)
0.103966 seconds (114.75 k allocations: 6.304 MiB, 9.53% gc time)
0.000035 seconds (49 allocations: 3.297 KiB)
y ~ exp(a) + exp(b) + exp(c) + exp(d) + exp(e)
0.087459 seconds (118.21 k allocations: 6.461 MiB)
0.000039 seconds (57 allocations: 3.828 KiB)
y ~ exp(a) + exp(b) + exp(c) + exp(d) + exp(e) + exp(f)
0.080457 seconds (121.96 k allocations: 6.629 MiB)
0.000038 seconds (64 allocations: 4.219 KiB)
Bottom line is, this PR is MUCH faster for anything involving a custom function, There was, I remember, a good reason why it made sense to move the parsing rules |
Project.toml
Outdated
@@ -15,6 +16,7 @@ Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" | |||
|
|||
[compat] | |||
CategoricalArrays = "0.8" | |||
Compat = "2.2, 3" |
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 line isn't in the diff anymore. Is this intended?
src/terms.jl
Outdated
Base.:&(::ConstantTerm, b::AbstractTerm) = b | ||
Base.:&(a::AbstractTerm, ::ConstantTerm) = a | ||
|
||
# Avoid method ambiguities | ||
Base.:&(::ConstantTerm, b::InteractionTerm) = b | ||
Base.:&(a::InteractionTerm, ::ConstantTerm) = a | ||
Base.:&(a::ConstantTerm, ::ConstantTerm) = a |
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. It would sound safer to check that the value is 1 then.
Co-authored-by: Milan Bouchet-Valat <nalimilan@club.fr>
Project.toml
Outdated
@@ -1,8 +1,9 @@ | |||
name = "StatsModels" | |||
uuid = "3eaba693-59b7-5ba5-a881-562e759f1c8d" | |||
version = "0.6.33" | |||
version = "0.7.0-DEV" |
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.
Usually we just set the version that will be released (I'm not even sure Pkg accepts letters, which may explain the docs build failure).
version = "0.7.0-DEV" | |
version = "0.7.0" |
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.
Actually we should even do this:
version = "0.7.0-DEV" | |
version = "1.0.0" |
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.
ehhh I'm on the fence about that...there are a few other potentially breaking changes that I think we should at least CONSIDER before releasing 1.0 (that don't have PRs/WIP already or are pretty complex, like like using Tables.Columns, removing all teh type parameters from the AbstractTerms, handling the missing masks, etc.), and I'd like to get this released while I still have the spoons to work on it, rather than waiting on all the other breaking changes we might want to consider before 1.0.
src/terms.jl
Outdated
Base.:&(::ConstantTerm, b::AbstractTerm) = b | ||
Base.:&(a::AbstractTerm, ::ConstantTerm) = a | ||
|
||
# Avoid method ambiguities | ||
Base.:&(::ConstantTerm, b::InteractionTerm) = b | ||
Base.:&(a::InteractionTerm, ::ConstantTerm) = a | ||
Base.:&(a::ConstantTerm, ::ConstantTerm) = a |
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.
Referring to #183 (comment):
Yes better throw errors in absurd cases rather than risk hiding bugs. So with the new state of the PR an error is thrown in both cases above, right?
Co-authored-by: Milan Bouchet-Valat <nalimilan@club.fr>
* functionterm news * run-time * Update NEWS.md Co-authored-by: Phillip Alday <palday@users.noreply.github.com> * Update NEWS.md Co-authored-by: Phillip Alday <palday@users.noreply.github.com> --------- Co-authored-by: Phillip Alday <palday@users.noreply.github.com>
This is a pretty substantial change in how non-special function calls are
represented. Instead of generating an anonymous function, the new
FunctionTerm
simple wraps the called function, it's arguments (wrapped asTerm
s), and the original expression. When evaluated withmodelcols
, it usesBase.Broadcast.broadcasted
to lazily fuse nested function calls and then atthe top level calls
materialize
to run. I hope that this will provideperformance that's comparable to the anonymous function, while being both more
run-time friendly and much simpler.
As a side effect, all of the macro-time parsing has been removed, and all the
special syntax now applies at run time (via method overloading of
+
,&
,~
,and
*
(I know, I'm sorry, this is just for continuity's sake right now).Also,
shamelessly stealing frominspired by @oxinabox suggestions for how tohandle nested special and non-special functions (#117), I've added some
additional special syntax:
protect
andunprotect
.protect
says to treatevery call below it as non-special, while
unprotect
(in an otherwise protectedcontext) says to treat everything below it as potentially special (e.g., as if
it occurred at the top level of a formula). This isn't perfect at the moment
but it's a long way towards being able to do something like
1 - unprotect(poly(x, 3))
and having it do something sensible (actually that might work; what doesn't work
is
(1 - unprotect(a*b))
since that will generateda + b + a&b
which don'tget fused into a single matrix which messes with the broadcasting).
I also added on a whim an
@support_unprotect op
macro which will designate a function asspecial in unprotected contexts. This simply adds a method like
So, if you have something like
FunctionTerm{typeof(+)}
in a non-protectedcontext, then it will be converted into a call to
+(args[1], args[2], ...)
when you do
apply_schema
. This is taking advantage of the fact that all thespecial handling of special syntax is done via method overloading of the
corresponding functions, so it should work fine at runtime. At the moment it's
basically only useful for internal purposes but a two-argument form (function
and context type) might be useful for packages.
Tests do pass with this change, but it probably needs more tests to specifically
cover the protect/unprotect stuff.
Docs aren't updated because that seemspremature at this point before folks have had a chance to weigh in.