Los fundamentos de
las matrices NumPy
La manipulación de datos en Python es casi sinónimo de la manipulación de matrices NumPy: incluso las herramientas más nuevas como Pandas se construyen alrededor de la matriz NumPy. Esta sección presentará varios ejemplos del uso de la manipulación de matrices NumPy para acceder a datos y submatrices, y para dividir, remodelar y unir las matrices. Si bien los tipos de operaciones que se muestran aquí pueden parecer un poco áridos y pedantes, comprenden los componentes básicos de muchos otros ejemplos utilizados a lo largo del libro. ¡Conócelos bien!
Cubriremos algunas categorías de manipulaciones básicas de matrices aquí:
- Atributos de las matrices: Determinar el tamaño, la forma, el consumo de memoria y los tipos de datos de las matrices.
- Indexación de matrices: Obtención y configuración del valor de elementos de matriz individuales.
- División de matrices: Obtener y configurar submatrices más pequeñas dentro de una matriz más grande.
- Remodelación de matrices: Cambiar la forma de una matriz determinada.
- Unir y dividir matrices: Combinar varias matrices en una y dividir una matriz en muchas.
Atributos de una matriz NumPy¶
Primero, analicemos algunos atributos útiles de la matriz. Comenzaremos definiendo tres matrices aleatorias, una matriz unidimensional, bidimensional y tridimensional. Usaremos el generador de números aleatorios de NumPy, que semilla con un valor establecido para asegurarnos de que se generen las mismas matrices aleatorias cada vez que se ejecute este código:
import numpy as np
np.random.seed(0) # seed for reproducibility
x1 = np.random.randint(10, size=6) # One-dimensional array
x2 = np.random.randint(10, size=(3, 4)) # Two-dimensional array
x3 = np.random.randint(10, size=(3, 4, 5)) # Three-dimensional array
Cada matriz tiene atributos ndim
(el número de dimensiones), shape
(el tamaño de cada dimensión) y size
(el tamaño total de la matriz):
print("x3 ndim: ", x3.ndim)
print("x3 shape:", x3.shape)
print("x3 size: ", x3.size)
Otro atributo útil es el dtype
, el tipo de datos de la matriz (que se discutimó anteriormente en Comprensión de los tipos de datos en Python):
print("dtype:", x3.dtype)
Otros atributos incluyen itemize
, que enumera el tamaño (en bytes) de cada elemento de la matriz, y nbytes
, que enumera el tamaño total (en bytes) de la matriz:
print("itemsize:", x3.itemsize, "bytes")
print("nbytes:", x3.nbytes, "bytes")
En general, esperamos que nbytes
sea igual a itemize
multiplicado por size
.
Indexación de matrices:
Acceso a elementos individuales¶
Si está familiarizado con la indexación de listas estándar de Python, la indexación en NumPy le resultará bastante familiar. En una matriz unidimensional, se puede acceder al valor $i^{th}$ (contando desde cero) especificando el índice deseado entre corchetes, al igual que con las listas de Python:
x1
x1[0]
x1[4]
Para indexar desde el final de la matriz, se pueden usar índices negativos:
x1[-1]
x1[-2]
En una matriz multidimensional, se puede acceder a los elementos mediante una tupla de índices separados por comas:
x2
x2[0, 0]
x2[2, 0]
x2[2, -1]
Los valores también se pueden modificar utilizando cualquiera de las notación de índice anterior:
x2[0, 0] = 12
x2
Tenga en cuenta que, a diferencia de las listas de Python, las matrices NumPy tienen un tipo fijo. Esto significa, por ejemplo, que si intenta insertar un valor de punto flotante en una matriz de enteros, el valor se truncará silenciosamente. ¡No se deje sorprender por este comportamiento!
x1[0] = 3.14159 # this will be truncated!
x1
Rebanado de matrices:
Acceso a submatrices¶
Así como podemos usar corchetes para acceder a elementos individuales de la matriz, también podemos usarlos para acceder a submatrices con la notación rebanada, marcada con dos puntos (:
) personaje. La sintaxis de segmentación de NumPy sigue la de la lista estándar de Python; para acceder a una porción de una matriz x
, use esto:
x[start:stop:step]
Si alguno de estos no está especificado, por defecto tienen los valores start = 0
, stop=
size of dimension
, step = 1
. Echaremos un vistazo al acceso a submatrices en una dimensión y en múltiples dimensiones.
Subarreglos unidimensionales¶
x = np.arange(10)
x
x[:5] # first five elements
x[5:] # elements after index 5
x[4:7] # middle sub-array
x[::2] # every other element
x[1::2] # every other element, starting at index 1
Un caso potencialmente confuso es cuando el valor de step
es negativo. En este caso, se intercambian los valores predeterminados de start
y stop
. Esta se convierte en una forma conveniente de invertir una matriz:
x[::-1] # all elements, reversed
x[5::-2] # reversed every other from index 5
Subarreglos multidimensionales¶
Los sectores multidimensionales funcionan de la misma manera, con varios sectores separados por comas. Por ejemplo:
x2
x2[:2, :3] # two rows, three columns
x2[:3, ::2] # all rows, every other column
Finalmente, las dimensiones del subarreglo incluso se pueden invertir juntas:
x2[::-1, ::-1]
Acceder a filas y columnas de matriz¶
Una rutina comúnmente necesaria es acceder a filas o columnas individuales de una matriz. Esto se puede hacer combinando la indexación y la división, utilizando una división vacía marcada con dos puntos (:
):
print(x2[:, 0]) # first column of x2
print(x2[0, :]) # first row of x2
En el caso del acceso a filas, el segmento vacío se puede omitir para obtener una sintaxis más compacta:
print(x2[0]) # equivalent to x2[0, :]
Submatrices como vistas sin copia¶
Una cosa importante, y extremadamente útil, que debe saber acerca de los segmentos de matriz es que devuelven vistas en lugar de copias de los datos de la matriz. Esta es un área en la que la división de matrices de NumPy difiere de la división de listas de Python: en las listas, las divisiones serán copias. Considere nuestra matriz bidimensional de antes:
print(x2)
Extraigamos un subarreglo $2\times2$ de esto:
x2_sub = x2[:2, :2]
print(x2_sub)
Ahora, si modificamos este subarreglo, veremos que se cambia el arreglo original. Observar:
x2_sub[0, 0] = 99
print(x2_sub)
print(x2)
Este comportamiento predeterminado es bastante útil: significa que cuando trabajamos con grandes conjuntos de datos, podemos acceder y procesar partes de estos conjuntos de datos sin la necesidad de copiar el búfer de datos subyacente.
Creando copias de matrices¶
A pesar de las buenas características de las vistas de matriz, a veces es útil copiar explícitamente los datos dentro de una matriz o submatriz. Esto se puede hacer más fácilmente con el método copy ()
:
x2_sub_copy = x2[:2, :2].copy()
print(x2_sub_copy)
Si ahora modificamos este subarreglo, el arreglo original no se toca:
x2_sub_copy[0, 0] = 42
print(x2_sub_copy)
print(x2)
Remodelación de matrices¶
Otro tipo de operación útil es la remodelación de matrices. La forma más flexible de hacer esto es con el método reshape
. Por ejemplo, si desea colocar los números del 1 al 9 en una cuadrícula de $3 \times 3$, puede hacer lo siguiente:
grid = np.arange(1, 10).reshape((3, 3))
print(grid)
Tenga en cuenta que para que esto funcione, el tamaño de la matriz inicial debe coincidir con el tamaño de la matriz reformada.
Siempre que sea posible, el método reshape
utilizará una vista sin copia de la matriz inicial, pero con búferes de memoria no contiguos este no es siempre el caso.
Otro patrón de remodelación común es la conversión de una matriz unidimensional en una matriz bidimensional de filas o columnas. Esto se puede hacer con el método reshape
, o más fácilmente haciendo uso de la palabra clave newaxis
dentro de una operación de corte:
x = np.array([1, 2, 3])
# row vector via reshape
x.reshape((1, 3))
# row vector via newaxis
x[np.newaxis, :]
# column vector via reshape
x.reshape((3, 1))
# column vector via newaxis
x[:, np.newaxis]
Veremos este tipo de transformación a menudo a lo largo del resto del libro.
Concatenación y división de matrices¶
Todas las rutinas anteriores funcionaron en matrices únicas. También es posible combinar varias matrices en una y, a la inversa, dividir una sola matriz en varias matrices. Echaremos un vistazo a esas operaciones aquí.
Concatenación de matrices¶
La concatenación, o unión de dos matrices en NumPy, se logra principalmente mediante las rutinas np.concatenate
, np.vstack
y np.hstack
. np.concatenate
toma una tupla o lista de matrices como primer argumento, como podemos ver aquí:
x = np.array([1, 2, 3])
y = np.array([3, 2, 1])
np.concatenate([x, y])
También puede concatenar más de dos matrices a la vez:
z = [99, 99, 99]
print(np.concatenate([x, y, z]))
También se puede utilizar para matrices bidimensionales:
grid = np.array([[1, 2, 3],
[4, 5, 6]])
# concatenate along the first axis
np.concatenate([grid, grid])
# concatenate along the second axis (zero-indexed)
np.concatenate([grid, grid], axis=1)
Para trabajar con matrices de dimensiones mixtas, puede resultar más claro utilizar las funciones np.vstack
(pila vertical) y np.hstack
(pila horizontal):
x = np.array([1, 2, 3])
grid = np.array([[9, 8, 7],
[6, 5, 4]])
# vertically stack the arrays
np.vstack([x, grid])
# horizontally stack the arrays
y = np.array([[99],
[99]])
np.hstack([grid, y])
De manera similar, np.dstack
apilará matrices a lo largo del tercer eje.
División de matrices¶
Lo opuesto a la concatenación es la división, que se implementa mediante las funciones np.split
, np.hsplit
y np.vsplit
. Para cada uno de estos, podemos pasar una lista de índices dando los puntos de división:
x = [1, 2, 3, 99, 99, 3, 2, 1]
x1, x2, x3 = np.split(x, [3, 5])
print(x1, x2, x3)
Observe que N puntos de división, conduce a subarreglos N + 1. Las funciones relacionadas np.hsplit
y np.vsplit
son similares:
grid = np.arange(16).reshape((4, 4))
grid
upper, lower = np.vsplit(grid, [2])
print(upper)
print(lower)
left, right = np.hsplit(grid, [2])
print(left)
print(right)
De manera similar, np.dsplit
dividirá las matrices a lo largo del tercer eje.