diff --git a/metcalcpy/util/ctc_statistics.py b/metcalcpy/util/ctc_statistics.py index 5d2fb317..13b005b9 100644 --- a/metcalcpy/util/ctc_statistics.py +++ b/metcalcpy/util/ctc_statistics.py @@ -114,7 +114,7 @@ def calculate_fbias(input_data, columns_names, logger=None): result = oyn / oy result = round_half_up(result, PRECISION) safe_log(logger, "info", f"ACC calculation successful: {result}") - except (TypeError, ZeroDivisionError, Warning, ValueError): + except (TypeError, ZeroDivisionError, Warning, ValueError) as e: safe_log(logger, "error", f"Error in ACC calculation: {e}") result = None warnings.filterwarnings('ignore') @@ -150,7 +150,7 @@ def calculate_fmean(input_data, columns_names, logger=None): result = oyn / total result = round_half_up(result, PRECISION) safe_log(logger, "info", f"FMEAN calculation successful: {result}") - except (TypeError, ZeroDivisionError, Warning, ValueError): + except (TypeError, ZeroDivisionError, Warning, ValueError) as e: safe_log(logger, "error", f"Error in FMEAN calculation: {e}") result = None warnings.filterwarnings('ignore') @@ -317,7 +317,7 @@ def calculate_podn(input_data, columns_names, logger=None): return result -def calculate_far(input_data, column_names, logger=None): +def calculate_far(input_data, columns_names, logger=None): """Performs calculation of FAR - false alarms Args: @@ -941,11 +941,15 @@ def calculate_ctc_on(input_data, columns_names, logger=None): calculated ON as float or None if some data values are missing or invalid """ - safe_log(logger, "debug", "Starting calculation of CTC ON.") - fy_on = sum_column_data_by_name(input_data, columns_names, 'fy_on') - fn_on = sum_column_data_by_name(input_data, columns_names, 'fn_on') - return round_half_up(fy_on + fn_on, PRECISION) - + try: + safe_log(logger, "debug", "Starting calculation of CTC ON.") + fy_on = sum_column_data_by_name(input_data, columns_names, 'fy_on') + fn_on = sum_column_data_by_name(input_data, columns_names, 'fn_on') + result = round_half_up(fy_on + fn_on, PRECISION) + except (TypeError, ZeroDivisionError, Warning, ValueError) as e: + result = None + safe_log(logger, "error", f"Error in calculating CTC ON: {str(e)}") + return result def calculate_ctc_fy(input_data, columns_names, logger=None): """Calculates the Total Number of forecast yes and observation no plus diff --git a/test/test_ctc_statistics.py b/test/test_ctc_statistics.py index f888f4d9..1328d5e6 100644 --- a/test/test_ctc_statistics.py +++ b/test/test_ctc_statistics.py @@ -1,7 +1,10 @@ import pytest import pandas as pd +import numpy as np import sys import os +from unittest.mock import patch +from functools import cache sys.path.append("../../") from metcalcpy.util import ctc_statistics as ctc @@ -139,6 +142,63 @@ def test_CTC_ROC_thresh(): # if we get here, then all elements matched in value and position assert True +@cache +def ctc_data(): + df = pd.read_csv(f"{cwd}/data/ROC_CTC.data", sep='\t', header='infer') + return df.to_numpy(), np.array(df.columns) + + +# TODO: Some of the functions here that +# are evaluating to None could be better +# tested with a different dataset. e.g. acc +# needs a 'total' column in the test data. +@pytest.mark.parametrize( + "func_name,expected", + [ + ("fbias", 0.8549794), + ("acc", None), + ("fmean", None), + ("pody", 0.5603757), + ("pofd", 0.0368738), + ("podn", 0.9631262), + ("far", 0.3445741), + ("csi", 0.432855), + ("gss", None), + ("hk", 0.5235019), + ("hss", None), + ("odds", 33.2937647), + ("lodds", 3.5053691), + ("bagss", None), + ("eclv", None), + ("ctc_total", None), + ("cts_total", None), + ("ctc_fn_on", 44984491.0), + ("ctc_fn_oy", 2570049.0), + ("ctc_fy_on", 1722257.0), + ("ctc_fy_oy", 3275963.0), + ("ctc_oy", 5846012.0), + ("ctc_on", 46706748.0), + ("ctc_fy", 46706748.0), + ("ctc_fn", 47554540.0), + ("odds1", 33.2937647), + ("orss", 0.9416803), + ("sedi", 0.7397156), + ("seds", None), + ("edi", 0.7014241), + ("eds", None), + ] +) +def test_ctc_statistics(func_name, expected): + func_str = f"calculate_{func_name}" + func = getattr(ctc, func_str) + actual = func(*ctc_data()) + assert actual == expected + + # Check None is returned on Exception + with patch.object(ctc, "sum_column_data_by_name", side_effect=TypeError): + actual = func(*ctc_data()) + assert actual == None + if __name__ == "__main__": diff --git a/test/test_mode_2d_arearat_statistics.py b/test/test_mode_2d_arearat_statistics.py new file mode 100644 index 00000000..5a103de4 --- /dev/null +++ b/test/test_mode_2d_arearat_statistics.py @@ -0,0 +1,70 @@ +import pytest +from unittest.mock import patch +import numpy as np +import metcalcpy.util.mode_2d_arearat_statistics as m2as + +column_names = np.array( + ["object_type", "area", "fcst_flag", "simple_flag", "matched_flag"] +) + +data = np.array( + [ + ["2d", 100, 1, 1, 1], + ["2d", 30, 0, 1, 1], + ["2d", 120, 1, 1, 0], + ["2d", 12, 0, 1, 1], + ["2d", 1, 1, 0, 1], + ["2d", 17, 0, 1, 1], + ["2d", 200, 1, 1, 1], + ["3d", 66, 0, 1, 0], + ] +) + +@pytest.mark.parametrize( + "func_name,data_values,expected", + [ + ("arearat_fsa_asa", data, 0.8768267), + ("arearat_osa_asa", data, 0.1231733), + ("arearat_asm_asa", data, 0.7494781), + ("arearat_asu_asa", data, 0.2505219), + ("arearat_fsm_fsa", data, 0.7142857), + ("arearat_fsu_fsa", data, 0.2857143), + ("arearat_osm_osa", data, 1.0), + ("arearat_osu_osa", data, None), + ("arearat_fsm_asm", data, 0.2857143), + ("arearat_osm_asm", data, 0.1643454), + ("arearat_osu_asu", data, None), + ("arearat_fsa_aaa", data, 0.875), + ("arearat_osa_aaa", data, 0.1229167), + ("arearat_fsa_faa", data, 0.9976247), + ("arearat_fca_faa", data, 0.0023753), + ("arearat_osa_oaa", data, 1.0), + ("arearat_oca_oaa", data, None), + ("arearat_fca_aca", data, 1.0), + ("arearat_oca_aca", data, None), + ("arearat_fsa_osa", data, 7.1186441), + ("arearat_osa_fsa", data, 1.0), + ("arearat_aca_asa", data, 0.0020877), + ("arearat_asa_aca", data, 479.0), + ("arearat_fca_fsa", data, 0.002381), + ("arearat_fsa_fca", data, 420.0), + ("arearat_oca_osa", data, None), + ("arearat_osa_oca", data, None), + ("objahits", data, 179.5), + ("objamisses", data, None), + ("objafas", data, 120.0), + ("objacsi", data, 0.5993322), + ("objapody", data, None), + ("objafar", data, 0.1431981), + ], +) +def test_calculate_3d_ratio(func_name, data_values, expected): + func_str = f"calculate_2d_{func_name}" + func = getattr(m2as, func_str) + actual = func(data_values, column_names) + assert actual == expected + + # Check None is returned on Exception + with patch.object(m2as, "column_data_by_name_value", side_effect=TypeError): + actual = func(data_values, column_names) + assert actual == None diff --git a/test/test_mode_3d_ratio_statistics.py b/test/test_mode_3d_ratio_statistics.py new file mode 100644 index 00000000..da3b69e3 --- /dev/null +++ b/test/test_mode_3d_ratio_statistics.py @@ -0,0 +1,69 @@ +import pytest +from unittest.mock import patch +import numpy as np +import metcalcpy.util.mode_3d_ratio_statistics as m3rs + +column_names = np.array( + ["object_type", "volume", "fcst_flag", "simple_flag", "matched_flag"] +) + +data = np.array( + [ + ["3d", 100, 1, 1, 1], + ["3d", 30, 0, 1, 1], + ["3d", 120, 1, 1, 0], + ["3d", 12, 0, 1, 1], + ["3d", 1, 1, 0, 1], + ["3d", 17, 0, 1, 1], + ["2d", 200, 1, 1, 1], + ["3d", 66, 0, 1, 0], + ] +) + +@pytest.mark.parametrize( + "func_name,data_values,expected", + [ + ("ratio_fsa_asa", data, 0.3333333), + ("ratio_osa_asa", data, 0.6666667), + ("ratio_asm_asa", data, 0.6666667), + ("ratio_asu_asa", data, 0.3333333), + ("ratio_fsm_fsa", data, 1.0), + ("ratio_fsu_fsa", data, 0.5), + ("ratio_osm_osa", data, 0.75), + ("ratio_osu_osa", data, 0.25), + ("ratio_fsm_asm", data, 0.25), + ("ratio_osm_asm", data, 0.25), + ("ratio_fsu_asu", data, 0.5), + ("ratio_osu_asu", data, 0.5), + ("ratio_fsa_aaa", data, 0.2857143), + ("ratio_osa_aaa", data, 0.5714286), + ("ratio_fsa_faa", data, 0.6666667), + ("ratio_fca_faa", data, 0.3333333), + ("ratio_osa_oaa", data, 1.0), + ("ratio_oca_oaa", data, 0.0), + ("ratio_fca_aca", data, 1.0), + ("ratio_fsa_osa", data, 0.5), + ("ratio_osa_fsa", data, 2.0), + ("ratio_asa_aca", data, 6.0), + ("ratio_fca_fsa", data, 0.5), + ("ratio_fsa_fca", data, 2.0), + ("ratio_oca_osa", data, 0.0), + ("ratio_osa_oca", data, None), + ("objhits", data, 2.0), + ("objmisses", data, 1.0), + ("objfas", data, 1.0), + ("objcsi", data, 0.5), + ("objpody", data, 0.6666667), + ("objfar", data, 0.3333333), + ], +) +def test_calculate_3d_ratio(func_name, data_values, expected): + func_str = f"calculate_3d_{func_name}" + func = getattr(m3rs, func_str) + actual = func(data_values, column_names) + assert actual == expected + + # Check None is returned on Exception + with patch.object(m3rs, "column_data_by_name_value", side_effect=TypeError): + actual = func(data_values, column_names) + assert actual == None