Skip to content

Commit a930fa0

Browse files
committed
small docs notification, break some stuff into different files.
1 parent 85bbb93 commit a930fa0

File tree

5 files changed

+152
-142
lines changed

5 files changed

+152
-142
lines changed

docs/contribute.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@ The steps to contributing are simple:
1313
5. Create a new pull request with your changes.
1414

1515
A note to members of the BYU-Hydroinformatics group. Please make pull requests before merging changes to the master
16-
branch, as this allows continuous integration testing to take place.
16+
branch, as this allows continuous integration testing to take place.

docs/installation.rst

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
Installation
22
============
33

4-
HydroErr is freely available on the Python Package index repository (PyPI). It can be installed
5-
with the following command using either virtualenv or Anaconda::
4+
HydroErr is available on `PyPI <https://pypi.org/project/HydroErr/>`_ and can be installed using any
5+
of the following package managers:
6+
7+
**pip**::
68

79
pip install HydroErr
810

9-
The extension is also available through conda package management system. It can be installed with::
11+
**uv**::
12+
13+
uv add HydroErr
14+
15+
**conda**::
1016

11-
conda install -c conda-forge hydroerr
17+
conda install -c conda-forge hydroerr

src/HydroErr/HydroErr.py

Lines changed: 2 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
11
"""HydroErr metrics and helpers."""
22

33
import warnings
4-
from collections.abc import Sequence
54
from typing import Any
65

76
import numpy as np
8-
from numpy.typing import NDArray
97
from scipy.stats import gmean, rankdata
108

11-
# Typing aliases
12-
InputArray = NDArray[np.floating | np.integer] | Sequence[int | float]
13-
FloatArray = NDArray[np.floating | np.integer]
9+
from .typing_aliases import InputArray
10+
from .util import treat_values
1411

1512
__all__: list[str] = [
1613
"acc",
@@ -6287,11 +6284,6 @@ def mean_var(
62876284
return np.var(np.log1p(observed_array) - np.log1p(simulated_array))
62886285

62896286

6290-
####################################################################################################
6291-
# HELPER FUNCTIONS AND API INFORMATION #
6292-
####################################################################################################
6293-
6294-
62956287
metric_names = [
62966288
"Mean Error",
62976289
"Mean Absolute Error",
@@ -6531,130 +6523,3 @@ def mean_var(
65316523
for i in range(len(function_list)):
65326524
function_list[i].name = metric_names[i] # type: ignore[invalid-assignment]
65336525
function_list[i].abbr = metric_abbr[i] # type: ignore[invalid-assignment]
6534-
6535-
6536-
def treat_values( # noqa: C901
6537-
simulated_array: InputArray,
6538-
observed_array: InputArray,
6539-
replace_nan: float | None = None,
6540-
replace_inf: float | None = None,
6541-
remove_neg: bool = False,
6542-
remove_zero: bool = False,
6543-
) -> tuple[FloatArray, FloatArray]:
6544-
"""Remove the nan, negative, and inf values in two numpy arrays."""
6545-
sim_copy: FloatArray = np.copy(simulated_array)
6546-
obs_copy: FloatArray = np.copy(observed_array)
6547-
6548-
# Checking to see if the vectors are the same length
6549-
if not sim_copy.ndim == 1:
6550-
raise ValueError("The simulated array is not one dimensional.")
6551-
if not obs_copy.ndim == 1:
6552-
raise ValueError("The observed array is not one dimensional.")
6553-
6554-
if sim_copy.size != obs_copy.size:
6555-
raise ValueError("The two ndarrays are not the same size.")
6556-
6557-
# Treat missing data in observed_array and simulated_array, rows in simulated_array or
6558-
# observed_array that contain nan values
6559-
all_treatment_array = np.ones(obs_copy.size, dtype=bool)
6560-
6561-
if np.any(np.isnan(obs_copy)) or np.any(np.isnan(sim_copy)):
6562-
if replace_nan is not None:
6563-
# Finding the NaNs
6564-
sim_nan = np.isnan(sim_copy)
6565-
obs_nan = np.isnan(obs_copy)
6566-
# Replacing the NaNs with the input
6567-
sim_copy[sim_nan] = replace_nan
6568-
obs_copy[obs_nan] = replace_nan
6569-
6570-
warnings.warn(
6571-
f"Elements(s) {np.where(sim_nan)[0]} contained NaN values in the simulated array"
6572-
f" and elements(s) {np.where(obs_nan)[0]} contained NaN values in the observed"
6573-
" array and have been replaced (Elements are zero indexed).",
6574-
UserWarning,
6575-
stacklevel=2,
6576-
)
6577-
else:
6578-
# Getting the indices of the nan values, combining them, and informing user.
6579-
nan_indices_fcst = ~np.isnan(sim_copy)
6580-
nan_indices_obs = ~np.isnan(obs_copy)
6581-
all_nan_indices = np.logical_and(nan_indices_fcst, nan_indices_obs)
6582-
all_treatment_array = np.logical_and(all_treatment_array, all_nan_indices)
6583-
6584-
warnings.warn(
6585-
f"Row(s) {np.where(~all_nan_indices)[0]} contained NaN values and the row(s) have"
6586-
f" been removed (Rows are zero indexed).",
6587-
UserWarning,
6588-
stacklevel=2,
6589-
)
6590-
6591-
if np.any(np.isinf(obs_copy)) or np.any(np.isinf(sim_copy)):
6592-
if replace_nan is not None:
6593-
# Finding the NaNs
6594-
sim_inf = np.isinf(sim_copy)
6595-
obs_inf = np.isinf(obs_copy)
6596-
# Replacing the NaNs with the input
6597-
sim_copy[sim_inf] = replace_inf
6598-
obs_copy[obs_inf] = replace_inf
6599-
6600-
warnings.warn(
6601-
f"Elements(s) {np.where(sim_inf)[0]} contained Inf values in the simulated array"
6602-
f" and elements(s) {np.where(obs_inf)[0]} contained Inf values in the observed"
6603-
" array and have been replaced (Elements are zero indexed).",
6604-
UserWarning,
6605-
stacklevel=2,
6606-
)
6607-
else:
6608-
inf_indices_fcst = ~(np.isinf(sim_copy))
6609-
inf_indices_obs = ~np.isinf(obs_copy)
6610-
all_inf_indices = np.logical_and(inf_indices_fcst, inf_indices_obs)
6611-
all_treatment_array = np.logical_and(all_treatment_array, all_inf_indices)
6612-
6613-
warnings.warn(
6614-
f"Row(s) {np.where(~all_inf_indices)[0]} contained Inf or -Inf values and"
6615-
" the row(s) have been removed (Rows are zero indexed).",
6616-
UserWarning,
6617-
stacklevel=2,
6618-
)
6619-
6620-
# Treat zero data in observed_array and simulated_array, rows in simulated_array or
6621-
# observed_array that contain zero values
6622-
if remove_zero and ((obs_copy == 0).any() or (sim_copy == 0).any()):
6623-
zero_indices_fcst = ~(sim_copy == 0)
6624-
zero_indices_obs = ~(obs_copy == 0)
6625-
all_zero_indices = np.logical_and(zero_indices_fcst, zero_indices_obs)
6626-
all_treatment_array = np.logical_and(all_treatment_array, all_zero_indices)
6627-
6628-
warnings.warn(
6629-
f"Row(s) {np.where(~all_zero_indices)[0]} contained zero values and the row(s)"
6630-
" have been removed (Rows are zero indexed).",
6631-
UserWarning,
6632-
stacklevel=2,
6633-
)
6634-
6635-
# Treat negative data in observed_array and simulated_array, rows in simulated_array or
6636-
# observed_array that contain negative values
6637-
6638-
# Ignore runtime warnings from comparing
6639-
if remove_neg:
6640-
with np.errstate(invalid="ignore"):
6641-
obs_copy_bool = obs_copy < 0
6642-
sim_copy_bool = sim_copy < 0
6643-
6644-
if obs_copy_bool.any() or sim_copy_bool.any():
6645-
neg_indices_fcst = ~sim_copy_bool
6646-
neg_indices_obs = ~obs_copy_bool
6647-
all_neg_indices = np.logical_and(neg_indices_fcst, neg_indices_obs)
6648-
all_treatment_array = np.logical_and(all_treatment_array, all_neg_indices)
6649-
6650-
warnings.warn(
6651-
f"Row(s) {np.where(~all_neg_indices)[0]} contained negative values and the row(s)"
6652-
f" have been removed (Rows are zero indexed).",
6653-
UserWarning,
6654-
stacklevel=2,
6655-
)
6656-
6657-
obs_copy = obs_copy[all_treatment_array]
6658-
sim_copy = sim_copy[all_treatment_array]
6659-
6660-
return sim_copy, obs_copy

src/HydroErr/typing_aliases.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from collections.abc import Sequence
2+
3+
import numpy as np
4+
from numpy.typing import NDArray
5+
6+
InputArray = NDArray[np.floating | np.integer] | Sequence[int | float]
7+
FloatArray = NDArray[np.floating | np.integer]

src/HydroErr/util.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import warnings
2+
3+
import numpy as np
4+
5+
from .typing_aliases import FloatArray, InputArray
6+
7+
8+
def treat_values( # noqa: C901
9+
simulated_array: InputArray,
10+
observed_array: InputArray,
11+
replace_nan: float | None = None,
12+
replace_inf: float | None = None,
13+
remove_neg: bool = False,
14+
remove_zero: bool = False,
15+
) -> tuple[FloatArray, FloatArray]:
16+
"""Remove the nan, negative, and inf values in two numpy arrays."""
17+
sim_copy: FloatArray = np.copy(simulated_array)
18+
obs_copy: FloatArray = np.copy(observed_array)
19+
20+
# Checking to see if the vectors are the same length
21+
if not sim_copy.ndim == 1:
22+
raise ValueError("The simulated array is not one dimensional.")
23+
if not obs_copy.ndim == 1:
24+
raise ValueError("The observed array is not one dimensional.")
25+
26+
if sim_copy.size != obs_copy.size:
27+
raise ValueError("The two ndarrays are not the same size.")
28+
29+
# Treat missing data in observed_array and simulated_array, rows in simulated_array or
30+
# observed_array that contain nan values
31+
all_treatment_array = np.ones(obs_copy.size, dtype=bool)
32+
33+
if np.any(np.isnan(obs_copy)) or np.any(np.isnan(sim_copy)):
34+
if replace_nan is not None:
35+
# Finding the NaNs
36+
sim_nan = np.isnan(sim_copy)
37+
obs_nan = np.isnan(obs_copy)
38+
# Replacing the NaNs with the input
39+
sim_copy[sim_nan] = replace_nan
40+
obs_copy[obs_nan] = replace_nan
41+
42+
warnings.warn(
43+
f"Elements(s) {np.where(sim_nan)[0]} contained NaN values in the simulated array"
44+
f" and elements(s) {np.where(obs_nan)[0]} contained NaN values in the observed"
45+
" array and have been replaced (Elements are zero indexed).",
46+
UserWarning,
47+
stacklevel=2,
48+
)
49+
else:
50+
# Getting the indices of the nan values, combining them, and informing user.
51+
nan_indices_fcst = ~np.isnan(sim_copy)
52+
nan_indices_obs = ~np.isnan(obs_copy)
53+
all_nan_indices = np.logical_and(nan_indices_fcst, nan_indices_obs)
54+
all_treatment_array = np.logical_and(all_treatment_array, all_nan_indices)
55+
56+
warnings.warn(
57+
f"Row(s) {np.where(~all_nan_indices)[0]} contained NaN values and the row(s) have"
58+
f" been removed (Rows are zero indexed).",
59+
UserWarning,
60+
stacklevel=2,
61+
)
62+
63+
if np.any(np.isinf(obs_copy)) or np.any(np.isinf(sim_copy)):
64+
if replace_nan is not None:
65+
# Finding the NaNs
66+
sim_inf = np.isinf(sim_copy)
67+
obs_inf = np.isinf(obs_copy)
68+
# Replacing the NaNs with the input
69+
sim_copy[sim_inf] = replace_inf
70+
obs_copy[obs_inf] = replace_inf
71+
72+
warnings.warn(
73+
f"Elements(s) {np.where(sim_inf)[0]} contained Inf values in the simulated array"
74+
f" and elements(s) {np.where(obs_inf)[0]} contained Inf values in the observed"
75+
" array and have been replaced (Elements are zero indexed).",
76+
UserWarning,
77+
stacklevel=2,
78+
)
79+
else:
80+
inf_indices_fcst = ~(np.isinf(sim_copy))
81+
inf_indices_obs = ~np.isinf(obs_copy)
82+
all_inf_indices = np.logical_and(inf_indices_fcst, inf_indices_obs)
83+
all_treatment_array = np.logical_and(all_treatment_array, all_inf_indices)
84+
85+
warnings.warn(
86+
f"Row(s) {np.where(~all_inf_indices)[0]} contained Inf or -Inf values and"
87+
" the row(s) have been removed (Rows are zero indexed).",
88+
UserWarning,
89+
stacklevel=2,
90+
)
91+
92+
# Treat zero data in observed_array and simulated_array, rows in simulated_array or
93+
# observed_array that contain zero values
94+
if remove_zero and ((obs_copy == 0).any() or (sim_copy == 0).any()):
95+
zero_indices_fcst = ~(sim_copy == 0)
96+
zero_indices_obs = ~(obs_copy == 0)
97+
all_zero_indices = np.logical_and(zero_indices_fcst, zero_indices_obs)
98+
all_treatment_array = np.logical_and(all_treatment_array, all_zero_indices)
99+
100+
warnings.warn(
101+
f"Row(s) {np.where(~all_zero_indices)[0]} contained zero values and the row(s)"
102+
" have been removed (Rows are zero indexed).",
103+
UserWarning,
104+
stacklevel=2,
105+
)
106+
107+
# Treat negative data in observed_array and simulated_array, rows in simulated_array or
108+
# observed_array that contain negative values
109+
110+
# Ignore runtime warnings from comparing
111+
if remove_neg:
112+
with np.errstate(invalid="ignore"):
113+
obs_copy_bool = obs_copy < 0
114+
sim_copy_bool = sim_copy < 0
115+
116+
if obs_copy_bool.any() or sim_copy_bool.any():
117+
neg_indices_fcst = ~sim_copy_bool
118+
neg_indices_obs = ~obs_copy_bool
119+
all_neg_indices = np.logical_and(neg_indices_fcst, neg_indices_obs)
120+
all_treatment_array = np.logical_and(all_treatment_array, all_neg_indices)
121+
122+
warnings.warn(
123+
f"Row(s) {np.where(~all_neg_indices)[0]} contained negative values and the row(s)"
124+
f" have been removed (Rows are zero indexed).",
125+
UserWarning,
126+
stacklevel=2,
127+
)
128+
129+
obs_copy = obs_copy[all_treatment_array]
130+
sim_copy = sim_copy[all_treatment_array]
131+
132+
return sim_copy, obs_copy

0 commit comments

Comments
 (0)