-
Notifications
You must be signed in to change notification settings - Fork 21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add more flexibility to visualize.pulls #342
base: master
Are you sure you want to change the base?
Changes from all commits
000a7d3
80458f5
11126cf
fefece4
9ed8358
6a3ab96
fc581ca
f5bd953
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -71,6 +71,7 @@ def pulls( | |
uncertainty: np.ndarray, | ||
labels: Union[List[str], np.ndarray], | ||
*, | ||
numeric: Optional[Union[List[bool], np.ndarray]] = None, | ||
figure_path: Optional[pathlib.Path] = None, | ||
close_figure: bool = False, | ||
) -> mpl.figure.Figure: | ||
|
@@ -80,6 +81,8 @@ def pulls( | |
bestfit (np.ndarray): best-fit parameter results | ||
uncertainty (np.ndarray): parameter uncertainties | ||
labels (Union[List[str], np.ndarray]): parameter names | ||
numeric Optional[Union[List[bool], np.ndarray]]: which parameters are numeric, | ||
and should only be shown directly | ||
figure_path (Optional[pathlib.Path], optional): path where figure should be | ||
saved, or None to not save it, defaults to None | ||
close_figure (bool, optional): whether to close each figure immediately after | ||
|
@@ -90,23 +93,42 @@ def pulls( | |
matplotlib.figure.Figure: the pull figure | ||
""" | ||
num_pars = len(bestfit) | ||
numeric = np.zeros(num_pars, dtype=bool) if numeric is None else np.asarray(numeric) | ||
y_positions = np.arange(num_pars)[::-1] | ||
fig, ax = plt.subplots(figsize=(6, 1 + num_pars / 4), dpi=100) | ||
ax.errorbar(bestfit, y_positions, xerr=uncertainty, fmt="o", color="black") | ||
|
||
ax.fill_between([-2, 2], -0.5, len(bestfit) - 0.5, color="yellow") | ||
ax.fill_between([-1, 1], -0.5, len(bestfit) - 0.5, color="limegreen") | ||
ax.vlines(0, -0.5, len(bestfit) - 0.5, linestyles="dotted", color="black") | ||
fig, ax = plt.subplots() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's keep the |
||
if num_pars > np.sum(numeric): # Actual pulls | ||
ax.errorbar( | ||
np.ma.masked_array(bestfit, mask=numeric), | ||
np.ma.masked_array(y_positions, mask=numeric), | ||
xerr=uncertainty, | ||
fmt="o", | ||
color="black", | ||
) | ||
mask = np.ones(2 * num_pars + 1).astype(bool) | ||
if numeric is not None: | ||
mask[1::2] = ~numeric[::-1] | ||
x_dummy = np.linspace(-0.5, num_pars - 0.5, 2 * num_pars + 1) | ||
ax.fill_betweenx(x_dummy, -2, 2, color="yellow", where=mask, linewidth=0) | ||
ax.fill_betweenx(x_dummy, -1, 1, color="limegreen", where=mask, linewidth=0) | ||
ax.plot( | ||
np.zeros(len(mask)), | ||
np.ma.masked_array(x_dummy, mask=~mask), | ||
linestyle="dotted", | ||
color="black", | ||
) | ||
if np.sum(numeric) > 0: | ||
for i, (show, par, unc) in enumerate( | ||
zip(numeric[::-1], bestfit[::-1], uncertainty[::-1]) | ||
): | ||
if show: | ||
ax.text(0, i, rf"{par:.2f} $\pm$ {unc:.2f}", ha="center", va="center") | ||
|
||
ax.set_xlim([-3, 3]) | ||
ax.set_xlabel(r"$\left(\hat{\theta} - \theta_0\right) / \Delta \theta$") | ||
ax.set_ylim([-0.5, num_pars - 0.5]) | ||
ax.set_yticks(y_positions) | ||
ax.set_yticklabels(labels) | ||
ax.xaxis.set_minor_locator(mpl.ticker.AutoMinorLocator()) # minor ticks | ||
ax.tick_params(axis="both", which="major", pad=8) | ||
ax.tick_params(direction="in", top=True, right=True, which="both") | ||
fig.set_tight_layout(True) | ||
Comment on lines
-106
to
-109
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe this removal is related to making figure customization easier via style sheets? If so, could we split that out into a separate PR? I'd like things to be more easily configurable, but I also do think the minor ticks help with legibility of constraints. Is there a way to move this into a default style sheet that users could override? |
||
|
||
utils._save_and_close(fig, figure_path, close_figure) | ||
return fig | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am wondering whether we should extract and save the information about the constraint term (none, Gaussian, Poisson) instead of the modifier types. The problem with the types is that a parameter may control multiple modifiers (e.g. a
normsys
plus ashapesys
). For the purpose of plotting pulls, the constraint term type is all that is needed to decide the handling.We could get the constraint term types like this:
Going one step further, we could also save the
.width()
information to evaluate constraints for Poisson-constrained parameters (since the pre-fit uncertainty for those varies per parameter).For other use cases (like #332) it might be useful to also know all the modifier types.
While it is currently possible to determine the constraint term type from knowing the modifier, that will change when constraint terms become configurable in the future with
pyhf
. So perhaps it is best to store constraint term information directly, and optionally add another field in the future to also keep track of the modifier types?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am remembering now where the idea of storing the modifier types come from: that allows to exclude by type in the plot, like excluding
staterror
. The constraint term type does not help there. It seems more likely that users would want to exclude by modifier type than by constraint term type, so perhaps it is best to stick with the implemented approach. The only thing that would need to be generalized is matching multiple modifier types.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something like the following could help simplify things by using more of the
pyhf
API: