Capítulo 4. Cálculo numérico con NumPy

Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com

Los ordenadores son inútiles. Sólo pueden dar respuestas.

Pablo Picasso

Aunque el propio intérprete de Python ya trae consigo una rica variedad de estructuras de datos, NumPy y otras bibliotecas las complementan de forma valiosa. Este capítulo de se centra en NumPy, que proporciona un objeto array multidimensional para almacenar arrays de datos homogéneos o heterogéneos y admite la vectorización del código.

El capítulo cubre las siguientes estructuras de datos:

Tipo de objeto Significado Se utiliza para

ndarray (regular)

objeto matrizn-dimensional

Grandes matrices de datos numéricos

ndarray (registro)

Objeto matriz bidimensional

Datos tabulares organizados en columnas

Este capítulo está organizado como sigue:

"Matrices de datos"

Esta sección trata sobre el manejo de matrices de datos con código Python puro.

"Matrices NumPy regulares"

Esta es la sección principal sobre la clase NumPy ndarray regular, el caballo de batalla en casi todos los casos de uso intensivo de datos en Python que implican datos numéricos.

"Matrices NumPy estructuradas"

Esta breve sección presenta los objetos estructurados (o de registro) ndarray para el manejo de datos tabulares con columnas.

"Vectorización del código"

En esta sección se trata la vectorización del código y sus ventajas; también se analiza la importancia de la disposición de la memoria en determinados escenarios.

Matrices de datos

El capítulo anterior mostró que Python proporciona algunas estructuras de datos generales bastante útiles y flexibles. En particular, los objetos list pueden considerarse un verdadero caballo de batalla con muchas características y áreas de aplicación convenientes. Utilizar una estructura de datos tan flexible (mutable) tiene un coste, en forma de un uso de memoria relativamente alto, un rendimiento más lento, o ambas cosas. Sin embargo, las aplicaciones científicas y financieras suelen necesitar operaciones de alto rendimiento con estructuras de datos especiales. Una de las estructuras de datos más importantes en este sentido es la matriz. Las matrices suelen estructurar otros objetos (fundamentales) del mismo tipo de datos en filas y columnas.

Supongamos por el momento que sólo son relevantes los números, aunque el concepto se generaliza también a otros tipos de datos. En el caso más sencillo, una matriz unidimensional representa entonces, matemáticamente hablando, un vector de, en general, números reales, representados internamente por objetos float. Consta entonces de una sola fila o columna de elementos únicamente. En el caso más común, una matriz representa una matriz i × j de elementos. Este concepto se generaliza a cubos de elementos i × j × k en tres dimensiones, así como a matrices generales de n dimensiones de forma i × j × k × l × ... .

Las disciplinas matemáticas como el álgebra lineal y la teoría de los espacios vectoriales ilustran que dichas estructuras matemáticas son de gran importancia en varias disciplinas y campos científicos. Por tanto, puede resultar fructífero disponer de una clase especializada de estructuras de datos diseñadas explícitamente para manejar matrices de forma cómoda y eficiente. Aquí es donde entra en juego la biblioteca de Python NumPy, con su potente clase ndarray. Antes de presentar esta clase en el siguiente apartado, esta sección ilustra dos alternativas para el manejo de matrices.

Matrices con listas en Python

Las matrices pueden construirse con las estructuras de datos incorporadas presentadas en el capítulo anterior. Los objetos list son especialmente adecuados para realizar esta tarea. Un simple list ya puede considerarse una matriz unidimensional:

In [1]: v = [0.5, 0.75, 1.0, 1.5, 2.0]  1
1

list con números.

Puesto que los objetos list pueden contener otros objetos arbitrarios, también pueden contener otros objetos list. De este modo, las matrices bidimensionales y de mayor dimensión se construyen fácilmente anidando objetos list:

In [2]: m = [v, v, v]  1
        m  2
Out[2]: [[0.5, 0.75, 1.0, 1.5, 2.0],
         [0.5, 0.75, 1.0, 1.5, 2.0],
         [0.5, 0.75, 1.0, 1.5, 2.0]]
1

list objeto con list objetos ...

2

... dando como resultado una matriz de números.

También se pueden seleccionar fácilmente filas mediante indexación simple o elementos individuales mediante indexación doble (en cambio, las columnas enteras no son tan fáciles de seleccionar):

In [3]: m[1]
Out[3]: [0.5, 0.75, 1.0, 1.5, 2.0]

In [4]: m[1][0]
Out[4]: 0.5

La anidación puede ampliarse aún más para obtener estructuras más generales:

In [5]: v1 = [0.5, 1.5]
        v2 = [1, 2]
        m = [v1, v2]
        c = [m, m]  1
        c
Out[5]: [[[0.5, 1.5], [1, 2]], [[0.5, 1.5], [1, 2]]]

In [6]: c[1][1][0]
Out[6]: 1
1

Cubo de números.

Ten en cuenta que la combinación de objetos de la forma que acabamos de presentar suele funcionar con punteros de referencia a los objetos originales. ¿Qué significa esto en la práctica? Echa un vistazo a las siguientes operaciones:

In [7]: v = [0.5, 0.75, 1.0, 1.5, 2.0]
        m = [v, v, v]
        m
Out[7]: [[0.5, 0.75, 1.0, 1.5, 2.0],
         [0.5, 0.75, 1.0, 1.5, 2.0],
         [0.5, 0.75, 1.0, 1.5, 2.0]]

Cambia ahora el valor del primer elemento del objeto v y observa qué ocurre con el objeto m:

In [8]: v[0] = 'Python'
        m
Out[8]: [['Python', 0.75, 1.0, 1.5, 2.0],
         ['Python', 0.75, 1.0, 1.5, 2.0],
         ['Python', 0.75, 1.0, 1.5, 2.0]]

Esto puede evitarse utilizando la función deepcopy() del módulo copy:

In [9]: from copy import deepcopy
        v = [0.5, 0.75, 1.0, 1.5, 2.0]
        m = 3 * [deepcopy(v), ]  1
        m
Out[9]: [[0.5, 0.75, 1.0, 1.5, 2.0],
         [0.5, 0.75, 1.0, 1.5, 2.0],
         [0.5, 0.75, 1.0, 1.5, 2.0]]

In [10]: v[0] = 'Python'  2
         m  3
Out[10]: [[0.5, 0.75, 1.0, 1.5, 2.0],
          [0.5, 0.75, 1.0, 1.5, 2.0],
          [0.5, 0.75, 1.0, 1.5, 2.0]]
1

En lugar de punteros de referencia, se utilizan copias físicas.

2

Como consecuencia, un cambio en el objeto original ...

3

... ya no tiene ningún impacto.

La clase array de Python

Existe es un módulo dedicado array disponible en Python. Según la documentación

Este módulo define un tipo de objeto que puede representar de forma compacta una matriz de valores básicos: caracteres, enteros, números de coma flotante. Las matrices son tipos de secuencia y se comportan de forma muy parecida a las listas, salvo que el tipo de los objetos almacenados en ellas está restringido. El tipo se especifica en el momento de crear el objeto mediante un código de tipo, que es un único carácter.

Considera el siguiente código, que crea un objeto array a partir de un objeto list:

In [11]: v = [0.5, 0.75, 1.0, 1.5, 2.0]

In [12]: import array

In [13]: a = array.array('f', v)  1
         a
Out[13]: array('f', [0.5, 0.75, 1.0, 1.5, 2.0])

In [14]: a.append(0.5)  2
         a
Out[14]: array('f', [0.5, 0.75, 1.0, 1.5, 2.0, 0.5])

In [15]: a.extend([5.0, 6.75])  2
         a
Out[15]: array('f', [0.5, 0.75, 1.0, 1.5, 2.0, 0.5, 5.0, 6.75])

In [16]: 2 * a  3
Out[16]: array('f', [0.5, 0.75, 1.0, 1.5, 2.0, 0.5, 5.0, 6.75, 0.5, 0.75, 1.0,
          1.5, 2.0, 0.5, 5.0, 6.75])
1

La instanciación del objeto array con float como código de tipo.

2

Los métodos principales funcionan de forma similar a los del objeto list.

3

Aunque la "multiplicación escalar" funciona en principio, el resultado no es el matemáticamente esperado, sino que los elementos se repiten.

Si se intenta añadir un objeto de un tipo de datos distinto del especificado, se produce un error TypeError:

In [17]: a.append('string')  1

         ---------------------------------------
         TypeErrorTraceback (most recent call last)
         <ipython-input-17-14cd6281866b> in <module>()
         ----> 1 a.append('string')  1

         TypeError: must be real number, not str

In [18]: a.tolist()  2
Out[18]: [0.5, 0.75, 1.0, 1.5, 2.0, 0.5, 5.0, 6.75]
1

Sólo se pueden añadir objetos float; otros tipos de datos/códigos de tipo provocan errores.

2

Sin embargo, el objeto array puede volver a convertirse fácilmente en un objeto list si se necesita esa flexibilidad.

Una ventaja de la clase array es que tiene incorporada la función de almacenamiento y recuperación:

In [19]: f = open('array.apy', 'wb')  1
         a.tofile(f)  2
         f.close()  3

In [20]: with open('array.apy', 'wb') as f:  4
             a.tofile(f)  4

In [21]: !ls -n arr*  5
         -rw-r--r--@ 1 503  20  32 Nov  7 11:46 array.apy
1

Abre un archivo en el disco para escribir datos binarios.

2

Escribe los datos de array en el archivo.

3

Cierra el archivo.

4

Alternativa: utiliza un contexto with para la misma operación.

5

Muestra el archivo tal y como está escrito en el disco.

Como antes, el tipo de datos del objeto array es importante a la hora de leer los datos del disco:

In [22]: b = array.array('f')  1

In [23]: with open('array.apy', 'rb') as f:  2
             b.fromfile(f, 5)  3

In [24]: b  3
Out[24]: array('f', [0.5, 0.75, 1.0, 1.5, 2.0])

In [25]: b = array.array('d')  4

In [26]: with open('array.apy', 'rb') as f:
             b.fromfile(f, 2)  5

In [27]: b  6
Out[27]: array('d', [0.0004882813645963324, 0.12500002956949174])
1

Instantiza un nuevo objeto array con código de tipo float.

2

Abre el archivo para leer datos binarios ...

3

... y lee cinco elementos en el objeto b.

4

Instantiza un nuevo objeto array con código de tipo double.

5

Lee dos elementos del archivo.

6

La diferencia en los códigos de tipo da lugar a números "erróneos".

Matrices NumPy regulares

Componer estructuras de matrices con objetos list funciona, en cierto modo. Pero no es realmente cómodo, y la clase list no se ha creado con este objetivo específico. Su ámbito de aplicación es mucho más amplio y general. La clase array es un poco más especializada, ya que proporciona algunas funciones útiles para trabajar con matrices de datos. Sin embargo, una clase verdaderamente especializada podría ser realmente beneficiosa para manejar estructuras de tipo matriz.

Lo básico

numpy.ndarray es una clase de este tipo, creada con el objetivo específico de manejar matrices n-dimensionales de forma cómoda y eficaz, es decir, con un alto rendimiento. La mejor forma de ilustrar el manejo básico de las instancias de esta clase es mediante ejemplos:

In [28]: import numpy as np  1

In [29]: a = np.array([0, 0.5, 1.0, 1.5, 2.0])  2
         a
Out[29]: array([0. , 0.5, 1. , 1.5, 2. ])

In [30]: type(a)  2
Out[30]: numpy.ndarray

In [31]: a = np.array(['a', 'b', 'c'])  3
         a
Out[31]: array(['a', 'b', 'c'], dtype='<U1')

In [32]: a = np.arange(2, 20, 2)  4
         a
Out[32]: array([ 2,  4,  6,  8, 10, 12, 14, 16, 18])

In [33]: a = np.arange(8, dtype=np.float)  5
         a
Out[33]: array([0., 1., 2., 3., 4., 5., 6., 7.])

In [34]: a[5:]  6
Out[34]: array([5., 6., 7.])

In [35]: a[:2]  6
Out[35]: array([0., 1.])
1

Importa el paquete numpy.

2

Crea un objeto ndarray a partir de un objeto list con floats.

3

Crea un objeto ndarray a partir de un objeto list con strs.

4

np.arange() funciona de forma similar a range()...

5

... pero toma como entrada adicional el parámetro dtype.

6

Con objetos unidimensionales ndarray, la indexación funciona como de costumbre.

Una característica importante de la clase ndarray es la multitud de métodos incorporados. Por ejemplo

In [36]: a.sum()  1
Out[36]: 28.0

In [37]: a.std()  2
Out[37]: 2.29128784747792

In [38]: a.cumsum()  3
Out[38]: array([ 0.,  1.,  3.,  6., 10., 15., 21., 28.])
1

La suma de todos los elementos.

2

La desviación típica de los elementos.

3

La suma acumulada de todos los elementos (empezando en la posición 0 del índice).

Otra característica importante de son las operaciones matemáticas (vectorizadas) definidas sobre ndarray objetos:

In [39]: l = [0., 0.5, 1.5, 3., 5.]
         2 * l  1
Out[39]: [0.0, 0.5, 1.5, 3.0, 5.0, 0.0, 0.5, 1.5, 3.0, 5.0]

In [40]: a
Out[40]: array([0., 1., 2., 3., 4., 5., 6., 7.])

In [41]: 2 * a  2
Out[41]: array([ 0.,  2.,  4.,  6.,  8., 10., 12., 14.])

In [42]: a ** 2  3
Out[42]: array([ 0.,  1.,  4.,  9., 16., 25., 36., 49.])

In [43]: 2 ** a  4
Out[43]: array([  1.,   2.,   4.,   8.,  16.,  32.,  64., 128.])

In [44]: a ** a  5
Out[44]: array([1.00000e+00, 1.00000e+00, 4.00000e+00, 2.70000e+01, 2.56000e+02,
                3.12500e+03, 4.66560e+04, 8.23543e+05])
1

La multiplicación escalar con objetos list da lugar a una repetición de elementos.

2

Por el contrario, el trabajo con objetos ndarray implementa una multiplicación escalar adecuada.

3

Calcula los valores cuadrados elemento a elemento.

4

Esto interpreta los elementos del ndarray como los poderes.

5

Calcula la potencia de cada elemento respecto a sí mismo.

Las funciones universales son otra característica importante del paquete NumPy. Son "universales" en el sentido de que, en general, operan tanto sobre objetos ndarray como sobre tipos de datos básicos de Python. Sin embargo, al aplicar funciones universales a, por ejemplo, un objeto Python float, hay que tener en cuenta la reducción del rendimiento en comparación con la misma funcionalidad que se encuentra en el módulo math:

In [45]: np.exp(a)  1
Out[45]: array([1.00000000e+00, 2.71828183e+00, 7.38905610e+00, 2.00855369e+01,
                5.45981500e+01, 1.48413159e+02, 4.03428793e+02, 1.09663316e+03])

In [46]: np.sqrt(a)  2
Out[46]: array([0.        , 1.        , 1.41421356, 1.73205081, 2.        ,
                2.23606798, 2.44948974, 2.64575131])

In [47]: np.sqrt(2.5)  3
Out[47]: 1.5811388300841898

In [48]: import math  4

In [49]: math.sqrt(2.5)  4
Out[49]: 1.5811388300841898

In [50]: math.sqrt(a)  5

         ---------------------------------------
         TypeErrorTraceback (most recent call last)
         <ipython-input-50-b39de4150838> in <module>()
         ----> 1 math.sqrt(a)  5

         TypeError: only size-1 arrays can be converted to Python scalars

In [51]: %timeit np.sqrt(2.5)  6
         722 ns ± 13.7 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops
          each)

In [52]: %timeit math.sqrt(2.5)  7
         91.8 ns ± 4.13 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops
          each)
1

Calcula los valores exponenciales por elementos.

2

Calcula la raíz cuadrada de cada elemento.

3

Calcula la raíz cuadrada de un objeto Python float.

4

El mismo cálculo, esta vez utilizando el módulo math.

5

La función math.sqrt() no puede aplicarse directamente al objeto ndarray.

6

Aplicación de la función universal np.sqrt() a un objeto Python float...

7

... es mucho más lento que la misma operación con la función math.sqrt().

Dimensiones múltiples

La transición de a más de una dimensión es perfecta, y todas las características presentadas hasta ahora se trasladan a los casos más generales. En particular, el sistema de indexación es coherente en todas las dimensiones:

In [53]: b = np.array([a, a * 2])  1
         b
Out[53]: array([[ 0.,  1.,  2.,  3.,  4.,  5.,  6.,  7.],
                [ 0.,  2.,  4.,  6.,  8., 10., 12., 14.]])

In [54]: b[0]  2
Out[54]: array([0., 1., 2., 3., 4., 5., 6., 7.])

In [55]: b[0, 2]  3
Out[55]: 2.0

In [56]: b[:, 1]  4
Out[56]: array([1., 2.])

In [57]: b.sum()  5
Out[57]: 84.0

In [58]: b.sum(axis=0)  6
Out[58]: array([ 0.,  3.,  6.,  9., 12., 15., 18., 21.])

In [59]: b.sum(axis=1)  7
Out[59]: array([28., 56.])
1

Construye un objeto bidimensional ndarray a partir del unidimensional.

2

Selecciona la primera fila.

3

Selecciona el tercer elemento de la primera fila; los índices están separados, dentro de los corchetes, por una coma.

4

Selecciona la segunda columna.

5

Calcula la suma de todos los valores.

6

Calcula la suma a lo largo del primer eje; es decir, por columnas.

7

Calcula la suma a lo largo del segundo eje; es decir, por filas.

Hay varias formas de inicializar (instanciar) objetos ndarray. Una es la presentada anteriormente, a través de np.array. Sin embargo, esto supone que todos los elementos de la matriz ya están disponibles. Por el contrario, uno puede desear que los objetos ndarray se instancien primero para rellenarlos después con los resultados generados durante la ejecución del código. Para ello, puedes utilizar las siguientes funciones:

In [60]: c = np.zeros((2, 3), dtype='i', order='C')  1
         c
Out[60]: array([[0, 0, 0],
                [0, 0, 0]], dtype=int32)

In [61]: c = np.ones((2, 3, 4), dtype='i', order='C')  2
         c
Out[61]: array([[[1, 1, 1, 1],
                 [1, 1, 1, 1],
                 [1, 1, 1, 1]],

                [[1, 1, 1, 1],
                 [1, 1, 1, 1],
                 [1, 1, 1, 1]]], dtype=int32)

In [62]: d = np.zeros_like(c, dtype='f16', order='C')  3
         d
Out[62]: array([[[0., 0., 0., 0.],
                 [0., 0., 0., 0.],
                 [0., 0., 0., 0.]],

                [[0., 0., 0., 0.],
                 [0., 0., 0., 0.],
                 [0., 0., 0., 0.]]], dtype=float128)

In [63]: d = np.ones_like(c, dtype='f16', order='C')  3
         d
Out[63]: array([[[1., 1., 1., 1.],
                 [1., 1., 1., 1.],
                 [1., 1., 1., 1.]],

                [[1., 1., 1., 1.],
                 [1., 1., 1., 1.],
                 [1., 1., 1., 1.]]], dtype=float128)

In [64]: e = np.empty((2, 3, 2))  4
         e
Out[64]: array([[[0.00000000e+000, 0.00000000e+000],
                 [0.00000000e+000, 0.00000000e+000],
                 [0.00000000e+000, 0.00000000e+000]],

                [[0.00000000e+000, 0.00000000e+000],
                 [0.00000000e+000, 7.49874326e+247],
                 [1.28822975e-231, 4.33190018e-311]]])

In [65]: f = np.empty_like(c)  4
         f
Out[65]: array([[[         0,          0,          0,          0],
                 [         0,          0,          0,          0],
                 [         0,          0,          0,          0]],

                [[         0,          0,          0,          0],
                 [         0,          0,  740455269, 1936028450],
                 [         0,  268435456, 1835316017,       2041]]], dtype=int32)

In [66]: np.eye(5)  5
Out[66]: 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.]])

In [67]: g = np.linspace(5, 15, 12) 6
         g
Out[67]: array([ 5.        ,  5.90909091,  6.81818182,  7.72727273,  8.63636364,
                 9.54545455, 10.45454545, 11.36363636, 12.27272727, 13.18181818,
                14.09090909, 15.        ])
1

Crea un objeto ndarray prepoblado con ceros.

2

Crea un objeto ndarray prepoblado con unos.

3

Lo mismo, pero toma otro objeto ndarray para deducir la forma.

4

Crea un objeto ndarray no prepoblado con nada (los números dependen de los bits presentes en la memoria).

5

Crea una matriz cuadrada como un objeto ndarray con la diagonal poblada por unos.

6

Crea un objeto unidimensional ndarray con intervalos uniformemente espaciados entre los números; los parámetros utilizados son start, end, y num (número de elementos).

Para todas estas funciones, puedes proporcionar los siguientes parámetros:

shape

Puede ser un int, una secuencia de objetos int o una referencia a otro ndarray

dtype (opcional)

A dtype-estos son tipos de datos NumPy-específicos para objetos ndarray

order (opcional)

El orden de almacenamiento de los elementos en la memoria: C para C (es decir, por filas) o F para Fortran (es decir, por columnas)

Aquí se hace evidente cómo NumPy especializa la construcción de matrices con la clase ndarray, en comparación con el enfoque basado en list:

  • El objeto ndarray tiene dimensiones incorporadas (ejes).

  • El objeto ndarray es inmutable; su longitud (tamaño) es fija.

  • Sólo permite un único tipo de datos (np.dtype) para toda la matriz.

En cambio, la clase array sólo comparte la característica de permitir un único tipo de datos (código de tipo, dtype).

La función del parámetro order se trata más adelante en el capítulo. La Tabla 4-1 ofrece una visión general de los objetos np.dtype seleccionados (es decir, los tipos de datos básicos que permite NumPy ).

Tabla 4-1. Objetos dtype de NumPy
dtype Descripción Ejemplo

?

Booleano

? (True o False)

i

Entero con signo

i8 (64 bits)

u

Entero sin signo

u8 (64 bits)

f

Punto flotante

f8 (64 bits)

c

Punto flotante complejo

c32 (256 bits)

m

timedelta

m (64 bits)

M

datetime

M (64 bits)

O

Objeto

O (puntero al objeto)

U

Unicode

U24 (24 caracteres Unicode)

V

Datos brutos (vacío)

V12 (bloque de datos de 12 bytes)

Metainformación

Cada objeto de ndarray proporciona acceso a una serie de atributos útiles:

In [68]: g.size  1
Out[68]: 12

In [69]: g.itemsize  2
Out[69]: 8

In [70]: g.ndim  3
Out[70]: 1

In [71]: g.shape  4
Out[71]: (12,)

In [72]: g.dtype  5
Out[72]: dtype('float64')

In [73]: g.nbytes  6
Out[73]: 96
1

El número de elementos.

2

El número de bytes utilizados para representar un elemento.

3

El número de dimensiones.

4

La forma del objeto ndarray.

5

El dtype de los elementos.

6

El número total de bytes utilizados en memoria.

Remodelar y redimensionar

Aunque los objetos ndarray son inmutables por defecto, existen múltiples opciones para remodelar y redimensionar un objeto de este tipo. Mientras que la remodelación, en general, sólo proporciona otra visión de los mismos datos, el redimensionamiento, en general, crea un nuevo objeto (temporal). Primero, algunos ejemplos de remodelación:

In [74]: g = np.arange(15)

In [75]: g
Out[75]: array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14])

In [76]: g.shape  1
Out[76]: (15,)

In [77]: np.shape(g) 1
Out[77]: (15,)

In [78]: g.reshape((3, 5))  2
Out[78]: array([[ 0,  1,  2,  3,  4],
                [ 5,  6,  7,  8,  9],
                [10, 11, 12, 13, 14]])

In [79]: h = g.reshape((5, 3))  3
         h
Out[79]: array([[ 0,  1,  2],
                [ 3,  4,  5],
                [ 6,  7,  8],
                [ 9, 10, 11],
                [12, 13, 14]])

In [80]: h.T  4
Out[80]: array([[ 0,  3,  6,  9, 12],
                [ 1,  4,  7, 10, 13],
                [ 2,  5,  8, 11, 14]])

In [81]: h.transpose()  4
Out[81]: array([[ 0,  3,  6,  9, 12],
                [ 1,  4,  7, 10, 13],
                [ 2,  5,  8, 11, 14]])
1

La forma del objeto original ndarray.

2

Remodelación a dos dimensiones (vista de memoria).

3

Crear un nuevo objeto.

4

La transposición del nuevo objeto ndarray.

Durante una operación de redimensionamiento, el número total de elementos del objeto ndarray no cambia. Durante una operación de redimensionamiento, este número cambia: disminuye ("down-sizing") o aumenta ("up-sizing"). Aquí tienes algunos ejemplos de redimensionamiento:

In [82]: g
Out[82]: array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14])

In [83]: np.resize(g, (3, 1))  1
Out[83]: array([[0],
                [1],
                [2]])

In [84]: np.resize(g, (1, 5))  1
Out[84]: array([[0, 1, 2, 3, 4]])

In [85]: np.resize(g, (2, 5))  1
Out[85]: array([[0, 1, 2, 3, 4],
                [5, 6, 7, 8, 9]])

In [86]: n = np.resize(g, (5, 4))  2
         n
Out[86]: array([[ 0,  1,  2,  3],
                [ 4,  5,  6,  7],
                [ 8,  9, 10, 11],
                [12, 13, 14,  0],
                [ 1,  2,  3,  4]])
1

Dos dimensiones, reducción de tamaño.

2

Dos dimensiones, aumento de tamaño.

El apilamiento es una operación especial de que permite combinar horizontal o verticalmente dos objetos de ndarray. Sin embargo, el tamaño de la dimensión de "conexión" debe ser el mismo:

In [87]: h
Out[87]: array([[ 0,  1,  2],
                [ 3,  4,  5],
                [ 6,  7,  8],
                [ 9, 10, 11],
                [12, 13, 14]])

In [88]: np.hstack((h, 2 * h))  1
Out[88]: array([[ 0,  1,  2,  0,  2,  4],
                [ 3,  4,  5,  6,  8, 10],
                [ 6,  7,  8, 12, 14, 16],
                [ 9, 10, 11, 18, 20, 22],
                [12, 13, 14, 24, 26, 28]])

In [89]: np.vstack((h, 0.5 * h))  2
Out[89]: array([[ 0. ,  1. ,  2. ],
                [ 3. ,  4. ,  5. ],
                [ 6. ,  7. ,  8. ],
                [ 9. , 10. , 11. ],
                [12. , 13. , 14. ],
                [ 0. ,  0.5,  1. ],
                [ 1.5,  2. ,  2.5],
                [ 3. ,  3.5,  4. ],
                [ 4.5,  5. ,  5.5],
                [ 6. ,  6.5,  7. ]])
1

Apilamiento horizontal de dos objetos ndarray.

2

Apilamiento vertical de dos objetos ndarray.

Otra operación especial es el aplanamiento de un objeto multidimensional ndarray a uno unidimensional. Puedes elegir si el aplanamiento se realiza fila a fila (ordenC ) o columna a columna (ordenF ):

In [90]: h
Out[90]: array([[ 0,  1,  2],
                [ 3,  4,  5],
                [ 6,  7,  8],
                [ 9, 10, 11],
                [12, 13, 14]])

In [91]: h.flatten()  1
Out[91]: array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14])

In [92]: h.flatten(order='C')  1
Out[92]: array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14])

In [93]: h.flatten(order='F')  2
Out[93]: array([ 0,  3,  6,  9, 12,  1,  4,  7, 10, 13,  2,  5,  8, 11, 14])

In [94]: for i in h.flat:  3
             print(i, end=',')
         0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,
In [95]: for i in h.ravel(order='C'):  4
             print(i, end=',')
         0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,
In [96]: for i in h.ravel(order='F'):  4
             print(i, end=',')
         0,3,6,9,12,1,4,7,10,13,2,5,8,11,14,
1

El orden por defecto para aplanar es C.

2

Aplanamiento con orden F.

3

El atributo flat proporciona un iterador plano (ordenC ).

4

El método ravel() es una alternativa a flatten().

Matrices booleanas

La comparación y las operaciones lógicas en general funcionan sobre los objetos ndarray del mismo modo, por elementos, que sobre los tipos de datos estándar de Python. Las condiciones de evaluación producen por defecto un objeto booleano ndarray (dtype es bool):

In [97]: h
Out[97]: array([[ 0,  1,  2],
                [ 3,  4,  5],
                [ 6,  7,  8],
                [ 9, 10, 11],
                [12, 13, 14]])

In [98]: h > 8  1
Out[98]: array([[False, False, False],
                [False, False, False],
                [False, False, False],
                [ True,  True,  True],
                [ True,  True,  True]])

In [99]: h <= 7  2
Out[99]: array([[ True,  True,  True],
                [ True,  True,  True],
                [ True,  True, False],
                [False, False, False],
                [False, False, False]])

In [100]: h == 5  3
Out[100]: array([[False, False, False],
                 [False, False,  True],
                 [False, False, False],
                 [False, False, False],
                 [False, False, False]])

In [101]: (h == 5).astype(int)  4
Out[101]: array([[0, 0, 0],
                 [0, 0, 1],
                 [0, 0, 0],
                 [0, 0, 0],
                 [0, 0, 0]])

In [102]: (h > 4) & (h <= 12)  5
Out[102]: array([[False, False, False],
                 [False, False,  True],
                 [ True,  True,  True],
                 [ True,  True,  True],
                 [ True, False, False]])
1

¿El valor es mayor que ...?

2

¿El valor es menor o igual que ...?

3

¿El valor es igual a ...?

4

Presenta True y False como valores enteros 0 y 1.

5

¿El valor es mayor que ... y menor o igual que ...?

Estas matrices booleanas pueden utilizarse para indexar y seleccionar datos. Observa que las siguientes operaciones aplanan los datos:

In [103]: h[h > 8]  1
Out[103]: array([ 9, 10, 11, 12, 13, 14])

In [104]: h[(h > 4) & (h <= 12)]  2
Out[104]: array([ 5,  6,  7,  8,  9, 10, 11, 12])

In [105]: h[(h < 4) | (h >= 12)]  3
Out[105]: array([ 0,  1,  2,  3, 12, 13, 14])
1

Dame todos los valores mayores que ...

2

Dame todos los valores mayores que ... y menores o iguales que ...

3

Dame todos los valores mayores que ... o menores o iguales que ...

Una potente herramienta en este sentido es la función np.where(), que permite definir acciones/operaciones en función de si una condición es True o False. El resultado de aplicar np.where() es un nuevo objeto ndarray con la misma forma que el original:

In [106]: np.where(h > 7, 1, 0)  1
Out[106]: array([[0, 0, 0],
                 [0, 0, 0],
                 [0, 0, 1],
                 [1, 1, 1],
                 [1, 1, 1]])

In [107]: np.where(h % 2 == 0, 'even', 'odd')  2
Out[107]: array([['even', 'odd', 'even'],
                 ['odd', 'even', 'odd'],
                 ['even', 'odd', 'even'],
                 ['odd', 'even', 'odd'],
                 ['even', 'odd', 'even']], dtype='<U4')

In [108]: np.where(h <= 7, h * 2, h / 2)  3
Out[108]: array([[ 0. ,  2. ,  4. ],
                 [ 6. ,  8. , 10. ],
                 [12. , 14. ,  4. ],
                 [ 4.5,  5. ,  5.5],
                 [ 6. ,  6.5,  7. ]])
1

En el nuevo objeto, establece 1 si es True y 0 en caso contrario.

2

En el nuevo objeto, establece even si es True y odd en caso contrario.

3

En el nuevo objeto, pon dos veces el elemento h si es True y la mitad del elemento h en caso contrario.

En capítulos posteriores encontrarás más ejemplos de estas importantes operaciones con objetos de ndarray.

Comparación de velocidades

En breve pasaremos a las matrices estructuradas con NumPy, pero sigamos con las matrices normales por un momento y veamos qué aporta la especialización en términos de rendimiento.

Como ejemplo sencillo, considera la generación de una matriz/matriz de forma 5.000 × 5.000 elementos, poblada con números pseudoaleatorios estándar distribuidos normalmente. A continuación, se calculará la suma de todos los elementos. En primer lugar, el enfoque Python puro, en el que se utilizan las comprensiones list:

In [109]: import random
          I = 5000

In [110]: %time mat = [[random.gauss(0, 1) for j in range(I)] \
                       for i in range(I)]  1
          CPU times: user 17.1 s, sys: 361 ms, total: 17.4 s
          Wall time: 17.4 s

In [111]: mat[0][:5]  2
Out[111]: [-0.40594967782329183,
           -1.357757478015285,
           0.05129566894355976,
           -0.8958429976582192,
           0.6234174778878331]

In [112]: %time sum([sum(l) for l in mat])  3
          CPU times: user 142 ms, sys: 1.69 ms, total: 144 ms
          Wall time: 143 ms

Out[112]: -3561.944965714259

In [113]: import sys
          sum([sys.getsizeof(l) for l in mat])  4
Out[113]: 215200000
1

La creación de la matriz mediante una comprensión anidada de list.

2

Algunos seleccionaron números al azar de entre los sorteados.

3

Las sumas de los objetos individuales list se calculan primero durante la comprensión de una lista; luego se toma la suma de las sumas.

4

Esto suma el uso de memoria de todos los objetos de list.

Pasemos ahora a NumPy y veamos cómo se resuelve allí el mismo problema. Por comodidad, el subpaquete NumPy random ofrece multitud de funciones para instanciar un objeto ndarray y poblarlo al mismo tiempo con números pseudoaleatorios:

In [114]: %time mat = np.random.standard_normal((I, I))  1
          CPU times: user 1.01 s, sys: 200 ms, total: 1.21 s
          Wall time: 1.21 s

In [115]: %time mat.sum()  2
          CPU times: user 29.7 ms, sys: 1.15 ms, total: 30.8 ms
          Wall time: 29.4 ms

Out[115]: -186.12767026606448

In [116]: mat.nbytes  3
Out[116]: 200000000

In [117]: sys.getsizeof(mat)  3
Out[117]: 200000112
1

Crea el objeto ndarray con números aleatorios estándar distribuidos normalmente; es más rápido por un factor de aproximadamente 14.

2

Calcula la suma de todos los valores del objeto ndarray; es más rápido por un factor de 4,5.

3

El enfoque NumPy también ahorra algo de memoria, ya que la sobrecarga de memoria del objeto ndarray es minúscula en comparación con el tamaño de los propios datos.

Utilizar matrices NumPy

El uso de NumPy para operaciones y algoritmos basados en matrices suele dar como resultado un código compacto y fácil de leer, y mejoras significativas de rendimiento respecto al código Python puro.

Matrices NumPy estructuradas

La especialización de la clase ndarray conlleva, obviamente, una serie de valiosas ventajas. Sin embargo, una especialización demasiado estrecha podría resultar una carga demasiado pesada para la mayoría de los algoritmos y aplicaciones basados en arrays. Por ello, NumPy proporciona objetosestructurados ndarray y de registro recarray que te permiten tener un dtype diferente por columna. ¿Qué significa "por columna"? Considera la siguiente inicialización de un objeto estructurado ndarray:

In [118]: dt = np.dtype([('Name', 'S10'), ('Age', 'i4'),
                         ('Height', 'f'), ('Children/Pets', 'i4', 2)])  1

In [119]: dt  1
Out[119]: dtype([('Name', 'S10'), ('Age', '<i4'), ('Height', '<f4'),
           ('Children/Pets', '<i4', (2,))])

In [120]: dt = np.dtype({'names': ['Name', 'Age', 'Height', 'Children/Pets'],
                       'formats':'O int float int,int'.split()})  2

In [121]: dt  2
Out[121]: dtype([('Name', 'O'), ('Age', '<i8'), ('Height', '<f8'),
           ('Children/Pets', [('f0', '<i8'), ('f1', '<i8')])])

In [122]: s = np.array([('Smith', 45, 1.83, (0, 1)),
                        ('Jones', 53, 1.72, (2, 2))], dtype=dt)  3

In [123]: s  3
Out[123]: array([('Smith', 45, 1.83, (0, 1)), ('Jones', 53, 1.72, (2, 2))],
          dtype=[('Name', 'O'), ('Age', '<i8'), ('Height', '<f8'),
           ('Children/Pets', [('f0', '<i8'), ('f1', '<i8')])])

In [124]: type(s)  4
Out[124]: numpy.ndarray
1

El complejo dtype está compuesto.

2

Una sintaxis alternativa para conseguir el mismo resultado.

3

El ndarray estructurado se instanciará con dos registros.

4

El tipo de objeto sigue siendo ndarray.

En cierto sentido, esta construcción se parece bastante a la operación de inicialización de tablas en una base de datos SQL: se tienen los nombres de las columnas y los tipos de datos de las columnas, quizá con alguna información adicional (por ejemplo, el número máximo de caracteres por objeto str ). Ahora se puede acceder fácilmente a las columnas individuales por sus nombres y a las filas por sus valores de índice:

In [125]: s['Name']  1
Out[125]: array(['Smith', 'Jones'], dtype=object)

In [126]: s['Height'].mean()  2
Out[126]: 1.775

In [127]: s[0]  3
Out[127]: ('Smith', 45, 1.83, (0, 1))

In [128]: s[1]['Age']  4
Out[128]: 53
1

Seleccionar una columna por su nombre.

2

Llamar a un método en una columna seleccionada.

3

Seleccionar un registro.

4

Seleccionar un campo en un registro.

En resumen, las matrices estructuradas son una generalización del tipo de objeto normal ndarray, en el sentido de que el tipo de datos sólo tiene que ser el mismo por columna, como en las tablas de las bases de datos SQL. Una ventaja de las matrices estructuradas es que un solo elemento de una columna puede ser otro objeto multidimensional y no tiene por qué ajustarse a los tipos de datos básicos de NumPy.

Matrices estructuradas

NumPy proporciona, además de matrices normales, matrices estructuradas (y de registros) que permiten describir y manejar estructuras de datos similares a tablas con una variedad de tipos de datos diferentes por columna (con nombre). Traen a Python estructuras de datos similares a tablas SQL, con la mayoría de las ventajas de los objetos normales ndarray (sintaxis, métodos, rendimiento).

Vectorización del código

La vectorización es una estrategia de para conseguir un código más compacto que posiblemente se ejecute más rápido. La idea fundamental es realizar una operación o aplicar una función a un objeto complejo "de una vez" y no haciendo un bucle sobre los elementos individuales del objeto. En Python, las herramientas de programación funcional como map() y filter() proporcionan algunos medios básicos para la vectorización. Sin embargo, NumPy tiene la vectorización incorporada en lo más profundo de su núcleo.

Vectorización básica

Como se ha demostrado en el apartado anterior, las operaciones matemáticas sencillas -como calcular la suma de todos los elementos- pueden implementarse en los objetos ndarray directamente (mediante métodos o funciones universales). También son posibles operaciones vectoriales más generales. Por ejemplo, se pueden sumar dos matrices NumPy elemento a elemento de la siguiente manera:

In [129]: np.random.seed(100)
          r = np.arange(12).reshape((4, 3))  1
          s = np.arange(12).reshape((4, 3)) * 0.5  2

In [130]: r  1
Out[130]: array([[ 0,  1,  2],
                 [ 3,  4,  5],
                 [ 6,  7,  8],
                 [ 9, 10, 11]])

In [131]: s  2
Out[131]: array([[0. , 0.5, 1. ],
                 [1.5, 2. , 2.5],
                 [3. , 3.5, 4. ],
                 [4.5, 5. , 5.5]])

In [132]: r + s  3
Out[132]: array([[ 0. ,  1.5,  3. ],
                 [ 4.5,  6. ,  7.5],
                 [ 9. , 10.5, 12. ],
                 [13.5, 15. , 16.5]])
1

El primer objeto ndarray con números aleatorios.

2

El segundo objeto ndarray con números aleatorios.

3

Suma de elementos como operación vectorizada (sin bucles).

NumPy también admite lo que se denomina difusión. Esto te permite combinar objetos de distinta forma en una sola operación. Los ejemplos anteriores ya han hecho uso de esto. Considera los siguientes ejemplos:

In [133]: r + 3  1
Out[133]: array([[ 3,  4,  5],
                 [ 6,  7,  8],
                 [ 9, 10, 11],
                 [12, 13, 14]])

In [134]: 2 * r  2
Out[134]: array([[ 0,  2,  4],
                 [ 6,  8, 10],
                 [12, 14, 16],
                 [18, 20, 22]])

In [135]: 2 * r + 3  3
Out[135]: array([[ 3,  5,  7],
                 [ 9, 11, 13],
                 [15, 17, 19],
                 [21, 23, 25]])
1

Durante la adición escalar, el escalar se difunde y se añade a cada elemento.

2

Durante la multiplicación escalar, el escalar también se transmite a cada elemento y se multiplica con él.

3

Esta transformación lineal combina ambas operaciones.

Estas operaciones también funcionan con objetos ndarray de formas diferentes, hasta cierto punto:

In [136]: r
Out[136]: array([[ 0,  1,  2],
                 [ 3,  4,  5],
                 [ 6,  7,  8],
                 [ 9, 10, 11]])

In [137]: r.shape
Out[137]: (4, 3)

In [138]: s = np.arange(0, 12, 4)  1
          s  1
Out[138]: array([0, 4, 8])

In [139]: r + s  2
Out[139]: array([[ 0,  5, 10],
                 [ 3,  8, 13],
                 [ 6, 11, 16],
                 [ 9, 14, 19]])

In [140]: s = np.arange(0, 12, 3)  3
          s  3
Out[140]: array([0, 3, 6, 9])

In [141]: r + s  4

          ---------------------------------------
          ValueErrorTraceback (most recent call last)
          <ipython-input-141-1890b26ec965> in <module>()
          ----> 1 r + s  4

          ValueError: operands could not be broadcast together
                      with shapes (4,3) (4,)

In [142]: r.transpose() + s  5
Out[142]: array([[ 0,  6, 12, 18],
                 [ 1,  7, 13, 19],
                 [ 2,  8, 14, 20]])

In [143]: sr = s.reshape(-1, 1)  6
          sr
Out[143]: array([[0],
                 [3],
                 [6],
                 [9]])

In [144]: sr.shape  6
Out[144]: (4, 1)

In [145]: r + s.reshape(-1, 1)  6
Out[145]: array([[ 0,  1,  2],
                 [ 6,  7,  8],
                 [12, 13, 14],
                 [18, 19, 20]])
1

Un nuevo objeto unidimensional ndarray de longitud 3.

2

Los objetos r (matriz) y s (vector) pueden añadirse directamente.

3

Otro objeto unidimensional ndarray de longitud 4.

4

La longitud del nuevo objeto s (vector) es ahora diferente de la longitud de la segunda dimensión del objeto r.

5

Transponer de nuevo el objeto r permite la suma vectorizada.

6

Alternativamente, se puede cambiar la forma de s a (4, 1) para que funcione la suma (sin embargo, los resultados son diferentes).

A menudo, las funciones Python definidas a medida también funcionan con objetos ndarray. Si la implementación lo permite, las matrices pueden utilizarse con funciones del mismo modo que los objetos int o float. Considera la siguiente función:

In [146]: def f(x):
              return 3 * x + 5  1

In [147]: f(0.5)  2
Out[147]: 6.5

In [148]: f(r)  3
Out[148]: array([[ 5,  8, 11],
                 [14, 17, 20],
                 [23, 26, 29],
                 [32, 35, 38]])
1

Una sencilla función de Python que implementa una transformación lineal en el parámetro x.

2

La función f() aplicada a un objeto Python float.

3

La misma función aplicada a un objeto ndarray, dando como resultado una evaluación vectorizada y por elementos de la función.

Lo que hace NumPy es simplemente aplicar la función f al objeto elemento a elemento. En ese sentido, al utilizar este tipo de operación no se evitan los bucles; sólo se evitan en el nivel Python y se delega el bucle en NumPy. En el nivel NumPy, de los bucles sobre el objeto ndarray se encarga el código optimizado, la mayor parte de él escrito en C y, por tanto, generalmente más rápido que el Python puro. Esto explica el "secret" que hay detrás de las ventajas de rendimiento que supone utilizar NumPy para casos de uso basados en matrices.

Disposición de la memoria

Cuando los objetos ndarray se inicializan utilizando np.zeros(), como en "Dimensiones múltiples", se proporciona un argumento opcional para la disposición en memoria. Este argumento especifica, a grandes rasgos, qué elementos de una matriz se almacenan en memoria unos junto a otros (contiguamente). Cuando se trabaja con matrices pequeñas, esto apenas tiene un impacto apreciable en el rendimiento de las operaciones con matrices. Sin embargo, cuando las matrices se hacen grandes, y dependiendo del algoritmo (financiero) que se implemente en ellas, la historia puede ser diferente. Es entonces cuando entra en juego la disposición de la memoria (consulta, por ejemplo, el artículo de Eli Bendersky "Memory Layout of Multi-Dimensional Arrays").

Para ilustrar la importancia potencial de la disposición en memoria de las matrices en ciencia y finanzas, considera la siguiente construcción de objetos multidimensionales ndarray:

In [149]: x = np.random.standard_normal((1000000, 5))  1

In [150]: y = 2 * x + 3  2

In [151]: C = np.array((x, y), order='C')  3

In [152]: F = np.array((x, y), order='F')  4

In [153]: x = 0.0; y = 0.0  5

In [154]: C[:2].round(2)  6
Out[154]: array([[[-1.75,  0.34,  1.15, -0.25,  0.98],
                  [ 0.51,  0.22, -1.07, -0.19,  0.26],
                  [-0.46,  0.44, -0.58,  0.82,  0.67],
                  ...,
                  [-0.05,  0.14,  0.17,  0.33,  1.39],
                  [ 1.02,  0.3 , -1.23, -0.68, -0.87],
                  [ 0.83, -0.73,  1.03,  0.34, -0.46]],

                 [[-0.5 ,  3.69,  5.31,  2.5 ,  4.96],
                  [ 4.03,  3.44,  0.86,  2.62,  3.51],
                  [ 2.08,  3.87,  1.83,  4.63,  4.35],
                  ...,
                  [ 2.9 ,  3.28,  3.33,  3.67,  5.78],
                  [ 5.04,  3.6 ,  0.54,  1.65,  1.26],
                  [ 4.67,  1.54,  5.06,  3.69,  2.07]]])
1

Un objeto ndarray con gran asimetría en las dos dimensiones.

2

Una transformación lineal de los datos del objeto original.

3

Esto crea un objeto bidimensional ndarray con el orden C (fila-mayor).

4

Esto crea un objeto bidimensional ndarray con el orden F (columna-mayor).

5

Se libera memoria (depende de la recogida de basura).

6

Algunos números del objeto C.

Veamos algunos ejemplos fundamentales y casos de uso de ambos tipos de objetos ndarray y consideremos la velocidad con la que se ejecutan, dadas las diferentes disposiciones de memoria:

In [155]: %timeit C.sum()  1
          4.36 ms ± 89.3 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [156]: %timeit F.sum()  1
          4.21 ms ± 71.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [157]: %timeit C.sum(axis=0)  2
          17.9 ms ± 776 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [158]: %timeit C.sum(axis=1)  3
          35.1 ms ± 999 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [159]: %timeit F.sum(axis=0)  2
          83.8 ms ± 2.63 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [160]: %timeit F.sum(axis=1)  3
          67.9 ms ± 5.16 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [161]: F = 0.0; C = 0.0
1

Calcula la suma de todos los elementos.

2

Calcula las sumas por fila ("muchos").

3

Calcula las sumas por columnas ("pocas").

Podemos resumir los resultados de rendimiento como sigue:

  • Al calcular la suma de todos los elementos, la disposición de la memoria no importa realmente.

  • La suma sobre los objetos C-ordenados ndarray es más rápida tanto sobre filas como sobre columnas (una ventaja de velocidad absoluta ).

  • CCon el objeto ndarray ordenado (fila-mayor), la suma de filas es relativamente más rápida que la suma de columnas.

  • FCon el objeto ndarray ordenado por columnas, la suma de columnas es relativamente más rápida que la suma de filas.

Conclusión

NumPy es el paquete de elección para la computación numérica en Python. La clase ndarray está diseñada específicamente para ser cómoda y eficaz en el manejo de datos numéricos (grandes). Sus potentes métodos y las funciones universales NumPy permiten un código vectorizado que, en su mayor parte, evita los bucles lentos a nivel de Python. Muchos de los enfoques introducidos en este capítulo se trasladan también a pandas y a su clase DataFrame (véase el Capítulo 5).

Otros recursos

En el sitio web NumPy encontrarás muchos recursos útiles:

Buenas introducciones a NumPy en forma de libro son:

  • McKinney, Wes (2017). Python para el Análisis de Datos. Sebastopol, CA: O'Reilly.

  • VanderPlas, Jake (2016). Manual de Ciencia de Datos en Python. Sebastopol, CA: O'Reilly.

Get Python para Finanzas, 2ª 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.