dataeval-plots 0.0.12__tar.gz → 0.0.14__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.12 → dataeval_plots-0.0.14}/PKG-INFO +34 -30
  2. {dataeval_plots-0.0.12 → dataeval_plots-0.0.14}/README.md +33 -29
  3. {dataeval_plots-0.0.12 → dataeval_plots-0.0.14}/src/dataeval_plots/__init__.py +40 -17
  4. {dataeval_plots-0.0.12 → dataeval_plots-0.0.14}/src/dataeval_plots/_version.py +2 -2
  5. {dataeval_plots-0.0.12 → dataeval_plots-0.0.14}/src/dataeval_plots/backends/_feasibility_layout.py +4 -2
  6. {dataeval_plots-0.0.12 → dataeval_plots-0.0.14}/src/dataeval_plots/backends/_shared.py +34 -24
  7. {dataeval_plots-0.0.12 → dataeval_plots-0.0.14}/.gitignore +0 -0
  8. {dataeval_plots-0.0.12 → dataeval_plots-0.0.14}/pyproject.toml +0 -0
  9. {dataeval_plots-0.0.12 → dataeval_plots-0.0.14}/src/dataeval_plots/_registry.py +0 -0
  10. {dataeval_plots-0.0.12 → dataeval_plots-0.0.14}/src/dataeval_plots/backends/__init__.py +0 -0
  11. {dataeval_plots-0.0.12 → dataeval_plots-0.0.14}/src/dataeval_plots/backends/_altair.py +0 -0
  12. {dataeval_plots-0.0.12 → dataeval_plots-0.0.14}/src/dataeval_plots/backends/_base.py +0 -0
  13. {dataeval_plots-0.0.12 → dataeval_plots-0.0.14}/src/dataeval_plots/backends/_matplotlib.py +0 -0
  14. {dataeval_plots-0.0.12 → dataeval_plots-0.0.14}/src/dataeval_plots/backends/_plotly.py +0 -0
  15. {dataeval_plots-0.0.12 → dataeval_plots-0.0.14}/src/dataeval_plots/backends/_seaborn.py +0 -0
  16. {dataeval_plots-0.0.12 → dataeval_plots-0.0.14}/src/dataeval_plots/protocols.py +0 -0
  17. {dataeval_plots-0.0.12 → dataeval_plots-0.0.14}/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.12
3
+ Version: 0.0.14
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,64 @@ 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".
236
239
 
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.
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)".
240
242
 
241
243
  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.
244
+ for example "Group-based ceiling (72.1%) applies only to models that give one answer per group."
243
245
 
244
246
  ### Other outcomes
245
247
 
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."
248
+ - **Ruled out.** A requirement above the lowest applicable ceiling's upper confidence bound is marked "ruled out\*
249
+ ✗". The ruled-out zone covers everything above that bound, which the label under the band names, for example
250
+ "93.0% upper confidence bound". The caption ends with the footnote "\* Ruled out under the recorded assumptions, at
251
+ the stated confidence."
249
252
  - **Evidence conflict.** When bounds from different sources contradict each other, the band draws no zones. A dashed
250
253
  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).
254
+ reads "Bounds disagree: check the assumptions, such as evaluation images entering training. Even when they hold,
255
+ the chance that any bound fails is at most 5%." The 5% is the stated alpha, at 95% confidence.
254
256
  - **Bounds at 0% or 100%.** A lower confidence bound of 0% demonstrates nothing and an upper confidence bound of 100%
255
257
  rules nothing out, so neither draws a zone.
256
258
 
257
259
  ### Showing the evidence
258
260
 
259
261
  `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.
262
+ and the confidence bound as a tick, with the legend "● observed · ┃ lower confidence bound" (and "· ┃ upper confidence
263
+ bound" when a ceiling has one). The caption says what both mean: "Observed on the evaluation images; bounds apply to
264
+ new images from the same population, at 95% joint confidence." In a conflict, the conflict caption follows it.
265
+ Evidence that does not apply to the judged models is drawn faint, and its label names the models it applies to, for
266
+ example "Group-based ceiling (one answer per group)", so this view has no narrower-scope notes. A source that produced
267
+ no bound for the quantity is listed faint as "no bound available", and when no source has a bound the view says "No
268
+ bound is available yet." instead of showing empty axes.
265
269
 
266
270
  ## Extending the Package
267
271
 
@@ -162,60 +162,64 @@ 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".
190
193
 
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.
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)".
194
196
 
195
197
  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.
198
+ for example "Group-based ceiling (72.1%) applies only to models that give one answer per group."
197
199
 
198
200
  ### Other outcomes
199
201
 
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."
202
+ - **Ruled out.** A requirement above the lowest applicable ceiling's upper confidence bound is marked "ruled out\*
203
+ ✗". The ruled-out zone covers everything above that bound, which the label under the band names, for example
204
+ "93.0% upper confidence bound". The caption ends with the footnote "\* Ruled out under the recorded assumptions, at
205
+ the stated confidence."
203
206
  - **Evidence conflict.** When bounds from different sources contradict each other, the band draws no zones. A dashed
204
207
  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).
208
+ reads "Bounds disagree: check the assumptions, such as evaluation images entering training. Even when they hold,
209
+ the chance that any bound fails is at most 5%." The 5% is the stated alpha, at 95% confidence.
208
210
  - **Bounds at 0% or 100%.** A lower confidence bound of 0% demonstrates nothing and an upper confidence bound of 100%
209
211
  rules nothing out, so neither draws a zone.
210
212
 
211
213
  ### Showing the evidence
212
214
 
213
215
  `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.
216
+ and the confidence bound as a tick, with the legend "● observed · ┃ lower confidence bound" (and "· ┃ upper confidence
217
+ bound" when a ceiling has one). The caption says what both mean: "Observed on the evaluation images; bounds apply to
218
+ new images from the same population, at 95% joint confidence." In a conflict, the conflict caption follows it.
219
+ Evidence that does not apply to the judged models is drawn faint, and its label names the models it applies to, for
220
+ example "Group-based ceiling (one answer per group)", so this view has no narrower-scope notes. A source that produced
221
+ no bound for the quantity is listed faint as "no bound available", and when no source has a bound the view says "No
222
+ bound is available yet." instead of showing empty axes.
219
223
 
220
224
  ## Extending the Package
221
225
 
@@ -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.12'
22
- __version_tuple__ = version_tuple = (0, 0, 12)
21
+ __version__ = version = '0.0.14'
22
+ __version_tuple__ = version_tuple = (0, 0, 14)
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,13 +684,16 @@ _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"
@@ -750,6 +753,7 @@ class FeasibilityBand:
750
753
  evidence_markers: list[tuple[float, str]] # requirement labels in the evidence view
751
754
  caption: list[str]
752
755
  notes: list[str]
756
+ evidence_caption: list[str] # the evidence view's caption, in place of the caption and notes
753
757
  evidence: list[FeasibilityEvidenceRow]
754
758
  evidence_legend: str
755
759
  evidence_note: str | None # drawn in the evidence view when no source has a bound for the band
@@ -788,7 +792,7 @@ def _feasibility_evidence(
788
792
  rank, judged = _SCOPE_RANK[info], _SCOPE_RANK[scope]
789
793
  restricted = rank < judged if kind == "upper" else rank > judged
790
794
  models = _SCOPE_MODELS[info].format(unit=unit)
791
- label = _sentence_case(e["source"]) + (f" ({models})" if restricted else "")
795
+ label = _sentence_case(e["source"]) + (f" ({_SCOPE_ROW.get(info, models)})" if restricted else "")
792
796
  observed = None if bound is None else e["observed"] # an observed score alone bounds nothing
793
797
  rows.append(
794
798
  FeasibilityEvidenceRow(
@@ -807,10 +811,9 @@ def _feasibility_evidence(
807
811
  if restricted and kind == "upper" and bound is not None:
808
812
  ceiling = _pct(bound, "upper")
809
813
  notes.append(
810
- f"The group-based ceiling ({ceiling}) does not limit models that distinguish individual {unit}s."
814
+ f"Group-based ceiling ({ceiling}) applies only to models that give one answer per group."
811
815
  if info == "partition_restricted"
812
- else f"The {e['source']} ({ceiling}) applies only to {models}; "
813
- "it does not limit the models judged here."
816
+ else f"{_sentence_case(e['source'])} ({ceiling}) applies only to {models}."
814
817
  )
815
818
  return rows, list(dict.fromkeys(notes))
816
819
 
@@ -880,37 +883,43 @@ def _feasibility_band(
880
883
  markers = []
881
884
  for r in sorted(rows, key=lambda r: r["threshold"]):
882
885
  words = _MARKER_WORDS.get(r["determination"])
883
- markers.append((r["threshold"], f"{_percent(r['threshold'])} requirement" + (f": {words}" if words else "")))
886
+ markers.append((r["threshold"], _percent(r["threshold"]) + (f": {words}" if words else "")))
884
887
 
888
+ conflict_caption = (
889
+ 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)}."
891
+ )
885
892
  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
- ]
893
+ caption = [conflict_caption]
890
894
  else:
891
895
  caption = []
892
896
  witness = None if best is None else _deciding(best, evidence)[0]
893
897
  if witness and witness["observed"] is not None and w is not None:
894
- on = "" if witness["n"] is None else f"{witness['n']:,} "
898
+ on = "the " if witness["n"] is None else f"{witness['n']:,} "
895
899
  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')}."
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')}."
898
902
  )
903
+ caption.append(
904
+ f"Bounds apply to new {noun}s from the same population, at {_percent(1 - alpha)} joint confidence."
905
+ )
906
+ # The "*" footnote to every "ruled out*" goes last, as a footnote does
899
907
  if deciding is not None and c is not None:
900
908
  ceiling = _deciding(deciding, evidence)[1]
901
909
  sign_off = ceiling is not None and ceiling["source"] == _GROUP_CEILING
902
910
  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 "")
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 "")
906
913
  + "."
907
914
  )
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
- )
915
+ evidence_caption = [
916
+ 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."
918
+ ]
919
+ if conflict:
920
+ evidence_caption.append(conflict_caption)
912
921
 
913
- legend = f"● observed on the evaluation {unit}s · ┃ lower confidence bound for new {unit}s from the same population"
922
+ legend = "● observed · ┃ lower confidence bound"
914
923
  if any(r.bound_type == "upper" and r.bound is not None for r in ev_rows):
915
924
  legend += " · ┃ upper confidence bound"
916
925
 
@@ -926,6 +935,7 @@ def _feasibility_band(
926
935
  evidence_markers=[(t, f"{_percent(t)} required") for t, _ in markers],
927
936
  caption=caption,
928
937
  notes=notes,
938
+ evidence_caption=evidence_caption,
929
939
  evidence=ev_rows,
930
940
  evidence_legend=legend,
931
941
  evidence_note=None if any(r.bound is not None for r in ev_rows) else "No bound is available yet.",
@@ -1042,7 +1052,7 @@ def feasibility_zones(band: FeasibilityBand) -> list[tuple[str, float, float, st
1042
1052
  end = hi if band.ruled_out is None else band.ruled_out
1043
1053
  zones = [("unknown", start, end, "unknown")]
1044
1054
  if band.demonstrated is not None:
1045
- zones.insert(0, ("demonstrated", lo, start, f"Requirements through {_pct(start, 'lower')} are demonstrated"))
1055
+ zones.insert(0, ("demonstrated", lo, start, "demonstrated"))
1046
1056
  if band.ruled_out is not None:
1047
1057
  zones.append(("ruled_out", end, hi, _RULED_OUT))
1048
1058
  return [zone for zone in zones if zone[2] > zone[1]] # a zero-width zone has nothing to show