From 0cfb68130ec3d2017a38a1e39c55e9e84df5251e Mon Sep 17 00:00:00 2001 From: tolange <torben.lange@cern.ch> Date: Thu, 2 Sep 2021 18:12:45 +0200 Subject: [PATCH 1/3] Added unblinding feature to PlotMultipleUpperLimits --- dhi/plots/limits.py | 49 ++++++++++++++++++++++++++++++++++++--------- dhi/tasks/limits.py | 2 +- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/dhi/plots/limits.py b/dhi/plots/limits.py index aae3ddcb..78a04ef6 100644 --- a/dhi/plots/limits.py +++ b/dhi/plots/limits.py @@ -23,7 +23,6 @@ from dhi.plots.util import ( get_graph_points, get_y_range, get_contours, fill_hist_from_points, infer_binning_from_grid, ) - colors = colors.root @@ -266,6 +265,7 @@ def plot_limit_scans( scan_parameter, names, expected_values, + unblinded = False, theory_values=None, y_log=False, x_min=None, @@ -299,6 +299,9 @@ def plot_limit_scans( When *show_points* is *True*, the central scan points are drawn on top of the interpolated curve. When *paper* is *True*, certain plot configurations are adjusted for use in publications. + If unblinded is *True*, *expected_values* should also contain the observed limits, + so that both the expected and observed limits can be drawn. + Example: https://cms-hh.web.cern.ch/tools/inference/tasks/limits.html#multiple-limits-on-poi-vs-scan-parameter """ import plotlib.root as r @@ -311,13 +314,14 @@ def plot_limit_scans( _ev = {key: _ev[key] for key in _ev.dtype.names} _expected_values.append(_ev) expected_values = _expected_values - # input checks n_graphs = len(expected_values) assert n_graphs >= 1 assert len(names) == n_graphs assert all(scan_parameter in ev for ev in expected_values) assert all("limit" in ev for ev in expected_values) + if unblinded: + assert all("observed" in ev for ev in expected_values) scan_values = expected_values[0][scan_parameter] has_thy = theory_values is not None has_thy_err = False @@ -352,10 +356,17 @@ def plot_limit_scans( r.setup_hist(h_dummy, pad=pad, props={"LineWidth": 0}) draw_objs.append((h_dummy, "HIST")) + g_exp_dummy = None + g_obs_dummy = None # central values for i, (ev, col, ms) in enumerate(zip(expected_values[::-1], color_sequence[:n_graphs][::-1], marker_sequence[:n_graphs][::-1])): mask = ~np.isnan(ev["limit"]) + limit_obs_values = None + if unblinded: + mask_obs = ~np.isnan(ev["observed"]) + mask = np.logical_and(mask,mask_obs) + limit_obs_values = ev["observed"][mask] limit_values = ev["limit"][mask] scan_values = ev[scan_parameter][mask] n_nans = (~mask).sum() @@ -363,13 +374,27 @@ def plot_limit_scans( print("WARNING: found {} NaN(s) in limit values at index {}".format(n_nans, len(expected_values) - 1 - i)) - g_exp = create_tgraph(mask.sum(), scan_values, limit_values) - r.setup_graph(g_exp, props={"LineWidth": 2, "MarkerStyle": ms, "MarkerSize": 1.2}, - color=colors[col]) - draw_objs.append((g_exp, "SAME,CP" if show_points else "SAME,C")) name = names[n_graphs - i - 1] - legend_entries.insert(0, (g_exp, to_root_latex(br_hh_names.get(name, name)), - "LP" if show_points else "L")) + if unblinded: + g_exp = create_tgraph(mask.sum(), scan_values, limit_values) + g_obs = create_tgraph(mask.sum(), scan_values, limit_obs_values) + r.setup_graph(g_obs, props={"LineWidth": 2, "MarkerStyle": ms, "MarkerSize": 1.2}, + color=colors[col]) + r.setup_graph(g_exp, props={"LineWidth": 2, "MarkerStyle": ms, "MarkerSize": 1.2, "LineStyle": 2}, + color=colors[col]) + draw_objs.append((g_exp, "SAME,C")) + draw_objs.append((g_obs, "SAME,CP" if show_points else "SAME,C")) + legend_entries.insert(0, (g_obs, to_root_latex(br_hh_names.get(name, name)), + "LP" if show_points else "L")) + g_exp_dummy = g_exp.Clone() + g_obs_dummy = g_obs.Clone() + else: + g_exp = create_tgraph(mask.sum(), scan_values, limit_values) + r.setup_graph(g_exp, props={"LineWidth": 2, "MarkerStyle": ms, "MarkerSize": 1.2}, + color=colors[col]) + draw_objs.append((g_exp, "SAME,CP" if show_points else "SAME,C")) + legend_entries.insert(0, (g_exp, to_root_latex(br_hh_names.get(name, name)), + "LP" if show_points else "L")) y_max_value = max(y_max_value, max(limit_values)) y_min_value = min(y_min_value, min(limit_values)) # print excluded ranges @@ -388,7 +413,13 @@ def plot_limit_scans( y_min, y_max, _ = get_y_range(y_min_value, y_max_value, y_min, y_max, log=y_log) h_dummy.SetMinimum(y_min) h_dummy.SetMaximum(y_max) - + if unblinded: + r.apply_properties(g_exp_dummy, {"LineColor": colors.black}) + r.apply_properties(g_obs_dummy, {"LineColor": colors.black}) + legend_entries.insert(0, (g_obs_dummy, to_root_latex("observed"), + "L")) + legend_entries.insert(0, (g_exp_dummy, to_root_latex("expected"), + "L")) # draw the theory prediction if has_thy: scan_values_thy = theory_values[scan_parameter] diff --git a/dhi/tasks/limits.py b/dhi/tasks/limits.py index 86db70a9..38f13cfd 100644 --- a/dhi/tasks/limits.py +++ b/dhi/tasks/limits.py @@ -328,7 +328,6 @@ class PlotUpperLimits(UpperLimitsBase, POIPlotTask): class PlotMultipleUpperLimits(PlotUpperLimits, MultiDatacardTask): - unblinded = None @classmethod def modify_param_values(cls, params): @@ -431,6 +430,7 @@ class PlotMultipleUpperLimits(PlotUpperLimits, MultiDatacardTask): scan_parameter=self.scan_parameter, names=names, expected_values=limit_values, + unblinded = self.unblinded, theory_values=thy_values, x_min=self.get_axis_limit("x_min"), x_max=self.get_axis_limit("x_max"), -- GitLab From 2befe7eaf1fd138989046f2898f587bee171b196 Mon Sep 17 00:00:00 2001 From: Marcel R <github.riga@icloud.com> Date: Tue, 7 Sep 2021 20:50:35 +0200 Subject: [PATCH 2/3] Align multi limit plot functions. --- dhi/plots/limits.py | 127 ++++++++++-------- dhi/tasks/limits.py | 34 ++++- .../plotmultipleupperlimits_param_tab.md | 2 +- 3 files changed, 101 insertions(+), 62 deletions(-) diff --git a/dhi/plots/limits.py b/dhi/plots/limits.py index 78a04ef6..748f77b7 100644 --- a/dhi/plots/limits.py +++ b/dhi/plots/limits.py @@ -23,6 +23,7 @@ from dhi.plots.util import ( get_graph_points, get_y_range, get_contours, fill_hist_from_points, infer_binning_from_grid, ) + colors = colors.root @@ -265,7 +266,7 @@ def plot_limit_scans( scan_parameter, names, expected_values, - unblinded = False, + observed_values=None, theory_values=None, y_log=False, x_min=None, @@ -283,7 +284,9 @@ def plot_limit_scans( Creates a plot showing multiple upper limit scans of a *poi* over a *scan_parameter* and saves it at *paths*. *expected_values* should be a list of mappings to lists of values or a record array with keys "<scan_parameter>" and "limit". Each mapping in *expected_values* will result in - a different curve. When *theory_values* is set, it should have a similar format with keys + a different curve. When *observed_values* is set, it should have a similar format with keys + "<scan_parameter>" and "limit", where the i-th element is corresponding to the i-th element in + *expected_values*. When *theory_values* is set, it should have a similar format with keys "<scan_parameter>" and "xsec", and optionally "xsec_p1" and "xsec_m1". *names* denote the names of limit curves shown in the legend. When a name is found to be in dhi.config.br_hh_names, its value is used as a label instead. @@ -299,29 +302,32 @@ def plot_limit_scans( When *show_points* is *True*, the central scan points are drawn on top of the interpolated curve. When *paper* is *True*, certain plot configurations are adjusted for use in publications. - If unblinded is *True*, *expected_values* should also contain the observed limits, - so that both the expected and observed limits can be drawn. - Example: https://cms-hh.web.cern.ch/tools/inference/tasks/limits.html#multiple-limits-on-poi-vs-scan-parameter """ import plotlib.root as r ROOT = import_ROOT() # convert record arrays to dicts mapping to arrays - _expected_values = [] - for _ev in expected_values: - if isinstance(_ev, np.ndarray): - _ev = {key: _ev[key] for key in _ev.dtype.names} - _expected_values.append(_ev) - expected_values = _expected_values + def check_values(values): + _values = [] + for v in values: + if isinstance(v, np.ndarray): + v = {key: v[key] for key in v.dtype.names} + assert "limit" in v + assert scan_parameter in v + _values.append(v) + return _values + # input checks + expected_values = check_values(expected_values) n_graphs = len(expected_values) assert n_graphs >= 1 assert len(names) == n_graphs - assert all(scan_parameter in ev for ev in expected_values) - assert all("limit" in ev for ev in expected_values) - if unblinded: - assert all("observed" in ev for ev in expected_values) + has_obs = False + if observed_values: + assert len(observed_values) == n_graphs + observed_values = check_values(observed_values) + has_obs = True scan_values = expected_values[0][scan_parameter] has_thy = theory_values is not None has_thy_err = False @@ -361,65 +367,78 @@ def plot_limit_scans( # central values for i, (ev, col, ms) in enumerate(zip(expected_values[::-1], color_sequence[:n_graphs][::-1], marker_sequence[:n_graphs][::-1])): + name = names[n_graphs - i - 1] + + # expected graph mask = ~np.isnan(ev["limit"]) - limit_obs_values = None - if unblinded: - mask_obs = ~np.isnan(ev["observed"]) - mask = np.logical_and(mask,mask_obs) - limit_obs_values = ev["observed"][mask] limit_values = ev["limit"][mask] scan_values = ev[scan_parameter][mask] n_nans = (~mask).sum() if n_nans: - print("WARNING: found {} NaN(s) in limit values at index {}".format(n_nans, - len(expected_values) - 1 - i)) - - name = names[n_graphs - i - 1] - if unblinded: - g_exp = create_tgraph(mask.sum(), scan_values, limit_values) - g_obs = create_tgraph(mask.sum(), scan_values, limit_obs_values) - r.setup_graph(g_obs, props={"LineWidth": 2, "MarkerStyle": ms, "MarkerSize": 1.2}, - color=colors[col]) - r.setup_graph(g_exp, props={"LineWidth": 2, "MarkerStyle": ms, "MarkerSize": 1.2, "LineStyle": 2}, - color=colors[col]) - draw_objs.append((g_exp, "SAME,C")) - draw_objs.append((g_obs, "SAME,CP" if show_points else "SAME,C")) - legend_entries.insert(0, (g_obs, to_root_latex(br_hh_names.get(name, name)), - "LP" if show_points else "L")) - g_exp_dummy = g_exp.Clone() - g_obs_dummy = g_obs.Clone() - else: - g_exp = create_tgraph(mask.sum(), scan_values, limit_values) - r.setup_graph(g_exp, props={"LineWidth": 2, "MarkerStyle": ms, "MarkerSize": 1.2}, - color=colors[col]) - draw_objs.append((g_exp, "SAME,CP" if show_points else "SAME,C")) - legend_entries.insert(0, (g_exp, to_root_latex(br_hh_names.get(name, name)), - "LP" if show_points else "L")) + print("WARNING: found {} NaN(s) in expected limit values at index {}".format(n_nans, + n_graphs - 1 - i)) + g_exp = create_tgraph(mask.sum(), scan_values, limit_values) + r.setup_graph(g_exp, props={"LineWidth": 2, "MarkerStyle": ms, "MarkerSize": 1.2, + "LineStyle": 2 if has_obs else 1}, color=colors[col]) + draw_objs.append((g_exp, "SAME,CP" if show_points and not has_obs else "SAME,C")) + legend_entries.insert(0, (g_exp, to_root_latex(br_hh_names.get(name, name)), + "LP" if show_points and not has_obs else "L")) y_max_value = max(y_max_value, max(limit_values)) y_min_value = min(y_min_value, min(limit_values)) - # print excluded ranges - print_excluded_ranges(scan_parameter, poi + " " + name, + + # print expected excluded ranges + print_excluded_ranges(scan_parameter, poi + " " + name + " (expected)", scan_values, limit_values, theory_values[scan_parameter] if has_thy else None, theory_values["xsec"] if has_thy else None, ) + # observed graph + if has_obs: + ov = observed_values[n_graphs - i - 1] + obs_mask = ~np.isnan(ov["limit"]) + obs_limit_values = ov["limit"][obs_mask] + obs_scan_values = ov[scan_parameter][obs_mask] + n_nans = (~obs_mask).sum() + if n_nans: + print("WARNING: found {} NaN(s) in observed limit values at index {}".format(n_nans, + n_graphs - 1 - i)) + g_obs = create_tgraph(obs_mask.sum(), obs_scan_values, obs_limit_values) + r.setup_graph(g_obs, props={"LineWidth": 2, "MarkerStyle": ms, "MarkerSize": 1.2}, + color=colors[col]) + draw_objs.append((g_obs, "SAME,CP" if show_points else "SAME,C")) + legend_entries[0] = (g_obs, to_root_latex(br_hh_names.get(name, name)), + "LP" if show_points else "L") + y_max_value = max(y_max_value, max(obs_limit_values)) + y_min_value = min(y_min_value, min(obs_limit_values)) + + # print observed excluded ranges + print_excluded_ranges(scan_parameter, poi + " " + name + " (observed)", + obs_scan_values, + obs_limit_values, + theory_values[scan_parameter] if has_thy else None, + theory_values["xsec"] if has_thy else None, + ) + + # add additional legend entries to distinguish expected and observed lines + if has_obs: + g_exp_dummy = g_exp.Clone() + g_obs_dummy = g_obs.Clone() + r.apply_properties(g_exp_dummy, {"LineColor": colors.black}) + r.apply_properties(g_obs_dummy, {"LineColor": colors.black}) + legend_entries.append((g_exp_dummy, "expected", "L")) + legend_entries.append((g_obs_dummy, "observed", "L")) + # get theory prediction limits if has_thy: y_min_value = min(y_min_value, min(theory_values["xsec_m1" if has_thy_err else "xsec"])) - # set limits + # set axis limits y_min, y_max, _ = get_y_range(y_min_value, y_max_value, y_min, y_max, log=y_log) h_dummy.SetMinimum(y_min) h_dummy.SetMaximum(y_max) - if unblinded: - r.apply_properties(g_exp_dummy, {"LineColor": colors.black}) - r.apply_properties(g_obs_dummy, {"LineColor": colors.black}) - legend_entries.insert(0, (g_obs_dummy, to_root_latex("observed"), - "L")) - legend_entries.insert(0, (g_exp_dummy, to_root_latex("expected"), - "L")) + # draw the theory prediction if has_thy: scan_values_thy = theory_values[scan_parameter] diff --git a/dhi/tasks/limits.py b/dhi/tasks/limits.py index 0c833930..fcba2846 100644 --- a/dhi/tasks/limits.py +++ b/dhi/tasks/limits.py @@ -354,7 +354,6 @@ class PlotUpperLimits(UpperLimitsBase, POIPlotTask): class PlotMultipleUpperLimits(PlotUpperLimits, MultiDatacardTask): - @classmethod def modify_param_values(cls, params): params = PlotUpperLimits.modify_param_values.__func__.__get__(cls)(params) @@ -443,11 +442,22 @@ class PlotMultipleUpperLimits(PlotUpperLimits, MultiDatacardTask): if self.datacard_names: names = list(self.datacard_names) - # reoder if requested + # reorder if requested if self.datacard_order: limit_values = [limit_values[i] for i in self.datacard_order] names = [names[i] for i in self.datacard_order] + # prepare observed values + obs_values = None + if self.unblinded: + obs_values = [ + { + self.scan_parameter: _limit_values[self.scan_parameter], + "limit": _limit_values["observed"], + } + for _limit_values in limit_values + ] + # call the plot function self.call_plot_func( "dhi.plots.limits.plot_limit_scans", @@ -456,7 +466,7 @@ class PlotMultipleUpperLimits(PlotUpperLimits, MultiDatacardTask): scan_parameter=self.scan_parameter, names=names, expected_values=limit_values, - unblinded = self.unblinded, + observed_values=obs_values, theory_values=thy_values, x_min=self.get_axis_limit("x_min"), x_max=self.get_axis_limit("x_max"), @@ -474,8 +484,6 @@ class PlotMultipleUpperLimits(PlotUpperLimits, MultiDatacardTask): class PlotMultipleUpperLimitsByModel(PlotUpperLimits, MultiHHModelTask): - unblinded = None - allow_empty_hh_model = True def requires(self): @@ -568,11 +576,22 @@ class PlotMultipleUpperLimitsByModel(PlotUpperLimits, MultiHHModelTask): if self.hh_model_names: names = list(self.hh_model_names) - # reoder if requested + # reorder if requested if self.hh_model_order: limit_values = [limit_values[i] for i in self.hh_model_order] names = [names[i] for i in self.hh_model_order] + # prepare observed values + obs_values = None + if self.unblinded: + obs_values = [ + { + self.scan_parameter: _limit_values[self.scan_parameter], + "limit": _limit_values["observed"], + } + for _limit_values in limit_values + ] + # call the plot function self.call_plot_func( "dhi.plots.limits.plot_limit_scans", @@ -581,6 +600,7 @@ class PlotMultipleUpperLimitsByModel(PlotUpperLimits, MultiHHModelTask): scan_parameter=self.scan_parameter, names=names, expected_values=limit_values, + observed_values=obs_values, theory_values=thy_values, x_min=self.get_axis_limit("x_min"), x_max=self.get_axis_limit("x_max"), @@ -829,7 +849,7 @@ class PlotUpperLimitsAtPoint(POIPlotTask, SnapshotUser, MultiDatacardTask, BoxPl for d, label in zip(data, self.extra_labels): d["label"] = label - # reoder if requested + # reorder if requested if self.datacard_order: data = [data[i] for i in self.datacard_order] diff --git a/docs/content/snippets/plotmultipleupperlimits_param_tab.md b/docs/content/snippets/plotmultipleupperlimits_param_tab.md index 26c26221..63fe295d 100644 --- a/docs/content/snippets/plotmultipleupperlimits_param_tab.md +++ b/docs/content/snippets/plotmultipleupperlimits_param_tab.md @@ -2,6 +2,6 @@ The `PlotMultipleUpperLimits` task collects the fit results from multiple `Merge <div class="dhi_parameter_table"> ---8<-- "content/snippets/parameters.md@-2,21,19,16,54,14,66,12,17,18,74,10,11,3,4,22,23,5,6,7,8,9" +--8<-- "content/snippets/parameters.md@-2,21,19,16,54,14,66,12,48,17,18,74,10,11,3,4,22,23,5,6,7,8,9" </div> -- GitLab From 6acacf1f6b8ccfc61d0e6ccc760a748062829b58 Mon Sep 17 00:00:00 2001 From: Marcel R <github.riga@icloud.com> Date: Tue, 7 Sep 2021 20:52:43 +0200 Subject: [PATCH 3/3] Align multi limit plot functions. --- dhi/plots/limits.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/dhi/plots/limits.py b/dhi/plots/limits.py index 748f77b7..d2e797eb 100644 --- a/dhi/plots/limits.py +++ b/dhi/plots/limits.py @@ -362,8 +362,6 @@ def plot_limit_scans( r.setup_hist(h_dummy, pad=pad, props={"LineWidth": 0}) draw_objs.append((h_dummy, "HIST")) - g_exp_dummy = None - g_obs_dummy = None # central values for i, (ev, col, ms) in enumerate(zip(expected_values[::-1], color_sequence[:n_graphs][::-1], marker_sequence[:n_graphs][::-1])): @@ -387,7 +385,7 @@ def plot_limit_scans( y_min_value = min(y_min_value, min(limit_values)) # print expected excluded ranges - print_excluded_ranges(scan_parameter, poi + " " + name + " (expected)", + print_excluded_ranges(scan_parameter, "{}, {}, expected".format(poi, name), scan_values, limit_values, theory_values[scan_parameter] if has_thy else None, @@ -414,7 +412,7 @@ def plot_limit_scans( y_min_value = min(y_min_value, min(obs_limit_values)) # print observed excluded ranges - print_excluded_ranges(scan_parameter, poi + " " + name + " (observed)", + print_excluded_ranges(scan_parameter, "{}, {}, observed".format(poi, name), obs_scan_values, obs_limit_values, theory_values[scan_parameter] if has_thy else None, -- GitLab