Skip to main content

graphed-histogram

Deferred boost-histogram / hist filling for graphed — the dask-histogram shape, without a .compute(): .fill() records, a runner computes.

  • Fill with the boost-histogram API you already use; nothing runs until you hand a plan to a runner, so a thousand-file fill costs nothing to describe.
  • Several histograms sharing a selection run in one pass over the data — a shared sub-expression is read and evaluated once, not once per histogram.
  • On fixed axes histograms add, so partial results merge in any combine tree: integer counts are exact however the run is split, and nothing depends on the worker count. Axes with growth=True merge too, to the result Growth axes defines.

Install

pip install "graphed[awkward]" graphed-histogram   # awkward events + deferred fills
pip install graphed-executors                      # process-pool runners
pip install "graphed-executors[dask]"              # or [parsl] for a cluster

Building graphed from source (no wheel for your platform) needs a Rust toolchain.

Your first deferred histogram

A complete program: awkward events in, a filled boost_histogram.Histogram out. The only new ingredient over eager boost-histogram is a source — the object that hands your dataset out in chunks, so workers each fill their piece. Here it is a parquet file; a ROOT file read through uproot's uproot.graphed works the same way.

import awkward as ak
import boost_histogram as bh
import graphed_histogram as gh
from graphed import Session
from graphed.awkward import AwkwardBackend, from_parquet
from graphed.core.execution import SequentialRunner

events = ak.Array({
    "Jet": ak.zip({"pt": ak.Array([[40.0, 25.0], [55.0], [30.0, 60.0, 20.0],
                                   [80.0], [15.0, 45.0], [70.0, 10.0]])}),
    "MET": ak.zip({"pt": [10.0, 40.0, 70.0, 120.0, 30.0, 90.0]}),
    "genweight": [1.0, 1.0, -1.0, 1.0, 1.0, 1.0],
    "pu_sf": [0.9, 1.1, 1.0, 0.95, 1.05, 1.0],
})
ak.to_parquet(events, "events.parquet")    # stand in for your dataset

s = Session(AwkwardBackend())
evt = from_parquet("events", "events.parquet", steps_per_file=2, session=s)

h = gh.boost.Histogram(bh.axis.Regular(4, 0.0, 100.0), storage=bh.storage.Int64())
h.fill(evt.Jet.pt)                         # records the fill; nothing is read yet

plan = h.plan(steps_per_file=2)            # 2 chunks -> 2 fill tasks + a combine
result = SequentialRunner().run(plan).value
print(result.values())
# [3 4 3 1]

h keeps the eager boost API (axes, storage, views of the empty state); what changed is that .fill() stages work and evaluation belongs to a runner. Ragged values flatten at fill time, exactly as an eager fill(ak.flatten(...)) would.

The one thing that's different

There is no .compute(). You export a plan and run it — and every runner accepts the same plan:

# keep the imports and `events` from the program above, and replace everything after
# them with this — needs graphed-executors. A process pool spawns its workers and each
# re-imports your file, so anything with an effect goes under a __main__ guard: writing
# the file, staging the fill, running. Otherwise a re-importing worker rewrites the very
# file the run is reading.
from graphed_executors.local import ProcessPoolExecutor

if __name__ == "__main__":
    ak.to_parquet(events, "events.parquet")

    s = Session(AwkwardBackend())
    evt = from_parquet("events", "events.parquet", steps_per_file=2, session=s)

    h = gh.boost.Histogram(bh.axis.Regular(4, 0.0, 100.0), storage=bh.storage.Int64())
    h.fill(evt.Jet.pt)

    result = ProcessPoolExecutor(max_workers=2).run(h.plan(steps_per_file=2)).value
    print(result.values())
    # [3 4 3 1]

Same numbers, on two processes.

The dask and parsl runners in graphed-executors take the identical plan onto a cluster; they come with the [dask] and [parsl] extras.

Weights come in factors

HEP event weights arrive as several factors. weight= takes a list of per-event arrays and multiplies them elementwise — no pre-multiplying in your own code:

# continuing from the first example (same evt)
hmet = gh.boost.Histogram(bh.axis.Regular(4, 0.0, 200.0), storage=bh.storage.Weight())
hmet.fill(evt.MET.pt, weight=[evt.genweight, evt.pu_sf])

A single array (weight=w) works as before.

Several histograms, one pass

gh.plan({...}) is the compute(dict_of_hists) analogue: all the fills compile into one graph, so a selection feeding several histograms is read and evaluated once, and only the columns any fill touches are read off disk.

plan = gh.plan({"met": hmet, "jet_pt": h}, steps_per_file=2)
out = gh.unpack(SequentialRunner().run(plan).value)   # {name: histogram}
print(out["jet_pt"].values())
# [3 4 3 1]

If your fills carry systematic variations (via graphed.vary), each name maps to {label: histogram} instead — or pass variation_axis=True to fill() to get one histogram with a variation axis rather than one histogram per label. gh.label_listing(...) shows which labels reach which histogram before you run anything. The variations walkthrough covers the choice.

Which entry point do I want?

You write You get
gh.boost.Histogram(*axes, storage=...) a deferred boost_histogram.Histogram: .fill() records and returns self, fills accumulate, .plan() exports the plan
Hist.new.Reg(100, 0, 200, name="met").Double() the same, through the hist integration — QuickConstruct and named-axis fills
gh.factory(*arrays, histref=...) dask-histogram's factory: a reference histogram's axes plus one staged fill
gh.histogram / histogram2d / histogramdd numpy-like one-liners (explicit bins= and range=)
gh.plan({name: hist, ...}) one plan for several histograms in a single pass

Whichever you build with, a run hands back the class you instantiated, with your axis names and labels intact: boost_histogram.Histogram for gh.boost.Histogram, and hist.Hist or hist.NamedHist for the hist.graphed builders.

The hist builder lives in the hist.graphed module, which hist ships from 2.12 on:

pip install "hist>=2.12"

All standard boost storages and the Regular / Variable / Integer / IntCategory / StrCategory / Boolean axes are supported, and growth=True on Regular, Integer, IntCategory and StrCategory (Growth axes).

Beyond those, the toolbox splits by task:

  • run: h.plan(...), gh.plan(...), gh.unpack(...), gh.add_histograms(a, b)
  • inspect variations: gh.label_listing(...), gh.fill_nodes_by_label(h)
  • identity and reproducibility (advanced): gh.spec_of(h) — the canonical axes/storage description that doubles as the histogram's fingerprint; gh.zero_of(spec) rebuilds the empty histogram anywhere; gh.content_hash, gh.evaluators wire fills into a graph you evaluate yourself. That fingerprint is why a plan re-run on another machine fills the same histogram.

What you can count on

  • Fills read partition by partition; a source's whole-dataset loader is never invoked.
  • On fixed axes, integer counts are exact under any combine tree. Inexact float sums (and every Mean / WeightedMean field) keep their last bits for a given runner family and partitioning, whatever the worker count: floating-point addition is order-sensitive, and each runner fixes its combine tree up front. For growth axes, including the values whose bins depend on the partitioning, see Growth axes.
  • Worker backends are passed as a factory/class or an importable "module:attr" string and built in the worker; a worker missing a required behavior fails loudly rather than filling the wrong thing.

Not supported yet

  • Growing Variable axes. They raise TypeError; declare the edges up front. Other axes grow (Growth axes).
  • dask-style persist / to_delayed. A plan is a live object your script builds, not a file format — rebuild it from the script and hand it to whichever runner you have.
  • Two datasets in one plan. A plan reads one chunked dataset: every fill in it records into the same session, and that session has exactly one partitioned source. Run a plan per dataset and add the results with gh.add_histograms.

Next

Metadata

Release files for graphed-histogram 0.0.7

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for graphed-histogram 0.0.7
File Size Uploaded Provenance Actions
graphed_histogram-0.0.7.tar.gz 200.7 kB With provenance Details for graphed_histogram-0.0.7.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for graphed-histogram 0.0.7
File Interpreter ABI Platform Actions
graphed_histogram-0.0.7-py3-none-any.whl Python 3 none any Details for graphed_histogram-0.0.7-py3-none-any.whl

Total release size: 244.8 kB

Release files / graphed_histogram-0.0.7.tar.gz

Download URL graphed_histogram-0.0.7.tar.gz
Size 200.7 kB
Tags Source
SHA-256 checksum
How to use checksums
ccc3d27871e5d16104e3492c8979b07e71f3032fe0cb66fbf499647d2b6976f7
BLAKE2b-256 checksum
How to use checksums
781597fbadd1e035c8c311e3958fec6b9442e76ca165c975cb10ee558db24533
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 30, 2026.

Transparency log

Release files / graphed_histogram-0.0.7-py3-none-any.whl

Download URL graphed_histogram-0.0.7-py3-none-any.whl
Size 44.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
6e46dfa0eb49bb9ed6c32de22ebe32416918e9b0a6a2972a3a51c98b6494de85
BLAKE2b-256 checksum
How to use checksums
e9e919a205e6b0225e8c42d76d76ec19453b594fa70d276c61825391cd819df0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 30, 2026.

Transparency log

Security for graphed-histogram 0.0.7

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for graphed_histogram-0.0.7.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 30, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page