Skip to content

Support N-D array columns when constructing a Dataset from Julia data #55

Description

@CarloLucibello

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) — scalars
  • List(feature) / Sequence(feature) — variable-length (or fixed-length) nested lists
  • Array2D/Array3D/Array4D/Array5D(shape, dtype)fixed-shape tensors; shape is
    Python row-major, first axis may be None for a variable dim
  • Image / Audio / Video — modality features

Verified empirically against the pinned datasets 4.x:

  1. Inferencefrom_dict with no explicit features infers List(List(Value)) for a
    column of 2-D arrays, not Array2D. Rectangular data still works; Array2D only adds
    shape/dtype enforcement.
  2. 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 guarantees numpy2jl(jl2numpy(x)) == x.
  3. 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 real Matrix.
    • 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.
  4. Per-column formatting worksset_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 explicit ArrayND with 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's Vector{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).
  • W2jl2numpy each element, infer List(List). Simplest; rectangular round-trips;
    ragged allowed but won't stack.
  • W3jl2numpy + explicit ArrayND features (reversed shape). Enforces shape/dtype,
    reliable stacking; needs exposing Array2D…5D (ties into the Features/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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions