<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 Introducción a Python
Step1:  Con esta línea, ya tendremos en nuestro pequeño script el paquete listo para ser usado. Para acceder a los módulos tendremos que hacer numpy.nombre_de_la_función. Esto no es que este mal, pero por así decirlo, el estandar es importar NumPy como se ve a continuación
Step2:  De esta forma, vuestro código pasa a ser más legible, el resto de gente y mas cómodo y rápido de escribir. Además de que por convenio, debes importar NumPy como np.
Arrays de NumPy
Numpy puede trabajar con arrays de $n$ dimensiones, pero los más comunes son los arrays de una, dos o tres dimensiones (1D, 2D, 3D). Además de esto, podemos establecer el tipo de los elementos del array a la hora de crearlo. Esto es muy importante, ya que si el array del tipo está determinado antes de ejecutar el script, Python no tiene que pararse a inferir el tipo de estos elementos, por lo que ganamos mucha velocidad.
Además de esto, ciertas operaciones en NumPy, o librerías que requieren el uso de NumPy, necesitan que los operandos sean de un determinado tipo.
A continuación vamos a crear una serie de arrays de distintas dimensiones a partir de una lista propia de Python
Step3:  Hay que tener encuenta que usar np.array requiere de un objeto ya existente, como en este caso son las listas utilizadas anteriormente. Este objeto es el que se inserta en el parámetro object. El otro parámetro más importante es dtype que nos sirve para indicar de qué tipo será el array que construyamos. Para más información sobre la creación de un array, puedes consultar la documentación oficial.
Además de crear arrays a partir de un objeto, NumPy nos ofrece la posibilidad de crear arrays "vacíos" o con valores predeterminados, gracias a las siguientes funciones
Step4:  Además de esto, podemos acceder a un elemento del array, podemos acceder de la misma manera que se hace en las listas de Python, usando el operador [].
Step5:  También podemos acceder a subconjuntos dentro del array usando el operador [] y el operador 
Step6:  Y usando estos dos mismos operadores, por ejemplo, en los arrays bidimensionales podemos acceder a todos los elementos de una fila o una columna de la siguiente forma
Step7:  Ahora que ya sabemos cómo manejarnos con los arrays de NumPy, cómo crearlos y acceder a la información de ellos, ya podemos empezar a realizar operaciones matemáticas con ellos.
Ejercicio
Gracias a las distintas formas de indexar un array que nos permite NumPy, podemos hacer operaciones de forma vectorizada, evitando los bucles. Esto supone un incremento en la eficiencia del código y tener un código más corto y legible. Para ello, vamos a realizar el siguiente ejercicio. 
Genera una matriz aleatoria cuadrada de tamaño 1000. Una vez creada, genera una nueva matriz donde las filas y columnas 0 y $n-1$ estén repetidas 500 veces y el centro de la matriz quede exactamente igual a la original. Un ejemplo de esto lo podemos ver a continuación
Step8:  También podemos encontrar funciones para calcular el coseno entre dos arrays, el producto vectorial, elevar a un exponente un array. Todas las funciones matemáticas las podemos encontrar en la documentación.
Otro módulo muy importante de NumPy es el de álgebra lineal, que podemos acceder a las funciones de este módulo haciendo np.linalg. Este módulo contiene operaciones como el producto vectorial de dos arrays, la descomposición en valores singulares, funciones para resolver sistemas de ecuaciones, etc. Una vez más, la documentación es nuestra amiga y en ella podemos encontrar toda la información sobre estas funciones, junto con ejemplos de uso.
Ejercicio
Una matriz de rotación $R$ es una matriz que representa una rotación en el espacio euclídeo. Esta matriz $R$ se representa como $$R=\left(\begin{matrix} \cos\theta & -\sin\theta \ \sin\theta & -\cos\theta \end{matrix}\right)$$ donde $\theta$ es el número de ángulos rotados en sentido antihorario.
Estas matrices son muy usadas en geometría, informática o física. Un ejemplo de uso de estas matrices puede ser el cálculo de una rotación de un objeto en un sistema gráfico, la rotación de una cámara respecto a un punto en el espacio, etc.
Estas matrices tienen como propiedades que son matrices ortogonales (su inversa y su traspuesta son iguales) y su determinante es igual a 1. Por tanto, genera un array y muestra si ese array es una matriz de rotación.
Ejercicio
Dados el array que se ve a continuación, realiza los siguientes apartados
Step9:  Multiplica array1 por $\frac{\pi}{4}$ y calcula el seno del array resultante.
Genera un nuevo array cuyo valor sea el doble del resultado anterior mas el vector array1.
Calcula la norma del vector resultante. Para ello, consulta la documentación para ver qué función realiza esta tarea, y ten en cuenta los parámetros que recibe.
Operaciones lógicas
Además de las funciones algebraicas y aritméticas, también podemos hacer uso de los operadores lógicos para comparar dos arrays. Por ejemplo, si queremos comparar elemento a elemento si son iguales dos arrays, usaremos el operador ==.
Step10:  Lo mismo sucede con los operadores de comparación &lt; y &gt;, que comparan elemento a elemento según el operador que hayamos decidido usar.
Step11:  Pero, en el caso de que queramos comparar si dos arrays son completamente iguales, devolviendo un único valor que sea True o False, debemos usar la función np.array_equal(array_1, array_2).
Step12:  También están disponibles los operadores lógicos &amp; y | para realizar operaciones lógicas con ellas. Sin embargo, NumPy también ofrece las funciones np.logical_and(), np.logical_or() para realizar estas operaciones y np.logical_not para negar el array.
Otras funciones a las que tenemos acceso son a funciones que nos ofrecen datos estadísticos como la media o la desviación típica, o el valor de la suma de una fila de un array, etc. A continuación, podemos ver algunas de las que ofrece
Step13:  Calcula la media y la desviación típica de la matriz.
Obtén el elemento mínimo y máximo de la matriz.
Calcula el determinante, la traza y la traspuesta de la matriz.
Calcula la descomposición en valores singulares de la matriz.
Calcula el valor de la suma de los elementos de la diagonal principal de la matriz.
Salvar arrays de NumPy en ficheros
Hay veces que en nuestro problema podemos generar matrices de un tamaño considerable, y que el tiempo de cómputo que hemos necesitado para obtener esa matriz es demasiado grande como para permitirnos el lujo de generarlo de nuevo, o simplemente no es posible reproducir los cálculos.
Es por ello que NumPy ofrece una solución, y es que nos permite el poder salvar estos arrays en ficheros gracias a las funciones save, savetxt, savez o savez_compressed.
* save
Step14:  Como podemos ver, ambas funciones dan el mismo resultado, así que, ¿para qué aprender a usar funciones lambda? Bueno, con estas funciones podemos hacer cosas como se ve a continuación
Step15:  ¿Qué ha pasado aquí? Pues bien, para empezar, esto es una de las ventajas de las funciones lambda, que podemos crear funciones anónimas en una sola línea, con un uso específico como acaba de pasar aquí, que no está asociada a ningún nombre en específico como pasa con las funciones def. Concretamente, lo que acaba de pasar es que hemos declarado una función lambda que recibe dos parámetros, $x$ e $y$, los suma y los multiplica por dos.
Acto seguido, hemos pasado como parámetros los dos números que van a realizar la operación, como parámetros de la función lambda, que se suman, se multiplican por dos y automáticamente se devuelven. 
Además, aquí podemos ver una de las diferencias con las funciones clásicas, y es que en las funciones lambda existe un return implícito. Es decir, una vez realizadas todas las operaciones que hay definidas en la función lambda automáticamente se devuelve este valor.
Otra diferencia es que las funciones lambda son expresiones, no sentencias, por lo que pueden aparecer en sitios donde por la sintaxis de Python, no puede aparecer un def. 
También tienen otras peculiaridades, como el que no se puede crear dentro de una función lambda un bloque if-else como el que estamos acostumbrados, sino que se tiene que expresar como vemos a continuación
Step16:  También son capaces de realizar bucles internos y llamar a funciones como map, usar las expresiones de comprensión de listas, etc.
Step17:  Cuándo usar funciones lambda
Uno de los mejores momentos para usar una función lambda, es el momento en el que vamos a usar una función para ordenar los elementos de una lista, siguiendo un orden en concreto. Este orden se puede definir usando una función, así que, qué mejor que usar una función lambda para establecer este orden y al ser una función anónima, no es necesario predefinirla. Vamos a ver un ejemplo de esto
Step18:  También podemos hacer que una función lambda actúe como una clausura léxica. Pero, ¿qué es una clausura? Una clausura léxica es un nombre para una función que recuerda los valores que se encuentran en un cierto espacio de nombres, incluso cuando el flujo de ejecución del programa no se encuentra en ese espacio de nombres. Vamos a ver un ejemplo de esto.
Step19:  Otro uso muy conocido de las funciones lambda es el uso de estas en las funciones filter o map, ya que podemos filtrar elementos que cumplan una serie de condiciones, haciendo uso de la Programación Funcional.
La programación funcional es un paradigma de programación que evalua las expresiones al igual que se hace en matemáticas, es decir, se evalua la expresión, se devuelve un resultado y los operandos quedan inmutables. Esto hace que dada una función $f$ que realiza una tarea determinada, siempre devuelva el mismo resultado si recibe como parámetro el elemento $x$. Esto es conocido como transparencia referencial. Esta programación funcional se basa mucho en el $\lambda$-calculus.
Python, aunque sea un lenguaje por naturaleza imperativo, tiene la posibilidad de emular esta programación funcional y emular también la transparencia referencial, usando las funciones lambda que hemos visto, y funciones como filter, map o reduce, que gracias a la programación funcional, nos permite hacer en una sola línea de código lo que haríamos en bastante más escribiendo de forma "clásica", y de forma más eficiente y elegante.
Función map
La función map recibe como parámetros una función y una lista o sequence. map aplica la función que recibe como entrada sobre la lista y devuelve un generador (en Python 2 devuelve una lista). Un generador es una estructura de Python que actúa como un iterador. Por ejemplo, al usar la función range(5), obtenemos un generador de números enteros de 0 a 5. Los elementos de este generador los podemos obtener todos del tirón haciendo list(range(5)) o bien, sacarlos de uno en uno en un bucle for i in range(5). Una vez generados estos elementos, desaparecen del generador (obviamente).
Vamos a ver un ejemplo a continuación, en el que usaremos la función map sin y con funciones lambda.
Step20:  Como puedes ver, el código usado escribiendo la función lambda es mucho menor que usando una función def y más limpio. Además, no es necesario definir una función y asociarla a un nombre para hacer esta tarea.
Además de esto, también podemos hacer una función map a varias listas a la vez. El único requisito es que estas listas sean de la misma longitud.
Step21:  Función filter
La función filter es la manera elegante de filtrar nuestras listas y eliminar aquellos elementos que no cumplan cierta condición. Al igual que map, recibe como parámetros una función y una sequence. Por ejemplo, vamos a extraer los elementos pares e impares de la sucesión de Fibonacci en listas separadas usando filter
Step22:  Función reduce
La función reduce se encarga de, tal y como dice su nombre, reducir una lista a un único valor, mediante una función que hayamos definido. A diferencia de las otras dos, esta función no se encuentra en el ámbito por defecto de Python, es decir, no podemos acceder a ella cargando simplemente el prompt de Python en Python 3.
Para poder usarla, tenemos que hacer lo siguiente
Step23:  Con esto, ya podemos utilizar la función reduce. Al igual que las anteriores, recibe como primer argumento una función que aplicar sobre la lista que recibe como segundo argumento. Vamos a ver un ejemplo de cómo calcular el factorial de un número en una sola línea.
<ASSISTANT_TASK:>
Python Code:
import numpy
Explanation: Introducción a Python: nivel intermedio
Python es un lenguaje muy extendido, con una rica comunidad que abarca muchos aspectos, muy fácil de aprender y programar con él, y que nos permite realizar un montón de tareas diferentes. Pero, no solo de pan vive. Python tiene muchas librerías o módulos que nos facilitan muchas tareas, que están respaldados por un gran número de personas detrás y que en muchos casos, realizan tareas específicas de una manera mucho más eficiente que haciéndolo puramente en Python.
Este es el caso de librerías como NumPy, Pandas, scikit-learn, Django y muchísimas más. 
Además de esto, aunque Python sea un lenguaje puramente imperativo, nos permite simular lo que se conoce como programación funcional, que viene directamente del Lambda Calculus, gracias al uso de las funciones lambda.
A continuación, veremos una introducción a NumPy que nos sirve como puerta de entrada a un montón de módulos más, y la programación funcional con las funciones lambda.
NumPy
NumPy es el paquete central que usaremos para tareas como ciencia de datos, o muchos cálculos matemáticos que requieren el uso de estructuras algebraicas como los vectores y matrices. Este paquete tiene la ventaja de que podemos trabajar cómodamente con la sintaxis de Python, creando estructuras $n$-dimensionales de forma sencilla y realizar complejas funciones sobre estas estructuras en una sola línea de código muy eficiente. 
Esta eficiencia se debe a que NumPy utiliza código C para realizar estos cálculos, que es mucho más rápido que la misma implementación en Python.
Además de esto, otras de las grandes ventajas de NumPy es la documentación oficial que tiene, que es una de las más completas y bien documentadas que hay, ya que cubren muy bien todos los aspectos de las distintas funciones que tiene, además de incorporar una gran cantidad de ejemplos.
Instalación
Para instalar NumPy tenemos que ejecutar la siguiente línea en nuestro terminal:
[braulio@braulio-PC ~]$ sudo pip install numpy
Con esto, gracias a pip, descargaremos e instalaremos NumPy. Así que, una vez instalados... a trabajar!!
Primeros pasos con NumPy
Para empezar a trabajar con NumPy, lo primero es importar el paquete en nuestro script.
End of explanation
import numpy as np
Explanation: Con esta línea, ya tendremos en nuestro pequeño script el paquete listo para ser usado. Para acceder a los módulos tendremos que hacer numpy.nombre_de_la_función. Esto no es que este mal, pero por así decirlo, el estandar es importar NumPy como se ve a continuación:
End of explanation
lista1D = [1, 5, 6, 79]
lista2D = [[2, 5.3, 0, -1.99], [14.5, 5, -5., 1]]
lista3D = [[[1, 5, 6, 79], [5, 7, 9, 0]], [[1, 5, 6, 79], [5, 7, 9, 0]]]
uni_dimensional = np.array(lista1D, dtype=np.int32) # Los elementos de este array serán enteros de 32 bits
bi_dimensional  = np.array(lista2D, dtype=np.float64) # En este caso, serán float de 64 bits
tri_dimensional = np.array(lista3D) # Y en este caso? 
print("Array 1D:\n", uni_dimensional)
print("Array 2D:\n", bi_dimensional)
print("Array 3D:\n", tri_dimensional)
Explanation: De esta forma, vuestro código pasa a ser más legible, el resto de gente y mas cómodo y rápido de escribir. Además de que por convenio, debes importar NumPy como np.
Arrays de NumPy
Numpy puede trabajar con arrays de $n$ dimensiones, pero los más comunes son los arrays de una, dos o tres dimensiones (1D, 2D, 3D). Además de esto, podemos establecer el tipo de los elementos del array a la hora de crearlo. Esto es muy importante, ya que si el array del tipo está determinado antes de ejecutar el script, Python no tiene que pararse a inferir el tipo de estos elementos, por lo que ganamos mucha velocidad.
Además de esto, ciertas operaciones en NumPy, o librerías que requieren el uso de NumPy, necesitan que los operandos sean de un determinado tipo.
A continuación vamos a crear una serie de arrays de distintas dimensiones a partir de una lista propia de Python:
End of explanation
x = np.arange(0.0,10.0,0.5)
print(x.ndim) # Nos muestra el número de dimensiones que tiene el array
print(x.size) # Muestra el número de elementos que tiene x
print(x.flags) # Nos muestra la información de los distintos flags de x
print(x.itemsize) # Nos muestra los bytes que ocupa un elemento de x
print(x.nbytes) # Nos muestra el número total de bytes que ocupan los elementos de x
Explanation: Hay que tener encuenta que usar np.array requiere de un objeto ya existente, como en este caso son las listas utilizadas anteriormente. Este objeto es el que se inserta en el parámetro object. El otro parámetro más importante es dtype que nos sirve para indicar de qué tipo será el array que construyamos. Para más información sobre la creación de un array, puedes consultar la documentación oficial.
Además de crear arrays a partir de un objeto, NumPy nos ofrece la posibilidad de crear arrays "vacíos" o con valores predeterminados, gracias a las siguientes funciones:
* np.ones: genera un array de unos.
* np.zeros: genera un array de ceros.
* np.random.random: genera un array de números aleatorios, float en el intervalo $[0,1]$.
* np.full: similar a np.ones con la diferencia de que todos los valores serán iguales al que reciba como parámetro.
* np.arange: genera un array con números dentro de un intervalo, con la frecuencia que queramos.
* np.linspace: similar al anterior, solo que fijamos el intervalo y el número de elementos que queremos.
* np.eye__o np.identity__: crean la matriz identidad.
Ejercicio
Ahora que hemos visto cómo crear arrays a partir de un objeto y otros para crear arrays con tipos prefijados, crea distintos arrays con las funciones anteriores para 1D, 2D y 3D e imprímelas por pantalla. Prueba a usar distintos tipos para ver cómo cambian los arrays. Si tienes dudas sobre cómo usarlos, puedes consultar la documentación oficial.
Una vez creado nuestros arrays, podemos encontrar información muy útil sobre ellos en los propios atributos del array. Estos atributos nos pueden dar información sobre el número de dimensiones, el espacio en memoria que ocupa cada elemento, etc. Los más comunes y el acceso a esta información podemos verlos a continuación:
End of explanation
array_1d = np.arange(-5,5,1)
array_1d[1]
Explanation: Además de esto, podemos acceder a un elemento del array, podemos acceder de la misma manera que se hace en las listas de Python, usando el operador [].
End of explanation
array_1d[3:6]
Explanation: También podemos acceder a subconjuntos dentro del array usando el operador [] y el operador : de la siguente forma:
End of explanation
array_random2d = np.random.random((4,6))
print("Matriz aleatoria:\n", array_random2d)
# Para acceder a todos los elementos de la segunda fila
print("Fila: \n", array_random2d[1])
# Para acceder a todos los elementos de la cuarta columna
print("Columna: \n", array_random2d[:,3])
# Filas de la segunda fila en adelante incluida esta
print("Filas: \n", array_random2d[1:])
# Columnas de la tercera columna en adelante
print("Columnas: \n", array_random2d[:,2:])
# Subconjuntos en filas y comunas
print("Subconjunto de columnas: \n", array_random2d[0:2,2:5])
Explanation: Y usando estos dos mismos operadores, por ejemplo, en los arrays bidimensionales podemos acceder a todos los elementos de una fila o una columna de la siguiente forma:
End of explanation
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
print("Suma usando el operador +:\n\t", a + b)
print("Suma usando np.add:\n\t", np.add(a,b))
Explanation: Ahora que ya sabemos cómo manejarnos con los arrays de NumPy, cómo crearlos y acceder a la información de ellos, ya podemos empezar a realizar operaciones matemáticas con ellos.
Ejercicio
Gracias a las distintas formas de indexar un array que nos permite NumPy, podemos hacer operaciones de forma vectorizada, evitando los bucles. Esto supone un incremento en la eficiencia del código y tener un código más corto y legible. Para ello, vamos a realizar el siguiente ejercicio. 
Genera una matriz aleatoria cuadrada de tamaño 1000. Una vez creada, genera una nueva matriz donde las filas y columnas 0 y $n-1$ estén repetidas 500 veces y el centro de la matriz quede exactamente igual a la original. Un ejemplo de esto lo podemos ver a continuación: $$\left(\begin{matrix} 1 & 2 & 3 \ 2 & 3 & 4 \ 3 & 4 & 5\end{matrix}\right) \Longrightarrow 
\left(\begin{matrix} 
1 & 1 & 1 & 2 & 3 & 3 & 3 \
1 & 1 & 1 & 2 & 3 & 3 & 3 \ 
1 & 1 & 1 & 2 & 3 & 3 & 3 \ 
2 & 2 & 2 & 3 & 4 & 4 & 4 \ 
3 & 3 & 3 & 4 & 5 & 5 & 5 \ 
3 & 3 & 3 & 4 & 5 & 5 & 5 \ 
3 & 3 & 3 & 4 & 5 & 5 & 5
\end{matrix}\right) $$ 
Impleméntalo usando un bucle for y vectorizando el cálculo usando lo anteriormente visto para ver la diferencias de tiempos usando ambas variantes. Para medir el tiempo, puedes usar el módulo time.
Operaciones con Arrays
Ahora que ya hemos visto cómo crear y manejar arrays, podemos pasar a ver cómo realizar operaciones aritméticas con ellos. NumPy tiene funciones como np.add, np.substract, np.multiply y np.divide para sumar, restar, multiplicar y dividir arrays. También podemos calcular el módulo entre dos arrays usando np.remainder. Pero, no es necesario usar estas funciones como tal, sino que podemos usar nuestros operadores aritméticos de siempre: +, -, *, / y %.
End of explanation
array1 = np.array([ -1., 4., -9.])
Explanation: También podemos encontrar funciones para calcular el coseno entre dos arrays, el producto vectorial, elevar a un exponente un array. Todas las funciones matemáticas las podemos encontrar en la documentación.
Otro módulo muy importante de NumPy es el de álgebra lineal, que podemos acceder a las funciones de este módulo haciendo np.linalg. Este módulo contiene operaciones como el producto vectorial de dos arrays, la descomposición en valores singulares, funciones para resolver sistemas de ecuaciones, etc. Una vez más, la documentación es nuestra amiga y en ella podemos encontrar toda la información sobre estas funciones, junto con ejemplos de uso.
Ejercicio
Una matriz de rotación $R$ es una matriz que representa una rotación en el espacio euclídeo. Esta matriz $R$ se representa como $$R=\left(\begin{matrix} \cos\theta & -\sin\theta \ \sin\theta & -\cos\theta \end{matrix}\right)$$ donde $\theta$ es el número de ángulos rotados en sentido antihorario.
Estas matrices son muy usadas en geometría, informática o física. Un ejemplo de uso de estas matrices puede ser el cálculo de una rotación de un objeto en un sistema gráfico, la rotación de una cámara respecto a un punto en el espacio, etc.
Estas matrices tienen como propiedades que son matrices ortogonales (su inversa y su traspuesta son iguales) y su determinante es igual a 1. Por tanto, genera un array y muestra si ese array es una matriz de rotación.
Ejercicio
Dados el array que se ve a continuación, realiza los siguientes apartados:
End of explanation
a = np.array([1, 2, 2, -1, 5])
b = np.array([0, 1, 2, 42, 5])
print(a == b)
Explanation: Multiplica array1 por $\frac{\pi}{4}$ y calcula el seno del array resultante.
Genera un nuevo array cuyo valor sea el doble del resultado anterior mas el vector array1.
Calcula la norma del vector resultante. Para ello, consulta la documentación para ver qué función realiza esta tarea, y ten en cuenta los parámetros que recibe.
Operaciones lógicas
Además de las funciones algebraicas y aritméticas, también podemos hacer uso de los operadores lógicos para comparar dos arrays. Por ejemplo, si queremos comparar elemento a elemento si son iguales dos arrays, usaremos el operador ==.
End of explanation
print("Operador <:\t", a < b)
print("Operador >:\t", a > b)
print("Operador <=:\t", a <= b)
Explanation: Lo mismo sucede con los operadores de comparación &lt; y &gt;, que comparan elemento a elemento según el operador que hayamos decidido usar.
End of explanation
print("¿Son iguales a y b?", np.array_equal(a, b))
Explanation: Pero, en el caso de que queramos comparar si dos arrays son completamente iguales, devolviendo un único valor que sea True o False, debemos usar la función np.array_equal(array_1, array_2).
End of explanation
n_array1 =  np.array([[ 1., 3., 5.], [7., -9., 2.], [4., 6., 8.]])
Explanation: También están disponibles los operadores lógicos &amp; y | para realizar operaciones lógicas con ellas. Sin embargo, NumPy también ofrece las funciones np.logical_and(), np.logical_or() para realizar estas operaciones y np.logical_not para negar el array.
Otras funciones a las que tenemos acceso son a funciones que nos ofrecen datos estadísticos como la media o la desviación típica, o el valor de la suma de una fila de un array, etc. A continuación, podemos ver algunas de las que ofrece:
* array.sum(): devuelve la suma total de los componentes del array.
* array.min(): devuelve el mínimo valor del array.
* array.max(): devuelve el valor máximo de la fila o la columna, dependiendo del valor que tenga el parámetro axis.
* array.cumsum(): nos devuelve un nuevo array donde cada elemento es la suma acumulativa de los elementos del array. Al igual que antes, es dependiente del parámetro axis.
* array.mean(): para obtener la media.
* array.median(): para obtener la mediana.
* np.std(array): para obtener la desviación típica del array.
Ejercicio
Dada la siguiente matriz, realiza los siguientes apartados:
End of explanation
# Definimos la función lambda de esta manera
suma_l = lambda x, y: x + y
print("Suma con lambda: ", suma_l(4,1))
def suma(x,y):
    return x+y
print("Suma con def: ", suma(4,1))
Explanation: Calcula la media y la desviación típica de la matriz.
Obtén el elemento mínimo y máximo de la matriz.
Calcula el determinante, la traza y la traspuesta de la matriz.
Calcula la descomposición en valores singulares de la matriz.
Calcula el valor de la suma de los elementos de la diagonal principal de la matriz.
Salvar arrays de NumPy en ficheros
Hay veces que en nuestro problema podemos generar matrices de un tamaño considerable, y que el tiempo de cómputo que hemos necesitado para obtener esa matriz es demasiado grande como para permitirnos el lujo de generarlo de nuevo, o simplemente no es posible reproducir los cálculos.
Es por ello que NumPy ofrece una solución, y es que nos permite el poder salvar estos arrays en ficheros gracias a las funciones save, savetxt, savez o savez_compressed.
* save: guarda el array en un fichero binario de NumPy. Este fichero tiene como formato .npy.
* savetxt: guarda el array en un fichero de texto.
* savez: guarda varios arrays en un mismo fichero sin comprimir. Este fichero tiene formato .npz.
* savez_compressed: similar al anterior pero en este caso el fichero estará comprimido. Su formato también es .npz.
Para más infomración y ejemplos de uso, siempre podréis consultar la documentación oficial.
Quiero más ejercicios para seguir entrenando con NumPy
Existe un repositorio en GitHub lleno de ejercicios que cubren un gran número de módulos y funciones de NumPy, con las soluciones a dichos ejercicios. Estos ejercicios, al igual que esta breve introducción, se encuentran en jupyter-notebooks que podemos descargar y manipular. Este repositorio se encuentra aquí. Y recordar que siempre que useis un repositorio en GitHub y os ha sido de utilidad, dadle star al repositorio.
Funciones lambda ($\lambda$)
Las funciones lambda se utilizan para declarar pequeñas funciones anónimas con un objetivo claro. Su comportamiento es parecido a las funciones que declaramos con def, aunque no son exactamente iguales.
Estas funciones lambda se asocian a una única expresión, por lo que elementos como return no están permitidos al igual que tampoco están permitidos los bloques de sentencias. Además de esto, las funciones lambda no tienen porque estar asociadas a un nombre en concreto, como lo hacen de forma obligatoria las funciones definidas con def. Vamos a ver un ejemplo a continuación.
En este ejemplo, vamos a hacer una función que recibe como parámetros dos elementos y devuelve la suma de ellos. Pero, vamos a definirlo primero como una función lambda y luego con una función clásica con def.
End of explanation
(lambda x, y: 2*(x+y))(5,4)
Explanation: Como podemos ver, ambas funciones dan el mismo resultado, así que, ¿para qué aprender a usar funciones lambda? Bueno, con estas funciones podemos hacer cosas como se ve a continuación:
End of explanation
get_min = lambda x, y: x if x < y else y
print("Mínimo entre 2 y 5: ", get_min(2,5))
Explanation: ¿Qué ha pasado aquí? Pues bien, para empezar, esto es una de las ventajas de las funciones lambda, que podemos crear funciones anónimas en una sola línea, con un uso específico como acaba de pasar aquí, que no está asociada a ningún nombre en específico como pasa con las funciones def. Concretamente, lo que acaba de pasar es que hemos declarado una función lambda que recibe dos parámetros, $x$ e $y$, los suma y los multiplica por dos.
Acto seguido, hemos pasado como parámetros los dos números que van a realizar la operación, como parámetros de la función lambda, que se suman, se multiplican por dos y automáticamente se devuelven. 
Además, aquí podemos ver una de las diferencias con las funciones clásicas, y es que en las funciones lambda existe un return implícito. Es decir, una vez realizadas todas las operaciones que hay definidas en la función lambda automáticamente se devuelve este valor.
Otra diferencia es que las funciones lambda son expresiones, no sentencias, por lo que pueden aparecer en sitios donde por la sintaxis de Python, no puede aparecer un def. 
También tienen otras peculiaridades, como el que no se puede crear dentro de una función lambda un bloque if-else como el que estamos acostumbrados, sino que se tiene que expresar como vemos a continuación:
End of explanation
import sys
pinta = lambda x: list(map(sys.stdout.write, x))
lista = pinta(['Un\n', 'Dos\n', 'Tres\n'])
bucle = lambda x: [i for i in range(0,10) if i%x==0]
bucle(2)
Explanation: También son capaces de realizar bucles internos y llamar a funciones como map, usar las expresiones de comprensión de listas, etc.
End of explanation
ini = list(range(-10, 10,))
print("Lista inicial con range:\t", ini)
# ¿Y si la queremos ordenada de esta manera?
# [0, -1, 1, -2, 2, ...]
sorted_list = sorted(range(-10, 10), key=lambda x: x**2)
print("Lista ordenada con lambda:\t", sorted_list)
Explanation: Cuándo usar funciones lambda
Uno de los mejores momentos para usar una función lambda, es el momento en el que vamos a usar una función para ordenar los elementos de una lista, siguiendo un orden en concreto. Este orden se puede definir usando una función, así que, qué mejor que usar una función lambda para establecer este orden y al ser una función anónima, no es necesario predefinirla. Vamos a ver un ejemplo de esto:
End of explanation
def suma_n(n):
    return lambda x: x+n
suma5 = suma_n(5)
suma10 = suma_n(10)
print("Suma 5: ", suma5(5))
print("Suma 10: ",suma10(5))
Explanation: También podemos hacer que una función lambda actúe como una clausura léxica. Pero, ¿qué es una clausura? Una clausura léxica es un nombre para una función que recuerda los valores que se encuentran en un cierto espacio de nombres, incluso cuando el flujo de ejecución del programa no se encuentra en ese espacio de nombres. Vamos a ver un ejemplo de esto.
End of explanation
from numpy import pi
# Definimos nuestra lista que queremos transformar
degrees = [180, 250, 0, 18, 214, ]
# Pasar de grados a radianes
def deg2rad(deg):
    return deg * pi/180
rads_generator = map(deg2rad, degrees)
print("Lista transformada: ", list(rads_generator))
# En caso de que queramos extraerlos de uno en uno:
# for rads in rads_generator:
#     print(rads)
rads_generator_lambda = map(lambda x: x*pi/180, degrees)
print("Lista transformada con lambda: ", list(rads_generator_lambda))
Explanation: Otro uso muy conocido de las funciones lambda es el uso de estas en las funciones filter o map, ya que podemos filtrar elementos que cumplan una serie de condiciones, haciendo uso de la Programación Funcional.
La programación funcional es un paradigma de programación que evalua las expresiones al igual que se hace en matemáticas, es decir, se evalua la expresión, se devuelve un resultado y los operandos quedan inmutables. Esto hace que dada una función $f$ que realiza una tarea determinada, siempre devuelva el mismo resultado si recibe como parámetro el elemento $x$. Esto es conocido como transparencia referencial. Esta programación funcional se basa mucho en el $\lambda$-calculus.
Python, aunque sea un lenguaje por naturaleza imperativo, tiene la posibilidad de emular esta programación funcional y emular también la transparencia referencial, usando las funciones lambda que hemos visto, y funciones como filter, map o reduce, que gracias a la programación funcional, nos permite hacer en una sola línea de código lo que haríamos en bastante más escribiendo de forma "clásica", y de forma más eficiente y elegante.
Función map
La función map recibe como parámetros una función y una lista o sequence. map aplica la función que recibe como entrada sobre la lista y devuelve un generador (en Python 2 devuelve una lista). Un generador es una estructura de Python que actúa como un iterador. Por ejemplo, al usar la función range(5), obtenemos un generador de números enteros de 0 a 5. Los elementos de este generador los podemos obtener todos del tirón haciendo list(range(5)) o bien, sacarlos de uno en uno en un bucle for i in range(5). Una vez generados estos elementos, desaparecen del generador (obviamente).
Vamos a ver un ejemplo a continuación, en el que usaremos la función map sin y con funciones lambda.
End of explanation
a = [1, 2, 3, 4]
b = [5, 6, 7, 8]
result = list(map(lambda x, y: 2*(x+y), a, b))
print("Resultado: ", result)
Explanation: Como puedes ver, el código usado escribiendo la función lambda es mucho menor que usando una función def y más limpio. Además, no es necesario definir una función y asociarla a un nombre para hacer esta tarea.
Además de esto, también podemos hacer una función map a varias listas a la vez. El único requisito es que estas listas sean de la misma longitud.
End of explanation
def Fibonacci(n):
    a, b = 0, 1
    yield a
    yield b
    i = 0
    while i < n-2:
        a, b = b, a + b
        yield b
        i+=1
pybonacci = list(Fibonacci(1000))
print(pybonacci[0:50], '...', pybonacci[-10:])
# Obtenemos los pares
even = list(filter(lambda x: x%2==0, pybonacci))
# Y los impares
odd = list(filter(lambda x: x%2!=0, pybonacci))
print("Impares:\n", odd[:50], "...", odd[-3])
print("Pares:\n", even[:50], "...", even[-3])
Explanation: Función filter
La función filter es la manera elegante de filtrar nuestras listas y eliminar aquellos elementos que no cumplan cierta condición. Al igual que map, recibe como parámetros una función y una sequence. Por ejemplo, vamos a extraer los elementos pares e impares de la sucesión de Fibonacci en listas separadas usando filter:
End of explanation
from functools import reduce
Explanation: Función reduce
La función reduce se encarga de, tal y como dice su nombre, reducir una lista a un único valor, mediante una función que hayamos definido. A diferencia de las otras dos, esta función no se encuentra en el ámbito por defecto de Python, es decir, no podemos acceder a ella cargando simplemente el prompt de Python en Python 3.
Para poder usarla, tenemos que hacer lo siguiente:
End of explanation
n = 10
fact = reduce(lambda x,y:x*y, range(1,n+1))
print("Factorial de un 10: ", fact)
Explanation: Con esto, ya podemos utilizar la función reduce. Al igual que las anteriores, recibe como primer argumento una función que aplicar sobre la lista que recibe como segundo argumento. Vamos a ver un ejemplo de cómo calcular el factorial de un número en una sola línea.
End of explanation
<END_TASK>