Indexing

Selecting and changing parts of arrays: positions, blocks, one item per row or column, sorted columns, coordinates, paths, windows, edges and named indices

BPL has three ways to select. I⌷A takes selectors for leading axes and gives every combination of them. A.[I J] pairs its arrays of positions item by item, as NumPy’s advanced indexing does. P⊃A follows a path into nested arrays. Each section below shows an everyday task and the form that does it. Axis keys covers selecting by key.

Positions

Positions count from 0. A negative position counts from the end, as in Python:

v←10 20 30 40 50
v.[[0 ¯1]]
v₋₁
10 50
50

A position past the end of an axis is an INDEX error. : selects one whole axis. ∞ selects all remaining axes here. Reverse, ⌽, reverses the last axis:

m←[0 1 2 3⋄4 5 6 7⋄8 9 10 11]
⌽m
 3  2 1 0
 7  6 5 4
11 10 9 8

¯∞ inserts an unnamed, unkeyed axis of length one without consuming an input axis. It works with both forms of indexing, as Python’s None does:

⍴[¯∞ ∞]⌷m
⍴m.[: ¯∞ :]
[1 3 4]ₓ
[3 1 4]ₓ

Blocks of rows and columns

⌷ selects every combination of its positions. Here are rows 1 and 2 with columns 0 and 3:

[1 2;0 3]⌷m
4  7
8 11

.[] pairs its arrays instead. The same two arrays read m at row 1, column 0 and at row 2, column 3:

m.[[1 2] [0 3]]
4 11

Named positions build the block for you. Arrays with different axis names don’t pair. Each keeps its own axis, as in xarray’s broadcasting by dimension name:

x←["i":5 "j":5]⍴⍳25
r←1ₓ+⍳["i":3ₓ] ⋄ c←2ₓ+⍳["j":2ₓ]
x.[r c]
 7  8
12 13
17 18

One item from each row or column

Pairing picks one item from each row. k gives a column for each row of m, as NumPy’s m[np.arange(3), k] does:

k←[2 0 3]ₓ
m.[⍳3ₓ k]
2 4 11

A classifier’s loss reads the probability of each sample’s true class. Each column of P holds one sample’s probabilities for three classes. L gives each sample’s true class. The loss is the mean negative log of those probabilities:

P←[0.7 0.1 0.2 0.1⋄0.2 0.8 0.3 0.1⋄0.1 0.1 0.5 0.8]
L←[0 1 2 2]ₓ
P.[L ⍳4ₓ]
- +/÷≢ ⍟P.[L ⍳4ₓ]
0.7 0.8 0.5 0.8
0.3740273068

With named axes, the same pick works through names. labels holds one class number for each sample, along the sample axis b. Because labels and b share that name, they pair and take Pn’s axes in order. labels gives the row and b gives the column, as in P.[L ⍳4ₓ]:

Pn←["c":3 "b":4]⍴P
labels←["b":4]⍴L ⋄ b←⍳["b":4]
Pn.[labels b]
["b":4]⍴[0.7 0.8 0.5 0.8]

An array that appears twice pairs with itself, giving a diagonal:

i←⍳3ₓ ⋄ m.[i i]
0 5 10

Sorting each column

⍋⍠"i" sc grades each column of sc, giving that column’s rows in sorted order. The grades carry both of sc’s axis names. As a selector, they read rows while their j axis lines up with sc’s columns. Each column of sc is read in sorted order, as NumPy’s take_along_axis does with argsort:

sc←["i":3 "j":4]⍴[3 1 4 1⋄5 9 2 6⋄5 3 5 8]
gr←⍋⍠"i" sc
gr
sc.[gr]
[0 0 1 0
 1 2 0 1
 2 1 2 2]ₓ
3 1 2 1
5 3 4 6
5 9 5 8

Coordinates

⍸ gives the coordinates where a condition holds. A selector of coordinates reads at each one:

m.[⍸m>8]
9 10 11

Assigning through the same selector changes those items:

z←m ⋄ z.[⍸z>8]←0 ⋄ z
0 1 2 3
4 5 6 7
8 0 0 0

Paths

⊃ follows a path into nested arrays, one level for each item of the path. Item 2 of item 1:

n←[1 2;3 4 5]
[1 2]⊃n
5

Index also takes arrays of paths, with ¯∞ placing new axes before the paths or among the selected cells’ axes.

Windows and neighbours

Adding two named indices builds a table of positions, because their names differ. Reading a vector through that table gives sliding windows:

w←10 20 30 40 50 ⋄ s←⍳["s":3] ⋄ t←⍳["t":3]
s+t
w.[s+t]
0 1 2
1 2 3
2 3 4
10 20 30
20 30 40
30 40 50

Differences between neighbours read a vector twice, one position apart:

y←10 20 40 70 ⋄ p←⍳["p":3]
y.[p+1]-y.[p]
["p":3]⍴[10 20 30]

Edges

A neighbour’s position comes from arithmetic and can fall off the axis. At the start, e-1 is ¯1, which reads the last item:

u←10 20 30 40 ⋄ e←⍳4ₓ
u.[e-1ₓ]
40 10 20 30

•edge says what happens at the edge instead. 4•edge e-1 turns each position outside an axis of length 4 into NaN. A NaN position reads fill. Reading u through these positions shifts it right, with a zero at the start:

4•edge e-1
u.[(4•edge e-1)]
$n 0 1 2
0 10 20 30

•edge "clamp" gives a function that moves each position outside the axis to the nearest end instead. "wrap", "reflect" and "symmetric" give the other modes:

clamp←•edge "clamp"
u.[(4 clamp e-1)]
10 10 20 30

On an axis of length 6, here is where each mode sends the positions from ¯2 to 5:

[wrap reflect symmetric]←•edge "wrap" "reflect" "symmetric"
pos←¯2ₓ+⍳8ₓ
6 clamp pos
6 wrap pos
6 reflect pos
6 symmetric pos
[0 0 0 1 2 3 4 5]ₓ
[4 5 0 1 2 3 4 5]ₓ
[2 1 0 1 2 3 4 5]ₓ
[1 0 0 1 2 3 4 5]ₓ

Changing a selection

Any selection can be an assignment target:

a←10 20 30 40 50 ⋄ a.[[1 3]]←0 ⋄ a
10 0 30 0 50

To write a block, use ⌷ with a space before ←. The space makes the whole run before it the target:

bk←3 6⍴0 ⋄ [0 2;1 4]⌷bk +←10 ⋄ bk
0 10 0 0 10 0
0  0 0 0  0 0
0 10 0 0 10 0

Modified assignment applies once for each time a position appears. Counting a histogram takes one line:

h←5⍴0 ⋄ h.[[1 3 1 1]]+←1 ⋄ h
0 3 0 1 0

A write through a NaN position writes nothing. Passing bin numbers through •edge first drops the values that fall outside the bins, here 7 and ¯1:

g←5⍴0 ⋄ d←0.5 1.5 1.7 4.2 7 ¯1
g.[(5•edge ⌊d)]+←1 ⋄ g
1 2 0 0 1

A write is dropped wherever the same read gives fill. 5↑abc reads two fills past the end of abc. Writing through it changes only the three items that exist:

abc←1 2 3 ⋄ 5↑abc ←10 20 30 40 50 ⋄ abc
10 20 30

First of an empty string reads a fill. Under can therefore change the first letter of each word, empty words included, with no special case. 1•c converts to uppercase:

1•c@↑¨ "hello" "" "world"
"Hello" "" "World"

Under

f@g applies f to the selection that g makes, then writes the result back:

{⍵×10}@[1 3] u
10 200 30 400

After a function, .[I] selects from its result. Under can use ⊢.[I] as its selector, with no dfn:

{⍵×10}@ ⊢.[[1 3]] u
10 200 30 400

⊢.name reads a field in the same way, as {⍵.name} does:

orders←[["price":10 "qty":2] ["price":5 "qty":1]]
⊢.price¨ orders
10 5

Named and fresh indices

A named index on its own reads the axis with its name, wherever it’s written. sales.[mo cy] reads months with mo and cities with cy. The result is sales itself:

sales←["city":"NY" "LA";"month":"Jan" "Feb" "Mar"]:[10 20 30⋄40 50 60]
cy←⍳⍠"city" sales ⋄ mo←⍳⍠"month" sales
sales≡sales.[mo cy]
$t

•index gives an index along a fresh name. Two fresh indices build a grid. Subtracting one reading of q from another gives the difference between every pair of items:

q←10 20 40 ⋄ [ri ci]←•index 3 3
q.[ri]-q.[ci]
 0 ¯10 ¯30
10   0 ¯20
30  20   0