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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions