From b93ab6fb5c30c8cbd9990afd646fd435adc1c589 Mon Sep 17 00:00:00 2001 From: rileyh Date: Wed, 11 Dec 2024 14:53:02 -0600 Subject: [PATCH] [#179] Add hypothesis and some property tests for core.model_metrics --- hlink/tests/core/model_metrics_test.py | 46 ++++++++++++++++++++++++++ pyproject.toml | 1 + 2 files changed, 47 insertions(+) diff --git a/hlink/tests/core/model_metrics_test.py b/hlink/tests/core/model_metrics_test.py index c8d046d..7f861a8 100644 --- a/hlink/tests/core/model_metrics_test.py +++ b/hlink/tests/core/model_metrics_test.py @@ -2,9 +2,15 @@ # For copyright and licensing information, see the NOTICE and LICENSE files # in this project's top-level directory, and also on-line at: # https://github.com/ipums/hlink +import math + +from hypothesis import assume, given +import hypothesis.strategies as st from hlink.linking.core.model_metrics import mcc, precision, recall +NonNegativeInt = st.integers(min_value=0) + def test_mcc_example() -> None: tp = 3112 @@ -26,6 +32,26 @@ def test_precision_example() -> None: ), "expected precision to be near 0.9381972" +@given(true_pos=NonNegativeInt, false_pos=NonNegativeInt) +def test_precision_between_0_and_1(true_pos: int, false_pos: int) -> None: + """ + Under "normal circumstances" (there were at least some positive predictions) + precision()'s range is the interval [0.0, 1.0]. + """ + assume(true_pos + false_pos > 0) + precision_score = precision(true_pos, false_pos) + assert 0.0 <= precision_score <= 1.0 + + +def test_precision_no_positive_predictions() -> None: + """ + When there are no positive predictions, true_pos=0 and false_pos=0, and + precision is not well defined. In this case we return NaN. + """ + precision_score = precision(0, 0) + assert math.isnan(precision_score) + + def test_recall_example() -> None: tp = 3112 fn = 1134 @@ -34,3 +60,23 @@ def test_recall_example() -> None: assert ( abs(recall_score - 0.7329251) < 0.0001 ), "expected recall to be near 0.7329251" + + +@given(true_pos=NonNegativeInt, false_neg=NonNegativeInt) +def test_recall_between_0_and_1(true_pos: int, false_neg: int) -> None: + """ + Under "normal circumstances" (there is at least one true positive or false + negative), the range of recall() is the interval [0.0, 1.0]. + """ + assume(true_pos + false_neg > 0) + recall_score = recall(true_pos, false_neg) + assert 0.0 <= recall_score <= 1.0 + + +def test_recall_no_true_pos_or_false_neg() -> None: + """ + When both true_pos and false_neg are 0, recall is not well defined, and we + return NaN. + """ + recall_score = recall(0, 0) + assert math.isnan(recall_score) diff --git a/pyproject.toml b/pyproject.toml index 2a4b001..5c13c39 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ dependencies = [ [project.optional-dependencies] dev = [ "pytest>=7.1.0", + "hypothesis>=6.0", "black>=23.0", "flake8>=5.0", "pre-commit>=2.0",