-
-
Notifications
You must be signed in to change notification settings - Fork 127
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
interpret
verbosity using logging
#745
Conversation
bambi/interpret/logs.py
Outdated
|
||
|
||
def log_interpret_defaults(func): | ||
interpret_logger = logger.get_logger() |
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.
If I recall correctly, you need to pass getLogger
a name to get the same logger. As it's done here
Line 30 in d001e06
_log = logging.getLogger("bambi") |
So you could do logger.get_logger("bambi_interpret")
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.
That's correct. When I did this, it wasn't returning the info logs to the console. Once I removed the name, it returned the info logs. I will look into it more.
bambi/interpret/logs.py
Outdated
|
||
def log_interpret_defaults(func): | ||
interpret_logger = logger.get_logger() | ||
interpret_logger.setLevel(logging.INFO) |
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 also think you need this
Lines 32 to 36 in d001e06
if not logging.root.handlers: | |
_log.setLevel(logging.INFO) | |
if len(_log.handlers) == 0: | |
handler = logging.StreamHandler() | |
_log.addHandler(handler) |
I don't remember the details of when it was added. But I think it makes sure the messages are sent once to the place where you want to send them.
bambi/interpret/logs.py
Outdated
if not logger.messages: | ||
return func(*args, **kwargs) | ||
|
||
if func.__name__ == "set_default_values": |
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'm not sure if we should handle specific cases here. I think it would be better to allow the decorator to receive arguments and then determine what to do from those.
Also, in this case, the function receives a dictionary and returns a dictionary. So you compare keys. Are there any other cases we may be interested?
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'm not sure if we should handle specific cases here. I think it would be better to allow the decorator to receive arguments and then determine what to do from those.
I agree. That would generalize better.
Also, in this case, the function receives a dictionary and returns a dictionary. So you compare keys. Are there any other cases we may be interested?
There are two functions that compute default values. Both in utils.py
.
- set_default_values that computes default values for covariates specified in the model, but not passed to
conditional
. - set_default_variable_values that computes default
contrast
orwrt
values forcomparisons
andslopes
, respectively if the user does not pass a value (but they obviously need to pass the variable name of interest).
These are the functions that I initially wanted to decorate. However, in other scenarios, e.g. a user passes a list of covariate names to conditional
, but no values, then the following functions are called to compute default values:
- make_main_values that computes main values based on original data using a grid of evenly spaced values for numeric predictors and unique levels for categoric predictors.
- make_group_values that computes group values based on original data using unique levels for categoric predictors and quantiles for numeric predictors.
Knowing that a np.linspace
or np.quantile
would also be informative for the user. I will decorate these functions also since they compute default values as well.
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.
Sounds great
I didn't have the decorator idea in my mind when we discussed logging. But I do think it's a very neat idea. I'm not 100% sure if it generalizes well. Maybe, it's enough for it to generalize to the cases we care about. Do we know if we're going to decorate functions with different a wider variety of input/output types? |
That is also what I am questioning myself on after replying to your comment above with the list of functions that compute default values. Most function parameters accept an array and return an array. Generally speaking, you will see the following:
Unless I added parameters to the decorator or had different decorators for different functions, then I don't see how it will generalize. I am open to other ideas. I don't have too much time sunk into this PR. hah |
23956b6
to
ce93b35
Compare
Check out this pull request on See visual diffs & provide feedback on Jupyter Notebooks. Powered by ReviewNB |
Based on the discussion above, the following functions that compute default values
now have a decorator I decided to keep the handling of specific cases, i.e., bmb.interpret.comparisons(
model,
idata,
contrast="hp",
conditional="drat"
)
I am open to changing the specific cases if anyone has a better design idea. I have also added tests |
Codecov Report
@@ Coverage Diff @@
## main #745 +/- ##
==========================================
+ Coverage 89.78% 89.84% +0.05%
==========================================
Files 43 45 +2
Lines 3604 3664 +60
==========================================
+ Hits 3236 3292 +56
- Misses 368 372 +4
... and 1 file with indirect coverage changes 📣 Codecov offers a browser extension for seamless coverage viewing on GitHub. Try it in Chrome or Firefox today! |
You successfully implemented a solution and I think that's great. As far as I understand, the decorator doesn't have arguments that determine how to behave with that function. Instead, the decorator "knows" about some functions, and the functionality for those is hardcoded. I think it's fine and I wouldn't invest a lot of effort in modifying it. The only thing I want to raise is that I'm not sure why we need an
Regarding these last two points, I'm more than happy to add an implementation to this existing PR as you have already done a lot of effort @GStechschulte, just let me know what you think |
Thank you @tomicapretto, and your interpretation is correct. The two points you bring up are great, and I think they would both enhance this PR. Since you have already implemented this in formulae, and if you don't think it will take you long, that would be greatly appreciated 😄 |
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.
Feels weird to be approving my own changes haha. @GStechschulte I think this is good to go. Let me know if you have questions or suggestions about the changes.
LGTM 👍🏼 Thanks a lot! I was testing it with some of our docs notebooks and it works great. However, we log messages to the console by default when using __FIELDS = {"INTERPRET_VERBOSE": (False, True)} I am updating docs related to |
@GStechschulte i would set it to True by default. Otherwise, I think people not familiar with how it works will not realize this is happening. This way, the message will be a signal telling something is happening. |
4dc02c2
to
eaf14d7
Compare
@tomicapretto I updated the three notebooks related to |
Resolves PR #740. I am opening as a draft PR to collect feedback on my initial idea and design.
In order to make
interpret
verbose about some of the "hidden" computations such as default values, I am creating a config instance (similar to formulae) that allows the user to optionally turn on or off the logging of various information. To log information to the console, I want to use the built inlogging
module to create a logger object specific to theinterpret
module.Then, I create a
log_interpret_functions
decorator so I can simply decorate the desired functions inutils.py
that compute default values. This allows me to extend the functionality of the existing util functions without changing the underlying code, and thereby separating the logging code into a different filelogs.py
.Below is a demo:
Calling
predictions
:Calling
plot_predictions
Calling
predictions
where no default values are computed means no messages.I need to make sure the logger object I am creating and referencing is not messing with the
"bambi"
logger inbambi/__init__.py
(which I think it is because of theINFO:root:
tag in the console logs above). Using logging in multiple modules should be possible according to the logging cookbook. Thus, Bambi would have two loggers: one for the main modulebambi
, and then one forinterpret
.Before I move forward with this implementation, I want to get feedback from the community and or anyone with more experience in logging. Thanks!