feat: add nw.list#3694
Conversation
MarcoGorelli
left a comment
There was a problem hiding this comment.
thanks @CangyuanLi !
my main question is - what's scalars_only?
| >>> data = { | ||
| ... "a": [1, 2, 3], | ||
| ... "b": [4, None, 6], | ||
| ... "d": [[10, 20], None, [40, 50, 60]], | ||
| ... "e": [[100], [200, 300], [None]], | ||
| ... } | ||
| >>> df = nw.from_native(pl.DataFrame(data)) | ||
| >>> df.select( | ||
| ... nw.list("a", "b").alias("list"), nw.list("d", "e").alias("list_of_lists") | ||
| ... ) |
There was a problem hiding this comment.
can we keep it simple and just have:
- a
- b
and use with_columns to make a a_b column?
it is only relevant for thr polars backend and uses concat_list directly instead of the .implode().over() trick. I was worried about the potential performance impact but maybe its leaking implementation details too much? |
|
The stable api tests are failing would you want me to edit the tests to explicitly xfail list, or add it to the stable api? |
The performance impact is relatively minimal for concatenating two columns. It's only really much faster for very small dataframes.
However, it does appear to make a pretty big difference as the number of columns to concatenate increases.
Timings are the median timing of 50 runs, with the CIs representing the 5th and 95th percentile. At 50 columns and 1 million rows, the median time using scalars_only=False 26.26 seconds. Using scalars_only=True, the median time is 8.9694 seconds, a roughly 2.93x speedup. |
| narwhals_to_native_dtype(dtype, self._version).__class__ | ||
| if isinstance(dtype, type) and issubclass(dtype, DType) | ||
| else narwhals_to_native_dtype(dtype, self._version) | ||
| ( | ||
| narwhals_to_native_dtype(dtype, self._version).__class__ | ||
| if isinstance(dtype, type) and issubclass(dtype, DType) | ||
| else narwhals_to_native_dtype(dtype, self._version) | ||
| ) |
There was a problem hiding this comment.
I think this is a change that prek made (probably the ruff-format portion). I can revert it and commit with skip checks.
There was a problem hiding this comment.
ruff-format is passing on main so it wouldn't have been that - you can revert this part anyway, the checks need to pass for the pr to be mergeable
MarcoGorelli
left a comment
There was a problem hiding this comment.
thanks for updating!
some comments
can you also make sure some tests include null values?
| def corr(self, a: ArrowExpr, b: ArrowExpr, *, method: CorrelationMethod) -> ArrowExpr: | ||
| def corr( | ||
| self, a: ArrowExpr, b: ArrowExpr, *, method: CorrelationMethod | ||
| ) -> ArrowExpr: |
There was a problem hiding this comment.
can we revert this (and any other cosmetic-only changes)?
| narwhals_to_native_dtype(dtype, self._version).__class__ | ||
| if isinstance(dtype, type) and issubclass(dtype, DType) | ||
| else narwhals_to_native_dtype(dtype, self._version) | ||
| ( | ||
| narwhals_to_native_dtype(dtype, self._version).__class__ | ||
| if isinstance(dtype, type) and issubclass(dtype, DType) | ||
| else narwhals_to_native_dtype(dtype, self._version) | ||
| ) |
There was a problem hiding this comment.
ruff-format is passing on main so it wouldn't have been that - you can revert this part anyway, the checks need to pass for the pr to be mergeable
| init_series, *series = tuple(chain.from_iterable(expr(df) for expr in exprs)) | ||
| init_series, *series = tuple( | ||
| chain.from_iterable(expr(df) for expr in exprs) | ||
| ) |
| init_series, *series = tuple( | ||
| chain.from_iterable(expr(df) for expr in exprs) | ||
| ) |
| init_series, *series = align(*chain.from_iterable(expr(df) for expr in exprs)) | ||
| init_series, *series = align( | ||
| *chain.from_iterable(expr(df) for expr in exprs) | ||
| ) |
| def build_list_array(arrays: list[pa.Array]) -> pa.ChunkedArray: | ||
| # This works by using concat_arrays to vertically stack the arrays. | ||
| # Then we use take to grab the data by index and to horizontally | ||
| # pack the arrays. | ||
| n = len(arrays[0]) | ||
| num_cols = len(arrays) | ||
| flat = pa.concat_arrays(arrays) | ||
| indices = [j * n + i for i in range(n) for j in range(num_cols)] | ||
| interleaved = pc.take(flat, pa.array(indices)) | ||
| offsets = pa.array(range(0, n * num_cols + 1, num_cols), type=pa.int32()) | ||
| return pa.chunked_array([pa.ListArray.from_arrays(offsets, interleaved)]) |
There was a problem hiding this comment.
nice, clever! out of interest, where does this solution come from?
There was a problem hiding this comment.
I found concat_arrays, realized it didn't do what I wanted, then I found ListArray.from_arrays, and found it confusing, and then I asked Claude to mash them together.
|
i think I fixed all the formatting errors, as well as a typing error. for the stable api tests I followed "from_dict" excluded list from the tests. I also added the nulls in the test data. With the default backend for pandas, something like [None, 5] gets converted to a float, so I only tested pyarrow-backed pandas. Sorry again for the formatting noise and thanks for your patience! |
|
thanks for updating! there may be some coverage issue, check if all lines are covered (or use |
the coverage issue should be fixed! I used no cover for the branch where polars is > 2.0 since it is not out yet |
|
Another try to fix CI errors--old polars versions now use pl.count() instead of pl.len(). I also removed what looked to be an extraneous type ignore |



Description
Adds a nw.list function that constructs a list datatype from columns. It mimics duckdb's list_pack semantics. Scalars are collected into a list and lists are collected into a list of lists.
If you have comments or want to explain your changes, please do so in this section
I basically tried to mimic nw.struct wherever possible. For example, since nw.struct is not implemented for dask, neither is nw.list.
I also added a scalars_only flag which is false by default. This allows us to directly use concat_list for the polars backend without calling .implode().over(). It does nothing for all other backends.
What type of PR is this? (check all applicable)
Related issues
Checklist