Indexado, Slicing y operaciones básicas#
Vamos a explorar más a fondo la diferentes formas que tenemos de acceder y operar con componentes de un array multidimensional.
import numpy as np
Indexado y slicing#
Otra de las características más interesantes de numpy es la gran flexibilidad para acceder a las componentes de un array, o a un subconjunto del mismo. Vamos a ver a continuación algunos ejemplos básicos.
Arrays unidimensonales
Para arrays unidimensionales, el acceso es muy parecido al de listas. Por ejemplo, acceso a las componentes:
v = np.arange(10)
v[5]
5
La operación de slicing en arrays es similar a la de listas. Por ejemplo:
v[5:8]
array([5, 6, 7])
Sin embargo, hay una diferencia fundamental: en general en python, el slicing siempre crea una copia de la secuencia original (aunque no de los elementos) a la hora de hacer asignaciones. En numpy, el slicing es una vista de array original. Esto tiene como consecuencia que las modificaciones que se realicen sobre dicha vista se están realizando sobre el array original. Por ejemplo:
l = list(range(10))
l_sublist = l[5:8]
v_subarray = v[5:8]
l_sublist[:] = [12, 12, 12]
v_subarray[:] = 12
print(l)
print(v)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[ 0 1 2 3 4 12 12 12 8 9]
Y además hay que tener en cuenta que cualquier referencia a una vista es en realidad una referencia a los datos originales, y que las modificaciones que se realicen a través de esa referencia, se realizarán igualmente sobre el original.
Veámos esto con el siguiente ejemplo:
Modificamos la componente 1 de v_slice
:
v_subarray[1] = 12345
print(v_subarray)
[ 12 12345 12]
Pero la componente 1 de v_subarray
es en realidad la componente 6 de v
, así que v
ha cambiado:
print(v)
[ 0 1 2 3 4 12 12345 12 8 9]
Nótese la diferencia con las listas de python, en las que l[:]
es la manera estándar de crear una copia de una lista l
. En el caso de numpy, si se quiere realizar una copia, se ha de usar el método copy
(por ejemplo, v.copy()
).
v_copy = v.copy()
Arrays de más dimensiones
El acceso a los componentes de arrays de dos o más dimensiones es similar, aunque la casuística es más variada.
Cuando accedemos con un único índice, estamos accediendo al correspondiente subarray de esa posición. Por ejemplo, en array de dos dimensiones, con 3 filas y 3 columnas, la posición 2 es la tercera fila:
c2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
c2d[2]
array([7, 8, 9])
print(c2d)
[[1 2 3]
[4 5 6]
[7 8 9]]
De esta manera, recursivamente, podríamos acceder a los componentes individuales de una array de cualquier dimensión. En el ejemplo anterior, el elemento de la primera fila y la tercera columna sería:
c2d[0][2]
3
Normalmente no se suele usar la notación anterior para acceder a los elementos individuales, sino que se usa un único corchete con los índices separados por comas: Lo siguiente es equivalente:
c2d[0, 2]
3
Veamos más ejemplos de acceso y modificación en arrays multidimensionales, en este caso con tres dimensiones.
c3d = np.array([[[1, 2, 3], [4, 5, 6]], [[7, 8, 9], [10, 11, 12]]])
c3d
array([[[ 1, 2, 3],
[ 4, 5, 6]],
[[ 7, 8, 9],
[10, 11, 12]]])
Accediendo a la posición 0 obtenemos el correspondiente subarray de dos dimensiones:
c3d[0]
array([[1, 2, 3],
[4, 5, 6]])
Similar a la función enumerate
de Python, tenemos la función np.ndenumearte
para iterar con los elementos del array y su índice
l = list(range(10, 20))
print(list(enumerate(l)))
[(0, 10), (1, 11), (2, 12), (3, 13), (4, 14), (5, 15), (6, 16), (7, 17), (8, 18), (9, 19)]
[i for i in np.ndenumerate(c3d)]
[((0, 0, 0), 1),
((0, 0, 1), 2),
((0, 0, 2), 3),
((0, 1, 0), 4),
((0, 1, 1), 5),
((0, 1, 2), 6),
((1, 0, 0), 7),
((1, 0, 1), 8),
((1, 0, 2), 9),
((1, 1, 0), 10),
((1, 1, 1), 11),
((1, 1, 2), 12)]
Vamos a guardar una copia de de ese subarray y lo modificamos en el original con el número 42
en todas las posiciones:
old_values = c3d[0].copy()
c3d[0] = 42
c3d
array([[[42, 42, 42],
[42, 42, 42]],
[[ 7, 8, 9],
[10, 11, 12]]])
Y ahora reestablecemos los valores originales:
c3d[0] = old_values
c3d
array([[[ 1, 2, 3],
[ 4, 5, 6]],
[[ 7, 8, 9],
[10, 11, 12]]])
Exercise 49
Devuelve el número 813 indexando el array np.arange(2100).reshape((25, 6, 7, 2))
.
# con fuerza bruta y np.ndenumerate
arr_enumerate = {k:v for v, k in np.ndenumerate(arr)}
idx = arr_enumerate[813]
print(idx)
(9, 4, 0, 1)
arr[idx]
813
813 % 25
13
arr = np.arange(2100).reshape((25, 6, 7, 2))
n = 813
i = n // (6*7*2)
j = (n % (6*7*2)) // (7*2)
k = (n % (6*7*2)) // 7
l = n - i*25 - j*6 - k
print(i, j, k, l)
# print(arr[i, j, k, l])
9 4 80 484
Indexado usando slices#
c2d
array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
Los slicings en arrays multidimensionales se hacen a lo largo de los correspondientes ejes. Por ejemplo, en un array bidimensional, lo haríamos sobre la secuencia de filas.
c2d[:2]
array([[1, 2, 3],
[4, 5, 6]])
Pero también podríamos hacerlo en ambos ejes. Por ejemplo para obtener el subarray hasta la segunda fila y a partir de la primera columna:
c2d[:2, 1:]
array([[2, 3],
[5, 6]])
Si en alguno de los ejes se usa un índice individual, entonces se pierde una de las dimensiones:
c2d[1, :2]
array([4, 5])
Nótese la diferencia con la operación C2d[1:2,:2]
. Puede parecer que el resultado ha de ser el mismo, pero si se usa slicing en ambos ejes se mantiene el número de dimensiones:
c2d[1:2,:2]
array([[4, 5]])
Más ejemplos:
c2d
array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
print(c2d[:2, 2])
print(c2d[:2, 2].shape)
print(c2d[:2, 2:])
print(c2d[:2, 2:].shape)
[3 6]
(2,)
[[3]
[6]]
(2, 1)
c2d[:, :1]
array([[1],
[4],
[7]])
Como hemos visto más arriba, podemos usar slicing para asignar valores a las componentes de un array. Por ejemplo
c2d[:2, 1:] = 0
c2d
array([[1, 0, 0],
[4, 0, 0],
[7, 8, 9]])
Finalmente, notemos que podemos usar cualquier slice
de Python para arrays
slice_1 = slice(2, 0, -1)
slice_2 = slice(0, 3, 2)
c2d[slice_1, slice_2]
array([[7, 9],
[4, 0]])
Exercise 50
Crea un array tridimensional de dimensiones \((3, 4, 2)\) y obtén el subarray indicada en la figura (shape = (1, 2)
). Obtén también un subarray a tu elección de dimensiones \((2, 3, 1)\).
arr = np.arange(3*4*2).reshape((3, 4, 2))
arr2 = np.zeros((3, 4, 2))
assert arr.shape == (3, 4, 2)
assert arr2.shape == (3, 4, 2)
subarr = arr[2, 3:4, :]
print(subarr)
print(subarr.shape)
subarr2 = arr[:2, :3, :1]
print(subarr2.shape)
[[22 23]]
(1, 2)
(2, 3, 1)
Indexado con booleanos#
Los arrays de booleanos se pueden usar en numpy como una forma de indexado para seleccionar determinadas componenetes en una serie de ejes.
Veamos el siguiente ejemplo:
nombres = np.array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'])
nombres.shape
(7,)
rng = np.random.default_rng()
data = rng.normal(0, 1, (7, 4))
data
array([[-0.42485576, -0.61984103, 0.34435114, -0.18426761],
[-0.38003979, 0.11368257, -0.77323438, -0.33762353],
[-0.55766453, -0.37463439, 1.71212066, 0.83282071],
[ 0.45976903, 1.21320606, -0.81950898, -0.07196881],
[ 0.22204767, -0.31588078, 0.0959285 , 0.90929319],
[-1.03417402, -0.09117112, 0.16562553, 1.12910964],
[ 0.80557194, 0.51326164, -1.39353509, -0.30230354]])
nombres == "Bob"
array([ True, False, False, True, False, False, False])
data[nombres == "Bob"]
array([[-1.12179778, 0.16710429, 1.92063108, 0.17525895],
[-0.87349974, -0.65564437, 0.41772414, -1.08822151]])
Podríamos interpretar que cada fila del array data
son datos asociados a las correspondientes personas del array nombres
. Si ahora queremos quedarnos por ejemplos con las filas correspondientes a Bob, podemos usar indexado booleano de la siguiente manera:
El array de booleanos que vamos a usar será:
nombres == 'Bob'
array([ True, False, False, True, False, False, False])
Y el indexado con ese array, en el eje de las filas, nos dará el subarray de las filas correspondientes a Bob:
data[nombres == 'Bob']
array([[-1.12179778, 0.16710429, 1.92063108, 0.17525895],
[-0.87349974, -0.65564437, 0.41772414, -1.08822151]])
Podemos mezclar indexado booleano con índices concretos o con slicing en distintos ejes:
data[nombres == 'Bob', 2:]
array([[ 1.92063108, 0.17525895],
[ 0.41772414, -1.08822151]])
data[nombres == 'Bob', 3]
array([ 0.17525895, -1.08822151])
Para usar el indexado complementario (en el ejemplo, las filas correspondientes a las personas que no son Bob), podríamos usar el array de booleanos nombres != 'Bob'
. Sin embargo, es más habitual usar el operador ~
:
x = True
if not True:
print("foo")
else:
print("bar")
bar
data[~(nombres == 'Bob')]
array([[-0.38003979, 0.11368257, -0.77323438, -0.33762353],
[-0.55766453, -0.37463439, 1.71212066, 0.83282071],
[ 0.22204767, -0.31588078, 0.0959285 , 0.90929319],
[-1.03417402, -0.09117112, 0.16562553, 1.12910964],
[ 0.80557194, 0.51326164, -1.39353509, -0.30230354]])
Incluso podemos jugar con otros operadores booleanos como &
(and) y |
(or), para construir indexados booleanos que combinan condiciones.
Por ejemplo, para obtener las filas correspondiente a Bob o a Will:
mask = (nombres == 'Bob') | (nombres == 'Will')
mask
array([ True, False, True, True, True, False, False])
data[mask]
array([[-0.42485576, -0.61984103, 0.34435114, -0.18426761],
[-0.55766453, -0.37463439, 1.71212066, 0.83282071],
[ 0.45976903, 1.21320606, -0.81950898, -0.07196881],
[ 0.22204767, -0.31588078, 0.0959285 , 0.90929319]])
Y como en los anteriores indexados, podemos usar el indexado booleano para modificar componentes de los arrays. Lo siguiente pone a 0 todos los componentes neativos de data
:
data < 0
array([[ True, True, False, True],
[ True, False, True, True],
[ True, True, False, False],
[False, False, True, True],
[False, True, False, False],
[ True, True, False, False],
[False, False, True, True]])
data[data < 0]
array([-1.12179778, -0.64661053, -0.4950793 , -1.05215919, -1.7182702 ,
-0.11062548, -0.87349974, -0.65564437, -1.08822151, -0.5534005 ,
-0.16258268, -1.39485421, -0.0867283 , -0.04034513, -0.47707921])
data[data < 0] = 0
data
array([[0. , 0. , 0.34435114, 0. ],
[0. , 0.11368257, 0. , 0. ],
[0. , 0. , 1.71212066, 0.83282071],
[0.45976903, 1.21320606, 0. , 0. ],
[0.22204767, 0. , 0.0959285 , 0.90929319],
[0. , 0. , 0.16562553, 1.12910964],
[0.80557194, 0.51326164, 0. , 0. ]])
Obsérvese que ahora data < 0
es un array de booleanos bidimensional con la misma estructura que el propio data
y que por tanto tanto estamos haciendo indexado booleano sobre ambos ejes.
Podríamos incluso fijar un valor a filas completas, usando indexado por un booleano unidimensional:
data[~(nombres == 'Joe')] = 7
data
array([[7. , 7. , 7. , 7. ],
[0. , 1.37144151, 0. , 0. ],
[7. , 7. , 7. , 7. ],
[7. , 7. , 7. , 7. ],
[7. , 7. , 7. , 7. ],
[0.26356597, 0.95143527, 1.00639533, 0. ],
[0. , 1.0444593 , 0. , 0. ]])
Exercise 51
Devuelve las filas de data
correspondientes a aquellos nombres que empiecen por «B» o «J». Puedes utilizar la función np.char.startswith
.
nombres = np.array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'])
data = rng.normal(0, 1, (7, 4))
mask = np.char.startswith(nombres, prefix="B") | np.char.startswith(nombres, prefix="J")
mask.shape
data[mask]
array([[-1.24467584, 0.23014917, -1.5190614 , 1.36721673],
[-1.71463964, -0.26529706, 1.53238966, 0.24672242],
[-0.34132537, -0.06560819, -0.96228856, -1.09909685],
[ 0.13588173, 1.16987016, -1.29417478, -0.94341006],
[-0.60167379, -0.18647037, -0.03708144, -0.23882663]])
Exercise 52
Crea una función flip
que tome como inputs un array arr
y un número entero positivo i
e invierta el eje i-ésimo, es decir, si la dimensión del eje \(i\) vale \(d_i\), la transformación lleva el elemento con índice \((x_1, \dots, x_i, \dots, x_n)\) en \((x_1, \dots, x_i^*, \dots, x_n)\) donde \(x_i + x_i^* = d_i + 1\)
Por ejemplo,
arr = np.arange(9).reshape((3, 3))
arr
>>>
[[0 1 2]
[3 4 5]
[6 7 8]]
flip(arr, 1)
>>>
[[2 1 0]
[5 4 3]
[8 7 6]]
arr = np.arange(12).reshape((3, -1))
do_nothing = slice(None, None, None)
reverse = slice(None, None, -1)
def flip(arr: np.ndarray, i: int):
slices = (reverse if j==i else do_nothing for j in range(arr.ndim))
return arr[tuple(slices)]
print(arr)
print(flip(arr, 1))
[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
[[ 3 2 1 0]
[ 7 6 5 4]
[11 10 9 8]]