Skip to content

Indexing

Indexing a frame-labeled array follows NumPy-style indexing, and extends it so that you can also select along an axis by filtering its labels. The two styles can be mixed in a single indexing expression, one entry per axis, just as with a NumPy array.

Eager only for now

Indexing is currently supported on eager arrays. To index a lazy array, convert it to the eager implementation first.

Given an N-dimensional array, there are five ways to index it.

1. Single-valued numerical indexing

When an axis is indexed with a single integer, the N-1 dimensional slice at that location is returned, and that axis disappears from the result. This matches NumPy exactly.

import numpy as np
import polars as pl

import polder as pld

values = np.array([
    [1.0, 2.0, 3.0],
    [4.0, 5.0, 6.0],
    [7.0, 8.0, 9.0],
    [10.0, 11.0, 12.0],
    [13.0, 14.0, 15.0],
])
labels = [pl.DataFrame({"x": [0, 1, 2, 3, 4]}), pl.DataFrame({"y": [10, 20, 30]})]
array = pld.from_values_and_labels(values, labels)

row = array[2]  # shape (3,), the "x" axis is gone
column = array[:, 1]  # shape (5,), the "y" axis is gone

assert row.shape() == (3,)
assert column.shape() == (5,)

2. Multi-valued numerical indexing

When an axis is indexed with a slice or a numerical array (of the same type as the values), the array is sliced along that axis and the result keeps its dimensionality. Indexing several axes at once works as you would expect.

sliced = array[1:3]              # shape (2, 3)
picked = array[[1, 3], [0, 2]]   # shape (2, 2)

assert sliced.shape() == (2, 3)
assert picked.shape() == (2, 2)

As with NumPy, a slice such as array[1:3] can return a view of the values, while indexing with an array such as array[[1, 3]] returns a copy. The labels are sliced to match.

3. Simple label filtering

When an axis is indexed with a mapping (such as a dict), it is used to filter the labels of that axis. The keys refer to columns in the label frame, and the values to the single value that is kept for that column. After filtering, that column is removed from the label frame, analogous to single-valued numerical indexing. If it was the only column, the whole axis is removed and the result is N-1 dimensional.

values = np.array([
    [1.0, 2.0, 3.0, 4.0],
    [5.0, 6.0, 7.0, 8.0],
    [9.0, 10.0, 11.0, 12.0],
])
labels = [
    pl.DataFrame({"x": ["a", "b", "c"]}),
    pl.DataFrame({"y": [10, 20, 30, 40]}),
]
array = pld.from_values_and_labels(values, labels)

row = array[{"x": "b"}, :]   # shape (4,), keeps the second row
col = array[:, {"y": 20}]    # shape (3,), keeps the second column

assert row.shape() == (4,)
assert col.shape() == (3,)

If the axis has several label columns, filtering on one of them keeps the axis but narrows it. The filtered column is removed, and the others remain.

labels = [
    pl.DataFrame({"letter": ["a", "b", "c"], "number": [1, 2, 3]}),
    pl.DataFrame({"y": [10, 20, 30]}),
]
array = pld.from_values_and_labels(values[:, :3], labels)

result = array[{"number": 2}, :]  # shape (1, 3), "letter" column kept
assert result.shape() == (1, 3)

4. Complex label filtering

When an axis is indexed with a Narwhals expression, that expression is used to filter the label frame for that axis. Neither the width of the label frame nor the dimensionality of the array changes, the axis is just made shorter to keep the rows that match.

import narwhals as nw

prices = pld.from_values_and_labels(
    np.arange(15.0).reshape(5, 3),
    [pl.DataFrame({"x": [0, 1, 2, 3, 4]}), pl.DataFrame({"y": [10, 20, 30]})],
)

# Keep only the rows of the first axis where x is greater than 2.
result = prices[nw.col("x") > 2]
assert result.shape() == (2, 3)

This is the most flexible form, since the full Narwhals expression API is available to describe which rows to keep.

5. Axis creation indexing

When None is used to index the array at position i, a new size-1 unlabeled axis is created between the current axes i-1 and i. This is useful for broadcasting, exactly as None (or np.newaxis) is in NumPy.

a = pld.from_values_and_labels(np.arange(4.0), [pl.DataFrame({"i": np.arange(4)})])
b = pld.from_values_and_labels(np.arange(3.0), [pl.DataFrame({"j": np.arange(3)})])

# Outer product through broadcasting.
outer = a[:, None] * b[None, :]
assert outer.shape() == (4, 3)

The new axis is unlabeled (its label frame is None), which marks it as broadcastable. See Alignment for how unlabeled axes interact with binary operations.

Indexing the values directly

The five forms above index the whole container, labels included. If you only want to index the underlying value array, and possibly avoid some work, use values with a subscript, as in array.values[...].