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

Rework missing values validation in etna metrics #514

Merged
merged 6 commits into from
Dec 4, 2024
Merged

Conversation

d-a-bunin
Copy link
Collaborator

@d-a-bunin d-a-bunin commented Nov 28, 2024

Before submitting (must do checklist)

  • Did you read the contribution guide?
  • Did you update the docs? We use Numpy format for all the methods and classes.
  • Did you write any new necessary tests?
  • Did you update the CHANGELOG?

Proposed Changes

See #513.

Closing issues

Closes #513.

@d-a-bunin d-a-bunin self-assigned this Nov 28, 2024
Copy link

github-actions bot commented Nov 28, 2024

🚀 Deployed on https://deploy-preview-514--etna-docs.netlify.app

@github-actions github-actions bot temporarily deployed to pull request November 28, 2024 12:54 Inactive
Copy link

codecov bot commented Nov 28, 2024

Codecov Report

Attention: Patch coverage is 69.56522% with 21 lines in your changes missing coverage. Please review.

Project coverage is 90.42%. Comparing base (a1647bb) to head (2fab53a).
Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
etna/metrics/base.py 66.12% 21 Missing ⚠️
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.
📢 Have feedback on the report? Share it here.

:
aggregated value of metric
"""
return np.nanmean(list(metrics_per_segments.values())).item() # type: ignore
Copy link
Collaborator Author

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.

Copy link
Collaborator

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

Suggested change
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

Copy link
Collaborator

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]]
Copy link
Collaborator Author

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 into None inside the functional metrics

Copy link
Collaborator

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.

Copy link
Collaborator Author

@d-a-bunin d-a-bunin Dec 2, 2024

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 both None and NaN
  • If we work in matrix_to_array mode the result from functional metric is np.array. By default it has dtype float, to return None we should rework dtype or return list.

Copy link
Collaborator

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 that None is returned if everything is empty.
  • This can also be processed, if you convert it to the object type, you can put None there, but most likely we will lose a little in performance in such a case.

Copy link
Collaborator Author

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 returning list or ndarray[object] isn't that people regularly expect from functions like mean_squared_error, etc.

Copy link
Collaborator Author

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.

Copy link
Collaborator

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 to None at the last step in the functional metric, wouldn't it ?
  • This is true. Should we consider not replacing nan with None?

@d-a-bunin d-a-bunin requested a review from brsnw250 December 2, 2024 07:38
@@ -27,6 +27,7 @@ Enums:
:template: class.rst

MetricAggregationMode
MetricMissingMode
Copy link
Collaborator

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.

Copy link
Collaborator Author

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.

Copy link
Collaborator

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

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 ?

etna/metrics/__init__.py Show resolved Hide resolved
@@ -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]]
Copy link
Collaborator

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.

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()
Copy link
Collaborator

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

Suggested change
df_true_isna = df_true.isna().any().any()
df_true_isna = np.sum(df_true.isna().values)

:
aggregated value of metric
"""
return np.nanmean(list(metrics_per_segments.values())).item() # type: ignore
Copy link
Collaborator

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

Suggested change
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

:
aggregated value of metric
"""
return np.nanmean(list(metrics_per_segments.values())).item() # type: ignore
Copy link
Collaborator

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

etna/metrics/intervals_metrics.py Show resolved Hide resolved
etna/pipeline/base.py Show resolved Hide resolved
@github-actions github-actions bot temporarily deployed to pull request December 2, 2024 12:29 Inactive
@github-actions github-actions bot temporarily deployed to pull request December 2, 2024 12:37 Inactive
@github-actions github-actions bot temporarily deployed to pull request December 2, 2024 14:53 Inactive
@d-a-bunin d-a-bunin requested a review from brsnw250 December 3, 2024 08:40
@github-actions github-actions bot temporarily deployed to pull request December 3, 2024 12:21 Inactive
@d-a-bunin d-a-bunin merged commit 0b1b29d into master Dec 4, 2024
17 checks passed
@d-a-bunin d-a-bunin deleted the issue-513 branch December 4, 2024 07:40
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.

Rework missing values validation in etna metrics
2 participants