Introduccion al Data Science [Numpy]

En este nuevo articulo,se realiza una introduccion al data science en python, empezando con la liberiria numpy y gracias a un repost de los notebooks de la gente de Python Mallorca, en la Pycon 2018. Van desde 0 hasta un nivel muy aceptable.

En esta primera entrega, empezamos tocando la libreria de numpy, uno de los clasicos para gestionar matrices.

Ahora empezemos con la salsa!

In [ ]:
import sys
In [ ]:
import numpy as np

Numpy

Numpy proporciona un nuevo contenedor de datos a Python, los ndarrays, además de funcionalidad especializada para poder manipularlos de forma eficiente.

Hablar de manipulación de datos en Python es sinónimo de Numpy y prácticamente todo el ecosistema científico de Python está construido sobre Numpy. Digamos que Numpy es el ladrillo que ha permitido levantar edificios tan sólidos como Pandas, Matplotlib, Scipy, scikit-learn,...

Índice

¿Por qué un nuevo contenedor de datos?

En Python, disponemos, de partida, de diversos contenedores de datos, listas, tuplas, diccionarios, conjuntos,..., ¿por qué añadir uno más?.

¡Por conveniencia!, a pesar de la pérdida de flexibilidad. Es una solución de compromiso.

  • Uso de memoria más eficiente: Por ejemplo, una lista puede contener distintos tipos de objetos lo que provoca que Python deba guardar información del tipo de cada elemento contenido en la lista. Por otra parte, un ndarray contiene tipos homogéneos, es decir, todos los elementos son del mismo tipo, por lo que la información del tipo solo debe guardarse una vez independientemente del número de elementos que tenga el ndarray.

arrays_vs_listas(imagen por Jake VanderPlas y extraída de GitHub).

  • Más rápido: Por ejemplo, en una lista que consta de elementos con diferentes tipos Python debe realizar trabajos extra para saber si los tipos son compatibles con las operaciones que estamos realizando. Cuando trabajamos con un ndarray ya podemos saber eso de partida y podemos tener operaciones más eficientes (además de que mucha funcionalidad está programada en C, C++, Cython, Fortran).
  • Operaciones vectorizadas
  • Funcionalidad extra: Muchas operaciones de álgebra lineal, transformadas rápidas de Fourier, estadística básica, histogramas,...
  • Acceso a los elementos más conveniente: Indexación más avanzada que con los tipos normales de Python
  • ...

Uso de memoria

In [ ]:
# AVISO: SYS.GETSYZEOF NO ES FIABLE

lista = list(range(5_000_000))
arr = np.array(lista, dtype=np.uint32)
print("5 millones de elementos")
print(sys.getsizeof(lista))
print(sys.getsizeof(arr))

print()

lista = list(range(100))
arr = np.array(lista, dtype=np.uint8)
print("100 elementos")
print(sys.getsizeof(lista))
print(sys.getsizeof(arr))

Velocidad de operaciones

In [ ]:
a = list(range(1000000))
%timeit sum(a)
print(sum(a))
In [ ]:
a = np.array(a)
%timeit np.sum(a)
print(np.sum(a))

Operaciones vectorizadas

In [ ]:
# Suma de dos vectores elemento a elemento
a = [1, 1, 1]
b = [3, 4, 3]
print(a + b)
print('Fail')
In [ ]:
# Suma de dos vectores elemento a elemento
a = np.array([1, 1, 1])
b = np.array([3, 4, 3])
print(a + b)
print('\o/')

Funcionalidad más conveniente

In [ ]:
# suma acumulada
a = list(range(100))
print([sum(a[:i+1]) for i in a])

a = np.array(a)
print(a.cumsum())

Acceso a elementos más conveniente

In [ ]:
a = [[11, 12, 13],
     [21, 22, 23],
     [31, 32, 33]]
print('acceso a la primera fila: ', a[0])
print('acceso a la primera columna: ', a[:][0], ' Fail!!!')
In [ ]:
a = np.array(a)
print('acceso a la primera fila: ', a[0])
print('acceso a la primera columna: ', a[:,0], ' \o/')

...

Recapitulando un poco.

Los ndarrays son contenedores multidimensionales, homogéneos con elementos de tamaño fijo, de dimensión predefinida.

Tipos de datos

Como los arrays deben ser homogéneos tenemos tipos de datos. Algunos de ellos se pueden ver en la siguiente tabla:

Data typeDescripción
bool_Booleano (True o False) almacenado como un Byte
int_El tipo entero por defecto (igual que el long de C; normalmente será int64 o int32)
intcIdéntico al int de C (normalmente int32 o int64)
intpEntero usado para indexación (igual que ssize_t en C; normalmente int32 o int64)
int8Byte (de -128 a 127)
int16Entero (de -32768 a 32767)
int32Entero (de -2147483648 a 2147483647)
int64Entero (de -9223372036854775808 a 9223372036854775807)
uint8Entero sin signo (de 0 a 255)
uint16Entero sin signo (de 0 a 65535)
uint32Entero sin signo (de 0 a 4294967295)
uint64Entero sin signo (de 0 a 18446744073709551615)
float_Atajo para float64.
float16Half precision float: un bit para el signo, 5 bits para el exponente, 10 bits para la mantissa
float32Single precision float: un bit para el signo, 8 bits para el exponente, 23 bits para la mantissa
float64Double precision float: un bit para el signo, 11 bits para el exponente, 52 bits para la mantissa
complex_Atajo para complex128.
complex64Número complejo, represantedo por dos floats de 32-bits
complex128Número complejo, represantedo por dos floats de 64-bits

Es posible tener una especificación de tipos más detallada, pudiendo especificar números con big endian o little endian. No vamos a ver esto en este momento.

El tipo por defecto que usa numpy al crear un ndarray es np.float_, siempre que no específiquemos explícitamente el tipo a usar.

Por ejemplo, un array de tipo np.uint8 puede tener los siguientes valores:

In [ ]:
import itertools

for i, bits in enumerate(itertools.product((0, 1), repeat=8)):
    print(i, bits)

Es decir, puede contener valores que van de 0 a 255 ($2^8$).

¿Cuántos bytes tendrá un ndarray de 10 elementos cuyo tipo de datos es un np.int8?

In [ ]:
a = np.arange(10, dtype=np.int8)
print(a.nbytes)
print(sys.getsizeof(a))
In [ ]:
a = np.repeat(1, 100000).astype(np.int8)
print(a.nbytes)
print(sys.getsizeof(a))

Creación de numpy arrays

Podemos crear numpy arrays de muchas formas.

  • Rangos numéricos

np.arange, np.linspace, np.logspace

  • Datos homogéneos

np.zeros, np.ones

  • Elementos diagonales

np.diag, np.eye

  • A partir de otras estructuras de datos ya creadas

np.array

  • A partir de otros numpy arrays

np.empty_like

  • A partir de ficheros

np.loadtxt, np.genfromtxt,...

  • A partir de un escalar

np.full, np.tile,...

  • A partir de valores aleatorios

np.random.randint, np.random.randint, np.random.randn,...

...

In [ ]:
a = np.arange(10) # similar a range pero devuelve un ndarray en lugar de un objeto range
print(a)
In [ ]:
a = np.linspace(0, 1, 101)
print(a)
In [ ]:
a_i = np.zeros((2, 3), dtype=np.int)
a_f = np.zeros((2, 3))
print(a_i)
print(a_f)
In [ ]:
a = np.eye(3)
print(a)
In [ ]:
a = np.array(
    (
        (1, 2, 3, 4, 5, 6),
        (10, 20, 30, 40, 50, 60)
    ), 
    dtype=np.float
)
print(a)
In [ ]:
np.full((5, 5), -999)
In [ ]:
np.random.randint(0, 50, 15)

Practicando

Recordad que siempre podéis usar help, ?, np.lookfor,..., para obtener más información.

In [ ]:
help(np.sum)
In [ ]:
np.rad2deg?
In [ ]:
np.lookfor("create array")

Ved un poco como funciona np.repeat, np.empty_like,...

In [ ]:
# Play area
In [ ]:
%load ../../solutions/03_01_np_array_creacion.py

Operaciones disponibles más típicas

In [ ]:
a = np.random.rand(5, 2)
print(a)
In [ ]:
a.sum()
In [ ]:
a.sum(axis=0)
In [ ]:
a.sum(axis=1)
In [ ]:
a.ravel()
In [ ]:
a.reshape(2, 5)
In [ ]:
a.T
In [ ]:
a.transpose()
In [ ]:
a.mean()
In [ ]:
a.mean(axis=1)
In [ ]:
a.cumsum(axis=1)

Practicando

Mirad más métodos de un ndarray y toquetead. Si no entendéis algo, preguntad:

In [ ]:
dir(a)
In [ ]:
# Play area
In [ ]:
%load ../../solutions/03_02_np_operaciones_tipicas.py

Metadatos y anatomía de un ndarray

En realidad, un ndarray es un bloque de memoria con información extra sobre como interpretar su contenido. La memoria dinámica (RAM) se puede considerar como un 'churro' lineal y es por ello que necesitamos esa información extra para saber como formar ese ndarray, sobre todo la información de shape y strides.

Esta parte va a ser un poco más esotérica para los no iniciados pero considero que es necesaria para poder entender mejor nuestra nueva estructura de datos y poder sacarle mejor partido.

In [ ]:
a = np.random.randn(5000, 5000)

El número de dimensiones del ndarray

In [ ]:
a.ndim

El número de elementos en cada una de las dimensiones

In [ ]:
a.shape

El número de elementos

In [ ]:
a.size

El tipo de datos de los elementos

In [ ]:
a.dtype

El número de bytes de cada elemento

In [ ]:
a.itemsize

El número de bytes que ocupa el ndarray (es lo mismo que size por itemsize)

In [ ]:
a.nbytes

El buffer que contiene los elementos del ndarray

In [ ]:
a.data

Pasos a dar en cada dimensión cuando nos movemos entre elementos

In [ ]:
a.strides

strides(imagen extraída de GitHub).

Más cosas

In [ ]:
a.flags

Pequeño ejercicio, ¿por qué tarda menos en sumar elementos en una dimensión que en otra si es un array regular?

In [ ]:
%timeit a.sum(axis=0)
%timeit a.sum(axis=1)

Pequeño ejercicio, ¿por qué ahora el resultado es diferente?

In [ ]:
aT = a.T
%timeit aT.sum(axis=0)
%timeit aT.sum(axis=1)
In [ ]:
print(aT.strides)
print(aT.flags)
In [ ]:
print(np.repeat((1,2,3), 3))
print()
a = np.repeat((1,2,3), 3).reshape(3, 3)
print(a)
print()
print(a.sum(axis=0))
print()
print(a.sum(axis=1))

Indexación

Si ya has trabajado con indexación en estructuras de Python, como listas, tuplas o strings, la indexación en Numpy te resultará muy familiar.

Por ejemplo, por hacer las cosas sencillas, vamos a crear un ndarray de 1D:

In [ ]:
a = np.arange(10, dtype=np.uint8)
print(a)
In [ ]:
print(a[:]) # para acceder a todos los elementos
print(a[:-1]) # todos los elementos menos el último
print(a[1:]) # todos los elementos menos el primero
print(a[::2]) # el primer, el tercer, el quinto,..., elemento
print(a[3]) # el cuarto elemento
print(a[-1:-5:-1]) # ¿?
In [ ]:
# Practicad vosotros

Para ndarrays de una dimensión es exactamente igual que si usásemos listas o tuplas de Python:

  • Primer elemento tiene índice 0
  • Los índices negativos empiezan a contar desde el final
  • slices/rebanadas con [start:stop:step]

Con un ndarray de más dimensiones las cosas ya cambian con respecto a Python puro:

In [ ]:
a = np.random.randn(10, 2)
print(a)
In [ ]:
a[1] # ¿Qué nos dará esto?
a[1, 1] # Si queremos acceder a un elemento específico hay que dar su posición completa en el ndarray
a[::3, 1]

Si tenemos dimensiones mayores a 1 es parecido a las listas pero los índices se separan por comas para las nuevas dimensiones. (imagen extraída de aquí)

In [ ]:
a = np.arange(40).reshape(5, 8)
print(a)
In [ ]:
a[2, -3]

Para obtener más de un elemento hacemos slicing para cada eje: (imagen extraída de aquí)

In [ ]:
a[:3, :5]

¿Cómo podemos conseguir los elementos señalados en esta imagen?

(imagen extraída de aquí)

In [ ]:
a[x:x ,x:x]

¿Cómo podemos conseguir los elementos señalados en esta imagen?

(imagen extraída de aquí)

In [ ]:
a[x:x ,x:x]

¿Cómo podemos conseguir los elementos señalados en esta imagen?

(imagen extraída de aquí)

In [ ]:
a[x:x ,x:x]

¿Cómo podemos conseguir los elementos señalados en esta imagen?

(imagen extraída de aquí)

In [ ]:
a[x:x ,x:x]

Soluciones a lo anterior:

(imágenes extraídas de aquí)

Fancy indexing

Con fancy indexing podemos hacer cosas tan variopintas como:

(imágenes extraídas de aquí)

Es decir, podemos indexar usando ndarrays de booleanos ó usando listas de índices para extraer elementos concretos de una sola vez.

WARNING: En el momento que usamos fancy indexing nos devuelve un nuevo ndarray que no tiene porque conservar la estructura original.

Por ejemplo, en el siguiente caso no devuelve un ndarray de dos dimensiones porque la máscara no tiene porqué ser regular y, por tanto, devuelve solo los valores que cumplen el criterio en un vector (ndarray de una dimensión).

In [ ]:
a = np.arange(10).reshape(2, 5)
print(a)
In [ ]:
bool_indexes = (a % 2 == 0)
print(bool_indexes)
In [ ]:
a[bool_indexes]

Sin embargo, sí que lo podríamos usar para modificar el ndarray original en base a un criterio y seguir manteniendo la misma forma.

In [ ]:
a[bool_indexes] = 999
print(a)

Manejo de valores especiales

numpy provee de varios valores especiales: np.nan, np.Inf, np.Infinity, np.inf, np.infty,...

In [ ]:
a = 1 / np.arange(10)
print(a)
In [ ]:
a[0] == np.inf
In [ ]:
a.max() # Esto no es lo que queremos
In [ ]:
a.mean() # Esto no es lo que queremos
In [ ]:
a[np.isfinite(a)].max()
In [ ]:
a[-1] = np.nan
print(a)
In [ ]:
a.mean()
In [ ]:
np.isnan(a)
In [ ]:
np.isfinite(a)
In [ ]:
np.isinf(a) # podéis mirar también np.isneginf, np.isposinf

numpy usa el estándar IEEE de números flotantes para aritmética (IEEE 754). Esto significa que Not a Number no es equivalente a infinity. También, positive infinity no es equivalente a negative infinity. Pero infinity es equivalente a positive infinity.

In [ ]:
1 < np.inf
In [ ]:
1 < -np.inf
In [ ]:
1 > -np.inf
In [ ]:
1 == np.inf
In [ ]:
1 < np.nan
In [ ]:
1 > np.nan
In [ ]:
1 == np.nan

Subarrays, vistas y copias

¡IMPORTANTE!

Vistas y copias: numpy, por defecto, siempre devuelve vistas para evitar incrementos innecesarios de memoria. Este comportamiento difiere del de Python puro donde una rebanada (slicing) de una lista devuelve una copia. Si queremos una copia de un ndarray debemos obtenerla de forma explícita:

In [ ]:
a = np.arange(10)
b = a[2:5]
print(a)
print(b)
In [ ]:
b[0] = 222
print(a)
print(b)

Este comportamiento por defecto es realmente muy útil, significa que, trabajando con grandes conjuntos de datos, podemos acceder y procesar piezas de estos conjuntos de datos sin necesidad de copiar el buffer de datos original.

A veces, es necesario crear una copia. Esto se puede realizar fácilmente usando el método copy de los ndarrays. El ejemplo anterior usando una copia en lugar de una vista:

In [ ]:
a = np.arange(10)
b = a[2:5].copy()
print(a)
print(b)
b[0] = 222
print(a)
print(b)

¿Cómo funcionan los ejes en un ndarray?

Por ejemplo, cuando hacemos a.sum(), a.sum(axis=0), a.sum(axis=1).

¿Qué pasa si tenemos más de dos dimensiones?

Vamos a ver ejemplos:

In [ ]:
a = np.arange(10).reshape(5,2)
In [ ]:
a.shape
In [ ]:
a.sum()
In [ ]:
a.sum(axis=0)
In [ ]:
a.sum(axis=1)

(imagen extraída de aquí)

In [ ]:
a = np.arange(9).reshape(3, 3)
print(a)
print(a.sum(axis=0))
print(a.sum(axis=1))

(imagen extraída de aquí)

In [ ]:
a = np.arange(24).reshape(2, 3, 4)
print(a)
print(a.sum(axis=0))
print(a.sum(axis=1))
print(a.sum(axis=2))

Por ejemplo, en el primer caso, axis=0, lo que sucede es que cogemos todos los elementos del primer índice y aplicamos la operación para cada uno de los elementos de los otros dos ejes. Hecho de uno en uno sería lo siguiente:

In [ ]:
print(a[:,0,0].sum(), a[:,0,1].sum(), a[:,0,2].sum(), a[:,0,3].sum())
print(a[:,1,0].sum(), a[:,1,1].sum(), a[:,1,2].sum(), a[:,1,3].sum())
print(a[:,2,0].sum(), a[:,2,1].sum(), a[:,2,2].sum(), a[:,2,3].sum())

Sin contar el eje que estamos usando, las dimensiones que quedan son 3 x 4 (segunda y tercera dimensiones) por lo que el resultado son 12 elementos.

Para el caso de axis=1:

In [ ]:
print(a[0,:,0].sum(), a[0,:,1].sum(), a[0,:,2].sum(), a[0,:,3].sum())
print(a[1,:,0].sum(), a[1,:,1].sum(), a[1,:,2].sum(), a[1,:,3].sum())

Sin contar el eje que estamos usando, las dimensiones que quedan son 2 x 4 (primera y tercera dimensiones) por lo que el resultado son 8 elementos.

Para el caso de axis=2:

In [ ]:
print(a[0,0,:].sum(), a[0,1,:].sum(), a[0,2,:].sum())
print(a[1,0,:].sum(), a[1,1,:].sum(), a[1,2,:].sum())

Sin contar el eje que estamos usando, las dimensiones que quedan son 2 x 3 (primera y segunda dimensiones) por lo que el resultado son 3 elementos.

Reformateo de ndarrays

Podemos cambiar la forma de los ndarrays usando el método reshape. Por ejemplo, si queremos colocar los números del 1 al 9 en un grid $3 \times 3$ lo podemos hacer de la siguiente forma:

In [ ]:
a = np.arange(1, 10).reshape(3, 3)

Para que el cambio de forma no dé errores hemos de tener cuidado en que los tamaños del ndarray inicial y del ndarray final sean compatibles.

In [ ]:
# Por ejemplo, lo siguiente dará error?
a = np.arange(1, 10). reshape(5, 2)

Otro patrón común de cambio de forma sería la conversion de un ndarray de 1D en uno de 2D añadiendo un nuevo eje. Lo podemos hacer usando, nuevamente, el método reshape o usando numpy.newaxis.

In [ ]:
# Por ejemplo un array 2D de una fila
a = np.arange(3)
a1_2D = a.reshape(1,3)
a2_2D = a[np.newaxis, :]
print(a1_2D)
print(a1_2D.shape)
print(a2_2D)
print(a2_2D.shape)
In [ ]:
# Por ejemplo un array 2D de una columna
a = np.arange(3)
a1_2D = a.reshape(3,1)
a2_2D = a[:, np.newaxis]
print(a1_2D)
print(a1_2D.shape)
print(a2_2D)
print(a2_2D.shape)

Broadcasting

Es poible realizar operaciones en ndarrays de diferentes tamaños. En algunos casos numpy puede transformar estos ndarrays automáticamente de forma que todos tienen la misma forma. Esta conversión automática se llama broadcasting.

Normas del Broadcasting

Para determinar la interacción entre dos ndarrays en Numpy se sigue un conjunto de reglas estrictas:

  • Regla 1: Si dos ndarrays difieren en su número de dimensiones la forma de aquel con menos dimensiones se rellena con 1's a su derecha.
  • Regla 2: Si la forma de dos ndarrays no es la misma en ninguna de sus dimensiones, el ndarry con forma igual a 1 en esa dimensión se 'alarga' para tener simulares dimensiones que los del otros ndarray.
  • Regla 3: Si en cualquier dimensión el tamaño no es igual y ninguno de ellos es igual a 1 entonces obtendremos un error.

Resumiendo, cuando se opera en dos ndarrays, numpy compara sus formas (shapes) elemento a elemento. Empieza por las dimensiones más a la izquierda y trabaja hacia las siguientes dimensiones. Dos dimensiones son compatibles cuando

ambas son iguales o
una de ellas es 1

Si estas condiciones no se cumplen se lanzará una excepción ValueError: frames are not aligned indicando que los ndarrays tienen formas incompatibles. El tamaño del ndarray resultante es el tamaño máximo a lo largo de cada dimensión de los ndarrays de partida.

De forma más gráfica:

numpy broadcasting in 2D (imagen extraída de aquí)

a:      4 x 3     a:      4 x 3      a:      4 x 1
b:      4 x 3     b:          3      b:          3
result: 4 x 3     result: 4 x 3      result: 4 x 3

Intentemos reproducir los esquemas de la imagen anterior.

In [ ]:
a = np.repeat((0, 10, 20, 30), 3).reshape(4, 3)
b = np.repeat((0, 1, 2), 4).reshape(3,4).T
print(a)
print(b)
print(a + b)
In [ ]:
a = np.repeat((0, 10, 20, 30), 3).reshape(4, 3)
b = np.array((0, 1, 2))
print(a)
print(b)
print(a + b)
In [ ]:
a = np.array((0, 10, 20, 30)).reshape(4,1)
b = np.array((0, 1, 2))
print(a)
print(b)
print(a + b)

ndarrays estructurados y recarrays

Antes hemos comentado que los ndarrays deben ser homogéneos pero era un poco inexacto, en realidad, podemos tener ndarrays que tengan diferentes tipos. Estos se llaman ndarrays estructurados y recarrays.

Veamos ejemplos:

In [ ]:
nombre = ['paca', 'pancracio', 'nemesia', 'eulogio']
edad = [72, 68, 86, 91]
a = np.array(np.zeros(4), dtype=[('name', '<S10'), ('age', np.int)])
a['name'] = nombre
a['age'] = edad
print(a)

Podemos acceder a las columnas por nombre

In [ ]:
a['name']

A todos los elementos menos el primero

In [ ]:
a['age'][1:]

Un recarray es similar pero podemos acceder a los campos con notación de punto (dot notation).

In [ ]:
ra = a.view(np.recarray)
In [ ]:
ra.name

Esto introduce un poco de overhead para acceder ya que se realizan algunas operaciones de más.

Concatenación y partición de ndarrays

Podemos combinar múltiples ndarrays en uno o separar uno en varios.

Para concatenar podemos usar np.concatenate, np.hstack, np.vstack, np.dstack. Ejemplos:

In [ ]:
a = np.array([1, 1, 1, 1])
b = np.array([2, 2, 2, 2])

Podemos concatenar esos dos arrays usando np.concatenate:

In [ ]:
np.concatenate([a, b])

No solo podemos concatenar ndarrays de una sola dimensión:

In [ ]:
np.concatenate([a.reshape(2, 2), b.reshape(2, 2)])

Podemos elegir sobre qué eje concatenamos:

In [ ]:
np.concatenate([a.reshape(2, 2), b.reshape(2, 2)], axis=1)

Podemos concatenar más de dos arrays:

In [ ]:
c = [3, 3, 3, 3]
np.concatenate([a, b, c])

Si queremos ser más explícitos podemos usar np.hstack o np.vstack. La h y la v son para horizontal y vertical, respectivamente.

In [ ]:
np.hstack([a, b])
In [ ]:
np.vstack([a, b])

Podemos concatenar en la tercera dimensión usamos np.dstack.

De la misma forma que podemos concatenar, podemos partir ndarrays usando np.split, np.hsplit, np.vsplit, np.dsplit.

In [ ]:
# Intentamos entender como funciona la partición probando...

Funciones matemáticas, funciones universales ufuncs y vectorización

¿Qué es eso de ufunc?

De la documentación oficial de Numpy:

A universal function (or ufunc for short) is a function that operates on ndarrays in an element-by-element fashion, supporting array broadcasting, type casting, and several other standard features. That is, a ufunc is a “vectorized” wrapper for a function that takes a fixed number of scalar inputs and produces a fixed number of scalar outputs.

Una ufunc es una Universal function o función universal que actúa sobre todos los elementos de un ndarray, es decir aplica la funcionalidad sobre cada uno de los elementos del ndarray. Esto se conoce como vectorización.

Por ejemplo, veamos la operación de elevar al cuadrado una lista en python puro o en numpy:

In [ ]:
# En Python puro
a_list = list(range(10000))

%timeit [i ** 2 for i in a_list]
In [ ]:
# En numpy
an_arr = np.arange(10000)

%timeit np.power(an_arr, 2)
In [ ]:
a = np.arange(10)

np.power(a, 2)

La función anterior eleva al cuadrado cada uno de los elementos del ndarray anterior.

Dentro de numpy hay muchísimas ufuncs y scipy (no lo vamos a ver) dispone de muchas más ufuns mucho más especializadas.

En numpy tenemos, por ejemplo:

  • Funciones trigonométricas: sin, cos, tan, arcsin, arccos, arctan, hypot, arctan2, degrees, radians, unwrap, deg2rad, rad2deg
In [ ]:
# juguemos un poco con ellas
  • Funciones hiperbólicas: sinh, cosh, tanh, arcsinh, arccosh, arctanh
In [ ]:
# juguemos un poco con ellas
  • Redondeo: around, round_, rint, fix, floor, ceil, trunc
In [ ]:
# juguemos un poco con ellas
  • Sumas, productos, diferencias: prod, sum, nansum, cumprod, cumsum, diff, ediff1d, gradient, cross, trapz
In [ ]:
# juguemos un poco con ellas
  • Exponentes y logaritmos: exp, expm1, exp2, log, log10, log2, log1p, logaddexp, logaddexp2
In [ ]:
# juguemos un poco con ellas
  • Otras funciones especiales: i0, sinc
In [ ]:
# juguemos un poco con ellas
  • Trabajo con decimales: signbit, copysign, frexp, ldexp
In [ ]:
# juguemos un poco con ellas
  • Operaciones aritméticas: add, reciprocal, negative, multiply, divide, power, subtract, true_divide, floor_divide, fmod, mod, modf, remainder
In [ ]:
# juguemos un poco con ellas
  • Manejo de números complejos: angle, real, imag, conj
In [ ]:
# juguemos un poco con ellas
  • Miscelanea: convolve, clip, sqrt, square, absolute, fabs, sign, maximum, minimum, fmax, fmin, nan_to_num, real_if_close, interp

...

In [ ]:
# juguemos un poco con ellas

Referencias:

Ufuncs

Estadística

  • Orden: amin, amax, nanmin, nanmax, ptp, percentile, nanpercentile
  • Medias y varianzas: median, average, mean, std, var, nanmedian, nanmean, nanstd, nanvar
  • Correlacionando: corrcoef, correlate, cov
  • Histogramas: histogram, histogram2d, histogramdd, bincount, digitize

...

In [ ]:
# juguemos un poco con ellas

Ordenando, buscando y contando

  • Ordenando: sort, lexsort, argsort, ndarray.sort, msort, sort_complex, partition, argpartition
  • Buscando: argmax, nanargmax, argmin, nanargmin, argwhere, nonzero, flatnonzero, where, searchsorted, extract
  • Contando: count_nonzero

...

In [ ]:
# juguemos un poco con ellas

Polinomios

  • Series de potencias: numpy.polynomial.polynomial
  • Clase Polynomial: np.polynomial.Polynomial
  • Básicos: polyval, polyval2d, polyval3d, polygrid2d, polygrid3d, polyroots, polyfromroots
  • Ajuste: polyfit, polyvander, polyvander2d, polyvander3d
  • Cálculo: polyder, polyint
  • Álgebra: polyadd, polysub, polymul, polymulx, polydiv, polypow
  • Miscelánea: polycompanion, polydomain, polyzero, polyone, polyx, polytrim, polyline
  • Otras funciones polinómicas: Chebyshev, Legendre, Laguerre, Hermite

...

In [ ]:
# juguemos un poco con ellas

Álgebra lineal

Lo siguiente que se encuentra dentro de numpy.linalg vendrá precedido por LA.

  • Productos para vectores y matrices: dot, vdot, inner, outer, matmul, tensordot, einsum, LA.matrix_power, kron
  • Descomposiciones: LA.cholesky, LA.qr, LA.svd
  • Eigenvalores: LA.eig, LA.eigh, LA.eigvals, LA.eigvalsh
  • Normas y otros números: LA.norm, LA.cond, LA.det, LA.matrix_rank, LA.slogdet, trace
  • Resolución de ecuaciones e inversión de matrices: LA.solve, LA.tensorsolve, LA.lstsq, LA.inv, LA.pinv, LA.tensorinv

Dentro de scipy tenemos más cosas relacionadas.

In [ ]:
# juguemos un poco con ellas

Manipulación de ndarrays

tile, hstack, vstack, dstack, hsplit, vsplit, dsplit, repeat, reshape, ravel, resize,...

In [ ]:
# juguemos un poco con ellas

Módulos de interés dentro de numpy

Dentro de numpy podemos encontrar módulos para:

  • Usar números aleatorios: np.random
  • Usar FFT: np.fft
  • Usar masked arrays: np.ma
  • Usar polinomios: np.polynomial
  • Usar álgebra lineal: np.linalg
  • Usar matrices: np.matlib
  • ...

Toda esta funcionalidad se puede ampliar y mejorar usando scipy.

Cálculo matricial

In [ ]:
a1 = np.repeat(2, 9).reshape(3, 3)
a2 = np.tile(2, (3, 3))
a3 = np.ones((3, 3), dtype=np.int) * 2
print(a1)
print(a2)
print(a3)
In [ ]:
b = np.arange(1,4)
print(b)
In [ ]:
print(a1.dot(b))
print(np.dot(a2, b))
print(a3 @ b) # only python version >= 3.5

Lo anterior lo hemos hecho usando ndarrays pero numpy también ofrece una estructura de datos matrix.

In [ ]:
a_mat = np.matrix(a1)
a_mat
In [ ]:
b_mat = np.matrix(b)
In [ ]:
a_mat @ b_mat
In [ ]:
a_mat @ b_mat.T

Como vemos, con los ndarrays no hace falta que seamos rigurosos con las dimensiones, en cambio, si usamos np.matrix como tipos hemos de realizar operaciones matriciales válidas (por ejemplo, que las dimensiones sean correctas).

A efectos prácticos, en general, los ndarrays se pueden usar como matrix conociendo estas pequeñas cosas.