<jupyter_start><jupyter_text>PLOTS, FILES Y UN POCO MÁS...Ya que he introducido el proceso para generar numeros, calcular otros numeros derivados de estos con funciones e incluso impartir condiciones sobre estos, es necesario también presentar una forma de visualizar los resultados. Es por esto que en esta clase se presentará un *abrebocas* a  graficar con python, más específicamente con la librería **matplotlib**.Otro concepto importante a considerar en esta clase es leer y escribir archivos con python. Esto es indispensable en la mayoría de los usos que se le dan a python en un contexto científico y permite hacer tareas que van desde graficar un set de datos hasta hacer análisis estadísticos de datos y alimentar programas con información en tiempo real del mundo exterior.Antes, vamos a ver unas cuantas utlidades más de Python :  UTILIDADES InputLa utilidad **input** es usada para que el usuario del script pueda introducir valores al programa sin necesidad de alterar el código. **Importante** remarcar que el ususario del script (quien le da un uso) no es necesariamente el progrmaador (quien creo el código y la lógica asociada). Poniendolo en práctica, si se ejecuta dentro de un script o un Notebok el siguiente comando : `````pythoninput("Introduce tu nombre : ")`````Aparecerá en consola o en el espacio de salida del Notebook el siguiente mensaje:````Introduce tu nombre : ___________________________````Donde el espacio de la línea continua es editable. Una vez introduzco mi nombre en la línea y oprimo "Enter", me retorna el valor que introduje.<jupyter_code>input("Introduce tu nombre : ")<jupyter_output>Introduce tu nombre : PaipaPsyche<jupyter_text>Como se puede inferir, **input** es una función y esta a su vez retorna un valor, por lo cual se puede almacenar en otra variable. **Importante** tener en cuenta que la salida de la función **input** es una cadena de caracteres o **string**, por lo cual se debe tratar correctamente antes de usarse en un cálculo.<jupyter_code>variable_a  = int(input("Introduce un valor enetero : "))
print("El triple de ese valor es ",3*variable_a)<jupyter_output>Introduce un valor enetero : 20
El triple de ese valor es  60<jupyter_text>Se usa el cambio de tipo a **int** para evitar inconvenientes de este tipo:<jupyter_code>variable_a  =input("Introduce un valor enetero : ")  # no se cambia el tipo a int
print("El triple de ese valor es ",3*variable_a)<jupyter_output>Introduce un valor enetero : 20
El triple de ese valor es  202020<jupyter_text>**Otro ejemplo :**<jupyter_code>variable_a  = int(input("Introduce un valor enetero : "))
variable_b  = int(input("Introduce un valor enetero : "))
print("La suma de estos valores es ",variable_a + variable_b)<jupyter_output>Introduce un valor enetero : 45
Introduce un valor enetero : 60
La suma de estos valores es  105<jupyter_text>assertLos **assert** se usan para hacer afirmaciones que deben ser ciertas para continuar con el programa. Estas declaraciones son útiles para detectar errores en el código o notificar errores por mal manejo del usuario. A esta afirmación se puede asociar un mensaje de error que saltará en consola cuando el **assert** NO se cumpla:<jupyter_code>variable_a  = 100

assert variable_a>50 , "La variable no es mayor a 50"
print("Todo en orden")
assert variable_a<50 , "La variable no es menor a 50"
print("Todo en orden")<jupyter_output><empty_output><jupyter_text>Nótese que en el último caso NO se imprimió el mensaje *"Todo en orden"* puesto que el **AssertionError** saltó primero y detuvo la ejecución del código.  break y continue Muchas veces en un loop se está aplicando alguna operación sobre los valores del loop, ya sean elementos de un arreglo o números, y se quieren omitir algunos valores o se quiere detener el loop si alguna condición se cumple. En estos casos es preciso usar declaraciones como **continue** para hacer un salto de iteración y  **break** para poner un alto al loop.**Por ejemplo**, quiero iterar sobre los numeros naturales del 0 al 20 omitiendo los numeros divisibles en 5 (Es decir, que su residuo de division en 5, o módulo 5, es cero).<jupyter_code>for  i in range(20): #para los numeros del 0 al 40
    print("inicio iteracion")
    if(i%5 == 0): #si el numero es divisible por 5
        continue # continue a la siguiente iteracion (elsiguiente numero)
    else:  # si no 
        print(i) # imprima el numero
    
    print("iteracion exitosa") # independientemente del "if", imprimir este mensaje

print("FIN")<jupyter_output>inicio iteracion
inicio iteracion
1
iteracion exitosa
inicio iteracion
2
iteracion exitosa
inicio iteracion
3
iteracion exitosa
inicio iteracion
4
iteracion exitosa
inicio iteracion
inicio iteracion
6
iteracion exitosa
inicio iteracion
7
iteracion exitosa
inicio iteracion
8
iteracion exitosa
inicio iteracion
9
iteracion exitosa
inicio iteracion
inicio iteracion
11
iteracion exitosa
inicio iteracion
12
iteracion exitosa
inicio iteracion
13
iteracion exitosa
inicio iteracion
14
iteracion exitosa
inicio iteracion
inicio iteracion
16
iteracion exitosa
inicio iteracion
17
iteracion exitosa
inicio iteracion
18
iteracion exitosa
inicio iteracion
19
iteracion exitosa
FIN<jupyter_text>**Importante** notar que en los casos de 0, 5, 10, 15 NO se imprimio el mensaje *"iteracion exitosa"* incluso si este era independiente del **if**. Esto sucede porque al momento de leer la declaración **continue** el loop salta al siguiente número de una vez, sin considerar lo que hay después en esa misma iteración.  Ahora, suponga que queremos leer una secuencia de números desconocida en orden e imprimir su valor. Esto **siempre y cuando** no se haya leido ningun numero mayor a 0.5.<jupyter_code>import numpy as np

X = np.linspace(0,1,27) #generar 27 decimales entre 0 y 1

for num in X: # para cada numero del arreglo X
    print("inicio iteracion")
    if(num>0.5): #si el numero es mayor a 0.5
        break # detener el "for"
    else: # si no
        print(num) # imprimir el numero
    print("iteracion exitosa")

print("FIN")<jupyter_output>inicio iteracion
0.0
iteracion exitosa
inicio iteracion
0.038461538461538464
iteracion exitosa
inicio iteracion
0.07692307692307693
iteracion exitosa
inicio iteracion
0.11538461538461539
iteracion exitosa
inicio iteracion
0.15384615384615385
iteracion exitosa
inicio iteracion
0.19230769230769232
iteracion exitosa
inicio iteracion
0.23076923076923078
iteracion exitosa
inicio iteracion
0.2692307692307693
iteracion exitosa
inicio iteracion
0.3076923076923077
iteracion exitosa
inicio iteracion
0.34615384615384615
iteracion exitosa
inicio iteracion
0.38461538461538464
iteracion exitosa
inicio iteracion
0.42307692307692313
iteracion exitosa
inicio iteracion
0.46153846153846156
iteracion exitosa
inicio iteracion
0.5
iteracion exitosa
inicio iteracion
FIN<jupyter_text>Note que solo siguio el ciclo hasta 0.5, a pesar de que X tenia valores superiores y el **for** se especificó para todos los numeros en X. En el momento en el que el código lee el **break** detiene el loop y pasa de largo a cualquier código que haya después de este. Es por esto que sí lee "inicio iteracion" pero al momento de leer el **if** , y por ende el **break**, solo pasa al código que hay despues del **for**.**Importante** tener en cuenta que estas declaraciones tambien sirven para los loops **while**.  quit()El comando quit (más que eso, es una función sin argumentos) se usa para detener el código en determinado punto. Donde sea dentro del código, si se llega a leer la línea:````pythonquit()````Se detendrá la ejecución de todo el código, sin importar que proceso se esté llevando a cabo.**Importante** remarcar que se DEBE leer la linea para que el comando funcione. Si la línea se encuentra dentro de un **if** que no se cumple, no se ejecuta su función. Esta utilidad sirve, por ejemplo, para revisar el código y detectar problemas. Muchas veces se tiene un código que se demora mucho en *correr* y sólo se quieren revisar cambios hechos en la primera parte de este. Para ahorrar tiempo, en lugar de dejar que se ejecute TODO el código cada vez que se quiera ver un resultado, se puede usar  un **quit()** en la parte donde se quiere cortar el código y así no se tiene que compilar todo, solo la parte de interés.  split Se usa para generar particiones en cadenas de caracteres (o strings) a partir de un caracter (o cadena de caracteres) determinado. Para hacerlo más claro, acá hay un ejemplo:<jupyter_code>frase = "para duque es importante querer al queso que le di aquel dia en el buque azul"
frase.split(" ") #se separa la frase en cada "espacio" del texto
frase.split("que") #se separa la frase usando los caracteres "que" como separador
frase.split("e") #se usa la letra "e" como separador<jupyter_output><empty_output><jupyter_text>**Recuerde** que para separar caracter por caracter solo hace falta:<jupyter_code>frase  = "amo el queso"
list(frase)<jupyter_output><empty_output><jupyter_text>**Importante** notar que tanto list como split retornan listas y que estas listas pueden ser almacenadas en variables y recorridas como cualquier variable contenedora.   RoundHay una forma de *"redondear"* números fáclmente, sean decimales o enteros muy grandes, con el método **round()**.<jupyter_code>variable_float = 13/96
variable_int = 13**7
print(variable_float)
print(variable_int)
round(variable_float,4)  #redondear a 4 cifras decimales (diez-milécimas)
round(variable_int,-2) # redondear a 2 ordenes de 10 (centenas)
round(variable_float,1)  #redondear a 1 cifra decimal (décimas)
round(variable_int,-5)  #redondear a 5 ordenes de 10 (cientos de miles)<jupyter_output><empty_output><jupyter_text>Note como se usan argumentos positivos para las cifras decimales y negativos para los espacios enteros.  ImportCuando hablamos antes de librerías, vimos como importarlas al script con el comando import y como usar *Alias* para llamarlas en el código. Cada vez que se importa una librería (o módulo, como  se le llama en Python) al código, se almacena en caché del **kernel** el contenido de esta.Del módulo se pueden importar funciones, varables y clases en específico en caso de no querer usar todo el modulo, sin embargo los desarrolladores de python concuerdan en que, en cuanto a memoria, no importa si se importa una parte o todo el módulo, el kernel almacena en caché el módulo completo.<jupyter_code>import numpy    # importando todo el modulo numpy

numpy.sqrt(1E4)   # 1E4 = 1 x 10^4 = 10000<jupyter_output><empty_output><jupyter_text>**IMPORTANTE** notar la declaración de *numpy* sin alias, siendo llamado por el nombre de la libreria.**IMPORTANTE** saber que la notación "E" sirve para notación científica de potencias, reemplazando al operador [*x 10^*]<jupyter_code>import numpy as np

np.log10(10000)<jupyter_output><empty_output><jupyter_text>En ese último caso, se asignó el *Alias* habitual **np** a Numpy. Los *Alias* también se pueden asignar a elementos o sub-módulos de librerías importadas:<jupyter_code>from numpy import log2

log2(2048)<jupyter_output><empty_output><jupyter_text>Ahora, se importó en específico la funcíon del logaritmo en base 2 del módulo de Numpy. Si usted es un estudiante atento habrá notado que **no se necesito citar a numpy para llamar la función:** no se requirió un 'np.'  o un  'numpy.' antes de llamar a la función. Esto se debe a que, en este caso, quien fue importada fue la función y no el modulo. Así, si intento llamar otra función de numpy esta no va a estar disponible.<jupyter_code>from numpy import log2,log10,log # se pueden importar multiples elementos del módulo
from numpy import sinh as sh   # importo la funcion "seno hiperbólico" de numpy con el alias sh


sh(1)
from scipy.stats import pearsonr as pR
# de la libreria scipy, el sub-modulo stats, importar la funcion de "R de person" con el alias "pR"<jupyter_output>/home/david/anaconda3/lib/python3.6/importlib/_bootstrap.py:219: RuntimeWarning: numpy.dtype size changed, may indicate binary incompatibility. Expected 96, got 88
  return f(*args, **kwds)<jupyter_text>Manejo de Archivos Me atrevo a decir que la mayoría de las veces que un científico, ingeniero o incluso diseñador, hace uso de Python es para **procesar** algún tipo de dato: tablas con números, texto, imágenes, audio, señales e incluso retroalimentación en tiempo real de redes sociales. Estos datos usualmente son generados **fuera** del código de Python por algún agente externo y requieren ser **procesados o analizados**. Para poder introducir estos datos a un código de Python e incluso poder almacenar nuevos datos generados por un código es preciso aprender a **manejar archivos** desde el código de python mismo.Para comenzar, determinemos como abrir un archivo y qué uso se le dará despues de abierto.<jupyter_code>archivo_temperatura  = open("data/temp_SaoPaulo.txt","r")<jupyter_output><empty_output><jupyter_text>Veamos esta línea con detenimiento: con **open** declaramos que queremos abrir un archivo o ,más especificamente, que se quiere crear un vinculo entre el código y este archivo. como primer argumento entra el path o dirección del archivo en si mismo que en este caso es *"temp_SaoPaulo.txt"* (En breve hablaremos de los path). Pero ¿Qué significa este segundo argumento *"r"*? Con este argumento se determina el uso que se le dará al archivo:| **nombre** |**modo**|    **descripción**                | **exception**                   ||------------|-------|------------------------------------|---------------------------------|| read       |  "r"  | extraer información del archivo    |error si el archivo no existe    || write      |  "w"  | escribir información en el archivo |crea el archivo si no existe     || append     |  "a"  | agregar información al archivo     |crea el archivo si no existe     || create     |  "x"  | crear el archivo                   |error si el archivo ya existe    |A esto se le puede sumar una letra adicional: si se quiere trabajar con **texto solamente "t"** o si se quiere trabajar con **datos binarios "b"**. Esta última se usa para tratar infomración codificada que o es legible para el humano pero si para el computador así como  imágenes u objetos. Si se quisiera leer un archivo con codificación binaria se usaría el modo **"rb"**.**[GP]** Es buena costumbre nombrar a los arcivos de texto y scripts con nombres descriptivos para evitar confusiones. Nombres de archivos como "TTsp.txt" o "datos.txt" no sirven mucho al momento de estar buscando desesperadamente estos datos en un posible futuro. Es mejor poner nombres *medianamente* largos pero con suficiente información para reconocer el contenido del archivo sin tener que abrirlo.   dicho esto, se infiere que en la línea de código escrita se leyó la información almacenada en el archivo. ¿Y que sucede con esta información?¿en que forma se almacena y como se pede usar?<jupyter_code>archivo_temperatura<jupyter_output><empty_output><jupyter_text>Al llamar la variable nos retorna un objeto del tipo *io.TextIOWrapper*. Con este objeto yo no puedo formular operaciones ni mucho menos hacer calculos matemáticos. Para extraer información del TextIOWrapper es preciso aber algunos métodos. El primero es leer todo el archivo y así *"ojear"* un poco el contenido de este:<jupyter_code>archivo_temperatura.read()<jupyter_output><empty_output><jupyter_text>**Importante** recordar que si en un string aparecen los caracteres **"\n"** implican un salto de línea, es decir, pasar de una linea a la siguiente abajo en el archivo.   Si se observa con detenimiento se puede ver que el archivo se trata de 3 numero separados por espacios en cada linea, de tal forma que la primera línea sería | 1.0 | 34.6 | 237.4 | ,  la segunda linea sería| 2.0 | 35.8 | 221.5 | y así sucesivamente.Ahora, si le pido otra vez al archivo ser leido, ocurre lo siguiente:<jupyter_code>archivo_temperatura.read()<jupyter_output><empty_output><jupyter_text>Ya no hay nada.Esto no sucede porque el archivo haya quedado vacío o algo similar, sino porque el código usa un **cursor de lectura** para leer el archivo. Una vez leyó todo, el cursor queda al final del archivo y ya no queda más por leer. Una forma de volver a poner este cursor al principio del archivo es con **seek()**.<jupyter_code>archivo_temperatura.seek(0)
archivo_temperatura.read()<jupyter_output><empty_output><jupyter_text>Si bien se sabe diferenciar en el texto de lectura cuando hay un salto de línea, poder interpretar los datos a partir de esto es molesto ya que hay que generar un split para cada salto de linea. Otro metodo de lectura es almacenar una lista con el contenido de cada línea. Para ello:<jupyter_code>archivo_temperatura  = open("data/temp_SaoPaulo.txt","r") # se importan los datos

lista_lineas  = archivo_temperatura.readlines() 

lista_lineas<jupyter_output><empty_output><jupyter_text>Acá as lineas aún tienen el separador de linea anexo al final. El objeto *"TextIOWrapper"* puede compararse con un iterable también, y por ende se pueden recorrer sus lineas com si fueran los objetos contables del contenedor iterable. es decir:<jupyter_code>archivo_temperatura  = open("data/temp_SaoPaulo.txt","r") # importar los datos

for linea in archivo_temperatura: #para cada elemento del archivo
    print(linea)<jupyter_output>1.0 34.6 237.4

2.0 35.8 221.5

3.0 33.5 160.5

4.0 32.0 72.6

5.0 29.7 71.4

6.0 29.4 50.1

7.0 29.3 43.9

8.0 33.2 39.6

9.0 35.2 70.7

10.0 34.5 126.9

11.0 34.6 145.8

12.0 33.9 200.7<jupyter_text>Así, usando splits y cambios de tipo se pueden usar los datos del archivo de forma lógica y matemática:<jupyter_code>archivo_temperatura  = open("data/temp_SaoPaulo.txt","r") # importar los datos

for linea in archivo_temperatura: #para cada elemento del archivo
    valores  = linea.split(" ") # separar por cada espacio
    x = float(valores[0]) #tomar el primer valor como float
    
    print(x,2*x)  # imporimer el valor y el doble del valor<jupyter_output>1.0 2.0
2.0 4.0
3.0 6.0
4.0 8.0
5.0 10.0
6.0 12.0
7.0 14.0
8.0 16.0
9.0 18.0
10.0 20.0
11.0 22.0
12.0 24.0<jupyter_text>Tipos de datoPara ponerlo simple: si se puede poner en un archivo, entonces es un dato. Desde textos del Quijote hasta datos del telescopio hubble, si se puede plasmar en un archivo es un dato. Ahora, el tipo de dato y el uso que se le de es lo que varía: yo no puedo hacer análisis sintácticos sobre datos del hubble ni cromatografía sobre un tramo de texto del Quijote. Numpy ayuda Cuando se trata de valores numéricos organizados en una tabla, numpy ofrece una herramienta para reconocer facilmente valores numéricos y estrcturarlos como tal:<jupyter_code>archivo_temperatura = np.loadtxt("data/temp_SaoPaulo.txt",delimiter=" ")
archivo_temperatura<jupyter_output><empty_output><jupyter_text>La función **loadtxt()** de *Numpy* permite leer datos directamente de un archivo de texto y hacer la separación de valores basandose en el valor *"delimiter"* que se ingrese como parámetro. Esta función retorna un vector o matriz de Numpy con los valores solicitados **si todo esta en orden**.**Importante** tener en cuenta que hay varios motivos por los cuales **loadtxt()** podría fallar: Las columnas o filas son de diferente longitud, no todos los valores pueden ser interpretados como numéricos, el delimitador es errado, entre otros. Si este es el caso, conviene revisar el archivo para determinar la causa del error o simplemente usar un método manual para extraer los datos. **Importante** recordar que este método solo se debe usar sobre datos numéricos. NO puede usarse ara hacer tratamiento de variables de texto u objetos.  Absolute & Relative PathsLa traducción literal de path es *"camino"*, que en el caso de la localización de archivos algo tiene que ver. En computación un *path* es la forma de señalar la **dirección exacta** de localización de un archivo o una carpeta con un conjuto de carcteres. Si ha usado omputadores por un buen tiempo alguna vez se habrá cruzado con un path de la forma :````C:\Users\PaipaPsyche\Desktop\PythonFiles\temp_SaoPaulo.txt````Este, por ejemlo, es el **path absoluto** en mi computador para el archivo que estamos tratando. Es absoluto porque referencia la dirección al archivo **desde el origen en el disco mismo (C:)**.Si yo uso e path absolto para importar el archivo a python, este lo lee sin problemas:````pythonarchivo_temperatura = open("C:\Users\PaipaPsyche\Desktop\PythonFiles\temp_SaoPaulo.txt","r")````Al comienzo cuando se llamaba al archivo con el path *"temp_SaoPaulo.txt"* se estaba usando un **path relativo**. Es relativo  ya que tanto el script del código como el archivo están en la misma carpeta. El path relativo referencia al archivo **a partir de la dirección del script que se está ejecutando**. Si, por ejemplo, el archivo estuviera dentro de una carpeta adicional llamada *"data"* el path relativo sería:````pythonarchivo_temperatura = open("data\temp_SaoPaulo.txt","r")````Esto ya que el código tendria que primero visitar la carpeta adicional (que se encuentra en la misma carpeta que el script) y luego si abrir el archivo. En este caso hipotético, el path absoluto sería:````pythonarchivo_temperatura = open("C:\Users\PaipaPsyche\Desktop\PythonFiles\data\temp_SaoPaulo.txt","r")````El archivo esta dentro de la carpeta "Datos", en la carpeta "PythonFiles" están el script y la carpeta "data", en la carpeta "Desktop" está la carpeta "PythonFiles" y así sucesivamente.Si por el contrario el archivo NO se encuentra una carpeta adicional a la del script, sino una carpeta *mas arriba* que el script ( es decir en Desktop), el path relativo sería:````pythonarchivo_temperatura = open("..\temp_SaoPaulo.txt","r")````En la carpeta "PythonFiles" está el script , en la carpeta "Desktop" está la carpeta "PythonFiles" y el archivo, y así sucesivamente.La notación de los dos puntos **".."** denota que hay que moverse una carpeta hacia arriba o *"hacia atrás"*.  Cerrar el ArchivoCuando se manejan archivos hay que tener algo claro:**TODOS LOS ARCHIVOS QUE SE ABREN DEBEN CERRARSE !!**Es importante tener esto claro pues olvidar este último paso es el comienzo de muchos dolores de cabeza. Es importate siempre cerrar los vinculos del código con cualuier archivo que se tenga abierto con el método **open()** pues de lo contrario el kernel de Python se va a llenar de archivos abiertos o se puede sobreescribr información valiosa por accidente. Para cerrar el archivo basta con:<jupyter_code>archivo_temperatura  = open("data/temp_SaoPaulo.txt","r") # se importan los datos

lista_lineas  = archivo_temperatura.readlines() # se almacenan las lineas dentro del archivo

archivo_temperatura.close() # se cierra el archivo<jupyter_output><empty_output><jupyter_text>**Importante** saber que el método **close()** se usa sobre objetos que se abrieron desde python **directamente**. El método **loadtxt()** de *Numpy* abre el archivo , importa los datos y cierra el vinculo con el archivo (llamado *thread*) por sí mismo. Es por eso que en ese caso, entre otros, NO es necesario cerrar el archivo *manualmente*.   Escribir archivosEn Python se pueden generar datos para muchas situaciones y en muchos formatos. Muchas veces se requiere usar estos datos más de una vez  o incluso en otros compiladores o programas. Python también puede escribir archivos y no es muy diferente a como se leen. Pongo un ejemlo práctico para introducir el tema:  Serie ArmónicaLa serie armónica es aquella serie formada por la suma de los inversos multiplicativos de los numeros naturales a partir del 1. Se llama la serie armónica porque se le asocia con la longitud de onda y la frecuencia en vibraciones fundamentales de las cuerdas, que estan relacionadas con el inverso unitario de la longitud de la cuerda misma.$                   \sum_{k=1}^{\infty}\frac{1}{k}=1 + \frac{1}{2} + \frac{1}{3} + \frac{1}{4} + ...$ Suponga que queremos saber el valor de la serie hasta un valor N de tope. Es decir, el valor $S_{N}$ :$S_{N} = \sum_{k=1}^{N}\frac{1}{k}=1 + \frac{1}{2} + ... + \frac{1}{N-1} + \frac{1}{N}$Y aún más interesante, se quiere hacer una función que pueda guardar en un archivo de texto con dos columnas (separadas por espacio) el valor e N junto con el valor de $S_{N}$ para cada N entre 1 y un $N_{max}$ que entra por parámetro a la función.Es decir que si se proporciona $N_{max} = 3$ a la función, el archivo se deberia ver así:1  $N_{1}$2  $N_{2}$3  $N_{3}$ DesarrolloComenzamos definiendo la función para obtener el valor de $S_{N}$:<jupyter_code>def suma_armonica(N):
    total_suma = 0  # inicializamos el valor total de la suma en 0
    for  k in range(1,N+1): #esto recorre los numeros desde el 1 (incluyendolo) hasta N+1 (sin incluir N+1, solo hasta N)
        total_suma = total_suma + 1/k #para cada iteracion se suma 1/k
    return total_suma #retorna el valor total de la suma<jupyter_output><empty_output><jupyter_text>Si todo está en orden, es cierto que:$S_{1}=1$$S_{2}=1.5$$S_{3}=1.8333$...<jupyter_code>suma_armonica(1)
suma_armonica(2)
suma_armonica(3)<jupyter_output><empty_output><jupyter_text>Ya que todo está bien, estudiemos la lógica del archivo. También debe usarse un loop pero, más importante aún, en la función misma se debe abrir, escribir y cerrar el archivo.**Importante** recordar que:* Al abrir un archivo en modo escritura que **NO existe**, este se crea en la carpeta destino con el nombre especificado.* **TODO** archivo abierto debe cerrarse.* Los strings se pueden sumar entre si para formar strings más largos. Sin embargo, para adicionar un valor numérico al string primero debe ser cambado de tipo a string (por ahora, màs adelante veremos una forma màs eficiente de hacerlo).* La forma de simbolizar un salto de línea en un string es con los caracteres "**\n**".<jupyter_code>def archivo_suma_armonica (N_max):
    nombre_archivo  = "data/suma_armonica_"+str(N_max)+".txt"  #crea el nombre con Nmax introducido
    archivo_destino = open(nombre_archivo,"w") # vamos a escribir en el archivo 
    
    for N in range(1,N_max+1):  # Recorre N desde 1 hasta N_max
        
        S_N = suma_armonica(N) # se crea el numero S_N
        
        linea = str(N) + " "+str(S_N)+"\n" #linea = N + espacio + S_N + salto de linea
        print(linea)
        
        archivo_destino.write(linea)  # ESCRIBIR EN EL ARCHIVO
        
    archivo_destino.close() # se cierra el archivo
archivo_suma_armonica(10) # verificamos su funcionamiento ....<jupyter_output>1 1.0

2 1.5

3 1.8333333333333333

4 2.083333333333333

5 2.283333333333333

6 2.4499999999999997

7 2.5928571428571425

8 2.7178571428571425

9 2.8289682539682537

10 2.9289682539682538<jupyter_text>Y , verificando en los **archivos locales**, hay un archivo de texto **NUEVO** llamado *"suma_armonica_10.txt"* con las líneas mostradas arriba. Note que el comando usado para escribir en el archivo es **write()** y que esribe en el archivo **SIN** hacer por si mismo el cambio de línea. Es preciso poner el salto de línea de **forma manual** en este tipo de implementación.  INTRODUCCIÓN A GRÁFICAS Y VISUALIZACIÓNSin duda alguna mi parte favorita de Python es graficar. La versatilidad que ofrece Python para la visualización de datos se resume en **un amplio compedio de módulos** especializados en cada necesidad que uno pueda tener, desde la creación de graficas simples para una visualización exploratoria hasta poder generar informes completos para ciencia de datos. Como se trata de una **introducción**, voy a exponer el módulo básico de gráficas para python **por el momento**. Más adelante en el manual se expondrán más utilidades gráficas de este maravilloso lenguaje.  El módulo MATPLOTLIB **MATPLOTLIB** es de las librerías más famosas para Python, siendo la librería defecto para realizar elementos gráficos con versatilidad de personalización y de alta calidad. El módulo está compuesto de varios submódulos que sirven ya sea para hacer diferentes tipos de gráfico o , en su mayoría, para personalización de graficas e  integración a otras subrutinas de python (nada de que preocuparse, por ahora).El submódulo por defecto para hacer diferentes tipos de gráficos de manera sencila es **PYPLOT** y se importa al código usualmente con el siguiente *Alias* :<jupyter_code>import matplotlib.pyplot as plt<jupyter_output><empty_output><jupyter_text>PLOTPara **graficar curvas** el proceso es bastante sencillo, basta con un ejemplo para introducir el concepto:**La función seno :**<jupyter_code>x = np.linspace(0,2*np.pi,1000)  # genera 1000 puntos entre 0 y 2pi

y = np.sin(x) #se aplica la funcion seno a X para obtener Y (se puede porque X es un vector de numpy)

plt.figure() # se crea una figura
plt.plot(x,y) # se grafica Y vs. X 
plt.show() #se muestra la figura<jupyter_output><empty_output><jupyter_text>**Graficar la función seno nunca fue tan sencillo!**El procedimiento estándar es claro: se crea un figura, se hacen **lo gráficos** que se tengan que hacer y posteriormente se muestra la figura.Hice énfasis en **los gráficos** porque se puede tener más de un gráfico por marco:<jupyter_code>y1 = np.sin(x)
y3 = np.sin(3*x)
y5 = np.sin(5*x)


plt.figure()

plt.plot(x,y1)
plt.plot(x,y3)
plt.plot(x,y5)

plt.show()<jupyter_output><empty_output><jupyter_text>Solo basta con indicar las gráficas que se quieren hacer en el marco para tener multiples curvas por gráfico. Python incluso tiene la decencia de asignar un color diferente a cada curva para evitar confusiones.Si bien esto es una maravilla, también puede llevar a tener un **gráfico muy desordenado**. Nada más en este ejemplo ya se pueden generar confusiones porque las curvas **NO estan etiquetadas** y están muy juntas una sobre otra. Afortunadamente python permite **personalizar** las gráficas con una versatilidad intachable.Entre los elementos personalizables de la gráfica están:* Las dimensiones del marco (Parámetro *figsize* de **Figure**)* El color de la curva  (parámetro *color* o parámetro *c* de **Plot**)* El estilo de línea de la curva (parámetro *linestyle* de **Plot**)* La Leyenda del gráfico (parámetro *label* de **Plot** + elemento *legend*)* Las etiquetas de los ejes X y Y del gráfico (Elementos *xlabel/ylabel*)* Los límites de los ejes X y Y del gráfico (Elementos *xlim/ylim*)* El titulo del gráfico (Elemento *title*)Para mostrar un poco de todos, veamos **unos cuantos ejemplos más**:<jupyter_code>x = np.linspace(0,8*np.pi,2000) # 2000 puntos de 0 a 8pi

y1 = np.sin(3*x)+np.cos(x)
y2 = np.sin(x)+np.cos(2*x)
y3 = np.sin(2*x)+np.cos(3*x)
plt.figure( figsize = (12,4) ) # define dimensiones del grafico (ancho, alto)
plt.plot(x , y1 , color="red", linestyle = ":") #color rojo, linea punteada
plt.plot(x , y2 , color="blue", linestyle = "--") #color rojo, linea cortada<jupyter_output><empty_output><jupyter_text>**IMPORTANTE** notar que no hubo que pedirle al código que mostrara la gráfica. Esto es porque el manual se desarrolla en un notebook de Jupyter y a este le basta la declaración de **plt.plot()** para hacer un gráfico. En el código de un script (archivo de extensión .py) si es necesario escribir el código necesario para mostrar **o guardar** el gráfico.<jupyter_code>plt.plot(x,y3) # en el notebook solo basta con la declaracion plot para hacer graficas rapidas<jupyter_output><empty_output><jupyter_text>Continuando con los ejemplos:<jupyter_code>plt.figure( figsize = (10,4) ) # define dimensiones del grafico (ancho, alto)

plt.plot(x , y1 , color="red", linestyle = ":",label = "curva 1") #color rojo, linea punteada
plt.plot(x , y2 , color="blue", linestyle = "--",label = "curva 2") #color rojo, linea cortada
plt.plot(x , y3 , color="green",label = "curva 3") #color verde, linea

plt.legend() # elemento leyenda para mostrar los labels de las curvas
plt.xlim(0,10) #limites en X de 0 a 10
plt.ylim(-1,1) #limites en Y de -1 a 1

plt.title("tres funciones") # titulo del grafico

plt.show()


plt.figure( figsize = (10,4) ) # define dimensiones del grafico (ancho, alto)


plt.plot(x , y1+y2+y3 , color="green",label = "<NAME>") #color verde, linea

plt.legend() # elemento leyenda para mostrar los labels de las curvas
plt.xlim(0,15) #limites en X de 0 a 15
plt.ylim(-5,5) #limites en Y de -5 a 5

plt.xlabel("eje X")
plt.ylabel("eje Y") # usando etiqueteas en los ejes X y Y

plt.title("suma de las funciones" , fontsize=15) #titulo. El tamaño de la fuente se puede cambiar

plt.show()<jupyter_output><empty_output><jupyter_text>SCATTERLas *scatter plots* son simplemente **gráficas de putos coordenada a coordenada** y se grafcian de forma similar a las curvas que ya vimos:<jupyter_code>x = np.linspace(0,100,25) #25  puntos generados en el intervalo de 0 a 100

y = np.sqrt(x) # definir Y como la riz cuadrada de X

plt.figure( figsize=(6,6) ) # definir dimensiones del gráfico

plt.scatter(x,y) # scatter es el comando para hacer la grafica de puntos<jupyter_output><empty_output><jupyter_text>Entre los elementos personalizables de la gráfica están:* El color de los puntos  (parámetro *color* o parámetro *c* de **Scatter**)* El tamaño de los puntos  (parámetro *size* o parámetro *s* de **Scatter**)* La transparencia u opacidad de los puntos  (parámetro *alpha* de **Scatter**)* El estilo del punto o marcador (parámetro *marker* de **Scatter**)* La Leyenda del gráfico (parámetro *label* de **Scatter** + elemento *legend*)**IMPORTANTE** mencionar que se pueden hacer diferentes tipos de graficos en un solo marco:<jupyter_code>x1 = np.linspace(0,2*np.pi,15) # 15 puntos de 0 a 2pi
y1 = np.sin(x1)+np.cos(2*x1)  

x2 = np.linspace(0,2*np.pi,50) # 50 puntos de 0 a 2pi
y2 = np.sin(2*x2)*np.cos(x2)



plt.figure(figsize=(12,4))

#para x1
plt.plot(x1,y1,c="blue") # curva azul
plt.scatter(x1,y1,c="red",s=150,marker="^") #puntos triangulares rojos de tamaño 150
#para x2
plt.plot(x2,y2,c="orange")
plt.scatter(x2,y2,c="green",s=50, marker = "x") #puntos en forma de "X"  verdes de tamaño 50

x = np.random.normal(scale=1,loc=0,size=500)
y = np.random.normal(scale=1,loc=0,size=500)

#tanto X como Y son arreglos con 500 puntos aleatorios que siguen una distribución normal
# al rededor de 0 con una desviación estandar de 1

plt.figure(figsize=(10,10))
plt.scatter(x,y,s=200,alpha=0.6,c = "orange") 
#puntos redondos (por defecto) naranjas de tamaño 200 y transparencia de 0.6


plt.scatter(y,x,s=50,alpha=0.1,c = "blue") # invierte las coordenadas X y Y
#puntos redondos (por defecto) azules de tamaño 50 y transparencia de 0.1<jupyter_output><empty_output><jupyter_text>HISTOGRAMAPara hacer un histograma se necesita **un único array de datos**, puesto que solo se quiere un gráfico de frecuencia por intervalos. Recordemos que un histograma muestra en qué abundancias se encuentran los datos en intervalos definidos, dando así información sobre su **distribución**.Un ejemplo visual para un concepto que hemos venido trabajando: **la distribución normal**. En varias ocasiones se ha mencionado que *np.random.normal* genera numeros aleatorios que siguen una distribución normal, pero nunca se ha demostrado que así es. El gráfico de *scatter* anterior es una demostración válida para aquellos atentos que notaron el **patrón de concentración al rededor de 0** en cada eje, pero mas alla de eso no se ha hecho mucho por dar validez a esta afirmación.Una forma válida de visualizar dichos resultados es con un **histograma**. Si es cierto que estos puntos aleatrios siguen dicha distribución, un grafico de frecuencia deberia mostrar una tendencia centrada en donde dice tener su centro y un ancho congruente con la desviación que presuntamente se tiene.<jupyter_code>centro = 10
desviacion = 1



x = np.random.normal(loc=centro , scale=desviacion, size=5000 ) 
#5000 puntos con distribucion normal centrada en 10 y con desviación estandar de 1 


#recordar que la distribución normal viene dada por:

def dist_normal(x,sigma,mu):
    coeficiente = 1/(sigma*np.sqrt(2*np.pi))
    #el coeficiente se implementa con el objetivo de hacer el area bajo la curva de la distribucion
    # iguala a  uno, cumpliendo asi el propoito de la distribución normal
    exponencial = np.exp(-((x-mu)**2)/(2*sigma**2))
    return coeficiente*exponencial


# se generan 2000 puntos para graficar entre 5 y 15
x_plot = np.linspace(5,15,2000)
# se aplica la funcion de distribucion normal al arreglo x_plot 
y_plot = dist_normal(x_plot,desviacion,centro)


plt.figure(figsize=(12,4))

plt.plot(x_plot,y_plot,c="red",linestyle="--") #se grafica la distribucion normal teorica
h = plt.hist(x,bins=25,density=True) # se grafica el histograma de los 5000 puntos generados
#el parametro bins determina el numero de bins,barras o 'intervalos'
#el parametro density en True determina que el area total abarcada por el historigrama sera de 1<jupyter_output><empty_output><jupyter_text>Así, queda demostrado que  los números aleatorios generados cumplen con la distribución normal. Entre los elementos personalizables de la gráfica están:* El color de las barras  (parámetro *color*  de **Hist**)* El numero de intervalos o "barras"  (parámetro *bins* de **Hist**)* El color de los bordes de las barras  (parámetro *edgecolor* o parámetro *ec* de **Hist**)* Si se trata de un histograma de densdad o frecuencia (parámetro *density* de **Hist**)**IMPORTANTE** mencionar que el número de bins se selecciona **por defecto** en el minimo **necesario** para no tener intervalos vacíos. También cabe recalcar que un histograma de densidad quiere **estimar una función de probabilidad** dada la distribución de los datos mientras que un histograma de frecuencia (parámetro *density* = False) solo **cuenta el numero de datos** por intervalo. **IMPORTANTE** notar que el histograma, especificamente el método *np.hist*, retorna algo! En este caso guardamos aquello que retorna en una variable llamada h, la cual usted va a tener que explorar por si msimo.  GUARDAR GRAFICOS*Matplotlib* permite guardar el gráfico generado con el metodo *savefig*:<jupyter_code>x = np.linspace(0,5,1000)
y = np.exp(-x)*np.sin(10*x) # funcion seno con etenuamiento exponencial


plt.figure(figsize=(10,10) )

plt.plot(x,y,color = "orange") # graficar linea

plt.xlabel("eje X")
plt.ylabel("eje Y")
#etiquetar ejes

plt.savefig("imgs/plot_prueba.png")
#salvar figura en el directorio imgs con el nombre "plot_prueba.png"<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.07952702349186418, 'fraction_numerical': 0.026212525506199968, 'mean_word_length': 4.869338246583755, 'pattern_counts': {'":': 2, '<': 141, '<?xml version=': 0, '>': 142, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 4, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '37', 'dir': 'jupyter-structured-clean-dedup', 'id': '559258', 'license': 'MIT', 'n_tokens_mistral': 13600, 'n_tokens_neox': 12968, 'n_words': 5211, 'path': 'Chapter_1/clase_3_plots.ipynb', 'repo_name': 'PaipaPsyche/LearningPython', 'size': '38232'}