Repository: staircase-dev/piso (language: python)

Issue:
ENH: "how" parameter for coverage
#### Describe the solution you'd like

values for how are either "fraction" (default) or "sum".   "sum" is equivalent to the coverage fraction * domain length.



Discussion / hints:

Fix patch:
diff --git a/docs/release_notes/index.rst b/docs/release_notes/index.rst
index 93d9943..9cab4e6 100644
--- a/docs/release_notes/index.rst
+++ b/docs/release_notes/index.rst
@@ -5,6 +5,7 @@ Release notes
 ========================
 
 - Added `bins` parameter to :func:`piso.coverage` and :meth:`ArrayAccessor.coverage() <piso.accessor.ArrayAccessor.coverage>`
+- Added `how` parameter to :func:`piso.coverage` and :meth:`ArrayAccessor.coverage() <piso.accessor.ArrayAccessor.coverage>`
 
 ADD UNRELEASED CHANGES ABOVE THIS LINE
 
diff --git a/piso/accessor.py b/piso/accessor.py
index 8eb9bae..d63d2e0 100644
--- a/piso/accessor.py
+++ b/piso/accessor.py
@@ -142,11 +142,12 @@ def issubset(self, *interval_arrays, squeeze=False):
         )
 
     @Appender(docstrings.coverage_docstring, join="\n", indents=1)
-    def coverage(self, domain=None, bins=False):
+    def coverage(self, domain=None, bins=False, how="fraction"):
         return intervalarray.coverage(
             self._interval_array,
             domain,
             bins,
+            how,
         )
 
     @Appender(docstrings.complement_docstring, join="\n", indents=1)
diff --git a/piso/docstrings/accessor.py b/piso/docstrings/accessor.py
index ae10dc3..0fe739e 100644
--- a/piso/docstrings/accessor.py
+++ b/piso/docstrings/accessor.py
@@ -591,7 +591,7 @@ def join_params(list_of_param_strings):
 
 
 coverage_docstring = """
-Calculates the fraction of a domain (or possibly multiple domains) covered by a collection of intervals.
+Calculates the size of a domain (or possibly multiple domains) covered by a collection of intervals.
 
 The intervals are contained in the array object the accessor belongs to.
 The (possibly overlapping) intervals may not, or partially, or wholly cover the domain.
@@ -610,13 +610,15 @@ def join_params(list_of_param_strings):
 bins : boolean, default False
     If False, then the *domain* is interpreted as a single domain and returns one value.
     If True, then *domain* is interpreted as disjoint bins over which coverage is calculated for each.
+how : {"fraction", "sum"}, default "fraction"
+    If *how* = "fraction" then the result is a fraction of the size of the domain.
+    If *how* = "sum" then the result is the length of the domain covered.
 
     .. versionadded:: 0.8.0
 
 Returns
 ----------
 float or :class:`pandas.Series`
-    a number, or Series, with value(s) between 0 and 1, representing the fraction of the domain(s) covered.
 
 Examples
 -----------
@@ -638,6 +640,9 @@ def join_params(list_of_param_strings):
 >>> arr1.piso.coverage(pd.Interval(-10, 10))
 0.3
 
+>>> arr1.piso.coverage(pd.Interval(-10, 10), how="sum")
+6
+
 >>> domain = pd.arrays.IntervalArray.from_tuples(
 ...     [(4,6), (7, 10)],
 ... )
@@ -648,6 +653,11 @@ def join_params(list_of_param_strings):
 (4, 6]     0.500000
 (7, 10]    0.333333
 dtype: float64
+
+>>> arr1.piso.coverage(domain, bins=True, how="sum")
+(4, 6]     1.0
+(7, 10]    1.0
+dtype: float64
 """
 
 complement_docstring = """
diff --git a/piso/docstrings/intervalarray.py b/piso/docstrings/intervalarray.py
index af9e624..28e31aa 100644
--- a/piso/docstrings/intervalarray.py
+++ b/piso/docstrings/intervalarray.py
@@ -615,13 +615,15 @@ def join_params(list_of_param_strings):
 bins : boolean, default False
     If False, then the *domain* is interpreted as a single domain and returns one value.
     If True, then *domain* is interpreted as disjoint bins over which coverage is calculated for each.
+how : {"fraction", "sum"}, default "fraction"
+    If *how* = "fraction" then the result is a fraction of the size of the domain.
+    If *how* = "sum" then the result is the length of the domain covered.
 
     .. versionadded:: 0.8.0
 
 Returns
 ----------
 float or :class:`pandas.Series`
-    a number, or Series, with value(s) between 0 and 1, representing the fraction of the domain(s) covered.
 
 Examples
 -----------
@@ -642,6 +644,9 @@ def join_params(list_of_param_strings):
 >>> piso.coverage(arr1, pd.Interval(-10, 10))
 0.3
 
+>>> piso.coverage(arr1, pd.Interval(-10, 10), how="sum")
+6
+
 >>> domain = pd.arrays.IntervalArray.from_tuples(
 ...     [(4,6), (7, 10)],
 ... )
@@ -652,6 +657,11 @@ def join_params(list_of_param_strings):
 (4, 6]     0.500000
 (7, 10]    0.333333
 dtype: float64
+
+>>> piso.coverage(arr1, domain, bins=True, how="sum")
+(4, 6]     1.0
+(7, 10]    1.0
+dtype: float64
 """
 
 
diff --git a/piso/intervalarray.py b/piso/intervalarray.py
index 1cd0755..a019866 100644
--- a/piso/intervalarray.py
+++ b/piso/intervalarray.py
@@ -178,7 +178,9 @@ def _get_domain_tuple(interval_array, domain):
 
 
 @Appender(docstrings.coverage_docstring, join="\n", indents=1)
-def coverage(interval_array, domain=None, bins=False):
+def coverage(interval_array, domain=None, bins=False, how="fraction"):
+    assert how in ("fraction", "sum")
+
     def _validate_domain():
         if not isinstance(domain, (pd.IntervalIndex, pd.arrays.IntervalArray)):
             raise ValueError(
@@ -192,14 +194,14 @@ def _validate_domain():
     stepfunction = _interval_x_to_stairs(interval_array).make_boolean()
     if bins:
         _validate_domain()
-        result = stepfunction.slice(pd.IntervalIndex(domain)).mean()
+        adjusted_domain = stepfunction.slice(pd.IntervalIndex(domain))
     elif isinstance(domain, (pd.IntervalIndex, pd.arrays.IntervalArray)):
         domain = _interval_x_to_stairs(domain)
-        result = stepfunction.where(domain).mean()
+        adjusted_domain = stepfunction.where(domain)
     else:
         domain = _get_domain_tuple(interval_array, domain)
-        result = stepfunction.clip(*domain).mean()
-    return result
+        adjusted_domain = stepfunction.clip(*domain)
+    return adjusted_domain.mean() if how == "fraction" else adjusted_domain.integral()
 
 
 @Appender(docstrings.complement_docstring, join="\n", indents=1)


Test patch:
diff --git a/tests/test_single_interval_array.py b/tests/test_single_interval_array.py
index 74eefb9..1143d1b 100644
--- a/tests/test_single_interval_array.py
+++ b/tests/test_single_interval_array.py
@@ -39,12 +39,12 @@ def get_package_method(function):
     }[function]
 
 
-def perform_op(*args, how, function, **kwargs):
-    # how = "supplied, accessor, or package"
-    if how == "accessor":
+def perform_op(*args, method, function, **kwargs):
+    # method = "supplied, accessor, or package"
+    if method == "accessor":
         self, *args = args
         return get_accessor_method(self, function)(*args, **kwargs)
-    elif how == "package":
+    elif method == "package":
         return get_package_method(function)(*args, **kwargs)
     else:
         return function(*args, **kwargs)
@@ -119,16 +119,16 @@ def assert_interval_array_equal(interval_array, expected, interval_index):
     ["infer", pd.arrays.IntervalArray, pd.IntervalIndex],
 )
 @pytest.mark.parametrize(
-    "how",
+    "method",
     ["supplied", "accessor", "package"],
 )
-def test_union(closed, interval_index, return_type, how):
+def test_union(closed, interval_index, return_type, method):
     interval_array = make_ia1(interval_index, closed)
     # result = piso_intervalarray.union(interval_array, return_type)
     result = perform_op(
         interval_array,
         return_type=return_type,
-        how=how,
+        method=method,
         function=piso_intervalarray.union,
     )
     expected = pd.arrays.IntervalArray.from_tuples(
@@ -158,17 +158,17 @@ def test_union(closed, interval_index, return_type, how):
     ["infer", pd.arrays.IntervalArray, pd.IntervalIndex],
 )
 @pytest.mark.parametrize(
-    "how",
+    "method",
     ["supplied", "accessor", "package"],
 )
 def test_intersection_overlaps_all_empty_result(
-    closed, interval_index, return_type, how
+    closed, interval_index, return_type, method
 ):
     interval_array = make_ia1(interval_index, closed)
     result = perform_op(
         interval_array,
         return_type=return_type,
-        how=how,
+        method=method,
         function=piso_intervalarray.intersection,
     )
     expected = pd.arrays.IntervalArray([], closed=closed)
@@ -195,17 +195,17 @@ def test_intersection_overlaps_all_empty_result(
     ["infer", pd.arrays.IntervalArray, pd.IntervalIndex],
 )
 @pytest.mark.parametrize(
-    "how",
+    "method",
     ["supplied", "accessor", "package"],
 )
 def test_intersection_overlaps_all_nonempty_result(
-    closed, interval_index, return_type, how
+    closed, interval_index, return_type, method
 ):
     interval_array = make_ia2(interval_index, closed=closed)
     result = perform_op(
         interval_array,
         return_type=return_type,
-        how=how,
+        method=method,
         function=piso_intervalarray.intersection,
     )
     expected = pd.arrays.IntervalArray.from_tuples([(3, 4)], closed=closed)
@@ -232,16 +232,16 @@ def test_intersection_overlaps_all_nonempty_result(
     ["infer", pd.arrays.IntervalArray, pd.IntervalIndex],
 )
 @pytest.mark.parametrize(
-    "how",
+    "method",
     ["supplied", "accessor", "package"],
 )
-def test_intersection_overlaps_2(closed, interval_index, return_type, how):
+def test_intersection_overlaps_2(closed, interval_index, return_type, method):
     interval_array = make_ia1(interval_index, closed)
     result = perform_op(
         interval_array,
         min_overlaps=2,
         return_type=return_type,
-        how=how,
+        method=method,
         function=piso_intervalarray.intersection,
     )
     expected = pd.arrays.IntervalArray.from_tuples([(2, 5)], closed=closed)
@@ -268,16 +268,16 @@ def test_intersection_overlaps_2(closed, interval_index, return_type, how):
     ["infer", pd.arrays.IntervalArray, pd.IntervalIndex],
 )
 @pytest.mark.parametrize(
-    "how",
+    "method",
     ["supplied", "accessor", "package"],
 )
-def test_intersection_overlaps_3(closed, interval_index, return_type, how):
+def test_intersection_overlaps_3(closed, interval_index, return_type, method):
     interval_array = make_ia1(interval_index, closed)
     result = perform_op(
         interval_array,
         min_overlaps=3,
         return_type=return_type,
-        how=how,
+        method=method,
         function=piso_intervalarray.intersection,
     )
     expected = pd.arrays.IntervalArray.from_tuples([(3, 4)], closed=closed)
@@ -304,15 +304,15 @@ def test_intersection_overlaps_3(closed, interval_index, return_type, how):
     ["infer", pd.arrays.IntervalArray, pd.IntervalIndex],
 )
 @pytest.mark.parametrize(
-    "how",
+    "method",
     ["supplied", "accessor", "package"],
 )
-def test_symmetric_difference(closed, interval_index, return_type, how):
+def test_symmetric_difference(closed, interval_index, return_type, method):
     interval_array = make_ia1(interval_index, closed)
     result = perform_op(
         interval_array,
         return_type=return_type,
-        how=how,
+        method=method,
         function=piso_intervalarray.symmetric_difference,
     )
     expected = pd.arrays.IntervalArray.from_tuples(
@@ -342,16 +342,18 @@ def test_symmetric_difference(closed, interval_index, return_type, how):
     ["infer", pd.arrays.IntervalArray, pd.IntervalIndex],
 )
 @pytest.mark.parametrize(
-    "how",
+    "method",
     ["supplied", "accessor", "package"],
 )
-def test_symmetric_difference_min_overlaps_3(closed, interval_index, return_type, how):
+def test_symmetric_difference_min_overlaps_3(
+    closed, interval_index, return_type, method
+):
     interval_array = make_ia1(interval_index, closed)
     result = perform_op(
         interval_array,
         min_overlaps=3,
         return_type=return_type,
-        how=how,
+        method=method,
         function=piso_intervalarray.symmetric_difference,
     )
     expected = pd.arrays.IntervalArray.from_tuples(
@@ -381,18 +383,18 @@ def test_symmetric_difference_min_overlaps_3(closed, interval_index, return_type
     ["infer", pd.arrays.IntervalArray, pd.IntervalIndex],
 )
 @pytest.mark.parametrize(
-    "how",
+    "method",
     ["supplied", "accessor", "package"],
 )
 def test_symmetric_difference_min_overlaps_all_1(
-    closed, interval_index, return_type, how
+    closed, interval_index, return_type, method
 ):
     interval_array = make_ia1(interval_index, closed)
     result = perform_op(
         interval_array,
         min_overlaps="all",
         return_type=return_type,
-        how=how,
+        method=method,
         function=piso_intervalarray.symmetric_difference,
     )
     expected = pd.arrays.IntervalArray.from_tuples(
@@ -422,18 +424,18 @@ def test_symmetric_difference_min_overlaps_all_1(
     ["infer", pd.arrays.IntervalArray, pd.IntervalIndex],
 )
 @pytest.mark.parametrize(
-    "how",
+    "method",
     ["supplied", "accessor", "package"],
 )
 def test_symmetric_difference_min_overlaps_all_2(
-    closed, interval_index, return_type, how
+    closed, interval_index, return_type, method
 ):
     interval_array = make_ia2(interval_index, closed)
     result = perform_op(
         interval_array,
         min_overlaps="all",
         return_type=return_type,
-        how=how,
+        method=method,
         function=piso_intervalarray.symmetric_difference,
     )
     expected = pd.arrays.IntervalArray.from_tuples(
@@ -498,16 +500,18 @@ def make_date(x):
     ["timestamp", "numpy", "datetime", "timedelta", None],
 )
 @pytest.mark.parametrize(
-    "how",
+    "method",
     ["supplied", "accessor", "package"],
 )
 def test_isdisjoint_left_right_neither(
-    interval_index, tuples, expected, closed, date_type, how
+    interval_index, tuples, expected, closed, date_type, method
 ):
 
     interval_array = make_ia_from_tuples(interval_index, tuples, closed)
     interval_array = map_to_dates(interval_array, date_type)
-    result = perform_op(interval_array, how=how, function=piso_intervalarray.isdisjoint)
+    result = perform_op(
+        interval_array, method=method, function=piso_intervalarray.isdisjoint
+    )
     assert result == expected
 
 
@@ -535,15 +539,16 @@ def test_isdisjoint_left_right_neither(
     ["timestamp", "numpy", "datetime", "timedelta", None],
 )
 @pytest.mark.parametrize(
-    "how",
+    "method",
     ["supplied", "accessor", "package"],
 )
-def test_isdisjoint_both(interval_index, tuples, expected, date_type, how):
+def test_isdisjoint_both(interval_index, tuples, expected, date_type, method):
 
     interval_array = make_ia_from_tuples(interval_index, tuples, "both")
     interval_array = map_to_dates(interval_array, date_type)
-    print(interval_array)
-    result = perform_op(interval_array, how=how, function=piso_intervalarray.isdisjoint)
+    result = perform_op(
+        interval_array, method=method, function=piso_intervalarray.isdisjoint
+    )
     assert result == expected
 
 
@@ -552,14 +557,14 @@ def test_isdisjoint_both(interval_index, tuples, expected, date_type, how):
     [True, False],
 )
 @pytest.mark.parametrize(
-    "domain, expected",
+    "domain, expected_fraction, expected_sum",
     [
-        (None, 10 / 12),
-        ((0, 10), 0.8),
-        (pd.Interval(0, 10), 0.8),
-        ((15, 20), 0),
-        (pd.IntervalIndex.from_tuples([(0, 6), (10, 12)]), 1),
-        (pd.IntervalIndex.from_tuples([(6, 7), (9, 10)]), 0),
+        (None, 10 / 12, 10),
+        ((0, 10), 0.8, 8),
+        (pd.Interval(0, 10), 0.8, 8),
+        ((15, 20), 0, 0),
+        (pd.IntervalIndex.from_tuples([(0, 6), (10, 12)]), 1, 8),
+        (pd.IntervalIndex.from_tuples([(6, 7), (9, 10)]), 0, 0),
     ],
 )
 @pytest.mark.parametrize(
@@ -567,19 +572,27 @@ def test_isdisjoint_both(interval_index, tuples, expected, date_type, how):
     ["left", "right"],
 )
 @pytest.mark.parametrize(
-    "how",
+    "method",
     ["supplied", "accessor", "package"],
 )
-def test_coverage(interval_index, domain, expected, closed, how):
+@pytest.mark.parametrize(
+    "how",
+    ["fraction", "sum"],
+)
+def test_coverage(
+    interval_index, domain, expected_fraction, expected_sum, closed, method, how
+):
     if hasattr(domain, "set_closed"):
         domain = domain.set_closed(closed)
     ia = make_ia1(interval_index, closed)
     result = perform_op(
         ia,
-        how=how,
+        method=method,
         function=piso_intervalarray.coverage,
         domain=domain,
+        how=how,
     )
+    expected = expected_fraction if how == "fraction" else expected_sum
     assert result == expected
 
 
@@ -596,10 +609,14 @@ def test_coverage(interval_index, domain, expected, closed, how):
     ["left", "right"],
 )
 @pytest.mark.parametrize(
-    "how",
+    "method",
     ["supplied", "accessor", "package"],
 )
-def test_coverage_bins(interval_index, domain_interval_index, closed, how):
+@pytest.mark.parametrize(
+    "how",
+    ["fraction", "sum"],
+)
+def test_coverage_bins(interval_index, domain_interval_index, closed, method, how):
     domain = pd.arrays.IntervalArray.from_tuples(
         [(0, 2), (3, 7), (8, 10)],
         closed=closed,
@@ -609,12 +626,14 @@ def test_coverage_bins(interval_index, domain_interval_index, closed, how):
     ia = make_ia1(interval_index, closed)
     result = perform_op(
         ia,
-        how=how,
+        method=method,
         function=piso_intervalarray.coverage,
         domain=domain,
         bins=True,
+        how=how,
     )
-    expected = pd.Series([1, 0.75, 0.5], index=domain)
+    values = [1, 0.75, 0.5] if how == "fraction" else [2.0, 3.0, 1.0]
+    expected = pd.Series(values, index=domain)
     pd.testing.assert_series_equal(result, expected)
 
 
@@ -627,14 +646,14 @@ def test_coverage_bins(interval_index, domain_interval_index, closed, how):
     ["left", "right"],
 )
 @pytest.mark.parametrize(
-    "how",
+    "method",
     ["supplied", "accessor", "package"],
 )
-def test_coverage_edge_case(interval_index, closed, how):
+def test_coverage_edge_case(interval_index, closed, method):
     ia = make_ia_from_tuples(interval_index, [], closed)
     result = perform_op(
         ia,
-        how=how,
+        method=method,
         function=piso_intervalarray.coverage,
         domain=None,
     )
@@ -650,16 +669,16 @@ def test_coverage_edge_case(interval_index, closed, how):
     ["left", "right"],
 )
 @pytest.mark.parametrize(
-    "how",
+    "method",
     ["supplied", "accessor", "package"],
 )
-def test_coverage_exception(interval_index, closed, how):
+def test_coverage_exception(interval_index, closed, method):
     domain = (1, 2, 3)
     with pytest.raises(ValueError):
         ia = make_ia1(interval_index, closed)
         perform_op(
             ia,
-            how=how,
+            method=method,
             function=piso_intervalarray.coverage,
             domain=domain,
         )
@@ -674,16 +693,16 @@ def test_coverage_exception(interval_index, closed, how):
     ["left", "right"],
 )
 @pytest.mark.parametrize(
-    "how",
+    "method",
     ["supplied", "accessor", "package"],
 )
-def test_coverage_exception2(interval_index, closed, how):
+def test_coverage_exception2(interval_index, closed, method):
     domain = (1, 2)
     with pytest.raises(ValueError):
         ia = make_ia1(interval_index, closed)
         perform_op(
             ia,
-            how=how,
+            method=method,
             function=piso_intervalarray.coverage,
             domain=domain,
             bins=True,
@@ -699,16 +718,16 @@ def test_coverage_exception2(interval_index, closed, how):
     ["left", "right"],
 )
 @pytest.mark.parametrize(
-    "how",
+    "method",
     ["supplied", "accessor", "package"],
 )
-def test_coverage_exception3(interval_index, closed, how):
+def test_coverage_exception3(interval_index, closed, method):
     domain = pd.IntervalIndex.from_tuples([(1, 3), (2, 4)])
     with pytest.raises(ValueError):
         ia = make_ia1(interval_index, closed)
         perform_op(
             ia,
-            how=how,
+            method=method,
             function=piso_intervalarray.coverage,
             domain=domain,
             bins=True,
@@ -737,17 +756,17 @@ def test_coverage_exception3(interval_index, closed, how):
     ["left", "right"],
 )
 @pytest.mark.parametrize(
-    "how",
+    "method",
     ["supplied", "accessor", "package"],
 )
-def test_complement(interval_index, domain, expected_tuples, closed, how):
+def test_complement(interval_index, domain, expected_tuples, closed, method):
     if hasattr(domain, "set_closed"):
         domain = domain.set_closed(closed)
     ia = make_ia1(interval_index, closed)
     expected = make_ia_from_tuples(False, expected_tuples, closed)
     result = perform_op(
         ia,
-        how=how,
+        method=method,
         function=piso_intervalarray.complement,
         domain=domain,
     )
@@ -796,20 +815,20 @@ def test_complement(interval_index, domain, expected_tuples, closed, how):
     ],
 )
 @pytest.mark.parametrize(
-    "how",
+    "method",
     ["supplied", "accessor", "package"],
 )
 @pytest.mark.parametrize(
     "include_index",
     [True, False],
 )
-def test_contains(interval_index, x, closed, expected, how, include_index):
+def test_contains(interval_index, x, closed, expected, method, include_index):
     ia = make_ia2(interval_index, closed)
     result = perform_op(
         ia,
         x,
         include_index,
-        how=how,
+        method=method,
         function=piso_intervalarray.contains,
     )
     if include_index:
@@ -841,14 +860,14 @@ def test_contains(interval_index, x, closed, expected, how, include_index):
     ["left", "right", "both", "neither"],
 )
 @pytest.mark.parametrize(
-    "how",
+    "method",
     ["supplied", "accessor", "package"],
 )
 @pytest.mark.parametrize(
     "date_type",
     ["timestamp", "numpy", "datetime", "timedelta", None],
 )
-def test_split(interval_index, x, expected_tuples, closed, how, date_type):
+def test_split(interval_index, x, expected_tuples, closed, method, date_type):
     ia = make_ia4(interval_index, closed)
     ia = map_to_dates(ia, date_type)
 
@@ -859,7 +878,7 @@ def test_split(interval_index, x, expected_tuples, closed, how, date_type):
     result = perform_op(
         ia,
         x,
-        how=how,
+        method=method,
         function=piso_intervalarray.split,
     )
     assert_interval_array_equal(