Skip to content

Commit 79d449c

Browse files
author
miranov25
committed
Phase 13.42.DF FIX2: close 5 items deferred at FIX1.
B6: suptitle padding adapts to title line count (per v1.2 §4) B7: faceted+fit no-crash smoke verify (per v1.2 §4, cascade from B2) I-8: hist + weights + fit emits UserWarning (per v1.2 §8 B5(c) commitment) ADV-1: stacked + selection_vector(>1) + fit raises NotImplementedError (per v1.2 §8 D9(d) commitment) ADV-3: fit_textbox_kwargs in 3 FORWARDED_NAMES tuples + outer DFDraw signatures + explicit forwarding sites (per Sonnet55 v1.2 P2-2; also satisfies Phase 13.43 v1.2 §9 carry-forward checklist) Closes the items wrongly deferred at FIX1. See companion governance memo Claude48_Feedback_FIX1_Defer_Anti_Pattern_20260527.md for the process gap analysis and proposed QRC AliceO2Group#11 + Reviewer supplement. Tests: 6 new (F.59, F.60, F.61, F.61b, F.62, F.63) in TestPhase1342FIX2Regressions. Taxonomy staged in-commit (FIT.inlTestPhase1342FIX2Regressions. Taxonomy staged in-commit (F0 / 1.
1 parent e463161 commit 79d449c

6 files changed

Lines changed: 307 additions & 15 deletions

File tree

UTILS/dfextensions/dfdraw/docs/CAPABILITY_MATRIX.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Capability Matrix — dfdraw
22

3-
**Generated:** 2026-05-27 06:53 UTC
3+
**Generated:** 2026-05-27 11:43 UTC
44
**Phase:** 13.15.DF
55
**Generator:** `scripts/generate_capability_matrix.py`
66
**Sources:** `tests/feature_taxonomy.py` + `tests/test_layer_classification.py`
@@ -14,8 +14,8 @@
1414
| 🧨 Broken | 0 | 0% |
1515
| 📋 Planned | 1 | 1% |
1616
| **Total features** | **108** | |
17-
| **Total proof tests** | **536** | |
18-
| **Invariance tests** | **307** | |
17+
| **Total proof tests** | **542** | |
18+
| **Invariance tests** | **313** | |
1919

2020
**Status key:**
2121
- ✅ Verified — has at least one invariance test (A ≡ B check)
@@ -172,7 +172,7 @@
172172
| | **FACET** | | |
173173
| ✅ | **FACET.list_grid** — facet_by accepts Union[str, List[str]] for 1D/2D/3D faceting. Convention LOCKED matching numpy/pandas (n_rows, n_cols, ...) shape: facet_by[0]=ROW (vertical within figure), facet_by[1]=COLUMN (horizontal within figure), facet_by[2]=FIGID (separate figures, one per value). facet_by[3+] raises NotImplementedError. 3D returns (List[Figure], List[axes_2d], List[stats_dict]) — DEVIATES from standard (fig, ax, stats) contract; documented prominently in inline help. New params: share_x/share_y ∈ {'all','row','col','none'} (within-figure axis sharing), share_across_figures: bool (3D global range lock). Per-plot-kind lock for share_across_figures (CP1-2): scatter locks x AND y; hist/profile locks x only (y auto-scales per figure to handle sparse-figID variance). New helpers: _normalize_facet_args, _to_mpl_share (symmetric {'all':True,'row':'row','col':'col','none':False} — v1.2 CP0-1 fix for Hard Constraint #3), _validate_share_axis_value, _resolve_facet_values (discrete or pd.cut/qcut Interval), _filter_facet_value (CP1-3 discrete vs binned), _compute_global_ranges. dfdraw is FIRST major plotting library with unified API where Nth faceting dimension generates separate figures (seaborn/ggplot2/plotly/altair all require manual loops). _validate_facet_by_binning guard for list input (v1.3 P1-A). Per-plot-kind dispatch: hist uses range= (matplotlib convention); profile uses range= which DFDraw.profile remaps to draw_profile's x_range= internally; scatter uses ax.set_xlim/set_ylim post-draw (no native range params); hist also locks ax.set_xlim post-draw (range= only locks bins, not axis xlim). Empty cell handling: '(no data)' diagnostic + stats={'n':0,'empty':True} — Phase 13.41.DF | 23 | 0 |
174174
| | **FIT** | | |
175-
|| **FIT.inline** — Inline fits (fit= parameter on hist/profile/scatter/draw) | 35 | 0 |
175+
|| **FIT.inline** — Inline fits (fit= parameter on hist/profile/scatter/draw) | 41 | 0 |
176176

177177
---
178178

UTILS/dfextensions/dfdraw/drawer.py

Lines changed: 99 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,26 @@ def _filter_facet_value(df, col, value, bins=None, quantiles=None):
209209
return df[mask.fillna(False) if mask.dtype == object else mask]
210210

211211

212+
def _suptitle_top_for_title(title_text):
213+
"""Phase 13.42.DF FIX2 (B6): compute a reasonable subplots_adjust top= value
214+
based on suptitle line count, so multi-line titles or dense facet grids
215+
don't overlap subplot titles.
216+
217+
Production-gate B6 finding (Phase 13.42 close): single-line auto_title +
218+
long-facet-tuple subtitle frequently overlapped the row-0 subplot titles
219+
when facet_by was 2D. The fixed top=0.92 reserved 8% for the suptitle
220+
block, which is insufficient for 2-3 line titles.
221+
222+
Returns top fraction in [0.84, 0.94] depending on the number of newlines
223+
in title_text. None → falls back to 0.92 (existing behavior).
224+
"""
225+
if not title_text:
226+
return 0.92
227+
n_lines = str(title_text).count('\n') + 1
228+
# 1 line → 0.93; 2 lines → 0.89; 3 lines → 0.86; 4+ → 0.84
229+
return max(0.84, 0.93 - 0.035 * max(0, n_lines - 1))
230+
231+
212232
def _compute_global_ranges(df, x_expr, y_expr, plot_kind):
213233
"""For 3D share_across_figures=True, compute global x/y ranges.
214234
@@ -839,6 +859,11 @@ def _auto_label(self, y_expr, x_expr=None):
839859
'time_format',
840860
# Phase 13.42.DF: Inline fits
841861
'fit',
862+
# Phase 13.42.DF FIX2 (ADV-3, Sonnet55 P2-2 carry-forward): per-call
863+
# fit textbox formatting overrides. Pattern B (forwarded inward):
864+
# draw_profile() has fit_textbox_kwargs as an explicit parameter
865+
# since FIX1 and consumes it via render_fit_textbox.
866+
'fit_textbox_kwargs',
842867
)
843868

844869
_HIST_FORWARDED_NAMES = (
@@ -886,6 +911,9 @@ def _auto_label(self, y_expr, x_expr=None):
886911
'cumulative',
887912
# Phase 13.42.DF: Inline fits
888913
'fit',
914+
# Phase 13.42.DF FIX2 (ADV-3, Sonnet55 P2-2 carry-forward): per-call
915+
# fit textbox formatting overrides. See _PROFILE_FORWARDED_NAMES note.
916+
'fit_textbox_kwargs',
889917
)
890918

891919
_SCATTER_FORWARDED_NAMES = (
@@ -910,9 +938,10 @@ def _auto_label(self, y_expr, x_expr=None):
910938
'vector_compose', 'delta_facet',
911939
# Phase 13.42.DF: Inline fits
912940
'fit',
941+
# Phase 13.42.DF FIX2 (ADV-3, Sonnet55 P2-2 carry-forward): per-call
942+
# fit textbox formatting overrides. See _PROFILE_FORWARDED_NAMES note.
943+
'fit_textbox_kwargs',
913944
)
914-
915-
# Phase 13.32.DF Sub-fix 3: hist2d gets its own FORWARDED_NAMES tuple
916945
# (previously absent — hist2d used inline kwargs handling).
917946
_HIST2D_FORWARDED_NAMES = (
918947
'selection', 'sample', 'bins', 'range', 'norm', 'stats',
@@ -2108,7 +2137,7 @@ def _dispatch_normalize_render(
21082137
f"{_main}\n{_sub}" if _sub else _main,
21092138
fontsize=get_style_value("axes.titlesize", 14),
21102139
)
2111-
plt.subplots_adjust(top=0.92)
2140+
plt.subplots_adjust(top=_suptitle_top_for_title(fig._suptitle.get_text() if getattr(fig, '_suptitle', None) else None))
21122141
except Exception:
21132142
# Failsafe — same defensive pattern as BUG-002 fix.
21142143
_y_str = (y_list if isinstance(y_list, str)
@@ -2117,7 +2146,7 @@ def _dispatch_normalize_render(
21172146
else str(x_list[0] if x_list else ''))
21182147
fig.suptitle(f"{_y_str} vs {_x_str}",
21192148
fontsize=get_style_value("axes.titlesize", 14))
2120-
plt.subplots_adjust(top=0.92)
2149+
plt.subplots_adjust(top=_suptitle_top_for_title(fig._suptitle.get_text() if getattr(fig, '_suptitle', None) else None))
21212150

21222151
# --- 9. Build stats dict (M1 scope per v1.1 §7) ------------------------
21232152
# Drop profile_data from user-facing stats — internal-only.
@@ -2451,15 +2480,15 @@ def _dispatch_normalize_grouped_render(
24512480
f"{_main}\n{_sub}" if _sub else _main,
24522481
fontsize=get_style_value("axes.titlesize", 14),
24532482
)
2454-
plt.subplots_adjust(top=0.92)
2483+
plt.subplots_adjust(top=_suptitle_top_for_title(fig._suptitle.get_text() if getattr(fig, '_suptitle', None) else None))
24552484
except Exception:
24562485
_y_str = (y_list if isinstance(y_list, str)
24572486
else f"[{','.join(map(str, y_list))}]")
24582487
_x_str = (x_list if isinstance(x_list, str)
24592488
else str(x_list[0] if x_list else ''))
24602489
fig.suptitle(f"{_y_str} vs {_x_str}",
24612490
fontsize=get_style_value("axes.titlesize", 14))
2462-
plt.subplots_adjust(top=0.92)
2491+
plt.subplots_adjust(top=_suptitle_top_for_title(fig._suptitle.get_text() if getattr(fig, '_suptitle', None) else None))
24632492

24642493
# --- 11. Build stats dict (M2 grouped contract) -----------------------
24652494
stats_dict: Dict[str, Any] = {
@@ -2757,15 +2786,15 @@ def _dispatch_normalize_faceted_render(
27572786
f"{_main}\n{_sub}" if _sub else _main,
27582787
fontsize=get_style_value("axes.titlesize", 14),
27592788
)
2760-
plt.subplots_adjust(top=0.92)
2789+
plt.subplots_adjust(top=_suptitle_top_for_title(fig._suptitle.get_text() if getattr(fig, '_suptitle', None) else None))
27612790
except Exception:
27622791
_y_str = (y_list if isinstance(y_list, str)
27632792
else f"[{','.join(map(str, y_list))}]")
27642793
_x_str = (x_list if isinstance(x_list, str)
27652794
else str(x_list[0] if x_list else ''))
27662795
fig.suptitle(f"{_y_str} vs {_x_str}",
27672796
fontsize=get_style_value("axes.titlesize", 14))
2768-
plt.subplots_adjust(top=0.92)
2797+
plt.subplots_adjust(top=_suptitle_top_for_title(fig._suptitle.get_text() if getattr(fig, '_suptitle', None) else None))
27692798

27702799
# --- 8. Stats dict ----------------------------------------------------
27712800
stats_dict: Dict[str, Any] = {
@@ -3352,7 +3381,7 @@ def _dispatch_faceted_render(
33523381
f"{_main}\n{_sub}" if _sub else _main,
33533382
fontsize=get_style_value("axes.titlesize", 14),
33543383
)
3355-
plt.subplots_adjust(top=0.92)
3384+
plt.subplots_adjust(top=_suptitle_top_for_title(fig._suptitle.get_text() if getattr(fig, '_suptitle', None) else None))
33563385
except Exception:
33573386
# Failsafe: minimal title — prevents auto_title import/build
33583387
# errors from crashing the plot. Same defensive pattern as
@@ -3361,11 +3390,11 @@ def _dispatch_faceted_render(
33613390
else f"[{','.join(y_expr)}]")
33623391
fig.suptitle(f"{y_str} vs {x_expr}",
33633392
fontsize=get_style_value("axes.titlesize", 14))
3364-
plt.subplots_adjust(top=0.92)
3393+
plt.subplots_adjust(top=_suptitle_top_for_title(fig._suptitle.get_text() if getattr(fig, '_suptitle', None) else None))
33653394

33663395
plt.tight_layout()
33673396
if title:
3368-
plt.subplots_adjust(top=0.92)
3397+
plt.subplots_adjust(top=_suptitle_top_for_title(fig._suptitle.get_text() if getattr(fig, '_suptitle', None) else None))
33693398

33703399
# ---- Combined stats ------------------------------------------------
33713400
combined_stats = {
@@ -3682,6 +3711,11 @@ def draw(
36823711
normalize_layout: str = "overlay+diff",
36833712
# Phase 13.42.DF: Inline fit specification (architect 2026-05-22).
36843713
fit: Optional[Union[str, Dict, Callable, List]] = None,
3714+
# Phase 13.42.DF FIX2 (ADV-3, Sonnet55 P2-2 carry-forward):
3715+
# per-call fit textbox formatting overrides. Pattern B —
3716+
# forwarded inward; consumed by inner draw_hist/profile/scatter
3717+
# via render_fit_textbox.
3718+
fit_textbox_kwargs: Optional[Dict] = None,
36853719
**kwargs
36863720
) -> DrawResult:
36873721
"""
@@ -3950,6 +3984,11 @@ def hist(
39503984
linestyle_cycle: bool = False,
39513985
# Phase 13.42.DF: Inline fit specification
39523986
fit: Optional[Union[str, Dict, Callable, List]] = None,
3987+
# Phase 13.42.DF FIX2 (ADV-3, Sonnet55 P2-2 carry-forward):
3988+
# per-call fit textbox formatting overrides. Pattern B —
3989+
# forwarded inward; consumed by inner draw_hist/profile/scatter
3990+
# via render_fit_textbox.
3991+
fit_textbox_kwargs: Optional[Dict] = None,
39533992
**kwargs
39543993
) -> DrawResult:
39553994
"""
@@ -4006,6 +4045,37 @@ def hist(
40064045
# Parse expression (take first part only for 1D)
40074046
y_expr, x_expr = self._parse_expr(expr)
40084047

4048+
# Phase 13.42.DF FIX2 (ADV-1, Sonnet55 v1.2 panel finding, v1.2 §8
4049+
# D9(d) commitment): stacked=True + selection_vector (>1 selection)
4050+
# + fit= has ambiguous semantics in the current architecture (does
4051+
# each selection get its own stack? overlay without sum? fit per
4052+
# selection or per-stack-component or on the stacked total?).
4053+
# Architect did not ratify any of these in v1.2. Until a concrete
4054+
# use case drives the design, raise NotImplementedError with a clear
4055+
# actionable suggestion so users don't get silently wrong output.
4056+
# Placed BEFORE vector dispatch so the 3-axis-inner length-equality
4057+
# validator inside _compute_vector_iteration_indices does not pre-empt
4058+
# this more-specific architectural error.
4059+
# Note: `stacked` is forwarded via **kwargs on DFDraw.hist (not a
4060+
# named outer param), so we read it from kwargs.
4061+
if (kwargs.get('stacked') is True
4062+
and selection_vector is not None and len(selection_vector) > 1
4063+
and fit is not None):
4064+
raise NotImplementedError(
4065+
"Phase 13.42.DF FIX2 (ADV-1): the combination "
4066+
"stacked=True + selection_vector (>1 selection) + fit= is not "
4067+
"supported. Selection-vector semantics for stacked-hist fits "
4068+
"were not ratified in Phase 13.42 (v1.2 §8 D9(d) — coder did "
4069+
"not specify which of {per-selection fit, per-stack-component "
4070+
"fit, fit on the stacked total} applies). "
4071+
"Fix options: "
4072+
"(a) drop stacked=True (per-group fits work via D9/R4); or "
4073+
"(b) loop over selections in user code with separate hist() "
4074+
"calls; or "
4075+
"(c) drop fit= and use Phase 13.43 summary_fit= when "
4076+
"available."
4077+
)
4078+
40094079
# Phase 13.16.DF: Vector dispatch
40104080
# Phase 13.16.DF FIX1: tuple-driven forwarding via _HIST_FORWARDED_NAMES
40114081
# + R4 fail-fast guard on facet=True + vector.
@@ -4154,6 +4224,9 @@ def hist(
41544224
cumulative=cumulative,
41554225
# Phase 13.42.DF: inline fits — recursive forwarding through facet
41564226
fit=fit,
4227+
# Phase 13.42.DF FIX2 (ADV-3): fit_textbox_kwargs paired
4228+
# with fit= (R6 validator now requires explicit forwarding).
4229+
fit_textbox_kwargs=fit_textbox_kwargs,
41574230
**kwargs
41584231
)
41594232
# Facet mode (legacy path, same=True ignored in facet mode)
@@ -4200,6 +4273,7 @@ def hist(
42004273
cumulative=cumulative,
42014274
# Phase 13.42.DF: inline fits (QRC v1.32 #6 recursive forwarding)
42024275
fit=fit,
4276+
fit_textbox_kwargs=fit_textbox_kwargs,
42034277
**kwargs
42044278
)
42054279
axes = ax
@@ -4275,6 +4349,11 @@ def scatter(
42754349
delta_facet: Optional[str] = None,
42764350
# Phase 13.42.DF: Inline fit specification
42774351
fit: Optional[Union[str, Dict, Callable, List]] = None,
4352+
# Phase 13.42.DF FIX2 (ADV-3, Sonnet55 P2-2 carry-forward):
4353+
# per-call fit textbox formatting overrides. Pattern B —
4354+
# forwarded inward; consumed by inner draw_hist/profile/scatter
4355+
# via render_fit_textbox.
4356+
fit_textbox_kwargs: Optional[Dict] = None,
42784357
**kwargs
42794358
) -> DrawResult:
42804359
"""
@@ -4477,6 +4556,7 @@ def scatter(
44774556
share_across_figures=share_across_figures,
44784557
# Phase 13.42.DF: inline fits
44794558
fit=fit,
4559+
fit_textbox_kwargs=fit_textbox_kwargs,
44804560
**kwargs
44814561
)
44824562
# Facet mode (legacy path, same=True ignored in facet mode)
@@ -4506,6 +4586,7 @@ def scatter(
45064586
time_format=time_format,
45074587
# Phase 13.42.DF: inline fits
45084588
fit=fit,
4589+
fit_textbox_kwargs=fit_textbox_kwargs,
45094590
**kwargs
45104591
)
45114592
axes = ax
@@ -4607,6 +4688,11 @@ def profile(
46074688
linestyle_cycle: bool = False,
46084689
# Phase 13.42.DF: Inline fit specification
46094690
fit: Optional[Union[str, Dict, Callable, List]] = None,
4691+
# Phase 13.42.DF FIX2 (ADV-3, Sonnet55 P2-2 carry-forward):
4692+
# per-call fit textbox formatting overrides. Pattern B —
4693+
# forwarded inward; consumed by inner draw_hist/profile/scatter
4694+
# via render_fit_textbox.
4695+
fit_textbox_kwargs: Optional[Dict] = None,
46104696
**kwargs
46114697
) -> DrawResult:
46124698
"""
@@ -5114,6 +5200,7 @@ def profile(
51145200
share_across_figures=share_across_figures,
51155201
# Phase 13.42.DF: inline fits
51165202
fit=fit,
5203+
fit_textbox_kwargs=fit_textbox_kwargs,
51175204
**kwargs
51185205
)
51195206
else:
@@ -5146,6 +5233,7 @@ def profile(
51465233
time_format=time_format,
51475234
# Phase 13.42.DF: inline fits
51485235
fit=fit,
5236+
fit_textbox_kwargs=fit_textbox_kwargs,
51495237
**kwargs
51505238
)
51515239
axes = ax

UTILS/dfextensions/dfdraw/plots/histogram.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -763,6 +763,28 @@ def draw_hist(
763763
# → curve_fit minimizes Σresid² as if yerr=1; reported χ² scales with N²).
764764
# `hist_errors` now controls *display* errorbars only, NOT fit yerr.
765765
# max(counts, 1) matches ROOT precisely for empty bins.
766+
#
767+
# Phase 13.42.DF FIX2 (I-8, Sonnet53_R2 v1.0 panel finding, v1.2 §8
768+
# B5(c) commitment): when user has weights= AND fit=, χ² uses
769+
# sqrt(counts) (UNWEIGHTED Neyman) not sqrt(Σw²) (weighted Poisson).
770+
# The errorbar display path (Phase 13.37 CP1-6) DOES use Σw², but
771+
# the fit path does not — that's a known limitation. Emit a
772+
# one-time UserWarning so analysts choosing weighted hist for QA
773+
# know the reported chi² treats weights as if they were counts.
774+
if _hist_weights is not None:
775+
import warnings as _warnings
776+
_warnings.warn(
777+
"Phase 13.42.DF FIX2 (I-8): fit= combined with weights= "
778+
"uses sqrt(counts) Neyman errors, NOT sqrt(Σw²) weighted "
779+
"Poisson. The reported χ²/ndf treats weighted bin contents "
780+
"as if they were raw counts; if you need the weighted "
781+
"Poisson formula, compute it manually from stats['fit'] "
782+
"params and a separate np.histogram(weights=w**2, ...) "
783+
"call. (FIX2 limitation; full sum-of-weights fit deferred "
784+
"to a later phase.)",
785+
UserWarning,
786+
stacklevel=2,
787+
)
766788
yerr_hist = np.sqrt(np.maximum(counts_fit, 1))
767789
curve = {
768790
'x_data': bin_centers_fit,

UTILS/dfextensions/dfdraw/tests/feature_taxonomy.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1415,6 +1415,13 @@
14151415
"test_phase_13_42_df_inline_fits.py::TestPhase1342FIX1Regressions::test_f32_stacked_hist_per_group_fits",
14161416
"test_phase_13_42_df_inline_fits.py::TestPhase1342FIX1Regressions::test_f33_fit_textbox_kwargs_fontsize_override",
14171417
"test_phase_13_42_df_inline_fits.py::TestPhase1342FIX1Regressions::test_f33_fit_textbox_kwargs_precedence_over_style",
1418+
# Phase 13.42.DF FIX2 regressions (close items deferred at FIX1 close)
1419+
"test_phase_13_42_df_inline_fits.py::TestPhase1342FIX2Regressions::test_f59_suptitle_top_for_title_helper",
1420+
"test_phase_13_42_df_inline_fits.py::TestPhase1342FIX2Regressions::test_f60_facet_fit_no_crash",
1421+
"test_phase_13_42_df_inline_fits.py::TestPhase1342FIX2Regressions::test_f61_weighted_hist_fit_userwarning",
1422+
"test_phase_13_42_df_inline_fits.py::TestPhase1342FIX2Regressions::test_f61_unweighted_hist_fit_no_warning",
1423+
"test_phase_13_42_df_inline_fits.py::TestPhase1342FIX2Regressions::test_f62_stacked_selection_vector_fit_raises",
1424+
"test_phase_13_42_df_inline_fits.py::TestPhase1342FIX2Regressions::test_f63_fit_textbox_kwargs_signature_and_forwarded_names",
14181425
],
14191426
},
14201427
]

0 commit comments

Comments
 (0)