Skip to content

Commit 672b027

Browse files
authored
Merge pull request #340 from uriahf/fix/binary-input-alignment
Align binary outcome dictionaries by population key
2 parents 6a48772 + e86479e commit 672b027

2 files changed

Lines changed: 111 additions & 0 deletions

File tree

src/rtichoke/performance_data/performance_data.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,47 @@
4444
]
4545

4646

47+
def _validate_and_align_binary_inputs(
48+
probs: Dict[str, np.ndarray],
49+
reals: Union[np.ndarray, Dict[str, np.ndarray]],
50+
) -> Union[np.ndarray, Dict[str, np.ndarray]]:
51+
"""Validate binary input alignment and normalize outcome dictionaries."""
52+
if not isinstance(probs, dict) or not probs:
53+
raise ValueError("`probs` must be a non-empty dictionary of probability arrays.")
54+
55+
groups = list(probs)
56+
57+
if isinstance(reals, dict):
58+
expected_keys = set(groups)
59+
if set(reals) != expected_keys:
60+
raise ValueError("`reals` dictionary keys must exactly match `probs`.")
61+
62+
for group in groups:
63+
n_probs = len(np.asarray(probs[group]))
64+
n_reals = len(np.asarray(reals[group]))
65+
if n_probs != n_reals:
66+
raise ValueError(
67+
f"Input lengths must match within group {group!r}: "
68+
f"len(probs)={n_probs}, len(reals)={n_reals}."
69+
)
70+
71+
if len(groups) == 1:
72+
return np.asarray(reals[groups[0]])
73+
74+
return {group: np.asarray(reals[group]) for group in groups}
75+
76+
n_reals = len(np.asarray(reals))
77+
for group in groups:
78+
n_probs = len(np.asarray(probs[group]))
79+
if n_probs != n_reals:
80+
raise ValueError(
81+
f"Shared outcome length must match probabilities for group {group!r}: "
82+
f"len(probs)={n_probs}, len(reals)={n_reals}."
83+
)
84+
85+
return reals
86+
87+
4788
def _probs_with_r_binary_cutoff_semantics(
4889
probs: Dict[str, np.ndarray], by: float
4990
) -> Dict[str, np.ndarray]:
@@ -113,6 +154,7 @@ def prepare_binned_classification_data(
113154
any other stratification variables. It forms the basis for subsequent
114155
performance calculations.
115156
"""
157+
reals = _validate_and_align_binary_inputs(probs=probs, reals=reals)
116158
breaks = create_breaks_values(None, "probability_threshold", by)
117159

118160
aj_data_combinations = _create_aj_data_combinations_binary(
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import numpy as np
2+
import pytest
3+
from polars.testing import assert_frame_equal
4+
5+
from rtichoke import prepare_performance_data
6+
7+
8+
def test_binary_reals_dict_is_aligned_by_group_key_not_insertion_order():
9+
probs = {
10+
"group_a": np.array([0.1, 0.8, 0.7, 0.2]),
11+
"group_b": np.array([0.9, 0.2, 0.3, 0.8]),
12+
}
13+
reals_a = np.array([0, 1, 1, 0])
14+
reals_b = np.array([1, 0, 0, 1])
15+
16+
aligned = prepare_performance_data(
17+
probs=probs,
18+
reals={"group_a": reals_a, "group_b": reals_b},
19+
by=0.5,
20+
)
21+
reversed_order = prepare_performance_data(
22+
probs=probs,
23+
reals={"group_b": reals_b, "group_a": reals_a},
24+
by=0.5,
25+
)
26+
27+
assert_frame_equal(aligned, reversed_order)
28+
29+
30+
def test_binary_single_group_reals_dict_matches_array_input():
31+
probs = {"group_a": np.array([0.1, 0.8, 0.7, 0.2])}
32+
reals = np.array([0, 1, 1, 0])
33+
34+
from_array = prepare_performance_data(probs=probs, reals=reals, by=0.5)
35+
from_dict = prepare_performance_data(
36+
probs=probs,
37+
reals={"group_a": reals},
38+
by=0.5,
39+
)
40+
41+
assert_frame_equal(from_array, from_dict)
42+
43+
44+
def test_binary_reals_dict_keys_must_match_probability_groups():
45+
probs = {
46+
"group_a": np.array([0.1, 0.8]),
47+
"group_b": np.array([0.9, 0.2]),
48+
}
49+
50+
with pytest.raises(ValueError, match="keys must exactly match"):
51+
prepare_performance_data(
52+
probs=probs,
53+
reals={
54+
"group_a": np.array([0, 1]),
55+
"wrong_group": np.array([1, 0]),
56+
},
57+
by=0.5,
58+
)
59+
60+
61+
def test_binary_input_lengths_must_match_within_group():
62+
probs = {"group_a": np.array([0.1, 0.8, 0.7])}
63+
64+
with pytest.raises(ValueError, match="Input lengths must match"):
65+
prepare_performance_data(
66+
probs=probs,
67+
reals={"group_a": np.array([0, 1])},
68+
by=0.5,
69+
)

0 commit comments

Comments
 (0)