teehr 0.5.0__tar.gz → 0.5.1.dev10__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. {teehr-0.5.0 → teehr-0.5.1.dev10}/PKG-INFO +3 -2
  2. {teehr-0.5.0 → teehr-0.5.1.dev10}/pyproject.toml +2 -1
  3. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/__init__.py +2 -2
  4. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/examples/03-metric_queries.ipynb +1 -1
  5. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/fetching/usgs/usgs.py +16 -3
  6. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/metrics/bootstrap_funcs.py +82 -5
  7. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/metrics/deterministic_funcs.py +166 -34
  8. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/metrics/signature_funcs.py +65 -0
  9. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/models/calculated_fields/row_level.py +47 -2
  10. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/models/calculated_fields/timeseries_aware.py +453 -13
  11. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/models/metrics/deterministic_models.py +130 -19
  12. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/models/metrics/metric_attributes.py +16 -0
  13. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/models/metrics/signature_models.py +92 -11
  14. {teehr-0.5.0 → teehr-0.5.1.dev10}/LICENSE.md +0 -0
  15. {teehr-0.5.0 → teehr-0.5.1.dev10}/README.md +0 -0
  16. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/const.py +0 -0
  17. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/evaluation/__init__.py +0 -0
  18. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/evaluation/evaluation.py +0 -0
  19. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/evaluation/fetch.py +0 -0
  20. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/evaluation/generate.py +0 -0
  21. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/evaluation/metrics.py +0 -0
  22. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/evaluation/tables/attribute_table.py +0 -0
  23. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/evaluation/tables/base_table.py +0 -0
  24. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/evaluation/tables/configuration_table.py +0 -0
  25. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/evaluation/tables/domain_table.py +0 -0
  26. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/evaluation/tables/joined_timeseries_table.py +0 -0
  27. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/evaluation/tables/location_attribute_table.py +0 -0
  28. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/evaluation/tables/location_crosswalk_table.py +0 -0
  29. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/evaluation/tables/location_table.py +0 -0
  30. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/evaluation/tables/primary_timeseries_table.py +0 -0
  31. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/evaluation/tables/secondary_timeseries_table.py +0 -0
  32. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/evaluation/tables/timeseries_table.py +0 -0
  33. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/evaluation/tables/unit_table.py +0 -0
  34. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/evaluation/tables/variable_table.py +0 -0
  35. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/evaluation/utils.py +0 -0
  36. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/example_data/__init__.py +0 -0
  37. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/example_data/ensemble_example_data.py +0 -0
  38. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/example_data/nwm_gridded_example_data.py +0 -0
  39. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/example_data/nwm_streamflow_example_data.py +0 -0
  40. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/example_data/two_locations.py +0 -0
  41. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/example_data/v0_3_test_study.py +0 -0
  42. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/examples/01-evaluation_setup.ipynb +0 -0
  43. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/examples/02-table_queries.ipynb +0 -0
  44. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/examples/04-table_filters.ipynb +0 -0
  45. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/examples/05-2_site_setup.ipynb +0 -0
  46. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/examples/06-2_site_query.ipynb +0 -0
  47. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/examples/07-2_site_plotting.ipynb +0 -0
  48. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/examples/08-sql.ipynb +0 -0
  49. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/examples/09-ensemble_metric_queries.ipynb +0 -0
  50. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/examples/__init__.py +0 -0
  51. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/examples/clone_from_s3.ipynb +0 -0
  52. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/examples/load_netcdf.ipynb +0 -0
  53. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/examples/ngen_example.py +0 -0
  54. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/examples/partition_by_reference_time.py +0 -0
  55. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/examples/setup_ensemble_example.py +0 -0
  56. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/examples/setup_evaluation.py +0 -0
  57. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/examples/setup_nwm_grid_example.py +0 -0
  58. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/examples/setup_nwm_streamflow_example.py +0 -0
  59. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/examples/two_site_debug_script.py +0 -0
  60. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/fetching/__init__.py +0 -0
  61. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/fetching/const.py +0 -0
  62. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/fetching/nwm/__init__.py +0 -0
  63. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/fetching/nwm/grid_utils.py +0 -0
  64. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/fetching/nwm/nwm_grids.py +0 -0
  65. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/fetching/nwm/nwm_points.py +0 -0
  66. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/fetching/nwm/point_utils.py +0 -0
  67. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/fetching/nwm/retrospective_grids.py +0 -0
  68. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/fetching/nwm/retrospective_points.py +0 -0
  69. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/fetching/readme.md +0 -0
  70. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/fetching/usgs/__init__.py +0 -0
  71. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/fetching/utils.py +0 -0
  72. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/generate/utils.py +0 -0
  73. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/loading/__init__.py +0 -0
  74. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/loading/location_attributes.py +0 -0
  75. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/loading/location_crosswalks.py +0 -0
  76. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/loading/locations.py +0 -0
  77. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/loading/readme.md +0 -0
  78. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/loading/s3/clone_from_s3.py +0 -0
  79. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/loading/timeseries.py +0 -0
  80. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/loading/utils.py +0 -0
  81. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/metrics/__init__.py +0 -0
  82. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/metrics/gumboot_bootstrap.py +0 -0
  83. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/metrics/probabilistic_funcs.py +0 -0
  84. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/metrics/readme.md +0 -0
  85. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/models/__init__.py +0 -0
  86. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/models/calculated_fields/base.py +0 -0
  87. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/models/fetching/__init__.py +0 -0
  88. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/models/fetching/nwm12_grid.py +0 -0
  89. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/models/fetching/nwm12_point.py +0 -0
  90. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/models/fetching/nwm20_grid.py +0 -0
  91. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/models/fetching/nwm20_point.py +0 -0
  92. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/models/fetching/nwm22_grid.py +0 -0
  93. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/models/fetching/nwm22_point.py +0 -0
  94. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/models/fetching/nwm30_grid.py +0 -0
  95. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/models/fetching/nwm30_point.py +0 -0
  96. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/models/fetching/utils.py +0 -0
  97. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/models/filters.py +0 -0
  98. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/models/generate/base.py +0 -0
  99. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/models/generate/timeseries_generator_models.py +0 -0
  100. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/models/metrics/__init__.py +0 -0
  101. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/models/metrics/basemodels.py +0 -0
  102. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/models/metrics/bootstrap_models.py +0 -0
  103. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/models/metrics/probabilistic_models.py +0 -0
  104. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/models/pandera_dataframe_schemas.py +0 -0
  105. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/models/pydantic_table_models.py +0 -0
  106. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/models/readme.md +0 -0
  107. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/models/str_enum.py +0 -0
  108. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/models/table_enums.py +0 -0
  109. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/querying/__init__.py +0 -0
  110. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/querying/filter_format.py +0 -0
  111. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/querying/metric_format.py +0 -0
  112. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/querying/readme.md +0 -0
  113. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/querying/utils.py +0 -0
  114. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/template/__init__.py +0 -0
  115. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/template/cache/readme.md +0 -0
  116. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/template/dataset/attributes/_readme.md +0 -0
  117. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/template/dataset/attributes/attributes.csv +0 -0
  118. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/template/dataset/configurations/_readme.md +0 -0
  119. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/template/dataset/configurations/configurations.csv +0 -0
  120. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/template/dataset/joined_timeseries/_readme.md +0 -0
  121. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/template/dataset/location_attributes/_readme.md +0 -0
  122. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/template/dataset/location_crosswalks/_readme.md +0 -0
  123. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/template/dataset/locations/_readme.md +0 -0
  124. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/template/dataset/primary_timeseries/_readme.md +0 -0
  125. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/template/dataset/secondary_timeseries/_readme.md +0 -0
  126. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/template/dataset/units/_readme.md +0 -0
  127. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/template/dataset/units/units.csv +0 -0
  128. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/template/dataset/variables/_readme.md +0 -0
  129. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/template/dataset/variables/variables.csv +0 -0
  130. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/template/gitignore_template +0 -0
  131. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/template/readme.md +0 -0
  132. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/template/scripts/__init__.py +0 -0
  133. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/template/scripts/user_defined_fields.py +0 -0
  134. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/utilities/__init__.py +0 -0
  135. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/utilities/generate_weights.py +0 -0
  136. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/utils/remove_spark_jars.py +0 -0
  137. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/utils/s3path.py +0 -0
  138. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/utils/utils.py +0 -0
  139. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/visualization/__init__.py +0 -0
  140. {teehr-0.5.0 → teehr-0.5.1.dev10}/src/teehr/visualization/dataframe_accessor.py +0 -0
@@ -1,8 +1,9 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: teehr
3
- Version: 0.5.0
3
+ Version: 0.5.1.dev10
4
4
  Summary: Tools for Exploratory Evaluation in Hydrologic Research
5
5
  License: GNU v3
6
+ License-File: LICENSE.md
6
7
  Author: RTI International
7
8
  Requires-Python: >=3.10,<3.14
8
9
  Classifier: License :: Other/Proprietary License
@@ -1,12 +1,13 @@
1
1
  [tool.poetry]
2
2
  name = "teehr"
3
- version = "0.5.0"
3
+ version = "0.5.1dev10"
4
4
  description = "Tools for Exploratory Evaluation in Hydrologic Research"
5
5
  authors = [
6
6
  "RTI International",
7
7
  "Matthew Denno <mdenno@rti.org>",
8
8
  "Katie van Werkhoven <kvanwerkhoven@rti.org>",
9
9
  "Sam Lamont <slamont@rti.org>",
10
+ "Sam Landsteiner <slandsteiner@rti.org>",
10
11
  ]
11
12
  license = "GNU v3"
12
13
  readme = "README.md"
@@ -1,7 +1,7 @@
1
1
  """Initialize the TEEHR package."""
2
2
  import warnings
3
3
 
4
- __version__ = "0.5.0"
4
+ __version__ = "0.5.1dev10"
5
5
 
6
6
  with warnings.catch_warnings():
7
7
  warnings.simplefilter("ignore", UserWarning)
@@ -10,7 +10,7 @@ with warnings.catch_warnings():
10
10
  from teehr.evaluation.evaluation import Evaluation # noqa
11
11
  from teehr.models.metrics.deterministic_models import DeterministicMetrics # noqa
12
12
  from teehr.models.metrics.probabilistic_models import ProbabilisticMetrics # noqa
13
- from teehr.models.metrics.signature_models import SignatureMetrics # noqa
13
+ from teehr.models.metrics.signature_models import Signatures # noqa
14
14
  from teehr.models.metrics.bootstrap_models import Bootstrappers # noqa
15
15
  from teehr.models.metrics.basemodels import Operators # noqa
16
16
  from teehr.models.pydantic_table_models import ( # noqa
@@ -118,7 +118,7 @@
118
118
  "outputs": [],
119
119
  "source": [
120
120
  "from teehr import DeterministicMetrics as metrics\n",
121
- "from teehr import SignatureMetrics as sig_metrics"
121
+ "from teehr import Signatures as sig_metrics"
122
122
  ]
123
123
  },
124
124
  {
@@ -44,7 +44,9 @@ from teehr.fetching.const import (
44
44
  USGS_VARIABLE_MAPPER
45
45
  )
46
46
 
47
- DATETIME_STR_FMT = "%Y-%m-%dT%H:%M:00+0000"
47
+ HOURLY_DATETIME_STR_FMT = "%Y-%m-%dT%H:%M:00+0000"
48
+ DAILY_DATETIME_STR_FMT = "%Y-%m-%d"
49
+
48
50
  DAYLIGHT_SAVINGS_PAD = timedelta(hours=2)
49
51
 
50
52
  pd.options.mode.copy_on_write = True
@@ -211,11 +213,22 @@ def _fetch_usgs_streamflow(
211
213
  ) -> pd.DataFrame:
212
214
  """Fetch USGS gage data and format to TEEHR format."""
213
215
  logger.debug("Fetching USGS streamflow data from NWIS.")
214
- start_dt_str = start_date.strftime(DATETIME_STR_FMT)
216
+
217
+ if service == "iv":
218
+ datetime_str_format = HOURLY_DATETIME_STR_FMT
219
+ elif service == "dv":
220
+ datetime_str_format = DAILY_DATETIME_STR_FMT
221
+ else:
222
+ err_msg = f"Service '{service}' is not supported. Valid options are 'iv' and 'dv'."
223
+ logger.error(err_msg)
224
+ raise ValueError(err_msg)
225
+
226
+ start_dt_str = start_date.strftime(datetime_str_format)
227
+
215
228
  end_dt_str = (
216
229
  end_date
217
230
  - timedelta(minutes=1)
218
- ).strftime(DATETIME_STR_FMT)
231
+ ).strftime(datetime_str_format)
219
232
 
220
233
  # Parse out list of sites.
221
234
  parsed_sites = _parse_site_id_list(sites)
@@ -4,13 +4,8 @@ import logging
4
4
 
5
5
  import pandas as pd
6
6
  import numpy as np
7
- # from arch.bootstrap import (
8
- # StationaryBootstrap,
9
- # CircularBlockBootstrap,
10
- # )
11
7
 
12
8
  from teehr.models.metrics.basemodels import MetricsBasemodel
13
- # from teehr.metrics.gumboot_bootstrap import GumbootBootstrap
14
9
 
15
10
  logger = logging.getLogger(__name__)
16
11
 
@@ -57,6 +52,33 @@ def create_circularblock_func(model: MetricsBasemodel) -> Callable:
57
52
  )
58
53
  else:
59
54
  return results.ravel()
55
+
56
+ def signature_bootstrap_func(p: pd.Series) -> Dict:
57
+ """Bootstrap function."""
58
+ bs = CircularBlockBootstrap(
59
+ model.bootstrap.block_size,
60
+ p,
61
+ seed=model.bootstrap.seed,
62
+ random_state=model.bootstrap.random_state
63
+ )
64
+
65
+ results = bs.apply(
66
+ model.func(model),
67
+ model.bootstrap.reps
68
+ )
69
+
70
+ if model.bootstrap.quantiles is not None:
71
+ return _calculate_quantiles(
72
+ model.output_field_name,
73
+ results,
74
+ model.bootstrap.quantiles,
75
+ )
76
+ else:
77
+ return results.ravel()
78
+
79
+ if len(model.input_field_names) == 1:
80
+ return signature_bootstrap_func
81
+
60
82
  return bootstrap_func
61
83
 
62
84
 
@@ -91,6 +113,34 @@ def create_gumboot_func(model: MetricsBasemodel) -> Callable:
91
113
  )
92
114
  else:
93
115
  return results.ravel()
116
+
117
+ def signature_bootstrap_func(p: pd.Series, vt: pd.Series) -> Dict:
118
+ """Bootstrap function."""
119
+ bs = GumbootBootstrap(
120
+ p,
121
+ value_time=vt,
122
+ seed=model.bootstrap.seed,
123
+ water_year_month=model.bootstrap.water_year_month,
124
+ boot_year_file=model.bootstrap.boot_year_file
125
+ )
126
+
127
+ results = bs.apply(
128
+ model.func(model),
129
+ model.bootstrap.reps
130
+ )
131
+
132
+ if model.bootstrap.quantiles is not None:
133
+ return _calculate_quantiles(
134
+ model.output_field_name,
135
+ results,
136
+ model.bootstrap.quantiles,
137
+ )
138
+ else:
139
+ return results.ravel()
140
+
141
+ if len(model.input_field_names) == 1:
142
+ return signature_bootstrap_func
143
+
94
144
  return bootstrap_func
95
145
 
96
146
 
@@ -124,4 +174,31 @@ def create_stationary_func(model: MetricsBasemodel) -> Callable:
124
174
  )
125
175
  else:
126
176
  return results.ravel()
177
+
178
+ def signature_bootstrap_func(p: pd.Series) -> Dict:
179
+ """Bootstrap function."""
180
+ bs = StationaryBootstrap(
181
+ model.bootstrap.block_size,
182
+ p,
183
+ seed=model.bootstrap.seed,
184
+ random_state=model.bootstrap.random_state
185
+ )
186
+
187
+ results = bs.apply(
188
+ model.func(model),
189
+ model.bootstrap.reps
190
+ )
191
+
192
+ if model.bootstrap.quantiles is not None:
193
+ return _calculate_quantiles(
194
+ model.output_field_name,
195
+ results,
196
+ model.bootstrap.quantiles,
197
+ )
198
+ else:
199
+ return results.ravel()
200
+
201
+ if len(model.input_field_names) == 1:
202
+ return signature_bootstrap_func
203
+
127
204
  return bootstrap_func
@@ -2,6 +2,7 @@
2
2
  import numpy as np
3
3
  import numpy.typing as npt
4
4
  import pandas as pd
5
+ from scipy.stats import rankdata
5
6
 
6
7
  from teehr.models.metrics.basemodels import MetricsBasemodel
7
8
  from teehr.models.metrics.basemodels import TransformEnum
@@ -10,6 +11,8 @@ from typing import Callable, Optional
10
11
  import logging
11
12
  logger = logging.getLogger(__name__)
12
13
 
14
+ EPSILON = 1e-6 # Small constant to avoid division by zero
15
+
13
16
 
14
17
  def _transform(
15
18
  p: pd.Series,
@@ -22,6 +25,12 @@ def _transform(
22
25
  if model.transform is not None:
23
26
  match model.transform:
24
27
  case TransformEnum.log:
28
+ if model.add_epsilon:
29
+ logger.debug(
30
+ "Applying epsilon before log transform"
31
+ )
32
+ p = p + EPSILON
33
+ s = s + EPSILON
25
34
  logger.debug("Applying log transform")
26
35
  p = np.log(p)
27
36
  s = np.log(s)
@@ -42,6 +51,12 @@ def _transform(
42
51
  p = np.exp(p)
43
52
  s = np.exp(s)
44
53
  case TransformEnum.inv:
54
+ if model.add_epsilon:
55
+ logger.debug(
56
+ "Applying epsilon before inverse transform"
57
+ )
58
+ p = p + EPSILON
59
+ s = s + EPSILON
45
60
  logger.debug("Applying inverse transform")
46
61
  p = 1.0 / p
47
62
  s = 1.0 / s
@@ -125,7 +140,12 @@ def relative_bias(model: MetricsBasemodel) -> Callable:
125
140
  """Relative Bias."""
126
141
  p, s = _transform(p, s, model)
127
142
  difference = s - p
128
- return np.sum(difference)/np.sum(p)
143
+ if model.add_epsilon:
144
+ result = np.sum(difference)/(np.sum(p) + EPSILON)
145
+ else:
146
+ result = np.sum(difference)/np.sum(p)
147
+
148
+ return result
129
149
 
130
150
  return relative_bias_inner
131
151
 
@@ -142,7 +162,12 @@ def mean_absolute_relative_error(model: MetricsBasemodel) -> Callable:
142
162
  """Absolute Relative Error."""
143
163
  p, s = _transform(p, s, model)
144
164
  absolute_difference = np.abs(s - p)
145
- return np.sum(absolute_difference)/np.sum(p)
165
+ if model.add_epsilon:
166
+ result = np.sum(absolute_difference)/(np.sum(p) + EPSILON)
167
+ else:
168
+ result = np.sum(absolute_difference)/np.sum(p)
169
+
170
+ return result
146
171
 
147
172
  return mean_absolute_relative_error_inner
148
173
 
@@ -157,7 +182,12 @@ def multiplicative_bias(model: MetricsBasemodel) -> Callable:
157
182
  def multiplicative_bias_inner(p: pd.Series, s: pd.Series) -> float:
158
183
  """Multiplicative Bias."""
159
184
  p, s = _transform(p, s, model)
160
- return np.mean(s)/np.mean(p)
185
+ if model.add_epsilon:
186
+ result = np.mean(s)/(np.mean(p) + EPSILON)
187
+ else:
188
+ result = np.mean(s)/np.mean(p)
189
+
190
+ return result
161
191
 
162
192
  return multiplicative_bias_inner
163
193
 
@@ -172,11 +202,45 @@ def pearson_correlation(model: MetricsBasemodel) -> Callable:
172
202
  def pearson_correlation_inner(p: pd.Series, s: pd.Series) -> float:
173
203
  """Pearson Correlation Coefficient."""
174
204
  p, s = _transform(p, s, model)
175
- return np.corrcoef(s, p)[0][1]
205
+
206
+ if model.add_epsilon:
207
+ # Calculate covariance between p and s
208
+ numerator = np.cov(p, s)[0, 1]
209
+
210
+ # Calculate standard deviations and multiply them
211
+ denominator = np.nanstd(p) * np.nanstd(s) + EPSILON
212
+
213
+ # Calculate correlation coefficient
214
+ result = numerator / denominator
215
+
216
+ else:
217
+ result = np.corrcoef(s, p)[0][1]
218
+
219
+ return result
176
220
 
177
221
  return pearson_correlation_inner
178
222
 
179
223
 
224
+ def variability_ratio(model: MetricsBasemodel) -> Callable:
225
+ """Create the Variability Ratio metric function.
226
+
227
+ :math:`VR=\\frac{\\sigma_{sec}}{\\sigma_{prim}}`
228
+ """ # noqa
229
+ logger.debug("Building the variability_ratio metric function")
230
+
231
+ def variability_ratio_inner(p: pd.Series, s: pd.Series) -> float:
232
+ """Variability Ratio."""
233
+ p, s = _transform(p, s, model)
234
+ if model.add_epsilon:
235
+ result = (np.std(s))/(np.std(p) + EPSILON)
236
+ else:
237
+ result = np.std(s)/np.std(p)
238
+
239
+ return result
240
+
241
+ return variability_ratio_inner
242
+
243
+
180
244
  def r_squared(model: MetricsBasemodel) -> Callable:
181
245
  """Create the R-squared metric function.
182
246
 
@@ -187,8 +251,23 @@ def r_squared(model: MetricsBasemodel) -> Callable:
187
251
  def r_squared_inner(p: pd.Series, s: pd.Series) -> float:
188
252
  """R-squared."""
189
253
  p, s = _transform(p, s, model)
190
- pearson_correlation_coefficient = np.corrcoef(s, p)[0][1]
191
- return np.power(pearson_correlation_coefficient, 2)
254
+
255
+ if model.add_epsilon:
256
+ # Calculate covariance between p and s
257
+ numerator = np.cov(p, s)[0, 1]
258
+
259
+ # Calculate standard deviations and multiply them
260
+ denominator = np.nanstd(p) * np.nanstd(s) + EPSILON
261
+
262
+ # Calculate correlation coefficient and square it
263
+ pearson_correlation_coefficient = numerator / denominator
264
+ result = np.power(pearson_correlation_coefficient, 2)
265
+
266
+ else:
267
+ pearson_correlation_coefficient = np.corrcoef(s, p)[0][1]
268
+ result = np.power(pearson_correlation_coefficient, 2)
269
+
270
+ return result
192
271
 
193
272
  return r_squared_inner
194
273
 
@@ -235,10 +314,18 @@ def annual_peak_relative_bias(model: MetricsBasemodel) -> Callable:
235
314
  secondary_yearly_max_values = df.groupby(
236
315
  df.value_time.dt.year
237
316
  ).secondary_value.max()
238
- return np.sum(
239
- secondary_yearly_max_values
240
- - primary_yearly_max_values
241
- ) / np.sum(primary_yearly_max_values)
317
+ if model.add_epsilon:
318
+ result = np.sum(
319
+ secondary_yearly_max_values
320
+ - primary_yearly_max_values
321
+ ) / (np.sum(primary_yearly_max_values) + EPSILON)
322
+ else:
323
+ result = np.sum(
324
+ secondary_yearly_max_values
325
+ - primary_yearly_max_values
326
+ ) / np.sum(primary_yearly_max_values)
327
+
328
+ return result
242
329
 
243
330
  return annual_peak_relative_bias_inner
244
331
 
@@ -254,13 +341,23 @@ def spearman_correlation(model: MetricsBasemodel) -> Callable:
254
341
  """Spearman Rank Correlation Coefficient."""
255
342
  p, s = _transform(p, s, model)
256
343
 
257
- primary_rank = p.rank()
258
- secondary_rank = s.rank()
259
- count = len(p)
260
- return 1 - (
261
- 6 * np.sum(np.abs(primary_rank - secondary_rank)**2)
262
- / (count * (count**2 - 1))
263
- )
344
+ # calculate ranks (average method for ties)
345
+ primary_ranks = rankdata(p, method='average')
346
+ secondary_ranks = rankdata(s, method='average')
347
+
348
+ # calculate covariance between p_rank and s_rank
349
+ covariance = np.cov(primary_ranks, secondary_ranks)[0, 1]
350
+
351
+ # calculate standard deviations of ranks
352
+ std_primary = np.std(primary_ranks)
353
+ std_secondary = np.std(secondary_ranks)
354
+
355
+ if model.add_epsilon:
356
+ result = covariance / (std_primary * std_secondary + EPSILON)
357
+ else:
358
+ result = covariance / (std_primary * std_secondary)
359
+
360
+ return result
264
361
 
265
362
  return spearman_correlation_inner
266
363
 
@@ -282,7 +379,10 @@ def nash_sutcliffe_efficiency(model: MetricsBasemodel) -> Callable:
282
379
  p, s = _transform(p, s, model)
283
380
 
284
381
  numerator = np.sum(np.subtract(p, s) ** 2)
285
- denominator = np.sum(np.subtract(p, np.mean(p)) ** 2)
382
+ if model.add_epsilon:
383
+ denominator = np.sum(np.subtract(p, np.mean(p)) ** 2) + EPSILON
384
+ else:
385
+ denominator = np.sum(np.subtract(p, np.mean(p)) ** 2)
286
386
  if numerator == np.nan or denominator == np.nan:
287
387
  return np.nan
288
388
  if denominator == 0:
@@ -313,7 +413,10 @@ def nash_sutcliffe_efficiency_normalized(model: MetricsBasemodel) -> Callable:
313
413
  p, s = _transform(p, s, model)
314
414
 
315
415
  numerator = np.sum(np.subtract(p, s) ** 2)
316
- denominator = np.sum(np.subtract(p, np.mean(p)) ** 2)
416
+ if model.add_epsilon:
417
+ denominator = np.sum(np.subtract(p, np.mean(p)) ** 2) + EPSILON
418
+ else:
419
+ denominator = np.sum(np.subtract(p, np.mean(p)) ** 2)
317
420
  if numerator == np.nan or denominator == np.nan:
318
421
  return np.nan
319
422
  if denominator == 0:
@@ -343,10 +446,16 @@ def kling_gupta_efficiency(model: MetricsBasemodel) -> Callable:
343
446
  linear_correlation = np.corrcoef(s, p)[0, 1]
344
447
 
345
448
  # Relative variability
346
- relative_variability = np.std(s) / np.std(p)
449
+ if model.add_epsilon:
450
+ relative_variability = np.std(s) / (np.std(p) + EPSILON)
451
+ else:
452
+ relative_variability = np.std(s) / np.std(p)
347
453
 
348
454
  # Relative mean
349
- relative_mean = np.mean(s) / np.mean(p)
455
+ if model.add_epsilon:
456
+ relative_mean = np.mean(s) / (np.mean(p) + EPSILON)
457
+ else:
458
+ relative_mean = np.mean(s) / np.mean(p)
350
459
 
351
460
  # Scaled Euclidean distance
352
461
  euclidean_distance = np.sqrt(
@@ -379,13 +488,21 @@ def kling_gupta_efficiency_mod1(model: MetricsBasemodel) -> Callable:
379
488
  linear_correlation = np.corrcoef(s, p)[0, 1]
380
489
 
381
490
  # Variability_ratio
382
- variability_ratio = (
383
- (np.std(s) / np.mean(s))
384
- / (np.std(p) / np.mean(p))
385
- )
386
-
491
+ if model.add_epsilon:
492
+ variability_ratio = (
493
+ (np.std(s) / (np.mean(s) + EPSILON))
494
+ / (np.std(p) / (np.mean(p) + EPSILON))
495
+ )
496
+ else:
497
+ variability_ratio = (
498
+ (np.std(s) / np.mean(s))
499
+ / (np.std(p) / np.mean(p))
500
+ )
387
501
  # Relative mean (same as kge)
388
- relative_mean = (np.mean(s) / np.mean(p))
502
+ if model.add_epsilon:
503
+ relative_mean = (np.mean(s) / (np.mean(p) + EPSILON))
504
+ else:
505
+ relative_mean = (np.mean(s) / np.mean(p))
389
506
 
390
507
  # Scaled Euclidean distance
391
508
  euclidean_distance = np.sqrt(
@@ -417,14 +534,24 @@ def kling_gupta_efficiency_mod2(model: MetricsBasemodel) -> Callable:
417
534
  linear_correlation = np.corrcoef(s, p)[0, 1]
418
535
 
419
536
  # Relative variability (same as kge)
420
- relative_variability = (np.std(s) / np.std(p))
537
+ if model.add_epsilon:
538
+ relative_variability = (np.std(s) / (np.std(p) + EPSILON))
539
+ else:
540
+ relative_variability = (np.std(s) / np.std(p))
421
541
 
422
542
  # bias component
423
- bias_component = (
424
- ((np.mean(s) - np.mean(p)) ** 2)
425
- /
426
- (np.std(p) ** 2)
427
- )
543
+ if model.add_epsilon:
544
+ bias_component = (
545
+ ((np.mean(s) - np.mean(p)) ** 2)
546
+ /
547
+ ((np.std(p) ** 2) + EPSILON)
548
+ )
549
+ else:
550
+ bias_component = (
551
+ ((np.mean(s) - np.mean(p)) ** 2)
552
+ /
553
+ (np.std(p) ** 2)
554
+ )
428
555
 
429
556
  # Scaled Euclidean distance
430
557
  euclidean_distance = np.sqrt(
@@ -499,7 +626,12 @@ def root_mean_standard_deviation_ratio(model: MetricsBasemodel) -> Callable:
499
626
  p, s = _transform(p, s, model)
500
627
  rmse = _root_mean_squared_error(p, s)
501
628
  obs_std_dev = np.std(p)
502
- return rmse / obs_std_dev
629
+ if model.add_epsilon:
630
+ result = rmse / (obs_std_dev + EPSILON)
631
+ else:
632
+ result = rmse / obs_std_dev
633
+
634
+ return result
503
635
 
504
636
  return root_mean_standard_deviation_ratio_inner
505
637
 
@@ -8,6 +8,8 @@ from typing import Callable, Optional
8
8
  import logging
9
9
  logger = logging.getLogger(__name__)
10
10
 
11
+ EPSILON = 1e-6 # Small constant to avoid division by zero
12
+
11
13
 
12
14
  def _transform(
13
15
  p: pd.Series,
@@ -19,6 +21,9 @@ def _transform(
19
21
  if model.transform is not None:
20
22
  match model.transform:
21
23
  case TransformEnum.log:
24
+ if model.add_epsilon:
25
+ logger.debug("Adding epsilon to avoid log(0)")
26
+ p = p + EPSILON
22
27
  logger.debug("Applying log transform")
23
28
  p = np.log(p)
24
29
  case TransformEnum.sqrt:
@@ -34,6 +39,9 @@ def _transform(
34
39
  logger.debug("Applying exponential transform")
35
40
  p = np.exp(p)
36
41
  case TransformEnum.inv:
42
+ if model.add_epsilon:
43
+ logger.debug("Adding epsilon to avoid division by zero")
44
+ p = p + EPSILON
37
45
  logger.debug("Applying inverse transform")
38
46
  p = 1.0 / p
39
47
  case TransformEnum.abs:
@@ -151,3 +159,60 @@ def sum(model: MetricsBasemodel) -> Callable:
151
159
  return np.sum(p)
152
160
 
153
161
  return sum_inner
162
+
163
+
164
+ def flow_duration_curve_slope(model: MetricsBasemodel) -> Callable:
165
+ """Create flow duration curve slope metric function."""
166
+ logger.debug("Building the flow duration curve slope metric function")
167
+
168
+ def flow_duration_curve_slope_inner(
169
+ p: pd.Series,
170
+ ) -> float:
171
+ """Flow duration curve slope."""
172
+ # ensure percentiles are within valid range
173
+ if not (0 <= model.lower_quantile <= 1):
174
+ raise ValueError(
175
+ "Lower quantile must be between 0 and 1"
176
+ )
177
+ if not (0 <= model.upper_quantile <= 1):
178
+ raise ValueError(
179
+ "Upper quantile must be between 0 and 1"
180
+ )
181
+
182
+ # ensure lower quantile is less than upper quantile
183
+ if model.lower_quantile >= model.upper_quantile:
184
+ raise ValueError(
185
+ "Lower quantile must be less than upper quantile"
186
+ )
187
+
188
+ # apply any specified transform
189
+ p = _transform(p, model)
190
+
191
+ # sort the streamflow values in descending order
192
+ p_sorted = p.sort_values(ascending=False).reset_index(drop=True)
193
+
194
+ # calculate exceedance probabilities
195
+ n = len(p_sorted)
196
+ fdc_probs = (p_sorted.index/(n+1))
197
+
198
+ # determine indices for the specified quantiles
199
+ lower_idx = np.argmin(np.abs(fdc_probs - model.lower_quantile))
200
+ upper_idx = np.argmin(np.abs(fdc_probs - model.upper_quantile))
201
+
202
+ # check for as_percentile flag
203
+ if model.as_percentile:
204
+ fdc_probs = fdc_probs * 100
205
+
206
+ # calculate slope between the two quantiles
207
+ if model.add_epsilon:
208
+ slope = (p_sorted.iloc[upper_idx] - p_sorted.iloc[lower_idx]) / ((
209
+ fdc_probs[upper_idx] - fdc_probs[lower_idx]
210
+ ) + EPSILON)
211
+ else:
212
+ slope = (p_sorted.iloc[upper_idx] - p_sorted.iloc[lower_idx]) / (
213
+ fdc_probs[upper_idx] - fdc_probs[lower_idx]
214
+ )
215
+
216
+ return slope
217
+
218
+ return flow_duration_curve_slope_inner
@@ -267,7 +267,7 @@ class ThresholdValueExceeded(CalculatedFieldABC, CalculatedFieldBaseModel):
267
267
  Default: "primary_value"
268
268
  - threshold_field_name:
269
269
  The name of the column containing the threshold value.
270
- Default: 0
270
+ Default: "secondary_value"
271
271
  - output_field_name:
272
272
  The name of the column to store the boolean value.
273
273
  Default: "threshold_value_exceeded"
@@ -290,7 +290,51 @@ class ThresholdValueExceeded(CalculatedFieldABC, CalculatedFieldBaseModel):
290
290
  def func(input_value: pd.Series,
291
291
  threshold_value: pd.Series
292
292
  ) -> pd.Series:
293
- mask = input_value > threshold_value
293
+ mask = input_value.astype(float) > threshold_value.astype(float)
294
+ return mask
295
+
296
+ sdf = sdf.withColumn(
297
+ self.output_field_name,
298
+ func(self.input_field_name,
299
+ self.threshold_field_name)
300
+ )
301
+ return sdf
302
+
303
+
304
+ class ThresholdValueNotExceeded(CalculatedFieldABC, CalculatedFieldBaseModel):
305
+ """Adds boolean column indicating if the input value is less than or equal to a threshold.
306
+
307
+ Properties
308
+ ----------
309
+ - input_field_name:
310
+ The name of the column containing the primary value.
311
+ Default: "primary_value"
312
+ - threshold_field_name:
313
+ The name of the column containing the threshold value.
314
+ Default: "secondary_value"
315
+ - output_field_name:
316
+ The name of the column to store the boolean value.
317
+ Default: "threshold_value_not_exceeded"
318
+
319
+ """ # noqa
320
+
321
+ input_field_name: str = Field(
322
+ default="primary_value"
323
+ )
324
+ threshold_field_name: str = Field(
325
+ default="secondary_value"
326
+ )
327
+ output_field_name: str = Field(
328
+ default="threshold_value_not_exceeded"
329
+ )
330
+
331
+ def apply_to(self, sdf: ps.DataFrame) -> ps.DataFrame:
332
+ """Apply the calculated field to the Spark DataFrame."""
333
+ @pandas_udf(returnType=T.BooleanType())
334
+ def func(input_value: pd.Series,
335
+ threshold_value: pd.Series
336
+ ) -> pd.Series:
337
+ mask = input_value.astype(float) <= threshold_value.astype(float)
294
338
  return mask
295
339
 
296
340
  sdf = sdf.withColumn(
@@ -429,5 +473,6 @@ class RowLevelCalculatedFields:
429
473
  Seasons = Seasons
430
474
  ForecastLeadTime = ForecastLeadTime
431
475
  ThresholdValueExceeded = ThresholdValueExceeded
476
+ ThresholdValueNotExceeded = ThresholdValueNotExceeded
432
477
  DayOfYear = DayOfYear
433
478
  HourOfYear = HourOfYear