dataeval-plots 0.0.15__tar.gz → 0.0.17__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dataeval_plots-0.0.15 → dataeval_plots-0.0.17}/PKG-INFO +1 -1
- {dataeval_plots-0.0.15 → dataeval_plots-0.0.17}/src/dataeval_plots/__init__.py +10 -0
- {dataeval_plots-0.0.15 → dataeval_plots-0.0.17}/src/dataeval_plots/_version.py +2 -2
- {dataeval_plots-0.0.15 → dataeval_plots-0.0.17}/src/dataeval_plots/backends/_altair.py +78 -0
- {dataeval_plots-0.0.15 → dataeval_plots-0.0.17}/src/dataeval_plots/backends/_base.py +20 -0
- {dataeval_plots-0.0.15 → dataeval_plots-0.0.17}/src/dataeval_plots/backends/_matplotlib.py +32 -0
- {dataeval_plots-0.0.15 → dataeval_plots-0.0.17}/src/dataeval_plots/backends/_plotly.py +109 -0
- {dataeval_plots-0.0.15 → dataeval_plots-0.0.17}/src/dataeval_plots/backends/_seaborn.py +44 -0
- {dataeval_plots-0.0.15 → dataeval_plots-0.0.17}/src/dataeval_plots/backends/_shared.py +291 -4
- {dataeval_plots-0.0.15 → dataeval_plots-0.0.17}/src/dataeval_plots/protocols.py +82 -5
- {dataeval_plots-0.0.15 → dataeval_plots-0.0.17}/.gitignore +0 -0
- {dataeval_plots-0.0.15 → dataeval_plots-0.0.17}/README.md +0 -0
- {dataeval_plots-0.0.15 → dataeval_plots-0.0.17}/pyproject.toml +0 -0
- {dataeval_plots-0.0.15 → dataeval_plots-0.0.17}/src/dataeval_plots/_registry.py +0 -0
- {dataeval_plots-0.0.15 → dataeval_plots-0.0.17}/src/dataeval_plots/backends/__init__.py +0 -0
- {dataeval_plots-0.0.15 → dataeval_plots-0.0.17}/src/dataeval_plots/backends/_feasibility_layout.py +0 -0
- {dataeval_plots-0.0.15 → dataeval_plots-0.0.17}/src/dataeval_plots/py.typed +0 -0
|
@@ -19,6 +19,7 @@ from dataeval_plots.protocols import (
|
|
|
19
19
|
Dataset,
|
|
20
20
|
PlottableBalance,
|
|
21
21
|
PlottableDiversity,
|
|
22
|
+
PlottableDrift,
|
|
22
23
|
PlottableDriftMVDC,
|
|
23
24
|
PlottableFeasibility,
|
|
24
25
|
PlottableStats,
|
|
@@ -98,6 +99,15 @@ def plot(
|
|
|
98
99
|
) -> Any: ...
|
|
99
100
|
|
|
100
101
|
|
|
102
|
+
@overload
|
|
103
|
+
def plot(
|
|
104
|
+
output: PlottableDrift,
|
|
105
|
+
/,
|
|
106
|
+
figsize: tuple[float, float] | None = None,
|
|
107
|
+
backend: str | None = None,
|
|
108
|
+
) -> Any: ...
|
|
109
|
+
|
|
110
|
+
|
|
101
111
|
@overload
|
|
102
112
|
def plot(
|
|
103
113
|
output: PlottableFeasibility,
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.0.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 0,
|
|
21
|
+
__version__ = version = '0.0.17'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 0, 17)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -29,6 +29,7 @@ from dataeval_plots.backends._shared import (
|
|
|
29
29
|
normalize_reference_outputs,
|
|
30
30
|
parse_dataset_item,
|
|
31
31
|
prepare_balance_data,
|
|
32
|
+
prepare_chunked_drift,
|
|
32
33
|
prepare_diversity_data,
|
|
33
34
|
prepare_drift_data,
|
|
34
35
|
prepare_feasibility_data,
|
|
@@ -39,6 +40,7 @@ from dataeval_plots.protocols import (
|
|
|
39
40
|
Dataset,
|
|
40
41
|
PlottableBalance,
|
|
41
42
|
PlottableDiversity,
|
|
43
|
+
PlottableDrift,
|
|
42
44
|
PlottableDriftMVDC,
|
|
43
45
|
PlottableFeasibility,
|
|
44
46
|
PlottableStats,
|
|
@@ -819,6 +821,82 @@ class AltairBackend(BasePlottingBackend):
|
|
|
819
821
|
width=width, height=height, title="Domain Classifier, Drift Detection"
|
|
820
822
|
)
|
|
821
823
|
|
|
824
|
+
def _plot_drift(
|
|
825
|
+
self,
|
|
826
|
+
output: PlottableDrift,
|
|
827
|
+
figsize: tuple[float, float] | None = None,
|
|
828
|
+
) -> Any: # alt.LayerChart
|
|
829
|
+
"""
|
|
830
|
+
Render the metric of each chunk in relation to the thresholds of the detector.
|
|
831
|
+
|
|
832
|
+
When the output carries the metric of the reference chunks, they are drawn in gray to the left of the
|
|
833
|
+
analysis chunks. The chunks are numbered from -n for the reference and from 0 for the analysis.
|
|
834
|
+
|
|
835
|
+
Parameters
|
|
836
|
+
----------
|
|
837
|
+
output : PlottableDrift
|
|
838
|
+
The chunked drift output object to plot
|
|
839
|
+
figsize : tuple[float, float] or None, default None
|
|
840
|
+
Figure size in inches (width, height), drawn at 100 pixels per inch. If None, defaults to (6, 3).
|
|
841
|
+
|
|
842
|
+
Returns
|
|
843
|
+
-------
|
|
844
|
+
alt.LayerChart
|
|
845
|
+
"""
|
|
846
|
+
import altair as alt
|
|
847
|
+
import pandas as pd
|
|
848
|
+
|
|
849
|
+
data = prepare_chunked_drift(output)
|
|
850
|
+
width_inches, height_inches = figsize or (6, 3)
|
|
851
|
+
|
|
852
|
+
# Convert via dict to avoid pyarrow dependency
|
|
853
|
+
plot_df = pd.DataFrame(
|
|
854
|
+
data.frame.with_columns(
|
|
855
|
+
pl.when(pl.col("period") == "reference")
|
|
856
|
+
.then(pl.lit("Reference chunks"))
|
|
857
|
+
.otherwise(pl.lit("Analysis chunks"))
|
|
858
|
+
.alias("period")
|
|
859
|
+
).to_dict(as_series=False)
|
|
860
|
+
)
|
|
861
|
+
|
|
862
|
+
x = alt.X("index:Q", title=data.chunk_label, axis=alt.Axis(tickMinStep=1))
|
|
863
|
+
scale = alt.Scale(zero=False)
|
|
864
|
+
y = alt.Y("value:Q", title=data.metric_name, scale=scale)
|
|
865
|
+
tooltip = ["index:Q", alt.Tooltip("value:Q", title=data.metric_name, format=".4f")]
|
|
866
|
+
|
|
867
|
+
# The no-drift range is the band between the two thresholds
|
|
868
|
+
layers = [
|
|
869
|
+
alt.Chart(plot_df)
|
|
870
|
+
.mark_area(color="green", opacity=0.15, interpolate="step")
|
|
871
|
+
.encode(x=x, y=alt.Y("lower_threshold:Q", title=data.metric_name, scale=scale), y2="upper_threshold:Q")
|
|
872
|
+
]
|
|
873
|
+
|
|
874
|
+
# The metric of each chunk, with the reference chunks in gray
|
|
875
|
+
has_reference = bool((plot_df["period"] == "Reference chunks").any())
|
|
876
|
+
color = alt.Color(
|
|
877
|
+
"period:N",
|
|
878
|
+
scale=alt.Scale(domain=["Reference chunks", "Analysis chunks"], range=["gray", "royalblue"]),
|
|
879
|
+
legend=alt.Legend(title=None),
|
|
880
|
+
)
|
|
881
|
+
layers.append(alt.Chart(plot_df).mark_line(point=True).encode(x=x, y=y, color=color, tooltip=tooltip))
|
|
882
|
+
if has_reference:
|
|
883
|
+
layers.append(
|
|
884
|
+
alt.Chart(pd.DataFrame({"index": [-0.5]}))
|
|
885
|
+
.mark_rule(color="gray", strokeDash=[2, 2])
|
|
886
|
+
.encode(x="index:Q")
|
|
887
|
+
)
|
|
888
|
+
|
|
889
|
+
# The chunks that the detector flagged
|
|
890
|
+
layers.append(
|
|
891
|
+
alt.Chart(plot_df[plot_df["drifted"]]) # type: ignore[arg-type]
|
|
892
|
+
.mark_point(shape="diamond", size=120, color="crimson", filled=True)
|
|
893
|
+
.encode(x=x, y=y, tooltip=tooltip)
|
|
894
|
+
)
|
|
895
|
+
|
|
896
|
+
return alt.layer(*layers).properties(
|
|
897
|
+
width=int(width_inches * 100), height=int(height_inches * 100), title="Drift by chunk"
|
|
898
|
+
)
|
|
899
|
+
|
|
822
900
|
def _plot_image_grid(
|
|
823
901
|
self,
|
|
824
902
|
dataset: Dataset,
|
|
@@ -14,6 +14,7 @@ from dataeval_plots.protocols import (
|
|
|
14
14
|
Dataset,
|
|
15
15
|
PlottableBalance,
|
|
16
16
|
PlottableDiversity,
|
|
17
|
+
PlottableDrift,
|
|
17
18
|
PlottableDriftMVDC,
|
|
18
19
|
PlottableFeasibility,
|
|
19
20
|
PlottableStats,
|
|
@@ -81,6 +82,14 @@ class PlottingBackend(Protocol):
|
|
|
81
82
|
figsize: tuple[float, float] | None = None,
|
|
82
83
|
) -> Any: ...
|
|
83
84
|
|
|
85
|
+
@overload
|
|
86
|
+
def plot(
|
|
87
|
+
self,
|
|
88
|
+
output: PlottableDrift,
|
|
89
|
+
*,
|
|
90
|
+
figsize: tuple[float, float] | None = None,
|
|
91
|
+
) -> Any: ...
|
|
92
|
+
|
|
84
93
|
@overload
|
|
85
94
|
def plot(
|
|
86
95
|
self,
|
|
@@ -245,6 +254,8 @@ class BasePlottingBackend(PlottingBackend, ABC):
|
|
|
245
254
|
return self._plot_sufficiency(cast(PlottableSufficiency, output), figsize=figsize, **kwargs)
|
|
246
255
|
if plot_type == "drift_mvdc":
|
|
247
256
|
return self._plot_drift_mvdc(cast(PlottableDriftMVDC, output), figsize=figsize, **kwargs)
|
|
257
|
+
if plot_type == "drift":
|
|
258
|
+
return self._plot_drift(cast(PlottableDrift, output), figsize=figsize, **kwargs)
|
|
248
259
|
if plot_type == "stats":
|
|
249
260
|
return self._plot_stats(cast(PlottableStats, output), figsize=figsize, **kwargs)
|
|
250
261
|
if plot_type == "feasibility":
|
|
@@ -310,6 +321,15 @@ class BasePlottingBackend(PlottingBackend, ABC):
|
|
|
310
321
|
"""Plot drift MVDC output."""
|
|
311
322
|
...
|
|
312
323
|
|
|
324
|
+
@abstractmethod
|
|
325
|
+
def _plot_drift(
|
|
326
|
+
self,
|
|
327
|
+
output: PlottableDrift,
|
|
328
|
+
figsize: tuple[float, float] | None = None,
|
|
329
|
+
) -> Any:
|
|
330
|
+
"""Plot the per-chunk results of a chunked drift detector."""
|
|
331
|
+
...
|
|
332
|
+
|
|
313
333
|
@abstractmethod
|
|
314
334
|
def _plot_feasibility(
|
|
315
335
|
self,
|
|
@@ -21,8 +21,10 @@ from dataeval_plots.backends._shared import (
|
|
|
21
21
|
calculate_subplot_grid,
|
|
22
22
|
feasibility_zones,
|
|
23
23
|
normalize_reference_outputs,
|
|
24
|
+
plot_chunked_drift_on_axis,
|
|
24
25
|
plot_drift_on_axis,
|
|
25
26
|
prepare_balance_data,
|
|
27
|
+
prepare_chunked_drift,
|
|
26
28
|
prepare_diversity_data,
|
|
27
29
|
prepare_drift_data,
|
|
28
30
|
prepare_feasibility_data,
|
|
@@ -32,6 +34,7 @@ from dataeval_plots.backends._shared import (
|
|
|
32
34
|
from dataeval_plots.protocols import (
|
|
33
35
|
PlottableBalance,
|
|
34
36
|
PlottableDiversity,
|
|
37
|
+
PlottableDrift,
|
|
35
38
|
PlottableDriftMVDC,
|
|
36
39
|
PlottableFeasibility,
|
|
37
40
|
PlottableStats,
|
|
@@ -833,6 +836,35 @@ class MatplotlibBackend(BasePlottingBackend):
|
|
|
833
836
|
)
|
|
834
837
|
return fig
|
|
835
838
|
|
|
839
|
+
def _plot_drift(
|
|
840
|
+
self,
|
|
841
|
+
output: PlottableDrift,
|
|
842
|
+
figsize: tuple[float, float] | None = None,
|
|
843
|
+
) -> Figure:
|
|
844
|
+
"""
|
|
845
|
+
Render the metric of each chunk in relation to the thresholds of the detector.
|
|
846
|
+
|
|
847
|
+
Parameters
|
|
848
|
+
----------
|
|
849
|
+
output : PlottableDrift
|
|
850
|
+
The chunked drift output object to plot
|
|
851
|
+
figsize : tuple[float, float] or None, default None
|
|
852
|
+
Figure size in inches (width, height). If None, defaults to (8, 3.5).
|
|
853
|
+
|
|
854
|
+
Returns
|
|
855
|
+
-------
|
|
856
|
+
matplotlib.figure.Figure
|
|
857
|
+
"""
|
|
858
|
+
import matplotlib.pyplot as plt
|
|
859
|
+
|
|
860
|
+
data = prepare_chunked_drift(output)
|
|
861
|
+
|
|
862
|
+
fig, ax = plt.subplots(figsize=figsize or (8, 3.5))
|
|
863
|
+
plot_chunked_drift_on_axis(ax, data)
|
|
864
|
+
ax.set_title("Drift by chunk")
|
|
865
|
+
fig.tight_layout()
|
|
866
|
+
return fig
|
|
867
|
+
|
|
836
868
|
def _plot_feasibility(
|
|
837
869
|
self,
|
|
838
870
|
output: PlottableFeasibility,
|
|
@@ -22,6 +22,7 @@ from dataeval_plots.backends._shared import (
|
|
|
22
22
|
feasibility_zones,
|
|
23
23
|
format_label_from_target,
|
|
24
24
|
prepare_balance_data,
|
|
25
|
+
prepare_chunked_drift,
|
|
25
26
|
prepare_diversity_data,
|
|
26
27
|
prepare_drift_data,
|
|
27
28
|
prepare_feasibility_data,
|
|
@@ -33,6 +34,7 @@ from dataeval_plots.protocols import (
|
|
|
33
34
|
Dataset,
|
|
34
35
|
PlottableBalance,
|
|
35
36
|
PlottableDiversity,
|
|
37
|
+
PlottableDrift,
|
|
36
38
|
PlottableDriftMVDC,
|
|
37
39
|
PlottableFeasibility,
|
|
38
40
|
PlottableStats,
|
|
@@ -849,6 +851,113 @@ class PlotlyBackend(BasePlottingBackend):
|
|
|
849
851
|
|
|
850
852
|
return fig
|
|
851
853
|
|
|
854
|
+
def _plot_drift(
|
|
855
|
+
self,
|
|
856
|
+
output: PlottableDrift,
|
|
857
|
+
figsize: tuple[float, float] | None = None,
|
|
858
|
+
) -> Any: # go.Figure
|
|
859
|
+
"""
|
|
860
|
+
Render the metric of each chunk in relation to the thresholds of the detector.
|
|
861
|
+
|
|
862
|
+
When the output carries the metric of the reference chunks, they are drawn in gray to the left of the
|
|
863
|
+
analysis chunks. The chunks are numbered from -n for the reference and from 0 for the analysis.
|
|
864
|
+
|
|
865
|
+
Parameters
|
|
866
|
+
----------
|
|
867
|
+
output : PlottableDrift
|
|
868
|
+
The chunked drift output object to plot
|
|
869
|
+
figsize : tuple[float, float] or None, default None
|
|
870
|
+
Figure size in inches (width, height), drawn at 100 pixels per inch. If None, defaults to (8, 3.5).
|
|
871
|
+
|
|
872
|
+
Returns
|
|
873
|
+
-------
|
|
874
|
+
plotly.graph_objects.Figure
|
|
875
|
+
"""
|
|
876
|
+
import plotly.graph_objects as go
|
|
877
|
+
|
|
878
|
+
data = prepare_chunked_drift(output)
|
|
879
|
+
frame, reference, analysis = data.frame, data.reference, data.analysis
|
|
880
|
+
drifted = analysis.filter(pl.col("drifted"))
|
|
881
|
+
hover = f"Chunk: %{{x}}<br>{data.metric_name}: %{{y:.4f}}<extra></extra>"
|
|
882
|
+
|
|
883
|
+
fig = go.Figure()
|
|
884
|
+
|
|
885
|
+
# The no-drift range is the band between the two thresholds
|
|
886
|
+
fig.add_trace(
|
|
887
|
+
go.Scatter(
|
|
888
|
+
x=frame["index"].to_numpy(),
|
|
889
|
+
y=frame["upper_threshold"].to_numpy(),
|
|
890
|
+
mode="lines",
|
|
891
|
+
line={"width": 0, "shape": "hvh"},
|
|
892
|
+
showlegend=False,
|
|
893
|
+
hoverinfo="skip",
|
|
894
|
+
)
|
|
895
|
+
)
|
|
896
|
+
fig.add_trace(
|
|
897
|
+
go.Scatter(
|
|
898
|
+
x=frame["index"].to_numpy(),
|
|
899
|
+
y=frame["lower_threshold"].to_numpy(),
|
|
900
|
+
mode="lines",
|
|
901
|
+
line={"width": 0, "shape": "hvh"},
|
|
902
|
+
fill="tonexty",
|
|
903
|
+
fillcolor="rgba(44, 160, 44, 0.15)",
|
|
904
|
+
name="No-drift range",
|
|
905
|
+
hoverinfo="skip",
|
|
906
|
+
)
|
|
907
|
+
)
|
|
908
|
+
|
|
909
|
+
# The reference chunks, which the thresholds derive from
|
|
910
|
+
if len(reference):
|
|
911
|
+
fig.add_trace(
|
|
912
|
+
go.Scatter(
|
|
913
|
+
x=reference["index"].to_numpy(),
|
|
914
|
+
y=reference["value"].to_numpy(),
|
|
915
|
+
mode="lines+markers",
|
|
916
|
+
name="Reference chunks",
|
|
917
|
+
line={"color": "gray", "width": 2},
|
|
918
|
+
hovertemplate=hover,
|
|
919
|
+
)
|
|
920
|
+
)
|
|
921
|
+
fig.add_vline(x=-0.5, line_dash="dot", line_color="gray", line_width=1)
|
|
922
|
+
|
|
923
|
+
# The metric of each analysis chunk
|
|
924
|
+
fig.add_trace(
|
|
925
|
+
go.Scatter(
|
|
926
|
+
x=analysis["index"].to_numpy(),
|
|
927
|
+
y=analysis["value"].to_numpy(),
|
|
928
|
+
mode="lines+markers",
|
|
929
|
+
name="Analysis chunks",
|
|
930
|
+
line={"color": "royalblue", "width": 2},
|
|
931
|
+
hovertemplate=hover,
|
|
932
|
+
)
|
|
933
|
+
)
|
|
934
|
+
|
|
935
|
+
# The chunks that the detector flagged
|
|
936
|
+
if len(drifted):
|
|
937
|
+
fig.add_trace(
|
|
938
|
+
go.Scatter(
|
|
939
|
+
x=drifted["index"].to_numpy(),
|
|
940
|
+
y=drifted["value"].to_numpy(),
|
|
941
|
+
mode="markers",
|
|
942
|
+
name="Drift",
|
|
943
|
+
marker={"symbol": "diamond", "size": 11, "color": "crimson"},
|
|
944
|
+
hovertemplate=hover,
|
|
945
|
+
)
|
|
946
|
+
)
|
|
947
|
+
|
|
948
|
+
width_inches, height_inches = figsize or (8, 3.5)
|
|
949
|
+
fig.update_layout(
|
|
950
|
+
title="Drift by chunk",
|
|
951
|
+
xaxis_title=data.chunk_label,
|
|
952
|
+
yaxis_title=data.metric_name,
|
|
953
|
+
xaxis={"dtick": 1} if len(frame) <= 20 else {},
|
|
954
|
+
width=int(width_inches * 100),
|
|
955
|
+
height=int(height_inches * 100),
|
|
956
|
+
hovermode="closest",
|
|
957
|
+
)
|
|
958
|
+
|
|
959
|
+
return fig
|
|
960
|
+
|
|
852
961
|
def _plot_image_grid(
|
|
853
962
|
self,
|
|
854
963
|
dataset: Dataset,
|
|
@@ -10,14 +10,17 @@ from numpy.typing import NDArray
|
|
|
10
10
|
from dataeval_plots.backends._base import BasePlottingBackend
|
|
11
11
|
from dataeval_plots.backends._shared import (
|
|
12
12
|
CHANNELWISE_METRICS,
|
|
13
|
+
plot_chunked_drift_on_axis,
|
|
13
14
|
plot_drift_on_axis,
|
|
14
15
|
prepare_balance_data,
|
|
16
|
+
prepare_chunked_drift,
|
|
15
17
|
prepare_diversity_data,
|
|
16
18
|
prepare_drift_data,
|
|
17
19
|
)
|
|
18
20
|
from dataeval_plots.protocols import (
|
|
19
21
|
PlottableBalance,
|
|
20
22
|
PlottableDiversity,
|
|
23
|
+
PlottableDrift,
|
|
21
24
|
PlottableDriftMVDC,
|
|
22
25
|
PlottableFeasibility,
|
|
23
26
|
PlottableStats,
|
|
@@ -493,6 +496,47 @@ class SeabornBackend(BasePlottingBackend):
|
|
|
493
496
|
fig.tight_layout()
|
|
494
497
|
return fig
|
|
495
498
|
|
|
499
|
+
def _plot_drift(
|
|
500
|
+
self,
|
|
501
|
+
output: PlottableDrift,
|
|
502
|
+
figsize: tuple[float, float] | None = None,
|
|
503
|
+
) -> Figure:
|
|
504
|
+
"""
|
|
505
|
+
Render the metric of each chunk in relation to the thresholds of the detector.
|
|
506
|
+
|
|
507
|
+
Parameters
|
|
508
|
+
----------
|
|
509
|
+
output : PlottableDrift
|
|
510
|
+
The chunked drift output object to plot
|
|
511
|
+
figsize : tuple[float, float] or None, default None
|
|
512
|
+
Figure size in inches (width, height). If None, defaults to (10, 4).
|
|
513
|
+
|
|
514
|
+
Returns
|
|
515
|
+
-------
|
|
516
|
+
matplotlib.figure.Figure
|
|
517
|
+
"""
|
|
518
|
+
import matplotlib.pyplot as plt
|
|
519
|
+
import seaborn as sns
|
|
520
|
+
|
|
521
|
+
sns.set_style("whitegrid")
|
|
522
|
+
palette = sns.color_palette("deep") # not the global palette, which a user can shorten
|
|
523
|
+
|
|
524
|
+
data = prepare_chunked_drift(output)
|
|
525
|
+
|
|
526
|
+
fig, ax = plt.subplots(figsize=figsize or (10, 4))
|
|
527
|
+
plot_chunked_drift_on_axis(
|
|
528
|
+
ax,
|
|
529
|
+
data,
|
|
530
|
+
line_color=palette[0],
|
|
531
|
+
band_color=palette[2],
|
|
532
|
+
drift_color=palette[3],
|
|
533
|
+
reference_color=palette[7],
|
|
534
|
+
)
|
|
535
|
+
ax.set_title("Drift by chunk")
|
|
536
|
+
sns.despine(ax=ax)
|
|
537
|
+
fig.tight_layout()
|
|
538
|
+
return fig
|
|
539
|
+
|
|
496
540
|
def _plot_feasibility(
|
|
497
541
|
self,
|
|
498
542
|
output: PlottableFeasibility,
|
|
@@ -15,12 +15,16 @@ import numpy as np
|
|
|
15
15
|
import polars as pl
|
|
16
16
|
from numpy.typing import NDArray
|
|
17
17
|
|
|
18
|
+
from dataeval_plots.protocols import PlottableFeasibilityDecisions
|
|
19
|
+
|
|
18
20
|
if TYPE_CHECKING:
|
|
19
21
|
from dataeval_plots.protocols import (
|
|
20
22
|
PlottableBalance,
|
|
21
23
|
PlottableDiversity,
|
|
24
|
+
PlottableDrift,
|
|
22
25
|
PlottableDriftMVDC,
|
|
23
26
|
PlottableFeasibility,
|
|
27
|
+
PlottableFeasibilityDeterminations,
|
|
24
28
|
)
|
|
25
29
|
|
|
26
30
|
MethodType = Literal[
|
|
@@ -46,6 +50,10 @@ __all__ = [
|
|
|
46
50
|
"prepare_diversity_data",
|
|
47
51
|
"prepare_drift_data",
|
|
48
52
|
"plot_drift_on_axis",
|
|
53
|
+
"CHUNKED_DRIFT_COLUMNS",
|
|
54
|
+
"ChunkedDriftData",
|
|
55
|
+
"prepare_chunked_drift",
|
|
56
|
+
"plot_chunked_drift_on_axis",
|
|
49
57
|
"FEASIBILITY_COLORS",
|
|
50
58
|
"FEASIBILITY_CONFLICT_TEXT",
|
|
51
59
|
"FeasibilityBand",
|
|
@@ -498,6 +506,200 @@ def prepare_drift_data(
|
|
|
498
506
|
return resdf, trndf, tstdf, driftx, True
|
|
499
507
|
|
|
500
508
|
|
|
509
|
+
CHUNKED_DRIFT_COLUMNS = ("index", "value", "lower_threshold", "upper_threshold", "drifted")
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
@dataclass(frozen=True)
|
|
513
|
+
class ChunkedDriftData:
|
|
514
|
+
"""
|
|
515
|
+
The per-chunk results of a chunked drift detector, ready for plotting.
|
|
516
|
+
|
|
517
|
+
Attributes
|
|
518
|
+
----------
|
|
519
|
+
frame : pl.DataFrame
|
|
520
|
+
One row per chunk with the columns ``index``, ``value``, ``lower_threshold``, ``upper_threshold``,
|
|
521
|
+
``drifted`` and ``period``. The rows of the reference chunks come first, numbered from ``-n`` to ``-1``
|
|
522
|
+
so that the analysis chunks keep their own indexes, and have ``period`` set to ``"reference"``. The
|
|
523
|
+
analysis chunks have ``period`` set to ``"analysis"``.
|
|
524
|
+
metric_name : str
|
|
525
|
+
Name of the metric that ``value`` reports
|
|
526
|
+
chunk_label : str
|
|
527
|
+
Label for the axis of chunks, which includes the size of a chunk when every analysis chunk has the same size
|
|
528
|
+
"""
|
|
529
|
+
|
|
530
|
+
frame: pl.DataFrame
|
|
531
|
+
metric_name: str
|
|
532
|
+
chunk_label: str
|
|
533
|
+
|
|
534
|
+
@property
|
|
535
|
+
def reference(self) -> pl.DataFrame:
|
|
536
|
+
"""The rows of the reference chunks, which is empty when the output carries no baseline."""
|
|
537
|
+
return self.frame.filter(pl.col("period") == "reference")
|
|
538
|
+
|
|
539
|
+
@property
|
|
540
|
+
def analysis(self) -> pl.DataFrame:
|
|
541
|
+
"""The rows of the analysis chunks."""
|
|
542
|
+
return self.frame.filter(pl.col("period") == "analysis")
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def prepare_chunked_drift(output: PlottableDrift) -> ChunkedDriftData:
|
|
546
|
+
"""
|
|
547
|
+
Read the per-chunk table, the reference chunks and the metric name from a chunked drift output.
|
|
548
|
+
|
|
549
|
+
The reference chunks are the optional ``baseline`` attribute of the output, which holds the metric of each
|
|
550
|
+
reference chunk. An output without it, such as one from an earlier version of DataEval, has no reference rows
|
|
551
|
+
and the plot leaves them out.
|
|
552
|
+
|
|
553
|
+
Parameters
|
|
554
|
+
----------
|
|
555
|
+
output : PlottableDrift
|
|
556
|
+
The drift output object to plot
|
|
557
|
+
|
|
558
|
+
Returns
|
|
559
|
+
-------
|
|
560
|
+
ChunkedDriftData
|
|
561
|
+
The per-chunk results. A threshold that is missing, as for a one-sided detector, is replaced by a bound just
|
|
562
|
+
beyond the data, so that the range of no drift has no limit on that side.
|
|
563
|
+
|
|
564
|
+
Raises
|
|
565
|
+
------
|
|
566
|
+
ValueError
|
|
567
|
+
If the output holds the statistics of a single test and not a per-chunk table, or if the table lacks a column
|
|
568
|
+
that the plot needs
|
|
569
|
+
"""
|
|
570
|
+
details = output.details
|
|
571
|
+
if not isinstance(details, pl.DataFrame):
|
|
572
|
+
raise ValueError("Plotting drift needs chunked results. Call .chunked() on the detector before predict().")
|
|
573
|
+
missing = [name for name in CHUNKED_DRIFT_COLUMNS if name not in details.columns]
|
|
574
|
+
if missing:
|
|
575
|
+
raise ValueError(f"The per-chunk drift results lack the columns {missing}.")
|
|
576
|
+
|
|
577
|
+
analysis = details.select(
|
|
578
|
+
pl.col("index").cast(pl.Int64),
|
|
579
|
+
pl.col("value", "lower_threshold", "upper_threshold").cast(pl.Float64),
|
|
580
|
+
pl.col("drifted").cast(pl.Boolean),
|
|
581
|
+
pl.lit("analysis").alias("period"),
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
# The thresholds come from the reference chunks, so the band spans them too
|
|
585
|
+
baseline = np.asarray(getattr(output, "baseline", None) or (), dtype=np.float64)
|
|
586
|
+
if len(baseline) and len(analysis):
|
|
587
|
+
reference = pl.DataFrame(
|
|
588
|
+
{
|
|
589
|
+
"index": np.arange(-len(baseline), 0, dtype=np.int64),
|
|
590
|
+
"value": baseline,
|
|
591
|
+
"lower_threshold": [analysis["lower_threshold"][0]] * len(baseline),
|
|
592
|
+
"upper_threshold": [analysis["upper_threshold"][0]] * len(baseline),
|
|
593
|
+
"drifted": [False] * len(baseline),
|
|
594
|
+
"period": ["reference"] * len(baseline),
|
|
595
|
+
},
|
|
596
|
+
schema=analysis.schema,
|
|
597
|
+
)
|
|
598
|
+
frame = pl.concat([reference, analysis])
|
|
599
|
+
else:
|
|
600
|
+
frame = analysis
|
|
601
|
+
|
|
602
|
+
if frame["lower_threshold"].null_count() or frame["upper_threshold"].null_count():
|
|
603
|
+
extent = pl.concat([frame["value"], frame["lower_threshold"], frame["upper_threshold"]]).drop_nulls()
|
|
604
|
+
low, high = extent.min(), extent.max()
|
|
605
|
+
pad = 0.1 * (high - low) if low != high else 1.0 # type: ignore[operator]
|
|
606
|
+
frame = frame.with_columns(
|
|
607
|
+
pl.col("lower_threshold").fill_null(low - pad), # type: ignore[operator]
|
|
608
|
+
pl.col("upper_threshold").fill_null(high + pad), # type: ignore[operator]
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
return ChunkedDriftData(frame, output.metric_name, _chunk_label(details))
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def _chunk_label(details: pl.DataFrame) -> str:
|
|
615
|
+
"""Label the axis of chunks with the size of a chunk, when every chunk has the same size."""
|
|
616
|
+
if "start_index" in details.columns and "end_index" in details.columns and len(details):
|
|
617
|
+
sizes = (details["end_index"] - details["start_index"] + 1).unique()
|
|
618
|
+
if len(sizes) == 1 and (details["start_index"] >= 0).all():
|
|
619
|
+
return f"Chunk ({sizes[0]} samples each)"
|
|
620
|
+
return "Chunk"
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
def plot_chunked_drift_on_axis(
|
|
624
|
+
ax: Any,
|
|
625
|
+
data: ChunkedDriftData,
|
|
626
|
+
line_color: Any = "tab:blue",
|
|
627
|
+
band_color: Any = "tab:green",
|
|
628
|
+
drift_color: Any = "tab:red",
|
|
629
|
+
reference_color: Any = "tab:gray",
|
|
630
|
+
linewidth: float = 1.5,
|
|
631
|
+
) -> None:
|
|
632
|
+
"""
|
|
633
|
+
Plot the metric of each chunk against the threshold range of the detector on a matplotlib axis.
|
|
634
|
+
|
|
635
|
+
This is a shared helper for the matplotlib and seaborn backends. The metric is a line with a marker per chunk,
|
|
636
|
+
the band is the range between the lower and upper thresholds in which a chunk counts as no drift, and a diamond
|
|
637
|
+
marks each chunk that the detector flagged. When the output carries the metric of the reference chunks, they are
|
|
638
|
+
drawn in a second color to the left of the analysis chunks, with a dotted line between them.
|
|
639
|
+
|
|
640
|
+
Parameters
|
|
641
|
+
----------
|
|
642
|
+
ax : matplotlib.axes.Axes
|
|
643
|
+
Axis to plot on
|
|
644
|
+
data : ChunkedDriftData
|
|
645
|
+
Per-chunk results from :func:`prepare_chunked_drift`
|
|
646
|
+
line_color : Any, default "tab:blue"
|
|
647
|
+
Color of the metric line of the analysis chunks
|
|
648
|
+
band_color : Any, default "tab:green"
|
|
649
|
+
Color of the band between the thresholds
|
|
650
|
+
drift_color : Any, default "tab:red"
|
|
651
|
+
Color of the markers on the chunks with drift
|
|
652
|
+
reference_color : Any, default "tab:gray"
|
|
653
|
+
Color of the metric line of the reference chunks and of the line that separates them
|
|
654
|
+
linewidth : float, default 1.5
|
|
655
|
+
Width of the metric lines
|
|
656
|
+
"""
|
|
657
|
+
from matplotlib.ticker import MaxNLocator
|
|
658
|
+
|
|
659
|
+
frame, reference, analysis = data.frame, data.reference, data.analysis
|
|
660
|
+
ax.fill_between(
|
|
661
|
+
frame["index"].to_numpy(),
|
|
662
|
+
frame["lower_threshold"].to_numpy(),
|
|
663
|
+
frame["upper_threshold"].to_numpy(),
|
|
664
|
+
step="mid",
|
|
665
|
+
color=band_color,
|
|
666
|
+
alpha=0.15,
|
|
667
|
+
label="No-drift range",
|
|
668
|
+
)
|
|
669
|
+
if len(reference):
|
|
670
|
+
ax.plot(
|
|
671
|
+
reference["index"].to_numpy(),
|
|
672
|
+
reference["value"].to_numpy(),
|
|
673
|
+
marker="o",
|
|
674
|
+
color=reference_color,
|
|
675
|
+
linewidth=linewidth,
|
|
676
|
+
label="Reference chunks",
|
|
677
|
+
)
|
|
678
|
+
ax.axvline(-0.5, color=reference_color, linestyle=":", linewidth=1)
|
|
679
|
+
ax.plot(
|
|
680
|
+
analysis["index"].to_numpy(),
|
|
681
|
+
analysis["value"].to_numpy(),
|
|
682
|
+
marker="o",
|
|
683
|
+
color=line_color,
|
|
684
|
+
linewidth=linewidth,
|
|
685
|
+
label="Analysis chunks",
|
|
686
|
+
)
|
|
687
|
+
drifted = analysis.filter(pl.col("drifted"))
|
|
688
|
+
if len(drifted):
|
|
689
|
+
ax.scatter(
|
|
690
|
+
drifted["index"].to_numpy(),
|
|
691
|
+
drifted["value"].to_numpy(),
|
|
692
|
+
marker="D",
|
|
693
|
+
color=drift_color,
|
|
694
|
+
zorder=3,
|
|
695
|
+
label="Drift",
|
|
696
|
+
)
|
|
697
|
+
ax.xaxis.set_major_locator(MaxNLocator(integer=True))
|
|
698
|
+
ax.set_xlabel(data.chunk_label)
|
|
699
|
+
ax.set_ylabel(data.metric_name)
|
|
700
|
+
ax.legend(loc="best")
|
|
701
|
+
|
|
702
|
+
|
|
501
703
|
def plot_drift_on_axis(
|
|
502
704
|
ax: Any,
|
|
503
705
|
resdf: Any,
|
|
@@ -982,12 +1184,19 @@ def _feasibility_band(
|
|
|
982
1184
|
)
|
|
983
1185
|
|
|
984
1186
|
|
|
985
|
-
#
|
|
1187
|
+
# The readable name of each source: by DataEval 1.2.0's evidence id (these mirror its performance/_feasibility.py:
|
|
1188
|
+
# _SOURCES, _source_name, _unit_noun and FeasibilityOutput._bounds), and by the `source` value of the decisions and
|
|
1189
|
+
# bounds shape. Any other value shows its own text.
|
|
986
1190
|
_SOURCE_NAMES = {
|
|
987
1191
|
"replica_ceiling/v1": "repeated labels",
|
|
988
1192
|
"builtin_witness/v1:knn": "built-in k-NN",
|
|
989
1193
|
"builtin_witness/v1:probe": "built-in linear probe",
|
|
990
1194
|
"user_witness/v1": "your model",
|
|
1195
|
+
"local_panels": _GROUP_CEILING,
|
|
1196
|
+
"replicas": "repeated labels",
|
|
1197
|
+
"knn": "built-in k-NN",
|
|
1198
|
+
"linear_probe": "built-in linear probe",
|
|
1199
|
+
"user_model": "your model",
|
|
991
1200
|
}
|
|
992
1201
|
_EVIDENCE_SCHEMA = {
|
|
993
1202
|
"target_spec": pl.String,
|
|
@@ -1009,7 +1218,7 @@ _EVIDENCE_SCHEMA = {
|
|
|
1009
1218
|
}
|
|
1010
1219
|
|
|
1011
1220
|
|
|
1012
|
-
def _evidence_rows(output:
|
|
1221
|
+
def _evidence_rows(output: PlottableFeasibilityDeterminations) -> pl.DataFrame:
|
|
1013
1222
|
"""One row per endpoint on a target in ``determinations``, in their order, then the order of ``evidence``."""
|
|
1014
1223
|
targets = {r["target_spec"]: r for r in output.determinations.iter_rows(named=True)}
|
|
1015
1224
|
rows = []
|
|
@@ -1049,6 +1258,84 @@ def _evidence_rows(output: PlottableFeasibility) -> pl.DataFrame:
|
|
|
1049
1258
|
return pl.DataFrame(rows, schema=_EVIDENCE_SCHEMA)
|
|
1050
1259
|
|
|
1051
1260
|
|
|
1261
|
+
def _decisions_inputs(output: PlottableFeasibilityDecisions) -> tuple[list[dict[str, Any]], pl.DataFrame]:
|
|
1262
|
+
"""
|
|
1263
|
+
Read the decisions and bounds shape as the determination rows and evidence frame the bands are built from.
|
|
1264
|
+
|
|
1265
|
+
The rows use the determinations shape's vocabulary, so everything downstream reads either shape alike: an
|
|
1266
|
+
unresolved decision with limitation ``conflict`` is an ``evidence_conflict``, and ``envelope`` is a ``ceiling``.
|
|
1267
|
+
"""
|
|
1268
|
+
decisions = list(output.decisions.iter_rows(named=True))
|
|
1269
|
+
bounds = list(output.bounds.iter_rows(named=True))
|
|
1270
|
+
rows: list[dict[str, Any]] = []
|
|
1271
|
+
evidence: list[dict[str, Any]] = []
|
|
1272
|
+
for d in decisions:
|
|
1273
|
+
# ponytail: a private join key in the old reader's vocabulary; typed keys replace it when that reader goes
|
|
1274
|
+
spec = json.dumps(
|
|
1275
|
+
{
|
|
1276
|
+
"metric": d["metric"],
|
|
1277
|
+
"scope": d["judged_scope"],
|
|
1278
|
+
"class_name": d["class_name"],
|
|
1279
|
+
"aggregation": d["aggregation"],
|
|
1280
|
+
"ap_functional": d["ap_functional"],
|
|
1281
|
+
"max_false_positive_rate": d["max_false_positive_rate"],
|
|
1282
|
+
"threshold": d["threshold"],
|
|
1283
|
+
},
|
|
1284
|
+
sort_keys=True,
|
|
1285
|
+
)
|
|
1286
|
+
conflict = d["decision"] == "unresolved" and d["limitation"] == "conflict"
|
|
1287
|
+
rows.append(
|
|
1288
|
+
{
|
|
1289
|
+
"target_spec": spec,
|
|
1290
|
+
"metric": d["metric"],
|
|
1291
|
+
"threshold": d["threshold"],
|
|
1292
|
+
"judged_scope": d["judged_scope"],
|
|
1293
|
+
"determination": "evidence_conflict" if conflict else d["decision"],
|
|
1294
|
+
"ceiling": d["envelope"],
|
|
1295
|
+
"ceiling_scope": d["envelope_scope"],
|
|
1296
|
+
"witness": d["witness"],
|
|
1297
|
+
"witness_source": d["witness_source"],
|
|
1298
|
+
}
|
|
1299
|
+
)
|
|
1300
|
+
mine = [b for b in bounds if b["requirement"] == d["requirement"]]
|
|
1301
|
+
f0 = d["max_false_positive_rate"]
|
|
1302
|
+
for b in mine:
|
|
1303
|
+
q = _FALSE_POSITIVE if b["quantity"] == _FALSE_POSITIVE else "metric"
|
|
1304
|
+
fp = next(
|
|
1305
|
+
(x["bound"] for x in mine if x["source"] == b["source"] and x["quantity"] == _FALSE_POSITIVE), None
|
|
1306
|
+
)
|
|
1307
|
+
# A recall witness counts only when its own false-positive upper bound is within the limit
|
|
1308
|
+
uncertified = q == "metric" and b["bound_type"] == "lower" and f0 is not None and (fp is None or fp > f0)
|
|
1309
|
+
evidence.append(
|
|
1310
|
+
{
|
|
1311
|
+
"target_spec": spec,
|
|
1312
|
+
"metric": d["metric"],
|
|
1313
|
+
"threshold": d["threshold"],
|
|
1314
|
+
"source": _SOURCE_NAMES.get(b["source"], b["source"]),
|
|
1315
|
+
"evidence_id": b["source"],
|
|
1316
|
+
"information_scope": b["information_scope"],
|
|
1317
|
+
"unit": b["unit"],
|
|
1318
|
+
"bound_type": b["bound_type"],
|
|
1319
|
+
"observed": b["observed"],
|
|
1320
|
+
"bound": b["bound"],
|
|
1321
|
+
"n": b["n"],
|
|
1322
|
+
"state": b["state"],
|
|
1323
|
+
"quantity": q,
|
|
1324
|
+
"uncertified": uncertified,
|
|
1325
|
+
"false_positive_bound": fp,
|
|
1326
|
+
"false_positive_limit": f0,
|
|
1327
|
+
}
|
|
1328
|
+
)
|
|
1329
|
+
return rows, pl.DataFrame(evidence, schema=_EVIDENCE_SCHEMA)
|
|
1330
|
+
|
|
1331
|
+
|
|
1332
|
+
def _feasibility_inputs(output: PlottableFeasibility) -> tuple[list[dict[str, Any]], pl.DataFrame]:
|
|
1333
|
+
"""The determination rows and evidence frame of a feasibility output in either shape."""
|
|
1334
|
+
if isinstance(output, PlottableFeasibilityDecisions):
|
|
1335
|
+
return _decisions_inputs(output)
|
|
1336
|
+
return list(output.determinations.iter_rows(named=True)), _evidence_rows(output)
|
|
1337
|
+
|
|
1338
|
+
|
|
1052
1339
|
def prepare_feasibility_data(output: PlottableFeasibility) -> list[FeasibilityBand]:
|
|
1053
1340
|
"""
|
|
1054
1341
|
Prepare feasibility data for plotting across all backends.
|
|
@@ -1066,13 +1353,13 @@ def prepare_feasibility_data(output: PlottableFeasibility) -> list[FeasibilityBa
|
|
|
1066
1353
|
list[FeasibilityBand]
|
|
1067
1354
|
One band per quantity, in the order the determinations first name it
|
|
1068
1355
|
"""
|
|
1069
|
-
rows =
|
|
1356
|
+
rows, evidence_frame = _feasibility_inputs(output)
|
|
1070
1357
|
groups: dict[str, list[dict[str, Any]]] = {}
|
|
1071
1358
|
for row in rows:
|
|
1072
1359
|
spec = json.loads(row["target_spec"])
|
|
1073
1360
|
spec.pop("threshold", None)
|
|
1074
1361
|
groups.setdefault(json.dumps(spec, sort_keys=True), []).append(row)
|
|
1075
|
-
evidence =
|
|
1362
|
+
evidence = evidence_frame.to_dicts()
|
|
1076
1363
|
alpha = output.meta().state["alpha"]
|
|
1077
1364
|
noun = _unit(rows, [e for e in evidence if e["quantity"] == "metric"]) or "image" # the printout's footer noun
|
|
1078
1365
|
bands = []
|
|
@@ -12,7 +12,10 @@ The protocol hierarchy:
|
|
|
12
12
|
- PlottableSufficiency
|
|
13
13
|
- PlottableStats
|
|
14
14
|
- PlottableDriftMVDC
|
|
15
|
-
-
|
|
15
|
+
- PlottableDrift
|
|
16
|
+
- PlottableFeasibilityDeterminations
|
|
17
|
+
- PlottableFeasibilityDecisions
|
|
18
|
+
- PlottableFeasibility (either of the two feasibility shapes)
|
|
16
19
|
"""
|
|
17
20
|
|
|
18
21
|
from __future__ import annotations
|
|
@@ -122,13 +125,15 @@ class Plottable(Protocol):
|
|
|
122
125
|
"""
|
|
123
126
|
|
|
124
127
|
@property
|
|
125
|
-
def plot_type(
|
|
128
|
+
def plot_type(
|
|
129
|
+
self,
|
|
130
|
+
) -> Literal["balance", "diversity", "sufficiency", "stats", "drift_mvdc", "drift", "feasibility"]:
|
|
126
131
|
"""Return the plot type identifier for routing to appropriate plot function.
|
|
127
132
|
|
|
128
133
|
Returns
|
|
129
134
|
-------
|
|
130
135
|
str
|
|
131
|
-
One of: 'balance', 'diversity', 'sufficiency', 'stats', 'drift_mvdc', 'feasibility'
|
|
136
|
+
One of: 'balance', 'diversity', 'sufficiency', 'stats', 'drift_mvdc', 'drift', 'feasibility'
|
|
132
137
|
"""
|
|
133
138
|
...
|
|
134
139
|
|
|
@@ -289,6 +294,38 @@ class PlottableDriftMVDC(Plottable, Protocol):
|
|
|
289
294
|
def plot_type(self) -> Literal["drift_mvdc"]: ...
|
|
290
295
|
|
|
291
296
|
|
|
297
|
+
@runtime_checkable
|
|
298
|
+
class PlottableDrift(Plottable, Protocol):
|
|
299
|
+
"""Protocol for chunked drift detector outputs.
|
|
300
|
+
|
|
301
|
+
Required attributes:
|
|
302
|
+
- details: Per-chunk results as a polars DataFrame
|
|
303
|
+
- metric_name: Name of the drift metric that each chunk reports
|
|
304
|
+
- plot_type -> 'drift'
|
|
305
|
+
"""
|
|
306
|
+
|
|
307
|
+
@property
|
|
308
|
+
def details(self) -> Any:
|
|
309
|
+
"""Per-chunk drift results.
|
|
310
|
+
|
|
311
|
+
Returns
|
|
312
|
+
-------
|
|
313
|
+
pl.DataFrame
|
|
314
|
+
One row per chunk with the columns ``index``, ``value``, ``lower_threshold``, ``upper_threshold`` and
|
|
315
|
+
``drifted``. A detector that was not chunked holds a mapping of statistics instead, which cannot be
|
|
316
|
+
plotted.
|
|
317
|
+
"""
|
|
318
|
+
...
|
|
319
|
+
|
|
320
|
+
@property
|
|
321
|
+
def metric_name(self) -> str:
|
|
322
|
+
"""Name of the metric that ``value`` reports, such as ``auroc`` or ``mmd2``."""
|
|
323
|
+
...
|
|
324
|
+
|
|
325
|
+
@property
|
|
326
|
+
def plot_type(self) -> Literal["drift"]: ...
|
|
327
|
+
|
|
328
|
+
|
|
292
329
|
class FeasibilityEndpoint(Protocol):
|
|
293
330
|
"""
|
|
294
331
|
One bound on one target, as DataEval's ``Endpoint`` records it; only the fields the plot reads.
|
|
@@ -323,8 +360,8 @@ class FeasibilityEvidence(Protocol):
|
|
|
323
360
|
|
|
324
361
|
|
|
325
362
|
@runtime_checkable
|
|
326
|
-
class
|
|
327
|
-
"""Protocol for feasibility plot outputs.
|
|
363
|
+
class PlottableFeasibilityDeterminations(Plottable, Protocol):
|
|
364
|
+
"""Protocol for feasibility plot outputs in the ``determinations`` and ``evidence`` shape.
|
|
328
365
|
|
|
329
366
|
Required attributes:
|
|
330
367
|
- determinations: One row per target (DataFrame)
|
|
@@ -347,6 +384,45 @@ class PlottableFeasibility(Plottable, Protocol):
|
|
|
347
384
|
def plot_type(self) -> Literal["feasibility"]: ...
|
|
348
385
|
|
|
349
386
|
|
|
387
|
+
@runtime_checkable
|
|
388
|
+
class PlottableFeasibilityDecisions(Plottable, Protocol):
|
|
389
|
+
"""Protocol for feasibility plot outputs in the ``decisions`` and ``bounds`` shape.
|
|
390
|
+
|
|
391
|
+
Required attributes:
|
|
392
|
+
- decisions: One row per requirement (DataFrame)
|
|
393
|
+
- bounds: One row per source, requirement and quantity (DataFrame)
|
|
394
|
+
- meta().state["alpha"]: The call's error budget
|
|
395
|
+
- plot_type -> 'feasibility'
|
|
396
|
+
|
|
397
|
+
Structures:
|
|
398
|
+
- decisions columns: requirement, metric, threshold, judged_scope, class_name, aggregation, ap_functional,
|
|
399
|
+
max_false_positive_rate, decision, limitation, envelope, envelope_scope, witness, witness_source
|
|
400
|
+
- bounds columns: requirement, source, role, quantity, bound_type, information_scope, unit, state, bound,
|
|
401
|
+
observed, n; ``requirement`` joins a bound to its decisions row
|
|
402
|
+
|
|
403
|
+
Enum columns are read as strings, with these values:
|
|
404
|
+
- decision: demonstrated_feasible, infeasible_under_recorded_assumptions, not_ruled_out, unresolved
|
|
405
|
+
- limitation: only ``conflict`` is read; with decision ``unresolved`` it marks a conflict between a witness and an
|
|
406
|
+
envelope
|
|
407
|
+
- judged_scope, envelope_scope, information_scope: full_input, fixed_representation, partition_restricted
|
|
408
|
+
- source, witness_source: local_panels, replicas, knn, linear_probe, user_model (any other value shows its text)
|
|
409
|
+
- role: envelope, witness. bound_type: lower, upper
|
|
410
|
+
- quantity: a metric name (accuracy, ap, recall, recall_at_fp, mota) or false_positive_rate
|
|
411
|
+
- state: unavailable, uninformative, informative. unit: image, sequence
|
|
412
|
+
- aggregation: frame_pooled, sequence_mean. ap_functional: population_ap_integral, population_ap_101
|
|
413
|
+
"""
|
|
414
|
+
|
|
415
|
+
@property
|
|
416
|
+
def decisions(self) -> pl.DataFrame: ...
|
|
417
|
+
@property
|
|
418
|
+
def bounds(self) -> pl.DataFrame: ...
|
|
419
|
+
@property
|
|
420
|
+
def plot_type(self) -> Literal["feasibility"]: ...
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
PlottableFeasibility = PlottableFeasibilityDeterminations | PlottableFeasibilityDecisions
|
|
424
|
+
"""A feasibility output in either shape; DataEval 1.2.0 gives the first, later releases the second."""
|
|
425
|
+
|
|
350
426
|
# Type alias for all plottable types
|
|
351
427
|
PlottableType = (
|
|
352
428
|
Dataset
|
|
@@ -355,5 +431,6 @@ PlottableType = (
|
|
|
355
431
|
| PlottableSufficiency
|
|
356
432
|
| PlottableStats
|
|
357
433
|
| PlottableDriftMVDC
|
|
434
|
+
| PlottableDrift
|
|
358
435
|
| PlottableFeasibility
|
|
359
436
|
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dataeval_plots-0.0.15 → dataeval_plots-0.0.17}/src/dataeval_plots/backends/_feasibility_layout.py
RENAMED
|
File without changes
|
|
File without changes
|