Skip to content

Commit

Permalink
feat & fix: add some features (#38)
Browse files Browse the repository at this point in the history
## Features
* calc_mbe: mean bias error
* calc_threshold_hit_ratio

## Fix
* drop_nan: The problem that if input as list, the result is unexpect.

## Tests
* add test for:
	* calc_mae
    * calc_mbe
    * calc_threshold_hit_ratio
    * calc_threshold_false_alarm_ratio
    * calc_threshold_ts
    * calc_linregress
  • Loading branch information
Clarmy authored May 24, 2023
1 parent c478610 commit b72ce98
Show file tree
Hide file tree
Showing 7 changed files with 282 additions and 2 deletions.
26 changes: 26 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# 贡献指南

感谢您考虑为本项目做出贡献!为了帮助您入手并快速贡献,本文提供了一些基本的指导。

## Issue

如果您发现了一个 bug 或者有一个新的功能请求,可以在我们的 [GitHub Issues](https://github.com/example-project/issues) 页面上创建一个 issue,我们会及时查看并回复您的问题。

## Pull Request

我们非常欢迎您为本项目提交 Pull Request。在提交 PR 之前,请确保您已经遵循了以下步骤:

1. 从主分支(`main`)拉取最新的代码并创建一个新的分支(比如 `feature/new-feature`)。
2. 在新分支上开发您的代码,确保您的代码风格符合项目的编码规范。
3. 通过测试用例,确保代码能够正常运行并且不会影响其他部分。
4. 通过测试后,提交您的 Pull Request。在 PR 描述中,请详细描述您的代码变更,包括对相关 issue 的引用。

## Coding Style

本项目的代码风格遵循 [PEP 8](https://www.python.org/dev/peps/pep-0008/) 规范。在提交代码前,请确保您的代码风格符合这个规范。

## License

本项目采用 [MIT License](./LICENSE) 协议。在为本项目做出贡献之前,请确保您已经完全了解了该协议内容并同意遵守该协议的所有规定。

如果您有任何问题,请随时在 GitHub Issues 页面上发起讨论。我们非常感谢您的贡献!
2 changes: 1 addition & 1 deletion cyeva/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from .core import *

__version__ = "0.1.0.beta.7"
__version__ = "0.1.0.beta.8"
18 changes: 18 additions & 0 deletions cyeva/core/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
calc_diff_accuracy_ratio,
calc_rmse,
calc_mae,
calc_mbe,
calc_rss,
calc_chi_square,
calc_linregress,
Expand Down Expand Up @@ -71,6 +72,23 @@ def calc_mae(

return calc_mae(observation, forecast)

@result_round_digit(4)
@source_round_digit()
def calc_mbe(
self,
observation: Union[np.ndarray, list] = None,
forecast: Union[np.ndarray, list] = None,
*args,
**kwargs
) -> float:
"""Mean Bias Error"""
if observation is None:
observation = self.observation
if forecast is None:
forecast = self.forecast

return calc_mbe(observation, forecast)

@result_round_digit(4)
@source_round_digit()
def calc_chi_square(
Expand Down
56 changes: 56 additions & 0 deletions cyeva/core/statistic.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,27 @@ def calc_mae(
return np.sum(np.abs(observation - forecast)) / len(observation)


@assert_length
@fix_zero_division
@convert_to_ndarray
@drop_nan
def calc_mbe(
observation: Union[list, np.ndarray], forecast: Union[list, np.ndarray]
) -> float:
"""Calculate MAE(mean absolute error).
Args:
observation (Union[list, np.ndarray]): Binarized observation data array
that consist of numbers.
forecast (Union[list, np.ndarray]): Binarized forecast data array that
consist of numbers.
Returns:
float: The MAE of forecast to observation.
"""
return np.sum(forecast - observation) / len(observation)


@assert_length
@fix_zero_division
@convert_to_ndarray
Expand Down Expand Up @@ -423,6 +444,41 @@ def calc_threshold_accuracy_ratio(
return calc_binary_accuracy_ratio(obs_bins, fct_bins)


@assert_length
@drop_nan
def calc_threshold_hit_ratio(
observation: Union[list, np.ndarray],
forecast: Union[list, np.ndarray],
threshold: Number,
compare: str = ">=",
) -> float:
"""Calculate hit ratio of forecast filtered by threshold.
Args:
observation (Union[list, np.ndarray]): Binarized observation data array
that consist of numbers.
forecast (Union[list, np.ndarray]): Binarized forecast data array that
consist of numbers.
threshold (Number): The threshold to filter obervation and forecast.
This parameter should be used with `compare` parameter,
The `compare` parameter will control the logical operator.
compare (str, optional): The logical operator applying `threshold` parameter.
* '>' greater
* '<' less
* '>=' greater or equal
* '<=' less or equal
Defaults to '>='.
Returns:
float: The missing ratio of forecast filtered by threshold.
"""
obs_bins, fct_bins = threshold_binarize(
observation, forecast, threshold=threshold, compare=compare
)

return calc_hit_ratio(obs_bins, fct_bins)


@assert_length
@drop_nan
def calc_threshold_miss_ratio(
Expand Down
2 changes: 1 addition & 1 deletion cyeva/core/wind.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ def filter_wind_scales(


class WindComparison(Comparison):
# FIXME: 需要加最小扇形逻辑
# FIXME: 需要加最小扇形逻辑,Issue:#33
def __init__(
self,
obs_spd: Union[np.ndarray, list] = None,
Expand Down
5 changes: 5 additions & 0 deletions cyeva/utils/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ def drop_nan(func):

@wraps(func)
def wrapper(observation, forecast, *args, **kwargs):
if not isinstance(observation, np.ndarray):
observation = np.array(observation)
if not isinstance(forecast, np.ndarray):
forecast = np.array(forecast)

where_obs_nan = observation != observation
where_fct_nan = forecast != forecast

Expand Down
175 changes: 175 additions & 0 deletions tests/test_statistic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import numpy as np

from cyeva.core.statistic import (
calc_mae,
calc_mbe,
calc_threshold_hit_ratio,
calc_threshold_false_alarm_ratio,
calc_threshold_ts,
calc_linregress,
)


def test_calc_mae():
MAE_CASE = [
{"obs": [1, 2, 3, 4, 5], "fct": [1, 2, 3, 4, 5], "result": 0.0},
{"obs": [1, 2, 3, 4, 5], "fct": [3, 3, 3, 3, 3], "result": 1.2},
]
for case in MAE_CASE:
obs = case["obs"]
fct = case["fct"]
result = case["result"]
_result = calc_mae(obs, fct)
if not np.isnan(result):
assert _result == result
else:
assert np.isnan(_result)


def test_calc_mbe():
MBE_CASE = [
{"obs": [1, 2, 3, 4, 5], "fct": [1, 2, 3, 4, 5], "result": 0.0},
{"obs": [1, 2, 3, 4, 5], "fct": [3, 3, 3, 3, 3], "result": 0.0},
{"obs": [1, 2, 3, 4, 5], "fct": [1, 1, 1, 1, 1], "result": -2.0},
{"obs": [1, 2, 3, 4, 5], "fct": [5, 5, 5, 5, 5], "result": 2.0},
]
for case in MBE_CASE:
obs = case["obs"]
fct = case["fct"]
result = case["result"]
_result = calc_mbe(obs, fct)
if not np.isnan(result):
assert _result == result
else:
assert np.isnan(_result)


def test_calc_threshold_hit_ratio():
THRESHOLD_HIT_RATIO_CASE = [
{
"obs": [1, 2, 2, 2, 2],
"fct": [1, 1, 1, 1, 1],
"threshold": 2,
"result": 0.0,
},
{
"obs": [1, 2, 2, 2, 2],
"fct": [1, 2, 1, 1, 1],
"threshold": 2,
"result": 25,
},
{"obs": [1, 2, 2, 2, 2], "fct": [1, 2, 2, 1, 1], "threshold": 2, "result": 50},
{
"obs": [1, 2, 2, 2, 2],
"fct": [1, 2, 2, 2, 1],
"threshold": 2,
"result": 75,
},
{"obs": [1, 2, 2, 2, 2], "fct": [1, 2, 2, 2, 2], "threshold": 2, "result": 100},
]
for case in THRESHOLD_HIT_RATIO_CASE:
obs = case["obs"]
fct = case["fct"]
threshold = case["threshold"]
result = case["result"]
_result = calc_threshold_hit_ratio(obs, fct, threshold)
if not np.isnan(result):
assert _result == result
else:
assert np.isnan(_result)


def test_calc_threshold_false_alarm_ratio():
THRESHOLD_FAR_CASE = [
{
"obs": [1, 2, 2, 2, 2],
"fct": [1, 1, 1, 1, 1],
"threshold": 2,
"result": np.nan,
},
{
"obs": [1, 2, 2, 2, 2],
"fct": [1, 2, 1, 1, 1],
"threshold": 2,
"result": 0,
},
{"obs": [1, 2, 1, 2, 2], "fct": [1, 2, 2, 1, 1], "threshold": 2, "result": 50},
{
"obs": [1, 2, 1, 1, 1],
"fct": [1, 2, 2, 2, 2],
"threshold": 2,
"result": 75,
},
{"obs": [1, 1, 1, 1, 1], "fct": [1, 2, 2, 2, 2], "threshold": 2, "result": 100},
]
for case in THRESHOLD_FAR_CASE:
obs = case["obs"]
fct = case["fct"]
threshold = case["threshold"]
result = case["result"]
_result = calc_threshold_false_alarm_ratio(obs, fct, threshold)
if not np.isnan(result):
assert _result == result
else:
assert np.isnan(_result)


def test_calc_threshold_ts():
THRESHOLD_TS_CASE = [
{
"obs": [1, 2, 2, 2, 2],
"fct": [1, 1, 1, 1, 1],
"threshold": 2,
"result": 0,
},
{
"obs": [1, 2, 2, 2, 2],
"fct": [1, 2, 1, 1, 1],
"threshold": 2,
"result": 0.25,
},
{"obs": [1, 2, 2, 2, 2], "fct": [2, 2, 1, 1, 1], "threshold": 2, "result": 0.2},
{
"obs": [1, 2, 1, 1, 1],
"fct": [1, 2, 2, 2, 2],
"threshold": 2,
"result": 0.25,
},
{"obs": [1, 1, 1, 1, 1], "fct": [1, 2, 2, 2, 2], "threshold": 2, "result": 0},
]
for case in THRESHOLD_TS_CASE:
obs = case["obs"]
fct = case["fct"]
threshold = case["threshold"]
result = case["result"]
_result = calc_threshold_ts(obs, fct, threshold)
if not np.isnan(result):
assert _result == result
else:
assert np.isnan(_result)


def calc_correlation_coefficient():
THRESHOLD_TS_CASE = [
{
"obs": [1, 2, 3, 4, 5],
"fct": [1, 2, 3, 4, 5],
"result": 1,
},
{
"obs": [1, 2, 3, 4, 5],
"fct": [5, 4, 3, 2, 1],
"result": -1,
},
{"obs": [1, 1, 1, 1, 1], "fct": [2, 2, 2, 2, 2], "result": 0},
]
for case in THRESHOLD_TS_CASE:
obs = case["obs"]
fct = case["fct"]
threshold = case["threshold"]
result = case["result"]
_, _, _result, _ = calc_linregress(obs, fct)
if not np.isnan(result):
assert _result == result
else:
assert np.isnan(_result)

0 comments on commit b72ce98

Please sign in to comment.