Skip to content

Enhancing CTable, phase 2: pandas engine fix, assign()/col() chaining#676

Merged
FrancescAlted merged 8 commits into
mainfrom
enhancing-ctable2
Jul 16, 2026
Merged

Enhancing CTable, phase 2: pandas engine fix, assign()/col() chaining#676
FrancescAlted merged 8 commits into
mainfrom
enhancing-ctable2

Conversation

@FrancescAlted

Copy link
Copy Markdown
Member

Lands phase 2 of the CTable enhancement work (plan: plans/enhancing-ctable-phase2.md). The remaining items (first-class variable-length strings, mask-based nullable columns) are deferred to plans/enhancing-ctable-phase3.md, included here.

New features

  • CTable.assign(**named_exprs): returns a view with additional computed columns — never mutates the table, never copies column data. Accepts the same forms as add_computed_column, plus Column/NullableExpr and the new unbound expressions.

  • blosc2.col(name): unbound column expression. Defers operator replay until bound to a table (assign(), t[...], where()), reusing the existing Column/NullableExpr operator machinery so null propagation and comparison semantics are identical to the bound form. Enables the pandas-3 headline idiom:

    result = (
        t.assign(profit=col("revenue") - col("cost"))[col("profit") > 0]
        .sort_by("profit", ascending=False)
        .head(10)
    )

Bug fixes

  • engine=blosc2.jit vs pandas 3.0.3: with the default raw=False, DataFrame.apply returned a raw ndarray instead of a properly indexed DataFrame/Series. Now reconstructs the pandas object, mirroring pandas' own raw=True path. Series.map(f, engine=blosc2.jit) is now implemented; non-numeric columns raise a clear ValueError instead of a deep numexpr error.
  • CTable.head()/tail() on lazily-sorted views silently discarded sort order (they ignored _cached_live_positions); t.where(...).sort_by(...).head(10) came back in physical order. Pre-existing bug, found while testing the chain above.
  • @blosc2.dsl_kernel-decorated groupby UDF aggregations crashed unconditionally (calling-convention mismatch); the wrapped plain function is now used.

Explicitly not merged

The plan's P4 (segmented reduceat acceleration for dsl_kernel groupby UDFs) was built, verified correct, and rejected by its own benchmark gate: 1.2x measured vs the required ≥5x — the groupby pipeline's partial-merging bookkeeping dominates, not the per-group UDF calls. Details in the plan's P4 implementation notes; only its crash fix (above) was kept.

Docs / examples / benchmarks

  • New guide "Using Blosc2 as a pandas engine" (with the measured 4.3x axis=0 win on 1M×8, and why axis=1 is not a fit), plus bench/bench_pandas_engine.py and examples/pandas_engine.py.
  • examples/ctable/assign_col.py and examples/ctable/treestore_bundle.py.
  • CTable.assign/blosc2.col added to the API reference with a chained-pipelines section.

Testing

  • tests/ctable (1332 tests) and tests/test_pandas_udf_engine.py all green, including new end-to-end tests through real pandas 3.0.3 and 11 new tests for col()/assign().

🤖 Generated with Claude Code

FrancescAlted and others added 8 commits July 16, 2026 12:05
DataFrame.apply(f, engine=blosc2.jit) returned a raw NumPy array instead
of a properly indexed DataFrame/Series when raw=False (the default),
since pandas only reconstructs the pandas object itself for raw=True.
Series.map(f, engine=blosc2.jit) now works instead of always raising
NotImplementedError. Non-numeric columns raise a clear ValueError
instead of a deep numexpr error.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
col(name) builds a deferred column expression that replays operators
against a table's columns only once bound (assign(), indexing, where()),
reusing all existing Column/NullableExpr null-propagation and comparison
semantics rather than a second expression engine. assign() returns a view
with additional computed columns without mutating the table or copying
column data.

Also fixes a pre-existing bug found while testing the chain end-to-end:
CTable.head()/tail() ignored _cached_live_positions and silently returned
rows in physical order instead of sorted order after a lazily-sorted view.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Built and benchmarked the segmented reduceat-based UDF aggregation path
specified for P4 (1e6 rows, 100k groups, a.max()-a.min()). Measured 1.2x
against the plan's own >=5x gate (checked at other scales too: 1.1x-1.4x,
never close), root-caused via cProfile to per-chunk-per-group Python
bookkeeping in _merge_partials/_compute_partials that both the loop and
segmented paths pay identically and that segmentation cannot skip. Per the
plan's own benchmark-gate rule, the dispatch is not merged; the code was
reverted rather than left in disabled.

Kept the one fix worth keeping on its own merits: a @blosc2.dsl_kernel-
decorated function passed as a groupby UDF aggregation crashed
unconditionally (DSLKernel.__call__ uses a different calling convention
than this call site expects) -- now unwrapped to the plain function.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Demonstrates engine=blosc2.jit for DataFrame.apply/Series.map, including
the axis=0 speedup (measured, not just asserted) and the honest axis=1
result (no speedup, typically slower), plus the clear error for
non-numeric columns.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
axis=1 is slower with the engine (already noted in the code comment);
showing timings for it added noise without a point. Also fixes column
alignment in the axis=0 timing output.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Demonstrates the pandas-3 style chain, col() reuse across tables, null
propagation, assign() on a view plus its read-only/no-copy guarantees,
and the two documented error cases (unknown column at bind time, no
method calls unbound).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ase-3 plan

- Add CTable.assign and blosc2.col to the API reference, with a short
  chained-pipelines section.
- Fix the assign() docstring: values are bound against self, not the view.
- Make the pandas major-version test guard numeric instead of a string
  comparison.
- Close the phase-2 plan and carry deferred P3/P5 forward into
  plans/enhancing-ctable-phase3.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@FrancescAlted FrancescAlted merged commit c851704 into main Jul 16, 2026
21 checks passed
@FrancescAlted FrancescAlted deleted the enhancing-ctable2 branch July 16, 2026 12:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant