-
Notifications
You must be signed in to change notification settings - Fork 9
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
Rework missing values validation in etna metrics #514
Conversation
🚀 Deployed on https://deploy-preview-514--etna-docs.netlify.app |
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #514 +/- ##
==========================================
- Coverage 90.52% 90.42% -0.10%
==========================================
Files 247 247
Lines 16511 16559 +48
==========================================
+ Hits 14946 14973 +27
- Misses 1565 1586 +21 ☔ View full report in Codecov by Sentry. |
etna/metrics/base.py
Outdated
: | ||
aggregated value of metric | ||
""" | ||
return np.nanmean(list(metrics_per_segments.values())).item() # type: ignore |
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.
As I understand, it won't work correctly, the error will be:
TypeError: unsupported operand type(s) for +: 'float' and 'NoneType'
The None
value isn't equal to NaN
and it causes some problems.
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 suggest the following to resolve this issue
return np.nanmean(list(metrics_per_segments.values())).item() # type: ignore | |
return np.nanmean(np.fromiter(metrics_per_segments.values(), dtype=float)).item() # type: ignore |
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 may throw an error if all segments metrics are None
/ nan
. We should handle this case properly
@@ -322,7 +345,7 @@ def __call__(self, y_true: TSDataset, y_pred: TSDataset) -> Union[float, Dict[st | |||
|
|||
segments = df_true.columns.get_level_values("segment").unique() | |||
|
|||
metrics_per_segment: Dict[str, float] | |||
metrics_per_segment: Dict[str, Optional[float]] |
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.
We should also probably do smth with NaNs returned from metric functions:
- We should cast them into
None
inside__call__
- We should cast
NaNs
intoNone
inside the functional metrics
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 should be handeled inside functional metric.
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 yes, but
- It makes implementation of functional metric more difficult
- It doesn't save us from getting
NaNs
from functional metrics, in that case we have bothNone
and NaN - If we work in
matrix_to_array
mode the result from functional metric isnp.array
. By default it has dtypefloat
, to returnNone
we should rework dtype or return list.
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.
- There will be more logic there, for example, if you use
nanmean
, an error may occur, it should be handled as well. - And when can
nan
occur there? I proceeded from the fact thatNone
is returned if everything is empty. - This can also be processed, if you convert it to the
object
type, you can putNone
there, but most likely we will lose a little in performance in such a case.
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 saw only
RuntimeWarning: Mean of empty slice
. What error are you talking about? - There are two scenarios: we allow
NaNs
to occur, we have some error that makes them to occur... - We could probably just return a
list
. I'm worried that returninglist
orndarray[object]
isn't that people regularly expect from functions likemean_squared_error
, etc.
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 we are going to move this logic into functional metric we should probably update ArrayLike
or make a separate type hint for return value.
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, you're right. Sorry for misleading I referred to this
- If
nan
is a result of some kind of error, it still would be converted toNone
at the last step in the functional metric, wouldn't it ? - This is true. Should we consider not replacing
nan
withNone
?
@@ -27,6 +27,7 @@ Enums: | |||
:template: class.rst | |||
|
|||
MetricAggregationMode | |||
MetricMissingMode |
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.
We should add MetricWithMissingHandling
to the documentation 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.
Do we really want to add this? It seems like a developer hack than a public API.
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.
Why is it a hack? Users themselves can inherit from this class to make their custom metrics handle missing values.
CHANGELOG.md
Outdated
@@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 | |||
### Added | |||
- Add `load_dataset` to public API ([#484](https://github.com/etna-team/etna/pull/484)) | |||
- Add example of using custom pipeline pools in `Auto` ([#504](https://github.com/etna-team/etna/pull/504)) | |||
- | |||
- Change signature of `etna.metrics.Metric` to return `None` values ([#514](https://github.com/etna-team/etna/pull/514)) |
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 we add new class MetricWithMissingHandling
here ?
@@ -322,7 +345,7 @@ def __call__(self, y_true: TSDataset, y_pred: TSDataset) -> Union[float, Dict[st | |||
|
|||
segments = df_true.columns.get_level_values("segment").unique() | |||
|
|||
metrics_per_segment: Dict[str, float] | |||
metrics_per_segment: Dict[str, Optional[float]] |
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 should be handeled inside functional metric.
etna/metrics/base.py
Outdated
df_true = y_true.df.loc[:, pd.IndexSlice[:, "target"]] | ||
df_pred = y_pred.df.loc[:, pd.IndexSlice[:, "target"]] | ||
|
||
df_true_isna = df_true.isna().any().any() |
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.
Instead of calling any
two times we can do
df_true_isna = df_true.isna().any().any() | |
df_true_isna = np.sum(df_true.isna().values) |
etna/metrics/base.py
Outdated
: | ||
aggregated value of metric | ||
""" | ||
return np.nanmean(list(metrics_per_segments.values())).item() # type: ignore |
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 suggest the following to resolve this issue
return np.nanmean(list(metrics_per_segments.values())).item() # type: ignore | |
return np.nanmean(np.fromiter(metrics_per_segments.values(), dtype=float)).item() # type: ignore |
etna/metrics/base.py
Outdated
: | ||
aggregated value of metric | ||
""" | ||
return np.nanmean(list(metrics_per_segments.values())).item() # type: ignore |
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 may throw an error if all segments metrics are None
/ nan
. We should handle this case properly
…hMissingHandling into public API
Before submitting (must do checklist)
Proposed Changes
See #513.
Closing issues
Closes #513.