teehr 0.5.1.dev10__tar.gz → 0.5.3__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.
- {teehr-0.5.1.dev10 → teehr-0.5.3}/PKG-INFO +2 -2
- {teehr-0.5.1.dev10 → teehr-0.5.3}/pyproject.toml +2 -2
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/__init__.py +1 -1
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/evaluation/fetch.py +2 -4
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/evaluation/metrics.py +33 -1
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/fetching/nwm/grid_utils.py +7 -1
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/fetching/nwm/nwm_grids.py +2 -1
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/fetching/nwm/nwm_points.py +2 -1
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/fetching/nwm/point_utils.py +18 -3
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/loading/s3/clone_from_s3.py +23 -6
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/loading/timeseries.py +1 -11
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/metrics/deterministic_funcs.py +221 -11
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/metrics/probabilistic_funcs.py +86 -0
- teehr-0.5.3/src/teehr/models/calculated_fields/row_level.py +938 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/models/calculated_fields/timeseries_aware.py +5 -4
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/models/metrics/basemodels.py +13 -2
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/models/metrics/bootstrap_models.py +0 -9
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/models/metrics/deterministic_models.py +217 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/models/metrics/metric_attributes.py +84 -1
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/models/metrics/probabilistic_models.py +36 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/querying/metric_format.py +9 -0
- teehr-0.5.1.dev10/src/teehr/models/calculated_fields/row_level.py +0 -478
- {teehr-0.5.1.dev10 → teehr-0.5.3}/LICENSE.md +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/README.md +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/const.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/evaluation/__init__.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/evaluation/evaluation.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/evaluation/generate.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/evaluation/tables/attribute_table.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/evaluation/tables/base_table.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/evaluation/tables/configuration_table.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/evaluation/tables/domain_table.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/evaluation/tables/joined_timeseries_table.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/evaluation/tables/location_attribute_table.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/evaluation/tables/location_crosswalk_table.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/evaluation/tables/location_table.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/evaluation/tables/primary_timeseries_table.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/evaluation/tables/secondary_timeseries_table.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/evaluation/tables/timeseries_table.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/evaluation/tables/unit_table.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/evaluation/tables/variable_table.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/evaluation/utils.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/example_data/__init__.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/example_data/ensemble_example_data.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/example_data/nwm_gridded_example_data.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/example_data/nwm_streamflow_example_data.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/example_data/two_locations.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/example_data/v0_3_test_study.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/examples/01-evaluation_setup.ipynb +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/examples/02-table_queries.ipynb +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/examples/03-metric_queries.ipynb +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/examples/04-table_filters.ipynb +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/examples/05-2_site_setup.ipynb +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/examples/06-2_site_query.ipynb +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/examples/07-2_site_plotting.ipynb +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/examples/08-sql.ipynb +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/examples/09-ensemble_metric_queries.ipynb +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/examples/__init__.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/examples/clone_from_s3.ipynb +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/examples/load_netcdf.ipynb +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/examples/ngen_example.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/examples/partition_by_reference_time.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/examples/setup_ensemble_example.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/examples/setup_evaluation.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/examples/setup_nwm_grid_example.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/examples/setup_nwm_streamflow_example.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/examples/two_site_debug_script.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/fetching/__init__.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/fetching/const.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/fetching/nwm/__init__.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/fetching/nwm/retrospective_grids.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/fetching/nwm/retrospective_points.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/fetching/readme.md +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/fetching/usgs/__init__.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/fetching/usgs/usgs.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/fetching/utils.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/generate/utils.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/loading/__init__.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/loading/location_attributes.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/loading/location_crosswalks.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/loading/locations.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/loading/readme.md +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/loading/utils.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/metrics/__init__.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/metrics/bootstrap_funcs.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/metrics/gumboot_bootstrap.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/metrics/readme.md +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/metrics/signature_funcs.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/models/__init__.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/models/calculated_fields/base.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/models/fetching/__init__.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/models/fetching/nwm12_grid.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/models/fetching/nwm12_point.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/models/fetching/nwm20_grid.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/models/fetching/nwm20_point.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/models/fetching/nwm22_grid.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/models/fetching/nwm22_point.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/models/fetching/nwm30_grid.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/models/fetching/nwm30_point.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/models/fetching/utils.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/models/filters.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/models/generate/base.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/models/generate/timeseries_generator_models.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/models/metrics/__init__.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/models/metrics/signature_models.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/models/pandera_dataframe_schemas.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/models/pydantic_table_models.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/models/readme.md +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/models/str_enum.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/models/table_enums.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/querying/__init__.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/querying/filter_format.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/querying/readme.md +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/querying/utils.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/template/__init__.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/template/cache/readme.md +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/template/dataset/attributes/_readme.md +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/template/dataset/attributes/attributes.csv +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/template/dataset/configurations/_readme.md +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/template/dataset/configurations/configurations.csv +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/template/dataset/joined_timeseries/_readme.md +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/template/dataset/location_attributes/_readme.md +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/template/dataset/location_crosswalks/_readme.md +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/template/dataset/locations/_readme.md +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/template/dataset/primary_timeseries/_readme.md +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/template/dataset/secondary_timeseries/_readme.md +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/template/dataset/units/_readme.md +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/template/dataset/units/units.csv +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/template/dataset/variables/_readme.md +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/template/dataset/variables/variables.csv +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/template/gitignore_template +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/template/readme.md +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/template/scripts/__init__.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/template/scripts/user_defined_fields.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/utilities/__init__.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/utilities/generate_weights.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/utils/remove_spark_jars.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/utils/s3path.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/utils/utils.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/visualization/__init__.py +0 -0
- {teehr-0.5.1.dev10 → teehr-0.5.3}/src/teehr/visualization/dataframe_accessor.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: teehr
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.3
|
|
4
4
|
Summary: Tools for Exploratory Evaluation in Hydrologic Research
|
|
5
5
|
License: GNU v3
|
|
6
6
|
License-File: LICENSE.md
|
|
@@ -34,7 +34,7 @@ Requires-Dist: psutil (>=7.0.0,<8)
|
|
|
34
34
|
Requires-Dist: pyarrow (>=15.0.0,<21)
|
|
35
35
|
Requires-Dist: pydantic (>=2.4.2,<3)
|
|
36
36
|
Requires-Dist: pyiceberg (>=0.9.1,<1)
|
|
37
|
-
Requires-Dist: pyspark[pandas-on-spark] (
|
|
37
|
+
Requires-Dist: pyspark[pandas-on-spark] (==4.0.1)
|
|
38
38
|
Requires-Dist: rasterio (>=1.3.0,<2)
|
|
39
39
|
Requires-Dist: rioxarray (>=0.15.0,<1)
|
|
40
40
|
Requires-Dist: s3fs (>=2023.10.0,<2026)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "teehr"
|
|
3
|
-
version = "0.5.
|
|
3
|
+
version = "0.5.3"
|
|
4
4
|
description = "Tools for Exploratory Evaluation in Hydrologic Research"
|
|
5
5
|
authors = [
|
|
6
6
|
"RTI International",
|
|
@@ -29,7 +29,7 @@ h5py = ">=3.12.1,<4"
|
|
|
29
29
|
pyarrow = ">=15.0.0,<21"
|
|
30
30
|
httpx = ">=0.25.1,<1"
|
|
31
31
|
pandas = ">=2.2.0,<3"
|
|
32
|
-
pyspark = {extras = ["pandas-on-spark"], version = "
|
|
32
|
+
pyspark = {extras = ["pandas-on-spark"], version = "4.0.1"}
|
|
33
33
|
dataretrieval = ">=1.0.9,<2"
|
|
34
34
|
numba = ">=0.60.0,<1"
|
|
35
35
|
arch = ">=7.0.0,<8"
|
|
@@ -911,8 +911,7 @@ class Fetch:
|
|
|
911
911
|
),
|
|
912
912
|
timeseries_type=timeseries_type,
|
|
913
913
|
write_mode=write_mode,
|
|
914
|
-
drop_duplicates=drop_duplicates
|
|
915
|
-
drop_overlapping_assimilation_values=drop_overlapping_assimilation_values # noqa
|
|
914
|
+
drop_duplicates=drop_duplicates
|
|
916
915
|
)
|
|
917
916
|
|
|
918
917
|
def nwm_operational_grids(
|
|
@@ -1215,6 +1214,5 @@ class Fetch:
|
|
|
1215
1214
|
in_path=Path(self.nwm_cache_dir),
|
|
1216
1215
|
timeseries_type=timeseries_type,
|
|
1217
1216
|
write_mode=write_mode,
|
|
1218
|
-
drop_duplicates=drop_duplicates
|
|
1219
|
-
drop_overlapping_assimilation_values=drop_overlapping_assimilation_values # noqa
|
|
1217
|
+
drop_duplicates=drop_duplicates
|
|
1220
1218
|
)
|
|
@@ -239,11 +239,34 @@ class Metrics:
|
|
|
239
239
|
"""
|
|
240
240
|
for model in include_metrics:
|
|
241
241
|
if model.reference_configuration is not None:
|
|
242
|
+
"""
|
|
242
243
|
self.df = self._calculate_metric_skill_score(
|
|
243
244
|
model.output_field_name,
|
|
244
245
|
model.reference_configuration,
|
|
245
246
|
group_by
|
|
246
247
|
)
|
|
248
|
+
"""
|
|
249
|
+
# 1) get the original cols ahead of skill score join
|
|
250
|
+
original_cols = self.df.columns
|
|
251
|
+
# 2) calculate skill score sdf
|
|
252
|
+
sdf = self._calculate_metric_skill_score(
|
|
253
|
+
model.output_field_name,
|
|
254
|
+
model.reference_configuration,
|
|
255
|
+
group_by
|
|
256
|
+
)
|
|
257
|
+
# 3) remove original metric column from skill score sdf
|
|
258
|
+
sdf = sdf.drop(model.output_field_name)
|
|
259
|
+
# 3) get join columns
|
|
260
|
+
join_cols = parse_fields_to_list(group_by)
|
|
261
|
+
# 4) join returned table back to self.df, trim
|
|
262
|
+
self.df = self.df.join(
|
|
263
|
+
sdf,
|
|
264
|
+
on=join_cols,
|
|
265
|
+
how="left"
|
|
266
|
+
).select(
|
|
267
|
+
*original_cols,
|
|
268
|
+
F.col(f"{model.output_field_name}_skill_score")
|
|
269
|
+
)
|
|
247
270
|
|
|
248
271
|
if model.unpack_results:
|
|
249
272
|
self.df = model.unpack_function(
|
|
@@ -292,11 +315,20 @@ class Metrics:
|
|
|
292
315
|
temp_col = f"{config}_{metric_field}_skill"
|
|
293
316
|
pivot_sdf = pivot_sdf.withColumn(
|
|
294
317
|
temp_col,
|
|
295
|
-
1 - F.col(config)
|
|
318
|
+
1 - F.try_divide(F.col(config), F.col(reference_configuration))
|
|
296
319
|
).withColumn(
|
|
297
320
|
"configuration_name",
|
|
298
321
|
F.lit(config)
|
|
299
322
|
)
|
|
323
|
+
# warn user if try_divide results in nulls (division by zero)
|
|
324
|
+
null_count = pivot_sdf.filter(F.col(temp_col).isNull()).count()
|
|
325
|
+
if null_count > 0:
|
|
326
|
+
logger.warning(
|
|
327
|
+
f"Division by zero encountered when calculating skill "
|
|
328
|
+
f"score for configuration '{config}' relative to "
|
|
329
|
+
f"reference configuration '{reference_configuration}'. "
|
|
330
|
+
f"{null_count} null values were produced."
|
|
331
|
+
)
|
|
300
332
|
# Join skill score values from the pivot table.
|
|
301
333
|
join_cols = group_by_strings + ["configuration_name"]
|
|
302
334
|
sdf = sdf.join(
|
|
@@ -183,7 +183,8 @@ def fetch_and_format_nwm_grids(
|
|
|
183
183
|
overwrite_output: bool,
|
|
184
184
|
location_id_prefix: Union[str, None],
|
|
185
185
|
variable_mapper: Dict[str, Dict[str, str]],
|
|
186
|
-
timeseries_type: TimeseriesTypeEnum
|
|
186
|
+
timeseries_type: TimeseriesTypeEnum,
|
|
187
|
+
drop_overlapping_assimilation_values: bool
|
|
187
188
|
):
|
|
188
189
|
"""Compute weighted average, grouping by reference time.
|
|
189
190
|
|
|
@@ -243,6 +244,11 @@ def fetch_and_format_nwm_grids(
|
|
|
243
244
|
Path(output_parquet_dir), f"{ref_time_str}.parquet"
|
|
244
245
|
)
|
|
245
246
|
z_hour_df.sort_values([LOCATION_ID, VALUE_TIME], inplace=True)
|
|
247
|
+
|
|
248
|
+
if drop_overlapping_assimilation_values and "assim" in nwm_configuration_name:
|
|
249
|
+
# Set reference_time to NaT for assimilation values
|
|
250
|
+
z_hour_df.loc[:, REFERENCE_TIME] = pd.NaT
|
|
251
|
+
|
|
246
252
|
write_timeseries_parquet_file(
|
|
247
253
|
filepath=parquet_filepath,
|
|
248
254
|
overwrite_output=overwrite_output,
|
|
@@ -382,5 +382,6 @@ def nwm_grids_to_parquet(
|
|
|
382
382
|
overwrite_output=overwrite_output,
|
|
383
383
|
location_id_prefix=location_id_prefix,
|
|
384
384
|
variable_mapper=variable_mapper,
|
|
385
|
-
timeseries_type=timeseries_type
|
|
385
|
+
timeseries_type=timeseries_type,
|
|
386
|
+
drop_overlapping_assimilation_values=drop_overlapping_assimilation_values
|
|
386
387
|
)
|
|
@@ -102,7 +102,8 @@ def process_chunk_of_files(
|
|
|
102
102
|
overwrite_output: bool,
|
|
103
103
|
nwm_version: str,
|
|
104
104
|
variable_mapper: Dict[str, Dict[str, str]],
|
|
105
|
-
timeseries_type: TimeseriesTypeEnum
|
|
105
|
+
timeseries_type: TimeseriesTypeEnum,
|
|
106
|
+
drop_overlapping_assimilation_values: bool
|
|
106
107
|
):
|
|
107
108
|
"""Assemble a table for a chunk of NWM files."""
|
|
108
109
|
location_ids = np.array(location_ids).astype(int)
|
|
@@ -156,6 +157,12 @@ def process_chunk_of_files(
|
|
|
156
157
|
end = f"{end_json[1]}T{end_json[3][1:3]}F{end_json[6][1:]}"
|
|
157
158
|
filename = f"{start}_{end}.parquet"
|
|
158
159
|
|
|
160
|
+
if drop_overlapping_assimilation_values and "assim" in configuration:
|
|
161
|
+
# Set reference_time to NaT for assimilation values
|
|
162
|
+
df_output = output_table.to_pandas()
|
|
163
|
+
df_output.loc[:, REFERENCE_TIME] = pd.NaT
|
|
164
|
+
output_table = pa.Table.from_pandas(df_output, schema=schema)
|
|
165
|
+
|
|
159
166
|
write_timeseries_parquet_file(
|
|
160
167
|
Path(output_parquet_dir, filename),
|
|
161
168
|
overwrite_output,
|
|
@@ -176,7 +183,8 @@ def fetch_and_format_nwm_points(
|
|
|
176
183
|
overwrite_output: bool,
|
|
177
184
|
nwm_version: str,
|
|
178
185
|
variable_mapper: Dict[str, Dict[str, str]],
|
|
179
|
-
timeseries_type: TimeseriesTypeEnum
|
|
186
|
+
timeseries_type: TimeseriesTypeEnum,
|
|
187
|
+
drop_overlapping_assimilation_values: bool
|
|
180
188
|
):
|
|
181
189
|
"""Fetch NWM point data and save as parquet files.
|
|
182
190
|
|
|
@@ -211,6 +219,12 @@ def fetch_and_format_nwm_points(
|
|
|
211
219
|
they already exist. True = overwrite; False = fail.
|
|
212
220
|
nwm_version : str
|
|
213
221
|
Specified NWM version.
|
|
222
|
+
variable_mapper : Dict[str, Dict[str, str]]
|
|
223
|
+
A mapping dictionary for variable names and units.
|
|
224
|
+
timeseries_type : TimeseriesTypeEnum
|
|
225
|
+
The type of timeseries being processed.
|
|
226
|
+
drop_overlapping_assimilation_values : bool
|
|
227
|
+
Whether to drop assimilation values that overlap in value_time.
|
|
214
228
|
"""
|
|
215
229
|
output_parquet_dir = Path(output_parquet_dir)
|
|
216
230
|
if not output_parquet_dir.exists():
|
|
@@ -241,5 +255,6 @@ def fetch_and_format_nwm_points(
|
|
|
241
255
|
overwrite_output,
|
|
242
256
|
nwm_version,
|
|
243
257
|
variable_mapper,
|
|
244
|
-
timeseries_type
|
|
258
|
+
timeseries_type,
|
|
259
|
+
drop_overlapping_assimilation_values
|
|
245
260
|
)
|
|
@@ -49,9 +49,22 @@ def subset_the_table(
|
|
|
49
49
|
"""Subset the dataset based on location and start/end time."""
|
|
50
50
|
if table.name == "locations" and primary_location_ids is not None:
|
|
51
51
|
sdf_in = sdf_in.filter(sdf_in.id.isin(primary_location_ids))
|
|
52
|
-
|
|
52
|
+
# warn user if any primary_location_ids were excluded
|
|
53
|
+
available_ids = set(
|
|
54
|
+
sdf_in.select("id").rdd.flatMap(
|
|
55
|
+
lambda x: x).collect()
|
|
56
|
+
)
|
|
57
|
+
missing_ids = set(primary_location_ids) - available_ids
|
|
58
|
+
if len(missing_ids) > 0:
|
|
59
|
+
logger.warning(
|
|
60
|
+
"The following primary_location_ids were not found in the "
|
|
61
|
+
f"locations table and will be excluded: {missing_ids}"
|
|
62
|
+
)
|
|
63
|
+
elif table.name == "location_attributes" and \
|
|
64
|
+
primary_location_ids is not None:
|
|
53
65
|
sdf_in = sdf_in.filter(sdf_in.location_id.isin(primary_location_ids))
|
|
54
|
-
elif table.name == "location_crosswalks" and
|
|
66
|
+
elif table.name == "location_crosswalks" and \
|
|
67
|
+
primary_location_ids is not None:
|
|
55
68
|
sdf_in = sdf_in.filter(
|
|
56
69
|
sdf_in.primary_location_id.isin(primary_location_ids)
|
|
57
70
|
)
|
|
@@ -64,7 +77,8 @@ def subset_the_table(
|
|
|
64
77
|
if primary_location_ids is not None:
|
|
65
78
|
secondary_ids = (
|
|
66
79
|
ev.location_crosswalks.to_sdf()
|
|
67
|
-
.select("secondary_location_id").rdd.flatMap(
|
|
80
|
+
.select("secondary_location_id").rdd.flatMap(
|
|
81
|
+
lambda x: x).collect()
|
|
68
82
|
)
|
|
69
83
|
sdf_in = sdf_in.filter(sdf_in.location_id.isin(secondary_ids))
|
|
70
84
|
elif table.name == "joined_timeseries":
|
|
@@ -119,7 +133,6 @@ def clone_from_s3(
|
|
|
119
133
|
|
|
120
134
|
Note: future version will allow subsetting the tables to clone.
|
|
121
135
|
"""
|
|
122
|
-
|
|
123
136
|
# Make the Evaluation directories
|
|
124
137
|
logger.info(f"Creating directories for evaluation: {evaluation_name}")
|
|
125
138
|
Path(ev.cache_dir).mkdir()
|
|
@@ -174,7 +187,10 @@ def clone_from_s3(
|
|
|
174
187
|
logger.debug(f"Making directory {table.dir}")
|
|
175
188
|
Path(table.dir).mkdir()
|
|
176
189
|
|
|
177
|
-
logger.debug(
|
|
190
|
+
logger.debug(
|
|
191
|
+
f"Cloning {table.name} from {s3_dataset_path}/{table.name}/ to "
|
|
192
|
+
f"{table.dir}"
|
|
193
|
+
)
|
|
178
194
|
|
|
179
195
|
sdf_in = table._read_files(
|
|
180
196
|
path=f"{s3_dataset_path}/{table.name}/",
|
|
@@ -200,7 +216,8 @@ def clone_from_s3(
|
|
|
200
216
|
dest = f"{ev.scripts_dir}/user_defined_fields.py"
|
|
201
217
|
logger.debug(f"Copying from {source}/ to {dest}")
|
|
202
218
|
|
|
203
|
-
# ToDo: there is a permission issue that prevents copying the entire
|
|
219
|
+
# ToDo: there is a permission issue that prevents copying the entire
|
|
220
|
+
# directory.
|
|
204
221
|
# This works for now.
|
|
205
222
|
with fsspec.open(source, 'r', anon=True) as file:
|
|
206
223
|
with open(dest, 'w') as f:
|
|
@@ -247,8 +247,7 @@ def validate_and_insert_timeseries(
|
|
|
247
247
|
timeseries_type: str,
|
|
248
248
|
pattern: str = "**/*.parquet",
|
|
249
249
|
write_mode: TableWriteEnum = "append",
|
|
250
|
-
drop_duplicates: bool = True
|
|
251
|
-
drop_overlapping_assimilation_values: bool = False
|
|
250
|
+
drop_duplicates: bool = True
|
|
252
251
|
):
|
|
253
252
|
"""Validate and insert primary timeseries data.
|
|
254
253
|
|
|
@@ -272,12 +271,6 @@ def validate_and_insert_timeseries(
|
|
|
272
271
|
drop_duplicates : bool, optional (default: True)
|
|
273
272
|
Whether to drop duplicates in the dataframe before writing
|
|
274
273
|
to the table.
|
|
275
|
-
drop_overlapping_assimilation_values: Optional[bool] = True
|
|
276
|
-
Whether to drop overlapping assimilation values. Default is True.
|
|
277
|
-
If True, values that overlap in value_time are dropped, keeping those with
|
|
278
|
-
the most recent reference_time. In this case, all reference_time values
|
|
279
|
-
are set to None. If False, overlapping values are kept and reference_time
|
|
280
|
-
is retained.
|
|
281
274
|
""" # noqa
|
|
282
275
|
in_path = Path(in_path)
|
|
283
276
|
logger.info(f"Validating and inserting timeseries data from {in_path}")
|
|
@@ -292,9 +285,6 @@ def validate_and_insert_timeseries(
|
|
|
292
285
|
# Read the converted files to Spark DataFrame
|
|
293
286
|
df = table._read_files(in_path, pattern)
|
|
294
287
|
|
|
295
|
-
if drop_overlapping_assimilation_values:
|
|
296
|
-
df = df.withColumn("reference_time", lit(None))
|
|
297
|
-
|
|
298
288
|
# Validate using the _validate() method
|
|
299
289
|
validated_df = table._validate(
|
|
300
290
|
df=df,
|
|
@@ -18,7 +18,8 @@ def _transform(
|
|
|
18
18
|
p: pd.Series,
|
|
19
19
|
s: pd.Series,
|
|
20
20
|
model: MetricsBasemodel,
|
|
21
|
-
t: Optional[pd.Series] = None
|
|
21
|
+
t: Optional[pd.Series] = None,
|
|
22
|
+
threshold_series: Optional[pd.Series] = None
|
|
22
23
|
) -> tuple:
|
|
23
24
|
"""Apply timeseries transform for metrics calculations."""
|
|
24
25
|
# Apply transform
|
|
@@ -31,25 +32,37 @@ def _transform(
|
|
|
31
32
|
)
|
|
32
33
|
p = p + EPSILON
|
|
33
34
|
s = s + EPSILON
|
|
35
|
+
if threshold_series is not None:
|
|
36
|
+
threshold_series = threshold_series + EPSILON
|
|
34
37
|
logger.debug("Applying log transform")
|
|
35
38
|
p = np.log(p)
|
|
36
39
|
s = np.log(s)
|
|
40
|
+
if threshold_series is not None:
|
|
41
|
+
threshold_series = np.log(threshold_series)
|
|
37
42
|
case TransformEnum.sqrt:
|
|
38
43
|
logger.debug("Applying square root transform")
|
|
39
44
|
p = np.sqrt(p)
|
|
40
45
|
s = np.sqrt(s)
|
|
46
|
+
if threshold_series is not None:
|
|
47
|
+
threshold_series = np.sqrt(threshold_series)
|
|
41
48
|
case TransformEnum.square:
|
|
42
49
|
logger.debug("Applying square transform")
|
|
43
50
|
p = np.square(p)
|
|
44
51
|
s = np.square(s)
|
|
52
|
+
if threshold_series is not None:
|
|
53
|
+
threshold_series = np.square(threshold_series)
|
|
45
54
|
case TransformEnum.cube:
|
|
46
55
|
logger.debug("Applying cube transform")
|
|
47
56
|
p = np.power(p, 3)
|
|
48
57
|
s = np.power(s, 3)
|
|
58
|
+
if threshold_series is not None:
|
|
59
|
+
threshold_series = np.power(threshold_series, 3)
|
|
49
60
|
case TransformEnum.exp:
|
|
50
61
|
logger.debug("Applying exponential transform")
|
|
51
62
|
p = np.exp(p)
|
|
52
63
|
s = np.exp(s)
|
|
64
|
+
if threshold_series is not None:
|
|
65
|
+
threshold_series = np.exp(threshold_series)
|
|
53
66
|
case TransformEnum.inv:
|
|
54
67
|
if model.add_epsilon:
|
|
55
68
|
logger.debug(
|
|
@@ -57,13 +70,19 @@ def _transform(
|
|
|
57
70
|
)
|
|
58
71
|
p = p + EPSILON
|
|
59
72
|
s = s + EPSILON
|
|
73
|
+
if threshold_series is not None:
|
|
74
|
+
threshold_series = threshold_series + EPSILON
|
|
60
75
|
logger.debug("Applying inverse transform")
|
|
61
76
|
p = 1.0 / p
|
|
62
77
|
s = 1.0 / s
|
|
78
|
+
if threshold_series is not None:
|
|
79
|
+
threshold_series = 1.0 / threshold_series
|
|
63
80
|
case TransformEnum.abs:
|
|
64
81
|
logger.debug("Applying absolute value transform")
|
|
65
82
|
p = np.abs(p)
|
|
66
83
|
s = np.abs(s)
|
|
84
|
+
if threshold_series is not None:
|
|
85
|
+
threshold_series = np.abs(threshold_series)
|
|
67
86
|
case _:
|
|
68
87
|
raise ValueError(
|
|
69
88
|
f"Unsupported transform: {model.transform}"
|
|
@@ -71,24 +90,41 @@ def _transform(
|
|
|
71
90
|
else:
|
|
72
91
|
logger.debug("No transform specified, using original values")
|
|
73
92
|
|
|
74
|
-
# Remove invalid values and align series
|
|
75
|
-
if
|
|
76
|
-
|
|
93
|
+
# Remove invalid values and align series if transform applied
|
|
94
|
+
if model.transform is not None:
|
|
95
|
+
logger.debug("Removing invalid values and aligning series")
|
|
96
|
+
if (t is not None) and (threshold_series is not None):
|
|
97
|
+
valid_mask = np.isfinite(p) & np.isfinite(s)
|
|
98
|
+
p = p[valid_mask]
|
|
99
|
+
s = s[valid_mask]
|
|
100
|
+
t = t[valid_mask]
|
|
101
|
+
threshold_series = threshold_series[valid_mask]
|
|
102
|
+
elif t is not None:
|
|
77
103
|
valid_mask = np.isfinite(p) & np.isfinite(s)
|
|
78
104
|
p = p[valid_mask]
|
|
79
105
|
s = s[valid_mask]
|
|
80
106
|
t = t[valid_mask]
|
|
107
|
+
elif threshold_series is not None:
|
|
108
|
+
valid_mask = np.isfinite(p) & np.isfinite(s)
|
|
109
|
+
p = p[valid_mask]
|
|
110
|
+
s = s[valid_mask]
|
|
111
|
+
threshold_series = threshold_series[valid_mask]
|
|
81
112
|
else:
|
|
82
|
-
|
|
83
|
-
|
|
113
|
+
valid_mask = np.isfinite(p) & np.isfinite(s)
|
|
114
|
+
p = p[valid_mask]
|
|
115
|
+
s = s[valid_mask]
|
|
116
|
+
logger.debug(
|
|
117
|
+
f"Removed {len(valid_mask) - np.sum(valid_mask)} invalid entries"
|
|
118
|
+
" from the transformed input series"
|
|
84
119
|
)
|
|
85
|
-
else:
|
|
86
|
-
valid_mask = np.isfinite(p) & np.isfinite(s)
|
|
87
|
-
p = p[valid_mask]
|
|
88
|
-
s = s[valid_mask]
|
|
89
120
|
|
|
90
|
-
|
|
121
|
+
# return results
|
|
122
|
+
if (t is not None) and (threshold_series is not None):
|
|
123
|
+
return p, s, t, threshold_series
|
|
124
|
+
elif t is not None:
|
|
91
125
|
return p, s, t
|
|
126
|
+
elif threshold_series is not None:
|
|
127
|
+
return p, s, threshold_series
|
|
92
128
|
else:
|
|
93
129
|
return p, s
|
|
94
130
|
|
|
@@ -659,3 +695,177 @@ def max_value_timedelta(model: MetricsBasemodel) -> Callable:
|
|
|
659
695
|
return td.total_seconds()
|
|
660
696
|
|
|
661
697
|
return max_value_timedelta_inner
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
# Categorical Metrics
|
|
701
|
+
def _validate_threshold_field(threshold_series: pd.Series) -> float:
|
|
702
|
+
"""Validate threshold input field."""
|
|
703
|
+
unique_thresholds = threshold_series.unique()
|
|
704
|
+
if len(unique_thresholds) != 1:
|
|
705
|
+
raise ValueError(
|
|
706
|
+
"Threshold field must contain a single unique value for each"
|
|
707
|
+
" population grouping."
|
|
708
|
+
)
|
|
709
|
+
threshold = unique_thresholds[0]
|
|
710
|
+
return threshold
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
def confusion_matrix(model: MetricsBasemodel) -> Callable:
|
|
714
|
+
"""Create the confusion_matrix metric function.
|
|
715
|
+
|
|
716
|
+
Returns counts of TP, TN, FP, FN as a dictionary.
|
|
717
|
+
|
|
718
|
+
:math:`TP=\\sum((prim>=threshold_{prim})\\ and\\ (sec>=threshold_{sec}))`
|
|
719
|
+
:math:`TN=\\sum((prim<threshold_{prim})\\ and\\ (sec<threshold_{sec}))`
|
|
720
|
+
:math:`FP=\\sum((prim<threshold_{prim})\\ and\\ (sec>=threshold_{sec}))`
|
|
721
|
+
:math:`FN=\\sum((prim>=threshold_{prim})\\ and\\ (sec<threshold_{sec}))`
|
|
722
|
+
""" # noqa
|
|
723
|
+
logger.debug("Building the confusion_matrix metric function")
|
|
724
|
+
|
|
725
|
+
def confusion_matrix_inner(p: pd.Series,
|
|
726
|
+
s: pd.Series,
|
|
727
|
+
threshold_series: pd.Series) -> dict:
|
|
728
|
+
"""Confusion matrix counts."""
|
|
729
|
+
p, s, threshold_series = _transform(p,
|
|
730
|
+
s,
|
|
731
|
+
model,
|
|
732
|
+
None,
|
|
733
|
+
threshold_series)
|
|
734
|
+
|
|
735
|
+
threshold = _validate_threshold_field(threshold_series)
|
|
736
|
+
|
|
737
|
+
tp = np.sum((p >= threshold) & (s >= threshold))
|
|
738
|
+
tn = np.sum((p < threshold) & (s < threshold))
|
|
739
|
+
fp = np.sum((p < threshold) & (s >= threshold))
|
|
740
|
+
fn = np.sum((p >= threshold) & (s < threshold))
|
|
741
|
+
result = {"TP": tp, "TN": tn, "FP": fp, "FN": fn}
|
|
742
|
+
|
|
743
|
+
return result
|
|
744
|
+
|
|
745
|
+
return confusion_matrix_inner
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
def false_alarm_ratio(model: MetricsBasemodel) -> Callable:
|
|
749
|
+
"""Create the false_alarm_ratio metric function.
|
|
750
|
+
|
|
751
|
+
:math:`FAR=\\frac{FP}{(TP+FP)}`
|
|
752
|
+
""" # noqa
|
|
753
|
+
logger.debug("Building the false_alarm_ratio metric function")
|
|
754
|
+
|
|
755
|
+
def false_alarm_ratio_inner(p: pd.Series,
|
|
756
|
+
s: pd.Series,
|
|
757
|
+
threshold_series: pd.Series) -> float:
|
|
758
|
+
"""Calculate False alarm ratio."""
|
|
759
|
+
p, s, threshold_series = _transform(p,
|
|
760
|
+
s,
|
|
761
|
+
model,
|
|
762
|
+
None,
|
|
763
|
+
threshold_series)
|
|
764
|
+
|
|
765
|
+
threshold = _validate_threshold_field(threshold_series)
|
|
766
|
+
|
|
767
|
+
tp = np.sum((p >= threshold) & (s >= threshold))
|
|
768
|
+
fp = np.sum((p < threshold) & (s >= threshold))
|
|
769
|
+
if (tp + fp) == 0:
|
|
770
|
+
result = np.nan
|
|
771
|
+
else:
|
|
772
|
+
result = fp / (tp + fp)
|
|
773
|
+
|
|
774
|
+
return result
|
|
775
|
+
|
|
776
|
+
return false_alarm_ratio_inner
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
def probability_of_detection(model: MetricsBasemodel) -> Callable:
|
|
780
|
+
"""Create the probability_of_detection metric function.
|
|
781
|
+
|
|
782
|
+
:math:`POD=\\frac{TP}{(TP+FN)}`
|
|
783
|
+
""" # noqa
|
|
784
|
+
logger.debug("Building the probability_of_detection metric function")
|
|
785
|
+
|
|
786
|
+
def probability_of_detection_inner(p: pd.Series,
|
|
787
|
+
s: pd.Series,
|
|
788
|
+
threshold_series: pd.Series) -> float:
|
|
789
|
+
"""Probability of detection."""
|
|
790
|
+
p, s, threshold_series = _transform(p,
|
|
791
|
+
s,
|
|
792
|
+
model,
|
|
793
|
+
None,
|
|
794
|
+
threshold_series)
|
|
795
|
+
|
|
796
|
+
threshold = _validate_threshold_field(threshold_series)
|
|
797
|
+
|
|
798
|
+
tp = np.sum((p >= threshold) & (s >= threshold))
|
|
799
|
+
fn = np.sum((p >= threshold) & (s < threshold))
|
|
800
|
+
if (tp + fn) == 0:
|
|
801
|
+
result = np.nan
|
|
802
|
+
else:
|
|
803
|
+
result = tp / (tp + fn)
|
|
804
|
+
|
|
805
|
+
return result
|
|
806
|
+
|
|
807
|
+
return probability_of_detection_inner
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
def probability_of_false_detection(model: MetricsBasemodel) -> Callable:
|
|
811
|
+
"""Create the probability_of_false_detection metric function.
|
|
812
|
+
|
|
813
|
+
:math:`POFD=\\frac{FP}{(FP+TN)}`
|
|
814
|
+
""" # noqa
|
|
815
|
+
logger.debug("Building the probability_of_false_detection metric function")
|
|
816
|
+
|
|
817
|
+
def probability_of_false_detection_inner(p: pd.Series,
|
|
818
|
+
s: pd.Series,
|
|
819
|
+
threshold_series: pd.Series
|
|
820
|
+
) -> float:
|
|
821
|
+
"""Probability of false detection."""
|
|
822
|
+
p, s, threshold_series = _transform(p,
|
|
823
|
+
s,
|
|
824
|
+
model,
|
|
825
|
+
None,
|
|
826
|
+
threshold_series)
|
|
827
|
+
|
|
828
|
+
threshold = _validate_threshold_field(threshold_series)
|
|
829
|
+
|
|
830
|
+
fp = np.sum((p < threshold) & (s >= threshold))
|
|
831
|
+
tn = np.sum((p < threshold) & (s < threshold))
|
|
832
|
+
if (fp + tn) == 0:
|
|
833
|
+
result = np.nan
|
|
834
|
+
else:
|
|
835
|
+
result = fp / (fp + tn)
|
|
836
|
+
|
|
837
|
+
return result
|
|
838
|
+
|
|
839
|
+
return probability_of_false_detection_inner
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
def critical_success_index(model: MetricsBasemodel) -> Callable:
|
|
843
|
+
"""Create the critical_success_index metric function.
|
|
844
|
+
|
|
845
|
+
:math:`CSI=\\frac{TP}{(TP+FP+FN)}`
|
|
846
|
+
""" # noqa
|
|
847
|
+
logger.debug("Building the critical_success_index metric function")
|
|
848
|
+
|
|
849
|
+
def critical_success_index_inner(p: pd.Series,
|
|
850
|
+
s: pd.Series,
|
|
851
|
+
threshold_series: pd.Series) -> float:
|
|
852
|
+
"""Critical success index."""
|
|
853
|
+
p, s, threshold_series = _transform(p,
|
|
854
|
+
s,
|
|
855
|
+
model,
|
|
856
|
+
None,
|
|
857
|
+
threshold_series)
|
|
858
|
+
|
|
859
|
+
threshold = _validate_threshold_field(threshold_series)
|
|
860
|
+
|
|
861
|
+
tp = np.sum((p >= threshold) & (s >= threshold))
|
|
862
|
+
fp = np.sum((p < threshold) & (s >= threshold))
|
|
863
|
+
fn = np.sum((p >= threshold) & (s < threshold))
|
|
864
|
+
if (tp + fp + fn) == 0:
|
|
865
|
+
result = np.nan
|
|
866
|
+
else:
|
|
867
|
+
result = tp / (tp + fp + fn)
|
|
868
|
+
|
|
869
|
+
return result
|
|
870
|
+
|
|
871
|
+
return critical_success_index_inner
|