<jupyter_start><jupyter_text>Taller de Python Capítulo 2:  Numérico y Laboratorio![taller_python](fig/logo_fifa.png)  _Bibliotecas_Ya hemos visto la vez anterior que python tiene varias funciones que vienen "de fábrica" como ```help()```, ```print()``` así también como operaciones básicas entre números como sumar, restar, etc; también vimos que nosotros podemos crear nustras propias funciones para que hagan lo que necesitemos usando la palabra clave ```def nombre_funcion```. Si uno necesita siempre realizar las mismas operaciones, reutilizará la misma función en todos sus códigos.Por ejemplo: Supongamos que uno quiere calcular ```cos(x)```. Python no viene por defecto con esa operación, uno debería crear un algoritmo (es decir, una serie de acciones) que calcule el valor del cos de x (cosa que puede ser no trivial), pero es _obvio_ que alguien ya lo hizo antes, alguien ya pensó el algoritmo, lo escribió, y lo utiliza diariamente, si esta persona subió su código a internet, todos podemos aprovechar y utilizarlo sin preocuparnos en cómo hizo esta persona!! Solamente hay que decirle a Python _donde_ es que está guardado esta función. **Esta posibilidad de usar algoritmos de otros es fundamental en la programación, porque es lo que permite que nuestro problema se limite solamente a entender cómo llamar a estos algoritmos ya pensados y no tener que pensarlos cada vez**.Vamos entonces a decirle a python que, además de sus operaciones de fábrica, queremos ampliar nuestro abanico de operaciones matemáticas en todas las opciones que aparecen dentro de la biblioteca "math" (una biblioteca es, tal como sugiere el nombre, un archivo con un montón de funciones, objetos y demás).<jupyter_code>import math #Importo a mi programa todo lo que este contenido dentro del archivo "math"<jupyter_output><empty_output><jupyter_text>Con esta linea Python entiende que queremos que traiga todo lo que está dentro del archivo "math" OJO: No todas las bibliotecas vienen instaladas por defecto, si queremos usar una muy rara es probable que tengamos que instalarla nosotros. Las que vamos a utilizar en el curso ya fueron instaladas automaticamente por Anaconda.Macanudo, ahora que Python trajo esa biblioteca, nosotros podemos acceder a su contenido usando la sintaxis ```math.lo_que_haya_dentro_de_math``` por ejemplo, ```math.cos()```<jupyter_code>math.cos(0)<jupyter_output><empty_output><jupyter_text>Hay una infinidad de bibliotecas o librerías dando vueltas por internet. Muchas veces el problema que queremos solucionar se reduce simplemente a encontrar la librería que tenga la funcionalidad correcta.Ahora continuaremos usando una muy utilizada en nuestro ámbito, la querídisima NumPy.  _NumPy_ : vectores, matrices y tablas de datosNumPy (NUMeric PYthon) es **LA** biblioteca para operar sobre vectores de números. Además de contener un nuevo [tipo de dato](http://docs.scipy.org/doc/numpy/reference/arrays.ndarray.html) que nos va a ser muy útil para representar vectores y matrices, nos provee de un arsenal de [funciones de todo tipo](https://docs.scipy.org/doc/numpy/reference/routines.html).Vamos a empezar por importar la bliblioteca _numpy_. La sintaxis típica de eso era `import biblio as nombre`:<jupyter_code>import numpy as np # con eso voy a poder acceder a las funciones de numpy a través de np.función()

# en ejemplo
print('El numero e = ', np.e)
#print(f"El numero e = {np.e}")
#print("El numero e = {}".format(np.e))

print('O el numero Pi = ', np.pi)
#print(f"O el numero Pi = {np.pi}")
#print("O el numero Pi = {}".format(np.pi))


# Podemos calcular senos y cosenos de numeros igual que con math!
print(np.sin(np.pi)) # casi cero! guarda con los floats!<jupyter_output>El numero e =  2.718281828459045
O el numero Pi =  3.141592653589793
1.2246467991473532e-16<jupyter_text>Todo eso está muy bien, pero lo importante de _numpy_ son los arrays numéricos, que van a ser como una lista de números pero con muchos esteroides. Los arrays numéricos nos van a servir para representar vectores (el objeto matemático de "la tira de números", no el físico) o columnas/tablas de datos (el objeto oriyinezco o de laboratorio).La idea es que es parecido a una lista: son muchos números juntos en la misma variable y están indexados (los puedo llamar de a uno dando la posición dentro de la variable). La gran diferencia con las listas de Python es que los arrays de _numpy_ operan de la forma que todos queremos:1. Si sumamos o restamos dos arrays, se suman componente a componente.2. Si multiplicamos o dividimos dos arrays, se multiplican o dividen componente a componente.Veamos ejemplos usando la función `np.array` para crear arrays básicos.<jupyter_code>array_1 = np.array([1, 2, 3, 4]) # array toma como argumento un vector_like (lista, tupla, otro array...vector_like)
array_2 = np.array([5, 6, 7, 8])

# Se acuerdan como antes pasabamos de un objeto tipo 'range' a un objeto tipo 'list' porque eran "parecidos"?
# Aca hacemos lo mismo. Pasamos de un objeto tipo 'list' a un objeto tipo 'ndarray'

print("ARRAYS:")
print(f"{array_1} + {array_2} =  {array_1 + array_2}") # aca los sumo
print(f"{array_1} * {array_2} = {array_1*array_2}") # acá los multiplico


# en el caso de las listas esto era muy molesto
lista_1 = [1, 2, 3, 4]
lista_2 = [5, 6, 7, 8]
print("LISTAS:")
print(f"{lista_2} + {lista_1} = {lista_1 + lista_2}") # sumar concatena
# print(l1 * l2) # esto ni siquiera se puede hacer!<jupyter_output>ARRAYS:
[1 2 3 4] + [5 6 7 8] =  [ 6  8 10 12]
[1 2 3 4] * [5 6 7 8] = [ 5 12 21 32]
LISTAS:
[5, 6, 7, 8] + [1, 2, 3, 4] = [1, 2, 3, 4, 5, 6, 7, 8]<jupyter_text>Y al igual que con las listas, uno puede acceder a elementos específicos de un array con su índice:<jupyter_code>print(array_1[0], array_1[1], array_1[2], array_1[3]) # son 4 elementos, los indices van del 0 al 3

# y más o menos vale todo lo que valía con listas
print(array_2[-1]) # agarro al último elemento de b
print(array_2[0:3]) # desde el primero hasta el 3 (no incluido el final, nunca se incluye)<jupyter_output>1 2 3 4
8
[5 6 7]<jupyter_text>También podemos iterar sobre ellos con _for_ , de las mismas formas que habíamos visto para iterar listas.<jupyter_code>print("array_1:")
for num in array_1:  # Aca itero sobre los ELEMENTOS de array_1
    print(num)

print('\n') # Esto es solo para que agregue una linea nueva y no queden pegados

print("array_2:")
for j in range(len(array_2)): # Aca itero sobre los INDICES de array_2
    print(array_2[j])<jupyter_output>array_1:
1
2
3
4


array_2:
5
6
7
8<jupyter_text>Para crear *arrays* de dos dimensiones (o más), podemos aplicar la función `np.array` sobre una *lista de listas*:<jupyter_code>c = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) # cada lista corresponde a una fila de la matriz
print(c)

# Aca hacer c[0] dara la primer FILA de la matrix. 
# Si queremos sacar la primer componente habra que hacer c[0, 0]<jupyter_output>[[1 2 3]
 [4 5 6]
 [7 8 9]]<jupyter_text>Para facilitar la vida del usuario _numpy_ viene con un montón de rutinas de creación de arrays típicos. En particular, matrices típicas como las identidades o de todos elementos iguales a 1 o 0 y arrays con cierta cantidad de elementos entre dos números (muy útil para crear dominios para gráficos).Veamos ejemplos de esos:<jupyter_code># Equiespaciados >> Elijo CANTIDAD DE PUNTOS en un dado intervalo [a, b]
equi_linspace = np.linspace(0, 1, 10) # 9 números equiespaciados linealmente entre 0 y 1 (LINear SPACE = linspace)
print('Numero de puntos fijo:', equi_linspace) # N-1 puntos entre [0, 1] = {0, 1/n, 2/n, 3/n, ... , n/n = 1}

print('\n')
# Equiespaciados >> Elijo EL PASO entre los puntos en un dado intervalo [a, b)
equi_arange = np.arange(0, 1, 1./10) # como el range de Python pero puede venir con un paso en coma flotante
print('Paso entre puntos fijo:', equi_arange) # {0, paso, 2*paso, ... n*paso} siempre que n*paso<1

print('\n Identidad de 3×3')
identidad = np.identity(3)
print(identidad)

#otros para que prueben ustedes
ceros = np.zeros((4, 4)) # todos ceros, matriz de 4x4
unos = np.ones((2,3)) # todos unos, matriz de 2x3
ojos = np.eye(5, k=0) # unos en la diagonal, como identidad
ojos2 = np.eye(5, k=2) # qué pasó acá?

print()
print(ceros)
print() 
print(unos)
print()
print(ojos)
print()
print(ojos2)<jupyter_output>Numero de puntos fijo: [0.         0.11111111 0.22222222 0.33333333 0.44444444 0.55555556
 0.66666667 0.77777778 0.88888889 1.        ]


Paso entre puntos fijo: [0.  0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9]

 Identidad de 3×3
[[1. 0. 0.]
 [0. 1. 0.]
 [0. 0. 1.]]

[[0. 0. 0. 0.]
 [0. 0. 0. 0.]
 [0. 0. 0. 0.]
 [0. 0. 0. 0.]]

[[1. 1. 1.]
 [1. 1. 1.]]

[[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.]]

[[0. 0. 1. 0. 0.]
 [0. 0. 0. 1. 0.]
 [0. 0. 0. 0. 1.]
 [0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0.]]<jupyter_text>Y antes de seguir, algo que siempre puede ser útil: los arrays tienen ciertas propiedades como su _shape_ (de cuánto por cuánto) y el _dtype_ (qué tipo de cosas tiene adentro). Podemos acceder a estos datos de la siguiente manera:<jupyter_code>x = np.linspace(0, 10, 1000) # ese array tiene 1000 elementos, andar printeando es poco práctico!

print(x.dtype) # Esto nos dice qué tipo de elemento es x (tener en cuenta que acá no van paréntesis)

ceros = np.zeros((100, 100)) # matriz de 100x100
print(ceros.shape) # np.shape nos dice cuántas filas y columnas tiene el array
# obs: esto es equivalente a:
# print(np.shape(ceros))

# Prueben qué pasa cuando le piden el shape a un array con una sola fila o columna como el x<jupyter_output>float64
(100, 100)<jupyter_text>--- Ejercicio 1Creen un vector con 20 valores entre -5 y 5 llamado ```dominio```, y un vector llamado ```imagen``` que contenga los valores de la función $x^2$ en el mismo intervalo.**Ejercicio 1 _bis_ :** creen otro vector que calcule el valor absoluto de ```dominio``` (Ayuda: googlear "numpy valor absoluto") --- Ejercicio 2La idea de este ejercicio es crear una matriz de 16x16 con los números del 1 al 256 siguiendo los pasos:1) Crear un array que contenga los valores 1, 2, ..., 2562) Modificar las dimensiones del array de 1×256 a 16×16*Ayuda*: siempre que puedan utilicen las funciones de creación de arrays de numpy. *Ayuda* bis: la función `np.reshape` hace lo que promete (reshapear) y puede tomar tuplas como argumento. Vean como usarla ya sea con su documentacion ```help(np.reshape)``` o con google. --- Graficar funciones A lo largo de nuestras carreras, y de la vida, nos encontramos con información que queremos **graficar**. Vamos a ver que con la ayuda de la biblioteca [```matplotlib```](https://matplotlib.org/) esto no es nada complicado.Esta librería es muy grande, por lo que tiene divididas sus funciones en distintos módulos. Para lo que vamos a ver en esta segunda parte nos alcanza con la sublibrería ```pyplot```. Una de las formas de importar esta librería es:```pythonimport matplotlib.pyplot as plt``` Podemos usar esta librería en conjunto con ```numpy``` para graficar las funciones que querramos.Veamos un ejemplo, grafiquemos la función $f(x) = \sin(x)\sin(20x)$ entre $0$ y $2\pi$ (no tienen por qué entender la función, es solo el logo de [Arctic Monkeys](https://c1.staticflickr.com/7/6126/5933658807_601e0b1751_b.jpg))<jupyter_code>import numpy as np
import matplotlib.pyplot as plt
# La siguiente linea va si estás usando un Notebook
%matplotlib inline 

# Definimos la función
def f(x): return np.sin(x)*np.sin(20*x)

# Definimos el dominio y la imagen
x = np.linspace(0, 2*np.pi, 500)
y = f(x)

# Graficamos la función
plt.plot(x, y)
plt.show() # Esta linea va si no estás usando un Notenook<jupyter_output><empty_output><jupyter_text>Desglosemos el código que acabamos de escribir:* Primero escribimos la función que queremos graficar.* Luego, definimos nuestro dominio ```x``` y nuestra imagen ```y```.* Y por último usamos la función de ```plot``` de ```matplotlib.pyplot``` para graficar ```y``` en función de ```x```.**Aclaración 1: plt.show() vs %matplotlib:**En este ejemplo pusimos estas dos lineas, pero son redudantes, solo una de ellas es necesaria. Si están trabajando en un Notebook, o en algo que use un Notebook de fondo, este puede trabajar con gráficos de forma interactiva, por lo que necesitan de la linea ```%matplotlib inline```. En el caso contrario, si están trabajando en un IDLE más usual, o ejecutando Python desde la terminal, necesitan usar la función ```plt.show()``` para mostrar los gráficos. No se vuelvan locos/as con esto, simplemente prueben cuál les funcina! (En Spyder puede funcionar cualquiera de las dos dependiendo de la configuración, pero por defecto se usa ```plt.show()```.)**Aclaración 2:** ```y``` no es una función. Sino que es un _array_ que contiene los números que resultan de aplicarle ```f``` a cada uno de los valores de ```x```. Lo que vemos arriba parece ser una _función continua_ pero en realidad es un conjunto de punto unidos con una línea. Para visualizar mejor esto podemos hacer un gráfico que muestre explícitamente los puntos que forman el gráfico:<jupyter_code>plt.plot(x, y, '.-');<jupyter_output><empty_output><jupyter_text>Qué hace plt.plot()?Esta función que parece mágica lo único que hace es tomar los vectores que le damos como inputs y graficar un punto en cada par de coordenadas $(x,y)$. Además, por defecto une estos puntos con lineas rectas. Por lo tanto, para que la función pueda hacer su trabajo, es necesario que los _array_ ```x``` e ```y``` tengan la misma longitud. Para dejar claro este concepto veamos un ejemplo con pocos puntos escritos a mano:<jupyter_code>coords_en_x = np.array( [0, 2, 6, 8,  10] )
coords_en_y = np.array( [2, 4, 6, 14, 14] )

plt.plot(coords_en_x, coords_en_y, 'o:r')
plt.grid() # Esto me deja poner una grilla detrás!<jupyter_output><empty_output><jupyter_text>Vemos que los puntos que se grafican son los $(x,y)$ = $\{(0,2);\,(2,4);\,(6,6);\,(8,14);\,(10,14)\}$  Dar formato y estética a los gráficosEn los gráficos anteriores cuando usamos ```plt.plot``` agregamos un término opcional para cambiar el color y la forma en la que se muestran los puntos que graficamos. Si usamos ```help(plt.plot)``` vemos que este argumento opcional es el _[fmt]_ y que toma distintos _strings_ con los que se puede cambiar la forma en que se ve el gráfico. La estructura general es```fmt = '[color][marker][line]'```donde _marker_ hace referencia a la forma de los puntitos y _line_ hace referencia a la forma de la linea.Los _strings_ válidos son un montón y los pueden ver en la documentación usando el _help_ o en la [documentación online](https://matplotlib.org/3.2.1/api/_as_gen/matplotlib.pyplot.plot.html) de la función.Acá dejamos algunas de las opciones para que se den una idea:* **color:** 'r' (rojo), 'b' (azul), 'g' (verde), 'm' (magenta), etc* **marker:** '.' (puntito chiquito), 'o' (punto grande), '^' (triángulo), '*' (estrella), etc* **line:** '-' (sólida), '--' (de a rayas), ':' (de a puntos) Título, labels y gridVeamos también algunas funciones que nos permiten agregar información a la figura sobre la que estamos trabajando. Veamos un ejemplo con la función de los [Arctic Monkeys](https://qph.fs.quoracdn.net/main-qimg-69d69b537bb5e7e3f21e44c36781a10d).<jupyter_code>import numpy as np
import matplotlib.pyplot as plt
# La siguiente linea solo es necesaria si estás usando Jupyter Notebook
%matplotlib inline 

# Definimos la función
def f(x): return np.sin(x)*np.sin(20*x)

# Definimos el dominio y la imagen
x = np.linspace(0, 2*np.pi, 500)
y = f(x)

# Graficamos la función con linea sólida y le ponemos una etiqueta
plt.plot(x, y, '-', label='Arctic Monkeys')

# Agregamos título y etiquetamos los ejes
plt.title('Logo de los Arctic Monkeys', fontsize=16)
plt.xlabel('Eje de $\hat x$')
plt.ylabel('Eje de $\hat y$')

plt.grid()
plt.legend()
plt.show()<jupyter_output><empty_output><jupyter_text>La mayoría de las funciones son bastante descriptivas. Veamos solo algunas aclaraciones:* El _fontsize_ en el ```plt.title``` es un argumento opcional y también se puede utilizar en el ```plt.xlabel``` o ```plt.ylabel``` por ejemplo.* El ```plt.grid()``` agrega una grid si no exite y la elimina si existe. Si se quiere forzar a que aparezca se puede utilizar ```plt.grid(True)```* El ```plt.legend()``` es el que permite que se muestren los labels asociados a las lineas (en este caso es el "Arctic Monkeys"). Esta función tiene algunos parámetros opcionales que pueden probar como el _fontsize_ o el _loc_. (ver ```help(plt.legend)```)* Podemos usar $LaTeX$ en los labels! --- Ejercicio 3* Graficar la función $f(x) = x^2$ entre $-5$ y $5$ (puedenreutilizar el código del Ejercicio 1)* Etiquetar la función, los ejes y ponerun título (pueden usar Latex si conocen la syntaxis)---  Graficar información guardada y ajustes Llevando los datos a PythonMuchas veces estamos acostumbrados a guardar los datos en hojas de Excel o en las Hoja de Cálculo de Drive. Python puede leer estas hojas utilizando las librerías adecuadas, pero es más sencillo, y es mejor práctica, exportar estos archivos en CSV para luego leerlos desde ahí. Por qué CSV* El formato **CSV (Comma Separated Values)** es abierto y un estándar a la hora de guardar datos de tipo fila o columna. Guardar los datos en Excel o Drive no está mal, pero se dificulta a la hora de pasarle los archivos a un/a colega, los CSV los puede abrir cualquiera! Inclusive, se pueden abrir Excel y Drive si se busca visualizarlo mejor.* Con pocos datos no hay problemas de _lag_ o *crasheos*, pero si son muchos datos: _"Microsoft Excel dejó de responder"_ .$^1$Acá una imagen de cómo descargar los archivos de Drive en CSV y de cómo se ven una vez en la compu. Es solo un archivo de texto con valores separados con comas, nada místico. ![](fig/export_drive_calc_to_csv.gif)$\scriptsize\text{1. Para archivos muchos muuuchos datos inclusive los CSV se pueden quedar corto, y para estos casos existen formatos específicos que dependen de la naturaleza de los datos.}$ Una vez tenemos el archivo que descargamos podemos leerlo desde Python utilizando numpy. En este caso, la linea es```pythondata = np.loadtxt('data_exp.csv', delimiter=',', skiprows=1, unpack=True)```Desglosemos esto por argumentos:1. El primer argumento es ubicación del archivo con la ruta respecto del archivo de Python. En este caso tenemos ambos archivos en la misma carpeta por lo que es solo el nombre.2. El segundo argumento, _delimiter_, recibe un _string_ que le indica a Python cómo están separados los valores en nuestro archivo. En este caso es por comas porque son archivos CSV.3. El tercer argumento es más opcional, y nos permite evitar que Python lea algunas lineas del archivo. En nuestro caso pusimos *1* ya que queremos evitar que lea la primera linea, que dice _"VAR 1, VAR 2"_.4. Por qué ```unpack=True```    ```np.loadtxt()``` por defecto te da los valores exactamente como están en el CSV, por lo que en este caso nos daría 2 columnas de datos con 20 filas. Utilizando ```unpack=True``` trasponemos los datos para pasar las columnas a filas, de modo que la primera fila (```data[0]```) sean los datos de _VAR 1_ y la segunda fila (```data[1]```) sean los valores de _VAR 2_. Acá el código utilizando ```unpack=True``` y sin utilizarlo        También podríamos importarlo sin usar el ```unpack=True``` y luego usar ```data = np.transpose(data)```<jupyter_code>data = np.loadtxt('data_exp.csv', delimiter=',', skiprows=1)
print(f"Data sin unpack:\n {data}")

data = np.loadtxt('data_exp.csv', delimiter=',', skiprows=1, unpack=True)
print(f"\nData con unpack:\n {data}")<jupyter_output>Data sin unpack:
 [[ 0.    1.93]
 [ 1.58  1.67]
 [ 3.16  1.3 ]
 [ 4.74  0.63]
 [ 6.32  1.26]
 [ 7.89  2.38]
 [ 9.47  2.91]
 [11.05  2.36]
 [12.63  3.93]
 [14.21  4.8 ]
 [15.79  4.83]
 [17.37  5.17]
 [18.95  6.21]
 [20.53  7.94]
 [22.11  9.12]
 [23.68 11.09]
 [25.26 12.57]
 [26.84 14.28]
 [28.42 17.1 ]
 [30.   20.56]]

Data con unpack:
 [[ 0.    1.58  3.16  4.74  6.32  7.89  9.47 11.05 12.63 14.21 15.79 17.37
  18.95 20.53 22.11 23.68 25.26 26.84 28.42 30.  ]
 [ 1.93  1.67  1.3   0.63  1.26  2.38  2.91  2.36  3.93  4.8   4.83  5.17
   6.21  7.94  9.12 11.09 12.57 14.28 17.1  20.56]]<jupyter_text>Graficar datos y hacer ajustesUna vez tenemos los datos en Python podemos graficarlos igual que lo hacíamos antes. Por ahora vamos a graficarlo sin agregar labels para que quede el código más cortito.<jupyter_code>import numpy as np
import matplotlib.pyplot as plt

# Importamos los datos
data = np.loadtxt('data_exp.csv', delimiter=',', skiprows=1, unpack=True)
var_x, var_y = data # es lo mismo que poner en dos lineas x = data[0] e y = data[1]

# Graficamos los datos crudos
plt.plot(var_x, var_y, 'o-k', label='Datos')

plt.legend()
plt.show()<jupyter_output><empty_output><jupyter_text>Los datos parecen tener un comportamiento exponencial. Para realizar un ajuste podemos usar la función ```curve_fit()``` que nos ofrece la librería/sublibrería ```scipy.optimize```. Veamos cómo funciona con un ejemplo:<jupyter_code># Importo la función desde la librería
from scipy.optimize import curve_fit

# Declaro la función con la que quiero ajustar con parámetros genéricos
def f_ajuste(x, A, C): return A*np.exp(C*x)

# Utilizo curve_fit() para el ajuste
popt, pcov = curve_fit( f_ajuste, var_x, var_y )

# Imprimo en pantalla los valores de popt y pcov
print(f'Parámetros óptimos para A y C (popt): {popt}')
print(f'\nMatriz de covariancia de popt (pcov):\n{pcov}')

# Graficamos los datos crudos
plt.plot(var_x, var_y, 'o-k', label='Datos')

# Graficamos el ajuste
A, C = popt
var_x_ajuste = var_x
var_y_ajuste = f_ajuste(var_x, A, C)
plt.plot(var_x_ajuste, var_y_ajuste, '-r', label='Ajuste')

# Legend y show
plt.legend()
plt.show()<jupyter_output>Parámetros óptimos para A y C (popt): [0.97349007 0.10119234]

Matriz de covariancia de popt (pcov):
[[ 4.92276143e-03 -1.89187399e-04]
 [-1.89187399e-04  7.51566980e-06]]<jupyter_text>Vemos que la función recibe tres inputs (obligatorios):* **El primer argumento es la función _modelo_ con la que queremos ajustar.**        La función modelo tiene que tener como primer argumento la variable que corresponde al eje $\hat x$. Luego se usan los otros argumentos para poner las constantes que queremos hallar (en nuestro caso A y C).  * **Los otros dos argumentos son nuestros datos**    Estos son los datos crudos, los que usamos para visualizarlos normalmente.     Outputs del ```curve_fit()```* **popt**        Vemos que ```curve_fit()``` nos devuelve como primer parámetro una lista con los valores óptimos para nuestro ajuste. En este caso son dos porque en la función modelo pusimos dos constantes (A y C). Estos valores vienen ordenados del mismo modo que están en la función modelo por lo que ```popt[0]``` es el A óptimo y ```popt[1]``` es el C óptimo.  * **pcov**        Esta es la matriz de covarianza de los resultados que nos da _popt_. Esta matriz nos da una idea de cuál es el error de este ajuste y de cuán ligados están estos errores entre sí. No es el objetivo del curso dar una clase de estadísitica, pero si conocemos que las variables son independientes podemos obtener el error de nuestros parámetros de ajuste como la raíz cuadrada de la covarianza. Para este ejemplo sería    ```python    err_A = np.sqrt(pcov[0,0])    err_C = np.sqrt(pcov[1,1])    ```    o    ```python    err_A, err_C = np.sqrt(np.diag(pcov))    ```   **Una cosita más:** En este caso pusimos ```var_x_ajuste = var_x```, lo que implica que el dominio para graficar nuestro ajuste tiene 20 puntos (los mismos que var_x), pero como conocemos la función podríamos hacer un linspace del estilo ```np.linspace( min(var_x), max(var_x), 1000 )``` para tener una mejor densidad de puntos. Esto esta bueno para hacer que los gráficos se vean menos acartonados, pero SOLAMENTE se puede usar a la hora de graficar.  Error y barras de error* Si ven la [documentación](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.curve_fit.html) de ```curve_fit()``` ven que como parámetro opcional también se le puede agregar el error de nuestros datos.* Si queremos poner barras de error a nuestro gráfico podemos usar ```plt.errorbar()```, que funciona de forma muy similar a ```plt.plot()``` pero nos permite dar un array con los errores o un error constante para todos. Guardar un gráficoUna vez tenemos un gráfico podemos guardarlo en el mismo directorio donde tenemos el archivo con el que estamos trabajando utilizando la linea```pythonplt.savefig('nombre_del_archivo.png')``` Ejemplo finalPerfecto, utilizando todo lo aprendido el código para gráfico completo y exportado sería:<jupyter_code>import numpy as np
import matplotlib.pyplot as plt
from scipy.optimize import curve_fit

# Importamos los datos
var_x, var_y = np.loadtxt('data_exp.csv', delimiter=',', skiprows=1, unpack=True)
err_var_y = 1.1 * np.ones(len(var_y)) # ponemos que todos los var_y tiene 1.1 de error a modo de ej

# Graficamos los datos crudos con su error
plt.errorbar(var_x, var_y, yerr=err_var_y, fmt='o-k', label='Datos')

# Calculamos el ajuste
def f_ajuste(x, A, C): return A*np.exp(C*x)
popt, pcov = curve_fit(f_ajuste, var_x, var_y, sigma=err_var_y)
A, C = popt

# Declaramos nuestro nuevo dominio e imagen y graficamos el ajuste
new_var_x = np.linspace(min(var_x), max(var_x), 1000)
new_var_y = f_ajuste(new_var_x, A, C)
plt.plot(new_var_x, new_var_y, '-r', label='Ajuste exponencial')

# Títuo y labels
plt.title('Datos y Ajuste Exponencial', fontsize=16)
plt.xlabel('Variable 1', fontsize=14)
plt.ylabel('Variable 2', fontsize=14)


# Grid, legend, save y show
plt.grid(True)
plt.legend()
plt.savefig('ajuste_exponencial.png')
plt.show()

# Printiamos en pantalla los parámetros óptimos con sus errores
err_A, err_C = np.sqrt(np.diag(pcov))
print(f'A: {A} ± {err_A}')
print(f'C: {C} ± {err_C}')<jupyter_output><empty_output><jupyter_text>--- Ejercicio 4La idea ahora es que hagan un _script_ similar al de arriba para un set de datos lineal. Dejamos una pequeña guía.1. Descargar el archivo en este [link](https://raw.githubusercontent.com/fifabsas/talleresfifabsas/master/python/2_Numerico/data_lineal.csv). Pueden hacerlo con click derecho -> "Guardar como..."2. Importen el archivo data_lineal.csv a Python (miren el archivo para ver cuántas filas tienen que saltarse)3. Grafiquen los datos crudos4. Realicen un ajuste lineal de los datos (tengan en cuenta que la f_ajuste ahora va a ser otra!)5. Grafiquen los datos crudos junto a el ajuste.6. Decoren el gráfico tanto como les guste y guárdenlo en la computadora.Este ejercicio es el más largo y complicado hasta ahora. Hay muchas cosas! No se preocupen si no llegan, después en su casa pueden ir paso a paso leyendo la guía, probando y preguntando cosas por el [canal de Discord](https://discord.gg/bN2KeTu).------  Acá termina nuestra clase. Si aún hay tiempo abajo tenemos armado algunos párrafos sobre distintas cosas piolas que podemos hacer. La elección esta en ustedes. (Si, se convirtió en un elija su propia aventura, no se la esperaba nadie esa)* Pandas y DataFrames* Histogramas* Sistemas de ecuaciones diferenciales* Algebra Lineal* Calculo Simbólico* Integración Numérica* Funciones Locas --- Pandas y DataFrames[Pandas](https://pandas.pydata.org/) es una librería bastante utilizada para Análisis de Datos y tiene [demasiadas](https://pandas.pydata.org/docs/user_guide/index.html) cosas disponibles, demasiadas. Aca en el curso no vamos a cubrir casi nada, sólo veremos algunas cosas.Pandas, tal como NumPy y SymPy, trae un nuevo tipo de dato muy genial, los DataFrames. Recomendado es el [tutorial](https://pandas.pydata.org/docs/getting_started/index.html) de 10 minutos que aporta la misma página de Pandas, en los que hace un paneo sobre las cosas básicas de la librería. Hay un [ejemplo práctico](https://github.com/fifabsas/talleresfifabsas/blob/master/python/Extras/Pandas_ejemplo.ipynb) hecho por la FIFA en el que se trabaja un poco los datos de inscripción de una jornada anterior del cursito este.Los DataFrames, el nuevo tipo de dato introducido, son básicamente tablas de datos donde cada columna tiene un nombre descriptivo. Podemos operar con las columnas por su nombre, y también elegir filas como haciamos con arrays antes.<jupyter_code>import pandas as pd

df = pd.DataFrame(np.random.randn(8, 3), columns=['A', 'B', 'Columna'])

print(df) # Esto lo muestra mas "crudamente"

print()

df  # Si solo escribimos esto la Notebook lo printea re cheto
    # pero solo la notebook (Y quizas la terminal de spyder)<jupyter_output>A         B   Columna
0  1.306630 -2.291737 -0.569453
1 -1.152830  0.732933  0.321360
2 -0.764704  0.196400 -0.936674
3  1.058185 -0.324949 -1.742166
4  0.330806  0.085614  0.438102
5  1.626899  0.383054  0.628802
6  1.001783 -0.187721  1.806652
7  0.802827 -0.100067  0.662701<jupyter_text>Además podemos acceder a cada columna con su nombre! Genial. De yapa nos dice el tipo de dato que esa columna contiene.<jupyter_code>print(df.A)
print()
print(df.Columna)<jupyter_output>0    1.306630
1   -1.152830
2   -0.764704
3    1.058185
4    0.330806
5    1.626899
6    1.001783
7    0.802827
Name: A, dtype: float64

0   -0.569453
1    0.321360
2   -0.936674
3   -1.742166
4    0.438102
5    0.628802
6    1.806652
7    0.662701
Name: Columna, dtype: float64<jupyter_text>Ya que está podemos indexear de otra forma y no con números del 0-7. Abajo mostramos una forma para indexear con fechas consecutivas. Además podemos tomar cachos del DataFrame igual que como hacíamos con vectores antes.<jupyter_code>dates = pd.date_range('20200404', periods=8)
df = pd.DataFrame({'Columna': np.random.randint(0,5,8),
                  'B': np.random.rand(8)},
                  index=dates)
print(df)
print()
print(df.loc[:, ['B']])
print()
print(df[0:4])
print()
print(df.sort_values(by='Columna'))<jupyter_output>Columna         B
2020-04-04        1  0.876051
2020-04-05        1  0.783758
2020-04-06        2  0.881846
2020-04-07        0  0.303063
2020-04-08        4  0.192155
2020-04-09        2  0.913549
2020-04-10        2  0.879073
2020-04-11        2  0.583418

                   B
2020-04-04  0.876051
2020-04-05  0.783758
2020-04-06  0.881846
2020-04-07  0.303063
2020-04-08  0.192155
2020-04-09  0.913549
2020-04-10  0.879073
2020-04-11  0.583418

            Columna         B
2020-04-04        1  0.876051
2020-04-05        1  0.783758
2020-04-06        2  0.881846
2020-04-07        0  0.303063

            Columna         B
2020-04-07        0  0.303063
2020-04-04        1  0.876051
2020-04-05        1  0.783758
2020-04-06        2  0.881846
2020-04-09        2  0.913549
2020-04-10        2  0.879073
2020-04-11        2  0.583418
2020-04-08        4  0.192155<jupyter_text>La librería también viene con una forma bastante poderosa para levantar archivos de un [montón](https://pandas.pydata.org/docs/user_guide/io.htmlcsv-text-files) de tipos distintos (incluidas cosas tipo excel, json, html, etc...), vamos a usar ```read_csv``` para levantar el archivo que vimos arriba de la exponencial.<jupyter_code>pd.read_csv("data_exp.csv")

#
# Escrito asi nomas dejamos que Pandas trate de leer el archivo lo mejor que pueda
# pero hay muchas opciones para levantar archivos mas "conflictivos", como para
# definir el separador entre datos, las lineas iniciales, tipo de datos, etc...
# esto puede verse desde la documentación de la librería o google.
#<jupyter_output><empty_output><jupyter_text>--- HistogramasUna cosita más que nos va a ser útil a la hora de dejar el Oriyin sin instalar es poder hacer histogramas. Con _pyplot_ eso lo podemos obtener de la función _hist_.Recordemos que en un histograma dividimos una serie de datos en rangos y contamos cuántos de nuestros datos caen en cada rango. A esos rangos se los llama _bins_._hist_ toma como argumentos un array de números, en cuántos _bins_ queremos dividir a nuestro eje x y algunas otras opciones de color como constante de normalización y color de las barras.Hagamos un histograma simple de un set gaussiano. Para eso, creemos datos sintéticos usando ```random.normal``` de _NumPy_ . Esto de crear datos lo hacemos acá a modo de ejemplo, en la vida real uno importaria algun dataset de las formas que ya hemos visto.<jupyter_code>import numpy as np
import matplotlib.pyplot as plt

# Distribución gaussiana centrada en 100, con 15 de desviación estándar
data = np.random.normal(100, 15, 2000) 

n, bins, patches = plt.hist(data, bins=20, edgecolor='black', facecolor='royalblue', alpha=0.75)
# n la variable n se encuentran los datos del histograma
# bins es un vector con los bordes de los rangos de datos
# patches no nos interesa en general

print(f'n:\n{n}\n')
print(f'bins:\n{bins}')
# Si lo quisieramos normalizar el area hay que agregar una opcion mas que es density=True como para 
# que entienda que queremos ver la "densidad de probabilidad", forma cheta para decir: normalizar el area.

n, bins, patches = plt.hist(data, bins=20, edgecolor='black', facecolor='royalblue', alpha=0.75, density=True)<jupyter_output><empty_output><jupyter_text>Y ya que estamos, para concientizar acerca de los peligros a la hora de la elección de *bins*, graficamos algunos histogramas superpuestos.<jupyter_code>n, bins, patches = plt.hist(data, bins=100, density=True, edgecolor='black', facecolor='royalblue', alpha=0.75)
n, bins, patches = plt.hist(data, bins=20 , density=True, edgecolor='black', facecolor='purple'  , alpha=0.5)
n, bins, patches = plt.hist(data, bins=2 , density=True, edgecolor='black', facecolor='indianred'  , alpha=0.3)<jupyter_output><empty_output><jupyter_text>Ejercicio 6La función `randn` que usamos nos brinda números aleatorios distribuidos de manera gaussiana (distribución normal).1. Haga el mismo ejercicio de recién pero con números aleatorios distribuidos de manera uniforme2. Lo mismo pero con números con una distribución pareto_Info:_ https://docs.scipy.org/doc/numpy-1.14.0/reference/routines.random.html --- Resolución de un sistema de ecuaciones diferenciales ordinarias (ODE) Vamos con un caso simple y conocido por la mayoría: el infaltable problema del péndulo. Arrancamos desde donde sabemos todos. (Donde sabemos todos? Si no saben de donde salió esto no se hagan problema, es solo algo físico, no lo podemos evitar)$$ \frac{d^2\theta}{dt^2} + \omega^2 \theta = 0$$con $\omega^2 = \frac{g}{l}$Para resolver numéricamente este problema, proponemos utilizar la función `odeint` de la biblioteca `scipy.integrate` (Otra opción podría ser la función `solve_ivp`, para problemas de valores iniciales), y esa función utiliza un método no taaaaan distinto al [método de Euler](https://es.wikipedia.org/wiki/M%C3%A9todo_de_Euler) para la solución de ODE's, (inserte conocimientos de Cálculo Numérico aquí, sí, dale, cursala antes de recibirte) por lo que, para un problema con derivadas de segundo orden debemos armar un sistema de ecuaciones de primer orden.Sea $\phi = \dot{\theta} \rightarrow \dot{\phi} = \ddot{\theta}$Nos queda entonces $$\begin{align}\dot{\theta} = \phi \\\dot{\phi} = -\omega^2 \theta\end{align}$$ o bien $$\begin{equation}\begin{pmatrix}\dot{\theta} \\ \dot{\phi}\end{pmatrix}= \begin{pmatrix}  0 & 1  \\   -\omega^2 & 0  \end{pmatrix}\begin{pmatrix}\theta \\ \phi \end{pmatrix}\end{equation}$$ o también$$ \dot{\vec{X}} = A \vec{X} $$Escencialmente, nuestro `odeint` va a intentar resolver ese sistema para distintos $t$ mientras le hayamos dado un valor inicial de donde comenzar. Nada demasiado extraño. Así que lo que la función va a necesitar es una función que tome como primer argumento $\vec{X}$, segundo argumento $t$ (por ser la variable independiente) y luego los demás parametros que necesite (en este caso sean $g$ y $l$) y opere para obtener $\dot{\vec{X}}$.<jupyter_code>from scipy.integrate import odeint
import numpy as np
def ecdif(X, t, g, l):
    theta, phi = X
    omega2 = g/l
    return [phi, -omega2 * theta]

g = 9.8
l = 2
X0 = [np.pi/4, 0] # Inicio a 45 grados con velocidad = 0
t = np.linspace(0, 10, 101)
solucion = odeint(ecdif, X0, t, args = (g,l))

plt.plot(t,solucion[:,0], label = 'theta')
plt.plot(t,solucion[:,1], label = 'phi')
plt.title('Resolucion del pendulo')
plt.xlabel('Tiempo')
plt.ylabel('Valores')
plt.grid(True)
plt.legend(loc = 'best')
plt.show()<jupyter_output><empty_output><jupyter_text>Si quieren ver otro ejemplo que esto, recomendamos el [resuelto](https://github.com/fifabsas/talleresfifabsas/blob/master/python/Extras/Fisica2/oscilador_forzado.py) que hizo un Ex-FIFA del oscilador con forzante sin aproximación y comparado con la solución analítica (sí, esa que sin aproximación no buscó nadie). También recomendamos, a cuento de esto, un pasito más en resolución de ODE's que es una [simulación](https://github.com/fifabsas/talleresfifabsas/blob/master/python/Extras/Fisica1/simulacion.ipynb) de uno de los ejercicios de F1, donde se arma una animación y una visualización más interactiva con la biblioteca `ipywidgets` de tantas otras posibles.  Ejercicio1. Resuelvan el oscilador armónico amortiguado con el parámetro $\gamma$2. Vuelvan a todas esas guías que nadie terminó, busquen los ejercicios con asterisco, métanlos en Python y aprecien el poder del cálculo numérico. --- Un poco de algebra lineal con *NumPy*NumPy trae muchas funciones para resolver problemas típicos de algebra lineal usando a los arrays como vectores. El que nos interesa en general es el de autovalores y autovectores y el sistema de ecuaciones lineales, pero empecemos con un ejemplo más fácil:<jupyter_code># cargamos el módulo de algebra lineal
from numpy import linalg
v = np.array([1, 1, 1])
w = np.array([2, 2, 2])
z = np.array([1, 0, 1])

norma =linalg.norm(v) # la norma 2 / módulo del vector v
print(norma)
print(np.sqrt(v[0]**2 + v[1]**2 + v[2]**2)) # calculado a mano == sqrt(3)
print(norma == np.sqrt(3)) # y numpy sabe que son lo mismo<jupyter_output>1.7320508075688772
1.7320508075688772
True<jupyter_text>Ahora si, usemos los vectores que creamos recién para crear una matriz y digamosle a _NumPy_ que calcule los autovectoresy autovalores de esa matriz:<jupyter_code>matriz = np.array([v, w, z], dtype=np.float64) 
#eig devuelve una tupla de arrays con los autovalores en un array 1D y los autovec en un array 2D
eigens = linalg.eig(matriz)

autvals, autvecs = eigens

print('Los autovalores:', autvals)
print()
print('Los autovectores:', autvecs)

#se terminó el problema<jupyter_output>Los autovalores: [ 3.41421356e+00 -1.23150120e-16  5.85786438e-01]

Los autovectores: [[ 4.39732612e-01  7.07106781e-01 -3.03890631e-01]
 [ 8.79465224e-01 -5.45825614e-17 -6.07781262e-01]
 [ 1.82143212e-01 -7.07106781e-01  7.33656883e-01]]<jupyter_text>Y para un sistema de ecuaciones del tipo $Ax = b$:<jupyter_code>mat = np.array([[1, 2, 5], [2, 5, 8], [4, 0, 8]], dtype=np.float64)
b = np.array([1, 2, 3])
x = linalg.solve(mat, b) #resuelve el sistema A*x = b
print(x)

#se terminó el problema<jupyter_output>[0.67857143 0.07142857 0.03571429]<jupyter_text>Por supuesto, también se puede hacer producto matriz con vector, y... oh si, se pueden calcular *inversas*.<jupyter_code>print(np.dot(mat,x))
print(linalg.inv(mat))<jupyter_output>[1. 2. 3.]
[[-1.42857143  0.57142857  0.32142857]
 [-0.57142857  0.42857143 -0.07142857]
 [ 0.71428571 -0.28571429 -0.03571429]]<jupyter_text>---  EjercicioParecen incrédulos. Fabriquen entonces la matriz $$\begin{equation}A = \begin{pmatrix}  0 & 1 \\  1 & 0  \end{pmatrix}\end{equation}$$cuyos autovalores son $\lambda_1 = 1$ y $\lambda_2 = -1 $, hallen sus autovalores y autovectores (a mano y con Python) y calculen $Ax$ con $$\begin{equation}x = \begin{pmatrix}1\\0\end{pmatrix}\end{equation}$$ --- EjercicioInventen una matriz de 5x5 (con el método que quieran y ¡que no sea la identidad!) y averigüen si es invertible. Si están prestando atención, saben que para resolver este problema les conviene ver la documentación de numpy.linalg.*Ayuda*: ¿Es necesario calcular la inversa? ¿Se acuerdan algo de álgebra lineal? --- Cálculo simbólico Ahora no sólo nos vamos a emancipar del *Oriyin*, sino también del bendito *Wolfram Alpha* (Disclaimer: si bien la librería que se usa es muy buena, todavía no llega a tener el nivel de software como Mathematica, Maple, etc). Así es, Python nos va a permitir hacer cálculos simbólicos, como las integrales que nunca supimos calcular. Todo eso y más, en el paquete **SymPy**:Hemos dicho que en general no es una buena práctica, pero este es uno de los pocos casos para los cuales se justifica importar toda la librería.<jupyter_code>from sympy import *
from sympy.abc import *<jupyter_output><empty_output><jupyter_text>Les daremos ahora un breve tour por algunas de las funciones que nos ofrece *SymPy*. Así como *numpy* nos introdujo el array como tipo de variable, las expresiones algebraicas en *sympy* son del tipo *symbols*. Estas pueden representar números enteros, reales, funciones, etc.A diferencia de las librerías anteriores, en las cuales usualmente escribimos los comandos dentro de un archivo a ejectutar todo junto (*script*); a modo de demostración utilizaremos *SymPy* de forma *interactiva*. Esta es posiblemente la manera en la que usaron *Wolfram Alpha* o *Mathematica* (para quienes lo hayan hecho).Una forma conveniente para empezar es utilizando el siguiente comando:<jupyter_code>init_session(use_latex='matplotlib') #el argumento nos permite renderear los resultados con el latex de matplotlib<jupyter_output>IPython console for SymPy 1.5.1 (Python 3.7.3-64-bit) (ground types: gmpy)

These commands were executed:
>>> from __future__ import division
>>> from sympy import *
>>> x, y, z, t = symbols('x y z t')
>>> k, m, n = symbols('k m n', integer=True)
>>> f, g, h = symbols('f g h', cls=Function)
>>> init_printing()

Documentation can be found at https://docs.sympy.org/1.5.1/<jupyter_text>Vimos aquí que una cantidad de variables se definieron como *symbols* de distintos tipos.Veamos ahora algunas de las posibilidades que tenemos:<jupyter_code>expand( (x + y)**5 )
factor( x**6 - 1 )<jupyter_output><empty_output><jupyter_text>Utilizando variables simbólicas de tipo integer, podemos hacer algunas sumatorias, por ejemplo, la famosa $$\sum_{k=0}^{m}k$$<jupyter_code>Sum(k, (k,0,m) ).doit().factor()  #el comando .doit() evalúa la suma<jupyter_output><empty_output><jupyter_text>O incluso algunas series, como $$ \sum_{n=1}^{\infty}\frac{1}{n^2} $$<jupyter_code>Sum(1/n**2, (n, 1, oo)).doit() #el infinito se escribe como oo (dos o minúscula)<jupyter_output><empty_output><jupyter_text>---Posiblemente si queremos algo de cálculo simbólico, sea para calcular derivadas e integrales que nos molesten. Veamos algunos ejemplos $\dfrac{\text{d}x^n}{\text{d}x}$<jupyter_code>diff(x**n , x).simplify()<jupyter_output><empty_output><jupyter_text>$$ \frac{d}{dx}\big(\ \sin(\tan(8x))\ \big)$$<jupyter_code>diff(sin(tan(8*x)), x)
out = diff(sin(tan(8*x)), x).simplify()
# Podemos hacer sustituciones de simbolos por otros simbolos!!
out.subs(x, sqrt(y))
# Hasta podemos sustituir por valores numéricos y evaluar la funcion!!
print(f"{out.subs(x, 8)} = {out.subs(x, 8).evalf()}")<jupyter_output>8*cos(tan(64))/cos(64)**2 = -36.5316624851842<jupyter_text>Si alguno desconfia, puede verlo desde [WolframAlfa](https://www.wolframalpha.com/input/?i=%288+%2B+8*tan%2864%29**2%29*log%288%29*cos%28tan%2864%29%29+%2B+sin%28tan%2864%29%29%2F8) ---Otro ejemplo! Derivadas parciales: $\dfrac{\partial^2}{\partial y \partial x}\left( x\sin{y} \right)$<jupyter_code>diff(x*sin(y), x, y) #qué pasa si en lugar de x,y ponemos y,y?<jupyter_output><empty_output><jupyter_text>También se pueden hacer integrales indefinidas o definidas con dominio infinito/acotado, obviamente luego se podría reemplazar en la expresión final por algun valor numérico, si así se quisiera$$\int \dfrac{1}{1+x^2}\text{d}x$$<jupyter_code>integrate(1/(1+x**2), x)<jupyter_output><empty_output><jupyter_text>$$\int_{-\infty}^{+\infty} \dfrac{\sin(x)}{x}\text{d}x$$<jupyter_code>integrate( sin(x)/x, (x,-oo,oo) )<jupyter_output><empty_output><jupyter_text>---Algo importantisimo y fabuloso de Sympy (algo que sorprendentemente NO tiene Mathematica ni Matlab) es que a cualquier expresión, podemos envolverla en un `latex()` para que devuelva la misma expresión que renderizó, pero en el formato de LaTeX. Derecho para mandar al informe o trabajo sin tipearla.<jupyter_code>out = diff(sin(tan(8*x))*log(x), x)
out
print(f"Normal: {out}")
print(f"LaTeX: {latex(out)}")<jupyter_output>Normal: (8*tan(8*x)**2 + 8)*log(x)*cos(tan(8*x)) + sin(tan(8*x))/x
LaTeX: \left(8 \tan^{2}{\left(8 x \right)} + 8\right) \log{\left(x \right)} \cos{\left(\tan{\left(8 x \right)} \right)} + \frac{\sin{\left(\tan{\left(8 x \right)} \right)}}{x}<jupyter_text>Ejercicio 9 Calculen la siguiente integral:  $$ \int_{-\infty}^{+\infty} e^{-x^2} \text{d}x $$ (opcional: verificar el resultado a mano!) --- Integración numéricaSiempre es útil tener a mano una rutina de integración numérica. Puede ser para el caso en el cual la integral analítica sea muy complicada o no estándar. También es fundamental para poder integrar datos obtenidos experimentalmente, sin asumir alguna función que los modele. Para ambos casos, el paquete relevante será `scipy.integrate`  A partir de una función predefinidaEn este caso, la función a importar es `quad`, porque usa un método de [cuadraturas](https://en.wikipedia.org/wiki/Numerical_integrationQuadrature_rules_based_on_interpolating_functions)<jupyter_code>from scipy.integrate import quad<jupyter_output><empty_output><jupyter_text>Definamos una función para ser integrada. A modo de ejemplo, calcularemos $$\int_a^b \dfrac{\text{e}^{-x^2 / 2}}{\sqrt{2\pi}}\text{d}x$$<jupyter_code>def integrando(x): return np.exp(-x**2 / 2)/np.sqrt(2*np.pi)<jupyter_output><empty_output><jupyter_text>Ahora, llamamos a `quad` para integrar esta función entre $a=-1$ y $b=1$<jupyter_code>a = -1
b = 1
integral, error = quad(integrando, a, b)
print(integral, error)<jupyter_output>0.682689492137086 7.579375928402476e-15<jupyter_text>Podemos, con este método, incluso obtener la primitiva numéricamente. Por ejemplo$$F(x) = \int_a^x \dfrac{\text{e}^{-t^2 / 2}}{\sqrt{2\pi}}\text{d}t$$<jupyter_code># Definimos este dominio
x = np.linspace(-5,5,100)

# Ahora obtenemos la primitiva
prim = np.zeros(x.size)

for i in range(x.size):
    prim[i], error = quad(integrando, a, x[i])<jupyter_output><empty_output><jupyter_text>Veamos la gráfica para este dominio<jupyter_code>plt.figure()

plt.plot(x, integrando(x), 'b', label = 'integrando')
plt.plot(x, prim, 'r', label = 'primitiva')
plt.grid(True)
plt.legend()<jupyter_output><empty_output><jupyter_text>--- Funciones especiales Tal vez en algún momento de la vida nos encontremos con funciones más exóticas que el seno, coseno y exponenciales, funciones que las conocemos de nombre pero de graficarlas ni hablemos. Por suerte, la biblioteca *scipy* cuenta con abanico muy grande de estas funciones con nombre propio. En la documentación (bloque de ayuda del spyder) podemos encontrar la lista completa de funciones, buscando `scipy.special`<jupyter_code>import scipy.special as sp<jupyter_output><empty_output><jupyter_text>Empecemos graficando un ejemplo común en probabilidad, la *función error*:<jupyter_code>import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline

x = np.linspace(-5,5,100) #generamos el dominio
y = sp.erf(x) #llama a la función error de la librería

plt.plot(x,y, label='función error')
plt.grid(True)
plt.legend(loc='best')
plt.show()<jupyter_output><empty_output><jupyter_text>Dentro de las muchas funciones que nos ofrece la librería, otro ejemplo importante en la física son las *funciones de Bessel* $J_{\nu}(x)$. Hay una función para cada valor del índice $\nu$ (que llamamos orden). En la librería, se llaman con el comando `jv(i,x)`, donde la primer entrada corresponde al orden $\nu$.Como demostración, veamos ahora algunos de sus gráficos:<jupyter_code>num = 500
r = np.linspace(0,10,num)

for i in range(5):
    y_i = sp.jv(i,r) # para cada i, grafica la función de orden i
    plt.plot(r,y_i,label='orden {}'.format(i))
    plt.legend(loc='best')
plt.grid(True)
plt.xlim(0, 10)
plt.title('Funciones de Bessel')<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.10673084903311478, 'fraction_numerical': 0.04805378039671342, 'mean_word_length': 4.6140943506115315, 'pattern_counts': {'":': 0, '<': 170, '<?xml version=': 0, '>': 192, 'https://': 20, 'lorem ipsum': 0, 'www.': 1, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 8, 'word_list_counts': {'cursed_substrings.json': 1, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '47', 'dir': 'jupyter-structured-clean-dedup', 'id': '249481', 'license': 'MIT', 'n_tokens_mistral': 19124, 'n_tokens_neox': 17694, 'n_words': 6278, 'path': 'python/2_Numerico/numerico.ipynb', 'repo_name': 'maurosilber/talleresfifabsas', 'size': '48196'}