Skip to content

Commit ff8fb3c

Browse files
rmnmllrRoman Muelleralexander-held
authored
fix: handle negative model predictions in visualizations (#394)
* catch negative total model prediction in visualize.plot_model.data_mc * handle negative nominal template prediction in visualize.plot_model.templates --------- Co-authored-by: Roman Mueller <[email protected]> Co-authored-by: Alexander Held <[email protected]>
1 parent 4645cb8 commit ff8fb3c

File tree

2 files changed

+39
-3
lines changed

2 files changed

+39
-3
lines changed

src/cabinetry/visualize/plot_model.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ def data_mc(
5252
saving it, defaults to False (enable when producing many figures to avoid
5353
memory issues, prevents rendering in notebooks)
5454
55+
Raises:
56+
ValueError: when total model yield is negative in any bin
57+
5558
Returns:
5659
matplotlib.figure.Figure: the data/MC figure
5760
"""
@@ -149,6 +152,11 @@ def data_mc(
149152
)
150153
nonzero_model_yield = total_yield != 0.0
151154

155+
if np.any(total_yield < 0.0):
156+
raise ValueError(
157+
f"{label} total model yield has negative bin(s): {total_yield.tolist()}"
158+
)
159+
152160
# add uncertainty band around y=1
153161
rel_mc_unc = total_model_unc / total_yield
154162
# do not show band in bins where total model yield is 0
@@ -312,6 +320,15 @@ def templates(
312320
# x positions for lines drawn showing the template distributions
313321
line_x = [y for y in bin_edges for _ in range(2)][1:-1]
314322

323+
neg_nom_bin = False # negative bin(s) present in nominal histogram
324+
if np.any(nominal_histo["yields"] < 0.0):
325+
neg_nom_bin = True
326+
log.warning(
327+
f"{label} nominal histogram yield has negative bin(s): "
328+
f"{nominal_histo['yields'].tolist()}, taking absolute value for "
329+
"ratio plot uncertainty"
330+
)
331+
315332
# draw templates
316333
for template, color, linestyle, template_label in zip(
317334
all_templates, colors, linestyles, template_labels
@@ -354,7 +371,7 @@ def templates(
354371
ax2.errorbar(
355372
bin_centers,
356373
template_ratio_plot,
357-
yerr=template["stdev"] / nominal_histo["yields"],
374+
yerr=template["stdev"] / np.abs(nominal_histo["yields"]),
358375
fmt="none",
359376
color=color,
360377
)
@@ -395,7 +412,7 @@ def templates(
395412
ax2.set_xlim([bin_edges[0], bin_edges[-1]])
396413
ax2.set_ylim([0.5, 1.5])
397414
ax2.set_xlabel(variable)
398-
ax2.set_ylabel("variation / nominal")
415+
ax2.set_ylabel(f"variation / {'nominal' if not neg_nom_bin else 'abs(nominal)'}")
399416
ax2.set_yticks([0.5, 0.75, 1.0, 1.25, 1.5])
400417
ax2.set_yticklabels([0.5, 0.75, 1.0, 1.25, ""])
401418
ax2.tick_params(axis="both", which="major", pad=8)

tests/visualize/test_visualize_plot_model.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,20 @@ def test_data_mc(tmp_path, caplog):
125125
# expect three RuntimeWarnings from numpy due to division by zero
126126
assert sum("divide by zero" in str(m.message) for m in warn_record) == 3
127127

128+
# negative bin yield
129+
histo_dict_list[0]["yields"] = [-50, -50]
130+
with pytest.raises(
131+
ValueError,
132+
match=r"abc total model yield has negative bin\(s\): \[-50, -45\]",
133+
):
134+
plot_model.data_mc(
135+
histo_dict_list, total_model_unc_log, bin_edges_log, label="abc"
136+
)
137+
128138
plt.close("all")
129139

130140

131-
def test_templates(tmp_path):
141+
def test_templates(tmp_path, caplog):
132142
fname = tmp_path / "fig.png"
133143
nominal_histo = {
134144
"yields": np.asarray([1.0, 1.2]),
@@ -172,9 +182,12 @@ def test_templates(tmp_path):
172182
assert (
173183
compare_images("tests/visualize/reference/templates.png", str(fname), 0) is None
174184
)
185+
caplog.clear()
175186

176187
# do not save figure, but close it
177188
# only single variation specified
189+
# negative bin present in nominal histogram
190+
nominal_histo["yields"][0] = -1
178191
with mock.patch("cabinetry.visualize.utils._save_and_close") as mock_close_safe:
179192
fig = plot_model.templates(
180193
nominal_histo,
@@ -188,6 +201,12 @@ def test_templates(tmp_path):
188201
close_figure=True,
189202
)
190203
assert mock_close_safe.call_args_list == [((fig, None, True), {})]
204+
assert (
205+
"region: Signal region\nsample: Signal\nsystematic: Modeling nominal histogram "
206+
"yield has negative bin(s): [-1.0, 1.2], taking absolute value for ratio plot "
207+
"uncertainty"
208+
) in [rec.message for rec in caplog.records]
209+
caplog.clear()
191210

192211
plt.close("all")
193212

0 commit comments

Comments
 (0)