Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions groupselect/algorithms/algorithm_dream.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,11 @@ def algorithm_dream(
for j in order_cluster + order_diverse:
peopledata_vals_used[i][j] = int(participants[i][j])

if not order_diverse_dict:
raise Exception(
"Error: One diversification field required!",
"You have to set at least one field that is used to "
"diversify people across groups.",
)
# No diversification field is required: with `order_diverse_dict`
# empty, `cats_diverse` is empty throughout the swap machinery below,
# so every table is reported as perfectly balanced and swaps are
# chosen on unique-meeting count alone -- DREAM then behaves as a
# meeting-optimisation-only allocator.

if len(order_cluster_dict) > 1:
raise Exception(
Expand Down
21 changes: 13 additions & 8 deletions groupselect/algorithms/algorithm_hermes.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,10 @@ def algorithm_hermes(
for j in order_cluster + order_diverse:
peopledata_vals_used[i][j] = int(participants[i][j])

if not order_diverse_dict:
raise Exception(
"Error: One diversification field required!",
"You have to set at least one field that is used to "
"diversify people across groups.",
)
# No diversification field is required: with `order_diverse_dict`
# empty, `cats_diverse` is empty throughout the swap machinery below
# (see `pareto_swaps` in this module), so HERMES then behaves as a
# meeting-optimisation-only allocator, same as DREAM in that case.

if len(order_cluster_dict) > 1:
raise Exception(
Expand Down Expand Up @@ -380,12 +378,19 @@ def pareto_swaps(
# Use the highest configured probability among the diversity
# fields the chosen candidate actually pareto-improves (falling
# back to the overall maximum if it improves none, e.g. it was
# accepted purely for its meeting-uniqueness score).
# accepted purely for its meeting-uniqueness score). With no
# diversity fields configured at all, `pareto_probs` is empty and
# no candidate ever pareto-improves a field, so fall back to 0.0
# instead of `max()`-ing an empty sequence -- `select_key` then
# always takes its meeting-uniqueness branch, matching DREAM's
# own degenerate no-diversify-field behaviour.
def pareto_prob_for(k):
relevant = [
pareto_probs[field_id] for field_id in demog_scores.get(k, ())
]
return max(relevant) if relevant else max(pareto_probs.values())
if relevant:
return max(relevant)
return max(pareto_probs.values()) if pareto_probs else 0.0

final_swap = select_key(
final_candidates, final_meetings, pareto_prob_for, random
Expand Down
13 changes: 12 additions & 1 deletion groupselect/allocate_pandas.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import warnings
from typing import Callable, Iterable, Hashable

import numpy as np

from groupselect import FieldMode, Algorithm, allocate_numpy, AllocatorResult


Expand Down Expand Up @@ -57,7 +59,16 @@ def allocate_pandas(
participants_codes = participants[list(fields.keys())].apply(
lambda col: col.cat.codes
)
participants_numpy = participants_codes.to_numpy()
if fields:
participants_numpy = participants_codes.to_numpy()
else:
# `.apply()` on a zero-column selection returns an empty
# DataFrame with no columns to infer a dtype from, so pandas
# defaults it to float64 -- `to_numpy()` would then fail
# `allocate_numpy`'s integer-dtype check below. Build the
# (n_participants, 0) array directly instead, with an explicit
# integer dtype.
participants_numpy = np.empty((len(participants_codes), 0), dtype=int)
fields_numpy = {
participants_codes.columns.tolist().index(k): v
for k, v in fields.items()
Expand Down
63 changes: 63 additions & 0 deletions tests/test_pandas.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,66 @@ def test_philipps_example_data():
"display.max_rows", None, "display.max_columns", None
):
print(res)


@pytest.mark.parametrize("algorithm", ["DREAM", "HERMES"])
def test_dream_hermes_without_diversify_field(algorithm):
"""DREAM/HERMES must allocate even with no diversify field defined.

Unlike LEGACY (see ``test_legacy_requires_diversify_field`` below),
DREAM and HERMES do not need a diversify field: their swap machinery
degrades to optimising only for unique meetings when no diversity
fields are configured -- see `algorithm_dream.py`/`algorithm_hermes.py`
for how this is handled.
"""
df = example_data_pd["default"]
settings = {"pareto_prob": 0.4} if algorithm == "DREAM" else {}
res = df.groupselect.allocate(
fields={"photo consent": "cluster"},
n_part_per_group=6,
algorithm=algorithm,
settings=settings,
)
assert len(res) == len(df)


def test_legacy_requires_diversify_field():
"""LEGACY, unlike DREAM/HERMES, still requires a diversify field."""
df = example_data_pd["default"]
with pytest.raises(Exception, match="diversification field required"):
df.groupselect.allocate(
fields={"photo consent": "cluster"},
n_part_per_group=3 * [6],
algorithm="Legacy",
)


@pytest.mark.parametrize("algorithm", ["DREAM", "HERMES"])
def test_dream_hermes_with_no_fields_at_all(algorithm):
"""DREAM/HERMES must allocate with an entirely empty `fields` dict.

`participants[[]].apply(...)` returns a zero-column DataFrame that
pandas defaults to dtype float64 (there are no columns to infer a
dtype from), which used to fail `allocate_numpy`'s integer-dtype
check -- see `allocate_pandas()`'s explicit `fields`-empty branch.
"""
df = example_data_pd["default"]
settings = {"pareto_prob": 0.4} if algorithm == "DREAM" else {}
res = df.groupselect.allocate(
fields={},
n_part_per_group=6,
algorithm=algorithm,
settings=settings,
)
assert len(res) == len(df)


def test_legacy_requires_diversify_field_with_no_fields_at_all():
"""LEGACY must still reject an entirely empty `fields` dict too."""
df = example_data_pd["default"]
with pytest.raises(Exception, match="diversification field required"):
df.groupselect.allocate(
fields={},
n_part_per_group=6,
algorithm="Legacy",
)
Loading