Repository navigation
Support N-D array columns when constructing a Dataset from Julia data #55
Description
Support N-D array columns when constructing a Dataset from Julia data
PR #54 added Dataset(::AbstractDict) / Dataset(::NamedTuple) but rejects array-valued
(multi-dimensional) columns loudly rather than silently transposing them. This issue
scopes out how to lift that restriction, and pairs naturally with the batch-stacking gap
(item 7 in the internal review). Filing to collect design decisions before implementing.
Python / datasets conventions
Feature types for arrays:
Value(dtype)— scalarsList(feature)/Sequence(feature)— variable-length (or fixed-length) nested listsArray2D/Array3D/Array4D/Array5D(shape, dtype)— fixed-shape tensors;shapeis
Python row-major, first axis may beNonefor a variable dimImage/Audio/Video— modality features
Verified empirically against the pinned datasets 4.x:
- Inference —
from_dictwith no explicitfeaturesinfersList(List(Value))for a
column of 2-D arrays, notArray2D. Rectangular data still works;Array2Donly adds
shape/dtype enforcement. - Orientation — Arrow/numpy are row-major, Julia is column-major. Our
jl2numpy
reverses axes on write ((2,3)→(3,2)),numpy2jl(DLPack) reverses them back; the
package already guaranteesnumpy2jl(jl2numpy(x)) == x. - Read format matters:
- python/default format → array cells come back as nested Python lists (no numpy).
This is what our"julia"format uses today, so an array column reads as
Vector{Vector{…}}, never a realMatrix. - numpy format → array cells come back as numpy
ndarrays, and a range index
stacks rows into an(N, dims…)tensor. - ragged columns under numpy format → object array (can't stack) → must fall back to
per-row.
- python/default format → array cells come back as nested Python lists (no numpy).
- Per-column formatting works —
set_format("numpy", columns=[…], output_all_columns=True)
gives numpy for the array columns and leaves string/scalar columns as Python, so mixed
schemas are fine.
The core problem
Our current "julia" format = python default format + py2jl, so array columns come back
as nested lists (Vector{Vector}), never a real N-D array — and a naive jl2numpy write
path does not line up with that nested read. Supporting N-D requires making write and
read symmetric.
The recipe that works (verified):
- Write — each element via
jl2numpy(axis-reversed numpy); features inferred
List(List)or explicitArrayNDwith the reversed shape. - Read — route array columns through numpy format, then
numpy2jl. The two axis
reversals cancel:
Dataset(d)[i]["x"] == d["x"][i] # single row → real Julia Matrix ✓
Dataset(d)[:]["x"] → (dims…, N) tensor # batch stacks, observation axis LAST (MLUtils convention) ✓This one mechanism solves N-D tensor columns and batch-stacking together.
Design choices to settle
1. Read semantics under the "julia" format
- R1 — leave as nested lists (
Vector{Vector}); no true N-D. - R2 — auto-route numeric array columns through per-column numpy format by default. Real
N-D + free stacking, but changes today'sVector{Vector}batch semantics. - R3 — opt-in, e.g.
with_format(ds, "julia"; stack=true)or a distinct format; default
behavior unchanged, incremental.
2. Write path (constructor)
- W1 — reject N-D (status quo).
- W2 —
jl2numpyeach element, inferList(List). Simplest; rectangular round-trips;
ragged allowed but won't stack. - W3 —
jl2numpy+ explicitArrayNDfeatures (reversed shape). Enforces shape/dtype,
reliable stacking; needs exposingArray2D…5D(ties into theFeatures/ClassLabel
view work). - W4 — a
features=kwarg for user-supplied schema. Most flexible; larger surface.
3. Input shapes to accept for an array column
Vector{Matrix}(element = one observation), and/or- a single stacked
(dims…, N)array (last axis = observations) — mirrors exactly what
the read path returns, giving clean symmetry
(Dataset((; x=batch))[:]["x"] == batch).
4. Ragged / non-numeric columns
- Ragged numeric columns can't numpy-stack → fall back to vector-of-arrays on read.
- Only numeric dtypes go through DLPack; strings/mixed stay on the python-default path.
Recommendation
Implement the symmetric write + read recipe (covers N-D construction and batch stacking
in one pass, sharing the axis-reversal logic), accept both input shapes, and gate the
stacking read behind an opt-in first (R3) so current "julia" semantics don't change —
promoting to default (R2) can follow once it's proven. Start with inferred List(List)
features (W2) and add explicit ArrayND (W3) alongside the planned Features view.
Context: follow-up to #54.
Activity
CarloLucibello commentedon Jul 1, 2026
Python
datasetsusage examplesReproducible snippets illustrating the conventions above (verified against the pinned
datasets4.x; outputs shown as comments).1. Inferred schema — a column of N-D arrays becomes nested
List, notArray2D:2.
numpyformat — per-rowndarray, and a range index stacks rows:3. Explicit fixed-shape schema —
Array2D/3D/4D/5D(shape is row-major):4. Ragged columns — fine per-row, but cannot stack (object array on a batch):
5. Per-column formats — array column as
numpy, keep the rest as Python:How this maps onto the Julia wrapper
then
numpy2jl(DLPack) reverses axes → a Julia(dims…, N)tensor with the obs axislast (MLUtils convention). On the write side
jl2numpyapplies the same reversal, sothe two cancel and
Dataset(d)[i] == d["x"][i].through numpy while leaving strings/scalars on the python-default path (options R2/R3).
List(List)(no new exports) oremit explicit
Array2D…5D(needs exposing theArray*feature types).Make "julia" the default format and support N-D array columns (#55)
Make "julia" the default format and support N-D array columns (#55) (#56