API
Public API (frozen for 1.0)
Everything in this section is public: documented, tested, and covered by semantic versioning. Before 1.0 the minor digit is the breaking one; from 1.0 a breaking change to anything listed here needs a major bump, and the changelog names every change. Anything not listed here is internal and may change without notice: every _-prefixed name, TreeArrays.meta, TreeArrays.name, TreeArrays.sliced, and the layout of a tree's or a dim's meta NamedTuple. A test (test/public_api.jl) holds the export list to exactly this surface.
Types and construction
TreeDim:TreeDim(name, values),TreeDim(name),TreeDim(name => values). Coordinates are kept exactly as provided.TreeData:TreeData(backing, dims...; kwarg_dims...),TreeData(name => NamedTuple, dims...)(a record with a named record axis),TreeData(X::TreeData, dims...)(append dims).TreeData(A::AbstractDimArray; axes = nothing, kwarg_dims...)(DimensionalData extension): A's dims become named axes with their lookups as labels (:iterbecomes:draw). Number cells stay a zero-copy leaf. One NamedTuple or array per cell (a model's return values,returned(model, chain)) is converted once into records, dense leaves and ragged nodes;axesnames the inner axes, mirroring the value's structure ((pe = (:participant, :trial),)).The four dispatch aliases
TreeArray,TreeNamedTuple,TreeTuple,TreeRaggedArray. They are for dispatch only; always construct throughTreeData. Dispatching onTreeData{P}'s first parameter (the backing type) is supported; the second parameter is internal.TreeActualArray:TreeActualArray(X)andTreeActualArray(X::TreeRaggedArray; dims, align).TreeTable:TreeTable(X; wide = ()).
Reading a tree
dims,outerdim,coords(coords(d)andcoords(X, name)), andparent(X)for the zero-copy backing.X.fieldon a record (Base.getproperty) andpropertynames(X); on a ragged tree of records.fieldmaps over the leaves.On a dense leaf the basic array interface is forwarded to the backing:
size,length,ndims,axes,eltype,getindex,iterate.collect(X)andArray(X)copy the backing out. At the outer level of a ragged tree the same methods answer about the outer axis (count, sub-trees).size(X, name)andaxes(X, name)on every tree shape: by dim name. A name inside a ragged tree answers when every child agrees; reduced and fixed dims have no length.Broadcasting (
f.(X),X .- Y) returns a tree with the same dims. Trees combine when their axes match by name, order and labels; a reduced or fixed dim stretches along the other tree's axis; records go field by field, ragged trees leaf by leaf. A plain array refuses by name.showfor text,text/htmlandtext/markdown, all bounded previews.
Reductions and selection
mapslices:mapslices(f, X; dims, coords = false, threads = false, align = nothing).mean,sum,std,var:(X; dims = nothing, threads = true, align = nothing);stdandvartakecorrected = true.quantile:quantile(X, TreeDim(name, levels); dims),quantile(X, p::NamedTuple; dims),quantile(X, name => p::NamedTuple; dims), each withthreads = true, align = nothing.nanquantilehas the same three forms through the NaNStatistics extension.hdi:hdi(y, mass = 0.9)andhdi(X; dims, mass = 0.9, threads = true, align = nothing).@kernel:@kernel (:reduces => :into) function f(slice) ... end, the(slice, coords)form, and the post-hoc form on an existing function.selectdim:selectdim(X, name => selector)andselectdim(X, name, selector).Scenario sweeps:
map(f, d::TreeDim)andmap(f, Iterators.product(dims::TreeDim...)), withstackaccepted as a spelling of the same thing.
Tables
Tables.columns,Tables.rowsandTables.schemaon aTreeData(the long melt; ragged trees melt long) and on aTreeTable(thewide =pivot, which refuses a ragged axis by name).
Extensions
FlexiChains:
TreeData(chain; extras = false),FlexiChain(X),TreeData(summary::FlexiSummary).DynamicObjects:
save(Val(:mmap), path, X)andload(Val(:mmap), path, T)for mmap-backed caches.NaNStatistics:
nanquantile, as above.
Settled before 1.0 (0.2.0)
Three corners were decided and closed in 0.2.0:
setdimis gone. It was an exported stub that errored on every call; a lazy re-label / fix-a-dim operation can be added later without a breaking change.TreeData(:time)with no backing throws by name. It used to return a builder closure formap; writemap((v, t) -> TreeData(v, :time => t), values, times).An
AbstractArray{<:NamedTuple}backing (an array of structs), or aTupleofNamedTuples, is refused by name at construction. Records are stored struct-of-arrays: useTreeData(:field => (; a = …, b = …), dims...), or wrap each element in its own record leaf. The same refusal fires insidemapslicesand the sweeps when a kernel or cell returns a plainNamedTuple: returnTreeData(:stat => nt)instead.
Settled design points
No eager restructuring: a reduction never pivots, stacks or
vcats, andTreeDatastores itsparentverbatim.Never a valid-looking wrong value:
dims =,coords(X, name)and.fieldon ragged records resolve every name or throw by name.Built-in reducers thread by default;
mapslicesand@kernelopt in withthreads = true.TreeTable(wide =)refuses a ragged axis by name; melt a ragged tree long.
Reference
Everything TreeArrays exports, plus the Base and Statistics methods it extends.
TreeArrays.TreeActualArrayTreeArrays.TreeArrayTreeArrays.TreeDataTreeArrays.TreeDimTreeArrays.TreeNamedTupleTreeArrays.TreeRaggedArrayTreeArrays.TreeTableTreeArrays.TreeTupleTreeArrays._AlignedRaggedTreeActualArrayBase.getpropertyBase.mapBase.mapslicesBase.selectdimBase.sumStatistics.meanStatistics.quantileStatistics.stdStatistics.varTreeArrays.coordsTreeArrays.dimsTreeArrays.hdiTreeArrays.outerdimTreeArrays.@kernel
TreeArrays.TreeActualArray Type
TreeActualArray(X::TreeData)A lazy, zero-copy AbstractArray{T,N} view of a rectangular tree — the way to feed a TreeData to a numeric N-D-array API (ess, rhat, anything typed on AbstractArray) without copying and without losing the dim labels.
A TreeData is deliberately not an AbstractArray, and parent(X) is one only when the leaf happens to be array-backed. TreeActualArray is the explicit, one-way view that crosses the boundary: getindex walks the tree, nothing is materialized, and a TreeNamedTuple's record axis is promoted to a real array dimension.
A = TreeActualArray(X) # <: AbstractArray, zero-copy, labels kept, any backing
ess(A); rhat(A)Both draw representations therefore arrive at the same array — a dense leaf TreeData(arr3d, :draw, :chain, :param => names) and a record TreeData(:param => per_param_matrices, :draw, :chain). The axes are X's real axes in dims order, with the record axis trailing — exactly the (draw, chain, param) shape ess/rhat want.
A numeric scalar-backed leaf, including a fully reduced result, presents as a zero-dimensional array: size(A) == () and A[] === parent(X). Fixed and reduced dimensions remain in dims(A) without adding physical axes. parent(X) reads the number directly when an array interface is not needed.
An N-D outer array of numeric scalar-terminal trees also presents as a numeric array, in the outer array's axis order. This includes a field extracted from a NamedTuple quantile reduction:
using NaNStatistics
X = TreeData(reshape(1.0:24.0, 3, 2, 4), :subject, :time => (0.0, 1.0), :draw)
p05 = nanquantile(X, (p05=0.05, p50=0.5); dims=:subject).p05
A = TreeActualArray(p05) # numeric 2 × 4 view, no dense assembly
A[1, 2] # 7.1
coords(dims(A)[1]) # (0.0, 1.0)Every scalar-grid leaf must hold the same concrete Number type and have no real inner axes; fixed dimensions and reduced ghosts are allowed. Empty grids must retain a concrete scalar-terminal TreeData element type. Outer physical axes and coordinates are preserved. parent(A) recovers the tree and dims(A) its dimension metadata, including fixed/ghost dimensions; those dimensions do not add physical array axes.
The view is read-only and retains its tree for its lifetime. Reads alias the existing backing; replacing a scalar-grid cell with a compatible scalar tree is visible through the view. Keep shapes, axes, coordinates and terminal types unchanged while a view exists. Use collect(A) or Array(A) for an independent, mutable numeric array. Construction validates the leaves without packing their values; a view of a reduced field aliases that field, not the reducer's input. Dense copies preserve shape and value order and use one-based axes.
Other outer arrays of sub-trees are refused even when rectangular — unequal leaves name the outer axis plus the differing inner axis and lengths — unless explicit ragged alignment is requested with dims= and align=. Mixed scalar/array, nonnumeric, nested-grid and heterogeneous scalar types are unsupported. A zero-dimensional array-backed leaf is not a scalar terminal: it works as a dense leaf on its own, but a grid of such leaves still takes the array-subtree refusal path. Tuple-record shapes remain unsupported.
Two lower-level ways across the same boundary: parent(X) is the backing verbatim and zero-copy, but a scalar grid's backing contains trees rather than numbers and a record's backing is a NamedTuple. collect(X) / Array(X) copy array-backed storage; collect(A) / Array(A) materialize the numeric view.
TreeArrays.TreeArray Type
TreeArrayDispatch alias for a TreeData whose parent is an AbstractArray — a dense leaf. This is the only shape that forwards the basic array interface (size, length, ndims, axes, getindex, iterate, collect, Array) to its backing array; a TreeData is still not an AbstractArray subtype.
Because TreeRaggedArray{P<:AbstractArray{<:TreeData}} is a strict subconstraint, those methods also apply to a ragged value at the outer level: size is the outer count and getindex returns a sub-tree.
TreeArrays.TreeData Type
TreeData(backing, dims...; kwarg_dims...)
TreeData(name => namedtuple, dims...)
TreeData(dims::Symbol...)A named, hierarchical, possibly-ragged array over flat backing storage.
A TreeData pairs a parent — the backing storage, never copied — with a tuple of named axes (TreeDim). Positional arguments become dims, in the order of the backing array's axes; a name => values pair gives that axis its coordinates, and keyword arguments become fixed dims.
X = TreeData(randn(1000, 179, 100), :draw, :subject, :time => range(0, 1, 100))
D = TreeData(randn(1000, 8), :draw, :param; placebo = :on) # `placebo` is a fixed dimFour leaf shapes are distinguished by the type of parent. They are dispatch-only aliases — you always construct through TreeData:
| Alias | parent is… | meaning |
|---|---|---|
TreeArray | an AbstractArray of numbers | a dense leaf |
TreeNamedTuple | a NamedTuple | a record; carries a separate record axis, see outerdim |
TreeTuple | a Tuple | a positional record |
TreeRaggedArray | an AbstractArray{<:TreeData} | a ragged nesting of sub-trees of differing shape |
Build a record with the name => NamedTuple form, which names the field axis:
TreeData(:param => (; alpha = a_matrix, beta = b_matrix), :draw, :chain)A backing array is required
TreeData(:time) — dim names and no backing array — throws by name. (Before 0.2.0 it returned a builder closure: a constructor call that silently gave back a non-tree.) To build one tree per item, map an explicit closure:
measurement = map((v, t) -> TreeData(v, :time => t), per_subject_values, per_subject_times)An AbstractArray{<:NamedTuple} backing (an array of structs) is refused by name too: records are stored struct-of-arrays. Use the record form TreeData(:field => (; a = …, b = …), dims...), or wrap each element in its own record leaf.
A TreeData is deliberately not an AbstractArray (only the array-backed TreeArray leaf forwards size/getindex/iterate to its parent). To feed a numeric-array API, wrap it in TreeActualArray; to feed a Tables.jl consumer, use TreeTable.
See also mapslices, quantile, selectdim, dims, coords.
TreeArrays.TreeDim Type
TreeDim(name::Symbol, values)
TreeDim(name::Symbol)
TreeDim(name => values)One named axis of a TreeData. The axis name lives in the type, so reductions that name it (dims = :time) resolve statically; the coordinates live in the instance and are kept exactly as provided.
values decides what kind of dim this is:
values | kind | meaning |
|---|---|---|
a Tuple / AbstractVector / AbstractRange | labelled axis | a real axis; its elements are the coordinates |
missing (the 1-arg form) | unlabelled axis | a real axis with no coordinates |
any scalar (dose = 20) | fixed position | one coordinate, contributing no array axis |
nothing | aggregated ghost | what a reduction leaves behind in place of the axis it collapsed |
A Tuple stays a Tuple, a Vector stays a Vector, a range stays a range — the same keep-as-provided convention quantile's levels and selectdim's subsets follow. Read the coordinates back with coords; never with collect, which answers Any[missing] / Any[nothing] for the two kinds that have no coordinates at all.
Examples
TreeDim(:time, range(0, 1, 100)) # labelled axis
TreeDim(:draw) # unlabelled axis
TreeDim(:ribbon, (0.025, 0.5, 0.975)) # the output axis a `quantile` reduction writes into
TreeDim(:dose, 20) # a fixed position, not an axisSee also TreeData, coords, dims.
TreeArrays.TreeNamedTuple Type
TreeNamedTupleDispatch alias for a TreeData whose parent is a NamedTuple — a record. Its fields are its record axis; read them with X.fieldname (zero-copy: a field that is already a TreeData comes back as is, a raw field is wrapped with the container's inner axes) and name that axis with outerdim.
Construct one with the pair form so the record axis is named: TreeData(:param => (; alpha = …, beta = …), :draw, :chain).
A record's fields melt to wide columns (one column per field) — see TreeTable.
TreeArrays.TreeRaggedArray Type
TreeRaggedArrayDispatch alias for a TreeData whose parent is an array of TreeData — a ragged nesting: one outer axis over sub-trees whose shapes need not agree.
subjects = TreeData(map(s -> TreeData(view(mat, :, idx[s]), :draw, :time), 1:n), :subject)mapslices(f, X; dims = :time) reduces each sub-tree over its own :time axis; reducing the outer axis pushes it down to the leaves and recurses. Nothing is stacked or materialized at any point. A ragged tree also melts to a long table (row count sum(length, leaves)), so it need not be made rectangular before plotting — but TreeTable's wide= pivot does require rectangularity and refuses a ragged axis by name.
TreeArrays.TreeTable Type
TreeTable(X::TreeData; wide = ())A lazy tabular view of a tree: the Tables.jl source you hand to a plotting or data-frame layer.
A TreeData is N-dimensional, possibly-ragged data — not itself a table. TreeTable assigns its dims to tabular roles. Records (TreeNamedTuple fields) are wide by default, because distinct variables are the natural column shape; every other dim is long, contributing one coordinate column, alongside a single :value column. Orientation is a consumer choice imposed by this view, never a property of the data.
Nothing materializes at construction, and nothing materializes on Tables.columns either: the columns handed back are lazy views over the eagerly-computed result, so allocation is flat as the row count grows and repeated metadata is never physically expanded. Rows materialize only at the rowtable / DataFrame / JSON boundary.
r = quantile(Y, :band => (; lower = 0.025, median = 0.5, upper = 0.975); dims = :draw)
Tables.columnnames(Tables.columns(TreeTable(r))) # (:time, :band, :value) — long
Tables.columnnames(Tables.columns(TreeTable(r; wide = :band))) # (:time, :lower, :median, :upper)wide = — pivot labelled axes into columns
Name one dim (wide = :band) or several (wide = (:band, :param), whose columns are the cartesian product of their levels, joined by _ in wide order).
Column naming: a Symbol level becomes a bare column (:lower), which is what drops straight into a ribbon plot's band pairs; any other level is prefixed by its dim (:time levels [0.1, 0.25] become time_0_1, time_0_25). Dots are rewritten to _ in every level, Symbols included, because a . in a column name is read as a nested field by Vega-Lite and would plot silently-wrong data rather than error.
The rules, each erroring by name:
a wide dim may sit anywhere in the melt, not just at the top level — the band axis a chained reduction leaves on the leaf is the whole point;
it must be a real axis — a fixed or ghost dim has no levels to spread;
naming the same dim twice is an error;
level labels colliding with another column, with each other after dot rewriting, or (for several wide dims) whose
_-joined combinations collide, all error and name the culprit —_is not an injective separator;ragged throws under
wide =, and only there. A pivot needs one level set and one column length, and a ragged tree has neither. The long melt of the same tree works — and so does oneTreeTableper sibling: multi-response outputs melt each response on its own, and each rectangular sibling pivots.
Wide-mode column names are not inferable
In wide mode the column names are the axis's coordinate values, which do not live in the type, so Tables.columns/Tables.schema are not type-stable in their names — call them at the presentation boundary, not inside a hot kernel. The columns themselves are still concretely-typed lazy views, which is all a Tables.jl consumer needs. Long mode is unaffected and fully type-stable.
TreeArrays.TreeTuple Type
TreeTupleDispatch alias for a TreeData whose parent is a Tuple — a positional record. This is what a quantile reduction over a Tuple of levels leaves on the leaf.
TreeArrays._AlignedRaggedTreeActualArray Type
TreeActualArray(X::TreeRaggedArray; dims, align)A lazy, zero-copy AbstractArray view of a one-level ragged tree whose leaves differ only along the inner axis dims, aligned under the named policy. The caller must name both — there is no implicit ragged-to-rectangular conversion, and a half-named call throws for the missing keyword. A bare TreeActualArray(X) on array-backed sub-trees keeps refusing; homogeneous numeric scalar-terminal grids instead expose their outer axes without alignment.
A = TreeActualArray(chains; dims = :draw, align = :truncate_min)
ess(A); rhat(A) # any `AbstractArray`-typed consumer, no overloads neededPolicies:
:truncate_min— head truncation: every leaf contributes its firsttargetpositions, wheretargetis the minimum sibling length.:thin_min— equidistant thinning: every leaf contributestargetevenly spread positions (round.(Int, range(1, n; length = target))).:error_equal— refuse unless every sibling already has the same length.
The aligned axis keeps its coordinates under one uniform rule: the READ positions' coordinates must agree across leaves (compared content-wise, so a Tuple beside a Vector still aligns), and the view carries the prototype's. Truncation reads positions 1:target (heads must agree — a chain continued from iteration 101 is not the same iterations 1–60); thinning reads evenly spread positions per leaf (which agree only when the leaves share a grid, so a labelled unevenly-sized thin refuses rather than label positions that mean different things per leaf). Unlabelled axes align vacuously. Non-aligned leaf axes must match in length and coordinates, every leaf must be array-backed at the same rank with the same dim structure and eltype, and the outer container must be a one-dimensional Vector of siblings — anything else throws by name.
TreeArrays.@kernel Macro
@kernel (:reduces => :into) function f(slice) ... end
@kernel (:reduces => :into) function f(slice, coords) ... end
@kernel (:reduces => :into) [coords=true] fGive a per-slice kernel a dim signature, so it also works on a TreeData.
Write the kernel once as plain array math and annotate it with the dim it consumes and the record axis it produces. The defining form emits both the plain-array kernel and a TreeData method that threads it through mapslices; the post-hoc form (a bare function name) adds only the TreeData method to an existing function.
@kernel (:time => :stat) function compute_stats(L)
trough, peak = extrema(L)
baseline = L[1]
dtrough, dpeak = extrema(L .- baseline)
(; trough, peak, baseline, dtrough, dpeak)
end
compute_stats(some_vector) # plain AbstractArray — unchanged
compute_stats(X) # TreeData — reduces :time into a :stat recordCoordinates
A kernel declaring two plain positional arguments additionally receives the reduced axis's coordinates — the AUC / tmax shape:
@kernel (:time => :stat) function nca(y, t)
(; cmax = maximum(y), tmax = t[argmax(y)], auc = trapz(t, y))
end
nca(ragged) # every sub-tree's call sees ITS OWN gridInferring the opt-in from arity is safe here and only here: the macro reads the literal argument list at expansion time rather than probing a value, which at the mapslices boundary would be actively dangerous.
Only plain positional arguments count — a default (f(L, scale = 2)), keywords and a splat (f(L, rest...)) are all one-slice kernels with extra machinery, so they keep the one-argument behaviour instead of having a coordinate vector pushed into their second slot. The post-hoc form has no argument list to read, so it defaults to one argument and takes an explicit opt-in: @kernel (:time => :stat) coords=true nca.
Contradictions (coords=true on a one-argument kernel, and the converse) and unsupported arities are macro-expansion errors, by name.
Not for parameterized reducers
mean/sum/quantile choose their dim and output per call, so they do not fit this fixed-signature shape — use their dims = methods instead.
Base.getproperty Method
X.fieldnameRead one field of a TreeNamedTuple record — post.beta — zero-copy.
A record's fields are its record axis, so . is the natural reader for them. The descent is exactly what a reduction over the record does: a field that is already a TreeData is returned as is (it knows its own dims), and a raw field is wrapped with its container's inner axes, so :draw/:chain survive either way and nothing densifies. Ghost dims stay on the container.
propertynames(X) lists the fields; an unknown name errors and names the record axis and the fields it does have.
On a TreeRaggedArray of records — e.g. per-subject records ragged over :subject — . maps over the leaves: X.beta reads beta from every leaf and re-wraps the children under the container's own outer dims. The fresh outer vector is O(leaves) pointers; the children are zero-copy (each is its leaf's own .beta). Like dims=, access is foundALL: a leaf that is not a record, or a record missing the field, throws and names the leaf — fields are never silently skipped. A doubly-nested ragged container recurses leaf by leaf. propertynames(X) lists the prototype (first) leaf's fields; an empty or non-record ragged container has no accessible fields, so it answers ().
Base.map Method
map(f, d::TreeDim)
map(f, Iterators.product(dims::TreeDim...))Build a scenario sweep: a TreeData whose axes are the swept dims, in order, and whose every cell is whatever f returns: a scalar or a TreeData. A plain NamedTuple cell is refused by name (it would make the sweep an array of structs); return TreeData(:stat => nt) instead.
f receives the coordinate tuple (the labels, not data), in dim order — so destructure it:
ds = (TreeDim(:fit, (:a, :b)), TreeDim(:dose, (20, 200)), TreeDim(:health, ("hi", "lo")))
sweep = map(Iterators.product(ds...)) do (fit, dose, health)
quantile(draws_for(fit, dose, health), TreeDim(:band, (0.1, 0.5, 0.9)); dims = :draw)
end
# sweep isa TreeData; size(parent(sweep)) == (2, 2, 2); dims are (:fit, :dose, :health)Arity-generic — there is no two-dim limit, and label types may be heterogeneous. A returned TreeData stays a leaf; nothing is densified, and the sweep composes with the reductions like any other tree.
A scalar dim is a fixed position, not an axis: it contributes zero array axes and lands as a trailing fixed dim. To sweep one value as a real length-1 axis, pass a 1-tuple — TreeDim(:schedule, ("s",)). An unlabelled dim has no coordinates to sweep and errors by name.
Splat a Tuple, not a generator
Splatting a runtime-length container of dims is inference-opaque, as any runtime-length splat is in Julia. It still works; to also infer, splat a Tuple (Tuple(ds)) or cross a Tuple-typed function barrier.
stack is accepted as a spelling of map here and does the same thing.
Base.mapslices Method
mapslices(f, X::TreeData; dims, coords = false, threads = false, align = nothing)Reduce the named dims of a tree with f, thread every remaining index recursively, and return a TreeData.
dims is a Symbol or a collection of them. f receives the slice along the reduced axis and returns either a scalar (which collapses the axis) or a TreeData (which introduces a new record or output axis). Nothing is stacked, pivoted or vcated: the result stays a tree of arrays, and a reduced axis is kept as an aggregated ghost dim so the provenance of the reduction survives it.
X = TreeData(randn(1000, 179, 100), :draw, :subject, :time => range(0, 1, 100))
qoi = mapslices(maximum, X; dims = :time) # (draw × subject), still a TreeDataReturning a TreeData from f gives the reduction an output axis:
mapslices(X; dims = :time) do y
TreeData(:stat => (; trough = minimum(y), peak = maximum(y)))
endcoords = true — let the kernel see the axis it reduces
By default f sees the data slice only. coords = true additionally hands it the coordinates of the reduced axis — what trapz(t, y) (AUC) and t[argmax(y)] (tmax) need:
mapslices(X; dims = :time, coords = true) do y, t
TreeData(:stat => (; cmax = maximum(y), tmax = t[argmax(y)], auc = trapz(t, y)))
endThe binding happens per node, so on a ragged tree every sub-tree's kernel call sees its own grid — which is exactly why closing over one shared coordinate vector was never an answer there. One reduced axis hands f the bare coordinate vector; several hand it one vector per axis in slice-dim order. Reducing a ragged tree's outer axis hands the kernel that axis's labels.
Two shapes throw rather than answer with a plausible value: an unlabelled axis has no coordinates to give, and a pooled straddle (reducing a ragged outer axis together with a leaf-inner one) concatenates every leaf's slice into one bag that no single axis's coordinates line up with.
@kernel infers this opt-in from the kernel's arity; mapslices does not, deliberately — at this boundary a hasmethod(f, (slice, coords)) probe is true for maximum (because maximum(f, itr) exists) and would silently treat your data slice as a predicate.
threads = true — reduce independent slices on several tasks
Every slice is reduced on its own, so mapslices can spread the slices over Julia's threads. It does so only when asked: TreeArrays cannot see whether your kernel is safe to run on two tasks at once, and a kernel that writes to a buffer it closes over would race and return plausible-looking wrong numbers.
mapslices(X; dims = :draw, threads = true) do y # reads its slice, shares no state
TreeData(:stat => (; lo = minimum(y), hi = maximum(y)))
endThe result is the serial result, cell for cell: the same kernel computes each cell from the same slice and writes it to its own position, nothing is combined across slices, and a kernel that throws throws the exception the serial loop would have. Work is split into at most 2 * Threads.nthreads() tasks, and only when there is enough of it (tens of thousands of elements per task), so with one thread or a small input the reduction stays on the calling task. Dense reductions, the gather over a ragged outer axis, and the pooled straddle all thread; descending into the children of a ragged node does not (each child threads its own slices).
TreeArrays' own reducers — quantile, nanquantile, hdi, mean, sum, var, std — know their kernels, so they default to threads = true; pass threads = false to keep one on the calling task. Each task gets its own copy of their sort buffer.
align — pool ragged leaves whose kept axis differs
Reducing a ragged tree's outer axis — alone (dims = :participant) or pooled together with inner dims (dims = (:draw, :chain, :participant)) — keeps the leaves' other axes and pools position k of every leaf together. That needs every leaf to mean the same thing at k: each kept axis must have the same length and the same labels in every leaf. When it does not — participants with different numbers of trials, subjects sampled at different times — the reduction refuses by name unless align says how to match the labels:
align = :union— every label any leaf carries; at each, pool the leaves that carry it. How many leaves are pooled can change along the axis.align = :intersect— only the labels every leaf carries.
X = TreeData([TreeData(randn(1000, 4, n), :draw, :chain, :trial => 1:n) for n in (30, 25, 28)],
:participant => 1:3)
quantile(X, (lower = 0.025, upper = 0.975); dims = (:draw, :chain, :participant), align = :union)
mean(X; dims = :participant, align = :intersect) # draw × chain × trial 1:25Labels are matched by value, never by position, so an unlabelled axis that would need aligning, and a label repeated within one leaf, throw. The aligned axis is the sorted union when every leaf's labels are ascending numbers, and otherwise lists the labels in the order first met, leaf by leaf. When the leaves already agree, align changes nothing. A coords = true kernel reducing the outer axis sees only the labels of the leaves pooled at that position. To count them, reduce each leaf first and count across the outer axis: mapslices(length, mean(X; dims = (:draw, :chain)); dims = :participant, align = :union).
Rules
Every name in
dimsmust exist somewhere in the tree. A name that resolves nowhere throws:dimsis foundALL, not found-any. The check is made from the type, so a correct call costs nothing at runtime; on a jagged tree it falls back to an instance walk rather than let a typo through.A dim present on some branches and absent on others throws at the branch that lacks it.
Pass the kernel as a concrete function value.
mapslicesspecializes ontypeof(f), so a named function or adoblock monomorphizes the inner loop; a kernel that branches on a runtime value inside the slice loop does not. Select the concrete kernel once, outside.
See also quantile, @kernel, selectdim.
Base.selectdim Method
selectdim(X::TreeData, name => selector)
selectdim(X::TreeData, name::Symbol, selector)Restrict a named axis to the coordinates selector keeps, leaving every other axis intact — the selection dual of the dims = reductions, which collapse an axis instead.
No data is copied. The backing array is sliced with a view, so a subset of a (draw × chain × 600_000) matrix never materializes; only the axis's own (tiny) coordinate labels are subset.
selectdim(X, :param => r"^unit_params_") # Regex: keep labels it `contains`-matches
selectdim(X, :param => startswith("beta")) # a `label -> Bool` predicate
selectdim(X, :param => boolmask) # an explicit Bool mask (length == axis)
selectdim(X, :param => [8, 1, 2]) # integer indices — subset and/or reorder
selectdim(X, :param, sel) # 3-arg form, mirroring Base.selectdim(A, d, i)The filtered axis carries exactly the matched labels, keep-as-provided (a Tuple stays a Tuple; a filtered range necessarily becomes a Vector). The result is a TreeData, so it composes with the reductions: mean(selectdim(X, :param => r"…"); dims = :draw).
Positional selectors (a Bool mask, integer indices) also restrict an unlabelled axis — it stays unlabelled and merely gets shorter. A Regex or predicate on one errors: there are no labels to match.
Like dims =, this is foundALL and never silently wrong. Each of these throws by name: a typo'd axis name; a bare String selector (ambiguous between exact and substring — use ==("x"), contains("x") or a Regex); a predicate that does not return Bool; a wrong-length mask; and selecting an inner or record axis (only the outer array axes are selectable, a ragged tree's outer axis included).
Base.sum Method
sum(X::TreeData; dims = nothing, threads = true, align = nothing)Sum over the named dims, delegating to mapslices. dims = nothing (the default) reduces the whole tree to a scalar. See mean.
Statistics.mean Method
mean(X::TreeData; dims = nothing, threads = true, align = nothing)Mean over the named dims, delegating to mapslices — so it composes with the whole reduction machinery, ragged trees included.
dims = nothing (the default) reduces the whole tree to a single scalar.
mean(X; dims = :subject) # collapse one axis, keep the rest
mean(X) # one numberLike every built-in reducer, it spreads independent slices over Julia's threads when there is enough work (threads = false keeps it on the calling task); the result is identical either way. See Threads in mapslices.
On a ragged tree whose leaves label a kept axis differently (participants with different numbers of trials), reducing the outer axis refuses unless align says how to match the labels: align = :union or align = :intersect. Every built-in reducer takes it; see align in mapslices.
Statistics.quantile Method
quantile(X::TreeData, pdim::TreeDim; dims, threads = true, align = nothing)
quantile(X::TreeData, p::NamedTuple; dims, threads = true, align = nothing)
quantile(X::TreeData, name => spec::NamedTuple; dims, threads = true, align = nothing)Quantiles over the named dims, computing every level in one sorted pass (a reused sort buffer). Never loop quantile once per level. Independent slices run on Julia's threads when there is enough work, with identical results; threads = false keeps the reduction on the calling task (see Threads in mapslices). align = :union | :intersect pools ragged leaves whose kept axis differs, by label (see align in mapslices).
The second argument decides both the levels and the shape of the output:
TreeDim(name, levels)— the levels become a real output axis, kept exactly as given (aTuplestays aTuple, aVectoraVector). A scalar level mirrorsBase: a single fixed coordinate, not a length-1 axis.juliar = quantile(X, TreeDim(:ribbon, (0.025, 0.1, 0.5, 0.9, 0.975)); dims = :draw) med = quantile(X, TreeDim(:median, 0.5); dims = :draw)A
NamedTupleof probabilities — the levels are packed into a record leaf, whose fields melt straight to wide columns. This bakes wide-ness in at reduction time.juliaquantile(X, (; median = 0.5, lo = 0.025, hi = 0.975); dims = :draw)name => NamedTuple— the middle road: a real axis labelled by the keys and computed from the values, so the same reduction can feed a long consumer or be pivoted at the presentation boundary withTreeTable(r; wide = name).juliaquantile(X, :band => (; lower = 0.025, median = 0.5, upper = 0.975); dims = :draw)
NaN
These delegate to Statistics.quantile! and therefore throw on NaN. For NaN-safe bands load NaNStatistics and use nanquantile, which accepts the same three spellings, drops NaNs per slice first, and returns NaN at every level for an all-NaN slice.
See also mapslices, TreeTable, TreeDim.
Statistics.std Method
std(X::TreeData; dims = nothing, corrected = true, threads = true, align = nothing)Standard deviation over the named dims. Same contract as var.
Statistics.var Method
var(X::TreeData; dims = nothing, corrected = true, threads = true, align = nothing)Variance over the named dims, delegating to mapslices.
corrected = true (the default, matching Statistics) gives the sample variance ÷ (n-1); corrected = false gives the population form ÷ n. A pooled reduction — naming a ragged tree's outer axis together with a leaf-inner one, e.g. dims = (:draw, :chain) — is one pass over the whole bag, byte-identical to the same reduction on the equivalent dense tree, and not the composition of two reductions.
mean = is deliberately not forwarded: one precomputed mean cannot apply across the per-slice reductions. threads as in mean.
TreeArrays.coords Method
coords(d::TreeDim)
coords(X::TreeData, name::Symbol)The coordinates an axis is labelled with, exactly as they were provided — a Tuple stays a Tuple, a Vector stays a Vector, a range stays a range.
coords(X, :time) # by name, on this node — the ergonomic form
coords(TreeArrays.dims(X)[1]) # or straight off a TreeDimFour shapes have no coordinates to return and error by name rather than answer with a plausible-looking value — an unlabelled axis (TreeDim(:time)), an aggregated ghost (the dim was reduced away), a fixed single position (dose = 20; pass :dose => (20,) for a real length-1 axis), and a name that resolves nowhere. Like dims =, the two-argument form is foundALL: a name that matches no dim on this node is a typo, not an empty answer. A dim living inside the leaves of a ragged tree is on the child — reach it as coords(parent(X)[i], :time).
Do not use collect for this
collect(d) runs (a TreeDim has iterate/length) and looks like the answer, but it yields an Any-eltype vector and silently returns Any[missing] for an unlabelled axis and Any[nothing] for a reduced ghost — a length-1 coordinate vector for the two kinds that have none.
The name must be a literal
coords(X, :time) is type-stable via Base.@constprop :aggressive, so the name has to be a literal in the code, not a runtime variable — the same rule as dims =. @inferred cannot see const-prop through its own call: test a small wrapper function, not the bare call. The one-argument form is stable unconditionally.
Inside a kernel, ask for the reduced axis's coordinates with mapslices(f, X; dims, coords = true) instead — see mapslices.
TreeArrays.dims Method
dims(X::TreeData)
dims(A::TreeActualArray)The tree's axes, as a tuple of TreeDims — including fixed dims and the aggregated ghosts reductions leave behind, in declaration order.
Self-qualify inside method bodies
The bare name dims collides with the dims = keyword argument used throughout mapslices/quantile, so inside a function that takes one, write TreeArrays.dims(X).
To read one axis's coordinates, prefer coords.
TreeArrays.hdi Function
hdi(y::AbstractVector, mass = 0.9)
hdi(X::TreeData; dims, mass = 0.9, threads = true, align = nothing)The highest-density interval: the shortest interval holding at least mass of the draws. Endpoints are always draws (no interpolation); tied widths resolve to the first (lowest) interval.
hdi(randn(4000), 0.9) # (; lower, upper)
hdi(post.beta; dims = :draw) # per-parameter `(; lower, upper)` record
hdi(post.beta; dims = :draw, mass = 0.5)The tree method reduces dims through mapslices (a reused sort buffer, ragged trees included; threaded like quantile) and packs each slice's interval into a TreeNamedTuple record with lower/upper fields — the same wide-baking shape as quantile(X, p::NamedTuple; dims), so it melts straight to lower/upper columns for an AoV lineribbon(bands = [:lower => :upper]). For several masses, call once per mass.
mass must lie in (0, 1]; an empty slice, and (matching quantile) a slice containing NaN, throws. Drop NaNs first (filter(!isnan, y)) or reduce a NaN-free tree.
TreeArrays.outerdim Method
outerdim(X::TreeNamedTuple)The record axis of a TreeNamedTuple — the TreeDim whose coordinates are the record's field names.
Only a tree built through the pair form (TreeData(:param => (; a = …, b = …), …)) carries one; handing a NamedTuple straight to TreeData(x, dims...) does not name a record axis, and this errors.