basedpl

Axis keys

Glyph index

An array can have string keys on any of its axes. Keys name positions on that axis. Numeric coordinates remain 1-origin. Array operations retain their ordinary meaning.

Construction

One string names the whole right value. Several strings name its leading-axis cells.

T←'price' 'qty':(1 2 3 ⋄ 4 5 6)
≢T                          ⍝ 2ₓ
⍴('ab':11 12 13)            ⍝ ,1ₓ
'price' 'qty':2 3⍴⍳6       ⍝ ('price' 'qty':[1]2 3⍴⍳6)
≢⍬:⍬                        ⍝ 0ₓ

A character atom or vector is one string key. Use ,¨'ab' for two one-character keys.

⍳[1]('a' 'b':1 2)           ⍝ ,⊂'ab'
⍳[1]((,¨'ab'):1 2)          ⍝ ,¨'ab'

K:[axis]M attaches keys to an existing axis. Store the result with .

M←2 3⍴⍳6
M←'Jan' 'Feb' 'Mar':[2]M
⍳[2]M                       ⍝ 'Jan' 'Feb' 'Mar'
M[;'Feb']                   ⍝ 2 5

Several key vectors label successive axes. [axes] chooses their axes in the supplied order.

M←('alice' 'bob' ⋄ 'price' 'qty'):2 2⍴10 2 20 4
⍴M                          ⍝ 2ₓ 2ₓ
⍳[1]M                       ⍝ 'alice' 'bob'
⍳[2]M                       ⍝ 'price' 'qty'
⍳[1 2]M                     ⍝ ('alice' 'bob' ⋄ 'price' 'qty')
M≡('price' 'qty' ⋄ 'alice' 'bob'):[2 1]2 2⍴10 2 20 4 ⍝ 1ₓ

Monadic : removes all keys; :[axes] removes only the selected axes’ keys.

M←('alice' 'bob' ⋄ 'price' 'qty'):2 2⍴10 2 20 4
:M                          ⍝ 2 2⍴10 2 20 4
:[2]M                       ⍝ 'alice' 'bob':2 2⍴10 2 20 4
⍳[1 2](:M)                  ⍝ (1ₓ 2ₓ ⋄ 1ₓ 2ₓ)

Inside dfns, group functional colon: (X:Y) or (:Y). A direct colon separates a guard.

{+/(:⍵)}'aa' 'bb':1 2       ⍝ 3
{('total':+/⍵)}1 2 3        ⍝ 'total':6

Keys must be unique strings on each axis: DOMAIN. A key-list length must match its axis: LENGTH.

Expression Error
'aa' 'aa':1 2 DOMAIN
'aa' 'bb':[1]1 2 3 LENGTH

Selection and assignment

A scalar selector removes its axis. A vector selector retains it. A character vector selects one string key.

M←('alice' 'bob' ⋄ 'price' 'qty'):2 2⍴10 2 20 4
M['bob']                    ⍝ 'price' 'qty':20 4
M[2]                        ⍝ 'price' 'qty':20 4
M[;'qty']                   ⍝ 'alice' 'bob':2 4
M['alice';'qty']             ⍝ 2
M['bob';1]                  ⍝ 20
M[,⊂'alice']                ⍝ ('alice':[1]('price' 'qty':[2]1 2⍴10 2))
('bob' 'qty')⊃M             ⍝ 4
('bob' 'price')⌷M           ⍝ 20
M⍎'bob'                    ⍝ 'price' 'qty':20 4

Dot names are literal keys. T.a.b is 'b'⊃'a'⊃T.

T←('n':1),('addr':'city':'Paris')
T.addr.city                 ⍝ 'Paris'
T.n+←1 ⋄ T.n                ⍝ 2

Plain named assignment appends missing rows or columns. New cells receive the array’s prototype before assignment.

M←('alice' 'bob' ⋄ 'price' 'qty'):2 2⍴10 2 20 4
M['cara']←30 5
M['cara';'qty']              ⍝ 5
M[;'tax']←1 2 3
M['alice';'tax']             ⍝ 1

Appending on several axes fills the new cross-cells too.

M←('alice' 'bob' ⋄ 'price' 'qty'):2 2⍴10 2 20 4
M['cara';'tax']←3
M['cara']                   ⍝ 'price' 'qty' 'tax':0 0 3
M[;'tax']                   ⍝ 'alice' 'bob' 'cara':0 0 3

Missing keys on read or modified assignment: INDEX. Repeated selections on a keyed axis: DOMAIN. Numeric out-of-range assignment: INDEX.

For T←'aa' 'bb':1 2:

Expression Error
T['cc'] INDEX
T['cc']+←1 INDEX
T[1 1] DOMAIN
T[3]←9 INDEX

Scalar selection stores the whole RHS. A vector selection aligns its retained axis.

T←'aa' 'bb':1 2 ⋄ U←'bb' 'aa':8 9
T['aa']←U ⋄ T['aa']         ⍝ 'bb' 'aa':8 9
T[,⊂'aa']←U ⋄ T['aa']      ⍝ 9

Agreement

Corresponding keyed axes align by union: left keys, then right-only keys. An absent counterpart receives the present value’s prototype.

('aa':2)+('bb':3)            ⍝ 'aa' 'bb':2 3
('aa':2)×('bb':3)            ⍝ 'aa' 'bb':0 0
('aa':0)=('bb':0)            ⍝ 'aa' 'bb':1ₓ 1ₓ

Each keyed axis aligns independently, including reordered keys.

A←('alice':[1]'price':[2]1 1⍴10)
B←('bob':[1]'qty':[2]1 1⍴2)
A+B                         ⍝ ('alice' 'bob' ⋄ 'price' 'qty'):2 2⍴10 0 0 2
('aa' 'bb':1 2)+('bb' 'aa':10 20) ⍝ 'aa' 'bb':21 12

With one axis unkeyed, agreement is positional. Expanding a keyed singleton drops that axis’s labels. Enclosure broadcasts within the named value.

('base':5)+10 20 30          ⍝ 15 25 35
('base':5)+⊂10 20 30         ⍝ 'base':15 25 35
('aa' 'bb':1 2)+10 20        ⍝ 'aa' 'bb':11 22

Explicit axes choose the correspondence.

M←('alice' 'bob' ⋄ 'price' 'qty'):2 2⍴10 2 20 4
M+[2]('qty' 'tax':10 2)      ⍝ ('alice' 'bob' ⋄ 'price' 'qty' 'tax'):2 3⍴10 12 2 20 14 2

Array operations

Transpose moves axes with their keys.

M←('alice' 'bob' ⋄ 'price' 'qty'):2 2⍴10 2 20 4
⍉M                          ⍝ ('price' 'qty' ⋄ 'alice' 'bob'):2 2⍴10 20 2 4

Reverse, rotate, take, drop and filtering move keys with their positions.

V←'aa' 'bb' 'cc':10 20 5
⌽V                          ⍝ 'cc' 'bb' 'aa':5 20 10
1⌽V                         ⍝ 'bb' 'cc' 'aa':20 5 10
¯1↑V                        ⍝ 'cc':5
1↓V                         ⍝ 'bb' 'cc':20 5
1 0 1/V                     ⍝ 'aa' 'cc':10 5

Reduction removes the reduced axis. Scan retains it. Each retains its frame keys.

M←('alice' 'bob' ⋄ 'price' 'qty'):2 2⍴10 2 20 4
+/M                         ⍝ 'alice' 'bob':12 24
+⌿M                         ⍝ 'price' 'qty':30 6
+\'aa' 'bb' 'cc':1 2 3       ⍝ 'aa' 'bb' 'cc':1 3 6
+/¨'price' 'qty':(1 2 3 ⋄ 4 5 6) ⍝ 'price' 'qty':6 15

Shape-changing reshape drops keys. Same-shape reshape retains them. Ravel and table drop keys on merged/split axes, retaining unchanged axes.

V←'aa' 'bb':1 2
2⍴V                         ⍝ 'aa' 'bb':1 2
4⍴V                         ⍝ 1 2 1 2
M←'aa' 'bb':2 2⍴⍳4
,M                          ⍝ 1 2 3 4
⍳[1]⍪M                     ⍝ 'aa' 'bb'

Rank preserves frame keys. Assembled cell axes keep keys shared by every result cell in the same order.

V←'aa' 'bb':1 2
{⍵ ⍵}⍤0⊢V                  ⍝ 'aa' 'bb':2 2⍴1 1 2 2
M←('alice' 'bob' ⋄ 'price' 'qty'):2 2⍴10 2 20 4
⌽⍤1⊢M                      ⍝ ('alice' 'bob' ⋄ 'qty' 'price'):2 2⍴2 10 4 20
{10=↑⍵:⌽⍵ ⋄ ⍵}⍤1⊢M         ⍝ 'alice' 'bob':2 2⍴2 10 20 4

Per-cell rotation drops keys on the rotated axis when cells use different rotations.

M←('alice' 'bob' ⋄ 'price' 'qty'):2 2⍴10 2 20 4
0 1⌽M                       ⍝ 'alice' 'bob':2 2⍴10 2 4 20

Outer product retains both arguments’ axes. Array-valued power counts supply the result frame.

('aa' 'bb':1 2)+⌝('xx' 'yy':10 20) ⍝ ('aa' 'bb' ⋄ 'xx' 'yy'):2 2⍴11 21 12 22
N←'initial' 'once' 'twice':0 1 2
(2∘×)⍣N⊢3                  ⍝ 'initial' 'once' 'twice':3 6 12

Inner product, Decode and matrix solve align contracted keys. Both keyed: equal key sets required (LENGTH otherwise). Either unkeyed: positional contraction.

('hi' 'lo':1 2)+.×('lo' 'hi':10 20) ⍝ 40
('hi' 'lo':10 10)⊥('lo' 'hi':2 1) ⍝ 12
M←('r1' 'r2' ⋄ 'xx' 'yy'):2 2⍴2x 0x 0x 4x
('r2' 'r1':12x 4x)⌹M        ⍝ 'xx' 'yy':2ₓ 3ₓ
⌹M                          ⍝ ('xx' 'yy' ⋄ 'r1' 'r2'):2 2⍴1r2 0ₓ 0ₓ 1r4

Catenate joins keys on its joined axis and aligns other keyed axes. An unkeyed joined axis makes the result axis unkeyed.

('aa':1),('bb':2)            ⍝ 'aa' 'bb':1 2
('aa' 'bb':1 2),5            ⍝ 1 2 5
A←('aa' 'bb' ⋄ 'xx' 'yy'):2 2⍴1 2 3 4
B←('bb' 'aa' ⋄ ,⊂'zz'):2 1⍴5 6
A,B                         ⍝ ('aa' 'bb' ⋄ 'xx' 'yy' 'zz'):2 3⍴1 2 6 3 4 5

Replicate can repeat an unkeyed axis while retaining keys on other axes.

2/'aa' 'bb':2 2⍴1 2 3 4     ⍝ 'aa' 'bb':2 4⍴1 1 2 2 3 3 4 4

Repeated or invented positions on a keyed axis give DOMAIN. For V←'aa' 'bb':1 2:

Expression Reason
V,V Duplicate joined keys
2/V Repeated keys
3↑V Unnamed padding
1 0 1\V Unnamed inserted position

Windows

Full windows have an unkeyed position axis. Each window retains its slice’s keys; assembly keeps only keys shared by all windows. Stencil frames retain their centre keys. Padding drops keys on the padded window axis.

V←'aa' 'bb' 'cc':1 2 3
2↕V                         ⍝ 2 2⍴1 2 2 3
3↕V                         ⍝ 'aa' 'bb' 'cc':[2]1 3⍴1 2 3
{+/⍵}⌺3⊢V                  ⍝ 'aa' 'bb' 'cc':3 6 5
{(⍳[1]⍵)≡'aa' 'bb' 'cc'}⌺3⊢V ⍝ 'aa' 'bb' 'cc':0ₓ 1ₓ 0ₓ

Match aligns keys independently on every keyed axis. Shapes and unkeyed axes must match positionally. Searches compare values or cells. Found positions return axis keys.

V←'low' 'mid' 'high':10 20 30
V≡⌽V                        ⍝ 1ₓ
V⍳20 99                     ⍝ ('mid' ⋄ 4ₓ)
V⍸5 15 25 40                ⍝ (0ₓ ⋄ 'low' ⋄ 'mid' ⋄ 'high')
⍒V                          ⍝ 'high' 'mid' 'low'
V[⍒V]                       ⍝ 'high' 'mid' 'low':30 20 10
('aa':7)∊('bb':7)            ⍝ 'aa':1ₓ

Multidimensional positions mix keys and numeric coordinates. Bare Iota generates from values as usual.

M←'price' 'qty':[2]2 2⍴10 2 20 4
↑⍸M=20                      ⍝ (2ₓ ⋄ 'price')
⍳('rows' 'cols':2 3)        ⍝ ⍳2 3

Unique and set operations compare values and retain the selected positions’ keys.

A←'aa' 'bb':1 2 ⋄ B←'xx' 'yy':2 3
A∩B                         ⍝ 'bb':2
A~B                         ⍝ 'aa':1
A∪B                         ⍝ 'aa' 'bb' 'yy':1 2 3
∪'aa' 'bb' 'cc':1 1 2       ⍝ 'aa' 'cc':1 2

Monadic Key passes named positions to its operand. Dyadic Key preserves labels on the grouped values.

V←'aa' 'bb' 'cc':1 2 1
{⊂⍵}⌸V                     ⍝ (('aa' 'cc') ⋄ ,⊂'bb')
1 2 1{⊂⍵}⌸V                ⍝ (('aa' 'cc':1 1) ⋄ ('bb':[1],2))

Named axes

An axis name identifies a dimension: city. Its keys identify positions: Paris London. Names are unique within an array.

M←'city' 'month':[0]2 3⍴⍳6
⍳[0]M                         ⍝ 'city' 'month'
+/['month']M                   ⍝ 'city':[0]6 15

Axis 0 refers to the list of axes. N:[0]M supplies one name per axis; use an axis’s own index to leave it unnamed. :[0]M removes names. These return new values sharing the element buffer. Assign with M←… to store the result.

M←'city' 'month':[0]2 3⍴⍳6
⍳[0]('city' ⋄ 2):[0]M         ⍝ ('city' ⋄ 2ₓ)
⍳[0](:[0]M)                  ⍝ 1ₓ 2ₓ
(⍳[0]M):[0](:[0]M)           ⍝ 'city' 'month':[0]2 3⍴⍳6

A keyed vector on the left of : supplies both axis names and position keys. Its keys name axes; its values list their positions.

sales←('city' 'month':('London' 'Paris' ⋄ 'Jan' 'Feb' 'Mar')):2 3⍴10 20 30 40 50 60
⍳[0]sales                     ⍝ 'city' 'month'
⍳[1 2]sales                   ⍝ ('London' 'Paris' ⋄ 'Jan' 'Feb' 'Mar')
sales['Paris';'Feb']           ⍝ 50

Reuse descriptions, or attach them to selected axes. Other axes keep their names and keys.

months←'month':'Jan' 'Feb' 'Mar'
M←months:[2]2 3⍴⍳6
⍳[0]M                        ⍝ (1ₓ ⋄ 'month')
⍳[2]M                        ⍝ 'Jan' 'Feb' 'Mar'
⍳[0]('city':'London' 'Paris'):M ⍝ 'city' 'month'
axes←'month' 'city':('Jan' 'Feb' 'Mar' ⋄ 'London' 'Paris')
M←axes:[2 1]2 3⍴⍳6
⍳[0]M                        ⍝ 'city' 'month'
⍳[1 2]M                      ⍝ ('London' 'Paris' ⋄ 'Jan' 'Feb' 'Mar')

Agreement first pairs equal names, wherever they occur. Unpaired axes broadcast. Result order is left axes, then right-only axes.

M←'city' 'month':[0]2 3⍴⍳6
N←'month':[0]10 20 30
M+N                           ⍝ 'city' 'month':[0]2 3⍴11 22 33 14 25 36
⍳[0]N+M                      ⍝ 'month' 'city'
M+⍉M                         ⍝ 'city' 'month':[0]2 3⍴2 4 6 8 10 12

Remaining axes pair in leading-axis order. A named/unnamed pair keeps the name. Two different names remain separate broadcast dimensions. Length agreement and position-key alignment then apply.

M←'city' 'month':[0]2 3⍴⍳6
M+10 20                       ⍝ 'city' 'month':[0]2 3⍴11 12 13 24 25 26
A←('city' ⋄ 2):[0]2 3⍴1
B←('product' ⋄ 2):[0]4 3⍴2
⍳[0]A+B                      ⍝ ('city' ⋄ 2ₓ ⋄ 'product')
⍴A+B                          ⍝ 2ₓ 3ₓ 4ₓ

Function qualifiers select axes by name. Array brackets select positions on those axes.

M←'city' 'month':[0]('Paris' 'London' ⋄ 'Jan' 'Feb' 'Mar'):2 3⍴⍳6
⍳[1]'Paris'⌷['city']M        ⍝ 'Jan' 'Feb' 'Mar'
M['Paris']≡'Paris'⌷['city']M  ⍝ 1ₓ
⍳[0]⊂['month']M              ⍝ ,⊂'city'
+/['city' 'month']M            ⍝ ⊂21

Transpose moves names. Scalar selection removes the selected axis. Repeating positions retains names. Shape-changing reshape and axis merging drop names on replaced axes; new axes start unnamed.

M←'city' 'month':[0]2 3⍴⍳6
⍳[0]⍉M                      ⍝ 'month' 'city'
⍳[0]M[1]                     ⍝ ,⊂'month'
⍳[0]2 3⍴M                   ⍝ 'city' 'month'
⍳[0]3 2⍴M                   ⍝ 1ₓ 2ₓ
⍳[0],M                       ⍝ ,1ₓ
V←'city':[0]1 2
⍳[0]2/V                      ⍝ ,⊂'city'
⍳[0]⍪V                      ⍝ ('city' ⋄ 2ₓ)

Duplicating an axis name drops every occurrence of it. Position keys remain.

V←'city':[0]'Paris' 'London':1 2
W←V×⌝V
⍳[0]W                        ⍝ 1ₓ 2ₓ
⍳[1 2]W                      ⍝ ('Paris' 'London' ⋄ 'Paris' 'London')
Expression Error
+/['missing']M INDEX
'city' 'city':[0]2 3⍴0 DOMAIN
'city':[0]2 3⍴0 LENGTH

Python and JSON

Python dicts become keyed vectors recursively. .py converts them back to dicts.

from basedpl import Array
import numpy as np

record = Array({'price': 10, 'address': {'city': 'Paris'}})
assert record.py == {'price': 10, 'address': {'city': 'Paris'}}
assert record.axis_keys == (('price', 'address'),)

Attach position keys with axis_keys, axis names with axis_names. None leaves an axis unkeyed/unnamed. The corresponding properties inspect them; .np copies the values to NumPy.

rows, cols = ['London', 'Paris'], ['Jan', 'Feb', 'Mar']
sales = Array([[10, 20, 30], [40, 50, 60]], axis_keys=(rows, cols), axis_names=('city', 'month'))
assert sales.axis_keys == (tuple(rows), tuple(cols))
assert sales.axis_names == ('city', 'month')
assert sales.np.tolist() == [[10, 20, 30], [40, 50, 60]]
columns_only = Array(sales.np, axis_keys=(None, cols), axis_names=(None, 'month'))
assert columns_only.axis_keys == (None, tuple(cols))
assert columns_only.axis_names == (None, 'month')

.df converts to pandas. For a matrix, keys label rows/columns and axis names name the index/columns. .py uses this conversion for keyed arrays of rank ≥2. Install pandas with pip install 'basedpl[pandas]'; it is imported on conversion.

df = sales.df
assert df.loc['Paris', 'Feb'] == 50
assert (df.index.name, df.columns.name) == ('city', 'month')
assert sales.py.equals(df)
assert columns_only.df.index.tolist() == [1, 2]

Above rank 2, the last axis supplies columns; earlier axes form a row MultiIndex. Unkeyed axes use 1-origin labels. Scalars and vectors become one-column DataFrames.

cube = Array(np.arange(12).reshape(2, 2, 3), axis_keys=(None, rows, cols), axis_names=('year', 'city', 'month'))
assert cube.df.index.names == ['year', 'city']
assert cube.df.loc[(2, 'Paris'), 'Mar'] == 11
assert Array({'aa': 1, 'bb': 2}).df.loc['bb', 1] == 2
assert Array(7).df.loc[1, 1] == 7

•json converts ordinary JSON objects to keyed vectors. The process protocol carries axis_keys and axis_names beside shape, row-major data and prototype. For sales above:

{
  "shape": [2, 3],
  "data": [10, 20, 30, 40, 50, 60],
  "prototype": 0,
  "axis_keys": [["London", "Paris"], ["Jan", "Feb", "Mar"]],
  "axis_names": ["city", "month"]
}

Use null for an unkeyed/unnamed axis. Omit each metadata field when none of its axes has that metadata:

{"shape":[1,2],"data":[10,20],"prototype":0,"axis_keys":[null,["Jan","Feb"]]}
{"shape":[2],"data":[10,20],"prototype":0}