<jupyter_start><jupyter_text>Introducción a las ciencias de la computación *y programación en Python* *Numerical Python*: NumPy <NAME>  Abstract>  "*I do not fear computers. I fear lack of them.*" **<NAME>**- Paquete de computación científica NumPy    - Introducción        - Conceptos básicos de utilización        - Ejercicios  NumPy- Paquete de Python para **computación científica** de **alto desempeño** sobre vectores, matrices y estructuras de mayores dimensiones (tensores).- Implementado en C y Fortran, por lo que cuando se **vectorizan** las oepraciones, el desempeño es muy bueno.- Ofrece una estructura de datos `ndarray` para procesar de forma **eficiente** datos homogéneos.- Capacidades de algebra lineal, transformada de Fourier y generación de números aleatorios.  Pero, ¿y las listas de Python?- Las listas son muy generales, pueden contener cualquier tipo de objeto.    - Implementan tipos dinámicos.        - No permiten operaciones matemáticas como la *multiplicación matricial* o *producto punto*.    - Los vectores de NumPy son de tipado estático y homogéneo. El tipo del arreglo se determina cuando se crea el vector.    - Son eficientes en memoria.        - Es posible implementar **funciones universales** (ufuncs).         - El tipado estático permite implementar operaciones matemáticas de forma eficiente a través de C y Fortran.      Scientific computing benchmark ![julia-benchmarks.svg](attachment:julia-benchmarks.svg)[5 Reasons You Should Know NumPy](https://insights.dice.com/2016/09/01/5-reasons-know-numpy/)  Importando NumPy<jupyter_code>import numpy as np<jupyter_output><empty_output><jupyter_text>- Revisamos la versión de NumPy<jupyter_code>np.__version__<jupyter_output><empty_output><jupyter_text>Obteniendo ayuda<jupyter_code># Obtiene una ventana de ayuda
np.ndarray?
# Función help para una función
help(np.array)<jupyter_output><empty_output><jupyter_text>*** La clase `ndarray` - La clase básica de NumPy se llama `ndarray` (alias `array`). Sus atributos más importantes son:     - `ndarray.ndim` : número de dimensiones del arreglo.    - `ndarray.shape` : dimensiones del arreglo. Esta es una **tupla de enteros** que indica el tamaño de la matriz en cada dimensión. Para una matriz con $n$ filas y $m$ columnas, la forma será $(n, m)$. La longitud de la tupla de forma es, por lo tanto, el número de ejes, `ndim`.    - `darray.size` : el número total de elementos del arreglo. Es igual al producto de los elementos de la tupla `shape`.    - `ndarray.dtype` : un **objeto** que describe el **tipo** de los elementos en el arreglo. Es posible crear y especificar `dtypes` propios.        - Adicionalmente, NumPy provee algunos tipos propios muy utilizados: `numpy.int32`, `numpy.int16`, and `numpy.float64`.    - `ndarray.itemsize` : el tamaño en bytes de cada elemento en el arreglo. Por ejemplo, un arreglo de tipo `float64` tiene tamaño en bytes de $8 (=64/8)$. Equivalente a `ndarray.dtype.itemsize`.    - `ndarray.data` : el búfer que contiene los elementos del arreglo. Aunque normalmente, no accedemos al atributo directamente, ya que hacemos uso de los elementos con *slicing*.<jupyter_code>a = np.arange(15).reshape(3, 5)
a
a.shape
a.ndim
a.dtype
a.dtype.name
a.itemsize
a.size
type(a)<jupyter_output><empty_output><jupyter_text>*** **Creando arreglos N-dimensionales de NumPy**- Hay varias formas de inicializar nuevas matrices numpy, por ejemplo desde    - Una lista de Python o tuplas.        - Utilizando funciones dedicadas a **generar** arreglos de NumPy.        - Leer información de archivos.<jupyter_code>a = np.array([2,3,4])
a
a.dtype
b = np.array([1.2, 3.5, 5.1])
b
b.dtype<jupyter_output><empty_output><jupyter_text>Error frecuenteCrear el arreglo con varios argumentos en vez de proveer una sola lista o secuencia como argumento<jupyter_code># Error frecuente
a = np.array(1,2,3,4)<jupyter_output><empty_output><jupyter_text>A partir de listas o tuplas `ndarray` transforma secuencias de secuencias en matrices bidimensionales, secuencias de secuencias de secuencias en matrices tridimensionales, etc.<jupyter_code>b = np.array([[1.5,2,3], [4,5,6]])
b<jupyter_output><empty_output><jupyter_text>El tipo de matriz también se puede especificar explícitamente en el momento de la creación:<jupyter_code>c = np.array( [ [1,2], [3,4] ], dtype=complex )
c<jupyter_output><empty_output><jupyter_text>Funciones de generación de arreglos<jupyter_code>np.arange(10, 30, 5)
# A diferencia de range
np.arange(0, 2, 0.3)<jupyter_output><empty_output><jupyter_text>Evaluación de funciones<jupyter_code>x = np.linspace(0, 2*np.pi, 20)
x
y = np.sin(x)
y<jupyter_output><empty_output><jupyter_text>Variables aleatorias<jupyter_code>b = np.random.random((5, 5))
b<jupyter_output><empty_output><jupyter_text>Unos, ceros<jupyter_code>np.zeros([10, 10])
np.ones([5, 5])
# Copiando la forma de x
np.ones_like(x)
np.identity(5)<jupyter_output><empty_output><jupyter_text>Desde archivos<jupyter_code>data = np.loadtxt("data.csv", delimiter=',', skiprows=1)
data<jupyter_output><empty_output><jupyter_text>*** Impresión de arreglosCuando imprimes una matriz, NumPy la muestra de forma similar a las listas anidadas, pero con el siguiente diseño:- el último eje se imprime de izquierda a derecha,- el penúltimo se imprime de arriba a abajo,- el resto también se imprime de arriba a abajo, con cada corte separado del siguiente por una línea vacía.Las matrices unidimensionales se imprimen como filas, bidimensionales como matrices y tridimensionales como listas de matrices.<jupyter_code>a = np.arange(6)
print(a)
b = np.arange(12).reshape(4,3)
print(b)
c = np.arange(24).reshape(2,3,4) 
print(c)
# Si es muy grande, se suprime parte de la salida
print(np.arange(10000))
print(np.arange(10000).reshape(100,100))<jupyter_output>[[   0    1    2 ...   97   98   99]
 [ 100  101  102 ...  197  198  199]
 [ 200  201  202 ...  297  298  299]
 ...
 [9700 9701 9702 ... 9797 9798 9799]
 [9800 9801 9802 ... 9897 9898 9899]
 [9900 9901 9902 ... 9997 9998 9999]]<jupyter_text>*** Operaciones básicasLas operaciones básicas son aplicadas elemento a elemento (*elementwise*). Se crea un nuevo arreglo lleno con el resultado<jupyter_code>a = np.array( [20,30,40,50] )
b = np.arange(4)
a, b
c = a - b
c<jupyter_output><empty_output><jupyter_text>`*` opera en sentido elemento a elemento sobre los arreglos.<jupyter_code># Constante por arreglo
10 * np.sin(a)
# Arreglos lógicos
a < 35<jupyter_output><empty_output><jupyter_text>La multiplicación matricial puede hacerse utilizando `@` o el método `dot`<jupyter_code>A = np.array( [[1, 1], [0, 1]] )
B = np.array( [[2, 0], [3, 4]] )
A * B
A @ B
A.dot(B)
np.matmul(A, B)
b = np.random.random((2,3))
b<jupyter_output><empty_output><jupyter_text>Operadores `+=` y `*=` son válidos para modificar arreglos existentes.<jupyter_code>a = np.ones((2,3), dtype=int)
a *= 3
a
b = np.random.random((2,3))
b +=a 
b<jupyter_output><empty_output><jupyter_text>Métodos útiles<jupyter_code>a = np.random.random((2, 3))
a
a.sum() # equivalente a np.sum(a)
a.min()
a.max()<jupyter_output><empty_output><jupyter_text>Por defecto operan sobre el arreglo como si fueran una lista de números. Es posible especificar el eje sobre el cuál se desea aplicar la operación con el parámetro `axis`<jupyter_code>b = np.arange(12).reshape(3,4)
b
# Suma de las columnas 
b.sum(axis=0)   
# Mínimo de cada fila
b.min(axis=1)
# Suma acumulada en las columnas 
b.cumsum(axis=1)<jupyter_output><empty_output><jupyter_text>*** Funciones universales (*ufuncs*)<jupyter_code>a = np.arange(5)
a
np.exp(a)
np.sqrt(a)<jupyter_output><empty_output><jupyter_text>Podemos definir funciones de usuario que sean universales: $$ f(x) = 2x + e^x - \sqrt{x} $$<jupyter_code>def f(x):
    return 2*x + np.exp(x) - np.sqrt(x)
f(a)<jupyter_output><empty_output><jupyter_text>*** Indexing, Slicing and IteratingLos arreglos unidimensionales se pueden indexar, dividir e iterar, al igual que las listas y otras secuencias de Python.<jupyter_code>a = np.arange(10)**3
a
a[2]
a[2:5]
# Asignación múltiple
a[0:6:2] = -1000
a
for i in a:
    print(i**(1/3.))
def f(x,y):
    return 10*x + y
b = np.fromfunction(f, (5,4), dtype=int)
b
b[2, 3]
b[0:5, 1]
b[:, 1]<jupyter_output><empty_output><jupyter_text>Cuando se proporcionan menos índices que el número de ejes, los índices faltantes se consideran segmentos completos `:`<jupyter_code># Ultima fila, todas las columnas
b[-1]
np.ones((5,4,3,2,1))<jupyter_output><empty_output><jupyter_text>La iteración sobre matrices multidimensionales se realiza con respecto al primer eje<jupyter_code>for row in b:
    print(row)<jupyter_output>[0 1 2 3]
[10 11 12 13]
[20 21 22 23]
[30 31 32 33]
[40 41 42 43]<jupyter_text>Sin embargo, si se desea realizar una operación en cada elemento de la matriz, se puede usar el atributo `flat` que es un iterador sobre todos los elementos de la matriz:<jupyter_code>for element in b.flat:
    print(element)
b.flatten()<jupyter_output><empty_output><jupyter_text>*** Manipulación del `shape` (forma) del arregloUna matriz tiene una forma dada por el número de elementos a lo largo de cada eje:<jupyter_code>a = np.floor ( 10 * np.random.random (( 3 , 4 )))
a
a.shape<jupyter_output><empty_output><jupyter_text>La forma de una matriz se puede cambiar con varios comandos. Tenga en cuenta que los siguientes tres comandos devuelven una matriz modificada, pero **no cambian la matriz original**:<jupyter_code>a.ravel()  # returns the array, flattened
a.flatten()
a.reshape(6,2)  # returns the array with a modified shape
a.T
a.T.shape<jupyter_output><empty_output><jupyter_text>- El orden de los elementos en la matriz resultante de `ravel()` es normalmente "estilo `C`", es decir, el índice de la derecha "cambia más rápido", por lo que el elemento después de un `[0,0]` es un `[0,1]`. - Si la matriz se reforma a otra forma, nuevamente la matriz se trata como "estilo `C`". NumPy normalmente crea matrices almacenadas en este orden, por lo que `ravel()` generalmente no necesitará copiar su argumento, pero si la matriz se hizo tomando segmentos de otra matriz o se creó con opciones inusuales, es posible que deba copiarse. - Las funciones `ravel()` y `reshape()` también se pueden instruir, utilizando un argumento opcional, para usar matrices de estilo FORTRAN, en las que el índice más a la izquierda cambia más rápido. La función de `reshape` devuelve su argumento con una forma modificada, mientras que el método `ndarray.resize` modifica la matriz en sí:<jupyter_code>a
a.resize((2, 6))
a<jupyter_output><empty_output><jupyter_text>*** Apilando los arreglos Se pueden apilar varios arreglos a lo largo de diferentes ejes:<jupyter_code>a = np.floor(10*np.random.random((2,2)))
a
b = np.floor(10*np.random.random((2,2)))
b
# np.vstack pide una tupla de ndarrays 
np.vstack((a,b))
# np.hstack pide una tupla de ndarrays 
np.hstack((a,b))
np.column_stack((a,b)) # Equivalente a np.hstack para arreglos 2D
np.hstack((a,b))<jupyter_output><empty_output><jupyter_text>- Cuando los arreglos son 1D, `np.column_stack()` y `np.hstack()` se comportan diferente<jupyter_code>a = np.array([4.,2.])
b = np.array([3.,8.])
np.column_stack((a,b))
np.hstack((a,b))<jupyter_output><empty_output><jupyter_text>- Podemos crear nuevas dimensiones en el arreglo con `np.newaxis` o con `None`<jupyter_code>a[:, np.newaxis]
a[:, None]
np.column_stack((a[:, np.newaxis], b[:, np.newaxis]))<jupyter_output><empty_output><jupyter_text>- Por otro lado, la función `row_stack` es equivalente a `vstack` para cualquier arreglo de entrada. De hecho, `row_stack` es un alias para `vstack`<jupyter_code>np.row_stack is np.vstack<jupyter_output><empty_output><jupyter_text>- En general, para arreglos con más de dos dimensiones:        - `hstack` apila a lo largo del segundo eje,         - `vstack` apila a lo largo del primer eje y         - `concatenate` permite argumentos opcionales que dan el número del eje (`axis`) a lo largo del cual debe ocurrir la concatenación. *** Separar un arreglo en varios más pequeños<jupyter_code>a = np.floor(10*np.random.random((2,12)))
a
np.hsplit(a,3)   # Split a into 3
np.hsplit(a, (3,4))   # Split a after the third and the fourth column
help(np.hsplit)<jupyter_output>Help on function hsplit in module numpy:

hsplit(ary, indices_or_sections)
    Split an array into multiple sub-arrays horizontally (column-wise).
    
    Please refer to the `split` documentation.  `hsplit` is equivalent
    to `split` with ``axis=1``, the array is always split along the second
    axis regardless of the array dimension.
    
    See Also
    --------
    split : Split an array into multiple sub-arrays of equal size.
    
    Examples
    --------
    >>> x = np.arange(16.0).reshape(4, 4)
    >>> x
    array([[  0.,   1.,   2.,   3.],
           [  4.,   5.,   6.,   7.],
           [  8.,   9.,  10.,  11.],
           [ 12.,  13.,  14.,  15.]])
    >>> np.hsplit(x, 2)
    [array([[  0.,   1.],
           [  4.,   5.],
           [  8.,   9.],
           [ 12.,  13.]]),
     array([[  2.,   3.],
           [  6.,   7.],
           [ 10.,  11.],
           [ 14.,  15.]])]
    >>> np.hsplit(x, np.array([3, 6]))
    [array([[  0.,   1.,   2.],
           [  4.,   5.,   6[...]<jupyter_text>*** Mutabilidad y clonadoAl operar y manipular arreglos, los datos a veces se copian en un nuevo arreglo y otras no. Esto es a menudo una fuente de confusión para los principiantes. Hay tres casos:<jupyter_code>a = np.arange( 12 )
b = a
b is a
b.shape = 3, 4
a.shape<jupyter_output><empty_output><jupyter_text>- Cortar una matriz devuelve una vista de ella:<jupyter_code>a
s = a [:, 1:3]
s
s is a<jupyter_output><empty_output><jupyter_text>Copia profunda El método de copy hace una copia completa de la matriz y sus datos.<jupyter_code># a new array object with new data is created
d = a.copy()
d
d is a
d == a<jupyter_output><empty_output><jupyter_text>*** Rutinas de NumPy[Rutinas y funcionalidad](https://numpy.org/devdocs/reference/routines.htmlroutines) *** Para usuarios ***pro*** (como ustedes)  *Broadcasting* (difusión)- Este mecanismo permite que las funciones universales manejen de manera significativa entradas que no tienen exactamente la misma forma.[General Broadcasting Rules](https://numpy.org/devdocs/user/basics.broadcasting.html) - Un ejemplo:<jupyter_code>x = np.arange(4)
x
xx = x.reshape(4,1)
xx
x + xx
x.shape
xx.shape<jupyter_output><empty_output><jupyter_text>- El *broadcasting* proporciona una forma conveniente de tomar el producto externo (o cualquier otra operación externa) de dos matrices. El siguiente ejemplo muestra una operación de adición externa de dos matrices 1D:<jupyter_code>a = np.array([0.0, 10.0, 20.0, 30.0])
b = np.array([1.0, 2.0, 3.0])
a[:, None] + b<jupyter_output><empty_output><jupyter_text>Aquí, `None` inserta un nuevo eje en `a`, haciéndolo un arreglo bidimensional de `4x1`. Al combinar este arreglo de `4x1` con `b`, que tiene forma `(3,)`, resulta un arreglo de `4x3`  ¿Por qué funciona?- Cuando se operan dos arreglos, NumPy compara sus formas (`shape`) elemento a elemento, empezando por la última dimensión y hacia arriba. - Dos dimensiones son compatibles cuando:        - Son iguales, o        - Una de ellas es 1    - Cuando alguna de las dos dimensiones es uno, **se utiliza la otra**.     - En otras palabras, las dimensiones con tamaño 1 son "estiradas" o "copiadas" para ajustarse a la otra. *** *Fancy indexing* y trucos de indexación- NumPy ofrece más funciones de indexación que las secuencias regulares de Python. Además de la indexación por enteros y sectores, como vimos antes, las matrices se pueden indexar por matrices de enteros y matrices de booleanos.<jupyter_code>a = np.arange(11)**2
a
i = np.array( [ 1,1,3,8,5 ] )
i
a[i]   # the elements of a at the positions i
j = np.array([[ 3, 4], [ 9, 7 ]])  # a bidimensional array of indices
j
a[j] # the same shape as j<jupyter_output><empty_output><jupyter_text>- También podemos dar índices para más de una dimensión. Las matrices de índices para cada dimensión deben tener la misma forma.<jupyter_code>a = np.arange(12).reshape(3,4)
a
i = np.array([[0,1], [1,2]])
j = np.array([[2,1], [3,3]])
i
j
a[i,j] # i and j must have equal shape
a[i,2]
a[:,j]<jupyter_output><empty_output><jupyter_text>*** Búsqueda del máximo<jupyter_code>data = np.sin(np.arange(20)).reshape(5,4)
data
# index of the maxima for each series
ind = data.argmax(axis=0)
ind<jupyter_output><empty_output><jupyter_text>***  Asignación múltiple con indexación<jupyter_code>a = np.arange(5)
a
a[[1,3,4]] = 0
a
# Se queda la última asignación
a = np.arange(5)
a[[0,0,2]] = [1,2,3]
a<jupyter_output><empty_output><jupyter_text>*** Indexado con arreglos booleanos- Cuando indexamos arreglos con otros arreglos de índices (enteros), proporcionamos la lista de índices para elegir. Con los índices booleanos, el enfoque es diferente; **elegimos explícitamente qué elementos de la matriz queremos y cuáles no**.La forma más natural en la que uno puede pensar para la indexación booleana es usar matrices booleanas que tengan la misma forma que la matriz original:<jupyter_code>a = np.arange(12).reshape(3,4)
a
b = a > 4
b
a[b]<jupyter_output><empty_output><jupyter_text>- También se puede utilizar en asignaciones:<jupyter_code>a[b] = 0
a<jupyter_output><empty_output><jupyter_text>*** Álgebra lineal<jupyter_code>a = np.array([[1.0, 2.0], [3.0, 4.0]])
a
a.transpose()
np.linalg.inv(a)
np.eye(2)
j = np.array([[0.0, -1.0], [1.0, 0.0]])

j @ j
y = np.array([[5.], [7.]])
y
np.linalg.solve(a, y)
# eigenvalores y eigenvectores
np.linalg.eig(a)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.11987704918032786, 'fraction_numerical': 0.02959927140255009, 'mean_word_length': 4.627482383087764, 'pattern_counts': {'":': 0, '<': 185, '<?xml version=': 0, '>': 198, 'https://': 3, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '46', 'dir': 'jupyter-structured-clean-dedup', 'id': '175025', 'license': 'MIT', 'n_tokens_mistral': 6894, 'n_tokens_neox': 6628, 'n_words': 2029, 'path': 'jupyter-notebooks/12-numpy/12-numpy.ipynb', 'repo_name': 'r2cp/PES-Programacion-I', 'size': '17600'}