Indexing, Selecting and Assigning
Import the LArray library:
[1]:
from larray import *
Import the test array population
:
[2]:
# let's start with
population = load_example_data('demography_eurostat').population
population
[2]:
country gender\time 2013 2014 2015 2016 2017
Belgium Male 5472856 5493792 5524068 5569264 5589272
Belgium Female 5665118 5687048 5713206 5741853 5762455
France Male 31772665 32045129 32174258 32247386 32318973
France Female 33827685 34120851 34283895 34391005 34485148
Germany Male 39380976 39556923 39835457 40514123 40697118
Germany Female 41142770 41210540 41362080 41661561 41824535
Selecting (Subsets)
The Array
class allows to select a subset either by labels or indices (positions)
Selecting by Labels
To take a subset of an array using labels, use brackets [ ].
Let’s start by selecting a single element:
[3]:
population['Belgium', 'Female', 2017]
[3]:
5762455
As long as there is no ambiguity (i.e. axes sharing one or several same label(s)), the order of indexing does not matter. So you usually do not care/have to remember about axes positions during computation. It only matters for output.
[4]:
# order of index doesn't matter
population['Female', 2017, 'Belgium']
[4]:
5762455
Selecting a subset is done by using slices or lists of labels:
[5]:
population[['Belgium', 'Germany'], 2014:2016]
[5]:
country gender\time 2014 2015 2016
Belgium Male 5493792 5524068 5569264
Belgium Female 5687048 5713206 5741853
Germany Male 39556923 39835457 40514123
Germany Female 41210540 41362080 41661561
Slices bounds are optional: if not given, start is assumed to be the first label and stop is the last one.
[6]:
# select all years starting from 2015
population[2015:]
[6]:
country gender\time 2015 2016 2017
Belgium Male 5524068 5569264 5589272
Belgium Female 5713206 5741853 5762455
France Male 32174258 32247386 32318973
France Female 34283895 34391005 34485148
Germany Male 39835457 40514123 40697118
Germany Female 41362080 41661561 41824535
[7]:
# select all first years until 2015
population[:2015]
[7]:
country gender\time 2013 2014 2015
Belgium Male 5472856 5493792 5524068
Belgium Female 5665118 5687048 5713206
France Male 31772665 32045129 32174258
France Female 33827685 34120851 34283895
Germany Male 39380976 39556923 39835457
Germany Female 41142770 41210540 41362080
Slices can also have a step (defaults to 1), to take every Nth labels:
[8]:
# select all even years starting from 2014
population[2014::2]
[8]:
country gender\time 2014 2016
Belgium Male 5493792 5569264
Belgium Female 5687048 5741853
France Male 32045129 32247386
France Female 34120851 34391005
Germany Male 39556923 40514123
Germany Female 41210540 41661561
Warning: Selecting by labels as in above examples works well as long as there is no ambiguity. When two or more axes have common labels, it leads to a crash. The solution is then to precise to which axis belong the labels.
[9]:
immigration = load_example_data('demography_eurostat').immigration
# the 'immigration' array has two axes (country and citizenship) which share the same labels
immigration
[9]:
country citizenship gender\time 2013 2014 2015 2016 2017
Belgium Belgium Male 8822 10512 11378 11055 11082
Belgium Belgium Female 5727 6301 6486 6560 6454
Belgium Luxembourg Male 102 117 105 130 110
Belgium Luxembourg Female 117 123 114 108 118
Belgium Netherlands Male 4185 4222 4183 4199 4138
Belgium Netherlands Female 3737 3844 3942 3664 3632
Luxembourg Belgium Male 896 937 880 762 781
Luxembourg Belgium Female 574 655 622 558 575
Luxembourg Luxembourg Male 694 722 660 740 650
Luxembourg Luxembourg Female 607 586 535 591 549
Luxembourg Netherlands Male 160 165 147 141 167
Luxembourg Netherlands Female 92 97 85 94 119
Netherlands Belgium Male 1063 1141 1113 1364 1493
Netherlands Belgium Female 980 1071 1181 1340 1449
Netherlands Luxembourg Male 23 43 59 70 83
Netherlands Luxembourg Female 24 34 46 60 97
Netherlands Netherlands Male 19374 20037 21119 22707 23750
Netherlands Netherlands Female 16945 17411 18084 19815 20894
[10]:
# LArray doesn't use the position of the labels used inside the brackets
# to determine the corresponding axes. Instead LArray will try to guess the
# corresponding axis for each label whatever is its position.
# Then, if a label is shared by two or more axes, LArray will not be able
# to choose between the possible axes and will raise an error.
try:
immigration['Belgium', 'Netherlands']
except Exception as e:
print(type(e).__name__, ':', e)
ValueError : 'Belgium' is ambiguous, it is valid in the following axes:
country [3]: 'Belgium' 'Luxembourg' 'Netherlands'
citizenship [3]: 'Belgium' 'Luxembourg' 'Netherlands'
[11]:
# the solution is simple. You need to precise the axes on which you make a selection
immigration[immigration.country['Belgium'], immigration.citizenship['Netherlands']]
[11]:
gender\time 2013 2014 2015 2016 2017
Male 4185 4222 4183 4199 4138
Female 3737 3844 3942 3664 3632
Ambiguous Cases - Specifying Axes Using The Special Variable X
When selecting, assigning or using aggregate functions, an axis can be referred via the special variable X
:
population[X.time[2015:]]
population.sum(X.time)
This gives you access to axes of the array you are manipulating. The main drawback of using X
is that you lose the autocompletion available from many editors. It only works with non-anonymous axes for which names do not contain whitespaces or special characters.
[12]:
# the previous example can also be written as
immigration[X.country['Belgium'], X.citizenship['Netherlands']]
[12]:
gender\time 2013 2014 2015 2016 2017
Male 4185 4222 4183 4199 4138
Female 3737 3844 3942 3664 3632
Selecting by Indices
Sometimes it is more practical to use indices (positions) along the axis, instead of labels. You need to add the character i
before the brackets: .i[indices]
. As for selection with labels, you can use a single index, a slice or a list of indices. Indices can be also negative (-1 represent the last element of an axis).
Note: Remember that indices (positions) are always 0-based in Python. So the first element is at index 0, the second is at index 1, etc.
[13]:
# select the last year
population[X.time.i[-1]]
[13]:
country\gender Male Female
Belgium 5589272 5762455
France 32318973 34485148
Germany 40697118 41824535
[14]:
# same but for the last 3 years
population[X.time.i[-3:]]
[14]:
country gender\time 2015 2016 2017
Belgium Male 5524068 5569264 5589272
Belgium Female 5713206 5741853 5762455
France Male 32174258 32247386 32318973
France Female 34283895 34391005 34485148
Germany Male 39835457 40514123 40697118
Germany Female 41362080 41661561 41824535
[15]:
# using a list of indices
population[X.time.i[0, 2, 4]]
[15]:
country gender\time 2013 2015 2017
Belgium Male 5472856 5524068 5589272
Belgium Female 5665118 5713206 5762455
France Male 31772665 32174258 32318973
France Female 33827685 34283895 34485148
Germany Male 39380976 39835457 40697118
Germany Female 41142770 41362080 41824535
Warning: The end indice (position) is EXCLUSIVE while the end label is INCLUSIVE.
[16]:
year = 2015
# with labels
population[X.time[:year]]
[16]:
country gender\time 2013 2014 2015
Belgium Male 5472856 5493792 5524068
Belgium Female 5665118 5687048 5713206
France Male 31772665 32045129 32174258
France Female 33827685 34120851 34283895
Germany Male 39380976 39556923 39835457
Germany Female 41142770 41210540 41362080
[17]:
# with indices (i.e. using the .i[indices] syntax)
index_year = population.time.index(year)
population[X.time.i[:index_year]]
[17]:
country gender\time 2013 2014
Belgium Male 5472856 5493792
Belgium Female 5665118 5687048
France Male 31772665 32045129
France Female 33827685 34120851
Germany Male 39380976 39556923
Germany Female 41142770 41210540
You can use .i[]
selection directly on array instead of axes. In this context, if you want to select a subset of the first and third axes for example, you must use a full slice :
for the second one.
[18]:
# select first country and last three years
population.i[0, :, -3:]
[18]:
gender\time 2015 2016 2017
Male 5524068 5569264 5589272
Female 5713206 5741853 5762455
Using Groups In Selections
[19]:
even_years = population.time[2014::2]
population[even_years]
[19]:
country gender\time 2014 2016
Belgium Male 5493792 5569264
Belgium Female 5687048 5741853
France Male 32045129 32247386
France Female 34120851 34391005
Germany Male 39556923 40514123
Germany Female 41210540 41661561
Boolean Filtering
Boolean filtering can be used to extract subsets. Filtering can be done on axes:
[20]:
# select even years
population[X.time % 2 == 0]
[20]:
country gender\time 2014 2016
Belgium Male 5493792 5569264
Belgium Female 5687048 5741853
France Male 32045129 32247386
France Female 34120851 34391005
Germany Male 39556923 40514123
Germany Female 41210540 41661561
or data:
[21]:
# select population for the year 2017
population_2017 = population[2017]
# select all data with a value greater than 30 million
population_2017[population_2017 > 30e6]
[21]:
country_gender France_Male France_Female Germany_Male Germany_Female
32318973 34485148 40697118 41824535
Note: Be aware that after boolean filtering, several axes may have merged.
Arrays can also be used to create boolean filters:
[22]:
start_year = Array([2015, 2016, 2017], axes=population.country)
start_year
[22]:
country Belgium France Germany
2015 2016 2017
[23]:
population[X.time >= start_year]
---------------------------------------------------------------------------
KeyError Traceback (most recent call last)
Cell In[23], line 1
----> 1 population[X.time >= start_year]
File ~/checkouts/readthedocs.org/user_builds/larray/envs/latest/lib/python3.11/site-packages/larray/core/expr.py:13, in ExprNode._binop.<locals>.opmethod(self, other)
10 def opmethod(self, other):
11 # evaluate eagerly when possible
12 if isinstance(other, ABCArray):
---> 13 self_value = self.evaluate(other.axes)
14 return getattr(self_value, f'__{opname}__')(other)
15 else:
File ~/checkouts/readthedocs.org/user_builds/larray/envs/latest/lib/python3.11/site-packages/larray/core/axis.py:3818, in AxisReference.evaluate(self, context)
3811 def evaluate(self, context) -> Axis:
3812 r"""
3813 Parameters
3814 ----------
3815 context : AxisCollection
3816 Use axes from this collection
3817 """
-> 3818 return context[self.name]
File ~/checkouts/readthedocs.org/user_builds/larray/envs/latest/lib/python3.11/site-packages/larray/core/axis.py:1735, in AxisCollection.__getitem__(self, key)
1733 return self._map[key]
1734 else:
-> 1735 raise KeyError(f"axis '{key}' not found in {self}")
KeyError: "axis 'time' not found in {country}"
Iterating over an axis
Iterating over an axis is straightforward:
[24]:
for year in population.time:
print(year)
2013
2014
2015
2016
2017
Assigning subsets
Assigning A Value
Assigning a value to a subset is simple:
[25]:
population[2017] = 0
population
[25]:
country gender\time 2013 2014 2015 2016 2017
Belgium Male 5472856 5493792 5524068 5569264 0
Belgium Female 5665118 5687048 5713206 5741853 0
France Male 31772665 32045129 32174258 32247386 0
France Female 33827685 34120851 34283895 34391005 0
Germany Male 39380976 39556923 39835457 40514123 0
Germany Female 41142770 41210540 41362080 41661561 0
Now, let’s store a subset in a new variable and modify it:
[26]:
# store the data associated with the year 2016 in a new variable
population_2016 = population[2016]
population_2016
[26]:
country\gender Male Female
Belgium 5569264 5741853
France 32247386 34391005
Germany 40514123 41661561
[27]:
# now, we modify the new variable
population_2016['Belgium'] = 0
# and we can see that the original array has been also modified
population
[27]:
country gender\time 2013 2014 2015 2016 2017
Belgium Male 5472856 5493792 5524068 0 0
Belgium Female 5665118 5687048 5713206 0 0
France Male 31772665 32045129 32174258 32247386 0
France Female 33827685 34120851 34283895 34391005 0
Germany Male 39380976 39556923 39835457 40514123 0
Germany Female 41142770 41210540 41362080 41661561 0
One very important gotcha though…
Warning: Storing a subset of an array in a new variable and modifying it after may also impact the original array. The reason is that selecting a contiguous subset of the data does not return a copy of the selected subset, but rather a view on a subset of the array. To avoid such behavior, use the .copy()
method.
Remember:
taking a contiguous subset of an array is extremely fast (no data is copied)
if one modifies that subset, one also modifies the original array
.copy() returns a copy of the subset (takes speed and memory) but allows you to change the subset without modifying the original array in the same time
The same warning apply for entire arrays:
[28]:
# reload the 'population' array
population = load_example_data('demography_eurostat').population
# create a second 'population2' variable
population2 = population
population2
[28]:
country gender\time 2013 2014 2015 2016 2017
Belgium Male 5472856 5493792 5524068 5569264 5589272
Belgium Female 5665118 5687048 5713206 5741853 5762455
France Male 31772665 32045129 32174258 32247386 32318973
France Female 33827685 34120851 34283895 34391005 34485148
Germany Male 39380976 39556923 39835457 40514123 40697118
Germany Female 41142770 41210540 41362080 41661561 41824535
[29]:
# set all data corresponding to the year 2017 to 0
population2[2017] = 0
population2
[29]:
country gender\time 2013 2014 2015 2016 2017
Belgium Male 5472856 5493792 5524068 5569264 0
Belgium Female 5665118 5687048 5713206 5741853 0
France Male 31772665 32045129 32174258 32247386 0
France Female 33827685 34120851 34283895 34391005 0
Germany Male 39380976 39556923 39835457 40514123 0
Germany Female 41142770 41210540 41362080 41661561 0
[30]:
# and now take a look of what happened to the original array 'population'
# after modifying the 'population2' array
population
[30]:
country gender\time 2013 2014 2015 2016 2017
Belgium Male 5472856 5493792 5524068 5569264 0
Belgium Female 5665118 5687048 5713206 5741853 0
France Male 31772665 32045129 32174258 32247386 0
France Female 33827685 34120851 34283895 34391005 0
Germany Male 39380976 39556923 39835457 40514123 0
Germany Female 41142770 41210540 41362080 41661561 0
Warning: The syntax new_array = old_array
does not create a new array but rather an ‘alias’ variable. To actually create a new array as a copy of a previous one, the .copy()
method must be called.
[31]:
# reload the 'population' array
population = load_example_data('demography_eurostat').population
# copy the 'population' array and store the copy in a new variable
population2 = population.copy()
# modify the copy
population2[2017] = 0
population2
[31]:
country gender\time 2013 2014 2015 2016 2017
Belgium Male 5472856 5493792 5524068 5569264 0
Belgium Female 5665118 5687048 5713206 5741853 0
France Male 31772665 32045129 32174258 32247386 0
France Female 33827685 34120851 34283895 34391005 0
Germany Male 39380976 39556923 39835457 40514123 0
Germany Female 41142770 41210540 41362080 41661561 0
[32]:
# the data from the original array have not been modified
population
[32]:
country gender\time 2013 2014 2015 2016 2017
Belgium Male 5472856 5493792 5524068 5569264 5589272
Belgium Female 5665118 5687048 5713206 5741853 5762455
France Male 31772665 32045129 32174258 32247386 32318973
France Female 33827685 34120851 34283895 34391005 34485148
Germany Male 39380976 39556923 39835457 40514123 40697118
Germany Female 41142770 41210540 41362080 41661561 41824535
Assigning Arrays And Broadcasting
Instead of a value, we can also assign an array to a subset. In that case, that array can have less axes than the target but those which are present must be compatible with the subset being targeted.
[33]:
# select population for the year 2015
population_2015 = population[2015]
# propagate population for the year 2015 to all next years
population[2016:] = population_2015
population
[33]:
country gender\time 2013 2014 2015 2016 2017
Belgium Male 5472856 5493792 5524068 5524068 5524068
Belgium Female 5665118 5687048 5713206 5713206 5713206
France Male 31772665 32045129 32174258 32174258 32174258
France Female 33827685 34120851 34283895 34283895 34283895
Germany Male 39380976 39556923 39835457 39835457 39835457
Germany Female 41142770 41210540 41362080 41362080 41362080
Warning: The array being assigned must have compatible axes (i.e. same axes names and same labels) with the target subset.
[34]:
# replace 'Male' and 'Female' labels by 'M' and 'F'
population_2015 = population_2015.set_labels('gender', 'M,F')
population_2015
[34]:
country\gender M F
Belgium 5524068 5713206
France 32174258 34283895
Germany 39835457 41362080
[35]:
# now let's try to repeat the assignement operation above with the new labels.
# An error is raised because of incompatible axes
try:
population[2016:] = population_2015
except Exception as e:
print(type(e).__name__, ':', e)
ValueError : incompatible axes:
Axis(['Male', 'Female'], 'gender')
vs
Axis(['M', 'F'], 'gender')