rootfig 0.2.2__tar.gz → 0.3.0__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.
- {rootfig-0.2.2 → rootfig-0.3.0}/PKG-INFO +41 -24
- {rootfig-0.2.2 → rootfig-0.3.0}/README.md +40 -23
- {rootfig-0.2.2 → rootfig-0.3.0}/docs/composable.md +7 -5
- {rootfig-0.2.2 → rootfig-0.3.0}/docs/hooks/gallery.py +2 -1
- rootfig-0.3.0/docs/images/gallery/arrays.png +0 -0
- rootfig-0.3.0/docs/images/gallery/cms_density.png +0 -0
- rootfig-0.3.0/docs/images/gallery/correlation.png +0 -0
- rootfig-0.3.0/docs/images/gallery/expressions.png +0 -0
- rootfig-0.3.0/docs/images/gallery/fill_stats.png +0 -0
- rootfig-0.3.0/docs/images/gallery/hist2d.png +0 -0
- rootfig-0.3.0/docs/images/gallery/log_axes.png +0 -0
- rootfig-0.3.0/docs/images/gallery/luminosity.png +0 -0
- rootfig-0.3.0/docs/images/gallery/many_plots.png +0 -0
- rootfig-0.3.0/docs/images/gallery/object_vs_event.png +0 -0
- rootfig-0.3.0/docs/images/gallery/overlay_ratio.png +0 -0
- rootfig-0.3.0/docs/images/gallery/quick.png +0 -0
- rootfig-0.3.0/docs/images/gallery/ratio_reference.png +0 -0
- rootfig-0.3.0/docs/images/gallery/robust_range.png +0 -0
- rootfig-0.3.0/docs/images/gallery/stack_data.png +0 -0
- rootfig-0.3.0/docs/images/gallery/style_colors.png +0 -0
- rootfig-0.3.0/docs/images/gallery/variable_bins.png +0 -0
- rootfig-0.3.0/docs/images/gallery/xbreak_ratio.png +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/docs/plotting.md +95 -6
- {rootfig-0.2.2 → rootfig-0.3.0}/docs/quickstart.md +5 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/examples/gallery/__init__.py +24 -14
- {rootfig-0.2.2 → rootfig-0.3.0}/examples/gallery/registry.py +5 -2
- {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/__init__.py +1 -1
- {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/api.py +31 -7
- {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/histograms/pipeline.py +11 -3
- {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/model/__init__.py +10 -1
- rootfig-0.3.0/src/rootfig/model/binning.py +465 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/model/variables.py +8 -5
- {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/plotting/figure.py +9 -3
- {rootfig-0.2.2 → rootfig-0.3.0}/tests/test_api.py +93 -1
- {rootfig-0.2.2 → rootfig-0.3.0}/tests/test_model.py +207 -5
- {rootfig-0.2.2 → rootfig-0.3.0}/tests/test_plotting.py +7 -7
- rootfig-0.2.2/docs/images/gallery/arrays.png +0 -0
- rootfig-0.2.2/docs/images/gallery/cms_density.png +0 -0
- rootfig-0.2.2/docs/images/gallery/correlation.png +0 -0
- rootfig-0.2.2/docs/images/gallery/expressions.png +0 -0
- rootfig-0.2.2/docs/images/gallery/fill_stats.png +0 -0
- rootfig-0.2.2/docs/images/gallery/hist2d.png +0 -0
- rootfig-0.2.2/docs/images/gallery/log_axes.png +0 -0
- rootfig-0.2.2/docs/images/gallery/luminosity.png +0 -0
- rootfig-0.2.2/docs/images/gallery/many_plots.png +0 -0
- rootfig-0.2.2/docs/images/gallery/object_vs_event.png +0 -0
- rootfig-0.2.2/docs/images/gallery/overlay_ratio.png +0 -0
- rootfig-0.2.2/docs/images/gallery/quick.png +0 -0
- rootfig-0.2.2/docs/images/gallery/ratio_reference.png +0 -0
- rootfig-0.2.2/docs/images/gallery/robust_range.png +0 -0
- rootfig-0.2.2/docs/images/gallery/stack_data.png +0 -0
- rootfig-0.2.2/docs/images/gallery/style_colors.png +0 -0
- rootfig-0.2.2/docs/images/gallery/variable_bins.png +0 -0
- rootfig-0.2.2/docs/images/gallery/xbreak_ratio.png +0 -0
- rootfig-0.2.2/src/rootfig/model/binning.py +0 -242
- {rootfig-0.2.2 → rootfig-0.3.0}/.gitignore +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/CONTRIBUTING.md +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/LICENSE +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/docs/api.md +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/docs/ecosystem.md +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/docs/expressions.md +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/docs/gallery.md +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/docs/images/gallery/efficiency.png +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/docs/images/gallery/profile.png +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/docs/index.md +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/examples/gallery/__main__.py +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/examples/gallery/data.py +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/mkdocs.yml +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/pyproject.toml +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/_typing.py +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/errors.py +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/expressions/__init__.py +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/expressions/functions.py +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/expressions/parser.py +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/histograms/__init__.py +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/histograms/build.py +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/histograms/cutflow.py +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/histograms/efficiency.py +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/histograms/normalize.py +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/histograms/ratio.py +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/histograms/stats.py +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/io/__init__.py +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/io/sources.py +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/model/cuts.py +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/model/samples.py +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/model/style.py +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/model/units.py +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/plotting/__init__.py +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/plotting/annotations.py +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/plotting/correlation.py +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/plotting/hist1d.py +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/plotting/hist2d.py +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/plotting/points.py +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/plotting/ratio.py +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/plotting/result.py +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/plotting/style.py +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/py.typed +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/selection/__init__.py +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/src/rootfig/selection/columns.py +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/tests/conftest.py +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/tests/data/split_collection.root +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/tests/test_expressions.py +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/tests/test_gallery.py +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/tests/test_histograms.py +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/tests/test_io.py +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/tests/test_selection.py +0 -0
- {rootfig-0.2.2 → rootfig-0.3.0}/tests/test_tutorials.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: rootfig
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Publication-quality figures straight from ROOT trees, without ROOT: uproot + Awkward + hist + mplhep with a TTree::Draw-like API.
|
|
5
5
|
Project-URL: Homepage, https://github.com/jbeirer/rootfig
|
|
6
6
|
Project-URL: Documentation, https://jbeirer.github.io/rootfig/
|
|
@@ -31,22 +31,35 @@ Requires-Dist: numpy>=1.26
|
|
|
31
31
|
Requires-Dist: uproot>=5.7.4
|
|
32
32
|
Description-Content-Type: text/markdown
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
<p align="center">
|
|
35
|
+
<picture>
|
|
36
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jbeirer/rootfig/main/.github/assets/rootfig-logo-dark.svg">
|
|
37
|
+
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/jbeirer/rootfig/main/.github/assets/rootfig-logo-light.svg">
|
|
38
|
+
<img src="https://raw.githubusercontent.com/jbeirer/rootfig/main/.github/assets/rootfig-logo-light.svg" alt="rootfig" width="560">
|
|
39
|
+
</picture>
|
|
40
|
+
</p>
|
|
35
41
|
|
|
36
|
-
|
|
42
|
+
<p align="center">
|
|
43
|
+
<strong>Publication-quality figures straight from ROOT trees, without ROOT.</strong>
|
|
44
|
+
</p>
|
|
37
45
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
[](LICENSE)
|
|
46
|
+
<p align="center">
|
|
47
|
+
<a href="https://jbeirer.github.io/rootfig/">Documentation</a> ·
|
|
48
|
+
<a href="https://jbeirer.github.io/rootfig/gallery/">Gallery</a> ·
|
|
49
|
+
<a href="https://jbeirer.github.io/rootfig/quickstart/">Quick start</a>
|
|
50
|
+
</p>
|
|
44
51
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
52
|
+
<p align="center">
|
|
53
|
+
<a href="https://jbeirer.github.io/rootfig/"><img src="https://img.shields.io/badge/docs-online-blue" alt="Documentation"></a>
|
|
54
|
+
<a href="https://doi.org/10.5281/zenodo.22726311"><img src="https://zenodo.org/badge/1366702602.svg" alt="DOI"></a>
|
|
55
|
+
<a href="https://github.com/jbeirer/rootfig/actions/workflows/ci.yml"><img src="https://github.com/jbeirer/rootfig/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
56
|
+
<a href="https://codecov.io/gh/jbeirer/rootfig"><img src="https://codecov.io/gh/jbeirer/rootfig/branch/main/graph/badge.svg" alt="codecov"></a>
|
|
57
|
+
<a href="https://pypi.org/project/rootfig/"><img src="https://img.shields.io/pypi/v/rootfig.svg" alt="PyPI"></a>
|
|
58
|
+
<a href="https://pypi.org/project/rootfig/"><img src="https://img.shields.io/pypi/pyversions/rootfig.svg" alt="Python"></a>
|
|
59
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
|
|
60
|
+
</p>
|
|
48
61
|
|
|
49
|
-
Go from ROOT
|
|
62
|
+
Go from a ROOT file to a styled figure in one call. Choose a variable, add a
|
|
50
63
|
selection, and plot:
|
|
51
64
|
|
|
52
65
|
```python
|
|
@@ -57,15 +70,17 @@ rf.plot("events.root", "Muon_pt", tree="events", selection="Muon_pt > 20", bins=
|
|
|
57
70
|
|
|
58
71
|
Start with a single distribution; add samples, weights, stacks and ratio
|
|
59
72
|
panels as your analysis grows. Every plot gives you a matplotlib figure to
|
|
60
|
-
customise and save.
|
|
73
|
+
customise and save.
|
|
61
74
|
|
|
62
75
|
<p align="center">
|
|
63
|
-
<a href="https://jbeirer.github.io/rootfig/gallery
|
|
64
|
-
|
|
76
|
+
<a href="https://jbeirer.github.io/rootfig/gallery/#logarithmic-axes-with-log-spaced-bins"><img src="docs/images/gallery/log_axes.png" alt="Logarithmic axes with log-spaced bins" width="46%"></a>
|
|
77
|
+
|
|
78
|
+
<a href="https://jbeirer.github.io/rootfig/gallery/#a-broken-x-axis-peak-and-far-tail-without-the-empty-middle"><img src="docs/images/gallery/xbreak_ratio.png" alt="Broken x axis with a ratio panel" width="46%"></a>
|
|
65
79
|
</p>
|
|
66
80
|
<p align="center">
|
|
67
|
-
<a href="https://jbeirer.github.io/rootfig/gallery
|
|
68
|
-
|
|
81
|
+
<a href="https://jbeirer.github.io/rootfig/gallery/#a-correlation-matrix"><img src="docs/images/gallery/correlation.png" alt="A correlation matrix" width="46%"></a>
|
|
82
|
+
|
|
83
|
+
<a href="https://jbeirer.github.io/rootfig/gallery/#a-two-dimensional-histogram"><img src="docs/images/gallery/hist2d.png" alt="Two-dimensional histogram" width="46%"></a>
|
|
69
84
|
</p>
|
|
70
85
|
|
|
71
86
|
**[Explore the gallery →](https://jbeirer.github.io/rootfig/gallery/)**
|
|
@@ -88,16 +103,16 @@ files are both supported.
|
|
|
88
103
|
```python
|
|
89
104
|
import rootfig as rf
|
|
90
105
|
|
|
91
|
-
# Overlay two samples, normalised to unity, with a
|
|
106
|
+
# Overlay two samples, each normalised to unity, with a Signal / Background panel.
|
|
92
107
|
rf.plot(
|
|
93
|
-
|
|
108
|
+
{"Signal": "signal.root", "Background": "background.root"},
|
|
94
109
|
"Muon_pt",
|
|
95
110
|
tree="events",
|
|
96
111
|
selection="abs(Muon_eta) < 2.5",
|
|
97
112
|
weight="event_weight",
|
|
98
113
|
bins=(50, 0, 200),
|
|
99
114
|
normalize=True,
|
|
100
|
-
ratio=
|
|
115
|
+
ratio="Background",
|
|
101
116
|
)
|
|
102
117
|
```
|
|
103
118
|
|
|
@@ -140,8 +155,10 @@ matplotlib `Figure`/`Axes`, `p.hists` are `hist.Hist` objects, and
|
|
|
140
155
|
`count(Jet_pt) >= 2` or `Muon_pt > 20`; event and object selections have
|
|
141
156
|
explicit rules, and event weights carry through to each selected object.
|
|
142
157
|
- **Compare samples with a few keywords.** Overlays, stacks, data points and
|
|
143
|
-
ratio panels share binning and propagate histogram uncertainties
|
|
144
|
-
|
|
158
|
+
ratio panels share binning and propagate histogram uncertainties; bin edges
|
|
159
|
+
and `(n, low, high)` are used as given, while a range inferred from the data
|
|
160
|
+
ignores far outliers, so `-999` sentinels do not set the axis. Normalise to
|
|
161
|
+
unity, density, bin width or luminosity.
|
|
145
162
|
- **Style figures for your analysis.** Add experiment labels, units, log axes
|
|
146
163
|
and broken axes, then refine the result with matplotlib.
|
|
147
164
|
- **Go beyond 1D plots.** Draw 2D histograms, correlations, efficiencies,
|
|
@@ -172,7 +189,7 @@ stack, building on familiar libraries:
|
|
|
172
189
|
| --- | --- | --- |
|
|
173
190
|
| Reading ROOT files | [uproot](https://github.com/scikit-hep/uproot5) | file globs, tree auto-detection, reading only the required branches |
|
|
174
191
|
| Jagged arrays | [Awkward Array](https://github.com/scikit-hep/awkward) | the per-event/per-object rules for cuts and weights |
|
|
175
|
-
| Histograms | [hist](https://github.com/scikit-hep/hist) / boost-histogram | shared binning, automatic ranges, normalisation, ratios |
|
|
192
|
+
| Histograms | [hist](https://github.com/scikit-hep/hist) / boost-histogram | shared binning, robust automatic ranges, normalisation, ratios |
|
|
176
193
|
| Drawing | [mplhep](https://github.com/scikit-hep/mplhep) + matplotlib | overlays, stacks, ratio panels, labels and legends with good defaults |
|
|
177
194
|
|
|
178
195
|
If you already have `hist.Hist` objects, `rf.plot_histograms` draws them with
|
|
@@ -1,19 +1,32 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jbeirer/rootfig/main/.github/assets/rootfig-logo-dark.svg">
|
|
4
|
+
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/jbeirer/rootfig/main/.github/assets/rootfig-logo-light.svg">
|
|
5
|
+
<img src="https://raw.githubusercontent.com/jbeirer/rootfig/main/.github/assets/rootfig-logo-light.svg" alt="rootfig" width="560">
|
|
6
|
+
</picture>
|
|
7
|
+
</p>
|
|
2
8
|
|
|
3
|
-
|
|
9
|
+
<p align="center">
|
|
10
|
+
<strong>Publication-quality figures straight from ROOT trees, without ROOT.</strong>
|
|
11
|
+
</p>
|
|
4
12
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
[](LICENSE)
|
|
13
|
+
<p align="center">
|
|
14
|
+
<a href="https://jbeirer.github.io/rootfig/">Documentation</a> ·
|
|
15
|
+
<a href="https://jbeirer.github.io/rootfig/gallery/">Gallery</a> ·
|
|
16
|
+
<a href="https://jbeirer.github.io/rootfig/quickstart/">Quick start</a>
|
|
17
|
+
</p>
|
|
11
18
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
19
|
+
<p align="center">
|
|
20
|
+
<a href="https://jbeirer.github.io/rootfig/"><img src="https://img.shields.io/badge/docs-online-blue" alt="Documentation"></a>
|
|
21
|
+
<a href="https://doi.org/10.5281/zenodo.22726311"><img src="https://zenodo.org/badge/1366702602.svg" alt="DOI"></a>
|
|
22
|
+
<a href="https://github.com/jbeirer/rootfig/actions/workflows/ci.yml"><img src="https://github.com/jbeirer/rootfig/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
23
|
+
<a href="https://codecov.io/gh/jbeirer/rootfig"><img src="https://codecov.io/gh/jbeirer/rootfig/branch/main/graph/badge.svg" alt="codecov"></a>
|
|
24
|
+
<a href="https://pypi.org/project/rootfig/"><img src="https://img.shields.io/pypi/v/rootfig.svg" alt="PyPI"></a>
|
|
25
|
+
<a href="https://pypi.org/project/rootfig/"><img src="https://img.shields.io/pypi/pyversions/rootfig.svg" alt="Python"></a>
|
|
26
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
|
|
27
|
+
</p>
|
|
15
28
|
|
|
16
|
-
Go from ROOT
|
|
29
|
+
Go from a ROOT file to a styled figure in one call. Choose a variable, add a
|
|
17
30
|
selection, and plot:
|
|
18
31
|
|
|
19
32
|
```python
|
|
@@ -24,15 +37,17 @@ rf.plot("events.root", "Muon_pt", tree="events", selection="Muon_pt > 20", bins=
|
|
|
24
37
|
|
|
25
38
|
Start with a single distribution; add samples, weights, stacks and ratio
|
|
26
39
|
panels as your analysis grows. Every plot gives you a matplotlib figure to
|
|
27
|
-
customise and save.
|
|
40
|
+
customise and save.
|
|
28
41
|
|
|
29
42
|
<p align="center">
|
|
30
|
-
<a href="https://jbeirer.github.io/rootfig/gallery
|
|
31
|
-
|
|
43
|
+
<a href="https://jbeirer.github.io/rootfig/gallery/#logarithmic-axes-with-log-spaced-bins"><img src="docs/images/gallery/log_axes.png" alt="Logarithmic axes with log-spaced bins" width="46%"></a>
|
|
44
|
+
|
|
45
|
+
<a href="https://jbeirer.github.io/rootfig/gallery/#a-broken-x-axis-peak-and-far-tail-without-the-empty-middle"><img src="docs/images/gallery/xbreak_ratio.png" alt="Broken x axis with a ratio panel" width="46%"></a>
|
|
32
46
|
</p>
|
|
33
47
|
<p align="center">
|
|
34
|
-
<a href="https://jbeirer.github.io/rootfig/gallery
|
|
35
|
-
|
|
48
|
+
<a href="https://jbeirer.github.io/rootfig/gallery/#a-correlation-matrix"><img src="docs/images/gallery/correlation.png" alt="A correlation matrix" width="46%"></a>
|
|
49
|
+
|
|
50
|
+
<a href="https://jbeirer.github.io/rootfig/gallery/#a-two-dimensional-histogram"><img src="docs/images/gallery/hist2d.png" alt="Two-dimensional histogram" width="46%"></a>
|
|
36
51
|
</p>
|
|
37
52
|
|
|
38
53
|
**[Explore the gallery →](https://jbeirer.github.io/rootfig/gallery/)**
|
|
@@ -55,16 +70,16 @@ files are both supported.
|
|
|
55
70
|
```python
|
|
56
71
|
import rootfig as rf
|
|
57
72
|
|
|
58
|
-
# Overlay two samples, normalised to unity, with a
|
|
73
|
+
# Overlay two samples, each normalised to unity, with a Signal / Background panel.
|
|
59
74
|
rf.plot(
|
|
60
|
-
|
|
75
|
+
{"Signal": "signal.root", "Background": "background.root"},
|
|
61
76
|
"Muon_pt",
|
|
62
77
|
tree="events",
|
|
63
78
|
selection="abs(Muon_eta) < 2.5",
|
|
64
79
|
weight="event_weight",
|
|
65
80
|
bins=(50, 0, 200),
|
|
66
81
|
normalize=True,
|
|
67
|
-
ratio=
|
|
82
|
+
ratio="Background",
|
|
68
83
|
)
|
|
69
84
|
```
|
|
70
85
|
|
|
@@ -107,8 +122,10 @@ matplotlib `Figure`/`Axes`, `p.hists` are `hist.Hist` objects, and
|
|
|
107
122
|
`count(Jet_pt) >= 2` or `Muon_pt > 20`; event and object selections have
|
|
108
123
|
explicit rules, and event weights carry through to each selected object.
|
|
109
124
|
- **Compare samples with a few keywords.** Overlays, stacks, data points and
|
|
110
|
-
ratio panels share binning and propagate histogram uncertainties
|
|
111
|
-
|
|
125
|
+
ratio panels share binning and propagate histogram uncertainties; bin edges
|
|
126
|
+
and `(n, low, high)` are used as given, while a range inferred from the data
|
|
127
|
+
ignores far outliers, so `-999` sentinels do not set the axis. Normalise to
|
|
128
|
+
unity, density, bin width or luminosity.
|
|
112
129
|
- **Style figures for your analysis.** Add experiment labels, units, log axes
|
|
113
130
|
and broken axes, then refine the result with matplotlib.
|
|
114
131
|
- **Go beyond 1D plots.** Draw 2D histograms, correlations, efficiencies,
|
|
@@ -139,7 +156,7 @@ stack, building on familiar libraries:
|
|
|
139
156
|
| --- | --- | --- |
|
|
140
157
|
| Reading ROOT files | [uproot](https://github.com/scikit-hep/uproot5) | file globs, tree auto-detection, reading only the required branches |
|
|
141
158
|
| Jagged arrays | [Awkward Array](https://github.com/scikit-hep/awkward) | the per-event/per-object rules for cuts and weights |
|
|
142
|
-
| Histograms | [hist](https://github.com/scikit-hep/hist) / boost-histogram | shared binning, automatic ranges, normalisation, ratios |
|
|
159
|
+
| Histograms | [hist](https://github.com/scikit-hep/hist) / boost-histogram | shared binning, robust automatic ranges, normalisation, ratios |
|
|
143
160
|
| Drawing | [mplhep](https://github.com/scikit-hep/mplhep) + matplotlib | overlays, stacks, ratio panels, labels and legends with good defaults |
|
|
144
161
|
|
|
145
162
|
If you already have `hist.Hist` objects, `rf.plot_histograms` draws them with
|
|
@@ -61,14 +61,16 @@ What to histogram and how to present it.
|
|
|
61
61
|
```python
|
|
62
62
|
pt = rf.Variable("Muon_pt", bins=(50, 0, 200), label=r"$p_T^{\mu}$", unit="GeV")
|
|
63
63
|
met = rf.Variable(
|
|
64
|
-
"MET / 1000", bins=40, range="
|
|
64
|
+
"MET / 1000", bins=40, range="auto", label=r"$E_T^{miss}$", unit="TeV", log=True, name="met"
|
|
65
65
|
)
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
-
- `bins`: an `int` (range from the data), `(n, low, high)`, a sequence
|
|
69
|
-
edges (e.g. `rf.log_bins(30, 1, 1000)`), or a `hist.axis.Regular`/`Variable`.
|
|
70
|
-
- `range`: `(low, high)`, `"
|
|
71
|
-
`
|
|
68
|
+
- `bins`: an `int` (range inferred from the data), `(n, low, high)`, a sequence
|
|
69
|
+
of edges (e.g. `rf.log_bins(30, 1, 1000)`), or a `hist.axis.Regular`/`Variable`.
|
|
70
|
+
- `range`: `(low, high)`, `"robust"` (the default: ignores far outliers such as
|
|
71
|
+
`-999` sentinels and cuts a thin tail, both of which then land in the
|
|
72
|
+
under/overflow) or `"auto"` (the finite min/max over all samples). See
|
|
73
|
+
[Binning and range](plotting.md#binning-and-range).
|
|
72
74
|
- `label` and `unit` form the axis label `label [unit]`; the unit also appears
|
|
73
75
|
in the automatic y label (`Events / 4 GeV`).
|
|
74
76
|
- `name` is used for file names by `Plot.save(directory)`; it must be a plain
|
|
@@ -60,7 +60,8 @@ def render_section(gallery: ModuleType, example: Any) -> str:
|
|
|
60
60
|
return (
|
|
61
61
|
f"## {example.title}\n\n"
|
|
62
62
|
f"{example.description}\n\n"
|
|
63
|
-
f
|
|
63
|
+
f""
|
|
64
|
+
f'{{ width="{example.image_width}" }}\n\n'
|
|
64
65
|
f"```python\n{code}```\n"
|
|
65
66
|
)
|
|
66
67
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -86,12 +86,98 @@ shorter `ratio_label` such as `"Ratio"` to keep it at full size. The
|
|
|
86
86
|
computed values are returned in `Plot.ratios` as
|
|
87
87
|
[`Ratio`][rootfig.Ratio] objects (`values`, `errors`, `band`, `edges`).
|
|
88
88
|
|
|
89
|
+
## Binning and range
|
|
90
|
+
|
|
91
|
+
`bins` takes an `int`, a `(n, low, high)` triple, a sequence of edges or a
|
|
92
|
+
`hist` axis, and is shared by every sample of one plot (and by the numerator
|
|
93
|
+
and denominator of an [efficiency](#efficiencies)).
|
|
94
|
+
|
|
95
|
+
With an integer `bins` the range comes from `range`:
|
|
96
|
+
|
|
97
|
+
| `range` | Meaning |
|
|
98
|
+
| --- | --- |
|
|
99
|
+
| `(low, high)` | explicit |
|
|
100
|
+
| `"robust"` | **default**: the min/max of the data, ignoring values far from the bulk |
|
|
101
|
+
| `"auto"` | the full finite minimum and maximum over all samples |
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
rf.plot("events.root", "d0_significance", bins=50) # robust
|
|
105
|
+
rf.plot("events.root", "d0_significance", bins=50, range="auto") # full extent
|
|
106
|
+
rf.plot("events.root", "d0_significance", bins=50, range=(-5, 5)) # explicit
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
!!! note "Rejected values are not discarded"
|
|
110
|
+
|
|
111
|
+
A value outside the range is **not removed from the data**: it goes to the
|
|
112
|
+
under/overflow, shown by the flow arrows (`flow="show"` turns them into
|
|
113
|
+
visible bins, `flow="sum"` folds them into the edge bins). Statistics boxes
|
|
114
|
+
and `rf.summarize` are computed before binning, so means and entry counts
|
|
115
|
+
cover the full sample whichever range is used.
|
|
116
|
+
|
|
117
|
+
This happens in two steps. Outliers are rejected by their modified z-score
|
|
118
|
+
(`0.6745 * |x - median| / MAD`), with a threshold of 30, which removes sentinels
|
|
119
|
+
and anything else far from the bulk. That is done within each sample, and the
|
|
120
|
+
ranges they keep are unioned, so a sample is judged against its own median and
|
|
121
|
+
spread: a signal offset from a background is not an outlier merely because the
|
|
122
|
+
background outnumbers it, and a sample keeps the same values whether it is
|
|
123
|
+
plotted alone or in an overlay. The threshold is then tightened for as
|
|
124
|
+
long as each step costs no more than an *additional* 1 percent of any one
|
|
125
|
+
sample, by entries and by weight. Additional is meant literally: the budget is
|
|
126
|
+
measured against what the first step already moved out of the view, which may
|
|
127
|
+
be a good deal more than 1 percent. This second step cuts a tail that reaches
|
|
128
|
+
far but thins out smoothly, the kind a distance threshold keeps and that leaves
|
|
129
|
+
the interesting part of the distribution in a corner of the axis.
|
|
130
|
+
|
|
131
|
+
The budget is charged per sample rather than over the pooled entries, so a small
|
|
132
|
+
signal sitting far from a large background keeps its own place on the axis
|
|
133
|
+
instead of being cut as a rounding error, and it is charged against the weight a
|
|
134
|
+
cut would remove as well as the entries, so a handful of high-weight entries is
|
|
135
|
+
not treated as negligible. A sample with fewer than 20 distinct values is
|
|
136
|
+
categorical — counts, flags, multiplicities — has no tail to cut and gets no
|
|
137
|
+
budget at all, so the second step takes no value off its axis; this is decided
|
|
138
|
+
per sample too, and holds when it is overlaid with a continuous one. If MAD is zero, the mean absolute deviation from
|
|
139
|
+
the median is used instead. Every candidate is padded by 5 percent and clamped
|
|
140
|
+
to the `"auto"` range (whose upper edge is nudged above the maximum to include
|
|
141
|
+
it), so the range never reaches past the data. Degenerate ranges are widened
|
|
142
|
+
symmetrically. The threshold cannot distinguish sentinels from valid data.
|
|
143
|
+
Cases where you may want `range="auto"`:
|
|
144
|
+
|
|
145
|
+
- a distribution with a long tail (log-normal, Student-t, or an invariant mass
|
|
146
|
+
with a continuum) has valid tail entries pushed into the flow bins — this is
|
|
147
|
+
what the second step is for, so `"auto"` is the way to see the whole tail;
|
|
148
|
+
- a sparse discrete distribution can lose rare valid values from the visible
|
|
149
|
+
range, for example the ones in a binary sample with 999 zeros and one one;
|
|
150
|
+
- a lone value far from a bulk of near-identical ones looks exactly like a
|
|
151
|
+
sentinel and is rejected with them, however real it is;
|
|
152
|
+
- `xbreak=(a, b)` is validated against the inferred axis, so a break meant to
|
|
153
|
+
span a far tail needs `range="auto"` or an explicit range.
|
|
154
|
+
|
|
155
|
+
Inferring a robust range requires additional median and deviation calculations
|
|
156
|
+
over the combined samples, with additional time and memory costs. An explicit
|
|
157
|
+
range avoids range inference.
|
|
158
|
+
|
|
159
|
+
!!! tip "The plot is mostly empty space"
|
|
160
|
+
|
|
161
|
+
The inferred range cuts a thin tail, but only as far as its coverage
|
|
162
|
+
budget allows. A distribution whose tail carries more than that — a heavy
|
|
163
|
+
Student-t, a steeply falling spectrum over several decades — still spreads
|
|
164
|
+
the axis over bins holding a fraction of a percent of the peak. Three ways
|
|
165
|
+
out, in order of how often they are what you want:
|
|
166
|
+
|
|
167
|
+
- `range=(a, b)` around the core. Nothing is lost: entries outside go to
|
|
168
|
+
the flow bins, where `flow="hint"` (the default) marks them with arrows
|
|
169
|
+
and `flow="sum"` folds them into the edge bins.
|
|
170
|
+
- `logy=True`, which makes the tail visible instead of hiding it.
|
|
171
|
+
- `xbreak=(a, b)` to cut the empty middle out and keep both ends, with
|
|
172
|
+
`range="auto"` or an explicit range so the break lies inside the axis.
|
|
173
|
+
|
|
89
174
|
## Axes
|
|
90
175
|
|
|
91
176
|
- `logx`, `logy`: logarithmic scales. Log-spaced bins: `bins=rf.log_bins(n, low, high)`.
|
|
92
177
|
- `xlim`, `ylim`: limits; `ylim=(None, 1e4)` keeps the automatic lower value.
|
|
93
|
-
Automatic y limits
|
|
94
|
-
linear scale,
|
|
178
|
+
Automatic y limits add a small margin above the tallest bin (a factor 1.2 in
|
|
179
|
+
linear scale, 12 in log scale) and then raise it further as the drawn
|
|
180
|
+
legend, label, statistics box and text lines need.
|
|
95
181
|
- `xbreak=(a, b)`: cut the range between `a` and `b` out of the x axis and
|
|
96
182
|
draw the two remaining segments side by side with break marks, sharing the
|
|
97
183
|
y axis (and the ratio panel, if any). Useful for a peak plus a far tail or
|
|
@@ -99,7 +185,8 @@ computed values are returned in `Plot.ratios` as
|
|
|
99
185
|
(`Plot.ratio_ax_right`). Not available together with `ax=` or `flow="show"`.
|
|
100
186
|
|
|
101
187
|
{ width="60%" }
|
|
102
|
-
- `flow`: how under/overflow is shown
|
|
188
|
+
- `flow`: how under/overflow is shown (this is where entries outside an
|
|
189
|
+
inferred [range](#binning-and-range) end up), `"hint"` (small arrows, default),
|
|
103
190
|
`"show"` (extra bins labelled `<low` / `>high`, added on a side as soon as any
|
|
104
191
|
sample has content there, identical for all samples and the ratio panel),
|
|
105
192
|
`"sum"` (added to the edge bins before anything is computed, so ratios,
|
|
@@ -110,9 +197,11 @@ computed values are returned in `Plot.ratios` as
|
|
|
110
197
|
- `xlabel`, `ylabel`, `unit`, `title`. The title sits above the axes, where
|
|
111
198
|
the CMS-style label is also drawn; with such a style prefer `text=`.
|
|
112
199
|
- Automatic y limits leave room for the legend, the experiment label, the
|
|
113
|
-
statistics box and `text` lines:
|
|
114
|
-
|
|
115
|
-
|
|
200
|
+
statistics box and `text` lines: a small fixed margin is added above the
|
|
201
|
+
tallest bin, and the upper limit is then raised until none of them covers a
|
|
202
|
+
histogram (the legend picks a free upper corner). Room is only made for
|
|
203
|
+
what is actually drawn, so a plot without annotations keeps the margin.
|
|
204
|
+
A `ylim` with an explicit upper value switches this off.
|
|
116
205
|
|
|
117
206
|
## Legend, labels, text and statistics
|
|
118
207
|
|
|
@@ -19,6 +19,11 @@ the weight (nothing else), evaluates them, fills a `hist.Hist` and draws it.
|
|
|
19
19
|
If the file contains exactly one tree you can leave `tree` out. The file can
|
|
20
20
|
also be a glob (`"run_*.root"`), a list of files, or `"file.root:tree"`.
|
|
21
21
|
|
|
22
|
+
A bare `bins=50` infers the range from the data, ignoring far outliers so that
|
|
23
|
+
sentinel values such as `-999` do not set the axis; pass `range=(low, high)` to
|
|
24
|
+
be explicit or `range="auto"` for the full extent (see
|
|
25
|
+
[Binning and range](plotting.md#binning-and-range)).
|
|
26
|
+
|
|
22
27
|
The return value is a [`Plot`][rootfig.Plot] with `fig`, `ax`, `hists` and a
|
|
23
28
|
`save()` method:
|
|
24
29
|
|
|
@@ -125,15 +125,16 @@ def quick() -> rf.Plot:
|
|
|
125
125
|
@example("overlay_ratio", "Several samples, normalised, with a ratio panel")
|
|
126
126
|
def overlay_ratio() -> rf.Plot:
|
|
127
127
|
"""A ``{label: file}`` mapping gives one histogram per sample with a binning shared by
|
|
128
|
-
all. ``
|
|
129
|
-
|
|
128
|
+
all. An integer ``bins`` infers the range from every sample at once, cutting the thin
|
|
129
|
+
end of the tail. ``normalize=True`` scales each to unit area and ``ratio=True`` adds a
|
|
130
|
+
panel with every sample divided by the first, uncertainties propagated."""
|
|
130
131
|
return rf.plot(
|
|
131
132
|
{"Signal": "signal.root", "Z + jets": "background.root"},
|
|
132
133
|
"Muon_pt",
|
|
133
134
|
tree="events",
|
|
134
135
|
selection="Muon_isTight and abs(Muon_eta) < 2.5",
|
|
135
136
|
weight="weight",
|
|
136
|
-
bins=
|
|
137
|
+
bins=40,
|
|
137
138
|
unit="GeV",
|
|
138
139
|
normalize=True,
|
|
139
140
|
ratio=True,
|
|
@@ -204,17 +205,21 @@ def log_axes(signal: rf.Sample, zjets: rf.Sample, diboson: rf.Sample) -> rf.Plot
|
|
|
204
205
|
)
|
|
205
206
|
|
|
206
207
|
|
|
207
|
-
@example(
|
|
208
|
+
@example(
|
|
209
|
+
"robust_range",
|
|
210
|
+
"Automatic ranges: robust by default, or the full extent",
|
|
211
|
+
image_width="100%",
|
|
212
|
+
)
|
|
208
213
|
def robust_range(signal: rf.Sample, zjets: rf.Sample) -> rf.Plot:
|
|
209
|
-
"""Sentinels such as ``-999`` wreck an automatic range
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
214
|
+
"""Sentinels such as ``-999`` wreck an automatic range, so an integer ``bins`` without an
|
|
215
|
+
explicit range infers one robustly: far outliers are ignored when choosing the axis, and
|
|
216
|
+
the thin end of a tail is cut as long as few entries leave the view (nothing is removed
|
|
217
|
+
from the data, it ends up in the flow bins, where the arrows point at it).
|
|
218
|
+
``range="auto"`` asks for the full finite minimum and maximum instead. Passing ``ax=``
|
|
219
|
+
draws into your own axes, so two rootfig plots share one figure."""
|
|
213
220
|
_, (left, right) = plt.subplots(1, 2, figsize=(11, 4.2))
|
|
214
221
|
rf.plot([signal, zjets], "lep_iso", bins=40, range="auto", ax=left, title='range="auto"')
|
|
215
|
-
return rf.plot(
|
|
216
|
-
[signal, zjets], "lep_iso", bins=40, range="robust", ax=right, title='range="robust"'
|
|
217
|
-
)
|
|
222
|
+
return rf.plot([signal, zjets], "lep_iso", bins=40, ax=right, title="default (robust)")
|
|
218
223
|
|
|
219
224
|
|
|
220
225
|
@example("xbreak_ratio", "A broken x axis: peak and far tail without the empty middle")
|
|
@@ -233,6 +238,7 @@ def xbreak_ratio(
|
|
|
233
238
|
logy=True,
|
|
234
239
|
xbreak=(125, 195),
|
|
235
240
|
style=atlas,
|
|
241
|
+
figsize=(7, 5.6),
|
|
236
242
|
)
|
|
237
243
|
|
|
238
244
|
|
|
@@ -339,27 +345,31 @@ def arrays() -> rf.Plot:
|
|
|
339
345
|
def hist2d(signal: rf.Sample, met: rf.Variable) -> rf.Plot:
|
|
340
346
|
"""``rf.plot2d`` fills a 2D histogram of one sample; both variables must have the same
|
|
341
347
|
structure (both per-event here). ``logz`` and ``cmap`` control the colour scale."""
|
|
342
|
-
|
|
348
|
+
p = rf.plot2d(
|
|
343
349
|
signal,
|
|
344
350
|
rf.Variable("sum(Jet_pt)", bins=(40, 0, 800), label=r"$H_T$", unit="GeV"),
|
|
345
351
|
met,
|
|
346
352
|
selection="nJet >= 2",
|
|
347
353
|
logz=True,
|
|
348
354
|
cmap="magma",
|
|
349
|
-
zlabel="Events",
|
|
350
355
|
)
|
|
356
|
+
p.fig.axes[-1].set_ylabel("") # Omit the colour-bar title in this gallery layout.
|
|
357
|
+
return p
|
|
351
358
|
|
|
352
359
|
|
|
353
360
|
@example("correlation", "A correlation matrix")
|
|
354
361
|
def correlation(signal: rf.Sample) -> rf.Plot:
|
|
355
362
|
"""``rf.correlation`` computes the (weighted) linear correlation of several per-event
|
|
356
363
|
quantities and draws it as an annotated matrix; ``percent=True`` labels cells in percent."""
|
|
357
|
-
|
|
364
|
+
p = rf.correlation(
|
|
358
365
|
signal,
|
|
359
366
|
["MET", "sum(Jet_pt)", "nJet", "nMuon", "m_ll"],
|
|
360
367
|
labels=[r"$E_T^{miss}$", r"$H_T$", r"$N_{jet}$", r"$N_{\mu}$", r"$m_{\ell\ell}$"],
|
|
361
368
|
percent=True,
|
|
369
|
+
figsize=(7, 5.6),
|
|
362
370
|
)
|
|
371
|
+
p.fig.axes[-1].set_ylabel("") # Omit the colour-bar title in this gallery layout.
|
|
372
|
+
return p
|
|
363
373
|
|
|
364
374
|
|
|
365
375
|
@example("luminosity", "Cross sections and a luminosity instead of hand-made scale factors")
|
|
@@ -27,6 +27,7 @@ class Example:
|
|
|
27
27
|
name: str
|
|
28
28
|
title: str
|
|
29
29
|
func: Callable[..., rf.Plot]
|
|
30
|
+
image_width: str = "75%"
|
|
30
31
|
|
|
31
32
|
@property
|
|
32
33
|
def description(self) -> str:
|
|
@@ -47,11 +48,13 @@ class Example:
|
|
|
47
48
|
EXAMPLES: list[Example] = []
|
|
48
49
|
|
|
49
50
|
|
|
50
|
-
def example(
|
|
51
|
+
def example(
|
|
52
|
+
name: str, title: str, *, image_width: str = "75%"
|
|
53
|
+
) -> Callable[[Callable[..., rf.Plot]], Callable[..., rf.Plot]]:
|
|
51
54
|
"""Register a gallery example."""
|
|
52
55
|
|
|
53
56
|
def register(func: Callable[..., rf.Plot]) -> Callable[..., rf.Plot]:
|
|
54
|
-
EXAMPLES.append(Example(name, title, func))
|
|
57
|
+
EXAMPLES.append(Example(name, title, func, image_width=image_width))
|
|
55
58
|
return func
|
|
56
59
|
|
|
57
60
|
return register
|