dataeval-plots 0.0.13__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.13 → dataeval_plots-0.0.15}/PKG-INFO +40 -30
  2. {dataeval_plots-0.0.13 → dataeval_plots-0.0.15}/README.md +39 -29
  3. {dataeval_plots-0.0.13 → dataeval_plots-0.0.15}/src/dataeval_plots/_version.py +2 -2
  4. {dataeval_plots-0.0.13 → dataeval_plots-0.0.15}/src/dataeval_plots/backends/_feasibility_layout.py +4 -2
  5. {dataeval_plots-0.0.13 → dataeval_plots-0.0.15}/src/dataeval_plots/backends/_shared.py +100 -38
  6. {dataeval_plots-0.0.13 → dataeval_plots-0.0.15}/src/dataeval_plots/protocols.py +6 -1
  7. {dataeval_plots-0.0.13 → dataeval_plots-0.0.15}/.gitignore +0 -0
  8. {dataeval_plots-0.0.13 → dataeval_plots-0.0.15}/pyproject.toml +0 -0
  9. {dataeval_plots-0.0.13 → dataeval_plots-0.0.15}/src/dataeval_plots/__init__.py +0 -0
  10. {dataeval_plots-0.0.13 → dataeval_plots-0.0.15}/src/dataeval_plots/_registry.py +0 -0
  11. {dataeval_plots-0.0.13 → dataeval_plots-0.0.15}/src/dataeval_plots/backends/__init__.py +0 -0
  12. {dataeval_plots-0.0.13 → dataeval_plots-0.0.15}/src/dataeval_plots/backends/_altair.py +0 -0
  13. {dataeval_plots-0.0.13 → dataeval_plots-0.0.15}/src/dataeval_plots/backends/_base.py +0 -0
  14. {dataeval_plots-0.0.13 → dataeval_plots-0.0.15}/src/dataeval_plots/backends/_matplotlib.py +0 -0
  15. {dataeval_plots-0.0.13 → dataeval_plots-0.0.15}/src/dataeval_plots/backends/_plotly.py +0 -0
  16. {dataeval_plots-0.0.13 → dataeval_plots-0.0.15}/src/dataeval_plots/backends/_seaborn.py +0 -0
  17. {dataeval_plots-0.0.13 → 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.13
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/
@@ -208,60 +208,70 @@ subtitle that says which models the band judges:
208
208
 
209
209
  The axis is split into three zones. Each zone classifies the requirements that fall inside it:
210
210
 
211
- - **Demonstrated** runs from the axis minimum up to the lower confidence bound of the best tested model. The band
212
- reads "Requirements through *x*% are demonstrated".
211
+ - **Demonstrated** runs from the axis minimum up to the lower confidence bound of the best tested model, which the
212
+ label under the band names, for example "95.7% lower confidence bound". The zone reads "demonstrated".
213
213
  - **Unknown** lies between the other two zones. The evidence neither demonstrates nor rules out these requirements.
214
- - **Ruled out** runs from the upper confidence bound of the lowest applicable ceiling to the top of the axis. It holds
215
- only under the recorded assumptions, and the zone reads "ruled out under the recorded assumptions" wherever the
216
- label fits.
214
+ - **Ruled out** runs from the upper confidence bound of the lowest applicable ceiling to the top of the axis. The zone
215
+ reads "ruled out\*" wherever the label fits.
216
+
217
+ The `*` marks a footnote: "ruled out" holds only under the recorded assumptions. Wherever "ruled out\*" appears, the
218
+ caption under the same band ends with that footnote, "\* Ruled out under the recorded assumptions, at the stated
219
+ confidence."
217
220
 
218
221
  Each of your requirements is marked on the axis, and the marker states the determination in words:
219
222
 
220
223
  | Marker | Meaning |
221
224
  |--------|---------|
222
- | "95% requirement: demonstrated ✓" | The requirement is at or below the lower confidence bound |
223
- | "not enough evidence ?" | The requirement falls in the unknown zone, and the evidence cannot decide it |
224
- | "ruled out under the recorded assumptions ✗" | The requirement is above the upper confidence bound of a ceiling |
225
- | "not ruled out ?" | The requirement also falls in the unknown zone: it is not above any ceiling's upper confidence bound, and no tested model reaches it |
225
+ | "95%: demonstrated ✓" | The requirement is at or below the lower confidence bound |
226
+ | "95%: insufficient evidence ?" | The requirement falls in the unknown zone, and the evidence cannot decide it |
227
+ | "95%: ruled out\* ✗" | The requirement is above the upper confidence bound of a ceiling, under the recorded assumptions (see the footnote) |
228
+ | "95%: not ruled out ?" | The requirement also falls in the unknown zone: it is not above any ceiling's upper confidence bound, and no tested model reaches it |
226
229
 
227
230
  The caption under the band reports the observed score next to the lower confidence bound it supports, for example
228
- "Your model scored 91.0% on the 2,000 evaluation images. Allowing for sampling uncertainty gives a lower
229
- confidence bound of 88.5%." The sentence about the observed score appears only when an observed score exists. The
230
- caption then states that the bounds concern new images drawn from the same population, and that confidence holds
231
- jointly across the whole evaluation (for example "Confidence is 95% jointly across this evaluation").
231
+ "Your model scored 91.0% on 2,000 evaluation images; given sampling uncertainty, its lower confidence bound is
232
+ 88.5%." The sentence about the observed score appears only when an observed score exists. The caption then states
233
+ that the bounds apply to new images from the same population, and that confidence holds jointly across the whole
234
+ evaluation (for example "Bounds apply to new images from the same population, at 95% joint confidence.").
232
235
 
233
236
  Numbers read as DataEval prints them. Bounds are given to 0.1%, a lower bound rounded down and an upper bound rounded
234
237
  up, so rounding never overstates what the evidence shows. Requirements and confidence keep their own decimals, such as
235
- "95.5% requirement" or "Confidence is 97.5%".
238
+ "95.5%" or "at 97.5% joint confidence".
239
+
240
+ When a ceiling bounds the band, the caption ends with the footnote "\* Ruled out under the recorded assumptions, at
241
+ the stated confidence."
236
242
 
237
- When a ceiling bounds the band, the caption adds "Under the recorded assumptions, requirements above *x*% are ruled out
238
- at the stated confidence." A group-based ceiling carries the extra note that its validity argument awaits lead-scientist
239
- sign-off.
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%)".
240
248
 
241
249
  A ceiling that applies only to narrower models than the band judges never draws a zone. It appears as a note instead,
242
- for example that the group-based ceiling does not limit models that distinguish individual images.
250
+ for example "Group-based ceiling (72.1%) applies only to models that give one answer per group."
243
251
 
244
252
  ### Other outcomes
245
253
 
246
- - **Ruled out.** A requirement above the lowest applicable ceiling's upper confidence bound is marked "ruled out
247
- under the recorded assumptions ✗". The ruled-out zone covers everything above that bound, and the caption reads
248
- "Under the recorded assumptions, requirements above *x*% are ruled out at the stated confidence."
254
+ - **Ruled out.** A requirement above the lowest applicable ceiling's upper confidence bound is marked "ruled out\*
255
+ ✗". The ruled-out zone covers everything above that bound, which the label under the band names, for example
256
+ "93.0% upper confidence bound". The caption ends with the footnote "\* Ruled out under the recorded assumptions, at
257
+ the stated confidence."
249
258
  - **Evidence conflict.** When bounds from different sources contradict each other, the band draws no zones. A dashed
250
259
  warning outline reads "Evidence conflicts: check assumptions", both clashing bounds are marked, and the caption
251
- says that the bounds disagree and to check the assumptions, including whether evaluation images entered training.
252
- Even when the assumptions hold, the probability that any bound fails is at most the stated alpha (for example 5% at
253
- 95% confidence).
260
+ reads "Bounds disagree: check the assumptions, such as evaluation images entering training. Even when they hold,
261
+ the chance that any bound fails is at most 5%." The 5% is the stated alpha, at 95% confidence.
254
262
  - **Bounds at 0% or 100%.** A lower confidence bound of 0% demonstrates nothing and an upper confidence bound of 100%
255
263
  rules nothing out, so neither draws a zone.
256
264
 
257
265
  ### Showing the evidence
258
266
 
259
267
  `plot_evidence=True` replaces each band with one row per evidence source. Each row shows the observed score as a dot
260
- and the confidence bound as a tick, with a legend line explaining both: the dot is the score observed on the
261
- evaluation images, and the tick is the lower (or upper) confidence bound for new images from the same population.
262
- Evidence that does not apply to the judged models is drawn faint and labeled with the models it applies to. A source
263
- that produced no bound for the quantity is listed faint as "no bound available", and when no source has a bound the
264
- view says "No bound is available yet." instead of showing empty axes.
268
+ and the confidence bound as a tick, with the legend "● observed · ┃ lower confidence bound" (and "· ┃ upper confidence
269
+ bound" when a ceiling has one). The caption says what both mean: "Observed on the evaluation images; bounds apply to
270
+ new images from the same population, at 95% joint confidence." In a conflict, the conflict caption follows it.
271
+ Evidence that does not apply to the judged models is drawn faint, and its label names the models it applies to, for
272
+ example "Group-based ceiling (one answer per group)", so this view has no narrower-scope notes. A source that produced
273
+ no bound for the quantity is listed faint as "no bound available", and when no source has a bound the view says "No
274
+ bound is available yet." instead of showing empty axes.
265
275
 
266
276
  ## Extending the Package
267
277
 
@@ -162,60 +162,70 @@ subtitle that says which models the band judges:
162
162
 
163
163
  The axis is split into three zones. Each zone classifies the requirements that fall inside it:
164
164
 
165
- - **Demonstrated** runs from the axis minimum up to the lower confidence bound of the best tested model. The band
166
- reads "Requirements through *x*% are demonstrated".
165
+ - **Demonstrated** runs from the axis minimum up to the lower confidence bound of the best tested model, which the
166
+ label under the band names, for example "95.7% lower confidence bound". The zone reads "demonstrated".
167
167
  - **Unknown** lies between the other two zones. The evidence neither demonstrates nor rules out these requirements.
168
- - **Ruled out** runs from the upper confidence bound of the lowest applicable ceiling to the top of the axis. It holds
169
- only under the recorded assumptions, and the zone reads "ruled out under the recorded assumptions" wherever the
170
- label fits.
168
+ - **Ruled out** runs from the upper confidence bound of the lowest applicable ceiling to the top of the axis. The zone
169
+ reads "ruled out\*" wherever the label fits.
170
+
171
+ The `*` marks a footnote: "ruled out" holds only under the recorded assumptions. Wherever "ruled out\*" appears, the
172
+ caption under the same band ends with that footnote, "\* Ruled out under the recorded assumptions, at the stated
173
+ confidence."
171
174
 
172
175
  Each of your requirements is marked on the axis, and the marker states the determination in words:
173
176
 
174
177
  | Marker | Meaning |
175
178
  |--------|---------|
176
- | "95% requirement: demonstrated ✓" | The requirement is at or below the lower confidence bound |
177
- | "not enough evidence ?" | The requirement falls in the unknown zone, and the evidence cannot decide it |
178
- | "ruled out under the recorded assumptions ✗" | The requirement is above the upper confidence bound of a ceiling |
179
- | "not ruled out ?" | The requirement also falls in the unknown zone: it is not above any ceiling's upper confidence bound, and no tested model reaches it |
179
+ | "95%: demonstrated ✓" | The requirement is at or below the lower confidence bound |
180
+ | "95%: insufficient evidence ?" | The requirement falls in the unknown zone, and the evidence cannot decide it |
181
+ | "95%: ruled out\* ✗" | The requirement is above the upper confidence bound of a ceiling, under the recorded assumptions (see the footnote) |
182
+ | "95%: not ruled out ?" | The requirement also falls in the unknown zone: it is not above any ceiling's upper confidence bound, and no tested model reaches it |
180
183
 
181
184
  The caption under the band reports the observed score next to the lower confidence bound it supports, for example
182
- "Your model scored 91.0% on the 2,000 evaluation images. Allowing for sampling uncertainty gives a lower
183
- confidence bound of 88.5%." The sentence about the observed score appears only when an observed score exists. The
184
- caption then states that the bounds concern new images drawn from the same population, and that confidence holds
185
- jointly across the whole evaluation (for example "Confidence is 95% jointly across this evaluation").
185
+ "Your model scored 91.0% on 2,000 evaluation images; given sampling uncertainty, its lower confidence bound is
186
+ 88.5%." The sentence about the observed score appears only when an observed score exists. The caption then states
187
+ that the bounds apply to new images from the same population, and that confidence holds jointly across the whole
188
+ evaluation (for example "Bounds apply to new images from the same population, at 95% joint confidence.").
186
189
 
187
190
  Numbers read as DataEval prints them. Bounds are given to 0.1%, a lower bound rounded down and an upper bound rounded
188
191
  up, so rounding never overstates what the evidence shows. Requirements and confidence keep their own decimals, such as
189
- "95.5% requirement" or "Confidence is 97.5%".
192
+ "95.5%" or "at 97.5% joint confidence".
193
+
194
+ When a ceiling bounds the band, the caption ends with the footnote "\* Ruled out under the recorded assumptions, at
195
+ the stated confidence."
190
196
 
191
- When a ceiling bounds the band, the caption adds "Under the recorded assumptions, requirements above *x*% are ruled out
192
- at the stated confidence." A group-based ceiling carries the extra note that its validity argument awaits lead-scientist
193
- sign-off.
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%)".
194
202
 
195
203
  A ceiling that applies only to narrower models than the band judges never draws a zone. It appears as a note instead,
196
- for example that the group-based ceiling does not limit models that distinguish individual images.
204
+ for example "Group-based ceiling (72.1%) applies only to models that give one answer per group."
197
205
 
198
206
  ### Other outcomes
199
207
 
200
- - **Ruled out.** A requirement above the lowest applicable ceiling's upper confidence bound is marked "ruled out
201
- under the recorded assumptions ✗". The ruled-out zone covers everything above that bound, and the caption reads
202
- "Under the recorded assumptions, requirements above *x*% are ruled out at the stated confidence."
208
+ - **Ruled out.** A requirement above the lowest applicable ceiling's upper confidence bound is marked "ruled out\*
209
+ ✗". The ruled-out zone covers everything above that bound, which the label under the band names, for example
210
+ "93.0% upper confidence bound". The caption ends with the footnote "\* Ruled out under the recorded assumptions, at
211
+ the stated confidence."
203
212
  - **Evidence conflict.** When bounds from different sources contradict each other, the band draws no zones. A dashed
204
213
  warning outline reads "Evidence conflicts: check assumptions", both clashing bounds are marked, and the caption
205
- says that the bounds disagree and to check the assumptions, including whether evaluation images entered training.
206
- Even when the assumptions hold, the probability that any bound fails is at most the stated alpha (for example 5% at
207
- 95% confidence).
214
+ reads "Bounds disagree: check the assumptions, such as evaluation images entering training. Even when they hold,
215
+ the chance that any bound fails is at most 5%." The 5% is the stated alpha, at 95% confidence.
208
216
  - **Bounds at 0% or 100%.** A lower confidence bound of 0% demonstrates nothing and an upper confidence bound of 100%
209
217
  rules nothing out, so neither draws a zone.
210
218
 
211
219
  ### Showing the evidence
212
220
 
213
221
  `plot_evidence=True` replaces each band with one row per evidence source. Each row shows the observed score as a dot
214
- and the confidence bound as a tick, with a legend line explaining both: the dot is the score observed on the
215
- evaluation images, and the tick is the lower (or upper) confidence bound for new images from the same population.
216
- Evidence that does not apply to the judged models is drawn faint and labeled with the models it applies to. A source
217
- that produced no bound for the quantity is listed faint as "no bound available", and when no source has a bound the
218
- view says "No bound is available yet." instead of showing empty axes.
222
+ and the confidence bound as a tick, with the legend "● observed · ┃ lower confidence bound" (and "· ┃ upper confidence
223
+ bound" when a ceiling has one). The caption says what both mean: "Observed on the evaluation images; bounds apply to
224
+ new images from the same population, at 95% joint confidence." In a conflict, the conflict caption follows it.
225
+ Evidence that does not apply to the judged models is drawn faint, and its label names the models it applies to, for
226
+ example "Group-based ceiling (one answer per group)", so this view has no narrower-scope notes. A source that produced
227
+ no bound for the quantity is listed faint as "no bound available", and when no source has a bound the view says "No
228
+ bound is available yet." instead of showing empty axes.
219
229
 
220
230
  ## Extending the Package
221
231
 
@@ -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.13'
22
- __version_tuple__ = version_tuple = (0, 0, 13)
21
+ __version__ = version = '0.0.15'
22
+ __version_tuple__ = version_tuple = (0, 0, 15)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -48,7 +48,8 @@ def text_width(text: str, size: float = LABEL_PT) -> float:
48
48
 
49
49
  def wrap(text: str, size: float, width: float) -> str:
50
50
  """Wrap to the figure's text width, never leaving a legend glyph at a line end."""
51
- glued = text.replace("● ", "●\xa0").replace("┃ ", "┃\xa0") # textwrap breaks only at ASCII whitespace
51
+ # textwrap breaks only at ASCII whitespace; a footnote's "*" stays with its first word
52
+ glued = text.replace("● ", "●\xa0").replace("┃ ", "┃\xa0").replace("* ", "*\xa0")
52
53
  return textwrap.fill(glued, int((width - 2 * MARGIN) * 72 / (size * 0.55))).replace("\xa0", " ")
53
54
 
54
55
 
@@ -114,7 +115,8 @@ def blocks(bands: list[FeasibilityBand], plot_evidence: bool, width: float) -> t
114
115
  ax_h = body + (max((p[3] for p in placed), default=-1) + 1) * LEVEL + 0.1
115
116
  # Below the axes: the evidence legend, the caption paragraph, then the notes, each wrapped
116
117
  texts = [(band.evidence_legend, 10.0, muted)] if plot_evidence else []
117
- texts += [(" ".join(band.caption), CAPTION_PT, ink), *((note, 10.0, muted) for note in band.notes)]
118
+ caption, notes = (band.evidence_caption, []) if plot_evidence else (band.caption, band.notes)
119
+ texts += [(" ".join(caption), CAPTION_PT, ink), *((note, 10.0, muted) for note in notes)]
118
120
  wrapped = [(wrap(text, size, width), size, color) for text, size, color in texts]
119
121
  out.append((band, x_range, xlabel, ax_h, placed, wrapped))
120
122
  height += TITLE + ax_h + XAXIS + sum(text_height(text) for text, _, _ in wrapped) + GAP
@@ -684,16 +684,20 @@ _SCOPE_MODELS = {
684
684
  "fixed_representation": "models restricted to the embeddings",
685
685
  "partition_restricted": "models that give one answer per group",
686
686
  }
687
+ _SCOPE_ROW = {"fixed_representation": "embeddings only", "partition_restricted": "one answer per group"}
688
+ # "ruled out*" never stands alone: its "*" footnote in the same band's caption says it holds only under the recorded
689
+ # assumptions, and an infeasible row always has a ceiling, which writes that footnote
687
690
  _MARKER_WORDS = {
688
691
  "demonstrated_feasible": "demonstrated ✓",
689
- "unresolved": "not enough evidence ?",
690
- "infeasible_under_recorded_assumptions": "ruled out under the recorded assumptions ✗",
692
+ "unresolved": "insufficient evidence ?",
693
+ "infeasible_under_recorded_assumptions": "ruled out* ✗",
691
694
  "not_ruled_out": "not ruled out ?",
692
695
  }
693
- _RULED_OUT = "ruled out under the recorded assumptions" # never "ruled out" alone: it holds only under them
696
+ _RULED_OUT = "ruled out*"
694
697
  _AGGREGATION_WORDS = {None: "", "sequence_mean": "per-sequence mean ", "frame_pooled": "frame-pooled "}
695
698
  _GROUP_CEILING = "group-based ceiling"
696
699
  _NO_BOUND = "no bound available"
700
+ _FALSE_POSITIVE = "false_positive_rate"
697
701
 
698
702
 
699
703
  def _pct(value: float, direction: str | None = None) -> str:
@@ -709,6 +713,11 @@ def _pct(value: float, direction: str | None = None) -> str:
709
713
  return f"{tenths / 10:.1f}%"
710
714
 
711
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
+
712
721
  def _percent(value: float) -> str:
713
722
  """A threshold, confidence or alpha as DataEval prints it: no rounding to whole percents, no trailing zeros."""
714
723
  return f"{value * 100:g}%"
@@ -750,6 +759,7 @@ class FeasibilityBand:
750
759
  evidence_markers: list[tuple[float, str]] # requirement labels in the evidence view
751
760
  caption: list[str]
752
761
  notes: list[str]
762
+ evidence_caption: list[str] # the evidence view's caption, in place of the caption and notes
753
763
  evidence: list[FeasibilityEvidenceRow]
754
764
  evidence_legend: str
755
765
  evidence_note: str | None # drawn in the evidence view when no source has a bound for the band
@@ -765,6 +775,16 @@ def _feasibility_range(values: Iterable[float]) -> tuple[float, float]:
765
775
  return round(max(low, 0.0), 2), 1.0
766
776
 
767
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
+
768
788
  def _feasibility_evidence(
769
789
  evidence: list[dict[str, Any]], scope: str, unit: str
770
790
  ) -> tuple[list[FeasibilityEvidenceRow], list[str]]:
@@ -788,7 +808,7 @@ def _feasibility_evidence(
788
808
  rank, judged = _SCOPE_RANK[info], _SCOPE_RANK[scope]
789
809
  restricted = rank < judged if kind == "upper" else rank > judged
790
810
  models = _SCOPE_MODELS[info].format(unit=unit)
791
- label = _sentence_case(e["source"]) + (f" ({models})" if restricted else "")
811
+ label = _sentence_case(e["source"]) + (f" ({_SCOPE_ROW.get(info, models)})" if restricted else "")
792
812
  observed = None if bound is None else e["observed"] # an observed score alone bounds nothing
793
813
  rows.append(
794
814
  FeasibilityEvidenceRow(
@@ -798,19 +818,18 @@ def _feasibility_evidence(
798
818
  observed=observed,
799
819
  restricted=restricted,
800
820
  label=label,
801
- 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),
802
822
  observed_label=None if observed is None else f"{_pct(observed)} observed",
803
823
  bound_left=kind == "lower",
804
- faint=restricted or bound is None,
824
+ faint=restricted or bound is None or e["uncertified"],
805
825
  )
806
826
  )
807
827
  if restricted and kind == "upper" and bound is not None:
808
828
  ceiling = _pct(bound, "upper")
809
829
  notes.append(
810
- f"The group-based ceiling ({ceiling}) does not limit models that distinguish individual {unit}s."
830
+ f"Group-based ceiling ({ceiling}) applies only to models that give one answer per group."
811
831
  if info == "partition_restricted"
812
- else f"The {e['source']} ({ceiling}) applies only to {models}; "
813
- "it does not limit the models judged here."
832
+ else f"{_sentence_case(e['source'])} ({ceiling}) applies only to {models}."
814
833
  )
815
834
  return rows, list(dict.fromkeys(notes))
816
835
 
@@ -846,18 +865,29 @@ def _unit(rows: list[dict[str, Any]], evidence: list[dict[str, Any]]) -> str | N
846
865
  return None
847
866
 
848
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
+
849
873
  def _feasibility_title(spec: dict[str, Any]) -> str:
850
874
  """The axis title, naming the AP class (or macro) and any aggregation so per-class bands differ."""
851
875
  metric = spec["metric"]
852
876
  if metric == "ap":
853
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)"
854
881
  return "Required " + _AGGREGATION_WORDS[spec.get("aggregation")] + metric
855
882
 
856
883
 
857
884
  def _feasibility_band(
858
- 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
859
886
  ) -> FeasibilityBand:
860
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]
861
891
  scope = rows[0]["judged_scope"]
862
892
  unit = _unit(rows, evidence) or "image"
863
893
  # A lower bound of 0% demonstrates nothing and an upper bound of 100% rules nothing out
@@ -880,37 +910,56 @@ def _feasibility_band(
880
910
  markers = []
881
911
  for r in sorted(rows, key=lambda r: r["threshold"]):
882
912
  words = _MARKER_WORDS.get(r["determination"])
883
- markers.append((r["threshold"], f"{_percent(r['threshold'])} requirement" + (f": {words}" if words else "")))
913
+ markers.append((r["threshold"], _percent(r["threshold"]) + (f": {words}" if words else "")))
884
914
 
915
+ conflict_caption = (
916
+ f"Bounds disagree: check the assumptions, such as evaluation {unit}s entering training. "
917
+ f"Even when they hold, the chance that any bound fails is at most {_confidence(alpha, 'upper')}."
918
+ )
885
919
  if conflict:
886
- caption = [
887
- f"The bounds disagree. Check the assumptions, including whether evaluation {unit}s entered training. "
888
- f"Even when assumptions hold, the probability that any bound fails is at most {_percent(alpha)}."
889
- ]
920
+ caption = [conflict_caption]
890
921
  else:
891
922
  caption = []
892
923
  witness = None if best is None else _deciding(best, evidence)[0]
893
924
  if witness and witness["observed"] is not None and w is not None:
894
- on = "" if witness["n"] is None else f"{witness['n']:,} "
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"
895
931
  caption.append(
896
- f"{_sentence_case(witness['source'])} scored {_pct(witness['observed'])} on the {on}evaluation "
897
- f"{unit}s. Allowing for sampling uncertainty gives a lower confidence bound of {_pct(w, 'lower')}."
932
+ f"{_sentence_case(witness['source'])} scored {scored}; given sampling uncertainty, its lower "
933
+ f"confidence bound is {_pct(w, 'lower')}."
898
934
  )
899
- if deciding is not None and c is not None:
900
- ceiling = _deciding(deciding, evidence)[1]
901
- sign_off = ceiling is not None and ceiling["source"] == _GROUP_CEILING
902
- caption.append(
903
- f"Under the recorded assumptions, requirements above {_pct(c, 'upper')} are ruled out at the stated "
904
- "confidence"
905
- + (" (group-based ceiling; its validity argument awaits lead-scientist sign-off)" if sign_off else "")
906
- + "."
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,
907
946
  )
908
- caption.append(
909
- f"Bounds concern new {noun}s drawn from the same population. "
910
- f"Confidence is {_percent(1 - alpha)} jointly across this evaluation."
911
- )
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.")
952
+ # The "*" footnote to every "ruled out*" goes last, as a footnote does
953
+ if deciding is not None and c is not None:
954
+ caption.append("* Ruled out under the recorded assumptions, at the stated confidence.")
955
+ evidence_caption = [
956
+ f"Observed on the evaluation {unit}s; bounds apply to new {unit}s from the same population, "
957
+ f"at {_confidence(1 - alpha, 'lower')} joint confidence."
958
+ ]
959
+ if conflict:
960
+ evidence_caption.append(conflict_caption)
912
961
 
913
- legend = f"● observed on the evaluation {unit}s · ┃ lower confidence bound for new {unit}s from the same population"
962
+ legend = "● observed · ┃ lower confidence bound"
914
963
  if any(r.bound_type == "upper" and r.bound is not None for r in ev_rows):
915
964
  legend += " · ┃ upper confidence bound"
916
965
 
@@ -926,6 +975,7 @@ def _feasibility_band(
926
975
  evidence_markers=[(t, f"{_percent(t)} required") for t, _ in markers],
927
976
  caption=caption,
928
977
  notes=notes,
978
+ evidence_caption=evidence_caption,
929
979
  evidence=ev_rows,
930
980
  evidence_legend=legend,
931
981
  evidence_note=None if any(r.bound is not None for r in ev_rows) else "No bound is available yet.",
@@ -952,6 +1002,10 @@ _EVIDENCE_SCHEMA = {
952
1002
  "bound": pl.Float64,
953
1003
  "n": pl.Int64,
954
1004
  "state": pl.String,
1005
+ "quantity": pl.String,
1006
+ "uncertified": pl.Boolean,
1007
+ "false_positive_bound": pl.Float64,
1008
+ "false_positive_limit": pl.Float64,
955
1009
  }
956
1010
 
957
1011
 
@@ -961,10 +1015,14 @@ def _evidence_rows(output: PlottableFeasibility) -> pl.DataFrame:
961
1015
  rows = []
962
1016
  for spec, target in targets.items():
963
1017
  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 {}
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)
968
1026
  rows.append(
969
1027
  {
970
1028
  "target_spec": spec,
@@ -978,10 +1036,14 @@ def _evidence_rows(output: PlottableFeasibility) -> pl.DataFrame:
978
1036
  "unit": "sequence" if ev.unit == "group" else "image",
979
1037
  "bound_type": e.direction,
980
1038
  # A ceiling's mean is panel agreement, not an observed score
981
- "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,
982
1040
  "bound": e.value,
983
1041
  "n": stats.get("n"),
984
1042
  "state": e.state,
1043
+ "quantity": q,
1044
+ "uncertified": uncertified,
1045
+ "false_positive_bound": fp,
1046
+ "false_positive_limit": f0,
985
1047
  }
986
1048
  )
987
1049
  return pl.DataFrame(rows, schema=_EVIDENCE_SCHEMA)
@@ -1012,7 +1074,7 @@ def prepare_feasibility_data(output: PlottableFeasibility) -> list[FeasibilityBa
1012
1074
  groups.setdefault(json.dumps(spec, sort_keys=True), []).append(row)
1013
1075
  evidence = _evidence_rows(output).to_dicts()
1014
1076
  alpha = output.meta().state["alpha"]
1015
- 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
1016
1078
  bands = []
1017
1079
  for band in groups.values():
1018
1080
  specs = {r["target_spec"] for r in band}
@@ -1042,7 +1104,7 @@ def feasibility_zones(band: FeasibilityBand) -> list[tuple[str, float, float, st
1042
1104
  end = hi if band.ruled_out is None else band.ruled_out
1043
1105
  zones = [("unknown", start, end, "unknown")]
1044
1106
  if band.demonstrated is not None:
1045
- zones.insert(0, ("demonstrated", lo, start, f"Requirements through {_pct(start, 'lower')} are demonstrated"))
1107
+ zones.insert(0, ("demonstrated", lo, start, "demonstrated"))
1046
1108
  if band.ruled_out is not None:
1047
1109
  zones.append(("ruled_out", end, hi, _RULED_OUT))
1048
1110
  return [zone for zone in zones if zone[2] > zone[1]] # a zero-width zone has nothing to show
@@ -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: ...