dataeval-plots 0.0.11__tar.gz → 0.0.13__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.
Files changed (17) hide show
  1. {dataeval_plots-0.0.11 → dataeval_plots-0.0.13}/PKG-INFO +3 -3
  2. {dataeval_plots-0.0.11 → dataeval_plots-0.0.13}/pyproject.toml +11 -3
  3. {dataeval_plots-0.0.11 → dataeval_plots-0.0.13}/src/dataeval_plots/__init__.py +40 -17
  4. {dataeval_plots-0.0.11 → dataeval_plots-0.0.13}/src/dataeval_plots/_version.py +2 -2
  5. {dataeval_plots-0.0.11 → dataeval_plots-0.0.13}/src/dataeval_plots/backends/_shared.py +58 -3
  6. {dataeval_plots-0.0.11 → dataeval_plots-0.0.13}/src/dataeval_plots/protocols.py +37 -16
  7. {dataeval_plots-0.0.11 → dataeval_plots-0.0.13}/.gitignore +0 -0
  8. {dataeval_plots-0.0.11 → dataeval_plots-0.0.13}/README.md +0 -0
  9. {dataeval_plots-0.0.11 → dataeval_plots-0.0.13}/src/dataeval_plots/_registry.py +0 -0
  10. {dataeval_plots-0.0.11 → dataeval_plots-0.0.13}/src/dataeval_plots/backends/__init__.py +0 -0
  11. {dataeval_plots-0.0.11 → dataeval_plots-0.0.13}/src/dataeval_plots/backends/_altair.py +0 -0
  12. {dataeval_plots-0.0.11 → dataeval_plots-0.0.13}/src/dataeval_plots/backends/_base.py +0 -0
  13. {dataeval_plots-0.0.11 → dataeval_plots-0.0.13}/src/dataeval_plots/backends/_feasibility_layout.py +0 -0
  14. {dataeval_plots-0.0.11 → dataeval_plots-0.0.13}/src/dataeval_plots/backends/_matplotlib.py +0 -0
  15. {dataeval_plots-0.0.11 → dataeval_plots-0.0.13}/src/dataeval_plots/backends/_plotly.py +0 -0
  16. {dataeval_plots-0.0.11 → dataeval_plots-0.0.13}/src/dataeval_plots/backends/_seaborn.py +0 -0
  17. {dataeval_plots-0.0.11 → dataeval_plots-0.0.13}/src/dataeval_plots/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: dataeval-plots
3
- Version: 0.0.11
3
+ Version: 0.0.13
4
4
  Summary: DataEval companion package for plotting utilities
5
5
  Project-URL: Homepage, https://dataeval.ai/
6
6
  Project-URL: Repository, https://github.com/aria-ml/dataeval/
@@ -13,12 +13,12 @@ Classifier: Intended Audience :: Science/Research
13
13
  Classifier: License :: OSI Approved :: MIT License
14
14
  Classifier: Operating System :: OS Independent
15
15
  Classifier: Programming Language :: Python :: 3 :: Only
16
- Classifier: Programming Language :: Python :: 3.10
17
16
  Classifier: Programming Language :: Python :: 3.11
18
17
  Classifier: Programming Language :: Python :: 3.12
19
18
  Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
20
  Classifier: Topic :: Scientific/Engineering
21
- Requires-Python: >=3.10
21
+ Requires-Python: >=3.11
22
22
  Requires-Dist: matplotlib>=3.7.1
23
23
  Requires-Dist: numpy>=1.24.2; python_version < '3.12'
24
24
  Requires-Dist: numpy>=1.26.2; python_version >= '3.12'
@@ -6,7 +6,7 @@ authors = [
6
6
  { name = "Andrew Weng", email = "andrew.weng@ariacoustics.com" },
7
7
  { name = "Ryan Wood", email = "ryan.wood@ariacoustics.com" },
8
8
  ]
9
- requires-python = ">=3.10"
9
+ requires-python = ">=3.11"
10
10
  readme = "README.md"
11
11
  license = "MIT"
12
12
  maintainers = [
@@ -18,10 +18,10 @@ classifiers = [
18
18
  "Intended Audience :: Science/Research",
19
19
  "License :: OSI Approved :: MIT License",
20
20
  "Programming Language :: Python :: 3 :: Only",
21
- "Programming Language :: Python :: 3.10",
22
21
  "Programming Language :: Python :: 3.11",
23
22
  "Programming Language :: Python :: 3.12",
24
23
  "Programming Language :: Python :: 3.13",
24
+ "Programming Language :: Python :: 3.14",
25
25
  "Topic :: Scientific/Engineering",
26
26
  ]
27
27
  dependencies = [
@@ -86,6 +86,14 @@ dev = [
86
86
  "ipywidgets",
87
87
  ]
88
88
 
89
+ [tool.uv]
90
+ constraint-dependencies = [
91
+ "pillow>=13.0.0", # Pillow 13.0.0 security release (decoder/encoder OOB reads and writes, DoS):
92
+ # CVE-2026-112395, CVE-2026-112397, CVE-2026-112400,
93
+ # CVE-2026-112403, CVE-2026-112404, CVE-2026-112895,
94
+ # CVE-2026-112896, CVE-2026-112897, CVE-2026-112899
95
+ ]
96
+
89
97
  [[tool.uv.index]]
90
98
  name = "pytorch-cpu"
91
99
  url = "https://download.pytorch.org/whl/cpu"
@@ -155,7 +163,7 @@ exclude = [
155
163
  ]
156
164
  line-length = 120
157
165
  indent-width = 4
158
- target-version = "py310"
166
+ target-version = "py311"
159
167
 
160
168
  [tool.ruff.lint]
161
169
  select = ["A", "ANN", "C4", "C90", "E", "F", "I", "NPY", "S", "SIM", "RET", "RUF100", "UP"]
@@ -198,7 +198,26 @@ def plot(
198
198
  >>> plot(result, images=dataset) # Uses seaborn
199
199
  """
200
200
  plotting_backend = get_backend(backend)
201
- return plotting_backend.plot(output, figsize=figsize, **kwargs)
201
+ return _display_once(plotting_backend.plot(output, figsize=figsize, **kwargs))
202
+
203
+
204
+ def _display_once(result: Any) -> Any:
205
+ """
206
+ Close a returned matplotlib figure under the notebook inline backend, so the notebook draws it once.
207
+
208
+ The inline backend draws every open pyplot figure when a cell ends, and the notebook draws a returned figure as
209
+ the cell's value, so an open returned figure appears twice. Closing it leaves the returned value as the only
210
+ drawing. Under any other backend the figure stays open for ``plt.show()``, and a list of figures is left open
211
+ because a notebook shows a list as text.
212
+ """
213
+ import matplotlib
214
+ from matplotlib.figure import Figure
215
+
216
+ if isinstance(result, Figure) and "inline" in matplotlib.get_backend().lower():
217
+ import matplotlib.pyplot as plt
218
+
219
+ plt.close(result)
220
+ return result
202
221
 
203
222
 
204
223
  def project(
@@ -319,14 +338,16 @@ def project(
319
338
  )
320
339
  for m in methods
321
340
  ]
322
- return plotting_backend.project_grid(
323
- reduced_list,
324
- methods=methods,
325
- labels=labels_array,
326
- label_names=label_names,
327
- dimensions=dimensions,
328
- figsize=figsize,
329
- title=title,
341
+ return _display_once(
342
+ plotting_backend.project_grid(
343
+ reduced_list,
344
+ methods=methods,
345
+ labels=labels_array,
346
+ label_names=label_names,
347
+ dimensions=dimensions,
348
+ figsize=figsize,
349
+ title=title,
350
+ )
330
351
  )
331
352
 
332
353
  # Single method
@@ -347,12 +368,14 @@ def project(
347
368
  )
348
369
  dimensions = embeddings_array.shape[1] # type: ignore[assignment]
349
370
 
350
- return plotting_backend.project(
351
- embeddings_array,
352
- labels=labels_array,
353
- label_names=label_names,
354
- method=method or "custom",
355
- dimensions=dimensions,
356
- figsize=figsize,
357
- title=title,
371
+ return _display_once(
372
+ plotting_backend.project(
373
+ embeddings_array,
374
+ labels=labels_array,
375
+ label_names=label_names,
376
+ method=method or "custom",
377
+ dimensions=dimensions,
378
+ figsize=figsize,
379
+ title=title,
380
+ )
358
381
  )
@@ -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.11'
22
- __version_tuple__ = version_tuple = (0, 0, 11)
21
+ __version__ = version = '0.0.13'
22
+ __version_tuple__ = version_tuple = (0, 0, 13)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -167,8 +167,8 @@ def reduce_embeddings(
167
167
 
168
168
  # scikit-learn 1.8 renamed the non-metric switch from `metric` to `metric_mds`
169
169
  # -- `metric` now names the distance metric -- and moved `init` from fit() onto
170
- # the constructor. 1.8 requires Python >=3.11, so on the 3.10 leg the lockfile
171
- # necessarily resolves 1.7.x, where neither new spelling exists.
170
+ # the constructor. The projection extra still allows older releases, where neither
171
+ # new spelling exists.
172
172
  #
173
173
  # Ask the signature rather than compare version strings: it needs no extra
174
174
  # dependency and tests exactly the thing being depended on. Omitting `init` on
@@ -932,6 +932,61 @@ def _feasibility_band(
932
932
  )
933
933
 
934
934
 
935
+ # These mirror DataEval's performance/_feasibility.py: _SOURCES, _source_name, _unit_noun and FeasibilityOutput._bounds
936
+ _SOURCE_NAMES = {
937
+ "replica_ceiling/v1": "repeated labels",
938
+ "builtin_witness/v1:knn": "built-in k-NN",
939
+ "builtin_witness/v1:probe": "built-in linear probe",
940
+ "user_witness/v1": "your model",
941
+ }
942
+ _EVIDENCE_SCHEMA = {
943
+ "target_spec": pl.String,
944
+ "metric": pl.String,
945
+ "threshold": pl.Float64,
946
+ "source": pl.String,
947
+ "evidence_id": pl.String,
948
+ "information_scope": pl.String,
949
+ "unit": pl.String,
950
+ "bound_type": pl.String,
951
+ "observed": pl.Float64,
952
+ "bound": pl.Float64,
953
+ "n": pl.Int64,
954
+ "state": pl.String,
955
+ }
956
+
957
+
958
+ def _evidence_rows(output: PlottableFeasibility) -> pl.DataFrame:
959
+ """One row per endpoint on a target in ``determinations``, in their order, then the order of ``evidence``."""
960
+ targets = {r["target_spec"]: r for r in output.determinations.iter_rows(named=True)}
961
+ rows = []
962
+ for spec, target in targets.items():
963
+ for eid, ev in output.evidence.items():
964
+ for e in ev.endpoints:
965
+ if e.target_spec != spec:
966
+ continue
967
+ stats = ev.details["endpoints"][spec] or {}
968
+ rows.append(
969
+ {
970
+ "target_spec": spec,
971
+ "metric": target["metric"],
972
+ "threshold": target["threshold"],
973
+ "source": _GROUP_CEILING
974
+ if ev.kind.startswith("local_panel_ceiling/v1")
975
+ else _SOURCE_NAMES.get(eid, eid),
976
+ "evidence_id": eid,
977
+ "information_scope": ev.information_scope,
978
+ "unit": "sequence" if ev.unit == "group" else "image",
979
+ "bound_type": e.direction,
980
+ # A ceiling's mean is panel agreement, not an observed score
981
+ "observed": stats.get("mean") if e.direction == "lower" else None,
982
+ "bound": e.value,
983
+ "n": stats.get("n"),
984
+ "state": e.state,
985
+ }
986
+ )
987
+ return pl.DataFrame(rows, schema=_EVIDENCE_SCHEMA)
988
+
989
+
935
990
  def prepare_feasibility_data(output: PlottableFeasibility) -> list[FeasibilityBand]:
936
991
  """
937
992
  Prepare feasibility data for plotting across all backends.
@@ -955,7 +1010,7 @@ def prepare_feasibility_data(output: PlottableFeasibility) -> list[FeasibilityBa
955
1010
  spec = json.loads(row["target_spec"])
956
1011
  spec.pop("threshold", None)
957
1012
  groups.setdefault(json.dumps(spec, sort_keys=True), []).append(row)
958
- evidence = output.aggregate_by_evidence().to_dicts()
1013
+ evidence = _evidence_rows(output).to_dicts()
959
1014
  alpha = output.meta().state["alpha"]
960
1015
  noun = _unit(rows, evidence) or "image" # the printout's footer noun, for the whole call
961
1016
  bands = []
@@ -22,14 +22,16 @@ from datetime import datetime
22
22
  from typing import (
23
23
  Any,
24
24
  Literal,
25
+ NotRequired,
25
26
  Protocol,
27
+ Required,
26
28
  TypedDict,
27
29
  runtime_checkable,
28
30
  )
29
31
 
30
32
  import polars as pl
31
33
  from numpy.typing import NDArray
32
- from typing_extensions import NotRequired, ReadOnly, Required
34
+ from typing_extensions import ReadOnly
33
35
 
34
36
 
35
37
  class ExecutionMetadata(Protocol):
@@ -287,36 +289,55 @@ class PlottableDriftMVDC(Plottable, Protocol):
287
289
  def plot_type(self) -> Literal["drift_mvdc"]: ...
288
290
 
289
291
 
292
+ class FeasibilityEndpoint(Protocol):
293
+ """One bound on one target, as DataEval's ``Endpoint`` records it; only the fields the plot reads."""
294
+
295
+ @property
296
+ def target_spec(self) -> str: ...
297
+ @property
298
+ def value(self) -> float | None: ...
299
+ @property
300
+ def direction(self) -> Literal["lower", "upper"]: ...
301
+ @property
302
+ def state(self) -> str: ...
303
+
304
+
305
+ class FeasibilityEvidence(Protocol):
306
+ """One evidence item, as DataEval's ``Evidence`` records it; only the fields the plot reads."""
307
+
308
+ @property
309
+ def kind(self) -> str: ...
310
+ @property
311
+ def information_scope(self) -> str: ...
312
+ @property
313
+ def unit(self) -> str: ...
314
+ @property
315
+ def endpoints(self) -> Sequence[FeasibilityEndpoint]: ...
316
+ @property
317
+ def details(self) -> Mapping[str, Any]: ...
318
+
319
+
290
320
  @runtime_checkable
291
321
  class PlottableFeasibility(Plottable, Protocol):
292
322
  """Protocol for feasibility plot outputs.
293
323
 
294
324
  Required attributes:
295
325
  - determinations: One row per target (DataFrame)
296
- - aggregate_by_evidence(): One row per evidence item and target (DataFrame)
326
+ - evidence: Every evidence item of the call, by evidence id
297
327
  - meta().state["alpha"]: The call's error budget
298
328
  - plot_type -> 'feasibility'
299
329
 
300
- DataFrame structures:
330
+ Structures:
301
331
  - determinations columns: target_spec, metric, threshold, judged_scope, determination, ceiling,
302
332
  ceiling_scope, witness, witness_source
303
- - aggregate_by_evidence() columns: target_spec, metric, threshold, source, evidence_id,
304
- information_scope, unit, bound_type, observed, bound, n, state, alpha, basis
333
+ - each evidence item: kind, information_scope, unit (``item`` or ``group``), endpoints (target_spec, value,
334
+ direction, state) and details["endpoints"][target_spec], which is None or holds the bound's mean and n
305
335
  """
306
336
 
307
337
  @property
308
338
  def determinations(self) -> pl.DataFrame: ...
309
- def aggregate_by_evidence(self) -> pl.DataFrame:
310
- """Evidence as a polars DataFrame.
311
-
312
- Returns
313
- -------
314
- pl.DataFrame
315
- One row per evidence item and target: each source's bound on the target, and its observed score
316
- where one exists
317
- """
318
- ...
319
-
339
+ @property
340
+ def evidence(self) -> Mapping[str, FeasibilityEvidence]: ...
320
341
  @property
321
342
  def plot_type(self) -> Literal["feasibility"]: ...
322
343