Capítulo 4. Conceptos básicos de NumPy: Matrices y cálculo vectorial
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com
NumPy, abreviatura de Numerical Python, esuno de los paquetes fundacionales más importantes para la computación numérica en Python. Muchos paquetes computacionales que proporcionan funcionalidad científica utilizan los objetos array de NumPy como una de las lingua francas de interfaz estándar para el intercambio de datos. Gran parte de los conocimientos sobre NumPy que cubro son transferibles también a pandas.
Éstas son algunas de las cosas que encontrarás en NumPy:
ndarray, una eficiente matriz multidimensional que proporciona rápidas operaciones aritméticas orientadas a matrices y capacidades de difusiónflexibles
Funciones matemáticas para operaciones rápidas en matrices enteras de datos sin tener que escribir bucles
Herramientas para leer/escribir datos de matrices en disco y trabajar con archivos mapeados en memoria
Capacidades de álgebra lineal, generación de números aleatorios y transformada de Fourier
Una API en C para conectar NumPy con bibliotecas escritas en C, C++ o FORTRAN
Dado que NumPy proporciona una API de C completa y bien documentada, essencillo pasar datos a bibliotecas externas escritas en un lenguaje de bajo nivel, y que las bibliotecas externas devuelvan datos a Python como matrices NumPy. Esta característica ha convertido a Python en el lenguaje preferido para envolver bases de código C, C++ o FORTRAN heredadas y dotarlas de una interfaz dinámica y accesible.
Aunque NumPy por sí mismo no proporciona funciones de modelado o científicas, conocer las matrices de NumPy y la computación orientada a matrices te ayudará a utilizar herramientas con semántica de computación de matrices, como pandas, de forma mucho más eficaz. Como NumPy es un tema extenso, más adelante trataré con más profundidad muchas funciones avanzadas de NumPy, como la transmisión (véase el Apéndice A). Muchas de estas funciones avanzadas no son necesarias para seguir el resto de este libro, pero pueden ayudarte a medida que profundices en la computación científica en Python.
Para la mayoría de las aplicaciones de análisis de datos, las principales áreas de funcionalidad en las que me centraré son:
Operaciones rápidas basadas en matrices para agrupar y limpiar datos, subconjuntar y filtrar, transformar y cualquier otro tipo de cálculo
Algoritmos comunes de matrices, como operaciones de ordenación, únicas y conjuntos
Estadística descriptiva eficaz y agregación/resumen de datos
Alineación de datos y manipulaciones relacionales de datos para fusionar y unir conjuntos de datos heterogéneos
Expresar la lógica condicional como expresiones de matriz en lugar de bucles con ramas
if-elif-else
Manipulaciones de datos en grupo (agregación, transformación y aplicación de funciones)
Aunque NumPy proporciona una base computacional para el procesamiento general de datos numéricos, muchos lectores querrán utilizar pandas como base para la mayoría de tipos de estadísticas o análisis, especialmente sobre datos tabulares. Además, pandas proporciona algunas funcionalidades más específicas del dominio, como la manipulación de series temporales, que no están presentes en NumPy.
Nota
La computación orientada a matrices en Python hunde sus raíces en 1995,cuando Jim Hugunin creó la biblioteca Numeric. Durante los 10 años siguientes, muchas comunidades de programación científica empezaron a programar matrices en Python, pero el ecosistema de bibliotecas se había fragmentado ena principios de la década de 2000. En 2005, Travis Oliphant consiguió forjar el proyecto NumPy a partir de los entonces proyectos Numeric y Numarray para reunir a la comunidad en torno a un único marco de computación de matrices.
Una de las razones por las que NumPy es tan importante para los cálculos numéricos enPython es porque está diseñado para ser eficiente con grandes matrices de datos. Hay varias razones para ello:
NumPy almacena internamente los datos en un bloque contiguo de memoria, independiente de otros objetos incorporados de Python. La biblioteca de algoritmos de NumPy, escrita en lenguaje C, puede operar en esta memoria sin comprobación de tipos ni otros gastos generales. Las matrices de NumPy también utilizan mucha menos memoria que las secuencias incorporadas de Python.
Las operaciones NumPy realizan cálculos complejos en matrices enteras sin necesidad de bucles Python
for
, que pueden ser lentos para secuencias grandes. NumPy es más rápido que el código Python normal porque sus algoritmos basados en C evitan la sobrecarga presente en el código Python interpretado normal.
Para que te hagas una idea de la diferencia de rendimiento, considera una matriz NumPy de un millón de enteros, y la lista Python equivalente:
In
[
7
]:
import
numpy
as
np
In
[
8
]:
my_arr
=
np
.
arange
(
1_000_000
)
In
[
9
]:
my_list
=
list
(
range
(
1_000_000
))
Ahora multipliquemos cada secuencia por 2:
In
[
10
]:
%
timeit
my_arr2
=
my_arr
*
2
721
us
+-
7.49
us
per
loop
(
mean
+-
std
.
dev
.
of
7
runs
,
1000
loops
each
)
In
[
11
]:
%
timeit
my_list2
=
[
x
*
2
for
x
in
my_list
]
49
ms
+-
1.02
ms
per
loop
(
mean
+-
std
.
dev
.
of
7
runs
,
10
loops
each
)
Los algoritmos basados en NumPy suelen ser de 10 a 100 veces más rápidos (o más) que sus homólogos en Python puro y utilizan mucha menos memoria.
4.1 La matriz NumPy ndarray: Un objeto matriz multidimensional
Una de las características clave de NumPy es su objeto matriz N-dimensional, o ndarray, que es un contenedor rápido y flexible para grandes conjuntos de datos en Python. Las matrices te permiten realizar operaciones matemáticas en bloques enteros de datos utilizando una sintaxis similar a la de las operaciones equivalentes entre elementos escalares.
Para que te hagas una idea de cómo NumPy permite realizar cálculos por lotes con una sintaxis similar a la de los valores escalares en los objetos incorporados de Python, primero importo NumPy y creo una pequeña matriz:
In
[
12
]:
import
numpy
as
np
In
[
13
]:
data
=
np
.
array
([[
1.5
,
-
0.1
,
3
],
[
0
,
-
3
,
6.5
]])
In
[
14
]:
data
Out
[
14
]:
array
([[
1.5
,
-
0.1
,
3.
],
[
0.
,
-
3.
,
6.5
]])
A continuación, escribo operaciones matemáticas con data
:
In
[
15
]:
data
*
10
Out
[
15
]:
array
([[
15.
,
-
1.
,
30.
],
[
0.
,
-
30.
,
65.
]])
In
[
16
]:
data
+
data
Out
[
16
]:
array
([[
3.
,
-
0.2
,
6.
],
[
0.
,
-
6.
,
13.
]])
En el primer ejemplo, todos los elementos se han multiplicado por 10. En el segundo, los valores correspondientes de cada "celda" de la matriz se han sumado entre sí.
Nota
En este capítulo y en todo el libro, utilizo la convención estándar de NumPy de utilizar siempre import numpy as np
. Sería posible poner from numpy import *
en tu código para evitar tener que escribir np.
, pero te desaconsejo que lo hagas por costumbre. El espacio de nombresnumpy
es grande y contiene varias funciones cuyos nombres entran en conflicto con funciones incorporadas de Python (como min
ymax
). Seguir convenciones estándar como éstas es casi siempre una buena idea.
Un ndarray es un contenedor multidimensional genérico para datos homogéneos; es decir, todos los elementos deben ser del mismo tipo. Toda matriz tiene un shape
, una tupla que indica el tamaño de cada dimensión, y un dtype
, un objeto que describe el tipo de datos de la matriz:
In
[
17
]:
data
.
shape
Out
[
17
]:
(
2
,
3
)
In
[
18
]:
data
.
dtype
Out
[
18
]:
dtype
(
'float64'
)
Este capítulo te introducirá en los fundamentos del uso de matrices NumPy, y debería ser suficiente para seguir el resto del libro. Aunque no es necesario tener un conocimiento profundo de NumPy para muchas aplicaciones analíticas de datos, dominar la programación y el pensamiento orientados a matrices es un paso clave en el camino para convertirse en un gurú científico de Python.
Nota
Siempre que veas "array", "NumPy array" o "ndarray" en el texto del libro, en la mayoría de los casos todos se refieren al objeto ndarray.
Crear ndarrays
La forma más sencilla de crear una matriz es utilizar la función array
. Ésta acepta cualquier objeto tipo secuencia (incluidas otras matrices) y produce una nueva matriz NumPy que contiene los datos pasados. Por ejemplo, una lista es un buen candidato para la conversión:
In
[
19
]:
data1
=
[
6
,
7.5
,
8
,
0
,
1
]
In
[
20
]:
arr1
=
np
.
array
(
data1
)
In
[
21
]:
arr1
Out
[
21
]:
array
([
6.
,
7.5
,
8.
,
0.
,
1.
])
Las secuencias anidadas, como una lista de listas de igual longitud, se convertirán en una matriz multidimensional:
In
[
22
]:
data2
=
[[
1
,
2
,
3
,
4
],
[
5
,
6
,
7
,
8
]]
In
[
23
]:
arr2
=
np
.
array
(
data2
)
In
[
24
]:
arr2
Out
[
24
]:
array
([[
1
,
2
,
3
,
4
],
[
5
,
6
,
7
,
8
]])
Puesto que data2
era una lista de listas, la matriz NumPy arr2
tiene dos dimensiones, con forma inferida a partir de los datos. Podemos confirmarlo inspeccionando los atributos ndim
y shape
:
In
[
25
]:
arr2
.
ndim
Out
[
25
]:
2
In
[
26
]:
arr2
.
shape
Out
[
26
]:
(
2
,
4
)
A menos que se especifique explícitamente (se trata en "Tipos de datos para ndarrays"), numpy.array
intenta inferir un buen tipo de datos para el array que crea. El tipo de datos se almacena en un objeto especial de metadatos dtype
; por ejemplo, en los dos ejemplos anteriores tenemos:
In
[
27
]:
arr1
.
dtype
Out
[
27
]:
dtype
(
'float64'
)
In
[
28
]:
arr2
.
dtype
Out
[
28
]:
dtype
(
'int64'
)
Además de numpy.array
, existen otras funciones para crear nuevas matrices. Como ejemplos, numpy.zeros
y numpy.ones
crean matrices de 0s o 1s, respectivamente, con una longitud o forma dadas. numpy.empty
crea una matriz sin inicializar sus valores a ningún valor concreto. Para crear una matriz de mayor dimensión con estos métodos, pasa una tupla para la forma:
In
[
29
]:
np
.
zeros
(
10
)
Out
[
29
]:
array
([
0.
,
0.
,
0.
,
0.
,
0.
,
0.
,
0.
,
0.
,
0.
,
0.
])
In
[
30
]:
np
.
zeros
((
3
,
6
))
Out
[
30
]:
array
([[
0.
,
0.
,
0.
,
0.
,
0.
,
0.
],
[
0.
,
0.
,
0.
,
0.
,
0.
,
0.
],
[
0.
,
0.
,
0.
,
0.
,
0.
,
0.
]])
In
[
31
]:
np
.
empty
((
2
,
3
,
2
))
Out
[
31
]:
array
([[[
0.
,
0.
],
[
0.
,
0.
],
[
0.
,
0.
]],
[[
0.
,
0.
],
[
0.
,
0.
],
[
0.
,
0.
]]])
Precaución
No es seguro asumir que numpy.empty
devolverá una matriz de todos ceros. Esta función devuelve memoria no inicializada y, por tanto, puede contener valores "basura" distintos de cero. Sólo debes utilizar esta función si pretendes rellenar la nueva matriz con datos.
numpy.arange
es una versión con valores de matrizde la función incorporada de Python range
:
In
[
32
]:
np
.
arange
(
15
)
Out
[
32
]:
array
([
0
,
1
,
2
,
3
,
4
,
5
,
6
,
7
,
8
,
9
,
10
,
11
,
12
,
13
,
14
])
Consulta la Tabla 4-1 para ver una breve lista de funciones estándar de creación de matrices. Dado que NumPy se centra en la computación numérica, el tipo de datos, si no se especifica, será en muchos casos float64
(coma flotante).
Función | Descripción |
---|---|
array | Convierte los datos de entrada (lista, tupla, matriz u otro tipo de secuencia) en una ndarray, ya sea deduciendo un tipo de datos o especificando explícitamente un tipo de datos; copia los datos de entrada por defecto |
asarray | Convertir la entrada en ndarray, pero no copiar si la entrada ya es un ndarray |
arange | Como el built-in range pero devuelve un ndarray en lugar de una lista |
ones, ones_like | Produce una matriz de todos los 1s con la forma y el tipo de datos dados; ones_like toma otra matriz y produce una matriz ones de la misma forma y tipo de datos |
zeros, zeros_like | Como ones y ones_like pero produciendo matrices de 0s en lugar de 0s |
empty, empty_like | Crea nuevas matrices asignando nueva memoria, pero no las rellenes con ningún valor como ones y zeros |
full, full_like | Produce una matriz de la forma y tipo de datos dados con todos los valores ajustados al "valor de relleno" indicado; full_like toma otra matriz y produce una matriz rellena de la misma forma y tipo de datos |
eye, identity | Crea una matriz cuadrada N × N de identidad (1s en la diagonal y 0s en el resto) |
Tipos de datos para ndarrays
El tipo de datos o dtype
es un objeto especial que contiene la información (o metadatos, datos sobre datos) que la ndarray necesita para interpretar un trozo de memoria como un tipo de datos concreto:
In
[
33
]:
arr1
=
np
.
array
([
1
,
2
,
3
],
dtype
=
np
.
float64
)
In
[
34
]:
arr2
=
np
.
array
([
1
,
2
,
3
],
dtype
=
np
.
int32
)
In
[
35
]:
arr1
.
dtype
Out
[
35
]:
dtype
(
'float64'
)
In
[
36
]:
arr2
.
dtype
Out
[
36
]:
dtype
(
'int32'
)
Los tipos de datos son una fuente de la flexibilidad de NumPy para interactuar con datos procedentes de otros sistemas. En la mayoría de los casos, proporcionan un mapeo directo a una representación subyacente en disco o memoria, lo que permite leer y escribir flujos binarios de datos en disco y conectar con código escrito en un lenguaje de bajo nivel como C o FORTRAN. Los tipos de datos numéricos se nombran de la misma manera: un nombre de tipo, como float
o int
, seguido de un número que indica el número de bits por elemento. Un valor estándar de coma flotante de doble precisión (lo que se utiliza en el objeto float
de Python) ocupa 8 bytes o 64 bits. Así, este tipo se conoce en NumPy como float64
. Consulta la Tabla 4-2 para ver una lista completa de los tipos de datos soportados por NumPy.
Nota
No te preocupes por memorizar los tipos de datos de NumPy, sobre todo si eres un usuario nuevo. A menudo sólo es necesario preocuparse por el tipo general de datos con los que estás tratando, ya sean de coma flotante, complejos, enteros, booleanos, cadenas u objetos generales de Python. Cuando necesites más control sobre cómo se almacenan los datos en la memoria y en el disco, especialmente los grandes conjuntos de datos, es bueno saber que tienes control sobre el tipo de almacenamiento.
Tipo | Código de tipo | Descripción |
---|---|---|
int8, uint8 | i1, u1 | Tipos enteros de 8 bits (1 byte) con signo y sin signo |
int16, uint16 | i2, u2 | Tipos enteros de 16 bits con signo y sin signo |
int32, uint32 | i4, u4 | Tipos enteros de 32 bits con signo y sin signo |
int64, uint64 | i8, u8 | Tipos enteros de 64 bits con signo y sin signo |
float16 | f2 | Punto flotante de precisión media |
float32 | f4 or f | Punto flotante de precisión única estándar; compatible con C float |
float64 | f8 or d | Punto flotante de doble precisión estándar; compatible con C double y Python float object |
float128 | f16 or g | Punto flotante de precisión ampliada |
complex64 , complex128 , complex256 | c8, c16, c32 | Números complejos representados por dos flotantes de 32, 64 ó 128, respectivamente |
bool | ? | Tipo booleano que almacena los valores True y False |
object | O | Tipo de objeto Python; un valor puede ser cualquier objeto Python |
string_ | S | Tipo de cadena ASCII de longitud fija (1 byte por carácter); por ejemplo, para crear un tipo de datos de cadena con longitud 10, utiliza 'S10' |
unicode_ | U | Tipo Unicode de longitud fija (número de bytes específico de la plataforma); misma semántica de especificación que string_ (por ejemplo, 'U10' ) |
Nota
Existen tipos de enteros con signo y sin signo, y muchos lectores no estarán familiarizados con esta terminología. Un entero con signo puede representar enteros positivos y negativos, mientras que un entero sin signosólo puede representar enteros distintos de cero. Por ejemplo, int8
(entero de 8 bits con signo) puede representar enteros de -128 a 127 (ambos inclusive), mientras que uint8
(entero de 8 bits sin signo) puede representar de 0 a 255.
Puedes convertir explícitamente o fundir una matriz de un tipo de datos a otro utilizando el método astype
de ndarray:
In
[
37
]:
arr
=
np
.
array
([
1
,
2
,
3
,
4
,
5
])
In
[
38
]:
arr
.
dtype
Out
[
38
]:
dtype
(
'int64'
)
In
[
39
]:
float_arr
=
arr
.
astype
(
np
.
float64
)
In
[
40
]:
float_arr
Out
[
40
]:
array
([
1.
,
2.
,
3.
,
4.
,
5.
])
In
[
41
]:
float_arr
.
dtype
Out
[
41
]:
dtype
(
'float64'
)
En este ejemplo, los números enteros se han convertido a coma flotante. Si convierto algunos números de coma flotante en enteros, la parte decimal se truncará:
In
[
42
]:
arr
=
np
.
array
([
3.7
,
-
1.2
,
-
2.6
,
0.5
,
12.9
,
10.1
])
In
[
43
]:
arr
Out
[
43
]:
array
([
3.7
,
-
1.2
,
-
2.6
,
0.5
,
12.9
,
10.1
])
In
[
44
]:
arr
.
astype
(
np
.
int32
)
Out
[
44
]:
array
([
3
,
-
1
,
-
2
,
0
,
12
,
10
],
dtype
=
int32
)
Si tienes una matriz de cadenas que representan números, puedes utilizarastype
para convertirlas a forma numérica:
In
[
45
]:
numeric_strings
=
np
.
array
([
"1.25"
,
"-9.6"
,
"42"
],
dtype
=
np
.
string_
)
In
[
46
]:
numeric_strings
.
astype
(
float
)
Out
[
46
]:
array
([
1.25
,
-
9.6
,
42.
])
Precaución
Ten cuidado cuando utilices el tipo numpy.string_
, ya que los datos de cadena en NumPy son de tamaño fijo y pueden truncar la entrada sin previo aviso. pandas tiene un comportamiento más intuitivo con los datos no numéricos.
Si la conversión fallara por algún motivo (como una cadena que no se puede convertir a float64
), se producirá un errorValueError
. Antes, me daba un poco de pereza y escribía float
en lugar de np.float64
; NumPy asigna alias a los tipos de Python a sus propios tipos de datos equivalentes.
También puedes utilizar el atributo dtype
de otra matriz:
In
[
47
]:
int_array
=
np
.
arange
(
10
)
In
[
48
]:
calibers
=
np
.
array
([
.22
,
.270
,
.357
,
.380
,
.44
,
.50
],
dtype
=
np
.
float64
)
In
[
49
]:
int_array
.
astype
(
calibers
.
dtype
)
Out
[
49
]:
array
([
0.
,
1.
,
2.
,
3.
,
4.
,
5.
,
6.
,
7.
,
8.
,
9.
])
Existen cadenas de código de tipo abreviado que también puedes utilizar para referirte a un dtype
:
In
[
50
]:
zeros_uint32
=
np
.
zeros
(
8
,
dtype
=
"u4"
)
In
[
51
]:
zeros_uint32
Out
[
51
]:
array
([
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
],
dtype
=
uint32
)
Aritmética con matrices NumPy
Las matrices son importantes porque te permiten expresar operaciones por lotes sobre datos sin escribir ningún bucle for
. Los usuarios de NumPy llaman a esto vectorización. Cualquier operación aritmética entre matrices de igual tamaño aplica la operación elemento a elemento:
In
[
52
]:
arr
=
np
.
array
([[
1.
,
2.
,
3.
],
[
4.
,
5.
,
6.
]])
In
[
53
]:
arr
Out
[
53
]:
array
([[
1.
,
2.
,
3.
],
[
4.
,
5.
,
6.
]])
In
[
54
]:
arr
*
arr
Out
[
54
]:
array
([[
1.
,
4.
,
9.
],
[
16.
,
25.
,
36.
]])
In
[
55
]:
arr
-
arr
Out
[
55
]:
array
([[
0.
,
0.
,
0.
],
[
0.
,
0.
,
0.
]])
Las operaciones aritméticas con escalares propagan el argumento escalar a cada elemento de la matriz:
In
[
56
]:
1
/
arr
Out
[
56
]:
array
([[
1.
,
0.5
,
0.3333
],
[
0.25
,
0.2
,
0.1667
]])
In
[
57
]:
arr
**
2
Out
[
57
]:
array
([[
1.
,
4.
,
9.
],
[
16.
,
25.
,
36.
]])
Las comparaciones entre matrices del mismo tamaño producen matrices booleanas:
In
[
58
]:
arr2
=
np
.
array
([[
0.
,
4.
,
1.
],
[
7.
,
2.
,
12.
]])
In
[
59
]:
arr2
Out
[
59
]:
array
([[
0.
,
4.
,
1.
],
[
7.
,
2.
,
12.
]])
In
[
60
]:
arr2
>
arr
Out
[
60
]:
array
([[
False
,
True
,
False
],
[
True
,
False
,
True
]])
La evaluación de operaciones entre matrices de distinto tamaño se denomina difusión y se tratará con más detalle enApéndice A. Para la mayor parte de este libro no es necesario tener un conocimiento profundo de la difusión.
Indexación y segmentación básicas
La indexación de matrices en NumPy es un tema profundo, ya que hay muchas formas en las que puedesquerer seleccionar un subconjunto de tus datos o elementos individuales. Las matrices unidimensionales son sencillas; en apariencia actúan de forma similar a las listas de Python:
In
[
61
]:
arr
=
np
.
arange
(
10
)
In
[
62
]:
arr
Out
[
62
]:
array
([
0
,
1
,
2
,
3
,
4
,
5
,
6
,
7
,
8
,
9
])
In
[
63
]:
arr
[
5
]
Out
[
63
]:
5
In
[
64
]:
arr
[
5
:
8
]
Out
[
64
]:
array
([
5
,
6
,
7
])
In
[
65
]:
arr
[
5
:
8
]
=
12
In
[
66
]:
arr
Out
[
66
]:
array
([
0
,
1
,
2
,
3
,
4
,
12
,
12
,
12
,
8
,
9
])
Como puedes ver, si asignas un valor escalar a una rebanada, como en arr[5:8] = 12
, el valor se propaga (o difunde en adelante) a toda la selección.
Nota
Una primera distinción importante con respecto a las listas incorporadas de Python es que las rebanadas de matriz son vistas de la matriz original. Esto significa que los datos no se copian, y que cualquier modificación de la vista se reflejará en la matriz de origen.
Para dar un ejemplo de esto, primero creo una rebanada de arr
:
In
[
67
]:
arr_slice
=
arr
[
5
:
8
]
In
[
68
]:
arr_slice
Out
[
68
]:
array
([
12
,
12
,
12
])
Ahora, cuando cambio valores en arr_slice
, las mutaciones se reflejan en la matriz original arr
:
In
[
69
]:
arr_slice
[
1
]
=
12345
In
[
70
]:
arr
Out
[
70
]:
array
([
0
,
1
,
2
,
3
,
4
,
12
,
12345
,
12
,
8
,
9
])
La rebanada "desnuda" [:]
asignará a todos los valores de una matriz:
In
[
71
]:
arr_slice
[:]
=
64
In
[
72
]:
arr
Out
[
72
]:
array
([
0
,
1
,
2
,
3
,
4
,
64
,
64
,
64
,
8
,
9
])
Si eres nuevo en NumPy, puede que esto te sorprenda, sobre todo si has utilizado otros lenguajes de programación de matrices que copian los datos con más avidez. Como NumPy se ha diseñado para poder trabajar con matrices muy grandes, podrías imaginar problemas de rendimiento y de memoria si NumPy insistiera en copiar siempre los datos.
Precaución
Si quieres una copia de una porción de un ndarray en lugar de una vista, tendrás que copiar explícitamente el array; por ejemplo, arr[5:8].copy()
. Como verás, pandas también funciona así.
Con matrices de mayor dimensión, tienes muchas más opciones. En una matriz bidimensional, los elementos de cada índice ya no son escalares, sino matrices unidimensionales:
In
[
73
]:
arr2d
=
np
.
array
([[
1
,
2
,
3
],
[
4
,
5
,
6
],
[
7
,
8
,
9
]])
In
[
74
]:
arr2d
[
2
]
Out
[
74
]:
array
([
7
,
8
,
9
])
Así, se puede acceder recursivamente a elementos individuales. Pero eso es demasiado trabajo, así que puedes pasar una lista de índices separados por comas para seleccionar elementos individuales. Así que son equivalentes:
In
[
75
]:
arr2d
[
0
][
2
]
Out
[
75
]:
3
In
[
76
]:
arr2d
[
0
,
2
]
Out
[
76
]:
3
Consulta la Figura 4-1 para ver una ilustración de la indexación en una matriz bidimensional. Me resulta útil pensar en el eje 0 como las "filas" del array y en el eje 1 como las "columnas".
En los arrays multidimensionales, si omites los índices posteriores, el objeto devuelto será un ndarray de dimensión inferior formado por todos los datos de las dimensiones superiores. Así, en la matriz de 2 × 2 × 3 arr3d
:
In
[
77
]:
arr3d
=
np
.
array
([[[
1
,
2
,
3
],
[
4
,
5
,
6
]],
[[
7
,
8
,
9
],
[
10
,
11
,
12
]]])
In
[
78
]:
arr3d
Out
[
78
]:
array
([[[
1
,
2
,
3
],
[
4
,
5
,
6
]],
[[
7
,
8
,
9
],
[
10
,
11
,
12
]]])
arr3d[0]
es una matriz de 2 × 3:
In
[
79
]:
arr3d
[
0
]
Out
[
79
]:
array
([[
1
,
2
,
3
],
[
4
,
5
,
6
]])
Tanto los valores escalares como las matrices pueden asignarse a arr3d[0]
:
In
[
80
]:
old_values
=
arr3d
[
0
]
.
copy
()
In
[
81
]:
arr3d
[
0
]
=
42
In
[
82
]:
arr3d
Out
[
82
]:
array
([[[
42
,
42
,
42
],
[
42
,
42
,
42
]],
[[
7
,
8
,
9
],
[
10
,
11
,
12
]]])
In
[
83
]:
arr3d
[
0
]
=
old_values
In
[
84
]:
arr3d
Out
[
84
]:
array
([[[
1
,
2
,
3
],
[
4
,
5
,
6
]],
[[
7
,
8
,
9
],
[
10
,
11
,
12
]]])
Del mismo modo, arr3d[1, 0]
te da todos los valores cuyos índices empiezan por (1, 0)
, formando una matriz unidimensional:
In
[
85
]:
arr3d
[
1
,
0
]
Out
[
85
]:
array
([
7
,
8
,
9
])
Esta expresión es la misma que si hubiéramos indexado en dos pasos:
In
[
86
]:
x
=
arr3d
[
1
]
In
[
87
]:
x
Out
[
87
]:
array
([[
7
,
8
,
9
],
[
10
,
11
,
12
]])
In
[
88
]:
x
[
0
]
Out
[
88
]:
array
([
7
,
8
,
9
])
Observa que en todos estos casos en los que se han seleccionado subsecciones de la matriz, las matrices devueltas son vistas.
Precaución
Esta sintaxis de indexación multidimensional para matrices NumPy no funcionará con objetos normales de Python, como listas de listas.
Indexar con rodajas
Al igual que los objetos unidimensionales, como las listas de Python, las ndarrays se pueden cortar con la sintaxis conocida:
In
[
89
]:
arr
Out
[
89
]:
array
([
0
,
1
,
2
,
3
,
4
,
64
,
64
,
64
,
8
,
9
])
In
[
90
]:
arr
[
1
:
6
]
Out
[
90
]:
array
([
1
,
2
,
3
,
4
,
64
])
Considera la matriz bidimensional de antes, arr2d
. Cortar esta matriz es un poco diferente:
In
[
91
]:
arr2d
Out
[
91
]:
array
([[
1
,
2
,
3
],
[
4
,
5
,
6
],
[
7
,
8
,
9
]])
In
[
92
]:
arr2d
[:
2
]
Out
[
92
]:
array
([[
1
,
2
,
3
],
[
4
,
5
,
6
]])
Como puedes ver, se ha cortado a lo largo del eje 0, el primer eje. Un corte, por tanto, selecciona un rango de elementos a lo largo de un eje. Puede ser útil leer la expresión arr2d[:2]
como "selecciona las dos primeras filas de arr2d
."
Puedes pasar varios cortes, igual que puedes pasar varios índices:
In
[
93
]:
arr2d
[:
2
,
1
:]
Out
[
93
]:
array
([[
2
,
3
],
[
5
,
6
]])
Al rebanar así, siempre obtienes vistas de matriz del mismo número de dimensiones. Mezclando índices enteros y cortes, obtienes cortes de menor dimensión.
Por ejemplo, puedo seleccionar la segunda fila pero sólo las dos primeras columnas, así:
In
[
94
]:
lower_dim_slice
=
arr2d
[
1
,
:
2
]
Aquí, mientras que arr2d
es bidimensional, lower_dim_slice
es unidimensional, y su forma es una tupla con un tamaño de eje:
In
[
95
]:
lower_dim_slice
.
shape
Out
[
95
]:
(
2
,)
Del mismo modo, puedo seleccionar la tercera columna pero sólo las dos primeras filas, así:
In
[
96
]:
arr2d
[:
2
,
2
]
Out
[
96
]:
array
([
3
,
6
])
Mira la Figura 4-2 para ver una ilustración. Ten en cuenta que dos puntos por sí solos significan tomar todo el eje, por lo que sólo puedes cortar ejes de dimensiones superiores haciendo:
In
[
97
]:
arr2d
[:,
:
1
]
Out
[
97
]:
array
([[
1
],
[
4
],
[
7
]])
Por supuesto, al asignar a una expresión de trozo se asigna a toda la selección:
In
[
98
]:
arr2d
[:
2
,
1
:]
=
0
In
[
99
]:
arr2d
Out
[
99
]:
array
([[
1
,
0
,
0
],
[
4
,
0
,
0
],
[
7
,
8
,
9
]])
Indexación booleana
Consideremos un ejemplo en el que tenemos unos datos en una matrizy una matriz de nombres con duplicados:
In
[
100
]:
names
=
np
.
array
([
"Bob"
,
"Joe"
,
"Will"
,
"Bob"
,
"Will"
,
"Joe"
,
"Joe"
])
In
[
101
]:
data
=
np
.
array
([[
4
,
7
],
[
0
,
2
],
[
-
5
,
6
],
[
0
,
0
],
[
1
,
2
],
.....
:
[
-
12
,
-
4
],
[
3
,
4
]])
In
[
102
]:
names
Out
[
102
]:
array
([
'Bob'
,
'Joe'
,
'Will'
,
'Bob'
,
'Will'
,
'Joe'
,
'Joe'
],
dtype
=
'<U4'
)
In
[
103
]:
data
Out
[
103
]:
array
([[
4
,
7
],
[
0
,
2
],
[
-
5
,
6
],
[
0
,
0
],
[
1
,
2
],
[
-
12
,
-
4
],
[
3
,
4
]])
Supongamos que cada nombre corresponde a una fila de la matriz data
y queremos seleccionar todas las filas con el nombre correspondiente "Bob"
. Al igual que las operaciones aritméticas, las comparaciones (como ==
) con matrices también se vectorizan. Así, al comparar names
con la cadena "Bob"
se obtiene una matriz booleana:
In
[
104
]:
names
==
"Bob"
Out
[
104
]:
array
([
True
,
False
,
False
,
True
,
False
,
False
,
False
])
Esta matriz booleana se puede pasar al indexar la matriz:
In
[
105
]:
data
[
names
==
"Bob"
]
Out
[
105
]:
array
([[
4
,
7
],
[
0
,
0
]])
La matriz booleana debe tener la misma longitud que el eje de la matriz que está indexando. Incluso puedes mezclar y combinar matrices booleanas con rebanadas o enteros (o secuencias de enteros; más adelante hablaremos de ello).
En estos ejemplos, selecciono de las filas donde names ==
"Bob"
e indizo también las columnas:
In
[
106
]:
data
[
names
==
"Bob"
,
1
:]
Out
[
106
]:
array
([[
7
],
[
0
]])
In
[
107
]:
data
[
names
==
"Bob"
,
1
]
Out
[
107
]:
array
([
7
,
0
])
Para seleccionar todo menos "Bob"
puedes utilizar !=
o negar la condición utilizando ~
:
In
[
108
]:
names
!=
"Bob"
Out
[
108
]:
array
([
False
,
True
,
True
,
False
,
True
,
True
,
True
])
In
[
109
]:
~
(
names
==
"Bob"
)
Out
[
109
]:
array
([
False
,
True
,
True
,
False
,
True
,
True
,
True
])
In
[
110
]:
data
[
~
(
names
==
"Bob"
)]
Out
[
110
]:
array
([[
0
,
2
],
[
-
5
,
6
],
[
1
,
2
],
[
-
12
,
-
4
],
[
3
,
4
]])
El operador ~
puede ser útil cuando quieras invertir una matriz booleana referenciada por una variable:
In
[
111
]:
cond
=
names
==
"Bob"
In
[
112
]:
data
[
~
cond
]
Out
[
112
]:
array
([[
0
,
2
],
[
-
5
,
6
],
[
1
,
2
],
[
-
12
,
-
4
],
[
3
,
4
]])
Para seleccionar dos de los tres nombres para combinar varias condiciones booleanas , utiliza operadores aritméticos booleanos como &
(y) y |
(o):
In
[
113
]:
mask
=
(
names
==
"Bob"
)
|
(
names
==
"Will"
)
In
[
114
]:
mask
Out
[
114
]:
array
([
True
,
False
,
True
,
True
,
True
,
False
,
False
])
In
[
115
]:
data
[
mask
]
Out
[
115
]:
array
([[
4
,
7
],
[
-
5
,
6
],
[
0
,
0
],
[
1
,
2
]])
Seleccionar datos de una matriz mediante indexación booleana y asignar el resultado a una nueva variable siemprecrea una copia de los datos, aunque la matriz devuelta no se modifique.
Precaución
Las palabras clave de Python and
y or
no funcionan con matrices booleanas. Utiliza &
(y) y |
(o) en su lugar.
La fijación de valores con matrices booleanas funciona sustituyendo el valor o valores de la parte derecha en los lugares donde están los valores de la matriz booleana True
. Para poner a 0 todos los valores negativos de data
, sólo tenemos que hacer:
In
[
116
]:
data
[
data
<
0
]
=
0
In
[
117
]:
data
Out
[
117
]:
array
([[
4
,
7
],
[
0
,
2
],
[
0
,
6
],
[
0
,
0
],
[
1
,
2
],
[
0
,
0
],
[
3
,
4
]])
También puedes establecer filas o columnas enteras utilizando una matriz booleana unidimensional:
In
[
118
]:
data
[
names
!=
"Joe"
]
=
7
In
[
119
]:
data
Out
[
119
]:
array
([[
7
,
7
],
[
0
,
2
],
[
7
,
7
],
[
7
,
7
],
[
7
,
7
],
[
0
,
0
],
[
3
,
4
]])
Como veremos más adelante, este tipo de operaciones sobre datos bidimensionales es conveniente hacerlas con pandas.
Indexación de fantasía
La indexación extravagante es un término adoptado por NumPy para describir la indexación mediante matrices de enteros. Supongamos que tenemos una matriz de 8 × 4:
In
[
120
]:
arr
=
np
.
zeros
((
8
,
4
))
In
[
121
]:
for
i
in
range
(
8
):
.....
:
arr
[
i
]
=
i
In
[
122
]:
arr
Out
[
122
]:
array
([[
0.
,
0.
,
0.
,
0.
],
[
1.
,
1.
,
1.
,
1.
],
[
2.
,
2.
,
2.
,
2.
],
[
3.
,
3.
,
3.
,
3.
],
[
4.
,
4.
,
4.
,
4.
],
[
5.
,
5.
,
5.
,
5.
],
[
6.
,
6.
,
6.
,
6.
],
[
7.
,
7.
,
7.
,
7.
]])
Para seleccionar un subconjunto de filas en un orden determinado, sólo tienes que pasar una lista o ndarray de enteros especificando el orden deseado:
In
[
123
]:
arr
[[
4
,
3
,
0
,
6
]]
Out
[
123
]:
array
([[
4.
,
4.
,
4.
,
4.
],
[
3.
,
3.
,
3.
,
3.
],
[
0.
,
0.
,
0.
,
0.
],
[
6.
,
6.
,
6.
,
6.
]])
¡Espero que este código haya hecho lo que esperabas! Utilizar índices negativos selecciona las filas desde el final:
In
[
124
]:
arr
[[
-
3
,
-
5
,
-
7
]]
Out
[
124
]:
array
([[
5.
,
5.
,
5.
,
5.
],
[
3.
,
3.
,
3.
,
3.
],
[
1.
,
1.
,
1.
,
1.
]])
Pasar múltiples matrices de índices hace algo ligeramente distinto;selecciona una matriz unidimensional de elementos correspondientes a cada tupla de índices:
In
[
125
]:
arr
=
np
.
arange
(
32
)
.
reshape
((
8
,
4
))
In
[
126
]:
arr
Out
[
126
]:
array
([[
0
,
1
,
2
,
3
],
[
4
,
5
,
6
,
7
],
[
8
,
9
,
10
,
11
],
[
12
,
13
,
14
,
15
],
[
16
,
17
,
18
,
19
],
[
20
,
21
,
22
,
23
],
[
24
,
25
,
26
,
27
],
[
28
,
29
,
30
,
31
]])
In
[
127
]:
arr
[[
1
,
5
,
7
,
2
],
[
0
,
3
,
1
,
2
]]
Out
[
127
]:
array
([
4
,
23
,
29
,
10
])
Para saber más sobre el método reshape
, echa un vistazo al Apéndice A.
Aquí se seleccionaron los elementos (1, 0), (5, 3),
(7, 1)
, y (2, 2)
. El resultado de una indexación fantasiosa con tantas matrices de enteros como ejes haya es siempre unidimensional.
El comportamiento de la indexación de fantasía en este caso es un poco diferente de lo que algunos usuarios podrían haber esperado (yo incluido), que es la región rectangular formada al seleccionar un subconjunto de filas y columnas de la matriz. He aquí una forma de conseguirlo:
In
[
128
]:
arr
[[
1
,
5
,
7
,
2
]][:,
[
0
,
3
,
1
,
2
]]
Out
[
128
]:
array
([[
4
,
7
,
5
,
6
],
[
20
,
23
,
21
,
22
],
[
28
,
31
,
29
,
30
],
[
8
,
11
,
9
,
10
]])
Ten en cuenta que la indexación extravagante, a diferencia de la segmentación, siempre copia los datos en una nueva matriz al asignar el resultado a una nueva variable. Si asignas valores con la indexación extravagante, los valores indexados se modificarán:
In
[
129
]:
arr
[[
1
,
5
,
7
,
2
],
[
0
,
3
,
1
,
2
]]
Out
[
129
]:
array
([
4
,
23
,
29
,
10
])
In
[
130
]:
arr
[[
1
,
5
,
7
,
2
],
[
0
,
3
,
1
,
2
]]
=
0
In
[
131
]:
arr
Out
[
131
]:
array
([[
0
,
1
,
2
,
3
],
[
0
,
5
,
6
,
7
],
[
8
,
9
,
0
,
11
],
[
12
,
13
,
14
,
15
],
[
16
,
17
,
18
,
19
],
[
20
,
21
,
22
,
0
],
[
24
,
25
,
26
,
27
],
[
28
,
0
,
30
,
31
]])
Transponer matrices e intercambiar ejes
La transposición es una forma especial de remodelación que, de forma similar, devuelve una vistasobre los datos subyacentes sin copiar nada. Las matrices tienen el método transpose
y el atributo especial T
:
In
[
132
]:
arr
=
np
.
arange
(
15
)
.
reshape
((
3
,
5
))
In
[
133
]:
arr
Out
[
133
]:
array
([[
0
,
1
,
2
,
3
,
4
],
[
5
,
6
,
7
,
8
,
9
],
[
10
,
11
,
12
,
13
,
14
]])
In
[
134
]:
arr
.
T
Out
[
134
]:
array
([[
0
,
5
,
10
],
[
1
,
6
,
11
],
[
2
,
7
,
12
],
[
3
,
8
,
13
],
[
4
,
9
,
14
]])
Al realizar cálculos matriciales, puedes hacer esto muy a menudo; por ejemplo, al calcular el producto interno de matrices utilizando numpy.dot
:
In
[
135
]:
arr
=
np
.
array
([[
0
,
1
,
0
],
[
1
,
2
,
-
2
],
[
6
,
3
,
2
],
[
-
1
,
0
,
-
1
],
[
1
,
0
,
1
]])
In
[
136
]:
arr
Out
[
136
]:
array
([[
0
,
1
,
0
],
[
1
,
2
,
-
2
],
[
6
,
3
,
2
],
[
-
1
,
0
,
-
1
],
[
1
,
0
,
1
]])
In
[
137
]:
np
.
dot
(
arr
.
T
,
arr
)
Out
[
137
]:
array
([[
39
,
20
,
12
],
[
20
,
14
,
2
],
[
12
,
2
,
10
]])
El operador infijo @
es otra forma de realizar la multiplicación de matrices :
In
[
138
]:
arr
.
T
@
arr
Out
[
138
]:
array
([[
39
,
20
,
12
],
[
20
,
14
,
2
],
[
12
,
2
,
10
]])
La transposición simple con .T
es un caso especial dede intercambio de ejes. ndarray tiene el método swapaxes
, que toma un par de números de eje e intercambia los ejes indicados para reordenar los datos:
In
[
139
]:
arr
Out
[
139
]:
array
([[
0
,
1
,
0
],
[
1
,
2
,
-
2
],
[
6
,
3
,
2
],
[
-
1
,
0
,
-
1
],
[
1
,
0
,
1
]])
In
[
140
]:
arr
.
swapaxes
(
0
,
1
)
Out
[
140
]:
array
([[
0
,
1
,
6
,
-
1
,
1
],
[
1
,
2
,
3
,
0
,
0
],
[
0
,
-
2
,
2
,
-
1
,
1
]])
swapaxes
devuelve de forma similar una vista de los datos sin hacer una copia.
4.2 Generación de números pseudoaleatorios
El módulo numpy.random
complementa el módulo incorporado de Python random
con funciones para generar eficazmente matrices enteras de valores muestrales a partir de muchos tipos de distribuciones de probabilidad. Por ejemplo, puedes obtener una matriz de 4 × 4 muestras de la distribución normal estándar utilizando numpy.random.standard_normal
:
In
[
141
]:
samples
=
np
.
random
.
standard_normal
(
size
=
(
4
,
4
))
In
[
142
]:
samples
Out
[
142
]:
array
([[
-
0.2047
,
0.4789
,
-
0.5194
,
-
0.5557
],
[
1.9658
,
1.3934
,
0.0929
,
0.2817
],
[
0.769
,
1.2464
,
1.0072
,
-
1.2962
],
[
0.275
,
0.2289
,
1.3529
,
0.8864
]])
En cambio, el módulo incorporado de Python random
sólo muestrea un valor cada vez. Como puedes ver en esta comparativa, numpy.random
es más de un orden de magnitud más rápido para generar muestras muy grandes:
In
[
143
]:
from
random
import
normalvariate
In
[
144
]:
N
=
1_000_000
In
[
145
]:
%
timeit
samples
=
[
normalvariate
(
0
,
1
)
for
_
in
range
(
N
)]
1.05
s
+-
14.5
ms
per
loop
(
mean
+-
std
.
dev
.
of
7
runs
,
1
loop
each
)
In
[
146
]:
%
timeit
np
.
random
.
standard_normal
(
N
)
21.8
ms
+-
212
us
per
loop
(
mean
+-
std
.
dev
.
of
7
runs
,
10
loops
each
)
Estos números aleatorios no son realmente aleatorios (más bien,pseudoaleatorios), sino que se generan mediante un generador de números aleatorios configurable que determina de forma determinista qué valores se crean. Funciones como numpy.random.standard_normal
utilizan el generador de números aleatorios por defecto del módulonumpy.random
, pero tu código puede configurarse para utilizar un generador explícito:
In
[
147
]:
rng
=
np
.
random
.
default_rng
(
seed
=
12345
)
In
[
148
]:
data
=
rng
.
standard_normal
((
2
,
3
))
El argumento seed
es el que determina el estado inicial del generador, y el estado cambia cada vez que se utiliza el objeto rng
para generar datos. El objeto generador rng
también está aislado de otro código que pueda utilizar el módulo numpy.random
:
In
[
149
]:
type
(
rng
)
Out
[
149
]:
numpy
.
random
.
_generator
.
Generator
Consulta la Tabla 4-3 para ver una lista parcial de los métodos disponibles en objetos generadores aleatorios como rng
. Utilizaré el objeto rng
que he creado anteriormente para generar datos aleatorios durante el resto del capítulo.
Método | Descripción |
---|---|
permutation | Devuelve una permutación aleatoria de una secuencia, o devuelve un rango permutado |
shuffle | Permutar aleatoriamente una secuencia en su lugar |
uniform | Extraer muestras de una distribución uniforme |
integers | Extrae números enteros aleatorios de un intervalo dado de menor a mayor |
standard_normal | Extrae muestras de una distribución normal con media 0 y desviación típica 1 |
binomial | Extraer muestras de una distribución binomial |
normal | Extrae muestras de una distribución normal (gaussiana) |
beta | Extraer muestras de una distribución beta |
chisquare | Extraer muestras de una distribución chi-cuadrado |
gamma | Extraer muestras de una distribución gamma |
uniform | Extrae muestras de una distribución uniforme [0, 1]. |
4.3 Funciones universales: Funciones rápidas de matrices por elementos
Una función universal, o ufunc, es una función que realizaoperaciones elemento a elemento sobre datos en ndarrays. Puedes pensar en ellas como envoltorios vectoriales rápidos para funciones sencillas que toman uno o varios valores escalares y producen uno o varios resultados escalares.
Muchas ufuncs son simples transformaciones de elemento a elemento, como numpy.sqrt
o numpy.exp
:
In
[
150
]:
arr
=
np
.
arange
(
10
)
In
[
151
]:
arr
Out
[
151
]:
array
([
0
,
1
,
2
,
3
,
4
,
5
,
6
,
7
,
8
,
9
])
In
[
152
]:
np
.
sqrt
(
arr
)
Out
[
152
]:
array
([
0.
,
1.
,
1.4142
,
1.7321
,
2.
,
2.2361
,
2.4495
,
2.6458
,
2.8284
,
3.
])
In
[
153
]:
np
.
exp
(
arr
)
Out
[
153
]:
array
([
1.
,
2.7183
,
7.3891
,
20.0855
,
54.5982
,
148.4132
,
403.4288
,
1096.6332
,
2980.958
,
8103.0839
])
Se denominan ufuncs unarios. Otras, como numpy.add
o numpy.maximum
, toman dos matrices (por tanto, ufuncs binarias ) y devuelven una única matriz como resultado:
In
[
154
]:
x
=
rng
.
standard_normal
(
8
)
In
[
155
]:
y
=
rng
.
standard_normal
(
8
)
In
[
156
]:
x
Out
[
156
]:
array
([
-
1.3678
,
0.6489
,
0.3611
,
-
1.9529
,
2.3474
,
0.9685
,
-
0.7594
,
0.9022
])
In
[
157
]:
y
Out
[
157
]:
array
([
-
0.467
,
-
0.0607
,
0.7888
,
-
1.2567
,
0.5759
,
1.399
,
1.3223
,
-
0.2997
])
In
[
158
]:
np
.
maximum
(
x
,
y
)
Out
[
158
]:
array
([
-
0.467
,
0.6489
,
0.7888
,
-
1.2567
,
2.3474
,
1.399
,
1.3223
,
0.9022
])
En este ejemplo, numpy.maximum
calcula el máximo por elemento de los elementos de x
y y
.
Aunque no es habitual, una ufunc puede devolver varias matrices. numpy.modf
es un ejemplo: una versión vectorizada de la incorporada en Pythonmath.modf
, devuelve las partes fraccionaria e integral de una matriz de punto flotante:
In
[
159
]:
arr
=
rng
.
standard_normal
(
7
)
*
5
In
[
160
]:
arr
Out
[
160
]:
array
([
4.5146
,
-
8.1079
,
-
0.7909
,
2.2474
,
-
6.718
,
-
0.4084
,
8.6237
])
In
[
161
]:
remainder
,
whole_part
=
np
.
modf
(
arr
)
In
[
162
]:
remainder
Out
[
162
]:
array
([
0.5146
,
-
0.1079
,
-
0.7909
,
0.2474
,
-
0.718
,
-
0.4084
,
0.6237
])
In
[
163
]:
whole_part
Out
[
163
]:
array
([
4.
,
-
8.
,
-
0.
,
2.
,
-
6.
,
-
0.
,
8.
])
Las ufuncs aceptan un argumento opcional out
que les permite asignar sus resultados a una matriz existente en lugar de crear una nueva:
In
[
164
]:
arr
Out
[
164
]:
array
([
4.5146
,
-
8.1079
,
-
0.7909
,
2.2474
,
-
6.718
,
-
0.4084
,
8.6237
])
In
[
165
]:
out
=
np
.
zeros_like
(
arr
)
In
[
166
]:
np
.
add
(
arr
,
1
)
Out
[
166
]:
array
([
5.5146
,
-
7.1079
,
0.2091
,
3.2474
,
-
5.718
,
0.5916
,
9.6237
])
In
[
167
]:
np
.
add
(
arr
,
1
,
out
=
out
)
Out
[
167
]:
array
([
5.5146
,
-
7.1079
,
0.2091
,
3.2474
,
-
5.718
,
0.5916
,
9.6237
])
In
[
168
]:
out
Out
[
168
]:
array
([
5.5146
,
-
7.1079
,
0.2091
,
3.2474
,
-
5.718
,
0.5916
,
9.6237
])
Consulta las Tablas 4-4 y 4-5 para ver un listado de algunas de las ufuncs de NumPy. Se siguen añadiendo nuevas ufuncs a NumPy, por lo que consultar la documentación online de NumPy es la mejor forma de obtener un listado completo y estar al día.
Función | Descripción |
---|---|
abs, fabs | Calcula el valor absoluto elemento a elemento para valores enteros, de coma flotante o complejos |
sqrt | Calcula la raíz cuadrada de cada elemento (equivalente a arr ** 0.5 ) |
square | Calcula el cuadrado de cada elemento (equivalente a arr ** 2 ) |
exp | Calcula el exponenteex de cada elemento |
log, log10, log2, log1p | Logaritmo natural (base e), log base 10, log base 2 y log(1 + x), respectivamente |
sign | Calcula el signo de cada elemento: 1 (positivo), 0 (cero) o -1 (negativo) |
ceil | Calcula el techo de cada elemento (es decir, el menor número entero mayor o igual que ese número) |
floor | Calcula el suelo de cada elemento (es decir, el mayor entero menor o igual que cada elemento) |
rint | Redondea los elementos al entero más próximo, conservando el dtype |
modf | Devuelve las partes fraccionaria e integral de la matriz como matrices separadas |
isnan | Devuelve una matriz booleana que indica si cada valor es NaN (No es un número) |
isfinite, isinf | Devuelve una matriz booleana que indica si cada elemento es finito (noinf , noNaN ) o infinito, respectivamente |
cos, cosh, sin, sinh, tan, tanh | Funciones trigonométricas regulares e hiperbólicas |
arccos, arccosh, arcsin, arcsinh, arctan, arctanh | Funciones trigonométricas inversas |
logical_not | Calcula el valor de verdad de not x elemento a elemento (equivalente a ~arr ) |
Función | Descripción |
---|---|
add | Añadir los elementos correspondientes en matrices |
subtract | Resta los elementos de la segunda matriz de la primera matriz |
multiply | Multiplicar elementos de la matriz |
divide, floor_divide | Dividir o dividir por el suelo (truncando el resto) |
power | Eleva los elementos de la primera matriz a las potencias indicadas en la segunda matriz |
maximum, fmax | Máximo por elemento; fmax ignora NaN |
minimum, fmin | Mínimo por elemento; fmin ignora NaN |
mod | Módulo elemental (resto de la división) |
copysign | Copia el signo de los valores del segundo argumento a los valores del primer argumento |
greater, greater_equal, less, less_equal, equal, not_equal | Realiza una comparación elemento a elemento, obteniendo una matriz booleana (equivalente a los operadores infijos >, >=, <, <=, ==, != ) |
logical_and | Calcula el valor verdadero por elementos de la operación lógica AND (& ) |
logical_or | Calcula el valor verdadero por elementos de la operación lógica OR (| ) |
logical_xor | Calcula el valor verdadero de la operación lógica XOR (^ ) en función de los elementos |
4.4 Programación orientada a matrices con matrices
Utilizar matrices NumPy te permite expresar muchos tipos de tareas de procesamiento de datos como expresiones concisas de matrices que, de otro modo, requerirían escribir bucles. Algunas personas denominan vectorización a esta práctica de sustituir los bucles explícitos por expresiones de matrices. En general, las operaciones con matrices vectorizadas suelen ser mucho más rápidas que sus equivalentes en Python puro, con un mayor impacto en cualquier tipo de cálculo numérico. Más adelante, en el Apéndice A, explico la difusión, un potente método para vectorizar los cálculos.
Como ejemplo sencillo, supongamos que deseamos evaluar la función sqrt(x^2 + y^2)
en una rejilla regular de valores. La función numpy.meshgrid
toma dos matrices unidimensionales y produce dos matrices bidimensionales correspondientes a todos los pares de (x, y)
en las dos matrices:
In
[
169
]:
points
=
np
.
arange
(
-
5
,
5
,
0.01
)
# 100 equally spaced points
In
[
170
]:
xs
,
ys
=
np
.
meshgrid
(
points
,
points
)
In
[
171
]:
ys
Out
[
171
]:
array
([[
-
5.
,
-
5.
,
-
5.
,
...
,
-
5.
,
-
5.
,
-
5.
],
[
-
4.99
,
-
4.99
,
-
4.99
,
...
,
-
4.99
,
-
4.99
,
-
4.99
],
[
-
4.98
,
-
4.98
,
-
4.98
,
...
,
-
4.98
,
-
4.98
,
-
4.98
],
...
,
[
4.97
,
4.97
,
4.97
,
...
,
4.97
,
4.97
,
4.97
],
[
4.98
,
4.98
,
4.98
,
...
,
4.98
,
4.98
,
4.98
],
[
4.99
,
4.99
,
4.99
,
...
,
4.99
,
4.99
,
4.99
]])
Ahora, evaluar la función es cuestión de escribir la misma expresión que escribirías con dos puntos:
In
[
172
]:
z
=
np
.
sqrt
(
xs
**
2
+
ys
**
2
)
In
[
173
]:
z
Out
[
173
]:
array
([[
7.0711
,
7.064
,
7.0569
,
...
,
7.0499
,
7.0569
,
7.064
],
[
7.064
,
7.0569
,
7.0499
,
...
,
7.0428
,
7.0499
,
7.0569
],
[
7.0569
,
7.0499
,
7.0428
,
...
,
7.0357
,
7.0428
,
7.0499
],
...
,
[
7.0499
,
7.0428
,
7.0357
,
...
,
7.0286
,
7.0357
,
7.0428
],
[
7.0569
,
7.0499
,
7.0428
,
...
,
7.0357
,
7.0428
,
7.0499
],
[
7.064
,
7.0569
,
7.0499
,
...
,
7.0428
,
7.0499
,
7.0569
]])
Como avance del Capítulo 9, utilizo matplotlib para crear visualizaciones de esta matriz bidimensional:
In
[
174
]:
import
matplotlib.pyplot
as
plt
In
[
175
]:
plt
.
imshow
(
z
,
cmap
=
plt
.
cm
.
gray
,
extent
=
[
-
5
,
5
,
-
5
,
5
])
Out
[
175
]:
<
matplotlib
.
image
.
AxesImage
at
0x7f7132db3ac0
>
In
[
176
]:
plt
.
colorbar
()
Out
[
176
]:
<
matplotlib
.
colorbar
.
Colorbar
at
0x7f713a5833a0
>
In
[
177
]:
plt
.
title
(
"Image plot of $\sqrt{x^2 + y^2}$ for a grid of values"
)
Out
[
177
]:
Text
(
0.5
,
1.0
,
'Image plot of $
\\
sqrt{x^2 + y^2}$ for a grid of values'
)
En la Figura 4-3, he utilizado la función matplotlib imshow
para crear un gráfico de imagen a partir de una matriz bidimensional de valores de función.
Si trabajas en IPython, puedes cerrar todas las ventanas de trama abiertas ejecutando plt.close("all")
:
In
[
179
]:
plt
.
close
(
"all"
)
Nota
El término vectorización se utiliza para describir algunos otros conceptos de informática, pero en este libro lo utilizo para describir operaciones sobre matrices enteras de datos a la vez, en lugar de ir valor a valor utilizando un bucle de Python for
.
Expresar la lógica condicional como operaciones de matriz
La función numpy.where
es una versión vectorizada de la expresión ternaria x if condition else y
. Supongamos que tenemos una matriz booleana y dos matrices de valores:
In
[
180
]:
xarr
=
np
.
array
([
1.1
,
1.2
,
1.3
,
1.4
,
1.5
])
In
[
181
]:
yarr
=
np
.
array
([
2.1
,
2.2
,
2.3
,
2.4
,
2.5
])
In
[
182
]:
cond
=
np
.
array
([
True
,
False
,
True
,
True
,
False
])
Supongamos que queremos tomar un valor de xarr
siempre que el valor correspondiente en cond
sea True
, y en caso contrario tomar el valor de yarr
. Una comprensión de lista para hacer esto podría tener el siguiente aspecto:
In
[
183
]:
result
=
[(
x
if
c
else
y
)
.....
:
for
x
,
y
,
c
in
zip
(
xarr
,
yarr
,
cond
)]
In
[
184
]:
result
Out
[
184
]:
[
1.1
,
2.2
,
1.3
,
1.4
,
2.5
]
Esto tiene múltiples problemas. En primer lugar, no será muy rápido con matrices grandes (porque todo el trabajo se realiza en código Python interpretado). En segundo lugar, no funcionará con matrices multidimensionales. Con numpy.where
puedes hacerlo con una sola llamada a una función:
In
[
185
]:
result
=
np
.
where
(
cond
,
xarr
,
yarr
)
In
[
186
]:
result
Out
[
186
]:
array
([
1.1
,
2.2
,
1.3
,
1.4
,
2.5
])
El segundo y tercer argumento de numpy.where
no tienen por qué ser matrices; uno o ambos pueden ser escalares. Un uso típico de where
en el análisis de datos es producir una nueva matriz de valores basada en otra matriz. Supón que tienes una matriz de datos generados aleatoriamente y quieres sustituir todos los valores positivos por 2 y todos los negativos por -2. Esto es posible hacerlo con numpy.where
:
In
[
187
]:
arr
=
rng
.
standard_normal
((
4
,
4
))
In
[
188
]:
arr
Out
[
188
]:
array
([[
2.6182
,
0.7774
,
0.8286
,
-
0.959
],
[
-
1.2094
,
-
1.4123
,
0.5415
,
0.7519
],
[
-
0.6588
,
-
1.2287
,
0.2576
,
0.3129
],
[
-
0.1308
,
1.27
,
-
0.093
,
-
0.0662
]])
In
[
189
]:
arr
>
0
Out
[
189
]:
array
([[
True
,
True
,
True
,
False
],
[
False
,
False
,
True
,
True
],
[
False
,
False
,
True
,
True
],
[
False
,
True
,
False
,
False
]])
In
[
190
]:
np
.
where
(
arr
>
0
,
2
,
-
2
)
Out
[
190
]:
array
([[
2
,
2
,
2
,
-
2
],
[
-
2
,
-
2
,
2
,
2
],
[
-
2
,
-
2
,
2
,
2
],
[
-
2
,
2
,
-
2
,
-
2
]])
Puedes combinar escalares y matrices cuando utilices numpy.where
. Por ejemplo, puedo sustituir todos los valores positivos de arr
por la constante 2, así:
In
[
191
]:
np
.
where
(
arr
>
0
,
2
,
arr
)
# set only positive values to 2
Out
[
191
]:
array
([[
2.
,
2.
,
2.
,
-
0.959
],
[
-
1.2094
,
-
1.4123
,
2.
,
2.
],
[
-
0.6588
,
-
1.2287
,
2.
,
2.
],
[
-
0.1308
,
2.
,
-
0.093
,
-
0.0662
]])
Métodos matemáticos y estadísticos
Un conjunto de funciones matemáticas que calculan estadísticas sobre toda una matriz o sobre los datos a lo largo de un eje son accesibles como métodos de la clase matriz. Puedes utilizar agregaciones (a veces llamadas reducciones) como sum
, mean
, y std
(desviación estándar) llamando al método de instancia del array o utilizando la función NumPy de nivel superior. Cuando utilices la función NumPy, como numpy.sum
, tienes que pasar el array que quieres agregar como primer argumento.
Aquí genero algunos datos aleatorios distribuidos normalmente y calculo algunas estadísticas agregadas:
In
[
192
]:
arr
=
rng
.
standard_normal
((
5
,
4
))
In
[
193
]:
arr
Out
[
193
]:
array
([[
-
1.1082
,
0.136
,
1.3471
,
0.0611
],
[
0.0709
,
0.4337
,
0.2775
,
0.5303
],
[
0.5367
,
0.6184
,
-
0.795
,
0.3
],
[
-
1.6027
,
0.2668
,
-
1.2616
,
-
0.0713
],
[
0.474
,
-
0.4149
,
0.0977
,
-
1.6404
]])
In
[
194
]:
arr
.
mean
()
Out
[
194
]:
-
0.08719744457434529
In
[
195
]:
np
.
mean
(
arr
)
Out
[
195
]:
-
0.08719744457434529
In
[
196
]:
arr
.
sum
()
Out
[
196
]:
-
1.743948891486906
Las funciones como mean
y sum
toman un argumento opcional axis
que calcula la estadística sobre el eje dado, lo que da como resultado una matriz con una dimensión menos:
In
[
197
]:
arr
.
mean
(
axis
=
1
)
Out
[
197
]:
array
([
0.109
,
0.3281
,
0.165
,
-
0.6672
,
-
0.3709
])
In
[
198
]:
arr
.
sum
(
axis
=
0
)
Out
[
198
]:
array
([
-
1.6292
,
1.0399
,
-
0.3344
,
-
0.8203
])
Aquí, arr.mean(axis=1)
significa "calcular la media de las columnas", mientras que arr.sum(axis=0)
significa "calcular la suma de las filas".
Otros métodos como cumsum
y cumprod
no agregan, sino que producen una matriz con los resultados intermedios:
In
[
199
]:
arr
=
np
.
array
([
0
,
1
,
2
,
3
,
4
,
5
,
6
,
7
])
In
[
200
]:
arr
.
cumsum
()
Out
[
200
]:
array
([
0
,
1
,
3
,
6
,
10
,
15
,
21
,
28
])
En matrices multidimensionales, las funciones de acumulación como cumsum
devuelven una matriz del mismo tamaño pero con los agregados parciales calculados a lo largo del eje indicado según cada corte dimensional inferior:
In
[
201
]:
arr
=
np
.
array
([[
0
,
1
,
2
],
[
3
,
4
,
5
],
[
6
,
7
,
8
]])
In
[
202
]:
arr
Out
[
202
]:
array
([[
0
,
1
,
2
],
[
3
,
4
,
5
],
[
6
,
7
,
8
]])
La expresión arr.cumsum(axis=0)
calcula la suma acumulada a lo largo de las filas, mientras que arr.cumsum(axis=1)
calcula las sumas a lo largo de las columnas:
In
[
203
]:
arr
.
cumsum
(
axis
=
0
)
Out
[
203
]:
array
([[
0
,
1
,
2
],
[
3
,
5
,
7
],
[
9
,
12
,
15
]])
In
[
204
]:
arr
.
cumsum
(
axis
=
1
)
Out
[
204
]:
array
([[
0
,
1
,
3
],
[
3
,
7
,
12
],
[
6
,
13
,
21
]])
Consulta la Tabla 4-6 para ver una lista completa. Veremos muchos ejemplos de estos métodos en acción en capítulos posteriores.
Método | Descripción |
---|---|
sum | Suma de todos los elementos de la matriz o a lo largo de un eje; las matrices de longitud cero tienen suma 0 |
mean | Media aritmética; no válida (devuelve NaN ) en matrices de longitud cero |
std, var | Desviación típica y varianza, respectivamente |
min, max | Mínimo y máximo |
argmin, argmax | Índices de los elementos mínimo y máximo, respectivamente |
cumsum | Suma acumulada de elementos a partir de 0 |
cumprod | Producto acumulativo de elementos empezando por 1 |
Métodos para matrices booleanas
Los valores booleanos se coercionan a 1 (True
) y 0 (False
) en los métodos anteriores. Así, sum
se utiliza a menudo como medio para contar valores True
en una matriz booleana:
In
[
205
]:
arr
=
rng
.
standard_normal
(
100
)
In
[
206
]:
(
arr
>
0
)
.
sum
()
# Number of positive values
Out
[
206
]:
48
In
[
207
]:
(
arr
<=
0
)
.
sum
()
# Number of non-positive values
Out
[
207
]:
52
Los paréntesis aquí en la expresión (arr > 0).sum()
son necesarios para poder llamar a sum()
sobre el resultado temporal de arr > 0
.
Dos métodos adicionales, any
yall
, son útiles especialmente para matrices booleanas. any
comprueba si uno o más valores de una matriz son True
, mientras que all
comprueba si cada valor esTrue
:
In
[
208
]:
bools
=
np
.
array
([
False
,
False
,
True
,
False
])
In
[
209
]:
bools
.
any
()
Out
[
209
]:
True
In
[
210
]:
bools
.
all
()
Out
[
210
]:
False
Estos métodos también funcionan con matrices no booleanas, en las que los elementos distintos de cero se tratan como True
.
Clasificación
Al igual que el tipo de lista incorporado en Python, las matrices de NumPy se pueden ordenar en su lugar con el método sort
:
In
[
211
]:
arr
=
rng
.
standard_normal
(
6
)
In
[
212
]:
arr
Out
[
212
]:
array
([
0.0773
,
-
0.6839
,
-
0.7208
,
1.1206
,
-
0.0548
,
-
0.0824
])
In
[
213
]:
arr
.
sort
()
In
[
214
]:
arr
Out
[
214
]:
array
([
-
0.7208
,
-
0.6839
,
-
0.0824
,
-
0.0548
,
0.0773
,
1.1206
])
Puedes ordenar cada sección unidimensional de valores de una matriz multidimensional en su lugar a lo largo de un eje pasando el número de eje a sort
. En los datos de este ejemplo:
In
[
215
]:
arr
=
rng
.
standard_normal
((
5
,
3
))
In
[
216
]:
arr
Out
[
216
]:
array
([[
0.936
,
1.2385
,
1.2728
],
[
0.4059
,
-
0.0503
,
0.2893
],
[
0.1793
,
1.3975
,
0.292
],
[
0.6384
,
-
0.0279
,
1.3711
],
[
-
2.0528
,
0.3805
,
0.7554
]])
arr.sort(axis=0)
ordena los valores dentro de cada columna, mientras que arr.sort(axis=1)
ordena a través de cada fila:
In
[
217
]:
arr
.
sort
(
axis
=
0
)
In
[
218
]:
arr
Out
[
218
]:
array
([[
-
2.0528
,
-
0.0503
,
0.2893
],
[
0.1793
,
-
0.0279
,
0.292
],
[
0.4059
,
0.3805
,
0.7554
],
[
0.6384
,
1.2385
,
1.2728
],
[
0.936
,
1.3975
,
1.3711
]])
In
[
219
]:
arr
.
sort
(
axis
=
1
)
In
[
220
]:
arr
Out
[
220
]:
array
([[
-
2.0528
,
-
0.0503
,
0.2893
],
[
-
0.0279
,
0.1793
,
0.292
],
[
0.3805
,
0.4059
,
0.7554
],
[
0.6384
,
1.2385
,
1.2728
],
[
0.936
,
1.3711
,
1.3975
]])
El método de nivel superior numpy.sort
devuelve una copia ordenada de una matriz (como la función incorporada de Python sorted
) en lugar de modificar la matriz in situ. Por ejemplo:
In
[
221
]:
arr2
=
np
.
array
([
5
,
-
10
,
7
,
1
,
0
,
-
3
])
In
[
222
]:
sorted_arr2
=
np
.
sort
(
arr2
)
In
[
223
]:
sorted_arr2
Out
[
223
]:
array
([
-
10
,
-
3
,
0
,
1
,
5
,
7
])
Para más detalles sobre el uso de los métodos de ordenación de NumPy, y técnicas más avanzadas como la ordenación indirecta, consulta el Apéndice A. También se pueden encontrar en pandas otros tipos de manipulaciones de datos relacionadas con la ordenación (por ejemplo, ordenar una tabla de datos por una o más columnas).
Lógica de conjuntos únicos y otros
NumPy dispone de algunas operaciones de conjunto básicas para ndarrays unidimensionales. Una muy utilizada en es numpy.unique
, que devuelve los valores únicos ordenados de una matriz:
In
[
224
]:
names
=
np
.
array
([
"Bob"
,
"Will"
,
"Joe"
,
"Bob"
,
"Will"
,
"Joe"
,
"Joe"
])
In
[
225
]:
np
.
unique
(
names
)
Out
[
225
]:
array
([
'Bob'
,
'Joe'
,
'Will'
],
dtype
=
'<U4'
)
In
[
226
]:
ints
=
np
.
array
([
3
,
3
,
3
,
2
,
2
,
1
,
1
,
4
,
4
])
In
[
227
]:
np
.
unique
(
ints
)
Out
[
227
]:
array
([
1
,
2
,
3
,
4
])
Contrasta numpy.unique
con la alternativa de Python puro:
In
[
228
]:
sorted
(
set
(
names
))
Out
[
228
]:
[
'Bob'
,
'Joe'
,
'Will'
]
En muchos casos, la versión NumPy es más rápida y devuelve una matriz NumPy en lugar de una lista Python.
Otra función, numpy.in1d
, comprueba la pertenencia de los valores de una matriz a otra, devolviendo una matriz booleana:
In
[
229
]:
values
=
np
.
array
([
6
,
0
,
0
,
3
,
2
,
5
,
6
])
In
[
230
]:
np
.
in1d
(
values
,
[
2
,
3
,
6
])
Out
[
230
]:
array
([
True
,
False
,
False
,
True
,
True
,
False
,
True
])
Consulta la Tabla 4-7 para ver un listado de las operaciones de conjunto de matrices en NumPy.
Método | Descripción |
---|---|
unique(x) | Calcula los elementos ordenados y únicos de x |
intersect1d(x, y) | Calcula los elementos comunes ordenados en x y y |
union1d(x, y) | Calcula la unión ordenada de los elementos |
in1d(x, y) | Calcula una matriz booleana que indique si cada elemento de x está contenido en y |
setdiff1d(x, y) | Diferencia de conjunto, elementos en x que no están en y |
setxor1d(x, y) | Establece diferencias simétricas; elementos que están en cualquiera de las matrices, pero no en ambas |
4.5 Entrada y salida de archivos con matrices
NumPy puede guardar y cargar datos en y desde el disco en algunos formatos binarios de texto o . En esta sección sólo hablo del formato binario incorporado de NumPy, ya que la mayoría de los usuarios preferirán pandas y otras herramientas para cargar datos de texto o tabulares (para más información, consulta el Capítulo 6 ).
numpy.save
y numpy.load
son las dos funciones más eficaces para guardar y cargar datos de matrices en el disco. Las matrices se guardan por defecto en un formato binario sin comprimir con la extensión de archivo .npy:
In
[
231
]:
arr
=
np
.
arange
(
10
)
In
[
232
]:
np
.
save
(
"some_array"
,
arr
)
Si la ruta del archivo no termina ya en .npy, se añadirá la extensión. La matriz en disco se puede cargar entonces con numpy.load
:
In
[
233
]:
np
.
load
(
"some_array.npy"
)
Out
[
233
]:
array
([
0
,
1
,
2
,
3
,
4
,
5
,
6
,
7
,
8
,
9
])
Puedes guardar varias matrices en un archivo descomprimido utilizandonumpy.savez
y pasando las matrices como argumentos de palabra clave:
In
[
234
]:
np
.
savez
(
"array_archive.npz"
,
a
=
arr
,
b
=
arr
)
Al cargar un archivo .npz, obtienes de vuelta un objeto similar a un diccionario que carga las matrices individuales perezosamente:
In
[
235
]:
arch
=
np
.
load
(
"array_archive.npz"
)
In
[
236
]:
arch
[
"b"
]
Out
[
236
]:
array
([
0
,
1
,
2
,
3
,
4
,
5
,
6
,
7
,
8
,
9
])
Si tus datos se comprimen bien, puedes utilizar numpy.savez_compressed
en su lugar:
In
[
237
]:
np
.
savez_compressed
(
"arrays_compressed.npz"
,
a
=
arr
,
b
=
arr
)
4.6 Álgebra lineal
Las operaciones de álgebra lineal, como la multiplicación de matrices , las descomposiciones, los determinantes y otras matemáticas de matrices cuadradas, son una parte importante de muchas bibliotecas de matrices. Multiplicar dos matrices bidimensionales con *
es un producto elemento a elemento, mientras que las multiplicaciones matriciales requieren utilizar una función. Por tanto, existe una función dot
, tanto un método de matriz como una función en el espacio de nombres numpy
, para la multiplicación de matrices:
In
[
241
]:
x
=
np
.
array
([[
1.
,
2.
,
3.
],
[
4.
,
5.
,
6.
]])
In
[
242
]:
y
=
np
.
array
([[
6.
,
23.
],
[
-
1
,
7
],
[
8
,
9
]])
In
[
243
]:
x
Out
[
243
]:
array
([[
1.
,
2.
,
3.
],
[
4.
,
5.
,
6.
]])
In
[
244
]:
y
Out
[
244
]:
array
([[
6.
,
23.
],
[
-
1.
,
7.
],
[
8.
,
9.
]])
In
[
245
]:
x
.
dot
(
y
)
Out
[
245
]:
array
([[
28.
,
64.
],
[
67.
,
181.
]])
x.dot(y)
es equivalente a np.dot(x,
y)
:
In
[
246
]:
np
.
dot
(
x
,
y
)
Out
[
246
]:
array
([[
28.
,
64.
],
[
67.
,
181.
]])
Un producto matricial entre una matriz bidimensional y una matriz unidimensional del tamaño adecuado da como resultado una matriz unidimensional:
In
[
247
]:
x
@
np
.
ones
(
3
)
Out
[
247
]:
array
([
6.
,
15.
])
numpy.linalg
tiene un conjunto estándar de descomposiciones de matrices y cosas como la inversa y el determinante:
In
[
248
]:
from
numpy.linalg
import
inv
,
qr
In
[
249
]:
X
=
rng
.
standard_normal
((
5
,
5
))
In
[
250
]:
mat
=
X
.
T
@
X
In
[
251
]:
inv
(
mat
)
Out
[
251
]:
array
([[
3.4993
,
2.8444
,
3.5956
,
-
16.5538
,
4.4733
],
[
2.8444
,
2.5667
,
2.9002
,
-
13.5774
,
3.7678
],
[
3.5956
,
2.9002
,
4.4823
,
-
18.3453
,
4.7066
],
[
-
16.5538
,
-
13.5774
,
-
18.3453
,
84.0102
,
-
22.0484
],
[
4.4733
,
3.7678
,
4.7066
,
-
22.0484
,
6.0525
]])
In
[
252
]:
mat
@
inv
(
mat
)
Out
[
252
]:
array
([[
1.
,
0.
,
-
0.
,
0.
,
-
0.
],
[
0.
,
1.
,
0.
,
0.
,
-
0.
],
[
0.
,
-
0.
,
1.
,
-
0.
,
-
0.
],
[
0.
,
-
0.
,
0.
,
1.
,
-
0.
],
[
0.
,
-
0.
,
0.
,
-
0.
,
1.
]])
La expresión X.T.dot(X)
calcula el producto punto de X
con su transpuesto X.T
.
Consulta la Tabla 4-8 para ver una lista de algunas de las funciones de álgebra lineal más utilizadas.
Función | Descripción |
---|---|
diag | Devuelve los elementos diagonales (u off-diagonales) de una matriz cuadrada como una matriz 1D, o convierte una matriz 1D en una matriz cuadrada con ceros en la off-diagonal |
dot | Multiplicación de matrices |
trace | Calcula la suma de los elementos diagonales |
det | Calcula el determinante de la matriz |
eig | Calcula los valores y vectores propios de una matriz cuadrada |
inv | Calcula la inversa de una matriz cuadrada |
pinv | Calcula el pseudoinverso de Moore-Penrose de una matriz |
qr | Calcula la descomposición QR |
svd | Calcula la descomposición en valores singulares (SVD) |
solve | Resuelve el sistema lineal Ax = b para x, donde A es una matriz cuadrada |
lstsq | Calcula la solución por mínimos cuadrados de Ax = b |
4.7 Ejemplo: Paseos aleatorios
La simulación de los recorridos aleatorios proporciona una aplicación ilustrativa de la utilización de las operaciones con matrices. Consideremos primero un recorrido aleatorio simple que comienza en 0 con pasos de 1 y -1 que se producen con igual probabilidad.
Aquí tienes una forma pura en Python de implementar un único paseo aleatorio con 1.000 pasos utilizando el módulo incorporado random
:
#! blockstart
import
random
position
=
0
walk
=
[
position
]
nsteps
=
1000
for
_
in
range
(
nsteps
):
step
=
1
if
random
.
randint
(
0
,
1
)
else
-
1
position
+=
step
walk
.
append
(
position
)
#! blockend
Consulta la Figura 4-4 para ver un ejemplo de gráfico de los 100 primeros valores de uno de estos paseos aleatorios:
In
[
255
]:
plt
.
plot
(
walk
[:
100
])
Podrías hacer la observación de que walk
es la suma acumulada de los pasos aleatorios y podría evaluarse como una expresión de matriz. Así, utilizo el módulo numpy.random
para extraer 1.000 lanzamientos de moneda a la vez, ponerlos a 1 y -1, y calcular la suma acumulativa:
In
[
256
]:
nsteps
=
1000
In
[
257
]:
rng
=
np
.
random
.
default_rng
(
seed
=
12345
)
# fresh random generator
In
[
258
]:
draws
=
rng
.
integers
(
0
,
2
,
size
=
nsteps
)
In
[
259
]:
steps
=
np
.
where
(
draws
==
0
,
1
,
-
1
)
In
[
260
]:
walk
=
steps
.
cumsum
()
A partir de ahí podemos empezar a extraer estadísticas como el valor mínimo y máximo a lo largo de la trayectoria del paseo:
In
[
261
]:
walk
.
min
()
Out
[
261
]:
-
8
In
[
262
]:
walk
.
max
()
Out
[
262
]:
50
Un estadístico más complicado es el tiempo del primer cruce, el paso en el que el paseo aleatorio alcanza un valor determinado. Aquí podríamos querer saber cuánto tardó el paseo aleatorio en alejarse al menos 10 pasos del origen 0 en cualquier dirección. np.abs(walk) >=
10
nos da una matriz booleana que indica dónde ha alcanzado o superado 10 el paseo, pero queremos el índice del primer 10 o -10. Resulta que podemos calcularlo utilizando argmax
, que devuelve el primer índice del valor máximo de la matriz booleana (True
es el valor máximo):
In
[
263
]:
(
np
.
abs
(
walk
)
>=
10
)
.
argmax
()
Out
[
263
]:
155
Ten en cuenta que utilizar argmax
aquí no siempre es eficiente, porque siempre hace un barrido completo de la matriz. En este caso especial, una vez que se observa un True
sabemos que es el valor máximo.
Simular muchos paseos aleatorios a la vez
Si tu objetivo fuera simular muchos paseos aleatorios, digamos cinco mil de ellos, puedes generar todos los paseos aleatorios con pequeñas modificaciones del código anterior. Si se les pasa una 2-tupla, las funciones numpy.random
generarán una matriz bidimensional de extracciones, y podemos calcular la suma acumulada de cada fila para calcular los cinco mil paseos aleatorios de una sola vez:
In
[
264
]:
nwalks
=
5000
In
[
265
]:
nsteps
=
1000
In
[
266
]:
draws
=
rng
.
integers
(
0
,
2
,
size
=
(
nwalks
,
nsteps
))
# 0 or 1
In
[
267
]:
steps
=
np
.
where
(
draws
>
0
,
1
,
-
1
)
In
[
268
]:
walks
=
steps
.
cumsum
(
axis
=
1
)
In
[
269
]:
walks
Out
[
269
]:
array
([[
1
,
2
,
3
,
...
,
22
,
23
,
22
],
[
1
,
0
,
-
1
,
...
,
-
50
,
-
49
,
-
48
],
[
1
,
2
,
3
,
...
,
50
,
49
,
48
],
...
,
[
-
1
,
-
2
,
-
1
,
...
,
-
10
,
-
9
,
-
10
],
[
-
1
,
-
2
,
-
3
,
...
,
8
,
9
,
8
],
[
-
1
,
0
,
1
,
...
,
-
4
,
-
3
,
-
2
]])
Ahora podemos calcular los valores máximo y mínimo obtenidos en todos los recorridos:
In
[
270
]:
walks
.
max
()
Out
[
270
]:
114
In
[
271
]:
walks
.
min
()
Out
[
271
]:
-
120
De estos paseos, calculemos el tiempo mínimo de paso a 30 o -30. Esto es un poco complicado porque no todos los 5.000 llegan a 30. Podemos comprobarlo utilizando el método any
:
In
[
272
]:
hits30
=
(
np
.
abs
(
walks
)
>=
30
)
.
any
(
axis
=
1
)
In
[
273
]:
hits30
Out
[
273
]:
array
([
False
,
True
,
True
,
...
,
True
,
False
,
True
])
In
[
274
]:
hits30
.
sum
()
# Number that hit 30 or -30
Out
[
274
]:
3395
Podemos utilizar esta matriz booleana para seleccionar las filas de walks
que realmente cruzan el nivel 30 absoluto, y llamar a argmax
a través del eje 1 para obtener los tiempos de cruce:
In
[
275
]:
crossing_times
=
(
np
.
abs
(
walks
[
hits30
])
>=
30
)
.
argmax
(
axis
=
1
)
In
[
276
]:
crossing_times
Out
[
276
]:
array
([
201
,
491
,
283
,
...
,
219
,
259
,
541
])
Por último, calculamos el tiempo medio de paso mínimo:
In
[
277
]:
crossing_times
.
mean
()
Out
[
277
]:
500.5699558173785
Siéntete libre de experimentar con otras distribuciones para los pasos que no sean lanzamientos de monedas de igual tamaño. Sólo tienes que utilizar un método generador aleatorio diferente, como standard_normal
para generar pasos distribuidos normalmente con cierta media y desviación típica:
In
[
278
]:
draws
=
0.25
*
rng
.
standard_normal
((
nwalks
,
nsteps
))
4.8 Conclusión
Aunque gran parte del resto del libro se centrará en desarrollar habilidades de manejo de datos con pandas, seguiremos trabajando con un estilo similar basado en matrices. En el Apéndice A, profundizaremos en las funciones de NumPy para ayudarte a desarrollar aún más tus habilidades de cálculo de matrices.
Get Python para el Análisis de Datos, 3ª Edición now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.