dataeval-plots 0.0.14__tar.gz → 0.0.15__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.14 → dataeval_plots-0.0.15}/PKG-INFO +8 -2
  2. {dataeval_plots-0.0.14 → dataeval_plots-0.0.15}/README.md +7 -1
  3. {dataeval_plots-0.0.14 → dataeval_plots-0.0.15}/src/dataeval_plots/_version.py +2 -2
  4. {dataeval_plots-0.0.14 → dataeval_plots-0.0.15}/src/dataeval_plots/backends/_shared.py +75 -23
  5. {dataeval_plots-0.0.14 → dataeval_plots-0.0.15}/src/dataeval_plots/protocols.py +6 -1
  6. {dataeval_plots-0.0.14 → dataeval_plots-0.0.15}/.gitignore +0 -0
  7. {dataeval_plots-0.0.14 → dataeval_plots-0.0.15}/pyproject.toml +0 -0
  8. {dataeval_plots-0.0.14 → dataeval_plots-0.0.15}/src/dataeval_plots/__init__.py +0 -0
  9. {dataeval_plots-0.0.14 → dataeval_plots-0.0.15}/src/dataeval_plots/_registry.py +0 -0
  10. {dataeval_plots-0.0.14 → dataeval_plots-0.0.15}/src/dataeval_plots/backends/__init__.py +0 -0
  11. {dataeval_plots-0.0.14 → dataeval_plots-0.0.15}/src/dataeval_plots/backends/_altair.py +0 -0
  12. {dataeval_plots-0.0.14 → dataeval_plots-0.0.15}/src/dataeval_plots/backends/_base.py +0 -0
  13. {dataeval_plots-0.0.14 → dataeval_plots-0.0.15}/src/dataeval_plots/backends/_feasibility_layout.py +0 -0
  14. {dataeval_plots-0.0.14 → dataeval_plots-0.0.15}/src/dataeval_plots/backends/_matplotlib.py +0 -0
  15. {dataeval_plots-0.0.14 → dataeval_plots-0.0.15}/src/dataeval_plots/backends/_plotly.py +0 -0
  16. {dataeval_plots-0.0.14 → dataeval_plots-0.0.15}/src/dataeval_plots/backends/_seaborn.py +0 -0
  17. {dataeval_plots-0.0.14 → dataeval_plots-0.0.15}/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.14
3
+ Version: 0.0.15
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/
@@ -238,7 +238,13 @@ up, so rounding never overstates what the evidence shows. Requirements and confi
238
238
  "95.5%" or "at 97.5% joint confidence".
239
239
 
240
240
  When a ceiling bounds the band, the caption ends with the footnote "\* Ruled out under the recorded assumptions, at
241
- the stated confidence." A group-based ceiling adds "(group-based ceiling; validity pending lead-scientist sign-off)".
241
+ the stated confidence."
242
+
243
+ A recall requirement is titled "Required recall for Tank (at most 5% false positives)". Only a witness whose own upper
244
+ confidence bound on false positives is within that limit counts; the caption adds its bound after the observed
245
+ sentence, for example "Its upper confidence bound on false positives is 4.1% (observed 2.6% of 651 other images)." In
246
+ the evidence view, a witness over the limit keeps its recall bound and observed score but is drawn faint, its label
247
+ saying why it is not counted, for example "66.0% lower bound · not counted: false positives ≤8.3% (limit 5%)".
242
248
 
243
249
  A ceiling that applies only to narrower models than the band judges never draws a zone. It appears as a note instead,
244
250
  for example "Group-based ceiling (72.1%) applies only to models that give one answer per group."
@@ -192,7 +192,13 @@ up, so rounding never overstates what the evidence shows. Requirements and confi
192
192
  "95.5%" or "at 97.5% joint confidence".
193
193
 
194
194
  When a ceiling bounds the band, the caption ends with the footnote "\* Ruled out under the recorded assumptions, at
195
- the stated confidence." A group-based ceiling adds "(group-based ceiling; validity pending lead-scientist sign-off)".
195
+ the stated confidence."
196
+
197
+ A recall requirement is titled "Required recall for Tank (at most 5% false positives)". Only a witness whose own upper
198
+ confidence bound on false positives is within that limit counts; the caption adds its bound after the observed
199
+ sentence, for example "Its upper confidence bound on false positives is 4.1% (observed 2.6% of 651 other images)." In
200
+ the evidence view, a witness over the limit keeps its recall bound and observed score but is drawn faint, its label
201
+ saying why it is not counted, for example "66.0% lower bound · not counted: false positives ≤8.3% (limit 5%)".
196
202
 
197
203
  A ceiling that applies only to narrower models than the band judges never draws a zone. It appears as a note instead,
198
204
  for example "Group-based ceiling (72.1%) applies only to models that give one answer per group."
@@ -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.14'
22
- __version_tuple__ = version_tuple = (0, 0, 14)
21
+ __version__ = version = '0.0.15'
22
+ __version_tuple__ = version_tuple = (0, 0, 15)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -697,6 +697,7 @@ _RULED_OUT = "ruled out*"
697
697
  _AGGREGATION_WORDS = {None: "", "sequence_mean": "per-sequence mean ", "frame_pooled": "frame-pooled "}
698
698
  _GROUP_CEILING = "group-based ceiling"
699
699
  _NO_BOUND = "no bound available"
700
+ _FALSE_POSITIVE = "false_positive_rate"
700
701
 
701
702
 
702
703
  def _pct(value: float, direction: str | None = None) -> str:
@@ -712,6 +713,11 @@ def _pct(value: float, direction: str | None = None) -> str:
712
713
  return f"{tenths / 10:.1f}%"
713
714
 
714
715
 
716
+ def _confidence(value: float, direction: str) -> str:
717
+ """Round a confidence down or an error share up to 0.1%, dropping a trailing ".0", as the printout does."""
718
+ return _pct(value, direction).replace(".0%", "%")
719
+
720
+
715
721
  def _percent(value: float) -> str:
716
722
  """A threshold, confidence or alpha as DataEval prints it: no rounding to whole percents, no trailing zeros."""
717
723
  return f"{value * 100:g}%"
@@ -769,6 +775,16 @@ def _feasibility_range(values: Iterable[float]) -> tuple[float, float]:
769
775
  return round(max(low, 0.0), 2), 1.0
770
776
 
771
777
 
778
+ def _not_counted(e: dict[str, Any]) -> str:
779
+ """The label suffix of a recall witness that does not count: its false-positive bound is over the limit or none."""
780
+ if not e["uncertified"]:
781
+ return ""
782
+ fp = e["false_positive_bound"]
783
+ seen = "no false-positive bound" if fp is None else f"false positives ≤{_pct(fp, 'upper')}"
784
+ limit = "" if fp is None else f" (limit {_percent(e['false_positive_limit'])})"
785
+ return f" · not counted: {seen}{limit}"
786
+
787
+
772
788
  def _feasibility_evidence(
773
789
  evidence: list[dict[str, Any]], scope: str, unit: str
774
790
  ) -> tuple[list[FeasibilityEvidenceRow], list[str]]:
@@ -802,10 +818,10 @@ def _feasibility_evidence(
802
818
  observed=observed,
803
819
  restricted=restricted,
804
820
  label=label,
805
- bound_label=_NO_BOUND if bound is None else f"{_pct(bound, kind)} {kind} bound",
821
+ bound_label=_NO_BOUND if bound is None else f"{_pct(bound, kind)} {kind} bound" + _not_counted(e),
806
822
  observed_label=None if observed is None else f"{_pct(observed)} observed",
807
823
  bound_left=kind == "lower",
808
- faint=restricted or bound is None,
824
+ faint=restricted or bound is None or e["uncertified"],
809
825
  )
810
826
  )
811
827
  if restricted and kind == "upper" and bound is not None:
@@ -849,18 +865,29 @@ def _unit(rows: list[dict[str, Any]], evidence: list[dict[str, Any]]) -> str | N
849
865
  return None
850
866
 
851
867
 
868
+ def _other(fp: dict[str, Any]) -> str:
869
+ """The audit negatives a false-positive bound was measured on, such as ``651 other images``."""
870
+ return ("" if fp["n"] is None else f"{fp['n']:,} ") + f"other {fp['unit']}s"
871
+
872
+
852
873
  def _feasibility_title(spec: dict[str, Any]) -> str:
853
874
  """The axis title, naming the AP class (or macro) and any aggregation so per-class bands differ."""
854
875
  metric = spec["metric"]
855
876
  if metric == "ap":
856
877
  metric = "macro AP" if spec.get("class_name") is None else f"AP for class {spec['class_name']}"
878
+ elif metric == "recall":
879
+ f0 = _percent(spec["max_false_positive_rate"])
880
+ metric = f"recall for {spec['class_name']} (at most {f0} false positives)"
857
881
  return "Required " + _AGGREGATION_WORDS[spec.get("aggregation")] + metric
858
882
 
859
883
 
860
884
  def _feasibility_band(
861
- rows: list[dict[str, Any]], evidence: list[dict[str, Any]], alpha: float, noun: str
885
+ rows: list[dict[str, Any]], all_evidence: list[dict[str, Any]], alpha: float, noun: str
862
886
  ) -> FeasibilityBand:
863
887
  """One band; ``noun`` is the unit the printout's footer names for the whole call."""
888
+ # Bands and evidence rows draw the metric; a recall witness's false-positive bound only joins the caption
889
+ evidence = [e for e in all_evidence if e["quantity"] == "metric"]
890
+ false_positives = [e for e in all_evidence if e["quantity"] == _FALSE_POSITIVE]
864
891
  scope = rows[0]["judged_scope"]
865
892
  unit = _unit(rows, evidence) or "image"
866
893
  # A lower bound of 0% demonstrates nothing and an upper bound of 100% rules nothing out
@@ -887,7 +914,7 @@ def _feasibility_band(
887
914
 
888
915
  conflict_caption = (
889
916
  f"Bounds disagree: check the assumptions, such as evaluation {unit}s entering training. "
890
- f"Even when they hold, the chance that any bound fails is at most {_percent(alpha)}."
917
+ f"Even when they hold, the chance that any bound fails is at most {_confidence(alpha, 'upper')}."
891
918
  )
892
919
  if conflict:
893
920
  caption = [conflict_caption]
@@ -896,25 +923,38 @@ def _feasibility_band(
896
923
  witness = None if best is None else _deciding(best, evidence)[0]
897
924
  if witness and witness["observed"] is not None and w is not None:
898
925
  on = "the " if witness["n"] is None else f"{witness['n']:,} "
926
+ # A recall witness's count is the class's positives, which the printout names by class
927
+ spec = json.loads(rows[0]["target_spec"])
928
+ scored = f"{_pct(witness['observed'])} on {on}evaluation {unit}s"
929
+ if spec["metric"] == "recall":
930
+ scored = f"{_pct(witness['observed'])} recall on {on}{spec['class_name']} {unit}s"
899
931
  caption.append(
900
- f"{_sentence_case(witness['source'])} scored {_pct(witness['observed'])} on {on}evaluation "
901
- f"{unit}s; given sampling uncertainty, its lower confidence bound is {_pct(w, 'lower')}."
932
+ f"{_sentence_case(witness['source'])} scored {scored}; given sampling uncertainty, its lower "
933
+ f"confidence bound is {_pct(w, 'lower')}."
902
934
  )
903
- caption.append(
904
- f"Bounds apply to new {noun}s from the same population, at {_percent(1 - alpha)} joint confidence."
905
- )
935
+ fp = None
936
+ if witness and w is not None:
937
+ fp = next(
938
+ (
939
+ e
940
+ for e in false_positives
941
+ if e["evidence_id"] == witness["evidence_id"]
942
+ and e["target_spec"] == witness["target_spec"]
943
+ and e["bound"] is not None
944
+ ),
945
+ None,
946
+ )
947
+ if fp is not None:
948
+ seen = "" if fp["observed"] is None else f" (observed {_pct(fp['observed'])} of {_other(fp)})"
949
+ caption.append(f"Its upper confidence bound on false positives is {_pct(fp['bound'], 'upper')}{seen}.")
950
+ joint = _confidence(1 - alpha, "lower")
951
+ caption.append(f"Bounds apply to new {noun}s from the same population, at {joint} joint confidence.")
906
952
  # The "*" footnote to every "ruled out*" goes last, as a footnote does
907
953
  if deciding is not None and c is not None:
908
- ceiling = _deciding(deciding, evidence)[1]
909
- sign_off = ceiling is not None and ceiling["source"] == _GROUP_CEILING
910
- caption.append(
911
- "* Ruled out under the recorded assumptions, at the stated confidence"
912
- + (" (group-based ceiling; validity pending lead-scientist sign-off)" if sign_off else "")
913
- + "."
914
- )
954
+ caption.append("* Ruled out under the recorded assumptions, at the stated confidence.")
915
955
  evidence_caption = [
916
956
  f"Observed on the evaluation {unit}s; bounds apply to new {unit}s from the same population, "
917
- f"at {_percent(1 - alpha)} joint confidence."
957
+ f"at {_confidence(1 - alpha, 'lower')} joint confidence."
918
958
  ]
919
959
  if conflict:
920
960
  evidence_caption.append(conflict_caption)
@@ -962,6 +1002,10 @@ _EVIDENCE_SCHEMA = {
962
1002
  "bound": pl.Float64,
963
1003
  "n": pl.Int64,
964
1004
  "state": pl.String,
1005
+ "quantity": pl.String,
1006
+ "uncertified": pl.Boolean,
1007
+ "false_positive_bound": pl.Float64,
1008
+ "false_positive_limit": pl.Float64,
965
1009
  }
966
1010
 
967
1011
 
@@ -971,10 +1015,14 @@ def _evidence_rows(output: PlottableFeasibility) -> pl.DataFrame:
971
1015
  rows = []
972
1016
  for spec, target in targets.items():
973
1017
  for eid, ev in output.evidence.items():
974
- for e in ev.endpoints:
975
- if e.target_spec != spec:
976
- continue
977
- stats = ev.details["endpoints"][spec] or {}
1018
+ # An older DataEval has no quantity; its endpoints all bound the metric
1019
+ mine = [(e, getattr(e, "quantity", "metric")) for e in ev.endpoints if e.target_spec == spec]
1020
+ fp = next((e.value for e, q in mine if q == _FALSE_POSITIVE), None)
1021
+ f0 = json.loads(spec).get("max_false_positive_rate")
1022
+ for e, q in mine:
1023
+ stats = ev.details["endpoints"].get(spec if q == "metric" else f"{spec}#{q}") or {}
1024
+ # A recall witness counts only when its own false-positive upper bound is within the limit
1025
+ uncertified = q == "metric" and e.direction == "lower" and f0 is not None and (fp is None or fp > f0)
978
1026
  rows.append(
979
1027
  {
980
1028
  "target_spec": spec,
@@ -988,10 +1036,14 @@ def _evidence_rows(output: PlottableFeasibility) -> pl.DataFrame:
988
1036
  "unit": "sequence" if ev.unit == "group" else "image",
989
1037
  "bound_type": e.direction,
990
1038
  # A ceiling's mean is panel agreement, not an observed score
991
- "observed": stats.get("mean") if e.direction == "lower" else None,
1039
+ "observed": stats.get("mean") if e.direction == "lower" or q == _FALSE_POSITIVE else None,
992
1040
  "bound": e.value,
993
1041
  "n": stats.get("n"),
994
1042
  "state": e.state,
1043
+ "quantity": q,
1044
+ "uncertified": uncertified,
1045
+ "false_positive_bound": fp,
1046
+ "false_positive_limit": f0,
995
1047
  }
996
1048
  )
997
1049
  return pl.DataFrame(rows, schema=_EVIDENCE_SCHEMA)
@@ -1022,7 +1074,7 @@ def prepare_feasibility_data(output: PlottableFeasibility) -> list[FeasibilityBa
1022
1074
  groups.setdefault(json.dumps(spec, sort_keys=True), []).append(row)
1023
1075
  evidence = _evidence_rows(output).to_dicts()
1024
1076
  alpha = output.meta().state["alpha"]
1025
- noun = _unit(rows, evidence) or "image" # the printout's footer noun, for the whole call
1077
+ noun = _unit(rows, [e for e in evidence if e["quantity"] == "metric"]) or "image" # the printout's footer noun
1026
1078
  bands = []
1027
1079
  for band in groups.values():
1028
1080
  specs = {r["target_spec"] for r in band}
@@ -290,7 +290,12 @@ class PlottableDriftMVDC(Plottable, Protocol):
290
290
 
291
291
 
292
292
  class FeasibilityEndpoint(Protocol):
293
- """One bound on one target, as DataEval's ``Endpoint`` records it; only the fields the plot reads."""
293
+ """
294
+ One bound on one target, as DataEval's ``Endpoint`` records it; only the fields the plot reads.
295
+
296
+ Newer outputs also give ``quantity``, ``"metric"`` or ``"false_positive_rate"``; the plot reads it with a default
297
+ of ``"metric"`` so older outputs still plot.
298
+ """
294
299
 
295
300
  @property
296
301
  def target_spec(self) -> str: ...