Skip to content
Open
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
18 changes: 17 additions & 1 deletion src/evidently/core/metric_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -564,8 +564,24 @@ def convert_types(val):
return int(val)
if isinstance(val, str):
return val
if val is None or np.isnan(val):
# `pd.NA` / `pd.NaT` reach here when label values come from nullable pandas
# dtypes (e.g. "string", "Int64"). `np.isnan(pd.NA)` raises TypeError
# ("boolean value of NA is ambiguous"); `pd.isna` safely covers all
# NA flavors (None, NaN, NaT, NA). We return None — these can't survive
# downstream as dict keys for ByLabelCountValue anyway, and the existing
# numpy-NaN call site already relied on returning a NA-shaped value here.
if val is None:
return val
try:
if pd.isna(val):
# Preserve numpy.nan as-is (back-compat for the existing
# ByLabelCountValue serializer that turns it into the string
# "nan"); collapse pandas-flavored NA to None.
if isinstance(val, float):
return val
return None
except (TypeError, ValueError):
pass
raise ValueError(f"type {type(val)} not supported as Label")


Expand Down
45 changes: 45 additions & 0 deletions tests/future/test_metric_types.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import numpy as np
import pandas as pd
import pytest

from evidently.core.metric_types import ByLabelCountValue
from evidently.core.metric_types import SingleValue
from evidently.core.metric_types import convert_types


@pytest.mark.parametrize(
Expand All @@ -20,3 +22,46 @@ def test_by_label_count_value(input: dict, output: tuple):
tests=[],
)
assert {"counts": output[0], "shares": output[1]} == value.to_simple_dict()


@pytest.mark.parametrize(
"value",
[
pd.NA,
pd.NaT,
],
)
def test_convert_types_handles_pandas_na(value):
# Regression for #1844: convert_types previously called np.isnan(val) which
# raises TypeError on pd.NA ("boolean value of NA is ambiguous") and on
# pd.NaT. Pandas-flavored NA should now collapse to None instead of
# propagating a TypeError up through ByLabelCountValue construction.
assert convert_types(value) is None


def test_convert_types_preserves_existing_contracts():
# Non-NA labels must still flow through untouched, and numpy.nan must
# round-trip as a float so the ByLabelCountValue serializer keeps emitting
# the string "nan" (see test_by_label_count_value above).
assert convert_types(True) is True
assert convert_types(0) == 0
assert convert_types(42) == 42
assert convert_types("class_a") == "class_a"
assert convert_types(None) is None
nan_out = convert_types(np.nan)
assert isinstance(nan_out, float) and np.isnan(nan_out)


def test_by_label_count_value_handles_pd_na_key():
# End-to-end: ByLabelCountValue construction must not crash when a count
# dict keyed by pd.NA reaches the convert_types pipeline.
value = ByLabelCountValue(
counts={pd.NA: SingleValue(value=1.0, display_name="t", metric_value_location=None)},
shares={pd.NA: SingleValue(value=1.0, display_name="t", metric_value_location=None)},
display_name="t",
metric_value_location=None,
tests=[],
)
# No assertion on key shape — just that construction + dict export succeed.
out = value.to_simple_dict()
assert "counts" in out and "shares" in out